From dfe410b3c0246f9daa00b8d78b44238680f4c22f Mon Sep 17 00:00:00 2001 From: Wickedness Date: Thu, 18 Jun 2026 20:26:01 +0900 Subject: [PATCH] Add Wuchao camp conversations --- data/scenarios/013_wuchao_raid.json | 61 +++++++++++++++++ scripts/scenes/battle_scene.gd | 31 ++++++++- tools/smoke_camp_conversation_rewards.gd | 83 ++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) diff --git a/data/scenarios/013_wuchao_raid.json b/data/scenarios/013_wuchao_raid.json index 2944381..fd69e73 100644 --- a/data/scenarios/013_wuchao_raid.json +++ b/data/scenarios/013_wuchao_raid.json @@ -32,9 +32,70 @@ "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": { + "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": [ "bean", "wine", diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index f3a754b..be7e2d0 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -3541,7 +3541,7 @@ func _rebuild_talk_menu() -> void: _clear_talk_list() var conversations := _camp_conversation_entries() 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(): var empty_label := Label.new() empty_label.text = "No camp conversations" @@ -3555,6 +3555,35 @@ func _rebuild_talk_menu() -> void: 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: for child in talk_list.get_children(): talk_list.remove_child(child) diff --git a/tools/smoke_camp_conversation_rewards.gd b/tools/smoke_camp_conversation_rewards.gd index 140149a..27f440d 100644 --- a/tools/smoke_camp_conversation_rewards.gd +++ b/tools/smoke_camp_conversation_rewards.gd @@ -8,6 +8,8 @@ const SCENARIO_PATH := "res://data/scenarios/001_yellow_turbans.json" const CONVERSATION_ID := "northern_woods_cache" const BRANCH_SCENARIO_ID := "003_xingyang_ambush" 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 MANUAL_SAVE_PATH := "user://campaign_manual_save.json" @@ -22,6 +24,7 @@ func _init() -> void: _check_manual_checkpoint_reverts_claim(failures) _check_completed_replay_cannot_claim(failures) _check_conditional_camp_conversations(failures) + _check_wuchao_branch_camp_conversations(failures) if not _restore_user_file(SAVE_PATH, save_backup): failures.append("could not restore automatic campaign save") @@ -197,6 +200,86 @@ func _check_conditional_camp_conversations(failures: Array[String]) -> void: 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): return _new_prebattle_scene_for(failures, label, SCENARIO_ID, SCENARIO_PATH)