pixospritz-core 0.10.1 → 1.0.1

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 (157) hide show
  1. package/README.md +36 -286
  2. package/dist/bundle.js +13 -3
  3. package/dist/bundle.js.map +1 -1
  4. package/dist/style.css +1 -0
  5. package/package.json +43 -44
  6. package/src/components/WebGLView.jsx +318 -0
  7. package/src/css/pixos.css +372 -0
  8. package/src/engine/actions/animate.js +41 -0
  9. package/src/engine/actions/changezone.js +135 -0
  10. package/src/engine/actions/chat.js +109 -0
  11. package/src/engine/actions/dialogue.js +90 -0
  12. package/src/engine/actions/face.js +22 -0
  13. package/src/engine/actions/greeting.js +28 -0
  14. package/src/engine/actions/interact.js +86 -0
  15. package/src/engine/actions/move.js +67 -0
  16. package/src/engine/actions/patrol.js +109 -0
  17. package/src/engine/actions/prompt.js +185 -0
  18. package/src/engine/actions/script.js +42 -0
  19. package/src/engine/core/audio/AudioSystem.js +543 -0
  20. package/src/engine/core/cutscene/PxcPlayer.js +956 -0
  21. package/src/engine/core/cutscene/manager.js +243 -0
  22. package/src/engine/core/database/index.js +75 -0
  23. package/src/engine/core/debug/index.js +371 -0
  24. package/src/engine/core/hud/index.js +765 -0
  25. package/src/engine/core/index.js +540 -0
  26. package/src/engine/core/input/gamepad/Controller.js +71 -0
  27. package/src/engine/core/input/gamepad/ControllerButtons.js +231 -0
  28. package/src/engine/core/input/gamepad/ControllerStick.js +173 -0
  29. package/src/engine/core/input/gamepad/index.js +592 -0
  30. package/src/engine/core/input/keyboard.js +196 -0
  31. package/src/engine/core/input/manager.js +485 -0
  32. package/src/engine/core/input/mouse.js +203 -0
  33. package/src/engine/core/input/touch.js +175 -0
  34. package/src/engine/core/mode/manager.js +199 -0
  35. package/src/engine/core/net/manager.js +535 -0
  36. package/src/engine/core/queue/action.js +83 -0
  37. package/src/engine/core/queue/event.js +82 -0
  38. package/src/engine/core/queue/index.js +44 -0
  39. package/src/engine/core/queue/loadable.js +33 -0
  40. package/src/engine/core/render/CameraEffects.js +494 -0
  41. package/src/engine/core/render/FrustumCuller.js +417 -0
  42. package/src/engine/core/render/LODManager.js +285 -0
  43. package/src/engine/core/render/ParticleManager.js +529 -0
  44. package/src/engine/core/render/TextureAtlas.js +465 -0
  45. package/src/engine/core/render/camera.js +338 -0
  46. package/src/engine/core/render/light.js +197 -0
  47. package/src/engine/core/render/manager.js +1079 -0
  48. package/src/engine/core/render/shaders.js +110 -0
  49. package/src/engine/core/render/skybox.js +342 -0
  50. package/src/engine/core/resource/manager.js +133 -0
  51. package/src/engine/core/resource/object.js +611 -0
  52. package/src/engine/core/resource/texture.js +103 -0
  53. package/src/engine/core/resource/tileset.js +177 -0
  54. package/src/engine/core/scene/avatar.js +215 -0
  55. package/src/engine/core/scene/speech.js +138 -0
  56. package/src/engine/core/scene/sprite.js +702 -0
  57. package/src/engine/core/scene/spritz.js +189 -0
  58. package/src/engine/core/scene/world.js +681 -0
  59. package/src/engine/core/scene/zone.js +1167 -0
  60. package/src/engine/core/store/index.js +110 -0
  61. package/src/engine/dynamic/animatedSprite.js +64 -0
  62. package/src/engine/dynamic/animatedTile.js +98 -0
  63. package/src/engine/dynamic/avatar.js +110 -0
  64. package/src/engine/dynamic/map.js +174 -0
  65. package/src/engine/dynamic/sprite.js +255 -0
  66. package/src/engine/dynamic/spritz.js +119 -0
  67. package/src/engine/events/EventSystem.js +609 -0
  68. package/src/engine/events/camera.js +142 -0
  69. package/src/engine/events/chat.js +75 -0
  70. package/src/engine/events/menu.js +186 -0
  71. package/src/engine/scripting/CallbackManager.js +514 -0
  72. package/src/engine/scripting/PixoScriptInterpreter.js +81 -0
  73. package/src/engine/scripting/PixoScriptLibrary.js +704 -0
  74. package/src/engine/shaders/effects/index.js +450 -0
  75. package/src/engine/shaders/fs.js +222 -0
  76. package/src/engine/shaders/particles/fs.js +41 -0
  77. package/src/engine/shaders/particles/vs.js +61 -0
  78. package/src/engine/shaders/picker/fs.js +34 -0
  79. package/src/engine/shaders/picker/init.js +62 -0
  80. package/src/engine/shaders/picker/vs.js +42 -0
  81. package/src/engine/shaders/pxsl/README.md +250 -0
  82. package/src/engine/shaders/pxsl/index.js +25 -0
  83. package/src/engine/shaders/pxsl/library.js +608 -0
  84. package/src/engine/shaders/pxsl/manager.js +338 -0
  85. package/src/engine/shaders/pxsl/specification.js +363 -0
  86. package/src/engine/shaders/pxsl/transpiler.js +753 -0
  87. package/src/engine/shaders/skybox/cosmic/fs.js +147 -0
  88. package/src/engine/shaders/skybox/cosmic/vs.js +23 -0
  89. package/src/engine/shaders/skybox/matrix/fs.js +127 -0
  90. package/src/engine/shaders/skybox/matrix/vs.js +23 -0
  91. package/src/engine/shaders/skybox/morning/fs.js +109 -0
  92. package/src/engine/shaders/skybox/morning/vs.js +23 -0
  93. package/src/engine/shaders/skybox/neon/fs.js +119 -0
  94. package/src/engine/shaders/skybox/neon/vs.js +23 -0
  95. package/src/engine/shaders/skybox/sky/fs.js +114 -0
  96. package/src/engine/shaders/skybox/sky/vs.js +23 -0
  97. package/src/engine/shaders/skybox/sunset/fs.js +101 -0
  98. package/src/engine/shaders/skybox/sunset/vs.js +23 -0
  99. package/src/engine/shaders/transition/blur/fs.js +42 -0
  100. package/src/engine/shaders/transition/blur/vs.js +26 -0
  101. package/src/engine/shaders/transition/cross/fs.js +36 -0
  102. package/src/engine/shaders/transition/cross/vs.js +26 -0
  103. package/src/engine/shaders/transition/crossBlur/fs.js +41 -0
  104. package/src/engine/shaders/transition/crossBlur/vs.js +25 -0
  105. package/src/engine/shaders/transition/dissolve/fs.js +78 -0
  106. package/src/engine/shaders/transition/dissolve/vs.js +24 -0
  107. package/src/engine/shaders/transition/fade/fs.js +31 -0
  108. package/src/engine/shaders/transition/fade/vs.js +27 -0
  109. package/src/engine/shaders/transition/iris/fs.js +52 -0
  110. package/src/engine/shaders/transition/iris/vs.js +24 -0
  111. package/src/engine/shaders/transition/pixelate/fs.js +44 -0
  112. package/src/engine/shaders/transition/pixelate/vs.js +24 -0
  113. package/src/engine/shaders/transition/slide/fs.js +53 -0
  114. package/src/engine/shaders/transition/slide/vs.js +24 -0
  115. package/src/engine/shaders/transition/swirl/fs.js +39 -0
  116. package/src/engine/shaders/transition/swirl/vs.js +26 -0
  117. package/src/engine/shaders/transition/wipe/fs.js +50 -0
  118. package/src/engine/shaders/transition/wipe/vs.js +24 -0
  119. package/src/engine/shaders/vs.js +60 -0
  120. package/src/engine/utils/CameraController.js +506 -0
  121. package/src/engine/utils/ObjHelper.js +551 -0
  122. package/src/engine/utils/debug-logger.js +110 -0
  123. package/src/engine/utils/enums.js +305 -0
  124. package/src/engine/utils/generator.js +156 -0
  125. package/src/engine/utils/index.js +21 -0
  126. package/src/engine/utils/loaders/ActionLoader.js +77 -0
  127. package/src/engine/utils/loaders/AudioLoader.js +157 -0
  128. package/src/engine/utils/loaders/EventLoader.js +66 -0
  129. package/src/engine/utils/loaders/ObjectLoader.js +67 -0
  130. package/src/engine/utils/loaders/SpriteLoader.js +77 -0
  131. package/src/engine/utils/loaders/TilesetLoader.js +103 -0
  132. package/src/engine/utils/loaders/index.js +21 -0
  133. package/src/engine/utils/math/matrix4.js +367 -0
  134. package/src/engine/utils/math/vector.js +458 -0
  135. package/src/engine/utils/obj/_old_js/index.js +46 -0
  136. package/src/engine/utils/obj/_old_js/layout.js +308 -0
  137. package/src/engine/utils/obj/_old_js/material.js +711 -0
  138. package/src/engine/utils/obj/_old_js/mesh.js +761 -0
  139. package/src/engine/utils/obj/_old_js/utils.js +647 -0
  140. package/src/engine/utils/obj/index.js +24 -0
  141. package/src/engine/utils/obj/js/index.js +277 -0
  142. package/src/engine/utils/obj/js/loader.js +232 -0
  143. package/src/engine/utils/obj/layout.js +246 -0
  144. package/src/engine/utils/obj/material.js +665 -0
  145. package/src/engine/utils/obj/mesh.js +657 -0
  146. package/src/engine/utils/obj/ts/index.ts +72 -0
  147. package/src/engine/utils/obj/ts/layout.ts +265 -0
  148. package/src/engine/utils/obj/ts/material.ts +760 -0
  149. package/src/engine/utils/obj/ts/mesh.ts +785 -0
  150. package/src/engine/utils/obj/ts/utils.ts +501 -0
  151. package/src/engine/utils/obj/utils.js +428 -0
  152. package/src/engine/utils/resources.js +18 -0
  153. package/src/index.jsx +55 -0
  154. package/src/spritz/player.js +18 -0
  155. package/src/spritz/readme.md +18 -0
  156. package/LICENSE +0 -437
  157. package/dist/bundle.js.LICENSE.txt +0 -31
