global dnd5e conditions - Drowbe/coffee-pub-blacksmith GitHub Wiki
Working with dnd5e Conditions and Active Effects
Audience: anyone in the suite applying or removing conditions and Active Effects directly.
Practical, hard-won knowledge about Foundry v13 + dnd5e 5.x conditions. Verified core-only โ no DFreds, no third-party condition modules.
Most consumers should not need this: effects.getDisplayEffects(actor) already handles filtering, labels, durations, and description enrichment. Read this when you are applying or removing effects yourself, or when something in the substrate is behaving unexpectedly.
Originally field notes from Bibliosoph's injury and Check-Up build.
1. Applying and removing conditions
For any official condition, the only correct call is the core toggle:
await actor.toggleStatusEffect('blinded', { active: true }); // apply
await actor.toggleStatusEffect('blinded', { active: false }); // remove
This creates or deletes the system's own condition ActiveEffect with the right icon, name, and rules wiring. Do not hand-build an ActiveEffect for an official condition โ you get a lookalike that other code, including dnd5e itself, does not recognise as the condition.
- Guard before toggling on:
actor.statuses.has('blinded'). Toggling an already-active condition off when you meant on is a classic. - Do not take a DFreds dependency. Its API signatures did not match its documentation and it was missing dnd5e 5.x conditions entirely. Core-only has been reliable.
- Exhaustion is leveled.
toggleStatusEffect('exhaustion')gives you level 1; the real level lives atactor.system.attributes.exhaustion. If you surface exhaustion, surface the level. - All of this requires ownership. Gate UI on
actor.isOwner.
2. Never hardcode the condition list
The set of legal conditions changed in dnd5e 5.x and will change again. Enumerate at runtime from two registries:
CONFIG.statusEffects // toggleable โ what the token HUD shows; `name` is a LOCALIZATION KEY
CONFIG.DND5E.conditionTypes // full registry, including pseudo-conditions
Always game.i18n.localize() before displaying or comparing, and compare lowercased.
Pseudo-conditions are the trap
bleeding, burning, diseased and friends are flagged pseudo: true. They cannot be toggled and never get their own effect. A pseudo-condition exists only by riding on another effect's statuses array:
await actor.createEmbeddedDocuments('ActiveEffect', [{
name: 'Gnash Wound', img: '...', statuses: ['bleeding']
}]);
It lives and dies with the carrying effect. An add-a-condition picker that offers pseudo-conditions as toggleables will silently fail โ either exclude pseudo entries from "add", or create a carrier effect.
3. Descriptions need the enricher
dnd5e condition effects do not store description text. They store enricher syntax pointing at the rules journal:
@Embed[Compendium.dnd5e.content24.JournalEntry.phbAppendixCRule.JournalEntryPage.QxCrRcgMdUd3gfzz inline]
Render effect.description raw and your users see exactly that string. Run it through the enricher:
const TextEditorImpl = foundry.applications?.ux?.TextEditor?.implementation ?? TextEditor;
const html = await TextEditorImpl.enrichHTML(effect.description ?? '', {
relativeTo: effect,
rollData: actor.getRollData?.() ?? {}
});
- It is async โ build row data with
Promise.all; do not try to enrich in a synchronous render path. - Wrap in try/catch and fall back to raw text. A broken embed should not kill the whole list.
CONFIG.DND5E.conditionTypes[id].referenceis a UUID straight to the rules page if you would rather link than inline.- Watch heights: the Exhaustion page embeds a full table. Cap or scroll your container.
getDisplayEffects does all of this for you, permission-aware.
4. Detecting what is actually afflicting an actor
actor.statuses is not enough. The battle-tested filter, in order:
const isAffliction = (e) =>
!e.disabled && !e.isSuppressed && (
!!e.getFlag(moduleId, flagKey) // module-stamped
|| e.isTemporary // has a duration
|| e.statuses?.size > 0 // carries condition ids
|| conditionNames.has(e.name.toLowerCase()) // NAME matches a localized condition
);
That last clause matters: GMs hand-author effects named "Frightened" with no duration and no statuses. Every other test misses them. Build conditionNames from both registries in ยง2, localized and lowercased.
The disabled / isSuppressed exclusions keep toggled-off and unequipped-item effects out.
This is exactly what EffectsAPI.getActiveEffects() implements โ prefer it over rolling your own.
5. Removal and the unwind problem
await effect.delete() removes any effect regardless of who created it, ownership permitting. Deleting dnd5e's own condition effect is fine โ it untoggles cleanly.
The real gotcha: if the effect conveyed a toggled condition, deleting it may leave the condition behind. After deleting, check whether any remaining effect still conveys that condition, and only then toggle it off. Skipping the still-conveyed check makes removing one of two bleed-inducing wounds cure the bleeding.
Bibliosoph implements this as a global deleteActiveEffect hook rather than a callback, so the unwind runs no matter which UI performed the delete. That is the recommended shape โ see "Prefer hooks over registry callbacks" in architecture-ownership.
Duration display: effect.duration.label gives a localized remaining-time string ("10 Rounds", "1 Minute"). Show nothing when the effect is permanent: !effect.isTemporary. On Foundry 14 the duration unit is duration.units; reading duration.type there logs a compatibility warning.
6. A conveyed condition legitimately appears twice
When an injury toggles Prone, the Prone condition exists as its own effect too. So it correctly appears both inside the injury's row and as an independent row. That is not a dedup bug โ the patient can stand up while the injury persists.
To label such rows, match the loose condition's statuses against the flagged effects' conveyed conditions and render "via <source name>". getDisplayEffects already does this and returns it in context.
7. Miscellaneous, learned the hard way
- Hooks fire on every client.
createActiveEffect/deleteActiveEffectreach all clients, so canvas animations can be driven entirely off effect flags with zero socket traffic. Anything that writes from such a hook must guard with an active-GM check. effect.statusesis a Set;actor.statusesis the aggregate of every condition id currently conveyed by anything. Good for duplicate guards, useless for "which effect conveys it."- Treat duplicate applies as success with a notification, not an error. GMs double-click.
- Do not model HP damage as an ActiveEffect change. HP changes applied that way suppress rather than damage, and un-suppress on removal. Apply one-time damage immediately via
actor.update. Routing it through the damage pipeline instead risks a large hit re-triggering damage-threshold automation โ an infinite loop.