@smoregg/sdk 0.4.0 → 0.5.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/dist/cjs/SmoreHost.cjs +306 -0
- package/dist/cjs/SmoreHost.cjs.map +1 -0
- package/dist/cjs/SmorePlayer.cjs +229 -0
- package/dist/cjs/SmorePlayer.cjs.map +1 -0
- package/dist/cjs/components/IframeGameBridge.cjs +115 -0
- package/dist/cjs/components/IframeGameBridge.cjs.map +1 -0
- package/dist/cjs/context/RoomProvider.cjs +3 -3
- package/dist/cjs/context/RoomProvider.cjs.map +1 -1
- package/dist/cjs/hooks/useGameHost.cjs +91 -14
- package/dist/cjs/hooks/useGameHost.cjs.map +1 -1
- package/dist/cjs/hooks/useGamePlayer.cjs +65 -6
- package/dist/cjs/hooks/useGamePlayer.cjs.map +1 -1
- package/dist/cjs/iframe/index.cjs +58 -315
- package/dist/cjs/iframe/index.cjs.map +1 -1
- package/dist/cjs/index.cjs +4 -22
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/transport/protocol.cjs.map +1 -1
- package/dist/cjs/utils/connectionMonitor.cjs +77 -0
- package/dist/cjs/utils/connectionMonitor.cjs.map +1 -0
- package/dist/cjs/utils/preloadAssets.cjs +66 -0
- package/dist/cjs/utils/preloadAssets.cjs.map +1 -0
- package/dist/cjs/utils/serverTime.cjs +43 -0
- package/dist/cjs/utils/serverTime.cjs.map +1 -0
- package/dist/esm/SmoreHost.js +304 -0
- package/dist/esm/SmoreHost.js.map +1 -0
- package/dist/esm/SmorePlayer.js +227 -0
- package/dist/esm/SmorePlayer.js.map +1 -0
- package/dist/esm/components/IframeGameBridge.js +113 -0
- package/dist/esm/components/IframeGameBridge.js.map +1 -0
- package/dist/esm/context/RoomProvider.js +3 -3
- package/dist/esm/context/RoomProvider.js.map +1 -1
- package/dist/esm/hooks/useGameHost.js +92 -15
- package/dist/esm/hooks/useGameHost.js.map +1 -1
- package/dist/esm/hooks/useGamePlayer.js +66 -7
- package/dist/esm/hooks/useGamePlayer.js.map +1 -1
- package/dist/esm/iframe/index.js +59 -313
- package/dist/esm/iframe/index.js.map +1 -1
- package/dist/esm/index.js +2 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/transport/protocol.js.map +1 -1
- package/dist/esm/utils/connectionMonitor.js +75 -0
- package/dist/esm/utils/connectionMonitor.js.map +1 -0
- package/dist/esm/utils/preloadAssets.js +63 -0
- package/dist/esm/utils/preloadAssets.js.map +1 -0
- package/dist/esm/utils/serverTime.js +41 -0
- package/dist/esm/utils/serverTime.js.map +1 -0
- package/dist/types/SmoreHost.d.ts +187 -0
- package/dist/types/SmoreHost.d.ts.map +1 -0
- package/dist/types/SmorePlayer.d.ts +146 -0
- package/dist/types/SmorePlayer.d.ts.map +1 -0
- package/dist/types/components/IframeGameBridge.d.ts +2 -2
- package/dist/types/components/IframeGameBridge.d.ts.map +1 -1
- package/dist/types/components/index.d.ts +2 -4
- package/dist/types/components/index.d.ts.map +1 -1
- package/dist/types/context/RoomProvider.d.ts +3 -3
- package/dist/types/context/RoomProvider.d.ts.map +1 -1
- package/dist/types/hooks/useGameHost.d.ts +33 -7
- package/dist/types/hooks/useGameHost.d.ts.map +1 -1
- package/dist/types/hooks/useGamePlayer.d.ts +29 -3
- package/dist/types/hooks/useGamePlayer.d.ts.map +1 -1
- package/dist/types/iframe/index.d.ts +10 -10
- package/dist/types/iframe/index.d.ts.map +1 -1
- package/dist/types/iframe/vanilla.d.ts +12 -4
- package/dist/types/iframe/vanilla.d.ts.map +1 -1
- package/dist/types/index.d.ts +36 -20
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/transport/protocol.d.ts +1 -1
- package/dist/types/transport/protocol.d.ts.map +1 -1
- package/dist/types/utils/connectionMonitor.d.ts +57 -0
- package/dist/types/utils/connectionMonitor.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +7 -0
- package/dist/types/utils/index.d.ts.map +1 -0
- package/dist/types/utils/preloadAssets.d.ts +29 -0
- package/dist/types/utils/preloadAssets.d.ts.map +1 -0
- package/dist/types/utils/serverTime.d.ts +28 -0
- package/dist/types/utils/serverTime.d.ts.map +1 -0
- package/dist/umd/smore-sdk-iframe.umd.js +62 -316
- package/dist/umd/smore-sdk-iframe.umd.js.map +1 -1
- package/dist/umd/smore-sdk-iframe.umd.min.js +1 -1
- package/dist/umd/smore-sdk-iframe.umd.min.js.map +1 -1
- package/dist/umd/smore-sdk-vanilla.umd.js +553 -127
- package/dist/umd/smore-sdk-vanilla.umd.js.map +1 -1
- package/dist/umd/smore-sdk-vanilla.umd.min.js +1 -1
- package/dist/umd/smore-sdk-vanilla.umd.min.js.map +1 -1
- package/dist/umd/smore-sdk.umd.js +496 -577
- package/dist/umd/smore-sdk.umd.js.map +1 -1
- package/dist/umd/smore-sdk.umd.min.js +1 -1
- package/dist/umd/smore-sdk.umd.min.js.map +1 -1
- package/package.json +1 -26
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var DirectTransport = require('./transport/DirectTransport.cjs');
|
|
4
|
+
var PostMessageTransport = require('./transport/PostMessageTransport.cjs');
|
|
5
|
+
var protocol = require('./transport/protocol.cjs');
|
|
6
|
+
|
|
7
|
+
const SYSTEM_PREFIX = "smore:";
|
|
8
|
+
const SYSTEM_EVENTS = {
|
|
9
|
+
PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,
|
|
10
|
+
PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`,
|
|
11
|
+
GAME_OVER: `${SYSTEM_PREFIX}game-over`
|
|
12
|
+
};
|
|
13
|
+
const EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;
|
|
14
|
+
function validateEventName(event) {
|
|
15
|
+
if (!EVENT_NAME_REGEX.test(event)) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`[SmoreHost] Invalid event name "${event}". Event names must:
|
|
18
|
+
- Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)
|
|
19
|
+
- Start and end with a letter (no leading/trailing - or _)`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class SmoreHost {
|
|
24
|
+
transport = null;
|
|
25
|
+
config;
|
|
26
|
+
_players = [];
|
|
27
|
+
_roomCode = "";
|
|
28
|
+
_leaderIndex = -1;
|
|
29
|
+
_isReady = false;
|
|
30
|
+
_isDestroyed = false;
|
|
31
|
+
boundMessageHandler = null;
|
|
32
|
+
registeredHandlers = [];
|
|
33
|
+
constructor(config = {}) {
|
|
34
|
+
this.config = config;
|
|
35
|
+
if (config.listeners) {
|
|
36
|
+
for (const event of Object.keys(config.listeners)) {
|
|
37
|
+
validateEventName(event);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (config.socket) {
|
|
41
|
+
this.initBundled(config);
|
|
42
|
+
} else {
|
|
43
|
+
this.initIframe(config);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Initialization
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
initBundled(config) {
|
|
50
|
+
if (!config.socket) {
|
|
51
|
+
throw new Error("[SmoreHost] socket is required for bundled games");
|
|
52
|
+
}
|
|
53
|
+
this.transport = new DirectTransport.DirectTransport(config.socket);
|
|
54
|
+
this._roomCode = config.roomCode || "";
|
|
55
|
+
this._players = config.players || [];
|
|
56
|
+
this._leaderIndex = config.leaderIndex ?? -1;
|
|
57
|
+
this.setupEventHandlers();
|
|
58
|
+
this._isReady = true;
|
|
59
|
+
this.config.onReady?.();
|
|
60
|
+
}
|
|
61
|
+
initIframe(config) {
|
|
62
|
+
const parentOrigin = config.parentOrigin || "*";
|
|
63
|
+
window.parent.postMessage({ type: "smore:ready" }, parentOrigin);
|
|
64
|
+
this.boundMessageHandler = (e) => {
|
|
65
|
+
if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
|
|
66
|
+
const msg = e.data;
|
|
67
|
+
if (!protocol.isSmoreMessage(msg)) return;
|
|
68
|
+
if (msg.type === "smore:init") {
|
|
69
|
+
const initData = msg.payload;
|
|
70
|
+
if (initData.side !== "host") {
|
|
71
|
+
console.error("[SmoreHost] Received init for wrong side:", initData.side);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.transport = new PostMessageTransport.PostMessageTransport(parentOrigin);
|
|
75
|
+
this._roomCode = initData.roomCode;
|
|
76
|
+
this._players = this.mapPlayersFromInit(initData.players);
|
|
77
|
+
this._leaderIndex = this.findLeaderIndex(initData.players, initData.leaderId);
|
|
78
|
+
this.setupEventHandlers();
|
|
79
|
+
this._isReady = true;
|
|
80
|
+
this.config.onReady?.();
|
|
81
|
+
} else if (msg.type === "smore:update") {
|
|
82
|
+
const updateData = msg.payload;
|
|
83
|
+
if (updateData.players) {
|
|
84
|
+
this._players = this.mapPlayersFromInit(updateData.players);
|
|
85
|
+
}
|
|
86
|
+
if (updateData.leaderId !== void 0) {
|
|
87
|
+
this._leaderIndex = this.findLeaderIndex(this._players, updateData.leaderId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
window.addEventListener("message", this.boundMessageHandler);
|
|
92
|
+
}
|
|
93
|
+
mapPlayersFromInit(players) {
|
|
94
|
+
return players.map((p, index) => ({
|
|
95
|
+
playerIndex: p.playerIndex ?? index,
|
|
96
|
+
nickname: p.nickname || `Player ${index + 1}`,
|
|
97
|
+
connected: p.connected !== false,
|
|
98
|
+
appearance: p.appearance
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
findLeaderIndex(players, leaderId) {
|
|
102
|
+
if (!leaderId) return -1;
|
|
103
|
+
const idx = players.findIndex((p) => p.sessionId === leaderId);
|
|
104
|
+
return idx >= 0 ? idx : -1;
|
|
105
|
+
}
|
|
106
|
+
setupEventHandlers() {
|
|
107
|
+
if (!this.transport) return;
|
|
108
|
+
this.registerHandler(SYSTEM_EVENTS.PLAYER_JOIN, (data) => {
|
|
109
|
+
const playerIndex = data.player?.playerIndex;
|
|
110
|
+
if (playerIndex !== void 0) {
|
|
111
|
+
this.config.onPlayerJoin?.(playerIndex);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
this.registerHandler(SYSTEM_EVENTS.PLAYER_LEAVE, (data) => {
|
|
115
|
+
if (data.playerIndex !== void 0) {
|
|
116
|
+
this.config.onPlayerLeave?.(data.playerIndex);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
this.registerHandler("room:player-joined", (data) => {
|
|
120
|
+
const playerIndex = data?.player?.playerIndex ?? data?.playerIndex;
|
|
121
|
+
if (playerIndex !== void 0) {
|
|
122
|
+
this.config.onPlayerJoin?.(playerIndex);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
this.registerHandler("room:player-left", (data) => {
|
|
126
|
+
const playerIndex = data?.playerIndex ?? data?.player?.playerIndex;
|
|
127
|
+
if (playerIndex !== void 0) {
|
|
128
|
+
this.config.onPlayerLeave?.(playerIndex);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
if (this.config.listeners) {
|
|
132
|
+
for (const [event, handler] of Object.entries(this.config.listeners)) {
|
|
133
|
+
if (!handler) continue;
|
|
134
|
+
this.registerHandler(event, (data) => {
|
|
135
|
+
const { playerIndex, ...rest } = data;
|
|
136
|
+
if (playerIndex !== void 0) {
|
|
137
|
+
handler(playerIndex, rest);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
registerHandler(event, handler) {
|
|
144
|
+
if (!this.transport) return;
|
|
145
|
+
this.transport.on(event, handler);
|
|
146
|
+
this.registeredHandlers.push({ event, handler });
|
|
147
|
+
}
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Public Properties
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
/**
|
|
152
|
+
* Get all players in the room.
|
|
153
|
+
* Returns a copy to prevent external mutation.
|
|
154
|
+
*/
|
|
155
|
+
get players() {
|
|
156
|
+
return [...this._players];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Get the room code.
|
|
160
|
+
*/
|
|
161
|
+
get roomCode() {
|
|
162
|
+
return this._roomCode;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get the leader's player index (-1 if no leader).
|
|
166
|
+
*/
|
|
167
|
+
get leaderIndex() {
|
|
168
|
+
return this._leaderIndex;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Check if the host is initialized and ready.
|
|
172
|
+
*/
|
|
173
|
+
get isReady() {
|
|
174
|
+
return this._isReady;
|
|
175
|
+
}
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Public Methods
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
/**
|
|
180
|
+
* Broadcast an event to all players.
|
|
181
|
+
*
|
|
182
|
+
* @param event - Event name (no colons allowed)
|
|
183
|
+
* @param data - Optional data payload
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* host.broadcast('phase-update', { phase: 'playing' });
|
|
188
|
+
* host.broadcast('timer-tick', { remaining: 30 });
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
broadcast(event, data) {
|
|
192
|
+
this.ensureReady("broadcast");
|
|
193
|
+
validateEventName(event);
|
|
194
|
+
this.transport.emit(event, data);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Send an event to a specific player.
|
|
198
|
+
*
|
|
199
|
+
* @param playerIndex - Target player index (0, 1, 2, ...)
|
|
200
|
+
* @param event - Event name (no colons allowed)
|
|
201
|
+
* @param data - Optional data payload
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```ts
|
|
205
|
+
* host.sendToPlayer(0, 'your-turn', { timeLimit: 30 });
|
|
206
|
+
* host.sendToPlayer(1, 'wait', { message: 'Not your turn' });
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
sendToPlayer(playerIndex, event, data) {
|
|
210
|
+
this.ensureReady("sendToPlayer");
|
|
211
|
+
validateEventName(event);
|
|
212
|
+
this.transport.emit(event, {
|
|
213
|
+
targetPlayerIndex: playerIndex,
|
|
214
|
+
...data && typeof data === "object" ? data : { data }
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Signal game over with results.
|
|
219
|
+
* This will broadcast the game over event to all players.
|
|
220
|
+
*
|
|
221
|
+
* @param results - Game results (scores, winner, etc.)
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* host.gameOver({
|
|
226
|
+
* scores: { 0: 100, 1: 75, 2: 50 },
|
|
227
|
+
* winner: 0,
|
|
228
|
+
* });
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
gameOver(results) {
|
|
232
|
+
this.ensureReady("gameOver");
|
|
233
|
+
this.transport.emit(SYSTEM_EVENTS.GAME_OVER, { results });
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Add a listener for a specific event after construction.
|
|
237
|
+
*
|
|
238
|
+
* @param event - Event name (no colons allowed)
|
|
239
|
+
* @param handler - Handler function (playerIndex, data) => void
|
|
240
|
+
* @returns Cleanup function to remove the listener
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```ts
|
|
244
|
+
* const cleanup = host.on('tap', (playerIndex, data) => {
|
|
245
|
+
* console.log(`Player ${playerIndex} tapped`);
|
|
246
|
+
* });
|
|
247
|
+
*
|
|
248
|
+
* // Later
|
|
249
|
+
* cleanup();
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
on(event, handler) {
|
|
253
|
+
validateEventName(event);
|
|
254
|
+
const wrappedHandler = (data) => {
|
|
255
|
+
const { playerIndex, ...rest } = data;
|
|
256
|
+
if (playerIndex !== void 0) {
|
|
257
|
+
handler(playerIndex, rest);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
if (this.transport) {
|
|
261
|
+
this.transport.on(event, wrappedHandler);
|
|
262
|
+
this.registeredHandlers.push({ event, handler: wrappedHandler });
|
|
263
|
+
}
|
|
264
|
+
return () => {
|
|
265
|
+
this.transport?.off(event, wrappedHandler);
|
|
266
|
+
this.registeredHandlers = this.registeredHandlers.filter(
|
|
267
|
+
(h) => h.event !== event || h.handler !== wrappedHandler
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Clean up all resources.
|
|
273
|
+
* Call this when unmounting/destroying the game.
|
|
274
|
+
*/
|
|
275
|
+
destroy() {
|
|
276
|
+
if (this._isDestroyed) return;
|
|
277
|
+
this._isDestroyed = true;
|
|
278
|
+
this._isReady = false;
|
|
279
|
+
for (const { event, handler } of this.registeredHandlers) {
|
|
280
|
+
this.transport?.off(event, handler);
|
|
281
|
+
}
|
|
282
|
+
this.registeredHandlers = [];
|
|
283
|
+
if (this.transport instanceof PostMessageTransport.PostMessageTransport) {
|
|
284
|
+
this.transport.destroy();
|
|
285
|
+
}
|
|
286
|
+
this.transport = null;
|
|
287
|
+
if (this.boundMessageHandler) {
|
|
288
|
+
window.removeEventListener("message", this.boundMessageHandler);
|
|
289
|
+
this.boundMessageHandler = null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Private Helpers
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
ensureReady(method) {
|
|
296
|
+
if (!this._isReady || !this.transport) {
|
|
297
|
+
throw new Error(`[SmoreHost] Cannot call ${method}() before host is ready. Wait for onReady callback.`);
|
|
298
|
+
}
|
|
299
|
+
if (this._isDestroyed) {
|
|
300
|
+
throw new Error(`[SmoreHost] Cannot call ${method}() after destroy()`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
exports.SmoreHost = SmoreHost;
|
|
306
|
+
//# sourceMappingURL=SmoreHost.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SmoreHost.cjs","sources":["../../src/SmoreHost.ts"],"sourcesContent":["/**\n * SmoreHost - Unified Host-side class for the S'MORE SDK (AirConsole style)\n *\n * Works in any environment: React, Phaser, Vanilla JS.\n * Automatically detects iframe vs bundled environment.\n *\n * @example Iframe game (auto-detection)\n * ```ts\n * const host = new SmoreHost({\n * onPlayerJoin: (playerIndex) => console.log('Player joined:', playerIndex),\n * listeners: {\n * tap: (playerIndex, data) => handleTap(playerIndex, data),\n * },\n * });\n *\n * // Later\n * host.broadcast('phase-update', { phase: 'playing' });\n * host.gameOver({ scores: { 0: 100, 1: 50 } });\n * ```\n *\n * @example Bundled game (direct socket)\n * ```ts\n * const host = new SmoreHost({\n * socket,\n * roomCode: 'ABCD',\n * players: [...],\n * leaderIndex: 0,\n * listeners: { ... },\n * });\n * ```\n */\n\nimport type { Socket } from 'socket.io-client';\nimport type { Transport, TransportEventHandler } from './transport/types';\nimport { DirectTransport } from './transport/DirectTransport';\nimport { PostMessageTransport } from './transport/PostMessageTransport';\nimport { isSmoreMessage, type SmoreInitMessage, type SmoreUpdateMessage } from './transport/protocol';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst SYSTEM_PREFIX = 'smore:';\n\nconst SYSTEM_EVENTS = {\n READY: `${SYSTEM_PREFIX}ready`,\n PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,\n PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`,\n GAME_OVER: `${SYSTEM_PREFIX}game-over`,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nconst EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;\n\nfunction validateEventName(event: string): void {\n if (!EVENT_NAME_REGEX.test(event)) {\n throw new Error(\n `[SmoreHost] Invalid event name \"${event}\". Event names must:\\n` +\n ` - Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)\\n` +\n ` - Start and end with a letter (no leading/trailing - or _)`\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Player information exposed to game developers.\n * Uses playerIndex (0, 1, 2, ...) instead of internal sessionId.\n */\nexport interface SmorePlayer {\n /** Player index (0, 1, 2, ...) */\n playerIndex: number;\n /** Player's chosen nickname */\n nickname: string;\n /** Whether player is currently connected */\n connected: boolean;\n /** Player's character appearance (optional) */\n appearance?: {\n skinColor?: string;\n hairColor?: string;\n shirtColor?: string;\n pantsColor?: string;\n };\n}\n\n/**\n * Configuration for SmoreHost constructor.\n */\nexport interface SmoreHostConfig {\n // === Callbacks ===\n\n /** Called when the host is initialized and ready (iframe games only) */\n onReady?: () => void;\n\n /** Called when a player joins the room */\n onPlayerJoin?: (playerIndex: number) => void;\n\n /** Called when a player leaves the room */\n onPlayerLeave?: (playerIndex: number) => void;\n\n /**\n * Event listeners for specific events.\n * Keys are event names (no colons), values are handler functions.\n * Handler receives (playerIndex, data).\n */\n listeners?: Record<string, (playerIndex: number, data: any) => void>;\n\n // === Bundled game options (skip iframe detection) ===\n\n /** Socket.IO socket instance (bundled games only) */\n socket?: Socket;\n\n /** Room code (bundled games only) */\n roomCode?: string;\n\n /** Initial players array (bundled games only) */\n players?: SmorePlayer[];\n\n /** Leader player index (bundled games only) */\n leaderIndex?: number;\n\n // === Iframe game options ===\n\n /** Parent window origin for postMessage validation (iframe games) */\n parentOrigin?: string;\n}\n\n// ---------------------------------------------------------------------------\n// SmoreHost Class\n// ---------------------------------------------------------------------------\n\n/**\n * SmoreHost - Main host-side class for game development.\n *\n * Automatically detects iframe vs bundled environment:\n * - Iframe: Uses PostMessageTransport, waits for smore:init from parent\n * - Bundled: Uses DirectTransport with provided socket\n */\nexport class SmoreHost {\n private transport: Transport | null = null;\n private config: SmoreHostConfig;\n private _players: SmorePlayer[] = [];\n private _roomCode: string = '';\n private _leaderIndex: number = -1;\n private _isReady: boolean = false;\n private _isDestroyed: boolean = false;\n private boundMessageHandler: ((e: MessageEvent) => void) | null = null;\n private registeredHandlers: Array<{ event: string; handler: TransportEventHandler }> = [];\n\n constructor(config: SmoreHostConfig = {}) {\n this.config = config;\n\n // Validate event names in listeners\n if (config.listeners) {\n for (const event of Object.keys(config.listeners)) {\n validateEventName(event);\n }\n }\n\n // Detect environment and initialize\n if (config.socket) {\n // Bundled game mode: use DirectTransport\n this.initBundled(config);\n } else {\n // Iframe game mode: use PostMessageTransport\n this.initIframe(config);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Initialization\n // ---------------------------------------------------------------------------\n\n private initBundled(config: SmoreHostConfig): void {\n if (!config.socket) {\n throw new Error('[SmoreHost] socket is required for bundled games');\n }\n\n this.transport = new DirectTransport(config.socket);\n this._roomCode = config.roomCode || '';\n this._players = config.players || [];\n this._leaderIndex = config.leaderIndex ?? -1;\n\n this.setupEventHandlers();\n\n // Mark as ready immediately for bundled games\n this._isReady = true;\n this.config.onReady?.();\n }\n\n private initIframe(config: SmoreHostConfig): void {\n const parentOrigin = config.parentOrigin || '*';\n\n // Signal ready to parent\n window.parent.postMessage({ type: 'smore:ready' }, parentOrigin);\n\n // Listen for init message from parent\n this.boundMessageHandler = (e: MessageEvent) => {\n if (parentOrigin !== '*' && e.origin !== parentOrigin) return;\n\n const msg = e.data;\n if (!isSmoreMessage(msg)) return;\n\n if (msg.type === 'smore:init') {\n const initData = (msg as SmoreInitMessage).payload;\n\n if (initData.side !== 'host') {\n console.error('[SmoreHost] Received init for wrong side:', initData.side);\n return;\n }\n\n // Initialize transport\n this.transport = new PostMessageTransport(parentOrigin);\n this._roomCode = initData.roomCode;\n this._players = this.mapPlayersFromInit(initData.players);\n this._leaderIndex = this.findLeaderIndex(initData.players, initData.leaderId);\n\n this.setupEventHandlers();\n\n this._isReady = true;\n this.config.onReady?.();\n } else if (msg.type === 'smore:update') {\n const updateData = (msg as SmoreUpdateMessage).payload;\n\n if (updateData.players) {\n this._players = this.mapPlayersFromInit(updateData.players);\n }\n if (updateData.leaderId !== undefined) {\n this._leaderIndex = this.findLeaderIndex(this._players as any[], updateData.leaderId);\n }\n }\n };\n\n window.addEventListener('message', this.boundMessageHandler);\n }\n\n private mapPlayersFromInit(players: any[]): SmorePlayer[] {\n return players.map((p, index) => ({\n playerIndex: p.playerIndex ?? index,\n nickname: p.nickname || `Player ${index + 1}`,\n connected: p.connected !== false,\n appearance: p.appearance,\n }));\n }\n\n private findLeaderIndex(players: any[], leaderId: string | null): number {\n if (!leaderId) return -1;\n const idx = players.findIndex((p) => p.sessionId === leaderId);\n return idx >= 0 ? idx : -1;\n }\n\n private setupEventHandlers(): void {\n if (!this.transport) return;\n\n // System events: player join/leave\n this.registerHandler(SYSTEM_EVENTS.PLAYER_JOIN, (data: { player: SmorePlayer }) => {\n const playerIndex = data.player?.playerIndex;\n if (playerIndex !== undefined) {\n this.config.onPlayerJoin?.(playerIndex);\n }\n });\n\n this.registerHandler(SYSTEM_EVENTS.PLAYER_LEAVE, (data: { playerIndex: number }) => {\n if (data.playerIndex !== undefined) {\n this.config.onPlayerLeave?.(data.playerIndex);\n }\n });\n\n // Legacy room events (backward compatibility)\n this.registerHandler('room:player-joined', (data: any) => {\n const playerIndex = data?.player?.playerIndex ?? data?.playerIndex;\n if (playerIndex !== undefined) {\n this.config.onPlayerJoin?.(playerIndex);\n }\n });\n\n this.registerHandler('room:player-left', (data: any) => {\n const playerIndex = data?.playerIndex ?? data?.player?.playerIndex;\n if (playerIndex !== undefined) {\n this.config.onPlayerLeave?.(playerIndex);\n }\n });\n\n // User event listeners\n if (this.config.listeners) {\n for (const [event, handler] of Object.entries(this.config.listeners)) {\n if (!handler) continue;\n\n this.registerHandler(event, (data: { playerIndex: number; [key: string]: any }) => {\n const { playerIndex, ...rest } = data;\n if (playerIndex !== undefined) {\n handler(playerIndex, rest);\n }\n });\n }\n }\n }\n\n private registerHandler(event: string, handler: TransportEventHandler): void {\n if (!this.transport) return;\n this.transport.on(event, handler);\n this.registeredHandlers.push({ event, handler });\n }\n\n // ---------------------------------------------------------------------------\n // Public Properties\n // ---------------------------------------------------------------------------\n\n /**\n * Get all players in the room.\n * Returns a copy to prevent external mutation.\n */\n get players(): SmorePlayer[] {\n return [...this._players];\n }\n\n /**\n * Get the room code.\n */\n get roomCode(): string {\n return this._roomCode;\n }\n\n /**\n * Get the leader's player index (-1 if no leader).\n */\n get leaderIndex(): number {\n return this._leaderIndex;\n }\n\n /**\n * Check if the host is initialized and ready.\n */\n get isReady(): boolean {\n return this._isReady;\n }\n\n // ---------------------------------------------------------------------------\n // Public Methods\n // ---------------------------------------------------------------------------\n\n /**\n * Broadcast an event to all players.\n *\n * @param event - Event name (no colons allowed)\n * @param data - Optional data payload\n *\n * @example\n * ```ts\n * host.broadcast('phase-update', { phase: 'playing' });\n * host.broadcast('timer-tick', { remaining: 30 });\n * ```\n */\n broadcast(event: string, data?: any): void {\n this.ensureReady('broadcast');\n validateEventName(event);\n // Emit user event directly - genericRelay handles Host → Room broadcast\n this.transport!.emit(event, data);\n }\n\n /**\n * Send an event to a specific player.\n *\n * @param playerIndex - Target player index (0, 1, 2, ...)\n * @param event - Event name (no colons allowed)\n * @param data - Optional data payload\n *\n * @example\n * ```ts\n * host.sendToPlayer(0, 'your-turn', { timeLimit: 30 });\n * host.sendToPlayer(1, 'wait', { message: 'Not your turn' });\n * ```\n */\n sendToPlayer(playerIndex: number, event: string, data?: any): void {\n this.ensureReady('sendToPlayer');\n validateEventName(event);\n // Emit user event with targetPlayerIndex - genericRelay handles Host → Player\n this.transport!.emit(event, {\n targetPlayerIndex: playerIndex,\n ...(data && typeof data === 'object' ? data : { data }),\n });\n }\n\n /**\n * Signal game over with results.\n * This will broadcast the game over event to all players.\n *\n * @param results - Game results (scores, winner, etc.)\n *\n * @example\n * ```ts\n * host.gameOver({\n * scores: { 0: 100, 1: 75, 2: 50 },\n * winner: 0,\n * });\n * ```\n */\n gameOver(results?: any): void {\n this.ensureReady('gameOver');\n this.transport!.emit(SYSTEM_EVENTS.GAME_OVER, { results });\n }\n\n /**\n * Add a listener for a specific event after construction.\n *\n * @param event - Event name (no colons allowed)\n * @param handler - Handler function (playerIndex, data) => void\n * @returns Cleanup function to remove the listener\n *\n * @example\n * ```ts\n * const cleanup = host.on('tap', (playerIndex, data) => {\n * console.log(`Player ${playerIndex} tapped`);\n * });\n *\n * // Later\n * cleanup();\n * ```\n */\n on(event: string, handler: (playerIndex: number, data: any) => void): () => void {\n validateEventName(event);\n\n const wrappedHandler = (data: { playerIndex: number; [key: string]: any }) => {\n const { playerIndex, ...rest } = data;\n if (playerIndex !== undefined) {\n handler(playerIndex, rest);\n }\n };\n\n if (this.transport) {\n this.transport.on(event, wrappedHandler);\n this.registeredHandlers.push({ event, handler: wrappedHandler });\n }\n\n return () => {\n this.transport?.off(event, wrappedHandler);\n this.registeredHandlers = this.registeredHandlers.filter(\n (h) => h.event !== event || h.handler !== wrappedHandler\n );\n };\n }\n\n /**\n * Clean up all resources.\n * Call this when unmounting/destroying the game.\n */\n destroy(): void {\n if (this._isDestroyed) return;\n\n this._isDestroyed = true;\n this._isReady = false;\n\n // Remove all registered handlers\n for (const { event, handler } of this.registeredHandlers) {\n this.transport?.off(event, handler);\n }\n this.registeredHandlers = [];\n\n // Destroy transport\n if (this.transport instanceof PostMessageTransport) {\n (this.transport as PostMessageTransport).destroy();\n }\n this.transport = null;\n\n // Remove message listener\n if (this.boundMessageHandler) {\n window.removeEventListener('message', this.boundMessageHandler);\n this.boundMessageHandler = null;\n }\n }\n\n // ---------------------------------------------------------------------------\n // Private Helpers\n // ---------------------------------------------------------------------------\n\n private ensureReady(method: string): void {\n if (!this._isReady || !this.transport) {\n throw new Error(`[SmoreHost] Cannot call ${method}() before host is ready. Wait for onReady callback.`);\n }\n if (this._isDestroyed) {\n throw new Error(`[SmoreHost] Cannot call ${method}() after destroy()`);\n }\n }\n}\n"],"names":["DirectTransport","isSmoreMessage","PostMessageTransport"],"mappings":"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"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var DirectTransport = require('./transport/DirectTransport.cjs');
|
|
4
|
+
var PostMessageTransport = require('./transport/PostMessageTransport.cjs');
|
|
5
|
+
var protocol = require('./transport/protocol.cjs');
|
|
6
|
+
|
|
7
|
+
const SYSTEM_PREFIX = "smore:";
|
|
8
|
+
const SYSTEM_EVENTS = {
|
|
9
|
+
PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,
|
|
10
|
+
PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`
|
|
11
|
+
};
|
|
12
|
+
const EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;
|
|
13
|
+
function validateEventName(event) {
|
|
14
|
+
if (!EVENT_NAME_REGEX.test(event)) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`[SmorePlayer] Invalid event name "${event}". Event names must:
|
|
17
|
+
- Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)
|
|
18
|
+
- Start and end with a letter (no leading/trailing - or _)`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
class SmorePlayer {
|
|
23
|
+
transport = null;
|
|
24
|
+
config;
|
|
25
|
+
_roomCode = "";
|
|
26
|
+
_myIndex = -1;
|
|
27
|
+
_isLeader = false;
|
|
28
|
+
_isReady = false;
|
|
29
|
+
_isDestroyed = false;
|
|
30
|
+
boundMessageHandler = null;
|
|
31
|
+
registeredHandlers = [];
|
|
32
|
+
constructor(config = {}) {
|
|
33
|
+
this.config = config;
|
|
34
|
+
if (config.listeners) {
|
|
35
|
+
for (const event of Object.keys(config.listeners)) {
|
|
36
|
+
validateEventName(event);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (config.socket) {
|
|
40
|
+
this.initBundled(config);
|
|
41
|
+
} else {
|
|
42
|
+
this.initIframe(config);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Initialization
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
initBundled(config) {
|
|
49
|
+
if (!config.socket) {
|
|
50
|
+
throw new Error("[SmorePlayer] socket is required for bundled games");
|
|
51
|
+
}
|
|
52
|
+
this.transport = new DirectTransport.DirectTransport(config.socket);
|
|
53
|
+
this._roomCode = config.roomCode || "";
|
|
54
|
+
this._myIndex = config.myIndex ?? -1;
|
|
55
|
+
this._isLeader = config.isLeader ?? false;
|
|
56
|
+
this.setupEventHandlers();
|
|
57
|
+
this._isReady = true;
|
|
58
|
+
this.config.onReady?.();
|
|
59
|
+
}
|
|
60
|
+
initIframe(config) {
|
|
61
|
+
const parentOrigin = config.parentOrigin || "*";
|
|
62
|
+
window.parent.postMessage({ type: "smore:ready" }, parentOrigin);
|
|
63
|
+
this.boundMessageHandler = (e) => {
|
|
64
|
+
if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
|
|
65
|
+
const msg = e.data;
|
|
66
|
+
if (!protocol.isSmoreMessage(msg)) return;
|
|
67
|
+
if (msg.type === "smore:init") {
|
|
68
|
+
const initData = msg.payload;
|
|
69
|
+
if (initData.side !== "player") {
|
|
70
|
+
console.error("[SmorePlayer] Received init for wrong side:", initData.side);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (initData.myIndex === void 0) {
|
|
74
|
+
console.error("[SmorePlayer] Missing myIndex in init payload");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.transport = new PostMessageTransport.PostMessageTransport(parentOrigin);
|
|
78
|
+
this._roomCode = initData.roomCode;
|
|
79
|
+
this._myIndex = initData.myIndex;
|
|
80
|
+
this._isLeader = initData.isLeader ?? false;
|
|
81
|
+
this.setupEventHandlers();
|
|
82
|
+
this._isReady = true;
|
|
83
|
+
this.config.onReady?.();
|
|
84
|
+
} else if (msg.type === "smore:update") {
|
|
85
|
+
const updateData = msg.payload;
|
|
86
|
+
if (updateData.leaderId !== void 0) ;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
window.addEventListener("message", this.boundMessageHandler);
|
|
90
|
+
}
|
|
91
|
+
setupEventHandlers() {
|
|
92
|
+
if (!this.transport) return;
|
|
93
|
+
this.registerHandler(SYSTEM_EVENTS.PLAYER_JOIN, (data) => {
|
|
94
|
+
const playerIndex = data.player?.playerIndex ?? data.playerIndex;
|
|
95
|
+
if (playerIndex !== void 0) {
|
|
96
|
+
this.config.onPlayerJoin?.(playerIndex);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
this.registerHandler(SYSTEM_EVENTS.PLAYER_LEAVE, (data) => {
|
|
100
|
+
const playerIndex = data.player?.playerIndex ?? data.playerIndex;
|
|
101
|
+
if (playerIndex !== void 0) {
|
|
102
|
+
this.config.onPlayerLeave?.(playerIndex);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
if (this.config.listeners) {
|
|
106
|
+
for (const [event, handler] of Object.entries(this.config.listeners)) {
|
|
107
|
+
if (!handler) continue;
|
|
108
|
+
this.registerHandler(event, handler);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
registerHandler(event, handler) {
|
|
113
|
+
if (!this.transport) return;
|
|
114
|
+
this.transport.on(event, handler);
|
|
115
|
+
this.registeredHandlers.push({ event, handler });
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Public Properties
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
/**
|
|
121
|
+
* Get my player index (0, 1, 2, ...).
|
|
122
|
+
*/
|
|
123
|
+
get myIndex() {
|
|
124
|
+
return this._myIndex;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check if I am the room leader.
|
|
128
|
+
*/
|
|
129
|
+
get isLeader() {
|
|
130
|
+
return this._isLeader;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get the room code.
|
|
134
|
+
*/
|
|
135
|
+
get roomCode() {
|
|
136
|
+
return this._roomCode;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Check if the player is initialized and ready.
|
|
140
|
+
*/
|
|
141
|
+
get isReady() {
|
|
142
|
+
return this._isReady;
|
|
143
|
+
}
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Public Methods
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
/**
|
|
148
|
+
* Send an event to the host.
|
|
149
|
+
*
|
|
150
|
+
* @param event - Event name (no colons allowed)
|
|
151
|
+
* @param data - Optional data payload
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* player.send('tap', { timestamp: Date.now() });
|
|
156
|
+
* player.send('answer', { choice: 2 });
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
send(event, data) {
|
|
160
|
+
this.ensureReady("send");
|
|
161
|
+
validateEventName(event);
|
|
162
|
+
this.transport.emit(event, data);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Add a listener for a specific event after construction.
|
|
166
|
+
*
|
|
167
|
+
* @param event - Event name (no colons allowed)
|
|
168
|
+
* @param handler - Handler function (data) => void
|
|
169
|
+
* @returns Cleanup function to remove the listener
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* const cleanup = player.on('phase-update', (data) => {
|
|
174
|
+
* console.log('New phase:', data.phase);
|
|
175
|
+
* });
|
|
176
|
+
*
|
|
177
|
+
* // Later
|
|
178
|
+
* cleanup();
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
on(event, handler) {
|
|
182
|
+
validateEventName(event);
|
|
183
|
+
if (this.transport) {
|
|
184
|
+
this.transport.on(event, handler);
|
|
185
|
+
this.registeredHandlers.push({ event, handler });
|
|
186
|
+
}
|
|
187
|
+
return () => {
|
|
188
|
+
this.transport?.off(event, handler);
|
|
189
|
+
this.registeredHandlers = this.registeredHandlers.filter(
|
|
190
|
+
(h) => h.event !== event || h.handler !== handler
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Clean up all resources.
|
|
196
|
+
* Call this when unmounting/destroying the game.
|
|
197
|
+
*/
|
|
198
|
+
destroy() {
|
|
199
|
+
if (this._isDestroyed) return;
|
|
200
|
+
this._isDestroyed = true;
|
|
201
|
+
this._isReady = false;
|
|
202
|
+
for (const { event, handler } of this.registeredHandlers) {
|
|
203
|
+
this.transport?.off(event, handler);
|
|
204
|
+
}
|
|
205
|
+
this.registeredHandlers = [];
|
|
206
|
+
if (this.transport instanceof PostMessageTransport.PostMessageTransport) {
|
|
207
|
+
this.transport.destroy();
|
|
208
|
+
}
|
|
209
|
+
this.transport = null;
|
|
210
|
+
if (this.boundMessageHandler) {
|
|
211
|
+
window.removeEventListener("message", this.boundMessageHandler);
|
|
212
|
+
this.boundMessageHandler = null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Private Helpers
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
ensureReady(method) {
|
|
219
|
+
if (!this._isReady || !this.transport) {
|
|
220
|
+
throw new Error(`[SmorePlayer] Cannot call ${method}() before player is ready. Wait for onReady callback.`);
|
|
221
|
+
}
|
|
222
|
+
if (this._isDestroyed) {
|
|
223
|
+
throw new Error(`[SmorePlayer] Cannot call ${method}() after destroy()`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
exports.SmorePlayer = SmorePlayer;
|
|
229
|
+
//# sourceMappingURL=SmorePlayer.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SmorePlayer.cjs","sources":["../../src/SmorePlayer.ts"],"sourcesContent":["/**\n * SmorePlayer - Unified Player-side class for the S'MORE SDK (AirConsole style)\n *\n * Works in any environment: React, Phaser, Vanilla JS.\n * Automatically detects iframe vs bundled environment.\n *\n * @example Iframe game (auto-detection)\n * ```ts\n * const player = new SmorePlayer({\n * onReady: () => console.log('Ready! My index:', player.myIndex),\n * listeners: {\n * 'phase-update': (data) => handlePhaseUpdate(data),\n * },\n * });\n *\n * // Later\n * player.send('tap', { timestamp: Date.now() });\n * ```\n *\n * @example Bundled game (direct socket)\n * ```ts\n * const player = new SmorePlayer({\n * socket,\n * roomCode: 'ABCD',\n * myIndex: 0,\n * isLeader: true,\n * listeners: { ... },\n * });\n * ```\n */\n\nimport type { Socket } from 'socket.io-client';\nimport type { Transport, TransportEventHandler } from './transport/types';\nimport { DirectTransport } from './transport/DirectTransport';\nimport { PostMessageTransport } from './transport/PostMessageTransport';\nimport { isSmoreMessage, type SmoreInitMessage, type SmoreUpdateMessage } from './transport/protocol';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst SYSTEM_PREFIX = 'smore:';\n\nconst SYSTEM_EVENTS = {\n READY: `${SYSTEM_PREFIX}ready`,\n PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,\n PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nconst EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;\n\nfunction validateEventName(event: string): void {\n if (!EVENT_NAME_REGEX.test(event)) {\n throw new Error(\n `[SmorePlayer] Invalid event name \"${event}\". Event names must:\\n` +\n ` - Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)\\n` +\n ` - Start and end with a letter (no leading/trailing - or _)`\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Player information.\n */\nexport interface SmorePlayerInfo {\n /** Player index (0, 1, 2, ...) */\n playerIndex: number;\n /** Player's chosen nickname */\n nickname: string;\n /** Whether player is currently connected */\n connected: boolean;\n}\n\n/**\n * Configuration for SmorePlayer constructor.\n */\nexport interface SmorePlayerConfig {\n // === Callbacks ===\n\n /** Called when the player is ready and initialized (iframe games only) */\n onReady?: () => void;\n\n /** Called when another player joins the room */\n onPlayerJoin?: (playerIndex: number) => void;\n\n /** Called when another player leaves the room */\n onPlayerLeave?: (playerIndex: number) => void;\n\n /**\n * Event listeners for specific events.\n * Keys are event names (no colons), values are handler functions.\n * Handler receives (data) only - player side doesn't need playerIndex.\n */\n listeners?: Record<string, (data: any) => void>;\n\n // === Bundled game options (skip iframe detection) ===\n\n /** Socket.IO socket instance (bundled games only) */\n socket?: Socket;\n\n /** Room code (bundled games only) */\n roomCode?: string;\n\n /** My player index (bundled games only) */\n myIndex?: number;\n\n /** Am I the leader? (bundled games only) */\n isLeader?: boolean;\n\n // === Iframe game options ===\n\n /** Parent window origin for postMessage validation (iframe games) */\n parentOrigin?: string;\n}\n\n// ---------------------------------------------------------------------------\n// SmorePlayer Class\n// ---------------------------------------------------------------------------\n\n/**\n * SmorePlayer - Main player-side class for game development.\n *\n * Automatically detects iframe vs bundled environment:\n * - Iframe: Uses PostMessageTransport, waits for smore:init from parent\n * - Bundled: Uses DirectTransport with provided socket\n */\nexport class SmorePlayer {\n private transport: Transport | null = null;\n private config: SmorePlayerConfig;\n private _roomCode: string = '';\n private _myIndex: number = -1;\n private _isLeader: boolean = false;\n private _isReady: boolean = false;\n private _isDestroyed: boolean = false;\n private boundMessageHandler: ((e: MessageEvent) => void) | null = null;\n private registeredHandlers: Array<{ event: string; handler: TransportEventHandler }> = [];\n\n constructor(config: SmorePlayerConfig = {}) {\n this.config = config;\n\n // Validate event names in listeners\n if (config.listeners) {\n for (const event of Object.keys(config.listeners)) {\n validateEventName(event);\n }\n }\n\n // Detect environment and initialize\n if (config.socket) {\n // Bundled game mode: use DirectTransport\n this.initBundled(config);\n } else {\n // Iframe game mode: use PostMessageTransport\n this.initIframe(config);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Initialization\n // ---------------------------------------------------------------------------\n\n private initBundled(config: SmorePlayerConfig): void {\n if (!config.socket) {\n throw new Error('[SmorePlayer] socket is required for bundled games');\n }\n\n this.transport = new DirectTransport(config.socket);\n this._roomCode = config.roomCode || '';\n this._myIndex = config.myIndex ?? -1;\n this._isLeader = config.isLeader ?? false;\n\n this.setupEventHandlers();\n\n // Mark as ready immediately for bundled games\n this._isReady = true;\n this.config.onReady?.();\n }\n\n private initIframe(config: SmorePlayerConfig): void {\n const parentOrigin = config.parentOrigin || '*';\n\n // Signal ready to parent\n window.parent.postMessage({ type: 'smore:ready' }, parentOrigin);\n\n // Listen for init message from parent\n this.boundMessageHandler = (e: MessageEvent) => {\n if (parentOrigin !== '*' && e.origin !== parentOrigin) return;\n\n const msg = e.data;\n if (!isSmoreMessage(msg)) return;\n\n if (msg.type === 'smore:init') {\n const initData = (msg as SmoreInitMessage).payload;\n\n if (initData.side !== 'player') {\n console.error('[SmorePlayer] Received init for wrong side:', initData.side);\n return;\n }\n\n if (initData.myIndex === undefined) {\n console.error('[SmorePlayer] Missing myIndex in init payload');\n return;\n }\n\n // Initialize transport\n this.transport = new PostMessageTransport(parentOrigin);\n this._roomCode = initData.roomCode;\n this._myIndex = initData.myIndex;\n this._isLeader = initData.isLeader ?? false;\n\n this.setupEventHandlers();\n\n this._isReady = true;\n this.config.onReady?.();\n } else if (msg.type === 'smore:update') {\n const updateData = (msg as SmoreUpdateMessage).payload;\n\n // Update leader status if changed\n if (updateData.leaderId !== undefined) {\n // Note: Without players array, we can't determine isLeader change\n // This would require the parent to send myIndex in update\n }\n }\n };\n\n window.addEventListener('message', this.boundMessageHandler);\n }\n\n private setupEventHandlers(): void {\n if (!this.transport) return;\n\n // System events: player join/leave\n this.registerHandler(SYSTEM_EVENTS.PLAYER_JOIN, (data: { player?: SmorePlayerInfo; playerIndex?: number }) => {\n const playerIndex = data.player?.playerIndex ?? data.playerIndex;\n if (playerIndex !== undefined) {\n this.config.onPlayerJoin?.(playerIndex);\n }\n });\n\n this.registerHandler(SYSTEM_EVENTS.PLAYER_LEAVE, (data: { player?: { playerIndex?: number }; playerIndex?: number }) => {\n const playerIndex = data.player?.playerIndex ?? data.playerIndex;\n if (playerIndex !== undefined) {\n this.config.onPlayerLeave?.(playerIndex);\n }\n });\n\n // User event listeners\n if (this.config.listeners) {\n for (const [event, handler] of Object.entries(this.config.listeners)) {\n if (!handler) continue;\n\n // Player side receives data directly (no playerIndex unwrapping)\n this.registerHandler(event, handler);\n }\n }\n }\n\n private registerHandler(event: string, handler: TransportEventHandler): void {\n if (!this.transport) return;\n this.transport.on(event, handler);\n this.registeredHandlers.push({ event, handler });\n }\n\n // ---------------------------------------------------------------------------\n // Public Properties\n // ---------------------------------------------------------------------------\n\n /**\n * Get my player index (0, 1, 2, ...).\n */\n get myIndex(): number {\n return this._myIndex;\n }\n\n /**\n * Check if I am the room leader.\n */\n get isLeader(): boolean {\n return this._isLeader;\n }\n\n /**\n * Get the room code.\n */\n get roomCode(): string {\n return this._roomCode;\n }\n\n /**\n * Check if the player is initialized and ready.\n */\n get isReady(): boolean {\n return this._isReady;\n }\n\n // ---------------------------------------------------------------------------\n // Public Methods\n // ---------------------------------------------------------------------------\n\n /**\n * Send an event to the host.\n *\n * @param event - Event name (no colons allowed)\n * @param data - Optional data payload\n *\n * @example\n * ```ts\n * player.send('tap', { timestamp: Date.now() });\n * player.send('answer', { choice: 2 });\n * ```\n */\n send(event: string, data?: any): void {\n this.ensureReady('send');\n validateEventName(event);\n this.transport!.emit(event, data);\n }\n\n /**\n * Add a listener for a specific event after construction.\n *\n * @param event - Event name (no colons allowed)\n * @param handler - Handler function (data) => void\n * @returns Cleanup function to remove the listener\n *\n * @example\n * ```ts\n * const cleanup = player.on('phase-update', (data) => {\n * console.log('New phase:', data.phase);\n * });\n *\n * // Later\n * cleanup();\n * ```\n */\n on(event: string, handler: (data: any) => void): () => void {\n validateEventName(event);\n\n if (this.transport) {\n this.transport.on(event, handler);\n this.registeredHandlers.push({ event, handler });\n }\n\n return () => {\n this.transport?.off(event, handler);\n this.registeredHandlers = this.registeredHandlers.filter(\n (h) => h.event !== event || h.handler !== handler\n );\n };\n }\n\n /**\n * Clean up all resources.\n * Call this when unmounting/destroying the game.\n */\n destroy(): void {\n if (this._isDestroyed) return;\n\n this._isDestroyed = true;\n this._isReady = false;\n\n // Remove all registered handlers\n for (const { event, handler } of this.registeredHandlers) {\n this.transport?.off(event, handler);\n }\n this.registeredHandlers = [];\n\n // Destroy transport\n if (this.transport instanceof PostMessageTransport) {\n (this.transport as PostMessageTransport).destroy();\n }\n this.transport = null;\n\n // Remove message listener\n if (this.boundMessageHandler) {\n window.removeEventListener('message', this.boundMessageHandler);\n this.boundMessageHandler = null;\n }\n }\n\n // ---------------------------------------------------------------------------\n // Private Helpers\n // ---------------------------------------------------------------------------\n\n private ensureReady(method: string): void {\n if (!this._isReady || !this.transport) {\n throw new Error(`[SmorePlayer] Cannot call ${method}() before player is ready. Wait for onReady callback.`);\n }\n if (this._isDestroyed) {\n throw new Error(`[SmorePlayer] Cannot call ${method}() after destroy()`);\n }\n }\n}\n"],"names":["DirectTransport","isSmoreMessage","PostMessageTransport"],"mappings":"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"}
|