Add generated UI panel frames

This commit is contained in:
2026-06-20 03:13:34 +09:00
parent 4cda39f440
commit 5decb837fd
8 changed files with 160 additions and 7 deletions

View File

@@ -37,7 +37,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
- Pre-battle prep commands use compact icon buttons with active selection state and Korean hover labels for chapters, shop, talks, armory, roster, formation, and save records. - Pre-battle prep commands use compact icon buttons with active selection state and Korean hover labels for chapters, shop, talks, armory, roster, formation, and save records.
- Battle, top-toolbar, and pre-battle command buttons now prefer generated high-resolution bronze/parchment icon art under `art/ui/icons`, with procedural icon drawing kept only as a fallback. - Battle, top-toolbar, and pre-battle command buttons now prefer generated high-resolution bronze/parchment icon art under `art/ui/icons`, with procedural icon drawing kept only as a fallback.
- Battle maps blend generated terrain texture tiles for plains, forests, hills, wasteland, roads, water, villages, and castles over the high-resolution battlefield backdrop. - Battle maps blend generated terrain texture tiles for plains, forests, hills, wasteland, roads, water, villages, and castles over the high-resolution battlefield backdrop.
- Core UI panels now use a calmer ink/wood/jade palette with softer rounded corners and lighter button frames instead of the earlier yellow, hard-edged frame treatment. - Core UI panels now prefer generated high-resolution lacquer, scroll, jade, and command-seal frame textures under `art/ui/panels`, with the calmer ink/wood/jade flat styles kept as fallbacks instead of the earlier yellow, hard-edged frame treatment.
- Pre-battle Chapters overview shows story-arc progress with map thumbnail rows, compact status badges, hover details, and can open completed or current battles. - Pre-battle Chapters overview shows story-arc progress with map thumbnail rows, compact status badges, hover details, and can open completed or current battles.
- Pre-battle Save menu can write and load one manual campaign checkpoint separate from the automatic save, using compact seal-style slot rows and hover checkpoint details. - Pre-battle Save menu can write and load one manual campaign checkpoint separate from the automatic save, using compact seal-style slot rows and hover checkpoint details.
- The title screen exposes Start, Load, and Settings, and Load now lets the player choose between the automatic record and the manual checkpoint. - The title screen exposes Start, Load, and Settings, and Load now lets the player choose between the automatic record and the manual checkpoint.
@@ -130,7 +130,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
- Victory and defeat result overlay. - Victory and defeat result overlay.
- Dialogue portrait slot stays fixed across speaker and narration lines, with officer default image paths, optional per-line overrides, and speaker-initial or record fallback. - Dialogue portrait slot stays fixed across speaker and narration lines, with officer default image paths, optional per-line overrides, and speaker-initial or record fallback.
- Initial AI-generated photorealistic officer portraits for Cao Cao, Xiahou Dun, Xiahou Yuan, Cao Ren, Dian Wei, Guo Jia, and Zhang He. - Initial AI-generated photorealistic officer portraits for Cao Cao, Xiahou Dun, Xiahou Yuan, Cao Ren, Dian Wei, Guo Jia, and Zhang He.
- AI-generated scenario battlefield backgrounds, opening story panels, terrain textures, transparent class units, transparent generic-enemy units, item icons, and toolbar UI icons live under `art/backgrounds`, `art/story`, `art/terrain`, `art/units`, `art/items`, and `art/ui/icons`. - AI-generated scenario battlefield backgrounds, opening story panels, terrain textures, transparent class units, transparent generic-enemy units, item icons, toolbar UI icons, and generated UI panel frames live under `art/backgrounds`, `art/story`, `art/terrain`, `art/units`, `art/items`, `art/ui/icons`, and `art/ui/panels`.
- Battle HUD portrait thumbnails show the selected unit, or a hovered unit when nothing is selected. - Battle HUD portrait thumbnails show the selected unit, or a hovered unit when nothing is selected.
- Selected and hovered unit focus text shows combat role, movement type, current terrain move cost, terrain bonuses, core stats, and status effects. - Selected and hovered unit focus text shows combat role, movement type, current terrain move cost, terrain bonuses, core stats, and status effects.
- Dialogue lines can mark speaking side for text alignment inside a fixed visual-novel-style dialogue panel with stable portrait, progress, and previous-line controls. - Dialogue lines can mark speaking side for text alignment inside a fixed visual-novel-style dialogue panel with stable portrait, progress, and previous-line controls.

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 KiB

