Make base unit art transparent
@@ -114,7 +114,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 with officer default image paths, optional per-line overrides, and speaker-initial fallback.
|
- 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 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.
|
- 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 place the portrait on the left or right side of an expanded visual-novel-style dialogue panel with progress and previous-line controls.
|
- 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.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 1009 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.2 MiB |
@@ -16,7 +16,7 @@
|
|||||||
- `scripts/core/battle_state.gd`: Battle rules, unit state, turns, AI, and victory checks.
|
- `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.
|
- `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.
|
- `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/campaign/campaign.json`: Campaign chapter ranges, scenario order, and scenario paths.
|
||||||
- `data/defs/*.json`: Officers, classes, terrain, items, and skills.
|
- `data/defs/*.json`: Officers, classes, terrain, items, and skills.
|
||||||
- `data/scenarios/*.json`: Scenario definitions.
|
- `data/scenarios/*.json`: Scenario definitions.
|
||||||
|
|||||||
@@ -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.
|
`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.
|
`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.
|
||||||
|
|
||||||
|
|||||||
@@ -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_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_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)
|
_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()
|
var gate_state = BattleStateScript.new()
|
||||||
if not gate_state.load_battle("res://data/scenarios/002_sishui_gate.json"):
|
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)
|
_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:
|
func _check_image_path(failures: Array[String], path: String, context: String) -> void:
|
||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
failures.append("%s has an empty image path" % context)
|
failures.append("%s has an empty image path" % context)
|
||||||
|
|||||||