Skip to content

osrlib.core.monsters

Monster templates, the creatures spawned from them, and the ids they are given.

Two models do the work here, and they do different jobs. MonsterTemplate is the stat block, frozen and shared: one troll template describes every troll in the game. MonsterInstance is one creature in play, mutable, with its own hit points and its own wounds. Load the catalog once with load_monsters, then call spawn_monster for each creature that enters a fight. Nothing that happens to a creature then reaches the template or the other creatures spawned from it.

The instances you spawn are combatants. Hand them to osrlib.core.combat to fight, which takes a Character or a monster instance as attacker or target, and to osrlib.core.effects for conditions and timed modifiers. Their treasure comes from osrlib.core.treasure using the letters in TreasureRef, and the experience they are worth is on the template.

What a monster can do beyond hitting things is on the template as MonsterAbility records, each a tag the rules match on plus the SRD's own text. The tags osrlib acts on are regeneration, energy_drain, poison, paralysis, petrification, breath_weapon, gaze, disease, and uses_fire, and each one includes the numbers the procedures need. Every other ability is marked manual and is text for a referee to read. What a monster resists is separate, in Defenses, which the damage rules check every time a hit lands.

Each monster page in the SRD becomes one template per creature it describes, so a page that prints several sizes of hydra becomes one entry per size. Each template has to be spawnable by itself, and a template covering several creatures at once would not be.

Hit points are rolled from the MONSTER_SPAWN_STREAM stream, kept apart from combat so that a change to the combat rules never alters the creatures a seeded scenario spawns. IdAllocator, also here, hands out the entity ids those creatures are known by.

Typical usage:

from osrlib.core.monsters import MONSTER_SPAWN_STREAM, IdAllocator, spawn_monster
from osrlib.core.rng import RngStreams
from osrlib.data import load_monsters

allocator = IdAllocator()
stream = RngStreams(master_seed=3).get(MONSTER_SPAWN_STREAM)
template = load_monsters().get("troll")
troll = spawn_monster(template, id=allocator.allocate("monster"), stream=stream)
print(troll.id, troll.name, troll.max_hp, troll.armour_class, troll.thac0)
# monster-0001 Troll 42 4 13
print(template.xp, template.ability("regeneration").params["per_round"])
# 650 3

MONSTER_SPAWN_STREAM module-attribute

MONSTER_SPAWN_STREAM = StreamName.MONSTER_SPAWN

The stream key every session uses for rolling a spawned monster's hit points.

A stream key names one independent random-number sequence inside an RngStreams set. Pass streams.get(MONSTER_SPAWN_STREAM) as the stream argument of spawn_monster.

It is separate from the combat stream so that a change to how a fight resolves never alters the creatures a seeded scenario puts in front of the players.

AcAlternate

Bases: BaseModel

An armour class a monster has only in particular circumstances.

Read them from MonsterTemplate.ac_alternates. A creature that changes shape, or a band whose members wear different armour, prints more than one armour class and this contains the ones that are not the creature's ordinary value. Deciding when one applies is the referee's call. Nothing switches between them for you. Frozen.

ac instance-attribute

ac: int

The armour class in the descending presentation, where lower is better.

ac_ascending instance-attribute

ac_ascending: int

The same defense in the ascending presentation, where higher is better.

condition class-attribute instance-attribute

condition: str = ''

When this value applies, in the SRD's own words, like "in human form". Empty when the SRD prints the alternative without saying when it holds, as it does for a band whose members are armed differently.

AlignmentSpec

Bases: BaseModel

Which alignments a monster may have, and which it usually has.

Read it as MonsterTemplate.alignment. spawn_monster settles on one alignment for each creature it spawns, because the wards that turn on alignment need a single answer. Frozen.

options class-attribute instance-attribute

options: tuple[Alignment, ...] = Field(min_length=1)

The alignments this creature may be, at least one. A troll is chaotic and nothing else. A creature the SRD prints as any alignment has all three here.

usual class-attribute instance-attribute

usual: Alignment | None = None

Which of the options the creature usually is, when the SRD says so. None when it does not, and a creature with several options and no usual one spawns unresolved unless you name its alignment yourself.

