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,810 @@
1
+ # ActionNet - Networking for ActionEngine
2
+
3
+ A complete multiplayer networking solution for ActionEngine games, providing:
4
+
5
+ - **ActionNetManager**: Client-side WebSocket manager with room/lobby system
6
+ - **ActionNetManagerP2P**: Peer-to-peer networking with DHT discovery and WebRTC
7
+ - **ActionNetManagerGUI**: Unified GUI supporting both WebSocket and P2P modes
8
+ - **ActionNetServerUtils**: Server-side utilities for client and room management
9
+ - **SyncSystem**: Generic state synchronization for client-to-client data sharing
10
+
11
+ ## Client Identity System
12
+
13
+ ActionNet provides a clean client identity system with three key concepts:
14
+
15
+ ### 1. **Client ID** (Unique Identifier)
16
+ - Auto-generated unique identifier for internal tracking
17
+ - Format: `client_1234567890` (WebSocket) or `peer_abc123def` (P2P)
18
+ - Never changes during connection
19
+ - Used for data lookups and internal logic
20
+
21
+ ### 2. **Username** (User-Provided Name)
22
+ - Human-readable name provided by the user
23
+ - What users type when connecting (e.g., "Alice", "Player123")
24
+ - Can be changed during session with `setUsername()`
25
+ - May not be unique (multiple users can request same name)
26
+
27
+ ### 3. **Display Name** (Server-Generated Unique Name)
28
+ - Auto-generated by server/peer to ensure uniqueness
29
+ - Based on username but made unique with suffixes
30
+ - Examples:
31
+ - First "Player" → `"Player"`
32
+ - Second "Player" → `"Player (1)"`
33
+ - Third "Player" → `"Player (2)"`
34
+ - **This is what you should show in your UI**
35
+ - Automatically updated in user lists
36
+
37
+ ## Quick Start - WebSocket Mode
38
+
39
+ ```javascript
40
+ // Create and connect
41
+ const net = new ActionNetManager({
42
+ url: 'ws://yourserver.com:3000',
43
+ reconnect: true,
44
+ debug: true
45
+ });
46
+
47
+ net.on('connected', () => console.log('Connected!'));
48
+ net.on('roomList', (rooms) => console.log('Available rooms:', rooms));
49
+
50
+ // Connect with username
51
+ await net.connectToServer({ username: 'Player123' });
52
+
53
+ // Join a room
54
+ await net.joinRoom('lobby-1');
55
+
56
+ // Listen for other players
57
+ net.on('userList', (users) => {
58
+ users.forEach(user => {
59
+ console.log(`${user.displayName} (ID: ${user.id})`);
60
+ });
61
+ });
62
+
63
+ // Send messages
64
+ net.send({ type: 'chat', text: 'Hello!' });
65
+ ```
66
+
67
+ ## Quick Start - P2P Mode (No Server Required)
68
+
69
+ P2P mode uses bittorrent trackers for peer discovery - no central server needed.
70
+
71
+ ```javascript
72
+ // Initialize GUI with P2P mode - handles lobby UI
73
+ const gui = new ActionNetManagerGUI(canvases, input, audio, { mode: 'p2p' });
74
+
75
+ // Listen for when user creates/joins a room
76
+ gui.on('joinedRoom', (roomName) => {
77
+ console.log('Joined room:', roomName);
78
+ // Get the network manager and data channel
79
+ const net = gui.getNetManager();
80
+ const dataChannel = net.getDataChannel();
81
+ // Your game session starts here
82
+ });
83
+
84
+ gui.on('leftRoom', () => {
85
+ console.log('Left room');
86
+ });
87
+
88
+ // Update GUI each frame
89
+ gui.action_update(deltaTime);
90
+ gui.action_draw();
91
+ ```
92
+
93
+ Or integrate P2P directly:
94
+
95
+ ```javascript
96
+ const net = new ActionNetManagerP2P({ gameId: 'tetris-1v1', debug: true });
97
+
98
+ // Join DHT network and search for rooms
99
+ await net.joinGame('tetris-1v1', 'Player123');
100
+
101
+ // Listen for discovered rooms
102
+ net.on('roomList', (rooms) => {
103
+ rooms.forEach(room => {
104
+ console.log(`${room.username}'s room: ${room.currentPlayers}/${room.maxPlayers} players`);
105
+ });
106
+ });
107
+
108
+ // Create your own room (become host)
109
+ net.createRoom();
110
+ net.on('joinedRoom', ({ peerId, dataChannel }) => {
111
+ console.log('Room created, waiting for players...');
112
+ });
113
+
114
+ // Or join someone else's room
115
+ await net.joinRoom(hostPeerId);
116
+ net.on('joinedRoom', ({ peerId, dataChannel }) => {
117
+ console.log('Joined room! Connected via WebRTC');
118
+ });
119
+ ```
120
+
121
+ ### P2P vs WebSocket Mode
122
+
123
+ | Feature | P2P | WebSocket |
124
+ |---------|-----|-----------|
125
+ | Server Required | ❌ No | ✅ Yes |
126
+ | Discovery | DHT (distributed) | Server list |
127
+ | Connection | WebRTC (direct) | WebSocket relay |
128
+ | Latency | Low (direct) | Medium (relay) |
129
+ | Scalability | Unlimited | Server-limited |
130
+ | Setup Complexity | Medium (browser APIs) | Low (server required) |
131
+
132
+ ## SyncSystem - State Synchronization
133
+
134
+ `SyncSystem` provides a simple way to synchronize state between clients without writing custom sync logic.
135
+
136
+ ### Basic Usage (WebSocket)
137
+
138
+ ```javascript
139
+ const net = new ActionNetManager({ url: 'ws://localhost:3000' });
140
+
141
+ // Create sync system
142
+ const sync = new SyncSystem({
143
+ send: (msg) => net.send(msg),
144
+ broadcastInterval: 16, // Broadcast every 16ms (~60fps)
145
+ staleThreshold: 200 // Consider remote stale after 200ms
146
+ });
147
+
148
+ // Register sync sources
149
+ sync.register('player', {
150
+ getFields: () => ({
151
+ score: player.score,
152
+ level: player.level,
153
+ alive: !player.gameOver
154
+ })
155
+ });
156
+
157
+ sync.register('match', {
158
+ getFields: () => ({
159
+ state: matchStateMachine.getState(),
160
+ ready: isReady
161
+ })
162
+ });
163
+
164
+ // Listen for remote updates
165
+ sync.on('remoteUpdated', (allRemoteData) => {
166
+ updateOpponentDisplay(allRemoteData.player);
167
+ });
168
+
169
+ sync.on('remoteStale', () => {
170
+ showDisconnectedWarning();
171
+ });
172
+
173
+ sync.on('remoteFresh', () => {
174
+ hideDisconnectedWarning();
175
+ });
176
+
177
+ // Start syncing
178
+ sync.start();
179
+
180
+ // Hook up incoming messages
181
+ net.on('syncUpdate', (msg) => {
182
+ sync.handleSyncUpdate(msg);
183
+ });
184
+
185
+ // Query remote data
186
+ const remotePlayer = sync.getRemote('player');
187
+ if (remotePlayer) {
188
+ opponentScore.text = remotePlayer.score;
189
+ }
190
+ ```
191
+
192
+ ### SyncSystem with P2P
193
+
194
+ For P2P, pass the dataChannel's send function:
195
+
196
+ ```javascript
197
+ const net = new ActionNetManagerP2P({ gameId: 'tetris-1v1' });
198
+
199
+ const sync = new SyncSystem({
200
+ send: (msg) => {
201
+ const channel = net.getDataChannel();
202
+ if (channel && channel.readyState === 'open') {
203
+ channel.send(JSON.stringify(msg));
204
+ }
205
+ },
206
+ broadcastInterval: 16,
207
+ staleThreshold: 200
208
+ });
209
+
210
+ // Same registration and event handling as above
211
+ sync.register('player', { getFields: () => ({...}) });
212
+ sync.start();
213
+
214
+ // For P2P, listen to messages on the data channel directly
215
+ const channel = net.getDataChannel();
216
+ channel.onmessage = (event) => {
217
+ try {
218
+ const msg = JSON.parse(event.data);
219
+ if (msg.type === 'syncUpdate') {
220
+ sync.handleSyncUpdate(msg);
221
+ }
222
+ } catch (e) {
223
+ console.error('Failed to parse message:', e);
224
+ }
225
+ };
226
+ ```
227
+
228
+ ### SyncSystem API
229
+
230
+ ```javascript
231
+ // Register a sync source
232
+ sync.register('sourceId', {
233
+ getFields: () => ({ field1: value1, field2: value2 })
234
+ });
235
+
236
+ // Query remote data
237
+ sync.getRemote('sourceId') // Returns all fields
238
+ sync.getRemoteField('sourceId', 'field1') // Returns single field
239
+
240
+ // Check connection health
241
+ sync.isRemoteStale() // Boolean
242
+ sync.hasRemoteData() // Boolean
243
+ sync.getTimeSinceLastUpdate() // Milliseconds
244
+
245
+ // Manual control
246
+ sync.forceBroadcast() // Send immediately
247
+ sync.clearRemoteData() // Clear remote state
248
+ sync.start() // Begin syncing
249
+ sync.stop() // Stop syncing
250
+
251
+ // Events
252
+ sync.on('remoteUpdated', (data) => {})
253
+ sync.on('remoteStale', () => {})
254
+ sync.on('remoteFresh', () => {})
255
+ ```
256
+
257
+ ## Ping and RTT Tracking (WebSocket Only)
258
+
259
+ ActionNetManager includes built-in ping/pong for latency tracking.
260
+
261
+ ```javascript
262
+ const net = new ActionNetManager({
263
+ url: 'ws://localhost:3000',
264
+ pingInterval: 30000, // Ping every 30 seconds
265
+ pongTimeout: 5000 // Expect pong within 5 seconds
266
+ });
267
+
268
+ // Get current round-trip time
269
+ const rtt = net.getRTT();
270
+ console.log(`Ping: ${rtt}ms`);
271
+
272
+ // Listen for RTT updates
273
+ net.on('rtt', (rtt) => {
274
+ pingDisplay.text = `${rtt}ms`;
275
+ if (rtt > 200) {
276
+ showLagWarning();
277
+ }
278
+ });
279
+
280
+ // Listen for timeout (pong not received)
281
+ net.on('timeout', () => {
282
+ console.warn('Connection timeout - may be disconnected');
283
+ });
284
+ ```
285
+
286
+ ## Auto-Reconnection (WebSocket Only)
287
+
288
+ ActionNetManager supports automatic reconnection with exponential backoff.
289
+
290
+ ```javascript
291
+ const net = new ActionNetManager({
292
+ url: 'ws://localhost:3000',
293
+ reconnect: true, // Enable auto-reconnect
294
+ reconnectDelay: 1000, // Start with 1 second delay
295
+ maxReconnectDelay: 30000, // Cap at 30 seconds
296
+ reconnectAttempts: -1 // -1 = infinite attempts
297
+ });
298
+
299
+ // Listen for reconnection events
300
+ net.on('reconnecting', ({ attempt, delay }) => {
301
+ console.log(`Reconnecting... attempt ${attempt} in ${delay}ms`);
302
+ showReconnectingMessage(attempt);
303
+ });
304
+
305
+ net.on('connected', () => {
306
+ console.log('Reconnected!');
307
+ hideReconnectingMessage();
308
+ });
309
+
310
+ net.on('reconnectFailed', () => {
311
+ console.log('Max reconnect attempts reached');
312
+ showConnectionFailedMessage();
313
+ });
314
+
315
+ // Get current reconnect attempt count
316
+ const attempts = net.getReconnectAttempts();
317
+ console.log(`Reconnect attempts: ${attempts}`);
318
+ ```
319
+
320
+ ### Exponential Backoff
321
+
322
+ Reconnect delays increase exponentially:
323
+ - Attempt 1: 1 second
324
+ - Attempt 2: 2 seconds
325
+ - Attempt 3: 4 seconds
326
+ - Attempt 4: 8 seconds
327
+ - Attempt 5: 16 seconds
328
+ - Attempt 6+: 30 seconds (capped at maxReconnectDelay)
329
+
330
+ ## Host System
331
+
332
+ The first person to join a room becomes the host. This is useful for peer selection, game flow control, or special privileges.
333
+
334
+ ### Client API
335
+
336
+ ```javascript
337
+ // Check if current user is the host
338
+ if (net.isCurrentUserHost()) {
339
+ console.log('You are the host - you can start the game!');
340
+ showStartButton();
341
+ }
342
+
343
+ // Get the host's info
344
+ const host = net.getHost();
345
+ if (host) {
346
+ console.log('Host:', host.displayName);
347
+ }
348
+
349
+ // Listen for host leaving (only guests receive this)
350
+ net.on('hostLeft', (msg) => {
351
+ console.log('Host left - returning to lobby');
352
+ showLobbyScreen();
353
+ });
354
+ ```
355
+
356
+ ### Server API
357
+
358
+ ```javascript
359
+ const utils = new ActionNetServerUtils(wss);
360
+
361
+ // Check if a client is the host
362
+ if (utils.isHost(ws)) {
363
+ console.log('This client is the host!');
364
+ }
365
+
366
+ // Get host info for a room
367
+ const host = utils.getHostOfRoom('game-room-1');
368
+ if (host) {
369
+ console.log('Host:', host.displayName);
370
+ }
371
+ ```
372
+
373
+ ## Best Practices
374
+
375
+ ### ✅ DO:
376
+ ```javascript
377
+ // Use displayName for anything users see
378
+ chatMessage.text = `${user.displayName}: ${text}`;
379
+ userListItem.text = user.displayName;
380
+ scoreboard.add(user.displayName, score);
381
+
382
+ // Use client ID for internal tracking
383
+ playerData[user.id] = { score: 100 };
384
+ entityMap.set(user.id, entity);
385
+
386
+ // Check host status for game logic
387
+ if (net.isCurrentUserHost()) {
388
+ // Only host can start the game
389
+ showStartButton();
390
+ }
391
+
392
+ // Use SyncSystem for continuous state
393
+ sync.register('position', {
394
+ getFields: () => ({ x: player.x, y: player.y })
395
+ });
396
+
397
+ // Use custom messages for one-shot events
398
+ net.send({ type: 'attack', damage: 10 });
399
+ ```
400
+
401
+ ### ❌ DON'T:
402
+ ```javascript
403
+ // Don't show client IDs to users
404
+ chat.addMessage(client.id, text); // Shows "client_1234567890" - confusing!
405
+
406
+ // Don't use usernames as unique keys
407
+ playerData[client.username] = data; // Can conflict if names aren't unique!
408
+
409
+ // Don't confuse username and displayName
410
+ chat.text = client.username; // Use displayName instead!
411
+
412
+ // Don't sync everything constantly
413
+ sync.register('everything', {
414
+ getFields: () => game.entireState // Too much data!
415
+ });
416
+ ```
417
+
418
+ ## Server Setup
419
+
420
+ ### WebSocket Server (Node.js)
421
+
422
+ ```javascript
423
+ const WebSocket = require('ws');
424
+ const ActionNetServerUtils = require('./ActionNetServerUtils');
425
+
426
+ const wss = new WebSocket.Server({ port: 3000 });
427
+ const utils = new ActionNetServerUtils(wss);
428
+
429
+ wss.on('connection', (ws) => {
430
+ ws.on('message', (data) => {
431
+ const msg = JSON.parse(data.toString());
432
+
433
+ if (msg.type === 'connect') {
434
+ // Register client - automatically generates unique displayName
435
+ utils.registerClient(ws, msg);
436
+ const client = utils.getClient(ws);
437
+
438
+ // Send confirmation
439
+ ws.send(JSON.stringify({
440
+ type: 'connectSuccess',
441
+ clientId: client.id,
442
+ displayName: client.displayName
443
+ }));
444
+
445
+ // Broadcast room list
446
+ utils.broadcastToAllClients({
447
+ type: 'roomList',
448
+ rooms: utils.getAvailableRooms()
449
+ });
450
+ }
451
+
452
+ if (msg.type === 'joinRoom') {
453
+ utils.addToRoom(ws, msg.roomName);
454
+ const client = utils.getClient(ws);
455
+
456
+ ws.send(JSON.stringify({
457
+ type: 'joinSuccess',
458
+ roomName: msg.roomName
459
+ }));
460
+
461
+ // Notify room members
462
+ utils.broadcastToRoom(msg.roomName, {
463
+ type: 'userJoined',
464
+ id: client.id,
465
+ displayName: client.displayName,
466
+ isHost: utils.isHost(ws)
467
+ });
468
+
469
+ // Send user list to joiner
470
+ const users = utils.getClientsInRoom(msg.roomName).map(c => ({
471
+ id: c.id,
472
+ displayName: c.displayName,
473
+ isHost: utils.isHost(ws) && utils.getHostOfRoom(msg.roomName) === ws
474
+ }));
475
+
476
+ ws.send(JSON.stringify({
477
+ type: 'userList',
478
+ users: users
479
+ }));
480
+ }
481
+
482
+ if (msg.type === 'leaveRoom') {
483
+ const client = utils.getClient(ws);
484
+ const wasHost = utils.isHost(ws);
485
+
486
+ utils.removeFromRoom(ws);
487
+
488
+ if (wasHost) {
489
+ // Host left - close room
490
+ utils.broadcastToRoom(client.roomName, {
491
+ type: 'hostLeft',
492
+ displayName: client.displayName
493
+ });
494
+ utils.closeRoom(client.roomName);
495
+ } else {
496
+ // Guest left - notify room
497
+ utils.broadcastToRoom(client.roomName, {
498
+ type: 'userLeft',
499
+ id: client.id,
500
+ displayName: client.displayName
501
+ });
502
+ }
503
+ }
504
+ });
505
+
506
+ ws.on('close', () => {
507
+ const client = utils.getClient(ws);
508
+ if (client) {
509
+ const wasHost = utils.isHost(ws);
510
+ utils.unregisterClient(ws);
511
+
512
+ if (wasHost && client.roomName) {
513
+ // Host disconnected - close room
514
+ utils.broadcastToRoom(client.roomName, {
515
+ type: 'hostLeft',
516
+ displayName: client.displayName
517
+ });
518
+ utils.closeRoom(client.roomName);
519
+ }
520
+ }
521
+ });
522
+ });
523
+ ```
524
+
525
+ ## API Reference
526
+
527
+ ### ActionNetManager (WebSocket Client)
528
+
529
+ #### Connection
530
+ ```javascript
531
+ net.connectToServer({ username: 'Bob' }) // Returns Promise
532
+ net.disconnect()
533
+ net.isConnected() // Returns Boolean
534
+ net.connectionFailed() // Returns Boolean
535
+ net.testServerConnection() // Returns Promise<{available, error}>
536
+ ```
537
+
538
+ #### Room Management
539
+ ```javascript
540
+ net.joinRoom('lobby') // Returns Promise
541
+ net.leaveRoom()
542
+ net.isInRoom() // Returns Boolean
543
+ net.getCurrentRoomName() // Returns String|null
544
+ net.getAvailableRooms() // Returns Array<String>
545
+ ```
546
+
547
+ #### Identity
548
+ ```javascript
549
+ net.getClientId() // Returns String (unique ID)
550
+ net.getUsername() // Returns String (user-provided)
551
+ net.setUsername(name) // Returns Promise
552
+ ```
553
+
554
+ #### Host System
555
+ ```javascript
556
+ net.isCurrentUserHost() // Returns Boolean
557
+ net.getHost() // Returns {id, displayName, isHost}
558
+ ```
559
+
560
+ #### Messaging
561
+ ```javascript
562
+ net.send(message) // Returns Boolean
563
+ net.getNewMessages() // Returns Array (polling pattern)
564
+ ```
565
+
566
+ #### Latency (WebSocket only)
567
+ ```javascript
568
+ net.getRTT() // Returns Number (milliseconds)
569
+ net.getReconnectAttempts() // Returns Number
570
+ ```
571
+
572
+ #### Connected Users
573
+ ```javascript
574
+ net.getConnectedUsers() // Returns Array<{id, displayName, isHost}>
575
+ ```
576
+
577
+ ### ActionNetManagerP2P (P2P Client)
578
+
579
+ #### Game Management
580
+ ```javascript
581
+ net.joinGame(gameId, username) // Returns Promise
582
+ net.createRoom()
583
+ net.joinRoom(hostPeerId) // Returns Promise
584
+ net.leaveRoom()
585
+ net.disconnect() // Returns Promise
586
+ ```
587
+
588
+ #### State
589
+ ```javascript
590
+ net.isConnected() // Returns Boolean
591
+ net.isInRoom() // Returns Boolean
592
+ net.isCurrentUserHost() // Returns Boolean
593
+ ```
594
+
595
+ #### Identity
596
+ ```javascript
597
+ net.getUsername() // Returns String
598
+ net.setUsername(name) // Returns Promise
599
+ ```
600
+
601
+ #### Data
602
+ ```javascript
603
+ net.getDataChannel() // Returns RTCDataChannel|null
604
+ net.getAvailableRooms() // Returns Array<{peerId, username, displayName, currentPlayers, maxPlayers, slots}>
605
+ net.getConnectedUsers() // Returns Array<{id, displayName, isHost}>
606
+ net.getConnectedPeerCount() // Returns Number (direct connections)
607
+ net.getDiscoveredPeerCount() // Returns Number (DHT peers)
608
+ ```
609
+
610
+ #### Host-specific (Host only)
611
+ ```javascript
612
+ net.acceptJoin(peerId) // Accept pending join request
613
+ ```
614
+
615
+ ### ActionNetManagerGUI (UI Bridge)
616
+
617
+ #### Setup
618
+ ```javascript
619
+ // WebSocket mode
620
+ const gui = new ActionNetManagerGUI(canvases, input, audio, 8000);
621
+
622
+ // P2P mode
623
+ const gui = new ActionNetManagerGUI(canvases, input, audio, { mode: 'p2p' });
624
+ ```
625
+
626
+ #### Update & Render
627
+ ```javascript
628
+ gui.action_update(deltaTime)
629
+ gui.action_draw()
630
+ ```
631
+
632
+ #### Access
633
+ ```javascript
634
+ gui.getNetManager() // Returns ActionNetManager or ActionNetManagerP2P
635
+ gui.getUsername() // Returns String
636
+ gui.isConnected() // Returns Boolean
637
+ gui.isInRoom() // Returns Boolean
638
+ ```
639
+
640
+ #### SyncSystem
641
+ ```javascript
642
+ gui.syncSystem // Access SyncSystem instance
643
+ gui.activateSyncForRoom() // Start syncing when room joined
644
+ gui.deactivateSyncForRoom() // Stop syncing when room left
645
+ ```
646
+
647
+ #### Custom Messages
648
+ ```javascript
649
+ gui.registerMessageHandler('myEvent', (msg) => {
650
+ // Handle custom message
651
+ });
652
+
653
+ gui.unregisterMessageHandler('myEvent')
654
+ ```
655
+
656
+ #### Events
657
+ ```javascript
658
+ gui.on('joinedRoom', (roomName) => {})
659
+ gui.on('leftRoom', (roomName) => {})
660
+ gui.on('buttonPressed', () => {})
661
+ gui.on('back', () => {})
662
+ gui.on('selectionChanged', ({oldIndex, newIndex}) => {})
663
+ gui.on('disconnected', () => {})
664
+ ```
665
+
666
+ ### ActionNetServerUtils (Server Utilities)
667
+
668
+ #### Client Management
669
+ ```javascript
670
+ utils.registerClient(ws, {username, clientId, ...metadata})
671
+ utils.unregisterClient(ws)
672
+ utils.getClient(ws) // Returns {id, username, displayName, roomName}
673
+ ```
674
+
675
+ #### Room Management
676
+ ```javascript
677
+ utils.addToRoom(ws, roomName) // Returns Boolean
678
+ utils.removeFromRoom(ws)
679
+ utils.closeRoom(roomName)
680
+ utils.getClientsInRoom(roomName) // Returns Array of client objects
681
+ utils.getAvailableRooms() // Returns Array<String>
682
+ ```
683
+
684
+ #### Broadcasting
685
+ ```javascript
686
+ utils.broadcastToRoom(roomName, message) // Send to room members
687
+ utils.broadcastToAllClients(message) // Send to everyone
688
+ ```
689
+
690
+ #### Host System
691
+ ```javascript
692
+ utils.isHost(ws) // Returns Boolean
693
+ utils.getHostOfRoom(roomName) // Returns client object or null
694
+ ```
695
+
696
+ #### Display Names
697
+ ```javascript
698
+ utils.generateUniqueDisplayName(username, excludeId) // Returns String
699
+ ```
700
+
701
+ ### SyncSystem
702
+
703
+ #### Registration
704
+ ```javascript
705
+ sync.register(sourceId, {
706
+ getFields: () => ({ field1, field2 })
707
+ })
708
+
709
+ sync.unregister(sourceId)
710
+ ```
711
+
712
+ #### Query
713
+ ```javascript
714
+ sync.getRemote(sourceId) // Returns Object|null
715
+ sync.getRemoteField(sourceId, field) // Returns Any|null
716
+ sync.getAllRemote() // Returns Object
717
+ ```
718
+
719
+ #### State Checks
720
+ ```javascript
721
+ sync.isRemoteStale() // Returns Boolean
722
+ sync.hasRemoteData() // Returns Boolean
723
+ sync.getTimeSinceLastUpdate() // Returns Number (ms)
724
+ sync.getRegisteredSources() // Returns Array<String>
725
+ ```
726
+
727
+ #### Manual Control
728
+ ```javascript
729
+ sync.forceBroadcast() // Broadcast immediately
730
+ sync.clearRemoteData() // Clear remote state
731
+ sync.handleSyncUpdate(message) // Process incoming sync
732
+ sync.start() // Begin syncing
733
+ sync.stop() // Stop syncing
734
+ sync.setSendFunction(fn) // Change transport
735
+ ```
736
+
737
+ #### Events
738
+ ```javascript
739
+ sync.on('remoteUpdated', (allRemoteData) => {})
740
+ sync.on('remoteStale', () => {})
741
+ sync.on('remoteFresh', () => {})
742
+ sync.on('broadcast', (localData) => {})
743
+ ```
744
+
745
+ ## Events Reference
746
+
747
+ ### ActionNetManager Events
748
+
749
+ - `connected`: `() => {}` - Connected to server
750
+ - `disconnected`: `() => {}` - Disconnected from server
751
+ - `error`: `(error) => {}` - Connection or server error
752
+ - `message`: `(msg) => {}` - Any message received
753
+ - `roomList`: `(rooms) => {}` - Available rooms updated
754
+ - `userList`: `(users) => {}` - Users in room updated
755
+ - `joinedRoom`: `(roomName) => {}` - Successfully joined room
756
+ - `leftRoom`: `(roomName) => {}` - Left room
757
+ - `userJoined`: `(user) => {}` - Someone joined your room
758
+ - `userLeft`: `(user) => {}` - Someone left your room
759
+ - `hostLeft`: `(msg) => {}` - Host left, room closing
760
+ - `usernameChanged`: `({oldUsername, newUsername, displayName}) => {}` - Username changed
761
+ - `chat`: `(msg) => {}` - Chat message received
762
+ - `system`: `(msg) => {}` - System message received
763
+ - `rtt`: `(milliseconds) => {}` - Round-trip time updated
764
+ - `timeout`: `() => {}` - Pong not received (connection issue)
765
+ - `reconnecting`: `({attempt, delay}) => {}` - Attempting to reconnect
766
+ - `reconnectFailed`: `() => {}` - Max reconnect attempts reached
767
+ - Custom events based on `message.type`
768
+
769
+ ### ActionNetManagerP2P Events
770
+
771
+ - `connected`: `() => {}` - Joined DHT network
772
+ - `disconnected`: `() => {}` - Disconnected from DHT
773
+ - `error`: `(error) => {}` - Connection error
774
+ - `roomList`: `(rooms) => {}` - Available rooms updated
775
+ - `userList`: `(users) => {}` - Users in room updated
776
+ - `joinedRoom`: `({peerId, dataChannel}) => {}` - Joined/created room
777
+ - `leftRoom`: `(peerId) => {}` - Left room
778
+ - `userJoined`: `(user) => {}` - Someone joined your room
779
+ - `userLeft`: `(user) => {}` - Someone left your room
780
+ - `hostLeft`: `({peerId}) => {}` - Host left (guests only)
781
+ - `guestLeft`: `({peerId}) => {}` - Guest left (host only)
782
+ - `joinRequest`: `({peerId, username}) => {}` - Join request received (host only)
783
+ - `joinAccepted`: `({peerId, users}) => {}` - Join accepted by host (guest only)
784
+ - `joinRejected`: `({peerId, reason}) => {}` - Join rejected by host (guest only)
785
+ - `usernameChanged`: `({oldUsername, newUsername, displayName}) => {}` - Username changed
786
+ - `peerHandshook`: `({peerId, username}) => {}` - Initial handshake completed
787
+
788
+ ### ActionNetManagerGUI Events
789
+
790
+ - `joinedRoom`: `(roomName) => {}` - Joined room
791
+ - `leftRoom`: `(roomName) => {}` - Left room
792
+ - `buttonPressed`: `() => {}` - Button pressed (for sound effects)
793
+ - `back`: `() => {}` - Back button pressed
794
+ - `backToLogin`: `() => {}` - Back to login screen
795
+ - `selectionChanged`: `({oldIndex, newIndex}) => {}` - Selection changed
796
+ - `disconnected`: `() => {}` - Network disconnected
797
+ - `message:{type}`: `(msg) => {}` - Custom message (emitted for unhandled types)
798
+
799
+ ### SyncSystem Events
800
+
801
+ - `remoteUpdated`: `(remoteData) => {}` - Remote data received
802
+ - `remoteStale`: `() => {}` - Remote stopped sending updates
803
+ - `remoteFresh`: `() => {}` - Remote resumed after being stale
804
+ - `broadcast`: `(localData) => {}` - We broadcasted data
805
+
806
+ ## See Also
807
+
808
+ - Server example: `game/server/ActionNetServer.js`
809
+ - Server README: `game/server/README.md`
810
+ - Run server: `game/server/start.bat` (Windows) or `game/server/start.sh` (Mac/Linux)