diff options
Diffstat (limited to 'web/play/src')
| -rw-r--r-- | web/play/src/blackjack.ts | 14 | ||||
| -rw-r--r-- | web/play/src/crash.ts | 14 | ||||
| -rw-r--r-- | web/play/src/main.ts | 6 | ||||
| -rw-r--r-- | web/play/src/roulette.ts | 15 | ||||
| -rw-r--r-- | web/play/src/slots.ts | 18 |
5 files changed, 59 insertions, 8 deletions
diff --git a/web/play/src/blackjack.ts b/web/play/src/blackjack.ts index 3537f45..49d46fa 100644 --- a/web/play/src/blackjack.ts +++ b/web/play/src/blackjack.ts | |||
| @@ -1,6 +1,14 @@ | |||
| 1 | import type { View } from "./types"; | 1 | import type { View } from "./types"; |
| 2 | import { cardFace, clear, money, node } from "./dom"; | 2 | import { cardFace, clear, money, node } from "./dom"; |
| 3 | 3 | ||
| 4 | let lastAction = 0; | ||
| 5 | |||
| 6 | function actionAllowed(): boolean { | ||
| 7 | if (Date.now() - lastAction < 400) return false; | ||
| 8 | lastAction = Date.now(); | ||
| 9 | return true; | ||
| 10 | } | ||
| 11 | |||
| 4 | function cardElement(card: string): HTMLElement { | 12 | function cardElement(card: string): HTMLElement { |
| 5 | if (card === "?") { | 13 | if (card === "?") { |
| 6 | const back = node("div", "card back"); | 14 | const back = node("div", "card back"); |
| @@ -124,6 +132,7 @@ export function renderBlackjack( | |||
| 124 | ? node("button", "btn primary", label) | 132 | ? node("button", "btn primary", label) |
| 125 | : node("button", "btn", label); | 133 | : node("button", "btn", label); |
| 126 | button.addEventListener("click", () => { | 134 | button.addEventListener("click", () => { |
| 135 | if (!actionAllowed()) return; | ||
| 127 | if (action === "hit") send("Hit"); | 136 | if (action === "hit") send("Hit"); |
| 128 | else if (action === "stand") send("Stand"); | 137 | else if (action === "stand") send("Stand"); |
| 129 | else send("Double"); | 138 | else send("Double"); |
| @@ -133,12 +142,15 @@ export function renderBlackjack( | |||
| 133 | actions.append(actionsRow); | 142 | actions.append(actionsRow); |
| 134 | const forfeit = node("button", "btn danger wide", "Forfeit hand"); | 143 | const forfeit = node("button", "btn danger wide", "Forfeit hand"); |
| 135 | forfeit.addEventListener("click", () => { | 144 | forfeit.addEventListener("click", () => { |
| 145 | if (!actionAllowed()) return; | ||
| 136 | if (window.confirm("Forfeit this hand? The reserved bet is lost.")) send("Forfeit"); | 146 | if (window.confirm("Forfeit this hand? The reserved bet is lost.")) send("Forfeit"); |
| 137 | }); | 147 | }); |
| 138 | actions.append(forfeit); | 148 | actions.append(forfeit); |
| 139 | } else if (canDeal) { | 149 | } else if (canDeal) { |
| 140 | const deal = node("button", "btn primary wide big", "DEAL"); | 150 | const deal = node("button", "btn primary wide big", "DEAL"); |
| 141 | deal.addEventListener("click", () => send("Deal")); | 151 | deal.addEventListener("click", () => { |
| 152 | if (actionAllowed()) send("Deal"); | ||
| 153 | }); | ||
| 142 | actions.append(deal); | 154 | actions.append(deal); |
| 143 | } | 155 | } |
| 144 | 156 | ||
diff --git a/web/play/src/crash.ts b/web/play/src/crash.ts index 6f3c8dd..a683dcd 100644 --- a/web/play/src/crash.ts +++ b/web/play/src/crash.ts | |||
| @@ -31,6 +31,7 @@ let sendCommand: ((command: unknown) => void) | null = null; | |||
| 31 | let frameHandle: number | undefined; | 31 | let frameHandle: number | undefined; |
| 32 | let verifyToken = 0; | 32 | let verifyToken = 0; |
| 33 | let lastStatus = ""; | 33 | let lastStatus = ""; |
| 34 | let lastAction = 0; | ||
| 34 | 35 | ||
| 35 | function build( | 36 | function build( |
| 36 | board: HTMLElement, | 37 | board: HTMLElement, |
| @@ -88,6 +89,8 @@ function build( | |||
| 88 | const primary = node("button", "btn primary wide big", "START") as HTMLButtonElement; | 89 | const primary = node("button", "btn primary wide big", "START") as HTMLButtonElement; |
| 89 | const leave = node("button", "btn ghost wide", "Leave table") as HTMLButtonElement; | 90 | const leave = node("button", "btn ghost wide", "Leave table") as HTMLButtonElement; |
| 90 | primary.addEventListener("click", () => { | 91 | primary.addEventListener("click", () => { |
| 92 | if (Date.now() - lastAction < 500) return; | ||
| 93 | lastAction = Date.now(); | ||
| 91 | if (current?.status === "running") sendCommand?.("CashOut"); | 94 | if (current?.status === "running") sendCommand?.("CashOut"); |
| 92 | else sendCommand?.("Start"); | 95 | else sendCommand?.("Start"); |
| 93 | }); | 96 | }); |
| @@ -118,6 +121,15 @@ function build( | |||
| 118 | }; | 121 | }; |
| 119 | } | 122 | } |
| 120 | 123 | ||
| 124 | /// Stop the chart loop when another screen takes over. | ||
| 125 | export function stopCrashAnimation(): void { | ||
| 126 | if (frameHandle !== undefined) { | ||
| 127 | cancelAnimationFrame(frameHandle); | ||
| 128 | frameHandle = undefined; | ||
| 129 | } | ||
| 130 | current = null; | ||
| 131 | } | ||
| 132 | |||
| 121 | export function renderCrash( | 133 | export function renderCrash( |
| 122 | board: HTMLElement, | 134 | board: HTMLElement, |
| 123 | side: HTMLElement, | 135 | side: HTMLElement, |
| @@ -125,7 +137,7 @@ export function renderCrash( | |||
| 125 | send: (command: unknown) => void, | 137 | send: (command: unknown) => void, |
| 126 | now: Clock, | 138 | now: Clock, |
| 127 | ): void { | 139 | ): void { |
| 128 | if (!parts || parts.board !== board) { | 140 | if (!parts || parts.board !== board || !parts.canvas.isConnected) { |
| 129 | parts = build(board, side, send, now); | 141 | parts = build(board, side, send, now); |
| 130 | lastStatus = ""; | 142 | lastStatus = ""; |
| 131 | } | 143 | } |
diff --git a/web/play/src/main.ts b/web/play/src/main.ts index fd55250..f051429 100644 --- a/web/play/src/main.ts +++ b/web/play/src/main.ts | |||
| @@ -1,10 +1,10 @@ | |||
| 1 | import { byId, money, node, sha256Hex, toast, toneFor } from "./dom"; | 1 | import { byId, money, node, sha256Hex, toast, toneFor } from "./dom"; |
| 2 | import { Gateway } from "./net"; | 2 | import { Gateway } from "./net"; |
| 3 | import { renderBlackjack } from "./blackjack"; | 3 | import { renderBlackjack } from "./blackjack"; |
| 4 | import { renderCrash } from "./crash"; | 4 | import { renderCrash, stopCrashAnimation } from "./crash"; |
| 5 | import { renderHistory } from "./history"; | 5 | import { renderHistory } from "./history"; |
| 6 | import { renderRoulette } from "./roulette"; | 6 | import { renderRoulette } from "./roulette"; |
| 7 | import { renderSlots } from "./slots"; | 7 | import { renderSlots, stopSlotsAnimation } from "./slots"; |
| 8 | import type { View } from "./types"; | 8 | import type { View } from "./types"; |
| 9 | 9 | ||
| 10 | const authScreen = byId("auth-screen"); | 10 | const authScreen = byId("auth-screen"); |
| @@ -72,6 +72,8 @@ const gateway = new Gateway(wsUrl, { | |||
| 72 | }, | 72 | }, |
| 73 | onView(next) { | 73 | onView(next) { |
| 74 | view = next; | 74 | view = next; |
| 75 | if (next.screen !== "slots") stopSlotsAnimation(); | ||
| 76 | if (next.screen !== "crash") stopCrashAnimation(); | ||
| 75 | // Game and history views carry no account field; the lobby is the source | 77 | // Game and history views carry no account field; the lobby is the source |
| 76 | // of truth for who is signed in. A null lobby user is a logout or an | 78 | // of truth for who is signed in. A null lobby user is a logout or an |
| 77 | // expired session. | 79 | // expired session. |
diff --git a/web/play/src/roulette.ts b/web/play/src/roulette.ts index cbb7c6e..a42686a 100644 --- a/web/play/src/roulette.ts +++ b/web/play/src/roulette.ts | |||
| @@ -20,6 +20,9 @@ type Hotspot = { | |||
| 20 | rank: number; | 20 | rank: number; |
| 21 | }; | 21 | }; |
| 22 | 22 | ||
| 23 | let observer: ResizeObserver | null = null; | ||
| 24 | let lastSpin = 0; | ||
| 25 | |||
| 23 | type Layout = { | 26 | type Layout = { |
| 24 | width: number; | 27 | width: number; |
| 25 | height: number; | 28 | height: number; |
| @@ -413,9 +416,11 @@ export function renderRoulette( | |||
| 413 | const zone = hit(event.clientX, event.clientY); | 416 | const zone = hit(event.clientX, event.clientY); |
| 414 | canvas.style.cursor = zone && !spinning ? "pointer" : "default"; | 417 | canvas.style.cursor = zone && !spinning ? "pointer" : "default"; |
| 415 | }); | 418 | }); |
| 416 | new ResizeObserver(() => { | 419 | observer?.disconnect(); |
| 420 | observer = new ResizeObserver(() => { | ||
| 417 | current = draw(canvas, view); | 421 | current = draw(canvas, view); |
| 418 | }).observe(canvas); | 422 | }); |
| 423 | observer.observe(canvas); | ||
| 419 | 424 | ||
| 420 | // Sidebar | 425 | // Sidebar |
| 421 | side.append(node("div", "label", "Roulette")); | 426 | side.append(node("div", "label", "Roulette")); |
| @@ -459,7 +464,11 @@ export function renderRoulette( | |||
| 459 | const actions = node("div", "actions"); | 464 | const actions = node("div", "actions"); |
| 460 | const spin = node("button", "btn primary wide big", spinning ? "SPINNING…" : "SPIN WHEEL"); | 465 | const spin = node("button", "btn primary wide big", spinning ? "SPINNING…" : "SPIN WHEEL"); |
| 461 | spin.disabled = spinning || total === 0; | 466 | spin.disabled = spinning || total === 0; |
| 462 | spin.addEventListener("click", () => send("Spin")); | 467 | spin.addEventListener("click", () => { |
| 468 | if (Date.now() - lastSpin < 600) return; | ||
| 469 | lastSpin = Date.now(); | ||
| 470 | send("Spin"); | ||
| 471 | }); | ||
| 463 | const clearBets = node("button", "btn wide", "Clear bets"); | 472 | const clearBets = node("button", "btn wide", "Clear bets"); |
| 464 | clearBets.disabled = spinning || total === 0; | 473 | clearBets.disabled = spinning || total === 0; |
| 465 | clearBets.addEventListener("click", () => send("ClearBets")); | 474 | clearBets.addEventListener("click", () => send("ClearBets")); |
diff --git a/web/play/src/slots.ts b/web/play/src/slots.ts index dba1e25..7c4677d 100644 --- a/web/play/src/slots.ts +++ b/web/play/src/slots.ts | |||
| @@ -7,6 +7,7 @@ const SYMBOLS = "CLOGDS"; | |||
| 7 | const LINE_NAMES = ["top", "middle", "bottom", "V", "inverted V"]; | 7 | const LINE_NAMES = ["top", "middle", "bottom", "V", "inverted V"]; |
| 8 | 8 | ||
| 9 | let spinUntil = 0; | 9 | let spinUntil = 0; |
| 10 | let lastSpin = 0; | ||
| 10 | let animation: number | undefined; | 11 | let animation: number | undefined; |
| 11 | let latest: View | null = null; | 12 | let latest: View | null = null; |
| 12 | let latestBoard: HTMLElement | null = null; | 13 | let latestBoard: HTMLElement | null = null; |
| @@ -15,6 +16,17 @@ export function requestSlotsSpin(): void { | |||
| 15 | spinUntil = Date.now() + 900; | 16 | spinUntil = Date.now() + 900; |
| 16 | } | 17 | } |
| 17 | 18 | ||
| 19 | /// Stop the reel animation when another screen takes over. | ||
| 20 | export function stopSlotsAnimation(): void { | ||
| 21 | if (animation !== undefined) { | ||
| 22 | window.clearInterval(animation); | ||
| 23 | animation = undefined; | ||
| 24 | } | ||
| 25 | latest = null; | ||
| 26 | latestBoard = null; | ||
| 27 | spinUntil = 0; | ||
| 28 | } | ||
| 29 | |||
| 18 | function spinning(view: View): boolean { | 30 | function spinning(view: View): boolean { |
| 19 | return view.phase === "spinning" || Date.now() < spinUntil; | 31 | return view.phase === "spinning" || Date.now() < spinUntil; |
| 20 | } | 32 | } |
| @@ -42,7 +54,9 @@ export function renderSlots( | |||
| 42 | 54 | ||
| 43 | const reels = node("div", "reels"); | 55 | const reels = node("div", "reels"); |
| 44 | const tiles: HTMLElement[] = []; | 56 | const tiles: HTMLElement[] = []; |
| 45 | const flat = String(view.reels ?? ""); | 57 | const flat = Array.isArray(view.reels) |
| 58 | ? view.reels.join("") | ||
| 59 | : String(view.reels ?? ""); | ||
| 46 | for (let column = 0; column < REELS; column += 1) { | 60 | for (let column = 0; column < REELS; column += 1) { |
| 47 | const reel = node("div", "reel"); | 61 | const reel = node("div", "reel"); |
| 48 | for (let row = 0; row < ROWS; row += 1) { | 62 | for (let row = 0; row < ROWS; row += 1) { |
| @@ -133,6 +147,8 @@ export function renderSlots( | |||
| 133 | const spin = node("button", "btn primary wide big", active ? "SPINNING…" : "SPIN"); | 147 | const spin = node("button", "btn primary wide big", active ? "SPINNING…" : "SPIN"); |
| 134 | spin.disabled = active; | 148 | spin.disabled = active; |
| 135 | spin.addEventListener("click", () => { | 149 | spin.addEventListener("click", () => { |
| 150 | if (Date.now() - lastSpin < 900) return; | ||
| 151 | lastSpin = Date.now(); | ||
| 136 | requestSlotsSpin(); | 152 | requestSlotsSpin(); |
| 137 | send("Spin"); | 153 | send("Spin"); |
| 138 | }); | 154 | }); |
