diff --git a/public/img/characters/anneli-karin.png b/public/img/characters/anneli-karin.png new file mode 100644 index 0000000..76707f6 Binary files /dev/null and b/public/img/characters/anneli-karin.png differ diff --git a/public/img/characters/journalist.png b/public/img/characters/journalist.png new file mode 100644 index 0000000..f8fe897 Binary files /dev/null and b/public/img/characters/journalist.png differ diff --git a/public/img/characters/researcher.png b/public/img/characters/researcher.png new file mode 100644 index 0000000..848e40d Binary files /dev/null and b/public/img/characters/researcher.png differ diff --git a/public/img/characters/sailor.png b/public/img/characters/sailor.png new file mode 100644 index 0000000..f940393 Binary files /dev/null and b/public/img/characters/sailor.png differ diff --git a/src/characters.ts b/src/characters.ts index 72013de..7135575 100644 --- a/src/characters.ts +++ b/src/characters.ts @@ -57,7 +57,7 @@ export const CHARACTERS: Character[] = [ role: 'Talar i sömnen. Inte på svenska.', bio: 'Talar i sömnen på ett språk ingen känner igen. Inspelningen, uppspelad baklänges, var lika obegriplig — fast på ett annat sätt.', unlockEventId: 'anneli_dream', - img: '/img/characters/anneli_karin.png', + img: '/img/characters/anneli-karin.png', }, { id: 'sailor', diff --git a/src/data/version.ts b/src/data/version.ts index 0766909..076cfa8 100644 --- a/src/data/version.ts +++ b/src/data/version.ts @@ -1,4 +1,4 @@ -export const VERSION = '0.5.0'; +export const VERSION = '0.6.0'; export interface ChangelogEntry { version: string; @@ -7,6 +7,17 @@ export interface ChangelogEntry { } export const CHANGELOG: ChangelogEntry[] = [ + { + version: '0.6.0', + date: '2026-08-01', + changes: [ + 'Alla porträtt i Sällskapet är nu på plats', + 'Nytt läge i Sällskapet: Fullständigt arkiv — se alla porträtt oavsett hur långt ni kommit, klicka för fullstorlek', + 'Platser på kartan visar nu en liten bild i förhandsvisningen (när konst finns för platsen)', + 'Två nya platser: Möllevångstorget och Malmö universitet, båda besökbara direkt från start', + 'Fler händelser tillagda, inklusive ett fotografi från 1930-talet som inte borde finnas', + ], + }, { version: '0.5.0', date: '2026-08-01', diff --git a/src/engine.ts b/src/engine.ts index 37eae1e..597639c 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -386,6 +386,28 @@ const LOCATION_VISITS: Record = { effect: { kaos: 8 }, lockedText: 'Stortorget är fullt av folk som inte vet vad de gör här heller. Du passar in.', }, + mollan: { + texts: [ + 'Marknadsstånden på Möllan säljer samma fisk som alltid, men ingen minns vilket hav den kom från. Den smakar rätt ändå. +kaos', + 'Ett gäng ungdomar sjunger något på ett språk som låter som tre olika språk samtidigt, i perfekt harmoni. +kaos', + 'Fontänen på torget rinner uppåt i en sekund innan den rättar till sig. Ingen annan såg det. Du är säker på att du såg det. +kaos', + 'En gatuförsäljare erbjuder en frukt du inte känner igen, från ett land ingen kan namnge. Den är söt. Den är för söt. +kaos', + 'Graffitin på muren mot innergården ändras varje gång du vänder ryggen till, alltid samma motiv, aldrig samma vinkel. +kaos', + ], + effect: { kaos: 10 }, + lockedText: 'Möllevångstorget är fullt av liv, musik, dofter från femtio kök samtidigt. Det är svårt att höra sig själv tänka här. Det är kanske poängen.', + }, + universitet: { + texts: [ + 'Biblioteket har ett rum för sällan efterfrågade samlingar. Din kod fungerar där ingen minns att du fått en. +andakt +dread', + 'En föreläsningssal står tom men whiteboarden är full av ekvationer ingen lärare skrivit. Den sista raden är inte matematik. +andakt +dread', + 'Doktoranderna på plan tre pratar lägre än vanligt när du går förbi. En av dem tittar upp för länge. +andakt +dread', + 'Bibliotekets sökkatalog listar en avhandling om Skriften, publicerad om tre år. Ingen kan checka ut den förrän dess. +andakt +dread', + 'Studenthälsan har en anmärkningsvärt hög andel patienter med sömnproblem i just den här korridoren. Ingen har satt det i samband med något. +andakt +dread', + ], + effect: { andakt: 12, dread: 6 }, + lockedText: 'Malmö universitet vid Niagara. Glas och stål och lysrör som surrar på en frekvens du inte borde kunna höra genom väggarna.', + }, }; export const VISITABLE_LOCATION_IDS = Object.keys(LOCATION_VISITS); diff --git a/src/events.ts b/src/events.ts index 5cf54c3..96680dc 100644 --- a/src/events.ts +++ b/src/events.ts @@ -743,6 +743,71 @@ export const STANDARD_EVENTS: GameEvent[] = [ weight: 5, requires: { minTroende: 4 }, }, + { + id: 'mollan_market_stall', + text: 'En marknadshandlare på Möllevångstorget säger att hon känner igen er, fast ingen av er har handlat hos henne förut. Hon lägger till en extra påse i kassen utan att fråga. Innehållet är inte på svenska.', + effect: { andakt: 12, kaos: 6 }, + weight: 6, + requires: { unlockedArea: 'rosengard' }, + }, + { + id: 'university_manuscript_hit', + text: 'En sökning på universitetsbiblioteket ger en enda träff på en fras ur Skriften, i en avhandling om maritim folktro från 1958. Författaren dog samma år. Boken är utlånad. Den har alltid varit utlånad.', + effect: { dread: 14, ritual: 3 }, + weight: 5, + requires: { minRitual: 15 }, + }, + { + id: 'choice_survey_call', + text: 'Statistiska centralbyrån ringer och frågar om er upplevda livskvalitet, på en skala ett till tio. Frågorna blir successivt mer specifika. Till slut frågar hen om ni "hör röster som inte är era egna, och om ni lyssnar."', + effect: {}, + weight: 5, + requires: { minDread: 25 }, + choices: [ + { + text: 'Svara ärligt', + effect: { hemlighet: -15, dread: 5 }, + resultText: 'Hen tackar för svaret utan att låta förvånad. Samtalet avslutas normalt. Ni är inte säkra på vem ni just pratade med.', + }, + { + text: 'Ljug och lägg på', + effect: { hemlighet: 5, kaos: 8 }, + resultText: 'Ni lägger på mitt i en mening. Telefonen ringer igen tre sekunder senare. Ni svarar inte.', + }, + ], + }, + { + id: 'bus_2_driver', + text: 'Föraren på linje 2 hälsar er vid namn när ni kliver på, trots att ingen av er sagt det. Bussen kör en rutt som inte finns i tidtabellen och släpper av er exakt utanför källaren, tio minuter tidigare än ni räknat med.', + effect: { dread: 9, andakt: 6 }, + weight: 6, + requires: { minDread: 15 }, + }, + { + id: 'malmo_live_reflection', + text: 'Glasfasaden på Malmö Live speglar en himmel med fel antal moln jämfört med den riktiga. Ni räknar två gånger. Talen stämmer inte överens med varandra heller.', + effect: { kaos: 9, dread: 6 }, + weight: 6, + }, + { + id: 'choice_old_photograph', + text: 'En gammal svartvit fotografi dyker upp i källaren, ingen minns var den kom ifrån. Den föreställer nio personer runt ett bord i exakt samma rum. Kläderna är från 1930-talet. En av personerna ser ut som Broder Lindqvist.', + effect: {}, + weight: 4, + requires: { minDread: 70, minRitual: 1 }, + choices: [ + { + text: 'Visa fotografiet för Lindqvist', + effect: { dread: 15, andakt: 10 }, + resultText: 'Lindqvist tittar länge. Han säger att han känner igen rummet men inte människorna. Han lägger fotografiet i sin ficka istället för att lämna tillbaka det.', + }, + { + text: 'Göm fotografiet', + effect: { kaos: 12, hemlighet: -5 }, + resultText: 'Ni lägger det längst ner i en låda. Nästa vecka ligger det på bordet igen, överst.', + }, + ], + }, ]; // ── Kaos events (Henderson-tier) ───────────────────────────────────────────── @@ -977,4 +1042,32 @@ export const KAOS_EVENTS: GameEvent[] = [ weight: 6, requires: { minDread: 35 }, }, + { + id: 'k_mollan_food_truck', + text: 'En food truck som ingen beställt eller godkänt har parkerat på Möllevångstorget och serverar en rätt som inte står på skylten till alla som frågar. Kommunen kan inte hitta några tillståndspapper. Truckens ägare finns inte i något register.', + effect: { kaos: -40, troende: 2, andakt: 20 }, + weight: 7, + requires: { minTroende: 3 }, + }, + { + id: 'k_university_thesis_defense', + text: 'En doktorandförsvar vid universitetet avbryts när opponenten ställer en fråga ingen i publiken hör men alla ser avhandlingsförfattaren svara på. Betyget blir godkänt med beröm. Ingen av professorerna minns frågan efteråt.', + effect: { kaos: -45, ritual: 6, dread: 20 }, + weight: 5, + requires: { minRitual: 10 }, + }, + { + id: 'k_all_clocks_torso', + text: 'Alla digitala klockor inom synhåll från Turning Torso har visat samma sekund i över en timme. Analoga klockor går som vanligt. Ingen teknisk avdelning kan förklara det, och ingen verkar särskilt intresserad av att försöka.', + effect: { kaos: -60, dread: 50, ritual: 8 }, + weight: 4, + requires: { unlockedArea: 'torso', minDread: 70 }, + }, + { + id: 'k_ribersborg_tide', + text: 'Tidvattnet vid Ribersborg går ut längre än någonsin uppmätt, i tio minuter, och avslöjar stenformationer på havsbotten som inte finns på någon sjökarta. Sedan kommer vattnet tillbaka som om inget hänt. Fotografierna på nätet försvinner inom en timme.', + effect: { kaos: -55, dread: 45, ritual: 6 }, + weight: 4, + requires: { minDread: 90 }, + }, ]; diff --git a/src/map.ts b/src/map.ts index b3c43bd..17e7725 100644 --- a/src/map.ts +++ b/src/map.ts @@ -18,8 +18,15 @@ const LOCATIONS: Omit[] = [ { id: 'ribersborg',lat: 55.6063, lng: 12.9652, label: 'Ribersborgsstranden' }, { id: 'davidshall',lat: 55.5963, lng: 13.0021, label: 'Davidshallsgatan' }, { id: 'stortorget',lat: 55.6047, lng: 13.0038, label: 'Stortorget' }, + { id: 'mollan', lat: 55.5904, lng: 13.0203, label: 'Möllevångstorget' }, + { id: 'universitet', lat: 55.5721, lng: 12.9998, label: 'Malmö universitet' }, ]; +// Optional tiny preview shown in each location's tooltip — same +// missing-asset-is-fine convention as the character portraits. Path is +// tried once per marker; a 404 just means the tooltip falls back to text. +const THUMB = (id: string): string => `/img/locations/${id}.png`; + type LeafletMap = ReturnType; type LeafletMarker = ReturnType; @@ -54,7 +61,14 @@ export function initMap(containerId: string, onClick: (id: string) => void): voi fillColor: '#1a1a2e', fillOpacity: 0.3, weight: 1, - }).addTo(map).bindTooltip(loc.label, { className: 'map-tooltip', permanent: false }); + }).addTo(map).bindTooltip( + `
+ + ${loc.label} +
`, + { className: 'map-tooltip', permanent: false }, + ); m.on('click', () => onClick(loc.id)); markers.set(loc.id, m); } @@ -71,6 +85,8 @@ export function updateMap(dread: number, areas: { rosengard: boolean; varnhem: b ribersborg: Math.min(2, Math.floor(dread / 45)), davidshall: Math.min(1, Math.floor(dread / 70)), stortorget: Math.min(2, Math.floor(dread / 55)), + mollan: Math.min(2, Math.floor(dread / 48)), + universitet: Math.min(2, Math.floor(dread / 65)), }; for (const [id, marker] of markers) { diff --git a/src/style.css b/src/style.css index 9c9a68b..68e29f9 100644 --- a/src/style.css +++ b/src/style.css @@ -161,12 +161,26 @@ body { color: var(--text) !important; font-family: inherit !important; font-size: 11px !important; - padding: 3px 8px !important; + padding: 4px !important; border-radius: 2px !important; box-shadow: none !important; } .map-tooltip::before { display: none !important; } +.map-tooltip-inner { + display: flex; + align-items: center; + gap: 6px; +} +.map-tooltip-thumb { + width: 40px; + height: 40px; + object-fit: cover; + border: 1px solid var(--border); + flex-shrink: 0; +} +.map-tooltip-inner span { padding-right: 2px; } + /* ── Upgrades ── */ .upgrade-btn { display: block; @@ -587,6 +601,69 @@ body { margin-left: 6px; } +.archive-toggle { + display: flex; + gap: 0; + margin-bottom: 12px; + border: 1px solid var(--border); + width: fit-content; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.06em; +} +.archive-toggle-btn { + appearance: none; + border: none; + background: transparent; + color: var(--dim); + padding: 7px 12px; + cursor: pointer; + font: inherit; + letter-spacing: inherit; +} +.archive-toggle-btn + .archive-toggle-btn { border-left: 1px solid var(--border); } +.archive-toggle-btn.active { background: var(--accent); color: #0d0d1a; } + +.character-card:not(.locked) .character-portrait { cursor: pointer; } +.character-card:not(.locked) .character-portrait:hover img { filter: grayscale(0) contrast(1.1) brightness(1.08); } + +.lightbox { + display: none; + position: fixed; + inset: 0; + background: rgba(4, 4, 10, 0.94); + z-index: 1100; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 12px; + padding: 40px; +} +.lightbox.visible { display: flex; } +.lightbox img { + max-width: min(90vw, 720px); + max-height: 78vh; + object-fit: contain; + border: 1px solid var(--border); +} +.lightbox-caption { + font-size: 12px; + color: var(--accent); + letter-spacing: 0.06em; + text-transform: uppercase; +} +.lightbox-close { + position: absolute; + top: 20px; + right: 24px; + background: none; + border: none; + color: var(--dim); + font-size: 20px; + cursor: pointer; +} +.lightbox-close:hover { color: var(--text); } + @media (max-width: 900px) { #app { grid-template-columns: 1fr; } .panel { border-right: none; border-bottom: 1px solid var(--border); } diff --git a/src/ui.ts b/src/ui.ts index 33ea51a..cfce89c 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -84,7 +84,11 @@ export function initUI(
-

De som suttit vid bordet. Vissa längre än andra.

+
+ + +
+

De som suttit vid bordet. Vissa längre än andra.

@@ -182,6 +186,12 @@ export function initUI( + + `; document.getElementById('overlay-btn')!.addEventListener('click', onRestart); @@ -212,6 +222,35 @@ export function initUI( document.getElementById('characters-close')!.addEventListener('click', () => charactersModal.classList.remove('visible')); charactersModal.addEventListener('click', (e) => { if (!charactersInner.contains(e.target as Node)) charactersModal.classList.remove('visible'); }); + document.querySelectorAll('.archive-toggle-btn').forEach((btn) => { + btn.addEventListener('click', () => { + document.querySelectorAll('.archive-toggle-btn').forEach((b) => b.classList.remove('active')); + btn.classList.add('active'); + archiveMode = btn.dataset['archive'] === 'full'; + document.getElementById('characters-hint')!.textContent = archiveMode + ? 'Fullständigt arkiv — alla porträtt, oavsett hur långt ni kommit. Klicka för att se dem i fullstorlek.' + : 'De som suttit vid bordet. Vissa längre än andra.'; + renderCharacterGrid(latestState); + }); + }); + + const lightbox = document.getElementById('lightbox')!; + const lightboxImg = document.getElementById('lightbox-img') as HTMLImageElement; + const lightboxCaption = document.getElementById('lightbox-caption')!; + document.getElementById('characters-grid')!.addEventListener('click', (e) => { + const portrait = (e.target as Element).closest('.character-card:not(.locked) .character-portrait'); + if (!portrait) return; + const card = portrait.closest('[data-char]'); + const img = portrait.querySelector('img'); + if (!card || !img?.src) return; + lightboxImg.src = img.src; + lightboxCaption.textContent = card.querySelector('.character-name')?.textContent ?? ''; + lightbox.classList.add('visible'); + }); + document.getElementById('lightbox-close')!.addEventListener('click', () => lightbox.classList.remove('visible')); + lightbox.addEventListener('click', (e) => { if (e.target === lightbox) lightbox.classList.remove('visible'); }); + document.addEventListener('keydown', (e) => { if (e.key === 'Escape') lightbox.classList.remove('visible'); }); + // Split the map container: map on top, log below const mapContainer = document.getElementById('map-container')!; mapContainer.style.cssText = 'display:flex;flex-direction:column;overflow:hidden;height:100%;'; @@ -228,6 +267,7 @@ export function initUI( } let latestState: State | null = null; +let archiveMode = false; export function renderState(state: State): void { latestState = state; @@ -421,7 +461,7 @@ function renderCharacterGrid(state: State | null): void { const grid = document.getElementById('characters-grid')!; const met = new Set(state?.usedEventIds ?? []); grid.innerHTML = CHARACTERS.map(c => { - const known = !c.unlockEventId || met.has(c.unlockEventId); + const known = archiveMode || !c.unlockEventId || met.has(c.unlockEventId); if (!known) { return `