diff --git a/data/scenarios/008_wan_castle_escape.json b/data/scenarios/008_wan_castle_escape.json index a5d1eb3..f8d8e16 100644 --- a/data/scenarios/008_wan_castle_escape.json +++ b/data/scenarios/008_wan_castle_escape.json @@ -38,6 +38,102 @@ "The army's hard pursuit after the imperial escort keeps the vanguard sharp, but Zhang Xiu's scouts know Cao Cao's marching habits." ] } + ], + "camp_conversations": [ + { + "id": "wan_escape_route_council", + "group": "officer", + "officer_id": "cao_cao", + "label": "Cao Cao - Escape Route", + "speaker": "Cao Cao", + "summary": "Mark the western road before Wan Castle closes around the column.", + "lines": [ + { + "speaker": "Cao Cao", + "side": "left", + "text": "A banquet that falls silent has already drawn its blade. If Zhang Xiu closes the east, we leave by the west." + }, + { + "speaker": "Cao Ang", + "side": "right", + "text": "Then I will keep the road beside you clear. Wan Castle will not take our banner in a hallway." + } + ] + }, + { + "id": "wan_dian_wei_rear_guard", + "group": "officer", + "officer_id": "dian_wei", + "label": "Dian Wei - Rear Guard", + "speaker": "Dian Wei", + "summary": "Assign Dian Wei to hold the rear while Cao Cao and Cao Ang cut west.", + "lines": [ + { + "speaker": "Dian Wei", + "side": "right", + "text": "Gates are honest things. They either open or they break. I can work with both." + }, + { + "speaker": "Cao Cao", + "side": "left", + "text": "Hold the rear only long enough to keep Cao Ang moving. A brave death is still a cost I will not pay lightly." + } + ] + }, + { + "id": "wan_court_seal_postern", + "campaign_flags": { "secured_imperial_court": true }, + "group": "topic", + "label": "Court Seal Postern", + "summary": "Use imperial authority to open a narrow support route.", + "lines": [ + { + "speaker": "Escort Road Sutler", + "side": "right", + "text": "The court seal still opens doors, even in a castle that wants them shut. I can move one food bundle through the west postern." + }, + { + "speaker": "Cao Ren", + "side": "left", + "text": "One bundle is enough if it reaches the men cutting the road." + } + ], + "effects": [ + { + "type": "grant_item", + "item_id": "bean", + "count": 1, + "text": "The court seal postern adds a Bean to the field supplies." + } + ] + }, + { + "id": "wan_hard_pursuit_drums", + "campaign_flags": { "pursued_li_jue_remnants": true }, + "group": "topic", + "label": "Hard Pursuit Drums", + "summary": "Issue wine to the vanguard whose pace is known by enemy scouts.", + "lines": [ + { + "speaker": "Xiahou Yuan", + "side": "right", + "text": "Zhang Xiu's scouts know our old marching path because we used it hard after Li Jue. They will expect speed." + }, + { + "speaker": "Dian Wei", + "side": "left", + "text": "Then give the vanguard wine and change the rhythm. Let them chase the sound they expected." + } + ], + "effects": [ + { + "type": "grant_item", + "item_id": "wine", + "count": 1, + "text": "The hard pursuit drums add Wine to the field supplies." + } + ] + } ] }, "shop": { @@ -61,7 +157,14 @@ "campaign_flags": { "pursued_li_jue_remnants": true }, "items": ["war_drum"] } - ] + ], + "merchant": { + "name": "Wan Castle Sutler", + "lines": [ + "Buy quietly. The walls have learned to listen since the banquet began.", + "If the west road opens, carry only what keeps men moving: beans, wine, and blades already in hand." + ] + } }, "roster": { "max_units": 5, diff --git a/tools/smoke_camp_conversation_rewards.gd b/tools/smoke_camp_conversation_rewards.gd index ea63802..b8dd6a0 100644 --- a/tools/smoke_camp_conversation_rewards.gd +++ b/tools/smoke_camp_conversation_rewards.gd @@ -26,6 +26,10 @@ const XIAN_SCENARIO_ID := "007_xian_emperor_escort" const XIAN_SCENARIO_PATH := "res://data/scenarios/007_xian_emperor_escort.json" const XIAN_FORTIFIED_CONVERSATION_ID := "xian_fortified_road_supplies" const XIAN_PRESSED_CONVERSATION_ID := "xian_forced_march_canteens" +const WAN_SCENARIO_ID := "008_wan_castle_escape" +const WAN_SCENARIO_PATH := "res://data/scenarios/008_wan_castle_escape.json" +const WAN_COURT_CONVERSATION_ID := "wan_court_seal_postern" +const WAN_PURSUIT_CONVERSATION_ID := "wan_hard_pursuit_drums" const WUCHAO_SCENARIO_ID := "013_wuchao_raid" const WUCHAO_SCENARIO_PATH := "res://data/scenarios/013_wuchao_raid.json" const SAVE_PATH := "user://campaign_save.json" @@ -43,6 +47,7 @@ func _init() -> void: _check_puyang_raid_camp_supplies(failures) _check_dingtao_branch_camp_conversations(failures) _check_xian_branch_camp_conversations(failures) + _check_wan_branch_camp_conversations(failures) _check_manual_checkpoint_reverts_claim(failures) _check_completed_replay_cannot_claim(failures) _check_conditional_camp_conversations(failures) @@ -412,6 +417,88 @@ func _check_xian_branch_camp_conversations(failures: Array[String]) -> void: pressed_scene.free() +func _check_wan_branch_camp_conversations(failures: Array[String]) -> void: + var base_scene = _new_prebattle_scene_for(failures, "Wan Castle base camp", WAN_SCENARIO_ID, WAN_SCENARIO_PATH) + if base_scene != null: + if base_scene._camp_conversation_by_id("wan_escape_route_council").is_empty(): + failures.append("Wan Castle should expose the escape route council conversation") + if base_scene._camp_conversation_by_id("wan_dian_wei_rear_guard").is_empty(): + failures.append("Wan Castle should expose Dian Wei's rear guard conversation") + if not base_scene._camp_conversation_by_id(WAN_COURT_CONVERSATION_ID).is_empty(): + failures.append("Wan court seal supplies should be hidden without court flag") + if not base_scene._camp_conversation_by_id(WAN_PURSUIT_CONVERSATION_ID).is_empty(): + failures.append("Wan hard pursuit supplies should be hidden without pursuit flag") + var merchant: Dictionary = base_scene.state.get_shop_merchant() + if str(merchant.get("name", "")) != "Wan Castle Sutler": + failures.append("Wan Castle shop merchant name should be present") + var merchant_lines: Array = merchant.get("lines", []) + if merchant_lines.size() < 2: + failures.append("Wan Castle shop merchant should expose flavor lines") + if base_scene._format_talk_status_text(base_scene._camp_conversation_entries()) != "Camp conversations | 2 topics": + failures.append("Wan Castle base talk status should summarize two non-supply topics") + base_scene.free() + + var court_scene = _new_prebattle_scene_for( + failures, + "Wan Castle court seal camp", + WAN_SCENARIO_ID, + WAN_SCENARIO_PATH, + {"secured_imperial_court": true} + ) + if court_scene != null: + var postern: Dictionary = court_scene._camp_conversation_by_id(WAN_COURT_CONVERSATION_ID) + if postern.is_empty(): + failures.append("court seal flag should expose Wan postern supplies") + if not court_scene._camp_conversation_by_id(WAN_PURSUIT_CONVERSATION_ID).is_empty(): + failures.append("court seal flag should not expose Wan hard pursuit drums") + if not postern.is_empty(): + if court_scene._format_talk_status_text(court_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready": + failures.append("Wan court seal talk status should show one ready supply") + if not court_scene._format_camp_conversation_button_text(postern).contains("Supply Bean"): + failures.append("Wan court seal postern button should preview Bean supply") + var before_bean := int(court_scene.campaign_state.get_inventory_snapshot().get("bean", 0)) + court_scene._apply_camp_conversation_effects(postern) + if int(court_scene.campaign_state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1: + failures.append("Wan court seal postern should add Bean to campaign inventory") + if int(court_scene.state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1: + failures.append("Wan court seal postern should refresh battle Bean inventory") + if not court_scene.campaign_state.has_claimed_camp_conversation_effects(WAN_SCENARIO_ID, WAN_COURT_CONVERSATION_ID): + failures.append("Wan court seal postern claim should be saved") + if court_scene._format_talk_status_text(court_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed": + failures.append("Wan court seal talk status should show claimed supply after claim") + court_scene.free() + + var pursuit_scene = _new_prebattle_scene_for( + failures, + "Wan Castle hard pursuit camp", + WAN_SCENARIO_ID, + WAN_SCENARIO_PATH, + {"pursued_li_jue_remnants": true} + ) + if pursuit_scene != null: + var drums: Dictionary = pursuit_scene._camp_conversation_by_id(WAN_PURSUIT_CONVERSATION_ID) + if drums.is_empty(): + failures.append("hard pursuit flag should expose Wan hard pursuit drums") + if not pursuit_scene._camp_conversation_by_id(WAN_COURT_CONVERSATION_ID).is_empty(): + failures.append("hard pursuit flag should not expose Wan postern supplies") + if not drums.is_empty(): + if pursuit_scene._format_talk_status_text(pursuit_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready": + failures.append("Wan hard pursuit talk status should show one ready supply") + if not pursuit_scene._format_camp_conversation_button_text(drums).contains("Supply Wine"): + failures.append("Wan hard pursuit drums button should preview Wine supply") + var before_wine := int(pursuit_scene.campaign_state.get_inventory_snapshot().get("wine", 0)) + pursuit_scene._apply_camp_conversation_effects(drums) + if int(pursuit_scene.campaign_state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1: + failures.append("Wan hard pursuit drums should add Wine to campaign inventory") + if int(pursuit_scene.state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1: + failures.append("Wan hard pursuit drums should refresh battle Wine inventory") + if not pursuit_scene.campaign_state.has_claimed_camp_conversation_effects(WAN_SCENARIO_ID, WAN_PURSUIT_CONVERSATION_ID): + failures.append("Wan hard pursuit drums claim should be saved") + if pursuit_scene._format_talk_status_text(pursuit_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed": + failures.append("Wan hard pursuit talk status should show claimed supply after claim") + pursuit_scene.free() + + func _check_manual_checkpoint_reverts_claim(failures: Array[String]) -> void: var scene = _new_prebattle_scene(failures, "manual checkpoint") if scene == null: diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index e054c23..bdedbe5 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -359,6 +359,64 @@ func _check_battle_visual_data(failures: Array[String]) -> void: _check_shop_item_visibility(failures, xian_pressed_state, "iron_armor", false, "007 pressed") _check_shop_item_visibility(failures, xian_pressed_state, "war_axe", true, "007 pressed") + var wan_state = BattleStateScript.new() + if not wan_state.load_battle("res://data/scenarios/008_wan_castle_escape.json"): + failures.append("could not load Wan Castle Escape camp data") + else: + var wan_conversations: Array = wan_state.get_briefing().get("camp_conversations", []) + if wan_conversations.size() != 2: + failures.append("008 base briefing should expose exactly two camp conversations") + else: + _check_camp_conversation(failures, wan_conversations[0], "wan_escape_route_council", "officer", "cao_cao") + _check_camp_conversation(failures, wan_conversations[1], "wan_dian_wei_rear_guard", "officer", "dian_wei") + var wan_merchant := wan_state.get_shop_merchant() + if str(wan_merchant.get("name", "")) != "Wan Castle Sutler": + failures.append("008 shop should expose Wan Castle merchant name") + if (wan_merchant.get("lines", []) as Array).size() < 2: + failures.append("008 shop should expose merchant flavor lines") + _check_shop_item_visibility(failures, wan_state, "imperial_seal", false, "008 base") + _check_shop_item_visibility(failures, wan_state, "war_drum", false, "008 base") + + var wan_court_state = BattleStateScript.new() + if not wan_court_state.load_battle("res://data/scenarios/008_wan_castle_escape.json", {}, {}, {"secured_imperial_court": true}): + failures.append("could not load Wan court seal camp data") + else: + var postern: Dictionary = _find_camp_conversation( + wan_court_state.get_briefing().get("camp_conversations", []), + "wan_court_seal_postern" + ) + if postern.is_empty(): + failures.append("008 secured court flag should expose court seal postern conversation") + else: + _check_camp_conversation_effect(failures, postern, "bean", 1) + if not _find_camp_conversation( + wan_court_state.get_briefing().get("camp_conversations", []), + "wan_hard_pursuit_drums" + ).is_empty(): + failures.append("008 secured court flag should not expose hard pursuit drums conversation") + _check_shop_item_visibility(failures, wan_court_state, "imperial_seal", true, "008 secured court") + _check_shop_item_visibility(failures, wan_court_state, "war_drum", false, "008 secured court") + + var wan_pursuit_state = BattleStateScript.new() + if not wan_pursuit_state.load_battle("res://data/scenarios/008_wan_castle_escape.json", {}, {}, {"pursued_li_jue_remnants": true}): + failures.append("could not load Wan hard pursuit camp data") + else: + var drums: Dictionary = _find_camp_conversation( + wan_pursuit_state.get_briefing().get("camp_conversations", []), + "wan_hard_pursuit_drums" + ) + if drums.is_empty(): + failures.append("008 hard pursuit flag should expose hard pursuit drums conversation") + else: + _check_camp_conversation_effect(failures, drums, "wine", 1) + if not _find_camp_conversation( + wan_pursuit_state.get_briefing().get("camp_conversations", []), + "wan_court_seal_postern" + ).is_empty(): + failures.append("008 hard pursuit flag should not expose court seal postern conversation") + _check_shop_item_visibility(failures, wan_pursuit_state, "imperial_seal", false, "008 hard pursuit") + _check_shop_item_visibility(failures, wan_pursuit_state, "war_drum", true, "008 hard pursuit") + for item_id in ["bronze_sword", "training_spear", "short_bow", "hand_axe", "iron_armor", "panacea"]: var item := state.get_item_def(item_id) if item.is_empty():