AttackRoutine

Bases: BaseModel

One set of attacks a monster can make in a round, chosen as a whole.

Read them from MonsterTemplate.attacks. Most creatures have one. A creature with more than one is choosing between them, not doing both: a dragon either claws and bites or breathes, and the referee or your game decides which in a given round. Frozen.

attacks class-attribute instance-attribute

attacks: tuple[MonsterAttack, ...] = Field(min_length=1)

The attacks the routine makes, at least one; see MonsterAttack.

DamageKey

Bases: StrEnum

What a source of damage can be, for the purposes of a monster's defenses.

A monster that can be hurt only by certain kinds of attack names them here in Defenses.harmed_only_by, and a monster that takes reduced damage from a kind names it in DamageReduction. The damage rules check the keys on the attack against both.

holy is the one that behaves unlike the rest. Holy water has it, and it gets through any gate when the target is undead, because the SRD says outright that holy water harms undead. The wight's silver-or-magic gate would otherwise absorb the one weapon made for killing wights.

SILVER class-attribute instance-attribute

SILVER = 'silver'

A silver weapon, which is what gets through the lesser undead.

MAGIC class-attribute instance-attribute

MAGIC = 'magic'

An enchanted weapon or a spell.

FIRE class-attribute instance-attribute

FIRE = 'fire'

Fire, whether from a torch, a flask of oil, or a spell.

COLD class-attribute instance-attribute

COLD = 'cold'

Cold, from a creature's attack or a spell.

HOLY class-attribute instance-attribute

HOLY = 'holy'

Holy water, which harms undead whatever else they resist.

DamageReduction

Bases: BaseModel

A cut taken out of the damage a monster suffers, applied after the dice are rolled.

Read them from Defenses.reductions. The damage is divided and rounded down, and a hit that gets through always does at least 1 point. Frozen.

keys class-attribute instance-attribute

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

Which sources of damage are reduced. Empty means all of them that got past the monster's gate, which is the mummy taking half from everything. Naming keys narrows it, as with the wraith, which takes half from silver weapons alone.

divisor class-attribute instance-attribute

divisor: int = Field(default=2, ge=2)

What the damage is divided by. 2 is halving, which is what the SRD prints.

Defenses

Bases: BaseModel

What a monster resists, in the form the damage rules check.

Read it as MonsterTemplate.defenses. The combat procedures consult it every time a hit lands, before any damage is rolled. The SRD's own wording for the same defenses stays on the template's abilities, for a front end to show. Frozen.

harmed_only_by class-attribute instance-attribute

harmed_only_by: tuple[DamageKey, ...] = ()

The kinds of damage that can hurt this creature at all. An attack with none of them is absorbed and no damage is rolled, which is how a wight ignores an ordinary sword. Empty means anything hurts it.

reductions class-attribute instance-attribute

reductions: tuple[DamageReduction, ...] = ()

Cuts taken out of the damage that does get through; see DamageReduction.

energy class-attribute instance-attribute

energy: dict[Element, EnergyDefense] = {}

How the creature resists each kind of energy; see EnergyDefense.

condition_immunities class-attribute instance-attribute

condition_immunities: tuple[Condition, ...] = ()

Conditions the creature can never be put into, like the undead being beyond poison, charm, and sleep. Attempts to apply one are dropped.

Element

Bases: StrEnum

The kinds of energy a monster's breath, attack, or defense can be made of.

A dragon's breath names one of these, and so does a creature's immunity in EnergyDefense. The damage rules match the two against each other to decide whether an attack lands at all.

FIRE class-attribute instance-attribute

FIRE = 'fire'

Fire, as a red dragon breathes and a fire giant ignores.

COLD class-attribute instance-attribute

COLD = 'cold'

Cold, as a white dragon breathes.

LIGHTNING class-attribute instance-attribute

LIGHTNING = 'lightning'

Lightning, as a blue dragon breathes.

ACID class-attribute instance-attribute

ACID = 'acid'

Acid, as a black dragon breathes.

GAS class-attribute instance-attribute

GAS = 'gas'

