@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.
- package/SDK-GUIDE.md +1726 -0
- package/dist/core/Transport.d.ts +28 -0
- package/dist/core/Transport.js +7 -0
- package/dist/core/Umicat.d.ts +45 -0
- package/dist/core/Umicat.js +60 -0
- package/dist/core/UmicatGame.d.ts +43 -0
- package/dist/core/UmicatGame.js +64 -0
- package/dist/core/UmicatScene.d.ts +19 -0
- package/dist/core/UmicatScene.js +38 -0
- package/dist/core/transports/LocalStorageTransport.d.ts +22 -0
- package/dist/core/transports/LocalStorageTransport.js +78 -0
- package/dist/core/transports/PostMessageTransport.d.ts +28 -0
- package/dist/core/transports/PostMessageTransport.js +105 -0
- package/dist/editor/EditorBridge.d.ts +114 -0
- package/dist/editor/EditorBridge.js +2608 -0
- package/dist/editor/EditorOverlayScene.d.ts +333 -0
- package/dist/editor/EditorOverlayScene.js +1896 -0
- package/dist/editor/EditorState.d.ts +251 -0
- package/dist/editor/EditorState.js +197 -0
- package/dist/gamedata/GameDataModule.d.ts +45 -0
- package/dist/gamedata/GameDataModule.js +59 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +43 -0
- package/dist/orientation.d.ts +5 -0
- package/dist/orientation.js +4 -0
- package/dist/protocol.d.ts +807 -0
- package/dist/protocol.js +3 -0
- package/dist/realtime/RealtimeModule.d.ts +93 -0
- package/dist/realtime/RealtimeModule.js +115 -0
- package/dist/realtime/UmicatRoom.d.ts +197 -0
- package/dist/realtime/UmicatRoom.js +353 -0
- package/dist/recording/RecordingManager.d.ts +11 -0
- package/dist/recording/RecordingManager.js +59 -0
- package/dist/saves/SavesModule.d.ts +23 -0
- package/dist/saves/SavesModule.js +37 -0
- package/dist/scene/EditorMode.d.ts +17 -0
- package/dist/scene/EditorMode.js +22 -0
- package/dist/scene/EntityRegistry.d.ts +39 -0
- package/dist/scene/EntityRegistry.js +103 -0
- package/dist/scene/GameConfig.d.ts +60 -0
- package/dist/scene/GameConfig.js +50 -0
- package/dist/scene/HudRuntime.d.ts +131 -0
- package/dist/scene/HudRuntime.js +1224 -0
- package/dist/scene/Prefabs.d.ts +92 -0
- package/dist/scene/Prefabs.js +175 -0
- package/dist/scene/Rules.d.ts +73 -0
- package/dist/scene/Rules.js +164 -0
- package/dist/scene/SceneLoader.d.ts +118 -0
- package/dist/scene/SceneLoader.js +615 -0
- package/dist/scene/Waves.d.ts +85 -0
- package/dist/scene/Waves.js +365 -0
- package/dist/scene/autotile.d.ts +103 -0
- package/dist/scene/autotile.js +321 -0
- package/dist/scene/renderScripts.d.ts +53 -0
- package/dist/scene/renderScripts.js +67 -0
- package/dist/scene/spawnEntity.d.ts +201 -0
- package/dist/scene/spawnEntity.js +1326 -0
- package/dist/scene/types.d.ts +1166 -0
- package/dist/scene/types.js +34 -0
- package/dist/screenshot/ScreenshotManager.d.ts +14 -0
- package/dist/screenshot/ScreenshotManager.js +33 -0
- 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,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;
|