nova64 0.2.6 → 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 (47) hide show
  1. package/dist/examples/strider-demo-3d/fix-game.sh +0 -0
  2. package/dist/runtime/api-2d.js +1158 -0
  3. package/dist/runtime/api-3d/camera.js +73 -0
  4. package/dist/runtime/api-3d/instancing.js +180 -0
  5. package/dist/runtime/api-3d/lights.js +51 -0
  6. package/dist/runtime/api-3d/materials.js +47 -0
  7. package/dist/runtime/api-3d/models.js +84 -0
  8. package/dist/runtime/api-3d/particles.js +296 -0
  9. package/dist/runtime/api-3d/pbr.js +113 -0
  10. package/dist/runtime/api-3d/primitives.js +304 -0
  11. package/dist/runtime/api-3d/scene.js +169 -0
  12. package/dist/runtime/api-3d/transforms.js +161 -0
  13. package/dist/runtime/api-3d.js +166 -0
  14. package/dist/runtime/api-effects.js +840 -0
  15. package/dist/runtime/api-gameutils.js +476 -0
  16. package/dist/runtime/api-generative.js +610 -0
  17. package/dist/runtime/api-presets.js +85 -0
  18. package/dist/runtime/api-skybox.js +232 -0
  19. package/dist/runtime/api-sprites.js +100 -0
  20. package/dist/runtime/api-voxel.js +712 -0
  21. package/dist/runtime/api.js +201 -0
  22. package/dist/runtime/assets.js +27 -0
  23. package/dist/runtime/audio.js +114 -0
  24. package/dist/runtime/collision.js +47 -0
  25. package/dist/runtime/console.js +101 -0
  26. package/dist/runtime/editor.js +233 -0
  27. package/dist/runtime/font.js +233 -0
  28. package/dist/runtime/framebuffer.js +28 -0
  29. package/dist/runtime/fullscreen-button.js +185 -0
  30. package/dist/runtime/gpu-canvas2d.js +47 -0
  31. package/dist/runtime/gpu-threejs.js +643 -0
  32. package/dist/runtime/gpu-webgl2.js +310 -0
  33. package/dist/runtime/index.d.ts +682 -0
  34. package/dist/runtime/index.js +22 -0
  35. package/dist/runtime/input.js +225 -0
  36. package/dist/runtime/logger.js +60 -0
  37. package/dist/runtime/physics.js +101 -0
  38. package/dist/runtime/screens.js +213 -0
  39. package/dist/runtime/storage.js +38 -0
  40. package/dist/runtime/store.js +151 -0
  41. package/dist/runtime/textinput.js +68 -0
  42. package/dist/runtime/ui/buttons.js +124 -0
  43. package/dist/runtime/ui/panels.js +105 -0
  44. package/dist/runtime/ui/text.js +86 -0
  45. package/dist/runtime/ui/widgets.js +141 -0
  46. package/dist/runtime/ui.js +111 -0
  47. package/package.json +34 -32
