diff --git a/app.js b/app.js index bb5db86..24c0157 100644 --- a/app.js +++ b/app.js @@ -596,13 +596,13 @@ function initCubePage() { }); refs.whiteButton?.addEventListener("click", () => { - captureCubeTime(match, "white"); + handleCubeTap("white"); dirty = true; render(); }); refs.blackButton?.addEventListener("click", () => { - captureCubeTime(match, "black"); + handleCubeTap("black"); dirty = true; render(); }); @@ -618,17 +618,6 @@ function initCubePage() { return; } - if (!match.cube.running && match.cube.times.white === null && match.cube.times.black === null) { - startCubePhase(match); - dirty = true; - render(); - return; - } - - if (match.cube.running) { - return; - } - if (match.cube.times.white !== null && match.cube.times.black !== null) { if (match.config.mode === "twice" && match.cube.times.white === match.cube.times.black) { replayCubePhase(match); @@ -655,6 +644,23 @@ function initCubePage() { replaceTo("index.html"); }); + function handleCubeTap(color) { + if (match.result || match.phase !== "cube") { + return; + } + + if (match.cube.times[color] !== null) { + return; + } + + if (match.cube.playerState[color].running) { + captureCubeTime(match, color); + return; + } + + startCubeTimer(match, color); + } + function renderCubeZone(color) { const isWhite = color === "white"; const button = isWhite ? refs.whiteButton : refs.blackButton; @@ -662,10 +668,11 @@ function initCubePage() { const result = isWhite ? refs.whiteResult : refs.blackResult; const cap = isWhite ? refs.whiteCap : refs.blackCap; const hint = isWhite ? refs.whiteHint : refs.blackHint; + const playerState = match.cube.playerState[color]; const time = match.cube.times[color]; name.textContent = playerName(match, color); - result.textContent = time === null ? "--" : formatStopwatch(time); + result.textContent = formatCubePlayerTime(match, color); cap.textContent = renderCubeCap(match, time); if (match.result) { @@ -682,29 +689,23 @@ function initCubePage() { return; } - if (!match.cube.running) { - if (time !== null) { - button.textContent = "Temps capture"; - button.disabled = true; - hint.textContent = "Arret deja enregistre pour ce joueur."; - } else { - button.textContent = "Pret"; - button.disabled = true; - hint.textContent = "Attente du demarrage de la phase cube."; - } - return; - } - if (time !== null) { - button.textContent = "Temps capture"; + button.textContent = "Temps enregistre"; button.disabled = true; - hint.textContent = "Le temps de ce joueur est deja enregistre."; + hint.textContent = "Ce joueur a deja termine son cube."; return; } - button.textContent = "J'ai fini le cube"; + if (playerState.running) { + button.textContent = "J'ai fini le cube"; + button.disabled = false; + hint.textContent = "Tape au moment exact ou le cube est resolu."; + return; + } + + button.textContent = "Demarrer mon chrono"; button.disabled = false; - hint.textContent = "Tape au moment exact ou le cube est resolu."; + hint.textContent = "Chaque joueur lance son propre chrono quand il commence vraiment."; } function render() { @@ -721,14 +722,6 @@ function initCubePage() { refs.spineText.textContent = "Retournez a la configuration pour relancer une rencontre."; refs.primaryButton.textContent = "Retour a l'accueil"; refs.helpStatus.textContent = "Le match est termine."; - } else if (match.cube.running) { - refs.centerLabel.textContent = "Etat"; - refs.centerValue.textContent = "Cube en cours"; - refs.spineLabel.textContent = "Arrets"; - refs.spineHeadline.textContent = "Attendre les deux fins"; - refs.spineText.textContent = "Chaque joueur tape sur sa grande zone des qu'il a resolu son cube."; - refs.primaryButton.textContent = "Chrono cube en cours"; - refs.helpStatus.textContent = "Phase cube en cours. Les arrets sont saisis directement par les joueurs."; } else if ( match.cube.times.white !== null && match.cube.times.black !== null && @@ -750,19 +743,40 @@ function initCubePage() { refs.spineText.textContent = "Appliquer le resultat du cube pour preparer le block suivant."; refs.primaryButton.textContent = "Appliquer et ouvrir la page chrono"; refs.helpStatus.textContent = refs.spineText.textContent; + } else if (match.cube.running) { + refs.centerLabel.textContent = "Etat"; + refs.centerValue.textContent = "Chronos lances"; + refs.spineLabel.textContent = "Arrets"; + refs.spineHeadline.textContent = "Chaque joueur se chronometre"; + refs.spineText.textContent = "Chaque joueur demarre quand il veut, puis retape sa zone une fois le cube termine."; + refs.primaryButton.textContent = "Attendre les deux temps"; + refs.helpStatus.textContent = refs.spineText.textContent; + } else if (match.cube.times.white !== null || match.cube.times.black !== null) { + refs.centerLabel.textContent = "Etat"; + refs.centerValue.textContent = "Un temps saisi"; + refs.spineLabel.textContent = "Suite"; + refs.spineHeadline.textContent = "Attendre l'autre joueur"; + refs.spineText.textContent = "Le deuxieme joueur peut encore demarrer puis arreter son propre chrono quand il le souhaite."; + refs.primaryButton.textContent = "Attendre le deuxieme temps"; + refs.helpStatus.textContent = refs.spineText.textContent; } else { refs.centerLabel.textContent = "Etat"; refs.centerValue.textContent = "Pret"; - refs.spineLabel.textContent = "Depart"; - refs.spineHeadline.textContent = "Lancer la phase cube"; - refs.spineText.textContent = "Demarrez le chrono commun, puis laissez chaque joueur utiliser uniquement sa grande zone."; - refs.primaryButton.textContent = "Demarrer la phase cube"; - refs.helpStatus.textContent = "La phase cube n'a pas encore commence."; + refs.spineLabel.textContent = "Depart libre"; + refs.spineHeadline.textContent = "Chaque joueur lance son chrono"; + refs.spineText.textContent = "Au debut de sa resolution, chaque joueur tape sur sa grande zone pour demarrer son propre chrono."; + refs.primaryButton.textContent = "En attente des joueurs"; + refs.helpStatus.textContent = refs.spineText.textContent; } renderCubeZone("black"); renderCubeZone("white"); - refs.primaryButton.disabled = match.result ? false : match.cube.running; + refs.primaryButton.disabled = + !match.result && + !( + match.cube.times.white !== null && + match.cube.times.black !== null + ); } render(); @@ -819,6 +833,10 @@ function createMatch(config) { white: null, black: null, }, + playerState: { + white: createCubePlayerState(), + black: createCubePlayerState(), + }, round: 1, history: [], }, @@ -867,11 +885,26 @@ function normalizeRecoveredMatch(storedMatch) { startedAt: null, elapsedMs: 0, times: { white: null, black: null }, + playerState: { + white: createCubePlayerState(), + black: createCubePlayerState(), + }, round: 1, history: [], }; } + if (!storedMatch.cube.playerState) { + storedMatch.cube.playerState = { + white: createCubePlayerState(), + black: createCubePlayerState(), + }; + } + + storedMatch.cube.playerState.white = normalizeCubePlayerState(storedMatch.cube.playerState.white); + storedMatch.cube.playerState.black = normalizeCubePlayerState(storedMatch.cube.playerState.black); + storedMatch.cube.running = isAnyCubeTimerRunning(storedMatch); + if (!storedMatch.doubleCoup) { storedMatch.doubleCoup = { eligible: false, @@ -968,11 +1001,15 @@ function openCubePhase(storedMatch) { storedMatch.cube.startedAt = null; storedMatch.cube.elapsedMs = 0; storedMatch.cube.times = { white: null, black: null }; + storedMatch.cube.playerState = { + white: createCubePlayerState(), + black: createCubePlayerState(), + }; storedMatch.cube.round = 1; logEvent(storedMatch, `Page cube ouverte. Cube n${storedMatch.cube.number} designe.`); } -function startCubePhase(storedMatch) { +function startCubeTimer(storedMatch, color) { if (!storedMatch || storedMatch.phase !== "cube" || storedMatch.result) { return; } @@ -981,25 +1018,41 @@ function startCubePhase(storedMatch) { storedMatch.cube.number = pickCubeNumber(); } + if (storedMatch.cube.times[color] !== null || storedMatch.cube.playerState[color].running) { + return; + } + + const playerState = storedMatch.cube.playerState[color]; + playerState.running = true; + playerState.startedAt = Date.now(); storedMatch.cube.running = true; - storedMatch.cube.startedAt = Date.now(); - storedMatch.cube.elapsedMs = 0; - logEvent(storedMatch, `Phase cube demarree sur le cube n${storedMatch.cube.number}.`); + logEvent( + storedMatch, + `${playerName(storedMatch, color)} demarre son chrono cube sur le cube n${storedMatch.cube.number}.`, + ); } function captureCubeTime(storedMatch, color) { if ( !storedMatch || storedMatch.phase !== "cube" || - !storedMatch.cube.running || + !storedMatch.cube.playerState[color].running || storedMatch.cube.times[color] !== null ) { return; } - const elapsedMs = Date.now() - storedMatch.cube.startedAt; + const playerState = storedMatch.cube.playerState[color]; + const elapsedMs = playerState.elapsedMs + (Date.now() - playerState.startedAt); storedMatch.cube.times[color] = elapsedMs; - storedMatch.cube.elapsedMs = Math.max(storedMatch.cube.elapsedMs, elapsedMs); + playerState.elapsedMs = elapsedMs; + playerState.startedAt = null; + playerState.running = false; + storedMatch.cube.elapsedMs = Math.max( + getCubePlayerElapsed(storedMatch, "white"), + getCubePlayerElapsed(storedMatch, "black"), + ); + storedMatch.cube.running = isAnyCubeTimerRunning(storedMatch); logEvent(storedMatch, `${playerName(storedMatch, color)} arrete le cube en ${formatStopwatch(elapsedMs)}.`); if ( @@ -1059,6 +1112,10 @@ function prepareNextTwiceBlock(storedMatch, winner) { storedMatch.cube.startedAt = null; storedMatch.cube.elapsedMs = 0; storedMatch.cube.times = { white: null, black: null }; + storedMatch.cube.playerState = { + white: createCubePlayerState(), + black: createCubePlayerState(), + }; storedMatch.cube.number = null; if (hadDouble) { @@ -1085,6 +1142,10 @@ function prepareNextTimeBlock(storedMatch) { storedMatch.cube.startedAt = null; storedMatch.cube.elapsedMs = 0; storedMatch.cube.times = { white: null, black: null }; + storedMatch.cube.playerState = { + white: createCubePlayerState(), + black: createCubePlayerState(), + }; storedMatch.cube.number = null; logEvent( storedMatch, @@ -1134,6 +1195,10 @@ function replayCubePhase(storedMatch) { storedMatch.cube.startedAt = null; storedMatch.cube.elapsedMs = 0; storedMatch.cube.times = { white: null, black: null }; + storedMatch.cube.playerState = { + white: createCubePlayerState(), + black: createCubePlayerState(), + }; storedMatch.cube.round += 1; logEvent(storedMatch, `Phase cube relancee (tentative ${storedMatch.cube.round}).`); } @@ -1288,15 +1353,9 @@ function renderCubeElapsed(storedMatch) { return "00:00.0"; } - if (storedMatch.cube.running) { - return formatStopwatch(Date.now() - storedMatch.cube.startedAt); - } - - if (storedMatch.cube.elapsedMs > 0) { - return formatStopwatch(storedMatch.cube.elapsedMs); - } - - return "00:00.0"; + return formatStopwatch( + Math.max(getCubePlayerElapsed(storedMatch, "white"), getCubePlayerElapsed(storedMatch, "black")), + ); } function renderCubeCap(storedMatch, time) { @@ -1502,3 +1561,52 @@ function readWindowNameState() { return null; } } + +function createCubePlayerState() { + return { + running: false, + startedAt: null, + elapsedMs: 0, + }; +} + +function normalizeCubePlayerState(playerState) { + return { + running: Boolean(playerState?.running), + startedAt: typeof playerState?.startedAt === "number" ? playerState.startedAt : null, + elapsedMs: typeof playerState?.elapsedMs === "number" ? playerState.elapsedMs : 0, + }; +} + +function isAnyCubeTimerRunning(storedMatch) { + return storedMatch.cube.playerState.white.running || storedMatch.cube.playerState.black.running; +} + +function getCubePlayerElapsed(storedMatch, color) { + const playerState = storedMatch.cube.playerState[color]; + + if (storedMatch.cube.times[color] !== null) { + return storedMatch.cube.times[color]; + } + + if (playerState.running && playerState.startedAt) { + return playerState.elapsedMs + (Date.now() - playerState.startedAt); + } + + return playerState.elapsedMs; +} + +function formatCubePlayerTime(storedMatch, color) { + const elapsed = getCubePlayerElapsed(storedMatch, color); + const playerState = storedMatch.cube.playerState[color]; + + if (playerState.running) { + return formatStopwatch(elapsed); + } + + if (elapsed <= 0 && storedMatch.cube.times[color] === null) { + return "--"; + } + + return formatStopwatch(elapsed); +} diff --git a/cube.html b/cube.html index c58b424..929461c 100644 --- a/cube.html +++ b/cube.html @@ -30,7 +30,7 @@ 1
- Chrono cube + Temps max 00:00.0
@@ -54,7 +54,7 @@

@@ -69,7 +69,7 @@
@@ -87,7 +87,7 @@

diff --git a/styles.css b/styles.css index 82e32d8..a4618ea 100644 --- a/styles.css +++ b/styles.css @@ -697,8 +697,7 @@ textarea:focus { .setup-grid, .mode-grid, .preset-grid, - .faceoff-board, - .status-strip { + .faceoff-board { grid-template-columns: 1fr; } @@ -714,6 +713,10 @@ textarea:focus { } @media (max-width: 900px) { + .phase-body { + overflow-y: auto; + } + .setup-shell, .phase-shell { width: min(100% - 1rem, 100%); @@ -734,18 +737,102 @@ textarea:focus { display: grid; } + .phase-shell { + gap: 0.55rem; + padding: 0.55rem 0 0.55rem; + } + + .phase-header { + grid-template-columns: auto 1fr auto; + gap: 0.45rem; + text-align: center; + } + + .phase-title h1 { + font-size: clamp(1.55rem, 5.8vw, 2.1rem); + } + + .phase-subtitle { + font-size: 0.92rem; + } + + .status-strip { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.45rem; + } + + .status-card { + min-height: 4.5rem; + padding: 0.65rem 0.45rem; + } + + .status-card strong { + font-size: clamp(1.25rem, 4vw, 1.7rem); + } + .faceoff-board { grid-template-rows: minmax(0, 1fr) auto minmax(0, 1fr); - gap: 0.7rem; + gap: 0.45rem; } .opponent-zone .mirrored-mobile { transform: rotate(180deg); } + .zone-inner { + padding: 0.7rem; + gap: 0.55rem; + } + + .zone-head { + align-items: center; + } + + .zone-head h2 { + font-size: 1.25rem; + } + + .zone-stats strong { + font-size: 1.05rem; + } + .zone-button { - min-height: clamp(180px, 28vh, 340px); - font-size: clamp(1.9rem, 7vw, 3.2rem); + min-height: clamp(118px, 18vh, 180px); + font-size: clamp(1.7rem, 6.6vw, 2.7rem); + } + + .zone-foot { + min-height: 1.3rem; + font-size: 0.88rem; + } + + .phase-spine { + gap: 0.45rem; + } + + .spine-card { + padding: 0.75rem; + } + + .spine-card strong { + font-size: 1.15rem; + } + + .spine-card p { + font-size: 0.92rem; + } + + .spine-button { + min-height: 3.4rem; + } + + .brand-link { + justify-self: start; + } + + .utility-button { + justify-self: end; + width: auto; } } @@ -755,25 +842,34 @@ textarea:focus { } .status-card { - min-height: 5.6rem; + min-height: 4rem; + padding: 0.55rem 0.35rem; + } + + .status-card span { + font-size: 0.76rem; } .status-card strong { - font-size: 1.9rem; + font-size: 1.18rem; } - .zone-head, .modal-head, .section-heading { flex-direction: column; } - .zone-stats { - text-align: left; + .zone-head { + flex-direction: row; + align-items: center; } .button, .spine-button { width: 100%; } + + .utility-button { + width: auto; + } }