class_name ABGEntryShell extends Control const BootService = preload("res://entry/boot_service.gd") const SettingsStore = preload("res://entry/settings_store.gd") const EntryChrome = preload("res://entry/entry_chrome.gd") const GUILD_BACKGROUND = preload("res://assets/backgrounds/p9_3_guild_hall.png") const UI_FONT = preload("res://assets/fonts/NotoSansSC.ttf") signal boot_completed @export var auto_start := true var boot_service := BootService.new() var settings_store := SettingsStore.new() var settings: Dictionary = {} var setup_draft: Dictionary = {} var boot_snapshot: Dictionary = {} var boot_generation := 0 var status_label: Label var detail_label: Label var progress_bar: ProgressBar var retry_button: Button var diagnostics_label: Label var current_screen := "boot" var title_notice := "" var settings_draft: Dictionary = {} var settings_category := "display" var settings_notice := "" func _ready() -> void: set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _build_boot_screen() if auto_start: call_deferred("start_boot") func start_boot() -> void: boot_generation += 1 var generation := boot_generation boot_snapshot = boot_service.begin() _render_boot_state() while generation == boot_generation and String(boot_snapshot.state) == "running": await get_tree().process_frame boot_snapshot = boot_service.verify_next() _render_boot_state() if generation == boot_generation and String(boot_snapshot.state) == "complete": await get_tree().create_timer(0.18).timeout if generation == boot_generation: boot_completed.emit() _open_after_boot() func retry_boot() -> void: if String(boot_snapshot.get("state", "")) != "error": return start_boot() func _build_boot_screen() -> void: for child in get_children(): child.free() var background := TextureRect.new() background.name = "EntryBackground" background.texture = GUILD_BACKGROUND background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE background.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) background.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(background) var shade := ColorRect.new() shade.name = "EntryShade" shade.color = Color(0.035, 0.07, 0.10, 0.40) shade.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) shade.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(shade) var safe := MarginContainer.new() safe.name = "EntrySafeArea" safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) safe.add_theme_constant_override("margin_left", 32 if size.y < 500.0 else 48) safe.add_theme_constant_override("margin_right", 32 if size.y < 500.0 else 48) safe.add_theme_constant_override("margin_top", 24 if size.y < 500.0 else 40) safe.add_theme_constant_override("margin_bottom", 24 if size.y < 500.0 else 40) add_child(safe) safe.add_child(_entry_chrome("BootPlate", "boot_plate")) var layout := VBoxContainer.new() layout.name = "BootLayout" layout.alignment = BoxContainer.ALIGNMENT_END layout.add_theme_constant_override("separation", 6) safe.add_child(layout) var overline := _label(_tr("本地入口检查", "LOCAL ENTRY CHECK"), 12, Color("#e8d39d")) overline.name = "BootOverline" layout.add_child(overline) var identity := Label.new() identity.name = "BootIdentity" identity.text = "AETHERBOUND GUILD" identity.add_theme_font_override("font", UI_FONT) identity.add_theme_font_size_override("font_size", 28 if size.y < 500.0 else 34) identity.add_theme_color_override("font_color", Color("#f5ead0")) identity.add_theme_color_override("font_shadow_color", Color(0.05, 0.08, 0.10, 0.8)) identity.add_theme_constant_override("shadow_offset_x", 2) identity.add_theme_constant_override("shadow_offset_y", 2) layout.add_child(identity) status_label = Label.new() status_label.name = "BootStatus" status_label.add_theme_font_override("font", UI_FONT) status_label.add_theme_font_size_override("font_size", 18 if size.y < 500.0 else 20) status_label.add_theme_color_override("font_color", Color("#f5ead0")) layout.add_child(status_label) detail_label = Label.new() detail_label.name = "BootDetail" detail_label.add_theme_font_override("font", UI_FONT) detail_label.add_theme_font_size_override("font_size", 14 if size.y < 500.0 else 16) detail_label.add_theme_color_override("font_color", Color("#aebbc2")) detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART layout.add_child(detail_label) progress_bar = ProgressBar.new() progress_bar.name = "BootProgress" progress_bar.custom_minimum_size = Vector2(420, 12) progress_bar.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN progress_bar.show_percentage = false progress_bar.add_theme_stylebox_override("background", _flat_style(Color("#0d151d"), Color("#866f47"), 1, 4)) progress_bar.add_theme_stylebox_override("fill", _flat_style(Color("#72b9e8"), Color("#ead49b"), 1, 4)) layout.add_child(progress_bar) retry_button = _action_button("BootRetry", _tr("重试检查", "Retry verification"), retry_boot) retry_button.custom_minimum_size = Vector2(240, 52) retry_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN layout.add_child(retry_button) diagnostics_label = Label.new() diagnostics_label.name = "BootDiagnostics" diagnostics_label.add_theme_font_override("font", UI_FONT) diagnostics_label.add_theme_font_size_override("font_size", 14) diagnostics_label.add_theme_color_override("font_color", Color("#ffd4d4")) layout.add_child(diagnostics_label) func _render_boot_state() -> void: if status_label == null: return var overline := get_node_or_null("EntrySafeArea/BootLayout/BootOverline") as Label if overline != null: overline.text = _tr("本地入口检查", "LOCAL ENTRY CHECK") var state := String(boot_snapshot.get("state", "running")) var step := String(boot_snapshot.get("step", "resources")) progress_bar.max_value = max(1, int(boot_snapshot.get("total", 1))) progress_bar.value = int(boot_snapshot.get("completed", 0)) retry_button.visible = state == "error" diagnostics_label.visible = state == "error" match state: "error": status_label.text = _tr("本地检查需要处理", "Verification needs attention") detail_label.text = _tr("本地数据保持不变。解决问题后重试本检查。", "Local data was left unchanged. Resolve the issue, then retry this check.") diagnostics_label.text = "%s: %s" % [_tr("诊断", "Diagnostic"), String(boot_snapshot.get("code", "UNKNOWN"))] "complete": status_label.text = _tr("本地检查完成", "Local verification complete") detail_label.text = _tr("资源、设置存储与存档访问均已就绪。", "Resources, settings storage and save access are ready.") diagnostics_label.text = "" _: status_label.text = _tr("正在准备公会门槛", "Preparing the guild threshold") detail_label.text = _step_text(step) diagnostics_label.text = "" func _step_text(step: String) -> String: match step: "resources": return _tr("正在检查必需的本地资源...", "Checking required local resources...") "storage": return _tr("正在检查本地存储访问...", "Checking local storage access...") "preferences": return _tr("正在检查显示与辅助设置...", "Checking display and accessibility settings...") "save": return _tr("正在检查最近的本地远征记录...", "Checking the latest local run record...") return _tr("正在完成本地检查...", "Completing local verification...") func _open_after_boot() -> void: var loaded: Dictionary = settings_store.load_settings() if not loaded.ok: boot_snapshot = {"state": "error", "step": "preferences", "code": String(loaded.code), "completed": 0, "total": 1} _build_boot_screen() _render_boot_state() return settings = loaded.settings.duplicate(true) if bool(settings.setup_completed): show_title() else: show_first_launch() func show_first_launch() -> void: setup_draft = settings.duplicate(true) if not settings.is_empty() else settings_store.defaults() _build_world_base() var safe := MarginContainer.new() safe.name = "FirstLaunchSafeArea" safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) var margin := int(setup_draft.safe_area) + 24 safe.add_theme_constant_override("margin_left", margin) safe.add_theme_constant_override("margin_right", margin) safe.add_theme_constant_override("margin_top", mini(24, margin)) safe.add_theme_constant_override("margin_bottom", mini(24, margin)) add_child(safe) safe.add_child(_entry_chrome("FirstLaunchChrome", "setup_split")) var row := HBoxContainer.new() row.name = "FirstLaunchLayout" row.add_theme_constant_override("separation", 16) safe.add_child(row) var preview := VBoxContainer.new() preview.name = "PaintedPreview" preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL preview.clip_contents = true preview.alignment = BoxContainer.ALIGNMENT_END row.add_child(preview) preview.add_child(_label(_tr("公会门槛", "GUILD THRESHOLD"), 15, Color("#e8d39d"))) var preview_title := _label(_tr("裂脉远征团", "AETHERBOUND GUILD"), _scaled(30), Color("#f5ead0")) preview_title.name = "SetupPreviewTitle" preview_title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART preview.add_child(preview_title) preview.add_child(_label(_tr("字体、边距与字幕会在这里即时预览。", "Text, safe margins and captions preview here."), _scaled(14), Color("#f5ead0"))) var controls := VBoxContainer.new() controls.name = "FirstLaunchControls" controls.custom_minimum_size = Vector2(minf(390.0, size.x * 0.46), 0) controls.add_theme_constant_override("separation", 5) row.add_child(controls) controls.add_child(_label(_tr("首次启动设置", "FIRST-LAUNCH SETUP"), _scaled(22), Color("#f5ead0"))) controls.add_child(_label(_tr("这些选项只影响显示与辅助功能。", "These choices affect presentation and accessibility only."), _scaled(13), Color("#aebbc2"))) var grid := GridContainer.new() grid.name = "SetupGrid" grid.columns = 2 grid.add_theme_constant_override("h_separation", 8) grid.add_theme_constant_override("v_separation", 5) controls.add_child(grid) grid.add_child(_choice_button("SetupLanguage", _tr("语言", "Language"), _tr("简体中文", "English"), toggle_setup_locale)) grid.add_child(_choice_button("SetupSafeArea", _tr("安全区", "Safe area"), "%d px" % int(setup_draft.safe_area), cycle_setup_safe_area)) grid.add_child(_choice_button("SetupTextScale", _tr("字体", "Text"), "%d%%" % int(round(float(setup_draft.text_scale) * 100.0)), cycle_setup_text_scale)) grid.add_child(_check_button("SetupReducedMotion", _tr("减少动画", "Reduced motion"), bool(setup_draft.reduced_motion), set_setup_reduced_motion)) grid.add_child(_check_button("SetupReducedFlashes", _tr("减少闪烁", "Reduced flashes"), bool(setup_draft.reduced_flashes), set_setup_reduced_flashes)) grid.add_child(_check_button("SetupCaptions", _tr("字幕与事件标签", "Captions"), bool(setup_draft.captions), set_setup_captions)) var apply := _action_button("SetupApply", _tr("应用并进入标题首页", "Apply and continue to Title"), apply_first_launch) apply.custom_minimum_size = Vector2(0, 52) controls.add_child(apply) var error := _label("", _scaled(13), Color("#ffd4d4")) error.name = "SetupError" controls.add_child(error) func toggle_setup_locale() -> void: setup_draft.locale = "en" if String(setup_draft.locale) == "zh_CN" else "zh_CN" settings = setup_draft.duplicate(true) show_first_launch() func cycle_setup_safe_area() -> void: var options := SettingsStore.SAFE_AREA_PRESETS var next := (options.find(int(setup_draft.safe_area)) + 1) % options.size() setup_draft.safe_area = options[next] settings = setup_draft.duplicate(true) show_first_launch() func cycle_setup_text_scale() -> void: var options := SettingsStore.TEXT_SCALES var current := 0 for index in range(options.size()): if is_equal_approx(float(setup_draft.text_scale), float(options[index])): current = index setup_draft.text_scale = options[(current + 1) % options.size()] settings = setup_draft.duplicate(true) show_first_launch() func set_setup_reduced_motion(value: bool) -> void: setup_draft.reduced_motion = value func set_setup_reduced_flashes(value: bool) -> void: setup_draft.reduced_flashes = value func set_setup_captions(value: bool) -> void: setup_draft.captions = value func apply_first_launch() -> void: setup_draft.setup_completed = true var saved: Dictionary = settings_store.save_settings(setup_draft) if not saved.ok: var error := get_node_or_null("FirstLaunchSafeArea/FirstLaunchLayout/FirstLaunchControls/SetupError") if error != null: error.text = _tr("无法保存设置。请重试。", "Settings could not be saved. Retry.") + " [%s]" % String(saved.code) return settings = saved.settings.duplicate(true) show_title() func show_title() -> void: current_screen = "title" _build_world_base() var safe := MarginContainer.new() safe.name = "TitleSafeArea" safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) var margin := maxi(12, int(settings.get("safe_area", 24))) safe.add_theme_constant_override("margin_left", margin) safe.add_theme_constant_override("margin_right", margin) safe.add_theme_constant_override("margin_top", margin) safe.add_theme_constant_override("margin_bottom", margin) add_child(safe) var layout := Control.new() layout.name = "TitleLayout" safe.add_child(layout) var available := Vector2(maxf(1.0, size.x - margin * 2.0), maxf(1.0, size.y - margin * 2.0)) var short_layout := size.y < 500.0 var header_height := 82.0 if short_layout else 116.0 var tools_width := 252.0 if short_layout else 292.0 var header_width := minf(720.0, maxf(330.0, available.x - tools_width - 12.0)) var header := Control.new() header.name = "TitleHeader" header.position = Vector2.ZERO header.size = Vector2(header_width, header_height) header.add_child(_entry_chrome("TitleHeaderChrome", "title_header")) layout.add_child(header) var crest := Control.new() crest.name = "TitleGuildBanner" crest.position = Vector2(12.0, 0.0) crest.size = Vector2(66.0 if short_layout else 82.0, header_height + (8.0 if short_layout else 14.0)) crest.add_child(_entry_chrome("TitleGuildBannerChrome", "crest_banner")) header.add_child(crest) var header_left := crest.position.x + crest.size.x + (14.0 if short_layout else 20.0) var overline := _label(_tr("风路公会 // 远征入口", "WIND-ROAD GUILD // EXPEDITION THRESHOLD"), _scaled(11 if short_layout else 13), Color("#e8d39d")) overline.name = "TitleOverline" overline.position = Vector2(header_left, 13.0 if short_layout else 20.0) overline.size = Vector2(maxf(40.0, header_width - header_left - 20.0), 20.0 if short_layout else 24.0) header.add_child(overline) var identity := _label(_tr("裂脉远征团", "AETHERBOUND GUILD"), _scaled(24 if short_layout else 34), Color("#f5ead0")) identity.name = "TitleIdentity" identity.position = Vector2(header_left, 31.0 if short_layout else 43.0) identity.size = Vector2(maxf(40.0, header_width - header_left - 20.0), 40.0 if short_layout else 52.0) identity.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS header.add_child(identity) var chapter := _label(_tr("原创横屏幻想远征", "THE FIRST CHRONICLE"), _scaled(10 if short_layout else 12), Color("#8fc7e8")) chapter.name = "TitleChronicle" chapter.position = Vector2(header_left, 61.0 if short_layout else 87.0) chapter.size = Vector2(maxf(40.0, header_width - header_left - 20.0), 18.0) header.add_child(chapter) var tools := HBoxContainer.new() tools.name = "TitleTools" tools.alignment = BoxContainer.ALIGNMENT_END tools.position = Vector2(maxf(0.0, available.x - tools_width), 0.0) tools.size = Vector2(tools_width, 48.0 if short_layout else 54.0) tools.add_theme_constant_override("separation", 4) var settings_button := _title_button("TitleSettings", _tr("设置", "Settings"), show_settings_placeholder, "settings", "utility") settings_button.custom_minimum_size = Vector2((tools_width - 4.0) * 0.5, tools.size.y) tools.add_child(settings_button) var credits_button := _title_button("TitleCredits", _tr("制作人员", "Credits"), show_credits_placeholder, "credits", "utility") credits_button.custom_minimum_size = Vector2((tools_width - 4.0) * 0.5, tools.size.y) tools.add_child(credits_button) layout.add_child(tools) var dock_height := 128.0 if short_layout else 156.0 var dock := Control.new() dock.name = "TitleCommandDock" dock.position = Vector2(0.0, maxf(header_height + 10.0, available.y - dock_height)) dock.size = Vector2(available.x, dock_height) dock.add_child(_entry_chrome("TitleCommandDockChrome", "command_dock")) layout.add_child(dock) var copy := Control.new() copy.name = "TitleCopy" copy.position = dock.position copy.size = dock.size layout.add_child(copy) var ledger := _label(_tr("远征记录", "EXPEDITION LEDGER"), _scaled(10 if short_layout else 12), Color("#e8d39d")) ledger.name = "TitleLedgerHeading" ledger.position = Vector2(22.0, 20.0 if short_layout else 26.0) ledger.size = Vector2(maxf(120.0, available.x * 0.35), 20.0) copy.add_child(ledger) var save_status := _save_status() var summary := _label( _tr("发现可恢复的本地远征记录", "A local expedition record is available") if save_status == "valid" else _tr("尚无远征记录 · 从新游戏开始", "No expedition record · begin with New Game"), _scaled(14 if short_layout else 16), Color("#f5ead0") ) summary.name = "TitleSaveSummary" summary.position = Vector2(22.0, 42.0 if short_layout else 52.0) summary.size = Vector2(maxf(120.0, available.x - (342.0 if short_layout else 470.0)), 28.0) summary.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS copy.add_child(summary) var actions := HBoxContainer.new() actions.name = "TitleActions" var action_width := minf(442.0, maxf(304.0, available.x * (0.48 if short_layout else 0.40))) actions.position = Vector2(available.x - action_width - 20.0, 42.0 if short_layout else 51.0) actions.size = Vector2(action_width, 60.0 if short_layout else 70.0) actions.add_theme_constant_override("separation", 8) copy.add_child(actions) var new_game := _title_button("TitleNewGame", _tr("新游戏", "New Game"), open_new_game_boundary, "new_game", "action_primary") new_game.custom_minimum_size = Vector2((action_width - 8.0) * 0.5, actions.size.y) actions.add_child(new_game) var continue_button := _title_button("TitleContinue", _tr("继续游戏", "Continue"), open_continue_boundary, "continue", "action_secondary") continue_button.custom_minimum_size = Vector2((action_width - 8.0) * 0.5, actions.size.y) continue_button.disabled = save_status != "valid" continue_button.tooltip_text = _tr("没有可继续的本地远征记录", "No local expedition is available") if continue_button.disabled else _tr("查看可恢复的远征记录", "Review the resumable expedition") _set_title_button_disabled(continue_button, continue_button.disabled) actions.add_child(continue_button) new_game.call_deferred("grab_focus") var reason := _label( _tr("继续游戏:没有可恢复的本地远征记录", "Continue: no resumable local expedition") if continue_button.disabled else _tr("继续游戏会先进入恢复确认,不会立即推进模拟。", "Continue opens resume review before simulation advances."), _scaled(11 if short_layout else 13), Color("#aebbc2") ) reason.name = "TitleContinueReason" reason.position = Vector2(22.0, 72.0 if short_layout else 84.0) reason.size = Vector2(maxf(120.0, available.x - action_width - 64.0), 42.0 if short_layout else 50.0) reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART copy.add_child(reason) var notice := _label(title_notice, _scaled(11 if short_layout else 13), Color("#f0ca75")) notice.name = "TitleNotice" notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART notice.position = reason.position notice.size = reason.size reason.visible = title_notice.is_empty() copy.add_child(notice) func open_new_game_boundary() -> void: title_notice = _tr("新游戏入口已就绪;四选二创建流程属于冻结的 DEV-2,当前不会创建或覆盖存档。", "New Game is ready; the choose-two creation flow is frozen for DEV-2, so no save is created or replaced here.") show_title() func open_continue_boundary() -> void: if _save_status() != "valid": title_notice = _tr("没有可恢复的本地远征记录。", "No resumable local expedition is available.") else: title_notice = _tr("本地远征记录保持不变;恢复确认属于后续存档页面。", "The local expedition remains unchanged; Resume Review belongs to the later save pages.") show_title() func show_settings_placeholder() -> void: show_settings() func show_credits_placeholder() -> void: show_credits() func show_settings(category := "") -> void: current_screen = "settings" if settings_draft.is_empty() or not category.is_empty(): settings_draft = settings.duplicate(true) if not category.is_empty(): settings_category = category _build_world_base() var safe := MarginContainer.new() safe.name = "SettingsSafeArea" safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) var margin := int(settings_draft.get("safe_area", 24)) + 20 safe.add_theme_constant_override("margin_left", margin) safe.add_theme_constant_override("margin_right", margin) var short_layout := size.y < 500.0 safe.add_theme_constant_override("margin_top", 12 if short_layout else 24) safe.add_theme_constant_override("margin_bottom", 12 if short_layout else 24) add_child(safe) safe.add_child(_entry_chrome("SettingsFrame", "settings_frame")) var outer := VBoxContainer.new() outer.name = "SettingsOuter" outer.add_theme_constant_override("separation", 8) safe.add_child(outer) var heading := HBoxContainer.new() heading.name = "SettingsHeading" outer.add_child(heading) var title := _label(_tr("设置", "SETTINGS"), _scaled(24 if short_layout else 26), Color("#f5ead0")) title.size_flags_horizontal = Control.SIZE_EXPAND_FILL heading.add_child(title) heading.add_child(_label(_tr("来源:标题首页", "Caller: Title"), _scaled(12), Color("#e8d39d"))) var workspace := HBoxContainer.new() workspace.name = "SettingsWorkspace" workspace.size_flags_vertical = Control.SIZE_EXPAND_FILL workspace.add_theme_constant_override("separation", 14) outer.add_child(workspace) var rail: Container if short_layout: rail = GridContainer.new() rail.columns = 3 rail.name = "SettingsCategories" rail.add_theme_constant_override("h_separation", 5) rail.add_theme_constant_override("v_separation", 4) outer.move_child(workspace, outer.get_child_count() - 1) outer.add_child(rail) outer.move_child(rail, workspace.get_index()) else: rail = VBoxContainer.new() rail.name = "SettingsCategories" rail.custom_minimum_size = Vector2(174, 0) rail.add_theme_constant_override("separation", 4) workspace.add_child(rail) for row in [ ["display", _tr("显示与文字", "Display & Text")], ["motion", _tr("动态与省电", "Motion & Power")], ["audio", _tr("音频与字幕", "Audio & Captions")], ["controls", _tr("控制", "Controls")], ["language", _tr("语言", "Language")], ["credits", _tr("制作人员与数据", "Credits & Data")], ]: var button := _tool_button("SettingsCategory_%s" % row[0], row[1], open_settings_category.bind(row[0])) button.custom_minimum_size = Vector2(0 if short_layout else 174, 38 if short_layout else 44) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL var selected: bool = settings_category == String(row[0]) button.set_meta("selected_entry_tab", selected) _set_entry_button_disabled(button, selected, selected) rail.add_child(button) var scroll := ScrollContainer.new() scroll.name = "SettingsScroll" scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL workspace.add_child(scroll) var content := VBoxContainer.new() content.name = "SettingsContent" content.size_flags_horizontal = Control.SIZE_EXPAND_FILL content.add_theme_constant_override("separation", 6) scroll.add_child(content) _build_settings_category(content) var footer := HBoxContainer.new() footer.name = "SettingsFooter" footer.add_theme_constant_override("separation", 8) outer.add_child(footer) var notice := _label(settings_notice, _scaled(12 if short_layout else 13), Color("#f0ca75")) notice.name = "SettingsNotice" notice.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(notice) var back := _tool_button("SettingsBack", _tr("返回", "Back"), request_settings_back) back.custom_minimum_size = Vector2(110, 44 if short_layout else 48) footer.add_child(back) var discard := _tool_button("SettingsDiscard", _tr("放弃并返回", "Discard & Back"), discard_settings) discard.custom_minimum_size = Vector2(160, 44 if short_layout else 48) footer.add_child(discard) var apply := _action_button("SettingsApply", _tr("应用", "Apply"), apply_settings) apply.custom_minimum_size = Vector2(118 if short_layout else 190, 44 if short_layout else 52) footer.add_child(apply) func _build_settings_category(content: VBoxContainer) -> void: match settings_category: "display": content.add_child(_settings_heading(_tr("显示与文字", "DISPLAY & TEXT"))) content.add_child(_choice_button("SettingSafeArea", _tr("安全区域", "Safe area"), "%d px" % int(settings_draft.safe_area), cycle_setting_safe_area)) content.add_child(_choice_button("SettingTextScale", _tr("文字大小", "Text size"), "%d%%" % int(round(float(settings_draft.text_scale) * 100.0)), cycle_setting_text_scale)) content.add_child(_slider_row("SettingBrightness", _tr("亮度", "Brightness"), float(settings_draft.brightness), 0.7, 1.2, 0.1, set_brightness)) content.add_child(_check_button("SettingHighContrast", _tr("高对比轮廓", "High-contrast contours"), bool(settings_draft.high_contrast), set_high_contrast)) content.add_child(_check_button("SettingColorCues", _tr("颜色以外的形状提示", "Shape cues beyond color"), bool(settings_draft.color_cues), set_color_cues)) "motion": content.add_child(_settings_heading(_tr("动态、闪烁与省电", "MOTION, FLASHES & POWER"))) content.add_child(_check_button("SettingReducedMotion", _tr("减少动画", "Reduced motion"), bool(settings_draft.reduced_motion), set_reduced_motion)) content.add_child(_check_button("SettingReducedFlashes", _tr("减少闪烁", "Reduced flashes"), bool(settings_draft.reduced_flashes), set_reduced_flashes)) content.add_child(_slider_row("SettingShake", _tr("镜头震动", "Camera shake"), float(settings_draft.shake_intensity), 0.0, 1.0, 0.25, set_shake)) content.add_child(_check_button("SettingHitPause", _tr("命中停顿", "Hit pause"), bool(settings_draft.hit_pause), set_hit_pause)) content.add_child(_choice_button("SettingParticles", _tr("粒子密度", "Particles"), _tr("低", "Low") if settings_draft.particle_density == "low" else _tr("标准", "Standard"), cycle_particles)) content.add_child(_check_button("SettingLowPower", _tr("低功耗模式", "Low-power mode"), bool(settings_draft.low_power), set_low_power)) "audio": content.add_child(_settings_heading(_tr("音频与字幕", "AUDIO & CAPTIONS"))) content.add_child(_check_button("SettingMasterMute", _tr("主静音", "Master mute"), bool(settings_draft.master_muted), set_master_mute)) content.add_child(_slider_row("SettingMusic", _tr("音乐", "Music"), float(settings_draft.music_volume), 0.0, 1.0, 0.1, set_music_volume)) content.add_child(_slider_row("SettingSFX", _tr("音效", "SFX"), float(settings_draft.sfx_volume), 0.0, 1.0, 0.1, set_sfx_volume)) content.add_child(_unavailable_row("SettingAmbienceUnavailable", _tr("环境音:DEV-1 未集成独立总线", "Ambience: no separate DEV-1 bus is integrated"))) content.add_child(_unavailable_row("SettingVoiceUnavailable", _tr("语音:当前没有语音资产", "Voice: no voice assets are present"))) content.add_child(_check_button("SettingCaptions", _tr("字幕", "Captions"), bool(settings_draft.captions), set_captions)) content.add_child(_choice_button("SettingCaptionDetail", _tr("字幕细节", "Caption detail"), _tr("完整", "Full") if settings_draft.caption_detail == "full" else _tr("关键事件", "Critical"), cycle_caption_detail)) content.add_child(_choice_button("SettingDynamicRange", _tr("动态范围", "Dynamic range"), String(settings_draft.dynamic_range).capitalize(), cycle_dynamic_range)) "controls": content.add_child(_settings_heading(_tr("控制", "CONTROLS"))) content.add_child(_label(_tr("触控、键鼠与控制器共享确认、取消和导航语义。", "Touch, keyboard/mouse and controller share confirm, cancel and navigation semantics."), _scaled(15), Color("#aebbc2"))) content.add_child(_slider_row("SettingHoldDuration", _tr("长按确认时间", "Hold duration"), float(settings_draft.hold_duration), 0.3, 1.5, 0.1, set_hold_duration)) content.add_child(_check_button("SettingVibration", _tr("震动", "Vibration"), bool(settings_draft.vibration), set_vibration)) content.add_child(_unavailable_row("SettingRemapUnavailable", _tr("按键重映射:DEV-1 仅提供默认入口映射;完整冲突检查属于后续控制页面", "Remapping: DEV-1 uses the default entry map; conflict-safe remapping belongs to the later Controls page"))) "language": content.add_child(_settings_heading(_tr("语言与地区格式", "LANGUAGE & LOCALE"))) content.add_child(_choice_button("SettingLocale", _tr("界面语言", "Interface language"), _tr("简体中文", "English"), cycle_setting_locale)) content.add_child(_label(_tr("日期 2026-08-17 · 数字 1,234.5 · 文本可换行", "Date 2026-08-17 · Number 1,234.5 · Text may wrap"), _scaled(16), Color("#f5ead0"))) "credits": content.add_child(_settings_heading(_tr("制作人员、许可与数据", "CREDITS, LICENSES & DATA"))) var credits := _label(_credits_text(), _scaled(14), Color("#f5ead0")) credits.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART credits.size_flags_horizontal = Control.SIZE_EXPAND_FILL content.add_child(credits) func open_settings_category(category: String) -> void: settings_category = category show_settings() func request_settings_back() -> void: if settings_draft != settings: settings_notice = _tr("有未保存的更改。请选择应用或放弃并返回。", "Unsaved changes. Choose Apply or Discard & Back.") show_settings() else: back_to_title() func discard_settings() -> void: settings_draft = {} settings_notice = "" back_to_title() func apply_settings() -> void: var saved: Dictionary = settings_store.save_settings(settings_draft) if not saved.ok: settings_notice = _tr("设置保存失败,请重试。", "Settings save failed. Retry.") + " [%s]" % String(saved.code) show_settings() return settings = saved.settings.duplicate(true) settings_draft = settings.duplicate(true) _apply_runtime_settings() settings_notice = _tr("设置已应用", "Settings applied") show_settings() func show_credits() -> void: settings_draft = settings.duplicate(true) settings_category = "credits" show_settings("credits") func _credits_text() -> String: return _tr( "Aetherbound Guild · 原创游戏与运行实现\n字体:Noto Sans SC,SIL Open Font License 1.1(完整许可随游戏提供)\n数据:核心游戏完全离线;DEV-1 不收集分析数据,不创建账户,不打开外部链接。\n隐私:设置与远征记录只保存在本地。", "Aetherbound Guild · original game and runtime implementation\nFont: Noto Sans SC, SIL Open Font License 1.1 (full license included)\nData: core play is fully offline; DEV-1 collects no analytics, creates no account and opens no external links.\nPrivacy: settings and expedition records remain local." ) func _apply_runtime_settings() -> void: var master := AudioServer.get_bus_index(&"Master") var music := AudioServer.get_bus_index(&"Music") var sfx := AudioServer.get_bus_index(&"SFX") if master >= 0: AudioServer.set_bus_mute(master, bool(settings.master_muted)) if music >= 0: AudioServer.set_bus_volume_db(music, linear_to_db(maxf(0.001, float(settings.music_volume)))) if sfx >= 0: AudioServer.set_bus_volume_db(sfx, linear_to_db(maxf(0.001, float(settings.sfx_volume)))) func cycle_setting_safe_area() -> void: settings_draft.safe_area = _next_value(SettingsStore.SAFE_AREA_PRESETS, int(settings_draft.safe_area)) show_settings() func cycle_setting_text_scale() -> void: settings_draft.text_scale = _next_float(SettingsStore.TEXT_SCALES, float(settings_draft.text_scale)) show_settings() func cycle_setting_locale() -> void: settings_draft.locale = "en" if String(settings_draft.locale) == "zh_CN" else "zh_CN" setup_draft = settings_draft.duplicate(true) show_settings() func cycle_particles() -> void: settings_draft.particle_density = "low" if settings_draft.particle_density == "standard" else "standard" show_settings() func cycle_caption_detail() -> void: settings_draft.caption_detail = "full" if settings_draft.caption_detail == "critical" else "critical" show_settings() func cycle_dynamic_range() -> void: settings_draft.dynamic_range = _next_value(["wide", "standard", "night"], String(settings_draft.dynamic_range)) show_settings() func set_brightness(value: float) -> void: settings_draft.brightness = value func set_high_contrast(value: bool) -> void: settings_draft.high_contrast = value func set_color_cues(value: bool) -> void: settings_draft.color_cues = value func set_reduced_motion(value: bool) -> void: settings_draft.reduced_motion = value func set_reduced_flashes(value: bool) -> void: settings_draft.reduced_flashes = value func set_shake(value: float) -> void: settings_draft.shake_intensity = value func set_hit_pause(value: bool) -> void: settings_draft.hit_pause = value func set_low_power(value: bool) -> void: settings_draft.low_power = value func set_master_mute(value: bool) -> void: settings_draft.master_muted = value func set_music_volume(value: float) -> void: settings_draft.music_volume = value func set_sfx_volume(value: float) -> void: settings_draft.sfx_volume = value func set_captions(value: bool) -> void: settings_draft.captions = value func set_hold_duration(value: float) -> void: settings_draft.hold_duration = value func set_vibration(value: bool) -> void: settings_draft.vibration = value func _next_value(options: Array, current: Variant) -> Variant: return options[(options.find(current) + 1) % options.size()] func _next_float(options: Array, current: float) -> float: for index in range(options.size()): if is_equal_approx(float(options[index]), current): return float(options[(index + 1) % options.size()]) return float(options[0]) func _settings_heading(text_value: String) -> Label: return _label(text_value, _scaled(19), Color("#e8d39d")) func _slider_row(node_name: String, text_value: String, value: float, minimum: float, maximum: float, step: float, callback: Callable) -> HBoxContainer: var row := HBoxContainer.new() row.name = node_name row.custom_minimum_size.y = 42 var label := _label("%s %.1f" % [text_value, value], _scaled(14), Color("#f5ead0")) label.custom_minimum_size.x = 190 row.add_child(label) var slider := HSlider.new() slider.name = "Slider" slider.min_value = minimum slider.max_value = maximum slider.step = step slider.value = value slider.custom_minimum_size.y = 24 slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL slider.add_theme_stylebox_override("slider", _flat_style(Color("#0d151d"), Color("#866f47"), 1, 3)) slider.add_theme_stylebox_override("grabber_area", _flat_style(Color("#73b9e7"), Color("#ead49b"), 1, 3)) slider.add_theme_stylebox_override("grabber_area_highlight", _flat_style(Color("#a9d9f2"), Color("#f5ead0"), 1, 3)) slider.value_changed.connect(callback) row.add_child(slider) return row func _unavailable_row(node_name: String, reason: String) -> Button: var button := _choice_button(node_name, reason, _tr("不可用", "Unavailable"), show_unavailable_reason.bind(reason)) _set_entry_button_disabled(button, true) button.tooltip_text = reason return button func show_unavailable_reason(reason: String) -> void: settings_notice = reason show_settings() func _show_child_placeholder(screen_id: String, heading: String, body: String) -> void: current_screen = screen_id _build_world_base() var safe := MarginContainer.new() safe.name = "ChildSafeArea" safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) safe.add_theme_constant_override("margin_left", 56) safe.add_theme_constant_override("margin_right", 56) safe.add_theme_constant_override("margin_top", 40) safe.add_theme_constant_override("margin_bottom", 40) add_child(safe) safe.add_child(_entry_chrome("ChildFrame", "settings_frame")) var box := VBoxContainer.new() box.name = "ChildLayout" box.alignment = BoxContainer.ALIGNMENT_CENTER box.add_theme_constant_override("separation", 14) safe.add_child(box) box.add_child(_label(heading, _scaled(28), Color("#f5ead0"))) var explanation := _label(body, _scaled(17), Color("#aebbc2")) explanation.name = "ChildExplanation" explanation.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART box.add_child(explanation) box.add_child(_action_button("ChildBack", _tr("返回标题首页", "Back to Title"), back_to_title)) func back_to_title() -> void: title_notice = "" show_title() func request_exit() -> void: current_screen = "exit_review" _show_child_placeholder("exit_review", _tr("退出游戏?", "EXIT GAME?"), _tr("不会改变设置或本地远征记录。", "Settings and the local expedition will remain unchanged.")) var box := get_node("ChildSafeArea/ChildLayout") _set_entry_button_text(box.get_node("ChildBack"), _tr("取消", "Cancel")) box.add_child(_action_button("ExitConfirm", _tr("退出", "Exit"), confirm_exit)) func confirm_exit() -> void: get_tree().quit() func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("ui_cancel"): get_viewport().set_input_as_handled() if current_screen == "title": request_exit() elif current_screen in ["settings", "credits", "exit_review"]: back_to_title() func _save_status() -> String: var save_path := BootService.DEFAULT_SAVE_PATH if not FileAccess.file_exists(save_path): return "missing" var parser := JSON.new() if parser.parse(FileAccess.get_file_as_string(save_path)) != OK or not parser.data is Dictionary: return "unsafe" return "valid" if parser.data.has("save_kind") and parser.data.has("save_version") else "unsafe" func _build_world_base() -> void: for child in get_children(): child.free() var background := TextureRect.new() background.texture = GUILD_BACKGROUND background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE background.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) background.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(background) var shade := ColorRect.new() shade.color = Color(0.03, 0.06, 0.08, 0.34) shade.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) shade.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(shade) func _entry_chrome(node_name: String, variant: String, icon := "") -> Control: var chrome = EntryChrome.new().configure(variant, icon) chrome.name = node_name chrome.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) return chrome func _title_button(node_name: String, text_value: String, callback: Callable, icon: String, variant: String) -> Button: var button := Button.new() button.name = node_name button.flat = true button.clip_contents = false button.tooltip_text = text_value button.add_theme_font_override("font", UI_FONT) for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]: button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) button.add_child(_entry_chrome("Chrome", variant, icon)) var label := Label.new() label.name = "ButtonLabel" label.text = text_value label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) label.offset_left = 49.0 label.offset_right = -9.0 label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS label.mouse_filter = Control.MOUSE_FILTER_IGNORE label.add_theme_font_override("font", UI_FONT) label.add_theme_font_size_override("font_size", _scaled(14 if variant == "utility" else 17)) label.add_theme_color_override("font_color", Color("#202a31") if variant == "utility" else Color("#f5ead0")) button.add_child(label) button.set_meta("entry_chrome_variant", variant) button.pressed.connect(callback, CONNECT_DEFERRED) return button func _set_title_button_disabled(button: Button, disabled: bool) -> void: button.disabled = disabled var label := button.get_node_or_null("ButtonLabel") as Label if label != null: var utility := String(button.get_meta("entry_chrome_variant", "")) == "utility" label.add_theme_color_override("font_color", Color("#626970") if utility else Color("#89939a") if disabled else Color("#f5ead0")) func _choice_button(node_name: String, label_text: String, value_text: String, callback: Callable) -> Button: var button := _entry_button_base(node_name, callback, "choice") button.tooltip_text = "%s: %s" % [label_text, value_text] button.custom_minimum_size = Vector2(0, 48) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL var label := _button_label("ChoiceLabel", label_text, _scaled(15), Color("#f5ead0")) label.anchor_right = 0.58 label.offset_left = 14.0 label.offset_right = -6.0 label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT button.add_child(label) var value := _button_label("ValueLabel", value_text, _scaled(14), Color("#202a31")) value.anchor_left = 0.58 value.offset_left = 6.0 value.offset_right = -10.0 button.add_child(value) return button func _action_button(node_name: String, text_value: String, callback: Callable) -> Button: var button := _entry_button_base(node_name, callback, "action_primary") button.tooltip_text = text_value button.custom_minimum_size = Vector2(190, 52) var label := _button_label("ButtonLabel", text_value, _scaled(16), Color("#f5ead0")) label.offset_left = 46.0 label.offset_right = -10.0 button.add_child(label) return button func _tool_button(node_name: String, text_value: String, callback: Callable) -> Button: var button := _entry_button_base(node_name, callback, "tool") button.tooltip_text = text_value button.custom_minimum_size = Vector2(112, 48) var label := _button_label("ButtonLabel", text_value, _scaled(14), Color("#f5ead0")) label.offset_left = 16.0 label.offset_right = -10.0 button.add_child(label) return button func _check_button(node_name: String, text_value: String, enabled: bool, callback: Callable) -> CheckButton: var check := CheckButton.new() check.name = node_name check.button_pressed = enabled check.custom_minimum_size = Vector2(0, 48) check.size_flags_horizontal = Control.SIZE_EXPAND_FILL check.flat = true for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]: check.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) check.add_child(_entry_chrome("Chrome", "toggle")) var label := _button_label("ButtonLabel", text_value, _scaled(14), Color("#f5ead0")) label.offset_left = 14.0 label.offset_right = -52.0 label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART check.add_child(label) check.toggled.connect(callback) return check func _entry_button_base(node_name: String, callback: Callable, variant: String) -> Button: var button := Button.new() button.name = node_name button.flat = true for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]: button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) button.add_child(_entry_chrome("Chrome", variant)) button.set_meta("entry_chrome_variant", variant) button.pressed.connect(callback, CONNECT_DEFERRED) return button func _button_label(node_name: String, text_value: String, font_size: int, color: Color) -> Label: var label := _label(text_value, font_size, color) label.name = node_name label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS label.mouse_filter = Control.MOUSE_FILTER_IGNORE return label func _set_entry_button_disabled(button: Button, disabled: bool, selected := false) -> void: button.disabled = disabled for child_name in ["ButtonLabel", "ChoiceLabel", "ValueLabel"]: var label := button.get_node_or_null(child_name) as Label if label == null: continue if selected: label.add_theme_color_override("font_color", Color("#202a31")) elif disabled: label.add_theme_color_override("font_color", Color("#4f565b") if child_name == "ValueLabel" else Color("#89939a")) elif child_name == "ValueLabel": label.add_theme_color_override("font_color", Color("#202a31")) else: label.add_theme_color_override("font_color", Color("#f5ead0")) func _set_entry_button_text(button: Button, text_value: String) -> void: var label := button.get_node_or_null("ButtonLabel") as Label if label != null: label.text = text_value button.tooltip_text = text_value func _flat_style(background: Color, border: Color, width: int, radius: int) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = background style.border_color = border style.set_border_width_all(width) style.set_corner_radius_all(radius) return style func _label(text_value: String, font_size: int, color: Color) -> Label: var label := Label.new() label.text = text_value label.add_theme_font_override("font", UI_FONT) label.add_theme_font_size_override("font_size", font_size) label.add_theme_color_override("font_color", color) return label func _tr(zh: String, en: String) -> String: var locale := String(setup_draft.get("locale", settings.get("locale", "zh_CN"))) return zh if locale == "zh_CN" else en func _scaled(value: int) -> int: var scale := float(setup_draft.get("text_scale", settings.get("text_scale", 1.0))) return int(round(float(value) * scale))