Add Xiapi siege camp content

This commit is contained in:
2026-06-19 00:06:43 +09:00
parent 409a4c62d4
commit e2c4f9f360
3 changed files with 270 additions and 1 deletions

View File

@@ -32,6 +32,102 @@
"The army reached Xiapi quickly after leaving Wan Castle behind, but some siege stores are still catching up." "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": { "shop": {
@@ -55,7 +151,14 @@
"campaign_flags": { "swift_wan_escape": true }, "campaign_flags": { "swift_wan_escape": true },
"items": ["war_axe"] "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": { "roster": {
"max_units": 5, "max_units": 5,

View File

@@ -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_SCENARIO_PATH := "res://data/scenarios/008_wan_castle_escape.json"
const WAN_COURT_CONVERSATION_ID := "wan_court_seal_postern" const WAN_COURT_CONVERSATION_ID := "wan_court_seal_postern"
const WAN_PURSUIT_CONVERSATION_ID := "wan_hard_pursuit_drums" 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_ID := "013_wuchao_raid"
const WUCHAO_SCENARIO_PATH := "res://data/scenarios/013_wuchao_raid.json" const WUCHAO_SCENARIO_PATH := "res://data/scenarios/013_wuchao_raid.json"
const SAVE_PATH := "user://campaign_save.json" const SAVE_PATH := "user://campaign_save.json"
@@ -48,6 +52,7 @@ func _init() -> void:
_check_dingtao_branch_camp_conversations(failures) _check_dingtao_branch_camp_conversations(failures)
_check_xian_branch_camp_conversations(failures) _check_xian_branch_camp_conversations(failures)
_check_wan_branch_camp_conversations(failures) _check_wan_branch_camp_conversations(failures)
_check_xiapi_branch_camp_conversations(failures)
_check_manual_checkpoint_reverts_claim(failures) _check_manual_checkpoint_reverts_claim(failures)
_check_completed_replay_cannot_claim(failures) _check_completed_replay_cannot_claim(failures)
_check_conditional_camp_conversations(failures) _check_conditional_camp_conversations(failures)
@@ -499,6 +504,88 @@ func _check_wan_branch_camp_conversations(failures: Array[String]) -> void:
pursuit_scene.free() 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: func _check_manual_checkpoint_reverts_claim(failures: Array[String]) -> void:
var scene = _new_prebattle_scene(failures, "manual checkpoint") var scene = _new_prebattle_scene(failures, "manual checkpoint")
if scene == null: if scene == null:

View File

@@ -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, "imperial_seal", false, "008 hard pursuit")
_check_shop_item_visibility(failures, wan_pursuit_state, "war_drum", true, "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"]: for item_id in ["bronze_sword", "training_spear", "short_bow", "hand_axe", "iron_armor", "panacea"]:
var item := state.get_item_def(item_id) var item := state.get_item_def(item_id)
if item.is_empty(): if item.is_empty():