Skip to content

🧠 Status Effects System — Mental Model

This page explains how to think about Status Effects in RevFramework.

It is not an API reference.
It is a conceptual map that helps you decide:

  • Where does this behaviour belong?
  • Which extension point should I use?
  • What should I never touch?

If you understand this page, the API will feel obvious.


🎯 The Core Idea

Status Effects are controller-driven state mutations applied to actors over time.

A status is not “add a buff component”.

It is a runtime effect instance managed by a controller, with explicit rules for:

  • stacking
  • timing
  • potency scaling
  • resistance
  • immunity
  • authority

Everything in the system exists to protect that execution model.


🧱 The Single Orchestrator

StatusEffectController

There is exactly one authority on status effect truth:

StatusEffectController

It owns:

  • the active effect list
  • effect lifecycle
  • ticking and expiry
  • stacking rules
  • immunity checks
  • potency and resistance evaluation
  • runtime events

The controller is the only authority that may mutate status state.

If something modifies status state outside the controller, it is a bug.

Nothing else should:

  • tick status effects
  • mutate the active list
  • remove effects directly
  • modify controller state

If something outside the controller attempts to do these things directly, the system boundaries have likely been violated.


🔄 The Status Lifecycle

Every status effect follows the same conceptual lifecycle:

Apply
 ↓
Tick
 ↓
Refresh (optional)
 ↓
Expire / Remove

This lifecycle is always driven externally by the controller.

Effects do not advance themselves through this lifecycle.

Each stage has clear responsibilities.


▶️ Stage 1: Apply

Question: “A new effect is being applied — what should happen?”

During application the controller:

  1. checks immunity
  2. evaluates stacking rules
  3. applies potency scaling
  4. applies duration scaling
  5. calls the effect's Apply() method

The effect instance becomes part of the controller's active set.

What belongs here

  • stacking decisions
  • potency modifiers
  • duration scaling
  • source attribution

What does NOT belong here

  • UI updates
  • spawning visual effects outside the FX seam
  • direct mutation of other gameplay systems

Application is controlled by the controller.

Application is a state transition, not gameplay execution.

Effects should not perform gameplay logic here beyond initialization.


⏱️ Stage 2: Tick (Runtime Progress)

Active effects advance through controller-driven ticks.

The controller calls:

effect.Tick(target, deltaTime)

Effects never tick themselves.

All runtime behaviour happens during Tick.

This keeps behaviour consistent, controllable, and testable.

Why this matters

Centralized ticking guarantees:

  • deterministic behaviour
  • authority control
  • testability
  • predictable stacking interactions

The effect only implements behaviour — the controller owns time.


🔁 Stage 3: Refresh

Some stacking rules refresh the timer of an existing effect.

Examples:

Stacking Rule Behaviour
Replace Remove existing effect, apply new
Refresh Reset timer on existing instance
Stack Add a second instance

Refresh logic is controller-driven so stacking behaviour remains deterministic.


❌ Stage 4: Expire / Remove

An effect leaves the system when:

  • its timer reaches zero
  • it is explicitly removed
  • it is dispelled or cleansed

The controller then:

  1. calls Remove() on the effect
  2. removes it from the active set
  3. emits lifecycle events

Effects should clean up any side-effects during Remove.


🧩 Effect Instances vs Definitions

Status Effects support two ways of defining behaviour.

Runtime Effect Instances

Effects are runtime objects implementing:

IStatusEffect

They contain:

  • behaviour
  • timers
  • stacking rules

Example:

PoisonStatus
SlowStatus
BurnStatus

These instances live inside the controller.


Definition Assets

Definitions are ScriptableObject data assets.

StatusEffectDefinitionBase

They exist to:

  • configure effects in data
  • allow designers to author effects
  • provide reusable definitions for abilities or items

Definitions build runtime instances via:

BuildEffect()

Definitions never contain runtime behaviour.


🏷️ Effect Identity

Every effect has a typed identifier:

StatusId

The identifier is used for:

  • stacking decisions
  • queries
  • UI display
  • registry construction
  • integration logic

Prefer StatusId over raw strings in gameplay code.


🔌 Extension Points

Status Effects are designed to be extended through capability interfaces.

These allow behaviour to be layered without modifying the controller.

Extension points are additive.

They do not replace controller behaviour — they modify it.


Effect Capability Interfaces

Optional interfaces effects may implement:

Interface Purpose
IAdjustableMagnitude Allow potency scaling
IStatusMetadata Expose tags and dispel rules
IDispellable Allow removal via dispel

These describe what the effect supports.


Receiver-Side Modifiers

Actors may influence incoming effects.

Attach components implementing:

Interface Behaviour
IStatusPotency Amplifies effect magnitude
IStatusResistance Reduces duration
IStatusImmunity Blocks effects entirely

These are evaluated by the controller when effects are applied.


⚖️ Potency vs Resistance

These are often confused.

Potency

Controls how strong the effect is.

Examples:

  • stronger poison damage
  • stronger slow multiplier
  • stronger vulnerability modifier

Implemented through IStatusPotency.


Resistance

Controls how long the effect lasts.

Examples:

  • reduced stun duration
  • shorter poison effects
  • partial immunity to CC

Implemented through IStatusResistance.


⏱️ Time Sources

The controller advances time through a time provider abstraction.

ITimeSource

This allows:

  • scaled gameplay time
  • unscaled UI time
  • paused systems
  • custom time providers

Time is injected, not hardcoded.


🔐 Authority (Optional)

Status mutation can be gated by:

IStatusAuthority

This allows systems such as:

  • multiplayer servers
  • replay simulations
  • deterministic lockstep systems

to control who may tick or mutate statuses.

When authority is denied:

  • effects do not tick
  • effects do not apply
  • effects do not mutate

🧠 Built-In Status Effects

RevFramework includes several built-in effect implementations:

  • Poison
  • Burn
  • Slow
  • Haste
  • Stun
  • Vulnerability
  • Thorns

Additional effects may appear when optional integrations are present.

Effects are allowed to do nothing if their required integrations are not present.

They still:

  • exist
  • tick
  • expire normally

🔗 Integrations

The Status Effects system can integrate with other RevFramework modules.

For example:

Health Integration

Adds effects such as:

  • RegenStatus
  • ShieldStatus

These live in:

RevFramework > Integrations > StatusEffects > HealthIntegration

The base system does not depend on Health, but can integrate when available.


🎨 UI Representation

Status effects may be represented visually through UI components such as:

  • StatusBuffBar
  • StatusIconView
  • StatusIconLibrary

These are optional helpers built on top of controller events.


🚫 What Never Belongs Where

  • Effects should never tick themselves
  • Effects should never modify the controller’s internal list
  • UI should never mutate status state
  • Gameplay systems should not bypass the controller
  • Definitions should never contain runtime logic

If you feel the need to break these rules, you are likely using the wrong extension point.


🧠 The One-Sentence Model

Status Effects are controller-managed runtime effect instances applied over time and modified through a layered pipeline.


TL;DR

  • One controller owns status truth
  • Effects are runtime instances
  • Lifecycle: apply → tick → refresh → expire
  • Potency changes strength
  • Resistance changes duration
  • Extension seams keep the system modular
  • Integrations layer behaviour across gameplay systems