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

304 lines
14 KiB
GDScript

class_name ABGFG4RuntimeController
extends ABGFG2GrayboxController
const FG4Adapter = preload("res://campaign/fg4_adapter.gd")
const FG4EventPresenter = preload("res://campaign/fg2_event_presenter.gd")
const FG4AudioRouter = preload("res://campaign/fg2_audio_router.gd")
func _init() -> void:
if audio_router != null and is_instance_valid(audio_router):
audio_router.free()
adapter = FG4Adapter.new()
event_presenter = FG4EventPresenter.new(
"res://assets/fg4/motion-manifest.json", "abg-fg4-motion-manifest"
)
audio_router = FG4AudioRouter.new(
"res://assets/fg4/audio-manifest.json", "abg-fg4-audio-manifest",
"FG4-MUS-EMBERLOOM", "FG4-AMB-EMBERLOOM"
)
func _initialize_entry() -> void:
var source := pending_snapshot
if source.is_empty():
last_code = "fg4_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)
_sync_region_audio()
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)
_sync_region_audio()
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("FG4ClosedTitle", 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("FG4ClosedBody", 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(
"FG4Option_%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("FG4ContinueButton", action_text, Rect2(size.x - 198, size.y - 62, 182, 52), _depart_pressed, true)
_button("FG4InspectButton", _tr("查看队伍", "INSPECT COMPANY"), Rect2(16, size.y - 60, 148, 48), _inspect_pressed)
func _render_reward_controls() -> void:
var result: Dictionary = view.get("reward", {})
_label("FG4RecordHeadline", 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("FG4RecordBody", 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("FG4RecordButton", action, Rect2(size.x * 0.5 - 125, size.y - 92, 250, 56), _reward_pressed, true)
func _render_recovery_controls() -> void:
_label("FG4RecoveryHeadline", _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)
var cause := _label("FG4RecoveryCause", _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.33), Vector2(size.x * 0.76, 72), 14, TEXT, HORIZONTAL_ALIGNMENT_CENTER)
cause.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
cause.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING
_button("FG4RecoveryButton", _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 region_id := String(view.get("region_id", ""))
var contract_id := String(view.get("contract_id", ""))
var contract_label := _tr("第一契约", "CONTRACT ONE") if contract_id in ["CTR-R3-01", "CTR-R4-01"] else _tr("第二契约", "CONTRACT TWO")
if screen == SCREEN_BATTLE:
var boss_id := String(view.get("battle", {}).get("boss_id", ""))
if boss_id == "BO-R3-01":
return _tr("行走熔炉 · 自动交战", "FURNACE THAT WALKS · AUTO BATTLE")
if boss_id == "BO-R3-02":
return _tr("九火监工 · 自动交战", "FOREMAN NINE-SPARKS · AUTO BATTLE")
if boss_id == "BO-R4-01":
return _tr("行走索引 · 自动交战", "THE WALKING INDEX · AUTO BATTLE")
if boss_id == "BO-R4-02":
return _tr("删节之鹿 · 自动交战", "REDACTION HART · AUTO BATTLE")
return _tr("羽根典藏库 · 自动交战", "QUILLROOT ARCHIVE · AUTO BATTLE") if region_id == "RG-04" else _tr("余烬织境 · 自动交战", "EMBERLOOM WORKS · AUTO BATTLE")
if screen == SCREEN_RECOVERY:
return _tr("典藏战线恢复", "ARCHIVE LINE RECOVERY") if region_id == "RG-04" else _tr("余烬战线恢复", "EMBERLOOM LINE RECOVERY")
if screen == SCREEN_REWARD:
return _tr("队伍记录", "COMPANY RECORD")
if mode == "region_closed":
return _tr("羽根典藏库 · 区域完成", "QUILLROOT ARCHIVE · REGION COMPLETE") if region_id == "RG-04" else _tr("余烬织境 · 区域完成", "EMBERLOOM WORKS · 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))], "QUILLROOT %s · MARKET %d" % [contract_label, int(view.get("market_index", 1))]) if region_id == "RG-04" else _tr("余烬织境 %s · 市场 %d" % [contract_label, int(view.get("market_index", 1))], "EMBERLOOM %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:
var survivors: int = view.get("company", []).size()
var survivor_label := "SURVIVOR" if survivors == 1 else "SURVIVORS"
return _tr("幸存 %d 人 · 金币 %d · 恢复花费 %d" % [survivors, int(view.get("coin", 0)), int(view.get("recovery_cost", 0))], "%d %s · %d COIN · RECOVERY COST %d" % [survivors, survivor_label, 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-05 已开放", "TWO ARCHIVE RECORDS SAVED · RG-05 UNLOCKED") if String(view.get("region_id", "")) == "RG-04" else _tr("两份记录已保存 · RG-04 已开放", "TWO RECORDS SAVED · RG-04 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 {}
func _sync_region_audio() -> void:
if String(view.get("region_id", "")) == "RG-04":
audio_router.set_bed_ids("FG4-MUS-QUILLROOT", "FG4-AMB-QUILLROOT")
else:
audio_router.set_bed_ids("FG4-MUS-EMBERLOOM", "FG4-AMB-EMBERLOOM")