Skip to content

Commit 40aef8b

Browse files
thalismindclaude
andcommitted
Add v2 mechanics: sanity dread meter and rituals/wards
Bundle two survival-horror mechanics onto the spectersim detector plugin, bumping it to 0.2.0. Sanity: SanityComponent(current, maximum) on characters, driven by a per-tick SanityConsequence that drains near SpectralMarker entities and in dark rooms, recovers in safe or bright spirit-free rooms (clamped 0..maximum), and emits SanityChangedEvent on band crossings. sanity_fragments inject escalating first-person distortion lines (shaking, whispering, a hallucinated presence at critical) into the afflicted character's own prompt only. Rituals & wards: WardComponent protects a room or placed ward entity; RitualKitComponent is a held banishing tool. WardConsequence passively weakens then banishes (strips SpectralMarkerComponent from) presences in warded rooms. Verbs draw-ward and perform-ritual follow the validate-order convention and emit WardDrawnEvent / PresenceWeakenedEvent / PresenceBanishedEvent. ritual_fragments surface ward protection and the held-kit line. New spawn_ritual_kit / spawn_ward factories. All new surfaces registered in plugin.py; v1 behavior and tests untouched. Adds test_sanity.py and test_rituals.py; 90 tests pass, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c978a2b commit 40aef8b

12 files changed

Lines changed: 1652 additions & 17 deletions

File tree

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ particular marker component share its room**. Two variants ship out of the box:
88
- **Ghost detector** — reacts to `SpectralMarkerComponent` (monsters, spirits, the undead).
99
- **Radio** — reacts to `RadioSourceMarkerComponent` (transmitters, beacons, broadcasters).
1010

11+
**v2** bundles two survival-horror mechanics on top of the detectors:
12+
13+
- **Sanity** — a dread meter (`SanityComponent`) that drains near spectral presences and in
14+
the dark, recovers in safe or bright spirit-free rooms, and injects escalating first-person
15+
distortion lines ("Your hands won't stop shaking.", whispering, a hallucinated presence)
16+
into the afflicted character's own prompt.
17+
- **Rituals & wards**`draw-ward` protects a room and `perform-ritual` channels a held
18+
ritual kit to weaken and finally banish a spectral presence; a warded room also erodes any
19+
trapped presence passively.
20+
1121
This repo intentionally keeps all detector work outside the main `bunnyland-server` repo.
1222

1323
## Layout
@@ -30,6 +40,17 @@ The plugin exposes `bunnyland_spectersim.bunnyland_plugins()` and contributes:
3040
- `power-detector` and `set-detector-volume` - verbs for the holder (human or AI).
3141
- `spawn_ghost_detector`, `spawn_radio` - spawn factories.
3242

43+
v2 additionally contributes:
44+
45+
- `SanityComponent` and `SanityConsequence` - the per-tick dread meter, with band-crossing
46+
`SanityChangedEvent`s and first-person `sanity_fragments`.
47+
- `WardComponent`, `RitualKitComponent`, and `WardConsequence` - room protection plus the
48+
passive weaken/banish pass, with `WardDrawnEvent`, `PresenceWeakenedEvent`, and
49+
`PresenceBanishedEvent`.
50+
- `draw-ward` and `perform-ritual` - verbs for placing wards and banishing presences.
51+
- `ritual_fragments` - renders ward protection and the held-kit line into prompts.
52+
- `spawn_ritual_kit`, `spawn_ward` - spawn factories.
53+
3354
## Running
3455

3556
This package builds no containers. It is loaded into the stock server via `--module`:

server/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,20 @@ bunnyland serve --module bunnyland_spectersim
4141
- **Two verbs**`power-detector` and `set-detector-volume` — usable by the holder
4242
(human or AI).
4343
- **Spawn factories**`spawn_ghost_detector`, `spawn_radio`.
44+
45+
### v2 — sanity and rituals
46+
47+
- **Sanity**`SanityComponent(current, maximum)` on characters and a per-tick
48+
`SanityConsequence` that drains sanity near `SpectralMarkerComponent` entities and in dark
49+
rooms, recovers it in safe or bright spirit-free rooms (clamped `0..maximum`), and emits a
50+
`SanityChangedEvent` on band crossings. `sanity_fragments` inject escalating first-person
51+
distortion lines at low sanity (shaking, whispering, and a hallucinated presence at
52+
critical), visible only to the afflicted character.
53+
- **Rituals & wards**`WardComponent` marks a room (or a placed ward entity) as protected;
54+
`RitualKitComponent` is a held banishing tool. `WardConsequence` passively weakens then
55+
banishes (strips the `SpectralMarkerComponent` from) any presence caught in a warded room.
56+
Verbs `draw-ward` (place a ward, optionally consuming a held reagent) and `perform-ritual`
57+
(channel a held kit to weaken/banish a presence in the room) emit `WardDrawnEvent`,
58+
`PresenceWeakenedEvent`, and `PresenceBanishedEvent`. `ritual_fragments` render ward
59+
protection and the held-kit line.
60+
- **Spawn factories**`spawn_ritual_kit`, `spawn_ward`.

