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"
/>
+