osrlib.core.combat
The combat kernel: initiative, attacks, damage, saving throws, morale, and targeting.
Part of the core kernel and usable à la carte: every function is a pure resolution over
explicitly passed state — no session, dungeon, or battle machine required. A round of
B/X combat flows through the module in order:
roll_initiative orders the actors,
resolve_attack runs one attack end to end (the
roll, the immunity gate, the damage),
saving_throw resolves forced saves, and
check_morale decides whether a side keeps
fighting. Start with resolve_attack — most of the module is the pieces it composes,
plus the specialized resolutions (breath weapons, gazes, splash weapons, energy drain)
and the shared targeting model (select_targets).
Combatant-typed parameters (attacker, defender, target, and kin) follow one
convention across the whole library: they accept a
Character or a
MonsterInstance. Both expose THAC0, attack
bonus, armour class, saving throws, hit points, and conditions, and the kernel reads
only that shared surface. NPC adventurers are Character instances, so there is no
third combatant type.
Resolutions also take an AttackContext carrying
the caller-asserted situation (distance, cover-like situational modifiers, back-stab
position), the Ruleset in play, and an RNG stream —
conventionally the COMBAT_STREAM stream from the
session's RngStreams. They return frozen result models
carrying an events tuple: a session appends result.events to its log, while à la
carte callers read the plain result fields.
The damage pipeline always runs in this order: (1) the immunity gate — if the
defender's harmed_only_by/energy defenses exclude the source, no damage is rolled and
the event says so; (2) the damage roll plus STR for melee, then quality/context
doublings (brace, charge, back-stab), minimum 1 on a hit; (3) reductions (the wraith's
half-from-silver, the mummy's half-everything), floored but never below 1; (4) apply:
hit points floor at 0, fire and acid route into a regenerating monster's
non-regenerable ledger, and death emits at 0.
Validators (validate_attack,
validate_breath) follow the house convention:
pure pre-phase functions returning Rejection
lists — no RNG draws, no mutation. Rejections are free (no roll, no time, no log
entry), which is why holy water against the living is not a rejection: it resolves
normally and the damage pipeline reports no effect — a free rejection would be a
zero-cost undead detector.
Attack
module-attribute
Attack = WeaponTemplate | CombatFacet | GearTemplate | MonsterAttack | MagicItemInstance | None
What a combatant attacks with; None is an unarmed attack (1d2).
A MagicItemInstance attack is an enchanted arm: its base weapon supplies the
dice, qualities, and ranges, and its template supplies the attack and damage
bonuses (versus-clauses swapping in their alternate bonus when the defender's
template carries the referenced tag or id) — and it counts as magical for the
graded-immunity checks, cursed forms included: a cursed sword is still a magic
sword.
COMBAT_STREAM
module-attribute
The stream key for battle-resolution draws: attacks, damage, saving throws, morale.
Pass it to RngStreams.get to obtain the stream the
combat functions conventionally draw from.
AttackContext
Bases: BaseModel
The caller-asserted situation an attack resolves under.
Everything here is the RAW referee surface: the kernel checks the rules given
honest context, and supplying the context (was the charge 60 feet? is the target
unaware?) is the caller's or the crawl layer's job. situational_modifier is the RAW
referee adjustment (cover −1 to −4, the dozing dragon's +2, and kin).
defender_ally_ac_bonus is an ally-granted AC bonus the caller asserts — the
Ring of Protection 5' Radius shielding the wearer's rank-mates; adjacency is
the caller's spatial judgment, so the value arrives as context.
AttackResult
AttackRollResult
DamageSource
Bases: BaseModel
What a damage packet presents to the defender's defenses.
keys are material/enchantment keys (silver, magic, holy); element is the
energy element, if any; kind names the delivery (weapon, unarmed, splash,
breath, falling, effect, spell); destructive marks sources that destroy
a victim's equipment on death (breath weapons, lightning bolt). missile marks
small-missile deliveries for protection from normal missiles — osrlib draws the
boundary from the OSE SRD's own examples: character weapon missiles and thrown
splash items are small missiles; monster attacks are never auto-marked (the hurled
boulder is the SRD's counter-example), with AttackContext.monster_missile as the
caller's opt-in when the fiction says small missile (a hobgoblin's arrow).
InitiativeResult
Bases: BaseModel
An initiative resolution: per-key rolls (re-rolls included) and the acting order.
MoraleResult
Bases: BaseModel
A morale check's outcome; exempt marks ML 2 and ML 12 (no roll made).
MoraleTracker
Bases: BaseModel
The two-passed-checks memory: after two held checks, no further checks.
"If a monster passes two morale checks in an encounter, it will fight until killed, with no further checks."
check
check(subject: str, score: int, *, modifier: int = 0, stream: RngStream) -> MoraleResult | None
Check morale unless the subject has already passed twice.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subject
|
str
|
The side or group key. |
required |
score
|
int
|
The morale score. |
required |
modifier
|
int
|
The situational adjustment, clamped to ±2. |
0
|
stream
|
RngStream
|
The combat stream. |
required |
Returns:
| Type | Description |
|---|---|
MoraleResult | None
|
The result, or |
Participant
Bases: BaseModel
One initiative participant: a stable key, a side, and the modifier hooks.
modifier is the individual-initiative modifier (DEX for characters plus the
halfling's class tag, the caller-supplied modifier for monsters — compute it with
participant_modifier). slow marks
slow-weapon actors, who always act after all non-slow actors.
ReactionRollResult
Bases: BaseModel
A reaction roll's outcome: the raw 2d6, the modifier, and the table band.
SaveCategory
SaveResult
TargetingMode
Bases: StrEnum
The shared targeting model's modes.
Spells, breath weapons, and thrown weapons resolve through these. The kernel resolves against explicitly supplied candidate lists; the battle machine maps its range-track geometry onto them.
alignments_differ
Return whether two combatants' operative alignments differ, for warding gates.
osrlib adopts the reading that a combatant whose alignment is unresolved (None —
a multi-option monster spawned without a choice) counts as being of another
alignment: the ward errs protective.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Any
|
The creature the ward is checked against (the attacker) — a
|
required |
target
|
Any
|
The warded creature — a |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when the alignments differ or either is unresolved. |
apply_healing
Apply instantaneous healing, capped at max HP.
Mummy rot blocks magical healing: a diseased target emits the blocked
event and heals nothing from a magical source. osrlib treats instantaneous
healing as magical healing, so magical is the default — a cure spell that
forgets to name its source still respects the rot rule. The raise-dead weakness
blocks healing from every source: RAW says the subject "has 1 hit
point" until the recovery completes and the period "may not be shortened by any
magical healing" — the hit point returns when the weakness effect ends. The dead
cannot be healed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
Any
|
The creature to heal — a
|
required |
amount
|
int
|
The healing amount. Non-negative. |
required |
source
|
str
|
The healing kind: |
'magical'
|
Returns:
| Type | Description |
|---|---|
list[Event]
|
The healing and state events. |
attack_facet
attack_facet(attack: Attack) -> WeaponTemplate | CombatFacet | None
Return the combat stats behind any attack — the battle machine's lookup.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attack
|
Attack
|
The weapon, facet, gear item, magic instance, or |
required |
Returns:
| Type | Description |
|---|---|
WeaponTemplate | CombatFacet | None
|
The facet carrying dice, qualities, and ranges, or |
WeaponTemplate | CombatFacet | None
|
monster attacks. |
attack_roll
attack_roll(
attacker: Any, defender: Any, attack: Attack, *, context: AttackContext, ruleset: Ruleset, stream: RngStream
) -> AttackRollResult
Roll an attack: 1d20 plus modifiers against the defender's armour class.
Helpless defenders (paralysed, asleep) are hit automatically in melee — no roll
is consumed, damage only; a No hit roll required defender is likewise hit
without a roll. Natural 20 always hits and natural 1 always misses. Resolution is
the attack-matrix lookup, or unclamped THAC0 − AC under the thac0_arithmetic
flag.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attacker
|
Any
|
The attacking combatant — a
|
required |
defender
|
Any
|
The defending combatant — a |
required |
attack
|
Attack
|
The weapon, facet, gear item, or monster attack ( |
required |
context
|
AttackContext
|
The caller-asserted situation. |
required |
ruleset
|
Ruleset
|
The ruleset in play. |
required |
stream
|
RngStream
|
The combat stream. |
required |
Returns:
| Type | Description |
|---|---|
AttackRollResult
|
The roll outcome, with its events. |
burning_oil_pool_definition
burning_oil_pool_definition() -> EffectDefinition
Build the burning oil pool: a location-attached fire that burns for one turn.
Unlit oil may be compiled into a 3-foot pool; once lit it burns 1 turn and deals
1d8 to creatures passing through — who passes through is the caller's assertion
(the caller applies the damage with
deal_damage).
Returns:
| Type | Description |
|---|---|
EffectDefinition
|
The one-turn pool effect definition. |
cannot_move
Return whether a combatant cannot move — the web entanglement hook.
Consumed by the movement rules: an entangled creature "can't move" per the web spell's rules, and the incapacitated states cannot move either.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
combatant
|
Any
|
The combatant — a |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when movement is impossible. |
check_immunity
check_immunity(defender: Any, source: DamageSource, *, ruleset: Ruleset, attacker: Any | None = None) -> bool
Return True when the defender's defenses absorb the source: no damage is rolled.
The rules resolved here: the holy key is admitted through any
harmed_only_by gate on undead targets and has no effect on anything else;
uses_fire monsters ignore burning oil; the hd5_counts_as_magical flag lets a
5+ HD monster attacker (or one bearing a silver/magic-subset gate itself) bypass
gates whose keys are a subset of {silver, magic}; and a defender under
protection from normal missiles absorbs any small nonmagical missile (the
source's missile flag — an arrow or thrown flask is blocked, a hurled boulder
or enchanted arrow is not).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
defender
|
Any
|
The defending combatant — a
|
required |
source
|
DamageSource
|
The damage source presented. |
required |
ruleset
|
Ruleset
|
The ruleset in play. |
required |
attacker
|
Any | None
|
The attacking combatant (a |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
True when the hit is absorbed. |
check_morale
check_morale(subject: str, score: int, *, modifier: int = 0, stream: RngStream) -> MoraleResult
Check morale: 2d6 versus ML; over means flee or surrender.
ML 2 never fights and ML 12 never checks — both are exempt from the roll, and situational adjustments (clamped to ±2 per RAW) never apply to them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subject
|
str
|
The side or group key, for the event. |
required |
score
|
int
|
The morale score, 2–12. |
required |
modifier
|
int
|
The situational adjustment, clamped to ±2. |
0
|
stream
|
RngStream
|
The combat stream. |
required |
Returns:
| Type | Description |
|---|---|
MoraleResult
|
The outcome; referee-visibility events. |
damage_roll
damage_roll(
attacker: Any,
attack: Attack,
*,
context: AttackContext,
ruleset: Ruleset,
stream: RngStream,
defender: Any | None = None
) -> RollResult
Roll an attack's damage: dice, STR for melee, doublings, minimum 1.
With variable_weapon_damage off, every weapon and gear combat facet deals 1d6;
unarmed attacks stay 1d2 (the specific unarmed rule) and monster damage is
unaffected. Doublings (brace against a charging attacker, a 60-foot
mounted charge, the thief's back-stab multiplier, and the item damage
multipliers — giant strength on weapon attacks, growth on melee) apply after
the roll and STR. An enchanted arm adds its damage bonus, versus-clauses
swapping in the alternate against a matching defender. The Girdle of Giant
Strength branches on variable_weapon_damage: twice normal weapon damage under
the default, the printed 2d8 with the flag off.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attacker
|
Any
|
The attacking combatant — a
|
required |
attack
|
Attack
|
The weapon, facet, gear item, or monster attack ( |
required |
context
|
AttackContext
|
The caller-asserted situation. |
required |
ruleset
|
Ruleset
|
The ruleset in play. |
required |
stream
|
RngStream
|
The combat stream. |
required |
defender
|
Any | None
|
The defender (a |
None
|
Returns:
| Type | Description |
|---|---|
RollResult
|
The damage roll; |
damage_source_for
damage_source_for(attacker: Any, attack: Attack, context: AttackContext) -> DamageSource
Build the damage source an attack presents to the defender's defenses.
Silver weapons present silver; holy water presents holy; torch and burning
oil deal fire damage (they are burning brands — this is what routes them into a
regenerating monster's non-regenerable ledger). Monster natural attacks are
mundane; the hd5_counts_as_magical flag is resolved by
check_immunity from the attacker, not
here. A wielder under striking presents magic on weapon attacks (never
unarmed — the enchantment is the weapon's, carried on the wielder because item
instances have no ids of their own). Missile-ness is recorded for the
small-missile immunity gate (see DamageSource).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attacker
|
Any
|
The attacking combatant — a
|
required |
attack
|
Attack
|
The weapon, facet, gear item, or monster attack ( |
required |
context
|
AttackContext
|
The attack context ( |
required |
Returns:
| Type | Description |
|---|---|
DamageSource
|
The frozen damage source. |
deal_damage
deal_damage(
target: Any,
amount: int,
*,
source: DamageSource,
attacker_id: str | None = None,
rolls: tuple[int, ...] = (),
clock: GameClock | None = None,
ruleset: Ruleset | None = None,
stream: RngStream | None = None
) -> list[Event]
Apply damage: reductions, the hit point floor, ledgers, and death.
Reductions floor but never below 1. Fire and acid damage against a
regenerating monster whose regeneration they block accrue in the non-regenerable
ledger (capped at max HP); the monster is permanently dead only when that ledger
alone reaches max HP. A destructive killing source destroys the victim's
mundane equipment; ruleset and stream feed the magic-item death save when a
destructive kill lands (callers of destructive sources pass them).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
Any
|
The creature taking damage — a
|
required |
amount
|
int
|
The rolled amount, before reductions. |
required |
source
|
DamageSource
|
The damage source. |
required |
attacker_id
|
str | None
|
The attacker's entity id, for the event. |
None
|
rolls
|
tuple[int, ...]
|
The raw damage dice, for the event. |
()
|
clock
|
GameClock | None
|
When passed, stamps the target's |
None
|
ruleset
|
Ruleset | None
|
The ruleset in play, for the magic-item death save. |
None
|
stream
|
RngStream | None
|
The stream the death save rolls on (the resolving subsystem's own). |
None
|
Returns:
| Type | Description |
|---|---|
list[Event]
|
The damage, state, and death events, in order. |
destroy_equipment
destroy_equipment(
target: Any, *, source: DamageSource | None = None, ruleset: Ruleset | None = None, stream: RngStream | None = None
) -> list[Event]
Destroy a victim's carried equipment — the destructive-death outcome.
Called by the damage pipeline for destructive killing sources and by disintegrate (the material form destroyed includes what it carries).
Under the magic_item_death_save flag (default on), each magic item in the
doomed inventory rolls 1d20 against the owner's save value for the destructive
source's category (breath weapons save versus breath, destructive spells versus
spells, devices versus wands, anything else versus death), plus the
item's best combat bonus (the highest of its attack, damage, and AC bonuses —
a cursed item saves at its penalty). Survivors stay in the item list and their
instance ids ride the event's saved_items; the crawl lands them in a drop
pile at the victim's cell (surviving the blast but not the looting
would be no survival at all). The rolls are silent bookkeeping on the caller's
stream — the event carries the outcome.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
Any
|
The victim — a |
required |
source
|
DamageSource | None
|
The destructive damage source, for the save category. |
None
|
ruleset
|
Ruleset | None
|
The ruleset in play; |
None
|
stream
|
RngStream | None
|
The stream the item saves roll on. |
None
|
Returns:
| Type | Description |
|---|---|
list[Event]
|
The destruction event, or nothing for an empty inventory. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the flag is on, magic items are present, and no stream was supplied (programmer misuse — the save cannot roll). |
drain_monster_hd
Drain a monster's Hit Dice — the SRD says "experience level (or Hit Die)".
Symmetric with character drain: the instance re-derives THAC0 and saves from the reduced HD via the tables and loses a rolled d8 (minimum 1) from max and current hit points per die; a monster drained below 1 HD dies.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
monster
|
Any
|
The drained |
required |
levels
|
int
|
How many Hit Dice the drain removes. |
1
|
stream
|
RngStream
|
The RNG stream for the lost die rolls, conventionally the advancement stream — the same subsystem as character drain. |
required |
Returns:
| Type | Description |
|---|---|
list[Event]
|
The drain, state, and death events. |
effective_hd
Return a combatant's effective Hit Dice for the HD-budget targeting mode.
Sub-1 HD rounds up to 1 and fixed hit-point bonuses are dropped; characters count their level.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
combatant
|
Any
|
The combatant — a |
required |
Returns:
| Type | Description |
|---|---|
int
|
The effective HD, minimum 1. |
falling_damage
falling_damage(feet: int, stream: RngStream) -> RollResult | None
Roll falling damage: 1d6 per full 10 feet fallen, floored.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
feet
|
int
|
The distance fallen. |
required |
stream
|
RngStream
|
The combat stream. |
required |
Returns:
| Type | Description |
|---|---|
RollResult | None
|
The damage roll, or |
incapacitated
Return whether a combatant counts as incapacitated for morale triggers.
osrlib reads RAW's "slain, paralysed, etc" as: dead, paralysed, petrified, or asleep.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
combatant
|
Any
|
The combatant — a |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when incapacitated. |
melee_modifier_for
Return a combatant's melee attack-and-damage modifier, strength_set aware.
A strength_set modifier (Gauntlets of Ogre Power's 18, the Ring of
Weakness's 3) replaces the STR score the ability table's melee modifier
derives from; monsters keep their intrinsic 0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
combatant
|
Any
|
The attacking combatant — a
|
required |
Returns:
| Type | Description |
|---|---|
int
|
The signed melee modifier. |
morale_modifier
Return a combatant's spell morale modifier (bless/blight), for check_morale.
check_morale receives a side key and score,
never a creature, so spell morale modifiers ride its existing modifier
argument: the caller folds this into the situational adjustment. Spell morale
modifiers count inside the same ±2 total-adjustment clamp and the ML 2/12
exemptions as situational adjustments — one uniform adjustment rule, not a
second channel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
combatant
|
Any
|
The creature whose morale is being checked — a
|
required |
Returns:
| Type | Description |
|---|---|
int
|
The signed cumulative morale modifier. |
morale_triggers
Return the morale triggers a side's current state raises.
Queryable à la carte and consumed by the battle machine's auto-invocation:
first_death after the side's first death, half_incapacitated when half the
side (or more) is dead, paralysed, petrified, or asleep.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
members
|
Sequence[object]
|
The side's combatants — |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
The raised trigger keys; the caller tracks which it has already acted on. |
natural_healing
natural_healing(target: Any, stream: RngStream, *, ledger: EffectsLedger | None = None) -> list[Event]
Apply one full day of complete rest: 1d3 hit points.
Callable by whoever can attest the rest was uninterrupted (the crawl's rest
procedure automates the attestation).
Slowed-healing effects stretch the cadence: under mummy rot, natural healing
runs ten times slower, and an effect carrying a healing_rest_days
param (cause disease's and the cursed scroll's "twice the usual amount of
time" is 2) heals once per that many consecutive full rest days, tracked on the
effect — disease or not. When several apply, the slowest wins. Without a ledger
to track on, a diseased target does not heal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
Any
|
The resting creature — a
|
required |
stream
|
RngStream
|
The effects stream (natural-healing rolls count as effect-internal randomness, so they draw from the effects stream, not the combat stream). |
required |
ledger
|
EffectsLedger | None
|
The effects ledger carrying the disease effect, when any. |
None
|
Returns:
| Type | Description |
|---|---|
list[Event]
|
The healing and state events; empty on a non-healing rest day. |
participant_modifier
Return a combatant's individual-initiative modifier.
Characters get their DEX modifier plus the halfling's initiative_bonus class
tag; monsters take the caller-supplied modifier — the RAW referee-judgment
surface.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
combatant
|
Any
|
The combatant — a |
required |
monster_modifier
|
int
|
The referee's modifier for monsters. |
0
|
Returns:
| Type | Description |
|---|---|
int
|
The signed modifier. |
resolve_attack
resolve_attack(
attacker: Any,
defender: Any,
attack: Attack,
*,
context: AttackContext,
ruleset: Ruleset,
stream: RngStream,
clock: GameClock | None = None
) -> AttackResult
Resolve one attack end to end: roll, gate, damage.
The pipeline order is fixed: the attack roll first; on a hit, the immunity gate — if the defender's defenses exclude the source, no damage is rolled and the absorbed event says so; otherwise the damage roll and application.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attacker
|
Any
|
The attacking combatant — a
|
required |
defender
|
Any
|
The defending combatant — a |
required |
attack
|
Attack
|
The weapon, facet, gear item, or monster attack ( |
required |
context
|
AttackContext
|
The caller-asserted situation. |
required |
ruleset
|
Ruleset
|
The ruleset in play. |
required |
stream
|
RngStream
|
The combat stream. |
required |
clock
|
GameClock | None
|
When passed, damage stamps the defender's |
None
|
Returns:
| Type | Description |
|---|---|
AttackResult
|
The full resolution with its events. |
Examples:
A 1st-level fighter swings a sword at a goblin. Every draw comes from a named, seeded stream, so the same seed always replays the same fight:
from osrlib.core.alignment import Alignment
from osrlib.core.character import CHARACTER_CREATION_STREAM, create_character
from osrlib.core.combat import COMBAT_STREAM, AttackContext, resolve_attack
from osrlib.core.monsters import MONSTER_SPAWN_STREAM, spawn_monster
from osrlib.core.rng import RngStreams
from osrlib.core.ruleset import Ruleset
from osrlib.data import load_equipment, load_monsters
rules = Ruleset()
streams = RngStreams(master_seed=7)
hild = create_character(
name="Hild",
class_id="fighter",
alignment=Alignment.LAWFUL,
ruleset=rules,
stream=streams.get(CHARACTER_CREATION_STREAM),
).character
spawn = streams.get(MONSTER_SPAWN_STREAM)
goblin = spawn_monster(load_monsters().get("goblin"), id="goblin-1", stream=spawn)
sword = load_equipment().get("sword")
combat = streams.get(COMBAT_STREAM)
result = resolve_attack(hild, goblin, sword, context=AttackContext(), ruleset=rules, stream=combat)
assert result.events[0].code in ("combat.attack.hit", "combat.attack.missed")
assert result.attack_roll.hit # seed 7 hits: 12 rolled + 1 = 13 against a required 13
assert result.damage == 9 # same seed, same damage roll
assert goblin.current_hp == 0
assert "combat.death.died" in [event.code for event in result.events]
resolve_breath
resolve_breath(
monster: Any, targets: Sequence[object], *, ruleset: Ruleset, stream: RngStream, clock: GameClock | None = None
) -> list[Event]
Resolve a breath weapon against an explicitly supplied target list.
Damage is the monster's current hit points with save-for-half (dragons, three uses per day tracked on the instance), dice (the hellhound's per-HD dice — no daily limit; its 2-in-6 per-round gate is action-policy data), or save-or-die (the sea dragon's spittle). Save-for-half halving floors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
monster
|
Any
|
The breathing |
required |
targets
|
Sequence[object]
|
The affected combatants (the caller resolves the area for now) —
each a |
required |
ruleset
|
Ruleset
|
The ruleset in play. |
required |
stream
|
RngStream
|
The combat stream. |
required |
clock
|
GameClock | None
|
When passed, damage stamps targets' |
None
|
Returns:
| Type | Description |
|---|---|
list[Event]
|
The save and damage events, per target in order. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the monster has no breath weapon or its daily uses are spent —
validate with |
resolve_energy_drain
Wire a drain-tagged monster's touch to the drain procedure.
Reads the attacker's energy_drain tag (levels and XP policy are per-monster
data — the wight's floored halfway, the wraith/spectre/vampire's level minimum)
and applies character or monster drain by target kind. The spawn-consequence
prose rides the drain event from the tag's SRD text.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attacker
|
Any
|
The draining |
required |
target
|
Any
|
The drained combatant — a
|
required |
stream
|
RngStream
|
The RNG stream for the lost hit die rolls, conventionally the advancement stream (drain reverses advancement, so it draws from advancement's stream). |
required |
Returns:
| Type | Description |
|---|---|
list[Event]
|
The drain events. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the attacker has no |
resolve_gaze
resolve_gaze(
gazer: object,
engaged: Sequence[object],
*,
stream: RngStream,
ledger: EffectsLedger,
clock: GameClock,
allocator: Any,
registry: dict[str, Any]
) -> list[Event]
Resolve one round of a petrifying gaze against the engaged combatants.
Each engaged combatant not averting its eyes saves versus petrify or is turned to stone (a permanent effect — recoverable, stone is not dead). The ±modifiers for fighting with averted eyes are attack modifiers, applied by the caller through the attack context; mirror counterplay stays manual prose.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gazer
|
object
|
The gazing |
required |
engaged
|
Sequence[object]
|
The combatants in melee with it — each a
|
required |
stream
|
RngStream
|
The combat stream. |
required |
ledger
|
EffectsLedger
|
The effects ledger petrification attaches through. |
required |
clock
|
GameClock
|
The game clock. |
required |
allocator
|
Any
|
The |
required |
registry
|
dict[str, Any]
|
Live objects by entity id. |
required |
Returns:
| Type | Description |
|---|---|
list[Event]
|
The save and petrification events, per engaged combatant in order. |
resolve_splash_attack
resolve_splash_attack(
attacker: Any,
defender: Any,
attack: GearTemplate,
*,
context: AttackContext,
ruleset: Ruleset,
stream: RngStream,
ledger: EffectsLedger,
clock: GameClock,
allocator: Any,
registry: dict[str, Any]
) -> AttackResult
Resolve a thrown splash weapon: the attack, the first application, the douse.
A damaging hit attaches the two-round dousing effect (the second application
lands at the next round boundary). Holy water against the living, and burning
oil against uses_fire monsters, resolve as no effect through the damage
pipeline — never as a rejection (a rejection is free and would leak what
B/X hides until it matters).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attacker
|
Any
|
The throwing combatant — a
|
required |
defender
|
Any
|
The target — a |
required |
attack
|
GearTemplate
|
The splash gear item (holy water or burning oil). |
required |
context
|
AttackContext
|
The caller-asserted situation ( |
required |
ruleset
|
Ruleset
|
The ruleset in play. |
required |
stream
|
RngStream
|
The combat stream. |
required |
ledger
|
EffectsLedger
|
The effects ledger the douse attaches through. |
required |
clock
|
GameClock
|
The game clock. |
required |
allocator
|
Any
|
The |
required |
registry
|
dict[str, Any]
|
Live objects by entity id. |
required |
Returns:
| Type | Description |
|---|---|
AttackResult
|
The full resolution with its events. |
roll_initiative
roll_initiative(participants: Sequence[Participant], *, ruleset: Ruleset, stream: RngStream) -> InitiativeResult
Roll initiative: by side, or per participant under individual_initiative.
Ties always re-roll — RAW offers "re-roll or simultaneous", and osrlib adopts re-rolling because simultaneous resolution is a different combat model: tied sides, or tied individuals among themselves, re-roll in stable input order until distinct, each re-roll consuming draws. Slow-weapon actors act after all non-slow actors, ordered among themselves by their side's initiative (their own results under individual initiative) then stable order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
participants
|
Sequence[Participant]
|
The combatants' initiative entries, in stable order. |
required |
ruleset
|
Ruleset
|
The ruleset in play. |
required |
stream
|
RngStream
|
The combat stream. |
required |
Returns:
| Type | Description |
|---|---|
InitiativeResult
|
The rolls (re-rolls included) and the full acting order. |
roll_reaction
roll_reaction(*, modifier: int = 0, stream: RngStream) -> ReactionRollResult
Roll a monster reaction: 2d6 plus the modifier against the reaction table.
The CHA modifier is the caller's to supply from npc_reaction_modifier — RAW
applies it only when one specific character attempts to speak with the monsters.
Totals outside 2..12 clamp into the table's own outer bands. The event is
referee visibility: players learn reactions from behavior, just as they do
morale.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
modifier
|
int
|
The speaking character's CHA reaction modifier, when one applies. |
0
|
stream
|
RngStream
|
The RNG stream, conventionally the crawl's |
required |
Returns:
| Type | Description |
|---|---|
ReactionRollResult
|
The outcome, with its referee-visibility event. |
saving_throw
saving_throw(
target: Any,
category: SaveCategory,
*,
modifier: int = 0,
magical: bool = False,
element: str | None = None,
source: Any | None = None,
stream: RngStream
) -> SaveResult
Roll a saving throw: 1d20 at or above the target's value for the category.
The WIS magic-save modifier applies to characters when magical is true and the
category is not breath — osrlib reads "does not normally include saves against
breath attacks" as exactly that exclusion; referee discretion beyond it arrives
as a caller modifier. Energy auto_save defenses (a dragon versus similar
magical forms of its element) pass without a roll. Save-bonus stat modifiers
apply under the cumulative rule: unconditional, element-scoped (resist
cold/fire versus a matching element), and alignment-scoped (protection from
evil, consulted against source).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
Any
|
The saving combatant — a
|
required |
category
|
SaveCategory
|
The saving throw category. |
required |
modifier
|
int
|
The caller-supplied adjustment. |
0
|
magical
|
bool
|
Whether the effect is magical (WIS applies; auto-save defenses key off it). |
False
|
element
|
str | None
|
The effect's element, consulted by auto-save defenses and scoped save bonuses. |
None
|
source
|
Any | None
|
The creature whose attack or ability forced the save (a |
None
|
stream
|
RngStream
|
The combat stream. |
required |
Returns:
| Type | Description |
|---|---|
SaveResult
|
The outcome, with its events. |
select_targets
select_targets(
mode: TargetingMode,
candidates: Sequence[object],
*,
stream: RngStream,
count: int | None = None,
count_dice: str | None = None,
hd_budget: int | None = None
) -> tuple[list[object], list[Event]]
Resolve the shared targeting model against an explicit candidate list.
Modes: self and single take the (single) supplied candidate; up_to_n takes
the first N (fixed count or rolled count_dice — hold person's 1d4) in the
caller's order; area and gaze affect every candidate (the battle machine
supplies the footprint's candidates); hd_budget consumes candidates
weakest-first by effective HD, ties broken by stable input order — the budget
spends whole creatures, and a target whose HD exceed the remainder is skipped
while selection continues (sleep is this arithmetic's consumer).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mode
|
TargetingMode
|
The targeting mode. |
required |
candidates
|
Sequence[object]
|
The explicit candidate list, in the caller's order — each a
|
required |
stream
|
RngStream
|
The combat stream, for rolled counts. |
required |
count
|
int | None
|
The fixed N for |
None
|
count_dice
|
str | None
|
The rolled N for |
None
|
hd_budget
|
int | None
|
The dice budget for |
None
|
Returns:
| Type | Description |
|---|---|
tuple[list[object], list[Event]]
|
The selected targets and the referee targeting event. |
splash_douse_definition
splash_douse_definition(attack: Attack, source: DamageSource) -> EffectDefinition
Build the splash weapon's dousing effect: one more application next round.
osrlib adopts the reading that "inflicted for two rounds" means two applications — the hit's damage now, and the douse's expiry applies the listed damage once more at the next round boundary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attack
|
Attack
|
The splash item (its facet's damage dice carry over). |
required |
source
|
DamageSource
|
The damage source the hit presented. |
required |
Returns:
| Type | Description |
|---|---|
EffectDefinition
|
The one-round douse effect definition. |
validate_attack
validate_attack(
attacker: Any, defender: Any, attack: Attack, context: AttackContext, *, ruleset: Ruleset
) -> list[Rejection]
Validate an attack — the pure pre-phase: no RNG draws, no mutation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attacker
|
Any
|
The attacking combatant — a
|
required |
defender
|
Any
|
The defending combatant — a |
required |
attack
|
Attack
|
The weapon, facet, gear item, or monster attack ( |
required |
context
|
AttackContext
|
The caller-asserted situation. |
required |
ruleset
|
Ruleset
|
The ruleset in play ( |
required |
Returns:
| Type | Description |
|---|---|
list[Rejection]
|
Structured rejections; empty when the attack may be rolled. |
validate_breath
Validate a breath weapon use against the per-monster daily limit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
monster
|
Any
|
The breathing |
required |
Returns:
| Type | Description |
|---|---|
list[Rejection]
|
Structured rejections; empty when the monster may breathe. |