Add Wuchao camp conversations

This commit is contained in:
2026-06-18 20:26:01 +09:00
parent 458de3b79f
commit dfe410b3c0
3 changed files with 174 additions and 1 deletions

View File

@@ -32,9 +32,70 @@
"The reinforced Guandu camps can withstand a longer absence, but the raid must still finish before dawn." "The reinforced Guandu camps can withstand a longer absence, but the raid must still finish before dawn."
] ]
} }
],
"camp_conversations": [
{
"id": "wuchao_night_raid_council",
"group": "topic",
"label": "Night Raid Council",
"speaker": "Guo Jia",
"summary": "Review the depot objective and escape route.",
"lines": [
{ "speaker": "Guo Jia", "side": "right", "text": "Wuchao feeds Yuan Shao's whole line. Fire there will travel faster than any messenger." },
{ "speaker": "Cao Cao", "side": "left", "text": "Then the first objective is silence. The second is flame. The third is getting back through the panic." },
{ "speaker": "Dian Wei", "side": "right", "text": "Silence, flame, then panic. I can remember that order." }
]
},
{
"id": "wuchao_guo_jia_empty_bowl",
"group": "officer",
"officer_id": "guo_jia",
"label": "Guo Jia - Empty Bowl",
"speaker": "Guo Jia",
"summary": "Weigh fire against formation.",
"lines": [
{ "speaker": "Guo Jia", "side": "right", "text": "A burned granary wins nothing if our line scatters in the smoke." },
{ "speaker": "Cao Cao", "side": "left", "text": "So we cut the bowl from Yuan Shao's hand, then march out as one blade." }
]
},
{
"id": "wuchao_fast_raid_supplies",
"group": "topic",
"label": "Fast Raid Stores",
"summary": "Claim scout-carried field supplies.",
"campaign_flags": { "prepared_wuchao_raid": true },
"lines": [
{ "speaker": "Xiahou Yuan", "side": "right", "text": "The scouts hid dry rations beside the narrow track. Fast riders can carry them without slowing the column." },
{ "speaker": "Cao Cao", "side": "left", "text": "Good. Speed is only useful if it still has breath at the far end." }
],
"effects": [
{ "type": "grant_item", "item_id": "bean", "count": 1, "text": "The raid scouts add a Bean to the field supplies." }
]
},
{
"id": "wuchao_guandu_reserve_wagon",
"group": "topic",
"label": "Guandu Reserve Wagon",
"summary": "Claim medicine from the reinforced camp line.",
"campaign_flags": { "strengthened_guandu_defense": true },
"lines": [
{ "speaker": "Cao Ren", "side": "right", "text": "The reinforced camps sent a reserve wagon after us. It carries medicine, not speed." },
{ "speaker": "Guo Jia", "side": "right", "text": "Medicine is speed after the first arrow lands." }
],
"effects": [
{ "type": "grant_item", "item_id": "panacea", "count": 1, "text": "The Guandu reserve wagon adds a Panacea to the field supplies." }
]
}
] ]
}, },
"shop": { "shop": {
"merchant": {
"name": "Wuchao Night Trader",
"lines": [
"No lanterns, my lord. I wrapped the beans and wine in felt so they make no sound.",
"If you buy armor tonight, wear it before the moon drops. Dawn will make merchants honest and arrows accurate."
]
},
"items": [ "items": [
"bean", "bean",
"wine", "wine",

View File

@@ -3541,7 +3541,7 @@ func _rebuild_talk_menu() -> void:
_clear_talk_list() _clear_talk_list()
var conversations := _camp_conversation_entries() var conversations := _camp_conversation_entries()
if talk_status_label != null: if talk_status_label != null:
talk_status_label.text = "Camp conversations | %d available" % conversations.size() talk_status_label.text = _format_talk_status_text(conversations)
if conversations.is_empty(): if conversations.is_empty():
var empty_label := Label.new() var empty_label := Label.new()
empty_label.text = "No camp conversations" empty_label.text = "No camp conversations"
@@ -3555,6 +3555,35 @@ func _rebuild_talk_menu() -> void:
talk_list.add_child(button) talk_list.add_child(button)
func _format_talk_status_text(conversations: Array) -> String:
var supply_available := 0
var supply_claimed := 0
for conversation in conversations:
if typeof(conversation) != TYPE_DICTIONARY:
continue
var effects: Array = conversation.get("effects", [])
if effects.is_empty():
continue
var has_supply := false
for effect in effects:
if typeof(effect) == TYPE_DICTIONARY and str(effect.get("type", "")) == "grant_item":
has_supply = true
break
if not has_supply:
continue
var conversation_id := str(conversation.get("id", ""))
if campaign_state.has_claimed_camp_conversation_effects(active_scenario_id, conversation_id):
supply_claimed += 1
else:
supply_available += 1
var parts := ["Camp conversations", "%d topics" % conversations.size()]
if supply_available > 0:
parts.append("%d supplies ready" % supply_available)
if supply_claimed > 0:
parts.append("%d claimed" % supply_claimed)
return _join_strings(parts, " | ")
func _clear_talk_list() -> void: func _clear_talk_list() -> void:
for child in talk_list.get_children(): for child in talk_list.get_children():
talk_list.remove_child(child) talk_list.remove_child(child)

View File

@@ -8,6 +8,8 @@ const SCENARIO_PATH := "res://data/scenarios/001_yellow_turbans.json"
const CONVERSATION_ID := "northern_woods_cache" const CONVERSATION_ID := "northern_woods_cache"
const BRANCH_SCENARIO_ID := "003_xingyang_ambush" const BRANCH_SCENARIO_ID := "003_xingyang_ambush"
const BRANCH_SCENARIO_PATH := "res://data/scenarios/003_xingyang_ambush.json" const BRANCH_SCENARIO_PATH := "res://data/scenarios/003_xingyang_ambush.json"
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" 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"
@@ -22,6 +24,7 @@ func _init() -> void:
_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_wuchao_branch_camp_conversations(failures)
if not _restore_user_file(SAVE_PATH, save_backup): if not _restore_user_file(SAVE_PATH, save_backup):
failures.append("could not restore automatic campaign save") failures.append("could not restore automatic campaign save")
@@ -197,6 +200,86 @@ func _check_conditional_camp_conversations(failures: Array[String]) -> void:
regroup_scene.free() regroup_scene.free()
func _check_wuchao_branch_camp_conversations(failures: Array[String]) -> void:
var base_scene = _new_prebattle_scene_for(failures, "Wuchao base camp", WUCHAO_SCENARIO_ID, WUCHAO_SCENARIO_PATH)
if base_scene != null:
if base_scene._camp_conversation_by_id("wuchao_night_raid_council").is_empty():
failures.append("Wuchao should expose the night raid council conversation")
if base_scene._camp_conversation_by_id("wuchao_guo_jia_empty_bowl").is_empty():
failures.append("Wuchao should expose Guo Jia's officer conversation")
if not base_scene._camp_conversation_by_id("wuchao_fast_raid_supplies").is_empty():
failures.append("Wuchao fast raid supplies should be hidden without the prepared raid flag")
if not base_scene._camp_conversation_by_id("wuchao_guandu_reserve_wagon").is_empty():
failures.append("Wuchao reserve wagon should be hidden without the strengthened defense flag")
var merchant: Dictionary = base_scene.state.get_shop_merchant()
if str(merchant.get("name", "")) != "Wuchao Night Trader":
failures.append("Wuchao shop merchant name should be present")
var merchant_lines: Array = merchant.get("lines", [])
if merchant_lines.size() < 2:
failures.append("Wuchao shop merchant should expose flavor lines")
if base_scene._format_talk_status_text(base_scene._camp_conversation_entries()) != "Camp conversations | 2 topics":
failures.append("Wuchao base talk status should summarize two non-supply topics")
base_scene.free()
var raid_scene = _new_prebattle_scene_for(
failures,
"Wuchao prepared raid camp",
WUCHAO_SCENARIO_ID,
WUCHAO_SCENARIO_PATH,
{"prepared_wuchao_raid": true}
)
if raid_scene != null:
var fast_supplies: Dictionary = raid_scene._camp_conversation_by_id("wuchao_fast_raid_supplies")
if fast_supplies.is_empty():
failures.append("prepared raid flag should expose Wuchao fast supplies")
if not raid_scene._camp_conversation_by_id("wuchao_guandu_reserve_wagon").is_empty():
failures.append("prepared raid flag should not expose Wuchao reserve wagon")
if not fast_supplies.is_empty():
if raid_scene._format_talk_status_text(raid_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready":
failures.append("Wuchao prepared raid talk status should show one ready supply")
if not raid_scene._format_camp_conversation_button_text(fast_supplies).contains("Supply Bean"):
failures.append("Wuchao fast supplies button should show Bean supply")
var before_bean := int(raid_scene.campaign_state.get_inventory_snapshot().get("bean", 0))
raid_scene._apply_camp_conversation_effects(fast_supplies)
if int(raid_scene.campaign_state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("Wuchao fast supplies should add Bean to campaign inventory")
if int(raid_scene.state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("Wuchao fast supplies should refresh battle Bean inventory")
if not raid_scene.campaign_state.has_claimed_camp_conversation_effects(WUCHAO_SCENARIO_ID, "wuchao_fast_raid_supplies"):
failures.append("Wuchao fast supplies claim should be saved")
if raid_scene._format_talk_status_text(raid_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed":
failures.append("Wuchao prepared raid talk status should show claimed supply after claim")
raid_scene.free()
var defense_scene = _new_prebattle_scene_for(
failures,
"Wuchao strengthened defense camp",
WUCHAO_SCENARIO_ID,
WUCHAO_SCENARIO_PATH,
{"strengthened_guandu_defense": true}
)
if defense_scene != null:
var reserve_wagon: Dictionary = defense_scene._camp_conversation_by_id("wuchao_guandu_reserve_wagon")
if reserve_wagon.is_empty():
failures.append("strengthened defense flag should expose Wuchao reserve wagon")
if not defense_scene._camp_conversation_by_id("wuchao_fast_raid_supplies").is_empty():
failures.append("strengthened defense flag should not expose Wuchao fast supplies")
if not reserve_wagon.is_empty():
if defense_scene._format_talk_status_text(defense_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready":
failures.append("Wuchao strengthened defense talk status should show one ready supply")
if not defense_scene._format_camp_conversation_button_text(reserve_wagon).contains("Supply Panacea"):
failures.append("Wuchao reserve wagon button should show Panacea supply")
var before_panacea := int(defense_scene.campaign_state.get_inventory_snapshot().get("panacea", 0))
defense_scene._apply_camp_conversation_effects(reserve_wagon)
if int(defense_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("Wuchao reserve wagon should add Panacea to campaign inventory")
if int(defense_scene.state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("Wuchao reserve wagon should refresh battle Panacea inventory")
if not defense_scene.campaign_state.has_claimed_camp_conversation_effects(WUCHAO_SCENARIO_ID, "wuchao_guandu_reserve_wagon"):
failures.append("Wuchao reserve wagon claim should be saved")
defense_scene.free()
func _new_prebattle_scene(failures: Array[String], label: String): func _new_prebattle_scene(failures: Array[String], label: String):
return _new_prebattle_scene_for(failures, label, SCENARIO_ID, SCENARIO_PATH) return _new_prebattle_scene_for(failures, label, SCENARIO_ID, SCENARIO_PATH)