Add intro screen, win/loss art, and a way to abandon the ritual

- Adds a full-screen intro/title screen (new 'intro' game phase) with
  a moody Malmö/Turning Torso background and a "Börja ritualen"
  button; the game no longer starts ticking until the player begins.
- Win and loss overlays now show matching generated artwork behind
  the existing text, instead of a flat scrim.
- New "Avbryt" header button lets the player abandon a run mid-game
  (with a confirm prompt) via a new 'abandoned' phase, reusing the
  existing overlay/restart flow.
- Source images converted from PNG (~2.1MB each) to WebP (~120KB
  each) and placed under public/img/ so Vite serves them as-is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-07-13 09:54:46 +02:00
parent 10c7999701
commit 4d47409e03
8 changed files with 130 additions and 10 deletions

View File

@@ -24,7 +24,7 @@ export interface State {
areas: { rosengard: boolean; varnhem: boolean; torso: boolean };
upgrades: Set<string>;
log: LogEntry[];
phase: 'playing' | 'won' | 'lost';
phase: 'intro' | 'playing' | 'won' | 'lost' | 'abandoned';
lostReason?: string;
usedEventIds: Set<string>;
flags: Set<string>;
@@ -69,7 +69,7 @@ export function createState(): State {
areas: { rosengard: false, varnhem: false, torso: false },
upgrades: new Set(),
log: [{ tick: 0, text: 'Mötet började i en fuktig källare i Rosengård. Nio stolar. En av dem är alltid lite för varm.', kind: 'system' }],
phase: 'playing',
phase: 'intro',
usedEventIds: new Set(),
flags: new Set(),
nextEventTick: EVENT_INTERVAL_TICKS,
@@ -78,6 +78,19 @@ export function createState(): State {
};
}
export function startGame(state: State): State {
if (state.phase !== 'intro') return state;
return { ...state, phase: 'playing' };
}
export function abandonGame(state: State): State {
return {
...state,
phase: 'abandoned',
log: [{ tick: state.tick, text: 'Ni lämnar källaren. Cirkeln bryts. Den Sovande sover vidare, ovetande, ofärdig.', kind: 'system' }, ...state.log],
};
}
function pickEvent(pool: GameEvent[], state: State): GameEvent | null {
const available = pool.filter(e => {
if (state.usedEventIds.has(e.id)) return false;