stoopshake / tg.js
CyberMax-tools's picture
Deploy stoopshake
9759528 verified
Raw History Blame Contribute Delete
8.01 kB
// 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 = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">Stoopshake</p><h2>Get ${it.name}: ${it.stars} ⭐</h2>
<ul class="fp-perks">${perks.map((p) => `<li>${p}</li>`).join('')}<li>One payment, yours for good (also on the web with the code we send you)</li></ul>
<p class="fp-row"><button class="btn pass" type="button" data-buy>Pay ${it.stars} ⭐ with Telegram Stars</button></p>
<p class="fp-small">Paid through Telegram. 14-day promise: if it doesn't work as described, we fix it or refund your Stars (/paysupport in @${BOT}).</p></div>`;
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); }