Effects
Ongoing conditions and buffs are modeled as ActiveEffect documents — a
Foundry-aligned, Pydantic-typed shape. A spell like bless, a poisoned
condition, or a magic-item bonus all become ActiveEffects attached to a
combatant for the duration of combat.
The model
An ActiveEffect carries:
id— the template id analog (e.g."effect:bless").origin— what produced it (e.g."cast:bless:1","item:sword+1:abc12").target_id— the combatant it is attached to.duration— anActiveEffectDuration(see Durations below).changes— a list ofActiveEffectChangeentries (the mechanical modifications it applies).statuses— the set of condition slugs it imposes.flags— a free-form dict (Phase 6 uses it for concentration and applicable-action-type metadata).
Durations
ActiveEffectDuration carries three counters, all of which tick at a turn
boundary (turn_lifecycle's turn_end hooks). SRD 5.2 §Duration puts a round
at about 6 seconds, which is the whole conversion:
| Field | Ticks at | Notes |
|---|---|---|
rounds |
the caster's turn end (parsed from origin; item/environment origins fall back to the target's) |
reaching zero emits EffectExpired(reason="duration") |
turns |
the target's own turn end | independent of rounds — whichever counter hits zero first expires the effect |
seconds |
the caster's turn end, as ceil(seconds / 6) rounds |
materialised once into rounds; seconds is never mutated. If both are set, rounds wins |
flags["until_end_of_next_turn_of"] = "<entity_id>" expresses SRD's "until the
end of your next turn": the effect expires at that actor's next turn end, with
a one-turn grace if it was applied during that actor's own turn.
Concentration-flagged effects are exempt from all of the above — the concentration cascade and the per-turn repeat save own their lifetime, and the imported packs carry display-only counters on them.
Lifecycle and scope
Effects are combat-scoped and engine-owned. The engine holds active
effects in memory during the encounter; concentration and repeat-save linkage
key on (target_id, id, origin) identity. You can pass starting effects into
start_combat(..., active_effects=...) and read a combatant's current effects
during combat with get_actor_active_effects(handle, entity_id).
When combat ends, end_combat returns the final tuple on
EndCombatResult.final_active_effects — but the engine itself discards them:
effects do not persist across combats. Cross-combat persistence is a host
concern, not the engine's. This is a deliberate boundary: the engine is the
in-combat effect authority and nothing more.