action-engine-js 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.
Files changed (93) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +348 -0
  3. package/actionengine/3rdparty/goblin/goblin.js +9609 -0
  4. package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
  5. package/actionengine/camera/actioncamera.js +90 -0
  6. package/actionengine/camera/cameracollisionhandler.js +69 -0
  7. package/actionengine/character/actioncharacter.js +360 -0
  8. package/actionengine/character/actioncharacter3D.js +61 -0
  9. package/actionengine/core/app.js +430 -0
  10. package/actionengine/debug/basedebugpanel.js +858 -0
  11. package/actionengine/display/canvasmanager.js +75 -0
  12. package/actionengine/display/gl/programmanager.js +570 -0
  13. package/actionengine/display/gl/shaders/lineshader.js +118 -0
  14. package/actionengine/display/gl/shaders/objectshader.js +1756 -0
  15. package/actionengine/display/gl/shaders/particleshader.js +43 -0
  16. package/actionengine/display/gl/shaders/shadowshader.js +319 -0
  17. package/actionengine/display/gl/shaders/spriteshader.js +100 -0
  18. package/actionengine/display/gl/shaders/watershader.js +67 -0
  19. package/actionengine/display/graphics/actionmodel3D.js +191 -0
  20. package/actionengine/display/graphics/actionsprite3D.js +230 -0
  21. package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
  22. package/actionengine/display/graphics/lighting/actionlight.js +211 -0
  23. package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
  24. package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
  25. package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
  26. package/actionengine/display/graphics/renderableobject.js +44 -0
  27. package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
  28. package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
  29. package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
  30. package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
  31. package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
  32. package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
  33. package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
  34. package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
  35. package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
  36. package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
  37. package/actionengine/display/graphics/texture/texturemanager.js +242 -0
  38. package/actionengine/display/graphics/texture/textureregistry.js +177 -0
  39. package/actionengine/input/actionscrollablearea.js +1405 -0
  40. package/actionengine/input/inputhandler.js +1647 -0
  41. package/actionengine/math/geometry/geometrybuilder.js +161 -0
  42. package/actionengine/math/geometry/glbexporter.js +364 -0
  43. package/actionengine/math/geometry/glbloader.js +722 -0
  44. package/actionengine/math/geometry/modelcodegenerator.js +97 -0
  45. package/actionengine/math/geometry/triangle.js +33 -0
  46. package/actionengine/math/geometry/triangleutils.js +34 -0
  47. package/actionengine/math/mathutils.js +25 -0
  48. package/actionengine/math/matrix4.js +785 -0
  49. package/actionengine/math/physics/actionphysics.js +108 -0
  50. package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
  51. package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
  52. package/actionengine/math/physics/actionraycast.js +129 -0
  53. package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
  54. package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
  55. package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
  56. package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
  57. package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
  58. package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
  59. package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
  60. package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
  61. package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
  62. package/actionengine/math/quaternion.js +61 -0
  63. package/actionengine/math/vector2.js +277 -0
  64. package/actionengine/math/vector3.js +318 -0
  65. package/actionengine/math/viewfrustum.js +136 -0
  66. package/actionengine/network/ACTIONNETREADME.md +810 -0
  67. package/actionengine/network/client/ActionNetManager.js +802 -0
  68. package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
  69. package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
  70. package/actionengine/network/client/SyncSystem.js +422 -0
  71. package/actionengine/network/p2p/ActionNetPeer.js +142 -0
  72. package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
  73. package/actionengine/network/p2p/DataConnection.js +282 -0
  74. package/actionengine/network/p2p/README.md +510 -0
  75. package/actionengine/network/p2p/example.html +502 -0
  76. package/actionengine/network/server/ActionNetServer.js +577 -0
  77. package/actionengine/network/server/ActionNetServerSSL.js +579 -0
  78. package/actionengine/network/server/ActionNetServerUtils.js +458 -0
  79. package/actionengine/network/server/SERVERREADME.md +314 -0
  80. package/actionengine/network/server/package-lock.json +35 -0
  81. package/actionengine/network/server/package.json +13 -0
  82. package/actionengine/network/server/start.bat +27 -0
  83. package/actionengine/network/server/start.sh +25 -0
  84. package/actionengine/network/server/startwss.bat +27 -0
  85. package/actionengine/sound/audiomanager.js +1589 -0
  86. package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
  87. package/actionengine/sound/soundfont/actionparser.js +718 -0
  88. package/actionengine/sound/soundfont/actionreverb.js +252 -0
  89. package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
  90. package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
  91. package/actionengine/sound/soundfont/soundfont.js +2 -0
  92. package/dist/action-engine.min.js +328 -0
  93. package/package.json +35 -0
