diff --git a/CHANGELOG.md b/CHANGELOG.md index 0756874..9fe1baf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,36 +8,105 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added -- [`SpawnNpcParty`][osrlib.crawl.commands.SpawnNpcParty] takes an exact band size in the new `count_fixed` field, so a referee can place a band of four adventurers (#167). Before, the size had to be a dice expression, and `"4"` isn't one. Set at most one of `count_fixed` and `count_dice`. With neither, the session rolls the B/X number for the band's kind as before. A saved command without the field still parses. -- A game can end an encounter peacefully with the referee command [`EndEncounter`][osrlib.crawl.commands.EndEncounter], when the party pays a toll, talks its way past, or walks away (#167). The command closes the encounter, and the battle if one is open. Set `outcome` to `"parley"` when the party talks or pays its way past, and to `"departed"` when the party walks away from monsters that let it go. The command moves no coins, so to charge a toll, issue a [`RemoveCoins`][osrlib.crawl.commands.RemoveCoins] beside it. The session logs the command, so a saved game and a replay close the encounter too. Before, the only way was to call [`end_encounter`][osrlib.crawl.encounter.end_encounter] directly, and a replay of that game stayed in the encounter. Set `resolve_area` to `True` to mark the keyed area the monsters came from as resolved, so they aren't there the next time the party walks in. An adventure can use the command as a trigger's consequence too. -- A game can take items and coins away with logged, replayable referee commands, so it can run a shop that buys back armour, charge a price or a toll, or use up a quest item (#173). [`RemoveItem`][osrlib.crawl.commands.RemoveItem] takes units of a mundane item, or one magic item or valuable by its instance id. An equipped item comes off first, and any effect it attached ends. A revealed curse doesn't stop the removal. [`RemoveCoins`][osrlib.crawl.commands.RemoveCoins] takes exactly the denominations the command lists and makes no change from other coins. Both are legal in every mode, cost no draws and no time, and report an [`ItemsRemovedEvent`][osrlib.crawl.events.ItemsRemovedEvent] with the code `session.items.removed`. When the member in `character_id` doesn't have the item or the coins, the session refuses the command with `items.remove.not_carried` or `items.coins.insufficient`. -- A game can add a character to a running party with the referee command [`AddMember`][osrlib.crawl.commands.AddMember], so it can bring in a replacement for a character who died or an adventurer the party meets (#172). Build the character as you build a starting member and send it in the command. The command is legal in `town` and `exploring`. In every other mode the session refuses it with `session.command.wrong_mode`. The session adds a copy of the character at the back of the marching order and gives it the next `character-NNNN` id. It reports the new member in a player-visible [`MemberAddedEvent`][osrlib.crawl.events.MemberAddedEvent] with the code `session.member.added`. Read the id from the event and use it in later commands. The character must be new to this game. The session refuses a character who already has an id with `party.add.id_assigned`, one who has a condition or a stat modifier with `party.add.under_effects`, and one who carries a magic item or a valuable with `party.add.carries_instances`. The new member keeps their mundane gear and coins. Coins a new member brings into a dungeon earn no experience. A member who joins during a delve gets an equal share of the experience the party earns on that delve, including what it earned before they joined. The command makes no draws and takes no time, and the session logs it, so a saved game and a replay have the new member too. -- [`GrantItem`][osrlib.crawl.commands.GrantItem] can grant a magic item by its catalog id, so a game can sell potions or hand out a magic reward (#173). Each copy arrives unidentified, with a separate instance id in the [`ItemAcquiredEvent`][osrlib.crawl.events.ItemAcquiredEvent]. The session rolls whatever the item's entry in the OSE SRD leaves to chance, like a wand's charges, on the treasure stream, as it does for a magic item found in a cache. A mundane grant is unchanged. -- An adventure document can use `RemoveItem`, `RemoveCoins`, and a `GrantItem` of a magic item as trigger consequences and quest rewards (#173). A `RemoveItem` can name its member with [`CARRIER_SELECTOR`][osrlib.crawl.triggers.CARRIER_SELECTOR] (`"@carrier"`), which the interpreter replaces with the first living member in marching order who carries the item. When nobody does, the interpreter drops the command with a note. A quest reward written this way uses up the quest item when the quest completes. [`validate_adventure`][osrlib.crawl.adventure.validate_adventure] checks an authored `GrantItem`'s or `RemoveItem`'s item id against the equipment and magic item catalogs, and accepts `"@carrier"` only on a `RemoveItem`. -- [`Rest`][osrlib.crawl.commands.Rest] has a new field `hidden`, and [`GameSession.advance_turns`][osrlib.crawl.session.GameSession.advance_turns] takes a matching `hidden` parameter, so a game can let the party hide while it rests. The OSE SRD lowers the chance of wandering monsters when the party rests quietly in an out-of-the-way place, and a game sets `hidden` to say the party does. Each wandering-monster check during a hidden rest takes 1 off the chance in 6, after noise and bright light add theirs. On a level with the usual 1 in 6 chance, the session rolls no check during a hidden rest. The field defaults to `False`, so a saved command without it still parses as a plain rest. In town no checks run, so `hidden` changes nothing there. -- [`BattleDeclaration`][osrlib.crawl.commands.BattleDeclaration] has a new field [`replacement_id`][osrlib.crawl.commands.BattleDeclaration.replacement_id], so a member can make a fighting withdrawal or a retreat alone (#171). A member who withdraws or retreats alone must set it. It names the back-rank member who steps up into the front rank in the withdrawing member's place. The new event [`MemberWithdrewEvent`][osrlib.crawl.events.MemberWithdrewEvent] reports each swap, with the code `battle.member.withdrew` for a fighting withdrawal and `battle.member.retreated` for a retreat. Its `character_id` is the member who stepped back and its `replacement_id` is the member who stepped up, so a battle screen can redraw the ranks from it. The field defaults to `None`, so a saved command without it still parses and [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4. -- Each attack routine has a new field, [`AttackRoutine.use`][osrlib.core.monsters.AttackRoutine.use], that gives when the monster uses the routine. Its value is a [`RoutineUse`][osrlib.core.monsters.RoutineUse]. The first routine is `PRIMARY`. A later routine is `ALTERNATIVE` when the OSE SRD lets the monster use it in place of the first in any round, like a thoul's weapon. It's `SITUATIONAL` when the SRD ties it to a situation osrlib doesn't model, like an elephant's trample or a giant's thrown boulder. It's `SPECIAL` when it's a breath, a gaze, or a spell. Every routine in the built-in monster and spell catalogs has its label. A routine with no label is `PRIMARY`, so in a monster template of your own, label each later routine, or the default policy picks it as often as the first. -- A policy of your own can choose which attack routine a monster uses, in the new field [`MonsterAction.routine`][osrlib.crawl.battle.MonsterAction.routine]. The field is an index into the template's `attacks`, and `None` uses the first routine. So your policy can have an elephant trample when your game decides it charges. Battle rolls the routine's attacks as melee attacks, and rolls nothing for a `SPECIAL` routine. An index the template doesn't have makes the action impossible, so the session skips it and the monster doesn't attack that turn. -- [`BattleDeclaration`][osrlib.crawl.commands.BattleDeclaration] has a new field [`gaze_defense`][osrlib.crawl.commands.BattleDeclaration.gaze_defense], so a member can guard against a basilisk's or a medusa's petrifying gaze. Set it on a member's declaration, alongside whatever action they declare, to `"avert_eyes"` or `"mirror"`. The defense lasts only the round it's declared in, and it protects the member whether the monsters or the party act first. A member who averts their eyes doesn't meet the gaze, but every attack roll they make that round takes a −4 penalty, and a basilisk or a medusa gains +2 to attack them. A member who fights by a mirror doesn't meet the gaze either, and every attack roll they make that round takes a −1 penalty. No monster gains a bonus against them. The member has to carry a `mirror`, or the session refuses the whole `ResolveBattleRound` command with the new code `battle.declaration.no_mirror`. The penalties come from the basilisk's and the medusa's entries in the OSE SRD. A member who averts their eyes has the `averted_eyes` condition until the round ends. The defense makes no draw and emits no event. The basilisk's and the medusa's `averting_eyes` abilities are no longer `manual`. The field defaults to `None`, so a saved command without it still parses and [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4. -- [`DeclarationFizzledEvent`][osrlib.core.events.DeclarationFizzledEvent] reports a wand, staff, or rod use or a turning that fizzled in a battle round (#145). Its code is `magic.device.fizzled` for a device and `magic.turning.fizzled` for a turning. `actor_id` is the member who declared it, `item_id` is the device's instance id (`None` for a turning), and `reason` is the rejection code of the check that failed. Tell the player the action failed. A device that fizzles keeps its charge. +- [`SpawnNpcParty`][osrlib.crawl.commands.SpawnNpcParty] has a new `count_fixed` field for an exact band size, like a band of four adventurers (#167). Before, the size had to be a dice expression, and `"4"` isn't one. Set at most one of `count_fixed` and `count_dice`. With neither, the session rolls the B/X number for the band's kind, as before. A saved command without the field still parses. +- The new referee command [`EndEncounter`][osrlib.crawl.commands.EndEncounter] ends an encounter peacefully, when the party pays a toll, talks its way past, or walks away (#167). A referee command is one your game issues itself, not on a player's behalf. `EndEncounter` closes the encounter, and the battle if one is open. The session logs the command, so a saved game and a replay close the encounter too. Before, the only way was to call [`end_encounter`][osrlib.crawl.encounter.end_encounter] directly, and a replay of that game stayed in the encounter. + - Set `outcome` to `"parley"` when the party talks or pays its way past, or to `"departed"` when the party walks away from monsters that let it go. + - Set `resolve_area` to `True` to mark the keyed area the monsters came from as resolved, so they aren't there the next time the party walks in. A keyed area is a room or cave that an [`AreaSpec`][osrlib.crawl.dungeon.AreaSpec] defines, with its monsters and treasure. + - The command moves no coins. To charge a toll, issue a [`RemoveCoins`][osrlib.crawl.commands.RemoveCoins] beside it. + - An adventure can use the command as a [trigger's consequence](https://mmacy.github.io/osrlib-python/guides/gates-triggers-quests/#scripting-the-dungeon-with-triggers) too. +- Two new referee commands take items and coins away from a member (#173). Use them to run a shop that buys back armour, charge a price or a toll, or use up a quest item. The session logs them, so a replay repeats them. + - [`RemoveItem`][osrlib.crawl.commands.RemoveItem] takes units of a mundane item, or one magic item or valuable (a gem or a piece of jewellery) by its instance id. An equipped item comes off first, and any effect it attached ends. A revealed curse doesn't stop the removal. + - [`RemoveCoins`][osrlib.crawl.commands.RemoveCoins] takes exactly the denominations the command lists, and makes no change from other coins. + - Both are legal in every mode, cost no draws and no time, and report an [`ItemsRemovedEvent`][osrlib.crawl.events.ItemsRemovedEvent] with the code `session.items.removed`. A draw is one random number the session takes from one of its named [random streams](https://mmacy.github.io/osrlib-python/reference/rng-streams/). + - When the member in `character_id` doesn't have the item or the coins, the session refuses the command with `items.remove.not_carried` or `items.coins.insufficient`. +- The new referee command [`AddMember`][osrlib.crawl.commands.AddMember] adds a character to a running party, like a replacement for a character who died or an adventurer the party meets (#172). Build the character as you build a starting member, and send it in the command. Then read the new member's id from the event the session reports, and use it in later commands. + - The command is legal in `town` and `exploring`. In every other mode, the session refuses it with `session.command.wrong_mode`. + - The session adds a copy of the character at the back of the marching order and gives it the next `character-NNNN` id. It reports the new member in a player-visible [`MemberAddedEvent`][osrlib.crawl.events.MemberAddedEvent] with the code `session.member.added`. + - The character must be new to this game. The session refuses a character who already has an id with `party.add.id_assigned`, one who has a condition or a stat modifier with `party.add.under_effects`, and one who carries a magic item or a valuable with `party.add.carries_instances`. + - The new member keeps their mundane gear and coins. The coins a new member brings into a dungeon earn the party no experience. + - The party's experience for a delve, one trip from town into a dungeon and back, is shared evenly among the members who survive it. So a member who joins during a delve gets an equal share of everything the party earns on it, including what it earned before they joined. + - The command makes no draws and takes no time. The session logs it, so a saved game and a replay have the new member too. +- [`GrantItem`][osrlib.crawl.commands.GrantItem] can grant a magic item by its catalog id, so a game can sell potions or hand out a magic reward (#173). Each copy arrives unidentified, with a separate instance id in the [`ItemAcquiredEvent`][osrlib.crawl.events.ItemAcquiredEvent]. The session rolls whatever the item's entry in the OSE SRD leaves to chance, like a wand's charges, on the treasure stream ([`TREASURE_STREAM`][osrlib.core.treasure.TREASURE_STREAM]), as it does for a magic item found in a cache. A mundane grant works as before. +- An adventure document can use `RemoveItem`, `RemoveCoins`, and a `GrantItem` of a magic item as trigger consequences and quest rewards (#173). + - A `RemoveItem` can name its member with [`CARRIER_SELECTOR`][osrlib.crawl.triggers.CARRIER_SELECTOR] (`"@carrier"`). The [interpreter](https://mmacy.github.io/osrlib-python/guides/listeners-and-flags/#the-interpreter) replaces it with the first living member in marching order who carries the item. When nobody does, the interpreter drops the command with a note. A quest reward written this way uses up the quest item when the quest completes. + - [`validate_adventure`][osrlib.crawl.adventure.validate_adventure] checks the item id of an authored `GrantItem` or `RemoveItem` against the equipment and magic item catalogs. It accepts `"@carrier"` only on a `RemoveItem`. +- The [`Rest`][osrlib.crawl.commands.Rest] command has a new `hidden` field, and [`GameSession.advance_turns`][osrlib.crawl.session.GameSession.advance_turns] has a matching `hidden` parameter. Set `hidden` when the party rests quietly in an out-of-the-way place, which the OSE SRD says lowers the chance of wandering monsters. Each wandering-monster check during a hidden rest lowers the chance by 1, for example from 2 in 6 to 1 in 6, after noise and bright light raise it. On a level with the usual 1 in 6 chance, the session rolls no check during a hidden rest. In town no checks run, so `hidden` changes nothing there. The field defaults to `False`, so a saved command without it still parses as a plain rest. This also changes a plain rest: it no longer gets the lower chance. If your game wants the odds a rest had in 2.0.1, pass `hidden=True`. The Changed entry "A plain `Rest` in a dungeon is now riskier" gives the details. +- [`BattleDeclaration`][osrlib.crawl.commands.BattleDeclaration] has a new field [`replacement_id`][osrlib.crawl.commands.BattleDeclaration.replacement_id], so a member can make a fighting withdrawal or a retreat alone (#171). To withdraw one member, send a `BattleDeclaration` for that member with `action="move"`, `move="fighting_withdrawal"` or `move="retreat"`, and `replacement_id` set to the back-rank member who steps up into the front rank in their place. The Changed entry on withdrawing alone gives the rules for the swap. + - The new event [`MemberWithdrewEvent`][osrlib.crawl.events.MemberWithdrewEvent] reports each swap, with the code `battle.member.withdrew` for a fighting withdrawal and `battle.member.retreated` for a retreat. Its `character_id` is the member who stepped back and its `replacement_id` is the member who stepped up, so a battle screen can redraw the ranks from it. + - The field defaults to `None`, so a saved command without it still parses and [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4. +- Each attack routine has a new field, [`AttackRoutine.use`][osrlib.core.monsters.AttackRoutine.use], that labels when the monster uses the routine. If you define monster templates of your own, label every routine after the first. An unlabelled routine is `PRIMARY` by default. The default policy, [`ScriptedPolicy`][osrlib.crawl.battle.ScriptedPolicy], picks with equal chance among a monster's `PRIMARY` and `ALTERNATIVE` routines, and never picks a `SITUATIONAL` or `SPECIAL` one. So it picks an unlabelled routine as often as the first, even when it's a breath, a gaze, or a spell. A policy of your own can name any routine. A policy is an [`ActionPolicy`][osrlib.crawl.battle.ActionPolicy], the object that chooses what a monster group does each round. The session uses the default policy for any group that has no policy of yours in [`GameSession.action_policies`][osrlib.crawl.session.GameSession.action_policies]. Every routine in the built-in monster and spell catalogs has its label. The value is a [`RoutineUse`][osrlib.core.monsters.RoutineUse]: + - `PRIMARY` for the first routine. + - `ALTERNATIVE` when the OSE SRD lets the monster use the routine in place of the first in any round, like a thoul's weapon. + - `SITUATIONAL` when the SRD ties the routine to a situation osrlib doesn't model, like an elephant's trample or a giant's thrown boulder. + - `SPECIAL` for a breath, a gaze, or a spell. +- A policy of your own can choose which attack routine a monster uses, with the new field [`MonsterAction.routine`][osrlib.crawl.battle.MonsterAction.routine]. The field is an index into the template's `attacks`, and `None` uses the first routine. For example, your policy can have an elephant trample when your game decides it charges. Battle rolls the routine's attacks as melee attacks. A `SPECIAL` routine does nothing that turn, because battle rolls nothing for it. If the template has no routine at that index, the session skips the action without an error or an event, so the monster doesn't attack that turn. +- [`BattleDeclaration`][osrlib.crawl.commands.BattleDeclaration] has a new field [`gaze_defense`][osrlib.crawl.commands.BattleDeclaration.gaze_defense], so a member can guard against a basilisk's or a medusa's petrifying gaze. Set it to `"avert_eyes"` or `"mirror"`, alongside whatever action the member declares. The defense lasts only the round it's declared in, and it protects the member whether the monsters or the party act first. The penalties come from the basilisk's and the medusa's entries in the OSE SRD. + - A member who averts their eyes doesn't meet the gaze. Every attack roll they make that round takes a −4 penalty, and a basilisk or a medusa gains +2 to attack them. The member has the `averted_eyes` condition until the round ends. + - A member who fights by a mirror doesn't meet the gaze either. Every attack roll they make that round takes a −1 penalty, and no monster gains a bonus against them. The member has to carry a `mirror`, or the session refuses the whole `ResolveBattleRound` command with the new code `battle.declaration.no_mirror`. + - The defense makes no draw and emits no event. + - On the monster side, the basilisk's and the medusa's `averting_eyes` abilities, which give those penalties, are no longer `manual`. A [`MonsterAbility`][osrlib.core.monsters.MonsterAbility] marked `manual` is text for a referee to act on, and osrlib doesn't apply it. + - The field defaults to `None`, so a saved command without it still parses and [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4. +- The new event [`DeclarationFizzledEvent`][osrlib.core.events.DeclarationFizzledEvent] reports a device use or a turning that fizzled in a battle round (#145). A device is a wand, staff, or rod, and a turning is a cleric's `turn_undead`. Tell the player the action failed. Its code is `magic.device.fizzled` for a device and `magic.turning.fizzled` for a turning. `actor_id` is the member who declared it, `item_id` is the device's instance id (`None` for a turning), and `reason` is the rejection code of the check that failed. A device that fizzles doesn't use up a charge. ### Changed -- [`SpawnMonsters`][osrlib.crawl.commands.SpawnMonsters] works during an encounter or a battle, so a game can resolve the allies a monster summons mid-fight, like a lycanthrope's animals or a nixie's fish (#170). The command adds one new group to the open encounter at `distance_feet`. Before, the session refused it with `session.command.encounter_in_progress`. The group rolls no surprise or reaction, and it shares the encounter's stance. It fights on the monster side from the next round. The spawn takes no time and runs no round. The players learn of the group from a new event, [`EncounterGroupJoinedEvent`][osrlib.crawl.events.EncounterGroupJoinedEvent] (`encounter.group.joined`). The event gives the group's id, label, size, and distance. A monster that regenerates, like a troll, starts to regenerate as it joins. A chase runs at the pace of its slowest group, so during a pursuit the session refuses the spawn with the new code `encounter.spawn.mid_pursuit`, as it refuses a parley or a turning. The session logs the spawn like any other command, so loading a save or replaying the log reproduces it. When a trigger's `SpawnMonsters` consequence fires during an encounter, its monsters now join that encounter. Before, the interpreter dropped the consequence with a note. In exploration, the command opens an encounter as before. The session still refuses `SpawnNpcParty` and `PlaceParty` during an encounter. -- Turning the undead ends a cleric's concentration (#167). A cleric who declares `turn_undead` in [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound] stops maintaining a concentration spell, as with any action other than `cast` or `hold`. Before, a turning that confusion stopped left the spell running. -- A caster maintains one concentration spell at a time (#167). Casting a concentration spell in battle ends the one the caster already maintains. Before, both kept running. A spell that needs no concentration still leaves the maintained one alone. -- *Cure light wounds* cast to cure paralysis removes the `paralysed` condition however the target got it (#166). Before, it released only ledger effects, so a target that a game paralysed with [`grant_condition`][osrlib.core.effects.grant_condition] stayed paralysed and the cast reported no effect. The spell still removes no other condition. -- The temple refuses a healing service that would do nothing, and takes no payment for it (#167). [`PurchaseHealing`][osrlib.crawl.commands.PurchaseHealing] for `raise_dead` on a living member, `cure_disease` on a member who isn't diseased, or `neutralize_poison` on a member who isn't poisoned is rejected with the new code `town.healing.no_effect`. Before, the party paid 1,500 gp to raise a living member and the spell did nothing. A party that can't pay the fee still gets `items.purchase.insufficient_funds` first. -- A chute trap drops the party only when at least one member fails the save (#167). When every save passes, the party stays on the trapped cell. Before, a [`TrapEffect`][osrlib.crawl.dungeon.TrapEffect] with a `transition` moved the party whatever the saves rolled. A chute with no save still always drops the party, and the session rolls the saves as before. -- A melee attack on a petrified creature hits automatically, as on a paralysed or sleeping one (#166). [`attack_roll`][osrlib.core.combat.attack_roll] reports `combat.attack.auto_hit` and draws no d20, so only the damage is rolled. A missile attack on a petrified creature is still rolled. -- [`UseItem`][osrlib.crawl.commands.UseItem] with a potion and a `target_id` that names someone other than the member using it is now refused with `items.use.target_not_allowed`. Before, the session accepted the command and the member in `character_id` drank the potion, so a front end that expected the target to drink got no warning. A `target_id` of `None`, or the id of the member using the potion, drinks the potion as before, and a healing device still takes another member (#174). -- A plain [`Rest`][osrlib.crawl.commands.Rest] in a dungeon now checks for wandering monsters at the level's full chance. Before, every rest took 1 off the chance, so a level with the usual 1 in 6 chance never rolled during a rest and resting was always safe. The session still adds 1 for noise and 1 for bright light, and a rest of a night or a day still checks once each turn and stops when monsters come. To rest quietly and take the 1 off, set the new `hidden` field. The `resting` keyword of [`wandering_check`][osrlib.crawl.exploration.wandering_check] still takes 1 off the chance. [`GameSession.advance_turns`][osrlib.crawl.session.GameSession.advance_turns] now sets it from `hidden`, so `advance_turns(resting=True)` alone no longer lowers the chance. It still counts the turns as rest. -- A party member can make a fighting withdrawal or a retreat alone, to pull a badly hurt character out of the front rank without the whole party leaving the fight (#171). Before, [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound] refused a round in which only some of the members it expects to declare chose one of these moves, with `battle.declaration.formation_split`. That code is gone, so a game that handled it should handle the three new codes instead: `battle.declaration.no_replacement`, `battle.declaration.replacement_unavailable`, and `battle.declaration.replacement_too_far`. A defensive move that every declarer makes still moves the whole party, as before. Any other fighting withdrawal or retreat is that member's alone. The member has to stand in the front rank, and steps back to trade places in the marching order with the member named in `replacement_id`. The party doesn't move and the battle goes on. The new order stays after the battle, until a [`ReorderParty`][osrlib.crawl.commands.ReorderParty] sets it again, and the session still refuses `ReorderParty` during a battle. Each rank stands 5 feet behind the one in front of it, and both members have to cover that distance. The withdrawing member covers it at up to half their encounter movement rate for a fighting withdrawal, or their full rate for a retreat. The replacement covers it at up to their full rate. *Haste* doubles either limit for the member who has it. The session refuses a member behind the front rank with `battle.declaration.not_in_front_rank`. It refuses a member who names no replacement, a replacement who can't step up, and a replacement too far back with three new codes: `battle.declaration.no_replacement`, `battle.declaration.replacement_unavailable`, and `battle.declaration.replacement_too_far`. The swap happens in the party's movement step, so monsters that win initiative strike at the ranks as they were declared. From the swap on, melee no longer reaches the member who stepped back. The replacement may declare a melee attack in the round they step up, and it resolves only if the swap happens. A member who retreats alone is easier to hit for the whole round: attackers get +2 against them and ignore their shield. A refused round costs no draw and no time, and a swap makes no draw. -- An `attack` in a [`BattleDeclaration`][osrlib.crawl.commands.BattleDeclaration] with no `weapon_id` now uses the wielded weapon that suits the attack (#162). Before, the member struck unarmed, even with a sword in hand. The group's distance when the session accepts the round decides the attack: melee at 5 feet or less, missile beyond. The session takes the first wielded weapon, in the order the member equipped them, that can make that attack. For a missile attack it prefers the first whose long range reaches the group. A real weapon comes before an improvised one, like a torch, so a member who lit a torch and then drew a sword swings the sword. The torch still counts when the member wields nothing else that suits the attack. The session passes over a splash weapon, like a flask of oil, and an item with no weapon stats, like a wand. A member with no suitable weapon still strikes unarmed. The round plays out as if you had named the chosen weapon, and the command log records the declaration as you sent it. The attack events name the weapon the member used. [`BattleDeclaration.weapon_id`][osrlib.crawl.commands.BattleDeclaration.weapon_id] describes the choice. A declaration that names a weapon works as before. -- A wand, staff, or rod use and a turning declared in [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound] are checked again in the magic phase, just before they resolve, as a spell cast and a scroll read are (#145). Before, they resolved even when the round had changed something they depend on. For example, a staff of striking struck a group that had already run out of melee reach. Now a declaration that fails the second check fizzles, and the session reports it with a [`DeclarationFizzledEvent`][osrlib.core.events.DeclarationFizzledEvent]. A device that fizzles was never used, so it keeps its charge, the session doesn't identify it, and the member who declared it stays invisible if they were. A turning that fizzles rolls nothing. A device use or turning that passes the second check resolves with the same rolls as before. -- A party turned wholly to stone ends the game, as a party that has died does. A member stands while they are neither dead nor petrified. When a command kills or petrifies a member and no member is left standing, the session moves to `game_over` and reports a [`GameOverEvent`][osrlib.crawl.events.GameOverEvent] last. Before, a statue counted as alive, so a party that a basilisk's gaze turned to stone stayed in the encounter or in a battle with nobody able to act. In a battle, `battle.ended.defeat` comes just before the ending. When a basilisk's surprise gaze turns the whole party to stone as an encounter opens, the monsters get no round against the statues. A statue still counts among [`Party.living_members`][osrlib.crawl.party.Party.living_members] and keeps its rank. The temple can't cure stone, so the way back is a magic-user who casts *stone to flesh* in town. Place the party in town with [`PlaceParty`][osrlib.crawl.commands.PlaceParty], which doesn't end the game again. Then add the magic-user with [`AddMember`][osrlib.crawl.commands.AddMember] and cast the spell with [`CastSpell`][osrlib.crawl.commands.CastSpell]. The ending makes no draws. -- Some command logs recorded under 2.0.1 replay differently under this release. Loading a 2.0.1 save with [`load_game`][osrlib.persistence.load_game] isn't affected, because a load restores the saved state and runs no commands. The loaded game then plays on under this release's rules. Replaying a 2.0.1 log with [`replay_game`][osrlib.persistence.replay_game] can differ in two ways. Each change named below has an entry in this section or under Fixed. - - The replay raises [`ContentValidationError`][osrlib.errors.ContentValidationError] when the log contains a command this release refuses. Three commands that 2.0.1 accepted are refused now: a potion `UseItem` aimed at another member, a temple service that would do nothing, and a battle round in which a member behind the front rank strikes with a staff of striking. +- [`SpawnMonsters`][osrlib.crawl.commands.SpawnMonsters] now works during an encounter or a battle, so a game can add the allies a monster summons mid-fight, like a lycanthrope's animals or a nixie's fish (#170). Before, the session refused it with `session.command.encounter_in_progress`. To show the new group, handle the new event [`EncounterGroupJoinedEvent`][osrlib.crawl.events.EncounterGroupJoinedEvent] (`encounter.group.joined`), which gives the group's id, label, size, and distance. Also handle the new refusal code `encounter.spawn.mid_pursuit`, described below. + - The command adds one new group to the open encounter at `distance_feet`. The group rolls no surprise or reaction, and it shares the encounter's stance, the monsters' attitude toward the party that the reaction roll set. It fights on the monster side from the next round. + - The spawn takes no time and runs no round. + - A monster that regenerates, like a troll, starts to regenerate as it joins. + - During a pursuit, the session refuses the spawn with `encounter.spawn.mid_pursuit`, as it refuses a parley or a turning, because a chase runs at the pace of its slowest group. + - The session logs the spawn like any other command, so loading a save or replaying the log reproduces it. + - When a trigger's `SpawnMonsters` consequence fires during an encounter, its monsters now join that encounter. Before, the interpreter dropped the consequence with a note. + - In exploration, the command opens an encounter as before. The session still refuses `SpawnNpcParty` and `PlaceParty` during an encounter. +- Turning the undead now ends a cleric's concentration (#167). A concentration spell is one whose effect lasts only while the caster concentrates on it. A cleric who declares `turn_undead` in [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound] stops maintaining a concentration spell, as with any action other than `cast` or `hold`. Before, a turning that confusion stopped left the spell running. +- A caster now maintains one concentration spell at a time (#167). Casting a concentration spell in battle ends the one the caster already maintains. Before, both kept running. A spell that needs no concentration still leaves the maintained one alone. +- *Cure light wounds* cast to cure paralysis now removes the `paralysed` condition however the target got it (#166). Before, it released only effects attached through the [`EffectsLedger`][osrlib.core.effects.EffectsLedger]. So a target that a game paralysed with [`grant_condition`][osrlib.core.effects.grant_condition] stayed paralysed, and the cast reported no effect. The spell still removes no other condition. +- The temple now refuses a healing service that would do nothing, and takes no payment for it (#167). The session rejects [`PurchaseHealing`][osrlib.crawl.commands.PurchaseHealing] with the new code `town.healing.no_effect` for `raise_dead` on a living member, `cure_disease` on a member who isn't diseased, or `neutralize_poison` on a member who isn't poisoned. Before, the party paid 1,500 gp to raise a living member and the spell did nothing. A party that can't pay the fee still gets `items.purchase.insufficient_funds` first. +- A chute trap now drops the party only when at least one member fails the save (#167). A chute is a trap whose [`TrapEffect`][osrlib.crawl.dungeon.TrapEffect] has a `transition`, which slides the party to another cell (one square of a level's grid) or to another level. When every save passes, the party stays on the trapped cell. Before, a chute moved the party whatever the saves rolled. A chute with no save still always drops the party, and the session rolls the saves as before. +- A melee attack on a petrified creature now hits automatically, as on a paralysed or sleeping one (#166). [`attack_roll`][osrlib.core.combat.attack_roll] reports `combat.attack.auto_hit` and draws no d20, so only the damage is rolled. A missile attack on a petrified creature is still rolled. +- [`UseItem`][osrlib.crawl.commands.UseItem] now refuses a potion whose `target_id` names someone other than the member using it, with `items.use.target_not_allowed` (#174). Before, the session accepted the command and the member in `character_id` drank the potion, so a front end that expected the target to drink got no warning. A `target_id` of `None`, or the id of the member using the potion, drinks the potion as before. A healing device still takes another member as its target. +- A plain [`Rest`][osrlib.crawl.commands.Rest] in a dungeon is now riskier: it checks for wandering monsters at the level's full chance. If your game wants the lower chance that every rest had in 2.0.1, set `hidden=True` on the `Rest` command, and pass `hidden=True` to `advance_turns` if you call it yourself. Before, every rest lowered the chance by 1, so a level with the usual 1 in 6 chance never rolled during a rest, and resting was always safe. + - The session still raises the chance by 1 for noise and by 1 for bright light. + - A rest of a night or a day, `Rest(kind="night")` or `Rest(kind="day")`, still checks on the level's usual schedule, every two turns unless the level sets another interval, and stops when monsters come. + - [`GameSession.advance_turns`][osrlib.crawl.session.GameSession.advance_turns] lowers the chance only when you pass `hidden=True`. `advance_turns(resting=True)` alone no longer lowers it, though it still counts the turns as rest. + - The `resting` keyword of [`wandering_check`][osrlib.crawl.exploration.wandering_check] still lowers the chance by 1. `advance_turns` now sets it from `hidden`. +- A party member can now make a fighting withdrawal or a retreat alone, to pull a badly hurt character out of the front rank without the whole party leaving the fight (#171). The code `battle.declaration.formation_split` is gone. If your game handled it, handle the three new refusal codes instead: `battle.declaration.no_replacement`, `battle.declaration.replacement_unavailable`, and `battle.declaration.replacement_too_far`. The first item below says when the session uses each. Before, [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound] used it to refuse a round in which only some of the members it expects to declare, the [`EncounterView.declarers`][osrlib.crawl.views.EncounterView.declarers], chose one of these moves. + - The session refuses a member's move alone with one of these codes: + - `battle.declaration.not_in_front_rank` when the member stands behind the front rank. + - `battle.declaration.no_replacement` when the declaration names no `replacement_id`. + - `battle.declaration.replacement_unavailable` when the named member can't step up. + - `battle.declaration.replacement_too_far` when either member can't cover the distance between their ranks. + - When every declarer makes the same fighting withdrawal or retreat, the whole party makes it, as before. Any other fighting withdrawal or retreat is that member's alone. + - The member has to stand in the front rank. They step back and trade places in the marching order with the member named in [`BattleDeclaration.replacement_id`][osrlib.crawl.commands.BattleDeclaration.replacement_id]. The party doesn't move and the battle goes on. + - Each rank stands 5 feet behind the one in front of it, and both members have to cover that distance. The withdrawing member covers it at up to half their encounter movement rate for a fighting withdrawal, or their full rate for a retreat. The replacement covers it at up to their full rate. *Haste* doubles either limit for the member who has it. + - The swap happens in the party's movement step, so monsters that win initiative strike at the ranks as they were declared. From the swap on, melee no longer reaches the member who stepped back. + - The replacement may declare a melee attack in the round they step up, and it resolves only if the swap happens. + - A member who retreats alone is easier to hit for the whole round: attackers get +2 against them and ignore the retreating member's shield. + - The new order stays after the battle, until a [`ReorderParty`][osrlib.crawl.commands.ReorderParty] sets it again. The session still refuses `ReorderParty` during a battle. + - A refused round costs no draw and no time, and a swap makes no draw. +- An `attack` in a [`BattleDeclaration`][osrlib.crawl.commands.BattleDeclaration] with no `weapon_id` now uses the wielded weapon that suits the attack (#162). Before, the member struck unarmed, even with a sword in hand. A declaration that names a weapon works as before. No declaration asks for an unarmed strike, so a member who wields a suitable weapon can't strike unarmed. [`BattleDeclaration.weapon_id`][osrlib.crawl.commands.BattleDeclaration.weapon_id] describes the choice. + - The distance to the target group when the session accepts the round decides the attack: melee at 5 feet or less, missile beyond. + - The session takes the first wielded weapon that can make that attack, in the order the member equipped them. For a missile attack, it prefers the first whose long range reaches the group. + - A real weapon comes before an improvised one, like a torch, so a member who lit a torch and then drew a sword swings the sword. The torch still counts when the member wields nothing else that suits the attack. + - The session passes over a splash weapon, like a flask of oil, and an item with no weapon stats, like a wand. + - A member with no suitable weapon still strikes unarmed. + - The round plays out as if you had named the chosen weapon, and the attack events name the weapon the member used. The command log records the declaration as you sent it. +- A device use or a turning declared in [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound] is now checked again in the magic phase, just before it resolves, as a spell cast and a scroll read are (#145). The magic phase is the step of a battle round in which spells, devices, and turnings resolve. A declaration that fails the second check fizzles, and the session reports it with a [`DeclarationFizzledEvent`][osrlib.core.events.DeclarationFizzledEvent], so handle that event. Before, these declarations resolved even when the round had changed something they depend on. For example, a staff of striking struck a group that had already run out of melee reach. + - A device that fizzles was never used. It doesn't use up a charge, the session doesn't identify it, and the member who declared it stays invisible if they were. + - A turning that fizzles rolls nothing. + - A device use or a turning that passes the second check resolves with the same rolls as before. +- A party turned wholly to stone now ends the game, as a party that has died does. Handle it as you handle a dead party: the session moves to `game_over` and reports a [`GameOverEvent`][osrlib.crawl.events.GameOverEvent]. Before, a statue counted as alive, so a party that a basilisk's gaze turned to stone stayed in the encounter or in a battle with nobody able to act. + - A member stands while they are neither dead nor petrified. When a command kills or petrifies a member and no member is left standing, the session ends the game, and the `GameOverEvent` comes last. The ending makes no draws. + - In a battle, `battle.ended.defeat` comes just before the ending. + - When a basilisk's surprise gaze turns the whole party to stone as an encounter opens, the monsters get no round against the statues. + - A statue still counts among [`Party.living_members`][osrlib.crawl.party.Party.living_members] and keeps its rank, because a member turned to stone isn't dead. Only the check that ends the game treats a statue as fallen. + - Your game can stop at the ending, or offer a way back. The temple can't cure stone, so the way back is a magic-user who casts *stone to flesh* in town. [`PlaceParty`][osrlib.crawl.commands.PlaceParty] is legal in `game_over`, so first place the party in town with it, which doesn't end the game again. Then add the magic-user with [`AddMember`][osrlib.crawl.commands.AddMember] and cast the spell with [`CastSpell`][osrlib.crawl.commands.CastSpell]. +- Some command logs recorded under 2.0.1 replay differently under this release. To continue a 2.0.1 game, load its save with [`load_game`][osrlib.persistence.load_game]. A load restores the saved state and runs no commands, so this change doesn't affect it. The loaded game then plays on under this release's rules. To reproduce a 2.0.1 game exactly from its log, install `osrlib==2.0.1` in a separate environment and replay the log there. To catch a log from another version before it replays, pass the save's `engine_version` to [`replay_game`][osrlib.persistence.replay_game] as `recorded_engine_version`. The call then raises [`ReplayVersionError`][osrlib.errors.ReplayVersionError] when the versions differ. Replaying a 2.0.1 log under this release can differ in two ways. Each case below matches an entry in the Changed or Fixed section of these notes. + - The replay raises [`ContentValidationError`][osrlib.errors.ContentValidationError] when the log contains a command this release refuses. These commands that 2.0.1 accepted are refused now: + - A potion `UseItem` aimed at another member. + - A temple service that would do nothing. + - A battle round in which a member behind the front rank strikes with a staff of striking. + - A battle round that declares a `close` move (an order to move toward a group) or an area spell at a group that has fled, that is, run out of the battle. - The replay runs without an error but plays out differently when the log contains any of these: - A lair the party left and came back to. - A rest in a dungeon, or a rest that a wandering monster interrupted. @@ -61,38 +130,102 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Fixed -- A retreat by the whole party now makes its members easier to hit for the whole round, as the OSE SRD's retreat says: "This round: ... the opponent gains a +2 bonus to all attacks against the retreating combatant and ignores any AC bonus due to the combatant's shield". Before, the penalty started only when the party moved off, so monsters that won initiative and attacked first got no bonus and counted the shield. A confused monster's attack never got it. Now every attack the monster side makes on the party in that round gets +2 and ignores the shield, whichever side acts first. -- A ring of regeneration no longer heals fire or acid damage, as the ring's entry in the OSE SRD says. Before, the ring healed every kind of damage at 1 hit point a round. Now [`deal_damage`][osrlib.core.combat.deal_damage] records fire and acid damage to the wearer in the new field [`Character.nonregen_damage`][osrlib.core.character.Character.nonregen_damage], and the ring heals the wearer only up to their maximum hit points less that damage. The ring still heals the rest, and a potion or a cure spell still heals all of it. [`DamageDealtEvent`][osrlib.core.events.DamageDealtEvent]`.non_regenerable` is now True when fire or acid hits a wearer. The new field defaults to 0, so a save from before this change still loads and [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4. Saves and party documents now include the field, so the golden files that contain characters were regenerated to add it. -- *Sleep* judges a drained monster by the Hit Dice it has left, because energy drain takes Hit Dice away. Before, both of the spell's modes read the monster's template. The single-creature mode takes only a creature of exactly 4+1 Hit Dice: 4 Hit Dice with a bonus, no more and no fewer. An ogre drained from 4+1 to 3+1 Hit Dice is no longer a 4+1 creature, so this mode now has no effect on it. Before, the mode put that ogre to sleep. The 2d8 Hit Dice mode takes creatures of 4 Hit Dice or fewer and leaves out 4+1 creatures. It now takes the drained ogre for 3 Hit Dice of its budget. Before, it left the ogre out as a 4+1 creature. This fix covers the 4+1 test in both modes. The [`effective_hd`][osrlib.core.combat.effective_hd] fix below covers the rest of the 2d8 mode: what each creature costs from the budget, and whether it's under the 4 Hit Dice limit. With that fix, a 5 Hit Dice grizzly bear drained to 4 is eligible for the 2d8 mode too. -- Three more rules judge a drained monster by the Hit Dice it has left, because energy drain takes Hit Dice away. Before, each used the monster's template. *Confusion*'s re-save each round uses the drained count, so a 3 Hit Dice boar drained to 2 Hit Dice is a subject of 2 Hit Dice and no longer gets the re-save. The Hit Dice bands of the protection scrolls use it too, so a 4 Hit Dice wraith drained to 3 Hit Dice counts in the 1 to 3 Hit Dice band of a scroll of protection from undead. Last, the optional rule that lets a monster of 5 Hit Dice or more harm a creature that only magic or silver can hurt uses it, so a grizzly drained from 5 to 4 Hit Dice no longer qualifies. The count these rules use is [`MonsterInstance.hit_dice_count`][osrlib.core.monsters.MonsterInstance.hit_dice_count], the same count that sets the monster's THAC0 and saves. The turning table and a monster's experience value still use the template, because the OSE SRD ties them to the kind of monster. -- *Death spell* cast at a group with `target_group_id` now takes only the creatures its 60' cube covers. Before, the cast took every living member of the group. The cast now uses the same footprint as any other area spell in [`BattleDeclaration`][osrlib.crawl.commands.BattleDeclaration]: the group's living members in spawn order up to the footprint's capacity, then the party's front rank when the `aoe_friendly_fire` ruleset option is on, the group is within 5 feet, and the cube has room left. A cast with named `targets` still uses those targets. *Sleep* has no area in the OSE SRD, so it still takes every living member of the group. -- When a wandering monster interrupts a rest, the full turns the party rested before the monsters came now count (#169). Before, a 48-turn night broken after four full turns cleared neither fatigue nor exhaustion. Now one full turn of rest clears fatigue. Each full turn also counts toward the three that clear exhaustion from a chase, and the next rest adds to that count. A rest interrupted during its first turn still clears nothing. An interrupted night still isn't sleep for preparing spells, and an interrupted day still heals nobody. The fix makes no new draws, and no golden file changed. -- Trolls, thouls, and vampires regenerate in play (#167). Until this release a spawned one never healed, though its template had the ability. A monster whose template has the `regeneration` ability now starts to regenerate when its encounter opens. That's true whether the monster came from a [`SpawnMonsters`][osrlib.crawl.commands.SpawnMonsters] command, a keyed area, or a wandering-monster check. The command that opened the encounter reports an [`EffectAttachedEvent`][osrlib.core.events.EffectAttachedEvent] for each such monster. A hurt troll gets back 3 hit points a round, starting 3 rounds after it last took damage. It never gets back damage from fire or acid. A troll slain while its encounter is still running gets back up 2d6 rounds later and fights again. It stays dead if fire and acid alone did as much damage as its maximum hit points. The session rolls the revival delay on the effects stream. A thoul or a vampire heals every round while it lives. The session releases the effect when the encounter ends, so a troll that is still dead then stays dead. A game recorded under 2.0.1 that met one of these monsters replays differently under this release. No golden file changed. -- Undead that a cleric turns now flee and attack no one, even when the cleric turns only some of a group (#168). Before, the turned members of a partly turned group kept closing and attacking, the cleric who turned them included. Now they break away as a new group, which runs from the battle the way a wholly turned group does. The session reports the split with a [`MonsterFledEvent`][osrlib.crawl.events.MonsterFledEvent] whose `group_id` is the new group's id. The members who weren't turned stay in the original group and fight on, and a policy you registered for that group still chooses their actions. The group's shared treasure stays with the members who fight on, and each turned member takes only the treasure it carries. A turned monster that can't move, such as one caught in a *web*, stays where it is and does nothing. The session skips any action a policy returns for a turned monster. A wholly turned group behaves as before, and no golden file changed. -- When a monster's attack hits in battle, the session applies the effects the attack lists (#167). A ghoul's or a thoul's claw paralyses for 2d4 turns unless the target saves versus paralysis. An elf isn't affected. A carcass crawler's tentacle and a gelatinous cube's touch paralyse the same way, and they affect elves too. A poisonous bite or sting kills unless the target saves versus death. The target dies at once, or when the poison's onset runs out if the monster's poison has one. The session records the death as a poison death, so *neutralize poison* can revive the character. A mummy's touch gives mummy rot, with no save. A wight's, a wraith's, a spectre's, or a vampire's touch drains levels, also with no save. A basilisk's bite and a cockatrice's beak turn the target to stone unless it saves versus petrification. The basilisk's and the medusa's petrifying gaze reaches each character in the party's front rank once a round while the gazer is in melee. The gaze takes no attack roll, and the session no longer rolls the basilisk's gaze as an attack. A character with the `averted_eyes` condition doesn't meet the gaze, and a member who sets the new [`BattleDeclaration.gaze_defense`][osrlib.crawl.commands.BattleDeclaration.gaze_defense] field doesn't meet it either. A blind character can't meet a gaze, so [`resolve_gaze`][osrlib.core.combat.resolve_gaze] spares one too. The adaptations register lists the attack effects that the session leaves to the referee. In a battle against one of these monsters, the session now makes more draws from the combat, effects, and advancement streams. So the same seed and commands give a different battle than they gave under 2.0.1. No golden file changed. -- Every member of a party a basilisk surprises now meets its gaze as the encounter opens, as the OSE SRD's basilisk says: "Characters surprised by a basilisk meet its gaze." Each living member, back rank included, makes one saving throw versus paralysis, which in B/X also covers petrification, or turns to stone. A blind member can't meet the gaze and makes no save. The gaze comes at whatever distance the encounter opened and whatever the monsters' reaction, so it comes in an encounter that never becomes a battle. It comes when the basilisk is surprised too, because a gaze takes no action. The saving throws come among the opening events of [`start_encounter`][osrlib.crawl.encounter.start_encounter], so the command that opened the encounter reports them, whether the encounter came from a [`SpawnMonsters`][osrlib.crawl.commands.SpawnMonsters] command, a keyed area, or a wandering-monster check. A battle that opens from the encounter doesn't repeat the gaze. When the basilisk attacks at once, it doesn't gaze again in its free round, so a member in melee meets its gaze once. Before, a basilisk that surprised the party gazed only once it was in melee, and then only at the front rank. A medusa doesn't do this, because her entry has no such rule. In the monster catalog, the basilisk's `surprise` ability is no longer `manual`, and its `params` include `meets_gaze: true`. You don't need to set either. -- Battle now uses a monster's other attack routines, not only the first. Before, it used only the first routine in the monster's stat block, so a thoul never used a weapon, a mule never bit, and an elephant never trampled, and an effect in a later routine never applied. Now the default policy, [`ScriptedPolicy`][osrlib.crawl.battle.ScriptedPolicy], picks a routine for each melee attack, with equal chance among the first routine and the alternatives the OSE SRD offers. Each round, the spitting cobra either spits or bites, and its bite poisons. The thoul either claws or swings a weapon, the mule kicks or bites, the minotaur gores and bites or swings a weapon, and the wererat bites or uses a weapon. The policy never picks a situational routine, like an elephant's trample. A policy of your own can name one, and battle then rolls it as a melee attack. Battle never rolls a breath, a gaze, or a spell as an attack, even when a policy names one. The policy draws the pick from the monster-action stream, and only for a monster that has a choice. Every other monster draws as before. -- A spitting cobra's spit now blinds, as the OSE SRD's snake entry says: "A hit causes permanent blindness (save vs poison)." Before, a spit that hit did nothing. Now the target makes a saving throw versus death, which in B/X also covers poison, or is blind for good. The SRD's combat rules say "A blind character is unable to attack", so [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound] refuses an attack from a blind member with `combat.attack.attacker_blind`. The blindness is permanent and *dispel magic* doesn't end it. No spell osrlib runs cures it, so a cure is the referee's call. In the monster catalog, the cobra's spit ability has the tag `blindness` where it had `blinding_spit`, and it's no longer `manual`. A game that looks the ability up with [`MonsterTemplate.ability`][osrlib.core.monsters.MonsterTemplate.ability] passes `"blindness"` now. The save draws from the combat stream, so a battle in which a spit hits plays out differently than before. No golden file changed. -- A ghoul or a thoul now turns from a member it has paralysed and attacks someone else, as the OSE SRD's ghoul entry says: "After paralysing a target, ghouls will attack others." The thoul's entry says the same of thouls. Before, the default policy, [`ScriptedPolicy`][osrlib.crawl.battle.ScriptedPolicy], picked a paralysed member as often as any other. Now the policy passes over a paralysed member while someone who is neither paralysed nor turned to stone is in reach. It attacks a paralysed member only when no one else is in reach, and a paralysed member still comes before a statue. The policy reads the new param `attacks_others` in the monster's `paralysis` ability, not the monster's id, so a monster you define can have it too. In the monster catalog, the ghoul's and the thoul's `paralysis` abilities have `attacks_others: true`. A carcass crawler and a gelatinous cube still attack the members they paralyse, because their entries don't say otherwise. The policy picks a target once a round, so the rest of a ghoul's attacks that round stay on a member its claw has just paralysed. When a ghoul or a thoul passes over a paralysed member, its pick from the monster-action stream changes, so that fight plays out differently than before. No golden file changed. -- A monster or an NPC adventurer no longer attacks a member who was turned to stone while someone else is in reach. Before, the default policies, [`ScriptedPolicy`][osrlib.crawl.battle.ScriptedPolicy] for monsters and [`NpcPartyPolicy`][osrlib.crawl.battle.NpcPartyPolicy] for NPC adventurers, picked a statue as often as any other member of the front rank, so a basilisk kept biting the members it had already turned to stone. A statue is out of the fight, so now both policies pass over one while a member who isn't stone is in reach. A paralysed member comes before a statue. When every member in reach is stone, the attacker picks a statue, as before. The SRD doesn't say this, so the adaptations register records it as a judgement. A monster with no paralysed or petrified member in reach, an NPC adventurer with no statue in reach, and either one with only statues in reach draw from the monster-action stream exactly as before. No golden file changed. -- A trap's saving throw now adds the `modifier` of the trap's [`SaveSpec`][osrlib.core.spells.SaveSpec], as a spell's save does. Before, the session left the modifier out, so a trap you wrote with a bonus or a penalty on its save played as if it had none. The modifier changes whether the save passes, and the dice the session rolls are the same as before (#167). -- Lighting the last oil flask in a pile on the floor with [`LightSource`][osrlib.crawl.commands.LightSource] now leaves the pile when gems, jewellery, or magic items are still in it. Before, the session removed the pile once it had no ordinary items and no coins, which deleted any gems, jewellery, or magic items still in it (#167). -- When a breath weapon or *disintegrate* kills a party member, the magic items that make their saving throw now go to a drop pile on the party's cell in the same command. The party picks them up with [`TakeTreasure`][osrlib.crawl.commands.TakeTreasure] and the feature id `"pile"`. Before, the items stayed in the dead member's inventory, and the commands that hand over or drop an item refuse a dead member, so the party couldn't recover them. [`EquipmentDestroyedEvent`][osrlib.core.events.EquipmentDestroyedEvent]`.saved_items` still lists them, and the draws are unchanged (#167). -- A keyed encounter's lair hoard is now rolled once. Before, a party that evaded the monsters, left, and came back made the session roll the hoard again and put down a second cache, so walking in and out of a lair produced more treasure each time. Now the monsters spawn again but the party finds the cache from the first visit, or nothing if it took that cache. The new [`DungeonState.generated_lair_hoards`][osrlib.crawl.dungeon.DungeonState.generated_lair_hoards] list records the lairs whose hoard was rolled. It defaults to empty, so a save from before this change still loads and [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4. Such a save has no record of the hoards already rolled. So after you load it, a lair that the party visited before the save, and whose monsters it didn't clear, rolls its hoard once more the next time the party walks in, even if the party took the first cache. The session records that roll, so the hoard isn't rolled again (#167). -- The players now see a gate's `success` text when the gate is on a passage or stairs that lead to another cell of the same level. The text is in the `narrative` of the [`PartyMovedEvent`][osrlib.crawl.events.PartyMovedEvent] for the landing cell, and [`format_message`][osrlib.messages.format_message] adds it to that event's line. For stairs within one level, the session emits that event only when the gate has success text. Before, the session put the text only on a level or dungeon entry, which it doesn't report for a crossing within one level, so the text never appeared. `PartyMovedEvent.narrative` is a new field that defaults to `None`, so [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4 (#167). -- Reading a door's state with [`DungeonState.door`][osrlib.crawl.dungeon.DungeonState.door] no longer changes the door. Before, the first call for a door stored an all-`False` entry, so a front end that read an untouched door whose [`DoorSpec`][osrlib.crawl.dungeon.DoorSpec] sets `starts_open` left it shut for the rest of play, and a save recorded it as shut. Now the method stores nothing for a read. Setting a field on the state it returns still stores the entry, as before (#159). -- Worn gauntlets of ogre power raise the wearer's carrying capacity under detailed encumbrance, as their entry in the OSE SRD says. Each movement band and the maximum load rise by the template's `max_load_bonus_coins`, 1,000 coins, so the wearer moves at 120' up to 1,400 coins and can still move at 2,600. When the party takes treasure with [`TakeTreasure`][osrlib.crawl.commands.TakeTreasure], the engine loads the wearer up to that raised maximum. Basic encumbrance is unchanged, because the entry gives the extra weight under detailed encumbrance only (#166). -- [`damage_roll`][osrlib.core.combat.damage_roll] adds an enchanted weapon's `damage_bonus`. Before, it added the `attack_bonus`. Every built-in weapon has equal bonuses and deals the same damage as before. A weapon made from a custom template whose two bonuses differ now adds its damage bonus. A matching versus clause's `bonus` still replaces the damage bonus (#166). -- [`effective_hd`][osrlib.core.combat.effective_hd] counts the Hit Dice a drained monster has left. Before, it counted the template's, so a drained ogre still counted as 4 Hit Dice. *Sleep*'s 2d8 mode uses this count for what each creature costs from its budget and for its 4 Hit Dice limit. The *sleep* fix above covers the spell's test for a 4+1 creature (#166). -- A spent stack, a magic item at quantity 0 like an emptied staff, weighs nothing. Before, [`treasure_weight_coins`][osrlib.core.items.treasure_weight_coins] counted it as one item, though [`Inventory.carried_item`][osrlib.core.items.Inventory.carried_item] reported none carried, so it could slow its carrier (#166). -- When an enchanted item's instance has no `base_item_id`, every function reads the template's base. Before, [`equipment_weight_coins`][osrlib.core.items.equipment_weight_coins] and the movement rate of worn armour read only the instance's field, so a shield +1 built without it weighed nothing, and enchanted armour whose template sets its base moved as unarmoured. Items made by [`instantiate_magic_item`][osrlib.core.treasure.instantiate_magic_item] have their base on the instance and aren't affected (#166). -- The [`RollResult`][osrlib.core.dice.RollResult] that [`damage_roll`][osrlib.core.combat.damage_roll] returns obeys its documented rule, `total == (sum(rolls) + modifier) * multiplier`, before the total is raised to 1. `modifier` includes the Strength modifier, the magic weapon's bonus, spell bonuses, and a fixed amount. `multiplier` includes the doublings, like a back-stab or a braced spear. Before, both fields were always 0 and 1, so a front end that showed the breakdown showed the wrong numbers. Totals and draw sequences are unchanged (#166). -- A front end built on the player view can cast *sleep* at a group in battle (#162). When a [`BattleDeclaration`][osrlib.crawl.commands.BattleDeclaration] for an `hd_budget` mode has a `target_group_id` and no `targets`, the session now spends the spell's Hit Dice budget on that group's living members, weakest first. Before, the session refused it with `magic.cast.target_count`, and the player view shows no monster ids to name instead. It's the same cast as one that lists every living member of the group in spawn order, and it isn't limited to an area footprint. When both are set, the session still uses the named `targets`, so a saved command log replays as it did. No golden file changed. -- A front end can tell that a monster group has fled, and the session refuses every declaration aimed at it (#162). [`EncounterGroupView`][osrlib.crawl.views.EncounterGroupView] has a new `fled` field, which is true once the group has left the battle. A fled group stays in the player view's `groups`, in order. Before, the session refused an attack or an item aimed at a fled group but accepted a `close` move or an area spell. Now the session refuses each of these with `battle.declaration.unknown_group`, with the declarer's id as the `character` param. You can still declare against a group that is running away but hasn't left the battle. A spell aimed at a group that flees earlier in the same round fizzles in the magic phase, as a spell does when any of its checks fails there. The field defaults to `False`, so [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4. No golden file changed. -- A turning reaches only the monsters still in the fight. Before, a `turn_undead` in [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound] reached every monster in the encounter, the dead and the fled included. A skeleton shot dead in the missile phase was still read on the turning table, so the turning could report `magic.turning.turned` with no monster affected. Now it reaches the living monsters of the groups that haven't fled. When the magic phase finds none left, the turning fizzles with a [`DeclarationFizzledEvent`][osrlib.core.events.DeclarationFizzledEvent] at `magic.turning.fizzled`, whose `reason` is `battle.declaration.no_target`, and it draws nothing. [`TurnUndead`][osrlib.crawl.commands.TurnUndead] before a battle skips a monster killed earlier in the encounter in the same way. -- Only a member in the front rank can strike with a staff of striking, because the OSE SRD's entry says it's "Used as a magical melee weapon". Before, a member in the back rank could use one in [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound]. Now the session refuses that use with `battle.declaration.not_in_front_rank`, as it refuses a melee attack. A member who steps up this round, named in [`replacement_id`][osrlib.crawl.commands.BattleDeclaration.replacement_id], may strike. If that swap doesn't happen, the use fizzles in the magic phase at `magic.device.fizzled`, and the staff keeps its charge. With the ruleset's `formation_width_limit` off, every member stands in the front rank and may strike. -- The `hp_lost` of a [`LevelDrainedEvent`][osrlib.core.events.LevelDrainedEvent] is how far the victim's maximum hit points fell. Before, it was the hit points rolled for the lost levels, which overstated the loss when the maximum stopped at 1. A level 2 dwarf with 3 maximum hit points who rolled a 7 for the lost level was reported to lose 7, and now is reported to lose 2. The `hp_lost` of [`DrainResult`][osrlib.core.classes.DrainResult] is still the hit points rolled. -- The `spawn_consequence` of a [`LevelDrainedEvent`][osrlib.core.events.LevelDrainedEvent] appears only on a `combat.drain.slain` event, because it says what a character drained of all levels becomes. Before, a drain the character survived carried it too, as if the character were about to rise as a wight. +- When the whole party retreats, every attack the monster side makes on the party that round now gets +2 and ignores the shield, whichever side acts first. That's what the OSE SRD's retreat says: "This round: ... the opponent gains a +2 bonus to all attacks against the retreating combatant and ignores any AC bonus due to the combatant's shield". Before, the penalty started only when the party moved off, so monsters that won initiative and attacked first got no bonus and counted the shield. A confused monster's attack never got it. +- A ring of regeneration no longer heals fire or acid damage, as the ring's entry in the OSE SRD says. Before, the ring healed every kind of damage at 1 hit point a round. + - [`deal_damage`][osrlib.core.combat.deal_damage] records fire and acid damage to the wearer in the new field [`Character.nonregen_damage`][osrlib.core.character.Character.nonregen_damage]. The ring heals the wearer only up to their maximum hit points less that damage. The ring still heals the rest, and a potion or a cure spell still heals all of it. + - [`DamageDealtEvent`][osrlib.core.events.DamageDealtEvent]`.non_regenerable` is now True when fire or acid hits a wearer. + - The new field defaults to 0, so a save from before this change still loads and [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4. Saves and party documents now include the field. +- *Sleep* now judges a drained monster by the Hit Dice it has left, because energy drain takes Hit Dice away. Before, both of the spell's modes read the monster's template. Take an ogre drained from 4+1 to 3+1 Hit Dice as the example: + - The single-creature mode takes only a creature of exactly 4+1 Hit Dice: 4 Hit Dice with a bonus, no more and no fewer. The drained ogre is no longer a 4+1 creature, so this mode now has no effect on it. Before, the mode put that ogre to sleep. + - The 2d8 Hit Dice mode affects as many creatures as a 2d8 roll of Hit Dice, its budget, pays for. It takes creatures of 4 Hit Dice or fewer and leaves out 4+1 creatures. It now takes the drained ogre for 3 Hit Dice of its budget. Before, it left the ogre out as a 4+1 creature. + - This fix covers the 4+1 test in both modes. The [`effective_hd`][osrlib.core.combat.effective_hd] entry, later in this section, covers the rest of the 2d8 mode: what each creature costs from the budget, and whether it's under the 4 Hit Dice limit. With that fix, a 5 Hit Dice grizzly bear drained to 4 is eligible for the 2d8 mode too. +- Three more rules now judge a drained monster by the Hit Dice it has left, because energy drain takes Hit Dice away. Before, each used the monster's template. The count these rules use is [`MonsterInstance.hit_dice_count`][osrlib.core.monsters.MonsterInstance.hit_dice_count], the same count that sets the monster's THAC0 and saves. The turning table and a monster's experience value still use the template, because the OSE SRD ties them to the kind of monster. The three rules: + - *Confusion*'s re-save each round, which a monster gets when it has 3 Hit Dice or more, or 2 Hit Dice with a bonus, like 2+1. A 3 Hit Dice boar drained to 2 Hit Dice is a subject of 2 Hit Dice and no longer gets the re-save. + - The Hit Dice bands of the protection scrolls. A 4 Hit Dice wraith drained to 3 Hit Dice counts in the 1 to 3 Hit Dice band of a scroll of protection from undead. + - The optional rule that lets a monster of 5 Hit Dice or more harm a creature that only magic or silver can hurt. A grizzly drained from 5 to 4 Hit Dice no longer qualifies. +- *Death spell* cast at a group with `target_group_id` now takes only the creatures its 60' cube covers. Before, the cast took every living member of the group. The cast now uses the same [area footprint](https://mmacy.github.io/osrlib-python/adaptations/#the-area-footprint) as any other area spell in [`BattleDeclaration`][osrlib.crawl.commands.BattleDeclaration]: the group's living members in the order the session spawned them, up to the number of creatures the area can cover. The party's front rank comes next when the `aoe_friendly_fire` option of the [`Ruleset`][osrlib.core.ruleset.Ruleset] is on, the group is within 5 feet of the party, and the cube has room left. The option is on by default. With the option off, the cube never takes party members. A cast with named `targets` still uses those targets. *Sleep* has no area in the OSE SRD, so it still takes every living member of the group. +- When a wandering monster interrupts a rest, the full turns the party rested before the monsters came now count (#169). Before, a 48-turn night broken after four full turns cleared neither fatigue nor exhaustion. Now one full turn of rest clears fatigue. Each full turn also counts toward the three that clear exhaustion from a chase, and the next rest adds to that count. A rest interrupted during its first turn still clears nothing. An interrupted night still isn't sleep for preparing spells, and an interrupted day still heals nobody. The fix makes no new draws. +- Trolls, thouls, and vampires now regenerate in play (#167). Until this release a spawned one never healed, though its template had the ability. A monster whose template has the `regeneration` ability now starts to regenerate when its encounter opens, whether it came from a [`SpawnMonsters`][osrlib.crawl.commands.SpawnMonsters] command, a keyed area, or a wandering-monster check. The command that opened the encounter reports an [`EffectAttachedEvent`][osrlib.core.events.EffectAttachedEvent] for each such monster. A game recorded under 2.0.1 that met one of these monsters replays differently under this release, as the replay entry under Changed describes. + - A troll's regeneration attaches when the encounter opens, but a hurt troll heals only from 3 rounds after it last took damage. Then it gets back 3 hit points a round. It never gets back damage from fire or acid. + - A troll slain while its encounter is still running gets back up 2d6 rounds later and fights again. It stays dead if fire and acid alone did as much damage as its maximum hit points. The session rolls the revival delay on the effects stream ([`EFFECTS_STREAM`][osrlib.core.effects.EFFECTS_STREAM]). + - A hurt thoul gets back 1 hit point, and a hurt vampire 3 hit points, at the start of each round while it lives. + - The session releases the effect when the encounter ends, so a troll that is still dead then stays dead. +- Undead that a cleric turns now flee and attack no one, even when the cleric turns only some of a group (#168). Before, the turned members of a partly turned group kept closing and attacking, the cleric who turned them included. Now they break away as a new group, with a new group id, which runs from the battle the way a wholly turned group does. The new group needs no policy from your game. Each round, the session moves it away at its full movement rate and reports the move in a [`GroupMovedEvent`][osrlib.crawl.events.GroupMovedEvent]. Once it's more than [`FLEE_EXIT_FEET`][osrlib.crawl.battle.FLEE_EXIT_FEET] (120 feet) away, it has fled the battle. The session reports the split in the middle of the battle with a [`MonsterFledEvent`][osrlib.crawl.events.MonsterFledEvent] whose `group_id` is the new group's id, so add that group to your battle screen when you see the event. + - The members who weren't turned stay in the original group and fight on. A policy you registered for that group still chooses their actions. + - The group's shared treasure stays with the members who fight on. Each turned member takes only the treasure it carries. + - A turned monster that can't move, like one caught in a *web*, can't run away. It stays where it is and does nothing. + - The session skips any action a policy returns for a turned monster. + - A wholly turned group behaves as before. +- When a monster's attack hits in battle, the session now applies the effects the attack lists (#167). In a battle against one of these monsters, the session makes more draws from the combat, effects, and advancement streams ([`COMBAT_STREAM`][osrlib.core.combat.COMBAT_STREAM], `EFFECTS_STREAM`, and [`ADVANCEMENT_STREAM`][osrlib.core.character.ADVANCEMENT_STREAM]). So the same seed and commands give a different battle than they gave under 2.0.1. The [adaptations register](https://mmacy.github.io/osrlib-python/adaptations/), the docs page that records where osrlib interprets or adapts the rules, lists the attack effects that the session leaves to the referee. + - A ghoul's or a thoul's claw paralyses for 2d4 turns unless the target saves versus paralysis. An elf isn't affected. + - A carcass crawler's tentacle and a gelatinous cube's touch paralyse the same way, and they affect elves too. + - A poisonous bite or sting kills unless the target saves versus death. The target dies at once, or when the poison's onset runs out if the monster's poison has one. The session records the death as a poison death, so *neutralize poison* can revive the character. + - A mummy's touch gives mummy rot, with no save. + - A wight's, a wraith's, a spectre's, or a vampire's touch drains levels, also with no save. + - A basilisk's bite and a cockatrice's beak turn the target to stone unless it saves versus petrification. + - The basilisk's and the medusa's petrifying gaze reaches each character in the party's front rank once a round while the gazer is in melee. The gaze takes no attack roll, and the session no longer rolls the basilisk's gaze as an attack. + - A character with the `averted_eyes` condition doesn't meet the gaze, and neither does a member who sets the new [`BattleDeclaration.gaze_defense`][osrlib.crawl.commands.BattleDeclaration.gaze_defense] field. A blind character can't meet a gaze, so [`resolve_gaze`][osrlib.core.combat.resolve_gaze] spares one too. +- When a basilisk surprises the party, every member now meets its gaze as the encounter opens, as the OSE SRD's basilisk says: "Characters surprised by a basilisk meet its gaze." Each living member, back rank included, makes one saving throw versus paralysis, which in B/X also covers petrification, or turns to stone. Before, a basilisk that surprised the party gazed only once it was in melee, and then only at the front rank. + - A blind member can't meet the gaze and makes no save. + - The gaze comes at whatever distance the encounter opened and whatever the monsters' reaction, so it comes in an encounter that never becomes a battle. It comes even when the basilisk is also surprised, because a gaze takes no action. + - The saving throws come among the opening events of [`start_encounter`][osrlib.crawl.encounter.start_encounter], so the command that opened the encounter reports them. That's true whether the encounter came from a [`SpawnMonsters`][osrlib.crawl.commands.SpawnMonsters] command, a keyed area, or a wandering-monster check. + - A battle that opens from the encounter doesn't repeat the gaze. A side that surprises the other gets a free round to act before the other side can. When the basilisk attacks at once, it doesn't gaze again in its free round, so a member in melee meets its gaze once. + - A medusa doesn't do this, because her entry has no such rule. + - In the monster catalog, the basilisk's `surprise` ability is no longer `manual`, and its `params`, the values osrlib reads from the ability, include `meets_gaze: true`. You don't need to set either. +- Battle now uses a monster's other attack routines, not only the first. Before, it used only the first routine in the monster's stat block. So a thoul never used a weapon and a mule never bit, an effect in a later routine never applied, and not even a policy of your own could make an elephant trample. Now the default policy, [`ScriptedPolicy`][osrlib.crawl.battle.ScriptedPolicy], picks a routine for each melee attack, with equal chance among the first routine and the alternatives the OSE SRD offers. + - Each round, the spitting cobra either spits or bites, and its bite poisons. The thoul either claws or swings a weapon, the mule kicks or bites, the minotaur gores and bites or swings a weapon, and the wererat bites or uses a weapon. + - The default policy never picks a situational routine, like an elephant's trample. A policy of your own can name one, and battle then rolls it as a melee attack. + - Battle never rolls a breath, a gaze, or a spell as an attack, even when a policy names one. + - The policy draws the pick from the monster-action stream ([`MONSTER_ACTION_STREAM`][osrlib.crawl.session.MONSTER_ACTION_STREAM]), and only for a monster that has a choice. Every other monster draws as before. +- A spitting cobra's spit ability has a new tag, `blindness`, where it had `blinding_spit`, and a blind member can't attack. If your game looks the ability up with [`MonsterTemplate.ability`][osrlib.core.monsters.MonsterTemplate.ability], pass `"blindness"` now. [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound] refuses an attack from a blind member with `combat.attack.attacker_blind`, so don't offer the attack. The spit now blinds, as the OSE SRD's snake entry says: "A hit causes permanent blindness (save vs poison)." Before, a spit that hit did nothing. + - The target makes a saving throw versus death, which in B/X also covers poison, or is blind for good. The save draws from the combat stream, so a battle in which a spit hits plays out differently than before. + - The refusal follows the SRD's combat rules, which say "A blind character is unable to attack". + - The blindness is permanent, and *dispel magic* doesn't end it. No spell osrlib runs cures it, so a cure is the referee's call. + - In the monster catalog, the spit ability is no longer `manual`. +- A ghoul or a thoul now turns from a member it has paralysed and attacks someone else, as the OSE SRD's ghoul entry says: "After paralysing a target, ghouls will attack others." The thoul's entry says the same of thouls. To give a monster of your own this behavior, set the new param `attacks_others: true` in its `paralysis` ability. Before, the default policy, [`ScriptedPolicy`][osrlib.crawl.battle.ScriptedPolicy], picked a paralysed member as often as any other. + - The policy passes over a paralysed member while someone who is neither paralysed nor turned to stone is in reach. It attacks a paralysed member only when no one else is in reach, and it attacks a paralysed member before a statue. + - The policy picks a target once a round, so the rest of a ghoul's attacks that round stay on a member its claw has just paralysed. The ghoul turns to someone else from the next round. + - The policy reads `attacks_others` in the monster's `paralysis` ability, not the monster's id. In the monster catalog, the ghoul's and the thoul's `paralysis` abilities have `attacks_others: true`. + - A carcass crawler and a gelatinous cube still attack the members they paralyse, because their entries don't say otherwise. + - When a ghoul or a thoul passes over a paralysed member, its pick from the monster-action stream changes, so that fight plays out differently than before. +- A monster or an NPC adventurer no longer attacks a member who was turned to stone while someone else is in reach. Before, the default policies, [`ScriptedPolicy`][osrlib.crawl.battle.ScriptedPolicy] for monsters and [`NpcPartyPolicy`][osrlib.crawl.battle.NpcPartyPolicy] for NPC adventurers, picked a statue as often as any other member of the front rank, so a basilisk kept biting the members it had already turned to stone. + - A statue is out of the fight, so both policies now pass over one while a member who isn't stone is in reach. A monster attacks a paralysed member before a statue. When every member in reach is stone, the attacker picks a statue, as before. + - The SRD doesn't say this, so the adaptations register records it as a judgement. + - The draws from the monster-action stream are the same as before for a monster with no paralysed or petrified member in reach, for an NPC adventurer with no statue in reach, and for either one with only statues in reach. +- A trap's saving throw now adds the `modifier` of the trap's [`SaveSpec`][osrlib.core.spells.SaveSpec], as a spell's save does (#167). Before, the session left the modifier out, so a trap you wrote with a bonus or a penalty on its save played as if it had none. The modifier changes whether the save passes. The dice the session rolls are the same as before. +- Lighting the last oil flask in a pile on the floor with [`LightSource`][osrlib.crawl.commands.LightSource] no longer deletes the gems, jewellery, or magic items still in the pile (#167). A pile, a [`DropPile`][osrlib.crawl.dungeon.DropPile], is the loose goods lying on one cell's floor. Before, the session removed the pile once it had no ordinary items and no coins. Now it leaves the pile while gems, jewellery, or magic items are still in it. +- When a breath weapon or *disintegrate* kills a party member, the magic items that survive now go to a drop pile on the party's cell in the same command (#167). To recover them, send [`TakeTreasure`][osrlib.crawl.commands.TakeTreasure] with the feature id `"pile"`. If a pile is already on the cell, the items join it. Before, the items stayed in the dead member's inventory. The commands that hand over or drop an item refuse a dead member, so the party couldn't recover them. [`EquipmentDestroyedEvent`][osrlib.core.events.EquipmentDestroyedEvent]`.saved_items` still lists them, and the draws are unchanged. +- The lair hoard of a [`KeyedEncounter`][osrlib.crawl.dungeon.KeyedEncounter], the monsters an adventure places in a keyed area, is now rolled once (#167). A lair hoard is the treasure in the monsters' lair. The session rolls it the first time the party walks in, and puts it down as a cache, treasure on a cell that the party takes with [`TakeTreasure`][osrlib.crawl.commands.TakeTreasure]. Before, a party that evaded the monsters, left, and came back made the session roll the hoard again and put down a second cache, so walking in and out of a lair produced more treasure each time. Now the monsters spawn again, but the party finds the cache from the first visit, or nothing if it took that cache. + - The new [`DungeonState.generated_lair_hoards`][osrlib.crawl.dungeon.DungeonState.generated_lair_hoards] list records the lairs whose hoard was rolled. It defaults to empty, so a save from before this change still loads and [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4. + - A save from before this change has no record of the hoards already rolled. After you load it, the session rolls the hoard once more for a lair that the party visited before the save and whose monsters it didn't clear. That happens the next time the party walks in, even if the party took the first cache. The session records that roll, so it doesn't roll the hoard again. +- The players now see a gate's `success` text when the gate is on a passage or stairs that lead to another cell of the same level (#167). A [gate](https://mmacy.github.io/osrlib-python/guides/gates-triggers-quests/#gating-a-door-or-a-stair), a [`GateSpec`][osrlib.crawl.gates.GateSpec], is a condition the party must meet to get through a door, a stair, or a passage. If your front end builds the line for a [`PartyMovedEvent`][osrlib.crawl.events.PartyMovedEvent] instead of calling [`format_message`][osrlib.messages.format_message], show the event's `narrative`. Before, the session put the text only on a level or dungeon entry, which it doesn't report for a crossing within one level, so the text never appeared. + - The text is in the `narrative` of the `PartyMovedEvent` for the landing cell, the cell where the party arrives, and `format_message` adds it to that event's line. + - For stairs within one level, the session emits that event only when the gate has success text. + - `PartyMovedEvent.narrative` is a new field. It's `None` when the gate has no success text and when no gate was crossed. It defaults to `None`, so [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4. +- Reading a door's state with [`DungeonState.door`][osrlib.crawl.dungeon.DungeonState.door] no longer changes the door (#159). Before, the first call for a door stored an all-`False` entry. So a front end that read an untouched door whose [`DoorSpec`][osrlib.crawl.dungeon.DoorSpec] sets `starts_open` left it shut for the rest of play, and a save recorded it as shut. Now the method stores nothing for a read. For an untouched door, it returns a new all-`False` state, so `open` reads `False` even when `starts_open` is set. To show doors as the party sees them, read the player view from [`GameSession.view`][osrlib.crawl.session.GameSession.view]. Setting a field on the state the method returns still stores the entry, as before. +- Worn gauntlets of ogre power now raise the wearer's carrying capacity under detailed encumbrance, as their entry in the OSE SRD says (#166). A game uses detailed encumbrance when its [`Ruleset`][osrlib.core.ruleset.Ruleset] sets `encumbrance` to `EncumbranceMode.DETAILED`. That mode sets each character's movement rate from bands of carried weight. Each movement band and the maximum load rise by the template's `max_load_bonus_coins`, 1,000 coins. So the wearer moves at 120' up to 1,400 coins, not 400, and can still move at 2,600 coins, the raised maximum. When the party takes treasure with [`TakeTreasure`][osrlib.crawl.commands.TakeTreasure], the engine loads the wearer up to that raised maximum. Basic encumbrance is unchanged, because the entry gives the extra weight under detailed encumbrance only. +- [`damage_roll`][osrlib.core.combat.damage_roll] now adds an enchanted weapon's `damage_bonus` (#166). Before, it added the `attack_bonus`. Every built-in weapon has equal bonuses and deals the same damage as before. A weapon made from a custom template whose two bonuses differ now adds its damage bonus. A matching versus clause's `bonus` still replaces the damage bonus. +- [`effective_hd`][osrlib.core.combat.effective_hd] now counts the Hit Dice a drained monster has left (#166). Before, it counted the template's, so a drained ogre still counted as 4 Hit Dice. *Sleep*'s 2d8 mode uses this count for what each creature costs from its budget and for its 4 Hit Dice limit. The entry "*Sleep* now judges a drained monster by the Hit Dice it has left", earlier in this section, covers the spell's test for a 4+1 creature. +- A spent stack, a magic item at quantity 0 like an emptied staff, now weighs nothing (#166). Before, [`treasure_weight_coins`][osrlib.core.items.treasure_weight_coins] counted it as one item, though [`Inventory.carried_item`][osrlib.core.items.Inventory.carried_item] reported none carried, so it could slow the member who had it. +- When an enchanted item's instance has no `base_item_id`, every function now reads the template's base (#166). Before, [`equipment_weight_coins`][osrlib.core.items.equipment_weight_coins] and the movement rate of worn armour read only the instance's field. So a shield +1 built without it weighed nothing, and enchanted armour whose template sets its base moved as unarmoured. Items made by [`instantiate_magic_item`][osrlib.core.treasure.instantiate_magic_item] have their base on the instance and aren't affected. +- The [`RollResult`][osrlib.core.dice.RollResult] that [`damage_roll`][osrlib.core.combat.damage_roll] returns now obeys its documented rule, `total == (sum(rolls) + modifier) * multiplier`, before the total is raised to 1 (#166). Before, `modifier` was always 0 and `multiplier` was always 1, so a front end that showed the breakdown showed the wrong numbers. Totals and draw sequences are unchanged. + - `modifier` includes the Strength modifier, the magic weapon's bonus, spell bonuses, and a fixed amount. + - `multiplier` includes the doublings, like a back-stab or a braced spear. +- A front end built on the player view can now cast *sleep* at a group in battle (#162). Set `target_group_id` and leave `targets` empty. Before, the session refused that with `magic.cast.target_count`, and the player view shows no monster ids to name instead. + - This works for any `hd_budget` mode, a mode that affects as many creatures as a rolled pool of Hit Dice pays for. When a [`BattleDeclaration`][osrlib.crawl.commands.BattleDeclaration] for such a mode has a `target_group_id` and no `targets`, the session collects that group's living members, in the order it spawned them, as the candidates. The spell then spends its budget on the candidates with the fewest Hit Dice first, until the budget can't pay for the next one. + - For *sleep*, which has no area, the candidates are every living member of the group, so it's the same cast as one that lists them all in `targets`. A mode with an area, like *death spell*'s 60' cube, takes only the creatures the area covers. + - When both are set, the session still uses the named `targets`, so a saved command log replays as it did. +- A front end can now tell that a monster group has fled, and the session refuses every declaration aimed at a fled group (#162). [`EncounterGroupView`][osrlib.crawl.views.EncounterGroupView] has a new `fled` field, which is true once the group has left the battle. If your front end lists groups as targets, leave out the groups whose `fled` is true. A fled group stays in the player view's `groups`, in order. + - Before, the session refused an attack or an item aimed at a fled group but accepted a `close` move or an area spell. Now the session refuses each of these with `battle.declaration.unknown_group`, with the declaring member's id as the `character` param. + - You can still declare against a group that is running away but hasn't left the battle. + - A spell aimed at a group that flees earlier in the same round fizzles in the magic phase, as a spell does when any of its checks fails there. + - The field defaults to `False`, so [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4. +- A turning now reaches only the monsters still in the fight: the living monsters of the groups that haven't fled. Before, a `turn_undead` in [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound] reached every monster in the encounter, the dead and the fled included. A skeleton shot dead in the missile phase was still read on the turning table, so the turning could report `magic.turning.turned` with no monster affected. When the magic phase finds no such monster left, the turning fizzles with a [`DeclarationFizzledEvent`][osrlib.core.events.DeclarationFizzledEvent] at `magic.turning.fizzled`, whose `reason` is `battle.declaration.no_target`. A fizzled turning spends nothing and draws nothing, though it still ends the cleric's concentration. [`TurnUndead`][osrlib.crawl.commands.TurnUndead] before a battle skips a monster killed earlier in the encounter in the same way. +- Only a member in the front rank can now strike with a staff of striking, because the OSE SRD's entry says it's "Used as a magical melee weapon". If your front end offers the staff's use, offer it only to members in [`EncounterView.front_rank`][osrlib.crawl.views.EncounterView.front_rank] and to a member who steps up this round. Before, a member in the back rank could use one in [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound], with a `BattleDeclaration` whose `action` is `"use_item"` and whose `item_id` is the staff's. Now the session refuses that use with `battle.declaration.not_in_front_rank`, as it refuses a melee attack. + - A member who steps up this round, named in [`replacement_id`][osrlib.crawl.commands.BattleDeclaration.replacement_id], may strike. A side that acted first can stop the swap, for example by leaving either member unable to move, or the replacement asleep, paralysed, charmed, or confused. If the swap doesn't happen, the use fizzles in the magic phase at `magic.device.fizzled`, and the staff doesn't use up a charge. + - With the ruleset's `formation_width_limit` off, every member stands in the front rank and may strike. +- The `hp_lost` of a [`LevelDrainedEvent`][osrlib.core.events.LevelDrainedEvent] is now how far the victim's maximum hit points fell. Before, it was the hit points rolled for the lost levels, which overstated the loss when the maximum stopped at 1. A level 2 dwarf with 3 maximum hit points who rolled a 7 for the lost level drops to 1 maximum hit point. The event reported a loss of 7, and now reports a loss of 2. The `hp_lost` of [`DrainResult`][osrlib.core.classes.DrainResult] is still the hit points rolled, so the two values can differ, and that's intended. +- The `spawn_consequence` of a [`LevelDrainedEvent`][osrlib.core.events.LevelDrainedEvent] now appears only on a `combat.drain.slain` event, because it gives what a character drained of all levels becomes. It's `None` on every other drain event. Before, a drain the character survived had it too, as if the character were about to rise as a wight. ## [2.0.1] - 2026-10-30