feat(frontend): unlocks/inventory UI, sigil designer, listening-tool EVP threshold

Workstream F of the character-depth/ghost-log spec.

- InventoryPanel.tsx: essence balance, owned unlocks/items (terminal
  listing + Codex-style rarity-glow borders on items), and a buy flow
  for a small fixed unlock catalog (currently just "listening_tool",
  the only key the contract names) with afford/can't-afford button
  states. Prices are fetched from a best-guess /api/inventory/catalog
  endpoint and fall back to a flagged "(est.)" price sourced from the
  spec's own worked example when that endpoint isn't available yet —
  the contract doesn't define a price-list REST shape.

- SigilDesigner.tsx + lib/sigil.ts: constrained geometric builder —
  points snap to 24 fixed clock-face slots around a circle, capped at
  12 to match the backend's payload limit, connected in placement
  order. Five hand-drawn stroke-only rune glyphs (eye/crescent/key/
  spiral/thorn) overlay the center. Point-cap enforcement, rune
  selection and payload-shape building are pure functions in
  lib/sigil.ts, unit-tested directly. Saves via POST
  /api/inventory/sigils (path inferred; payload shape matches the
  contract exactly: {"points": [[x,y],...], "rune": str}). Art
  direction: the builder itself reads like a plotting/debug tool
  (crosshair cursor, monospace coordinate HUD) while the rendered
  lines + rune glow violet, consistent with GhostGlyph's conventions.

- lib/evp.ts: new evpThresholdDb(hasListeningTool) pure function and
  EvpListener.start() now accepts { hasListeningTool } to lower the
  EVP anomaly threshold (8dB -> 4dB) when the unlock is owned — wired
  from useAuth().user.unlocks in SeancePage's EvpPanel, a real
  gameplay effect on which faint signals register as anomalies.

- api.ts User type gains unlocks/essence per the contract's /auth/me
  extension; new InventoryPage.tsx mounts both components behind auth
  at /inventory, linked from the séance nav.

166/166 tests pass (137 pre-existing + 29 new), i18n coverage check
clean, tsc -b clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Indiana
2026-07-23 16:26:18 +00:00
parent 6d8c6f2496
commit ebe6a4b0b8
18 changed files with 1792 additions and 4 deletions

View File

@@ -13,6 +13,7 @@
"home": "el umbral",
"codex": "códice",
"shop": "relicario",
"inventory": "inventario",
"seance": "sesión",
"enter": "entrar",
"logout": "partir"
@@ -333,6 +334,43 @@
},
"honesty": "la Ultimate Quantum Box está en desarrollo activo — esto es un manifiesto de reserva, no se cobra nada. el sitio funciona por completo sin ella; el hardware solo golpea más fuerte."
},
"inventory": {
"back": "← volver a la sesión",
"pageTitle": "EL ESTANTE",
"pageSubtitle": "lo que el velo te ha dejado, y lo que aún cambiará por más",
"title": "inventario",
"essenceLabel": "esencia",
"unlocksTitle": "desbloqueos",
"noUnlocks": "nada desbloqueado todavía",
"ownedTag": "[POSEÍDO]",
"lockedTag": "[BLOQUEADO]",
"priceValue": "{{price}} esencia",
"priceUnknown": "precio desconocido",
"priceEstimate": "(est.)",
"buy": "comprar",
"buying": "comprando…",
"buyInsufficient": "no hay esencia suficiente",
"buyError": "la compra falló",
"itemsTitle": "objetos",
"itemsLoading": "leyendo el estante…",
"itemsEmpty": "nada recolectado todavía",
"sigil": {
"title": "diseñador de sigilos",
"canvasLabel": "círculo de puntos de anclaje",
"slotLabel": "punto de anclaje {{slot}}",
"coordLabel": "coords",
"hoverHint": "pasa el cursor sobre un punto de anclaje",
"pointsLabel": "puntos",
"lastLabel": "último",
"runeLabel": "runa",
"nameLabel": "nombre",
"namePlaceholder": "sigilo sin título",
"save": "inscribir sigilo",
"saving": "inscribiendo…",
"saved": "el sigilo quedó ligado",
"error": "el sigilo no se sostuvo"
}
},
"haunting": {
"whisper": {
"behindYou": "detrás de ti",