From 5a10a638bda0a46bd6aa2a85c0ce1718502db810 Mon Sep 17 00:00:00 2001 From: autocommit Date: Wed, 3 Jun 2026 06:03:32 -0700 Subject: [PATCH] =?UTF-8?q?feat(generation):=20=E2=9C=A8=20Implement=20new?= =?UTF-8?q?=20UID=20generation/validation=20for=20unit=20rendering=20with?= =?UTF-8?q?=20updated=20metadata=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../engine/src/generation/unit_renderer.gd | 350 ------------------ .../src/generation/unit_renderer.gd.uid | 1 - 2 files changed, 351 deletions(-) delete mode 100644 src/game/engine/src/generation/unit_renderer.gd delete mode 100644 src/game/engine/src/generation/unit_renderer.gd.uid diff --git a/src/game/engine/src/generation/unit_renderer.gd b/src/game/engine/src/generation/unit_renderer.gd deleted file mode 100644 index 532ac250..00000000 --- a/src/game/engine/src/generation/unit_renderer.gd +++ /dev/null @@ -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() diff --git a/src/game/engine/src/generation/unit_renderer.gd.uid b/src/game/engine/src/generation/unit_renderer.gd.uid deleted file mode 100644 index 44c75c7e..00000000 --- a/src/game/engine/src/generation/unit_renderer.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c4uv8rqsbn8ix