/** * Procedural Terrain Generator — Minecraft Clone * * Features: * 1. Biome System — 10 biomes driven by temperature & humidity noise * 2. Multi-noise Terrain — Continentalness, Erosion, Peaks/Valleys * 3. Cave Generation — Spaghetti + Cheese caves via overlapping 3D noise * 4. Ore Distribution — Depth-based with deepslate variants * 5. Tree Placement — Seed-deterministic, biome-aware (7 tree types) * 6. Structure Seeds — Lava pools, water springs, sand/gravel patches * 7. Height Map — Proper per-column height map */ import { createNoise2D, createNoise3D } from 'simplex-noise' import { CHUNK_WIDTH, CHUNK_HEIGHT, CHUNK_DEPTH, SEA_LEVEL, MIN_BUILD_HEIGHT, BLOCK_AIR, BLOCK_STONE, BLOCK_GRANITE, BLOCK_DIORITE, BLOCK_ANDESITE, BLOCK_DEEPSLATE, BLOCK_TUFF, BLOCK_DIRT, BLOCK_GRASS_BLOCK, BLOCK_COARSE_DIRT, BLOCK_MUD, BLOCK_PODZOL, BLOCK_MYCELIUM, BLOCK_SAND, BLOCK_RED_SAND, BLOCK_GRAVEL, BLOCK_CLAY, BLOCK_SANDSTONE, BLOCK_SNOW, BLOCK_SNOW_GRASS, BLOCK_ICE, BLOCK_BEDROCK, BLOCK_COAL_ORE, BLOCK_IRON_ORE, BLOCK_GOLD_ORE, BLOCK_DIAMOND_ORE, BLOCK_EMERALD_ORE, BLOCK_LAPIS_ORE, BLOCK_REDSTONE_ORE, BLOCK_COPPER_ORE, BLOCK_DEEPSLATE_COAL_ORE, BLOCK_DEEPSLATE_IRON_ORE, BLOCK_DEEPSLATE_GOLD_ORE, BLOCK_DEEPSLATE_DIAMOND_ORE, BLOCK_DEEPSLATE_EMERALD_ORE, BLOCK_DEEPSLATE_LAPIS_ORE, BLOCK_DEEPSLATE_REDSTONE_ORE, BLOCK_DEEPSLATE_COPPER_ORE, BLOCK_OAK_LOG, BLOCK_SPRUCE_LOG, BLOCK_BIRCH_LOG, BLOCK_JUNGLE_LOG, BLOCK_DARK_OAK_LOG, BLOCK_ACACIA_LOG, BLOCK_MANGROVE_LOG, BLOCK_OAK_LEAVES, BLOCK_SPRUCE_LEAVES, BLOCK_BIRCH_LEAVES, BLOCK_JUNGLE_LEAVES, BLOCK_DARK_OAK_LEAVES, BLOCK_ACACIA_LEAVES, BLOCK_MANGROVE_LEAVES, BLOCK_WATER, BLOCK_LAVA, BLOCK_CACTUS, BLOCK_SUGAR_CANE, BLOCK_POPPY, BLOCK_DANDELION, BLOCK_TALL_GRASS, BLOCK_FERN, BLOCK_RED_MUSHROOM, BLOCK_BROWN_MUSHROOM, BLOCK_RED_MUSHROOM_BLOCK, BLOCK_BROWN_MUSHROOM_BLOCK, BLOCK_MUSHROOM_STEM, BLOCK_SEAGRASS, BLOCK_LILY_PAD, BLOCK_BLUE_ORCHID, } from '../constants' import type { ChunkData } from '@/types' // ═══════════════════════════════════════════════════════════════ // SEEDED RNG // ═══════════════════════════════════════════════════════════════ function seededRandom(seed: number): () => number { let s = seed | 0 return () => { s = (s * 1664525 + 1013904223) | 0 return (s >>> 0) / 4294967296 } } /** Deterministic hash from world coords → float in [0,1) */ function hashCoord(x: number, y: number, z: number, seed: number): number { let h = (seed | 0) ^ 0x5bd1e995 h = ((h ^ (x * 0x5bd1e995)) * 0x27d4eb2d) | 0 h = ((h ^ (y * 0x5bd1e995)) * 0x27d4eb2d) | 0 h = ((h ^ (z * 0x5bd1e995)) * 0x27d4eb2d) | 0 h ^= h >>> 16 return (h >>> 0) / 4294967296 } // ═══════════════════════════════════════════════════════════════ // BIOME DEFINITIONS // ═══════════════════════════════════════════════════════════════ enum BiomeType { Plains, Forest, Desert, Taiga, Mountains, Swamp, Ocean, Jungle, Savanna, MushroomFields, } interface BiomeDef { type: BiomeType name: string temperature: number // 0 = freezing, 1 = scorching humidity: number // 0 = arid, 1 = soggy surfaceBlock: number subsurfaceBlock: number subsurfaceDepth: number treeType: TreeType treeDensity: number // 0-1 chance per valid column vegetationDensity: number } enum TreeType { None = 'none', Oak = 'oak', Spruce = 'spruce', Birch = 'birch', Jungle = 'jungle', DarkOak = 'dark_oak', Acacia = 'acacia', Mangrove = 'mangrove', Mushroom = 'mushroom', } const BIOME_DEFS: BiomeDef[] = [ { type: BiomeType.Plains, name: 'Plains', temperature: 0.5, humidity: 0.4, surfaceBlock: BLOCK_GRASS_BLOCK, subsurfaceBlock: BLOCK_DIRT, subsurfaceDepth: 4, treeType: TreeType.Oak, treeDensity: 0.005, vegetationDensity: 0.2, }, { type: BiomeType.Forest, name: 'Forest', temperature: 0.5, humidity: 0.6, surfaceBlock: BLOCK_GRASS_BLOCK, subsurfaceBlock: BLOCK_DIRT, subsurfaceDepth: 4, treeType: TreeType.Oak, treeDensity: 0.04, vegetationDensity: 0.08, }, { type: BiomeType.Desert, name: 'Desert', temperature: 0.95, humidity: 0.05, surfaceBlock: BLOCK_SAND, subsurfaceBlock: BLOCK_SANDSTONE, subsurfaceDepth: 6, treeType: TreeType.None, treeDensity: 0, vegetationDensity: 0.005, }, { type: BiomeType.Taiga, name: 'Taiga', temperature: 0.05, humidity: 0.5, surfaceBlock: BLOCK_SNOW_GRASS, subsurfaceBlock: BLOCK_DIRT, subsurfaceDepth: 4, treeType: TreeType.Spruce, treeDensity: 0.03, vegetationDensity: 0.04, }, { type: BiomeType.Mountains, name: 'Mountains', temperature: 0.2, humidity: 0.3, surfaceBlock: BLOCK_GRASS_BLOCK, subsurfaceBlock: BLOCK_STONE, subsurfaceDepth: 1, treeType: TreeType.Spruce, treeDensity: 0.004, vegetationDensity: 0.01, }, { type: BiomeType.Swamp, name: 'Swamp', temperature: 0.6, humidity: 0.9, surfaceBlock: BLOCK_GRASS_BLOCK, subsurfaceBlock: BLOCK_MUD, subsurfaceDepth: 5, treeType: TreeType.Mangrove, treeDensity: 0.015, vegetationDensity: 0.12, }, { type: BiomeType.Ocean, name: 'Ocean', temperature: 0.5, humidity: 0.5, surfaceBlock: BLOCK_SAND, subsurfaceBlock: BLOCK_GRAVEL, subsurfaceDepth: 4, treeType: TreeType.None, treeDensity: 0, vegetationDensity: 0, }, { type: BiomeType.Jungle, name: 'Jungle', temperature: 0.9, humidity: 0.85, surfaceBlock: BLOCK_GRASS_BLOCK, subsurfaceBlock: BLOCK_DIRT, subsurfaceDepth: 4, treeType: TreeType.Jungle, treeDensity: 0.05, vegetationDensity: 0.25, }, { type: BiomeType.Savanna, name: 'Savanna', temperature: 0.85, humidity: 0.15, surfaceBlock: BLOCK_GRASS_BLOCK, subsurfaceBlock: BLOCK_DIRT, subsurfaceDepth: 4, treeType: TreeType.Acacia, treeDensity: 0.008, vegetationDensity: 0.1, }, { type: BiomeType.MushroomFields, name: 'Mushroom Fields', temperature: 0.5, humidity: 0.7, surfaceBlock: BLOCK_MYCELIUM, subsurfaceBlock: BLOCK_DIRT, subsurfaceDepth: 4, treeType: TreeType.Mushroom, treeDensity: 0.01, vegetationDensity: 0, }, ] // ═══════════════════════════════════════════════════════════════ // TERRAIN GENERATOR CLASS // ═══════════════════════════════════════════════════════════════ export class TerrainGenerator { // ---- Noise instances ---- // Biome private noiseTemp: ReturnType private noiseHumidity: ReturnType // Terrain shape private noiseContinentalness: ReturnType private noiseErosion: ReturnType private noisePeaks: ReturnType private noiseDetail: ReturnType // Caves private noiseCaveSpaghetti1: ReturnType private noiseCaveSpaghetti2: ReturnType private noiseCaveCheese1: ReturnType private noiseCaveCheese2: ReturnType // Ore private noiseOre: ReturnType // Structure patches private noisePatch: ReturnType // Vegetation scatter private noiseVeg: ReturnType private seed: number constructor(seed: number) { this.seed = seed const rng = seededRandom(seed) // Biome noise this.noiseTemp = createNoise2D(rng) this.noiseHumidity = createNoise2D(rng) // Terrain shape noise this.noiseContinentalness = createNoise2D(rng) this.noiseErosion = createNoise2D(rng) this.noisePeaks = createNoise2D(rng) this.noiseDetail = createNoise2D(rng) // Cave noise (4 independent 3D fields) this.noiseCaveSpaghetti1 = createNoise3D(rng) this.noiseCaveSpaghetti2 = createNoise3D(rng) this.noiseCaveCheese1 = createNoise3D(rng) this.noiseCaveCheese2 = createNoise3D(rng) // Ore noise this.noiseOre = createNoise3D(rng) // Patch noise this.noisePatch = createNoise2D(rng) // Vegetation noise this.noiseVeg = createNoise2D(rng) } // ═══════════════════════════════════════════════════════════ // PUBLIC API // ═══════════════════════════════════════════════════════════ generateChunk(cx: number, cz: number): ChunkData { const blocks = new Uint8Array(CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH) const heightMap = new Uint16Array(CHUNK_WIDTH * CHUNK_DEPTH) // Phase 1: Compute biome & height map for the chunk const biomeCache = new Array(CHUNK_WIDTH * CHUNK_DEPTH) for (let lx = 0; lx < CHUNK_WIDTH; lx++) { for (let lz = 0; lz < CHUNK_DEPTH; lz++) { const wx = cx * CHUNK_WIDTH + lx const wz = cz * CHUNK_DEPTH + lz const biomeIdx = lx + lz * CHUNK_WIDTH biomeCache[biomeIdx] = this.getBiome(wx, wz) const height = this.getHeight(wx, wz, biomeCache[biomeIdx]) heightMap[biomeIdx] = height } } // Phase 2: Fill blocks column by column for (let lx = 0; lx < CHUNK_WIDTH; lx++) { for (let lz = 0; lz < CHUNK_DEPTH; lz++) { const wx = cx * CHUNK_WIDTH + lx const wz = cz * CHUNK_DEPTH + lz const biomeIdx = lx + lz * CHUNK_WIDTH const biome = biomeCache[biomeIdx] const surfaceHeight = heightMap[biomeIdx] for (let ly = 0; ly < CHUNK_HEIGHT; ly++) { const y = ly + MIN_BUILD_HEIGHT const blockId = this.getBlockAt(wx, y, wz, surfaceHeight, biome) const idx = lx + lz * CHUNK_WIDTH + ly * CHUNK_WIDTH * CHUNK_DEPTH if (blockId !== BLOCK_AIR) { blocks[idx] = blockId } } } } // Phase 3: Place trees (deterministic) this.placeTrees(cx, cz, blocks, heightMap, biomeCache) // Phase 4: Place vegetation & decorations this.placeVegetation(cx, cz, blocks, heightMap, biomeCache) // Phase 5: Structure seeds (lava pools, water springs, patches) this.placeStructures(cx, cz, blocks, heightMap, biomeCache) return { position: { x: cx, z: cz }, blocks, heightMap, dirty: true, meshVersion: 0, } } /** * Public height query for spawn point calculation. */ getHeight(wx: number, wz: number, biome?: BiomeDef): number { const b = biome ?? this.getBiome(wx, wz) return this.computeHeight(wx, wz, b) } // ═══════════════════════════════════════════════════════════ // BIOME DETERMINATION // ═══════════════════════════════════════════════════════════ private getBiome(wx: number, wz: number): BiomeDef { const temp = this.octave2D(this.noiseTemp, wx * 0.0008, wz * 0.0008, 4) const humid = this.octave2D(this.noiseHumidity, wx * 0.0008, wz * 0.0008, 4) // Continentalness determines ocean vs land const continentalness = this.octave2D(this.noiseContinentalness, wx * 0.0005, wz * 0.0005, 3) // Strong ocean bias for deep negative continentalness if (continentalness < -0.3) { return BIOME_DEFS[BiomeType.Ocean] } // Mushroom fields: rare, near ocean boundary if (continentalness < -0.15 && temp > 0.3 && humid > 0.5) { return BIOME_DEFS[BiomeType.MushroomFields] } // Mountains: driven by peaks noise, independent of temp/humid const peaks = this.octave2D(this.noisePeaks, wx * 0.002, wz * 0.002, 3) if (peaks > 0.45) { return BIOME_DEFS[BiomeType.Mountains] } // Temperature/humidity grid for remaining biomes const t = (temp + 1) * 0.5 // map [-1,1] → [0,1] const h = (humid + 1) * 0.5 if (t < 0.2) { // Cold return BIOME_DEFS[BiomeType.Taiga] } if (t > 0.75) { // Hot if (h < 0.3) return BIOME_DEFS[BiomeType.Desert] if (h < 0.55) return BIOME_DEFS[BiomeType.Savanna] return BIOME_DEFS[BiomeType.Jungle] } // Temperate if (h > 0.75) return BIOME_DEFS[BiomeType.Swamp] if (h > 0.45) return BIOME_DEFS[BiomeType.Forest] return BIOME_DEFS[BiomeType.Plains] } // ═══════════════════════════════════════════════════════════ // TERRAIN HEIGHT // ═══════════════════════════════════════════════════════════ private computeHeight(wx: number, wz: number, biome: BiomeDef): number { // Continentalness — large scale land/ocean shape const continentalness = this.octave2D(this.noiseContinentalness, wx * 0.0005, wz * 0.0005, 3) // Erosion — terrain roughness const erosion = this.octave2D(this.noiseErosion, wx * 0.003, wz * 0.003, 4) // Peaks/Valleys — mountain height const peaks = this.octave2D(this.noisePeaks, wx * 0.002, wz * 0.002, 3) // Detail — small-scale variation const detail = this.octave2D(this.noiseDetail, wx * 0.015, wz * 0.015, 2) let height = SEA_LEVEL // Continentalness base shift if (continentalness > -0.15) { height += continentalness * 25 } else { // Ocean floor is below sea level height += continentalness * 18 } // Mountain peaks if (biome.type === BiomeType.Mountains) { height += Math.max(0, peaks) * 50 height += Math.max(0, peaks * peaks) * 30 // Extra tall for high peaks } else { height += Math.max(0, peaks) * 8 } // Erosion smooths or roughens height += erosion * 10 // Biome-specific base height switch (biome.type) { case BiomeType.Ocean: height -= 15 break case BiomeType.Swamp: height = Math.min(height, SEA_LEVEL + 1) break case BiomeType.Desert: height += 2 break case BiomeType.MushroomFields: height -= 5 break } // Detail noise height += detail * 4 // Flatten near spawn (radius 32) const distFromSpawn = Math.sqrt(wx * wx + wz * wz) if (distFromSpawn < 32) { const factor = distFromSpawn / 32 height = SEA_LEVEL + (height - SEA_LEVEL) * factor * factor } return Math.max(Math.min(Math.floor(height), 319), MIN_BUILD_HEIGHT + 1) } // ═══════════════════════════════════════════════════════════ // BLOCK DETERMINATION // ═══════════════════════════════════════════════════════════ private getBlockAt(wx: number, y: number, wz: number, surfaceHeight: number, biome: BiomeDef): number { // Bedrock floor (bottom 5 layers, probability-based) if (y <= MIN_BUILD_HEIGHT + 4) { if (y === MIN_BUILD_HEIGHT) return BLOCK_BEDROCK return hashCoord(wx, y, wz, this.seed ^ 0xBED70C7) < 0.5 ? BLOCK_BEDROCK : BLOCK_STONE } // Above surface → air or water if (y > surfaceHeight) { if (y <= SEA_LEVEL) return BLOCK_WATER return BLOCK_AIR } // ---- Surface block ---- if (y === surfaceHeight) { // Beach override: if surface is near sea level, place sand if (surfaceHeight <= SEA_LEVEL + 3 && surfaceHeight >= SEA_LEVEL - 2 && biome.type !== BiomeType.Ocean && biome.type !== BiomeType.Swamp) { return BLOCK_SAND } // Frozen ocean surface if (biome.type === BiomeType.Ocean && surfaceHeight === SEA_LEVEL) { return BLOCK_ICE } return biome.surfaceBlock } // ---- Subsurface layers ---- if (y > surfaceHeight - biome.subsurfaceDepth) { // Beach subsurface override if (surfaceHeight <= SEA_LEVEL + 3 && surfaceHeight >= SEA_LEVEL - 2 && biome.type !== BiomeType.Ocean && biome.type !== BiomeType.Swamp) { return BLOCK_SAND } return biome.subsurfaceBlock } // ---- Underground stone ---- return this.getUndergroundBlock(wx, y, wz, biome) } // ═══════════════════════════════════════════════════════════ // UNDERGROUND: caves, stone types, ores // ═══════════════════════════════════════════════════════════ private getUndergroundBlock(wx: number, y: number, wz: number, biome: BiomeDef): number { // ---- Cave carving ---- if (this.isCave(wx, y, wz)) return BLOCK_AIR // ---- Base stone / deepslate ---- const stone = y < 0 ? BLOCK_DEEPSLATE : BLOCK_STONE // ---- Stone variety (granite, diorite, andesite, tuff) ---- const stoneVar = this.getStoneVariety(wx, y, wz) if (stoneVar !== BLOCK_AIR) { // Deepslate variants don't apply to variety stones — they stay as is return stoneVar } // ---- Ore distribution ---- const ore = this.getOre(wx, y, wz, biome) if (ore !== BLOCK_AIR) return ore return stone } /** * Spaghetti caves: long winding tunnels * Cheese caves: large open caverns */ private isCave(wx: number, y: number, wz: number): boolean { // No caves in the top 10 blocks of surface to avoid sky exposure // (simplified: no caves above Y 50) if (y > 50) return false // ---- Spaghetti caves ---- // Two overlapping 3D noise fields offset from each other const sp1 = this.noiseCaveSpaghetti1(wx * 0.02, y * 0.05, wz * 0.02) const sp2 = this.noiseCaveSpaghetti2(wx * 0.02 + 100, y * 0.05, wz * 0.02 + 100) // Spaghetti: narrow winding tunnels where both noises agree if (sp1 * sp1 + sp2 * sp2 < 0.012) return true // ---- Cheese caves ---- // Large spherical caverns from two slower 3D noises const ch1 = this.noiseCaveCheese1(wx * 0.008, y * 0.01, wz * 0.008) const ch2 = this.noiseCaveCheese2(wx * 0.008 + 200, y * 0.01 + 200, wz * 0.008 + 200) // Cheese: open caverns where noise exceeds threshold if (ch1 > 0.55 && ch2 > 0.35) return true // Smaller cheese pockets if (ch1 > 0.7) return true return false } /** * Stone variety patches (granite, diorite, andesite, tuff) */ private getStoneVariety(wx: number, y: number, wz: number): number { const v = this.noiseOre(wx * 0.05, y * 0.05, wz * 0.05) if (v > 0.7) return BLOCK_GRANITE if (v < -0.7) return BLOCK_DIORITE if (v > 0.5 && v < 0.6) return BLOCK_ANDESITE if (y < 0 && v > 0.4 && v < 0.5) return BLOCK_TUFF return BLOCK_AIR // signals "no variety" } /** * Ore distribution matching vanilla MC depth ranges. * Returns BLOCK_AIR if no ore at this position. */ private getOre(wx: number, y: number, wz: number, biome: BiomeDef): number { const useDeepslate = y < 0 // Single ore noise field sampled at different scales for each ore type // This ensures ores don't overlap at the same position // Coal: Y 0-320, peak at Y 96 if (y >= 0 && y <= 320) { const n = this.noiseOre(wx * 0.12 + 1000, y * 0.12, wz * 0.12 + 1000) const distFromPeak = Math.abs(y - 96) / 96 const threshold = 0.82 + distFromPeak * 0.1 if (n > threshold) return useDeepslate ? BLOCK_DEEPSLATE_COAL_ORE : BLOCK_COAL_ORE } // Iron: Y -64-320, peaks at Y 16 and Y 232 if (y >= -64 && y <= 320) { const n = this.noiseOre(wx * 0.13 + 2000, y * 0.13, wz * 0.13 + 2000) const distFromPeak1 = Math.abs(y - 16) / 80 const distFromPeak2 = Math.abs(y - 232) / 88 const distFromPeak = Math.min(distFromPeak1, distFromPeak2) const threshold = 0.82 + distFromPeak * 0.1 if (n > threshold) return useDeepslate ? BLOCK_DEEPSLATE_IRON_ORE : BLOCK_IRON_ORE } // Gold: Y -64-32, peak at Y -16 if (y >= -64 && y <= 32) { const n = this.noiseOre(wx * 0.14 + 3000, y * 0.14, wz * 0.14 + 3000) const distFromPeak = Math.abs(y + 16) / 48 const threshold = 0.87 + distFromPeak * 0.08 if (n > threshold) return useDeepslate ? BLOCK_DEEPSLATE_GOLD_ORE : BLOCK_GOLD_ORE } // Diamond: Y -64-16, peak at Y -59 if (y >= -64 && y <= 16) { const n = this.noiseOre(wx * 0.15 + 4000, y * 0.15, wz * 0.15 + 4000) const distFromPeak = Math.abs(y + 59) / 75 const threshold = 0.92 + distFromPeak * 0.05 if (n > threshold) return useDeepslate ? BLOCK_DEEPSLATE_DIAMOND_ORE : BLOCK_DIAMOND_ORE } // Redstone: Y -64-16 if (y >= -64 && y <= 16) { const n = this.noiseOre(wx * 0.16 + 5000, y * 0.16, wz * 0.16 + 5000) const distFromPeak = Math.abs(y + 8) / 56 const threshold = 0.86 + distFromPeak * 0.08 if (n > threshold) return useDeepslate ? BLOCK_DEEPSLATE_REDSTONE_ORE : BLOCK_REDSTONE_ORE } // Lapis: Y -64-64, peak at Y 0 if (y >= -64 && y <= 64) { const n = this.noiseOre(wx * 0.12 + 6000, y * 0.12, wz * 0.12 + 6000) const distFromPeak = Math.abs(y) / 64 const threshold = 0.88 + distFromPeak * 0.07 if (n > threshold) return useDeepslate ? BLOCK_DEEPSLATE_LAPIS_ORE : BLOCK_LAPIS_ORE } // Copper: Y -16-112, peak at Y 48 if (y >= -16 && y <= 112) { const n = this.noiseOre(wx * 0.13 + 7000, y * 0.13, wz * 0.13 + 7000) const distFromPeak = Math.abs(y - 48) / 64 const threshold = 0.86 + distFromPeak * 0.08 if (n > threshold) return useDeepslate ? BLOCK_DEEPSLATE_COPPER_ORE : BLOCK_COPPER_ORE } // Emerald: Y -16-320, mountains only if (biome.type === BiomeType.Mountains && y >= -16 && y <= 320) { const n = this.noiseOre(wx * 0.2 + 8000, y * 0.2, wz * 0.2 + 8000) if (n > 0.93) return useDeepslate ? BLOCK_DEEPSLATE_EMERALD_ORE : BLOCK_EMERALD_ORE } return BLOCK_AIR } // ═══════════════════════════════════════════════════════════ // TREE PLACEMENT (seed-based deterministic) // ═══════════════════════════════════════════════════════════ private placeTrees( cx: number, cz: number, blocks: Uint8Array, heightMap: Uint16Array, biomeCache: BiomeDef[] ): void { const rng = seededRandom(this.seed ^ (cx * 7919 + cz * 104729 + 1)) for (let lx = 0; lx < CHUNK_WIDTH; lx++) { for (let lz = 0; lz < CHUNK_DEPTH; lz++) { const biomeIdx = lx + lz * CHUNK_WIDTH const biome = biomeCache[biomeIdx] if (biome.treeType === TreeType.None) continue // Deterministic per-column tree check if (rng() > biome.treeDensity) continue const surfaceHeight = heightMap[biomeIdx] // Trees must be above sea level and on the correct surface if (surfaceHeight <= SEA_LEVEL + 1) continue const surfaceBlock = this.getBlockAtLocal(blocks, lx, surfaceHeight, lz) if (surfaceBlock !== biome.surfaceBlock && surfaceBlock !== BLOCK_GRASS_BLOCK) continue // Randomize height within the column const treeRng = seededRandom( this.seed ^ (cx * CHUNK_WIDTH + lx) * 374761393 + (cz * CHUNK_DEPTH + lz) * 668265263 ) const treeHeight = this.getTreeHeight(biome.treeType, treeRng) // Place the tree this.placeTreeByType(blocks, lx, lz, surfaceHeight, biome.treeType, treeHeight, treeRng) } } } private getTreeHeight(type: TreeType, rng: () => number): number { switch (type) { case TreeType.Oak: return 4 + Math.floor(rng() * 3) // 4-6 case TreeType.Spruce: return 6 + Math.floor(rng() * 5) // 6-10 case TreeType.Birch: return 5 + Math.floor(rng() * 3) // 5-7 case TreeType.Jungle: return 7 + Math.floor(rng() * 6) // 7-12 case TreeType.DarkOak: return 6 + Math.floor(rng() * 3) // 6-8 case TreeType.Acacia: return 5 + Math.floor(rng() * 5) // 5-9 case TreeType.Mangrove: return 4 + Math.floor(rng() * 3) // 4-6 case TreeType.Mushroom: return 4 + Math.floor(rng() * 4) // 4-7 default: return 5 } } private placeTreeByType( blocks: Uint8Array, lx: number, lz: number, surfaceY: number, type: TreeType, height: number, rng: () => number ): void { switch (type) { case TreeType.Oak: this.placeOakTree(blocks, lx, lz, surfaceY, height); break case TreeType.Spruce: this.placeSpruceTree(blocks, lx, lz, surfaceY, height); break case TreeType.Birch: this.placeBirchTree(blocks, lx, lz, surfaceY, height); break case TreeType.Jungle: this.placeJungleTree(blocks, lx, lz, surfaceY, height, rng); break case TreeType.DarkOak: this.placeDarkOakTree(blocks, lx, lz, surfaceY, height); break case TreeType.Acacia: this.placeAcaciaTree(blocks, lx, lz, surfaceY, height, rng); break case TreeType.Mangrove: this.placeMangroveTree(blocks, lx, lz, surfaceY, height); break case TreeType.Mushroom: this.placeMushroomTree(blocks, lx, lz, surfaceY, height); break } } // ---- Individual tree placers ---- /** Oak: sphere leaves */ private placeOakTree(blocks: Uint8Array, lx: number, lz: number, surfaceY: number, height: number): void { const logBlock = BLOCK_OAK_LOG const leafBlock = BLOCK_OAK_LEAVES // Trunk for (let ty = 1; ty <= height; ty++) { this.setBlockLocal(blocks, lx, surfaceY + ty, lz, logBlock) } // Sphere leaves centered at top - 1 const leafCenter = surfaceY + height - 1 const radius = 2 for (let dx = -radius; dx <= radius; dx++) { for (let dy = -1; dy <= 2; dy++) { for (let dz = -radius; dz <= radius; dz++) { const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) if (dist > radius + 0.5) continue // Skip corners for rounder shape if (Math.abs(dx) === 2 && Math.abs(dz) === 2 && dy < 1) continue if (dy === 2 && (Math.abs(dx) > 1 || Math.abs(dz) > 1)) continue const nx = lx + dx const ny = leafCenter + dy const nz = lz + dz if (nx < 0 || nx >= CHUNK_WIDTH || nz < 0 || nz >= CHUNK_DEPTH) continue if (ny <= surfaceY || ny > 319) continue const existing = this.getBlockAtLocal(blocks, nx, ny, nz) if (existing === BLOCK_AIR || existing === 0) { this.setBlockLocal(blocks, nx, ny, nz, leafBlock) } } } } } /** Spruce: cone leaves */ private placeSpruceTree(blocks: Uint8Array, lx: number, lz: number, surfaceY: number, height: number): void { const logBlock = BLOCK_SPRUCE_LOG const leafBlock = BLOCK_SPRUCE_LEAVES // Trunk for (let ty = 1; ty <= height; ty++) { this.setBlockLocal(blocks, lx, surfaceY + ty, lz, logBlock) } // Cone: leaves start a few blocks up, taper toward top const leafStart = 3 for (let dy = leafStart; dy <= height; dy++) { const layerFromTop = height - dy const layerRadius = Math.max(0, Math.floor(layerFromTop / 2) + 1) for (let dx = -layerRadius; dx <= layerRadius; dx++) { for (let dz = -layerRadius; dz <= layerRadius; dz++) { // Skip corners of each layer if (Math.abs(dx) === layerRadius && Math.abs(dz) === layerRadius && layerRadius > 1) continue const nx = lx + dx const ny = surfaceY + dy const nz = lz + dz if (nx < 0 || nx >= CHUNK_WIDTH || nz < 0 || nz >= CHUNK_DEPTH) continue if (ny <= surfaceY || ny > 319) continue const existing = this.getBlockAtLocal(blocks, nx, ny, nz) if (existing === BLOCK_AIR || existing === 0) { this.setBlockLocal(blocks, nx, ny, nz, leafBlock) } } } } // Top leaf this.setBlockLocal(blocks, lx, surfaceY + height + 1, lz, leafBlock) } /** Birch: smaller, rounder leaves */ private placeBirchTree(blocks: Uint8Array, lx: number, lz: number, surfaceY: number, height: number): void { const logBlock = BLOCK_BIRCH_LOG const leafBlock = BLOCK_BIRCH_LEAVES // Trunk for (let ty = 1; ty <= height; ty++) { this.setBlockLocal(blocks, lx, surfaceY + ty, lz, logBlock) } // Leaves — compact egg shape const leafCenter = surfaceY + height - 2 for (let dx = -1; dx <= 1; dx++) { for (let dy = 0; dy <= 3; dy++) { for (let dz = -1; dz <= 1; dz++) { if (dy === 3 && (Math.abs(dx) + Math.abs(dz)) > 1) continue if (dy === 0 && Math.abs(dx) === 1 && Math.abs(dz) === 1) continue const nx = lx + dx const ny = leafCenter + dy const nz = lz + dz if (nx < 0 || nx >= CHUNK_WIDTH || nz < 0 || nz >= CHUNK_DEPTH) continue if (ny <= surfaceY || ny > 319) continue const existing = this.getBlockAtLocal(blocks, nx, ny, nz) if (existing === BLOCK_AIR || existing === 0) { this.setBlockLocal(blocks, nx, ny, nz, leafBlock) } } } } } /** Jungle: tall with large leaves */ private placeJungleTree(blocks: Uint8Array, lx: number, lz: number, surfaceY: number, height: number, rng: () => number): void { const logBlock = BLOCK_JUNGLE_LOG const leafBlock = BLOCK_JUNGLE_LEAVES // Sometimes place a 2x2 trunk for giant jungle trees const isGiant = height >= 10 && rng() > 0.5 if (isGiant) { // 2x2 trunk for (let dx = 0; dx <= 1; dx++) { for (let dz = 0; dz <= 1; dz++) { for (let ty = 1; ty <= height; ty++) { this.setBlockLocal(blocks, lx + dx, surfaceY + ty, lz + dz, logBlock) } } } // Large leaf cluster const leafCenter = surfaceY + height - 3 for (let dx = -2; dx <= 3; dx++) { for (let dy = 0; dy <= 4; dy++) { for (let dz = -2; dz <= 3; dz++) { if (dy === 4 && (Math.abs(dx) > 1 || Math.abs(dz) > 1)) continue if (dy === 0 && Math.abs(dx) === 2 && Math.abs(dz) === 2) continue const nx = lx + dx const ny = leafCenter + dy const nz = lz + dz if (nx < 0 || nx >= CHUNK_WIDTH || nz < 0 || nz >= CHUNK_DEPTH) continue if (ny <= surfaceY || ny > 319) continue const existing = this.getBlockAtLocal(blocks, nx, ny, nz) if (existing === BLOCK_AIR || existing === 0) { this.setBlockLocal(blocks, nx, ny, nz, leafBlock) } } } } } else { // Single trunk for (let ty = 1; ty <= height; ty++) { this.setBlockLocal(blocks, lx, surfaceY + ty, lz, logBlock) } // Leaf cluster const leafCenter = surfaceY + height - 2 for (let dx = -2; dx <= 2; dx++) { for (let dy = -1; dy <= 3; dy++) { for (let dz = -2; dz <= 2; dz++) { if (Math.abs(dx) === 2 && Math.abs(dz) === 2 && dy > 1) continue if (dy === 3 && (Math.abs(dx) > 1 || Math.abs(dz) > 1)) continue const nx = lx + dx const ny = leafCenter + dy const nz = lz + dz if (nx < 0 || nx >= CHUNK_WIDTH || nz < 0 || nz >= CHUNK_DEPTH) continue if (ny <= surfaceY || ny > 319) continue const existing = this.getBlockAtLocal(blocks, nx, ny, nz) if (existing === BLOCK_AIR || existing === 0) { this.setBlockLocal(blocks, nx, ny, nz, leafBlock) } } } } } } /** Dark Oak: dense, needs 2x2 trunk */ private placeDarkOakTree(blocks: Uint8Array, lx: number, lz: number, surfaceY: number, height: number): void { const logBlock = BLOCK_DARK_OAK_LOG const leafBlock = BLOCK_DARK_OAK_LEAVES // 2x2 trunk for (let dx = 0; dx <= 1; dx++) { for (let dz = 0; dz <= 1; dz++) { for (let ty = 1; ty <= height; ty++) { this.setBlockLocal(blocks, lx + dx, surfaceY + ty, lz + dz, logBlock) } } } // Dense leaf canopy const leafStart = surfaceY + height - 3 for (let dx = -2; dx <= 3; dx++) { for (let dy = 0; dy <= 3; dy++) { for (let dz = -2; dz <= 3; dz++) { // Trim corners if (dy === 3 && (Math.abs(dx) > 2 || Math.abs(dz) > 2)) continue if (dy === 0 && Math.abs(dx) === 2 && Math.abs(dz) === 2) continue const nx = lx + dx const ny = leafStart + dy const nz = lz + dz if (nx < 0 || nx >= CHUNK_WIDTH || nz < 0 || nz >= CHUNK_DEPTH) continue if (ny <= surfaceY || ny > 319) continue const existing = this.getBlockAtLocal(blocks, nx, ny, nz) if (existing === BLOCK_AIR || existing === 0) { this.setBlockLocal(blocks, nx, ny, nz, leafBlock) } } } } } /** Acacia: flat-topped canopy with angled trunk */ private placeAcaciaTree(blocks: Uint8Array, lx: number, lz: number, surfaceY: number, height: number, rng: () => number): void { const logBlock = BLOCK_ACACIA_LOG const leafBlock = BLOCK_ACACIA_LEAVES // Acacia has a diagonal trunk bend at the middle const bendAt = Math.floor(height * 0.6) const bendDir = rng() > 0.5 ? 1 : -1 // Lower trunk (straight) for (let ty = 1; ty <= bendAt; ty++) { this.setBlockLocal(blocks, lx, surfaceY + ty, lz, logBlock) } // Upper trunk (bent by 1 block in random direction) for (let ty = bendAt + 1; ty <= height; ty++) { this.setBlockLocal(blocks, lx + bendDir, surfaceY + ty, lz, logBlock) } // Flat-top leaf canopy at the bend point const leafY = surfaceY + height for (let dx = -2; dx <= 2; dx++) { for (let dy = 0; dy <= 1; dy++) { for (let dz = -2; dz <= 2; dz++) { if (Math.abs(dx) === 2 && Math.abs(dz) === 2 && dy === 1) continue const nx = lx + bendDir + dx const ny = leafY + dy const nz = lz + dz if (nx < 0 || nx >= CHUNK_WIDTH || nz < 0 || nz >= CHUNK_DEPTH) continue if (ny <= surfaceY || ny > 319) continue const existing = this.getBlockAtLocal(blocks, nx, ny, nz) if (existing === BLOCK_AIR || existing === 0) { this.setBlockLocal(blocks, nx, ny, nz, leafBlock) } } } } } /** Mangrove: short with wide roots */ private placeMangroveTree(blocks: Uint8Array, lx: number, lz: number, surfaceY: number, height: number): void { const logBlock = BLOCK_MANGROVE_LOG const leafBlock = BLOCK_MANGROVE_LEAVES // Roots: 3-5 blocks below surface spreading outward const rootDepth = Math.min(3, surfaceY - SEA_LEVEL) if (rootDepth > 0) { for (let r = 1; r <= rootDepth; r++) { // Place root logs spreading out if (lx - 1 >= 0) this.setBlockLocal(blocks, lx - 1, surfaceY - r, lz, logBlock) if (lx + 1 < CHUNK_WIDTH) this.setBlockLocal(blocks, lx + 1, surfaceY - r, lz, logBlock) if (lz - 1 >= 0) this.setBlockLocal(blocks, lx, surfaceY - r, lz - 1, logBlock) if (lz + 1 < CHUNK_DEPTH) this.setBlockLocal(blocks, lx, surfaceY - r, lz + 1, logBlock) } } // Trunk for (let ty = 1; ty <= height; ty++) { this.setBlockLocal(blocks, lx, surfaceY + ty, lz, logBlock) } // Leaves — wider spread const leafCenter = surfaceY + height - 1 for (let dx = -2; dx <= 2; dx++) { for (let dy = -1; dy <= 2; dy++) { for (let dz = -2; dz <= 2; dz++) { if (Math.abs(dx) === 2 && Math.abs(dz) === 2 && dy < 1) continue if (dy === 2 && (Math.abs(dx) > 1 || Math.abs(dz) > 1)) continue const nx = lx + dx const ny = leafCenter + dy const nz = lz + dz if (nx < 0 || nx >= CHUNK_WIDTH || nz < 0 || nz >= CHUNK_DEPTH) continue if (ny <= surfaceY || ny > 319) continue const existing = this.getBlockAtLocal(blocks, nx, ny, nz) if (existing === BLOCK_AIR || existing === 0) { this.setBlockLocal(blocks, nx, ny, nz, leafBlock) } } } } } /** Mushroom: huge mushroom with stem and cap */ private placeMushroomTree(blocks: Uint8Array, lx: number, lz: number, surfaceY: number, height: number): void { const isBrown = hashCoord(lx, surfaceY, lz, this.seed ^ 0x5A7000) > 0.5 const capBlock = isBrown ? BLOCK_BROWN_MUSHROOM_BLOCK : BLOCK_RED_MUSHROOM_BLOCK // Stem for (let ty = 1; ty <= height; ty++) { this.setBlockLocal(blocks, lx, surfaceY + ty, lz, BLOCK_MUSHROOM_STEM) } // Cap shape const capY = surfaceY + height if (isBrown) { // Brown: dome shape for (let dx = -2; dx <= 2; dx++) { for (let dy = -1; dy <= 1; dy++) { for (let dz = -2; dz <= 2; dz++) { if (Math.abs(dx) === 2 && Math.abs(dz) === 2 && dy < 0) continue if (dy === 1 && (Math.abs(dx) > 1 || Math.abs(dz) > 1)) continue const nx = lx + dx const ny = capY + dy const nz = lz + dz if (nx < 0 || nx >= CHUNK_WIDTH || nz < 0 || nz >= CHUNK_DEPTH) continue if (ny <= surfaceY || ny > 319) continue this.setBlockLocal(blocks, nx, ny, nz, capBlock) } } } } else { // Red: taller, more pointed cap with white spots for (let dx = -2; dx <= 2; dx++) { for (let dy = 0; dy <= 2; dy++) { for (let dz = -2; dz <= 2; dz++) { if (dy === 2 && (Math.abs(dx) > 0 || Math.abs(dz) > 0)) continue if (dy === 1 && Math.abs(dx) === 2 && Math.abs(dz) === 2) continue if (dy === 0 && Math.abs(dx) === 2 && Math.abs(dz) === 2) continue const nx = lx + dx const ny = capY + dy const nz = lz + dz if (nx < 0 || nx >= CHUNK_WIDTH || nz < 0 || nz >= CHUNK_DEPTH) continue if (ny <= surfaceY || ny > 319) continue this.setBlockLocal(blocks, nx, ny, nz, capBlock) } } } } } // ═══════════════════════════════════════════════════════════ // VEGETATION & DECORATIONS // ═══════════════════════════════════════════════════════════ private placeVegetation( cx: number, cz: number, blocks: Uint8Array, heightMap: Uint16Array, biomeCache: BiomeDef[] ): void { for (let lx = 0; lx < CHUNK_WIDTH; lx++) { for (let lz = 0; lz < CHUNK_DEPTH; lz++) { const biomeIdx = lx + lz * CHUNK_WIDTH const biome = biomeCache[biomeIdx] const surfaceY = heightMap[biomeIdx] if (surfaceY <= SEA_LEVEL) continue const wx = cx * CHUNK_WIDTH + lx const wz = cz * CHUNK_DEPTH + lz // Use deterministic noise for vegetation placement const vegNoise = this.noiseVeg(wx * 0.5, wz * 0.5) const vegHash = hashCoord(wx, surfaceY, wz, this.seed ^ 0x7E3999) if (vegHash > biome.vegetationDensity) continue const surfaceBlock = this.getBlockAtLocal(blocks, lx, surfaceY, lz) switch (biome.type) { case BiomeType.Plains: if (surfaceBlock === BLOCK_GRASS_BLOCK) { if (vegNoise > 0.3) this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_TALL_GRASS) else if (vegNoise > 0.1) this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_POPPY) else this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_DANDELION) } break case BiomeType.Forest: if (surfaceBlock === BLOCK_GRASS_BLOCK) { if (vegNoise > 0.2) this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_TALL_GRASS) else if (vegNoise > -0.2) this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_POPPY) else this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_DANDELION) } break case BiomeType.Desert: if (surfaceBlock === BLOCK_SAND && vegHash < 0.003) { this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_CACTUS) } break case BiomeType.Taiga: if (surfaceBlock === BLOCK_SNOW_GRASS) { if (vegNoise > 0) this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_FERN) else this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_TALL_GRASS) } break case BiomeType.Swamp: if (surfaceBlock === BLOCK_GRASS_BLOCK) { this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_FERN) } // Lily pads on water if (surfaceY === SEA_LEVEL) { const lilyHash = hashCoord(wx, SEA_LEVEL + 1, wz, this.seed ^ 0x11EE99) if (lilyHash < 0.04) { this.setBlockLocal(blocks, lx, SEA_LEVEL + 1, lz, BLOCK_LILY_PAD) } } break case BiomeType.Jungle: if (surfaceBlock === BLOCK_GRASS_BLOCK) { if (vegNoise > 0.2) this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_TALL_GRASS) else if (vegNoise > -0.3) this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_FERN) else this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_BLUE_ORCHID) } break case BiomeType.Savanna: if (surfaceBlock === BLOCK_GRASS_BLOCK) { if (vegNoise > 0) this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_TALL_GRASS) else this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_DANDELION) } break case BiomeType.MushroomFields: if (surfaceBlock === BLOCK_MYCELIUM) { if (vegNoise > 0) this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_RED_MUSHROOM) else this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_BROWN_MUSHROOM) } break case BiomeType.Mountains: if (surfaceBlock === BLOCK_GRASS_BLOCK && vegHash < 0.02) { this.setBlockLocal(blocks, lx, surfaceY + 1, lz, BLOCK_TALL_GRASS) } break } } } } // ═══════════════════════════════════════════════════════════ // STRUCTURE SEEDS // ═══════════════════════════════════════════════════════════ private placeStructures( cx: number, cz: number, blocks: Uint8Array, heightMap: Uint16Array, biomeCache: BiomeDef[] ): void { const rng = seededRandom(this.seed ^ (cx * 31337 + cz * 69105 + 0x57DE55)) // ---- Small lava pools underground ---- const lavaPoolCount = Math.floor(rng() * 2) // 0-1 per chunk for (let i = 0; i < lavaPoolCount; i++) { const lx = 2 + Math.floor(rng() * 12) const lz = 2 + Math.floor(rng() * 12) const depth = -54 + Math.floor(rng() * 30) // Y -54 to -24 const size = 2 + Math.floor(rng() * 3) // 2-4 radius for (let dx = -size; dx <= size; dx++) { for (let dz = -size; dz <= size; dz++) { if (dx * dx + dz * dz > size * size + 1) continue const nx = lx + dx const nz = lz + dz if (nx < 0 || nx >= CHUNK_WIDTH || nz < 0 || nz >= CHUNK_DEPTH) continue // Only replace stone/deepslate const existing = this.getBlockAtLocal(blocks, nx, depth, nz) if (existing === BLOCK_STONE || existing === BLOCK_DEEPSLATE) { this.setBlockLocal(blocks, nx, depth, nz, BLOCK_LAVA) } } } } // ---- Water springs on cliff faces ---- const springCount = Math.floor(rng() * 3) // 0-2 per chunk for (let i = 0; i < springCount; i++) { const lx = Math.floor(rng() * CHUNK_WIDTH) const lz = Math.floor(rng() * CHUNK_DEPTH) const biomeIdx = lx + lz * CHUNK_WIDTH const surfaceY = heightMap[biomeIdx] // Spring must be on a cliff face (surface is significantly above sea level) if (surfaceY <= SEA_LEVEL + 5) continue const springY = SEA_LEVEL + 5 + Math.floor(rng() * (surfaceY - SEA_LEVEL - 5)) const existing = this.getBlockAtLocal(blocks, lx, springY, lz) if (existing !== BLOCK_STONE && existing !== BLOCK_DEEPSLATE) continue // Check that at least one adjacent block is air (cliff face) const isCliff = (lx > 0 && this.getBlockAtLocal(blocks, lx - 1, springY, lz) === BLOCK_AIR) || (lx < CHUNK_WIDTH - 1 && this.getBlockAtLocal(blocks, lx + 1, springY, lz) === BLOCK_AIR) || (lz > 0 && this.getBlockAtLocal(blocks, lx, springY, lz - 1) === BLOCK_AIR) || (lz < CHUNK_DEPTH - 1 && this.getBlockAtLocal(blocks, lx, springY, lz + 1) === BLOCK_AIR) if (isCliff) { this.setBlockLocal(blocks, lx, springY, lz, BLOCK_WATER) } } // ---- Sand and gravel patches ---- for (let lx = 0; lx < CHUNK_WIDTH; lx += 4) { for (let lz = 0; lz < CHUNK_DEPTH; lz += 4) { const wx = cx * CHUNK_WIDTH + lx const wz = cz * CHUNK_DEPTH + lz const patchNoise = this.noisePatch(wx * 0.02, wz * 0.02) if (patchNoise > 0.65) { // Sand patch const biomeIdx = lx + lz * CHUNK_WIDTH const surfaceY = heightMap[biomeIdx] for (let dx = 0; dx < 4 && lx + dx < CHUNK_WIDTH; dx++) { for (let dz = 0; dz < 4 && lz + dz < CHUNK_DEPTH; dz++) { const nx = lx + dx const nz = lz + dz const y = surfaceY const existing = this.getBlockAtLocal(blocks, nx, y, nz) if (existing === BLOCK_DIRT || existing === BLOCK_GRASS_BLOCK) { this.setBlockLocal(blocks, nx, y, nz, BLOCK_SAND) } } } } else if (patchNoise < -0.65) { // Gravel patch const biomeIdx = lx + lz * CHUNK_WIDTH const surfaceY = heightMap[biomeIdx] for (let dx = 0; dx < 4 && lx + dx < CHUNK_WIDTH; dx++) { for (let dz = 0; dz < 4 && lz + dz < CHUNK_DEPTH; dz++) { const nx = lx + dx const nz = lz + dz const y = surfaceY const existing = this.getBlockAtLocal(blocks, nx, y, nz) if (existing === BLOCK_DIRT || existing === BLOCK_GRASS_BLOCK) { this.setBlockLocal(blocks, nx, y, nz, BLOCK_GRAVEL) } } } } // Underground gravel clusters for (let dy = 0; dy < 6; dy++) { const y = -10 + dy * 10 const gNoise = this.noisePatch(wx * 0.05 + y * 0.1, wz * 0.05 + y * 0.1) if (gNoise > 0.75 || gNoise < -0.75) { for (let dx = 0; dx < 4 && lx + dx < CHUNK_WIDTH; dx++) { for (let dz = 0; dz < 4 && lz + dz < CHUNK_DEPTH; dz++) { const nx = lx + dx const nz = lz + dz const existing = this.getBlockAtLocal(blocks, nx, y, nz) if (existing === BLOCK_STONE || existing === BLOCK_DEEPSLATE) { this.setBlockLocal(blocks, nx, y, nz, BLOCK_GRAVEL) } } } } } } } } // ═══════════════════════════════════════════════════════════ // HELPERS // ═══════════════════════════════════════════════════════════ /** * Multi-octave 2D noise */ private octave2D( noise: ReturnType, x: number, z: number, octaves: number ): number { let value = 0 let amplitude = 1 let frequency = 1 let maxValue = 0 for (let i = 0; i < octaves; i++) { value += noise(x * frequency, z * frequency) * amplitude maxValue += amplitude amplitude *= 0.5 frequency *= 2 } return value / maxValue } /** * Get block from local chunk coords (read) */ private getBlockAtLocal(blocks: Uint8Array, lx: number, y: number, lz: number): number { const ly = y - MIN_BUILD_HEIGHT if (ly < 0 || ly >= CHUNK_HEIGHT) return BLOCK_AIR const idx = lx + lz * CHUNK_WIDTH + ly * CHUNK_WIDTH * CHUNK_DEPTH if (idx < 0 || idx >= blocks.length) return BLOCK_AIR return blocks[idx] } /** * Set block at local chunk coords (write) */ private setBlockLocal(blocks: Uint8Array, lx: number, y: number, lz: number, blockId: number): void { const ly = y - MIN_BUILD_HEIGHT if (ly < 0 || ly >= CHUNK_HEIGHT) return if (lx < 0 || lx >= CHUNK_WIDTH || lz < 0 || lz >= CHUNK_DEPTH) return if (y > 319 || y < MIN_BUILD_HEIGHT) return const idx = lx + lz * CHUNK_WIDTH + ly * CHUNK_WIDTH * CHUNK_DEPTH if (idx < 0 || idx >= blocks.length) return blocks[idx] = blockId } }