nova64 0.2.4 → 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.
Files changed (140) 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/index.html +6 -1
  118. package/package.json +37 -32
  119. package/public/assets/sky/studio/nx.png +0 -0
  120. package/public/assets/sky/studio/ny.png +0 -0
  121. package/public/assets/sky/studio/nz.png +0 -0
  122. package/public/assets/sky/studio/px.png +0 -0
  123. package/public/assets/sky/studio/py.png +0 -0
  124. package/public/assets/sky/studio/pz.png +0 -0
  125. package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
  126. package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  127. package/public/os9-shell/index.html +10 -1
  128. package/runtime/api-2d.js +301 -21
  129. package/runtime/api-3d/pbr.js +45 -1
  130. package/runtime/api-3d.js +1 -0
  131. package/runtime/api-effects.js +90 -3
  132. package/runtime/api-gameutils.js +476 -0
  133. package/runtime/api-generative.js +610 -0
  134. package/runtime/api-skybox.js +54 -0
  135. package/runtime/api-voxel.js +139 -28
  136. package/runtime/gpu-threejs.js +13 -9
  137. package/runtime/ui.js +2 -2
  138. package/src/main.js +24 -1
  139. package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
  140. 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
+ }