action-engine-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +348 -0
  3. package/actionengine/3rdparty/goblin/goblin.js +9609 -0
  4. package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
  5. package/actionengine/camera/actioncamera.js +90 -0
  6. package/actionengine/camera/cameracollisionhandler.js +69 -0
  7. package/actionengine/character/actioncharacter.js +360 -0
  8. package/actionengine/character/actioncharacter3D.js +61 -0
  9. package/actionengine/core/app.js +430 -0
  10. package/actionengine/debug/basedebugpanel.js +858 -0
  11. package/actionengine/display/canvasmanager.js +75 -0
  12. package/actionengine/display/gl/programmanager.js +570 -0
  13. package/actionengine/display/gl/shaders/lineshader.js +118 -0
  14. package/actionengine/display/gl/shaders/objectshader.js +1756 -0
  15. package/actionengine/display/gl/shaders/particleshader.js +43 -0
  16. package/actionengine/display/gl/shaders/shadowshader.js +319 -0
  17. package/actionengine/display/gl/shaders/spriteshader.js +100 -0
  18. package/actionengine/display/gl/shaders/watershader.js +67 -0
  19. package/actionengine/display/graphics/actionmodel3D.js +191 -0
  20. package/actionengine/display/graphics/actionsprite3D.js +230 -0
  21. package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
  22. package/actionengine/display/graphics/lighting/actionlight.js +211 -0
  23. package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
  24. package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
  25. package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
  26. package/actionengine/display/graphics/renderableobject.js +44 -0
  27. package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
  28. package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
  29. package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
  30. package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
  31. package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
  32. package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
  33. package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
  34. package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
  35. package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
  36. package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
  37. package/actionengine/display/graphics/texture/texturemanager.js +242 -0
  38. package/actionengine/display/graphics/texture/textureregistry.js +177 -0
  39. package/actionengine/input/actionscrollablearea.js +1405 -0
  40. package/actionengine/input/inputhandler.js +1647 -0
  41. package/actionengine/math/geometry/geometrybuilder.js +161 -0
  42. package/actionengine/math/geometry/glbexporter.js +364 -0
  43. package/actionengine/math/geometry/glbloader.js +722 -0
  44. package/actionengine/math/geometry/modelcodegenerator.js +97 -0
  45. package/actionengine/math/geometry/triangle.js +33 -0
  46. package/actionengine/math/geometry/triangleutils.js +34 -0
  47. package/actionengine/math/mathutils.js +25 -0
  48. package/actionengine/math/matrix4.js +785 -0
  49. package/actionengine/math/physics/actionphysics.js +108 -0
  50. package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
  51. package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
  52. package/actionengine/math/physics/actionraycast.js +129 -0
  53. package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
  54. package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
  55. package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
  56. package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
  57. package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
  58. package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
  59. package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
  60. package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
  61. package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
  62. package/actionengine/math/quaternion.js +61 -0
  63. package/actionengine/math/vector2.js +277 -0
  64. package/actionengine/math/vector3.js +318 -0
  65. package/actionengine/math/viewfrustum.js +136 -0
  66. package/actionengine/network/ACTIONNETREADME.md +810 -0
  67. package/actionengine/network/client/ActionNetManager.js +802 -0
  68. package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
  69. package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
  70. package/actionengine/network/client/SyncSystem.js +422 -0
  71. package/actionengine/network/p2p/ActionNetPeer.js +142 -0
  72. package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
  73. package/actionengine/network/p2p/DataConnection.js +282 -0
  74. package/actionengine/network/p2p/README.md +510 -0
  75. package/actionengine/network/p2p/example.html +502 -0
  76. package/actionengine/network/server/ActionNetServer.js +577 -0
  77. package/actionengine/network/server/ActionNetServerSSL.js +579 -0
  78. package/actionengine/network/server/ActionNetServerUtils.js +458 -0
  79. package/actionengine/network/server/SERVERREADME.md +314 -0
  80. package/actionengine/network/server/package-lock.json +35 -0
  81. package/actionengine/network/server/package.json +13 -0
  82. package/actionengine/network/server/start.bat +27 -0
  83. package/actionengine/network/server/start.sh +25 -0
  84. package/actionengine/network/server/startwss.bat +27 -0
  85. package/actionengine/sound/audiomanager.js +1589 -0
  86. package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
  87. package/actionengine/sound/soundfont/actionparser.js +718 -0
  88. package/actionengine/sound/soundfont/actionreverb.js +252 -0
  89. package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
  90. package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
  91. package/actionengine/sound/soundfont/soundfont.js +2 -0
  92. package/dist/action-engine.min.js +328 -0
  93. package/package.json +35 -0
@@ -0,0 +1,858 @@
1
+ // game/debug/basedebugpanel.js
2
+ class BaseDebugPanel {
3
+ constructor(debugCanvas, game, options = {}) {
4
+ this.canvas = debugCanvas;
5
+ this.ctx = debugCanvas.getContext("2d");
6
+ this.game = game;
7
+
8
+ // Panel state
9
+ this.visible = false;
10
+ this.activeTab = options.defaultTab || 'main';
11
+
12
+ // UI settings
13
+ this.panelWidth = options.panelWidth || 400;
14
+ this.panelHeight = options.panelHeight || 500;
15
+ this.panelX = options.panelX || 20;
16
+ this.panelY = options.panelY || (this.canvas.height - this.panelHeight) / 2;
17
+
18
+ // Tabs (can be overridden by child classes)
19
+ this.tabs = options.tabs || [
20
+ { id: 'main', label: 'Main' }
21
+ ];
22
+
23
+ // Configure toggle button
24
+ this.toggleButton = {
25
+ x: options.toggleX || (this.canvas.width - 150) / 2,
26
+ y: options.toggleY || 10,
27
+ width: options.toggleWidth || 150,
28
+ height: options.toggleHeight || 30,
29
+ text: options.toggleText || "Debug Panel",
30
+ color: options.toggleColor || "#444444"
31
+ };
32
+
33
+ // Register toggle button with input system if toggleId is provided
34
+ if (options.toggleId) {
35
+ this.toggleId = options.toggleId;
36
+ this.game.input.registerElement(
37
+ this.toggleId,
38
+ {
39
+ bounds: () => ({
40
+ x: this.toggleButton.x,
41
+ y: this.toggleButton.y,
42
+ width: this.toggleButton.width,
43
+ height: this.toggleButton.height
44
+ })
45
+ },
46
+ "debug"
47
+ );
48
+ }
49
+
50
+ // Register the tab buttons
51
+ this.tabs.forEach((tab, index) => {
52
+ this.game.input.registerElement(
53
+ `${options.panelId || 'debug'}_tab_${tab.id}`,
54
+ {
55
+ bounds: () => ({
56
+ x: this.panelX + (index * (this.panelWidth / this.tabs.length)),
57
+ y: this.panelY,
58
+ width: this.panelWidth / this.tabs.length,
59
+ height: 30
60
+ })
61
+ },
62
+ "debug"
63
+ );
64
+ });
65
+
66
+ // Configuration for number formatting
67
+ this.roundingConfig = {
68
+ defaultPrecision: 2, // Default decimal places
69
+ fpsPrecision: 1, // FPS decimal places
70
+ positionPrecision: 2, // Position decimal places
71
+ anglePrecision: 4, // Normal/angle decimal places
72
+ timePrecision: 1 // Time/ms decimal places
73
+ };
74
+
75
+ // Initialize Map to store input field registrations
76
+ this.inputFields = new Map();
77
+ }
78
+
79
+ // Utility function to round a number to specified decimals
80
+ roundTo(num, decimals = this.roundingConfig.defaultPrecision) {
81
+ const multiplier = Math.pow(10, decimals);
82
+ return Math.round(num * multiplier) / multiplier;
83
+ }
84
+
85
+ // Format a Vector3 to a string
86
+ formatVector(vec) {
87
+ if (!vec) return "0,0,0";
88
+ return `${this.roundTo(vec.x || 0, this.roundingConfig.positionPrecision)},${this.roundTo(vec.y || 0, this.roundingConfig.positionPrecision)},${this.roundTo(vec.z || 0, this.roundingConfig.positionPrecision)}`;
89
+ }
90
+
91
+ // Helper color functions
92
+ lightenColor(color) {
93
+ const r = parseInt(color.substr(1, 2), 16);
94
+ const g = parseInt(color.substr(3, 2), 16);
95
+ const b = parseInt(color.substr(5, 2), 16);
96
+ const factor = 1.3; // Lighten by 30%
97
+
98
+ return `#${Math.min(255, Math.floor(r * factor)).toString(16).padStart(2, '0')}${Math.min(255, Math.floor(g * factor)).toString(16).padStart(2, '0')}${Math.min(255, Math.floor(b * factor)).toString(16).padStart(2, '0')}`;
99
+ }
100
+
101
+ darkenColor(color) {
102
+ const r = parseInt(color.substr(1, 2), 16);
103
+ const g = parseInt(color.substr(3, 2), 16);
104
+ const b = parseInt(color.substr(5, 2), 16);
105
+ const factor = 0.8;
106
+ return `#${Math.floor(r * factor).toString(16).padStart(2, "0")}${Math.floor(g * factor).toString(16).padStart(2, "0")}${Math.floor(b * factor).toString(16).padStart(2, "0")}`;
107
+ }
108
+
109
+ // Register sliders with input system - similar to LightingDebugPanel
110
+ registerSliders(sliders, tabName) {
111
+ // Clear any existing slider elements for this tab
112
+ Object.entries(sliders).forEach(([name, slider]) => {
113
+ this.game.input.removeElement(slider.id, "debug");
114
+ if (slider.options) {
115
+ this.game.input.removeElement(`${slider.id}_left`, "debug");
116
+ this.game.input.removeElement(`${slider.id}_right`, "debug");
117
+ }
118
+ });
119
+
120
+ // Register each slider with unique bounds
121
+ Object.entries(sliders).forEach(([name, slider], index) => {
122
+ // Register the slider track
123
+ this.game.input.registerElement(
124
+ slider.id,
125
+ {
126
+ bounds: () => ({
127
+ x: this.panelX + 160,
128
+ y: this.panelY + 100 + (index * 40),
129
+ width: 180,
130
+ height: 20
131
+ })
132
+ },
133
+ "debug"
134
+ );
135
+
136
+ // For sliders with discrete options, register left/right buttons
137
+ if (slider.options) {
138
+ // Left button
139
+ this.game.input.registerElement(
140
+ `${slider.id}_left`,
141
+ {
142
+ bounds: () => ({
143
+ x: this.panelX + 160 - 30,
144
+ y: this.panelY + 100 + (index * 40),
145
+ width: 20,
146
+ height: 20
147
+ })
148
+ },
149
+ "debug"
150
+ );
151
+
152
+ // Right button
153
+ this.game.input.registerElement(
154
+ `${slider.id}_right`,
155
+ {
156
+ bounds: () => ({
157
+ x: this.panelX + 160 + 190,
158
+ y: this.panelY + 100 + (index * 40),
159
+ width: 20,
160
+ height: 20
161
+ })
162
+ },
163
+ "debug"
164
+ );
165
+ }
166
+ });
167
+ }
168
+
169
+ // Register buttons with the input system
170
+ registerButtons(buttons) {
171
+ buttons.forEach((button) => {
172
+ this.game.input.registerElement(
173
+ button.id,
174
+ {
175
+ bounds: () => ({
176
+ x: button.x,
177
+ y: button.y,
178
+ width: button.width,
179
+ height: button.height
180
+ })
181
+ },
182
+ "debug"
183
+ );
184
+ });
185
+ }
186
+
187
+ // Register toggle controls with the input system
188
+ registerToggles(toggles) {
189
+ toggles.forEach((toggle, index) => {
190
+ this.game.input.registerElement(
191
+ toggle.id,
192
+ {
193
+ bounds: () => ({
194
+ x: this.panelX + 20,
195
+ y: this.panelY + 100 + (index * 30),
196
+ width: 20,
197
+ height: 20
198
+ })
199
+ },
200
+ "debug"
201
+ );
202
+ });
203
+ }
204
+
205
+ // Handle slider interactions
206
+ handleOptionSliders(sliders) {
207
+ if (!sliders) return;
208
+
209
+ // Check for edit button clicks
210
+ this.inputFields.forEach((info, editBtnId) => {
211
+ if (this.game.input.isElementJustPressed(editBtnId, "debug")) {
212
+ console.log(`Edit button clicked for ${info.name}`);
213
+ this.showInputDialog(info.name, info.slider);
214
+ return; // Exit early if we found a clicked button
215
+ }
216
+ });
217
+
218
+ Object.entries(sliders).forEach(([name, slider]) => {
219
+ // Handle discrete option sliders (with left/right buttons)
220
+ if (slider.options) {
221
+ // Left button (decrease)
222
+ if (this.game.input.isElementJustPressed(`${slider.id}_left`, "debug")) {
223
+ slider.currentOption = Math.max(0, slider.currentOption - 1);
224
+ slider.value = slider.options[slider.currentOption];
225
+ if (slider.updateProperty) {
226
+ slider.updateProperty(slider.value);
227
+ }
228
+ }
229
+
230
+ // Right button (increase)
231
+ if (this.game.input.isElementJustPressed(`${slider.id}_right`, "debug")) {
232
+ slider.currentOption = Math.min(slider.options.length - 1, slider.currentOption + 1);
233
+ slider.value = slider.options[slider.currentOption];
234
+ if (slider.updateProperty) {
235
+ slider.updateProperty(slider.value);
236
+ }
237
+ }
238
+ }
239
+ // Handle continuous sliders
240
+ else {
241
+ // Direct method similar to debugpanel.js
242
+ if (this.game.input.isElementPressed(slider.id, "debug")) {
243
+ const pointerX = this.game.input.getPointerPosition().x;
244
+ const sliderStartX = this.panelX + 160;
245
+ const sliderWidth = 180;
246
+
247
+ // Calculate normalized position (0-1)
248
+ const percentage = Math.max(0, Math.min(1, (pointerX - sliderStartX) / sliderWidth));
249
+
250
+ // Calculate actual value based on min/max
251
+ let newValue = slider.min + (slider.max - slider.min) * percentage;
252
+
253
+ // Apply step if defined
254
+ if (slider.step) {
255
+ newValue = Math.round(newValue / slider.step) * slider.step;
256
+ }
257
+
258
+ // Update slider value
259
+ slider.value = newValue;
260
+ if (slider.updateProperty) {
261
+ slider.updateProperty(newValue);
262
+ }
263
+ }
264
+ }
265
+ });
266
+ }
267
+
268
+ // Initialize input dialog if needed
269
+ initializeInputDialog() {
270
+ if (!this.inputDialog) {
271
+ this.createInputDialog();
272
+ }
273
+ }
274
+
275
+ // Draw sliders on the panel
276
+ drawSliders(sliders) {
277
+ // Make sure input dialog is created
278
+ this.initializeInputDialog();
279
+
280
+ // Register edit buttons if they haven't been registered yet
281
+ if (Object.keys(sliders).length > 0 && this.inputFields.size === 0) {
282
+ this.registerEditButtons(sliders);
283
+ }
284
+ Object.entries(sliders).forEach(([name, slider], index) => {
285
+ const sliderY = this.panelY + 100 + (index * 40);
286
+
287
+ // Draw label
288
+ this.ctx.fillStyle = "#ffffff";
289
+ this.ctx.font = "14px Arial";
290
+ this.ctx.textAlign = "right";
291
+ this.ctx.fillText(name, this.panelX + 150, sliderY + 10);
292
+
293
+ // For sliders with discrete options
294
+ if (slider.options) {
295
+ // Draw left/right buttons
296
+ this.drawOptionButtons(slider, sliderY);
297
+
298
+ // Draw current value
299
+ this.ctx.textAlign = "center";
300
+ this.ctx.fillText(slider.value.toString(), this.panelX + 250, sliderY + 10);
301
+ }
302
+ // For continuous sliders
303
+ else {
304
+ // Draw slider background track
305
+ this.ctx.fillStyle = "#444444";
306
+ this.ctx.fillRect(this.panelX + 160, sliderY, 180, 20);
307
+
308
+ // Calculate position based on value (clamped for display purposes)
309
+ let displayPercentage = 0;
310
+ if (slider.min !== undefined && slider.max !== undefined) {
311
+ // Clamp the display percentage between 0 and 1
312
+ displayPercentage = Math.max(0, Math.min(1, (slider.value - slider.min) / (slider.max - slider.min)));
313
+ }
314
+
315
+ // Draw slider value fill
316
+ if (slider.outOfRange) {
317
+ // Use a special color for out-of-range values
318
+ this.ctx.fillStyle = (slider.value < slider.min) ? "#aa5500" : "#aa0055";
319
+ } else {
320
+ this.ctx.fillStyle = this.game.input.isElementPressed(slider.id, "debug") ? "#00ff00" : "#00aa00";
321
+ }
322
+ this.ctx.fillRect(this.panelX + 160, sliderY, 180 * displayPercentage, 20);
323
+
324
+ // Draw slider handle
325
+ this.ctx.fillStyle = slider.outOfRange ? "#ffaa00" : "#ffffff";
326
+ this.ctx.fillRect(this.panelX + 160 + (180 * displayPercentage) - 2, sliderY - 2, 4, 24);
327
+
328
+ // Draw value text
329
+ this.ctx.textAlign = "right";
330
+ const valueText = Number.isInteger(slider.value) ? slider.value.toString() : slider.value.toFixed(6);
331
+
332
+ // Use different text color for out-of-range values
333
+ if (slider.outOfRange) {
334
+ this.ctx.fillStyle = "#ffaa00";
335
+ // Add indicator for out-of-range values
336
+ if (slider.value < slider.min) {
337
+ this.ctx.fillText(`${valueText} (< min)`, this.panelX + 380, sliderY + 10);
338
+ } else {
339
+ this.ctx.fillText(`${valueText} (> max)`, this.panelX + 380, sliderY + 10);
340
+ }
341
+ } else {
342
+ this.ctx.fillStyle = "#ffffff";
343
+ this.ctx.fillText(valueText, this.panelX + 380, sliderY + 10);
344
+ }
345
+
346
+ // Draw "Edit" button for precise value input
347
+ const hoverId = `${slider.id}_edit`;
348
+ const btnInfo = this.inputFields.get(hoverId);
349
+
350
+ if (btnInfo) {
351
+ const btnX = btnInfo.x;
352
+ const btnY = btnInfo.y;
353
+ const btnWidth = btnInfo.width;
354
+ const btnHeight = btnInfo.height;
355
+
356
+ // Check if mouse is hovering over the edit button
357
+ const isHovered = this.game.input.isElementHovered(hoverId, "debug");
358
+
359
+ // Draw edit button
360
+ this.ctx.fillStyle = isHovered ? "#5555ff" : "#3333aa";
361
+ this.ctx.fillRect(btnX, btnY, btnWidth, btnHeight);
362
+ this.ctx.strokeStyle = "#ffffff";
363
+ this.ctx.lineWidth = 1;
364
+ this.ctx.strokeRect(btnX, btnY, btnWidth, btnHeight);
365
+
366
+ // Draw edit button text
367
+ this.ctx.fillStyle = "#ffffff";
368
+ this.ctx.textAlign = "center";
369
+ this.ctx.textBaseline = "middle";
370
+ this.ctx.fillText("Edit", btnX + btnWidth/2, btnY + btnHeight/2);
371
+ this.ctx.textBaseline = "alphabetic";
372
+ }
373
+ }
374
+ });
375
+ }
376
+
377
+ // Draw option buttons for discrete sliders
378
+ drawOptionButtons(slider, sliderY) {
379
+ // Draw left button
380
+ const leftButtonX = this.panelX + 160 - 30;
381
+ const isLeftHovered = this.game.input.isElementHovered(`${slider.id}_left`, "debug");
382
+ this.ctx.fillStyle = isLeftHovered ? "#666666" : "#444444";
383
+ this.ctx.fillRect(leftButtonX, sliderY, 20, 20);
384
+
385
+ this.ctx.fillStyle = "#ffffff";
386
+ this.ctx.textAlign = "center";
387
+ this.ctx.fillText("<", leftButtonX + 10, sliderY + 10);
388
+
389
+ // Draw right button
390
+ const rightButtonX = this.panelX + 160 + 190;
391
+ const isRightHovered = this.game.input.isElementHovered(`${slider.id}_right`, "debug");
392
+ this.ctx.fillStyle = isRightHovered ? "#666666" : "#444444";
393
+ this.ctx.fillRect(rightButtonX, sliderY, 20, 20);
394
+
395
+ this.ctx.fillStyle = "#ffffff";
396
+ this.ctx.textAlign = "center";
397
+ this.ctx.fillText(">", rightButtonX + 10, sliderY + 10);
398
+ }
399
+
400
+ // Draw toggle controls
401
+ drawToggles(toggles) {
402
+ this.ctx.textAlign = "left";
403
+ this.ctx.font = "14px Arial";
404
+
405
+ toggles.forEach((toggle, index) => {
406
+ const toggleX = this.panelX + 20;
407
+ const toggleY = this.panelY + 100 + (index * 30);
408
+
409
+ // Draw checkbox
410
+ this.ctx.strokeStyle = "#ffffff";
411
+ this.ctx.lineWidth = 2;
412
+ this.ctx.strokeRect(toggleX, toggleY, 20, 20);
413
+
414
+ // Draw check if enabled
415
+ if (toggle.checked) {
416
+ this.ctx.fillStyle = "#55ff55";
417
+ this.ctx.fillRect(toggleX + 4, toggleY + 4, 12, 12);
418
+ }
419
+
420
+ // Draw label (increased spacing from 30 to 40 pixels to prevent overlap)
421
+ this.ctx.fillStyle = "#ffffff";
422
+ this.ctx.fillText(toggle.label, toggleX + 40, toggleY + 14);
423
+ });
424
+ }
425
+
426
+ // Draw button controls
427
+ drawButtons(buttons) {
428
+ buttons.forEach(button => {
429
+ const isHovered = this.game.input.isElementHovered(button.id, "debug");
430
+
431
+ // Draw button background
432
+ this.ctx.fillStyle = isHovered ? this.lightenColor(button.color) : button.color;
433
+ this.ctx.fillRect(button.x, button.y, button.width, button.height);
434
+
435
+ // Draw button border
436
+ this.ctx.strokeStyle = "#ffffff";
437
+ this.ctx.lineWidth = 2;
438
+ this.ctx.strokeRect(button.x, button.y, button.width, button.height);
439
+
440
+ // Draw button text
441
+ this.ctx.fillStyle = "#ffffff";
442
+ this.ctx.textAlign = "center";
443
+ this.ctx.textBaseline = "middle";
444
+ this.ctx.fillText(button.label, button.x + button.width / 2, button.y + button.height / 2);
445
+ });
446
+ }
447
+
448
+ // Draw the toggle button to show/hide the panel
449
+ drawToggleButton() {
450
+ if (!this.toggleId) return;
451
+
452
+ const isHovered = this.game.input.isElementHovered(this.toggleId, "debug");
453
+ const baseColor = this.visible ? "#00aa00" : "#666666";
454
+ const hoverColor = this.visible ? "#00cc00" : "#888888";
455
+
456
+ // Draw button background
457
+ this.ctx.fillStyle = isHovered ? hoverColor : baseColor;
458
+ this.ctx.fillRect(
459
+ this.toggleButton.x,
460
+ this.toggleButton.y,
461
+ this.toggleButton.width,
462
+ this.toggleButton.height
463
+ );
464
+
465
+ // Draw button text
466
+ this.ctx.fillStyle = "#ffffff";
467
+ this.ctx.font = "14px Arial";
468
+ this.ctx.textAlign = "center";
469
+ this.ctx.textBaseline = "middle";
470
+ this.ctx.fillText(
471
+ this.toggleButton.text,
472
+ this.toggleButton.x + this.toggleButton.width / 2,
473
+ this.toggleButton.y + this.toggleButton.height / 2
474
+ );
475
+ }
476
+
477
+ // Draw the tabs at the top of the panel
478
+ drawTabs() {
479
+ this.tabs.forEach((tab, index) => {
480
+ const tabX = this.panelX + (index * (this.panelWidth / this.tabs.length));
481
+ const tabWidth = this.panelWidth / this.tabs.length;
482
+ const isActive = this.activeTab === tab.id;
483
+
484
+ // For Scene panel we need to use "scene" as the prefix
485
+ let panelPrefix;
486
+ if (this.toggleId === 'sceneDebugToggle') {
487
+ panelPrefix = 'scene';
488
+ } else if (this.toggleId === 'weatherDebugToggle') {
489
+ panelPrefix = 'weather';
490
+ } else if (this.toggleId === 'lightingToggleButton') {
491
+ panelPrefix = 'lighting';
492
+ } else {
493
+ panelPrefix = this.toggleId ? this.toggleId.split('Toggle')[0] : 'debug';
494
+ }
495
+
496
+ const tabElementId = `${panelPrefix}_tab_${tab.id}`;
497
+ const isHovered = this.game.input.isElementHovered(tabElementId, "debug");
498
+
499
+ // Tab background
500
+ this.ctx.fillStyle = isActive ? "#559955" : (isHovered ? "#557755" : "#444444");
501
+ this.ctx.fillRect(tabX, this.panelY, tabWidth, 30);
502
+
503
+ // Tab label
504
+ this.ctx.fillStyle = "#ffffff";
505
+ this.ctx.font = "14px Arial";
506
+ this.ctx.textAlign = "center";
507
+ this.ctx.textBaseline = "middle";
508
+ this.ctx.fillText(tab.label, tabX + tabWidth / 2, this.panelY + 15);
509
+ });
510
+ }
511
+
512
+ // Update method to handle input and panel state
513
+ update() {
514
+ // Check toggle button state if it exists
515
+ if (this.toggleId && this.game.input.isElementJustPressed(this.toggleId, "debug")) {
516
+ this.visible = !this.visible;
517
+
518
+ // If panel is becoming visible, additional initialization can be done here
519
+ if (this.visible) {
520
+ this.lastActivatedTime = Date.now();
521
+ this.onShow();
522
+ } else {
523
+ this.onHide();
524
+ }
525
+ }
526
+
527
+ // If panel isn't visible, no need to check other inputs
528
+ if (!this.visible) return;
529
+
530
+ // Check tab selection
531
+ this.tabs.forEach(tab => {
532
+ // For Scene panel we need to use "scene" as the prefix
533
+ let panelPrefix;
534
+ if (this.toggleId === 'sceneDebugToggle') {
535
+ panelPrefix = 'scene';
536
+ } else if (this.toggleId === 'weatherDebugToggle') {
537
+ panelPrefix = 'weather';
538
+ } else if (this.toggleId === 'lightingToggleButton') {
539
+ panelPrefix = 'lighting';
540
+ } else {
541
+ panelPrefix = this.toggleId ? this.toggleId.split('Toggle')[0] : 'debug';
542
+ }
543
+
544
+ const tabElementId = `${panelPrefix}_tab_${tab.id}`;
545
+
546
+ if (this.game.input.isElementJustPressed(tabElementId, "debug")) {
547
+ this.activeTab = tab.id;
548
+ this.onTabChange(tab.id);
549
+ }
550
+ });
551
+
552
+ // Child classes should implement their specific update logic
553
+ this.updateContent();
554
+ }
555
+
556
+ // Override these methods in child classes
557
+ onShow() {
558
+ // Called when panel becomes visible
559
+ this.lastActivatedTime = Date.now();
560
+ }
561
+
562
+
563
+ updateContent() {
564
+ // Update panel content, handle input, etc.
565
+ }
566
+
567
+ // Main draw method
568
+ draw() {
569
+ // Draw toggle button if we have one
570
+ if (this.toggleId) {
571
+ this.drawToggleButton();
572
+ }
573
+
574
+ // If panel isn't visible, don't draw it
575
+ if (!this.visible) return;
576
+
577
+ // Draw panel background
578
+ this.ctx.fillStyle = "rgba(0, 0, 0, 0.85)";
579
+ this.ctx.fillRect(this.panelX, this.panelY, this.panelWidth, this.panelHeight);
580
+
581
+ // Draw tabs if we have multiple
582
+ if (this.tabs.length > 1) {
583
+ this.drawTabs();
584
+ }
585
+
586
+ // Child classes should implement their specific draw logic in drawContent
587
+ this.drawContent();
588
+ }
589
+
590
+ // Override this method in child classes
591
+ drawContent() {
592
+ // Draw panel content based on active tab
593
+ }
594
+
595
+ // Show an input dialog for precise value editing
596
+ showInputDialog(sliderName, slider) {
597
+ console.log(`Showing input dialog for ${sliderName}, current value: ${slider.value}`);
598
+ // Create modal dialog elements if they don't exist
599
+ if (!this.inputDialog) {
600
+ this.createInputDialog();
601
+ }
602
+
603
+ // Set dialog title and current value
604
+ this.dialogTitle.textContent = `Edit ${sliderName}`;
605
+ this.dialogInput.value = slider.value.toString();
606
+ this.currentSlider = slider;
607
+
608
+ // Show the dialog
609
+ this.inputDialogOverlay.style.display = 'block';
610
+ this.inputDialog.style.display = 'block';
611
+
612
+ // Focus the input field
613
+ setTimeout(() => {
614
+ this.dialogInput.focus();
615
+ this.dialogInput.select();
616
+ }, 10);
617
+ }
618
+
619
+ // Create the input dialog DOM elements
620
+ createInputDialog() {
621
+ // Create overlay for modal effect
622
+ this.inputDialogOverlay = document.createElement('div');
623
+ this.inputDialogOverlay.style.position = 'fixed';
624
+ this.inputDialogOverlay.style.top = '0';
625
+ this.inputDialogOverlay.style.left = '0';
626
+ this.inputDialogOverlay.style.width = '100%';
627
+ this.inputDialogOverlay.style.height = '100%';
628
+ this.inputDialogOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
629
+ this.inputDialogOverlay.style.zIndex = '999';
630
+ this.inputDialogOverlay.style.display = 'none';
631
+
632
+ // Create dialog container
633
+ this.inputDialog = document.createElement('div');
634
+ this.inputDialog.style.position = 'fixed';
635
+ this.inputDialog.style.top = '50%';
636
+ this.inputDialog.style.left = '50%';
637
+ this.inputDialog.style.transform = 'translate(-50%, -50%)';
638
+ this.inputDialog.style.backgroundColor = '#333';
639
+ this.inputDialog.style.border = '2px solid #555';
640
+ this.inputDialog.style.borderRadius = '5px';
641
+ this.inputDialog.style.padding = '15px';
642
+ this.inputDialog.style.zIndex = '1000';
643
+ this.inputDialog.style.minWidth = '250px';
644
+ this.inputDialog.style.color = '#fff';
645
+ this.inputDialog.style.display = 'none';
646
+
647
+ // Create dialog title
648
+ this.dialogTitle = document.createElement('div');
649
+ this.dialogTitle.style.fontSize = '16px';
650
+ this.dialogTitle.style.fontWeight = 'bold';
651
+ this.dialogTitle.style.marginBottom = '10px';
652
+ this.dialogTitle.textContent = 'Edit Value';
653
+
654
+ // Create input field
655
+ this.dialogInput = document.createElement('input');
656
+ this.dialogInput.type = 'text';
657
+ this.dialogInput.style.width = '100%';
658
+ this.dialogInput.style.padding = '5px';
659
+ this.dialogInput.style.marginBottom = '15px';
660
+ this.dialogInput.style.backgroundColor = '#222288';
661
+ this.dialogInput.style.color = '#fff';
662
+ this.dialogInput.style.border = '1px solid #555';
663
+ this.dialogInput.style.borderRadius = '3px';
664
+
665
+ // Create button container
666
+ const buttonContainer = document.createElement('div');
667
+ buttonContainer.style.display = 'flex';
668
+ buttonContainer.style.justifyContent = 'space-between';
669
+
670
+ // Create OK button
671
+ this.dialogOkButton = document.createElement('button');
672
+ this.dialogOkButton.textContent = 'OK';
673
+ this.dialogOkButton.style.padding = '5px 15px';
674
+ this.dialogOkButton.style.backgroundColor = '#00aa00';
675
+ this.dialogOkButton.style.color = '#fff';
676
+ this.dialogOkButton.style.border = 'none';
677
+ this.dialogOkButton.style.borderRadius = '3px';
678
+ this.dialogOkButton.style.cursor = 'pointer';
679
+
680
+ // Create Cancel button
681
+ this.dialogCancelButton = document.createElement('button');
682
+ this.dialogCancelButton.textContent = 'Cancel';
683
+ this.dialogCancelButton.style.padding = '5px 15px';
684
+ this.dialogCancelButton.style.backgroundColor = '#aa0000';
685
+ this.dialogCancelButton.style.color = '#fff';
686
+ this.dialogCancelButton.style.border = 'none';
687
+ this.dialogCancelButton.style.borderRadius = '3px';
688
+ this.dialogCancelButton.style.cursor = 'pointer';
689
+
690
+ // Add buttons to container
691
+ buttonContainer.appendChild(this.dialogOkButton);
692
+ buttonContainer.appendChild(this.dialogCancelButton);
693
+
694
+ // Add elements to dialog
695
+ this.inputDialog.appendChild(this.dialogTitle);
696
+ this.inputDialog.appendChild(this.dialogInput);
697
+ this.inputDialog.appendChild(buttonContainer);
698
+
699
+ // Add dialog to document
700
+ document.body.appendChild(this.inputDialogOverlay);
701
+ document.body.appendChild(this.inputDialog);
702
+
703
+ // Add event listeners
704
+ const self = this;
705
+
706
+ // OK button confirms input
707
+ this.dialogOkButton.addEventListener('click', function() {
708
+ self.confirmInputDialog();
709
+ });
710
+
711
+ // Cancel button closes dialog without changes
712
+ this.dialogCancelButton.addEventListener('click', function() {
713
+ self.closeInputDialog();
714
+ });
715
+
716
+ // Enter key confirms input
717
+ this.dialogInput.addEventListener('keydown', function(e) {
718
+ if (e.key === 'Enter') {
719
+ self.confirmInputDialog();
720
+ e.preventDefault();
721
+ } else if (e.key === 'Escape') {
722
+ self.closeInputDialog();
723
+ e.preventDefault();
724
+ }
725
+ });
726
+
727
+ // Allow clicks on the overlay to close the dialog
728
+ this.inputDialogOverlay.addEventListener('click', function(e) {
729
+ self.closeInputDialog();
730
+ });
731
+
732
+ // But prevent clicks on the dialog from bubbling to the overlay
733
+ this.inputDialog.addEventListener('click', function(e) {
734
+ e.stopPropagation();
735
+ });
736
+ }
737
+
738
+ // Confirm the input and update the slider value
739
+ confirmInputDialog() {
740
+ if (!this.currentSlider) return;
741
+
742
+ try {
743
+ const numValue = parseFloat(this.dialogInput.value);
744
+ if (!isNaN(numValue)) {
745
+ // Don't clamp values - allow any input value to be used
746
+ let finalValue = numValue;
747
+
748
+ // Flag if the value is outside the slider's normal range
749
+ if (this.currentSlider.min !== undefined && this.currentSlider.max !== undefined) {
750
+ if (numValue < this.currentSlider.min || numValue > this.currentSlider.max) {
751
+ // Mark the slider as having an out-of-range value
752
+ this.currentSlider.outOfRange = true;
753
+ } else {
754
+ // Reset the flag if value is back in range
755
+ this.currentSlider.outOfRange = false;
756
+ }
757
+ }
758
+
759
+ // Update slider value (no clamping)
760
+ this.currentSlider.value = finalValue;
761
+ if (this.currentSlider.updateProperty) {
762
+ this.currentSlider.updateProperty(finalValue);
763
+ }
764
+ }
765
+ } catch (e) {
766
+ console.error("Error parsing number input:", e);
767
+ }
768
+
769
+ this.closeInputDialog();
770
+ }
771
+
772
+ // Close the input dialog without saving changes
773
+ closeInputDialog() {
774
+ if (this.inputDialog) {
775
+ this.inputDialog.style.display = 'none';
776
+ }
777
+ if (this.inputDialogOverlay) {
778
+ this.inputDialogOverlay.style.display = 'none';
779
+ }
780
+ this.currentSlider = null;
781
+ }
782
+
783
+ // Handle tab or panel visibility changes
784
+ onHide() {
785
+ // Close input dialog if open
786
+ this.closeInputDialog();
787
+
788
+ // Clear edit buttons when panel is hidden
789
+ this.clearEditButtons();
790
+ }
791
+
792
+ onTabChange(tabId) {
793
+ // Close input dialog if open
794
+ this.closeInputDialog();
795
+
796
+ // Re-register edit buttons for the new tab
797
+ const sliders = this.getActiveSliders();
798
+ if (sliders && Object.keys(sliders).length > 0) {
799
+ this.registerEditButtons(sliders);
800
+ }
801
+ }
802
+
803
+
804
+
805
+ // Register edit buttons for sliders
806
+ registerEditButtons(sliders) {
807
+ // First, clear any existing edit buttons
808
+ this.clearEditButtons();
809
+
810
+ Object.entries(sliders).forEach(([name, slider], index) => {
811
+ if (!slider.options) { // Only for numeric sliders
812
+ const sliderY = this.panelY + 100 + (index * 40);
813
+ const btnX = this.panelX + 400;
814
+ const btnY = sliderY - 2;
815
+ const btnWidth = 45; // Wider button
816
+ const btnHeight = 26; // Taller button
817
+
818
+ const hoverId = `${slider.id}_edit`;
819
+
820
+ // Register the edit button with input system
821
+ this.game.input.registerElement(
822
+ hoverId,
823
+ {
824
+ bounds: () => ({
825
+ x: btnX - 2, // Add a bit more hit area
826
+ y: btnY - 2, // Add a bit more hit area
827
+ width: btnWidth + 4, // Add a bit more hit area
828
+ height: btnHeight + 4 // Add a bit more hit area
829
+ })
830
+ },
831
+ "debug"
832
+ );
833
+
834
+ this.inputFields.set(hoverId, {
835
+ slider: slider,
836
+ name: name,
837
+ x: btnX,
838
+ y: btnY,
839
+ width: btnWidth,
840
+ height: btnHeight
841
+ });
842
+ }
843
+ });
844
+ }
845
+
846
+
847
+
848
+ // Clear all registered edit buttons
849
+ clearEditButtons() {
850
+ // Remove all buttons from the input system
851
+ this.inputFields.forEach((info, id) => {
852
+ this.game.input.removeElement(id, "debug");
853
+ });
854
+
855
+ // Clear the inputFields map
856
+ this.inputFields.clear();
857
+ }
858
+ }