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