Skip to content

t1k:unity:dots-combat:modifiers

FieldValue
Moduledots-combat
Version2.3.8
Effortmedium
Tools

Keywords: 4-scope, ActModifiers, booster, consumable, modifier, PhaseModifiers, RunModifiers, status effect, StepModifiers

/t1k:unity:dots-combat:modifiers

DOTS Modifiers — com.the1studio.dots-modifiers

Section titled “DOTS Modifiers — com.the1studio.dots-modifiers”

Namespace: DOTSModifiers.* | Package: com.the1studio.dots-modifiers Depends on: DOTSCore.*, DOTSCombat.*


  • Implementing run/act/step/phase scoped stat modifiers (wagers, daily modifiers, skill-tree bonuses, set bonuses)
  • Adding status effects (Poison, Burn, Freeze, Stun, Slow) that expire over time
  • Building a consumable / booster system (one-shot items that push modifiers onto a target)
  • Looking for RequestBufferSingletonHelper — the drain-and-clear singleton-buffer pattern
  • Using ModifierKind, ModifierElement, RunModifiers, ActModifiers, StepModifiers, PhaseModifiers
  • Projecting modifier elements into StatModifier via ModifierTranslator

The library uses four DynamicBuffer components, each with a distinct lifetime:

BufferCleared whenTypical use
RunModifiersRun resetWager choices, hero passives selected at run start
ActModifiersAct transitionDaily modifiers, weekly events scoped to one act
StepModifiersEncounter step advanceStep-only bonuses, single-encounter cards
PhaseModifiersPhase change (consumer-owned)Short-duration status effects, booster consumables

All four buffers hold ModifierElement structs. The element carries a ModifierKind discriminator so downstream apply-systems know which feature emitted the entry.


TypeLocationNotes
ModifierElementComponents/Generic payload — Kind, SourceId, Tier, ValueA, ValueB, FlagMask, ExpiresAtSeconds
RunModifiersComponents/IBufferElementData on run-scope singleton
ActModifiersComponents/IBufferElementData on act-scope singleton
StepModifiersComponents/IBufferElementData on step-scope singleton
PhaseModifiersComponents/IBufferElementData per target — status effects and consumables write here
ModifierTargetTagComponents/Marks entities eligible for modifier application
LastAppliedRollupHashComponents/FNV-1a gate — skips StatModifier rebuild if buffer unchanged

Status Effects (DOTSModifiers.StatusEffects)

Section titled “Status Effects (DOTSModifiers.StatusEffects)”
TypeNotes
StatusEffectVariantByte enum: Poison, Burn, Freeze, Stun, Slow
StatusEffectApplyRequestSingleton-buffer entry: TargetEntity, Variant, DurationSec, Magnitude
StatusEffectRequestSingletonTagMarker tag for the request singleton entity
StatusEffectActiveStatePer-frame rollup — IsImmobilized, SpeedMultiplier
StatusEffectApplySystemDrains request buffer → pushes ModifierElement into PhaseModifiers
StatusEffectTickSystemRewrites StatusEffectActiveState from PhaseModifiers each frame
TypeNotes
ConsumableActivationRequestSingleton-buffer entry: TargetEntity, pre-resolved Element
ConsumableRequestSingletonTagMarker tag for the consumable request singleton entity
IConsumableRegistry<TId>Contract for consumer catalogs — IsKnown(id), BuildModifierElement(id, time)
ConsumableApplySystemDrains buffer → appends Element to target’s PhaseModifiers
TypeNotes
ModifierApplySystemProjects all four buffer scopes into StatModifier rows via ModifierTranslator
ModifierExpirySystemPrunes PhaseModifiers entries when ElapsedTime > ExpiresAtSeconds
ModifierTranslatorStatic: ToStatModifier(ModifierElement)TierStatType, ValueA→value, FlagMask&0x3ModifierType
RequestBufferSingletonHelperLazy-create pattern for singleton-tag + request-buffer drain pattern

