/* Twiddle Play SDK v1 · window.TwiddlePlay · ES5, no deps, idempotent, never throws into the game. Transports: Android bridge, parent-frame host (400 ms hello), standalone test ads + test banner. */ (function (w, d) { 'use strict'; if (w.TwiddlePlay) return; var P = w.TwiddlePlay = { version: '1', env: 'standalone' }; var mode = null, waiting = [], pending = {}, seq = 0, hostWin = null, hostOrigin = '*'; var subs = {}, held = { pause: {}, mute: {} }; var t0 = Date.now(), busy = false, interN = 0, interLast = 0; var NO_I = { shown: false }, NO_R = { rewarded: false }; function info(m) { try { console.info('[TwiddlePlay] ' + m); } catch (e) {} } function any(o) { for (var k in o) if (o.hasOwnProperty(k)) return true; return false; } /* pause/mute: held per reason (host, bg, ad), fire on change */ function emit(name) { var a = (subs[name] || []).slice(); for (var i = 0; i < a.length; i++) { try { a[i](); } catch (e) { try { console.error('[TwiddlePlay] ' + name + ' handler', e); } catch (x) {} } } } function hold(kind, why, on) { var h = held[kind], was = any(h); if (on) h[why] = 1; else delete h[why]; if (was !== any(h)) emit(on ? kind : (kind === 'pause' ? 'resume' : 'unmute')); } function hostEvt(n) { if (n === 'pause' || n === 'resume') hold('pause', 'host', n === 'pause'); else if (n === 'mute' || n === 'unmute') hold('mute', 'host', n === 'mute'); else emit(n); } function onVis() { var h = !!d.hidden; hold('pause', 'bg', h); hold('mute', 'bg', h); } if (d.addEventListener) d.addEventListener('visibilitychange', onVis); /* host results, normalised */ function norm(cmd, r, def) { var k = cmd === 'showRewarded' ? 'rewarded' : /^show[IB]/.test(cmd) ? 'shown' : '', o = {}, b = typeof r === 'boolean'; if (k && (b || r && typeof r === 'object')) { o[k] = b ? r : !!r[k]; return o; } return cmd === 'hasRewarded' ? b ? r : !!(r && (r.available || r.hasRewarded)) : def; } function parse(m) { if (typeof m === 'string') { try { m = JSON.parse(m); } catch (e) { return null; } } return m && typeof m === 'object' ? m : null; } function receive(m) { if (!(m = parse(m))) return; if (m.pb === 'res') { var id = String(m.id), p = pending[id]; if (!p) return; delete pending[id]; clearTimeout(p.timer); p.done(m.ok === false ? p.def : norm(p.cmd, m.result, p.def)); } else if (m.pb === 'evt' && m.name === 'adStarted') { /* ad on screen: 10 min cap, not 60 s */ for (var k in pending) if (pending[k].ad && (m.id == null || k === String(m.id))) arm(k, 6e5); } else if (m.pb === 'evt' && m.name) hostEvt(String(m.name)); } function arm(id, ms) { var p = pending[id]; clearTimeout(p.timer); p.timer = setTimeout(function () { delete pending[id]; p.done(p.def); }, ms); } w.__twiddleplayReceive = function (m) { if (mode === 'native') receive(m); }; function send(msg) { if (mode === 'native') w.TwiddlePlayNativeBridge.postMessage(JSON.stringify(msg)); else hostWin.postMessage(msg, hostOrigin); } /* requests wait for the transport */ function call(cmd, def, ms, track, args) { return new Promise(function (resolve) { function done(v) { try { resolve(v); } catch (e) {} } function go() { if (mode === 'standalone') return local(cmd, def, done, args); var id = 'pb' + (++seq), msg = { pb: 'req', id: id, cmd: cmd }; if (args) msg.args = args; if (track) { pending[id] = { cmd: cmd, def: def, done: done, ad: /^show/.test(cmd) }; arm(id, ms); } try { send(msg); } catch (e) { if (track) { clearTimeout(pending[id].timer); delete pending[id]; } done(def); return; } if (!track) done(def); } if (mode) go(); else waiting.push(go); }); } function pick(m) { mode = m; P.env = m === 'native' ? 'android' : m; var q = waiting; waiting = []; for (var i = 0; i < q.length; i++) q[i](); } /* standalone: test ads, app caps */ function capWhy() { var t = Date.now(), el = t - t0; if (el < 6e4) return 'first 60 s of a session'; if (interLast && t - interLast < 9e4) return 'under 90 s since the last'; if (interN + 1 > Math.ceil(el / 18e4)) return 'max 1 per 3 min of play'; return ''; } function local(cmd, def, done, args) { if (/Banner$/.test(cmd)) return done(banner(args)); if (cmd === 'ready') return done(); if (cmd === 'hasRewarded') return done(true); if (cmd === 'exit') { info('exit() closes the game in the app'); return done(); } if (cmd !== 'showInterstitial' && cmd !== 'showRewarded') return done(); var rew = cmd === 'showRewarded', why = busy ? 'an ad is showing' : rew ? '' : capWhy(); if (why) { info((rew ? 'rewarded' : 'interstitial') + ' not shown: ' + why); return done(def); } if (!rew) { interN++; interLast = Date.now(); } testAd(rew, function () { done(rew ? { rewarded: true } : { shown: true }); }); } function testAd(rew, cb) { busy = true; hold('pause', 'ad', true); hold('mute', 'ad', true); var o = add('twiddleplay-test-ad', 'inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;' + 'background:rgba(14,10,36,.95);color:#fff;font:600 16px/1.35 system-ui,sans-serif;text-align:center;touch-action:none'), n = 3, num; o.setAttribute('role', 'dialog'); o.setAttribute('aria-modal', 'true'); o.innerHTML = '
' + (rew ? 'REWARDED' : 'INTERSTITIAL') + ' · TEST
Test ad — real ads show only in the Twiddle Play app
' + '
3
'; num = o.firstChild.lastChild; var iv = setInterval(function () { if (--n > 0) { num.textContent = n; return; } clearInterval(iv); if (o.parentNode) o.parentNode.removeChild(o); busy = false; hold('pause', 'ad', false); hold('mute', 'ad', false); cb(); }, 1000); } /* standalone test banner (in the app the game's viewport shrinks instead) */ var bar = null; function banner(a) { if (bar && bar.parentNode) bar.parentNode.removeChild(bar); if (!a) return bar = null; bar = add('twiddleplay-test-banner', 'left:0;right:0;' + a.position + ':0;height:50px;z-index:2147483646;' + 'background:#241b4d;color:#ffd23f;font:600 12px/50px system-ui,sans-serif;text-align:center;overflow:hidden'); bar.textContent = 'Test banner — real ads only in the Twiddle Play app'; return { shown: true }; } function add(id, css) { var o = d.createElement('div'); o.id = id; o.style.cssText = 'position:fixed;' + css; (d.body || d.documentElement).appendChild(o); return o; } var readyP = null; P.ready = function () { return readyP || (readyP = call('ready', undefined, 5000, true).then(function () {})); }; P.showInterstitial = function () { return call('showInterstitial', NO_I, 6e4, true); }; P.showRewarded = function () { return call('showRewarded', NO_R, 6e4, true); }; P.hasRewarded = function () { return call('hasRewarded', false, 5000, true); }; P.gameplayStart = function () { call('gameplayStart'); }; P.gameplayStop = function () { call('gameplayStop'); }; P.exit = function () { call('exit'); }; P.showBanner = function (o) { return call('showBanner', NO_I, 15e3, true, { position: o && o.position === 'top' ? 'top' : 'bottom' }); }; P.hideBanner = function () { return call('hideBanner', 0, 5e3, true).then(function () {}); }; P.on = function (evt, cb) { if (typeof cb !== 'function') return function () {}; (subs[evt] = subs[evt] || []).push(cb); return function () { P.off(evt, cb); }; }; P.off = function (evt, cb) { var a = subs[evt] || []; for (var i = a.length - 1; i >= 0; i--) if (a[i] === cb) a.splice(i, 1); }; /* pick a transport */ var nb = w.TwiddlePlayNativeBridge; if (nb && typeof nb.postMessage === 'function') pick('native'); else if (w.parent && w.parent !== w) { var timer = setTimeout(function () { if (!mode) pick('standalone'); }, 400); w.addEventListener('message', function (e) { if (e.source !== w.parent) return; var m = parse(e.data); if (!m) return; if (m.pb === 'host') { if (mode === 'host') return; var late = !!mode; /* a late host wins; repeat ready() */ clearTimeout(timer); hostWin = w.parent; hostOrigin = e.origin && e.origin !== 'null' ? e.origin : '*'; pick('host'); if (late && readyP) call('ready', 0, 5000, true); } else if (mode === 'host') receive(m); }); try { w.parent.postMessage({ pb: 'hello' }, '*'); } catch (e) { clearTimeout(timer); pick('standalone'); } } else pick('standalone'); })(window, document);