File diff suppressed because one or more lines are too long

View File

@@ -29,6 +29,7 @@ const BGM_BATTLE_PATH := "res://audio/bgm/battle_loop_placeholder.wav"
const TITLE_BACKGROUND_PATH := "res://art/backgrounds/title_war_camp_dawn.png" const TITLE_BACKGROUND_PATH := "res://art/backgrounds/title_war_camp_dawn.png"
const TITLE_PORTRAIT_PATH := "res://art/portraits/cao_cao.png" const TITLE_PORTRAIT_PATH := "res://art/portraits/cao_cao.png"
const TOOLBAR_ICON_PATH_TEMPLATE := "res://art/ui/icons/toolbar_%s.png" const TOOLBAR_ICON_PATH_TEMPLATE := "res://art/ui/icons/toolbar_%s.png"
const PANEL_TEXTURE_PATH_TEMPLATE := "res://art/ui/panels/panel_%s.png"
const TERRAIN_TEXTURE_PATH_TEMPLATE := "res://art/terrain/terrain_%s.png" const TERRAIN_TEXTURE_PATH_TEMPLATE := "res://art/terrain/terrain_%s.png"
const OPENING_PROLOGUE_REBELLION_PATH := "res://art/story/opening/yellow_turban_rebellion.png" const OPENING_PROLOGUE_REBELLION_PATH := "res://art/story/opening/yellow_turban_rebellion.png"
const OPENING_PROLOGUE_RESOLVE_PATH := "res://art/story/opening/cao_cao_resolves.png" const OPENING_PROLOGUE_RESOLVE_PATH := "res://art/story/opening/cao_cao_resolves.png"
@@ -692,6 +693,46 @@ func _make_panel_style(fill: Color, border: Color, border_width: int, radius: in
return style return style
func _make_panel_texture_style(variant: String, content_margin: int) -> StyleBoxTexture:
var spec := _panel_texture_spec(variant)
if spec.is_empty():
return null
var texture_key := str(spec.get("texture", ""))
var texture := _load_art_texture(PANEL_TEXTURE_PATH_TEMPLATE % texture_key)
if texture == null:
return null
var slice_margin := int(spec.get("slice_margin", 88))
var style := StyleBoxTexture.new()
style.texture = texture
style.draw_center = true
style.texture_margin_left = slice_margin
style.texture_margin_right = slice_margin
style.texture_margin_top = slice_margin
style.texture_margin_bottom = slice_margin
var resolved_margin: int = maxi(content_margin, int(spec.get("content_margin", content_margin)))
style.content_margin_left = resolved_margin
style.content_margin_right = resolved_margin
style.content_margin_top = resolved_margin
style.content_margin_bottom = resolved_margin
return style
func _panel_texture_spec(variant: String) -> Dictionary:
match variant:
"paper", "edict", "notice_edict", "speech_scroll":
return {"texture": "paper_scroll", "slice_margin": 108, "content_margin": 22}
"silk_map", "bamboo_slips", "hud_info", "target_hint", "caption", "edict_compact":
return {"texture": "hud_jade", "slice_margin": 46, "content_margin": 9}
"dialogue", "compact", "result_tablet":
return {"texture": "lacquer", "slice_margin": 96, "content_margin": 14}
"command_seal", "notice", "result_defeat_tablet":
return {"texture": "command_seal", "slice_margin": 72, "content_margin": 12}
"result_victory_tablet":
return {"texture": "hud_jade", "slice_margin": 92, "content_margin": 22}
_:
return {}
func _age_panel_style(style: StyleBoxFlat, variant: String, border_width: int) -> void: func _age_panel_style(style: StyleBoxFlat, variant: String, border_width: int) -> void:
if style == null: if style == null:
return return
@@ -872,7 +913,13 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void:
pass pass
var style := _make_panel_style(fill, border, border_width, radius, margin, shadow_size) var style := _make_panel_style(fill, border, border_width, radius, margin, shadow_size)
_age_panel_style(style, variant, border_width) _age_panel_style(style, variant, border_width)
var texture_style := _make_panel_texture_style(variant, margin)
if texture_style != null:
panel.add_theme_stylebox_override("panel", texture_style)
panel.set_meta("generated_panel_texture", true)
return
panel.add_theme_stylebox_override("panel", style) panel.add_theme_stylebox_override("panel", style)
panel.set_meta("generated_panel_texture", false)
func _make_button_style(fill: Color, border: Color, border_width: int) -> StyleBoxFlat: func _make_button_style(fill: Color, border: Color, border_width: int) -> StyleBoxFlat:

View File

@@ -92,6 +92,7 @@ func _init() -> void:
var failures: Array[String] = [] var failures: Array[String] = []
_check_battle_visual_data(failures) _check_battle_visual_data(failures)
_check_generated_toolbar_icons(failures) _check_generated_toolbar_icons(failures)
_check_generated_panel_textures(failures)
_check_generated_terrain_textures(failures) _check_generated_terrain_textures(failures)
_check_scene_texture_loading(failures) _check_scene_texture_loading(failures)
_check_hud_focus_text(failures) _check_hud_focus_text(failures)
@@ -2977,6 +2978,12 @@ func _check_generated_toolbar_icons(failures: Array[String]) -> void:
_check_toolbar_icon_path(failures, "res://art/ui/icons/toolbar_%s.png" % icon_kind, "toolbar icon %s" % icon_kind) _check_toolbar_icon_path(failures, "res://art/ui/icons/toolbar_%s.png" % icon_kind, "toolbar icon %s" % icon_kind)
func _check_generated_panel_textures(failures: Array[String]) -> void:
var panel_keys := ["lacquer", "paper_scroll", "hud_jade", "command_seal"]
for panel_key in panel_keys:
_check_panel_texture_path(failures, "res://art/ui/panels/panel_%s.png" % panel_key, "panel texture %s" % panel_key)
func _check_generated_terrain_textures(failures: Array[String]) -> void: func _check_generated_terrain_textures(failures: Array[String]) -> void:
var terrain_keys := ["g", "f", "h", "d", "r", "w", "t", "c"] var terrain_keys := ["g", "f", "h", "d", "r", "w", "t", "c"]
for terrain_key in terrain_keys: for terrain_key in terrain_keys:
@@ -3327,6 +3334,12 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void:
_check_panel_style_fill(failures, scene.dialogue_portrait_panel, "dialogue portrait panel", Color(0.025, 0.023, 0.020, 1.0)) _check_panel_style_fill(failures, scene.dialogue_portrait_panel, "dialogue portrait panel", Color(0.025, 0.023, 0.020, 1.0))
_check_panel_style_fill(failures, scene.dialogue_speaker_panel, "dialogue speaker seal panel", Color(0.26, 0.035, 0.030, 0.995)) _check_panel_style_fill(failures, scene.dialogue_speaker_panel, "dialogue speaker seal panel", Color(0.26, 0.035, 0.030, 0.995))
_check_panel_style_fill(failures, scene.dialogue_text_panel, "dialogue text scroll panel", Color(0.58, 0.55, 0.48, 0.996)) _check_panel_style_fill(failures, scene.dialogue_text_panel, "dialogue text scroll panel", Color(0.58, 0.55, 0.48, 0.996))
_check_panel_uses_generated_texture(failures, scene.briefing_panel, "briefing generated frame")
_check_panel_uses_generated_texture(failures, scene.briefing_objective_panel, "briefing objective generated frame")
_check_panel_uses_generated_texture(failures, scene.briefing_camp_overview_panel, "briefing map generated frame")
_check_panel_uses_generated_texture(failures, scene.dialogue_panel, "dialogue generated frame")
_check_panel_uses_generated_texture(failures, scene.dialogue_text_panel, "dialogue text generated frame")
_check_panel_uses_generated_texture(failures, scene.auto_end_turn_prompt_panel, "auto end turn generated frame")
if scene.screen_backdrop == null: if scene.screen_backdrop == null:
failures.append("ancient briefing screen should create an ink-wash backdrop") failures.append("ancient briefing screen should create an ink-wash backdrop")
else: else:
@@ -3356,6 +3369,9 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void:
_check_panel_style_frame(failures, scene.cell_info_panel, "HUD terrain info panel", Color(0.20, 0.24, 0.21, 0.96), 2) _check_panel_style_frame(failures, scene.cell_info_panel, "HUD terrain info panel", Color(0.20, 0.24, 0.21, 0.96), 2)
_check_panel_style_fill(failures, scene.forecast_panel, "HUD forecast info panel", Color(0.045, 0.050, 0.048, 0.90)) _check_panel_style_fill(failures, scene.forecast_panel, "HUD forecast info panel", Color(0.045, 0.050, 0.048, 0.90))
_check_panel_style_frame(failures, scene.forecast_panel, "HUD forecast info panel", Color(0.20, 0.24, 0.21, 0.96), 2) _check_panel_style_frame(failures, scene.forecast_panel, "HUD forecast info panel", Color(0.20, 0.24, 0.21, 0.96), 2)
_check_panel_uses_generated_texture(failures, scene.mission_detail_panel, "mission detail generated frame")
_check_panel_uses_generated_texture(failures, scene.cell_info_panel, "HUD terrain generated frame")
_check_panel_uses_generated_texture(failures, scene.forecast_panel, "HUD forecast generated frame")
if scene.cell_info_icon == null: if scene.cell_info_icon == null:
failures.append("HUD terrain info panel should expose an icon chip") failures.append("HUD terrain info panel should expose an icon chip")
if scene.forecast_icon == null: if scene.forecast_icon == null:
@@ -3487,6 +3503,7 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void:
failures.append("briefing shop sell button should avoid modern sale wording") failures.append("briefing shop sell button should avoid modern sale wording")
if scene.post_move_title_label == null or scene.post_move_title_label.text != "군령 선택": if scene.post_move_title_label == null or scene.post_move_title_label.text != "군령 선택":
failures.append("post-move command title should use order wording") failures.append("post-move command title should use order wording")
_check_panel_uses_generated_texture(failures, scene.post_move_menu, "post-move command generated frame")
_check_local_command_icon_button(failures, scene.post_move_attack_button, "타격령", "attack") _check_local_command_icon_button(failures, scene.post_move_attack_button, "타격령", "attack")
_check_local_command_icon_button(failures, scene.post_move_tactic_button, "책략첩", "tactic") _check_local_command_icon_button(failures, scene.post_move_tactic_button, "책략첩", "tactic")
_check_local_command_icon_button(failures, scene.post_move_item_button, "보급첩", "item") _check_local_command_icon_button(failures, scene.post_move_item_button, "보급첩", "item")
@@ -3506,8 +3523,10 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void:
failures.append("briefing map fallback should read as an old campaign map") failures.append("briefing map fallback should read as an old campaign map")
scene._apply_panel_style(scene.result_panel, "result_victory_tablet") scene._apply_panel_style(scene.result_panel, "result_victory_tablet")
_check_panel_style_fill(failures, scene.result_panel, "victory result panel", Color(0.27, 0.34, 0.28, 0.997)) _check_panel_style_fill(failures, scene.result_panel, "victory result panel", Color(0.27, 0.34, 0.28, 0.997))
_check_panel_uses_generated_texture(failures, scene.result_panel, "victory result generated frame")
scene._apply_panel_style(scene.result_panel, "result_defeat_tablet") scene._apply_panel_style(scene.result_panel, "result_defeat_tablet")
_check_panel_style_fill(failures, scene.result_panel, "defeat result panel", Color(0.10, 0.035, 0.028, 0.997)) _check_panel_style_fill(failures, scene.result_panel, "defeat result panel", Color(0.10, 0.035, 0.028, 0.997))
_check_panel_uses_generated_texture(failures, scene.result_panel, "defeat result generated frame")
if not scene._format_victory_result_text().contains("승리"): if not scene._format_victory_result_text().contains("승리"):
failures.append("victory result should use Korean title") failures.append("victory result should use Korean title")
if not scene._format_defeat_result_text().contains("패배"): if not scene._format_defeat_result_text().contains("패배"):
@@ -3628,9 +3647,13 @@ func _check_panel_style_fill(failures: Array[String], panel: PanelContainer, lab
if panel == null: if panel == null:
failures.append("%s missing" % label) failures.append("%s missing" % label)
return return
var style := panel.get_theme_stylebox("panel") as StyleBoxFlat var stylebox := panel.get_theme_stylebox("panel")
if stylebox is StyleBoxTexture:
_check_panel_style_texture(failures, stylebox as StyleBoxTexture, label)
return
var style := stylebox as StyleBoxFlat
if style == null: if style == null:
failures.append("%s should use a flat styled panel" % label) failures.append("%s should use a styled panel" % label)
return return
if not _colors_nearly_equal(style.bg_color, expected): if not _colors_nearly_equal(style.bg_color, expected):
failures.append("%s fill mismatch: %s" % [label, str(style.bg_color)]) failures.append("%s fill mismatch: %s" % [label, str(style.bg_color)])
@@ -3640,9 +3663,13 @@ func _check_panel_style_frame(failures: Array[String], panel: PanelContainer, la
if panel == null: if panel == null:
failures.append("%s missing" % label) failures.append("%s missing" % label)
return return
var style := panel.get_theme_stylebox("panel") as StyleBoxFlat var stylebox := panel.get_theme_stylebox("panel")
if stylebox is StyleBoxTexture:
_check_panel_style_texture(failures, stylebox as StyleBoxTexture, label)
return
var style := stylebox as StyleBoxFlat
if style == null: if style == null:
failures.append("%s should use a flat styled panel" % label) failures.append("%s should use a styled panel" % label)
return return
if not _colors_nearly_equal(style.border_color, expected_border): if not _colors_nearly_equal(style.border_color, expected_border):
failures.append("%s border mismatch: %s" % [label, str(style.border_color)]) failures.append("%s border mismatch: %s" % [label, str(style.border_color)])
@@ -3653,6 +3680,32 @@ func _check_panel_style_frame(failures: Array[String], panel: PanelContainer, la
failures.append("%s should use softened rounded corners, got %d" % [label, radius]) failures.append("%s should use softened rounded corners, got %d" % [label, radius])
func _check_panel_style_texture(failures: Array[String], style: StyleBoxTexture, label: String) -> void:
if style == null:
failures.append("%s should use a generated texture style" % label)
return
if style.texture == null:
failures.append("%s generated texture style is missing its texture" % label)
return
if style.texture.get_width() < 512 or style.texture.get_height() < 512:
failures.append("%s generated panel texture should keep high-resolution source pixels: %dx%d" % [label, style.texture.get_width(), style.texture.get_height()])
if style.texture_margin_left < 24 or style.texture_margin_top < 24:
failures.append("%s generated panel texture should define a visible frame slice margin" % label)
if style.content_margin_left < 7 or style.content_margin_top < 7:
failures.append("%s generated panel texture should reserve readable content margins" % label)
func _check_panel_uses_generated_texture(failures: Array[String], panel: PanelContainer, label: String) -> void:
if panel == null:
failures.append("%s missing" % label)
return
var stylebox := panel.get_theme_stylebox("panel")
if not (stylebox is StyleBoxTexture):
failures.append("%s should use a generated panel texture" % label)
return
_check_panel_style_texture(failures, stylebox as StyleBoxTexture, label)
func _colors_nearly_equal(left: Color, right: Color) -> bool: func _colors_nearly_equal(left: Color, right: Color) -> bool:
return ( return (
absf(left.r - right.r) < 0.01 absf(left.r - right.r) < 0.01
@@ -3874,6 +3927,7 @@ func _check_battle_unit_list_panel(failures: Array[String]) -> void:
failures.append("battle unit list panel should open from the top toolbar") failures.append("battle unit list panel should open from the top toolbar")
else: else:
_check_panel_style_fill(failures, scene.battle_unit_list_panel, "battle unit list panel", Color(0.045, 0.048, 0.044, 0.93)) _check_panel_style_fill(failures, scene.battle_unit_list_panel, "battle unit list panel", Color(0.045, 0.048, 0.044, 0.93))
_check_panel_uses_generated_texture(failures, scene.battle_unit_list_panel, "battle unit list generated frame")
if scene.battle_unit_list_title_label == null or not scene.battle_unit_list_title_label.text.contains("아군"): if scene.battle_unit_list_title_label == null or not scene.battle_unit_list_title_label.text.contains("아군"):
failures.append("battle unit list should open on the ally tab") failures.append("battle unit list should open on the ally tab")
if scene.battle_unit_list_rows == null or scene.battle_unit_list_rows.get_child_count() < 2: if scene.battle_unit_list_rows == null or scene.battle_unit_list_rows.get_child_count() < 2:
@@ -4843,6 +4897,58 @@ func _check_toolbar_icon_path(failures: Array[String], path: String, context: St
failures.append("%s icon should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples]) failures.append("%s icon should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples])
func _check_panel_texture_path(failures: Array[String], path: String, context: String) -> void:
if path.is_empty():
failures.append("%s has an empty panel path" % context)
return
if not path.begins_with("res://"):
failures.append("%s must use a res:// path: %s" % [context, path])
return
if not FileAccess.file_exists(path):
failures.append("%s references missing panel texture: %s" % [context, path])
return
var image := Image.new()
var err := image.load(path)
if err != OK:
failures.append("%s references unreadable panel texture: %s" % [context, path])
return
if image.get_width() < 512 or image.get_height() < 512:
failures.append("%s should keep high-resolution source pixels: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()])
return
for corner in [Vector2i(0, 0), Vector2i(image.get_width() - 1, 0), Vector2i(0, image.get_height() - 1), Vector2i(image.get_width() - 1, image.get_height() - 1)]:
if image.get_pixelv(corner).a > 0.01:
failures.append("%s should have transparent corners: %s" % [context, path])
return
var opaque_samples := 0
var transparent_samples := 0
var green_fringe_samples := 0
var luma_min := 999.0
var luma_max := -999.0
var total_samples := 0
for y in range(0, image.get_height(), 8):
for x in range(0, image.get_width(), 8):
total_samples += 1
var pixel := image.get_pixel(x, y)
if pixel.a <= 0.01:
transparent_samples += 1
continue
if pixel.a >= 0.80:
opaque_samples += 1
if pixel.g > 0.80 and pixel.r < 0.18 and pixel.b < 0.18:
green_fringe_samples += 1
var luma := pixel.r * 0.299 + pixel.g * 0.587 + pixel.b * 0.114
luma_min = minf(luma_min, luma)
luma_max = maxf(luma_max, luma)
if transparent_samples < int(total_samples * 0.08):
failures.append("%s should retain transparent frame padding: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples])
if opaque_samples < int(total_samples * 0.36):
failures.append("%s should contain substantial generated frame artwork: %s (%d/%d sampled pixels)" % [context, path, opaque_samples, total_samples])
if green_fringe_samples > 0:
failures.append("%s should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples])
if luma_max - luma_min < 0.10:
failures.append("%s should contain visible material contrast: %s" % [context, path])
func _check_terrain_texture_path(failures: Array[String], path: String, context: String) -> void: func _check_terrain_texture_path(failures: Array[String], path: String, context: String) -> void:
if path.is_empty(): if path.is_empty():
failures.append("%s has an empty texture path" % context) failures.append("%s has an empty texture path" % context)