feat: Adding audio player + music to play

This commit is contained in:
gauvainboiche
2026-10-25 22:35:53 +01:00
parent 7a525d084a
commit dd4714f499
39 changed files with 850 additions and 81 deletions
+52 -5
View File
@@ -26,6 +26,9 @@ import { createEndgame } from "./ui/endgame.js";
import { createMenu } from "./ui/menu.js";
import { createShelf } from "./ui/shelf.js";
import { createAudio } from "./ui/audio.js";
import { createMusic } from "./ui/music.js";
import { createWalkman } from "./ui/walkman.js";
import { DEFAULT_MIX } from "./save/storage.js";
const params = new URLSearchParams(location.search);
const getJson = (url) => fetch(url).then((r) => r.json());
@@ -54,7 +57,23 @@ let settings = loadSettings(storage);
const browserLang = navigator.language?.startsWith("fr") ? "fr" : "en";
await initI18n(params.get("lang") ?? settings.lang ?? browserLang);
const audio = createAudio({ enabled: () => settings.sound, volume: () => settings.volume });
const audio = createAudio({ enabled: () => settings.sound, volume: () => settings.volume, mix: () => settings.mix });
// Background music from the music folder, driven by the cassette player on the desk.
const music = createMusic({
getVolume: () => settings.volume * settings.mix.music,
onUserChoice: (on) => { if (settings.musicOn !== on) applySettings({ musicOn: on }); },
});
let gestured = false;
const startMusic = () => gestured && music.unlockedByGesture(settings.musicOn);
for (const ev of ["pointerdown", "keydown"]) {
document.addEventListener(ev, (e) => {
if (e.target.closest?.("#walkman")) return; // pressing a walkman key is its own choice
gestured = true;
startMusic();
}, { passive: true });
}
music.load().then(startMusic);
const DEV_PARAMS = ["seed", "pronouns", "alloc", "skip", "card", "dev", "new"];
const devRun = DEV_PARAMS.some((k) => params.has(k)) && !params.has("menu");
@@ -79,9 +98,11 @@ const el = {
news: document.getElementById("newspaper"),
menu: document.getElementById("menu"),
shelf: document.getElementById("shelf"),
walkman: document.getElementById("walkman"),
};
const pad = document.getElementById("pad");
for (const id of ["dash", "folder", "cal", "news"]) tilt(el[id], id);
tilt(el.walkman, "walkman", 0.5);
// A new week brings a proposal to the tray; with none eligible the week is simply empty.
function prepareWeek() {
@@ -136,6 +157,7 @@ function build() {
ledger: createLedger(el.ledger, scenario, { onConfirm: () => { audio.play("sign"); render(); } }),
cal: createCalendar(el.cal, balance, { onSpeed, onMenu: openMenu }),
shelf: createShelf(el.shelf, balance, { getView: () => view, onView: setView, onSpeed, onMenu: openMenu }),
walkman: createWalkman(el.walkman, { music, onPress: () => audio.play("click") }),
news: createNewspaper(el.news, newsTpl),
alerts: createTelegram(el.alerts, { alerts: endgame.alerts, onDismiss }),
endgame: createEndgame(el.endgame, { newsName: newsTpl.name, endgame, cards, onAgain: quitToMenu }),
@@ -218,7 +240,7 @@ function quitToMenu() {
panels?.decision.destroy();
state = null;
panels = null;
for (const id of ["dash", "folder", "ledger", "alerts", "endgame", "cal", "news", "shelf"]) el[id].innerHTML = "";
for (const id of ["dash", "folder", "ledger", "alerts", "endgame", "cal", "news", "shelf", "walkman"]) el[id].innerHTML = "";
document.title = t("app.title");
el.ledger.hidden = true;
el.alerts.hidden = true;
@@ -231,9 +253,12 @@ function quitToMenu() {
function applySettings(patch) {
const langChanged = patch.lang && patch.lang !== getLang();
settings = { ...settings, ...patch };
const { resetMix, ...rest } = patch;
settings = { ...settings, ...rest, mix: resetMix ? { ...DEFAULT_MIX } : { ...settings.mix, ...rest.mix } };
if (resetMix) settings.volume = 0.6;
saveSettings(storage, settings);
if (patch.sound === true || patch.volume !== undefined) audio.play("click");
if (patch.sound === true) audio.play("click");
if (patch.volume !== undefined || patch.mix?.music !== undefined || resetMix) music.applyVolume();
if (!langChanged) return;
setLang(patch.lang);
document.title = t("app.title");
@@ -241,6 +266,12 @@ function applySettings(patch) {
menu.refresh();
}
// The sound heard when a mixer slider is released.
const PREVIEW = {
master: "chime", stamp: "stampYes", paper: "paper", page: "page", sign: "sign", telegram: "telegram", alert: "alert",
chime: "chime", tick: "tick", countdown: "clockTick", click: "click", ending: "fanfare",
};
const menu = createMenu(el.menu, {
content,
storage,
@@ -252,6 +283,7 @@ const menu = createMenu(el.menu, {
onResume: closeMenu,
onQuit: quitToMenu,
onSettings: applySettings,
onPreview: (key) => audio.play(PREVIEW[key] ?? "click"),
},
});
@@ -345,7 +377,7 @@ function step(now) {
if (!r.advanced) break;
advanced = true;
if (r.events.weeklyCard) prepareWeek();
if (!document.hidden && state.speed <= 2) audio.play(state.day % 2 ? "tick" : "tock");
if (!document.hidden && state.speed <= 2 && !isDayBeforeIssue()) audio.play(state.day % 2 ? "tick" : "tock");
if (isWaiting(state) || state.over) break;
}
if (advanced) render();
@@ -355,12 +387,27 @@ function step(now) {
const devMsPerDay = devRun && params.has("msPerDay") ? Number(params.get("msPerDay")) : 0; // dev: faster days for tests
const msPerDay = () => devMsPerDay || (settings.slow ? balance.time.slowMsPerDay : balance.time.msPerDay);
// The day before a new proposal arrives (the Sunday before each Monday), a small clock ticks while that day runs.
const isDayBeforeIssue = () => (state.day + 1) % balance.time.daysPerWeek === 0;
let lastCountdown = "";
function countdownTicks(p) {
if (!state || menuOpen || state.speed <= 0 || state.over || isWaiting(state) || document.hidden || !isDayBeforeIssue()) return;
const perDay = state.speed >= 4 ? 10 : 20; // the ticks follow the dial: faster at higher speeds
const n = Math.floor(p * perDay);
const key = `${state.day}:${n}`;
if (key === lastCountdown) return;
lastCountdown = key;
audio.play(n % 2 ? "clockTock" : "clockTick");
}
function frame(now) {
step(now);
if (panels) {
const p = Math.min(1, clock.acc / msPerDay());
panels.cal.clockHand(p);
panels.shelf.clockHand(p);
countdownTicks(p);
}
requestAnimationFrame(frame);
}