// Stoopshake as a Telegram Mini App (opened from @CyberMaxTools_bot: menu button, /play, challenge deep links, the // pinned channel post). Inside Telegram: telegram-web-app.js, full-height stage with safe areas, Telegram theme colours, // haptics, sharing into chats, and purchases with Telegram Stars only (Telegram's rule for digital goods in Mini Apps): // Crew Pass (full game) $2.99 = 235 Stars, Dice & Table Pack $1.99 = 155 Stars (same USD price points as the web, 0.013 USD/Star rule). // Payment: data Worker /tg/game-invoice -> WebApp.openInvoice; ownership comes back from /tg/game-owned as signed codes // that the normal unlock check accepts, so a Telegram purchase also unlocks the web game. (c) 2026 CyberMax. const DATA = 'https://data.cybermaxtools.com'; export const BOT = 'CyberMaxTools_bot'; export const ITEMS = { 'stoopshake-full': { stars: 235, usd: '$2.99', name: 'the Crew Pass' }, 'stoopshake-skins': { stars: 155, usd: '$1.99', name: 'the Dice & Table Pack' } }; const q = new URLSearchParams(location.search); export const inTG = q.has('tg') || /tgWebAppData=/.test(location.hash); let W = null; export const app = () => W; function load() { return new Promise((res) => { if (window.Telegram && window.Telegram.WebApp) { res(window.Telegram.WebApp); return; } const s = document.createElement('script'); s.src = 'https://telegram.org/js/telegram-web-app.js'; s.onload = () => res(window.Telegram && window.Telegram.WebApp); s.onerror = () => res(null); document.head.appendChild(s); }); } function insets() { if (!W) return; const a = W.safeAreaInset || {}; const c = W.contentSafeAreaInset || {}; const r = document.documentElement.style; r.setProperty('--tgt', `${(a.top || 0) + (c.top || 0)}px`); r.setProperty('--tgb', `${(a.bottom || 0) + (c.bottom || 0)}px`); r.setProperty('--tgh', `${W.viewportStableHeight || window.innerHeight}px`); } /** Start the Mini App shell. onResize: re-layout the canvas; onBack: Telegram back button pressed. */ export async function init({ onResize, onBack } = {}) { if (!inTG) return null; document.body.classList.add('tg'); W = await load(); if (!W || !W.initData) { document.body.classList.remove('tg'); W = null; return null; } // opened with ?tg=1 outside Telegram: plain web game try { W.ready(); W.expand(); } catch { /* old client */ } try { if (W.disableVerticalSwipes) W.disableVerticalSwipes(); } catch { /* < 7.7 */ } try { W.setHeaderColor('#0B0912'); W.setBackgroundColor('#07060C'); if (W.setBottomBarColor) W.setBottomBarColor('#07060C'); } catch { /* < 6.1 */ } const tp = W.themeParams || {}; if (tp.button_color) document.documentElement.style.setProperty('--tg-btn', tp.button_color); insets(); for (const ev of ['viewportChanged', 'safeAreaChanged', 'contentSafeAreaChanged']) { try { W.onEvent(ev, () => { insets(); onResize && onResize(); }); } catch { /* */ } } try { W.BackButton.onClick(() => onBack && onBack()); } catch { /* */ } onResize && onResize(); return W; } export function back(show) { try { if (W) (show ? W.BackButton.show() : W.BackButton.hide()); } catch { /* */ } } export function haptic(kind) { if (!W || !W.HapticFeedback) return; try { if (kind === 'select') W.HapticFeedback.selectionChanged(); else if (kind === 'success' || kind === 'error' || kind === 'warning') W.HapticFeedback.notificationOccurred(kind); else W.HapticFeedback.impactOccurred(kind || 'light'); } catch { /* */ } } /** Share into a Telegram chat (the native chat picker). */ export function share(text, url) { const link = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`; try { W.openTelegramLink(link); } catch { window.open(link, '_blank'); } } export const playUrl = () => `https://t.me/${BOT}?start=dice`; async function post(path, body) { const r = await fetch(DATA + path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ...body, initData: W.initData }) }); return r.json().catch(() => ({})); } /** What this Telegram user already bought (codes the normal unlock check accepts). */ export async function owned() { if (!W) return {}; try { return (await post('/tg/game-owned', {})).codes || {}; } catch { return {}; } } /** Buy with Stars: opens Telegram's own payment sheet. Resolves 'paid' | 'cancelled' | 'failed' | 'pending'. */ export async function buy(item) { if (!W) return 'failed'; let j; try { j = await post('/tg/game-invoice', { item }); } catch { j = {}; } if (!j.link) return 'failed'; return new Promise((res) => { try { W.openInvoice(j.link, (status) => res(status)); } catch { res('failed'); } }); } /** The Stars purchase sheet shown instead of the web checkout inside Telegram. */ export function sheet(item, { perks = [], onPaid } = {}) { const it = ITEMS[item]; let d = document.getElementById('tgbuy'); if (!d) { d = document.createElement('dialog'); d.id = 'tgbuy'; d.className = 'funpass'; document.body.appendChild(d); d.addEventListener('click', (e) => { if (e.target === d || e.target.closest('[data-close]')) d.close(); }); } d.innerHTML = `

Stoopshake

Get ${it.name}: ${it.stars} ⭐

Paid through Telegram. 14-day promise: if it doesn't work as described, we fix it or refund your Stars (/paysupport in @${BOT}).

`; d.querySelector('[data-buy]').addEventListener('click', async (e) => { e.target.disabled = true; e.target.textContent = 'Opening Telegram payment…'; const st = await buy(item); if (st === 'paid') { haptic('success'); d.close(); onPaid && onPaid(); } else { e.target.disabled = false; e.target.textContent = `Pay ${it.stars} ⭐ with Telegram Stars`; if (st === 'failed') haptic('error'); } }); if (d.showModal) d.showModal(); else d.setAttribute('open', ''); } // ---------- growth loops (all optional for the player; rewards are cosmetic only) export const CHANNEL = 'cybermaxtools'; const startParam = () => { try { return (W && W.initDataUnsafe && W.initDataUnsafe.start_param) || ''; } catch { return ''; } }; export function refFromLaunch() { const r = q.get('ref') || (/^r_([a-z0-9]{6,16})$/.exec(startParam()) || [])[1] || ''; return /^[a-z0-9]{6,16}$/.test(r) ? r : ''; } /** First open: records who invited this player (only for players we have never seen). */ export async function hello() { if (!W) return {}; try { return await post('/tg/game-hello', { game: 'stoopshake', ref: refFromLaunch() }); } catch { return {}; } } /** After the first finished round: records the invite as played (cosmetic rewards only). */ export async function played() { if (!W) return {}; try { return await post('/tg/game-played', {}); } catch { return {}; } } /** Everything this player has: paid codes, free cosmetics, invite link, channel follow. */ export async function status() { if (!W) return {}; try { return await post('/tg/game-owned', {}); } catch { return {}; } } export function invite(link) { share('Shake dice with me on Stoopshake: free 3D street dice (4-5-6). No real money, just bragging rights.', link); } export function joinChannel(onBack) { try { W.openTelegramLink(`https://t.me/${CHANNEL}`); } catch { window.open(`https://t.me/${CHANNEL}`, '_blank'); } const again = () => { try { W.offEvent('activated', again); } catch { /* */ } onBack && onBack(); }; try { W.onEvent('activated', again); } catch { setTimeout(() => onBack && onBack(), 8000); } } /** Challenge a friend: the native Telegram share sheet with the game's own challenge link (state lives in the link). */ export function challenge(text, url) { share(text, url); }