Files
aetherbound-guild/runtime/entry/company_page_controller.gd
T
wushenghua 1033ecc5be feat(m84b): carry line position and field state on both member detail surfaces
The brief asks the Member detail for the Recruit's position and readiness, but
neither was reachable: the controller snapshots never carried them. Both
surfaces now show the prepared line position from the active contract's draft
order (reserve members read as not in line) and the current battle's field state
(on the field / fallen this battle). No approved master or slot authority exists
for Roster / Recruit Detail, so this is an incremental improvement and the plan
records that the formal design authority is still pending. The M68 probe gains
the row assertions and a switch that skips the entry-shell captures, which the
dedicated DEV-6 tests cover.
2026-09-18 08:52:13 +08:00

120 lines
4.7 KiB
GDScript

class_name ABGCompanyPageController
extends RefCounted
## UI-independent view-model assembly for the unaccepted Company pages.
## Callers provide a Company snapshot; this controller never loads, saves or
## mutates persistence and never reaches into Godot Control nodes.
func roster_snapshot(company: Dictionary, requested_recruit_id := "", meta: Dictionary = {}) -> Dictionary:
var members: Array[Dictionary] = []
for recruit in company.get("owned_recruits", []):
if recruit is Dictionary:
members.append(_member_snapshot(recruit))
var selected_id := _selected_id(members, requested_recruit_id)
var selected := _member_by_id(members, selected_id)
var dismissal := _dismissal_state(company, selected_id)
return {
"ok": true,
"page_id": "roster",
"selected_recruit_id": selected_id,
"members": _with_line_meta(_copy_array(members), meta),
"selected": selected,
"dismissal": dismissal,
}
func recruit_detail_snapshot(company: Dictionary, recruit_id: String, meta: Dictionary = {}) -> Dictionary:
var recruit := _find_recruit(company, recruit_id)
if recruit.is_empty():
return {"ok": false, "code": "RECRUIT_NOT_FOUND", "recruit_id": recruit_id}
var member := _member_snapshot(recruit)
return {
"ok": true,
"page_id": "recruit_detail",
"selected_recruit_id": recruit_id,
"recruit": _with_line_meta([member], meta)[0],
"dismissal": _dismissal_state(company, recruit_id),
}
func _with_line_meta(members: Array, meta: Dictionary) -> Array:
# M84-B: the prepared line order comes from the active contract's formation and
# the current-battle state names the members who left the field, so the detail
# surfaces can show where a Recruit stands and whether the run still counts them
# as on the field. Reserve members (not in the line) get position -1.
var order: Array = Array(meta.get("front_to_rear_ids", []))
var knocked_out: Array = Array(meta.get("knocked_out_ids", []))
var out: Array = []
for raw in members:
var member: Dictionary = Dictionary(raw).duplicate(true)
var recruit_id := String(member.get("recruit_id", ""))
member["position"] = order.find(recruit_id)
member["readiness"] = "fallen" if recruit_id in knocked_out else "ready"
out.append(member)
return out
func _member_snapshot(recruit: Dictionary) -> Dictionary:
return {
"recruit_id": String(recruit.get("recruit_id", "")),
"rendered_name_snapshot": Dictionary(recruit.get("rendered_name_snapshot", {})).duplicate(true),
"profession_id": String(recruit.get("profession_id", "")),
"profession_job": String(recruit.get("profession_job", "")),
"profession_name": Dictionary(recruit.get("profession_name", {})).duplicate(true),
"trait_id": String(recruit.get("trait_id", "")),
"trait_name": Dictionary(recruit.get("trait_name", {})).duplicate(true),
"level": int(recruit.get("level", 0)),
"xp": int(recruit.get("xp", 0)),
"equipped_item_id": String(recruit.get("equipped_item_id", "")),
"starting_item_id": String(recruit.get("starting_item_id", "")),
"starting_item_name": Dictionary(recruit.get("starting_item_name", {})).duplicate(true),
"battle_history": Dictionary(recruit.get("battle_history", {})).duplicate(true),
}
func _dismissal_state(company: Dictionary, recruit_id: String) -> Dictionary:
var member_count := Array(company.get("owned_recruits", [])).size()
var phase := String(Dictionary(company.get("active_contract", {})).get("status", ""))
var eligible := not recruit_id.is_empty() and member_count > 1 and phase == "market"
var reason := "eligible"
if recruit_id.is_empty():
reason = "no_selection"
elif member_count <= 1:
reason = "last_member"
elif phase != "market":
reason = "phase_locked"
return {
"eligible": eligible,
"reason": reason,
"member_count": member_count,
"phase": phase,
}
func _selected_id(members: Array[Dictionary], requested_recruit_id: String) -> String:
if not requested_recruit_id.is_empty() and not _member_by_id(members, requested_recruit_id).is_empty():
return requested_recruit_id
return String(members[0].get("recruit_id", "")) if not members.is_empty() else ""
func _find_recruit(company: Dictionary, recruit_id: String) -> Dictionary:
for recruit in company.get("owned_recruits", []):
if recruit is Dictionary and String(recruit.get("recruit_id", "")) == recruit_id:
return Dictionary(recruit).duplicate(true)
return {}
func _member_by_id(members: Array[Dictionary], recruit_id: String) -> Dictionary:
for member in members:
if String(member.get("recruit_id", "")) == recruit_id:
return Dictionary(member).duplicate(true)
return {}
func _copy_array(values: Array[Dictionary]) -> Array[Dictionary]:
var copied: Array[Dictionary] = []
for value in values:
copied.append(Dictionary(value).duplicate(true))
return copied