Files
aetherbound-guild/runtime/campaign/fg3_runtime_controller.gd
T

274 lines
12 KiB
GDScript

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 {}