feat(game-engine): ✨ Add runtime state fields like health and position, plus helper methods for updating and rendering Units
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
db2070b2a3
commit
f633ab95e5
1 changed files with 178 additions and 0 deletions
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue