osrlib 2.2.0__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {osrlib-2.2.0 → osrlib-2.4.0}/PKG-INFO +1 -1
- {osrlib-2.2.0 → osrlib-2.4.0}/pyproject.toml +1 -1
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/character.py +5 -16
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/combat.py +16 -8
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/events.py +37 -12
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/items.py +22 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/ruleset.py +11 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/battle.py +1566 -430
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/commands.py +258 -137
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/dungeon.py +4 -2
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/encounter.py +13 -6
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/events.py +135 -40
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/exploration.py +9 -3
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/session.py +57 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/views.py +172 -22
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/messages.py +18 -2
- {osrlib-2.2.0 → osrlib-2.4.0}/LICENSE +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/LICENSE-OGL.md +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/README.md +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/__init__.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/__init__.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/abilities.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/alignment.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/classes.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/clock.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/creature.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/dice.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/effects.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/monsters.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/npc.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/rng.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/spells.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/tables.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/treasure.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/core/validation.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/__init__.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/adventure.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/content_pack.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/gates.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/interpreter.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/narrative.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/party.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/quests.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/stocking.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/crawl/triggers.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/data/LICENSE-OGL.md +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/data/__init__.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/data/abilities.json +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/data/classes.json +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/data/combat_tables.json +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/data/encounter_tables.json +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/data/equipment.json +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/data/languages.json +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/data/magic_items.json +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/data/monsters.json +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/data/spells.json +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/data/treasure.json +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/errors.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/persistence.py +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/py.typed +0 -0
- {osrlib-2.2.0 → osrlib-2.4.0}/src/osrlib/versioning.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "osrlib"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.4.0"
|
|
4
4
|
description = "B/X (1981 Basic/Expert) tabletop RPG rules engine for turn-based dungeon crawlers"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
# The wheel ships Open Game Content (osrlib/data/*.json and its LICENSE-OGL.md
|
|
@@ -492,7 +492,7 @@ class Character(BaseModel):
|
|
|
492
492
|
the +1 an ordinary shield gives. A worn item with an armour class bonus that is always
|
|
493
493
|
active, like a ring of protection, adds to the bonus too.
|
|
494
494
|
"""
|
|
495
|
-
from osrlib.core.items import ArmourTemplate, MagicItemInstance, magic_item_template
|
|
495
|
+
from osrlib.core.items import ArmourTemplate, MagicItemInstance, _shield_ac_parts, magic_item_template
|
|
496
496
|
from osrlib.data import load_equipment
|
|
497
497
|
|
|
498
498
|
base = 10 if ascending else 9
|
|
@@ -509,21 +509,10 @@ class Character(BaseModel):
|
|
|
509
509
|
elif worn is not None and isinstance(worn.template, ArmourTemplate) and worn.template.ac is not None:
|
|
510
510
|
printed_ascending = worn.template.ac_ascending
|
|
511
511
|
base = printed_ascending if ascending and printed_ascending is not None else worn.template.ac
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if
|
|
515
|
-
|
|
516
|
-
if template.ac_set is not None and template.ac_set_ascending is not None:
|
|
517
|
-
# The cursed shield's AC 9 [10] sets the wearer's base outright.
|
|
518
|
-
base = template.ac_set_ascending if ascending else template.ac_set
|
|
519
|
-
else:
|
|
520
|
-
mundane_shield = load_equipment().get("shield")
|
|
521
|
-
mundane_bonus = (getattr(mundane_shield, "ac_bonus", None) or 0) if mundane_shield else 0
|
|
522
|
-
bonus += mundane_bonus + template.ac_bonus
|
|
523
|
-
elif (
|
|
524
|
-
shield is not None and isinstance(shield.template, ArmourTemplate) and shield.template.ac_bonus is not None
|
|
525
|
-
):
|
|
526
|
-
bonus += shield.template.ac_bonus
|
|
512
|
+
# The cursed shield's AC 9 [10] sets the wearer's base outright.
|
|
513
|
+
shield_base, bonus = _shield_ac_parts(self.inventory.shield, ascending=ascending)
|
|
514
|
+
if shield_base is not None:
|
|
515
|
+
base = shield_base
|
|
527
516
|
for ring in self.inventory.rings:
|
|
528
517
|
template = magic_item_template(ring)
|
|
529
518
|
if template.always_active:
|
|
@@ -172,6 +172,7 @@ from osrlib.core.items import (
|
|
|
172
172
|
MissileRanges,
|
|
173
173
|
WeaponQuality,
|
|
174
174
|
WeaponTemplate,
|
|
175
|
+
_shield_ac_parts,
|
|
175
176
|
equipped_item_modifiers,
|
|
176
177
|
magic_item_template,
|
|
177
178
|
)
|
|
@@ -379,8 +380,12 @@ class AttackContext(BaseModel):
|
|
|
379
380
|
behind_target: bool = False
|
|
380
381
|
"""Whether the attacker strikes from behind.
|
|
381
382
|
|
|
382
|
-
The defender's shield doesn't count
|
|
383
|
-
|
|
383
|
+
The defender's shield doesn't count, as the OSE SRD's "AC bonuses from shields are
|
|
384
|
+
negated" says. The attack is made against the armour class the defender would have
|
|
385
|
+
without the shield, so a magic shield loses its enchantment along with the +1 that every
|
|
386
|
+
shield gives. A cursed shield that makes the defender's armour class worse still counts,
|
|
387
|
+
because the rule takes away a shield's bonus and not its penalty. With `target_unaware`,
|
|
388
|
+
a thief attacker gets the back-stab bonus to hit and to damage, and the attack's
|
|
384
389
|
[`AttackRolledEvent`][osrlib.core.events.AttackRolledEvent] has `back_stab` set.
|
|
385
390
|
"""
|
|
386
391
|
|
|
@@ -395,7 +400,9 @@ class AttackContext(BaseModel):
|
|
|
395
400
|
defender_retreating: bool = False
|
|
396
401
|
"""Whether the defender is retreating.
|
|
397
402
|
|
|
398
|
-
The attacker gets +2 to hit, and the defender's shield doesn't count
|
|
403
|
+
The attacker gets +2 to hit, and the defender's shield doesn't count, as it doesn't with
|
|
404
|
+
`behind_target`. The attack is made against the armour class the defender would have
|
|
405
|
+
without the shield, a magic shield's enchantment included, but never a better one. A
|
|
399
406
|
[`GameSession`][osrlib.crawl.session.GameSession] sets it for you in two cases:
|
|
400
407
|
|
|
401
408
|
- On an attack against a monster group that's fleeing.
|
|
@@ -1356,12 +1363,12 @@ def _defender_descending_ac(defender: Any, context: AttackContext, *, missile: b
|
|
|
1356
1363
|
ac = getattr(defender, "armour_class", None)
|
|
1357
1364
|
if ac is None:
|
|
1358
1365
|
return None
|
|
1359
|
-
#
|
|
1366
|
+
# A retreating defender, or one attacked from behind, loses the shield's bonus, a magic
|
|
1367
|
+
# shield's enchantment included. A cursed shield's penalty has no bonus to lose, so it stays.
|
|
1360
1368
|
if context.defender_retreating or context.behind_target:
|
|
1361
1369
|
inventory = getattr(defender, "inventory", None)
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
ac += shield.template.ac_bonus
|
|
1370
|
+
_, shield_bonus = _shield_ac_parts(getattr(inventory, "shield", None), ascending=False)
|
|
1371
|
+
ac += max(shield_bonus, 0)
|
|
1365
1372
|
if context.attacker_large:
|
|
1366
1373
|
params = _class_ability_params(defender, "defensive_bonus")
|
|
1367
1374
|
if params is not None:
|
|
@@ -1412,7 +1419,8 @@ def attack_roll(
|
|
|
1412
1419
|
- The context's `situational_modifier`.
|
|
1413
1420
|
|
|
1414
1421
|
The function adjusts the defender's armour class too. The defender's shield doesn't
|
|
1415
|
-
count against an attack from behind, or while the defender retreats
|
|
1422
|
+
count against an attack from behind, or while the defender retreats, and a magic
|
|
1423
|
+
shield's enchantment doesn't count either. The context's
|
|
1416
1424
|
`defender_ally_ac_bonus`, an armour class bonus that an ally gives the defender, does
|
|
1417
1425
|
count.
|
|
1418
1426
|
|
|
@@ -1139,7 +1139,7 @@ class SpellDisruptedEvent(Event):
|
|
|
1139
1139
|
|
|
1140
1140
|
|
|
1141
1141
|
class DeclarationFizzledEvent(Event):
|
|
1142
|
-
"""A declared wand, staff, or rod use,
|
|
1142
|
+
"""A declared wand, staff, or rod use, a declared turning, or a declared melee attack failed before it took effect.
|
|
1143
1143
|
|
|
1144
1144
|
A [`GameSession`][osrlib.crawl.session.GameSession] reports it during a battle round. At the start of each
|
|
1145
1145
|
round, every party member declares an action in a
|
|
@@ -1150,36 +1150,53 @@ class DeclarationFizzledEvent(Event):
|
|
|
1150
1150
|
caused the failure. For example, the group a staff of striking was aimed at moved out of melee reach, or the
|
|
1151
1151
|
group a wand was aimed at left the battle.
|
|
1152
1152
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1153
|
+
A melee attack fizzles when the party's melee step comes and the attack has nothing left to strike, or the
|
|
1154
|
+
member stands in the wrong place. The session judges a melee attack before the round starts, as if every move the
|
|
1155
|
+
round declares had happened. When the attack comes up, it resolves only if its group still has a living monster
|
|
1156
|
+
the member can see to strike, and the member stands in the group's front rank within 5' of it. Something earlier
|
|
1157
|
+
in the round can change either. For example, the party's missiles or spells kill every monster in the group, the
|
|
1158
|
+
monsters win initiative and the group runs before the member's `close`, or a member the session expected to step
|
|
1159
|
+
back can no longer move. Then the attack fizzles with the code `battle.melee.fizzled`. A group that has started to
|
|
1160
|
+
flee is an exception to the position check: a member who stood within 5' of it when the round was accepted still
|
|
1161
|
+
gets the attack, if the group has a monster left to strike and hasn't run out of the battle.
|
|
1162
|
+
|
|
1163
|
+
The event's `actor_id` is the member who declared the action, `item_id` is the device, `group_id` is the group a
|
|
1164
|
+
melee attack was aimed at, and `reason` is the rejection code of the check that failed.
|
|
1155
1165
|
|
|
1156
1166
|
A fizzled declaration spends nothing. The device wasn't used, so it keeps its charge and isn't identified. Tell
|
|
1157
|
-
the player the action failed, and that the device still has its charge.
|
|
1167
|
+
the player the action failed, and that the device still has its charge. A fizzled melee attack makes no attack
|
|
1168
|
+
roll, so tell the player why the member didn't swing. When a cast or a scroll read fizzles,
|
|
1158
1169
|
the session reports a [`SpellDisruptedEvent`][osrlib.core.events.SpellDisruptedEvent] instead, because the
|
|
1159
1170
|
spell is lost.
|
|
1160
1171
|
"""
|
|
1161
1172
|
|
|
1162
|
-
allowed_codes: ClassVar[frozenset[str]] = frozenset(
|
|
1163
|
-
|
|
1164
|
-
|
|
1173
|
+
allowed_codes: ClassVar[frozenset[str]] = frozenset(
|
|
1174
|
+
{"magic.device.fizzled", "magic.turning.fizzled", "battle.melee.fizzled"}
|
|
1175
|
+
)
|
|
1176
|
+
"""The three codes: `magic.device.fizzled` for a wand, staff, or rod use, `magic.turning.fizzled` for a
|
|
1177
|
+
turning, and `battle.melee.fizzled` for a melee attack."""
|
|
1165
1178
|
|
|
1166
1179
|
event_type: Literal["declaration_fizzled"] = "declaration_fizzled"
|
|
1167
1180
|
"""The fixed string that identifies this event class in a serialized event."""
|
|
1168
1181
|
|
|
1169
1182
|
code: str
|
|
1170
|
-
"""Which declaration fizzled: `magic.device.fizzled` or `
|
|
1183
|
+
"""Which declaration fizzled: `magic.device.fizzled`, `magic.turning.fizzled`, or `battle.melee.fizzled`."""
|
|
1171
1184
|
|
|
1172
1185
|
visibility: Visibility = Visibility.PLAYER
|
|
1173
1186
|
"""Player visibility: everyone sees the action fail."""
|
|
1174
1187
|
|
|
1175
1188
|
actor_id: str
|
|
1176
|
-
"""The entity id of the member who declared the device use or the
|
|
1189
|
+
"""The entity id of the member who declared the device use, the turning, or the melee attack."""
|
|
1177
1190
|
|
|
1178
1191
|
item_id: str | None = None
|
|
1179
1192
|
"""On a `magic.device.fizzled` event, the device's instance id, as the declaration's `item_id` gave it.
|
|
1180
1193
|
|
|
1181
1194
|
The player already used this id, so it reveals nothing about a device that isn't identified. It's `None` on a
|
|
1182
|
-
`magic.turning.fizzled` event."""
|
|
1195
|
+
`magic.turning.fizzled` or a `battle.melee.fizzled` event."""
|
|
1196
|
+
|
|
1197
|
+
group_id: str | None = None
|
|
1198
|
+
"""On a `battle.melee.fizzled` event, the id of the group the melee attack was aimed at, as the declaration's
|
|
1199
|
+
`target_group_id` gave it. It's `None` on the other codes."""
|
|
1183
1200
|
|
|
1184
1201
|
reason: str
|
|
1185
1202
|
"""Why the declaration fizzled: the code of the first check that failed when the session checked it again.
|
|
@@ -1187,9 +1204,17 @@ class DeclarationFizzledEvent(Event):
|
|
|
1187
1204
|
Any check the declaration passed at the start of the round can fail, so read the code and don't assume a
|
|
1188
1205
|
cause. Some codes you may see:
|
|
1189
1206
|
|
|
1190
|
-
- `combat.attack.out_of_reach`: the group a staff of striking was aimed at moved out of reach
|
|
1207
|
+
- `combat.attack.out_of_reach`: the group a staff of striking was aimed at moved out of reach, or the member
|
|
1208
|
+
who declared a melee attack is farther than 5' from its group when the attack comes up. A melee attack gets
|
|
1209
|
+
it too when its group ran out of the battle, wherever the member stood.
|
|
1210
|
+
- `battle.declaration.not_in_front_rank`: the member who declared a melee attack is within 5' of its group
|
|
1211
|
+
but not in its front rank when the attack comes up. A melee attack gets this code only when the member is
|
|
1212
|
+
within 5', and `combat.attack.out_of_reach` otherwise.
|
|
1191
1213
|
- `battle.declaration.unknown_group`: the group a wand was aimed at left the battle.
|
|
1192
|
-
- `battle.declaration.no_target`: no living monster is left in the battle for a turning
|
|
1214
|
+
- `battle.declaration.no_target`: no living monster is left in the battle for a turning, or no living monster
|
|
1215
|
+
the member can see is left in the group a melee attack was aimed at, for example because the party's missiles
|
|
1216
|
+
or spells killed them earlier in the round. A melee attack gets this code wherever the member stands, whether
|
|
1217
|
+
or not the member closed, because the session checks for a target before it checks the member's position.
|
|
1193
1218
|
|
|
1194
1219
|
Match on it as you match on the code of a [`Rejection`][osrlib.core.validation.Rejection].
|
|
1195
1220
|
[The rejection code reference][rejection-codes] explains each code.
|
|
@@ -2254,6 +2254,28 @@ def magic_item_template(instance: MagicItemInstance) -> MagicItemTemplate:
|
|
|
2254
2254
|
return load_magic_items().get(instance.template_id)
|
|
2255
2255
|
|
|
2256
2256
|
|
|
2257
|
+
def _shield_ac_parts(shield: AnyInstance | None, *, ascending: bool) -> tuple[int | None, int]:
|
|
2258
|
+
"""Return the armour class a shield sets, or None, and the bonus the shield adds to armour class.
|
|
2259
|
+
|
|
2260
|
+
A mundane shield adds its +1. A magic shield adds its enchantment on top of the +1 an ordinary
|
|
2261
|
+
shield gives, so a cursed shield's negative enchantment can make the sum a penalty. The cursed
|
|
2262
|
+
shield that prints AC 9 [10] sets the wearer's armour class outright and adds nothing.
|
|
2263
|
+
`Character._armour_parts` and the attack roll both read a shield here, so they agree on what it
|
|
2264
|
+
gives.
|
|
2265
|
+
"""
|
|
2266
|
+
from osrlib.data import load_equipment
|
|
2267
|
+
|
|
2268
|
+
if isinstance(shield, MagicItemInstance):
|
|
2269
|
+
template = magic_item_template(shield)
|
|
2270
|
+
if template.ac_set is not None and template.ac_set_ascending is not None:
|
|
2271
|
+
return (template.ac_set_ascending if ascending else template.ac_set), 0
|
|
2272
|
+
mundane = load_equipment().get("shield")
|
|
2273
|
+
return None, (getattr(mundane, "ac_bonus", None) or 0) + template.ac_bonus
|
|
2274
|
+
if shield is not None and isinstance(shield.template, ArmourTemplate) and shield.template.ac_bonus is not None:
|
|
2275
|
+
return None, shield.template.ac_bonus
|
|
2276
|
+
return None, 0
|
|
2277
|
+
|
|
2278
|
+
|
|
2257
2279
|
def equipped_item_modifiers(inventory: Inventory) -> list[ModifierSpec]:
|
|
2258
2280
|
"""Return the stat modifiers a character's equipped magic items grant.
|
|
2259
2281
|
|
|
@@ -263,3 +263,14 @@ class Ruleset(BaseModel):
|
|
|
263
263
|
in a ten-foot passage, and osrlib uses the lower number. Turn the flag off and there's
|
|
264
264
|
no cap, so every combatant may melee.
|
|
265
265
|
"""
|
|
266
|
+
|
|
267
|
+
failed_trap_removal_springs: bool = True
|
|
268
|
+
"""Spring a treasure trap on the thief who fails to remove it. An adaptation, on by default.
|
|
269
|
+
|
|
270
|
+
The SRD says a thief may try only once to remove a treasure trap, and it doesn't say what
|
|
271
|
+
a failure does. Turn the flag off and a failed
|
|
272
|
+
[`RemoveTreasureTrap`][osrlib.crawl.commands.RemoveTreasureTrap] leaves the trap armed.
|
|
273
|
+
The party still knows where it is. That thief can't try again, another thief in the
|
|
274
|
+
party can, and the trap still gets its 2-in-6 chance to spring when someone takes the
|
|
275
|
+
treasure.
|
|
276
|
+
"""
|