CyberMax-tools commited on
Commit
cc6eefe
·
verified ·
1 Parent(s): 60e5e29

Deploy brimbloom

Browse files
Files changed (5) hide show
  1. fun.css +2 -0
  2. funpass.js +106 -29
  3. index.html +4 -4
  4. offer.js +2 -0
  5. 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 Fun Pass: ONE optional supporter pass for every CyberMax game (copied into each game's web/ folder).
2
- // Free games stay free. The pass removes the supporter nudges and unlocks extra packs, the full archive and themes.
3
- // Monthly $2.99 · Yearly $19 · Lifetime $29.99 once (Stripe Payment Links, pack revenue/packs/stripe/fun-pass).
4
- // Buy: counted redirect https://data.cybermaxtools.com/buy/st-fun-pass-<plan>?s=game-<id> -> Stripe checkout.
5
- // After checkout Stripe sends the buyer to data.cybermaxtools.com/unlock/fun-pass?session_id=..., which checks
6
- // the Checkout Session (and, for a subscription, that it is still active) and returns to /games/ with #cmx-pro=<code>.
7
- // Every game on the same site reads the same browser key, so one pass covers them all. The code on the receipt page
8
- // unlocks any other device or browser. No account, no cookies, nothing personal stored. (c) 2026 CyberMax.
 
 
 
 
 
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 = ['No supporter nudges, ever', 'Extra puzzle packs in every game', 'The full archive of past dailies', 'Custom colour themes'];
 
 
 
21
 
22
  let state = store(KEY);
 
23
  const listeners = [];
24
  let cfg = { game: 'games', name: 'CyberMax games', perks: [] };
25
 
26
- export const isPro = () => !!(state && state.code);
 
 
 
 
 
 
 
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/fun-pass?json=1&session_id=${encodeURIComponent(code)}`);
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
- } else if (ok === false) toast('That code is not an active Fun Pass.', 3500);
 
 
 
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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
- d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">CyberMax Fun Pass</p><h2>You're a supporter. Thank you!</h2>
81
- <p>The pass is on in this browser for every CyberMax game: ${perks.map(esc).join(' · ')}.</p>
82
- <p class="fp-small">Your code (paste it on another device to turn the pass on there):</p><code class="fp-code">${esc(state.code)}</code>
83
- <p class="fp-small">Manage or cancel a subscription any time from the link in your Stripe receipt email.</p></div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  return;
85
  }
86
- d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">CyberMax Fun Pass</p>
 
 
 
 
 
 
 
 
 
 
 
 
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">${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('')}</div>
90
- <p class="fp-small">Secure Stripe checkout. Works in every CyberMax game. The free daily games stay free for everyone.</p>
91
- <details><summary>Already have a pass?</summary><form class="fp-form"><input aria-label="Pass code" autocomplete="off" placeholder="Receipt code (cs_live_…) or Gumroad licence key"><button class="btn sm" type="submit">Turn on</button></form></details></div>`;
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
- el.innerHTML = `<p><b>Want more today?</b> ${esc(cfg.extra || 'The Fun Pass unlocks extra packs, the full archive and themes in every CyberMax game.')}</p>
118
- <p class="fp-row"><a class="btn sm" href="${buyUrl(best.id)}-after" rel="nofollow" data-after>Get the Fun Pass: ${esc(best.price)}${esc(best.per)}</a> <button class="btn sm ghost" type="button" data-plans>See all plans</button></p>
119
- <p class="fp-small">Secure Stripe checkout · the pass turns on as soon as you pay · 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 free game stays free.</p>
 
 
 
 
 
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 Pro perks (listed first). */
 
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
- history.replaceState(null, '', location.pathname + location.search);
147
- unlock(decodeURIComponent(m[1]));
 
 
 
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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&#x27;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> <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,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>Free to play with no ads and no sign-up. The optional <a href="https://cybermaxtools.com/games/#pass">CyberMax Fun Pass</a> ($2.99/month, $19/year or $29.99 for life) adds cosmetic extras and bonus modes 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,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
- <!-- cybermax-count:start --><script>(function(){try{var u="https://data.cybermaxtools.com/v?p="+encodeURIComponent(location.host+location.pathname)+"&r="+encodeURIComponent((document.referrer||"").split("/")[2]||"")+(function(){var q=new URLSearchParams(location.search),s=q.get("utm_source");return s?"&us="+encodeURIComponent(s)+"&uc="+encodeURIComponent(q.get("utm_campaign")||""):""})();navigator.sendBeacon?navigator.sendBeacon(u):(new Image).src=u}catch(e){}})()</script><!-- cybermax-count:end --></body></html>
 
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&#x27;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}-510a600419`;
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()));