@umicat/phaser-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/SDK-GUIDE.md +1726 -0
  2. package/dist/core/Transport.d.ts +28 -0
  3. package/dist/core/Transport.js +7 -0
  4. package/dist/core/Umicat.d.ts +45 -0
  5. package/dist/core/Umicat.js +60 -0
  6. package/dist/core/UmicatGame.d.ts +43 -0
  7. package/dist/core/UmicatGame.js +64 -0
  8. package/dist/core/UmicatScene.d.ts +19 -0
  9. package/dist/core/UmicatScene.js +38 -0
  10. package/dist/core/transports/LocalStorageTransport.d.ts +22 -0
  11. package/dist/core/transports/LocalStorageTransport.js +78 -0
  12. package/dist/core/transports/PostMessageTransport.d.ts +28 -0
  13. package/dist/core/transports/PostMessageTransport.js +105 -0
  14. package/dist/editor/EditorBridge.d.ts +114 -0
  15. package/dist/editor/EditorBridge.js +2608 -0
  16. package/dist/editor/EditorOverlayScene.d.ts +333 -0
  17. package/dist/editor/EditorOverlayScene.js +1896 -0
  18. package/dist/editor/EditorState.d.ts +251 -0
  19. package/dist/editor/EditorState.js +197 -0
  20. package/dist/gamedata/GameDataModule.d.ts +45 -0
  21. package/dist/gamedata/GameDataModule.js +59 -0
  22. package/dist/index.d.ts +43 -0
  23. package/dist/index.js +43 -0
  24. package/dist/orientation.d.ts +5 -0
  25. package/dist/orientation.js +4 -0
  26. package/dist/protocol.d.ts +807 -0
  27. package/dist/protocol.js +3 -0
  28. package/dist/realtime/RealtimeModule.d.ts +93 -0
  29. package/dist/realtime/RealtimeModule.js +115 -0
  30. package/dist/realtime/UmicatRoom.d.ts +197 -0
  31. package/dist/realtime/UmicatRoom.js +353 -0
  32. package/dist/recording/RecordingManager.d.ts +11 -0
  33. package/dist/recording/RecordingManager.js +59 -0
  34. package/dist/saves/SavesModule.d.ts +23 -0
  35. package/dist/saves/SavesModule.js +37 -0
  36. package/dist/scene/EditorMode.d.ts +17 -0
  37. package/dist/scene/EditorMode.js +22 -0
  38. package/dist/scene/EntityRegistry.d.ts +39 -0
  39. package/dist/scene/EntityRegistry.js +103 -0
  40. package/dist/scene/GameConfig.d.ts +60 -0
  41. package/dist/scene/GameConfig.js +50 -0
  42. package/dist/scene/HudRuntime.d.ts +131 -0
  43. package/dist/scene/HudRuntime.js +1224 -0
  44. package/dist/scene/Prefabs.d.ts +92 -0
  45. package/dist/scene/Prefabs.js +175 -0
  46. package/dist/scene/Rules.d.ts +73 -0
  47. package/dist/scene/Rules.js +164 -0
  48. package/dist/scene/SceneLoader.d.ts +118 -0
  49. package/dist/scene/SceneLoader.js +615 -0
  50. package/dist/scene/Waves.d.ts +85 -0
  51. package/dist/scene/Waves.js +365 -0
  52. package/dist/scene/autotile.d.ts +103 -0
  53. package/dist/scene/autotile.js +321 -0
  54. package/dist/scene/renderScripts.d.ts +53 -0
  55. package/dist/scene/renderScripts.js +67 -0
  56. package/dist/scene/spawnEntity.d.ts +201 -0
  57. package/dist/scene/spawnEntity.js +1326 -0
  58. package/dist/scene/types.d.ts +1166 -0
  59. package/dist/scene/types.js +34 -0
  60. package/dist/screenshot/ScreenshotManager.d.ts +14 -0
  61. package/dist/screenshot/ScreenshotManager.js +33 -0
  62. package/package.json +35 -0
