diff --git a/data/scenarios/009_xiapi_siege.json b/data/scenarios/009_xiapi_siege.json index 0748253..74531b3 100644 --- a/data/scenarios/009_xiapi_siege.json +++ b/data/scenarios/009_xiapi_siege.json @@ -32,6 +32,102 @@ "The army reached Xiapi quickly after leaving Wan Castle behind, but some siege stores are still catching up." ] } + ], + "camp_conversations": [ + { + "id": "xiapi_floodgate_council", + "group": "officer", + "officer_id": "cao_cao", + "label": "Cao Cao - Flood Gates", + "speaker": "Cao Cao", + "summary": "Set the siege rhythm around the sluice-gate signal.", + "lines": [ + { + "speaker": "Cao Cao", + "side": "left", + "text": "Lu Bu wants speed to decide this siege. Water is slower than cavalry, but it remembers every low road." + }, + { + "speaker": "Xiahou Dun", + "side": "right", + "text": "Then we hold until the channels answer. Once the mud rises, his riders lose the field they trust." + } + ] + }, + { + "id": "xiapi_dian_wei_siege_road", + "group": "officer", + "officer_id": "dian_wei", + "label": "Dian Wei - Siege Road", + "speaker": "Dian Wei", + "summary": "Place Dian Wei where Lu Bu's first countercharge must break.", + "lines": [ + { + "speaker": "Dian Wei", + "side": "right", + "text": "Give me the road before the water reaches it. Horsemen hate a man who refuses to step aside." + }, + { + "speaker": "Cao Cao", + "side": "left", + "text": "Stand where the line can see you. A siege holds better when courage has a face." + } + ] + }, + { + "id": "xiapi_wan_baggage_dressings", + "campaign_flags": { "held_wan_gate": true }, + "group": "topic", + "label": "Wan Baggage Dressings", + "summary": "Open the recovered baggage for antidotes and field medicine.", + "lines": [ + { + "speaker": "Cao Ren", + "side": "left", + "text": "The baggage saved at Wan still has dry medicine. The river air is already turning wounds sour." + }, + { + "speaker": "Cao Cao", + "side": "right", + "text": "Issue it before the gates open. A steady siege line is built from small mercies." + } + ], + "effects": [ + { + "type": "grant_item", + "item_id": "panacea", + "count": 1, + "text": "The Wan baggage dressings add a Panacea to the field supplies." + } + ] + }, + { + "id": "xiapi_forced_march_rations", + "campaign_flags": { "swift_wan_escape": true }, + "group": "topic", + "label": "Forced March Rations", + "summary": "Ration portable food to soldiers who reached Xiapi before the heavy stores.", + "lines": [ + { + "speaker": "Xiahou Yuan", + "side": "right", + "text": "We reached Xiapi ahead of the slow wagons. The men can fight light, but the night work has emptied their canteens." + }, + { + "speaker": "Dian Wei", + "side": "left", + "text": "Give them beans, then point them at Lu Bu's road. Light packs still hit hard." + } + ], + "effects": [ + { + "type": "grant_item", + "item_id": "bean", + "count": 1, + "text": "The forced march rations add a Bean to the field supplies." + } + ] + } ] }, "shop": { @@ -55,7 +151,14 @@ "campaign_flags": { "swift_wan_escape": true }, "items": ["war_axe"] } - ] + ], + "merchant": { + "name": "Xiapi Flood-Gate Sutler", + "lines": [ + "Keep the sacks off the low road. Once the channels open, every dry bean becomes expensive.", + "Buy before the signal rises. After the water moves, I sell only to men who can still reach the wagons." + ] + } }, "roster": { "max_units": 5, diff --git a/tools/smoke_camp_conversation_rewards.gd b/tools/smoke_camp_conversation_rewards.gd index b8dd6a0..5a0b343 100644 --- a/tools/smoke_camp_conversation_rewards.gd +++ b/tools/smoke_camp_conversation_rewards.gd @@ -30,6 +30,10 @@ 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 XIAPI_SCENARIO_ID := "009_xiapi_siege" +const XIAPI_SCENARIO_PATH := "res://data/scenarios/009_xiapi_siege.json" +const XIAPI_HELD_CONVERSATION_ID := "xiapi_wan_baggage_dressings" +const XIAPI_SWIFT_CONVERSATION_ID := "xiapi_forced_march_rations" 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" @@ -48,6 +52,7 @@ func _init() -> void: _check_dingtao_branch_camp_conversations(failures) _check_xian_branch_camp_conversations(failures) _check_wan_branch_camp_conversations(failures) + _check_xiapi_branch_camp_conversations(failures) _check_manual_checkpoint_reverts_claim(failures) _check_completed_replay_cannot_claim(failures) _check_conditional_camp_conversations(failures) @@ -499,6 +504,88 @@ func _check_wan_branch_camp_conversations(failures: Array[String]) -> void: pursuit_scene.free() +func _check_xiapi_branch_camp_conversations(failures: Array[String]) -> void: + var base_scene = _new_prebattle_scene_for(failures, "Xiapi base camp", XIAPI_SCENARIO_ID, XIAPI_SCENARIO_PATH) + if base_scene != null: + if base_scene._camp_conversation_by_id("xiapi_floodgate_council").is_empty(): + failures.append("Xiapi should expose the floodgate council conversation") + if base_scene._camp_conversation_by_id("xiapi_dian_wei_siege_road").is_empty(): + failures.append("Xiapi should expose Dian Wei's siege road conversation") + if not base_scene._camp_conversation_by_id(XIAPI_HELD_CONVERSATION_ID).is_empty(): + failures.append("Xiapi Wan baggage dressings should be hidden without held Wan gate flag") + if not base_scene._camp_conversation_by_id(XIAPI_SWIFT_CONVERSATION_ID).is_empty(): + failures.append("Xiapi forced march rations should be hidden without swift Wan escape flag") + var merchant: Dictionary = base_scene.state.get_shop_merchant() + if str(merchant.get("name", "")) != "Xiapi Flood-Gate Sutler": + failures.append("Xiapi shop merchant name should be present") + var merchant_lines: Array = merchant.get("lines", []) + if merchant_lines.size() < 2: + failures.append("Xiapi shop merchant should expose flavor lines") + if base_scene._format_talk_status_text(base_scene._camp_conversation_entries()) != "Camp conversations | 2 topics": + failures.append("Xiapi base talk status should summarize two non-supply topics") + base_scene.free() + + var held_scene = _new_prebattle_scene_for( + failures, + "Xiapi held Wan gate camp", + XIAPI_SCENARIO_ID, + XIAPI_SCENARIO_PATH, + {"held_wan_gate": true} + ) + if held_scene != null: + var dressings: Dictionary = held_scene._camp_conversation_by_id(XIAPI_HELD_CONVERSATION_ID) + if dressings.is_empty(): + failures.append("held Wan gate flag should expose Xiapi Wan baggage dressings") + if not held_scene._camp_conversation_by_id(XIAPI_SWIFT_CONVERSATION_ID).is_empty(): + failures.append("held Wan gate flag should not expose Xiapi forced march rations") + if not dressings.is_empty(): + if held_scene._format_talk_status_text(held_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready": + failures.append("Xiapi held Wan gate talk status should show one ready supply") + if not held_scene._format_camp_conversation_button_text(dressings).contains("Supply Panacea"): + failures.append("Xiapi Wan baggage dressings button should preview Panacea supply") + var before_panacea := int(held_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) + held_scene._apply_camp_conversation_effects(dressings) + if int(held_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1: + failures.append("Xiapi Wan baggage dressings should add Panacea to campaign inventory") + if int(held_scene.state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1: + failures.append("Xiapi Wan baggage dressings should refresh battle Panacea inventory") + if not held_scene.campaign_state.has_claimed_camp_conversation_effects(XIAPI_SCENARIO_ID, XIAPI_HELD_CONVERSATION_ID): + failures.append("Xiapi Wan baggage dressings claim should be saved") + if held_scene._format_talk_status_text(held_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed": + failures.append("Xiapi held Wan gate talk status should show claimed supply after claim") + held_scene.free() + + var swift_scene = _new_prebattle_scene_for( + failures, + "Xiapi swift Wan escape camp", + XIAPI_SCENARIO_ID, + XIAPI_SCENARIO_PATH, + {"swift_wan_escape": true} + ) + if swift_scene != null: + var rations: Dictionary = swift_scene._camp_conversation_by_id(XIAPI_SWIFT_CONVERSATION_ID) + if rations.is_empty(): + failures.append("swift Wan escape flag should expose Xiapi forced march rations") + if not swift_scene._camp_conversation_by_id(XIAPI_HELD_CONVERSATION_ID).is_empty(): + failures.append("swift Wan escape flag should not expose Xiapi Wan baggage dressings") + if not rations.is_empty(): + if swift_scene._format_talk_status_text(swift_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready": + failures.append("Xiapi swift Wan escape talk status should show one ready supply") + if not swift_scene._format_camp_conversation_button_text(rations).contains("Supply Bean"): + failures.append("Xiapi forced march rations button should preview Bean supply") + var before_bean := int(swift_scene.campaign_state.get_inventory_snapshot().get("bean", 0)) + swift_scene._apply_camp_conversation_effects(rations) + if int(swift_scene.campaign_state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1: + failures.append("Xiapi forced march rations should add Bean to campaign inventory") + if int(swift_scene.state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1: + failures.append("Xiapi forced march rations should refresh battle Bean inventory") + if not swift_scene.campaign_state.has_claimed_camp_conversation_effects(XIAPI_SCENARIO_ID, XIAPI_SWIFT_CONVERSATION_ID): + failures.append("Xiapi forced march rations claim should be saved") + if swift_scene._format_talk_status_text(swift_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed": + failures.append("Xiapi swift Wan escape talk status should show claimed supply after claim") + swift_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 bdedbe5..6a546c0 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -417,6 +417,85 @@ func _check_battle_visual_data(failures: Array[String]) -> void: _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") + var xiapi_state = BattleStateScript.new() + if not xiapi_state.load_battle("res://data/scenarios/009_xiapi_siege.json"): + failures.append("could not load Xiapi Siege camp data") + else: + var xiapi_conversations: Array = xiapi_state.get_briefing().get("camp_conversations", []) + if xiapi_conversations.size() != 2: + failures.append("009 base briefing should expose exactly two camp conversations") + else: + _check_camp_conversation(failures, xiapi_conversations[0], "xiapi_floodgate_council", "officer", "cao_cao") + _check_camp_conversation(failures, xiapi_conversations[1], "xiapi_dian_wei_siege_road", "officer", "dian_wei") + var xiapi_merchant := xiapi_state.get_shop_merchant() + if str(xiapi_merchant.get("name", "")) != "Xiapi Flood-Gate Sutler": + failures.append("009 shop should expose Xiapi merchant name") + if (xiapi_merchant.get("lines", []) as Array).size() < 2: + failures.append("009 shop should expose merchant flavor lines") + _check_shop_item_visibility(failures, xiapi_state, "war_drum", false, "009 base") + _check_shop_item_visibility(failures, xiapi_state, "war_axe", false, "009 base") + + var xiapi_held_state = BattleStateScript.new() + if not xiapi_held_state.load_battle("res://data/scenarios/009_xiapi_siege.json", {}, {}, {"held_wan_gate": true}): + failures.append("could not load Xiapi held Wan gate camp data") + else: + var medicine: Dictionary = _find_camp_conversation( + xiapi_held_state.get_briefing().get("camp_conversations", []), + "xiapi_wan_baggage_dressings" + ) + if medicine.is_empty(): + failures.append("009 held Wan gate flag should expose Wan baggage dressings conversation") + else: + _check_camp_conversation_effect(failures, medicine, "panacea", 1) + if not _find_camp_conversation( + xiapi_held_state.get_briefing().get("camp_conversations", []), + "xiapi_forced_march_rations" + ).is_empty(): + failures.append("009 held Wan gate flag should not expose forced march rations conversation") + _check_shop_item_visibility(failures, xiapi_held_state, "war_drum", true, "009 held Wan gate") + _check_shop_item_visibility(failures, xiapi_held_state, "war_axe", false, "009 held Wan gate") + + var xiapi_swift_state = BattleStateScript.new() + if not xiapi_swift_state.load_battle("res://data/scenarios/009_xiapi_siege.json", {}, {}, {"swift_wan_escape": true}): + failures.append("could not load Xiapi swift Wan escape camp data") + else: + var canteens: Dictionary = _find_camp_conversation( + xiapi_swift_state.get_briefing().get("camp_conversations", []), + "xiapi_forced_march_rations" + ) + if canteens.is_empty(): + failures.append("009 swift Wan escape flag should expose forced march rations conversation") + else: + _check_camp_conversation_effect(failures, canteens, "bean", 1) + if not _find_camp_conversation( + xiapi_swift_state.get_briefing().get("camp_conversations", []), + "xiapi_wan_baggage_dressings" + ).is_empty(): + failures.append("009 swift Wan escape flag should not expose Wan baggage dressings conversation") + _check_shop_item_visibility(failures, xiapi_swift_state, "war_drum", false, "009 swift Wan escape") + _check_shop_item_visibility(failures, xiapi_swift_state, "war_axe", true, "009 swift Wan escape") + + var white_horse_state = BattleStateScript.new() + if not white_horse_state.load_battle("res://data/scenarios/010_white_horse_relief.json"): + failures.append("could not load White Horse Relief shop data") + else: + _check_shop_item_visibility(failures, white_horse_state, "imperial_seal", false, "010 base") + _check_shop_item_visibility(failures, white_horse_state, "war_drum", false, "010 base") + + var white_horse_xu_state = BattleStateScript.new() + if not white_horse_xu_state.load_battle("res://data/scenarios/010_white_horse_relief.json", {}, {}, {"integrated_xu_province": true}): + failures.append("could not load White Horse integrated Xu shop data") + else: + _check_shop_item_visibility(failures, white_horse_xu_state, "imperial_seal", true, "010 integrated Xu") + _check_shop_item_visibility(failures, white_horse_xu_state, "war_drum", false, "010 integrated Xu") + + var white_horse_pressed_state = BattleStateScript.new() + if not white_horse_pressed_state.load_battle("res://data/scenarios/010_white_horse_relief.json", {}, {}, {"pressed_northern_campaign": true}): + failures.append("could not load White Horse pressed north shop data") + else: + _check_shop_item_visibility(failures, white_horse_pressed_state, "imperial_seal", false, "010 pressed north") + _check_shop_item_visibility(failures, white_horse_pressed_state, "war_drum", true, "010 pressed north") + 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():