Poisonous gas, as a green dragon breathes.

POISON class-attribute instance-attribute

POISON = 'poison'

Poison delivered by an attack rather than as a cloud.

STEAM class-attribute instance-attribute

STEAM = 'steam'

Scalding steam, as a dragon turtle breathes.

EnergyDefense

Bases: BaseModel

How a monster resists one kind of energy.

Read them from Defenses.energy, keyed by Element. The damage rules check the entry for the element of an incoming attack before rolling any damage. Frozen.

immunity instance-attribute

immunity: Literal['all', 'nonmagical']

"all" when nothing of this element can hurt the creature, magical or not, as with a fire giant and fire. "nonmagical" when ordinary sources cannot but magic can: a red dragon shrugs off its own breath and a flask of burning oil, and still takes damage from a fire ball.

auto_save_magical class-attribute instance-attribute

auto_save_magical: bool = False

True when the creature passes any saving throw against a magical form of this element without rolling, which is the dragons' automatic save against attacks like their own breath.

IdAllocator

Bases: BaseModel

Hands out entity ids that no two things in a game share.

Every creature, effect, and valuable a game creates needs an id, and this is what gives them one. A GameSession keeps its own and passes it to everything that allocates, so pass the session's allocator rather than a fresh one when you are inside a session. Outside one, build your own.

Ids count up per prefix and never repeat, which is what lets a save and its replay refer to the same creature. The counters are ordinary state, so an allocator serializes with the rest of a save and resumes where it left off.

Examples:

from osrlib.core.monsters import IdAllocator

allocator = IdAllocator()
print(allocator.allocate("monster"), allocator.allocate("monster"))
# monster-0001 monster-0002
print(allocator.allocate("effect"))
# effect-0001

counters class-attribute instance-attribute

counters: dict[str, int] = {}

How many ids have been handed out under each prefix. Written by allocate; you never set it yourself.

allocate

allocate(prefix: str) -> str

Return the next unused id under prefix.

Each prefix counts independently and never repeats, so calling it twice with the same prefix gives two different ids. It mutates the allocator, which is the point: the id is spent once it is returned.

Parameters:

Name Type Description Default
prefix str

What kind of thing is being named, like "monster", "effect", "npc", or "valuable".

required

Returns:

Type Description
str

The id, which is the prefix, a hyphen, and a number padded to four digits, counting

str

from 0001.

MonsterAbility

Bases: BaseModel

One thing a monster can do, as a tag the rules read plus the SRD text it came from.

Read them from MonsterTemplate.abilities, or look one up by tag with MonsterTemplate.ability. The combat and effect procedures match on the tag and read the numbers out of params, so a monster's special powers are data rather than branches in the code. Frozen.

Not everything reduces to numbers. An ability marked manual is text for a referee to read and act on. Nothing in osrlib does anything with it.

tag class-attribute instance-attribute

tag: str = Field(min_length=1)

The identifier the rules match on. The ones osrlib acts on are "regeneration", "energy_drain", "poison", "paralysis", "petrification", "breath_weapon", "gaze", "disease", and "uses_fire".

name class-attribute instance-attribute

name: str = Field(min_length=1)

The ability's name as the SRD prints it, for display.

prose instance-attribute

prose: str

The SRD's own description, which is what to show a player or referee.

manual class-attribute instance-attribute

manual: bool = False

True when nothing in osrlib acts on this ability and the prose is the whole of it.

params class-attribute instance-attribute

params: dict[str, int | str | bool | tuple[int | str, ...]] = {}

The values the rules read, like a troll's regeneration delay and rate, or the number of levels an energy drain takes. Empty when there are none.

MonsterAttack

Bases: BaseModel

One attack a monster makes: what it is called, how often, how much it hurts, and what else it does.

Read them from an AttackRoutine. A troll's routine contains two talon attacks and a bite, so its routine has two of these, one with a count of 2. Frozen.

count class-attribute instance-attribute

count: int = Field(default=1, ge=1)

How many of this attack the monster makes in a round.

name class-attribute instance-attribute

name: str = Field(min_length=1)

