osrlib 2.1.0__tar.gz → 2.3.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.1.0 → osrlib-2.3.0}/PKG-INFO +1 -1
- {osrlib-2.1.0 → osrlib-2.3.0}/pyproject.toml +1 -1
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/character.py +5 -16
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/combat.py +29 -14
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/events.py +47 -12
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/items.py +22 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/battle.py +1600 -416
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/commands.py +396 -136
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/encounter.py +13 -6
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/events.py +145 -34
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/exploration.py +3 -1
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/party.py +17 -11
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/session.py +172 -2
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/views.py +172 -22
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/messages.py +27 -4
- {osrlib-2.1.0 → osrlib-2.3.0}/LICENSE +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/LICENSE-OGL.md +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/README.md +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/__init__.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/__init__.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/abilities.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/alignment.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/classes.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/clock.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/creature.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/dice.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/effects.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/monsters.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/npc.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/rng.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/ruleset.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/spells.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/tables.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/treasure.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/core/validation.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/__init__.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/adventure.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/content_pack.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/dungeon.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/gates.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/interpreter.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/narrative.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/quests.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/stocking.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/crawl/triggers.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/data/LICENSE-OGL.md +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/data/__init__.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/data/abilities.json +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/data/classes.json +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/data/combat_tables.json +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/data/encounter_tables.json +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/data/equipment.json +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/data/languages.json +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/data/magic_items.json +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/data/monsters.json +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/data/spells.json +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/data/treasure.json +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/errors.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/persistence.py +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/py.typed +0 -0
- {osrlib-2.1.0 → osrlib-2.3.0}/src/osrlib/versioning.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "osrlib"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.3.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,20 +380,29 @@ 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
|
|
389
|
+
[`AttackRolledEvent`][osrlib.core.events.AttackRolledEvent] has `back_stab` set.
|
|
384
390
|
"""
|
|
385
391
|
|
|
386
392
|
target_unaware: bool = False
|
|
387
393
|
"""Whether the defender is unaware of the attack.
|
|
388
394
|
|
|
389
|
-
With `behind_target`, a thief attacker gets the back-stab bonus to hit and to damage
|
|
395
|
+
With `behind_target`, a thief attacker gets the back-stab bonus to hit and to damage, and
|
|
396
|
+
the attack's [`AttackRolledEvent`][osrlib.core.events.AttackRolledEvent] has `back_stab`
|
|
397
|
+
set.
|
|
390
398
|
"""
|
|
391
399
|
|
|
392
400
|
defender_retreating: bool = False
|
|
393
401
|
"""Whether the defender is retreating.
|
|
394
402
|
|
|
395
|
-
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
|
|
396
406
|
[`GameSession`][osrlib.crawl.session.GameSession] sets it for you in two cases:
|
|
397
407
|
|
|
398
408
|
- On an attack against a monster group that's fleeing.
|
|
@@ -1353,12 +1363,12 @@ def _defender_descending_ac(defender: Any, context: AttackContext, *, missile: b
|
|
|
1353
1363
|
ac = getattr(defender, "armour_class", None)
|
|
1354
1364
|
if ac is None:
|
|
1355
1365
|
return None
|
|
1356
|
-
#
|
|
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.
|
|
1357
1368
|
if context.defender_retreating or context.behind_target:
|
|
1358
1369
|
inventory = getattr(defender, "inventory", None)
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
ac += shield.template.ac_bonus
|
|
1370
|
+
_, shield_bonus = _shield_ac_parts(getattr(inventory, "shield", None), ascending=False)
|
|
1371
|
+
ac += max(shield_bonus, 0)
|
|
1362
1372
|
if context.attacker_large:
|
|
1363
1373
|
params = _class_ability_params(defender, "defensive_bonus")
|
|
1364
1374
|
if params is not None:
|
|
@@ -1399,7 +1409,8 @@ def attack_roll(
|
|
|
1399
1409
|
modifier from the context's `distance_feet`, and, for a halfling attacker, the
|
|
1400
1410
|
halfling's missile bonus.
|
|
1401
1411
|
- A thief's back-stab bonus, when the context sets both `behind_target` and
|
|
1402
|
-
`target_unaware`.
|
|
1412
|
+
`target_unaware`. The event's `back_stab` field is then true, on an automatic hit
|
|
1413
|
+
too.
|
|
1403
1414
|
- +2 against a retreating defender.
|
|
1404
1415
|
- A magic weapon's attack bonus, or its bonus against the kind of monster it's made
|
|
1405
1416
|
against.
|
|
@@ -1408,7 +1419,8 @@ def attack_roll(
|
|
|
1408
1419
|
- The context's `situational_modifier`.
|
|
1409
1420
|
|
|
1410
1421
|
The function adjusts the defender's armour class too. The defender's shield doesn't
|
|
1411
|
-
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
|
|
1412
1424
|
`defender_ally_ac_bonus`, an armour class bonus that an ally gives the defender, does
|
|
1413
1425
|
count.
|
|
1414
1426
|
|
|
@@ -1478,12 +1490,16 @@ def attack_roll(
|
|
|
1478
1490
|
name = _attack_name(attack)
|
|
1479
1491
|
missile = _is_missile_use(attack, context) or (isinstance(attack, MonsterAttack) and context.monster_missile)
|
|
1480
1492
|
helpless = not missile and any(has_condition(defender, condition) for condition in _HELPLESS)
|
|
1493
|
+
back_stab = (
|
|
1494
|
+
_class_ability_params(attacker, "back_stab") if context.behind_target and context.target_unaware else None
|
|
1495
|
+
)
|
|
1481
1496
|
if helpless or getattr(defender, "armour_class", 0) is None:
|
|
1482
1497
|
event = AttackRolledEvent(
|
|
1483
1498
|
code="combat.attack.auto_hit",
|
|
1484
1499
|
attacker_id=attacker_id,
|
|
1485
1500
|
defender_id=defender_id,
|
|
1486
1501
|
attack_name=name,
|
|
1502
|
+
back_stab=back_stab is not None,
|
|
1487
1503
|
)
|
|
1488
1504
|
return AttackRollResult(hit=True, auto=True, events=(event,))
|
|
1489
1505
|
|
|
@@ -1497,10 +1513,8 @@ def attack_roll(
|
|
|
1497
1513
|
modifier += int(halfling.get("bonus", 0))
|
|
1498
1514
|
else:
|
|
1499
1515
|
modifier += melee_modifier_for(attacker)
|
|
1500
|
-
if
|
|
1501
|
-
|
|
1502
|
-
if back_stab is not None:
|
|
1503
|
-
modifier += int(back_stab.get("attack_bonus", 0))
|
|
1516
|
+
if back_stab is not None:
|
|
1517
|
+
modifier += int(back_stab.get("attack_bonus", 0))
|
|
1504
1518
|
if context.defender_retreating:
|
|
1505
1519
|
modifier += 2
|
|
1506
1520
|
# An enchanted arm's bonus (versus-clauses swapping in their alternate).
|
|
@@ -1548,6 +1562,7 @@ def attack_roll(
|
|
|
1548
1562
|
required=required,
|
|
1549
1563
|
defender_ac=ac,
|
|
1550
1564
|
natural=natural_override,
|
|
1565
|
+
back_stab=back_stab is not None,
|
|
1551
1566
|
)
|
|
1552
1567
|
return AttackRollResult(
|
|
1553
1568
|
hit=hit,
|
|
@@ -340,6 +340,16 @@ class AttackRolledEvent(Event):
|
|
|
340
340
|
A natural 20 always hits and a natural 1 always misses. This field is set only when that rule changed the
|
|
341
341
|
outcome: a 20 whose total was below `required`, or a 1 whose total reached it."""
|
|
342
342
|
|
|
343
|
+
back_stab: bool = False
|
|
344
|
+
"""True when the attack was a thief's back-stab, so your game can say so.
|
|
345
|
+
|
|
346
|
+
[`attack_roll`][osrlib.core.combat.attack_roll] sets it when the attack's
|
|
347
|
+
[`AttackContext`][osrlib.core.combat.AttackContext] sets both `behind_target` and `target_unaware`, and the
|
|
348
|
+
attacker's class has the `back_stab` ability. The back-stab's bonus to hit is part of `modifier`, and the damage
|
|
349
|
+
that follows is doubled. It's true on an automatic hit too, because the damage still doubles. A
|
|
350
|
+
[`GameSession`][osrlib.crawl.session.GameSession] makes that attack for a member who declares
|
|
351
|
+
[`BattleDeclaration.back_stab`][osrlib.crawl.commands.BattleDeclaration.back_stab]."""
|
|
352
|
+
|
|
343
353
|
|
|
344
354
|
class DamageDealtEvent(Event):
|
|
345
355
|
"""Damage dealt to a creature.
|
|
@@ -1129,7 +1139,7 @@ class SpellDisruptedEvent(Event):
|
|
|
1129
1139
|
|
|
1130
1140
|
|
|
1131
1141
|
class DeclarationFizzledEvent(Event):
|
|
1132
|
-
"""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.
|
|
1133
1143
|
|
|
1134
1144
|
A [`GameSession`][osrlib.crawl.session.GameSession] reports it during a battle round. At the start of each
|
|
1135
1145
|
round, every party member declares an action in a
|
|
@@ -1140,36 +1150,53 @@ class DeclarationFizzledEvent(Event):
|
|
|
1140
1150
|
caused the failure. For example, the group a staff of striking was aimed at moved out of melee reach, or the
|
|
1141
1151
|
group a wand was aimed at left the battle.
|
|
1142
1152
|
|
|
1143
|
-
|
|
1144
|
-
|
|
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.
|
|
1145
1165
|
|
|
1146
1166
|
A fizzled declaration spends nothing. The device wasn't used, so it keeps its charge and isn't identified. Tell
|
|
1147
|
-
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,
|
|
1148
1169
|
the session reports a [`SpellDisruptedEvent`][osrlib.core.events.SpellDisruptedEvent] instead, because the
|
|
1149
1170
|
spell is lost.
|
|
1150
1171
|
"""
|
|
1151
1172
|
|
|
1152
|
-
allowed_codes: ClassVar[frozenset[str]] = frozenset(
|
|
1153
|
-
|
|
1154
|
-
|
|
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."""
|
|
1155
1178
|
|
|
1156
1179
|
event_type: Literal["declaration_fizzled"] = "declaration_fizzled"
|
|
1157
1180
|
"""The fixed string that identifies this event class in a serialized event."""
|
|
1158
1181
|
|
|
1159
1182
|
code: str
|
|
1160
|
-
"""Which declaration fizzled: `magic.device.fizzled` or `
|
|
1183
|
+
"""Which declaration fizzled: `magic.device.fizzled`, `magic.turning.fizzled`, or `battle.melee.fizzled`."""
|
|
1161
1184
|
|
|
1162
1185
|
visibility: Visibility = Visibility.PLAYER
|
|
1163
1186
|
"""Player visibility: everyone sees the action fail."""
|
|
1164
1187
|
|
|
1165
1188
|
actor_id: str
|
|
1166
|
-
"""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."""
|
|
1167
1190
|
|
|
1168
1191
|
item_id: str | None = None
|
|
1169
1192
|
"""On a `magic.device.fizzled` event, the device's instance id, as the declaration's `item_id` gave it.
|
|
1170
1193
|
|
|
1171
1194
|
The player already used this id, so it reveals nothing about a device that isn't identified. It's `None` on a
|
|
1172
|
-
`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."""
|
|
1173
1200
|
|
|
1174
1201
|
reason: str
|
|
1175
1202
|
"""Why the declaration fizzled: the code of the first check that failed when the session checked it again.
|
|
@@ -1177,9 +1204,17 @@ class DeclarationFizzledEvent(Event):
|
|
|
1177
1204
|
Any check the declaration passed at the start of the round can fail, so read the code and don't assume a
|
|
1178
1205
|
cause. Some codes you may see:
|
|
1179
1206
|
|
|
1180
|
-
- `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.
|
|
1181
1213
|
- `battle.declaration.unknown_group`: the group a wand was aimed at left the battle.
|
|
1182
|
-
- `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.
|
|
1183
1218
|
|
|
1184
1219
|
Match on it as you match on the code of a [`Rejection`][osrlib.core.validation.Rejection].
|
|
1185
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
|
|