Add Wan Castle escape camp content

This commit is contained in:
2026-06-18 23:59:14 +09:00
parent 5e9ab7afec
commit 409a4c62d4
3 changed files with 249 additions and 1 deletions

View File

@@ -38,6 +38,102 @@
"The army's hard pursuit after the imperial escort keeps the vanguard sharp, but Zhang Xiu's scouts know Cao Cao's marching habits." "The army's hard pursuit after the imperial escort keeps the vanguard sharp, but Zhang Xiu's scouts know Cao Cao's marching habits."
] ]
} }
],
"camp_conversations": [
{
"id": "wan_escape_route_council",
"group": "officer",
"officer_id": "cao_cao",
"label": "Cao Cao - Escape Route",
"speaker": "Cao Cao",
"summary": "Mark the western road before Wan Castle closes around the column.",
"lines": [
{
"speaker": "Cao Cao",
"side": "left",
"text": "A banquet that falls silent has already drawn its blade. If Zhang Xiu closes the east, we leave by the west."
},
{
"speaker": "Cao Ang",
"side": "right",
"text": "Then I will keep the road beside you clear. Wan Castle will not take our banner in a hallway."
}
]
},
{
"id": "wan_dian_wei_rear_guard",
"group": "officer",
"officer_id": "dian_wei",
"label": "Dian Wei - Rear Guard",
"speaker": "Dian Wei",
"summary": "Assign Dian Wei to hold the rear while Cao Cao and Cao Ang cut west.",
"lines": [
{
"speaker": "Dian Wei",
"side": "right",
"text": "Gates are honest things. They either open or they break. I can work with both."
},
{
"speaker": "Cao Cao",
"side": "left",
"text": "Hold the rear only long enough to keep Cao Ang moving. A brave death is still a cost I will not pay lightly."
}
]
},
{
"id": "wan_court_seal_postern",
"campaign_flags": { "secured_imperial_court": true },
"group": "topic",
"label": "Court Seal Postern",
"summary": "Use imperial authority to open a narrow support route.",
"lines": [
{
"speaker": "Escort Road Sutler",
"side": "right",
"text": "The court seal still opens doors, even in a castle that wants them shut. I can move one food bundle through the west postern."
},
{
"speaker": "Cao Ren",
"side": "left",
"text": "One bundle is enough if it reaches the men cutting the road."
}
],
"effects": [
{
"type": "grant_item",
"item_id": "bean",
"count": 1,
"text": "The court seal postern adds a Bean to the field supplies."
}
]
},
{
"id": "wan_hard_pursuit_drums",
"campaign_flags": { "pursued_li_jue_remnants": true },
"group": "topic",
"label": "Hard Pursuit Drums",
"summary": "Issue wine to the vanguard whose pace is known by enemy scouts.",
"lines": [
{
"speaker": "Xiahou Yuan",
"side": "right",
"text": "Zhang Xiu's scouts know our old marching path because we used it hard after Li Jue. They will expect speed."
},
{
"speaker": "Dian Wei",
"side": "left",
"text": "Then give the vanguard wine and change the rhythm. Let them chase the sound they expected."
}
],
"effects": [
{
"type": "grant_item",
"item_id": "wine",
"count": 1,
"text": "The hard pursuit drums add Wine to the field supplies."
}
]
}
] ]
}, },
"shop": { "shop": {
@@ -61,7 +157,14 @@
"campaign_flags": { "pursued_li_jue_remnants": true }, "campaign_flags": { "pursued_li_jue_remnants": true },
"items": ["war_drum"] "items": ["war_drum"]
} }
],
"merchant": {
"name": "Wan Castle Sutler",
"lines": [
"Buy quietly. The walls have learned to listen since the banquet began.",
"If the west road opens, carry only what keeps men moving: beans, wine, and blades already in hand."
] ]
}
}, },
"roster": { "roster": {
"max_units": 5, "max_units": 5,

View File

@@ -26,6 +26,10 @@ const XIAN_SCENARIO_ID := "007_xian_emperor_escort"
const XIAN_SCENARIO_PATH := "res://data/scenarios/007_xian_emperor_escort.json" const XIAN_SCENARIO_PATH := "res://data/scenarios/007_xian_emperor_escort.json"
const XIAN_FORTIFIED_CONVERSATION_ID := "xian_fortified_road_supplies" const XIAN_FORTIFIED_CONVERSATION_ID := "xian_fortified_road_supplies"
const XIAN_PRESSED_CONVERSATION_ID := "xian_forced_march_canteens" const XIAN_PRESSED_CONVERSATION_ID := "xian_forced_march_canteens"
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 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"
@@ -43,6 +47,7 @@ func _init() -> void:
_check_puyang_raid_camp_supplies(failures) _check_puyang_raid_camp_supplies(failures)
_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_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)
@@ -412,6 +417,88 @@ func _check_xian_branch_camp_conversations(failures: Array[String]) -> void:
pressed_scene.free() pressed_scene.free()
func _check_wan_branch_camp_conversations(failures: Array[String]) -> void:
var base_scene = _new_prebattle_scene_for(failures, "Wan Castle base camp", WAN_SCENARIO_ID, WAN_SCENARIO_PATH)
if base_scene != null:
if base_scene._camp_conversation_by_id("wan_escape_route_council").is_empty():
failures.append("Wan Castle should expose the escape route council conversation")
if base_scene._camp_conversation_by_id("wan_dian_wei_rear_guard").is_empty():
failures.append("Wan Castle should expose Dian Wei's rear guard conversation")
if not base_scene._camp_conversation_by_id(WAN_COURT_CONVERSATION_ID).is_empty():
failures.append("Wan court seal supplies should be hidden without court flag")
if not base_scene._camp_conversation_by_id(WAN_PURSUIT_CONVERSATION_ID).is_empty():
failures.append("Wan hard pursuit supplies should be hidden without pursuit flag")
var merchant: Dictionary = base_scene.state.get_shop_merchant()
if str(merchant.get("name", "")) != "Wan Castle Sutler":
failures.append("Wan Castle shop merchant name should be present")
var merchant_lines: Array = merchant.get("lines", [])
if merchant_lines.size() < 2:
failures.append("Wan Castle shop merchant should expose flavor lines")
if base_scene._format_talk_status_text(base_scene._camp_conversation_entries()) != "Camp conversations | 2 topics":
failures.append("Wan Castle base talk status should summarize two non-supply topics")
base_scene.free()
var court_scene = _new_prebattle_scene_for(
failures,
"Wan Castle court seal camp",
WAN_SCENARIO_ID,
WAN_SCENARIO_PATH,
{"secured_imperial_court": true}
)
if court_scene != null:
var postern: Dictionary = court_scene._camp_conversation_by_id(WAN_COURT_CONVERSATION_ID)
if postern.is_empty():
failures.append("court seal flag should expose Wan postern supplies")
if not court_scene._camp_conversation_by_id(WAN_PURSUIT_CONVERSATION_ID).is_empty():
failures.append("court seal flag should not expose Wan hard pursuit drums")
if not postern.is_empty():
if court_scene._format_talk_status_text(court_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready":
failures.append("Wan court seal talk status should show one ready supply")
if not court_scene._format_camp_conversation_button_text(postern).contains("Supply Bean"):
failures.append("Wan court seal postern button should preview Bean supply")
var before_bean := int(court_scene.campaign_state.get_inventory_snapshot().get("bean", 0))
court_scene._apply_camp_conversation_effects(postern)
if int(court_scene.campaign_state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("Wan court seal postern should add Bean to campaign inventory")
if int(court_scene.state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("Wan court seal postern should refresh battle Bean inventory")
if not court_scene.campaign_state.has_claimed_camp_conversation_effects(WAN_SCENARIO_ID, WAN_COURT_CONVERSATION_ID):
failures.append("Wan court seal postern claim should be saved")
if court_scene._format_talk_status_text(court_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed":
failures.append("Wan court seal talk status should show claimed supply after claim")
court_scene.free()
var pursuit_scene = _new_prebattle_scene_for(
failures,
"Wan Castle hard pursuit camp",
WAN_SCENARIO_ID,
WAN_SCENARIO_PATH,
{"pursued_li_jue_remnants": true}
)
if pursuit_scene != null:
var drums: Dictionary = pursuit_scene._camp_conversation_by_id(WAN_PURSUIT_CONVERSATION_ID)
if drums.is_empty():
failures.append("hard pursuit flag should expose Wan hard pursuit drums")
if not pursuit_scene._camp_conversation_by_id(WAN_COURT_CONVERSATION_ID).is_empty():
failures.append("hard pursuit flag should not expose Wan postern supplies")
if not drums.is_empty():
if pursuit_scene._format_talk_status_text(pursuit_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready":
failures.append("Wan hard pursuit talk status should show one ready supply")
if not pursuit_scene._format_camp_conversation_button_text(drums).contains("Supply Wine"):
failures.append("Wan hard pursuit drums 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(drums)
if int(pursuit_scene.campaign_state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1:
failures.append("Wan hard pursuit drums should add Wine to campaign inventory")
if int(pursuit_scene.state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1:
failures.append("Wan hard pursuit drums should refresh battle Wine inventory")
if not pursuit_scene.campaign_state.has_claimed_camp_conversation_effects(WAN_SCENARIO_ID, WAN_PURSUIT_CONVERSATION_ID):
failures.append("Wan hard pursuit drums claim should be saved")
if pursuit_scene._format_talk_status_text(pursuit_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed":
failures.append("Wan hard pursuit talk status should show claimed supply after claim")
pursuit_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

@@ -359,6 +359,64 @@ func _check_battle_visual_data(failures: Array[String]) -> void:
_check_shop_item_visibility(failures, xian_pressed_state, "iron_armor", false, "007 pressed") _check_shop_item_visibility(failures, xian_pressed_state, "iron_armor", false, "007 pressed")
_check_shop_item_visibility(failures, xian_pressed_state, "war_axe", true, "007 pressed") _check_shop_item_visibility(failures, xian_pressed_state, "war_axe", true, "007 pressed")
var wan_state = BattleStateScript.new()
if not wan_state.load_battle("res://data/scenarios/008_wan_castle_escape.json"):
failures.append("could not load Wan Castle Escape camp data")
else:
var wan_conversations: Array = wan_state.get_briefing().get("camp_conversations", [])
if wan_conversations.size() != 2:
failures.append("008 base briefing should expose exactly two camp conversations")
else:
_check_camp_conversation(failures, wan_conversations[0], "wan_escape_route_council", "officer", "cao_cao")
_check_camp_conversation(failures, wan_conversations[1], "wan_dian_wei_rear_guard", "officer", "dian_wei")
var wan_merchant := wan_state.get_shop_merchant()
if str(wan_merchant.get("name", "")) != "Wan Castle Sutler":
failures.append("008 shop should expose Wan Castle merchant name")
if (wan_merchant.get("lines", []) as Array).size() < 2:
failures.append("008 shop should expose merchant flavor lines")
_check_shop_item_visibility(failures, wan_state, "imperial_seal", false, "008 base")
_check_shop_item_visibility(failures, wan_state, "war_drum", false, "008 base")
var wan_court_state = BattleStateScript.new()
if not wan_court_state.load_battle("res://data/scenarios/008_wan_castle_escape.json", {}, {}, {"secured_imperial_court": true}):
failures.append("could not load Wan court seal camp data")
else:
var postern: Dictionary = _find_camp_conversation(
wan_court_state.get_briefing().get("camp_conversations", []),
"wan_court_seal_postern"
)
if postern.is_empty():
failures.append("008 secured court flag should expose court seal postern conversation")
else:
_check_camp_conversation_effect(failures, postern, "bean", 1)
if not _find_camp_conversation(
wan_court_state.get_briefing().get("camp_conversations", []),
"wan_hard_pursuit_drums"
).is_empty():
failures.append("008 secured court flag should not expose hard pursuit drums conversation")
_check_shop_item_visibility(failures, wan_court_state, "imperial_seal", true, "008 secured court")
_check_shop_item_visibility(failures, wan_court_state, "war_drum", false, "008 secured court")
var wan_pursuit_state = BattleStateScript.new()
if not wan_pursuit_state.load_battle("res://data/scenarios/008_wan_castle_escape.json", {}, {}, {"pursued_li_jue_remnants": true}):
failures.append("could not load Wan hard pursuit camp data")
else:
var drums: Dictionary = _find_camp_conversation(
wan_pursuit_state.get_briefing().get("camp_conversations", []),
"wan_hard_pursuit_drums"
)
if drums.is_empty():
failures.append("008 hard pursuit flag should expose hard pursuit drums conversation")
else:
_check_camp_conversation_effect(failures, drums, "wine", 1)
if not _find_camp_conversation(
wan_pursuit_state.get_briefing().get("camp_conversations", []),
"wan_court_seal_postern"
).is_empty():
failures.append("008 hard pursuit flag should not expose court seal postern conversation")
_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")
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():