feat: Minecraft Clone MVP – prozedurale Welt, Blöcke, Abbauen/Setzen
- Weltgenerierung: FBM-Terrain, 3 Biome (Plains/Wald/Wüste), Höhlen, Wasser, Bäume - Blöcke: Grass, Dirt, Stein, Sand, Holz, Blätter, Wasser (+Bedrock) - Interaktion: DDA-Voxel-Raycast, Abbauen (LMB) & Setzen (RMB), Hotbar 1-6 - Spieler: First-Person, Gravitation, AABB-Kollision, Springen, Fly-Mode (F) - Engine: Chunk-Streaming (16x16x64), Face-Culling, opaker+transparenter Pass - Stack: Three.js + simplex-noise via CDN, prozedurale Texturen, kein Build-Step Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
1049253d54
15 files changed
+1436
No files matched your search
+132
@@ -0,0 +1,132 @@
|
||||
// Weltgenerierung: Terrain, Biome, Hoehlen, Wasser, Baeume.
|
||||
// Arbeitet direkt auf Chunk-Block-Arrays. Baeume koennen chunk-uebergreifend
|
||||
// in Nachbarn schreiben (world.setGen / world.getGen).
|
||||
import {
|
||||
CHUNK_SIZE, CHUNK_HEIGHT, SEA_LEVEL, BLOCK, BIOME,
|
||||
} from './constants.js';
|
||||
import {
|
||||
noise2DTerrain, noise2DDetail, noise2DTemp, noise2DHumid, noise3DCave, fbm2,
|
||||
} from './noise.js';
|
||||
import { hash2 } from './rng.js';
|
||||
|
||||
// Biom aus Temperatur & Feuchtigkeit
|
||||
export function getBiome(wx, wz) {
|
||||
const temp = noise2DTemp(wx * 0.004, wz * 0.004); // -1..1
|
||||
const humid = noise2DHumid(wx * 0.004 + 50, wz * 0.004 - 50);
|
||||
if (temp > 0.35 && humid < 0.0) return BIOME.DESERT;
|
||||
if (humid > 0.05) return BIOME.FOREST;
|
||||
return BIOME.PLAINS;
|
||||
}
|
||||
|
||||
// Terrainhoehe (Ganzzahl) pro Weltsaeule in Abhaengigkeit vom Biom
|
||||
export function terrainHeight(wx, wz, biome) {
|
||||
// Basis-Huegel
|
||||
let n = fbm2(noise2DTerrain, wx * 0.0125, wz * 0.0125, 4, 2.0, 0.5); // -1..1
|
||||
let height;
|
||||
if (biome === BIOME.DESERT) {
|
||||
const dunes = noise2DDetail(wx * 0.03, wz * 0.03) * 0.5;
|
||||
height = SEA_LEVEL + 1 + (n * 0.5 + dunes) * 6; // flach
|
||||
} else if (biome === BIOME.FOREST) {
|
||||
height = SEA_LEVEL + 4 + n * 10;
|
||||
} else {
|
||||
// Plains mit gelegentlichen Huegeln
|
||||
const hill = fbm2(noise2DTerrain, wx * 0.005 + 100, wz * 0.005 - 100, 2, 2.0, 0.5);
|
||||
height = SEA_LEVEL + 2 + n * 7 + hill * 6;
|
||||
}
|
||||
return Math.max(2, Math.min(CHUNK_HEIGHT - 6, Math.round(height)));
|
||||
}
|
||||
|
||||
// Setzt einen Baum (Stamm + Blattkrone) an (wx, groundY, wz)
|
||||
function placeTree(world, wx, groundY, wz) {
|
||||
const h = 4 + (Math.floor(hash2(wx + 7, wz - 3) * 3)); // 4..6
|
||||
for (let i = 1; i <= h; i++) {
|
||||
world.setGen(wx, groundY + i, wz, BLOCK.WOOD);
|
||||
}
|
||||
const topY = groundY + h;
|
||||
// Blattkrone
|
||||
for (let dy = -2; dy <= 1; dy++) {
|
||||
const radius = dy <= -1 ? 2 : 1;
|
||||
for (let dx = -radius; dx <= radius; dx++) {
|
||||
for (let dz = -radius; dz <= radius; dz++) {
|
||||
if (dx === 0 && dz === 0 && dy <= 0) continue; // Stammbereich freilassen
|
||||
// Ecken etwas ausduennen
|
||||
const dist = Math.abs(dx) + Math.abs(dz) + Math.abs(dy);
|
||||
if (dist > 3) continue;
|
||||
if ((Math.abs(dx) === radius && Math.abs(dz) === radius) && hash2(wx + dx * 13, wz + dz * 17) < 0.5) continue;
|
||||
const ly = topY + dy;
|
||||
if (ly < 0 || ly >= CHUNK_HEIGHT) continue;
|
||||
if (world.getGen(wx + dx, ly, wz + dz) === BLOCK.AIR) {
|
||||
world.setGen(wx + dx, ly, wz + dz, BLOCK.LEAVES);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Spitze
|
||||
if (world.getGen(wx, topY + 1, wz) === BLOCK.AIR) {
|
||||
world.setGen(wx, topY + 1, wz, BLOCK.LEAVES);
|
||||
}
|
||||
}
|
||||
|
||||
// Fuellt Block-Daten eines Chunks (ohne Meshing).
|
||||
export function generateChunk(world, chunk) {
|
||||
const baseX = chunk.cx * CHUNK_SIZE;
|
||||
const baseZ = chunk.cz * CHUNK_SIZE;
|
||||
|
||||
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
|
||||
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
|
||||
const wx = baseX + lx;
|
||||
const wz = baseZ + lz;
|
||||
const biome = getBiome(wx, wz);
|
||||
const surface = terrainHeight(wx, wz, biome);
|
||||
|
||||
for (let y = 0; y <= surface; y++) {
|
||||
let block = BLOCK.STONE;
|
||||
if (y === 0) {
|
||||
block = BLOCK.BEDROCK;
|
||||
} else if (y === surface) {
|
||||
block = (biome === BIOME.DESERT || surface <= SEA_LEVEL + 1) ? BLOCK.SAND : BLOCK.GRASS;
|
||||
} else if (y >= surface - 3) {
|
||||
block = (biome === BIOME.DESERT || surface <= SEA_LEVEL + 1) ? BLOCK.SAND : BLOCK.DIRT;
|
||||
} else {
|
||||
block = BLOCK.STONE;
|
||||
}
|
||||
|
||||
// Hoehlen aushoehlen (nicht ganz unten, nicht an der Oberflaeche)
|
||||
if (y > 2 && y < surface - 1) {
|
||||
const c = noise3DCave(wx * 0.055, y * 0.09, wz * 0.055);
|
||||
if (c > 0.62) block = BLOCK.AIR;
|
||||
}
|
||||
|
||||
if (block !== BLOCK.AIR) chunk.set(lx, y, lz, block);
|
||||
}
|
||||
|
||||
// Wasser auffuellen bis Meereshoehe
|
||||
if (surface < SEA_LEVEL) {
|
||||
for (let y = surface + 1; y <= SEA_LEVEL; y++) {
|
||||
if (chunk.get(lx, y, lz) === BLOCK.AIR) chunk.set(lx, y, lz, BLOCK.WATER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Baeume (nur in Wald/Plains, oberhalb Wasser, deterministisch)
|
||||
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
|
||||
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
|
||||
const wx = baseX + lx;
|
||||
const wz = baseZ + lz;
|
||||
const biome = getBiome(wx, wz);
|
||||
if (biome === BIOME.DESERT) continue;
|
||||
const treeChance = biome === BIOME.FOREST ? 0.028 : 0.006;
|
||||
if (hash2(wx * 3 + 1, wz * 5 + 2) < treeChance) {
|
||||
// Bodenflaeche finden
|
||||
let gy = -1;
|
||||
for (let y = CHUNK_HEIGHT - 1; y >= 0; y--) {
|
||||
const b = world.getGen(wx, y, wz);
|
||||
if (b === BLOCK.GRASS) { gy = y; break; }
|
||||
if (b !== BLOCK.AIR && b !== BLOCK.LEAVES && b !== BLOCK.WATER) break;
|
||||
}
|
||||
if (gy > SEA_LEVEL) placeTree(world, wx, gy, wz);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user