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.
- package/README.md +25 -8
- package/bin/nova64.js +165 -0
- package/dist/assets/console-CY_kygm3.js +14 -0
- package/dist/assets/console-CY_kygm3.js.map +1 -0
- package/dist/assets/main-l0sNRNKZ.js.map +1 -0
- package/dist/assets/sky/studio/nx.png +0 -0
- package/dist/assets/sky/studio/ny.png +0 -0
- package/dist/assets/sky/studio/nz.png +0 -0
- package/dist/assets/sky/studio/px.png +0 -0
- package/dist/assets/sky/studio/py.png +0 -0
- package/dist/assets/sky/studio/pz.png +0 -0
- package/dist/assets/vanilla-Dcuy32gi.js +2 -0
- package/dist/assets/vanilla-Dcuy32gi.js.map +1 -0
- package/dist/console.html +899 -0
- package/dist/docs/BENCHMARK.md +77 -0
- package/dist/docs/CHEATSHEET.md +255 -0
- package/dist/docs/EFFECTS_API_GUIDE.md +577 -0
- package/dist/docs/EFFECTS_QUICK_REFERENCE.md +331 -0
- package/dist/docs/FONT_CHARACTER_REFERENCE.md +219 -0
- package/dist/docs/FREE_GLB_ASSETS.md +330 -0
- package/dist/docs/FULLSCREEN_BUTTON_FEATURE.md +296 -0
- package/dist/docs/GAMEPAD_SUPPORT.md +348 -0
- package/dist/docs/GAME_IMPROVEMENTS.md +278 -0
- package/dist/docs/GAME_QUALITY_STATUS.md +300 -0
- package/dist/docs/MIGRATION_GUIDE.md +553 -0
- package/dist/docs/NOVA64_3D_API.md +356 -0
- package/dist/docs/NOVA64_API_REFERENCE.md +1406 -0
- package/dist/docs/NOVA64_UI_API.md +503 -0
- package/dist/docs/UI_SYSTEM_SUMMARY.md +445 -0
- package/dist/docs/VOXEL_ENGINE_GUIDE.md +662 -0
- package/dist/docs/VOXEL_QUICK_REFERENCE.md +386 -0
- package/dist/docs/api-3d.html +750 -0
- package/dist/docs/api-effects.html +385 -0
- package/dist/docs/api-improvements.md +121 -0
- package/dist/docs/api-skybox.html +407 -0
- package/dist/docs/api-sprites.html +321 -0
- package/dist/docs/api-voxel.html +337 -0
- package/dist/docs/api.html +543 -0
- package/dist/docs/assets.html +306 -0
- package/dist/docs/audio.html +340 -0
- package/dist/docs/blogs.html +286 -0
- package/dist/docs/collision.html +316 -0
- package/dist/docs/console.html +247 -0
- package/dist/docs/editor.html +297 -0
- package/dist/docs/font.html +247 -0
- package/dist/docs/framebuffer.html +247 -0
- package/dist/docs/fullscreen-button.html +297 -0
- package/dist/docs/gpu-systems.html +247 -0
- package/dist/docs/index.html +580 -0
- package/dist/docs/input.html +491 -0
- package/dist/docs/physics.html +311 -0
- package/dist/docs/screens.html +311 -0
- package/dist/docs/storage.html +311 -0
- package/dist/docs/textinput.html +332 -0
- package/dist/docs/ui.html +488 -0
- package/dist/examples/3d-advanced/code.js +695 -0
- package/dist/examples/adventure-comic-3d/code.js +342 -0
- package/dist/examples/audio-lab/code.js +150 -0
- package/dist/examples/boids-flocking/code.js +270 -0
- package/dist/examples/crystal-cathedral-3d/code.js +706 -0
- package/dist/examples/cyberpunk-city-3d/code.js +1383 -0
- package/dist/examples/demoscene/README.md +192 -0
- package/dist/examples/demoscene/code.js +1081 -0
- package/dist/examples/demoscene/meta.json +21 -0
- package/dist/examples/dungeon-crawler-3d/code.js +1117 -0
- package/dist/examples/f-zero-nova-3d/code.js +865 -0
- package/dist/examples/f-zero-nova-3d/code_old.js +1555 -0
- package/dist/examples/fps-demo-3d/code.js +744 -0
- package/dist/examples/game-of-life-3d/code.js +338 -0
- package/dist/examples/generative-art/code.js +632 -0
- package/dist/examples/hello-3d/code.js +325 -0
- package/dist/examples/hello-skybox/code.js +183 -0
- package/dist/examples/hello-world/code.js +19 -0
- package/dist/examples/input-showcase/code.js +109 -0
- package/dist/examples/instancing-demo/code.js +315 -0
- package/dist/examples/minecraft-demo/code.js +387 -0
- package/dist/examples/model-viewer-3d/code.js +114 -0
- package/dist/examples/mystical-realm-3d/code.js +1203 -0
- package/dist/examples/nature-explorer-3d/code.js +1318 -0
- package/dist/examples/particles-demo/code.js +522 -0
- package/dist/examples/pbr-showcase/code.js +140 -0
- package/dist/examples/physics-demo-3d/code.js +948 -0
- package/dist/examples/screen-demo/code.js +267 -0
- package/dist/examples/shooter-demo-3d/code.js +1286 -0
- package/dist/examples/space-combat-3d/IMPLEMENTATION_SUMMARY.md +109 -0
- package/dist/examples/space-combat-3d/README.md +135 -0
- package/dist/examples/space-combat-3d/code.js +1332 -0
- package/dist/examples/space-harrier-3d/code.js +923 -0
- package/dist/examples/star-fox-nova-3d/code.js +1116 -0
- package/dist/examples/star-fox-nova-3d/code_backup.js +410 -0
- package/dist/examples/star-fox-nova-3d/code_broken.js +1821 -0
- package/dist/examples/storage-quest/code.js +209 -0
- package/dist/examples/strider-demo-3d/IMPROVEMENT_OPTIONS.md +285 -0
- package/dist/examples/strider-demo-3d/cache-test.html +132 -0
- package/dist/examples/strider-demo-3d/code-fixed.js +582 -0
- package/dist/examples/strider-demo-3d/code-old.js +1537 -0
- package/dist/examples/strider-demo-3d/code.js +1462 -0
- package/dist/examples/strider-demo-3d/code.js.bak2 +1169 -0
- package/dist/examples/strider-demo-3d/fix-game.sh +53 -0
- package/dist/examples/super-plumber-64/README.md +128 -0
- package/dist/examples/super-plumber-64/code.js +1185 -0
- package/dist/examples/super-plumber-64/index.html +88 -0
- package/dist/examples/test-2d-overlay/code.js +32 -0
- package/dist/examples/test-font/code.js +51 -0
- package/dist/examples/test-minimal/code.js +21 -0
- package/dist/examples/ui-demo/code.js +306 -0
- package/dist/examples/wing-commander-space/README.md +180 -0
- package/dist/examples/wing-commander-space/code.js +1285 -0
- package/dist/examples/wizardry-3d/CHANGELOG.md +366 -0
- package/dist/examples/wizardry-3d/code.js +3928 -0
- package/dist/index.html +666 -0
- package/dist/os9-shell/assets/index-DIHfrTaW.css +1 -0
- package/dist/os9-shell/assets/index-KchE_ngx.js +483 -0
- package/dist/os9-shell/assets/index-KchE_ngx.js.map +1 -0
- package/dist/os9-shell/index.html +23 -0
- package/dist/os9-shell/nova-icon.svg +12 -0
- package/dist/runtime/api-2d.js +1158 -0
- package/dist/runtime/api-3d/camera.js +73 -0
- package/dist/runtime/api-3d/instancing.js +180 -0
- package/dist/runtime/api-3d/lights.js +51 -0
- package/dist/runtime/api-3d/materials.js +47 -0
- package/dist/runtime/api-3d/models.js +84 -0
- package/dist/runtime/api-3d/particles.js +296 -0
- package/dist/runtime/api-3d/pbr.js +113 -0
- package/dist/runtime/api-3d/primitives.js +304 -0
- package/dist/runtime/api-3d/scene.js +169 -0
- package/dist/runtime/api-3d/transforms.js +161 -0
- package/dist/runtime/api-3d.js +166 -0
- package/dist/runtime/api-effects.js +840 -0
- package/dist/runtime/api-gameutils.js +476 -0
- package/dist/runtime/api-generative.js +610 -0
- package/dist/runtime/api-presets.js +85 -0
- package/dist/runtime/api-skybox.js +232 -0
- package/dist/runtime/api-sprites.js +100 -0
- package/dist/runtime/api-voxel.js +712 -0
- package/dist/runtime/api.js +201 -0
- package/dist/runtime/assets.js +27 -0
- package/dist/runtime/audio.js +114 -0
- package/dist/runtime/collision.js +47 -0
- package/dist/runtime/console.js +101 -0
- package/dist/runtime/editor.js +233 -0
- package/dist/runtime/font.js +233 -0
- package/dist/runtime/framebuffer.js +28 -0
- package/dist/runtime/fullscreen-button.js +185 -0
- package/dist/runtime/gpu-canvas2d.js +47 -0
- package/dist/runtime/gpu-threejs.js +643 -0
- package/dist/runtime/gpu-webgl2.js +310 -0
- package/dist/runtime/index.d.ts +682 -0
- package/dist/runtime/index.js +22 -0
- package/dist/runtime/input.js +225 -0
- package/dist/runtime/logger.js +60 -0
- package/dist/runtime/physics.js +101 -0
- package/dist/runtime/screens.js +213 -0
- package/dist/runtime/storage.js +38 -0
- package/dist/runtime/store.js +151 -0
- package/dist/runtime/textinput.js +68 -0
- package/dist/runtime/ui/buttons.js +124 -0
- package/dist/runtime/ui/panels.js +105 -0
- package/dist/runtime/ui/text.js +86 -0
- package/dist/runtime/ui/widgets.js +141 -0
- package/dist/runtime/ui.js +111 -0
- package/index.html +6 -1
- package/package.json +9 -2
- package/public/assets/sky/studio/nx.png +0 -0
- package/public/assets/sky/studio/ny.png +0 -0
- package/public/assets/sky/studio/nz.png +0 -0
- package/public/assets/sky/studio/px.png +0 -0
- package/public/assets/sky/studio/py.png +0 -0
- package/public/assets/sky/studio/pz.png +0 -0
- package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
- package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
- package/public/os9-shell/index.html +10 -1
- package/runtime/api-2d.js +301 -21
- package/runtime/api-3d/pbr.js +45 -1
- package/runtime/api-3d.js +1 -0
- package/runtime/api-effects.js +90 -3
- package/runtime/api-gameutils.js +476 -0
- package/runtime/api-generative.js +610 -0
- package/runtime/api-skybox.js +54 -0
- package/runtime/api-voxel.js +139 -28
- package/runtime/gpu-threejs.js +13 -9
- package/runtime/ui.js +2 -2
- package/src/main.js +20 -0
- package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
- 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
|
+
}
|