Spaces:
Sleeping
Sleeping
| /** | |
| * 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<typeof createNoise2D> | |
| private noiseHumidity: ReturnType<typeof createNoise2D> | |
| // Terrain shape | |
| private noiseContinentalness: ReturnType<typeof createNoise2D> | |
| private noiseErosion: ReturnType<typeof createNoise2D> | |
| private noisePeaks: ReturnType<typeof createNoise2D> | |
| private noiseDetail: ReturnType<typeof createNoise2D> | |
| // Caves | |
| private noiseCaveSpaghetti1: ReturnType<typeof createNoise3D> | |
| private noiseCaveSpaghetti2: ReturnType<typeof createNoise3D> | |
| private noiseCaveCheese1: ReturnType<typeof createNoise3D> | |
| private noiseCaveCheese2: ReturnType<typeof createNoise3D> | |
| // Ore | |
| private noiseOre: ReturnType<typeof createNoise3D> | |
| // Structure patches | |
| private noisePatch: ReturnType<typeof createNoise2D> | |
| // Vegetation scatter | |
| private noiseVeg: ReturnType<typeof createNoise2D> | |
| 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<BiomeDef>(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<typeof createNoise2D>, | |
| 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 | |
| } | |
| } | |