Two distinct status-effect systems coexist by design (Phase 4, Option A):

  • DOTSCombat.StatusEffect — the legacy combat-pipeline variant for generic live combat (MaxStacks, StackBehavior). Lives in DOTSCombat.*.
  • DOTSModifiers.StatusEffects.StatusEffectVariant — the typed roguelike variant (Poison/Burn/Freeze/Stun/Slow, DurationSec, Magnitude). Lives here.

If both packages are imported, use the fully qualified namespace to resolve the StatusEffectTickSystem ambiguity (see integration defect #6 in the BPC extraction handoff).


// Producer (managed, in UI layer):
var registry = this.container.Resolve<IConsumableRegistry<BoosterId>>();
var element = registry.BuildModifierElement(boosterId, (float)World.DefaultGameObjectInjectionWorld.Time.ElapsedTime);
var buffer = RequestBufferSingletonHelper.GetOrCreate<ConsumableRequestSingletonTag, ConsumableActivationRequest>(ref state);
buffer.Add(new ConsumableActivationRequest { TargetEntity = playerEntity, Element = element });
var buffer = RequestBufferSingletonHelper.GetOrCreate<StatusEffectRequestSingletonTag, StatusEffectApplyRequest>(ref state);
buffer.Add(new StatusEffectApplyRequest
{
TargetEntity = enemyEntity,
Variant = StatusEffectVariant.Poison,
DurationSec = 5f,
Magnitude = 10f, // 10 damage/second
});

Reference: references/4-scope-guide.md, references/consumable-pattern.md, references/status-effect-coexistence.md


  • ExpiresAtSeconds uses monotonic SystemAPI.Time.ElapsedTime — not per-encounter clock. Run-reset does NOT reset this clock. Use short durations or a custom expiry pass for encounter-scoped effects.
  • RequestBufferSingletonHelper is NOT Burst-compatible — it calls typeof(T).Name (managed). Call it from a non-Burst entry point; access the buffer directly in Burst code.
  • ModifierApplySystem cleans up its own rows using ModifierSource.Aura as the discriminator. Do NOT manually remove rows emitted by this system — the next apply pass overwrites them.
  • StatusEffectTickSystem fully rewrites StatusEffectActiveState each frame — it is a derived component, not a persistent source. Do not read it outside of the frame it was written.
  • Fully qualify when both DOTSCombat and DOTSModifiers are referenced in the same asmdef — StatusEffectTickSystem exists in both namespaces (integration defect #6).
  • ConsumableApplySystem acquires the EndSimulationEntityCommandBufferSystem singleton LAZILY — only on the cold (structural-change) branch, never unconditionally at OnUpdate start. The system has two paths: a hot path (target already has the PhaseModifiers buffer → inline buffer.Add, no ECB) and a cold path (target lacks the buffer → ecb.AddBuffer<PhaseModifiers> + AppendToBuffer). Calling SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>() at the top of OnUpdate makes the system hard-depend on EndSimulationEntityCommandBufferSystem existing in every world — it throws in worlds that don’t register the EndSim ECB (minimal / registry-contract test worlds, custom bootstrap worlds), silently failing the entire system update for BOTH paths. Correct pattern: gate the acquire behind the cold-path branch with a one-shot guard.
    EntityCommandBuffer ecb = default;
    bool ecbAcquired = false;
    // hot path: inline buffer.Add(), no ECB
    // cold path:
    if (!ecbAcquired)
    {
    ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>()
    .CreateCommandBuffer(state.WorldUnmanaged);
    ecbAcquired = true;
    }
    ecb.AddBuffer<PhaseModifiers>(req.TargetEntity);
    General rule: only acquire an ECB singleton inside the branch that actually performs the structural change — an unconditional acquire couples the system to that ECB system’s presence in every world. See t1k:unity:dots-core:entity-command-buffer. (Fixed in unity-dots-library d42be59.)