Skip to content

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

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

COMBAT_STREAM = 'combat'

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.

MELEE_REACH_FEET module-attribute

MELEE_REACH_FEET = 5

Melee attacks reach up to 5 feet.

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.

distance_feet class-attribute instance-attribute

distance_feet: int | None = None

situational_modifier class-attribute instance-attribute

situational_modifier: int = 0

defender_ally_ac_bonus class-attribute instance-attribute

defender_ally_ac_bonus: int = 0

behind_target class-attribute instance-attribute

behind_target: bool = False

target_unaware class-attribute instance-attribute

target_unaware: bool = False

defender_retreating class-attribute instance-attribute

defender_retreating: bool = False

braced class-attribute instance-attribute

braced: bool = False

charging class-attribute instance-attribute

charging: bool = False

fired_last_round class-attribute instance-attribute

fired_last_round: bool = False

attacker_large class-attribute instance-attribute

attacker_large: bool = False

lit class-attribute instance-attribute

lit: bool = False

fixed_damage_option class-attribute instance-attribute

fixed_damage_option: int = 0

monster_missile class-attribute instance-attribute

monster_missile: bool = False

AttackResult

Bases: BaseModel

A full attack resolution: the roll, the gate verdict, and any damage.

attack_roll instance-attribute

attack_roll: AttackRollResult

absorbed class-attribute instance-attribute

absorbed: bool = False

damage class-attribute instance-attribute

damage: int | None = None

events class-attribute instance-attribute

events: tuple[Event, ...] = ()

AttackRollResult

Bases: BaseModel

An attack roll's outcome; roll is None for the helpless auto-hit.

hit instance-attribute

hit: bool

auto class-attribute instance-attribute

auto: bool = False

roll class-attribute instance-attribute

roll: int | None = None

modifier class-attribute instance-attribute

modifier: int = 0

total class-attribute instance-attribute

total: int | None = None

required class-attribute instance-attribute

required: int | None = None

natural class-attribute instance-attribute

natural: int | None = None

events class-attribute instance-attribute

events: tuple[Event, ...] = ()

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).

keys class-attribute instance-attribute

keys: tuple[str, ...] = ()

element class-attribute instance-attribute

element: str | None = None

magical class-attribute instance-attribute

magical: bool = False

kind class-attribute instance-attribute

kind: str = 'weapon'

destructive class-attribute instance-attribute

destructive: bool = False

missile class-attribute instance-attribute

missile: bool = False

InitiativeResult

Bases: BaseModel

An initiative resolution: per-key rolls (re-rolls included) and the acting order.

mode instance-attribute

mode: str

entries instance-attribute

entries: tuple[InitiativeRoll, ...]

order instance-attribute

order: tuple[str, ...]

events class-attribute instance-attribute

events: tuple[Event, ...] = ()

MoraleResult

Bases: BaseModel

A morale check's outcome; exempt marks ML 2 and ML 12 (no roll made).

held instance-attribute

held: bool

exempt class-attribute instance-attribute

exempt: bool = False

roll class-attribute instance-attribute

roll: int | None = None

modifier class-attribute instance-attribute

modifier: int = 0

events class-attribute instance-attribute

events: tuple[Event, ...] = ()

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."

passed class-attribute instance-attribute

passed: dict[str, int] = {}

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 None when no further checks are made (they fight on).

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.

key instance-attribute

key: str

side instance-attribute

side: str

slow class-attribute instance-attribute

slow: bool = False

modifier class-attribute instance-attribute

modifier: int = 0

ReactionRollResult

Bases: BaseModel

A reaction roll's outcome: the raw 2d6, the modifier, and the table band.

result instance-attribute

roll instance-attribute

roll: int

modifier class-attribute instance-attribute

modifier: int = 0

total instance-attribute

total: int

events class-attribute instance-attribute

events: tuple[Event, ...] = ()

SaveCategory

Bases: StrEnum

The five saving throw categories.

DEATH class-attribute instance-attribute

DEATH = 'death'

WANDS class-attribute instance-attribute

WANDS = 'wands'

PARALYSIS class-attribute instance-attribute

PARALYSIS = 'paralysis'

BREATH class-attribute instance-attribute

BREATH = 'breath'

SPELLS class-attribute instance-attribute

SPELLS = 'spells'

SaveResult

Bases: BaseModel

A saving throw's outcome; roll is None for auto-save defenses.

passed instance-attribute

passed: bool

auto class-attribute instance-attribute

auto: bool = False

roll class-attribute instance-attribute

roll: int | None = None

modifier class-attribute instance-attribute

modifier: int = 0

required class-attribute instance-attribute

required: int | None = None

events class-attribute instance-attribute

events: tuple[Event, ...] = ()

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.

SELF class-attribute instance-attribute

SELF = 'self'

SINGLE class-attribute instance-attribute

SINGLE = 'single'

UP_TO_N class-attribute instance-attribute

UP_TO_N = 'up_to_n'

HD_BUDGET class-attribute instance-attribute

HD_BUDGET = 'hd_budget'

AREA class-attribute instance-attribute

AREA = 'area'

GAZE class-attribute instance-attribute

GAZE = 'gaze'

alignments_differ

alignments_differ(source: Any, target: Any) -> bool

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 Character or a MonsterInstance.

required
target Any

The warded creature — a Character or a MonsterInstance.

required

Returns:

