@unboxy/phaser-sdk 0.2.10 → 0.2.12

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 CHANGED
@@ -237,6 +237,107 @@ room.sessionId; // this connection's session id within the room
237
237
  room.state; // proxy of the server-authoritative schema
238
238
  ```
239
239
 
240
+ **Room type is always `'lobby'`.** This is the only room type the realtime server registers — there is no `'blokus'` / `'chess'` / `'mygame'` type. Passing any other name results in "no handler" and throws. To distinguish rooms (private matches, per-code lobbies, etc.) use the `roomCode` option, documented below.
241
+
242
+ #### Matchmaking — one game can have many rooms
243
+
244
+ By default (no `roomCode`), every player of the same game lands in **one shared room**. Fine for a single hangout or a "public lobby" — but not for games where users need to open *private* rooms (e.g. "play this match with my friend" / "create a room with code ABC123 and share it").
245
+
246
+ Use the `roomCode` option to bucket players. Two clients with the same `(gameId, roomCode)` join the same room; different codes create independent rooms.
247
+
248
+ ```ts
249
+ // --- Create a private room with a fresh code ---
250
+ const code = Math.random().toString(36).slice(2, 8).toUpperCase(); // e.g. 'AB3X7Y'
251
+ const hostRoom = await unboxy.rooms.joinOrCreate('lobby', {
252
+ roomCode: code,
253
+ maxClients: 4, // cap to 4 players (server clamps to [1, 16])
254
+ displayName: unboxy.user?.name ?? 'host',
255
+ });
256
+
257
+ // Show `code` in the UI; the host shares it with friends manually.
258
+
259
+ // --- Friend joins the same room with the shared code ---
260
+ const guestRoom = await unboxy.rooms.joinOrCreate('lobby', {
261
+ roomCode: 'AB3X7Y', // whatever the host shared
262
+ displayName: unboxy.user?.name ?? 'guest',
263
+ });
264
+ ```
265
+
266
+ **`roomCode` caveats:**
267
+ - Not a password. Anyone who knows the code can join. Use it for unguessable rather than *secret*.
268
+ - Defaults to empty string when omitted. Two clients that both omit `roomCode` share the same "default" room — useful for a single global lobby per game.
269
+ - Changing `roomCode` between joins does NOT transfer state; it's a different room. Leave the old room (`room.leave()`) before joining a new one.
270
+
271
+ **`maxClients` caveats:**
272
+ - Only honored by the client who CREATES the room (the first one in). Later joiners see the creator's choice.
273
+ - Server clamps to `[1, 16]`. Passing `maxClients: 1000` silently becomes 16.
274
+ - When full, `joinOrCreate` throws. Handle that in your UI (e.g. "Room is full. Ask your friend to create a smaller room or try a different code.").
275
+
276
+ **Quick match / public room pattern.** If you want a "click Play, get matched with someone" flow, omit `roomCode` — everyone lands in the same default room. Players are differentiated by their `sessionId`. Add your own ready / waiting logic in `room.data`.
277
+
278
+ ```ts
279
+ // Public quick match — no roomCode, everyone shares one room per game
280
+ const room = await unboxy.rooms.joinOrCreate('lobby', {
281
+ maxClients: 2,
282
+ displayName: unboxy.user?.name ?? 'guest',
283
+ });
284
+ ```
285
+
286
+ #### Browsing open rooms — `unboxy.rooms.list()`
287
+
288
+ A room code is enough for "my friend and I agreed on code ABC123 out of band", but not for "show me a list of open games and let me click one to join." For that, ask the server what rooms are currently open for your game:
289
+
290
+ ```ts
291
+ interface RoomListEntry {
292
+ roomId: string; // opaque — pass to joinById
293
+ roomCode: string; // the code the host passed (empty for default rooms)
294
+ clients: number; // current occupants
295
+ maxClients: number; // cap the host set (server-clamped to [1, 16])
296
+ metadata?: unknown; // whatever the host set via room.setMetadata (optional)
297
+ createdAt?: number; // ms since epoch
298
+ }
299
+
300
+ const rooms = await unboxy.rooms.list();
301
+ rooms
302
+ .filter(r => r.clients < r.maxClients) // hide full rooms
303
+ .forEach(renderLobbyCard);
304
+
305
+ // On click:
306
+ async function joinRoom(entry: RoomListEntry) {
307
+ const room = await unboxy.rooms.joinById(entry.roomId, {
308
+ displayName: unboxy.user?.name ?? 'guest',
309
+ });
310
+ // room state is populated after the first onStateChange — see
311
+ // "Connection lifecycle" below for the state-timing rule.
312
+ }
313
+ ```
314
+
315
+ **Scope.** `list()` only returns rooms for the caller's game. A player can't see rooms from other games (the server cross-checks the gameId claim on the token).
316
+
317
+ **Filter.** Pass `{ roomCode: 'ABC' }` to narrow to one code without paging through the whole list — useful for "is the room with code ABC up yet?" polling.
318
+
319
+ **Polling, not subscription.** There is no push-on-change API today. Poll on a timer (e.g. every 3-5 s) while the lobby is open, and refresh on the user's explicit click. Cancel the timer when the player leaves the lobby.
320
+
321
+ ```ts
322
+ // Simple lobby browser refresh loop
323
+ let stop = false;
324
+ async function refreshLoop() {
325
+ while (!stop) {
326
+ try {
327
+ const rooms = await unboxy.rooms.list();
328
+ renderLobby(rooms);
329
+ } catch (e) {
330
+ // Network hiccup or REALTIME_UNAVAILABLE — surface in UI, keep looping.
331
+ }
332
+ await new Promise(r => setTimeout(r, 3000));
333
+ }
334
+ }
335
+ refreshLoop();
336
+ scene.events.once('shutdown', () => { stop = true; });
337
+ ```
338
+
339
+ **Unlisted rooms.** Every room created via `joinOrCreate` / `create` is currently listable by players of the same game. There is no "private room" flag yet — if you need that, tell your users to share the code out of band and don't render a lobby browser.
340
+
240
341
  #### Delta-synced state — `room.player` and `room.data`
241
342
 
242
343
  Use this for anything that represents the *current world*: positions, hp, score, turn state, scoreboard, etc. The server delta-syncs diffs to every client.
@@ -381,6 +482,8 @@ Don't blindly apply interpolation to everything. It's wrong for a chess piece an
381
482
 
382
483
  ## Changelog
383
484
 
485
+ - **0.2.12** — added `unboxy.rooms.list(options?)` — returns the open rooms for the caller's game right now, scoped server-side to the caller's gameId. Use this to build a lobby browser ("show me open rooms, click to join") without needing an out-of-band room code. SDK-GUIDE adds a "Browsing open rooms" section under Multiplayer covering the `RoomListEntry` shape, the poll-on-timer pattern, and the `{ roomCode }` filter for "is room X up yet?" probes.
486
+ - **0.2.11** — added `roomCode` + `maxClients` to `JoinOptions`. SDK-GUIDE's Multiplayer chapter now documents matchmaking: room type is always `'lobby'`; use `roomCode` to bucket independent rooms per gameId (create-with-fresh-code, join-with-shared-code, quick-match with empty code). Fixes a real bug where agents were passing a gameId-derived string as the room type (e.g. `joinOrCreate('blokus-<code>', ...)`) and failing with "no handler" server-side.
384
487
  - **0.2.10** — docs: added a `createUnboxyGame` options table (including the new `plugins` field) and a third-party Phaser plugin registration example. No code changes; version bumped so existing workspaces pick up the new guidance via `npm update @unboxy/phaser-sdk`.
385
488
  - **0.2.9** — `createUnboxyGame` now forwards the optional `plugins` field to Phaser's `GameConfig`. Previously the option was accepted at the call site but silently dropped — any `phaser3-rex-plugins` (or similar) registration never took effect, and `this.plugins.get(key)` returned undefined at runtime. Purely additive; no effect on games that don't pass `plugins`. Lets the `phaser-rex-touch` agent skill work end-to-end.
386
489
  - **0.2.8** — docs: added "Making remote motion feel smooth" section to the multiplayer chapter with concrete examples for interpolation (continuous motion), extrapolation (projectiles), and snap (discrete/turn-based/infrequent). No code changes — version bumped so existing workspaces pick up the new guidance via `npm update @unboxy/phaser-sdk`.
package/dist/index.d.ts CHANGED
@@ -8,7 +8,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
10
  export { RealtimeModule } from './realtime/RealtimeModule.js';
11
- export type { JoinOptions } from './realtime/RealtimeModule.js';
11
+ export type { JoinOptions, RoomListEntry, RoomListOptions } from './realtime/RealtimeModule.js';
12
12
  export { UnboxyRoom, PlayerDataFacade, RoomDataFacade } from './realtime/UnboxyRoom.js';
13
13
  export { RpcError } from './core/Transport.js';
14
14
  export type { Transport, TransportKind } from './core/Transport.js';
@@ -1,8 +1,51 @@
1
1
  import type { Transport } from '../core/Transport.js';
2
2
  import { UnboxyRoom } from './UnboxyRoom.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
+ }
3
26
  export interface JoinOptions extends Record<string, unknown> {
4
27
  /** Optional display name passed to the server-side room on join. */
5
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;
6
49
  }
7
50
  /**
8
51
  * Multiplayer rooms backed by unboxy-realtime-service (Colyseus under the hood).
@@ -26,5 +69,25 @@ export declare class RealtimeModule {
26
69
  create<State = unknown>(roomType: string, options?: JoinOptions): Promise<UnboxyRoom<State>>;
27
70
  join<State = unknown>(roomType: string, options?: JoinOptions): Promise<UnboxyRoom<State>>;
28
71
  joinById<State = unknown>(roomId: string, options?: JoinOptions): Promise<UnboxyRoom<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 unboxy.rooms.list();
84
+ * rooms
85
+ * .filter(r => r.clients < r.maxClients)
86
+ * .forEach(r => {
87
+ * // render a button; on click: unboxy.rooms.joinById(r.roomId)
88
+ * });
89
+ * ```
90
+ */
91
+ list(options?: RoomListOptions): Promise<RoomListEntry[]>;
29
92
  private connect;
30
93
  }
@@ -33,6 +33,56 @@ export class RealtimeModule {
33
33
  async joinById(roomId, options = {}) {
34
34
  return this.connect(roomId, options, 'joinById');
35
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 unboxy.rooms.list();
48
+ * rooms
49
+ * .filter(r => r.clients < r.maxClients)
50
+ * .forEach(r => {
51
+ * // render a button; on click: unboxy.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. Unboxy.rooms.list requires running inside unboxy-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
+ }
36
86
  async connect(nameOrId, options, method) {
37
87
  if (!this.available) {
38
88
  throw new RpcError('REALTIME_UNAVAILABLE', 'Multiplayer is not available for this host. Unboxy.rooms requires running inside unboxy-home-ui with realtime enabled.');
@@ -52,3 +102,14 @@ export class RealtimeModule {
52
102
  return new UnboxyRoom(room);
53
103
  }
54
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboxy/phaser-sdk",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
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",