nova64 0.2.5 → 0.2.6
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/index.html +6 -1
- package/package.json +37 -32
- 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,315 @@
|
|
|
1
|
+
// NOVA64 INSTANCING SHOWCASE
|
|
2
|
+
// Demonstrates GPU instancing, LOD, and PBR/normal-map features
|
|
3
|
+
// added in the Phase 2/4 performance and feature updates.
|
|
4
|
+
//
|
|
5
|
+
// Controls:
|
|
6
|
+
// WASD / Arrow keys — move camera
|
|
7
|
+
// Q / E — zoom out / in
|
|
8
|
+
// 1 / 2 / 3 — switch showcase scene
|
|
9
|
+
// F — toggle wireframe HUD overlay
|
|
10
|
+
|
|
11
|
+
// ── State ───────────────────────────────────────────────────────────────────
|
|
12
|
+
let scene = 1;
|
|
13
|
+
let prevScene = 0;
|
|
14
|
+
let time = 0;
|
|
15
|
+
let showHUD = true;
|
|
16
|
+
|
|
17
|
+
// Instanced mesh handles (crystalId/dustId checked in update)
|
|
18
|
+
let crystalId = null;
|
|
19
|
+
let dustId = null;
|
|
20
|
+
let crystalLightId = null;
|
|
21
|
+
|
|
22
|
+
// Camera orbit
|
|
23
|
+
let camAngle = 0;
|
|
24
|
+
let camRadius = 30;
|
|
25
|
+
let camHeight = 12;
|
|
26
|
+
|
|
27
|
+
// ── Init ─────────────────────────────────────────────────────────────────────
|
|
28
|
+
export async function init() {
|
|
29
|
+
setCameraFOV(60);
|
|
30
|
+
setAmbientLight(0x223344, 0.4);
|
|
31
|
+
setDirectionalLight([-1, -2, -1], 0xffffff, 1.2);
|
|
32
|
+
setFog(0x0a0a1a, 30, 120);
|
|
33
|
+
enableBloom(0.8, 0.3, 0.5);
|
|
34
|
+
enableVignette(1.0, 0.85);
|
|
35
|
+
|
|
36
|
+
await loadScene(scene);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Scene loader ─────────────────────────────────────────────────────────────
|
|
40
|
+
async function loadScene(id) {
|
|
41
|
+
clearScene();
|
|
42
|
+
crystalId = dustId = crystalLightId = null;
|
|
43
|
+
|
|
44
|
+
// Ground plane — shared across scenes
|
|
45
|
+
createPlane(200, 200, 0x1a2a1a, [0, 0, 0]);
|
|
46
|
+
|
|
47
|
+
if (id === 1) await buildForestScene();
|
|
48
|
+
else if (id === 2) await buildCrystalScene();
|
|
49
|
+
else if (id === 3) await buildLODScene();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Scene 1: Forest — 500 instanced trees ────────────────────────────────
|
|
53
|
+
async function buildForestScene() {
|
|
54
|
+
setFog(0x0a1a08, 35, 130);
|
|
55
|
+
setAmbientLight(0x204020, 0.5);
|
|
56
|
+
|
|
57
|
+
// Trunks: 500 instanced cylinders
|
|
58
|
+
const trunkId = createInstancedMesh('cylinder', 500, 0x4a2e10, {
|
|
59
|
+
size: 0.25,
|
|
60
|
+
height: 3,
|
|
61
|
+
roughness: 1,
|
|
62
|
+
metalness: 0,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Canopies: 500 instanced spheres, per-instance colour
|
|
66
|
+
const canopyId = createInstancedMesh('sphere', 500, 0x226622, {
|
|
67
|
+
size: 0.9,
|
|
68
|
+
segments: 5,
|
|
69
|
+
roughness: 0.9,
|
|
70
|
+
metalness: 0,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const RANGE = 40;
|
|
74
|
+
for (let i = 0; i < 500; i++) {
|
|
75
|
+
const x = (Math.random() - 0.5) * RANGE;
|
|
76
|
+
const z = (Math.random() - 0.5) * RANGE;
|
|
77
|
+
const h = 2 + Math.random() * 3;
|
|
78
|
+
|
|
79
|
+
setInstanceTransform(trunkId, i, x, h * 0.5, z, 0, 0, 0, 1, h, 1);
|
|
80
|
+
setInstanceTransform(
|
|
81
|
+
canopyId,
|
|
82
|
+
i,
|
|
83
|
+
x,
|
|
84
|
+
h + 1.2,
|
|
85
|
+
z,
|
|
86
|
+
0,
|
|
87
|
+
0,
|
|
88
|
+
0,
|
|
89
|
+
1 + Math.random() * 0.5,
|
|
90
|
+
1 + Math.random() * 0.3,
|
|
91
|
+
1 + Math.random() * 0.5
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Vary canopy green shade
|
|
95
|
+
const g = 0x33 + Math.floor(Math.random() * 0x55);
|
|
96
|
+
setInstanceColor(canopyId, i, (0x10 << 16) | (g << 8) | 0x10);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
finalizeInstances(trunkId);
|
|
100
|
+
finalizeInstances(canopyId);
|
|
101
|
+
|
|
102
|
+
// Floating dust particles (animated in update)
|
|
103
|
+
dustId = createInstancedMesh('sphere', 200, 0xaaffaa, {
|
|
104
|
+
size: 0.05,
|
|
105
|
+
segments: 3,
|
|
106
|
+
emissive: 0x224422,
|
|
107
|
+
emissiveIntensity: 0.8,
|
|
108
|
+
});
|
|
109
|
+
for (let i = 0; i < 200; i++) {
|
|
110
|
+
setInstanceTransform(
|
|
111
|
+
dustId,
|
|
112
|
+
i,
|
|
113
|
+
(Math.random() - 0.5) * 35,
|
|
114
|
+
0.5 + Math.random() * 8,
|
|
115
|
+
(Math.random() - 0.5) * 35
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
finalizeInstances(dustId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Scene 2: Crystal Field — instanced prisms with per-instance colour ─────
|
|
122
|
+
async function buildCrystalScene() {
|
|
123
|
+
setFog(0x0a0020, 30, 100);
|
|
124
|
+
setAmbientLight(0x100030, 0.3);
|
|
125
|
+
crystalLightId = createPointLight(0x6644ff, 4, 20, 0, 8, 0);
|
|
126
|
+
createPointLight(0xff44aa, 3, 20, 10, 5, -10);
|
|
127
|
+
|
|
128
|
+
const CRYSTAL_COUNT = 300;
|
|
129
|
+
crystalId = createInstancedMesh('cone', CRYSTAL_COUNT, 0x8855ff, {
|
|
130
|
+
size: 0.6,
|
|
131
|
+
height: 3,
|
|
132
|
+
roughness: 0.1,
|
|
133
|
+
metalness: 0.8,
|
|
134
|
+
emissive: 0x220044,
|
|
135
|
+
emissiveIntensity: 0.5,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const hues = [0x8855ff, 0xff44cc, 0x44aaff, 0xffaa00, 0x44ffaa];
|
|
139
|
+
for (let i = 0; i < CRYSTAL_COUNT; i++) {
|
|
140
|
+
const angle = Math.random() * Math.PI * 2;
|
|
141
|
+
const r = 3 + Math.random() * 22;
|
|
142
|
+
const x = Math.cos(angle) * r;
|
|
143
|
+
const z = Math.sin(angle) * r;
|
|
144
|
+
const h = 0.8 + Math.random() * 3.5;
|
|
145
|
+
const tiltX = (Math.random() - 0.5) * 0.4;
|
|
146
|
+
const tiltZ = (Math.random() - 0.5) * 0.4;
|
|
147
|
+
|
|
148
|
+
setInstanceTransform(
|
|
149
|
+
crystalId,
|
|
150
|
+
i,
|
|
151
|
+
x,
|
|
152
|
+
h * 0.5,
|
|
153
|
+
z,
|
|
154
|
+
tiltX,
|
|
155
|
+
0,
|
|
156
|
+
tiltZ,
|
|
157
|
+
0.5 + Math.random() * 0.8,
|
|
158
|
+
h,
|
|
159
|
+
0.5 + Math.random() * 0.8
|
|
160
|
+
);
|
|
161
|
+
setInstanceColor(crystalId, i, hues[i % hues.length]);
|
|
162
|
+
}
|
|
163
|
+
finalizeInstances(crystalId);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Scene 3: LOD Rock Field ──────────────────────────────────────────────────
|
|
167
|
+
async function buildLODScene() {
|
|
168
|
+
setFog(0x1a1208, 40, 120);
|
|
169
|
+
setAmbientLight(0x302010, 0.6);
|
|
170
|
+
setDirectionalLight([-1, -2, 0.5], 0xffd090, 1.4);
|
|
171
|
+
|
|
172
|
+
// One LOD rock model: high-poly close, low-poly far
|
|
173
|
+
createLODMesh(
|
|
174
|
+
[
|
|
175
|
+
{
|
|
176
|
+
shape: 'sphere',
|
|
177
|
+
size: 2,
|
|
178
|
+
color: 0x887766,
|
|
179
|
+
distance: 0,
|
|
180
|
+
options: { segments: 8, roughness: 1, metalness: 0 },
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
shape: 'sphere',
|
|
184
|
+
size: 2,
|
|
185
|
+
color: 0x887766,
|
|
186
|
+
distance: 15,
|
|
187
|
+
options: { segments: 5, roughness: 1, metalness: 0 },
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
shape: 'cube',
|
|
191
|
+
size: 2,
|
|
192
|
+
color: 0x776655,
|
|
193
|
+
distance: 35,
|
|
194
|
+
options: { roughness: 1, metalness: 0 },
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
[0, 1, 0]
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Scatter 80 individual rocks using instancing
|
|
201
|
+
const ROCK_COUNT = 80;
|
|
202
|
+
const rocksId = createInstancedMesh('sphere', ROCK_COUNT, 0x887766, {
|
|
203
|
+
size: 1,
|
|
204
|
+
segments: 5,
|
|
205
|
+
roughness: 1,
|
|
206
|
+
metalness: 0,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const RANGE = 45;
|
|
210
|
+
for (let i = 0; i < ROCK_COUNT; i++) {
|
|
211
|
+
const x = (Math.random() - 0.5) * RANGE;
|
|
212
|
+
const z = (Math.random() - 0.5) * RANGE;
|
|
213
|
+
const s = 0.4 + Math.random() * 1.8;
|
|
214
|
+
setInstanceTransform(rocksId, i, x, s * 0.5, z, 0, Math.random() * Math.PI, 0, s, s * 0.7, s);
|
|
215
|
+
const shade = 0x66 + Math.floor(Math.random() * 0x44);
|
|
216
|
+
setInstanceColor(rocksId, i, (shade << 16) | ((shade - 0x10) << 8) | (shade - 0x20));
|
|
217
|
+
}
|
|
218
|
+
finalizeInstances(rocksId);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Update ───────────────────────────────────────────────────────────────────
|
|
222
|
+
export function update(dt) {
|
|
223
|
+
time += dt;
|
|
224
|
+
|
|
225
|
+
// Switch scene on 1/2/3
|
|
226
|
+
if (keyp('Digit1') && scene !== 1) {
|
|
227
|
+
scene = 1;
|
|
228
|
+
prevScene = 0;
|
|
229
|
+
}
|
|
230
|
+
if (keyp('Digit2') && scene !== 2) {
|
|
231
|
+
scene = 2;
|
|
232
|
+
prevScene = 0;
|
|
233
|
+
}
|
|
234
|
+
if (keyp('Digit3') && scene !== 3) {
|
|
235
|
+
scene = 3;
|
|
236
|
+
prevScene = 0;
|
|
237
|
+
}
|
|
238
|
+
if (keyp('KeyF')) showHUD = !showHUD;
|
|
239
|
+
|
|
240
|
+
if (scene !== prevScene) {
|
|
241
|
+
loadScene(scene);
|
|
242
|
+
prevScene = scene;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Camera orbit
|
|
246
|
+
if (key('KeyA') || key('ArrowLeft')) camAngle -= dt * 0.8;
|
|
247
|
+
if (key('KeyD') || key('ArrowRight')) camAngle += dt * 0.8;
|
|
248
|
+
if (key('KeyW') || key('ArrowUp')) camHeight = Math.min(40, camHeight + dt * 6);
|
|
249
|
+
if (key('KeyS') || key('ArrowDown')) camHeight = Math.max(3, camHeight - dt * 6);
|
|
250
|
+
if (key('KeyQ')) camRadius = Math.min(55, camRadius + dt * 8);
|
|
251
|
+
if (key('KeyE')) camRadius = Math.max(8, camRadius - dt * 8);
|
|
252
|
+
|
|
253
|
+
const cx = Math.cos(camAngle) * camRadius;
|
|
254
|
+
const cz = Math.sin(camAngle) * camRadius;
|
|
255
|
+
setCameraPosition(cx, camHeight, cz);
|
|
256
|
+
setCameraTarget(0, 3, 0);
|
|
257
|
+
|
|
258
|
+
// Animate dust in forest scene
|
|
259
|
+
if (scene === 1 && dustId !== null) {
|
|
260
|
+
for (let i = 0; i < 200; i++) {
|
|
261
|
+
const offset = i * 1.37;
|
|
262
|
+
const x = Math.sin(time * 0.3 + offset) * 17;
|
|
263
|
+
const y = 0.5 + ((time * 0.2 + offset * 0.5) % 8);
|
|
264
|
+
const z = Math.cos(time * 0.2 + offset * 0.7) * 17;
|
|
265
|
+
setInstanceTransform(dustId, i, x, y, z);
|
|
266
|
+
}
|
|
267
|
+
finalizeInstances(dustId);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Animate crystals — subtle pulse
|
|
271
|
+
if (scene === 2 && crystalId !== null && crystalLightId !== null) {
|
|
272
|
+
// Update point light position to orbit
|
|
273
|
+
setPointLightPosition(
|
|
274
|
+
crystalLightId,
|
|
275
|
+
Math.cos(time * 0.5) * 8,
|
|
276
|
+
6 + Math.sin(time * 0.7) * 2,
|
|
277
|
+
Math.sin(time * 0.5) * 8
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Update LOD (required each frame)
|
|
282
|
+
updateLODs();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Draw ─────────────────────────────────────────────────────────────────────
|
|
286
|
+
export function draw() {
|
|
287
|
+
if (!showHUD) return;
|
|
288
|
+
|
|
289
|
+
const sceneNames = [
|
|
290
|
+
'',
|
|
291
|
+
'FOREST (500 instanced trees)',
|
|
292
|
+
'CRYSTAL FIELD (300 instanced prisms)',
|
|
293
|
+
'LOD ROCKS (80 instanced + 1 LOD)',
|
|
294
|
+
];
|
|
295
|
+
const BLUE = rgba8(40, 80, 180, 200);
|
|
296
|
+
const WHITE = rgba8(255, 255, 255, 255);
|
|
297
|
+
const YELLOW = rgba8(255, 220, 50, 255);
|
|
298
|
+
const DIM = rgba8(180, 180, 180, 200);
|
|
299
|
+
|
|
300
|
+
// Title bar
|
|
301
|
+
drawRoundedRect(0, 0, 320, 18, 0, rgba8(0, 0, 0, 160));
|
|
302
|
+
printCentered('NOVA64 — INSTANCING SHOWCASE', 160, 4, WHITE);
|
|
303
|
+
|
|
304
|
+
// Scene name
|
|
305
|
+
drawRoundedRect(0, 210, 320, 30, 0, rgba8(0, 0, 0, 140));
|
|
306
|
+
printCentered(sceneNames[scene] ?? '', 160, 217, YELLOW);
|
|
307
|
+
printCentered('1=Forest 2=Crystals 3=LOD F=HUD WASD=Orbit QE=Zoom', 160, 225, DIM);
|
|
308
|
+
|
|
309
|
+
// Scene-specific stats
|
|
310
|
+
let statLine = '';
|
|
311
|
+
if (scene === 1) statLine = 'GPU draw calls: 3 | instances: 700';
|
|
312
|
+
else if (scene === 2) statLine = 'GPU draw calls: 1 | instances: 300';
|
|
313
|
+
else if (scene === 3) statLine = 'GPU draw calls: 2 | LOD levels: 3 | instances: 80';
|
|
314
|
+
print(statLine, 8, 22, BLUE);
|
|
315
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// Minecraft Demo - Ultimate Edition with Biomes
|
|
2
|
+
let player = {
|
|
3
|
+
x: 0,
|
|
4
|
+
y: 30,
|
|
5
|
+
z: 0,
|
|
6
|
+
vx: 0,
|
|
7
|
+
vy: 0,
|
|
8
|
+
vz: 0,
|
|
9
|
+
speed: 0.15,
|
|
10
|
+
jump: 0.35,
|
|
11
|
+
size: 0.6,
|
|
12
|
+
onGround: false,
|
|
13
|
+
yaw: 0,
|
|
14
|
+
pitch: 0,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let selectedBlock = 1; // 1 = Grass
|
|
18
|
+
let time = 0;
|
|
19
|
+
let loadState = 0;
|
|
20
|
+
let isLoaded = false;
|
|
21
|
+
let loadProgress = 0;
|
|
22
|
+
let currentBiome = 'Plains';
|
|
23
|
+
|
|
24
|
+
const BLOCK_NAMES = {
|
|
25
|
+
1: 'GRASS',
|
|
26
|
+
2: 'DIRT',
|
|
27
|
+
3: 'STONE',
|
|
28
|
+
4: 'SAND',
|
|
29
|
+
5: 'WATER',
|
|
30
|
+
6: 'WOOD',
|
|
31
|
+
7: 'LEAVES',
|
|
32
|
+
8: 'PLANKS',
|
|
33
|
+
};
|
|
34
|
+
const BLOCK_COLORS = {
|
|
35
|
+
1: 0x55cc33,
|
|
36
|
+
2: 0x886644,
|
|
37
|
+
3: 0x888888,
|
|
38
|
+
4: 0xddcc88,
|
|
39
|
+
5: 0x3388ff,
|
|
40
|
+
6: 0x664422,
|
|
41
|
+
7: 0x228833,
|
|
42
|
+
8: 0xccaa66,
|
|
43
|
+
};
|
|
44
|
+
const HOTBAR_BLOCKS = [1, 2, 3, 8, 6, 4];
|
|
45
|
+
|
|
46
|
+
// Biome detection (mirrors runtime terrain gen noise)
|
|
47
|
+
function detectBiome(px, pz) {
|
|
48
|
+
// Use same perlinNoise-based logic as runtime generateChunkTerrain
|
|
49
|
+
// We approximate using sin-based hash since we don't have perlinNoise exposed
|
|
50
|
+
const tx = px * 0.5,
|
|
51
|
+
tz = pz * 0.5;
|
|
52
|
+
const temperature =
|
|
53
|
+
Math.sin(tx * 0.01 * 3.7 + tz * 0.01 * 2.3) * 0.3 +
|
|
54
|
+
Math.sin(tx * 0.01 * 7.1 + tz * 0.01 * 5.9) * 0.15 +
|
|
55
|
+
0.5;
|
|
56
|
+
const mx = px * 0.3 + 1000,
|
|
57
|
+
mz = pz * 0.3 + 1000;
|
|
58
|
+
const moisture =
|
|
59
|
+
Math.sin(mx * 0.01 * 3.7 + mz * 0.01 * 2.3) * 0.3 +
|
|
60
|
+
Math.sin(mx * 0.01 * 7.1 + mz * 0.01 * 5.9) * 0.15 +
|
|
61
|
+
0.5;
|
|
62
|
+
|
|
63
|
+
if (temperature < 0.2) return 'Frozen Tundra';
|
|
64
|
+
if (temperature < 0.35 && moisture > 0.5) return 'Taiga';
|
|
65
|
+
if (temperature > 0.7 && moisture < 0.25) return 'Desert';
|
|
66
|
+
if (temperature > 0.6 && moisture > 0.6) return 'Jungle';
|
|
67
|
+
if (moisture < 0.3) return 'Savanna';
|
|
68
|
+
if (temperature > 0.4 && moisture > 0.4) return 'Forest';
|
|
69
|
+
if (temperature < 0.35) return 'Snowy Hills';
|
|
70
|
+
return 'Plains';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const BIOME_COLORS = {
|
|
74
|
+
'Frozen Tundra': rgba8(200, 220, 255),
|
|
75
|
+
Taiga: rgba8(100, 180, 140),
|
|
76
|
+
Desert: rgba8(255, 220, 130),
|
|
77
|
+
Jungle: rgba8(80, 220, 80),
|
|
78
|
+
Savanna: rgba8(220, 180, 100),
|
|
79
|
+
Forest: rgba8(100, 200, 100),
|
|
80
|
+
'Snowy Hills': rgba8(220, 230, 255),
|
|
81
|
+
Plains: rgba8(150, 220, 150),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function createVoxelTexture() {
|
|
85
|
+
const canvas = document.createElement('canvas');
|
|
86
|
+
canvas.width = 64;
|
|
87
|
+
canvas.height = 64;
|
|
88
|
+
const ctx = canvas.getContext('2d');
|
|
89
|
+
|
|
90
|
+
for (let y = 0; y < 64; y++) {
|
|
91
|
+
for (let x = 0; x < 64; x++) {
|
|
92
|
+
let noise = Math.random() * 60 - 30;
|
|
93
|
+
let isBorder = x % 16 === 0 || y % 16 === 0 ? -20 : 0;
|
|
94
|
+
let val = 180 + noise + isBorder;
|
|
95
|
+
ctx.fillStyle = `rgb(${val}, ${val}, ${val})`;
|
|
96
|
+
ctx.fillRect(x, y, 1, 1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const tex = new globalThis.THREE.CanvasTexture(canvas);
|
|
101
|
+
tex.magFilter = globalThis.THREE.NearestFilter;
|
|
102
|
+
tex.minFilter = globalThis.THREE.NearestFilter;
|
|
103
|
+
|
|
104
|
+
globalThis.window.VOXEL_MATERIAL = new globalThis.THREE.MeshStandardMaterial({
|
|
105
|
+
vertexColors: true,
|
|
106
|
+
map: tex,
|
|
107
|
+
flatShading: true,
|
|
108
|
+
roughness: 0.9,
|
|
109
|
+
metalness: 0.0,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getHighestBlockAlt(hx, hz) {
|
|
114
|
+
for (let i = 60; i > 0; i--) {
|
|
115
|
+
if (getVoxelBlock(hx, i, hz) !== 0) return i;
|
|
116
|
+
}
|
|
117
|
+
return 30;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function init() {
|
|
121
|
+
createVoxelTexture();
|
|
122
|
+
setCameraPosition(0, 30, 0);
|
|
123
|
+
setFog(0x87ceeb, 10, 60);
|
|
124
|
+
|
|
125
|
+
// We don't block here, we let the update/draw loop handle state
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function update() {
|
|
129
|
+
if (loadState === 0) {
|
|
130
|
+
loadState = 1;
|
|
131
|
+
return;
|
|
132
|
+
} else if (loadState === 1) {
|
|
133
|
+
// Wait a few frames for the canvas to present the loading text
|
|
134
|
+
loadState = 2;
|
|
135
|
+
return;
|
|
136
|
+
} else if (loadState === 2) {
|
|
137
|
+
if (typeof updateVoxelWorld === 'function') {
|
|
138
|
+
updateVoxelWorld(0, 0); // Gen initial chunks
|
|
139
|
+
}
|
|
140
|
+
player.y = getHighestBlockAlt(Math.floor(player.x), Math.floor(player.z)) + 2;
|
|
141
|
+
if (player.y < 5) player.y = 40; // Fallback
|
|
142
|
+
loadState = 3;
|
|
143
|
+
isLoaded = true;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!isLoaded) return;
|
|
148
|
+
|
|
149
|
+
time += 0.005;
|
|
150
|
+
let skyR = Math.sin(time) > 0 ? 135 : 10;
|
|
151
|
+
let skyG = Math.sin(time) > 0 ? 206 : 10;
|
|
152
|
+
let skyB = Math.sin(time) > 0 ? 235 : 20;
|
|
153
|
+
setFog((skyR << 16) | (skyG << 8) | skyB, 20, 80);
|
|
154
|
+
|
|
155
|
+
// Detect current biome
|
|
156
|
+
currentBiome = detectBiome(player.x, player.z);
|
|
157
|
+
|
|
158
|
+
handleInput();
|
|
159
|
+
updatePhysics();
|
|
160
|
+
updateCamera();
|
|
161
|
+
handleBlockInteraction();
|
|
162
|
+
|
|
163
|
+
if (typeof updateVoxelWorld === 'function') {
|
|
164
|
+
updateVoxelWorld(player.x, player.z);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handleInput() {
|
|
169
|
+
if (key('ArrowLeft')) player.yaw -= 0.05;
|
|
170
|
+
if (key('ArrowRight')) player.yaw += 0.05;
|
|
171
|
+
if (key('ArrowUp') && player.pitch < Math.PI / 2) player.pitch += 0.05;
|
|
172
|
+
if (key('ArrowDown') && player.pitch > -Math.PI / 2) player.pitch -= 0.05;
|
|
173
|
+
|
|
174
|
+
let dx = 0,
|
|
175
|
+
dz = 0;
|
|
176
|
+
const cosY = Math.cos(player.yaw);
|
|
177
|
+
const sinY = Math.sin(player.yaw);
|
|
178
|
+
|
|
179
|
+
if (key('KeyW')) {
|
|
180
|
+
dx -= sinY;
|
|
181
|
+
dz -= cosY;
|
|
182
|
+
}
|
|
183
|
+
if (key('KeyS')) {
|
|
184
|
+
dx += sinY;
|
|
185
|
+
dz += cosY;
|
|
186
|
+
}
|
|
187
|
+
if (key('KeyA')) {
|
|
188
|
+
dx -= cosY;
|
|
189
|
+
dz += sinY;
|
|
190
|
+
}
|
|
191
|
+
if (key('KeyD')) {
|
|
192
|
+
dx += cosY;
|
|
193
|
+
dz -= sinY;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (dx !== 0 || dz !== 0) {
|
|
197
|
+
const len = Math.sqrt(dx * dx + dz * dz);
|
|
198
|
+
player.vx = (dx / len) * player.speed;
|
|
199
|
+
player.vz = (dz / len) * player.speed;
|
|
200
|
+
} else {
|
|
201
|
+
player.vx = 0;
|
|
202
|
+
player.vz = 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (key('Space') && player.onGround) {
|
|
206
|
+
player.vy = player.jump;
|
|
207
|
+
player.onGround = false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Number keys for block selection
|
|
211
|
+
if (keyp('Digit1')) selectedBlock = HOTBAR_BLOCKS[0];
|
|
212
|
+
if (keyp('Digit2')) selectedBlock = HOTBAR_BLOCKS[1];
|
|
213
|
+
if (keyp('Digit3')) selectedBlock = HOTBAR_BLOCKS[2];
|
|
214
|
+
if (keyp('Digit4')) selectedBlock = HOTBAR_BLOCKS[3];
|
|
215
|
+
if (keyp('Digit5')) selectedBlock = HOTBAR_BLOCKS[4];
|
|
216
|
+
if (keyp('Digit6')) selectedBlock = HOTBAR_BLOCKS[5];
|
|
217
|
+
|
|
218
|
+
if (btnp(0)) selectedBlock = 1; // Grass
|
|
219
|
+
if (btnp(1)) selectedBlock = 2; // Dirt
|
|
220
|
+
if (btnp(2)) selectedBlock = 3; // Stone
|
|
221
|
+
if (btnp(3)) selectedBlock = 8; // Planks
|
|
222
|
+
|
|
223
|
+
// B key = respawn with new biome (random world + random position)
|
|
224
|
+
if (keyp('KeyB') && typeof resetVoxelWorld === 'function') {
|
|
225
|
+
resetVoxelWorld();
|
|
226
|
+
// Scatter to a random position so we land in a different biome
|
|
227
|
+
player.x = (Math.random() - 0.5) * 400;
|
|
228
|
+
player.z = (Math.random() - 0.5) * 400;
|
|
229
|
+
updateVoxelWorld(player.x, player.z);
|
|
230
|
+
player.y = getHighestBlockAlt(Math.floor(player.x), Math.floor(player.z)) + 2;
|
|
231
|
+
if (player.y < 5) player.y = 40;
|
|
232
|
+
player.vy = 0;
|
|
233
|
+
player.onGround = false;
|
|
234
|
+
player.yaw = 0;
|
|
235
|
+
player.pitch = 0;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function updatePhysics() {
|
|
240
|
+
player.vy -= 0.02;
|
|
241
|
+
|
|
242
|
+
let nx = player.x + player.vx;
|
|
243
|
+
let ny = player.y + player.vy;
|
|
244
|
+
let nz = player.z + player.vz;
|
|
245
|
+
|
|
246
|
+
player.onGround = false;
|
|
247
|
+
|
|
248
|
+
if (
|
|
249
|
+
checkCollision(player.x, ny - player.size, player.z) ||
|
|
250
|
+
checkCollision(player.x, ny + 0.2, player.z)
|
|
251
|
+
) {
|
|
252
|
+
if (player.vy < 0) player.onGround = true;
|
|
253
|
+
player.vy = 0;
|
|
254
|
+
ny = player.y;
|
|
255
|
+
}
|
|
256
|
+
if (
|
|
257
|
+
checkCollision(
|
|
258
|
+
nx + (player.vx > 0 ? player.size : -player.size),
|
|
259
|
+
player.y - player.size + 0.1,
|
|
260
|
+
player.z
|
|
261
|
+
)
|
|
262
|
+
) {
|
|
263
|
+
player.vx = 0;
|
|
264
|
+
nx = player.x;
|
|
265
|
+
}
|
|
266
|
+
if (
|
|
267
|
+
checkCollision(
|
|
268
|
+
player.x,
|
|
269
|
+
player.y - player.size + 0.1,
|
|
270
|
+
nz + (player.vz > 0 ? player.size : -player.size)
|
|
271
|
+
)
|
|
272
|
+
) {
|
|
273
|
+
player.vz = 0;
|
|
274
|
+
nz = player.z;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
player.x = nx;
|
|
278
|
+
player.y = ny;
|
|
279
|
+
player.z = nz;
|
|
280
|
+
|
|
281
|
+
// Antifall fallback
|
|
282
|
+
if (player.y < -10) {
|
|
283
|
+
player.y = 40;
|
|
284
|
+
player.vy = 0;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function checkCollision(x, y, z) {
|
|
289
|
+
if (typeof checkVoxelCollision === 'function') {
|
|
290
|
+
return checkVoxelCollision([x, y, z], player.size);
|
|
291
|
+
}
|
|
292
|
+
const block = getVoxelBlock(Math.floor(x), Math.floor(y), Math.floor(z));
|
|
293
|
+
return block !== 0 && block !== undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function updateCamera() {
|
|
297
|
+
setCameraPosition(player.x, player.y + 0.8, player.z);
|
|
298
|
+
const targetX = player.x - Math.sin(player.yaw) * Math.cos(player.pitch);
|
|
299
|
+
const targetY = player.y + 0.8 + Math.sin(player.pitch);
|
|
300
|
+
const targetZ = player.z - Math.cos(player.yaw) * Math.cos(player.pitch);
|
|
301
|
+
setCameraTarget(targetX, targetY, targetZ);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function handleBlockInteraction() {
|
|
305
|
+
if (typeof raycastVoxelBlock === 'function') {
|
|
306
|
+
const targetX = player.x - Math.sin(player.yaw) * Math.cos(player.pitch);
|
|
307
|
+
const targetY = player.y + 0.8 + Math.sin(player.pitch);
|
|
308
|
+
const targetZ = player.z - Math.cos(player.yaw) * Math.cos(player.pitch);
|
|
309
|
+
|
|
310
|
+
const dx = targetX - player.x;
|
|
311
|
+
const dy = targetY - (player.y + 0.8);
|
|
312
|
+
const dz = targetZ - player.z;
|
|
313
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
314
|
+
|
|
315
|
+
const rayStr = raycastVoxelBlock(
|
|
316
|
+
player.x,
|
|
317
|
+
player.y + 0.8,
|
|
318
|
+
player.z,
|
|
319
|
+
dx / len,
|
|
320
|
+
dy / len,
|
|
321
|
+
dz / len,
|
|
322
|
+
5
|
|
323
|
+
);
|
|
324
|
+
if (rayStr) {
|
|
325
|
+
if (btnp(4)) {
|
|
326
|
+
setVoxelBlock(rayStr.hit.x, rayStr.hit.y, rayStr.hit.z, 0); // break
|
|
327
|
+
}
|
|
328
|
+
if (btnp(5)) {
|
|
329
|
+
setVoxelBlock(rayStr.prev.x, rayStr.prev.y, rayStr.prev.z, selectedBlock); // place
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function draw() {
|
|
337
|
+
if (!isLoaded) {
|
|
338
|
+
rectfill(0, 0, 640, 360, rgba8(10, 10, 20, 255));
|
|
339
|
+
print('NOVA64 MINECRAFT EDITION', 20, 40, rgba8(255, 255, 255, 255));
|
|
340
|
+
print('GENERATING WORLD...', 20, 60, rgba8(255, 255, 255, 255));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Title bar
|
|
345
|
+
rect(0, 0, 640, 16, rgba8(0, 0, 0, 150), true);
|
|
346
|
+
print('MINECRAFT ULTIMATE 64', 5, 4, 0xffdd88);
|
|
347
|
+
const pos = `${Math.floor(player.x)}, ${Math.floor(player.y)}, ${Math.floor(player.z)}`;
|
|
348
|
+
print(pos, 560, 4, rgba8(200, 200, 200, 200));
|
|
349
|
+
|
|
350
|
+
// Biome indicator
|
|
351
|
+
const biomeCol = BIOME_COLORS[currentBiome] || rgba8(200, 200, 200);
|
|
352
|
+
print(currentBiome, 220, 4, biomeCol);
|
|
353
|
+
|
|
354
|
+
// Crosshair
|
|
355
|
+
const cx = 320,
|
|
356
|
+
cy = 180;
|
|
357
|
+
rect(cx - 1, cy - 8, 2, 16, rgba8(255, 255, 255, 200), true);
|
|
358
|
+
rect(cx - 8, cy - 1, 16, 2, rgba8(255, 255, 255, 200), true);
|
|
359
|
+
|
|
360
|
+
// Hotbar
|
|
361
|
+
const hbY = 340;
|
|
362
|
+
const hbW = HOTBAR_BLOCKS.length * 32 + 8;
|
|
363
|
+
const hbX = (640 - hbW) / 2;
|
|
364
|
+
rect(hbX, hbY, hbW, 20, rgba8(0, 0, 0, 180), true);
|
|
365
|
+
rect(hbX, hbY, hbW, 20, rgba8(100, 100, 100, 150), false);
|
|
366
|
+
for (let i = 0; i < HOTBAR_BLOCKS.length; i++) {
|
|
367
|
+
const bx = hbX + 4 + i * 32;
|
|
368
|
+
const bid = HOTBAR_BLOCKS[i];
|
|
369
|
+
const col = BLOCK_COLORS[bid] || 0xffffff;
|
|
370
|
+
if (bid === selectedBlock) {
|
|
371
|
+
rect(bx - 1, hbY - 1, 30, 22, rgba8(255, 255, 255, 255), false);
|
|
372
|
+
}
|
|
373
|
+
rect(bx + 2, hbY + 2, 24, 16, col, true);
|
|
374
|
+
print(`${i + 1}`, bx + 10, hbY + 4, rgba8(255, 255, 255, 220));
|
|
375
|
+
}
|
|
376
|
+
// Block name below hotbar
|
|
377
|
+
const bname = BLOCK_NAMES[selectedBlock] || 'UNKNOWN';
|
|
378
|
+
print(bname, (640 - bname.length * 8) / 2, hbY - 14, rgba8(255, 255, 255, 200));
|
|
379
|
+
|
|
380
|
+
// Controls hint
|
|
381
|
+
print(
|
|
382
|
+
'WASD=Move Space=Jump Arrows=Look L/R Click=Break/Place 1-6=Block B=New Biome',
|
|
383
|
+
30,
|
|
384
|
+
20,
|
|
385
|
+
rgba8(255, 255, 255, 200)
|
|
386
|
+
);
|
|
387
|
+
}
|