Add Ye campaign camp content

This commit is contained in:
2026-06-19 01:04:49 +09:00
parent 258cdeb60c
commit dfdba012aa
3 changed files with 381 additions and 0 deletions

View File

@@ -32,9 +32,121 @@
"Secured crossings behind the army keep Cao Cao's supply line steady as the campaign enters Yuan territory." "Secured crossings behind the army keep Cao Cao's supply line steady as the campaign enters Yuan territory."
] ]
} }
],
"camp_conversations": [
{
"id": "ye_outer_council",
"group": "topic",
"label": "Outer Defense Council",
"speaker": "Cao Cao",
"summary": "Set the plan for breaking the Yuan heirs' divided outer defense.",
"lines": [
{
"speaker": "Cao Cao",
"side": "left",
"text": "Ye is not guarded by one mind. Yuan Tan and Yuan Shang share walls, not trust."
},
{
"speaker": "Guo Jia",
"side": "right",
"text": "Then the wall is stone, but the command is paper. We strike where the orders overlap."
},
{
"speaker": "Zhang He",
"side": "right",
"text": "Their outer guard obeys names before signals. Make two names answer one alarm, and the line will hesitate."
}
]
},
{
"id": "ye_zhang_he_city_map",
"group": "officer",
"officer_id": "zhang_he",
"label": "Zhang He - City Map",
"speaker": "Zhang He",
"summary": "Study Ye's outer roads with Zhang He before the assault.",
"lines": [
{
"speaker": "Zhang He",
"side": "right",
"text": "The center road looks direct because Yuan Shang wants attackers to think with their feet."
},
{
"speaker": "Cao Cao",
"side": "left",
"text": "And what does a commander think with?"
},
{
"speaker": "Zhang He",
"side": "right",
"text": "With the road the enemy forgot to fear."
}
]
},
{
"id": "ye_rapid_march_rations",
"campaign_flags": { "advanced_to_ye": true },
"group": "topic",
"label": "Rapid March Rations",
"summary": "Claim light rations packed for the swift advance from Cangting.",
"lines": [
{
"speaker": "Xiahou Yuan",
"side": "right",
"text": "The rapid column kept only dry beans and spare bowstrings. It is not comfortable, but it arrives first."
},
{
"speaker": "Guo Jia",
"side": "left",
"text": "Comfort is for the army that arrives second."
}
],
"effects": [
{
"type": "grant_item",
"item_id": "bean",
"count": 1,
"text": "The rapid march rations add a Bean to the field supplies."
}
]
},
{
"id": "ye_crossing_medicine",
"campaign_flags": { "secured_hebei_crossings": true },
"group": "topic",
"label": "Crossing Medicine",
"summary": "Claim medicine brought safely across the secured Hebei crossings.",
"lines": [
{
"speaker": "Cao Ren",
"side": "right",
"text": "The secured crossings kept the medicine carts with us. No river mud spoiled the seals."
},
{
"speaker": "Cao Cao",
"side": "left",
"text": "Then issue it before Ye teaches us the price of its walls."
}
],
"effects": [
{
"type": "grant_item",
"item_id": "panacea",
"count": 1,
"text": "The crossing medicine adds a Panacea to the field supplies."
}
]
}
] ]
}, },
"shop": { "shop": {
"merchant": {
"name": "Ye Outskirts Sutler",
"lines": [
"The Yuan heirs argue inside the walls. Out here, coin still gives a clean answer.",
"Buy what can cross a ditch and climb a wall. I left the pretty goods for men who expect to sit."
]
},
"items": [ "items": [
"bean", "bean",
"wine", "wine",

View File

@@ -54,6 +54,10 @@ const CANGTING_PRESSED_CONVERSATION_ID := "cangting_fast_pursuit_beans"
const CANGTING_STORES_CONVERSATION_ID := "cangting_wuchao_store_medicine" const CANGTING_STORES_CONVERSATION_ID := "cangting_wuchao_store_medicine"
const YE_SCENARIO_ID := "015_ye_campaign" const YE_SCENARIO_ID := "015_ye_campaign"
const YE_SCENARIO_PATH := "res://data/scenarios/015_ye_campaign.json" const YE_SCENARIO_PATH := "res://data/scenarios/015_ye_campaign.json"
const YE_RAPID_CONVERSATION_ID := "ye_rapid_march_rations"
const YE_CROSSING_CONVERSATION_ID := "ye_crossing_medicine"
const YE_SIEGE_SCENARIO_ID := "016_ye_siege"
const YE_SIEGE_SCENARIO_PATH := "res://data/scenarios/016_ye_siege.json"
const SAVE_PATH := "user://campaign_save.json" const SAVE_PATH := "user://campaign_save.json"
const MANUAL_SAVE_PATH := "user://campaign_manual_save.json" const MANUAL_SAVE_PATH := "user://campaign_manual_save.json"
@@ -77,12 +81,14 @@ func _init() -> void:
_check_guandu_post_battle_choice_bridges_to_wuchao(failures) _check_guandu_post_battle_choice_bridges_to_wuchao(failures)
_check_wuchao_post_battle_choice_bridges_to_cangting(failures) _check_wuchao_post_battle_choice_bridges_to_cangting(failures)
_check_cangting_post_battle_choice_bridges_to_ye(failures) _check_cangting_post_battle_choice_bridges_to_ye(failures)
_check_ye_post_battle_choice_bridges_to_ye_siege(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)
_check_qingzhou_branch_camp_conversations(failures) _check_qingzhou_branch_camp_conversations(failures)
_check_wuchao_branch_camp_conversations(failures) _check_wuchao_branch_camp_conversations(failures)
_check_cangting_branch_camp_conversations(failures) _check_cangting_branch_camp_conversations(failures)
_check_ye_branch_camp_conversations(failures)
_check_talk_menu_closes_on_cancel_and_reload(failures) _check_talk_menu_closes_on_cancel_and_reload(failures)
if not _restore_user_file(SAVE_PATH, save_backup): if not _restore_user_file(SAVE_PATH, save_backup):
@@ -970,6 +976,8 @@ func _check_cangting_post_battle_choice_bridges_to_ye(failures: Array[String]) -
"advance_to_ye", "advance_to_ye",
"advanced_to_ye", "advanced_to_ye",
"secured_hebei_crossings", "secured_hebei_crossings",
YE_RAPID_CONVERSATION_ID,
YE_CROSSING_CONVERSATION_ID,
"war_drum", "war_drum",
"imperial_seal" "imperial_seal"
) )
@@ -978,6 +986,27 @@ func _check_cangting_post_battle_choice_bridges_to_ye(failures: Array[String]) -
"secure_hebei_crossings", "secure_hebei_crossings",
"secured_hebei_crossings", "secured_hebei_crossings",
"advanced_to_ye", "advanced_to_ye",
YE_CROSSING_CONVERSATION_ID,
YE_RAPID_CONVERSATION_ID,
"imperial_seal",
"war_drum"
)
func _check_ye_post_battle_choice_bridges_to_ye_siege(failures: Array[String]) -> void:
_check_ye_choice_branch_to_ye_siege(
failures,
"press_ye_siege",
"pressed_ye_siege",
"split_yuan_heirs",
"war_drum",
"imperial_seal"
)
_check_ye_choice_branch_to_ye_siege(
failures,
"split_yuan_heirs",
"split_yuan_heirs",
"pressed_ye_siege",
"imperial_seal", "imperial_seal",
"war_drum" "war_drum"
) )
@@ -1049,6 +1078,8 @@ func _check_cangting_choice_branch_to_ye(
choice_id: String, choice_id: String,
expected_flag: String, expected_flag: String,
cleared_flag: String, cleared_flag: String,
expected_conversation_id: String,
hidden_conversation_id: String,
expected_shop_item_id: String, expected_shop_item_id: String,
hidden_shop_item_id: String hidden_shop_item_id: String
) -> void: ) -> void:
@@ -1095,6 +1126,10 @@ func _check_cangting_choice_branch_to_ye(
return return
if scene.state.get_unit("zhang_he_ch15").is_empty(): if scene.state.get_unit("zhang_he_ch15").is_empty():
failures.append("Cangting choice bridge %s should deploy Zhang He in Ye" % choice_id) failures.append("Cangting choice bridge %s should deploy Zhang He in Ye" % choice_id)
if scene._camp_conversation_by_id(expected_conversation_id).is_empty():
failures.append("Cangting choice bridge %s should expose %s" % [choice_id, expected_conversation_id])
if not scene._camp_conversation_by_id(hidden_conversation_id).is_empty():
failures.append("Cangting choice bridge %s should hide %s" % [choice_id, hidden_conversation_id])
var shop_items: Array = scene.state.get_shop_item_ids() var shop_items: Array = scene.state.get_shop_item_ids()
if not shop_items.has(expected_shop_item_id): if not shop_items.has(expected_shop_item_id):
failures.append("Cangting choice bridge %s should expose shop item %s" % [choice_id, expected_shop_item_id]) failures.append("Cangting choice bridge %s should expose shop item %s" % [choice_id, expected_shop_item_id])
@@ -1103,6 +1138,77 @@ func _check_cangting_choice_branch_to_ye(
scene.free() scene.free()
func _check_ye_choice_branch_to_ye_siege(
failures: Array[String],
choice_id: String,
expected_flag: String,
cleared_flag: String,
expected_shop_item_id: String,
hidden_shop_item_id: String
) -> void:
var scene = _new_prebattle_scene_for(failures, "Ye choice bridge %s" % choice_id, YE_SCENARIO_ID, YE_SCENARIO_PATH)
if scene == null:
return
if not scene.campaign_state.joined_officers.has("zhang_he"):
scene.campaign_state.joined_officers.append("zhang_he")
if not scene.state.load_battle(
YE_SCENARIO_PATH,
scene.campaign_state.get_roster_overrides(),
scene.campaign_state.get_inventory_snapshot(),
scene.campaign_state.get_flags_snapshot(),
scene.campaign_state.get_joined_officers_snapshot()
):
failures.append("Ye choice bridge %s could not reload Ye with Zhang He joined" % choice_id)
scene.free()
return
scene.state.battle_status = "victory"
var result: Dictionary = scene.campaign_state.apply_battle_result(scene.state)
if not bool(result.get("saved", false)):
failures.append("Ye choice bridge %s should save 015 victory" % choice_id)
scene.free()
return
if not scene.campaign_state.get_joined_officers_snapshot().has("zhang_he"):
failures.append("Ye choice bridge %s should keep Zhang He before Ye Siege" % choice_id)
if bool(result.get("choice_applied", true)):
failures.append("Ye choice bridge %s should wait for a selected post-battle choice" % choice_id)
if scene.campaign_state.pending_post_battle_choice_scenario_id != YE_SCENARIO_ID:
failures.append("Ye choice bridge %s should mark Ye choice pending" % choice_id)
var choice := _find_choice_by_id(result.get("post_battle_choices", []), choice_id)
if choice.is_empty():
failures.append("Ye choice bridge missing choice: %s" % choice_id)
scene.free()
return
if not scene.campaign_state.try_apply_post_battle_choice(choice, YE_SCENARIO_ID):
failures.append("Ye choice bridge %s should save selected choice" % choice_id)
scene.free()
return
var flags: Dictionary = scene.campaign_state.get_flags_snapshot()
if flags.get(expected_flag, false) != true:
failures.append("Ye choice bridge %s should set %s" % [choice_id, expected_flag])
if flags.get(cleared_flag, true) != false:
failures.append("Ye choice bridge %s should clear %s" % [choice_id, cleared_flag])
if scene.campaign_state.current_scenario_id != YE_SIEGE_SCENARIO_ID:
failures.append("Ye choice bridge %s should advance to Ye Siege, got %s" % [choice_id, scene.campaign_state.current_scenario_id])
if not scene.state.load_battle(
YE_SIEGE_SCENARIO_PATH,
scene.campaign_state.get_roster_overrides(),
scene.campaign_state.get_inventory_snapshot(),
flags,
scene.campaign_state.get_joined_officers_snapshot()
):
failures.append("Ye choice bridge %s could not load Ye Siege" % choice_id)
scene.free()
return
if scene.state.get_unit("zhang_he_ch16").is_empty():
failures.append("Ye choice bridge %s should deploy Zhang He in Ye Siege" % choice_id)
var shop_items: Array = scene.state.get_shop_item_ids()
if not shop_items.has(expected_shop_item_id):
failures.append("Ye choice bridge %s should expose shop item %s" % [choice_id, expected_shop_item_id])
if shop_items.has(hidden_shop_item_id):
failures.append("Ye choice bridge %s should hide shop item %s" % [choice_id, hidden_shop_item_id])
scene.free()
func _find_choice_by_id(choices, choice_id: String) -> Dictionary: func _find_choice_by_id(choices, choice_id: String) -> Dictionary:
if typeof(choices) != TYPE_ARRAY: if typeof(choices) != TYPE_ARRAY:
return {} return {}
@@ -1450,6 +1556,88 @@ func _check_cangting_branch_camp_conversations(failures: Array[String]) -> void:
stores_scene.free() stores_scene.free()
func _check_ye_branch_camp_conversations(failures: Array[String]) -> void:
var base_scene = _new_prebattle_scene_for(failures, "Ye base camp", YE_SCENARIO_ID, YE_SCENARIO_PATH)
if base_scene != null:
if base_scene._camp_conversation_by_id("ye_outer_council").is_empty():
failures.append("Ye should expose the outer defense council conversation")
if base_scene._camp_conversation_by_id("ye_zhang_he_city_map").is_empty():
failures.append("Ye should expose Zhang He's city map conversation")
if not base_scene._camp_conversation_by_id(YE_RAPID_CONVERSATION_ID).is_empty():
failures.append("Ye rapid march rations should be hidden without advanced flag")
if not base_scene._camp_conversation_by_id(YE_CROSSING_CONVERSATION_ID).is_empty():
failures.append("Ye crossing medicine should be hidden without crossings flag")
var merchant: Dictionary = base_scene.state.get_shop_merchant()
if str(merchant.get("name", "")) != "Ye Outskirts Sutler":
failures.append("Ye shop merchant name should be present")
var merchant_lines: Array = merchant.get("lines", [])
if merchant_lines.size() < 2:
failures.append("Ye shop merchant should expose flavor lines")
if base_scene._format_talk_status_text(base_scene._camp_conversation_entries()) != "Camp conversations | 2 topics":
failures.append("Ye base talk status should summarize two non-supply topics")
base_scene.free()
var advanced_scene = _new_prebattle_scene_for(
failures,
"Ye advanced camp",
YE_SCENARIO_ID,
YE_SCENARIO_PATH,
{"advanced_to_ye": true}
)
if advanced_scene != null:
var rapid_rations: Dictionary = advanced_scene._camp_conversation_by_id(YE_RAPID_CONVERSATION_ID)
if rapid_rations.is_empty():
failures.append("advanced to Ye flag should expose rapid march rations")
if not advanced_scene._camp_conversation_by_id(YE_CROSSING_CONVERSATION_ID).is_empty():
failures.append("advanced to Ye flag should not expose crossing medicine")
if not rapid_rations.is_empty():
if advanced_scene._format_talk_status_text(advanced_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready":
failures.append("Ye advanced talk status should show one ready supply")
if not advanced_scene._format_camp_conversation_button_text(rapid_rations).contains("Supply Bean"):
failures.append("Ye rapid rations button should show Bean supply")
var before_bean := int(advanced_scene.campaign_state.get_inventory_snapshot().get("bean", 0))
advanced_scene._apply_camp_conversation_effects(rapid_rations)
if int(advanced_scene.campaign_state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("Ye rapid rations should add Bean to campaign inventory")
if int(advanced_scene.state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("Ye rapid rations should refresh battle Bean inventory")
if not advanced_scene.campaign_state.has_claimed_camp_conversation_effects(YE_SCENARIO_ID, YE_RAPID_CONVERSATION_ID):
failures.append("Ye rapid rations claim should be saved")
if advanced_scene._format_talk_status_text(advanced_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed":
failures.append("Ye advanced talk status should show claimed supply after claim")
advanced_scene.free()
var crossing_scene = _new_prebattle_scene_for(
failures,
"Ye crossings camp",
YE_SCENARIO_ID,
YE_SCENARIO_PATH,
{"secured_hebei_crossings": true}
)
if crossing_scene != null:
var crossing_medicine: Dictionary = crossing_scene._camp_conversation_by_id(YE_CROSSING_CONVERSATION_ID)
if crossing_medicine.is_empty():
failures.append("secured Hebei crossings flag should expose Ye crossing medicine")
if not crossing_scene._camp_conversation_by_id(YE_RAPID_CONVERSATION_ID).is_empty():
failures.append("secured Hebei crossings flag should not expose Ye rapid rations")
if not crossing_medicine.is_empty():
if crossing_scene._format_talk_status_text(crossing_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready":
failures.append("Ye crossings talk status should show one ready supply")
if not crossing_scene._format_camp_conversation_button_text(crossing_medicine).contains("Supply Panacea"):
failures.append("Ye crossing medicine button should show Panacea supply")
var before_panacea := int(crossing_scene.campaign_state.get_inventory_snapshot().get("panacea", 0))
crossing_scene._apply_camp_conversation_effects(crossing_medicine)
if int(crossing_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("Ye crossing medicine should add Panacea to campaign inventory")
if int(crossing_scene.state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("Ye crossing medicine should refresh battle Panacea inventory")
if not crossing_scene.campaign_state.has_claimed_camp_conversation_effects(YE_SCENARIO_ID, YE_CROSSING_CONVERSATION_ID):
failures.append("Ye crossing medicine claim should be saved")
if crossing_scene._format_talk_status_text(crossing_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed":
failures.append("Ye crossings talk status should show claimed supply after claim")
crossing_scene.free()
func _check_talk_menu_closes_on_cancel_and_reload(failures: Array[String]) -> void: func _check_talk_menu_closes_on_cancel_and_reload(failures: Array[String]) -> void:
var scene = _new_prebattle_scene(failures, "talk menu close guards") var scene = _new_prebattle_scene(failures, "talk menu close guards")
if scene == null: if scene == null:

View File

@@ -789,6 +789,87 @@ func _check_battle_visual_data(failures: Array[String]) -> void:
).is_empty(): ).is_empty():
failures.append("014 old Wuchao flags should not expose Wuchao store medicine") failures.append("014 old Wuchao flags should not expose Wuchao store medicine")
var ye_state = BattleStateScript.new()
if not ye_state.load_battle("res://data/scenarios/015_ye_campaign.json"):
failures.append("could not load Ye Campaign camp data")
else:
_check_shop_items_unique(failures, ye_state, "015 base")
var ye_conversations: Array = ye_state.get_briefing().get("camp_conversations", [])
if ye_conversations.size() != 2:
failures.append("015 base briefing should expose exactly two camp conversations")
else:
_check_camp_conversation(failures, ye_conversations[0], "ye_outer_council", "topic", "")
_check_camp_conversation(failures, ye_conversations[1], "ye_zhang_he_city_map", "officer", "zhang_he")
var ye_merchant := ye_state.get_shop_merchant()
if str(ye_merchant.get("name", "")) != "Ye Outskirts Sutler":
failures.append("015 shop should expose Ye merchant name")
if (ye_merchant.get("lines", []) as Array).size() < 2:
failures.append("015 shop should expose merchant flavor lines")
_check_shop_item_visibility(failures, ye_state, "war_drum", false, "015 base")
_check_shop_item_visibility(failures, ye_state, "imperial_seal", false, "015 base")
_check_shop_item_visibility(failures, ye_state, "wind_chaser_bow", false, "015 base")
var ye_advanced_state = BattleStateScript.new()
if not ye_advanced_state.load_battle("res://data/scenarios/015_ye_campaign.json", {}, {}, {"advanced_to_ye": true}):
failures.append("could not load Ye advanced camp data")
else:
_check_shop_items_unique(failures, ye_advanced_state, "015 advanced to Ye")
var rapid_rations: Dictionary = _find_camp_conversation(
ye_advanced_state.get_briefing().get("camp_conversations", []),
"ye_rapid_march_rations"
)
if rapid_rations.is_empty():
failures.append("015 advanced to Ye flag should expose rapid march rations")
else:
_check_camp_conversation_effect(failures, rapid_rations, "bean", 1)
if not _find_camp_conversation(
ye_advanced_state.get_briefing().get("camp_conversations", []),
"ye_crossing_medicine"
).is_empty():
failures.append("015 advanced to Ye flag should not expose crossing medicine")
_check_shop_item_visibility(failures, ye_advanced_state, "war_drum", true, "015 advanced to Ye")
_check_shop_item_visibility(failures, ye_advanced_state, "imperial_seal", false, "015 advanced to Ye")
_check_shop_item_visibility(failures, ye_advanced_state, "wind_chaser_bow", false, "015 advanced to Ye")
var ye_crossing_state = BattleStateScript.new()
if not ye_crossing_state.load_battle("res://data/scenarios/015_ye_campaign.json", {}, {}, {"secured_hebei_crossings": true}):
failures.append("could not load Ye secured crossings camp data")
else:
_check_shop_items_unique(failures, ye_crossing_state, "015 secured Hebei crossings")
var crossing_medicine: Dictionary = _find_camp_conversation(
ye_crossing_state.get_briefing().get("camp_conversations", []),
"ye_crossing_medicine"
)
if crossing_medicine.is_empty():
failures.append("015 secured Hebei crossings flag should expose crossing medicine")
else:
_check_camp_conversation_effect(failures, crossing_medicine, "panacea", 1)
if not _find_camp_conversation(
ye_crossing_state.get_briefing().get("camp_conversations", []),
"ye_rapid_march_rations"
).is_empty():
failures.append("015 secured Hebei crossings flag should not expose rapid march rations")
_check_shop_item_visibility(failures, ye_crossing_state, "war_drum", false, "015 secured Hebei crossings")
_check_shop_item_visibility(failures, ye_crossing_state, "imperial_seal", true, "015 secured Hebei crossings")
_check_shop_item_visibility(failures, ye_crossing_state, "wind_chaser_bow", false, "015 secured Hebei crossings")
var ye_old_flags_state = BattleStateScript.new()
if not ye_old_flags_state.load_battle("res://data/scenarios/015_ye_campaign.json", {}, {}, {"pressed_yuan_rout": true, "secured_wuchao_stores": true}):
failures.append("could not load Ye old Cangting flag camp data")
else:
_check_shop_item_visibility(failures, ye_old_flags_state, "war_drum", false, "015 old Cangting flags")
_check_shop_item_visibility(failures, ye_old_flags_state, "imperial_seal", false, "015 old Cangting flags")
if not _find_camp_conversation(
ye_old_flags_state.get_briefing().get("camp_conversations", []),
"ye_rapid_march_rations"
).is_empty():
failures.append("015 old Cangting flags should not expose rapid march rations")
if not _find_camp_conversation(
ye_old_flags_state.get_briefing().get("camp_conversations", []),
"ye_crossing_medicine"
).is_empty():
failures.append("015 old Cangting flags should not expose crossing medicine")
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():