class_name ABGBattleLivePageController extends RefCounted ## UI-independent BAT-002 Live Battle view-data assembly. ## ## BattleService remains responsible for observation preview/commit, snapshot ## retry, settlement and every receipt boundary. This controller only validates ## and copies the facts that the existing Live Battle View is allowed to show. const VALID_STATES: Array[String] = [ "waiting_for_next_event", "event_forecast", "action_committed", "event_resolving", "event_resolved", "paused", "inspect_handoff", "pause_controls_handoff", "outcome_locked", "invalid_event_data", "snapshot_retrying", "loading_resume", "interrupted_background", "no_resumable_battle", ] func snapshot( company: Dictionary, battle: Dictionary, observation: Dictionary, current_event: Dictionary, state := "waiting_for_next_event", resume_state := "waiting_for_next_event", paused := true, speed := 1.0, notice := "", error_code := "", interruption: Dictionary = {}, pause_log_open := false, locale := "zh_CN" ) -> Dictionary: if company.is_empty(): return {"ok": false, "code": "COMPANY_REQUIRED"} var raw_contract = company.get("active_contract", {}) if not raw_contract is Dictionary or Dictionary(raw_contract).is_empty(): return {"ok": false, "code": "LIVE_CONTRACT_REQUIRED"} if battle.is_empty(): return {"ok": false, "code": "LIVE_BATTLE_REQUIRED"} if String(battle.get("status", "")) not in ["active", "outcome_locked"]: return {"ok": false, "code": "LIVE_BATTLE_INVALID"} if state not in VALID_STATES or resume_state not in VALID_STATES: return {"ok": false, "code": "LIVE_STATE_INVALID"} var raw_observation = observation if not raw_observation is Dictionary or Dictionary(raw_observation).is_empty(): return {"ok": false, "code": "LIVE_OBSERVATION_REQUIRED"} var copied_observation: Dictionary = Dictionary(raw_observation).duplicate(true) var operation_id := String(battle.get("operation_id", "")) if operation_id.is_empty() or String(copied_observation.get("operation_id", "")) != operation_id: return {"ok": false, "code": "LIVE_OPERATION_MISMATCH"} var battle_snapshot_hash := String(battle.get("snapshot_hash", "")) var observation_snapshot_hash := String(copied_observation.get("snapshot_hash", "")) if not battle_snapshot_hash.is_empty() and not observation_snapshot_hash.is_empty() and battle_snapshot_hash != observation_snapshot_hash: return {"ok": false, "code": "LIVE_SNAPSHOT_MISMATCH"} if String(copied_observation.get("status", "")) not in ["waiting_for_next_event", "outcome_locked"]: return {"ok": false, "code": "LIVE_OBSERVATION_INVALID"} if state == "outcome_locked" and String(battle.get("status", "")) != "outcome_locked" and String(copied_observation.get("status", "")) != "outcome_locked": return {"ok": false, "code": "LIVE_OUTCOME_LOCK_MISSING"} var contract: Dictionary = Dictionary(raw_contract).duplicate(true) var members := _members_by_id(company.get("owned_recruits", [])) var party_ids = battle.get("front_to_rear_ids", []) if not party_ids is Array or party_ids.is_empty(): return {"ok": false, "code": "LIVE_PARTY_INVALID"} var party_validation := _validate_ids(party_ids, members) if not party_validation.ok: return party_validation var enemy_ids = battle.get("enemy_ids", []) if not enemy_ids is Array or enemy_ids.is_empty(): return {"ok": false, "code": "LIVE_ENEMY_LINE_INVALID"} var copied_event := Dictionary(current_event).duplicate(true) var copied_events := _copy_array(battle.get("phase_events", [])) var copied_interrupt := Dictionary(interruption).duplicate(true) var ordered_recruits: Array[Dictionary] = [] for position in range(party_validation.ids.size()): var recruit_id := String(party_validation.ids[position]) ordered_recruits.append({ "position": position, "recruit_id": recruit_id, "recruit": Dictionary(members[recruit_id]).duplicate(true), }) var copied_battle: Dictionary = battle.duplicate(true) var state_flags := _state_flags(state) var commands := _commands_for_state(state) return { "ok": true, "page_id": "battle_live", "locale": String(locale), "company": { "company_id": String(company.get("company_id", "")), "owned_count": members.size(), "line_capacity": int(contract.get("line_capacity", party_validation.ids.size())), }, "contract": { "contract_id": String(contract.get("contract_id", "")), "run_id": String(contract.get("run_id", "")), "contract_hash": String(contract.get("contract_hash", "")), }, "battle": { "operation_id": operation_id, "battle_hash": String(battle.get("battle_hash", "")), "status": String(battle.get("status", "")), "snapshot_id": String(battle.get("snapshot_id", "")), "snapshot_hash": battle_snapshot_hash, "front_to_rear_ids": party_validation.ids.duplicate(true), "enemy_ids": _copy_array(enemy_ids), "phase_index": int(battle.get("phase_index", 0)), "total_phases": int(battle.get("total_phases", 0)), "phase_events": copied_events, "copy": copied_battle, }, "observation": copied_observation, "party": { "front_to_rear_ids": party_validation.ids.duplicate(true), "ordered_recruits": ordered_recruits, }, "enemies": { "ids": _copy_array(enemy_ids), "count": enemy_ids.size(), }, "event": copied_event, "recent_events": copied_events.slice(maxi(0, copied_events.size() - 4), copied_events.size()), "state": { "name": String(state), "resume_state": String(resume_state), "paused": bool(paused), "speed": maxf(1.0, float(speed)), "notice": String(notice), "error_code": String(error_code), "pause_log_open": bool(pause_log_open), "interruption": copied_interrupt, "flags": state_flags, }, "commands": commands, } func _state_flags(state: String) -> Dictionary: return { "waiting": state == "waiting_for_next_event", "event_active": state in ["event_forecast", "action_committed", "event_resolving", "event_resolved"], "paused": state in ["paused", "inspect_handoff", "pause_controls_handoff"], "inspect": state == "inspect_handoff", "pause_controls": state == "pause_controls_handoff", "outcome_locked": state == "outcome_locked", "interrupted": state == "interrupted_background", "invalid": state in ["invalid_event_data", "no_resumable_battle"], "loading": state in ["snapshot_retrying", "loading_resume"], } func _commands_for_state(state: String) -> Dictionary: var controls_locked := state in ["outcome_locked", "invalid_event_data", "snapshot_retrying", "loading_resume", "interrupted_background", "no_resumable_battle"] return { "pause_enabled": not controls_locked and state not in ["inspect_handoff", "pause_controls_handoff"], "speed_enabled": not controls_locked and state not in ["action_committed", "event_resolving", "inspect_handoff", "pause_controls_handoff"], "inspect_enabled": state not in ["outcome_locked", "invalid_event_data", "snapshot_retrying", "loading_resume", "interrupted_background", "no_resumable_battle", "pause_controls_handoff"], "settings_enabled": state not in ["invalid_event_data", "snapshot_retrying", "loading_resume", "interrupted_background", "no_resumable_battle"], "next_boundary_enabled": state not in ["action_committed", "event_resolving", "invalid_event_data", "snapshot_retrying", "loading_resume", "no_resumable_battle"], "retry_snapshot_enabled": state == "invalid_event_data", "safe_exit_enabled": state in ["invalid_event_data", "interrupted_background", "no_resumable_battle"], "outcome_handoff_enabled": state == "outcome_locked", "interruption_review_enabled": state == "interrupted_background", } func _validate_ids(values: Array, members: Dictionary) -> Dictionary: var ids: Array = [] for value in values: var recruit_id := String(value) if not members.has(recruit_id): return {"ok": false, "code": "LIVE_RECRUIT_UNKNOWN"} if recruit_id in ids: return {"ok": false, "code": "LIVE_RECRUIT_DUPLICATE"} ids.append(recruit_id) return {"ok": true, "code": "LIVE_PARTY_VALID", "ids": ids} func _members_by_id(values) -> Dictionary: var result := {} if not values is Array: return result for value in values: if value is Dictionary: var copied: Dictionary = Dictionary(value).duplicate(true) var recruit_id := String(copied.get("recruit_id", "")) if not recruit_id.is_empty(): result[recruit_id] = copied return result func _copy_array(values) -> Array: var result: Array = [] if not values is Array: return result for value in values: result.append(value.duplicate(true) if value is Dictionary or value is Array else value) return result