class_name ABGBattleOpeningPageController extends RefCounted ## UI-independent BAT-001 Battle Opening view-data assembly. ## ## BattleService remains responsible for loading the active battle and starting ## observation. This controller only validates/copies the phase-zero facts and ## exposes opening-state command eligibility. const VALID_STATES: Array[String] = ["ready_to_observe", "interrupted_resume", "opening_handoff", "snapshot_mismatch"] func snapshot( company: Dictionary, battle: Dictionary, opening_state := "ready_to_observe", notice := "", handoff_receipt: Dictionary = {}, commitment_receipt: Dictionary = {}, 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": "OPENING_CONTRACT_REQUIRED"} if battle.is_empty(): return {"ok": false, "code": "OPENING_BATTLE_REQUIRED"} if String(battle.get("status", "")) != "active": return {"ok": false, "code": "OPENING_BATTLE_INVALID"} if int(battle.get("phase_index", -1)) != 0 or not Array(battle.get("phase_events", [])).is_empty(): return {"ok": false, "code": "OPENING_PHASE_NOT_ZERO"} if opening_state not in VALID_STATES: return {"ok": false, "code": "OPENING_STATE_INVALID"} var contract: Dictionary = Dictionary(raw_contract).duplicate(true) var formation: Dictionary = Dictionary(contract.get("formation", {})).duplicate(true) var raw_snapshot = formation.get("accepted_party_snapshot", {}) if not raw_snapshot is Dictionary or Dictionary(raw_snapshot).is_empty(): return {"ok": false, "code": "OPENING_PARTY_SNAPSHOT_REQUIRED"} var party_snapshot: Dictionary = Dictionary(raw_snapshot).duplicate(true) var members := _members_by_id(company.get("owned_recruits", [])) var party_ids = battle.get("front_to_rear_ids", party_snapshot.get("front_to_rear_ids", [])) if not party_ids is Array or party_ids.size() != int(contract.get("line_capacity", 4)): return {"ok": false, "code": "OPENING_PARTY_ORDER_INVALID"} var order_validation := _validate_ids(party_ids, members) if not order_validation.ok: return order_validation var enemy_ids = battle.get("enemy_ids", []) if not enemy_ids is Array or enemy_ids.is_empty(): return {"ok": false, "code": "OPENING_ENEMY_LINE_INVALID"} var operation_id := String(battle.get("operation_id", "")) if operation_id.is_empty(): return {"ok": false, "code": "OPENING_OPERATION_REQUIRED"} var copied_relations: Array = _copy_array(party_snapshot.get("relations", formation.get("relations", []))) var relation_by_id := _relations_by_id(copied_relations) var ordered_recruits: Array[Dictionary] = [] for position in range(order_validation.ids.size()): var recruit_id := String(order_validation.ids[position]) ordered_recruits.append({ "position": position, "recruit_id": recruit_id, "recruit": Dictionary(members[recruit_id]).duplicate(true), "relation": Dictionary(relation_by_id.get(recruit_id, {})).duplicate(true), }) var resolved_receipt: Dictionary = Dictionary(commitment_receipt).duplicate(true) if resolved_receipt.is_empty(): resolved_receipt = _receipt_for_operation(contract.get("battle_receipts", []), operation_id) var copied_battle: Dictionary = battle.duplicate(true) var copied_handoff: Dictionary = Dictionary(handoff_receipt).duplicate(true) var copied_enemy_ids: Array = _copy_array(enemy_ids) var handoff := opening_state == "opening_handoff" var mismatch := opening_state == "snapshot_mismatch" var interrupted := opening_state == "interrupted_resume" var can_start := not handoff and not mismatch and not resolved_receipt.is_empty() return { "ok": true, "page_id": "battle_opening", "locale": String(locale), "company": { "company_id": String(company.get("company_id", "")), "owned_count": members.size(), "line_capacity": int(contract.get("line_capacity", order_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", "active")), "risk_option_id": String(battle.get("risk_option_id", "")), "risk": int(battle.get("risk", 0)), "snapshot_id": String(battle.get("snapshot_id", party_snapshot.get("snapshot_id", ""))), "snapshot_hash": String(battle.get("snapshot_hash", party_snapshot.get("snapshot_hash", ""))), "phase_index": int(battle.get("phase_index", 0)), "total_phases": int(battle.get("total_phases", 0)), "front_to_rear_ids": order_validation.ids.duplicate(true), "enemy_ids": copied_enemy_ids, "phase_events": _copy_array(battle.get("phase_events", [])), "copy": copied_battle, }, "receipt": { "present": not resolved_receipt.is_empty(), "battle": resolved_receipt, "handoff": copied_handoff, }, "party": { "snapshot_id": String(party_snapshot.get("snapshot_id", "")), "snapshot_hash": String(party_snapshot.get("snapshot_hash", "")), "front_to_rear_ids": order_validation.ids.duplicate(true), "ordered_recruits": ordered_recruits, "orientation": String(party_snapshot.get("orientation", formation.get("orientation", "rear_left_front_right"))), "front_position": int(party_snapshot.get("front_position", formation.get("front_position", 0))), "relations": copied_relations, "coverage": Dictionary(party_snapshot.get("coverage", formation.get("coverage", {}))).duplicate(true), "first_threat": Dictionary(formation.get("first_threat", {})).duplicate(true), }, "enemies": { "ids": copied_enemy_ids, "count": copied_enemy_ids.size(), }, "state": { "name": String(opening_state), "notice": String(notice), "phase_zero": true, "interrupted_resume": interrupted, "handoff": handoff, "snapshot_mismatch": mismatch, }, "commands": { "pause_enabled": true, "speed_enabled": true, "inspect_enabled": true, "settings_enabled": true, "start_observation_enabled": can_start, }, } 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": "OPENING_RECRUIT_UNKNOWN"} if recruit_id in ids: return {"ok": false, "code": "OPENING_RECRUIT_DUPLICATE"} ids.append(recruit_id) return {"ok": true, "code": "OPENING_PARTY_VALID", "ids": ids} func _receipt_for_operation(values, operation_id: String) -> Dictionary: if not values is Array: return {} for value in values: if value is Dictionary and String(value.get("operation_id", "")) == operation_id: return Dictionary(value).duplicate(true) return {} 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 _relations_by_id(values: Array) -> Dictionary: var result := {} for value in values: if value is Dictionary: result[String(value.get("recruit_id", ""))] = Dictionary(value).duplicate(true) 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