/** * Hackcade SDK — postMessage bridge between game iframe and the hack.tez Hackcade. * * ESM only. Import as a module: * * * * The SDK is intentionally tiny — it does NOT ship a renderer, physics, audio, * or input. Use anything you like. The SDK is just the umbilical to the * platform: identity in, score out, lifecycle in both directions. * * Two-way protocol: * game → platform: hackcade:ready, hackcade:score, hackcade:gameover, hackcade:event * platform → game: hackcade:init, hackcade:start, hackcade:pause, hackcade:resume, hackcade:visibility * * @typedef {object} HackcadePlayer * @property {string} domain Full name, e.g. "skull.hack.tez". Empty for guests. * @property {string} label Label only, e.g. "skull". "guest" for unauthenticated. * @property {string} address tz1... wallet address. Empty for guests. * @property {string} avatarUrl Profile picture or hackatar URL. * @property {string} hackatarUrl Generative hackatar URL (always set for authed players). * * @typedef {"start"|"pause"|"resume"|"visibility"} HackcadeEventName */ const events = new EventTarget(); let _player = /** @type {HackcadePlayer|null} */ (null); let _session = /** @type {string|null} */ (null); let _ready = false; let _resolveReady = /** @type {((p: HackcadePlayer) => void) | null} */ (null); const _readyPromise = /** @type {Promise} */ ( new Promise((res) => { _resolveReady = res; }) ); function send(msg) { try { window.parent.postMessage(msg, "*"); } catch { /* parent gone */ } } window.addEventListener("message", (e) => { const data = e && e.data; if (!data || typeof data !== "object") return; const type = data.type; if (typeof type !== "string" || !type.startsWith("hackcade:")) return; if (type === "hackcade:init") { _session = typeof data.sessionId === "string" ? data.sessionId : null; _player = data.player || makeGuest(); _ready = true; if (_resolveReady) { _resolveReady(_player); _resolveReady = null; } events.dispatchEvent(new CustomEvent("init", { detail: { player: _player, session: _session } })); return; } const name = type.slice("hackcade:".length); if (name === "start" || name === "pause" || name === "resume" || name === "visibility") { events.dispatchEvent(new CustomEvent(name, { detail: data })); } }); function makeGuest() { return { domain: "", label: "guest", address: "", avatarUrl: "", hackatarUrl: "", }; } const sdk = { /** Live `EventTarget` for `init`, `start`, `pause`, `resume`, `visibility`. */ events, /** Synchronous accessor for the current player. `null` until `ready()` resolves. */ get player() { return _player; }, /** Synchronous accessor for the current session id. `null` until `ready()` resolves. */ get session() { return _session; }, /** Whether the platform has sent `init`. */ get isReady() { return _ready; }, /** * Tell the platform we're loaded. Resolves with the player once `init` arrives. * Call this once during boot. Safe to await many times. * @returns {Promise} */ async ready() { send({ type: "hackcade:ready" }); return _readyPromise; }, /** * Get the current player. Resolves after `init`. * @returns {Promise} */ async getPlayer() { if (_player) return _player; return _readyPromise; }, /** True if the player has no hack.tez domain. */ isGuest() { return !_player || !_player.domain; }, /** * Friendly greeting helper: "Hi, skull.hack.tez" or "Hi, guest". * @returns {string} */ greeting() { if (!_player || !_player.domain) return "Hi, guest"; return `Hi, ${_player.domain}`; }, /** * Update the live score shown in the platform chrome above the iframe. * Call as often as you like during play. * @param {number} score */ updateScore(score) { if (typeof score !== "number" || !isFinite(score)) return; send({ type: "hackcade:score", score: Math.floor(score), sessionId: _session }); }, /** * Submit the final score to the leaderboard. The platform will only persist * scores for authenticated players (guests' results are dropped). * @param {number} finalScore * @param {object} [options] * @param {number} [options.durationSeconds] Duration of the round in seconds. * @param {number} [options.durationMs] Or in milliseconds. * @param {Record} [options.metadata] Free-form metadata for the entry. */ gameOver(finalScore, options) { let score = Math.floor(typeof finalScore === "number" && isFinite(finalScore) ? finalScore : 0); const durationSeconds = options && typeof options.durationSeconds === "number" ? options.durationSeconds : undefined; const durationMs = options && typeof options.durationMs === "number" ? options.durationMs : undefined; const metadata = options && options.metadata && typeof options.metadata === "object" ? options.metadata : undefined; send({ type: "hackcade:gameover", score, sessionId: _session, durationSeconds, durationMs, metadata, }); }, /** * Convenience event subscription. Same as `events.addEventListener` but returns * an unsubscribe function. * @param {HackcadeEventName | "init"} event * @param {(detail: any) => void} handler * @returns {() => void} */ on(event, handler) { const wrapped = (e) => handler(e.detail); events.addEventListener(event, wrapped); return () => events.removeEventListener(event, wrapped); }, }; export default sdk; export { events };