// Stoopshake: free 3D street dice (classic Cee-lo, 4-5-6) on a night-time stoop. Real-physics dice (dice3d.js), the rules // engine (rules.js), an original soundtrack (audio.js). Modes: Quick 1v1, Table (up to 6, pot), Banker, the Daily Table, // pass-and-play, async challenge links (state in the URL, no server), Crew tables (Crew Pass), Telegram Mini App + group // chats (/dice on the bot, rolled with Telegram's own animated dice). Free play chips only: daily top-up, earned by playing, // never bought, sold or cashed out; no prizes. Paid = looks and crew features only, never odds. (c) 2026 CyberMax. import { count, store, toast, share, download } from './cmx.js'; import * as FP from './funpass.js'; import * as pack from './pack.js'; import * as TGM from './tg.js'; import * as R from './rules.js'; import * as A from './audio.js'; const GAME = 'stoopshake'; const SITE = 'https://cybermaxtools.com'; const PATH = '/games/stoopshake/'; const DATA = 'https://data.cybermaxtools.com'; const BOT_LINK = 'https://t.me/CyberMaxTools_bot'; const $ = (id) => document.getElementById(id); const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); const sleep = (ms) => new Promise((r) => setTimeout(r, ms * (CAP ? 0 : FAST ? 0.15 : 1))); const Q = new URLSearchParams(location.search); const CAP = Q.has('cap'); // trailer / screenshots: frame-stepped from outside (window.__cap) const FAST = Q.has('fast'); // tests: shorter pauses const TODAY = R.dayNumber(); const dz = (d) => (d || []).map(R.face).join(''); // ------------------------------------------------------------------ paid extras (cosmetic / convenience only) pack.capture({ game: GAME, name: 'Stoopshake', title: 'Dice & Table Pack', skins: 4, price: '$1.99', preview: ['#D9DDE3', '#E6B54A', '#E4D6B8', '#3CFFE0'], perks: ['4 dice: Chrome, Gold, Bone and Neon', '3 tables: Brownstone Stoop, Rooftop and Arcade', '5 win emotes for your big rolls'] }); const CREW_PERKS = ['Crew tables: save your crew (up to 6) and seat them in one tap', 'Your crew name and colours on the dice and every share card', 'Stats history: every round, win rate, best rolls', '2 more tables: Night Court and Boardwalk']; // Named exactly as the Stripe product (prod "Stoopshake Crew Pass: crew tables, colours and stats"); the Crew Pass also // turns on every Dice & Table Pack look (ownPack() below), so the sheet says so. Every mode stays free (17 Oct 2026). FP.init({ game: GAME, name: 'Stoopshake', title: 'Stoopshake Crew Pass', short: 'Crew Pass', exact: true, free: 'Every mode, the Daily Table and the daily free chips stay free for everyone.', perks: [...CREW_PERKS, 'Also every Dice & Table Pack look: 4 dice, 3 tables, 5 emotes'], extra: 'The Crew Pass opens crew tables, your crew name and colours, stats history and two more tables. Same dice odds for everyone.' }); const ownPack = () => pack.owned() || FP.isPro(); const ownCrew = () => FP.isPro(); const EMOTES = [{ id: 'none', t: '' }, { id: 'mic', t: 'π€ MIC DROP' }, { id: 'ice', t: 'π§ ICE COLD' }, { id: 'easy', t: 'π€ TOO EASY' }, { id: 'crown', t: 'π RUN IT BACK' }, { id: 'dance', t: 'πΊ DANCE BREAK' }]; // ------------------------------------------------------------------ saved on this device only const DEF = { name: '', wallet: null, rules: false, skin: 'classic', theme: 'corner', emote: 'none', daily: {}, hist: [], crew: { name: 'MY CREW', a: '#E23B5A', b: '#FFFFFF', members: [] }, board: false, dev: '', locals: [] }; const S = Object.assign({}, DEF, store('ssk:s') || {}); if (!S.dev) S.dev = Math.random().toString(36).slice(2, 12); if (!S.name) S.name = ['ACE', 'BLAZE', 'DOT', 'KASH', 'NOVA', 'RICO', 'JAY', 'MIKA', 'ZEE', 'LUX'][Math.floor(Math.random() * 10)] + (10 + Math.floor(Math.random() * 89)); const save = () => store('ssk:s', S); let gift = 0; ({ wallet: S.wallet, gift } = R.dailyRefill(S.wallet, TODAY)); save(); // ------------------------------------------------------------------ challenge link from a friend (?c=...) let chal = Q.get('c') ? R.decodeChallenge(Q.get('c')) : null; // ------------------------------------------------------------------ 3D boot let table = null, DT = null; const cv = $('c'); async function boot() { const ok = (() => { try { return !!document.createElement('canvas').getContext('webgl2'); } catch { return false; } })(); if (!ok) { $('loading').hidden = true; $('nogl').hidden = false; count(GAME, 'nogl'); return; } $('lbar').style.width = '35%'; DT = await import('./dice3d.js'); $('lbar').style.width = '75%'; try { await document.fonts.load('800 20px "Barlow Condensed"'); } catch { /* */ } table = new DT.DiceTable(cv, { theme: S.theme, skin: skinFor(), record: CAP, onImpact: (k, v) => A.hit(k, v), onSlow: () => { A.whoosh(0.3); $('app').classList.add('slow'); } }); if (S.skin === 'crew') table.setSkin('crew', S.crew); if (FAST) { table.speed = 3; table.stage.setQuality(0.5); table.stage.q.bloom = false; table.r.shadowMap.enabled = false; } // tests on software GL: same physics, faster clock, cheap frames addEventListener('resize', () => table.resize()); table.resize(); // compile every shader behind the loading screen (parallel where the GPU allows), so the first frame doesn't stall $('lbar').style.width = '90%'; const tc = performance.now(); try { table.stage.setCam(table.camPos, table.camLook); await table.r.compileAsync(table.scene, table.stage.cam); } catch { /* older three: compiles on first frame */ } table.update(1 / 60); // first frame (post-processing programs) window.__sskCompile = Math.round(performance.now() - tc); $('loading').hidden = true; count(GAME, 'view', TGM.app() ? 'telegram' : chal ? 'challenge' : ''); if (!CAP) requestAnimationFrame(loop); window.__ssk = { ready: true, readyAt: Math.round(performance.now()), rolls: 0, state: () => ({ mode: M && M.mode, turn: turnSeat && turnSeat.name, chips: S.wallet.chips, waiting: !!waiting }), table }; renderTop(); if (gift > 0 && S.wallet.played > 0) setTimeout(() => toast(`Daily top-up: +${gift} free chips. Come back tomorrow for more.`, 3500), 600); if (chal) home(); else if (!S.rules) { $('rules').hidden = false; } else home(); } const skinFor = () => { const sk = DT.SKINS[S.skin]; if (!sk || (sk.pack === 'skins' && !ownPack()) || (sk.pack === 'crew' && !ownCrew())) return 'classic'; return S.skin; }; const themeFor = () => { const th = DT.THEMES[S.theme]; if (!th || (th.pack === 'skins' && !ownPack()) || (th.pack === 'crew' && !ownCrew())) return 'corner'; return S.theme; }; let last = performance.now(); function loop(now) { const dt = Math.min(0.05, (now - last) / 1000); last = now; if (holding && table.state === 'hand') { table.shake(holdX, holdK); A.rattle(holdK); holdK = Math.max(0.35, holdK * 0.96); } table.update(dt); rec.frame(); if (table.state !== 'rolling') $('app').classList.remove('slow'); requestAnimationFrame(loop); } // frame-stepped capture for the trailer and store shots (no wall clock) window.__cap = { step: (dt = 1 / 30, draw = true) => { if (!table) return; table.noDraw = !draw; table.update(dt); table.noDraw = false; }, ready: () => !!table, // trailer + store shots: drive the real scene and the real HUD from outside (tests/trailer.mjs) hideAll: () => { hideAll(); actions([]); }, seats: (list, turn) => { M = { mode: 'cap', seats: list.map((x) => ({ ...x, last: x.last || null, sc: x.last ? R.score(x.last) : null })), chipsShown: !!list[0].chips, potLabel: () => list.pot || '' }; turnSeat = M.seats[turn ?? -1] || null; renderSeats(); }, pot: (t) => { $('pot').textContent = t; }, banner: (kind, text, sub, emote) => { const b = $('banner'); b.className = `banner k${kind}`; b.innerHTML = `${esc(text)}${sub ? `${esc(sub)}` : ''}${emote ? `${esc(emote)}` : ''}`; b.classList.add('show'); }, unbanner: () => $('banner').classList.remove('show'), talk: (who, text) => { const t = $('talk'); t.innerHTML = `${esc(who)}: ${esc(text)}`; t.classList.add('show'); }, untalk: () => $('talk').classList.remove('show'), hint: (t) => { $('hint').textContent = t || ''; $('hint').classList.toggle('hide', !t); }, roll: (on) => { $('roll').disabled = !on; }, hud: (on) => { $('hud').style.display = on ? '' : 'none'; }, theme: (id) => { table.setTheme(id); table.hand(0); }, skin: (id) => table.setSkin(id, S.crew), find: (want, from = 1, n = 400, power = 0.62) => { for (let s = from; s < from + n; s++) { const r = table.simulate({ seed: s, power }); if (!r.off && !r.cocked && R.score(r.dice).kind === want) return { seed: s, dice: r.dice }; } return null; }, throwSeed: (seed, power = 0.62) => { table.hand(0); table.throw({ seed, power }); }, state: () => table.state, celebrate: (k) => table.celebrate(k), shake: (x, k) => table.shake(x, k), hand: (x) => table.hand(x || 0), read: () => table.read().map((r) => r.val), score: () => R.score(table.read().map((r) => r.val)), }; // ------------------------------------------------------------------ input: hold to shake, flick to throw let waiting = null, holding = false, holdX = 0, holdK = 0, trail = []; const toX = (clientX) => { const r = cv.getBoundingClientRect(); return ((clientX - r.left) / r.width - 0.5) * 8; }; cv.addEventListener('pointerdown', (e) => { A.ensure(); A.startMusic(); if (!waiting || table.state !== 'hand') return; holding = true; holdX = toX(e.clientX); holdK = 1; trail = [{ x: e.clientX, y: e.clientY, t: performance.now() }]; try { cv.setPointerCapture(e.pointerId); } catch { /* */ } $('hint').classList.add('hide'); }); cv.addEventListener('pointermove', (e) => { if (!holding) return; const p = { x: e.clientX, y: e.clientY, t: performance.now() }; const q = trail[trail.length - 1]; holdX = toX(e.clientX); holdK = Math.min(1, holdK + Math.hypot(p.x - q.x, p.y - q.y) / 60); trail.push(p); if (trail.length > 30) trail.shift(); }); const endHold = (e) => { if (!holding) return; holding = false; const now = performance.now(); const pts = trail.filter((p) => now - p.t < 110); const a = pts[0] || trail[0], b = trail[trail.length - 1]; const dt = Math.max(16, b.t - a.t), vx = (b.x - a.x) / dt, vy = (b.y - a.y) / dt; // px per ms const r = cv.getBoundingClientRect(), k = 900 / Math.max(320, r.height); const flick = -vy * k; const power = flick > 0.25 ? Math.min(1, 0.3 + flick * 0.42) : 0.55; // a tap or a sideways drag = a steady toss doThrow({ x: Math.max(-1, Math.min(1, vx * k * 0.6)), power }); }; cv.addEventListener('pointerup', endHold); cv.addEventListener('pointercancel', endHold); addEventListener('keydown', (e) => { if ((e.key === ' ' || e.key === 'Enter') && waiting && document.activeElement === cv) { e.preventDefault(); doThrow({ power: 0.6 + Math.random() * 0.3, x: (Math.random() - 0.5) * 0.4 }); } }); $('roll').onclick = () => { A.ensure(); A.startMusic(); if (waiting) doThrow({ power: 0.55 + Math.random() * 0.4, x: (Math.random() - 0.5) * 0.5 }); }; function doThrow(o) { if (!waiting || table.state !== 'hand') return; if (window.__ssk && !window.__ssk.firstThrow) window.__ssk.firstThrow = Math.round(performance.now()); const w = waiting; waiting = null; $('roll').disabled = true; A.whoosh(o.power); TGM.haptic('medium'); w(o); } // ------------------------------------------------------------------ clip recorder (on this device only, watermark link) const rec = (() => { let mr = null, chunks = [], c2 = null, g = null, type = '', on = false; const ok = () => !CAP && window.MediaRecorder && HTMLCanvasElement.prototype.captureStream; return { start() { if (!ok() || on) return; type = ['video/mp4;codecs=avc1', 'video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm'].find((t) => MediaRecorder.isTypeSupported && MediaRecorder.isTypeSupported(t)); if (!type) return; const w = 720, h = Math.round(720 * cv.clientHeight / Math.max(1, cv.clientWidth)); c2 = c2 || document.createElement('canvas'); c2.width = w; c2.height = Math.min(1280, Math.max(480, h)); g = c2.getContext('2d'); try { const s = c2.captureStream(30); const au = A.stream(); if (au) au.getAudioTracks().forEach((t) => s.addTrack(t)); mr = new MediaRecorder(s, { mimeType: type, videoBitsPerSecond: 4_000_000 }); } catch { mr = null; return; } chunks = []; mr.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); }; mr.start(250); on = true; }, frame() { if (!on || !g) return; const W = c2.width, H = c2.height, sw = cv.width, sh = cv.height, k = Math.max(W / sw, H / sh); g.drawImage(cv, (W - sw * k) / 2, (H - sh * k) / 2, sw * k, sh * k); g.fillStyle = 'rgba(0,0,0,.55)'; g.fillRect(0, H - 54, W, 54); g.font = '800 30px "Barlow Condensed", sans-serif'; g.fillStyle = '#FFC56B'; g.textBaseline = 'middle'; g.fillText('STOOPSHAKE', 16, H - 27); g.fillStyle = '#FFFFFF'; g.textAlign = 'right'; g.font = '700 22px "Barlow Condensed", sans-serif'; g.fillText('cybermaxtools.com/games/stoopshake Β· free play chips', W - 16, H - 27); g.textAlign = 'left'; }, stop() { return new Promise((res) => { if (!on || !mr) { res(null); return; } on = false; mr.onstop = () => res(chunks.length ? { blob: new Blob(chunks, { type: type.split(';')[0] }), ext: type.includes('mp4') ? 'mp4' : 'webm' } : null); try { mr.stop(); } catch { res(null); } }); }, }; })(); let clip = null; // ------------------------------------------------------------------ UI bits function renderTop() { $('chipsN').textContent = S.wallet.chips.toLocaleString('en-US'); const rk = R.rankOf(S.wallet.rep); $('rank').textContent = rk.name.toUpperCase(); $('b-snd').classList.toggle('off', !A.settings().music); document.body.classList.toggle('cmx-pro', ownCrew()); } let bannerT = 0; function banner(kind, text, sub = '', ms = 1700, emote = '') { const b = $('banner'); b.className = `banner k${kind}`; b.innerHTML = `${esc(text)}${sub ? `${esc(sub)}` : ''}${emote ? `${esc(emote)}` : ''}`; requestAnimationFrame(() => b.classList.add('show')); clearTimeout(bannerT); bannerT = setTimeout(() => b.classList.remove('show'), ms * (FAST ? 0.3 : 1)); } let talkT = 0; function talk(who, text, ms = 2600) { const t = $('talk'); t.innerHTML = `${esc(who)}: ${esc(text)}`; t.classList.add('show'); clearTimeout(talkT); talkT = setTimeout(() => t.classList.remove('show'), ms); } function renderSeats() { if (!M) { $('seats').innerHTML = ''; $('pot').textContent = ''; return; } $('seats').innerHTML = M.seats.map((s, i) => `
CHALLENGE Β· FIRST TO ${chal.w}
${esc(msg)}
${over !== null ? 'Send a rematch link:' : `Send this link to ${esc(them)} so they can take their turn (anywhere, any time):`}
${esc(d.label)}
chips over ${d.rounds} rounds
${esc(text)}Free chips top up every day. Come back tomorrow, or keep playing the Practice table (no chips).
No real money. Just for fun.
'; for (const [l, f, c] of btns) { const b = document.createElement('button'); b.type = 'button'; b.className = c; b.textContent = l; b.onclick = f; $('end-b').appendChild(b); } $('end').hidden = false; } function home() { hideAll(); actions([]); M = null; turnSeat = null; renderSeats(); waiting = null; $('roll').disabled = true; if (table && table.state !== 'rolling') { if (table.themeId !== themeFor()) table.setTheme(themeFor()); table.hand(0); } const d = R.dailyTable(TODAY), done = S.daily[TODAY]; $('h-daily').textContent = `DAILY TABLE #${TODAY}${done ? ' β' : ''}`; $('h-daily-s').textContent = done ? `Played: ${done.em} ${done.net >= 0 ? '+' : ''}${done.net}` : `${d.lineup.map((id) => R.botById(id).name).join(', ')} Β· ${d.rounds} rounds`; $('crew-lock').hidden = ownCrew(); const hc = $('h-chal'); if (chal) { hc.hidden = false; const them = chal.n[1 - chal.t]; hc.innerHTML = `${esc(them)} challenged you${chal.d ? ` and rolled ${dz(chal.d)} (${esc(R.score(chal.d).short)})` : ''}. Series ${chal.s[0]}-${chal.s[1]}, first to ${chal.w}.${mode === 'crew' ? 'Your saved crew. Everyone shoots on this phone; crew stats are kept.' : 'Everyone on one phone. Each round: 25 table chips in the pot.'}
` + `You roll first, then send the link. Your friend rolls on their phone and sends it back. No account, no server, first to 3.
` + `YOUR NAMEFRIEND'S NAME`; for (const b of c.querySelectorAll('[data-s]')) b.onclick = () => { stake = Number(b.dataset.s); A.tap(); draw(); }; for (const b of c.querySelectorAll('[data-n]')) b.onclick = () => { n = Number(b.dataset.n); A.tap(); draw(); }; $('back').onclick = home; $('go').onclick = () => { A.ensure(); A.startMusic(); hideAll(); if (mode === 'duel') runDuel({ stake }); else if (mode === 'banker') runBanker({ stake }); else if (mode === 'table') runTable({ stake, seats: [ME(), ...pickBots(n - 1)] }); else if (mode === 'local' || mode === 'crew') { const names = [...c.querySelectorAll('#nm input')].map((i) => R.cleanName(i.value)).filter((v, i, a) => v && v !== 'Player' || i < 2).slice(0, 6); const list = names.length >= 2 ? names : [S.name, 'Player 2']; if (mode === 'crew') S.crew.members = list; else S.locals = list; save(); runLocal(list, mode === 'crew'); } else if (mode === 'link') { S.name = R.cleanName($('n1').value) || S.name; save(); chal = { n: [S.name, R.cleanName($('n2').value || 'Friend')], s: [0, 0], k: 1, t: 0, d: null, w: 3 }; runLinkTurn(); } }; }; hideAll(); draw(); $('setup').hidden = false; } // ------------------------------------------------------------------ style, profile, board, shop function openStyle() { const c = $('style-c'); const lockP = (p) => (p === 'skins' && !ownPack()) || (p === 'crew' && !ownCrew()); const tagP = (p) => (p === 'skins' ? 'PACK' : p === 'crew' ? 'CREW' : ''); c.innerHTML = `Crew colours, crew tables and stats history come with the Crew Pass.
') + `Looks only: every die rolls with exactly the same physics and odds.
`; for (const b of c.querySelectorAll('[data-k]')) b.onclick = () => { const s = DT.SKINS[b.dataset.k]; if (lockP(s.pack)) { s.pack === 'crew' ? openCrew('style-dice') : openPack('style-dice'); return; } S.skin = b.dataset.k; save(); table.setSkin(S.skin, S.crew); A.tap(); openStyle(); }; for (const b of c.querySelectorAll('[data-t]')) b.onclick = () => { const t = DT.THEMES[b.dataset.t]; if (lockP(t.pack)) { t.pack === 'crew' ? openCrew('style-table') : openPack('style-table'); return; } S.theme = b.dataset.t; save(); table.setTheme(S.theme); table.hand(0); A.tap(); openStyle(); }; for (const b of c.querySelectorAll('[data-e]')) b.onclick = () => { if (b.dataset.e !== 'none' && !ownPack()) { openPack('style-emote'); return; } S.emote = b.dataset.e; save(); A.tap(); openStyle(); }; if ($('csave')) $('csave').onclick = () => { S.crew.name = R.cleanName($('cn').value).toUpperCase(); S.crew.a = $('ca').value; S.crew.b = $('cb').value; save(); if (S.skin === 'crew') table.setSkin('crew', S.crew); toast('Crew saved.'); }; if ($('s-crew')) $('s-crew').onclick = (e) => { e.preventDefault(); openCrew('style'); }; $('s-done').onclick = home; hideAll(); $('style').hidden = false; } function openMe() { const w = S.wallet, rk = R.rankOf(w.rep), hist = ownCrew() ? S.hist : S.hist.slice(-10); const wr = w.played ? Math.round((w.won / w.played) * 100) : 0; $('me-c').innerHTML = `${rk.name.toUpperCase()}${rk.next ? ` Β· ${rk.toNext} rep to ${rk.next.name}` : ' Β· top rank'}
` + `` + `| ${esc(n)} | ${s.w}/${s.p} won |
Ranked by rep (wins and big rolls). Region comes from your device's time zone (${esc(reg.label)}), never your exact location.
`; if (!S.board) { c.innerHTML = top + `Show ${esc(S.name)} on the ${esc(reg.label)} and world boards? Only your nickname, rank and rep are sent, once a day.
`; $('bd-on').onclick = () => { S.board = true; save(); count(GAME, 'cta', 'board-join'); openBoard(); }; $('bd-x').onclick = home; return; } c.innerHTML = top + 'Loadingβ¦
'; let j = null; try { if (S.boardDay !== TODAY || S.boardRep !== S.wallet.rep) { const r = await fetch(`${DATA}/dice/board`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ dev: S.dev, name: S.name, region: reg.id, label: reg.label, rep: S.wallet.rep, wins: S.wallet.won }) }); j = await r.json(); S.boardDay = TODAY; S.boardRep = S.wallet.rep; save(); } else { const r = await fetch(`${DATA}/dice/board?region=${encodeURIComponent(reg.id)}`); j = await r.json(); } } catch { j = null; } if (!j || !j.world) { c.innerHTML = top + 'The board is resting right now. Try again in a bit.
'; $('bd-b').onclick = home; return; } const rows = (list) => `| ${i + 1}. ${esc(x.name)} ${esc(x.label || '')} | ${esc(R.rankOf(x.rep).name)} Β· ${x.rep} |
| Be the first. |
| ${i + 1}. ${esc(x.label)} | ${x.players} players Β· ${x.rep} |
The game, every mode and the daily chips are free. Extras are looks and crew features, paid once. They never change the dice.
` + `Prices in USD; tax for your country is added at checkout. Play more than one CyberMax game? The CyberMax Fun Pass is a separate pass that covers every CyberMax game, both Stoopshake extras included. Chips can never be bought.
`) + ``; $('sh-p').onclick = () => { if (!ownPack()) openPack('shop'); }; $('sh-c').onclick = () => { if (!ownCrew()) openCrew('shop'); }; $('sh-b').onclick = home; hideAll(); $('shop').hidden = false; count(GAME, 'cta', 'shop-open'); } // ------------------------------------------------------------------ Telegram Mini App let tgInfo = {}; function openCrew(from) { if (TGM.app()) TGM.sheet('stoopshake-full', { perks: CREW_PERKS, onPaid: claimTG }); else FP.open(from); } function openPack(from) { if (TGM.app()) TGM.sheet('stoopshake-skins', { perks: ['4 dice: Chrome, Gold, Bone, Neon', '3 tables: Stoop, Rooftop, Arcade', '5 win emotes', 'Looks only: same dice odds'], onPaid: claimTG }); else pack.open(from); } async function claimTG() { tgInfo = { ...tgInfo, ...(await TGM.status()) }; const codes = tgInfo.codes || {}; if (codes['stoopshake-full'] && !FP.ownsGame()) await FP.unlockGame(codes['stoopshake-full']); if (codes['stoopshake-skins'] && !pack.owned()) await pack.unlock(codes['stoopshake-skins']); renderTop(); } FP.onChange(() => renderTop()); pack.onChange(() => renderTop()); // ------------------------------------------------------------------ wiring for (const b of document.querySelectorAll('.mode')) b.onclick = () => { A.tap(); setup(b.dataset.m); }; $('rules-ok').onclick = () => { S.rules = true; save(); A.ensure(); A.startMusic(); hideAll(); if (chal) home(); else runDuel({ stake: Math.min(25, S.wallet.chips) || 0, practice: S.wallet.chips < 10 }); }; $('b-rules').onclick = () => { hideAll(); $('rules').hidden = false; $('rules-ok').textContent = 'BACK'; $('rules-ok').onclick = home; }; $('b-style').onclick = () => openStyle(); $('b-me').onclick = () => openMe(); $('b-board').onclick = () => openBoard(); $('b-shop').onclick = (e) => { e.preventDefault(); openShop(); }; $('b-extras').onclick = () => { A.tap(); openShop(); }; if (TGM.app()) $('b-extras').textContent = 'β¨ EXTRAS: CREW PASS Β· DICE & TABLE PACK'; $('b-menu').onclick = () => { A.tap(); if (M && !confirm('Leave this table? Chips already settled stay settled.')) return; location.hash = ''; if (M) { location.href = location.pathname + (chal ? `?c=${R.encodeChallenge(chal)}` : ''); return; } home(); }; $('b-snd').onclick = () => { A.ensure(); const on = !A.settings().music; A.setMusic(on); if (!on) A.stopMusic(); renderTop(); }; TGM.init({ onResize: () => table && table.resize(), onBack: () => home() }).then(async (W) => { if (!W) return; count(GAME, 'tg-open'); tgInfo = await TGM.hello(); claimTG(); }); boot();