refactor(runtime): route risk commitment through controller

This commit is contained in:
2026-08-25 17:14:07 +08:00
parent 3e80083806
commit 9518e2eac8
2 changed files with 159 additions and 31 deletions
+155 -31
View File
@@ -16,6 +16,7 @@ const GuildPageControllerScript = preload("res://entry/guild_page_controller.gd"
const PartyLinePageControllerScript = preload("res://entry/party_line_page_controller.gd")
const ReadinessPageControllerScript = preload("res://entry/readiness_page_controller.gd")
const RiskBoardPageControllerScript = preload("res://entry/risk_board_page_controller.gd")
const RiskCommitmentPageControllerScript = preload("res://entry/risk_commitment_page_controller.gd")
const MarketPageControllerScript = preload("res://entry/market_page_controller.gd")
const RecruitReviewPageControllerScript = preload("res://entry/recruit_review_page_controller.gd")
const ContractChoicePageControllerScript = preload("res://entry/contract_choice_page_controller.gd")
@@ -115,6 +116,7 @@ var guild_page_controller := GuildPageControllerScript.new()
var party_line_page_controller := PartyLinePageControllerScript.new()
var readiness_page_controller := ReadinessPageControllerScript.new()
var risk_board_page_controller := RiskBoardPageControllerScript.new()
var risk_commitment_page_controller := RiskCommitmentPageControllerScript.new()
var market_page_controller := MarketPageControllerScript.new()
var recruit_review_page_controller := RecruitReviewPageControllerScript.new()
var contract_choice_page_controller := ContractChoicePageControllerScript.new()
@@ -182,6 +184,7 @@ var dismissal_notice := ""
var contract_return_notice := ""
var risk_selected_id := ""
var risk_view_data: Dictionary = {}
var risk_commitment_view_data: Dictionary = {}
var risk_commitment_acknowledged := false
var risk_commitment_bound_risk_id := ""
var risk_commitment_bound_snapshot_hash := ""
@@ -259,6 +262,7 @@ func _ready() -> void:
app_services.register_service(&"party_line_page_controller", party_line_page_controller)
app_services.register_service(&"readiness_page_controller", readiness_page_controller)
app_services.register_service(&"risk_board_page_controller", risk_board_page_controller)
app_services.register_service(&"risk_commitment_page_controller", risk_commitment_page_controller)
app_services.register_service(&"market_page_controller", market_page_controller)
app_services.register_service(&"recruit_review_page_controller", recruit_review_page_controller)
app_services.register_service(&"contract_choice_page_controller", contract_choice_page_controller)
@@ -3816,6 +3820,7 @@ func show_risk_commitment(company := {}) -> void:
var snapshot_hash := String(snapshot.get("snapshot_hash", ""))
var option_hash := String(option.get("option_hash", ""))
var contract_hash := String(resolved.active_contract.get("contract_hash", ""))
var had_binding := not risk_commitment_bound_risk_id.is_empty()
var binding_changed := risk_commitment_bound_risk_id != risk_selected_id or risk_commitment_bound_snapshot_hash != snapshot_hash or risk_commitment_bound_option_hash != option_hash or risk_commitment_bound_contract_hash != contract_hash
if risk_commitment_bound_risk_id.is_empty():
risk_commitment_bound_risk_id = risk_selected_id
@@ -3830,6 +3835,29 @@ func show_risk_commitment(company := {}) -> void:
risk_commitment_bound_snapshot_hash = snapshot_hash
risk_commitment_bound_option_hash = option_hash
risk_commitment_bound_contract_hash = contract_hash
var dependency := _risk_commitment_dependency_status(resolved, formation, option)
var binding := {
"selected_risk_id": risk_selected_id,
"snapshot_hash": snapshot_hash,
"option_hash": option_hash,
"contract_hash": contract_hash,
"stale": had_binding and binding_changed,
}
var commitment_snapshot: Dictionary = risk_commitment_page_controller.snapshot(
resolved,
formation,
option,
_risk_commitment_consequence_facts(resolved.active_contract, option),
dependency,
binding,
risk_commitment_acknowledged,
String(settings.get("locale", "zh_CN"))
)
if not bool(commitment_snapshot.get("ok", false)):
party_notice = _formation_error_text(String(commitment_snapshot.get("code", "RISK_COMMITMENT_LOAD_FAILED")))
show_risk_board(resolved)
return
risk_commitment_view_data = commitment_snapshot
_build_risk_commitment_master_base()
_build_risk_commitment_master_ui(resolved, formation, option)
@@ -3957,18 +3985,110 @@ func _risk_commitment_marker(parent: Control, node_name: String, area: Rect2, gl
return marker
func _build_risk_commitment_master_ui(resolved: Dictionary, formation: Dictionary, option: Dictionary) -> void:
func _risk_commitment_consequence_facts(contract: Dictionary, option: Dictionary) -> Array:
return [
{
"id": "threat",
"label": _tr("威胁", "THREAT"),
"text": _tr("%d人 · %d波", "%d · %d WAVES") % [int(option.get("known_threat", {}).get("enemy_count", 0)), int(option.get("known_threat", {}).get("wave_count", 0))],
"value": _tr("%d人 · %d波", "%d · %d WAVES") % [int(option.get("known_threat", {}).get("enemy_count", 0)), int(option.get("known_threat", {}).get("wave_count", 0))],
"glyph": "!",
"color": Color("#a74337"),
},
{
"id": "uncertainty",
"label": _tr("不确定", "UNCERTAINTY"),
"text": _risk_uncertainty_short(String(option.get("uncertainty_id", ""))),
"value": _risk_uncertainty_short(String(option.get("uncertainty_id", ""))),
"glyph": "?",
"color": Color("#5d3f91"),
},
{
"id": "entry",
"label": _tr("入场 Coin", "ENTRY COIN"),
"text": _tr("%d · 余额 %d", "%d · %d LEFT") % [int(option.get("entry_cost", 0)), int(contract.get("coin", 0))],
"value": _tr("%d · 余额 %d", "%d · %d LEFT") % [int(option.get("entry_cost", 0)), int(contract.get("coin", 0))],
"glyph": "¢",
"color": Color("#a07816"),
},
{
"id": "reward",
"label": _tr("胜利回报", "REWARD"),
"text": _tr("%d · %s", "%d · %s") % [int(option.get("reward_coin", 0)), _risk_reward_short(String(option.get("reward_class_id", "")))],
"value": _tr("%d · %s", "%d · %s") % [int(option.get("reward_coin", 0)), _risk_reward_short(String(option.get("reward_class_id", "")))],
"glyph": "◆",
"color": Color("#3d7449"),
},
{
"id": "recovery",
"label": _tr("恢复暴露", "RECOVERY"),
"text": _recovery_name(String(option.get("recovery_exposure_id", ""))),
"value": _recovery_name(String(option.get("recovery_exposure_id", ""))),
"glyph": "!",
"color": Color("#a44a2e"),
},
{
"id": "retreat",
"label": _tr("撤退规则", "RETREAT"),
"text": _tr("下阶段归队", "NEXT STAGE"),
"value": _tr("下阶段归队", "NEXT STAGE"),
"glyph": "↩",
"color": Color("#2c67a3"),
},
]
func _build_risk_commitment_master_ui(resolved: Dictionary, formation: Dictionary, source_option: Dictionary) -> void:
var canvas := risk_commitment_master_design_canvas
if not is_instance_valid(canvas):
return
var view_data: Dictionary = risk_commitment_view_data
if not bool(view_data.get("ok", false)):
var fallback_dependency := _risk_commitment_dependency_status(resolved, formation, source_option)
view_data = risk_commitment_page_controller.snapshot(
resolved,
formation,
source_option,
_risk_commitment_consequence_facts(resolved.active_contract, source_option),
fallback_dependency,
{
"selected_risk_id": risk_selected_id,
"snapshot_hash": String(formation.get("accepted_party_snapshot", {}).get("snapshot_hash", "")),
"option_hash": String(source_option.get("option_hash", "")),
"contract_hash": String(resolved.active_contract.get("contract_hash", "")),
"stale": false,
},
risk_commitment_acknowledged,
String(settings.get("locale", "zh_CN"))
)
if not bool(view_data.get("ok", false)):
return
var view_contract: Dictionary = Dictionary(view_data.get("contract", {}))
var view_risk: Dictionary = Dictionary(view_data.get("risk", {}))
var view_option: Dictionary = Dictionary(view_risk.get("option", {}))
var view_party: Dictionary = Dictionary(view_data.get("party", {}))
var view_dependency: Dictionary = Dictionary(view_data.get("dependency", {}))
var dependency_facts: Dictionary = Dictionary(view_dependency.get("facts", {}))
var view_confirmation: Dictionary = Dictionary(view_data.get("confirmation", {}))
var view_commands: Dictionary = Dictionary(view_data.get("commands", {}))
var view_facts: Array = Array(Dictionary(view_data.get("consequences", {})).get("facts", []))
var party_recruits: Array = Array(view_party.get("ordered_recruits", []))
var acknowledged := bool(view_confirmation.get("acknowledged", false))
var overlay := Control.new()
overlay.name = "RiskCommitmentMasterOverlay"
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
overlay.mouse_filter = Control.MOUSE_FILTER_PASS
canvas.add_child(overlay)
var contract: Dictionary = resolved.active_contract
var snapshot: Dictionary = formation.get("accepted_party_snapshot", {})
var party_ids: Array = Array(snapshot.get("front_to_rear_ids", formation.get("saved_front_to_rear_ids", [])))
var dependency := _risk_commitment_dependency_status(resolved, formation, option)
var ready := risk_commitment_acknowledged and bool(dependency.get("ok", false))
var contract: Dictionary = view_contract
var option: Dictionary = view_option
var dependency: Dictionary = dependency_facts
var snapshot: Dictionary = {
"snapshot_id": String(view_party.get("snapshot_id", "")),
"snapshot_hash": String(view_party.get("snapshot_hash", "")),
"front_to_rear_ids": Array(view_party.get("front_to_rear_ids", [])).duplicate(true),
}
var party_ids: Array = Array(view_party.get("front_to_rear_ids", []))
var ready := bool(view_commands.get("begin_enabled", false))
_risk_commitment_label(overlay, "RiskCommitmentPageTitle", Rect2(0.08, 0.018, 0.27, 0.052), _tr("风险承诺", "RISK COMMITMENT"), 12, Color("#fff1c2"))
_risk_commitment_label(overlay, "RiskCommitmentHeaderRule", Rect2(0.08, 0.068, 0.30, 0.032), _tr("在进入战斗前核对一条已选路线。", "VERIFY ONE SELECTED ROUTE BEFORE BATTLE."), 7, Color("#e8d39d"))
@@ -3986,14 +4106,15 @@ func _build_risk_commitment_master_ui(resolved: Dictionary, formation: Dictionar
source.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
var contract_id := String(contract.get("contract_id", "CTR-R1-01"))
_risk_commitment_label(overlay, "RiskSelectedRegion", Rect2(0.10, 0.53, 0.16, 0.04), _tr("区域 %s", "REGION %s") % contract_id, 6, Color("#3d3124"), HORIZONTAL_ALIGNMENT_CENTER)
var slot_labels := [
[_tr("威胁", "THREAT"), _tr("%d人 · %d波", "%d · %d WAVES") % [int(option.known_threat.enemy_count), int(option.known_threat.wave_count)], "!", Color("#a74337")],
[_tr("信息", "INFO"), _risk_uncertainty_short(String(option.uncertainty_id)), "?", Color("#5d3f91")],
[_tr("入场", "ENTRY"), _tr("%d 金", "%d COIN") % int(option.entry_cost), "¢", Color("#a07816")],
[_tr("回报", "REWARD"), _tr("%d · %s", "%d · %s") % [int(option.reward_coin), _risk_reward_short(String(option.reward_class_id))], "◆", Color("#3d7449")],
[_tr("恢复", "RECOVERY"), _recovery_name(String(option.recovery_exposure_id)), "!", Color("#a44a2e")],
[_tr("撤退", "RETREAT"), _tr("下阶段归队", "NEXT STAGE"), "↩", Color("#2c67a3")],
]
var slot_labels: Array = []
for fact in view_facts:
if fact is Dictionary:
slot_labels.append([
String(fact.get("label", "")),
String(fact.get("value", fact.get("text", ""))),
String(fact.get("glyph", "•")),
fact.get("color", Color("#3d7449")),
])
for index in range(slot_labels.size()):
var y := 0.115 + float(index) * 0.073
_risk_commitment_marker(overlay, "RiskSlotMarker%d" % index, Rect2(0.265, y + 0.004, 0.026, 0.038), String(slot_labels[index][2]), slot_labels[index][3])
@@ -4025,14 +4146,15 @@ func _build_risk_commitment_master_ui(resolved: Dictionary, formation: Dictionar
fact_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
fact_root.mouse_filter = Control.MOUSE_FILTER_IGNORE
overlay.add_child(fact_root)
var facts := [
[_tr("威胁", "THREAT"), _tr("%d人 · %d波", "%d · %d WAVES") % [int(option.known_threat.enemy_count), int(option.known_threat.wave_count)], "!", Color("#a74337")],
[_tr("不确定", "UNCERTAINTY"), _risk_uncertainty_short(String(option.uncertainty_id)), "?", Color("#5d3f91")],
[_tr("入场 Coin", "ENTRY COIN"), _tr("%d · 余额 %d", "%d · %d LEFT") % [int(option.entry_cost), int(contract.get("coin", 0))], "¢", Color("#a07816")],
[_tr("胜利回报", "REWARD"), _tr("%d · %s", "%d · %s") % [int(option.reward_coin), _risk_reward_short(String(option.reward_class_id))], "◆", Color("#3d7449")],
[_tr("恢复暴露", "RECOVERY"), _recovery_name(String(option.recovery_exposure_id)), "!", Color("#a44a2e")],
[_tr("撤退规则", "RETREAT"), _tr("下阶段归队", "NEXT STAGE"), "↩", Color("#2c67a3")],
]
var facts: Array = []
for fact in view_facts:
if fact is Dictionary:
facts.append([
String(fact.get("label", "")),
String(fact.get("value", fact.get("text", ""))),
String(fact.get("glyph", "•")),
fact.get("color", Color("#3d7449")),
])
for index in range(facts.size()):
var fact := Control.new()
fact.name = "RiskConsequenceFact%d" % index
@@ -4055,10 +4177,11 @@ func _build_risk_commitment_master_ui(resolved: Dictionary, formation: Dictionar
var snapshot_version := String(snapshot.get("snapshot_id", snapshot.get("snapshot_hash", ""))).substr(0, 10)
_risk_commitment_label(overlay, "RiskPartySnapshotVersion", Rect2(0.80, 0.055, 0.145, 0.04), _tr("快照 %s", "SNAP %s") % snapshot_version, 7, Color("#463725"), HORIZONTAL_ALIGNMENT_RIGHT)
for visual_index in range(party_ids.size() - 1, -1, -1):
var position_index := visual_index
var ordered: Dictionary = Dictionary(party_recruits[visual_index]) if visual_index < party_recruits.size() else {}
var position_index := int(ordered.get("position", visual_index))
var display_index := party_ids.size() - 1 - visual_index
var recruit_id := String(party_ids[visual_index])
var recruit := _recruit_by_id(resolved, recruit_id)
var recruit: Dictionary = Dictionary(ordered.get("recruit", {})).duplicate(true)
var member := Control.new()
member.name = "RiskFrozenMember%d" % display_index
_risk_commitment_place(member, Rect2(float(display_index) * 0.245, 0.0, 0.225, 1.0))
@@ -4075,24 +4198,25 @@ func _build_risk_commitment_master_ui(resolved: Dictionary, formation: Dictionar
_risk_commitment_place(actor, Rect2(0.02, 0.02, 0.96, 0.72))
actor.mouse_filter = Control.MOUSE_FILTER_IGNORE
member.add_child(actor)
var relation := _risk_commitment_relation_for(formation, recruit_id)
var relation: Dictionary = Dictionary(ordered.get("relation", {})).duplicate(true)
var exposure := _risk_commitment_exposure_text(String(relation.get("exposure_id", "rear")))
var marker := _risk_commitment_marker(member, "PartyFactMarker", Rect2(0.28, 0.10, 0.16, 0.14), "●", Color("#3d7449"))
marker.set_meta("reach_id", String(relation.get("reach_id", "limited")))
_risk_commitment_label(member, "PartyPosition", Rect2(0.02, 0.70, 0.96, 0.12), _tr("位置 %d · %s", "POS %d · %s") % [position_index, exposure], 7, Color("#3a3025"), HORIZONTAL_ALIGNMENT_CENTER)
_risk_commitment_label(member, "PartyIdentity", Rect2(0.0, 0.81, 1.0, 0.19), _tr("%s\n%s", "%s\n%s") % [_recruit_name(recruit), _recruit_profession_name(recruit)], 7, Color("#2e271e"), HORIZONTAL_ALIGNMENT_CENTER)
_risk_commitment_label(overlay, "RiskPartyCoverage", Rect2(0.49, 0.505, 0.44, 0.035), _tr("覆盖 · 保护 %d 远程 %d 续航 %d 控制 %d", "COVERAGE · GUARD %d REACH %d SUSTAIN %d CONTROL %d") % [int(formation.coverage.get("protection", 0)), int(formation.coverage.get("long_reach", 0)), int(formation.coverage.get("sustain", 0)), int(formation.coverage.get("control", 0))], 6, Color("#3e3428"), HORIZONTAL_ALIGNMENT_CENTER)
var party_coverage: Dictionary = Dictionary(view_party.get("coverage", {}))
_risk_commitment_label(overlay, "RiskPartyCoverage", Rect2(0.49, 0.505, 0.44, 0.035), _tr("覆盖 · 保护 %d 远程 %d 续航 %d 控制 %d", "COVERAGE · GUARD %d REACH %d SUSTAIN %d CONTROL %d") % [int(party_coverage.get("protection", 0)), int(party_coverage.get("long_reach", 0)), int(party_coverage.get("sustain", 0)), int(party_coverage.get("control", 0))], 6, Color("#3e3428"), HORIZONTAL_ALIGNMENT_CENTER)
# The command rail remains a transparent semantic overlay on the authored
# dark rail. Begin Battle is disabled until acknowledgement and dependency
# validation both succeed.
var cancel := _risk_commitment_command_button("RiskCommitmentCancel", Rect2(0.03, 0.83, 0.19, 0.15), _tr("返回风险板", "RISK BOARD"), "‹", cancel_risk_commitment, false)
overlay.add_child(cancel)
var acknowledge := _risk_commitment_command_button("RiskCommitmentAcknowledge", Rect2(0.40, 0.83, 0.20, 0.15), _tr("确认", "ACK") if risk_commitment_acknowledged else _tr("确认", "ACK"), "✓" if risk_commitment_acknowledged else "!", toggle_risk_commitment_confirmation, true)
acknowledge.set_meta("confirmed", risk_commitment_acknowledged)
var acknowledge := _risk_commitment_command_button("RiskCommitmentAcknowledge", Rect2(0.40, 0.83, 0.20, 0.15), _tr("确认", "ACK"), "✓" if acknowledged else "!", toggle_risk_commitment_confirmation, true, not bool(view_commands.get("acknowledge_enabled", false)))
acknowledge.set_meta("confirmed", acknowledged)
overlay.add_child(acknowledge)
var begin := _risk_commitment_command_button("RiskBeginBattle", Rect2(0.78, 0.83, 0.19, 0.15), _tr("开始战斗", "BEGIN BATTLE"), "➜", begin_battle, true, not ready)
begin.tooltip_text = _tr("确认后创建一次承诺回执并进入战斗开场。", "After acknowledgement, create one commitment receipt and enter Battle Opening.") if not begin.disabled else _risk_commitment_block_reason(dependency, risk_commitment_acknowledged)
begin.tooltip_text = _tr("确认后创建一次承诺回执并进入战斗开场。", "After acknowledgement, create one commitment receipt and enter Battle Opening.") if not begin.disabled else _risk_commitment_block_reason(dependency, acknowledged)
_set_risk_commitment_button_disabled(begin, not ready)
overlay.add_child(begin)
# Historical DEV-4 callers inspect this node to verify the pre-commit boundary.
@@ -4108,9 +4232,9 @@ func _build_risk_commitment_master_ui(resolved: Dictionary, formation: Dictionar
summary.mouse_filter = Control.MOUSE_FILTER_IGNORE
var reason_text := risk_commitment_notice
if reason_text.is_empty():
reason_text = _tr("先确认后果;确认前不改变金币或队伍。", "Acknowledge first; Coin and Party stay unchanged.") if not risk_commitment_acknowledged else _tr("已确认:下一步进入战斗开场。", "Acknowledged: next step opens Battle Opening.")
reason_text = _tr("先确认后果;确认前不改变金币或队伍。", "Acknowledge first; Coin and Party stay unchanged.") if not acknowledged else _tr("已确认:下一步进入战斗开场。", "Acknowledged: next step opens Battle Opening.")
if not bool(dependency.get("ok", false)):
reason_text = _risk_commitment_block_reason(dependency, risk_commitment_acknowledged)
reason_text = _risk_commitment_block_reason(dependency, acknowledged)
var reason := _risk_commitment_label(overlay, "RiskBeginBattleReason", Rect2(0.34, 0.925, 0.32, 0.038), reason_text, 6, Color("#f0ca75") if not ready else Color("#9ed7aa"), HORIZONTAL_ALIGNMENT_CENTER)
reason.mouse_filter = Control.MOUSE_FILTER_IGNORE
@@ -65,6 +65,7 @@ func _verify_page(company: Dictionary, locale: String, viewport_size: Vector2i)
shell.initial_company_service.company_path = COMPANY_PATH
viewport.add_child(shell)
await _frames(3)
expect(shell.app_services.has_service(&"risk_commitment_page_controller"), "%s AppServices registers Risk Commitment controller" % locale)
shell.settings = settings
shell.setup_draft = settings.duplicate(true)
shell.risk_selected_id = String(company.active_contract.formation.risk_options[1].risk_option_id)
@@ -74,6 +75,8 @@ func _verify_page(company: Dictionary, locale: String, viewport_size: Vector2i)
await _frames(3)
await _capture(shell, "%dx%d-%s-before" % [viewport_size.x, viewport_size.y, locale])
expect(shell.current_screen == "risk_commitment", "%s %s enters commitment page" % [locale, viewport_size])
expect(shell.risk_commitment_view_data.ok and shell.risk_commitment_view_data.page_id == "risk_commitment", "%s controller snapshot reaches RSK-002 View" % locale)
expect(shell.risk_commitment_view_data.consequences.count == 6 and not shell.risk_commitment_view_data.commands.begin_enabled, "%s controller exposes six facts and blocked Begin boundary" % locale)
expect(shell.find_child("RiskCommitmentStaticMaster", true, false) != null, "%s static master is present" % locale)
expect(shell.find_child("RiskFrozenMembers", true, false).get_child_count() == 4, "%s exact four frozen members" % locale)
expect(shell.find_child("RiskConsequenceFact0", true, false) != null and shell.find_child("RiskConsequenceFact5", true, false) != null, "%s six consequence facts" % locale)
@@ -91,6 +94,7 @@ func _verify_page(company: Dictionary, locale: String, viewport_size: Vector2i)
await _touch(acknowledge)
await _capture(shell, "%dx%d-%s-ready" % [viewport_size.x, viewport_size.y, locale])
expect(shell.find_child("RiskCommitmentAcknowledge", true, false).get_meta("confirmed", false), "%s acknowledgement state is explicit" % locale)
expect(shell.risk_commitment_view_data.confirmation.acknowledged and shell.risk_commitment_view_data.commands.begin_enabled, "%s controller refreshes acknowledgement and Begin eligibility" % locale)
begin = shell.find_child("RiskBeginBattle", true, false) as Button
expect(begin != null and not begin.disabled, "%s Begin Battle enables only after acknowledgement" % locale)
await _touch(begin)