@bluebottle_gg/league-broadcast-client 0.1.0 โ†’ 0.2.0

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
@@ -5,8 +5,8 @@ A TypeScript/JavaScript client library for connecting to the League Broadcast ba
5
5
  ## Features
6
6
 
7
7
  - ๐Ÿ”Œ Automatic WebSocket connection management with reconnection
8
- - ๐Ÿ“Š Real-time game state and data updates
9
- - ๐ŸŽฎ Event-driven architecture for game events (kills, objectives, etc.)
8
+ - ๐Ÿ“Š Real-time game state and data updates โ€” in-game **and** pre-game (champion select)
9
+ - ๐ŸŽฎ Event-driven architecture for game events (kills, objectives, pick/ban actions, etc.)
10
10
  - โšก **Framework-agnostic reactive store** with fine-grained subscriptions
11
11
  - ๐Ÿ”„ Works seamlessly with React, Vue, Svelte, Solid, Angular, and vanilla JS
12
12
  - ๐Ÿ“ฆ TypeScript support with full type definitions
@@ -20,7 +20,10 @@ npm install @bluebottle_gg/league-broadcast-client
20
20
  ## Quick Start
21
21
 
22
22
  ```typescript
23
- import { LeagueBroadcastClient, GameState } from "@bluebottle_gg/league-broadcast-client";
23
+ import {
24
+ LeagueBroadcastClient,
25
+ GameState,
26
+ } from "@bluebottle_gg/league-broadcast-client";
24
27
 
25
28
  // Create client instance
26
29
  const client = new LeagueBroadcastClient({
@@ -30,13 +33,13 @@ const client = new LeagueBroadcastClient({
30
33
  });
31
34
 
32
35
  // Listen for state updates
33
- client.onStateUpdate((gameData) => {
36
+ client.onIngameStateUpdate((gameData) => {
34
37
  console.log("Game time:", gameData.gameTime);
35
38
  console.log("Scoreboard:", gameData.scoreboard);
36
39
  });
37
40
 
38
41
  // Listen for game status changes
39
- client.onGameStatusChange((status, isTestingEnv) => {
42
+ client.onIngameStatusChange((status, isTestingEnv) => {
40
43
  if (status === GameState.InGame) {
41
44
  console.log("Game started!");
42
45
  } else if (status === GameState.OutOfGame) {
@@ -45,7 +48,7 @@ client.onGameStatusChange((status, isTestingEnv) => {
45
48
  });
46
49
 
47
50
  // Listen for game events
48
- client.onGameEvents({
51
+ client.onIngameEvents({
49
52
  onKillFeedEvent: (event) => {
50
53
  console.log("Kill event:", event);
51
54
  },
@@ -58,6 +61,50 @@ client.onGameEvents({
58
61
  });
59
62
  ```
60
63
 
64
+ ## Pre-Game Quick Start
65
+
66
+ The same `LeagueBroadcastClient` also connects to the pre-game WebSocket
67
+ (`/ws/pre`) automatically โ€” no second client needed.
68
+
69
+ ```typescript
70
+ import {
71
+ LeagueBroadcastClient,
72
+ PickBanPhase,
73
+ } from "@bluebottle_gg/league-broadcast-client";
74
+
75
+ const client = new LeagueBroadcastClient({
76
+ host: "localhost",
77
+ port: 58869,
78
+ autoConnect: true,
79
+ });
80
+
81
+ // Receive state updates every time the server pushes new champ-select data
82
+ client.onChampSelectUpdate((data) => {
83
+ if (!data.isActive) return;
84
+ console.log("Phase:", PickBanPhase[data.timer.phaseName]);
85
+ console.log("Time remaining:", data.timer.timeRemaining);
86
+ console.log(
87
+ "Blue picks:",
88
+ data.blueTeam.slots.map((s) => s.champion?.name),
89
+ );
90
+ });
91
+
92
+ // High-level lifecycle events
93
+ client.onChampSelectEvents({
94
+ onChampSelectStart: () => console.log("Draft started!"),
95
+ onChampSelectEnd: () => console.log("Champions locked โ€” game loading"),
96
+ onAction: (action) => console.log("Pick/ban action:", action),
97
+ onRouteUpdate: (uri) => console.log("Frontend route:", uri),
98
+ });
99
+
100
+ // Reactive selector (works with React useSyncExternalStore, Svelte $, Vue ref, etc.)
101
+ const timer = client.selectChampSelect((s) => s.champSelectData.timer);
102
+ client.watchChampSelect(
103
+ (s) => s.champSelectData.blueTeam.slots,
104
+ (slots) => console.log("Blue picks updated:", slots),
105
+ );
106
+ ```
107
+
61
108
  ## API Reference
