osrlib 2.3.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.
Files changed (61) hide show
  1. {osrlib-2.3.0 → osrlib-2.4.0}/PKG-INFO +1 -1
  2. {osrlib-2.3.0 → osrlib-2.4.0}/pyproject.toml +1 -1
  3. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/ruleset.py +11 -0
  4. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/commands.py +6 -2
  5. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/dungeon.py +4 -2
  6. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/events.py +22 -6
  7. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/exploration.py +6 -2
  8. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/messages.py +3 -0
  9. {osrlib-2.3.0 → osrlib-2.4.0}/LICENSE +0 -0
  10. {osrlib-2.3.0 → osrlib-2.4.0}/LICENSE-OGL.md +0 -0
  11. {osrlib-2.3.0 → osrlib-2.4.0}/README.md +0 -0
  12. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/__init__.py +0 -0
  13. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/__init__.py +0 -0
  14. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/abilities.py +0 -0
  15. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/alignment.py +0 -0
  16. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/character.py +0 -0
  17. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/classes.py +0 -0
  18. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/clock.py +0 -0
  19. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/combat.py +0 -0
  20. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/creature.py +0 -0
  21. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/dice.py +0 -0
  22. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/effects.py +0 -0
  23. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/events.py +0 -0
  24. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/items.py +0 -0
  25. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/monsters.py +0 -0
  26. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/npc.py +0 -0
  27. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/rng.py +0 -0
  28. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/spells.py +0 -0
  29. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/tables.py +0 -0
  30. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/treasure.py +0 -0
  31. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/core/validation.py +0 -0
  32. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/__init__.py +0 -0
  33. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/adventure.py +0 -0
  34. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/battle.py +0 -0
  35. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/content_pack.py +0 -0
  36. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/encounter.py +0 -0
  37. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/gates.py +0 -0
  38. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/interpreter.py +0 -0
  39. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/narrative.py +0 -0
  40. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/party.py +0 -0
  41. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/quests.py +0 -0
  42. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/session.py +0 -0
  43. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/stocking.py +0 -0
  44. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/triggers.py +0 -0
  45. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/crawl/views.py +0 -0
  46. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/data/LICENSE-OGL.md +0 -0
  47. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/data/__init__.py +0 -0
  48. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/data/abilities.json +0 -0
  49. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/data/classes.json +0 -0
  50. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/data/combat_tables.json +0 -0
  51. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/data/encounter_tables.json +0 -0
  52. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/data/equipment.json +0 -0
  53. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/data/languages.json +0 -0
  54. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/data/magic_items.json +0 -0
  55. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/data/monsters.json +0 -0
  56. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/data/spells.json +0 -0
  57. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/data/treasure.json +0 -0
  58. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/errors.py +0 -0
  59. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/persistence.py +0 -0
  60. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/py.typed +0 -0
  61. {osrlib-2.3.0 → osrlib-2.4.0}/src/osrlib/versioning.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osrlib
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: B/X (1981 Basic/Expert) tabletop RPG rules engine for turn-based dungeon crawlers
5
5
  Keywords: basic-expert,bx,dungeon-crawler,game-engine,osr,rpg,srd,ttrpg
