From 0be2cadb9fe63a7c7d5a79c2cda6f368c72afbe8 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Fri, 19 Jun 2026 01:27:42 +0900 Subject: [PATCH] Style ancient briefing UI and add Ye surrender camp --- data/scenarios/017_ye_surrender.json | 107 +++++++++++++ data/scenarios/018_northern_pursuit.json | 2 +- scripts/scenes/battle_scene.gd | 127 +++++++++++---- tools/smoke_camp_conversation_rewards.gd | 190 +++++++++++++++++++++++ tools/smoke_visual_assets.gd | 113 +++++++++++++- 5 files changed, 500 insertions(+), 39 deletions(-) diff --git a/data/scenarios/017_ye_surrender.json b/data/scenarios/017_ye_surrender.json index 58aea9a..5761779 100644 --- a/data/scenarios/017_ye_surrender.json +++ b/data/scenarios/017_ye_surrender.json @@ -32,9 +32,116 @@ "Negotiations have drawn several Yuan captains away from the palace, leaving the loyalists angry and isolated." ] } + ], + "camp_conversations": [ + { + "id": "ye_surrender_palace_road_council", + "group": "topic", + "label": "Palace Road Council", + "speaker": "Cao Cao", + "summary": "Set the terms for Ye's final surrender signal.", + "lines": [ + { + "speaker": "Cao Cao", + "side": "left", + "text": "The city has opened its teeth. Now we decide whether the mouth speaks surrender or bites until it breaks." + }, + { + "speaker": "Guo Jia", + "side": "right", + "text": "A banner raised on the palace road will be seen by every frightened household. Mercy must arrive with discipline." + }, + { + "speaker": "Zhang He", + "side": "right", + "text": "The loyalists still listen for Yuan orders that will not come. Strike cleanly, and the silence will do half the work." + } + ] + }, + { + "id": "ye_surrender_zhang_he_last_loyalists", + "group": "officer", + "officer_id": "zhang_he", + "label": "Zhang He - Last Loyalists", + "speaker": "Zhang He", + "summary": "Read the palace loyalists before the surrender signal.", + "lines": [ + { + "speaker": "Zhang He", + "side": "right", + "text": "The men ahead are not guarding Ye. They are guarding the last moment when they can call themselves loyal." + }, + { + "speaker": "Cao Cao", + "side": "left", + "text": "Then we leave them only two names: citizen, or corpse." + } + ] + }, + { + "id": "ye_surrender_pressure_rations", + "campaign_flags": { "forced_ye_surrender": true }, + "group": "topic", + "label": "Pressure Rations", + "summary": "Claim compact rations prepared for the direct palace road assault.", + "lines": [ + { + "speaker": "Xiahou Dun", + "side": "right", + "text": "The men who pushed the gate brought forward dry beans. No ceremony, no cart, just food that survives dust." + }, + { + "speaker": "Cao Cao", + "side": "left", + "text": "Good. A forced surrender leaves little time for kitchens." + } + ], + "effects": [ + { + "type": "grant_item", + "item_id": "bean", + "count": 1, + "text": "The pressure rations add a Bean to the field supplies." + } + ] + }, + { + "id": "ye_surrender_defector_medicine", + "campaign_flags": { "negotiated_yuan_defections": true }, + "group": "topic", + "label": "Defector Medicine", + "summary": "Claim a medicine bundle handed over by Yuan defectors.", + "lines": [ + { + "speaker": "Guo Jia", + "side": "left", + "text": "The defectors brought a medicine bundle and the names of two palace guards. One gift heals men, the other opens doors." + }, + { + "speaker": "Cao Ren", + "side": "right", + "text": "The cloth bears a Yuan store mark, but the seal has been cut away." + } + ], + "effects": [ + { + "type": "grant_item", + "item_id": "panacea", + "count": 1, + "text": "The defector medicine adds a Panacea to the field supplies." + } + ] + } ] }, "shop": { + "merchant": { + "name": "Ye Palace Road Sutler", + "lines": [ + "The palace road is bad for banners and excellent for prices.", + "Buy what you need before the surrender bell. After that, every cellar in Ye will claim it was loyal all along." + ] + }, "items": [ "bean", "wine", diff --git a/data/scenarios/018_northern_pursuit.json b/data/scenarios/018_northern_pursuit.json index dace7de..eb0dace 100644 --- a/data/scenarios/018_northern_pursuit.json +++ b/data/scenarios/018_northern_pursuit.json @@ -59,7 +59,7 @@ }, "roster": { "max_units": 7, - "required_officers": ["cao_cao", "guo_jia"] + "required_officers": ["cao_cao", "guo_jia", "zhang_he"] }, "formation": { "cells": [[1, 3], [1, 4], [1, 5], [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [3, 2], [3, 3], [3, 4], [3, 5], [3, 6]] diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 1916b9d..8693aec 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -69,14 +69,17 @@ const POST_MOVE_PICKER_PANEL_SIZE := Vector2(320, 210) const TARGETING_HINT_PANEL_SIZE := Vector2(240, 94) const LOCAL_COMMAND_PANEL_GAP := 8.0 const LOCAL_COMMAND_PANEL_BOARD_PADDING := 4.0 -const UI_RICE_PAPER := Color(0.77, 0.67, 0.48, 0.96) -const UI_RICE_PAPER_DARK := Color(0.48, 0.34, 0.18, 0.96) -const UI_AGED_INK := Color(0.10, 0.065, 0.04, 1.0) -const UI_DARK_LACQUER := Color(0.18, 0.055, 0.035, 0.96) -const UI_CINNABAR := Color(0.48, 0.055, 0.035, 1.0) -const UI_OLD_BRONZE := Color(0.72, 0.52, 0.22, 1.0) -const UI_MUTED_JADE := Color(0.20, 0.36, 0.28, 1.0) -const UI_PARCHMENT_TEXT := Color(0.94, 0.84, 0.62, 1.0) +const UI_RICE_PAPER := Color(0.66, 0.55, 0.36, 0.97) +const UI_RICE_PAPER_LIGHT := Color(0.76, 0.64, 0.43, 0.98) +const UI_RICE_PAPER_DARK := Color(0.34, 0.22, 0.11, 0.98) +const UI_AGED_INK := Color(0.09, 0.055, 0.032, 1.0) +const UI_DARK_LACQUER := Color(0.12, 0.045, 0.025, 0.98) +const UI_LACQUER_EDGE := Color(0.055, 0.025, 0.016, 1.0) +const UI_CINNABAR := Color(0.52, 0.045, 0.025, 1.0) +const UI_CINNABAR_DARK := Color(0.32, 0.025, 0.016, 1.0) +const UI_OLD_BRONZE := Color(0.78, 0.58, 0.27, 1.0) +const UI_MUTED_JADE := Color(0.16, 0.30, 0.23, 1.0) +const UI_PARCHMENT_TEXT := Color(0.95, 0.82, 0.55, 1.0) const UI_DISABLED_TEXT := Color(0.48, 0.40, 0.29, 1.0) var state: BattleState = BattleStateScript.new() @@ -118,6 +121,7 @@ var inventory_label: Label var briefing_panel: PanelContainer var briefing_title_label: Label var briefing_location_label: Label +var briefing_objective_panel: PanelContainer var briefing_objective_label: Label var briefing_camp_overview_row: HBoxContainer var briefing_camp_overview_texture: TextureRect @@ -164,7 +168,9 @@ var dialogue_portrait_panel: PanelContainer var dialogue_portrait_texture: TextureRect var dialogue_portrait_label: Label var dialogue_column: VBoxContainer +var dialogue_speaker_panel: PanelContainer var dialogue_speaker_label: Label +var dialogue_text_panel: PanelContainer var dialogue_text_label: Label var dialogue_progress_label: Label var dialogue_previous_button: Button @@ -304,34 +310,64 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void: return var fill := UI_DARK_LACQUER var border := UI_OLD_BRONZE - var border_width := 2 - var radius := 1 + var border_width := 3 + var radius := 0 var margin := 10 var shadow_size := 6 match variant: "paper": fill = UI_RICE_PAPER border = UI_RICE_PAPER_DARK - margin = 12 + border_width = 3 + margin = 14 + shadow_size = 8 + "edict": + fill = UI_RICE_PAPER_LIGHT + border = UI_AGED_INK + border_width = 3 + margin = 10 + shadow_size = 4 "seal": fill = UI_CINNABAR border = UI_OLD_BRONZE + border_width = 2 + margin = 8 + "speaker_seal": + fill = UI_CINNABAR_DARK + border = UI_OLD_BRONZE + border_width = 2 margin = 8 "portrait": - fill = UI_AGED_INK + fill = UI_LACQUER_EDGE border = UI_OLD_BRONZE + border_width = 3 margin = 3 + "dialogue": + fill = UI_DARK_LACQUER + border = UI_OLD_BRONZE + border_width = 3 + margin = 12 + shadow_size = 10 + "speech_scroll": + fill = UI_RICE_PAPER_LIGHT + border = UI_RICE_PAPER_DARK + border_width = 2 + margin = 12 + shadow_size = 2 "notice": - fill = Color(0.20, 0.09, 0.035, 0.96) + fill = Color(0.18, 0.065, 0.032, 0.97) border = UI_CINNABAR + border_width = 2 margin = 8 "map": - fill = UI_AGED_INK + fill = Color(0.20, 0.12, 0.065, 1.0) border = UI_OLD_BRONZE + border_width = 3 margin = 3 "compact": - fill = Color(0.16, 0.075, 0.04, 0.96) + fill = Color(0.13, 0.065, 0.035, 0.97) border = UI_OLD_BRONZE + border_width = 2 margin = 7 shadow_size = 4 _: @@ -340,7 +376,7 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void: func _make_button_style(fill: Color, border: Color, border_width: int) -> StyleBoxFlat: - return _make_panel_style(fill, border, border_width, 1, 8, 0) + return _make_panel_style(fill, border, border_width, 0, 8, 0) func _apply_button_style(button: Button, important: bool = false) -> void: @@ -378,6 +414,15 @@ func _make_separator(width: float = 0.0, height: float = 2.0) -> ColorRect: return separator +func _make_ornament_bar(width: float = 4.0, height: float = 0.0) -> ColorRect: + var bar := ColorRect.new() + bar.color = UI_CINNABAR + bar.custom_minimum_size = Vector2(width, height) + bar.size_flags_vertical = Control.SIZE_EXPAND_FILL + bar.mouse_filter = Control.MOUSE_FILTER_IGNORE + return bar + + func _create_hud() -> void: var layer := CanvasLayer.new() add_child(layer) @@ -776,16 +821,23 @@ func _create_hud() -> void: briefing_column.add_child(_make_separator(680, 2)) - var briefing_objective_panel := PanelContainer.new() + briefing_objective_panel = PanelContainer.new() briefing_objective_panel.custom_minimum_size = Vector2(680, 92) - _apply_panel_style(briefing_objective_panel, "compact") + _apply_panel_style(briefing_objective_panel, "edict") briefing_column.add_child(briefing_objective_panel) + var briefing_objective_row := HBoxContainer.new() + briefing_objective_row.add_theme_constant_override("separation", 8) + briefing_objective_panel.add_child(briefing_objective_row) + + briefing_objective_row.add_child(_make_ornament_bar(5, 72)) + briefing_objective_label = Label.new() briefing_objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - briefing_objective_label.custom_minimum_size = Vector2(656, 72) - _apply_label_style(briefing_objective_label, UI_PARCHMENT_TEXT, 15) - briefing_objective_panel.add_child(briefing_objective_label) + briefing_objective_label.custom_minimum_size = Vector2(632, 72) + briefing_objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _apply_label_style(briefing_objective_label, UI_AGED_INK, 15) + briefing_objective_row.add_child(briefing_objective_label) briefing_camp_overview_row = HBoxContainer.new() briefing_camp_overview_row.custom_minimum_size = Vector2(680, 84) @@ -1042,11 +1094,11 @@ func _create_hud() -> void: dialogue_panel.position = DIALOGUE_PANEL_POSITION dialogue_panel.size = DIALOGUE_PANEL_SIZE dialogue_panel.mouse_filter = Control.MOUSE_FILTER_STOP - _apply_panel_style(dialogue_panel, "compact") + _apply_panel_style(dialogue_panel, "dialogue") root.add_child(dialogue_panel) dialogue_row = HBoxContainer.new() - dialogue_row.add_theme_constant_override("separation", 18) + dialogue_row.add_theme_constant_override("separation", 14) dialogue_panel.add_child(dialogue_row) dialogue_portrait_panel = PanelContainer.new() @@ -1078,9 +1130,9 @@ func _create_hud() -> void: dialogue_column.add_theme_constant_override("separation", 8) dialogue_row.add_child(dialogue_column) - var dialogue_speaker_panel := PanelContainer.new() + dialogue_speaker_panel = PanelContainer.new() dialogue_speaker_panel.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x, 32) - _apply_panel_style(dialogue_speaker_panel, "seal") + _apply_panel_style(dialogue_speaker_panel, "speaker_seal") dialogue_column.add_child(dialogue_speaker_panel) dialogue_speaker_label = Label.new() @@ -1089,17 +1141,24 @@ func _create_hud() -> void: _apply_label_style(dialogue_speaker_label, UI_PARCHMENT_TEXT, 18) dialogue_speaker_panel.add_child(dialogue_speaker_label) - var dialogue_text_panel := PanelContainer.new() + dialogue_text_panel = PanelContainer.new() dialogue_text_panel.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x, 104) - _apply_panel_style(dialogue_text_panel, "paper") + _apply_panel_style(dialogue_text_panel, "speech_scroll") dialogue_column.add_child(dialogue_text_panel) + var dialogue_text_row := HBoxContainer.new() + dialogue_text_row.add_theme_constant_override("separation", 8) + dialogue_text_panel.add_child(dialogue_text_row) + + dialogue_text_row.add_child(_make_ornament_bar(4, DIALOGUE_TEXT_SIZE.y - 20)) + dialogue_text_label = Label.new() dialogue_text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - dialogue_text_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x - 24, DIALOGUE_TEXT_SIZE.y - 20) + dialogue_text_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x - 42, DIALOGUE_TEXT_SIZE.y - 20) + dialogue_text_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL dialogue_text_label.size_flags_vertical = Control.SIZE_EXPAND_FILL _apply_label_style(dialogue_text_label, UI_AGED_INK, 17) - dialogue_text_panel.add_child(dialogue_text_label) + dialogue_text_row.add_child(dialogue_text_label) var dialogue_control_row := HBoxContainer.new() dialogue_control_row.add_theme_constant_override("separation", 8) @@ -1113,14 +1172,14 @@ func _create_hud() -> void: dialogue_control_row.add_child(dialogue_progress_label) dialogue_previous_button = Button.new() - dialogue_previous_button.text = "Prev" + dialogue_previous_button.text = "Prior" dialogue_previous_button.disabled = true dialogue_previous_button.custom_minimum_size = Vector2(88, 32) dialogue_previous_button.pressed.connect(_retreat_dialogue) dialogue_control_row.add_child(dialogue_previous_button) dialogue_continue_button = Button.new() - dialogue_continue_button.text = "Next" + dialogue_continue_button.text = "Continue" dialogue_continue_button.custom_minimum_size = Vector2(120, 32) dialogue_continue_button.pressed.connect(_advance_dialogue) dialogue_control_row.add_child(dialogue_continue_button) @@ -3829,10 +3888,10 @@ func _render_dialogue_line() -> void: func _update_dialogue_controls() -> void: if dialogue_progress_label != null: - dialogue_progress_label.text = "%d / %d" % [active_dialogue_index + 1, active_dialogue_lines.size()] + dialogue_progress_label.text = "Scroll %d / %d" % [active_dialogue_index + 1, active_dialogue_lines.size()] if dialogue_previous_button != null: dialogue_previous_button.disabled = active_dialogue_index <= 0 - dialogue_continue_button.text = "Close" if active_dialogue_index >= active_dialogue_lines.size() - 1 else "Next" + dialogue_continue_button.text = "Close" if active_dialogue_index >= active_dialogue_lines.size() - 1 else "Continue" func _apply_dialogue_side(side: String) -> void: @@ -4266,7 +4325,7 @@ func _update_briefing_camp_overview(briefing: Dictionary, prep_locked: bool) -> briefing_camp_overview_texture.texture = texture briefing_camp_overview_texture.visible = has_texture if briefing_camp_overview_fallback_label != null: - briefing_camp_overview_fallback_label.text = "Camp" + briefing_camp_overview_fallback_label.text = "War Map" briefing_camp_overview_fallback_label.visible = not has_texture diff --git a/tools/smoke_camp_conversation_rewards.gd b/tools/smoke_camp_conversation_rewards.gd index 43ccab6..a143d15 100644 --- a/tools/smoke_camp_conversation_rewards.gd +++ b/tools/smoke_camp_conversation_rewards.gd @@ -62,6 +62,10 @@ const YE_SIEGE_PRESSED_CONVERSATION_ID := "ye_siege_ladder_rations" const YE_SIEGE_SPLIT_CONVERSATION_ID := "ye_siege_defector_medicine" const YE_SURRENDER_SCENARIO_ID := "017_ye_surrender" const YE_SURRENDER_SCENARIO_PATH := "res://data/scenarios/017_ye_surrender.json" +const YE_SURRENDER_PRESSURE_CONVERSATION_ID := "ye_surrender_pressure_rations" +const YE_SURRENDER_DEFECTOR_CONVERSATION_ID := "ye_surrender_defector_medicine" +const NORTHERN_PURSUIT_SCENARIO_ID := "018_northern_pursuit" +const NORTHERN_PURSUIT_SCENARIO_PATH := "res://data/scenarios/018_northern_pursuit.json" const SAVE_PATH := "user://campaign_save.json" const MANUAL_SAVE_PATH := "user://campaign_manual_save.json" @@ -87,6 +91,7 @@ func _init() -> void: _check_cangting_post_battle_choice_bridges_to_ye(failures) _check_ye_post_battle_choice_bridges_to_ye_siege(failures) _check_ye_siege_post_battle_choice_bridges_to_surrender(failures) + _check_ye_surrender_post_battle_choice_bridges_to_northern_pursuit(failures) _check_manual_checkpoint_reverts_claim(failures) _check_completed_replay_cannot_claim(failures) _check_conditional_camp_conversations(failures) @@ -95,6 +100,7 @@ func _init() -> void: _check_cangting_branch_camp_conversations(failures) _check_ye_branch_camp_conversations(failures) _check_ye_siege_branch_camp_conversations(failures) + _check_ye_surrender_branch_camp_conversations(failures) _check_talk_menu_closes_on_cancel_and_reload(failures) if not _restore_user_file(SAVE_PATH, save_backup): @@ -1028,6 +1034,8 @@ func _check_ye_siege_post_battle_choice_bridges_to_surrender(failures: Array[Str "force_ye_surrender", "forced_ye_surrender", "negotiated_yuan_defections", + YE_SURRENDER_PRESSURE_CONVERSATION_ID, + YE_SURRENDER_DEFECTOR_CONVERSATION_ID, "war_drum", "imperial_seal" ) @@ -1036,6 +1044,27 @@ func _check_ye_siege_post_battle_choice_bridges_to_surrender(failures: Array[Str "negotiate_yuan_defections", "negotiated_yuan_defections", "forced_ye_surrender", + YE_SURRENDER_DEFECTOR_CONVERSATION_ID, + YE_SURRENDER_PRESSURE_CONVERSATION_ID, + "imperial_seal", + "war_drum" + ) + + +func _check_ye_surrender_post_battle_choice_bridges_to_northern_pursuit(failures: Array[String]) -> void: + _check_ye_surrender_choice_branch_to_northern_pursuit( + failures, + "pursue_yuan_shang", + "pursued_yuan_shang", + "stabilized_ye", + "war_drum", + "imperial_seal" + ) + _check_ye_surrender_choice_branch_to_northern_pursuit( + failures, + "stabilize_ye", + "stabilized_ye", + "pursued_yuan_shang", "imperial_seal", "war_drum" ) @@ -1253,6 +1282,8 @@ func _check_ye_siege_choice_branch_to_surrender( choice_id: String, expected_flag: String, cleared_flag: String, + expected_conversation_id: String, + hidden_conversation_id: String, expected_shop_item_id: String, hidden_shop_item_id: String ) -> void: @@ -1313,6 +1344,10 @@ func _check_ye_siege_choice_branch_to_surrender( failures.append("Ye Siege choice bridge %s should deploy Zhang He in Ye Surrender" % choice_id) elif not scene.state.is_required_deployment("zhang_he_ch17"): failures.append("Ye Siege choice bridge %s should require Zhang He in Ye Surrender" % choice_id) + if scene._camp_conversation_by_id(expected_conversation_id).is_empty(): + failures.append("Ye Siege 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("Ye Siege choice bridge %s should hide %s" % [choice_id, hidden_conversation_id]) var shop_items: Array = scene.state.get_shop_item_ids() if not shop_items.has(expected_shop_item_id): failures.append("Ye Siege choice bridge %s should expose shop item %s" % [choice_id, expected_shop_item_id]) @@ -1321,6 +1356,79 @@ func _check_ye_siege_choice_branch_to_surrender( scene.free() +func _check_ye_surrender_choice_branch_to_northern_pursuit( + 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 Surrender choice bridge %s" % choice_id, YE_SURRENDER_SCENARIO_ID, YE_SURRENDER_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_SURRENDER_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 Surrender choice bridge %s could not reload Ye Surrender 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 Surrender choice bridge %s should save 017 victory" % choice_id) + scene.free() + return + if not scene.campaign_state.get_joined_officers_snapshot().has("zhang_he"): + failures.append("Ye Surrender choice bridge %s should keep Zhang He before Northern Pursuit" % choice_id) + if bool(result.get("choice_applied", true)): + failures.append("Ye Surrender choice bridge %s should wait for a selected post-battle choice" % choice_id) + if scene.campaign_state.pending_post_battle_choice_scenario_id != YE_SURRENDER_SCENARIO_ID: + failures.append("Ye Surrender choice bridge %s should mark Ye Surrender choice pending" % choice_id) + var choice := _find_choice_by_id(result.get("post_battle_choices", []), choice_id) + if choice.is_empty(): + failures.append("Ye Surrender choice bridge missing choice: %s" % choice_id) + scene.free() + return + if not scene.campaign_state.try_apply_post_battle_choice(choice, YE_SURRENDER_SCENARIO_ID): + failures.append("Ye Surrender 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 Surrender choice bridge %s should set %s" % [choice_id, expected_flag]) + if flags.get(cleared_flag, true) != false: + failures.append("Ye Surrender choice bridge %s should clear %s" % [choice_id, cleared_flag]) + if scene.campaign_state.current_scenario_id != NORTHERN_PURSUIT_SCENARIO_ID: + failures.append("Ye Surrender choice bridge %s should advance to Northern Pursuit, got %s" % [choice_id, scene.campaign_state.current_scenario_id]) + if not scene.state.load_battle( + NORTHERN_PURSUIT_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 Surrender choice bridge %s could not load Northern Pursuit" % choice_id) + scene.free() + return + if scene.state.get_unit("zhang_he_ch18").is_empty(): + failures.append("Ye Surrender choice bridge %s should deploy Zhang He in Northern Pursuit" % choice_id) + elif not scene.state.is_required_deployment("zhang_he_ch18"): + failures.append("Ye Surrender choice bridge %s should require Zhang He in Northern Pursuit" % choice_id) + var shop_items: Array = scene.state.get_shop_item_ids() + if not shop_items.has(expected_shop_item_id): + failures.append("Ye Surrender 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 Surrender 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: if typeof(choices) != TYPE_ARRAY: return {} @@ -1832,6 +1940,88 @@ func _check_ye_siege_branch_camp_conversations(failures: Array[String]) -> void: split_scene.free() +func _check_ye_surrender_branch_camp_conversations(failures: Array[String]) -> void: + var base_scene = _new_prebattle_scene_for(failures, "Ye Surrender base camp", YE_SURRENDER_SCENARIO_ID, YE_SURRENDER_SCENARIO_PATH) + if base_scene != null: + if base_scene._camp_conversation_by_id("ye_surrender_palace_road_council").is_empty(): + failures.append("Ye Surrender should expose the palace road council conversation") + if base_scene._camp_conversation_by_id("ye_surrender_zhang_he_last_loyalists").is_empty(): + failures.append("Ye Surrender should expose Zhang He's last loyalists conversation") + if not base_scene._camp_conversation_by_id(YE_SURRENDER_PRESSURE_CONVERSATION_ID).is_empty(): + failures.append("Ye Surrender pressure rations should be hidden without forced surrender flag") + if not base_scene._camp_conversation_by_id(YE_SURRENDER_DEFECTOR_CONVERSATION_ID).is_empty(): + failures.append("Ye Surrender defector medicine should be hidden without negotiated defections flag") + var merchant: Dictionary = base_scene.state.get_shop_merchant() + if str(merchant.get("name", "")) != "Ye Palace Road Sutler": + failures.append("Ye Surrender shop merchant name should be present") + var merchant_lines: Array = merchant.get("lines", []) + if merchant_lines.size() < 2: + failures.append("Ye Surrender 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 Surrender base talk status should summarize two non-supply topics") + base_scene.free() + + var forced_scene = _new_prebattle_scene_for( + failures, + "Ye Surrender forced camp", + YE_SURRENDER_SCENARIO_ID, + YE_SURRENDER_SCENARIO_PATH, + {"forced_ye_surrender": true} + ) + if forced_scene != null: + var pressure_rations: Dictionary = forced_scene._camp_conversation_by_id(YE_SURRENDER_PRESSURE_CONVERSATION_ID) + if pressure_rations.is_empty(): + failures.append("forced Ye surrender flag should expose pressure rations") + if not forced_scene._camp_conversation_by_id(YE_SURRENDER_DEFECTOR_CONVERSATION_ID).is_empty(): + failures.append("forced Ye surrender flag should not expose defector medicine") + if not pressure_rations.is_empty(): + if forced_scene._format_talk_status_text(forced_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready": + failures.append("Ye Surrender forced talk status should show one ready supply") + if not forced_scene._format_camp_conversation_button_text(pressure_rations).contains("Supply Bean"): + failures.append("Ye Surrender pressure rations button should show Bean supply") + var before_bean := int(forced_scene.campaign_state.get_inventory_snapshot().get("bean", 0)) + forced_scene._apply_camp_conversation_effects(pressure_rations) + if int(forced_scene.campaign_state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1: + failures.append("Ye Surrender pressure rations should add Bean to campaign inventory") + if int(forced_scene.state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1: + failures.append("Ye Surrender pressure rations should refresh battle Bean inventory") + if not forced_scene.campaign_state.has_claimed_camp_conversation_effects(YE_SURRENDER_SCENARIO_ID, YE_SURRENDER_PRESSURE_CONVERSATION_ID): + failures.append("Ye Surrender pressure rations claim should be saved") + if forced_scene._format_talk_status_text(forced_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed": + failures.append("Ye Surrender forced talk status should show claimed supply after claim") + forced_scene.free() + + var negotiated_scene = _new_prebattle_scene_for( + failures, + "Ye Surrender negotiated camp", + YE_SURRENDER_SCENARIO_ID, + YE_SURRENDER_SCENARIO_PATH, + {"negotiated_yuan_defections": true} + ) + if negotiated_scene != null: + var defector_medicine: Dictionary = negotiated_scene._camp_conversation_by_id(YE_SURRENDER_DEFECTOR_CONVERSATION_ID) + if defector_medicine.is_empty(): + failures.append("negotiated Yuan defections flag should expose Ye Surrender defector medicine") + if not negotiated_scene._camp_conversation_by_id(YE_SURRENDER_PRESSURE_CONVERSATION_ID).is_empty(): + failures.append("negotiated Yuan defections flag should not expose pressure rations") + if not defector_medicine.is_empty(): + if negotiated_scene._format_talk_status_text(negotiated_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready": + failures.append("Ye Surrender negotiated talk status should show one ready supply") + if not negotiated_scene._format_camp_conversation_button_text(defector_medicine).contains("Supply Panacea"): + failures.append("Ye Surrender defector medicine button should show Panacea supply") + var before_panacea := int(negotiated_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) + negotiated_scene._apply_camp_conversation_effects(defector_medicine) + if int(negotiated_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1: + failures.append("Ye Surrender defector medicine should add Panacea to campaign inventory") + if int(negotiated_scene.state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1: + failures.append("Ye Surrender defector medicine should refresh battle Panacea inventory") + if not negotiated_scene.campaign_state.has_claimed_camp_conversation_effects(YE_SURRENDER_SCENARIO_ID, YE_SURRENDER_DEFECTOR_CONVERSATION_ID): + failures.append("Ye Surrender defector medicine claim should be saved") + if negotiated_scene._format_talk_status_text(negotiated_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed": + failures.append("Ye Surrender negotiated talk status should show claimed supply after claim") + negotiated_scene.free() + + func _check_talk_menu_closes_on_cancel_and_reload(failures: Array[String]) -> void: var scene = _new_prebattle_scene(failures, "talk menu close guards") if scene == null: diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 3e6bb29..e7d13e2 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -949,6 +949,85 @@ func _check_battle_visual_data(failures: Array[String]) -> void: ).is_empty(): failures.append("016 old Ye flags should not expose defector medicine") + var ye_surrender_state = BattleStateScript.new() + if not ye_surrender_state.load_battle("res://data/scenarios/017_ye_surrender.json"): + failures.append("could not load Ye Surrender camp data") + else: + _check_shop_items_unique(failures, ye_surrender_state, "017 base") + var ye_surrender_conversations: Array = ye_surrender_state.get_briefing().get("camp_conversations", []) + if ye_surrender_conversations.size() != 2: + failures.append("017 base briefing should expose exactly two camp conversations") + else: + _check_camp_conversation(failures, ye_surrender_conversations[0], "ye_surrender_palace_road_council", "topic", "") + _check_camp_conversation(failures, ye_surrender_conversations[1], "ye_surrender_zhang_he_last_loyalists", "officer", "zhang_he") + var ye_surrender_merchant := ye_surrender_state.get_shop_merchant() + if str(ye_surrender_merchant.get("name", "")) != "Ye Palace Road Sutler": + failures.append("017 shop should expose Ye Palace Road merchant name") + if (ye_surrender_merchant.get("lines", []) as Array).size() < 2: + failures.append("017 shop should expose merchant flavor lines") + _check_shop_item_visibility(failures, ye_surrender_state, "war_drum", false, "017 base") + _check_shop_item_visibility(failures, ye_surrender_state, "imperial_seal", false, "017 base") + _check_shop_item_visibility(failures, ye_surrender_state, "iron_armor", true, "017 base") + + var ye_surrender_forced_state = BattleStateScript.new() + if not ye_surrender_forced_state.load_battle("res://data/scenarios/017_ye_surrender.json", {}, {}, {"forced_ye_surrender": true}): + failures.append("could not load Ye Surrender forced camp data") + else: + _check_shop_items_unique(failures, ye_surrender_forced_state, "017 forced Ye surrender") + var pressure_rations: Dictionary = _find_camp_conversation( + ye_surrender_forced_state.get_briefing().get("camp_conversations", []), + "ye_surrender_pressure_rations" + ) + if pressure_rations.is_empty(): + failures.append("017 forced Ye surrender flag should expose pressure rations") + else: + _check_camp_conversation_effect(failures, pressure_rations, "bean", 1) + if not _find_camp_conversation( + ye_surrender_forced_state.get_briefing().get("camp_conversations", []), + "ye_surrender_defector_medicine" + ).is_empty(): + failures.append("017 forced Ye surrender flag should not expose defector medicine") + _check_shop_item_visibility(failures, ye_surrender_forced_state, "war_drum", true, "017 forced Ye surrender") + _check_shop_item_visibility(failures, ye_surrender_forced_state, "imperial_seal", false, "017 forced Ye surrender") + + var ye_surrender_negotiated_state = BattleStateScript.new() + if not ye_surrender_negotiated_state.load_battle("res://data/scenarios/017_ye_surrender.json", {}, {}, {"negotiated_yuan_defections": true}): + failures.append("could not load Ye Surrender negotiated camp data") + else: + _check_shop_items_unique(failures, ye_surrender_negotiated_state, "017 negotiated Yuan defections") + var surrender_defector_medicine: Dictionary = _find_camp_conversation( + ye_surrender_negotiated_state.get_briefing().get("camp_conversations", []), + "ye_surrender_defector_medicine" + ) + if surrender_defector_medicine.is_empty(): + failures.append("017 negotiated Yuan defections flag should expose defector medicine") + else: + _check_camp_conversation_effect(failures, surrender_defector_medicine, "panacea", 1) + if not _find_camp_conversation( + ye_surrender_negotiated_state.get_briefing().get("camp_conversations", []), + "ye_surrender_pressure_rations" + ).is_empty(): + failures.append("017 negotiated Yuan defections flag should not expose pressure rations") + _check_shop_item_visibility(failures, ye_surrender_negotiated_state, "war_drum", false, "017 negotiated Yuan defections") + _check_shop_item_visibility(failures, ye_surrender_negotiated_state, "imperial_seal", true, "017 negotiated Yuan defections") + + var ye_surrender_old_flags_state = BattleStateScript.new() + if not ye_surrender_old_flags_state.load_battle("res://data/scenarios/017_ye_surrender.json", {}, {}, {"pressed_ye_siege": true, "split_yuan_heirs": true}): + failures.append("could not load Ye Surrender old Ye Siege flag camp data") + else: + _check_shop_item_visibility(failures, ye_surrender_old_flags_state, "war_drum", false, "017 old Ye Siege flags") + _check_shop_item_visibility(failures, ye_surrender_old_flags_state, "imperial_seal", false, "017 old Ye Siege flags") + if not _find_camp_conversation( + ye_surrender_old_flags_state.get_briefing().get("camp_conversations", []), + "ye_surrender_pressure_rations" + ).is_empty(): + failures.append("017 old Ye Siege flags should not expose pressure rations") + if not _find_camp_conversation( + ye_surrender_old_flags_state.get_briefing().get("camp_conversations", []), + "ye_surrender_defector_medicine" + ).is_empty(): + failures.append("017 old Ye Siege flags should not expose defector medicine") + 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(): @@ -1124,10 +1203,20 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: for expected in ["War Edict:", "Ruin If:", "War Report:", "Ill Omen:", "Spoils:"]: if not briefing_text.contains(expected): failures.append("briefing objective parchment should use ancient heading `%s`: %s" % [expected, briefing_text]) - _check_panel_style_fill(failures, scene.briefing_panel, "briefing panel", Color(0.77, 0.67, 0.48, 0.96)) - _check_panel_style_fill(failures, scene.dialogue_panel, "dialogue panel", Color(0.16, 0.075, 0.04, 0.96)) - _check_panel_style_fill(failures, scene.dialogue_portrait_panel, "dialogue portrait panel", Color(0.10, 0.065, 0.04, 1.0)) - _check_panel_style_fill(failures, scene.result_panel, "result panel", Color(0.77, 0.67, 0.48, 0.96)) + _check_panel_style_fill(failures, scene.briefing_panel, "briefing panel", Color(0.66, 0.55, 0.36, 0.97)) + _check_panel_style_frame(failures, scene.briefing_panel, "briefing panel", Color(0.34, 0.22, 0.11, 0.98), 3) + _check_panel_style_fill(failures, scene.briefing_objective_panel, "briefing objective edict panel", Color(0.76, 0.64, 0.43, 0.98)) + _check_panel_style_frame(failures, scene.briefing_objective_panel, "briefing objective edict panel", Color(0.09, 0.055, 0.032, 1.0), 3) + _check_panel_style_fill(failures, scene.dialogue_panel, "dialogue panel", Color(0.12, 0.045, 0.025, 0.98)) + _check_panel_style_frame(failures, scene.dialogue_panel, "dialogue panel", Color(0.78, 0.58, 0.27, 1.0), 3) + _check_panel_style_fill(failures, scene.dialogue_portrait_panel, "dialogue portrait panel", Color(0.055, 0.025, 0.016, 1.0)) + _check_panel_style_fill(failures, scene.dialogue_speaker_panel, "dialogue speaker seal panel", Color(0.32, 0.025, 0.016, 1.0)) + _check_panel_style_fill(failures, scene.dialogue_text_panel, "dialogue text scroll panel", Color(0.76, 0.64, 0.43, 0.98)) + if scene.dialogue_continue_button == null or scene.dialogue_continue_button.text != "Continue": + failures.append("dialogue continue button should use ancient continue text") + if scene.dialogue_previous_button == null or scene.dialogue_previous_button.text != "Prior": + failures.append("dialogue previous button should use ancient prior text") + _check_panel_style_fill(failures, scene.result_panel, "result panel", Color(0.66, 0.55, 0.36, 0.97)) scene.free() @@ -1143,6 +1232,22 @@ func _check_panel_style_fill(failures: Array[String], panel: PanelContainer, lab failures.append("%s fill mismatch: %s" % [label, str(style.bg_color)]) +func _check_panel_style_frame(failures: Array[String], panel: PanelContainer, label: String, expected_border: Color, expected_width: int) -> void: + if panel == null: + failures.append("%s missing" % label) + return + var style := panel.get_theme_stylebox("panel") as StyleBoxFlat + if style == null: + failures.append("%s should use a flat styled panel" % label) + return + if not _colors_nearly_equal(style.border_color, expected_border): + failures.append("%s border mismatch: %s" % [label, str(style.border_color)]) + if style.get_border_width(SIDE_LEFT) != expected_width: + failures.append("%s border width mismatch: %d" % [label, style.get_border_width(SIDE_LEFT)]) + if style.get_corner_radius(CORNER_TOP_LEFT) != 0: + failures.append("%s should use square old-frame corners" % label) + + func _colors_nearly_equal(left: Color, right: Color) -> bool: return ( absf(left.r - right.r) < 0.01