Ajoute un avertissement sonore pour la phase cube
This commit is contained in:
150
app.js
150
app.js
@@ -9,6 +9,7 @@ const DEFAULT_MOVE_LIMIT_MS = 20000;
|
||||
const TIME_MODE_INITIAL_CLOCK_MS = 600000;
|
||||
const CUBE_TIME_CAP_MS = 120000;
|
||||
const CUBE_START_HOLD_MS = 2000;
|
||||
const CUBE_PHASE_ALERT_NAVIGATION_DELAY_MS = 700;
|
||||
|
||||
const PRESETS = {
|
||||
fast: {
|
||||
@@ -56,6 +57,8 @@ window.requestAnimationFrame(() => {
|
||||
|
||||
let match = readStoredMatch();
|
||||
let dirty = false;
|
||||
let audioContext = null;
|
||||
let cubePhaseAlertRetryCleanup = null;
|
||||
|
||||
if (match) {
|
||||
if (normalizeRecoveredMatch(match)) {
|
||||
@@ -67,6 +70,8 @@ if (match) {
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", flushState);
|
||||
document.addEventListener("pointerdown", tryUnlockAudioContext, { passive: true });
|
||||
document.addEventListener("keydown", tryUnlockAudioContext);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) {
|
||||
if (match) {
|
||||
@@ -110,6 +115,120 @@ function syncViewportHeight() {
|
||||
document.documentElement.style.setProperty("--app-viewport-height", `${Math.round(viewportHeight)}px`);
|
||||
}
|
||||
|
||||
function getAudioContext() {
|
||||
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
||||
if (!AudioContextClass) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!audioContext) {
|
||||
try {
|
||||
audioContext = new AudioContextClass();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return audioContext;
|
||||
}
|
||||
|
||||
function tryUnlockAudioContext() {
|
||||
const context = getAudioContext();
|
||||
if (!context || context.state === "running") {
|
||||
return;
|
||||
}
|
||||
|
||||
context.resume().catch(() => undefined);
|
||||
}
|
||||
|
||||
function playCubePhaseAlert() {
|
||||
const context = getAudioContext();
|
||||
if (!context || context.state !== "running") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pattern = [
|
||||
{ frequency: 740, offset: 0, duration: 0.12, gain: 0.035 },
|
||||
{ frequency: 988, offset: 0.18, duration: 0.12, gain: 0.04 },
|
||||
{ frequency: 1318, offset: 0.36, duration: 0.2, gain: 0.05 },
|
||||
];
|
||||
const startAt = context.currentTime + 0.02;
|
||||
|
||||
pattern.forEach(({ frequency, offset, duration, gain }) => {
|
||||
const oscillator = context.createOscillator();
|
||||
const envelope = context.createGain();
|
||||
const toneStartAt = startAt + offset;
|
||||
|
||||
oscillator.type = "triangle";
|
||||
oscillator.frequency.setValueAtTime(frequency, toneStartAt);
|
||||
envelope.gain.setValueAtTime(0.0001, toneStartAt);
|
||||
envelope.gain.exponentialRampToValueAtTime(gain, toneStartAt + 0.02);
|
||||
envelope.gain.exponentialRampToValueAtTime(0.0001, toneStartAt + duration);
|
||||
|
||||
oscillator.connect(envelope);
|
||||
envelope.connect(context.destination);
|
||||
oscillator.start(toneStartAt);
|
||||
oscillator.stop(toneStartAt + duration + 0.03);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function clearCubePhaseAlertRetry() {
|
||||
if (typeof cubePhaseAlertRetryCleanup === "function") {
|
||||
cubePhaseAlertRetryCleanup();
|
||||
cubePhaseAlertRetryCleanup = null;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleCubePhaseAlertRetry(storedMatch) {
|
||||
if (!storedMatch?.cube?.phaseAlertPending || cubePhaseAlertRetryCleanup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tryPlayOnGesture = () => {
|
||||
tryUnlockAudioContext();
|
||||
if (!playCubePhaseAlert()) {
|
||||
return;
|
||||
}
|
||||
|
||||
storedMatch.cube.phaseAlertPending = false;
|
||||
dirty = true;
|
||||
persistMatch();
|
||||
clearCubePhaseAlertRetry();
|
||||
};
|
||||
|
||||
const events = ["pointerdown", "keydown"];
|
||||
cubePhaseAlertRetryCleanup = () => {
|
||||
events.forEach((eventName) => {
|
||||
document.removeEventListener(eventName, tryPlayOnGesture);
|
||||
});
|
||||
};
|
||||
|
||||
events.forEach((eventName) => {
|
||||
document.addEventListener(eventName, tryPlayOnGesture, { passive: eventName === "pointerdown" });
|
||||
});
|
||||
}
|
||||
|
||||
function maybePlayPendingCubePhaseAlert(storedMatch) {
|
||||
if (!storedMatch?.cube?.phaseAlertPending) {
|
||||
clearCubePhaseAlertRetry();
|
||||
return false;
|
||||
}
|
||||
|
||||
tryUnlockAudioContext();
|
||||
if (!playCubePhaseAlert()) {
|
||||
scheduleCubePhaseAlertRetry(storedMatch);
|
||||
return false;
|
||||
}
|
||||
|
||||
storedMatch.cube.phaseAlertPending = false;
|
||||
dirty = true;
|
||||
persistMatch();
|
||||
clearCubePhaseAlertRetry();
|
||||
return true;
|
||||
}
|
||||
|
||||
function initSetupPage() {
|
||||
const form = document.querySelector("#setupForm");
|
||||
const summary = document.querySelector("#setupSummary");
|
||||
@@ -325,7 +444,25 @@ function initChronoPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
let cubeNavigationTimeoutId = null;
|
||||
const goToCubePage = () => {
|
||||
if (cubeNavigationTimeoutId !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (match?.cube?.phaseAlertPending) {
|
||||
tryUnlockAudioContext();
|
||||
if (playCubePhaseAlert()) {
|
||||
match.cube.phaseAlertPending = false;
|
||||
dirty = true;
|
||||
persistMatch();
|
||||
cubeNavigationTimeoutId = window.setTimeout(() => {
|
||||
replaceTo("cube.html");
|
||||
}, CUBE_PHASE_ALERT_NAVIGATION_DELAY_MS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dirty = true;
|
||||
persistMatch();
|
||||
replaceTo("cube.html");
|
||||
@@ -681,6 +818,8 @@ function initCubePage() {
|
||||
return;
|
||||
}
|
||||
|
||||
maybePlayPendingCubePhaseAlert(match);
|
||||
|
||||
const refs = {
|
||||
title: document.querySelector("#cubeTitle"),
|
||||
subtitle: document.querySelector("#cubeSubtitle"),
|
||||
@@ -790,6 +929,7 @@ function initCubePage() {
|
||||
resultModalKey = null;
|
||||
replayCubePhase(match);
|
||||
dirty = true;
|
||||
maybePlayPendingCubePhaseAlert(match);
|
||||
render();
|
||||
return;
|
||||
}
|
||||
@@ -806,6 +946,7 @@ function initCubePage() {
|
||||
resultModalKey = null;
|
||||
replayCubePhase(match);
|
||||
dirty = true;
|
||||
maybePlayPendingCubePhaseAlert(match);
|
||||
render();
|
||||
});
|
||||
|
||||
@@ -1303,6 +1444,7 @@ function createMatch(config) {
|
||||
running: false,
|
||||
startedAt: null,
|
||||
elapsedMs: 0,
|
||||
phaseAlertPending: false,
|
||||
times: {
|
||||
white: null,
|
||||
black: null,
|
||||
@@ -1405,6 +1547,7 @@ function normalizeRecoveredMatch(storedMatch) {
|
||||
running: false,
|
||||
startedAt: null,
|
||||
elapsedMs: 0,
|
||||
phaseAlertPending: false,
|
||||
times: { white: null, black: null },
|
||||
playerState: {
|
||||
white: createCubePlayerState(),
|
||||
@@ -1424,6 +1567,11 @@ function normalizeRecoveredMatch(storedMatch) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof storedMatch.cube.phaseAlertPending !== "boolean") {
|
||||
storedMatch.cube.phaseAlertPending = false;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
storedMatch.cube.playerState.white = normalizeCubePlayerState(storedMatch.cube.playerState.white);
|
||||
storedMatch.cube.playerState.black = normalizeCubePlayerState(storedMatch.cube.playerState.black);
|
||||
storedMatch.cube.running = isAnyCubeTimerRunning(storedMatch);
|
||||
@@ -1537,6 +1685,7 @@ function openCubePhase(storedMatch, reason = "") {
|
||||
storedMatch.cube.running = false;
|
||||
storedMatch.cube.startedAt = null;
|
||||
storedMatch.cube.elapsedMs = 0;
|
||||
storedMatch.cube.phaseAlertPending = true;
|
||||
storedMatch.cube.times = { white: null, black: null };
|
||||
storedMatch.cube.playerState = {
|
||||
white: createCubePlayerState(),
|
||||
@@ -1730,6 +1879,7 @@ function replayCubePhase(storedMatch) {
|
||||
storedMatch.cube.running = false;
|
||||
storedMatch.cube.startedAt = null;
|
||||
storedMatch.cube.elapsedMs = 0;
|
||||
storedMatch.cube.phaseAlertPending = true;
|
||||
storedMatch.cube.times = { white: null, black: null };
|
||||
storedMatch.cube.playerState = {
|
||||
white: createCubePlayerState(),
|
||||
|
||||
Reference in New Issue
Block a user