@@ -0,0 +1,28 @@
1
+ import type { UmicatUser } from '../protocol.js';
2
+ /**
3
+ * Low-level transport for platform RPC calls. Swapped per host.
4
+ *
5
+ * Current implementations:
6
+ * - PostMessageTransport: iframe hosted inside umicat-home-ui
7
+ * - LocalStorageTransport: game running standalone (no host, no user)
8
+ *
9
+ * Future implementations (not shipped):
10
+ * - DiscordTransport: game running as a Discord Activity
11
+ */
12
+ export interface Transport {
13
+ readonly kind: TransportKind;
14
+ readonly user: UmicatUser | null;
15
+ readonly gameId: string;
16
+ /**
17
+ * Endpoint for umicat-realtime-service (WebSocket URL). Set by the host
18
+ * during handshake when multiplayer is enabled. Absent in standalone mode —
19
+ * `umicat.rooms.*` is unavailable in that case.
20
+ */
21
+ readonly realtimeUrl?: string;
22
+ call<T = unknown>(method: string, params?: unknown): Promise<T>;
23
+ }
24
+ export type TransportKind = 'umicat-home-ui' | 'standalone' | 'discord';
25
+ export declare class RpcError extends Error {
26
+ code: string;
27
+ constructor(code: string, message: string);
28
+ }
@@ -0,0 +1,7 @@
1
+ export class RpcError extends Error {
2
+ constructor(code, message) {
3
+ super(message);
4
+ this.code = code;
5
+ this.name = 'RpcError';
6
+ }
7
+ }
@@ -0,0 +1,45 @@
1
+ import type { TransportKind } from './Transport.js';
2
+ import { SavesModule } from '../saves/SavesModule.js';
3
+ import { GameDataModule } from '../gamedata/GameDataModule.js';
4
+ import { RealtimeModule } from '../realtime/RealtimeModule.js';
5
+ import type { UmicatUser } from '../protocol.js';
6
+ export interface UmicatInitOptions {
7
+ /** Handshake timeout in ms when running inside a host. Default 5000. */
8
+ handshakeTimeoutMs?: number;
9
+ /**
10
+ * Game ID used for the localStorage fallback. Ignored when connected to a
11
+ * host (the host provides the authoritative gameId). Default 'standalone'.
12
+ */
13
+ standaloneGameId?: string;
14
+ }
15
+ /**
16
+ * Umicat platform services bound to the current (game, user).
17
+ *
18
+ * The public API is host-agnostic. Under the hood, `Umicat.init()` detects the
19
+ * host (home-ui iframe, Discord Activity, or standalone) and picks the right
20
+ * transport. Games should not care which one is active.
21
+ */
22
+ export declare class Umicat {
23
+ private transport;
24
+ readonly saves: SavesModule;
25
+ readonly gameData: GameDataModule;
26
+ readonly rooms: RealtimeModule;
27
+ private constructor();
28
+ /** The current authenticated user, or `null` if anonymous / standalone. */
29
+ get user(): UmicatUser | null;
30
+ get isAuthenticated(): boolean;
31
+ /** Stable game identifier issued by the host. May be a placeholder when standalone. */
32
+ get gameId(): string;
33
+ /** Which transport Umicat.init() selected. Useful for debugging. */
34
+ get host(): TransportKind;
35
+ /**
36
+ * Initialize the SDK. Detects the host and negotiates identity.
37
+ *
38
+ * Resolution order:
39
+ * 1. If embedded in a window that responds to the Umicat handshake → PostMessageTransport
40
+ * 2. Otherwise → LocalStorageTransport (anonymous, local-only saves)
41
+ *
42
+ * Discord Activity support is designed for but not shipped in this version.
43
+ */
44
+ static init(options?: UmicatInitOptions): Promise<Umicat>;
45
+ }
@@ -0,0 +1,60 @@
1
+ import { PostMessageTransport } from './transports/PostMessageTransport.js';
2
+ import { LocalStorageTransport } from './transports/LocalStorageTransport.js';
3
+ import { SavesModule } from '../saves/SavesModule.js';
4
+ import { GameDataModule } from '../gamedata/GameDataModule.js';
5
+ import { RealtimeModule } from '../realtime/RealtimeModule.js';
6
+ // Kept in sync with package.json on each publish.
7
+ const SDK_VERSION = '0.2.8';
8
+ /**
9
+ * Umicat platform services bound to the current (game, user).
10
+ *
11
+ * The public API is host-agnostic. Under the hood, `Umicat.init()` detects the
12
+ * host (home-ui iframe, Discord Activity, or standalone) and picks the right
13
+ * transport. Games should not care which one is active.
14
+ */
15
+ export class Umicat {
16
+ constructor(transport) {
17
+ this.transport = transport;
18
+ // Saves are per-user. When the viewer is anonymous, route save calls to
19
+ // localStorage even if a host is attached — the backend requires auth,
20
+ // so forwarding would 401. Games see the same `umicat.saves` API either
21
+ // way and don't have to branch on auth state.
22
+ const savesTransport = transport.user === null
23
+ ? new LocalStorageTransport(transport.gameId || 'anonymous')
24
+ : transport;
25
+ this.saves = new SavesModule(savesTransport);
26
+ // gameData is game-scope: reads are public (anonymous OK), only writes
27
+ // need auth. The primary transport handles both; if it's a LocalStorage
28
+ // fallback (standalone mode) that also works — the module speaks the
29
+ // same RPC interface.
30
+ this.gameData = new GameDataModule(transport);
31
+ // Multiplayer requires a realtime endpoint from the host. When absent
32
+ // (standalone or a host without multiplayer), methods throw a clear
33
+ // REALTIME_UNAVAILABLE error rather than silently misbehaving.
34
+ this.rooms = new RealtimeModule(transport, transport.realtimeUrl);
35
+ }
36
+ /** The current authenticated user, or `null` if anonymous / standalone. */
37
+ get user() { return this.transport.user; }
38
+ get isAuthenticated() { return this.transport.user !== null; }
39
+ /** Stable game identifier issued by the host. May be a placeholder when standalone. */
40
+ get gameId() { return this.transport.gameId; }
41
+ /** Which transport Umicat.init() selected. Useful for debugging. */
42
+ get host() { return this.transport.kind; }
43
+ /**
44
+ * Initialize the SDK. Detects the host and negotiates identity.
45
+ *
46
+ * Resolution order:
47
+ * 1. If embedded in a window that responds to the Umicat handshake → PostMessageTransport
48
+ * 2. Otherwise → LocalStorageTransport (anonymous, local-only saves)
49
+ *
50
+ * Discord Activity support is designed for but not shipped in this version.
51
+ */
52
+ static async init(options = {}) {
53
+ const handshakeTimeoutMs = options.handshakeTimeoutMs ?? 5000;
54
+ const standaloneGameId = options.standaloneGameId ?? 'standalone';
55
+ const pm = await PostMessageTransport.connect(handshakeTimeoutMs, SDK_VERSION);
56
+ if (pm)
57
+ return new Umicat(pm);
58
+ return new Umicat(new LocalStorageTransport(standaloneGameId));
59
+ }
60
+ }
@@ -0,0 +1,43 @@
1
+ import Phaser from 'phaser';
2
+ import { type Orientation } from '../orientation.js';
3
+ import { RenderScriptModule } from '../scene/renderScripts.js';
4
+ interface UmicatGameBaseOptions {
5
+ /** Phaser scene classes to register */
6
+ scenes: (new (...args: any[]) => Phaser.Scene)[];
7
+ /** Background color (default: '#1a1a2e') */
8
+ backgroundColor?: string;
9
+ /** Enable pixel art rendering (default: false) */
10
+ pixelArt?: boolean;
11
+ /** Custom physics config (default: arcade with no gravity) */
12
+ physics?: Phaser.Types.Core.PhysicsConfig;
13
+ /** Phaser plugin registrations (e.g. `phaser3-rex-plugins` virtual joystick) */
14
+ plugins?: Phaser.Types.Core.PluginObject;
15
+ /**
16
+ * Map of render-script paths → modules that export `render(g, params)`.
17
+ * Templates build this via `import.meta.glob('./visuals/*.ts', { eager: true })`.
18
+ * Used by `code-rendered` entities and consumed inside `loadWorldScene`.
19
+ * Optional — games without code-rendered visuals can omit it.
20
+ */
21
+ renderScripts?: Record<string, RenderScriptModule>;
22
+ }
23
+ /**
24
+ * Either pass `orientation` (preset dims from ORIENTATION_DIMENSIONS) OR
25
+ * explicit `width`/`height` — not both. The TS union enforces this at
26
+ * compile time so games can't drift out of the orientation presets by
27
+ * accident.
28
+ */
29
+ export type UmicatGameOptions = (UmicatGameBaseOptions & {
30
+ orientation: Orientation;
31
+ width?: never;
32
+ height?: never;
33
+ }) | (UmicatGameBaseOptions & {
34
+ width: number;
35
+ height: number;
36
+ orientation?: never;
37
+ });
38
+ /**
39
+ * Create an Umicat-enhanced Phaser game instance.
40
+ * Includes built-in integrations: screenshot capture, preserveDrawingBuffer, etc.
41
+ */
42
+ export declare function createUmicatGame(options: UmicatGameOptions): Phaser.Game;
43
+ export {};
@@ -0,0 +1,64 @@
1
+ import Phaser from 'phaser';
2
+ import NinePatchPlugin from 'phaser3-rex-plugins/plugins/ninepatch-plugin.js';
3
+ import { setupScreenshotListener } from '../screenshot/ScreenshotManager.js';
4
+ import { setupRecordingListener } from '../recording/RecordingManager.js';
5
+ import { setupEditorModeListener } from '../scene/EditorMode.js';
6
+ import { ORIENTATION_DIMENSIONS } from '../orientation.js';
7
+ import { setRenderScriptRegistry, } from '../scene/renderScripts.js';
8
+ import { UmicatHudScene } from '../scene/HudRuntime.js';
9
+ /**
10
+ * Create an Umicat-enhanced Phaser game instance.
11
+ * Includes built-in integrations: screenshot capture, preserveDrawingBuffer, etc.
12
+ */
13
+ export function createUmicatGame(options) {
14
+ const { width, height } = 'orientation' in options && options.orientation
15
+ ? ORIENTATION_DIMENSIONS[options.orientation]
16
+ : { width: options.width, height: options.height };
17
+ // Auto-register rex-plugins NinePatch so HUD widgets with 9-slice asset
18
+ // metadata (`Asset.ninePatch`) can scale without corner distortion. Plugin
19
+ // exposes `scene.add.rexNinePatch(...)` factory; HudRuntime calls it on
20
+ // demand. Merged with any user-supplied `plugins.global` so existing
21
+ // rexVirtualJoystick / rexUI registrations keep working.
22
+ const ninePatchEntry = {
23
+ key: 'rexNinePatchPlugin',
24
+ plugin: NinePatchPlugin,
25
+ start: true,
26
+ };
27
+ const plugins = {
28
+ ...(options.plugins ?? {}),
29
+ global: [ninePatchEntry, ...(options.plugins?.global ?? [])],
30
+ };
31
+ const config = {
32
+ type: Phaser.AUTO,
33
+ backgroundColor: options.backgroundColor ?? '#1a1a2e',
34
+ scale: {
35
+ mode: Phaser.Scale.FIT,
36
+ autoCenter: Phaser.Scale.CENTER_BOTH,
37
+ width,
38
+ height,
39
+ },
40
+ render: {
41
+ preserveDrawingBuffer: true,
42
+ },
43
+ pixelArt: options.pixelArt ?? false,
44
+ physics: options.physics ?? {
45
+ default: 'arcade',
46
+ arcade: { gravity: { x: 0, y: 0 }, debug: false },
47
+ },
48
+ plugins,
49
+ // UmicatHudScene is auto-registered alongside the game's scenes so
50
+ // `loadWorldScene` can launch it when the active world scene's manifest
51
+ // entry sets `hud: '<id>'`. Registered at the end so the user's scenes
52
+ // own the boot order.
53
+ scene: [...options.scenes, UmicatHudScene],
54
+ };
55
+ const game = new Phaser.Game(config);
56
+ // Built-in integrations
57
+ setupScreenshotListener(game);
58
+ setupRecordingListener(game);
59
+ setupEditorModeListener(game);
60
+ if (options.renderScripts) {
61
+ setRenderScriptRegistry(game, options.renderScripts);
62
+ }
63
+ return game;
64
+ }
@@ -0,0 +1,19 @@
1
+ import Phaser from 'phaser';
2
+ /**
3
+ * Base scene class with common Umicat utilities.
4
+ * Extend this instead of Phaser.Scene for built-in helpers.
5
+ */
6
+ export declare class UmicatScene extends Phaser.Scene {
7
+ /**
8
+ * Create a simple text button with hover effects.
9
+ */
10
+ protected createButton(x: number, y: number, text: string, style?: Phaser.Types.GameObjects.Text.TextStyle, onClick?: () => void): Phaser.GameObjects.Text;
11
+ /**
12
+ * Shake the camera briefly (e.g., on damage).
13
+ */
14
+ protected shakeCamera(duration?: number, intensity?: number): void;
15
+ /**
16
+ * Flash the camera briefly (e.g., on hit).
17
+ */
18
+ protected flashCamera(duration?: number, r?: number, g?: number, b?: number): void;
19
+ }
@@ -0,0 +1,38 @@
1
+ import Phaser from 'phaser';
2
+ /**
3
+ * Base scene class with common Umicat utilities.
4
+ * Extend this instead of Phaser.Scene for built-in helpers.
5
+ */
6
+ export class UmicatScene extends Phaser.Scene {
7
+ /**
8
+ * Create a simple text button with hover effects.
9
+ */
10
+ createButton(x, y, text, style, onClick) {
11
+ const btn = this.add.text(x, y, text, {
12
+ fontSize: '20px',
13
+ color: '#ffffff',
14
+ backgroundColor: '#4662D8',
15
+ padding: { x: 16, y: 8 },
16
+ ...style,
17
+ })
18
+ .setOrigin(0.5)
19
+ .setInteractive({ useHandCursor: true });
20
+ btn.on('pointerover', () => btn.setAlpha(0.8));
21
+ btn.on('pointerout', () => btn.setAlpha(1));
22
+ if (onClick)
23
+ btn.on('pointerdown', onClick);
24
+ return btn;
25
+ }
26
+ /**
27
+ * Shake the camera briefly (e.g., on damage).
28
+ */
29
+ shakeCamera(duration = 100, intensity = 0.01) {
30
+ this.cameras.main.shake(duration, intensity);
31
+ }
32
+ /**
33
+ * Flash the camera briefly (e.g., on hit).
34
+ */
35
+ flashCamera(duration = 100, r = 255, g = 255, b = 255) {
36
+ this.cameras.main.flash(duration, r, g, b);
37
+ }
38
+ }
@@ -0,0 +1,22 @@
1
+ import type { Transport } from '../Transport.js';
2
+ import type { UmicatUser } from '../../protocol.js';
3
+ /**
4
+ * Fallback transport when the game runs without a host (e.g., opened directly
5
+ * outside umicat-home-ui) or when the viewer is anonymous. Data lives in
6
+ * localStorage, keyed by gameId. No network calls. Quotas mirror the backend.
7
+ */
8
+ export declare class LocalStorageTransport implements Transport {
9
+ readonly gameId: string;
10
+ readonly kind: "standalone";
11
+ readonly user: UmicatUser | null;
12
+ constructor(gameId: string);
13
+ call<T = unknown>(method: string, params?: unknown): Promise<T>;
14
+ private savesPrefix;
15
+ private gameDataPrefix;
16
+ private read;
17
+ private write;
18
+ private kvGet;
19
+ private kvSet;
20
+ private kvDelete;
21
+ private kvList;
22
+ }
@@ -0,0 +1,78 @@
1
+ import { RpcError } from '../Transport.js';
2
+ /**
3
+ * Fallback transport when the game runs without a host (e.g., opened directly
4
+ * outside umicat-home-ui) or when the viewer is anonymous. Data lives in
5
+ * localStorage, keyed by gameId. No network calls. Quotas mirror the backend.
6
+ */
7
+ export class LocalStorageTransport {
8
+ constructor(gameId) {
9
+ this.gameId = gameId;
10
+ this.kind = 'standalone';
11
+ this.user = null;
12
+ }
13
+ async call(method, params) {
14
+ switch (method) {
15
+ case 'saves.get': return this.kvGet(this.savesPrefix(), params);
16
+ case 'saves.set': return this.kvSet(this.savesPrefix(), params);
17
+ case 'saves.delete': return this.kvDelete(this.savesPrefix(), params);
18
+ case 'saves.list': return this.kvList(this.savesPrefix());
19
+ case 'gameData.get': return this.kvGet(this.gameDataPrefix(), params);
20
+ case 'gameData.set': return this.kvSet(this.gameDataPrefix(), params);
21
+ case 'gameData.delete': return this.kvDelete(this.gameDataPrefix(), params);
22
+ case 'gameData.list': return this.kvList(this.gameDataPrefix());
23
+ default: throw new RpcError('UNKNOWN_METHOD', `Unknown method: ${method}`);
24
+ }
25
+ }
26
+ savesPrefix() { return `umicat:saves:${this.gameId}:`; }
27
+ gameDataPrefix() { return `umicat:gameData:${this.gameId}:`; }
28
+ read(prefix, key) {
29
+ if (typeof localStorage === 'undefined')
30
+ return null;
31
+ const raw = localStorage.getItem(prefix + key);
32
+ if (!raw)
33
+ return null;
34
+ try {
35
+ return JSON.parse(raw);
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ write(prefix, key, rec) {
42
+ if (typeof localStorage === 'undefined')
43
+ throw new RpcError('UNAVAILABLE', 'localStorage unavailable');
44
+ localStorage.setItem(prefix + key, JSON.stringify(rec));
45
+ }
46
+ kvGet(prefix, { key }) {
47
+ const rec = this.read(prefix, key);
48
+ return rec ? { value: rec.value, version: rec.version } : { value: null, version: null };
49
+ }
50
+ kvSet(prefix, { key, value, ifVersion }) {
51
+ const existing = this.read(prefix, key);
52
+ if (ifVersion !== undefined && existing && existing.version !== ifVersion) {
53
+ throw new RpcError('VERSION_MISMATCH', `Expected version ${ifVersion} but stored is ${existing.version}`);
54
+ }
55
+ const next = { value, version: (existing?.version ?? 0) + 1 };
56
+ this.write(prefix, key, next);
57
+ return { version: next.version };
58
+ }
59
+ kvDelete(prefix, { key }) {
60
+ if (typeof localStorage === 'undefined')
61
+ return { deleted: false };
62
+ const full = prefix + key;
63
+ const existed = localStorage.getItem(full) !== null;
64
+ localStorage.removeItem(full);
65
+ return { deleted: existed };
66
+ }
67
+ kvList(prefix) {
68
+ if (typeof localStorage === 'undefined')
69
+ return { keys: [] };
70
+ const keys = [];
71
+ for (let i = 0; i < localStorage.length; i++) {
72
+ const k = localStorage.key(i);
73
+ if (k && k.startsWith(prefix))
74
+ keys.push(k.slice(prefix.length));
75
+ }
76
+ return { keys };
77
+ }
78
+ }
@@ -0,0 +1,28 @@
1
+ import type { Transport } from '../Transport.js';
2
+ import { type UmicatUser } from '../../protocol.js';
3
+ /**
4
+ * Connects to a parent window that implements the Umicat RPC host protocol.
5
+ * Used when the game is loaded inside umicat-home-ui.
6
+ */
7
+ export declare class PostMessageTransport implements Transport {
8
+ private parent;
9
+ readonly kind: "umicat-home-ui";
10
+ user: UmicatUser | null;
11
+ gameId: string;
12
+ realtimeUrl?: string;
13
+ private pending;
14
+ private seq;
15
+ private constructor();
16
+ /**
17
+ * Perform the handshake. Resolves once parent has replied with `umicat:init`.
18
+ * Resolves to null if no response within `timeoutMs`.
19
+ *
20
+ * The hello is retried at `helloRetryMs` intervals because the parent's
21
+ * RPC-host message listener may not be mounted yet when the iframe first
22
+ * fires hello — especially on slower mobile devices where React takes
23
+ * longer to boot. One-shot hello was losing the handshake on Android.
24
+ */
25
+ static connect(timeoutMs?: number, sdkVersion?: string, helloRetryMs?: number): Promise<PostMessageTransport | null>;
26
+ private installResultListener;
27
+ call<T = unknown>(method: string, params?: unknown): Promise<T>;
28
+ }
@@ -0,0 +1,105 @@
1
+ import { RpcError } from '../Transport.js';
2
+ import { PROTOCOL_VERSION, } from '../../protocol.js';
3
+ /**
4
+ * Connects to a parent window that implements the Umicat RPC host protocol.
5
+ * Used when the game is loaded inside umicat-home-ui.
6
+ */
7
+ export class PostMessageTransport {
8
+ constructor(parent) {
9
+ this.parent = parent;
10
+ this.kind = 'umicat-home-ui';
11
+ this.user = null;
12
+ this.gameId = '';
13
+ this.pending = new Map();
14
+ this.seq = 0;
15
+ }
16
+ /**
17
+ * Perform the handshake. Resolves once parent has replied with `umicat:init`.
18
+ * Resolves to null if no response within `timeoutMs`.
19
+ *
20
+ * The hello is retried at `helloRetryMs` intervals because the parent's
21
+ * RPC-host message listener may not be mounted yet when the iframe first
22
+ * fires hello — especially on slower mobile devices where React takes
23
+ * longer to boot. One-shot hello was losing the handshake on Android.
24
+ */
25
+ static async connect(timeoutMs = 5000, sdkVersion = '0.0.0', helloRetryMs = 200) {
26
+ if (typeof window === 'undefined' || window.parent === window)
27
+ return null;
28
+ const transport = new PostMessageTransport(window.parent);
29
+ const hello = {
30
+ type: 'umicat:hello',
31
+ protocolVersion: PROTOCOL_VERSION,
32
+ sdkVersion,
33
+ };
34
+ const init = await new Promise((resolve) => {
35
+ let settled = false;
36
+ const finish = (value) => {
37
+ if (settled)
38
+ return;
39
+ settled = true;
40
+ window.removeEventListener('message', onMessage);
41
+ clearInterval(helloInterval);
42
+ clearTimeout(timeoutHandle);
43
+ resolve(value);
44
+ };
45
+ const onMessage = (event) => {
46
+ if (event.source !== window.parent)
47
+ return;
48
+ const data = event.data;
49
+ if (!data || data.type !== 'umicat:init')
50
+ return;
51
+ finish(data);
52
+ };
53
+ window.addEventListener('message', onMessage);
54
+ // Fire the first hello immediately, then keep retrying until init
55
+ // arrives or timeout fires. Retries are cheap (a postMessage with a
56
+ // small payload); the parent's RPC host replies on first valid hello.
57
+ transport.parent.postMessage(hello, '*');
58
+ const helloInterval = setInterval(() => {
59
+ transport.parent.postMessage(hello, '*');
60
+ }, helloRetryMs);
61
+ const timeoutHandle = setTimeout(() => finish(null), timeoutMs);
62
+ });
63
+ if (!init)
64
+ return null;
65
+ if (init.protocolVersion !== PROTOCOL_VERSION) {
66
+ console.warn('[UmicatSDK] protocol version mismatch', init.protocolVersion, PROTOCOL_VERSION);
67
+ }
68
+ transport.user = init.user;
69
+ transport.gameId = init.gameId;
70
+ transport.realtimeUrl = init.realtimeUrl;
71
+ transport.installResultListener();
72
+ return transport;
73
+ }
74
+ installResultListener() {
75
+ window.addEventListener('message', (event) => {
76
+ if (event.source !== this.parent)
77
+ return;
78
+ const data = event.data;
79
+ if (!data || data.type !== 'umicat:rpc.result')
80
+ return;
81
+ const pending = this.pending.get(data.id);
82
+ if (!pending)
83
+ return;
84
+ this.pending.delete(data.id);
85
+ if (data.ok)
86
+ pending.resolve(data.result);
87
+ else
88
+ pending.reject(new RpcError(data.error.code, data.error.message));
89
+ });
90
+ }
91
+ call(method, params) {
92
+ const id = `${++this.seq}-${Date.now()}`;
93
+ const message = { type: 'umicat:rpc', id, method, params };
94
+ return new Promise((resolve, reject) => {
95
+ this.pending.set(id, { resolve: resolve, reject });
96
+ this.parent.postMessage(message, '*');
97
+ setTimeout(() => {
98
+ if (this.pending.has(id)) {
99
+ this.pending.delete(id);
100
+ reject(new RpcError('TIMEOUT', `RPC ${method} timed out`));
101
+ }
102
+ }, 10000);
103
+ });
104
+ }
105
+ }
@@ -0,0 +1,114 @@
1
+ import Phaser from 'phaser';
2
+ import { AssetRecord } from '../scene/types.js';
3
+ import { TilemapEditOp } from '../protocol.js';
4
+ export declare function setupEditorBridge(game: Phaser.Game): void;
5
+ /**
6
+ * FB.9a — return ALL entities at the given world coords, sorted by depth
7
+ * DESC (topmost first). Used by EditorOverlayScene's Alt+click handler
8
+ * to cycle through overlapping entities. When no Alt held, the regular
9
+ * hitTest (returning just topmost) handles normal selection.
10
+ */
11
+ export declare function hitTestAll(game: Phaser.Game, worldX: number, worldY: number): Phaser.GameObjects.GameObject[];
12
+ /**
13
+ * Apply pan/zoom to the world scene's camera + mirror to the editor
14
+ * overlay scene's camera so the overlay graphics (selection rect, world-
15
+ * bounds rect, hitbox debug) draw in the right world position.
16
+ *
17
+ * Scoped to world + overlay only — HUD has an identity camera by design
18
+ * (widgets are anchor-positioned to canvas edges), so panning/zooming it
19
+ * would visually rip HUD widgets off their anchors.
20
+ *
21
+ * Exported for `EditorOverlayScene` so the in-canvas wheel + middle-click
22
+ * + space+drag handlers route through the same mutation function as the
23
+ * host-driven `umicat:editor:panZoom` message.
24
+ */
25
+ /**
26
+ * Apply pan/zoom to the editor camera (NOT the game's runtime camera).
27
+ *
28
+ * P1 infinite canvas (2026-05-17). Mutates `editorCam` on the world scene
29
+ * + the overlay scene's mirror. `cameras.main` (the game's runtime cam)
30
+ * is never touched in edit mode — the game's intended camera position is
31
+ * preserved exactly as the game code left it, and surfaces in the editor
32
+ * as a dashed "Camera" rect (drawn by `EditorOverlayScene.update`). This
33
+ * matches Godot 2D editor's "editor viewport vs Camera2D" split.
34
+ *
35
+ * Exported for `EditorOverlayScene` so the in-canvas wheel + middle-click
36
+ * + space+drag handlers route through the same mutation function as the
37
+ * host-driven `umicat:editor:panZoom` message. Renamed from the misleading
38
+ * `applyPanZoomToWorld` (which sounded like "pan the world camera").
39
+ */
40
+ export declare function applyEditorPanZoom(game: Phaser.Game, msg: {
41
+ scrollX?: number;
42
+ scrollY?: number;
43
+ zoom?: number;
44
+ relative?: boolean;
45
+ }): void;
46
+ /**
47
+ * Back-compat alias for callers (e.g. older host messages, internal call
48
+ * sites) referencing the old name. Keep until we're sure nothing still
49
+ * relies on it.
50
+ */
51
+ export declare const applyPanZoomToWorld: typeof applyEditorPanZoom;
52
+ /**
53
+ * Post the editor camera's current state to the host so it can render the
54
+ * zoom indicator + reset button, and decide which Hierarchy entries are
55
+ * off-screen. Falls back to the game's cameras.main when editorCam isn't
56
+ * installed yet (race during enterEdit).
57
+ */
58
+ export declare function postCameraState(game: Phaser.Game): void;
59
+ /**
60
+ * Read-only access to the editor camera. Null when edit mode hasn't been
61
+ * entered yet OR was just exited. Used by `applyEditorPanZoom`,
62
+ * `postCameraState`, and the overlay scene's mirror in `create`.
63
+ */
64
+ export declare function getEditorCamera(game: Phaser.Game): Phaser.Cameras.Scene2D.Camera | null;
65
+ /**
66
+ * Apply one or more tilemap edit ops to the live Phaser TilemapLayer(s).
67
+ *
68
+ * Called from two paths:
69
+ * 1. Host pushes `umicat:editor:editTilemap` (agent writes, undo/redo
70
+ * replay) — see EditorBridge.handleMessage dispatch.
71
+ * 2. SDK's pointer-up handler (live painter stroke) — calls this directly
72
+ * after composing the stroke's accumulated cells into one op, before
73
+ * posting `umicat:editor:tilemapEdited` to the host.
74
+ *
75
+ * Mirror of handleEditPrefab's pattern: lookup container in registry → find
76
+ * matching layer child by `tilemapLayerId` data tag → apply Phaser tilemap
77
+ * mutation API. Soft-fails (warn + skip) on missing entity / missing layer
78
+ * / out-of-bounds cell so a stale op doesn't crash the runtime.
79
+ */
80
+ /**
81
+ * Apply tilemap edit ops to live runtime. Public because the editor's
82
+ * own drag-resize commit (in EditorOverlayScene) needs to apply the op
83
+ * locally before posting `tilemapEdited` for host undo recording —
84
+ * same pattern as brush/rect/fill commits (apply local, post for undo).
85
+ * Skipping the local apply leaves SDK runtime + host draft out of sync
86
+ * (host knows the new size, SDK keeps rendering old size → bounds snap-
87
+ * back UX).
88
+ *
89
+ * Async because addLayer ops can reference a fresh tileset whose
90
+ * texture hasn't loaded yet. Caller supplies optional `manifestAsset`
91
+ * (same pattern as createEntity's drag-to-place) — handler upserts
92
+ * cache + AWAITS texture load before invoking applyTilemapStructureOp.
93
+ * Without the await, addLayer's `textures.exists()` check fails on
94
+ * the in-flight load → skips layer creation → painter can't find
95
+ * layer → click falls through to drag-the-entity.
96
+ */
97
+ export declare function handleEditTilemap(game: Phaser.Game, entityId: string, ops: TilemapEditOp[], manifestAsset?: AssetRecord): Promise<void>;
98
+ /**
99
+ * Find the TilemapLayer tagged with `layerId` belonging to the given tilemap
100
+ * container. Layers live in scene root (not as container children) because
101
+ * Phaser's TilemapLayer doesn't inherit parent Container transforms — see
102
+ * `createTilemap` for the rationale. The container's data manager stashes
103
+ * a `tilemapLayers` array of refs so we can find them without a scene walk.
104
+ */
105
+ export declare function findTilemapLayerById(container: Phaser.GameObjects.Container, layerId: string): Phaser.Tilemaps.TilemapLayer | null;
106
+ /**
107
+ * Apply a single op to a live TilemapLayer using Phaser's native mutation
108
+ * APIs. All ops are in-place — no full re-render needed. Out-of-bounds
109
+ * cells are tolerated by Phaser's `putTileAt` (no-op outside layer bounds).
110
+ *
111
+ * The optional `asset` arg is required for `autotilePaint` (the only op
112
+ * that needs to resolve a terrain's ruleMap); other ops ignore it.
113
+ */
114
+ export declare function applyTilemapOp(layer: Phaser.Tilemaps.TilemapLayer, op: TilemapEditOp, asset?: AssetRecord | null): void;