Location thumbnails on the map, full-art archive mode, more content

- Map markers now show a tiny thumbnail preview in their tooltip when
  art exists for that location (same missing-asset-is-fine fallback
  as character portraits — onerror just removes the broken img).
- Two new locations, both free-roam from the start: Möllevångstorget
  and Malmö universitet (the latter ties into the researcher
  character's arc). VISITABLE_LOCATION_IDS picks them up automatically
  since it's derived from the data, not a separate list.
- Sällskapet gets a second mode: "Fullständigt arkiv" shows every
  character's real portrait and bio regardless of story progress
  (bypassing the usedEventIds lock), and clicking any known portrait
  opens a lightbox at full size instead of the cropped card thumbnail.
- All 9 character portraits are now in and wired (fixed two more
  filename mismatches along the way: anneli-karin and the rest use
  hyphens, not underscores, matching what was actually generated).
- More content: 2 new location-visit variants each for the two new
  areas, 6 new Standard events (including a found 1930s photograph
  that seems to show Lindqvist in a room nobody recognizes), 4 new
  Kaos events tied to the new locations and existing ones.
- Caught and fixed a bug in my own new content before commit: an
  event's requires.completedEventId pointed at an upgrade id, which
  can never appear in usedEventIds — switched to a minRitual check
  that actually gates on the right precondition.

Verified: 15/15 tests, clean typecheck and build, and a real
headless-browser pass — all 9 map markers present, full-archive mode
correctly bypassing locks, and the lightbox opening at full size with
a caption.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-08-01 21:09:43 +02:00
co-authored by Claude Sonnet 5
parent aeedf0abb5
commit 899ab72c80
11 changed files with 265 additions and 6 deletions
+42 -2
View File
@@ -84,7 +84,11 @@ export function initUI(
<button id="characters-close" class="help-close">✕</button>
</div>
<div class="help-modal-body">
<p class="help-flavor" style="margin-bottom:14px;">De som suttit vid bordet. Vissa längre än andra.</p>
<div class="archive-toggle" role="group" aria-label="Visningsläge">
<button type="button" class="archive-toggle-btn active" data-archive="story">Under spelets gång</button>
<button type="button" class="archive-toggle-btn" data-archive="full">Fullständigt arkiv</button>
</div>
<p class="help-flavor" id="characters-hint" style="margin-bottom:14px;">De som suttit vid bordet. Vissa längre än andra.</p>
<div id="characters-grid" class="characters-grid"></div>
</div>
</div>
@@ -182,6 +186,12 @@ export function initUI(
</div>
</div>
</div>
<div id="lightbox" class="lightbox">
<button id="lightbox-close" class="lightbox-close" aria-label="Stäng">✕</button>
<img id="lightbox-img" alt="">
<div id="lightbox-caption" class="lightbox-caption"></div>
</div>
`;
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<HTMLButtonElement>('.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<HTMLElement>('[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 `
<div class="character-card locked">