Style ancient briefing UI and add Ye surrender camp

This commit is contained in:
2026-06-19 01:27:42 +09:00
parent cc2eecd7ff
commit 0be2cadb9f
5 changed files with 500 additions and 39 deletions

View File

@@ -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",

View File

@@ -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]]

View File

@@ -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

View File

@@ -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:

View File

@@ -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