osrlib.crawl.quests
Authored quests: the matching clause, the objective spec, and the quest spec.
A quest is the errand the adventure keeps score of: what starts it, what it asks for, what it pays, and whether finishing it ends the adventure.
Where a quest sits. You write QuestSpecs into the
quests tuple of an Adventure, and that tuple's
order is document order. A session seeds one
QuestState per quest at construction and keeps them
in session.quests, keyed by quest id, with each quest's objectives keyed in the order
QuestSpec.objectives authored them, so every walk over
either is deterministic. Nothing advances that state until your game registers an
Interpreter on the session, and even then the
state moves only through the four lifecycle commands
ActivateQuest,
RevealObjective,
CompleteObjective, and
CompleteQuest. Each reports itself with a
player-visible event:
QuestActivatedEvent,
ObjectiveRevealedEvent,
ObjectiveCompletedEvent,
QuestCompletedEvent, and, for the quest
that concludes the adventure,
AdventureCompletedEvent. The active
quests and their revealed objectives reach a front end through
PlayerView.quests.
A quest composes the trigger vocabulary rather than introducing one of its own. An
activation, an objective's completion, and a hidden objective's reveal are each a
TriggerClause: a
TriggerPattern naming the observable, plus the
ConditionSpecs that have to hold when it matches.
Those are the same edge-triggered patterns and the same live condition evaluation an
authored TriggerSpec uses. Rewards are the same
ConsequenceCommand surface under the same
party selectors (PARTY_SELECTOR and
FIRST_LIVING_SELECTOR).
A quest observes, it does not take. A clause condition with consumes=True is rejected
at parse for the reason a trigger's is: the event a clause matches has already happened,
so there is no attempt of the quest's own to charge a toll against.
Author a trigger instead when nothing has to be scored and the adventure only has to react. The guide Gates, triggers, and quests runs a quest end to end from an adventure document.
ObjectiveSpec
Bases: BaseModel
One objective: what it is called, how it completes, whether it starts hidden, and its text.
Put your objectives in the objectives tuple of a
QuestSpec, in the order the quest log should show
them. Their live state is ObjectiveState,
and the revealed ones reach a front end as
ObjectiveViews.
Objectives are monotonic: hidden becomes revealed, incomplete becomes complete, and neither goes back, because the quest vocabulary authors no repeat.
A hidden objective with no reveal_when is a normal shape. It surfaces when it
completes, because completing an objective reveals it. reveal_when on an objective
that starts visible is rejected at parse, since a reveal clause for something already
on the list would never be read.
Examples:
from osrlib.crawl.narrative import NarrativeBlock
from osrlib.crawl.quests import ObjectiveSpec, TriggerClause
from osrlib.crawl.triggers import ItemAcquiredPattern
recover = ObjectiveSpec(
id="recover-idol",
name="Recover the flask",
when=TriggerClause(pattern=ItemAcquiredPattern(item_id="holy_water")),
narrative=NarrativeBlock(progress="The flask is yours; the shrine is quiet again."),
)
assert not recover.hidden and recover.reveal_when is None
id
class-attribute
instance-attribute
The objective's id, unique within its quest and free to repeat in another. It is
the key its state and its view use, and the label everything falls back to when
name is unauthored.
name
class-attribute
instance-attribute
name: str = ''
The objective's display label, the words a quest log shows beside its checkbox. It defaults empty, so a document written before the field existed loads unchanged, and empty means unauthored: the view, the lifecycle events, and the default formatter all fall back to the id.
when
instance-attribute
when: TriggerClause
The clause that completes the objective. Completing it also reveals it, so a hidden objective needs no reveal clause to show up once it is done.
hidden
class-attribute
instance-attribute
hidden: bool = False
Whether the objective starts off the party's list. A hidden objective has no view until it is revealed.
reveal_when
class-attribute
instance-attribute
reveal_when: TriggerClause | None = None
The clause that surfaces a hidden objective ahead of its completion. It is rejected at parse on an objective that starts visible.
narrative
class-attribute
instance-attribute
narrative: NarrativeBlock | None = None
The objective's own beats. It reads two of the block: offer, the line its reveal
shows and journals, and progress, the line its completion shows and journals.
QuestSpec
Bases: BaseModel
One authored quest: when it starts, what it asks for, and what it pays.
Put your quests in the quests tuple of an
Adventure, and register an
Interpreter on the session to play them. A
spec on its own is inert data. Its live state is
QuestState in session.quests, and an active
quest reaches a front end as a QuestView.
Rewards are issued after the quest completes, in authored order, and only for a
completion the interpreter itself ruled: a quest your game completes by hand with
CompleteQuest pays nothing, because paying
is this listener reading the quest.
Examples:
from osrlib.crawl.commands import AwardXP
from osrlib.crawl.narrative import NarrativeBlock
from osrlib.crawl.quests import ObjectiveSpec, QuestSpec, TriggerClause
from osrlib.crawl.triggers import PARTY_SELECTOR, DungeonEnteredPattern, ItemAcquiredPattern
recover = ObjectiveSpec(id="recover", when=TriggerClause(pattern=ItemAcquiredPattern(item_id="holy_water")))
errand = QuestSpec(
id="the-flask",
name="The Stolen Reliquary",
activation=TriggerClause(pattern=DungeonEnteredPattern(dungeon_id="barrow")),
objectives=(recover,),
rewards=(AwardXP(character_id=PARTY_SELECTOR, amount=200),),
concludes_adventure=True,
narrative=NarrativeBlock(
offer="Sister Halda wants the reliquary back, and she is not asking twice.",
completion="The flask returns to its niche. The temple bells answer.",
),
)
assert errand.completion == "all"
id
class-attribute
instance-attribute
The quest's id, unique across the adventure. It keys the quest's state, and the
source stamp on every command the quest issues names it, in the form
quest:{id}.
name
class-attribute
instance-attribute
The quest's display name, included in its lifecycle events and its view so a renderer needs no document to look it up in.
activation
class-attribute
instance-attribute
activation: TriggerClause | None = None
The clause that brings the quest into play. None means the quest is active from
session start, a standing charge on the party from round 0 with no activation beat
to show, because there is no command channel before the first command. An
authored clause makes activation an event the party crosses.
objectives
class-attribute
instance-attribute
objectives: tuple[ObjectiveSpec, ...] = Field(min_length=1)
What the quest asks for, in the order a quest log should show it. At least one is required, because an objective-less quest under the all rule would be born complete.
rewards
class-attribute
instance-attribute
rewards: tuple[ConsequenceCommand, ...] = ()
The referee commands issued after the quest completes, in authored order, with
PARTY_SELECTOR and
FIRST_LIVING_SELECTOR expanded to
the members they name. Each stands or drops on its own. An authored source is
rejected at parse, because the issuing quest stamps it.
completion
class-attribute
instance-attribute
completion: Literal['all', 'any'] = 'all'
The completion rule: "all" requires every objective, "any" takes the first one
to land.
The quest walk stops at the completion it issues, so when one event would complete two
objectives at once, the second one is left incomplete and finishes on the next event
that matches its clause. Under "any" that is what usually happens, since the first
objective to land finishes the quest and the rest stay open.
concludes_adventure
class-attribute
instance-attribute
concludes_adventure: bool = False
Whether finishing this quest ends the adventure. The session moves to victory
and emits an
AdventureCompletedEvent, which
happens before the first reward is issued, so a reward that would resume play there
is dropped with a note.
narrative
class-attribute
instance-attribute
narrative: NarrativeBlock | None = None
The quest's own beats. It reads two of the block: offer, the line its activation
shows and journals, and completion, the line its completion shows and journals.
Per-objective beats live on the objectives.
TriggerClause
Bases: BaseModel
One matching clause: the observable, and what has to hold when it happens.
A quest uses clauses in three places, and they behave the same in all three: the
activation of a QuestSpec, and the when and
reveal_when of an ObjectiveSpec. The
Interpreter matches a clause exactly the
way it matches a TriggerSpec, so a quest and a
trigger can never disagree about what an event means.
Examples:
from osrlib.crawl.gates import HasItemCondition
from osrlib.crawl.quests import TriggerClause
from osrlib.crawl.triggers import TownEnteredPattern
walked_home_carrying_it = TriggerClause(
pattern=TownEnteredPattern(),
conditions=(HasItemCondition(item_id="holy_water"),),
)
assert walked_home_carrying_it.pattern.pattern_type == "town_entered"
pattern
instance-attribute
pattern: TriggerPattern
The observable that matches the clause, one member of
TriggerPattern. The field is pattern
rather than when, so an objective's completion clause reads
objective.when.pattern.
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.