diff --git a/README.md b/README.md index ae0eb8d..c6ba126 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ the collectible layer sitting on top of the anomaly-detection plumbing: nothing about a spirit is user-authored, all of it comes from the same signature → LLM-mint → normalize pipeline that runs live during a séance. -## The Armory +## The Reliquary `/shop` is a waitlist page for **the Ultimate Quantum Box** — a proposed ESP32-P4/C6 handheld séance instrument (thermal camera, EMF whisker array, @@ -158,7 +158,7 @@ frontend/ src/i18n/ react-i18next init + en.json / es.json docs/superpowers/ specs/ the design spec — note the live app now exceeds it - (5 modes, not 4; the Codex, the Armory and the + (5 modes, not 4; the Codex, the Reliquary and the haunting layer are all further along than documented) plans/ the 7 implementation plans this repo was built from ``` @@ -380,7 +380,7 @@ Honestly, in order of confidence: the landing page's marketing grid yet. - **Cosmetic.** The frontend build ships one 871 KB JS chunk (Vite warns about it); code-splitting would help first paint but hasn't been done. -- **Intentionally unfinished.** The Armory / Ultimate Quantum Box is a +- **Intentionally unfinished.** The Reliquary / Ultimate Quantum Box is a waitlist only — no hardware exists, no payment is taken, and the page says so. diff --git a/docs/superpowers/specs/2026-07-23-character-depth-ghost-log-design.md b/docs/superpowers/specs/2026-07-23-character-depth-ghost-log-design.md index 18af9c7..83d5f57 100644 --- a/docs/superpowers/specs/2026-07-23-character-depth-ghost-log-design.md +++ b/docs/superpowers/specs/2026-07-23-character-depth-ghost-log-design.md @@ -22,9 +22,15 @@ async with engine.begin() as conn: await conn.execute(text( "ALTER TABLE users ADD COLUMN IF NOT EXISTS favor DOUBLE PRECISION NOT NULL DEFAULT 0.0" )) + await conn.execute(text( + "ALTER TABLE users ADD COLUMN IF NOT EXISTS essence INTEGER NOT NULL DEFAULT 0" + )) await conn.execute(text( "ALTER TABLE entities ADD COLUMN IF NOT EXISTS traits JSONB NOT NULL DEFAULT '{}'::jsonb" )) + await conn.execute(text( + "ALTER TABLE entities ADD COLUMN IF NOT EXISTS at_peace BOOLEAN NOT NULL DEFAULT false" + )) ``` New tables (`unlocks`, `inventory_items`, `sigils`) don't need this — @@ -53,6 +59,26 @@ legible/less volatile; lower favor → more volatile/deceptive). The read-back bias is a small effect — a `favor`-scaled adjustment to the volatility and deceptiveness rolls in the trait-rolling function, not a hard gate. +**`User.essence: int`** (new column, default `0`) — a visible, spendable +currency, distinct from the hidden `favor` score. Earned in small amounts +just for summoning (every successful summon, any mode) and larger amounts +for meaningful actions (ritual success, a correct judgment, crossing a +spirit over — see below). This is the currency Workstream C's unlocks/items +are priced in — earning it should feel constant and easy ("the more you +summon"), spending it on unlocks/tools is the sink. Exact amounts are the +implementer's call; keep summon income small (single digits) and +milestone income larger (tens), so unlocks feel reachable within a handful +of sessions, not grindy. + +**`Entity.at_peace: bool`** (new column, default `false`) — set when a +seeker successfully helps a genuinely benevolent, "stuck" spirit cross +over (see the `cross_over` verdict below). An at-peace entity stays in the +Codex permanently (memorialized, never deleted) but can no longer be +re-contacted — `signature_from_anomalies` matching it should mint a new +entity instead of re-summoning the retired one. The Codex API/UI marking +it distinctly is a later concern (Workstream D can add a simple "at peace" +badge if time allows, but it's not required for this spec to be complete). + **New WS frames on `/ws/session`** (extends the existing protocol in `backend/app/ws.py`): @@ -60,10 +86,19 @@ Client → server: - `{"type": "ritual_start"}` — begins a ritual attempt for the current entity. - `{"type": "ritual_step", "step": }` — one completed interactive step. -- `{"type": "judgment", "verdict": "trust" | "banish" | "test"}` — always - allowed (a seeker can judge blind without completing a ritual — the - ritual doesn't gate eligibility, only whether accurate info was shown - first). +- `{"type": "judgment", "verdict": "trust" | "banish" | "test" | + "cross_over"}` — always allowed (a seeker can judge blind without + completing a ritual — the ritual doesn't gate eligibility, only whether + accurate info was shown first). `cross_over` is the compassionate + resolution: helping a spirit move on rather than simply continuing + contact (`trust`) or expelling it (`banish`). It's the *correct* call + specifically for a genuinely benevolent entity that reads as "stuck" + (implementer's call on the exact signal — a reasonable rule: alignment + >= 0.5 and volatility above some threshold, since the existing fallback + personas already lean on "died with something unfinished" themes). Used + on a demon, it fails outright — demons resist crossing over — with no + reward and no `at_peace` change, distinct from a wrong `trust`/`banish` + call (it's a naive read, not a reckless one, so no favor penalty either). Server → client: - `{"type": "ritual_complete", "success": bool, "revealed": {"alignment": @@ -79,23 +114,29 @@ Server → client: work with without the backend leaking ground truth outside of `ritual_complete`. - `{"type": "judgment_result", "correct": bool, "favor_delta": float, - "consequence": "reward" | "escalation" | "withdrawal" | "neutral"}` — - `correct` = trust called on real alignment >= 0.5, or banish called on - alignment < 0.5. `test` verdict always returns `consequence: "neutral"`, - `favor_delta: 0.0`, and requires a completed ritual this session to do - anything (a `judgment_result` with `correct: false` and - `consequence: "neutral"` if attempted without one — no crash, just no - effect). Wrong-trust favor penalty should be larger in magnitude than - wrong-banish penalty (recklessness costs more than caution) — exact - numbers are the implementer's call, keep them small (single-digit - percent of the `[-1,1]` range per event). + "essence_delta": int, "at_peace": bool, + "consequence": "reward" | "escalation" | "withdrawal" | "crossed_over" | + "resisted" | "neutral"}` — `correct` = trust called on real alignment + >= 0.5, or banish called on alignment < 0.5, or cross_over called + correctly per the rule above. `test` verdict always returns + `consequence: "neutral"`, both deltas `0`, and requires a completed + ritual this session to do anything (a `judgment_result` with + `correct: false` and `consequence: "neutral"` if attempted without one — + no crash, just no effect). `cross_over` on a demon returns + `consequence: "resisted"`, both deltas `0`. `cross_over` correctly called + returns `consequence: "crossed_over"`, `at_peace: true`, and the largest + essence reward of any outcome. Wrong-trust favor penalty should be larger + in magnitude than wrong-banish penalty (recklessness costs more than + caution) — exact numbers are the implementer's call, keep favor deltas + small (single-digit percent of the `[-1,1]` range per event). - `{"type": "item_drop", "item": {"item_type": str, "item_key": str, "payload": dict}}` — emitted opportunistically (implementer's call on exact odds) after a correct judgment, a successful ritual, or a high-rarity summon. **`GET /auth/me`** gains an `unlocks: list[str]` field (unlock keys the -user has earned) so the frontend knows what's enabled — e.g. `"listening_tool"`. +user has earned) and an `essence: int` field (current spendable balance) +so the frontend knows what's enabled and affordable. **New tables** (SQLAlchemy models, follow the existing `Entity`/`User` style in `backend/app/models/`): @@ -114,36 +155,49 @@ from persona/LLM input as specified above. Tests: trait ranges, signature determinism, persona/trait independence (same persona template can pair with any alignment). -## Workstream B (backend) — ritual + judgment + favor +## Workstream B (backend) — ritual + judgment + favor + cross-over New `backend/app/judgment.py` (pure functions: ritual success roll, -judgment-correctness + favor-delta + consequence selection) plus the WS -handlers in `backend/app/ws.py` for `ritual_start`/`ritual_step`/`judgment`, -and the `User.favor` column + migration line. Reads `state.entity["traits"]` -(entity dict shape — check how `serialize_entity` exposes it and add -`traits` there). On wrong-trust, call into the existing haunting-escalation -hook if one exists server-side, or note in your report if escalation is -frontend-only (check `frontend/src/lib/haunting.ts` first — if escalation -state lives purely client-side, your job is just to emit -`consequence: "escalation"` correctly and leave the actual effect to -Workstream D). Tests: ritual success/fail paths, judgment correctness -matrix (trust/banish × real spirit/demon), favor clamping, favor read-back -bias on trait rolls (small, verifiable effect size). +judgment-correctness + favor-delta + essence-delta + consequence selection +for all four verdicts including `cross_over`'s "stuck spirit" rule and its +demon-resists-crossing-over failure case) plus the WS handlers in +`backend/app/ws.py` for `ritual_start`/`ritual_step`/`judgment`, and the +`User.favor`/`User.essence`/`Entity.at_peace` columns + migration lines. +Reads `state.entity["traits"]` (entity dict shape — check how +`serialize_entity` exposes it and add `traits` there). On correct +`cross_over`, set `Entity.at_peace = true` and ensure +`signature_from_anomalies` re-contact no longer matches an at-peace entity +(mint a fresh one instead — check `entities.py`'s matching path). On +wrong-trust, call into the existing haunting-escalation hook if one exists +server-side, or note in your report if escalation is frontend-only (check +`frontend/src/lib/haunting.ts` first — if escalation state lives purely +client-side, your job is just to emit `consequence: "escalation"` correctly +and leave the actual effect to Workstream D). Tests: ritual success/fail +paths, judgment correctness matrix (trust/banish/cross_over × real +spirit/demon/stuck spirit), favor clamping, essence crediting per outcome, +at_peace persistence + re-contact behavior, favor read-back bias on trait +rolls (small, verifiable effect size). -## Workstream C (backend) — unlocks, items, sigils, drops +## Workstream C (backend) — unlocks, items, sigils, drops, essence New models (`unlocks`, `inventory_items`, `sigils` per the contract), a new `backend/app/routes/inventory.py` (or extend an existing routes file if more consistent with this codebase's conventions — check `backend/app/routes/`) -exposing REST endpoints to list a user's unlocks/items/sigils and to save a +exposing REST endpoints to list a user's unlocks/items/sigils, to save a sigil design (`POST`, validates the `design` JSON shape against whatever Workstream F defines — coordinate via your report if you build before seeing their output; a reasonable placeholder shape is `{"points": [[x,y],...], "rune": str}`, cap `points` length e.g. at 12 to -bound payload size). Extend `GET /auth/me` with `unlocks: list[str]`. Wire -`item_drop` emission into `backend/app/ws.py` per the contract's trigger -points. Tests: drop-roll probability sanity, inventory/sigil CRUD, `/auth/me` -unlock list shape. +bound payload size), and a new `POST /api/inventory/unlocks/{unlock_key}` +that spends `essence` to buy a named unlock (define a small fixed price +table, e.g. `{"listening_tool": 40}` — reject with 402/insufficient-funds +style error if the user's `essence` is below the price, otherwise deduct +and insert into `unlocks`). Extend `GET /auth/me` with `unlocks: list[str]` +and `essence: int`. Wire `item_drop` emission and `essence` crediting +(summon-time trickle + judgment/ritual/cross-over milestones) into +`backend/app/ws.py` per the contract's trigger points and amounts. Tests: +drop-roll probability sanity, inventory/sigil CRUD, essence purchase +success/insufficient-funds paths, `/auth/me` unlock list + essence shape. ## Workstream D (frontend) — Ghost Log HUD + evil meter + tells @@ -164,18 +218,24 @@ override, idle-state rendering. New `frontend/src/components/RitualPanel.tsx` — a short interactive sequence (your call on exact interaction, e.g. 3-5 click/hold steps) that sends `ritual_step` frames and handles `ritual_complete`. Judgment UI -(Trust/Banish/Test buttons) sending the `judgment` frame and handling -`judgment_result` — on `consequence: "escalation"`, read -`frontend/src/lib/haunting.ts` first and hook into its existing -idle-escalation state if there's a sensible extension point; if not, add a -clearly-scoped "forced escalation" mode to it (report which you did). -Tests: ritual step sequencing, judgment button → frame shape, escalation -wiring (mock haunting.ts's public surface, don't test its internals). +(Trust/Banish/Cross Over/Test buttons — four verdicts per the contract) +sending the `judgment` frame and handling `judgment_result` — on +`consequence: "escalation"`, read `frontend/src/lib/haunting.ts` first and +hook into its existing idle-escalation state if there's a sensible +extension point; if not, add a clearly-scoped "forced escalation" mode to +it (report which you did). On `consequence: "crossed_over"`, a distinct +calm/peaceful visual beat (your call — e.g. the entity's glyph fading out +gently) rather than reusing the trust "reward" treatment, since it's a +farewell, not a continuation. Tests: ritual step sequencing, judgment +button → frame shape (all four verdicts), escalation wiring (mock +haunting.ts's public surface, don't test its internals). ## Workstream F (frontend) — unlocks/inventory UI + sigil designer + listening tool New `frontend/src/components/InventoryPanel.tsx` (list unlocks/items from -`/auth/me` and the Workstream C endpoints) and +`/auth/me` and the Workstream C endpoints, show current `essence` balance, +and a "buy" button per locked unlock that calls Workstream C's purchase +endpoint — disable/gray out unlocks the user can't yet afford) and `frontend/src/components/SigilDesigner.tsx` — a **constrained geometric builder**, not freeform drawing: seeker places points around a fixed circle (cap at 12, matching Workstream C's payload limit), the tool connects them @@ -184,7 +244,8 @@ and overlays a rune choice from a fixed small set. Saves via Workstream C's `unlocks` list) into `frontend/src/lib/evp.ts`'s detection threshold — when present, lower the anomaly threshold so fainter signals register (a real gameplay effect, not cosmetic). Tests: sigil point-cap enforcement, rune -selection, listening-tool threshold change when unlock present vs. absent. +selection, listening-tool threshold change when unlock present vs. absent, +purchase button afford/can't-afford states. ## Explicitly out of scope here diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index e4cb5ea..48cecee 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -28,7 +28,7 @@ "heroKicker": "an instrument for speaking with the dead bandwidth", "cta": "BEGIN THE SÉANCE", "ctaCodex": "browse the codex", - "ctaShop": "visit the armory", + "ctaShop": "visit the reliquary", "stats": { "entities": "spirits catalogued", "sessions": "séances held", @@ -57,10 +57,10 @@ }, "featured": "most disturbed spirits", "featuredEmpty": "the registry is still blank — be the first to disturb it", - "armory": { - "title": "HARDWARE FROM THE ARMORY", + "reliquary": { + "title": "HARDWARE FROM THE RELIQUARY", "line": "the Ultimate Quantum Box is coming — a handheld séance instrument that makes the veil knock louder.", - "link": "enter the armory →" + "link": "enter the reliquary →" }, "footer": "an interactive horror art installation — the spirits are fiction; the static is real", "footerColophon": "self-hosted · no cloud · the veil runs on your machine" @@ -267,7 +267,7 @@ }, "shop": { "back": "← the threshold", - "title": "THE ARMORY", + "title": "THE RELIQUARY", "subtitle": "instruments for those who would knock louder", "flagship": { "tag": "flagship instrument", diff --git a/frontend/src/i18n/es.json b/frontend/src/i18n/es.json index b477bfd..39b65ad 100644 --- a/frontend/src/i18n/es.json +++ b/frontend/src/i18n/es.json @@ -12,7 +12,7 @@ "nav": { "home": "el umbral", "codex": "códice", - "shop": "arsenal", + "shop": "relicario", "seance": "sesión", "enter": "entrar", "logout": "partir" @@ -28,7 +28,7 @@ "heroKicker": "un instrumento para hablar con el ancho de banda muerto", "cta": "COMENZAR LA SESIÓN", "ctaCodex": "hojear el códice", - "ctaShop": "visitar el arsenal", + "ctaShop": "visitar el relicario", "stats": { "entities": "espíritus catalogados", "sessions": "sesiones celebradas", @@ -57,10 +57,10 @@ }, "featured": "los espíritus más perturbados", "featuredEmpty": "el registro aún está en blanco — sé quien lo perturbe primero", - "armory": { - "title": "HARDWARE DEL ARSENAL", + "reliquary": { + "title": "HARDWARE DEL RELICARIO", "line": "la Ultimate Quantum Box se acerca — un instrumento de sesión de mano que hace que el velo golpee más fuerte.", - "link": "entrar al arsenal →" + "link": "entrar al relicario →" }, "footer": "una instalación de arte de terror interactiva — los espíritus son ficción; la estática es real", "footerColophon": "autoalojado · sin nube · el velo corre en tu máquina" @@ -267,7 +267,7 @@ }, "shop": { "back": "← el umbral", - "title": "EL ARSENAL", + "title": "EL RELICARIO", "subtitle": "instrumentos para quienes quisieran golpear más fuerte", "flagship": { "tag": "instrumento insignia", diff --git a/frontend/src/pages/LandingPage.css b/frontend/src/pages/LandingPage.css index 5a85a66..7cef8f2 100644 --- a/frontend/src/pages/LandingPage.css +++ b/frontend/src/pages/LandingPage.css @@ -437,7 +437,7 @@ font-size: 0.7rem; } -/* ---------- armory teaser strip ---------- */ +/* ---------- reliquary teaser strip ---------- */ .hero-secondary-links { display: flex; @@ -445,12 +445,12 @@ gap: 1.6rem; } -.landing-section.armory-strip { +.landing-section.reliquary-strip { padding-top: 2.5rem; padding-bottom: 2.5rem; } -.armory-strip-inner { +.reliquary-strip-inner { display: flex; flex-wrap: wrap; align-items: center; @@ -462,7 +462,7 @@ text-align: center; } -.armory-title { +.reliquary-title { font-family: 'Cinzel', serif; font-size: 0.85rem; font-weight: 700; @@ -472,13 +472,13 @@ margin: 0; } -.armory-line { +.reliquary-line { font-size: 0.78rem; color: rgba(216, 230, 221, 0.55); margin: 0; } -.armory-link { +.reliquary-link { color: #b26bff; font-size: 0.78rem; letter-spacing: 0.14em; @@ -489,7 +489,7 @@ transition: color 0.2s ease, border-color 0.2s ease, text-shadow 0.2s ease; } -.armory-link:hover { +.reliquary-link:hover { color: #d3a8ff; border-color: #d3a8ff; text-shadow: 0 0 14px rgba(178, 107, 255, 0.6); diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index 26c84f0..f4d7fea 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -283,7 +283,7 @@ export function LandingPage() { {t('landing.ctaCodex')} - {t('landing.ctaShop', { defaultValue: 'visit the armory' })} + {t('landing.ctaShop', { defaultValue: 'visit the reliquary' })} @@ -343,19 +343,19 @@ export function LandingPage() { )} -
-
-

