games: a ladder you climb against the clock
This commit is contained in:
@@ -1081,6 +1081,122 @@ html[data-phase="night"] {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* ---- trivia --------------------------------------------------------------
|
||||
The clock is the game, so the clock is the biggest thing on the felt: a bar
|
||||
that drains across the top of the question, going hot as it runs out. It is
|
||||
*decoration* — the server timed the answer the moment it arrived — but it
|
||||
has to be honest decoration, so it's driven from the seconds the server said
|
||||
were left rather than from when the browser happened to paint. */
|
||||
|
||||
.pete-clock {
|
||||
position: relative;
|
||||
height: 0.6rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.pete-clock-fill {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 999px;
|
||||
background: var(--accent);
|
||||
transform-origin: left center;
|
||||
/* Driven by a transform the browser can run on its own: a width animated on
|
||||
every frame from JS is a layout on every frame. */
|
||||
transform: scaleX(1);
|
||||
}
|
||||
/* The last few seconds. The bar stops being a progress meter and starts being
|
||||
a warning. */
|
||||
.pete-clock[data-hot="1"] .pete-clock-fill { background: #cc3d4a; }
|
||||
.pete-clock[data-hot="1"] { animation: pete-clock-pulse 0.7s ease-in-out infinite; }
|
||||
@keyframes pete-clock-pulse {
|
||||
0%, 100% { box-shadow: inset 0 0 0 2px rgba(204, 61, 74, 0.35); }
|
||||
50% { box-shadow: inset 0 0 0 2px rgba(204, 61, 74, 0.95); }
|
||||
}
|
||||
|
||||
/* The four answers. Big targets, because the clock is already the hard part —
|
||||
a question you lose to a mis-tap is a question about pointing. */
|
||||
.pete-answer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
border-radius: 1rem;
|
||||
padding: 0.85rem 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.26);
|
||||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.1);
|
||||
transition: background 0.15s ease, transform 0.1s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.pete-answer:hover:not(:disabled) {
|
||||
background: rgba(0, 0, 0, 0.36);
|
||||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
.pete-answer:active:not(:disabled) { transform: translateY(1px); }
|
||||
.pete-answer:disabled { cursor: default; }
|
||||
|
||||
/* The letter down the side, so an answer can be picked with the keyboard and
|
||||
the key you press is printed on the thing you're pressing it for. */
|
||||
.pete-answer-key {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 1.75rem;
|
||||
width: 1.75rem;
|
||||
flex: none;
|
||||
border-radius: 0.6rem;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* How it went. Only ever set once the server has decided — the browser has no
|
||||
idea which of these is right until it's told. */
|
||||
.pete-answer[data-state="right"] {
|
||||
background: rgba(46, 160, 103, 0.9);
|
||||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.pete-answer[data-state="wrong"] {
|
||||
background: rgba(204, 61, 74, 0.9);
|
||||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.35);
|
||||
animation: pete-answer-no 0.45s ease;
|
||||
}
|
||||
/* The one they *should* have picked, shown alongside the one they did. */
|
||||
.pete-answer[data-state="missed"] {
|
||||
box-shadow: inset 0 0 0 2px rgba(46, 160, 103, 0.95);
|
||||
}
|
||||
.pete-answer[data-state="dim"] { opacity: 0.4; }
|
||||
@keyframes pete-answer-no {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-6px); }
|
||||
45% { transform: translateX(5px); }
|
||||
70% { transform: translateX(-3px); }
|
||||
}
|
||||
|
||||
/* The ladder: one pip per rung, filling as they're climbed. It is the only
|
||||
picture of how far there is left to fall. */
|
||||
.pete-ladder {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.pete-rung {
|
||||
height: 0.5rem;
|
||||
width: 1.1rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
transition: background 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
.pete-rung[data-on="1"] {
|
||||
background: var(--accent);
|
||||
transform: scaleY(1.35);
|
||||
}
|
||||
|
||||
/* ---- solitaire -----------------------------------------------------------
|
||||
Seven columns, four foundations, a stock and a waste, all of which have to
|
||||
fit across the felt on a phone as well as a desk. So the card size is one
|
||||
@@ -1299,6 +1415,11 @@ html[data-phase="night"] {
|
||||
.pete-nope,
|
||||
.pete-home-flash { animation: none; }
|
||||
.pete-card[data-held="1"] { transition: none; }
|
||||
/* The clock still drains — it is information, not decoration — but it stops
|
||||
pulsing at you, and a wrong answer stops shaking. */
|
||||
.pete-clock[data-hot="1"],
|
||||
.pete-answer[data-state="wrong"] { animation: none; }
|
||||
.pete-rung { transition: none; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
546
internal/web/static/js/trivia.js
Normal file
546
internal/web/static/js/trivia.js
Normal file
@@ -0,0 +1,546 @@
|
||||
// The trivia table.
|
||||
//
|
||||
// Same bargain as every other table in the room: the browser holds no game. It
|
||||
// sends an answer, and the server says how it went. The four buttons arrive
|
||||
// without any mark on which of them is right — that index is in the engine
|
||||
// state, on the server — and the reveal only comes back in the event that
|
||||
// decides the question, by which point knowing it is worth nothing.
|
||||
//
|
||||
// The countdown here is decoration, and it is important to be clear about that.
|
||||
// Nothing it does scores anything: the server timed the answer the moment it
|
||||
// arrived, against the clock it started when it served the question. Stopping
|
||||
// this bar, or reloading to restart it, changes nothing at all. What the bar
|
||||
// owes the player is *honesty* — so it is seeded from the seconds the server
|
||||
// says are left, not from when the browser got round to painting.
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var root = document.querySelector("[data-trivia]");
|
||||
if (!root) return;
|
||||
|
||||
var FX = window.PeteFX;
|
||||
|
||||
var questionEl = root.querySelector("[data-question]");
|
||||
var categoryEl = root.querySelector("[data-category]");
|
||||
var answersEl = root.querySelector("[data-answers]");
|
||||
var clockEl = root.querySelector("[data-clock]");
|
||||
var clockFillEl = root.querySelector("[data-clock-fill]");
|
||||
var countdownEl = root.querySelector("[data-countdown]");
|
||||
var ladderEl = root.querySelector("[data-ladder]");
|
||||
var multEl = root.querySelector("[data-multiple]");
|
||||
var meterEl = root.querySelector("[data-meter]");
|
||||
var standsEl = root.querySelector("[data-stands]");
|
||||
var standsLbl = root.querySelector("[data-stands-label]");
|
||||
var rungEl = root.querySelector("[data-rung]");
|
||||
var verdictEl = root.querySelector("[data-verdict]");
|
||||
|
||||
var betting = root.querySelector("[data-betting]");
|
||||
var playing = root.querySelector("[data-playing]");
|
||||
var walkBtn = root.querySelector("[data-walk]");
|
||||
var walkAmtEl = root.querySelector("[data-walk-amount]");
|
||||
var betAmount = root.querySelector("[data-bet-amount]");
|
||||
var startBtn = root.querySelector("[data-start]");
|
||||
var msgEl = root.querySelector("[data-table-msg]");
|
||||
var gameMsgEl = root.querySelector("[data-game-msg]");
|
||||
|
||||
var purseEl = document.querySelector("[data-chips]");
|
||||
var spotEl = root.querySelector("[data-spot]");
|
||||
var houseEl = root.querySelector("[data-house]");
|
||||
|
||||
// The bet spot, and the rule that comes with it: the number under the pile is
|
||||
// a readout of the pile, never the other way round.
|
||||
var spot = FX.spot({
|
||||
spot: spotEl,
|
||||
stack: root.querySelector("[data-stack]"),
|
||||
total: root.querySelector("[data-spot-total]"),
|
||||
});
|
||||
|
||||
var bet = 0; // what you're building between games
|
||||
var busy = false;
|
||||
var game = null; // the round as the server last described it
|
||||
var tier = "medium";
|
||||
|
||||
var reduced = FX.reduced;
|
||||
function pace(ms) { return reduced ? 0 : ms; }
|
||||
function wait(ms) { return new Promise(function (r) { setTimeout(r, pace(ms)); }); }
|
||||
|
||||
function say(text, tone, where) {
|
||||
var el = where || msgEl;
|
||||
if (!el) return;
|
||||
if (!text) { el.classList.add("hidden"); return; }
|
||||
el.textContent = text;
|
||||
el.classList.remove("hidden");
|
||||
el.style.color = tone === "bad" ? "#cc3d4a" : "";
|
||||
}
|
||||
|
||||
// ---- the clock -------------------------------------------------------------
|
||||
|
||||
var raf = null;
|
||||
var clockDeadline = 0; // performance.now() ms at which this question dies
|
||||
var clockLimit = 1;
|
||||
var timedOut = false;
|
||||
|
||||
// HOT is when the bar stops being a progress meter and starts being a warning.
|
||||
var HOT = 5;
|
||||
|
||||
// GRACE is how long past its own zero the browser waits before reporting the
|
||||
// timeout. It cannot be negative: the browser's countdown starts when the
|
||||
// response *arrives*, so it is already behind the server's by the latency of
|
||||
// that response, and it therefore always reaches zero after the server has.
|
||||
// The grace is only there so that "always" survives a rounding error.
|
||||
var GRACE = 400;
|
||||
|
||||
function stopClock() {
|
||||
if (raf) cancelAnimationFrame(raf);
|
||||
raf = null;
|
||||
}
|
||||
|
||||
function startClock(left, limit) {
|
||||
stopClock();
|
||||
timedOut = false;
|
||||
clockLimit = limit > 0 ? limit : 1;
|
||||
clockDeadline = performance.now() + left * 1000;
|
||||
tick();
|
||||
}
|
||||
|
||||
function tick() {
|
||||
var left = (clockDeadline - performance.now()) / 1000;
|
||||
if (left < 0) left = 0;
|
||||
|
||||
// A transform, not a width: the browser can run this one without laying the
|
||||
// page out again on every frame.
|
||||
clockFillEl.style.transform = "scaleX(" + (left / clockLimit).toFixed(4) + ")";
|
||||
countdownEl.textContent = left.toFixed(1) + "s";
|
||||
clockEl.dataset.hot = left <= HOT && left > 0 ? "1" : "0";
|
||||
|
||||
if (left <= 0) {
|
||||
countdownEl.textContent = "0.0s";
|
||||
clockEl.dataset.hot = "0";
|
||||
timeUp();
|
||||
return;
|
||||
}
|
||||
raf = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
// timeUp reports the clock running out. It is not the browser *deciding* the
|
||||
// question — it is the browser telling the server the player never answered,
|
||||
// and the server (which has been holding the real clock all along) agreeing.
|
||||
function timeUp() {
|
||||
stopClock();
|
||||
if (timedOut || busy || !game || game.phase !== "playing") return;
|
||||
timedOut = true;
|
||||
lockAnswers();
|
||||
setTimeout(function () {
|
||||
// -1 is "no answer". The engine checks the clock before it checks the
|
||||
// choice, so this resolves as the timeout it is rather than a bad move.
|
||||
send("/api/games/trivia/answer", { choice: -1 }, gameMsgEl);
|
||||
}, GRACE);
|
||||
}
|
||||
|
||||
function clearClock() {
|
||||
stopClock();
|
||||
clockFillEl.style.transform = "scaleX(0)";
|
||||
countdownEl.textContent = "";
|
||||
clockEl.dataset.hot = "0";
|
||||
}
|
||||
|
||||
// ---- the question ----------------------------------------------------------
|
||||
|
||||
var KEYS = ["1", "2", "3", "4"];
|
||||
|
||||
function renderQuestion(v) {
|
||||
answersEl.innerHTML = "";
|
||||
if (!v || v.phase !== "playing" || !v.answers) {
|
||||
questionEl.textContent = "";
|
||||
categoryEl.textContent = "";
|
||||
return;
|
||||
}
|
||||
categoryEl.textContent = v.category || "";
|
||||
questionEl.textContent = v.question || "";
|
||||
|
||||
v.answers.forEach(function (text, i) {
|
||||
var b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = "pete-answer";
|
||||
b.dataset.at = String(i);
|
||||
|
||||
var key = document.createElement("span");
|
||||
key.className = "pete-answer-key";
|
||||
key.textContent = KEYS[i] || "";
|
||||
key.setAttribute("aria-hidden", "true");
|
||||
|
||||
var label = document.createElement("span");
|
||||
label.textContent = text;
|
||||
|
||||
b.appendChild(key);
|
||||
b.appendChild(label);
|
||||
b.addEventListener("click", function () { answer(i); });
|
||||
answersEl.appendChild(b);
|
||||
});
|
||||
}
|
||||
|
||||
function lockAnswers() {
|
||||
answersEl.querySelectorAll(".pete-answer").forEach(function (b) { b.disabled = true; });
|
||||
}
|
||||
|
||||
// reveal marks the board once the server has decided. Nothing in here is known
|
||||
// until it comes back: the right answer arrives in the event, not in the view.
|
||||
function reveal(choice, correct) {
|
||||
answersEl.querySelectorAll(".pete-answer").forEach(function (b) {
|
||||
var i = parseInt(b.dataset.at, 10);
|
||||
b.disabled = true;
|
||||
if (i === choice && i === correct) b.dataset.state = "right";
|
||||
else if (i === choice) b.dataset.state = "wrong";
|
||||
else if (i === correct) b.dataset.state = "missed";
|
||||
else b.dataset.state = "dim";
|
||||
});
|
||||
}
|
||||
|
||||
// ---- the meters ------------------------------------------------------------
|
||||
|
||||
function renderLadder(v) {
|
||||
ladderEl.innerHTML = "";
|
||||
var rungs = (v && v.rungs) || 12;
|
||||
var done = (v && v.rung) || 0;
|
||||
for (var i = 0; i < rungs; i++) {
|
||||
var pip = document.createElement("span");
|
||||
pip.className = "pete-rung";
|
||||
pip.dataset.on = i < done ? "1" : "0";
|
||||
ladderEl.appendChild(pip);
|
||||
}
|
||||
}
|
||||
|
||||
function renderMeter(v) {
|
||||
if (!v) {
|
||||
multEl.textContent = "—";
|
||||
standsEl.textContent = "—";
|
||||
standsLbl.textContent = "if you walk";
|
||||
meterEl.dataset.cold = "1";
|
||||
rungEl.textContent = "";
|
||||
return;
|
||||
}
|
||||
multEl.textContent = v.multiple.toFixed(2) + "×";
|
||||
standsEl.textContent = (v.stands || 0).toLocaleString();
|
||||
meterEl.dataset.cold = v.rung === 0 ? "1" : "0";
|
||||
|
||||
if (v.phase === "done") {
|
||||
standsLbl.textContent = v.net > 0 ? "banked" : "gone";
|
||||
rungEl.textContent = "";
|
||||
return;
|
||||
}
|
||||
standsLbl.textContent = v.can_walk ? "if you walk" : "answer one to unlock";
|
||||
rungEl.textContent = "Question " + (v.rung + 1) + " of " + v.rungs;
|
||||
}
|
||||
|
||||
// knock rolls the multiple up to its new value rather than swapping it, so a
|
||||
// right answer reads as the total *growing* — which is the thing you're
|
||||
// deciding whether to risk.
|
||||
function climb(v) {
|
||||
var from = parseFloat(multEl.textContent) || 1;
|
||||
var to = v.multiple;
|
||||
if (reduced) { renderMeter(v); return; }
|
||||
var t0 = performance.now();
|
||||
meterEl.dataset.hit = "0";
|
||||
|
||||
(function step(now) {
|
||||
var p = Math.min(1, (now - t0) / 420);
|
||||
var eased = 1 - Math.pow(1 - p, 3);
|
||||
multEl.textContent = (from + (to - from) * eased).toFixed(2) + "×";
|
||||
if (p < 1) requestAnimationFrame(step);
|
||||
else renderMeter(v);
|
||||
})(t0);
|
||||
}
|
||||
|
||||
// ---- the money -------------------------------------------------------------
|
||||
|
||||
function settleChips(final) {
|
||||
var payout = final.payout || 0;
|
||||
var back = payout - final.bet;
|
||||
|
||||
if (payout <= 0) {
|
||||
var chain = spot.sweep(houseEl, final.bet, { gap: 45, lift: 0.6, fade: true });
|
||||
return chain;
|
||||
}
|
||||
return spot
|
||||
.pour(houseEl, back, { gap: 60 })
|
||||
.then(function () { return wait(back > 0 ? 380 : 200); })
|
||||
.then(function () { return spot.sweep(purseEl, payout, { gap: 40, lift: 0.8 }); });
|
||||
}
|
||||
|
||||
// standing puts the stake back on the spot for the next ladder, the way every
|
||||
// other table in the room leaves your bet up.
|
||||
function standing(amount) {
|
||||
var money = window.PeteGames.view();
|
||||
if (!amount || !money || money.chips < amount) {
|
||||
bet = 0;
|
||||
showBet();
|
||||
return;
|
||||
}
|
||||
bet = amount;
|
||||
showBet();
|
||||
spot.amount = amount;
|
||||
return spot.pour(purseEl, amount);
|
||||
}
|
||||
|
||||
// ---- phases ----------------------------------------------------------------
|
||||
|
||||
var VERDICTS = {
|
||||
walked: "Banked it.",
|
||||
cleared: "Cleared the board! 🎉",
|
||||
wrong: "Wrong.",
|
||||
timeout: "Out of time.",
|
||||
};
|
||||
|
||||
function verdict(v) {
|
||||
var text = VERDICTS[v.outcome] || "";
|
||||
if (!text) { verdictEl.classList.add("hidden"); return; }
|
||||
if (v.net > 0) text += " +" + v.net.toLocaleString();
|
||||
else if (v.net < 0) text += " " + v.net.toLocaleString();
|
||||
verdictEl.textContent = text;
|
||||
verdictEl.classList.remove("hidden");
|
||||
|
||||
// Confetti only for clearing all twelve — the one thing in here worth it.
|
||||
if (v.outcome === "cleared") FX.burst(verdictEl, { count: 34 });
|
||||
}
|
||||
|
||||
function setPhase(v) {
|
||||
game = v;
|
||||
var live = !!v && v.phase === "playing";
|
||||
betting.classList.toggle("hidden", live);
|
||||
playing.classList.toggle("hidden", !live);
|
||||
if (walkBtn) {
|
||||
walkBtn.disabled = !live || !v.can_walk;
|
||||
walkAmtEl.textContent = (v && v.can_walk ? v.stands : 0).toLocaleString();
|
||||
}
|
||||
if (!v || !v.outcome) verdictEl.classList.add("hidden");
|
||||
}
|
||||
|
||||
// paint puts a round up with no animation: the resume path, after a reload or a
|
||||
// redeploy. The clock picks up exactly where the server says it is — which is
|
||||
// the whole point of it being the server's clock.
|
||||
function paint(v) {
|
||||
if (!v) {
|
||||
clearClock();
|
||||
renderQuestion(null);
|
||||
renderLadder(null);
|
||||
renderMeter(null);
|
||||
spot.render(0);
|
||||
setPhase(null);
|
||||
return;
|
||||
}
|
||||
renderQuestion(v);
|
||||
renderLadder(v);
|
||||
renderMeter(v);
|
||||
spot.render(v.phase === "done" ? 0 : v.bet);
|
||||
setPhase(v);
|
||||
|
||||
if (v.phase === "playing") startClock(v.left, v.limit);
|
||||
else clearClock();
|
||||
}
|
||||
|
||||
// ---- the script ------------------------------------------------------------
|
||||
|
||||
// play walks the server's events. Same rule as the other tables: on a live
|
||||
// round the money is already right (your stake left your pile when you pressed
|
||||
// Play, and it's on the spot), but on a settling one the chip bar is held back
|
||||
// until the chips have physically come home. A counter that pays you before
|
||||
// the reveal is a counter that has told you the ending.
|
||||
function play(view, money) {
|
||||
var events = view.triv_events || [];
|
||||
var final = view.trivia;
|
||||
var settles = !!final && final.phase === "done";
|
||||
var chain = Promise.resolve();
|
||||
|
||||
if (!settles) money();
|
||||
|
||||
stopClock();
|
||||
|
||||
events.forEach(function (e) {
|
||||
chain = chain.then(function () {
|
||||
switch (e.kind) {
|
||||
case "ask":
|
||||
// A fresh question. Everything about the last one goes.
|
||||
verdictEl.classList.add("hidden");
|
||||
renderQuestion(final);
|
||||
renderLadder(final);
|
||||
if (final && final.phase === "playing") startClock(final.left, final.limit);
|
||||
return;
|
||||
|
||||
case "right":
|
||||
reveal(e.choice, e.correct);
|
||||
if (final) {
|
||||
// The rung lighting and the multiple climbing are one event,
|
||||
// because they are one event: this is what the answer was worth.
|
||||
climb({ multiple: e.multiple, rung: final.rung, rungs: final.rungs,
|
||||
stands: final.stands, can_walk: true, phase: "playing" });
|
||||
renderLadder(final);
|
||||
}
|
||||
return wait(900);
|
||||
|
||||
case "wrong":
|
||||
reveal(e.choice, e.correct);
|
||||
return wait(1100);
|
||||
|
||||
case "timeout":
|
||||
reveal(-1, e.correct);
|
||||
return wait(1100);
|
||||
|
||||
case "settle":
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return chain.then(function () {
|
||||
if (!final) { paint(null); money(); return; }
|
||||
|
||||
if (!settles) {
|
||||
renderMeter(final);
|
||||
setPhase(final);
|
||||
return;
|
||||
}
|
||||
|
||||
// Over: the clock stops, the money moves, and only then does the bar catch up.
|
||||
clearClock();
|
||||
playing.classList.add("hidden");
|
||||
renderMeter(final);
|
||||
renderLadder(final);
|
||||
verdict(final);
|
||||
return settleChips(final)
|
||||
.then(money)
|
||||
.then(function () { return standing(final.bet); })
|
||||
.then(function () { setPhase(final); });
|
||||
});
|
||||
}
|
||||
|
||||
// ---- talking to the table ---------------------------------------------------
|
||||
|
||||
function send(path, body, where) {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
say("", null, where);
|
||||
return window.PeteGames.post(path, body)
|
||||
.then(function (view) {
|
||||
return play(view, function () { window.PeteGames.apply(view); });
|
||||
})
|
||||
.catch(function (err) {
|
||||
say(err.message, "bad", where);
|
||||
return window.PeteGames.refresh().then(function (v) {
|
||||
if (v && v.trivia) paint(v.trivia);
|
||||
else { paint(null); spot.render(0); }
|
||||
});
|
||||
})
|
||||
.then(function () { busy = false; });
|
||||
}
|
||||
|
||||
function answer(i) {
|
||||
if (busy || timedOut || !game || game.phase !== "playing") return;
|
||||
stopClock();
|
||||
lockAnswers();
|
||||
send("/api/games/trivia/answer", { choice: i }, gameMsgEl);
|
||||
}
|
||||
|
||||
if (walkBtn) {
|
||||
walkBtn.addEventListener("click", function () {
|
||||
if (busy || !game || game.phase !== "playing" || !game.can_walk) return;
|
||||
stopClock();
|
||||
lockAnswers();
|
||||
send("/api/games/trivia/answer", { walk: true }, gameMsgEl);
|
||||
});
|
||||
}
|
||||
|
||||
// 1–4 answers the question. The key is printed on the button it answers.
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
if (/^(input|textarea|select)$/i.test(e.target.tagName || "")) return;
|
||||
if (!game || game.phase !== "playing" || busy) return;
|
||||
var i = KEYS.indexOf(e.key);
|
||||
if (i === -1) return;
|
||||
var btn = answersEl.querySelector('.pete-answer[data-at="' + i + '"]');
|
||||
if (!btn || btn.disabled) return;
|
||||
e.preventDefault();
|
||||
answer(i);
|
||||
});
|
||||
|
||||
// ---- betting ----------------------------------------------------------------
|
||||
|
||||
function showBet() {
|
||||
betAmount.textContent = bet.toLocaleString();
|
||||
var money = window.PeteGames.view();
|
||||
if (startBtn) startBtn.disabled = bet <= 0 || !tier || !money || money.chips < bet;
|
||||
}
|
||||
|
||||
function pickTier(slug) {
|
||||
tier = slug;
|
||||
root.querySelectorAll("[data-tier]").forEach(function (b) {
|
||||
b.dataset.on = b.dataset.tier === slug ? "1" : "0";
|
||||
});
|
||||
showBet();
|
||||
}
|
||||
|
||||
root.querySelectorAll("[data-tier]").forEach(function (b) {
|
||||
b.addEventListener("click", function () {
|
||||
if (busy) return;
|
||||
pickTier(b.dataset.tier);
|
||||
});
|
||||
});
|
||||
|
||||
// The chip you click is the chip that flies. Scoped to buttons: the bare
|
||||
// [data-chip] spans in the corner are the house's rack, and it is not betting.
|
||||
root.querySelectorAll("button[data-chip]").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
if (busy) return;
|
||||
var d = parseInt(btn.dataset.chip, 10);
|
||||
var money = window.PeteGames.view();
|
||||
if (money && bet + d > money.chips) {
|
||||
say("You haven't got that many chips.", "bad");
|
||||
return;
|
||||
}
|
||||
bet += d;
|
||||
showBet();
|
||||
|
||||
var target = bet;
|
||||
spot.amount = bet;
|
||||
FX.fly(btn, spotEl, { denom: d }).then(function () {
|
||||
if (bet >= target) spot.render(target); // unless Clear got there first
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var clearBtn = root.querySelector("[data-bet-clear]");
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener("click", function () {
|
||||
if (busy) return;
|
||||
if (spot.amount) spot.sweep(purseEl, null, { gap: 40, lift: 0.7 });
|
||||
bet = 0;
|
||||
showBet();
|
||||
});
|
||||
}
|
||||
|
||||
if (startBtn) {
|
||||
startBtn.addEventListener("click", function () {
|
||||
if (busy) return;
|
||||
if (!tier) { say("Pick a difficulty first.", "bad"); return; }
|
||||
if (bet <= 0) { say("Put something on it first.", "bad"); return; }
|
||||
// The stake stays on the spot for the whole ladder: it is what's at risk,
|
||||
// and it is riding on every question until you take it back or lose it.
|
||||
send("/api/games/trivia/start", { bet: bet, tier: tier });
|
||||
});
|
||||
}
|
||||
|
||||
pickTier(tier);
|
||||
|
||||
var resumed = false;
|
||||
window.PeteGames.onUpdate(function (v) {
|
||||
if (!resumed) {
|
||||
resumed = true;
|
||||
if (v.trivia) {
|
||||
paint(v.trivia);
|
||||
if (v.trivia.phase === "done") verdict(v.trivia);
|
||||
} else {
|
||||
paint(null);
|
||||
}
|
||||
}
|
||||
showBet();
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user