208 lines
8.0 KiB
GDScript
208 lines
8.0 KiB
GDScript
class_name ABGRecoveryPageController
|
|
extends RefCounted
|
|
|
|
## UI-independent OUT-004 Recovery view-data assembly.
|
|
##
|
|
## BattleService remains responsible for recover_next_stage and persistence.
|
|
## This controller validates/copies the completed-consequence boundary and
|
|
## exposes Commit/Market eligibility for the existing Recovery View.
|
|
|
|
|
|
func snapshot(
|
|
company: Dictionary,
|
|
battle: Dictionary,
|
|
result: Dictionary = {},
|
|
reward_draft: Dictionary = {},
|
|
recovery_receipts: Array = [],
|
|
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": "RECOVERY_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": "RECOVERY_CONTRACT_REQUIRED"}
|
|
if battle.is_empty():
|
|
return {"ok": false, "code": "RECOVERY_BATTLE_REQUIRED"}
|
|
if String(battle.get("status", "")) not in ["result", "recovered"]:
|
|
return {"ok": false, "code": "RECOVERY_RESULT_REQUIRED"}
|
|
var operation_id := String(battle.get("operation_id", ""))
|
|
if operation_id.is_empty():
|
|
return {"ok": false, "code": "RECOVERY_OPERATION_REQUIRED"}
|
|
var members := _members_by_id(raw_recruits)
|
|
if members.is_empty():
|
|
return {"ok": false, "code": "RECOVERY_COMPANY_EMPTY"}
|
|
var contract: Dictionary = Dictionary(raw_contract).duplicate(true)
|
|
var copied_result: Dictionary = result
|
|
if copied_result.is_empty():
|
|
copied_result = battle.get("result", {})
|
|
if not copied_result is Dictionary or Dictionary(copied_result).is_empty():
|
|
return {"ok": false, "code": "RECOVERY_RESULT_REQUIRED"}
|
|
copied_result = Dictionary(copied_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": "RECOVERY_RESULT_IDENTITY_MISMATCH"}
|
|
|
|
var source_draft := reward_draft
|
|
if source_draft.is_empty():
|
|
source_draft = battle.get("reward_draft", {})
|
|
if source_draft == null:
|
|
source_draft = {}
|
|
if not source_draft is Dictionary:
|
|
return {"ok": false, "code": "RECOVERY_REWARD_DRAFT_INVALID"}
|
|
var copied_draft: Dictionary = Dictionary(source_draft).duplicate(true)
|
|
var draft_status := "none" if copied_draft.is_empty() else String(copied_draft.get("status", "open"))
|
|
if draft_status == "open":
|
|
return {"ok": false, "code": "RECOVERY_REWARD_REQUIRED"}
|
|
|
|
var raw_knockouts = copied_result.get("knocked_out_ids", battle.get("knocked_out_ids", []))
|
|
if not raw_knockouts is Array:
|
|
return {"ok": false, "code": "RECOVERY_IDS_INVALID"}
|
|
var ids: Array = []
|
|
var id_set := {}
|
|
var identities: Array = []
|
|
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": "RECOVERY_RECRUIT_UNKNOWN"}
|
|
if id_set.has(recruit_id):
|
|
return {"ok": false, "code": "RECOVERY_RECRUIT_DUPLICATE"}
|
|
id_set[recruit_id] = true
|
|
var recruit: Dictionary = Dictionary(members[recruit_id]).duplicate(true)
|
|
identities.append({
|
|
"recruit_id": recruit_id,
|
|
"recruit": recruit,
|
|
"battle_local": true,
|
|
"stable_identity": true,
|
|
"returns_next_stage": true,
|
|
})
|
|
ids.append(recruit_id)
|
|
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": "RECOVERY_IDENTITY_MISMATCH"}
|
|
|
|
var receipt_state := _recovery_receipt(contract, battle, recovery_receipts, operation_id)
|
|
if not bool(receipt_state.get("ok", false)):
|
|
return receipt_state
|
|
var has_receipt := bool(receipt_state.present)
|
|
var battle_status := String(battle.get("status", ""))
|
|
var commit_enabled := battle_status == "result" and not has_receipt
|
|
var copied_battle: Dictionary = battle.duplicate(true)
|
|
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", Array(battle.get("front_to_rear_ids", [])).size())),
|
|
"contract_hash": String(contract.get("contract_hash", "")),
|
|
}
|
|
var copied_receipt: Dictionary = Dictionary(receipt_state.receipt).duplicate(true)
|
|
return {
|
|
"ok": true,
|
|
"page_id": "recovery",
|
|
"locale": String(locale),
|
|
"company": {
|
|
"company_id": String(company.get("company_id", "")),
|
|
"owned_count": members.size(),
|
|
"line_capacity": int(contract.get("line_capacity", Array(battle.get("front_to_rear_ids", [])).size())),
|
|
"recruits": _copy_array(raw_recruits),
|
|
},
|
|
"contract": copied_contract,
|
|
"battle": {
|
|
"operation_id": operation_id,
|
|
"battle_hash": String(battle.get("battle_hash", "")),
|
|
"status": battle_status,
|
|
"snapshot_id": String(battle.get("snapshot_id", "")),
|
|
"snapshot_hash": String(battle.get("snapshot_hash", "")),
|
|
"front_to_rear_ids": _copy_array(battle.get("front_to_rear_ids", [])),
|
|
"enemy_ids": _copy_array(battle.get("enemy_ids", [])),
|
|
"copy": copied_battle,
|
|
},
|
|
"result": copied_result,
|
|
"knockouts": {
|
|
"ids": ids.duplicate(true),
|
|
"count": ids.size(),
|
|
"identities": identities,
|
|
"battle_local": true,
|
|
"returns_next_stage": true,
|
|
},
|
|
"reward": {
|
|
"draft": copied_draft,
|
|
"present": not copied_draft.is_empty(),
|
|
"status": draft_status,
|
|
"completed": draft_status in ["claimed", "declined", "skipped", "none"],
|
|
"pending": false,
|
|
},
|
|
"receipt": {
|
|
"present": has_receipt,
|
|
"recovery": copied_receipt,
|
|
"operation_id": operation_id,
|
|
"receipt_hash": String(copied_receipt.get("receipt_hash", "")),
|
|
"duplicate_safe": bool(receipt_state.duplicate_safe),
|
|
},
|
|
"notice": String(notice),
|
|
"continuation": {
|
|
"target": "market",
|
|
"next_stage_ready": true,
|
|
"same_identity": true,
|
|
},
|
|
"commands": {
|
|
"back_enabled": true,
|
|
"commit_enabled": commit_enabled,
|
|
"market_enabled": has_receipt,
|
|
},
|
|
}
|
|
|
|
|
|
func _recovery_receipt(contract: Dictionary, battle: Dictionary, supplied, operation_id: String) -> Dictionary:
|
|
var source = supplied
|
|
if not source is Array or source.is_empty():
|
|
source = contract.get("recovery_receipts", [])
|
|
if not source is Array:
|
|
return {"ok": false, "code": "RECOVERY_RECEIPTS_INVALID"}
|
|
var matches: Array = []
|
|
for raw_receipt in source:
|
|
if raw_receipt is Dictionary and String(raw_receipt.get("battle_operation_id", raw_receipt.get("operation_id", ""))) == operation_id:
|
|
matches.append(Dictionary(raw_receipt).duplicate(true))
|
|
if matches.is_empty():
|
|
var battle_receipt = battle.get("recovery_receipt", {})
|
|
if battle_receipt is Dictionary and not Dictionary(battle_receipt).is_empty() and String(battle_receipt.get("battle_operation_id", battle_receipt.get("operation_id", ""))) == operation_id:
|
|
matches.append(Dictionary(battle_receipt).duplicate(true))
|
|
if matches.size() > 1:
|
|
return {"ok": false, "code": "RECOVERY_RECEIPT_DUPLICATE"}
|
|
return {
|
|
"ok": true,
|
|
"present": not matches.is_empty(),
|
|
"receipt": Dictionary(matches[0]).duplicate(true) if not matches.is_empty() else {},
|
|
"duplicate_safe": matches.size() <= 1,
|
|
}
|
|
|
|
|
|
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
|