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.
- package/LICENSE +45 -0
- package/README.md +348 -0
- package/actionengine/3rdparty/goblin/goblin.js +9609 -0
- package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
- package/actionengine/camera/actioncamera.js +90 -0
- package/actionengine/camera/cameracollisionhandler.js +69 -0
- package/actionengine/character/actioncharacter.js +360 -0
- package/actionengine/character/actioncharacter3D.js +61 -0
- package/actionengine/core/app.js +430 -0
- package/actionengine/debug/basedebugpanel.js +858 -0
- package/actionengine/display/canvasmanager.js +75 -0
- package/actionengine/display/gl/programmanager.js +570 -0
- package/actionengine/display/gl/shaders/lineshader.js +118 -0
- package/actionengine/display/gl/shaders/objectshader.js +1756 -0
- package/actionengine/display/gl/shaders/particleshader.js +43 -0
- package/actionengine/display/gl/shaders/shadowshader.js +319 -0
- package/actionengine/display/gl/shaders/spriteshader.js +100 -0
- package/actionengine/display/gl/shaders/watershader.js +67 -0
- package/actionengine/display/graphics/actionmodel3D.js +191 -0
- package/actionengine/display/graphics/actionsprite3D.js +230 -0
- package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
- package/actionengine/display/graphics/lighting/actionlight.js +211 -0
- package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
- package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
- package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
- package/actionengine/display/graphics/renderableobject.js +44 -0
- package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
- package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
- package/actionengine/display/graphics/texture/texturemanager.js +242 -0
- package/actionengine/display/graphics/texture/textureregistry.js +177 -0
- package/actionengine/input/actionscrollablearea.js +1405 -0
- package/actionengine/input/inputhandler.js +1647 -0
- package/actionengine/math/geometry/geometrybuilder.js +161 -0
- package/actionengine/math/geometry/glbexporter.js +364 -0
- package/actionengine/math/geometry/glbloader.js +722 -0
- package/actionengine/math/geometry/modelcodegenerator.js +97 -0
- package/actionengine/math/geometry/triangle.js +33 -0
- package/actionengine/math/geometry/triangleutils.js +34 -0
- package/actionengine/math/mathutils.js +25 -0
- package/actionengine/math/matrix4.js +785 -0
- package/actionengine/math/physics/actionphysics.js +108 -0
- package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
- package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
- package/actionengine/math/physics/actionraycast.js +129 -0
- package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
- package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
- package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
- package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
- package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
- package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
- package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
- package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
- package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
- package/actionengine/math/quaternion.js +61 -0
- package/actionengine/math/vector2.js +277 -0
- package/actionengine/math/vector3.js +318 -0
- package/actionengine/math/viewfrustum.js +136 -0
- package/actionengine/network/ACTIONNETREADME.md +810 -0
- package/actionengine/network/client/ActionNetManager.js +802 -0
- package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
- package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
- package/actionengine/network/client/SyncSystem.js +422 -0
- package/actionengine/network/p2p/ActionNetPeer.js +142 -0
- package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
- package/actionengine/network/p2p/DataConnection.js +282 -0
- package/actionengine/network/p2p/README.md +510 -0
- package/actionengine/network/p2p/example.html +502 -0
- package/actionengine/network/server/ActionNetServer.js +577 -0
- package/actionengine/network/server/ActionNetServerSSL.js +579 -0
- package/actionengine/network/server/ActionNetServerUtils.js +458 -0
- package/actionengine/network/server/SERVERREADME.md +314 -0
- package/actionengine/network/server/package-lock.json +35 -0
- package/actionengine/network/server/package.json +13 -0
- package/actionengine/network/server/start.bat +27 -0
- package/actionengine/network/server/start.sh +25 -0
- package/actionengine/network/server/startwss.bat +27 -0
- package/actionengine/sound/audiomanager.js +1589 -0
- package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
- package/actionengine/sound/soundfont/actionparser.js +718 -0
- package/actionengine/sound/soundfont/actionreverb.js +252 -0
- package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
- package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
- package/actionengine/sound/soundfont/soundfont.js +2 -0
- package/dist/action-engine.min.js +328 -0
- 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
|
+
}
|