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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,41 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Added

- Each party member now has a place on a battle's range track, so members can stand at different distances from the monsters (#209). [`BattleState.advance_feet`][osrlib.crawl.battle.BattleState.advance_feet] gives each member's place in feet forward of the party line, where the party stood when the encounter opened, and [`BattleState.arrived_round`][osrlib.crawl.battle.BattleState.arrived_round] gives the round they got there. A member's distance to a group is the group's `distance_feet` minus the member's advance. Every member starts a battle on the line, at zero. Both fields default to empty, so a 2.2.0 save loads with every member on the line, and [`SCHEMA_VERSION`][osrlib.versioning.SCHEMA_VERSION] stays 4. The [adaptations register](https://mmacy.github.io/osrlib-python/adaptations/#the-battle-round) gives every rule.
- A member can close on a group and attack it in the same round (#209). Declare `action="attack"` with `move="close"` in the member's [`BattleDeclaration`][osrlib.crawl.commands.BattleDeclaration]. The member closes in the party's movement step and attacks in the missile or the melee step. The session judges the attack where the close will leave the member, and judges a melee attack against the front rank that every declared move produces. A member reaches a group in melee when their distance to it is at most their encounter rate plus 5 feet. So a thief can close and back-stab in the party's free round, from up to 35 feet away in leather armour under the default `basic` encumbrance.
- The new function [`preview_battle_round`][osrlib.crawl.battle.preview_battle_round] judges the declarations planned so far for a round without playing it (#209). Call it after each choice on a battle screen. It returns a [`BattleRoundPreview`][osrlib.crawl.battle.BattleRoundPreview] with each declaration's rejections, each member's projected advance, and each group's projected front rank. It runs the same checks as [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound], leaves out the roster check, takes no draw, and changes nothing.
- Two new functions in [`osrlib.crawl.battle`][osrlib.crawl.battle] report positions for a policy of your own or for server-side code (#209). [`member_distance_feet`][osrlib.crawl.battle.member_distance_feet] returns a member's distance to a group, and [`group_front_rank`][osrlib.crawl.battle.group_front_rank] returns the members who can fight a group hand to hand this round without moving.
- The player view shows a battle screen everything it needs to draw the positions and offer each member's choices (#209). Each new field has a default, so a client that ignores it keeps working.
- [`EncounterView.advance_feet`][osrlib.crawl.views.EncounterView.advance_feet] gives each living member's place on the track.
- [`EncounterGroupView.member_distance_feet`][osrlib.crawl.views.EncounterGroupView.member_distance_feet] gives each living member's distance to the group.
- [`EncounterGroupView.front_rank`][osrlib.crawl.views.EncounterGroupView.front_rank] lists the members who can attack the group in melee this round without moving, and whom the group's melee reaches. It's the same value as `group_front_rank`.
- [`EncounterView.formation_width`][osrlib.crawl.views.EncounterView.formation_width] gives how many members can fight side by side where the party stands, or `None` when `formation_width_limit` is off.
- [`EncounterView.party_free_round`][osrlib.crawl.views.EncounterView.party_free_round] is true in the party's free round after it surprised the monsters, the round in which a back-stab is legal.
- [`MemberView.encounter_rate_feet`][osrlib.crawl.views.MemberView.encounter_rate_feet] gives how far a member can move in a round, after encumbrance and *haste*.
- [`EncounterGroupView.lit`][osrlib.crawl.views.EncounterGroupView.lit] says whether a light a member carries reaches the group, and [`EncounterGroupView.seen_by`][osrlib.crawl.views.EncounterGroupView.seen_by] lists the members who can see it. The session refuses nothing because of light or sight, so use these fields to decide what your game offers and how it describes the fight.
- The new event [`MemberMovedEvent`][osrlib.crawl.events.MemberMovedEvent] (`battle.member.moved`) reports each member who moves by themselves, closing on a group or falling back alone (#209). Its fields are `character_id`, `move`, `advance_feet`, `group_id`, and `distance_feet`. It reports the member's place right after the move, before the party line moves back, so use it to narrate the move and read the player view after the round to draw where everyone stands.
- [`DeclarationFizzledEvent`][osrlib.core.events.DeclarationFizzledEvent] has a new code, `battle.melee.fizzled`, and a new optional field, `group_id` (#209). The session reports it when a declared melee attack can't resolve because of where the member stands when the party's melee step comes, for example when the monsters win initiative and run before the member's close. Its `reason` is `combat.attack.out_of_reach` or `battle.declaration.not_in_front_rank`, so tell the player why the member didn't swing. The field defaults to `None`.
- Two new refusal codes for a move set on an action that can't take one (#209). [`ResolveBattleRound`][osrlib.crawl.commands.ResolveBattleRound] refuses a `cast` that sets any `move` with `battle.declaration.cast_cannot_move`, because a caster can't move in the round they cast. It refuses a `turn_undead`, a `use_item`, or a `hold` that sets any `move`, and an `attack` with a `fighting_withdrawal` or a `retreat`, with `battle.declaration.move_not_combinable`. Each has the member's id as its `character` param. An attack that closes, by a member who can't move, gets the existing `battle.declaration.cannot_move`.

### Changed

- A `close` now moves only the member who declares it, at that member's encounter rate (#209). Before, the first `close` in a round moved the whole party at its slowest member's rate. To bring the whole party up, send a `close` for each member. A member stops 5 feet from the group, and stops short of 5 feet from any other group still in the fight. A member already within 5 feet doesn't move.
- A fighting withdrawal or a retreat that not every declarer makes now moves that member back on the range track, at half their encounter rate or their full rate (#209). Any member can make one, wherever they stand, and it needs no replacement. Once the member has moved, the members then nearest each group form its front rank. When every declarer makes the same one, the whole party still moves as one body, and members who stand apart stay apart.
- [`BattleDeclaration.replacement_id`][osrlib.crawl.commands.BattleDeclaration.replacement_id] now only trades places in the marching order, and the trade moves nobody. Marching order decides who stands in front among members at the same distance from a group. To bring a member farther back into the front rank, declare a `close` for them.
- The codes `battle.declaration.no_replacement` and `battle.declaration.replacement_too_far` are gone, and a withdrawal no longer gets `battle.declaration.not_in_front_rank`. If your game handled them, remove those cases.
- `battle.declaration.replacement_unavailable` now means the named member isn't another declarer, is falling back too, or was named by a withdrawing member earlier in marching order.
- The ranks toward each group now come from distance (#209). They're the living members sorted by their distance to the group, then by the round they got there, then by marching order, and cut into rows of the formation width. A melee attack needs the member within 5 feet of the group and in its front rank. Before, the ranks were the marching order whatever the distance. [`EncounterView.front_rank`][osrlib.crawl.views.EncounterView.front_rank] is now the first row toward the nearest group, which is the old value until members stand apart. To decide who can attack a group in melee, read that group's `front_rank` instead.
- Monsters now close on the nearest member they can see and strike only what they reach (#209). A group stops as soon as it's within 5 feet of that member, so it never walks past a member it can see. An invisible member doesn't stop it, and a member turned to stone does. A monster's melee attack resolves only on a member in the group's front rank within 5 feet of it, so the members in front screen the members behind. A `melee` from a policy of your own that aims at anyone else is skipped.
- Monsters now close and attack in the same round. The default policies follow a `close` that brings a member within reach with a `melee`, so a group that closes strikes a round sooner than in 2.2.0.
- An NPC adventurer with a missile weapon or a spell with one target picks from the first row of the party's ranks toward its group, at any distance. A breath that hits one target, like the hellhound's, picks the same way.
- A breath, or an NPC's area spell, covers the rows toward its group from the front, by each row's distance. Under `aoe_friendly_fire`, a party member's area spell catches the members of the target group's front rank who stand within 5 feet of it.
- A petrifying gaze reaches the members of the gazer's group's front rank within 5 feet of it.
- Distances in a battle are now measured from each member (#209). A missile's range band and a spell's range are measured from the member's place. A group that runs leaves the battle once it stands more than 120 feet from every living member. After a formation retreat, the pursuit starts from the standing member nearest a group that can follow. A ring of protection with a 5-foot radius protects the members in the wearer's row toward the attacking group, the members at the wearer's distance from it.
- Some command logs recorded under 2.2.0 replay differently under this release. That's the replay contract: a replay is valid only under the engine version that recorded it. To continue a 2.2.0 game, load its save with [`load_game`][osrlib.persistence.load_game], which restores the saved state and runs no commands. To reproduce a 2.2.0 game exactly from its log, install `osrlib==2.2.0` 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 replay raises [`ContentValidationError`][osrlib.errors.ContentValidationError] when the log contains a round this release refuses, like a `cast`, `turn_undead`, `use_item`, or `hold` that sets a `move`, or a melee attack by a member whom a 2.2.0 `close` had carried into reach with the rest of the party.
- The replay plays out differently when the log contains a `close`, an `attack` that set `move="close"`, which 2.2.0 ignored, a fighting withdrawal or a retreat that one member made alone, or a monster group that closed on the party. A battle plays as before when no member moves and no group closes.
- [`RemoveMember`][osrlib.crawl.commands.RemoveMember] without `give_to` now refuses to remove a member who carries an item the adventure depends on, so the party can't lose a quest item by dropping the member who held it. The adventure depends on an item when a gate on a door or transition, a trigger that can still fire, or any clause of a quest that isn't completed names its catalog id. The session refuses with `party.remove.carries_quest_item`, and the rejection's `character` and `item` params give the member and the item. Name a member in `give_to` and the item moves to them, so the removal goes through.

## [2.2.0] - 2026-10-30
Expand Down
Loading
Loading