server/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "bunnyland-spectersim"
3-
version = "0.1.0"
4-
description = "Out-of-tree Bunnyland plugin: Silent Hill / Ghost Hunters-style monster-detecting devices (ghost detector + radio)."
3+
version = "0.2.0"
4+
description = "Out-of-tree Bunnyland plugin: Silent Hill / Ghost Hunters-style monster-detecting devices, plus a sanity meter and banishing rituals/wards."
55
readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [

server/src/bunnyland_spectersim/__init__.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
"""Out-of-tree Bunnyland plugin: monster-detecting devices (ghost detector + radio)."""
1+
"""Out-of-tree Bunnyland plugin: monster-detecting devices (ghost detector + radio).
2+
3+
v2 adds two bundled mechanics: a **sanity** dread meter and **rituals/wards** for banishing
4+
spectral presences.
5+
"""
26

37
from .commands import PowerDetectorHandler, SetDetectorVolumeHandler
48
from .components import (
@@ -14,9 +18,27 @@
1418
from .enrichment import SpecterWorldgenHook
1519
from .events import DetectorPoweredEvent, DetectorVolumeSetEvent
1620
from .fragments import spectersim_fragments
17-
from .install import install_spectersim
21+
from .install import install_spectersim, install_spectersim_v2
1822
from .plugin import PLUGIN_ID, bunnyland_plugins, plugin
19-
from .prefabs import spawn_ghost_detector, spawn_radio
23+
from .prefabs import spawn_ghost_detector, spawn_radio, spawn_ritual_kit, spawn_ward
24+
from .rituals import (
25+
DrawWardHandler,
26+
PerformRitualHandler,
27+
PresenceBanishedEvent,
28+
PresenceWeakenedEvent,
29+
RitualKitComponent,
30+
WardComponent,
31+
WardConsequence,
32+
WardDrawnEvent,
33+
ritual_fragments,
34+
)
35+
from .sanity import (
36+
SanityChangedEvent,
37+
SanityComponent,
38+
SanityConsequence,
39+
sanity_band,
40+
sanity_fragments,
41+
)
2042
from .spatial import holder_of, room_of
2143

2244
__all__ = [
@@ -26,20 +48,37 @@
2648
"DetectionConsequence",
2749
"DetectorPoweredEvent",
2850
"DetectorVolumeSetEvent",
51+
"DrawWardHandler",
2952
"GhostDetectorComponent",
53+
"PerformRitualHandler",
3054
"PowerDetectorHandler",
55+
"PresenceBanishedEvent",
56+
"PresenceWeakenedEvent",
3157
"RadioDetectorComponent",
3258
"RadioSourceMarkerComponent",
59+
"RitualKitComponent",
60+
"SanityChangedEvent",
61+
"SanityComponent",
62+
"SanityConsequence",
3363
"SetDetectorVolumeHandler",
3464
"SpecterWorldgenHook",
3565
"SpectralMarkerComponent",
66+
"WardComponent",
67+
"WardConsequence",
68+
"WardDrawnEvent",
3669
"bunnyland_plugins",
3770
"detector_component_of",
3871
"holder_of",
3972
"install_spectersim",
73+
"install_spectersim_v2",
4074
"plugin",
75+
"ritual_fragments",
4176
"room_of",
77+
"sanity_band",
78+
"sanity_fragments",
4279
"spawn_ghost_detector",
4380
"spawn_radio",
81+
"spawn_ritual_kit",
82+
"spawn_ward",
4483
"spectersim_fragments",
4584
]
Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
"""Runtime wiring: register the detection consequence on a world actor."""
1+
"""Runtime wiring: register the per-tick consequences on a world actor."""
22

33
from __future__ import annotations
44

55
from bunnyland.core.world_actor import WorldActor
66

77
from .detection import DetectionConsequence
8+
from .rituals import WardConsequence
9+
from .sanity import SanityConsequence
810

911

1012
def install_spectersim(actor: WorldActor) -> None:
11-
"""Register the per-tick detection consequence (a ``service_factories`` entry)."""
13+
"""Register the v1 detection consequence (a ``service_factories`` entry)."""
1214
actor.register_consequence(DetectionConsequence())
1315

1416

15-
__all__ = ["install_spectersim"]
17+
def install_spectersim_v2(actor: WorldActor) -> None:
18+
"""Register the v2 sanity and ward consequences (a ``service_factories`` entry)."""
19+
actor.register_consequence(SanityConsequence())
20+
actor.register_consequence(WardConsequence())
21+
22+
23+
__all__ = ["install_spectersim", "install_spectersim_v2"]

server/src/bunnyland_spectersim/plugin.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,18 @@
2020
from .enrichment import SpecterWorldgenHook
2121
from .events import DetectorPoweredEvent, DetectorVolumeSetEvent
2222
from .fragments import spectersim_fragments
23-
from .install import install_spectersim
23+
from .install import install_spectersim, install_spectersim_v2
24+
from .rituals import (
25+
RITUAL_ACTION_DEFINITIONS,
26+
RITUAL_ACTION_HANDLERS,
27+
PresenceBanishedEvent,
28+
PresenceWeakenedEvent,
29+
RitualKitComponent,
30+
WardComponent,
31+
WardDrawnEvent,
32+
ritual_fragments,
33+
)
34+
from .sanity import SanityChangedEvent, SanityComponent, sanity_fragments
2435

2536
PLUGIN_ID = "bunnyland_spectersim"
2637

@@ -29,24 +40,36 @@ def plugin() -> Plugin:
2940
return Plugin(
3041
id=PLUGIN_ID,
3142
name="Bunnyland Spectersim",
32-
version="0.1.0",
43+
version="0.2.0",
3344
default_enabled=True,
3445
ecs=EcsContribution(
3546
components=(
3647
SpectralMarkerComponent,
3748
RadioSourceMarkerComponent,
3849
GhostDetectorComponent,
3950
RadioDetectorComponent,
51+
SanityComponent,
52+
WardComponent,
53+
RitualKitComponent,
4054
),
4155
),
4256
commands=CommandContribution(
43-
action_handlers=DETECTOR_ACTION_HANDLERS,
44-
action_definitions=DETECTOR_ACTION_DEFINITIONS,
45-
typed_events=(DetectorPoweredEvent, DetectorVolumeSetEvent),
57+
action_handlers=DETECTOR_ACTION_HANDLERS + RITUAL_ACTION_HANDLERS,
58+
action_definitions=DETECTOR_ACTION_DEFINITIONS + RITUAL_ACTION_DEFINITIONS,
59+
typed_events=(
60+
DetectorPoweredEvent,
61+
DetectorVolumeSetEvent,
62+
SanityChangedEvent,
63+
WardDrawnEvent,
64+
PresenceWeakenedEvent,
65+
PresenceBanishedEvent,
66+
),
67+
),
68+
runtime=RuntimeContribution(
69+
service_factories=(install_spectersim, install_spectersim_v2),
4670
),
47-
runtime=RuntimeContribution(service_factories=(install_spectersim,)),
4871
content=ContentContribution(
49-
prompt_fragments=(spectersim_fragments,),
72+
prompt_fragments=(spectersim_fragments, sanity_fragments, ritual_fragments),
5073
worldgen_hooks=(SpecterWorldgenHook,),
5174
),
5275
)

server/src/bunnyland_spectersim/prefabs.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from relics import Entity, World
2020

2121
from .components import GhostDetectorComponent, RadioDetectorComponent
22+
from .rituals import RitualKitComponent, WardComponent
2223

2324

2425
def _link_into_room(world: World, item: Entity, room_id) -> None:
@@ -57,4 +58,32 @@ def spawn_radio(world: World, *, room_id=None, sound: str = "hiss") -> Entity:
5758
return item
5859

5960

60-
__all__ = ["spawn_ghost_detector", "spawn_radio"]
61+
def spawn_ritual_kit(world: World, *, room_id=None, potency: float = 0.5) -> Entity:
62+
"""Spawn a holdable ritual kit item, optionally placed in ``room_id``."""
63+
item = spawn_entity(
64+
world,
65+
[
66+
IdentityComponent(name="ritual kit", kind="item", tags=("spectersim",)),
67+
PortableComponent(),
68+
HoldableComponent(slot="hand"),
69+
RitualKitComponent(potency=potency),
70+
],
71+
)
72+
_link_into_room(world, item, room_id)
73+
return item
74+
75+
76+
def spawn_ward(world: World, *, room_id=None, strength: float = 1.0) -> Entity:
77+
"""Spawn a standalone ward entity, optionally placed in ``room_id`` to protect it."""
78+
ward = spawn_entity(
79+
world,
80+
[
81+
IdentityComponent(name="ward", kind="ward", tags=("spectersim",)),
82+
WardComponent(strength=strength),
83+
],
84+
)
85+
_link_into_room(world, ward, room_id)
86+
return ward
87+
88+
89+
__all__ = ["spawn_ghost_detector", "spawn_radio", "spawn_ritual_kit", "spawn_ward"]

0 commit comments

Comments
 (0)