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
package/src/main.js ADDED
@@ -0,0 +1,474 @@
1
+ import { Nova64 } from '../runtime/console.js';
2
+ import { GpuThreeJS } from '../runtime/gpu-threejs.js';
3
+ import { logger } from '../runtime/logger.js';
4
+ globalThis.novaLogger = logger;
5
+ import { stdApi } from '../runtime/api.js';
6
+ import { spriteApi } from '../runtime/api-sprites.js';
7
+ import { threeDApi } from '../runtime/api-3d.js';
8
+ import { editorApi } from '../runtime/editor.js';
9
+ import { physicsApi } from '../runtime/physics.js';
10
+ import { textInputApi } from '../runtime/textinput.js';
11
+ import { aabb, circle as circleCollision, raycastTilemap } from '../runtime/collision.js';
12
+ import { audioApi } from '../runtime/audio.js';
13
+ import { inputApi } from '../runtime/input.js';
14
+ import { storageApi } from '../runtime/storage.js';
15
+ import { screenApi } from '../runtime/screens.js';
16
+ import { skyboxApi } from '../runtime/api-skybox.js';
17
+ import { uiApi } from '../runtime/ui.js';
18
+ import { effectsApi } from '../runtime/api-effects.js';
19
+ import { voxelApi } from '../runtime/api-voxel.js';
20
+ import { createFullscreenButton } from '../runtime/fullscreen-button.js';
21
+ import { storeApi } from '../runtime/store.js';
22
+ import { api2d } from '../runtime/api-2d.js';
23
+ import { presetsApi } from '../runtime/api-presets.js';
24
+
25
+ const canvas = document.getElementById('screen');
26
+
27
+ // Create fullscreen button - stored globally for cleanup if needed
28
+ globalThis.fullscreenButton = createFullscreenButton(canvas);
29
+
30
+ // ONLY use Three.js renderer - Nintendo 64/PlayStation style 3D console
31
+ let gpu;
32
+ try {
33
+ gpu = new GpuThreeJS(canvas, 640, 360);
34
+ console.log('✅ Using Three.js renderer - Nintendo 64/PlayStation GPU mode');
35
+ } catch (e) {
36
+ console.error('❌ Three.js renderer failed to initialize:', e);
37
+ throw new Error('Fantasy console requires 3D GPU support (Three.js)');
38
+ }
39
+
40
+ const api = stdApi(gpu);
41
+ const sApi = spriteApi(gpu);
42
+ const threeDApi_instance = threeDApi(gpu);
43
+ const eApi = editorApi(sApi);
44
+ const pApi = physicsApi();
45
+ const tApi = textInputApi();
46
+ const aApi = audioApi();
47
+ const iApi = inputApi();
48
+ const stApi = storageApi('nova64');
49
+ const scrApi = screenApi();
50
+ const skyApi = skyboxApi(gpu);
51
+ const fxApi = effectsApi(gpu);
52
+ const vxApi = voxelApi(gpu);
53
+ const storeApiInst = storeApi();
54
+ const api2dInst = api2d(gpu);
55
+ const presetsInst = presetsApi(gpu);
56
+
57
+ // Create UI API - needs to be created after api is fully initialized
58
+ let uiApiInstance;
59
+
60
+ // gather and expose to global
61
+ const g = {};
62
+ api.exposeTo(g);
63
+ sApi.exposeTo(g);
64
+ threeDApi_instance.exposeTo(g);
65
+ eApi.exposeTo(g);
66
+ pApi.exposeTo(g);
67
+ tApi.exposeTo(g);
68
+ Object.assign(g, { aabb, circleCollision, raycastTilemap });
69
+ aApi.exposeTo(g);
70
+ iApi.exposeTo(g);
71
+ stApi.exposeTo(g);
72
+ scrApi.exposeTo(g);
73
+ skyApi.exposeTo(g);
74
+ fxApi.exposeTo(g);
75
+ vxApi.exposeTo(g);
76
+ storeApiInst.exposeTo(g);
77
+ api2dInst.exposeTo(g);
78
+ presetsInst.exposeTo(g);
79
+
80
+ // Now create UI API after g has rgba8 and other functions
81
+ uiApiInstance = uiApi(gpu, g);
82
+ uiApiInstance.exposeTo(g);
83
+
84
+ // Connect input system to UI system for mouse events
85
+ iApi.connectUI(uiApiInstance.setMousePosition, uiApiInstance.setMouseButton);
86
+
87
+ Object.assign(globalThis, g);
88
+ // inject camera ref into sprite system
89
+ if (g.getCamera) sApi.setCameraRef(g.getCamera());
90
+
91
+ const nova = new Nova64(gpu);
92
+
93
+ let paused = false;
94
+ let stepOnce = false;
95
+ let statsEl = document.getElementById('stats');
96
+
97
+ async function loadCart(path) {
98
+ await nova.loadCart(path);
99
+ }
100
+
101
+ function attachUI() {
102
+ const sel = document.getElementById('cart');
103
+ const pauseBtn = document.getElementById('pause');
104
+ const stepBtn = document.getElementById('step');
105
+ const shotBtn = document.getElementById('shot');
106
+
107
+ sel.addEventListener('change', async () => {
108
+ paused = false;
109
+ pauseBtn.textContent = 'Pause';
110
+ await loadCart(sel.value);
111
+ });
112
+
113
+ // Renderer is now fixed to Three.js only - no UI controls needed
114
+ // Editor button now handled by inline onclick in HTML to open OS9 shell
115
+
116
+ pauseBtn.addEventListener('click', () => {
117
+ paused = !paused;
118
+ pauseBtn.textContent = paused ? 'Resume' : 'Pause';
119
+ });
120
+ stepBtn.addEventListener('click', () => {
121
+ stepOnce = true;
122
+ });
123
+ shotBtn.addEventListener('click', () => {
124
+ const url = canvas.toDataURL('image/png');
125
+ const a = document.createElement('a');
126
+ a.href = url;
127
+ a.download = 'nova64.png';
128
+ a.click();
129
+ });
130
+ }
131
+
132
+ let last = performance.now();
133
+ let uMs = 0,
134
+ dMs = 0,
135
+ fps = 0;
136
+ let currentDt = 0;
137
+
138
+ // Expose timing functions globally
139
+ globalThis.getDeltaTime = () => currentDt;
140
+ globalThis.getFPS = () => fps;
141
+
142
+ function loop() {
143
+ const now = performance.now();
144
+ const dt = Math.min(0.1, (now - last) / 1000);
145
+ currentDt = dt;
146
+ last = now;
147
+
148
+ if (!paused || stepOnce) {
149
+ iApi.step();
150
+ const u0 = performance.now();
151
+
152
+ // Tick the global novaStore time counter
153
+ storeApiInst.tick(dt);
154
+ // Auto-animate skybox if enabled
155
+ skyApi._tick(dt);
156
+ // Update post-processing shader uniforms (time, etc.)
157
+ fxApi.update(dt);
158
+
159
+ // Update cart first (for manual screen management)
160
+ // Check if cart exists to prevent errors during scene transitions
161
+ if (nova.cart && nova.cart.update) {
162
+ try {
163
+ if (typeof globalThis.updateAnimations === 'function') {
164
+ globalThis.updateAnimations(dt);
165
+ }
166
+ nova.cart.update(dt);
167
+ } catch (e) {
168
+ console.error('❌ Cart update() error:', e.message);
169
+ }
170
+ }
171
+
172
+ // Then update screen manager (for automatic screen management)
173
+ scrApi.manager.update(dt);
174
+
175
+ const u1 = performance.now();
176
+ uMs = u1 - u0;
177
+
178
+ gpu.beginFrame();
179
+ const d0 = performance.now();
180
+
181
+ // Draw cart first (for manual rendering)
182
+ // Check if cart exists to prevent errors during scene transitions
183
+ if (nova.cart && nova.cart.draw) {
184
+ try {
185
+ nova.cart.draw();
186
+ } catch (e) {
187
+ console.error('❌ Cart draw() error:', e.message, e.stack);
188
+ }
189
+ }
190
+
191
+ // Then draw screen manager (for automatic screen rendering)
192
+ scrApi.manager.draw();
193
+
194
+ const d1 = performance.now();
195
+ dMs = d1 - d0;
196
+ try {
197
+ gpu.endFrame();
198
+ } catch (e) {
199
+ console.error('❌ gpu.endFrame() error:', e.message, e.stack);
200
+ }
201
+ }
202
+ if (stepOnce) {
203
+ stepOnce = false;
204
+ }
205
+
206
+ fps = Math.round(1000 / (performance.now() - now));
207
+
208
+ // 3D GPU stats
209
+ let statsText = `3D GPU (Three.js) â€ĸ fps: ${fps}, update: ${uMs.toFixed(2)}ms, draw: ${dMs.toFixed(2)}ms`;
210
+
211
+ // Add 3D stats if available
212
+ if (typeof get3DStats === 'function') {
213
+ const stats3D = get3DStats();
214
+ if (stats3D.render) {
215
+ statsText += ` â€ĸ triangles: ${stats3D.render.triangles}, calls: ${stats3D.render.calls}`;
216
+ }
217
+ }
218
+
219
+ statsEl.textContent = statsText;
220
+ requestAnimationFrame(loop);
221
+ }
222
+
223
+ attachUI();
224
+
225
+ // Check for game parameter in URL
226
+ const urlParams = new URLSearchParams(window.location.search);
227
+ const gameParam = urlParams.get('game');
228
+ const gamePathParam = urlParams.get('path'); // Allow direct path parameter
229
+ const demoParam = urlParams.get('demo'); // Also handle ?demo= from console.html links
230
+
231
+ // Map game IDs to their paths
232
+ const gameMap = {
233
+ 'space-harrier': '/examples/space-harrier-3d/code.js',
234
+ fzero: '/examples/f-zero-nova-3d/code.js',
235
+ knight: '/examples/strider-demo-3d/code.js',
236
+ cyberpunk: '/examples/cyberpunk-city-3d/code.js',
237
+ strider: '/examples/strider-demo-3d/code.js',
238
+ demoscene: '/examples/demoscene/code.js',
239
+ 'space-combat': '/examples/star-fox-nova-3d/code.js',
240
+ minecraft: '/examples/minecraft-demo/code.js',
241
+ };
242
+
243
+ // Map demo names (from ?demo= URL param) to paths
244
+ const demoMap = {
245
+ 'hello-world': '/examples/hello-world/code.js',
246
+ 'crystal-cathedral-3d': '/examples/crystal-cathedral-3d/code.js',
247
+ 'f-zero-nova-3d': '/examples/f-zero-nova-3d/code.js',
248
+ 'star-fox-nova-3d': '/examples/star-fox-nova-3d/code.js',
249
+ 'minecraft-demo': '/examples/minecraft-demo/code.js',
250
+ 'super-plumber-64': '/examples/super-plumber-64/code.js',
251
+ 'cyberpunk-city-3d': '/examples/cyberpunk-city-3d/code.js',
252
+ 'strider-demo-3d': '/examples/strider-demo-3d/code.js',
253
+ demoscene: '/examples/demoscene/code.js',
254
+ 'space-harrier-3d': '/examples/space-harrier-3d/code.js',
255
+ 'hello-3d': '/examples/hello-3d/code.js',
256
+ 'mystical-realm-3d': '/examples/mystical-realm-3d/code.js',
257
+ 'physics-demo-3d': '/examples/physics-demo-3d/code.js',
258
+ 'shooter-demo-3d': '/examples/shooter-demo-3d/code.js',
259
+ 'hello-skybox': '/examples/hello-skybox/code.js',
260
+ 'fps-demo-3d': '/examples/fps-demo-3d/code.js',
261
+ 'adventure-comic-3d': '/examples/adventure-comic-3d/code.js',
262
+ 'input-showcase': '/examples/input-showcase/code.js',
263
+ 'audio-lab': '/examples/audio-lab/code.js',
264
+ 'storage-quest': '/examples/storage-quest/code.js',
265
+ };
266
+
267
+ // default cart - load from URL param or default to space-harrier-3d
268
+ (async () => {
269
+ let gamePath = document.getElementById('cart')?.value || '/examples/space-harrier-3d/code.js';
270
+
271
+ if (gamePathParam) {
272
+ gamePath = gamePathParam;
273
+ } else if (demoParam && demoMap[demoParam]) {
274
+ // Handle ?demo= parameter (from console.html links / index.html demo cards)
275
+ gamePath = demoMap[demoParam];
276
+ } else if (demoParam) {
277
+ // Fallback: try constructing path from demo name directly
278
+ gamePath = `/examples/${demoParam}/code.js`;
279
+ } else if (gameParam && gameMap[gameParam]) {
280
+ gamePath = gameMap[gameParam];
281
+ }
282
+
283
+ console.log(`🎮 Loading game: ${gamePath}`);
284
+ await loadCart(gamePath);
285
+ requestAnimationFrame(loop);
286
+ })();
287
+
288
+ // Listen for messages from Game Studio to execute code
289
+ window.addEventListener('message', event => {
290
+ if (event.data && event.data.type === 'EXECUTE_CODE') {
291
+ console.log('🎮 Game Studio: Executing code...');
292
+ console.log('📝 Code to execute:', event.data.code.substring(0, 200) + '...');
293
+ console.log('🔧 Available APIs:', {
294
+ api: typeof api,
295
+ iApi: typeof iApi,
296
+ threeDApi_instance: typeof threeDApi_instance,
297
+ });
298
+
299
+ try {
300
+ // Stop current game loop if running
301
+ console.log('â¸ī¸ Pausing game...');
302
+ paused = true;
303
+
304
+ // Reset the 3D scene
305
+ console.log('🧹 Clearing 3D scene...');
306
+ if (threeDApi_instance && typeof threeDApi_instance.clearScene === 'function') {
307
+ threeDApi_instance.clearScene();
308
+ console.log('✅ Scene cleared');
309
+ } else {
310
+ console.warn('âš ī¸ clearScene not available');
311
+ }
312
+
313
+ // Execute the new code
314
+ const userCode = event.data.code;
315
+
316
+ console.log('🔨 Creating game function...');
317
+ // Create a function from the code and execute it
318
+ const gameFunction = new Function(
319
+ 'cls',
320
+ 'pset',
321
+ 'pget',
322
+ 'rectfill',
323
+ 'rect',
324
+ 'circfill',
325
+ 'circ',
326
+ 'line',
327
+ 'print',
328
+ 'btn',
329
+ 'btnp',
330
+ 'rgba8',
331
+ 'spr',
332
+ 'map',
333
+ 'mset',
334
+ 'mget',
335
+ 'rect3d',
336
+ 'cube3d',
337
+ 'sphere3d',
338
+ 'cylinder3d',
339
+ 'cone3d',
340
+ 'model3d',
341
+ 'light3d',
342
+ 'setCamera',
343
+ 'lookAt',
344
+ 'fog3d',
345
+ 'clearScene',
346
+ 'updateModel',
347
+ 'createSpaceSkybox',
348
+ 'animateSkybox',
349
+ 'clearSkybox',
350
+ 'bloom',
351
+ 'chromaticAberration',
352
+ 'vignette',
353
+ 'scanlines',
354
+ 'crt',
355
+ 'glitch',
356
+ 'createVoxelEngine',
357
+ 'voxelSet',
358
+ 'voxelGet',
359
+ 'voxelClear',
360
+ 'voxelRender',
361
+ 'console',
362
+ 'Math',
363
+ 'Date',
364
+ 'Array',
365
+ 'Object',
366
+ 'String',
367
+ 'Number',
368
+ userCode +
369
+ '\n; return { update: typeof update !== "undefined" ? update : null, draw: typeof draw !== "undefined" ? draw : null, render: typeof render !== "undefined" ? render : null };'
370
+ );
371
+
372
+ console.log('🚀 Executing game function...');
373
+ // Call with the API functions and capture the returned functions
374
+ const gameFunctions = gameFunction(
375
+ api.cls,
376
+ api.pset,
377
+ api.pget,
378
+ api.rectfill,
379
+ api.rect,
380
+ api.circfill,
381
+ api.circ,
382
+ api.line,
383
+ api.print,
384
+ iApi.btn,
385
+ iApi.btnp,
386
+ api.rgba8,
387
+ sApi.spr,
388
+ sApi.map,
389
+ sApi.mset,
390
+ sApi.mget,
391
+ threeDApi_instance.rect3d,
392
+ threeDApi_instance.cube3d,
393
+ threeDApi_instance.sphere3d,
394
+ threeDApi_instance.cylinder3d,
395
+ threeDApi_instance.cone3d,
396
+ threeDApi_instance.model3d,
397
+ threeDApi_instance.light3d,
398
+ threeDApi_instance.setCamera,
399
+ threeDApi_instance.lookAt,
400
+ threeDApi_instance.fog3d,
401
+ threeDApi_instance.clearScene,
402
+ threeDApi_instance.updateModel,
403
+ g.createSpaceSkybox,
404
+ g.animateSkybox,
405
+ g.clearSkybox,
406
+ fxApi.bloom,
407
+ fxApi.chromaticAberration,
408
+ fxApi.vignette,
409
+ fxApi.scanlines,
410
+ fxApi.crt,
411
+ fxApi.glitch,
412
+ vxApi.createVoxelEngine,
413
+ vxApi.voxelSet,
414
+ vxApi.voxelGet,
415
+ vxApi.voxelClear,
416
+ vxApi.voxelRender,
417
+ console,
418
+ Math,
419
+ Date,
420
+ Array,
421
+ Object,
422
+ String,
423
+ Number
424
+ );
425
+
426
+ console.log('📋 Game functions:', gameFunctions);
427
+
428
+ // Replace the cart's update/draw functions with the new ones
429
+ if (gameFunctions.update || gameFunctions.draw || gameFunctions.render) {
430
+ console.log('🔄 Replacing cart functions...');
431
+ if (!nova.cart) {
432
+ nova.cart = {};
433
+ }
434
+ if (gameFunctions.update) {
435
+ nova.cart.update = gameFunctions.update;
436
+ console.log('✅ Replaced update function');
437
+ }
438
+ if (gameFunctions.draw) {
439
+ nova.cart.draw = gameFunctions.draw;
440
+ console.log('✅ Replaced draw function');
441
+ } else if (gameFunctions.render) {
442
+ // Support both draw() and render() naming conventions
443
+ nova.cart.draw = gameFunctions.render;
444
+ console.log('✅ Replaced draw function (from render)');
445
+ }
446
+ } else {
447
+ console.log('â„šī¸ No update/draw functions found in code');
448
+ }
449
+
450
+ // Resume the game loop
451
+ console.log('â–ļī¸ Resuming game loop...');
452
+ paused = false;
453
+
454
+ console.log('✅ Game Studio: Code executed successfully!');
455
+
456
+ // Send success message back
457
+ if (event.source) {
458
+ event.source.postMessage({ type: 'EXECUTE_SUCCESS' }, event.origin);
459
+ }
460
+ } catch (error) {
461
+ console.error('❌ Game Studio: Error executing code:', error);
462
+ console.error('Stack trace:', error.stack);
463
+ if (event.source) {
464
+ event.source.postMessage(
465
+ {
466
+ type: 'EXECUTE_ERROR',
467
+ error: error.message,
468
+ },
469
+ event.origin
470
+ );
471
+ }
472
+ }
473
+ }
474
+ });
package/vite.config.js ADDED
@@ -0,0 +1,63 @@
1
+ import { defineConfig } from 'vite';
2
+ import { resolve } from 'path';
3
+
4
+ export default defineConfig({
5
+ // Base path for deployment
6
+ base: './',
7
+
8
+ // Build configuration
9
+ build: {
10
+ target: 'es2022',
11
+ outDir: 'dist',
12
+ assetsDir: 'assets',
13
+ sourcemap: true,
14
+
15
+ // Optimize output
16
+ minify: 'terser',
17
+ terserOptions: {
18
+ compress: {
19
+ drop_console: false, // Keep console for debugging
20
+ drop_debugger: true,
21
+ },
22
+ },
23
+
24
+ // Chunk size warnings
25
+ chunkSizeWarningLimit: 1000,
26
+
27
+ // Rollup options
28
+ rollupOptions: {
29
+ input: {
30
+ main: resolve(__dirname, 'index.html'),
31
+ console: resolve(__dirname, 'console.html'),
32
+ },
33
+ },
34
+ },
35
+
36
+ // Dev server configuration
37
+ server: {
38
+ port: 5173,
39
+ open: true,
40
+ host: true, // Allow network access
41
+
42
+ // Hot module replacement
43
+ hmr: {
44
+ overlay: true,
45
+ },
46
+ },
47
+
48
+ // Optimizations
49
+ optimizeDeps: {
50
+ include: ['three', 'zustand'],
51
+ },
52
+
53
+ // Plugin configuration
54
+ plugins: [],
55
+
56
+ // Resolve configuration
57
+ resolve: {
58
+ alias: {
59
+ '@runtime': resolve(__dirname, 'runtime'),
60
+ '@examples': resolve(__dirname, 'examples'),
61
+ },
62
+ },
63
+ });