diff --git a/README.md b/README.md index a2dd0b9..63de63b 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Pre-battle Formation lets deployed officers swap starting positions inside scenario-defined cells, with portrait rows, readable zone badges, and hover placement instructions. - Pre-battle briefing shows the campaign chapter, chapter battle number, location, victory/defeat summary, and first-clear reward preview. - 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. - 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. - 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. @@ -128,7 +129,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - 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. - 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, transparent class units, transparent generic-enemy units, and transparent item icon assets live under `art/backgrounds`, `art/story`, `art/units`, and `art/items`. +- AI-generated scenario battlefield backgrounds, opening story panels, transparent class units, transparent generic-enemy units, item icons, and toolbar UI icons live under `art/backgrounds`, `art/story`, `art/units`, `art/items`, and `art/ui/icons`. - 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. - 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. diff --git a/art/ui/icons/toolbar_attack.png b/art/ui/icons/toolbar_attack.png new file mode 100644 index 0000000..1bffbd8 Binary files /dev/null and b/art/ui/icons/toolbar_attack.png differ diff --git a/art/ui/icons/toolbar_campaign.png b/art/ui/icons/toolbar_campaign.png new file mode 100644 index 0000000..58078fd Binary files /dev/null and b/art/ui/icons/toolbar_campaign.png differ diff --git a/art/ui/icons/toolbar_cancel.png b/art/ui/icons/toolbar_cancel.png new file mode 100644 index 0000000..25dbfd3 Binary files /dev/null and b/art/ui/icons/toolbar_cancel.png differ diff --git a/art/ui/icons/toolbar_end_turn.png b/art/ui/icons/toolbar_end_turn.png new file mode 100644 index 0000000..3b6a75d Binary files /dev/null and b/art/ui/icons/toolbar_end_turn.png differ diff --git a/art/ui/icons/toolbar_equip.png b/art/ui/icons/toolbar_equip.png new file mode 100644 index 0000000..217dbdc Binary files /dev/null and b/art/ui/icons/toolbar_equip.png differ diff --git a/art/ui/icons/toolbar_forecast.png b/art/ui/icons/toolbar_forecast.png new file mode 100644 index 0000000..9321ddf Binary files /dev/null and b/art/ui/icons/toolbar_forecast.png differ diff --git a/art/ui/icons/toolbar_formation.png b/art/ui/icons/toolbar_formation.png new file mode 100644 index 0000000..684486e Binary files /dev/null and b/art/ui/icons/toolbar_formation.png differ diff --git a/art/ui/icons/toolbar_item.png b/art/ui/icons/toolbar_item.png new file mode 100644 index 0000000..da7568f Binary files /dev/null and b/art/ui/icons/toolbar_item.png differ diff --git a/art/ui/icons/toolbar_load.png b/art/ui/icons/toolbar_load.png new file mode 100644 index 0000000..9d2b4a2 Binary files /dev/null and b/art/ui/icons/toolbar_load.png differ diff --git a/art/ui/icons/toolbar_move.png b/art/ui/icons/toolbar_move.png new file mode 100644 index 0000000..0aa8fe4 Binary files /dev/null and b/art/ui/icons/toolbar_move.png differ diff --git a/art/ui/icons/toolbar_new.png b/art/ui/icons/toolbar_new.png new file mode 100644 index 0000000..7388bca Binary files /dev/null and b/art/ui/icons/toolbar_new.png differ diff --git a/art/ui/icons/toolbar_objective.png b/art/ui/icons/toolbar_objective.png new file mode 100644 index 0000000..159dd77 Binary files /dev/null and b/art/ui/icons/toolbar_objective.png differ diff --git a/art/ui/icons/toolbar_restart.png b/art/ui/icons/toolbar_restart.png new file mode 100644 index 0000000..007d23d Binary files /dev/null and b/art/ui/icons/toolbar_restart.png differ diff --git a/art/ui/icons/toolbar_roster.png b/art/ui/icons/toolbar_roster.png new file mode 100644 index 0000000..9bf2e17 Binary files /dev/null and b/art/ui/icons/toolbar_roster.png differ diff --git a/art/ui/icons/toolbar_save.png b/art/ui/icons/toolbar_save.png new file mode 100644 index 0000000..6e57a42 Binary files /dev/null and b/art/ui/icons/toolbar_save.png differ diff --git a/art/ui/icons/toolbar_settings.png b/art/ui/icons/toolbar_settings.png new file mode 100644 index 0000000..46e4890 Binary files /dev/null and b/art/ui/icons/toolbar_settings.png differ diff --git a/art/ui/icons/toolbar_shop.png b/art/ui/icons/toolbar_shop.png new file mode 100644 index 0000000..fb10932 Binary files /dev/null and b/art/ui/icons/toolbar_shop.png differ diff --git a/art/ui/icons/toolbar_status.png b/art/ui/icons/toolbar_status.png new file mode 100644 index 0000000..33f1f75 Binary files /dev/null and b/art/ui/icons/toolbar_status.png differ diff --git a/art/ui/icons/toolbar_tactic.png b/art/ui/icons/toolbar_tactic.png new file mode 100644 index 0000000..e9dc4ec Binary files /dev/null and b/art/ui/icons/toolbar_tactic.png differ diff --git a/art/ui/icons/toolbar_talk.png b/art/ui/icons/toolbar_talk.png new file mode 100644 index 0000000..b985b4e Binary files /dev/null and b/art/ui/icons/toolbar_talk.png differ diff --git a/art/ui/icons/toolbar_terrain.png b/art/ui/icons/toolbar_terrain.png new file mode 100644 index 0000000..2c9401b Binary files /dev/null and b/art/ui/icons/toolbar_terrain.png differ diff --git a/art/ui/icons/toolbar_threat.png b/art/ui/icons/toolbar_threat.png new file mode 100644 index 0000000..371342a Binary files /dev/null and b/art/ui/icons/toolbar_threat.png differ diff --git a/art/ui/icons/toolbar_unit_list.png b/art/ui/icons/toolbar_unit_list.png new file mode 100644 index 0000000..45f4ed9 Binary files /dev/null and b/art/ui/icons/toolbar_unit_list.png differ diff --git a/art/ui/icons/toolbar_wait.png b/art/ui/icons/toolbar_wait.png new file mode 100644 index 0000000..2bcf5b8 Binary files /dev/null and b/art/ui/icons/toolbar_wait.png differ diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 98ca60e..7de202c 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -28,6 +28,7 @@ const BGM_MENU_PATH := "res://audio/bgm/menu_theme_placeholder.wav" 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_PORTRAIT_PATH := "res://art/portraits/cao_cao.png" +const TOOLBAR_ICON_PATH_TEMPLATE := "res://art/ui/icons/toolbar_%s.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_XIAHOU_DUN_PATH := "res://art/story/opening/xiahou_dun_joins.png" @@ -976,7 +977,7 @@ func _configure_hud_command_button(button: Button, icon_kind: String = "") -> vo if not icon_kind.is_empty(): button.text = "" button.icon = _make_toolbar_icon(icon_kind) - button.expand_icon = false + button.expand_icon = true button.set_meta("icon_only", true) button.set_meta("command_icon", icon_kind) @@ -1022,7 +1023,7 @@ func _configure_local_command_button(button: Button, icon_kind: String, label: S button.custom_minimum_size = minimum_size button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.icon = _make_toolbar_icon(icon_kind) - button.expand_icon = false + button.expand_icon = true button.add_theme_font_size_override("font_size", 1) button.set_meta("icon_only", true) button.set_meta("command_icon", icon_kind) @@ -1332,6 +1333,9 @@ func _set_control_tree_tooltip(node: Node, tooltip: String) -> void: func _make_toolbar_icon(kind: String) -> Texture2D: + var generated_icon := _load_toolbar_icon_texture(kind) + if generated_icon != null: + return generated_icon var image := Image.create(32, 32, false, Image.FORMAT_RGBA8) image.fill(Color(0, 0, 0, 0)) var ink := Color(0.82, 0.84, 0.78, 1.0) @@ -1524,6 +1528,13 @@ func _make_toolbar_icon(kind: String) -> Texture2D: return ImageTexture.create_from_image(image) +func _load_toolbar_icon_texture(kind: String) -> Texture2D: + var normalized_kind := kind.strip_edges().to_lower() + if normalized_kind.is_empty(): + return null + return _load_art_texture(TOOLBAR_ICON_PATH_TEMPLATE % normalized_kind) + + func _icon_rect(image: Image, rect: Rect2i, color: Color) -> void: for y in range(maxi(0, rect.position.y), mini(image.get_height(), rect.position.y + rect.size.y)): for x in range(maxi(0, rect.position.x), mini(image.get_width(), rect.position.x + rect.size.x)): diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index eb6d555..bff16a2 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -91,6 +91,7 @@ const SCENARIO_BACKGROUNDS := [ func _init() -> void: var failures: Array[String] = [] _check_battle_visual_data(failures) + _check_generated_toolbar_icons(failures) _check_scene_texture_loading(failures) _check_hud_focus_text(failures) _check_ancient_ui_theme(failures) @@ -2944,6 +2945,37 @@ func _check_scenario_backgrounds_data(failures: Array[String]) -> void: _check_image_path(failures, background_path, label) +func _check_generated_toolbar_icons(failures: Array[String]) -> void: + var icon_kinds := [ + "attack", + "tactic", + "item", + "wait", + "end_turn", + "threat", + "unit_list", + "save", + "load", + "settings", + "campaign", + "shop", + "talk", + "equip", + "roster", + "formation", + "status", + "objective", + "terrain", + "forecast", + "move", + "cancel", + "restart", + "new" + ] + for icon_kind in icon_kinds: + _check_toolbar_icon_path(failures, "res://art/ui/icons/toolbar_%s.png" % icon_kind, "toolbar icon %s" % icon_kind) + + func _check_scene_texture_loading(failures: Array[String]) -> void: var scene = BattleSceneScript.new() if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): @@ -3715,6 +3747,13 @@ func _has_descendant_texture(root: Node) -> bool: return false +func _check_button_uses_generated_toolbar_icon(failures: Array[String], button: Button, context: String) -> void: + if button == null or button.icon == null: + return + if button.icon.get_width() < 128 or button.icon.get_height() < 128: + failures.append("%s should use generated high-resolution toolbar art, got %dx%d" % [context, button.icon.get_width(), button.icon.get_height()]) + + func _check_local_command_icon_button(failures: Array[String], button: Button, expected_label: String, expected_icon: String) -> void: if button == null: failures.append("local command button missing: %s" % expected_label) @@ -3723,6 +3762,7 @@ func _check_local_command_icon_button(failures: Array[String], button: Button, e failures.append("local command should be icon-first without visible text: %s text=%s" % [expected_label, button.text]) if button.icon == null or str(button.get_meta("command_icon", "")) != expected_icon: failures.append("local command should carry an immediate visual icon: %s icon=%s meta=%s" % [expected_label, str(button.icon), str(button.get_meta("command_icon", ""))]) + _check_button_uses_generated_toolbar_icon(failures, button, expected_label) if str(button.get_meta("command_label", "")) != expected_label: failures.append("local command should keep old wording in hover metadata: %s meta=%s" % [expected_label, str(button.get_meta("command_label", ""))]) if str(button.tooltip_text).strip_edges().is_empty() or not str(button.tooltip_text).contains(expected_label): @@ -3759,6 +3799,7 @@ func _check_hud_command_grid_layout(failures: Array[String]) -> void: failures.append("unit command button should reserve fixed icon space: %s" % str(button.custom_minimum_size)) if button.icon == null or str(button.get_meta("command_icon", "")) != str(expectation["icon"]): failures.append("unit command button should carry an immediate visual icon: %s icon=%s meta=%s" % [button.text, str(button.icon), str(button.get_meta("command_icon", ""))]) + _check_button_uses_generated_toolbar_icon(failures, button, str(expectation["icon"])) if not bool(button.get_meta("icon_only", false)) or button.text != "": failures.append("unit command button should be icon-first without visible text: %s text=%s" % [str(button.name), button.text]) if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): @@ -3793,6 +3834,7 @@ func _check_hud_command_grid_layout(failures: Array[String]) -> void: failures.append("top tool button should share the top toolbar: %s" % str(button.name)) if not bool(button.get_meta("icon_only", false)) or button.text != "" or button.icon == null: failures.append("top tool button should be icon-only with tooltip detail: %s text=%s icon=%s" % [str(button.name), button.text, str(button.icon)]) + _check_button_uses_generated_toolbar_icon(failures, button, str(button.name)) if button.custom_minimum_size.x < BattleSceneScript.TOP_TOOL_BUTTON_SIZE.x or button.custom_minimum_size.y < BattleSceneScript.TOP_TOOL_BUTTON_SIZE.y: failures.append("top tool button should reserve stable icon space: %s" % str(button.custom_minimum_size)) scene.free() @@ -3901,6 +3943,7 @@ func _check_prep_menu_icon_button(failures: Array[String], button: Button, expec failures.append("pre-battle prep button should be icon-first without visible text: %s text=%s" % [expected_label, button.text]) if button.icon == null or str(button.get_meta("command_icon", "")) != expected_icon: failures.append("pre-battle prep button should carry a distinct icon: %s icon=%s meta=%s" % [expected_label, str(button.icon), str(button.get_meta("command_icon", ""))]) + _check_button_uses_generated_toolbar_icon(failures, button, expected_label) if str(button.get_meta("command_label", "")) != expected_label: failures.append("pre-battle prep button should preserve Korean label in metadata: %s meta=%s" % [expected_label, str(button.get_meta("command_label", ""))]) if str(button.tooltip_text).strip_edges().is_empty() or not str(button.tooltip_text).contains(expected_label): @@ -4738,6 +4781,50 @@ func _check_alpha_cutout_path(failures: Array[String], path: String, context: St failures.append("%s should have substantial transparent background: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples]) +func _check_toolbar_icon_path(failures: Array[String], path: String, context: String) -> void: + if path.is_empty(): + failures.append("%s has an empty icon 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 icon: %s" % [context, path]) + return + var image := Image.new() + var err := image.load(path) + if err != OK: + failures.append("%s references unreadable icon: %s" % [context, path]) + return + if image.get_width() < 128 or image.get_height() < 128: + failures.append("%s icon 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 icon should have transparent corners: %s" % [context, path]) + return + var opaque_samples := 0 + var transparent_samples := 0 + var green_fringe_samples := 0 + var total_samples := 0 + for y in range(0, image.get_height(), 4): + for x in range(0, image.get_width(), 4): + total_samples += 1 + var pixel := image.get_pixel(x, y) + if pixel.a <= 0.01: + transparent_samples += 1 + elif pixel.a >= 0.80: + opaque_samples += 1 + if pixel.g > 0.88 and pixel.r < 0.20 and pixel.b < 0.20: + green_fringe_samples += 1 + if transparent_samples < int(total_samples * 0.12): + failures.append("%s icon should retain transparent UI padding: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples]) + if opaque_samples < int(total_samples * 0.18): + failures.append("%s icon should contain substantial artwork: %s (%d/%d sampled pixels)" % [context, path, opaque_samples, total_samples]) + if green_fringe_samples > 0: + failures.append("%s icon should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples]) + + func _check_image_path(failures: Array[String], path: String, context: String) -> void: if path.is_empty(): failures.append("%s has an empty image path" % context)