nova64 0.2.1

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +786 -0
  3. package/index.html +651 -0
  4. package/package.json +255 -0
  5. package/public/os9-shell/assets/index-B1Uvacma.js +32825 -0
  6. package/public/os9-shell/assets/index-B1Uvacma.js.map +1 -0
  7. package/public/os9-shell/assets/index-DIHfrTaW.css +1 -0
  8. package/public/os9-shell/index.html +14 -0
  9. package/public/os9-shell/nova-icon.svg +12 -0
  10. package/runtime/api-2d.js +878 -0
  11. package/runtime/api-3d/camera.js +73 -0
  12. package/runtime/api-3d/instancing.js +180 -0
  13. package/runtime/api-3d/lights.js +51 -0
  14. package/runtime/api-3d/materials.js +47 -0
  15. package/runtime/api-3d/models.js +84 -0
  16. package/runtime/api-3d/pbr.js +69 -0
  17. package/runtime/api-3d/primitives.js +304 -0
  18. package/runtime/api-3d/scene.js +169 -0
  19. package/runtime/api-3d/transforms.js +161 -0
  20. package/runtime/api-3d.js +154 -0
  21. package/runtime/api-effects.js +753 -0
  22. package/runtime/api-presets.js +85 -0
  23. package/runtime/api-skybox.js +178 -0
  24. package/runtime/api-sprites.js +100 -0
  25. package/runtime/api-voxel.js +601 -0
  26. package/runtime/api.js +201 -0
  27. package/runtime/assets.js +27 -0
  28. package/runtime/audio.js +114 -0
  29. package/runtime/collision.js +47 -0
  30. package/runtime/console.js +101 -0
  31. package/runtime/editor.js +233 -0
  32. package/runtime/font.js +233 -0
  33. package/runtime/framebuffer.js +28 -0
  34. package/runtime/fullscreen-button.js +185 -0
  35. package/runtime/gpu-canvas2d.js +47 -0
  36. package/runtime/gpu-threejs.js +639 -0
  37. package/runtime/gpu-webgl2.js +310 -0
  38. package/runtime/index.js +22 -0
  39. package/runtime/input.js +225 -0
  40. package/runtime/logger.js +60 -0
  41. package/runtime/physics.js +101 -0
  42. package/runtime/screens.js +213 -0
  43. package/runtime/storage.js +38 -0
  44. package/runtime/store.js +151 -0
  45. package/runtime/textinput.js +68 -0
  46. package/runtime/ui/buttons.js +124 -0
  47. package/runtime/ui/panels.js +105 -0
  48. package/runtime/ui/text.js +86 -0
  49. package/runtime/ui/widgets.js +141 -0
  50. package/runtime/ui.js +111 -0
  51. package/src/main.js +474 -0
  52. package/vite.config.js +63 -0
