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 InitialCompanyArt = preload("res://entry/initial_company_art.gd") const GuildDestinationSticker = preload("res://entry/guild_destination_sticker.gd") const GUILD_CONTRACT_BOARD = preload("res://assets/entry/guild_props/contract_board.png") const GUILD_ROUTE_MAP = preload("res://assets/entry/guild_props/route_map.png") const InitialCompanyDraftService = preload("res://entry/initial_company_draft_service.gd") const GuildContractService = preload("res://entry/guild_contract_service.gd") const FormationRiskService = preload("res://entry/formation_risk_service.gd") const BattleService = preload("res://entry/battle_service.gd") const RuntimeContentRegistry = preload("res://p10_0/content_registry.gd") const MarketLocalization = preload("res://entry/market_localization.gd") const InitialCompanyPage = preload("res://entry/initial_company_page.gd") const GUILD_BACKGROUND = preload("res://assets/backgrounds/p9_3_guild_hall.png") const CONTENT_BACKGROUND = preload("res://assets/backgrounds/first_expedition_field.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 initial_company_service := InitialCompanyDraftService.new() var guild_contract_service := GuildContractService.new() var formation_risk_service := FormationRiskService.new() var battle_service := BattleService.new() var initial_company_page: Control 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 := "" var settings_caller := "title" var guild_company: Dictionary = {} var contract_difficulty := "standard" var contract_notice := "" var market_category := "recruit" var market_selected_kind := "" var market_selected_offer_id := "" var market_notice := "" var market_content: Dictionary = RuntimeContentRegistry.snapshot() var party_draft_order: Array[String] = [] var party_undo_order: Array[String] = [] var party_selected_recruit_id := "" var party_preview: Dictionary = {} var party_notice := "" var roster_selected_recruit_id := "" var dismissal_recruit_id := "" var dismissal_return_screen := "roster" var dismissal_notice := "" var contract_return_notice := "" var risk_selected_id := "" var battle_operation_id := "" var battle_paused := false var battle_speed := 1.0 var battle_notice := "" var battle_playback_generation := 0 var battle_playback_timer: Timer var battle_result_diagnosis_open := false func _ready() -> void: set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) tree_exiting.connect(_invalidate_battle_playback) _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 = CONTENT_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.12) 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_field_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): var company := initial_company_service.load_company() if company.ok: _open_company_destination(company.company) else: if String(company.code) != "COMPANY_MISSING": title_notice = _tr("远征团存档需要处理,已停止创建新游戏。", "Company data needs attention; New Game is blocked.") + " [%s]" % String(company.code) 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 company_result := initial_company_service.load_company() var has_company := bool(company_result.ok) var data_unsafe := save_status == "unsafe" var summary := _label( _tr("远征团已建立 · 可继续进入公会", "Expedition Company established · continue to the Guild") if has_company else _tr("本地数据需要处理", "Local data needs attention") if data_unsafe else _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) new_game.disabled = has_company or data_unsafe new_game.tooltip_text = _tr("远征团已建立;继续游戏进入公会", "A Company already exists; Continue opens the Guild") if has_company else _tr("本地数据需要先处理", "Local data needs attention") if new_game.disabled else _tr("建立初始远征团", "Create the initial Company") _set_title_button_disabled(new_game, new_game.disabled) 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 if not new_game.disabled else continue_button if not continue_button.disabled else settings_button).call_deferred("grab_focus") var reason := _label( _tr("本地数据需要处理;新游戏与继续游戏均已停止。", "Local data needs attention; New Game and Continue are blocked.") if data_unsafe else _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: var company := initial_company_service.load_company() if company.ok: _open_company_destination(company.company) return if String(company.code) != "COMPANY_MISSING": title_notice = _tr("远征团存档需要处理,未创建新候选。", "Company data needs attention; no new candidates were created.") + " [%s]" % String(company.code) show_title() return show_initial_company() func show_initial_company() -> void: current_screen = "initial_company" for child in get_children(): child.free() initial_company_page = InitialCompanyPage.new() initial_company_page.name = "InitialCompanyPage" initial_company_page.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) initial_company_page.back_requested.connect(back_to_title, CONNECT_DEFERRED) initial_company_page.company_committed.connect(show_guild_handoff, CONNECT_DEFERRED) add_child(initial_company_page) initial_company_page.open(initial_company_service, settings) func open_continue_boundary() -> void: var company := initial_company_service.load_company() if company.ok: _open_company_destination(company.company) return 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 _open_company_destination(company: Dictionary) -> void: if company.has("active_contract") and not company.active_contract.is_empty(): if company.active_contract.has("battle_state") and not company.active_contract.battle_state.is_empty(): var battle_status := String(company.active_contract.battle_state.get("status", "")) if battle_status == "active": show_battle(company) return if battle_status == "result": show_battle_result(company) return if String(company.active_contract.get("status", "")) == "returned": show_guild_handoff(company) return if company.active_contract.has("formation") and not company.active_contract.formation.is_empty(): var formation: Dictionary = company.active_contract.formation if String(formation.get("status", "draft")) in ["ordered", "ready"]: show_readiness_review(company) else: show_party_line(company) else: show_market_handoff(company) else: show_guild_handoff(company) func show_guild_handoff(company := {}) -> void: var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else {} if resolved.is_empty(): var loaded := initial_company_service.load_company() if not loaded.ok: title_notice = _tr("无法读取远征团。", "The Company could not be loaded.") + " [%s]" % String(loaded.code) show_title() return resolved = loaded.company.duplicate(true) guild_contract_service.company_path = initial_company_service.company_path guild_company = resolved show_guild_anchor(resolved) return current_screen = "guild_handoff" _build_world_base(true) var frame_safe := MarginContainer.new() frame_safe.name = "GuildHandoffFrameSafeArea" frame_safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) frame_safe.mouse_filter = Control.MOUSE_FILTER_IGNORE var margin := maxi(12, int(settings.get("safe_area", 24))) var vertical_margin := 12 if size.y < 500.0 else margin frame_safe.add_theme_constant_override("margin_left", margin) frame_safe.add_theme_constant_override("margin_right", margin) frame_safe.add_theme_constant_override("margin_top", vertical_margin) frame_safe.add_theme_constant_override("margin_bottom", vertical_margin) add_child(frame_safe) frame_safe.add_child(_entry_chrome("GuildHandoffFrame", "content_scene_frame")) var safe := MarginContainer.new() safe.name = "GuildHandoffSafeArea" safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) safe.add_theme_constant_override("margin_left", margin + 12) safe.add_theme_constant_override("margin_right", margin + 12) safe.add_theme_constant_override("margin_top", vertical_margin + 8) safe.add_theme_constant_override("margin_bottom", vertical_margin + 8) add_child(safe) var layout := VBoxContainer.new() layout.name = "GuildHandoffLayout" layout.add_theme_constant_override("separation", 6) safe.add_child(layout) var header := HBoxContainer.new() header.custom_minimum_size = Vector2(0, 50) layout.add_child(header) var copy := VBoxContainer.new() copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(copy) copy.add_child(_label(_tr("公会大厅", "GUILD HALL"), _scaled(21), Color("#f5ead0"))) var contract_status := String(resolved.get("active_contract", {}).get("status", "")) var active_contract: bool = contract_status in ["market", "battle", "result", "recovery"] var returned_contract: bool = contract_status == "returned" copy.add_child(_label( _tr("首个委托已开始", "FIRST CONTRACT ACTIVE") if active_contract else (_tr("委托已返回 · 成长保留", "CONTRACT RETURNED · GROWTH RETAINED") if returned_contract else _tr("初始远征团已建立", "INITIAL EXPEDITION COMPANY ESTABLISHED")), _scaled(11), Color("#e8d39d") )) var identity := _label( _tr("市场准备", "MARKET READY") if active_contract else (_tr("可继续管理", "READY FOR MANAGEMENT") if returned_contract else _tr("持续远征", "PERSISTENT COMPANY")), _scaled(12), Color("#73b9e7") ) identity.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(identity) var body := HBoxContainer.new() body.name = "GuildHandoffBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 18) layout.add_child(body) var roster := HBoxContainer.new() roster.name = "GuildRoster" roster.size_flags_horizontal = Control.SIZE_EXPAND_FILL roster.size_flags_stretch_ratio = 1.45 roster.add_theme_constant_override("separation", 12) body.add_child(roster) for index in range(resolved.owned_recruits.size()): roster.add_child(_guild_member_column(index, resolved.owned_recruits[index])) var next := VBoxContainer.new() next.name = "GuildNextActions" next.custom_minimum_size = Vector2(minf(330.0, size.x * 0.36), 0) next.add_theme_constant_override("separation", 6) body.add_child(next) next.add_child(_label(_tr("下一步", "NEXT"), _scaled(10), Color("#e8d39d"))) var next_copy := _label( _tr("风镜初渡已建立。前往市场扩充远征团。", "First Windglass Crossing is active. Enter the Market to expand the Company.") if active_contract else (_tr("委托已主动返回;金币、成员、等级、装备与历史均保留。", "The Contract was returned voluntarily; Coin, members, levels, equipment and history are retained.") if returned_contract else _tr("选择首个委托与难度,然后进入市场。", "Choose the first Contract and difficulty, then enter the Market.")), _scaled(13), Color("#f5ead0") ) next_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART next.add_child(next_copy) var contract := _guild_destination_button( "GuildContractActive" if active_contract else ("GuildContractReturned" if returned_contract else "GuildContractBoard"), "contract", _tr("委托已开始", "Contract Active") if active_contract else (_tr("委托已返回", "Contract Returned") if returned_contract else _tr("委托板", "Contract Board")), func(): _set_guild_notice(_tr("已有进行中的委托。先进入市场。", "A Contract is active. Enter its Market next.")) if active_contract else (_set_guild_notice(_tr("该委托已返回;后续 Contract 创建属于更晚步骤。", "This Contract has returned; creating another Contract belongs to a later step.")) if returned_contract else show_contract_choice()) ) _set_entry_button_disabled(contract, active_contract or returned_contract) contract.visible = not active_contract and not returned_contract contract.tooltip_text = _tr("已有进行中的委托", "A Contract is already active") if active_contract else (_tr("该委托已返回;下一份委托尚未开放", "This Contract has returned; the next Contract is not open yet") if returned_contract else _tr("查看风镜初渡并选择难度", "Review First Windglass Crossing and choose difficulty")) next.add_child(contract) var contract_reason := _label( _tr("已开始:风镜初渡 · %s", "Active: First Windglass Crossing · %s") % _difficulty_name(String(resolved.active_contract.get("difficulty", "standard"))) if active_contract else (_tr("已返回:成长继续保留", "Returned: persistent growth remains available") if returned_contract else _tr("可用:风镜初渡 · 选择难度后确认", "Available: First Windglass Crossing · choose difficulty, then confirm")), _scaled(10), Color("#aebbc2") ) contract_reason.name = "GuildContractReason" contract_reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART next.add_child(contract_reason) var market := _guild_destination_button( "GuildMarket" if active_contract else "GuildMarketUnavailable", "market", _tr("进入市场", "Enter Market") if active_contract else _tr("市场", "Market"), func(): show_market_handoff(resolved) if active_contract else _set_guild_notice(_tr("请先选择并确认首个委托。", "Choose and confirm the first Contract before entering the Market.")) ) _set_entry_button_disabled(market, not active_contract) market.visible = active_contract market.tooltip_text = _tr("进入首个委托市场", "Enter the first Contract Market") if active_contract else _tr("先确认首个委托", "Confirm the first Contract first") next.add_child(market) var market_reason := _label( _tr("可用:%d 金币 · 远征团 %d · 队列容量 %d", "Available: %d Coin · Company %d · Line capacity %d") % [int(resolved.active_contract.get("coin", 0)), resolved.owned_recruits.size(), int(resolved.active_contract.get("line_capacity", 4))] if active_contract else (_tr("不可用:委托已返回;成员与成长仍可查看。", "Unavailable: the Contract has returned; members and growth remain viewable.") if returned_contract else _tr("不可用:尚未建立首个委托。", "Unavailable: no first Contract has been created.")), _scaled(10), Color("#aebbc2") ) market_reason.name = "GuildMarketReason" market_reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART next.add_child(market_reason) var notice := _label("", _scaled(10), Color("#f0ca75")) notice.name = "GuildNotice" notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART next.add_child(notice) var footer := HBoxContainer.new() footer.name = "GuildHandoffFooter" footer.custom_minimum_size = Vector2(0, 52) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) footer.add_child(_company_button("GuildTitle", _tr("标题首页", "Title"), back_to_title, "company_utility", "back")) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) footer.add_child(_company_button("GuildRosterButton", _tr("成员", "Roster"), show_roster.bind(resolved), "company_secondary", "inspect")) footer.add_child(_company_button("GuildSettings", _tr("设置", "Settings"), show_settings_from_guild, "company_secondary", "settings")) var focus_target := contract if contract.visible else market if market.visible else footer.get_node("GuildRosterButton") focus_target.call_deferred("grab_focus") func show_guild_anchor(company: Dictionary) -> void: var resolved := company.duplicate(true) guild_company = resolved current_screen = "guild_handoff" _build_world_base(true) var margin := maxi(14, int(settings.get("safe_area", 24))) var root := Control.new() root.name = "GuildAnchorRoot" root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) root.offset_left = margin root.offset_right = -margin root.offset_top = 10 root.offset_bottom = -10 add_child(root) var top_rail := ColorRect.new() top_rail.name = "GuildAnchorTopRail" top_rail.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE) top_rail.offset_bottom = 64 top_rail.color = Color(0.04, 0.10, 0.13, 0.72) top_rail.mouse_filter = Control.MOUSE_FILTER_IGNORE root.add_child(top_rail) var top_rule := ColorRect.new() top_rule.name = "GuildAnchorTopRule" top_rule.position = Vector2(18, 58) top_rule.size = Vector2(maxf(1.0, size.x - margin * 2.0 - 36.0), 2) top_rule.color = Color("#d6b76c") top_rule.mouse_filter = Control.MOUSE_FILTER_IGNORE root.add_child(top_rule) var title := _label(_tr("公会大厅", "GUILD HALL"), _scaled(22), Color("#fff4d6")) title.name = "GuildAnchorTitle" title.position = Vector2(18, 9) title.size = Vector2(330, 30) root.add_child(title) var status := _label(_tr("持续远征", "PERSISTENT COMPANY"), _scaled(11), Color("#a9d9ef")) status.name = "GuildAnchorStatus" status.position = Vector2(20, 38) status.size = Vector2(330, 18) root.add_child(status) var company_status := _label(_tr("成员 %d", "%d MEMBERS") % resolved.owned_recruits.size(), _scaled(13), Color("#f0ca75")) company_status.name = "GuildAnchorCompanyStatus" company_status.anchor_left = 1.0 company_status.anchor_right = 1.0 company_status.offset_left = -230 company_status.offset_right = -18 company_status.offset_top = 20 company_status.offset_bottom = 48 company_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT root.add_child(company_status) var body := Control.new() body.name = "GuildHandoffBody" body.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) body.offset_top = 74 body.offset_bottom = -62 root.add_child(body) var roster := HBoxContainer.new() roster.name = "GuildRoster" roster.anchor_right = 0.57 roster.anchor_bottom = 1.0 roster.offset_left = 18 roster.offset_top = 10 roster.offset_right = -6 roster.offset_bottom = -8 roster.size_flags_horizontal = Control.SIZE_EXPAND_FILL roster.add_theme_constant_override("separation", 8) body.add_child(roster) for index in range(resolved.owned_recruits.size()): roster.add_child(_guild_member_column(index, resolved.owned_recruits[index])) var next := VBoxContainer.new() next.name = "GuildNextActions" next.anchor_left = 0.59 next.anchor_right = 1.0 next.anchor_bottom = 1.0 next.offset_left = 0 next.offset_top = 10 next.offset_right = -8 next.offset_bottom = -8 next.add_theme_constant_override("separation", 5) body.add_child(next) var active_contract: bool = String(resolved.get("active_contract", {}).get("status", "")) in ["market", "battle", "result", "recovery"] var returned_contract: bool = String(resolved.get("active_contract", {}).get("status", "")) == "returned" var next_label := _label(_tr("下一步", "NEXT ACTION"), _scaled(10), Color("#e8d39d")) next_label.name = "GuildNextHeading" next.add_child(next_label) var next_copy := _label( _tr("前往市场扩充远征团。", "Enter the Market to expand the Company.") if active_contract else _tr("选择首个委托与难度。", "Choose the first Contract and difficulty."), _scaled(14), Color("#fff4d6") ) next_copy.name = "GuildNextCopy" next_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART next.add_child(next_copy) var contract := _guild_destination_button( "GuildContractActive" if active_contract else "GuildContractBoard", "contract", _tr("委托已开始", "Contract Active") if active_contract else _tr("委托板", "Contract Board"), func(): _set_guild_notice(_tr("委托已开始,请进入市场。", "The Contract is active; enter its Market.")) if active_contract else show_contract_choice() ) _set_entry_button_disabled(contract, active_contract or returned_contract) contract.visible = not active_contract and not returned_contract next.add_child(contract) var contract_reason := _label( _tr("已开始:风镜初渡", "Active: First Windglass Crossing") if active_contract else _tr("可用:选择难度后确认", "Available: choose a difficulty, then confirm"), _scaled(10), Color("#e8d39d") ) contract_reason.name = "GuildContractReason" contract_reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART next.add_child(contract_reason) var market := _guild_destination_button( "GuildMarket" if active_contract else "GuildMarketUnavailable", "market", _tr("进入市场", "Enter Market") if active_contract else _tr("市场", "Market"), func(): show_market_handoff(resolved) if active_contract else _set_guild_notice(_tr("请先确认首个委托。", "Confirm the first Contract first.")) ) _set_entry_button_disabled(market, not active_contract) market.visible = active_contract next.add_child(market) var market_reason := _label( _tr("可用:市场准备", "Available: Market preparation") if active_contract else _tr("不可用:尚未建立首个委托。", "Unavailable: no first Contract has been created."), _scaled(10), Color("#e8d39d") ) market_reason.name = "GuildMarketReason" market_reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART next.add_child(market_reason) var notice := _label("", _scaled(10), Color("#f0ca75")) notice.name = "GuildNotice" notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART next.add_child(notice) var bottom_rail := ColorRect.new() bottom_rail.name = "GuildAnchorBottomRail" bottom_rail.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_WIDE) bottom_rail.offset_top = -54 bottom_rail.color = Color(0.04, 0.09, 0.11, 0.76) bottom_rail.mouse_filter = Control.MOUSE_FILTER_IGNORE root.add_child(bottom_rail) var footer := HBoxContainer.new() footer.name = "GuildHandoffFooter" footer.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_WIDE) footer.offset_top = -52 footer.add_theme_constant_override("separation", 8) root.add_child(footer) var back := _company_button("GuildTitle", _tr("返回标题", "Back to Title"), back_to_title, "company_utility", "back") back.custom_minimum_size = Vector2(190, 48) footer.add_child(back) var footer_spacer := Control.new() footer_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(footer_spacer) var roster_button := _company_button("GuildRosterButton", _tr("成员", "Roster"), show_roster.bind(resolved), "content_secondary", "inspect") roster_button.custom_minimum_size = Vector2(160, 48) footer.add_child(roster_button) var settings_button := _company_button("GuildSettings", _tr("设置", "Settings"), show_settings_from_guild, "content_secondary", "settings") settings_button.custom_minimum_size = Vector2(160, 48) footer.add_child(settings_button) (contract if contract.visible else market if market.visible else roster_button).call_deferred("grab_focus") func show_roster(company := {}) -> void: var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else guild_company.duplicate(true) if resolved.is_empty(): var loaded := initial_company_service.load_company() if not loaded.ok: show_title() return resolved = loaded.company.duplicate(true) guild_company = resolved current_screen = "roster" if roster_selected_recruit_id.is_empty() or _recruit_by_id(resolved, roster_selected_recruit_id).is_empty(): roster_selected_recruit_id = String(resolved.owned_recruits[0].recruit_id) if not resolved.owned_recruits.is_empty() else "" _build_world_base() var frame_safe := _framed_page_safe("Roster") frame_safe.add_child(_entry_chrome("RosterFrame", "initial_company_scene_frame")) var safe := _content_page_safe("Roster") var layout := VBoxContainer.new() layout.name = "RosterLayout" layout.add_theme_constant_override("separation", 5) safe.add_child(layout) var header := HBoxContainer.new() header.custom_minimum_size = Vector2(0, 44 if size.y < 500.0 else 52) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("远征团成员", "COMPANY ROSTER"), _scaled(21), Color("#f5ead0"))) heading.add_child(_label(_tr("稳定身份 · 个人成长 · 装备与战斗历史", "STABLE IDENTITY · PERSONAL GROWTH · EQUIPMENT & BATTLE HISTORY"), _scaled(10), Color("#e8d39d"))) var count := _label(_tr("成员 %d", "%d MEMBERS") % resolved.owned_recruits.size(), _scaled(12), Color("#73b9e7")) count.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(count) var body := HBoxContainer.new() body.name = "RosterBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 14) layout.add_child(body) var list := VBoxContainer.new() list.name = "RosterList" list.custom_minimum_size = Vector2(minf(420.0, size.x * 0.54), 0) list.size_flags_vertical = Control.SIZE_EXPAND_FILL list.add_theme_constant_override("separation", 5) body.add_child(list) for index in range(resolved.owned_recruits.size()): list.add_child(_roster_member_button(index, resolved.owned_recruits[index])) var detail := VBoxContainer.new() detail.name = "RosterDetail" detail.size_flags_horizontal = Control.SIZE_EXPAND_FILL detail.add_theme_constant_override("separation", 5) body.add_child(detail) _render_roster_detail(detail, resolved, roster_selected_recruit_id) var footer := HBoxContainer.new() footer.name = "RosterFooter" footer.custom_minimum_size = Vector2(0, 44 if size.y < 500.0 else 52) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) footer.add_child(_company_button("RosterBackGuild", _tr("返回公会", "Back to Guild"), back_to_guild, "company_utility", "back")) var roster_back := footer.get_child(footer.get_child_count() - 1) as Button roster_back.custom_minimum_size = Vector2(190, footer.custom_minimum_size.y) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var dismiss_eligible: bool = not roster_selected_recruit_id.is_empty() and resolved.owned_recruits.size() > 1 and String(resolved.active_contract.get("status", "")) == "market" var dismiss := _company_button("RosterDismiss", _tr("解雇", "Dismiss"), open_dismissal_review.bind(roster_selected_recruit_id), "company_secondary", "utility") dismiss.custom_minimum_size = Vector2(190, footer.custom_minimum_size.y) _set_entry_button_disabled(dismiss, not dismiss_eligible) dismiss.tooltip_text = _tr("审查 %s 的解雇后果", "Review dismissal consequences for %s") % _recruit_name(_recruit_by_id(resolved, roster_selected_recruit_id)) if dismiss_eligible else _tr("最后一名成员不可解雇,或当前阶段不是 Market", "The last member cannot be dismissed, or the current phase is not Market") footer.add_child(dismiss) var detail_button := _company_button("RosterOpenDetail", _tr("查看详情", "Open Detail"), show_recruit_detail.bind(roster_selected_recruit_id), "company_primary", "inspect") detail_button.custom_minimum_size = Vector2(190, footer.custom_minimum_size.y) footer.add_child(detail_button) find_child("RosterOpenDetail", true, false).call_deferred("grab_focus") func show_recruit_detail(recruit_id: String) -> void: var loaded: Dictionary = initial_company_service.load_company() if not loaded.ok: show_title() return var recruit := _recruit_by_id(loaded.company, recruit_id) if recruit.is_empty(): roster_selected_recruit_id = "" show_roster(loaded.company) return guild_company = loaded.company.duplicate(true) roster_selected_recruit_id = recruit_id current_screen = "recruit_detail" _build_world_base() var frame_safe := _framed_page_safe("RecruitDetail") frame_safe.add_child(_entry_chrome("RecruitDetailFrame", "initial_company_scene_frame")) var safe := _content_page_safe("RecruitDetail") var layout := VBoxContainer.new() layout.name = "RecruitDetailLayout" layout.add_theme_constant_override("separation", 5) safe.add_child(layout) var header := HBoxContainer.new() header.custom_minimum_size = Vector2(0, 48 if size.y < 500.0 else 56) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("成员详情", "RECRUIT DETAIL"), _scaled(21), Color("#f5ead0"))) heading.add_child(_label(_recruit_name(recruit), _scaled(12), Color("#e8d39d"))) var state := _label(_tr("保留中", "RETAINED"), _scaled(11), Color("#9ed7aa")) state.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(state) var body := HBoxContainer.new() body.name = "RecruitDetailBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 16) layout.add_child(body) var portrait_stage := Control.new() portrait_stage.name = "RecruitDetailPortrait" portrait_stage.size_flags_horizontal = Control.SIZE_EXPAND_FILL portrait_stage.size_flags_stretch_ratio = 0.8 body.add_child(portrait_stage) var portrait := TextureRect.new() portrait.texture = InitialCompanyArt.texture_for(recruit) portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS portrait.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE portrait_stage.add_child(portrait) var details := VBoxContainer.new() details.name = "RecruitDetailFacts" details.size_flags_horizontal = Control.SIZE_EXPAND_FILL details.size_flags_stretch_ratio = 1.2 details.add_theme_constant_override("separation", 5) body.add_child(details) _add_review_detail(details, "RecruitDetailProfession", _tr("职业", "PROFESSION"), "%s · %s" % [_recruit_profession_name(recruit), String(recruit.profession_job)]) _add_review_detail(details, "RecruitDetailTrait", _tr("特质", "TRAIT"), _recruit_trait_name(recruit)) _add_review_detail(details, "RecruitDetailGrowth", _tr("个人成长", "PERSONAL GROWTH"), _tr("等级 %d · XP %d", "Level %d · XP %d") % [int(recruit.level), int(recruit.xp)]) _add_review_detail(details, "RecruitDetailEquipment", _tr("当前装备", "EQUIPPED ITEM"), _recruit_item_name(recruit)) var history: Dictionary = recruit.get("battle_history", {}) _add_review_detail(details, "RecruitDetailHistory", _tr("战斗历史", "BATTLE HISTORY"), _tr("参战 %d · 胜利 %d · 击倒 %d", "Entered %d · Victories %d · Knockouts %d") % [int(history.get("battles_entered", 0)), int(history.get("victories", 0)), int(history.get("knockouts", 0))]) var note := _label(_tr("解雇、晋升与更多构筑操作将在后续 DEV-6 步骤启用。", "Dismissal, promotion and further build actions are enabled in later DEV-6 steps."), _scaled(10), Color("#e8d39d")) note.name = "RecruitDetailFutureReason" note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART details.add_child(note) var footer := HBoxContainer.new() footer.name = "RecruitDetailFooter" footer.custom_minimum_size = Vector2(0, 44 if size.y < 500.0 else 52) layout.add_child(footer) var detail_back := _company_button("RecruitDetailBackRoster", _tr("返回成员", "Back to Roster"), show_roster.bind(guild_company), "company_utility", "back") detail_back.custom_minimum_size = Vector2(190, footer.custom_minimum_size.y) footer.add_child(detail_back) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var dismiss_eligible: bool = guild_company.owned_recruits.size() > 1 and String(guild_company.active_contract.get("status", "")) == "market" var dismiss := _company_button("RecruitDetailDismiss", _tr("解雇", "Dismiss"), open_dismissal_review.bind(recruit_id), "company_secondary", "utility") dismiss.custom_minimum_size = Vector2(190, footer.custom_minimum_size.y) _set_entry_button_disabled(dismiss, not dismiss_eligible) dismiss.tooltip_text = _tr("审查 %s 的解雇后果", "Review dismissal consequences for %s") % _recruit_name(recruit) if dismiss_eligible else _tr("最后一名成员不可解雇,或当前阶段不是 Market", "The last member cannot be dismissed, or the current phase is not Market") footer.add_child(dismiss) find_child("RecruitDetailBackRoster", true, false).call_deferred("grab_focus") func open_dismissal_review(recruit_id: String) -> void: var loaded := initial_company_service.load_company() if not loaded.ok: show_title() return var recruit := _recruit_by_id(loaded.company, recruit_id) if recruit.is_empty(): roster_selected_recruit_id = "" show_roster(loaded.company) return guild_company = loaded.company.duplicate(true) dismissal_recruit_id = recruit_id dismissal_return_screen = current_screen if current_screen in ["roster", "recruit_detail"] else "roster" current_screen = "dismissal_review" _build_world_base() var frame_safe := _framed_page_safe("DismissalReview") frame_safe.add_child(_entry_chrome("DismissalReviewFrame", "initial_company_scene_frame")) var safe := _content_page_safe("DismissalReview") var layout := VBoxContainer.new() layout.name = "DismissalReviewLayout" layout.add_theme_constant_override("separation", 6) safe.add_child(layout) var header := HBoxContainer.new() header.custom_minimum_size = Vector2(0, 48 if size.y < 500.0 else 56) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("解雇审查", "DISMISSAL REVIEW"), _scaled(21), Color("#f5ead0"))) heading.add_child(_label(_recruit_name(recruit), _scaled(13), Color("#e8d39d"))) var eligibility := _label(_tr("可执行 · 当前成员 %d", "ELIGIBLE · %d LIVE MEMBERS") % loaded.company.owned_recruits.size(), _scaled(11), Color("#9ed7aa")) eligibility.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(eligibility) var body := HBoxContainer.new() body.name = "DismissalReviewBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 18) layout.add_child(body) var portrait_stage := Control.new() portrait_stage.name = "DismissalReviewPortrait" portrait_stage.size_flags_horizontal = Control.SIZE_EXPAND_FILL portrait_stage.size_flags_stretch_ratio = 0.72 body.add_child(portrait_stage) var portrait := TextureRect.new() portrait.texture = InitialCompanyArt.texture_for(recruit) portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS portrait.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE portrait_stage.add_child(portrait) var facts := VBoxContainer.new() facts.name = "DismissalReviewFacts" facts.size_flags_horizontal = Control.SIZE_EXPAND_FILL facts.add_theme_constant_override("separation", 6) body.add_child(facts) facts.add_child(_label(_tr("将离开当前 Company", "LEAVES THE CURRENT COMPANY"), _scaled(11), Color("#e8d39d"))) _add_review_detail(facts, "DismissalReviewProfession", _tr("职业", "PROFESSION"), "%s · %s" % [_recruit_profession_name(recruit), String(recruit.profession_job)]) _add_review_detail(facts, "DismissalReviewTrait", _tr("特质", "TRAIT"), _recruit_trait_name(recruit)) _add_review_detail(facts, "DismissalReviewEquipment", _tr("装备归还", "EQUIPMENT RETURN"), _tr("%s 将回到库存", "%s returns to inventory") % _recruit_item_name(recruit)) _add_review_detail(facts, "DismissalReviewGrowth", _tr("保留成长", "GROWTH RETAINED"), _tr("等级 %d · XP %d · 不扣金币", "Level %d · XP %d · no Coin refund or charge") % [int(recruit.level), int(recruit.xp)]) var consequence := _label(_tr("解雇不可撤销;至少保留一名成员。当前阶段必须仍是 Market,已锁定的队列不可解雇。", "Dismissal is irreversible; at least one member must remain. The current phase must be Market, and a locked Line cannot dismiss members."), _scaled(11), Color("#f0ca75")) consequence.name = "DismissalReviewConsequence" consequence.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART consequence.size_flags_vertical = Control.SIZE_EXPAND_FILL facts.add_child(consequence) var notice := _label(dismissal_notice, _scaled(10), Color("#ffd4d4")) notice.name = "DismissalReviewNotice" notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART facts.add_child(notice) var footer := HBoxContainer.new() footer.name = "DismissalReviewFooter" footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 52) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) var cancel := _company_button("DismissalReviewCancel", _tr("取消", "Cancel"), cancel_dismissal_review, "company_utility", "back") cancel.custom_minimum_size = Vector2(170 if size.y < 500.0 else 190, footer.custom_minimum_size.y) footer.add_child(cancel) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var confirm := _company_button("DismissalReviewConfirm", _tr("确认解雇", "Confirm Dismissal"), commit_dismissal, "company_primary", "confirm") confirm.custom_minimum_size = Vector2(170 if size.y < 500.0 else 190, footer.custom_minimum_size.y) confirm.tooltip_text = _tr("移除该成员,装备归还库存,金币不变", "Remove this member, return equipment to inventory and leave Coin unchanged") footer.add_child(confirm) confirm.call_deferred("grab_focus") func cancel_dismissal_review() -> void: dismissal_notice = "" if dismissal_return_screen == "recruit_detail": show_recruit_detail(dismissal_recruit_id) else: show_roster(guild_company) func commit_dismissal() -> void: if dismissal_recruit_id.is_empty(): return guild_contract_service.company_path = initial_company_service.company_path var operation_id := ("%s|dismiss|%s" % [String(guild_company.get("company_id", "")), dismissal_recruit_id]).sha256_text().substr(0, 32) var result := guild_contract_service.dismiss_recruit(operation_id, dismissal_recruit_id) if not result.ok: dismissal_notice = _dismissal_error_text(String(result.get("code", "DISMISS_FAILED"))) open_dismissal_review(dismissal_recruit_id) return dismissal_notice = "" guild_company = result.company.duplicate(true) if roster_selected_recruit_id == dismissal_recruit_id: roster_selected_recruit_id = String(guild_company.owned_recruits[0].recruit_id) if not guild_company.owned_recruits.is_empty() else "" dismissal_recruit_id = "" show_roster(guild_company) func open_contract_return_review() -> void: var loaded := initial_company_service.load_company() if not loaded.ok: show_title() return var contract: Dictionary = loaded.company.get("active_contract", {}) if contract.is_empty() or String(contract.get("status", "")) != "market": contract_return_notice = _tr("当前委托已不在可返回的 Market 阶段。", "The current Contract is no longer in a returnable Market phase.") show_market(loaded.company) return guild_company = loaded.company.duplicate(true) current_screen = "contract_return_review" _build_world_base() var frame_safe := _framed_page_safe("ContractReturnReview") frame_safe.add_child(_entry_chrome("ContractReturnReviewFrame", "initial_company_scene_frame")) var safe := _content_page_safe("ContractReturnReview") var layout := VBoxContainer.new() layout.name = "ContractReturnReviewLayout" layout.add_theme_constant_override("separation", 6) safe.add_child(layout) var header := HBoxContainer.new() header.custom_minimum_size = Vector2(0, 48 if size.y < 500.0 else 56) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("主动返回委托", "RETURN CONTRACT"), _scaled(21), Color("#f5ead0"))) heading.add_child(_label(_tr("风镜初渡 · 保留 Company 成长", "FIRST WINDGLASS CROSSING · RETAIN COMPANY GROWTH"), _scaled(11), Color("#e8d39d"))) var status := _label(_tr("可确认", "READY TO CONFIRM"), _scaled(11), Color("#9ed7aa")) status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(status) var body := HBoxContainer.new() body.name = "ContractReturnReviewBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 18) layout.add_child(body) var scene := VBoxContainer.new() scene.name = "ContractReturnCompanyScene" scene.size_flags_horizontal = Control.SIZE_EXPAND_FILL scene.size_flags_stretch_ratio = 1.2 scene.add_theme_constant_override("separation", 2) body.add_child(scene) scene.add_child(_label(_tr("当前远征团", "CURRENT COMPANY"), _scaled(11), Color("#e8d39d"))) for index in range(guild_company.owned_recruits.size()): var recruit: Dictionary = guild_company.owned_recruits[index] var line := HBoxContainer.new() line.name = "ContractReturnMember%d" % index line.custom_minimum_size = Vector2(0, 40 if size.y < 380.0 else (56 if size.y < 500.0 else 68)) var portrait := TextureRect.new() portrait.texture = InitialCompanyArt.texture_for(recruit) portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED portrait.custom_minimum_size = Vector2(46 if size.y < 380.0 else 58, 40 if size.y < 380.0 else (56 if size.y < 500.0 else 68)) portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE line.add_child(portrait) line.add_child(_label("%s\n%s · Lv %d · XP %d" % [_recruit_name(recruit), _recruit_profession_name(recruit), int(recruit.level), int(recruit.xp)], _scaled(11), Color("#f5ead0"))) scene.add_child(line) var facts := VBoxContainer.new() facts.name = "ContractReturnFacts" facts.size_flags_horizontal = Control.SIZE_EXPAND_FILL facts.add_theme_constant_override("separation", 6) body.add_child(facts) facts.add_child(_label(_tr("这是自愿退出,不是 Recruit 解雇。", "VOLUNTARY WITHDRAWAL, NOT RECRUIT DISMISSAL"), _scaled(11), Color("#f0ca75"))) facts.add_child(_label(_tr("保留金币:%d", "Coin retained: %d") % int(contract.coin), _scaled(13), Color("#f5ead0"))) facts.add_child(_label(_tr("成员:%d · 等级/XP、职业、特质、装备、历史全部保留。", "Members: %d · levels/XP, Professions, Traits, equipment and history are all retained.") % guild_company.owned_recruits.size(), _scaled(12), Color("#9ed7aa"))) var warning := _label(_tr("返回后当前委托状态变为 returned,不能再次进入这份 Market;下一份 Contract 属于更晚步骤。", "After returning, this Contract becomes returned and cannot re-open its Market; a later Contract belongs to a later step."), _scaled(11), Color("#e8d39d")) warning.name = "ContractReturnWarning" warning.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART warning.size_flags_vertical = Control.SIZE_EXPAND_FILL facts.add_child(warning) var notice := _label(contract_return_notice, _scaled(10), Color("#ffd4d4")) notice.name = "ContractReturnNotice" notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART facts.add_child(notice) for fact_node in facts.get_children(): if fact_node is Label: fact_node.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART fact_node.size_flags_horizontal = Control.SIZE_EXPAND_FILL var footer := HBoxContainer.new() footer.name = "ContractReturnReviewFooter" footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 52) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) var cancel := _company_button("ContractReturnCancel", _tr("取消", "Cancel"), cancel_contract_return, "company_utility", "back") cancel.custom_minimum_size = Vector2(170 if size.y < 500.0 else 190, footer.custom_minimum_size.y) footer.add_child(cancel) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var confirm := _company_button("ContractReturnConfirm", _tr("确认返回公会", "Confirm Return to Guild"), commit_contract_return, "company_primary", "confirm") confirm.custom_minimum_size = Vector2(170 if size.y < 500.0 else 190, footer.custom_minimum_size.y) confirm.tooltip_text = _tr("保留全部 Company 成长并返回公会", "Retain all Company growth and return to Guild") footer.add_child(confirm) confirm.call_deferred("grab_focus") func cancel_contract_return() -> void: contract_return_notice = "" show_market(guild_company) func commit_contract_return() -> void: guild_contract_service.company_path = initial_company_service.company_path var operation_id := ("%s|return-contract|%s" % [String(guild_company.get("company_id", "")), String(guild_company.get("active_contract", {}).get("run_id", ""))]).sha256_text().substr(0, 32) var result := guild_contract_service.return_contract(operation_id) if not result.ok: contract_return_notice = _contract_return_error_text(String(result.get("code", "CONTRACT_RETURN_FAILED"))) open_contract_return_review() return contract_return_notice = "" guild_company = result.company.duplicate(true) show_guild_handoff(guild_company) func _roster_member_button(index: int, recruit: Dictionary) -> Button: var button := _entry_button_base("RosterMember%d" % index, select_roster_recruit.bind(String(recruit.recruit_id)), "company_primary" if String(recruit.recruit_id) == roster_selected_recruit_id else "company_secondary") button.set_meta("recruit_id", String(recruit.recruit_id)) button.custom_minimum_size = Vector2(0, 48 if size.y < 380.0 else (62 if size.y < 500.0 else 72)) var portrait := TextureRect.new() portrait.texture = InitialCompanyArt.texture_for(recruit) portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED portrait.position = Vector2(8, 3) portrait.size = Vector2(42 if size.y < 380.0 else 52, 42 if size.y < 380.0 else 54) portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(portrait) var name := _label(_recruit_name(recruit), _scaled(14), Color("#f5ead0")) name.position = Vector2(58 if size.y < 380.0 else 70, 3 if size.y < 380.0 else 6) name.size = Vector2(230, 20 if size.y < 380.0 else 24) name.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(name) var facts := _label(_tr("%s · 等级 %d · XP %d", "%s · Level %d · XP %d") % [_recruit_profession_name(recruit), int(recruit.level), int(recruit.xp)], _scaled(10), Color("#aebbc2")) facts.position = Vector2(58 if size.y < 380.0 else 70, 24 if size.y < 380.0 else 31) facts.size = Vector2(290, 20 if size.y < 380.0 else 24) facts.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS facts.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(facts) return button func select_roster_recruit(recruit_id: String) -> void: roster_selected_recruit_id = recruit_id show_roster(guild_company) func _render_roster_detail(parent: VBoxContainer, company: Dictionary, recruit_id: String) -> void: var recruit := _recruit_by_id(company, recruit_id) if recruit.is_empty(): parent.add_child(_label(_tr("选择一名成员查看详情。", "Select a Recruit to inspect detail."), _scaled(13), Color("#f5ead0"))) return parent.add_child(_label(_tr("已选成员", "SELECTED RECRUIT"), _scaled(10), Color("#e8d39d"))) parent.add_child(_label(_recruit_name(recruit), _scaled(18), Color("#f0ca75"))) parent.add_child(_label(_tr("%s · 等级 %d · XP %d", "%s · Level %d · XP %d") % [_recruit_profession_name(recruit), int(recruit.level), int(recruit.xp)], _scaled(11), Color("#f5ead0"))) parent.add_child(_label(_tr("当前装备:%s", "Equipped: %s") % _recruit_item_name(recruit), _scaled(11), Color("#9ed7aa"))) var history: Dictionary = recruit.get("battle_history", {}) var history_copy := _label(_tr("参战 %d · 胜利 %d · 击倒 %d", "Entered %d · Victories %d · Knockouts %d") % [int(history.get("battles_entered", 0)), int(history.get("victories", 0)), int(history.get("knockouts", 0))], _scaled(10), Color("#aebbc2")) history_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART history_copy.size_flags_vertical = Control.SIZE_EXPAND_FILL parent.add_child(history_copy) func show_contract_choice() -> void: var loaded := initial_company_service.load_company() if not loaded.ok: contract_notice = _tr("无法读取远征团。", "The Company could not be loaded.") + " [%s]" % String(loaded.code) show_guild_handoff() return if not loaded.company.active_contract.is_empty(): show_market_handoff(loaded.company) return guild_company = loaded.company.duplicate(true) current_screen = "contract_choice" _build_world_base(true) var frame_safe := _framed_page_safe("ContractChoice") frame_safe.add_child(_entry_chrome("ContractChoiceFrame", "content_scene_frame")) var safe := _content_page_safe("ContractChoice") var layout := VBoxContainer.new() layout.name = "ContractChoiceLayout" layout.add_theme_constant_override("separation", 6) safe.add_child(layout) var header := HBoxContainer.new() header.custom_minimum_size = Vector2(0, 48) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("委托板", "CONTRACT BOARD"), _scaled(21), Color("#f5ead0"))) heading.add_child(_label(_tr("风镜原野 · 首次远征", "WINDGLASS VERGE · FIRST EXPEDITION"), _scaled(10), Color("#e8d39d"))) var company_status := _label(_tr("远征团 2 · 持续保留", "COMPANY 2 · PERSISTENT"), _scaled(11), Color("#73b9e7")) company_status.name = "ContractCompanyStatus" company_status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(company_status) var body := HBoxContainer.new() body.name = "ContractChoiceBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 18) layout.add_child(body) var scene := VBoxContainer.new() scene.name = "ContractBoardScene" scene.size_flags_horizontal = Control.SIZE_EXPAND_FILL scene.size_flags_stretch_ratio = 1.25 scene.add_theme_constant_override("separation", 2) body.add_child(scene) scene.add_child(_label(_tr("01 · 风镜初渡", "01 · FIRST WINDGLASS CROSSING"), _scaled(16), Color("#f5ead0"))) var route := _label(_tr("穿越悬浮风镜,建立公会的第一条安全通路。", "Cross the suspended windglass and establish the Guild's first safe route."), _scaled(11), Color("#e8d39d")) route.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART scene.add_child(route) scene.add_child(_contract_company_scene(guild_company.owned_recruits)) var review := VBoxContainer.new() review.name = "ContractReview" review.custom_minimum_size = Vector2(minf(360.0, size.x * 0.42), 0) review.add_theme_constant_override("separation", 5) body.add_child(review) review.add_child(_label(_tr("选择难度", "CHOOSE DIFFICULTY"), _scaled(11), Color("#e8d39d"))) var segments := HBoxContainer.new() segments.name = "ContractDifficultySegments" segments.custom_minimum_size = Vector2(0, 44) segments.add_theme_constant_override("separation", 3) review.add_child(segments) for difficulty in GuildContractService.DIFFICULTIES: segments.add_child(_contract_segment_button(String(difficulty))) var difficulty_copy := _label(_difficulty_copy(contract_difficulty), _scaled(11), Color("#f5ead0")) difficulty_copy.name = "ContractDifficultyCopy" difficulty_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART difficulty_copy.size_flags_vertical = Control.SIZE_EXPAND_FILL review.add_child(difficulty_copy) var facts := _label(_tr("建立后:52 金币 · 队列容量 4\n市场:3 名招募 · 4 件装备 · 3 条战斗路线", "On creation: 52 Coin · Line capacity 4\nMarket: 3 Recruits · 4 equipment · 3 battle routes"), _scaled(11), Color("#9ed7aa")) facts.name = "ContractCreationFacts" facts.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART review.add_child(facts) var notice := _label(contract_notice, _scaled(10), Color("#f0ca75")) notice.name = "ContractNotice" notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART review.add_child(notice) var footer := HBoxContainer.new() footer.name = "ContractChoiceFooter" footer.custom_minimum_size = Vector2(0, 52) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) footer.add_child(_company_button("ContractCancel", _tr("返回公会", "Back to Guild"), back_to_guild, "company_utility", "back")) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var confirm := _company_button("ContractConfirm", _tr("建立委托", "Create Contract"), confirm_contract_choice, "content_primary", "confirm") confirm.custom_minimum_size = Vector2(230, 52) confirm.tooltip_text = _tr("确认风镜初渡、当前难度、52 金币与队列容量 4", "Confirm First Windglass Crossing, current difficulty, 52 Coin and Line capacity 4") footer.add_child(confirm) segments.get_child(GuildContractService.DIFFICULTIES.find(contract_difficulty)).call_deferred("grab_focus") func select_contract_difficulty(value: String) -> void: if value not in GuildContractService.DIFFICULTIES: return contract_difficulty = value contract_notice = "" show_contract_choice() func confirm_contract_choice() -> void: guild_contract_service.company_path = initial_company_service.company_path var operation_id := ("%s|first-contract|%s" % [String(guild_company.get("company_id", "")), contract_difficulty]).sha256_text().substr(0, 32) var created: Dictionary = guild_contract_service.create_contract(operation_id, GuildContractService.FIRST_CONTRACT_ID, contract_difficulty) if not created.ok: contract_notice = _contract_error_text(String(created.get("code", "CONTRACT_CREATE_FAILED"))) show_contract_choice() return contract_notice = "" guild_company = created.company.duplicate(true) show_market_handoff(guild_company) func show_market_handoff(company := {}) -> void: show_market(company) func show_market(company := {}) -> void: var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else {} if resolved.is_empty(): var loaded := initial_company_service.load_company() if not loaded.ok: title_notice = _tr("无法读取远征团。", "The Company could not be loaded.") + " [%s]" % String(loaded.code) show_title() return resolved = loaded.company.duplicate(true) if not resolved.has("active_contract") or resolved.active_contract.is_empty(): show_guild_handoff(resolved) return guild_company = resolved current_screen = "market" _build_world_base(true) var frame_safe := _framed_page_safe("Market") frame_safe.add_child(_entry_chrome("MarketFrame", "content_scene_frame")) var safe := _content_page_safe("Market") var layout := VBoxContainer.new() layout.name = "MarketLayout" layout.add_theme_constant_override("separation", 6) safe.add_child(layout) var contract: Dictionary = resolved.active_contract var next_stage_market: bool = not contract.get("recovery_receipts", []).is_empty() var short_market := size.y < 380.0 var header := HBoxContainer.new() header.custom_minimum_size = Vector2(0, 40 if short_market else 44) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("下一阶段市场", "NEXT-STAGE MARKET") if next_stage_market else _tr("首次市场", "FIRST MARKET"), _scaled(21), Color("#f5ead0"))) heading.add_child(_label(_tr("战后归队 · 稳定货单", "POST-BATTLE RETURN · STABLE LEDGER") if next_stage_market else _tr("风镜初渡 · 稳定货单", "FIRST WINDGLASS CROSSING · STABLE LEDGER"), _scaled(10), Color("#e8d39d"))) var company_line := _label(_tr("远征团 %d | 队列容量 %d", "Company %d | Line capacity %d") % [resolved.owned_recruits.size(), int(contract.line_capacity)], _scaled(12), Color("#73b9e7")) company_line.name = "MarketCompanyStatus" company_line.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(company_line) var coin := _label(_tr("%d 金币", "%d COIN") % int(contract.coin), _scaled(14), Color("#f0ca75")) coin.name = "MarketCoinStatus" coin.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(coin) var body := HBoxContainer.new() body.name = "MarketBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 14) layout.add_child(body) var ledger := VBoxContainer.new() ledger.name = "MarketLedger" ledger.size_flags_horizontal = Control.SIZE_EXPAND_FILL ledger.size_flags_stretch_ratio = 1.35 ledger.add_theme_constant_override("separation", 4) body.add_child(ledger) var categories := HBoxContainer.new() categories.name = "MarketCategories" categories.custom_minimum_size = Vector2(0, 44) categories.add_theme_constant_override("separation", 4) ledger.add_child(categories) for category in ["recruit", "equipment", "battle"]: categories.add_child(_market_category_button(category)) var offers := VBoxContainer.new() offers.name = "MarketOfferList" offers.size_flags_vertical = Control.SIZE_EXPAND_FILL offers.add_theme_constant_override("separation", 3) ledger.add_child(offers) var rows: Array = contract.market_board.get(_market_collection_field(market_category), []) for index in range(rows.size()): offers.add_child(_market_offer_row(market_category, rows[index], index)) var context := VBoxContainer.new() context.name = "MarketContext" context.custom_minimum_size = Vector2(minf(350.0, size.x * 0.40), 0) context.add_theme_constant_override("separation", 5) body.add_child(context) _render_market_context(context, contract) var footer := HBoxContainer.new() footer.name = "MarketFooter" footer.custom_minimum_size = Vector2(0, 44 if short_market else 48) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) var back := _company_button("MarketBackGuild", _tr("返回公会", "Back to Guild"), back_to_guild, "company_utility", "back") back.custom_minimum_size = Vector2(180, 44 if short_market else 48) footer.add_child(back) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var title := _company_button("MarketTitle", _tr("标题首页", "Title"), back_to_title, "company_secondary", "home") title.custom_minimum_size = Vector2(150 if short_market else 180, 44 if short_market else 48) footer.add_child(title) var return_contract := _company_button("MarketReturnContract", _tr("返回委托", "Return Contract"), open_contract_return_review, "company_secondary", "back") return_contract.custom_minimum_size = Vector2(170 if short_market else 190, 44 if short_market else 48) return_contract.tooltip_text = _tr("主动结束当前委托;金币、成员、等级、装备与历史均保留", "Voluntarily leave this Contract; Coin, members, levels, equipment and history are retained") footer.add_child(return_contract) var formation_available: bool = contract.hire_receipts.size() == 2 and resolved.owned_recruits.size() == int(contract.line_capacity) var formation := _company_button( "MarketFormation", _tr("建立队列", "Establish Line"), open_party_line, "company_primary", "confirm" ) formation.custom_minimum_size = Vector2(160 if short_market else 190, 44 if short_market else 48) _set_entry_button_disabled(formation, not formation_available) formation.tooltip_text = ( _tr("四名成员已就位;进入自动编队", "Four members are ready; enter automatic formation") if formation_available else _tr("需要完成两笔独立招募并达到远征团 4/4", "Complete both separate hires and reach Company 4/4 first") ) footer.add_child(formation) var focus_target := find_child("MarketOffer%s" % market_selected_offer_id, true, false) as Control if focus_target != null: focus_target.call_deferred("grab_focus") else: categories.get_child(_market_categories().find(market_category)).call_deferred("grab_focus") func open_party_line() -> void: show_party_line() func show_party_line(company := {}) -> void: formation_risk_service.company_path = initial_company_service.company_path var begun: Dictionary = formation_risk_service.begin_formation() if not begun.ok: market_notice = _formation_error_text(String(begun.get("code", "FORMATION_FAILED"))) show_market(company) return var resolved: Dictionary = begun.company.duplicate(true) var formation: Dictionary = resolved.active_contract.formation if party_draft_order.is_empty() or not _same_string_members(party_draft_order, formation.base_front_to_rear_ids): party_draft_order.assign(formation.saved_front_to_rear_ids if not formation.saved_front_to_rear_ids.is_empty() else formation.draft_front_to_rear_ids) party_undo_order.clear() party_selected_recruit_id = "" party_notice = "" var previewed: Dictionary = formation_risk_service.preview_order(party_draft_order) if not previewed.ok: party_draft_order.assign(formation.base_front_to_rear_ids) previewed = formation_risk_service.preview_order(party_draft_order) party_preview = previewed guild_company = resolved current_screen = "party_line" _build_world_base() var frame_safe := _framed_page_safe("PartyLine") frame_safe.add_child(_entry_chrome("PartyLineFrame", "initial_company_scene_frame")) var safe := _content_page_safe("PartyLine") var layout := VBoxContainer.new() layout.name = "PartyLineLayout" layout.add_theme_constant_override("separation", 5) safe.add_child(layout) var header := HBoxContainer.new() header.name = "PartyLineHeader" header.custom_minimum_size = Vector2(0, 42 if size.y < 500.0 else 50) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("自动编队", "AUTOMATIC FORMATION"), _scaled(20), Color("#f5ead0"))) heading.add_child(_label(_tr("点选成员,再点目标位置交换。无需拖动。", "Select a member, then a destination to swap. Drag is not required."), _scaled(10), Color("#e8d39d"))) var status := _label(_tr("后排在左 · 0 号前排在右", "REAR LEFT · FRONT POSITION 0 RIGHT"), _scaled(11), Color("#73b9e7")) status.name = "PartyOrientation" status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(status) var body := HBoxContainer.new() body.name = "PartyLineBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 14) layout.add_child(body) var stage := VBoxContainer.new() stage.name = "PartyLineStage" stage.size_flags_horizontal = Control.SIZE_EXPAND_FILL stage.size_flags_stretch_ratio = 1.55 stage.add_theme_constant_override("separation", 2) body.add_child(stage) var threat := _label(_first_threat_text(formation.first_threat), _scaled(10), Color("#f0ca75")) threat.name = "PartyFirstThreat" threat.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT stage.add_child(threat) var cord := HBoxContainer.new() cord.name = "PartyCord" cord.size_flags_vertical = Control.SIZE_EXPAND_FILL cord.add_theme_constant_override("separation", 4) stage.add_child(cord) for visual_index in range(party_draft_order.size() - 1, -1, -1): var recruit_id := String(party_draft_order[visual_index]) cord.add_child(_party_member_button(visual_index, _recruit_by_id(resolved, recruit_id))) var endpoints := HBoxContainer.new() endpoints.name = "PartyEndpoints" stage.add_child(endpoints) var rear := _label(_tr("◁ 受保护后排", "< PROTECTED REAR"), _scaled(10), Color("#9ed7aa")) rear.size_flags_horizontal = Control.SIZE_EXPAND_FILL endpoints.add_child(rear) var front := _label(_tr("暴露前排 ▷", "EXPOSED FRONT >"), _scaled(10), Color("#f0ca75")) front.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT front.size_flags_horizontal = Control.SIZE_EXPAND_FILL endpoints.add_child(front) var context := VBoxContainer.new() context.name = "PartyContext" context.custom_minimum_size = Vector2(minf(300.0, size.x * 0.34), 0) context.add_theme_constant_override("separation", 4) body.add_child(context) _render_party_context(context, resolved) var footer := HBoxContainer.new() footer.name = "PartyLineFooter" footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 52) footer.add_theme_constant_override("separation", 7) layout.add_child(footer) footer.add_child(_company_button("PartyBack", _tr("返回公会", "Back to Guild"), cancel_party_line, "company_utility", "back")) var undo := _company_button("PartyUndo", _tr("撤销", "Undo"), undo_party_swap, "company_secondary", "undo") undo.custom_minimum_size = Vector2(116, footer.custom_minimum_size.y) _set_entry_button_disabled(undo, party_undo_order.is_empty()) undo.tooltip_text = _tr("撤销上一次位置交换", "Undo the last position swap") if not undo.disabled else _tr("还没有可撤销的位置交换", "No position swap to undo") footer.add_child(undo) var reset := _company_button("PartyReset", _tr("重置", "Reset"), reset_party_order, "company_secondary", "reset") reset.custom_minimum_size = Vector2(116, footer.custom_minimum_size.y) _set_entry_button_disabled(reset, party_draft_order == formation.base_front_to_rear_ids) reset.tooltip_text = _tr("恢复系统初始顺序", "Restore the automatic starting order") if not reset.disabled else _tr("当前已是系统初始顺序", "The automatic starting order is already restored") footer.add_child(reset) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var review := _company_button("PartySaveReview", _tr("保存并检查", "Save & Review"), save_party_order, "company_primary", "confirm") review.custom_minimum_size = Vector2(210, footer.custom_minimum_size.y) review.tooltip_text = _tr("保存当前四人顺序并打开准备检查;不会开始战斗", "Save the current four-member order and open readiness review; battle will not start") footer.add_child(review) var selected_button := _party_button_for_recruit(party_selected_recruit_id) if selected_button != null: selected_button.call_deferred("grab_focus") else: var front_button := find_child("PartyPosition0", true, false) as Button if front_button != null: front_button.call_deferred("grab_focus") func select_party_member(recruit_id: String) -> void: var position := party_draft_order.find(recruit_id) if position < 0: party_notice = _tr("该成员已不在当前队列。", "That member is no longer in the current line.") show_party_line(guild_company) return if party_selected_recruit_id.is_empty(): party_selected_recruit_id = recruit_id party_notice = _tr("已选择 %s;再点一名成员交换位置。", "%s selected; choose another member to swap positions.") % _recruit_name(_recruit_by_id(guild_company, recruit_id)) elif party_selected_recruit_id == recruit_id: party_selected_recruit_id = "" party_notice = _tr("已取消位置选择。", "Position selection cleared.") else: var from_position := party_draft_order.find(party_selected_recruit_id) if from_position < 0: party_selected_recruit_id = "" party_notice = _tr("原选择已失效,请重试。", "The previous selection became stale; try again.") else: party_undo_order.assign(party_draft_order) var moved_id := String(party_selected_recruit_id) party_draft_order[from_position] = recruit_id party_draft_order[position] = moved_id party_selected_recruit_id = moved_id party_notice = _tr("已预览交换:%d 号 ↔ %d 号。首要目标与保护关系已更新。", "Previewed swap: position %d <-> %d. Primary target and protection are updated.") % [from_position, position] show_party_line(guild_company) func undo_party_swap() -> void: if party_undo_order.is_empty(): party_notice = _tr("还没有可撤销的位置交换。", "No position swap is available to undo.") else: var current := party_draft_order.duplicate() party_draft_order.assign(party_undo_order) party_undo_order.assign(current) party_selected_recruit_id = "" party_notice = _tr("已撤销上一次位置交换。", "The last position swap was undone.") show_party_line(guild_company) func reset_party_order() -> void: formation_risk_service.company_path = initial_company_service.company_path var reset: Dictionary = formation_risk_service.reset_order() if not reset.ok: party_notice = _formation_error_text(String(reset.get("code", "FORMATION_RESET_FAILED"))) else: party_undo_order.assign(party_draft_order) party_draft_order.assign(reset.front_to_rear_ids) party_selected_recruit_id = "" party_notice = _tr("已恢复系统初始顺序。", "Automatic starting order restored.") show_party_line(guild_company) func cancel_party_line() -> void: party_draft_order.clear() party_undo_order.clear() party_selected_recruit_id = "" party_preview = {} party_notice = "" back_to_guild() func save_party_order() -> void: formation_risk_service.company_path = initial_company_service.company_path var order_hash := JSON.stringify(_canonical_value(party_draft_order)).sha256_text() var operation_id := ("%s|save-party-order|%s" % [String(guild_company.company_id), order_hash]).sha256_text().substr(0, 32) var saved: Dictionary = formation_risk_service.save_order(operation_id, party_draft_order) if not saved.ok: party_notice = _formation_error_text(String(saved.get("code", "FORMATION_SAVE_FAILED"))) show_party_line(guild_company) return guild_company = saved.company.duplicate(true) party_notice = "" show_readiness_review(guild_company) func show_readiness_review(company := {}) -> void: formation_risk_service.company_path = initial_company_service.company_path var loaded: Dictionary = formation_risk_service.load_company() if not loaded.ok: party_notice = _formation_error_text(String(loaded.get("code", "READINESS_LOAD_FAILED"))) show_party_line(company) return var resolved: Dictionary = loaded.company.duplicate(true) var formation: Dictionary = resolved.active_contract.get("formation", {}) if formation.is_empty() or formation.saved_front_to_rear_ids.is_empty(): party_notice = _tr("请先保存当前队列顺序。", "Save the current line order first.") show_party_line(resolved) return guild_company = resolved party_draft_order.assign(formation.saved_front_to_rear_ids) party_preview = { "relations": Array(formation.relations).duplicate(true), "coverage": Dictionary(formation.coverage).duplicate(true), } current_screen = "readiness_review" _build_world_base() var frame_safe := _framed_page_safe("Readiness") frame_safe.add_child(_entry_chrome("ReadinessFrame", "initial_company_scene_frame")) var safe := _content_page_safe("Readiness") var layout := VBoxContainer.new() layout.name = "ReadinessLayout" layout.add_theme_constant_override("separation", 5) safe.add_child(layout) var header := HBoxContainer.new() header.custom_minimum_size = Vector2(0, 42 if size.y < 500.0 else 50) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("准备检查", "READINESS REVIEW"), _scaled(20), Color("#f5ead0"))) heading.add_child(_label(_tr("确认队列事实,不预测胜负。", "Confirm party facts without predicting the outcome."), _scaled(10), Color("#e8d39d"))) var state := _label( _tr("准备已接受", "PREPARATION ACCEPTED") if String(formation.status) == "ready" else _tr("已保存 · 尚未接受", "SAVED · NOT YET ACCEPTED"), _scaled(11), Color("#9ed7aa") if String(formation.status) == "ready" else Color("#f0ca75") ) state.name = "ReadinessState" state.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(state) var body := HBoxContainer.new() body.name = "ReadinessBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 14) layout.add_child(body) var line := VBoxContainer.new() line.name = "ReadinessLineArea" line.size_flags_horizontal = Control.SIZE_EXPAND_FILL line.size_flags_stretch_ratio = 1.45 line.add_theme_constant_override("separation", 3) body.add_child(line) line.add_child(_label(_tr("受保护后排 ◁────────▷ 暴露前排 · 0 号", "PROTECTED REAR <--------> EXPOSED FRONT · POSITION 0"), _scaled(10), Color("#73b9e7"))) var members := HBoxContainer.new() members.name = "ReadinessMembers" members.size_flags_vertical = Control.SIZE_EXPAND_FILL members.add_theme_constant_override("separation", 4) line.add_child(members) for visual_index in range(party_draft_order.size() - 1, -1, -1): members.add_child(_readiness_member(visual_index, _recruit_by_id(resolved, String(party_draft_order[visual_index])))) var detail := VBoxContainer.new() detail.name = "ReadinessFacts" detail.custom_minimum_size = Vector2(minf(315.0, size.x * 0.35), 0) detail.add_theme_constant_override("separation", 4) body.add_child(detail) detail.add_child(_label(_tr("已冻结事实", "FROZEN FACTS"), _scaled(10), Color("#e8d39d"))) var front_recruit := _recruit_by_id(resolved, String(party_draft_order[0])) var coverage: Dictionary = formation.coverage var facts := _label( _tr( "0 号首要目标:%s\n保护 %d · 远程 %d · 续航 %d · 控制 %d\n四名成员同时出战;没有自动替补。", "Primary target at position 0: %s\nGuard %d · Reach %d · Sustain %d · Control %d\nAll four deploy together; no automatic Reserve entry." ) % [_recruit_name(front_recruit), int(coverage.protection), int(coverage.long_reach), int(coverage.sustain), int(coverage.control)], _scaled(11), Color("#f5ead0") ) facts.name = "ReadinessFrozenFacts" facts.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART facts.size_flags_vertical = Control.SIZE_EXPAND_FILL detail.add_child(facts) var warning := _label(_readiness_warning(formation), _scaled(10), Color("#f0ca75")) warning.name = "ReadinessWarning" warning.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART detail.add_child(warning) var knockout := _label(_tr("击倒只离开当前战斗;同一成员下关归队,不会永久死亡。", "Knockout removes a member only from this battle; the same Recruit returns next stage and never dies permanently."), _scaled(9), Color("#9ed7aa")) knockout.name = "ReadinessKnockoutRule" knockout.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART detail.add_child(knockout) if String(formation.status) == "ready": var receipt := _label(_tr("准备回执已保存 · 风险选择尚未提交", "Readiness receipt saved · no risk has been committed"), _scaled(9), Color("#73b9e7")) receipt.name = "ReadinessReceipt" detail.add_child(receipt) var footer := HBoxContainer.new() footer.name = "ReadinessFooter" footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 52) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) footer.add_child(_company_button("ReadinessRevise", _tr("修改队列", "Revise Line"), revise_readiness, "company_utility", "back")) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) if String(formation.status) == "ready": var risk := _company_button("ReadinessRisk", _tr("比较风险", "Compare Risk"), open_risk_board, "company_primary", "confirm") risk.custom_minimum_size = Vector2(220, footer.custom_minimum_size.y) risk.tooltip_text = _tr("比较首个委托的三种压力与回报", "Compare three pressure and reward options for the first Contract") footer.add_child(risk) else: var accept := _company_button("ReadinessAccept", _tr("接受准备", "Accept Preparation"), accept_readiness, "company_primary", "confirm") accept.custom_minimum_size = Vector2(230, footer.custom_minimum_size.y) accept.tooltip_text = _tr("冻结当前四人顺序与关系;不会选择风险或开始战斗", "Freeze this four-member order and its relations; no risk or battle starts") footer.add_child(accept) var focus := find_child("ReadinessRisk" if String(formation.status) == "ready" else "ReadinessAccept", true, false) as Control if focus != null and not bool(focus.get("disabled")): focus.call_deferred("grab_focus") else: find_child("ReadinessRevise", true, false).call_deferred("grab_focus") func revise_readiness() -> void: party_selected_recruit_id = "" party_undo_order.clear() party_notice = _tr("已加载保存的顺序;修改后需要再次保存并接受。", "Saved order loaded; save and accept again after any change.") show_party_line(guild_company) func accept_readiness() -> void: formation_risk_service.company_path = initial_company_service.company_path var formation: Dictionary = guild_company.active_contract.formation var order_hash := JSON.stringify(_canonical_value(formation.saved_front_to_rear_ids)).sha256_text() var operation_id := ("%s|accept-readiness|%s" % [String(guild_company.company_id), order_hash]).sha256_text().substr(0, 32) var accepted: Dictionary = formation_risk_service.accept_readiness(operation_id, order_hash) if not accepted.ok: party_notice = _formation_error_text(String(accepted.get("code", "READINESS_FAILED"))) show_readiness_review(guild_company) return guild_company = accepted.company.duplicate(true) show_readiness_review(guild_company) func open_risk_board() -> void: show_risk_board(guild_company) func show_risk_board(company := {}) -> void: formation_risk_service.company_path = initial_company_service.company_path var loaded: Dictionary = formation_risk_service.load_company() if not loaded.ok: party_notice = _formation_error_text(String(loaded.get("code", "RISK_LOAD_FAILED"))) show_readiness_review(company) return var resolved: Dictionary = loaded.company.duplicate(true) var formation: Dictionary = resolved.active_contract.get("formation", {}) if formation.is_empty() or String(formation.get("status", "draft")) != "ready": party_notice = _tr("请先接受准备检查。", "Accept the readiness review first.") show_readiness_review(resolved) return guild_company = resolved current_screen = "risk_board" _build_world_base() var frame_safe := _framed_page_safe("RiskBoard") frame_safe.add_child(_entry_chrome("RiskBoardFrame", "initial_company_scene_frame")) var safe := _content_page_safe("RiskBoard") var layout := VBoxContainer.new() layout.name = "RiskBoardLayout" layout.add_theme_constant_override("separation", 5) safe.add_child(layout) var header := HBoxContainer.new() header.custom_minimum_size = Vector2(0, 42 if size.y < 500.0 else 50) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("风险板", "RISK BOARD"), _scaled(20), Color("#f5ead0"))) heading.add_child(_label(_tr("选择只用于比较;没有推荐答案。", "Selection is for comparison only; nothing is recommended."), _scaled(10), Color("#e8d39d"))) var party_status := _label(_tr("四人准备已冻结", "FOUR-MEMBER PARTY FROZEN"), _scaled(11), Color("#9ed7aa")) party_status.name = "RiskPartyStatus" party_status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(party_status) var body := HBoxContainer.new() body.name = "RiskBoardBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 14) layout.add_child(body) var band := VBoxContainer.new() band.name = "RiskOptionBand" band.size_flags_horizontal = Control.SIZE_EXPAND_FILL band.size_flags_stretch_ratio = 1.35 band.add_theme_constant_override("separation", 4) body.add_child(band) band.add_child(_label(_tr("压力比较带", "PRESSURE COMPARISON BAND"), _scaled(10), Color("#e8d39d"))) var options: Array = formation.risk_options for option in options: band.add_child(_risk_option_button(option)) var band_note := _label(_tr("已知威胁 · 未解析信息 · 入场成本 · 回报 · 恢复暴露", "Known threat · uncertainty · entry cost · reward · recovery exposure"), _scaled(9), Color("#73b9e7")) band_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART band.add_child(band_note) var context := VBoxContainer.new() context.name = "RiskContext" context.custom_minimum_size = Vector2(minf(320.0, size.x * 0.36), 0) context.add_theme_constant_override("separation", 4) body.add_child(context) _render_risk_context(context, formation) var footer := HBoxContainer.new() footer.name = "RiskBoardFooter" footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 52) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) footer.add_child(_company_button("RiskBackReadiness", _tr("返回准备", "Readiness"), back_to_readiness, "company_utility", "back")) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var review := _company_button("RiskReview", _tr("检查选项", "Review Option"), open_risk_commitment, "company_primary", "inspect") review.custom_minimum_size = Vector2(210, footer.custom_minimum_size.y) _set_entry_button_disabled(review, risk_selected_id.is_empty()) review.tooltip_text = _tr("先选择一条风险选项", "Select a risk option first") if risk_selected_id.is_empty() else _tr("查看选项与冻结的四人准备", "Review the option with the frozen four-member preparation") footer.add_child(review) var focus := find_child("RiskOption%s" % risk_selected_id, true, false) as Button if focus != null: focus.call_deferred("grab_focus") else: band.get_child(1).call_deferred("grab_focus") func select_risk_option(option_id: String) -> void: risk_selected_id = option_id show_risk_board(guild_company) func open_risk_commitment() -> void: if risk_selected_id.is_empty(): show_risk_board(guild_company) return show_risk_commitment(guild_company) func show_risk_commitment(company := {}) -> void: formation_risk_service.company_path = initial_company_service.company_path var loaded: Dictionary = formation_risk_service.load_company() if not loaded.ok: show_readiness_review(company) return var resolved: Dictionary = loaded.company.duplicate(true) var formation: Dictionary = resolved.active_contract.get("formation", {}) var option := _risk_option_by_id(formation, risk_selected_id) if option.is_empty(): party_notice = _tr("该风险选项已失效,请重新检查风险板。", "That risk option is stale; review the Risk Board again.") show_risk_board(resolved) return guild_company = resolved current_screen = "risk_commitment" _build_world_base() var frame_safe := _framed_page_safe("RiskCommitment") frame_safe.add_child(_entry_chrome("RiskCommitmentFrame", "initial_company_scene_frame")) var safe := _content_page_safe("RiskCommitment") var layout := VBoxContainer.new() layout.name = "RiskCommitmentLayout" layout.add_theme_constant_override("separation", 5) safe.add_child(layout) var heading := VBoxContainer.new() heading.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 54) layout.add_child(heading) heading.add_child(_label(_tr("风险确认", "RISK COMMITMENT"), _scaled(20), Color("#f5ead0"))) heading.add_child(_label(_tr("开始战斗前,确认已知压力与恢复暴露。", "Confirm known pressure and recovery exposure before battle."), _scaled(10), Color("#e8d39d"))) var body := HBoxContainer.new() body.name = "RiskCommitmentBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 14) layout.add_child(body) var scene := VBoxContainer.new() scene.name = "RiskCommitmentScene" scene.size_flags_horizontal = Control.SIZE_EXPAND_FILL scene.size_flags_stretch_ratio = 1.15 scene.add_theme_constant_override("separation", 3) body.add_child(scene) scene.add_child(_label(_risk_title(option), _scaled(17), Color("#f0ca75"))) scene.add_child(_label(_risk_threat_copy(option), _scaled(11), Color("#f5ead0"))) scene.add_child(_label(_tr("冻结队列", "FROZEN PARTY"), _scaled(10), Color("#e8d39d"))) var members := HBoxContainer.new() members.name = "RiskFrozenMembers" members.size_flags_vertical = Control.SIZE_EXPAND_FILL members.add_theme_constant_override("separation", 2) scene.add_child(members) var party_ids: Array = Array(formation.accepted_party_snapshot.front_to_rear_ids) for visual_index in range(party_ids.size() - 1, -1, -1): members.add_child(_readiness_member(visual_index, _recruit_by_id(resolved, String(party_ids[visual_index])))) var facts := VBoxContainer.new() facts.name = "RiskCommitmentFacts" facts.custom_minimum_size = Vector2(minf(335.0, size.x * 0.37), 0) facts.add_theme_constant_override("separation", 4) body.add_child(facts) facts.add_child(_label(_tr("已知后果", "KNOWN CONSEQUENCES"), _scaled(10), Color("#e8d39d"))) var detail := _label(_risk_detail_copy(option), _scaled(11), Color("#f5ead0")) detail.name = "RiskCommitmentDetail" detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART detail.size_flags_vertical = Control.SIZE_EXPAND_FILL facts.add_child(detail) var recovery := _label(_tr("击倒回执:成员只离开当前战斗,下关以同一身份归队。", "Knockout receipt: a member leaves only this battle and returns next stage with the same identity."), _scaled(9), Color("#9ed7aa")) recovery.name = "RiskRecoveryRule" recovery.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART facts.add_child(recovery) var reason := _label(_tr("自动战斗:可暂停与切换速度;没有战斗指令。", "Automatic battle: pause or change speed; no combat commands."), _scaled(9), Color("#e8d39d")) reason.name = "RiskBeginBattleReason" reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART facts.add_child(reason) var footer := HBoxContainer.new() footer.name = "RiskCommitmentFooter" footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 52) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) footer.add_child(_company_button("RiskCommitmentCancel", _tr("返回风险板", "Risk Board"), cancel_risk_commitment, "company_utility", "back")) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var begin := _company_button("RiskBeginBattle", _tr("开始战斗", "Begin Battle"), begin_battle, "company_primary", "confirm") begin.custom_minimum_size = Vector2(220, footer.custom_minimum_size.y) begin.tooltip_text = _tr("创建一次战斗回执并开始自动观察", "Create one battle receipt and begin automatic observation") footer.add_child(begin) find_child("RiskCommitmentCancel", true, false).call_deferred("grab_focus") func back_to_readiness() -> void: show_readiness_review(guild_company) func cancel_risk_commitment() -> void: show_risk_board(guild_company) func begin_battle() -> void: if risk_selected_id.is_empty(): show_risk_board(guild_company) return battle_service.company_path = initial_company_service.company_path var loaded: Dictionary = battle_service.load_company() if not loaded.ok: party_notice = _battle_error_text(String(loaded.get("code", "BATTLE_LOAD_FAILED"))) show_readiness_review(guild_company) return var operation_id := ("%s|battle|%s" % [String(loaded.company.company_id), risk_selected_id]).sha256_text().substr(0, 32) var started: Dictionary = battle_service.begin_battle(operation_id, risk_selected_id) if not started.ok: party_notice = _battle_error_text(String(started.get("code", "BATTLE_START_FAILED"))) show_risk_commitment(loaded.company) return battle_operation_id = operation_id battle_paused = false battle_speed = 1.0 battle_notice = "" guild_company = started.company.duplicate(true) show_battle(guild_company) func show_battle(company := {}, auto_play := true) -> void: _clear_battle_playback_timer() battle_playback_generation += 1 var playback_generation := battle_playback_generation battle_service.company_path = initial_company_service.company_path var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else {} if resolved.is_empty(): var loaded: Dictionary = battle_service.load_company() if not loaded.ok: party_notice = _battle_error_text(String(loaded.get("code", "BATTLE_LOAD_FAILED"))) show_readiness_review(guild_company) return resolved = loaded.company.duplicate(true) var battle: Dictionary = resolved.active_contract.get("battle_state", {}) if battle.is_empty(): party_notice = _tr("没有可恢复的战斗回执。", "No resumable battle receipt exists.") show_readiness_review(resolved) return if String(battle.status) == "result": show_battle_result(resolved) return battle_operation_id = String(battle.operation_id) guild_company = resolved current_screen = "battle" _build_world_base() var frame_safe := _framed_page_safe("Battle") frame_safe.add_child(_entry_chrome("BattleFrame", "initial_company_scene_frame")) var safe := _content_page_safe("Battle") var layout := VBoxContainer.new() layout.name = "BattleLayout" layout.add_theme_constant_override("separation", 4) safe.add_child(layout) var header := HBoxContainer.new() header.name = "BattleHeader" header.custom_minimum_size = Vector2(0, 42 if size.y < 500.0 else 50) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("风镜初渡 · 自动战斗", "FIRST WINDGLASS CROSSING · AUTO BATTLE"), _scaled(19), Color("#f5ead0"))) heading.add_child(_label(_tr("目标:击破敌方队列 · 首要威胁锁定 0 号前排", "OBJECTIVE: BREAK THE ENEMY LINE · FIRST THREAT TARGETS FRONT 0"), _scaled(9), Color("#e8d39d"))) var phase_status := _label(_tr("阶段 %d/%d", "PHASE %d/%d") % [int(battle.phase_index), int(battle.total_phases)], _scaled(11), Color("#73b9e7")) phase_status.name = "BattlePhaseStatus" phase_status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(phase_status) var body := HBoxContainer.new() body.name = "BattleBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 12) layout.add_child(body) var field := VBoxContainer.new() field.name = "Battlefield" field.size_flags_horizontal = Control.SIZE_EXPAND_FILL field.size_flags_stretch_ratio = 1.7 field.add_theme_constant_override("separation", 2) body.add_child(field) var enemy_heading := _label(_tr("敌方队列 ◁ 接触走廊", "ENEMY LINE < CONTACT CORRIDOR"), _scaled(9), Color("#f0ca75")) enemy_heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT field.add_child(enemy_heading) var enemies := HBoxContainer.new() enemies.name = "BattleEnemies" enemies.custom_minimum_size = Vector2(0, 58 if size.y < 500.0 else 100) enemies.alignment = BoxContainer.ALIGNMENT_END enemies.add_theme_constant_override("separation", 7) field.add_child(enemies) for enemy_index in range(battle.enemy_ids.size()): enemies.add_child(_battle_enemy_marker(enemy_index, String(battle.enemy_ids[enemy_index]), battle)) var corridor := ColorRect.new() corridor.name = "BattleContactCorridor" corridor.custom_minimum_size = Vector2(0, 3) corridor.color = Color("#d6b76c") corridor.mouse_filter = Control.MOUSE_FILTER_IGNORE field.add_child(corridor) var actors := HBoxContainer.new() actors.name = "BattleActors" actors.size_flags_vertical = Control.SIZE_EXPAND_FILL actors.add_theme_constant_override("separation", 4) field.add_child(actors) for visual_index in range(battle.front_to_rear_ids.size() - 1, -1, -1): var recruit_id := String(battle.front_to_rear_ids[visual_index]) actors.add_child(_battle_actor(visual_index, _recruit_by_id(resolved, recruit_id), battle)) var orientation := _label(_tr("受保护后排 ◁────────▷ 暴露前排 · 0 号", "PROTECTED REAR <--------> EXPOSED FRONT · POSITION 0"), _scaled(9), Color("#9ed7aa")) orientation.name = "BattleOrientation" field.add_child(orientation) var context := VBoxContainer.new() context.name = "BattleEventContext" context.custom_minimum_size = Vector2(minf(310.0, size.x * 0.35), 0) context.add_theme_constant_override("separation", 4) body.add_child(context) _render_battle_event_context(context, resolved, battle) var footer := HBoxContainer.new() footer.name = "BattleFooter" footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 52) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) var pause := _company_button("BattlePause", _tr("继续观察", "Resume") if battle_paused else _tr("暂停", "Pause"), toggle_battle_pause, "company_utility", "pause") pause.custom_minimum_size = Vector2(170, footer.custom_minimum_size.y) pause.tooltip_text = _tr("继续自动事件时间线", "Resume the automatic event timeline") if battle_paused else _tr("暂停自动事件时间线", "Pause the automatic event timeline") footer.add_child(pause) var speed := _company_button("BattleSpeed", _tr("速度 %.0fx", "Speed %.0fx") % battle_speed, cycle_battle_speed, "company_secondary", "speed") speed.custom_minimum_size = Vector2(150, footer.custom_minimum_size.y) speed.tooltip_text = _tr("切换 1x / 2x 自动观察速度", "Cycle automatic observation speed between 1x and 2x") footer.add_child(speed) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var lock := _label(_tr("风险已提交 · 战斗中不能返回 · 没有战斗指令", "RISK COMMITTED · NO BACK · NO COMBAT COMMANDS"), _scaled(9), Color("#e8d39d")) lock.name = "BattleCommittedRule" lock.vertical_alignment = VERTICAL_ALIGNMENT_CENTER footer.add_child(lock) pause.call_deferred("grab_focus") if auto_play and not battle_paused: _schedule_battle_playback(playback_generation) func _invalidate_battle_playback() -> void: battle_playback_generation += 1 battle_paused = true _clear_battle_playback_timer() func _clear_battle_playback_timer() -> void: if is_instance_valid(battle_playback_timer): battle_playback_timer.stop() battle_playback_timer.queue_free() battle_playback_timer = null func _schedule_battle_playback(playback_generation: int) -> void: _clear_battle_playback_timer() battle_playback_timer = Timer.new() battle_playback_timer.name = "BattlePlaybackTimer" battle_playback_timer.one_shot = true battle_playback_timer.wait_time = 0.42 / maxf(1.0, battle_speed) battle_playback_timer.timeout.connect(_on_battle_playback_timeout.bind(playback_generation)) add_child(battle_playback_timer) battle_playback_timer.start() func _on_battle_playback_timeout(playback_generation: int) -> void: if playback_generation != battle_playback_generation or current_screen != "battle" or battle_paused: return battle_playback_timer = null advance_battle_phase() func advance_battle_phase() -> void: battle_service.company_path = initial_company_service.company_path var advanced: Dictionary = battle_service.advance_battle(battle_operation_id) if not advanced.ok: battle_notice = _battle_error_text(String(advanced.get("code", "BATTLE_ADVANCE_FAILED"))) battle_paused = true show_battle(guild_company, false) return guild_company = advanced.company.duplicate(true) if String(advanced.battle.status) == "result": show_battle_result(guild_company) else: show_battle(guild_company, true) func toggle_battle_pause() -> void: battle_paused = not battle_paused show_battle(guild_company, not battle_paused) func cycle_battle_speed() -> void: battle_speed = 2.0 if is_equal_approx(battle_speed, 1.0) else 1.0 show_battle(guild_company, not battle_paused) func show_battle_result(company := {}) -> void: var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else guild_company.duplicate(true) guild_company = resolved current_screen = "battle_result" _build_world_base() var frame_safe := _framed_page_safe("BattleResult") frame_safe.add_child(_entry_chrome("BattleResultFrame", "initial_company_scene_frame")) var safe := _content_page_safe("BattleResult") var battle: Dictionary = resolved.active_contract.get("battle_state", {}) var result: Dictionary = battle.get("result", {}) var short_result := size.y < 500.0 var layout := VBoxContainer.new() layout.name = "BattleResultLayout" layout.add_theme_constant_override("separation", 5) safe.add_child(layout) var header := HBoxContainer.new() header.name = "BattleResultHeader" header.custom_minimum_size = Vector2(0, 44 if short_result else 54) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("战斗结果", "BATTLE RESULT"), _scaled(21), Color("#f5ead0"))) heading.add_child(_label(_tr("风镜初渡 · 可审计结算", "FIRST WINDGLASS CROSSING · REVIEWABLE SETTLEMENT"), _scaled(10), Color("#e8d39d"))) var outcome := _label(_battle_outcome_text(String(result.get("outcome", "unknown"))), _scaled(15), Color("#9ed7aa") if String(result.get("outcome", "")) == "victory" else Color("#ffd4d4")) outcome.name = "BattleResultSummary" outcome.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(outcome) var body := HBoxContainer.new() body.name = "BattleResultBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 14) layout.add_child(body) var report := VBoxContainer.new() report.name = "BattleResultReport" report.size_flags_horizontal = Control.SIZE_EXPAND_FILL report.size_flags_stretch_ratio = 1.35 report.add_theme_constant_override("separation", 5) body.add_child(report) var objective := _label(_tr("目标:击破敌方队列", "OBJECTIVE: BREAK THE ENEMY LINE"), _scaled(13), Color("#f5ead0")) objective.name = "BattleResultObjective" report.add_child(objective) var decisive := _battle_result_decisive_event(battle) var decisive_copy := _label( _tr("第一决定性事件:%s", "FIRST DECISIVE EVENT: %s") % _battle_event_text(String(decisive.get("caption_id", ""))), _scaled(13), Color("#f0ca75") ) decisive_copy.name = "BattleResultDecisive" decisive_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART report.add_child(decisive_copy) var totals := HBoxContainer.new() totals.name = "BattleResultTotals" totals.custom_minimum_size = Vector2(0, 42 if short_result else 48) totals.add_theme_constant_override("separation", 8) report.add_child(totals) var coin := _label(_tr("金币 %d → %d\n+%d", "COIN %d -> %d\n+%d") % [int(result.get("coin_before", 0)), int(result.get("coin_after", 0)), int(result.get("reward_coin", 0))], _scaled(12), Color("#f0ca75")) coin.name = "BattleResultReward" coin.size_flags_horizontal = Control.SIZE_EXPAND_FILL coin.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART totals.add_child(coin) var xp := _label(_tr("经验 +%d\n存活成员各 +%d", "XP +%d\n%d per survivor") % [int(result.get("xp_award", 0)), int(result.get("xp_per_recruit", 0))], _scaled(12), Color("#9ed7aa")) xp.name = "BattleResultXP" xp.size_flags_horizontal = Control.SIZE_EXPAND_FILL xp.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART totals.add_child(xp) var knockouts: Array = result.get("knocked_out_ids", []) var knockout_copy := _label(_battle_result_knockout_text(resolved, knockouts), _scaled(11), Color("#ffd4d4") if not knockouts.is_empty() else Color("#9ed7aa")) knockout_copy.name = "BattleResultKnockouts" knockout_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART knockout_copy.size_flags_vertical = Control.SIZE_EXPAND_FILL report.add_child(knockout_copy) var context := VBoxContainer.new() context.name = "BattleResultDiagnosisContext" context.custom_minimum_size = Vector2(minf(350.0, size.x * 0.38), 0) context.add_theme_constant_override("separation", 4) body.add_child(context) context.add_child(_label(_tr("因果链", "CAUSAL CHAIN"), _scaled(10), Color("#e8d39d"))) var chain := _label(_battle_result_chain_summary(battle), _scaled(11), Color("#f5ead0")) chain.name = "BattleResultChain" chain.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART chain.size_flags_vertical = Control.SIZE_EXPAND_FILL context.add_child(chain) if battle_result_diagnosis_open: var detail := _label(_battle_result_diagnosis_text(resolved, battle), _scaled(10), Color("#aebbc2")) detail.name = "BattleResultDiagnosisDetail" detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART detail.size_flags_vertical = Control.SIZE_EXPAND_FILL context.add_child(detail) var footer := HBoxContainer.new() footer.name = "BattleResultFooter" footer.custom_minimum_size = Vector2(0, 46 if short_result else 52) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) var inspect := _company_button("BattleResultDiagnosis", _tr("因果详情", "Cause Details"), toggle_battle_result_diagnosis, "company_secondary", "inspect") inspect.custom_minimum_size = Vector2(230, footer.custom_minimum_size.y) footer.add_child(inspect) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var continue_button := _company_button("BattleResultContinue", _tr("继续", "Continue"), continue_from_battle_result, "company_primary", "confirm") continue_button.custom_minimum_size = Vector2(220, footer.custom_minimum_size.y) continue_button.tooltip_text = _tr("先查看因果链,再进入下一阶段恢复", "Review the causal chain before next-stage recovery") footer.add_child(continue_button) inspect.call_deferred("grab_focus") func toggle_battle_result_diagnosis() -> void: battle_result_diagnosis_open = not battle_result_diagnosis_open show_battle_result(guild_company) func continue_from_battle_result() -> void: show_battle_knockout_review(guild_company) func show_battle_knockout_review(company := {}) -> void: var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else guild_company.duplicate(true) guild_company = resolved current_screen = "battle_knockout_review" _build_world_base() var frame_safe := _framed_page_safe("BattleKnockout") frame_safe.add_child(_entry_chrome("BattleKnockoutFrame", "initial_company_scene_frame")) var safe := _content_page_safe("BattleKnockout") var layout := VBoxContainer.new() layout.name = "BattleKnockoutLayout" layout.add_theme_constant_override("separation", 5) safe.add_child(layout) var header := HBoxContainer.new() header.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 54) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("击倒回顾", "KNOCKOUT REVIEW"), _scaled(21), Color("#f5ead0"))) heading.add_child(_label(_tr("确认战斗内后果;没有成员被删除。", "Confirm the battle-local consequence; no Recruit was deleted."), _scaled(10), Color("#e8d39d"))) var status := _label(_tr("战斗回执已提交", "BATTLE RECEIPT COMMITTED"), _scaled(11), Color("#9ed7aa")) status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(status) var body := HBoxContainer.new() body.name = "BattleKnockoutBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 14) layout.add_child(body) var scene := VBoxContainer.new() scene.name = "BattleKnockoutScene" scene.size_flags_horizontal = Control.SIZE_EXPAND_FILL scene.size_flags_stretch_ratio = 1.2 scene.add_theme_constant_override("separation", 3) body.add_child(scene) var battle: Dictionary = resolved.active_contract.get("battle_state", {}) var knocked: Array = battle.get("result", {}).get("knocked_out_ids", []) if knocked.is_empty(): scene.add_child(_label(_tr("本场没有击倒成员。", "No Recruit was knocked out in this battle."), _scaled(15), Color("#9ed7aa"))) else: for recruit_id in knocked: var recruit := _recruit_by_id(resolved, String(recruit_id)) if recruit.is_empty(): continue scene.add_child(_readiness_member(0, recruit)) scene.add_child(_label(_tr("%s · %s", "%s · %s") % [_recruit_name(recruit), _recruit_profession_name(recruit)], _scaled(16), Color("#f0ca75"))) scene.add_child(_label(_tr("特质:%s\n等级 %d · XP %d\n装备:%s", "Trait: %s\nLevel %d · XP %d\nEquipment: %s") % [_recruit_trait_name(recruit), int(recruit.level), int(recruit.xp), _recruit_item_name(recruit)], _scaled(11), Color("#f5ead0"))) var context := VBoxContainer.new() context.name = "BattleKnockoutContext" context.custom_minimum_size = Vector2(minf(350.0, size.x * 0.38), 0) context.add_theme_constant_override("separation", 5) body.add_child(context) context.add_child(_label(_tr("当前战斗空位", "CURRENT BATTLE GAP"), _scaled(10), Color("#e8d39d"))) var gap := _label(_tr("被击倒成员只离开本场战斗。其稳定 Recruit ID、Profession、Trait、等级、XP 和装备所有权仍在 Company 中。", "The knocked-out member leaves this battle only. The stable Recruit ID, Profession, Trait, level, XP and equipment ownership remain in the Company."), _scaled(12), Color("#ffd4d4")) gap.name = "BattleKnockoutCurrentGap" gap.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART gap.size_flags_vertical = Control.SIZE_EXPAND_FILL context.add_child(gap) var return_rule := _label(_tr("下关归队:恢复后进入下一阶段 Market,不会永久死亡。", "Next-stage return: recovery opens the next Market; there is no permanent death."), _scaled(11), Color("#9ed7aa")) return_rule.name = "BattleKnockoutReturnRule" return_rule.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART context.add_child(return_rule) var footer := HBoxContainer.new() footer.name = "BattleKnockoutFooter" footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 52) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) var back := _company_button("BattleKnockoutBack", _tr("返回结果", "Back to Result"), show_battle_result.bind(resolved), "company_utility", "back") footer.add_child(back) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var acknowledge := _company_button("BattleKnockoutAcknowledge", _tr("确认击倒", "Acknowledge"), acknowledge_battle_knockout.bind(resolved), "company_primary", "confirm") acknowledge.custom_minimum_size = Vector2(230, footer.custom_minimum_size.y) acknowledge.tooltip_text = _tr("确认已提交的战斗内后果;不会删除成员", "Acknowledge the committed battle-local consequence; the member is not deleted") footer.add_child(acknowledge) back.call_deferred("grab_focus") func acknowledge_battle_knockout(company := {}) -> void: show_battle_recovery(company) func show_battle_recovery(company := {}) -> void: var resolved: Dictionary = Dictionary(company).duplicate(true) if company is Dictionary else guild_company.duplicate(true) guild_company = resolved current_screen = "battle_recovery" _build_world_base() var frame_safe := _framed_page_safe("BattleRecovery") frame_safe.add_child(_entry_chrome("BattleRecoveryFrame", "initial_company_scene_frame")) var safe := _content_page_safe("BattleRecovery") var layout := VBoxContainer.new() layout.name = "BattleRecoveryLayout" layout.add_theme_constant_override("separation", 6) safe.add_child(layout) var heading := VBoxContainer.new() heading.custom_minimum_size = Vector2(0, 48 if size.y < 500.0 else 58) layout.add_child(heading) heading.add_child(_label(_tr("下一阶段恢复", "NEXT-STAGE RECOVERY"), _scaled(21), Color("#f5ead0"))) heading.add_child(_label(_tr("一次原子回执将恢复同一 Company 的下一阶段准备入口。", "One atomic receipt restores the next-stage preparation entry for the same Company."), _scaled(10), Color("#e8d39d"))) var body := HBoxContainer.new() body.name = "BattleRecoveryBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 14) layout.add_child(body) var summary := VBoxContainer.new() summary.name = "BattleRecoverySummary" summary.size_flags_horizontal = Control.SIZE_EXPAND_FILL summary.add_theme_constant_override("separation", 5) body.add_child(summary) var battle: Dictionary = resolved.active_contract.get("battle_state", {}) var knocked: Array = battle.get("result", {}).get("knocked_out_ids", []) summary.add_child(_label(_tr("恢复成员 %d 名", "%d Recruit(s) to return") % knocked.size(), _scaled(16), Color("#f0ca75"))) var ids := _label("\n".join(_recovery_identity_lines(resolved, knocked)), _scaled(11), Color("#f5ead0")) ids.name = "BattleRecoveryIdentities" ids.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART ids.size_flags_vertical = Control.SIZE_EXPAND_FILL summary.add_child(ids) var context := VBoxContainer.new() context.name = "BattleRecoveryContext" context.custom_minimum_size = Vector2(minf(350.0, size.x * 0.38), 0) context.add_theme_constant_override("separation", 5) body.add_child(context) context.add_child(_label(_tr("恢复承诺", "RECOVERY COMMITMENT"), _scaled(10), Color("#e8d39d"))) var rule := _label(_tr("保留稳定 ID、Profession、Trait、等级、XP、历史和装备。战斗状态回到 Market;下一阶段可以重新建立 Party。", "Stable ID, Profession, Trait, level, XP, history and equipment are retained. Battle state returns to Market; the next stage can establish a new Party."), _scaled(12), Color("#9ed7aa")) rule.name = "BattleRecoveryRule" rule.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART rule.size_flags_vertical = Control.SIZE_EXPAND_FILL context.add_child(rule) var receipt := _label(_tr("恢复回执只提交一次;重试会返回原回执。", "The recovery receipt submits once; retries return the original receipt."), _scaled(10), Color("#e8d39d")) receipt.name = "BattleRecoveryReceiptRule" receipt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART context.add_child(receipt) var notice := _label(battle_notice, _scaled(10), Color("#ffd4d4")) notice.name = "BattleRecoveryNotice" notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART context.add_child(notice) var footer := HBoxContainer.new() footer.name = "BattleRecoveryFooter" footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 52) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) var back := _company_button("BattleRecoveryBack", _tr("返回", "Back"), show_battle_knockout_review.bind(resolved), "company_utility", "back") footer.add_child(back) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var commit := _company_button("BattleRecoveryCommit", _tr("恢复并进入市场", "Return to Market"), commit_battle_recovery, "company_primary", "confirm") commit.custom_minimum_size = Vector2(260, footer.custom_minimum_size.y) commit.tooltip_text = _tr("恢复相同 Recruit 身份并打开下一阶段 Market", "Return the same Recruit identities and open the next-stage Market") footer.add_child(commit) back.call_deferred("grab_focus") func commit_battle_recovery() -> void: battle_service.company_path = initial_company_service.company_path var loaded: Dictionary = battle_service.load_company() if not loaded.ok: battle_notice = _battle_error_text(String(loaded.get("code", "RECOVERY_LOAD_FAILED"))) show_battle_recovery(guild_company) return var battle: Dictionary = loaded.company.active_contract.get("battle_state", {}) var operation_id := ("%s|recovery|%s" % [String(battle.get("operation_id", "")), String(loaded.company.company_id)]).sha256_text().substr(0, 32) var recovered: Dictionary = battle_service.recover_next_stage(operation_id) if not recovered.ok: battle_notice = _battle_error_text(String(recovered.get("code", "RECOVERY_FAILED"))) show_battle_recovery(loaded.company) return battle_notice = "" guild_company = recovered.company.duplicate(true) show_market(guild_company) func set_market_category(value: String) -> void: if value not in _market_categories(): return market_category = value market_selected_kind = "" market_selected_offer_id = "" market_notice = "" show_market(guild_company) func select_market_offer(kind: String, offer_id: String) -> void: var offer := _market_offer_by_id(guild_company.active_contract, kind, offer_id) if offer.is_empty(): market_notice = _tr("该货单条目已不可用。", "That ledger entry is no longer available.") else: market_selected_kind = kind market_selected_offer_id = offer_id market_notice = "" show_market(guild_company) func open_recruit_review() -> void: if market_selected_kind != "recruit" or market_selected_offer_id.is_empty(): market_notice = _tr("请先选择一名招募候选。", "Select a Recruit offer first.") show_market(guild_company) return show_recruit_review(market_selected_offer_id) func open_equipment_review() -> void: if market_selected_kind != "equipment" or market_selected_offer_id.is_empty(): market_notice = _tr("请先选择一件装备。", "Select an equipment offer first.") show_market(guild_company) return show_equipment_review(market_selected_offer_id) func show_equipment_review(offer_id: String) -> void: var loaded: Dictionary = initial_company_service.load_company() if not loaded.ok or loaded.company.active_contract.is_empty(): market_notice = _tr("无法读取稳定装备货单。", "The stable Equipment ledger could not be loaded.") show_market() return var offer := _market_offer_by_id(loaded.company.active_contract, "equipment", offer_id) if offer.is_empty(): market_notice = _tr("该装备候选已不可用。", "That equipment offer is no longer available.") show_market(loaded.company) return guild_company = loaded.company.duplicate(true) market_category = "equipment" market_selected_kind = "equipment" market_selected_offer_id = offer_id current_screen = "equipment_review" _build_world_base(true) var frame_safe := _framed_page_safe("EquipmentReview") frame_safe.add_child(_entry_chrome("EquipmentReviewFrame", "content_scene_frame")) var safe := _content_page_safe("EquipmentReview") var layout := VBoxContainer.new() layout.name = "EquipmentReviewLayout" layout.add_theme_constant_override("separation", 5) safe.add_child(layout) var header := HBoxContainer.new() header.custom_minimum_size = Vector2(0, 44 if size.y < 500.0 else 50) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("装备详情", "EQUIPMENT REVIEW"), _scaled(20), Color("#f5ead0"))) heading.add_child(_label(_market_offer_title("equipment", offer), _scaled(11), Color("#e8d39d"))) var price := _label(_market_offer_price("equipment", offer), _scaled(15), Color("#f0ca75")) price.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(price) var body := HBoxContainer.new() body.name = "EquipmentReviewBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 16) layout.add_child(body) var detail := VBoxContainer.new() detail.name = "EquipmentReviewDetail" detail.size_flags_horizontal = Control.SIZE_EXPAND_FILL detail.size_flags_stretch_ratio = 1.05 detail.add_theme_constant_override("separation", 5) body.add_child(detail) detail.add_child(_label(_market_offer_title("equipment", offer), _scaled(17), Color("#f0ca75"))) var effect := _label(_market_context_detail("equipment", offer), _scaled(11), Color("#f5ead0")) effect.name = "EquipmentReviewEffect" effect.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART effect.size_flags_vertical = Control.SIZE_EXPAND_FILL detail.add_child(effect) var inventory_note := _label(_tr("购买后装备到一名 Recruit;原装备返回 Company inventory。", "Purchase equips one Recruit; the previous item returns to Company inventory."), _scaled(10), Color("#9ed7aa")) inventory_note.name = "EquipmentReviewInventoryRule" inventory_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART detail.add_child(inventory_note) var targets := VBoxContainer.new() targets.name = "EquipmentReviewTargets" targets.size_flags_horizontal = Control.SIZE_EXPAND_FILL targets.size_flags_stretch_ratio = 1.0 targets.add_theme_constant_override("separation", 4) body.add_child(targets) targets.add_child(_label(_tr("选择装备成员", "CHOOSE WEARER"), _scaled(10), Color("#e8d39d"))) var target_grid := GridContainer.new() target_grid.name = "EquipmentReviewTargetGrid" target_grid.columns = 2 target_grid.size_flags_vertical = Control.SIZE_EXPAND_FILL target_grid.add_theme_constant_override("h_separation", 5) target_grid.add_theme_constant_override("v_separation", 5) targets.add_child(target_grid) for recruit in guild_company.owned_recruits: var recruit_id := String(recruit.recruit_id) var equip := _company_button("EquipmentEquipRecruit%s" % recruit_id, _tr("装备 %s", "Equip %s") % _recruit_name(recruit), purchase_selected_equipment.bind(recruit_id), "content_secondary", "confirm") equip.custom_minimum_size = Vector2(180, 50 if size.y < 500.0 else 54) var affordable := int(guild_company.active_contract.coin) >= int(offer.list_price) _set_entry_button_disabled(equip, not affordable) equip.tooltip_text = _tr("装备到 %s;当前:%s", "Equip on %s; current: %s") % [_recruit_name(recruit), _recruit_item_name(recruit)] if affordable else _tr("金币不足:需要 %d,当前 %d;未改变状态", "Insufficient Coin: need %d, have %d; state is unchanged") % [int(offer.list_price), int(guild_company.active_contract.coin)] target_grid.add_child(equip) var footer := HBoxContainer.new() footer.name = "EquipmentReviewFooter" footer.custom_minimum_size = Vector2(0, 46 if size.y < 500.0 else 50) footer.add_theme_constant_override("separation", 8) layout.add_child(footer) var cancel := _company_button("EquipmentReviewCancel", _tr("返回市场", "Back to Market"), cancel_equipment_review, "company_utility", "back") footer.add_child(cancel) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(spacer) var state := _label(_tr("选择成员后提交一次", "Choose a Recruit to commit once"), _scaled(10), Color("#e8d39d")) state.vertical_alignment = VERTICAL_ALIGNMENT_CENTER footer.add_child(state) cancel.call_deferred("grab_focus") func cancel_equipment_review() -> void: market_notice = "" show_market(guild_company) func purchase_selected_equipment(recruit_id: String) -> void: if market_selected_kind != "equipment" or market_selected_offer_id.is_empty(): market_notice = _tr("没有可提交的装备候选。", "No equipment offer is ready to commit.") show_market(guild_company) return var offer := _market_offer_by_id(guild_company.active_contract, "equipment", market_selected_offer_id) if offer.is_empty(): market_notice = _tr("该装备候选已不可用。", "That equipment offer is unavailable.") show_market(guild_company) return guild_contract_service.company_path = initial_company_service.company_path var operation_id := ("%s|equip|%s|%s" % [String(guild_company.company_id), market_selected_offer_id, recruit_id]).sha256_text().substr(0, 32) var result: Dictionary = guild_contract_service.purchase_equipment(operation_id, market_selected_offer_id, recruit_id, int(offer.list_price)) if not result.ok: market_notice = _equipment_error_text(String(result.get("code", "EQUIPMENT_FAILED"))) show_equipment_review(market_selected_offer_id) return guild_company = result.company.duplicate(true) market_notice = _tr("已装备 %s · 金币 %d → %d", "Equipped %s · Coin %d -> %d") % [_market_offer_title("equipment", offer), int(result.receipt.coin_before), int(result.receipt.coin_after)] market_selected_kind = "" market_selected_offer_id = "" show_market(guild_company) func show_recruit_review(offer_id: String) -> void: var loaded := initial_company_service.load_company() if not loaded.ok or loaded.company.active_contract.is_empty(): market_notice = _tr("无法读取稳定货单。", "The stable Market ledger could not be loaded.") show_market() return var offer := _market_offer_by_id(loaded.company.active_contract, "recruit", offer_id) if offer.is_empty(): market_notice = _tr("该招募候选已不可用。", "That Recruit offer is no longer available.") show_market(loaded.company) return guild_company = loaded.company.duplicate(true) market_category = "recruit" market_selected_kind = "recruit" market_selected_offer_id = offer_id current_screen = "recruit_review" _build_world_base(true) var frame_safe := _framed_page_safe("RecruitReview") frame_safe.add_child(_entry_chrome("RecruitReviewFrame", "content_scene_frame")) var safe := _content_page_safe("RecruitReview") var layout := VBoxContainer.new() layout.name = "RecruitReviewLayout" layout.add_theme_constant_override("separation", 6) safe.add_child(layout) var contract: Dictionary = guild_company.active_contract var header := HBoxContainer.new() header.custom_minimum_size = Vector2(0, 44) layout.add_child(header) var heading := VBoxContainer.new() heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(heading) heading.add_child(_label(_tr("招募详情", "RECRUIT REVIEW"), _scaled(20), Color("#f5ead0"))) heading.add_child(_label(_market_recruit_name(offer), _scaled(11), Color("#e8d39d"))) var price := _label(_tr("%d 金币", "%d COIN") % int(offer.list_price), _scaled(15), Color("#f0ca75")) price.name = "RecruitReviewPrice" price.vertical_alignment = VERTICAL_ALIGNMENT_CENTER header.add_child(price) var body := HBoxContainer.new() body.name = "RecruitReviewBody" body.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_theme_constant_override("separation", 18) layout.add_child(body) var portrait_stage := Control.new() portrait_stage.name = "RecruitReviewPortrait" portrait_stage.size_flags_horizontal = Control.SIZE_EXPAND_FILL portrait_stage.size_flags_stretch_ratio = 0.9 body.add_child(portrait_stage) var portrait := TextureRect.new() portrait.name = "RecruitReviewPortraitTexture" portrait.texture = InitialCompanyArt.texture_for(offer) portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS portrait.anchor_left = 0.08 portrait.anchor_right = 0.92 portrait.anchor_bottom = 1.0 portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE portrait_stage.add_child(portrait) var scroll := ScrollContainer.new() scroll.name = "RecruitReviewScroll" scroll.custom_minimum_size = Vector2(minf(430.0, size.x * 0.52), 0) scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_child(scroll) var details := VBoxContainer.new() details.name = "RecruitReviewDetails" details.size_flags_horizontal = Control.SIZE_EXPAND_FILL details.add_theme_constant_override("separation", 5) scroll.add_child(details) _add_review_detail(details, "RecruitReviewProfession", _tr("职业", "PROFESSION"), "%s · %s" % [_content_name("professions", String(offer.profession_id)), _profession_job(offer)]) _add_review_detail(details, "RecruitReviewTrait", _tr("特质", "TRAIT"), "%s\n%s: %s\n%s: %s" % [_content_name("traits", String(offer.trait_id)), _tr("收益", "Benefit"), _trait_value(offer, "benefit"), _tr("代价", "Cost"), _trait_value(offer, "cost")]) _add_review_detail(details, "RecruitReviewAttributes", _tr("属性", "ATTRIBUTES"), _attribute_summary(offer)) _add_review_detail(details, "RecruitReviewItem", _tr("初始装备", "STARTING ITEM"), "%s · %s" % [_content_name("equipment", String(offer.starting_equipment_id)), _equipment_effect(offer)]) _add_review_detail(details, "RecruitReviewHistory", _tr("成长记录", "GROWTH & HISTORY"), _tr("等级 1 · 经验 0 · 参战 0 · 击倒 0", "Level 1 · XP 0 · Battles 0 · Knockouts 0")) var offer_available: bool = offer_id in contract.available_recruit_offer_ids var opening_hires_complete: bool = contract.hire_receipts.size() >= 2 var impact_text := _tr("远征团 %d → %d · 队列容量保持 %d;编队在下一步决定。", "Company %d → %d · Line capacity remains %d; formation is decided next.") % [guild_company.owned_recruits.size(), guild_company.owned_recruits.size() + 1, int(contract.line_capacity)] if not offer_available: impact_text = _tr("该成员已加入远征团;队列容量仍为 %d。", "This Recruit already joined the Company; Line capacity remains %d.") % int(contract.line_capacity) elif opening_hires_complete: impact_text = _tr("开场两笔招募已完成;先建立编队。队列容量仍为 %d。", "The two opening hires are complete; establish formation next. Line capacity remains %d.") % int(contract.line_capacity) _add_review_detail(details, "RecruitReviewImpact", _tr("远征团影响", "COMPANY IMPACT"), impact_text) var footer := HBoxContainer.new() footer.name = "RecruitReviewFooter" footer.custom_minimum_size = Vector2(0, 48) layout.add_child(footer) var cancel := _company_button("RecruitReviewCancel", _tr("取消并返回同一货单", "Cancel to Same Offer"), cancel_recruit_review, "company_utility", "back") cancel.custom_minimum_size = Vector2(250, 48) footer.add_child(cancel) var footer_spacer := Control.new() footer_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL footer.add_child(footer_spacer) if offer_available and not opening_hires_complete and int(contract.coin) >= int(offer.list_price): var hire := _company_button("RecruitHire", _tr("招募 · %d 金币", "Recruit · %d Coin") % int(offer.list_price), hire_selected_recruit, "company_primary", "confirm") hire.custom_minimum_size = Vector2(230, 48) hire.tooltip_text = _tr("确认招募 %s,花费 %d 金币,远征团 %d → %d", "Recruit %s for %d Coin; Company %d → %d") % [_market_recruit_name(offer), int(offer.list_price), guild_company.owned_recruits.size(), guild_company.owned_recruits.size() + 1] footer.add_child(hire) else: var state_text := _tr("已招募 · 无重复扣款", "HIRED · NO DUPLICATE CHARGE") if not offer_available else _tr("开场招募已完成 · 下一步建立编队", "OPENING HIRES COMPLETE · FORMATION NEXT") if opening_hires_complete else _tr("金币不足 · 未改变状态", "INSUFFICIENT COIN · NO CHANGE") var state := _label(state_text, _scaled(10), Color("#9ed7aa")) state.name = "RecruitHireState" state.custom_minimum_size = Vector2(190, 0) state.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART state.vertical_alignment = VERTICAL_ALIGNMENT_CENTER footer.add_child(state) cancel.call_deferred("grab_focus") func cancel_recruit_review() -> void: market_notice = "" show_market(guild_company) func hire_selected_recruit() -> void: if market_selected_kind != "recruit" or market_selected_offer_id.is_empty(): market_notice = _tr("没有可提交的招募候选。", "No Recruit offer is ready to commit.") show_market(guild_company) return var contract: Dictionary = guild_company.active_contract var offer := _market_offer_by_id(contract, "recruit", market_selected_offer_id) if offer.is_empty(): market_notice = _tr("该招募候选已不可用。", "That Recruit offer is unavailable.") show_market(guild_company) return guild_contract_service.company_path = initial_company_service.company_path var operation_id := ("%s|hire-recruit|%s" % [String(guild_company.company_id), market_selected_offer_id]).sha256_text().substr(0, 32) var result: Dictionary = guild_contract_service.hire_recruit(operation_id, market_selected_offer_id, int(offer.list_price)) if not result.ok: market_notice = _hire_error_text(String(result.get("code", "HIRE_FAILED"))) show_recruit_review(market_selected_offer_id) return guild_company = result.company.duplicate(true) var receipt: Dictionary = result.receipt market_category = "recruit" market_selected_kind = "" market_selected_offer_id = "" market_notice = _tr("已招募 %s · 金币 %d → %d · 远征团 %d → %d", "Recruited %s · Coin %d → %d · Company %d → %d") % [_market_recruit_name(offer), int(receipt.coin_before), int(receipt.coin_after), int(receipt.company_before), int(receipt.company_after)] show_market(guild_company) func _market_category_button(category: String) -> Button: var selected: bool = category == market_category var equipment_count := 4 if not guild_company.is_empty(): equipment_count = guild_company.active_contract.get("available_equipment_offer_ids", []).size() var title: String = { "recruit": _tr("招募 3", "Recruit 3"), "equipment": _tr("装备 %d", "Equipment %d") % equipment_count, "battle": _tr("战斗 3", "Battle 3"), }[category] var button := _entry_button_base("MarketCategory%s" % category.capitalize(), set_market_category.bind(category), "content_primary" if selected else "content_secondary") button.custom_minimum_size = Vector2(0, 44) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.add_child(_button_label("ButtonLabel", String(title), _scaled(11), Color("#202a31") if selected else Color("#f5ead0"))) button.set_meta("selected", selected) return button func _market_offer_row(kind: String, offer: Dictionary, index: int) -> Button: var offer_id: String = _market_offer_id(kind, offer) var selected: bool = kind == market_selected_kind and offer_id == market_selected_offer_id var button := _entry_button_base("MarketOffer%s" % offer_id, select_market_offer.bind(kind, offer_id), "content_primary" if selected else "content_secondary") button.set_meta("offer_id", offer_id) button.set_meta("offer_kind", kind) button.set_meta("selected", selected) var short_market := size.y < 380.0 button.custom_minimum_size = Vector2(0, 56 if kind == "recruit" and short_market else 62 if kind == "recruit" else 45 if short_market else 49) button.size_flags_vertical = Control.SIZE_EXPAND_FILL var left_inset := 62.0 if kind == "recruit" else 14.0 if kind == "recruit": var portrait := TextureRect.new() portrait.texture = InitialCompanyArt.texture_for(offer) portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS portrait.position = Vector2(7, 3) portrait.size = Vector2(46 if short_market else 48, 50 if short_market else 56) portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(portrait) var title := _label(_market_offer_title(kind, offer), _scaled(13), Color("#202a31") if selected else Color("#f5ead0")) title.position = Vector2(left_inset, 5) title.size = Vector2(maxf(80.0, 240.0 - left_inset), 22) title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS title.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(title) var detail := _label(_market_offer_detail(kind, offer), _scaled(9), Color("#394a52") if selected else Color("#d0d7d5")) detail.position = Vector2(left_inset, 25) detail.size = Vector2(maxf(80.0, 315.0 - left_inset), 30 if kind == "recruit" else 19) detail.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS detail.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(detail) var price := _label(_market_offer_price(kind, offer), _scaled(11), Color("#6f562d") if selected else Color("#f0ca75")) price.anchor_left = 1.0 price.anchor_right = 1.0 price.offset_left = -88 price.offset_right = -10 price.offset_top = 5 price.offset_bottom = 27 price.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT price.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(price) return button func _render_market_context(context: VBoxContainer, contract: Dictionary) -> void: context.add_child(_label(_tr("检查", "INSPECT"), _scaled(10), Color("#e8d39d"))) if market_selected_offer_id.is_empty(): var empty := _label(_tr("选择任意货单条目查看完整信息。\n没有候选被自动选择或标记为推荐。", "Select any ledger entry to inspect it.\nNo offer is auto-selected or marked recommended."), _scaled(13), Color("#f5ead0")) empty.name = "MarketNoSelection" empty.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART empty.size_flags_vertical = Control.SIZE_EXPAND_FILL context.add_child(empty) if not market_notice.is_empty(): var receipt_notice := _label(market_notice, _scaled(11), Color("#9ed7aa")) receipt_notice.name = "MarketReceiptNotice" receipt_notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART context.add_child(receipt_notice) return var offer := _market_offer_by_id(contract, market_selected_kind, market_selected_offer_id) if offer.is_empty(): context.add_child(_label(_tr("该条目已不可用。", "That entry is unavailable."), _scaled(13), Color("#ffd4d4"))) return context.add_child(_label(_market_offer_title(market_selected_kind, offer), _scaled(17), Color("#f5ead0"))) var detail := _label(_market_context_detail(market_selected_kind, offer), _scaled(11), Color("#aebbc2")) detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART detail.size_flags_vertical = Control.SIZE_EXPAND_FILL context.add_child(detail) context.add_child(_label(_market_offer_price(market_selected_kind, offer), _scaled(14), Color("#f0ca75"))) if market_selected_kind == "recruit": var inspect := _company_button("MarketReviewRecruit", _tr("查看招募详情", "Review Recruit"), open_recruit_review, "content_primary", "inspect") inspect.custom_minimum_size = Vector2(0, 50) context.add_child(inspect) elif market_selected_kind == "equipment": var equipment_review := _company_button("MarketReviewEquipment", _tr("查看装备并选择成员", "Review & Choose Wearer"), open_equipment_review, "content_primary", "inspect") equipment_review.custom_minimum_size = Vector2(0, 50) equipment_review.tooltip_text = _tr("查看效果、取舍与四名成员的当前装备", "Review effect, tradeoff and the current item on all four Recruits") context.add_child(equipment_review) if not market_notice.is_empty(): var notice := _label(market_notice, _scaled(10), Color("#f0ca75")) notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART context.add_child(notice) func _market_categories() -> Array[String]: return ["recruit", "equipment", "battle"] func _market_collection_field(kind: String) -> String: return {"recruit": "recruit_offers", "equipment": "equipment_offers", "battle": "battle_offers"}.get(kind, "recruit_offers") func _market_offer_id(kind: String, offer: Dictionary) -> String: return String(offer.get("source_offer_id" if kind == "recruit" else "offer_id", "")) func _market_offer_by_id(contract: Dictionary, kind: String, offer_id: String) -> Dictionary: for offer in contract.market_board.get(_market_collection_field(kind), []): if _market_offer_id(kind, offer) == offer_id: return Dictionary(offer).duplicate(true) return {} func _market_offer_title(kind: String, offer: Dictionary) -> String: match kind: "recruit": return _market_recruit_name(offer) "equipment": return _content_name("equipment", String(offer.catalog_id)) "battle": return _tr("路线 %d · 风压 %d", "Route %d · Risk %d") % [int(offer.get("risk", 0)) + 1, int(offer.get("risk", 0))] return "" func _market_offer_detail(kind: String, offer: Dictionary) -> String: match kind: "recruit": var detail := "%s · %s" % [_content_name("professions", String(offer.profession_id)), _content_name("traits", String(offer.trait_id))] if not guild_company.is_empty() and String(offer.source_offer_id) not in guild_company.active_contract.available_recruit_offer_ids: detail = "%s · %s" % [_tr("已招募", "HIRED"), detail] return detail "equipment": return _equipment_effect_by_id(String(offer.catalog_id)) "battle": return _tr("%d 波 · %d 名敌人", "%d waves · %d enemies") % [int(offer.get("wave_count", 0)), Array(offer.get("enemy_ids", [])).size()] return "" func _market_offer_price(kind: String, offer: Dictionary) -> String: if kind == "battle": return _tr("入场 0", "Entry 0") if kind == "recruit" and not guild_company.is_empty() and String(offer.source_offer_id) not in guild_company.active_contract.available_recruit_offer_ids: return _tr("已招募", "Hired") return _tr("%d 金币", "%d Coin") % int(offer.get("list_price", 0)) func _market_context_detail(kind: String, offer: Dictionary) -> String: match kind: "recruit": return "%s\n%s: %s\n%s: %s\n%s" % [_content_name("professions", String(offer.profession_id)), _tr("特质收益", "Trait benefit"), _trait_value(offer, "benefit"), _tr("特质代价", "Trait cost"), _trait_value(offer, "cost"), _attribute_summary(offer)] "equipment": var row: Dictionary = RuntimeContentRegistry.rows_by_id(market_content, "equipment").get(String(offer.catalog_id), {}) var locale := String(settings.get("locale", "zh_CN")) return "%s\n%s: %s\n%s: %s" % [MarketLocalization.equipment_value(String(offer.catalog_id), "effect", String(row.get("effect", "")), locale), _tr("适用", "Fit"), MarketLocalization.equipment_value(String(offer.catalog_id), "fit", String(row.get("fit", "")), locale), _tr("取舍", "Tradeoff"), MarketLocalization.equipment_value(String(offer.catalog_id), "tradeoff", String(row.get("tradeoff", "")), locale)] "battle": var enemy_names: Array[String] = [] for enemy_id in offer.get("enemy_ids", []): enemy_names.append(_content_name("enemies", String(enemy_id))) var risk_copy := _tr("稳健压力", "steady pressure") if int(offer.get("risk", 0)) == 0 else _tr("加剧压力", "elevated pressure") if int(offer.get("risk", 0)) == 1 else _tr("高压路线", "high-pressure route") return _tr("已知压力:%s\n敌方:%s\n选择路线不会在此页面开始战斗。", "Known pressure: %s\nEnemies: %s\nSelecting a route does not start battle from this page.") % [risk_copy, ", ".join(enemy_names)] return "" func _market_recruit_name(offer: Dictionary) -> String: var tokens: Array = offer.get("display_name_tokens", []) var parts: Array[String] = [] for token in tokens: var segments := String(token).split(".") parts.append(String(segments[segments.size() - 1])) return " ".join(parts) func _content_name(family: String, row_id: String) -> String: var locale := String(settings.get("locale", "zh_CN")) return RuntimeContentRegistry.localized_name(market_content, row_id, locale) func _profession_job(offer: Dictionary) -> String: var row: Dictionary = RuntimeContentRegistry.rows_by_id(market_content, "professions").get(String(offer.profession_id), {}) var fallback := String(row.get("role", "")) return MarketLocalization.profession_job(String(offer.profession_id), fallback, String(settings.get("locale", "zh_CN"))) func _trait_value(offer: Dictionary, field: String) -> String: var row: Dictionary = RuntimeContentRegistry.rows_by_id(market_content, "traits").get(String(offer.trait_id), {}) var fallback := String(row.get(field, "")) return MarketLocalization.trait_value(String(offer.trait_id), field, fallback, String(settings.get("locale", "zh_CN"))) func _equipment_effect(offer: Dictionary) -> String: return _equipment_effect_by_id(String(offer.starting_equipment_id)) func _equipment_effect_by_id(item_id: String) -> String: var row: Dictionary = RuntimeContentRegistry.rows_by_id(market_content, "equipment").get(item_id, {}) var fallback := String(row.get("effect", "")) return MarketLocalization.equipment_value(item_id, "effect", fallback, String(settings.get("locale", "zh_CN"))) func _equipment_error_text(code: String) -> String: match code: "EQUIPMENT_MARKET_REQUIRED": return _tr("装备只可在市场阶段提交。", "Equipment can only be committed during Market.") "EQUIPMENT_OFFER_UNAVAILABLE": return _tr("该装备已不可用;未改变状态。", "That equipment offer is unavailable; state is unchanged.") "EQUIPMENT_PRICE_MISMATCH": return _tr("价格已变化;请重新检查货单。", "The price changed; review the ledger again.") "EQUIPMENT_INSUFFICIENT_COIN": return _tr("金币不足;未购买装备。", "Insufficient Coin; equipment was not purchased.") "EQUIPMENT_RECRUIT_UNAVAILABLE": return _tr("该 Recruit 已不可用;未购买装备。", "That Recruit is unavailable; equipment was not purchased.") "EQUIPMENT_OPERATION_CONFLICT": return _tr("重复操作的目标不同;原装备回执保持不变。", "The repeated operation targets a different intent; the original receipt is unchanged.") "EQUIPMENT_OFFER_ALREADY_APPLIED_CONFLICT": return _tr("该装备已提交给另一名成员;请选择仍可用的货单。", "That equipment was already committed to another Recruit; choose an available offer.") return _tr("装备交易未提交;Company 没有改变。", "Equipment transaction was not committed; Company is unchanged.") + " [%s]" % code func _attribute_summary(offer: Dictionary) -> String: var attributes: Dictionary = offer.get("attribute_rolls", {}) return "VIG %s · FOR %s · FIN %s · RES %s · FOC %s" % [String(attributes.get("vigor", "-")), String(attributes.get("force", "-")), String(attributes.get("finesse", "-")), String(attributes.get("resolve", "-")), String(attributes.get("focus", "-"))] func _add_review_detail(parent: VBoxContainer, node_name: String, heading: String, value: String) -> void: var line := _label("%s\n%s" % [heading, value], _scaled(10), Color("#f5ead0")) line.name = node_name line.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART line.size_flags_horizontal = Control.SIZE_EXPAND_FILL parent.add_child(line) func back_to_guild() -> void: var loaded := initial_company_service.load_company() if loaded.ok: show_guild_handoff(loaded.company) else: title_notice = _tr("无法读取远征团。", "The Company could not be loaded.") + " [%s]" % String(loaded.code) show_title() func _framed_page_safe(prefix: String) -> MarginContainer: var safe := MarginContainer.new() safe.name = "%sFrameSafeArea" % prefix safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) safe.mouse_filter = Control.MOUSE_FILTER_IGNORE var margin := maxi(12, int(settings.get("safe_area", 24))) var vertical_margin := 12 if size.y < 500.0 else margin safe.add_theme_constant_override("margin_left", margin) safe.add_theme_constant_override("margin_right", margin) safe.add_theme_constant_override("margin_top", vertical_margin) safe.add_theme_constant_override("margin_bottom", vertical_margin) add_child(safe) return safe func _content_page_safe(prefix: String) -> MarginContainer: var safe := MarginContainer.new() safe.name = "%sSafeArea" % prefix safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) var margin := maxi(12, int(settings.get("safe_area", 24))) var vertical_margin := 12 if size.y < 500.0 else margin safe.add_theme_constant_override("margin_left", margin + 12) safe.add_theme_constant_override("margin_right", margin + 12) safe.add_theme_constant_override("margin_top", vertical_margin + 8) safe.add_theme_constant_override("margin_bottom", vertical_margin + 8) add_child(safe) return safe func _contract_company_scene(recruits: Array) -> Control: var stage := Control.new() stage.name = "ContractCompanyScene" stage.custom_minimum_size = Vector2(0, 150 if size.y < 500.0 else 220) stage.size_flags_vertical = Control.SIZE_EXPAND_FILL stage.clip_contents = true for index in range(mini(2, recruits.size())): var portrait := TextureRect.new() portrait.name = "ContractCompanyMember_%d" % index portrait.texture = InitialCompanyArt.texture_for(recruits[index]) portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS portrait.anchor_left = 0.02 + index * 0.55 portrait.anchor_top = 0.02 portrait.anchor_right = 0.43 + index * 0.55 portrait.anchor_bottom = 1.0 portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE stage.add_child(portrait) var board = GuildDestinationSticker.new().configure("contract") board.name = "ContractBoardSticker" board.anchor_left = 0.39 board.anchor_top = 0.20 board.anchor_right = 0.61 board.anchor_bottom = 0.78 board.mouse_filter = Control.MOUSE_FILTER_IGNORE stage.add_child(board) return stage func _contract_segment_button(difficulty: String) -> Button: var selected := difficulty == contract_difficulty var node_name := "ContractDifficulty%s" % difficulty.to_pascal_case().replace("_", "") var button := _entry_button_base(node_name, select_contract_difficulty.bind(difficulty), "content_primary" if selected else "content_secondary") button.custom_minimum_size = Vector2(0, 44) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.tooltip_text = _difficulty_copy(difficulty) var label := _button_label("ButtonLabel", _difficulty_name(difficulty), _scaled(10), Color("#202a31") if selected else Color("#f5ead0")) label.offset_left = 5 label.offset_right = -5 button.add_child(label) button.set_meta("selected", selected) return button func _difficulty_name(value: String) -> String: match value: "wayfinder": return _tr("引路", "Wayfinder") "stormbound": return _tr("风暴", "Stormbound") "iron_oath": return _tr("铁誓", "Iron Oath") return _tr("标准", "Standard") func _difficulty_copy(value: String) -> String: match value: "wayfinder": return _tr("较宽容的敌方参数;所有系统、进度与记录仍然可用。", "More forgiving enemy parameters; all systems, progression and Records remain available.") "stormbound": return _tr("更高压力与回报;目标规则和远征团所有权不变。", "Higher pressure and rewards; target rules and Company ownership remain unchanged.") "iron_oath": return _tr("最高压力;只改变敌人与回报参数,不隐藏规则。", "Highest pressure; only enemy and reward parameters change, never hidden rules.") return _tr("完整的标准平衡;不需要额外永久战力。", "The complete standard balance; no extra permanent power is required.") func _contract_error_text(code: String) -> String: match code: "CONTRACT_ALREADY_ACTIVE": return _tr("已有进行中的委托。", "A Contract is already active.") "CONTRACT_LOCKED": return _tr("该委托尚未解锁。", "That Contract is still locked.") "CONTRACT_DIFFICULTY_INVALID": return _tr("难度无效,请重新选择。", "The difficulty is invalid; choose again.") return _tr("无法建立委托;远征团与资源未改变。", "The Contract could not be created; Company and resources are unchanged.") + " [%s]" % code func _dismissal_error_text(code: String) -> String: match code: "FINAL_LIVE_RECRUIT": return _tr("最后一名成员不可解雇;Company 没有改变。", "The final live member cannot be dismissed; Company is unchanged.") "DISMISS_FORMATION_LOCKED": return _tr("队列已锁定;请先返回未准备的 Market。", "The Line is locked; return to an unprepared Market first.") "DISMISS_MARKET_REQUIRED": return _tr("解雇只在 Market 可用;Company 没有改变。", "Dismissal is only available in Market; Company is unchanged.") "DISMISS_RECRUIT_UNAVAILABLE": return _tr("该成员已不在 Company;请重新查看成员列表。", "That Recruit is no longer in the Company; review the roster again.") return _tr("解雇未提交;金币与 Company 没有改变。", "Dismissal was not committed; Coin and Company are unchanged.") + " [%s]" % code func _contract_return_error_text(code: String) -> String: match code: "CONTRACT_RETURN_FORMATION_LOCKED": return _tr("队列已锁定;不能从准备中的委托直接返回。", "The Line is locked; a prepared Contract cannot be returned directly.") "CONTRACT_RETURN_ALREADY_APPLIED": return _tr("该委托已经返回公会;Company 没有重复改变。", "This Contract has already returned to Guild; Company was not changed again.") "CONTRACT_RETURN_MARKET_REQUIRED": return _tr("主动返回只在 Market 可用;Company 没有改变。", "Voluntary return is only available in Market; Company is unchanged.") return _tr("委托未返回;金币、成员与成长保持不变。", "The Contract was not returned; Coin, members and growth remain unchanged.") + " [%s]" % code func _hire_error_text(code: String) -> String: match code: "HIRE_PRICE_MISMATCH": return _tr("价格已改变;未扣除金币。", "The price changed; no Coin was charged.") "HIRE_INSUFFICIENT_COIN": return _tr("金币不足;未改变远征团。", "Insufficient Coin; Company state is unchanged.") "HIRE_OFFER_UNAVAILABLE": return _tr("该招募候选已不可用;未扣除金币。", "That Recruit offer is unavailable; no Coin was charged.") "OPENING_HIRES_COMPLETE": return _tr("开场两笔招募已完成;下一步建立编队。", "The two opening hires are complete; establish formation next.") return _tr("招募未完成;金币与远征团保持不变。", "Recruitment did not complete; Coin and Company are unchanged.") + " [%s]" % code func _formation_error_text(code: String) -> String: match code: "FORMATION_OPENING_HIRES_REQUIRED": return _tr("需要先完成两笔独立招募,并达到远征团 4/4。", "Complete both separate hires and reach Company 4/4 first.") "FORMATION_CAPACITY_STALE": return _tr("队列容量已改变;请返回市场重新检查。", "Line capacity changed; return to the Market and review it again.") "FORMATION_COUNT_INVALID": return _tr("当前队列人数与容量不一致。", "The current party count does not match Line capacity.") "FORMATION_RECRUIT_UNKNOWN", "FORMATION_RECRUIT_DUPLICATE": return _tr("队列成员已改变;请重新进入编队。", "Party membership changed; reopen formation.") return _tr("无法建立队列;远征团与资源没有改变。", "The line could not be established; Company and resources are unchanged.") + " [%s]" % code func _party_member_button(position: int, recruit: Dictionary) -> Button: var recruit_id := String(recruit.get("recruit_id", "")) var selected := recruit_id == party_selected_recruit_id var button := Button.new() button.name = "PartyPosition%d" % position button.flat = true button.clip_contents = true button.custom_minimum_size = Vector2(92, 128 if size.y < 500.0 else 190) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.set_meta("recruit_id", recruit_id) button.tooltip_text = _tr("%d 号位置 · %s · 点选后再点目标位置交换", "Position %d · %s · select, then choose a destination to swap") % [position, _recruit_name(recruit)] for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]: button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) var selection := ColorRect.new() selection.name = "SelectionGround" selection.anchor_left = 0.12 selection.anchor_top = 0.82 selection.anchor_right = 0.88 selection.anchor_bottom = 0.89 selection.color = Color("#73b9e7") if selected else Color("#8b7141") selection.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(selection) var portrait := TextureRect.new() portrait.name = "PartyPortrait" portrait.texture = InitialCompanyArt.texture_for(recruit) portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS portrait.anchor_left = 0.08 portrait.anchor_top = 0.0 portrait.anchor_right = 0.92 portrait.anchor_bottom = 0.80 portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(portrait) var position_label := _button_label("PositionLabel", _tr("%d 号", "POS %d") % position, _scaled(10), Color("#202a31") if selected else Color("#f5ead0")) position_label.anchor_top = 0.78 position_label.anchor_bottom = 0.90 position_label.offset_left = 6 position_label.offset_right = -6 button.add_child(position_label) var name := _button_label("NameLabel", _recruit_name(recruit), _scaled(10), Color("#f5ead0")) name.anchor_top = 0.89 name.offset_left = 3 name.offset_right = -3 button.add_child(name) button.pressed.connect(select_party_member.bind(recruit_id), CONNECT_DEFERRED) return button func _readiness_member(position: int, recruit: Dictionary) -> Control: var member := Control.new() member.name = "ReadinessPosition%d" % position member.custom_minimum_size = Vector2(88, 126 if size.y < 500.0 else 180) member.size_flags_horizontal = Control.SIZE_EXPAND_FILL member.clip_contents = true var ground := ColorRect.new() ground.anchor_left = 0.14 ground.anchor_top = 0.80 ground.anchor_right = 0.86 ground.anchor_bottom = 0.86 ground.color = Color("#8b7141") ground.mouse_filter = Control.MOUSE_FILTER_IGNORE member.add_child(ground) var portrait := TextureRect.new() portrait.name = "ReadinessPortrait" portrait.texture = InitialCompanyArt.texture_for(recruit) portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS portrait.anchor_left = 0.08 portrait.anchor_right = 0.92 portrait.anchor_bottom = 0.79 portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE member.add_child(portrait) var position_label := _label(_tr("%d 号", "POS %d") % position, _scaled(9), Color("#e8d39d")) position_label.anchor_top = 0.79 position_label.anchor_right = 1.0 position_label.anchor_bottom = 0.90 position_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER position_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER member.add_child(position_label) var name := _label(_recruit_name(recruit), _scaled(9), Color("#f5ead0")) name.anchor_top = 0.89 name.anchor_right = 1.0 name.anchor_bottom = 1.0 name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER name.vertical_alignment = VERTICAL_ALIGNMENT_CENTER name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS member.add_child(name) return member func _readiness_warning(formation: Dictionary) -> String: var coverage: Dictionary = formation.coverage var warnings: Array[String] = [] if int(coverage.protection) == 0: warnings.append(_tr("没有明确保护贡献者", "No explicit protection contributor")) if int(coverage.long_reach) == 0: warnings.append(_tr("没有整线射程", "No full-line reach")) if int(coverage.sustain) == 0: warnings.append(_tr("没有续航职责", "No sustain responsibility")) if warnings.is_empty(): return _tr("允许的警告:无。队列覆盖完整,但结果仍不保证。", "Allowed warnings: none. Coverage is complete, but the outcome is not guaranteed.") return _tr("允许继续的警告:", "Allowed warnings: ") + " · ".join(warnings) func _risk_option_button(option: Dictionary) -> Button: var option_id := String(option.risk_option_id) var selected := option_id == risk_selected_id var button := Button.new() button.name = "RiskOption%s" % option_id button.flat = true button.custom_minimum_size = Vector2(0, 54 if size.y < 500.0 else 66) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.tooltip_text = _risk_title(option) button.set_meta("selected", selected) for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]: button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) var rail := ColorRect.new() rail.name = "RiskRail" rail.anchor_right = 1.0 rail.offset_left = 4 rail.offset_right = -4 rail.offset_top = 2 rail.offset_bottom = -2 rail.color = Color(0.09, 0.16, 0.21, 0.84 if selected else 0.52) rail.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(rail) var accent := ColorRect.new() accent.anchor_bottom = 1.0 accent.offset_left = 4 accent.offset_right = 10 accent.offset_top = 2 accent.offset_bottom = -2 accent.color = [Color("#75a884"), Color("#d6b76c"), Color("#b7584d")][int(option.risk)] accent.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(accent) var mark := _button_label("RiskMark", ["○", "△", "✦"][int(option.risk)], _scaled(18), Color("#f5ead0")) mark.anchor_right = 0.14 mark.offset_left = 12 button.add_child(mark) var title := _button_label("RiskTitle", _risk_title(option), _scaled(13), Color("#f5ead0")) title.anchor_left = 0.14 title.anchor_right = 0.48 title.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT title.offset_left = 5 button.add_child(title) var summary := _button_label("RiskSummary", _risk_option_summary(option), _scaled(9), Color("#e8d39d")) summary.anchor_left = 0.48 summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT summary.offset_left = 5 summary.offset_right = -8 summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART button.add_child(summary) button.pressed.connect(select_risk_option.bind(option_id), CONNECT_DEFERRED) return button func _render_risk_context(context: VBoxContainer, formation: Dictionary) -> void: context.add_child(_label(_tr("选项检查", "OPTION INSPECT"), _scaled(10), Color("#e8d39d"))) if risk_selected_id.is_empty(): var empty := _label(_tr("选择一条风险查看完整已知信息。\n没有选项被自动选中或标记为推荐。", "Select one risk to inspect all known information.\nNo option is auto-selected or marked recommended."), _scaled(12), Color("#f5ead0")) empty.name = "RiskNoSelection" empty.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART empty.size_flags_vertical = Control.SIZE_EXPAND_FILL context.add_child(empty) return var option := _risk_option_by_id(formation, risk_selected_id) if option.is_empty(): context.add_child(_label(_tr("该选项已失效,请返回准备检查。", "That option is stale; return to Readiness."), _scaled(11), Color("#ffd4d4"))) return context.add_child(_label(_risk_title(option), _scaled(17), Color("#f0ca75"))) var detail := _label(_risk_detail_copy(option), _scaled(10), Color("#f5ead0")) detail.name = "RiskSelectedDetail" detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART detail.size_flags_vertical = Control.SIZE_EXPAND_FILL context.add_child(detail) var coverage: Dictionary = formation.coverage var coverage_copy := _label(_tr("当前覆盖:保护 %d · 远程 %d · 续航 %d · 控制 %d", "Current coverage: Guard %d · Reach %d · Sustain %d · Control %d") % [int(coverage.protection), int(coverage.long_reach), int(coverage.sustain), int(coverage.control)], _scaled(9), Color("#9ed7aa")) coverage_copy.name = "RiskCoverage" coverage_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART context.add_child(coverage_copy) func _risk_option_by_id(formation: Dictionary, option_id: String) -> Dictionary: for option in formation.get("risk_options", []): if String(option.get("risk_option_id", "")) == option_id: return Dictionary(option) return {} func _risk_title(option: Dictionary) -> String: match String(option.get("tier_id", "steady")): "pressured": return _tr("受压路线", "PRESSURED ROUTE") "elite": return _tr("精英路线", "ELITE ROUTE") return _tr("稳健路线", "STEADY ROUTE") func _risk_option_summary(option: Dictionary) -> String: return _tr("%d 金币 · %s · %s", "%d Coin · %s · %s") % [int(option.reward_coin), _uncertainty_name(String(option.uncertainty_id)), _recovery_name(String(option.recovery_exposure_id))] func _risk_threat_copy(option: Dictionary) -> String: var threat: Dictionary = option.known_threat return _tr("已知:%d 名敌人 · %d 波 · 首要目标 0 号", "Known: %d enemies · %d waves · primary target position 0") % [int(threat.enemy_count), int(threat.wave_count)] func _risk_detail_copy(option: Dictionary) -> String: return _tr( "已知威胁:%d 名敌人 · %d 波 · 0 号为首要目标\n未解析信息:%s\n入场成本:%d 金币\n胜利回报:%d 金币 · %s\n恢复暴露:%s", "Known threat: %d enemies · %d waves · position 0 is primary\nUncertainty: %s\nEntry cost: %d Coin\nVictory reward: %d Coin · %s\nRecovery exposure: %s" ) % [ int(option.known_threat.enemy_count), int(option.known_threat.wave_count), _uncertainty_name(String(option.uncertainty_id)), int(option.entry_cost), int(option.reward_coin), _reward_class_name(String(option.reward_class_id)), _recovery_name(String(option.recovery_exposure_id)), ] func _uncertainty_name(value: String) -> String: match value: "one_intent_unresolved": return _tr("一项敌方意图未解析", "one enemy intent unresolved") "elite_response_unresolved": return _tr("精英反应时机未解析", "elite response timing unresolved") return _tr("全部公开", "fully disclosed") func _recovery_name(value: String) -> String: match value: "moderate": return _tr("中", "moderate") "high": return _tr("高", "high") return _tr("低", "low") func _reward_class_name(value: String) -> String: match value: "enhanced_choice": return _tr("强化选择", "enhanced choice") "elite_cache": return _tr("精英宝箱", "elite cache") return _tr("标准补给", "standard supplies") func _battle_enemy_marker(index: int, enemy_id: String, battle: Dictionary) -> Control: var marker := Control.new() marker.name = "BattleEnemy%d" % index marker.custom_minimum_size = Vector2(66, 54 if size.y < 500.0 else 82) marker.mouse_filter = Control.MOUSE_FILTER_IGNORE var silhouette := ColorRect.new() silhouette.anchor_left = 0.24 silhouette.anchor_top = 0.10 silhouette.anchor_right = 0.76 silhouette.anchor_bottom = 0.72 silhouette.color = Color("#7b3f3f") if index == 0 else Color("#5c4e50") silhouette.mouse_filter = Control.MOUSE_FILTER_IGNORE marker.add_child(silhouette) var label := _label("E%d" % (index + 1), _scaled(11), Color("#f5ead0")) label.anchor_right = 1.0 label.anchor_top = 0.70 label.anchor_bottom = 1.0 label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER marker.add_child(label) return marker func _battle_actor(position: int, recruit: Dictionary, battle: Dictionary) -> Control: var actor := Control.new() actor.name = "BattleActor%d" % position actor.custom_minimum_size = Vector2(88, 142 if size.y < 500.0 else 220) actor.size_flags_horizontal = Control.SIZE_EXPAND_FILL actor.clip_contents = true var portrait := TextureRect.new() portrait.name = "BattlePortrait" portrait.texture = InitialCompanyArt.texture_for(recruit) portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS portrait.anchor_left = 0.06 portrait.anchor_top = 0.0 portrait.anchor_right = 0.94 portrait.anchor_bottom = 0.76 portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE actor.add_child(portrait) var ground := ColorRect.new() ground.anchor_left = 0.10 ground.anchor_top = 0.74 ground.anchor_right = 0.90 ground.anchor_bottom = 0.79 ground.color = Color("#b7584d") if String(recruit.recruit_id) in Array(battle.get("knocked_out_ids", [])) else Color("#75a884") ground.mouse_filter = Control.MOUSE_FILTER_IGNORE actor.add_child(ground) var position_label := _label(_tr("%d 号", "POS %d") % position, _scaled(9), Color("#e8d39d")) position_label.anchor_top = 0.78 position_label.anchor_right = 1.0 position_label.anchor_bottom = 0.89 position_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER position_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER actor.add_child(position_label) var name := _label(_recruit_name(recruit), _scaled(9), Color("#f5ead0")) name.anchor_top = 0.88 name.anchor_right = 1.0 name.anchor_bottom = 1.0 name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER name.vertical_alignment = VERTICAL_ALIGNMENT_CENTER name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS actor.add_child(name) return actor func _render_battle_event_context(context: VBoxContainer, company: Dictionary, battle: Dictionary) -> void: context.add_child(_label(_tr("事件投影", "EVENT PROJECTION"), _scaled(10), Color("#e8d39d"))) var events: Array = battle.get("phase_events", []) if events.is_empty(): context.add_child(_label(_tr("正在等待第一条事件...", "Waiting for the first event..."), _scaled(12), Color("#f5ead0"))) else: var event: Dictionary = events.back() var title := _label(_battle_event_text(String(event.get("caption_id", "battle_opening"))), _scaled(16), Color("#f0ca75")) title.name = "BattleCurrentEvent" context.add_child(title) var actor_name := _recruit_name(_recruit_by_id(company, String(event.get("actor_id", "")))) if not String(event.get("actor_id", "")).is_empty() else _tr("敌方队列", "Enemy line") var target_name := _recruit_name(_recruit_by_id(company, String(event.get("target_id", "")))) if _recruit_by_id(company, String(event.get("target_id", ""))).size() > 0 else _tr("敌方目标", "Enemy target") var detail := _label(_tr("行动者:%s\n目标:%s\n事件类型:%s", "Actor: %s\nTarget: %s\nEvent: %s") % [actor_name, target_name, String(event.get("kind", ""))], _scaled(11), Color("#f5ead0")) detail.name = "BattleEventDetail" detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART detail.size_flags_vertical = Control.SIZE_EXPAND_FILL context.add_child(detail) var recent: Array[String] = [] for event in events.slice(maxi(0, events.size() - 4), events.size()): recent.append(_battle_event_text(String(event.get("caption_id", "")))) var rail := _label("\n".join(recent), _scaled(9), Color("#aebbc2")) rail.name = "BattleEventRail" rail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART context.add_child(rail) func _battle_event_text(caption_id: String) -> String: match caption_id: "incoming_threat": return _tr("敌方威胁已公开", "Incoming threat disclosed") "protection_intercepts": return _tr("保护关系承接压力", "Protection relation intercepts pressure") "ranged_contact": return _tr("远程接触命中", "Ranged contact lands") "magic_break": return _tr("魔法破坏命中", "Magic break lands") "healing_link": return _tr("治疗链接恢复队列", "Healing link restores the line") "decisive_damage": return _tr("决定性伤害改变结果", "Decisive damage changes the result") "knockout": return _tr("成员在本战斗中被击倒", "A member is knocked out for this battle") "line_holds": return _tr("队列守住压力", "The line holds pressure") "victory": return _tr("胜利结果已准备", "Victory result is ready") return _tr("战斗开场", "Battle opening") func _battle_outcome_text(outcome: String) -> String: return _tr("胜利", "VICTORY") if outcome == "victory" else _tr("失败", "DEFEAT") if outcome == "defeat" else _tr("结果待核对", "RESULT NEEDS REVIEW") func _battle_result_decisive_event(battle: Dictionary) -> Dictionary: var result: Dictionary = battle.get("result", {}) var decisive_hash := String(result.get("decisive_event_hash", "")) for event in battle.get("phase_events", []): if String(event.get("event_hash", "")) == decisive_hash: return Dictionary(event) for event in battle.get("phase_events", []): if String(event.get("kind", "")) in ["damage", "knockout"]: return Dictionary(event) return {} func _battle_result_knockout_text(company: Dictionary, knockouts: Array) -> String: if knockouts.is_empty(): return _tr("击倒:无 · 四名成员保留战斗状态", "Knockout: none · all four members retain battle state") var lines: Array[String] = [] for recruit_id in knockouts: var recruit := _recruit_by_id(company, String(recruit_id)) if recruit.is_empty(): lines.append(_tr("击倒成员:身份回执缺失", "Knocked-out member: identity receipt missing")) continue lines.append(_tr("击倒:%s · %s · 仅本战斗离场,下关归队", "Knocked out: %s · %s · battle-only absence; returns next stage") % [_recruit_name(recruit), _recruit_profession_name(recruit)]) return "\n".join(lines) func _battle_result_chain_summary(battle: Dictionary) -> String: var events: Array = battle.get("phase_events", []) if events.is_empty(): return _tr("因果数据缺失。结果保持暂停。", "Causal data is missing. Result remains paused.") var lines: Array[String] = [] for event in events: if String(event.get("kind", "")) in ["opening", "damage", "knockout", "guarded_recovery", "result"]: lines.append(_battle_event_text(String(event.get("caption_id", "")))) return " > ".join(lines) func _battle_result_diagnosis_text(company: Dictionary, battle: Dictionary) -> String: var decisive := _battle_result_decisive_event(battle) var result: Dictionary = battle.get("result", {}) var lines: Array[String] = [] lines.append(_tr("提交队列:%d 名成员 · 敌方:%d 名", "Committed line: %d members · enemy line: %d") % [battle.get("front_to_rear_ids", []).size(), battle.get("enemy_ids", []).size()]) lines.append(_tr("首要目标:0 号前排 · 决定性事件:%s", "First target: position 0 front · decisive event: %s") % _battle_event_text(String(decisive.get("caption_id", "")))) lines.append(_tr("事件 hash:%s", "Event hash: %s") % String(decisive.get("event_hash", "")).substr(0, 12)) lines.append(_tr("奖励回执:%d 金币 · XP 总计 %d", "Reward receipt: %d Coin · %d total XP") % [int(result.get("reward_coin", 0)), int(result.get("xp_award", 0))]) return "\n".join(lines) func _battle_error_text(code: String) -> String: match code: "BATTLE_READINESS_REQUIRED": return _tr("请先接受准备检查。", "Accept Readiness first.") "BATTLE_RISK_UNAVAILABLE": return _tr("该风险选项已失效,请返回风险板。", "That risk option is stale; return to the Risk Board.") "BATTLE_ALREADY_ACTIVE": return _tr("已有进行中的战斗;已恢复原战斗回执。", "A battle is already active; its original receipt was restored.") "RECOVERY_RESULT_REQUIRED": return _tr("结果回执尚未完成,无法恢复下一阶段。", "The result receipt is not ready for next-stage recovery.") "recovery_already_applied": return _tr("恢复已提交;正在打开下一阶段市场。", "Recovery was already committed; opening the next-stage Market.") return _tr("战斗回执未能创建;远征团没有改变。", "The battle receipt could not be created; Company state is unchanged.") + " [%s]" % code func _render_party_context(context: VBoxContainer, company: Dictionary) -> void: context.add_child(_label(_tr("位置关系", "POSITION RELATIONS"), _scaled(10), Color("#e8d39d"))) var context_id := party_selected_recruit_id if not party_selected_recruit_id.is_empty() else String(party_draft_order[0]) var recruit := _recruit_by_id(company, context_id) var relation := _party_relation(context_id) var name := _label(_recruit_name(recruit), _scaled(17), Color("#f5ead0")) name.name = "PartyContextName" context.add_child(name) var position := int(relation.get("position", 0)) var position_copy := _label( _tr("%d 号 · %s", "POSITION %d · %s") % [position, _exposure_name(String(relation.get("exposure_id", "highest")))], _scaled(11), Color("#f0ca75") ) position_copy.name = "PartyContextPosition" context.add_child(position_copy) var protected_names: Array[String] = [] for recruit_id in relation.get("protected_by_recruit_ids", []): protected_names.append(_recruit_name(_recruit_by_id(company, String(recruit_id)))) var protection_copy := _tr("无前方保护者", "No protector ahead") if protected_names.is_empty() else _tr("受到保护:", "Protected by: ") + " / ".join(protected_names) var target_copy := _tr("首要威胁目标", "Primary threat target") if bool(relation.get("first_threat_target", false)) else _tr("预期目标顺位 %d", "Expected target rank %d") % int(relation.get("expected_target_rank", position + 1)) var reach_copy := _reach_name(String(relation.get("reach_id", "limited"))) var summary := _label("%s\n%s\n%s" % [target_copy, protection_copy, reach_copy], _scaled(11), Color("#aebbc2")) summary.name = "PartyRelationSummary" summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART summary.size_flags_vertical = Control.SIZE_EXPAND_FILL context.add_child(summary) var coverage: Dictionary = party_preview.get("coverage", {}) var coverage_copy := _label( _tr("队列覆盖 保护 %d · 远程 %d · 续航 %d · 控制 %d", "LINE COVERAGE Guard %d · Reach %d · Sustain %d · Control %d") % [ int(coverage.get("protection", 0)), int(coverage.get("long_reach", 0)), int(coverage.get("sustain", 0)), int(coverage.get("control", 0)) ], _scaled(10), Color("#9ed7aa") ) coverage_copy.name = "PartyCoverage" coverage_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART context.add_child(coverage_copy) var instruction := _label( party_notice if not party_notice.is_empty() else _tr("先点一名成员,再点目标位置。选择只在当前预览中变化。", "Select a member, then a destination. Changes remain in this preview only."), _scaled(10), Color("#73b9e7") ) instruction.name = "PartyInstruction" instruction.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART context.add_child(instruction) var next_boundary := _label(_tr("尚未保存 · 准备检查将在下一步启用", "NOT SAVED · READINESS REVIEW ENABLES NEXT"), _scaled(9), Color("#e8d39d")) next_boundary.name = "PartyReviewReason" next_boundary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART context.add_child(next_boundary) func _party_relation(recruit_id: String) -> Dictionary: for relation in party_preview.get("relations", []): if String(relation.get("recruit_id", "")) == recruit_id: return Dictionary(relation) return {} func _party_button_for_recruit(recruit_id: String) -> Button: if recruit_id.is_empty(): return null for position in range(party_draft_order.size()): var button := find_child("PartyPosition%d" % position, true, false) as Button if button != null and String(button.get_meta("recruit_id", "")) == recruit_id: return button return null func _recruit_by_id(company: Dictionary, recruit_id: String) -> Dictionary: for recruit in company.get("owned_recruits", []): if String(recruit.get("recruit_id", "")) == recruit_id: return Dictionary(recruit) return {} func _recruit_name(recruit: Dictionary) -> String: var localized: Dictionary = recruit.get("rendered_name_snapshot", {}) var locale := String(settings.get("locale", "zh_CN")) return String(localized.get(locale, localized.get("en", recruit.get("recruit_id", "Unknown")))) func _recruit_profession_name(recruit: Dictionary) -> String: var localized: Dictionary = recruit.get("profession_name", {}) var locale := String(settings.get("locale", "zh_CN")) return String(localized.get(locale, localized.get("en", recruit.get("profession_id", "Unknown Profession")))) func _recruit_trait_name(recruit: Dictionary) -> String: var localized: Dictionary = recruit.get("trait_name", {}) var locale := String(settings.get("locale", "zh_CN")) return String(localized.get(locale, localized.get("en", recruit.get("trait_id", "Unknown Trait")))) func _recruit_item_name(recruit: Dictionary) -> String: if String(recruit.get("equipped_item_id", "")) != String(recruit.get("starting_item_id", "")): return _content_name("equipment", String(recruit.get("equipped_item_id", ""))) var localized: Dictionary = recruit.get("starting_item_name", {}) var locale := String(settings.get("locale", "zh_CN")) return String(localized.get(locale, localized.get("en", recruit.get("equipped_item_id", "Unknown Equipment")))) func _recovery_identity_lines(company: Dictionary, recruit_ids: Array) -> Array[String]: var lines: Array[String] = [] for recruit_id in recruit_ids: var recruit := _recruit_by_id(company, String(recruit_id)) if recruit.is_empty(): lines.append(_tr("身份回执缺失:%s", "Identity receipt missing: %s") % String(recruit_id)) continue lines.append(_tr("%s · %s · ID %s", "%s · %s · ID %s") % [_recruit_name(recruit), _recruit_profession_name(recruit), String(recruit.recruit_id).substr(0, 8)]) return lines func _exposure_name(exposure_id: String) -> String: match exposure_id: "rear": return _tr("受保护后排", "Protected rear") "guarded": return _tr("受保护中段", "Guarded center") "high": return _tr("高暴露", "High exposure") return _tr("最高暴露", "Highest exposure") func _reach_name(reach_id: String) -> String: match reach_id: "full_line": return _tr("射程:覆盖整条队列", "Reach: full line") "front_three": return _tr("射程:覆盖前 3 位", "Reach: front three") "front_two": return _tr("射程:覆盖前 2 位", "Reach: front two") return _tr("射程受限:调整到更前位置", "Limited reach: move farther forward") func _first_threat_text(threat: Dictionary) -> String: return _tr("首要威胁 → 0 号前排 · %d 名敌人 · %d 波", "FIRST THREAT -> FRONT 0 · %d ENEMIES · %d WAVES") % [int(threat.get("enemy_count", 0)), int(threat.get("wave_count", 0))] func _same_string_members(left: Array, right: Array) -> bool: if left.size() != right.size(): return false var left_ids: Array[String] = [] var right_ids: Array[String] = [] for value in left: left_ids.append(String(value)) for value in right: right_ids.append(String(value)) left_ids.sort() right_ids.sort() return left_ids == right_ids func _guild_member_column(index: int, recruit: Dictionary) -> Control: var box := Control.new() box.name = "GuildMember_%d" % index box.custom_minimum_size = Vector2(150, 180 if size.y < 500.0 else 230) box.size_flags_horizontal = Control.SIZE_EXPAND_FILL box.size_flags_vertical = Control.SIZE_EXPAND_FILL box.size_flags_stretch_ratio = 1.0 box.mouse_filter = Control.MOUSE_FILTER_IGNORE var member_tag := _label(_tr("远征团成员 %d" % (index + 1), "COMPANY MEMBER %d" % (index + 1)), _scaled(9), Color("#f5ead0")) member_tag.position = Vector2(8, 2) member_tag.size = Vector2(220, 20) member_tag.mouse_filter = Control.MOUSE_FILTER_IGNORE box.add_child(member_tag) var portrait_stage := Control.new() portrait_stage.name = "GuildMemberPortrait_%d" % index portrait_stage.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) portrait_stage.offset_top = 18 portrait_stage.offset_bottom = -54 portrait_stage.mouse_filter = Control.MOUSE_FILTER_IGNORE var ground := ColorRect.new() ground.name = "GuildMemberGround_%d" % index ground.anchor_left = 0.08 ground.anchor_top = 0.84 ground.anchor_right = 0.92 ground.anchor_bottom = 0.92 ground.color = Color(0.04, 0.13, 0.13, 0.38) ground.mouse_filter = Control.MOUSE_FILTER_IGNORE portrait_stage.add_child(ground) var tint := ColorRect.new() tint.color = Color(InitialCompanyArt.accent_for(recruit), 0.06) tint.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) tint.mouse_filter = Control.MOUSE_FILTER_IGNORE portrait_stage.add_child(tint) var portrait := TextureRect.new() portrait.name = "GuildMemberPortraitTexture_%d" % index portrait.texture = InitialCompanyArt.texture_for(recruit) portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED portrait.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS portrait.anchor_left = 0.16 portrait.anchor_top = 0.02 portrait.anchor_right = 0.84 portrait.anchor_bottom = 0.96 portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE portrait_stage.add_child(portrait) box.add_child(portrait_stage) var locale := String(settings.get("locale", "zh_CN")) var name := _label(String(recruit.rendered_name_snapshot.get(locale, recruit.rendered_name_snapshot.en)), _scaled(15), Color("#f5ead0")) name.anchor_top = 0.72 name.anchor_right = 1.0 name.anchor_bottom = 0.84 name.offset_left = 8 name.offset_right = -8 name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS name.mouse_filter = Control.MOUSE_FILTER_IGNORE box.add_child(name) var profession := _label("%s · %s" % [String(recruit.profession_name.get(locale, recruit.profession_name.en)), _tr("等级 1", "Level 1")], _scaled(10), Color("#f0ca75")) profession.anchor_top = 0.84 profession.anchor_right = 1.0 profession.anchor_bottom = 0.93 profession.offset_left = 8 profession.offset_right = -8 profession.mouse_filter = Control.MOUSE_FILTER_IGNORE box.add_child(profession) var trait_label := _label(String(recruit.trait_name.get(locale, recruit.trait_name.en)), _scaled(10), Color("#b9dfc0")) trait_label.anchor_top = 0.91 trait_label.anchor_right = 1.0 trait_label.anchor_bottom = 0.99 trait_label.offset_left = 8 trait_label.offset_right = -8 trait_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS trait_label.mouse_filter = Control.MOUSE_FILTER_IGNORE box.add_child(trait_label) return box func _guild_destination_button(node_name: String, kind: String, title: String, callable: Callable) -> Button: var button := _entry_button_base(node_name, callable, "content_destination") button.custom_minimum_size = Vector2(250, 66) var sticker = GuildDestinationSticker.new().configure(kind) sticker.name = "Guild%sSticker" % kind.capitalize() sticker.position = Vector2(10, 6) sticker.size = Vector2(82, 54) sticker.visible = false button.add_child(sticker) var prop := TextureRect.new() prop.name = "Guild%sProp" % kind.capitalize() prop.texture = GUILD_CONTRACT_BOARD if kind == "contract" else GUILD_ROUTE_MAP prop.expand_mode = TextureRect.EXPAND_IGNORE_SIZE prop.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED prop.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS prop.position = Vector2(8, 4) prop.size = Vector2(88, 58) prop.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(prop) var label := _label(title, _scaled(16), Color("#24313a")) label.position = Vector2(104, 0) label.size = Vector2(maxf(80.0, button.custom_minimum_size.x - 116.0), 66) label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(label) return button func _company_button(node_name: String, text_value: String, callback: Callable, variant: String, icon: String) -> Button: var button := Button.new() button.name = node_name button.flat = true button.tooltip_text = text_value button.custom_minimum_size = Vector2(190, 52) 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 color := Color("#202a31") if variant in ["company_utility", "content_primary", "content_destination"] else Color("#f5ead0") var label := _button_label("ButtonLabel", text_value, _scaled(16), color) label.offset_left = 30.0 label.offset_right = -10.0 button.add_child(label) button.set_meta("entry_chrome_variant", variant) button.pressed.connect(callback, CONNECT_DEFERRED) return button func _set_guild_notice(value: String) -> void: var label := get_node_or_null("GuildHandoffSafeArea/GuildHandoffLayout/GuildHandoffBody/GuildNextActions/GuildNotice") as Label if label != null: label.text = value func show_settings_from_guild() -> void: settings_caller = "guild_handoff" show_settings() func show_settings_placeholder() -> void: settings_caller = "title" show_settings() func show_credits_placeholder() -> void: settings_caller = "title" 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: _return_from_settings() func discard_settings() -> void: settings_draft = {} settings_notice = "" _return_from_settings() func _return_from_settings() -> void: if settings_caller == "guild_handoff": show_guild_handoff(guild_company) else: 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 = "" settings_caller = "title" 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 == "initial_company" and initial_company_page != null: initial_company_page.request_back() elif current_screen == "guild_handoff": back_to_title() elif current_screen == "roster": back_to_guild() elif current_screen == "recruit_detail": show_roster(guild_company) elif current_screen == "dismissal_review": cancel_dismissal_review() elif current_screen == "contract_return_review": cancel_contract_return() elif current_screen in ["contract_choice", "market"]: back_to_guild() elif current_screen == "equipment_review": cancel_equipment_review() elif current_screen == "party_line": cancel_party_line() elif current_screen == "readiness_review": revise_readiness() elif current_screen == "risk_board": back_to_readiness() elif current_screen == "risk_commitment": cancel_risk_commitment() elif current_screen == "battle": toggle_battle_pause() elif current_screen == "battle_result": get_viewport().set_input_as_handled() show_battle_result(guild_company) elif current_screen == "battle_knockout_review": get_viewport().set_input_as_handled() show_battle_result(guild_company) elif current_screen == "battle_recovery": get_viewport().set_input_as_handled() show_battle_knockout_review(guild_company) elif current_screen == "recruit_review": cancel_recruit_review() elif current_screen in ["settings", "credits", "exit_review"]: _return_from_settings() func _save_status() -> String: var company := initial_company_service.load_company() if company.ok: return "valid" if String(company.code) != "COMPANY_MISSING": return "unsafe" 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(content_scene := false) -> void: for child in get_children(): child.free() var background := TextureRect.new() background.texture = CONTENT_BACKGROUND if content_scene else 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.12 if content_scene else 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)) func _canonical_value(value): if value is float and is_equal_approx(value, round(value)): return int(round(value)) if value is Dictionary: var result := {} var keys: Array = value.keys() keys.sort_custom(func(left, right): return String(left) < String(right)) for key in keys: result[key] = _canonical_value(value[key]) return result if value is Array: var result: Array = [] for item in value: result.append(_canonical_value(item)) return result return value