What the attack is, as the SRD names it: "talon", "bite", "weapon".

damage class-attribute instance-attribute

damage: str | None = None

The damage as a dice expression, like "1d6", which roll evaluates. None when the damage is fixed instead.

fixed_damage class-attribute instance-attribute

fixed_damage: int | None = None

Damage that is a flat number rather than a roll. None when damage says it.

fixed_damage_options class-attribute instance-attribute

fixed_damage_options: tuple[int, ...] = ()

The alternatives the SRD prints when the damage depends on something it leaves to the referee, like an insect swarm doing 2 or 4 depending on the target's armour. Choosing between them is the referee's call.

by_weapon class-attribute instance-attribute

by_weapon: bool = False

True when the monster attacks with whatever weapon it carries, so damage is what the SRD prints as typical rather than a fixed property of the creature.

by_weapon_modifier class-attribute instance-attribute

by_weapon_modifier: int = 0

The bonus or penalty the SRD prints alongside a by-weapon attack.

effects class-attribute instance-attribute

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

What a hit does beyond damage, as tags like "poison", "paralysis", or "energy_drain". Look the matching MonsterAbility up on the template for the numbers behind each.

MonsterCatalog

Bases: BaseModel

Every monster, as returned by load_monsters.

Look one up by id with get, or iterate monsters to filter by Hit Dice, category, or whatever your encounter table needs. The catalog is loaded once and cached, so calling the loader again is free. Frozen.

monsters instance-attribute

monsters: tuple[MonsterTemplate, ...]

The templates, in the order the data file lists them. Ids are unique.

get

get(monster_id: str) -> MonsterTemplate

Return the monster with monster_id.

Parameters:

Name Type Description Default
monster_id str

The id to look up, like "troll" or "red_dragon"; see the monster id index for all of them.

required

Returns:

Type Description
MonsterTemplate

The monster template. Spawn a creature from it with

MonsterTemplate

Raises:

Type Description
ValueError

If no monster has that id. The message names the id you passed.

Examples:

from osrlib.data import load_monsters

catalog = load_monsters()
troll = catalog.get("troll")
print(troll.name, troll.hit_dice.count, troll.xp)
# Troll 6 650
print([template.id for template in catalog.monsters if template.page == "Hydra.md"])
# ['hydra_10', 'hydra_11', 'hydra_12', 'hydra_5', 'hydra_6', 'hydra_7', 'hydra_8', 'hydra_9']

MonsterHitDice

Bases: BaseModel

A monster's Hit Dice, as the stat block prints them.

Read it as MonsterTemplate.hit_dice. spawn_monster rolls hit points from it, and osrlib.core.tables uses it to place the monster on the attack matrix and in an experience band. Frozen.

count class-attribute instance-attribute

count: int = Field(default=0, ge=0)

How many dice are rolled. 0 for a creature whose hit points are fixed.

die class-attribute instance-attribute

die: int = 8

The size of each die, which is 8 for almost everything. A creature with half a Hit Die rolls 1d4 instead.

modifier class-attribute instance-attribute

modifier: int = 0

Hit points added to or taken from the total, and it may be negative: the SRD's 1-1 is a modifier of −1. A positive modifier also makes the creature attack as though it had one more Hit Die, and a negative one lowers the experience band.

asterisks class-attribute instance-attribute

asterisks: int = Field(default=0, ge=0)

How many special abilities the SRD credits the creature with, which is what raises its experience award. Not decoration.

average_hp class-attribute instance-attribute

average_hp: int | None = None

The average hit points the SRD prints for the creature, for a referee who would rather not roll. Spawning ignores it and rolls.

fixed_hp class-attribute instance-attribute

fixed_hp: int | None = None

Hit points that are not rolled at all, like the creature with exactly 1 hit point or a hydra with 8 per head. None when the dice decide.

MonsterInstance

Bases: BaseModel

One creature in play, spawned from a frozen MonsterTemplate.

Get one from spawn_monster. This is what takes damage, gains conditions, and dies. The template it came from never changes, and every other creature spawned from it is unaffected by what happens here.

