t1k:unity:dots-combat:core
| Field | Value |
|---|---|
| Module | dots-combat |
| Version | 2.3.8 |
| Effort | medium |
| 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
How to invoke
Section titled “How to invoke”/t1k:unity:dots-combat:coreDOTS Combat Core — Foundations
Section titled “DOTS Combat Core — Foundations”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.
Overview
Section titled “Overview”| Module | Key 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: BaseStats → StatModifier[] → DerivedCombatStats / DerivedResourceStats / DerivedLocomotion → StatSyncSystem syncs Health.Max / Mana.Max.
When to use
Section titled “When to use”- 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)
Core API
Section titled “Core API”// Damage formula — never inlinefloat dmg = CombatFormulas.ComputeAutoAttackDamage(atk, def, level);float cd = CombatFormulas.EffectiveCooldown(baseCd, haste);uint seed = CombatFormulas.FrameRngSeed(entity, frame);
// Events — per-entity IBufferElementDatabuffer.Add(DamageEvent.Create(source, amount, type));buffer.Add(new HealEvent { Amount = 20 });buffer.Add(new KnockbackEvent { Direction = dir, Force = 5f });
// CC masksif ((state.ActiveCC & CCMasks.MoveBlock) != 0) return; // unit is CC'dExamples
Section titled “Examples”// 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 requiredthis.AddComponent<RangedAttackTag>(entity);this.AddComponent(entity, new RangedAttackConfig { ... });Gotchas
Section titled “Gotchas”-
Custom baker bypassing
CombatAuthoringMUST addHitFlashTimermanually — missing it causes silent hit-feedback failure.OriginalScaleMUST match bakedLocalTransform.Scale(not hardcoded 1f). Real incident 2026-05-26 ChaosForge. -
Ranged: BOTH
RangedAttackTagANDRangedAttackConfigrequired.AutoAttackSystemexcludesRangedAttackTag— adding onlyRangedAttackConfigsilently falls back to melee. -
RangedAttackSystemis no-op withoutProjectileRegistryTagsingleton +TeamProjectilePrefabbuffer. Every team needs its own entry; missing-team archers fire 0 projectiles. (Wave-4 rename: wasArrowRegistryTag/TeamArrowPrefab— old names kept as[Obsolete]/[MovedFrom]shims for un-migrated demos.) -
RangedAttackSystem.LookRotationSafemakes 2D projectile quads invisible in sideview — normal points along flight direction (perpendicular to sideview camera). Fix: add a[UpdateAfter(ParabolicArcSystem), OrderLast]system inSkillsSystemGroupthat overrides rotation toquaternion.identity. -
RangedKitingDistanceMUST be ≤ heroAttackRange— hero parked beyond its own range idles indefinitely. -
ISystemforTalentUnlockSystem→ useSystemBase— Entities 1.4BufferTypeHandletwo-pass invalidation. -
SetComponentEnabledbefore buffer reads invalidatesBufferTypeHandle— complete all buffer reads first. -
Combat reward + structures systems consume the v2
DOTSEconomywallet — NOT the deprecatedDOTSInventory.CurrencyWallet/CurrencyTransaction/CurrencyWalletUtility(W7 wallet v1→v2 cutover, 2026-05-28). This is one coherent contract across two subsystems:- Reward systems (
KillRewardCurrencySystem,Boss.BossRewardSystem) enqueueDOTSEconomy.WalletTransaction(wasCurrencyTransaction) onto the killer’s wallet buffer. The transaction set is{ CurrencyId, long Delta, WalletTransactionSource, FixedString64Bytes Reference }— kill/boss loot usesWalletTransactionSource.Loot(ordinal 0, value-safe vs v1). Always setReference = defaultif you have no audit tag. - Structures systems (
BuildValidationSystem,BuildExecutionSystem,DemolishSystem) query.WithAll<Wallet, WalletEntry>(), read balances viaWalletUtility.FindEntryIndex(buffer, CurrencyId)/WalletUtility.GetBalance(...)(sentinelWalletUtility.InvalidEntryIndex == -1), and mutateWalletEntry.Amountdirectly (parity-first; bypasses the ledger/event audit — that’s an intentional deferred follow-up, documented inBuildExecutionSystemremarks). - Component currency keys are now
DOTSEconomy.CurrencyIdenums, notint—KillRewardCurrency.CurrencyType,BossRewardCurrency.CurrencyType,BuildRequest.CurrencyType,DemolishRequest.RefundCurrencyType.CurrencyIdhas named tiersSoft=0/Hard=1/Premium=2/Energy=3/EventToken=4plusCustom0..Custom7(100-107) for game-specific currencies. This is a BREAKING authoring change — demos authoring these buffers must pass enum values. - The
Wallettag is now REQUIRED on any reward/cost target (v1 had no tag — queries hitCurrencyWalletdirectly). An entity receiving rewards or paying build costs needs the full v2 buffer set:Wallettag +WalletEntry+WalletTransaction+WalletLedger+CurrencyEarnedEvent+CurrencySpentEvent+CurrencyInsufficientEvent(easiest: route throughDOTSEconomy.WalletAuthoring). Miss the tag/buffers →HasBuffer<WalletTransaction>is false,WalletSystemskips the entity, and rewards/costs silently no-op.WalletEntrycap is 8 (was 4);longamounts (wasint).
- Reward systems (
-
SkillSlotis split intoSkillSlotState(hot) +SkillSlotConfig(cold) — as of the 260530 refactor the monolithicSkillSlotbuffer is REMOVED. Skills now carry two parallel-indexedIBufferElementDatabuffers: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]andConfig[i]are the SAME skill — the baker writes both in lockstep (asserted bySkillSlotStateConfigBakerInvariantTests).Reader pattern: query BOTH buffers, iterate
int n = math.min(states.Length, configs.Length), readCooldownRemaining/SkillIdfrom State, read config from Config, write back ONLY State.SkillCooldownSystemqueriesDynamicBuffer<SkillSlotState>ONLY (hot path, 8B/slot). There is noSkillSlottype anymore — do not reference it. (Related: Gotcha 7 — do allAddBuffer/SetComponentEnabledstructural changes first, then re-fetch handles.) -
Generic component naming — name the ROLE, not the instance. (RESOLVED in Wave-4.) The
Arrow*family (ArrowRegistryTag,TeamArrowPrefab,ArrowPrefabAuthoring) andMage*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 genericProjectileData/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-rpgcombat-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”.