STATUS EFFECTS & BUFF STACKING

Other Systems › Status Effects & Buff Stacking

Every buff and debuff in the game belongs to exactly one ability. Two different abilities stack; one ability never stacks with itself. Magnitudes on the same stat combine additively, never multiplicatively, so a +50% and a +15% is +65% — not ×1.725.

Identity: a status is its ability

A status is identified by the ability that applied it, not by the stat it moves. Every buff payload a move produces is stamped at build time with:

The key is stamped onto every buff clause of the move — its self buff, its ally buff, its single-target ally buff and its Synergy clause — so all clauses of one ability share one identity. Riders, marks and DoTs applied to enemies keep their authored names (Mothervenom, Impale) and are identified by that name.

Stacking rules

SituationResult
Two different abilities buff the same statBoth statuses exist side by side. Their magnitudes add.
Two clauses of the same ability land on one unitOne status. Same key, so they merge — they do not stack.
The same ability is cast againDuration refreshes to the new cast. Magnitude keeps whichever is stronger. Never a second instance.

"Stronger" means further from the baseline in the direction the payload points: 1.0 for multipliers, 0 for flat adds. A +50% Attack buff re-applied at +35% stays +50%; a −40% Attack debuff re-applied at −25% stays −40%. The chip takes the name of the cast whose magnitude survived, so a weaker later cast cannot relabel a stronger running buff.

This is what caps buff spam. Stoke has Cooldown 1; without same-ability merging a player would reach +50%, +100%, +150% on consecutive turns.

Aggregation and clamps

Every status contributing to a stat has its delta summed once, then applied:

atkMult = clamp(1 + Σ(atkMul − 1) × stacks, 0.30, 2.50)

Legacy named statuses that carry no numeric payload contribute fixed deltas: Slagbite and Fracture −22% Defense, Gravechill −18% Attack and −2 Speed, Cinch −3 Speed, Doomrune +18% damage taken, Warded −18% damage taken (the Status_* rows of codex/stat_model.csv, which the game reads; the numbers on this page are filled from that file).

The named statuses

Most statuses are built from a move’s own text (+50% Attack, 3t) and carry that move’s name. A handful of riders are named effects the engine knows by name; this is what each one does to the unit carrying it.

StatusFamilyEffect while it lasts
Slagbite, FractureDefense downTakes more damage from every hit (the Defense delta above).
ScorchrotDamage over timeLoses 6% of max HP at the end of each of its turns.
Necrovenom, Mothervenom, GraveleechDamage over timeLoses 5% of max HP at the end of each of its turns.
DoomruneMarkTakes more damage from every source (the damage-taken delta above).
GravechillAttack down, slowerHits softer and acts later.
Cinch, SlowedSlowerActs later in the turn order.
BindStunSkips its action while bound.
Warded, Guarded, Hastened, Blessed, RalliedBuffsProtective or empowering effects granted by allies; Warded lowers damage taken by the delta above.

The family of each name is the STATUS_ITYPE map and the per-turn fractions the DOT map, both in src/battle.js, built into play/battle_module.js; they pick the chip icon and the tooltip family too.

Synergy clauses and the caster

A move's Synergy clause is a second buff clause gated on creature type. Two rules govern whether the caster is included:

Because the Synergy buff carries the same ability key as the move's own self buff, a caster who qualifies for both gets one status at the stronger of the two magnitudes. A weak type synergy can never overwrite the ability's own stronger self buff.

Worked example — Convocation of Storms

M-STORMFATHER-4, Targets=self, Effect_S3 Self +50% Attack, 3t, Synergy Elemental and Dragon allies gain +15% Attack, 3t. Party: Riptide (Elemental·Dragon, the caster), Ignis (Elemental·Dragon, already running its own Feed the Conflagration at +50%), and a Construct character.

UnitStatuses after the castAttack multiplier
Riptide (caster)One: Convocation of Storms, +50%, 3t. The +15% Synergy shares its key and merges at the stronger value.×1.50
IgnisTwo: Feed the Conflagration +50% and Convocation of Storms +15%. Different abilities, so both run.×1.65
Character (Construct)None — fails the type gate.×1.00

