Add Kodex: unlockable lore fragments about places, things, and people
Docker / build-and-push (push) Successful in 1m54s
Docker / build-and-push (push) Successful in 1m54s
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e449cba288
commit
61c56ce2b2
+8
-1
@@ -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',
|
||||
|
||||
+89
@@ -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'),
|
||||
},
|
||||
];
|
||||
@@ -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(
|
||||
<div class="header-actions">
|
||||
<button id="pause-btn" class="help-btn" title="Pausa spelet">Pausa</button>
|
||||
<button id="characters-btn" class="help-btn" title="Sällskapet">Sällskapet</button>
|
||||
<button id="lore-btn" class="help-btn" title="Kodex">Kodex</button>
|
||||
<button id="abandon-btn" class="help-btn" title="Överge kulten">Avbryt</button>
|
||||
<button id="help-btn" class="help-btn" title="Hjälp & Ändringslogg">? v${VERSION}</button>
|
||||
</div>
|
||||
@@ -188,6 +190,19 @@ export function initUI(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="lore-modal" class="help-modal">
|
||||
<div class="help-modal-inner" id="lore-modal-inner">
|
||||
<div class="help-modal-header">
|
||||
<span class="help-modal-title">Kodex</span>
|
||||
<button id="lore-close" class="help-close">✕</button>
|
||||
</div>
|
||||
<div class="help-modal-body">
|
||||
<p class="help-flavor" style="margin-bottom:14px;">Fragment ni samlat på er — platser, saker, folk. Fylls i allteftersom ritualen fördjupas.</p>
|
||||
<div id="lore-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="event-modal" class="help-modal">
|
||||
<div class="help-modal-inner" id="event-modal-inner">
|
||||
<div class="help-modal-body">
|
||||
@@ -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 => `
|
||||
<div class="help-section" style="margin-bottom:16px;">
|
||||
<div class="help-section-title" style="color:var(--accent);border-bottom:none;padding-bottom:0;">${l.title}</div>
|
||||
<p class="help-flavor" style="font-style:normal;color:var(--text);">${l.body}</p>
|
||||
</div>
|
||||
`).join('') + (lockedCount > 0
|
||||
? `<p class="help-flavor">…${lockedCount} fragment kvarstår olästa. Fördjupa ritualen för att hitta fler.</p>`
|
||||
: '');
|
||||
}
|
||||
|
||||
function renderCharacterGrid(state: State | null): void {
|
||||
const grid = document.getElementById('characters-grid')!;
|
||||
const met = new Set(state?.usedEventIds ?? []);
|
||||
|
||||
Reference in New Issue
Block a user