diff --git a/README.md b/README.md index 82dcc17..62d5440 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Target preview badges, minimap hover badges, and battlefield target markers include generated jade panel surfaces plus action icons for move, attack, threat, support, objective, terrain, and item outcomes. - Story, briefing, dialogue, mission-detail, and result scroll/ribbon/binding/bamboo/ink/tassel ornaments use generated seal/panel textures instead of flat color boxes. - Battlefield hover info badges use generated jade panel texture plus terrain, supply, tactic, objective, and threat icons, keeping raw grid coordinates out of visible play text. -- Battle, top-toolbar, and pre-battle command buttons now prefer generated high-resolution bronze/parchment icon art under `art/ui/icons` and generated lacquer/jade/cinnabar button surfaces under `art/ui/buttons`, with procedural icon drawing and flat button styles kept only as fallbacks. +- Battle, top-toolbar, and pre-battle command buttons now prefer generated high-resolution bronze/parchment icon art under `art/ui/icons`, wide generated lacquer/jade/cinnabar menu button surfaces, and compact generated bronze/jade icon-button surfaces under `art/ui/buttons`, with procedural icon drawing and flat button styles kept only as fallbacks. - Battle maps blend generated terrain texture tiles for plains, forests, hills, wasteland, roads, water, villages, and castles over the high-resolution battlefield backdrop, with generated transparent feature overlays for road connections, water, villages, castles, and wasteland accents. - Board unit nameplates and HP bars use generated jade panel surfaces so the high-resolution sprites are not paired with flat black UI strips. - 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. diff --git a/art/ui/buttons/button_icon_disabled.png b/art/ui/buttons/button_icon_disabled.png new file mode 100644 index 0000000..26cf1da Binary files /dev/null and b/art/ui/buttons/button_icon_disabled.png differ diff --git a/art/ui/buttons/button_icon_disabled.png.import b/art/ui/buttons/button_icon_disabled.png.import new file mode 100644 index 0000000..819de2b --- /dev/null +++ b/art/ui/buttons/button_icon_disabled.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://kxok6vy0kgff" +path="res://.godot/imported/button_icon_disabled.png-9a60593452cdb73db341d15285bedef9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/ui/buttons/button_icon_disabled.png" +dest_files=["res://.godot/imported/button_icon_disabled.png-9a60593452cdb73db341d15285bedef9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/art/ui/buttons/button_icon_hover.png b/art/ui/buttons/button_icon_hover.png new file mode 100644 index 0000000..d89b2f8 Binary files /dev/null and b/art/ui/buttons/button_icon_hover.png differ diff --git a/art/ui/buttons/button_icon_hover.png.import b/art/ui/buttons/button_icon_hover.png.import new file mode 100644 index 0000000..dfc60be --- /dev/null +++ b/art/ui/buttons/button_icon_hover.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bly0hgxvteg6y" +path="res://.godot/imported/button_icon_hover.png-258e43ceeb0e32224969041ab42c4171.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/ui/buttons/button_icon_hover.png" +dest_files=["res://.godot/imported/button_icon_hover.png-258e43ceeb0e32224969041ab42c4171.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/art/ui/buttons/button_icon_normal.png b/art/ui/buttons/button_icon_normal.png new file mode 100644 index 0000000..46d4f23 Binary files /dev/null and b/art/ui/buttons/button_icon_normal.png differ diff --git a/art/ui/buttons/button_icon_normal.png.import b/art/ui/buttons/button_icon_normal.png.import new file mode 100644 index 0000000..adbee85 --- /dev/null +++ b/art/ui/buttons/button_icon_normal.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b5t67kvbju5kc" +path="res://.godot/imported/button_icon_normal.png-b54072e9c76d3d350b28f0e2db210779.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/ui/buttons/button_icon_normal.png" +dest_files=["res://.godot/imported/button_icon_normal.png-b54072e9c76d3d350b28f0e2db210779.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/art/ui/buttons/button_icon_pressed.png b/art/ui/buttons/button_icon_pressed.png new file mode 100644 index 0000000..94dcf3f Binary files /dev/null and b/art/ui/buttons/button_icon_pressed.png differ diff --git a/art/ui/buttons/button_icon_pressed.png.import b/art/ui/buttons/button_icon_pressed.png.import new file mode 100644 index 0000000..bd06e99 --- /dev/null +++ b/art/ui/buttons/button_icon_pressed.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://w5uapeflsg45" +path="res://.godot/imported/button_icon_pressed.png-c9c8780b27dc85572372f133edf1181a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/ui/buttons/button_icon_pressed.png" +dest_files=["res://.godot/imported/button_icon_pressed.png-c9c8780b27dc85572372f133edf1181a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a007c21..ed72f85 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -62,7 +62,7 @@ Joined officers gate whether `requires_joined` player deployments are loaded at Inventory and campaign flags are copied from `CampaignState` into `BattleState` when a scenario starts. The pre-battle Chapters overview reads `CampaignState` chapter progress and can load completed or current battles without mutating the save. Briefings preview first-clear rewards, can expose `briefing.camp_conversations` through a selectable Talk menu with `briefing.camp_dialogue` as a fallback, can filter those conversations by saved campaign flags, can claim saved one-time camp conversation supply effects through `CampaignState` and refresh the loaded battle inventory copy, and show completed replay rewards as already claimed. The pre-battle Save menu can write the current campaign state to `user://campaign_manual_save.json`, and loading that checkpoint restores it into `user://campaign_save.json` before re-entering the same pending-choice, completion, or current-briefing branch as startup. Pre-battle shop stock comes from the loaded scenario's `shop.items` list plus matching `shop.conditional_items` blocks, with optional finite limits from item entries or `stock` maps, and `shop.merchant` lines are presentation-only camp flavor. Shop purchases and 50% sell-back are campaign transactions: they update saved gold, unequipped inventory, and finite-stock purchase counts immediately, write the save file, and refresh the already-loaded battle inventory before the player begins the battle. Camp conversation supply claims are also campaign transactions: they save a per-scenario conversation claim ledger and inventory immediately, then refresh the already-loaded battle inventory. Pre-battle Armory equipment changes use the same equipment rules as battle HUD equipment changes, then immediately save merged roster equipment/stat snapshots and inventory stock. Pre-battle Roster uses scenario `roster.max_units`, `roster.required_officers`, and `roster.required_units` to mark optional player deployments as sortie or reserve; reserve units remain in the candidate list but are excluded from board occupancy, drawing, selection, AI targeting, and living-unit victory checks. Non-controllable player units can act as protected escort targets: enemies can attack them and conditions can reference them, but they are skipped by player selection, Armory, Formation, counterattacks, and campaign roster progression snapshots when `persist_progression` is false. Scenario `ai_target_priority` nudges enemy movement and damage-skill scoring toward important targets such as envoys without overriding defeat bonuses or range checks, and `set_ai_target_priority` events can retune that battle-only pressure after an ambush, gate closure, or escort exposure. Pre-battle Formation uses the loaded scenario's `formation.cells` and only mutates current battle unit positions before battle-begin events fire. Destination victory cells from `unit_reaches_tile` conditions are exposed to the scene for objective overlays and tile info, and can be expressed as one `pos` or a multi-cell `cells` list; gated victory conditions can also expose the `unit_reaches_tile` event that unlocks them as the next visible objective marker. Rewarding movement events that grant items or gold can expose optional battlefield side markers and Korean tile-info labels through `when.label`, without being treated as victory objectives. Movement-triggered `unit_reaches_tile` events receive the unit that just moved, so ambushes and pickup events fire on entry rather than from units already standing on a marker; `unit_defeated` events receive the fallen unit before final victory/defeat checks, allowing named-officer reactions and last-moment reinforcements. Briefing data can append matching `briefing.conditional_lines`, and scenario events can use `when.campaign_flags` to branch dialogue, objective changes, item and gold pickups, AI target-priority shifts, and reinforcements from saved campaign choices or `when.after_event` to wait for earlier scenario beats. Consumable use can restore HP or MP, cure matching poison/seal/snare/disarm statuses, or cleanse negative stat debuffs, and in-battle item use, event pickup grants, event gold pickups, plus equipment swaps mutate only the battle copy until victory; defeats and restarts do not spend saved items, keep picked-up items or gold, or preserve in-battle gear changes. Post-battle choices write campaign `flags` only after the result-panel button is pressed; if the player reloads after rewards are saved but before selecting, the pending scenario id restores the victory choice panel without replaying rewards. The next battle button stays locked until the campaign choice save succeeds. Equipment rewards share the same `item_id -> count` inventory stock, cover weapon, armor, and accessory slots, can be equipped from the side HUD before the selected unit moves or acts, and do not appear in the consumable action menu. Named equipment currently uses the same stock model with a presentation-only rarity tag rather than unique item instances. Already completed scenarios can be replayed without granting duplicate rewards, choices, replay inventory consumption, event gold pickups, save writes, current-scenario advancement, finite-stock purchases, or camp conversation supply claims; pre-battle Shop, Armory, Roster, Formation, and Save are disabled on completed-scenario replays to avoid side effects. -The battle scene owns presentation feedback: briefing and result states use menu BGM, briefing headers combine `CampaignState` chapter ranges, campaign order, and title data with `BattleState` briefing, objective text, condition-progress text, and defeat-risk text, active battles use battle BGM and show an objective/progress/risk panel fed by `BattleState`, objective-update signals show a short HUD notice and refresh the objective panel immediately, core panels prefer generated lacquer, scroll, jade, and command-seal `StyleBoxTexture` frames with flat ink/wood/jade styles as fallbacks, core buttons prefer generated lacquer, jade-hover, cinnabar-pressed, and disabled-ink `StyleBoxTexture` surfaces with flat styles as fallbacks, dialogue lines can show optional cached portrait textures or speaker-initial fallback panels in an expanded visual-novel-style panel, keep the portrait slot fixed even for narration lines, mark speaking side through text alignment, and expose progress plus previous-line controls while a sequence is open, the side HUD reuses cached officer portrait textures or class sprites for the selected unit or hovered unit, uses generated class crest icons for role badges, and formats focus text with role, movement type, facing, current terrain move cost, defense/avoid bonuses, core stats, status effects, guard-zone AI hints, and gear, board maps can draw a scenario background texture under translucent terrain overlays, generated terrain texture tiles, generated terrain feature overlays for connected roads, water, villages, castles, and wasteland accents, generated tactical tile markers for move, attack, selection, target, objective, and recovery overlays, connected edge blends, generated map badge icons for objective, recovery, supply, gold, tactic, and side-event markers, and move-attack target badges for enemies that can be struck after movement, board units render class sprite tokens or generic-enemy transparent class cutouts with generated class crest markers, team rings, facing markers, generic-enemy marks, officer pins, low-HP warning rings, HP bar color states, active support/debuff/poison/seal/snare/disarm status pips, and a scene-clocked idle stance whose intensity reacts to selected, acted, and low-HP unit state, item buttons can show optional cached item icons, hover previews are rendered as target badges from existing `BattleState` forecast APIs and include compact side/rear attack bonuses when facing matters, equipment option menus show stat/range/effect deltas, result and inventory summaries show named equipment with compact rarity tags, selected area tactics highlight their affected cells, the Threat toggle overlays enemy physical and hostile tactic reach while tile info names threat sources, estimates physical damage, and summarizes hostile tactic damage or status effects against occupied cells, log/result hooks trigger draft SFX, support, debuff, poison, seal, snare, disarm, objective updates, AI target-focus changes, and item/gold pickup effects use distinct feedback, `BattleState.unit_motion_requested` asks the scene to interpolate a unit's draw position during movement, `BattleState.unit_action_motion_requested` keeps its rules-facing signal shape while the scene maps attacker class ids to infantry, archer, cavalry, command, and heavy attack presentation profiles, and `BattleState.combat_feedback_requested` asks the scene to draw transient floating combat text without changing battle rules. +The battle scene owns presentation feedback: briefing and result states use menu BGM, briefing headers combine `CampaignState` chapter ranges, campaign order, and title data with `BattleState` briefing, objective text, condition-progress text, and defeat-risk text, active battles use battle BGM and show an objective/progress/risk panel fed by `BattleState`, objective-update signals show a short HUD notice and refresh the objective panel immediately, core panels prefer generated lacquer, scroll, jade, and command-seal `StyleBoxTexture` frames with flat ink/wood/jade styles as fallbacks, text buttons prefer generated lacquer, jade-hover, cinnabar-pressed, and disabled-ink `StyleBoxTexture` surfaces, icon-only buttons prefer separate compact bronze/jade generated `StyleBoxTexture` surfaces, and flat styles remain fallbacks, dialogue lines can show optional cached portrait textures or speaker-initial fallback panels in an expanded visual-novel-style panel, keep the portrait slot fixed even for narration lines, mark speaking side through text alignment, and expose progress plus previous-line controls while a sequence is open, the side HUD reuses cached officer portrait textures or class sprites for the selected unit or hovered unit, uses generated class crest icons for role badges, and formats focus text with role, movement type, facing, current terrain move cost, defense/avoid bonuses, core stats, status effects, guard-zone AI hints, and gear, board maps can draw a scenario background texture under translucent terrain overlays, generated terrain texture tiles, generated terrain feature overlays for connected roads, water, villages, castles, and wasteland accents, generated tactical tile markers for move, attack, selection, target, objective, and recovery overlays, connected edge blends, generated map badge icons for objective, recovery, supply, gold, tactic, and side-event markers, and move-attack target badges for enemies that can be struck after movement, board units render class sprite tokens or generic-enemy transparent class cutouts with generated class crest markers, team rings, facing markers, generic-enemy marks, officer pins, low-HP warning rings, HP bar color states, active support/debuff/poison/seal/snare/disarm status pips, and a scene-clocked idle stance whose intensity reacts to selected, acted, and low-HP unit state, item buttons can show optional cached item icons, hover previews are rendered as target badges from existing `BattleState` forecast APIs and include compact side/rear attack bonuses when facing matters, equipment option menus show stat/range/effect deltas, result and inventory summaries show named equipment with compact rarity tags, selected area tactics highlight their affected cells, the Threat toggle overlays enemy physical and hostile tactic reach while tile info names threat sources, estimates physical damage, and summarizes hostile tactic damage or status effects against occupied cells, log/result hooks trigger draft SFX, support, debuff, poison, seal, snare, disarm, objective updates, AI target-focus changes, and item/gold pickup effects use distinct feedback, `BattleState.unit_motion_requested` asks the scene to interpolate a unit's draw position during movement, `BattleState.unit_action_motion_requested` keeps its rules-facing signal shape while the scene maps attacker class ids to infantry, archer, cavalry, command, and heavy attack presentation profiles, and `BattleState.combat_feedback_requested` asks the scene to draw transient floating combat text without changing battle rules. Movement audio is keyed from `BattleState.unit_motion_requested` rather than movement log text, so future log cleanup does not mute board feedback. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 4cf4f47..c11c1cc 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -138,6 +138,7 @@ - Unit sprites and animations. Board movement now has a short slide animation, attacks request class-specific motion profiles for infantry, archers, cavalry, command units, and heavy warrior/bandit units, and board units show class sprite tokens, team rings, officer/generic identity marks, low-HP warning rings, and small status pips for active support, debuff, poison, seal, snare, and disarm effects, with HUD summaries showing remaining phases. - Battlefield presentation. Maps can draw a high-resolution background under translucent terrain overlays, with terrain-specific detail patterns, connected road segments, shoreline/castle/forest/hill edge blends, and generic-enemy token marks. - Battle HUD presentation. Selected and hovered units now summarize battlefield role, movement type, current terrain move cost, defense/avoid bonuses, core stats, status effects, and gear so class identity reads alongside the visual tokens. +- Command surface polish. Text buttons keep wide lacquer/jade/cinnabar generated surfaces, while top-toolbar, battle-command, and prep-command icon-only buttons use compact generated bronze/jade surfaces instead of compressed text-button frames. - Battle effects. First floating combat text now covers damage, recovery, misses, support effects, poison ticks, item and gold pickups, AI target-focus changes, level-ups, and promotions. Hover target preview badges summarize attacks, tactics, and items, and the board can overlay enemy threat ranges with physical damage and hostile tactic hints. - Music and sound. Longer procedural draft BGM and basic SFX now play for menus, battle flow, unit actions, tactics, items, and result states. - Chapter UI, objective presentation, and visual novel scenes. Pre-battle briefing now uses campaign chapter ranges for chapter-aware numbering, has a chapter progress/replay overview plus objective/progress/risk tracker, active battle HUD shows the same live objective panel, and dialogue lines can choose left or right portrait placement in an expanded portrait dialogue panel with progress and previous-line controls. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 2a30932..5d3d27d 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -32,6 +32,7 @@ const TITLE_MENU_SURFACE_PATH := "res://art/ui/panels/panel_title_command.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 BUTTON_TEXTURE_PATH_TEMPLATE := "res://art/ui/buttons/button_%s.png" +const ICON_BUTTON_TEXTURE_PATH_TEMPLATE := "res://art/ui/buttons/button_icon_%s.png" const TILE_MARKER_TEXTURE_PATH_TEMPLATE := "res://art/ui/tiles/tile_marker_%s.png" const CLASS_ICON_TEXTURE_PATH_TEMPLATE := "res://art/ui/class_icons/class_%s.png" const MAP_BADGE_TEXTURE_PATH_TEMPLATE := "res://art/ui/map_badges/map_badge_%s.png" @@ -1038,12 +1039,32 @@ func _make_button_texture_style(state_key: String, content_margin: int = 10) -> return style +func _make_icon_button_texture_style(state_key: String, content_margin: int = 6) -> StyleBoxTexture: + var texture := _load_art_texture(ICON_BUTTON_TEXTURE_PATH_TEMPLATE % state_key) + if texture == null: + return null + var style := StyleBoxTexture.new() + style.texture = texture + style.draw_center = true + style.texture_margin_left = 72 + style.texture_margin_right = 72 + style.texture_margin_top = 72 + style.texture_margin_bottom = 72 + var resolved_margin: int = maxi(content_margin, 5) + 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 _apply_generated_button_style(button: Button, important: bool = false) -> bool: - var normal_style := _make_button_texture_style("pressed" if important else "normal") - var hover_style := _make_button_texture_style("hover") - var pressed_style := _make_button_texture_style("pressed") - var focus_style := _make_button_texture_style("hover") - var disabled_style := _make_button_texture_style("disabled") + var icon_only := bool(button.get_meta("icon_only", false)) + var normal_style := _make_icon_button_texture_style("pressed" if important else "normal") if icon_only else _make_button_texture_style("pressed" if important else "normal") + var hover_style := _make_icon_button_texture_style("hover") if icon_only else _make_button_texture_style("hover") + var pressed_style := _make_icon_button_texture_style("pressed") if icon_only else _make_button_texture_style("pressed") + var focus_style := _make_icon_button_texture_style("hover") if icon_only else _make_button_texture_style("hover") + var disabled_style := _make_icon_button_texture_style("disabled") if icon_only else _make_button_texture_style("disabled") if normal_style == null or hover_style == null or pressed_style == null or focus_style == null or disabled_style == null: return false button.add_theme_stylebox_override("normal", normal_style) @@ -1052,6 +1073,7 @@ func _apply_generated_button_style(button: Button, important: bool = false) -> b button.add_theme_stylebox_override("focus", focus_style) button.add_theme_stylebox_override("disabled", disabled_style) button.set_meta("generated_button_texture", true) + button.set_meta("generated_icon_button_texture", icon_only) return true @@ -1143,6 +1165,7 @@ func _apply_button_style(button: Button, important: bool = false) -> void: button.add_theme_color_override("font_disabled_color", UI_DISABLED_TEXT) _apply_control_font(button, important) button.set_meta("generated_button_texture", false) + button.set_meta("generated_icon_button_texture", false) func _suppress_local_command_tooltip(button: Button) -> void: diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 3e7d75b..27a31f2 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -2994,6 +2994,8 @@ func _check_generated_button_textures(failures: Array[String]) -> void: var button_keys := ["normal", "hover", "pressed", "disabled"] for button_key in button_keys: _check_button_texture_path(failures, "res://art/ui/buttons/button_%s.png" % button_key, "button texture %s" % button_key) + for button_key in button_keys: + _check_icon_button_texture_path(failures, "res://art/ui/buttons/button_icon_%s.png" % button_key, "icon button texture %s" % button_key) func _check_generated_tile_marker_textures(failures: Array[String]) -> void: @@ -4134,12 +4136,26 @@ func _check_button_uses_generated_surface(failures: Array[String], button: Butto if style.texture == null: failures.append("%s generated button style is missing its texture" % context) return - if style.texture.get_width() < 512 or style.texture.get_height() < 192: - failures.append("%s generated button texture should keep high-resolution source pixels: %dx%d" % [context, style.texture.get_width(), style.texture.get_height()]) - if style.texture_margin_left < 48 or style.texture_margin_top < 24: - failures.append("%s generated button texture should define visible frame slice margins" % context) - if style.content_margin_left < 8 or style.content_margin_top < 5: - failures.append("%s generated button texture should reserve readable content margins" % context) + if bool(button.get_meta("icon_only", false)): + if not bool(button.get_meta("generated_icon_button_texture", false)): + failures.append("%s icon-only button should use the generated compact icon surface" % context) + if style.texture.get_width() < 256 or style.texture.get_height() < 256: + failures.append("%s generated icon button texture should keep high-resolution source pixels: %dx%d" % [context, style.texture.get_width(), style.texture.get_height()]) + if abs(style.texture.get_width() - style.texture.get_height()) > 4: + failures.append("%s generated icon button texture should be square or near-square: %dx%d" % [context, style.texture.get_width(), style.texture.get_height()]) + if style.texture_margin_left < 48 or style.texture_margin_top < 48: + failures.append("%s generated icon button texture should define visible frame slice margins" % context) + if style.content_margin_left < 5 or style.content_margin_top < 5: + failures.append("%s generated icon button texture should reserve icon content margins" % context) + else: + if bool(button.get_meta("generated_icon_button_texture", false)): + failures.append("%s text button should keep the wide generated button surface" % context) + if style.texture.get_width() < 512 or style.texture.get_height() < 192: + failures.append("%s generated button texture should keep high-resolution source pixels: %dx%d" % [context, style.texture.get_width(), style.texture.get_height()]) + if style.texture_margin_left < 48 or style.texture_margin_top < 24: + failures.append("%s generated button texture should define visible frame slice margins" % context) + if style.content_margin_left < 8 or style.content_margin_top < 5: + failures.append("%s generated button texture should reserve readable content margins" % context) func _check_local_command_icon_button(failures: Array[String], button: Button, expected_label: String, expected_icon: String) -> void: @@ -5489,6 +5505,64 @@ func _check_button_texture_path(failures: Array[String], path: String, context: failures.append("%s should avoid a dominant yellow UI cast: %s (warm bias %.3f)" % [context, path, warm_bias]) +func _check_icon_button_texture_path(failures: Array[String], path: String, context: String) -> void: + if path.is_empty(): + failures.append("%s has an empty icon button 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 button texture: %s" % [context, path]) + return + var image := Image.new() + var err := image.load(path) + if err != OK: + failures.append("%s references unreadable icon button texture: %s" % [context, path]) + return + if image.get_width() < 256 or image.get_height() < 256: + failures.append("%s should keep high-resolution compact source pixels: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()]) + return + if abs(image.get_width() - image.get_height()) > 4: + failures.append("%s should be a square compact icon surface: %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 compact button padding: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples]) + if opaque_samples < int(total_samples * 0.42): + failures.append("%s should contain substantial generated compact button 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.08: + failures.append("%s should contain visible material contrast: %s" % [context, path]) + var warm_bias := _image_visible_warm_bias(image, 8) + if warm_bias > 0.12: + failures.append("%s should avoid a dominant yellow UI cast: %s (warm bias %.3f)" % [context, path, warm_bias]) + + func _image_visible_warm_bias(image: Image, sample_step: int) -> float: var total_bias := 0.0 var visible_samples := 0