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

208 lines
7.5 KiB
GDScript

class_name ABGReadinessPageController
extends RefCounted
## UI-independent Readiness Review view-data assembly.
##
## FormationRiskService remains responsible for loading, saving and accepting
## the readiness snapshot. This controller only validates the supplied saved
## formation boundary and copies the facts the existing PTY-002 View renders.
const VALID_STATUSES: Array[String] = ["ordered", "ready"]
func snapshot(
company: Dictionary,
formation: Dictionary,
warnings: Array = [],
reviewed_recruit_id := "",
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": "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": "FORMATION_CONTRACT_REQUIRED"}
if formation.is_empty():
return {"ok": false, "code": "FORMATION_REQUIRED"}
var contract: Dictionary = Dictionary(raw_contract).duplicate(true)
var members := _members_by_id(raw_recruits)
var capacity := int(formation.get("line_capacity", contract.get("line_capacity", 0)))
if capacity <= 0:
return {"ok": false, "code": "READINESS_CAPACITY_INVALID"}
var saved_order = formation.get("saved_front_to_rear_ids", [])
if not saved_order is Array or saved_order.is_empty():
return {"ok": false, "code": "READINESS_ORDER_REQUIRED"}
var order_validation := _validate_saved_order(saved_order, members, capacity)
if not order_validation.ok:
return order_validation
var status := String(formation.get("status", ""))
if status not in VALID_STATUSES:
return {"ok": false, "code": "READINESS_STATUS_INVALID"}
var raw_receipts = formation.get("readiness_receipts", [])
if not raw_receipts is Array:
return {"ok": false, "code": "READINESS_RECEIPTS_INVALID"}
if status == "ready" and raw_receipts.is_empty():
return {"ok": false, "code": "READINESS_RECEIPT_REQUIRED"}
if not warnings is Array:
return {"ok": false, "code": "READINESS_WARNINGS_INVALID"}
var normalized_order: Array = order_validation.ids
var copied_relations: Array = _copy_array(formation.get("relations", []))
var relation_by_id := _relations_by_id(copied_relations)
var ordered_recruits: Array[Dictionary] = []
for position in range(normalized_order.size()):
var recruit_id := String(normalized_order[position])
var recruit: Dictionary = Dictionary(members[recruit_id]).duplicate(true)
ordered_recruits.append({
"position": position,
"recruit_id": recruit_id,
"recruit": recruit,
"relation": Dictionary(relation_by_id.get(recruit_id, {})).duplicate(true),
})
var selected := String(reviewed_recruit_id) if String(reviewed_recruit_id) in normalized_order else ""
var copied_coverage: Dictionary = Dictionary(formation.get("coverage", {})).duplicate(true)
var copied_first_threat: Dictionary = Dictionary(formation.get("first_threat", {})).duplicate(true)
var copied_receipts: Array = _copy_array(raw_receipts)
var copied_snapshot: Dictionary = Dictionary(formation.get("accepted_party_snapshot", {})).duplicate(true)
var front_recruit: Dictionary = Dictionary(members[String(normalized_order[0])]).duplicate(true)
var state := "ready" if status == "ready" else "ordered"
var order_hash := _hash(normalized_order)
var contract_data := {
"contract_id": String(contract.get("contract_id", "")),
"run_id": String(contract.get("run_id", "")),
"difficulty": String(contract.get("difficulty", "standard")),
"coin": int(contract.get("coin", 0)),
"line_capacity": int(contract.get("line_capacity", capacity)),
}
var company_data := {
"company_id": String(company.get("company_id", "")),
"owned_count": members.size(),
"line_capacity": capacity,
"recruits": _copy_array(raw_recruits),
}
return {
"ok": true,
"page_id": "readiness_review",
"locale": String(locale),
"company": company_data,
"contract": contract_data,
"formation": {
"status": status,
"state": state,
"orientation": String(formation.get("orientation", "rear_left_front_right")),
"front_position": int(formation.get("front_position", 0)),
"line_capacity": capacity,
"order_revision": int(formation.get("order_revision", 0)),
"first_threat": copied_first_threat,
"relations": copied_relations,
"coverage": copied_coverage,
"risk_option_ids": _risk_option_ids(formation.get("risk_options", []), copied_snapshot),
},
"order": {
"front_to_rear_ids": normalized_order.duplicate(true),
"order_hash": order_hash,
"ordered_recruits": ordered_recruits,
"front_recruit": front_recruit,
"reviewed_recruit_id": selected,
"review_reason": "selected" if not selected.is_empty() else "no_selection",
},
"warnings": {
"rows": _copy_array(warnings),
"count": warnings.size(),
},
"receipt": {
"present": not copied_receipts.is_empty(),
"latest": Dictionary(copied_receipts.back()).duplicate(true) if not copied_receipts.is_empty() and copied_receipts.back() is Dictionary else {},
"all": copied_receipts,
},
"accepted_party_snapshot": copied_snapshot,
"commands": {
"settings_enabled": true,
"member_review_enabled": true,
"warning_review_enabled": true,
"revise_enabled": true,
"back_enabled": true,
"accept_enabled": state == "ordered",
"risk_enabled": state == "ready",
},
}
func _validate_saved_order(values: Array, members: Dictionary, capacity: int) -> Dictionary:
if values.size() != capacity:
return {"ok": false, "code": "READINESS_COUNT_INVALID"}
var ids: Array = []
for value in values:
var recruit_id := String(value)
if not members.has(recruit_id):
return {"ok": false, "code": "READINESS_RECRUIT_UNKNOWN"}
if recruit_id in ids:
return {"ok": false, "code": "READINESS_RECRUIT_DUPLICATE"}
ids.append(recruit_id)
return {"ok": true, "code": "READINESS_ORDER_VALID", "ids": ids}
func _members_by_id(values) -> Dictionary:
var 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 recruit_id.is_empty():
continue
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 _risk_option_ids(options, accepted_snapshot: Dictionary) -> Array:
var result: Array = []
if options is Array:
for option in options:
if option is Dictionary:
result.append(String(option.get("risk_option_id", "")))
if result.is_empty() and accepted_snapshot.has("risk_option_ids"):
result = _copy_array(accepted_snapshot.get("risk_option_ids", []))
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
func _hash(value) -> String:
return JSON.stringify(_canonical(value)).sha256_text()
func _canonical(value):
if value is float and is_equal_approx(value, round(value)):
return int(round(value))
if value is Dictionary:
var result := {}
var keys: Array = value.keys()
keys.sort_custom(func(left, right): return String(left) < String(right))
for key in keys:
result[key] = _canonical(value[key])
return result
if value is Array:
var result: Array = []
for item in value:
result.append(_canonical(item))
return result
return value