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,371 @@
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
+ /**
15
+ * Runs all updates for debug information.
16
+ * @param {import('../index.js').default} self - The engine core instance.
17
+ */
18
+ import { create, set, translate, rotate } from '../../utils/math/matrix4.js';
19
+ import { Vector } from '../../utils/math/vector.js';
20
+
21
+ export const updateDebugInformation = (self) => {
22
+ updateWebglDebugInformation(self);
23
+ updateFlagDebugInformation(self);
24
+ }
25
+
26
+ /**
27
+ * Updates WebGL debug information panel.
28
+ * @param {import('../index.js').default} self - The engine core instance.
29
+ */
30
+ export const updateWebglDebugInformation = (self) => {
31
+ if (self.showWebglDebug && self.webglDebugDiv) {
32
+ const now = (typeof performance !== 'undefined' ? performance.now() : Date.now());
33
+ const delta = now - self.lastDebugTime;
34
+ const fps = delta > 0 ? (1000.0 / delta).toFixed(1) : '0';
35
+ self.lastDebugTime = now;
36
+
37
+ const gl = self.gl;
38
+ let renderer = '';
39
+ let vendor = '';
40
+ let version = '';
41
+ if (gl) {
42
+ try {
43
+ renderer = gl.getParameter(gl.RENDERER);
44
+ vendor = gl.getParameter(gl.VENDOR);
45
+ version = gl.getParameter(gl.VERSION);
46
+ } catch (e) {
47
+ // WebGL context may be lost or parameters unavailable
48
+ }
49
+ }
50
+
51
+ const debug = self.renderManager.debug || {};
52
+ self.webglDebugDiv.innerHTML =
53
+ 'FPS: ' + fps + '<br>' +
54
+ 'Tiles Drawn: ' + (debug.tilesDrawn || 0) + '<br>' +
55
+ 'Sprites Drawn: ' + (debug.spritesDrawn || 0) + '<br>' +
56
+ 'Objects Drawn: ' + (debug.objectsDrawn || 0) + '<br>' +
57
+ 'Renderer: ' + renderer + '<br>' +
58
+ 'Vendor: ' + vendor + '<br>' +
59
+ 'GL Version: ' + version;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Updates flag debug information based on latest values.
65
+ * @param {import('../index.js').default} self - The engine core instance.
66
+ */
67
+ export const updateFlagDebugInformation = (self) => {
68
+ if (self.showFlagDebug && self.flagDebugDiv) {
69
+ self.store.set('Debug::Flag::UpdateTime', Date.now());
70
+ const flags = self.store.all();
71
+ console.log({ self, keys: JSON.stringify(flags), store: self.store.keys() });
72
+ const data = Object.keys(flags).map((key) => {
73
+ return '' + key + ': ' + JSON.stringify(flags[key]) + '<br>'
74
+ });
75
+ self.flagDebugDiv.innerHTML = 'FLAGS:<br>' + data.join('');
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Attaches flag debug information window to the top-right corner.
81
+ * @param {import('../index.js').default} self - The engine core instance.
82
+ */
83
+ export const attachFlagDebugInfo = (self) => {
84
+ const div = document.createElement('div');
85
+ div.style.position = 'absolute';
86
+ div.style.top = '0';
87
+ div.style.right = '0';
88
+ div.style.background = 'rgba(0, 0, 0, 0.6)';
89
+ div.style.color = '#0f0';
90
+ div.style.padding = '4px';
91
+ div.style.fontFamily = 'monospace';
92
+ div.style.fontSize = '12px';
93
+ div.style.zIndex = '10000';
94
+ div.style.pointerEvents = 'none';
95
+ div.style.display = 'none';
96
+ self.flagDebugDiv = div;
97
+ document.body.appendChild(div);
98
+ window.addEventListener('keydown', (e) => {
99
+ if (e.key === 'F4') {
100
+ self.showFlagDebug = !self.showFlagDebug;
101
+ self.store.set('Debug::Flag::showDebug', self.showFlagDebug);
102
+ self.flagDebugDiv.style.display = self.showFlagDebug ? 'block' : 'none';
103
+ }
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Attaches WebGL debug window to the instance.
109
+ * @param {import('../index.js').default} self - The engine core instance.
110
+ */
111
+ export const attachWebglDebugInfo = (self) => {
112
+ const div = document.createElement('div');
113
+ div.style.position = 'absolute';
114
+ div.style.top = '0';
115
+ div.style.left = '0';
116
+ div.style.background = 'rgba(0, 0, 0, 0.6)';
117
+ div.style.color = '#0f0';
118
+ div.style.padding = '4px';
119
+ div.style.fontFamily = 'monospace';
120
+ div.style.fontSize = '12px';
121
+ div.style.zIndex = '10000';
122
+ div.style.pointerEvents = 'none';
123
+ div.style.display = 'none';
124
+ self.webglDebugDiv = div;
125
+ document.body.appendChild(div);
126
+ window.addEventListener('keydown', (e) => {
127
+ if (e.key === 'F3') {
128
+ self.showWebglDebug = !self.showWebglDebug;
129
+ self.store.set('Debug::Webgl::showDebug', self.showWebglDebug);
130
+ self.webglDebugDiv.style.display = self.showWebglDebug ? 'block' : 'none';
131
+ }
132
+ });
133
+ // Free Camera toggle (F5) - register with engine keyboard so behavior matches other controls
134
+ try {
135
+ const kb = self.keyboard;
136
+ const kbHook = (ev, type) => {
137
+ // only handle keydown events here for toggles
138
+ if (type !== 'down') return;
139
+ if (ev.key !== 'F5') return;
140
+ try { ev.preventDefault(); ev.stopPropagation(); } catch (err) { }
141
+ // toggle and kick off same activation path below
142
+ self.showFreeCam = !self.showFreeCam;
143
+ self.store.set('Debug::FreeCam::show', self.showFreeCam);
144
+ // trigger a synthetic event to the same handler path by dispatching a custom event on window
145
+ const e = new CustomEvent('pixos:freecam:toggle');
146
+ window.dispatchEvent(e);
147
+ };
148
+ kb.addHook && kb.addHook(kbHook);
149
+ // store hook so it can be removed later if attachWebglDebugInfo is called multiple times
150
+ self._debugFreeCamHook = kbHook;
151
+ } catch (err) { }
152
+
153
+ // central handler for activation/deactivation triggered by keyboard hook
154
+ window.addEventListener('pixos:freecam:toggle', () => {
155
+ const rm = self.renderManager;
156
+ if (!rm || !rm.camera) return;
157
+ const canvas = self.canvas || document.body;
158
+ const panSpeed = 0.2;
159
+ const rotSpeed = 0.002;
160
+
161
+ // wheel handler -> zoom in/out using camera.zoom
162
+ const onWheel = (we) => {
163
+ try { we.preventDefault(); we.stopPropagation(); } catch (err) { };
164
+ const dz = (we.deltaY > 0 ? 1 : -1) * 0.5;
165
+ try { rm.camera.zoom && rm.camera.zoom(dz); moveCounter++; } catch (err) { }
166
+ };
167
+
168
+ // pointer move handler (pointer lock) -> rotate view matrix directly
169
+ const onPointerMove = (me) => {
170
+ const mx = (me.movementX || 0) * 0.002;
171
+ const my = (me.movementY || 0) * 0.002;
172
+ try {
173
+ // convert to angular delta and apply via rotateCam
174
+ const yawDelta = -mx * 1.5; // scale for responsiveness
175
+ const pitchDelta = -my * 1.5;
176
+ if (rm.camera) {
177
+ rm.camera.yaw += yawDelta;
178
+ rm.camera.pitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, rm.camera.pitch + pitchDelta));
179
+ rm.camera.updateViewFromAngles && rm.camera.updateViewFromAngles();
180
+ moveCounter++;
181
+ }
182
+ } catch (err) { }
183
+ };
184
+
185
+ // keyboard-driven movement uses keyboard.activeCodes from engine.keyboard
186
+ let rafId = null;
187
+ let statusEl = null;
188
+ let moveCounter = 0;
189
+ let firstTick = true;
190
+ const tick = () => {
191
+ // Use activeCodes which stores the unambiguous key strings (eg 'w', 'ArrowUp')
192
+ const codes = (self.keyboard && self.keyboard.activeCodes) || [];
193
+ const lower = codes.map((c) => (c || '').toString().toLowerCase());
194
+ // WASD movement - translate the view matrix in local camera space
195
+ try {
196
+ const step = 0.5;
197
+ if (rm.camera) {
198
+ if (lower.indexOf('w') >= 0 || lower.indexOf('arrowup') >= 0) { rm.camera.translateCam('UP'); moveCounter++; }
199
+ if (lower.indexOf('s') >= 0 || lower.indexOf('arrowdown') >= 0) { rm.camera.translateCam('DOWN'); moveCounter++; }
200
+ if (lower.indexOf('a') >= 0 || lower.indexOf('arrowleft') >= 0) { rm.camera.translateCam('LEFT'); moveCounter++; }
201
+ if (lower.indexOf('d') >= 0 || lower.indexOf('arrowright') >= 0) { rm.camera.translateCam('RIGHT'); moveCounter++; }
202
+ // Q/E yaw
203
+ if (lower.indexOf('q') >= 0) { rm.camera.yaw -= 0.03; rm.camera.updateViewFromAngles(); moveCounter++; }
204
+ if (lower.indexOf('e') >= 0) { rm.camera.yaw += 0.03; rm.camera.updateViewFromAngles(); moveCounter++; }
205
+ // R/F pitch
206
+ if (lower.indexOf('r') >= 0) { rm.camera.pitch = Math.max(-Math.PI / 2 + 0.01, rm.camera.pitch - 0.03); rm.camera.updateViewFromAngles(); moveCounter++; }
207
+ if (lower.indexOf('f') >= 0) { rm.camera.pitch = Math.min(Math.PI / 2 - 0.01, rm.camera.pitch + 0.03); rm.camera.updateViewFromAngles(); moveCounter++; }
208
+ }
209
+ } catch (err) { }
210
+
211
+ rafId = requestAnimationFrame(tick);
212
+ // update status element so user can see what keys are active (helps debug focus)
213
+ if (!statusEl) {
214
+ statusEl = document.createElement('div');
215
+ statusEl.id = 'pixos-freecam-status';
216
+ statusEl.style.position = 'absolute';
217
+ statusEl.style.top = '8px';
218
+ statusEl.style.left = '50%';
219
+ statusEl.style.transform = 'translateX(-50%)';
220
+ statusEl.style.background = 'rgba(0,0,0,0.6)';
221
+ statusEl.style.color = '#fff';
222
+ statusEl.style.padding = '6px 10px';
223
+ statusEl.style.fontFamily = 'monospace';
224
+ statusEl.style.fontSize = '12px';
225
+ statusEl.style.zIndex = '10002';
226
+ document.body.appendChild(statusEl);
227
+ }
228
+ try {
229
+ // collect camera state safely
230
+ const cam = rm.camera || {};
231
+ if (firstTick) {
232
+ try { console.log('[FreeCam] FIRST TICK viewMat:', Array.from(rm.camera.uViewMat)); } catch (err) { }
233
+ firstTick = false;
234
+ }
235
+ const pos = cam.cameraPosition ? [cam.cameraPosition.x, cam.cameraPosition.y, cam.cameraPosition.z] : ['n/a'];
236
+ const yaw = typeof cam.yaw !== 'undefined' ? cam.yaw.toFixed(3) : 'n/a';
237
+ const pitch = typeof cam.pitch !== 'undefined' ? cam.pitch.toFixed(3) : 'n/a';
238
+ const vm = rm.camera && rm.camera.uViewMat ? Array.from(rm.camera.uViewMat).slice(0, 8).map((n) => n.toFixed(3)) : [];
239
+ statusEl.innerText = 'Keys: ' + JSON.stringify(lower) + '\n' +
240
+ 'pointerLock: ' + ((document.pointerLockElement === canvas) ? 'yes' : 'no') + '\n' +
241
+ 'pos: ' + JSON.stringify(pos) + ' yaw:' + yaw + ' pitch:' + pitch + '\n' +
242
+ 'uViewMat (trim): ' + JSON.stringify(vm) + '\n' +
243
+ 'moves: ' + moveCounter;
244
+ } catch (err) { }
245
+ };
246
+
247
+ if (self.showFreeCam) {
248
+ // enter freecam
249
+ // keep raw camera matrix; avoid decomposing it here (fragile)
250
+ self._freeCamSaved = create();
251
+ // log the incoming view matrix for diagnosis
252
+ try { console.log('[FreeCam] ENTER - current viewMat:', Array.from(rm.camera.uViewMat)); } catch (err) { }
253
+ set(rm.camera.uViewMat, self._freeCamSaved);
254
+ try {
255
+ self._freeCamSavedState = {
256
+ position: new Vector(rm.camera.cameraPosition.x, rm.camera.cameraPosition.y, rm.camera.cameraPosition.z),
257
+ yaw: rm.camera.yaw,
258
+ pitch: rm.camera.pitch,
259
+ distance: rm.camera.cameraDistance,
260
+ target: rm.camera.cameraTarget ? new Vector(rm.camera.cameraTarget.x, rm.camera.cameraTarget.y, rm.camera.cameraTarget.z) : null,
261
+ viewMat: create(),
262
+ };
263
+ set(rm.camera.uViewMat, self._freeCamSavedState.viewMat);
264
+ try { console.log('[FreeCam] ENTER - savedState.viewMat:', Array.from(self._freeCamSavedState.viewMat)); } catch (err) { }
265
+ } catch (err) { }
266
+ if (self.spritz?.world) self.spritz.world.isPaused = true;
267
+ self._freecamActive = true;
268
+
269
+ const info = document.createElement('div');
270
+ info.style.position = 'absolute';
271
+ info.style.bottom = '8px';
272
+ info.style.left = '50%';
273
+ info.style.transform = 'translateX(-50%)';
274
+ info.style.background = 'rgba(0,0,0,0.6)';
275
+ info.style.color = '#fff';
276
+ info.style.padding = '6px 10px';
277
+ info.style.fontFamily = 'monospace';
278
+ info.style.fontSize = '12px';
279
+ info.style.zIndex = '10001';
280
+ info.id = 'pixos-freecam-info';
281
+ info.innerHTML = 'FREE CAM (F5 to exit) &nbsp; WASD/Arrows: move & strafe &nbsp; Q/E: yaw &nbsp; R/F: pitch &nbsp; Wheel: zoom';
282
+ document.body.appendChild(info);
283
+
284
+ const onPointerLockChange = () => {
285
+ const locked = document.pointerLockElement === canvas || document.mozPointerLockElement === canvas;
286
+ try {
287
+ const s = document.getElementById('pixos-freecam-status');
288
+ if (s) s.innerText = 'Keys: ' + JSON.stringify((self.keyboard && self.keyboard.activeCodes) || []) + ' | pointerLock: ' + (locked ? 'yes' : 'no');
289
+ try { console.log('[FreeCam] pointerLock change - locked:', locked, 'viewMat:', Array.from(rm.camera.uViewMat)); } catch (err) { }
290
+ } catch (err) { }
291
+ };
292
+
293
+ const captureClick = (ev) => {
294
+ ev && ev.preventDefault();
295
+ // focus the wrapper if available (WebGLView sets tabIndex on wrapper)
296
+ try {
297
+ if (canvas && canvas.parentElement) {
298
+ canvas.parentElement.focus && canvas.parentElement.focus();
299
+ }
300
+ } catch (err) { }
301
+ // request pointer lock on canvas
302
+ try { canvas.requestPointerLock = canvas.requestPointerLock || canvas.mozRequestPointerLock; canvas.requestPointerLock(); } catch (err) { }
303
+ };
304
+
305
+ // also mousedown as a final fallback
306
+ document.addEventListener('pointerlockchange', onPointerLockChange);
307
+ document.addEventListener('mozpointerlockchange', onPointerLockChange);
308
+
309
+ try { self._bodyOverflowSaved = document.body.style.overflow; document.body.style.overflow = 'hidden'; } catch (err) { }
310
+
311
+ // pointer lock request
312
+ try { canvas.requestPointerLock = canvas.requestPointerLock || canvas.mozRequestPointerLock; canvas.requestPointerLock(); } catch (err) { }
313
+
314
+ window.addEventListener('wheel', onWheel, { passive: false, capture: true });
315
+ document.addEventListener('pointermove', onPointerMove, { capture: true });
316
+ // also listen for mousemove for browsers that don't support pointermove while locked
317
+ document.addEventListener('mousemove', onPointerMove, { capture: true });
318
+ rafId = requestAnimationFrame(tick);
319
+ self._freecamHandlers = { onWheel, onPointerMove, rafId, captureClick, onPointerLockChange };
320
+ } else {
321
+ // exit freecam
322
+ if (self._freeCamSavedState) {
323
+ try {
324
+ try { console.log('[FreeCam] EXIT - restoring savedState.viewMat:', Array.from(self._freeCamSavedState.viewMat)); } catch (err) { }
325
+ // restore raw view matrix snapshot
326
+ set(self._freeCamSavedState.viewMat, rm.camera.uViewMat);
327
+ // restore camera params to ensure consistent tileset-style behavior
328
+ try {
329
+ if (rm.camera) {
330
+ if (typeof self._freeCamSavedState.yaw !== 'undefined') rm.camera.yaw = self._freeCamSavedState.yaw;
331
+ if (typeof self._freeCamSavedState.pitch !== 'undefined') rm.camera.pitch = self._freeCamSavedState.pitch;
332
+ if (typeof self._freeCamSavedState.distance !== 'undefined') rm.camera.cameraDistance = self._freeCamSavedState.distance;
333
+ if (self._freeCamSavedState.target) rm.camera.cameraTarget = self._freeCamSavedState.target;
334
+ rm.camera.updateViewFromAngles && rm.camera.updateViewFromAngles();
335
+ }
336
+ } catch (err) { }
337
+ } catch (err) { }
338
+ self._freeCamSavedState = null;
339
+ } else if (self._freeCamSaved) {
340
+ try {
341
+ try { console.log('[FreeCam] EXIT - restoring _freeCamSaved:', Array.from(self._freeCamSaved)); } catch (err) { }
342
+ set(self._freeCamSaved, rm.camera.uViewMat);
343
+ try { rm.camera.setFromViewMatrix(rm.camera.uViewMat); } catch (err) { }
344
+ } catch (err) { }
345
+ }
346
+ if (self.spritz?.world) self.spritz.world.isPaused = false;
347
+ self._freecamActive = false;
348
+ const info = document.getElementById('pixos-freecam-info'); if (info) document.body.removeChild(info);
349
+ try { document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock; document.exitPointerLock(); } catch (err) { }
350
+
351
+ if (self._freecamHandlers) {
352
+ window.removeEventListener('wheel', self._freecamHandlers.onWheel, { capture: true });
353
+ document.removeEventListener('pointermove', self._freecamHandlers.onPointerMove, { capture: true });
354
+ document.removeEventListener('mousemove', self._freecamHandlers.onPointerMove, { capture: true });
355
+ try {
356
+ if (self._freecamHandlers.onPointerLockChange) {
357
+ document.removeEventListener('pointerlockchange', self._freecamHandlers.onPointerLockChange);
358
+ document.removeEventListener('mozpointerlockchange', self._freecamHandlers.onPointerLockChange);
359
+ }
360
+ } catch (err) { }
361
+ cancelAnimationFrame(self._freecamHandlers.rafId);
362
+ self._freecamHandlers = null;
363
+ }
364
+ try { document.body.style.overflow = self._bodyOverflowSaved ?? ''; } catch (err) { }
365
+ try {
366
+ const s = document.getElementById('pixos-freecam-status');
367
+ if (s) document.body.removeChild(s);
368
+ } catch (err) { }
369
+ }
370
+ });
371
+ }