6
6
  Author: Marsh Macy
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "osrlib"
3
- version = "2.3.0"
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
@@ -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
+ """
@@ -1030,7 +1030,10 @@ class RemoveTreasureTrap(Command):
1030
1030
  isn't enough. A member must already have found the trap with
1031
1031
  [`InspectTreasure`][osrlib.crawl.commands.InspectTreasure]. Only a member with thief
1032
1032
  skills can try, and each member gets one attempt at each trap. A failed attempt springs
1033
- the trap on the thief who tried. Once the trap is removed,
1033
+ the trap on the thief who tried, unless
1034
+ [`Ruleset.failed_trap_removal_springs`][osrlib.core.ruleset.Ruleset.failed_trap_removal_springs]
1035
+ is off. With the flag off, the trap stays armed and found, and another thief in the party
1036
+ can try. Once the trap is removed,
1034
1037
  [`TakeTreasure`][osrlib.crawl.commands.TakeTreasure] empties the cache safely.
1035
1038
 
1036
1039
  Modes:
@@ -1054,7 +1057,8 @@ class RemoveTreasureTrap(Command):
1054
1057
  Events:
1055
1058
  [`DetectionRolledEvent`][osrlib.crawl.events.DetectionRolledEvent] with the
1056
1059
  skill roll, then a [`TrapEvent`][osrlib.crawl.events.TrapEvent]:
1057
- `exploration.trap.removed` on success, `exploration.trap.sprung` on failure.
1060
+ `exploration.trap.removed` on success. A failure gives `exploration.trap.sprung`, or
1061
+ `exploration.trap.removal_failed` when `failed_trap_removal_springs` is off.
1058
1062
  The sprung trap resolves at once against the thief, or against every living
1059
1063
  member when the trap catches the whole party. Each step is a separate event:
1060
1064
  a [`SavingThrowRolledEvent`][osrlib.core.events.SavingThrowRolledEvent] when
@@ -1040,7 +1040,8 @@ class FeatureSpec(BaseModel):
1040
1040
  party opens the cache. A thief finds it with
1041
1041
  [`InspectTreasure`][osrlib.crawl.commands.InspectTreasure] and takes it out with
1042
1042
  [`RemoveTreasureTrap`][osrlib.crawl.commands.RemoveTreasureTrap]. Each character gets one try at
1043
- each, and a failed removal springs the trap."""
1043
+ each. A failed removal springs the trap, unless
1044
+ [`Ruleset.failed_trap_removal_springs`][osrlib.core.ruleset.Ruleset.failed_trap_removal_springs] is off."""
1044
1045
 
1045
1046
  @model_validator(mode="after")
1046
1047
  def _trap_kind_matches(self) -> FeatureSpec:
@@ -1893,7 +1894,8 @@ class DungeonState(BaseModel):
1893
1894
  feature reference. Each character gets one try at each cache."""
1894
1895
  removal_attempts: dict[str, list[str]] = {}
1895
1896
  """Which characters have tried to remove each treasure trap, as lists of character ids keyed by
1896
- feature reference. Each character gets one try at each trap, and a failed try springs it."""
1897
+ feature reference. Each character gets one try at each trap. A failed try springs the trap, unless
1898
+ [`Ruleset.failed_trap_removal_springs`][osrlib.core.ruleset.Ruleset.failed_trap_removal_springs] is off."""
1897
1899
  lock_failures: dict[str, dict[str, int]] = {}
1898
1900
  """Which thieves failed to pick each lock, keyed by [`edge_ref`][osrlib.crawl.dungeon.edge_ref].
1899
1901
  Each value maps a character id to the thief's level at the time. A thief who failed a lock can
@@ -448,7 +448,7 @@ class SearchCompletedEvent(Event):
448
448
 
449
449
 
450
450
  class TrapEvent(Event):
451
- """A trap sprang, was found, was removed, or was known and didn't spring.
451
+ """A trap sprang, was found, was removed, resisted removal, or was known and didn't spring.
452
452
 
453
453
  Emitted by the commands that can set a trap off or look for one:
454
454
  [`MoveParty`][osrlib.crawl.commands.MoveParty],
@@ -478,14 +478,29 @@ class TrapEvent(Event):
478
478
  [`RemoveTreasureTrap`][osrlib.crawl.commands.RemoveTreasureTrap], because finding a
479
479
  treasure trap doesn't disarm it. That is why `exploration.trap.safe`, a known trap
480
480
  getting its chance and not springing, appears only for a treasure cache.
481
+
482
+ A thief who fails to remove a treasure trap springs it, unless
483
+ [`Ruleset.failed_trap_removal_springs`][osrlib.core.ruleset.Ruleset.failed_trap_removal_springs]
484
+ is off. With the flag off, the failure emits `exploration.trap.removal_failed` and the trap
485
+ stays armed and found.
481
486
  """
482
487
 
483
488
  allowed_codes: ClassVar[frozenset[str]] = frozenset(
484
- {"exploration.trap.sprung", "exploration.trap.safe", "exploration.trap.found", "exploration.trap.removed"}
489
+ {
490
+ "exploration.trap.sprung",
491
+ "exploration.trap.safe",
492
+ "exploration.trap.found",
493
+ "exploration.trap.removed",
494
+ "exploration.trap.removal_failed",
495
+ }
485
496
  )
486
497
  """`exploration.trap.sprung` when a trap goes off, `.found` when a search or inspection
