## TL;DR `@gamee/sdk` is a typed, promise-based JavaScript bridge between an HTML5 game and the GAMEE platform (web iframe, iOS WKWebView, Android WebView). The wire protocol is identical across all three hosts; the SDK picks the right transport at boot. Games declare capabilities at `init()` time; gated methods reject synchronously if the capability is missing. ```ts import { gamee } from '@gamee/sdk'; await gamee.init({ capabilities: ['saveState', 'rewardedAds'] }); gamee.gameReady(); gamee.on('start', () => game.run()); gamee.on('pause', () => game.pause()); ``` ## Glossary | Term | Meaning | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Game** | The HTML5 app written with `@gamee/sdk`. | | **Platform** | The GAMEE app that runs the game (web/iOS/Android). Older docs may call this the _host_ or _client_; we use **platform** everywhere (matches `GameInitResponse.platform`, the emulator UI, and the bridge log). | | **Bridge** | The transport layer the SDK picks at boot — `WebBridge` (postMessage), `IosBridge` (`webkit.messageHandlers.callbackHandler`), `AndroidBridge` (`window._toDevice`). | | **Request** | Game → platform RPC. Returns a `Promise`. | | **Response** | Platform's reply to a specific request. Carries data **or** error. | | **Event** | Platform → game push. Game subscribes via `gamee.on(name, handler)`. SDK auto-acks. | | **Signal** | A fire-and-forget request the SDK sends without tracking the platform's ack (e.g. `updateScore`). Resolves immediately. | | **Capability** | A string in `init({ capabilities: [...] })` that gates feature methods. | ## Imports ```ts // Default singleton — what most games use: import { gamee } from '@gamee/sdk'; // Fresh instance — for tests or multi-game setups: import { createSdk } from '@gamee/sdk'; const sdk = createSdk({ allowedOrigins: ['https://games.gameeapp.com'] }); // Errors: import { GameeError } from '@gamee/sdk'; // Types you'll commonly need: import type { Capability, Platform, Environment, GameContext, InitOptions, GameInitResponse, MissionData, GameOverData, PurchaseDetails, PurchaseResult, RewardedLoadResult, RewardedResult, GameeEvents, GameeEventName, GameeErrorCode, PlatformBridge, BridgeListener, SdkOptions, } from '@gamee/sdk'; ``` IIFE script-tag: `` — exposes `window.Gamee` with `Gamee.gamee`, `Gamee.createSdk`, `Gamee.GameeError`. ## Public API — every method Returns `Promise` unless noted. **Cap** = capability required at init. ### Lifecycle | Method | Args | Returns | Cap | Notes | | --------------------- | --------------------------------- | ------------------ | --- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `init` | `{ capabilities: Capability[] }` | `GameInitResponse` | — | Call **once per SDK instance**; second call rejects `INVALID_STATE`. Validates capability list synchronously. Sends SDK version on the wire. | | `gameReady` | — | `void` (signal) | — | Tells the platform first frame is rendered. | | `gameStart` | — | `void` (signal) | — | A run is starting. | | `gameOver` | `{ saveStateData?, rewardIds?, metadata? }?` | `void` (signal) | — | Run ended. `saveStateData` and `metadata` accept `object \| string`; objects auto-stringify. | | `requestPause` | — | `void` (signal) | — | Game-initiated pause (e.g. opened in-game menu). Distinct from the platform-pushed `pause` event. | | `dispose` | — | `void` (sync) | — | Release resources. Pending data-returning requests reject `INVALID_STATE`. | ### Save state | Method | Args | Cap | Notes | | --------------- | --------------- | ----------- | --------------------------------------------------------- | | `gameSaveState` | `data: object \| string` | `saveState` | Pass an object (auto-stringified) or a pre-stringified JSON string. Wire method is `saveState`. | ### Score & missions | Method | Args | Cap | Validation | | ----------------------- | -------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------- | | `updateScore` | `number \| { score, playTime?, checksum?, metadata? }` | — | `score` must be finite; `playTime` ≥ 0 and finite (if provided); `checksum` non-empty string (if provided). `score` is `Math.trunc()`-ed. Wire payload omits keys you didn't pass. | | `updateMissionProgress` | `progress: number` | `missions` | `progress` must be a finite number in `[0, 100]`. | ### Commerce | Method | Args | Returns | Cap | Validation | | ---------------------- | --------------------------- | ---------------- | ------ | -------------------------------------------- | | `purchaseItemWithGems` | `{ gemsCost, itemName }` | `{ success }` | `gems` | `gemsCost` ≥ 0, `itemName` non-empty string. | ### Rewarded ads | Method | Args | Returns | Cap | | ------------------- | ---- | -------------------------- | ------------- | | `loadRewardedVideo` | — | `{ videoLoaded: boolean }` | `rewardedAds` | | `showRewardedVideo` | — | `{ videoPlayed: boolean }` | `rewardedAds` | ### Analytics | Method | Args | Cap | Validation | | ---------- | ----------------------------- | ----------- | ---------------------------------------------------------- | | `logEvent` | `string \| { name, value? }` | `logEvents` | `name` non-empty, ≤ **24** chars; `value` (optional) ≤ **160** chars. Wire payload omits `eventValue` when `value` not provided. | ### Introspection / utilities All read-only getters on the SDK instance, or pure top-level imports. None of these send anything on the wire. | Member | Source | Type | Notes | | ---------------------- | ------------------ | -------------------------- | ------------------------------------------------------------------------------------ | | `hasPlatformContext()` | top-level import | `boolean` (sync) | `true` iff in an iframe, iOS WKWebView, or Android WebView. Call **before** `createSdk()`/`init()`; if `false`, render a fallback — `init()` would otherwise hang and reject `BRIDGE_TIMEOUT` after 30 s. | | `gamee.platform` | instance getter | `Platform` | Bridge's platform: `'web' \| 'mobile_web' \| 'ios' \| 'android'`. Stable for SDK's lifetime. | | `gamee.version` | instance getter | `string` | SDK version sent on the wire with `init`. | | `gamee.initResponse` | instance getter | `GameInitResponse \| null` | The `init` payload, or `null` before init. | | `SDK_VERSION` | top-level constant | `string` | Same as `gamee.version`, available pre-init. | | `ALL_CAPABILITIES` | top-level constant | `readonly Capability[]` | Every capability the SDK knows. | ### Events ```ts gamee.on(event, handler) // returns an unsubscribe function gamee.off(event, handler?) // drop one handler, or all if no fn passed ``` | Event | Payload | When | | ------------------- | ---------------------------- | ----------------------------------------------------------------------- | | `start` | `{ resetState?, gameSeed? }` | New run. May fire multiple times (replay, restart). | | `pause` | `undefined` | Platform paused (backgrounded, system interruption). Stop loop + audio. | | `resume` | `undefined` | OK to resume. | | `mute` | `undefined` | Audio toggled off. | | `unmute` | `undefined` | Audio toggled on. | | `submit` | `undefined` | Platform asks the game to call `gameOver` now. | | `useExtraLife` | `undefined` | Player redeemed an extra life — resume from end state. | | `avatarAnimation` | `{ animationType }` | Play a one-shot animation on the in-game avatar. | | `avatarUpdate` | `{ avatar }` | Avatar data changed. | | `miningEventUpdate` | `{ miningEvent }` | Mining feature payload changed. | Listeners that throw are caught and logged; one bad handler can't starve the others. Subscribe **before** calling `gameReady()` or you may miss the first `start`. ## Capabilities ```ts type Capability = 'saveState' | 'rewardedAds' | 'logEvents' | 'missions' | 'gems' | 'platformExtraLife'; ``` Calling a gated method without its capability throws `CAPABILITY_MISSING` **synchronously, before** anything reaches the wire. The error is a `GameeError` with `.code`, `.method`, `.message`. `platformExtraLife` does not gate any SDK method — it tells the platform to render its post-`gameOver` "extra life" overlay (rewarded ad / gem purchase). Without it, the platform will not offer extra lives. Pair it with a listener on the `useExtraLife` event. Removed (relative to legacy `gamee-js`): `replay`, `ghostMode`, `socialData`, `playerData`. ## `GameInitResponse` ```ts { environment: 'production' | 'development'; platform: 'web' | 'mobile_web' | 'ios' | 'android'; country: string; // ISO 3166-1 alpha-2 ('US', 'CZ', …) locale: string; // BCP-47 ('en-US', 'cs-CZ', …) gameContext: 'normal' | 'battle' | 'mission'; sound: boolean; noAds?: boolean; saveState?: string; // previously saved state (stringified JSON the game wrote) initData?: string; // deep-link payload (stringified JSON) missionData?: MissionData; // present when gameContext === 'mission' } ``` The SDK validates that `environment`, `platform`, `country`, `locale`, `gameContext`, `sound` are present. Missing fields cause the init Promise to reject with `BRIDGE_ERROR`. ## Errors Every failure mode is a `GameeError` with one of seven `code` values: | Code | When | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `VALIDATION` | Bad input that the SDK validated locally (`logEvent` over 160 chars, `updateMissionProgress` outside `[0, 100]`, unknown capability, etc.). | | `CAPABILITY_MISSING` | Gated method called without the capability. | | `BRIDGE_TIMEOUT` | Platform did not respond within `requestTimeoutMs` (default **30 000 ms**). Only data-returning methods can hit this. | | `BRIDGE_ERROR` | Platform returned an explicit error response, or returned a malformed payload (e.g. init missing required fields). | | `BRIDGE_OVERFLOW` | More than `maxPendingRequests` concurrent data-returning calls (default **256**). Oldest pending is rejected. | | `INVALID_STATE` | `init()` called twice, or any method called after `dispose()`. | | `NOT_SUPPORTED` | Method exists but the platform can't honor it. Reserved for future use. | `GameeError` has `.name === 'GameeError'`, `.code`, `.method?`, `.message`, `.cause?`. Branch on `.code`, never on `.message`. ```ts try { await gamee.gameSaveState({ level: 3 }); } catch (err) { if (err instanceof GameeError && err.code === 'CAPABILITY_MISSING') { // declare 'saveState' in init() } } ``` ## Behavior gotchas - **Signal methods** (`gameReady`, `gameStart`, `gameOver`, `gameSaveState`, `updateScore`, `updateMissionProgress`, `requestPause`, `logEvent`) return `Promise` but resolve immediately — they don't wait for the platform's ack. Validation errors surface as Promise rejections. If you don't `await` or `.catch()`, they become unhandled-rejection events. - **Data-returning methods** (`purchaseItemWithGems`, `loadRewardedVideo`, `showRewardedVideo`) DO wait for the platform's reply and can hit `BRIDGE_TIMEOUT` / `BRIDGE_ERROR`. - **`init()` is single-shot.** Second call → `INVALID_STATE`. Use `createSdk()` for a fresh instance in tests. - **Subscribe before `gameReady()`** or you'll miss the first `start` event. - **`saveState` is a string on the wire.** The SDK auto-stringifies objects. The platform sends it back via `initResponse.saveState` exactly as it was saved. - **`updateScore` truncates** non-integer scores to `Math.trunc(score)`. - **`messageId` is wire-internal** — games never see it. ## SDK options (`createSdk(opts)`) ```ts interface SdkOptions { allowedOrigins?: readonly string[] | '*'; // postMessage origin allowlist for the web bridge. Default: '*' with first-use warning. requestTimeoutMs?: number; // default 30_000 maxPendingRequests?: number; // default 256 (data-returning calls only) silentMode?: boolean; // suppress runtime warnings bridge?: PlatformBridge; // inject a custom bridge (used by emulator + MockBridge) } ``` ## Wire format One envelope shape, both directions: ```jsonc // Game → Platform request { "request": { "method": "init", "messageId": 0, "data": { "version": "1.0.0", "capabilities": { "saveState": true } } } } // Platform → Game response (data) — replies to a request { "response": { "messageId": 0, "data": { /* … */ } } } // Platform → Game response (error) { "response": { "messageId": 0, "error": { "code": "…", "message": "…" } } } // Platform → Game event (looks like a request; SDK auto-acks) { "request": { "method": "pause", "messageId": 7, "data": null } } ``` `messageId` is a **non-negative integer (`number`)** that increments by 1 per outbound message within a single SDK instance (starts at 0). Wire-compatible with legacy gamee-js clients. It correlates request ↔ response. `init`'s wire `data.capabilities` is an **object map** (`{ saveState: true }`), not an array — game-facing API uses arrays for ergonomics, the SDK transforms. ## `@gamee/sdk-test-utils` Sister package shipping `MockBridge` for game test suites. Inject it instead of the real bridge. ```ts import { describe, expect, test, beforeEach } from 'vitest'; import { createSdk, type GameeSDK } from '@gamee/sdk'; import { MockBridge } from '@gamee/sdk-test-utils'; let mock: MockBridge; let sdk: GameeSDK; beforeEach(() => { mock = new MockBridge(); sdk = createSdk({ bridge: mock }); }); test('updateScore reaches the wire with the right shape', async () => { await sdk.init({ capabilities: [] }); sdk.updateScore({ score: 420, playTime: 12, checksum: 'sum' }); expect(mock.requests.find((r) => r.method === 'updateScore')?.data).toMatchObject({ score: 420, currentPlayTime: 12, gameChecksum: 'sum', }); }); ``` ### `MockBridge` API | Member | Purpose | | --------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `new MockBridge(platform?)` | Construct. `platform` defaults to `'web'`; pass `'ios'` / `'android'` for tests that branch on it. | | `mock.requests` | Every recorded outbound request: `{ method, messageId, data }[]`. | | `mock.acked` | `messageId`s of platform events the SDK acked. | | `mock.expect(method).respondWith(data)` | Reply to the next request matching `method` with `data`. | | `mock.expect(method).rejectWith({ code, message })` | Reply with an error. | | `mock.expect(method).leavePending()` | Don't auto-respond; settle later via `mock.respondTo(method, data)`. | | `mock.respondTo(method, data)` | Manually settle the most recent matching pending request. | | `mock.emit(name, payload, messageId?)` | Push a platform → game event. Auto-acked. | | `mock.defaultResolution` | What happens to unexpected requests: `'ack-null'` (default) \| `'leave-pending'` \| `'error'`. | | `mock.reset()` | Clear `requests` and unconsumed expectations. | | `mock.dispose()` | Detach. Safe to call repeatedly. | ### Common test recipes ```ts // 1. Platform replies with success. mock.expect('showRewardedVideo').respondWith({ videoPlayed: true }); const r = await sdk.showRewardedVideo(); expect(r.videoPlayed).toBe(true); // 2. Platform rejects. mock.expect('purchaseItemWithGems').rejectWith({ code: 'BRIDGE_ERROR', message: 'insufficient gems' }); await expect(sdk.purchaseItemWithGems({ gemsCost: 99, itemName: 'X' })).rejects.toThrow('insufficient gems'); // 3. Capability gate is sync. expect(() => sdk.gameSaveState({ x: 1 })).toThrow(/CAPABILITY_MISSING|saveState/); // 4. Drive a platform-initiated event. let paused = false; sdk.on('pause', () => { paused = true; }); mock.emit('pause'); expect(paused).toBe(true); // 5. Force a timeout. sdk = createSdk({ bridge: mock, requestTimeoutMs: 50 }); await sdk.init({ capabilities: ['rewardedAds'] }); mock.expect('showRewardedVideo').leavePending(); await expect(sdk.showRewardedVideo()).rejects.toMatchObject({ code: 'BRIDGE_TIMEOUT' }); ``` ## Quick reference for code generation When generating game code that uses this SDK, default to: 1. `await gamee.init({ capabilities: [...] })` — list every capability the game's logic touches; otherwise gated calls throw. 2. `gamee.on(event, handler)` **before** `gamee.gameReady()`. 3. Synchronous-feeling fire-and-forget for telemetry: `gamee.updateScore(...)`, `gamee.logEvent(...)` — no need to `await`. 4. `await` for data-returning calls: `loadRewardedVideo`, `showRewardedVideo`, `purchaseItemWithGems`. 5. Error handling: `try { await … } catch (err) { if (err instanceof GameeError) … }` with `err.code` branching. 6. Tests: inject `MockBridge` via `createSdk({ bridge: mock })`. Never use the singleton in tests.