Spaces:
Running
Running
Deploy brimbloom
Browse files- fun.css +2 -0
- funpass.js +106 -29
- index.html +4 -4
- offer.js +2 -0
- sw.js +2 -2
fun.css
CHANGED
|
@@ -50,3 +50,5 @@ dialog.funpass::backdrop{background:rgba(10,12,18,.55)}
|
|
| 50 |
@media (max-width:560px){.chips{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;margin:0 -16px;padding:2px 16px 4px}.chips::-webkit-scrollbar{display:none}.chip{flex:0 0 auto}}
|
| 51 |
@media (min-width:760px){dialog.funpass{max-width:720px}.fp-perks{columns:2;column-gap:28px}}
|
| 52 |
.fp-after{margin:14px 0 0;padding:12px 14px;border:1px solid var(--line, rgba(127,127,127,.35));border-radius:14px;text-align:left}.fp-after p{margin:0 0 8px}.fp-after .fp-row{display:flex;gap:8px;flex-wrap:wrap}
|
|
|
|
|
|
|
|
|
| 50 |
@media (max-width:560px){.chips{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;margin:0 -16px;padding:2px 16px 4px}.chips::-webkit-scrollbar{display:none}.chip{flex:0 0 auto}}
|
| 51 |
@media (min-width:760px){dialog.funpass{max-width:720px}.fp-perks{columns:2;column-gap:28px}}
|
| 52 |
.fp-after{margin:14px 0 0;padding:12px 14px;border:1px solid var(--line, rgba(127,127,127,.35));border-radius:14px;text-align:left}.fp-after p{margin:0 0 8px}.fp-after .fp-row{display:flex;gap:8px;flex-wrap:wrap}
|
| 53 |
+
.fp-game{border-color:var(--acc);display:grid;grid-template-columns:1fr auto;align-items:center;text-align:left;gap:2px 12px;padding:14px 14px 12px;margin:0 0 6px}.fp-game b{font-size:16px}.fp-game .fp-price{grid-row:span 2}.fp-game .fp-note{grid-column:1}
|
| 54 |
+
.fp-or{margin:12px 0 8px;font-size:14px}
|
funpass.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
| 1 |
-
// CyberMax
|
| 2 |
-
//
|
| 3 |
-
//
|
| 4 |
-
//
|
| 5 |
-
//
|
| 6 |
-
// the
|
| 7 |
-
//
|
| 8 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
import { count, store, toast } from './cmx.js';
|
|
|
|
|
|
|
| 10 |
|
| 11 |
const DATA = 'https://data.cybermaxtools.com';
|
| 12 |
const KEY = 'cmx-pro:fun-pass';
|
|
@@ -17,27 +24,52 @@ export const PLANS = [
|
|
| 17 |
{ id: 'yearly', label: 'Yearly', price: '$19', per: '/year', note: 'Save 47%', best: true },
|
| 18 |
{ id: 'unlock', label: 'Lifetime', price: '$29.99', per: ' once', note: 'Pay once, keep forever' },
|
| 19 |
];
|
| 20 |
-
export const COMMON_PERKS = ['
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
let state = store(KEY);
|
|
|
|
| 23 |
const listeners = [];
|
| 24 |
let cfg = { game: 'games', name: 'CyberMax games', perks: [] };
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
export function onChange(fn) { listeners.push(fn); fn(isPro()); }
|
| 28 |
function emit() {
|
| 29 |
document.documentElement.classList.toggle('cmx-pro', isPro());
|
| 30 |
listeners.forEach((fn) => { try { fn(isPro()); } catch { /* game hook */ } });
|
| 31 |
}
|
| 32 |
|
| 33 |
-
async function check(code) {
|
| 34 |
try {
|
| 35 |
-
const r = await fetch(`${DATA}/unlock/
|
| 36 |
const j = await r.json();
|
| 37 |
return j && typeof j.pro === 'boolean' ? j.pro : null;
|
| 38 |
} catch { return null; }
|
| 39 |
}
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
export async function unlock(code) {
|
| 42 |
const ok = await check(code);
|
| 43 |
if (ok) {
|
|
@@ -46,7 +78,10 @@ export async function unlock(code) {
|
|
| 46 |
toast('Fun Pass is on. Thank you for supporting CyberMax games!', 3500);
|
| 47 |
emit();
|
| 48 |
render();
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
| 50 |
else toast('Could not reach the pass check. Try again in a minute.', 3500);
|
| 51 |
return ok;
|
| 52 |
}
|
|
@@ -60,6 +95,8 @@ async function recheck() {
|
|
| 60 |
|
| 61 |
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
| 62 |
const buyUrl = (plan) => `${DATA}/buy/st-fun-pass-${plan}?s=game-${cfg.game}`;
|
|
|
|
|
|
|
| 63 |
|
| 64 |
function dialog() {
|
| 65 |
let d = document.getElementById('funpass');
|
|
@@ -73,28 +110,59 @@ function dialog() {
|
|
| 73 |
return d;
|
| 74 |
}
|
| 75 |
|
|
|
|
|
|
|
|
|
|
| 76 |
function render() {
|
| 77 |
const d = dialog();
|
| 78 |
const perks = [...(cfg.perks || []), ...COMMON_PERKS.filter((p) => !(cfg.perks || []).some((q) => q.split(' ')[0] === p.split(' ')[0]))].slice(0, 6);
|
| 79 |
if (isPro()) {
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
<p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
return;
|
| 85 |
}
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
<h2>One pass. Every game. More to play.</h2>
|
| 88 |
<ul class="fp-perks">${perks.map((p) => `<li>${esc(p)}</li>`).join('')}</ul>
|
| 89 |
-
<div class="fp-plans">${
|
| 90 |
-
<p class="fp-small">Secure Stripe checkout. Works in every CyberMax game. The
|
| 91 |
-
<
|
| 92 |
d.querySelector('.fp-form').addEventListener('submit', (e) => {
|
| 93 |
e.preventDefault();
|
| 94 |
const c = e.target.querySelector('input').value.trim();
|
| 95 |
if (SID.test(c)) unlock(c); else toast('Paste the code from your receipt page (cs_live_…) or your Gumroad licence key.', 3500);
|
| 96 |
});
|
| 97 |
d.querySelectorAll('[data-plan]').forEach((a) => a.addEventListener('click', () => count(cfg.game, 'cta', `pass-${a.dataset.plan}`)));
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
/** Open the pass sheet; `from` names the button that opened it (counted as cta). */
|
|
@@ -110,15 +178,20 @@ export function open(from = 'button') {
|
|
| 110 |
* `host` is the result panel; the card is added once and removed for pass holders. */
|
| 111 |
export function afterWin(host, from = 'win') {
|
| 112 |
if (!host) return;
|
| 113 |
-
if (isPro()) { host.querySelector('.fp-after')?.remove(); return; }
|
| 114 |
let el = host.querySelector('.fp-after');
|
| 115 |
if (!el) { el = document.createElement('div'); el.className = 'fp-after'; host.appendChild(el); }
|
| 116 |
const best = PLANS.find((p) => p.best) || PLANS[0];
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
<div class="fp-cap"></div>`;
|
| 121 |
-
el.querySelector('[data-after]').addEventListener('click', () => { count(cfg.game, 'cta', `pass-${best.id}-after`); kit((K) => K.event('upgrade-click', cfg.game)); });
|
| 122 |
el.querySelector('[data-plans]').addEventListener('click', () => open(`after-${from}`));
|
| 123 |
kit((K) => {
|
| 124 |
K.event('result', cfg.game); K.event('upgrade-view', cfg.game);
|
|
@@ -138,13 +211,17 @@ function kit(fn) {
|
|
| 138 |
document.head.appendChild(sc);
|
| 139 |
}
|
| 140 |
|
| 141 |
-
/** Call once per page: game id, display name and the game's own
|
|
|
|
| 142 |
export function init(options) {
|
| 143 |
cfg = { ...cfg, ...options };
|
| 144 |
const m = /[#&]cmx-pro=([^&]+)/.exec(location.hash || '');
|
| 145 |
if (m && SID.test(decodeURIComponent(m[1]))) {
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
| 148 |
} else recheck();
|
| 149 |
emit();
|
| 150 |
}
|
|
|
|
| 1 |
+
// CyberMax games checkout: the per-game "full game" unlock + the Fun Pass (every game), copied into each game's web/ folder.
|
| 2 |
+
// Model (games lane GM, 9 Oct 2026; evidence docs/ceo/games-monetization-2026-10-09.md):
|
| 3 |
+
// - Free demo: every game's daily challenge stays free for everyone, plus the first levels (OFFER.demo once the per-game
|
| 4 |
+
// unlock is live; levels 1-20 until then). A level the player has opened is never locked again.
|
| 5 |
+
// - Full game: ONE payment per game (OFFER.price, Stripe pack revenue/packs/stripe/<game>-full) unlocks every level,
|
| 6 |
+
// mode, the daily archive and themes in that game, for good. Shown only when OFFER.live is true (owner-approved).
|
| 7 |
+
// - Fun Pass = the all-games bundle: Monthly $2.99 · Yearly $19 · Lifetime $29.99 (pack revenue/packs/stripe/fun-pass).
|
| 8 |
+
// - Portal builds (scripts/games/portals.py) never show a checkout; where the portal SDK offers rewarded ads, the player
|
| 9 |
+
// may watch one to open the full game for an hour (shared/portal-adapter/cmx-portal.js CMXPortal.rewarded()).
|
| 10 |
+
// Buy: counted redirect https://data.cybermaxtools.com/buy/st-<pack>-<plan>?s=game-<id> -> Stripe checkout.
|
| 11 |
+
// After checkout the data Worker (/unlock/fun-pass or /unlock/<game>-full) checks the Checkout Session and returns with
|
| 12 |
+
// #cmx-pro=<code> (the full-game return adds ?unlock=full). The code on the receipt page unlocks any other device.
|
| 13 |
+
// No account, no cookies, nothing personal stored. (c) 2026 CyberMax. All rights reserved.
|
| 14 |
import { count, store, toast } from './cmx.js';
|
| 15 |
+
import { OFFER } from './offer.js'; // written by scripts/games/sync_kit.py from products/fun-pass/games.json
|
| 16 |
+
export { OFFER };
|
| 17 |
|
| 18 |
const DATA = 'https://data.cybermaxtools.com';
|
| 19 |
const KEY = 'cmx-pro:fun-pass';
|
|
|
|
| 24 |
{ id: 'yearly', label: 'Yearly', price: '$19', per: '/year', note: 'Save 47%', best: true },
|
| 25 |
{ id: 'unlock', label: 'Lifetime', price: '$29.99', per: ' once', note: 'Pay once, keep forever' },
|
| 26 |
];
|
| 27 |
+
export const COMMON_PERKS = ['Every level and mode in every CyberMax game', 'The full archive of past dailies', 'Custom colour themes', 'New games included as they launch'];
|
| 28 |
+
const FULL_KEY = `cmx-full:${OFFER.game}`;
|
| 29 |
+
const REWARD_KEY = `cmx-reward:${OFFER.game}`;
|
| 30 |
+
const REWARD_MS = 60 * 60 * 1000; // one rewarded ad on a portal build opens the full game for an hour
|
| 31 |
|
| 32 |
let state = store(KEY);
|
| 33 |
+
let full = store(FULL_KEY);
|
| 34 |
const listeners = [];
|
| 35 |
let cfg = { game: 'games', name: 'CyberMax games', perks: [] };
|
| 36 |
|
| 37 |
+
const passOn = () => !!(state && state.code);
|
| 38 |
+
export const ownsGame = () => !!(full && full.code);
|
| 39 |
+
const rewardOn = () => { const r = store(REWARD_KEY); return !!(r && Date.now() - r.at < REWARD_MS); };
|
| 40 |
+
/** True when this player may use everything in this game: Fun Pass, this game's full unlock, or a portal reward hour. */
|
| 41 |
+
export const isPro = () => passOn() || ownsGame() || rewardOn();
|
| 42 |
+
/** Free levels in this game's demo: OFFER.demo once the per-game unlock is live, else the game's current number. */
|
| 43 |
+
export const freeLevels = (current = 20) => (OFFER.live && OFFER.demo ? Math.min(current, OFFER.demo) : current);
|
| 44 |
+
const portal = () => (typeof window !== 'undefined' && window.CMXPortal && window.CMXPortal.portal) || '';
|
| 45 |
export function onChange(fn) { listeners.push(fn); fn(isPro()); }
|
| 46 |
function emit() {
|
| 47 |
document.documentElement.classList.toggle('cmx-pro', isPro());
|
| 48 |
listeners.forEach((fn) => { try { fn(isPro()); } catch { /* game hook */ } });
|
| 49 |
}
|
| 50 |
|
| 51 |
+
async function check(code, what = 'fun-pass') {
|
| 52 |
try {
|
| 53 |
+
const r = await fetch(`${DATA}/unlock/${what}?json=1&session_id=${encodeURIComponent(code)}`);
|
| 54 |
const j = await r.json();
|
| 55 |
return j && typeof j.pro === 'boolean' ? j.pro : null;
|
| 56 |
} catch { return null; }
|
| 57 |
}
|
| 58 |
|
| 59 |
+
/** Turn on a full-game code for this game (receipt code of the <game>-full Payment Link). */
|
| 60 |
+
export async function unlockGame(code) {
|
| 61 |
+
const ok = await check(code, `${OFFER.game}-full`);
|
| 62 |
+
if (ok) {
|
| 63 |
+
full = store(FULL_KEY, { code, at: new Date().toISOString() });
|
| 64 |
+
count(cfg.game, 'pro', 'full-unlocked');
|
| 65 |
+
toast(`${OFFER.name}: the full game is on. Thank you for supporting CyberMax games!`, 3500);
|
| 66 |
+
emit(); render();
|
| 67 |
+
} else if (ok === false) toast('That code is not a full-game order for this game.', 3500);
|
| 68 |
+
else toast('Could not reach the unlock check. Try again in a minute.', 3500);
|
| 69 |
+
return ok;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
/** Turn on a pasted or returned code: a Fun Pass code, or (where this game sells its own unlock) a full-game code. */
|
| 73 |
export async function unlock(code) {
|
| 74 |
const ok = await check(code);
|
| 75 |
if (ok) {
|
|
|
|
| 78 |
toast('Fun Pass is on. Thank you for supporting CyberMax games!', 3500);
|
| 79 |
emit();
|
| 80 |
render();
|
| 81 |
+
return ok;
|
| 82 |
+
}
|
| 83 |
+
if (sellsGame() && (await check(code, `${OFFER.game}-full`))) return unlockGame(code);
|
| 84 |
+
if (ok === false) toast('That code is not an active Fun Pass.', 3500);
|
| 85 |
else toast('Could not reach the pass check. Try again in a minute.', 3500);
|
| 86 |
return ok;
|
| 87 |
}
|
|
|
|
| 95 |
|
| 96 |
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
| 97 |
const buyUrl = (plan) => `${DATA}/buy/st-fun-pass-${plan}?s=game-${cfg.game}`;
|
| 98 |
+
const gameUrl = (from = '') => `${DATA}/buy/st-${OFFER.game}-full-unlock?s=game-${cfg.game}${from ? `-${from}` : ''}`;
|
| 99 |
+
const sellsGame = () => !!(OFFER.live && OFFER.price && OFFER.game !== 'games');
|
| 100 |
|
| 101 |
function dialog() {
|
| 102 |
let d = document.getElementById('funpass');
|
|
|
|
| 110 |
return d;
|
| 111 |
}
|
| 112 |
|
| 113 |
+
const planHtml = () => PLANS.map((p) => `<a class="fp-plan${p.best ? ' best' : ''}" href="${buyUrl(p.id)}" rel="nofollow" data-plan="${p.id}">${p.best ? '<i>Best value</i>' : ''}<b>${p.label}</b><span class="fp-price">${p.price}<small>${p.per}</small></span><span class="fp-note">${p.note}</span></a>`).join('');
|
| 114 |
+
const codeForm = `<details><summary>Already bought?</summary><form class="fp-form"><input aria-label="Order code" autocomplete="off" placeholder="Receipt code (cs_live_…) or Gumroad licence key"><button class="btn sm" type="submit">Turn on</button></form></details>`;
|
| 115 |
+
|
| 116 |
function render() {
|
| 117 |
const d = dialog();
|
| 118 |
const perks = [...(cfg.perks || []), ...COMMON_PERKS.filter((p) => !(cfg.perks || []).some((q) => q.split(' ')[0] === p.split(' ')[0]))].slice(0, 6);
|
| 119 |
if (isPro()) {
|
| 120 |
+
const what = passOn() ? 'The Fun Pass is on in this browser for every CyberMax game' : ownsGame() ? `You own the full ${esc(OFFER.name)}` : 'The full game is open for the next hour (rewarded ad)';
|
| 121 |
+
const code = passOn() ? state.code : ownsGame() ? full.code : '';
|
| 122 |
+
d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${passOn() ? 'CyberMax Fun Pass' : esc(OFFER.name)}</p><h2>Everything is open. Thank you!</h2>
|
| 123 |
+
<p>${what}: ${perks.map(esc).join(' · ')}.</p>
|
| 124 |
+
${code ? `<p class="fp-small">Your code (paste it on another device to turn it on there):</p><code class="fp-code">${esc(code)}</code>` : ''}
|
| 125 |
+
${passOn() ? '<p class="fp-small">Manage or cancel a subscription any time from the link in your Stripe receipt email.</p>' : ''}</div>`;
|
| 126 |
+
return;
|
| 127 |
+
}
|
| 128 |
+
if (portal()) { // portal build: no outside checkout; a rewarded ad (when the portal offers one) opens the game for an hour
|
| 129 |
+
d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${esc(OFFER.name || 'Full game')}</p>
|
| 130 |
+
<h2>Open the full game for an hour</h2><ul class="fp-perks">${perks.slice(0, 4).map((p) => `<li>${esc(p)}</li>`).join('')}</ul>
|
| 131 |
+
<p><button class="btn" type="button" data-reward>▶ Watch a short ad</button></p>
|
| 132 |
+
<p class="fp-small">Optional. The daily challenge and the free levels stay free without ads.</p></div>`;
|
| 133 |
+
d.querySelector('[data-reward]').addEventListener('click', async () => {
|
| 134 |
+
count(cfg.game, 'cta', 'reward-ad');
|
| 135 |
+
const ok = await window.CMXPortal.rewarded().catch(() => false);
|
| 136 |
+
if (ok) { store(REWARD_KEY, { at: Date.now() }); count(cfg.game, 'pro', 'reward-hour'); toast('Everything is open for the next hour. Have fun!', 3000); emit(); d.close(); }
|
| 137 |
+
else toast('No ad is available right now. Try again later.', 3000);
|
| 138 |
+
});
|
| 139 |
return;
|
| 140 |
}
|
| 141 |
+
const allGames = `${OFFER.games || 'every'} CyberMax games`;
|
| 142 |
+
const own = cfg.perks || [];
|
| 143 |
+
const gamePerks = [...own, ...(own.some((p) => /archive/i.test(p)) ? [] : ['The full archive of past dailies']), 'Every bonus mode in this game'].slice(0, 6);
|
| 144 |
+
d.innerHTML = sellsGame()
|
| 145 |
+
? `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${esc(OFFER.name)} · full game</p>
|
| 146 |
+
<h2>Unlock the full ${esc(OFFER.name)}</h2>
|
| 147 |
+
<ul class="fp-perks">${gamePerks.map((p) => `<li>${esc(p)}</li>`).join('')}</ul>
|
| 148 |
+
<a class="fp-plan fp-game" href="${gameUrl()}" rel="nofollow" data-game><i>This game</i><b>Full ${esc(OFFER.name)}</b><span class="fp-price">${esc(OFFER.price)}<small> once</small></span><span class="fp-note">Pay once, keep it forever. No subscription.</span></a>
|
| 149 |
+
<p class="fp-or"><b>Or get all ${esc(allGames)}</b> with the Fun Pass:</p>
|
| 150 |
+
<div class="fp-plans">${planHtml()}</div>
|
| 151 |
+
<p class="fp-small">Secure Stripe checkout · turns on as soon as you pay · your code works on any device · 14-day promise: if it doesn't work as described, we fix it or refund you. The daily challenge and the first ${OFFER.demo || ''} levels stay free for everyone.</p>
|
| 152 |
+
${codeForm}</div>`
|
| 153 |
+
: `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">CyberMax Fun Pass</p>
|
| 154 |
<h2>One pass. Every game. More to play.</h2>
|
| 155 |
<ul class="fp-perks">${perks.map((p) => `<li>${esc(p)}</li>`).join('')}</ul>
|
| 156 |
+
<div class="fp-plans">${planHtml()}</div>
|
| 157 |
+
<p class="fp-small">Secure Stripe checkout. Works in every CyberMax game. The daily challenges and the free levels stay free for everyone.</p>
|
| 158 |
+
${codeForm}</div>`;
|
| 159 |
d.querySelector('.fp-form').addEventListener('submit', (e) => {
|
| 160 |
e.preventDefault();
|
| 161 |
const c = e.target.querySelector('input').value.trim();
|
| 162 |
if (SID.test(c)) unlock(c); else toast('Paste the code from your receipt page (cs_live_…) or your Gumroad licence key.', 3500);
|
| 163 |
});
|
| 164 |
d.querySelectorAll('[data-plan]').forEach((a) => a.addEventListener('click', () => count(cfg.game, 'cta', `pass-${a.dataset.plan}`)));
|
| 165 |
+
d.querySelector('[data-game]')?.addEventListener('click', () => count(cfg.game, 'cta', 'full-game'));
|
| 166 |
}
|
| 167 |
|
| 168 |
/** Open the pass sheet; `from` names the button that opened it (counted as cta). */
|
|
|
|
| 178 |
* `host` is the result panel; the card is added once and removed for pass holders. */
|
| 179 |
export function afterWin(host, from = 'win') {
|
| 180 |
if (!host) return;
|
| 181 |
+
if (isPro() || portal()) { host.querySelector('.fp-after')?.remove(); return; }
|
| 182 |
let el = host.querySelector('.fp-after');
|
| 183 |
if (!el) { el = document.createElement('div'); el.className = 'fp-after'; host.appendChild(el); }
|
| 184 |
const best = PLANS.find((p) => p.best) || PLANS[0];
|
| 185 |
+
const game = sellsGame();
|
| 186 |
+
const href = game ? gameUrl('after') : `${buyUrl(best.id)}-after`;
|
| 187 |
+
const label = game ? `Get the full ${OFFER.name}: ${OFFER.price} once` : `Get the Fun Pass: ${best.price}${best.per}`;
|
| 188 |
+
const line = game ? (cfg.extra || `The full game opens every level, mode, the daily archive and themes in ${OFFER.name}.`)
|
| 189 |
+
: (cfg.extra || 'The Fun Pass opens every level, mode, the full archive and themes in every CyberMax game.');
|
| 190 |
+
el.innerHTML = `<p><b>Want more today?</b> ${esc(line)}</p>
|
| 191 |
+
<p class="fp-row"><a class="btn sm" href="${href}" rel="nofollow" data-after>${esc(label)}</a> <button class="btn sm ghost" type="button" data-plans>${game ? 'All games pass' : 'See all plans'}</button></p>
|
| 192 |
+
<p class="fp-small">Secure Stripe checkout · turns on as soon as you pay · ${game ? 'one payment, no subscription' : 'cancel any time from your receipt email'} · 14-day promise: if it doesn't work as described, we fix it or refund you (<a href="https://cybermaxtools.com/terms#payments" target="_blank" rel="noopener">terms</a>). Today's daily challenge stays free.</p>
|
| 193 |
<div class="fp-cap"></div>`;
|
| 194 |
+
el.querySelector('[data-after]').addEventListener('click', () => { count(cfg.game, 'cta', game ? 'full-game-after' : `pass-${best.id}-after`); kit((K) => K.event('upgrade-click', cfg.game)); });
|
| 195 |
el.querySelector('[data-plans]').addEventListener('click', () => open(`after-${from}`));
|
| 196 |
kit((K) => {
|
| 197 |
K.event('result', cfg.game); K.event('upgrade-view', cfg.game);
|
|
|
|
| 211 |
document.head.appendChild(sc);
|
| 212 |
}
|
| 213 |
|
| 214 |
+
/** Call once per page: game id, display name and the game's own perks (listed first). Reads #cmx-pro=<code> after
|
| 215 |
+
* checkout (?unlock=full marks this game's full-game return). Skin-pack codes are taken first by skins.js capture(). */
|
| 216 |
export function init(options) {
|
| 217 |
cfg = { ...cfg, ...options };
|
| 218 |
const m = /[#&]cmx-pro=([^&]+)/.exec(location.hash || '');
|
| 219 |
if (m && SID.test(decodeURIComponent(m[1]))) {
|
| 220 |
+
const q = new URLSearchParams(location.search);
|
| 221 |
+
const fullReturn = q.get('unlock') === 'full';
|
| 222 |
+
q.delete('unlock');
|
| 223 |
+
history.replaceState(null, '', location.pathname + (q.toString() ? `?${q}` : ''));
|
| 224 |
+
if (fullReturn) unlockGame(decodeURIComponent(m[1])); else unlock(decodeURIComponent(m[1]));
|
| 225 |
} else recheck();
|
| 226 |
emit();
|
| 227 |
}
|
index.html
CHANGED
|
@@ -18,14 +18,14 @@
|
|
| 18 |
<meta name="twitter:title" content="Brimbloom: Free One-Tap Timing Game Online">
|
| 19 |
<meta name="twitter:description" content="Play Brimbloom free: a one-tap timing game with 30 levels and a daily garden. Stop the bloom on the brim of the ring, earn stars, beat a friend's score.">
|
| 20 |
<style>.h1kw{display:block;font-size:.42em;font-weight:600;opacity:.75;letter-spacing:.01em;margin-top:.15em}.seo ol,.seo ul{padding-left:1.2em}.seo li{margin:.25em 0}</style>
|
| 21 |
-
<script type="application/ld+json">{"@context": "https://schema.org", "@graph": [{"@type": "VideoGame", "@id": "https://cybermaxtools.com/games/brimbloom/#game", "name": "Brimbloom", "alternateName": "Timing game online free", "url": "https://cybermaxtools.com/games/brimbloom/", "image": "https://cybermaxtools.com/games/brimbloom/og.png", "description": "Brimbloom is a free one-tap timing game for your browser. A flower blooms outward from the middle of the pond; tap to stop it with the petal tips inside the ring, in the brighter middle for a perfect. 30 levels ask you to bloom a set number of flowers before your hearts run out, adding surging blooms, breathing rings, two brims, fading rings and thorn rings, with a one-heart challenge level every 10. Earn up to 3 stars, play the same daily garden as everyone else, keep a streak and send a friend a link to beat your score.", "genre": ["Arcade", "Casual", "Timing", "One-tap"], "gamePlatform": ["Web browser", "Android", "iOS", "Windows", "macOS", "ChromeOS"], "applicationCategory": "Game", "operatingSystem": "Any (web browser)", "playMode": "SinglePlayer", "inLanguage": "en", "isAccessibleForFree": true, "offers": {"@type": "Offer", "price": "0", "priceCurrency": "USD"}, "author": {"@type": "Organization", "name": "CyberMax", "url": "https://cybermaxtools.com/"}, "publisher": {"@type": "Organization", "name": "CyberMax"}, "datePublished": "2026-10-03", "keywords": "Timing game online free, free one-tap timing game, unblocked, no download, browser game", "trailer": {"@id": "https://cybermaxtools.com/games/brimbloom/#video"}}, {"@type": "FAQPage", "@id": "https://cybermaxtools.com/games/brimbloom/#faq", "mainEntity": [{"@type": "Question", "name": "Is Brimbloom free?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. Levels 1-20, the daily garden and practice are free; levels 21-30 come with the CyberMax Fun Pass and skins are cosmetic extras."}}, {"@type": "Question", "name": "How many levels are there?", "acceptedAnswer": {"@type": "Answer", "text": "30, with a new twist about every 5 levels and a one-heart challenge level every 10."}}, {"@type": "Question", "name": "How do I get 3 stars?", "acceptedAnswer": {"@type": "Answer", "text": "Stop most flowers in the brighter middle of the ring: a perfect is worth 2 points, a good stop 1."}}, {"@type": "Question", "name": "Can I challenge a friend?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. After a run, tap Challenge a friend to send a link to the same level or daily garden with your score."}}, {"@type": "Question", "name": "Can I play offline?", "acceptedAnswer": {"@type": "Answer", "text": "Yes, after the first visit, and you can install it like an app."}}, {"@type": "Question", "name": "Why is everyone's game the same today?", "acceptedAnswer": {"@type": "Answer", "text": "Each UTC day has one seed, so daily scores are directly comparable with friends."}}]}, {"@type": "VideoObject", "name": "Brimbloom demo: Stop the bloom on the brim", "description": "A 16-second screen recording of the live Brimbloom page: One tap stops the bloom; Inside the ring scores, centre is perfect; Share your grid, retry in one tap. Real result, no voice.", "thumbnailUrl": ["https://cybermax.cybermax-tools.workers.dev/media/demos/brimbloom/poster.jpg"], "uploadDate": "2026-10-03T15:52:14+00:00", "duration": "PT16S", "contentUrl": "https://cybermax.cybermax-tools.workers.dev/media/demos/brimbloom/demo-16x9.mp4", "embedUrl": "https://cybermaxtools.com/games/brimbloom/", "inLanguage": "en", "publisher": {"@type": "Organization", "name": "CyberMax", "logo": {"@type": "ImageObject", "url": "https://cybermax.cybermax-tools.workers.dev/store/img/favicon.webp"}}, "@id": "https://cybermaxtools.com/games/brimbloom/#video"}]}</script>
|
| 22 |
</head><body>
|
| 23 |
<div class="wrap">
|
| 24 |
<header class="top"><a class="brand" href="https://cybermaxtools.com/games/"><svg viewBox="0 0 512 512" aria-hidden="true"><clipPath id="lg"><rect width="512" height="512" rx="116"/></clipPath><g clip-path="url(#lg)"><rect width="512" height="512" fill="#FF6A3D"/><path d="M512 250V512H250Z" fill="#D9481F"/></g><g transform="translate(86 86) scale(3.4)"><path d="M66 27 A27 27 0 1 0 66 73" fill="none" stroke="#11141B" stroke-width="17" stroke-linecap="round"/><rect x="66" y="41" width="20" height="18" rx="4" fill="#11141B"/></g></svg>CyberMax</a>
|
| 25 |
<nav><a href="https://cybermaxtools.com/games/">Games</a></nav><button class="icon" id="mute" type="button" aria-label="Sound on or off">🔊</button><button class="icon" id="theme" type="button" aria-label="Switch light or dark">◐</button></header>
|
| 26 |
<main>
|
| 27 |
<section class="game">
|
| 28 |
-
<div class="headline"><h1>Brimbloom <span id="num" class="num"></span> <span class="pro-badge">PASS</span>
|
| 29 |
<p class="muted tag">Tap to stop the bloom inside the ring. 30 levels, plus one daily garden for everyone.</p></div>
|
| 30 |
<div class="chips" id="modes" role="group" aria-label="Mode"></div>
|
| 31 |
<div class="lv-banner" id="challenge" hidden></div>
|
|
@@ -57,7 +57,7 @@
|
|
| 57 |
<ul><li>Watch the rhythm of each bloom for a moment before committing; surging blooms slow down near the end.</li><li>Aim for the inner edge of the bright band, since reaction delay carries you outward.</li><li>On breathing rings, tap when the ring is at its widest.</li><li>Play with sound on: the rising tone helps timing.</li></ul>
|
| 58 |
<h2>Game modes</h2>
|
| 59 |
<ul><li>30 levels: surging blooms, breathing rings, two brims, fading rings and thorn rings, plus a one-heart challenge level every 10. Levels 1-20 are free.</li><li>Daily garden: the same blooms for everyone today, with a streak and a shareable emoji grid.</li><li>Beat-my-score links: send a friend the same level or daily garden with your score.</li><li>Practice: a random garden every try. Fun Pass: levels 21-30, past dailies and extra themes; Skin Pack adds six flowers.</li></ul>
|
| 60 |
-
<p>
|
| 61 |
<h2>Brimbloom FAQ</h2>
|
| 62 |
<h3>Is Brimbloom free?</h3><p>Yes. Levels 1-20, the daily garden and practice are free; levels 21-30 come with the CyberMax Fun Pass and skins are cosmetic extras.</p>
|
| 63 |
<h3>How many levels are there?</h3><p>30, with a new twist about every 5 levels and a one-heart challenge level every 10.</p>
|
|
@@ -72,4 +72,4 @@
|
|
| 72 |
<footer>© 2026 CyberMax · <a href="https://cybermaxtools.com/games/">Games</a> · <a href="https://cybermaxtools.com/toys/">Toys</a> · <a href="https://cybermaxtools.com/store/">Store</a> · <a href="https://cybermaxtools.com/privacy">Privacy</a> · <a href="https://cybermaxtools.com/terms">Terms</a></footer>
|
| 73 |
</div>
|
| 74 |
<script type="module" src="game.js"></script>
|
| 75 |
-
<
|
|
|
|
| 18 |
<meta name="twitter:title" content="Brimbloom: Free One-Tap Timing Game Online">
|
| 19 |
<meta name="twitter:description" content="Play Brimbloom free: a one-tap timing game with 30 levels and a daily garden. Stop the bloom on the brim of the ring, earn stars, beat a friend's score.">
|
| 20 |
<style>.h1kw{display:block;font-size:.42em;font-weight:600;opacity:.75;letter-spacing:.01em;margin-top:.15em}.seo ol,.seo ul{padding-left:1.2em}.seo li{margin:.25em 0}</style>
|
| 21 |
+
<script type="application/ld+json">{"@context": "https://schema.org", "@graph": [{"@type": "VideoGame", "@id": "https://cybermaxtools.com/games/brimbloom/#game", "name": "Brimbloom", "alternateName": "Timing game online free", "url": "https://cybermaxtools.com/games/brimbloom/", "image": "https://cybermaxtools.com/games/brimbloom/og.png", "description": "Brimbloom is a free one-tap timing game for your browser. A flower blooms outward from the middle of the pond; tap to stop it with the petal tips inside the ring, in the brighter middle for a perfect. 30 levels ask you to bloom a set number of flowers before your hearts run out, adding surging blooms, breathing rings, two brims, fading rings and thorn rings, with a one-heart challenge level every 10. Earn up to 3 stars, play the same daily garden as everyone else, keep a streak and send a friend a link to beat your score.", "genre": ["Arcade", "Casual", "Timing", "One-tap"], "gamePlatform": ["Web browser", "Android", "iOS", "Windows", "macOS", "ChromeOS"], "applicationCategory": "Game", "operatingSystem": "Any (web browser)", "playMode": "SinglePlayer", "inLanguage": "en", "isAccessibleForFree": true, "offers": [{"@type": "Offer", "name": "Free demo (daily challenge + levels 1-20)", "price": "0", "priceCurrency": "USD"}], "author": {"@type": "Organization", "name": "CyberMax", "url": "https://cybermaxtools.com/"}, "publisher": {"@type": "Organization", "name": "CyberMax"}, "datePublished": "2026-10-03", "keywords": "Timing game online free, free one-tap timing game, unblocked, no download, browser game", "trailer": {"@id": "https://cybermaxtools.com/games/brimbloom/#video"}}, {"@type": "FAQPage", "@id": "https://cybermaxtools.com/games/brimbloom/#faq", "mainEntity": [{"@type": "Question", "name": "Is Brimbloom free?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. Levels 1-20, the daily garden and practice are free; levels 21-30 come with the CyberMax Fun Pass and skins are cosmetic extras."}}, {"@type": "Question", "name": "How many levels are there?", "acceptedAnswer": {"@type": "Answer", "text": "30, with a new twist about every 5 levels and a one-heart challenge level every 10."}}, {"@type": "Question", "name": "How do I get 3 stars?", "acceptedAnswer": {"@type": "Answer", "text": "Stop most flowers in the brighter middle of the ring: a perfect is worth 2 points, a good stop 1."}}, {"@type": "Question", "name": "Can I challenge a friend?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. After a run, tap Challenge a friend to send a link to the same level or daily garden with your score."}}, {"@type": "Question", "name": "Can I play offline?", "acceptedAnswer": {"@type": "Answer", "text": "Yes, after the first visit, and you can install it like an app."}}, {"@type": "Question", "name": "Why is everyone's game the same today?", "acceptedAnswer": {"@type": "Answer", "text": "Each UTC day has one seed, so daily scores are directly comparable with friends."}}]}, {"@type": "VideoObject", "name": "Brimbloom demo: Stop the bloom on the brim", "description": "A 16-second screen recording of the live Brimbloom page: One tap stops the bloom; Inside the ring scores, centre is perfect; Share your grid, retry in one tap. Real result, no voice.", "thumbnailUrl": ["https://cybermax.cybermax-tools.workers.dev/media/demos/brimbloom/poster.jpg"], "uploadDate": "2026-10-03T15:52:14+00:00", "duration": "PT16S", "contentUrl": "https://cybermax.cybermax-tools.workers.dev/media/demos/brimbloom/demo-16x9.mp4", "embedUrl": "https://cybermaxtools.com/games/brimbloom/", "inLanguage": "en", "publisher": {"@type": "Organization", "name": "CyberMax", "logo": {"@type": "ImageObject", "url": "https://cybermax.cybermax-tools.workers.dev/store/img/favicon.webp"}}, "@id": "https://cybermaxtools.com/games/brimbloom/#video"}]}</script>
|
| 22 |
</head><body>
|
| 23 |
<div class="wrap">
|
| 24 |
<header class="top"><a class="brand" href="https://cybermaxtools.com/games/"><svg viewBox="0 0 512 512" aria-hidden="true"><clipPath id="lg"><rect width="512" height="512" rx="116"/></clipPath><g clip-path="url(#lg)"><rect width="512" height="512" fill="#FF6A3D"/><path d="M512 250V512H250Z" fill="#D9481F"/></g><g transform="translate(86 86) scale(3.4)"><path d="M66 27 A27 27 0 1 0 66 73" fill="none" stroke="#11141B" stroke-width="17" stroke-linecap="round"/><rect x="66" y="41" width="20" height="18" rx="4" fill="#11141B"/></g></svg>CyberMax</a>
|
| 25 |
<nav><a href="https://cybermaxtools.com/games/">Games</a></nav><button class="icon" id="mute" type="button" aria-label="Sound on or off">🔊</button><button class="icon" id="theme" type="button" aria-label="Switch light or dark">◐</button></header>
|
| 26 |
<main>
|
| 27 |
<section class="game">
|
| 28 |
+
<div class="headline"><h1>Brimbloom <span id="num" class="num"></span> <span class="pro-badge">PASS</span> <small class="h1kw">Free one-tap timing game</small></h1>
|
| 29 |
<p class="muted tag">Tap to stop the bloom inside the ring. 30 levels, plus one daily garden for everyone.</p></div>
|
| 30 |
<div class="chips" id="modes" role="group" aria-label="Mode"></div>
|
| 31 |
<div class="lv-banner" id="challenge" hidden></div>
|
|
|
|
| 57 |
<ul><li>Watch the rhythm of each bloom for a moment before committing; surging blooms slow down near the end.</li><li>Aim for the inner edge of the bright band, since reaction delay carries you outward.</li><li>On breathing rings, tap when the ring is at its widest.</li><li>Play with sound on: the rising tone helps timing.</li></ul>
|
| 58 |
<h2>Game modes</h2>
|
| 59 |
<ul><li>30 levels: surging blooms, breathing rings, two brims, fading rings and thorn rings, plus a one-heart challenge level every 10. Levels 1-20 are free.</li><li>Daily garden: the same blooms for everyone today, with a streak and a shareable emoji grid.</li><li>Beat-my-score links: send a friend the same level or daily garden with your score.</li><li>Practice: a random garden every try. Fun Pass: levels 21-30, past dailies and extra themes; Skin Pack adds six flowers.</li></ul>
|
| 60 |
+
<p>The daily challenge and levels 1-20 are free to play, with no ads and no sign-up. Later levels, bonus modes, the daily archive and colour themes come with the optional <a href="https://cybermaxtools.com/games/#pass">CyberMax Fun Pass</a> ($2.99/month, $19/year or $29.99 for life), which opens everything in every CyberMax game.</p>
|
| 61 |
<h2>Brimbloom FAQ</h2>
|
| 62 |
<h3>Is Brimbloom free?</h3><p>Yes. Levels 1-20, the daily garden and practice are free; levels 21-30 come with the CyberMax Fun Pass and skins are cosmetic extras.</p>
|
| 63 |
<h3>How many levels are there?</h3><p>30, with a new twist about every 5 levels and a one-heart challenge level every 10.</p>
|
|
|
|
| 72 |
<footer>© 2026 CyberMax · <a href="https://cybermaxtools.com/games/">Games</a> · <a href="https://cybermaxtools.com/toys/">Toys</a> · <a href="https://cybermaxtools.com/store/">Store</a> · <a href="https://cybermaxtools.com/privacy">Privacy</a> · <a href="https://cybermaxtools.com/terms">Terms</a></footer>
|
| 73 |
</div>
|
| 74 |
<script type="module" src="game.js"></script>
|
| 75 |
+
</body></html>
|
offer.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Written by scripts/games/sync_kit.py from products/fun-pass/games.json; do not edit by hand. (c) 2026 CyberMax.
|
| 2 |
+
export const OFFER = {"game": "brimbloom", "name": "Brimbloom", "price": "$2.99", "live": false, "demo": 20, "games": 10};
|
sw.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
// CyberMax game service worker: the game works offline after the first visit. Cache-first for the game's own files,
|
| 2 |
// network-first for the page itself (so updates arrive), never caches API calls or other sites. (c) 2026 CyberMax.
|
| 3 |
const GAME = 'brimbloom';
|
| 4 |
-
const CACHE = `cmx-${GAME}-
|
| 5 |
-
const FILES = ["index.html", "cmx.js", "core.js", "fun.css", "funpass.js", "game.js", "icon-192.png", "icon-512.png", "icon-maskable.png", "icon.svg", "index.html", "levels.js", "manifest.webmanifest", "skins.js", "style.css"];
|
| 6 |
|
| 7 |
self.addEventListener('install', (e) => {
|
| 8 |
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(FILES)).then(() => self.skipWaiting()));
|
|
|
|
| 1 |
// CyberMax game service worker: the game works offline after the first visit. Cache-first for the game's own files,
|
| 2 |
// network-first for the page itself (so updates arrive), never caches API calls or other sites. (c) 2026 CyberMax.
|
| 3 |
const GAME = 'brimbloom';
|
| 4 |
+
const CACHE = `cmx-${GAME}-2afadc780f`;
|
| 5 |
+
const FILES = ["index.html", "cmx.js", "core.js", "fun.css", "funpass.js", "game.js", "icon-192.png", "icon-512.png", "icon-maskable.png", "icon.svg", "index.html", "levels.js", "manifest.webmanifest", "offer.js", "skins.js", "style.css"];
|
| 6 |
|
| 7 |
self.addEventListener('install', (e) => {
|
| 8 |
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(FILES)).then(() => self.skipWaiting()));
|