diff --git a/app.js b/app.js index b15cbd4..c2fc12c 100644 --- a/app.js +++ b/app.js @@ -115,7 +115,9 @@ function initSetupPage() { const loadDemoButton = document.querySelector("#loadDemoButton"); const resumeCard = document.querySelector("#resumeCard"); const moveSecondsField = document.querySelector("#moveSecondsField"); + const timeInitialField = document.querySelector("#timeInitialField"); const moveSecondsInput = form?.querySelector('[name="moveSeconds"]'); + const timeInitialInput = form?.querySelector('[name="timeInitialMinutes"]'); if (!form || !summary || !loadDemoButton || !resumeCard) { return; @@ -127,14 +129,15 @@ function initSetupPage() { const quota = PRESETS[preset].quota; const blockDurationMs = getDurationInputMs(form, "blockSeconds", DEFAULT_BLOCK_DURATION_MS); const moveLimitMs = getDurationInputMs(form, "moveSeconds", DEFAULT_MOVE_LIMIT_MS); + const timeInitialMs = getMinuteInputMs(form, "timeInitialMinutes", TIME_MODE_INITIAL_CLOCK_MS); const moveLimitActive = usesMoveLimit(mode); const timeImpact = mode === "time" - ? "Chronos cumules de 10 minutes par joueur, ajustes apres chaque phase cube avec plafond de 120 s pris en compte. Aucun temps par coup en mode Time." + ? `Chronos cumules de ${formatClock(timeInitialMs)} par joueur, ajustes apres chaque phase cube avec plafond de 120 s pris en compte. Aucun temps par coup en mode Time.` : "Le gagnant du cube commence la partie suivante, avec double coup V2 possible."; const timingText = moveLimitActive ? `Temps configures : partie ${formatClock(blockDurationMs)}, coup ${formatClock(moveLimitMs)}.` - : `Temps configure : Block ${formatClock(blockDurationMs)}.`; + : `Temps configures : Block ${formatClock(blockDurationMs)}, chrono initial ${formatClock(timeInitialMs)} par joueur.`; const quotaText = moveLimitActive ? `Quota actif : ${quota} coups par joueur.` : `Quota actif : ${quota} coups par joueur et par Block.`; @@ -143,11 +146,17 @@ function initSetupPage() { moveSecondsField.hidden = !moveLimitActive; } + if (timeInitialField instanceof HTMLElement) { + timeInitialField.hidden = moveLimitActive; + } + if (moveSecondsInput instanceof HTMLInputElement) { moveSecondsInput.disabled = !moveLimitActive; } - document.body.classList.toggle("time-setup-mode", !moveLimitActive); + if (timeInitialInput instanceof HTMLInputElement) { + timeInitialInput.disabled = moveLimitActive; + } summary.innerHTML = ` ${MODES[mode].label} @@ -209,6 +218,7 @@ function initSetupPage() { preset: getRadioValue(form, "preset") || "fast", blockDurationMs: getDurationInputMs(form, "blockSeconds", DEFAULT_BLOCK_DURATION_MS), moveLimitMs: getDurationInputMs(form, "moveSeconds", DEFAULT_MOVE_LIMIT_MS), + timeInitialMs: getMinuteInputMs(form, "timeInitialMinutes", TIME_MODE_INITIAL_CLOCK_MS), whiteName: sanitizeText(data.get("whiteName")) || "Blanc", blackName: sanitizeText(data.get("blackName")) || "Noir", arbiterName: sanitizeText(data.get("arbiterName")), @@ -876,7 +886,7 @@ function initCubePage() { name.textContent = playerName(match, color); result.textContent = formatCubePlayerTime(match, color); - cap.textContent = renderCubeCap(match, time); + cap.textContent = renderCubeMeta(match, color); button.classList.toggle("cube-hold-arming", holdArmed && !holdReady); button.classList.toggle("cube-hold-ready", holdReady); button.style.setProperty("--cube-hold-progress", `${holdProgress}`); @@ -930,6 +940,12 @@ function initCubePage() { function render() { const blockHeading = formatBlockHeading(match, match.blockNumber); + const timePreview = + isTimeMode(match) && + match.cube.times.white !== null && + match.cube.times.black !== null + ? getTimeAdjustmentPreview(match, match.cube.times.white, match.cube.times.black) + : null; refs.title.textContent = match.cube.number ? `Cube n${match.cube.number}` : "Phase cube"; refs.subtitle.textContent = `${blockHeading} - ${MODES[match.config.mode].label} - ${renderModeContext(match)}`; @@ -959,13 +975,24 @@ function initCubePage() { refs.primaryButton.textContent = "Rejouer la phase cube"; refs.helpStatus.textContent = refs.spineText.textContent; } else if (match.cube.times.white !== null && match.cube.times.black !== null) { - refs.centerLabel.textContent = "Decision"; - refs.centerValue.textContent = "Phase cube complete"; - refs.spineLabel.textContent = "Suite"; - refs.spineHeadline.textContent = "Ouvrir la page chrono"; - refs.spineText.textContent = isTimeMode(match) - ? "Appliquer le resultat du cube pour preparer le Block suivant." - : "Appliquer le resultat du cube pour preparer la partie suivante."; + if (timePreview) { + refs.centerLabel.textContent = "Vainqueur cube"; + refs.centerValue.textContent = timePreview.winner + ? playerName(match, timePreview.winner) + : "Egalite"; + refs.spineLabel.textContent = "Impact chrono"; + refs.spineHeadline.textContent = + timePreview.blockType === "minus" ? "Bloc - a appliquer" : "Bloc + a appliquer"; + refs.spineText.textContent = + `Blanc ${formatSignedStopwatch(timePreview.whiteDelta)} -> ${formatSignedClock(timePreview.whiteAfter)}. ` + + `Noir ${formatSignedStopwatch(timePreview.blackDelta)} -> ${formatSignedClock(timePreview.blackAfter)}.`; + } else { + refs.centerLabel.textContent = "Decision"; + refs.centerValue.textContent = "Phase cube complete"; + refs.spineLabel.textContent = "Suite"; + refs.spineHeadline.textContent = "Ouvrir la page chrono"; + refs.spineText.textContent = "Appliquer le resultat du cube pour preparer la partie suivante."; + } refs.primaryButton.textContent = "Appliquer et ouvrir la page chrono"; refs.helpStatus.textContent = refs.spineText.textContent; } else if (match.cube.running) { @@ -1041,8 +1068,8 @@ function createMatch(config) { clocks: config.mode === "time" ? { - white: TIME_MODE_INITIAL_CLOCK_MS, - black: TIME_MODE_INITIAL_CLOCK_MS, + white: getTimeInitialMs(config), + black: getTimeInitialMs(config), } : null, lastMover: null, @@ -1077,7 +1104,7 @@ function createMatch(config) { newMatch, usesMoveLimit(config.mode) ? `Match cree en mode ${MODES[config.mode].label}, cadence ${PRESETS[config.preset].label}, partie ${formatClock(config.blockDurationMs)} et coup ${formatClock(config.moveLimitMs)}.` - : `Match cree en mode ${MODES[config.mode].label}, cadence ${PRESETS[config.preset].label}, Block ${formatClock(config.blockDurationMs)} sans temps par coup.`, + : `Match cree en mode ${MODES[config.mode].label}, cadence ${PRESETS[config.preset].label}, Block ${formatClock(config.blockDurationMs)} et chrono initial ${formatClock(getTimeInitialMs(config))} par joueur, sans temps par coup.`, ); logEvent(newMatch, `Les Blancs commencent ${formatBlockHeading(config, 1)}.`); return newMatch; @@ -1113,6 +1140,7 @@ function normalizeRecoveredMatch(storedMatch) { const blockDurationMs = getBlockDurationMs(storedMatch); const moveLimitMs = getMoveLimitMs(storedMatch); + const timeInitialMs = getTimeInitialMs(storedMatch); if (storedMatch.schemaVersion !== 3) { storedMatch.schemaVersion = 3; @@ -1129,6 +1157,11 @@ function normalizeRecoveredMatch(storedMatch) { changed = true; } + if (storedMatch.config.timeInitialMs !== timeInitialMs) { + storedMatch.config.timeInitialMs = timeInitialMs; + changed = true; + } + if (storedMatch.phase === "block" && typeof storedMatch.lastTickAt !== "number") { storedMatch.lastTickAt = null; changed = true; @@ -1442,30 +1475,26 @@ function prepareNextTimeBlock(storedMatch) { } function applyTimeAdjustments(storedMatch, whiteTime, blackTime) { - if (!storedMatch.clocks) { + const preview = getTimeAdjustmentPreview(storedMatch, whiteTime, blackTime); + if (!preview) { return; } - const cappedWhite = Math.min(whiteTime, CUBE_TIME_CAP_MS); - const cappedBlack = Math.min(blackTime, CUBE_TIME_CAP_MS); - const blockType = getTimeBlockType(storedMatch.blockNumber); + storedMatch.clocks.white = preview.whiteAfter; + storedMatch.clocks.black = preview.blackAfter; - if (blockType === "minus") { - storedMatch.clocks.white -= cappedWhite; - storedMatch.clocks.black -= cappedBlack; + if (preview.blockType === "minus") { logEvent( storedMatch, - `Bloc - : ${formatStopwatch(cappedWhite)} retire au chrono Blanc, ${formatStopwatch( - cappedBlack, + `Bloc - : ${formatStopwatch(preview.cappedWhite)} retire au chrono Blanc, ${formatStopwatch( + preview.cappedBlack, )} retire au chrono Noir.`, ); } else { - storedMatch.clocks.white += cappedBlack; - storedMatch.clocks.black += cappedWhite; logEvent( storedMatch, - `Bloc + : ${formatStopwatch(cappedBlack)} ajoute au chrono Blanc, ${formatStopwatch( - cappedWhite, + `Bloc + : ${formatStopwatch(preview.cappedBlack)} ajoute au chrono Blanc, ${formatStopwatch( + preview.cappedWhite, )} ajoute au chrono Noir.`, ); } @@ -1628,7 +1657,8 @@ function renderCubeElapsed(storedMatch) { ); } -function renderCubeCap(storedMatch, time) { +function renderCubeMeta(storedMatch, color) { + const time = storedMatch.cube.times[color]; if (time === null) { return ""; } @@ -1637,11 +1667,32 @@ function renderCubeCap(storedMatch, time) { return "temps capture"; } - if (time <= CUBE_TIME_CAP_MS) { - return "plafond non atteint"; + if ( + storedMatch.cube.times.white !== null && + storedMatch.cube.times.black !== null + ) { + const preview = getTimeAdjustmentPreview( + storedMatch, + storedMatch.cube.times.white, + storedMatch.cube.times.black, + ); + if (!preview) { + return ""; + } + + const delta = color === "white" ? preview.whiteDelta : preview.blackDelta; + const cappedTime = color === "white" ? preview.cappedWhite : preview.cappedBlack; + const wasCapped = time > cappedTime; + return wasCapped + ? `Impact chrono ${formatSignedStopwatch(delta)} (cap ${formatStopwatch(cappedTime)})` + : `Impact chrono ${formatSignedStopwatch(delta)}`; } - return `pris en compte ${formatStopwatch(CUBE_TIME_CAP_MS)}`; + if (time <= CUBE_TIME_CAP_MS) { + return "impact chrono en attente"; + } + + return `impact en attente, cap ${formatStopwatch(CUBE_TIME_CAP_MS)}`; } function resultText(storedMatch) { @@ -1727,6 +1778,7 @@ function loadDemo(form, onRender) { setRadioValue(form, "preset", "freeze"); setInputValue(form, "blockSeconds", "180"); setInputValue(form, "moveSeconds", "20"); + setInputValue(form, "timeInitialMinutes", "10"); setInputValue(form, "whiteName", "Nora"); setInputValue(form, "blackName", "Leo"); setInputValue(form, "arbiterName", "Arbitre demo"); @@ -1764,6 +1816,16 @@ function getDurationInputMs(form, name, fallbackMs) { return seconds * 1000; } +function getMinuteInputMs(form, name, fallbackMs) { + const input = form.querySelector(`[name="${name}"]`); + const minutes = Number.parseInt(String(input?.value || ""), 10); + if (!Number.isFinite(minutes) || minutes <= 0) { + return fallbackMs; + } + + return minutes * 60000; +} + function isTimeMode(matchOrConfig) { const mode = typeof matchOrConfig === "string" @@ -1776,6 +1838,13 @@ function usesMoveLimit(matchOrConfig) { return !isTimeMode(matchOrConfig); } +function getTimeInitialMs(matchOrConfig) { + return normalizeDurationMs( + matchOrConfig?.config?.timeInitialMs ?? matchOrConfig?.timeInitialMs, + TIME_MODE_INITIAL_CLOCK_MS, + ); +} + function getBlockLabel(matchOrConfig) { return isTimeMode(matchOrConfig) ? "Block" : "Partie"; } @@ -1834,6 +1903,11 @@ function formatSignedClock(ms) { return `${negative}${minutes}:${seconds}`; } +function formatSignedStopwatch(ms) { + const sign = ms < 0 ? "-" : "+"; + return `${sign}${formatStopwatch(ms)}`; +} + function formatStopwatch(ms) { const totalSeconds = Math.floor(Math.abs(ms) / 1000); const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0"); @@ -1893,6 +1967,29 @@ function normalizeDurationMs(value, fallbackMs) { return Math.round(duration); } +function getTimeAdjustmentPreview(storedMatch, whiteTime, blackTime) { + if (!storedMatch?.clocks) { + return null; + } + + const cappedWhite = Math.min(whiteTime, CUBE_TIME_CAP_MS); + const cappedBlack = Math.min(blackTime, CUBE_TIME_CAP_MS); + const blockType = getTimeBlockType(storedMatch.blockNumber); + const whiteDelta = blockType === "minus" ? -cappedWhite : cappedBlack; + const blackDelta = blockType === "minus" ? -cappedBlack : cappedWhite; + + return { + blockType, + winner: whiteTime < blackTime ? "white" : blackTime < whiteTime ? "black" : null, + cappedWhite, + cappedBlack, + whiteDelta, + blackDelta, + whiteAfter: storedMatch.clocks.white + whiteDelta, + blackAfter: storedMatch.clocks.black + blackDelta, + }; +} + function createCubePlayerState() { return { running: false, diff --git a/application.html b/application.html index a44c38f..1ada991 100644 --- a/application.html +++ b/application.html @@ -132,6 +132,17 @@ value="180" /> +