@watchtower-sdk/core 0.2.0 → 0.2.2

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 CHANGED
@@ -1,17 +1,17 @@
1
- # @watchtower/sdk
1
+ # @watchtower-sdk/core
2
2
 
3
- Simple game backend SDK - cloud saves, multiplayer rooms, automatic state sync.
3
+ Simple game backend SDK - cloud saves and multiplayer.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @watchtower/sdk
8
+ npm install @watchtower-sdk/core
9
9
  ```
10
10
 
11
11
  ## Quick Start
12
12
 
13
13
  ```typescript
14
- import { Watchtower } from '@watchtower/sdk'
14
+ import { Watchtower } from '@watchtower-sdk/core'
15
15
 
16
16
  const wt = new Watchtower({
17
17
  gameId: 'my-game',
@@ -19,265 +19,168 @@ const wt = new Watchtower({
19
19
  })
20
20
 
21
21
  // Cloud saves
22
- await wt.save('progress', { level: 5, coins: 100 })
22
+ await wt.save('progress', { level: 5 })
23
23
  const data = await wt.load('progress')
24
24
 
25
25
  // Multiplayer
26
- const room = await wt.createRoom()
27
- console.log('Share this code:', room.code) // e.g., "ABCD"
26
+ const state = { players: {} }
27
+ const sync = wt.sync(state)
28
+ await sync.join('room-code')
29
+ state.players[sync.myId] = { x: 0, y: 0 }
30
+ // Others appear in state.players automatically!
28
31
  ```
29
32
 
30
33
  ## Cloud Saves
31
34
 
32
- Simple key-value storage per player. Works across devices.
35
+ Key-value storage per player. JSON in, JSON out.
33
36
 
34
37
  ```typescript
35
- // Save anything JSON-serializable
38
+ // Save
36
39
  await wt.save('progress', { level: 5, coins: 100 })
37
40
  await wt.save('settings', { music: true, sfx: true })
38
- await wt.save('inventory', ['sword', 'shield', 'potion'])
39
41
 
40
- // Load it back
42
+ // Load
41
43
  const progress = await wt.load('progress')
42
44
  const settings = await wt.load('settings')
43
45
 
44
- // List all save keys
45
- const keys = await wt.listSaves() // ['progress', 'settings', 'inventory']
46
+ // List all saves
47
+ const keys = await wt.listSaves() // ['progress', 'settings']
46
48
 
47
- // Delete a save
48
- await wt.deleteSave('inventory')
49
+ // Delete
50
+ await wt.deleteSave('progress')
49
51
  ```
50
52
 
51
- ## Multiplayer Rooms
53
+ ## Multiplayer
52
54
 
53
- Create rooms with 4-letter codes. Share with friends to play together.
55
+ Point at your game state. Join a room. State syncs automatically.
54
56
 
55
57
  ```typescript
56
- // Create a room (you become the host)
57
- const room = await wt.createRoom()
58
- console.log('Room code:', room.code)
59
-
60
- // Join an existing room
61
- const room = await wt.joinRoom('ABCD')
62
-
63
- // Check room properties
64
- room.isHost // true if you're the host
65
- room.hostId // current host's player ID
66
- room.playerId // your player ID
67
- room.playerCount // number of players
68
- room.players // all players' states
69
- ```
58
+ // Your game state
59
+ const state = { players: {} }
70
60
 
71
- ## Player State Sync
61
+ // Connect to Watchtower
62
+ const sync = wt.sync(state)
72
63
 
73
- Automatically sync your player's position/state to all other players.
64
+ // Join a room
65
+ await sync.join('my-room')
74
66
 
75
- ```typescript
76
- // Set your player state (automatically synced at 20Hz)
77
- room.player.set({
78
- x: 100,
79
- y: 200,
80
- sprite: 'running',
81
- health: 100
82
- })
83
-
84
- // State is merged, so you can update individual fields
85
- room.player.set({ x: 150 }) // keeps y, sprite, health
67
+ // Add yourself
68
+ state.players[sync.myId] = {
69
+ x: 0,
70
+ y: 0,
71
+ name: 'Player1'
72
+ }
86
73
 
87
- // Force immediate sync
88
- room.player.sync()
74
+ // Move (automatically syncs to others!)
75
+ state.players[sync.myId].x += 5
89
76
 
90
- // See all players' states
91
- room.on('players', (players) => {
92
- for (const [playerId, state] of Object.entries(players)) {
93
- if (playerId !== room.playerId) {
94
- // Update other player's sprite
95
- updateOtherPlayer(playerId, state.x, state.y, state.sprite)
96
- }
97
- }
98
- })
77
+ // Draw everyone (others appear automatically!)
78
+ for (const [id, player] of Object.entries(state.players)) {
79
+ drawPlayer(player.x, player.y)
80
+ }
99
81
  ```
100
82
 
101
- ## Game State (Host-Controlled)
83
+ No events. No message handlers. Just read and write your state.
102
84
 
103
- Shared state for things like game phase, scores, round number. Only the host can modify it.
85
+ ### Creating & Joining Rooms
104
86
 
105
87
  ```typescript
106
- // Host sets game state
107
- if (room.isHost) {
108
- room.state.set({
109
- phase: 'lobby',
110
- round: 0,
111
- scores: {}
112
- })
113
-
114
- // Start the game
115
- room.state.set({ phase: 'playing', round: 1 })
116
- }
88
+ // Create a new room
89
+ const code = await sync.create({ maxPlayers: 4 })
90
+ console.log('Share this code:', code) // e.g., "A3B7X2"
117
91
 
118
- // Everyone receives state updates
119
- room.on('state', (state) => {
120
- if (state.phase === 'playing') {
121
- startGame()
122
- }
123
- if (state.phase === 'gameover') {
124
- showWinner(state.winner)
125
- }
126
- })
92
+ // Join existing room
93
+ await sync.join('A3B7X2')
127
94
 
128
- // Read current state anytime
129
- const currentState = room.state.get()
130
- ```
95
+ // Leave room
96
+ await sync.leave()
131
97
 
132
- ## Broadcast Messages
98
+ // List public rooms
99
+ const rooms = await sync.listRooms()
100
+ ```
133
101
 
134
- For one-off events that don't need persistent state.
102
+ ### Options
135
103
 
136
104
  ```typescript
137
- // Broadcast to all players
138
- room.broadcast({ type: 'explosion', x: 50, y: 50 })
139
- room.broadcast({ type: 'chat', message: 'gg!' })
140
-
141
- // Send to specific player
142
- room.sendTo(playerId, { type: 'private_message', text: 'hey' })
143
-
144
- // Receive messages
145
- room.on('message', (from, data) => {
146
- if (data.type === 'explosion') {
147
- createExplosion(data.x, data.y)
148
- }
149
- if (data.type === 'chat') {
150
- showChat(from, data.message)
151
- }
105
+ const sync = wt.sync(state, {
106
+ tickRate: 20, // Updates per second (default: 20)
107
+ interpolate: true // Smooth remote movement (default: true)
152
108
  })
153
109
  ```
154
110
 
155
- ## Room Events
111
+ ### Properties
156
112
 
157
113
  ```typescript
158
- // Connection established
159
- room.on('connected', ({ playerId, room }) => {
160
- console.log('Connected as', playerId)
161
- console.log('Host is', room.hostId)
162
- })
114
+ sync.myId // Your player ID
115
+ sync.roomId // Current room, or null
116
+ sync.connected // WebSocket connected?
117
+ ```
163
118
 
164
- // Player joined
165
- room.on('playerJoined', (playerId, playerCount) => {
166
- console.log(`${playerId} joined (${playerCount} players)`)
167
- spawnPlayer(playerId)
168
- })
119
+ ### Events (Optional)
169
120
 
170
- // Player left
171
- room.on('playerLeft', (playerId, playerCount) => {
172
- console.log(`${playerId} left (${playerCount} players)`)
173
- removePlayer(playerId)
174
- })
121
+ You don't need events—just read your state. But if you want notifications:
175
122
 
176
- // Host changed (automatic migration when host leaves)
177
- room.on('hostChanged', (newHostId) => {
178
- console.log('New host:', newHostId)
179
- if (newHostId === room.playerId) {
180
- console.log("I'm the host now!")
181
- }
182
- })
183
-
184
- // Disconnected
185
- room.on('disconnected', () => {
186
- console.log('Lost connection')
187
- })
188
-
189
- // Error
190
- room.on('error', (error) => {
191
- console.error('Room error:', error)
192
- })
123
+ ```typescript
124
+ sync.on('join', (playerId) => console.log(playerId, 'joined'))
125
+ sync.on('leave', (playerId) => console.log(playerId, 'left'))
126
+ sync.on('connected', () => console.log('Connected'))
127
+ sync.on('disconnected', () => console.log('Disconnected'))
193
128
  ```
194
129
 
195
- ## Host Transfer
130
+ ### Chat & Messages
131
+
132
+ Messages are just state:
196
133
 
197
134
  ```typescript
198
- // Host can transfer to another player
199
- if (room.isHost) {
200
- room.transferHost(otherPlayerId)
201
- }
135
+ state.chat = [
136
+ ...state.chat.slice(-50), // Keep last 50
137
+ { from: sync.myId, text: 'Hello!', ts: Date.now() }
138
+ ]
202
139
  ```
203
140
 
204
- ## Full Example: Simple Multiplayer Game
141
+ ## Full Example
205
142
 
206
143
  ```typescript
207
- import { Watchtower } from '@watchtower/sdk'
144
+ import { Watchtower } from '@watchtower-sdk/core'
208
145
 
209
146
  const wt = new Watchtower({ gameId: 'my-game', apiKey: 'wt_...' })
147
+ const state = { players: {} }
148
+ const sync = wt.sync(state)
210
149
 
211
150
  // Join or create room
212
- async function joinGame(code?: string) {
213
- const room = code
214
- ? await wt.joinRoom(code)
215
- : await wt.createRoom()
216
-
217
- console.log('Room:', room.code)
151
+ const code = prompt('Room code? (blank to create)')
152
+ if (code) {
153
+ await sync.join(code)
154
+ } else {
155
+ const newCode = await sync.create()
156
+ alert('Share: ' + newCode)
157
+ }
158
+
159
+ // Add yourself
160
+ state.players[sync.myId] = {
161
+ x: Math.random() * 800,
162
+ y: Math.random() * 600,
163
+ color: '#' + Math.floor(Math.random()*16777215).toString(16)
164
+ }
165
+
166
+ // Game loop
167
+ function loop() {
168
+ // Move
169
+ if (keys.left) state.players[sync.myId].x -= 5
170
+ if (keys.right) state.players[sync.myId].x += 5
171
+ if (keys.up) state.players[sync.myId].y -= 5
172
+ if (keys.down) state.players[sync.myId].y += 5
218
173
 
219
- // Game loop - update player position
220
- function gameLoop() {
221
- room.player.set({
222
- x: myPlayer.x,
223
- y: myPlayer.y,
224
- animation: myPlayer.currentAnim
225
- })
226
- requestAnimationFrame(gameLoop)
174
+ // Draw everyone
175
+ ctx.clearRect(0, 0, 800, 600)
176
+ for (const [id, p] of Object.entries(state.players)) {
177
+ ctx.fillStyle = p.color
178
+ ctx.fillRect(p.x - 10, p.y - 10, 20, 20)
227
179
  }
228
- gameLoop()
229
-
230
- // Render other players
231
- const otherPlayers: Record<string, Sprite> = {}
232
-
233
- room.on('players', (players) => {
234
- for (const [id, state] of Object.entries(players)) {
235
- if (id === room.playerId) continue
236
-
237
- // Create sprite if new player
238
- if (!otherPlayers[id]) {
239
- otherPlayers[id] = createSprite()
240
- }
241
-
242
- // Update position
243
- otherPlayers[id].x = state.x as number
244
- otherPlayers[id].y = state.y as number
245
- otherPlayers[id].play(state.animation as string)
246
- }
247
- })
248
-
249
- // Clean up when players leave
250
- room.on('playerLeft', (id) => {
251
- otherPlayers[id]?.destroy()
252
- delete otherPlayers[id]
253
- })
254
-
255
- // Handle game events
256
- room.on('message', (from, data: any) => {
257
- if (data.type === 'shoot') {
258
- createBullet(data.x, data.y, data.dir)
259
- }
260
- })
261
180
 
262
- // Game state (host manages rounds, scores)
263
- room.on('state', (state: any) => {
264
- if (state.phase === 'playing') {
265
- showRound(state.round)
266
- }
267
- if (state.phase === 'gameover') {
268
- showWinner(state.winner)
269
- }
270
- })
271
-
272
- // If we're host, start game when 2 players join
273
- room.on('playerJoined', (_, count) => {
274
- if (room.isHost && count >= 2) {
275
- room.state.set({ phase: 'playing', round: 1 })
276
- }
277
- })
278
-
279
- return room
181
+ requestAnimationFrame(loop)
280
182
  }
183
+ loop()
281
184
  ```
282
185
 
283
186
  ## API Reference
@@ -285,51 +188,40 @@ async function joinGame(code?: string) {
285
188
  ### Watchtower
286
189
 
287
190
  ```typescript
288
- const wt = new Watchtower(config)
191
+ const wt = new Watchtower({
192
+ gameId: string, // From dashboard
193
+ apiKey?: string, // From dashboard
194
+ playerId?: string // Auto-generated if not provided
195
+ })
196
+
197
+ wt.playerId // Current player ID
198
+ wt.gameId // Game ID
199
+
200
+ // Saves
201
+ await wt.save(key: string, data: any): Promise<void>
202
+ await wt.load<T>(key: string): Promise<T | null>
203
+ await wt.listSaves(): Promise<string[]>
204
+ await wt.deleteSave(key: string): Promise<void>
205
+
206
+ // Multiplayer
207
+ wt.sync(state: object, options?: SyncOptions): Sync
289
208
  ```
290
209
 
291
- | Config | Type | Description |
292
- |--------|------|-------------|
293
- | `gameId` | `string` | Your game's unique identifier |
294
- | `apiKey` | `string` | API key from dashboard |
295
- | `playerId` | `string?` | Custom player ID (auto-generated if not provided) |
296
- | `apiUrl` | `string?` | Custom API URL (default: Watchtower API) |
297
-
298
- ### Room
299
-
300
- | Property | Type | Description |
301
- |----------|------|-------------|
302
- | `code` | `string` | 4-letter room code |
303
- | `isHost` | `boolean` | True if you're the host |
304
- | `hostId` | `string` | Current host's player ID |
305
- | `playerId` | `string` | Your player ID |
306
- | `playerCount` | `number` | Number of players |
307
- | `players` | `PlayersState` | All players' states |
308
- | `connected` | `boolean` | Connection status |
309
-
310
- | Method | Description |
311
- |--------|-------------|
312
- | `player.set(state)` | Set your player state (auto-synced) |
313
- | `player.get()` | Get your current player state |
314
- | `player.sync()` | Force immediate sync |
315
- | `state.set(state)` | Set game state (host only) |
316
- | `state.get()` | Get current game state |
317
- | `broadcast(data)` | Send to all players |
318
- | `sendTo(id, data)` | Send to specific player |
319
- | `transferHost(id)` | Transfer host (host only) |
320
- | `disconnect()` | Leave the room |
321
-
322
- | Event | Callback | Description |
323
- |-------|----------|-------------|
324
- | `connected` | `({playerId, room}) => void` | Connected to room |
325
- | `players` | `(players) => void` | Player states updated |
326
- | `state` | `(state) => void` | Game state updated |
327
- | `playerJoined` | `(id, count) => void` | Player joined |
328
- | `playerLeft` | `(id, count) => void` | Player left |
329
- | `hostChanged` | `(newHostId) => void` | Host changed |
330
- | `message` | `(from, data) => void` | Received broadcast |
331
- | `disconnected` | `() => void` | Lost connection |
332
- | `error` | `(error) => void` | Error occurred |
210
+ ### Sync
211
+
212
+ ```typescript
213
+ sync.myId: string
214
+ sync.roomId: string | null
215
+ sync.connected: boolean
216
+
217
+ await sync.join(roomId: string, options?: JoinOptions): Promise<void>
218
+ await sync.leave(): Promise<void>
219
+ await sync.create(options?: CreateOptions): Promise<string>
220
+ await sync.listRooms(): Promise<RoomListing[]>
221
+
222
+ sync.on(event: string, callback: Function): void
223
+ sync.off(event: string, callback: Function): void
224
+ ```
333
225
 
334
226
  ## License
335
227
 
package/dist/index.d.mts CHANGED
@@ -206,6 +206,123 @@ 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
+ * Send a one-off message to all players in the room
288
+ *
289
+ * @param data - Any JSON-serializable data
290
+ */
291
+ broadcast(data: unknown): void;
292
+ /**
293
+ * Create a new room and join it
294
+ *
295
+ * @param options - Room creation options
296
+ * @returns The room code/ID
297
+ */
298
+ create(options?: Omit<JoinOptions, 'create'>): Promise<string>;
299
+ /**
300
+ * List public rooms
301
+ */
302
+ listRooms(): Promise<RoomListing[]>;
303
+ /**
304
+ * Subscribe to sync events
305
+ */
306
+ on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected' | 'message', callback: Function): void;
307
+ /**
308
+ * Unsubscribe from sync events
309
+ */
310
+ off(event: string, callback: Function): void;
311
+ private emit;
312
+ private connectWebSocket;
313
+ private handleMessage;
314
+ private applyFullState;
315
+ private applyPlayerState;
316
+ private removePlayer;
317
+ private clearRemotePlayers;
318
+ private findPlayersKey;
319
+ private startSyncLoop;
320
+ private stopSyncLoop;
321
+ private syncMyState;
322
+ private updateInterpolation;
323
+ private generateRoomCode;
324
+ private getHeaders;
325
+ }
209
326
  declare class Watchtower {
210
327
  /** @internal - Config is non-enumerable to prevent accidental API key exposure */
211
328
  private readonly config;
@@ -291,6 +408,36 @@ declare class Watchtower {
291
408
  * Note: This returns a promise, use `await wt.stats` or `wt.getStats()`
292
409
  */
293
410
  get stats(): Promise<GameStats>;
411
+ /**
412
+ * Create a synchronized state object
413
+ *
414
+ * Point this at your game state and it becomes multiplayer.
415
+ * No events, no callbacks - just read and write your state.
416
+ *
417
+ * @param state - Your game state object (e.g., { players: {} })
418
+ * @param options - Sync options (tickRate, interpolation)
419
+ * @returns A Sync instance
420
+ *
421
+ * @example
422
+ * ```ts
423
+ * const state = { players: {} }
424
+ * const sync = wt.sync(state)
425
+ *
426
+ * await sync.join('my-room')
427
+ *
428
+ * // Add yourself
429
+ * state.players[sync.myId] = { x: 0, y: 0 }
430
+ *
431
+ * // Move (automatically syncs to others)
432
+ * state.players[sync.myId].x = 100
433
+ *
434
+ * // Others appear automatically in state.players!
435
+ * for (const [id, player] of Object.entries(state.players)) {
436
+ * draw(player.x, player.y)
437
+ * }
438
+ * ```
439
+ */
440
+ sync<T extends Record<string, unknown>>(state: T, options?: SyncOptions): Sync<T>;
294
441
  }
295
442
 
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 };
443
+ 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 };