@watchtower-sdk/core 0.2.0 → 0.2.1
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/README.md +137 -245
- package/dist/index.d.mts +142 -1
- package/dist/index.d.ts +142 -1
- package/dist/index.js +314 -0
- package/dist/index.mjs +313 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -206,6 +206,117 @@ declare class Room {
|
|
|
206
206
|
/** Check if connected */
|
|
207
207
|
get connected(): boolean;
|
|
208
208
|
}
|
|
209
|
+
interface SyncOptions {
|
|
210
|
+
/** Updates per second (default: 20) */
|
|
211
|
+
tickRate?: number;
|
|
212
|
+
/** Enable interpolation for remote entities (default: true) */
|
|
213
|
+
interpolate?: boolean;
|
|
214
|
+
}
|
|
215
|
+
interface JoinOptions {
|
|
216
|
+
/** Create room if it doesn't exist */
|
|
217
|
+
create?: boolean;
|
|
218
|
+
/** Max players (only on create) */
|
|
219
|
+
maxPlayers?: number;
|
|
220
|
+
/** Make room public/discoverable (only on create) */
|
|
221
|
+
public?: boolean;
|
|
222
|
+
/** Room metadata (only on create) */
|
|
223
|
+
metadata?: Record<string, unknown>;
|
|
224
|
+
}
|
|
225
|
+
interface RoomListing {
|
|
226
|
+
id: string;
|
|
227
|
+
players: number;
|
|
228
|
+
maxPlayers?: number;
|
|
229
|
+
metadata?: Record<string, unknown>;
|
|
230
|
+
createdAt: number;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Sync - Automatic state synchronization
|
|
234
|
+
*
|
|
235
|
+
* Point this at your game state object and it becomes multiplayer.
|
|
236
|
+
* No events, no callbacks - just read and write your state.
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```ts
|
|
240
|
+
* const state = { players: {} }
|
|
241
|
+
* const sync = wt.sync(state)
|
|
242
|
+
*
|
|
243
|
+
* await sync.join('my-room')
|
|
244
|
+
*
|
|
245
|
+
* // Add yourself
|
|
246
|
+
* state.players[sync.myId] = { x: 0, y: 0, name: 'Player1' }
|
|
247
|
+
*
|
|
248
|
+
* // Move (automatically syncs to others)
|
|
249
|
+
* state.players[sync.myId].x = 100
|
|
250
|
+
*
|
|
251
|
+
* // Others appear automatically in state.players!
|
|
252
|
+
* for (const [id, player] of Object.entries(state.players)) {
|
|
253
|
+
* draw(player.x, player.y)
|
|
254
|
+
* }
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
declare class Sync<T extends Record<string, unknown>> {
|
|
258
|
+
/** The synchronized state object */
|
|
259
|
+
readonly state: T;
|
|
260
|
+
/** Your player ID */
|
|
261
|
+
readonly myId: string;
|
|
262
|
+
/** Current room ID (null if not in a room) */
|
|
263
|
+
get roomId(): string | null;
|
|
264
|
+
/** Whether currently connected to a room */
|
|
265
|
+
get connected(): boolean;
|
|
266
|
+
private config;
|
|
267
|
+
private options;
|
|
268
|
+
private _roomId;
|
|
269
|
+
private ws;
|
|
270
|
+
private syncInterval;
|
|
271
|
+
private lastSentState;
|
|
272
|
+
private interpolationTargets;
|
|
273
|
+
private listeners;
|
|
274
|
+
constructor(state: T, config: Required<WatchtowerConfig>, options?: SyncOptions);
|
|
275
|
+
/**
|
|
276
|
+
* Join a room - your state will sync with everyone in this room
|
|
277
|
+
*
|
|
278
|
+
* @param roomId - Room identifier (any string)
|
|
279
|
+
* @param options - Join options
|
|
280
|
+
*/
|
|
281
|
+
join(roomId: string, options?: JoinOptions): Promise<void>;
|
|
282
|
+
/**
|
|
283
|
+
* Leave the current room
|
|
284
|
+
*/
|
|
285
|
+
leave(): Promise<void>;
|
|
286
|
+
/**
|
|
287
|
+
* Create a new room and join it
|
|
288
|
+
*
|
|
289
|
+
* @param options - Room creation options
|
|
290
|
+
* @returns The room code/ID
|
|
291
|
+
*/
|
|
292
|
+
create(options?: Omit<JoinOptions, 'create'>): Promise<string>;
|
|
293
|
+
/**
|
|
294
|
+
* List public rooms
|
|
295
|
+
*/
|
|
296
|
+
listRooms(): Promise<RoomListing[]>;
|
|
297
|
+
/**
|
|
298
|
+
* Subscribe to sync events
|
|
299
|
+
*/
|
|
300
|
+
on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected', callback: Function): void;
|
|
301
|
+
/**
|
|
302
|
+
* Unsubscribe from sync events
|
|
303
|
+
*/
|
|
304
|
+
off(event: string, callback: Function): void;
|
|
305
|
+
private emit;
|
|
306
|
+
private connectWebSocket;
|
|
307
|
+
private handleMessage;
|
|
308
|
+
private applyFullState;
|
|
309
|
+
private applyPlayerState;
|
|
310
|
+
private removePlayer;
|
|
311
|
+
private clearRemotePlayers;
|
|
312
|
+
private findPlayersKey;
|
|
313
|
+
private startSyncLoop;
|
|
314
|
+
private stopSyncLoop;
|
|
315
|
+
private syncMyState;
|
|
316
|
+
private updateInterpolation;
|
|
317
|
+
private generateRoomCode;
|
|
318
|
+
private getHeaders;
|
|
319
|
+
}
|
|
209
320
|
declare class Watchtower {
|
|
210
321
|
/** @internal - Config is non-enumerable to prevent accidental API key exposure */
|
|
211
322
|
private readonly config;
|
|
@@ -291,6 +402,36 @@ declare class Watchtower {
|
|
|
291
402
|
* Note: This returns a promise, use `await wt.stats` or `wt.getStats()`
|
|
292
403
|
*/
|
|
293
404
|
get stats(): Promise<GameStats>;
|
|
405
|
+
/**
|
|
406
|
+
* Create a synchronized state object
|
|
407
|
+
*
|
|
408
|
+
* Point this at your game state and it becomes multiplayer.
|
|
409
|
+
* No events, no callbacks - just read and write your state.
|
|
410
|
+
*
|
|
411
|
+
* @param state - Your game state object (e.g., { players: {} })
|
|
412
|
+
* @param options - Sync options (tickRate, interpolation)
|
|
413
|
+
* @returns A Sync instance
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```ts
|
|
417
|
+
* const state = { players: {} }
|
|
418
|
+
* const sync = wt.sync(state)
|
|
419
|
+
*
|
|
420
|
+
* await sync.join('my-room')
|
|
421
|
+
*
|
|
422
|
+
* // Add yourself
|
|
423
|
+
* state.players[sync.myId] = { x: 0, y: 0 }
|
|
424
|
+
*
|
|
425
|
+
* // Move (automatically syncs to others)
|
|
426
|
+
* state.players[sync.myId].x = 100
|
|
427
|
+
*
|
|
428
|
+
* // Others appear automatically in state.players!
|
|
429
|
+
* for (const [id, player] of Object.entries(state.players)) {
|
|
430
|
+
* draw(player.x, player.y)
|
|
431
|
+
* }
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
434
|
+
sync<T extends Record<string, unknown>>(state: T, options?: SyncOptions): Sync<T>;
|
|
294
435
|
}
|
|
295
436
|
|
|
296
|
-
export { type GameState, type GameStats, type PlayerInfo, type PlayerState, type PlayerStats, type PlayersState, Room, type RoomEventMap, type RoomInfo, type SaveData, Watchtower, type WatchtowerConfig, Watchtower as default };
|
|
437
|
+
export { type GameState, type GameStats, type JoinOptions, type PlayerInfo, type PlayerState, type PlayerStats, type PlayersState, Room, type RoomEventMap, type RoomInfo, type RoomListing, type SaveData, Sync, type SyncOptions, Watchtower, type WatchtowerConfig, Watchtower as default };
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
Room: () => Room,
|
|
24
|
+
Sync: () => Sync,
|
|
24
25
|
Watchtower: () => Watchtower,
|
|
25
26
|
default: () => index_default
|
|
26
27
|
});
|
|
@@ -282,6 +283,285 @@ var Room = class {
|
|
|
282
283
|
return this.ws?.readyState === WebSocket.OPEN;
|
|
283
284
|
}
|
|
284
285
|
};
|
|
286
|
+
var Sync = class {
|
|
287
|
+
constructor(state, config, options) {
|
|
288
|
+
this._roomId = null;
|
|
289
|
+
this.ws = null;
|
|
290
|
+
this.syncInterval = null;
|
|
291
|
+
this.lastSentState = "";
|
|
292
|
+
this.interpolationTargets = /* @__PURE__ */ new Map();
|
|
293
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
294
|
+
this.state = state;
|
|
295
|
+
this.myId = config.playerId;
|
|
296
|
+
this.config = config;
|
|
297
|
+
this.options = {
|
|
298
|
+
tickRate: options?.tickRate ?? 20,
|
|
299
|
+
interpolate: options?.interpolate ?? true
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/** Current room ID (null if not in a room) */
|
|
303
|
+
get roomId() {
|
|
304
|
+
return this._roomId;
|
|
305
|
+
}
|
|
306
|
+
/** Whether currently connected to a room */
|
|
307
|
+
get connected() {
|
|
308
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Join a room - your state will sync with everyone in this room
|
|
312
|
+
*
|
|
313
|
+
* @param roomId - Room identifier (any string)
|
|
314
|
+
* @param options - Join options
|
|
315
|
+
*/
|
|
316
|
+
async join(roomId, options) {
|
|
317
|
+
if (this._roomId) {
|
|
318
|
+
await this.leave();
|
|
319
|
+
}
|
|
320
|
+
this._roomId = roomId;
|
|
321
|
+
await this.connectWebSocket(roomId, options);
|
|
322
|
+
this.startSyncLoop();
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Leave the current room
|
|
326
|
+
*/
|
|
327
|
+
async leave() {
|
|
328
|
+
this.stopSyncLoop();
|
|
329
|
+
if (this.ws) {
|
|
330
|
+
this.ws.close();
|
|
331
|
+
this.ws = null;
|
|
332
|
+
}
|
|
333
|
+
this.clearRemotePlayers();
|
|
334
|
+
this._roomId = null;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Create a new room and join it
|
|
338
|
+
*
|
|
339
|
+
* @param options - Room creation options
|
|
340
|
+
* @returns The room code/ID
|
|
341
|
+
*/
|
|
342
|
+
async create(options) {
|
|
343
|
+
const code = this.generateRoomCode();
|
|
344
|
+
await this.join(code, { ...options, create: true });
|
|
345
|
+
return code;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* List public rooms
|
|
349
|
+
*/
|
|
350
|
+
async listRooms() {
|
|
351
|
+
const response = await fetch(`${this.config.apiUrl}/v1/sync/rooms?gameId=${this.config.gameId}`, {
|
|
352
|
+
headers: this.getHeaders()
|
|
353
|
+
});
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
const data2 = await response.json();
|
|
356
|
+
throw new Error(data2.error || "Failed to list rooms");
|
|
357
|
+
}
|
|
358
|
+
const data = await response.json();
|
|
359
|
+
return data.rooms || [];
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Subscribe to sync events
|
|
363
|
+
*/
|
|
364
|
+
on(event, callback) {
|
|
365
|
+
if (!this.listeners.has(event)) {
|
|
366
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
367
|
+
}
|
|
368
|
+
this.listeners.get(event).add(callback);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Unsubscribe from sync events
|
|
372
|
+
*/
|
|
373
|
+
off(event, callback) {
|
|
374
|
+
this.listeners.get(event)?.delete(callback);
|
|
375
|
+
}
|
|
376
|
+
emit(event, ...args) {
|
|
377
|
+
this.listeners.get(event)?.forEach((cb) => {
|
|
378
|
+
try {
|
|
379
|
+
cb(...args);
|
|
380
|
+
} catch (e) {
|
|
381
|
+
console.error(`Error in sync ${event} handler:`, e);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
async connectWebSocket(roomId, options) {
|
|
386
|
+
return new Promise((resolve, reject) => {
|
|
387
|
+
const wsUrl = this.config.apiUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
388
|
+
const params = new URLSearchParams({
|
|
389
|
+
playerId: this.config.playerId,
|
|
390
|
+
gameId: this.config.gameId,
|
|
391
|
+
...this.config.apiKey ? { apiKey: this.config.apiKey } : {},
|
|
392
|
+
...options?.create ? { create: "true" } : {},
|
|
393
|
+
...options?.maxPlayers ? { maxPlayers: String(options.maxPlayers) } : {},
|
|
394
|
+
...options?.public ? { public: "true" } : {},
|
|
395
|
+
...options?.metadata ? { metadata: JSON.stringify(options.metadata) } : {}
|
|
396
|
+
});
|
|
397
|
+
const url = `${wsUrl}/v1/sync/${roomId}/ws?${params}`;
|
|
398
|
+
this.ws = new WebSocket(url);
|
|
399
|
+
const timeout = setTimeout(() => {
|
|
400
|
+
reject(new Error("Connection timeout"));
|
|
401
|
+
this.ws?.close();
|
|
402
|
+
}, 1e4);
|
|
403
|
+
this.ws.onopen = () => {
|
|
404
|
+
clearTimeout(timeout);
|
|
405
|
+
this.emit("connected");
|
|
406
|
+
resolve();
|
|
407
|
+
};
|
|
408
|
+
this.ws.onerror = () => {
|
|
409
|
+
clearTimeout(timeout);
|
|
410
|
+
const error = new Error("WebSocket connection failed");
|
|
411
|
+
this.emit("error", error);
|
|
412
|
+
reject(error);
|
|
413
|
+
};
|
|
414
|
+
this.ws.onclose = () => {
|
|
415
|
+
this.stopSyncLoop();
|
|
416
|
+
this.emit("disconnected");
|
|
417
|
+
};
|
|
418
|
+
this.ws.onmessage = (event) => {
|
|
419
|
+
try {
|
|
420
|
+
const data = JSON.parse(event.data);
|
|
421
|
+
this.handleMessage(data);
|
|
422
|
+
} catch (e) {
|
|
423
|
+
console.error("Failed to parse sync message:", e);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
handleMessage(data) {
|
|
429
|
+
switch (data.type) {
|
|
430
|
+
case "full_state":
|
|
431
|
+
this.applyFullState(data.state);
|
|
432
|
+
break;
|
|
433
|
+
case "state":
|
|
434
|
+
this.applyPlayerState(data.playerId, data.data);
|
|
435
|
+
break;
|
|
436
|
+
case "join":
|
|
437
|
+
this.emit("join", data.playerId);
|
|
438
|
+
break;
|
|
439
|
+
case "leave":
|
|
440
|
+
this.removePlayer(data.playerId);
|
|
441
|
+
this.emit("leave", data.playerId);
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
applyFullState(fullState) {
|
|
446
|
+
for (const [playerId, playerState] of Object.entries(fullState)) {
|
|
447
|
+
if (playerId !== this.myId) {
|
|
448
|
+
this.applyPlayerState(playerId, playerState);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
applyPlayerState(playerId, playerState) {
|
|
453
|
+
const playersKey = this.findPlayersKey();
|
|
454
|
+
if (!playersKey) return;
|
|
455
|
+
const players = this.state[playersKey];
|
|
456
|
+
if (this.options.interpolate && players[playerId]) {
|
|
457
|
+
this.interpolationTargets.set(playerId, {
|
|
458
|
+
target: { ...playerState },
|
|
459
|
+
current: { ...players[playerId] }
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
players[playerId] = playerState;
|
|
463
|
+
}
|
|
464
|
+
removePlayer(playerId) {
|
|
465
|
+
const playersKey = this.findPlayersKey();
|
|
466
|
+
if (!playersKey) return;
|
|
467
|
+
const players = this.state[playersKey];
|
|
468
|
+
delete players[playerId];
|
|
469
|
+
this.interpolationTargets.delete(playerId);
|
|
470
|
+
}
|
|
471
|
+
clearRemotePlayers() {
|
|
472
|
+
const playersKey = this.findPlayersKey();
|
|
473
|
+
if (!playersKey) return;
|
|
474
|
+
const players = this.state[playersKey];
|
|
475
|
+
for (const playerId of Object.keys(players)) {
|
|
476
|
+
if (playerId !== this.myId) {
|
|
477
|
+
delete players[playerId];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
this.interpolationTargets.clear();
|
|
481
|
+
}
|
|
482
|
+
findPlayersKey() {
|
|
483
|
+
const candidates = ["players", "entities", "gnomes", "users", "clients"];
|
|
484
|
+
for (const key of candidates) {
|
|
485
|
+
if (key in this.state && typeof this.state[key] === "object") {
|
|
486
|
+
return key;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
for (const key of Object.keys(this.state)) {
|
|
490
|
+
if (typeof this.state[key] === "object" && this.state[key] !== null) {
|
|
491
|
+
return key;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
startSyncLoop() {
|
|
497
|
+
if (this.syncInterval) return;
|
|
498
|
+
const intervalMs = 1e3 / this.options.tickRate;
|
|
499
|
+
this.syncInterval = setInterval(() => {
|
|
500
|
+
this.syncMyState();
|
|
501
|
+
if (this.options.interpolate) {
|
|
502
|
+
this.updateInterpolation();
|
|
503
|
+
}
|
|
504
|
+
}, intervalMs);
|
|
505
|
+
}
|
|
506
|
+
stopSyncLoop() {
|
|
507
|
+
if (this.syncInterval) {
|
|
508
|
+
clearInterval(this.syncInterval);
|
|
509
|
+
this.syncInterval = null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
syncMyState() {
|
|
513
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
514
|
+
const playersKey = this.findPlayersKey();
|
|
515
|
+
if (!playersKey) return;
|
|
516
|
+
const players = this.state[playersKey];
|
|
517
|
+
const myState = players[this.myId];
|
|
518
|
+
if (!myState) return;
|
|
519
|
+
const stateJson = JSON.stringify(myState);
|
|
520
|
+
if (stateJson === this.lastSentState) return;
|
|
521
|
+
this.lastSentState = stateJson;
|
|
522
|
+
this.ws.send(JSON.stringify({
|
|
523
|
+
type: "state",
|
|
524
|
+
data: myState
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
updateInterpolation() {
|
|
528
|
+
const playersKey = this.findPlayersKey();
|
|
529
|
+
if (!playersKey) return;
|
|
530
|
+
const players = this.state[playersKey];
|
|
531
|
+
const lerpFactor = 0.2;
|
|
532
|
+
for (const [playerId, interp] of this.interpolationTargets) {
|
|
533
|
+
const player = players[playerId];
|
|
534
|
+
if (!player) continue;
|
|
535
|
+
for (const [key, targetValue] of Object.entries(interp.target)) {
|
|
536
|
+
if (typeof targetValue === "number" && typeof interp.current[key] === "number") {
|
|
537
|
+
const current = interp.current[key];
|
|
538
|
+
const newValue = current + (targetValue - current) * lerpFactor;
|
|
539
|
+
interp.current[key] = newValue;
|
|
540
|
+
player[key] = newValue;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
generateRoomCode() {
|
|
546
|
+
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
547
|
+
let code = "";
|
|
548
|
+
for (let i = 0; i < 6; i++) {
|
|
549
|
+
code += chars[Math.floor(Math.random() * chars.length)];
|
|
550
|
+
}
|
|
551
|
+
return code;
|
|
552
|
+
}
|
|
553
|
+
getHeaders() {
|
|
554
|
+
const headers = {
|
|
555
|
+
"Content-Type": "application/json",
|
|
556
|
+
"X-Player-ID": this.config.playerId,
|
|
557
|
+
"X-Game-ID": this.config.gameId
|
|
558
|
+
};
|
|
559
|
+
if (this.config.apiKey) {
|
|
560
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
561
|
+
}
|
|
562
|
+
return headers;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
285
565
|
var Watchtower = class {
|
|
286
566
|
constructor(config) {
|
|
287
567
|
Object.defineProperty(this, "config", {
|
|
@@ -458,10 +738,44 @@ var Watchtower = class {
|
|
|
458
738
|
get stats() {
|
|
459
739
|
return this.getStats();
|
|
460
740
|
}
|
|
741
|
+
// ============ SYNC API ============
|
|
742
|
+
/**
|
|
743
|
+
* Create a synchronized state object
|
|
744
|
+
*
|
|
745
|
+
* Point this at your game state and it becomes multiplayer.
|
|
746
|
+
* No events, no callbacks - just read and write your state.
|
|
747
|
+
*
|
|
748
|
+
* @param state - Your game state object (e.g., { players: {} })
|
|
749
|
+
* @param options - Sync options (tickRate, interpolation)
|
|
750
|
+
* @returns A Sync instance
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* ```ts
|
|
754
|
+
* const state = { players: {} }
|
|
755
|
+
* const sync = wt.sync(state)
|
|
756
|
+
*
|
|
757
|
+
* await sync.join('my-room')
|
|
758
|
+
*
|
|
759
|
+
* // Add yourself
|
|
760
|
+
* state.players[sync.myId] = { x: 0, y: 0 }
|
|
761
|
+
*
|
|
762
|
+
* // Move (automatically syncs to others)
|
|
763
|
+
* state.players[sync.myId].x = 100
|
|
764
|
+
*
|
|
765
|
+
* // Others appear automatically in state.players!
|
|
766
|
+
* for (const [id, player] of Object.entries(state.players)) {
|
|
767
|
+
* draw(player.x, player.y)
|
|
768
|
+
* }
|
|
769
|
+
* ```
|
|
770
|
+
*/
|
|
771
|
+
sync(state, options) {
|
|
772
|
+
return new Sync(state, this.config, options);
|
|
773
|
+
}
|
|
461
774
|
};
|
|
462
775
|
var index_default = Watchtower;
|
|
463
776
|
// Annotate the CommonJS export names for ESM import in node:
|
|
464
777
|
0 && (module.exports = {
|
|
465
778
|
Room,
|
|
779
|
+
Sync,
|
|
466
780
|
Watchtower
|
|
467
781
|
});
|