import type { View } from "./types"; import { clear, money, node } from "./dom"; const REELS = 5; const ROWS = 3; const SYMBOLS = "CLOGDS"; const LINE_NAMES = ["top", "middle", "bottom", "V", "inverted V"]; let spinUntil = 0; let animation: number | undefined; let latest: View | null = null; let latestBoard: HTMLElement | null = null; export function requestSlotsSpin(): void { spinUntil = Date.now() + 900; } function spinning(view: View): boolean { return view.phase === "spinning" || Date.now() < spinUntil; } export function renderSlots( board: HTMLElement, side: HTMLElement, view: View, send: (command: unknown) => void, ): void { latest = view; latestBoard = board; if (animation !== undefined) { window.clearInterval(animation); animation = undefined; } const active = spinning(view); clear(board); clear(side); const title = node("h2", undefined, "Slots"); const note = node("p", "note", "Match three or more symbols from the left. Five paylines."); board.append(title, note); const reels = node("div", "reels"); const tiles: HTMLElement[] = []; const flat = String(view.reels ?? ""); for (let column = 0; column < REELS; column += 1) { const reel = node("div", "reel"); for (let row = 0; row < ROWS; row += 1) { const index = column * ROWS + row; const symbol = active ? SYMBOLS[(index * 3 + Math.floor(Date.now() / 70)) % SYMBOLS.length] : flat.length === REELS * ROWS ? flat[index] : flat.length === REELS ? flat[column] : SYMBOLS[(index * 5 + column) % SYMBOLS.length]; const tile = node("div", "tile", symbol); tile.classList.add(`sym-${symbol}`); if (active) tile.classList.add("spinning"); if (!active && (view.win_cells ?? []).includes(index)) tile.classList.add("win"); tiles.push(tile); reel.append(tile); } reels.append(reel); } board.append(reels); const lines = node("div", "win-lines"); if (!active && view.phase === "result") { const winnings = Number(view.winnings ?? 0); if (winnings > 0) { const names = (view.win_lines ?? []) .map((line: number) => `${line} (${LINE_NAMES[line - 1] ?? "line"})`) .join(" · "); lines.textContent = `${view.win_type === "mega" ? "MEGA WIN" : view.win_type === "big" ? "BIG WIN" : "WIN"} ${money(winnings)}${names ? ` · ${names}` : ""}`; lines.classList.add("win-text"); } else { lines.textContent = "No win — spin again."; } } else if (active) { lines.textContent = "Spinning…"; } else { lines.textContent = "Pick a bet and spin."; } board.append(lines); if (active) { let tick = 0; animation = window.setInterval(() => { tick += 1; for (let index = 0; index < tiles.length; index += 1) { const symbol = SYMBOLS[(index * 3 + tick) % SYMBOLS.length]; tiles[index].textContent = symbol; tiles[index].className = `tile spinning sym-${symbol}`; } if (Date.now() >= spinUntil && latest && latestBoard) { window.clearInterval(animation); animation = undefined; renderSlots(latestBoard, side, latest, send); } }, 70); } // Sidebar side.append(node("div", "label", "Slots")); side.append(node("p", "note", "5 reels × 3 rows, 5 paylines.")); const betRow = node("div", "row"); const minus = node("button", "btn small", "−10"); const bet = node("strong", "mono", money(Number(view.bet ?? 0))); const plus = node("button", "btn small", "+10"); minus.disabled = active; plus.disabled = active; minus.addEventListener("click", () => send({ AdjustBet: -10 })); plus.addEventListener("click", () => send({ AdjustBet: 10 })); betRow.append(minus, bet, plus); side.append(node("div", "label", "Bet"), betRow); if (view.error) { side.append(node("p", "error-text", String(view.error))); } const status = node("p", "note"); if (active) status.textContent = "Spinning…"; else if (view.phase === "result") { const winnings = Number(view.winnings ?? 0); status.textContent = winnings > 0 ? `Won ${money(winnings)}` : "No win this spin."; if (winnings > 0) status.classList.add("win-text"); } else status.textContent = "Ready when you are."; side.append(status); const actions = node("div", "actions"); const spin = node("button", "btn primary wide big", active ? "SPINNING…" : "SPIN"); spin.disabled = active; spin.addEventListener("click", () => { requestSlotsSpin(); send("Spin"); }); const leave = node("button", "btn ghost wide", "Leave machine"); leave.disabled = active; leave.addEventListener("click", () => send("Leave")); actions.append(spin, leave); side.append(actions); }