Skip to content

t1k:unity:dots-combat:core

FieldValue
Moduledots-combat
Version2.3.8
Effortmedium
Tools

Keywords: AttackConfig, BattleState, build cost, BuildRequest, CCMasks, combat, combat formulas, CombatAuthoring, CombatConstants, CurrencyId, damage, DamageEvent, dead tag, DeadTag, DemolishRequest, DerivedCombatStats, DerivedStats, DerivedStatsSystem, DerivedStatsSystem, DOTSEconomy, HealEvent, health, HitFlash, HitFlashTimer, invulnerable, kill reward, KillRewardCurrency, knockback, RangedAttack, RangedAttackConfig, RangedAttackTag, structures, wallet, WalletEntry, WalletTransaction

/t1k:unity:dots-combat:core

Package: com.the1studio.dots-combat | Namespace: DOTSCombat.*

Split from t1k:unity:dots-combat:rpg (Wave 2). For auto-battler → t1k:unity:dots-combat:autobattler. For survivor → t1k:unity:dots-combat:survivor. For boss → t1k:unity:dots-combat:boss.

ModuleKey Types
Combat (28c/19s)Health, Mana, Shield, DamageEvent, HealEvent, StatusEffect, DeadTag, InvulnerableTag, KnockbackEvent, BattleState, AttackConfig, OnHitEffect, OnDeathEffect, AuraEffect
Skills (11c/8s)SkillSlotState, SkillSlotConfig, ActiveCastState, ProjectileData, ParabolicArc, AreaEffect, HomingTarget, SkillBehaviorType, RangedAttackConfig (monolithic SkillSlot REMOVED — see Gotcha 9)
Synergy/Trait (5c/3s)TraitEntry, SynergyDefinition, ActiveSynergy, TeamSynergyState, SynergyModifierSource
Talent Tree (5c/2s)TalentNode, TalentProgress, TalentPoints, TalentUnlockRequest, TalentEvent
Summon (5c/4s)SummonConfig, SummonState, SummonRequest, SummonEvent, SummonedByTag

Stat pipeline: BaseStatsStatModifier[]DerivedCombatStats / DerivedResourceStats / DerivedLocomotionStatSyncSystem syncs Health.Max / Mana.Max.

  • Implementing or debugging damage, healing, knockback, status effects
  • Setting up unit stats (BaseStats → DerivedStatsSystem → combat pipeline)
  • Using skill casting, projectiles, AoE, homing, talent trees, synergy
  • Working with CombatAuthoring (canonical unit baker adds 15+ combat components)
