class_name ABGFG3RuntimeController extends ABGFG2GrayboxController const FG3Adapter = preload("res://campaign/fg3_adapter.gd") func _init() -> void: adapter = FG3Adapter.new() func _initialize_entry() -> void: var source := pending_snapshot if source.is_empty(): last_code = "fg3_snapshot_required" return var result: Dictionary = adapter.configure(source) last_code = String(result.get("code", "configure_failed")) if not bool(result.get("ok", false)): return view = adapter.view_state(locale) _sync_screen_from_view() selected_route_id = _default_selection() battle_result_visible = bool(view.get("battle", {}).get("terminal", false)) persistent_change_visible = int(view.get("cleared_rounds", 0)) >= 12 last_events.clear() presented_event_counts.clear() event_presenter.reset(view, reduced_motion, reduced_flashes, low_power) audio_router.reset(muted, low_power, screen == SCREEN_BATTLE) _build_world() _request_render() func _depart_pressed() -> void: var mode := String(view.get("mode", "")) if mode == "market": if selected_route_id.is_empty(): last_code = "route_required" _update_status() return var prepared: Dictionary = adapter.prepare_route(selected_route_id, locale) last_code = String(prepared.get("code", "route_prepare_failed")) if not bool(prepared.get("ok", false)): _update_status() return var receipt: Dictionary = prepared.get("receipt", {}) var operation_id := String(receipt.get("operation_id", "")) if not operation_id.is_empty(): _consume_events([{ "event_id": operation_id, "kind": "route_committed", "source_id": selected_route_id, "target_id": String(view.get("contract_id", "")), "detail": {"offer_id": selected_route_id}, }]) view = adapter.view_state(locale) selected_route_id = _default_selection() _sync_screen_from_view() _request_render() return if mode not in ["plan", "boss_muster"]: last_code = "plan_state_required" _update_status() return if selected_route_id.is_empty(): last_code = "plan_required" _update_status() return var started: Dictionary = adapter.start_plan(selected_route_id, locale) last_code = String(started.get("code", "battle_start_failed")) if not bool(started.get("ok", false)): _update_status() return view = adapter.view_state(locale) screen = SCREEN_BATTLE battle_result_visible = false battle_paused = false battle_accumulator = 0.0 last_events.clear() presented_event_counts.clear() event_presenter.reset(view, reduced_motion, reduced_flashes, low_power) event_presenter.begin_battle(view) audio_router.set_scene(true) var plan_event_id := String(started.get("event_log_hash", "")) if not plan_event_id.is_empty(): _consume_events([{ "event_id": "plan-%s" % plan_event_id, "kind": "plan_committed", "source_id": selected_route_id, "target_id": String(view.get("boss_id", view.get("contract_id", ""))), "detail": {"plan_id": selected_route_id}, }]) _request_render() func _reward_pressed() -> void: var result: Dictionary = adapter.continue_result(locale) last_code = String(result.get("code", "result_continue_failed")) if not bool(result.get("ok", false)): _update_status() return view = adapter.view_state(locale) _sync_screen_from_view() selected_route_id = _default_selection() event_presenter.reset(view, reduced_motion, reduced_flashes, low_power) audio_router.set_scene(false) _request_render() func _settle_pressed() -> void: super._settle_pressed() if screen != SCREEN_BATTLE: selected_route_id = _default_selection() _request_render() func _recovery_pressed() -> void: var result: Dictionary = adapter.recover(locale) last_code = String(result.get("code", "recovery_failed")) if not bool(result.get("ok", false)): _update_status() return view = adapter.view_state(locale) _sync_screen_from_view() selected_route_id = _default_selection() event_presenter.reset(view, reduced_motion, reduced_flashes, low_power) var receipt: Dictionary = result.get("receipt", {}) var operation_id := String(receipt.get("operation_id", "")) var company: Array = view.get("company", []) var target_id := String(company[0].get("recruit_id", "")) if not company.is_empty() else "line" if not operation_id.is_empty(): _consume_events([{ "event_id": operation_id, "kind": "recovery", "source_id": "system", "target_id": target_id, "detail": {"coin_cost": int(receipt.get("coin_cost", 0))}, }]) _request_render() func _commit_drag_order(_source_id: String, _target_id: String) -> void: last_code = "formation_authority_frozen" _update_status() func _render_market_controls() -> void: var mode := String(view.get("mode", "")) if mode == "region_closed": var result: Dictionary = view.get("reward", {}) _label("FG3ClosedTitle", String(result.get("title", "")), Vector2(size.x * 0.16, size.y * 0.22), Vector2(size.x * 0.68, 42), 24, GOLD, HORIZONTAL_ALIGNMENT_CENTER) _label("FG3ClosedBody", String(result.get("body", "")), Vector2(size.x * 0.14, size.y * 0.36), Vector2(size.x * 0.72, 52), 15, TEXT, HORIZONTAL_ALIGNMENT_CENTER) return var options: Array = view.get("routes", []) if mode == "market" else view.get("plans", []) var count := maxi(1, options.size()) var button_width := clampf((size.x - 32.0 - 8.0 * float(count - 1)) / float(count), 112.0, 196.0) var total_width := button_width * options.size() + 8.0 * maxi(0, options.size() - 1) var start_x := maxf(16.0, (size.x - total_width) * 0.5) for index in range(options.size()): var option: Dictionary = options[index] var offer_id := String(option.get("offer_id", option.get("plan_id", ""))) var text := String(option.get("name", offer_id)) var danger := mode == "market" and int(option.get("risk", 0)) == 2 var selected := offer_id == selected_route_id var button := _button( "FG3Option_%s" % offer_id, text, Rect2(start_x + index * (button_width + 8), size.y - 122, button_width, 52), _route_pressed.bind(offer_id), selected, danger ) button.tooltip_text = ( ", ".join(option.get("enemy_names", [])) if mode == "market" else _tr("主响应" if bool(option.get("primary", false)) else "备选响应", "PRIMARY" if bool(option.get("primary", false)) else "ALTERNATE") ) var action_text := _tr("确认路线", "CONFIRM ROUTE") if mode == "market" else _tr("执行方案", "EXECUTE PLAN") if mode == "boss_muster": action_text = _tr("迎战首领", "FACE THE BOSS") _button("FG3ContinueButton", action_text, Rect2(size.x - 198, size.y - 62, 182, 52), _depart_pressed, true) _button("FG3InspectButton", _tr("查看队伍", "INSPECT COMPANY"), Rect2(16, size.y - 60, 148, 48), _inspect_pressed) func _render_reward_controls() -> void: var result: Dictionary = view.get("reward", {}) _label("FG3RecordHeadline", String(result.get("title", "")), Vector2(size.x * 0.16, size.y * 0.22), Vector2(size.x * 0.68, 42), 24, GOLD, HORIZONTAL_ALIGNMENT_CENTER) _label("FG3RecordBody", String(result.get("body", "")), Vector2(size.x * 0.13, size.y * 0.35), Vector2(size.x * 0.74, 54), 14, TEXT, HORIZONTAL_ALIGNMENT_CENTER) var action := String(result.get("action", "")) if not action.is_empty(): _button("FG3RecordButton", action, Rect2(size.x * 0.5 - 125, size.y - 92, 250, 56), _reward_pressed, true) func _render_recovery_controls() -> void: _label("FG3RecoveryHeadline", _tr("战线出现永久缺口", "THE LINE HAS A PERMANENT GAP"), Vector2(size.x * 0.18, size.y * 0.22), Vector2(size.x * 0.64, 38), 22, DANGER, HORIZONTAL_ALIGNMENT_CENTER) _label("FG3RecoveryCause", _tr("阵亡成员不会复活。支付 %d 金币后,以相同情报重新部署幸存者。" % int(view.get("recovery_cost", 0)), "FALLEN RECRUITS DO NOT RETURN. PAY %d COIN TO REDEPLOY SURVIVORS WITH THE SAME INTEL." % int(view.get("recovery_cost", 0))), Vector2(size.x * 0.12, size.y * 0.34), Vector2(size.x * 0.76, 54), 14, TEXT, HORIZONTAL_ALIGNMENT_CENTER) _button("FG3RecoveryButton", _tr("支付并重新部署", "PAY & REDEPLOY"), Rect2(size.x * 0.5 - 120, size.y - 92, 240, 56), _recovery_pressed, true) func _title_text() -> String: var mode := String(view.get("mode", "")) var contract_label := _tr("第一契约", "CONTRACT ONE") if String(view.get("contract_id", "")) == "CTR-R2-01" else _tr("第二契约", "CONTRACT TWO") if screen == SCREEN_BATTLE: var boss_id := String(view.get("battle", {}).get("boss_id", "")) if boss_id == "BO-R2-01": return _tr("水闸背母兽 · 自动交战", "SLUICEBACK MOTHER · AUTO BATTLE") if boss_id == "BO-R2-02": return _tr("溺水钟 · 自动交战", "THE DROWNED BELL · AUTO BATTLE") return _tr("淤潮沼泽 · 自动交战", "SILTWAKE FENS · AUTO BATTLE") if screen == SCREEN_RECOVERY: return _tr("淤潮战线恢复", "SILTWAKE LINE RECOVERY") if screen == SCREEN_REWARD: return _tr("队伍记录", "COMPANY RECORD") if mode == "region_closed": return _tr("淤潮沼泽 · 区域完成", "SILTWAKE FENS · REGION COMPLETE") if mode == "boss_muster": return _tr("%s · 首领集结" % contract_label, "%s · BOSS MUSTER" % contract_label) if mode == "plan": return _tr("%s · 选择应对方案" % contract_label, "%s · CHOOSE RESPONSE" % contract_label) return _tr("淤潮沼泽 %s · 市场 %d" % [contract_label, int(view.get("market_index", 1))], "SILTWAKE %s · MARKET %d" % [contract_label, int(view.get("market_index", 1))]) func _status_text() -> String: var mode := String(view.get("mode", "")) if screen == SCREEN_BATTLE: var battle: Dictionary = view.get("battle", {}) if bool(battle.get("terminal", false)): return _tr("结果:%s · 决定性因果已记录" % _outcome_label(String(battle.get("outcome", ""))), "RESULT: %s · DECISIVE CAUSE RECORDED" % _outcome_label(String(battle.get("outcome", "")))) return _tr("事件 %d · 自动战斗 · 可暂停检查" % int(battle.get("event_count", 0)), "EVENT %d · AUTO BATTLE · PAUSE TO INSPECT" % int(battle.get("event_count", 0))) if screen == SCREEN_RECOVERY: return _tr("幸存 %d 人 · 金币 %d · 恢复花费 %d" % [view.get("company", []).size(), int(view.get("coin", 0)), int(view.get("recovery_cost", 0))], "%d SURVIVORS · %d COIN · RECOVERY COST %d" % [view.get("company", []).size(), int(view.get("coin", 0)), int(view.get("recovery_cost", 0))]) if screen == SCREEN_REWARD: return _tr("记录、声望与区域开放只写入一次", "RECORD, RENOWN AND REGION UNLOCK COMMIT ONCE") if mode == "region_closed": return _tr("两份记录已保存 · RG-03 已开放", "TWO RECORDS SAVED · RG-03 UNLOCKED") var selected := _selected_option() if selected.is_empty(): return _tr("选择一个可见路线或应对方案", "CHOOSE A VISIBLE ROUTE OR RESPONSE") if mode == "market": return _tr("风险 %d · %d 波 · 奖励 %d 金币" % [int(selected.get("risk", 0)), int(selected.get("wave_count", 0)), int(selected.get("reward_coin", 0))], "RISK %d · %d WAVES · %d COIN" % [int(selected.get("risk", 0)), int(selected.get("wave_count", 0)), int(selected.get("reward_coin", 0))]) return _tr("已选方案:%s" % String(selected.get("name", "")), "SELECTED PLAN: %s" % String(selected.get("name", ""))) func _sync_screen_from_view() -> void: match String(view.get("mode", "")): "battle": screen = SCREEN_BATTLE "recovery": screen = SCREEN_RECOVERY "contract_record", "contract_handoff", "region_record": screen = SCREEN_REWARD _: screen = SCREEN_MARKET func _default_selection() -> String: var mode := String(view.get("mode", "")) var options: Array = view.get("routes", []) if mode == "market" else view.get("plans", []) if options.is_empty(): return "" for option in options: if int(option.get("risk", -1)) == 0 or bool(option.get("primary", false)): return String(option.get("offer_id", option.get("plan_id", ""))) return String(options[0].get("offer_id", options[0].get("plan_id", ""))) func _selected_option() -> Dictionary: for option in view.get("routes", []): if String(option.get("offer_id", "")) == selected_route_id: return option for option in view.get("plans", []): if String(option.get("plan_id", "")) == selected_route_id: return option return {}