487
- reveals one, `.removed` when a thief disarms one, and `.safe` when a trap the party already
488
- knows about got its chance and didn't fire, which only a treasure trap does."""
498
+ reveals one, `.removed` when a thief disarms one, `.removal_failed` when a thief fails to
499
+ disarm one and it doesn't spring, and `.safe` when a trap the party already knows about got
500
+ its chance and didn't fire, which only a treasure trap does. A failed removal gives
501
+ `.removal_failed` only when
502
+ [`Ruleset.failed_trap_removal_springs`][osrlib.core.ruleset.Ruleset.failed_trap_removal_springs]
503
+ is off. Otherwise the trap springs."""
489
504
 
490
505
  event_type: Literal["trap"] = "trap"
491
506
  """The event type, `trap`, which identifies this class in serialized form."""
@@ -501,8 +516,9 @@ class TrapEvent(Event):
501
516
  tried to remove the trap. For a trap that springs when the party enters its area or opens its
502
517
  door with [`OpenDoor`][osrlib.crawl.commands.OpenDoor], it's the first living member in
503
518
  marching order. It's set even when the trap hits the whole party. For
504
- `exploration.trap.removed`, and for a treasure trap found with
505
- [`InspectTreasure`][osrlib.crawl.commands.InspectTreasure], it's the thief."""
519
+ `exploration.trap.removed` and `exploration.trap.removal_failed`, and for a treasure trap
520
+ found with [`InspectTreasure`][osrlib.crawl.commands.InspectTreasure], it's the thief who
521
+ tried."""
506
522
  direction: str | None = None
507
523
  """The direction of the door a room trap was found through, from the searched cell, as a
508
524
  [`Direction`][osrlib.crawl.dungeon.Direction] value. Set for that find alone: it is `None` on a
@@ -2452,9 +2452,13 @@ def _handle_remove_treasure_trap(session, command: RemoveTreasureTrap) -> tuple[
2452
2452
  if result.passed:
2453
2453
  state.removed_traps.append(ref)
2454
2454
  events.append(TrapEvent(code="exploration.trap.removed", trap_ref=ref, character_id=member.id))
2455
+ elif not session.ruleset.failed_trap_removal_springs:
2456
+ # RAW says only "attempted once per trap". With failed_trap_removal_springs off, a
2457
+ # failure leaves the trap armed and found, and only this thief is barred from retrying.
2458
+ events.append(TrapEvent(code="exploration.trap.removal_failed", trap_ref=ref, character_id=member.id))
2455
2459
  else:
2456
- # A failed removal springs the trap on the thief, the classic reading.
2457
- # RAW says only "attempted once per trap".
2460
+ # With failed_trap_removal_springs on (the default), a failed removal springs the trap
2461
+ # on the thief, the classic reading. RAW says only "attempted once per trap".
2458
2462
  state.sprung_traps.append(ref)
2459
2463
  events.append(TrapEvent(code="exploration.trap.sprung", trap_ref=ref, character_id=member.id))
2460
2464
  events.extend(_resolve_trap(session, feature.trap, triggerer=member))
@@ -296,6 +296,9 @@ _TEMPLATES: dict[str, Callable[[Any], str]] = {
296
296
  "exploration.trap.removed": lambda event: (
297
297
  (event.character_id or "The party") + f" removes the trap ({event.trap_ref})."
298
298
  ),
299
+ "exploration.trap.removal_failed": lambda event: (
300
+ (event.character_id or "The party") + f" fails to remove the trap ({event.trap_ref})."
301
+ ),
299
302
  "exploration.item.acquired": lambda event: (
300
303
  f"{event.character_id} acquires "
301
304
  + (", ".join(event.item_ids) if event.item_ids else "")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes