nova64 0.2.5 โ†’ 0.2.7

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 (185) hide show
  1. package/README.md +25 -8
  2. package/bin/nova64.js +165 -0
  3. package/dist/assets/console-CY_kygm3.js +14 -0
  4. package/dist/assets/console-CY_kygm3.js.map +1 -0
  5. package/dist/assets/main-l0sNRNKZ.js.map +1 -0
  6. package/dist/assets/sky/studio/nx.png +0 -0
  7. package/dist/assets/sky/studio/ny.png +0 -0
  8. package/dist/assets/sky/studio/nz.png +0 -0
  9. package/dist/assets/sky/studio/px.png +0 -0
  10. package/dist/assets/sky/studio/py.png +0 -0
  11. package/dist/assets/sky/studio/pz.png +0 -0
  12. package/dist/assets/vanilla-Dcuy32gi.js +2 -0
  13. package/dist/assets/vanilla-Dcuy32gi.js.map +1 -0
  14. package/dist/console.html +899 -0
  15. package/dist/docs/BENCHMARK.md +77 -0
  16. package/dist/docs/CHEATSHEET.md +255 -0
  17. package/dist/docs/EFFECTS_API_GUIDE.md +577 -0
  18. package/dist/docs/EFFECTS_QUICK_REFERENCE.md +331 -0
  19. package/dist/docs/FONT_CHARACTER_REFERENCE.md +219 -0
  20. package/dist/docs/FREE_GLB_ASSETS.md +330 -0
  21. package/dist/docs/FULLSCREEN_BUTTON_FEATURE.md +296 -0
  22. package/dist/docs/GAMEPAD_SUPPORT.md +348 -0
  23. package/dist/docs/GAME_IMPROVEMENTS.md +278 -0
  24. package/dist/docs/GAME_QUALITY_STATUS.md +300 -0
  25. package/dist/docs/MIGRATION_GUIDE.md +553 -0
  26. package/dist/docs/NOVA64_3D_API.md +356 -0
  27. package/dist/docs/NOVA64_API_REFERENCE.md +1406 -0
  28. package/dist/docs/NOVA64_UI_API.md +503 -0
  29. package/dist/docs/UI_SYSTEM_SUMMARY.md +445 -0
  30. package/dist/docs/VOXEL_ENGINE_GUIDE.md +662 -0
  31. package/dist/docs/VOXEL_QUICK_REFERENCE.md +386 -0
  32. package/dist/docs/api-3d.html +750 -0
  33. package/dist/docs/api-effects.html +385 -0
  34. package/dist/docs/api-improvements.md +121 -0
  35. package/dist/docs/api-skybox.html +407 -0
  36. package/dist/docs/api-sprites.html +321 -0
  37. package/dist/docs/api-voxel.html +337 -0
  38. package/dist/docs/api.html +543 -0
  39. package/dist/docs/assets.html +306 -0
  40. package/dist/docs/audio.html +340 -0
  41. package/dist/docs/blogs.html +286 -0
  42. package/dist/docs/collision.html +316 -0
  43. package/dist/docs/console.html +247 -0
  44. package/dist/docs/editor.html +297 -0
  45. package/dist/docs/font.html +247 -0
  46. package/dist/docs/framebuffer.html +247 -0
  47. package/dist/docs/fullscreen-button.html +297 -0
  48. package/dist/docs/gpu-systems.html +247 -0
  49. package/dist/docs/index.html +580 -0
  50. package/dist/docs/input.html +491 -0
  51. package/dist/docs/physics.html +311 -0
  52. package/dist/docs/screens.html +311 -0
  53. package/dist/docs/storage.html +311 -0
  54. package/dist/docs/textinput.html +332 -0
  55. package/dist/docs/ui.html +488 -0
  56. package/dist/examples/3d-advanced/code.js +695 -0
  57. package/dist/examples/adventure-comic-3d/code.js +342 -0
  58. package/dist/examples/audio-lab/code.js +150 -0
  59. package/dist/examples/boids-flocking/code.js +270 -0
  60. package/dist/examples/crystal-cathedral-3d/code.js +706 -0
  61. package/dist/examples/cyberpunk-city-3d/code.js +1383 -0
  62. package/dist/examples/demoscene/README.md +192 -0
  63. package/dist/examples/demoscene/code.js +1081 -0
  64. package/dist/examples/demoscene/meta.json +21 -0
  65. package/dist/examples/dungeon-crawler-3d/code.js +1117 -0
  66. package/dist/examples/f-zero-nova-3d/code.js +865 -0
  67. package/dist/examples/f-zero-nova-3d/code_old.js +1555 -0
  68. package/dist/examples/fps-demo-3d/code.js +744 -0
  69. package/dist/examples/game-of-life-3d/code.js +338 -0
  70. package/dist/examples/generative-art/code.js +632 -0
  71. package/dist/examples/hello-3d/code.js +325 -0
  72. package/dist/examples/hello-skybox/code.js +183 -0
  73. package/dist/examples/hello-world/code.js +19 -0
  74. package/dist/examples/input-showcase/code.js +109 -0
  75. package/dist/examples/instancing-demo/code.js +315 -0
  76. package/dist/examples/minecraft-demo/code.js +387 -0
  77. package/dist/examples/model-viewer-3d/code.js +114 -0
  78. package/dist/examples/mystical-realm-3d/code.js +1203 -0
  79. package/dist/examples/nature-explorer-3d/code.js +1318 -0
  80. package/dist/examples/particles-demo/code.js +522 -0
  81. package/dist/examples/pbr-showcase/code.js +140 -0
  82. package/dist/examples/physics-demo-3d/code.js +948 -0
  83. package/dist/examples/screen-demo/code.js +267 -0
  84. package/dist/examples/shooter-demo-3d/code.js +1286 -0
  85. package/dist/examples/space-combat-3d/IMPLEMENTATION_SUMMARY.md +109 -0
  86. package/dist/examples/space-combat-3d/README.md +135 -0
  87. package/dist/examples/space-combat-3d/code.js +1332 -0
  88. package/dist/examples/space-harrier-3d/code.js +923 -0
  89. package/dist/examples/star-fox-nova-3d/code.js +1116 -0
  90. package/dist/examples/star-fox-nova-3d/code_backup.js +410 -0
  91. package/dist/examples/star-fox-nova-3d/code_broken.js +1821 -0
  92. package/dist/examples/storage-quest/code.js +209 -0
  93. package/dist/examples/strider-demo-3d/IMPROVEMENT_OPTIONS.md +285 -0
  94. package/dist/examples/strider-demo-3d/cache-test.html +132 -0
  95. package/dist/examples/strider-demo-3d/code-fixed.js +582 -0
  96. package/dist/examples/strider-demo-3d/code-old.js +1537 -0
  97. package/dist/examples/strider-demo-3d/code.js +1462 -0
  98. package/dist/examples/strider-demo-3d/code.js.bak2 +1169 -0
  99. package/dist/examples/strider-demo-3d/fix-game.sh +53 -0
  100. package/dist/examples/super-plumber-64/README.md +128 -0
  101. package/dist/examples/super-plumber-64/code.js +1185 -0
  102. package/dist/examples/super-plumber-64/index.html +88 -0
  103. package/dist/examples/test-2d-overlay/code.js +32 -0
  104. package/dist/examples/test-font/code.js +51 -0
  105. package/dist/examples/test-minimal/code.js +21 -0
  106. package/dist/examples/ui-demo/code.js +306 -0
  107. package/dist/examples/wing-commander-space/README.md +180 -0
  108. package/dist/examples/wing-commander-space/code.js +1285 -0
  109. package/dist/examples/wizardry-3d/CHANGELOG.md +366 -0
  110. package/dist/examples/wizardry-3d/code.js +3928 -0
  111. package/dist/index.html +666 -0
  112. package/dist/os9-shell/assets/index-DIHfrTaW.css +1 -0
  113. package/dist/os9-shell/assets/index-KchE_ngx.js +483 -0
  114. package/dist/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  115. package/dist/os9-shell/index.html +23 -0
  116. package/dist/os9-shell/nova-icon.svg +12 -0
  117. package/dist/runtime/api-2d.js +1158 -0
  118. package/dist/runtime/api-3d/camera.js +73 -0
  119. package/dist/runtime/api-3d/instancing.js +180 -0
  120. package/dist/runtime/api-3d/lights.js +51 -0
  121. package/dist/runtime/api-3d/materials.js +47 -0
  122. package/dist/runtime/api-3d/models.js +84 -0
  123. package/dist/runtime/api-3d/particles.js +296 -0
  124. package/dist/runtime/api-3d/pbr.js +113 -0
  125. package/dist/runtime/api-3d/primitives.js +304 -0
  126. package/dist/runtime/api-3d/scene.js +169 -0
  127. package/dist/runtime/api-3d/transforms.js +161 -0
  128. package/dist/runtime/api-3d.js +166 -0
  129. package/dist/runtime/api-effects.js +840 -0
  130. package/dist/runtime/api-gameutils.js +476 -0
  131. package/dist/runtime/api-generative.js +610 -0
  132. package/dist/runtime/api-presets.js +85 -0
  133. package/dist/runtime/api-skybox.js +232 -0
  134. package/dist/runtime/api-sprites.js +100 -0
  135. package/dist/runtime/api-voxel.js +712 -0
  136. package/dist/runtime/api.js +201 -0
  137. package/dist/runtime/assets.js +27 -0
  138. package/dist/runtime/audio.js +114 -0
  139. package/dist/runtime/collision.js +47 -0
  140. package/dist/runtime/console.js +101 -0
  141. package/dist/runtime/editor.js +233 -0
  142. package/dist/runtime/font.js +233 -0
  143. package/dist/runtime/framebuffer.js +28 -0
  144. package/dist/runtime/fullscreen-button.js +185 -0
  145. package/dist/runtime/gpu-canvas2d.js +47 -0
  146. package/dist/runtime/gpu-threejs.js +643 -0
  147. package/dist/runtime/gpu-webgl2.js +310 -0
  148. package/dist/runtime/index.d.ts +682 -0
  149. package/dist/runtime/index.js +22 -0
  150. package/dist/runtime/input.js +225 -0
  151. package/dist/runtime/logger.js +60 -0
  152. package/dist/runtime/physics.js +101 -0
  153. package/dist/runtime/screens.js +213 -0
  154. package/dist/runtime/storage.js +38 -0
  155. package/dist/runtime/store.js +151 -0
  156. package/dist/runtime/textinput.js +68 -0
  157. package/dist/runtime/ui/buttons.js +124 -0
  158. package/dist/runtime/ui/panels.js +105 -0
  159. package/dist/runtime/ui/text.js +86 -0
  160. package/dist/runtime/ui/widgets.js +141 -0
  161. package/dist/runtime/ui.js +111 -0
  162. package/index.html +6 -1
  163. package/package.json +9 -2
  164. package/public/assets/sky/studio/nx.png +0 -0
  165. package/public/assets/sky/studio/ny.png +0 -0
  166. package/public/assets/sky/studio/nz.png +0 -0
  167. package/public/assets/sky/studio/px.png +0 -0
  168. package/public/assets/sky/studio/py.png +0 -0
  169. package/public/assets/sky/studio/pz.png +0 -0
  170. package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
  171. package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  172. package/public/os9-shell/index.html +10 -1
  173. package/runtime/api-2d.js +301 -21
  174. package/runtime/api-3d/pbr.js +45 -1
  175. package/runtime/api-3d.js +1 -0
  176. package/runtime/api-effects.js +90 -3
  177. package/runtime/api-gameutils.js +476 -0
  178. package/runtime/api-generative.js +610 -0
  179. package/runtime/api-skybox.js +54 -0
  180. package/runtime/api-voxel.js +139 -28
  181. package/runtime/gpu-threejs.js +13 -9
  182. package/runtime/ui.js +2 -2
  183. package/src/main.js +20 -0
  184. package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
  185. package/public/os9-shell/assets/index-B1Uvacma.js.map +0 -1
