From f633ab95e557e36e1bf9ef33295bdff482ad56d2 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 8 Apr 2026 16:11:49 -0700 Subject: [PATCH] =?UTF-8?q?feat(game-engine):=20=E2=9C=A8=20Add=20runtime?= =?UTF-8?q?=20state=20fields=20like=20health=20and=20position,=20plus=20he?= =?UTF-8?q?lper=20methods=20for=20updating=20and=20rendering=20Units?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- src/game/engine/src/entities/unit.gd | 178 +++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/src/game/engine/src/entities/unit.gd b/src/game/engine/src/entities/unit.gd index deef278e..11a2c158 100644 --- a/src/game/engine/src/entities/unit.gd +++ b/src/game/engine/src/entities/unit.gd @@ -1,2 +1,180 @@ class_name Unit extends RefCounted +## Game-side unit entity. Holds runtime state for one military or civilian +## unit on the world map. Stats originate from JSON unit data via DataLoader; +## this class tracks the per-instance mutable runtime state (position, hp, +## promotions, infusions, movement budget, fortification, XP/level). +## +## Restored in iter 7i from a 2-line stub. The field set is the union of +## every Unit field/method the existing GDScript codebase reads or writes +## (verified by grep across engine/src/ before authoring this class). +## +## The bridge to mc_turn::MapUnit lives in `to_bridge_dict()` and the +## inverse `apply_bridge_dict()` — that's the iter 7h dict adapter. + +const UnitStatHelpers: GDScript = preload( + "res://engine/src/entities/unit_stat_helpers.gd" +) + +# ── Identity ─────────────────────────────────────────────────────────── +## DataLoader unit type id, e.g. "dwarf_warrior". +var unit_id: String = "" +## Display name override; falls back to a humanised `unit_id`. +var display_name: String = "" + +# ── Position & ownership ────────────────────────────────────────────── +## Axial position on the world map. +var position: Vector2i = Vector2i.ZERO +## Owning player index. -1 = wild creature, 0..n = player slot. +var owner: int = -1 + +# ── Combat stats ────────────────────────────────────────────────────── +var hp: int = 1 +var max_hp: int = 1 +var attack: int = 0 +var defense: int = 0 +var ranged_attack: int = 0 + +# ── Movement ────────────────────────────────────────────────────────── +var movement_remaining: int = 2 +var max_movement: int = 2 + +# ── Per-turn state flags ────────────────────────────────────────────── +var has_attacked: bool = false +var is_fortified: bool = false +var fortified_turns: int = 0 + +# ── Veterancy ───────────────────────────────────────────────────────── +var xp: int = 0 +var level: int = 1 + +# ── Promotions, items, magic ────────────────────────────────────────── +var promo_ids: Array[String] = [] +## Equipped items: Array[Dictionary] {item_id: String, charges: int}. +var equipped_items: Array = [] +## Currently active enchantment IDs (non-channeled). +var infusions: Array[String] = [] +## Single channeled enchantment (mage-anchored) — empty if none active. +var channeled_infusion: String = "" +## Turns remaining before the channeled infusion fades after disconnection. +var channeled_fade_turns: int = 0 + + +# ── Lifecycle ───────────────────────────────────────────────────────── + +## Construct a Unit with the given identity. Stats are populated from the +## DataLoader entry if `populate_from_data` is true. +func _init(p_unit_id: String = "", p_owner: int = -1, p_position: Vector2i = Vector2i.ZERO, + populate_from_data: bool = true) -> void: + unit_id = p_unit_id + owner = p_owner + position = p_position + if populate_from_data and unit_id != "": + _populate_from_data() + + +## Read base stats from `DataLoader.get_unit(unit_id)` and apply them. +## Safe to call repeatedly — overwrites mutable stat fields. +func _populate_from_data() -> void: + var data: Dictionary = DataLoader.get_unit(unit_id) + if data.is_empty(): + return + max_hp = data.get("hp", 1) + hp = max_hp + attack = data.get("attack", 0) + defense = data.get("defense", 0) + ranged_attack = data.get("ranged_attack", 0) + max_movement = data.get("movement", 2) + movement_remaining = max_movement + if display_name.is_empty(): + display_name = data.get("name", unit_id.capitalize()) + + +# ── Per-turn refresh (called by turn_processor.gd) ──────────────────── + +## Reset per-turn state flags. Called at the top of each player turn. +func refresh_turn() -> void: + movement_remaining = get_movement() + has_attacked = false + if is_fortified: + fortified_turns += 1 + if channeled_fade_turns > 0: + channeled_fade_turns -= 1 + + +# ── Combat queries ──────────────────────────────────────────────────── + +func is_alive() -> bool: + return hp > 0 + + +func is_ranged() -> bool: + var combat_type: String = _combat_type() + return combat_type == "ranged" or combat_type == "siege" or ranged_attack > 0 + + +func is_flying() -> bool: + return _combat_type() == "flying" + + +func is_military() -> bool: + var combat_type: String = _combat_type() + if combat_type.is_empty(): + return false + return combat_type != "settler" and combat_type != "worker" and combat_type != "civilian" + + +func _combat_type() -> String: + if unit_id.is_empty(): + return "" + var data: Dictionary = DataLoader.get_unit(unit_id) + return data.get("combat_type", "") + + +# ── Movement / damage / healing helpers ─────────────────────────────── + +## Effective per-turn movement budget after promotions, items, infusions. +func get_movement() -> int: + var total: int = max_movement + total += UnitStatHelpers.get_promotion_stat(promo_ids, "movement_bonus") + total += UnitStatHelpers.get_infusion_movement_bonus(infusions) + var override: int = UnitStatHelpers.get_infusion_movement_override(infusions) + if override > 0: + return override + return maxi(1, total) + + +## Apply incoming damage. Returns true if the unit is now dead. +func take_damage(amount: int) -> bool: + hp = maxi(0, hp - amount) + return hp <= 0 + + +## Apply healing, capped at max_hp. +func heal(amount: int) -> void: + hp = mini(max_hp, hp + amount) + + +# ── Iter 7h bridge adapter ──────────────────────────────────────────── + +## Pack this unit into the dict shape that +## `GdGameState::set_player_units_from_dicts` accepts. +func to_bridge_dict() -> Dictionary: + return { + "col": position.x, + "row": position.y, + "hp": hp, + "max_hp": max_hp, + "attack": attack, + "defense": defense, + "is_fortified": is_fortified, + "unit_id": unit_id, + } + + +## Apply a post-step bridge dict back onto this unit. Used by the +## iter 7i adapter to sync hp/position back from `step()` results. +func apply_bridge_dict(d: Dictionary) -> void: + hp = int(d.get("hp", hp)) + position = Vector2i(int(d.get("col", position.x)), int(d.get("row", position.y))) + is_fortified = bool(d.get("is_fortified", is_fortified))