Add lore depth, fix recruit click bug, add localStorage save

- 20+ new events drawing from Anders Fager, Kult, and Call of Cthulhu:
  ICA cashiers who never blink, SJ trains stopping in non-existent tunnels,
  Kult lictors as city officials, death servants finishing lectures, Deep Ones,
  investigators dreaming about us, stars being right over Lund Observatory
- Rewrite upgrade descriptions to match the register: mundane detail,
  matter-of-fact horror, no melodrama
- Add a new mid-game upgrade (Paktens Förnyelse) to smooth the progression
- Fix recruit button: was destroying DOM every 30fps tick, killing mousedown→click
  events; now only rebuilds when availability/affordability actually changes
- Fix double andakt deduction in recruit effect
- Fix map growing over log (proper flex constraints)
- Add localStorage save/load with versioned key (SAVE_VERSION bump invalidates
  old saves); autosaves every 10s and on upgrade purchase, clears on restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-07-07 23:25:44 +02:00
co-authored by Claude Sonnet 4.6
parent d091e7382a
commit f5776f269b
5 changed files with 245 additions and 13 deletions
+16 -7
View File
@@ -16,14 +16,14 @@ export const UPGRADES: Upgrade[] = [
name: 'Rekrytera en troende',
desc: '+1 Troende. De undrade varför källarlamporna flimrade. Nu vet de.',
cost: { andakt: 20 },
effect: s => { s.troende += 1; s.andakt = Math.max(0, s.andakt - 20 * Math.max(1, s.troende - 2)); },
effect: s => { s.troende += 1; },
available: s => s.troende < 12,
once: false,
},
{
id: 'first_meeting',
name: 'Källarmöte i Rosengård',
desc: 'Formalisera mötena. +1 andakt/s. Det luktar lite konstigt men ingen säger något.',
desc: 'Broder Lindqvist vet en källarlokal. Den ska egentligen vara ett förråd. Hyresvärden vet inte om den. Det är okänt varför han inte vet om den. +1 andakt/s.',
cost: { andakt: 30 },
effect: s => { s.andaktRate += 1; s.areas.rosengard = true; },
available: s => s.troende >= 2 && !s.upgrades.has('first_meeting'),
@@ -32,16 +32,25 @@ export const UPGRADES: Upgrade[] = [
{
id: 'the_manuscript',
name: 'Översätt Skriften',
desc: 'Någon hittade ett dokument i en källarlokal vid Davidshallsgatan. Texten översätts bäst som "snälla sluta." Vi fortsätter.',
desc: 'Dokumentet hittades inmurad i en vägg vid Davidshallsgatan, bakom kakel från 1923. Texten rör sig om man tittar bort. Lingvisten vi anlitade satt tyst i tre timmar och gick sedan hem. Han skickade översättningen nästa morgon utan kommentar. Vi läser den inte högt.',
cost: { andakt: 80 },
effect: s => { s.dreadRate += 0.5; s.ritualUnlocked = true; },
available: s => s.troende >= 3 && s.upgrades.has('first_meeting') && !s.upgrades.has('the_manuscript'),
once: true,
},
{
id: 'blood_pact',
name: 'Paktens Förnyelse',
desc: 'Det är en gammal pakt. Någon tecknade den för länge sedan. Vi vet inte vem. Vi förnyar den ändå — det verkar artigare. Skriften vet om det innan vi är klara.',
cost: { andakt: 150, dread: 30 },
effect: s => { s.andaktRate += 1.5; s.hemlighet = Math.max(0, s.hemlighet - 10); },
available: s => s.upgrades.has('the_manuscript') && !s.upgrades.has('blood_pact'),
once: true,
},
{
id: 'idol_under_bridge',
name: 'Idolen under bron',
desc: 'Vi placerade en idol under Øresundsbron vid nattens mörkaste timme. Danskarna la märke till det. De säger ingenting.',
desc: 'Vi fann den redan nergrävd under Øresundsbron. Vi behövde bara gräva ner den djupare. Materialet är okänt. Det är varmt mot handen. Danskarna la märke till ingenting. Det är värre.',
cost: { andakt: 120, dread: 20 },
effect: s => { s.dreadRate += 1; s.andaktRate += 0.5; },
available: s => s.upgrades.has('the_manuscript') && !s.upgrades.has('idol_under_bridge'),
@@ -50,7 +59,7 @@ export const UPGRADES: Upgrade[] = [
{
id: 'varnhem_circle',
name: 'Cirkeln på Värnhemstorget',
desc: 'Rita cirkeln. Använd krita. Det är viktigt att det är krita. Kommunen tvättar bort den varje morgon. Den är tillbaka varje kväll.',
desc: 'Rita cirkeln. Använd krita — det är viktigt att det är krita, vi vet inte varför men vi känner det. Kommunen tvättar bort den varje morgon. Den är tillbaka varje kväll. Nu är den tillbaka lite större.',
cost: { andakt: 200, dread: 40 },
effect: s => { s.areas.varnhem = true; s.dreadRate += 1.5; s.kaosRate += 0.2; },
available: s => s.upgrades.has('idol_under_bridge') && s.dread >= 50 && !s.upgrades.has('varnhem_circle'),
@@ -59,7 +68,7 @@ export const UPGRADES: Upgrade[] = [
{
id: 'torso_beacon',
name: 'Altaret vid Turning Torso',
desc: 'Byggnaden har alltid lyssnat. Nu ber vi den svara. Den gör det.',
desc: 'Calatrava-Santiago ritade byggnaden som en vridande ryggrad. Det är korrekt. Det är inte en metafor. Byggnaden har alltid lyssnat. Nu ber vi den svara. Den gör det. Svaret är ett ljud under hörselgränsen som vi känner i tänderna.',
cost: { andakt: 400, dread: 100 },
effect: s => { s.areas.torso = true; s.dreadRate += 3; s.ritualRate += 0.5; },
available: s => s.upgrades.has('varnhem_circle') && s.dread >= 100 && !s.upgrades.has('torso_beacon'),
@@ -68,7 +77,7 @@ export const UPGRADES: Upgrade[] = [
{
id: 'grand_ritual_prep',
name: 'Förberedelser för Den Stora Ritualen',
desc: 'Allt har lett hit. Turning Torso pekar. Värnhemstorget väntar. Öresund drar sig tillbaka.',
desc: 'Allt har lett hit. Turning Torso pekar. Värnhemstorget svarar. Öresund drar sig tillbaka som om det vet vad som kommer. Skriften är nu fullständig. Den sista sidan skrevs av sig själv i natt. Det är ett recept.',
cost: { andakt: 600, dread: 200 },
effect: s => { s.ritualRate += 2; s.kaosRate += 0.5; },
available: s => s.upgrades.has('torso_beacon') && s.ritual >= 50 && !s.upgrades.has('grand_ritual_prep'),