Add ambient visual reactivity and location visit variety
- #app's filter (hue-rotate/saturate) now tracks dread continuously, and a fixed vignette bleeds red as hemlighet drops below 40 — no new assets, just CSS driven by existing state. - A kaos log entry now triggers a brief screen-glitch shake. - Location visits pick from 3 flavor-text variants per site instead of a single fixed line, so repeat visits don't repeat verbatim. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -213,7 +213,7 @@ export function resolveEventChoice(state: State, choiceIndex: number): State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface LocationVisit {
|
interface LocationVisit {
|
||||||
text: string;
|
texts: string[];
|
||||||
effect: Partial<{ andakt: number; dread: number; kaos: number; ritual: number; hemlighet: number }>;
|
effect: Partial<{ andakt: number; dread: number; kaos: number; ritual: number; hemlighet: number }>;
|
||||||
requiresArea?: keyof State['areas'];
|
requiresArea?: keyof State['areas'];
|
||||||
lockedText: string;
|
lockedText: string;
|
||||||
@@ -221,40 +221,68 @@ interface LocationVisit {
|
|||||||
|
|
||||||
const LOCATION_VISITS: Record<string, LocationVisit> = {
|
const LOCATION_VISITS: Record<string, LocationVisit> = {
|
||||||
rosengard: {
|
rosengard: {
|
||||||
text: 'Du sitter i källaren. Lamporna flimrar i takt med ditt hjärta. Eller tvärtom. +andakt',
|
texts: [
|
||||||
|
'Du sitter i källaren. Lamporna flimrar i takt med ditt hjärta. Eller tvärtom. +andakt',
|
||||||
|
'Någon har städat källaren medan ingen var där. Ingen av er erkänner sig skyldig. +andakt',
|
||||||
|
'Den varma stolen är varm igen. Ingen har suttit i den. +andakt',
|
||||||
|
],
|
||||||
effect: { andakt: 15 },
|
effect: { andakt: 15 },
|
||||||
requiresArea: 'rosengard',
|
requiresArea: 'rosengard',
|
||||||
lockedText: 'En fuktig källarlokal i Rosengård. Du kan se ljuset under dörren. Ni är inte redo än.',
|
lockedText: 'En fuktig källarlokal i Rosengård. Du kan se ljuset under dörren. Ni är inte redo än.',
|
||||||
},
|
},
|
||||||
varnhem: {
|
varnhem: {
|
||||||
text: 'Cirkeln är lite större sedan du var här sist. Du ritade den inte. +dread +ritual',
|
texts: [
|
||||||
|
'Cirkeln är lite större sedan du var här sist. Du ritade den inte. +dread +ritual',
|
||||||
|
'Kritan på asfalten har inte suddats trots regnet i natt. +dread +ritual',
|
||||||
|
'Torget är tomt förutom en duva som går i exakt cirkelns mönster. +dread +ritual',
|
||||||
|
],
|
||||||
effect: { dread: 8, ritual: 2 },
|
effect: { dread: 8, ritual: 2 },
|
||||||
requiresArea: 'varnhem',
|
requiresArea: 'varnhem',
|
||||||
lockedText: 'Värnhemstorget ser normalt ut. Det är ett dåligt tecken.',
|
lockedText: 'Värnhemstorget ser normalt ut. Det är ett dåligt tecken.',
|
||||||
},
|
},
|
||||||
torso: {
|
torso: {
|
||||||
text: 'Byggnaden pekar. Du tittar dit den pekar. Du tittar bort igen. Det hjälper inte. +dread +ritual',
|
texts: [
|
||||||
|
'Byggnaden pekar. Du tittar dit den pekar. Du tittar bort igen. Det hjälper inte. +dread +ritual',
|
||||||
|
'Ett fönster högt uppe är tänt som det aldrig varit förut. Ingen bor där. Ingen har någonsin bott där. +dread +ritual',
|
||||||
|
'Torso vrider sig en grad till i vinden som inte finns. +dread +ritual',
|
||||||
|
],
|
||||||
effect: { dread: 15, ritual: 5 },
|
effect: { dread: 15, ritual: 5 },
|
||||||
requiresArea: 'torso',
|
requiresArea: 'torso',
|
||||||
lockedText: 'Turning Torso vrider sig i vinden. Inte i vinden. I något annat.',
|
lockedText: 'Turning Torso vrider sig i vinden. Inte i vinden. I något annat.',
|
||||||
},
|
},
|
||||||
oresund: {
|
oresund: {
|
||||||
text: 'Vattnet sjunker lite långsammare just nu. Det vet att du stod här. +dread',
|
texts: [
|
||||||
|
'Vattnet sjunker lite långsammare just nu. Det vet att du stod här. +dread',
|
||||||
|
'En färja passerar utan ljud. Ingen ombord vinkar. Alla ombord tittar rakt på dig. +dread',
|
||||||
|
'Bron surrar på en frekvens du känner i tänderna snarare än öronen. +dread',
|
||||||
|
],
|
||||||
effect: { dread: 5 },
|
effect: { dread: 5 },
|
||||||
lockedText: 'Öresund. Danskarna på andra sidan verkar inte märka någonting. Det är det mest oroande av allt.',
|
lockedText: 'Öresund. Danskarna på andra sidan verkar inte märka någonting. Det är det mest oroande av allt.',
|
||||||
},
|
},
|
||||||
ribersborg: {
|
ribersborg: {
|
||||||
text: 'Änderna tittar på dig. En av dem nickar. Du nickar tillbaka. Det var ett misstag. +andakt',
|
texts: [
|
||||||
|
'Änderna tittar på dig. En av dem nickar. Du nickar tillbaka. Det var ett misstag. +andakt',
|
||||||
|
'Alla änderna vänder sig samtidigt mot Turning Torso när du kommer. +andakt',
|
||||||
|
'Sanden är kall trots solen. Ingen annan verkar märka det. +andakt',
|
||||||
|
],
|
||||||
effect: { andakt: 8 },
|
effect: { andakt: 8 },
|
||||||
lockedText: 'Stranden är tom nu. Änderna sitter stilla. De väntar på rätt tidpunkt.',
|
lockedText: 'Stranden är tom nu. Änderna sitter stilla. De väntar på rätt tidpunkt.',
|
||||||
},
|
},
|
||||||
davidshall: {
|
davidshall: {
|
||||||
text: 'Källarlokalen luktar fortfarande. Du andas djupare än du borde. +andakt',
|
texts: [
|
||||||
|
'Källarlokalen luktar fortfarande. Du andas djupare än du borde. +andakt',
|
||||||
|
'Kaklet från 1923 är kallt att röra vid, oavsett årstid. +andakt',
|
||||||
|
'Grannen genom väggen sjunger något som nästan, men inte riktigt, är en psalm. +andakt',
|
||||||
|
],
|
||||||
effect: { andakt: 10 },
|
effect: { andakt: 10 },
|
||||||
lockedText: 'Davidshallsgatan. Kakel från 1923. Bakom det finns något som väljer att vänta.',
|
lockedText: 'Davidshallsgatan. Kakel från 1923. Bakom det finns något som väljer att vänta.',
|
||||||
},
|
},
|
||||||
stortorget: {
|
stortorget: {
|
||||||
text: 'Du vet inte varför du kom hit. Du vet inte hur länge du stod här. +kaos',
|
texts: [
|
||||||
|
'Du vet inte varför du kom hit. Du vet inte hur länge du stod här. +kaos',
|
||||||
|
'Statyn på torget har flyttat en centimeter sedan i förra veckan. Du är den enda som mätt. +kaos',
|
||||||
|
'Alla klockor på torget visar olika tid. Ingen av dem är fel. +kaos',
|
||||||
|
],
|
||||||
effect: { kaos: 8 },
|
effect: { kaos: 8 },
|
||||||
lockedText: 'Stortorget är fullt av folk som inte vet vad de gör här heller. Du passar in.',
|
lockedText: 'Stortorget är fullt av folk som inte vet vad de gör här heller. Du passar in.',
|
||||||
},
|
},
|
||||||
@@ -272,7 +300,7 @@ export function visitLocation(state: State, id: string, cooldowns: Map<string, n
|
|||||||
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds), flags: new Set(state.flags) };
|
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds), flags: new Set(state.flags) };
|
||||||
|
|
||||||
const areaLocked = visit.requiresArea && !s.areas[visit.requiresArea];
|
const areaLocked = visit.requiresArea && !s.areas[visit.requiresArea];
|
||||||
const text = areaLocked ? visit.lockedText : visit.text;
|
const text = areaLocked ? visit.lockedText : visit.texts[Math.floor(Math.random() * visit.texts.length)];
|
||||||
|
|
||||||
if (!areaLocked) {
|
if (!areaLocked) {
|
||||||
if (visit.effect.andakt) s.andakt += visit.effect.andakt;
|
if (visit.effect.andakt) s.andakt += visit.effect.andakt;
|
||||||
|
|||||||
@@ -36,6 +36,29 @@ body {
|
|||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
filter: hue-rotate(calc(var(--dread-intensity, 0) * -35deg)) saturate(calc(1 + var(--dread-intensity, 0) * 0.7));
|
||||||
|
transition: filter 2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app.kaos-glitch { animation: kaos-glitch-shake 0.4s steps(2, end) 2; }
|
||||||
|
|
||||||
|
@keyframes kaos-glitch-shake {
|
||||||
|
0% { transform: translate(0, 0); }
|
||||||
|
20% { transform: translate(-2px, 1px); filter: hue-rotate(20deg); }
|
||||||
|
40% { transform: translate(2px, -1px); filter: hue-rotate(-15deg); }
|
||||||
|
60% { transform: translate(-1px, 2px); filter: hue-rotate(10deg); }
|
||||||
|
80% { transform: translate(1px, -2px); }
|
||||||
|
100% { transform: translate(0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#danger-vignette {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 999; /* below modals (1000/1001), above map/log */
|
||||||
|
background: radial-gradient(ellipse at center, transparent 55%, var(--danger) 150%);
|
||||||
|
opacity: var(--hemlighet-danger, 0);
|
||||||
|
transition: opacity 1.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Header ── */
|
/* ── Header ── */
|
||||||
|
|||||||
14
src/ui.ts
14
src/ui.ts
@@ -121,6 +121,8 @@ export function initUI(app: HTMLElement, onUpgrade: (id: string) => void, onRest
|
|||||||
<button class="overlay-btn" id="overlay-btn">Börja om</button>
|
<button class="overlay-btn" id="overlay-btn">Börja om</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="danger-vignette"></div>
|
||||||
|
|
||||||
<div id="event-modal" class="help-modal">
|
<div id="event-modal" class="help-modal">
|
||||||
<div class="help-modal-inner" id="event-modal-inner">
|
<div class="help-modal-inner" id="event-modal-inner">
|
||||||
<div class="help-modal-body">
|
<div class="help-modal-body">
|
||||||
@@ -172,6 +174,11 @@ export function renderState(state: State): void {
|
|||||||
setBar('bar-hemlighet', state.hemlighet);
|
setBar('bar-hemlighet', state.hemlighet);
|
||||||
setBar('bar-kaos', state.kaos);
|
setBar('bar-kaos', state.kaos);
|
||||||
|
|
||||||
|
// Ambient visual reactivity — the deeper the dread, the more the world tilts; low hemlighet bleeds red at the edges.
|
||||||
|
const root = document.documentElement.style;
|
||||||
|
root.setProperty('--dread-intensity', Math.min(1, state.dread / 150).toFixed(3));
|
||||||
|
root.setProperty('--hemlighet-danger', (state.hemlighet < 40 ? (1 - state.hemlighet / 40) * 0.35 : 0).toFixed(3));
|
||||||
|
|
||||||
const ritualSection = document.getElementById('ritual-section')!;
|
const ritualSection = document.getElementById('ritual-section')!;
|
||||||
if (state.ritualUnlocked) {
|
if (state.ritualUnlocked) {
|
||||||
ritualSection.style.display = '';
|
ritualSection.style.display = '';
|
||||||
@@ -287,6 +294,13 @@ function renderLog(state: State): void {
|
|||||||
`<div class="log-entry ${e.kind}">${e.text}</div>`
|
`<div class="log-entry ${e.kind}">${e.text}</div>`
|
||||||
).join('');
|
).join('');
|
||||||
log.scrollTop = 0;
|
log.scrollTop = 0;
|
||||||
|
|
||||||
|
if (state.log[0]?.kind === 'kaos') {
|
||||||
|
const app = document.getElementById('app')!;
|
||||||
|
app.classList.remove('kaos-glitch');
|
||||||
|
void app.offsetWidth; // restart animation
|
||||||
|
app.classList.add('kaos-glitch');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showOverlay(state: State): void {
|
function showOverlay(state: State): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user