osrlib.crawl.triggers
Authored triggers: the observable-event patterns and the trigger spec.
A trigger binds an observable event pattern, optionally narrowed by conditions, to the referee commands that run when it matches.
Where a trigger sits. You write TriggerSpecs into
the triggers tuple of an Adventure, and that
tuple's order is document order: triggers matching one event fire in it. Nothing plays
them until your game registers an
Interpreter on the session with
GameSession.register_listener.
The interpreter matches every accepted command's events against the adventure's triggers
and issues each firing's commands. A firing reports itself through a
TriggerFiredEvent, which includes the fired
beat, through a JournalEntryAddedEvent
when the block authors a journal form, through whatever events its consequences produce,
and through a NoteRecordedEvent for a
consequence the engine refused. The fired-mark lives in session.fired_triggers.
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, and the door that needs 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 and never against current state, because a consequence can move the party inside the same batch while an event keeps describing the moment it reported. - Conditions (
ConditionSpec) narrow the firing further, all of them evaluated live against session state at match time. A trigger fires, it does not take, so a condition withconsumes=Trueis rejected at parse: 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) contains the trigger's text.firedis the referee's beat andjournalthe players', so write a journal form for the line the table should see.
Reach for a quest (QuestSpec) instead when the
adventure has to keep score toward an ending. A quest composes these same patterns and
conditions. The guide
Gates, triggers, and quests
runs all three from one adventure document.
FIRST_LIVING_SELECTOR
module-attribute
The character_id an authored consequence writes to address one member.
The Interpreter expands it at issue time to
the first living member in marching order, which is the treasure-recipient convention:
one object goes to one member, and the party sorts it out with
GiveItems. With nobody standing there is no
recipient, so the consequence is dropped and a
NoteRecordedEvent says why rather than the
engine guessing. Use PARTY_SELECTOR when every
member should receive the reward.
PARTY_SELECTOR
module-attribute
The character_id an authored consequence writes to address the whole party.
Write it as the character_id of a
GrantItem,
GrantCoins, or
AwardXP in a trigger's consequences or a quest's
rewards, where a concrete character id would be a guess: a document written before play
cannot know the ids a session hands out.
The Interpreter expands it at issue time into
one command per living member, in marching order, so the command log stays concrete and
replays exactly. A party with nobody left standing expands to no commands at all,
because a reward for the dead is nothing rather than an error. Use
FIRST_LIVING_SELECTOR when one member
should receive the whole thing. No other command honors a selector, and a literal
character id is passed through untouched.
TriggerPattern
module-attribute
TriggerPattern = Annotated[
AreaEnteredPattern
| LevelEnteredPattern
| DungeonEnteredPattern
| TownEnteredPattern
| ItemAcquiredPattern
| MonsterDefeatedPattern
| FlagSetPattern,
Field(discriminator="pattern_type"),
]
The pattern union, discriminated on pattern_type.
Annotate a field with this alias when you write your own model that holds an authored
pattern. Pydantic then picks the member from the pattern_type value in the document.
It is the type of TriggerSpec.when and of
TriggerClause.pattern, so a quest watches exactly
what a trigger watches. New observables join the union additively, and the discriminator
values are wire values that appear in every document that includes a trigger.
AreaEnteredPattern
Bases: BaseModel
The party entered a keyed area.
Matches a LocationEnteredEvent for an
area. Area ids are scoped to their level, so the pattern names the whole triple: an
area id of "shrine" means nothing without the dungeon and level it belongs to.
Reach for LevelEnteredPattern when the
whole level is the crossing you want.
Examples:
from osrlib.crawl.triggers import AreaEnteredPattern
shrine = AreaEnteredPattern(dungeon_id="barrow", level_number=2, area_id="shrine")
assert shrine.pattern_type == "area_entered"
pattern_type
class-attribute
instance-attribute
pattern_type: Literal['area_entered'] = 'area_entered'
The discriminator value, area_entered. It appears in the document, and you
never set it yourself.
dungeon_id
class-attribute
instance-attribute
The id of the dungeon the area belongs to.
level_number
class-attribute
instance-attribute
The 1-based number of the level the area belongs to.
DungeonEnteredPattern
Bases: BaseModel
The party crossed into a dungeon, from town or from another dungeon.
The coarse arrival, matched by a
LocationEnteredEvent for a dungeon.
Use it for the beat that belongs to walking in the front door, and
LevelEnteredPattern for one that
belongs to a particular level.
Examples:
from osrlib.crawl.triggers import DungeonEnteredPattern
arrival = DungeonEnteredPattern(dungeon_id="barrow")
assert arrival.pattern_type == "dungeon_entered"
pattern_type
class-attribute
instance-attribute
pattern_type: Literal['dungeon_entered'] = 'dungeon_entered'
The discriminator value, dungeon_entered. It appears in the document, and you
never set it yourself.
FlagSetPattern
Bases: BaseModel
A session flag was written: the edge, not the state.
This is the lever. Your game, or another trigger's consequence, executes
SetFlag, the write emits a
FlagSetEvent, and the trigger watching that key
fires. The match is against the value the write set, so a flag rewritten with the
value it already had still fires. To ask about the value a flag has now instead of a
write that just happened, use a
FlagEqualsCondition.
Examples:
from osrlib.crawl.triggers import FlagSetPattern
pulled = FlagSetPattern(key="crypt.lever", value="pulled")
assert pulled.pattern_type == "flag_set"
pattern_type
class-attribute
instance-attribute
pattern_type: Literal['flag_set'] = 'flag_set'
The discriminator value, flag_set. It appears in the document, and you
never set it yourself.
value
class-attribute
instance-attribute
The written value to match, or None to match any write of the key. None 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 a FlagEqualsCondition uses,
so a stored True never matches an authored 1.
ItemAcquiredPattern
Bases: BaseModel
A party member acquired an item with item_id.
Matched by an ItemAcquiredEvent, however
the item arrived: out of a cache, from a referee's grant, or from another member's
hands. The id domain is the one a has_item condition reads, which is the effective
equipment catalog, meaning the shipped items plus the adventure's bundled ones, or
the magic-item catalog.
An acquisition reports 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.
Examples:
from osrlib.crawl.triggers import ItemAcquiredPattern
taken = ItemAcquiredPattern(item_id="brass_key")
assert taken.pattern_type == "item_acquired"
pattern_type
class-attribute
instance-attribute
pattern_type: Literal['item_acquired'] = 'item_acquired'
The discriminator value, item_acquired. It appears in the document, and you
never set it yourself.
LevelEnteredPattern
Bases: BaseModel
The party arrived on a dungeon level.
It matches however the party got there: a stair between levels and an entry from town both land it on the level. The engine reports the coarsest crossing a move passed, so a party walking in from town reports a dungeon entry, and this pattern counts that as a level arrival too.
Examples:
from osrlib.crawl.triggers import LevelEnteredPattern
deeper = LevelEnteredPattern(dungeon_id="barrow", level_number=2)
assert deeper.pattern_type == "level_entered"
pattern_type
class-attribute
instance-attribute
pattern_type: Literal['level_entered'] = 'level_entered'
The discriminator value, level_entered. It appears in the document, and you
never set it yourself.
dungeon_id
class-attribute
instance-attribute
The id of the dungeon the level belongs to.
MonsterDefeatedPattern
Bases: BaseModel
A monster of template_id was defeated: slain or routed.
Both outcomes count as a defeat, and the pattern can't tell them apart. Defeats are
reported at battle end through
MonsterDefeatedEvent, so a boss falling
opens the portcullis once the fighting stops and never mid-round. Author no trigger
that has to land the instant a blow kills.
Examples:
from osrlib.crawl.triggers import MonsterDefeatedPattern
beaten = MonsterDefeatedPattern(template_id="ogre")
assert beaten.pattern_type == "monster_defeated"
pattern_type
class-attribute
instance-attribute
pattern_type: Literal['monster_defeated'] = 'monster_defeated'
The discriminator value, monster_defeated. It appears in the document, and you
never set it yourself.
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. Pair it with a
HasItemCondition for the errand that is
finished only when the party walks home carrying the thing.
Examples:
from osrlib.crawl.triggers import TownEnteredPattern
home = TownEnteredPattern()
assert home.pattern_type == "town_entered"
pattern_type
class-attribute
instance-attribute
pattern_type: Literal['town_entered'] = 'town_entered'
The discriminator value, town_entered. It appears in the document, and you
never set it yourself.
TriggerSpec
Bases: BaseModel
One authored trigger: when it fires, what must hold, and what happens.
Put your triggers in the triggers tuple of an
Adventure, and register an
Interpreter on the session to play them. A
spec on its own is inert data, and nothing in the engine reads it without that
listener.
A trigger fires once ever by default. The fired-mark that
MarkTriggerFired writes is session state,
so once-only survives a save, a load, and a replay, and session.fired_triggers is
where you read it. repeatable=True is the authored opt-in for a trigger that fires
every time its pattern matches.
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
id
class-attribute
instance-attribute
The trigger's id, unique across the adventure. It is what the fired-mark records,
and what the source stamp on every command the firing issues names, in the form
trigger:{id}.
when
instance-attribute
when: TriggerPattern
The observable that fires it, one member of
TriggerPattern.
conditions
class-attribute
instance-attribute
conditions: tuple[ConditionSpec, ...] = ()
Extra tests that all have to hold at the moment of the match. The tuple is an AND
with no combinators, and each condition is evaluated live against session state
through condition_holds. A condition with
consumes=True is rejected at parse.
repeatable
class-attribute
instance-attribute
repeatable: bool = False
Whether the trigger fires every time its pattern matches. The default fires it once for the life of the session.
consequences
class-attribute
instance-attribute
consequences: tuple[ConsequenceCommand, ...] = ()
The referee commands the firing issues, in authored order, with
PARTY_SELECTOR and
FIRST_LIVING_SELECTOR expanded to
the members they name. Each command stands or drops on its own, so one rejection
never stops the rest. The tuple may be empty: a trigger whose whole job is its
journal beat is a normal shape. An authored source is rejected at parse, because
the issuing trigger stamps it.
narrative
class-attribute
instance-attribute
narrative: NarrativeBlock | None = None
The trigger's authored text. A trigger reads two beats of the block: fired,
reported by the referee-visibility
TriggerFiredEvent, and journal, which
becomes a player-visible journal entry.