Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion docs/adaptations.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,13 @@ The policy draws only from the monster-action stream. Monsters with a scripted p

When a monster's attack hits in battle, the session applies each effect the attack lists if the effect is `paralysis`, `poison`, `disease`, `energy_drain`, or `petrification` and the monster's ability for it isn't `manual`. The effects apply once for each hit, so a target hit by two paralysing claws saves twice. The target saves versus paralysis against paralysis and petrification, and versus death, with the ability's modifier, against poison. A failed save against poison kills at once, or when the poison's onset runs out. Mummy rot and energy drain allow no save. An elf, whose class has `ghoul_paralysis_immunity`, rolls no save against the ghoul's or the thoul's paralysis and isn't paralysed. osrlib has no model of size, so the same paralysis affects a creature larger than an ogre, which the SRD exempts. A dead or petrified target takes none of these effects.

The session runs the basilisk's and the medusa's petrifying gaze through `resolve_gaze` once a round while the gazer's group is at 5', before the group acts. The gaze reaches the party's front rank and takes no attack roll. The session never rolls the basilisk's `gaze` attack entry as an attack. `resolve_gaze` spares a creature with the `averted_eyes` condition, but no command gives a member that condition, so in play every member in the front rank meets the gaze. The SRD's penalties for fighting with averted eyes (−4 to the member's attacks and +2 to the basilisk's) aren't applied. A gazer that surprises the party from beyond 5' doesn't gaze in its free round, though the SRD says characters a basilisk surprises meet its gaze. Mirrors and the medusa's disguise are left to the referee.
The session runs the basilisk's and the medusa's petrifying gaze through `resolve_gaze` once a round while the gazer's group is at 5', before the group acts. The gaze reaches the party's front rank and takes no attack roll. The session never rolls the basilisk's `gaze` attack entry as an attack.

The SRD's basilisk says "Unless averting eyes or using a mirror, characters in melee are affected each round", so a member's `BattleDeclaration` takes a `gaze_defense` of `avert_eyes` or `mirror` for one round, with any action. A member who averts their eyes has the `averted_eyes` condition from the moment the round is accepted until it ends, and `resolve_gaze` spares them. The basilisk's and the medusa's entries give the cost: "-4 penalty to-hit; the basilisk gains a +2 bonus to attack". The −4 applies to every attack roll the member makes that round, whatever it's aimed at, because the SRD gives it without naming a target. The +2 applies only to the attacks a gazer makes on the member, because the SRD names the gazer for it (pinned). A member who watches a mirror is spared the gaze too, and every attack roll they make that round takes the −1 the basilisk's entry gives for "Fighting by looking in a mirror". No monster gains a bonus against them. The SRD's equipment list has the mirror, "Useful for looking around corners or for reflecting a gaze attack", so the member has to carry one, and the session refuses a `mirror` stance from a member who doesn't with `battle.declaration.no_mirror`. Neither stance draws anything or emits an event of its own, so a round without one draws exactly as before. A back-rank member is out of the gaze's reach and needs no stance, and a member who can't act, and so doesn't declare, is already spared by `resolve_gaze`.

The basilisk's entry also says "Characters surprised by a basilisk meet its gaze". In the monsters' free round after they surprise the party, each basilisk gazes once at every living member of the party, back rank included, at whatever distance the encounter opened, before its group acts. That gaze takes the place of the front-rank gaze, so a member at 5' meets it once that round. The party declares nothing in that round, so no stance can spare it. The medusa's entry has no such clause, so a medusa that surprises the party gazes in its free round only if it's already at 5', and then only at the front rank (pinned).

The SRD's rule that a basilisk or a medusa that sees its own reflection saves or turns to stone is left to the referee. The basilisk's entry gives that a 2-in-6 chance and the medusa's gives no chance at all, so both monsters' `mirrors` abilities stay `manual`. The basilisk's `surprise` ability and both monsters' `averting_eyes` abilities aren't `manual`, because the session applies them. The medusa's disguise is left to the referee too. Locked by `test_avert_eyes.py`.

An effect in a later attack routine applies when battle resolves that routine. The spitting cobra's poison bite is an alternative routine, so under the default policy the cobra bites in about half the rounds it fights. The vampire's charming gaze is a special routine, which battle never rolls, so the referee applies it.

