@unboxy/phaser-sdk 0.2.4 → 0.2.6
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 +122 -1
- package/dist/core/Transport.d.ts +6 -0
- package/dist/core/Unboxy.d.ts +2 -0
- package/dist/core/Unboxy.js +6 -1
- package/dist/core/transports/PostMessageTransport.d.ts +1 -0
- package/dist/core/transports/PostMessageTransport.js +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -0
- package/dist/protocol.d.ts +16 -1
- package/dist/realtime/RealtimeModule.d.ts +30 -0
- package/dist/realtime/RealtimeModule.js +54 -0
- package/dist/realtime/UnboxyRoom.d.ts +68 -0
- package/dist/realtime/UnboxyRoom.js +111 -0
- package/package.json +9 -2
package/SDK-GUIDE.md
CHANGED
|
@@ -9,7 +9,7 @@ Reference for AI agents building games on the Unboxy platform. Tracks the **inst
|
|
|
9
9
|
- `Unboxy.init()` — platform services entry point
|
|
10
10
|
- `unboxy.saves` — **per-user** key-value store (only the owner sees/writes their data)
|
|
11
11
|
- `unboxy.gameData` — **per-game** key-value store (read by anyone; write requires auth)
|
|
12
|
-
-
|
|
12
|
+
- `unboxy.rooms` — **multiplayer rooms** (server-authoritative state sync, requires sign-in and host support)
|
|
13
13
|
|
|
14
14
|
## Platform services
|
|
15
15
|
|
|
@@ -183,6 +183,125 @@ Rule of thumb: if two different players would write to the same key, it's `gameD
|
|
|
183
183
|
- **Reads are cheap and public.** No auth needed — even logged-out players see `gameData`. Don't put anything secret here.
|
|
184
184
|
- **Trust is thin.** The backend stores what you write, verbatim. A player with devtools could submit any JSON. For casual games this is fine; for competitive stakes, a future typed leaderboards API with server-side validation is the right fix.
|
|
185
185
|
|
|
186
|
+
### Multiplayer rooms — `unboxy.rooms`
|
|
187
|
+
|
|
188
|
+
Realtime rooms backed by unboxy-realtime-service. The platform is **game-agnostic** — the server tracks who's in the room and relays data, but does not know what kind of game you're building. Two opaque primitives are your building blocks:
|
|
189
|
+
|
|
190
|
+
1. **Delta-synced KV state** (`room.player.*`, `room.data.*`) — server-maintained maps. Values are JSON-serializable. Every client sees changes via `onStateChange`. State is preserved while the room exists; new joiners see the current snapshot.
|
|
191
|
+
2. **Transient message relay** (`room.send` + `room.on`) — fire-and-forget. Payloads are stamped with `from: senderSessionId` and broadcast to every other client in the room. Not persisted.
|
|
192
|
+
|
|
193
|
+
#### Join a room
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// Join or create a room scoped to this game. The SDK fetches a short-lived
|
|
197
|
+
// token from the host and forwards it; the iframe never handles credentials.
|
|
198
|
+
const room = await unboxy.rooms.joinOrCreate('lobby', {
|
|
199
|
+
displayName: unboxy.user?.name ?? 'guest',
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
room.id; // room identifier
|
|
203
|
+
room.sessionId; // this connection's session id within the room
|
|
204
|
+
room.state; // proxy of the server-authoritative schema
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### Delta-synced state — `room.player` and `room.data`
|
|
208
|
+
|
|
209
|
+
Use this for anything that represents the *current world*: positions, hp, score, turn state, scoreboard, etc. The server delta-syncs diffs to every client.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// Per-player (only you can write your own entry)
|
|
213
|
+
room.player.set('pos', { x: 120, y: 340 });
|
|
214
|
+
room.player.set('hp', 80);
|
|
215
|
+
room.player.delete('pos');
|
|
216
|
+
|
|
217
|
+
// Read another player's data at any time
|
|
218
|
+
const otherPos = room.player.get<{x:number; y:number}>(otherSessionId, 'pos');
|
|
219
|
+
|
|
220
|
+
// Per-room (any client can write)
|
|
221
|
+
room.data.set('currentTurn', playerSessionId);
|
|
222
|
+
room.data.set('scoreboard', [{ sid: 's1', score: 12 }, ...]);
|
|
223
|
+
const board = room.data.get<Entry[]>('scoreboard') ?? [];
|
|
224
|
+
|
|
225
|
+
// React to any state change
|
|
226
|
+
const offState = room.onStateChange(() => {
|
|
227
|
+
room.state.players.forEach((p, sid) => {
|
|
228
|
+
const pos = room.player.get<{x:number;y:number}>(sid, 'pos');
|
|
229
|
+
// ... re-render
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Values are JSON-stringified under the hood. Use for anything JSON-serializable.
|
|
235
|
+
|
|
236
|
+
#### Transient events — `room.send` and `room.on`
|
|
237
|
+
|
|
238
|
+
Use this for ephemeral actions: fires, hits, emotes, explosions, chat. Not persisted.
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
// Sender
|
|
242
|
+
room.send('fire', { x: 100, y: 200, angle: 1.5 });
|
|
243
|
+
|
|
244
|
+
// Every other client (sender does NOT receive their own)
|
|
245
|
+
const offFire = room.on('fire', (msg: { from: string; x: number; y: number; angle: number }) => {
|
|
246
|
+
spawnProjectile(msg.x, msg.y, msg.angle);
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### Server-tracked membership
|
|
251
|
+
|
|
252
|
+
`room.state.players` is server-owned. Each entry exposes:
|
|
253
|
+
- `userId` — Unboxy identity (from the JWT)
|
|
254
|
+
- `displayName` — set by the joining client
|
|
255
|
+
- `joinedAt` — ms timestamp
|
|
256
|
+
- `data` — the MapSchema<string> the player writes to via `room.player.set`
|
|
257
|
+
|
|
258
|
+
Games never mutate these directly; read them from `room.state.players`.
|
|
259
|
+
|
|
260
|
+
#### Connection lifecycle
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
room.onLeave((code) => console.log('disconnected', code));
|
|
264
|
+
room.onError((code, message) => console.warn('room error', code, message));
|
|
265
|
+
|
|
266
|
+
// Clean up on scene shutdown
|
|
267
|
+
offState();
|
|
268
|
+
offFire();
|
|
269
|
+
await room.leave();
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### Picking the right primitive
|
|
273
|
+
|
|
274
|
+
| Need | Use | Why |
|
|
275
|
+
|---|---|---|
|
|
276
|
+
| Smooth position sync of a player | `room.player.set('pos', {x,y})` at ~10-20 Hz + `onStateChange` | Delta-compressed, survives reconnect, server is source of truth. |
|
|
277
|
+
| Per-frame local movement feel | client-side prediction + interpolate remotes from state | State arrives at ~20 Hz; lerp toward it. |
|
|
278
|
+
| Scoreboard shared by all | `room.data.set('scoreboard', [...])` | One writer wins on conflict; use optimistic patterns or turn-based writes. |
|
|
279
|
+
| Projectile spawn / hit effect | `room.send('fire', {...})` + `room.on('fire', ...)` | Ephemeral, not needed on reconnect. |
|
|
280
|
+
| Chat message | `room.send('chat', text)` + `room.on('chat', ...)` | Same — ephemeral relay. |
|
|
281
|
+
| Current turn in a turn-based game | `room.data.set('turn', sid)` | Authoritative shared state. |
|
|
282
|
+
|
|
283
|
+
Rules of thumb:
|
|
284
|
+
- **If someone joining mid-game should see it, it's state** (`room.player.*` or `room.data.*`).
|
|
285
|
+
- **If it's a one-shot action, it's a message** (`room.send` / `room.on`).
|
|
286
|
+
- **Do not implement position sync via messages.** You will reinvent delta compression badly and spend more bandwidth. Use `room.player.set('pos', {x,y})`.
|
|
287
|
+
- **Server is authoritative but permissive** — it doesn't validate what you put in `data`. Treat inbound values from other players as untrusted (don't eval, validate shape before reading).
|
|
288
|
+
|
|
289
|
+
#### Availability
|
|
290
|
+
|
|
291
|
+
- Requires sign-in. Anonymous players get `RpcError('UNAUTHENTICATED')`.
|
|
292
|
+
- Requires a host with multiplayer enabled. Standalone games (no host) get `RpcError('REALTIME_UNAVAILABLE')` — guard with `if (unboxy.host === 'unboxy-home-ui')` if your game can run both hosted and standalone.
|
|
293
|
+
|
|
294
|
+
#### Available room types
|
|
295
|
+
|
|
296
|
+
- `lobby` — the generic room described above. Use for any multiplayer gameplay.
|
|
297
|
+
|
|
298
|
+
**Rules of the road**
|
|
299
|
+
- The server is the source of truth. Never trust `send()` payloads coming from other clients — the server validates.
|
|
300
|
+
- Room state is *ephemeral*. When the last player leaves, it disposes. Persist anything you want to keep via `saves` or `gameData`.
|
|
301
|
+
- Keep messages small. State syncs are delta-compressed; raw `send()` payloads are not.
|
|
302
|
+
- Unsubscribe on scene shutdown. `onStateChange`, `on`, `onLeave`, `onError` all return an unsubscribe function — call them from `scene.events.once('shutdown', ...)`.
|
|
303
|
+
- One Colyseus client is kept internally; repeated `joinOrCreate` calls reuse it and mint fresh tokens each connection.
|
|
304
|
+
|
|
186
305
|
## Anti-patterns (don't do these)
|
|
187
306
|
|
|
188
307
|
- Do **not** call `Unboxy.init()` inside a scene. Initialize at module load in `main.ts` and export the promise.
|
|
@@ -195,6 +314,8 @@ Rule of thumb: if two different players would write to the same key, it's `gameD
|
|
|
195
314
|
|
|
196
315
|
## Changelog
|
|
197
316
|
|
|
317
|
+
- **0.2.6** — redesigned `unboxy.rooms` around two generic primitives: delta-synced KV state (`room.player.set/get/delete`, `room.data.set/get/delete`) + transient relay (`room.send` / `room.on`). Server no longer bakes in game-specific fields like x/y/color/ready or a `move` handler — games define their own shapes via opaque JSON values, same contract as `gameData` / `saves`.
|
|
318
|
+
- **0.2.5** — added `unboxy.rooms` module (server-authoritative multiplayer rooms backed by Colyseus on unboxy-realtime-service). Requires sign-in. Host must advertise `realtime` capability. Colyseus client loaded lazily — single-player games do not pay for the dependency.
|
|
198
319
|
- **0.2.4** — added `unboxy.gameData` module (game-scope key-value store for scoreboards, shared state)
|
|
199
320
|
- **0.2.3** — anonymous users now use localStorage even inside a host (was throwing UNAUTHENTICATED when calling `saves` in home-ui without login)
|
|
200
321
|
- **0.2.2** — added `SDK-GUIDE.md`
|
package/dist/core/Transport.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export interface Transport {
|
|
|
13
13
|
readonly kind: TransportKind;
|
|
14
14
|
readonly user: UnboxyUser | null;
|
|
15
15
|
readonly gameId: string;
|
|
16
|
+
/**
|
|
17
|
+
* Endpoint for unboxy-realtime-service (WebSocket URL). Set by the host
|
|
18
|
+
* during handshake when multiplayer is enabled. Absent in standalone mode —
|
|
19
|
+
* `unboxy.rooms.*` is unavailable in that case.
|
|
20
|
+
*/
|
|
21
|
+
readonly realtimeUrl?: string;
|
|
16
22
|
call<T = unknown>(method: string, params?: unknown): Promise<T>;
|
|
17
23
|
}
|
|
18
24
|
export type TransportKind = 'unboxy-home-ui' | 'standalone' | 'discord';
|
package/dist/core/Unboxy.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TransportKind } from './Transport.js';
|
|
2
2
|
import { SavesModule } from '../saves/SavesModule.js';
|
|
3
3
|
import { GameDataModule } from '../gamedata/GameDataModule.js';
|
|
4
|
+
import { RealtimeModule } from '../realtime/RealtimeModule.js';
|
|
4
5
|
import type { UnboxyUser } from '../protocol.js';
|
|
5
6
|
export interface UnboxyInitOptions {
|
|
6
7
|
/** Handshake timeout in ms when running inside a host. Default 2000. */
|
|
@@ -22,6 +23,7 @@ export declare class Unboxy {
|
|
|
22
23
|
private transport;
|
|
23
24
|
readonly saves: SavesModule;
|
|
24
25
|
readonly gameData: GameDataModule;
|
|
26
|
+
readonly rooms: RealtimeModule;
|
|
25
27
|
private constructor();
|
|
26
28
|
/** The current authenticated user, or `null` if anonymous / standalone. */
|
|
27
29
|
get user(): UnboxyUser | null;
|
package/dist/core/Unboxy.js
CHANGED
|
@@ -2,8 +2,9 @@ import { PostMessageTransport } from './transports/PostMessageTransport.js';
|
|
|
2
2
|
import { LocalStorageTransport } from './transports/LocalStorageTransport.js';
|
|
3
3
|
import { SavesModule } from '../saves/SavesModule.js';
|
|
4
4
|
import { GameDataModule } from '../gamedata/GameDataModule.js';
|
|
5
|
+
import { RealtimeModule } from '../realtime/RealtimeModule.js';
|
|
5
6
|
// Kept in sync with package.json on each publish.
|
|
6
|
-
const SDK_VERSION = '0.2.
|
|
7
|
+
const SDK_VERSION = '0.2.6';
|
|
7
8
|
/**
|
|
8
9
|
* Unboxy platform services bound to the current (game, user).
|
|
9
10
|
*
|
|
@@ -27,6 +28,10 @@ export class Unboxy {
|
|
|
27
28
|
// fallback (standalone mode) that also works — the module speaks the
|
|
28
29
|
// same RPC interface.
|
|
29
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);
|
|
30
35
|
}
|
|
31
36
|
/** The current authenticated user, or `null` if anonymous / standalone. */
|
|
32
37
|
get user() { return this.transport.user; }
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,10 @@ export { Unboxy } from './core/Unboxy.js';
|
|
|
7
7
|
export type { UnboxyInitOptions } from './core/Unboxy.js';
|
|
8
8
|
export { SavesModule } from './saves/SavesModule.js';
|
|
9
9
|
export { GameDataModule } from './gamedata/GameDataModule.js';
|
|
10
|
+
export { RealtimeModule } from './realtime/RealtimeModule.js';
|
|
11
|
+
export type { JoinOptions } from './realtime/RealtimeModule.js';
|
|
12
|
+
export { UnboxyRoom, PlayerDataFacade, RoomDataFacade } from './realtime/UnboxyRoom.js';
|
|
10
13
|
export { RpcError } from './core/Transport.js';
|
|
11
14
|
export type { Transport, TransportKind } from './core/Transport.js';
|
|
12
15
|
export type { UnboxyUser } from './protocol.js';
|
|
13
|
-
export { PROTOCOL_VERSION, type HelloMessage, type InitMessage, type RpcRequestMessage, type RpcResultOk, type RpcResultError, type HostToSdkMessage, type SdkToHostMessage, type RpcErrorPayload, type RpcMethod, type SavesGetParams, type SavesGetResult, type SavesSetParams, type SavesSetResult, type SavesDeleteParams, type SavesDeleteResult, type SavesListResult, type GameDataGetParams, type GameDataGetResult, type GameDataSetParams, type GameDataSetResult, type GameDataDeleteParams, type GameDataDeleteResult, type GameDataListResult, } from './protocol.js';
|
|
16
|
+
export { PROTOCOL_VERSION, type HelloMessage, type InitMessage, type RpcRequestMessage, type RpcResultOk, type RpcResultError, type HostToSdkMessage, type SdkToHostMessage, type RpcErrorPayload, type RpcMethod, type SavesGetParams, type SavesGetResult, type SavesSetParams, type SavesSetResult, type SavesDeleteParams, type SavesDeleteResult, type SavesListResult, type GameDataGetParams, type GameDataGetResult, type GameDataSetParams, type GameDataSetResult, type GameDataDeleteParams, type GameDataDeleteResult, type GameDataListResult, type RealtimeGetTokenParams, type RealtimeGetTokenResult, } from './protocol.js';
|
package/dist/index.js
CHANGED
|
@@ -9,5 +9,7 @@ export { setupRecordingListener } from './recording/RecordingManager.js';
|
|
|
9
9
|
export { Unboxy } from './core/Unboxy.js';
|
|
10
10
|
export { SavesModule } from './saves/SavesModule.js';
|
|
11
11
|
export { GameDataModule } from './gamedata/GameDataModule.js';
|
|
12
|
+
export { RealtimeModule } from './realtime/RealtimeModule.js';
|
|
13
|
+
export { UnboxyRoom, PlayerDataFacade, RoomDataFacade } from './realtime/UnboxyRoom.js';
|
|
12
14
|
export { RpcError } from './core/Transport.js';
|
|
13
15
|
export { PROTOCOL_VERSION, } from './protocol.js';
|
package/dist/protocol.d.ts
CHANGED
|
@@ -22,6 +22,12 @@ export interface InitMessage {
|
|
|
22
22
|
gameId: string;
|
|
23
23
|
user: UnboxyUser | null;
|
|
24
24
|
capabilities: string[];
|
|
25
|
+
/**
|
|
26
|
+
* WebSocket endpoint for unboxy-realtime-service. Present only when the host
|
|
27
|
+
* has multiplayer configured (capabilities includes 'realtime'). SDK connects
|
|
28
|
+
* here after fetching a JWT via the 'realtime.getToken' RPC.
|
|
29
|
+
*/
|
|
30
|
+
realtimeUrl?: string;
|
|
25
31
|
}
|
|
26
32
|
export interface RpcResultOk {
|
|
27
33
|
type: 'unboxy:rpc.result';
|
|
@@ -40,7 +46,7 @@ export interface RpcResultError {
|
|
|
40
46
|
error: RpcErrorPayload;
|
|
41
47
|
}
|
|
42
48
|
export type HostToSdkMessage = InitMessage | RpcResultOk | RpcResultError;
|
|
43
|
-
export type RpcMethod = 'saves.get' | 'saves.set' | 'saves.delete' | 'saves.list' | 'gameData.get' | 'gameData.set' | 'gameData.delete' | 'gameData.list';
|
|
49
|
+
export type RpcMethod = 'saves.get' | 'saves.set' | 'saves.delete' | 'saves.list' | 'gameData.get' | 'gameData.set' | 'gameData.delete' | 'gameData.list' | 'realtime.getToken';
|
|
44
50
|
export interface SavesGetParams {
|
|
45
51
|
key: string;
|
|
46
52
|
}
|
|
@@ -89,3 +95,12 @@ export type GameDataDeleteResult = {
|
|
|
89
95
|
export interface GameDataListResult {
|
|
90
96
|
keys: string[];
|
|
91
97
|
}
|
|
98
|
+
export interface RealtimeGetTokenParams {
|
|
99
|
+
/** Opaque, forwarded to server-side auth (future use). */
|
|
100
|
+
purpose?: string;
|
|
101
|
+
}
|
|
102
|
+
export interface RealtimeGetTokenResult {
|
|
103
|
+
token: string;
|
|
104
|
+
/** Epoch millis when the token expires. */
|
|
105
|
+
expiresAt: number;
|
|
106
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Transport } from '../core/Transport.js';
|
|
2
|
+
import { UnboxyRoom } from './UnboxyRoom.js';
|
|
3
|
+
export interface JoinOptions extends Record<string, unknown> {
|
|
4
|
+
/** Optional display name passed to the server-side room on join. */
|
|
5
|
+
displayName?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Multiplayer rooms backed by unboxy-realtime-service (Colyseus under the hood).
|
|
9
|
+
*
|
|
10
|
+
* Availability: only when the host provided a `realtimeUrl` during the
|
|
11
|
+
* handshake (i.e. connected to unboxy-home-ui with multiplayer enabled).
|
|
12
|
+
* Unavailable in standalone/anonymous mode — methods throw RpcError
|
|
13
|
+
* 'REALTIME_UNAVAILABLE'.
|
|
14
|
+
*
|
|
15
|
+
* Tokens are fetched per connection attempt via the `realtime.getToken`
|
|
16
|
+
* RPC — the iframe never holds a long-lived credential.
|
|
17
|
+
*/
|
|
18
|
+
export declare class RealtimeModule {
|
|
19
|
+
private readonly transport;
|
|
20
|
+
private readonly realtimeUrl;
|
|
21
|
+
private client;
|
|
22
|
+
constructor(transport: Transport, realtimeUrl: string | undefined);
|
|
23
|
+
/** `true` when a realtime endpoint was provided by the host. */
|
|
24
|
+
get available(): boolean;
|
|
25
|
+
joinOrCreate<State = unknown>(roomType: string, options?: JoinOptions): Promise<UnboxyRoom<State>>;
|
|
26
|
+
create<State = unknown>(roomType: string, options?: JoinOptions): Promise<UnboxyRoom<State>>;
|
|
27
|
+
join<State = unknown>(roomType: string, options?: JoinOptions): Promise<UnboxyRoom<State>>;
|
|
28
|
+
joinById<State = unknown>(roomId: string, options?: JoinOptions): Promise<UnboxyRoom<State>>;
|
|
29
|
+
private connect;
|
|
30
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { RpcError } from '../core/Transport.js';
|
|
2
|
+
import { UnboxyRoom } from './UnboxyRoom.js';
|
|
3
|
+
/**
|
|
4
|
+
* Multiplayer rooms backed by unboxy-realtime-service (Colyseus under the hood).
|
|
5
|
+
*
|
|
6
|
+
* Availability: only when the host provided a `realtimeUrl` during the
|
|
7
|
+
* handshake (i.e. connected to unboxy-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
|
+
async connect(nameOrId, options, method) {
|
|
37
|
+
if (!this.available) {
|
|
38
|
+
throw new RpcError('REALTIME_UNAVAILABLE', 'Multiplayer is not available for this host. Unboxy.rooms requires running inside unboxy-home-ui with realtime enabled.');
|
|
39
|
+
}
|
|
40
|
+
if (!this.client) {
|
|
41
|
+
// Dynamic import so single-player games do not pull Colyseus into their
|
|
42
|
+
// bundle. Vite tree-shakes this path when `rooms` is never referenced.
|
|
43
|
+
const { Client } = await import('colyseus.js');
|
|
44
|
+
this.client = new Client(this.realtimeUrl);
|
|
45
|
+
}
|
|
46
|
+
const { token } = await this.transport.call('realtime.getToken', {});
|
|
47
|
+
// Inject gameId from the transport so the server-side gameId check sees
|
|
48
|
+
// a value consistent with the JWT — games never pass this directly.
|
|
49
|
+
const merged = { ...options, gameId: this.transport.gameId };
|
|
50
|
+
this.client.auth.token = token;
|
|
51
|
+
const room = await this.client[method](nameOrId, merged);
|
|
52
|
+
return new UnboxyRoom(room);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
/**
|
|
33
|
+
* Unboxy-flavored wrapper around a Colyseus Room. Exposes only the surface
|
|
34
|
+
* documented in SDK-GUIDE.md so games are portable to a different realtime
|
|
35
|
+
* backend should we migrate away from Colyseus.
|
|
36
|
+
*/
|
|
37
|
+
export declare class UnboxyRoom<State = unknown> {
|
|
38
|
+
private readonly room;
|
|
39
|
+
readonly player: PlayerDataFacade;
|
|
40
|
+
readonly data: RoomDataFacade;
|
|
41
|
+
constructor(room: Room<State>);
|
|
42
|
+
get id(): string;
|
|
43
|
+
get name(): string;
|
|
44
|
+
get sessionId(): string;
|
|
45
|
+
/**
|
|
46
|
+
* Current server-authoritative state. Proxied from Colyseus Schema — read
|
|
47
|
+
* values directly. Mutations do not propagate; only server-side handlers
|
|
48
|
+
* may change state.
|
|
49
|
+
*/
|
|
50
|
+
get state(): State;
|
|
51
|
+
/** Send a transient message. Relayed to every other client in the room
|
|
52
|
+
* with `from: sessionId` stamped onto the payload. Not persisted in state. */
|
|
53
|
+
send(type: string, payload?: unknown): void;
|
|
54
|
+
/** Register a handler for a server-sent message type. Returns unsubscribe. */
|
|
55
|
+
on(type: string, handler: (payload: unknown) => void): Unsubscribe;
|
|
56
|
+
/** Fires whenever the server-authoritative state changes. */
|
|
57
|
+
onStateChange(handler: (state: State) => void): Unsubscribe;
|
|
58
|
+
/**
|
|
59
|
+
* Fires when the connection closes (kick, server shutdown, network drop).
|
|
60
|
+
* `code` follows WebSocket close codes plus Colyseus-specific ones.
|
|
61
|
+
*/
|
|
62
|
+
onLeave(handler: (code: number) => void): Unsubscribe;
|
|
63
|
+
/** Fires when the server reports an error for this room. */
|
|
64
|
+
onError(handler: (code: number, message?: string) => void): Unsubscribe;
|
|
65
|
+
/** Disconnect from the room. Resolves with the close code. */
|
|
66
|
+
leave(consented?: boolean): Promise<number>;
|
|
67
|
+
}
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
function parseOrNull(raw) {
|
|
2
|
+
if (raw == null)
|
|
3
|
+
return null;
|
|
4
|
+
try {
|
|
5
|
+
return JSON.parse(raw);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Facade for per-player state — one entry per player, only the owner can write
|
|
13
|
+
* to their own entry. Delta-synced via Colyseus schema.
|
|
14
|
+
*/
|
|
15
|
+
export class PlayerDataFacade {
|
|
16
|
+
constructor(room) {
|
|
17
|
+
this.room = room;
|
|
18
|
+
}
|
|
19
|
+
/** Write a JSON-serializable value to the caller's own player data. */
|
|
20
|
+
set(key, value) {
|
|
21
|
+
this.room.send('player.set', { key, value });
|
|
22
|
+
}
|
|
23
|
+
/** Remove a key from the caller's own player data. */
|
|
24
|
+
delete(key) {
|
|
25
|
+
this.room.send('player.delete', { key });
|
|
26
|
+
}
|
|
27
|
+
/** Read a player's data value. Returns null if the player or key is absent. */
|
|
28
|
+
get(sessionId, key) {
|
|
29
|
+
const state = this.room.state;
|
|
30
|
+
const player = state?.players?.get?.(sessionId);
|
|
31
|
+
return parseOrNull(player?.data?.get?.(key));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Facade for room-scope shared state — one map per room, any client may write.
|
|
36
|
+
* Delta-synced via Colyseus schema. Use for shared game state like current
|
|
37
|
+
* turn, scoreboard, shared level-of-the-day, etc.
|
|
38
|
+
*/
|
|
39
|
+
export class RoomDataFacade {
|
|
40
|
+
constructor(room) {
|
|
41
|
+
this.room = room;
|
|
42
|
+
}
|
|
43
|
+
/** Write a JSON-serializable value into the room's shared data map. */
|
|
44
|
+
set(key, value) {
|
|
45
|
+
this.room.send('room.set', { key, value });
|
|
46
|
+
}
|
|
47
|
+
/** Remove a key from the room's shared data map. */
|
|
48
|
+
delete(key) {
|
|
49
|
+
this.room.send('room.delete', { key });
|
|
50
|
+
}
|
|
51
|
+
/** Read a shared room value. Returns null if the key is absent. */
|
|
52
|
+
get(key) {
|
|
53
|
+
const state = this.room.state;
|
|
54
|
+
return parseOrNull(state?.data?.get?.(key));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Unboxy-flavored wrapper around a Colyseus Room. Exposes only the surface
|
|
59
|
+
* documented in SDK-GUIDE.md so games are portable to a different realtime
|
|
60
|
+
* backend should we migrate away from Colyseus.
|
|
61
|
+
*/
|
|
62
|
+
export class UnboxyRoom {
|
|
63
|
+
constructor(room) {
|
|
64
|
+
this.room = room;
|
|
65
|
+
this.player = new PlayerDataFacade(room);
|
|
66
|
+
this.data = new RoomDataFacade(room);
|
|
67
|
+
}
|
|
68
|
+
get id() { return this.room.roomId; }
|
|
69
|
+
get name() { return this.room.name; }
|
|
70
|
+
get sessionId() { return this.room.sessionId; }
|
|
71
|
+
/**
|
|
72
|
+
* Current server-authoritative state. Proxied from Colyseus Schema — read
|
|
73
|
+
* values directly. Mutations do not propagate; only server-side handlers
|
|
74
|
+
* may change state.
|
|
75
|
+
*/
|
|
76
|
+
get state() { return this.room.state; }
|
|
77
|
+
/** Send a transient message. Relayed to every other client in the room
|
|
78
|
+
* with `from: sessionId` stamped onto the payload. Not persisted in state. */
|
|
79
|
+
send(type, payload) {
|
|
80
|
+
this.room.send(type, payload);
|
|
81
|
+
}
|
|
82
|
+
/** Register a handler for a server-sent message type. Returns unsubscribe. */
|
|
83
|
+
on(type, handler) {
|
|
84
|
+
return this.room.onMessage(type, handler);
|
|
85
|
+
}
|
|
86
|
+
/** Fires whenever the server-authoritative state changes. */
|
|
87
|
+
onStateChange(handler) {
|
|
88
|
+
const cb = () => handler(this.room.state);
|
|
89
|
+
this.room.onStateChange(cb);
|
|
90
|
+
return () => this.room.onStateChange.remove(cb);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Fires when the connection closes (kick, server shutdown, network drop).
|
|
94
|
+
* `code` follows WebSocket close codes plus Colyseus-specific ones.
|
|
95
|
+
*/
|
|
96
|
+
onLeave(handler) {
|
|
97
|
+
const cb = (code) => handler(code);
|
|
98
|
+
this.room.onLeave(cb);
|
|
99
|
+
return () => this.room.onLeave.remove(cb);
|
|
100
|
+
}
|
|
101
|
+
/** Fires when the server reports an error for this room. */
|
|
102
|
+
onError(handler) {
|
|
103
|
+
const cb = (code, message) => handler(code, message);
|
|
104
|
+
this.room.onError(cb);
|
|
105
|
+
return () => this.room.onError.remove(cb);
|
|
106
|
+
}
|
|
107
|
+
/** Disconnect from the room. Resolves with the close code. */
|
|
108
|
+
async leave(consented = true) {
|
|
109
|
+
return this.room.leave(consented);
|
|
110
|
+
}
|
|
111
|
+
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unboxy/phaser-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Unboxy Phaser 3 SDK — game infrastructure for the Unboxy platform",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
-
"files": [
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"SDK-GUIDE.md"
|
|
10
|
+
],
|
|
8
11
|
"scripts": {
|
|
9
12
|
"build": "tsc",
|
|
10
13
|
"prepublishOnly": "npm run build"
|
|
11
14
|
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"colyseus.js": "^0.16.0"
|
|
17
|
+
},
|
|
12
18
|
"peerDependencies": {
|
|
13
19
|
"phaser": "^3.60.0"
|
|
14
20
|
},
|
|
15
21
|
"devDependencies": {
|
|
22
|
+
"jose": "^6.2.2",
|
|
16
23
|
"phaser": "^3.80.0",
|
|
17
24
|
"typescript": "^5.5.0"
|
|
18
25
|
},
|