picker/js/main.js
2026-10-09 13:45:54 +02:00

136 lines
3.7 KiB
JavaScript

// Wires the modules together and runs the frame loop.
import { createSettings } from './settings.js';
import { setLang, detectLang } from './i18n.js';
import { Game } from './game.js';
import { Renderer } from './renderer.js';
import { createInput, blockBrowserGestures } from './input.js';
import { sfx, buzz, setMuted, setHaptics, installAudioUnlock } from './audio.js';
import { createWakeLock } from './device.js';
import { getTheme } from './themes.js';
import { createUI } from './ui.js';
const params = new URLSearchParams(location.search);
const DEBUG = params.has('debug') && params.get('debug') !== '0';
const WAKE_HOLD_MS = 2 * 60 * 1000; // keep the screen on this long after the last touch
const canvas = document.getElementById('stage');
const settings = createSettings();
setLang(settings.get().lang || detectLang());
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');
const renderer = new Renderer(canvas);
const wakeLock = createWakeLock();
const game = new Game({
emit: onGameEvent,
getSettings: () => settings.get(),
isReduced: () => reducedMotion.matches,
});
const noTouch = navigator.maxTouchPoints === 0 && matchMedia('(pointer: fine)').matches;
const ui = createUI({ settings, debug: DEBUG, showDesktopHint: noTouch && !DEBUG });
function applySettings(s) {
const theme = getTheme(s.theme);
renderer.setTheme(theme);
document.documentElement.dataset.theme = s.theme;
document.querySelector('meta[name="theme-color"]').content = theme.bg;
setMuted(s.muted);
setHaptics(s.haptics);
}
applySettings(settings.get());
settings.subscribe(applySettings);
// ---- game events -> sound, haptics, visuals, text ----
function onGameEvent(type, data = {}) {
const now = performance.now();
renderer.onEvent(type, data, now, game);
ui.onEvent(type, data);
switch (type) {
case 'join':
sfx.join(data.ring.colorIdx);
buzz(8);
break;
case 'beat':
sfx.beat(data.index);
buzz(25);
break;
case 'tick':
sfx.tick(data.progress, data.final);
buzz(data.final ? 35 : 8);
break;
case 'reveal':
sfx.reveal();
buzz([90, 60, 450]);
break;
case 'eliminate':
sfx.crack();
if (data.chickened) sfx.chicken();
buzz([40, 30, 120]);
break;
case 'orderStep':
sfx.pop(data.n);
buzz(data.n === 1 ? 120 : 20);
break;
case 'chicken':
sfx.chicken();
buzz([60, 50, 60, 50, 60]);
break;
}
}
// ---- input ----
blockBrowserGestures(canvas);
installAudioUnlock();
const input = createInput(canvas, {
debug: DEBUG,
onDown: (id, x, y, now) => game.fingerDown(id, x, y, now),
onMove: (id, x, y) => game.fingerMove(id, x, y),
onUp: (id, now) => game.fingerUp(id, now),
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
input.clearReal();
game.cancelAll(performance.now());
}
});
if (DEBUG) {
document.body.classList.add('debug');
document.getElementById('debug-hud').hidden = false;
const n = Number(params.get('fingers'));
if (n > 0) input.addFakeCircle(Math.min(n, 12));
}
// ---- frame loop ----
let lastTouchAt = -Infinity;
function frame() {
const now = performance.now();
game.update(now);
renderer.draw(game, now);
ui.update(game, now);
if (game.anyDown()) lastTouchAt = now;
wakeLock.set(now - lastTouchAt < WAKE_HOLD_MS);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
// ---- PWA ----
if ('serviceWorker' in navigator && !DEBUG) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js').catch(() => {});
});
}
// Handy in the console while testing.
if (DEBUG) Object.assign(window, { game, settings, renderer });