@@ -0,0 +1,540 @@
1
+ /* *\
2
+ ** ----------------------------------------------- **
3
+ ** Calliope - Pixos Game Engine **
4
+ ** ----------------------------------------------- **
5
+ ** Copyright (c) 2020-2025 - Kyle Derby MacInnis **
6
+ ** **
7
+ ** Any unauthorized distribution or transfer **
8
+ ** of this work is strictly prohibited. **
9
+ ** **
10
+ ** All Rights Reserved. **
11
+ ** ----------------------------------------------- **
12
+ \* */
13
+
14
+ import Utils from '../utils/index.js';
15
+ import Database from './database/index.js';
16
+ import Store from './store/index.js';
17
+ import Hud from './hud/index.js';
18
+ import RenderManager from './render/manager.js';
19
+ import ResourceManager from './resource/manager.js';
20
+ import CutsceneManager from './cutscene/manager.js';
21
+ import ModeManager from './mode/manager.js'; // Import ModeManager
22
+ import InputManager from './input/manager.js'; // Import InputManager
23
+ import NetworkManager from './net/manager.js';
24
+ import { attachFlagDebugInfo, attachWebglDebugInfo, updateDebugInformation } from './debug/index.js';
25
+
26
+ /**
27
+ * @typedef {object} SpritzGame
28
+ * @property {function(GLEngine): Promise<void>} init - Initializes the game.
29
+ * @property {function(GLEngine, number): void} render - Renders the game scene.
30
+ * @property {function(number): void} update - Updates the game state.
31
+ * @property {import('./scene/world.js').default} world - The game world instance.
32
+ * @property {object} shaders - Shader programs for the game.
33
+ * @property {object} effects - Visual effects for the game.
34
+ * @property {boolean} loaded - Indicates if the game resources are loaded.
35
+ * @property {object} [manifest] - Game manifest with network settings.
36
+ */
37
+
38
+ /**
39
+ * Core Pixos Graphics & Game Engine.
40
+ * Orchestrates the main game loop, rendering, input handling, and resource management.
41
+ */
42
+ export default class GLEngine {
43
+ /**
44
+ * Creates an instance of GLEngine.
45
+ * @param {HTMLCanvasElement} canvas - The main WebGL canvas.
46
+ * @param {HTMLCanvasElement} hudCanvas - The 2D canvas for HUD elements.
47
+ * @param {HTMLCanvasElement} mipmap - The canvas for mipmap generation (or similar utility).
48
+ * @param {HTMLCanvasElement} gamepadCanvas - The 2D canvas for mobile gamepad controls.
49
+ * @param {HTMLInputElement} fileUpload - The file input element for resource loading.
50
+ * @param {number} width - The desired width of the game viewport.
51
+ * @param {number} height - The desired height of the game viewport.
52
+ */
53
+ constructor(canvas, hudCanvas, mipmap, gamepadCanvas, fileUpload, width, height) {
54
+ /** @type {HTMLCanvasElement} */
55
+ this.canvas = canvas;
56
+ /** @type {HTMLCanvasElement} */
57
+ this.hudCanvas = hudCanvas;
58
+ /** @type {HTMLCanvasElement} */
59
+ this.gamepadCanvas = gamepadCanvas;
60
+ /** @type {HTMLCanvasElement} */
61
+ this.mipmap = mipmap;
62
+
63
+ /** @type {HTMLInputElement} */
64
+ this.fileUpload = fileUpload;
65
+
66
+ /** @type {number} */
67
+ this.width = width;
68
+ /** @type {number} */
69
+ this.height = height;
70
+
71
+ /** @type {object} */
72
+ this.utils = Utils;
73
+
74
+ /** @type {NetworkManager} */
75
+ this.networkManager = new NetworkManager(this);
76
+ // ResourceManager and RenderManager depend on a valid WebGL context
77
+ // and are initialized in `init()` after `this.gl` is created.
78
+ /** @type {ResourceManager|null} */
79
+ this.resourceManager = null;
80
+
81
+ /** @type {RenderManager|null} */
82
+ this.renderManager = null;
83
+
84
+ /** @type {Hud} */
85
+ this.hud = new Hud(this);
86
+
87
+ /** @type {InputManager} */
88
+ this.inputManager = new InputManager(this); // Initialize InputManager
89
+
90
+ /** @type {SpeechSynthesisUtterance} */
91
+ this.voice = new SpeechSynthesisUtterance();
92
+
93
+ /** @type {Database} */
94
+ this.database = new Database();
95
+
96
+ /** @type {Store} */
97
+ this.store = new Store();
98
+
99
+ /** @type {CutsceneManager} */
100
+ this.cutsceneManager = new CutsceneManager(this);
101
+
102
+ /** @type {ModeManager} */
103
+ this.modeManager = new ModeManager(this); // Initialize ModeManager
104
+
105
+ // Debug flags
106
+ /** @type {boolean} */
107
+ this.debug = false; // General debug mode (enables console logs)
108
+ /** @type {boolean} */
109
+ this.debugHeightOverlay = false; // Height debug overlay (shows z values on screen)
110
+
111
+ // Game Loop
112
+ /** @type {boolean} */
113
+ this.running = false;
114
+
115
+ /** @type {WebGL2RenderingContext|null} */
116
+ this.gl = null;
117
+ /** @type {CanvasRenderingContext2D|null} */
118
+ this.ctx = null;
119
+ /** @type {CanvasRenderingContext2D|null} */
120
+ this.gp = null;
121
+ /** @type {number} */
122
+ this.frameCount = 0;
123
+ /** @type {SpritzGame|null} */
124
+ this.spritz = null;
125
+ /** @type {boolean} */
126
+ this.fullscreen = false;
127
+ /** @type {number} */
128
+ this.time = 0;
129
+ /** @type {number|null} */
130
+ this.requestId = null; // For requestAnimationFrame
131
+
132
+ // Bind methods to the instance
133
+ this.screenSize = this.screenSize.bind(this);
134
+ this.render = this.render.bind(this);
135
+ this.init = this.init.bind(this);
136
+ this.close = this.close.bind(this);
137
+ }
138
+
139
+ /**
140
+ * Initializes the game engine and the Spritz game instance.
141
+ * @param {SpritzGame} spritz - The Spritz game object to initialize.
142
+ * @returns {Promise<void>} A promise that resolves when initialization is complete.
143
+ * @throws {Error} If WebGL, HUD canvas, or Gamepad canvas cannot be initialized.
144
+ */
145
+ async init(spritz) {
146
+ /** @type {CanvasRenderingContext2D|null} */
147
+ const ctx = this.hudCanvas.getContext('2d');
148
+ /** @type {WebGL2RenderingContext|null} */
149
+ const gl = this.canvas.getContext('webgl2', {
150
+ antialias: true,
151
+ depth: true,
152
+ preserveDrawingBuffer: false
153
+ });
154
+ /** @type {CanvasRenderingContext2D|null} */
155
+ const gp = this.gamepadCanvas.getContext('2d');
156
+
157
+ if (!gl) {
158
+ throw new Error('WebGL: unable to initialize');
159
+ }
160
+ if (!ctx) {
161
+ throw new Error('Canvas: unable to initialize HUD');
162
+ }
163
+ if (!gp) {
164
+ throw new Error('Gamepad: unable to initialize Mobile Canvas');
165
+ }
166
+
167
+ // Make HUD same size as canvas
168
+ ctx.canvas.width = gl.canvas.clientWidth;
169
+ ctx.canvas.height = gl.canvas.clientHeight;
170
+
171
+ /** @type {WebGL2RenderingContext} */
172
+ this.gl = gl;
173
+ /** @type {CanvasRenderingContext2D} */
174
+ this.ctx = ctx;
175
+ /** @type {CanvasRenderingContext2D} */
176
+ this.gp = gp;
177
+ this.frameCount = 0;
178
+
179
+ this.spritz = spritz;
180
+ this.fullscreen = false;
181
+
182
+ // Initial time
183
+ this.time = new Date().getTime();
184
+
185
+ // Init Input Manager
186
+ this.inputManager.init();
187
+
188
+ // Initialize HUD
189
+ this.hud.init();
190
+
191
+ // Initialize render manager and resource manager now that `this.gl` exists
192
+ if (!this.resourceManager) {
193
+ this.resourceManager = new ResourceManager(this);
194
+ }
195
+
196
+ if (!this.renderManager) {
197
+ this.renderManager = new RenderManager(this);
198
+ }
199
+
200
+ this.renderManager.init();
201
+
202
+ // Configure Gamepad & touch - now handled through InputManager
203
+ // Direct access deprecated, use inputManager instead
204
+ /** @deprecated Use inputManager.gamepad instead. */
205
+ this.gamepad = this.inputManager.gamepad;
206
+ /** @deprecated Use inputManager.keyboard instead. */
207
+ this.keyboard = this.inputManager.keyboard;
208
+ /** @deprecated Use inputManager.mouse instead. */
209
+ this.mouse = this.inputManager.mouse;
210
+ /** @deprecated Use inputManager.touch instead. */
211
+ this.touch = this.inputManager.touch;
212
+
213
+ /** @deprecated Eventually move this into inputManager.touch instead. */
214
+ this.touchHandler = this.gamepad.listen.bind(this.gamepad);
215
+
216
+
217
+ // Initialize network if enabled
218
+ if (spritz.manifest?.network?.enabled) {
219
+ await this.networkManager.connect(spritz.manifest.network.url);
220
+ if (spritz.manifest.network.authority) {
221
+ this.networkManager.setAuthority(spritz.manifest.network.authority);
222
+ }
223
+ }
224
+
225
+ // Initialize Spritz game
226
+ await spritz.init(this);
227
+
228
+ // Create and configure debug overlays. These overlays display
229
+ // performance information such as FPS and draw counts (toggled by F3)
230
+ // and flag information (toggled by F4). They are appended to the document body
231
+ // once the engine has been initialized and the DOM is available. The overlays
232
+ // remain hidden until toggled.
233
+ attachWebglDebugInfo(this);
234
+ attachFlagDebugInfo(this);
235
+ }
236
+
237
+ /**
238
+ * The main render loop for the game engine.
239
+ * Called continuously via `requestAnimationFrame` to update and draw the game.
240
+ * Handles debug counters, clears canvases, updates game state, renders the scene, and manages transitions.
241
+ */
242
+ render() {
243
+ this.frameCount++;
244
+ // Reset debug counters at the start of each frame so that metrics
245
+ // reflect only the current frame's draw calls.
246
+ if (this.renderManager && this.renderManager.resetDebugCounters) {
247
+ this.renderManager.resetDebugCounters();
248
+ }
249
+
250
+ // Clear canvases
251
+ this.hud.clearHud();
252
+ // Draw active mode label (if any)
253
+ if (this.hud.drawModeLabel) this.hud.drawModeLabel();
254
+ this.renderManager.clearScreen();
255
+
256
+ const timestamp = new Date().getTime();
257
+
258
+ // Update Input Manager
259
+ this.inputManager.update();
260
+
261
+ // Object picking pass (for selection) - only if mode has picker enabled
262
+ if (this.modeManager.hasPicker()) {
263
+ // Enable picker shader (Todo - Improve performance - make it only 1x1 pixel framebuffer - and avoid needing to reclear screen).
264
+ this.renderManager.activatePickerShaderProgram(false);
265
+ this.spritz.render(this, timestamp); // Render scene for picking pass
266
+ // Read pixel data immediately after picking render, before clearing screen
267
+ this.getSelectedObject('sprite|object|tile', false);
268
+ }
269
+
270
+ // Update and render based on the active game mode
271
+ if (!this.inputManager.handleInput(timestamp)) {
272
+ // If mode doesn't handle input, do default update
273
+ this.spritz.update(timestamp);
274
+ }
275
+
276
+ // Sync input mode with game mode
277
+ const currentMode = this.modeManager.getMode();
278
+ if (currentMode && this.inputManager.getMode() !== currentMode) {
279
+ this.inputManager.setMode(currentMode);
280
+ }
281
+
282
+ // Core render loop (actually render scene to screen)
283
+ const gl = this.renderManager.engine.gl;
284
+ this.renderManager.clearScreen();
285
+ // Draw skybox first, with depth writes disabled
286
+ gl.depthMask(false);
287
+ this.renderManager.renderSkybox();
288
+ gl.depthMask(true);
289
+ // Now draw world tiles/objects, then sprites
290
+ this.renderManager.activateShaderProgram();
291
+
292
+ this.modeManager.update(timestamp); // Update active mode
293
+
294
+ // Allow particle system to update physics with a stable timestamp
295
+ if (this.renderManager && this.renderManager.updateParticles) {
296
+ try { this.renderManager.updateParticles(timestamp); } catch (e) { console.warn('updateParticles failed', e); }
297
+ }
298
+
299
+ this.spritz.render(this); // Render scene (might be overridden by mode)
300
+
301
+ this.cutsceneManager.update(); // Update cutscene (if applicable)
302
+ this.renderManager.updateTransition(); // Update transitions
303
+
304
+ // Render particles after main scene but before HUD/gamepad
305
+ if (this.renderManager && this.renderManager.renderParticles) {
306
+ try { this.renderManager.renderParticles(); } catch (e) { console.warn('renderParticles failed', e); }
307
+ }
308
+ this.gamepad.render(); // Render gamepad (may be optimizable?)
309
+
310
+ // Draw height debug overlay if enabled (shows tile/sprite/object z values on screen)
311
+ if (this.debugHeightOverlay && this.hud.drawHeightDebugOverlay) {
312
+ try { this.hud.drawHeightDebugOverlay(); } catch (e) { console.warn('drawHeightDebugOverlay failed', e); }
313
+ }
314
+
315
+ // Update debug overlay if enabled
316
+ updateDebugInformation(this);
317
+
318
+ this.requestId = requestAnimationFrame(this.render);
319
+ }
320
+
321
+ /**
322
+ * Stops the main render loop.
323
+ */
324
+ close() {
325
+ if (this.requestId) {
326
+ cancelAnimationFrame(this.requestId);
327
+ this.requestId = null;
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Detects and returns the selected object on screen based on mouse/touch input.
333
+ * Uses a color-picking technique where objects are rendered with unique IDs to an off-screen buffer,
334
+ * and the pixel under the cursor is read to identify the object.
335
+ * @param {'sprite'|'object'|'tile'|string} [type='sprite|object|tile'] - The type(s) of objects to consider for selection, pipe-separated.
336
+ * @param {boolean} [useFrustum=false] - Whether to use a 1x1 pixel frustum for picking (performance optimization).
337
+ * @returns {number|null} The ID of the selected object, or null if no object is selected or freecam is active.
338
+ */
339
+ getSelectedObject(type = 'sprite|object|tile', useFrustum = false) {
340
+ // When FreeCam is active, suppress picking to avoid interfering with camera controls
341
+ if (this._freecamActive) return null;
342
+ if (this.spritz.world?.spriteList?.length <= 0 && this.spritz.world?.objectList?.length <= 0 && this.spritz.world?.zoneList?.length <= 0) {
343
+ return null; // No pickable objects in the scene
344
+ }
345
+
346
+ const gl = this.gl;
347
+ const data = new Uint8Array(4);
348
+ const mouseX = this.gamepad.x || 0;
349
+ const mouseY = this.gamepad.y || 0;
350
+ const pixelX = useFrustum ? 0 : (mouseX * gl.canvas.width) / gl.canvas.clientWidth;
351
+ const pixelY = useFrustum ? 0 : gl.canvas.height - (mouseY * gl.canvas.height) / gl.canvas.clientHeight - 1;
352
+
353
+ gl.readPixels(
354
+ pixelX, // x
355
+ pixelY, // y
356
+ 1, // width
357
+ 1, // height
358
+ gl.RGBA, // format
359
+ gl.UNSIGNED_BYTE, // type
360
+ data
361
+ ); // typed array to hold result
362
+
363
+ let id = data[0] + (data[1] << 8) + (data[2] << 16);
364
+
365
+ // Only process selection if a left click occurred this frame
366
+ if (!this.inputManager.isActionPressed('select')) {
367
+ return id;
368
+ }
369
+
370
+ // Select type(s) based on request
371
+ type.split('|').forEach((t) => {
372
+ switch (t) {
373
+ case 'sprite':
374
+ this.spritz.world.spriteList = this.spritz.world.spriteList.map((sprite) => {
375
+ if (sprite.objId === id) {
376
+ sprite.isSelected = true;
377
+ if (this.spritz.world.spriteDict[sprite.id]) {
378
+ this.spritz.world.spriteDict[sprite.id].isSelected = true;
379
+ // Allow mode to handle selection first
380
+ if (!this.modeManager.handleSelect(sprite.zone, sprite, null, 'sprite')) {
381
+ // TODO: Add a new trigger method onSelect()
382
+ if (typeof this.spritz.world.spriteDict[sprite.id].onSelect === 'function') {
383
+ this.spritz.world.spriteDict[sprite.id].onSelect(sprite.zone, sprite);
384
+ }
385
+ }
386
+ }
387
+ } else {
388
+ sprite.isSelected = false; // Deselect others
389
+ }
390
+ return sprite;
391
+ });
392
+ break;
393
+ case 'object':
394
+ this.spritz.world.objectList = this.spritz.world.objectList.map((obj) => {
395
+ if (obj.objId === id) {
396
+ obj.isSelected = true;
397
+ if (this.spritz.world.objectDict[obj.id]) {
398
+ this.spritz.world.objectDict[obj.id].isSelected = true;
399
+ // Allow mode to handle selection first
400
+ if (!this.modeManager.handleSelect(obj.zone, obj, null, 'object')) {
401
+ // TODO: Add a new trigger method onSelect()
402
+ if (typeof this.spritz.world.objectDict[obj.id].onSelect === 'function') {
403
+ this.spritz.world.objectDict[obj.id].onSelect(obj.zone, obj);
404
+ }
405
+ }
406
+ }
407
+ } else {
408
+ obj.isSelected = false; // Deselect others
409
+ }
410
+ return obj;
411
+ });
412
+ break;
413
+ case 'tile':
414
+ // Read in zone, tile, and cell data from pixel
415
+ let zoneObjId = data[0];
416
+ let row = data[1];
417
+ let cell = data[2];
418
+
419
+ // Search zones and find selected tile
420
+ this.spritz.world.zoneList.forEach((zone) => {
421
+ if (zone.objId === zoneObjId) {
422
+ // Allow mode to handle selection first
423
+ if (!this.modeManager.handleSelect(zone, row, cell, 'tile')) {
424
+ if (typeof zone.onSelect === 'function') {
425
+ zone.onSelect(row, cell);
426
+ }
427
+ }
428
+ }
429
+ });
430
+ if (id !== 0) { // If a valid ID was picked (not background)
431
+ console.log('TILE SELECTION:', { zoneObjId, row, cell, zones: this.spritz.world.zoneList });
432
+ }
433
+ break;
434
+ }
435
+ });
436
+
437
+ return id;
438
+ }
439
+
440
+ /**
441
+ * Sets a greeting text.
442
+ * @deprecated This method should be moved to a more appropriate class, e.g., `Hud` or a new `DialogueManager`.
443
+ * @param {string} text - The greeting text to set.
444
+ */
445
+ setGreeting(text) {
446
+ if (process.env.NODE_ENV === 'development') {
447
+ console.log('Setting GREETING:', text);
448
+ }
449
+ // Assuming globalStore exists and is the correct place for this
450
+ if (this.globalStore) {
451
+ this.globalStore.greeting = text;
452
+ } else {
453
+ console.warn('globalStore is not available to set greeting.');
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Converts text to speech using the Web Speech API.
459
+ * @param {string} text - The text to speak.
460
+ * @param {SpeechSynthesisVoice|null} [voice=null] - The voice to use. Defaults to the first available voice.
461
+ * @param {string} [lang='en'] - The language of the speech.
462
+ * @param {number|null} [rate=null] - The speed of the speech (0.1 to 10).
463
+ * @param {number|null} [volume=null] - The volume of the speech (0 to 1).
464
+ * @param {number|null} [pitch=null] - The pitch of the speech (0 to 2).
465
+ */
466
+ speechSynthesis(text, voice = null, lang = 'en', rate = null, volume = null, pitch = null) {
467
+ /** @type {SpeechSynthesisUtterance} */
468
+ let speech = this.voice;
469
+ /** @type {SpeechSynthesisVoice[]} */
470
+ let voices = window.speechSynthesis.getVoices() ?? [];
471
+ // Set voice
472
+ speech.voice = voice || voices[0];
473
+ if (rate) speech.rate = rate;
474
+ if (volume) speech.volume = volume;
475
+ if (pitch) speech.pitch = pitch;
476
+ speech.text = text;
477
+ speech.lang = lang;
478
+ // Speak
479
+ window.speechSynthesis.speak(speech);
480
+ }
481
+
482
+
483
+ /**
484
+ * Returns the current client width and height of the main canvas.
485
+ * @returns {{width: number, height: number}} An object containing the width and height.
486
+ */
487
+ screenSize() {
488
+ return {
489
+ width: this.canvas.clientWidth,
490
+ height: this.canvas.clientHeight,
491
+ };
492
+ }
493
+
494
+ /**
495
+ * Handles canvas resize events. Updates internal dimensions and notifies
496
+ * relevant subsystems (HUD, RenderManager, Gamepad) of the size change.
497
+ * This should be called when the canvas container is resized.
498
+ */
499
+ handleResize() {
500
+ if (!this.gl || !this.ctx) return;
501
+
502
+ const displayWidth = this.canvas.clientWidth;
503
+ const displayHeight = this.canvas.clientHeight;
504
+
505
+ // Check if the canvas needs resizing
506
+ if (this.canvas.width !== displayWidth || this.canvas.height !== displayHeight) {
507
+ // Update canvas internal dimensions to match display size
508
+ this.canvas.width = displayWidth;
509
+ this.canvas.height = displayHeight;
510
+
511
+ // Update HUD canvas
512
+ if (this.hudCanvas) {
513
+ this.hudCanvas.width = displayWidth;
514
+ this.hudCanvas.height = displayHeight;
515
+ }
516
+
517
+ // Update engine width/height
518
+ this.width = displayWidth;
519
+ this.height = displayHeight;
520
+
521
+ // Update WebGL viewport
522
+ this.gl.viewport(0, 0, displayWidth, displayHeight);
523
+
524
+ // Update render manager if available
525
+ if (this.renderManager && this.renderManager.handleResize) {
526
+ this.renderManager.handleResize(displayWidth, displayHeight);
527
+ }
528
+
529
+ // Update gamepad/input manager if available
530
+ if (this.inputManager && this.inputManager.gamepad) {
531
+ this.inputManager.gamepad.resize();
532
+ }
533
+
534
+ // Update HUD
535
+ if (this.hud && this.hud.handleResize) {
536
+ this.hud.handleResize(displayWidth, displayHeight);
537
+ }
538
+ }
539
+ }
540
+ }
@@ -0,0 +1,71 @@
1
+ /* *\
2
+ ** ----------------------------------------------- **
3
+ ** Calliope - Pixos Game Engine **
4
+ ** ----------------------------------------------- **
5
+ ** Copyright (c) 2020-2025 - Kyle Derby MacInnis **
6
+ ** **
7
+ ** Any unauthorized distribution or transfer **
8
+ ** of this work is strictly prohibited. **
9
+ ** **
10
+ ** All Rights Reserved. **
11
+ ** ----------------------------------------------- **
12
+ \* */
13
+
14
+ import { ControllerStick } from '@Engine/core/input/gamepad/ControllerStick.js';
15
+ import { ControllerButtons } from '@Engine/core/input/gamepad/ControllerButtons.js';
16
+ // Controller Manager for Gamepad
17
+ export class Controller {
18
+ /**
19
+ * Creates an instance of Controller.
20
+ * @param {CanvasRenderingContext2D} ctx - The canvas rendering context.
21
+ * @param {Object} buttonOffset - The offset for button placement.
22
+ * @param {Object.<string, any>} touches - The touch state object.
23
+ * @param {boolean} start - Whether start button is enabled.
24
+ * @param {boolean} select - Whether select button is enabled.
25
+ * @param {Object.<string, string>} colours - The color scheme.
26
+ * @param {GamePad} gamepad - The parent gamepad instance.
27
+ */
28
+ constructor(ctx, buttonOffset, touches, start, select, colours, gamepad) {
29
+ /** @type {CanvasRenderingContext2D} */
30
+ this.ctx = ctx;
31
+ /** @type {GamePad} */
32
+ this.gamepad = gamepad;
33
+ /** @type {number} */
34
+ this.width = ctx.canvas.width;
35
+ /** @type {number} */
36
+ this.height = ctx.canvas.height;
37
+ /** @type {number} */
38
+ this.radius = ctx.canvas.width / 10;
39
+ /** @type {Object.<string, any>} */
40
+ this.touches = touches;
41
+ /** @type {boolean} */
42
+ this.start = start;
43
+ /** @type {boolean} */
44
+ this.select = select;
45
+ /** @type {Object} */
46
+ this.buttonOffset = buttonOffset;
47
+ /** @type {Object.<string, string>} */
48
+ this.colours = colours;
49
+ /** @type {Object} */
50
+ this.layout = { x: this.width - this.buttonOffset.x, y: this.height - this.buttonOffset.y };
51
+ /** @type {ControllerStick} */
52
+ this.stick = new ControllerStick(this.ctx, this.layout, this.touches, this.colours, this.radius, this.gamepad);
53
+ /** @type {ControllerButtons} */
54
+ this.buttons = new ControllerButtons(this.ctx, this.layout, this.touches, this.start, this.select, this.colours, this.radius, this.gamepad);
55
+ }
56
+ /**
57
+ * Initializes the controller components.
58
+ */
59
+ init() {
60
+ this.stick.init();
61
+ this.buttons.init();
62
+ }
63
+
64
+ /**
65
+ * Draws the controller components.
66
+ */
67
+ draw() {
68
+ this.stick.draw();
69
+ this.buttons.draw();
70
+ }
71
+ }