62
109
 
63
110
  ### LeagueBroadcastClient
@@ -70,78 +117,159 @@ new LeagueBroadcastClient(config: LeagueBroadcastClientConfig)
70
117
 
71
118
  **Config Options:**
72
119
 
73
- | Option | Type | Default | Description |
74
- | ------------- | --------- | ------------ | -------------------------------------- |
75
- | `host` | `string` | **required** | Backend server hostname or IP |
76
- | `port` | `number` | `58869` | Backend server port |
77
- | `wsRoute` | `string` | `/ws/in` | WebSocket endpoint route |
78
- | `apiRoute` | `string` | `/api` | API endpoint route |
79
- | `cacheRoute` | `string` | `/cache` | Cache endpoint route |
80
- | `useHttps` | `boolean` | `false` | Use WSS/HTTPS instead of WS/HTTP |
81
- | `autoConnect` | `boolean` | `true` | Automatically connect on instantiation |
120
+ | Option | Type | Default | Description |
121
+ | ---------------- | --------- | ------------ | -------------------------------------- |
122
+ | `host` | `string` | **required** | Backend server hostname or IP |
123
+ | `port` | `number` | `58869` | Backend server port |
124
+ | `ingameWsRoute` | `string` | `/ws/in` | In-game WebSocket endpoint route |
125
+ | `preGameWsRoute` | `string` | `/ws/pre` | Pre-game WebSocket endpoint route |
126
+ | `apiRoute` | `string` | `/api` | API endpoint route |
127
+ | `cacheRoute` | `string` | `/cache` | Cache endpoint route |
128
+ | `useHttps` | `boolean` | `false` | Use WSS/HTTPS instead of WS/HTTP |
129
+ | `autoConnect` | `boolean` | `true` | Automatically connect on instantiation |
82
130
 
83
131
  #### Methods
84
132
 
85
133
  ##### Connection Management
86
134
 
87
135
  ```typescript
88
- // Connect to backend
136
+ // Connect to both in-game and pre-game WebSocket endpoints
89
137
  await client.connect(): Promise<void>
90
138
 
91
- // Disconnect from backend
139
+ // Disconnect from both endpoints
92
140
  client.disconnect(): void
93
141
 
94
- // Check connection status
95
- client.isConnected(): boolean
142
+ // Check in-game connection status
143
+ client.isIngameConnected(): boolean
144
+
145
+ // Check pre-game connection status
146
+ client.isPreGameConnected(): boolean
96
147
  ```
97
148
 
98
- ##### Data Access
149
+ ##### In-Game Data Access
99
150
 
100
151
  ```typescript
101
- // Get current game data
102
- client.getGameData(): ingameFrontendData
152
+ client.getIngameData(): ingameFrontendData
153
+ client.getIngameState(): GameState
154
+ client.isInTestingEnvironment(): boolean
155
+ ```
103
156
 
104
- // Get current game state
105
- client.getGameState(): GameState
157
+ ##### Pre-Game Data Access
106
158
 
107
- // Check if in testing environment
108
- client.isInTestingEnvironment(): boolean
159
+ ```typescript
160
+ client.getChampSelectData(): champSelectStateData
161
+ client.isChampSelectActive(): boolean
109
162
  ```
110
163
 
111
164
  ##### URLs
112
165
 
