Status Effects System — Public API¶
This page defines the supported, stable public API for the RevFramework Status Effects System.
This is the contract
If something is not listed here, it is not supported as a public integration point — even if it appears accessible in code.
Audience: Developers integrating Status Effects into gameplay, tools, UI, or other systems
Scope: Runtime public API and supported runtime components
Stability: Breaking changes to items listed on this page are avoided or clearly versioned
API Stability Policy
Items listed on this page are considered stable.
New public APIs and components may be added over time.
Internals may change without notice.
RevFramework guarantees stability only for documented public APIs and components.
Usage of internal or undocumented code is unsupported and may break without notice.
Core Concepts¶
- StatusEffectController is the runtime orchestration root.
- Status effects are controller-driven — effects do not self-register, self-tick, or self-remove.
- The controller is the sole owner of effect lifecycle.
- Effects are passive runtime objects and do not manage their own execution.
- Effect behaviour lives in effect instances implementing
IStatusEffect. - Construction is flexible: direct code construction, definition assets, or id-based registry creation.
- Optional behaviour is layered through metadata, potency, resistance, authority, and timing seams.
- The system avoids global state and does not require singletons.
Runtime Guarantees¶
Runtime guarantees
- Effects are always driven by the controller (never self-driven)
- Authority gating blocks both ticking and mutation
- Effects can exist without their target integrations
- Potency and duration are computed independently
Core Runtime Controller¶
StatusEffectController¶
The central runtime component responsible for applying, ticking, refreshing, querying, and removing status effects.
Responsibilities¶
- Apply and remove effect instances
- Advance active effects over time
- Enforce stacking behaviour
- Respect immunity and resistance
- Recompute potency and duration modifiers
- Gate ticking and mutation through authority (when enabled)
- Emit runtime events for gameplay, diagnostics, and UI
Gameplay systems typically interact with Status Effects through this controller.
Applying and Refreshing Effects¶
ApplyStatus¶
StatusApplyResult ApplyStatus(IStatusEffect effect)
StatusApplyResult ApplyStatus(IStatusEffect effect, StatusContext context)
Applies a concrete effect instance to the controller owner.
Supported behaviour includes:
- replace / refresh / stack handling
- immunity checks
- potency scaling
- duration scaling
- source attribution via
StatusContext
Use the overload with StatusContext when you want item / ability / instigator attribution.
ApplyOrRefresh¶
StatusApplyResult ApplyOrRefresh(Func<IStatusEffect> build, StatusContext context)
Builds and applies an effect through a factory delegate.
Useful for:
- centralised refresh flows
- lazy construction
- code-driven ability effects
- integrations that want one apply path without pre-constructing the effect
Factory Guarantees¶
Authority is checked before build is invoked.
A denied application never runs the factory, so side effects inside it — spawning VFX, consuming a charge, rolling RNG — do not happen for an application the controller rejects.
If build throws, the exception is logged and the call performs no mutation. It does not propagate out of this method.
build is invoked at most once per call. Authority and immunity are each evaluated once per application, so caller-supplied authority and immunity implementations are not queried twice.
⚠️ The factory still runs when the target is immune — immunity is evaluated on the built instance. Return early from your own code if you need to avoid constructing an effect for an immune target.
No mutation occurs when build is null, build returns null, authority denies, or the target is immune.
Removal / Cleanse / Dispel¶
void RemoveStatus(StatusId id)
void RemoveStatusAt(int index)
void ClearAll()
Removes active effects from the controller.
Dispel / Cleanse¶
int Dispel(DispelType type, int minTier = 0)
int CleanseByTag(StatusTag tagMask)
Removes active effects matching the requested dispel bucket or tag mask, and returns how many were removed.
minTier is a floor on the effect's own DispelTier, not a strength setting: an effect is removed only when effect.DispelTier >= minTier. Raising minTier therefore removes fewer effects, never more. Every status shipped with the framework declares DispelTier 1, so minTier of 0 or 1 reaches them all and anything higher reaches nothing until you author a higher-tier effect yourself. Use it to make a dispel selective — a minTier of 3 strips a DispelTier 3 curse and leaves a DispelTier 1 chill in place.
Runtime Queries¶
bool HasStatus(StatusId id)
int GetStackCount(StatusId id)
IReadOnlyList<IStatusEffect> Active { get; }
bool TryGetFirst(StatusId id, out IStatusEffect effect)
IStatusEffect GetFirstOrNull(StatusId id)
int TryGetAll(StatusId id, List<IStatusEffect> buffer)
bool TryGetFirstContext(StatusId id, out StatusContext context)
StatusContext GetContext(IStatusEffect effect)
These methods provide the supported runtime query surface for gameplay, UI, and tools.
Notes:
Activeis a live, read-only view- Do not modify the collection externally
- Ordering is implementation-defined and should not be relied upon
Runtime Policy Controls¶
void SetStackCap(StatusId id, int max)
void ClearStackCap(StatusId id)
void SetPerSourceStacks(StatusId id, bool enabled = true)
void SetTimeMode(StatusTimeMode mode, MonoBehaviour custom = null)
void RecomputePotencyForAll()
void RecomputeDurationsForAll(bool pulseFx = true)
These methods modify controller behaviour at runtime.
Read-only counterpart:
StatusTimeMode TimeMode { get; }
TimeMode reports the mode the controller actually holds, which is not necessarily the one a UI last displayed — a caller that sets the mode before the controller exists silently has no effect. Read this back when the displayed mode has to match reality.
Lifecycle Events¶
event Action<StatusId> StatusApplied
event Action<StatusId> StatusRefreshed
event Action<StatusId> StatusRemoved
event Action<StatusId> StatusExpired
These events provide the primary gameplay / UI event surface.
Effect Model¶
IStatusEffect¶
Core runtime contract representing a single status effect instance.
StatusId Id { get; }
float Duration { get; }
float TimeRemaining { get; }
bool IsExpired { get; }
StatusStackingRule Stacking { get; }
void Apply(GameObject target)
void Tick(GameObject target, float deltaTime)
void Remove(GameObject target)
void Refresh(float? newDuration = null)
Effects are runtime instances controlled by StatusEffectController.
TimedStatusEffect¶
Recommended base class for most custom effects.
Provides:
- duration handling
- expiry logic
- refresh behaviour
- FX seam integration
Effect Construction Paths¶
RevFramework supports three official construction paths.
1. Direct Runtime Construction¶
controller.ApplyStatus(new PoisonStatus(5f, 10f));
Used for code-driven gameplay.
2. Definition Assets¶
abstract class StatusEffectDefinitionBase : ScriptableObject
{
IStatusEffect BuildEffect();
}
Used for data-driven authoring.
Designers create ScriptableObject assets which build runtime effects.
Intended Usage of StatusEffectDefinitionBase¶
Definitions are recommended when:
- designers configure status behaviour
- abilities/items reference reusable effect data
- status configuration should live in assets
Direct construction is recommended when:
- gameplay logic creates effects directly in code
- effects are dynamic or procedurally generated
Both workflows are fully supported.
3. Registry Construction¶
StatusRegistry.TryBuild(StatusId id, float duration, float magnitudeOrMult, out IStatusEffect effect)
Used when:
- systems create effects by id
- integrations require decoupled construction
- plugins register new effect types
Note:
The registry is optional and primarily intended for:
- decoupled systems
- id-driven construction
- plugin registration
It is not required for normal gameplay usage.
Status Identity and Tags¶
StatusId¶
Strongly typed identifier used throughout the system.
Prefer StatusId over raw strings for gameplay code.
StatusId comparison is ordinal, so case matters¶
Two ids are equal only if their strings match exactly, including case — StatusId hashes and compares with StringComparison.Ordinal. There is no normalising overload and no case-insensitive fallback, so TryBuild simply misses on a case mismatch and returns false.
Every id RevFramework ships is lowercase. "Poison" is not "poison", and a designer who types the capitalised form into an inspector field gets an effect that is never built, with no error beyond whatever the calling component chooses to log.
The ids RevFramework ships¶
Typed constants live on StatusRegistry.Id; the raw string forms live on StatusRegistry.Id.Strings. Prefer the typed constants — they are the reason this table rarely needs to be read.
| Id string | Typed constant | Effect | Registered by |
|---|---|---|---|
poison | StatusRegistry.Id.Poison | Poison damage over time | Status Effects |
burn | StatusRegistry.Id.Burn | Burn damage over time, optional spread | Status Effects |
slow | StatusRegistry.Id.Slow | Movement slow | Status Effects |
haste | StatusRegistry.Id.Haste | Cooldown / speed multiplier | Status Effects |
vulnerability | StatusRegistry.Id.Vulnerability | Damage-taken multiplier | Status Effects |
stun | StatusRegistry.Id.Stun | Crowd control | Status Effects |
thorns | StatusRegistry.Id.Thorns | Damage reflection | Status Effects |
regen | StatusRegistry.Id.Regen | Healing over time | Health integration |
shield | StatusRegistry.Id.Shield | Absorb buffer | Health integration |
regen and shield are registered by StatusEffects.HealthIntegration, which is only present when Health is. In a project without Health, TryBuild returns false for those two exactly as it would for an id that does not exist — the id is real, the builder is not.
Registering your own id puts it in the same table and the same ordinal comparison. Pick a convention and hold to it; lowercase matches the first-party ids and is the one this system's own examples use.
StatusTag¶
Bit flags describing effect categories such as:
- Buff / Debuff
- DOT / HOT
- CC
- Movement
- Poison / Fire / Magic
Used by:
- immunity
- resistance
- UI filtering
- dispel logic
Extension Seams¶
Effect Capability Interfaces¶
Optional effect interfaces:
IAdjustableMagnitudeIStatusMetadataIDispellable
Receiver‑Side Modifier Interfaces¶
Attach to actors:
IStatusPotencyIStatusResistanceIStatusImmunity
These influence how incoming effects behave.
Math Service¶
IStatusMathService
Allows custom potency / duration calculation pipelines.
Time Source¶
ITimeSource
Custom time provider for the controller.
Authority¶
IStatusAuthority
Used for multiplayer or restricted mutation rules.
When authority is enabled:
- effects do not tick
- effects do not apply
- effects do not mutate
unless authority resolves successfully
The controller binds the serialized provider slot on enable, and otherwise resolves its authority lazily, on its first gated check, caching the answer. It searches again whenever the scene's authority cache is invalidated — which the shipped StatusAuthorityBinder does as it is enabled, disabled and destroyed — so a binder added, removed or moved after the fact is picked up without a call. The authority it holds is re-tested for liveness on every check, and a dropped one reopens the search.
With requireAuthority on and nothing resolved, the controller denies: every apply returns StatusApplyResult.NoAuthority and every tick is refused. It does not behave permissively — that is Inventory's model, not this one.
The one case that needs a call is a custom IStatusAuthority — anything that is not a StatusAuthorityBinder — appearing mid-session, because nothing else moves the scene cache's epoch:
void RefreshAuthority()
on the controllers it should gate.
This paragraph used to say the opposite, three ways
It read "resolves its authority once, on enable, and caches the answer", and said a controller with nothing resolved "keeps behaving permissively". Resolution is lazy rather than on enable, it re-opens on invalidation rather than happening once, and the absent-authority default is a refusal — which the shipped RequireAuthority_WithNoAuthority_DeniesApply test has always pinned, and which the FAQ, the Core README, the inspector and StatusApplyResult's own XML all describe correctly. The sentence was copied from SceneInventoryService.RefreshAuthority, where it is true because Inventory is permissive when no authority is present.
Utility Helpers¶
StatusUtility¶
Supported helpers:
ComputeFinalPotency()
ComputeFinalDurationScale()
Apply()
Useful for previews, diagnostics, or convenience application.
Built‑In Effect Types¶
- PoisonStatus
- BurnStatus
- SlowStatus
- HasteStatus
- StunStatus
- VulnerabilityStatus
- ThornsStatus — reflection needs the Health integration; see below
Integration Effects (Health Module)¶
When the Health integration module is present, additional effects become available:
RegenStatusShieldStatus
These live in:
RevFramework > Integrations > StatusEffects > HealthIntegration
They integrate the Status Effects system with the Health pipeline.
HealthStatusHooks.TickSink¶
Damage-over-time effects apply their ticks through HealthStatusHooks.ApplyStatusTick. Status Effects references only Core, so it cannot name the types Health uses to classify damage — and referencing Health would end the guarantee that this system compiles with Health deleted.
TickSink is how that gap is closed. The Health integration installs a route that turns the status id into a damage type and applies the tick through the health pipeline, so burn ticks arrive tagged as fire and poison ticks as poison, and resistances and immunities can see them.
static Action<GameObject, int, string> TickSink; // (target, amount, statusId)
With no route installed the tick is applied as an untyped damage call, which is the behaviour when Health is absent. Replace it only if you are routing status damage through your own pipeline; a status with no matching damage type is passed through untyped rather than given an invented one.
HealthStatusHooks — the reflection route¶
ThornsStatus lives in Status Effects core and carries only a percentage, for the same reason: damage reflection is a Health rule, and naming it here would end the compiles-without-Health guarantee. The Health integration installs the route, and the status asks for it on apply and gives it back on removal.
static Action<GameObject, float> ReflectPushSink; // (target, percent01)
static Action<GameObject, float> ReflectPopSink; // (target, percent01)
static bool TryPushReflect(GameObject target, float percent01);
static void PopReflect(GameObject target, float percent01);
TryPushReflect returns whether anything was installed, which is how a caller knows it owes a matching PopReflect. The underlying rule keeps a stack of pushed percentages and pops one matching entry, so an unmatched pop removes somebody else's contribution and an unmatched push leaves reflection installed for the object's life.
Thorns reflects nothing without the Health integration
The status still applies, ticks, expires and dispels — there is simply no damage pipeline to reflect through. This was also true with Health installed until 1.2.0: the bridge that installs the rule shipped with no callers, so Thorns did nothing whatever it was configured to do. If you wrote code around that, it now reflects.
Reflection additionally needs the attacker to expose an IHealthMutator, and a hit already tagged Reflect is never reflected again.
HealthStatusHooks — the damage-taken route¶
The same arrangement, for the same reason, one status along. VulnerabilityStatus carries a multiplier and no knowledge of how damage is applied; the Health integration installs the route that scales it.
static Action<GameObject, float> DamageTakenPushSink; // (target, multiplier)
static Action<GameObject, float> DamageTakenPopSink; // (target, multiplier)
static bool TryPushDamageTaken(GameObject target, float multiplier);
static void PopDamageTaken(GameObject target, float multiplier);
TryPushDamageTaken reports whether anything was installed, on the same pairing rule as TryPushReflect: the Health rule keeps a stack of factors and pops one matching entry, so an unmatched pop takes somebody else's contribution and an unmatched push leaves the target permanently more fragile. A multiplier of exactly 1 installs nothing — scaling by one is not a change, and an installed entry would still have to be popped.
A target that exposes IDamageTakenModifier is used instead. A project that implements it has said where incoming-damage scaling belongs, and routing past it to Health would apply the debuff twice on an object carrying both.
Vulnerability changed no damage before 1.2.0
VulnerabilityStatus looked only for an IDamageTakenModifier component, and nothing in the product implements that interface — so the status was inert in every shipped configuration, Health installed and a HealthSystem on the target included. It applied, ticked, showed in the buff bar and dispelled correctly, and the target took identical damage throughout. Health's DamageTakenMultiplierRule had the matching behaviour the whole time; it declared only IDamageRule, so the lookup never found it. If you wrote code around that, Vulnerability now changes damage.
Supported UI Components¶
Optional UI helpers:
StatusBuffBarStatusIconViewStatusIconLibrary
These provide a simple UGUI status bar implementation.
TL;DR¶
TL;DR
If it appears on this page, it is a supported public API.
Anything not listed should be treated as internal implementation detail.