// World: Chunk-Storage, Block-Zugriff, Generierung, Mesh-Verwaltung, Streaming. import * as THREE from 'three'; import { CHUNK_SIZE, CHUNK_HEIGHT, BLOCK, isSolid, } from './constants.js'; import { generateChunk } from './worldgen.js'; import { buildChunkGeometry } from './mesher.js'; class Chunk { constructor(cx, cz) { this.cx = cx; this.cz = cz; // Index: y * (S*S) + z * S + x this.blocks = new Uint8Array(CHUNK_SIZE * CHUNK_HEIGHT * CHUNK_SIZE); this.meshes = null; // { opaque: Mesh|null, water: Mesh|null } this.dirty = true; // Mesh muss (neu) gebaut werden } idx(x, y, z) { return y * CHUNK_SIZE * CHUNK_SIZE + z * CHUNK_SIZE + x; } get(x, y, z) { return this.blocks[this.idx(x, y, z)]; } set(x, y, z, v) { this.blocks[this.idx(x, y, z)] = v; } } export class World { constructor(scene, materials, seed) { this.scene = scene; this.materials = materials; // { opaque: Material, water: Material } this.seed = seed; this.chunks = new Map(); } key(cx, cz) { return cx + ',' + cz; } getChunk(cx, cz) { return this.chunks.get(this.key(cx, cz)); } // Generiert den Chunk (falls nicht vorhanden) inkl. Terrain+Baeume. // Da der Chunk VOR dem Generieren in die Map eingetragen wird, ist die // Rekursion (Baeume in Nachbar-Chunks) beschraenkt und terminiert. ensureChunk(cx, cz) { const k = this.key(cx, cz); let c = this.chunks.get(k); if (c) return c; c = new Chunk(cx, cz); this.chunks.set(k, c); generateChunk(this, c); return c; } // Block-Lesezugriff OHNE Generierung (fuer Rendering/Physik/Raycast). getBlock(wx, wy, wz) { if (wy < 0 || wy >= CHUNK_HEIGHT) return BLOCK.AIR; const cx = Math.floor(wx / CHUNK_SIZE); const cz = Math.floor(wz / CHUNK_SIZE); const c = this.getChunk(cx, cz); if (!c) return BLOCK.AIR; return c.get(wx - cx * CHUNK_SIZE, wy, wz - cz * CHUNK_SIZE); } // Lesezugriff MIT Generierung (fuer Weltgenerierung / Baeume). getGen(wx, wy, wz) { if (wy < 0 || wy >= CHUNK_HEIGHT) return BLOCK.AIR; const cx = Math.floor(wx / CHUNK_SIZE); const cz = Math.floor(wz / CHUNK_SIZE); const c = this.ensureChunk(cx, cz); return c.get(wx - cx * CHUNK_SIZE, wy, wz - cz * CHUNK_SIZE); } // Schreibzugriff waehrend Generierung. setGen(wx, wy, wz, v) { if (wy < 0 || wy >= CHUNK_HEIGHT) return; const cx = Math.floor(wx / CHUNK_SIZE); const cz = Math.floor(wz / CHUNK_SIZE); const c = this.ensureChunk(cx, cz); c.set(wx - cx * CHUNK_SIZE, wy, wz - cz * CHUNK_SIZE, v); } // Spieler-Setzen/Abbauen: schreibt Block und markiert betroffene Chunks dirty. setBlock(wx, wy, wz, v) { if (wy < 0 || wy >= CHUNK_HEIGHT) return false; const cx = Math.floor(wx / CHUNK_SIZE); const cz = Math.floor(wz / CHUNK_SIZE); const lx = wx - cx * CHUNK_SIZE; const lz = wz - cz * CHUNK_SIZE; const c = this.ensureChunk(cx, cz); if (c.get(lx, wy, lz) === v) return false; c.set(lx, wy, lz, v); c.dirty = true; // An Chunk-Grenzen Nachbar-Chunks ebenfalls aktualisieren if (lx === 0) this._markDirty(cx - 1, cz); if (lx === CHUNK_SIZE - 1) this._markDirty(cx + 1, cz); if (lz === 0) this._markDirty(cx, cz - 1); if (lz === CHUNK_SIZE - 1) this._markDirty(cx, cz + 1); return true; } _markDirty(cx, cz) { const c = this.getChunk(cx, cz); if (c) c.dirty = true; } // Alle geladenen Chunks für einen Neubau markieren – z.B. wenn ein global // mesh-relevanter Wert (AO_LEVEL) live geändert wird. updateStreaming baut // sie dann über mehrere Frames (maxOpsPerFrame) verteilt neu. markAllDirty() { for (const c of this.chunks.values()) c.dirty = true; } // --- Meshing --- getBlockWorld = (wx, wy, wz) => this.getBlock(wx, wy, wz); buildChunkMesh(chunk) { // Fuer korrektes Border-Culling Nachbarn sicherstellen (Blockdaten). for (let dz = -1; dz <= 1; dz++) { for (let dx = -1; dx <= 1; dx++) { this.ensureChunk(chunk.cx + dx, chunk.cz + dz); } } const { opaque, transparent } = buildChunkGeometry(this.getBlockWorld, chunk); // alte Meshes entfernen this.disposeChunkMesh(chunk); const ox = chunk.cx * CHUNK_SIZE; const oz = chunk.cz * CHUNK_SIZE; const meshes = { opaque: null, water: null }; if (opaque) { const m = new THREE.Mesh(opaque, this.materials.opaque); m.position.set(ox, 0, oz); m.frustumCulled = true; m.castShadow = true; m.receiveShadow = true; this.scene.add(m); meshes.opaque = m; } if (transparent) { const m = new THREE.Mesh(transparent, this.materials.water); m.position.set(ox, 0, oz); m.renderOrder = 1; m.receiveShadow = true; // Wasseroberflaeche kriegt Schatten; casten unnoetig this.scene.add(m); meshes.water = m; } chunk.meshes = meshes; chunk.dirty = false; } disposeChunkMesh(chunk) { if (!chunk.meshes) return; for (const key of ['opaque', 'water']) { const m = chunk.meshes[key]; if (m) { this.scene.remove(m); m.geometry.dispose(); } } chunk.meshes = null; } // --- Streaming: Chunks im Radius um Spieler halten --- updateStreaming(px, pz, renderDistance, maxOpsPerFrame) { const pcx = Math.floor(px / CHUNK_SIZE); const pcz = Math.floor(pz / CHUNK_SIZE); // 1) Entferne zu weit entfernte Chunks for (const [k, c] of this.chunks) { const dx = c.cx - pcx; const dz = c.cz - pcz; if (Math.max(Math.abs(dx), Math.abs(dz)) > renderDistance + 1) { this.disposeChunkMesh(c); this.chunks.delete(k); } } // 2) Sammle fehlende/dirty Chunks im Radius, sortiert nach Distanz const todo = []; for (let dz = -renderDistance; dz <= renderDistance; dz++) { for (let dx = -renderDistance; dx <= renderDistance; dx++) { const cx = pcx + dx; const cz = pcz + dz; const c = this.getChunk(cx, cz); if (!c) { todo.push({ cx, cz, d: dx * dx + dz * dz, gen: true }); } else if (c.dirty) { todo.push({ cx, cz, d: dx * dx + dz * dz, gen: false }); } } } todo.sort((a, b) => a.d - b.d); let ops = 0; for (const job of todo) { if (ops >= maxOpsPerFrame) break; const c = this.ensureChunk(job.cx, job.cz); this.buildChunkMesh(c); ops++; } } // Bei Spielstart: Chunk am Spawn sicherstellen (fuer sicheren Boden) highestSolidY(wx, wz) { const cx = Math.floor(wx / CHUNK_SIZE); const cz = Math.floor(wz / CHUNK_SIZE); const c = this.ensureChunk(cx, cz); const lx = wx - cx * CHUNK_SIZE; const lz = wz - cz * CHUNK_SIZE; for (let y = CHUNK_HEIGHT - 1; y >= 0; y--) { if (isSolid(c.get(lx, y, lz))) return y; } return CHUNK_HEIGHT - 1; } }