It offers the same surface a Character does, which is THAC0, attack bonus, both armour classes, saving throws, conditions, and stat modifiers, so the functions in osrlib.core.combat and osrlib.core.effects take either without caring which they got.

Anything the template already says is read through template rather than copied here, and the properties below do that for you where a drained creature would otherwise read the wrong value.

id instance-attribute

id: str

The entity id, usually from an IdAllocator. Events name the creature by it.

template instance-attribute

template: MonsterTemplate

The stat block this creature was spawned from. Read anything the creature has in common with its kind from here.

max_hp class-attribute instance-attribute

max_hp: int = Field(ge=1)

The hit points it was spawned with.

current_hp class-attribute instance-attribute

current_hp: int = Field(ge=0)

The hit points it has left, from 0 up to max_hp. Reaching 0 means it has dropped. Death itself is the dead condition, applied by kill.

conditions class-attribute instance-attribute

conditions: tuple[ActiveCondition, ...] = ()

The conditions on it, applied and cleared through osrlib.core.effects.

stat_modifiers class-attribute instance-attribute

stat_modifiers: tuple[ActiveModifier, ...] = ()

Timed bonuses and penalties on it, from spells and effects.

alignment class-attribute instance-attribute

alignment: Alignment | None = None

The alignment this creature actually has, settled when it was spawned. None when the template offered several and neither you nor the template named one. A ward that turns on alignment then treats it as differing, which errs toward protecting the party.

nonregen_damage class-attribute instance-attribute

nonregen_damage: int = Field(default=0, ge=0)

Damage a regenerating creature can never heal. Fire and acid land here, and a troll stays dead only once this alone reaches its maximum hit points.

last_damaged_round class-attribute instance-attribute

last_damaged_round: int | None = None

The combat round in which it was last hurt, which is what regeneration counts its delay from. None before anything has hurt it.

breath_uses_today class-attribute instance-attribute

breath_uses_today: int = Field(default=0, ge=0)

How many times it has used its breath weapon today. A creature with a breath weapon gets three uses a day.

drained_hd class-attribute instance-attribute

drained_hd: int = Field(default=0, ge=0)

How many Hit Dice have been drained from it. Its THAC0, attack bonus, and saving throws all re-derive from what is left.

name property

name: str

The creature's name, from its template. Use it wherever you show the creature to a player.

hit_dice_count property

hit_dice_count: int

How many Hit Dice this creature still has: its template's, less any that were drained away.

Its THAC0, attack bonus, and saving throws all follow from this rather than from the template, which is why draining a creature weakens it in every way at once.

thac0 property

thac0: int

The number this creature needs to hit armour class 0 under descending armour class.

It is the template's printed value, which already accounts for a creature whose Hit Dice have a bonus attacking as though it had one more. A creature that has been drained looks its value up again for the Hit Dice it has left.

attack_bonus property

attack_bonus: int

The bonus this creature adds to an attack roll under ascending armour class.

The ascending presentation of thac0, and it re-derives after a drain in the same way.

armour_class property

armour_class: int | None

The creature's armour class, where lower is better. None for a creature no attack roll is made against.

armour_class_ascending property

armour_class_ascending: int | None

The creature's armour class, where higher is better. None for a creature no attack roll is made against.

saves property

saves: SavingThrows

The five saving throw targets for this creature.

The template's printed values, unless the creature has been drained of Hit Dice, in which case it saves as the band its remaining Hit Dice put it in.

melee_modifier property

melee_modifier: int

Always 0: monsters have no STR score, and the SRD gives them no bonus in its place.

It exists so that the combat functions can read the same property on a monster as on a Character.

missile_modifier property

missile_modifier: int

Always 0: monsters have no DEX score, and the SRD gives them no missile bonus in its place.

initiative_modifier property

initiative_modifier: int

Always 0: a monster has no initiative modifier of its own.

The SRD leaves a monster's initiative to the referee, so pass one to the initiative roll yourself when you want it.

MonsterSaves

Bases: BaseModel

A monster's saving throws, and the stat block's note about where they come from.

Read it as MonsterTemplate.saves, or read MonsterInstance.saves to get the values already adjusted for a creature that has been drained of Hit Dice. Frozen.

