@unboxy/phaser-sdk 0.2.11 → 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 +57 -0
- package/dist/index.d.ts +1 -1
- package/dist/realtime/RealtimeModule.d.ts +43 -0
- package/dist/realtime/RealtimeModule.js +61 -0
- package/package.json +1 -1
package/SDK-GUIDE.md
CHANGED
|
@@ -283,6 +283,61 @@ const room = await unboxy.rooms.joinOrCreate('lobby', {
|
|
|
283
283
|
});
|
|
284
284
|
```
|
|
285
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
|
+
|
|
286
341
|
#### Delta-synced state — `room.player` and `room.data`
|
|
287
342
|
|
|
288
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.
|
|
@@ -427,6 +482,8 @@ Don't blindly apply interpolation to everything. It's wrong for a chess piece an
|
|
|
427
482
|
|
|
428
483
|
## Changelog
|
|
429
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.
|
|
430
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`.
|
|
431
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.
|
|
432
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,5 +1,28 @@
|
|
|
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;
|
|
@@ -46,5 +69,25 @@ export declare class RealtimeModule {
|
|
|
46
69
|
create<State = unknown>(roomType: string, options?: JoinOptions): Promise<UnboxyRoom<State>>;
|
|
47
70
|
join<State = unknown>(roomType: string, options?: JoinOptions): Promise<UnboxyRoom<State>>;
|
|
48
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[]>;
|
|
49
92
|
private connect;
|
|
50
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
|
+
}
|