osrlib.crawl.triggers
Authored triggers: the observable-event patterns and the trigger spec.
A trigger is an authored binding from an observable event pattern, optionally
gated by conditions, to referee-command consequences. Where a gate
(GateSpec) is level-triggered — evaluated live at
the moment the party attempts something — a trigger is edge-triggered: it watches
the events a command produced and fires on the crossing itself. The lever that
opens the portcullis is a trigger; the door that wants the brass key is a gate.
The pieces:
- A pattern (
TriggerPattern) names the observable: a location crossed, an item acquired, a monster defeated, a flag written. Patterns are matched against the events themselves, never against current state, because a consequence can move the party mid-batch and an event still carries the facts of the moment it described. - Conditions (
ConditionSpec) narrow the firing further, all of them evaluated live against session state at match time. A trigger fires, it does not take: a condition withconsumes=Trueis rejected at parse, because a trigger reacts to something that has already happened and has no attempt of its own to charge a toll against. - Consequences (
ConsequenceCommand) are the referee commands the firing issues, in authored order. - A narrative block (
NarrativeBlock) carries the trigger's text:firedis the referee's beat,journalis the players' — a trigger that should say something to the table authors a journal form.
Document order is the order of the Adventure.triggers
tuple: triggers matching one event fire in that order, and a trigger's consequences
execute in authored order.
Triggers are inert content on their own. The
Interpreter is the shipped listener that
plays them: registered on a session, it matches every command's events against the
adventure's triggers and issues each firing's commands.
FIRST_LIVING_SELECTOR
module-attribute
The character_id an authored consequence writes to address one member.
Expanded at issue time to the first living member in marching order — the treasure-recipient convention. With nobody standing there is no recipient, and the consequence is dropped with a note rather than guessed at.
PARTY_SELECTOR
module-attribute
The character_id an authored consequence writes to address the whole party.
Expanded at issue time to one command per living member, in marching order, so the command log stays fully concrete and replays exactly. A party with nobody left standing expands to no commands at all — a reward for the dead is nothing, not an error.
TriggerPattern
module-attribute
TriggerPattern = Annotated[
AreaEnteredPattern
| LevelEnteredPattern
| DungeonEnteredPattern
| TownEnteredPattern
| ItemAcquiredPattern
| MonsterDefeatedPattern
| FlagSetPattern,
Field(discriminator="pattern_type"),
]
The pattern union, discriminated on pattern_type. New observables join it
additively; the discriminator values are wire values and serialize into every
document that carries a trigger.
AreaEnteredPattern
DungeonEnteredPattern
FlagSetPattern
Bases: BaseModel
A session flag was written — the edge, not the state.
The match is against the value the write carried, so a flag rewritten with the
value it already held still fires. value=None matches any written value, which
is unambiguous because a flag value is a str, an int, or a bool and never
None; an authored value compares through
flag_values_equal, the same strict
comparison FlagEqualsCondition uses.
ItemAcquiredPattern
Bases: BaseModel
A party member acquired an item with item_id.
The id domain is the one has_item reads: the effective equipment catalog
(shipped ∪ adventure-bundled) or the magic-item catalog. Acquisitions report
mundane items by catalog id and magic items by their session-scoped instance id,
so a magic item_id matches by resolving that instance against the acquiring
character's inventory.
LevelEnteredPattern
Bases: BaseModel
The party arrived on a dungeon level.
However it got there: a stair between levels and an entry from town both land the party on the level, and both match. (The engine reports the coarser crossing when a move changes dungeons, and a dungeon crossing is a level arrival too.)
MonsterDefeatedPattern
Bases: BaseModel
A monster of template_id was defeated — slain, routed, or surrendered.
Every outcome is a defeat, so the pattern does not filter on one. Defeats are reported at battle end, so the boss falling opens the portcullis after the fighting stops, never mid-round.
TownEnteredPattern
Bases: BaseModel
The party arrived in the base town, however it got there.
An adventure has one town, so the pattern needs no fields: the homecoming beat fires on the return trip and on a referee's placement alike.
pattern_type
class-attribute
instance-attribute
pattern_type: Literal['town_entered'] = 'town_entered'
TriggerSpec
Bases: BaseModel
One authored trigger: when it fires, what must hold, and what happens.
A spec is inert data; the Interpreter
is the shipped listener that plays it.
Once-only by default — the fired-mark that
MarkTriggerFired writes is session
state, so once-only survives a save, a load, and a replay. repeatable=True is
the authored opt-in for a trigger that fires every time its pattern matches.
conditions all have to hold: the tuple is an AND with no combinators, each
condition evaluated live at the moment of the match. consequences may be empty
— a trigger whose whole job is its journal beat is a normal shape.
Examples:
from osrlib.crawl.commands import SetDoorState
from osrlib.crawl.dungeon import Direction
from osrlib.crawl.narrative import NarrativeBlock
from osrlib.crawl.triggers import FlagSetPattern, TriggerSpec
portcullis = SetDoorState(dungeon_id="crypt", level_number=1, x=2, y=0, direction=Direction.SOUTH, open=True)
trigger = TriggerSpec(
id="portcullis-rises",
when=FlagSetPattern(key="crypt.lever", value="pulled"),
consequences=(portcullis,),
narrative=NarrativeBlock(
fired="The counterweight drops somewhere in the wall.",
journal="The east lever gives; below, a portcullis grinds upward.",
),
)
assert not trigger.repeatable