diff --git a/data/scenarios/012_guandu_showdown.json b/data/scenarios/012_guandu_showdown.json index 9dbd803..6c56e9a 100644 --- a/data/scenarios/012_guandu_showdown.json +++ b/data/scenarios/012_guandu_showdown.json @@ -32,6 +32,102 @@ "Scouts from Yan Ford report unusually heavy traffic toward Wuchao. Yuan Shao's grain may be less protected than his banners." ] } + ], + "camp_conversations": [ + { + "id": "guandu_main_line_council", + "group": "officer", + "officer_id": "cao_cao", + "label": "Cao Cao - Main Line", + "speaker": "Cao Cao", + "summary": "Set the rhythm for holding Guandu against Yuan Shao's full army.", + "lines": [ + { + "speaker": "Cao Cao", + "side": "left", + "text": "Yuan Shao wants numbers to be a storm. A storm still breaks against a wall that knows why it stands." + }, + { + "speaker": "Xiahou Dun", + "side": "right", + "text": "Then the front holds by count, not pride. I will make the line step back only when ordered." + } + ] + }, + { + "id": "guandu_guo_jia_supply_wound", + "group": "officer", + "officer_id": "guo_jia", + "label": "Guo Jia - Supply Wound", + "speaker": "Guo Jia", + "summary": "Watch Yuan Shao's assault for the supply weakness that can decide the campaign.", + "lines": [ + { + "speaker": "Guo Jia", + "side": "right", + "text": "A large army has more than courage to protect. It has grain roads, signal fires, and arguments between commanders." + }, + { + "speaker": "Cao Cao", + "side": "left", + "text": "Then Guandu is not only a wall. It is where we make Yuan Shao reveal what feeds him." + } + ] + }, + { + "id": "guandu_secured_line_medicine", + "campaign_flags": { "secured_guandu_line": true }, + "group": "topic", + "label": "Secured Line Medicine", + "summary": "Open the prepared camp stores for field medicine before the main assault.", + "lines": [ + { + "speaker": "Cao Ren", + "side": "left", + "text": "The camps secured after Yan Ford have dry medicine and enough order to send it where the line bends." + }, + { + "speaker": "Cao Cao", + "side": "right", + "text": "Issue it before Yuan Shao's banners move. A prepared camp should save men before it praises them." + } + ], + "effects": [ + { + "type": "grant_item", + "item_id": "panacea", + "count": 1, + "text": "The secured line medicine adds a Panacea to the field supplies." + } + ] + }, + { + "id": "guandu_wuchao_scout_wine", + "campaign_flags": { "scouted_wuchao_depots": true }, + "group": "topic", + "label": "Wuchao Scout Wine", + "summary": "Reward the scouts who traced Yuan Shao's grain traffic toward Wuchao.", + "lines": [ + { + "speaker": "Xiahou Yuan", + "side": "right", + "text": "The scouts followed the grain marks far enough to name Wuchao. They came back with horses breathing hard." + }, + { + "speaker": "Guo Jia", + "side": "left", + "text": "Give them wine and fresh orders. A tired scout who found the grain has already moved a whole army." + } + ], + "effects": [ + { + "type": "grant_item", + "item_id": "wine", + "count": 1, + "text": "The Wuchao scout wine adds Wine to the field supplies." + } + ] + } ] }, "shop": { @@ -49,13 +145,20 @@ "conditional_items": [ { "campaign_flags": { "secured_guandu_line": true }, - "items": ["imperial_seal"] + "items": ["panacea"] }, { "campaign_flags": { "scouted_wuchao_depots": true }, - "items": ["war_drum"] + "items": ["antidote"] } - ] + ], + "merchant": { + "name": "Guandu Camp Sutler", + "lines": [ + "Buy before Yuan Shao's banners blot out the road. Once the main assault starts, even a bean needs an escort.", + "No loose loads on the front line. Medicine, wine, fitted blades, and armor that can be buckled in one breath." + ] + } }, "roster": { "max_units": 6, diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index dfef461..1916b9d 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -69,6 +69,15 @@ 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_DISABLED_TEXT := Color(0.48, 0.40, 0.29, 1.0) var state: BattleState = BattleStateScript.new() var campaign_state: CampaignState = CampaignStateScript.new() @@ -273,6 +282,102 @@ func _draw() -> void: _draw_floating_texts() +func _make_panel_style(fill: Color, border: Color, border_width: int, radius: int, margin: int, shadow_size: int) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = fill + style.border_color = border + style.set_border_width_all(border_width) + style.set_corner_radius_all(radius) + style.content_margin_left = margin + style.content_margin_right = margin + style.content_margin_top = margin + style.content_margin_bottom = margin + if shadow_size > 0: + style.shadow_color = Color(0.0, 0.0, 0.0, 0.38) + style.shadow_offset = Vector2(2, 3) + style.shadow_size = shadow_size + return style + + +func _apply_panel_style(panel: PanelContainer, variant: String) -> void: + if panel == null: + return + var fill := UI_DARK_LACQUER + var border := UI_OLD_BRONZE + var border_width := 2 + var radius := 1 + var margin := 10 + var shadow_size := 6 + match variant: + "paper": + fill = UI_RICE_PAPER + border = UI_RICE_PAPER_DARK + margin = 12 + "seal": + fill = UI_CINNABAR + border = UI_OLD_BRONZE + margin = 8 + "portrait": + fill = UI_AGED_INK + border = UI_OLD_BRONZE + margin = 3 + "notice": + fill = Color(0.20, 0.09, 0.035, 0.96) + border = UI_CINNABAR + margin = 8 + "map": + fill = UI_AGED_INK + border = UI_OLD_BRONZE + margin = 3 + "compact": + fill = Color(0.16, 0.075, 0.04, 0.96) + border = UI_OLD_BRONZE + margin = 7 + shadow_size = 4 + _: + pass + panel.add_theme_stylebox_override("panel", _make_panel_style(fill, border, border_width, radius, margin, shadow_size)) + + +func _make_button_style(fill: Color, border: Color, border_width: int) -> StyleBoxFlat: + return _make_panel_style(fill, border, border_width, 1, 8, 0) + + +func _apply_button_style(button: Button, important: bool = false) -> void: + if button == null: + return + var normal_fill := UI_CINNABAR if important else Color(0.21, 0.075, 0.04, 0.98) + var hover_fill := Color(0.58, 0.12, 0.055, 1.0) if important else Color(0.32, 0.12, 0.055, 1.0) + var pressed_fill := Color(0.36, 0.045, 0.03, 1.0) + button.add_theme_stylebox_override("normal", _make_button_style(normal_fill, UI_OLD_BRONZE, 1)) + button.add_theme_stylebox_override("hover", _make_button_style(hover_fill, UI_OLD_BRONZE, 1)) + button.add_theme_stylebox_override("pressed", _make_button_style(pressed_fill, UI_OLD_BRONZE, 1)) + button.add_theme_stylebox_override("focus", _make_button_style(Color(0.20, 0.36, 0.28, 0.95), UI_OLD_BRONZE, 2)) + button.add_theme_stylebox_override("disabled", _make_button_style(Color(0.13, 0.09, 0.065, 0.88), UI_RICE_PAPER_DARK, 1)) + button.add_theme_color_override("font_color", UI_PARCHMENT_TEXT) + button.add_theme_color_override("font_hover_color", UI_PARCHMENT_TEXT) + button.add_theme_color_override("font_pressed_color", UI_PARCHMENT_TEXT) + button.add_theme_color_override("font_focus_color", UI_PARCHMENT_TEXT) + button.add_theme_color_override("font_disabled_color", UI_DISABLED_TEXT) + + +func _apply_label_style(label: Label, font_color: Color, font_size: int = 0) -> void: + if label == null: + return + label.add_theme_color_override("font_color", font_color) + if font_size > 0: + label.add_theme_font_size_override("font_size", font_size) + + +func _make_separator(width: float = 0.0, height: float = 2.0) -> ColorRect: + var separator := ColorRect.new() + separator.color = UI_OLD_BRONZE + separator.custom_minimum_size = Vector2(width, height) + separator.size_flags_horizontal = Control.SIZE_EXPAND_FILL + separator.mouse_filter = Control.MOUSE_FILTER_IGNORE + return separator + + func _create_hud() -> void: var layer := CanvasLayer.new() add_child(layer) @@ -285,6 +390,7 @@ func _create_hud() -> void: var top_bar := PanelContainer.new() top_bar.position = Vector2(24, 16) top_bar.size = Vector2(1120, 64) + _apply_panel_style(top_bar, "compact") root.add_child(top_bar) var top_row := HBoxContainer.new() @@ -293,15 +399,18 @@ func _create_hud() -> void: status_label = Label.new() status_label.custom_minimum_size = Vector2(180, 40) + _apply_label_style(status_label, UI_PARCHMENT_TEXT) top_row.add_child(status_label) objective_label = Label.new() objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART objective_label.custom_minimum_size = Vector2(360, 48) + _apply_label_style(objective_label, UI_PARCHMENT_TEXT) top_row.add_child(objective_label) campaign_status_label = Label.new() campaign_status_label.custom_minimum_size = Vector2(300, 40) + _apply_label_style(campaign_status_label, UI_PARCHMENT_TEXT) top_row.add_child(campaign_status_label) restart_button = Button.new() @@ -319,6 +428,7 @@ func _create_hud() -> void: objective_notice_panel.position = Vector2(360, 84) objective_notice_panel.size = Vector2(560, 64) objective_notice_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE + _apply_panel_style(objective_notice_panel, "notice") root.add_child(objective_notice_panel) objective_notice_label = Label.new() @@ -326,11 +436,13 @@ func _create_hud() -> void: objective_notice_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER objective_notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART objective_notice_label.custom_minimum_size = Vector2(520, 48) + _apply_label_style(objective_notice_label, UI_PARCHMENT_TEXT, 16) objective_notice_panel.add_child(objective_notice_label) var side_panel := PanelContainer.new() side_panel.position = Vector2(760, 104) side_panel.size = Vector2(480, 610) + _apply_panel_style(side_panel, "compact") root.add_child(side_panel) var side_column := VBoxContainer.new() @@ -345,6 +457,7 @@ func _create_hud() -> void: hud_unit_portrait_panel = PanelContainer.new() hud_unit_portrait_panel.custom_minimum_size = HUD_UNIT_PORTRAIT_SIZE + _apply_panel_style(hud_unit_portrait_panel, "portrait") selected_row.add_child(hud_unit_portrait_panel) var hud_unit_portrait_stack := Control.new() @@ -362,37 +475,44 @@ func _create_hud() -> void: hud_unit_portrait_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER hud_unit_portrait_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER hud_unit_portrait_label.add_theme_font_size_override("font_size", 24) + _apply_label_style(hud_unit_portrait_label, UI_PARCHMENT_TEXT) hud_unit_portrait_stack.add_child(hud_unit_portrait_label) selected_label = Label.new() selected_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART selected_label.custom_minimum_size = Vector2(320, 126) selected_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _apply_label_style(selected_label, UI_PARCHMENT_TEXT) selected_row.add_child(selected_label) mission_title_label = Label.new() mission_title_label.text = "Objective" mission_title_label.add_theme_font_size_override("font_size", 14) + _apply_label_style(mission_title_label, UI_OLD_BRONZE) side_column.add_child(mission_title_label) mission_detail_label = Label.new() mission_detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART mission_detail_label.custom_minimum_size = Vector2(420, 126) + _apply_label_style(mission_detail_label, UI_PARCHMENT_TEXT) side_column.add_child(mission_detail_label) cell_info_label = Label.new() cell_info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART cell_info_label.custom_minimum_size = Vector2(420, 78) + _apply_label_style(cell_info_label, UI_PARCHMENT_TEXT) side_column.add_child(cell_info_label) forecast_label = Label.new() forecast_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART forecast_label.custom_minimum_size = Vector2(420, 60) + _apply_label_style(forecast_label, UI_PARCHMENT_TEXT) side_column.add_child(forecast_label) inventory_label = Label.new() inventory_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART inventory_label.custom_minimum_size = Vector2(420, 34) + _apply_label_style(inventory_label, UI_PARCHMENT_TEXT) side_column.add_child(inventory_label) var button_row := HBoxContainer.new() @@ -437,6 +557,7 @@ func _create_hud() -> void: var tactic_title := Label.new() tactic_title.text = "Tactics" + _apply_label_style(tactic_title, UI_OLD_BRONZE, 14) tactic_menu.add_child(tactic_title) tactic_list = VBoxContainer.new() @@ -450,6 +571,7 @@ func _create_hud() -> void: var item_title := Label.new() item_title.text = "Items" + _apply_label_style(item_title, UI_OLD_BRONZE, 14) item_menu.add_child(item_title) item_list = VBoxContainer.new() @@ -463,6 +585,7 @@ func _create_hud() -> void: var equip_title := Label.new() equip_title.text = "Equipment" + _apply_label_style(equip_title, UI_OLD_BRONZE, 14) equip_menu.add_child(equip_title) equip_list = VBoxContainer.new() @@ -474,6 +597,7 @@ func _create_hud() -> void: post_move_menu.mouse_filter = Control.MOUSE_FILTER_STOP post_move_menu.custom_minimum_size = POST_MOVE_MENU_SIZE post_move_menu.size = POST_MOVE_MENU_SIZE + _apply_panel_style(post_move_menu, "compact") root.add_child(post_move_menu) var post_move_column := VBoxContainer.new() @@ -483,6 +607,7 @@ func _create_hud() -> void: post_move_title_label = Label.new() post_move_title_label.text = "Choose Action" post_move_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _apply_label_style(post_move_title_label, UI_OLD_BRONZE, 14) post_move_column.add_child(post_move_title_label) var post_move_grid := GridContainer.new() @@ -526,6 +651,7 @@ func _create_hud() -> void: post_move_picker_panel.mouse_filter = Control.MOUSE_FILTER_STOP post_move_picker_panel.custom_minimum_size = POST_MOVE_PICKER_PANEL_SIZE post_move_picker_panel.size = POST_MOVE_PICKER_PANEL_SIZE + _apply_panel_style(post_move_picker_panel, "compact") root.add_child(post_move_picker_panel) var picker_column := VBoxContainer.new() @@ -535,11 +661,13 @@ func _create_hud() -> void: post_move_picker_title_label = Label.new() post_move_picker_title_label.text = "Choose" post_move_picker_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _apply_label_style(post_move_picker_title_label, UI_OLD_BRONZE, 14) picker_column.add_child(post_move_picker_title_label) post_move_picker_detail_label = Label.new() post_move_picker_detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART post_move_picker_detail_label.custom_minimum_size = Vector2(296, 32) + _apply_label_style(post_move_picker_detail_label, UI_PARCHMENT_TEXT) picker_column.add_child(post_move_picker_detail_label) var picker_scroll := ScrollContainer.new() @@ -573,6 +701,7 @@ func _create_hud() -> void: targeting_hint_panel.mouse_filter = Control.MOUSE_FILTER_STOP targeting_hint_panel.custom_minimum_size = TARGETING_HINT_PANEL_SIZE targeting_hint_panel.size = TARGETING_HINT_PANEL_SIZE + _apply_panel_style(targeting_hint_panel, "compact") root.add_child(targeting_hint_panel) var targeting_column := VBoxContainer.new() @@ -582,11 +711,13 @@ func _create_hud() -> void: targeting_hint_title_label = Label.new() targeting_hint_title_label.text = "Choose Target" targeting_hint_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _apply_label_style(targeting_hint_title_label, UI_OLD_BRONZE, 14) targeting_column.add_child(targeting_hint_title_label) targeting_hint_detail_label = Label.new() targeting_hint_detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART targeting_hint_detail_label.custom_minimum_size = Vector2(220, 28) + _apply_label_style(targeting_hint_detail_label, UI_PARCHMENT_TEXT) targeting_column.add_child(targeting_hint_detail_label) var targeting_button_row := HBoxContainer.new() @@ -608,41 +739,62 @@ func _create_hud() -> void: log_box = RichTextLabel.new() log_box.custom_minimum_size = Vector2(420, 70) log_box.fit_content = false + log_box.add_theme_color_override("default_color", UI_PARCHMENT_TEXT) + log_box.add_theme_stylebox_override("normal", _make_panel_style(Color(0.11, 0.07, 0.045, 0.78), UI_RICE_PAPER_DARK, 1, 1, 6, 0)) side_column.add_child(log_box) briefing_panel = PanelContainer.new() briefing_panel.visible = false - briefing_panel.position = Vector2(300, 88) - briefing_panel.size = Vector2(680, 600) + briefing_panel.position = Vector2(280, 72) + briefing_panel.size = Vector2(720, 628) + _apply_panel_style(briefing_panel, "paper") root.add_child(briefing_panel) var briefing_column := VBoxContainer.new() - briefing_column.add_theme_constant_override("separation", 8) + briefing_column.add_theme_constant_override("separation", 7) briefing_panel.add_child(briefing_column) + var briefing_title_panel := PanelContainer.new() + briefing_title_panel.custom_minimum_size = Vector2(680, 40) + _apply_panel_style(briefing_title_panel, "seal") + briefing_column.add_child(briefing_title_panel) + briefing_title_label = Label.new() briefing_title_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - briefing_title_label.custom_minimum_size = Vector2(640, 28) - briefing_title_label.add_theme_font_size_override("font_size", 20) - briefing_column.add_child(briefing_title_label) + briefing_title_label.custom_minimum_size = Vector2(656, 28) + briefing_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + briefing_title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _apply_label_style(briefing_title_label, UI_PARCHMENT_TEXT, 20) + briefing_title_panel.add_child(briefing_title_label) briefing_location_label = Label.new() briefing_location_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - briefing_location_label.custom_minimum_size = Vector2(640, 22) + briefing_location_label.custom_minimum_size = Vector2(680, 22) + briefing_location_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _apply_label_style(briefing_location_label, UI_MUTED_JADE, 14) briefing_column.add_child(briefing_location_label) + briefing_column.add_child(_make_separator(680, 2)) + + var briefing_objective_panel := PanelContainer.new() + briefing_objective_panel.custom_minimum_size = Vector2(680, 92) + _apply_panel_style(briefing_objective_panel, "compact") + briefing_column.add_child(briefing_objective_panel) + briefing_objective_label = Label.new() briefing_objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - briefing_objective_label.custom_minimum_size = Vector2(640, 48) - briefing_column.add_child(briefing_objective_label) + 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_camp_overview_row = HBoxContainer.new() - briefing_camp_overview_row.custom_minimum_size = Vector2(640, 76) - briefing_camp_overview_row.add_theme_constant_override("separation", 8) + briefing_camp_overview_row.custom_minimum_size = Vector2(680, 84) + briefing_camp_overview_row.add_theme_constant_override("separation", 10) briefing_column.add_child(briefing_camp_overview_row) var camp_thumbnail_panel := PanelContainer.new() camp_thumbnail_panel.custom_minimum_size = BRIEFING_CAMP_THUMBNAIL_SIZE + _apply_panel_style(camp_thumbnail_panel, "map") briefing_camp_overview_row.add_child(camp_thumbnail_panel) var camp_thumbnail_stack := Control.new() @@ -660,21 +812,26 @@ func _create_hud() -> void: briefing_camp_overview_fallback_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER briefing_camp_overview_fallback_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER briefing_camp_overview_fallback_label.add_theme_font_size_override("font_size", 18) + _apply_label_style(briefing_camp_overview_fallback_label, UI_OLD_BRONZE) camp_thumbnail_stack.add_child(briefing_camp_overview_fallback_label) briefing_camp_overview_label = Label.new() briefing_camp_overview_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - briefing_camp_overview_label.custom_minimum_size = Vector2(490, 72) + briefing_camp_overview_label.custom_minimum_size = Vector2(520, 72) briefing_camp_overview_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _apply_label_style(briefing_camp_overview_label, UI_AGED_INK, 14) briefing_camp_overview_row.add_child(briefing_camp_overview_label) + briefing_column.add_child(_make_separator(680, 1)) + var briefing_scroll := ScrollContainer.new() - briefing_scroll.custom_minimum_size = Vector2(640, 72) + briefing_scroll.custom_minimum_size = Vector2(680, 66) briefing_column.add_child(briefing_scroll) briefing_label = Label.new() briefing_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - briefing_label.custom_minimum_size = Vector2(620, 0) + briefing_label.custom_minimum_size = Vector2(656, 0) + _apply_label_style(briefing_label, UI_AGED_INK, 14) briefing_scroll.add_child(briefing_label) var prep_button_row := HBoxContainer.new() @@ -724,6 +881,7 @@ func _create_hud() -> void: chapter_status_label = Label.new() chapter_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART chapter_status_label.custom_minimum_size = Vector2(600, 22) + _apply_label_style(chapter_status_label, UI_AGED_INK, 14) chapter_menu.add_child(chapter_status_label) var chapter_scroll := ScrollContainer.new() @@ -743,6 +901,7 @@ func _create_hud() -> void: shop_status_label = Label.new() shop_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART shop_status_label.custom_minimum_size = Vector2(600, 22) + _apply_label_style(shop_status_label, UI_AGED_INK, 14) shop_menu.add_child(shop_status_label) var shop_mode_row := HBoxContainer.new() @@ -776,6 +935,7 @@ func _create_hud() -> void: talk_status_label = Label.new() talk_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART talk_status_label.custom_minimum_size = Vector2(600, 22) + _apply_label_style(talk_status_label, UI_AGED_INK, 14) talk_menu.add_child(talk_status_label) var talk_scroll := ScrollContainer.new() @@ -795,6 +955,7 @@ func _create_hud() -> void: armory_status_label = Label.new() armory_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART armory_status_label.custom_minimum_size = Vector2(600, 22) + _apply_label_style(armory_status_label, UI_AGED_INK, 14) armory_menu.add_child(armory_status_label) var armory_scroll := ScrollContainer.new() @@ -814,6 +975,7 @@ func _create_hud() -> void: roster_status_label = Label.new() roster_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART roster_status_label.custom_minimum_size = Vector2(600, 22) + _apply_label_style(roster_status_label, UI_AGED_INK, 14) roster_menu.add_child(roster_status_label) var roster_scroll := ScrollContainer.new() @@ -833,6 +995,7 @@ func _create_hud() -> void: formation_status_label = Label.new() formation_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART formation_status_label.custom_minimum_size = Vector2(600, 22) + _apply_label_style(formation_status_label, UI_AGED_INK, 14) formation_menu.add_child(formation_status_label) var formation_scroll := ScrollContainer.new() @@ -852,6 +1015,7 @@ func _create_hud() -> void: save_status_label = Label.new() save_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART save_status_label.custom_minimum_size = Vector2(600, 64) + _apply_label_style(save_status_label, UI_AGED_INK, 14) save_menu.add_child(save_status_label) var save_action_row := HBoxContainer.new() @@ -878,6 +1042,7 @@ 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") root.add_child(dialogue_panel) dialogue_row = HBoxContainer.new() @@ -886,6 +1051,7 @@ func _create_hud() -> void: dialogue_portrait_panel = PanelContainer.new() dialogue_portrait_panel.custom_minimum_size = DIALOGUE_PORTRAIT_SIZE + _apply_panel_style(dialogue_portrait_panel, "portrait") dialogue_row.add_child(dialogue_portrait_panel) var dialogue_portrait_stack := Control.new() @@ -903,6 +1069,7 @@ func _create_hud() -> void: dialogue_portrait_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER dialogue_portrait_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER dialogue_portrait_label.add_theme_font_size_override("font_size", 30) + _apply_label_style(dialogue_portrait_label, UI_PARCHMENT_TEXT) dialogue_portrait_stack.add_child(dialogue_portrait_label) dialogue_column = VBoxContainer.new() @@ -911,17 +1078,28 @@ 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.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x, 32) + _apply_panel_style(dialogue_speaker_panel, "seal") + dialogue_column.add_child(dialogue_speaker_panel) + dialogue_speaker_label = Label.new() - dialogue_speaker_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x, 26) - dialogue_speaker_label.add_theme_font_size_override("font_size", 18) - dialogue_column.add_child(dialogue_speaker_label) + dialogue_speaker_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x - 24, 22) + dialogue_speaker_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _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.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x, 104) + _apply_panel_style(dialogue_text_panel, "paper") + dialogue_column.add_child(dialogue_text_panel) dialogue_text_label = Label.new() dialogue_text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - dialogue_text_label.custom_minimum_size = DIALOGUE_TEXT_SIZE + dialogue_text_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x - 24, DIALOGUE_TEXT_SIZE.y - 20) dialogue_text_label.size_flags_vertical = Control.SIZE_EXPAND_FILL - dialogue_text_label.add_theme_font_size_override("font_size", 17) - dialogue_column.add_child(dialogue_text_label) + _apply_label_style(dialogue_text_label, UI_AGED_INK, 17) + dialogue_text_panel.add_child(dialogue_text_label) var dialogue_control_row := HBoxContainer.new() dialogue_control_row.add_theme_constant_override("separation", 8) @@ -931,6 +1109,7 @@ func _create_hud() -> void: dialogue_progress_label.custom_minimum_size = Vector2(520, 32) dialogue_progress_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL dialogue_progress_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + _apply_label_style(dialogue_progress_label, UI_OLD_BRONZE) dialogue_control_row.add_child(dialogue_progress_label) dialogue_previous_button = Button.new() @@ -950,6 +1129,7 @@ func _create_hud() -> void: result_panel.visible = false result_panel.position = Vector2(280, 156) result_panel.size = Vector2(560, 420) + _apply_panel_style(result_panel, "paper") root.add_child(result_panel) var result_column := VBoxContainer.new() @@ -961,6 +1141,7 @@ func _create_hud() -> void: result_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP result_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART result_label.custom_minimum_size = Vector2(520, 236) + _apply_label_style(result_label, UI_AGED_INK, 16) result_column.add_child(result_label) result_choice_list = VBoxContainer.new() @@ -977,6 +1158,43 @@ func _create_hud() -> void: next_battle_button.pressed.connect(_on_next_battle_pressed) result_column.add_child(next_battle_button) + for hud_button in [ + restart_button, + new_campaign_button, + wait_button, + tactic_button, + item_button, + equip_button, + threat_button, + end_turn_button, + post_move_attack_button, + post_move_tactic_button, + post_move_item_button, + post_move_wait_button, + post_move_cancel_button, + post_move_picker_back_button, + post_move_picker_cancel_move_button, + targeting_hint_back_button, + targeting_hint_cancel_move_button, + chapter_button, + shop_button, + talk_button, + armory_button, + roster_button, + formation_button, + save_button, + shop_buy_button, + shop_sell_button, + manual_save_button, + manual_load_button, + dialogue_previous_button, + dialogue_continue_button, + result_restart_button, + next_battle_button + ]: + _apply_button_style(hud_button) + _apply_button_style(begin_button, true) + func _create_audio() -> void: bgm_player = AudioStreamPlayer.new() @@ -3202,7 +3420,7 @@ func _update_result_panel() -> void: result_restart_button.visible = false if next_battle_button != null: next_battle_button.visible = false - result_label.text = "%s Complete\nAll current scenarios cleared.\nGold %d\nStart a new campaign to replay." % [ + result_label.text = "%s Chronicle Complete\nAll current scenarios cleared.\nGold %d\nStart a new campaign to replay." % [ campaign_state.campaign_title, campaign_state.gold ] @@ -3222,7 +3440,7 @@ func _update_result_panel() -> void: if result_restart_button != null: result_restart_button.visible = true _update_result_next_button_visibility() - result_label.text = "Victory\n%s\n%s" % [ + result_label.text = "Victory Proclamation\n%s\n%s" % [ state.objectives.get("victory", ""), _format_battle_result_summary() ] @@ -3234,7 +3452,7 @@ func _update_result_panel() -> void: result_restart_button.visible = true if next_battle_button != null: next_battle_button.visible = false - result_label.text = "Defeat\n%s" % state.objectives.get("defeat", "") + result_label.text = "Defeat Report\n%s" % state.objectives.get("defeat", "") else: _clear_result_choices() result_panel.visible = false @@ -3263,7 +3481,7 @@ func _on_log_added(message: String) -> void: func _on_objective_updated(victory: String, defeat: String) -> void: - var notice_lines := ["Objective Updated"] + var notice_lines := ["New War Order"] if not victory.is_empty(): notice_lines.append(victory) if not defeat.is_empty(): @@ -3989,27 +4207,27 @@ func _format_briefing_objectives() -> String: var text := "" var victory := str(state.objectives.get("victory", "")) if not victory.is_empty(): - text = "Victory: %s" % victory + text = "War Edict: %s" % victory var defeat := str(state.objectives.get("defeat", "")) if not defeat.is_empty(): if not text.is_empty(): text += "\n" - text += "Defeat: %s" % defeat + text += "Ruin If: %s" % defeat var progress_lines := state.get_objective_progress_lines(false, false) if not progress_lines.is_empty(): if not text.is_empty(): text += "\n" - text += "Progress: %s" % _join_strings(progress_lines, ", ") + text += "War Report: %s" % _join_strings(progress_lines, ", ") var risk_lines := state.get_defeat_progress_lines() if not risk_lines.is_empty(): if not text.is_empty(): text += "\n" - text += "Risk: %s" % _join_strings(risk_lines, ", ") + text += "Ill Omen: %s" % _join_strings(risk_lines, ", ") var rewards_text := _format_briefing_rewards() if not rewards_text.is_empty(): if not text.is_empty(): text += "\n" - text += "Rewards: %s" % rewards_text + text += "Spoils: %s" % rewards_text return text @@ -4250,6 +4468,7 @@ func _rebuild_chapter_menu() -> void: if entries.is_empty(): var empty_label := Label.new() empty_label.text = "No campaign chapters are defined." + _apply_label_style(empty_label, UI_AGED_INK) chapter_list.add_child(empty_label) return for entry in entries: @@ -4272,6 +4491,7 @@ func _rebuild_chapter_menu() -> void: chapter_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART chapter_label.custom_minimum_size = Vector2(560, 0) chapter_label.text = line + _apply_label_style(chapter_label, UI_AGED_INK) chapter_list.add_child(chapter_label) for scenario in entry.get("scenarios", []): _add_chapter_scenario_entry(scenario) @@ -4291,6 +4511,7 @@ func _add_chapter_scenario_entry(scenario: Dictionary) -> void: var scenario_button := Button.new() scenario_button.custom_minimum_size = Vector2(560, 0) scenario_button.text = text + _apply_button_style(scenario_button) scenario_button.pressed.connect(_on_chapter_scenario_pressed.bind(scenario_id)) chapter_list.add_child(scenario_button) else: @@ -4298,6 +4519,7 @@ func _add_chapter_scenario_entry(scenario: Dictionary) -> void: scenario_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART scenario_label.custom_minimum_size = Vector2(560, 0) scenario_label.text = text + _apply_label_style(scenario_label, UI_AGED_INK) chapter_list.add_child(scenario_label) @@ -4379,6 +4601,7 @@ func _rebuild_talk_menu() -> void: if conversations.is_empty(): var empty_label := Label.new() empty_label.text = "No camp conversations" + _apply_label_style(empty_label, UI_AGED_INK) talk_list.add_child(empty_label) return for conversation in conversations: @@ -4403,6 +4626,7 @@ func _add_camp_conversation_row(conversation: Dictionary) -> void: button.text = _format_camp_conversation_row_title(conversation) button.custom_minimum_size = Vector2(500, 32) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _apply_button_style(button) button.pressed.connect(_on_camp_conversation_pressed.bind(conversation_id)) column.add_child(button) @@ -4410,12 +4634,14 @@ func _add_camp_conversation_row(conversation: Dictionary) -> void: preview_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART preview_label.custom_minimum_size = Vector2(500, 24) preview_label.text = _format_camp_conversation_preview_text(conversation) + _apply_label_style(preview_label, UI_AGED_INK) column.add_child(preview_label) func _make_camp_conversation_portrait(conversation: Dictionary) -> PanelContainer: var portrait_panel := PanelContainer.new() portrait_panel.custom_minimum_size = CAMP_TALK_PORTRAIT_SIZE + _apply_panel_style(portrait_panel, "portrait") var stack := Control.new() stack.custom_minimum_size = CAMP_TALK_PORTRAIT_SIZE @@ -4432,6 +4658,7 @@ func _make_camp_conversation_portrait(conversation: Dictionary) -> PanelContaine initials_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER initials_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER initials_label.add_theme_font_size_override("font_size", 18) + _apply_label_style(initials_label, UI_PARCHMENT_TEXT) stack.add_child(initials_label) var speaker := _camp_conversation_preview_speaker(conversation) @@ -4914,6 +5141,7 @@ func _rebuild_roster_menu() -> void: if units.is_empty(): var empty_label := Label.new() empty_label.text = "No officers" + _apply_label_style(empty_label, UI_AGED_INK) roster_list.add_child(empty_label) return @@ -4921,6 +5149,7 @@ func _rebuild_roster_menu() -> void: var unit_id := str(unit.get("id", "")) var roster_unit_button := Button.new() roster_unit_button.text = _format_roster_unit_button_text(unit) + _apply_button_style(roster_unit_button) var deployed := bool(unit.get("deployed", true)) roster_unit_button.disabled = ( bool(unit.get("required_deployment", false)) @@ -4987,6 +5216,7 @@ func _rebuild_formation_menu() -> void: if units.is_empty(): var empty_label := Label.new() empty_label.text = "No deployed officers" + _apply_label_style(empty_label, UI_AGED_INK) formation_list.add_child(empty_label) return @@ -4994,6 +5224,7 @@ func _rebuild_formation_menu() -> void: var unit_id := str(unit.get("id", "")) var unit_button := Button.new() unit_button.text = _format_formation_unit_button_text(unit) + _apply_button_style(unit_button) unit_button.disabled = unit_id == formation_unit_id unit_button.pressed.connect(_on_formation_unit_pressed.bind(unit_id)) formation_list.add_child(unit_button) @@ -5055,17 +5286,20 @@ func _rebuild_armory_menu() -> void: if units.is_empty(): var empty_label := Label.new() empty_label.text = "No deployed officers" + _apply_label_style(empty_label, UI_AGED_INK) armory_list.add_child(empty_label) return var unit_title := Label.new() unit_title.text = "Officers" + _apply_label_style(unit_title, UI_CINNABAR, 14) armory_list.add_child(unit_title) for unit in units: var unit_id := str(unit.get("id", "")) var unit_button := Button.new() unit_button.text = _format_armory_unit_button_text(unit) + _apply_button_style(unit_button) unit_button.disabled = unit_id == armory_unit_id unit_button.pressed.connect(_on_armory_unit_pressed.bind(unit_id)) armory_list.add_child(unit_button) @@ -5077,12 +5311,14 @@ func _rebuild_armory_menu() -> void: var equipment_label := Label.new() equipment_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART equipment_label.text = "Equipped: %s" % _format_unit_equipment(selected) + _apply_label_style(equipment_label, UI_AGED_INK) armory_list.add_child(equipment_label) var equipped_slots := state.get_equipped_equipment_slots(armory_unit_id) if not equipped_slots.is_empty(): var unequip_title := Label.new() unequip_title.text = "Unequip" + _apply_label_style(unequip_title, UI_CINNABAR, 14) armory_list.add_child(unequip_title) var equipment := state.get_equipment_snapshot(armory_unit_id) for slot in equipped_slots: @@ -5091,6 +5327,7 @@ func _rebuild_armory_menu() -> void: var item := state.get_item_def(item_id) var unequip_button := Button.new() unequip_button.text = _format_equipment_unequip_text(selected, normalized_slot, item_id, item) + _apply_button_style(unequip_button) unequip_button.pressed.connect(_on_armory_unequip_pressed.bind(normalized_slot)) armory_list.add_child(unequip_button) @@ -5098,6 +5335,7 @@ func _rebuild_armory_menu() -> void: if item_ids.is_empty(): var empty_equipment_label := Label.new() empty_equipment_label.text = "No compatible equipment in inventory" + _apply_label_style(empty_equipment_label, UI_AGED_INK) armory_list.add_child(empty_equipment_label) else: var inventory := state.get_inventory_snapshot() @@ -5111,6 +5349,7 @@ func _rebuild_armory_menu() -> void: item, int(inventory.get(normalized_id, 0)) ) + _apply_button_style(armory_equip_button) armory_equip_button.pressed.connect(_on_armory_equip_pressed.bind(normalized_id)) armory_list.add_child(armory_equip_button) @@ -5214,6 +5453,7 @@ func _rebuild_shop_buy_list() -> void: if item_ids.is_empty(): var empty_label := Label.new() empty_label.text = "No shop stock" + _apply_label_style(empty_label, UI_AGED_INK) shop_list.add_child(empty_label) return @@ -5233,6 +5473,7 @@ func _rebuild_shop_buy_list() -> void: buy_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL buy_button.tooltip_text = _format_shop_item_tooltip_text(_format_shop_item_button_text(normalized_id, item, price, owned, stock_limit, remaining), detail_text) buy_button.disabled = campaign_state.gold < price or (stock_limit >= 0 and remaining <= 0) + _apply_button_style(buy_button) buy_button.pressed.connect(_on_shop_item_pressed.bind(normalized_id)) _add_shop_item_row(normalized_id, item, buy_button, detail_text) @@ -5242,6 +5483,7 @@ func _rebuild_shop_sell_list() -> void: if item_ids.is_empty(): var empty_label := Label.new() empty_label.text = "No sellable stock" + _apply_label_style(empty_label, UI_AGED_INK) shop_list.add_child(empty_label) return @@ -5257,6 +5499,7 @@ func _rebuild_shop_sell_list() -> void: sell_button.custom_minimum_size = Vector2(500, 32) sell_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL sell_button.tooltip_text = _format_shop_item_tooltip_text(_format_shop_sell_item_button_text(normalized_id, item, sale_price, owned), detail_text) + _apply_button_style(sell_button) sell_button.pressed.connect(_on_shop_sell_item_pressed.bind(normalized_id)) _add_shop_item_row(normalized_id, item, sell_button, detail_text) @@ -5284,6 +5527,7 @@ func _add_shop_merchant_notice() -> void: merchant_label.custom_minimum_size = Vector2(500, 0) merchant_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL merchant_label.text = notice_text + _apply_label_style(merchant_label, UI_AGED_INK) row.add_child(merchant_label) @@ -5306,11 +5550,13 @@ func _format_shop_merchant_notice_text(merchant: Dictionary) -> String: func _make_initials_badge(label_text: String, badge_size: Vector2, font_size: int) -> PanelContainer: var panel := PanelContainer.new() panel.custom_minimum_size = badge_size + _apply_panel_style(panel, "portrait") var label := Label.new() label.set_anchors_preset(Control.PRESET_FULL_RECT) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.add_theme_font_size_override("font_size", font_size) + _apply_label_style(label, UI_PARCHMENT_TEXT) label.text = _dialogue_portrait_initials(label_text) panel.add_child(label) return panel @@ -5335,12 +5581,14 @@ func _add_shop_item_row(item_id: String, item: Dictionary, action_button: Button detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART detail_label.custom_minimum_size = Vector2(500, 20) detail_label.text = detail_text + _apply_label_style(detail_label, UI_AGED_INK) column.add_child(detail_label) func _make_shop_item_icon_panel(item_id: String, item: Dictionary) -> PanelContainer: var icon_panel := PanelContainer.new() icon_panel.custom_minimum_size = SHOP_ITEM_ICON_SIZE + _apply_panel_style(icon_panel, "portrait") var stack := Control.new() stack.custom_minimum_size = SHOP_ITEM_ICON_SIZE @@ -5357,6 +5605,7 @@ func _make_shop_item_icon_panel(item_id: String, item: Dictionary) -> PanelConta initials_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER initials_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER initials_label.add_theme_font_size_override("font_size", 16) + _apply_label_style(initials_label, UI_PARCHMENT_TEXT) stack.add_child(initials_label) var texture := _load_art_texture(str(item.get("icon", ""))) @@ -5596,6 +5845,7 @@ func _rebuild_result_choices() -> void: continue var choice_button := Button.new() choice_button.text = _format_post_battle_choice_text(choice) + _apply_button_style(choice_button, true) choice_button.pressed.connect(_on_post_battle_choice_pressed.bind(choice)) result_choice_list.add_child(choice_button) @@ -5662,7 +5912,7 @@ func _on_post_battle_choice_pressed(choice: Dictionary) -> void: _rebuild_result_choices() _update_result_next_button_visibility() if result_label != null and state.battle_status == BattleState.STATUS_VICTORY: - result_label.text = "Victory\n%s\n%s" % [ + result_label.text = "Victory Proclamation\n%s\n%s" % [ state.objectives.get("victory", ""), _format_battle_result_summary() ] @@ -6222,6 +6472,7 @@ func _rebuild_post_move_tactic_picker(selected: Dictionary) -> void: if skill_ids.is_empty(): var empty_label := Label.new() empty_label.text = "No tactics" + _apply_label_style(empty_label, UI_PARCHMENT_TEXT) post_move_picker_list.add_child(empty_label) return for skill_id_value in skill_ids: @@ -6232,6 +6483,7 @@ func _rebuild_post_move_tactic_picker(selected: Dictionary) -> void: var disabled_reason := _post_move_tactic_disabled_reason(selected, skill_id, skill) skill_button.disabled = not disabled_reason.is_empty() skill_button.tooltip_text = disabled_reason if not disabled_reason.is_empty() else "Choose this tactic." + _apply_button_style(skill_button) skill_button.pressed.connect(_on_post_move_tactic_option_pressed.bind(skill_id)) post_move_picker_list.add_child(skill_button) @@ -6241,6 +6493,7 @@ func _rebuild_post_move_item_picker(selected: Dictionary) -> void: if item_ids.is_empty(): var empty_label := Label.new() empty_label.text = "No items" + _apply_label_style(empty_label, UI_PARCHMENT_TEXT) post_move_picker_list.add_child(empty_label) return var inventory := state.get_inventory_snapshot() @@ -6254,6 +6507,7 @@ func _rebuild_post_move_item_picker(selected: Dictionary) -> void: var disabled_reason := _post_move_item_disabled_reason(selected, item_id, count) item_button.disabled = not disabled_reason.is_empty() item_button.tooltip_text = disabled_reason if not disabled_reason.is_empty() else "Choose this item." + _apply_button_style(item_button) item_button.pressed.connect(_on_post_move_item_option_pressed.bind(item_id)) post_move_picker_list.add_child(item_button) @@ -6652,12 +6906,14 @@ func _rebuild_tactic_menu(selected: Dictionary) -> void: if skill_ids.is_empty(): var empty_label := Label.new() empty_label.text = "No tactics" + _apply_label_style(empty_label, UI_PARCHMENT_TEXT) tactic_list.add_child(empty_label) return var skill_sealed := state.is_unit_skill_sealed(selected["id"]) if skill_sealed: var sealed_label := Label.new() sealed_label.text = "Tactics sealed" + _apply_label_style(sealed_label, UI_PARCHMENT_TEXT) tactic_list.add_child(sealed_label) for skill_id in skill_ids: @@ -6665,11 +6921,13 @@ func _rebuild_tactic_menu(selected: Dictionary) -> void: var skill_button := Button.new() skill_button.text = _format_tactic_button_text(str(skill_id), skill, selected) skill_button.disabled = skill_sealed or selected.get("acted", false) or _is_input_locked() or int(selected.get("mp", 0)) < int(skill.get("mp_cost", 0)) + _apply_button_style(skill_button) skill_button.pressed.connect(_on_tactic_skill_pressed.bind(str(skill_id))) tactic_list.add_child(skill_button) var cancel_button := Button.new() cancel_button.text = "Cancel" + _apply_button_style(cancel_button) cancel_button.pressed.connect(_on_tactic_cancel_pressed) tactic_list.add_child(cancel_button) @@ -6832,6 +7090,7 @@ func _rebuild_item_menu(selected: Dictionary) -> void: if item_ids.is_empty(): var empty_label := Label.new() empty_label.text = "No items" + _apply_label_style(empty_label, UI_PARCHMENT_TEXT) item_list.add_child(empty_label) return @@ -6844,11 +7103,13 @@ func _rebuild_item_menu(selected: Dictionary) -> void: use_button.text = _format_item_button_text(normalized_id, item, count) _apply_item_button_icon(use_button, item) use_button.disabled = selected.get("acted", false) or _is_input_locked() or count <= 0 + _apply_button_style(use_button) use_button.pressed.connect(_on_item_selected_pressed.bind(normalized_id)) item_list.add_child(use_button) var cancel_button := Button.new() cancel_button.text = "Cancel" + _apply_button_style(cancel_button) cancel_button.pressed.connect(_on_item_cancel_pressed) item_list.add_child(cancel_button) @@ -6954,12 +7215,14 @@ func _rebuild_equip_menu(selected: Dictionary) -> void: var equipment_label := Label.new() equipment_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART equipment_label.text = "Equipped: %s" % _format_unit_equipment(selected) + _apply_label_style(equipment_label, UI_PARCHMENT_TEXT) equip_list.add_child(equipment_label) var equipped_slots := state.get_equipped_equipment_slots(str(selected["id"])) if not equipped_slots.is_empty(): var unequip_title := Label.new() unequip_title.text = "Unequip" + _apply_label_style(unequip_title, UI_OLD_BRONZE, 14) equip_list.add_child(unequip_title) var equipment := state.get_equipment_snapshot(str(selected["id"])) for slot in equipped_slots: @@ -6970,6 +7233,7 @@ func _rebuild_equip_menu(selected: Dictionary) -> void: unequip_button.text = _format_equipment_unequip_text(selected, normalized_slot, item_id, item) _apply_item_button_icon(unequip_button, item) unequip_button.disabled = _is_input_locked() + _apply_button_style(unequip_button) unequip_button.pressed.connect(_on_equip_unequip_pressed.bind(normalized_slot)) equip_list.add_child(unequip_button) @@ -6977,6 +7241,7 @@ func _rebuild_equip_menu(selected: Dictionary) -> void: if item_ids.is_empty(): var empty_label := Label.new() empty_label.text = "No compatible equipment" + _apply_label_style(empty_label, UI_PARCHMENT_TEXT) equip_list.add_child(empty_label) else: var inventory := state.get_inventory_snapshot() @@ -6992,11 +7257,13 @@ func _rebuild_equip_menu(selected: Dictionary) -> void: ) _apply_item_button_icon(equip_item_button, item) equip_item_button.disabled = _is_input_locked() + _apply_button_style(equip_item_button) equip_item_button.pressed.connect(_on_equip_item_pressed.bind(normalized_id)) equip_list.add_child(equip_item_button) var cancel_button := Button.new() cancel_button.text = "Cancel" + _apply_button_style(cancel_button) cancel_button.pressed.connect(_on_equip_cancel_pressed) equip_list.add_child(cancel_button) diff --git a/tools/smoke_camp_conversation_rewards.gd b/tools/smoke_camp_conversation_rewards.gd index 5e2269c..2d0fae6 100644 --- a/tools/smoke_camp_conversation_rewards.gd +++ b/tools/smoke_camp_conversation_rewards.gd @@ -42,6 +42,10 @@ const YAN_FORD_SCENARIO_ID := "011_yan_ford_pursuit" const YAN_FORD_SCENARIO_PATH := "res://data/scenarios/011_yan_ford_pursuit.json" const YAN_FORD_FORTIFIED_CONVERSATION_ID := "yan_ford_fortified_crossing_medicine" const YAN_FORD_RAIDED_CONVERSATION_ID := "yan_ford_raided_supply_beans" +const GUANDU_SCENARIO_ID := "012_guandu_showdown" +const GUANDU_SCENARIO_PATH := "res://data/scenarios/012_guandu_showdown.json" +const GUANDU_SECURED_CONVERSATION_ID := "guandu_secured_line_medicine" +const GUANDU_SCOUTED_CONVERSATION_ID := "guandu_wuchao_scout_wine" const WUCHAO_SCENARIO_ID := "013_wuchao_raid" const WUCHAO_SCENARIO_PATH := "res://data/scenarios/013_wuchao_raid.json" const SAVE_PATH := "user://campaign_save.json" @@ -63,6 +67,8 @@ func _init() -> void: _check_xiapi_branch_camp_conversations(failures) _check_white_horse_branch_camp_conversations(failures) _check_yan_ford_branch_camp_conversations(failures) + _check_guandu_branch_camp_conversations(failures) + _check_guandu_post_battle_choice_bridges_to_wuchao(failures) _check_manual_checkpoint_reverts_claim(failures) _check_completed_replay_cannot_claim(failures) _check_conditional_camp_conversations(failures) @@ -760,6 +766,181 @@ func _check_yan_ford_branch_camp_conversations(failures: Array[String]) -> void: raided_scene.free() +func _check_guandu_branch_camp_conversations(failures: Array[String]) -> void: + var base_scene = _new_prebattle_scene_for(failures, "Guandu base camp", GUANDU_SCENARIO_ID, GUANDU_SCENARIO_PATH) + if base_scene != null: + if base_scene._camp_conversation_by_id("guandu_main_line_council").is_empty(): + failures.append("Guandu should expose the main line council conversation") + if base_scene._camp_conversation_by_id("guandu_guo_jia_supply_wound").is_empty(): + failures.append("Guandu should expose Guo Jia's supply wound conversation") + if not base_scene._camp_conversation_by_id(GUANDU_SECURED_CONVERSATION_ID).is_empty(): + failures.append("Guandu secured line medicine should be hidden without secured line flag") + if not base_scene._camp_conversation_by_id(GUANDU_SCOUTED_CONVERSATION_ID).is_empty(): + failures.append("Guandu Wuchao scout wine should be hidden without scouted Wuchao flag") + var merchant: Dictionary = base_scene.state.get_shop_merchant() + if str(merchant.get("name", "")) != "Guandu Camp Sutler": + failures.append("Guandu shop merchant name should be present") + var merchant_lines: Array = merchant.get("lines", []) + if merchant_lines.size() < 2: + failures.append("Guandu shop merchant should expose flavor lines") + if base_scene._format_talk_status_text(base_scene._camp_conversation_entries()) != "Camp conversations | 2 topics": + failures.append("Guandu base talk status should summarize two non-supply topics") + base_scene.free() + + var secured_scene = _new_prebattle_scene_for( + failures, + "Guandu secured line camp", + GUANDU_SCENARIO_ID, + GUANDU_SCENARIO_PATH, + {"secured_guandu_line": true} + ) + if secured_scene != null: + var medicine: Dictionary = secured_scene._camp_conversation_by_id(GUANDU_SECURED_CONVERSATION_ID) + if medicine.is_empty(): + failures.append("secured Guandu line flag should expose Guandu secured line medicine") + if not secured_scene._camp_conversation_by_id(GUANDU_SCOUTED_CONVERSATION_ID).is_empty(): + failures.append("secured Guandu line flag should not expose Guandu Wuchao scout wine") + if not medicine.is_empty(): + if secured_scene._format_talk_status_text(secured_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready": + failures.append("Guandu secured line talk status should show one ready supply") + if not secured_scene._format_camp_conversation_button_text(medicine).contains("Supply Panacea"): + failures.append("Guandu secured line medicine button should preview Panacea supply") + var before_panacea := int(secured_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) + secured_scene._apply_camp_conversation_effects(medicine) + if int(secured_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1: + failures.append("Guandu secured line medicine should add Panacea to campaign inventory") + if int(secured_scene.state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1: + failures.append("Guandu secured line medicine should refresh battle Panacea inventory") + if not secured_scene.campaign_state.has_claimed_camp_conversation_effects(GUANDU_SCENARIO_ID, GUANDU_SECURED_CONVERSATION_ID): + failures.append("Guandu secured line 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("Guandu secured line talk status should show claimed supply after claim") + secured_scene.free() + + var scouted_scene = _new_prebattle_scene_for( + failures, + "Guandu scouted Wuchao camp", + GUANDU_SCENARIO_ID, + GUANDU_SCENARIO_PATH, + {"scouted_wuchao_depots": true} + ) + if scouted_scene != null: + var wine: Dictionary = scouted_scene._camp_conversation_by_id(GUANDU_SCOUTED_CONVERSATION_ID) + if wine.is_empty(): + failures.append("scouted Wuchao flag should expose Guandu Wuchao scout wine") + if not scouted_scene._camp_conversation_by_id(GUANDU_SECURED_CONVERSATION_ID).is_empty(): + failures.append("scouted Wuchao flag should not expose Guandu secured line medicine") + if not wine.is_empty(): + if scouted_scene._format_talk_status_text(scouted_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 supplies ready": + failures.append("Guandu scouted Wuchao talk status should show one ready supply") + if not scouted_scene._format_camp_conversation_button_text(wine).contains("Supply Wine"): + failures.append("Guandu Wuchao scout wine button should preview Wine supply") + var before_wine := int(scouted_scene.campaign_state.get_inventory_snapshot().get("wine", 0)) + scouted_scene._apply_camp_conversation_effects(wine) + if int(scouted_scene.campaign_state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1: + failures.append("Guandu Wuchao scout wine should add Wine to campaign inventory") + if int(scouted_scene.state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1: + failures.append("Guandu Wuchao scout wine should refresh battle Wine inventory") + if not scouted_scene.campaign_state.has_claimed_camp_conversation_effects(GUANDU_SCENARIO_ID, GUANDU_SCOUTED_CONVERSATION_ID): + failures.append("Guandu Wuchao scout wine claim should be saved") + if scouted_scene._format_talk_status_text(scouted_scene._camp_conversation_entries()) != "Camp conversations | 3 topics | 1 claimed": + failures.append("Guandu scouted Wuchao talk status should show claimed supply after claim") + scouted_scene.free() + + +func _check_guandu_post_battle_choice_bridges_to_wuchao(failures: Array[String]) -> void: + _check_guandu_choice_branch_to_wuchao( + failures, + "prepare_wuchao_raid", + "prepared_wuchao_raid", + "strengthened_guandu_defense", + "wuchao_fast_raid_supplies", + "wuchao_guandu_reserve_wagon", + "war_drum", + "imperial_seal" + ) + _check_guandu_choice_branch_to_wuchao( + failures, + "strengthen_guandu_defense", + "strengthened_guandu_defense", + "prepared_wuchao_raid", + "wuchao_guandu_reserve_wagon", + "wuchao_fast_raid_supplies", + "imperial_seal", + "war_drum" + ) + + +func _check_guandu_choice_branch_to_wuchao( + failures: Array[String], + 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: + var scene = _new_prebattle_scene_for(failures, "Guandu choice bridge %s" % choice_id, GUANDU_SCENARIO_ID, GUANDU_SCENARIO_PATH) + if scene == null: + 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("Guandu choice bridge %s should save 012 victory" % choice_id) + scene.free() + return + if bool(result.get("choice_applied", true)): + failures.append("Guandu choice bridge %s should wait for a selected post-battle choice" % choice_id) + if scene.campaign_state.pending_post_battle_choice_scenario_id != GUANDU_SCENARIO_ID: + failures.append("Guandu choice bridge %s should mark Guandu choice pending" % choice_id) + var choice := _find_choice_by_id(result.get("post_battle_choices", []), choice_id) + if choice.is_empty(): + failures.append("Guandu choice bridge missing choice: %s" % choice_id) + scene.free() + return + if not scene.campaign_state.try_apply_post_battle_choice(choice, GUANDU_SCENARIO_ID): + failures.append("Guandu 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("Guandu choice bridge %s should set %s" % [choice_id, expected_flag]) + if flags.get(cleared_flag, true) != false: + failures.append("Guandu choice bridge %s should clear %s" % [choice_id, cleared_flag]) + if scene.campaign_state.current_scenario_id != WUCHAO_SCENARIO_ID: + failures.append("Guandu choice bridge %s should advance to Wuchao, got %s" % [choice_id, scene.campaign_state.current_scenario_id]) + if not scene.state.load_battle( + WUCHAO_SCENARIO_PATH, + scene.campaign_state.get_roster_overrides(), + scene.campaign_state.get_inventory_snapshot(), + flags, + scene.campaign_state.get_joined_officers_snapshot() + ): + failures.append("Guandu choice bridge %s could not load Wuchao" % choice_id) + scene.free() + return + if scene._camp_conversation_by_id(expected_conversation_id).is_empty(): + failures.append("Guandu 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("Guandu 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("Guandu choice bridge %s should expose shop item %s" % [choice_id, expected_shop_item_id]) + if shop_items.has(hidden_shop_item_id): + failures.append("Guandu 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 {} + for choice in choices: + if typeof(choice) == TYPE_DICTIONARY and str(choice.get("id", "")) == choice_id: + return choice + return {} + + func _check_manual_checkpoint_reverts_claim(failures: Array[String]) -> void: var scene = _new_prebattle_scene(failures, "manual checkpoint") if scene == null: diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index dd848eb..e745b7e 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -43,6 +43,7 @@ func _init() -> void: _check_battle_visual_data(failures) _check_scene_texture_loading(failures) _check_hud_focus_text(failures) + _check_ancient_ui_theme(failures) _check_hover_intent_badges(failures) _check_action_button_disabled_reasons(failures) _check_terrain_and_unit_presentation(failures) @@ -599,24 +600,112 @@ func _check_battle_visual_data(failures: Array[String]) -> void: failures.append("could not load Guandu Showdown shop data") else: _check_shop_items_unique(failures, guandu_state, "012 base") + var guandu_conversations: Array = guandu_state.get_briefing().get("camp_conversations", []) + if guandu_conversations.size() != 2: + failures.append("012 base briefing should expose exactly two camp conversations") + else: + _check_camp_conversation(failures, guandu_conversations[0], "guandu_main_line_council", "officer", "cao_cao") + _check_camp_conversation(failures, guandu_conversations[1], "guandu_guo_jia_supply_wound", "officer", "guo_jia") + var guandu_merchant := guandu_state.get_shop_merchant() + if str(guandu_merchant.get("name", "")) != "Guandu Camp Sutler": + failures.append("012 shop should expose Guandu merchant name") + if (guandu_merchant.get("lines", []) as Array).size() < 2: + failures.append("012 shop should expose merchant flavor lines") _check_shop_item_visibility(failures, guandu_state, "imperial_seal", false, "012 base") _check_shop_item_visibility(failures, guandu_state, "war_drum", false, "012 base") + _check_shop_item_visibility(failures, guandu_state, "yitian_sword", false, "012 base") + _check_shop_item_visibility(failures, guandu_state, "panacea", false, "012 base") + _check_shop_item_visibility(failures, guandu_state, "antidote", false, "012 base") var guandu_secured_state = BattleStateScript.new() if not guandu_secured_state.load_battle("res://data/scenarios/012_guandu_showdown.json", {}, {}, {"secured_guandu_line": true}): failures.append("could not load Guandu secured line shop data") else: _check_shop_items_unique(failures, guandu_secured_state, "012 secured Guandu line") - _check_shop_item_visibility(failures, guandu_secured_state, "imperial_seal", true, "012 secured Guandu line") + var line_medicine: Dictionary = _find_camp_conversation( + guandu_secured_state.get_briefing().get("camp_conversations", []), + "guandu_secured_line_medicine" + ) + if line_medicine.is_empty(): + failures.append("012 secured Guandu line flag should expose secured line medicine conversation") + else: + _check_camp_conversation_effect(failures, line_medicine, "panacea", 1) + if not _find_camp_conversation( + guandu_secured_state.get_briefing().get("camp_conversations", []), + "guandu_wuchao_scout_wine" + ).is_empty(): + failures.append("012 secured Guandu line flag should not expose Wuchao scout wine conversation") + _check_shop_item_visibility(failures, guandu_secured_state, "imperial_seal", false, "012 secured Guandu line") _check_shop_item_visibility(failures, guandu_secured_state, "war_drum", false, "012 secured Guandu line") + _check_shop_item_visibility(failures, guandu_secured_state, "yitian_sword", false, "012 secured Guandu line") + _check_shop_item_visibility(failures, guandu_secured_state, "panacea", true, "012 secured Guandu line") + _check_shop_item_visibility(failures, guandu_secured_state, "antidote", false, "012 secured Guandu line") var guandu_scouted_state = BattleStateScript.new() if not guandu_scouted_state.load_battle("res://data/scenarios/012_guandu_showdown.json", {}, {}, {"scouted_wuchao_depots": true}): failures.append("could not load Guandu scouted Wuchao shop data") else: _check_shop_items_unique(failures, guandu_scouted_state, "012 scouted Wuchao") + var scout_wine: Dictionary = _find_camp_conversation( + guandu_scouted_state.get_briefing().get("camp_conversations", []), + "guandu_wuchao_scout_wine" + ) + if scout_wine.is_empty(): + failures.append("012 scouted Wuchao flag should expose Wuchao scout wine conversation") + else: + _check_camp_conversation_effect(failures, scout_wine, "wine", 1) + if not _find_camp_conversation( + guandu_scouted_state.get_briefing().get("camp_conversations", []), + "guandu_secured_line_medicine" + ).is_empty(): + failures.append("012 scouted Wuchao flag should not expose secured line medicine conversation") _check_shop_item_visibility(failures, guandu_scouted_state, "imperial_seal", false, "012 scouted Wuchao") - _check_shop_item_visibility(failures, guandu_scouted_state, "war_drum", true, "012 scouted Wuchao") + _check_shop_item_visibility(failures, guandu_scouted_state, "war_drum", false, "012 scouted Wuchao") + _check_shop_item_visibility(failures, guandu_scouted_state, "yitian_sword", false, "012 scouted Wuchao") + _check_shop_item_visibility(failures, guandu_scouted_state, "panacea", false, "012 scouted Wuchao") + _check_shop_item_visibility(failures, guandu_scouted_state, "antidote", true, "012 scouted Wuchao") + + var wuchao_state = BattleStateScript.new() + if not wuchao_state.load_battle("res://data/scenarios/013_wuchao_raid.json"): + failures.append("could not load Wuchao Raid shop data") + else: + _check_shop_items_unique(failures, wuchao_state, "013 base") + _check_shop_item_visibility(failures, wuchao_state, "war_drum", false, "013 base") + _check_shop_item_visibility(failures, wuchao_state, "imperial_seal", false, "013 base") + + var wuchao_prepared_state = BattleStateScript.new() + if not wuchao_prepared_state.load_battle("res://data/scenarios/013_wuchao_raid.json", {}, {}, {"prepared_wuchao_raid": true}): + failures.append("could not load Wuchao prepared raid shop data") + else: + _check_shop_items_unique(failures, wuchao_prepared_state, "013 prepared Wuchao raid") + _check_shop_item_visibility(failures, wuchao_prepared_state, "war_drum", true, "013 prepared Wuchao raid") + _check_shop_item_visibility(failures, wuchao_prepared_state, "imperial_seal", false, "013 prepared Wuchao raid") + + var wuchao_strengthened_state = BattleStateScript.new() + if not wuchao_strengthened_state.load_battle("res://data/scenarios/013_wuchao_raid.json", {}, {}, {"strengthened_guandu_defense": true}): + failures.append("could not load Wuchao strengthened Guandu shop data") + else: + _check_shop_items_unique(failures, wuchao_strengthened_state, "013 strengthened Guandu defense") + _check_shop_item_visibility(failures, wuchao_strengthened_state, "war_drum", false, "013 strengthened Guandu defense") + _check_shop_item_visibility(failures, wuchao_strengthened_state, "imperial_seal", true, "013 strengthened Guandu defense") + + var wuchao_old_flags_state = BattleStateScript.new() + if not wuchao_old_flags_state.load_battle("res://data/scenarios/013_wuchao_raid.json", {}, {}, {"secured_guandu_line": true, "scouted_wuchao_depots": true}): + failures.append("could not load Wuchao old Guandu flag shop data") + else: + _check_shop_items_unique(failures, wuchao_old_flags_state, "013 old Guandu flags") + _check_shop_item_visibility(failures, wuchao_old_flags_state, "war_drum", false, "013 old Guandu flags") + _check_shop_item_visibility(failures, wuchao_old_flags_state, "imperial_seal", false, "013 old Guandu flags") + if not _find_camp_conversation( + wuchao_old_flags_state.get_briefing().get("camp_conversations", []), + "wuchao_fast_raid_supplies" + ).is_empty(): + failures.append("013 old Guandu flags should not expose fast raid supplies") + if not _find_camp_conversation( + wuchao_old_flags_state.get_briefing().get("camp_conversations", []), + "wuchao_guandu_reserve_wagon" + ).is_empty(): + failures.append("013 old Guandu flags should not expose reserve wagon supplies") for item_id in ["bronze_sword", "training_spear", "short_bow", "hand_axe", "iron_armor", "panacea"]: var item := state.get_item_def(item_id) @@ -782,6 +871,45 @@ func _check_hud_focus_text(failures: Array[String]) -> void: scene.free() +func _check_ancient_ui_theme(failures: Array[String]) -> void: + var scene = BattleSceneScript.new() + scene._create_hud() + if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): + failures.append("could not load battle scene state for ancient UI theme") + scene.free() + return + var briefing_text := scene._format_briefing_objectives() + 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)) + scene.free() + + +func _check_panel_style_fill(failures: Array[String], panel: PanelContainer, label: String, expected: Color) -> 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.bg_color, expected): + failures.append("%s fill mismatch: %s" % [label, str(style.bg_color)]) + + +func _colors_nearly_equal(left: Color, right: Color) -> bool: + return ( + absf(left.r - right.r) < 0.01 + and absf(left.g - right.g) < 0.01 + and absf(left.b - right.b) < 0.01 + and absf(left.a - right.a) < 0.01 + ) + + func _check_action_button_disabled_reasons(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene.wait_button = Button.new()