@@ -0,0 +1,577 @@
1
+ /**
2
+ * Multiplayer Game Server
3
+ *
4
+ * A dedicated server for multiplayer games using ActionNetServerUtils.
5
+ * Handles connection/room management and generically relays game messages.
6
+ *
7
+ * Usage: node tetrisserver.js [port] [maxPlayersPerRoom]
8
+ * Example: node tetrisserver.js 8000 4
9
+ */
10
+
11
+ const WebSocket = require("ws");
12
+ const http = require("http");
13
+ const path = require("path");
14
+ const ActionNetServerUtils = require("./ActionNetServerUtils");
15
+
16
+ // ========================================
17
+ // CONFIGURATION
18
+ // ========================================
19
+ const CONFIG = {
20
+ port: 8000, // Default server port
21
+ debug: true, // Enable debug logging
22
+ maxPlayersPerRoom: -1 // Maximum players allowed per room (-1 = no limit)
23
+ };
24
+ // ========================================
25
+
26
+ // Allow command line override if needed
27
+ const port = process.argv[2] || CONFIG.port;
28
+ const maxPlayersPerRoom = process.argv[3] || CONFIG.maxPlayersPerRoom;
29
+
30
+ // Create HTTP server
31
+ const server = http.createServer();
32
+
33
+ // Create WebSocket server
34
+ const wss = new WebSocket.Server({ server });
35
+
36
+ // Create ActionNet server utilities
37
+ const utils = new ActionNetServerUtils(wss);
38
+
39
+ // Game state management
40
+ const gameRooms = new Map(); // roomName -> game state
41
+
42
+ console.log(`Starting Multiplayer Game server on port ${port}...`);
43
+ console.log(`Max players per room: ${maxPlayersPerRoom === -1 ? 'No limit' : maxPlayersPerRoom}`);
44
+
45
+ wss.on("connection", (ws, req) => {
46
+ console.log("New player connected from:", req.socket.remoteAddress);
47
+
48
+ // Send current room list immediately when client connects
49
+ const roomList = utils.getRoomListWithCounts(maxPlayersPerRoom);
50
+ if (roomList.length > 0) {
51
+ utils.sendToClient(ws, {
52
+ type: "roomList",
53
+ rooms: roomList
54
+ });
55
+ console.log(`Sent current room list to new player:`, roomList);
56
+ }
57
+
58
+ ws.on("message", (data) => {
59
+ try {
60
+ const message = JSON.parse(data.toString());
61
+ if (message.type === "connect") {
62
+ const displayName = utils.generateUniqueDisplayName(message.username);
63
+ console.log("Received connect message:", { ...message, displayName });
64
+ } else if (CONFIG.debug && message.type !== "pieceUpdate") {
65
+ // Don't spam logs with pieceUpdate messages
66
+ console.log("Received message:", message.type, message.playerNumber || "");
67
+ }
68
+
69
+ switch (message.type) {
70
+ case "connect":
71
+ handleConnect(ws, message);
72
+ break;
73
+
74
+ case "joinRoom":
75
+ handleJoinRoom(ws, message);
76
+ break;
77
+
78
+ case "leaveRoom":
79
+ handleLeaveRoom(ws, message);
80
+ break;
81
+
82
+ case "changeUsername":
83
+ handleChangeUsername(ws, message);
84
+ break;
85
+
86
+ case "ping":
87
+ // Auto-respond to pings
88
+ utils.sendToClient(ws, {
89
+ type: "pong",
90
+ sequence: message.sequence,
91
+ timestamp: message.timestamp
92
+ });
93
+ break;
94
+
95
+ default:
96
+ // All other messages are game messages - relay to room
97
+ relayGameMessage(ws, message);
98
+ break;
99
+ }
100
+ } catch (error) {
101
+ console.error("Error parsing message:", error);
102
+ }
103
+ });
104
+
105
+ ws.on("close", () => {
106
+ console.log("Player disconnected");
107
+ handleDisconnect(ws);
108
+ });
109
+
110
+ ws.on("error", (error) => {
111
+ console.error("WebSocket error:", error);
112
+ });
113
+ });
114
+
115
+ /**
116
+ * Handle player connection
117
+ */
118
+ function handleConnect(ws, message) {
119
+ const success = utils.registerClient(ws, message);
120
+
121
+ if (!success) {
122
+ // Client was rejected (duplicate username)
123
+ return;
124
+ }
125
+
126
+ const client = utils.getClient(ws);
127
+ console.log(`Player ${client.displayName} connected to lobby`);
128
+
129
+ // Send dedicated success message for reliable detection
130
+ utils.sendToClient(ws, {
131
+ type: "connectSuccess"
132
+ });
133
+
134
+ // Send room list
135
+ utils.sendToClient(ws, {
136
+ type: "roomList",
137
+ rooms: utils.getRoomListWithCounts(maxPlayersPerRoom)
138
+ });
139
+
140
+ // Broadcast updated room list to all clients
141
+ utils.broadcastToAll({
142
+ type: "roomList",
143
+ rooms: utils.getRoomListWithCounts(maxPlayersPerRoom)
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Handle room join request
149
+ */
150
+ function handleJoinRoom(ws, message) {
151
+ const client = utils.getClient(ws);
152
+ if (!client) {
153
+ console.warn("Join request from unregistered client");
154
+ return;
155
+ }
156
+
157
+ const roomName = message.roomName;
158
+
159
+ // Validate room name
160
+ if (!roomName || roomName.trim() === "") {
161
+ utils.sendToClient(ws, {
162
+ type: "error",
163
+ text: "Invalid room name. Room name cannot be empty."
164
+ });
165
+ return;
166
+ }
167
+
168
+ // Check if room is full (if limit is set)
169
+ if (maxPlayersPerRoom !== -1 && utils.roomExists(roomName)) {
170
+ const roomClients = utils.getClientsInRoom(roomName);
171
+ if (roomClients.length >= maxPlayersPerRoom) {
172
+ utils.sendToClient(ws, {
173
+ type: "error",
174
+ text: "Room is full."
175
+ });
176
+ return;
177
+ }
178
+ }
179
+
180
+ // Add client to room
181
+ const wasNewRoom = !utils.roomExists(roomName);
182
+ utils.addToRoom(ws, roomName);
183
+
184
+ // Initialize game state for the room if it's new
185
+ if (wasNewRoom) {
186
+ initializeGameRoom(roomName);
187
+ console.log(`Created new game room: ${roomName}`);
188
+
189
+ // Broadcast updated room list to all clients
190
+ utils.broadcastToAll({
191
+ type: "roomList",
192
+ rooms: utils.getRoomListWithCounts(maxPlayersPerRoom)
193
+ });
194
+ }
195
+
196
+ console.log(`Player ${client.displayName} joined room: ${roomName}`);
197
+
198
+ // Broadcast join message to the room
199
+ const joinMessage = wasNewRoom
200
+ ? `${client.displayName} created and joined room "${roomName}"`
201
+ : `${client.displayName} joined room "${roomName}"`;
202
+ utils.broadcastToRoom(roomName, {
203
+ type: "system",
204
+ text: joinMessage,
205
+ roomName: roomName,
206
+ timestamp: Date.now()
207
+ });
208
+
209
+ // Broadcast updated room list to all clients (for real-time player count updates)
210
+ utils.broadcastToAll({
211
+ type: "roomList",
212
+ rooms: utils.getRoomListWithCounts(maxPlayersPerRoom)
213
+ });
214
+
215
+ // Send dedicated success message for reliable detection
216
+ utils.sendToClient(ws, {
217
+ type: "joinSuccess",
218
+ roomName: roomName
219
+ });
220
+
221
+ // Send user list for this room
222
+ utils.sendToClient(ws, {
223
+ type: "userList",
224
+ users: utils.getClientsInRoom(roomName),
225
+ roomName: roomName
226
+ });
227
+
228
+ // Broadcast updated user list to room
229
+ utils.broadcastToRoom(roomName, {
230
+ type: "userList",
231
+ users: utils.getClientsInRoom(roomName),
232
+ roomName: roomName
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Handle leave room request
238
+ */
239
+ function handleLeaveRoom(ws, message) {
240
+ const client = utils.getClient(ws);
241
+ if (!client) return;
242
+
243
+ // Check if this player is the host BEFORE removing them
244
+ const wasHost = utils.isHost(ws);
245
+
246
+ const oldRoom = utils.removeFromRoom(ws, true); // true = return to lobby
247
+
248
+ if (oldRoom) {
249
+ console.log(`Player ${client.displayName} left room: ${oldRoom}`);
250
+
251
+ // Notify user
252
+ utils.sendToClient(ws, {
253
+ type: "system",
254
+ text: `Left Tetris battle "${oldRoom}". You are now in the lobby.`
255
+ });
256
+
257
+ // Send updated room list
258
+ utils.sendToClient(ws, {
259
+ type: "roomList",
260
+ rooms: utils.getRoomListWithCounts(maxPlayersPerRoom)
261
+ });
262
+
263
+ // If host left, close the room entirely
264
+ if (wasHost && utils.roomExists(oldRoom)) {
265
+ console.log(`Host left room ${oldRoom} - closing room and removing all players`);
266
+
267
+ // Notify remaining players that host left
268
+ utils.broadcastToRoom(oldRoom, {
269
+ type: "hostLeft",
270
+ id: client.id,
271
+ displayName: client.displayName,
272
+ roomName: oldRoom,
273
+ timestamp: Date.now()
274
+ });
275
+
276
+ // Get all clients still in the room
277
+ const remainingClients = Array.from(utils.rooms.get(oldRoom) || []);
278
+
279
+ // Remove all clients from the room and put them back in lobby
280
+ remainingClients.forEach(clientWs => {
281
+ utils.removeFromRoom(clientWs, true); // true = return to lobby
282
+
283
+ // Notify each client they were removed
284
+ utils.sendToClient(clientWs, {
285
+ type: "system",
286
+ text: `Room "${oldRoom}" closed because host left.`
287
+ });
288
+
289
+ // Send updated room list
290
+ utils.sendToClient(clientWs, {
291
+ type: "roomList",
292
+ rooms: utils.getRoomListWithCounts(maxPlayersPerRoom)
293
+ });
294
+ });
295
+
296
+ // Delete the room
297
+ gameRooms.delete(oldRoom);
298
+
299
+ // Broadcast updated room list to everyone
300
+ utils.broadcastToAll({
301
+ type: "roomList",
302
+ rooms: utils.getRoomListWithCounts(maxPlayersPerRoom)
303
+ });
304
+ } else if (!utils.roomExists(oldRoom)) {
305
+ // Room became empty naturally
306
+ console.log(`Deleted empty room: ${oldRoom}`);
307
+ gameRooms.delete(oldRoom);
308
+
309
+ // Broadcast updated room list
310
+ utils.broadcastToAll({
311
+ type: "roomList",
312
+ rooms: utils.getRoomListWithCounts(maxPlayersPerRoom)
313
+ });
314
+ } else {
315
+ // Non-host left, room continues
316
+ // Broadcast user left to remaining clients in room
317
+ utils.broadcastToRoom(oldRoom, {
318
+ type: "userLeft",
319
+ id: client.id,
320
+ displayName: client.displayName,
321
+ roomName: oldRoom,
322
+ timestamp: Date.now()
323
+ });
324
+
325
+ // Send updated user list to room
326
+ utils.broadcastToRoom(oldRoom, {
327
+ type: "userList",
328
+ users: utils.getClientsInRoom(oldRoom),
329
+ roomName: oldRoom
330
+ });
331
+
332
+ // Broadcast updated room list to all clients (for real-time player count updates)
333
+ utils.broadcastToAll({
334
+ type: "roomList",
335
+ rooms: utils.getRoomListWithCounts(maxPlayersPerRoom)
336
+ });
337
+ }
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Handle player disconnect
343
+ */
344
+ function handleDisconnect(ws) {
345
+ // Check if this player was the host BEFORE disconnecting
346
+ const wasHost = utils.isHost(ws);
347
+ const client = utils.getClient(ws);
348
+ const roomName = client ? client.roomName : null;
349
+
350
+ const clientInfo = utils.handleDisconnect(ws);
351
+
352
+ if (clientInfo) {
353
+ const { id, displayName } = clientInfo;
354
+ const disconnectRoomName = clientInfo.roomName;
355
+
356
+ if (disconnectRoomName) {
357
+ console.log(`Player ${displayName} disconnected from room: ${disconnectRoomName}`);
358
+
359
+ // If host disconnected, close the room entirely
360
+ if (wasHost && utils.roomExists(disconnectRoomName)) {
361
+ console.log(`Host disconnected from room ${disconnectRoomName} - closing room and removing all players`);
362
+
363
+ // Notify remaining players that host left
364
+ utils.broadcastToRoom(disconnectRoomName, {
365
+ type: "hostLeft",
366
+ id: id,
367
+ displayName: displayName,
368
+ roomName: disconnectRoomName,
369
+ timestamp: Date.now()
370
+ });
371
+
372
+ // Get all clients still in the room
373
+ const remainingClients = Array.from(utils.rooms.get(disconnectRoomName) || []);
374
+
375
+ // Remove all clients from the room and put them back in lobby
376
+ remainingClients.forEach(clientWs => {
377
+ utils.removeFromRoom(clientWs, true); // true = return to lobby
378
+
379
+ // Notify each client they were removed
380
+ utils.sendToClient(clientWs, {
381
+ type: "system",
382
+ text: `Room "${disconnectRoomName}" closed because host disconnected.`
383
+ });
384
+
385
+ // Send updated room list
386
+ utils.sendToClient(clientWs, {
387
+ type: "roomList",
388
+ rooms: utils.getRoomListWithCounts(maxPlayersPerRoom)
389
+ });
390
+ });
391
+
392
+ // Delete the room
393
+ gameRooms.delete(disconnectRoomName);
394
+
395
+ // Broadcast updated room list to everyone
396
+ utils.broadcastToAll({
397
+ type: "roomList",
398
+ rooms: utils.getRoomListWithCounts(maxPlayersPerRoom)
399
+ });
400
+ } else if (!utils.roomExists(disconnectRoomName)) {
401
+ // Room became empty naturally
402
+ console.log(`Deleted empty room: ${disconnectRoomName}`);
403
+ gameRooms.delete(disconnectRoomName);
404
+
405
+ // Broadcast updated room list
406
+ utils.broadcastToAll({
407
+ type: "roomList",
408
+ rooms: utils.getRoomListWithCounts(maxPlayersPerRoom)
409
+ });
410
+ } else {
411
+ // Non-host disconnected, room continues
412
+ // Broadcast user left to room
413
+ utils.broadcastToRoom(disconnectRoomName, {
414
+ type: "userLeft",
415
+ id: id,
416
+ displayName: displayName,
417
+ roomName: disconnectRoomName,
418
+ timestamp: Date.now()
419
+ });
420
+
421
+ // Send updated user list
422
+ utils.broadcastToRoom(disconnectRoomName, {
423
+ type: "userList",
424
+ users: utils.getClientsInRoom(disconnectRoomName),
425
+ roomName: disconnectRoomName
426
+ });
427
+ }
428
+ } else {
429
+ console.log(`Player ${displayName} disconnected from lobby`);
430
+ }
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Handle username change request
436
+ */
437
+ function handleChangeUsername(ws, message) {
438
+ const client = utils.getClient(ws);
439
+ if (!client) {
440
+ console.log("Username change from unknown client");
441
+ return;
442
+ }
443
+
444
+ const newUsername = message.username;
445
+
446
+ // Validate new username
447
+ if (!newUsername || newUsername.trim() === "" || newUsername.length < 2) {
448
+ utils.sendToClient(ws, {
449
+ type: "error",
450
+ text: "Username must be at least 2 characters long"
451
+ });
452
+ return;
453
+ }
454
+
455
+ if (!/^[a-zA-Z0-9_-]+$/.test(newUsername)) {
456
+ utils.sendToClient(ws, {
457
+ type: "error",
458
+ text: "Username can only contain letters, numbers, underscores, and hyphens"
459
+ });
460
+ return;
461
+ }
462
+
463
+ // Check if new username is already taken by another client
464
+ const existingClient = Array.from(utils.clients.values()).find(
465
+ (c) => c.username === newUsername && c.id !== client.id
466
+ );
467
+ if (existingClient) {
468
+ utils.sendToClient(ws, {
469
+ type: "error",
470
+ text: "Username is already taken"
471
+ });
472
+ return;
473
+ }
474
+
475
+ console.log(`Player ${client.displayName} changing username from ${client.username} to ${newUsername}`);
476
+
477
+ // Update client info
478
+ const oldUsername = client.username;
479
+ const oldDisplayName = client.displayName;
480
+
481
+ // Generate display name for the new username (exclude self from check)
482
+ const newDisplayName = utils.generateUniqueDisplayName(newUsername, client.id);
483
+ client.username = newUsername;
484
+ client.displayName = newDisplayName;
485
+
486
+ console.log(`Player ${oldDisplayName} is now ${newDisplayName}`);
487
+
488
+ // Send success confirmation
489
+ utils.sendToClient(ws, {
490
+ type: "usernameChangeSuccess",
491
+ oldUsername: oldUsername,
492
+ newUsername: newUsername,
493
+ displayName: client.displayName
494
+ });
495
+
496
+ // Broadcast updated user list to current room
497
+ if (client.roomName) {
498
+ utils.broadcastToRoom(client.roomName, {
499
+ type: "userList",
500
+ users: utils.getClientsInRoom(client.roomName),
501
+ roomName: client.roomName
502
+ });
503
+
504
+ // Broadcast username change notification
505
+ utils.broadcastToRoom(client.roomName, {
506
+ type: "system",
507
+ text: `${oldDisplayName} changed their name to ${client.displayName}`,
508
+ roomName: client.roomName,
509
+ timestamp: Date.now()
510
+ });
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Relay game message to other players in the room
516
+ * Server doesn't care about message content - just routes it
517
+ */
518
+ function relayGameMessage(ws, message) {
519
+ const client = utils.getClient(ws);
520
+ if (!client || !client.roomName) {
521
+ return;
522
+ }
523
+
524
+ // Simply broadcast the entire message to room, excluding sender
525
+ utils.broadcastToRoom(client.roomName, message, ws);
526
+ }
527
+
528
+ /**
529
+ * Initialize game state for a new room
530
+ */
531
+ function initializeGameRoom(roomName) {
532
+ gameRooms.set(roomName, {
533
+ players: new Map(),
534
+ startTime: Date.now(),
535
+ gameStarted: false
536
+ });
537
+
538
+ console.log(`Initialized game state for room: ${roomName}`);
539
+ }
540
+
541
+ /**
542
+ * Clean up game state for a room
543
+ */
544
+ function cleanupGameRoom(roomName) {
545
+ gameRooms.delete(roomName);
546
+ console.log(`Cleaned up game state for room: ${roomName}`);
547
+ }
548
+
549
+ // Start the server
550
+ server.listen(port, () => {
551
+ console.log(`Multiplayer Game server running on port ${port}`);
552
+ console.log(`WebSocket endpoint: ws://localhost:${port}`);
553
+ console.log("Press Ctrl+C to stop the server");
554
+ });
555
+
556
+ // Graceful shutdown
557
+ process.on("SIGINT", () => {
558
+ console.log("\nShutting down Game server...");
559
+ wss.clients.forEach((client) => {
560
+ client.close();
561
+ });
562
+ server.close(() => {
563
+ console.log("Game server stopped");
564
+ process.exit(0);
565
+ });
566
+ });
567
+
568
+ // Handle uncaught exceptions
569
+ process.on("uncaughtException", (error) => {
570
+ console.error("Uncaught Exception:", error);
571
+ process.exit(1);
572
+ });
573
+
574
+ process.on("unhandledRejection", (reason, promise) => {
575
+ console.error("Unhandled Rejection at:", promise, "reason:", reason);
576
+ process.exit(1);
577
+ });