HTML5 game integration into TileScore tile
Technical requirements and contracts for game developers (Phaser, PixiJS, plain Canvas/WebGL).
1. What a game tile is
A game tile on TileScore is a wrapper page with an iframe that loads your HTML5 bundle. Files are stored in the platform’s private object storage and served via a proxy endpoint with correct MIME types and no CSP-sandbox (the default static delivery from storage blocks script execution inside iframes — that’s why a dedicated proxy is used).
The HTML document is loaded into the iframe not as a direct URL but as a blob document. The platform automatically injects a <base href="..."> tag into <head> — so all relative paths keep working.
2. Bundle format
- Single ZIP archive, limit 30 MB.
index.html— required, must be at the ZIP root (not in a nested folder).- Allowed extensions:
html, htm, js, mjs, css, json, wasm, map, woff, woff2, ttf, otf, png, jpg, jpeg, gif, webp, svg, ico, mp3, ogg, wav, m4a, mp4, webm, txt, xml. Files of other types in the bundle are dropped. - Versioning is automatic: each upload bumps the version, the client appends ?v= for cache busting.
3. Path resolution (important!)
The platform injects this tag into <head>:
<base href="https://<project>.supabase.co/functions/v1/serve-game/<tile_id>/">
// или (если включён прокси для регионов с блокировками *.supabase.co):
<base href="https://api.lazytraders.club/api/v1/games/<tile_id>/">Therefore the rules are:
- All paths in the bundle should be relative to the ZIP root: assets/img.webp, js/game.js, css/style.css.
- Avoid ./ and ../ unless necessary.
- Dynamic loads also resolve via <base>: new Image().src="...", fetch("data.json"), document.createElement("script").src="...", Phaser load.image("k","assets/k.png") — all work.
- Absolute URLs (https://, data:, blob:, //) are not rewritten.
- NEVER hard-code http://localhost — there is no such server in production, the game will fail.
4. Runtime environment inside iframe
- sandbox:
allow-scripts allow-same-origin allow-pointer-lock allow-popups allow-popups-to-escape-sandbox - allow:
fullscreen; autoplay; gamepad; clipboard-write - The document is loaded as blob: → its origin differs from the parent tilescore.net. Any third-party APIs must support CORS on their side.
- localStorage inside a blob document is ephemeral — do NOT use it for game progress. Pass state to the parent via postMessage (see below).
- The platform does NOT run Node, an HTTP server, or a WebSocket server. Static files only.
5. Parameters from the platform
Before the game loads, a global object is injected into the iframe:
window.GAME_PARAMS = {
parentOrigin: "https://tilescore.net",
lang: "ru" | "en",
version: <number>,
isMobile: <boolean> // true when viewport <768px
};The game must read GAME_PARAMS.lang and apply localization. No separate query parameters — just this object.
6. postMessage contract
For future integrations (LAZY rewards, analytics, progress saving) the game sends messages to the parent:
parent.postMessage({
source: "<your-game-name>-game", // обязательно с суффиксом -game / required suffix -game
type: "score" | "finish" | "event" | "progress",
payload: { /* ... */ }
}, window.GAME_PARAMS.parentOrigin);The platform filters incoming messages by data.source.endsWith("-game"). Messages without this suffix are ignored.
7. Responsiveness & Mobile UX contract
Mobile behavior (<768px):
- The game auto-opens fullscreen (100dvw × 100dvh). Site header, tile description and the «Fullscreen» button are hidden — the player needs no extra clicks.
- The tile aspect ratio is ignored on mobile — the game takes the whole screen as-is.
- window.GAME_PARAMS.isMobile: boolean — the game can switch to a simplified UI / touch controls.
- The «exit» button is up to the game. Send postMessage({ source: "<name>-game", type: "exit" }) — the platform will navigate the user back.
General requirements (desktop + mobile):
- Listen to resize and orientationchange events.
- Use Phaser.Scale.RESIZE (preferred) or Phaser.Scale.FIT — never set the canvas to fixed pixel sizes.
- On mobile, respect safe-area: env(safe-area-inset-top/bottom/left/right).
- On desktop the owner-selected Frame aspect ratio is used (9:16, 16:9, etc.) — that is the page wrapper, not the canvas size.
8. What the platform does NOT do
- Does not run an HTTP server on localhost — forget http://localhost:8080 in code.
- Does not provide Node.js, a backend, or a WebSocket server. Static only.
- Does not write to the portal database from the game directly. Any write goes via postMessage and the platform layer.
- Does not give the game an authorized user token.
- Does not wrap the game in a Service Worker — offline mode is your responsibility if needed.
9. Server delivery constraints
- Response cache:
Cache-Control: public, max-age=60, must-revalidate(1 minute). - Cache-busting: ALL relative asset URLs (game.js, *.webp, fonts, css) automatically get ?v=<version> appended. Each new ZIP upload increments version in the DB, so players are guaranteed to receive fresh files — the game does not need to handle cache-busting itself.
- The owner/admin can instantly rebuild the iframe via the "Refresh" button in the tile editor.
- Content-Security-Policy and X-Frame-Options are NOT set (intentional — otherwise the game would not load in the iframe).
- Range requests are supported (useful for video/audio).
- Compression (gzip/brotli) is not enabled yet — keep JS size in mind.
10. Audio in the game (important — common cause of stutter)
If the game starts stuttering after you add sound, the cause is almost always sloppy Audio API usage, not the platform. Checklist:
- Audio pool, not new Audio() per effect. Create one instance per sound and reuse it: a.currentTime = 0; a.play(). Every new Audio() means a fresh decode + GC, hence the freezes.
- Preload on start. Decode all sounds once when the game boots (a.load() / decodeAudioData), not on first playback.
- Format and size. Short SFX — .ogg (Opus/Vorbis) or .m4a (AAC), <50 KB each. Long tracks — stream, do not decode the whole thing into memory.
- WebAudio for frequent SFX. Decode once into an AudioBuffer and schedule via AudioBufferSourceNode — zero latency, no parallel decode on the UI thread.
- One-voice rule. If a sound is already playing, do not retrigger it. Or cap to 4–6 simultaneous voices per channel.
- visibilitychange. On document.hidden === true — pause() every channel; resume on focus. Otherwise a background tab piles up decode jobs and dumps them all at once.
- Volume via GainNode. Do not recreate Audio elements just to change volume — that is a wasted decode.
- Mobile autoplay. The very first play() / AudioContext.resume() must run inside a user gesture (touchstart/click). Otherwise iOS Safari suspends the audio context silently.
- Diagnostics. If FPS drops only with sound — DevTools → Performance → look for "Decode Audio Data" or long tasks. That confirms audio is the cause.
The platform itself imposes no audio restrictions — everything above applies to code inside the bundle.
11. Pre-upload checklist
- index.html is at the ZIP root.
- All paths are relative, no http://localhost and no hard absolute URLs to dev domains.
- ZIP size < 30 MB. Compress heavy graphics to WebP.
- Game reacts to container resize (no fixed canvas size).
- Game reads window.GAME_PARAMS.lang.
- Progress/score is sent via parent.postMessage, not written to localStorage.
- Tested in Chrome desktop, iOS Safari, Android Chrome.
12. Format v2 — modular bundle (recommended for new games)
Format v1 (single 4–5K-line JS) is fine for prototypes but does not scale. v2 splits code across files — one JS per level, shared infra in core/. The ZIP stays single for upload convenience, but inside it holds separate scripts that the serve-game edge function wires together via manifest.json when synthesizing index.html on the fly.
Archive layout
mygame.zip ├── manifest.json # обязательно для v2 ├── vendor/phaser.min.js ├── core/ │ ├── boot.js │ ├── audio.js │ └── ui.js ├── levels/ │ ├── level-1.js │ ├── level-2.js │ └── level-3.js └── assets/...
manifest.json
{
"format": "tilescore-game/v2",
"scripts": [
"vendor/phaser.min.js",
"core/boot.js",
"core/audio.js",
"core/ui.js",
"levels/level-1.js",
"levels/level-2.js",
"levels/level-3.js"
],
"module": false,
"orientation": "portrait",
"aspect_ratio": "9:16"
}- No index.html required — serve-game generates one, wiring scripts in the order from scripts[].
- Scripts are plain global IIFEs (same as v1), just split. They talk via window.GameNS.* (see /public/games/_template/).
- For ESM — set "module": true, scripts are then attached as <script type="module"> with import/export.
- Legacy v1 (index.html + game.js at root) keeps working unchanged.
13. Dev mode — develop in the repo without re-uploading
Drop the game folder into /public/games/<slug>/ (see _template). In the tile admin, enable Dev mode and enter the slug. The game loads directly from public/ — bypassing storage, edge functions, and version bumps. Vite reloads the iframe on every save. When ready — zip and upload as usual.
- Dev mode works only for admins. Regular users always go through storage — even if dev_slug is set.
- In dev, wire scripts manually in index.html (see _template/index.html) — synthetic HTML is prod-only.
- To disable dev — clear the slug field in admin and click Save.