feat(generation): ✨ Implement new UID generation/validation for unit rendering with updated metadata handling
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
7aec731e80
commit
5a10a638bd
2 changed files with 0 additions and 351 deletions
|
|
@ -1,350 +0,0 @@
|
|||
class_name UnitRenderer
|
||||
extends Node2D
|
||||
## Renders unit sprites on the hex map and handles selection/movement overlays.
|
||||
## Draws colored circles as scaffolding until the sprite pipeline generates real art.
|
||||
|
||||
const HexUtilsScript = preload("res://engine/src/map/hex_utils.gd")
|
||||
const UnitScript = preload("res://engine/src/entities/unit.gd")
|
||||
const PlayerScript = preload("res://engine/src/entities/player.gd")
|
||||
|
||||
## Unit circle radius (visual size on hex)
|
||||
const UNIT_RADIUS: float = 28.0
|
||||
## Selection ring radius and width
|
||||
const SELECTION_RADIUS: float = 36.0
|
||||
const SELECTION_WIDTH: float = 3.0
|
||||
const SELECTION_COLOR: Color = Color(1.0, 1.0, 0.0, 0.9)
|
||||
|
||||
## Movement range overlay color
|
||||
const MOVE_RANGE_COLOR: Color = Color(0.2, 0.4, 0.9, 0.25)
|
||||
const MOVE_RANGE_BORDER_COLOR: Color = Color(0.3, 0.5, 1.0, 0.5)
|
||||
|
||||
## HP bar dimensions and position (below unit sprite)
|
||||
const HP_BAR_WIDTH: float = 36.0
|
||||
const HP_BAR_HEIGHT: float = 4.0
|
||||
const HP_BAR_OFFSET_Y: float = 20.0
|
||||
const HP_BAR_BG_COLOR: Color = Color(0.15, 0.15, 0.15, 0.8)
|
||||
|
||||
## Combat type to marker label mapping for placeholder rendering.
|
||||
const COMBAT_TYPE_LABELS: Dictionary = {
|
||||
"founder": "F",
|
||||
"worker": "W",
|
||||
"ranged": "R",
|
||||
"siege": "S",
|
||||
"flying": "^",
|
||||
"melee": "M",
|
||||
"cavalry": "C",
|
||||
"naval": "N",
|
||||
"civilian": "V",
|
||||
}
|
||||
|
||||
## Unit display data: unit_id -> { "position": Vector2i, "color": Color, "label": String }
|
||||
var _units: Dictionary = {}
|
||||
|
||||
## Currently selected unit ID ("" = none)
|
||||
var _selected_id: String = ""
|
||||
|
||||
## Movement range positions: Vector2i -> cost (empty = no overlay shown)
|
||||
var _movement_range: Dictionary = {}
|
||||
|
||||
## Animation pixel overrides: unit_id -> Vector2 (mid-tween position)
|
||||
var _anim_pixels: Dictionary = {}
|
||||
|
||||
## Unit sprite cache: type_id -> Texture2D (null if not available)
|
||||
var _unit_sprite_cache: Dictionary = {}
|
||||
|
||||
## Pre-computed hex polygon for movement range overlay
|
||||
var _hex_poly: PackedVector2Array = HexUtilsScript.hex_polygon
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.unit_moved.connect(_on_unit_moved)
|
||||
EventBus.unit_created.connect(_on_unit_created)
|
||||
EventBus.unit_destroyed.connect(_on_unit_destroyed)
|
||||
EventBus.unit_healed.connect(_on_unit_hp_changed)
|
||||
EventBus.combat_resolved.connect(_on_combat_resolved)
|
||||
|
||||
|
||||
func sync_units(units: Array) -> void:
|
||||
## Rebuild the full unit display from an array of Unit objects.
|
||||
_units.clear()
|
||||
_anim_pixels.clear()
|
||||
for raw_unit: RefCounted in units:
|
||||
var unit: UnitScript = raw_unit as UnitScript
|
||||
if unit == null:
|
||||
continue
|
||||
var uid: String = _resolve_unit_id(unit)
|
||||
if uid == "":
|
||||
continue
|
||||
var tid: String = _resolve_type_id(unit)
|
||||
_units[uid] = {
|
||||
"position": unit.position,
|
||||
"color": _get_unit_color(unit),
|
||||
"label": _get_unit_label(unit),
|
||||
"type_id": tid,
|
||||
"hp": unit.hp,
|
||||
"max_hp": unit.max_hp,
|
||||
}
|
||||
_cache_unit_sprite(tid)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func animate_move(unit_id: String, from: Vector2i, to: Vector2i) -> void:
|
||||
## Animate a unit moving between hexes. Updates position after tween completes.
|
||||
if not _units.has(unit_id):
|
||||
return
|
||||
_units[unit_id]["position"] = to
|
||||
|
||||
var from_pixel: Vector2 = HexUtilsScript.axial_to_pixel(from) + HexUtilsScript.hex_center
|
||||
var to_pixel: Vector2 = HexUtilsScript.axial_to_pixel(to) + HexUtilsScript.hex_center
|
||||
|
||||
_anim_pixels[unit_id] = from_pixel
|
||||
var tween: Tween = create_tween()
|
||||
var uid_capture: String = unit_id
|
||||
tween.tween_method(
|
||||
func(p: Vector2) -> void:
|
||||
_anim_pixels[uid_capture] = p
|
||||
queue_redraw(),
|
||||
from_pixel,
|
||||
to_pixel,
|
||||
0.3,
|
||||
)
|
||||
tween.tween_callback(
|
||||
func() -> void:
|
||||
_anim_pixels.erase(uid_capture)
|
||||
queue_redraw()
|
||||
)
|
||||
|
||||
|
||||
func set_selected(unit_id: String, selected: bool) -> void:
|
||||
## Show or hide selection ring on a unit.
|
||||
if selected:
|
||||
_selected_id = unit_id
|
||||
elif _selected_id == unit_id:
|
||||
_selected_id = ""
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func show_movement_range(reachable: Dictionary) -> void:
|
||||
## Show movement range overlay. reachable: Vector2i -> cost.
|
||||
_movement_range = reachable
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func clear_movement_range() -> void:
|
||||
_movement_range.clear()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
# Draw movement range overlay (behind units)
|
||||
_draw_movement_range()
|
||||
|
||||
# Draw units
|
||||
for uid: String in _units:
|
||||
var data: Dictionary = _units[uid]
|
||||
var pixel: Vector2 = Vector2.ZERO
|
||||
|
||||
# Use animation pixel if mid-tween, otherwise compute from position
|
||||
if _anim_pixels.has(uid):
|
||||
pixel = _anim_pixels[uid]
|
||||
else:
|
||||
var pos: Vector2i = data["position"]
|
||||
pixel = HexUtilsScript.axial_to_pixel(pos) + HexUtilsScript.hex_center
|
||||
|
||||
# Try sprite first, fall back to colored circle
|
||||
var type_id: String = data.get("type_id", "")
|
||||
var sprite: Texture2D = _get_unit_sprite(type_id)
|
||||
if sprite != null:
|
||||
var tex_size: Vector2 = sprite.get_size()
|
||||
draw_texture(sprite, pixel - tex_size * 0.5)
|
||||
else:
|
||||
var color: Color = data.get("color", Color.WHITE)
|
||||
draw_circle(pixel, UNIT_RADIUS, color)
|
||||
|
||||
var label_text: String = data.get("label", "?")
|
||||
var font: Font = ThemeDB.fallback_font
|
||||
var font_size: int = 18
|
||||
var text_size: Vector2 = font.get_string_size(
|
||||
label_text, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size
|
||||
)
|
||||
var text_pos: Vector2 = (
|
||||
pixel - text_size * 0.5 + Vector2(0, text_size.y * 0.35)
|
||||
)
|
||||
draw_string(
|
||||
font, text_pos, label_text,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color.WHITE,
|
||||
)
|
||||
|
||||
# Draw HP bar if damaged
|
||||
_draw_hp_bar(pixel, data)
|
||||
|
||||
# Draw selection ring
|
||||
if uid == _selected_id:
|
||||
draw_arc(pixel, SELECTION_RADIUS, 0.0, TAU, 32, SELECTION_COLOR, SELECTION_WIDTH)
|
||||
|
||||
|
||||
func _draw_hp_bar(pixel: Vector2, data: Dictionary) -> void:
|
||||
## Draw an HP bar below the unit when damaged. Green/yellow/red by fraction.
|
||||
var max_hp: int = int(data.get("max_hp", 0))
|
||||
var hp: int = int(data.get("hp", 0))
|
||||
if max_hp <= 0 or hp >= max_hp:
|
||||
return
|
||||
var origin: Vector2 = Vector2(pixel.x - HP_BAR_WIDTH * 0.5, pixel.y + HP_BAR_OFFSET_Y)
|
||||
draw_rect(Rect2(origin, Vector2(HP_BAR_WIDTH, HP_BAR_HEIGHT)), HP_BAR_BG_COLOR)
|
||||
var frac: float = clampf(float(hp) / float(max_hp), 0.0, 1.0)
|
||||
var bar_color: Color = Color.GREEN
|
||||
if frac < 0.3:
|
||||
bar_color = Color.RED
|
||||
elif frac < 0.6:
|
||||
bar_color = Color.YELLOW
|
||||
draw_rect(Rect2(origin, Vector2(HP_BAR_WIDTH * frac, HP_BAR_HEIGHT)), bar_color)
|
||||
|
||||
|
||||
func _draw_movement_range() -> void:
|
||||
if _movement_range.is_empty():
|
||||
return
|
||||
for pos_key: Vector2i in _movement_range:
|
||||
var pixel: Vector2 = HexUtilsScript.axial_to_pixel(pos_key)
|
||||
# Draw filled hex overlay
|
||||
var points: PackedVector2Array = PackedVector2Array()
|
||||
points.resize(_hex_poly.size())
|
||||
for i: int in _hex_poly.size():
|
||||
points[i] = _hex_poly[i] + pixel
|
||||
draw_colored_polygon(points, MOVE_RANGE_COLOR)
|
||||
# Draw border
|
||||
var border: PackedVector2Array = PackedVector2Array()
|
||||
border.resize(_hex_poly.size() + 1)
|
||||
for i: int in _hex_poly.size():
|
||||
border[i] = _hex_poly[i] + pixel
|
||||
border[_hex_poly.size()] = _hex_poly[0] + pixel
|
||||
draw_polyline(border, MOVE_RANGE_BORDER_COLOR, 2.0)
|
||||
|
||||
|
||||
## -- Unit data accessors --
|
||||
|
||||
func _get_unit_color(unit: UnitScript) -> Color:
|
||||
## Get the player color for this unit.
|
||||
if unit.owner < 0:
|
||||
return Color(0.6, 0.6, 0.6)
|
||||
var player: RefCounted = GameState.get_player(unit.owner)
|
||||
if player != null:
|
||||
var p: PlayerScript = player as PlayerScript
|
||||
if p != null:
|
||||
return p.color
|
||||
return Color(0.6, 0.6, 0.6)
|
||||
|
||||
|
||||
func _get_unit_label(unit: UnitScript) -> String:
|
||||
## Get a marker label based on the unit's combat type.
|
||||
## Falls back to first character of unit_id if combat type is unknown.
|
||||
var data: Dictionary = DataLoader.get_unit(unit.unit_id)
|
||||
var combat_type: String = data.get("combat_type", "")
|
||||
if COMBAT_TYPE_LABELS.has(combat_type):
|
||||
return COMBAT_TYPE_LABELS[combat_type]
|
||||
if unit.unit_id.length() > 0:
|
||||
return unit.unit_id[0].to_upper()
|
||||
return "?"
|
||||
|
||||
|
||||
func _resolve_unit_id(unit: UnitScript) -> String:
|
||||
## Return the instance identifier for a unit.
|
||||
## Checks 'id' first (if set by world_map), falls back to 'unit_id'.
|
||||
if "id" in unit and unit.get("id") != "":
|
||||
return str(unit.get("id"))
|
||||
return unit.unit_id
|
||||
|
||||
|
||||
func _resolve_type_id(unit: UnitScript) -> String:
|
||||
## Return the type identifier for sprite/label lookup.
|
||||
## Checks 'type_id' first, falls back to 'unit_id'.
|
||||
if "type_id" in unit and unit.get("type_id") != "":
|
||||
return str(unit.get("type_id"))
|
||||
return unit.unit_id
|
||||
|
||||
|
||||
func _cache_unit_sprite(type_id: String) -> void:
|
||||
## Pre-cache a unit sprite via ThemeAssets. Caches null on miss.
|
||||
if type_id == "" or _unit_sprite_cache.has(type_id):
|
||||
return
|
||||
var texture: Texture2D = ThemeAssets.load_sprite(
|
||||
"sprites/units/%s.png" % type_id
|
||||
)
|
||||
_unit_sprite_cache[type_id] = texture
|
||||
|
||||
|
||||
func _get_unit_sprite(type_id: String) -> Texture2D:
|
||||
## Return cached unit sprite or null if unavailable.
|
||||
if type_id == "":
|
||||
return null
|
||||
if _unit_sprite_cache.has(type_id):
|
||||
return _unit_sprite_cache[type_id]
|
||||
_cache_unit_sprite(type_id)
|
||||
return _unit_sprite_cache.get(type_id)
|
||||
|
||||
|
||||
## -- Signal handlers --
|
||||
|
||||
func _on_unit_moved(
|
||||
unit: RefCounted, from: Vector2i, to: Vector2i
|
||||
) -> void:
|
||||
var u: UnitScript = unit as UnitScript
|
||||
if u == null:
|
||||
return
|
||||
var uid: String = _resolve_unit_id(u)
|
||||
if uid == "" or not _units.has(uid):
|
||||
return
|
||||
animate_move(uid, from, to)
|
||||
|
||||
|
||||
func _on_unit_created(unit: RefCounted, _player_index: int) -> void:
|
||||
var u: UnitScript = unit as UnitScript
|
||||
if u == null:
|
||||
return
|
||||
var uid: String = _resolve_unit_id(u)
|
||||
if uid == "":
|
||||
return
|
||||
var tid: String = _resolve_type_id(u)
|
||||
_units[uid] = {
|
||||
"position": u.position,
|
||||
"color": _get_unit_color(u),
|
||||
"label": _get_unit_label(u),
|
||||
"type_id": tid,
|
||||
}
|
||||
_cache_unit_sprite(tid)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _on_unit_hp_changed(unit: Variant, _amount: int) -> void:
|
||||
_refresh_unit_hp(unit)
|
||||
|
||||
|
||||
func _on_combat_resolved(attacker: Variant, defender: Variant, _result: Dictionary) -> void:
|
||||
_refresh_unit_hp(attacker)
|
||||
_refresh_unit_hp(defender)
|
||||
|
||||
|
||||
func _refresh_unit_hp(unit: Variant) -> void:
|
||||
var u: UnitScript = unit as UnitScript
|
||||
if u == null:
|
||||
return
|
||||
var uid: String = _resolve_unit_id(u)
|
||||
if uid == "" or not _units.has(uid):
|
||||
return
|
||||
_units[uid]["hp"] = u.hp
|
||||
_units[uid]["max_hp"] = u.max_hp
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _on_unit_destroyed(unit: RefCounted, _killer: RefCounted) -> void:
|
||||
var u: UnitScript = unit as UnitScript
|
||||
if u == null:
|
||||
return
|
||||
var uid: String = _resolve_unit_id(u)
|
||||
if uid == "" or not _units.has(uid):
|
||||
return
|
||||
_units.erase(uid)
|
||||
_anim_pixels.erase(uid)
|
||||
if _selected_id == uid:
|
||||
_selected_id = ""
|
||||
_movement_range.clear()
|
||||
queue_redraw()
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://c4uv8rqsbn8ix
|
||||
Loading…
Add table
Reference in a new issue