From 61c56ce2b2ce819f9807be7554c71351f2a42dcf Mon Sep 17 00:00:00 2001 From: Fredrik Johansson Date: Sat, 8 Aug 2026 22:37:42 +0200 Subject: [PATCH] Add Kodex: unlockable lore fragments about places, things, and people MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ported the pattern from vildhjarta/Thön's own Kodex -- same shared .help-modal/.help-section CSS both projects already have, same LoreEntry{id,title,body,unlock} shape, same "N fragment kvarstår olästa" locked-count footer. Twelve entries pulled from LORE-BIBLE.md's established canon (the basement, Den Sovande, Skriften, the ducks, the number 13, Turning Torso, Värnhemstorget), gated by dread/ritual thresholds, upgrade ownership, and specific events having fired (coc_investigator_dream, k_bengt_ake, k_oresund_rises, nine_chairs_full, grand_ritual_prep) -- reusing hooks already in the engine rather than adding new ones. Deliberately doesn't resolve anything LORE-BIBLE.md lists under "what is deliberately never explained": these entries deepen the atmosphere around the warm chair, the sunken city, and Den Sovande's true shape, they don't answer them. Verified live via injected save state: at dread=25 with the_manuscript bought, exactly the 4 expected entries unlock (out of 12), the locked- count footer reads "8 fragment kvarstår olästa," and Escape correctly closes the modal. 21/21 tests pass, typecheck and build clean. Co-Authored-By: Claude Sonnet 5 --- src/data/version.ts | 9 ++++- src/lore.ts | 89 +++++++++++++++++++++++++++++++++++++++++++++ src/ui.ts | 40 ++++++++++++++++++++ 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 src/lore.ts diff --git a/src/data/version.ts b/src/data/version.ts index fef08f9..213b03a 100644 --- a/src/data/version.ts +++ b/src/data/version.ts @@ -1,4 +1,4 @@ -export const VERSION = '0.8.0'; +export const VERSION = '0.9.0'; export interface ChangelogEntry { version: string; @@ -7,6 +7,13 @@ export interface ChangelogEntry { } export const CHANGELOG: ChangelogEntry[] = [ + { + version: '0.9.0', + date: '2026-08-08', + changes: [ + 'Nytt: Kodex — tolv olåsbara textfragment om platser, saker och folk, upplåsta allteftersom dread/ritual/troende stiger eller specifika händelser inträffar. Samma mönster som Thön/Vildhjartas egen Kodex.', + ], + }, { version: '0.8.0', date: '2026-08-08', diff --git a/src/lore.ts b/src/lore.ts new file mode 100644 index 0000000..b8e348b --- /dev/null +++ b/src/lore.ts @@ -0,0 +1,89 @@ +import type { State } from './engine.ts'; + +export interface LoreEntry { + id: string; + title: string; + body: string; + unlock: (s: State) => boolean; +} + +// Unlockable fragments describing places, things, and people from the +// established canon in LORE-BIBLE.md — same pattern as vildhjarta/Thön's +// Kodex. Deliberately does not resolve anything LORE-BIBLE.md lists under +// "what is deliberately never explained" (the warm chair's occupant, the +// sunken city itself, Rolf, Den Sovande's true shape) — these entries +// deepen the atmosphere around those mysteries, they don't answer them. +export const LORE: LoreEntry[] = [ + { + id: 'kallaren', + title: 'Källaren i Rosengård', + body: 'Ett källarförråd som blev något annat. Ingen av er minns exakt när. Hyresvärden vet fortfarande inte om det. Nio stolar står där nu. Ingen av er köpte den nionde.', + unlock: () => true, + }, + { + id: 'den_sovande', + title: 'Den Sovande', + body: 'Inget dokument ni hittat ger den ett riktigt namn. "Den Sovande" är vad ni kallar den, för att ni måste kalla den något. Sover är förresten fel ord också — det närmaste ni kommit är "ofärdig". Vad den väntar på att bli färdig med säger ingen källa. Kanske vet ingen källa.', + unlock: s => s.dread >= 20, + }, + { + id: 'skriften', + title: 'Skriften', + body: 'Hittades inmurad bakom kakel från 1923 på Davidshallsgatan. Den skriver sig själv om nätterna. Rör sig om man tittar bort. En doktorand i lingvistik översatte tre rader av misstag, medan hon letade efter något helt annat. Hon vet fortfarande inte vad hon hittade. Det gör ni.', + unlock: s => s.upgrades.has('the_manuscript'), + }, + { + id: 'anderna', + title: 'Änderna vid Ribersborg', + body: 'De vänder sig mot Turning Torso i ett tillstånd som liknar tillbedjan mer än väder borde förklara. Ingen ornitolog har velat sätta sitt namn på en förklaring. Änderna vet mer än ni gör. Det är inte ett skämt.', + unlock: s => s.dread >= 15, + }, + { + id: 'varnhemstorget_cirkeln', + title: 'Cirkeln på Värnhemstorget', + body: 'Ritad med krita — det måste vara krita, ingen vet varför, alla känner det. Kommunen tvättar bort den varje morgon. Den är tillbaka varje kväll, och varje gång lite större än ni ritade den.', + unlock: s => s.areas.varnhem, + }, + { + id: 'turning_torso', + title: 'Turning Torso', + body: 'Calatrava-Santiago ritade byggnaden som en vridande ryggrad. Det är inte en metafor. Den har alltid lyssnat. Nu ber ni den svara, och den gör det — ett ljud under hörselgränsen som ni känner i tänderna snarare än öronen.', + unlock: s => s.areas.torso, + }, + { + id: 'talet_13', + title: 'Om talet 13', + body: 'Tretton minuter mellan avgångar som inte finns på någon tidtabell. Tretton meter i diameter, krympande. Tretton slag klockan tolv. Ingen av er valde talet. Det bara börjar dyka upp, och när ni väl sett det slutar ni kunna sluta se det.', + unlock: s => s.dread >= 40, + }, + { + id: 'kommissarie_fors', + title: 'Kommissarie Fors', + body: 'Grova brott. Hon drömmer om era möten utan att veta att hon gör det — ni har satt ut en stol åt henne, och hon sitter i den, i drömmen, varje gång. Varför just hon, varför just era möten, är en fråga ingen av er ställt högt än.', + unlock: s => s.usedEventIds.has('coc_investigator_dream'), + }, + { + id: 'bengt_ake_sekten', + title: 'Bengt-Åke och det äldre arvet', + body: 'Han köper en namnlös tidning på Pressbyrån varje dag och ser nöjd ut. Visade sig tillhöra en helt annan sekt sedan 1987 — vars egen linje går tillbaka till 1840-talet. Samordningsmötet gick förvånansvärt bra. Ni har inte frågat vad de faktiskt tillber. Det känns oartigt.', + unlock: s => s.usedEventIds.has('k_bengt_ake'), + }, + { + id: 'staden_pa_botten', + title: 'Staden på botten', + body: 'Vattnet i Öresund fortsätter sjunka, och på botten syns nu en stad. Det är inte en gammal stad — byggnaderna är moderna. En av dem har belysning. Ingen av er har föreslagit att undersöka den närmare. Ingen behöver föreslå det.', + unlock: s => s.usedEventIds.has('k_oresund_rises'), + }, + { + id: 'den_varma_stolen', + title: 'Den varma stolen', + body: 'Källarens nionde stol har alltid varit lite för varm, från första mötet. Ni har äntligen alla nio platser fyllda. Ingen pratar om vem som sitter i den varma. Ni har alla sett att den är upptagen. Ingen av er har sett vem.', + unlock: s => s.upgrades.has('nine_chairs_full'), + }, + { + id: 'skriften_receptet', + title: 'Ett recept, inte en besvärjelse', + body: 'Skriftens sista sida skrev sig själv på en enda natt. Ni hade väntat er en besvärjelse, ord som gör något om de sägs rätt. Det ni fick var mer praktiskt än så — steg, i ordning, med mått. Det gör det varken mindre eller mer verkligt. Bara mer skrämmande på ett sätt ni inte väntat er.', + unlock: s => s.upgrades.has('grand_ritual_prep'), + }, +]; diff --git a/src/ui.ts b/src/ui.ts index b3fe6a9..8011111 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -2,6 +2,7 @@ import type { State } from './engine.ts'; import { UPGRADES } from './upgrades.ts'; import { VERSION, CHANGELOG } from './data/version.ts'; import { CHARACTERS } from './characters.ts'; +import { LORE } from './lore.ts'; export function initUI( app: HTMLElement, @@ -19,6 +20,7 @@ export function initUI(
+
@@ -188,6 +190,19 @@ export function initUI( +
+
+
+ Kodex + +
+
+

Fragment ni samlat på er — platser, saker, folk. Fylls i allteftersom ritualen fördjupas.

+
+
+
+
+
@@ -245,6 +260,15 @@ export function initUI( }); }); + const loreModal = document.getElementById('lore-modal')!; + const loreInner = document.getElementById('lore-modal-inner')!; + document.getElementById('lore-btn')!.addEventListener('click', () => { + renderLoreList(latestState); + loreModal.classList.toggle('visible'); + }); + document.getElementById('lore-close')!.addEventListener('click', () => loreModal.classList.remove('visible')); + loreModal.addEventListener('click', (e) => { if (!loreInner.contains(e.target as Node)) loreModal.classList.remove('visible'); }); + const lightbox = document.getElementById('lightbox')!; const lightboxImg = document.getElementById('lightbox-img') as HTMLImageElement; const lightboxCaption = document.getElementById('lightbox-caption')!; @@ -270,6 +294,7 @@ export function initUI( if (e.key !== 'Escape') return; if (lightbox.classList.contains('visible')) { lightbox.classList.remove('visible'); return; } if (charactersModal.classList.contains('visible')) { charactersModal.classList.remove('visible'); return; } + if (loreModal.classList.contains('visible')) { loreModal.classList.remove('visible'); return; } if (helpModal.classList.contains('visible')) { helpModal.classList.remove('visible'); return; } }); @@ -489,6 +514,21 @@ function renderLog(state: State): void { } } +function renderLoreList(state: State | null): void { + const list = document.getElementById('lore-list')!; + if (!list) return; + const unlocked = state ? LORE.filter(l => l.unlock(state)) : []; + const lockedCount = LORE.length - unlocked.length; + list.innerHTML = unlocked.map(l => ` +
+
${l.title}
+

${l.body}

+
+ `).join('') + (lockedCount > 0 + ? `

…${lockedCount} fragment kvarstår olästa. Fördjupa ritualen för att hitta fler.

` + : ''); +} + function renderCharacterGrid(state: State | null): void { const grid = document.getElementById('characters-grid')!; const met = new Set(state?.usedEventIds ?? []);