feat: Ghost Log HUD + evil meter + tells (Workstream D)
App-shell HUD (mounted in App.tsx alongside HauntingLayer, visible on every screen) that shows ambient idle status until a séance is active, then streams `tell` WS frames as terminal-style log lines with a "hacker witch" crescent-arc evil-meter gauge (occult sigils/runes fused with Transcript.tsx's monospace log vocabulary). - lib/evilMeter.ts: pure function computing a malevolent<->benevolent belief from the accumulated tell history — starts wide/uncertain, narrows geometrically and shifts per tell (deterministic hash of the tell text, since tells never leak ground truth), and snaps to definitive certainty on a successful ritual_complete's revealed.alignment. Fully unit tested (narrowing, ordering, determinism, ritual override, idle/empty history). - lib/ghostLogBus.ts: tiny typed event bus (mirrors lib/haunting.ts's HauntBus) so the app-shell-level GhostLog can react to live séance frames — SeanceProvider is only mounted inside the séance route, so a shell-level sibling can't read its context directly. - state/seance.tsx: publish entity/tell/ritual_complete onto the bus, and session_end on provider teardown so the HUD falls back to idle when the seeker leaves the séance page. - lib/types.ts: add the `tell` and `ritual_complete` server frames from the Character Depth spec's Contract section (only what this workstream consumes). - components/GhostLog.tsx/.css: the HUD itself, plus i18n keys in en.json/es.json. 168/168 frontend tests pass (137 pre-existing + 20 evilMeter + 11 GhostLog); tsc -b and the i18n coverage pretest are clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
258
frontend/src/components/GhostLog.css
Normal file
258
frontend/src/components/GhostLog.css
Normal file
@@ -0,0 +1,258 @@
|
||||
/* GhostLog — app-shell HUD, mounted once alongside HauntingLayer so it sits
|
||||
above every page. "Hacker witch": Transcript.tsx's terminal vocabulary
|
||||
(monospace log rows, phosphor glow, flicker) fused with occult framing
|
||||
(sigil corners, rune bullets, pentagram divider, crescent evil-meter).
|
||||
Palette borrowed from App.css's --qm-* tokens / SeancePage.css — no new
|
||||
colors introduced. */
|
||||
|
||||
.ghost-log {
|
||||
position: fixed;
|
||||
left: 14px;
|
||||
bottom: 14px;
|
||||
z-index: 500; /* above page content, below the global CRT glass (9999) and haunting layer (9998) */
|
||||
width: 300px;
|
||||
max-width: calc(100vw - 28px);
|
||||
max-height: 46vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--qm-panel, rgba(16, 16, 26, 0.85));
|
||||
border: 1px solid var(--qm-border, rgba(124, 255, 178, 0.18));
|
||||
border-radius: 6px;
|
||||
box-shadow:
|
||||
0 6px 28px rgba(0, 0, 0, 0.55),
|
||||
0 0 18px rgba(178, 107, 255, 0.08),
|
||||
inset 0 0 24px rgba(124, 255, 178, 0.03);
|
||||
backdrop-filter: blur(4px);
|
||||
font-family: var(--qm-font-ui, 'IBM Plex Mono', monospace);
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* candle-flicker glow instead of a flat terminal green */
|
||||
.ghost-log--active {
|
||||
animation: ghostLogCandle 5.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.ghost-log-corner {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
font-size: 0.7rem;
|
||||
color: rgba(178, 107, 255, 0.55);
|
||||
text-shadow: 0 0 8px rgba(178, 107, 255, 0.5);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ghost-log-corner--tl {
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.ghost-log-corner--tr {
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
.ghost-log-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.55rem 1rem 0.35rem;
|
||||
}
|
||||
|
||||
.ghost-log-glyph {
|
||||
color: var(--qm-violet, #b26bff);
|
||||
text-shadow: 0 0 10px rgba(178, 107, 255, 0.7);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.ghost-log-title {
|
||||
font-family: var(--qm-font-display, 'Cinzel', serif);
|
||||
font-weight: 700;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--qm-green, #7cffb2);
|
||||
text-shadow: 0 0 10px rgba(124, 255, 178, 0.4);
|
||||
}
|
||||
|
||||
.ghost-log-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.1rem 0 0.3rem;
|
||||
color: rgba(178, 107, 255, 0.4);
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 0.3em;
|
||||
}
|
||||
|
||||
.ghost-log-divider-sigil {
|
||||
color: var(--qm-blood, #ff3b5c);
|
||||
text-shadow: 0 0 8px rgba(255, 59, 92, 0.5);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.ghost-log-status {
|
||||
margin: 0;
|
||||
padding: 0.3rem 1rem 0.8rem;
|
||||
font-size: 0.74rem;
|
||||
font-style: italic;
|
||||
letter-spacing: 0.04em;
|
||||
color: rgba(211, 228, 220, 0.55);
|
||||
}
|
||||
|
||||
.ghost-log-status.dim {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ghost-log-body {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
padding: 0.15rem 1rem 0.8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.32rem;
|
||||
font-size: 0.74rem;
|
||||
line-height: 1.4;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(124, 255, 178, 0.3) transparent;
|
||||
}
|
||||
|
||||
.ghost-log-body::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.ghost-log-body::-webkit-scrollbar-thumb {
|
||||
background: rgba(124, 255, 178, 0.25);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.ghost-log-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.ghost-log-rune {
|
||||
flex: 0 0 auto;
|
||||
color: var(--qm-violet, #b26bff);
|
||||
text-shadow: 0 0 6px rgba(178, 107, 255, 0.6);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.ghost-log-line--system .ghost-log-rune {
|
||||
color: var(--qm-green, #7cffb2);
|
||||
text-shadow: 0 0 6px rgba(124, 255, 178, 0.6);
|
||||
}
|
||||
|
||||
.ghost-log-text {
|
||||
color: rgba(211, 228, 220, 0.9);
|
||||
}
|
||||
|
||||
.ghost-log-entity-name {
|
||||
font-family: var(--qm-font-display, 'Cinzel', serif);
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--qm-green, #7cffb2);
|
||||
}
|
||||
|
||||
/* ---------- evil meter gauge ---------- */
|
||||
|
||||
.evil-meter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 0.6rem;
|
||||
}
|
||||
|
||||
.evil-meter-svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.evil-meter-track {
|
||||
fill: none;
|
||||
stroke: rgba(124, 255, 178, 0.14);
|
||||
stroke-width: 12;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.evil-meter-band {
|
||||
fill: rgba(178, 107, 255, 0.4);
|
||||
filter: drop-shadow(0 0 6px rgba(178, 107, 255, 0.55));
|
||||
transition: d 200ms ease;
|
||||
}
|
||||
|
||||
.evil-meter-band--certain {
|
||||
fill: rgba(124, 255, 178, 0.5);
|
||||
filter: drop-shadow(0 0 10px rgba(124, 255, 178, 0.7));
|
||||
}
|
||||
|
||||
/* CRT-scanline texture cut through the belief band — the gauge belongs to
|
||||
both worlds at once: occult crescent shape, hacker scanline fill. */
|
||||
.evil-meter-scanline {
|
||||
stroke: rgba(7, 7, 13, 0.55);
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
.evil-meter-band-texture {
|
||||
opacity: 0.6;
|
||||
mix-blend-mode: multiply;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.evil-meter-needle {
|
||||
stroke: #d9f5e4;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
filter: drop-shadow(0 0 4px rgba(217, 245, 228, 0.8));
|
||||
}
|
||||
|
||||
.evil-meter-hub {
|
||||
fill: #d9f5e4;
|
||||
}
|
||||
|
||||
.evil-meter--certain .evil-meter-needle {
|
||||
stroke: var(--qm-green, #7cffb2);
|
||||
}
|
||||
|
||||
.evil-meter-readout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: -0.35rem;
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.evil-meter-glyph {
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.evil-meter-glyph--malevolent {
|
||||
color: var(--qm-blood, #ff3b5c);
|
||||
text-shadow: 0 0 6px rgba(255, 59, 92, 0.5);
|
||||
}
|
||||
|
||||
.evil-meter-glyph--benevolent {
|
||||
color: var(--qm-green, #7cffb2);
|
||||
text-shadow: 0 0 6px rgba(124, 255, 178, 0.5);
|
||||
}
|
||||
|
||||
.evil-meter-label {
|
||||
color: rgba(211, 228, 220, 0.85);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@keyframes ghostLogCandle {
|
||||
0%, 100% { box-shadow: 0 6px 28px rgba(0, 0, 0, 0.55), 0 0 18px rgba(178, 107, 255, 0.08), inset 0 0 24px rgba(124, 255, 178, 0.03); }
|
||||
35% { box-shadow: 0 6px 28px rgba(0, 0, 0, 0.55), 0 0 24px rgba(178, 107, 255, 0.16), inset 0 0 28px rgba(124, 255, 178, 0.05); }
|
||||
60% { box-shadow: 0 6px 28px rgba(0, 0, 0, 0.55), 0 0 14px rgba(178, 107, 255, 0.06), inset 0 0 20px rgba(124, 255, 178, 0.02); }
|
||||
80% { box-shadow: 0 6px 28px rgba(0, 0, 0, 0.55), 0 0 22px rgba(178, 107, 255, 0.14), inset 0 0 26px rgba(124, 255, 178, 0.04); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ghost-log--active {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user