1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
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 lastSpin = 0;
let animation: number | undefined;
let latest: View | null = null;
let latestBoard: HTMLElement | null = null;
export function requestSlotsSpin(): void {
spinUntil = Date.now() + 900;
}
/// Stop the reel animation when another screen takes over.
export function stopSlotsAnimation(): void {
if (animation !== undefined) {
window.clearInterval(animation);
animation = undefined;
}
latest = null;
latestBoard = null;
spinUntil = 0;
}
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 (view.error) spinUntil = 0;
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 = Array.isArray(view.reels)
? view.reels.join("")
: 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", () => {
if (Date.now() - lastSpin < 900) return;
lastSpin = Date.now();
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);
}
|