values instance-attribute

values: SavingThrows

The five targets; see SavingThrows.

save_as instance-attribute

save_as: str

What the stat block says the creature saves as, like "2" for a second-level fighter or "Cleric 1". Keep it for display and for checking the values against the SRD's bands. The rules read values.

MonsterTemplate

Bases: BaseModel

A monster's stat block: everything true of every creature of that kind.

Get one from load_monsters().get(monster_id); see the monster id index for the ids. Then call spawn_monster to put an actual creature in front of the players.

It is frozen, and play never writes to it. A template is shared by every creature of its kind, while the hit points one creature has left, the wounds it has taken, and the conditions on it all live on the MonsterInstance spawned from it. That separation is why a wounded troll does not weaken every other troll in the dungeon.

id instance-attribute

id: str

The monster id, like "troll", which is what you look it up by.

name instance-attribute

name: str

The creature's name as the SRD prints it, for display.

page instance-attribute

page: str

Which SRD page this template was compiled from. Templates sharing a page are variants of one creature, like the sizes of hydra or the colours of dragon.

intro class-attribute instance-attribute

intro: str = ''

The SRD's description of the creature, for a referee or a narrator to read out.

ac class-attribute instance-attribute

ac: int | None = None

Armour class in the descending presentation. None for a creature whose attackers need no hit roll.

ac_ascending class-attribute instance-attribute

ac_ascending: int | None = None

The same defense in the ascending presentation, or None alongside ac.

ac_alternates class-attribute instance-attribute

ac_alternates: tuple[AcAlternate, ...] = ()

Armour classes that apply only in particular circumstances; see AcAlternate.

attack_roll_required class-attribute instance-attribute

attack_roll_required: bool = True

False for a creature no attack roll is needed against, like a green slime, whose attacks land without one. Both armour class fields are None then.

hit_dice instance-attribute

hit_dice: MonsterHitDice

The dice its hit points are rolled on; see MonsterHitDice.

attacks class-attribute instance-attribute

attacks: tuple[AttackRoutine, ...] = ()

The sets of attacks it can make, one chosen per round; see AttackRoutine.

thac0 class-attribute instance-attribute

thac0: int = Field(ge=2, le=20)

The number it needs to hit armour class 0 under descending armour class.

attack_bonus class-attribute instance-attribute

attack_bonus: int = Field(ge=-1)

The same attack under ascending armour class.

movement class-attribute instance-attribute

movement: tuple[MovementMode, ...] = Field(min_length=1)

How it gets around and how fast, ordinary movement first; see MovementMode.

saves instance-attribute

saves: MonsterSaves

Its saving throws; see MonsterSaves.

morale class-attribute instance-attribute

morale: int | None = Field(default=None, ge=2, le=12)

How willing it is to keep fighting, from 2 to 12. A morale check rolls 2d6 and the creature holds on a result at or under it. None when the stat block prints no morale score.

morale_alternates class-attribute instance-attribute

morale_alternates: tuple[MoraleAlternate, ...] = ()

Morale scores that apply only in particular circumstances; see MoraleAlternate.

alignment instance-attribute

alignment: AlignmentSpec

Which alignments it may have; see AlignmentSpec.

xp class-attribute instance-attribute

xp: int = Field(ge=0)

The experience for defeating one, as the SRD prints it.

xp_notes class-attribute instance-attribute

xp_notes: tuple[XpNote, ...] = ()

What a leader among them is worth instead; see XpNote.

number_appearing instance-attribute

number_appearing: NumberAppearing

How many turn up, which depends on where they are met; see NumberAppearing.

treasure class-attribute instance-attribute

treasure: TreasureRef = TreasureRef()

What they have; see TreasureRef.

abilities class-attribute instance-attribute

abilities: tuple[MonsterAbility, ...] = ()

What it can do beyond attacking; see MonsterAbility.

defenses class-attribute instance-attribute

defenses: Defenses = Defenses()

What it resists, in the form the damage rules check; see Defenses.

categories class-attribute instance-attribute

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

