osrlib.core.spells
Spell memorization, casting, spell resolution, and turning undead.
Part of the core kernel: everything here runs standalone — no game session required — and every random draw comes from a named, seeded RNG stream.
The OSE SRD's spell pages compile into a catalog of frozen
SpellTemplate models loaded by
load_spells. A template carries the page's presentation
data (duration, range, prose) alongside structured mechanics: one
SpellMode per castable usage, each naming its
targeting, saving throw, and — for the automated subset — a
SpellEffect the casting interpreter executes.
Modes the kernel doesn't automate ship manual=True with the SRD prose: casting one
is a supported operation (the slot is consumed, the event is emitted), and the game
or narrator resolves the fiction.
The daily flow: memorize_spells prepares a
caster's list (arcane casters choose from a spell book grown with
add_spell_to_book), then
validate_cast checks legality and
cast_spell consumes the memorized copy and
resolves the mode. cast_from_scroll
resolves an inscribed spell without a memorized copy, and
disrupt_casting loses one when a declared
cast is broken. Clerics also turn undead here:
validate_turn_undead, then
turn_undead.
Casters are Character objects; targets arrive
duck-typed per the combatant convention (see osrlib.core.combat)
as characters, MonsterInstance objects, or
location strings for effects a game attaches to places.
Reversed forms are entry data, not separate catalog entries: the nine concepts
printed as separate cleric and magic-user pages compile as two entries with
_c/_mu id suffixes because the pairs differ mechanically, while a reversible
spell's reverse lives on its entry as a
ReversedForm.
Every draw inside spell resolution — targeting dice, damage dice, touch-attack
rolls, cast-time forced saves, dispel survival rolls, and both turning rolls — comes
from the MAGIC_STREAM stream, so spell results
replay independently of combat draws and vice versa. Effect-internal draws (rolled
durations at attach, tick-time saves such as the charm re-save) stay on the
EFFECTS_STREAM stream.
EFFECT_KINDS
module-attribute
EFFECT_KINDS = frozenset(
{"damage", "heal", "cure", "condition", "modifiers", "kill", "restore_life", "dispel", "attach_only"}
)
The closed vocabulary of effect kinds the casting interpreter executes.
MAGIC_STREAM
module-attribute
Stream key convention for spell-resolution draws: targeting, damage, cast-time saves, turning.
CastContext
Bases: BaseModel
The caller-asserted situation a cast resolves under — the RAW referee surface.
in_combat gates the touch-attack roll ("In combat, a melee attack roll is
required"; outside combat the touch lands without a roll). bound/gagged are
the OSE SRD's freedom restraints, which the kernel has no model for.
rounds_since_death and days_since_death are the caller's attestations for
neutralize poison and raise dead (the session supplies them from its death
records) — the kernel has no cause-of-death model, so supplying
rounds_since_death is the attestation that the target died of poison; omit
it for any other death. strength_tiers maps entity ids to
"augmented"/"giant" for web's faster escape tiers (caller-asserted until
such effects exist).
CastResult
Bases: BaseModel
A cast's outcome: the consumed copy, what it affected, and the events.
manual=True means the kernel did the bookkeeping (the copy is spent, the cast
event is emitted) and the game narrates the effect — prose carries the mode's
SRD text for that. no_effect marks a resolved cast that affected nothing (an
ineligible target, every save passed on a negating spell): the copy is still
spent, never refunded — validation rejections are free, and refunding a resolved
cast would leak hidden state such as an unseen target's immunity.
CasterProfile
DurationSpec
Bases: BaseModel
A parsed spell duration.
kind="fixed" durations are amount (or dice, rolled at attach) counts of
unit, plus per_level extra units per caster level (the additive bonus:
light (MU) is 6 turns +1 per level; hold person (MU)'s 1 turn per level is
amount 0, per_level 1). concentration durations may carry a cap (Concentration
(up to 1 day)). Anything unparseable keeps kind="special" with the raw string
on the template — the parser never fails on prose.
MemorizationResult
Bases: BaseModel
The outcome of a preparation: rejections, or the memorized event.
MemorizedSpell
Bases: BaseModel
One memorized copy of a spell: the id and the form fixed at memorization.
spell_id is a spell id from load_spells — see
the spell id index. Arcane casters fix the normal or reversed
form when memorizing (the OSE SRD: "The normal or reversed form of a spell must
be selected when the spell is memorized"); divine casters always memorize the
normal form and choose at cast time, so their copies carry reversed=False.
RangeSpec
Bases: BaseModel
A parsed spell range.
feet carries the distance in feet for feet, yards (converted: 240 yards
around the caster is 720), and per_level kinds; per_level_feet is the extra
feet per caster level (cloudkill-style 60' +10' per level is feet 60,
per_level_feet 10). touch covers The caster or a creature touched —
self-targeting is allowed; presence forms and other prose are special.
ReversedForm
Bases: BaseModel
A reversible spell's reversed version — entry data, never a separate entry.
duration_spec overrides the normal form's duration when the page prints a
dual form (Instant / Permanent (curse) splits across the two); None means
the reverse shares the normal form's duration.
SaveSpec
Bases: BaseModel
One mode's saving throw: the category, a modifier, and what a pass means.
modifier is the target's adjustment (hold person's single-target −2,
feeblemind's −4). on_save="negates" means a passed save avoids the effect;
"half" halves damage, rounding down.
SpellBookResult
Bases: BaseModel
The outcome of a spell-book addition: rejections, or the book event.
SpellCatalog
Bases: BaseModel
The loaded spell list, with id lookup and per-list filtering.
get
get(spell_id: str) -> SpellTemplate
Return the spell template for spell_id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spell_id
|
str
|
The spell id, e.g. |
required |
Returns:
| Type | Description |
|---|---|
SpellTemplate
|
The spell template. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no spell has that id. |
by_list
by_list(spell_list: str, level: int | None = None) -> tuple[SpellTemplate, ...]
Return the spells on a class's list, optionally at one spell level.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spell_list
|
str
|
The list id, e.g. |
required |
level
|
int | None
|
A spell level to filter by, or |
None
|
Returns:
| Type | Description |
|---|---|
tuple[SpellTemplate, ...]
|
The matching templates, in catalog (id) order. |
SpellEffect
Bases: BaseModel
The structured effect a mode's resolution executes — the closed vocabulary.
kind names the interpreter behavior; params carries the per-spell
scalars (damage dice, per-level scaling, exclusions, revival windows).
condition is the condition a condition effect attaches; cures_conditions
and cures_effect_kinds name what a cure effect releases; modifiers is the
stat-modifier bundle a modifiers effect grants through the effects engine.
SpellMode
Bases: BaseModel
One castable usage of a spell.
Multi-usage pages (cure light wounds, light) carry one mode per numbered
usage. key is stable snake_case within the spell's form — casting names the
mode by it. manual=True marks modes the kernel doesn't execute: casting one
consumes the memorized copy and emits the cast event with the manual marker plus
the prose, and the game or narrator resolves the fiction. Manual modes may omit
targeting (the page carries no structured targeting); automated modes always
carry targeting and an effect.
SpellTemplate
Bases: BaseModel
A spell, compiled from its SRD page.
Frozen SRD data: play never mutates a spell template. id is the slugified
primary name (fire_ball, cure_light_wounds), with _c/_mu suffixes for
the nine dual-page concepts. spell_list is an open, validated list id matched
against CasterProfile.spell_list — the
Classic catalog carries cleric and magic_user, and Advanced lists are
additive data. duration and range keep the printed strings; the specs are
the parsed forms. conjured_monsters embeds stat blocks printed on the page
(sticks to snakes' snake, validated as a full monster template);
conjured_monster_ids references existing monsters.json entries (conjure
elemental's four 16-HD elementals).
conjured_monsters
class-attribute
instance-attribute
conjured_monsters: tuple[MonsterTemplate, ...] = ()
mode
Return the mode with key on the normal or reversed form.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The mode key, e.g. |
required |
reversed
|
bool
|
True to look on the reversed form. |
False
|
Returns:
| Type | Description |
|---|---|
SpellMode
|
The mode. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the form or the key doesn't exist. |
TargetingSpec
Bases: BaseModel
One mode's targeting: the shared combat targeting mode plus its parameters.
count/count_dice size up_to_n modes (hold person's group mode is 1d4);
hd_budget_dice sizes hd_budget modes (sleep's 2d8); hd_cap bounds
eligibility by Hit Dice (sleep mode 2's "4 HD or less", charm monster's
"3 HD or less"); hd_min bounds it from below (charm monster's single mode
takes "more than 3 HD"). Area shape and dimensions ship as structured data
now; the battle machine maps its range-track geometry onto candidates.
TurnUndeadResult
add_spell_to_book
add_spell_to_book(caster: Any, definition: ClassDefinition, catalog: SpellCatalog, spell_id: str) -> SpellBookResult
Add a spell to an arcane caster's book — the referee-level growth surface.
Covers mentoring and leveling; the fiction around it (the mentor's week, a lost book's rewriting costs) belongs to the game. The book holds, per spell level, at most the caster's current slot count at that level (the RAW "contains exactly the number of spells that the character is capable of memorizing", read per level). The book never auto-shrinks — it is a physical object; a drained character may hold a book over capacity and simply cannot add more until capacity catches up.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
caster
|
Any
|
The learning caster: a |
required |
definition
|
ClassDefinition
|
The caster's |
required |
catalog
|
SpellCatalog
|
The loaded spell catalog, from |
required |
spell_id
|
str
|
The spell id to add — see the spell id index. |
required |
Returns:
| Type | Description |
|---|---|
SpellBookResult
|
The rejections (nothing mutated) or the book-updated event. |
cast_from_scroll
cast_from_scroll(
reader: Any,
spell: SpellTemplate,
mode: str,
*,
reversed: bool = False,
targets: Sequence[object] = (),
context: CastContext | None = None,
ledger: EffectsLedger,
clock: GameClock,
allocator: Any,
registry: dict[str, Any],
ruleset: Ruleset,
stream: RngStream,
effects_stream: RngStream
) -> CastResult
Cast an inscribed spell from a scroll: the scroll is the copy, and it burns.
Reuses validate_cast's legality gates and
the full mode-resolution and effects machinery, but skips the memorized-copy
consume — "when a scroll is read, the words disappear", so the caller marks
the inscribed spell spent. The spell resolves at the minimum class level able
to cast it (the least-power reading — see
minimum_caster_level). Class-list
gating (arcane readers for arcane scrolls, the thief's scroll-use ability) and
the light requirement are the crawl's validation; the kernel resolves a legal
read.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reader
|
Any
|
The reading character: a
|
required |
spell
|
SpellTemplate
|
The inscribed |
required |
mode
|
str
|
The mode key on the chosen form (see
|
required |
reversed
|
bool
|
True to cast the reversed form (divine readers choose at cast). |
False
|
targets
|
Sequence[object]
|
The explicit target list, in the caller's order, per the combatant
convention (see |
()
|
context
|
CastContext | None
|
The caller-asserted |
None
|
ledger
|
EffectsLedger
|
The |
required |
clock
|
GameClock
|
The |
required |
allocator
|
Any
|
The id allocator for attached effects: an
|
required |
registry
|
dict[str, Any]
|
Live combatants by entity id —
|
required |
ruleset
|
Ruleset
|
The |
required |
stream
|
RngStream
|
The magic stream (the |
required |
effects_stream
|
RngStream
|
The effects stream, for attach-time dice. |
required |
Returns:
| Type | Description |
|---|---|
CastResult
|
The cast outcome with its events. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the cast is invalid — validate with
|
cast_spell
cast_spell(
caster: Any,
spell: SpellTemplate,
mode: str,
*,
profile: CasterProfile,
reversed: bool = False,
targets: Sequence[object] = (),
context: CastContext | None = None,
ledger: EffectsLedger,
clock: GameClock,
allocator: Any,
registry: dict[str, Any],
ruleset: Ruleset,
stream: RngStream,
effects_stream: RngStream
) -> CastResult
Cast a memorized spell: consume the copy, resolve the mode, return the events.
Consumes the first matching memorized copy (lowest tuple index) whether or not the resolution ends up affecting anything — including a touch attack that misses (nothing in RAW holds the charge). Divine casters consume any copy of the spell and choose the form at cast time; arcane casters fixed the form at memorization. Casting anything releases the caster's own invisibility (RAW: attacking or casting breaks it). Manual modes emit the cast event with the manual marker and the kernel stops there — the game or narrator resolves the fiction.
Spell-resolution draws (targeting dice, damage dice, touch-attack rolls,
cast-time forced saves, dispel survival rolls) come from stream — the
MAGIC_STREAM convention; attach-time draws
(rolled durations, web escape dice) come from effects_stream per the
effects-engine convention, so the two subsystems replay independently.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
caster
|
Any
|
The casting caster: a |
required |
spell
|
SpellTemplate
|
The |
required |
mode
|
str
|
The mode key on the chosen form (see
|
required |
profile
|
CasterProfile
|
The caster's |
required |
reversed
|
bool
|
True to cast the reversed form. |
False
|
targets
|
Sequence[object]
|
The explicit target list, in the caller's order, per the combatant
convention (see |
()
|
context
|
CastContext | None
|
The caller-asserted |
None
|
ledger
|
EffectsLedger
|
The |
required |
clock
|
GameClock
|
The |
required |
allocator
|
Any
|
The id allocator for attached effects: an
|
required |
registry
|
dict[str, Any]
|
Live combatants by entity id —
|
required |
ruleset
|
Ruleset
|
The |
required |
stream
|
RngStream
|
The magic stream (the |
required |
effects_stream
|
RngStream
|
The effects stream, for attach-time dice. |
required |
Returns:
| Type | Description |
|---|---|
CastResult
|
The cast outcome with its events. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the cast is invalid — validate with
|
Examples:
from osrlib.core.alignment import Alignment
from osrlib.core.character import CHARACTER_CREATION_STREAM, create_character
from osrlib.core.clock import GameClock
from osrlib.core.effects import EFFECTS_STREAM, EffectsLedger
from osrlib.core.monsters import MONSTER_SPAWN_STREAM, IdAllocator, spawn_monster
from osrlib.core.rng import RngStreams
from osrlib.core.ruleset import Ruleset
from osrlib.core.spells import MAGIC_STREAM, MemorizedSpell, cast_spell, caster_profile, memorize_spells
from osrlib.data import load_classes, load_monsters, load_spells
rules = Ruleset()
streams = RngStreams(master_seed=11)
catalog = load_spells()
definition = load_classes().get("magic_user")
# A 1st-level magic-user with *magic missile* in her book, memorized for the day.
created = create_character(
name="Zelia",
class_id="magic_user",
alignment=Alignment.NEUTRAL,
ruleset=rules,
stream=streams.get(CHARACTER_CREATION_STREAM),
starting_spell_ids=["magic_missile"],
)
zelia = created.character
prepared = memorize_spells(zelia, definition, catalog, [MemorizedSpell(spell_id="magic_missile")])
assert prepared.accepted
# One goblin target; the registry maps entity ids to the live objects.
template = load_monsters().get("goblin")
goblin = spawn_monster(template, id="monster-0001", stream=streams.get(MONSTER_SPAWN_STREAM))
outcome = cast_spell(
zelia,
catalog.get("magic_missile"),
"missiles",
profile=caster_profile(definition),
targets=[goblin],
ledger=EffectsLedger(),
clock=GameClock(),
allocator=IdAllocator(),
registry={"monster-0001": goblin},
ruleset=rules,
stream=streams.get(MAGIC_STREAM),
effects_stream=streams.get(EFFECTS_STREAM),
)
assert outcome.spell_id == "magic_missile" and not outcome.no_effect
assert outcome.affected_ids == ("monster-0001",)
assert zelia.memorized_spells == () # the cast consumed the memorized copy
assert goblin.max_hp - goblin.current_hp == 3 # 1d6+1 missile damage, stable under this seed
caster_profile
caster_profile(definition: ClassDefinition) -> CasterProfile | None
Return a class definition's casting profile, or None for non-casters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
definition
|
ClassDefinition
|
The |
required |
Returns:
| Type | Description |
|---|---|
CasterProfile | None
|
The profile, from the |
disrupt_casting
Disrupt a declared casting: the copy is lost "as if it had been cast".
The trigger — lost initiative, then successfully attacked or failed a save before acting — is detected by the battle machine; the kernel resolves a disruption when told it happened. The exactly-matching copy is removed first; failing that, any copy of the spell (a divine caster's reversed declaration consumes a normal copy, mirroring the cast-time rule).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
caster
|
Any
|
The disrupted caster: a
|
required |
spell_id
|
str
|
The declared spell's id — see the spell id index. |
required |
reversed
|
bool
|
Whether the declared cast was the reversed form. |
False
|
Returns:
| Type | Description |
|---|---|
list[Event]
|
The disruption event. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no copy of the spell is memorized (programmer misuse). |
forget_excess_memorized
forget_excess_memorized(caster: Any, definition: ClassDefinition, catalog: SpellCatalog) -> list[Event]
Forget memorized copies in excess of the caster's (shrunk) slots.
The drain interplay: after a level drop, for each spell level where the memorized count exceeds the new slot count, excess copies are forgotten newest-first (highest tuple index). RAW is silent on which copies go; osrlib adopts newest-first because it keeps the rule deterministic without new state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
caster
|
Any
|
The drained caster: a |
required |
definition
|
ClassDefinition
|
The caster's |
required |
catalog
|
SpellCatalog
|
The loaded spell catalog, from |
required |
Returns:
| Type | Description |
|---|---|
list[Event]
|
One forgotten event per dropped copy, newest first. |
memorize_spells
memorize_spells(
caster: Any, definition: ClassDefinition, catalog: SpellCatalog, selections: Sequence[MemorizedSpell]
) -> MemorizationResult
Prepare a caster's daily spells — a full replacement of the memorized list.
Models the daily preparation: the new list wholly replaces the old (partial top-ups are not a B/X operation). Divine casters choose freely from their class list and never fix the reversed form (they reverse at cast time, "by speaking the words and performing the gestures backwards"); arcane casters choose from their spell book and fix normal or reversed per copy at memorization. Duplicate selections are legal per RAW ("may opt to memorize the same spell twice"). The once-a-day/after-sleep/one-hour gates are exploration procedure owned by the crawl layer — standalone callers may call this freely, by design.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
caster
|
Any
|
The preparing caster: a |
required |
definition
|
ClassDefinition
|
The caster's |
required |
catalog
|
SpellCatalog
|
The loaded spell catalog, from |
required |
selections
|
Sequence[MemorizedSpell]
|
The prepared |
required |
Returns:
| Type | Description |
|---|---|
MemorizationResult
|
The rejections (nothing mutated) or the memorized event. |
minimum_caster_level
minimum_caster_level(spell: SpellTemplate) -> int
Return the minimum class level able to cast a spell, per the compiled progressions.
RAW is silent on a scroll's caster level; osrlib adopts the least-power reading (a documented adaptation): the lowest level at which any class on the spell's list has a slot of the spell's level.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spell
|
SpellTemplate
|
The |
required |
Returns:
| Type | Description |
|---|---|
int
|
The minimum caster level. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no class on the spell's list ever gains a slot of that level. |
pop_mirror_image
pop_mirror_image(ledger: EffectsLedger, target_ref: str, *, registry: dict[str, Any], clock: GameClock) -> list[Event]
Destroy one mirror image — called by the game or battle machine per incoming attack.
"Attacks on the caster destroy one of the mirror images (even if the attack misses)." When the last image pops, the effect is released.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ledger
|
EffectsLedger
|
The |
required |
target_ref
|
str
|
The mirrored caster's entity id. |
required |
registry
|
dict[str, Any]
|
Live combatants by entity id —
|
required |
clock
|
GameClock
|
The |
required |
Returns:
| Type | Description |
|---|---|
list[Event]
|
The pop (and final release) events; empty when no mirror-image effect is |
list[Event]
|
active. |
turn_undead
turn_undead(
cleric: Any,
definition: ClassDefinition,
candidates: Sequence[Any],
*,
ledger: EffectsLedger,
clock: GameClock,
allocator: Any,
registry: dict[str, Any],
stream: RngStream
) -> TurnUndeadResult
Turn undead — the full procedure, one call.
One 2d6 turn roll (the magic stream) is compared per candidate type against
that type's turning-table cell — — types are unaffected, number types succeed
when the roll meets the threshold, T/D succeed automatically. If any type
succeeded, one 2d6 HD-pool roll follows; eligible monsters are affected
lowest-HD-first (stable input order on ties), each costing its HD count
(minimum 1, fixed bonuses dropped — the sleep convention); the pool stops at
the first unaffordable monster (RAW: excess Hit Dice "are wasted", not
reallocated); at least one undead is always affected on a successful turn even
when the pool rolls short (RAW minimum effect), resolved as the cheapest
eligible monster. Affected monsters whose column says D die permanently
("instantly and permanently annihilated"); the rest gain the turned condition
via an indefinite, non-dispellable effect the encounter releases (flee behavior
is the battle machine's).
Only monsters bearing the undead category are candidates; non-undead in the
list resolve as unaffected rather than rejecting, so a turning attempt never
doubles as a free undead detector.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cleric
|
Any
|
The turning character: a |
required |
definition
|
ClassDefinition
|
The character's |
required |
candidates
|
Sequence[Any]
|
The encounter's monsters, in stable order:
|
required |
ledger
|
EffectsLedger
|
The |
required |
clock
|
GameClock
|
The |
required |
allocator
|
Any
|
The id allocator for the attached effect: an
|
required |
registry
|
dict[str, Any]
|
Live combatants by entity id —
|
required |
stream
|
RngStream
|
The magic stream — the player rolls turning dice in B/X, so both rolls are player-visible on the event. |
required |
Returns:
| Type | Description |
|---|---|
TurnUndeadResult
|
The turning outcome with its events. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the character cannot turn — validate with
|
validate_cast
validate_cast(
caster: Any,
spell: SpellTemplate,
mode: str,
*,
profile: CasterProfile | None,
reversed: bool = False,
targets: Sequence[object] = (),
context: CastContext | None = None,
ledger: EffectsLedger | None = None
) -> list[Rejection]
Validate a cast — the pure pre-phase: no RNG draws, no mutation.
Checks caster capacity (dead, petrified, paralysed, asleep, silenced,
feebleminded, or weakened cannot cast; bound/gagged arrive as context flags;
an active anti-magic shell blocks the caster's own casting when a ledger is
supplied), a matching memorized copy, form and mode legality, target counts
(single takes one; magic missile takes exactly one target per missile), and
range — only when the context supplies a distance, mirroring the combat
convention. Category and immunity gates are resolution outcomes, never
validator rejections, by design: casting charm person at a disguised
doppelgänger must not be a zero-cost detector. A cleric's holy symbol is not
checked — the OSE SRD states carrying one as a class edict, not a mechanical
gate on any procedure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
caster
|
Any
|
The casting caster: a |
required |
spell
|
SpellTemplate
|
The |
required |
mode
|
str
|
The mode key on the chosen form (see
|
required |
profile
|
CasterProfile | None
|
The caster's |
required |
reversed
|
bool
|
True to cast the reversed form. |
False
|
targets
|
Sequence[object]
|
The explicit target list, per the combatant convention (see
|
()
|
context
|
CastContext | None
|
The caller-asserted |
None
|
ledger
|
EffectsLedger | None
|
The |
None
|
Returns:
| Type | Description |
|---|---|
list[Rejection]
|
Structured rejections; empty when the cast may resolve. |
validate_turn_undead
validate_turn_undead(cleric: Any, definition: ClassDefinition) -> list[Rejection]
Validate a turning attempt — the pure pre-phase.
Turning is gated by the turn_undead class-ability tag; an incapacitated cleric
(dead, petrified, paralysed, asleep) cannot present the symbol, and a weakened
one cannot turn — the raise-dead weakness bans class abilities ("cannot attack,
cast spells, or use other class abilities"). The holy symbol itself is not a
precondition: the OSE SRD states carrying one as a class edict, not a mechanical
gate; games wanting the stricter reading check inventory themselves.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cleric
|
Any
|
The turning character: a |
required |
definition
|
ClassDefinition
|
The character's |
required |
Returns:
| Type | Description |
|---|---|
list[Rejection]
|
Structured rejections; empty when the attempt may be rolled. |