Deepen ancient Nanpi briefing flow

This commit is contained in:
2026-06-19 02:22:01 +09:00
parent 1911038d70
commit bda8d02c3c
5 changed files with 388 additions and 33 deletions

View File

@@ -32,9 +32,70 @@
"Secured Ji towns keep Cao Cao's rear steady, letting the army arrive at Nanpi with fresh supplies and fewer alarms behind it." "Secured Ji towns keep Cao Cao's rear steady, letting the army arrive at Nanpi with fresh supplies and fewer alarms behind it."
] ]
} }
],
"camp_conversations": [
{
"id": "nanpi_pressure_gate_council",
"group": "topic",
"label": "Nanpi Gate Council",
"speaker": "Cao Cao",
"summary": "Set the first order before Yuan Tan's outer line can steady itself.",
"lines": [
{ "speaker": "Cao Cao", "side": "left", "text": "Nanpi's wall is not the first gate. The first gate is fear, and Yuan Tan has already locked himself behind it." },
{ "speaker": "Guo Jia", "side": "right", "text": "Then the army should strike the outer line as if issuing a decree. Every captain must know surrender has a shape." },
{ "speaker": "Zhang He", "side": "right", "text": "Yuan Tan's men still know my banner. If I ride close, the doubtful ones will count their exits before their arrows." }
]
},
{
"id": "nanpi_pressure_zhang_he_gate_names",
"group": "officer",
"officer_id": "zhang_he",
"label": "Zhang He - Gate Names",
"speaker": "Zhang He",
"summary": "Identify which Nanpi captains are most likely to break.",
"lines": [
{ "speaker": "Zhang He", "side": "right", "text": "The outer guard obeys Yuan Tan because the city is behind him. The gate captain obeys whoever can keep the city alive." },
{ "speaker": "Cao Cao", "side": "left", "text": "Then we let the gate captain see life on our side and ruin on his." }
]
},
{
"id": "nanpi_pressure_hard_push_beans",
"campaign_flags": { "pressed_north_to_nanpi": true },
"group": "topic",
"label": "Hard Push Beans",
"summary": "Claim compact rations from the columns that arrived before Nanpi expected them.",
"lines": [
{ "speaker": "Xiahou Dun", "side": "right", "text": "The hard push left beans in every saddle bag and dust in every throat. The men can still chew both." },
{ "speaker": "Guo Jia", "side": "left", "text": "A quick arrival is only useful if the army still has breath to make it law." }
],
"effects": [
{ "type": "grant_item", "item_id": "bean", "count": 1, "text": "The hard push beans add a Bean to the field supplies." }
]
},
{
"id": "nanpi_pressure_admin_medicine",
"campaign_flags": { "secured_ji_province": true },
"group": "topic",
"label": "Admin Medicine",
"summary": "Claim medicine delivered through the steadied Ji Province offices.",
"lines": [
{ "speaker": "Cao Ren", "side": "right", "text": "Ji clerks sent medicine under command seals. No shouting, no heroics, just carts that arrived." },
{ "speaker": "Cao Cao", "side": "left", "text": "Quiet order wins more gates than loud courage." }
],
"effects": [
{ "type": "grant_item", "item_id": "panacea", "count": 1, "text": "The Nanpi administration medicine adds a Panacea to the field supplies." }
]
}
] ]
}, },
"shop": { "shop": {
"merchant": {
"name": "Nanpi Gate Sutler",
"lines": [
"Nanpi sells fear at the gate and grain behind it. I only trade in the useful half.",
"Buy before Yuan Tan changes his mind, my lord. Surrender makes prices polite and roads crowded."
]
},
"items": [ "items": [
"bean", "bean",
"wine", "wine",

View File

@@ -59,7 +59,7 @@
}, },
"roster": { "roster": {
"max_units": 7, "max_units": 7,
"required_officers": ["cao_cao", "guo_jia"] "required_officers": ["cao_cao", "guo_jia", "zhang_he"]
}, },
"formation": { "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]] "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

@@ -53,10 +53,10 @@ const UNIT_ARROW_ATTACK_DURATION := 0.36
const UNIT_COMMAND_ATTACK_DURATION := 0.38 const UNIT_COMMAND_ATTACK_DURATION := 0.38
const UNIT_HEAVY_ATTACK_DURATION := 0.34 const UNIT_HEAVY_ATTACK_DURATION := 0.34
const UNIT_TACTIC_CAST_DURATION := 0.42 const UNIT_TACTIC_CAST_DURATION := 0.42
const DIALOGUE_PANEL_POSITION := Vector2(96, 448) const DIALOGUE_PANEL_POSITION := Vector2(96, 424)
const DIALOGUE_PANEL_SIZE := Vector2(1040, 210) const DIALOGUE_PANEL_SIZE := Vector2(1040, 236)
const DIALOGUE_PORTRAIT_SIZE := Vector2(164, 186) const DIALOGUE_PORTRAIT_SIZE := Vector2(164, 208)
const DIALOGUE_COLUMN_SIZE := Vector2(820, 186) const DIALOGUE_COLUMN_SIZE := Vector2(820, 208)
const DIALOGUE_TEXT_SIZE := Vector2(800, 104) const DIALOGUE_TEXT_SIZE := Vector2(800, 104)
const BRIEFING_CAMP_THUMBNAIL_SIZE := Vector2(138, 72) const BRIEFING_CAMP_THUMBNAIL_SIZE := Vector2(138, 72)
const CAMP_TALK_PORTRAIT_SIZE := Vector2(58, 58) const CAMP_TALK_PORTRAIT_SIZE := Vector2(58, 58)
@@ -78,6 +78,8 @@ 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 := Color(0.52, 0.045, 0.025, 1.0)
const UI_CINNABAR_DARK := Color(0.32, 0.025, 0.016, 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_OLD_BRONZE := Color(0.78, 0.58, 0.27, 1.0)
const UI_BAMBOO := Color(0.43, 0.27, 0.10, 1.0)
const UI_BAMBOO_DARK := Color(0.20, 0.11, 0.045, 1.0)
const UI_MUTED_JADE := Color(0.16, 0.30, 0.23, 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_PARCHMENT_TEXT := Color(0.95, 0.82, 0.55, 1.0)
const UI_DISABLED_TEXT := Color(0.48, 0.40, 0.29, 1.0) const UI_DISABLED_TEXT := Color(0.48, 0.40, 0.29, 1.0)
@@ -316,17 +318,17 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void:
var shadow_size := 6 var shadow_size := 6
match variant: match variant:
"paper": "paper":
fill = UI_RICE_PAPER fill = Color(0.58, 0.47, 0.29, 0.985)
border = UI_RICE_PAPER_DARK
border_width = 3
margin = 14
shadow_size = 8
"edict":
fill = UI_RICE_PAPER_LIGHT
border = UI_AGED_INK border = UI_AGED_INK
border_width = 4
margin = 16
shadow_size = 10
"edict":
fill = Color(0.73, 0.60, 0.38, 0.99)
border = UI_CINNABAR_DARK
border_width = 3 border_width = 3
margin = 10 margin = 12
shadow_size = 4 shadow_size = 5
"seal": "seal":
fill = UI_CINNABAR fill = UI_CINNABAR
border = UI_OLD_BRONZE border = UI_OLD_BRONZE
@@ -343,15 +345,15 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void:
border_width = 3 border_width = 3
margin = 3 margin = 3
"dialogue": "dialogue":
fill = UI_DARK_LACQUER fill = Color(0.075, 0.028, 0.017, 0.99)
border = UI_OLD_BRONZE border = UI_OLD_BRONZE
border_width = 3 border_width = 4
margin = 12 margin = 14
shadow_size = 10 shadow_size = 12
"speech_scroll": "speech_scroll":
fill = UI_RICE_PAPER_LIGHT fill = Color(0.70, 0.58, 0.36, 0.99)
border = UI_RICE_PAPER_DARK border = UI_AGED_INK
border_width = 2 border_width = 3
margin = 12 margin = 12
shadow_size = 2 shadow_size = 2
"notice": "notice":
@@ -360,8 +362,8 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void:
border_width = 2 border_width = 2
margin = 8 margin = 8
"map": "map":
fill = Color(0.20, 0.12, 0.065, 1.0) fill = Color(0.12, 0.16, 0.11, 1.0)
border = UI_OLD_BRONZE border = UI_BAMBOO
border_width = 3 border_width = 3
margin = 3 margin = 3
"compact": "compact":
@@ -414,6 +416,31 @@ func _make_separator(width: float = 0.0, height: float = 2.0) -> ColorRect:
return separator return separator
func _make_bamboo_divider(width: float = 0.0, height: float = 4.0) -> HBoxContainer:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(width, height)
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_theme_constant_override("separation", 2)
for index in range(3):
var strip := ColorRect.new()
strip.mouse_filter = Control.MOUSE_FILTER_IGNORE
strip.custom_minimum_size = Vector2(8 if index != 1 else width, height)
strip.size_flags_horizontal = Control.SIZE_EXPAND_FILL if index == 1 else Control.SIZE_SHRINK_CENTER
strip.color = UI_BAMBOO if index == 1 else UI_BAMBOO_DARK
row.add_child(strip)
return row
func _make_section_caption(text: String, width: float = 0.0) -> Label:
var label := Label.new()
label.text = text
label.custom_minimum_size = Vector2(width, 20)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_apply_label_style(label, UI_AGED_INK, 13)
return label
func _make_ornament_bar(width: float = 4.0, height: float = 0.0) -> ColorRect: func _make_ornament_bar(width: float = 4.0, height: float = 0.0) -> ColorRect:
var bar := ColorRect.new() var bar := ColorRect.new()
bar.color = UI_CINNABAR bar.color = UI_CINNABAR
@@ -819,7 +846,8 @@ func _create_hud() -> void:
_apply_label_style(briefing_location_label, UI_MUTED_JADE, 14) _apply_label_style(briefing_location_label, UI_MUTED_JADE, 14)
briefing_column.add_child(briefing_location_label) briefing_column.add_child(briefing_location_label)
briefing_column.add_child(_make_separator(680, 2)) briefing_column.add_child(_make_bamboo_divider(680, 4))
briefing_column.add_child(_make_section_caption("Bronze War Edict", 680))
briefing_objective_panel = PanelContainer.new() briefing_objective_panel = PanelContainer.new()
briefing_objective_panel.custom_minimum_size = Vector2(680, 92) briefing_objective_panel.custom_minimum_size = Vector2(680, 92)
@@ -839,6 +867,8 @@ func _create_hud() -> void:
_apply_label_style(briefing_objective_label, UI_AGED_INK, 15) _apply_label_style(briefing_objective_label, UI_AGED_INK, 15)
briefing_objective_row.add_child(briefing_objective_label) briefing_objective_row.add_child(briefing_objective_label)
briefing_column.add_child(_make_section_caption("Bamboo Map and Camp Ledger", 680))
briefing_camp_overview_row = HBoxContainer.new() briefing_camp_overview_row = HBoxContainer.new()
briefing_camp_overview_row.custom_minimum_size = Vector2(680, 84) briefing_camp_overview_row.custom_minimum_size = Vector2(680, 84)
briefing_camp_overview_row.add_theme_constant_override("separation", 10) briefing_camp_overview_row.add_theme_constant_override("separation", 10)
@@ -857,6 +887,7 @@ func _create_hud() -> void:
briefing_camp_overview_texture.set_anchors_preset(Control.PRESET_FULL_RECT) briefing_camp_overview_texture.set_anchors_preset(Control.PRESET_FULL_RECT)
briefing_camp_overview_texture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE briefing_camp_overview_texture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
briefing_camp_overview_texture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED briefing_camp_overview_texture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
briefing_camp_overview_texture.modulate = Color(0.92, 0.78, 0.54, 0.94)
camp_thumbnail_stack.add_child(briefing_camp_overview_texture) camp_thumbnail_stack.add_child(briefing_camp_overview_texture)
briefing_camp_overview_fallback_label = Label.new() briefing_camp_overview_fallback_label = Label.new()
@@ -874,7 +905,7 @@ func _create_hud() -> void:
_apply_label_style(briefing_camp_overview_label, UI_AGED_INK, 14) _apply_label_style(briefing_camp_overview_label, UI_AGED_INK, 14)
briefing_camp_overview_row.add_child(briefing_camp_overview_label) briefing_camp_overview_row.add_child(briefing_camp_overview_label)
briefing_column.add_child(_make_separator(680, 1)) briefing_column.add_child(_make_bamboo_divider(680, 3))
var briefing_scroll := ScrollContainer.new() var briefing_scroll := ScrollContainer.new()
briefing_scroll.custom_minimum_size = Vector2(680, 66) briefing_scroll.custom_minimum_size = Vector2(680, 66)
@@ -1130,6 +1161,8 @@ func _create_hud() -> void:
dialogue_column.add_theme_constant_override("separation", 8) dialogue_column.add_theme_constant_override("separation", 8)
dialogue_row.add_child(dialogue_column) dialogue_row.add_child(dialogue_column)
dialogue_column.add_child(_make_bamboo_divider(DIALOGUE_TEXT_SIZE.x, 3))
dialogue_speaker_panel = PanelContainer.new() dialogue_speaker_panel = PanelContainer.new()
dialogue_speaker_panel.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x, 32) dialogue_speaker_panel.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x, 32)
_apply_panel_style(dialogue_speaker_panel, "speaker_seal") _apply_panel_style(dialogue_speaker_panel, "speaker_seal")
@@ -1160,6 +1193,8 @@ func _create_hud() -> void:
_apply_label_style(dialogue_text_label, UI_AGED_INK, 17) _apply_label_style(dialogue_text_label, UI_AGED_INK, 17)
dialogue_text_row.add_child(dialogue_text_label) dialogue_text_row.add_child(dialogue_text_label)
dialogue_column.add_child(_make_bamboo_divider(DIALOGUE_TEXT_SIZE.x, 3))
var dialogue_control_row := HBoxContainer.new() var dialogue_control_row := HBoxContainer.new()
dialogue_control_row.add_theme_constant_override("separation", 8) dialogue_control_row.add_theme_constant_override("separation", 8)
dialogue_column.add_child(dialogue_control_row) dialogue_column.add_child(dialogue_control_row)

View File

@@ -70,6 +70,10 @@ const NORTHERN_PURSUIT_HARD_MARCH_CONVERSATION_ID := "northern_pursuit_hard_marc
const NORTHERN_PURSUIT_JI_SUPPLY_CONVERSATION_ID := "northern_pursuit_ji_supply_medicine" const NORTHERN_PURSUIT_JI_SUPPLY_CONVERSATION_ID := "northern_pursuit_ji_supply_medicine"
const NANPI_SCENARIO_ID := "019_nanpi_pressure" const NANPI_SCENARIO_ID := "019_nanpi_pressure"
const NANPI_SCENARIO_PATH := "res://data/scenarios/019_nanpi_pressure.json" const NANPI_SCENARIO_PATH := "res://data/scenarios/019_nanpi_pressure.json"
const NANPI_PRESSURE_HARD_PUSH_CONVERSATION_ID := "nanpi_pressure_hard_push_beans"
const NANPI_PRESSURE_ADMIN_SUPPLY_CONVERSATION_ID := "nanpi_pressure_admin_medicine"
const NANPI_SURRENDER_SCENARIO_ID := "020_nanpi_surrender"
const NANPI_SURRENDER_SCENARIO_PATH := "res://data/scenarios/020_nanpi_surrender.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"
@@ -97,6 +101,7 @@ func _init() -> void:
_check_ye_siege_post_battle_choice_bridges_to_surrender(failures) _check_ye_siege_post_battle_choice_bridges_to_surrender(failures)
_check_ye_surrender_post_battle_choice_bridges_to_northern_pursuit(failures) _check_ye_surrender_post_battle_choice_bridges_to_northern_pursuit(failures)
_check_northern_pursuit_post_battle_choice_bridges_to_nanpi(failures) _check_northern_pursuit_post_battle_choice_bridges_to_nanpi(failures)
_check_nanpi_post_battle_choice_bridges_to_surrender(failures)
_check_manual_checkpoint_reverts_claim(failures) _check_manual_checkpoint_reverts_claim(failures)
_check_completed_replay_cannot_claim(failures) _check_completed_replay_cannot_claim(failures)
_check_conditional_camp_conversations(failures) _check_conditional_camp_conversations(failures)
@@ -107,6 +112,7 @@ func _init() -> void:
_check_ye_siege_branch_camp_conversations(failures) _check_ye_siege_branch_camp_conversations(failures)
_check_ye_surrender_branch_camp_conversations(failures) _check_ye_surrender_branch_camp_conversations(failures)
_check_northern_pursuit_branch_camp_conversations(failures) _check_northern_pursuit_branch_camp_conversations(failures)
_check_nanpi_branch_camp_conversations(failures)
_check_talk_menu_closes_on_cancel_and_reload(failures) _check_talk_menu_closes_on_cancel_and_reload(failures)
if not _restore_user_file(SAVE_PATH, save_backup): if not _restore_user_file(SAVE_PATH, save_backup):
@@ -1099,6 +1105,25 @@ func _check_northern_pursuit_post_battle_choice_bridges_to_nanpi(failures: Array
) )
func _check_nanpi_post_battle_choice_bridges_to_surrender(failures: Array[String]) -> void:
_check_nanpi_choice_branch_to_surrender(
failures,
"press_yuan_tan_surrender",
"pressed_yuan_tan_surrender",
"secured_nanpi_admin",
"war_drum",
"imperial_seal"
)
_check_nanpi_choice_branch_to_surrender(
failures,
"secure_nanpi_admin",
"secured_nanpi_admin",
"pressed_yuan_tan_surrender",
"imperial_seal",
"war_drum"
)
func _check_wuchao_choice_branch_to_cangting( func _check_wuchao_choice_branch_to_cangting(
failures: Array[String], failures: Array[String],
choice_id: String, choice_id: String,
@@ -1537,6 +1562,79 @@ func _check_northern_pursuit_choice_branch_to_nanpi(
scene.free() scene.free()
func _check_nanpi_choice_branch_to_surrender(
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, "Nanpi choice bridge %s" % choice_id, NANPI_SCENARIO_ID, NANPI_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(
NANPI_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("Nanpi choice bridge %s could not reload Nanpi 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("Nanpi choice bridge %s should save 019 victory" % choice_id)
scene.free()
return
if not scene.campaign_state.get_joined_officers_snapshot().has("zhang_he"):
failures.append("Nanpi choice bridge %s should keep Zhang He before Nanpi Surrender" % choice_id)
if bool(result.get("choice_applied", true)):
failures.append("Nanpi choice bridge %s should wait for a selected post-battle choice" % choice_id)
if scene.campaign_state.pending_post_battle_choice_scenario_id != NANPI_SCENARIO_ID:
failures.append("Nanpi choice bridge %s should mark Nanpi choice pending" % choice_id)
var choice := _find_choice_by_id(result.get("post_battle_choices", []), choice_id)
if choice.is_empty():
failures.append("Nanpi choice bridge missing choice: %s" % choice_id)
scene.free()
return
if not scene.campaign_state.try_apply_post_battle_choice(choice, NANPI_SCENARIO_ID):
failures.append("Nanpi 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("Nanpi choice bridge %s should set %s" % [choice_id, expected_flag])
if flags.get(cleared_flag, true) != false:
failures.append("Nanpi choice bridge %s should clear %s" % [choice_id, cleared_flag])
if scene.campaign_state.current_scenario_id != NANPI_SURRENDER_SCENARIO_ID:
failures.append("Nanpi choice bridge %s should advance to Nanpi Surrender, got %s" % [choice_id, scene.campaign_state.current_scenario_id])
if not scene.state.load_battle(
NANPI_SURRENDER_SCENARIO_PATH,
scene.campaign_state.get_roster_overrides(),
scene.campaign_state.get_inventory_snapshot(),
flags,
scene.campaign_state.get_joined_officers_snapshot()
):
failures.append("Nanpi choice bridge %s could not load Nanpi Surrender" % choice_id)
scene.free()
return
if scene.state.get_unit("zhang_he_ch20").is_empty():
failures.append("Nanpi choice bridge %s should deploy Zhang He in Nanpi Surrender" % choice_id)
elif not scene.state.is_required_deployment("zhang_he_ch20"):
failures.append("Nanpi choice bridge %s should require Zhang He in Nanpi Surrender" % choice_id)
var shop_items: Array = scene.state.get_shop_item_ids()
if not shop_items.has(expected_shop_item_id):
failures.append("Nanpi choice bridge %s should expose shop item %s" % [choice_id, expected_shop_item_id])
if shop_items.has(hidden_shop_item_id):
failures.append("Nanpi choice bridge %s should hide shop item %s" % [choice_id, hidden_shop_item_id])
scene.free()
func _find_choice_by_id(choices, choice_id: String) -> Dictionary: func _find_choice_by_id(choices, choice_id: String) -> Dictionary:
if typeof(choices) != TYPE_ARRAY: if typeof(choices) != TYPE_ARRAY:
return {} return {}
@@ -2212,6 +2310,88 @@ func _check_northern_pursuit_branch_camp_conversations(failures: Array[String])
stabilized_scene.free() stabilized_scene.free()
func _check_nanpi_branch_camp_conversations(failures: Array[String]) -> void:
var base_scene = _new_prebattle_scene_for(failures, "Nanpi base camp", NANPI_SCENARIO_ID, NANPI_SCENARIO_PATH)
if base_scene != null:
if base_scene._camp_conversation_by_id("nanpi_pressure_gate_council").is_empty():
failures.append("Nanpi should expose the gate council conversation")
if base_scene._camp_conversation_by_id("nanpi_pressure_zhang_he_gate_names").is_empty():
failures.append("Nanpi should expose Zhang He's gate names conversation")
if not base_scene._camp_conversation_by_id(NANPI_PRESSURE_HARD_PUSH_CONVERSATION_ID).is_empty():
failures.append("Nanpi hard push beans should be hidden without pressed north flag")
if not base_scene._camp_conversation_by_id(NANPI_PRESSURE_ADMIN_SUPPLY_CONVERSATION_ID).is_empty():
failures.append("Nanpi admin medicine should be hidden without secured Ji flag")
var merchant: Dictionary = base_scene.state.get_shop_merchant()
if str(merchant.get("name", "")) != "Nanpi Gate Sutler":
failures.append("Nanpi shop merchant name should be present")
var merchant_lines: Array = merchant.get("lines", [])
if merchant_lines.size() < 2:
failures.append("Nanpi shop merchant should expose flavor lines")
if base_scene._format_talk_status_text(base_scene._camp_conversation_entries()) != "Camp conversations | 2 topics":
failures.append("Nanpi base talk status should summarize two non-supply topics")
base_scene.free()
var pressed_scene = _new_prebattle_scene_for(
failures,
"Nanpi pressed camp",
NANPI_SCENARIO_ID,
NANPI_SCENARIO_PATH,
{"pressed_north_to_nanpi": true}
)
if pressed_scene != null:
var hard_push_beans: Dictionary = pressed_scene._camp_conversation_by_id(NANPI_PRESSURE_HARD_PUSH_CONVERSATION_ID)
if hard_push_beans.is_empty():
failures.append("pressed north flag should expose Nanpi hard push beans")
if not pressed_scene._camp_conversation_by_id(NANPI_PRESSURE_ADMIN_SUPPLY_CONVERSATION_ID).is_empty():
failures.append("pressed north flag should not expose Nanpi admin medicine")
if not hard_push_beans.is_empty():
if pressed_scene._format_talk_status_text(pressed_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready":
failures.append("Nanpi pressed talk status should show one ready supply")
if not pressed_scene._format_camp_conversation_button_text(hard_push_beans).contains("Supply Bean"):
failures.append("Nanpi hard push beans button should show Bean supply")
var before_bean := int(pressed_scene.campaign_state.get_inventory_snapshot().get("bean", 0))
pressed_scene._apply_camp_conversation_effects(hard_push_beans)
if int(pressed_scene.campaign_state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("Nanpi hard push beans should add Bean to campaign inventory")
if int(pressed_scene.state.get_inventory_snapshot().get("bean", 0)) != before_bean + 1:
failures.append("Nanpi hard push beans should refresh battle Bean inventory")
if not pressed_scene.campaign_state.has_claimed_camp_conversation_effects(NANPI_SCENARIO_ID, NANPI_PRESSURE_HARD_PUSH_CONVERSATION_ID):
failures.append("Nanpi hard push beans claim should be saved")
if pressed_scene._format_talk_status_text(pressed_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed":
failures.append("Nanpi pressed talk status should show claimed supply after claim")
pressed_scene.free()
var secured_scene = _new_prebattle_scene_for(
failures,
"Nanpi secured camp",
NANPI_SCENARIO_ID,
NANPI_SCENARIO_PATH,
{"secured_ji_province": true}
)
if secured_scene != null:
var admin_medicine: Dictionary = secured_scene._camp_conversation_by_id(NANPI_PRESSURE_ADMIN_SUPPLY_CONVERSATION_ID)
if admin_medicine.is_empty():
failures.append("secured Ji flag should expose Nanpi admin medicine")
if not secured_scene._camp_conversation_by_id(NANPI_PRESSURE_HARD_PUSH_CONVERSATION_ID).is_empty():
failures.append("secured Ji flag should not expose Nanpi hard push beans")
if not admin_medicine.is_empty():
if secured_scene._format_talk_status_text(secured_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready":
failures.append("Nanpi secured talk status should show one ready supply")
if not secured_scene._format_camp_conversation_button_text(admin_medicine).contains("Supply Panacea"):
failures.append("Nanpi admin medicine button should show Panacea supply")
var before_panacea := int(secured_scene.campaign_state.get_inventory_snapshot().get("panacea", 0))
secured_scene._apply_camp_conversation_effects(admin_medicine)
if int(secured_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("Nanpi admin medicine should add Panacea to campaign inventory")
if int(secured_scene.state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("Nanpi admin medicine should refresh battle Panacea inventory")
if not secured_scene.campaign_state.has_claimed_camp_conversation_effects(NANPI_SCENARIO_ID, NANPI_PRESSURE_ADMIN_SUPPLY_CONVERSATION_ID):
failures.append("Nanpi admin medicine claim should be saved")
if secured_scene._format_talk_status_text(secured_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed":
failures.append("Nanpi secured talk status should show claimed supply after claim")
secured_scene.free()
func _check_talk_menu_closes_on_cancel_and_reload(failures: Array[String]) -> void: func _check_talk_menu_closes_on_cancel_and_reload(failures: Array[String]) -> void:
var scene = _new_prebattle_scene(failures, "talk menu close guards") var scene = _new_prebattle_scene(failures, "talk menu close guards")
if scene == null: if scene == null:

View File

@@ -1107,6 +1107,85 @@ func _check_battle_visual_data(failures: Array[String]) -> void:
).is_empty(): ).is_empty():
failures.append("018 old Ye Surrender flags should not expose Ji supply medicine") failures.append("018 old Ye Surrender flags should not expose Ji supply medicine")
var nanpi_state = BattleStateScript.new()
if not nanpi_state.load_battle("res://data/scenarios/019_nanpi_pressure.json"):
failures.append("could not load Nanpi camp data")
else:
_check_shop_items_unique(failures, nanpi_state, "019 base")
var nanpi_conversations: Array = nanpi_state.get_briefing().get("camp_conversations", [])
if nanpi_conversations.size() != 2:
failures.append("019 base briefing should expose exactly two camp conversations")
else:
_check_camp_conversation(failures, nanpi_conversations[0], "nanpi_pressure_gate_council", "topic", "")
_check_camp_conversation(failures, nanpi_conversations[1], "nanpi_pressure_zhang_he_gate_names", "officer", "zhang_he")
var nanpi_merchant := nanpi_state.get_shop_merchant()
if str(nanpi_merchant.get("name", "")) != "Nanpi Gate Sutler":
failures.append("019 shop should expose Nanpi Gate merchant name")
if (nanpi_merchant.get("lines", []) as Array).size() < 2:
failures.append("019 shop should expose merchant flavor lines")
_check_shop_item_visibility(failures, nanpi_state, "war_drum", false, "019 base")
_check_shop_item_visibility(failures, nanpi_state, "imperial_seal", false, "019 base")
_check_shop_item_visibility(failures, nanpi_state, "iron_armor", true, "019 base")
var nanpi_pressed_state = BattleStateScript.new()
if not nanpi_pressed_state.load_battle("res://data/scenarios/019_nanpi_pressure.json", {}, {}, {"pressed_north_to_nanpi": true}):
failures.append("could not load Nanpi pressed north camp data")
else:
_check_shop_items_unique(failures, nanpi_pressed_state, "019 pressed north")
var hard_push_beans: Dictionary = _find_camp_conversation(
nanpi_pressed_state.get_briefing().get("camp_conversations", []),
"nanpi_pressure_hard_push_beans"
)
if hard_push_beans.is_empty():
failures.append("019 pressed north flag should expose hard push beans")
else:
_check_camp_conversation_effect(failures, hard_push_beans, "bean", 1)
if not _find_camp_conversation(
nanpi_pressed_state.get_briefing().get("camp_conversations", []),
"nanpi_pressure_admin_medicine"
).is_empty():
failures.append("019 pressed north flag should not expose admin medicine")
_check_shop_item_visibility(failures, nanpi_pressed_state, "war_drum", true, "019 pressed north")
_check_shop_item_visibility(failures, nanpi_pressed_state, "imperial_seal", false, "019 pressed north")
var nanpi_secured_state = BattleStateScript.new()
if not nanpi_secured_state.load_battle("res://data/scenarios/019_nanpi_pressure.json", {}, {}, {"secured_ji_province": true}):
failures.append("could not load Nanpi secured Ji camp data")
else:
_check_shop_items_unique(failures, nanpi_secured_state, "019 secured Ji")
var admin_medicine: Dictionary = _find_camp_conversation(
nanpi_secured_state.get_briefing().get("camp_conversations", []),
"nanpi_pressure_admin_medicine"
)
if admin_medicine.is_empty():
failures.append("019 secured Ji flag should expose admin medicine")
else:
_check_camp_conversation_effect(failures, admin_medicine, "panacea", 1)
if not _find_camp_conversation(
nanpi_secured_state.get_briefing().get("camp_conversations", []),
"nanpi_pressure_hard_push_beans"
).is_empty():
failures.append("019 secured Ji flag should not expose hard push beans")
_check_shop_item_visibility(failures, nanpi_secured_state, "war_drum", false, "019 secured Ji")
_check_shop_item_visibility(failures, nanpi_secured_state, "imperial_seal", true, "019 secured Ji")
var nanpi_old_flags_state = BattleStateScript.new()
if not nanpi_old_flags_state.load_battle("res://data/scenarios/019_nanpi_pressure.json", {}, {}, {"pursued_yuan_shang": true, "stabilized_ye": true}):
failures.append("could not load Nanpi old Northern Pursuit flag camp data")
else:
_check_shop_item_visibility(failures, nanpi_old_flags_state, "war_drum", false, "019 old Northern Pursuit flags")
_check_shop_item_visibility(failures, nanpi_old_flags_state, "imperial_seal", false, "019 old Northern Pursuit flags")
if not _find_camp_conversation(
nanpi_old_flags_state.get_briefing().get("camp_conversations", []),
"nanpi_pressure_hard_push_beans"
).is_empty():
failures.append("019 old Northern Pursuit flags should not expose hard push beans")
if not _find_camp_conversation(
nanpi_old_flags_state.get_briefing().get("camp_conversations", []),
"nanpi_pressure_admin_medicine"
).is_empty():
failures.append("019 old Northern Pursuit flags should not expose admin medicine")
for item_id in ["bronze_sword", "training_spear", "short_bow", "hand_axe", "iron_armor", "panacea"]: for item_id in ["bronze_sword", "training_spear", "short_bow", "hand_axe", "iron_armor", "panacea"]:
var item := state.get_item_def(item_id) var item := state.get_item_def(item_id)
if item.is_empty(): if item.is_empty():
@@ -1282,15 +1361,15 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void:
for expected in ["Mandate of Victory:", "Omen of Ruin:", "Battle Register:", "Dire Portents:", "Spoils Ledger:"]: for expected in ["Mandate of Victory:", "Omen of Ruin:", "Battle Register:", "Dire Portents:", "Spoils Ledger:"]:
if not briefing_text.contains(expected): if not briefing_text.contains(expected):
failures.append("briefing objective parchment should use ancient heading `%s`: %s" % [expected, briefing_text]) 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.66, 0.55, 0.36, 0.97)) _check_panel_style_fill(failures, scene.briefing_panel, "briefing panel", Color(0.58, 0.47, 0.29, 0.985))
_check_panel_style_frame(failures, scene.briefing_panel, "briefing panel", Color(0.34, 0.22, 0.11, 0.98), 3) _check_panel_style_frame(failures, scene.briefing_panel, "briefing panel", Color(0.09, 0.055, 0.032, 1.0), 4)
_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_fill(failures, scene.briefing_objective_panel, "briefing objective edict panel", Color(0.73, 0.60, 0.38, 0.99))
_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_frame(failures, scene.briefing_objective_panel, "briefing objective edict panel", Color(0.32, 0.025, 0.016, 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_fill(failures, scene.dialogue_panel, "dialogue panel", Color(0.075, 0.028, 0.017, 0.99))
_check_panel_style_frame(failures, scene.dialogue_panel, "dialogue panel", Color(0.78, 0.58, 0.27, 1.0), 3) _check_panel_style_frame(failures, scene.dialogue_panel, "dialogue panel", Color(0.78, 0.58, 0.27, 1.0), 4)
_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_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_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)) _check_panel_style_fill(failures, scene.dialogue_text_panel, "dialogue text scroll panel", Color(0.70, 0.58, 0.36, 0.99))
if scene.dialogue_continue_button == null or scene.dialogue_continue_button.text != "Next Slip": if scene.dialogue_continue_button == null or scene.dialogue_continue_button.text != "Next Slip":
failures.append("dialogue continue button should use ancient slip text") failures.append("dialogue continue button should use ancient slip text")
if scene.dialogue_previous_button == null or scene.dialogue_previous_button.text != "Earlier Slip": if scene.dialogue_previous_button == null or scene.dialogue_previous_button.text != "Earlier Slip":
@@ -1316,7 +1395,7 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void:
scene._update_briefing_camp_overview(scene.state.get_briefing(), false) scene._update_briefing_camp_overview(scene.state.get_briefing(), false)
if scene.briefing_camp_overview_fallback_label == null or scene.briefing_camp_overview_fallback_label.text != "Silk War Map": if scene.briefing_camp_overview_fallback_label == null or scene.briefing_camp_overview_fallback_label.text != "Silk War Map":
failures.append("briefing map fallback should read as an old campaign map") failures.append("briefing map fallback should read as an old campaign map")
_check_panel_style_fill(failures, scene.result_panel, "result panel", Color(0.66, 0.55, 0.36, 0.97)) _check_panel_style_fill(failures, scene.result_panel, "result panel", Color(0.58, 0.47, 0.29, 0.985))
scene.free() scene.free()