Add Yan Ford pursuit camp content

This commit is contained in:
2026-06-19 00:20:10 +09:00
parent 99d58d05ee
commit 0603d241d3
3 changed files with 264 additions and 1 deletions

View File

@@ -32,6 +32,102 @@
"Raids against Yuan Shao's baggage train leave Wen Chou's riders angry and hungry, but still eager to strike."
]
}
],
"camp_conversations": [
{
"id": "yan_ford_pursuit_council",
"group": "officer",
"officer_id": "guo_jia",
"label": "Guo Jia - Ford Trap",
"speaker": "Guo Jia",
"summary": "Shape the ford road into a charge that Wen Chou cannot safely recall.",
"lines": [
{
"speaker": "Guo Jia",
"side": "right",
"text": "Wen Chou rides to repair pride, not formation. Give him a road that feels open until it closes behind him."
},
{
"speaker": "Cao Cao",
"side": "left",
"text": "Yan Liang taught Yuan Shao fear. Wen Chou will try to answer with noise. We answer with banks and timing."
}
]
},
{
"id": "yan_ford_xiahou_yuan_banks",
"group": "officer",
"officer_id": "xiahou_yuan",
"label": "Xiahou Yuan - River Banks",
"speaker": "Xiahou Yuan",
"summary": "Watch the wooded banks while the center draws Wen Chou forward.",
"lines": [
{
"speaker": "Xiahou Yuan",
"side": "right",
"text": "The ford bends the field, but the wooded banks hide arrows. I will keep the scouts moving."
},
{
"speaker": "Guo Jia",
"side": "left",
"text": "Good. A trap fails when it forgets the second path. Watch the bank while the road does its work."
}
]
},
{
"id": "yan_ford_fortified_crossing_medicine",
"campaign_flags": { "fortified_white_horse": true },
"group": "topic",
"label": "Fortified Crossing Medicine",
"summary": "Use the stable White Horse crossing to bring field medicine forward.",
"lines": [
{
"speaker": "Cao Ren",
"side": "left",
"text": "The fortified crossing behind us is still open. I brought medicine across before Yuan Shao's messengers could stir it."
},
{
"speaker": "Cao Cao",
"side": "right",
"text": "Then give it to the men anchoring the ford. A steady rear makes a narrow road sharper."
}
],
"effects": [
{
"type": "grant_item",
"item_id": "panacea",
"count": 1,
"text": "The fortified crossing medicine adds a Panacea to the field supplies."
}
]
},
{
"id": "yan_ford_raided_supply_beans",
"campaign_flags": { "raided_yuan_supplies": true },
"group": "topic",
"label": "Raided Supply Beans",
"summary": "Distribute captured rations from Yuan Shao's disrupted baggage train.",
"lines": [
{
"speaker": "Xiahou Yuan",
"side": "right",
"text": "The raid scattered Yuan Shao's baggage. Some of the beans are already in our sacks, and his riders know it."
},
{
"speaker": "Dian Wei",
"side": "left",
"text": "Good. Hungry enemies chase harder. Fed soldiers close the road better."
}
],
"effects": [
{
"type": "grant_item",
"item_id": "bean",
"count": 1,
"text": "The raided supply beans add a Bean to the field supplies."
}
]
}
]
},
"shop": {
@@ -54,7 +150,14 @@
"campaign_flags": { "raided_yuan_supplies": true },
"items": ["war_axe"]
}
]
],
"merchant": {
"name": "Yan Ford Bank Sutler",
"lines": [
"Keep your purchases high and dry. The ford takes careless sacks before it takes careless men.",
"No heavy loads on the bank road. Beans, medicine, and blades already fitted to the hand."
]
}
},
"roster": {
"max_units": 6,

View File

@@ -38,6 +38,10 @@ 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 YAN_FORD_SCENARIO_ID := "011_yan_ford_pursuit"
const YAN_FORD_SCENARIO_PATH := "res://data/scenarios/011_yan_ford_pursuit.json"
const YAN_FORD_FORTIFIED_CONVERSATION_ID := "yan_ford_fortified_crossing_medicine"
const YAN_FORD_RAIDED_CONVERSATION_ID := "yan_ford_raided_supply_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"
@@ -58,6 +62,7 @@ func _init() -> void:
_check_wan_branch_camp_conversations(failures)
_check_xiapi_branch_camp_conversations(failures)
_check_white_horse_branch_camp_conversations(failures)
_check_yan_ford_branch_camp_conversations(failures)
_check_manual_checkpoint_reverts_claim(failures)
_check_completed_replay_cannot_claim(failures)
_check_conditional_camp_conversations(failures)
@@ -673,6 +678,88 @@ func _check_white_horse_branch_camp_conversations(failures: Array[String]) -> vo
pressed_scene.free()
func _check_yan_ford_branch_camp_conversations(failures: Array[String]) -> void:
var base_scene = _new_prebattle_scene_for(failures, "Yan Ford base camp", YAN_FORD_SCENARIO_ID, YAN_FORD_SCENARIO_PATH)
if base_scene != null:
if base_scene._camp_conversation_by_id("yan_ford_pursuit_council").is_empty():
failures.append("Yan Ford should expose the pursuit council conversation")
if base_scene._camp_conversation_by_id("yan_ford_xiahou_yuan_banks").is_empty():
failures.append("Yan Ford should expose Xiahou Yuan's river banks conversation")
if not base_scene._camp_conversation_by_id(YAN_FORD_FORTIFIED_CONVERSATION_ID).is_empty():
failures.append("Yan Ford fortified crossing medicine should be hidden without fortified White Horse flag")
if not base_scene._camp_conversation_by_id(YAN_FORD_RAIDED_CONVERSATION_ID).is_empty():
failures.append("Yan Ford raided supply beans should be hidden without raided Yuan supplies flag")
var merchant: Dictionary = base_scene.state.get_shop_merchant()
if str(merchant.get("name", "")) != "Yan Ford Bank Sutler":
failures.append("Yan Ford shop merchant name should be present")
var merchant_lines: Array = merchant.get("lines", [])
if merchant_lines.size() < 2:
failures.append("Yan Ford shop merchant should expose flavor lines")
if base_scene._format_talk_status_text(base_scene._camp_conversation_entries()) != "Camp conversations | 2 topics":
failures.append("Yan Ford base talk status should summarize two non-supply topics")
base_scene.free()
var fortified_scene = _new_prebattle_scene_for(
failures,
"Yan Ford fortified White Horse camp",
YAN_FORD_SCENARIO_ID,
YAN_FORD_SCENARIO_PATH,
{"fortified_white_horse": true}
)
if fortified_scene != null:
var medicine: Dictionary = fortified_scene._camp_conversation_by_id(YAN_FORD_FORTIFIED_CONVERSATION_ID)
if medicine.is_empty():
failures.append("fortified White Horse flag should expose Yan Ford crossing medicine")
if not fortified_scene._camp_conversation_by_id(YAN_FORD_RAIDED_CONVERSATION_ID).is_empty():
failures.append("fortified White Horse flag should not expose Yan Ford raided supply beans")
if not medicine.is_empty():
if fortified_scene._format_talk_status_text(fortified_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready":
failures.append("Yan Ford fortified talk status should show one ready supply")
if not fortified_scene._format_camp_conversation_button_text(medicine).contains("Supply Panacea"):
failures.append("Yan Ford crossing medicine button should preview Panacea supply")
var before_panacea := int(fortified_scene.campaign_state.get_inventory_snapshot().get("panacea", 0))
fortified_scene._apply_camp_conversation_effects(medicine)
if int(fortified_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("Yan Ford crossing medicine should add Panacea to campaign inventory")
if int(fortified_scene.state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("Yan Ford crossing medicine should refresh battle Panacea inventory")
if not fortified_scene.campaign_state.has_claimed_camp_conversation_effects(YAN_FORD_SCENARIO_ID, YAN_FORD_FORTIFIED_CONVERSATION_ID):
failures.append("Yan Ford crossing medicine claim should be saved")
if fortified_scene._format_talk_status_text(fortified_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed":
failures.append("Yan Ford fortified talk status should show claimed supply after claim")
fortified_scene.free()
var raided_scene = _new_prebattle_scene_for(
failures,
"Yan Ford raided Yuan supplies camp",
YAN_FORD_SCENARIO_ID,
YAN_FORD_SCENARIO_PATH,
{"raided_yuan_supplies": true}
)
if raided_scene != null:
var beans: Dictionary = raided_scene._camp_conversation_by_id(YAN_FORD_RAIDED_CONVERSATION_ID)
if beans.is_empty():
failures.append("raided Yuan supplies flag should expose Yan Ford raided supply beans")
if not raided_scene._camp_conversation_by_id(YAN_FORD_FORTIFIED_CONVERSATION_ID).is_empty():
failures.append("raided Yuan supplies flag should not expose Yan Ford crossing medicine")
if not beans.is_empty():
if raided_scene._format_talk_status_text(raided_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready":
failures.append("Yan Ford raided supplies talk status should show one ready supply")
if not raided_scene._format_camp_conversation_button_text(beans).contains("Supply Bean"):
failures.append("Yan Ford raided supply beans button should preview Bean supply")
var before_bean := int(raided_scene.campaign_state.get_inventory_snapshot().get("bean", 0))
raided_scene._apply_camp_conversation_effects(beans)
if int(raided_scene.campaign_state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("Yan Ford raided supply beans should add Bean to campaign inventory")
if int(raided_scene.state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("Yan Ford raided supply beans should refresh battle Bean inventory")
if not raided_scene.campaign_state.has_claimed_camp_conversation_effects(YAN_FORD_SCENARIO_ID, YAN_FORD_RAIDED_CONVERSATION_ID):
failures.append("Yan Ford raided supply beans claim should be saved")
if raided_scene._format_talk_status_text(raided_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed":
failures.append("Yan Ford raided supplies talk status should show claimed supply after claim")
raided_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

@@ -537,6 +537,18 @@ func _check_battle_visual_data(failures: Array[String]) -> void:
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_items_unique(failures, yan_ford_state, "011 base")
var yan_ford_conversations: Array = yan_ford_state.get_briefing().get("camp_conversations", [])
if yan_ford_conversations.size() != 2:
failures.append("011 base briefing should expose exactly two camp conversations")
else:
_check_camp_conversation(failures, yan_ford_conversations[0], "yan_ford_pursuit_council", "officer", "guo_jia")
_check_camp_conversation(failures, yan_ford_conversations[1], "yan_ford_xiahou_yuan_banks", "officer", "xiahou_yuan")
var yan_ford_merchant := yan_ford_state.get_shop_merchant()
if str(yan_ford_merchant.get("name", "")) != "Yan Ford Bank Sutler":
failures.append("011 shop should expose Yan Ford merchant name")
if (yan_ford_merchant.get("lines", []) as Array).size() < 2:
failures.append("011 shop should expose merchant flavor lines")
_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")
@@ -544,6 +556,20 @@ func _check_battle_visual_data(failures: Array[String]) -> void:
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_items_unique(failures, yan_ford_fortified_state, "011 fortified White Horse")
var crossing_medicine: Dictionary = _find_camp_conversation(
yan_ford_fortified_state.get_briefing().get("camp_conversations", []),
"yan_ford_fortified_crossing_medicine"
)
if crossing_medicine.is_empty():
failures.append("011 fortified White Horse flag should expose crossing medicine conversation")
else:
_check_camp_conversation_effect(failures, crossing_medicine, "panacea", 1)
if not _find_camp_conversation(
yan_ford_fortified_state.get_briefing().get("camp_conversations", []),
"yan_ford_raided_supply_beans"
).is_empty():
failures.append("011 fortified White Horse flag should not expose raided supply beans conversation")
_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")
@@ -551,9 +577,47 @@ func _check_battle_visual_data(failures: Array[String]) -> void:
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_items_unique(failures, yan_ford_raided_state, "011 raided Yuan supplies")
var supply_beans: Dictionary = _find_camp_conversation(
yan_ford_raided_state.get_briefing().get("camp_conversations", []),
"yan_ford_raided_supply_beans"
)
if supply_beans.is_empty():
failures.append("011 raided Yuan supplies flag should expose raided supply beans conversation")
else:
_check_camp_conversation_effect(failures, supply_beans, "bean", 1)
if not _find_camp_conversation(
yan_ford_raided_state.get_briefing().get("camp_conversations", []),
"yan_ford_fortified_crossing_medicine"
).is_empty():
failures.append("011 raided Yuan supplies flag should not expose crossing medicine conversation")
_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")
var guandu_state = BattleStateScript.new()
if not guandu_state.load_battle("res://data/scenarios/012_guandu_showdown.json"):
failures.append("could not load Guandu Showdown shop data")
else:
_check_shop_items_unique(failures, guandu_state, "012 base")
_check_shop_item_visibility(failures, guandu_state, "imperial_seal", false, "012 base")
_check_shop_item_visibility(failures, guandu_state, "war_drum", false, "012 base")
var guandu_secured_state = BattleStateScript.new()
if not guandu_secured_state.load_battle("res://data/scenarios/012_guandu_showdown.json", {}, {}, {"secured_guandu_line": true}):
failures.append("could not load Guandu secured line shop data")
else:
_check_shop_items_unique(failures, guandu_secured_state, "012 secured Guandu line")
_check_shop_item_visibility(failures, guandu_secured_state, "imperial_seal", true, "012 secured Guandu line")
_check_shop_item_visibility(failures, guandu_secured_state, "war_drum", false, "012 secured Guandu line")
var guandu_scouted_state = BattleStateScript.new()
if not guandu_scouted_state.load_battle("res://data/scenarios/012_guandu_showdown.json", {}, {}, {"scouted_wuchao_depots": true}):
failures.append("could not load Guandu scouted Wuchao shop data")
else:
_check_shop_items_unique(failures, guandu_scouted_state, "012 scouted Wuchao")
_check_shop_item_visibility(failures, guandu_scouted_state, "imperial_seal", false, "012 scouted Wuchao")
_check_shop_item_visibility(failures, guandu_scouted_state, "war_drum", true, "012 scouted Wuchao")
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():
@@ -1152,6 +1216,15 @@ func _check_shop_item_visibility(failures: Array[String], state, item_id: String
failures.append("%s shop visibility for %s should be %s" % [context, item_id, str(expected_visible)])
func _check_shop_items_unique(failures: Array[String], state, context: String) -> void:
var seen := {}
for item_id in state.get_shop_item_ids():
var key := str(item_id)
if seen.has(key):
failures.append("%s shop should not expose duplicate item id: %s" % [context, key])
seen[key] = true
func _check_camp_conversation_effect(failures: Array[String], conversation: Dictionary, expected_item_id: String, expected_count: int) -> void:
var effects: Array = conversation.get("effects", [])
if effects.is_empty():