Add White Horse relief camp content

This commit is contained in:
2026-06-19 00:13:27 +09:00
parent e2c4f9f360
commit 99d58d05ee
3 changed files with 249 additions and 1 deletions

View File

@@ -32,6 +32,102 @@
"The army marches quickly from Xiapi, hoping speed can strike Yuan Shao's vanguard before it settles."
]
}
],
"camp_conversations": [
{
"id": "white_horse_decoy_council",
"group": "officer",
"officer_id": "guo_jia",
"label": "Guo Jia - Decoy Line",
"speaker": "Guo Jia",
"summary": "Set the false retreat that will pull Yan Liang away from his supports.",
"lines": [
{
"speaker": "Guo Jia",
"side": "right",
"text": "Yan Liang will read a bending line as weakness. Let him see exactly enough of what he wants."
},
{
"speaker": "Cao Cao",
"side": "left",
"text": "A trap that looks too clever warns the prey. This one must look like fear."
}
]
},
{
"id": "white_horse_xiahou_dun_false_retreat",
"group": "officer",
"officer_id": "xiahou_dun",
"label": "Xiahou Dun - False Retreat",
"speaker": "Xiahou Dun",
"summary": "Prepare the center line to yield without breaking.",
"lines": [
{
"speaker": "Xiahou Dun",
"side": "left",
"text": "Men can pretend to retreat, but their backs still know the feeling. I will keep the center from turning real."
},
{
"speaker": "Guo Jia",
"side": "right",
"text": "Good. Bend the line by measure, not panic. Yan Liang must think he chose the road himself."
}
]
},
{
"id": "white_horse_xu_reserve_medicine",
"campaign_flags": { "integrated_xu_province": true },
"group": "topic",
"label": "Xu Reserve Medicine",
"summary": "Draw field medicine from the quiet rear roads secured after Xiapi.",
"lines": [
{
"speaker": "Cao Ren",
"side": "left",
"text": "Xu Province is quiet behind us. The reserve carts arrived with dry medicine and no arrows in the canvas."
},
{
"speaker": "Cao Cao",
"side": "right",
"text": "Then send it to the men who must look beaten without becoming so."
}
],
"effects": [
{
"type": "grant_item",
"item_id": "panacea",
"count": 1,
"text": "The Xu reserve medicine adds a Panacea to the field supplies."
}
]
},
{
"id": "white_horse_fast_march_beans",
"campaign_flags": { "pressed_northern_campaign": true },
"group": "topic",
"label": "Fast March Beans",
"summary": "Issue portable food to troops who reached White Horse ahead of the heavy carts.",
"lines": [
{
"speaker": "Xiahou Yuan",
"side": "right",
"text": "The army came north fast from Xiapi. The riders are fresh enough, but the footmen ate while walking."
},
{
"speaker": "Guo Jia",
"side": "left",
"text": "Give the center beans before it yields. A false retreat is easier on a full breath."
}
],
"effects": [
{
"type": "grant_item",
"item_id": "bean",
"count": 1,
"text": "The fast march beans add a Bean to the field supplies."
}
]
}
]
},
"shop": {
@@ -55,7 +151,14 @@
"campaign_flags": { "pressed_northern_campaign": true },
"items": ["war_drum"]
}
]
],
"merchant": {
"name": "White Horse Road Sutler",
"lines": [
"Buy before the banners start pretending to run. Once the decoy begins, every cart follows its assigned fear.",
"Beans for the center line, medicine for mistakes, and no heavy baggage where Yan Liang can see it."
]
}
},
"roster": {
"max_units": 6,

View File

@@ -34,6 +34,10 @@ 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 WHITE_HORSE_SCENARIO_ID := "010_white_horse_relief"
const WHITE_HORSE_SCENARIO_PATH := "res://data/scenarios/010_white_horse_relief.json"
const WHITE_HORSE_XU_CONVERSATION_ID := "white_horse_xu_reserve_medicine"
const WHITE_HORSE_PRESSED_CONVERSATION_ID := "white_horse_fast_march_beans"
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"
@@ -53,6 +57,7 @@ func _init() -> void:
_check_xian_branch_camp_conversations(failures)
_check_wan_branch_camp_conversations(failures)
_check_xiapi_branch_camp_conversations(failures)
_check_white_horse_branch_camp_conversations(failures)
_check_manual_checkpoint_reverts_claim(failures)
_check_completed_replay_cannot_claim(failures)
_check_conditional_camp_conversations(failures)
@@ -586,6 +591,88 @@ func _check_xiapi_branch_camp_conversations(failures: Array[String]) -> void:
swift_scene.free()
func _check_white_horse_branch_camp_conversations(failures: Array[String]) -> void:
var base_scene = _new_prebattle_scene_for(failures, "White Horse base camp", WHITE_HORSE_SCENARIO_ID, WHITE_HORSE_SCENARIO_PATH)
if base_scene != null:
if base_scene._camp_conversation_by_id("white_horse_decoy_council").is_empty():
failures.append("White Horse should expose the decoy council conversation")
if base_scene._camp_conversation_by_id("white_horse_xiahou_dun_false_retreat").is_empty():
failures.append("White Horse should expose Xiahou Dun's false retreat conversation")
if not base_scene._camp_conversation_by_id(WHITE_HORSE_XU_CONVERSATION_ID).is_empty():
failures.append("White Horse Xu reserve medicine should be hidden without integrated Xu flag")
if not base_scene._camp_conversation_by_id(WHITE_HORSE_PRESSED_CONVERSATION_ID).is_empty():
failures.append("White Horse fast march beans should be hidden without pressed north flag")
var merchant: Dictionary = base_scene.state.get_shop_merchant()
if str(merchant.get("name", "")) != "White Horse Road Sutler":
failures.append("White Horse shop merchant name should be present")
var merchant_lines: Array = merchant.get("lines", [])
if merchant_lines.size() < 2:
failures.append("White Horse shop merchant should expose flavor lines")
if base_scene._format_talk_status_text(base_scene._camp_conversation_entries()) != "Camp conversations | 2 topics":
failures.append("White Horse base talk status should summarize two non-supply topics")
base_scene.free()
var xu_scene = _new_prebattle_scene_for(
failures,
"White Horse integrated Xu camp",
WHITE_HORSE_SCENARIO_ID,
WHITE_HORSE_SCENARIO_PATH,
{"integrated_xu_province": true}
)
if xu_scene != null:
var medicine: Dictionary = xu_scene._camp_conversation_by_id(WHITE_HORSE_XU_CONVERSATION_ID)
if medicine.is_empty():
failures.append("integrated Xu flag should expose White Horse reserve medicine")
if not xu_scene._camp_conversation_by_id(WHITE_HORSE_PRESSED_CONVERSATION_ID).is_empty():
failures.append("integrated Xu flag should not expose White Horse fast march beans")
if not medicine.is_empty():
if xu_scene._format_talk_status_text(xu_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready":
failures.append("White Horse integrated Xu talk status should show one ready supply")
if not xu_scene._format_camp_conversation_button_text(medicine).contains("Supply Panacea"):
failures.append("White Horse Xu reserve medicine button should preview Panacea supply")
var before_panacea := int(xu_scene.campaign_state.get_inventory_snapshot().get("panacea", 0))
xu_scene._apply_camp_conversation_effects(medicine)
if int(xu_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("White Horse Xu reserve medicine should add Panacea to campaign inventory")
if int(xu_scene.state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("White Horse Xu reserve medicine should refresh battle Panacea inventory")
if not xu_scene.campaign_state.has_claimed_camp_conversation_effects(WHITE_HORSE_SCENARIO_ID, WHITE_HORSE_XU_CONVERSATION_ID):
failures.append("White Horse Xu reserve medicine claim should be saved")
if xu_scene._format_talk_status_text(xu_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed":
failures.append("White Horse integrated Xu talk status should show claimed supply after claim")
xu_scene.free()
var pressed_scene = _new_prebattle_scene_for(
failures,
"White Horse pressed north camp",
WHITE_HORSE_SCENARIO_ID,
WHITE_HORSE_SCENARIO_PATH,
{"pressed_northern_campaign": true}
)
if pressed_scene != null:
var beans: Dictionary = pressed_scene._camp_conversation_by_id(WHITE_HORSE_PRESSED_CONVERSATION_ID)
if beans.is_empty():
failures.append("pressed north flag should expose White Horse fast march beans")
if not pressed_scene._camp_conversation_by_id(WHITE_HORSE_XU_CONVERSATION_ID).is_empty():
failures.append("pressed north flag should not expose White Horse reserve medicine")
if not beans.is_empty():
if pressed_scene._format_talk_status_text(pressed_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready":
failures.append("White Horse pressed north talk status should show one ready supply")
if not pressed_scene._format_camp_conversation_button_text(beans).contains("Supply Bean"):
failures.append("White Horse fast march beans button should preview Bean supply")
var before_bean := int(pressed_scene.campaign_state.get_inventory_snapshot().get("bean", 0))
pressed_scene._apply_camp_conversation_effects(beans)
if int(pressed_scene.campaign_state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("White Horse fast march beans should add Bean to campaign inventory")
if int(pressed_scene.state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("White Horse fast march beans should refresh battle Bean inventory")
if not pressed_scene.campaign_state.has_claimed_camp_conversation_effects(WHITE_HORSE_SCENARIO_ID, WHITE_HORSE_PRESSED_CONVERSATION_ID):
failures.append("White Horse fast march beans claim should be saved")
if pressed_scene._format_talk_status_text(pressed_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed":
failures.append("White Horse pressed north 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:

View File

@@ -479,6 +479,17 @@ func _check_battle_visual_data(failures: Array[String]) -> void:
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:
var white_horse_conversations: Array = white_horse_state.get_briefing().get("camp_conversations", [])
if white_horse_conversations.size() != 2:
failures.append("010 base briefing should expose exactly two camp conversations")
else:
_check_camp_conversation(failures, white_horse_conversations[0], "white_horse_decoy_council", "officer", "guo_jia")
_check_camp_conversation(failures, white_horse_conversations[1], "white_horse_xiahou_dun_false_retreat", "officer", "xiahou_dun")
var white_horse_merchant := white_horse_state.get_shop_merchant()
if str(white_horse_merchant.get("name", "")) != "White Horse Road Sutler":
failures.append("010 shop should expose White Horse merchant name")
if (white_horse_merchant.get("lines", []) as Array).size() < 2:
failures.append("010 shop should expose merchant flavor lines")
_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")
@@ -486,6 +497,19 @@ func _check_battle_visual_data(failures: Array[String]) -> void:
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:
var medicine: Dictionary = _find_camp_conversation(
white_horse_xu_state.get_briefing().get("camp_conversations", []),
"white_horse_xu_reserve_medicine"
)
if medicine.is_empty():
failures.append("010 integrated Xu flag should expose Xu reserve medicine conversation")
else:
_check_camp_conversation_effect(failures, medicine, "panacea", 1)
if not _find_camp_conversation(
white_horse_xu_state.get_briefing().get("camp_conversations", []),
"white_horse_fast_march_beans"
).is_empty():
failures.append("010 integrated Xu flag should not expose fast march beans conversation")
_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")
@@ -493,9 +517,43 @@ func _check_battle_visual_data(failures: Array[String]) -> void:
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:
var beans: Dictionary = _find_camp_conversation(
white_horse_pressed_state.get_briefing().get("camp_conversations", []),
"white_horse_fast_march_beans"
)
if beans.is_empty():
failures.append("010 pressed north flag should expose fast march beans conversation")
else:
_check_camp_conversation_effect(failures, beans, "bean", 1)
if not _find_camp_conversation(
white_horse_pressed_state.get_briefing().get("camp_conversations", []),
"white_horse_xu_reserve_medicine"
).is_empty():
failures.append("010 pressed north flag should not expose Xu reserve medicine conversation")
_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")
var yan_ford_state = BattleStateScript.new()
if not yan_ford_state.load_battle("res://data/scenarios/011_yan_ford_pursuit.json"):
failures.append("could not load Yan Ford Pursuit shop data")
else:
_check_shop_item_visibility(failures, yan_ford_state, "imperial_seal", false, "011 base")
_check_shop_item_visibility(failures, yan_ford_state, "war_axe", false, "011 base")
var yan_ford_fortified_state = BattleStateScript.new()
if not yan_ford_fortified_state.load_battle("res://data/scenarios/011_yan_ford_pursuit.json", {}, {}, {"fortified_white_horse": true}):
failures.append("could not load Yan Ford fortified White Horse shop data")
else:
_check_shop_item_visibility(failures, yan_ford_fortified_state, "imperial_seal", true, "011 fortified White Horse")
_check_shop_item_visibility(failures, yan_ford_fortified_state, "war_axe", false, "011 fortified White Horse")
var yan_ford_raided_state = BattleStateScript.new()
if not yan_ford_raided_state.load_battle("res://data/scenarios/011_yan_ford_pursuit.json", {}, {}, {"raided_yuan_supplies": true}):
failures.append("could not load Yan Ford raided Yuan supplies shop data")
else:
_check_shop_item_visibility(failures, yan_ford_raided_state, "imperial_seal", false, "011 raided Yuan supplies")
_check_shop_item_visibility(failures, yan_ford_raided_state, "war_axe", true, "011 raided Yuan supplies")
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():