kodenique-game-sdk 1.0.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/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/GameContext.d.ts +4 -0
- package/dist/GameContext.js +327 -0
- package/dist/GameDebug.d.ts +5 -0
- package/dist/GameDebug.js +172 -0
- package/dist/GamePlayer.d.ts +26 -0
- package/dist/GamePlayer.js +54 -0
- package/dist/SimplePlayer.d.ts +11 -0
- package/dist/SimplePlayer.js +41 -0
- package/dist/components/GamePlayerOverlays.d.ts +29 -0
- package/dist/components/GamePlayerOverlays.js +467 -0
- package/dist/components/GamePlayerVideo.d.ts +7 -0
- package/dist/components/GamePlayerVideo.js +86 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +46 -0
- package/dist/contexts/GameStreamContext.d.ts +24 -0
- package/dist/contexts/GameStreamContext.js +170 -0
- package/dist/examples/GameStreamExample.d.ts +26 -0
- package/dist/examples/GameStreamExample.js +92 -0
- package/dist/examples/SimpleAutoSubscribe.d.ts +9 -0
- package/dist/examples/SimpleAutoSubscribe.js +29 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useGameStream.d.ts +29 -0
- package/dist/hooks/useGameStream.js +78 -0
- package/dist/hooks/useWebRTC.d.ts +21 -0
- package/dist/hooks/useWebRTC.js +555 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/lib/pusher.d.ts +50 -0
- package/dist/lib/pusher.js +137 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.js +1 -0
- package/dist/useGames.d.ts +2 -0
- package/dist/useGames.js +73 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Kodenique
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# @kodenique/game-sdk
|
|
2
|
+
|
|
3
|
+
React SDK for real-time game streaming with WebSocket updates and WebRTC video player.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎮 **Real-time Game Updates** - WebSocket integration with automatic reconnection
|
|
8
|
+
- 📹 **WebRTC Video Streaming** - Ultra-low latency WHEP protocol support
|
|
9
|
+
- 🎯 **Winner Animations** - Automatic full-screen animations when rounds finish
|
|
10
|
+
- 📊 **Live Score Overlays** - Beautiful score displays with team logos and colors
|
|
11
|
+
- 🌐 **Network Monitoring** - Real-time latency and bitrate tracking
|
|
12
|
+
- ⚡ **Auto-Subscribe** - Just pass a game object and SDK handles everything
|
|
13
|
+
- 🎨 **Customizable UI** - Show/hide overlays, custom titles, and styling
|
|
14
|
+
- 📱 **Responsive** - Works on desktop and mobile
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @kodenique/game-sdk
|
|
20
|
+
# or
|
|
21
|
+
yarn add @kodenique/game-sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { GameProvider, GamePlayer } from '@kodenique/game-sdk';
|
|
28
|
+
|
|
29
|
+
function App() {
|
|
30
|
+
return (
|
|
31
|
+
<GameProvider
|
|
32
|
+
access_token="your-access-token"
|
|
33
|
+
socketToken="your-socket-token"
|
|
34
|
+
environment="production"
|
|
35
|
+
>
|
|
36
|
+
<GamePlayer
|
|
37
|
+
game={gameObject}
|
|
38
|
+
showScoreOverlay={true}
|
|
39
|
+
showWinnerAnimation={true}
|
|
40
|
+
width="100%"
|
|
41
|
+
height="600px"
|
|
42
|
+
/>
|
|
43
|
+
</GameProvider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Props
|
|
49
|
+
|
|
50
|
+
### GamePlayer
|
|
51
|
+
|
|
52
|
+
| Prop | Type | Default | Description |
|
|
53
|
+
|------|------|---------|-------------|
|
|
54
|
+
| `game` | Game | - | Game object (auto-subscribes to updates) |
|
|
55
|
+
| `gameId` | string | - | Game ID (auto-subscribes to updates) |
|
|
56
|
+
| `streamUrl` | string | - | Direct WHEP stream URL |
|
|
57
|
+
| `showScoreOverlay` | boolean | true | Show score overlay |
|
|
58
|
+
| `showWinnerAnimation` | boolean | true | Show winner animation when round finishes |
|
|
59
|
+
| `showNetworkIndicator` | boolean | true | Show network quality indicator |
|
|
60
|
+
| `showGameTitle` | boolean | true | Show game title |
|
|
61
|
+
| `showRound` | boolean | true | Show round information |
|
|
62
|
+
| `gameTitle` | string | - | Custom game title |
|
|
63
|
+
| `width` | string \| number | "100%" | Player width |
|
|
64
|
+
| `height` | string \| number | "600px" | Player height |
|
|
65
|
+
| `autoPlay` | boolean | true | Auto-play video |
|
|
66
|
+
| `muted` | boolean | true | Mute video by default |
|
|
67
|
+
| `controls` | boolean | true | Show video controls |
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
- [API Documentation](./API_DOCUMENTATION.md) - Complete API reference
|
|
72
|
+
- [Integration Guide](./INTEGRATION_GUIDE.md) - Implementation details
|
|
73
|
+
|
|
74
|
+
## Features
|
|
75
|
+
|
|
76
|
+
### Winner Animations
|
|
77
|
+
|
|
78
|
+
Automatically shows full-screen animations when rounds finish:
|
|
79
|
+
- Trophy icon for wins, handshake for draws
|
|
80
|
+
- Team color highlights
|
|
81
|
+
- Confetti animation
|
|
82
|
+
- Auto-dismisses after 5 seconds
|
|
83
|
+
- Can be disabled with `showWinnerAnimation={false}`
|
|
84
|
+
|
|
85
|
+
### WebSocket Integration
|
|
86
|
+
|
|
87
|
+
Real-time updates via SocketCluster:
|
|
88
|
+
- Automatic decompression of messages
|
|
89
|
+
- Supports multiple message formats
|
|
90
|
+
- Auto-reconnection on disconnect
|
|
91
|
+
- Subscribes to 3 channels per game
|
|
92
|
+
|
|
93
|
+
### Video Streaming
|
|
94
|
+
|
|
95
|
+
WebRTC with WHEP protocol:
|
|
96
|
+
- Ultra-low latency streaming
|
|
97
|
+
- Automatic quality detection
|
|
98
|
+
- Network stats monitoring
|
|
99
|
+
- Retry on connection failure
|
|
100
|
+
|
|
101
|
+
## TypeScript
|
|
102
|
+
|
|
103
|
+
Fully typed with TypeScript. All interfaces and types are exported.
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import type { Game, Team, Round, GamePlayerProps } from '@kodenique/game-sdk';
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Examples
|
|
110
|
+
|
|
111
|
+
### Disable Winner Animation
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
<GamePlayer
|
|
115
|
+
game={game}
|
|
116
|
+
showWinnerAnimation={false} // Disable winner animation
|
|
117
|
+
/>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Custom Title
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
<GamePlayer
|
|
124
|
+
game={game}
|
|
125
|
+
gameTitle="Championship Finals"
|
|
126
|
+
showGameTitle={true}
|
|
127
|
+
/>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Hide Score Overlay
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
<GamePlayer
|
|
134
|
+
game={game}
|
|
135
|
+
showScoreOverlay={false} // Hide all overlays
|
|
136
|
+
/>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Manual WebSocket Control
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
import { useGameStream } from '@kodenique/game-sdk';
|
|
143
|
+
|
|
144
|
+
function CustomComponent() {
|
|
145
|
+
const { game, isConnected, subscribe, unsubscribe } = useGameStream({
|
|
146
|
+
gameId: 'game-123',
|
|
147
|
+
autoSubscribe: true
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div>
|
|
152
|
+
<p>Connected: {isConnected ? 'Yes' : 'No'}</p>
|
|
153
|
+
<p>Round: {game?.rounds.length}</p>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Browser Support
|
|
160
|
+
|
|
161
|
+
- Chrome/Edge (latest)
|
|
162
|
+
- Firefox (latest)
|
|
163
|
+
- Safari (latest)
|
|
164
|
+
- Mobile browsers with WebRTC support
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT
|
|
169
|
+
|
|
170
|
+
## Support
|
|
171
|
+
|
|
172
|
+
For issues or questions, please open an issue on [GitHub](https://github.com/kodenique/game-sdk/issues).
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
2
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
3
|
+
var m = o[Symbol.asyncIterator], i;
|
|
4
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
5
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
6
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
7
|
+
};
|
|
8
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
9
|
+
var t = {};
|
|
10
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
11
|
+
t[p] = s[p];
|
|
12
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
13
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
14
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
15
|
+
t[p[i]] = s[p[i]];
|
|
16
|
+
}
|
|
17
|
+
return t;
|
|
18
|
+
};
|
|
19
|
+
import React, { createContext, useContext, useState, useEffect, useCallback, useRef, } from "react";
|
|
20
|
+
import SocketClient from "socketcluster-wrapper-client";
|
|
21
|
+
import pako from "pako";
|
|
22
|
+
import { API_CONFIG } from "./config";
|
|
23
|
+
const GameContext = createContext(null);
|
|
24
|
+
const options = {
|
|
25
|
+
secure: true,
|
|
26
|
+
authType: "ws",
|
|
27
|
+
};
|
|
28
|
+
export const GameProvider = ({ access_token, api_url = "https://game-api.wspo.club/v1/external/games", socketToken, environment = "development", children, }) => {
|
|
29
|
+
const [activeGames, setActiveGames] = useState([]);
|
|
30
|
+
const [subscribedGameIds, setSubscribedGameIds] = useState([]);
|
|
31
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
32
|
+
const socketRef = useRef(null);
|
|
33
|
+
const channelsRef = useRef(new Map());
|
|
34
|
+
// Initialize SocketCluster connection
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
console.log("[GameProvider] useEffect triggered");
|
|
37
|
+
console.log("[GameProvider] socketToken:", socketToken ? "✓ provided" : "✗ MISSING");
|
|
38
|
+
console.log("[GameProvider] environment:", environment);
|
|
39
|
+
if (!socketToken) {
|
|
40
|
+
console.warn("[GameProvider] ⚠️ No socketToken provided - WebSocket connection will NOT be established");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const connectSocket = async () => {
|
|
44
|
+
try {
|
|
45
|
+
const wsHost = API_CONFIG[environment].websocket_url;
|
|
46
|
+
if (!socketRef.current) {
|
|
47
|
+
console.log("[GameProvider] Creating new SocketClient instance");
|
|
48
|
+
socketRef.current = new SocketClient(wsHost, socketToken, options);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log("[GameProvider] Reusing existing SocketClient instance");
|
|
52
|
+
}
|
|
53
|
+
// Connect
|
|
54
|
+
const clientSocket = await socketRef.current.connect({
|
|
55
|
+
autoConnect: true,
|
|
56
|
+
autoReconnect: true,
|
|
57
|
+
});
|
|
58
|
+
setIsConnected(true);
|
|
59
|
+
socketRef.current.clientSocket = clientSocket;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error("[GameProvider] ✗ Connection FAILED:", error);
|
|
63
|
+
console.log("[GameProvider] Connected:", false);
|
|
64
|
+
setIsConnected(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
connectSocket();
|
|
68
|
+
return () => {
|
|
69
|
+
// Cleanup channels
|
|
70
|
+
channelsRef.current.forEach((channel) => {
|
|
71
|
+
if (channel) {
|
|
72
|
+
channel.unsubscribe();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
channelsRef.current.clear();
|
|
76
|
+
// Close socket
|
|
77
|
+
if (socketRef.current) {
|
|
78
|
+
socketRef.current = null;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}, [socketToken, environment]);
|
|
82
|
+
// Update game data
|
|
83
|
+
const updateGame = useCallback((game) => {
|
|
84
|
+
setActiveGames((prev) => {
|
|
85
|
+
const existingIndex = prev.findIndex((g) => g.id === game.id);
|
|
86
|
+
if (existingIndex >= 0) {
|
|
87
|
+
const updated = [...prev];
|
|
88
|
+
updated[existingIndex] = game;
|
|
89
|
+
return updated;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
return [...prev, game];
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}, []);
|
|
96
|
+
// Remove game (when ended)
|
|
97
|
+
const removeGame = useCallback((gameId) => {
|
|
98
|
+
setActiveGames((prev) => prev.filter((g) => g.id !== gameId));
|
|
99
|
+
}, []);
|
|
100
|
+
// Update game score only
|
|
101
|
+
const updateGameScore = useCallback((gameId, scores) => {
|
|
102
|
+
setActiveGames((prev) => {
|
|
103
|
+
return prev.map((game) => {
|
|
104
|
+
if (game.id === gameId) {
|
|
105
|
+
return Object.assign(Object.assign({}, game), { teams: game.teams.map((team, index) => (Object.assign(Object.assign({}, team), { current_score: scores[index] || team.current_score }))) });
|
|
106
|
+
}
|
|
107
|
+
return game;
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}, []);
|
|
111
|
+
const subscribeToGame = useCallback(async (gameId) => {
|
|
112
|
+
var _a, _b, _c;
|
|
113
|
+
console.log("[GameProvider] 📡 subscribeToGame called");
|
|
114
|
+
console.log("[GameProvider] Game ID:", gameId);
|
|
115
|
+
console.log("[GameProvider] Socket connected:", !!((_a = socketRef.current) === null || _a === void 0 ? void 0 : _a.clientSocket));
|
|
116
|
+
setSubscribedGameIds((prev) => {
|
|
117
|
+
if (prev.includes(gameId))
|
|
118
|
+
return prev;
|
|
119
|
+
return [...prev, gameId];
|
|
120
|
+
});
|
|
121
|
+
if (!((_b = socketRef.current) === null || _b === void 0 ? void 0 : _b.clientSocket)) {
|
|
122
|
+
console.error("[GameProvider] ⚠️ Cannot subscribe - Socket not connected yet!");
|
|
123
|
+
console.log("[GameProvider] Current socket state:", socketRef.current);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const clientSocket = socketRef.current.clientSocket;
|
|
128
|
+
const prefix = ((_c = clientSocket.authToken) === null || _c === void 0 ? void 0 : _c.prefix) || "game";
|
|
129
|
+
console.log("[GameProvider] Using channel prefix:", prefix);
|
|
130
|
+
const channels = [
|
|
131
|
+
`${prefix}:game:${gameId}`, // Main game channel
|
|
132
|
+
`${prefix}:game:${gameId}:action`, // Action channel (status changes)
|
|
133
|
+
`${prefix}:game:${gameId}:rounds`, // Round events channel
|
|
134
|
+
];
|
|
135
|
+
console.log("[GameProvider] Channels to subscribe:", channels);
|
|
136
|
+
for (const channelName of channels) {
|
|
137
|
+
if (channelsRef.current.has(channelName)) {
|
|
138
|
+
console.log("[GameProvider] ⏭️ Already subscribed to:", channelName);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
console.log("[GameProvider] 🔔 Subscribing to channel:", channelName);
|
|
142
|
+
// Subscribe to channel
|
|
143
|
+
const channel = clientSocket.subscribe(channelName);
|
|
144
|
+
channelsRef.current.set(channelName, channel);
|
|
145
|
+
console.log("[GameProvider] ✓ Subscribed successfully to:", channelName);
|
|
146
|
+
// Listen for messages
|
|
147
|
+
(async () => {
|
|
148
|
+
var _a, e_1, _b, _c;
|
|
149
|
+
var _d, _e, _f, _g, _h, _j, _k, _l;
|
|
150
|
+
console.log("[GameProvider] 👂 Listening for messages on:", channelName);
|
|
151
|
+
try {
|
|
152
|
+
for (var _m = true, channel_1 = __asyncValues(channel), channel_1_1; channel_1_1 = await channel_1.next(), _a = channel_1_1.done, !_a; _m = true) {
|
|
153
|
+
_c = channel_1_1.value;
|
|
154
|
+
_m = false;
|
|
155
|
+
const data = _c;
|
|
156
|
+
try {
|
|
157
|
+
console.log("[GameProvider] 📨 Received raw data on:", channelName);
|
|
158
|
+
console.log("[GameProvider] Data length:", data === null || data === void 0 ? void 0 : data.length);
|
|
159
|
+
// Decompress data (browser-compatible base64 decoding)
|
|
160
|
+
const binaryString = atob(data);
|
|
161
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
162
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
163
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
164
|
+
}
|
|
165
|
+
const decompressed = pako.inflate(bytes, { to: "string" });
|
|
166
|
+
const message = JSON.parse(decompressed);
|
|
167
|
+
console.log("[GameProvider] 📦 Decompressed message:", message);
|
|
168
|
+
// Check if message is a round object directly (has round_number field)
|
|
169
|
+
if (message.round_number && message.game_id) {
|
|
170
|
+
// Direct round object from rounds channel
|
|
171
|
+
const roundData = message;
|
|
172
|
+
const action = "direct_update"; // Direct round updates don't have action
|
|
173
|
+
console.log("[GameProvider] 🎯 Round update - Round:", roundData.round_number);
|
|
174
|
+
console.log("[GameProvider] 🎯 Action:", action);
|
|
175
|
+
console.log("[GameProvider] 🎯 Scores - Red:", roundData.red_team_score, "Blue:", roundData.blue_team_score);
|
|
176
|
+
// Update game with round data - preserve existing rounds array
|
|
177
|
+
setActiveGames((prev) => prev.map((g) => {
|
|
178
|
+
if (g.id === gameId) {
|
|
179
|
+
const rounds = g.rounds || [];
|
|
180
|
+
// Update or add the round
|
|
181
|
+
const roundExists = rounds.some((r) => r.id === roundData.id);
|
|
182
|
+
const updatedRounds = roundExists
|
|
183
|
+
? rounds.map((r) => (r.id === roundData.id ? Object.assign(Object.assign({}, roundData), { _wsAction: action }) : r))
|
|
184
|
+
: [...rounds, Object.assign(Object.assign({}, roundData), { _wsAction: action })];
|
|
185
|
+
// If round has game data with updated teams, merge it (but preserve rounds!)
|
|
186
|
+
if (roundData.game) {
|
|
187
|
+
console.log("[GameProvider] ✨ Merging game data from round (preserving rounds array)");
|
|
188
|
+
const _a = roundData.game, { rounds: _ } = _a, gameDataWithoutRounds = __rest(_a, ["rounds"]);
|
|
189
|
+
return Object.assign(Object.assign(Object.assign({}, g), gameDataWithoutRounds), { rounds: updatedRounds });
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log("[GameProvider] ⚠️ No game data in round, only updating rounds array");
|
|
193
|
+
return Object.assign(Object.assign({}, g), { rounds: updatedRounds });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return g;
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
// Handle wrapped message format
|
|
200
|
+
else if (message.type === "game_update" || ((_d = message.data) === null || _d === void 0 ? void 0 : _d.game)) {
|
|
201
|
+
// Handle both direct game_update and action channel format
|
|
202
|
+
const gameData = message.game || ((_e = message.data) === null || _e === void 0 ? void 0 : _e.game);
|
|
203
|
+
if (gameData) {
|
|
204
|
+
updateGame(gameData);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (message.type === "score_update") {
|
|
208
|
+
updateGameScore(gameId, message.scores);
|
|
209
|
+
}
|
|
210
|
+
else if (message.type === "status_update" ||
|
|
211
|
+
((_f = message.data) === null || _f === void 0 ? void 0 : _f.action)) {
|
|
212
|
+
// Handle status update from both channels
|
|
213
|
+
const newStatus = message.status || ((_h = (_g = message.data) === null || _g === void 0 ? void 0 : _g.game) === null || _h === void 0 ? void 0 : _h.status);
|
|
214
|
+
if (newStatus) {
|
|
215
|
+
setActiveGames((prev) => prev.map((g) => g.id === gameId
|
|
216
|
+
? Object.assign(Object.assign({}, g), { status: newStatus }) : g));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else if (message.type === "game_ended" ||
|
|
220
|
+
((_j = message.data) === null || _j === void 0 ? void 0 : _j.action) === "finish" ||
|
|
221
|
+
((_k = message.data) === null || _k === void 0 ? void 0 : _k.action) === "cancel") {
|
|
222
|
+
removeGame(gameId);
|
|
223
|
+
}
|
|
224
|
+
else if ((_l = message.data) === null || _l === void 0 ? void 0 : _l.round) {
|
|
225
|
+
// Handle round events: created, status_changed, scores_updated, winner_set, finished
|
|
226
|
+
const roundData = message.data.round;
|
|
227
|
+
const action = message.data.action;
|
|
228
|
+
console.log("[GameProvider] 🎯 Round event:", action, "Round:", roundData.round_number);
|
|
229
|
+
console.log("[GameProvider] 🎯 Scores - Red:", roundData.red_team_score, "Blue:", roundData.blue_team_score);
|
|
230
|
+
// Update game with round data - preserve existing rounds array
|
|
231
|
+
setActiveGames((prev) => prev.map((g) => {
|
|
232
|
+
if (g.id === gameId) {
|
|
233
|
+
const rounds = g.rounds || [];
|
|
234
|
+
// Update or add the round WITH the action type
|
|
235
|
+
let updatedRounds;
|
|
236
|
+
if (action === "created") {
|
|
237
|
+
updatedRounds = [...rounds, Object.assign(Object.assign({}, roundData), { _wsAction: action })];
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
updatedRounds = rounds.map((r) => r.id === roundData.id ? Object.assign(Object.assign({}, roundData), { _wsAction: action }) : r);
|
|
241
|
+
}
|
|
242
|
+
// If round has game data with updated teams, merge it (but preserve rounds!)
|
|
243
|
+
if (roundData.game) {
|
|
244
|
+
console.log("[GameProvider] ✨ Merging game data from round (preserving rounds array)");
|
|
245
|
+
const _a = roundData.game, { rounds: _ } = _a, gameDataWithoutRounds = __rest(_a, ["rounds"]);
|
|
246
|
+
return Object.assign(Object.assign(Object.assign({}, g), gameDataWithoutRounds), { rounds: updatedRounds });
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
console.log("[GameProvider] ⚠️ No game data in round, only updating rounds array");
|
|
250
|
+
return Object.assign(Object.assign({}, g), { rounds: updatedRounds });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return g;
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
console.error("[GameProvider] Error processing message:", error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
263
|
+
finally {
|
|
264
|
+
try {
|
|
265
|
+
if (!_m && !_a && (_b = channel_1.return)) await _b.call(channel_1);
|
|
266
|
+
}
|
|
267
|
+
finally { if (e_1) throw e_1.error; }
|
|
268
|
+
}
|
|
269
|
+
})();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
console.error("[GameProvider] Error in subscribeToGame:", error);
|
|
274
|
+
}
|
|
275
|
+
}, [updateGame, updateGameScore, removeGame]);
|
|
276
|
+
// Unsubscribe from a game channel
|
|
277
|
+
const unsubscribeFromGame = useCallback((gameId) => {
|
|
278
|
+
var _a, _b;
|
|
279
|
+
console.log("[GameProvider] Unsubscribing from game:", gameId);
|
|
280
|
+
setSubscribedGameIds((prev) => prev.filter((id) => id !== gameId));
|
|
281
|
+
if (!((_a = socketRef.current) === null || _a === void 0 ? void 0 : _a.clientSocket))
|
|
282
|
+
return;
|
|
283
|
+
try {
|
|
284
|
+
const clientSocket = socketRef.current.clientSocket;
|
|
285
|
+
const prefix = ((_b = clientSocket.authToken) === null || _b === void 0 ? void 0 : _b.prefix) || "game";
|
|
286
|
+
// Unsubscribe from all channels
|
|
287
|
+
const channels = [
|
|
288
|
+
`${prefix}:game:${gameId}`,
|
|
289
|
+
`${prefix}:game:${gameId}:action`,
|
|
290
|
+
`${prefix}:game:${gameId}:rounds`,
|
|
291
|
+
];
|
|
292
|
+
for (const channelName of channels) {
|
|
293
|
+
const channel = channelsRef.current.get(channelName);
|
|
294
|
+
if (channel) {
|
|
295
|
+
channel.unsubscribe();
|
|
296
|
+
channelsRef.current.delete(channelName);
|
|
297
|
+
console.log("[GameProvider] Unsubscribed from:", channelName);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
console.error("[GameProvider] Error unsubscribing:", error);
|
|
303
|
+
}
|
|
304
|
+
}, []);
|
|
305
|
+
// Get a specific game
|
|
306
|
+
const getGame = useCallback((gameId) => {
|
|
307
|
+
return activeGames.find((game) => game.id === gameId);
|
|
308
|
+
}, [activeGames]);
|
|
309
|
+
return (React.createElement(GameContext.Provider, { value: {
|
|
310
|
+
access_token,
|
|
311
|
+
api_url,
|
|
312
|
+
subscribeToGame,
|
|
313
|
+
unsubscribeFromGame,
|
|
314
|
+
getGame,
|
|
315
|
+
updateGame,
|
|
316
|
+
isConnected,
|
|
317
|
+
subscribedGameIds,
|
|
318
|
+
activeGames,
|
|
319
|
+
} }, children));
|
|
320
|
+
};
|
|
321
|
+
export const useGameContext = () => {
|
|
322
|
+
const context = useContext(GameContext);
|
|
323
|
+
if (!context) {
|
|
324
|
+
throw new Error("useGameContext must be used within a GameProvider");
|
|
325
|
+
}
|
|
326
|
+
return context;
|
|
327
|
+
};
|