// Damage formula — never inline
float dmg = CombatFormulas.ComputeAutoAttackDamage(atk, def, level);
float cd = CombatFormulas.EffectiveCooldown(baseCd, haste);
uint seed = CombatFormulas.FrameRngSeed(entity, frame);
// Events — per-entity IBufferElementData
buffer.Add(DamageEvent.Create(source, amount, type));
buffer.Add(new HealEvent { Amount = 20 });
buffer.Add(new KnockbackEvent { Direction = dir, Force = 5f });
// CC masks
if ((state.ActiveCC & CCMasks.MoveBlock) != 0) return; // unit is CC'd
// Custom unit baker (MUST include HitFlashTimer if bypassing CombatAuthoring)
this.AddComponent(entity, new HitFlashTimer
{
Duration = VisualConstants.HitFlashDuration,
OriginalScale = bakedLocalTransform.Scale, // MUST match baked scale
});
// Ranged: BOTH Tag AND Config required
this.AddComponent<RangedAttackTag>(entity);
this.AddComponent(entity, new RangedAttackConfig { ... });
  1. Custom baker bypassing CombatAuthoring MUST add HitFlashTimer manually — missing it causes silent hit-feedback failure. OriginalScale MUST match baked LocalTransform.Scale (not hardcoded 1f). Real incident 2026-05-26 ChaosForge.

  2. Ranged: BOTH RangedAttackTag AND RangedAttackConfig required. AutoAttackSystem excludes RangedAttackTag — adding only RangedAttackConfig silently falls back to melee.

  3. RangedAttackSystem is no-op without ProjectileRegistryTag singleton + TeamProjectilePrefab buffer. Every team needs its own entry; missing-team archers fire 0 projectiles. (Wave-4 rename: was ArrowRegistryTag / TeamArrowPrefab — old names kept as [Obsolete]/[MovedFrom] shims for un-migrated demos.)

  4. RangedAttackSystem.LookRotationSafe makes 2D projectile quads invisible in sideview — normal points along flight direction (perpendicular to sideview camera). Fix: add a [UpdateAfter(ParabolicArcSystem), OrderLast] system in SkillsSystemGroup that overrides rotation to quaternion.identity.

  5. RangedKitingDistance MUST be ≤ hero AttackRange — hero parked beyond its own range idles indefinitely.

  6. ISystem for TalentUnlockSystem → use SystemBase — Entities 1.4 BufferTypeHandle two-pass invalidation.

  7. SetComponentEnabled before buffer reads invalidates BufferTypeHandle — complete all buffer reads first.

  8. Combat reward + structures systems consume the v2 DOTSEconomy wallet — NOT the deprecated DOTSInventory.CurrencyWallet/CurrencyTransaction/CurrencyWalletUtility (W7 wallet v1→v2 cutover, 2026-05-28). This is one coherent contract across two subsystems:

    • Reward systems (KillRewardCurrencySystem, Boss.BossRewardSystem) enqueue DOTSEconomy.WalletTransaction (was CurrencyTransaction) onto the killer’s wallet buffer. The transaction set is { CurrencyId, long Delta, WalletTransactionSource, FixedString64Bytes Reference } — kill/boss loot uses WalletTransactionSource.Loot (ordinal 0, value-safe vs v1). Always set Reference = default if you have no audit tag.
    • Structures systems (BuildValidationSystem, BuildExecutionSystem, DemolishSystem) query .WithAll<Wallet, WalletEntry>(), read balances via WalletUtility.FindEntryIndex(buffer, CurrencyId) / WalletUtility.GetBalance(...) (sentinel WalletUtility.InvalidEntryIndex == -1), and mutate WalletEntry.Amount directly (parity-first; bypasses the ledger/event audit — that’s an intentional deferred follow-up, documented in BuildExecutionSystem remarks).
    • Component currency keys are now DOTSEconomy.CurrencyId enums, not intKillRewardCurrency.CurrencyType, BossRewardCurrency.CurrencyType, BuildRequest.CurrencyType, DemolishRequest.RefundCurrencyType. CurrencyId has named tiers Soft=0/Hard=1/Premium=2/Energy=3/EventToken=4 plus Custom0..Custom7 (100-107) for game-specific currencies. This is a BREAKING authoring change — demos authoring these buffers must pass enum values.
    • The Wallet tag is now REQUIRED on any reward/cost target (v1 had no tag — queries hit CurrencyWallet directly). An entity receiving rewards or paying build costs needs the full v2 buffer set: Wallet tag + WalletEntry + WalletTransaction + WalletLedger + CurrencyEarnedEvent + CurrencySpentEvent + CurrencyInsufficientEvent (easiest: route through DOTSEconomy.WalletAuthoring). Miss the tag/buffers → HasBuffer<WalletTransaction> is false, WalletSystem skips the entity, and rewards/costs silently no-op. WalletEntry cap is 8 (was 4); long amounts (was int).
  9. SkillSlot is split into SkillSlotState (hot) + SkillSlotConfig (cold) — as of the 260530 refactor the monolithic SkillSlot buffer is REMOVED. Skills now carry two parallel-indexed IBufferElementData buffers:

    • SkillSlotState { int SkillId; float CooldownRemaining; } — per-frame mutable, ~8B/slot.
    • SkillSlotConfig { SkillId, Cooldown, ManaCost, CastTime, Power, DamageType, Range, SkillBehavior, ProjectilePrefab, ProjectileSpeed, HomingTurnRate, AoERadius, AoEDuration, AoETickInterval, AoEKnockbackForce, AoEKnockbackDuration } — baked once.

    Invariant: State[i] and Config[i] are the SAME skill — the baker writes both in lockstep (asserted by SkillSlotStateConfigBakerInvariantTests).

    Reader pattern: query BOTH buffers, iterate int n = math.min(states.Length, configs.Length), read CooldownRemaining/SkillId from State, read config from Config, write back ONLY State.

    SkillCooldownSystem queries DynamicBuffer<SkillSlotState> ONLY (hot path, 8B/slot). There is no SkillSlot type anymore — do not reference it. (Related: Gotcha 7 — do all AddBuffer/SetComponentEnabled structural changes first, then re-fetch handles.)

  10. Generic component naming — name the ROLE, not the instance. (RESOLVED in Wave-4.) The Arrow* family (ArrowRegistryTag, TeamArrowPrefab, ArrowPrefabAuthoring) and Mage* attack family were over-specific concrete-instance names — really “the projectile a ranged attacker fires” and “an AoE caster’s attack” — while the library ALREADY had generic ProjectileData/ProjectileCollisionSystem. Wave-4 (lib v3 post-ship) renamed them to the generic ROLE: Arrow*Projectile*, Mage*Caster*, with [MovedFrom]/[Obsolete] back-compat shims (see combat-rpg combat-guide.md § “Wave-4 Neutrality Renames”). The principle still holds for all NEW code: name the generic role (Projectile, Caster, Agent), never one concrete instance (Arrow, Mage, Sword) — a concrete instance belongs in an enum value or demo content. SSOT: rules/library-quality-mandate-unity.md § “Naming charter”.