nova64 0.2.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +786 -0
  3. package/index.html +651 -0
  4. package/package.json +255 -0
  5. package/public/os9-shell/assets/index-B1Uvacma.js +32825 -0
  6. package/public/os9-shell/assets/index-B1Uvacma.js.map +1 -0
  7. package/public/os9-shell/assets/index-DIHfrTaW.css +1 -0
  8. package/public/os9-shell/index.html +14 -0
  9. package/public/os9-shell/nova-icon.svg +12 -0
  10. package/runtime/api-2d.js +878 -0
  11. package/runtime/api-3d/camera.js +73 -0
  12. package/runtime/api-3d/instancing.js +180 -0
  13. package/runtime/api-3d/lights.js +51 -0
  14. package/runtime/api-3d/materials.js +47 -0
  15. package/runtime/api-3d/models.js +84 -0
  16. package/runtime/api-3d/pbr.js +69 -0
  17. package/runtime/api-3d/primitives.js +304 -0
  18. package/runtime/api-3d/scene.js +169 -0
  19. package/runtime/api-3d/transforms.js +161 -0
  20. package/runtime/api-3d.js +154 -0
  21. package/runtime/api-effects.js +753 -0
  22. package/runtime/api-presets.js +85 -0
  23. package/runtime/api-skybox.js +178 -0
  24. package/runtime/api-sprites.js +100 -0
  25. package/runtime/api-voxel.js +601 -0
  26. package/runtime/api.js +201 -0
  27. package/runtime/assets.js +27 -0
  28. package/runtime/audio.js +114 -0
  29. package/runtime/collision.js +47 -0
  30. package/runtime/console.js +101 -0
  31. package/runtime/editor.js +233 -0
  32. package/runtime/font.js +233 -0
  33. package/runtime/framebuffer.js +28 -0
  34. package/runtime/fullscreen-button.js +185 -0
  35. package/runtime/gpu-canvas2d.js +47 -0
  36. package/runtime/gpu-threejs.js +639 -0
  37. package/runtime/gpu-webgl2.js +310 -0
  38. package/runtime/index.js +22 -0
  39. package/runtime/input.js +225 -0
  40. package/runtime/logger.js +60 -0
  41. package/runtime/physics.js +101 -0
  42. package/runtime/screens.js +213 -0
  43. package/runtime/storage.js +38 -0
  44. package/runtime/store.js +151 -0
  45. package/runtime/textinput.js +68 -0
  46. package/runtime/ui/buttons.js +124 -0
  47. package/runtime/ui/panels.js +105 -0
  48. package/runtime/ui/text.js +86 -0
  49. package/runtime/ui/widgets.js +141 -0
  50. package/runtime/ui.js +111 -0
  51. package/src/main.js +474 -0
  52. package/vite.config.js +63 -0
