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.
- package/LICENSE +21 -0
- package/README.md +786 -0
- package/index.html +651 -0
- package/package.json +255 -0
- package/public/os9-shell/assets/index-B1Uvacma.js +32825 -0
- package/public/os9-shell/assets/index-B1Uvacma.js.map +1 -0
- package/public/os9-shell/assets/index-DIHfrTaW.css +1 -0
- package/public/os9-shell/index.html +14 -0
- package/public/os9-shell/nova-icon.svg +12 -0
- package/runtime/api-2d.js +878 -0
- package/runtime/api-3d/camera.js +73 -0
- package/runtime/api-3d/instancing.js +180 -0
- package/runtime/api-3d/lights.js +51 -0
- package/runtime/api-3d/materials.js +47 -0
- package/runtime/api-3d/models.js +84 -0
- package/runtime/api-3d/pbr.js +69 -0
- package/runtime/api-3d/primitives.js +304 -0
- package/runtime/api-3d/scene.js +169 -0
- package/runtime/api-3d/transforms.js +161 -0
- package/runtime/api-3d.js +154 -0
- package/runtime/api-effects.js +753 -0
- package/runtime/api-presets.js +85 -0
- package/runtime/api-skybox.js +178 -0
- package/runtime/api-sprites.js +100 -0
- package/runtime/api-voxel.js +601 -0
- package/runtime/api.js +201 -0
- package/runtime/assets.js +27 -0
- package/runtime/audio.js +114 -0
- package/runtime/collision.js +47 -0
- package/runtime/console.js +101 -0
- package/runtime/editor.js +233 -0
- package/runtime/font.js +233 -0
- package/runtime/framebuffer.js +28 -0
- package/runtime/fullscreen-button.js +185 -0
- package/runtime/gpu-canvas2d.js +47 -0
- package/runtime/gpu-threejs.js +639 -0
- package/runtime/gpu-webgl2.js +310 -0
- package/runtime/index.js +22 -0
- package/runtime/input.js +225 -0
- package/runtime/logger.js +60 -0
- package/runtime/physics.js +101 -0
- package/runtime/screens.js +213 -0
- package/runtime/storage.js +38 -0
- package/runtime/store.js +151 -0
- package/runtime/textinput.js +68 -0
- package/runtime/ui/buttons.js +124 -0
- package/runtime/ui/panels.js +105 -0
- package/runtime/ui/text.js +86 -0
- package/runtime/ui/widgets.js +141 -0
- package/runtime/ui.js +111 -0
- package/src/main.js +474 -0
- 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
|
+
}
|
package/runtime/index.js
ADDED
|
@@ -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';
|
package/runtime/input.js
ADDED
|
@@ -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
|
+
);
|