What kind of thing it is, as tags like "undead", "person", and "enchanted". Spells and effects that single out a kind of creature match on these.

overrides_applied class-attribute instance-attribute

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

The names of the compile-time corrections applied to this creature's SRD page. Provenance for anyone checking the data against the SRD. Nothing in play reads it.

ability

ability(tag: str) -> MonsterAbility | None

Return this monster's ability with tag, or None when it has none.

Use it to ask whether a creature has a power and to read the numbers behind it in one step: the troll's regeneration rate, the number of levels a wight's touch drains, the shape and element of a dragon's breath.

Parameters:

Name Type Description Default
tag str

The ability tag to look for, like "regeneration" or "energy_drain".

required

Returns:

Type Description
MonsterAbility | None

The first ability with that tag, or None.

Examples:

from osrlib.data import load_monsters

wight = load_monsters().get("wight")
drain = wight.ability("energy_drain")
print(drain.params["levels"], drain.params["xp_policy"])
# 1 halfway
print(wight.ability("breath_weapon"))
# None

MoraleAlternate

Bases: BaseModel

A morale score that applies only in a particular situation.

Read them from MonsterTemplate.morale_alternates. A creature that fights bravely except when fire is involved has its ordinary score on the template and the exception here. Deciding whether the condition holds is the referee's call, so nothing applies these for you. Frozen.

score class-attribute instance-attribute

score: int = Field(ge=2, le=12)

The morale score in this situation, from 2 to 12. A morale check rolls 2d6 and the creature holds on a result at or under it.

condition class-attribute instance-attribute

condition: str = Field(min_length=1)

When it applies, in the SRD's own words, like "in melee" or "fear of fire".

MovementMode

Bases: BaseModel

One way a monster gets around, and how fast.

Read them from MonsterTemplate.movement. The first is always the creature's ordinary movement. Later ones are its other ways of moving, and you pick the one the situation calls for. Frozen.

rate_feet class-attribute instance-attribute

rate_feet: int = Field(ge=0)

Feet covered in one exploration turn, which is the rate used while mapping and searching.

encounter_rate_feet class-attribute instance-attribute

encounter_rate_feet: int = Field(ge=0)

Feet covered in one combat round, which is a third of the exploration rate.

descriptor class-attribute instance-attribute

descriptor: str | None = None

How the creature is moving, in the SRD's own word: "flying", "swimming", "in webs". None for walking, which needs no word.

NumberAppearing

Bases: BaseModel

How many of a creature appear, which depends on where they are met.

Read it as MonsterTemplate.number_appearing and roll the value that matches the encounter. Frozen.

dungeon instance-attribute

How many are met wandering in a dungeon, which is the smaller group.

lair instance-attribute

How many are met in their lair or in the wilderness, where a creature is found in its full numbers.

NumberAppearingValue

Bases: BaseModel

How many of a creature turn up, as either dice to roll or a flat number.

Read them off NumberAppearing. Roll dice with roll when it is set, take fixed when that is, and fall back to the creature's own description when see_below is. Exactly one of the three applies. Frozen.

dice class-attribute instance-attribute

dice: str | None = None

The dice to roll, like "1d8". None when the count is fixed or described in prose.

fixed class-attribute instance-attribute

fixed: int | None = None

A flat count. None when dice or prose decide.

see_below class-attribute instance-attribute

see_below: bool = False

True when the SRD gives no number here and the creature's description says how many appear. dice and fixed are both None then.

TreasureRef

Bases: BaseModel

What treasure a monster has, as the stat block prints it.

Read it as MonsterTemplate.treasure, then pass it to plan_treasure_ref, which sorts the letters into lair, per-creature, and per-group treasure and carries parenthetical, extra_gp, and multiplier through. Generate each letter in the plan with generate_treasure; see the treasure type index for the letters.

Go through the plan rather than looping letters into generate_treasure yourself. A loop over letters alone drops the bracketed letters, the flat gold, and the multiplier without telling you, and it treats a per-creature letter as though it were a lair hoard.

A creature with no treasure has an empty reference. Frozen.

letters class-attribute instance-attribute

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

