From 47c59b2a055e6364d4f7c70c4652d162a2246920 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 11 Apr 2026 06:15:11 -0700 Subject: [PATCH] =?UTF-8?q?feat(game-engine):=20=E2=9C=A8=20Add=20D20=20at?= =?UTF-8?q?tribute=20system=20and=20movement=20domain=20mechanics=20to=20U?= =?UTF-8?q?nit=20class?= 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 | 62 +++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/src/game/engine/src/entities/unit.gd b/src/game/engine/src/entities/unit.gd index d93d36b8..5b77c6e9 100644 --- a/src/game/engine/src/entities/unit.gd +++ b/src/game/engine/src/entities/unit.gd @@ -27,6 +27,9 @@ var type_id: String = "" var display_name: String = "" ## Combat type string from JSON data, e.g. "melee", "civilian", "ranged". var unit_type: String = "" +## Movement domain string from JSON data, e.g. "land", "flying", "naval". +## Consumed by Pathfinder._is_passable to decide terrain passability. +var domain: String = "land" # ── Capability flags ───────────────────────────────────────────────── ## True if this unit can found cities (derived from "found_city" keyword). @@ -64,10 +67,30 @@ var fortified_turns: int = 0 var xp: int = 0 var level: int = 1 +# ── D20 attribute stubs ─────────────────────────────────────────────── +## These fields are probed by CombatResolver._has_d20_attributes to decide +## whether to send D20-derived stats or flat JSON stats to Rust mc-combat. +## Arena units have no D20 profile, so all stubs default to 0 and combat +## takes the flat-stats path. Real D20 units will populate these later. +var base_str: int = 0 +var base_dex: int = 0 +var base_con: int = 0 +var base_int: int = 0 +var bonus_str: int = 0 +var bonus_dex: int = 0 +var bonus_con: int = 0 +var bonus_int: int = 0 +var bonus_attack: int = 0 +var bonus_defense: int = 0 +var veteran_level: int = 0 +var formation_count: int = 1 +var stimulant_penalty: int = 0 + # ── Promotions, items, magic ────────────────────────────────────────── var promo_ids: Array[String] = [] -## Equipped items: Array[Dictionary] {item_id: String, charges: int}. -var equipped_items: Array = [] +## Equipped items: each entry is {item_id: String, charges_remaining: int}. +## Must stay strongly typed — GdItemSystem's FFI rejects untyped arrays. +var equipped_items: Array[Dictionary] = [] ## Currently active enchantment IDs (non-channeled). var infusions: Array[String] = [] ## Single channeled enchantment (mage-anchored) — empty if none active. @@ -104,7 +127,20 @@ func _populate_from_data() -> void: max_movement = data.get("movement", 2) movement_remaining = max_movement vision = data.get("vision", 2) - unit_type = data.get("combat_type", "") + # JSON data authored for Age of Dwarves uses `unit_type` for the role + # ("military"/"civilian"/"support") and `combat_type` for the weapon + # profile ("melee"/"ranged"/"siege"). Earlier iterations read + # `combat_type` into this field, which silently produced an empty + # string for every dwarf unit and broke downstream consumers like + # the pathfinder's unit_type gate. Read both fields now — prefer + # `combat_type` when present (some reference units still use it) and + # fall back to `unit_type`, so arena units always report a non-empty + # role string. `domain` carries the pathfinder-facing passability tag. + var combat_type: String = String(data.get("combat_type", "")) + if combat_type.is_empty(): + combat_type = String(data.get("unit_type", "")) + unit_type = combat_type + domain = String(data.get("domain", "land")) if display_name.is_empty(): display_name = data.get("name", unit_id.capitalize()) var is_founder: bool = ( @@ -188,11 +224,27 @@ func get_attack() -> int: func get_defense() -> int: var total: int = defense total += UnitStatHelpers.get_promotion_stat(promo_ids, "defense_bonus") - if is_fortified: - total += mini(fortified_turns, 2) * 2 + total += get_fortification_bonus() return total +## Defensive bonus earned by fortifying in place. Capped at 2 turns of +## accumulation (same formula that was inlined in `get_defense`). Read by +## CombatResolver._build_unit_dict when packing the attacker/defender +## dict for the Rust combat FFI. +func get_fortification_bonus() -> int: + if not is_fortified: + return 0 + return mini(fortified_turns, 2) * 2 + + +## Alias for `max_hp` used by CombatUtils.handle_unit_death's soul gem +## resurrection path. Kept as a method so the death code doesn't depend on +## the exact field name. +func get_max_hp() -> int: + return max_hp + + func fortify() -> void: if is_civilian(): return