113
166
  ```typescript
114
- // Get API base URL
115
167
  client.getApiUrl(): string
116
-
117
- // Get cache URL (optionally with path)
118
168
  client.getCacheUrl(path?: string): string
119
169
  ```
120
170
 
121
- ##### Event Handlers
171
+ ##### In-Game Event Handlers
122
172
 
123
173
  ```typescript
124
- // State updates - called when game data changes
125
- client.onStateUpdate(handler: (state: ingameFrontendData) => void): () => void
174
+ // State updates - called when in-game data changes
175
+ client.onIngameStateUpdate(handler: (state: ingameFrontendData) => void): () => void
126
176
 
127
177
  // Game status changes - called when game starts/ends
128
- client.onGameStatusChange(handler: (status: GameState, isTestingEnv: boolean) => void): () => void
178
+ client.onIngameStatusChange(handler: (status: GameState, isTestingEnv: boolean) => void): () => void
129
179
 
130
180
  // Game events - register handlers for various game events
131
- client.onGameEvents(handlers: GameDataEventHandlers): void
181
+ client.onIngameEvents(handlers: IngameEventHandlers): void
132
182
 
133
- // Connection events
134
- client.onConnect(handler: () => void): () => void
135
- client.onDisconnect(handler: () => void): () => void
136
- client.onError(handler: (error: Event) => void): () => void
183
+ // In-game connection events
184
+ client.onIngameConnect(handler: () => void): () => void
185
+ client.onIngameDisconnect(handler: () => void): () => void
186
+ client.onIngameError(handler: (error: Event) => void): () => void
187
+ ```
188
+
189
+ ##### Pre-Game Event Handlers
190
+
191
+ ```typescript
192
+ // Called on every champ-select state push from the server
193
+ client.onChampSelectUpdate(handler: (state: champSelectStateData) => void): () => void
194
+
195
+ // Register handlers for lifecycle and action events
196
+ client.onChampSelectEvents(handlers: ChampSelectEventHandlers): void
197
+
198
+ // Pre-game connection events
199
+ client.onPreGameConnect(handler: () => void): () => void
200
+ client.onPreGameDisconnect(handler: () => void): () => void
201
+ client.onPreGameError(handler: (error: Event) => void): () => void
137
202
  ```
138
203
 
139
204
  All event handler registration methods return an unsubscribe function.
140
205
 
141
- ### GameDataEventHandlers
206
+ ##### In-Game Reactive API
207
+
208
+ ```typescript
209
+ // Subscribe to a fine-grained slice of in-game state
210
+ client.selectIngame<S>(selector: (s: GameStateSnapshot) => S, equalityFn?): Subscribable<S>
211
+
212
+ // Watch a value imperatively; callback fires on change
213
+ client.watchIngame<S>(selector, callback, equalityFn?): () => void
214
+
215
+ // Direct access to the reactive in-game store
216
+ client.ingameStore: GameStateStore
217
+ ```
218
+
219
+ ##### Pre-Game Reactive API
220
+
221
+ ```typescript
222
+ // Subscribe to a fine-grained slice of champ-select state
223
+ client.selectChampSelect<S>(selector: (s: ChampSelectSnapshot) => S, equalityFn?): Subscribable<S>
224
+
225
+ // Watch a value imperatively; callback fires on change
226
+ client.watchChampSelect<S>(selector, callback, equalityFn?): () => void
227
+
228
+ // Direct access to the reactive pre-game store
229
+ client.preGameStore: ChampSelectStateStore
230
+ ```
231
+
232
+ #### `ChampSelectEventHandlers`
233
+
234
+ ```typescript
235
+ interface ChampSelectEventHandlers {
236
+ /** Every pick/ban action (hover, lock, ban reveal, phase transition). */
237
+ onAction?: (action: pickBanActionEventArgs) => void;
238
+ /** Fires when champ select becomes active. */
239
+ onChampSelectStart?: () => void;
240
+ /** Fires when champ select ends (isActive becomes false). */
241
+ onChampSelectEnd?: () => void;
242
+ /** Fires when the backend navigates the frontend to a new route. */
243
+ onRouteUpdate?: (uri: string) => void;
244
+ }
245
+ ```
246
+
247
+ #### `ChampSelectSnapshot`
248
+
249
+ The snapshot object passed to `selectChampSelect()` / `watchChampSelect()` selectors:
250
+
251
+ ```typescript
252
+ interface ChampSelectSnapshot {
253
+ readonly champSelectData: champSelectStateData; // Full champ-select state
254
+ readonly isActive: boolean; // Whether draft is in progress
255
+ readonly version: number; // Monotonic version counter
256
+ }
257
+ ```
258
+
259
+ #### WebSocket Message Types
260
+
261
+ | Message type | Handled as |
262
+ | ------------------------------ | ------------------------------------------------------------------------------- |
263
+ | `champion-select-state-update` | Updates `champSelectStateData`, fires `onChampSelectStart` / `onChampSelectEnd` |
264
+ | `champion-select-action` | Fires `onAction` |
265
+ | `frontend-route-update` | Fires `onRouteUpdate` |
266
+
267
+ ---
268
+
269
+ ### IngameEventHandlers
142
270
 
