343 lines
14 KiB
JavaScript
343 lines
14 KiB
JavaScript
document.addEventListener("DOMContentLoaded", () => {
|
|
const roundDisplay = document.getElementById("round");
|
|
const streetDisplay = document.getElementById("street");
|
|
const potDisplay = document.getElementById("pot");
|
|
const communityCards = document.getElementById("community-cards");
|
|
const playerCards = document.getElementById("player-cards");
|
|
const spectatorNote = document.getElementById("spectator-note");
|
|
const playerName = document.getElementById("player-name");
|
|
const currentBet = document.getElementById("current-bet");
|
|
const yourContribution = document.getElementById("your-contribution");
|
|
const yourStack = document.getElementById("your-stack");
|
|
const currentTurn = document.getElementById("current-turn");
|
|
const turnState = document.getElementById("turn-state");
|
|
const turnQueue = document.getElementById("turn-queue");
|
|
const lastAction = document.getElementById("last-action");
|
|
const showdownPanel = document.getElementById("showdown-panel");
|
|
const errorMessage = document.getElementById("error-message");
|
|
const leaveTable = document.getElementById("leave-table");
|
|
const betModal = document.getElementById("bet-modal");
|
|
const closeBet = document.getElementById("close-bet");
|
|
const betRange = document.getElementById("bet-range");
|
|
const betValue = document.getElementById("bet-value");
|
|
const decreaseBet = document.getElementById("decrease-bet");
|
|
const increaseBet = document.getElementById("increase-bet");
|
|
const confirmBet = document.getElementById("confirm-bet");
|
|
const actionButtons = document.querySelectorAll(".actions button");
|
|
const params = new URLSearchParams(window.location.search);
|
|
const lobbyId = params.get("lobbyId") || localStorage.getItem("activeLobbyId");
|
|
let socket = null;
|
|
let socketReady = null;
|
|
let isYourTurn = false;
|
|
let canLeaveTable = true;
|
|
let isSpectator = false;
|
|
let currentStackValue = 400;
|
|
let minimumStake = 10;
|
|
let callAmount = 0;
|
|
let countdownTimer = null;
|
|
const actionTooltips = {
|
|
hit: "Check or call if your bet already matches the table.",
|
|
fold: "Give up this hand and lose your current stake.",
|
|
bet: "Open the bet dialog to add chips to the pot."
|
|
};
|
|
|
|
actionButtons.forEach((button) => {
|
|
const tooltip = actionTooltips[button.dataset.action];
|
|
if (!tooltip) return;
|
|
button.title = tooltip;
|
|
button.setAttribute("aria-label", `${button.textContent.trim()}: ${tooltip}`);
|
|
});
|
|
closeBet.title = "Close the bet dialog.";
|
|
decreaseBet.title = "Decrease the bet amount.";
|
|
increaseBet.title = "Increase the bet amount.";
|
|
confirmBet.title = "Place the selected bet.";
|
|
|
|
const showError = (message) => {
|
|
errorMessage.textContent = message;
|
|
errorMessage.classList.remove("hidden");
|
|
};
|
|
|
|
const hideError = () => {
|
|
errorMessage.textContent = "";
|
|
errorMessage.classList.add("hidden");
|
|
};
|
|
|
|
const renderCard = (card, fallback) => {
|
|
const cardElement = document.createElement("div");
|
|
cardElement.className = "card";
|
|
if (!card) {
|
|
cardElement.classList.add("placeholder");
|
|
cardElement.textContent = fallback;
|
|
return cardElement;
|
|
}
|
|
|
|
cardElement.classList.toggle("red", card.suit === "H" || card.suit === "D");
|
|
cardElement.innerHTML = `<span>${card.rank}</span><span>${card.suit}</span>`;
|
|
return cardElement;
|
|
};
|
|
|
|
const renderCards = (container, cards, count, fallbackPrefix) => {
|
|
container.innerHTML = "";
|
|
for (let index = 0; index < count; index++) {
|
|
container.appendChild(renderCard(cards[index], `${fallbackPrefix} ${index + 1}`));
|
|
}
|
|
};
|
|
|
|
const renderQueue = (queue = [], currentPlayer = null) => {
|
|
turnQueue.innerHTML = "";
|
|
queue.forEach((player, index) => {
|
|
const item = document.createElement("li");
|
|
item.textContent = index === 0 ? `${player} (now)` : player;
|
|
item.classList.toggle("active", player === currentPlayer);
|
|
turnQueue.appendChild(item);
|
|
});
|
|
};
|
|
|
|
const renderShowdown = (showdown = [], winner = null) => {
|
|
showdownPanel.innerHTML = "";
|
|
showdownPanel.classList.toggle("hidden", showdown.length === 0 && !winner);
|
|
|
|
if (winner) {
|
|
const heading = document.createElement("h2");
|
|
heading.textContent = `Winner: ${winner}`;
|
|
showdownPanel.appendChild(heading);
|
|
}
|
|
|
|
showdown.forEach((entry) => {
|
|
const row = document.createElement("div");
|
|
row.className = "showdown-row";
|
|
|
|
const name = document.createElement("span");
|
|
name.textContent = `${entry.player}: ${entry.result.name}`;
|
|
row.appendChild(name);
|
|
|
|
const cards = document.createElement("div");
|
|
cards.className = "mini-cards";
|
|
(entry.result.comboCards || entry.hand).forEach((card) => cards.appendChild(renderCard(card, "?")));
|
|
row.appendChild(cards);
|
|
|
|
showdownPanel.appendChild(row);
|
|
});
|
|
};
|
|
|
|
const sendSocketMessage = async (message) => {
|
|
const ws = await connectSocket();
|
|
ws.send(JSON.stringify(message));
|
|
};
|
|
|
|
const updateActionButtons = () => {
|
|
actionButtons.forEach((button) => {
|
|
button.disabled = !isYourTurn || isSpectator;
|
|
if (button.dataset.action === "hit") {
|
|
button.title = callAmount > 0
|
|
? `Call with $${Math.min(callAmount, currentStackValue)}.`
|
|
: actionTooltips.hit;
|
|
}
|
|
});
|
|
confirmBet.disabled = !isYourTurn || isSpectator;
|
|
leaveTable.disabled = false;
|
|
leaveTable.title = "Leave table. Chips already in the pot are lost.";
|
|
};
|
|
|
|
const stopCountdown = () => {
|
|
if (countdownTimer) {
|
|
clearInterval(countdownTimer);
|
|
countdownTimer = null;
|
|
}
|
|
};
|
|
|
|
const renderIntermissionCountdown = (startsAt, baseMessage) => {
|
|
stopCountdown();
|
|
const updateCountdown = () => {
|
|
const seconds = Math.max(0, Math.ceil((Number(startsAt) - Date.now()) / 1000));
|
|
currentTurn.textContent = `${seconds}s`;
|
|
turnState.textContent = "Waiting for next round";
|
|
lastAction.textContent = baseMessage || `Next round starts in ${seconds}s. Minimum stake: $${minimumStake}.`;
|
|
if (seconds <= 0) {
|
|
stopCountdown();
|
|
}
|
|
};
|
|
updateCountdown();
|
|
countdownTimer = setInterval(updateCountdown, 1000);
|
|
};
|
|
|
|
const renderTurnCountdown = (startsAt, currentPlayer) => {
|
|
stopCountdown();
|
|
const updateCountdown = () => {
|
|
const seconds = Math.max(0, Math.ceil((Number(startsAt) - Date.now()) / 1000));
|
|
currentTurn.textContent = `${currentPlayer || "Player"} (${seconds}s)`;
|
|
turnState.textContent = isYourTurn
|
|
? `Your decision (${seconds}s)`
|
|
: `Waiting for other player (${seconds}s)`;
|
|
if (seconds <= 0) {
|
|
stopCountdown();
|
|
}
|
|
};
|
|
updateCountdown();
|
|
countdownTimer = setInterval(updateCountdown, 1000);
|
|
};
|
|
|
|
const connectSocket = () => {
|
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
return Promise.resolve(socket);
|
|
}
|
|
|
|
if (socketReady) {
|
|
return socketReady;
|
|
}
|
|
|
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
socket = new WebSocket(`${protocol}//${window.location.host}`);
|
|
|
|
socketReady = new Promise((resolve, reject) => {
|
|
socket.addEventListener("open", () => resolve(socket), {once: true});
|
|
socket.addEventListener("error", () => reject(new Error("WebSocket connection failed")), {once: true});
|
|
});
|
|
|
|
socket.addEventListener("message", (event) => {
|
|
const message = JSON.parse(event.data);
|
|
|
|
if (message.type === "poker:state") {
|
|
hideError();
|
|
isYourTurn = Boolean(message.isYourTurn);
|
|
canLeaveTable = Boolean(message.canLeaveTable);
|
|
isSpectator = Boolean(message.isSpectator);
|
|
currentStackValue = Number(message.yourStack ?? 0);
|
|
minimumStake = Number(message.minimumStake || 10);
|
|
callAmount = Number(message.callAmount || 0);
|
|
roundDisplay.textContent = message.round;
|
|
potDisplay.textContent = message.pot;
|
|
currentBet.textContent = message.currentBet ?? message.bet;
|
|
yourContribution.textContent = message.yourContribution || 0;
|
|
yourStack.textContent = currentStackValue;
|
|
streetDisplay.textContent = message.street || "Preflop";
|
|
playerName.textContent = message.username;
|
|
if (message.phase === "intermission" && message.nextRoundStartsAt) {
|
|
renderIntermissionCountdown(message.nextRoundStartsAt, message.lastAction);
|
|
} else if (message.phase === "betting" && message.turnEndsAt) {
|
|
renderTurnCountdown(message.turnEndsAt, message.currentPlayer);
|
|
lastAction.textContent = message.lastAction || "";
|
|
} else {
|
|
stopCountdown();
|
|
currentTurn.textContent = message.currentPlayer || "Round ended";
|
|
turnState.textContent = isSpectator
|
|
? "Spectating"
|
|
: isYourTurn
|
|
? "Your decision"
|
|
: message.phase === "betting" ? "Waiting for other player" : message.phase;
|
|
lastAction.textContent = message.lastAction || "";
|
|
}
|
|
renderCards(communityCards, message.communityCards || [], Math.max(3, message.communityCards?.length || 0), "Dealer Card");
|
|
renderCards(playerCards, message.hand || [], isSpectator ? 0 : 2, "Card");
|
|
spectatorNote.classList.toggle("hidden", !isSpectator);
|
|
renderQueue(message.queue || [], message.currentPlayer);
|
|
renderShowdown(message.showdown || [], message.winner);
|
|
updateActionButtons();
|
|
return;
|
|
}
|
|
|
|
if (message.type === "poker:action") {
|
|
return;
|
|
}
|
|
|
|
if (message.type === "lobby:left") {
|
|
localStorage.removeItem("activeLobbyId");
|
|
window.location.href = "/";
|
|
return;
|
|
}
|
|
|
|
if (message.type === "auth:error" || message.type === "poker:error" || message.type === "error") {
|
|
showError(message.message || "Poker connection failed");
|
|
}
|
|
});
|
|
|
|
socket.addEventListener("close", () => {
|
|
socket = null;
|
|
socketReady = null;
|
|
});
|
|
|
|
return socketReady;
|
|
};
|
|
|
|
const setBetValue = (value) => {
|
|
const nextValue = Math.max(Number(betRange.min), Math.min(Number(betRange.max), Number(value)));
|
|
betRange.value = nextValue;
|
|
betValue.textContent = nextValue;
|
|
};
|
|
|
|
const updateBetRange = () => {
|
|
const maxBet = Math.max(0, currentStackValue);
|
|
const requiredBet = Math.max(minimumStake, callAmount);
|
|
const minBet = maxBet > 0 ? Math.min(requiredBet, maxBet) : 0;
|
|
betRange.min = String(minBet);
|
|
betRange.max = String(maxBet);
|
|
betRange.step = String(minimumStake);
|
|
setBetValue(Math.max(minBet, Number(betRange.value) || minBet));
|
|
};
|
|
|
|
if (!lobbyId) {
|
|
showError("No active lobby found.");
|
|
isYourTurn = false;
|
|
updateActionButtons();
|
|
return;
|
|
}
|
|
|
|
updateActionButtons();
|
|
|
|
connectSocket()
|
|
.then(() => sendSocketMessage({type: "poker:join", lobbyId}))
|
|
.catch(() => showError("Could not connect to poker table."));
|
|
|
|
actionButtons.forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const action = button.dataset.action;
|
|
if (isSpectator) {
|
|
showError("You are out of chips and can only spectate.");
|
|
return;
|
|
}
|
|
if (!isYourTurn) {
|
|
showError("Wait until it is your turn.");
|
|
return;
|
|
}
|
|
if (action === "bet") {
|
|
updateBetRange();
|
|
betModal.classList.remove("hidden");
|
|
return;
|
|
}
|
|
|
|
sendSocketMessage({type: "poker:action", action, payload: {lobbyId}}).catch(() => {
|
|
showError("Could not send poker action.");
|
|
});
|
|
});
|
|
});
|
|
|
|
leaveTable.addEventListener("click", () => {
|
|
sendSocketMessage({type: "lobby:leave", lobbyId}).catch(() => {
|
|
showError("Could not leave table.");
|
|
});
|
|
});
|
|
|
|
closeBet.addEventListener("click", () => betModal.classList.add("hidden"));
|
|
betRange.addEventListener("input", () => setBetValue(betRange.value));
|
|
decreaseBet.addEventListener("click", () => setBetValue(Number(betRange.value) - 10));
|
|
increaseBet.addEventListener("click", () => setBetValue(Number(betRange.value) + 10));
|
|
confirmBet.addEventListener("click", () => {
|
|
if (isSpectator) {
|
|
showError("You are out of chips and can only spectate.");
|
|
betModal.classList.add("hidden");
|
|
return;
|
|
}
|
|
if (!isYourTurn) {
|
|
showError("Wait until it is your turn.");
|
|
betModal.classList.add("hidden");
|
|
return;
|
|
}
|
|
sendSocketMessage({
|
|
type: "poker:action",
|
|
action: "bet",
|
|
payload: {amount: Number(betRange.value), lobbyId}
|
|
}).catch(() => showError("Could not place bet."));
|
|
betModal.classList.add("hidden");
|
|
});
|
|
});
|