Spaces:
Running
Running
Deploy pawfoil
Browse files- .gitattributes +2 -0
- README.md +20 -4
- cmx.js +179 -0
- core.js +62 -0
- fun.css +51 -0
- funpass.js +117 -0
- game.js +171 -0
- icon-192.png +0 -0
- icon-512.png +0 -0
- icon-maskable.png +0 -0
- icon.svg +1 -0
- index.html +62 -18
- manifest.webmanifest +47 -0
- og.png +3 -0
- sample.jpg +0 -0
- shot-phone.png +3 -0
- style.css +18 -28
- sw.js +25 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
og.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
shot-phone.png filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,10 +1,26 @@
|
|
| 1 |
---
|
| 2 |
-
title: Pawfoil
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: purple
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
|
|
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: "Pawfoil: Pet card maker"
|
| 3 |
+
emoji: 🐾
|
| 4 |
colorFrom: purple
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
license: other
|
| 9 |
+
short_description: "Make your pet a holo trading card"
|
| 10 |
+
tags:
|
| 11 |
+
- game
|
| 12 |
+
- pets
|
| 13 |
+
- trading-card
|
| 14 |
+
- image-maker
|
| 15 |
+
- html5
|
| 16 |
+
- browser-game
|
| 17 |
+
- pwa
|
| 18 |
+
- cybermax
|
| 19 |
---
|
| 20 |
+
**Pawfoil**: Make your pet a holo trading card. A free daily game from CyberMax: the same challenge for everyone each day, a shareable result, works offline and installs on your phone (no app store, no sign-up).
|
| 21 |
|
| 22 |
+
Play it full screen: https://cybermax.cybermax-tools.workers.dev/games/pawfoil/?utm_source=huggingface
|
| 23 |
+
|
| 24 |
+
The optional **CyberMax Fun Pass** ($2.99/month, $19/year or $4.99 once) adds extra packs, the full archive of past dailies and colour themes in every CyberMax game: https://cybermax.cybermax-tools.workers.dev/games/?utm_source=huggingface
|
| 25 |
+
|
| 26 |
+
More free games: https://cybermax.cybermax-tools.workers.dev/games/ · © 2026 CyberMax. All rights reserved.
|
cmx.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// CyberMax Fun kit: shared browser helpers for the CyberMax games (copied into each game's web/ folder by
|
| 2 |
+
// scripts/games/sync_kit.py, so every game stays self-contained and sellable on its own).
|
| 3 |
+
// Counter beacon (game + event + channel only: no cookies, no IDs, nothing personal), daily seed, share, PWA install.
|
| 4 |
+
// (c) 2026 CyberMax. All rights reserved.
|
| 5 |
+
|
| 6 |
+
export const SITE = 'https://cybermax.cybermax-tools.workers.dev';
|
| 7 |
+
export const API = 'https://toys.cybermax-tools.workers.dev';
|
| 8 |
+
export const EPOCH = Date.UTC(2026, 9, 2); // 2 Oct 2026 = puzzle #1 for every CyberMax daily game
|
| 9 |
+
|
| 10 |
+
/** Today's date in the player's own time zone as YYYY-MM-DD (everyone gets a new puzzle at their local midnight). */
|
| 11 |
+
export function today(d = new Date()) {
|
| 12 |
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
| 13 |
+
}
|
| 14 |
+
/** Puzzle number for a YYYY-MM-DD date (#1 = 2026-10-02). */
|
| 15 |
+
export function dayNumber(iso) {
|
| 16 |
+
const [y, m, d] = iso.split('-').map(Number);
|
| 17 |
+
return Math.round((Date.UTC(y, m - 1, d) - EPOCH) / 864e5) + 1;
|
| 18 |
+
}
|
| 19 |
+
export function dateOf(n) {
|
| 20 |
+
const t = new Date(EPOCH + (n - 1) * 864e5);
|
| 21 |
+
return t.toISOString().slice(0, 10);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/** 32-bit string hash (FNV-1a) and a small fast PRNG (mulberry32): the same seed gives the same game everywhere. */
|
| 25 |
+
export function hash(str) {
|
| 26 |
+
let h = 0x811c9dc5;
|
| 27 |
+
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 0x01000193); }
|
| 28 |
+
return h >>> 0;
|
| 29 |
+
}
|
| 30 |
+
export function rng(seed) {
|
| 31 |
+
let a = typeof seed === 'string' ? hash(seed) : seed >>> 0;
|
| 32 |
+
return function () {
|
| 33 |
+
a = (a + 0x6d2b79f5) >>> 0;
|
| 34 |
+
let t = a;
|
| 35 |
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
| 36 |
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
| 37 |
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
| 38 |
+
};
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/** Count one event (play, win, share, install, pro, cta). Never breaks the page; skipped on localhost and in tests. */
|
| 42 |
+
export function count(game, ev, ch = '') {
|
| 43 |
+
try {
|
| 44 |
+
if (typeof location === 'undefined' || /^(localhost|127\.0\.0\.1)$/.test(location.hostname) || navigator.webdriver) return;
|
| 45 |
+
const u = `${API}/e?t=${encodeURIComponent(game)}&a=${encodeURIComponent(ev)}&c=${encodeURIComponent(String(ch).toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 40))}`;
|
| 46 |
+
if (navigator.sendBeacon && navigator.sendBeacon(u)) return;
|
| 47 |
+
fetch(u, { method: 'POST', keepalive: true, mode: 'no-cors' }).catch(() => {});
|
| 48 |
+
} catch { /* never break the game */ }
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
export function store(key, val) {
|
| 52 |
+
try {
|
| 53 |
+
if (val === undefined) return JSON.parse(localStorage.getItem(key) || 'null');
|
| 54 |
+
localStorage.setItem(key, JSON.stringify(val));
|
| 55 |
+
} catch { /* private mode */ }
|
| 56 |
+
return val;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
export function toast(msg, ms = 2400) {
|
| 60 |
+
let t = document.getElementById('toast');
|
| 61 |
+
if (!t) { t = document.createElement('div'); t.id = 'toast'; t.setAttribute('role', 'status'); document.body.appendChild(t); }
|
| 62 |
+
t.textContent = msg;
|
| 63 |
+
t.classList.add('on');
|
| 64 |
+
clearTimeout(t._h);
|
| 65 |
+
t._h = setTimeout(() => t.classList.remove('on'), ms);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/** The link people share: the canonical game page with UTM tags the site view counter reads. */
|
| 69 |
+
export function shareLink(path, channel, game) {
|
| 70 |
+
const u = new URL(SITE + path);
|
| 71 |
+
u.searchParams.set('utm_source', channel);
|
| 72 |
+
u.searchParams.set('utm_medium', 'share');
|
| 73 |
+
u.searchParams.set('utm_campaign', `game-${game}`);
|
| 74 |
+
return u.toString();
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/**
|
| 78 |
+
* Share a result: native share sheet on phones (text + optional PNG card), else copy to clipboard.
|
| 79 |
+
* text = Wordle-style text block; path = the game's path on the CyberMax site; blob = optional PNG card.
|
| 80 |
+
*/
|
| 81 |
+
export async function share(game, text, path, blob, filename) {
|
| 82 |
+
const url = shareLink(path, 'share', game);
|
| 83 |
+
const full = `${text}\n${url}`;
|
| 84 |
+
if (navigator.share) {
|
| 85 |
+
try {
|
| 86 |
+
const file = blob ? new File([blob], filename || `${game}.png`, { type: 'image/png' }) : null;
|
| 87 |
+
const data = file && navigator.canShare && navigator.canShare({ files: [file] }) ? { files: [file], text: full } : { text: full };
|
| 88 |
+
await navigator.share(data);
|
| 89 |
+
count(game, 'share', 'native');
|
| 90 |
+
return 'shared';
|
| 91 |
+
} catch (e) {
|
| 92 |
+
if (e && e.name === 'AbortError') return 'cancelled';
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
try { await navigator.clipboard.writeText(full); toast('Result copied. Paste it anywhere.'); } catch { prompt('Copy your result:', full); }
|
| 96 |
+
count(game, 'share', 'copy');
|
| 97 |
+
return 'copied';
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
export function intents(game, text, path) {
|
| 101 |
+
const u = (ch) => shareLink(path, ch, game);
|
| 102 |
+
return {
|
| 103 |
+
x: `https://x.com/intent/post?text=${encodeURIComponent(text)}&url=${encodeURIComponent(u('x'))}`,
|
| 104 |
+
bsky: `https://bsky.app/intent/compose?text=${encodeURIComponent(`${text}\n${u('bsky')}`)}`,
|
| 105 |
+
whatsapp: `https://wa.me/?text=${encodeURIComponent(`${text}\n${u('whatsapp')}`)}`,
|
| 106 |
+
};
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
export function download(blob, filename) {
|
| 110 |
+
const a = document.createElement('a');
|
| 111 |
+
a.href = URL.createObjectURL(blob);
|
| 112 |
+
a.download = filename;
|
| 113 |
+
document.body.appendChild(a);
|
| 114 |
+
a.click();
|
| 115 |
+
setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 1500);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/**
|
| 119 |
+
* Installable app: registers the service worker (offline play) and shows an "Add to Home Screen" button.
|
| 120 |
+
* Chrome/Edge/Android: the real install prompt. iPhone/iPad Safari: a short Share → Add to Home Screen hint.
|
| 121 |
+
* The button only appears after the first finished round (offer(): never on first load) and never once installed.
|
| 122 |
+
*/
|
| 123 |
+
export function pwa(game, btn) {
|
| 124 |
+
if ('serviceWorker' in navigator && (location.protocol === 'https:' || location.hostname === 'localhost')) {
|
| 125 |
+
navigator.serviceWorker.register('sw.js').catch(() => {});
|
| 126 |
+
}
|
| 127 |
+
const standalone = matchMedia('(display-mode: standalone)').matches || navigator.standalone;
|
| 128 |
+
if (standalone) { count(game, 'play', 'app'); return { offer() {} }; }
|
| 129 |
+
let deferred = null;
|
| 130 |
+
let wanted = false;
|
| 131 |
+
const ios = /iphone|ipad|ipod/i.test(navigator.userAgent) && !/crios|fxios/i.test(navigator.userAgent);
|
| 132 |
+
window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferred = e; if (wanted && btn) btn.hidden = false; });
|
| 133 |
+
window.addEventListener('appinstalled', () => { count(game, 'install', 'prompt'); if (btn) btn.hidden = true; });
|
| 134 |
+
if (btn) {
|
| 135 |
+
btn.addEventListener('click', async () => {
|
| 136 |
+
if (deferred) {
|
| 137 |
+
deferred.prompt();
|
| 138 |
+
const r = await deferred.userChoice.catch(() => null);
|
| 139 |
+
if (r && r.outcome === 'accepted') btn.hidden = true;
|
| 140 |
+
deferred = null;
|
| 141 |
+
} else if (ios) {
|
| 142 |
+
toast('Tap the Share button, then "Add to Home Screen".', 5000);
|
| 143 |
+
count(game, 'install', 'ios-hint');
|
| 144 |
+
}
|
| 145 |
+
});
|
| 146 |
+
}
|
| 147 |
+
return {
|
| 148 |
+
offer() {
|
| 149 |
+
wanted = true;
|
| 150 |
+
if (btn && (deferred || ios) && !store(`cmx-install-no:${game}`)) btn.hidden = false;
|
| 151 |
+
},
|
| 152 |
+
};
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/** Rounded rectangle path for canvas cards. */
|
| 156 |
+
export function rr(ctx, x, y, w, h, r) {
|
| 157 |
+
ctx.beginPath();
|
| 158 |
+
ctx.moveTo(x + r, y);
|
| 159 |
+
ctx.arcTo(x + w, y, x + w, y + h, r);
|
| 160 |
+
ctx.arcTo(x + w, y + h, x, y + h, r);
|
| 161 |
+
ctx.arcTo(x, y + h, x, y, r);
|
| 162 |
+
ctx.arcTo(x, y, x + w, y, r);
|
| 163 |
+
ctx.closePath();
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
export const canvasBlob = (canvas) => new Promise((res) => canvas.toBlob(res, 'image/png'));
|
| 167 |
+
|
| 168 |
+
/** Theme toggle shared by every page (light/dark, remembered). */
|
| 169 |
+
export function themeToggle(btn) {
|
| 170 |
+
if (!btn) return;
|
| 171 |
+
btn.addEventListener('click', () => {
|
| 172 |
+
const r = document.documentElement;
|
| 173 |
+
const c = r.dataset.theme || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
| 174 |
+
const n = c === 'dark' ? 'light' : 'dark';
|
| 175 |
+
r.dataset.theme = n;
|
| 176 |
+
store('cm-theme-v', n);
|
| 177 |
+
try { localStorage.setItem('cm-theme', n); } catch { /* private mode */ }
|
| 178 |
+
});
|
| 179 |
+
}
|
core.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Pawfoil core: turns a pet's name + kind into a collectible card's stats, moves and rarity (no DOM).
|
| 2 |
+
// Deterministic for a given name, kind and roll number, so a re-roll is reproducible and testable.
|
| 3 |
+
// (c) 2026 CyberMax. All rights reserved.
|
| 4 |
+
import { rng } from './cmx.js';
|
| 5 |
+
|
| 6 |
+
export const KINDS = {
|
| 7 |
+
dog: { label: 'Dog', emoji: '🐶', types: ['Zoomies', 'Loyal', 'Snack', 'Chaos'] },
|
| 8 |
+
cat: { label: 'Cat', emoji: '🐱', types: ['Shadow', 'Royal', 'Chaos', 'Nap'] },
|
| 9 |
+
bunny: { label: 'Bunny', emoji: '🐰', types: ['Hop', 'Fluff', 'Snack', 'Shadow'] },
|
| 10 |
+
bird: { label: 'Bird', emoji: '🐦', types: ['Sky', 'Song', 'Chaos', 'Royal'] },
|
| 11 |
+
other: { label: 'Other', emoji: '🐾', types: ['Mystery', 'Fluff', 'Chaos', 'Nap'] },
|
| 12 |
+
};
|
| 13 |
+
export const STATS = ['Cuteness', 'Chaos', 'Snack drive', 'Nap power', 'Zoomies', 'Loyalty'];
|
| 14 |
+
export const RARITY = [
|
| 15 |
+
{ id: 'common', label: 'Common', p: 0.5, stars: 1 },
|
| 16 |
+
{ id: 'rare', label: 'Rare', p: 0.3, stars: 2 },
|
| 17 |
+
{ id: 'epic', label: 'Epic', p: 0.15, stars: 3 },
|
| 18 |
+
{ id: 'legendary', label: 'Legendary', p: 0.05, stars: 4 },
|
| 19 |
+
];
|
| 20 |
+
const MOVES = {
|
| 21 |
+
any: [['Puppy Eyes', 'Opponent forgets why they were mad.'], ['Loaf Mode', 'Becomes bread. Immune to chores.'], ['3 AM Zoomies', 'Runs laps. Wakes the house.'],
|
| 22 |
+
['Sock Heist', 'Steals one sock. Never returns it.'], ['Treat Radar', 'Detects a wrapper from 3 rooms away.'], ['Lap Claim', 'Sits on you. You can never leave.'],
|
| 23 |
+
['Dramatic Sigh', 'Guilt-trips everyone in range.'], ['Zoom Call Cameo', 'Appears on camera at the worst time.'], ['Vet Vanish', 'Disappears when the carrier comes out.'],
|
| 24 |
+
['Sunbeam Charge', 'Recharges fully in any sunny spot.'], ['Head Tilt', 'Confuses the enemy with pure cuteness.'], ['Couch Fortress', 'Holds the best seat all day.']],
|
| 25 |
+
dog: [['Tail Helicopter', 'Knocks everything off the coffee table.'], ['Doorbell Alarm', 'Barks at every delivery, forever.'], ['Fetch Loop', 'Infinite fetch. You tire first.']],
|
| 26 |
+
cat: [['Desk Sweep', 'Pushes one item off the table, slowly.'], ['Keyboard Nap', 'Types "jjjjjjjjj" into your email.'], ['Box Fit', 'If it fits, it sits.']],
|
| 27 |
+
bunny: [['Binky Burst', 'A joyful jump that dodges all attacks.'], ['Cable Snip', 'Chews one charger. Critical hit.'], ['Hay Storm', 'Covers the floor in hay.']],
|
| 28 |
+
bird: [['Morning Concert', 'Sings at sunrise. No snooze.'], ['Shoulder Ride', 'Takes the high ground.'], ['Mimic', 'Copies the microwave beep perfectly.']],
|
| 29 |
+
other: [['Mystery Stare', 'Nobody knows what it is thinking.'], ['Tiny Speedrun', 'Escapes the enclosure in record time.'], ['Night Shift', 'Most active when you sleep.']],
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
export function cleanPetName(s) {
|
| 33 |
+
return String(s || '').normalize('NFKC').replace(/[^\p{L}\p{N} '.-]/gu, '').replace(/\s+/g, ' ').trim().slice(0, 18);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/** Card data for a pet. roll = 0 for the first card, 1, 2, … for re-rolls. pick() chooses rarity unless forced (Fun Pass). */
|
| 37 |
+
export function makeCard(name, kind = 'dog', roll = 0, forceRarity = null) {
|
| 38 |
+
const k = KINDS[kind] ? kind : 'other';
|
| 39 |
+
const r = rng(`pawfoil:${cleanPetName(name).toLowerCase()}:${k}:${roll}`);
|
| 40 |
+
let x = r(), rarity = RARITY[0];
|
| 41 |
+
for (const t of RARITY) { if (x < t.p) { rarity = t; break; } x -= t.p; }
|
| 42 |
+
if (forceRarity) rarity = RARITY.find((t) => t.id === forceRarity) || rarity;
|
| 43 |
+
const boost = { common: 0, rare: 6, epic: 12, legendary: 20 }[rarity.id];
|
| 44 |
+
const stats = STATS.map((s) => ({ s, v: Math.min(99, 35 + Math.floor(r() * 58) + boost) }));
|
| 45 |
+
stats[0].v = Math.max(stats[0].v, 88); // every pet is very cute; that is the one fixed fact
|
| 46 |
+
const pool = [...MOVES[k], ...MOVES.any];
|
| 47 |
+
const m1 = pool.splice(Math.floor(r() * MOVES[k].length), 1)[0];
|
| 48 |
+
const m2 = pool.splice(Math.floor(r() * pool.length), 1)[0];
|
| 49 |
+
const type = KINDS[k].types[Math.floor(r() * KINDS[k].types.length)];
|
| 50 |
+
const hp = 60 + Math.floor(r() * 9) * 10 + boost * 2;
|
| 51 |
+
const power = Math.round(stats.reduce((a, b) => a + b.v, 0) / stats.length);
|
| 52 |
+
const number = 1 + Math.floor(r() * 999);
|
| 53 |
+
return { name: cleanPetName(name) || 'Mystery Pet', kind: k, kindLabel: KINDS[k].label, emoji: KINDS[k].emoji, type, rarity: rarity.id, rarityLabel: rarity.label, stars: rarity.stars,
|
| 54 |
+
hp, power, stats, moves: [{ name: m1[0], text: m1[1], dmg: 10 * (2 + Math.floor(r() * 7)) }, { name: m2[0], text: m2[1], dmg: 10 * (3 + Math.floor(r() * 8)) }], number };
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
export const FRAMES = {
|
| 58 |
+
common: { a: '#E9E4D8', b: '#C9BFA8', ink: '#11141B', foil: false },
|
| 59 |
+
rare: { a: '#BFE3FF', b: '#5BA8E8', ink: '#0B2238', foil: false },
|
| 60 |
+
epic: { a: '#E5C8FF', b: '#9B5BE8', ink: '#1E0B38', foil: true },
|
| 61 |
+
legendary: { a: '#FFE9A8', b: '#E8A93A', ink: '#2B1A00', foil: true },
|
| 62 |
+
};
|
fun.css
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* CyberMax Fun kit: shared look for CyberMax games (copied into each game). Mobile first, light + dark. (c) 2026 CyberMax. */
|
| 2 |
+
:root{--bg:#F6F3EC;--card:#fff;--ink:#11141B;--muted:#565C6B;--line:#E2DCD0;--chip:#ECE6DA;--acc:#FF6A3D;--acc-ink:#11141B;--ok:#0B7F5E;--bad:#C2361B;--shadow:0 1px 2px rgba(17,20,27,.06),0 10px 30px rgba(17,20,27,.08);color-scheme:light}
|
| 3 |
+
@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--bg:#0F1218;--card:#181C25;--ink:#EDEAE3;--muted:#A6ACB9;--line:#2A303C;--chip:#222734;--ok:#2FD3A0;--bad:#FF7A5C;--shadow:0 1px 2px rgba(0,0,0,.3),0 10px 30px rgba(0,0,0,.3);color-scheme:dark}}
|
| 4 |
+
:root[data-theme=dark]{--bg:#0F1218;--card:#181C25;--ink:#EDEAE3;--muted:#A6ACB9;--line:#2A303C;--chip:#222734;--ok:#2FD3A0;--bad:#FF7A5C;--shadow:0 1px 2px rgba(0,0,0,.3),0 10px 30px rgba(0,0,0,.3);color-scheme:dark}
|
| 5 |
+
*{box-sizing:border-box}[hidden]{display:none!important}html{-webkit-text-size-adjust:100%}
|
| 6 |
+
body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;overflow-x:hidden}
|
| 7 |
+
a{color:inherit}img,canvas{max-width:100%}
|
| 8 |
+
.wrap{max-width:760px;margin:0 auto;padding:0 16px}
|
| 9 |
+
.top{display:flex;align-items:center;gap:6px;padding:12px 0}
|
| 10 |
+
.top .brand{display:flex;align-items:center;gap:8px;font-weight:800;text-decoration:none;font-size:18px;margin-right:auto}.top .brand svg{width:28px;height:28px}
|
| 11 |
+
.top nav a,.top .icon{color:var(--muted);text-decoration:none;padding:8px 10px;border-radius:10px;font-size:15px;background:none;border:0;font-family:inherit;cursor:pointer;min-height:40px}
|
| 12 |
+
.top nav a:hover,.top .icon:hover{color:var(--ink);background:var(--chip)}
|
| 13 |
+
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:48px;padding:10px 18px;border-radius:12px;border:0;background:var(--acc);color:var(--acc-ink);font:inherit;font-weight:800;font-size:16px;cursor:pointer;text-decoration:none;touch-action:manipulation}
|
| 14 |
+
.btn:hover{filter:brightness(1.06)}.btn.ghost{background:transparent;color:var(--ink);border:1.5px solid var(--line)}.btn.sm{min-height:40px;padding:6px 14px;font-size:15px}
|
| 15 |
+
.btn.pass{background:linear-gradient(135deg,#FFB547,#FF6A3D);color:#11141B}
|
| 16 |
+
.row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
|
| 17 |
+
.chips{display:flex;gap:6px;flex-wrap:wrap}
|
| 18 |
+
.chip{border:1.5px solid var(--line);background:var(--card);color:var(--ink);border-radius:999px;padding:6px 12px;font:inherit;font-size:14px;font-weight:700;cursor:pointer;min-height:36px}
|
| 19 |
+
.chip[aria-pressed=true]{background:var(--ink);color:var(--bg);border-color:var(--ink)}.chip .lock{opacity:.7;font-size:12px}
|
| 20 |
+
.panel{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:16px;box-shadow:var(--shadow)}
|
| 21 |
+
.muted{color:var(--muted)}.small{font-size:14px}
|
| 22 |
+
h1{font-size:clamp(26px,6vw,40px);line-height:1.08;letter-spacing:-.02em;margin:0 0 6px;text-wrap:balance}
|
| 23 |
+
h2{font-size:21px;letter-spacing:-.01em;margin:30px 0 8px}
|
| 24 |
+
.seo h3{font-size:17px;margin:18px 0 4px}.seo p,.seo li{color:var(--muted)}
|
| 25 |
+
.more{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin:12px 0}
|
| 26 |
+
.more a{display:block;padding:12px 14px;border:1px solid var(--line);border-radius:14px;background:var(--card);text-decoration:none}.more a b{display:block}.more a span{color:var(--muted);font-size:14px}
|
| 27 |
+
footer{color:var(--muted);font-size:14px;padding:26px 0 40px;border-top:1px solid var(--line);margin-top:36px}
|
| 28 |
+
#toast{position:fixed;left:50%;bottom:22px;transform:translate(-50%,40px);opacity:0;background:var(--ink);color:var(--bg);padding:12px 16px;border-radius:12px;font-weight:700;transition:.25s;z-index:99;max-width:calc(100% - 32px);text-align:center;pointer-events:none}
|
| 29 |
+
#toast.on{transform:translate(-50%,0);opacity:1}
|
| 30 |
+
.nudge{display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap;border:1.5px dashed var(--line);border-radius:14px;padding:10px 12px;margin-top:12px;font-size:15px}
|
| 31 |
+
html.cmx-pro .nudge{display:none}
|
| 32 |
+
.pro-badge{display:none;font-size:12px;font-weight:800;background:linear-gradient(135deg,#FFB547,#FF6A3D);color:#11141B;border-radius:999px;padding:2px 8px;vertical-align:middle}
|
| 33 |
+
html.cmx-pro .pro-badge{display:inline-block}
|
| 34 |
+
dialog.funpass{border:0;padding:0;border-radius:20px;max-width:min(520px,calc(100% - 24px));width:100%;background:var(--card);color:var(--ink);box-shadow:0 20px 60px rgba(0,0,0,.35)}
|
| 35 |
+
dialog.funpass::backdrop{background:rgba(10,12,18,.55)}
|
| 36 |
+
.fp{padding:22px 20px 18px;position:relative}.fp h2{margin:2px 0 10px;font-size:24px}
|
| 37 |
+
.fp-x{position:absolute;right:10px;top:8px;border:0;background:none;color:var(--muted);font-size:28px;cursor:pointer;width:44px;height:44px}
|
| 38 |
+
.fp-kick{margin:0;color:var(--acc);font-weight:800;font-size:13px;letter-spacing:.06em;text-transform:uppercase}
|
| 39 |
+
.fp-perks{margin:0 0 14px;padding-left:20px}.fp-perks li{margin:2px 0}
|
| 40 |
+
.fp-plans{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
|
| 41 |
+
.fp-plan{position:relative;display:flex;flex-direction:column;gap:2px;padding:14px 10px 12px;border:1.5px solid var(--line);border-radius:14px;text-decoration:none;text-align:center;background:var(--bg)}
|
| 42 |
+
.fp-plan:hover{border-color:var(--acc)}.fp-plan.best{border-color:var(--acc);box-shadow:0 0 0 2px var(--acc) inset}
|
| 43 |
+
.fp-plan i{position:absolute;top:-10px;left:50%;transform:translateX(-50%);background:var(--acc);color:#11141B;font-style:normal;font-size:11px;font-weight:800;padding:2px 8px;border-radius:999px;white-space:nowrap}
|
| 44 |
+
.fp-plan b{font-size:14px}.fp-price{font-size:22px;font-weight:900}.fp-price small{font-size:12px;font-weight:600;color:var(--muted)}.fp-note{font-size:12px;color:var(--muted)}
|
| 45 |
+
.fp-small{font-size:13px;color:var(--muted);margin:10px 0 4px}
|
| 46 |
+
.fp-code{display:block;user-select:all;word-break:break-all;background:var(--chip);padding:8px;border-radius:8px;font-size:12px}
|
| 47 |
+
.fp details{margin-top:6px;font-size:14px}.fp-form{display:flex;gap:6px;margin-top:8px}.fp-form input{flex:1;min-width:0;font:inherit;font-size:14px;padding:8px 10px;border-radius:10px;border:1.5px solid var(--line);background:var(--bg);color:var(--ink)}
|
| 48 |
+
@media (max-width:420px){.fp-plans{grid-template-columns:1fr}.fp-plan{flex-direction:row;align-items:center;justify-content:space-between;text-align:left;padding:12px}.fp-plan i{left:auto;right:10px;transform:none}}
|
| 49 |
+
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
| 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}}
|
funpass.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 · Unlock all $4.99 once (Stripe Payment Links, pack revenue/packs/stripe/fun-pass).
|
| 4 |
+
// Buy: counted redirect https://data.cybermax-tools.workers.dev/buy/st-fun-pass-<plan>?s=game-<id> -> Stripe checkout.
|
| 5 |
+
// After checkout Stripe sends the buyer to data.cybermax-tools.workers.dev/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.cybermax-tools.workers.dev';
|
| 12 |
+
const KEY = 'cmx-pro:fun-pass';
|
| 13 |
+
const SID = /^cs_(live|test)_[A-Za-z0-9]{10,200}$/;
|
| 14 |
+
const RECHECK = 3 * 864e5; // subscriptions are re-checked every 3 days; a cancelled pass turns off by itself
|
| 15 |
+
export const PLANS = [
|
| 16 |
+
{ id: 'monthly', label: 'Monthly', price: '$2.99', per: '/month', note: 'Cancel any time' },
|
| 17 |
+
{ id: 'yearly', label: 'Yearly', price: '$19', per: '/year', note: 'Save 47%', best: true },
|
| 18 |
+
{ id: 'unlock', label: 'Unlock all', price: '$4.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) {
|
| 44 |
+
state = store(KEY, { code, at: new Date().toISOString(), checked: Date.now() });
|
| 45 |
+
count(cfg.game, 'pro', 'unlocked');
|
| 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 |
+
}
|
| 53 |
+
|
| 54 |
+
async function recheck() {
|
| 55 |
+
if (!isPro() || Date.now() - (state.checked || 0) < RECHECK) return;
|
| 56 |
+
const ok = await check(state.code);
|
| 57 |
+
if (ok === false) { state = null; try { localStorage.removeItem(KEY); } catch { /* */ } emit(); render(); toast('Your Fun Pass has ended. Everything free is still here.', 4000); }
|
| 58 |
+
else if (ok) state = store(KEY, { ...state, checked: Date.now() });
|
| 59 |
+
}
|
| 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');
|
| 66 |
+
if (!d) {
|
| 67 |
+
d = document.createElement('dialog');
|
| 68 |
+
d.id = 'funpass';
|
| 69 |
+
d.className = 'funpass';
|
| 70 |
+
document.body.appendChild(d);
|
| 71 |
+
d.addEventListener('click', (e) => { if (e.target === d || e.target.closest('[data-close]')) d.close(); });
|
| 72 |
+
}
|
| 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="Code from your receipt page (cs_live_…)"><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. It starts with cs_live_.', 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). */
|
| 101 |
+
export function open(from = 'button') {
|
| 102 |
+
render();
|
| 103 |
+
const d = dialog();
|
| 104 |
+
if (d.showModal) d.showModal(); else d.setAttribute('open', '');
|
| 105 |
+
count(cfg.game, 'cta', `pass-open-${from}`);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/** Call once per page: game id, display name and the game's own Pro perks (listed first). */
|
| 109 |
+
export function init(options) {
|
| 110 |
+
cfg = { ...cfg, ...options };
|
| 111 |
+
const m = /[#&]cmx-pro=([^&]+)/.exec(location.hash || '');
|
| 112 |
+
if (m && SID.test(decodeURIComponent(m[1]))) {
|
| 113 |
+
history.replaceState(null, '', location.pathname + location.search);
|
| 114 |
+
unlock(decodeURIComponent(m[1]));
|
| 115 |
+
} else recheck();
|
| 116 |
+
emit();
|
| 117 |
+
}
|
game.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Pawfoil: photo + name -> collectible card on a canvas, holo tilt preview, HD PNG download, share. (c) 2026 CyberMax.
|
| 2 |
+
// The photo is read with createImageBitmap and drawn locally; it is never uploaded.
|
| 3 |
+
import { makeCard, KINDS, RARITY, FRAMES, cleanPetName } from './core.js';
|
| 4 |
+
import { count, store, toast, share, intents, download, canvasBlob, pwa, rr, themeToggle } from './cmx.js';
|
| 5 |
+
import * as pass from './funpass.js';
|
| 6 |
+
|
| 7 |
+
const GAME = 'pawfoil';
|
| 8 |
+
const PATH = '/games/pawfoil/';
|
| 9 |
+
const $ = (id) => document.getElementById(id);
|
| 10 |
+
const cv = $('card');
|
| 11 |
+
const g = cv.getContext('2d');
|
| 12 |
+
const W = 750, H = 1050, ART = { x: 52, y: 128, w: 646, h: 430 };
|
| 13 |
+
let photo = null, kind = store('pawfoil:kind') || 'dog', roll = 0, forced = null, ox = 0, oy = 0, zoom = 1, made = false;
|
| 14 |
+
|
| 15 |
+
pass.init({ game: GAME, name: 'Pawfoil', perks: ['Pick any frame: Legendary gold holo on demand', 'Hide the CyberMax line on your cards'] });
|
| 16 |
+
const install = pwa(GAME, $('install'));
|
| 17 |
+
themeToggle($('theme'));
|
| 18 |
+
|
| 19 |
+
// ------------------------------------------------------------------ controls
|
| 20 |
+
function renderKinds() {
|
| 21 |
+
const el = $('kinds'); el.innerHTML = '';
|
| 22 |
+
for (const [id, k] of Object.entries(KINDS)) {
|
| 23 |
+
const b = document.createElement('button');
|
| 24 |
+
b.type = 'button'; b.className = 'chip'; b.textContent = `${k.emoji} ${k.label}`; b.setAttribute('aria-pressed', String(id === kind));
|
| 25 |
+
b.onclick = () => { kind = id; store('pawfoil:kind', id); roll = 0; renderKinds(); draw(); };
|
| 26 |
+
el.appendChild(b);
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
function renderFrames() {
|
| 30 |
+
const el = $('frames'); el.innerHTML = '';
|
| 31 |
+
for (const r of [{ id: null, label: 'Roll' }, ...RARITY]) {
|
| 32 |
+
const b = document.createElement('button');
|
| 33 |
+
b.type = 'button'; b.className = 'chip'; b.setAttribute('aria-pressed', String(forced === r.id));
|
| 34 |
+
b.innerHTML = `${r.label}${r.id && !pass.isPro() ? ' <span class="lock">🔒</span>' : ''}`;
|
| 35 |
+
b.onclick = () => { if (r.id && !pass.isPro()) { pass.open(`frame-${r.id}`); return; } forced = r.id; renderFrames(); draw(); };
|
| 36 |
+
el.appendChild(b);
|
| 37 |
+
}
|
| 38 |
+
$('clean').disabled = false;
|
| 39 |
+
}
|
| 40 |
+
$('clean').onchange = (e) => { if (e.target.checked && !pass.isPro()) { e.target.checked = false; pass.open('clean'); return; } draw(); };
|
| 41 |
+
$('name').addEventListener('input', () => { roll = 0; draw(); });
|
| 42 |
+
$('zoom').addEventListener('input', (e) => { zoom = Number(e.target.value); draw(); });
|
| 43 |
+
$('roll').onclick = () => {
|
| 44 |
+
roll++; draw();
|
| 45 |
+
const c = card();
|
| 46 |
+
$('rolls').textContent = `Roll ${roll + 1}: ${c.rarityLabel}${c.rarity === 'legendary' ? '! 🌟' : ''}`;
|
| 47 |
+
if (navigator.vibrate) try { navigator.vibrate(c.rarity === 'legendary' ? [20, 40, 60] : 10); } catch { /* */ }
|
| 48 |
+
};
|
| 49 |
+
async function loadPhoto(blob, from) {
|
| 50 |
+
try {
|
| 51 |
+
photo = await createImageBitmap(blob);
|
| 52 |
+
ox = 0; oy = 0; zoom = 1; $('zoom').value = 1;
|
| 53 |
+
if (!made) { made = true; count(GAME, 'play', from); }
|
| 54 |
+
draw();
|
| 55 |
+
} catch { toast('Could not read that image. Try a JPG or PNG.'); }
|
| 56 |
+
}
|
| 57 |
+
$('file').onchange = (e) => { const f = e.target.files[0]; if (f) loadPhoto(f, 'photo'); };
|
| 58 |
+
$('sample').onclick = async () => { const r = await fetch('sample.jpg'); loadPhoto(await r.blob(), 'sample'); if (!$('name').value) $('name').value = 'Biscuit'; draw(); };
|
| 59 |
+
|
| 60 |
+
// drag to move the photo inside the art box
|
| 61 |
+
let drag = null;
|
| 62 |
+
cv.addEventListener('pointerdown', (e) => { if (!photo) return; drag = { x: e.clientX, y: e.clientY, ox, oy }; cv.setPointerCapture(e.pointerId); });
|
| 63 |
+
cv.addEventListener('pointermove', (e) => {
|
| 64 |
+
if (!drag) return;
|
| 65 |
+
const s = W / cv.getBoundingClientRect().width;
|
| 66 |
+
ox = drag.ox + (e.clientX - drag.x) * s; oy = drag.oy + (e.clientY - drag.y) * s; draw();
|
| 67 |
+
});
|
| 68 |
+
cv.addEventListener('pointerup', () => { drag = null; });
|
| 69 |
+
|
| 70 |
+
// holo tilt (mouse or device tilt)
|
| 71 |
+
const tilt = $('tilt');
|
| 72 |
+
function setTilt(rx, ry) { tilt.style.transform = `perspective(900px) rotateX(${rx}deg) rotateY(${ry}deg)`; $('holo').style.backgroundPosition = `${50 + ry * 4}% ${50 - rx * 4}%`; }
|
| 73 |
+
tilt.addEventListener('pointermove', (e) => { if (drag) return; const r = tilt.getBoundingClientRect(); setTilt(((e.clientY - r.top) / r.height - 0.5) * -14, ((e.clientX - r.left) / r.width - 0.5) * 14); });
|
| 74 |
+
tilt.addEventListener('pointerleave', () => setTilt(0, 0));
|
| 75 |
+
window.addEventListener('deviceorientation', (e) => { if (e.beta == null) return; setTilt(Math.max(-12, Math.min(12, (e.beta - 40) / 3)), Math.max(-12, Math.min(12, e.gamma / 3))); });
|
| 76 |
+
|
| 77 |
+
// ------------------------------------------------------------------ drawing
|
| 78 |
+
const card = () => makeCard($('name').value || 'Mystery Pet', kind, roll, forced);
|
| 79 |
+
function wrap(text, x, y, maxW, lh) {
|
| 80 |
+
const words = text.split(' '); let line = '';
|
| 81 |
+
for (const w of words) { if (g.measureText(`${line}${w} `).width > maxW && line) { g.fillText(line.trim(), x, y); y += lh; line = ''; } line += `${w} `; }
|
| 82 |
+
g.fillText(line.trim(), x, y);
|
| 83 |
+
}
|
| 84 |
+
function draw() {
|
| 85 |
+
const c = card(), F = FRAMES[c.rarity];
|
| 86 |
+
tilt.classList.toggle('foil', F.foil);
|
| 87 |
+
g.clearRect(0, 0, W, H);
|
| 88 |
+
// frame
|
| 89 |
+
const grd = g.createLinearGradient(0, 0, W, H); grd.addColorStop(0, F.a); grd.addColorStop(1, F.b);
|
| 90 |
+
g.fillStyle = grd; rr(g, 0, 0, W, H, 40); g.fill();
|
| 91 |
+
g.fillStyle = 'rgba(255,255,255,.55)'; rr(g, 24, 24, W - 48, H - 48, 28); g.fill();
|
| 92 |
+
// header
|
| 93 |
+
g.fillStyle = F.ink; g.font = '900 52px system-ui, sans-serif'; g.textBaseline = 'alphabetic';
|
| 94 |
+
g.fillText(c.name.slice(0, 16), 52, 96);
|
| 95 |
+
g.textAlign = 'right'; g.font = '800 26px system-ui, sans-serif'; g.fillText('HP', W - 150, 96);
|
| 96 |
+
g.font = '900 52px system-ui, sans-serif'; g.fillText(String(c.hp), W - 52, 96); g.textAlign = 'left';
|
| 97 |
+
// art
|
| 98 |
+
g.save(); rr(g, ART.x, ART.y, ART.w, ART.h, 18); g.clip();
|
| 99 |
+
g.fillStyle = '#1B1430'; g.fillRect(ART.x, ART.y, ART.w, ART.h);
|
| 100 |
+
if (photo) {
|
| 101 |
+
const s = Math.max(ART.w / photo.width, ART.h / photo.height) * zoom;
|
| 102 |
+
const dw = photo.width * s, dh = photo.height * s;
|
| 103 |
+
g.drawImage(photo, ART.x + (ART.w - dw) / 2 + ox, ART.y + (ART.h - dh) / 2 + oy, dw, dh);
|
| 104 |
+
} else {
|
| 105 |
+
g.fillStyle = '#ffffff22'; g.font = '200px system-ui, "Apple Color Emoji", "Segoe UI Emoji", sans-serif'; g.textAlign = 'center';
|
| 106 |
+
g.fillText(KINDS[c.kind].emoji, W / 2, ART.y + 280); g.font = '700 28px system-ui, sans-serif'; g.fillStyle = '#ffffffaa';
|
| 107 |
+
g.fillText('Add a photo of your pet', W / 2, ART.y + 380); g.textAlign = 'left';
|
| 108 |
+
}
|
| 109 |
+
if (F.foil) { // printed holo sheen
|
| 110 |
+
const h = g.createLinearGradient(ART.x, ART.y, ART.x + ART.w, ART.y + ART.h);
|
| 111 |
+
['rgba(255,0,170,0)', 'rgba(255,0,170,.18)', 'rgba(0,255,230,.2)', 'rgba(255,230,0,.18)', 'rgba(255,230,0,0)'].forEach((col, i) => h.addColorStop(i / 4, col));
|
| 112 |
+
g.globalCompositeOperation = 'screen'; g.fillStyle = h; g.fillRect(ART.x, ART.y, ART.w, ART.h); g.globalCompositeOperation = 'source-over';
|
| 113 |
+
}
|
| 114 |
+
g.restore();
|
| 115 |
+
g.strokeStyle = F.b; g.lineWidth = 6; rr(g, ART.x, ART.y, ART.w, ART.h, 18); g.stroke();
|
| 116 |
+
// type banner
|
| 117 |
+
g.fillStyle = F.ink; rr(g, 52, 576, W - 104, 50, 25); g.fill();
|
| 118 |
+
g.fillStyle = F.a; g.font = '800 26px system-ui, sans-serif';
|
| 119 |
+
g.fillText(`${c.emoji} ${c.type}-type ${c.kindLabel} · ${'★'.repeat(c.stars)} ${c.rarityLabel}`, 76, 610);
|
| 120 |
+
// moves
|
| 121 |
+
g.fillStyle = F.ink;
|
| 122 |
+
c.moves.forEach((m, i) => {
|
| 123 |
+
const y = 680 + i * 104;
|
| 124 |
+
g.font = '900 32px system-ui, sans-serif'; g.fillText(m.name, 60, y);
|
| 125 |
+
g.textAlign = 'right'; g.fillText(String(m.dmg), W - 60, y); g.textAlign = 'left';
|
| 126 |
+
g.font = '500 23px system-ui, sans-serif'; g.globalAlpha = 0.85; wrap(m.text, 60, y + 32, W - 140, 28); g.globalAlpha = 1;
|
| 127 |
+
});
|
| 128 |
+
// stats
|
| 129 |
+
c.stats.forEach((st, i) => {
|
| 130 |
+
const col = i % 3, row = Math.floor(i / 3), x = 60 + col * 214, y = 892 + row * 50;
|
| 131 |
+
g.font = '700 19px system-ui, sans-serif'; g.fillText(st.s, x, y);
|
| 132 |
+
g.textAlign = 'right'; g.font = '900 19px system-ui, sans-serif'; g.fillText(String(st.v), x + 180, y); g.textAlign = 'left';
|
| 133 |
+
g.fillStyle = 'rgba(0,0,0,.12)'; rr(g, x, y + 8, 180, 12, 6); g.fill();
|
| 134 |
+
g.globalAlpha = 0.8; g.fillStyle = F.ink; rr(g, x, y + 8, 180 * st.v / 99, 12, 6); g.fill(); g.globalAlpha = 1;
|
| 135 |
+
});
|
| 136 |
+
// footer
|
| 137 |
+
g.font = '800 18px system-ui, sans-serif'; g.globalAlpha = 0.75;
|
| 138 |
+
g.fillText(`#${String(c.number).padStart(3, '0')}/999`, 60, H - 48);
|
| 139 |
+
g.textAlign = 'right'; g.fillText(`Power ${c.power}`, W - 60, H - 48); g.textAlign = 'left';
|
| 140 |
+
if (!($('clean').checked && pass.isPro())) { g.font = '600 15px system-ui, sans-serif'; g.textAlign = 'center'; g.fillText('Made with Pawfoil · cybermax.cybermax-tools.workers.dev/games/pawfoil', W / 2, H - 22); g.textAlign = 'left'; }
|
| 141 |
+
g.globalAlpha = 1;
|
| 142 |
+
const text = `Meet ${c.name}: a ${c.rarityLabel} ${c.type}-type ${c.kindLabel} with ${c.hp} HP 🐾 Make your pet's card:`;
|
| 143 |
+
const it = intents(GAME, text, PATH);
|
| 144 |
+
$('sx').href = it.x; $('sb').href = it.bsky; $('sw').href = it.whatsapp;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
const fname = () => `pawfoil-${cleanPetName($('name').value || 'pet').toLowerCase().replace(/\s+/g, '-')}.png`;
|
| 148 |
+
$('dl').onclick = async () => {
|
| 149 |
+
if (!photo) { toast('Add a photo first (or try the sample pet).'); return; }
|
| 150 |
+
download(await canvasBlob(cv), fname()); count(GAME, 'win', 'download'); count(GAME, 'share', 'png'); install.offer();
|
| 151 |
+
};
|
| 152 |
+
$('share').onclick = async () => {
|
| 153 |
+
if (!photo) { toast('Add a photo first (or try the sample pet).'); return; }
|
| 154 |
+
const c = card();
|
| 155 |
+
await share(GAME, `Meet ${c.name}: a ${c.rarityLabel} ${c.type}-type ${c.kindLabel} with ${c.hp} HP 🐾`, PATH, await canvasBlob(cv), fname());
|
| 156 |
+
count(GAME, 'win', 'share'); install.offer();
|
| 157 |
+
};
|
| 158 |
+
['sx', 'sb', 'sw'].forEach((id) => $(id).addEventListener('click', () => count(GAME, 'share', { sx: 'x', sb: 'bsky', sw: 'whatsapp' }[id])));
|
| 159 |
+
$('pass-btn').onclick = () => pass.open('header');
|
| 160 |
+
document.addEventListener('click', (e) => { const b = e.target.closest('[data-pass]'); if (b) pass.open(b.dataset.pass); });
|
| 161 |
+
pass.onChange((pro) => { if (!pro) { forced = null; $('clean').checked = false; } renderFrames(); draw(); });
|
| 162 |
+
|
| 163 |
+
// Screenshot helper for scripts/games/render_assets.mjs and studio shots (no effect on normal play).
|
| 164 |
+
window.__cmxShot = async (stage = 1) => {
|
| 165 |
+
await new Promise((ok) => { $('sample').click(); setTimeout(ok, 600); });
|
| 166 |
+
$('name').value = stage === 2 ? 'Biscuit' : 'Biscuit';
|
| 167 |
+
roll = 0; forced = stage === 2 ? 'legendary' : null; draw(); forced = null;
|
| 168 |
+
};
|
| 169 |
+
|
| 170 |
+
renderKinds();
|
| 171 |
+
draw();
|
icon-192.png
ADDED
|
|
icon-512.png
ADDED
|
|
icon-maskable.png
ADDED
|
|
icon.svg
ADDED
|
|
index.html
CHANGED
|
@@ -1,19 +1,63 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
| 3 |
+
<title>Pawfoil: free pet trading card maker, holo cards in seconds | CyberMax</title>
|
| 4 |
+
<meta name="description" content="Turn your pet's photo into a shiny collectible trading card with funny stats and moves. Free HD download, no sign-up, no watermark grab: your photo never leaves your phone.">
|
| 5 |
+
<link rel="canonical" href="https://cybermax.cybermax-tools.workers.dev/games/pawfoil/">
|
| 6 |
+
<meta property="og:type" content="website"><meta property="og:site_name" content="CyberMax">
|
| 7 |
+
<meta property="og:title" content="Pawfoil: make your pet a holo trading card">
|
| 8 |
+
<meta property="og:description" content="Photo in, collectible card out: stats, two funny moves, a rarity roll. Free HD download.">
|
| 9 |
+
<meta property="og:url" content="https://cybermax.cybermax-tools.workers.dev/games/pawfoil/">
|
| 10 |
+
<meta property="og:image" content="https://cybermax.cybermax-tools.workers.dev/games/pawfoil/og.png"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630">
|
| 11 |
+
<meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="https://cybermax.cybermax-tools.workers.dev/games/pawfoil/og.png">
|
| 12 |
+
<meta name="theme-color" content="#1B1430">
|
| 13 |
+
<link rel="manifest" href="manifest.webmanifest">
|
| 14 |
+
<link rel="icon" href="icon.svg" type="image/svg+xml"><link rel="apple-touch-icon" href="icon-192.png">
|
| 15 |
+
<meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-title" content="Pawfoil">
|
| 16 |
+
<link rel="stylesheet" href="fun.css"><link rel="stylesheet" href="style.css">
|
| 17 |
+
<script>try{var t=localStorage.getItem('cm-theme');if(t)document.documentElement.dataset.theme=t}catch(e){}</script>
|
| 18 |
+
<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebApplication","name":"Pawfoil","url":"https://cybermax.cybermax-tools.workers.dev/games/pawfoil/","image":"https://cybermax.cybermax-tools.workers.dev/games/pawfoil/og.png","description":"Free pet trading card maker: your pet's photo becomes a collectible card with stats, moves and a rarity roll. Runs in the browser; photos never leave the device.","applicationCategory":"MultimediaApplication","operatingSystem":"Any","publisher":{"@type":"Organization","name":"CyberMax","url":"https://cybermax.cybermax-tools.workers.dev/"},"offers":[{"@type":"Offer","price":"0","priceCurrency":"USD","description":"Free HD card"},{"@type":"Offer","price":"2.99","priceCurrency":"USD","description":"CyberMax Fun Pass, monthly"}]}</script>
|
| 19 |
+
</head><body>
|
| 20 |
+
<div class="wrap wide">
|
| 21 |
+
<header class="top"><a class="brand" href="https://cybermax.cybermax-tools.workers.dev/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>
|
| 22 |
+
<nav><a href="https://cybermax.cybermax-tools.workers.dev/games/">Games</a></nav><button class="icon" id="theme" type="button" aria-label="Switch light or dark">◐</button></header>
|
| 23 |
+
<main>
|
| 24 |
+
<h1>Make your pet a holo trading card <span class="pro-badge">PASS</span></h1>
|
| 25 |
+
<p class="muted lede">Add a photo, type a name, roll for rarity. Stats and moves are made for your pet. Free HD download; your photo never leaves this device.</p>
|
| 26 |
+
<div class="maker">
|
| 27 |
+
<section class="controls panel">
|
| 28 |
+
<label class="photo"><input type="file" id="file" accept="image/*"><span class="btn">📷 Choose a photo</span></label>
|
| 29 |
+
<button class="btn ghost sm" id="sample" type="button">Try a sample pet (AI image)</button>
|
| 30 |
+
<label class="field">Pet's name <input id="name" maxlength="18" placeholder="e.g. Biscuit" autocomplete="off"></label>
|
| 31 |
+
<div class="chips" id="kinds" role="group" aria-label="Kind of pet"></div>
|
| 32 |
+
<div class="row adj"><label>Zoom <input id="zoom" type="range" min="1" max="3" step="0.01" value="1"></label><span class="muted small">Drag the photo on the card to move it.</span></div>
|
| 33 |
+
<div class="row"><button class="btn" id="roll" type="button">🎲 Roll again</button><span class="muted small" id="rolls"></span></div>
|
| 34 |
+
<div class="pro-box"><b>Fun Pass extras</b>
|
| 35 |
+
<div class="chips" id="frames" role="group" aria-label="Pick a frame"></div>
|
| 36 |
+
<label class="small"><input type="checkbox" id="clean"> Hide the CyberMax line on the card</label>
|
| 37 |
+
</div>
|
| 38 |
+
</section>
|
| 39 |
+
<section class="preview">
|
| 40 |
+
<div class="tilt" id="tilt"><canvas id="card" width="750" height="1050" aria-label="Your pet's trading card"></canvas><div class="holo" id="holo"></div></div>
|
| 41 |
+
<div class="row share"><button class="btn" id="dl" type="button">⬇ Download HD card</button><button class="btn ghost" id="share" type="button">Share</button><a class="btn ghost sm" id="sx" target="_blank" rel="noopener">X</a><a class="btn ghost sm" id="sb" target="_blank" rel="noopener">Bluesky</a><a class="btn ghost sm" id="sw" target="_blank" rel="noopener">WhatsApp</a></div>
|
| 42 |
+
<div class="row"><button class="btn ghost sm" id="install" type="button" hidden>📲 Add to Home Screen</button><button class="btn pass sm" id="pass-btn" type="button">✨ Fun Pass</button></div>
|
| 43 |
+
</section>
|
| 44 |
+
</div>
|
| 45 |
+
<div class="nudge"><span>Want a Legendary every time? The <b>Fun Pass</b> lets you pick any frame, hides the CyberMax line and works in every CyberMax game.</span><button class="btn pass sm" data-pass="nudge" type="button">See the pass</button></div>
|
| 46 |
+
|
| 47 |
+
<section class="seo">
|
| 48 |
+
<h2>How Pawfoil works</h2>
|
| 49 |
+
<p>Pick a photo of your dog, cat, bunny, bird or any pet. Pawfoil frames it as a collectible trading card with HP, six stats (Cuteness is always high), a type and two funny moves like <i>Sock Heist</i> or <i>3 AM Zoomies</i>. Each roll draws a rarity: Common, Rare, Epic (holo) or Legendary (gold holo). Roll again until you love it, then download the HD PNG or share it.</p>
|
| 50 |
+
<h3>Private: your photo stays on your device</h3>
|
| 51 |
+
<p>The card is drawn in your browser. Your photo is never uploaded, stored or seen by anyone but you. No account, no email.</p>
|
| 52 |
+
<h3>Free, with an optional Fun Pass</h3>
|
| 53 |
+
<p>Every card is free in full HD. The optional <b>CyberMax Fun Pass</b> ($2.99/month, $19/year, or $4.99 once) lets you pick any frame (Legendary on demand), hides the small CyberMax line on the card, and unlocks extras in every CyberMax game.</p>
|
| 54 |
+
<h3>FAQ</h3>
|
| 55 |
+
<p><b>Can I print it?</b> Yes: the download is 750×1050 px, a standard 2.5×3.5 inch card at 300 dpi. <b>Why not an AI card generator?</b> Pawfoil uses your real photo (no strange AI faces), is instant, works offline and needs no sign-up. <b>Is it really free?</b> Yes, the HD download is free; the pass only adds extras.</p>
|
| 56 |
+
<h2>More free games from CyberMax</h2>
|
| 57 |
+
<div class="more"><a href="https://cybermax.cybermax-tools.workers.dev/games/kenwren/"><b>Kenwren</b><span>How well do your friends know you?</span></a><a href="https://cybermax.cybermax-tools.workers.dev/games/pathmoor/"><b>Pathmoor</b><span>Daily path puzzle</span></a><a href="https://cybermax.cybermax-tools.workers.dev/games/whirlane/"><b>Whirlane</b><span>One-tap orbit dodger</span></a></div>
|
| 58 |
+
</section>
|
| 59 |
+
</main>
|
| 60 |
+
<footer>© 2026 CyberMax · <a href="https://cybermax.cybermax-tools.workers.dev/games/">Games</a> · <a href="https://cybermax.cybermax-tools.workers.dev/toys/">Toys</a> · <a href="https://cybermax.cybermax-tools.workers.dev/store/">Store</a> · <a href="https://cybermax.cybermax-tools.workers.dev/privacy">Privacy</a> · <a href="https://cybermax.cybermax-tools.workers.dev/terms">Terms</a></footer>
|
| 61 |
+
</div>
|
| 62 |
+
<script type="module" src="game.js"></script>
|
| 63 |
+
</body></html>
|
manifest.webmanifest
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Pawfoil: Make your pet a holo trading card",
|
| 3 |
+
"short_name": "Pawfoil",
|
| 4 |
+
"description": "Make your pet a holo trading card. Free daily game by CyberMax, works offline.",
|
| 5 |
+
"id": "index.html",
|
| 6 |
+
"start_url": "index.html?utm_source=pwa-hf",
|
| 7 |
+
"scope": "./",
|
| 8 |
+
"display": "standalone",
|
| 9 |
+
"orientation": "portrait",
|
| 10 |
+
"background_color": "#1B1430",
|
| 11 |
+
"theme_color": "#1B1430",
|
| 12 |
+
"categories": [
|
| 13 |
+
"games",
|
| 14 |
+
"entertainment"
|
| 15 |
+
],
|
| 16 |
+
"icons": [
|
| 17 |
+
{
|
| 18 |
+
"src": "icon-192.png",
|
| 19 |
+
"sizes": "192x192",
|
| 20 |
+
"type": "image/png"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"src": "icon-512.png",
|
| 24 |
+
"sizes": "512x512",
|
| 25 |
+
"type": "image/png"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"src": "icon-maskable.png",
|
| 29 |
+
"sizes": "512x512",
|
| 30 |
+
"type": "image/png",
|
| 31 |
+
"purpose": "maskable"
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"src": "icon.svg",
|
| 35 |
+
"sizes": "any",
|
| 36 |
+
"type": "image/svg+xml"
|
| 37 |
+
}
|
| 38 |
+
],
|
| 39 |
+
"screenshots": [
|
| 40 |
+
{
|
| 41 |
+
"src": "shot-phone.png",
|
| 42 |
+
"sizes": "780x1688",
|
| 43 |
+
"type": "image/png",
|
| 44 |
+
"form_factor": "narrow"
|
| 45 |
+
}
|
| 46 |
+
]
|
| 47 |
+
}
|
og.png
ADDED
|
Git LFS Details
|
sample.jpg
ADDED
|
shot-phone.png
ADDED
|
Git LFS Details
|
style.css
CHANGED
|
@@ -1,28 +1,18 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
}
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.
|
| 19 |
-
max-width: 620px;
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
.card p:last-child {
|
| 27 |
-
margin-bottom: 0;
|
| 28 |
-
}
|
|
|
|
| 1 |
+
/* Pawfoil layout. (c) 2026 CyberMax. */
|
| 2 |
+
.wrap.wide{max-width:1040px}
|
| 3 |
+
.lede{font-size:17px;margin:0 0 14px}
|
| 4 |
+
.maker{display:grid;grid-template-columns:1fr;gap:16px;align-items:start}
|
| 5 |
+
@media (min-width:860px){.maker{grid-template-columns:minmax(300px,380px) 1fr}.preview{position:sticky;top:12px}}
|
| 6 |
+
.controls{display:flex;flex-direction:column;gap:12px}
|
| 7 |
+
.photo input{position:absolute;width:1px;height:1px;opacity:0}.photo .btn{width:100%}
|
| 8 |
+
.field{display:flex;flex-direction:column;gap:4px;font-weight:700;font-size:14px}
|
| 9 |
+
.field input{font:inherit;font-size:18px;padding:11px 12px;border-radius:12px;border:1.5px solid var(--line);background:var(--bg);color:var(--ink)}
|
| 10 |
+
.adj label{display:flex;align-items:center;gap:8px;font-weight:700;font-size:14px}.adj input{width:150px}
|
| 11 |
+
.pro-box{border:1.5px dashed var(--line);border-radius:14px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}
|
| 12 |
+
.preview{display:flex;flex-direction:column;align-items:center;gap:10px}
|
| 13 |
+
.tilt{position:relative;width:min(100%,420px);aspect-ratio:750/1050;border-radius:22px;transform-style:preserve-3d;transition:transform .12s ease-out;will-change:transform;touch-action:none}
|
| 14 |
+
.tilt canvas{width:100%;height:100%;border-radius:22px;box-shadow:0 18px 50px rgba(0,0,0,.35);cursor:grab;display:block}
|
| 15 |
+
.holo{position:absolute;inset:0;border-radius:22px;pointer-events:none;mix-blend-mode:color-dodge;opacity:0;background:linear-gradient(115deg,transparent 20%,rgba(255,0,170,.35) 35%,rgba(0,255,230,.35) 50%,rgba(255,230,0,.35) 65%,transparent 80%);background-size:250% 250%;transition:opacity .3s}
|
| 16 |
+
.tilt.foil .holo{opacity:.55}
|
| 17 |
+
.share{justify-content:center}
|
| 18 |
+
.controls .chips{flex-wrap:wrap!important;overflow:visible!important;margin:0!important;padding:0!important}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sw.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = 'pawfoil';
|
| 4 |
+
const CACHE = `cmx-${GAME}-da5d827006`;
|
| 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", "manifest.webmanifest", "sample.jpg", "style.css"];
|
| 6 |
+
|
| 7 |
+
self.addEventListener('install', (e) => {
|
| 8 |
+
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(FILES)).then(() => self.skipWaiting()));
|
| 9 |
+
});
|
| 10 |
+
self.addEventListener('activate', (e) => {
|
| 11 |
+
e.waitUntil(caches.keys().then((keys) => Promise.all(keys.filter((k) => k.startsWith(`cmx-${GAME}-`) && k !== CACHE).map((k) => caches.delete(k)))).then(() => self.clients.claim()));
|
| 12 |
+
});
|
| 13 |
+
self.addEventListener('fetch', (e) => {
|
| 14 |
+
const u = new URL(e.request.url);
|
| 15 |
+
if (e.request.method !== 'GET' || u.origin !== location.origin || !u.pathname.startsWith(new URL(self.registration.scope).pathname)) return;
|
| 16 |
+
if (e.request.mode === 'navigate') {
|
| 17 |
+
e.respondWith(fetch(e.request).then((r) => { const c = r.clone(); caches.open(CACHE).then((x) => x.put(u.pathname, c)); return r; })
|
| 18 |
+
.catch(() => caches.match(u.pathname).then((r) => r || caches.match('index.html'))));
|
| 19 |
+
return;
|
| 20 |
+
}
|
| 21 |
+
e.respondWith(caches.match(e.request, { ignoreSearch: true }).then((r) => r || fetch(e.request).then((res) => {
|
| 22 |
+
if (res.ok) { const c = res.clone(); caches.open(CACHE).then((x) => x.put(e.request, c)); }
|
| 23 |
+
return res;
|
| 24 |
+
})));
|
| 25 |
+
});
|