The treasure type letters, like ("D",).

More than one means every one of them is generated. plan_treasure_ref sorts them by section, so a lair letter, a per-creature letter, and a per-group letter in the same reference each land in the right place.

parenthetical class-attribute instance-attribute

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

The letters the SRD prints in brackets.

plan_treasure_ref adds them to the lair treasure whatever section they belong to, so a bandit's U (A) puts U on the group and A in the lair.

extra_gp class-attribute instance-attribute

extra_gp: int = Field(default=0, ge=0)

Gold pieces the stat block adds on top of the rolled treasure, into the lair hoard.

multiplier class-attribute instance-attribute

multiplier: int = Field(default=1, ge=1)

How many times the whole listed generation repeats, as with a noble's V × 3.

1 unless the SRD says otherwise.

special class-attribute instance-attribute

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

Valuables that are not treasure types at all, like an elephant's tusks or a bee's honey. Nothing generates these; show them to the referee.

see_below class-attribute instance-attribute

see_below: bool = False

True when the creature's description says what it has rather than the treasure line.

XpNote

Bases: BaseModel

What a leader among a group of these creatures is worth in experience.

Read them from MonsterTemplate.xp_notes. A band of gnolls has a leader worth more than the rest, and this is that number. What else makes the leader different stays in the template's abilities as prose, because the SRD gives it as prose. Frozen.

role class-attribute instance-attribute

role: str = Field(min_length=1)

What the variant is called, like "leader", "chieftain", or "bodyguard".

xp class-attribute instance-attribute

xp: int = Field(ge=0)

The experience for defeating one.

spawn_monster

spawn_monster(
    template: MonsterTemplate, *, id: str, stream: RngStream, alignment: Alignment | None = None
) -> MonsterInstance

Put one creature into play: roll its hit points and give it an identity of its own.

Call it once per creature an encounter puts in front of the players. Load the catalog once with load_monsters and spawn from the same template as often as you like. Each creature gets its own hit points, its own wounds, and its own conditions, and nothing that happens to one reaches the template or any of its siblings. That is the reason to spawn rather than pass templates around.

Hand what you get to osrlib.core.combat to fight it. Its treasure comes from generate_treasure using the letters on the template.

Hit points are rolled from the template's Hit Dice and floored at 1, so even the unluckiest roll leaves a creature standing. A creature whose hit points the SRD fixes, like the one with exactly 1 or a hydra with 8 per head, gets that number and rolls nothing.

The creature's alignment is settled here rather than left open, because a ward like protection from evil has to have something to test. Your choice wins, then the template's usual alignment, then its only option. A creature whose template offers several with no usual one and no choice from you is left unresolved, and such a ward then treats it as differing, which errs toward protecting the party.

Parameters:

Name Type Description Default
template MonsterTemplate

The stat block to spawn from, from load_monsters().get(monster_id); see the monster id index for the ids.

required
id str

The entity id to give it, usually allocator.allocate("monster") from an IdAllocator.

required
stream RngStream

The stream to roll hit points on, conventionally streams.get(MONSTER_SPAWN_STREAM). No draw is taken for a creature with fixed hit points.

required
alignment Alignment | None

The alignment this particular creature has, when you want to choose. It must be one the template allows.

None

Returns:

Type Description
MonsterInstance

The creature, at full hit points, with no conditions and no wounds.

Raises:

Type Description
ValueError

If alignment is not one the template allows.

Examples:

from osrlib.core.monsters import MONSTER_SPAWN_STREAM, IdAllocator, spawn_monster
from osrlib.core.rng import RngStreams
from osrlib.data import load_monsters

allocator = IdAllocator()
stream = RngStreams(master_seed=3).get(MONSTER_SPAWN_STREAM)
template = load_monsters().get("troll")
first = spawn_monster(template, id=allocator.allocate("monster"), stream=stream)
second = spawn_monster(template, id=allocator.allocate("monster"), stream=stream)
print(first.id, first.max_hp, second.id, second.max_hp)
# monster-0001 42 monster-0002 22

first.current_hp -= 10
print(first.current_hp, second.current_hp)
# 32 22