diff --git a/README.md b/README.md index baac64c..39a1021 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Victory and defeat result overlay. - Dialogue portrait slot with officer default image paths, optional per-line overrides, and speaker-initial 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 battlefield, class unit, transparent generic-enemy unit, and item icon assets live under `art/backgrounds`, `art/units`, and `art/items`. +- Initial AI-generated battlefield, transparent class unit, transparent generic-enemy unit, and item icon assets live under `art/backgrounds`, `art/units`, and `art/items`. - 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 place the portrait on the left or right side of an expanded visual-novel-style dialogue panel with progress and previous-line controls. diff --git a/art/units/archer.png b/art/units/archer.png index ebd4e64..3c42201 100644 Binary files a/art/units/archer.png and b/art/units/archer.png differ diff --git a/art/units/cavalry.png b/art/units/cavalry.png index d7caf9d..53cd983 100644 Binary files a/art/units/cavalry.png and b/art/units/cavalry.png differ diff --git a/art/units/infantry.png b/art/units/infantry.png index 362eaf4..bcab4c6 100644 Binary files a/art/units/infantry.png and b/art/units/infantry.png differ diff --git a/art/units/strategist.png b/art/units/strategist.png index 692a41b..2c52963 100644 Binary files a/art/units/strategist.png and b/art/units/strategist.png differ diff --git a/art/units/warrior.png b/art/units/warrior.png index 4ebd5b9..1e8e017 100644 Binary files a/art/units/warrior.png and b/art/units/warrior.png differ diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 090426a..bca20d7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -16,7 +16,7 @@ - `scripts/core/battle_state.gd`: Battle rules, unit state, turns, AI, and victory checks. - `scripts/scenes/battle_scene.gd`: Rendering, input handling, and HUD wiring. - `audio/bgm/*.wav` and `audio/sfx/*.wav`: Placeholder music loops and interface/action stingers. -- `art/portraits/*.png`, `art/units/*.png`, `art/units/enemies/*.png`, `art/items/*.png`, and `art/backgrounds/*.png`: Generated visual assets used by dialogue, battle HUD, board tokens, generic-enemy transparent cutouts, item buttons, and map backdrops. +- `art/portraits/*.png`, `art/units/*.png`, `art/units/enemies/*.png`, `art/items/*.png`, and `art/backgrounds/*.png`: Generated visual assets used by dialogue, battle HUD, transparent board-token cutouts, generic-enemy transparent cutouts, item buttons, and map backdrops. - `data/campaign/campaign.json`: Campaign chapter ranges, scenario order, and scenario paths. - `data/defs/*.json`: Officers, classes, terrain, items, and skills. - `data/scenarios/*.json`: Scenario definitions. diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index a40ceac..0ccf74b 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -219,7 +219,7 @@ Classes provide movement, min/max attack range, skill access, growth grades, cla ``` `attack_range` is `[min, max]` in Manhattan distance. A range of `[2, 2]` means the unit cannot attack adjacent targets. -`sprite` is an optional class-level board token image used by hydrated units unless an officer or deployment overrides it. `enemy_sprite` is an optional class-level image for generic enemy units with `team: "enemy"` and no `officer_id`; runtime sprite precedence is deployment `sprite`, officer `sprite`, generic enemy class `enemy_sprite`, then class `sprite`. The current enemy class art is stored as transparent PNG cutouts under `res://art/units/enemies/` so named officers and player units can keep their cleaner class or officer presentation while ordinary troops read as their own force. +`sprite` is an optional class-level board token image used by hydrated units unless an officer or deployment overrides it. Base class sprites are transparent PNG cutouts under `res://art/units/` so board tokens can show only the character over the terrain and token backplate. `enemy_sprite` is an optional class-level image for generic enemy units with `team: "enemy"` and no `officer_id`; runtime sprite precedence is deployment `sprite`, officer `sprite`, generic enemy class `enemy_sprite`, then class `sprite`. The current enemy class art is stored as transparent PNG cutouts under `res://art/units/enemies/` so named officers and player units can keep their cleaner class or officer presentation while ordinary troops read as their own force. `promotion.level` and `promotion.to` define an automatic class promotion route. When a unit reaches the threshold during battle, the runtime updates class name, movement, movement type, growth, class skills, attack range, and class base-bonus deltas. The promoted `class_id` and derived roster fields are captured in the campaign roster snapshot, and the next battle rehydrates class-derived fields from the saved `class_id`. Promotion targets must be higher-tier classes and must keep all source equipment slot permissions unless runtime auto-unequip logic is added later. The current core routes cover hero, cavalry, infantry, archer, warrior, and strategist first-tier classes. diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index b8bdb55..c682d30 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -55,6 +55,19 @@ func _check_battle_visual_data(failures: Array[String]) -> void: _check_unit_sprite_resolution(failures, state, "yellow_turban_1", "res://art/units/enemies/enemy_warrior.png", true) _check_unit_sprite_resolution(failures, state, "yellow_turban_2", "res://art/units/enemies/enemy_infantry.png", true) _check_unit_sprite_resolution(failures, state, "yellow_turban_3", "res://art/units/enemies/enemy_archer.png", true) + for path in [ + "res://art/units/archer.png", + "res://art/units/cavalry.png", + "res://art/units/infantry.png", + "res://art/units/strategist.png", + "res://art/units/warrior.png", + "res://art/units/enemies/enemy_archer.png", + "res://art/units/enemies/enemy_cavalry.png", + "res://art/units/enemies/enemy_infantry.png", + "res://art/units/enemies/enemy_strategist.png", + "res://art/units/enemies/enemy_warrior.png" + ]: + _check_alpha_cutout_path(failures, path, "unit cutout %s" % path) var gate_state = BattleStateScript.new() if not gate_state.load_battle("res://data/scenarios/002_sishui_gate.json"): @@ -348,6 +361,39 @@ func _check_unit_sprite_resolution(failures: Array[String], state, unit_id: Stri _check_image_path(failures, sprite, "unit %s resolved sprite" % unit_id) +func _check_alpha_cutout_path(failures: Array[String], path: String, context: String) -> void: + if path.is_empty(): + failures.append("%s has an empty image 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 image: %s" % [context, path]) + return + var image := Image.new() + var err := image.load(path) + if err != OK: + failures.append("%s references unreadable image: %s" % [context, path]) + return + if image.get_width() < 512 or image.get_height() < 512: + failures.append("%s image should be at least 512x512: %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 transparent_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 + if image.get_pixel(x, y).a <= 0.01: + transparent_samples += 1 + if transparent_samples < int(total_samples * 0.20): + failures.append("%s should have substantial transparent background: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_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)