First sub-project of the "make contact feel real" arc (spec: docs/superpowers/specs/2026-07-23-possession-presentation-design.md). Direct Contact replies now feel like a spirit fighting through static to hold the channel rather than a plain chat bubble: - backend/app/possession.py: compute_stability(rarity, magnitude, rng) — a 0.05-0.98 score per reply (rarer entity + stronger triggering anomaly = cleaner signal), rng injectable for a later quantum-RNG source. - ws.py sends stability on reply_start; audio synthesis for that reply gets noise/bitcrush scaled by instability (1 - stability) via a new instability param on synthesize_spirit_voice — effects.py itself is untouched, only the params fed into it. - frontend/src/lib/possession.ts: renderPossessedText — pure, deterministic (tick-seeded, no Math.random) text corruption with self-correcting glitch bursts, wired into Transcript.tsx's streaming reply display. Stored transcript/reply text is unaffected — this is presentation only. 78/78 backend, 137/137 frontend tests passing.
40 lines
1.4 KiB
Python
40 lines
1.4 KiB
Python
"""Possession stability: a single number expressing how cleanly a spirit
|
|
holds the channel for one reply. Shared contract between the backend audio
|
|
degradation and the frontend text-glitch renderer (see
|
|
docs/superpowers/specs/2026-07-23-possession-presentation-design.md)."""
|
|
|
|
import random
|
|
from typing import Callable
|
|
|
|
_BASE_BY_RARITY = {
|
|
"common": 0.35,
|
|
"uncommon": 0.5,
|
|
"rare": 0.65,
|
|
"mythic": 0.8,
|
|
}
|
|
|
|
_MIN_STABILITY = 0.05
|
|
_MAX_STABILITY = 0.98
|
|
_MAX_MAGNITUDE_BONUS = 0.15
|
|
_JITTER_SPAN = 0.1 # rng() in [0, 1) is scaled to +/- this amount
|
|
|
|
|
|
def compute_stability(
|
|
rarity: str,
|
|
magnitude: float,
|
|
rng: Callable[[], float] = random.random,
|
|
) -> float:
|
|
"""How cleanly the spirit holds the channel for this reply, 0.05-0.98.
|
|
|
|
Rarer spirits hold the channel more steadily (higher base). A stronger
|
|
triggering anomaly gives a cleaner line, up to a capped bonus. A final
|
|
rng-driven jitter of +/- 0.1 keeps it from feeling deterministic to the
|
|
seeker. `rng` is injectable so a later quantum-RNG source can swap in
|
|
real entropy without touching call sites.
|
|
"""
|
|
base = _BASE_BY_RARITY.get(rarity, _BASE_BY_RARITY["common"])
|
|
magnitude_bonus = min(_MAX_MAGNITUDE_BONUS, magnitude / 100)
|
|
jitter = (rng() * 2 - 1) * _JITTER_SPAN
|
|
stability = base + magnitude_bonus + jitter
|
|
return max(_MIN_STABILITY, min(_MAX_STABILITY, stability))
|