Skip to content

Status Effects — System Guarantees Matrix

This page defines the behavioural contract of the Status Effects system.

No marketing. No implication. Just guarantees — and explicit non‑guarantees.


Quick Navigation

  • Core Controller
  • Effect Lifecycle
  • Stacking
  • Potency / Resistance
  • Authority
  • Application Results
  • Time / Ticking
  • Integrations
  • UI
  • Built‑in Effects
  • Determinism
  • Public API
  • Non‑Guarantees
  • Final Summary

1. Core Controller

StatusEffectController

Aspect Guarantee
Controller owns status truth ✔ Yes
Effects self-register with controller ❌ Never
External mutation of active effects list ❌ Not supported
Multiple controllers per scene supported ✔ Yes
Controller required for runtime behaviour ✔ Yes
UI required ❌ No
Inspector configuration required ❌ No
ApplyStatus before the controller's OnEnable ✔ Supported — services resolve on demand, so a sibling earlier in component order can apply on load

Guarantee

The controller is the only supported mutation authority.

All application, refresh, and removal of effects must go through StatusEffectController.

Direct mutation of effect instances is unsupported.


2. Effect Lifecycle

Aspect Guarantee
Effects applied via controller ✔ Yes
Effects tick themselves ❌ Never
Controller drives ticking ✔ Yes
Effects may refresh duration ✔ Yes
Controller handles expiry ✔ Yes
Effects removed directly outside controller ❌ Not supported

3. Stacking

Aspect Guarantee
Stacking rule defined per effect ✔ Yes
Replace behaviour supported ✔ Yes
Refresh behaviour supported ✔ Yes
Stack behaviour supported ✔ Yes
Per‑source stacking supported ✔ Yes
Stack caps configurable ✔ Yes
Stacking implemented inside effects ❌ Not supported

4. Potency / Resistance

Aspect Guarantee
Potency scaling supported ✔ Yes
Duration scaling supported ✔ Yes
Potency applied via providers ✔ Yes
Resistance applied via providers ✔ Yes
Potency compounding across recalculations ❌ Never
Effects required to support potency ❌ Optional
Overlapping aura zones ✔ Compose — potency multiplies, resistance takes the minimum
Result depends on which zone was entered first ❌ Never for resistance. ❌ Never for potency across two zones; ⚠ last-bit only across three or more
One zone leaving removes another's scaling ❌ Never

Potency is recomputed from current providers each time it is evaluated.

It is not accumulated or compounded across recalculations.

Overlapping StatusAuraZones each contribute separately to the provider on the actor, rather than sharing one configuration. A 2× poison aura crossing a 0.5× magic aura gives 1×, whichever order the actor entered them, and stepping out of one leaves the other's scaling intact.

Three or more zones agree to the last bit, not bit-for-bit. Resistance takes a minimum, which is order-independent outright. Potency multiplies, and the two-zone case above is exactly order-independent because commuting two operands is something floating point does guarantee. Three or more re-bracket the product, and IEEE 754 multiplication is not associative — so entry order can move the result by a few units in the last place. Measured with four factors (1.1, 0.9, 1.3, 0.7) in single precision across all 24 orders: three distinct products, differing at the eighth decimal.

That is invisible in a single-player game and it is not invisible in a lockstep one. If you are comparing simulation state across machines, this is one of the places to look; it is not a defect being worked on, because a bit-exact product would mean sorting contributions on every evaluation for a difference no player can see.

Corrected in 1.3.0

This row read "❌ Never (both rules are order-independent)". The provider's own remarks defended it with "multiplication is commutative", which is true and is not the property required — accumulating a product over a collection needs associativity.


5. Authority