@@ -0,0 +1,310 @@
1
+ // runtime/gpu-webgl2.js
2
+ // WebGL2 backend with RGBA16F upload + tone mapping and a simple sprite renderer.
3
+ import { Framebuffer64 } from './framebuffer.js';
4
+
5
+ const VERT_FSQ = `#version 300 es
6
+ precision highp float;
7
+ layout(location=0) in vec2 a_pos;
8
+ out vec2 v_uv;
9
+ void main() {
10
+ v_uv = 0.5 * (a_pos + 1.0);
11
+ gl_Position = vec4(a_pos, 0.0, 1.0);
12
+ }`;
13
+
14
+ const FRAG_TONEMAP = `#version 300 es
15
+ precision highp float;
16
+ in vec2 v_uv;
17
+ out vec4 o_col;
18
+ uniform sampler2D u_tex;
19
+ // Simple ACES-like tonemapper (approx) then gamma to sRGB.
20
+ vec3 tonemapACES( vec3 x ) {
21
+ float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
22
+ return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0.0, 1.0);
23
+ }
24
+ void main() {
25
+ vec4 c = texture(u_tex, v_uv);
26
+ c.rgb = tonemapACES(c.rgb);
27
+ c.rgb = pow(c.rgb, vec3(1.0/2.2));
28
+ o_col = c;
29
+ }`;
30
+
31
+ // Sprite shader (screen-space)
32
+ const VERT_SPR = `#version 300 es
33
+ precision highp float;
34
+ layout(location=0) in vec2 a_pos; // quad verts in pixels (0..1) scaled in VS
35
+ layout(location=1) in vec2 i_pos; // instance: screen position (pixels)
36
+ layout(location=2) in vec2 i_size; // instance: size in pixels
37
+ layout(location=3) in vec4 i_uv; // instance: uv rect (u0,v0,u1,v1)
38
+ out vec2 v_uv;
39
+ uniform vec2 u_resolution;
40
+ void main() {
41
+ vec2 px = i_pos + a_pos * i_size; // pixel space
42
+ v_uv = mix(i_uv.xy, i_uv.zw, a_pos);
43
+ vec2 ndc = (px / u_resolution)*2.0 - 1.0;
44
+ ndc.y = -ndc.y;
45
+ gl_Position = vec4(ndc, 0.0, 1.0);
46
+ }`;
47
+
48
+ const FRAG_SPR = `#version 300 es
49
+ precision highp float;
50
+ in vec2 v_uv;
51
+ out vec4 o_col;
52
+ uniform sampler2D u_tex;
53
+ void main(){
54
+ vec4 c = texture(u_tex, v_uv);
55
+ o_col = c;
56
+ }`;
57
+
58
+ export class GpuWebGL2 {
59
+ constructor(canvas, w, h) {
60
+ this.canvas = canvas;
61
+ /** @type {WebGL2RenderingContext} */
62
+ const gl = canvas.getContext('webgl2', {
63
+ antialias: false,
64
+ alpha: false,
65
+ premultipliedAlpha: false,
66
+ });
67
+ if (!gl) throw new Error('WebGL2 not supported');
68
+ this.gl = gl;
69
+ this.fb = new Framebuffer64(w, h);
70
+ this.w = w;
71
+ this.h = h;
72
+
73
+ // Programs
74
+ this.progFSQ = this._makeProgram(VERT_FSQ, FRAG_TONEMAP);
75
+ this.progSPR = this._makeProgram(VERT_SPR, FRAG_SPR);
76
+
77
+ // Fullscreen triangle VBO
78
+ this.vboFSQ = gl.createBuffer();
79
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vboFSQ);
80
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
81
+
82
+ // Quad for sprites (two-triangle unit quad encoded as [0,0]-[1,1])
83
+ this.vboQuad = gl.createBuffer();
84
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vboQuad);
85
+ gl.bufferData(
86
+ gl.ARRAY_BUFFER,
87
+ new Float32Array([0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1]),
88
+ gl.STATIC_DRAW
89
+ );
90
+
91
+ // Instance buffers
92
+ this.instPos = gl.createBuffer();
93
+ this.instSize = gl.createBuffer();
94
+ this.instUV = gl.createBuffer();
95
+
96
+ // Texture for framebuffer upload (RGBA16F, accepts FLOAT data)
97
+ this.texFB = gl.createTexture();
98
+ gl.bindTexture(gl.TEXTURE_2D, this.texFB);
99
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
100
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
101
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
102
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
103
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
104
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, w, h, 0, gl.RGBA, gl.FLOAT, null);
105
+
106
+ this.tmpF32 = new Float32Array(w * h * 4); // normalized 0..1
107
+
108
+ // Sprite batch state
109
+ this.spriteBatches = new Map(); // texture -> array of instances
110
+ this.texCache = new WeakMap(); // HTMLImageElement -> WebGLTexture
111
+
112
+ gl.enable(gl.BLEND);
113
+ gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
114
+ gl.viewport(0, 0, canvas.width, canvas.height);
115
+ }
116
+
117
+ _makeProgram(vsSrc, fsSrc) {
118
+ const gl = this.gl;
119
+ const vs = gl.createShader(gl.VERTEX_SHADER);
120
+ gl.shaderSource(vs, vsSrc);
121
+ gl.compileShader(vs);
122
+ if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(vs));
123
+ const fs = gl.createShader(gl.FRAGMENT_SHADER);
124
+ gl.shaderSource(fs, fsSrc);
125
+ gl.compileShader(fs);
126
+ if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(fs));
127
+ const p = gl.createProgram();
128
+ gl.attachShader(p, vs);
129
+ gl.attachShader(p, fs);
130
+ gl.linkProgram(p);
131
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
132
+ return p;
133
+ }
134
+
135
+ beginFrame() {
136
+ const gl = this.gl;
137
+ gl.viewport(0, 0, this.canvas.width, this.canvas.height);
138
+ gl.clearColor(0, 0, 0, 1);
139
+ gl.clear(gl.COLOR_BUFFER_BIT);
140
+ }
141
+
142
+ // Queue a sprite instance; img is HTMLImageElement, uv rect in pixels of the image
143
+ queueSprite(img, sx, sy, sw, sh, dx, dy, scale = 1) {
144
+ const gltex = this._getTexture(img);
145
+ let arr = this.spriteBatches.get(gltex);
146
+ if (!arr) {
147
+ arr = [];
148
+ this.spriteBatches.set(gltex, arr);
149
+ }
150
+ arr.push({
151
+ sx,
152
+ sy,
153
+ sw,
154
+ sh,
155
+ dx,
156
+ dy,
157
+ scale,
158
+ tex: gltex,
159
+ iw: img.naturalWidth,
160
+ ih: img.naturalHeight,
161
+ });
162
+ }
163
+
164
+ _getTexture(img) {
165
+ let tex = this.texCache.get(img);
166
+ if (tex) return tex;
167
+ const gl = this.gl;
168
+ tex = gl.createTexture();
169
+ gl.bindTexture(gl.TEXTURE_2D, tex);
170
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
171
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
172
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
173
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
174
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
175
+ gl.texImage2D(
176
+ gl.TEXTURE_2D,
177
+ 0,
178
+ gl.RGBA,
179
+ img.naturalWidth,
180
+ img.naturalHeight,
181
+ 0,
182
+ gl.RGBA,
183
+ gl.UNSIGNED_BYTE,
184
+ img
185
+ );
186
+ this.texCache.set(img, tex);
187
+ return tex;
188
+ }
189
+
190
+ endFrame() {
191
+ const gl = this.gl;
192
+
193
+ // Upload framebuffer as RGBA16F using Float32 normalized data
194
+ const p = this.fb.pixels;
195
+ const f = this.tmpF32;
196
+ let k = 0;
197
+ for (let i = 0; i < p.length; i += 4) {
198
+ f[k++] = p[i] / 65535.0;
199
+ f[k++] = p[i + 1] / 65535.0;
200
+ f[k++] = p[i + 2] / 65535.0;
201
+ f[k++] = p[i + 3] / 65535.0;
202
+ }
203
+ gl.bindTexture(gl.TEXTURE_2D, this.texFB);
204
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, this.w, this.h, gl.RGBA, gl.FLOAT, f);
205
+
206
+ // Draw FSQ with tone mapping
207
+ gl.useProgram(this.progFSQ);
208
+ gl.activeTexture(gl.TEXTURE0);
209
+ gl.bindTexture(gl.TEXTURE_2D, this.texFB);
210
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vboFSQ);
211
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
212
+ gl.enableVertexAttribArray(0);
213
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
214
+
215
+ // Draw sprite batches on top
216
+ if (this.spriteBatches.size) {
217
+ gl.useProgram(this.progSPR);
218
+ const uRes = gl.getUniformLocation(this.progSPR, 'u_resolution');
219
+ gl.uniform2f(uRes, this.canvas.width, this.canvas.height);
220
+ // bind quad verts
221
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vboQuad);
222
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
223
+ gl.enableVertexAttribArray(0);
224
+
225
+ // instance attribute locations
226
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instPos);
227
+ gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);
228
+ gl.enableVertexAttribArray(1);
229
+ gl.vertexAttribDivisor(1, 1);
230
+
231
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instSize);
232
+ gl.vertexAttribPointer(2, 2, gl.FLOAT, false, 0, 0);
233
+ gl.enableVertexAttribArray(2);
234
+ gl.vertexAttribDivisor(2, 1);
235
+
236
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instUV);
237
+ gl.vertexAttribPointer(3, 4, gl.FLOAT, false, 0, 0);
238
+ gl.enableVertexAttribArray(3);
239
+ gl.vertexAttribDivisor(3, 1);
240
+
241
+ for (const [tex, arr] of this.spriteBatches.entries()) {
242
+ const n = arr.length;
243
+ const pos = new Float32Array(n * 2);
244
+ const size = new Float32Array(n * 2);
245
+ const uvs = new Float32Array(n * 4);
246
+ for (let i = 0; i < n; i++) {
247
+ const s = arr[i];
248
+ pos[i * 2 + 0] = s.dx;
249
+ pos[i * 2 + 1] = s.dy;
250
+ size[i * 2 + 0] = s.sw * s.scale;
251
+ size[i * 2 + 1] = s.sh * s.scale;
252
+ const u0 = s.sx / s.iw,
253
+ v0 = s.sy / s.ih;
254
+ const u1 = (s.sx + s.sw) / s.iw,
255
+ v1 = (s.sy + s.sh) / s.ih;
256
+ uvs[i * 4 + 0] = u0;
257
+ uvs[i * 4 + 1] = v0;
258
+ uvs[i * 4 + 2] = u1;
259
+ uvs[i * 4 + 3] = v1;
260
+ }
261
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instPos);
262
+ gl.bufferData(gl.ARRAY_BUFFER, pos, gl.DYNAMIC_DRAW);
263
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instSize);
264
+ gl.bufferData(gl.ARRAY_BUFFER, size, gl.DYNAMIC_DRAW);
265
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instUV);
266
+ gl.bufferData(gl.ARRAY_BUFFER, uvs, gl.DYNAMIC_DRAW);
267
+
268
+ gl.activeTexture(gl.TEXTURE0);
269
+ gl.bindTexture(gl.TEXTURE_2D, tex);
270
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, n);
271
+ }
272
+
273
+ this.spriteBatches.clear();
274
+ }
275
+ }
276
+
277
+ // API surface hooks needed by higher-level APIs
278
+ getFramebuffer() {
279
+ return this.fb;
280
+ }
281
+ supportsSpriteBatch() {
282
+ return true;
283
+ }
284
+
285
+ updateTextureForImage(img) {
286
+ const gl = this.gl;
287
+ let tex = this.texCache.get(img);
288
+ if (!tex) {
289
+ tex = gl.createTexture();
290
+ this.texCache.set(img, tex);
291
+ }
292
+ gl.bindTexture(gl.TEXTURE_2D, tex);
293
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
294
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
295
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
296
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
297
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
298
+ gl.texImage2D(
299
+ gl.TEXTURE_2D,
300
+ 0,
301
+ gl.RGBA,
302
+ img.naturalWidth,
303
+ img.naturalHeight,
304
+ 0,
305
+ gl.RGBA,
306
+ gl.UNSIGNED_BYTE,
307
+ img
308
+ );
309
+ }
310
+ }
@@ -0,0 +1,22 @@
1
+ // runtime/index.js
2
+ // Public entry point for the Nova64 runtime package
3
+
4
+ export { threeDApi } from './api-3d.js';
5
+ export { uiApi } from './ui.js';
6
+ export { logger } from './logger.js';
7
+
8
+ // Re-export sub-modules for tree-shaking
9
+ export * from './api-3d/materials.js';
10
+ export * from './api-3d/primitives.js';
11
+ export * from './api-3d/transforms.js';
12
+ export * from './api-3d/camera.js';
13
+ export * from './api-3d/lights.js';
14
+ export * from './api-3d/models.js';
15
+ export * from './api-3d/instancing.js';
16
+ export * from './api-3d/pbr.js';
17
+ export * from './api-3d/scene.js';
18
+
19
+ export * from './ui/text.js';
20
+ export * from './ui/panels.js';
21
+ export * from './ui/buttons.js';
22
+ export * from './ui/widgets.js';
@@ -0,0 +1,225 @@
1
+ // runtime/input.js
2
+ // Gamepad button mapping (standard gamepad layout)
3
+ const GAMEPAD_BUTTONS = {
4
+ 0: 12, // A button → Start (button 12)
5
+ 1: 13, // B button → Select (button 13)
6
+ 2: 4, // X button → button 4
7
+ 3: 5, // Y button → button 5
8
+ 4: 6, // LB → button 6
9
+ 5: 7, // RB → button 7
10
+ 6: 8, // LT → button 8
11
+ 7: 9, // RT → button 9
12
+ 8: 13, // Select → button 13
13
+ 9: 12, // Start → button 12
14
+ 12: 0, // D-pad Up → button 0
15
+ 13: 3, // D-pad Down → button 3
16
+ 14: 1, // D-pad Left → button 1
17
+ 15: 2, // D-pad Right → button 2
18
+ };
19
+
20
+ const KEYMAP = {
21
+ // arrows + Z X C V
22
+ 0: 'ArrowLeft', // left
23
+ 1: 'ArrowRight', // right
24
+ 2: 'ArrowUp', // up
25
+ 3: 'ArrowDown', // down
26
+ 4: 'KeyZ',
27
+ 5: 'KeyX',
28
+ 6: 'KeyC',
29
+ 7: 'KeyV',
30
+ 8: 'KeyA',
31
+ 9: 'KeyS',
32
+ 10: 'KeyQ',
33
+ 11: 'KeyW',
34
+ 12: 'Enter', // Start
35
+ 13: 'Space', // Select/Action
36
+ };
37
+
38
+ class Input {
39
+ constructor() {
40
+ this.keys = new Map();
41
+ this.prev = new Map();
42
+ this.mouse = { x: 0, y: 0, down: false, prevDown: false };
43
+ this.uiCallbacks = { setMousePosition: null, setMouseButton: null };
44
+
45
+ // Gamepad state
46
+ this.gamepadButtons = new Map();
47
+ this.gamepadPrev = new Map();
48
+ this.gamepadAxes = { leftX: 0, leftY: 0, rightX: 0, rightY: 0 };
49
+ this.gamepadDeadzone = 0.15;
50
+
51
+ if (typeof window !== 'undefined') {
52
+ window.addEventListener('keydown', e => {
53
+ this.keys.set(e.code, true);
54
+ });
55
+ window.addEventListener('keyup', e => {
56
+ this.keys.set(e.code, false);
57
+ });
58
+ window.addEventListener('blur', () => {
59
+ this.keys.clear();
60
+ });
61
+
62
+ // Mouse event listeners
63
+ window.addEventListener('mousemove', e => {
64
+ const canvas = document.querySelector('canvas');
65
+ if (canvas) {
66
+ const rect = canvas.getBoundingClientRect();
67
+ // Scale mouse position to Nova64's 640x360 resolution
68
+ this.mouse.x = Math.floor(((e.clientX - rect.left) / rect.width) * 640);
69
+ this.mouse.y = Math.floor(((e.clientY - rect.top) / rect.height) * 360);
70
+
71
+ // Update UI system if connected
72
+ if (this.uiCallbacks.setMousePosition) {
73
+ this.uiCallbacks.setMousePosition(this.mouse.x, this.mouse.y);
74
+ }
75
+ }
76
+ });
77
+
78
+ window.addEventListener('mousedown', _e => {
79
+ this.mouse.down = true;
80
+ if (this.uiCallbacks.setMouseButton) {
81
+ this.uiCallbacks.setMouseButton(true);
82
+ }
83
+ });
84
+
85
+ window.addEventListener('mouseup', _e => {
86
+ this.mouse.down = false;
87
+ if (this.uiCallbacks.setMouseButton) {
88
+ this.uiCallbacks.setMouseButton(false);
89
+ }
90
+ });
91
+
92
+ // Gamepad connection events
93
+ window.addEventListener('gamepadconnected', _e => {
94
+ // Gamepad connected
95
+ });
96
+
97
+ window.addEventListener('gamepaddisconnected', _e => {
98
+ this.gamepadButtons.clear();
99
+ this.gamepadPrev.clear();
100
+ });
101
+ }
102
+ }
103
+
104
+ pollGamepad() {
105
+ const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
106
+ const gamepad = gamepads[0]; // Use first connected gamepad
107
+
108
+ if (gamepad) {
109
+ // Read button states
110
+ gamepad.buttons.forEach((button, index) => {
111
+ const mappedButton = GAMEPAD_BUTTONS[index];
112
+ if (mappedButton !== undefined) {
113
+ this.gamepadButtons.set(mappedButton, button.pressed);
114
+ }
115
+ });
116
+
117
+ // Read axes with deadzone
118
+ if (gamepad.axes.length >= 4) {
119
+ this.gamepadAxes.leftX =
120
+ Math.abs(gamepad.axes[0]) > this.gamepadDeadzone ? gamepad.axes[0] : 0;
121
+ this.gamepadAxes.leftY =
122
+ Math.abs(gamepad.axes[1]) > this.gamepadDeadzone ? gamepad.axes[1] : 0;
123
+ this.gamepadAxes.rightX =
124
+ Math.abs(gamepad.axes[2]) > this.gamepadDeadzone ? gamepad.axes[2] : 0;
125
+ this.gamepadAxes.rightY =
126
+ Math.abs(gamepad.axes[3]) > this.gamepadDeadzone ? gamepad.axes[3] : 0;
127
+ }
128
+ }
129
+ }
130
+
131
+ // Connect UI system callbacks
132
+ connectUI(setMousePosition, setMouseButton) {
133
+ this.uiCallbacks.setMousePosition = setMousePosition;
134
+ this.uiCallbacks.setMouseButton = setMouseButton;
135
+ }
136
+ step() {
137
+ this.prev = new Map(this.keys);
138
+ this.mouse.prevDown = this.mouse.down;
139
+ this.gamepadPrev = new Map(this.gamepadButtons);
140
+ this.pollGamepad(); // Poll gamepad state every frame
141
+ }
142
+ btn(i) {
143
+ // Check keyboard OR gamepad
144
+ return !!this.keys.get(KEYMAP[i | 0] || '') || !!this.gamepadButtons.get(i | 0);
145
+ }
146
+ btnp(i) {
147
+ const code = KEYMAP[i | 0] || '';
148
+ const keyPressed = !!this.keys.get(code) && !this.prev.get(code);
149
+ const gamepadPressed = !!this.gamepadButtons.get(i | 0) && !this.gamepadPrev.get(i | 0);
150
+ return keyPressed || gamepadPressed;
151
+ }
152
+ key(code) {
153
+ return !!this.keys.get(code);
154
+ } // Direct key code checking — is currently held
155
+ keyp(code) {
156
+ return !!this.keys.get(code) && !this.prev.get(code);
157
+ } // just-pressed this frame
158
+
159
+ // Gamepad-specific functions
160
+ getGamepadAxis(axisName) {
161
+ return this.gamepadAxes[axisName] || 0;
162
+ }
163
+
164
+ isGamepadConnected() {
165
+ const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
166
+ return gamepads[0] !== null && gamepads[0] !== undefined;
167
+ }
168
+
169
+ // Helper functions for easier key checking
170
+ isKeyDown(keyCode) {
171
+ // Space must be checked before single-char conversion (' ' → 'Key ' is wrong)
172
+ if (keyCode === ' ') keyCode = 'Space';
173
+ // Handle single character keys by converting to KeyCode format
174
+ if (keyCode.length === 1) {
175
+ keyCode = 'Key' + keyCode.toUpperCase();
176
+ }
177
+ return !!this.keys.get(keyCode);
178
+ }
179
+
180
+ isKeyPressed(keyCode) {
181
+ // Space must be checked before single-char conversion (' ' → 'Key ' is wrong)
182
+ if (keyCode === ' ') keyCode = 'Space';
183
+ // Handle single character keys by converting to KeyCode format
184
+ if (keyCode.length === 1) {
185
+ keyCode = 'Key' + keyCode.toUpperCase();
186
+ }
187
+ return !!this.keys.get(keyCode) && !this.prev.get(keyCode);
188
+ }
189
+ }
190
+
191
+ export const input = new Input();
192
+
193
+ export function inputApi() {
194
+ return {
195
+ exposeTo(target) {
196
+ Object.assign(target, {
197
+ btn: i => input.btn(i),
198
+ btnp: i => input.btnp(i),
199
+ key: code => input.key(code),
200
+ keyp: code => input.keyp(code),
201
+ isKeyDown: code => input.isKeyDown(code),
202
+ isKeyPressed: code => input.isKeyPressed(code),
203
+ // Mouse functions
204
+ mouseX: () => input.mouse.x,
205
+ mouseY: () => input.mouse.y,
206
+ mouseDown: () => input.mouse.down,
207
+ mousePressed: () => input.mouse.down && !input.mouse.prevDown,
208
+ // Gamepad functions
209
+ gamepadAxis: axisName => input.getGamepadAxis(axisName),
210
+ gamepadConnected: () => input.isGamepadConnected(),
211
+ // Axis aliases for convenience
212
+ leftStickX: () => input.getGamepadAxis('leftX'),
213
+ leftStickY: () => input.getGamepadAxis('leftY'),
214
+ rightStickX: () => input.getGamepadAxis('rightX'),
215
+ rightStickY: () => input.getGamepadAxis('rightY'),
216
+ });
217
+ },
218
+ step() {
219
+ input.step();
220
+ },
221
+ connectUI(setMousePosition, setMouseButton) {
222
+ input.connectUI(setMousePosition, setMouseButton);
223
+ },
224
+ };
225
+ }
@@ -0,0 +1,60 @@
1
+ // runtime/logger.js
2
+ // Configurable logging system for Nova64.
3
+ // Replaces scattered console.log calls with leveled, history-tracked output.
4
+ // In production builds the level is automatically raised to WARN.
5
+
6
+ export const LogLevel = Object.freeze({ DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, NONE: 4 });
7
+
8
+ class Logger {
9
+ constructor(initialLevel = LogLevel.INFO) {
10
+ this.level = initialLevel;
11
+ this.history = [];
12
+ this.maxHistory = 200;
13
+ }
14
+
15
+ _record(level, args) {
16
+ this.history.push({ level, args, ts: Date.now() });
17
+ if (this.history.length > this.maxHistory) this.history.shift();
18
+ }
19
+
20
+ debug(...args) {
21
+ if (this.level > LogLevel.DEBUG) return;
22
+ console.log('[Nova64:DEBUG]', ...args);
23
+ this._record('DEBUG', args);
24
+ }
25
+
26
+ info(...args) {
27
+ if (this.level > LogLevel.INFO) return;
28
+ console.log('[Nova64:INFO]', ...args);
29
+ this._record('INFO', args);
30
+ }
31
+
32
+ warn(...args) {
33
+ if (this.level > LogLevel.WARN) return;
34
+ console.warn('[Nova64:WARN]', ...args);
35
+ this._record('WARN', args);
36
+ }
37
+
38
+ error(...args) {
39
+ if (this.level > LogLevel.ERROR) return;
40
+ console.error('[Nova64:ERROR]', ...args);
41
+ this._record('ERROR', args);
42
+ }
43
+
44
+ setLevel(level) {
45
+ this.level = level;
46
+ }
47
+ getHistory() {
48
+ return [...this.history];
49
+ }
50
+ clearHistory() {
51
+ this.history = [];
52
+ }
53
+ }
54
+
55
+ export const logger = new Logger(
56
+ // Raise to WARN in production so debug noise is suppressed
57
+ typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.PROD
58
+ ? LogLevel.WARN
59
+ : LogLevel.INFO
60
+ );