@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 +275 -78
- package/dist/index.d.ts +1126 -88
- package/dist/index.js +1046 -111
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
74
|
-
|
|
|
75
|
-
| `host`
|
|
76
|
-
| `port`
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
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
|
|
136
|
+
// Connect to both in-game and pre-game WebSocket endpoints
|
|
89
137
|
await client.connect(): Promise<void>
|
|
90
138
|
|
|
91
|
-
// Disconnect from
|
|
139
|
+
// Disconnect from both endpoints
|
|
92
140
|
client.disconnect(): void
|
|
93
141
|
|
|
94
|
-
// Check connection status
|
|
95
|
-
client.
|
|
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
|
-
|
|
102
|
-
client.
|
|
152
|
+
client.getIngameData(): ingameFrontendData
|
|
153
|
+
client.getIngameState(): GameState
|
|
154
|
+
client.isInTestingEnvironment(): boolean
|
|
155
|
+
```
|
|
103
156
|
|
|
104
|
-
|
|
105
|
-
client.getGameState(): GameState
|
|
157
|
+
##### Pre-Game Data Access
|
|
106
158
|
|
|
107
|
-
|
|
108
|
-
client.
|
|
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.
|
|
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.
|
|
178
|
+
client.onIngameStatusChange(handler: (status: GameState, isTestingEnv: boolean) => void): () => void
|
|
129
179
|
|
|
130
180
|
// Game events - register handlers for various game events
|
|
131
|
-
client.
|
|
181
|
+
client.onIngameEvents(handlers: IngameEventHandlers): void
|
|
132
182
|
|
|
133
|
-
//
|
|
134
|
-
client.
|
|
135
|
-
client.
|
|
136
|
-
client.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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 `
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
419
|
+
const unsubscribe = client.ingameStore.subscribe(() => {
|
|
292
420
|
console.log("Store updated");
|
|
293
421
|
});
|
|
294
422
|
|
|
295
423
|
// Watch with immediate callback
|
|
296
|
-
client.
|
|
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.
|
|
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.
|
|
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
|
-
###
|
|
458
|
+
### Snapshots
|
|
331
459
|
|
|
332
|
-
|
|
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.
|
|
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.
|
|
355
|
-
const kills = client.
|
|
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.
|
|
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.
|
|
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.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
628
|
+
const unsubscribeState = client.onIngameStateUpdate((data) => {
|
|
432
629
|
setGameData(data);
|
|
433
630
|
});
|
|
434
631
|
|
|
435
|
-
const unsubscribeStatus = client.
|
|
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.
|
|
675
|
+
client.onIngameEvents({
|
|
479
676
|
onKillFeedEvent: (event) => {
|
|
480
677
|
console.log(`${event.killerName} killed ${event.victimName}`);
|
|
481
678
|
},
|