homegames-common 1.4.2 → 1.5.1
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/docker-helper.js +398 -0
- package/game-loader.js +224 -0
- package/game-session-manager.js +694 -0
- package/game-session.js +600 -0
- package/index.js +96 -624
- package/package.json +5 -1
package/game-session.js
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GameSession — Unified game session for all Homegames contexts.
|
|
3
|
+
*
|
|
4
|
+
* Replaces both the old GameSession (homegames-core) and MiniGameSession
|
|
5
|
+
* (homegames-common). One class, configurable via an options bag.
|
|
6
|
+
*
|
|
7
|
+
* Usage (minimal / no-frame):
|
|
8
|
+
* const session = new GameSession(game, squishVersion);
|
|
9
|
+
*
|
|
10
|
+
* Usage (full platform with bezel, spectators, homenames):
|
|
11
|
+
* const session = new GameSession(game, squishVersion, {
|
|
12
|
+
* frame: { root, topLayerRoot, assets, bezelX, bezelY, isDashboard, ... },
|
|
13
|
+
* homenames: homenamesHelper,
|
|
14
|
+
* spectators: true,
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* Consumers:
|
|
18
|
+
* - homegames-core/child_game_server.js (no-frame OR full-frame, depending on mode)
|
|
19
|
+
* - homegames-common/game-session-manager.js
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const WebSocket = require('ws');
|
|
23
|
+
const { squishMap, DEFAULT_SQUISH_VERSION } = require('./game-loader');
|
|
24
|
+
|
|
25
|
+
// Simple name generator for anonymous players
|
|
26
|
+
const _NAME_WORDS = [
|
|
27
|
+
'chocolate', 'iguana', 'cardigan', 'enormous', 'gargantuan', 'orangutan',
|
|
28
|
+
'cookies', 'monstera', 'daisy', 'grapefruit', 'blueberry', 'mango',
|
|
29
|
+
'elephant', 'bamboo', 'sapphire', 'papaya', 'waffle', 'turquoise',
|
|
30
|
+
'aquatic', 'goblin', 'funky', 'hotdog', 'elegant', 'cascade', 'euphoria',
|
|
31
|
+
];
|
|
32
|
+
const _generateName = () => {
|
|
33
|
+
const pick = () => _NAME_WORDS[Math.floor(Math.random() * _NAME_WORDS.length)];
|
|
34
|
+
return pick() + ' ' + pick();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
class GameSession {
|
|
38
|
+
constructor(game, squishVersion, opts = {}) {
|
|
39
|
+
const squishPkg = process.env.SQUISH_PATH
|
|
40
|
+
|| squishMap[squishVersion]
|
|
41
|
+
|| squishMap[DEFAULT_SQUISH_VERSION];
|
|
42
|
+
if (!squishPkg) {
|
|
43
|
+
throw new Error(`No squish package found for version "${squishVersion}"`);
|
|
44
|
+
}
|
|
45
|
+
const { Squisher } = require(squishPkg);
|
|
46
|
+
|
|
47
|
+
this.game = game;
|
|
48
|
+
this.squishVersion = squishVersion;
|
|
49
|
+
this.port = opts.port || null;
|
|
50
|
+
this.username = opts.username || null;
|
|
51
|
+
|
|
52
|
+
// Frame / bezel setup
|
|
53
|
+
this.frameEnabled = !!opts.frame;
|
|
54
|
+
this.frame = opts.frame || null;
|
|
55
|
+
|
|
56
|
+
const bezelX = this.frameEnabled ? (this.frame.bezelX ?? 10) : 0;
|
|
57
|
+
const bezelY = this.frameEnabled ? (this.frame.bezelY ?? 10) : 0;
|
|
58
|
+
this.bezelX = bezelX;
|
|
59
|
+
this.bezelY = bezelY;
|
|
60
|
+
this.scale = this.frameEnabled
|
|
61
|
+
? { x: (100 - bezelX) / 100, y: (100 - bezelY) / 100 }
|
|
62
|
+
: { x: 1, y: 1 };
|
|
63
|
+
|
|
64
|
+
// Squisher setup
|
|
65
|
+
const squisherOpts = {
|
|
66
|
+
game,
|
|
67
|
+
scale: this.scale,
|
|
68
|
+
onAssetUpdate: (newAssetBundle) => {
|
|
69
|
+
for (const pid in this.players) {
|
|
70
|
+
this._send(this.players[pid], newAssetBundle);
|
|
71
|
+
}
|
|
72
|
+
for (const sid in this.spectators) {
|
|
73
|
+
this._send(this.spectators[sid], newAssetBundle);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (this.frameEnabled) {
|
|
79
|
+
squisherOpts.customBottomLayer = {
|
|
80
|
+
root: this.frame.root,
|
|
81
|
+
scale: { x: 1, y: 1 },
|
|
82
|
+
assets: this.frame.assets || {},
|
|
83
|
+
};
|
|
84
|
+
squisherOpts.customTopLayer = {
|
|
85
|
+
root: this.frame.topLayerRoot,
|
|
86
|
+
scale: { x: 1, y: 1 },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.squisher = new Squisher(squisherOpts);
|
|
91
|
+
this.squisher.addListener(() => this._broadcastState());
|
|
92
|
+
|
|
93
|
+
this.gameMetadata = (typeof game.constructor.metadata === 'function') ? game.constructor.metadata() : {};
|
|
94
|
+
this.aspectRatio = this.gameMetadata.aspectRatio || { x: 16, y: 9 };
|
|
95
|
+
|
|
96
|
+
// Player / spectator maps — values are raw WebSocket objects
|
|
97
|
+
this.players = {};
|
|
98
|
+
this.spectators = {};
|
|
99
|
+
this.playerInfoMap = {};
|
|
100
|
+
this.clientInfoMap = {};
|
|
101
|
+
this.playerSettingsMap = {};
|
|
102
|
+
this.remotePlayerMap = {};
|
|
103
|
+
this.stateHistory = [];
|
|
104
|
+
|
|
105
|
+
// Optional subsystems
|
|
106
|
+
this.spectatorsEnabled = !!opts.spectators;
|
|
107
|
+
this.homenames = opts.homenames || null;
|
|
108
|
+
|
|
109
|
+
// Frame handler (HomegamesRoot instance)
|
|
110
|
+
this.frameHandler = (this.frame && this.frame.handler) || null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// -----------------------------------------------------------------------
|
|
114
|
+
// Initialization
|
|
115
|
+
// -----------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
initialize() {
|
|
118
|
+
if (this._initialized) return Promise.resolve();
|
|
119
|
+
if (this.squisher.initialize) {
|
|
120
|
+
return this.squisher.initialize().then(() => { this._initialized = true; });
|
|
121
|
+
}
|
|
122
|
+
this._initialized = true;
|
|
123
|
+
return Promise.resolve();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// -----------------------------------------------------------------------
|
|
127
|
+
// Player management
|
|
128
|
+
// -----------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
addPlayer(playerId, ws, playerOpts = {}) {
|
|
131
|
+
// If this ID is already connected, disconnect the old one first
|
|
132
|
+
if (this.players[playerId]) {
|
|
133
|
+
this.removePlayer(playerId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.players[playerId] = ws;
|
|
137
|
+
this.playerInfoMap[playerId] = playerOpts.info || {};
|
|
138
|
+
this.clientInfoMap[playerId] = playerOpts.clientInfo || {};
|
|
139
|
+
this.playerSettingsMap[playerId] = playerOpts.settings || { SOUND: true };
|
|
140
|
+
if (playerOpts.isRemote) this.remotePlayerMap[playerId] = true;
|
|
141
|
+
|
|
142
|
+
if (this.squisher.assetBundle) {
|
|
143
|
+
this._send(ws, this.squisher.assetBundle);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (this.homenames) {
|
|
147
|
+
const notifyPlayer = (extraInfo) => {
|
|
148
|
+
if (!this.players[playerId]) return; // disconnected during async
|
|
149
|
+
|
|
150
|
+
if (extraInfo) {
|
|
151
|
+
if (extraInfo.playerInfo) this.playerInfoMap[playerId] = extraInfo.playerInfo;
|
|
152
|
+
if (extraInfo.playerSettings) this.playerSettingsMap[playerId] = extraInfo.playerSettings;
|
|
153
|
+
if (extraInfo.clientInfo) this.clientInfoMap[playerId] = extraInfo.clientInfo;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const playerPayload = {
|
|
157
|
+
playerId,
|
|
158
|
+
settings: this.playerSettingsMap[playerId],
|
|
159
|
+
info: this.playerInfoMap[playerId],
|
|
160
|
+
clientInfo: this.clientInfoMap[playerId],
|
|
161
|
+
requestedGame: playerOpts.requestedGame || null,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
try { this.homenames.addListener(playerId); } catch (e) {
|
|
165
|
+
console.error('[GameSession] homenames.addListener failed:', e.message);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
if (this.frameHandler && this.frameHandler.handleNewPlayer) {
|
|
170
|
+
this.frameHandler.handleNewPlayer(playerPayload);
|
|
171
|
+
}
|
|
172
|
+
} catch (e) {
|
|
173
|
+
console.error('[GameSession] frameHandler.handleNewPlayer threw:', e);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
this.game.handleNewPlayer && this.game.handleNewPlayer(playerPayload);
|
|
178
|
+
} catch (e) {
|
|
179
|
+
console.error('[GameSession] game.handleNewPlayer threw:', e);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this._sendPlayerFrame(playerId, ws);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const finishAdd = () => {
|
|
186
|
+
Promise.all([
|
|
187
|
+
this.homenames.getPlayerInfo(playerId).catch(() => null),
|
|
188
|
+
this.homenames.getPlayerSettings(playerId).catch(() => null),
|
|
189
|
+
this.homenames.getClientInfo(playerId).catch(() => null),
|
|
190
|
+
]).then(([playerInfo, playerSettings, clientInfo]) => {
|
|
191
|
+
notifyPlayer({
|
|
192
|
+
playerInfo: playerInfo || this.playerInfoMap[playerId],
|
|
193
|
+
playerSettings: playerSettings || this.playerSettingsMap[playerId],
|
|
194
|
+
clientInfo: clientInfo || this.clientInfoMap[playerId],
|
|
195
|
+
});
|
|
196
|
+
}).catch((err) => {
|
|
197
|
+
console.error('[GameSession] Homenames fetch failed:', err);
|
|
198
|
+
notifyPlayer();
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const info = playerOpts.info || {};
|
|
203
|
+
if (info.name) {
|
|
204
|
+
finishAdd();
|
|
205
|
+
} else {
|
|
206
|
+
const playerName = _generateName();
|
|
207
|
+
this.homenames.updatePlayerInfo(playerId, { playerName })
|
|
208
|
+
.then(() => this.homenames.updateClientInfo(playerId, playerOpts.clientInfo || {}))
|
|
209
|
+
.then(() => finishAdd())
|
|
210
|
+
.catch(() => finishAdd());
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
const playerPayload = {
|
|
214
|
+
playerId,
|
|
215
|
+
settings: this.playerSettingsMap[playerId],
|
|
216
|
+
info: this.playerInfoMap[playerId],
|
|
217
|
+
clientInfo: this.clientInfoMap[playerId],
|
|
218
|
+
requestedGame: playerOpts.requestedGame || null,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
if (this.frameHandler && this.frameHandler.handleNewPlayer) {
|
|
223
|
+
this.frameHandler.handleNewPlayer(playerPayload);
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.error('[GameSession] frameHandler.handleNewPlayer threw:', e);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
this.game.handleNewPlayer && this.game.handleNewPlayer(playerPayload);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
console.error('[GameSession] game.handleNewPlayer threw:', e);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this._sendPlayerFrame(playerId, ws);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
removePlayer(playerId) {
|
|
240
|
+
try {
|
|
241
|
+
this.game.handlePlayerDisconnect && this.game.handlePlayerDisconnect(playerId);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
console.error('[GameSession] game.handlePlayerDisconnect threw:', e);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
if (this.frameHandler && this.frameHandler.handlePlayerDisconnect) {
|
|
248
|
+
this.frameHandler.handlePlayerDisconnect(playerId);
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
console.error('[GameSession] frameHandler.handlePlayerDisconnect threw:', e);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
delete this.players[playerId];
|
|
255
|
+
delete this.playerInfoMap[playerId];
|
|
256
|
+
delete this.clientInfoMap[playerId];
|
|
257
|
+
delete this.playerSettingsMap[playerId];
|
|
258
|
+
delete this.remotePlayerMap[playerId];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// -----------------------------------------------------------------------
|
|
262
|
+
// Spectator management
|
|
263
|
+
// -----------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
addSpectator(spectatorId, ws, spectatorOpts = {}) {
|
|
266
|
+
if (!this.spectatorsEnabled) return;
|
|
267
|
+
|
|
268
|
+
this.spectators[spectatorId] = ws;
|
|
269
|
+
if (spectatorOpts.isRemote) this.remotePlayerMap[spectatorId] = true;
|
|
270
|
+
|
|
271
|
+
if (this.squisher.assetBundle) {
|
|
272
|
+
this._send(ws, this.squisher.assetBundle);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
if (this.frameHandler && this.frameHandler.handleNewSpectator) {
|
|
277
|
+
this.frameHandler.handleNewSpectator({ id: spectatorId, ws });
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
console.error('[GameSession] frameHandler.handleNewSpectator threw:', e);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this._sendPlayerFrame(spectatorId, ws);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
removeSpectator(spectatorId) {
|
|
287
|
+
try {
|
|
288
|
+
if (this.frameHandler && this.frameHandler.handleSpectatorDisconnect) {
|
|
289
|
+
this.frameHandler.handleSpectatorDisconnect(spectatorId);
|
|
290
|
+
}
|
|
291
|
+
} catch (e) {
|
|
292
|
+
console.error('[GameSession] frameHandler.handleSpectatorDisconnect threw:', e);
|
|
293
|
+
}
|
|
294
|
+
delete this.spectators[spectatorId];
|
|
295
|
+
delete this.remotePlayerMap[spectatorId];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// -----------------------------------------------------------------------
|
|
299
|
+
// Player info updates (from homenames)
|
|
300
|
+
// -----------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
handlePlayerUpdate(playerId, { info, settings }) {
|
|
303
|
+
this.playerInfoMap[playerId] = info;
|
|
304
|
+
this.playerSettingsMap[playerId] = settings;
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
if (this.frameHandler && this.frameHandler.handlePlayerUpdate) {
|
|
308
|
+
this.frameHandler.handlePlayerUpdate(playerId, { info, settings });
|
|
309
|
+
}
|
|
310
|
+
} catch (e) {
|
|
311
|
+
console.error('[GameSession] frameHandler.handlePlayerUpdate threw:', e);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
this.game.handlePlayerUpdate && this.game.handlePlayerUpdate(playerId, { info, settings });
|
|
316
|
+
} catch (e) {
|
|
317
|
+
console.error('[GameSession] game.handlePlayerUpdate threw:', e);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// -----------------------------------------------------------------------
|
|
322
|
+
// Input handling
|
|
323
|
+
// -----------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
handleInput(playerId, input) {
|
|
326
|
+
if (!input || typeof input !== 'object' || typeof input.type !== 'string') return;
|
|
327
|
+
if (!this.players[playerId] && !this.spectators[playerId]) return;
|
|
328
|
+
|
|
329
|
+
const pid = Number(playerId);
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
if (input.type === 'click') {
|
|
333
|
+
const data = input.data;
|
|
334
|
+
if (!data || typeof data.x !== 'number' || typeof data.y !== 'number') return;
|
|
335
|
+
this._handleClick(pid, data);
|
|
336
|
+
} else if (input.type === 'keydown') {
|
|
337
|
+
if (typeof input.key !== 'string' || input.key.length > 20) return;
|
|
338
|
+
this.game.handleKeyDown && this.game.handleKeyDown(pid, input.key);
|
|
339
|
+
} else if (input.type === 'keyup') {
|
|
340
|
+
if (typeof input.key !== 'string' || input.key.length > 20) return;
|
|
341
|
+
this.game.handleKeyUp && this.game.handleKeyUp(pid, input.key);
|
|
342
|
+
} else if (input.type === 'mouseup') {
|
|
343
|
+
this.game.handleMouseUp && this.game.handleMouseUp(pid, input.data);
|
|
344
|
+
} else if (input.type === 'input') {
|
|
345
|
+
if (input.gamepad) {
|
|
346
|
+
this.game.handleGamepadInput && this.game.handleGamepadInput(pid, input);
|
|
347
|
+
} else {
|
|
348
|
+
const topLayer = this.frameEnabled ? this.frame.topLayerRoot : null;
|
|
349
|
+
const node = this.game.findNode(input.nodeId)
|
|
350
|
+
|| (topLayer && topLayer.findChild(input.nodeId));
|
|
351
|
+
if (node && node.node && node.node.input) {
|
|
352
|
+
if (node.node.input.type === 'file') {
|
|
353
|
+
node.node.input.oninput(pid, Object.values(input.input || {}));
|
|
354
|
+
} else {
|
|
355
|
+
node.node.input.oninput(pid, input.input);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} else if (input.type === 'onhover') {
|
|
360
|
+
const topLayer = this.frameEnabled ? this.frame.topLayerRoot : null;
|
|
361
|
+
const node = this.game.findNode(input.nodeId)
|
|
362
|
+
|| (topLayer && topLayer.findChild(input.nodeId));
|
|
363
|
+
if (node && node.node?.onHover) node.node.onHover(pid);
|
|
364
|
+
} else if (input.type === 'offhover') {
|
|
365
|
+
const topLayer = this.frameEnabled ? this.frame.topLayerRoot : null;
|
|
366
|
+
const node = this.game.findNode(input.nodeId)
|
|
367
|
+
|| (topLayer && topLayer.findChild(input.nodeId));
|
|
368
|
+
if (node && node.node?.offHover) node.node.offHover(pid);
|
|
369
|
+
}
|
|
370
|
+
} catch (e) {
|
|
371
|
+
console.error(`[GameSession] Error handling input type="${input.type}" for player ${pid}:`, e);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// -----------------------------------------------------------------------
|
|
376
|
+
// Asset handling
|
|
377
|
+
// -----------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
handleNewAsset(key, asset) {
|
|
380
|
+
return this.squisher.handleNewAsset(key, asset).then(newBundle => {
|
|
381
|
+
for (const pid in this.players) {
|
|
382
|
+
this._send(this.players[pid], newBundle);
|
|
383
|
+
}
|
|
384
|
+
for (const sid in this.spectators) {
|
|
385
|
+
this._send(this.spectators[sid], newBundle);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// -----------------------------------------------------------------------
|
|
391
|
+
// Session navigation
|
|
392
|
+
// -----------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
movePlayer(playerId, port) {
|
|
395
|
+
const ws = this.players[playerId];
|
|
396
|
+
if (ws) {
|
|
397
|
+
this._send(ws, [5, Math.floor(port / 100), Math.floor(port % 100)]);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
spectateSession(playerId) {
|
|
402
|
+
const ws = this.players[playerId];
|
|
403
|
+
if (ws && this.port) {
|
|
404
|
+
this._send(ws, [6, Math.floor(this.port / 100), Math.floor(this.port % 100)]);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
joinSession(spectatorId) {
|
|
409
|
+
const ws = this.spectators[spectatorId];
|
|
410
|
+
if (ws && this.port) {
|
|
411
|
+
this._send(ws, [5, Math.floor(this.port / 100), Math.floor(this.port % 100)]);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
setServerCode(serverCode) {
|
|
416
|
+
if (this.frameHandler && !this.frameHandler.isDashboard) {
|
|
417
|
+
this.frameHandler.handleServerCode(serverCode);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// -----------------------------------------------------------------------
|
|
422
|
+
// Utilities
|
|
423
|
+
// -----------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
getPlayerCount() {
|
|
426
|
+
return Object.keys(this.players).length;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
destroy() {
|
|
430
|
+
try { if (this.game.destroy) this.game.destroy(); } catch (e) {}
|
|
431
|
+
try { if (this.game.clearAllTimers) this.game.clearAllTimers(); } catch (e) {}
|
|
432
|
+
this.players = {};
|
|
433
|
+
this.spectators = {};
|
|
434
|
+
this.playerInfoMap = {};
|
|
435
|
+
this.clientInfoMap = {};
|
|
436
|
+
this.playerSettingsMap = {};
|
|
437
|
+
this.remotePlayerMap = {};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// -----------------------------------------------------------------------
|
|
441
|
+
// Private: broadcasting
|
|
442
|
+
// -----------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
_broadcastState() {
|
|
445
|
+
for (const pid in this.players) {
|
|
446
|
+
try {
|
|
447
|
+
this._sendPlayerFrame(pid, this.players[pid]);
|
|
448
|
+
} catch (e) {
|
|
449
|
+
console.error(`[GameSession] Broadcast failed for player ${pid}:`, e);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
for (const sid in this.spectators) {
|
|
453
|
+
try {
|
|
454
|
+
this._sendPlayerFrame(sid, this.spectators[sid]);
|
|
455
|
+
} catch (e) {
|
|
456
|
+
console.error(`[GameSession] Broadcast failed for spectator ${sid}:`, e);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
_sendPlayerFrame(playerId, ws) {
|
|
462
|
+
let frame = this.squisher.getPlayerFrame(playerId);
|
|
463
|
+
if (!frame) frame = this.squisher.state;
|
|
464
|
+
if (frame) {
|
|
465
|
+
const flat = Array.isArray(frame) ? frame.flat() : frame;
|
|
466
|
+
this._send(ws, flat);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
_send(ws, data) {
|
|
471
|
+
try {
|
|
472
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === 1) {
|
|
473
|
+
ws.send(Buffer.from(data));
|
|
474
|
+
}
|
|
475
|
+
} catch (e) {
|
|
476
|
+
// Swallow send errors — client likely disconnected
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// -----------------------------------------------------------------------
|
|
481
|
+
// Private: click / hit-testing
|
|
482
|
+
// -----------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
_handleClick(playerId, click) {
|
|
485
|
+
if (!click || typeof click.x !== 'number' || typeof click.y !== 'number') return;
|
|
486
|
+
if (click.x < 0 || click.y < 0 || click.x >= 100 || click.y >= 100) return;
|
|
487
|
+
|
|
488
|
+
const spectating = this.spectatorsEnabled && !!this.spectators[playerId];
|
|
489
|
+
const clickedNode = this._findClick(click.x, click.y, spectating, playerId);
|
|
490
|
+
|
|
491
|
+
if (clickedNode) {
|
|
492
|
+
const bottomLayer = this.frameEnabled ? this.frame.root : null;
|
|
493
|
+
const topLayer = this.frameEnabled ? this.frame.topLayerRoot : null;
|
|
494
|
+
|
|
495
|
+
const realNode = this.game.findNode(clickedNode.id)
|
|
496
|
+
|| (bottomLayer && bottomLayer.findChild(clickedNode.id))
|
|
497
|
+
|| (topLayer && topLayer.findChild(clickedNode.id));
|
|
498
|
+
|
|
499
|
+
if (realNode && realNode.node && realNode.node.handleClick) {
|
|
500
|
+
if (this.frameEnabled) {
|
|
501
|
+
if (click.x <= (this.bezelX / 2) || click.x >= (100 - this.bezelX / 2)
|
|
502
|
+
|| click.y <= (this.bezelY / 2) || click.y >= (100 - this.bezelY / 2)) {
|
|
503
|
+
realNode.node.handleClick(playerId, click.x, click.y);
|
|
504
|
+
} else {
|
|
505
|
+
const shiftedX = click.x - (this.bezelX / 2);
|
|
506
|
+
const shiftedY = click.y - (this.bezelY / 2);
|
|
507
|
+
const scaledX = shiftedX * (1 / ((100 - this.bezelX) / 100));
|
|
508
|
+
const scaledY = shiftedY * (1 / ((100 - this.bezelY) / 100));
|
|
509
|
+
realNode.node.handleClick(playerId, scaledX, scaledY);
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
realNode.node.handleClick(playerId, click.x, click.y);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
_findClick(x, y, spectating, playerId) {
|
|
519
|
+
let clicked = null;
|
|
520
|
+
|
|
521
|
+
if (this.frameEnabled && this.frame.root) {
|
|
522
|
+
clicked = this._findClickHelper(x, y, spectating, playerId, this.frame.root.node, null, { x: 1, y: 1 }, false, 0) || clicked;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const layers = this.game.getLayers();
|
|
526
|
+
for (let i = 0; i < layers.length; i++) {
|
|
527
|
+
const layer = layers[i];
|
|
528
|
+
const scale = layer.scale || this.scale;
|
|
529
|
+
clicked = this._findClickHelper(x, y, spectating, playerId, layer.root.node, null, scale, true, 0) || clicked;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (this.frameEnabled && this.frame.topLayerRoot) {
|
|
533
|
+
clicked = this._findClickHelper(x, y, spectating, playerId, this.frame.topLayerRoot.node, null, { x: 1, y: 1 }, false, 0) || clicked;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return clicked;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
_findClickHelper(x, y, spectating, playerId, node, clicked, scale, inGame, depth) {
|
|
540
|
+
// Guard against deep/cyclic trees
|
|
541
|
+
if (depth > 100) return clicked;
|
|
542
|
+
|
|
543
|
+
if (node.playerIds && node.playerIds.length > 0 && !node.playerIds.find(p => p == playerId)) {
|
|
544
|
+
return clicked;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (node.coordinates2d && node.coordinates2d.length > 0) {
|
|
548
|
+
const vertices = [];
|
|
549
|
+
for (let i = 0; i < node.coordinates2d.length; i++) {
|
|
550
|
+
const xOff = 100 - (scale.x * 100);
|
|
551
|
+
const yOff = 100 - (scale.y * 100);
|
|
552
|
+
const sx = node.coordinates2d[i][0] * ((100 - xOff) / 100) + (xOff / 2);
|
|
553
|
+
const sy = node.coordinates2d[i][1] * ((100 - yOff) / 100) + (yOff / 2);
|
|
554
|
+
vertices.push([sx, sy]);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (vertices.length > 0) {
|
|
558
|
+
let isInside = false;
|
|
559
|
+
let minX = vertices[0][0], maxX = vertices[0][0];
|
|
560
|
+
let minY = vertices[0][1], maxY = vertices[0][1];
|
|
561
|
+
for (let i = 1; i < vertices.length; i++) {
|
|
562
|
+
minX = Math.min(vertices[i][0], minX);
|
|
563
|
+
maxX = Math.max(vertices[i][0], maxX);
|
|
564
|
+
minY = Math.min(vertices[i][1], minY);
|
|
565
|
+
maxY = Math.max(vertices[i][1], maxY);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (!(x < minX || x > maxX || y < minY || y > maxY)) {
|
|
569
|
+
let ii = 0, jj = vertices.length - 1;
|
|
570
|
+
for (ii, jj; ii < vertices.length; jj = ii++) {
|
|
571
|
+
if ((vertices[ii][1] > y) !== (vertices[jj][1] > y) &&
|
|
572
|
+
x < (vertices[jj][0] - vertices[ii][0]) * (y - vertices[ii][1]) / (vertices[jj][1] - vertices[ii][1]) + vertices[ii][0]) {
|
|
573
|
+
isInside = !isInside;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (isInside) {
|
|
579
|
+
if (!(spectating && inGame)) {
|
|
580
|
+
clicked = node;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (node.children) {
|
|
587
|
+
const childKeys = Object.keys(node.children);
|
|
588
|
+
for (let i = 0; i < childKeys.length; i++) {
|
|
589
|
+
const child = node.children[childKeys[i]];
|
|
590
|
+
if (child && child.node) {
|
|
591
|
+
clicked = this._findClickHelper(x, y, spectating, playerId, child.node, clicked, scale, inGame, depth + 1);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return clicked;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
module.exports = GameSession;
|