@@ -0,0 +1,601 @@
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)
41
+ const BLOCK_COLORS = {
42
+ [BLOCK_TYPES.GRASS]: 0x44aa22,
43
+ [BLOCK_TYPES.DIRT]: 0x885533,
44
+ [BLOCK_TYPES.STONE]: 0x999999,
45
+ [BLOCK_TYPES.SAND]: 0xffdd88,
46
+ [BLOCK_TYPES.WATER]: 0x33aaff,
47
+ [BLOCK_TYPES.WOOD]: 0x774422,
48
+ [BLOCK_TYPES.LEAVES]: 0x228822,
49
+ [BLOCK_TYPES.COBBLESTONE]: 0x888888,
50
+ [BLOCK_TYPES.PLANKS]: 0xddaa55,
51
+ [BLOCK_TYPES.GLASS]: 0xccffff,
52
+ [BLOCK_TYPES.BRICK]: 0xcc4433,
53
+ [BLOCK_TYPES.SNOW]: 0xffffff,
54
+ [BLOCK_TYPES.ICE]: 0xbbffff,
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
+
62
+ // Noise function for terrain generation (simple Perlin-like)
63
+ function noise2D(x, z) {
64
+ const n = Math.sin(x * 12.9898 + z * 78.233) * 43758.5453;
65
+ return n - Math.floor(n);
66
+ }
67
+
68
+ function smoothNoise(x, z) {
69
+ const corners =
70
+ (noise2D(x - 1, z - 1) +
71
+ noise2D(x + 1, z - 1) +
72
+ noise2D(x - 1, z + 1) +
73
+ noise2D(x + 1, z + 1)) /
74
+ 16;
75
+ const sides =
76
+ (noise2D(x - 1, z) + noise2D(x + 1, z) + noise2D(x, z - 1) + noise2D(x, z + 1)) / 8;
77
+ const center = noise2D(x, z) / 4;
78
+ return corners + sides + center;
79
+ }
80
+
81
+ function interpolatedNoise(x, z) {
82
+ const intX = Math.floor(x);
83
+ const fracX = x - intX;
84
+ const intZ = Math.floor(z);
85
+ const fracZ = z - intZ;
86
+
87
+ const v1 = smoothNoise(intX, intZ);
88
+ const v2 = smoothNoise(intX + 1, intZ);
89
+ const v3 = smoothNoise(intX, intZ + 1);
90
+ const v4 = smoothNoise(intX + 1, intZ + 1);
91
+
92
+ const i1 = v1 * (1 - fracX) + v2 * fracX;
93
+ const i2 = v3 * (1 - fracX) + v4 * fracX;
94
+
95
+ return i1 * (1 - fracZ) + i2 * fracZ;
96
+ }
97
+
98
+ function perlinNoise(x, z, octaves = 4, persistence = 0.5) {
99
+ let total = 0;
100
+ let frequency = 1;
101
+ let amplitude = 1;
102
+ let maxValue = 0;
103
+
104
+ for (let i = 0; i < octaves; i++) {
105
+ total += interpolatedNoise(x * frequency * 0.01, z * frequency * 0.01) * amplitude;
106
+ maxValue += amplitude;
107
+ amplitude *= persistence;
108
+ frequency *= 2;
109
+ }
110
+
111
+ return total / maxValue;
112
+ }
113
+
114
+ // Chunk class
115
+ class Chunk {
116
+ constructor(chunkX, chunkZ) {
117
+ this.chunkX = chunkX;
118
+ this.chunkZ = chunkZ;
119
+ this.blocks = new Uint8Array(CHUNK_SIZE * CHUNK_HEIGHT * CHUNK_SIZE);
120
+ this.dirty = true;
121
+ }
122
+
123
+ getBlock(x, y, z) {
124
+ if (x < 0 || x >= CHUNK_SIZE || y < 0 || y >= CHUNK_HEIGHT || z < 0 || z >= CHUNK_SIZE) {
125
+ return BLOCK_TYPES.AIR;
126
+ }
127
+ const index = x + z * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE;
128
+ return this.blocks[index];
129
+ }
130
+
131
+ setBlock(x, y, z, blockType) {
132
+ if (x < 0 || x >= CHUNK_SIZE || y < 0 || y >= CHUNK_HEIGHT || z < 0 || z >= CHUNK_SIZE) {
133
+ return;
134
+ }
135
+ const index = x + z * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE;
136
+ this.blocks[index] = blockType;
137
+ this.dirty = true;
138
+ }
139
+ }
140
+
141
+ // Get or create chunk
142
+ function getChunk(chunkX, chunkZ) {
143
+ const key = `${chunkX},${chunkZ}`;
144
+ if (!chunks.has(key)) {
145
+ const chunk = new Chunk(chunkX, chunkZ);
146
+ generateChunkTerrain(chunk);
147
+ chunks.set(key, chunk);
148
+ }
149
+ return chunks.get(key);
150
+ }
151
+
152
+ // Get chunk without creating it (returns null if doesn't exist)
153
+ function getChunkIfExists(chunkX, chunkZ) {
154
+ const key = `${chunkX},${chunkZ}`;
155
+ return chunks.get(key) || null;
156
+ }
157
+
158
+ // Terrain generation
159
+ function generateChunkTerrain(chunk) {
160
+ const baseX = chunk.chunkX * CHUNK_SIZE;
161
+ const baseZ = chunk.chunkZ * CHUNK_SIZE;
162
+
163
+ for (let x = 0; x < CHUNK_SIZE; x++) {
164
+ for (let z = 0; z < CHUNK_SIZE; z++) {
165
+ const worldX = baseX + x;
166
+ const worldZ = baseZ + z;
167
+
168
+ // Generate height using Perlin noise
169
+ const height = Math.floor(perlinNoise(worldX, worldZ, 4, 0.5) * 20 + 32);
170
+
171
+ // Biome selection
172
+ const temperature = perlinNoise(worldX * 0.5, worldZ * 0.5, 2, 0.5);
173
+ const moisture = perlinNoise(worldX * 0.3 + 1000, worldZ * 0.3 + 1000, 2, 0.5);
174
+
175
+ for (let y = 0; y < CHUNK_HEIGHT; y++) {
176
+ if (y === 0) {
177
+ // Bedrock layer
178
+ chunk.setBlock(x, y, z, BLOCK_TYPES.BEDROCK);
179
+ } else if (y < height - 3) {
180
+ // Stone layer
181
+ chunk.setBlock(x, y, z, BLOCK_TYPES.STONE);
182
+ } else if (y < height - 1) {
183
+ // Dirt layer
184
+ chunk.setBlock(x, y, z, BLOCK_TYPES.DIRT);
185
+ } else if (y === height - 1) {
186
+ // Top layer based on biome
187
+ if (temperature < 0.3) {
188
+ chunk.setBlock(x, y, z, BLOCK_TYPES.SNOW);
189
+ } else if (moisture < 0.3) {
190
+ chunk.setBlock(x, y, z, BLOCK_TYPES.SAND);
191
+ } else {
192
+ chunk.setBlock(x, y, z, BLOCK_TYPES.GRASS);
193
+ }
194
+ } else if (y < 30 && y >= height) {
195
+ // Water level
196
+ chunk.setBlock(x, y, z, BLOCK_TYPES.WATER);
197
+ }
198
+
199
+ // Cave generation
200
+ if (y > 0 && y < height - 5) {
201
+ const cave = perlinNoise(worldX * 0.5, y * 0.5, worldZ * 0.5, 3, 0.5);
202
+ if (cave > 0.6) {
203
+ chunk.setBlock(x, y, z, BLOCK_TYPES.AIR);
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ // Greedy meshing algorithm for efficient rendering
212
+ function createChunkMesh(chunk) {
213
+ const geometry = new THREE.BufferGeometry();
214
+ const vertices = [];
215
+ const normals = [];
216
+ const colors = [];
217
+ const uvs = [];
218
+ const indices = [];
219
+ let vertexCount = 0;
220
+
221
+ // Face directions
222
+ const dirs = [
223
+ [0, 0, 1], // Front
224
+ [0, 0, -1], // Back
225
+ [1, 0, 0], // Right
226
+ [-1, 0, 0], // Left
227
+ [0, 1, 0], // Top
228
+ [0, -1, 0], // Bottom
229
+ ];
230
+
231
+ const dirNormals = [
232
+ [0, 0, 1],
233
+ [0, 0, -1],
234
+ [1, 0, 0],
235
+ [-1, 0, 0],
236
+ [0, 1, 0],
237
+ [0, -1, 0],
238
+ ];
239
+
240
+ // Check if block face should be rendered
241
+ function shouldRenderFace(x, y, z, dir) {
242
+ const nx = x + dir[0];
243
+ const ny = y + dir[1];
244
+ const nz = z + dir[2];
245
+
246
+ // Check neighbor in same chunk
247
+ if (
248
+ nx >= 0 &&
249
+ nx < CHUNK_SIZE &&
250
+ ny >= 0 &&
251
+ ny < CHUNK_HEIGHT &&
252
+ nz >= 0 &&
253
+ nz < CHUNK_SIZE
254
+ ) {
255
+ const neighbor = chunk.getBlock(nx, ny, nz);
256
+ return neighbor === BLOCK_TYPES.AIR || neighbor === BLOCK_TYPES.WATER;
257
+ }
258
+
259
+ // Check neighbor in adjacent chunk (only if it exists - don't create it!)
260
+ if (nx < 0 || nx >= CHUNK_SIZE || nz < 0 || nz >= CHUNK_SIZE) {
261
+ const neighborChunkX = chunk.chunkX + Math.floor(nx / CHUNK_SIZE);
262
+ const neighborChunkZ = chunk.chunkZ + Math.floor(nz / CHUNK_SIZE);
263
+ const neighborChunk = getChunkIfExists(neighborChunkX, neighborChunkZ);
264
+
265
+ // If neighbor chunk doesn't exist yet, assume it's air (render the face)
266
+ if (!neighborChunk) {
267
+ return true;
268
+ }
269
+
270
+ const localX = ((nx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
271
+ const localZ = ((nz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
272
+ const neighbor = neighborChunk.getBlock(localX, ny, localZ);
273
+ return neighbor === BLOCK_TYPES.AIR || neighbor === BLOCK_TYPES.WATER;
274
+ }
275
+
276
+ return ny < 0 || ny >= CHUNK_HEIGHT;
277
+ }
278
+
279
+ // Add face to mesh
280
+ function addFace(x, y, z, dir, dirIndex, blockType) {
281
+ const color = new THREE.Color(BLOCK_COLORS[blockType] || 0xffffff);
282
+
283
+ // Ambient occlusion factor based on face direction
284
+ const aoFactors = [0.9, 0.9, 0.85, 0.85, 1.0, 0.7];
285
+ const ao = aoFactors[dirIndex];
286
+ color.multiplyScalar(ao);
287
+
288
+ const normal = dirNormals[dirIndex];
289
+
290
+ // Define face vertices based on direction
291
+ let faceVertices;
292
+ if (dirIndex === 0) {
293
+ // Front (+Z)
294
+ faceVertices = [
295
+ [x, y, z + 1],
296
+ [x + 1, y, z + 1],
297
+ [x + 1, y + 1, z + 1],
298
+ [x, y + 1, z + 1],
299
+ ];
300
+ } else if (dirIndex === 1) {
301
+ // Back (-Z)
302
+ faceVertices = [
303
+ [x + 1, y, z],
304
+ [x, y, z],
305
+ [x, y + 1, z],
306
+ [x + 1, y + 1, z],
307
+ ];
308
+ } else if (dirIndex === 2) {
309
+ // Right (+X)
310
+ faceVertices = [
311
+ [x + 1, y, z + 1],
312
+ [x + 1, y, z],
313
+ [x + 1, y + 1, z],
314
+ [x + 1, y + 1, z + 1],
315
+ ];
316
+ } else if (dirIndex === 3) {
317
+ // Left (-X)
318
+ faceVertices = [
319
+ [x, y, z],
320
+ [x, y, z + 1],
321
+ [x, y + 1, z + 1],
322
+ [x, y + 1, z],
323
+ ];
324
+ } else if (dirIndex === 4) {
325
+ // Top (+Y)
326
+ faceVertices = [
327
+ [x, y + 1, z + 1],
328
+ [x + 1, y + 1, z + 1],
329
+ [x + 1, y + 1, z],
330
+ [x, y + 1, z],
331
+ ];
332
+ } else {
333
+ // Bottom (-Y)
334
+ faceVertices = [
335
+ [x, y, z],
336
+ [x + 1, y, z],
337
+ [x + 1, y, z + 1],
338
+ [x, y, z + 1],
339
+ ];
340
+ }
341
+
342
+ // Add vertices
343
+ const baseX = chunk.chunkX * CHUNK_SIZE;
344
+ const baseZ = chunk.chunkZ * CHUNK_SIZE;
345
+
346
+ for (let i = 0; i < 4; i++) {
347
+ vertices.push(faceVertices[i][0] + baseX, faceVertices[i][1], faceVertices[i][2] + baseZ);
348
+ normals.push(normal[0], normal[1], normal[2]);
349
+ colors.push(color.r, color.g, color.b);
350
+ }
351
+ // Add standard UV mapping
352
+ uvs.push(0, 1, 1, 1, 1, 0, 0, 0);
353
+
354
+ // Add indices (two triangles per face)
355
+ const offset = vertexCount;
356
+ indices.push(offset, offset + 1, offset + 2);
357
+ indices.push(offset, offset + 2, offset + 3);
358
+ vertexCount += 4;
359
+ }
360
+
361
+ // Iterate through all blocks
362
+ for (let y = 0; y < CHUNK_HEIGHT; y++) {
363
+ for (let z = 0; z < CHUNK_SIZE; z++) {
364
+ for (let x = 0; x < CHUNK_SIZE; x++) {
365
+ const blockType = chunk.getBlock(x, y, z);
366
+
367
+ if (blockType === BLOCK_TYPES.AIR) continue;
368
+
369
+ // Check each face
370
+ for (let d = 0; d < 6; d++) {
371
+ if (shouldRenderFace(x, y, z, dirs[d])) {
372
+ addFace(x, y, z, dirs[d], d, blockType);
373
+ }
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ // Build geometry
380
+ if (vertices.length > 0) {
381
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
382
+ geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
383
+ geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
384
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
385
+ geometry.setIndex(indices);
386
+ geometry.computeBoundingSphere();
387
+ }
388
+
389
+ return geometry;
390
+ }
391
+
392
+ // Update chunk mesh if dirty
393
+ function updateChunkMesh(chunk) {
394
+ if (!chunk.dirty) return;
395
+
396
+ const key = `${chunk.chunkX},${chunk.chunkZ}`;
397
+
398
+ // Remove old mesh
399
+ if (chunkMeshes.has(key)) {
400
+ const oldMesh = chunkMeshes.get(key);
401
+ gpu.scene.remove(oldMesh);
402
+ oldMesh.geometry.dispose();
403
+ chunkMeshes.delete(key);
404
+ }
405
+
406
+ // Create new mesh
407
+ const geometry = createChunkMesh(chunk);
408
+ if (geometry.attributes.position) {
409
+ const material =
410
+ window.VOXEL_MATERIAL ||
411
+ new THREE.MeshStandardMaterial({
412
+ vertexColors: true,
413
+ flatShading: true,
414
+ roughness: 0.8,
415
+ metalness: 0.1,
416
+ });
417
+
418
+ const mesh = new THREE.Mesh(geometry, material);
419
+ mesh.castShadow = true;
420
+ mesh.receiveShadow = true;
421
+
422
+ gpu.scene.add(mesh);
423
+ chunkMeshes.set(key, mesh);
424
+ }
425
+
426
+ chunk.dirty = false;
427
+ }
428
+
429
+ // World coordinates to chunk coordinates
430
+ function worldToChunk(x, z) {
431
+ return {
432
+ chunkX: Math.floor(x / CHUNK_SIZE),
433
+ chunkZ: Math.floor(z / CHUNK_SIZE),
434
+ localX: ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
435
+ localZ: ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
436
+ };
437
+ }
438
+
439
+ // Get block at world position
440
+ function getBlock(x, y, z) {
441
+ if (y < 0 || y >= CHUNK_HEIGHT) return BLOCK_TYPES.AIR;
442
+
443
+ const { chunkX, chunkZ, localX, localZ } = worldToChunk(x, z);
444
+ const chunk = getChunk(chunkX, chunkZ);
445
+ return chunk.getBlock(localX, y, localZ);
446
+ }
447
+
448
+ // Set block at world position
449
+ function setBlock(x, y, z, blockType) {
450
+ if (y < 0 || y >= CHUNK_HEIGHT) return;
451
+
452
+ const { chunkX, chunkZ, localX, localZ } = worldToChunk(x, z);
453
+ const chunk = getChunk(chunkX, chunkZ);
454
+ chunk.setBlock(localX, y, localZ, blockType);
455
+
456
+ // Mark adjacent chunks as dirty if on boundary
457
+ if (localX === 0) getChunk(chunkX - 1, chunkZ).dirty = true;
458
+ if (localX === CHUNK_SIZE - 1) getChunk(chunkX + 1, chunkZ).dirty = true;
459
+ if (localZ === 0) getChunk(chunkX, chunkZ - 1).dirty = true;
460
+ if (localZ === CHUNK_SIZE - 1) getChunk(chunkX, chunkZ + 1).dirty = true;
461
+ }
462
+
463
+ // Update visible chunks around player
464
+ function updateChunks(playerX, playerZ) {
465
+ const centerChunkX = Math.floor(playerX / CHUNK_SIZE);
466
+ const centerChunkZ = Math.floor(playerZ / CHUNK_SIZE);
467
+
468
+ // Load chunks in render distance
469
+ for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) {
470
+ for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) {
471
+ const chunkX = centerChunkX + dx;
472
+ const chunkZ = centerChunkZ + dz;
473
+ const chunk = getChunk(chunkX, chunkZ);
474
+ updateChunkMesh(chunk);
475
+ }
476
+ }
477
+
478
+ // Unload far chunks (optional, for memory management)
479
+ const keysToRemove = [];
480
+ for (const [key, mesh] of chunkMeshes.entries()) {
481
+ const [chunkX, chunkZ] = key.split(',').map(Number);
482
+ const dx = Math.abs(chunkX - centerChunkX);
483
+ const dz = Math.abs(chunkZ - centerChunkZ);
484
+
485
+ if (dx > RENDER_DISTANCE + 1 || dz > RENDER_DISTANCE + 1) {
486
+ gpu.scene.remove(mesh);
487
+ mesh.geometry.dispose();
488
+ mesh.material.dispose();
489
+ keysToRemove.push(key);
490
+ chunks.delete(key);
491
+ }
492
+ }
493
+
494
+ keysToRemove.forEach(key => chunkMeshes.delete(key));
495
+ }
496
+
497
+ // Raycast to find block player is looking at
498
+ function raycastBlock(origin, direction, maxDistance = 10) {
499
+ const step = 0.1;
500
+ const pos = { x: origin[0], y: origin[1], z: origin[2] };
501
+ const dir = { x: direction[0], y: direction[1], z: direction[2] };
502
+
503
+ for (let i = 0; i < maxDistance / step; i++) {
504
+ pos.x += dir.x * step;
505
+ pos.y += dir.y * step;
506
+ pos.z += dir.z * step;
507
+
508
+ const blockX = Math.floor(pos.x);
509
+ const blockY = Math.floor(pos.y);
510
+ const blockZ = Math.floor(pos.z);
511
+
512
+ const blockType = getBlock(blockX, blockY, blockZ);
513
+
514
+ if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.WATER) {
515
+ return {
516
+ hit: true,
517
+ position: [blockX, blockY, blockZ],
518
+ blockType: blockType,
519
+ distance: i * step,
520
+ };
521
+ }
522
+ }
523
+
524
+ return { hit: false };
525
+ }
526
+
527
+ // Check collision with voxel world
528
+ function checkCollision(pos, size) {
529
+ const minX = Math.floor(pos[0] - size);
530
+ const maxX = Math.floor(pos[0] + size);
531
+ const minY = Math.floor(pos[1]);
532
+ const maxY = Math.floor(pos[1] + size * 2);
533
+ const minZ = Math.floor(pos[2] - size);
534
+ const maxZ = Math.floor(pos[2] + size);
535
+
536
+ for (let x = minX; x <= maxX; x++) {
537
+ for (let y = minY; y <= maxY; y++) {
538
+ for (let z = minZ; z <= maxZ; z++) {
539
+ const blockType = getBlock(x, y, z);
540
+ if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.WATER) {
541
+ return true;
542
+ }
543
+ }
544
+ }
545
+ }
546
+
547
+ return false;
548
+ }
549
+
550
+ // Generate tree structure
551
+ function placeTree(x, y, z) {
552
+ const trunkHeight = 4 + Math.floor(Math.random() * 3);
553
+
554
+ // Trunk
555
+ for (let i = 0; i < trunkHeight; i++) {
556
+ setBlock(x, y + i, z, BLOCK_TYPES.WOOD);
557
+ }
558
+
559
+ // Leaves
560
+ const leafY = y + trunkHeight;
561
+ for (let dx = -2; dx <= 2; dx++) {
562
+ for (let dy = -2; dy <= 2; dy++) {
563
+ for (let dz = -2; dz <= 2; dz++) {
564
+ if (Math.abs(dx) + Math.abs(dy) + Math.abs(dz) < 4) {
565
+ setBlock(x + dx, leafY + dy, z + dz, BLOCK_TYPES.LEAVES);
566
+ }
567
+ }
568
+ }
569
+ }
570
+ }
571
+
572
+ // Public API
573
+ return {
574
+ BLOCK_TYPES,
575
+ CHUNK_SIZE,
576
+ CHUNK_HEIGHT,
577
+
578
+ // World management
579
+ updateChunks,
580
+ getBlock,
581
+ setBlock,
582
+
583
+ // Block interaction
584
+ raycastBlock,
585
+ checkCollision,
586
+
587
+ // Structures
588
+ placeTree,
589
+
590
+ // Expose to global game context
591
+ exposeTo: function (g) {
592
+ g.BLOCK_TYPES = BLOCK_TYPES;
593
+ g.updateVoxelWorld = updateChunks;
594
+ g.getVoxelBlock = getBlock;
595
+ g.setVoxelBlock = setBlock;
596
+ g.raycastVoxelBlock = raycastBlock;
597
+ g.checkVoxelCollision = checkCollision;
598
+ g.placeVoxelTree = placeTree;
599
+ },
600
+ };
601
+ }