- {t('landing.armory.title', { defaultValue: 'HARDWARE FROM THE ARMORY' })} +
+
+

+ {t('landing.reliquary.title', { defaultValue: 'HARDWARE FROM THE RELIQUARY' })}

-

- {t('landing.armory.line', { +

+ {t('landing.reliquary.line', { defaultValue: 'the Ultimate Quantum Box is coming — a handheld séance instrument that makes the veil knock louder.', })}

- - {t('landing.armory.link', { defaultValue: 'enter the armory →' })} + + {t('landing.reliquary.link', { defaultValue: 'enter the reliquary →' })}
diff --git a/frontend/src/pages/ShopPage.css b/frontend/src/pages/ShopPage.css index c059d37..1f6c85f 100644 --- a/frontend/src/pages/ShopPage.css +++ b/frontend/src/pages/ShopPage.css @@ -1,4 +1,4 @@ -/* The Armory — gothic storefront. Tokens: bg #07070d, panel rgba(16,16,26,0.85), +/* The Reliquary — gothic storefront. Tokens: bg #07070d, panel rgba(16,16,26,0.85), phosphor #7cffb2, violet #b26bff, blood #ff3b5c, borders rgba(124,255,178,0.18). */ .shop-page { diff --git a/frontend/src/pages/ShopPage.tsx b/frontend/src/pages/ShopPage.tsx index d25af2f..e801895 100644 --- a/frontend/src/pages/ShopPage.tsx +++ b/frontend/src/pages/ShopPage.tsx @@ -1,4 +1,4 @@ -// The Armory — storefront for Quantumancy hardware. Flagship 'Ultimate +// The Reliquary — storefront for Quantumancy hardware. Flagship 'Ultimate // Quantum Box' hero, a grid of knock-louder modules, and the reservation // manifest (POST /api/shop/waitlist — public, no payment taken). // Honest by design: the site works fully without the hardware. @@ -197,7 +197,7 @@ export function ShopPage() { {t('shop.back', { defaultValue: '← the threshold' })} -

{t('shop.title', { defaultValue: 'THE ARMORY' })}

+

{t('shop.title', { defaultValue: 'THE RELIQUARY' })}

{t('shop.subtitle', { defaultValue: 'instruments for those who would knock louder' })}