@@ -0,0 +1,201 @@
1
+ // runtime/api.js
2
+ // Helpers to pack/unpack 64-bit color (RGBA64)
3
+ function packRGBA64(r16, g16, b16, a16) {
4
+ // Ensure all values are integers before converting to BigInt
5
+ const r = BigInt(Math.floor(r16));
6
+ const g = BigInt(Math.floor(g16));
7
+ const b = BigInt(Math.floor(b16));
8
+ const a = BigInt(Math.floor(a16));
9
+ return (r << 48n) | (g << 32n) | (b << 16n) | a;
10
+ }
11
+ function unpackRGBA64(c) {
12
+ // Handle both BigInt and regular number inputs
13
+ if (typeof c === 'bigint') {
14
+ return {
15
+ r: Number((c >> 48n) & 0xffffn),
16
+ g: Number((c >> 32n) & 0xffffn),
17
+ b: Number((c >> 16n) & 0xffffn),
18
+ a: Number(c & 0xffffn),
19
+ };
20
+ } else {
21
+ // Handle regular number input - convert to BigInt first
22
+ const bigC = BigInt(Math.floor(c));
23
+ return {
24
+ r: Number((bigC >> 48n) & 0xffffn),
25
+ g: Number((bigC >> 32n) & 0xffffn),
26
+ b: Number((bigC >> 16n) & 0xffffn),
27
+ a: Number(bigC & 0xffffn),
28
+ };
29
+ }
30
+ }
31
+ function rgba8(r, g, b, a = 255) {
32
+ // Clamp input values to 0-255 range and ensure they're integers
33
+ const clampedR = Math.max(0, Math.min(255, Math.floor(r)));
34
+ const clampedG = Math.max(0, Math.min(255, Math.floor(g)));
35
+ const clampedB = Math.max(0, Math.min(255, Math.floor(b)));
36
+ const clampedA = Math.max(0, Math.min(255, Math.floor(a)));
37
+
38
+ const s = 257;
39
+ return packRGBA64(clampedR * s, clampedG * s, clampedB * s, clampedA * s);
40
+ }
41
+
42
+ import { BitmapFont } from './font.js';
43
+
44
+ export function stdApi(gpu) {
45
+ const fb = gpu.getFramebuffer();
46
+
47
+ // Camera
48
+ const camRef = { x: 0, y: 0 };
49
+ function setCamera(x, y) {
50
+ camRef.x = x | 0;
51
+ camRef.y = y | 0;
52
+ }
53
+ function getCamera() {
54
+ return camRef;
55
+ }
56
+
57
+ function _colorToRGBA16(c) {
58
+ if (typeof c === 'bigint' || typeof c === 'number') {
59
+ return unpackRGBA64(c);
60
+ }
61
+ return { r: 65535, g: 65535, b: 65535, a: 65535 };
62
+ }
63
+ function cls(color) {
64
+ const { r, g, b, a } =
65
+ typeof color === 'bigint' ? unpackRGBA64(color) : { r: 0, g: 0, b: 0, a: 65535 };
66
+ fb.fill(r, g, b, a);
67
+ }
68
+ function pset(x, y, color) {
69
+ const { r, g, b, a } = _colorToRGBA16(color);
70
+ fb.pset((x | 0) - camRef.x, (y | 0) - camRef.y, r, g, b, a);
71
+ }
72
+ function line(x0, y0, x1, y1, color) {
73
+ const { r, g, b, a } = _colorToRGBA16(color);
74
+ x0 = (x0 | 0) - camRef.x;
75
+ y0 = (y0 | 0) - camRef.y;
76
+ x1 = (x1 | 0) - camRef.x;
77
+ y1 = (y1 | 0) - camRef.y;
78
+ let dx = Math.abs(x1 - x0),
79
+ sx = x0 < x1 ? 1 : -1;
80
+ let dy = -Math.abs(y1 - y0),
81
+ sy = y0 < y1 ? 1 : -1;
82
+ let err = dx + dy;
83
+ // eslint-disable-next-line no-constant-condition
84
+ while (true) {
85
+ if (x0 >= 0 && y0 >= 0 && x0 < fb.width && y0 < fb.height) fb.pset(x0, y0, r, g, b, a);
86
+ if (x0 === x1 && y0 === y1) break;
87
+ const e2 = 2 * err;
88
+ if (e2 >= dy) {
89
+ err += dy;
90
+ x0 += sx;
91
+ }
92
+ if (e2 <= dx) {
93
+ err += dx;
94
+ y0 += sy;
95
+ }
96
+ }
97
+ }
98
+ function rect(x, y, w, h, color, fill = false) {
99
+ const { r, g, b, a } = _colorToRGBA16(color);
100
+ x = (x | 0) - camRef.x;
101
+ y = (y | 0) - camRef.y;
102
+ w |= 0;
103
+ h |= 0;
104
+ const x0 = Math.max(0, x),
105
+ y0 = Math.max(0, y);
106
+ const x1 = Math.min(fb.width, x + w),
107
+ y1 = Math.min(fb.height, y + h);
108
+ if (fill) {
109
+ for (let yy = y0; yy < y1; yy++) {
110
+ for (let xx = x0; xx < x1; xx++) fb.pset(xx, yy, r, g, b, a);
111
+ }
112
+ } else {
113
+ for (let xx = x0; xx < x1; xx++) {
114
+ if (y >= 0 && y < fb.height) fb.pset(xx, y, r, g, b, a);
115
+ if (y + h - 1 >= 0 && y + h - 1 < fb.height) fb.pset(xx, y + h - 1, r, g, b, a);
116
+ }
117
+ for (let yy = y0; yy < y1; yy++) {
118
+ if (x >= 0 && x < fb.width) fb.pset(x, yy, r, g, b, a);
119
+ if (x + w - 1 >= 0 && x + w - 1 < fb.width) fb.pset(x + w - 1, yy, r, g, b, a);
120
+ }
121
+ }
122
+ }
123
+
124
+ function circle(x, y, radius, color, fill = false) {
125
+ const { r, g, b, a } = _colorToRGBA16(color);
126
+ x = (x | 0) - camRef.x;
127
+ y = (y | 0) - camRef.y;
128
+ radius |= 0;
129
+
130
+ if (fill) {
131
+ // Filled circle using scanline algorithm
132
+ for (let dy = -radius; dy <= radius; dy++) {
133
+ const dx = Math.floor(Math.sqrt(radius * radius - dy * dy));
134
+ for (let xx = x - dx; xx <= x + dx; xx++) {
135
+ const yy = y + dy;
136
+ if (xx >= 0 && xx < fb.width && yy >= 0 && yy < fb.height) {
137
+ fb.pset(xx, yy, r, g, b, a);
138
+ }
139
+ }
140
+ }
141
+ } else {
142
+ // Midpoint circle algorithm (Bresenham)
143
+ let dx = radius,
144
+ dy = 0,
145
+ err = 0;
146
+ while (dx >= dy) {
147
+ const plots = [
148
+ [x + dx, y + dy],
149
+ [x + dy, y + dx],
150
+ [x - dy, y + dx],
151
+ [x - dx, y + dy],
152
+ [x - dx, y - dy],
153
+ [x - dy, y - dx],
154
+ [x + dy, y - dx],
155
+ [x + dx, y - dy],
156
+ ];
157
+ for (const [px, py] of plots) {
158
+ if (px >= 0 && px < fb.width && py >= 0 && py < fb.height) {
159
+ fb.pset(px, py, r, g, b, a);
160
+ }
161
+ }
162
+ if (err <= 0) {
163
+ dy += 1;
164
+ err += 2 * dy + 1;
165
+ }
166
+ if (err > 0) {
167
+ dx -= 1;
168
+ err -= 2 * dx + 1;
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ function rectfill(x, y, w, h, color) {
175
+ rect(x, y, w, h, color, true);
176
+ }
177
+
178
+ function print(text, x, y, color = rgba8(255, 255, 255, 255), scale = 1) {
179
+ BitmapFont.draw(fb, text, (x | 0) - camRef.x, (y | 0) - camRef.y, color, scale);
180
+ }
181
+
182
+ return {
183
+ exposeTo(target) {
184
+ Object.assign(target, {
185
+ cls,
186
+ pset,
187
+ line,
188
+ rect,
189
+ rectfill,
190
+ circle,
191
+ print,
192
+ packRGBA64,
193
+ rgba8,
194
+ setCamera,
195
+ getCamera,
196
+ });
197
+ },
198
+ };
199
+ }
200
+
201
+ export { packRGBA64, rgba8, unpackRGBA64 };
@@ -0,0 +1,27 @@
1
+ // runtime/assets.js
2
+ export class SpriteSheet {
3
+ constructor(image, width, tileSize = 8) {
4
+ this.image = image; // HTMLImageElement
5
+ this.sheetWidth = width;
6
+ this.tileSize = tileSize;
7
+ this.cols = Math.floor(width / tileSize);
8
+ }
9
+ }
10
+
11
+ export async function loadImageElement(url) {
12
+ const img = new Image();
13
+ img.src = url;
14
+ await img.decode();
15
+ return img;
16
+ }
17
+
18
+ export async function loadSpriteSheet(url, tileSize = 8) {
19
+ const img = await loadImageElement(url);
20
+ return new SpriteSheet(img, img.naturalWidth, tileSize);
21
+ }
22
+
23
+ export async function loadTilemap(url) {
24
+ const res = await fetch(url);
25
+ if (!res.ok) throw new Error('Failed to load tilemap: ' + url);
26
+ return await res.json(); // { width, height, data }
27
+ }
@@ -0,0 +1,114 @@
1
+ // runtime/audio.js
2
+ export class AudioSystem {
3
+ constructor() {
4
+ this.ctx = null;
5
+ this.master = null;
6
+ this.channels = 8;
7
+ this.gains = [];
8
+ }
9
+ _ensure() {
10
+ if (this.ctx) return;
11
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
12
+ this.ctx = ctx;
13
+ this.master = ctx.createGain();
14
+ this.master.gain.value = 0.4;
15
+ this.master.connect(ctx.destination);
16
+ for (let i = 0; i < this.channels; i++) {
17
+ const g = ctx.createGain();
18
+ g.gain.value = 0.0;
19
+ g.connect(this.master);
20
+ this.gains.push(g);
21
+ }
22
+ }
23
+ setVolume(v) {
24
+ this._ensure();
25
+ this.master.gain.value = Math.max(0, Math.min(1, v));
26
+ }
27
+ // sfx({ wave:'square'|'sine'|'sawtooth'|'triangle'|'noise', freq:Hz, dur:sec, vol:0..1, sweep:Hz/sec })
28
+ sfx(opts = {}) {
29
+ this._ensure();
30
+ const ctx = this.ctx;
31
+ const { wave = 'square', freq = 440, dur = 0.2, vol = 0.5, sweep = 0 } = opts;
32
+ const g = this.gains[Math.floor(Math.random() * this.channels)];
33
+ const now = ctx.currentTime;
34
+ const v = Math.max(0, Math.min(1, vol));
35
+ g.gain.cancelScheduledValues(now);
36
+ g.gain.setValueAtTime(0, now);
37
+ g.gain.linearRampToValueAtTime(v, now + 0.005);
38
+ g.gain.linearRampToValueAtTime(0.0001, now + dur);
39
+
40
+ if (wave === 'noise') {
41
+ const bufferSize = 1 << 14;
42
+ const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
43
+ const data = buffer.getChannelData(0);
44
+ for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
45
+ const src = ctx.createBufferSource();
46
+ src.buffer = buffer;
47
+ src.connect(g);
48
+ src.start(now);
49
+ src.stop(now + dur);
50
+ return;
51
+ }
52
+
53
+ const osc = ctx.createOscillator();
54
+ osc.type = wave;
55
+ osc.frequency.setValueAtTime(freq, now);
56
+ if (sweep !== 0)
57
+ osc.frequency.linearRampToValueAtTime(Math.max(1, freq + sweep * dur), now + dur);
58
+ osc.connect(g);
59
+ osc.start(now);
60
+ osc.stop(now + dur);
61
+ }
62
+ }
63
+
64
+ export const audio = new AudioSystem();
65
+
66
+ // Named SFX presets โ€” use sfx('jump'), sfx('coin'), sfx(0), or sfx({wave, freq, ...})
67
+ const SFX_PRESETS = {
68
+ // Numeric shortcuts (backward compat)
69
+ 0: { wave: 'square', freq: 880, dur: 0.1, vol: 0.4 },
70
+ 1: { wave: 'sine', freq: 220, dur: 0.3, vol: 0.3, sweep: -100 },
71
+ 2: { wave: 'noise', dur: 0.2, vol: 0.3 },
72
+ // Named presets
73
+ jump: { wave: 'square', freq: 300, dur: 0.12, vol: 0.4, sweep: 200 },
74
+ land: { wave: 'noise', dur: 0.08, vol: 0.3 },
75
+ coin: { wave: 'sine', freq: 1046, dur: 0.15, vol: 0.5, sweep: 400 },
76
+ powerup: { wave: 'sine', freq: 440, dur: 0.4, vol: 0.5, sweep: 880 },
77
+ explosion: { wave: 'noise', dur: 0.4, vol: 0.8 },
78
+ laser: { wave: 'square', freq: 1200, dur: 0.1, vol: 0.4, sweep: -800 },
79
+ hit: { wave: 'square', freq: 200, dur: 0.15, vol: 0.5, sweep: -100 },
80
+ death: { wave: 'sawtooth', freq: 440, dur: 0.6, vol: 0.5, sweep: -400 },
81
+ select: { wave: 'sine', freq: 660, dur: 0.08, vol: 0.3 },
82
+ confirm: { wave: 'sine', freq: 880, dur: 0.12, vol: 0.3, sweep: 220 },
83
+ error: { wave: 'square', freq: 180, dur: 0.3, vol: 0.4, sweep: -30 },
84
+ blip: { wave: 'square', freq: 440, dur: 0.06, vol: 0.3 },
85
+ };
86
+
87
+ export function audioApi() {
88
+ return {
89
+ exposeTo(target) {
90
+ Object.assign(target, {
91
+ /**
92
+ * Play a sound effect.
93
+ * @param {number|string|object} idOrOpts - Preset id (0/1/2), preset name ('jump','coin',...), or options object
94
+ * @param {object} [maybeOpts] - Extra options to merge when using a preset id/name
95
+ */
96
+ sfx: (idOrOpts, maybeOpts) => {
97
+ if (typeof idOrOpts === 'number' || typeof idOrOpts === 'string') {
98
+ const preset = SFX_PRESETS[idOrOpts];
99
+ if (preset) {
100
+ audio.sfx(Object.assign({}, preset, maybeOpts || {}));
101
+ }
102
+ } else {
103
+ audio.sfx(idOrOpts || {});
104
+ }
105
+ },
106
+ /**
107
+ * Set master volume (0 = silent, 1 = full).
108
+ * @param {number} v
109
+ */
110
+ setVolume: v => audio.setVolume(v),
111
+ });
112
+ },
113
+ };
114
+ }
@@ -0,0 +1,47 @@
1
+ // runtime/collision.js
2
+ export function aabb(ax, ay, aw, ah, bx, by, bw, bh) {
3
+ return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
4
+ }
5
+ export function circle(ax, ay, ar, bx, by, br) {
6
+ const dx = ax - bx,
7
+ dy = ay - by;
8
+ return dx * dx + dy * dy <= (ar + br) * (ar + br);
9
+ }
10
+
11
+ // DDA raycast against a tilemap; tileFn(tx,ty)->boolean solid
12
+ export function raycastTilemap(x0, y0, dx, dy, maxDist, tileSize, tileFn) {
13
+ let t = 0;
14
+ const stepX = Math.sign(dx) || 1;
15
+ const stepY = Math.sign(dy) || 1;
16
+ const invDx = dx !== 0 ? 1 / dx : 1e9;
17
+ const invDy = dy !== 0 ? 1 / dy : 1e9;
18
+ let tx = Math.floor(x0 / tileSize);
19
+ let ty = Math.floor(y0 / tileSize);
20
+ const nextBoundary = (p, dp, s) => {
21
+ const grid =
22
+ s > 0 ? (Math.floor(p / tileSize) + 1) * tileSize : Math.floor(p / tileSize) * tileSize;
23
+ return (grid - p) * (dp !== 0 ? 1 / dp : 1e9);
24
+ };
25
+ let tMaxX = nextBoundary(x0, dx, stepX);
26
+ let tMaxY = nextBoundary(y0, dy, stepY);
27
+ const tDeltaX = Math.abs(tileSize * invDx);
28
+ const tDeltaY = Math.abs(tileSize * invDy);
29
+
30
+ if (tileFn(tx, ty)) return { hit: true, tx, ty, t: 0, x: x0, y: y0 };
31
+
32
+ while (t <= maxDist) {
33
+ if (tMaxX < tMaxY) {
34
+ t = tMaxX;
35
+ tMaxX += tDeltaX;
36
+ tx += stepX;
37
+ } else {
38
+ t = tMaxY;
39
+ tMaxY += tDeltaY;
40
+ ty += stepY;
41
+ }
42
+ const x = x0 + dx * t;
43
+ const y = y0 + dy * t;
44
+ if (tileFn(tx, ty)) return { hit: true, tx, ty, t, x, y };
45
+ }
46
+ return { hit: false };
47
+ }
@@ -0,0 +1,101 @@
1
+ // runtime/console.js
2
+ import { logger } from './logger.js';
3
+ export class Nova64 {
4
+ constructor(gpu) {
5
+ this.gpu = gpu;
6
+ this.cart = null;
7
+ this._loadGeneration = 0; // Guard against concurrent loadCart race conditions
8
+ }
9
+ async loadCart(modulePath) {
10
+ // Bump generation โ€” any earlier in-flight loadCart will see the mismatch and bail.
11
+ const gen = ++this._loadGeneration;
12
+ logger.info(`๐Ÿงน Clearing previous scene before loading new cart... (gen=${gen})`);
13
+
14
+ // CRITICAL: Null out cart FIRST to prevent old update() from running during transition
15
+ this.cart = null;
16
+
17
+ // Clear UI buttons and panels from previous cart
18
+ if (typeof globalThis.clearButtons === 'function') {
19
+ globalThis.clearButtons();
20
+ }
21
+ if (typeof globalThis.clearPanels === 'function') {
22
+ globalThis.clearPanels();
23
+ }
24
+
25
+ // Reset screen manager to clear registered screens from previous cart
26
+ if (globalThis.screens && typeof globalThis.screens.reset === 'function') {
27
+ globalThis.screens.reset();
28
+ }
29
+
30
+ // Clear the 3D scene completely before loading new cart
31
+ if (typeof globalThis.clearScene === 'function') {
32
+ globalThis.clearScene();
33
+ }
34
+
35
+ // Also clear any skybox
36
+ if (typeof globalThis.clearSkybox === 'function') {
37
+ globalThis.clearSkybox();
38
+ }
39
+
40
+ // Reset camera to default position
41
+ if (typeof globalThis.setCameraPosition === 'function') {
42
+ globalThis.setCameraPosition(0, 5, 10);
43
+ }
44
+ if (typeof globalThis.setCameraTarget === 'function') {
45
+ globalThis.setCameraTarget(0, 0, 0);
46
+ }
47
+
48
+ // Reset fog to default
49
+ if (typeof globalThis.setFog === 'function') {
50
+ globalThis.setFog(0x87ceeb, 50, 200);
51
+ }
52
+
53
+ logger.info('โœ… Scene cleared, loading new cart:', modulePath);
54
+
55
+ const mod = await import(/* @vite-ignore */ modulePath + '?t=' + Date.now());
56
+
57
+ // RACE-CONDITION GUARD: If a newer loadCart was called while we awaited the
58
+ // import, our generation is stale โ€” abort so only the latest cart initialises.
59
+ if (gen !== this._loadGeneration) {
60
+ logger.warn(
61
+ `โš ๏ธ loadCart(${modulePath}) superseded by a newer load (gen ${gen} vs ${this._loadGeneration}), aborting.`
62
+ );
63
+ return;
64
+ }
65
+
66
+ // Clear scene AGAIN right before init โ€” in case a concurrent loadCart
67
+ // already ran and added its own objects while we were awaiting import.
68
+ if (typeof globalThis.clearScene === 'function') {
69
+ globalThis.clearScene();
70
+ }
71
+ if (typeof globalThis.clearSkybox === 'function') {
72
+ globalThis.clearSkybox();
73
+ }
74
+ if (typeof globalThis.clearButtons === 'function') {
75
+ globalThis.clearButtons();
76
+ }
77
+ if (typeof globalThis.clearPanels === 'function') {
78
+ globalThis.clearPanels();
79
+ }
80
+
81
+ this.cart = {
82
+ init: mod.init || (() => {}),
83
+ update: mod.update || (() => {}),
84
+ draw: mod.draw || (() => {}),
85
+ };
86
+ try {
87
+ await this.cart.init();
88
+ // Final guard: if yet ANOTHER loadCart fired during init, don't keep this cart
89
+ if (gen !== this._loadGeneration) {
90
+ logger.warn(
91
+ `โš ๏ธ loadCart(${modulePath}) superseded during init (gen ${gen} vs ${this._loadGeneration}), aborting.`
92
+ );
93
+ this.cart = null;
94
+ return;
95
+ }
96
+ logger.info('โœ… Cart init() complete:', modulePath);
97
+ } catch (e) {
98
+ logger.error('โŒ Cart init() threw:', e.message, e.stack);
99
+ }
100
+ }
101
+ }