summaryrefslogtreecommitdiffstats
path: root/web/play/src/main.ts
diff options
context:
space:
mode:
Diffstat (limited to 'web/play/src/main.ts')
-rw-r--r--web/play/src/main.ts97
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");
17const gameGrid = byId("game-grid"); 17const gameGrid = byId("game-grid");
18const gameBoard = byId("game-board"); 18const gameBoard = byId("game-board");
19const gameSide = byId("game-side"); 19const gameSide = byId("game-side");
20const gameTitle = byId("game-title"); 20const versionPill = byId("version-pill");
21const gameBadge = byId("game-badge"); 21const accountNumber = byId("account-number");
22const lobbyAccount = byId("lobby-account"); 22const lobbyCount = byId("lobby-count");
23
23const historyBody = byId("history-body"); 24const historyBody = byId("history-body");
24const authForm = byId<HTMLFormElement>("auth-form"); 25const authForm = byId<HTMLFormElement>("auth-form");
25const authUser = byId<HTMLInputElement>("auth-user"); 26const authUser = byId<HTMLInputElement>("auth-user");
26const authPass = byId<HTMLInputElement>("auth-pass"); 27const authPass = byId<HTMLInputElement>("auth-pass");
27const authError = byId("auth-error"); 28const authError = byId("auth-error");
28const authSubmit = byId<HTMLButtonElement>("auth-submit"); 29const authSubmit = byId<HTMLButtonElement>("auth-submit");
29const tabLogin = byId<HTMLButtonElement>("tab-login"); 30const authSwitch = byId<HTMLButtonElement>("auth-switch");
30const tabRegister = byId<HTMLButtonElement>("tab-register"); 31const authTitle = byId("auth-title");
31const reconnectOverlay = byId("reconnect-overlay"); 32const reconnectOverlay = byId("reconnect-overlay");
32const reconnectMessage = byId("reconnect-message"); 33const reconnectMessage = byId("reconnect-message");
33const replacedOverlay = byId("replaced-overlay"); 34const replacedOverlay = byId("replaced-overlay");
@@ -39,6 +40,7 @@ let authMode: "login" | "register" = "login";
39let infoFor: string | null = null; 40let infoFor: string | null = null;
40let serverOffset = 0; 41let serverOffset = 0;
41let reconnectAttempts = 0; 42let reconnectAttempts = 0;
43let lastScreen: string | null = null;
42const bets = new Map<string, number>([ 44const 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
56const descriptions: Record<string, string> = { 58const 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
63const basePath = location.pathname.includes("/play/") 65const 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
167function setAuthMode(mode: "login" | "register"): void { 173function 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
234function 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
242function renderLobby(lobby: View): void { 239function 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 -------------------------------------------------------
284tabLogin.addEventListener("click", () => setAuthMode("login")); 265authSwitch.addEventListener("click", () =>
285tabRegister.addEventListener("click", () => setAuthMode("register")); 266 setAuthMode(authMode === "login" ? "register" : "login"),
267);
286 268
287authForm.addEventListener("submit", async (event) => { 269authForm.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 ----------------------------------------------------
313byId("logout").addEventListener("click", () => send("Logout")); 295byId("logout").addEventListener("click", () => send("Logout"));
314byId("show-history").addEventListener("click", () => send({ History: { limit: 50 } })); 296byId("show-history").addEventListener("click", () => send({ History: { limit: 50 } }));
315byId("refresh-lobby").addEventListener("click", () => send("Info"));
316byId("history-back").addEventListener("click", () => send("ShowLobby")); 297byId("history-back").addEventListener("click", () => send("ShowLobby"));
317byId("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});
325byId("reload-replaced").addEventListener("click", () => location.reload()); 298byId("reload-replaced").addEventListener("click", () => location.reload());
326byId("reload-reconnect").addEventListener("click", () => location.reload()); 299byId("reload-reconnect").addEventListener("click", () => location.reload());
327 300