From 14cc7bda49034f659dcc5e5a9e0679b65de6a45a Mon Sep 17 00:00:00 2001 From: Steev <1+steev@noreply@slpnetwork.de> Date: Fri, 3 Jul 2026 01:40:02 +0200 Subject: [PATCH] franke-arcade-sdk.js aktualisiert --- franke-arcade-sdk.js | 197 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 181 insertions(+), 16 deletions(-) diff --git a/franke-arcade-sdk.js b/franke-arcade-sdk.js index 56bed44..454adc9 100644 --- a/franke-arcade-sdk.js +++ b/franke-arcade-sdk.js @@ -1,21 +1,51 @@ const FrankieSDK = (() => { const _handlers = {} const _llmResolvers = {} + const _storageResolvers = {} - // _handle dispatches an inbound postMessage event to the registered hook. + // Dev mock mode: active when the game runs outside a Frankie host frame (opened + // standalone) or when explicitly requested with ?frankie_dev=1. In mock mode the SDK + // emulates the host locally instead of posting to a parent frame. + let _mock = window.parent === window || /[?&]frankie_dev=1\b/.test(window.location.search) + let _gotInit = false + let _declarationCache // Promise resolving to the manifest storage declaration (or null) + + const MOCK_INIT = { userId: 0, username: 'DevPlayer', gameId: 'dev', theme: 'dark' } + + // _handle dispatches an inbound event to the registered lifecycle hook. function _handle(type, payload) { const hook = _handlers[type] if (typeof hook === 'function') hook(payload) } - // Boot: listen for messages from the Frankie parent frame. + // _send routes an outbound message: to the real parent frame, or to the local mock host. + function _send(message) { + if (_mock) { + _mockHandle(message) + return + } + window.parent.postMessage(message, '*') + } + + // _settleStorage resolves or rejects the pending storage Promise for a request id. + function _settleStorage(id, result) { + const pending = _storageResolvers[id] + if (!pending) return + delete _storageResolvers[id] + if (result && result.ok) pending.resolve(result.value) + else pending.reject(new Error(result?.error || 'storage error')) + } + + // Boot: listen for messages from the Frankie parent frame (ignored in mock mode). window.addEventListener('message', (event) => { const { type, payload, id } = event.data || {} if (typeof type !== 'string' || !type.startsWith('FRANKIE_')) return const hook = type.slice(8).toLowerCase() // FRANKIE_INIT → 'init' - // LLM responses are resolved as promises rather than dispatched as hooks. + if (hook === 'init') _gotInit = true + + // LLM + storage responses resolve Promises rather than dispatching hooks. if (hook === 'llm_response') { const resolve = _llmResolvers[id] if (typeof resolve === 'function') { @@ -24,38 +54,173 @@ const FrankieSDK = (() => { } return } + if (hook === 'storage_result') { + _settleStorage(id, payload) + return + } _handle(hook, payload) }) - return { + // In mock mode, deliver a synthetic init once the game has registered its handlers + // (games call sdk.on('init', …) synchronously on load, so a 0ms timer finds them). + if (_mock) { + setTimeout(() => _handle('init', MOCK_INIT), 0) + } else { + // Iframed but possibly not under a real Frankie host: if no init arrives shortly, + // fall back to mock mode so the game still boots during local testing. + setTimeout(() => { + if (!_gotInit) { + _mock = true + _handle('init', MOCK_INIT) + } + }, 1500) + } + + const api = { // on registers a lifecycle hook. Returns the SDK for chaining. on(event, handler) { _handlers[event] = handler return this }, - // emit sends a typed event to the Frankie parent frame. + // emit sends a typed event to Frankie (or logs it in mock mode). emit(event, payload = {}) { - window.parent.postMessage( - { type: `FRANKIE_${event.toUpperCase()}`, payload }, - '*' - ) + _send({ type: `FRANKIE_${event.toUpperCase()}`, payload }) }, - // llm sends a chat-completion request to Frankie's LLM and returns a Promise - // that resolves with the model's reply as a string. + // llm sends a chat-completion request and resolves with the model's reply. llm(messages) { return new Promise((resolve) => { const id = `llm_${Date.now()}_${Math.random().toString(36).slice(2)}` _llmResolvers[id] = resolve - window.parent.postMessage( - { type: 'FRANKIE_LLM_REQUEST', id, payload: { messages } }, - '*' - ) + _send({ type: 'FRANKIE_LLM_REQUEST', id, payload: { messages } }) }) + }, + + // storage is the only sanctioned way to persist game data. Both methods return a + // Promise; get resolves with the stored object, set with the persisted object. + storage: { + get(store = 'default') { + return _requestStorage('FRANKIE_STORAGE_GET', { store }) + }, + set(store, value) { + return _requestStorage('FRANKIE_STORAGE_SET', { store, value }) + } } } + + // _requestStorage issues a storage request and returns a Promise settled by the host + // (real or mock) via a FRANKIE_STORAGE_RESULT reply. + function _requestStorage(type, payload) { + return new Promise((resolve, reject) => { + const id = `st_${Date.now()}_${Math.random().toString(36).slice(2)}` + _storageResolvers[id] = { resolve, reject } + _send({ type, id, payload }) + }) + } + + // ========================================================================== + // Mock host (dev mode) — a self-contained emulation of Frankie's bridge so a + // game can be exercised standalone. It mirrors the storage validation rules. + // ========================================================================== + + function _mockHandle(message) { + const { type, id, payload } = message + switch (type) { + case 'FRANKIE_READY': + case 'FRANKIE_XP': + case 'FRANKIE_WIN': + case 'FRANKIE_SCORE': + case 'FRANKIE_CLOSE': + console.log(`[FrankieSDK dev] ${type}`, payload || {}) + break + case 'FRANKIE_LLM_REQUEST': { + const resolve = _llmResolvers[id] + if (typeof resolve === 'function') { + resolve('[FrankieSDK dev] LLM is stubbed in standalone mode.') + delete _llmResolvers[id] + } + break + } + case 'FRANKIE_STORAGE_GET': + _mockStorage('read', payload).then((result) => _settleStorage(id, result)) + break + case 'FRANKIE_STORAGE_SET': + _mockStorage('write', payload).then((result) => _settleStorage(id, result)) + break + default: + break + } + } + + // _mockDeclaration fetches and caches the game's own manifest storage block. + function _mockDeclaration() { + if (!_declarationCache) { + _declarationCache = fetch('./frankie-game.json') + .then((res) => (res.ok ? res.json() : {})) + .then((manifest) => manifest?.storage || null) + .catch(() => null) + } + return _declarationCache + } + + async function _mockStorage(op, payload) { + const storeName = payload?.store || 'default' + try { + const decl = await _mockDeclaration() + const store = decl?.stores?.[storeName] + if (!store) throw new Error(`store "${storeName}" is not declared by this game`) + _mockAssertAccess(store, op) + + const key = `frankie_dev_gamedata_${storeName}` + if (op === 'read') { + return { ok: true, value: JSON.parse(localStorage.getItem(key) || '{}') } + } + _mockValidate(store.fields, payload?.value) + localStorage.setItem(key, JSON.stringify(payload.value)) + return { ok: true, value: payload.value } + } catch (err) { + return { ok: false, error: err.message } + } + } + + function _mockAssertAccess(store, op) { + const a = store.access + if (op === 'read' && (a === 'read' || a === 'readwrite')) return + if (op === 'write' && (a === 'write' || a === 'readwrite')) return + throw new Error(op === 'read' ? 'this store is write-only' : 'this store is read-only') + } + + function _mockValidate(fields, value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error('value must be an object') + } + for (const [key, val] of Object.entries(value)) { + if (!(key in fields)) throw new Error(`unknown field "${key}"`) + _mockCheck(key, fields[key], val) + } + } + + function _mockCheck(name, spec, val) { + if (typeof spec === 'string') { + const ok = + (spec === 'string' && typeof val === 'string') || + (spec === 'number' && typeof val === 'number') || + (spec === 'boolean' && typeof val === 'boolean') || + (spec === 'object' && val && typeof val === 'object' && !Array.isArray(val)) || + (spec === 'string[]' && Array.isArray(val) && val.every((v) => typeof v === 'string')) || + (spec === 'number[]' && Array.isArray(val) && val.every((v) => typeof v === 'number')) + if (!ok) throw new Error(`field "${name}" must be a ${spec}`) + } else { + if (!val || typeof val !== 'object' || Array.isArray(val)) { + throw new Error(`field "${name}" must be an object`) + } + _mockValidate(spec, val) + } + } + + return api })() -export default FrankieSDK +export default FrankieSDK \ No newline at end of file