Skip to main content

Status System

Guide

How status stacks live on units at runtime, when they tick and decay, and which events keep your UI honest

Game Card Monitor Unit tab showing player and enemy units with HP, armor, and active status stacks

Authored status fields

FieldRuntime meaning
DisplayNameUser-facing name
DescriptionPlayer-facing description with optional tokens
IconUI icon
IsDebuffMarks the status as a debuff for filtering and presentation
StackRuleHow new stacks combine with existing stacks: Additive adds them, Max keeps the higher count, Replace overwrites
MaxStacksMaximum stack count; negative means no practical limit
DecayRuleWhen stacks decay: PerTurn loses one stack at the end of the owner's turn, Never waits for explicit removal
ShowStackCountWhether UI should show stack numbers
TagsAuthoring and FlowGraph filters
BehaviorFlowGraph that reacts to status lifecycle, hooks, and timings

The Behavior graph is where a status earns its keep; the demo's Poison, for example, deals 1 damage per stack at the start of the owner's turn, ignores armor, then loses one stack:

Poison status FlowGraph in the FlowGraph editor: turn-start entry dealing damage per stack, then reducing stacks

Runtime storage

Each UnitState stores status data:

Runtime dataMeaning
StatusesDictionary from GameStatus to stack count
StatusAppliersDictionary from GameStatus to the unit that applied it
ActiveHooksHook entries contributed by status and card behavior

Use these public helpers instead of reading the dictionaries directly:

NeedAPI
Stack countGCSApi.GetStatusStacks(unit, status)
Has at least N stacksGCSApi.HasStatus(unit, status, minStacks)
Who applied the statusGCSApi.GetStatusApplier(unit, status)
All statuses on a unitGCSApi.StatusesOn(unit)

StatusesOn returns an empty map when the unit is null or has no statuses

Applying and changing statuses

TaskAPI or node
Apply stacksGCSApi.ApplyStatus(target, status, stacks, source) or the Change Status node
Remove stacksGCSApi.RemoveStatus(target, status, stacks) or Remove Statuses / Change Status
Add or subtract by deltaGCSApi.ModifyStatusStacks(target, status, delta)
Re-run behavior without changing stacksThe Reapply Status node or the controller's RetriggerStatus

Status changes run through runtime rules and raise events; avoid manually editing UnitState.Statuses

Hooks versus lifecycle entries

Status behavior can do two different jobs

Behavior kindUse forExamples
Lifecycle entriesReact after something happenedOn Status Received, On Status Removed, On Turn Start
HooksChange a value before it is committedDamage Taken Hook, Card Cost Hook, Draw Count Hook

A hook that intends to change the value should end with a Write Hook node; lifecycle entries use normal action nodes

Decay and turn timing

Status decay and ticks are driven by the battle state machine; A status with PerTurn decay follows one loop from application to expiry:

TimingWhat can happen
PlayerTurnStartPlayer armor decay, player status start-turn behavior, delayed triggers
PlayerTurnEndDiscard phase, player status end-turn behavior, status tick/decay, hook ticking
EnemyPhase, per enemyEnemy status start-turn behavior, enemy action, enemy status end-turn behavior, status tick/decay
BattleEndBattle-end status behavior on all units

Use StatusTickedEventArgs when custom UI needs to animate a tick or decay; it carries the stacks and HP both before and after the tick

Status events

EventPayloadUse for
OnStatusChangedStatusChangedEventArgsRefresh a unit's status widget after stack changes
OnStatusTickedStatusTickedEventArgsAnimate damage, healing, or stack changes from a tick
OnStatusInflictedStatusTransitionEventArgsReact to incoming status application
OnStatusExpiredStatusTransitionEventArgsAnimate natural expiration
OnStatusRemovedStatusTransitionEventArgsAnimate cleanse or manual removal

StatusChangedEventArgs is the most useful UI refresh signal; the transition events are better for specialized effects and gameplay integrations

Damage and status attribution

Damage caused by a status carries source attribution through runtime hooks and damage events; when a status applies damage, DamageEventArgs tells subscribers:

FieldMeaning
SourceUnitIdUnit responsible for the damage, when known
TargetUnitIdUnit receiving the damage
AmountFinal amount after runtime rules
LethalWhether the target died from the damage

Use this for UI labels, statistics, and combat log entries; lethal damage should come from the event result because a stale HP snapshot cannot account for hooks and other changes in the same resolution

UI display rules

For status widgets:

  1. Listen to OnStatusChanged, OnStatusTicked, OnUnitHpChanged, and OnUnitDied as needed
  2. Refresh the unit's current status map from GCSApi.StatusesOn(unit)
  3. Use GameStatus.Icon, DisplayName, IsDebuff, ShowStackCount, and the stack count for display
  4. Hide statuses with zero stacks

The demo UnitView also listens to status events to drive procedural status visuals; custom UI can use the same event channel without depending on the demo view components

Common mistakes

MistakeBetter approach
Editing UnitState.Statuses directlyUse ApplyStatus, RemoveStatus, or ModifyStatusStacks
Using a lifecycle entry to modify damage before it happensUse the matching hook
Showing stack counts taken from an event aloneRefresh from GCSApi.StatusesOn(unit)
Assuming all statuses are debuffsCheck GameStatus.IsDebuff or tags
Treating removed and expired as the same eventUse OnStatusRemoved for manual removal and OnStatusExpired for natural expiration