@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
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Transport } from '../core/Transport.js';
|
|
2
|
+
import { UmicatRoom } from './UmicatRoom.js';
|
|
3
|
+
export interface RoomListEntry {
|
|
4
|
+
/** Opaque Colyseus room id — pass to `joinById` to connect. */
|
|
5
|
+
roomId: string;
|
|
6
|
+
/** Short code the host passed on create (empty string for default rooms). */
|
|
7
|
+
roomCode: string;
|
|
8
|
+
/** Current number of connected clients. */
|
|
9
|
+
clients: number;
|
|
10
|
+
/** Max clients the host set (server-clamped to [1, 16]). */
|
|
11
|
+
maxClients: number;
|
|
12
|
+
/** Whatever the host attached via `room.setMetadata(...)`. Unvalidated. */
|
|
13
|
+
metadata?: unknown;
|
|
14
|
+
/** Server-side creation time (ms since epoch). */
|
|
15
|
+
createdAt?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface RoomListOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Filter to rooms with this exact roomCode. Useful for "is a room with code
|
|
20
|
+
* X up yet?" polling without enumerating the whole list.
|
|
21
|
+
*/
|
|
22
|
+
roomCode?: string;
|
|
23
|
+
/** Max entries to return. Server caps to 100 regardless. */
|
|
24
|
+
limit?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface JoinOptions extends Record<string, unknown> {
|
|
27
|
+
/** Optional display name passed to the server-side room on join. */
|
|
28
|
+
displayName?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Short code that distinguishes independent rooms for the same game.
|
|
31
|
+
* Empty / omitted means "the one shared room per game."
|
|
32
|
+
*
|
|
33
|
+
* Pass a non-empty value on `joinOrCreate` / `create` / `join` to bucket
|
|
34
|
+
* players into a private room. Two clients with the same `(gameId, roomCode)`
|
|
35
|
+
* land in the same room; different `roomCode` values create separate rooms.
|
|
36
|
+
* This is how you implement "Create Room" / "Join with code" UX — the
|
|
37
|
+
* server's `filterBy(["gameId", "roomCode"])` enforces the separation.
|
|
38
|
+
*
|
|
39
|
+
* Not a password: anyone who knows the code can join. Treat as an
|
|
40
|
+
* unguessability measure, not an access control.
|
|
41
|
+
*/
|
|
42
|
+
roomCode?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Cap on concurrent clients in the room the creator makes. Server clamps
|
|
45
|
+
* to [1, 16]. Defaults to 8 server-side. Only honored by the FIRST client
|
|
46
|
+
* (the creator) — later joiners see whatever the creator set.
|
|
47
|
+
*/
|
|
48
|
+
maxClients?: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Multiplayer rooms backed by umicat-realtime-service (Colyseus under the hood).
|
|
52
|
+
*
|
|
53
|
+
* Availability: only when the host provided a `realtimeUrl` during the
|
|
54
|
+
* handshake (i.e. connected to umicat-home-ui with multiplayer enabled).
|
|
55
|
+
* Unavailable in standalone/anonymous mode — methods throw RpcError
|
|
56
|
+
* 'REALTIME_UNAVAILABLE'.
|
|
57
|
+
*
|
|
58
|
+
* Tokens are fetched per connection attempt via the `realtime.getToken`
|
|
59
|
+
* RPC — the iframe never holds a long-lived credential.
|
|
60
|
+
*/
|
|
61
|
+
export declare class RealtimeModule {
|
|
62
|
+
private readonly transport;
|
|
63
|
+
private readonly realtimeUrl;
|
|
64
|
+
private client;
|
|
65
|
+
constructor(transport: Transport, realtimeUrl: string | undefined);
|
|
66
|
+
/** `true` when a realtime endpoint was provided by the host. */
|
|
67
|
+
get available(): boolean;
|
|
68
|
+
joinOrCreate<State = unknown>(roomType: string, options?: JoinOptions): Promise<UmicatRoom<State>>;
|
|
69
|
+
create<State = unknown>(roomType: string, options?: JoinOptions): Promise<UmicatRoom<State>>;
|
|
70
|
+
join<State = unknown>(roomType: string, options?: JoinOptions): Promise<UmicatRoom<State>>;
|
|
71
|
+
joinById<State = unknown>(roomId: string, options?: JoinOptions): Promise<UmicatRoom<State>>;
|
|
72
|
+
/**
|
|
73
|
+
* List the open rooms for the caller's game, as seen by the realtime
|
|
74
|
+
* service right now. Results are scoped server-side to the caller's
|
|
75
|
+
* gameId — you can't enumerate rooms from a different game.
|
|
76
|
+
*
|
|
77
|
+
* Use this to build a lobby browser ("show me open rooms, click to join").
|
|
78
|
+
* Poll on a timer or on a user's "Refresh" click; the API is request/
|
|
79
|
+
* response, not a live subscription.
|
|
80
|
+
*
|
|
81
|
+
* Example:
|
|
82
|
+
* ```ts
|
|
83
|
+
* const rooms = await umicat.rooms.list();
|
|
84
|
+
* rooms
|
|
85
|
+
* .filter(r => r.clients < r.maxClients)
|
|
86
|
+
* .forEach(r => {
|
|
87
|
+
* // render a button; on click: umicat.rooms.joinById(r.roomId)
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
list(options?: RoomListOptions): Promise<RoomListEntry[]>;
|
|
92
|
+
private connect;
|
|
93
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { RpcError } from '../core/Transport.js';
|
|
2
|
+
import { UmicatRoom } from './UmicatRoom.js';
|
|
3
|
+
/**
|
|
4
|
+
* Multiplayer rooms backed by umicat-realtime-service (Colyseus under the hood).
|
|
5
|
+
*
|
|
6
|
+
* Availability: only when the host provided a `realtimeUrl` during the
|
|
7
|
+
* handshake (i.e. connected to umicat-home-ui with multiplayer enabled).
|
|
8
|
+
* Unavailable in standalone/anonymous mode — methods throw RpcError
|
|
9
|
+
* 'REALTIME_UNAVAILABLE'.
|
|
10
|
+
*
|
|
11
|
+
* Tokens are fetched per connection attempt via the `realtime.getToken`
|
|
12
|
+
* RPC — the iframe never holds a long-lived credential.
|
|
13
|
+
*/
|
|
14
|
+
export class RealtimeModule {
|
|
15
|
+
constructor(transport, realtimeUrl) {
|
|
16
|
+
this.transport = transport;
|
|
17
|
+
this.realtimeUrl = realtimeUrl;
|
|
18
|
+
this.client = null;
|
|
19
|
+
}
|
|
20
|
+
/** `true` when a realtime endpoint was provided by the host. */
|
|
21
|
+
get available() {
|
|
22
|
+
return typeof this.realtimeUrl === 'string' && this.realtimeUrl.length > 0;
|
|
23
|
+
}
|
|
24
|
+
async joinOrCreate(roomType, options = {}) {
|
|
25
|
+
return this.connect(roomType, options, 'joinOrCreate');
|
|
26
|
+
}
|
|
27
|
+
async create(roomType, options = {}) {
|
|
28
|
+
return this.connect(roomType, options, 'create');
|
|
29
|
+
}
|
|
30
|
+
async join(roomType, options = {}) {
|
|
31
|
+
return this.connect(roomType, options, 'join');
|
|
32
|
+
}
|
|
33
|
+
async joinById(roomId, options = {}) {
|
|
34
|
+
return this.connect(roomId, options, 'joinById');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* List the open rooms for the caller's game, as seen by the realtime
|
|
38
|
+
* service right now. Results are scoped server-side to the caller's
|
|
39
|
+
* gameId — you can't enumerate rooms from a different game.
|
|
40
|
+
*
|
|
41
|
+
* Use this to build a lobby browser ("show me open rooms, click to join").
|
|
42
|
+
* Poll on a timer or on a user's "Refresh" click; the API is request/
|
|
43
|
+
* response, not a live subscription.
|
|
44
|
+
*
|
|
45
|
+
* Example:
|
|
46
|
+
* ```ts
|
|
47
|
+
* const rooms = await umicat.rooms.list();
|
|
48
|
+
* rooms
|
|
49
|
+
* .filter(r => r.clients < r.maxClients)
|
|
50
|
+
* .forEach(r => {
|
|
51
|
+
* // render a button; on click: umicat.rooms.joinById(r.roomId)
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
async list(options = {}) {
|
|
56
|
+
if (!this.available) {
|
|
57
|
+
throw new RpcError('REALTIME_UNAVAILABLE', 'Multiplayer is not available for this host. Umicat.rooms.list requires running inside umicat-home-ui with realtime enabled.');
|
|
58
|
+
}
|
|
59
|
+
const { token } = await this.transport.call('realtime.getToken', {});
|
|
60
|
+
const url = new URL(toHttpBase(this.realtimeUrl) + '/rooms');
|
|
61
|
+
url.searchParams.set('gameId', this.transport.gameId);
|
|
62
|
+
if (typeof options.roomCode === 'string')
|
|
63
|
+
url.searchParams.set('roomCode', options.roomCode);
|
|
64
|
+
if (typeof options.limit === 'number' && Number.isFinite(options.limit)) {
|
|
65
|
+
url.searchParams.set('limit', String(Math.floor(options.limit)));
|
|
66
|
+
}
|
|
67
|
+
const res = await fetch(url.toString(), {
|
|
68
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
69
|
+
});
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
let detail = '';
|
|
72
|
+
try {
|
|
73
|
+
const body = await res.json();
|
|
74
|
+
if (body && typeof body.error === 'string')
|
|
75
|
+
detail = `: ${body.error}`;
|
|
76
|
+
}
|
|
77
|
+
catch { }
|
|
78
|
+
throw new RpcError('ROOMS_LIST_FAILED', `/rooms returned ${res.status}${detail}`);
|
|
79
|
+
}
|
|
80
|
+
const body = await res.json();
|
|
81
|
+
if (!body || !Array.isArray(body.rooms)) {
|
|
82
|
+
throw new RpcError('ROOMS_LIST_FAILED', '/rooms response missing rooms[]');
|
|
83
|
+
}
|
|
84
|
+
return body.rooms;
|
|
85
|
+
}
|
|
86
|
+
async connect(nameOrId, options, method) {
|
|
87
|
+
if (!this.available) {
|
|
88
|
+
throw new RpcError('REALTIME_UNAVAILABLE', 'Multiplayer is not available for this host. Umicat.rooms requires running inside umicat-home-ui with realtime enabled.');
|
|
89
|
+
}
|
|
90
|
+
if (!this.client) {
|
|
91
|
+
// Dynamic import so single-player games do not pull Colyseus into their
|
|
92
|
+
// bundle. Vite tree-shakes this path when `rooms` is never referenced.
|
|
93
|
+
const { Client } = await import('colyseus.js');
|
|
94
|
+
this.client = new Client(this.realtimeUrl);
|
|
95
|
+
}
|
|
96
|
+
const { token } = await this.transport.call('realtime.getToken', {});
|
|
97
|
+
// Inject gameId from the transport so the server-side gameId check sees
|
|
98
|
+
// a value consistent with the JWT — games never pass this directly.
|
|
99
|
+
const merged = { ...options, gameId: this.transport.gameId };
|
|
100
|
+
this.client.auth.token = token;
|
|
101
|
+
const room = await this.client[method](nameOrId, merged);
|
|
102
|
+
return new UmicatRoom(room);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Convert the realtime WebSocket base (wss://ws.unboxy.com/rt or ws://…) to
|
|
106
|
+
// the matching HTTP base used for the /rooms REST endpoint. Colyseus's HTTP
|
|
107
|
+
// routes (/matchmake, /rooms, /health) ride the same nginx vhost + path
|
|
108
|
+
// prefix as the WebSocket, so this is just a scheme swap.
|
|
109
|
+
function toHttpBase(wsUrl) {
|
|
110
|
+
if (wsUrl.startsWith('wss://'))
|
|
111
|
+
return 'https://' + wsUrl.slice('wss://'.length);
|
|
112
|
+
if (wsUrl.startsWith('ws://'))
|
|
113
|
+
return 'http://' + wsUrl.slice('ws://'.length);
|
|
114
|
+
return wsUrl;
|
|
115
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { Room } from 'colyseus.js';
|
|
2
|
+
type Unsubscribe = () => void;
|
|
3
|
+
/**
|
|
4
|
+
* Facade for per-player state — one entry per player, only the owner can write
|
|
5
|
+
* to their own entry. Delta-synced via Colyseus schema.
|
|
6
|
+
*/
|
|
7
|
+
export declare class PlayerDataFacade {
|
|
8
|
+
private readonly room;
|
|
9
|
+
constructor(room: Room<unknown>);
|
|
10
|
+
/** Write a JSON-serializable value to the caller's own player data. */
|
|
11
|
+
set(key: string, value: unknown): void;
|
|
12
|
+
/** Remove a key from the caller's own player data. */
|
|
13
|
+
delete(key: string): void;
|
|
14
|
+
/** Read a player's data value. Returns null if the player or key is absent. */
|
|
15
|
+
get<T = unknown>(sessionId: string, key: string): T | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Facade for room-scope shared state — one map per room, any client may write.
|
|
19
|
+
* Delta-synced via Colyseus schema. Use for shared game state like current
|
|
20
|
+
* turn, scoreboard, shared level-of-the-day, etc.
|
|
21
|
+
*/
|
|
22
|
+
export declare class RoomDataFacade {
|
|
23
|
+
private readonly room;
|
|
24
|
+
constructor(room: Room<unknown>);
|
|
25
|
+
/** Write a JSON-serializable value into the room's shared data map. */
|
|
26
|
+
set(key: string, value: unknown): void;
|
|
27
|
+
/** Remove a key from the room's shared data map. */
|
|
28
|
+
delete(key: string): void;
|
|
29
|
+
/** Read a shared room value. Returns null if the key is absent. */
|
|
30
|
+
get<T = unknown>(key: string): T | null;
|
|
31
|
+
}
|
|
32
|
+
/** Maximum text length accepted by `ChatFacade.send`. Longer input is silently
|
|
33
|
+
* truncated. Games should mirror this on their input box's `maxLength`. */
|
|
34
|
+
export declare const MAX_CHAT_TEXT_LEN = 500;
|
|
35
|
+
/**
|
|
36
|
+
* Discriminator for `ChatMessage`:
|
|
37
|
+
* - `'user'` — a real player message sent via `ChatFacade.send`.
|
|
38
|
+
* - `'system.joined'` — auto-emitted locally when a remote player enters the room.
|
|
39
|
+
* - `'system.left'` — auto-emitted locally when a player leaves the room.
|
|
40
|
+
*
|
|
41
|
+
* System messages are computed locally on each client from `room.state.players`
|
|
42
|
+
* add/remove events; they never go over the wire. The first joiner does not
|
|
43
|
+
* see "X joined" for players already in the room when they themselves arrived
|
|
44
|
+
* (initial hydration is suppressed).
|
|
45
|
+
*/
|
|
46
|
+
export type ChatMessageKind = 'user' | 'system.joined' | 'system.left';
|
|
47
|
+
/** Canonical chat-message shape delivered to `ChatFacade.onMessage` handlers.
|
|
48
|
+
* See umicat-design/features/multiplayer-chat.md §3 for the rationale on
|
|
49
|
+
* each field. */
|
|
50
|
+
export interface ChatMessage {
|
|
51
|
+
/** What kind of message this is — see `ChatMessageKind`. Default `'user'`. */
|
|
52
|
+
kind: ChatMessageKind;
|
|
53
|
+
/** For user messages: sender's session id (equals `room.sessionId` on the
|
|
54
|
+
* sender's local-echoed copy). For system messages: the joined/left player's
|
|
55
|
+
* session id. */
|
|
56
|
+
from: string;
|
|
57
|
+
/** Sender's (or subject's) display name at the time of the message. Empty if
|
|
58
|
+
* the Player entry carried no displayName. */
|
|
59
|
+
displayName: string;
|
|
60
|
+
/** For user messages: the trimmed, length-capped text. For system messages:
|
|
61
|
+
* a sensible English fallback (e.g. `"Alice joined"`). Games doing their own
|
|
62
|
+
* i18n should switch on `kind` and ignore this. */
|
|
63
|
+
text: string;
|
|
64
|
+
/** Sender-side `Date.now()` for user messages, local `Date.now()` for system
|
|
65
|
+
* messages. Advisory: order chat by arrival, not by ts. */
|
|
66
|
+
ts: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Convenience layer over the transient relay for in-game chat. Wraps
|
|
70
|
+
* `room.send('chat', ...)` / `room.on('chat', ...)` with three things every
|
|
71
|
+
* game would otherwise re-implement inconsistently:
|
|
72
|
+
* - Local echo so the sender's own message hits the same handler stream
|
|
73
|
+
* immediately (synchronous, before the network call).
|
|
74
|
+
* - A canonical `ChatMessage` payload shape every chat in every game shares.
|
|
75
|
+
* - Outbound `text.trim()` + length-cap to MAX_CHAT_TEXT_LEN, so a stray
|
|
76
|
+
* paste cannot flood the relay.
|
|
77
|
+
*
|
|
78
|
+
* `displayName` is read from the sender's Player entry at send time and
|
|
79
|
+
* stamped onto the payload so receivers don't have to chase room.state to
|
|
80
|
+
* resolve a sessionId. See umicat-design/features/multiplayer-chat.md §3.4.
|
|
81
|
+
*/
|
|
82
|
+
export declare class ChatFacade {
|
|
83
|
+
private readonly room;
|
|
84
|
+
private readonly handlers;
|
|
85
|
+
private remoteOff;
|
|
86
|
+
/** Sids known to this facade, mapped to the displayName they had at the
|
|
87
|
+
* time we last observed them. Populated either at construction (if state
|
|
88
|
+
* was already synced) or on the first `onStateChange` (initial hydration
|
|
89
|
+
* — silent, no system messages). Subsequent state changes diff against
|
|
90
|
+
* this map to emit `system.joined` / `system.left`. We track displayName
|
|
91
|
+
* here because by the time a player is detected as "left" they're already
|
|
92
|
+
* gone from `state.players` and we can't read it from there. */
|
|
93
|
+
private readonly knownNames;
|
|
94
|
+
/** Becomes true the first time we observe a usable `state.players` map.
|
|
95
|
+
* The very first observation is treated as "initial state arrived" and
|
|
96
|
+
* must NOT fire `system.joined` for the players already in the room when
|
|
97
|
+
* the local user joined — only subsequent diffs are real lifecycle events. */
|
|
98
|
+
private hydrated;
|
|
99
|
+
constructor(room: Room<unknown>);
|
|
100
|
+
/**
|
|
101
|
+
* Send a chat line. Empty-after-trim input is dropped silently. Text longer
|
|
102
|
+
* than MAX_CHAT_TEXT_LEN is silently truncated. Local echo fires
|
|
103
|
+
* synchronously before the wire send, so the input box can be cleared on
|
|
104
|
+
* the same tick.
|
|
105
|
+
*
|
|
106
|
+
* Returns a Promise that resolves once dispatch is complete (local echo +
|
|
107
|
+
* wire send queued). Rejects if the underlying `room.send` throws (e.g.
|
|
108
|
+
* the connection has dropped). Note: resolution does NOT confirm remote
|
|
109
|
+
* delivery — the underlying transient relay is fire-and-forget. Use this
|
|
110
|
+
* for catching local-side errors; do not treat it as an ack.
|
|
111
|
+
*/
|
|
112
|
+
send(text: string): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Subscribe to chat messages — both remote and local-echoed (your own), plus
|
|
115
|
+
* auto-emitted `'system.joined'` / `'system.left'` events. Returns an
|
|
116
|
+
* unsubscribe function. Call it on scene shutdown.
|
|
117
|
+
*/
|
|
118
|
+
onMessage(handler: (msg: ChatMessage) => void): Unsubscribe;
|
|
119
|
+
/**
|
|
120
|
+
* Hook `onStateChange` to detect joins / leaves and try an eager snapshot
|
|
121
|
+
* if `state.players` is already populated.
|
|
122
|
+
*
|
|
123
|
+
* **The onStateChange subscription must always be set up, even when
|
|
124
|
+
* `state.players` is `undefined` at construction.** Colyseus 0.16 (and
|
|
125
|
+
* earlier) deliver the initial state as a *separate* message that arrives
|
|
126
|
+
* a few ms after `client.joinOrCreate` resolves — so right when UmicatRoom
|
|
127
|
+
* is constructed, `room.state.players` is typically `undefined`. An earlier
|
|
128
|
+
* 0.2.14 implementation early-returned in that case and never subscribed,
|
|
129
|
+
* which meant the diff machinery never armed and BOTH `system.joined` /
|
|
130
|
+
* `system.left` silently dead-stopped in production (Blokus chat game,
|
|
131
|
+
* 2026-04-27).
|
|
132
|
+
*
|
|
133
|
+
* Eager snapshot if `state.players` is already there sets `hydrated=true`
|
|
134
|
+
* so the first state change does a real diff. Otherwise the first state
|
|
135
|
+
* change is treated as initial hydration: populate `knownNames` silently,
|
|
136
|
+
* skip system messages, set `hydrated=true`. Subsequent state changes do
|
|
137
|
+
* the real lifecycle diff.
|
|
138
|
+
*
|
|
139
|
+
* Why `onStateChange` and not `MapSchema.onAdd` / `.onRemove`: Colyseus
|
|
140
|
+
* 0.16 dropped those instance methods in favour of a separate
|
|
141
|
+
* `getStateCallbacks(room)` proxy API. Hooking `onStateChange` and diffing
|
|
142
|
+
* `players` keys ourselves works the same in any Colyseus version, keeps
|
|
143
|
+
* us decoupled from the realtime backend's callback shape, and adds < 1ms
|
|
144
|
+
* of work per state change for typical room sizes (max 16 players).
|
|
145
|
+
*/
|
|
146
|
+
private subscribeToPlayers;
|
|
147
|
+
/** Populate `knownNames` from `state.players` if it's already available
|
|
148
|
+
* and mark hydrated. Silent — never emits system messages. A successful
|
|
149
|
+
* `forEach` (even iterating zero items) means the schema is synced and
|
|
150
|
+
* this is "what the room looked like when we joined" — safe to hydrate. */
|
|
151
|
+
private tryEagerSnapshot;
|
|
152
|
+
/** Compare current `players` membership against `knownNames`. On the very
|
|
153
|
+
* first invocation (post-construction) when `hydrated=false`, this is
|
|
154
|
+
* the initial-hydration call: populate `knownNames` silently and bail.
|
|
155
|
+
* Subsequent invocations diff against `knownNames` and emit
|
|
156
|
+
* `system.joined` for new sids, `system.left` for vanished sids. */
|
|
157
|
+
private diffPlayers;
|
|
158
|
+
private dispatch;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Umicat-flavored wrapper around a Colyseus Room. Exposes only the surface
|
|
162
|
+
* documented in SDK-GUIDE.md so games are portable to a different realtime
|
|
163
|
+
* backend should we migrate away from Colyseus.
|
|
164
|
+
*/
|
|
165
|
+
export declare class UmicatRoom<State = unknown> {
|
|
166
|
+
private readonly room;
|
|
167
|
+
readonly player: PlayerDataFacade;
|
|
168
|
+
readonly data: RoomDataFacade;
|
|
169
|
+
readonly chat: ChatFacade;
|
|
170
|
+
constructor(room: Room<State>);
|
|
171
|
+
get id(): string;
|
|
172
|
+
get name(): string;
|
|
173
|
+
get sessionId(): string;
|
|
174
|
+
/**
|
|
175
|
+
* Current server-authoritative state. Proxied from Colyseus Schema — read
|
|
176
|
+
* values directly. Mutations do not propagate; only server-side handlers
|
|
177
|
+
* may change state.
|
|
178
|
+
*/
|
|
179
|
+
get state(): State;
|
|
180
|
+
/** Send a transient message. Relayed to every other client in the room
|
|
181
|
+
* with `from: sessionId` stamped onto the payload. Not persisted in state. */
|
|
182
|
+
send(type: string, payload?: unknown): void;
|
|
183
|
+
/** Register a handler for a server-sent message type. Returns unsubscribe. */
|
|
184
|
+
on(type: string, handler: (payload: unknown) => void): Unsubscribe;
|
|
185
|
+
/** Fires whenever the server-authoritative state changes. */
|
|
186
|
+
onStateChange(handler: (state: State) => void): Unsubscribe;
|
|
187
|
+
/**
|
|
188
|
+
* Fires when the connection closes (kick, server shutdown, network drop).
|
|
189
|
+
* `code` follows WebSocket close codes plus Colyseus-specific ones.
|
|
190
|
+
*/
|
|
191
|
+
onLeave(handler: (code: number) => void): Unsubscribe;
|
|
192
|
+
/** Fires when the server reports an error for this room. */
|
|
193
|
+
onError(handler: (code: number, message?: string) => void): Unsubscribe;
|
|
194
|
+
/** Disconnect from the room. Resolves with the close code. */
|
|
195
|
+
leave(consented?: boolean): Promise<number>;
|
|
196
|
+
}
|
|
197
|
+
export {};
|