diff options
Diffstat (limited to 'web/play/src/main.ts')
| -rw-r--r-- | web/play/src/main.ts | 97 |
1 files changed, 35 insertions, 62 deletions
diff --git a/web/play/src/main.ts b/web/play/src/main.ts index 690d827..2ca4ab5 100644 --- a/web/play/src/main.ts +++ b/web/play/src/main.ts | |||
| @@ -17,17 +17,18 @@ const accountBalance = byId("account-balance"); | |||
| 17 | const gameGrid = byId("game-grid"); | 17 | const gameGrid = byId("game-grid"); |
| 18 | const gameBoard = byId("game-board"); | 18 | const gameBoard = byId("game-board"); |
| 19 | const gameSide = byId("game-side"); | 19 | const gameSide = byId("game-side"); |
| 20 | const gameTitle = byId("game-title"); | 20 | const versionPill = byId("version-pill"); |
| 21 | const gameBadge = byId("game-badge"); | 21 | const accountNumber = byId("account-number"); |
| 22 | const lobbyAccount = byId("lobby-account"); | 22 | const lobbyCount = byId("lobby-count"); |
| 23 | |||
| 23 | const historyBody = byId("history-body"); | 24 | const historyBody = byId("history-body"); |
| 24 | const authForm = byId<HTMLFormElement>("auth-form"); | 25 | const authForm = byId<HTMLFormElement>("auth-form"); |
| 25 | const authUser = byId<HTMLInputElement>("auth-user"); | 26 | const authUser = byId<HTMLInputElement>("auth-user"); |
| 26 | const authPass = byId<HTMLInputElement>("auth-pass"); | 27 | const authPass = byId<HTMLInputElement>("auth-pass"); |
| 27 | const authError = byId("auth-error"); | 28 | const authError = byId("auth-error"); |
| 28 | const authSubmit = byId<HTMLButtonElement>("auth-submit"); | 29 | const authSubmit = byId<HTMLButtonElement>("auth-submit"); |
| 29 | const tabLogin = byId<HTMLButtonElement>("tab-login"); | 30 | const authSwitch = byId<HTMLButtonElement>("auth-switch"); |
| 30 | const tabRegister = byId<HTMLButtonElement>("tab-register"); | 31 | const authTitle = byId("auth-title"); |
| 31 | const reconnectOverlay = byId("reconnect-overlay"); | 32 | const reconnectOverlay = byId("reconnect-overlay"); |
| 32 | const reconnectMessage = byId("reconnect-message"); | 33 | const reconnectMessage = byId("reconnect-message"); |
| 33 | const replacedOverlay = byId("replaced-overlay"); | 34 | const replacedOverlay = byId("replaced-overlay"); |
| @@ -39,6 +40,7 @@ let authMode: "login" | "register" = "login"; | |||
| 39 | let infoFor: string | null = null; | 40 | let infoFor: string | null = null; |
| 40 | let serverOffset = 0; | 41 | let serverOffset = 0; |
| 41 | let reconnectAttempts = 0; | 42 | let reconnectAttempts = 0; |
| 43 | let lastScreen: string | null = null; | ||
| 42 | const bets = new Map<string, number>([ | 44 | const bets = new Map<string, number>([ |
| 43 | ["slots", 10], | 45 | ["slots", 10], |
| 44 | ["roulette", 10], | 46 | ["roulette", 10], |
| @@ -54,10 +56,10 @@ const titles: Record<string, string> = { | |||
| 54 | }; | 56 | }; |
| 55 | 57 | ||
| 56 | const descriptions: Record<string, string> = { | 58 | const descriptions: Record<string, string> = { |
| 57 | slots: "Five reels, five paylines. Match symbols from the left.", | 59 | slots: "Spin the reels. Three or more matching symbols from the left win.", |
| 58 | roulette: "European single-zero wheel with inside and outside bets.", | 60 | roulette: "Put chips on the table, then spin. Straight numbers pay the most.", |
| 59 | blackjack: "Six-deck shoe. Blackjack pays 3:2.", | 61 | blackjack: "Get closer to 21 than the dealer without going over.", |
| 60 | crash: "A rising multiplier. Cash out before it crashes.", | 62 | crash: "Start the round, then cash out before the multiplier crashes.", |
| 61 | }; | 63 | }; |
| 62 | 64 | ||
| 63 | const basePath = location.pathname.includes("/play/") | 65 | const basePath = location.pathname.includes("/play/") |
| @@ -83,9 +85,13 @@ const gateway = new Gateway(wsUrl, { | |||
| 83 | serverOffset = hello.time - Date.now(); | 85 | serverOffset = hello.time - Date.now(); |
| 84 | currentUser = hello.user ?? null; | 86 | currentUser = hello.user ?? null; |
| 85 | if (typeof hello.balance === "number") lastBalance = hello.balance; | 87 | if (typeof hello.balance === "number") lastBalance = hello.balance; |
| 86 | gameBadge.textContent = `v${hello.version}`; | 88 | versionPill.textContent = `v${hello.version}`; |
| 87 | }, | 89 | }, |
| 88 | onView(next) { | 90 | onView(next) { |
| 91 | if (next.screen !== lastScreen) { | ||
| 92 | byId("toasts").replaceChildren(); | ||
| 93 | lastScreen = next.screen; | ||
| 94 | } | ||
| 89 | view = next; | 95 | view = next; |
| 90 | if (next.screen !== "slots") stopSlotsAnimation(); | 96 | if (next.screen !== "slots") stopSlotsAnimation(); |
| 91 | if (next.screen !== "crash") stopCrashAnimation(); | 97 | if (next.screen !== "crash") stopCrashAnimation(); |
| @@ -124,12 +130,12 @@ const gateway = new Gateway(wsUrl, { | |||
| 124 | authUser.focus(); | 130 | authUser.focus(); |
| 125 | } | 131 | } |
| 126 | authSubmit.disabled = false; | 132 | authSubmit.disabled = false; |
| 127 | authSubmit.textContent = authMode === "login" ? "Log in" : "Create account"; | 133 | authSubmit.textContent = authMode === "login" ? "LOG IN" : "CREATE ACCOUNT"; |
| 128 | }, | 134 | }, |
| 129 | onError(text) { | 135 | onError(text) { |
| 130 | toast(text, "bad"); | 136 | toast(text, "bad"); |
| 131 | authSubmit.disabled = false; | 137 | authSubmit.disabled = false; |
| 132 | authSubmit.textContent = authMode === "login" ? "Log in" : "Create account"; | 138 | authSubmit.textContent = authMode === "login" ? "LOG IN" : "CREATE ACCOUNT"; |
| 133 | }, | 139 | }, |
| 134 | onReplaced() { | 140 | onReplaced() { |
| 135 | replacedOverlay.hidden = false; | 141 | replacedOverlay.hidden = false; |
| @@ -166,9 +172,9 @@ function serverNow(): number { | |||
| 166 | 172 | ||
| 167 | function setAuthMode(mode: "login" | "register"): void { | 173 | function setAuthMode(mode: "login" | "register"): void { |
| 168 | authMode = mode; | 174 | authMode = mode; |
| 169 | tabLogin.classList.toggle("active", mode === "login"); | 175 | authTitle.textContent = mode === "login" ? "Sign in" : "Create account"; |
| 170 | tabRegister.classList.toggle("active", mode === "register"); | 176 | authSubmit.textContent = mode === "login" ? "LOG IN" : "CREATE ACCOUNT"; |
| 171 | authSubmit.textContent = mode === "login" ? "Log in" : "Create account"; | 177 | authSwitch.textContent = mode === "login" ? "CREATE ACCOUNT" : "BACK TO SIGN IN"; |
| 172 | authPass.autocomplete = mode === "login" ? "current-password" : "new-password"; | 178 | authPass.autocomplete = mode === "login" ? "current-password" : "new-password"; |
| 173 | authError.textContent = ""; | 179 | authError.textContent = ""; |
| 174 | } | 180 | } |
| @@ -192,6 +198,7 @@ function render(): void { | |||
| 192 | accountName.textContent = String(user); | 198 | accountName.textContent = String(user); |
| 193 | } else { | 199 | } else { |
| 194 | sessionBar.hidden = true; | 200 | sessionBar.hidden = true; |
| 201 | accountNumber.textContent = "—"; | ||
| 195 | } | 202 | } |
| 196 | 203 | ||
| 197 | setBalance(lastBalance); | 204 | setBalance(lastBalance); |
| @@ -219,8 +226,6 @@ function render(): void { | |||
| 219 | case "blackjack": | 226 | case "blackjack": |
| 220 | case "crash": | 227 | case "crash": |
| 221 | show("game"); | 228 | show("game"); |
| 222 | gameTitle.textContent = titles[view.screen] ?? view.screen; | ||
| 223 | gameBadge.textContent = badge(view); | ||
| 224 | if (view.screen === "slots") renderSlots(gameBoard, gameSide, view, send); | 229 | if (view.screen === "slots") renderSlots(gameBoard, gameSide, view, send); |
| 225 | else if (view.screen === "roulette") renderRoulette(gameBoard, gameSide, view, send); | 230 | else if (view.screen === "roulette") renderRoulette(gameBoard, gameSide, view, send); |
| 226 | else if (view.screen === "blackjack") renderBlackjack(gameBoard, gameSide, view, send); | 231 | else if (view.screen === "blackjack") renderBlackjack(gameBoard, gameSide, view, send); |
| @@ -231,47 +236,23 @@ function render(): void { | |||
| 231 | } | 236 | } |
| 232 | } | 237 | } |
| 233 | 238 | ||
| 234 | function badge(view: View): string { | ||
| 235 | if (view.screen === "crash") return String(view.status ?? ""); | ||
| 236 | if (view.screen === "blackjack") return String(view.status ?? ""); | ||
| 237 | if (view.screen === "slots") return String(view.phase ?? ""); | ||
| 238 | if (view.screen === "roulette") return String(view.phase ?? ""); | ||
| 239 | return ""; | ||
| 240 | } | ||
| 241 | |||
| 242 | function renderLobby(lobby: View): void { | 239 | function renderLobby(lobby: View): void { |
| 243 | lobbyAccount.textContent = lobby.account ? `Account ${lobby.account}` : ""; | 240 | accountNumber.textContent = lobby.account ? String(lobby.account) : "—"; |
| 241 | const order = ["slots", "roulette", "blackjack", "crash"]; | ||
| 242 | const list: [string, string][] = [...(lobby.games ?? [])].sort( | ||
| 243 | (a, b) => order.indexOf(a[0]) - order.indexOf(b[0]), | ||
| 244 | ); | ||
| 245 | lobbyCount.textContent = `${list.length} ${list.length === 1 ? "game" : "games"}`; | ||
| 244 | gameGrid.replaceChildren(); | 246 | gameGrid.replaceChildren(); |
| 245 | const list: [string, string][] = lobby.games ?? []; | ||
| 246 | for (const [id, name] of list) { | 247 | for (const [id, name] of list) { |
| 247 | const card = node("article", `game-card ${id}`); | 248 | const card = node("article", `game-card ${id}`); |
| 248 | card.append(node("h3", undefined, name || titles[id] || id)); | 249 | card.append(node("h3", undefined, name || titles[id] || id)); |
| 249 | card.append(node("p", undefined, descriptions[id] ?? "Play a round.")); | 250 | card.append(node("p", undefined, descriptions[id] ?? "Play a round.")); |
| 250 | 251 | const play = node("button", "btn primary wide", "PLAY"); | |
| 251 | const row = node("div", "row wrap"); | ||
| 252 | const label = node("label", "field"); | ||
| 253 | label.style.flex = "1"; | ||
| 254 | label.append(node("span", "label", "Bet")); | ||
| 255 | const input = node("input", "input bet-input") as HTMLInputElement; | ||
| 256 | input.type = "number"; | ||
| 257 | input.min = "1"; | ||
| 258 | input.step = "10"; | ||
| 259 | input.value = String(bets.get(id) ?? 10); | ||
| 260 | input.addEventListener("change", () => { | ||
| 261 | const value = Math.max(1, Math.floor(Number(input.value) || 1)); | ||
| 262 | input.value = String(value); | ||
| 263 | bets.set(id, value); | ||
| 264 | }); | ||
| 265 | label.append(input); | ||
| 266 | |||
| 267 | const play = node("button", "btn primary", id === "roulette" ? "Open table" : "Play"); | ||
| 268 | play.addEventListener("click", () => { | 252 | play.addEventListener("click", () => { |
| 269 | const bet = Math.max(1, Math.floor(Number(input.value) || 1)); | 253 | send({ Play: { game: id, bet: bets.get(id) ?? 10 } }); |
| 270 | bets.set(id, bet); | ||
| 271 | send({ Play: { game: id, bet } }); | ||
| 272 | }); | 254 | }); |
| 273 | row.append(label, play); | 255 | card.append(play); |
| 274 | card.append(row); | ||
| 275 | gameGrid.append(card); | 256 | gameGrid.append(card); |
| 276 | } | 257 | } |
| 277 | 258 | ||
| @@ -280,9 +261,10 @@ function renderLobby(lobby: View): void { | |||
| 280 | } | 261 | } |
| 281 | } | 262 | } |
| 282 | 263 | ||
| 283 | // Auth tabs and form ------------------------------------------------------- | 264 | // Auth mode and form ------------------------------------------------------- |
| 284 | tabLogin.addEventListener("click", () => setAuthMode("login")); | 265 | authSwitch.addEventListener("click", () => |
| 285 | tabRegister.addEventListener("click", () => setAuthMode("register")); | 266 | setAuthMode(authMode === "login" ? "register" : "login"), |
| 267 | ); | ||
| 286 | 268 | ||
| 287 | authForm.addEventListener("submit", async (event) => { | 269 | authForm.addEventListener("submit", async (event) => { |
| 288 | event.preventDefault(); | 270 | event.preventDefault(); |
| @@ -312,16 +294,7 @@ authForm.addEventListener("submit", async (event) => { | |||
| 312 | // Chrome and navigation ---------------------------------------------------- | 294 | // Chrome and navigation ---------------------------------------------------- |
| 313 | byId("logout").addEventListener("click", () => send("Logout")); | 295 | byId("logout").addEventListener("click", () => send("Logout")); |
| 314 | byId("show-history").addEventListener("click", () => send({ History: { limit: 50 } })); | 296 | byId("show-history").addEventListener("click", () => send({ History: { limit: 50 } })); |
| 315 | byId("refresh-lobby").addEventListener("click", () => send("Info")); | ||
| 316 | byId("history-back").addEventListener("click", () => send("ShowLobby")); | 297 | byId("history-back").addEventListener("click", () => send("ShowLobby")); |
| 317 | byId("leave-game").addEventListener("click", () => { | ||
| 318 | if (!view) return; | ||
| 319 | const live = | ||
| 320 | (view.screen === "blackjack" && view.status === "player") || | ||
| 321 | (view.screen === "crash" && view.status === "running"); | ||
| 322 | if (live && !window.confirm("Leaving now forfeits the active bet. Continue?")) return; | ||
| 323 | send("Leave"); | ||
| 324 | }); | ||
| 325 | byId("reload-replaced").addEventListener("click", () => location.reload()); | 298 | byId("reload-replaced").addEventListener("click", () => location.reload()); |
| 326 | byId("reload-reconnect").addEventListener("click", () => location.reload()); | 299 | byId("reload-reconnect").addEventListener("click", () => location.reload()); |
| 327 | 300 | ||