Type Description
bool

True when the alignments differ or either is unresolved.

apply_healing

apply_healing(target: Any, amount: int, *, source: str = 'magical') -> list[Event]

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 Character or a MonsterInstance; mutated.

required
amount int

The healing amount. Non-negative.

required
source str

The healing kind: magical (the default), natural, or regeneration.

'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 None.

required

Returns:

Type Description
WeaponTemplate | CombatFacet | None

The facet carrying dice, qualities, and ranges, or None for unarmed and

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 Character or a MonsterInstance.

required
defender Any

The defending combatant — a Character or a MonsterInstance.

required
attack Attack

The weapon, facet, gear item, or monster attack (None for unarmed).

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

cannot_move(combatant: Any) -> bool

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 Character or a MonsterInstance.

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 Character or a MonsterInstance.

required
source DamageSource

The damage source presented.

required
ruleset Ruleset

The ruleset in play.

required
attacker Any | None

The attacking combatant (a Character or a MonsterInstance), consulted by hd5_counts_as_magical.

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 Character or a MonsterInstance.

required
attack Attack

The weapon, facet, gear item, or monster attack (None for unarmed).

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 Character or a MonsterInstance), for versus-clause resolution.

None

Returns:

Type Description
RollResult

The damage roll; total is the final amount (minimum 1).

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 Character or a MonsterInstance.

required
attack Attack

The weapon, facet, gear item, or monster attack (None for unarmed).

required
context AttackContext

The attack context (lit matters for burning oil).

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 Character or a MonsterInstance; mutated.

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 last_damaged_round (regeneration's delay anchor).

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 Character or a MonsterInstance; its inventory is emptied but for saved magic items.

required
source DamageSource | None

The destructive damage source, for the save category.

None
ruleset Ruleset | None

The ruleset in play; None skips the save (everything burns).

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_monster_hd(monster: Any, *, levels: int = 1, stream: RngStream) -> list[Event]

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 MonsterInstance; mutated.

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

effective_hd(combatant: Any) -> int

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 Character or a MonsterInstance.

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 None for falls under 10 feet (no dice, no draw).

incapacitated

incapacitated(combatant: Any) -> bool

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 Character or a MonsterInstance.

required

Returns:

Type Description
bool

True when incapacitated.

melee_modifier_for

melee_modifier_for(combatant: Any) -> int

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 Character or a MonsterInstance.

required

Returns:

Type Description
int

The signed melee modifier.

morale_modifier

morale_modifier(combatant: Any) -> int

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 Character or a MonsterInstance.

required

Returns:

Type Description
int

The signed cumulative morale modifier.

morale_triggers

morale_triggers(members: Sequence[object]) -> list[str]

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 — Character or MonsterInstance objects.

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 Character or a MonsterInstance; mutated.

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

participant_modifier(combatant: Any, *, monster_modifier: int = 0) -> int

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 Character or a MonsterInstance.

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 Character or a MonsterInstance.

required
defender Any

The defending combatant — a Character or a MonsterInstance.

required
attack Attack

The weapon, facet, gear item, or monster attack (None for unarmed).

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 last_damaged_round.

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 MonsterInstance; its daily counter increments.

required
targets Sequence[object]

The affected combatants (the caller resolves the area for now) — each a Character or a MonsterInstance.

required
ruleset Ruleset

The ruleset in play.

required
stream RngStream

The combat stream.

required
clock GameClock | None

When passed, damage stamps targets' last_damaged_round.

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 validate_breath first; over-breathing is programmer misuse.

resolve_energy_drain

resolve_energy_drain(attacker: Any, target: Any, *, stream: RngStream) -> list[Event]

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 MonsterInstance.

required
target Any

The drained combatant — a Character or a MonsterInstance.

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 energy_drain tag.

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 MonsterInstance.

required
engaged Sequence[object]

The combatants in melee with it — each a Character or a MonsterInstance.

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 IdAllocator granting effect ids.

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 Character or a MonsterInstance.

required
defender Any

The target — a Character or a MonsterInstance.

required
attack GearTemplate

The splash gear item (holy water or burning oil).

required
context AttackContext

The caller-asserted situation (lit matters for oil).

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 IdAllocator granting the douse effect's id.

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 "encounter" stream.

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 Character or a MonsterInstance.

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 Character or a MonsterInstance), consulted by alignment-scoped save bonuses.

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_dicehold 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 Character or a MonsterInstance.

required
stream RngStream

The combat stream, for rolled counts.

required
count int | None

The fixed N for up_to_n.

None
count_dice str | None

The rolled N for up_to_n.

None
hd_budget int | None

The dice budget for hd_budget.

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 Character or a MonsterInstance.

required
defender Any

The defending combatant — a Character or a MonsterInstance.

required
attack Attack

The weapon, facet, gear item, or monster attack (None for unarmed).

required
context AttackContext

The caller-asserted situation.

required
ruleset Ruleset

The ruleset in play (weapon_reload is enforced here).

required

Returns:

Type Description
list[Rejection]

Structured rejections; empty when the attack may be rolled.

validate_breath

validate_breath(monster: Any) -> list[Rejection]

Validate a breath weapon use against the per-monster daily limit.

Parameters:

Name Type Description Default
monster Any

The breathing MonsterInstance.

required

Returns:

Type Description
list[Rejection]

Structured rejections; empty when the monster may breathe.