Status Effect System

Status: Designed Author: user + game-designer Last Updated: 2026-03-28 System Index: #6 — MVP, Feature Layer

Overview

The Status Effect System manages buffs and debuffs on players and enemies — applying, stacking, ticking, and clearing them. Status effects are data-driven Godot Resources (.tres), following the same pattern as cards and enemies. Effects modify how other systems calculate damage, block, and healing through multipliers and flat bonuses. The system does not calculate damage itself — it provides modifiers that the Card Effect Resolver and Player Resources query during their calculations.

Player Fantasy

"I can stack advantages and exploit enemy weaknesses." Applying Vulnerable then hitting with a big attack feels powerful. Getting debuffed creates urgency — "I need to kill that enemy before Poison kills me." In co-op, one player debuffing while another deals damage is the core coordination loop.

Detailed Design

Core Rules

  1. Status effects are identified by StringName (e.g., "strength", "vulnerable", "poison").
  2. Every status has a stack count (integer ≥ 1). Applying a status that already exists adds to the stack count.
  3. Statuses have a duration type: PERMANENT (lasts until removed), TIMED (decreases by 1 each turn), or TICKED (deals effect then decreases).
  4. Statuses can exist on both players and enemies. The same status ID behaves identically on either.
  5. Status data is read-only resources. Runtime state (who has what, stack counts) is tracked per-entity by the Combat System.
  6. All status mutations are server-authoritative.

StatusData Resource Schema

FieldTypeDescription
idStringNameUnique identifier (e.g., "vulnerable")
display_nameStringPlayer-facing name
descriptionStringTooltip text (supports {stacks} token)
duration_typeDurationType enumPERMANENT, TIMED, TICKED
modifier_typeStatusModifierType enumDAMAGE_DEALT_MULT, DAMAGE_TAKEN_MULT, FLAT_DAMAGE, TICK_DAMAGE, BLOCK_SHARE, NONE
modifier_valuefloatThe modifier's magnitude (e.g., 1.5 for Vulnerable)
stacks_affectStackBehavior enumINTENSITY (stacks change magnitude), DURATION (stacks change duration), NONE
icon_idStringNameReference to status icon asset
is_debuffboolTrue if negative (for UI coloring and future cleanse mechanics)

Enums

  • DurationType: PERMANENT, TIMED, TICKED
  • StatusModifierType: DAMAGE_DEALT_MULT, DAMAGE_TAKEN_MULT, FLAT_DAMAGE, TICK_DAMAGE, BLOCK_SHARE, NONE
  • StackBehavior: INTENSITY, DURATION, NONE

V1 Status Effects

StatusDurationModifierStack BehaviorEffect Description
StrengthPERMANENTFLAT_DAMAGE, +1 per stackINTENSITY+X damage on all ATTACK actions. Stacks = damage bonus.
VulnerableTIMEDDAMAGE_TAKEN_MULT, 1.5DURATIONTake 50% more damage. Stacks = turns remaining.
WeakTIMEDDAMAGE_DEALT_MULT, 0.75DURATIONDeal 25% less damage. Stacks = turns remaining.
PoisonTICKEDTICK_DAMAGE, 1 per stackINTENSITYLose X HP at start of turn, then stacks decrease by 1.
LinkedTIMEDBLOCK_SHARE, 0.5DURATIONWhen this entity blocks, linked partner gains 50% of that block. Stacks = turns remaining.

Application Rules

apply_status(target, status_id, stacks):
    if target already has status_id:
        if status.stacks_affect == INTENSITY:
            target.statuses[status_id].stacks += stacks
        elif status.stacks_affect == DURATION:
            target.statuses[status_id].stacks += stacks  # extends duration
    else:
        target.statuses[status_id] = { stacks: stacks }

Tick Rules (start of turn)

for status in entity.statuses:
    if status.duration_type == TICKED:
        # Apply tick effect
        if status.modifier_type == TICK_DAMAGE:
            deal_damage(entity, status.stacks)  # Poison: stacks = damage
        status.stacks -= 1
    elif status.duration_type == TIMED:
        status.stacks -= 1
    # PERMANENT: no tick, no decrease

    if status.stacks <= 0:
        remove_status(entity, status.id)

Tick timing: Player statuses tick at the start of the player phase. Enemy statuses tick at the start of the enemy phase.

Query Interface

Other systems query the Status Effect System to get modifiers:

get_damage_dealt_modifier(entity) -> float:
    modifier = 1.0
    if entity has "weak": modifier *= 0.75
    flat_bonus = 0
    if entity has "strength": flat_bonus += strength.stacks
    return { multiplier: modifier, flat: flat_bonus }

get_damage_taken_modifier(entity) -> float:
    modifier = 1.0
    if entity has "vulnerable": modifier *= 1.5
    return { multiplier: modifier, flat: 0 }

States and Transitions

Per-entity status tracking is a simple dictionary:

entity.statuses = {
    "vulnerable": { stacks: 2 },
    "strength": { stacks: 3 }
}

No state machine — statuses are added, modified, and removed from the dictionary.

Interactions with Other Systems

SystemDirectionInterface
Player ResourcesQueried byPlayer Resources calls get_damage_taken_modifier() during damage resolution
Card Effect ResolverCalled by, queried byResolver calls apply_status() for APPLY_STATUS effects. Queries get_damage_dealt_modifier() for damage cards.
Enemy Data & AIBidirectionalEnemies can have statuses. Enemy BUFF/DEBUFF actions call apply_status().
Combat SystemCalled byCombat triggers status ticks at phase boundaries
Combat UIReadsUI displays status icons with stack counts on player/enemy portraits
Co-op MechanicsReads/writesLinked status is applied by Assist cards, queried during block resolution