Targeting

The Targets column in moves.csv routes sides and is never inferred from Power. It governs selection as well as resolution:

Where this lives in the client

FunctionResponsibility
buildMoveStamps the ability key and display name onto every buff payload of the move.
stKeyStatus identity: the ability key when present, else the display name.
addStatusObjFinds a status with the same identity and merges, or appends a new one.
mergeStatusRefresh duration, keep the stronger magnitude, keep the surviving cast's name.
statusMultAdditive aggregation of Attack, Defense and Speed with the 0.30–2.50 clamp.
applySynSynergy clause: type gate including the caster, "another ally" test excluding it.
targetOk / pickTargetTarget legality, shared with the selection highlight.

Signature mechanics cannot be removed (reports #117 / #118)

Status: on release. D's ruling, 2026-08-12: “Boss buffs / unique mechanics cannot drop. They need to be permanent for the fight and impossible to remove.”

The engine already promised this and did not keep it. A boss's unique rule is carried as a status with the sig flag: it is painted with the skull icon, and its own tooltip already reads “This cannot be removed.” But the predicate that answers “is this a buff?” returns TRUE for a signature status — correctly, it is a buff — and four separate paths acted on that answer without ever asking whether the thing was allowed to be taken:

Why it was seen only a few times. Every one of those needs a player carrying a strip, steal or shorten effect. A bare account has none, which is why the report never reproduced and was twice recorded as “could not reproduce”.

The rule now. One predicate — is this buff strippable — is asked at every removal site. A signature status still READS as a buff everywhere else: it shows in the buff row, it satisfies “target has a buff” conditions, and it is described in tooltips exactly as before. It is simply never a legal target for removal, theft or shortening. Authored counterplay is untouched — Sethek's second rising is still cancelled by killing his escort first (#116), because that is the boss's own written rule rather than a status being taken off him.

Engine: isStrippableBuff() and applyFxTo() in src/battle_round.js, built into play/battle_module.js, whose resolveRound runs the fight; the SIGFX rules in play/app.js. The applyFxTo still defined in play/app.js has no caller since 3.7 and is not the live path. Gate: tools/probe_sigperm.py 15/15, and 8/15 against the build it replaces — the failing run reproduces all four losses on a real client before fixing any of them, and every check has a control proving ordinary buffs are still removable.

Thorns — what is reflected

Thorns comes from two places and both follow one rule: a status on the unit (a move rider such as Quillhide, carrying thornsPct) and the soul package (the character’s own thorns souls, plus team thorns on every ally). Each fires once per hit taken and deals its share straight to the attacker’s HP, on the damage line as “N reflected”. Retaliation ultimates are a different mechanic and are not thorns.

The share is of the damage the unit actually lost on the hit — after its own defence, and never more than it had left. (#771, D ruling A, 2026-09-07 — LIVE on release since 2026-09-08 (0.71).; this retracts the 2026-09-02 rule below.) The 09-02 rule read the hit as the attacker put it out, before the target’s defence, so that a tank reflected as much as a paper target; against an armoured target it did the opposite of its purpose: the floor-108 Talos (Divine, defence 9, its +50% Defense up) sent 15% of a 27k-attack creature’s raw output back — 28,790 — and killed a 3,862-HP attacker outright for landing a 1,722 hit. Under ruling A that same hit returns 258. Both sources, status and soul package; multi-hit summed; overkill counts only what the unit had. Gate tools/probe_thorns.py 5/5 through the real floor-108 boss party and the shared engine.

Retracted rule, kept for the record (LIVE on release 0.66 until 0.71): the share was of the hit as the attacker put it out — the engine computed the hit a second time against its reference defence (10 × STAT_SCALE) and thorns read that figure, so a tank reflected exactly what a paper target would off the same swing.

Before 2026-09-02 thorns read the damage actually taken — the reading ruling A returns to. A tank that mitigated a 2,000 hit to 100 reflected 4% of 100 — the souls were only worth carrying on a unit that took no damage, which is the one unit that never got to use them.

Changelog