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,802 @@
1
+ /**
2
+ * ActionNetManager - Core networking API for ActionEngine
3
+ *
4
+ * A headless, event-driven WebSocket client for multiplayer games and apps.
5
+ * Provides room/lobby pattern out of the box, but flexible enough for custom protocols.
6
+ *
7
+ * FEATURES:
8
+ * - Event-driven API (on/off pattern)
9
+ * - Room/Lobby management
10
+ * - Connection state tracking
11
+ * - Message queue handling
12
+ * - Reconnection support with exponential backoff
13
+ * - Ping/RTT tracking
14
+ *
15
+ * USAGE:
16
+ * ```javascript
17
+ * const net = new ActionNetManager({
18
+ * url: 'ws://yourserver.com:3000',
19
+ * autoConnect: false
20
+ * });
21
+ *
22
+ * net.on('connected', () => console.log('Connected!'));
23
+ * net.on('roomList', (rooms) => console.log('Available rooms:', rooms));
24
+ * net.on('message', (msg) => console.log('Received:', msg));
25
+ *
26
+ * net.connectToServer({ username: 'Player1' });
27
+ * net.joinRoom('lobby-1');
28
+ * net.send({ type: 'chat', text: 'Hello!' });
29
+ * ```
30
+ */
31
+ class ActionNetManager {
32
+ constructor(config = {}) {
33
+ // Configuration
34
+ this.config = {
35
+ url: config.url || 'ws://localhost:3000',
36
+ autoConnect: config.autoConnect !== undefined ? config.autoConnect : false,
37
+ reconnect: config.reconnect !== undefined ? config.reconnect : false,
38
+ reconnectDelay: config.reconnectDelay || 1000,
39
+ maxReconnectDelay: config.maxReconnectDelay || 30000,
40
+ reconnectAttempts: config.reconnectAttempts || -1, // -1 = infinite
41
+ pingInterval: config.pingInterval || 30000, // Ping every 30 seconds
42
+ pongTimeout: config.pongTimeout || 5000, // Expect pong within 5 seconds
43
+ debug: config.debug || false
44
+ };
45
+
46
+ // Connection state
47
+ this.socket = null;
48
+ this.isConnectedFlag = false;
49
+ this.isInRoomFlag = false;
50
+ this.connectionFailedFlag = false;
51
+
52
+ // Client info
53
+ this.clientId = null; // Unique identifier (auto-generated or custom)
54
+ this.username = null; // User-facing name
55
+ this.clientData = {}; // Custom metadata developers can set
56
+ this.currentRoomName = null;
57
+
58
+ // Room/Lobby data
59
+ this.availableRooms = [];
60
+ this.connectedClients = []; // Clients in current room
61
+
62
+ // Event handlers
63
+ this.handlers = new Map();
64
+
65
+ // Message queue (for polling pattern)
66
+ this.messageQueue = [];
67
+
68
+ // Reconnection tracking
69
+ this.reconnectAttempt = 0;
70
+ this.reconnectTimer = null;
71
+ this.manualDisconnect = false;
72
+
73
+ // Connection abort
74
+ this.connectionAbortController = null;
75
+
76
+ // Ping/RTT tracking
77
+ this.pingTimer = null;
78
+ this.pongTimer = null;
79
+ this.lastPingTime = 0;
80
+ this.rtt = 0;
81
+ this.pingSequence = 0;
82
+
83
+ // Auto-connect if enabled
84
+ if (this.config.autoConnect && this.config.url) {
85
+ this.connectToServer();
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Register an event handler
91
+ *
92
+ * Available events:
93
+ * - 'connected': () => {} - Connected to server
94
+ * - 'disconnected': () => {} - Disconnected from server
95
+ * - 'error': (error) => {} - Connection/socket error
96
+ * - 'message': (msg) => {} - Any message received
97
+ * - 'roomList': (rooms) => {} - Available rooms updated
98
+ * - 'userList': (users) => {} - Users in room updated
99
+ * - 'joinedRoom': (roomName) => {} - Successfully joined room
100
+ * - 'leftRoom': (roomName) => {} - Left room
101
+ * - Custom events based on message.type
102
+ */
103
+ on(event, handler) {
104
+ if (!this.handlers.has(event)) {
105
+ this.handlers.set(event, []);
106
+ }
107
+ this.handlers.get(event).push(handler);
108
+ }
109
+
110
+ /**
111
+ * Remove an event handler
112
+ */
113
+ off(event, handler) {
114
+ if (!this.handlers.has(event)) return;
115
+ const handlers = this.handlers.get(event);
116
+ const index = handlers.indexOf(handler);
117
+ if (index > -1) {
118
+ handlers.splice(index, 1);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Emit an event to all registered handlers
124
+ */
125
+ emit(event, ...args) {
126
+ if (!this.handlers.has(event)) return;
127
+ const handlers = this.handlers.get(event);
128
+ handlers.forEach(handler => {
129
+ try {
130
+ handler(...args);
131
+ } catch (error) {
132
+ if (this.config.debug) {
133
+ console.error('[ActionNetManager] Error in event handler:', error);
134
+ }
135
+ }
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Connect to server
141
+ *
142
+ * @param {Object} data - Client data (e.g., {username: 'Player1'})
143
+ * @returns {Promise} - Resolves when connected
144
+ */
145
+ connectToServer(data = {}) {
146
+ return new Promise((resolve, reject) => {
147
+ try {
148
+ if (this.config.debug) {
149
+ // console.log('[ActionNetManager] Connecting to:', this.config.url);
150
+ }
151
+
152
+ // Create abort controller for this connection attempt
153
+ this.connectionAbortController = new AbortController();
154
+ const signal = this.connectionAbortController.signal;
155
+
156
+ // Check if already aborted
157
+ if (signal.aborted) {
158
+ reject(new Error('Connection cancelled'));
159
+ return;
160
+ }
161
+
162
+ // Listen for abort signal
163
+ signal.addEventListener('abort', () => {
164
+ reject(new Error('Connection cancelled'));
165
+ });
166
+
167
+ // Store client data
168
+ this.clientData = data;
169
+
170
+ // Set clientId (unique identifier)
171
+ this.clientId = data.clientId || data.id || `client_${Date.now()}`;
172
+
173
+ // Set username (user-facing name for UI)
174
+ this.username = data.username || data.name || this.clientId;
175
+
176
+ // Create WebSocket connection
177
+ this.socket = new WebSocket(this.config.url);
178
+
179
+ // Connection timeout
180
+ const timeout = setTimeout(() => {
181
+ this.socket.close();
182
+ reject(new Error('Connection timeout'));
183
+ }, 5000);
184
+
185
+ // Connection opened
186
+ this.socket.onopen = () => {
187
+ if (this.config.debug) {
188
+ // console.log('[ActionNetManager] WebSocket connected, waiting for server response');
189
+ }
190
+
191
+ // Send connect message with clientId and username
192
+ this.send({
193
+ type: 'connect',
194
+ clientId: this.clientId, // Unique identifier
195
+ username: this.username, // User-facing name
196
+ ...this.clientData // Any additional metadata
197
+ });
198
+
199
+ // Wait for server confirmation
200
+ const messageHandler = (msg) => {
201
+ if (msg.type === 'connectSuccess') {
202
+ clearTimeout(timeout);
203
+ this.isConnectedFlag = true;
204
+ this.connectionFailedFlag = false;
205
+ this.reconnectAttempt = 0; // Reset reconnect attempts on success
206
+
207
+ if (this.config.debug) {
208
+ // console.log('[ActionNetManager] Server connection confirmed');
209
+ }
210
+
211
+ // Start ping/pong if enabled
212
+ this.startPing();
213
+
214
+ this.emit('connected');
215
+ this.off('message', messageHandler);
216
+ this.connectionAbortController = null; // Clear abort controller
217
+ resolve();
218
+ } else if (msg.type === 'error') {
219
+ clearTimeout(timeout);
220
+ this.off('message', messageHandler);
221
+ this.connectionAbortController = null; // Clear abort controller
222
+ reject(new Error(msg.text));
223
+ }
224
+ };
225
+
226
+ this.on('message', messageHandler);
227
+ };
228
+
229
+ // Message received
230
+ this.socket.onmessage = (event) => {
231
+ try {
232
+ const message = JSON.parse(event.data);
233
+ this.handleMessage(message);
234
+ } catch (error) {
235
+ if (this.config.debug) {
236
+ console.error('[ActionNetManager] Failed to parse message:', error);
237
+ }
238
+ }
239
+ };
240
+
241
+ // Connection closed
242
+ this.socket.onclose = () => {
243
+ if (this.config.debug) {
244
+ // console.log('[ActionNetManager] Disconnected from server');
245
+ }
246
+
247
+ this.isConnectedFlag = false;
248
+ this.connectedClients = [];
249
+ this.rtt = 0; // Reset RTT
250
+ this.stopPing();
251
+ this.emit('disconnected');
252
+
253
+ // Auto-reconnect if enabled and not manually disconnected
254
+ if (this.config.reconnect && !this.manualDisconnect) {
255
+ this.scheduleReconnect();
256
+ }
257
+ };
258
+
259
+ // Connection error
260
+ this.socket.onerror = (error) => {
261
+ clearTimeout(timeout);
262
+ if (this.config.debug) {
263
+ console.error('[ActionNetManager] Connection error:', error);
264
+ }
265
+ this.connectionFailedFlag = true;
266
+ this.emit('error', error);
267
+ this.connectionAbortController = null; // Clear abort controller
268
+ reject(error);
269
+ };
270
+
271
+ } catch (error) {
272
+ reject(error);
273
+ }
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Join a room
279
+ *
280
+ * @param {String} roomName - Name of room to join
281
+ * @returns {Promise} - Resolves when joined
282
+ */
283
+ joinRoom(roomName) {
284
+ if (!this.isConnectedFlag) {
285
+ return Promise.reject(new Error('Not connected to server'));
286
+ }
287
+
288
+ return new Promise((resolve, reject) => {
289
+ // Send join request with clientId and username
290
+ this.send({
291
+ type: 'joinRoom',
292
+ roomName: roomName,
293
+ clientId: this.clientId, // Unique identifier
294
+ username: this.username // User-facing name
295
+ });
296
+
297
+ // Wait for confirmation
298
+ const timeout = setTimeout(() => {
299
+ this.off('message', successHandler);
300
+ this.off('message', errorHandler);
301
+ reject(new Error('Join room timeout'));
302
+ }, 5000);
303
+
304
+ // Listen for success
305
+ const successHandler = (msg) => {
306
+ if (msg.type === 'joinSuccess') {
307
+ clearTimeout(timeout);
308
+ this.currentRoomName = roomName;
309
+ this.isInRoomFlag = true;
310
+ this.emit('joinedRoom', roomName);
311
+ this.off('message', successHandler);
312
+ this.off('message', errorHandler);
313
+ resolve(roomName);
314
+ }
315
+ };
316
+
317
+ const errorHandler = (msg) => {
318
+ if (msg.type === 'error') {
319
+ clearTimeout(timeout);
320
+ this.off('message', successHandler);
321
+ this.off('message', errorHandler);
322
+ reject(new Error(msg.text || 'Failed to join room'));
323
+ }
324
+ };
325
+
326
+ this.on('message', successHandler);
327
+ this.on('message', errorHandler);
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Leave current room
333
+ */
334
+ leaveRoom() {
335
+ if (!this.isInRoomFlag || !this.currentRoomName) {
336
+ if (this.config.debug) {
337
+ // console.log('[ActionNetManager] Not in a room');
338
+ }
339
+ return;
340
+ }
341
+
342
+ this.send({
343
+ type: 'leaveRoom',
344
+ clientId: this.clientId,
345
+ username: this.username
346
+ });
347
+
348
+ const oldRoom = this.currentRoomName;
349
+ this.currentRoomName = null;
350
+ this.isInRoomFlag = false;
351
+ this.connectedClients = [];
352
+ this.emit('leftRoom', oldRoom);
353
+ }
354
+
355
+ /**
356
+ * Send a message to the server
357
+ *
358
+ * @param {Object} message - Message object to send
359
+ */
360
+ send(message) {
361
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
362
+ if (this.config.debug) {
363
+ console.error('[ActionNetManager] Cannot send: not connected');
364
+ }
365
+ return false;
366
+ }
367
+
368
+ try {
369
+ this.socket.send(JSON.stringify(message));
370
+ return true;
371
+ } catch (error) {
372
+ if (this.config.debug) {
373
+ console.error('[ActionNetManager] Send error:', error);
374
+ }
375
+ return false;
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Start ping timer
381
+ */
382
+ startPing() {
383
+ if (this.config.pingInterval <= 0) return;
384
+
385
+ this.stopPing(); // Clear any existing timers
386
+
387
+ this.pingTimer = setInterval(() => {
388
+ if (!this.isConnectedFlag) {
389
+ this.stopPing();
390
+ return;
391
+ }
392
+
393
+ // Send ping
394
+ this.pingSequence++;
395
+ this.lastPingTime = Date.now();
396
+
397
+ this.send({
398
+ type: 'ping',
399
+ sequence: this.pingSequence,
400
+ timestamp: this.lastPingTime
401
+ });
402
+
403
+ // Set pong timeout
404
+ this.pongTimer = setTimeout(() => {
405
+ if (this.config.debug) {
406
+ console.warn('[ActionNetManager] Pong timeout - connection may be dead');
407
+ }
408
+ this.emit('timeout');
409
+
410
+ // Reconnect if timeout occurs
411
+ if (this.config.reconnect) {
412
+ this.socket.close();
413
+ }
414
+ }, this.config.pongTimeout);
415
+
416
+ }, this.config.pingInterval);
417
+ }
418
+
419
+ /**
420
+ * Stop ping timer
421
+ */
422
+ stopPing() {
423
+ if (this.pingTimer) {
424
+ clearInterval(this.pingTimer);
425
+ this.pingTimer = null;
426
+ }
427
+ if (this.pongTimer) {
428
+ clearTimeout(this.pongTimer);
429
+ this.pongTimer = null;
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Schedule reconnect with exponential backoff
435
+ */
436
+ scheduleReconnect() {
437
+ // Check if we've exceeded max attempts
438
+ if (this.config.reconnectAttempts !== -1 &&
439
+ this.reconnectAttempt >= this.config.reconnectAttempts) {
440
+ if (this.config.debug) {
441
+ // console.log('[ActionNetManager] Max reconnect attempts reached');
442
+ }
443
+ this.emit('reconnectFailed');
444
+ return;
445
+ }
446
+
447
+ this.reconnectAttempt++;
448
+
449
+ // Exponential backoff: delay * 2^attempt, capped at maxReconnectDelay
450
+ const delay = Math.min(
451
+ this.config.reconnectDelay * Math.pow(2, this.reconnectAttempt - 1),
452
+ this.config.maxReconnectDelay
453
+ );
454
+
455
+ if (this.config.debug) {
456
+ // console.log(`[ActionNetManager] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);
457
+ }
458
+
459
+ this.emit('reconnecting', { attempt: this.reconnectAttempt, delay });
460
+
461
+ this.reconnectTimer = setTimeout(() => {
462
+ this.connectToServer(this.clientData).catch(error => {
463
+ if (this.config.debug) {
464
+ console.error('[ActionNetManager] Reconnect failed:', error);
465
+ }
466
+ });
467
+ }, delay);
468
+ }
469
+
470
+ /**
471
+ * Handle incoming messages
472
+ */
473
+ handleMessage(message) {
474
+ // Add to message queue for polling pattern
475
+ this.messageQueue.push(message);
476
+
477
+ // Emit generic message event
478
+ this.emit('message', message);
479
+
480
+ // Handle specific message types
481
+ switch (message.type) {
482
+ case 'pong':
483
+ // Handle pong response
484
+ if (this.pongTimer) {
485
+ clearTimeout(this.pongTimer);
486
+ this.pongTimer = null;
487
+ }
488
+
489
+ // Calculate RTT
490
+ if (message.sequence === this.pingSequence) {
491
+ this.rtt = Date.now() - this.lastPingTime;
492
+ this.emit('rtt', this.rtt);
493
+ }
494
+ break;
495
+
496
+ case 'ping':
497
+ // Auto-respond to server pings
498
+ this.send({
499
+ type: 'pong',
500
+ sequence: message.sequence,
501
+ timestamp: message.timestamp
502
+ });
503
+ break;
504
+
505
+ case 'roomList':
506
+ this.availableRooms = message.rooms || [];
507
+ this.emit('roomList', this.availableRooms);
508
+ break;
509
+
510
+ case 'userList':
511
+ this.connectedClients = message.users || [];
512
+ this.emit('userList', this.connectedClients);
513
+ break;
514
+
515
+ case 'userJoined':
516
+ if (!this.connectedClients.some(c => c.id === message.id)) {
517
+ this.connectedClients.push(message);
518
+ }
519
+ this.emit('userJoined', message);
520
+ break;
521
+
522
+ case 'userLeft':
523
+ this.connectedClients = this.connectedClients.filter(
524
+ c => c.id !== message.id
525
+ );
526
+ this.emit('userLeft', message);
527
+ break;
528
+
529
+ case 'hostLeft':
530
+ this.emit('hostLeft', message);
531
+ break;
532
+
533
+ case 'chat':
534
+ this.emit('chat', message);
535
+ break;
536
+
537
+ case 'system':
538
+ this.emit('system', message);
539
+ break;
540
+
541
+ case 'error':
542
+ this.emit('error', new Error(message.text || 'Server error'));
543
+ break;
544
+
545
+ default:
546
+ // Emit custom event based on message type
547
+ this.emit(message.type, message);
548
+ break;
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Get new messages from queue and clear it
554
+ * (Useful for polling pattern instead of events)
555
+ */
556
+ getNewMessages() {
557
+ const messages = [...this.messageQueue];
558
+ this.messageQueue = [];
559
+ return messages;
560
+ }
561
+
562
+ /**
563
+ * Disconnect from server
564
+ */
565
+ disconnect() {
566
+ this.manualDisconnect = true; // Flag to prevent auto-reconnect
567
+
568
+ // Abort pending connection attempt
569
+ if (this.connectionAbortController) {
570
+ this.connectionAbortController.abort();
571
+ this.connectionAbortController = null;
572
+ }
573
+
574
+ // Clear reconnect timer
575
+ if (this.reconnectTimer) {
576
+ clearTimeout(this.reconnectTimer);
577
+ this.reconnectTimer = null;
578
+ }
579
+
580
+ // Stop ping
581
+ this.stopPing();
582
+
583
+ if (this.socket) {
584
+ this.socket.close();
585
+ this.socket = null;
586
+ }
587
+
588
+ this.isConnectedFlag = false;
589
+ this.isInRoomFlag = false;
590
+ this.connectionFailedFlag = false;
591
+ this.currentRoomName = null;
592
+ this.connectedClients = [];
593
+ this.availableRooms = [];
594
+ this.messageQueue = [];
595
+ this.rtt = 0; // Reset RTT
596
+ this.reconnectAttempt = 0;
597
+ }
598
+
599
+ /**
600
+ * Get available rooms
601
+ */
602
+ getAvailableRooms() {
603
+ return [...this.availableRooms];
604
+ }
605
+
606
+ /**
607
+ * Get connected users in current room
608
+ */
609
+ getConnectedUsers() {
610
+ return [...this.connectedClients];
611
+ }
612
+
613
+ /**
614
+ * Get username
615
+ */
616
+ getUsername() {
617
+ return this.username;
618
+ }
619
+
620
+ /**
621
+ * Check if connected to server
622
+ */
623
+ isConnected() {
624
+ return this.isConnectedFlag;
625
+ }
626
+
627
+ /**
628
+ * Check if in a room
629
+ */
630
+ isInRoom() {
631
+ return this.isInRoomFlag;
632
+ }
633
+
634
+ /**
635
+ * Check if connection failed
636
+ */
637
+ connectionFailed() {
638
+ return this.connectionFailedFlag;
639
+ }
640
+
641
+ /**
642
+ * Get current room name
643
+ */
644
+ getCurrentRoomName() {
645
+ return this.currentRoomName;
646
+ }
647
+
648
+ /**
649
+ * Get client ID (unique identifier)
650
+ */
651
+ getClientId() {
652
+ return this.clientId;
653
+ }
654
+
655
+ /**
656
+ * Set username (sends change request to server)
657
+ *
658
+ * @param {String} name - New username
659
+ * @returns {Promise} - Resolves when username change is confirmed
660
+ */
661
+ setUsername(name) {
662
+ if (!this.isConnectedFlag) {
663
+ return Promise.reject(new Error('Not connected to server'));
664
+ }
665
+
666
+ // Validate new username locally
667
+ if (!name || name.trim() === '' || name.length < 2) {
668
+ return Promise.reject(new Error('Username must be at least 2 characters long'));
669
+ }
670
+
671
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
672
+ return Promise.reject(new Error('Username can only contain letters, numbers, underscores, and hyphens'));
673
+ }
674
+
675
+ return new Promise((resolve, reject) => {
676
+ // Send username change request
677
+ this.send({
678
+ type: 'changeUsername',
679
+ username: name
680
+ });
681
+
682
+ // Wait for confirmation
683
+ const timeout = setTimeout(() => {
684
+ this.off('message', successHandler);
685
+ this.off('message', errorHandler);
686
+ reject(new Error('Username change timeout'));
687
+ }, 5000);
688
+
689
+ // Listen for success
690
+ const successHandler = (msg) => {
691
+ if (msg.type === 'usernameChangeSuccess') {
692
+ clearTimeout(timeout);
693
+ // Update local username
694
+ this.username = msg.newUsername;
695
+ this.emit('usernameChanged', {
696
+ oldUsername: msg.oldUsername,
697
+ newUsername: msg.newUsername,
698
+ displayName: msg.displayName
699
+ });
700
+ this.off('message', successHandler);
701
+ this.off('message', errorHandler);
702
+ resolve({
703
+ oldUsername: msg.oldUsername,
704
+ newUsername: msg.newUsername,
705
+ displayName: msg.displayName
706
+ });
707
+ }
708
+ };
709
+
710
+ const errorHandler = (msg) => {
711
+ if (msg.type === 'error') {
712
+ clearTimeout(timeout);
713
+ this.off('message', successHandler);
714
+ this.off('message', errorHandler);
715
+ reject(new Error(msg.text || 'Username change failed'));
716
+ }
717
+ };
718
+
719
+ this.on('message', successHandler);
720
+ this.on('message', errorHandler);
721
+ });
722
+ }
723
+
724
+
725
+ /**
726
+ * Get current RTT in milliseconds
727
+ */
728
+ getRTT() {
729
+ return this.rtt;
730
+ }
731
+
732
+ /**
733
+ * Get current reconnect attempt count
734
+ */
735
+ getReconnectAttempts() {
736
+ return this.reconnectAttempt;
737
+ }
738
+
739
+ /**
740
+ * Get the host of the current room
741
+ * @returns {Object|null} - Host client info or null if not in room or no host found
742
+ */
743
+ getHost() {
744
+ if (!this.isInRoomFlag) {
745
+ return null;
746
+ }
747
+
748
+ const host = this.connectedClients.find(client => client.isHost === true);
749
+ return host || null;
750
+ }
751
+
752
+ /**
753
+ * Check if the current user is the host of their room
754
+ * @returns {Boolean}
755
+ */
756
+ isCurrentUserHost() {
757
+ if (!this.isInRoomFlag) {
758
+ return false;
759
+ }
760
+
761
+ const host = this.getHost();
762
+ return host && host.id === this.clientId;
763
+ }
764
+
765
+ /**
766
+ * Test server availability without full connection
767
+ * @returns {Promise<{available: boolean, error?: string}>}
768
+ */
769
+ testServerConnection() {
770
+ return new Promise((resolve) => {
771
+ const testSocket = new WebSocket(this.config.url);
772
+
773
+ const timeout = setTimeout(() => {
774
+ testSocket.close();
775
+ resolve({ available: false, error: 'Connection timeout' });
776
+ }, 1000); // 1 second timeout
777
+
778
+ testSocket.onopen = () => {
779
+ clearTimeout(timeout);
780
+ testSocket.close();
781
+ resolve({ available: true });
782
+ };
783
+
784
+ testSocket.onerror = (error) => {
785
+ clearTimeout(timeout);
786
+ testSocket.close();
787
+ resolve({ available: false, error: 'Connection failed' });
788
+ };
789
+ });
790
+ }
791
+
792
+ /**
793
+ * Update loop (optional - for compatibility with ActionEngine game loop)
794
+ */
795
+ update(deltaTime) {
796
+ // Handle connection state updates
797
+ if (this.socket && this.socket.readyState === WebSocket.CLOSED && this.isConnectedFlag) {
798
+ this.isConnectedFlag = false;
799
+ this.connectedClients = [];
800
+ }
801
+ }
802
+ }