Formulas

Damage Modification Pipeline

Full pipeline (owned by Card Effect Resolver, using Status Effect System modifiers):

1. base_value = card_effect.value + effect_modifiers (from EffectModifier resources)
2. flat_bonus = attacker.strength_stacks (if ATTACK action)
3. pre_mult_value = base_value + flat_bonus
4. attacker_mult = get_damage_dealt_modifier(attacker).multiplier  # Weak: 0.75
5. after_attacker = floor(pre_mult_value * attacker_mult)
6. target_mult = get_damage_taken_modifier(target).multiplier  # Vulnerable: 1.5
7. final_damage = floor(after_attacker * target_mult)

Order matters: Weak applies before Vulnerable. This is the Slay the Spire convention.

Example Calculations

8 damage, attacker has 2 Strength, target is Vulnerable:

base = 8, flat = 2, pre_mult = 10
attacker_mult = 1.0 (no Weak), after_attacker = 10
target_mult = 1.5, final = floor(10 * 1.5) = 15

6 damage, attacker is Weak, target is Vulnerable:

base = 6, flat = 0, pre_mult = 6
attacker_mult = 0.75, after_attacker = floor(6 * 0.75) = 4
target_mult = 1.5, final = floor(4 * 1.5) = 6

Poison 5 ticks on a player:

damage = stacks = 5, applied as unblockable HP loss
stacks becomes 4 after tick

Linked Block Sharing

When player A gains X block and has Linked status:
    partner = linked_partner(A)
    shared_block = floor(X * 0.5)
    partner.block += shared_block

Edge Cases

CaseResolution
Apply 0 stacks of a statusNo-op. Don't add a status with 0 stacks.
Status stacks overflow (e.g., 999 Strength)No hard cap in V1. If this becomes a problem, add a per-status max.
Poison kills a playerValid. Poison damage at start of phase can kill. Dead before they play cards.
Poison on an enemyValid. Players could have future Poison cards. Ticks at start of enemy phase.
Vulnerable + Weak on same entityBoth apply. Weak reduces outgoing damage, Vulnerable increases incoming. They affect different calculations.
Applying Linked to a player in a 1-player gameNo partner to link to. Status is applied but has no effect (no partner to share block with).
Linked: both partners gain block simultaneouslyEach triggers share for the other. To prevent infinite loop: block sharing does NOT trigger further sharing. Shared block is "passive" — it doesn't count as "gaining block" for Linked purposes.
Status applied to dead entityRejected. Dead entities cannot receive statuses.
All statuses cleared (future cleanse mechanic)Remove all entries from entity's status dictionary. Signal UI to update.
Status with TIMED duration at exactly 1 stackNext tick reduces to 0, status is removed. Effectively "lasts 1 turn."

Dependencies

Upstream

SystemDependency TypeInterface
Player ResourcesHardStatus modifiers affect damage calculations in Player Resources

Downstream

SystemDependency TypeInterface
Card Effect ResolverHardResolver queries modifiers and applies statuses
Combat SystemHardCombat triggers ticks at phase boundaries
Enemy Data & AIHardEnemy actions can apply statuses
Co-op MechanicsHardLinked status is a co-op mechanic
Combat UIHardDisplays status icons and stack counts

Contracts

  • Status effects are modifiers, not executors. They don't deal damage directly — they modify calculations done by other systems.
  • Exception: Poison TICK_DAMAGE is executed by the Status Effect System during tick, bypassing normal damage resolution (no Block, no Vulnerable modifier on Poison damage).
  • Status data resources are read-only. Runtime stacks are tracked separately per entity.

Tuning Knobs

KnobDefaultSafe RangeAffects
Vulnerable multiplier1.51.25-2.0Burst damage potential, debuff card value
Weak multiplier0.750.5-0.9Defensive debuff power
Poison tick rate1 per stack, -1 stack per turnFixed for V1Poison lethality, DOT pressure
Linked share ratio0.50.25-1.0Co-op block efficiency
Strength per stack+1 damage+1 to +2Scaling power, enemy threat from buff actions

Interaction warning: Vulnerable (1.5x) stacks multiplicatively with Strength (+flat). A 6-damage attack with 3 Strength = 9, with Vulnerable = floor(9 * 1.5) = 13. Small Strength values have outsized impact when combined with Vulnerable.

Acceptance Criteria

#CriterionVerification
1All V1 status effects load without errorsUnit test: load each .tres, assert valid fields
2Applying existing status adds stacks correctlyUnit test: apply Vulnerable 2, then Vulnerable 1 → stacks = 3
3TIMED statuses decrease and remove at 0Unit test: Vulnerable 2, tick, assert 1, tick, assert removed
4Poison deals damage then decreasesUnit test: Poison 5, tick → 5 damage dealt, stacks = 4
5Weak reduces damage by 25% (floored)Unit test: 7 damage, Weak → floor(5.25) = 5
6Vulnerable increases damage by 50% (floored)Unit test: 7 damage, Vulnerable → floor(10.5) = 10
7Strength adds flat damage to attacksUnit test: 6 base + 3 Strength → 9 damage
8Linked shares 50% block to partner (floored)Unit test: player gains 7 block, partner gains floor(3.5) = 3
9Linked block sharing doesn't cause infinite loopUnit test: both players Linked, player A gains 10 block → A has 10, B gains 5, no further sharing
10Poison bypasses BlockUnit test: player has 20 block, Poison 5 ticks → HP reduced by 5, Block unchanged
Built with LogoFlowershow