Unit Stat Modifiers
This is the implementation contract for additive unit bonuses and damage prevention. Keep the owning ability responsible for acquisition, expiry, interruption, and removal. Shared helpers own the arithmetic and the persistent ledger for a derived stat; they do not replace ability ownership.
One authoritative value per stat
- Treat SLK/DBC unit rows as immutable base data. Do not write bonuses back into those tables or repeatedly add deltas to a value that will later be rebuilt.
- Store persistent additive modifiers in named ledgers. Unit-local bonuses
belong on the edict; researched
rmnxmana belongs to player tech and is queried throughG_UnitUpgradeEffectBonus. Runtime values such asarmor_valueanddamageBaseare derived caches, not independent sources of truth. - Use the same operation for ordinary add/remove paths. Removal passes the inverse delta through that operation, keeping the ledger and live value in sync. A bespoke ability transition may use different current-resource rules, but must still update the ledger (Avatar expiry clamps current HP instead of preserving its fraction).
- Item maximum-health and maximum-mana bonuses preserve the unit's current
fraction.
rmnxresearch moves current mana by the absolute maximum delta. Keep each bonus in its authoritative ledger so Hero attribute recomputation and save/load do not erase it.
Current examples:
| Stat/effect | Ledger and calculation | Consumer |
|---|---|---|
| Attack damage | unitAttack_t.permanentDamageBonus / temporaryDamageBonus; primary Hero attribute is rebuilt from UnitWeapons |
ai_rolldamage1, then attack-resolution modifiers; use G_Apply*AttackDamageBonus |
| Armor | permanent_armor_bonus / temporary_armor_bonus; armor_value is rebuilt from base armor and Agility |
G_UnitArmorValue, shared by physical damage and HUD; use G_Apply*ArmorBonus |
| Maximum health | permanent_health_bonus / temporary_health_bonus; use G_Apply*MaxHealthBonus |
Hero recomputation and health cap |
| Maximum mana | Hero Intelligence, player-owned rmnx research via G_UnitUpgradeEffectBonus, plus temporary_mana_bonus; temporary changes use G_ApplyTemporaryMaxManaBonus |
Hero recomputation and mana cap |
Add another ledger only when the stat needs a separately owned contribution
that must survive recomputation or persistence. Timed buffs whose value can be
looked up from the active status and authored ability row can remain derived
inputs, as Bdef is in G_UnitArmorValue. G_TransformUnitType snapshots and
reapplies temporary armor, attack, health, and mana modifiers around the new
unit-type bind; add coverage there when introducing another persistent ledger.
Damage paths are intentionally distinct
Status-owned modifier resolution visits the fixed status slots and their descriptors once, combining strongest-wins families in bounded stack storage. This avoids nested status scans in movement, armor, and attack-rate queries. Expired or removed owners are excluded immediately, allowing the next strongest contributor to become effective without a separate inverse update.
The strongest-wins hash table is initialized lazily: units with no applicable strongest-wins modifiers avoid zeroing the 64-entry table and skip its final reduction pass. Additive modifiers still accumulate directly; strongest-family selection, expiration filtering, and numeric semantics are unchanged. This is a per-query fast path, not a cached value, so no invalidation is required.
T_Damage(target, source, amount) applies an already calculated amount. It
handles hard invulnerability, Mana Shield, Spirit Link, damage events, health,
and death. It does not apply weapon-type multipliers or armor.
- Ordinary physical attacks calculate base damage and dice, then call
G_AttackDamagefor attack-type/defense-type and armor math. The hit then passes throughS_ResolveAttackHitfor attack abilities such as critical strike, attack bonuses/reductions, and on-hit behavior beforeT_Damage. - Spell paths that need magic immunity or Anti-Magic Shell use
S_SpellDamage. Spell damage does not implicitly use the physical armor formula; each ability follows its authored contract. - Direct
T_Damagecallers provide damage in the units and mitigation state required by their effect. Do not route a pre-mitigated attack back throughG_AttackDamageor assume a direct hit received spell-immunity checks. invulnerableis a hard damage gate. Spell immunity is a target/impact rule; Mana Shield is a finite damage-to-mana conversion. Keep these as distinct mechanics, not generic numeric modifiers.
JASS UnitDamageTarget currently accepts but does not model its attack type,
damage type, and weapon type arguments; it calls T_Damage with the supplied
amount. Preserve that limitation explicitly until typed JASS damage is
implemented.
Change and test checklist
For a new modifier, identify its owning procedure, base value, ledger, read sites, ordering relative to other modifiers, and inverse/expiry path. Add a production-path regression that covers a non-default authored value, stacking or competing sources where relevant, removal/expiry, Hero recomputation, and save/load if new persistent state is added. For damage changes, test the relevant entry path (weapon, spell, or direct damage) and the nearest bypass path so mitigation is neither skipped nor applied twice.