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,1709 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActionNetManagerGUI - Bridge component for networking setup and lobby UI.
|
|
3
|
+
*
|
|
4
|
+
* Handles connection to server, login UI, and lobby UI using observer pattern to communicate with the main game.
|
|
5
|
+
* Hands off control when user joins a room (game takes over).
|
|
6
|
+
* Provides access to ActionNetManager for client info.
|
|
7
|
+
* Games should provide their own title screen and integrate this component for multiplayer features.
|
|
8
|
+
*/
|
|
9
|
+
class ActionNetManagerGUI {
|
|
10
|
+
static WIDTH = 800;
|
|
11
|
+
static HEIGHT = 600;
|
|
12
|
+
|
|
13
|
+
// Network configuration - matches Game.NETWORK_CONFIG
|
|
14
|
+
static NETWORK_CONFIG = {
|
|
15
|
+
hostname: window.location.hostname, // Auto-detect from current page
|
|
16
|
+
protocol: window.location.protocol === 'https:' ? 'wss:' : 'ws:', // Auto-detect protocol
|
|
17
|
+
autoConnect: false,
|
|
18
|
+
reconnect: true,
|
|
19
|
+
reconnectDelay: 1000,
|
|
20
|
+
maxReconnectDelay: 10000,
|
|
21
|
+
reconnectAttempts: 5,
|
|
22
|
+
pingInterval: 30000,
|
|
23
|
+
debug: true
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// P2P Network configuration
|
|
27
|
+
static P2P_NETWORK_CONFIG = {
|
|
28
|
+
gameId: 'game-id-00000',
|
|
29
|
+
debug: true
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
constructor(canvases, input, audio, configOrPort = 8000, networkConfig = null, syncConfig = null) {
|
|
33
|
+
// Store Action Engine systems
|
|
34
|
+
this.audio = audio;
|
|
35
|
+
this.input = input;
|
|
36
|
+
|
|
37
|
+
// Canvas references
|
|
38
|
+
this.gameCanvas = canvases.gameCanvas;
|
|
39
|
+
this.guiCanvas = canvases.guiCanvas;
|
|
40
|
+
this.debugCanvas = canvases.debugCanvas;
|
|
41
|
+
|
|
42
|
+
// Context references
|
|
43
|
+
this.gameCtx = this.gameCanvas.getContext("2d");
|
|
44
|
+
this.guiCtx = canvases.guiCtx;
|
|
45
|
+
this.debugCtx = canvases.debugCtx;
|
|
46
|
+
|
|
47
|
+
// Detect if configOrPort is a config object or a port number
|
|
48
|
+
let mode = 'websocket';
|
|
49
|
+
let port = 8000;
|
|
50
|
+
let p2pConfig = null;
|
|
51
|
+
|
|
52
|
+
if (typeof configOrPort === 'object' && configOrPort !== null) {
|
|
53
|
+
// It's a config object
|
|
54
|
+
mode = configOrPort.mode || 'websocket';
|
|
55
|
+
port = configOrPort.port || 8000;
|
|
56
|
+
p2pConfig = configOrPort.p2pConfig || null;
|
|
57
|
+
} else if (typeof configOrPort === 'number') {
|
|
58
|
+
// It's the old style (port number)
|
|
59
|
+
port = configOrPort;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Store mode for later use
|
|
63
|
+
this.networkMode = mode;
|
|
64
|
+
|
|
65
|
+
// Initialize networking based on mode
|
|
66
|
+
if (mode === 'p2p') {
|
|
67
|
+
const config = p2pConfig || { ...ActionNetManagerGUI.P2P_NETWORK_CONFIG };
|
|
68
|
+
this.networkManager = new ActionNetManagerP2P(config);
|
|
69
|
+
} else {
|
|
70
|
+
// WebSocket mode (default)
|
|
71
|
+
const config = networkConfig || { ...ActionNetManagerGUI.NETWORK_CONFIG };
|
|
72
|
+
|
|
73
|
+
// Build URL from hostname, port, and protocol
|
|
74
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
75
|
+
const hostname = window.location.hostname || 'localhost'; // Fallback to localhost for file:// protocol
|
|
76
|
+
config.url = `${protocol}//${hostname}:${port}`;
|
|
77
|
+
|
|
78
|
+
this.networkManager = new ActionNetManager(config);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Setup ActionNet event listeners
|
|
82
|
+
this.setupNetworkEvents();
|
|
83
|
+
|
|
84
|
+
// Initialize SyncSystem for automatic state synchronization
|
|
85
|
+
const defaultSyncConfig = {
|
|
86
|
+
send: (msg) => {
|
|
87
|
+
// Only send if connected and in room
|
|
88
|
+
if (this.networkManager.isConnected() && this.networkManager.isInRoom()) {
|
|
89
|
+
this.networkManager.send(msg);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
broadcastInterval: 16, // ~60fps
|
|
93
|
+
staleThreshold: 200 // ~12 frames
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
this.syncSystem = new SyncSystem({
|
|
97
|
+
...defaultSyncConfig,
|
|
98
|
+
...syncConfig
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Custom message handlers (for one-shot actions like garbageSent)
|
|
102
|
+
this.customMessageHandlers = new Map();
|
|
103
|
+
|
|
104
|
+
// Setup message routing from ActionNetManager
|
|
105
|
+
this.setupMessageRouting();
|
|
106
|
+
|
|
107
|
+
// Event handlers for observer pattern
|
|
108
|
+
this.handlers = new Map();
|
|
109
|
+
|
|
110
|
+
// Application state - start with login (no title screen)
|
|
111
|
+
this.currentState = "LOGIN"; // LOGIN, LOBBY
|
|
112
|
+
this.username = "";
|
|
113
|
+
this.availableRooms = [];
|
|
114
|
+
this.selectedRoom = null;
|
|
115
|
+
|
|
116
|
+
// Navigation state for keyboard/gamepad
|
|
117
|
+
this.selectedIndex = 0; // For button navigation
|
|
118
|
+
this.loginButtonCount = 2; // Connect, Back
|
|
119
|
+
this.lobbyButtonCount = 3; // Create Room, Change Name, Back
|
|
120
|
+
|
|
121
|
+
// Track scroll state for refresh optimization
|
|
122
|
+
this.lastRoomCount = -1;
|
|
123
|
+
this.lastScrollOffset = 0;
|
|
124
|
+
|
|
125
|
+
// P2P connection spinner state
|
|
126
|
+
this.isConnecting = false;
|
|
127
|
+
this.spinnerFrame = 0;
|
|
128
|
+
|
|
129
|
+
// Connection in progress flag to prevent multiple clicks
|
|
130
|
+
this.connectionInProgress = false;
|
|
131
|
+
|
|
132
|
+
// Create scrollable room list
|
|
133
|
+
this.roomScroller = new ActionScrollableArea({
|
|
134
|
+
listAreaX: 250,
|
|
135
|
+
listAreaY: 380,
|
|
136
|
+
listAreaWidth: 300,
|
|
137
|
+
listAreaHeight: 200,
|
|
138
|
+
itemHeight: 30,
|
|
139
|
+
scrollBarX: 552,
|
|
140
|
+
scrollBarY: 400,
|
|
141
|
+
scrollBarTrackHeight: 160,
|
|
142
|
+
scrollBarThumbStartY: 400,
|
|
143
|
+
|
|
144
|
+
// Enable clipping for precise bounds control
|
|
145
|
+
enableClipping: true,
|
|
146
|
+
clipBounds: {
|
|
147
|
+
x: 250,
|
|
148
|
+
y: 380,
|
|
149
|
+
width: 300,
|
|
150
|
+
height: 200
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// Let ActionScrollableArea handle input registration automatically with clipping
|
|
154
|
+
generateItemId: (item, index) => `room_item_${index}`,
|
|
155
|
+
|
|
156
|
+
// Custom styling for monochrome theme
|
|
157
|
+
colors: {
|
|
158
|
+
track: { normal: "rgba(0, 0, 0, 0.2)", hover: "rgba(0, 0, 0, 0.3)" },
|
|
159
|
+
thumb: {
|
|
160
|
+
normal: "rgba(136, 136, 136, 0.3)",
|
|
161
|
+
hover: "rgba(136, 136, 136, 0.6)",
|
|
162
|
+
drag: "rgba(136, 136, 136, 0.8)"
|
|
163
|
+
},
|
|
164
|
+
thumbBorder: { normal: "rgba(136, 136, 136, 0.5)", drag: "#ffffff" },
|
|
165
|
+
button: {
|
|
166
|
+
normal: "rgba(136, 136, 136, 0.1)",
|
|
167
|
+
hover: "rgba(136, 136, 136, 0.3)"
|
|
168
|
+
},
|
|
169
|
+
buttonText: {
|
|
170
|
+
normal: "rgba(136, 136, 136, 0.8)",
|
|
171
|
+
hover: "#ffffff"
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// Enable background drawing with monochrome styling
|
|
176
|
+
drawBackground: true,
|
|
177
|
+
backgroundColor: "rgba(26, 26, 26, 0.9)",
|
|
178
|
+
borderColor: "rgba(136, 136, 136, 0.6)",
|
|
179
|
+
borderWidth: 2,
|
|
180
|
+
cornerRadius: 0,
|
|
181
|
+
padding: 5
|
|
182
|
+
}, this.input, this.guiCtx);
|
|
183
|
+
|
|
184
|
+
// UI state for text input
|
|
185
|
+
this.inputFocus = null; // 'username' or null
|
|
186
|
+
this.textInputCursor = 0;
|
|
187
|
+
this.textInputBlinkTime = 0;
|
|
188
|
+
|
|
189
|
+
// Server status tracking
|
|
190
|
+
this.serverStatus = 'UNKNOWN';
|
|
191
|
+
this.serverStatusColor = '#ffff00';
|
|
192
|
+
this.serverCheckInterval = null;
|
|
193
|
+
|
|
194
|
+
// Error modal state
|
|
195
|
+
this.errorModalVisible = false;
|
|
196
|
+
this.errorModalTitle = '';
|
|
197
|
+
this.errorModalMessage = '';
|
|
198
|
+
|
|
199
|
+
// Join modal state
|
|
200
|
+
this.joinModalVisible = false;
|
|
201
|
+
this.joinModalStatus = ''; // 'contactingHost', 'offerSent', 'acceptedByHost', 'establishingConnection', 'connected'
|
|
202
|
+
this.joinModalHostPeerId = null;
|
|
203
|
+
|
|
204
|
+
// Spinner animation state
|
|
205
|
+
this.spinnerRotation = 0;
|
|
206
|
+
|
|
207
|
+
// Initialize UI elements
|
|
208
|
+
this.initializeUIElements();
|
|
209
|
+
|
|
210
|
+
// Register input elements
|
|
211
|
+
this.registerUIElements();
|
|
212
|
+
|
|
213
|
+
// console.log("[ActionNetManagerGUI] Initialization completed");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Register an event handler for observer pattern
|
|
218
|
+
*/
|
|
219
|
+
on(event, handler) {
|
|
220
|
+
if (!this.handlers.has(event)) {
|
|
221
|
+
this.handlers.set(event, []);
|
|
222
|
+
}
|
|
223
|
+
this.handlers.get(event).push(handler);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Remove an event handler
|
|
228
|
+
*/
|
|
229
|
+
off(event, handler) {
|
|
230
|
+
if (!this.handlers.has(event)) return;
|
|
231
|
+
const handlers = this.handlers.get(event);
|
|
232
|
+
const index = handlers.indexOf(handler);
|
|
233
|
+
if (index > -1) {
|
|
234
|
+
handlers.splice(index, 1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Emit an event to all registered handlers
|
|
240
|
+
*/
|
|
241
|
+
emit(event, ...args) {
|
|
242
|
+
if (!this.handlers.has(event)) return;
|
|
243
|
+
const handlers = this.handlers.get(event);
|
|
244
|
+
handlers.forEach(handler => {
|
|
245
|
+
try {
|
|
246
|
+
handler(...args);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error('[ActionNetManagerGUI] Error in event handler:', error);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Setup message routing from ActionNetManager to SyncSystem and custom handlers
|
|
255
|
+
*/
|
|
256
|
+
setupMessageRouting() {
|
|
257
|
+
// Route ALL messages through our system
|
|
258
|
+
this.networkManager.on('message', (message) => {
|
|
259
|
+
// Automatic routing: syncUpdate → SyncSystem
|
|
260
|
+
if (message.type === 'syncUpdate') {
|
|
261
|
+
if (this.syncSystem) {
|
|
262
|
+
this.syncSystem.handleSyncUpdate(message);
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Custom handler routing
|
|
268
|
+
if (this.customMessageHandlers.has(message.type)) {
|
|
269
|
+
const handler = this.customMessageHandlers.get(message.type);
|
|
270
|
+
try {
|
|
271
|
+
handler(message);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error(`[ActionNetManagerGUI] Error in custom handler '${message.type}':`, error);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// If no handler found, emit as custom event for developer to catch
|
|
279
|
+
this.emit(`message:${message.type}`, message);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Setup ActionNet event listeners
|
|
285
|
+
*/
|
|
286
|
+
setupNetworkEvents() {
|
|
287
|
+
// Connection events
|
|
288
|
+
this.networkManager.on("connected", () => {
|
|
289
|
+
// console.log("[ActionNetManagerGUI] Connected to server");
|
|
290
|
+
this.serverStatus = 'ONLINE';
|
|
291
|
+
this.serverStatusColor = '#00ff00';
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
this.networkManager.on("disconnected", () => {
|
|
295
|
+
// console.log("[ActionNetManagerGUI] Disconnected from server");
|
|
296
|
+
this.emit('disconnected');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
this.networkManager.on("reconnecting", ({ attempt, delay }) => {
|
|
300
|
+
// console.log(`[ActionNetManagerGUI] Reconnecting... attempt ${attempt}, waiting ${delay}ms`);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
this.networkManager.on("error", (error) => {
|
|
304
|
+
console.error("[ActionNetManagerGUI] Network error:", error);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
this.networkManager.on("roomList", (rooms) => {
|
|
308
|
+
this.availableRooms = rooms;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
this.networkManager.on("joinedRoom", (roomName) => {
|
|
312
|
+
// Don't emit immediately - let join modal finish if visible
|
|
313
|
+
if (this.joinModalVisible) {
|
|
314
|
+
// Modal will emit this after it closes
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
this.emit('joinedRoom', roomName);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
this.networkManager.on("leftRoom", (roomName) => {
|
|
321
|
+
// console.log("[ActionNetManagerGUI] Left room:", roomName);
|
|
322
|
+
|
|
323
|
+
// Stop syncing and clear remote data when leaving room
|
|
324
|
+
if (this.syncSystem) {
|
|
325
|
+
this.syncSystem.stop();
|
|
326
|
+
this.syncSystem.clearRemoteData();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this.emit('leftRoom', roomName);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
this.networkManager.on("userList", (users) => {
|
|
333
|
+
// Update connected users if needed
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Initialize UI elements
|
|
339
|
+
*/
|
|
340
|
+
initializeUIElements() {
|
|
341
|
+
// Login screen elements
|
|
342
|
+
this.connectButton = {
|
|
343
|
+
x: 280,
|
|
344
|
+
y: 220,
|
|
345
|
+
width: 240,
|
|
346
|
+
height: 60
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
this.backButton = {
|
|
350
|
+
x: 280,
|
|
351
|
+
y: 300,
|
|
352
|
+
width: 240,
|
|
353
|
+
height: 60
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Lobby screen elements
|
|
357
|
+
this.createRoomButton = {
|
|
358
|
+
x: 280,
|
|
359
|
+
y: 220,
|
|
360
|
+
width: 240,
|
|
361
|
+
height: 60
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
this.changeNameButton = {
|
|
365
|
+
x: 280,
|
|
366
|
+
y: 140,
|
|
367
|
+
width: 240,
|
|
368
|
+
height: 60
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
this.backToLoginButton = {
|
|
372
|
+
x: 280,
|
|
373
|
+
y: 300,
|
|
374
|
+
width: 240,
|
|
375
|
+
height: 60
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Text input
|
|
379
|
+
this.chatText = "";
|
|
380
|
+
this.inputFocus = null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Register UI elements with input system
|
|
385
|
+
*/
|
|
386
|
+
registerUIElements() {
|
|
387
|
+
// Register connect button
|
|
388
|
+
this.input.registerElement("connectButton", {
|
|
389
|
+
bounds: () => ({
|
|
390
|
+
x: this.connectButton.x,
|
|
391
|
+
y: this.connectButton.y,
|
|
392
|
+
width: this.connectButton.width,
|
|
393
|
+
height: this.connectButton.height
|
|
394
|
+
})
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Register back button
|
|
398
|
+
this.input.registerElement("backButton", {
|
|
399
|
+
bounds: () => ({
|
|
400
|
+
x: this.backButton.x,
|
|
401
|
+
y: this.backButton.y,
|
|
402
|
+
width: this.backButton.width,
|
|
403
|
+
height: this.backButton.height
|
|
404
|
+
})
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Register create room button
|
|
408
|
+
this.input.registerElement("createRoomButton", {
|
|
409
|
+
bounds: () => ({
|
|
410
|
+
x: this.createRoomButton.x,
|
|
411
|
+
y: this.createRoomButton.y,
|
|
412
|
+
width: this.createRoomButton.width,
|
|
413
|
+
height: this.createRoomButton.height
|
|
414
|
+
})
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Register change name button
|
|
418
|
+
this.input.registerElement("changeNameButton", {
|
|
419
|
+
bounds: () => ({
|
|
420
|
+
x: this.changeNameButton.x,
|
|
421
|
+
y: this.changeNameButton.y,
|
|
422
|
+
width: this.changeNameButton.width,
|
|
423
|
+
height: this.changeNameButton.height
|
|
424
|
+
})
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Register back to login button
|
|
428
|
+
this.input.registerElement("backToLoginButton", {
|
|
429
|
+
bounds: () => ({
|
|
430
|
+
x: this.backToLoginButton.x,
|
|
431
|
+
y: this.backToLoginButton.y,
|
|
432
|
+
width: this.backToLoginButton.width,
|
|
433
|
+
height: this.backToLoginButton.height
|
|
434
|
+
})
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Main update method
|
|
440
|
+
*/
|
|
441
|
+
action_update(deltaTime) {
|
|
442
|
+
switch (this.currentState) {
|
|
443
|
+
case "LOGIN":
|
|
444
|
+
this.updateLogin();
|
|
445
|
+
break;
|
|
446
|
+
case "LOBBY":
|
|
447
|
+
this.updateLobby();
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Update spinner rotation
|
|
452
|
+
this.spinnerRotation = (this.spinnerRotation + 1) % 360; // Rotate 6 degrees per frame
|
|
453
|
+
|
|
454
|
+
// Update spinner frame for P2P connection
|
|
455
|
+
if (this.isConnecting) {
|
|
456
|
+
this.spinnerFrame++;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Update network manager
|
|
460
|
+
this.networkManager.update();
|
|
461
|
+
|
|
462
|
+
// Handle UI input
|
|
463
|
+
this.handleUIInput();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Main render method
|
|
468
|
+
*/
|
|
469
|
+
action_draw() {
|
|
470
|
+
switch (this.currentState) {
|
|
471
|
+
case 'LOGIN':
|
|
472
|
+
this.renderLoginScreen();
|
|
473
|
+
break;
|
|
474
|
+
case 'LOBBY':
|
|
475
|
+
this.renderLobbyScreen();
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Render join modal on top if visible
|
|
480
|
+
if (this.joinModalVisible) {
|
|
481
|
+
this.renderJoinModal();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Render error modal on top if visible
|
|
485
|
+
if (this.errorModalVisible) {
|
|
486
|
+
this.renderErrorModal();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Render login screen
|
|
492
|
+
*/
|
|
493
|
+
renderLoginScreen() {
|
|
494
|
+
this.renderLabel('ActionNet Login', ActionNetManagerGUI.WIDTH / 2, 150, '36px Arial', '#808080');
|
|
495
|
+
|
|
496
|
+
// Draw connect button
|
|
497
|
+
this.renderButton(this.connectButton, 'Connect', this.selectedIndex === 0);
|
|
498
|
+
|
|
499
|
+
// Draw back button
|
|
500
|
+
this.renderButton(this.backButton, 'Back', this.selectedIndex === 1);
|
|
501
|
+
|
|
502
|
+
// Draw network status only for WebSocket mode (P2P uses DHT, not centralized server)
|
|
503
|
+
if (this.networkMode !== 'p2p') {
|
|
504
|
+
this.renderLabel(`Network connection: ${this.serverStatus}`, ActionNetManagerGUI.WIDTH / 2, 430, '14px Arial', this.serverStatusColor);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Show spinner and "Connecting..." message for P2P mode
|
|
508
|
+
if (this.networkMode === 'p2p' && this.isConnecting) {
|
|
509
|
+
this.renderLabel('Connecting...', ActionNetManagerGUI.WIDTH / 2, 410);
|
|
510
|
+
this.renderSpinner(ActionNetManagerGUI.WIDTH / 2, 450, 20, 3);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Render lobby screen
|
|
516
|
+
*/
|
|
517
|
+
renderLobbyScreen() {
|
|
518
|
+
// Render peer count in bottom right
|
|
519
|
+
if (this.networkMode === 'p2p') {
|
|
520
|
+
const connectedCount = this.networkManager.getConnectedPeerCount();
|
|
521
|
+
const discoveredCount = this.networkManager.getDiscoveredPeerCount();
|
|
522
|
+
const peerLabel = `Connected: ${connectedCount} | Online: ${discoveredCount}`;
|
|
523
|
+
this.renderLabel(peerLabel, ActionNetManagerGUI.WIDTH - 10, ActionNetManagerGUI.HEIGHT - 10, '12px Arial', '#888888', 'right');
|
|
524
|
+
}
|
|
525
|
+
this.renderLabel('ActionNet Lobby', ActionNetManagerGUI.WIDTH / 2, 40, '36px Arial', '#808080');
|
|
526
|
+
|
|
527
|
+
this.renderLabel(`Welcome, ${this.username}!`, ActionNetManagerGUI.WIDTH / 2, 85, '24px Arial', '#ffffff');
|
|
528
|
+
|
|
529
|
+
// Draw status
|
|
530
|
+
this.renderLabel('Select a room or create a new one', ActionNetManagerGUI.WIDTH / 2, 120, '14px Arial', '#cccccc');
|
|
531
|
+
|
|
532
|
+
// Draw connection status
|
|
533
|
+
// const isConnected = this.networkManager.isConnected();
|
|
534
|
+
// const connectionStatus = isConnected ? '✅ CONNECTED TO SERVER' : '❌ NOT CONNECTED';
|
|
535
|
+
// this.guiCtx.fillStyle = isConnected ? '#00ff00' : '#ff0000';
|
|
536
|
+
// this.guiCtx.fillText(`Server: ${connectionStatus}`, ActionNetManagerGUI.WIDTH / 2, 80);
|
|
537
|
+
|
|
538
|
+
// Draw change name button (index 0)
|
|
539
|
+
this.renderButton(this.changeNameButton, 'Change Name', this.selectedIndex === 0);
|
|
540
|
+
|
|
541
|
+
// Draw create room button (index 1)
|
|
542
|
+
this.renderButton(this.createRoomButton, 'Create Room', this.selectedIndex === 1);
|
|
543
|
+
|
|
544
|
+
// Draw back to login button (index 2)
|
|
545
|
+
this.renderButton(this.backToLoginButton, 'Back', this.selectedIndex === 2);
|
|
546
|
+
|
|
547
|
+
// Draw available rooms
|
|
548
|
+
this.renderRoomList();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Render button
|
|
553
|
+
*/
|
|
554
|
+
renderButton(button, text, isSelected = false) {
|
|
555
|
+
const isHovered = this.input.isElementHovered(button === this.connectButton ? 'connectButton' :
|
|
556
|
+
button === this.backButton ? 'backButton' :
|
|
557
|
+
button === this.createRoomButton ? 'createRoomButton' :
|
|
558
|
+
button === this.changeNameButton ? 'changeNameButton' :
|
|
559
|
+
'backToLoginButton');
|
|
560
|
+
|
|
561
|
+
// Check if connect button is disabled (connection in progress)
|
|
562
|
+
const isDisabled = button === this.connectButton && this.connectionInProgress;
|
|
563
|
+
|
|
564
|
+
// Highlight if selected via keyboard/gamepad or hovered via mouse
|
|
565
|
+
const isHighlighted = (isSelected || isHovered) && !isDisabled;
|
|
566
|
+
this.guiCtx.fillStyle = isDisabled ? '#222222' : (isHighlighted ? '#555555' : '#333333');
|
|
567
|
+
this.guiCtx.fillRect(button.x, button.y, button.width, button.height);
|
|
568
|
+
this.guiCtx.strokeStyle = isDisabled ? '#444444' : (isSelected ? '#ffffff' : '#888888');
|
|
569
|
+
this.guiCtx.lineWidth = isSelected ? 3 : 2;
|
|
570
|
+
this.guiCtx.strokeRect(button.x, button.y, button.width, button.height);
|
|
571
|
+
this.guiCtx.fillStyle = isDisabled ? '#666666' : '#ffffff';
|
|
572
|
+
this.guiCtx.font = 'bold 24px Arial';
|
|
573
|
+
this.guiCtx.textAlign = 'center';
|
|
574
|
+
this.guiCtx.textBaseline = 'middle';
|
|
575
|
+
this.guiCtx.fillText(text.toUpperCase(), button.x + button.width / 2, button.y + button.height / 2);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Render spinner for P2P connection
|
|
580
|
+
*/
|
|
581
|
+
renderSpinner(x, y, size = 30) {
|
|
582
|
+
const radius = size / 2;
|
|
583
|
+
const rotation = (this.spinnerFrame % 60) * (Math.PI * 2 / 60); // Full rotation every 60 frames
|
|
584
|
+
|
|
585
|
+
this.guiCtx.save();
|
|
586
|
+
this.guiCtx.translate(x, y);
|
|
587
|
+
this.guiCtx.rotate(rotation);
|
|
588
|
+
|
|
589
|
+
// Draw spinner arc
|
|
590
|
+
this.guiCtx.strokeStyle = '#ffffff';
|
|
591
|
+
this.guiCtx.lineWidth = 3;
|
|
592
|
+
this.guiCtx.lineCap = 'round';
|
|
593
|
+
this.guiCtx.beginPath();
|
|
594
|
+
this.guiCtx.arc(0, 0, radius, 0, Math.PI * 1.5); // 3/4 circle
|
|
595
|
+
this.guiCtx.stroke();
|
|
596
|
+
|
|
597
|
+
this.guiCtx.restore();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Render label with optional semi-transparent background
|
|
602
|
+
*/
|
|
603
|
+
renderLabel(text, x, y, font = '16px Arial', textColor = '#ffffff', textAlign = 'center', textBaseline = 'middle', padding = 8, drawBackground = true) {
|
|
604
|
+
// Save context state
|
|
605
|
+
this.guiCtx.save();
|
|
606
|
+
|
|
607
|
+
this.guiCtx.font = font;
|
|
608
|
+
this.guiCtx.textAlign = textAlign;
|
|
609
|
+
this.guiCtx.textBaseline = textBaseline;
|
|
610
|
+
|
|
611
|
+
if (drawBackground) {
|
|
612
|
+
const metrics = this.guiCtx.measureText(text);
|
|
613
|
+
const textWidth = metrics.width;
|
|
614
|
+
|
|
615
|
+
// Fallback-safe text height
|
|
616
|
+
const textHeightRaw = (metrics.actualBoundingBoxAscent || 0) + (metrics.actualBoundingBoxDescent || 0);
|
|
617
|
+
const textHeight = textHeightRaw || parseInt(font, 10) || 16;
|
|
618
|
+
|
|
619
|
+
const bgWidth = textWidth + padding * 2;
|
|
620
|
+
const bgHeight = textHeight + padding;
|
|
621
|
+
const cornerRadius = 4; // Rounded corners
|
|
622
|
+
|
|
623
|
+
let bgX, bgY;
|
|
624
|
+
if (textAlign === 'center') {
|
|
625
|
+
bgX = x - bgWidth / 2;
|
|
626
|
+
} else if (textAlign === 'left') {
|
|
627
|
+
bgX = x - padding;
|
|
628
|
+
} else if (textAlign === 'right') {
|
|
629
|
+
bgX = x - bgWidth + padding;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// With textBaseline = 'middle', y is the visual center of the glyphs.
|
|
633
|
+
// Center the background on that, then nudge down 1px to correct the "too high" feel.
|
|
634
|
+
bgY = y - bgHeight / 2 - 1;
|
|
635
|
+
|
|
636
|
+
// Semi-transparent dark background with rounded corners
|
|
637
|
+
this.guiCtx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
638
|
+
this.roundedRect(bgX, bgY, bgWidth, bgHeight, cornerRadius);
|
|
639
|
+
this.guiCtx.fill();
|
|
640
|
+
|
|
641
|
+
// Subtle grey border with rounded corners
|
|
642
|
+
this.guiCtx.strokeStyle = 'rgba(136, 136, 136, 0.3)';
|
|
643
|
+
this.guiCtx.lineWidth = 1;
|
|
644
|
+
this.roundedRect(bgX, bgY, bgWidth, bgHeight, cornerRadius);
|
|
645
|
+
this.guiCtx.stroke();
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Text
|
|
649
|
+
this.guiCtx.fillStyle = textColor;
|
|
650
|
+
this.guiCtx.fillText(text, x, y);
|
|
651
|
+
|
|
652
|
+
// Restore context
|
|
653
|
+
this.guiCtx.restore();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Render text with dynamic font sizing to fit within maxWidth
|
|
658
|
+
*/
|
|
659
|
+
renderWrappedText(text, x, y, maxWidth, font = '16px Arial', textColor = '#ffffff') {
|
|
660
|
+
this.guiCtx.save();
|
|
661
|
+
|
|
662
|
+
this.guiCtx.textAlign = 'center';
|
|
663
|
+
this.guiCtx.textBaseline = 'middle';
|
|
664
|
+
this.guiCtx.fillStyle = textColor;
|
|
665
|
+
|
|
666
|
+
// Extract font size from font string (e.g., "20px Arial" -> 20)
|
|
667
|
+
let fontSize = parseInt(font, 10) || 16;
|
|
668
|
+
const fontFamily = font.split(' ').slice(1).join(' ') || 'Arial';
|
|
669
|
+
|
|
670
|
+
// Reduce font size until text fits
|
|
671
|
+
let textWidth = Infinity;
|
|
672
|
+
while (textWidth > maxWidth && fontSize > 8) {
|
|
673
|
+
this.guiCtx.font = `${fontSize}px ${fontFamily}`;
|
|
674
|
+
const metrics = this.guiCtx.measureText(text);
|
|
675
|
+
textWidth = metrics.width;
|
|
676
|
+
|
|
677
|
+
if (textWidth > maxWidth) {
|
|
678
|
+
fontSize--;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Draw the text at the calculated size
|
|
683
|
+
this.guiCtx.fillText(text, x, y);
|
|
684
|
+
|
|
685
|
+
this.guiCtx.restore();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Helper method to draw rounded rectangles
|
|
690
|
+
*/
|
|
691
|
+
roundedRect(x, y, width, height, radius) {
|
|
692
|
+
const ctx = this.guiCtx;
|
|
693
|
+
ctx.beginPath();
|
|
694
|
+
ctx.moveTo(x + radius, y);
|
|
695
|
+
ctx.lineTo(x + width - radius, y);
|
|
696
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
697
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
698
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
699
|
+
ctx.lineTo(x + radius, y + height);
|
|
700
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
701
|
+
ctx.lineTo(x, y + radius);
|
|
702
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
703
|
+
ctx.closePath();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Render room list
|
|
708
|
+
*/
|
|
709
|
+
renderRoomList() {
|
|
710
|
+
const rooms = this.networkManager.getAvailableRooms();
|
|
711
|
+
|
|
712
|
+
if (rooms.length === 0) {
|
|
713
|
+
// Draw header and spinner animation
|
|
714
|
+
this.renderLabel('Searching for rooms...', ActionNetManagerGUI.WIDTH / 2, 410);
|
|
715
|
+
this.renderSpinner(ActionNetManagerGUI.WIDTH / 2, 450, 20, 3);
|
|
716
|
+
} else if (this.roomScroller) {
|
|
717
|
+
// Use the scroller for room list
|
|
718
|
+
this.roomScroller.draw(rooms, (room, index, y) => {
|
|
719
|
+
// Check if this specific room item is hovered or selected via keyboard/gamepad
|
|
720
|
+
const isHovered = this.input.isElementHovered(`room_item_${index}`) || this.roomScroller.scrollThumb.hovered;
|
|
721
|
+
const isSelected = this.selectedIndex === (this.lobbyButtonCount + index);
|
|
722
|
+
const isHighlighted = isHovered || isSelected;
|
|
723
|
+
|
|
724
|
+
// Draw room button background (matching GUI button style)
|
|
725
|
+
this.guiCtx.fillStyle = isHighlighted ? '#555555' : '#333333';
|
|
726
|
+
this.guiCtx.fillRect(260, y, 280, 30);
|
|
727
|
+
|
|
728
|
+
// Draw room button border (matching GUI button style)
|
|
729
|
+
this.guiCtx.strokeStyle = isSelected ? '#ffffff' : '#888888';
|
|
730
|
+
this.guiCtx.lineWidth = isSelected ? 3 : 2;
|
|
731
|
+
this.guiCtx.strokeRect(260, y, 280, 30);
|
|
732
|
+
|
|
733
|
+
// Draw room name and player count
|
|
734
|
+
this.guiCtx.fillStyle = '#ffffff';
|
|
735
|
+
this.guiCtx.font = '16px Arial';
|
|
736
|
+
this.guiCtx.textAlign = 'center';
|
|
737
|
+
|
|
738
|
+
// New format with player counts
|
|
739
|
+
const maxDisplay = room.maxPlayers === -1 ? '∞' : room.maxPlayers;
|
|
740
|
+
// Support both WebSocket (room.name, room.playerCount) and P2P (room.username, room.currentPlayers) formats
|
|
741
|
+
const roomName = room.name || room.username || 'Unknown Room';
|
|
742
|
+
const playerCount = room.playerCount !== undefined ? room.playerCount : room.currentPlayers || 0;
|
|
743
|
+
const displayText = `${roomName} (${playerCount}/${maxDisplay})`;
|
|
744
|
+
|
|
745
|
+
this.guiCtx.fillText(displayText, ActionNetManagerGUI.WIDTH / 2, y + 15);
|
|
746
|
+
}, {
|
|
747
|
+
renderHeader: () => {
|
|
748
|
+
this.renderLabel('Available Rooms:', ActionNetManagerGUI.WIDTH / 2, 330);
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
} else {
|
|
752
|
+
this.guiCtx.fillStyle = '#ff0000';
|
|
753
|
+
this.guiCtx.font = '20px Arial';
|
|
754
|
+
this.guiCtx.textAlign = 'center';
|
|
755
|
+
this.guiCtx.fillText('ERROR: roomScroller is null!', ActionNetManagerGUI.WIDTH / 2, ActionNetManagerGUI.HEIGHT / 2);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Update login
|
|
763
|
+
*/
|
|
764
|
+
updateLogin() {
|
|
765
|
+
if (!this.serverCheckInterval) {
|
|
766
|
+
// Perform initial check immediately when entering LOGIN state
|
|
767
|
+
(async () => {
|
|
768
|
+
try {
|
|
769
|
+
const result = await this.networkManager.testServerConnection();
|
|
770
|
+
this.serverStatus = result.available ? 'ONLINE' : 'UNAVAILABLE';
|
|
771
|
+
this.serverStatusColor = result.available ? '#00ff00' : '#ff0000';
|
|
772
|
+
} catch (error) {
|
|
773
|
+
this.serverStatus = 'UNAVAILABLE';
|
|
774
|
+
this.serverStatusColor = '#ff0000';
|
|
775
|
+
}
|
|
776
|
+
})();
|
|
777
|
+
|
|
778
|
+
// Start periodic checks
|
|
779
|
+
this.startServerCheck();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Update lobby
|
|
785
|
+
*/
|
|
786
|
+
updateLobby() {
|
|
787
|
+
// Update room list
|
|
788
|
+
this.availableRooms = this.networkManager.getAvailableRooms();
|
|
789
|
+
|
|
790
|
+
// Update scrollable room list
|
|
791
|
+
if (this.roomScroller) {
|
|
792
|
+
const currentCount = this.availableRooms.length;
|
|
793
|
+
const currentScroll = this.roomScroller.scrollOffset;
|
|
794
|
+
|
|
795
|
+
// Only refresh items when needed: initial, scroll change, or content change
|
|
796
|
+
const needsRefresh = currentCount !== this.lastRoomCount ||
|
|
797
|
+
currentScroll !== this.lastScrollOffset ||
|
|
798
|
+
this.lastRoomCount === -1;
|
|
799
|
+
|
|
800
|
+
if (needsRefresh) {
|
|
801
|
+
// Use the library's refreshItems method to handle registration properly
|
|
802
|
+
this.roomScroller.refreshItems(this.availableRooms, 'gui');
|
|
803
|
+
|
|
804
|
+
// Update tracking
|
|
805
|
+
this.lastRoomCount = currentCount;
|
|
806
|
+
this.lastScrollOffset = currentScroll;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
this.roomScroller.update(this.availableRooms.length, 0.016);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Handle UI input
|
|
815
|
+
*/
|
|
816
|
+
handleUIInput() {
|
|
817
|
+
// Handle join modal input first (blocks other input when visible)
|
|
818
|
+
if (this.joinModalVisible) {
|
|
819
|
+
this.handleJoinModalInput();
|
|
820
|
+
return; // Modal blocks other input
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Handle error modal input (blocks other input when visible)
|
|
824
|
+
if (this.errorModalVisible) {
|
|
825
|
+
this.handleErrorModalInput();
|
|
826
|
+
return; // Modal blocks other input
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
switch (this.currentState) {
|
|
830
|
+
case "LOGIN":
|
|
831
|
+
// Handle keyboard/gamepad navigation for LOGIN screen
|
|
832
|
+
if (this.input.isKeyJustPressed('DirUp') ||
|
|
833
|
+
this.input.isGamepadButtonJustPressed(12, 0) || this.input.isGamepadButtonJustPressed(12, 1) ||
|
|
834
|
+
this.input.isGamepadButtonJustPressed(12, 2) || this.input.isGamepadButtonJustPressed(12, 3)) {
|
|
835
|
+
const old = this.selectedIndex;
|
|
836
|
+
const next = Math.max(0, old - 1);
|
|
837
|
+
if (next !== old) {
|
|
838
|
+
this.selectedIndex = next;
|
|
839
|
+
this.emit('selectionChanged', { oldIndex: old, newIndex: next });
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (this.input.isKeyJustPressed('DirDown') ||
|
|
843
|
+
this.input.isGamepadButtonJustPressed(13, 0) || this.input.isGamepadButtonJustPressed(13, 1) ||
|
|
844
|
+
this.input.isGamepadButtonJustPressed(13, 2) || this.input.isGamepadButtonJustPressed(13, 3)) {
|
|
845
|
+
const old = this.selectedIndex;
|
|
846
|
+
const next = Math.min(this.loginButtonCount - 1, old + 1);
|
|
847
|
+
if (next !== old) {
|
|
848
|
+
this.selectedIndex = next;
|
|
849
|
+
this.emit('selectionChanged', { oldIndex: old, newIndex: next });
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
// Confirm with Action1 (Enter/A button)
|
|
853
|
+
if (this.input.isKeyJustPressed('Action1') ||
|
|
854
|
+
this.input.isGamepadButtonJustPressed(0, 0) || this.input.isGamepadButtonJustPressed(0, 1) ||
|
|
855
|
+
this.input.isGamepadButtonJustPressed(0, 2) || this.input.isGamepadButtonJustPressed(0, 3)) {
|
|
856
|
+
|
|
857
|
+
// Explicit handling:
|
|
858
|
+
// - Index 0: positive/forward action → emit buttonPressed (menu_confirm).
|
|
859
|
+
// - Index 1: back/cancel action → emit back only (menu_back handled by game), no confirm.
|
|
860
|
+
if (this.selectedIndex === 0) {
|
|
861
|
+
if (!this.connectionInProgress) {
|
|
862
|
+
this.emit('buttonPressed');
|
|
863
|
+
this.startConnection();
|
|
864
|
+
}
|
|
865
|
+
} else if (this.selectedIndex === 1) {
|
|
866
|
+
this.emit('back');
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Back with Action2 (Escape/B button)
|
|
871
|
+
if (this.input.isKeyJustPressed('Action2') ||
|
|
872
|
+
this.input.isGamepadButtonJustPressed(1, 0) || this.input.isGamepadButtonJustPressed(1, 1) ||
|
|
873
|
+
this.input.isGamepadButtonJustPressed(1, 2) || this.input.isGamepadButtonJustPressed(1, 3)) {
|
|
874
|
+
|
|
875
|
+
// This is a pure back/cancel: no confirm sound.
|
|
876
|
+
// Disconnect if connected before going back
|
|
877
|
+
if (this.networkManager.isConnected()) {
|
|
878
|
+
this.networkManager.disconnect();
|
|
879
|
+
}
|
|
880
|
+
this.emit('back');
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Mouse input
|
|
884
|
+
if (this.input.isElementJustPressed("connectButton")) {
|
|
885
|
+
if (!this.connectionInProgress) {
|
|
886
|
+
// Positive/forward: confirm sound via buttonPressed.
|
|
887
|
+
this.emit('buttonPressed');
|
|
888
|
+
this.startConnection();
|
|
889
|
+
}
|
|
890
|
+
} else if (this.input.isElementJustPressed("backButton")) {
|
|
891
|
+
// Disconnect if connected before going back
|
|
892
|
+
if (this.networkManager.isConnected()) {
|
|
893
|
+
this.networkManager.disconnect();
|
|
894
|
+
}
|
|
895
|
+
// Emit back event so game can return to title screen
|
|
896
|
+
this.emit('back');
|
|
897
|
+
}
|
|
898
|
+
// Update selection based on hover
|
|
899
|
+
if (this.input.isElementHovered("connectButton")) {
|
|
900
|
+
if (this.selectedIndex !== 0) {
|
|
901
|
+
const old = this.selectedIndex;
|
|
902
|
+
this.selectedIndex = 0;
|
|
903
|
+
this.emit('selectionChanged', { oldIndex: old, newIndex: 0 });
|
|
904
|
+
}
|
|
905
|
+
} else if (this.input.isElementHovered("backButton")) {
|
|
906
|
+
if (this.selectedIndex !== 1) {
|
|
907
|
+
const old = this.selectedIndex;
|
|
908
|
+
this.selectedIndex = 1;
|
|
909
|
+
this.emit('selectionChanged', { oldIndex: old, newIndex: 1 });
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
break;
|
|
913
|
+
case "LOBBY":
|
|
914
|
+
const availableRooms = this.networkManager.getAvailableRooms();
|
|
915
|
+
const totalSelectableItems = this.lobbyButtonCount + availableRooms.length;
|
|
916
|
+
|
|
917
|
+
// Handle keyboard/gamepad navigation for LOBBY screen
|
|
918
|
+
if (this.input.isKeyJustPressed('DirUp') ||
|
|
919
|
+
this.input.isGamepadButtonJustPressed(12, 0) || this.input.isGamepadButtonJustPressed(12, 1) ||
|
|
920
|
+
this.input.isGamepadButtonJustPressed(12, 2) || this.input.isGamepadButtonJustPressed(12, 3)) {
|
|
921
|
+
const old = this.selectedIndex;
|
|
922
|
+
const next = Math.max(0, old - 1);
|
|
923
|
+
if (next !== old) {
|
|
924
|
+
this.selectedIndex = next;
|
|
925
|
+
this.emit('selectionChanged', { oldIndex: old, newIndex: next });
|
|
926
|
+
this.scrollToSelectedItem();
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (this.input.isKeyJustPressed('DirDown') ||
|
|
930
|
+
this.input.isGamepadButtonJustPressed(13, 0) || this.input.isGamepadButtonJustPressed(13, 1) ||
|
|
931
|
+
this.input.isGamepadButtonJustPressed(13, 2) || this.input.isGamepadButtonJustPressed(13, 3)) {
|
|
932
|
+
const old = this.selectedIndex;
|
|
933
|
+
const next = Math.min(totalSelectableItems - 1, old + 1);
|
|
934
|
+
if (next !== old) {
|
|
935
|
+
this.selectedIndex = next;
|
|
936
|
+
this.emit('selectionChanged', { oldIndex: old, newIndex: next });
|
|
937
|
+
this.scrollToSelectedItem();
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Confirm with Action1 (Enter/A button)
|
|
942
|
+
if (this.input.isKeyJustPressed('Action1') ||
|
|
943
|
+
this.input.isGamepadButtonJustPressed(0, 0) || this.input.isGamepadButtonJustPressed(0, 1) ||
|
|
944
|
+
this.input.isGamepadButtonJustPressed(0, 2) || this.input.isGamepadButtonJustPressed(0, 3)) {
|
|
945
|
+
if (this.selectedIndex === 0) {
|
|
946
|
+
this.emit('buttonPressed');
|
|
947
|
+
this.changeUsername();
|
|
948
|
+
} else if (this.selectedIndex === 1) {
|
|
949
|
+
this.emit('buttonPressed');
|
|
950
|
+
this.createAndJoinRoom();
|
|
951
|
+
} else if (this.selectedIndex === 2) {
|
|
952
|
+
this.emit('backToLogin');
|
|
953
|
+
this.currentState = "LOGIN";
|
|
954
|
+
this.selectedIndex = 0; // Reset selection
|
|
955
|
+
} else {
|
|
956
|
+
// Room selection (index 3+)
|
|
957
|
+
const roomIndex = this.selectedIndex - this.lobbyButtonCount;
|
|
958
|
+
if (roomIndex >= 0 && roomIndex < availableRooms.length) {
|
|
959
|
+
console.log("✅ Room selected via keyboard/gamepad:", availableRooms[roomIndex]);
|
|
960
|
+
this.emit('buttonPressed');
|
|
961
|
+
// Support both WebSocket (name) and P2P (peerId) formats
|
|
962
|
+
this.selectedRoom = availableRooms[roomIndex].peerId || availableRooms[roomIndex].name;
|
|
963
|
+
this.joinSelectedRoom();
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Back with Action2 (Escape/B button)
|
|
969
|
+
if (this.input.isKeyJustPressed('Action2') ||
|
|
970
|
+
this.input.isGamepadButtonJustPressed(1, 0) || this.input.isGamepadButtonJustPressed(1, 1) ||
|
|
971
|
+
this.input.isGamepadButtonJustPressed(1, 2) || this.input.isGamepadButtonJustPressed(1, 3)) {
|
|
972
|
+
// Disconnect when going back from lobby
|
|
973
|
+
if (this.networkManager.isConnected()) {
|
|
974
|
+
this.networkManager.disconnect();
|
|
975
|
+
}
|
|
976
|
+
this.connectionInProgress = false; // Reset button state
|
|
977
|
+
this.emit('backToLogin');
|
|
978
|
+
this.currentState = "LOGIN";
|
|
979
|
+
this.selectedIndex = 0; // Reset selection
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Mouse input
|
|
983
|
+
if (this.input.isElementJustPressed("createRoomButton")) {
|
|
984
|
+
this.emit('buttonPressed');
|
|
985
|
+
this.createAndJoinRoom();
|
|
986
|
+
} else if (this.input.isElementJustPressed("changeNameButton")) {
|
|
987
|
+
this.emit('buttonPressed');
|
|
988
|
+
this.changeUsername();
|
|
989
|
+
} else if (this.input.isElementJustPressed("backToLoginButton")) {
|
|
990
|
+
// Disconnect when going back from lobby
|
|
991
|
+
if (this.networkManager.isConnected()) {
|
|
992
|
+
this.networkManager.disconnect();
|
|
993
|
+
}
|
|
994
|
+
this.connectionInProgress = false; // Reset button state
|
|
995
|
+
this.emit('backToLogin');
|
|
996
|
+
this.currentState = "LOGIN";
|
|
997
|
+
this.selectedIndex = 0; // Reset selection
|
|
998
|
+
} else {
|
|
999
|
+
// Handle scrollable room selection
|
|
1000
|
+
// Check all possible room indices (up to reasonable limit)
|
|
1001
|
+
for (let i = 0; i < Math.min(availableRooms.length, 20); i++) {
|
|
1002
|
+
const elementId = `room_item_${i}`;
|
|
1003
|
+
const isPressed = this.input.isElementJustPressed(elementId);
|
|
1004
|
+
|
|
1005
|
+
if (isPressed && availableRooms[i]) {
|
|
1006
|
+
this.emit('buttonPressed');
|
|
1007
|
+
console.log("✅ Room clicked:", availableRooms[i]);
|
|
1008
|
+
// Support both WebSocket (name) and P2P (peerId) formats
|
|
1009
|
+
this.selectedRoom = availableRooms[i].peerId || availableRooms[i].name;
|
|
1010
|
+
this.joinSelectedRoom();
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Update selection based on hover
|
|
1017
|
+
if (this.input.isElementHovered("changeNameButton")) {
|
|
1018
|
+
if (this.selectedIndex !== 0) {
|
|
1019
|
+
const old = this.selectedIndex;
|
|
1020
|
+
this.selectedIndex = 0;
|
|
1021
|
+
this.emit('selectionChanged', { oldIndex: old, newIndex: 0 });
|
|
1022
|
+
}
|
|
1023
|
+
} else if (this.input.isElementHovered("createRoomButton")) {
|
|
1024
|
+
if (this.selectedIndex !== 1) {
|
|
1025
|
+
const old = this.selectedIndex;
|
|
1026
|
+
this.selectedIndex = 1;
|
|
1027
|
+
this.emit('selectionChanged', { oldIndex: old, newIndex: 1 });
|
|
1028
|
+
}
|
|
1029
|
+
} else if (this.input.isElementHovered("backToLoginButton")) {
|
|
1030
|
+
if (this.selectedIndex !== 2) {
|
|
1031
|
+
const old = this.selectedIndex;
|
|
1032
|
+
this.selectedIndex = 2;
|
|
1033
|
+
this.emit('selectionChanged', { oldIndex: old, newIndex: 2 });
|
|
1034
|
+
}
|
|
1035
|
+
} else {
|
|
1036
|
+
// Check room hover
|
|
1037
|
+
for (let i = 0; i < availableRooms.length; i++) {
|
|
1038
|
+
if (this.input.isElementHovered(`room_item_${i}`)) {
|
|
1039
|
+
const next = this.lobbyButtonCount + i;
|
|
1040
|
+
if (this.selectedIndex !== next) {
|
|
1041
|
+
const old = this.selectedIndex;
|
|
1042
|
+
this.selectedIndex = next;
|
|
1043
|
+
this.emit('selectionChanged', { oldIndex: old, newIndex: next });
|
|
1044
|
+
}
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
break;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Start connection to server or P2P network
|
|
1055
|
+
*/
|
|
1056
|
+
async startConnection() {
|
|
1057
|
+
this.username = this.generateRandomUsername();
|
|
1058
|
+
this.currentState = "LOGIN";
|
|
1059
|
+
this.serverStatus = 'CONNECTING';
|
|
1060
|
+
this.serverStatusColor = '#ffff00';
|
|
1061
|
+
this.connectionInProgress = true; // Prevent multiple clicks
|
|
1062
|
+
|
|
1063
|
+
// Show spinner for P2P mode
|
|
1064
|
+
if (this.networkMode === 'p2p') {
|
|
1065
|
+
this.isConnecting = true;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
try {
|
|
1069
|
+
if (this.networkMode === 'p2p') {
|
|
1070
|
+
// P2P mode: join the game via DHT
|
|
1071
|
+
await this.networkManager.joinGame(this.networkManager.config.gameId, this.username);
|
|
1072
|
+
} else {
|
|
1073
|
+
// WebSocket mode: connect to server
|
|
1074
|
+
await this.networkManager.connectToServer({ username: this.username });
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Update status immediately on success
|
|
1078
|
+
this.serverStatus = 'ONLINE';
|
|
1079
|
+
this.serverStatusColor = '#00ff00';
|
|
1080
|
+
this.isConnecting = false; // Stop spinner
|
|
1081
|
+
this.connectionInProgress = false; // Connection complete, button is no longer greyed out
|
|
1082
|
+
// Clear server check interval since we're now connected
|
|
1083
|
+
if (this.serverCheckInterval) {
|
|
1084
|
+
clearInterval(this.serverCheckInterval);
|
|
1085
|
+
this.serverCheckInterval = null;
|
|
1086
|
+
}
|
|
1087
|
+
this.currentState = "LOBBY";
|
|
1088
|
+
this.selectedIndex = 0; // Reset selection when entering lobby
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
console.error("Failed to connect:", error);
|
|
1091
|
+
// Update status immediately on failure
|
|
1092
|
+
this.serverStatus = 'UNAVAILABLE';
|
|
1093
|
+
this.serverStatusColor = '#ff0000';
|
|
1094
|
+
this.isConnecting = false; // Stop spinner
|
|
1095
|
+
this.connectionInProgress = false; // Allow retry on failure
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Join selected room
|
|
1101
|
+
*/
|
|
1102
|
+
joinSelectedRoom() {
|
|
1103
|
+
if (!this.selectedRoom) return;
|
|
1104
|
+
|
|
1105
|
+
// Check if room is full before attempting to join
|
|
1106
|
+
const availableRooms = this.networkManager.getAvailableRooms();
|
|
1107
|
+
const selectedRoomData = availableRooms.find(r => (r.peerId || r.name) === this.selectedRoom);
|
|
1108
|
+
|
|
1109
|
+
if (selectedRoomData) {
|
|
1110
|
+
const maxDisplay = selectedRoomData.maxPlayers === -1 ? '∞' : selectedRoomData.maxPlayers;
|
|
1111
|
+
const currentPlayers = selectedRoomData.playerCount !== undefined ? selectedRoomData.playerCount : selectedRoomData.currentPlayers || 0;
|
|
1112
|
+
const isFull = selectedRoomData.maxPlayers > 0 && currentPlayers >= selectedRoomData.maxPlayers;
|
|
1113
|
+
|
|
1114
|
+
if (isFull) {
|
|
1115
|
+
this.showErrorModal("Room Full", `This room is full (${currentPlayers}/${maxDisplay}).`);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// P2P mode: do granular join with step-by-step messages
|
|
1121
|
+
if (this.networkMode === 'p2p') {
|
|
1122
|
+
this.performP2PJoin(this.selectedRoom);
|
|
1123
|
+
} else {
|
|
1124
|
+
// WebSocket mode: simple one-shot join
|
|
1125
|
+
this.networkManager.joinRoom(this.selectedRoom)
|
|
1126
|
+
.then(() => {
|
|
1127
|
+
// Event will be emitted by setupNetworkEvents
|
|
1128
|
+
})
|
|
1129
|
+
.catch((error) => {
|
|
1130
|
+
console.error("Failed to join room:", error);
|
|
1131
|
+
this.showErrorModal("Cannot Join Room", error.message || "Failed to join the selected room");
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Perform P2P join with granular steps and modal progress
|
|
1138
|
+
*/
|
|
1139
|
+
async performP2PJoin(hostPeerId) {
|
|
1140
|
+
try {
|
|
1141
|
+
this.joinModalVisible = true;
|
|
1142
|
+
this.joinModalHostPeerId = hostPeerId;
|
|
1143
|
+
|
|
1144
|
+
// Step 1: Contacting host
|
|
1145
|
+
this.joinModalStatus = 'contactingHost';
|
|
1146
|
+
this.joinModalStatusSetTime = Date.now();
|
|
1147
|
+
await this.delay(500);
|
|
1148
|
+
await this.networkManager.initiateConnection(hostPeerId);
|
|
1149
|
+
|
|
1150
|
+
// Step 2: Offer sent (start listening for acceptance immediately)
|
|
1151
|
+
this.joinModalStatus = 'offerSent';
|
|
1152
|
+
this.joinModalStatusSetTime = Date.now();
|
|
1153
|
+
|
|
1154
|
+
await this.networkManager.sendOffer(hostPeerId);
|
|
1155
|
+
|
|
1156
|
+
// Start waiting for acceptance, but ensure 500ms minimum display
|
|
1157
|
+
const acceptancePromise = this.networkManager.waitForAcceptance(hostPeerId);
|
|
1158
|
+
await this.delay(500);
|
|
1159
|
+
await acceptancePromise;
|
|
1160
|
+
|
|
1161
|
+
// Step 3: Accepted by host (now that it actually accepted)
|
|
1162
|
+
this.joinModalStatus = 'acceptedByHost';
|
|
1163
|
+
this.joinModalStatusSetTime = Date.now();
|
|
1164
|
+
await this.delay(500);
|
|
1165
|
+
|
|
1166
|
+
// Step 4: Establishing connection
|
|
1167
|
+
this.joinModalStatus = 'establishingConnection';
|
|
1168
|
+
this.joinModalStatusSetTime = Date.now();
|
|
1169
|
+
await this.delay(500);
|
|
1170
|
+
await this.networkManager.openGameChannel(hostPeerId);
|
|
1171
|
+
|
|
1172
|
+
// Step 5: Connected
|
|
1173
|
+
this.joinModalStatus = 'connected';
|
|
1174
|
+
await this.delay(500);
|
|
1175
|
+
|
|
1176
|
+
// Done - close modal and emit event
|
|
1177
|
+
this.joinModalVisible = false;
|
|
1178
|
+
this.emit('joinedRoom', hostPeerId);
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
this.joinModalVisible = false;
|
|
1181
|
+
console.error("P2P join failed:", error);
|
|
1182
|
+
|
|
1183
|
+
// Clean up the connection attempt
|
|
1184
|
+
const peerData = this.networkManager.peerConnections.get(hostPeerId);
|
|
1185
|
+
if (peerData) {
|
|
1186
|
+
if (peerData.pc) {
|
|
1187
|
+
peerData.pc.close();
|
|
1188
|
+
peerData.pc = null;
|
|
1189
|
+
}
|
|
1190
|
+
if (peerData.channel) {
|
|
1191
|
+
peerData.channel.close();
|
|
1192
|
+
peerData.channel = null;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
this.showErrorModal("Cannot Join Room", error.message || "Failed to join the selected room");
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Simple delay helper
|
|
1202
|
+
*/
|
|
1203
|
+
delay(ms) {
|
|
1204
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* Create and join room
|
|
1209
|
+
*/
|
|
1210
|
+
createAndJoinRoom() {
|
|
1211
|
+
// For P2P mode, create a room (become host)
|
|
1212
|
+
if (this.networkMode === 'p2p') {
|
|
1213
|
+
// P2P needs to call joinGame first to set up currentGameId
|
|
1214
|
+
// Use the default gameId from P2P config
|
|
1215
|
+
const gameId = this.networkManager.config.gameId || 'game-id-00000';
|
|
1216
|
+
this.networkManager.currentGameId = gameId;
|
|
1217
|
+
this.networkManager.createRoom();
|
|
1218
|
+
// console.log("[ActionNetManagerGUI] Created P2P room, waiting for players...");
|
|
1219
|
+
} else {
|
|
1220
|
+
// For WebSocket mode, join a room with a generated name
|
|
1221
|
+
const roomName = `${this.username}'s room`;
|
|
1222
|
+
this.networkManager.joinRoom(roomName)
|
|
1223
|
+
.then(() => {
|
|
1224
|
+
// Event will be emitted
|
|
1225
|
+
})
|
|
1226
|
+
.catch((error) => {
|
|
1227
|
+
console.error("Failed to create room:", error);
|
|
1228
|
+
this.showErrorModal("Cannot Create Room", error.message || "Failed to create a new room");
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Change username
|
|
1235
|
+
*/
|
|
1236
|
+
async changeUsername() {
|
|
1237
|
+
const newUsername = this.generateRandomUsername();
|
|
1238
|
+
try {
|
|
1239
|
+
await this.networkManager.setUsername(newUsername);
|
|
1240
|
+
this.username = newUsername;
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
console.error("Failed to change username:", error);
|
|
1243
|
+
this.showErrorModal("Cannot Change Name", error.message || "Failed to change username");
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Generate random username
|
|
1249
|
+
*/
|
|
1250
|
+
generateRandomUsername() {
|
|
1251
|
+
const adjectives = [
|
|
1252
|
+
'Big', 'Floppy', 'Little', 'Goofy', 'Wiggly',
|
|
1253
|
+
'Stinky', 'Chunky', 'Bouncy', 'Silly', 'Noisy',
|
|
1254
|
+
'Tiny', 'Cracked', 'Lit', 'Steamy', 'Epic',
|
|
1255
|
+
'Super', 'Mega', 'Giant', 'Double', 'Salty',
|
|
1256
|
+
'Farty', 'Smelly', 'Sneaky', 'Gassy', 'Crusty',
|
|
1257
|
+
'Soggy', 'Tooty', 'Ratchet', 'Nasty', 'Squeaky',
|
|
1258
|
+
'Skibidi', 'Rizzy', 'Saucy', 'Mid', 'Sussy', "Lil' "
|
|
1259
|
+
];
|
|
1260
|
+
|
|
1261
|
+
const nouns = [
|
|
1262
|
+
'Farter', 'Butt', 'PooPoo', 'Nugget', 'Tooter', 'Turd',
|
|
1263
|
+
'Poop', 'Squeaker', 'DooDoo', 'Pooter', 'Dumper', 'Keister',
|
|
1264
|
+
'Fart', 'Hiney', 'Pooper', 'Booty', 'Stinker', 'Skidmark',
|
|
1265
|
+
'Ahh', 'Buns', 'Cheeks', 'Tushy', 'Doody'
|
|
1266
|
+
];
|
|
1267
|
+
|
|
1268
|
+
const funNumbers = [
|
|
1269
|
+
'69', '420', '666', '1337', '123',
|
|
1270
|
+
'007', '101', '999', '321', '777',
|
|
1271
|
+
'67', '911', ''
|
|
1272
|
+
];
|
|
1273
|
+
|
|
1274
|
+
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
|
1275
|
+
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
|
1276
|
+
const number = funNumbers[Math.floor(Math.random() * funNumbers.length)];
|
|
1277
|
+
return `${adj}${noun}${number}`;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Start server check (WebSocket only)
|
|
1282
|
+
*/
|
|
1283
|
+
startServerCheck() {
|
|
1284
|
+
// Skip server check for P2P mode (DHT connectivity is implicit)
|
|
1285
|
+
if (this.networkMode === 'p2p') {
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
this.serverCheckInterval = setInterval(async () => {
|
|
1290
|
+
// Only check if we're not connected (don't override connection status)
|
|
1291
|
+
if (!this.networkManager.isConnected()) {
|
|
1292
|
+
try {
|
|
1293
|
+
const result = await this.networkManager.testServerConnection();
|
|
1294
|
+
this.serverStatus = result.available ? 'ONLINE' : 'UNAVAILABLE';
|
|
1295
|
+
this.serverStatusColor = result.available ? '#00ff00' : '#ff0000';
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
this.serverStatus = 'UNAVAILABLE';
|
|
1298
|
+
this.serverStatusColor = '#ff0000';
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}, 3000);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Get ActionNetManager instance
|
|
1306
|
+
*/
|
|
1307
|
+
getNetManager() {
|
|
1308
|
+
return this.networkManager;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Hide the GUI (for when game takes over)
|
|
1313
|
+
*/
|
|
1314
|
+
hide() {
|
|
1315
|
+
this.guiVisible = false;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Show the GUI (for when returning from game)
|
|
1320
|
+
*/
|
|
1321
|
+
show() {
|
|
1322
|
+
this.guiVisible = true;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Register a custom message handler for one-shot actions
|
|
1327
|
+
*
|
|
1328
|
+
* Use this for non-periodic game events like:
|
|
1329
|
+
* - garbageSent (Tetris attack)
|
|
1330
|
+
* - itemUsed (power-up activation)
|
|
1331
|
+
* - chatMessage (player communication)
|
|
1332
|
+
*
|
|
1333
|
+
* For periodic state sync (position, score, etc), use syncSystem.register() instead.
|
|
1334
|
+
*
|
|
1335
|
+
* @param {String} messageType - Message type to handle (e.g., 'garbageSent')
|
|
1336
|
+
* @param {Function} handler - Handler function (message) => {}
|
|
1337
|
+
*
|
|
1338
|
+
* Example:
|
|
1339
|
+
* gui.registerMessageHandler('garbageSent', (msg) => {
|
|
1340
|
+
* gameManager.addGarbage(msg.targetPlayer, msg.lines);
|
|
1341
|
+
* });
|
|
1342
|
+
*/
|
|
1343
|
+
registerMessageHandler(messageType, handler) {
|
|
1344
|
+
if (!messageType || typeof messageType !== 'string') {
|
|
1345
|
+
console.error('[ActionNetManagerGUI] Invalid message type:', messageType);
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (typeof handler !== 'function') {
|
|
1350
|
+
console.error('[ActionNetManagerGUI] Handler must be a function');
|
|
1351
|
+
return false;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
this.customMessageHandlers.set(messageType, handler);
|
|
1355
|
+
// console.log(`[ActionNetManagerGUI] Registered custom handler: '${messageType}'`);
|
|
1356
|
+
return true;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* Remove a custom message handler
|
|
1361
|
+
*
|
|
1362
|
+
* @param {String} messageType - Message type to unregister
|
|
1363
|
+
*/
|
|
1364
|
+
unregisterMessageHandler(messageType) {
|
|
1365
|
+
if (this.customMessageHandlers.delete(messageType)) {
|
|
1366
|
+
// console.log(`[ActionNetManagerGUI] Unregistered handler: '${messageType}'`);
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Activate SyncSystem for a room with proper peer connection context
|
|
1374
|
+
* Call this when joining a room to ensure SyncSystem is ready
|
|
1375
|
+
*/
|
|
1376
|
+
activateSyncForRoom() {
|
|
1377
|
+
if (this.syncSystem && !this.syncSystem.isRunning) {
|
|
1378
|
+
console.log("[ActionNetManagerGUI] Activating SyncSystem for room");
|
|
1379
|
+
this.syncSystem.start();
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Deactivate SyncSystem when leaving a room
|
|
1385
|
+
* Call this when leaving a room to clean up
|
|
1386
|
+
*/
|
|
1387
|
+
deactivateSyncForRoom() {
|
|
1388
|
+
if (this.syncSystem && this.syncSystem.isRunning) {
|
|
1389
|
+
console.log("[ActionNetManagerGUI] Deactivating SyncSystem for room");
|
|
1390
|
+
this.syncSystem.stop();
|
|
1391
|
+
this.syncSystem.clearRemoteData();
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* Get current username
|
|
1397
|
+
*/
|
|
1398
|
+
getUsername() {
|
|
1399
|
+
return this.username;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/**
|
|
1403
|
+
* Check if in room
|
|
1404
|
+
*/
|
|
1405
|
+
isInRoom() {
|
|
1406
|
+
return this.networkManager.isInRoom();
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Check if connected
|
|
1411
|
+
*/
|
|
1412
|
+
isConnected() {
|
|
1413
|
+
return this.networkManager.isConnected();
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/**
|
|
1417
|
+
* Auto-scroll to keep selected item visible
|
|
1418
|
+
*/
|
|
1419
|
+
scrollToSelectedItem() {
|
|
1420
|
+
// Only scroll if we're selecting a room (not a button)
|
|
1421
|
+
if (this.selectedIndex < this.lobbyButtonCount) {
|
|
1422
|
+
return; // Buttons don't need scrolling
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
if (!this.roomScroller) {
|
|
1426
|
+
return; // No scroller available
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Calculate which room is selected
|
|
1430
|
+
const roomIndex = this.selectedIndex - this.lobbyButtonCount;
|
|
1431
|
+
const availableRooms = this.networkManager.getAvailableRooms();
|
|
1432
|
+
|
|
1433
|
+
if (roomIndex < 0 || roomIndex >= availableRooms.length) {
|
|
1434
|
+
return; // Invalid room index
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Calculate the Y position of this room item
|
|
1438
|
+
const itemHeight = this.roomScroller.listArea.itemHeight + this.roomScroller.listArea.padding;
|
|
1439
|
+
const itemY = roomIndex * itemHeight;
|
|
1440
|
+
|
|
1441
|
+
// Calculate visible area bounds
|
|
1442
|
+
const scrollTop = this.roomScroller.scrollOffset;
|
|
1443
|
+
const scrollBottom = scrollTop + this.roomScroller.listArea.height;
|
|
1444
|
+
|
|
1445
|
+
// Check if item is above visible area (scroll up)
|
|
1446
|
+
if (itemY < scrollTop) {
|
|
1447
|
+
this.roomScroller.scrollOffset = itemY;
|
|
1448
|
+
}
|
|
1449
|
+
// Check if item is below visible area (scroll down)
|
|
1450
|
+
else if (itemY + itemHeight > scrollBottom) {
|
|
1451
|
+
this.roomScroller.scrollOffset = itemY + itemHeight - this.roomScroller.listArea.height;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// Clamp scroll offset to valid range
|
|
1455
|
+
this.roomScroller.scrollOffset = Math.max(0, Math.min(this.roomScroller.maxScrollOffset, this.roomScroller.scrollOffset));
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Show error modal
|
|
1460
|
+
*/
|
|
1461
|
+
showErrorModal(title, message) {
|
|
1462
|
+
this.errorModalVisible = true;
|
|
1463
|
+
this.errorModalTitle = title;
|
|
1464
|
+
this.errorModalMessage = message;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* Hide error modal
|
|
1469
|
+
*/
|
|
1470
|
+
hideErrorModal() {
|
|
1471
|
+
this.errorModalVisible = false;
|
|
1472
|
+
this.errorModalTitle = '';
|
|
1473
|
+
this.errorModalMessage = '';
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* Render error modal
|
|
1478
|
+
*/
|
|
1479
|
+
renderErrorModal() {
|
|
1480
|
+
// Semi-transparent overlay
|
|
1481
|
+
this.guiCtx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
1482
|
+
this.guiCtx.fillRect(0, 0, ActionNetManagerGUI.WIDTH, ActionNetManagerGUI.HEIGHT);
|
|
1483
|
+
|
|
1484
|
+
// Modal dimensions
|
|
1485
|
+
const modalWidth = 400;
|
|
1486
|
+
const modalHeight = 200;
|
|
1487
|
+
const modalX = (ActionNetManagerGUI.WIDTH - modalWidth) / 2;
|
|
1488
|
+
const modalY = (ActionNetManagerGUI.HEIGHT - modalHeight) / 2;
|
|
1489
|
+
|
|
1490
|
+
// Modal background (matching GUI button style)
|
|
1491
|
+
this.guiCtx.fillStyle = '#333333';
|
|
1492
|
+
this.guiCtx.fillRect(modalX, modalY, modalWidth, modalHeight);
|
|
1493
|
+
this.guiCtx.strokeStyle = '#888888';
|
|
1494
|
+
this.guiCtx.lineWidth = 2;
|
|
1495
|
+
this.guiCtx.strokeRect(modalX, modalY, modalWidth, modalHeight);
|
|
1496
|
+
|
|
1497
|
+
// Title
|
|
1498
|
+
this.renderLabel(this.errorModalTitle, ActionNetManagerGUI.WIDTH / 2, modalY + 40, 'bold 32px Arial', '#ffffff', 'center', 'middle', 8, false);
|
|
1499
|
+
|
|
1500
|
+
// Message (with text wrapping)
|
|
1501
|
+
this.renderWrappedText(this.errorModalMessage, ActionNetManagerGUI.WIDTH / 2, modalY + 90, 370, '20px Arial', '#cccccc');
|
|
1502
|
+
|
|
1503
|
+
// Back button (centered)
|
|
1504
|
+
const buttonWidth = 120;
|
|
1505
|
+
const buttonHeight = 50;
|
|
1506
|
+
const buttonX = (ActionNetManagerGUI.WIDTH - buttonWidth) / 2;
|
|
1507
|
+
const buttonY = modalY + modalHeight - 70;
|
|
1508
|
+
|
|
1509
|
+
// Check if back button is hovered or selected (for keyboard/gamepad navigation)
|
|
1510
|
+
const isHovered = this.input.isElementHovered('error_modal_back_button');
|
|
1511
|
+
const isSelected = true; // Always selected since it's the only button
|
|
1512
|
+
|
|
1513
|
+
this.guiCtx.fillStyle = isHovered ? '#555555' : '#333333';
|
|
1514
|
+
this.guiCtx.fillRect(buttonX, buttonY, buttonWidth, buttonHeight);
|
|
1515
|
+
this.guiCtx.strokeStyle = isSelected ? '#ffffff' : '#888888'; // White border for selection
|
|
1516
|
+
this.guiCtx.lineWidth = isSelected ? 3 : 2; // Thicker border for selection
|
|
1517
|
+
this.guiCtx.strokeRect(buttonX, buttonY, buttonWidth, buttonHeight);
|
|
1518
|
+
|
|
1519
|
+
// Button text
|
|
1520
|
+
this.guiCtx.fillStyle = '#ffffff';
|
|
1521
|
+
this.guiCtx.font = 'bold 20px Arial';
|
|
1522
|
+
this.guiCtx.textAlign = 'center';
|
|
1523
|
+
this.guiCtx.textBaseline = 'middle';
|
|
1524
|
+
this.guiCtx.fillText('BACK', buttonX + buttonWidth / 2, buttonY + buttonHeight / 2);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* Handle error modal input
|
|
1529
|
+
*/
|
|
1530
|
+
handleErrorModalInput() {
|
|
1531
|
+
// Register back button if not already registered
|
|
1532
|
+
if (!this.input.rawState.elements.gui.has('error_modal_back_button')) {
|
|
1533
|
+
const modalWidth = 400;
|
|
1534
|
+
const modalHeight = 200;
|
|
1535
|
+
const modalX = (ActionNetManagerGUI.WIDTH - modalWidth) / 2;
|
|
1536
|
+
const modalY = (ActionNetManagerGUI.HEIGHT - modalHeight) / 2;
|
|
1537
|
+
const buttonWidth = 120;
|
|
1538
|
+
const buttonHeight = 50;
|
|
1539
|
+
const buttonX = (ActionNetManagerGUI.WIDTH - buttonWidth) / 2;
|
|
1540
|
+
const buttonY = modalY + modalHeight - 70;
|
|
1541
|
+
|
|
1542
|
+
this.input.registerElement('error_modal_back_button', {
|
|
1543
|
+
bounds: () => ({
|
|
1544
|
+
x: buttonX,
|
|
1545
|
+
y: buttonY,
|
|
1546
|
+
width: buttonWidth,
|
|
1547
|
+
height: buttonHeight
|
|
1548
|
+
})
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// Handle button press
|
|
1553
|
+
if (this.input.isElementJustPressed('error_modal_back_button')) {
|
|
1554
|
+
this.hideErrorModal();
|
|
1555
|
+
// Unregister the button
|
|
1556
|
+
this.input.removeElement('error_modal_back_button');
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// Handle keyboard/gamepad input
|
|
1560
|
+
if (this.input.isKeyJustPressed('Action1') ||
|
|
1561
|
+
this.input.isGamepadButtonJustPressed(0, 0) || this.input.isGamepadButtonJustPressed(0, 1) ||
|
|
1562
|
+
this.input.isGamepadButtonJustPressed(0, 2) || this.input.isGamepadButtonJustPressed(0, 3)) {
|
|
1563
|
+
this.hideErrorModal();
|
|
1564
|
+
this.input.removeElement('error_modal_back_button');
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// Handle escape/back button
|
|
1568
|
+
if (this.input.isKeyJustPressed('Action2') ||
|
|
1569
|
+
this.input.isKeyJustPressed('Escape') ||
|
|
1570
|
+
this.input.isGamepadButtonJustPressed(1, 0) || this.input.isGamepadButtonJustPressed(1, 1) ||
|
|
1571
|
+
this.input.isGamepadButtonJustPressed(1, 2) || this.input.isGamepadButtonJustPressed(1, 3)) {
|
|
1572
|
+
this.hideErrorModal();
|
|
1573
|
+
this.input.removeElement('error_modal_back_button');
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Render a rotating spinner wheel
|
|
1579
|
+
*/
|
|
1580
|
+
renderSpinner(x, y, radius = 20, lineWidth = 3) {
|
|
1581
|
+
this.guiCtx.save();
|
|
1582
|
+
|
|
1583
|
+
// Translate to center
|
|
1584
|
+
this.guiCtx.translate(x, y);
|
|
1585
|
+
this.guiCtx.rotate((this.spinnerRotation * Math.PI) / 180);
|
|
1586
|
+
|
|
1587
|
+
// Draw spoke with trail/fade effect
|
|
1588
|
+
const trailLength = 40; // Number of trail segments
|
|
1589
|
+
const trailSpacing = 10; // Rotation degrees between trail segments
|
|
1590
|
+
|
|
1591
|
+
for (let i = trailLength; i > 0; i--) {
|
|
1592
|
+
// Calculate opacity (fade as we go back in trail)
|
|
1593
|
+
const opacity = i / trailLength;
|
|
1594
|
+
const trailRotation = i * trailSpacing;
|
|
1595
|
+
|
|
1596
|
+
// Save and rotate for this trail segment
|
|
1597
|
+
this.guiCtx.save();
|
|
1598
|
+
this.guiCtx.rotate((trailRotation * Math.PI) / 180);
|
|
1599
|
+
|
|
1600
|
+
this.guiCtx.strokeStyle = `rgba(136, 136, 136, ${opacity * 0.8})`;
|
|
1601
|
+
this.guiCtx.lineWidth = lineWidth;
|
|
1602
|
+
this.guiCtx.lineCap = 'round';
|
|
1603
|
+
|
|
1604
|
+
const x1 = 0;
|
|
1605
|
+
const y1 = -(radius / 3);
|
|
1606
|
+
const x2 = 0;
|
|
1607
|
+
const y2 = -radius;
|
|
1608
|
+
|
|
1609
|
+
this.guiCtx.beginPath();
|
|
1610
|
+
this.guiCtx.moveTo(x1, y1);
|
|
1611
|
+
this.guiCtx.lineTo(x2, y2);
|
|
1612
|
+
this.guiCtx.stroke();
|
|
1613
|
+
|
|
1614
|
+
this.guiCtx.restore();
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// Draw outer circle
|
|
1618
|
+
this.guiCtx.strokeStyle = '#666666';
|
|
1619
|
+
this.guiCtx.beginPath();
|
|
1620
|
+
this.guiCtx.arc(0, 0, radius, 0, Math.PI * 2);
|
|
1621
|
+
this.guiCtx.stroke();
|
|
1622
|
+
|
|
1623
|
+
this.guiCtx.restore();
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/**
|
|
1627
|
+
* Render join modal with connection status
|
|
1628
|
+
*/
|
|
1629
|
+
renderJoinModal() {
|
|
1630
|
+
// Semi-transparent overlay
|
|
1631
|
+
this.guiCtx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
1632
|
+
this.guiCtx.fillRect(0, 0, ActionNetManagerGUI.WIDTH, ActionNetManagerGUI.HEIGHT);
|
|
1633
|
+
|
|
1634
|
+
// Modal dimensions
|
|
1635
|
+
const modalWidth = 400;
|
|
1636
|
+
const modalHeight = 250;
|
|
1637
|
+
const modalX = (ActionNetManagerGUI.WIDTH - modalWidth) / 2;
|
|
1638
|
+
const modalY = (ActionNetManagerGUI.HEIGHT - modalHeight) / 2;
|
|
1639
|
+
|
|
1640
|
+
// Modal background
|
|
1641
|
+
this.guiCtx.fillStyle = '#333333';
|
|
1642
|
+
this.guiCtx.fillRect(modalX, modalY, modalWidth, modalHeight);
|
|
1643
|
+
this.guiCtx.strokeStyle = '#888888';
|
|
1644
|
+
this.guiCtx.lineWidth = 2;
|
|
1645
|
+
this.guiCtx.strokeRect(modalX, modalY, modalWidth, modalHeight);
|
|
1646
|
+
|
|
1647
|
+
// Title
|
|
1648
|
+
this.renderLabel('Joining Game', ActionNetManagerGUI.WIDTH / 2, modalY + 40, 'bold 32px Arial', '#ffffff', 'center', 'middle', 8, false);
|
|
1649
|
+
|
|
1650
|
+
// Status messages
|
|
1651
|
+
const statuses = {
|
|
1652
|
+
'contactingHost': 'Contacting host...',
|
|
1653
|
+
'offerSent': 'Waiting for host...',
|
|
1654
|
+
'acceptedByHost': 'Host accepted',
|
|
1655
|
+
'establishingConnection': 'Establishing connection...',
|
|
1656
|
+
'connected': 'Connected!'
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
const statusMessage = statuses[this.joinModalStatus] || 'Connecting...';
|
|
1660
|
+
|
|
1661
|
+
// Centered message with spinner below
|
|
1662
|
+
this.renderLabel(statusMessage, ActionNetManagerGUI.WIDTH / 2, modalY + 100, '22px Arial', '#ffffff', 'center', 'middle', 8, false);
|
|
1663
|
+
this.renderSpinner(ActionNetManagerGUI.WIDTH / 2, modalY + 145, 15, 2);
|
|
1664
|
+
|
|
1665
|
+
// Cancel button
|
|
1666
|
+
const buttonWidth = 120;
|
|
1667
|
+
const buttonHeight = 40;
|
|
1668
|
+
const buttonX = (ActionNetManagerGUI.WIDTH - buttonWidth) / 2;
|
|
1669
|
+
const buttonY = modalY + modalHeight - 60;
|
|
1670
|
+
|
|
1671
|
+
const isHovered = this.input.isElementHovered('join_modal_cancel_button');
|
|
1672
|
+
|
|
1673
|
+
this.guiCtx.fillStyle = isHovered ? '#555555' : '#333333';
|
|
1674
|
+
this.guiCtx.fillRect(buttonX, buttonY, buttonWidth, buttonHeight);
|
|
1675
|
+
this.guiCtx.strokeStyle = '#888888';
|
|
1676
|
+
this.guiCtx.lineWidth = 2;
|
|
1677
|
+
this.guiCtx.strokeRect(buttonX, buttonY, buttonWidth, buttonHeight);
|
|
1678
|
+
|
|
1679
|
+
this.guiCtx.fillStyle = '#ffffff';
|
|
1680
|
+
this.guiCtx.font = 'bold 16px Arial';
|
|
1681
|
+
this.guiCtx.textAlign = 'center';
|
|
1682
|
+
this.guiCtx.textBaseline = 'middle';
|
|
1683
|
+
this.guiCtx.fillText('CANCEL', buttonX + buttonWidth / 2, buttonY + buttonHeight / 2);
|
|
1684
|
+
|
|
1685
|
+
// Register cancel button
|
|
1686
|
+
if (!this.input.rawState.elements.gui.has('join_modal_cancel_button')) {
|
|
1687
|
+
this.input.registerElement('join_modal_cancel_button', {
|
|
1688
|
+
bounds: () => ({
|
|
1689
|
+
x: buttonX,
|
|
1690
|
+
y: buttonY,
|
|
1691
|
+
width: buttonWidth,
|
|
1692
|
+
height: buttonHeight
|
|
1693
|
+
})
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Handle join modal input
|
|
1700
|
+
*/
|
|
1701
|
+
handleJoinModalInput() {
|
|
1702
|
+
if (this.input.isElementJustPressed('join_modal_cancel_button') ||
|
|
1703
|
+
this.input.isKeyJustPressed('Escape')) {
|
|
1704
|
+
this.joinModalVisible = false;
|
|
1705
|
+
this.input.removeElement('join_modal_cancel_button');
|
|
1706
|
+
// TODO: abort the join attempt
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|