diff --git a/data/scenarios/005_puyang_raid.json b/data/scenarios/005_puyang_raid.json index 1b54491..676a74d 100644 --- a/data/scenarios/005_puyang_raid.json +++ b/data/scenarios/005_puyang_raid.json @@ -19,6 +19,73 @@ "Lu Bu has seized momentum in Yan Province.", "Cao Cao launches a night raid near Puyang to break the forward guard before the main cavalry can gather.", "Dian Wei joins the front line as Cao Cao's personal shield." + ], + "camp_conversations": [ + { + "id": "puyang_night_raid_council", + "group": "officer", + "officer_id": "cao_cao", + "label": "Cao Cao - Night Raid", + "speaker": "Cao Cao", + "summary": "Review the timing of the strike against Lu Bu's forward guard.", + "lines": [ + { "speaker": "Cao Cao", "side": "left", "text": "Lu Bu wins when a field becomes a race. Tonight we deny him daylight, distance, and the first clean charge." }, + { "speaker": "Xiahou Dun", "side": "right", "text": "Then we hit the guard camp before the horses are bridled. I like battles that begin with the enemy half awake." } + ] + }, + { + "id": "puyang_dian_wei_shield", + "group": "officer", + "officer_id": "dian_wei", + "label": "Dian Wei - Iron Shield", + "speaker": "Dian Wei", + "summary": "Hear Dian Wei's first pledge as Cao Cao's personal shield.", + "lines": [ + { "speaker": "Dian Wei", "side": "right", "text": "You gave me a place in the line. I will make that place difficult for any rider to cross." }, + { "speaker": "Cao Cao", "side": "left", "text": "Stand close when the alarm rises. Lu Bu's men will aim for the banner before they understand the field." } + ] + }, + { + "id": "puyang_raider_stores", + "group": "topic", + "label": "Raider Stores", + "summary": "Pack a small supply bundle for the night raid.", + "lines": [ + { "speaker": "Puyang Night Trader", "side": "right", "text": "A night raid does not leave room for wagons. I tied the beans into dark cloth so the men can carry them under armor." }, + { "speaker": "Xiahou Yuan", "side": "left", "text": "Good. When Lu Bu's riders wake, no one will have time to search a cart." } + ], + "effects": [ + { "type": "grant_item", "item_id": "bean", "count": 1, "text": "The raider stores add a Bean to the field supplies." } + ] + }, + { + "id": "puyang_hardened_vanguard", + "campaign_flags": { "pursued_dong_zhuo": true }, + "group": "topic", + "label": "Hardened Vanguard", + "summary": "Issue wine to veterans used to forced marches and sudden strikes.", + "lines": [ + { "speaker": "Xiahou Dun", "side": "right", "text": "The men who chased Dong Zhuo still move before the drum finishes. Puyang's night road will not frighten them." }, + { "speaker": "Cao Cao", "side": "left", "text": "Give those veterans wine before we step off. Men who endure hard roads should know their endurance is seen." } + ], + "effects": [ + { "type": "grant_item", "item_id": "wine", "count": 1, "text": "The hardened vanguard adds Wine to the field supplies." } + ] + }, + { + "id": "puyang_qingzhou_reserve_wagon", + "campaign_flags": { "regrouped_after_sishui": true }, + "group": "topic", + "label": "Qingzhou Reserve Wagon", + "summary": "Open a physician's bundle preserved during the steadier march.", + "lines": [ + { "speaker": "Puyang Night Trader", "side": "right", "text": "The reserve wagon from Qingzhou still has a sealed physician's bundle. Quiet hands packed it better than hurried ones." }, + { "speaker": "Dian Wei", "side": "left", "text": "Keep it near the banner. If Lu Bu's riders break through, wounded men will need more than courage." } + ], + "effects": [ + { "type": "grant_item", "item_id": "panacea", "count": 1, "text": "The Qingzhou reserve wagon adds a Panacea to the field supplies." } + ] + } ] }, "shop": { @@ -31,7 +98,14 @@ "short_bow", "hand_axe", "iron_armor" - ] + ], + "merchant": { + "name": "Puyang Night Trader", + "lines": [ + "Keep the lamps hooded. Lu Bu's scouts buy anything that shines.", + "His riders spend silver on speed. You will spend it on armor, beans, and living long enough to use both." + ] + } }, "roster": { "max_units": 4, diff --git a/tools/smoke_camp_conversation_rewards.gd b/tools/smoke_camp_conversation_rewards.gd index 7879c8c..b1dcf5a 100644 --- a/tools/smoke_camp_conversation_rewards.gd +++ b/tools/smoke_camp_conversation_rewards.gd @@ -13,6 +13,11 @@ const BRANCH_SCENARIO_ID := "003_xingyang_ambush" const BRANCH_SCENARIO_PATH := "res://data/scenarios/003_xingyang_ambush.json" const QINGZHOU_SCENARIO_ID := "004_qingzhou_campaign" const QINGZHOU_SCENARIO_PATH := "res://data/scenarios/004_qingzhou_campaign.json" +const PUYANG_SCENARIO_ID := "005_puyang_raid" +const PUYANG_SCENARIO_PATH := "res://data/scenarios/005_puyang_raid.json" +const PUYANG_CONVERSATION_ID := "puyang_raider_stores" +const PUYANG_PURSUIT_CONVERSATION_ID := "puyang_hardened_vanguard" +const PUYANG_REGROUP_CONVERSATION_ID := "puyang_qingzhou_reserve_wagon" 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" @@ -27,6 +32,7 @@ func _init() -> void: _check_claim_updates_campaign_and_battle(failures) _check_shop_and_armory_preserve_claim(failures) _check_sishui_gate_camp_supplies(failures) + _check_puyang_raid_camp_supplies(failures) _check_manual_checkpoint_reverts_claim(failures) _check_completed_replay_cannot_claim(failures) _check_conditional_camp_conversations(failures) @@ -136,6 +142,102 @@ func _check_sishui_gate_camp_supplies(failures: Array[String]) -> void: scene.free() +func _check_puyang_raid_camp_supplies(failures: Array[String]) -> void: + var scene = _new_prebattle_scene_for(failures, "Puyang raid camp supplies", PUYANG_SCENARIO_ID, PUYANG_SCENARIO_PATH) + if scene == null: + return + if scene._camp_conversation_by_id("puyang_night_raid_council").is_empty(): + failures.append("Puyang should expose the night raid council conversation") + if scene._camp_conversation_by_id("puyang_dian_wei_shield").is_empty(): + failures.append("Puyang should expose Dian Wei's officer conversation") + if not scene._camp_conversation_by_id(PUYANG_PURSUIT_CONVERSATION_ID).is_empty(): + failures.append("Puyang pursuit supplies should be hidden without pursuit flag") + if not scene._camp_conversation_by_id(PUYANG_REGROUP_CONVERSATION_ID).is_empty(): + failures.append("Puyang regroup supplies should be hidden without regroup flag") + var conversation: Dictionary = scene._camp_conversation_by_id(PUYANG_CONVERSATION_ID) + if conversation.is_empty(): + failures.append("Puyang should expose raider stores conversation") + scene.free() + return + var merchant: Dictionary = scene.state.get_shop_merchant() + if str(merchant.get("name", "")) != "Puyang Night Trader": + failures.append("Puyang shop merchant name should be present") + var merchant_lines: Array = merchant.get("lines", []) + if merchant_lines.size() < 2: + failures.append("Puyang shop merchant should expose flavor lines") + if scene._format_talk_status_text(scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready": + failures.append("Puyang talk status should show one ready supply") + if not scene._format_camp_conversation_button_text(conversation).contains("Supply Bean"): + failures.append("Puyang raider stores button should preview Bean supply") + var before_bean := int(scene.campaign_state.get_inventory_snapshot().get("bean", 0)) + scene._apply_camp_conversation_effects(conversation) + if int(scene.campaign_state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1: + failures.append("Puyang raider stores should add Bean to campaign inventory") + if int(scene.state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1: + failures.append("Puyang raider stores should refresh battle Bean inventory") + if not scene.campaign_state.has_claimed_camp_conversation_effects(PUYANG_SCENARIO_ID, PUYANG_CONVERSATION_ID): + failures.append("Puyang raider stores claim should be saved") + if scene._format_talk_status_text(scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed": + failures.append("Puyang talk status should show claimed supply after claim") + scene.free() + + var pursuit_scene = _new_prebattle_scene_for( + failures, + "Puyang pursuit camp supplies", + PUYANG_SCENARIO_ID, + PUYANG_SCENARIO_PATH, + {"pursued_dong_zhuo": true} + ) + if pursuit_scene != null: + var vanguard: Dictionary = pursuit_scene._camp_conversation_by_id(PUYANG_PURSUIT_CONVERSATION_ID) + if vanguard.is_empty(): + failures.append("Puyang pursuit flag should expose hardened vanguard supplies") + if not pursuit_scene._camp_conversation_by_id(PUYANG_REGROUP_CONVERSATION_ID).is_empty(): + failures.append("Puyang pursuit flag should not expose Qingzhou reserve wagon") + if not vanguard.is_empty(): + if pursuit_scene._format_talk_status_text(pursuit_scene._camp_conversation_entries()) != "Camp conversations | 4 topics | 2 supplies ready": + failures.append("Puyang pursuit talk status should show two ready supplies") + if not pursuit_scene._format_camp_conversation_button_text(vanguard).contains("Supply Wine"): + failures.append("Puyang hardened vanguard 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(vanguard) + if int(pursuit_scene.campaign_state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1: + failures.append("Puyang hardened vanguard should add Wine to campaign inventory") + if int(pursuit_scene.state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1: + failures.append("Puyang hardened vanguard should refresh battle Wine inventory") + if not pursuit_scene.campaign_state.has_claimed_camp_conversation_effects(PUYANG_SCENARIO_ID, PUYANG_PURSUIT_CONVERSATION_ID): + failures.append("Puyang hardened vanguard claim should be saved") + pursuit_scene.free() + + var regroup_scene = _new_prebattle_scene_for( + failures, + "Puyang regroup camp supplies", + PUYANG_SCENARIO_ID, + PUYANG_SCENARIO_PATH, + {"regrouped_after_sishui": true} + ) + if regroup_scene != null: + var reserve: Dictionary = regroup_scene._camp_conversation_by_id(PUYANG_REGROUP_CONVERSATION_ID) + if reserve.is_empty(): + failures.append("Puyang regroup flag should expose Qingzhou reserve wagon") + if not regroup_scene._camp_conversation_by_id(PUYANG_PURSUIT_CONVERSATION_ID).is_empty(): + failures.append("Puyang regroup flag should not expose hardened vanguard") + if not reserve.is_empty(): + if regroup_scene._format_talk_status_text(regroup_scene._camp_conversation_entries()) != "Camp conversations | 4 topics | 2 supplies ready": + failures.append("Puyang regroup talk status should show two ready supplies") + if not regroup_scene._format_camp_conversation_button_text(reserve).contains("Supply Panacea"): + failures.append("Puyang Qingzhou reserve button should preview Panacea supply") + var before_panacea := int(regroup_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) + regroup_scene._apply_camp_conversation_effects(reserve) + if int(regroup_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1: + failures.append("Puyang Qingzhou reserve should add Panacea to campaign inventory") + if int(regroup_scene.state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1: + failures.append("Puyang Qingzhou reserve should refresh battle Panacea inventory") + if not regroup_scene.campaign_state.has_claimed_camp_conversation_effects(PUYANG_SCENARIO_ID, PUYANG_REGROUP_CONVERSATION_ID): + failures.append("Puyang Qingzhou reserve claim should be saved") + regroup_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 41dc9bb..8bd9835 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -189,6 +189,60 @@ func _check_battle_visual_data(failures: Array[String]) -> void: ).is_empty(): failures.append("004 regroup flag should not expose hardened veterans conversation") + var puyang_state = BattleStateScript.new() + if not puyang_state.load_battle("res://data/scenarios/005_puyang_raid.json"): + failures.append("could not load Puyang Raid camp data") + else: + var puyang_conversations: Array = puyang_state.get_briefing().get("camp_conversations", []) + if puyang_conversations.size() != 3: + failures.append("005 base briefing should expose exactly three camp conversations") + else: + _check_camp_conversation(failures, puyang_conversations[0], "puyang_night_raid_council", "officer", "cao_cao") + _check_camp_conversation(failures, puyang_conversations[1], "puyang_dian_wei_shield", "officer", "dian_wei") + _check_camp_conversation(failures, puyang_conversations[2], "puyang_raider_stores", "topic", "") + _check_camp_conversation_effect(failures, puyang_conversations[2], "bean", 1) + var puyang_merchant := puyang_state.get_shop_merchant() + if str(puyang_merchant.get("name", "")) != "Puyang Night Trader": + failures.append("005 shop should expose Puyang merchant name") + if (puyang_merchant.get("lines", []) as Array).size() < 2: + failures.append("005 shop should expose merchant flavor lines") + + var puyang_pursuit_state = BattleStateScript.new() + if not puyang_pursuit_state.load_battle("res://data/scenarios/005_puyang_raid.json", {}, {}, {"pursued_dong_zhuo": true}): + failures.append("could not load Puyang pursuit camp data") + else: + var vanguard: Dictionary = _find_camp_conversation( + puyang_pursuit_state.get_briefing().get("camp_conversations", []), + "puyang_hardened_vanguard" + ) + if vanguard.is_empty(): + failures.append("005 pursuit flag should expose hardened vanguard conversation") + else: + _check_camp_conversation_effect(failures, vanguard, "wine", 1) + if not _find_camp_conversation( + puyang_pursuit_state.get_briefing().get("camp_conversations", []), + "puyang_qingzhou_reserve_wagon" + ).is_empty(): + failures.append("005 pursuit flag should not expose Qingzhou reserve wagon conversation") + + var puyang_regroup_state = BattleStateScript.new() + if not puyang_regroup_state.load_battle("res://data/scenarios/005_puyang_raid.json", {}, {}, {"regrouped_after_sishui": true}): + failures.append("could not load Puyang regroup camp data") + else: + var reserve: Dictionary = _find_camp_conversation( + puyang_regroup_state.get_briefing().get("camp_conversations", []), + "puyang_qingzhou_reserve_wagon" + ) + if reserve.is_empty(): + failures.append("005 regroup flag should expose Qingzhou reserve wagon conversation") + else: + _check_camp_conversation_effect(failures, reserve, "panacea", 1) + if not _find_camp_conversation( + puyang_regroup_state.get_briefing().get("camp_conversations", []), + "puyang_hardened_vanguard" + ).is_empty(): + failures.append("005 regroup flag should not expose hardened vanguard conversation") + 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():