Files
aetherbound-guild/runtime/entry/knockout_review_page_controller.gd
T

242 lines
9.7 KiB
GDScript

class_name ABGKnockoutReviewPageController
extends RefCounted
## UI-independent OUT-002 Knockout Review view-data assembly.
##
## BattleService remains responsible for settlement, reward, recovery and
## persistence. This controller only validates/copies the settled knockout
## facts and exposes the existing acknowledgement route eligibility.
func snapshot(
company: Dictionary,
battle: Dictionary,
result: Dictionary = {},
notice := "",
locale := "zh_CN"
) -> Dictionary:
if company.is_empty():
return {"ok": false, "code": "COMPANY_REQUIRED"}
var raw_recruits = company.get("owned_recruits", [])
if not raw_recruits is Array:
return {"ok": false, "code": "KNOCKOUT_COMPANY_INVALID"}
var raw_contract = company.get("active_contract", {})
if not raw_contract is Dictionary or Dictionary(raw_contract).is_empty():
return {"ok": false, "code": "KNOCKOUT_CONTRACT_REQUIRED"}
if battle.is_empty():
return {"ok": false, "code": "KNOCKOUT_BATTLE_REQUIRED"}
if String(battle.get("status", "")) != "result":
return {"ok": false, "code": "KNOCKOUT_BATTLE_NOT_SETTLED"}
var operation_id := String(battle.get("operation_id", ""))
if operation_id.is_empty():
return {"ok": false, "code": "KNOCKOUT_OPERATION_REQUIRED"}
var members := _members_by_id(raw_recruits)
if members.is_empty():
return {"ok": false, "code": "KNOCKOUT_COMPANY_EMPTY"}
var contract: Dictionary = Dictionary(raw_contract).duplicate(true)
var sourced_result := result
if sourced_result.is_empty():
sourced_result = battle.get("result", {})
if not sourced_result is Dictionary or Dictionary(sourced_result).is_empty():
return {"ok": false, "code": "KNOCKOUT_RESULT_REQUIRED"}
var copied_result: Dictionary = Dictionary(sourced_result).duplicate(true)
var persisted_result = battle.get("result", {})
if persisted_result is Dictionary and not Dictionary(persisted_result).is_empty():
var persisted_hash := String(Dictionary(persisted_result).get("result_hash", ""))
var supplied_hash := String(copied_result.get("result_hash", ""))
if not persisted_hash.is_empty() and not supplied_hash.is_empty() and persisted_hash != supplied_hash:
return {"ok": false, "code": "KNOCKOUT_RESULT_IDENTITY_MISMATCH"}
var result_operation_id := String(copied_result.get("operation_id", ""))
if not result_operation_id.is_empty() and result_operation_id != operation_id:
return {"ok": false, "code": "KNOCKOUT_RESULT_OPERATION_MISMATCH"}
var receipt_state := _settlement_receipt(contract, battle, operation_id)
if not bool(receipt_state.get("ok", false)):
return receipt_state
var raw_knockouts = copied_result.get("knocked_out_ids", battle.get("knocked_out_ids", []))
if not raw_knockouts is Array:
return {"ok": false, "code": "KNOCKOUT_IDS_INVALID"}
var ids: Array = []
var id_set := {}
var identities: Array = []
var front_to_rear: Array = _copy_array(battle.get("front_to_rear_ids", []))
for raw_id in raw_knockouts:
var recruit_id := String(raw_id)
if recruit_id.is_empty() or not members.has(recruit_id):
return {"ok": false, "code": "KNOCKOUT_RECRUIT_UNKNOWN"}
if id_set.has(recruit_id):
return {"ok": false, "code": "KNOCKOUT_RECRUIT_DUPLICATE"}
id_set[recruit_id] = true
ids.append(recruit_id)
var recruit: Dictionary = Dictionary(members[recruit_id]).duplicate(true)
identities.append({
"recruit_id": recruit_id,
"position": front_to_rear.find(recruit_id),
"recruit": recruit,
"battle_local": true,
"deleted": false,
"returns_next_stage": true,
})
var battle_knockouts = battle.get("knocked_out_ids", [])
if battle_knockouts is Array and not battle_knockouts.is_empty() and ids != battle_knockouts:
return {"ok": false, "code": "KNOCKOUT_IDENTITY_MISMATCH"}
var reward_state := _reward_state(battle.get("reward_draft", {}))
if not bool(reward_state.get("ok", false)):
return reward_state
var reward_pending := bool(reward_state.pending)
var acknowledgement_target := "battle_reward_choice" if reward_pending else "battle_recovery"
var copied_battle: Dictionary = battle.duplicate(true)
var copied_events: Array = _copy_array(battle.get("phase_events", []))
var copied_contract: Dictionary = {
"contract_id": String(contract.get("contract_id", "")),
"run_id": String(contract.get("run_id", "")),
"status": String(contract.get("status", "result")),
"difficulty": String(contract.get("difficulty", "standard")),
"coin": int(contract.get("coin", copied_result.get("coin_after", 0))),
"line_capacity": int(contract.get("line_capacity", front_to_rear.size())),
"contract_hash": String(contract.get("contract_hash", "")),
}
var copied_reward: Dictionary = Dictionary(reward_state.draft).duplicate(true)
var receipt: Dictionary = Dictionary(receipt_state.receipt).duplicate(true)
return {
"ok": true,
"page_id": "knockout_review",
"locale": String(locale),
"company": {
"company_id": String(company.get("company_id", "")),
"owned_count": members.size(),
"line_capacity": int(contract.get("line_capacity", front_to_rear.size())),
"recruits": _copy_array(raw_recruits),
},
"contract": copied_contract,
"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": String(battle.get("snapshot_hash", "")),
"phase_index": int(battle.get("phase_index", copied_events.size())),
"total_phases": int(battle.get("total_phases", copied_events.size())),
"front_to_rear_ids": front_to_rear,
"enemy_ids": _copy_array(battle.get("enemy_ids", [])),
"phase_events": copied_events,
"knocked_out_ids": ids.duplicate(true),
"copy": copied_battle,
},
"result": copied_result,
"knockouts": {
"ids": ids.duplicate(true),
"count": ids.size(),
"identities": identities,
"battle_local": true,
"deleted": false,
"returns_next_stage": true,
},
"gap": {
"state": "battle_local_absence" if not ids.is_empty() else "no_knockout",
"battle_local": true,
"company_identity_preserved": true,
"return_rule": "returns_next_stage_same_identity",
},
"reward": {
"draft": copied_reward,
"present": bool(reward_state.present),
"status": String(reward_state.status),
"pending": reward_pending,
"options": _copy_array(copied_reward.get("options", [])),
"options_count": Array(copied_reward.get("options", [])).size(),
},
"receipt": {
"settlement": receipt,
"operation_id": operation_id,
"receipt_hash": String(receipt.get("receipt_hash", "")),
"duplicate_safe": bool(receipt_state.duplicate_safe),
},
"acknowledgement": {
"eligible": true,
"target": acknowledgement_target,
"reward_pending": reward_pending,
"reason": "reward_pending" if reward_pending else "recovery_ready",
"notice": String(notice),
},
"commands": {
"back_enabled": true,
"acknowledge_enabled": true,
"settings_enabled": true,
"reward_review_enabled": reward_pending,
"recovery_enabled": not reward_pending,
},
}
func _settlement_receipt(contract: Dictionary, battle: Dictionary, operation_id: String) -> Dictionary:
var matches: Array = []
var raw_receipts = contract.get("battle_settlement_receipts", [])
if raw_receipts is Array:
for raw_receipt in raw_receipts:
if raw_receipt is Dictionary and String(raw_receipt.get("operation_id", "")) == operation_id:
matches.append(Dictionary(raw_receipt).duplicate(true))
if matches.is_empty():
var battle_receipt = battle.get("settlement_receipt", {})
if battle_receipt is Dictionary and not Dictionary(battle_receipt).is_empty() and String(battle_receipt.get("operation_id", operation_id)) == operation_id:
matches.append(Dictionary(battle_receipt).duplicate(true))
if matches.is_empty():
return {"ok": false, "code": "KNOCKOUT_SETTLEMENT_RECEIPT_REQUIRED"}
if matches.size() > 1:
return {"ok": false, "code": "KNOCKOUT_SETTLEMENT_RECEIPT_DUPLICATE"}
var receipt: Dictionary = matches[0]
var battle_id := String(receipt.get("battle_operation_id", operation_id))
if not battle_id.is_empty() and battle_id != operation_id:
return {"ok": false, "code": "KNOCKOUT_RECEIPT_OPERATION_MISMATCH"}
return {"ok": true, "receipt": receipt, "duplicate_safe": true}
func _reward_state(raw_draft) -> Dictionary:
if raw_draft == null:
raw_draft = {}
if not raw_draft is Dictionary:
return {"ok": false, "code": "KNOCKOUT_REWARD_DRAFT_INVALID"}
var draft: Dictionary = Dictionary(raw_draft).duplicate(true)
if draft.is_empty():
return {"ok": true, "present": false, "pending": false, "status": "none", "draft": {}}
var options = draft.get("options", [])
if not options is Array:
return {"ok": false, "code": "KNOCKOUT_REWARD_OPTIONS_INVALID"}
var option_ids := {}
for option in options:
if not option is Dictionary:
return {"ok": false, "code": "KNOCKOUT_REWARD_OPTION_INVALID"}
var reward_id := String(option.get("reward_id", ""))
if reward_id.is_empty():
return {"ok": false, "code": "KNOCKOUT_REWARD_ID_INVALID"}
if option_ids.has(reward_id):
return {"ok": false, "code": "KNOCKOUT_REWARD_ID_DUPLICATE"}
option_ids[reward_id] = true
var status := String(draft.get("status", "open"))
return {"ok": true, "present": true, "pending": status == "open", "status": status, "draft": draft}
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