Aspect Guarantee
Authority gating supported ✔ Yes
Authority binder optional ✔ Yes
Effects tick without authority ❌ Not allowed
Effects mutate without authority ❌ Not allowed
Caller told when authority refuses ✔ Yes — StatusApplyResult.NoAuthority, since 1.3.0
Authority affects read queries ❌ No
Disabled or destroyed authority ✔ Stops answering; resolution reopens on the next check, for a custom IStatusAuthority as well as for the shipped binder
Disabled or inactive binder on the actor or a parent ✔ Never handed out. All five resolution steps apply the same liveness rule
Authority published after the controller The shipped binder is picked up on its own (it invalidates the scene cache as it enables). A custom IStatusAuthority appearing mid-session needs RefreshAuthority() on the controllers it should gate
Controller disabled and re-enabled ✔ Resolves again. It used to be stranded denied: the enable cleared the held authority but left the "already searched" memo pinned
Scope Per scene. The cache is consulted before the context's hierarchy, so the first authority any controller resolves answers for every controller in the scene; the controller is the argument to HasAuthority
Multiplayer replication ❌ Not provided

5a. Application Results

Aspect Guarantee
Result returned for every application ✔ Yes — StatusApplyResult, since 1.3.0
Refusal reasons distinguishable ✔ Yes — authority, immunity, no effect, factory threw
Silent failure ❌ Never, for ApplyStatus and ApplyOrRefresh
Refusal also raised as an event ✔ Yes — StatusApplyRefused(StatusId, StatusApplyResult)
Event raised on success ❌ No — refusals only
Removal and cleanse report a reason ❌ No — RemoveStatus is void; Dispel / CleanseByTag return counts

ApplyStatus and ApplyOrRefresh returned void before 1.3.0, so an authority denial, a null effect and target immunity were one silent nothing and this matrix promised the gate without saying the caller is never told it fired. Both now return a reason, and every refusal also raises StatusApplyRefused for anything that is not the caller — a HUD or a combat log watching an aura it does not own.

Ignoring the return value is safe

Both methods were void, so existing calls discard the new value and behave exactly as they did. Nothing that only calls these methods needs changing; only a project that wrote its own IStatusEffectController implementation has to add the return type.


6. Time / Ticking

Aspect Guarantee
Controller-driven ticking ✔ Yes
Scaled time supported ✔ Yes
Unscaled time supported ✔ Yes
Paused time supported ✔ Yes
Custom time source supported ✔ Yes
Direct use of UnityEngine.Time required ❌ No

7. Integrations

Aspect Guarantee
Core system independent of other modules ✔ Yes
Health integration optional ✔ Yes
Movement integration optional ✔ Yes
Integration bridges required ❌ No
Core depends on integrations ❌ Never
Effects function without integrations present ✔ Yes

8. UI

Aspect Guarantee
UI required for system operation ❌ No
Built-in buff bar available ✔ Yes
Custom UI supported ✔ Yes
UI allowed to mutate status state ❌ Never

UI must observe state via events or queries.

UI must never drive or mutate status logic.


9. Built‑in Effects

Aspect Guarantee
Built-in effect examples included ✔ Yes
Built-in effects required ❌ No
Custom effects supported ✔ Yes
Definitions optional ✔ Yes

Determinism

Aspect Guarantee
Controller lifecycle deterministic ✔ Yes
Behaviour deterministic given stable inputs ✔ Yes
Network determinism provided ❌ Not guaranteed
Cross-system determinism guaranteed ❌ Not guaranteed

Determinism assumes:

  • consistent time input
  • consistent provider composition
  • consistent execution order from the calling environment

11. Public API

Aspect Guarantee
Stable controller API surface ✔ Yes
Stable extension interfaces ✔ Yes
Internal controller helpers public ❌ No
Integration bridges public Optional

Non‑Guarantees

Status Effects does not guarantee

  • Multiplayer replication
  • Network prediction or reconciliation
  • Atomic mutation across external gameplay systems
  • Deterministic behaviour across machines without external synchronization
  • Safe mutation of status state outside StatusEffectController
  • Deterministic ordering of external provider discovery
  • Automatic UI updates without subscribing to events
  • Ordering of Active effects list

Final Summary

Layer Strong Guarantee Best Effort Not Guaranteed
Controller ownership of effects
Mutation through controller only
Deterministic lifecycle execution
Potency / resistance scaling
Built-in UI helpers
Multiplayer replication
Cross-system atomic behaviour

System Philosophy

System philosophy

Status Effects is:

  • Controller‑driven
  • Instance‑based
  • Extension‑oriented
  • Authority‑aware
  • Explicit about guarantees