Think timer and players-remaining count in the action bar #25
@@ -1094,6 +1094,11 @@ body.hasActionBar #holdemSidebar {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actionPlayers {
|
||||||
|
color: #9c9c9c;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
.actionWho {
|
.actionWho {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
color: #9c9c9c;
|
color: #9c9c9c;
|
||||||
@@ -1102,6 +1107,26 @@ body.hasActionBar #holdemSidebar {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Stopwatch, not a shot clock: it colours up as a prompt to the dealer, and does
|
||||||
|
nothing else. */
|
||||||
|
.actionTank {
|
||||||
|
flex: none;
|
||||||
|
min-width: 3.1rem;
|
||||||
|
text-align: right;
|
||||||
|
color: #7d7d7d;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionTank.tankNudge {
|
||||||
|
color: #d8b46a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionTank.tankSlow {
|
||||||
|
color: #e07b7b;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Side-pot breakdown ---------- */
|
/* ---------- Side-pot breakdown ---------- */
|
||||||
|
|
||||||
.potBreakdown {
|
.potBreakdown {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ const ActionBar = {
|
|||||||
if (!this.isRunning()) {
|
if (!this.isRunning()) {
|
||||||
host.hidden = true;
|
host.hidden = true;
|
||||||
document.body.classList.remove("hasActionBar");
|
document.body.classList.remove("hasActionBar");
|
||||||
|
this.stopTankTimer();
|
||||||
|
this.tankSeatIndex = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
host.hidden = false;
|
host.hidden = false;
|
||||||
@@ -46,6 +48,8 @@ const ActionBar = {
|
|||||||
} else {
|
} else {
|
||||||
host.appendChild(this.renderActions());
|
host.appendChild(this.renderActions());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.syncTankTimer();
|
||||||
},
|
},
|
||||||
|
|
||||||
renderStatus() {
|
renderStatus() {
|
||||||
@@ -62,6 +66,14 @@ const ActionBar = {
|
|||||||
pot.textContent = `Pot ${Money.format(Betting.potSoFar(this.state))}`;
|
pot.textContent = `Pot ${Money.format(Betting.potSoFar(this.state))}`;
|
||||||
row.appendChild(pot);
|
row.appendChild(pot);
|
||||||
|
|
||||||
|
// How many are still in the hand — part of the standard per-street
|
||||||
|
// announcement, and the number a dealer is asked for most often.
|
||||||
|
const remaining = Betting.liveSeats(this.state).length;
|
||||||
|
const players = document.createElement("span");
|
||||||
|
players.className = "actionPlayers";
|
||||||
|
players.textContent = `${remaining} ${remaining === 1 ? "player" : "players"}`;
|
||||||
|
row.appendChild(players);
|
||||||
|
|
||||||
const index = this.state.actingSeatIndex;
|
const index = this.state.actingSeatIndex;
|
||||||
if (index !== null && index !== undefined && !Betting.isHandOver(this.state)) {
|
if (index !== null && index !== undefined && !Betting.isHandOver(this.state)) {
|
||||||
const player = this.state.seats[index];
|
const player = this.state.seats[index];
|
||||||
@@ -69,15 +81,87 @@ const ActionBar = {
|
|||||||
who.className = "actionWho";
|
who.className = "actionWho";
|
||||||
who.textContent = `${HoldemState.displayName(player, index)} · ${Money.format(Betting.stackOf(this.state, player))} behind`;
|
who.textContent = `${HoldemState.displayName(player, index)} · ${Money.format(Betting.stackOf(this.state, player))} behind`;
|
||||||
row.appendChild(who);
|
row.appendChild(who);
|
||||||
|
|
||||||
|
const tank = document.createElement("span");
|
||||||
|
tank.className = "actionTank";
|
||||||
|
tank.id = "actionTank";
|
||||||
|
tank.title = "How long this player has been thinking";
|
||||||
|
tank.textContent = "0:00";
|
||||||
|
row.appendChild(tank);
|
||||||
}
|
}
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ---- Think timer ----
|
||||||
|
//
|
||||||
|
// A player who hasn't noticed the action is on them costs more time than anything
|
||||||
|
// else a dealer does, so the bar shows how long the current player has been in the
|
||||||
|
// tank. It's a stopwatch, not a shot clock: nothing happens when it grows, it just
|
||||||
|
// tells the dealer when it's worth a nudge.
|
||||||
|
NUDGE_AFTER_S: 30,
|
||||||
|
SLOW_AFTER_S: 60,
|
||||||
|
|
||||||
|
tankSeatIndex: null,
|
||||||
|
tankStartedAt: null,
|
||||||
|
tankInterval: null,
|
||||||
|
|
||||||
|
// Restarts only when the action actually moves — a re-render for any other reason
|
||||||
|
// (a chip edit, a name change) must not put a thinking player back to zero.
|
||||||
|
syncTankTimer() {
|
||||||
|
const index = this.state.actingSeatIndex;
|
||||||
|
const running = index !== null && index !== undefined && !Betting.isHandOver(this.state);
|
||||||
|
|
||||||
|
if (!running) {
|
||||||
|
this.stopTankTimer();
|
||||||
|
this.tankSeatIndex = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.tankSeatIndex !== index) {
|
||||||
|
this.tankSeatIndex = index;
|
||||||
|
this.tankStartedAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.tankInterval) {
|
||||||
|
this.tankInterval = setInterval(() => this.paintTank(), 1000);
|
||||||
|
}
|
||||||
|
this.paintTank();
|
||||||
|
},
|
||||||
|
|
||||||
|
stopTankTimer() {
|
||||||
|
if (this.tankInterval) {
|
||||||
|
clearInterval(this.tankInterval);
|
||||||
|
this.tankInterval = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Updates only the timer's own text, so the seconds ticking over never rebuilds the
|
||||||
|
// buttons under the dealer's thumb.
|
||||||
|
paintTank() {
|
||||||
|
const el = document.getElementById("actionTank");
|
||||||
|
if (!el || this.tankStartedAt === null) return;
|
||||||
|
|
||||||
|
const seconds = Math.floor((Date.now() - this.tankStartedAt) / 1000);
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
el.textContent = `${mins}:${String(seconds % 60).padStart(2, "0")}`;
|
||||||
|
el.classList.toggle("tankNudge", seconds >= this.NUDGE_AFTER_S && seconds < this.SLOW_AFTER_S);
|
||||||
|
el.classList.toggle("tankSlow", seconds >= this.SLOW_AFTER_S);
|
||||||
|
},
|
||||||
|
|
||||||
// Shown only once someone is all-in and the money has actually split — a single
|
// Shown only once someone is all-in and the money has actually split — a single
|
||||||
// undivided pot needs no announcement, and a line that's always there stops being
|
// undivided pot needs no announcement, and a line that's always there stops being
|
||||||
// read.
|
// read.
|
||||||
renderPotBreakdown() {
|
renderPotBreakdown() {
|
||||||
|
// Only worth showing once someone is actually all-in. Mid-street the amounts in
|
||||||
|
// front of players are unequal all the time — the blinds alone are unequal —
|
||||||
|
// and layering that would announce a side pot on every hand before anyone had
|
||||||
|
// even acted.
|
||||||
|
const someoneAllIn = this.state.seats.some(
|
||||||
|
player => player.inHand && Betting.handTotal(player) > 0 && Betting.stackOf(this.state, player) <= 0
|
||||||
|
);
|
||||||
|
if (!someoneAllIn) return null;
|
||||||
|
|
||||||
const contributions = this.state.seats.map((player, index) => ({
|
const contributions = this.state.seats.map((player, index) => ({
|
||||||
id: player.id,
|
id: player.id,
|
||||||
index,
|
index,
|
||||||
|
|||||||
Reference in New Issue
Block a user