Track a match: length, score sheet, and a clock that survives a quit
Match length is a new first row in the settings, 1 to 29 points, stepping by
2 so it can only ever be odd. The Time setting now means time *per point* and
each clock starts at time × points, which the settings sheet spells out.
The middle bar icon opens a score sheet. Edits are staged there and only
reach the state on Done, which then does one of three things:
nothing changed turn, delay position and moves all untouched
score changed displayed times become the reserves, then back to the
first tap with moves zeroed — the clocks keep their value
clock changed the spent delay is added back on so the panel shows
exactly what was typed, and the turn carries on
Tapping outside the sheet discards the staged edits, so a stray tap can't
restart a game.
reset() was called both by the Reset button and by every settings change, so
clearing the score in it would have wiped a live match whenever the delay was
nudged. It splits into resetClocks() and resetAll(); only the button clears
the score. Shortening a match clamps a score that no longer fits.
The whole game now persists, not just the settings: score, both reserves,
moves, whose turn and how much delay was left, saved on the discrete
transitions and on pagehide/visibilitychange. Time passing while the app is
dead can't be charged to anyone, so a running clock is stored as paused and
comes back on the play button. A malformed saved game is ignored rather than
half-applied.
Each panel shows its own score first, stacked above its move count at the
top left.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+317
-29
@@ -75,16 +75,23 @@
|
||||
.panel--top .inner{padding:14px 20px max(14px,env(safe-area-inset-top))}
|
||||
.panel--bottom .inner{padding:14px 20px max(14px,env(safe-area-inset-bottom))}
|
||||
|
||||
.moves{
|
||||
align-self:flex-end;
|
||||
.head{
|
||||
display:flex;flex-direction:column;align-items:flex-start;
|
||||
gap:3px;
|
||||
order:-1;
|
||||
}
|
||||
.score,.moves{
|
||||
font-size:clamp(13px,3.7vw,20px);
|
||||
font-weight:700;
|
||||
letter-spacing:.01em;
|
||||
color:var(--idle-mute);
|
||||
order:-1;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.panel[data-state="active"] .score,
|
||||
.panel[data-state="active"] .moves,
|
||||
.panel[data-state="flagged"] .score,
|
||||
.panel[data-state="flagged"] .moves{color:var(--accent-mute)}
|
||||
.panel[data-state="flagged"] .score,
|
||||
.panel[data-state="flagged"] .moves{color:rgba(255,255,255,.55)}
|
||||
|
||||
.stack{
|
||||
@@ -138,7 +145,7 @@
|
||||
display:flex;align-items:center;
|
||||
padding:0 clamp(12px,4vw,28px);
|
||||
}
|
||||
.bar .gap{flex:1.5}
|
||||
.bar .gap{flex:.75}
|
||||
.btn{
|
||||
flex:1;
|
||||
height:100%;
|
||||
@@ -303,7 +310,10 @@
|
||||
<div class="app">
|
||||
<section class="panel panel--top" data-side="0" data-state="idle">
|
||||
<div class="inner">
|
||||
<div class="moves">Moves: <span data-moves>0</span></div>
|
||||
<div class="head">
|
||||
<div class="score">Score: <span data-score>0 - 0</span>, out of <span data-target>1</span></div>
|
||||
<div class="moves">Moves: <span data-moves>0</span></div>
|
||||
</div>
|
||||
<div class="stack">
|
||||
<div class="time" data-time>3:00</div>
|
||||
<div class="delay" data-delay>
|
||||
@@ -330,6 +340,15 @@
|
||||
</svg>
|
||||
</button>
|
||||
<span class="gap"></span>
|
||||
<button class="btn" id="score" aria-label="Score">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="4" y="3" width="16" height="18" rx="2.6"/>
|
||||
<path d="M12 7.5v10"/>
|
||||
<path d="M7.6 10.6h2.6"/><path d="M13.8 10.6h2.6"/>
|
||||
<path d="M7.6 14.6h2.6"/><path d="M13.8 14.6h2.6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="gap"></span>
|
||||
<button class="btn" id="settings" aria-label="Settings">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3.3"/>
|
||||
@@ -351,7 +370,10 @@
|
||||
|
||||
<section class="panel panel--bottom" data-side="1" data-state="idle">
|
||||
<div class="inner">
|
||||
<div class="moves">Moves: <span data-moves>0</span></div>
|
||||
<div class="head">
|
||||
<div class="score">Score: <span data-score>0 - 0</span>, out of <span data-target>1</span></div>
|
||||
<div class="moves">Moves: <span data-moves>0</span></div>
|
||||
</div>
|
||||
<div class="stack">
|
||||
<div class="time" data-time>3:00</div>
|
||||
<div class="delay" data-delay>
|
||||
@@ -368,6 +390,18 @@
|
||||
<div class="sheet" id="sheet" role="dialog" aria-modal="true" aria-label="Settings">
|
||||
<div class="grab"></div>
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<h2>Match</h2>
|
||||
<p>Points to play to.</p>
|
||||
</div>
|
||||
<div class="stepper">
|
||||
<button class="step" data-points-adj="-2">–</button>
|
||||
<span class="val"><span id="v-points">1</span><span class="unit">pts</span></span>
|
||||
<button class="step" data-points-adj="2">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row--presets">
|
||||
<button class="preset" data-base="120000" data-preset-delay="12000" aria-pressed="false">2m / 12s</button>
|
||||
<button class="preset" data-base="180000" data-preset-delay="12000" aria-pressed="false">3m / 12s</button>
|
||||
@@ -377,7 +411,7 @@
|
||||
<div class="row row--wide">
|
||||
<div>
|
||||
<h2>Time</h2>
|
||||
<p>Reserve time per player.</p>
|
||||
<p id="p-time">Reserve time per point.</p>
|
||||
</div>
|
||||
<div class="stepper">
|
||||
<button class="step" data-time-adj="-60">–</button>
|
||||
@@ -418,7 +452,67 @@
|
||||
</div>
|
||||
|
||||
<button class="done" id="done">Done</button>
|
||||
<p class="hint">Changing time or delay resets the clock.</p>
|
||||
<p class="hint">Changing match, time or delay resets the clocks. The score is left alone.</p>
|
||||
</div>
|
||||
|
||||
<div class="sheet" id="scoresheet" role="dialog" aria-modal="true" aria-label="Score">
|
||||
<div class="grab"></div>
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<h2>You</h2>
|
||||
<p>Points won, this end of the phone.</p>
|
||||
</div>
|
||||
<div class="stepper">
|
||||
<button class="step" data-score-adj="-1" data-who="1">–</button>
|
||||
<span class="val"><span id="v-you">0</span><span class="unit">pts</span></span>
|
||||
<button class="step" data-score-adj="1" data-who="1">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<h2>Them</h2>
|
||||
<p>Points won, far end.</p>
|
||||
</div>
|
||||
<div class="stepper">
|
||||
<button class="step" data-score-adj="-1" data-who="0">–</button>
|
||||
<span class="val"><span id="v-them">0</span><span class="unit">pts</span></span>
|
||||
<button class="step" data-score-adj="1" data-who="0">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row--wide">
|
||||
<div>
|
||||
<h2>Your clock</h2>
|
||||
</div>
|
||||
<div class="stepper">
|
||||
<button class="step" data-clock-adj="-60" data-who="1">–</button>
|
||||
<span class="val"><span id="v-you-min">3</span><span class="unit">min</span></span>
|
||||
<button class="step" data-clock-adj="60" data-who="1">+</button>
|
||||
<button class="step" data-clock-adj="-1" data-who="1" style="margin-left:8px">–</button>
|
||||
<span class="val"><span id="v-you-sec">00</span><span class="unit">sec</span></span>
|
||||
<button class="step" data-clock-adj="1" data-who="1">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row--wide">
|
||||
<div>
|
||||
<h2>Their clock</h2>
|
||||
</div>
|
||||
<div class="stepper">
|
||||
<button class="step" data-clock-adj="-60" data-who="0">–</button>
|
||||
<span class="val"><span id="v-them-min">3</span><span class="unit">min</span></span>
|
||||
<button class="step" data-clock-adj="60" data-who="0">+</button>
|
||||
<button class="step" data-clock-adj="-1" data-who="0" style="margin-left:8px">–</button>
|
||||
<span class="val"><span id="v-them-sec">00</span><span class="unit">sec</span></span>
|
||||
<button class="step" data-clock-adj="1" data-who="0">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="done" id="score-done">Done</button>
|
||||
<p class="hint">Changing the score starts a new game: moves back to zero, turn back to
|
||||
the first tap. The clocks keep their times.</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -444,18 +538,24 @@
|
||||
];
|
||||
|
||||
var cfg = {
|
||||
base: store.num("bg.base", 180000),
|
||||
delay: store.num("bg.delay", 15000),
|
||||
theme: store.get("bg.theme", "sage"),
|
||||
sound: store.get("bg.sound", "1") === "1"
|
||||
base: store.num("bg.base", 180000), // reserve per point
|
||||
delay: store.num("bg.delay", 15000),
|
||||
points: store.num("bg.points", 1), // match length, odd, 1..29
|
||||
theme: store.get("bg.theme", "sage"),
|
||||
sound: store.get("bg.sound", "1") === "1"
|
||||
};
|
||||
if(!(cfg.points >= 1 && cfg.points <= 29 && cfg.points % 2 === 1)) cfg.points = 1;
|
||||
|
||||
// a match to N points is worth N times the per-point reserve
|
||||
function matchTime(){ return cfg.base * cfg.points; }
|
||||
|
||||
/* ============ state ============ */
|
||||
var IDLE = 0, RUN = 1, PAUSE = 2, FLAG = 3;
|
||||
var st = {
|
||||
phase: IDLE,
|
||||
active: -1,
|
||||
reserve: [cfg.base, cfg.base], // reserve banked at the start of the current turn
|
||||
reserve: [matchTime(), matchTime()], // reserve banked at the start of the current turn
|
||||
score: [0, 0], // match points, by side
|
||||
moves: [0, 0],
|
||||
turnAt: 0, // performance.now() when the turn began
|
||||
held: 0, // frozen elapsed while paused
|
||||
@@ -469,6 +569,8 @@
|
||||
root: p,
|
||||
time: p.querySelector("[data-time]"),
|
||||
moves: p.querySelector("[data-moves]"),
|
||||
score: p.querySelector("[data-score]"),
|
||||
target: p.querySelector("[data-target]"),
|
||||
delay: p.querySelector("[data-delay]"),
|
||||
fill: p.querySelector("[data-fill]"),
|
||||
dnum: p.querySelector("[data-dnum]")
|
||||
@@ -478,6 +580,7 @@
|
||||
var btnSound = document.getElementById("sound");
|
||||
var btnReset = document.getElementById("reset");
|
||||
var sheet = document.getElementById("sheet");
|
||||
var scoreSheet = document.getElementById("scoresheet");
|
||||
var scrim = document.getElementById("scrim");
|
||||
|
||||
/* ============ audio (synthesised — no files, works offline) ============ */
|
||||
@@ -551,11 +654,24 @@
|
||||
return m + ":" + (s < 10 ? "0" : "") + s;
|
||||
}
|
||||
|
||||
// is this side's clock the one the turn is on?
|
||||
function isLive(i){
|
||||
return i === st.active && (st.phase === RUN || st.phase === PAUSE);
|
||||
}
|
||||
// how much of this turn has already come off the reserve (0 while the delay runs)
|
||||
function consumed(i){
|
||||
return isLive(i) ? Math.max(0, elapsed() - cfg.delay) : 0;
|
||||
}
|
||||
// the number the panel is showing, which is what the score sheet edits
|
||||
function shownTime(i){
|
||||
return Math.max(0, st.reserve[i] - consumed(i));
|
||||
}
|
||||
|
||||
/* ============ render ============ */
|
||||
function render(){
|
||||
var e = elapsed();
|
||||
for(var i = 0; i < 2; i++){
|
||||
var live = (i === st.active && (st.phase === RUN || st.phase === PAUSE));
|
||||
var live = isLive(i);
|
||||
var main = st.reserve[i], dLeft = 0;
|
||||
|
||||
if(live){
|
||||
@@ -571,6 +687,8 @@
|
||||
|
||||
el[i].time.textContent = fmt(main);
|
||||
el[i].moves.textContent = st.moves[i];
|
||||
el[i].score.textContent = st.score[i] + " - " + st.score[1 - i];
|
||||
el[i].target.textContent = cfg.points;
|
||||
|
||||
var showDelay = live && cfg.delay > 0 && dLeft > 0;
|
||||
el[i].delay.classList.toggle("on", showDelay);
|
||||
@@ -594,6 +712,7 @@
|
||||
render();
|
||||
sndFlag();
|
||||
buzz([90, 70, 90]);
|
||||
saveGame();
|
||||
}
|
||||
|
||||
function tap(side){
|
||||
@@ -607,6 +726,7 @@
|
||||
st.phase = RUN;
|
||||
paintPlay(); // the bar has to show what the clock is doing
|
||||
sndSwap(); buzz(14);
|
||||
saveGame();
|
||||
return;
|
||||
}
|
||||
if(side !== st.active) return; // the waiting player can't stop the clock
|
||||
@@ -618,6 +738,7 @@
|
||||
st.turnAt = performance.now();
|
||||
st.ticked = false;
|
||||
sndSwap(); buzz(14);
|
||||
saveGame();
|
||||
}
|
||||
|
||||
function pauseNow(){
|
||||
@@ -625,6 +746,7 @@
|
||||
st.held = elapsed();
|
||||
st.phase = PAUSE;
|
||||
paintPlay();
|
||||
saveGame();
|
||||
}
|
||||
|
||||
function togglePause(){
|
||||
@@ -636,6 +758,7 @@
|
||||
st.phase = RUN;
|
||||
}else return;
|
||||
paintPlay();
|
||||
saveGame();
|
||||
}
|
||||
|
||||
/* SVG elements aren't HTMLElements, so they have no .hidden property —
|
||||
@@ -652,16 +775,79 @@
|
||||
btnPlay.setAttribute("aria-label", showPlay ? "Start clock" : "Pause clock");
|
||||
}
|
||||
|
||||
function reset(){
|
||||
disarm();
|
||||
// back to the first tap of a game, clocks untouched
|
||||
function newGame(){
|
||||
st.phase = IDLE; st.active = -1;
|
||||
st.reserve = [cfg.base, cfg.base];
|
||||
st.moves = [0, 0];
|
||||
st.held = 0; st.ticked = false;
|
||||
paintPlay();
|
||||
render();
|
||||
}
|
||||
|
||||
// clocks back to full for the match length; the score is not this function's business
|
||||
function resetClocks(){
|
||||
disarm();
|
||||
st.reserve = [matchTime(), matchTime()];
|
||||
newGame();
|
||||
saveGame();
|
||||
}
|
||||
|
||||
// the Reset button — a whole new match
|
||||
function resetAll(){
|
||||
st.score = [0, 0];
|
||||
resetClocks();
|
||||
}
|
||||
|
||||
function clampScore(){
|
||||
st.score = [Math.min(cfg.points, st.score[0]), Math.min(cfg.points, st.score[1])];
|
||||
}
|
||||
|
||||
/* ============ the game survives a force-quit ============ */
|
||||
// Time that passes while the app is dead can't be charged to anyone, so a
|
||||
// running clock is stored as paused and comes back on the play button.
|
||||
function saveGame(){
|
||||
var mid = (st.phase === RUN || st.phase === PAUSE);
|
||||
try{
|
||||
store.set("bg.game", JSON.stringify({
|
||||
v: 1,
|
||||
score: st.score, reserve: st.reserve, moves: st.moves,
|
||||
active: st.active,
|
||||
held: mid ? elapsed() : 0,
|
||||
phase: st.phase === RUN ? PAUSE : st.phase
|
||||
}));
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
function loadGame(){
|
||||
var raw = store.get("bg.game", null), g;
|
||||
if(!raw) return;
|
||||
try{ g = JSON.parse(raw); }catch(e){ return; }
|
||||
// anything malformed is ignored rather than half-applied
|
||||
function pair(a){
|
||||
return a && a.length === 2 && isFinite(a[0]) && isFinite(a[1]) && a[0] >= 0 && a[1] >= 0;
|
||||
}
|
||||
if(!g || g.v !== 1 || !pair(g.score) || !pair(g.reserve) || !pair(g.moves)) return;
|
||||
if(!isFinite(g.held) || g.held < 0) return;
|
||||
if([IDLE, RUN, PAUSE, FLAG].indexOf(g.phase) < 0) return;
|
||||
if([-1, 0, 1].indexOf(g.active) < 0) return;
|
||||
|
||||
st.score = [g.score[0], g.score[1]];
|
||||
clampScore(); // the match may have been shortened since
|
||||
st.reserve = [g.reserve[0], g.reserve[1]];
|
||||
st.moves = [g.moves[0], g.moves[1]];
|
||||
st.active = g.active;
|
||||
st.phase = g.phase === RUN ? PAUSE : g.phase;
|
||||
st.held = st.phase === PAUSE ? g.held : 0;
|
||||
st.ticked = st.held > cfg.delay; // don't re-tick a delay that already expired
|
||||
if(st.phase === PAUSE && st.active < 0) st.phase = IDLE; // can't be mid-turn with no turn
|
||||
}
|
||||
|
||||
// the only hooks iOS reliably fires before killing an app
|
||||
document.addEventListener("visibilitychange", function(){
|
||||
if(document.visibilityState === "hidden") saveGame();
|
||||
});
|
||||
window.addEventListener("pagehide", saveGame);
|
||||
|
||||
/* reset needs two taps within 2s — one stray tap can't wipe a live game */
|
||||
var armT = null;
|
||||
function disarm(){
|
||||
@@ -670,7 +856,7 @@
|
||||
btnReset.setAttribute("aria-label", "Reset clock");
|
||||
}
|
||||
function armReset(){
|
||||
if(armT){ disarm(); reset(); buzz(20); return; } // second tap inside the window
|
||||
if(armT){ disarm(); resetAll(); buzz(20); return; } // second tap inside the window
|
||||
btnReset.classList.remove("armed");
|
||||
void btnReset.offsetWidth; // restart the drain animation
|
||||
btnReset.classList.add("armed");
|
||||
@@ -704,11 +890,21 @@
|
||||
swWrap.appendChild(b);
|
||||
});
|
||||
|
||||
function clock(ms){ // m:ss, for copy rather than the big readout
|
||||
var s = Math.round(ms / 1000);
|
||||
return Math.floor(s / 60) + ":" + ("0" + (s % 60)).slice(-2);
|
||||
}
|
||||
|
||||
function paintSettings(){
|
||||
var s = Math.round(cfg.base / 1000);
|
||||
document.getElementById("v-min").textContent = Math.floor(s / 60);
|
||||
document.getElementById("v-sec").textContent = ("0" + (s % 60)).slice(-2);
|
||||
document.getElementById("v-delay").textContent = Math.round(cfg.delay / 1000);
|
||||
document.getElementById("v-points").textContent = cfg.points;
|
||||
document.getElementById("p-time").textContent = cfg.points === 1
|
||||
? "Reserve time per point."
|
||||
: "Reserve time per point. " + cfg.points + " × " + clock(cfg.base) +
|
||||
" = " + clock(matchTime()) + " each.";
|
||||
document.getElementById("t-sound").setAttribute("aria-checked", cfg.sound ? "true" : "false");
|
||||
btnSound.setAttribute("aria-pressed", cfg.sound ? "true" : "false");
|
||||
showIcon(btnSound, "on", cfg.sound);
|
||||
@@ -731,7 +927,18 @@
|
||||
cfg.delay = parseInt(b.dataset.presetDelay, 10);
|
||||
store.set("bg.base", String(cfg.base));
|
||||
store.set("bg.delay", String(cfg.delay));
|
||||
paintSettings(); reset();
|
||||
paintSettings(); resetClocks();
|
||||
});
|
||||
});
|
||||
|
||||
[].forEach.call(document.querySelectorAll("[data-points-adj]"), function(b){
|
||||
b.addEventListener("click", function(){
|
||||
// stepping by 2 from 1 keeps it odd without any parity juggling
|
||||
var next = cfg.points + parseInt(b.dataset.pointsAdj, 10);
|
||||
cfg.points = Math.min(29, Math.max(1, next));
|
||||
store.set("bg.points", String(cfg.points));
|
||||
clampScore(); // a shorter match can't hold a bigger score
|
||||
paintSettings(); resetClocks();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -740,7 +947,7 @@
|
||||
var next = cfg.base + parseInt(b.dataset.timeAdj, 10) * 1000;
|
||||
cfg.base = Math.min(99 * 60000, Math.max(15000, next));
|
||||
store.set("bg.base", String(cfg.base));
|
||||
paintSettings(); reset();
|
||||
paintSettings(); resetClocks();
|
||||
});
|
||||
});
|
||||
[].forEach.call(document.querySelectorAll("[data-delay-adj]"), function(b){
|
||||
@@ -748,7 +955,7 @@
|
||||
var next = cfg.delay + parseInt(b.dataset.delayAdj, 10) * 1000;
|
||||
cfg.delay = Math.min(60000, Math.max(0, next));
|
||||
store.set("bg.delay", String(cfg.delay));
|
||||
paintSettings(); reset();
|
||||
paintSettings(); resetClocks();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -761,18 +968,98 @@
|
||||
document.getElementById("t-sound").addEventListener("click", function(){ setSound(!cfg.sound); });
|
||||
btnSound.addEventListener("click", function(){ setSound(!cfg.sound); });
|
||||
|
||||
function openSheet(on){
|
||||
sheet.classList.toggle("on", on);
|
||||
scrim.classList.toggle("on", on);
|
||||
/* ============ sheets ============ */
|
||||
var openNode = null;
|
||||
function openSheet(node){
|
||||
openNode = node;
|
||||
sheet.classList.toggle("on", node === sheet);
|
||||
scoreSheet.classList.toggle("on", node === scoreSheet);
|
||||
scrim.classList.toggle("on", !!node);
|
||||
}
|
||||
function closeSheet(){ openSheet(null); }
|
||||
|
||||
document.getElementById("settings").addEventListener("click", function(){
|
||||
pauseNow(); // nobody's clock runs while the sheet is open
|
||||
pauseNow(); // nobody's clock runs while a sheet is open
|
||||
disarm();
|
||||
paintSettings();
|
||||
openSheet(true);
|
||||
openSheet(sheet);
|
||||
});
|
||||
document.getElementById("done").addEventListener("click", closeSheet);
|
||||
// tapping outside the score sheet discards the staged edits rather than
|
||||
// committing them — a stray tap must never restart a game
|
||||
scrim.addEventListener("click", closeSheet);
|
||||
|
||||
/* ============ score sheet ============ */
|
||||
// Edits are staged here and only reach `st` on Done, so opening the sheet,
|
||||
// fiddling and tapping away changes nothing.
|
||||
var edit = null, before = null;
|
||||
|
||||
function paintScore(){
|
||||
document.getElementById("v-you").textContent = edit.score[1];
|
||||
document.getElementById("v-them").textContent = edit.score[0];
|
||||
[[1, "you"], [0, "them"]].forEach(function(p){
|
||||
var s = Math.round(edit.time[p[0]] / 1000);
|
||||
document.getElementById("v-" + p[1] + "-min").textContent = Math.floor(s / 60);
|
||||
document.getElementById("v-" + p[1] + "-sec").textContent = ("0" + (s % 60)).slice(-2);
|
||||
});
|
||||
}
|
||||
|
||||
function openScore(){
|
||||
pauseNow();
|
||||
disarm();
|
||||
// seed from what the panels are showing, not from the banked reserve — for
|
||||
// the interrupted turn those differ by the delay already spent
|
||||
edit = { score: st.score.slice(), time: [shownTime(0), shownTime(1)] };
|
||||
before = { score: edit.score.slice(), time: edit.time.slice() };
|
||||
paintScore();
|
||||
openSheet(scoreSheet);
|
||||
}
|
||||
|
||||
function applyScore(){
|
||||
if(!edit) return;
|
||||
var scoreChanged = edit.score[0] !== before.score[0] || edit.score[1] !== before.score[1];
|
||||
var timeChanged = edit.time[0] !== before.time[0] || edit.time[1] !== before.time[1];
|
||||
if(!scoreChanged && !timeChanged) return; // Done with no edits changes nothing
|
||||
|
||||
if(scoreChanged){
|
||||
// the displayed times become the reserves, which banks the interrupted
|
||||
// turn's spend — "the clocks keep their value"
|
||||
st.score = edit.score.slice();
|
||||
st.reserve = [edit.time[0], edit.time[1]];
|
||||
newGame();
|
||||
}else{
|
||||
// a correction mid-game: put the spent delay back on so the panel shows
|
||||
// exactly what was typed, and leave the turn and its delay alone
|
||||
st.reserve = [edit.time[0] + consumed(0), edit.time[1] + consumed(1)];
|
||||
render(); // every other mutator repaints; don't wait a frame
|
||||
}
|
||||
saveGame();
|
||||
}
|
||||
|
||||
[].forEach.call(document.querySelectorAll("[data-score-adj]"), function(b){
|
||||
b.addEventListener("click", function(){
|
||||
if(!edit) return;
|
||||
var who = parseInt(b.dataset.who, 10);
|
||||
var next = edit.score[who] + parseInt(b.dataset.scoreAdj, 10);
|
||||
edit.score[who] = Math.min(cfg.points, Math.max(0, next));
|
||||
paintScore();
|
||||
});
|
||||
});
|
||||
[].forEach.call(document.querySelectorAll("[data-clock-adj]"), function(b){
|
||||
b.addEventListener("click", function(){
|
||||
if(!edit) return;
|
||||
var who = parseInt(b.dataset.who, 10);
|
||||
var next = edit.time[who] + parseInt(b.dataset.clockAdj, 10) * 1000;
|
||||
edit.time[who] = Math.min(99 * 60000, Math.max(0, next));
|
||||
paintScore();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("score").addEventListener("click", openScore);
|
||||
document.getElementById("score-done").addEventListener("click", function(){
|
||||
applyScore();
|
||||
closeSheet();
|
||||
});
|
||||
document.getElementById("done").addEventListener("click", function(){ openSheet(false); });
|
||||
scrim.addEventListener("click", function(){ openSheet(false); });
|
||||
|
||||
btnReset.addEventListener("click", armReset);
|
||||
btnPlay.addEventListener("click", togglePause);
|
||||
@@ -818,8 +1105,9 @@
|
||||
/* ============ go ============ */
|
||||
applyTheme();
|
||||
paintSettings();
|
||||
loadGame(); // restores a match in progress; leaves the fresh state if there's none
|
||||
paintPlay();
|
||||
reset();
|
||||
render();
|
||||
loop();
|
||||
|
||||
if("serviceWorker" in navigator && location.protocol.indexOf("http") === 0){
|
||||
|
||||
Reference in New Issue
Block a user