Files
minecraft-clone/js/world.js
T
minecraft-cloneandClaude 6e8e207afa feat: live settings panel (≈45 Werte, Live/Reload, Persistenz, AO live)
- panel.js: Drawer-Panel (P / ⚙), schema-getrieben, ●live/⟳reload-Badges
- Werte direkt im Panel einstellbar statt in config.js grabben
- Live-Apply über mutateNested (nur Plain-Objekte, nie Modul-Namespace)
  + registerApply-Callbacks für Three.js-Objekte (FOV, Belichtung, Fog, Licht, Schatten-Frustum, Bloom)
- PLAYER.*/CONTROLS.* live (player.js liest jeden Frame neu)
- AO jetzt live: debounced world.markAllDirty() remesht alle Chunks ohne F5
- Reload-Pflichtige Werte persistiert + blinkender Neu-Starten-Button
  (Seed, Meereshöhe, Spawn, MSAA, Tone-Mapping, Schatten an/Größe, AO-Level)
- Persistenz: localStorage (debounced), config.js lädt beim Start
- HUD/Overlay ausgeblendet, solange das Panel offen ist (Welt frei sichtbar)
- Buttons: Neu starten, Auf Standard zurücksetzen, Werte kopieren (JSON)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-21 16:31:25 +02:00

212 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}
}