From 9518e2eac803e2eba3dea4f775eec06aec7613cb Mon Sep 17 00:00:00 2001 From: wusumac <736139669@qq.com> Date: Tue, 25 Aug 2026 17:14:07 +0800 Subject: [PATCH] refactor(runtime): route risk commitment through controller --- runtime/entry/entry_shell.gd | 186 +++++++++++++++--- .../rsk002/risk_commitment_runtime_test.gd | 4 + 2 files changed, 159 insertions(+), 31 deletions(-) diff --git a/runtime/entry/entry_shell.gd b/runtime/entry/entry_shell.gd index f3a601b6..63e93380 100644 --- a/runtime/entry/entry_shell.gd +++ b/runtime/entry/entry_shell.gd @@ -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 diff --git a/runtime/tests/rsk002/risk_commitment_runtime_test.gd b/runtime/tests/rsk002/risk_commitment_runtime_test.gd index 98f69a38..13ef9b09 100644 --- a/runtime/tests/rsk002/risk_commitment_runtime_test.gd +++ b/runtime/tests/rsk002/risk_commitment_runtime_test.gd @@ -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)