diff --git a/data/scenarios/006_dingtao_counterattack.json b/data/scenarios/006_dingtao_counterattack.json index 4870e37..dd59179 100644 --- a/data/scenarios/006_dingtao_counterattack.json +++ b/data/scenarios/006_dingtao_counterattack.json @@ -32,6 +32,60 @@ "Cao Cao's pressure has kept Lu Bu off balance, but the army enters Dingtao with little rest." ] } + ], + "camp_conversations": [ + { + "id": "dingtao_countercharge_council", + "group": "officer", + "officer_id": "cao_cao", + "label": "Cao Cao - Countercharge", + "speaker": "Cao Cao", + "summary": "Set the line before Lu Bu's cavalry reaches Dingtao.", + "lines": [ + { "speaker": "Cao Cao", "side": "left", "text": "Lu Bu answers the Puyang raid with speed. That is his habit, and habits can be prepared for." }, + { "speaker": "Cao Ren", "side": "right", "text": "The ridges will break his charge if our line does not break first. I will keep the reserves close." } + ] + }, + { + "id": "dingtao_dian_wei_roadblock", + "group": "officer", + "officer_id": "dian_wei", + "label": "Dian Wei - Roadblock", + "speaker": "Dian Wei", + "summary": "Let Dian Wei anchor the road against Lu Bu's riders.", + "lines": [ + { "speaker": "Dian Wei", "side": "right", "text": "A horse needs road and courage. I cannot take the horse's road away, but I can test the rider's courage." }, + { "speaker": "Xiahou Dun", "side": "left", "text": "Stand where the road narrows. If Lu Bu wants the banner, he must spend men to reach it." } + ] + }, + { + "id": "dingtao_fortified_reserves", + "campaign_flags": { "fortified_yan_province": true }, + "group": "topic", + "label": "Fortified Reserves", + "summary": "Distribute medicine from the secured Yan Province supply road.", + "lines": [ + { "speaker": "Dingtao Field Sutler", "side": "right", "text": "The fortified roads kept the physician's chest whole. I brought the clean bundle forward before the riders closed." }, + { "speaker": "Cao Cao", "side": "left", "text": "Then the choice to fortify already pays us back. Keep it near the center line." } + ], + "effects": [ + { "type": "grant_item", "item_id": "panacea", "count": 1, "text": "The fortified reserves add a Panacea to the field supplies." } + ] + }, + { + "id": "dingtao_pressed_vanguard", + "campaign_flags": { "pressed_lu_bu": true }, + "group": "topic", + "label": "Pressed Vanguard", + "summary": "Issue wine to the soldiers who kept pressure on Lu Bu after Puyang.", + "lines": [ + { "speaker": "Xiahou Yuan", "side": "right", "text": "The vanguard has not slept enough to dream, but they still reached Dingtao before Lu Bu expected." }, + { "speaker": "Dian Wei", "side": "left", "text": "Give them wine, then give them a short order. Tired men do best when the road ahead is simple." } + ], + "effects": [ + { "type": "grant_item", "item_id": "wine", "count": 1, "text": "The pressed vanguard adds Wine to the field supplies." } + ] + } ] }, "shop": { @@ -53,9 +107,16 @@ }, { "campaign_flags": { "pressed_lu_bu": true }, - "items": ["war_axe"] + "items": ["war_drum"] } - ] + ], + "merchant": { + "name": "Dingtao Field Sutler", + "lines": [ + "Buy before the dust rises. Once Lu Bu's riders arrive, even a good price will not stand still.", + "Fortified roads bring armor; forced marches bring drums. Either way, keep one hand free for medicine." + ] + } }, "roster": { "max_units": 4, diff --git a/tools/smoke_camp_conversation_rewards.gd b/tools/smoke_camp_conversation_rewards.gd index b1dcf5a..8be32bb 100644 --- a/tools/smoke_camp_conversation_rewards.gd +++ b/tools/smoke_camp_conversation_rewards.gd @@ -18,6 +18,10 @@ 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 DINGTAO_SCENARIO_ID := "006_dingtao_counterattack" +const DINGTAO_SCENARIO_PATH := "res://data/scenarios/006_dingtao_counterattack.json" +const DINGTAO_FORTIFIED_CONVERSATION_ID := "dingtao_fortified_reserves" +const DINGTAO_PRESSED_CONVERSATION_ID := "dingtao_pressed_vanguard" 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" @@ -33,6 +37,7 @@ func _init() -> void: _check_shop_and_armory_preserve_claim(failures) _check_sishui_gate_camp_supplies(failures) _check_puyang_raid_camp_supplies(failures) + _check_dingtao_branch_camp_conversations(failures) _check_manual_checkpoint_reverts_claim(failures) _check_completed_replay_cannot_claim(failures) _check_conditional_camp_conversations(failures) @@ -238,6 +243,88 @@ func _check_puyang_raid_camp_supplies(failures: Array[String]) -> void: regroup_scene.free() +func _check_dingtao_branch_camp_conversations(failures: Array[String]) -> void: + var base_scene = _new_prebattle_scene_for(failures, "Dingtao base camp", DINGTAO_SCENARIO_ID, DINGTAO_SCENARIO_PATH) + if base_scene != null: + if base_scene._camp_conversation_by_id("dingtao_countercharge_council").is_empty(): + failures.append("Dingtao should expose the countercharge council conversation") + if base_scene._camp_conversation_by_id("dingtao_dian_wei_roadblock").is_empty(): + failures.append("Dingtao should expose Dian Wei's roadblock conversation") + if not base_scene._camp_conversation_by_id(DINGTAO_FORTIFIED_CONVERSATION_ID).is_empty(): + failures.append("Dingtao fortified supplies should be hidden without fortified flag") + if not base_scene._camp_conversation_by_id(DINGTAO_PRESSED_CONVERSATION_ID).is_empty(): + failures.append("Dingtao pressed supplies should be hidden without pressed flag") + var merchant: Dictionary = base_scene.state.get_shop_merchant() + if str(merchant.get("name", "")) != "Dingtao Field Sutler": + failures.append("Dingtao shop merchant name should be present") + var merchant_lines: Array = merchant.get("lines", []) + if merchant_lines.size() < 2: + failures.append("Dingtao shop merchant should expose flavor lines") + if base_scene._format_talk_status_text(base_scene._camp_conversation_entries()) != "Camp conversations | 2 topics": + failures.append("Dingtao base talk status should summarize two non-supply topics") + base_scene.free() + + var fortified_scene = _new_prebattle_scene_for( + failures, + "Dingtao fortified camp", + DINGTAO_SCENARIO_ID, + DINGTAO_SCENARIO_PATH, + {"fortified_yan_province": true} + ) + if fortified_scene != null: + var reserves: Dictionary = fortified_scene._camp_conversation_by_id(DINGTAO_FORTIFIED_CONVERSATION_ID) + if reserves.is_empty(): + failures.append("fortified flag should expose Dingtao fortified reserves") + if not fortified_scene._camp_conversation_by_id(DINGTAO_PRESSED_CONVERSATION_ID).is_empty(): + failures.append("fortified flag should not expose Dingtao pressed vanguard") + if not reserves.is_empty(): + if fortified_scene._format_talk_status_text(fortified_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready": + failures.append("Dingtao fortified talk status should show one ready supply") + if not fortified_scene._format_camp_conversation_button_text(reserves).contains("Supply Panacea"): + failures.append("Dingtao fortified reserves button should preview Panacea supply") + var before_panacea := int(fortified_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) + fortified_scene._apply_camp_conversation_effects(reserves) + if int(fortified_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1: + failures.append("Dingtao fortified reserves should add Panacea to campaign inventory") + if int(fortified_scene.state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1: + failures.append("Dingtao fortified reserves should refresh battle Panacea inventory") + if not fortified_scene.campaign_state.has_claimed_camp_conversation_effects(DINGTAO_SCENARIO_ID, DINGTAO_FORTIFIED_CONVERSATION_ID): + failures.append("Dingtao fortified reserves claim should be saved") + if fortified_scene._format_talk_status_text(fortified_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed": + failures.append("Dingtao fortified talk status should show claimed supply after claim") + fortified_scene.free() + + var pressed_scene = _new_prebattle_scene_for( + failures, + "Dingtao pressed camp", + DINGTAO_SCENARIO_ID, + DINGTAO_SCENARIO_PATH, + {"pressed_lu_bu": true} + ) + if pressed_scene != null: + var vanguard: Dictionary = pressed_scene._camp_conversation_by_id(DINGTAO_PRESSED_CONVERSATION_ID) + if vanguard.is_empty(): + failures.append("pressed flag should expose Dingtao pressed vanguard") + if not pressed_scene._camp_conversation_by_id(DINGTAO_FORTIFIED_CONVERSATION_ID).is_empty(): + failures.append("pressed flag should not expose Dingtao fortified reserves") + if not vanguard.is_empty(): + if pressed_scene._format_talk_status_text(pressed_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready": + failures.append("Dingtao pressed talk status should show one ready supply") + if not pressed_scene._format_camp_conversation_button_text(vanguard).contains("Supply Wine"): + failures.append("Dingtao pressed vanguard button should preview Wine supply") + var before_wine := int(pressed_scene.campaign_state.get_inventory_snapshot().get("wine", 0)) + pressed_scene._apply_camp_conversation_effects(vanguard) + if int(pressed_scene.campaign_state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1: + failures.append("Dingtao pressed vanguard should add Wine to campaign inventory") + if int(pressed_scene.state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1: + failures.append("Dingtao pressed vanguard should refresh battle Wine inventory") + if not pressed_scene.campaign_state.has_claimed_camp_conversation_effects(DINGTAO_SCENARIO_ID, DINGTAO_PRESSED_CONVERSATION_ID): + failures.append("Dingtao pressed vanguard claim should be saved") + if pressed_scene._format_talk_status_text(pressed_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed": + failures.append("Dingtao pressed talk status should show claimed supply after claim") + pressed_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 8bd9835..68d04bc 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -243,6 +243,64 @@ func _check_battle_visual_data(failures: Array[String]) -> void: ).is_empty(): failures.append("005 regroup flag should not expose hardened vanguard conversation") + var dingtao_state = BattleStateScript.new() + if not dingtao_state.load_battle("res://data/scenarios/006_dingtao_counterattack.json"): + failures.append("could not load Dingtao Counterattack camp data") + else: + var dingtao_conversations: Array = dingtao_state.get_briefing().get("camp_conversations", []) + if dingtao_conversations.size() != 2: + failures.append("006 base briefing should expose exactly two camp conversations") + else: + _check_camp_conversation(failures, dingtao_conversations[0], "dingtao_countercharge_council", "officer", "cao_cao") + _check_camp_conversation(failures, dingtao_conversations[1], "dingtao_dian_wei_roadblock", "officer", "dian_wei") + var dingtao_merchant := dingtao_state.get_shop_merchant() + if str(dingtao_merchant.get("name", "")) != "Dingtao Field Sutler": + failures.append("006 shop should expose Dingtao merchant name") + if (dingtao_merchant.get("lines", []) as Array).size() < 2: + failures.append("006 shop should expose merchant flavor lines") + _check_shop_item_visibility(failures, dingtao_state, "iron_armor", false, "006 base") + _check_shop_item_visibility(failures, dingtao_state, "war_drum", false, "006 base") + + var dingtao_fortified_state = BattleStateScript.new() + if not dingtao_fortified_state.load_battle("res://data/scenarios/006_dingtao_counterattack.json", {}, {}, {"fortified_yan_province": true}): + failures.append("could not load Dingtao fortified camp data") + else: + var fortified: Dictionary = _find_camp_conversation( + dingtao_fortified_state.get_briefing().get("camp_conversations", []), + "dingtao_fortified_reserves" + ) + if fortified.is_empty(): + failures.append("006 fortified flag should expose fortified reserves conversation") + else: + _check_camp_conversation_effect(failures, fortified, "panacea", 1) + if not _find_camp_conversation( + dingtao_fortified_state.get_briefing().get("camp_conversations", []), + "dingtao_pressed_vanguard" + ).is_empty(): + failures.append("006 fortified flag should not expose pressed vanguard conversation") + _check_shop_item_visibility(failures, dingtao_fortified_state, "iron_armor", true, "006 fortified") + _check_shop_item_visibility(failures, dingtao_fortified_state, "war_drum", false, "006 fortified") + + var dingtao_pressed_state = BattleStateScript.new() + if not dingtao_pressed_state.load_battle("res://data/scenarios/006_dingtao_counterattack.json", {}, {}, {"pressed_lu_bu": true}): + failures.append("could not load Dingtao pressed camp data") + else: + var pressed: Dictionary = _find_camp_conversation( + dingtao_pressed_state.get_briefing().get("camp_conversations", []), + "dingtao_pressed_vanguard" + ) + if pressed.is_empty(): + failures.append("006 pressed flag should expose pressed vanguard conversation") + else: + _check_camp_conversation_effect(failures, pressed, "wine", 1) + if not _find_camp_conversation( + dingtao_pressed_state.get_briefing().get("camp_conversations", []), + "dingtao_fortified_reserves" + ).is_empty(): + failures.append("006 pressed flag should not expose fortified reserves conversation") + _check_shop_item_visibility(failures, dingtao_pressed_state, "iron_armor", false, "006 pressed") + _check_shop_item_visibility(failures, dingtao_pressed_state, "war_drum", true, "006 pressed") + 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(): @@ -835,6 +893,12 @@ func _find_camp_conversation(conversations: Array, conversation_id: String) -> D return {} +func _check_shop_item_visibility(failures: Array[String], state, item_id: String, expected_visible: bool, context: String) -> void: + var visible: bool = state.get_shop_item_ids().has(item_id) + if visible != expected_visible: + failures.append("%s shop visibility for %s should be %s" % [context, item_id, str(expected_visible)]) + + func _check_camp_conversation_effect(failures: Array[String], conversation: Dictionary, expected_item_id: String, expected_count: int) -> void: var effects: Array = conversation.get("effects", []) if effects.is_empty():