Expand Down
2 changes: 1 addition & 1 deletion docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ The kernel owns time as a first-class concept; the framework decides when it pas
- `core.clock` defines the units — the round (10 seconds), the turn (10 minutes), the day — and a `GameClock` that advances in those units and drives everything time-dependent.
- `core.effects` is a lifecycle engine for anything with a duration or a periodic tick: spell effects, potion effects, poison and disease, burning light sources, and ongoing monster abilities like troll regeneration. An effect declares its duration, tick behavior, stacking rule, and expiry outcome; as the clock advances, the engine resolves ticks and expirations and emits events (`SpellExpired`, `TorchExpired`, `PoisonTick`).
- Tick order is canonical: at each time-step boundary, expirations resolve first, then periodic ticks; simultaneous effects resolve in attachment order, tie-broken by effect ID. Whether troll regeneration or poison resolves first on the same round is defined, not incidental.
- The engine also owns named conditions, including indefinite and permanent states: paralysed/helpless, asleep, blind, charmed, petrified (recoverable — stone is not dead), diseased, exhausted, lycanthropy incubation, and the averted-eyes stance against gaze attacks (−4 to hit, +2 to be hit by the gazer; mirror counterplay stays `manual`-tagged prose). Conditions expose combat hooks (a helpless target is hit automatically and takes rolled damage; a sleeping creature dies to a blade) rather than being mere labels.
- The engine also owns named conditions, including indefinite and permanent states: paralysed/helpless, asleep, blind, charmed, petrified (recoverable — stone is not dead), diseased, exhausted, lycanthropy incubation, and the averted-eyes stance against gaze attacks (−4 to hit, +2 to be hit by the gazer, and a carried mirror's −1 as the alternative stance, while a gazer petrified by its own reflection stays `manual`-tagged prose). Conditions expose combat hooks (a helpless target is hit automatically and takes rolled damage; a sleeping creature dies to a blade) rather than being mere labels.
- Effects attach to creatures, items, or locations: a burning oil pool or a *web* occupies cells and ticks on whatever enters or stands in them.
- Consumables deplete through the same mechanism: lighting a torch attaches a 6-turn effect, a lantern flask burns for 24 turns, and rations and water are consumed daily. B/X leaves starvation and thirst to the referee, so consumption is tracked by default while deprivation penalties are a documented adaptation behind a `Ruleset` option.
- The crawl layer advances the clock (movement, searching, and resting cost exploration turns; battles cost rounds). Kernel-only users drive the clock directly.
Expand Down
7 changes: 5 additions & 2 deletions src/osrlib/core/combat.py
Original file line number Diff line number Diff line change
Expand Up @@ -3635,8 +3635,11 @@ def resolve_gaze(
condition. A petrified
creature isn't dead, and the effect can be ended. A creature that fights with its eyes
averted takes a penalty to hit, which you apply through the `situational_modifier` of
its [`AttackContext`][osrlib.core.combat.AttackContext]. Using a mirror against the
gaze is left to the referee.
its [`AttackContext`][osrlib.core.combat.AttackContext], as you do the gazer's bonus
against it. This function doesn't model a mirror, so leave a creature that watches the
gazer in a mirror out of `engaged`. A battle in a
[`GameSession`][osrlib.crawl.session.GameSession] does all of this for you, from
[`BattleDeclaration.gaze_defense`][osrlib.crawl.commands.BattleDeclaration.gaze_defense].

Args:
gazer: The gazing [`Creature`][osrlib.core.creature.Creature], usually a
Expand Down
5 changes: 4 additions & 1 deletion src/osrlib/core/effects.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,10 @@ class Condition(StrEnum):

AVERTED_EYES = "averted_eyes"
"""Fighting with eyes turned away from a gaze attack. [`resolve_gaze`][osrlib.core.combat.resolve_gaze] skips
the creature. Pass the attack penalty for fighting blind yourself, in the attack context."""
the creature. Pass the attack penalty for fighting blind yourself, in the attack context. A battle in a
[`GameSession`][osrlib.crawl.session.GameSession] grants it for one round to a member who declares the
[`gaze_defense`][osrlib.crawl.commands.BattleDeclaration.gaze_defense] `avert_eyes`, and applies the
penalty and the gazer's bonus itself."""

POISONED = "poisoned"
"""Poisoned. The monsters that can't be poisoned, the undead and the cave locust among them, have it in
Expand Down
4 changes: 3 additions & 1 deletion src/osrlib/core/monsters.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,9 @@ class MonsterAbility(BaseModel):

The combat functions read four tags: `"regeneration"`, `"energy_drain"`, `"breath_weapon"`, and
`"uses_fire"`. Battle also reads `"paralysis"`, `"poison"`, `"disease"`, and `"petrification"`
when a monster's attack hits.
when a monster's attack hits. It reads `"averting_eyes"` for the bonus a gazer gains against a
creature that averts its eyes, and a `"surprise"` ability whose `params` have `meets_gaze` for a
gaze, like the basilisk's, that reaches every character the monster surprises.
"""

name: str = Field(min_length=1)
Expand Down
Loading
Loading