Add Puyang raid camp content

This commit is contained in:
2026-06-18 23:38:09 +09:00
parent 684332c6d8
commit 201adb3bbc
3 changed files with 231 additions and 1 deletions

View File

@@ -19,6 +19,73 @@
"Lu Bu has seized momentum in Yan Province.",
"Cao Cao launches a night raid near Puyang to break the forward guard before the main cavalry can gather.",
"Dian Wei joins the front line as Cao Cao's personal shield."
],
"camp_conversations": [
{
"id": "puyang_night_raid_council",
"group": "officer",
"officer_id": "cao_cao",
"label": "Cao Cao - Night Raid",
"speaker": "Cao Cao",
"summary": "Review the timing of the strike against Lu Bu's forward guard.",
"lines": [
{ "speaker": "Cao Cao", "side": "left", "text": "Lu Bu wins when a field becomes a race. Tonight we deny him daylight, distance, and the first clean charge." },
{ "speaker": "Xiahou Dun", "side": "right", "text": "Then we hit the guard camp before the horses are bridled. I like battles that begin with the enemy half awake." }
]
},
{
"id": "puyang_dian_wei_shield",
"group": "officer",
"officer_id": "dian_wei",
"label": "Dian Wei - Iron Shield",
"speaker": "Dian Wei",
"summary": "Hear Dian Wei's first pledge as Cao Cao's personal shield.",
"lines": [
{ "speaker": "Dian Wei", "side": "right", "text": "You gave me a place in the line. I will make that place difficult for any rider to cross." },
{ "speaker": "Cao Cao", "side": "left", "text": "Stand close when the alarm rises. Lu Bu's men will aim for the banner before they understand the field." }
]
},
{
"id": "puyang_raider_stores",
"group": "topic",
"label": "Raider Stores",
"summary": "Pack a small supply bundle for the night raid.",
"lines": [
{ "speaker": "Puyang Night Trader", "side": "right", "text": "A night raid does not leave room for wagons. I tied the beans into dark cloth so the men can carry them under armor." },
{ "speaker": "Xiahou Yuan", "side": "left", "text": "Good. When Lu Bu's riders wake, no one will have time to search a cart." }
],
"effects": [
{ "type": "grant_item", "item_id": "bean", "count": 1, "text": "The raider stores add a Bean to the field supplies." }
]
},
{
"id": "puyang_hardened_vanguard",
"campaign_flags": { "pursued_dong_zhuo": true },
"group": "topic",
"label": "Hardened Vanguard",
"summary": "Issue wine to veterans used to forced marches and sudden strikes.",
"lines": [
{ "speaker": "Xiahou Dun", "side": "right", "text": "The men who chased Dong Zhuo still move before the drum finishes. Puyang's night road will not frighten them." },
{ "speaker": "Cao Cao", "side": "left", "text": "Give those veterans wine before we step off. Men who endure hard roads should know their endurance is seen." }
],
"effects": [
{ "type": "grant_item", "item_id": "wine", "count": 1, "text": "The hardened vanguard adds Wine to the field supplies." }
]
},
{
"id": "puyang_qingzhou_reserve_wagon",
"campaign_flags": { "regrouped_after_sishui": true },
"group": "topic",
"label": "Qingzhou Reserve Wagon",
"summary": "Open a physician's bundle preserved during the steadier march.",
"lines": [
{ "speaker": "Puyang Night Trader", "side": "right", "text": "The reserve wagon from Qingzhou still has a sealed physician's bundle. Quiet hands packed it better than hurried ones." },
{ "speaker": "Dian Wei", "side": "left", "text": "Keep it near the banner. If Lu Bu's riders break through, wounded men will need more than courage." }
],
"effects": [
{ "type": "grant_item", "item_id": "panacea", "count": 1, "text": "The Qingzhou reserve wagon adds a Panacea to the field supplies." }
]
}
]
},
"shop": {
@@ -31,7 +98,14 @@
"short_bow",
"hand_axe",
"iron_armor"
]
],
"merchant": {
"name": "Puyang Night Trader",
"lines": [
"Keep the lamps hooded. Lu Bu's scouts buy anything that shines.",
"His riders spend silver on speed. You will spend it on armor, beans, and living long enough to use both."
]
}
},
"roster": {
"max_units": 4,

View File

@@ -13,6 +13,11 @@ const BRANCH_SCENARIO_ID := "003_xingyang_ambush"
const BRANCH_SCENARIO_PATH := "res://data/scenarios/003_xingyang_ambush.json"
const QINGZHOU_SCENARIO_ID := "004_qingzhou_campaign"
const QINGZHOU_SCENARIO_PATH := "res://data/scenarios/004_qingzhou_campaign.json"
const PUYANG_SCENARIO_ID := "005_puyang_raid"
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 WUCHAO_SCENARIO_ID := "013_wuchao_raid"
const WUCHAO_SCENARIO_PATH := "res://data/scenarios/013_wuchao_raid.json"
const SAVE_PATH := "user://campaign_save.json"
@@ -27,6 +32,7 @@ func _init() -> void:
_check_claim_updates_campaign_and_battle(failures)
_check_shop_and_armory_preserve_claim(failures)
_check_sishui_gate_camp_supplies(failures)
_check_puyang_raid_camp_supplies(failures)
_check_manual_checkpoint_reverts_claim(failures)
_check_completed_replay_cannot_claim(failures)
_check_conditional_camp_conversations(failures)
@@ -136,6 +142,102 @@ func _check_sishui_gate_camp_supplies(failures: Array[String]) -> void:
scene.free()
func _check_puyang_raid_camp_supplies(failures: Array[String]) -> void:
var scene = _new_prebattle_scene_for(failures, "Puyang raid camp supplies", PUYANG_SCENARIO_ID, PUYANG_SCENARIO_PATH)
if scene == null:
return
if scene._camp_conversation_by_id("puyang_night_raid_council").is_empty():
failures.append("Puyang should expose the night raid council conversation")
if scene._camp_conversation_by_id("puyang_dian_wei_shield").is_empty():
failures.append("Puyang should expose Dian Wei's officer conversation")
if not scene._camp_conversation_by_id(PUYANG_PURSUIT_CONVERSATION_ID).is_empty():
failures.append("Puyang pursuit supplies should be hidden without pursuit flag")
if not scene._camp_conversation_by_id(PUYANG_REGROUP_CONVERSATION_ID).is_empty():
failures.append("Puyang regroup supplies should be hidden without regroup flag")
var conversation: Dictionary = scene._camp_conversation_by_id(PUYANG_CONVERSATION_ID)
if conversation.is_empty():
failures.append("Puyang should expose raider stores conversation")
scene.free()
return
var merchant: Dictionary = scene.state.get_shop_merchant()
if str(merchant.get("name", "")) != "Puyang Night Trader":
failures.append("Puyang shop merchant name should be present")
var merchant_lines: Array = merchant.get("lines", [])
if merchant_lines.size() < 2:
failures.append("Puyang shop merchant should expose flavor lines")
if scene._format_talk_status_text(scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready":
failures.append("Puyang talk status should show one ready supply")
if not scene._format_camp_conversation_button_text(conversation).contains("Supply Bean"):
failures.append("Puyang raider stores button should preview Bean supply")
var before_bean := int(scene.campaign_state.get_inventory_snapshot().get("bean", 0))
scene._apply_camp_conversation_effects(conversation)
if int(scene.campaign_state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("Puyang raider stores should add Bean to campaign inventory")
if int(scene.state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("Puyang raider stores should refresh battle Bean inventory")
if not scene.campaign_state.has_claimed_camp_conversation_effects(PUYANG_SCENARIO_ID, PUYANG_CONVERSATION_ID):
failures.append("Puyang raider stores claim should be saved")
if scene._format_talk_status_text(scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed":
failures.append("Puyang talk status should show claimed supply after claim")
scene.free()
var pursuit_scene = _new_prebattle_scene_for(
failures,
"Puyang pursuit camp supplies",
PUYANG_SCENARIO_ID,
PUYANG_SCENARIO_PATH,
{"pursued_dong_zhuo": true}
)
if pursuit_scene != null:
var vanguard: Dictionary = pursuit_scene._camp_conversation_by_id(PUYANG_PURSUIT_CONVERSATION_ID)
if vanguard.is_empty():
failures.append("Puyang pursuit flag should expose hardened vanguard supplies")
if not pursuit_scene._camp_conversation_by_id(PUYANG_REGROUP_CONVERSATION_ID).is_empty():
failures.append("Puyang pursuit flag should not expose Qingzhou reserve wagon")
if not vanguard.is_empty():
if pursuit_scene._format_talk_status_text(pursuit_scene._camp_conversation_entries()) != "Camp conversations | 4 topics | 2 supplies ready":
failures.append("Puyang pursuit talk status should show two ready supplies")
if not pursuit_scene._format_camp_conversation_button_text(vanguard).contains("Supply Wine"):
failures.append("Puyang hardened vanguard 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(vanguard)
if int(pursuit_scene.campaign_state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1:
failures.append("Puyang hardened vanguard should add Wine to campaign inventory")
if int(pursuit_scene.state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1:
failures.append("Puyang hardened vanguard should refresh battle Wine inventory")
if not pursuit_scene.campaign_state.has_claimed_camp_conversation_effects(PUYANG_SCENARIO_ID, PUYANG_PURSUIT_CONVERSATION_ID):
failures.append("Puyang hardened vanguard claim should be saved")
pursuit_scene.free()
var regroup_scene = _new_prebattle_scene_for(
failures,
"Puyang regroup camp supplies",
PUYANG_SCENARIO_ID,
PUYANG_SCENARIO_PATH,
{"regrouped_after_sishui": true}
)
if regroup_scene != null:
var reserve: Dictionary = regroup_scene._camp_conversation_by_id(PUYANG_REGROUP_CONVERSATION_ID)
if reserve.is_empty():
failures.append("Puyang regroup flag should expose Qingzhou reserve wagon")
if not regroup_scene._camp_conversation_by_id(PUYANG_PURSUIT_CONVERSATION_ID).is_empty():
failures.append("Puyang regroup flag should not expose hardened vanguard")
if not reserve.is_empty():
if regroup_scene._format_talk_status_text(regroup_scene._camp_conversation_entries()) != "Camp conversations | 4 topics | 2 supplies ready":
failures.append("Puyang regroup talk status should show two ready supplies")
if not regroup_scene._format_camp_conversation_button_text(reserve).contains("Supply Panacea"):
failures.append("Puyang Qingzhou reserve button should preview Panacea supply")
var before_panacea := int(regroup_scene.campaign_state.get_inventory_snapshot().get("panacea", 0))
regroup_scene._apply_camp_conversation_effects(reserve)
if int(regroup_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("Puyang Qingzhou reserve should add Panacea to campaign inventory")
if int(regroup_scene.state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("Puyang Qingzhou reserve should refresh battle Panacea inventory")
if not regroup_scene.campaign_state.has_claimed_camp_conversation_effects(PUYANG_SCENARIO_ID, PUYANG_REGROUP_CONVERSATION_ID):
failures.append("Puyang Qingzhou reserve claim should be saved")
regroup_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

@@ -189,6 +189,60 @@ func _check_battle_visual_data(failures: Array[String]) -> void:
).is_empty():
failures.append("004 regroup flag should not expose hardened veterans conversation")
var puyang_state = BattleStateScript.new()
if not puyang_state.load_battle("res://data/scenarios/005_puyang_raid.json"):
failures.append("could not load Puyang Raid camp data")
else:
var puyang_conversations: Array = puyang_state.get_briefing().get("camp_conversations", [])
if puyang_conversations.size() != 3:
failures.append("005 base briefing should expose exactly three camp conversations")
else:
_check_camp_conversation(failures, puyang_conversations[0], "puyang_night_raid_council", "officer", "cao_cao")
_check_camp_conversation(failures, puyang_conversations[1], "puyang_dian_wei_shield", "officer", "dian_wei")
_check_camp_conversation(failures, puyang_conversations[2], "puyang_raider_stores", "topic", "")
_check_camp_conversation_effect(failures, puyang_conversations[2], "bean", 1)
var puyang_merchant := puyang_state.get_shop_merchant()
if str(puyang_merchant.get("name", "")) != "Puyang Night Trader":
failures.append("005 shop should expose Puyang merchant name")
if (puyang_merchant.get("lines", []) as Array).size() < 2:
failures.append("005 shop should expose merchant flavor lines")
var puyang_pursuit_state = BattleStateScript.new()
if not puyang_pursuit_state.load_battle("res://data/scenarios/005_puyang_raid.json", {}, {}, {"pursued_dong_zhuo": true}):
failures.append("could not load Puyang pursuit camp data")
else:
var vanguard: Dictionary = _find_camp_conversation(
puyang_pursuit_state.get_briefing().get("camp_conversations", []),
"puyang_hardened_vanguard"
)
if vanguard.is_empty():
failures.append("005 pursuit flag should expose hardened vanguard conversation")
else:
_check_camp_conversation_effect(failures, vanguard, "wine", 1)
if not _find_camp_conversation(
puyang_pursuit_state.get_briefing().get("camp_conversations", []),
"puyang_qingzhou_reserve_wagon"
).is_empty():
failures.append("005 pursuit flag should not expose Qingzhou reserve wagon conversation")
var puyang_regroup_state = BattleStateScript.new()
if not puyang_regroup_state.load_battle("res://data/scenarios/005_puyang_raid.json", {}, {}, {"regrouped_after_sishui": true}):
failures.append("could not load Puyang regroup camp data")
else:
var reserve: Dictionary = _find_camp_conversation(
puyang_regroup_state.get_briefing().get("camp_conversations", []),
"puyang_qingzhou_reserve_wagon"
)
if reserve.is_empty():
failures.append("005 regroup flag should expose Qingzhou reserve wagon conversation")
else:
_check_camp_conversation_effect(failures, reserve, "panacea", 1)
if not _find_camp_conversation(
puyang_regroup_state.get_briefing().get("camp_conversations", []),
"puyang_hardened_vanguard"
).is_empty():
failures.append("005 regroup flag should not expose hardened vanguard conversation")
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():