minecraft-clone / src /engine /terrain /generator.ts
TomatitoToho's picture
Upload src/engine/terrain/generator.ts with huggingface_hub
e0e0f1c verified
Raw
History Blame Contribute Delete
50.8 kB
/**
* 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
}
}