@@ -0,0 +1,712 @@
1
+ /**
2
+ * Nova64 Voxel Engine API
3
+ *
4
+ * Efficient voxel rendering system for Minecraft-style games with:
5
+ * - Chunk-based world management
6
+ * - Greedy meshing for performance
7
+ * - Multiple block types with textures
8
+ * - World generation (terrain, caves, trees)
9
+ * - Block placement/breaking
10
+ * - Collision detection
11
+ */
12
+
13
+ import * as THREE from 'three';
14
+
15
+ export function voxelApi(gpu) {
16
+ // World configuration
17
+ const CHUNK_SIZE = 16;
18
+ const CHUNK_HEIGHT = 64;
19
+ const RENDER_DISTANCE = 4; // chunks in each direction
20
+
21
+ // Block types
22
+ const BLOCK_TYPES = {
23
+ AIR: 0,
24
+ GRASS: 1,
25
+ DIRT: 2,
26
+ STONE: 3,
27
+ SAND: 4,
28
+ WATER: 5,
29
+ WOOD: 6,
30
+ LEAVES: 7,
31
+ COBBLESTONE: 8,
32
+ PLANKS: 9,
33
+ GLASS: 10,
34
+ BRICK: 11,
35
+ SNOW: 12,
36
+ ICE: 13,
37
+ BEDROCK: 14,
38
+ };
39
+
40
+ // Block colors (for texture-less rendering) — tuned for visual biome distinction
41
+ const BLOCK_COLORS = {
42
+ [BLOCK_TYPES.GRASS]: 0x55cc33,
43
+ [BLOCK_TYPES.DIRT]: 0x996644,
44
+ [BLOCK_TYPES.STONE]: 0xaaaaaa,
45
+ [BLOCK_TYPES.SAND]: 0xffdd88,
46
+ [BLOCK_TYPES.WATER]: 0x2288dd,
47
+ [BLOCK_TYPES.WOOD]: 0x774422,
48
+ [BLOCK_TYPES.LEAVES]: 0x116622,
49
+ [BLOCK_TYPES.COBBLESTONE]: 0x667788,
50
+ [BLOCK_TYPES.PLANKS]: 0xddaa55,
51
+ [BLOCK_TYPES.GLASS]: 0xccffff,
52
+ [BLOCK_TYPES.BRICK]: 0xcc4433,
53
+ [BLOCK_TYPES.SNOW]: 0xeeeeff,
54
+ [BLOCK_TYPES.ICE]: 0x99ddff,
55
+ [BLOCK_TYPES.BEDROCK]: 0x333333,
56
+ };
57
+
58
+ // World data
59
+ const chunks = new Map(); // key: "x,z" -> Chunk
60
+ const chunkMeshes = new Map(); // key: "x,z" -> THREE.Mesh
61
+ let worldSeed = Math.floor(Math.random() * 50000); // random start for biome variety
62
+
63
+ // Noise function for terrain generation (simple Perlin-like)
64
+ function noise2D(x, z) {
65
+ const n = Math.sin(x * 12.9898 + z * 78.233) * 43758.5453;
66
+ return n - Math.floor(n);
67
+ }
68
+
69
+ function smoothNoise(x, z) {
70
+ const corners =
71
+ (noise2D(x - 1, z - 1) +
72
+ noise2D(x + 1, z - 1) +
73
+ noise2D(x - 1, z + 1) +
74
+ noise2D(x + 1, z + 1)) /
75
+ 16;
76
+ const sides =
77
+ (noise2D(x - 1, z) + noise2D(x + 1, z) + noise2D(x, z - 1) + noise2D(x, z + 1)) / 8;
78
+ const center = noise2D(x, z) / 4;
79
+ return corners + sides + center;
80
+ }
81
+
82
+ function interpolatedNoise(x, z) {
83
+ const intX = Math.floor(x);
84
+ const fracX = x - intX;
85
+ const intZ = Math.floor(z);
86
+ const fracZ = z - intZ;
87
+
88
+ const v1 = smoothNoise(intX, intZ);
89
+ const v2 = smoothNoise(intX + 1, intZ);
90
+ const v3 = smoothNoise(intX, intZ + 1);
91
+ const v4 = smoothNoise(intX + 1, intZ + 1);
92
+
93
+ const i1 = v1 * (1 - fracX) + v2 * fracX;
94
+ const i2 = v3 * (1 - fracX) + v4 * fracX;
95
+
96
+ return i1 * (1 - fracZ) + i2 * fracZ;
97
+ }
98
+
99
+ function perlinNoise(x, z, octaves = 4, persistence = 0.5) {
100
+ let total = 0;
101
+ let frequency = 1;
102
+ let amplitude = 1;
103
+ let maxValue = 0;
104
+
105
+ for (let i = 0; i < octaves; i++) {
106
+ total += interpolatedNoise(x * frequency * 0.01, z * frequency * 0.01) * amplitude;
107
+ maxValue += amplitude;
108
+ amplitude *= persistence;
109
+ frequency *= 2;
110
+ }
111
+
112
+ return total / maxValue;
113
+ }
114
+
115
+ // Chunk class
116
+ class Chunk {
117
+ constructor(chunkX, chunkZ) {
118
+ this.chunkX = chunkX;
119
+ this.chunkZ = chunkZ;
120
+ this.blocks = new Uint8Array(CHUNK_SIZE * CHUNK_HEIGHT * CHUNK_SIZE);
121
+ this.dirty = true;
122
+ }
123
+
124
+ getBlock(x, y, z) {
125
+ if (x < 0 || x >= CHUNK_SIZE || y < 0 || y >= CHUNK_HEIGHT || z < 0 || z >= CHUNK_SIZE) {
126
+ return BLOCK_TYPES.AIR;
127
+ }
128
+ const index = x + z * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE;
129
+ return this.blocks[index];
130
+ }
131
+
132
+ setBlock(x, y, z, blockType) {
133
+ if (x < 0 || x >= CHUNK_SIZE || y < 0 || y >= CHUNK_HEIGHT || z < 0 || z >= CHUNK_SIZE) {
134
+ return;
135
+ }
136
+ const index = x + z * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE;
137
+ this.blocks[index] = blockType;
138
+ this.dirty = true;
139
+ }
140
+ }
141
+
142
+ // Get or create chunk
143
+ function getChunk(chunkX, chunkZ) {
144
+ const key = `${chunkX},${chunkZ}`;
145
+ if (!chunks.has(key)) {
146
+ const chunk = new Chunk(chunkX, chunkZ);
147
+ generateChunkTerrain(chunk);
148
+ chunks.set(key, chunk);
149
+ }
150
+ return chunks.get(key);
151
+ }
152
+
153
+ // Get chunk without creating it (returns null if doesn't exist)
154
+ function getChunkIfExists(chunkX, chunkZ) {
155
+ const key = `${chunkX},${chunkZ}`;
156
+ return chunks.get(key) || null;
157
+ }
158
+
159
+ // Terrain generation
160
+ function generateChunkTerrain(chunk) {
161
+ const baseX = chunk.chunkX * CHUNK_SIZE;
162
+ const baseZ = chunk.chunkZ * CHUNK_SIZE;
163
+
164
+ // Pending trees to place after terrain fill (avoid overwriting terrain)
165
+ const pendingTrees = [];
166
+
167
+ for (let x = 0; x < CHUNK_SIZE; x++) {
168
+ for (let z = 0; z < CHUNK_SIZE; z++) {
169
+ const worldX = baseX + x;
170
+ const worldZ = baseZ + z;
171
+
172
+ // Biome selection via temperature + moisture noise
173
+ const temperature = perlinNoise(worldX * 0.5 + worldSeed, worldZ * 0.5 + worldSeed, 2, 0.5);
174
+ const moisture = perlinNoise(
175
+ worldX * 0.3 + 1000 + worldSeed,
176
+ worldZ * 0.3 + 1000 + worldSeed,
177
+ 2,
178
+ 0.5
179
+ );
180
+
181
+ // Per-biome height profile — each biome has unique surface, terrain shape, and density
182
+ let heightScale = 20;
183
+ let heightBase = 32;
184
+ let surfaceBlock = BLOCK_TYPES.GRASS;
185
+ let subBlock = BLOCK_TYPES.DIRT;
186
+ let treeChance = 0;
187
+ let waterLevel = 30;
188
+
189
+ if (temperature < 0.2) {
190
+ // ── Frozen Tundra ── flat icy plains
191
+ surfaceBlock = BLOCK_TYPES.SNOW;
192
+ subBlock = BLOCK_TYPES.ICE;
193
+ heightScale = 6;
194
+ heightBase = 33;
195
+ treeChance = 0;
196
+ } else if (temperature < 0.35 && moisture > 0.5) {
197
+ // ── Taiga ── gray rocky forest
198
+ surfaceBlock = BLOCK_TYPES.COBBLESTONE;
199
+ subBlock = BLOCK_TYPES.STONE;
200
+ heightScale = 18;
201
+ heightBase = 34;
202
+ treeChance = 0.06;
203
+ } else if (temperature > 0.7 && moisture < 0.25) {
204
+ // ── Desert ── flat sand dunes
205
+ surfaceBlock = BLOCK_TYPES.SAND;
206
+ subBlock = BLOCK_TYPES.SAND;
207
+ heightScale = 4;
208
+ heightBase = 31;
209
+ treeChance = 0;
210
+ } else if (temperature > 0.6 && moisture > 0.6) {
211
+ // ── Jungle ── dense dark-green valleys
212
+ surfaceBlock = BLOCK_TYPES.LEAVES;
213
+ subBlock = BLOCK_TYPES.DIRT;
214
+ heightScale = 22;
215
+ heightBase = 28;
216
+ treeChance = 0.15;
217
+ } else if (moisture < 0.3) {
218
+ // ── Savanna ── dry brown earth
219
+ surfaceBlock = BLOCK_TYPES.DIRT;
220
+ subBlock = BLOCK_TYPES.SAND;
221
+ heightScale = 5;
222
+ heightBase = 33;
223
+ treeChance = 0.005;
224
+ } else if (temperature > 0.4 && moisture > 0.4) {
225
+ // ── Forest ── classic green hills
226
+ surfaceBlock = BLOCK_TYPES.GRASS;
227
+ subBlock = BLOCK_TYPES.DIRT;
228
+ heightScale = 14;
229
+ heightBase = 32;
230
+ treeChance = 0.08;
231
+ } else if (temperature < 0.35) {
232
+ // ── Snowy Hills ── tall snowy mountains
233
+ surfaceBlock = BLOCK_TYPES.SNOW;
234
+ subBlock = BLOCK_TYPES.STONE;
235
+ heightScale = 35;
236
+ heightBase = 30;
237
+ treeChance = 0.02;
238
+ } else {
239
+ // ── Plains ── gentle rolling grassland
240
+ surfaceBlock = BLOCK_TYPES.GRASS;
241
+ subBlock = BLOCK_TYPES.DIRT;
242
+ heightScale = 6;
243
+ heightBase = 32;
244
+ treeChance = 0.015;
245
+ }
246
+
247
+ const height = Math.floor(perlinNoise(worldX, worldZ, 4, 0.5) * heightScale + heightBase);
248
+
249
+ for (let y = 0; y < CHUNK_HEIGHT; y++) {
250
+ if (y === 0) {
251
+ chunk.setBlock(x, y, z, BLOCK_TYPES.BEDROCK);
252
+ } else if (y < height - 3) {
253
+ chunk.setBlock(x, y, z, BLOCK_TYPES.STONE);
254
+ } else if (y < height - 1) {
255
+ chunk.setBlock(x, y, z, subBlock);
256
+ } else if (y === height - 1) {
257
+ chunk.setBlock(x, y, z, surfaceBlock);
258
+ } else if (y < waterLevel && y >= height) {
259
+ chunk.setBlock(x, y, z, BLOCK_TYPES.WATER);
260
+ }
261
+
262
+ // Cave generation
263
+ if (y > 0 && y < height - 5) {
264
+ const cave = perlinNoise(worldX * 0.5, y * 0.5, worldZ * 0.5, 3, 0.5);
265
+ if (cave > 0.6) {
266
+ chunk.setBlock(x, y, z, BLOCK_TYPES.AIR);
267
+ }
268
+ }
269
+ }
270
+
271
+ // Random tree placement (seeded by world position)
272
+ if (height > waterLevel && treeChance > 0) {
273
+ const treeSeed = Math.sin(worldX * 12.9898 + worldZ * 78.233) * 43758.5453;
274
+ const treeRoll = treeSeed - Math.floor(treeSeed);
275
+ if (treeRoll < treeChance && x > 2 && x < CHUNK_SIZE - 3 && z > 2 && z < CHUNK_SIZE - 3) {
276
+ pendingTrees.push({ x, y: height, z });
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ // Place trees after terrain is fully generated
283
+ for (const t of pendingTrees) {
284
+ const trunkHeight = 4 + Math.floor(Math.abs(Math.sin(t.x * 7 + t.z * 13)) * 3);
285
+ for (let i = 0; i < trunkHeight; i++) {
286
+ chunk.setBlock(t.x, t.y + i, t.z, BLOCK_TYPES.WOOD);
287
+ }
288
+ const leafY = t.y + trunkHeight;
289
+ for (let dx = -2; dx <= 2; dx++) {
290
+ for (let dy = -2; dy <= 2; dy++) {
291
+ for (let dz = -2; dz <= 2; dz++) {
292
+ if (Math.abs(dx) + Math.abs(dy) + Math.abs(dz) < 4) {
293
+ const lx = t.x + dx,
294
+ ly = leafY + dy,
295
+ lz = t.z + dz;
296
+ if (lx >= 0 && lx < CHUNK_SIZE && lz >= 0 && lz < CHUNK_SIZE && ly < CHUNK_HEIGHT) {
297
+ chunk.setBlock(lx, ly, lz, BLOCK_TYPES.LEAVES);
298
+ }
299
+ }
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ // Greedy meshing algorithm for efficient rendering
307
+ function createChunkMesh(chunk) {
308
+ const geometry = new THREE.BufferGeometry();
309
+ const vertices = [];
310
+ const normals = [];
311
+ const colors = [];
312
+ const uvs = [];
313
+ const indices = [];
314
+ let vertexCount = 0;
315
+
316
+ // Face directions
317
+ const dirs = [
318
+ [0, 0, 1], // Front
319
+ [0, 0, -1], // Back
320
+ [1, 0, 0], // Right
321
+ [-1, 0, 0], // Left
322
+ [0, 1, 0], // Top
323
+ [0, -1, 0], // Bottom
324
+ ];
325
+
326
+ const dirNormals = [
327
+ [0, 0, 1],
328
+ [0, 0, -1],
329
+ [1, 0, 0],
330
+ [-1, 0, 0],
331
+ [0, 1, 0],
332
+ [0, -1, 0],
333
+ ];
334
+
335
+ // Check if block face should be rendered
336
+ function shouldRenderFace(x, y, z, dir) {
337
+ const nx = x + dir[0];
338
+ const ny = y + dir[1];
339
+ const nz = z + dir[2];
340
+
341
+ // Check neighbor in same chunk
342
+ if (
343
+ nx >= 0 &&
344
+ nx < CHUNK_SIZE &&
345
+ ny >= 0 &&
346
+ ny < CHUNK_HEIGHT &&
347
+ nz >= 0 &&
348
+ nz < CHUNK_SIZE
349
+ ) {
350
+ const neighbor = chunk.getBlock(nx, ny, nz);
351
+ return neighbor === BLOCK_TYPES.AIR || neighbor === BLOCK_TYPES.WATER;
352
+ }
353
+
354
+ // Check neighbor in adjacent chunk (only if it exists - don't create it!)
355
+ if (nx < 0 || nx >= CHUNK_SIZE || nz < 0 || nz >= CHUNK_SIZE) {
356
+ const neighborChunkX = chunk.chunkX + Math.floor(nx / CHUNK_SIZE);
357
+ const neighborChunkZ = chunk.chunkZ + Math.floor(nz / CHUNK_SIZE);
358
+ const neighborChunk = getChunkIfExists(neighborChunkX, neighborChunkZ);
359
+
360
+ // If neighbor chunk doesn't exist yet, assume it's air (render the face)
361
+ if (!neighborChunk) {
362
+ return true;
363
+ }
364
+
365
+ const localX = ((nx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
366
+ const localZ = ((nz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
367
+ const neighbor = neighborChunk.getBlock(localX, ny, localZ);
368
+ return neighbor === BLOCK_TYPES.AIR || neighbor === BLOCK_TYPES.WATER;
369
+ }
370
+
371
+ return ny < 0 || ny >= CHUNK_HEIGHT;
372
+ }
373
+
374
+ // Add face to mesh
375
+ function addFace(x, y, z, dir, dirIndex, blockType) {
376
+ const color = new THREE.Color(BLOCK_COLORS[blockType] || 0xffffff);
377
+
378
+ // Ambient occlusion factor based on face direction
379
+ const aoFactors = [0.9, 0.9, 0.85, 0.85, 1.0, 0.7];
380
+ const ao = aoFactors[dirIndex];
381
+ color.multiplyScalar(ao);
382
+
383
+ const normal = dirNormals[dirIndex];
384
+
385
+ // Define face vertices based on direction
386
+ let faceVertices;
387
+ if (dirIndex === 0) {
388
+ // Front (+Z)
389
+ faceVertices = [
390
+ [x, y, z + 1],
391
+ [x + 1, y, z + 1],
392
+ [x + 1, y + 1, z + 1],
393
+ [x, y + 1, z + 1],
394
+ ];
395
+ } else if (dirIndex === 1) {
396
+ // Back (-Z)
397
+ faceVertices = [
398
+ [x + 1, y, z],
399
+ [x, y, z],
400
+ [x, y + 1, z],
401
+ [x + 1, y + 1, z],
402
+ ];
403
+ } else if (dirIndex === 2) {
404
+ // Right (+X)
405
+ faceVertices = [
406
+ [x + 1, y, z + 1],
407
+ [x + 1, y, z],
408
+ [x + 1, y + 1, z],
409
+ [x + 1, y + 1, z + 1],
410
+ ];
411
+ } else if (dirIndex === 3) {
412
+ // Left (-X)
413
+ faceVertices = [
414
+ [x, y, z],
415
+ [x, y, z + 1],
416
+ [x, y + 1, z + 1],
417
+ [x, y + 1, z],
418
+ ];
419
+ } else if (dirIndex === 4) {
420
+ // Top (+Y)
421
+ faceVertices = [
422
+ [x, y + 1, z + 1],
423
+ [x + 1, y + 1, z + 1],
424
+ [x + 1, y + 1, z],
425
+ [x, y + 1, z],
426
+ ];
427
+ } else {
428
+ // Bottom (-Y)
429
+ faceVertices = [
430
+ [x, y, z],
431
+ [x + 1, y, z],
432
+ [x + 1, y, z + 1],
433
+ [x, y, z + 1],
434
+ ];
435
+ }
436
+
437
+ // Add vertices
438
+ const baseX = chunk.chunkX * CHUNK_SIZE;
439
+ const baseZ = chunk.chunkZ * CHUNK_SIZE;
440
+
441
+ for (let i = 0; i < 4; i++) {
442
+ vertices.push(faceVertices[i][0] + baseX, faceVertices[i][1], faceVertices[i][2] + baseZ);
443
+ normals.push(normal[0], normal[1], normal[2]);
444
+ colors.push(color.r, color.g, color.b);
445
+ }
446
+ // Add standard UV mapping
447
+ uvs.push(0, 1, 1, 1, 1, 0, 0, 0);
448
+
449
+ // Add indices (two triangles per face)
450
+ const offset = vertexCount;
451
+ indices.push(offset, offset + 1, offset + 2);
452
+ indices.push(offset, offset + 2, offset + 3);
453
+ vertexCount += 4;
454
+ }
455
+
456
+ // Iterate through all blocks
457
+ for (let y = 0; y < CHUNK_HEIGHT; y++) {
458
+ for (let z = 0; z < CHUNK_SIZE; z++) {
459
+ for (let x = 0; x < CHUNK_SIZE; x++) {
460
+ const blockType = chunk.getBlock(x, y, z);
461
+
462
+ if (blockType === BLOCK_TYPES.AIR) continue;
463
+
464
+ // Check each face
465
+ for (let d = 0; d < 6; d++) {
466
+ if (shouldRenderFace(x, y, z, dirs[d])) {
467
+ addFace(x, y, z, dirs[d], d, blockType);
468
+ }
469
+ }
470
+ }
471
+ }
472
+ }
473
+
474
+ // Build geometry
475
+ if (vertices.length > 0) {
476
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
477
+ geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
478
+ geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
479
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
480
+ geometry.setIndex(indices);
481
+ geometry.computeBoundingSphere();
482
+ }
483
+
484
+ return geometry;
485
+ }
486
+
487
+ // Update chunk mesh if dirty
488
+ function updateChunkMesh(chunk) {
489
+ if (!chunk.dirty) return;
490
+
491
+ const key = `${chunk.chunkX},${chunk.chunkZ}`;
492
+
493
+ // Remove old mesh
494
+ if (chunkMeshes.has(key)) {
495
+ const oldMesh = chunkMeshes.get(key);
496
+ gpu.scene.remove(oldMesh);
497
+ oldMesh.geometry.dispose();
498
+ chunkMeshes.delete(key);
499
+ }
500
+
501
+ // Create new mesh
502
+ const geometry = createChunkMesh(chunk);
503
+ if (geometry.attributes.position) {
504
+ const material =
505
+ window.VOXEL_MATERIAL ||
506
+ new THREE.MeshStandardMaterial({
507
+ vertexColors: true,
508
+ flatShading: true,
509
+ roughness: 0.8,
510
+ metalness: 0.1,
511
+ });
512
+
513
+ const mesh = new THREE.Mesh(geometry, material);
514
+ mesh.castShadow = true;
515
+ mesh.receiveShadow = true;
516
+
517
+ gpu.scene.add(mesh);
518
+ chunkMeshes.set(key, mesh);
519
+ }
520
+
521
+ chunk.dirty = false;
522
+ }
523
+
524
+ // World coordinates to chunk coordinates
525
+ function worldToChunk(x, z) {
526
+ return {
527
+ chunkX: Math.floor(x / CHUNK_SIZE),
528
+ chunkZ: Math.floor(z / CHUNK_SIZE),
529
+ localX: ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
530
+ localZ: ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
531
+ };
532
+ }
533
+
534
+ // Get block at world position
535
+ function getBlock(x, y, z) {
536
+ if (y < 0 || y >= CHUNK_HEIGHT) return BLOCK_TYPES.AIR;
537
+
538
+ const { chunkX, chunkZ, localX, localZ } = worldToChunk(x, z);
539
+ const chunk = getChunk(chunkX, chunkZ);
540
+ return chunk.getBlock(localX, y, localZ);
541
+ }
542
+
543
+ // Set block at world position
544
+ function setBlock(x, y, z, blockType) {
545
+ if (y < 0 || y >= CHUNK_HEIGHT) return;
546
+
547
+ const { chunkX, chunkZ, localX, localZ } = worldToChunk(x, z);
548
+ const chunk = getChunk(chunkX, chunkZ);
549
+ chunk.setBlock(localX, y, localZ, blockType);
550
+
551
+ // Mark adjacent chunks as dirty if on boundary
552
+ if (localX === 0) getChunk(chunkX - 1, chunkZ).dirty = true;
553
+ if (localX === CHUNK_SIZE - 1) getChunk(chunkX + 1, chunkZ).dirty = true;
554
+ if (localZ === 0) getChunk(chunkX, chunkZ - 1).dirty = true;
555
+ if (localZ === CHUNK_SIZE - 1) getChunk(chunkX, chunkZ + 1).dirty = true;
556
+ }
557
+
558
+ // Update visible chunks around player
559
+ function updateChunks(playerX, playerZ) {
560
+ const centerChunkX = Math.floor(playerX / CHUNK_SIZE);
561
+ const centerChunkZ = Math.floor(playerZ / CHUNK_SIZE);
562
+
563
+ // Load chunks in render distance
564
+ for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) {
565
+ for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) {
566
+ const chunkX = centerChunkX + dx;
567
+ const chunkZ = centerChunkZ + dz;
568
+ const chunk = getChunk(chunkX, chunkZ);
569
+ updateChunkMesh(chunk);
570
+ }
571
+ }
572
+
573
+ // Unload far chunks (optional, for memory management)
574
+ const keysToRemove = [];
575
+ for (const [key, mesh] of chunkMeshes.entries()) {
576
+ const [chunkX, chunkZ] = key.split(',').map(Number);
577
+ const dx = Math.abs(chunkX - centerChunkX);
578
+ const dz = Math.abs(chunkZ - centerChunkZ);
579
+
580
+ if (dx > RENDER_DISTANCE + 1 || dz > RENDER_DISTANCE + 1) {
581
+ gpu.scene.remove(mesh);
582
+ mesh.geometry.dispose();
583
+ mesh.material.dispose();
584
+ keysToRemove.push(key);
585
+ chunks.delete(key);
586
+ }
587
+ }
588
+
589
+ keysToRemove.forEach(key => chunkMeshes.delete(key));
590
+ }
591
+
592
+ // Reset entire world (clear all chunks and meshes)
593
+ function resetWorld() {
594
+ for (const mesh of chunkMeshes.values()) {
595
+ gpu.scene.remove(mesh);
596
+ mesh.geometry.dispose();
597
+ mesh.material.dispose();
598
+ }
599
+ chunkMeshes.clear();
600
+ chunks.clear();
601
+ worldSeed += 5000 + Math.floor(Math.random() * 10000);
602
+ }
603
+
604
+ // Raycast to find block player is looking at
605
+ function raycastBlock(origin, direction, maxDistance = 10) {
606
+ const step = 0.1;
607
+ const pos = { x: origin[0], y: origin[1], z: origin[2] };
608
+ const dir = { x: direction[0], y: direction[1], z: direction[2] };
609
+
610
+ for (let i = 0; i < maxDistance / step; i++) {
611
+ pos.x += dir.x * step;
612
+ pos.y += dir.y * step;
613
+ pos.z += dir.z * step;
614
+
615
+ const blockX = Math.floor(pos.x);
616
+ const blockY = Math.floor(pos.y);
617
+ const blockZ = Math.floor(pos.z);
618
+
619
+ const blockType = getBlock(blockX, blockY, blockZ);
620
+
621
+ if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.WATER) {
622
+ return {
623
+ hit: true,
624
+ position: [blockX, blockY, blockZ],
625
+ blockType: blockType,
626
+ distance: i * step,
627
+ };
628
+ }
629
+ }
630
+
631
+ return { hit: false };
632
+ }
633
+
634
+ // Check collision with voxel world
635
+ function checkCollision(pos, size) {
636
+ const minX = Math.floor(pos[0] - size);
637
+ const maxX = Math.floor(pos[0] + size);
638
+ const minY = Math.floor(pos[1]);
639
+ const maxY = Math.floor(pos[1] + size * 2);
640
+ const minZ = Math.floor(pos[2] - size);
641
+ const maxZ = Math.floor(pos[2] + size);
642
+
643
+ for (let x = minX; x <= maxX; x++) {
644
+ for (let y = minY; y <= maxY; y++) {
645
+ for (let z = minZ; z <= maxZ; z++) {
646
+ const blockType = getBlock(x, y, z);
647
+ if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.WATER) {
648
+ return true;
649
+ }
650
+ }
651
+ }
652
+ }
653
+
654
+ return false;
655
+ }
656
+
657
+ // Generate tree structure
658
+ function placeTree(x, y, z) {
659
+ const trunkHeight = 4 + Math.floor(Math.random() * 3);
660
+
661
+ // Trunk
662
+ for (let i = 0; i < trunkHeight; i++) {
663
+ setBlock(x, y + i, z, BLOCK_TYPES.WOOD);
664
+ }
665
+
666
+ // Leaves
667
+ const leafY = y + trunkHeight;
668
+ for (let dx = -2; dx <= 2; dx++) {
669
+ for (let dy = -2; dy <= 2; dy++) {
670
+ for (let dz = -2; dz <= 2; dz++) {
671
+ if (Math.abs(dx) + Math.abs(dy) + Math.abs(dz) < 4) {
672
+ setBlock(x + dx, leafY + dy, z + dz, BLOCK_TYPES.LEAVES);
673
+ }
674
+ }
675
+ }
676
+ }
677
+ }
678
+
679
+ // Public API
680
+ return {
681
+ BLOCK_TYPES,
682
+ CHUNK_SIZE,
683
+ CHUNK_HEIGHT,
684
+
685
+ // World management
686
+ updateChunks,
687
+ getBlock,
688
+ setBlock,
689
+
690
+ // Block interaction
691
+ raycastBlock,
692
+ checkCollision,
693
+
694
+ // Structures
695
+ placeTree,
696
+
697
+ // World reset
698
+ resetWorld,
699
+
700
+ // Expose to global game context
701
+ exposeTo: function (g) {
702
+ g.BLOCK_TYPES = BLOCK_TYPES;
703
+ g.updateVoxelWorld = updateChunks;
704
+ g.getVoxelBlock = getBlock;
705
+ g.setVoxelBlock = setBlock;
706
+ g.raycastVoxelBlock = raycastBlock;
707
+ g.checkVoxelCollision = checkCollision;
708
+ g.placeVoxelTree = placeTree;
709
+ g.resetVoxelWorld = resetWorld;
710
+ },
711
+ };
712
+ }