143
271
  ```typescript
144
- interface GameDataEventHandlers {
272
+ interface IngameEventHandlers {
145
273
  onPlayerEvent?: (event: playerEvent) => void;
146
274
  onTeamEvent?: (event: teamEvent) => void;
147
275
  onObjectiveEvent?: (event: objectiveEvent) => void;
@@ -165,7 +293,7 @@ The reactive API is built around **selectors** โ€” functions that extract a slic
165
293
 
166
294
  ```typescript
167
295
  // Watch a specific value
168
- const unsubscribe = client.watch(
296
+ const unsubscribe = client.watchIngame(
169
297
  (s) => s.gameData.scoreboard?.teams[0]?.kills,
170
298
  (kills, prevKills) => {
171
299
  console.log(`Blue team kills: ${prevKills} โ†’ ${kills}`);
@@ -182,7 +310,7 @@ unsubscribe();
182
310
  import { useSyncExternalStore } from 'react';
183
311
 
184
312
  function BlueTeamKills() {
185
- const slice = client.select(s => s.gameData.scoreboard?.teams[0]?.kills);
313
+ const slice = client.selectIngame(s => s.gameData.scoreboard?.teams[0]?.kills);
186
314
  const kills = useSyncExternalStore(slice.subscribe, slice.getSnapshot);
187
315
 
188
316
  return <div>Blue Kills: {kills ?? 0}</div>;
@@ -198,7 +326,7 @@ export default {
198
326
  setup() {
199
327
  const kills = ref(0);
200
328
 
201
- const unsubscribe = client.watch(
329
+ const unsubscribe = client.watchIngame(
202
330
  (s) => s.gameData.scoreboard?.teams[0]?.kills,
203
331
  (value) => {
204
332
  kills.value = value ?? 0;
@@ -214,11 +342,11 @@ export default {
214
342
 
215
343
  #### Svelte
216
344
 
217
- The `Subscribable` returned by `select()` matches Svelte's store contract:
345
+ The `Subscribable` returned by `selectIngame()` matches Svelte's store contract:
218
346
 
219
347
  ```svelte
220
348
  <script lang="ts">
221
- const kills = client.select(s => s.gameData.scoreboard?.teams[0]?.kills);
349
+ const kills = client.selectIngame(s => s.gameData.scoreboard?.teams[0]?.kills);
222
350
  </script>
223
351
 
224
352
  <div>Blue Kills: {$kills ?? 0}</div>
@@ -226,12 +354,12 @@ The `Subscribable` returned by `select()` matches Svelte's store contract:
226
354
 
227
355
  ### Reactive Methods
228
356
 
229
- #### `client.select(selector, equalityFn?)`
357
+ #### `client.selectIngame(selector, equalityFn?)`
230
358
 
231
359
  Create a subscribable slice that only notifies listeners when the selected value changes.
232
360
 
233
361
  ```typescript
234
- const slice = client.select(
362
+ const slice = client.selectIngame(
235
363
  (s) => s.gameData.gameTime,
236
364
  Object.is, // Default equality function (can use shallowEqual for objects)
237
365
  );
@@ -255,12 +383,12 @@ const unsubscribe = slice.subscribe(() => {
255
383
  - `subscribe(listener: () => void): () => void` โ€” Subscribe to changes
256
384
  - `getSnapshot(): T` โ€” Get current value
257
385
 
258
- #### `client.watch(selector, callback, equalityFn?)`
386
+ #### `client.watchIngame(selector, callback, equalityFn?)`
259
387
 
260
388
  Imperative API for watching a derived value. Callback is invoked whenever the selected value changes.
261
389
 
262
390
  ```typescript
263
- const unsubscribe = client.watch(
391
+ const unsubscribe = client.watchIngame(
264
392
  (s) => s.gameData.scoreboard?.teams[0]?.kills,
265
393
  (kills, prevKills) => {
266
394
  console.log(`Kills changed: ${prevKills} โ†’ ${kills}`);
@@ -276,24 +404,24 @@ const unsubscribe = client.watch(
276
404
 
277
405
  **Returns:** `() => void` โ€” Unsubscribe function
278
406
 
279
- #### `client.store`
407
+ #### `client.ingameStore`
280
408
 
281
409
  Direct access to the `GameStateStore` instance for advanced use cases:
282
410
 
283
411
  ```typescript
284
412
  // Get full snapshot
285
- const snapshot = client.store.getSnapshot();
413
+ const snapshot = client.ingameStore.getSnapshot();
286
414
  console.log("Game time:", snapshot.gameData.gameTime);
287
415
  console.log("Game state:", snapshot.gameState);
288
416
  console.log("Version:", snapshot.version);
289
417
 
290
418
  // Subscribe to all changes (unfiltered)
291
- const unsubscribe = client.store.subscribe(() => {
419
+ const unsubscribe = client.ingameStore.subscribe(() => {
292
420
  console.log("Store updated");
293
421
  });
294
422
 
295
423
  // Watch with immediate callback
296
- client.store.watchImmediate(
424
+ client.ingameStore.watchImmediate(
297
425
  (s) => s.gameData.gameTime,
298
426
  (time, prevTime) => {
299
427
  console.log(`Time: ${prevTime} โ†’ ${time}`);
@@ -308,7 +436,7 @@ client.store.watchImmediate(
308
436
  Uses strict equality (`===`). Best for primitives and object references:
309
437
 
310
438
  ```typescript
311
- client.select((s) => s.gameData.gameTime); // Uses Object.is by default
439
+ client.selectIngame((s) => s.gameData.gameTime); // Uses Object.is by default
312
440
  ```
313
441
 
314
442
  #### `shallowEqual`
@@ -318,7 +446,7 @@ Compares objects by their enumerable properties. Use when selecting objects whos
318
446
  ```typescript
319
447
  import { shallowEqual } from "@bluebottle_gg/league-broadcast-client";
320
448
 
321
- const stats = client.select(
449
+ const stats = client.selectIngame(
322
450
  (s) => ({
323
451
  kills: s.gameData.scoreboard?.teams[0]?.kills ?? 0,
324
452
  deaths: s.gameData.scoreboardBottom?.teams[0]?.players[0]?.deaths ?? 0,
@@ -327,15 +455,25 @@ const stats = client.select(
327
455
  );
328
456
  ```
329
457
 
330
- ### GameStateSnapshot
458
+ ### Snapshots
331
459
 
332
- The snapshot passed to selectors contains:
460
+ #### `GameStateSnapshot` (in-game)
333
461
 
334
462
  ```typescript
335
463
  interface GameStateSnapshot {
336
- gameData: ingameFrontendData; // Full game data
337
- gameState: GameState; // Current game state enum
338
- version: number; // Monotonic version counter
464
+ readonly gameData: ingameFrontendData; // Full in-game data
465
+ readonly gameState: GameState; // Current game state enum
466
+ readonly version: number; // Monotonic version counter
467
+ }
468
+ ```
469
+
470
+ #### `ChampSelectSnapshot` (pre-game)
471
+
472
+ ```typescript
473
+ interface ChampSelectSnapshot {
474
+ readonly champSelectData: champSelectStateData; // Full champ-select state
475
+ readonly isActive: boolean; // Whether draft is in progress
476
+ readonly version: number; // Monotonic version counter
339
477
  }
340
478
  ```
341
479
 
@@ -344,15 +482,15 @@ interface GameStateSnapshot {
344
482
  **โœ… DO:** Use selectors to watch specific values
345
483
 
346
484
  ```typescript
347
- client.watch((s) => s.gameData.scoreboard?.teams[0]?.kills, handleKills);
485
+ client.watchIngame((s) => s.gameData.scoreboard?.teams[0]?.kills, handleKills);
348
486
  ```
349
487
 
350
488
  **โŒ DON'T:** Subscribe to entire store and manually filter
351
489
 
352
490
  ```typescript
353
491
  // This triggers on EVERY change, even unrelated ones
354
- client.store.subscribe(() => {
355
- const kills = client.store.getSnapshot().gameData.scoreboard?.teams[0]?.kills;
492
+ client.ingameStore.subscribe(() => {
493
+ const kills = client.ingameStore.getSnapshot().gameData.scoreboard?.teams[0]?.kills;
356
494
  handleKills(kills);
357
495
  });
358
496
  ```
@@ -360,7 +498,7 @@ client.store.subscribe(() => {
360
498
  **โœ… DO:** Use `shallowEqual` for derived objects
361
499
 
362
500
  ```typescript
363
- client.watch(
501
+ client.watchIngame(
364
502
  (s) => ({ kills: s.gameData.scoreboard?.teams[0]?.kills ?? 0 }),
365
503
  handleStats,
366
504
  shallowEqual,
@@ -371,24 +509,27 @@ client.watch(
371
509
 
372
510
  ```typescript
373
511
  // Only triggers when team object reference changes
374
- client.watch((s) => s.gameData.scoreboard?.teams[0], handleTeam);
512
+ client.watchIngame((s) => s.gameData.scoreboard?.teams[0], handleTeam);
375
513
  ```
376
514
 
377
515
  **โœ… DO:** Handle `null` as โ€œnot availableโ€
378
516
 
379
517
  ```typescript
380
- client.watch((s) => s.gameData.scoreboard, (scoreboard) => {
381
- if (!scoreboard) {
382
- hideScoreboard();
383
- return;
384
- }
385
- renderScoreboard(scoreboard);
386
- });
518
+ client.watchIngame(
519
+ (s) => s.gameData.scoreboard,
520
+ (scoreboard) => {
521
+ if (!scoreboard) {
522
+ hideScoreboard();
523
+ return;
524
+ }
525
+ renderScoreboard(scoreboard);
526
+ },
527
+ );
387
528
  ```
388
529
 
389
530
  ### Framework Integration Examples
390
531
 
391
- See [examples/reactivity.ts](examples/reactivity.ts) for complete examples with:
532
+ See [examples/reactivity.ts](examples/reactivity.ts) for complete in-game examples with:
392
533
 
393
534
  - React (with `useSyncExternalStore` and custom hooks)
394
535
  - Vue 3 (Composition API)
@@ -397,9 +538,20 @@ See [examples/reactivity.ts](examples/reactivity.ts) for complete examples with:
397
538
  - Angular (with Signals)
398
539
  - Vanilla JavaScript
399
540
 
541
+ See [examples/pregame-reactivity.ts](examples/pregame-reactivity.ts) for the equivalent examples using pre-game reactive selectors (`selectChampSelect` / `watchChampSelect`).
542
+
400
543
  ## Examples
401
544
 
402
- ### Basic Usage
545
+ See the [examples/](examples/) directory for full runnable code.
546
+
547
+ | File | Description |
548
+ | ---------------------------------------------------------------- | ----------------------------------------------------------------------- |
549
+ | [examples/usage.ts](examples/usage.ts) | In-game usage: state updates, events, cache URLs, scoreboards |
550
+ | [examples/reactivity.ts](examples/reactivity.ts) | In-game reactive API across all major frameworks |
551
+ | [examples/pregame-usage.ts](examples/pregame-usage.ts) | Pre-game usage: champ select state, pick/ban actions, team compositions |
552
+ | [examples/pregame-reactivity.ts](examples/pregame-reactivity.ts) | Pre-game reactive API across all major frameworks |
553
+
554
+ ### Basic In-Game Usage
403
555
 
404
556
  ```typescript
405
557
  import { LeagueBroadcastClient } from "@bluebottle_gg/league-broadcast-client";
@@ -411,11 +563,56 @@ const client = new LeagueBroadcastClient({
411
563
 
412
564
  // Game data is automatically updated in real-time
413
565
  setInterval(() => {
414
- const data = client.getGameData();
566
+ const data = client.getIngameData();
415
567
  console.log("Current game time:", data.gameTime);
416
568
  }, 1000);
417
569
  ```
418
570
 
571
+ ### Basic Pre-Game Usage
572
+
573
+ ```typescript
574
+ import {
575
+ LeagueBroadcastClient,
576
+ PickBanPhase,
577
+ } from "@bluebottle_gg/league-broadcast-client";
578
+
579
+ const client = new LeagueBroadcastClient({ host: "192.168.1.100" });
580
+
581
+ client.onChampSelectEvents({
582
+ onAction: (action) => {
583
+ console.log(
584
+ `Action: ${action.type} โ€” ${action.champion?.name ?? "hovering"}`,
585
+ );
586
+ },
587
+ });
588
+
589
+ client.watchChampSelect(
590
+ (s) => s.champSelectData.timer.phaseName,
591
+ (phase) => console.log("Phase:", PickBanPhase[phase]),
592
+ );
593
+ ```
594
+
595
+ ### Full Lifecycle (Draft โ†’ In-Game)
596
+
597
+ ```typescript
598
+ import {
599
+ LeagueBroadcastClient,
600
+ GameState,
601
+ } from "@bluebottle_gg/league-broadcast-client";
602
+
603
+ // One client handles both /ws/in and /ws/pre
604
+ const client = new LeagueBroadcastClient({ host: "localhost" });
605
+
606
+ client.onChampSelectEvents({
607
+ onChampSelectStart: () => console.log("Draft started"),
608
+ onChampSelectEnd: () => console.log("Game loading"),
609
+ });
610
+
611
+ client.onIngameStatusChange((status) => {
612
+ if (status === GameState.OutOfGame) console.log("Returned to lobby");
613
+ });
614
+ ```
615
+
419
616
  ### React Integration
420
617
 
421
618
  ```typescript
@@ -428,11 +625,11 @@ function useLeagueBroadcast(host: string, port: number) {
428
625
  const [gameState, setGameState] = useState<GameState>(GameState.OutOfGame);
429
626
 
430
627
  useEffect(() => {
431
- const unsubscribeState = client.onStateUpdate((data) => {
628
+ const unsubscribeState = client.onIngameStateUpdate((data) => {
432
629
  setGameData(data);
433
630
  });
434
631
 
435
- const unsubscribeStatus = client.onGameStatusChange((status) => {
632
+ const unsubscribeStatus = client.onIngameStatusChange((status) => {
436
633
  setGameState(status);
437
634
  });
438
635
 
@@ -475,7 +672,7 @@ const client = new LeagueBroadcastClient({
475
672
  });
476
673
 
477
674
  // Handle all game events
478
- client.onGameEvents({
675
+ client.onIngameEvents({
479
676
  onKillFeedEvent: (event) => {
480
677
  console.log(`${event.killerName} killed ${event.victimName}`);
481
678
  },