Spaces:
Sleeping
Sleeping
| /** | |
| * Dimension Terrain Generators — Nether & End | |
| * | |
| * NetherGenerator: | |
| * - Lava seas at y=32 | |
| * - Netherrack terrain with caves | |
| * - Soul sand valleys, Crimson/Warped forests, Basalt deltas | |
| * - Glowstone clusters, ores, bedrock borders | |
| * - Height range: 0-128 | |
| * | |
| * EndGenerator: | |
| * - Central island (end stone, ~50 block radius) | |
| * - Obsidian pillars of varying heights | |
| * - Void below, flat top at y=64 | |
| * - Only terrain within ~100 blocks of origin | |
| */ | |
| import { createNoise2D, createNoise3D } from 'simplex-noise' | |
| import { | |
| CHUNK_WIDTH, | |
| BLOCK_AIR, | |
| BLOCK_BEDROCK, | |
| BLOCK_NETHERRACK, | |
| BLOCK_LAVA, | |
| BLOCK_SOUL_SAND, | |
| BLOCK_SOUL_SOIL, | |
| BLOCK_BASALT, | |
| BLOCK_BLACKSTONE, | |
| BLOCK_GLOWSTONE, | |
| BLOCK_NETHER_QUARTZ_ORE, | |
| BLOCK_NETHER_GOLD_ORE, | |
| BLOCK_CRIMSON_NYLIUM, | |
| BLOCK_WARPED_NYLIUM, | |
| BLOCK_CRIMSON_STEM, | |
| BLOCK_WARPED_STEM, | |
| BLOCK_CRIMSON_FUNGUS, | |
| BLOCK_WARPED_FUNGUS, | |
| BLOCK_CRIMSON_PLANKS, | |
| BLOCK_WARPED_PLANKS, | |
| BLOCK_NETHER_WART_BLOCK, | |
| BLOCK_WARPED_WART_BLOCK, | |
| BLOCK_SHROOMLIGHT, | |
| BLOCK_MAGMA_BLOCK, | |
| BLOCK_END_STONE, | |
| BLOCK_OBSIDIAN, | |
| BLOCK_GRAVEL, | |
| } from '../constants' | |
| import type { ChunkData } from '@/types' | |
| // ═══════════════════════════════════════════════════════════════ | |
| // CONSTANTS | |
| // ═══════════════════════════════════════════════════════════════ | |
| const NETHER_HEIGHT = 128 | |
| const NETHER_LAVA_LEVEL = 32 | |
| const NETHER_CEILING = 128 | |
| const NETHER_FLOOR = 0 | |
| const END_HEIGHT = 128 | |
| const END_ISLAND_Y = 64 | |
| const END_ISLAND_RADIUS = 50 | |
| const END_TERRAIN_RADIUS = 100 | |
| // ═══════════════════════════════════════════════════════════════ | |
| // 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 | |
| } | |
| function hashCoord2(x: number, z: number, seed: number): number { | |
| let h = (seed | 0) ^ 0x5bd1e995 | |
| h = ((h ^ (x * 0x5bd1e995)) * 0x27d4eb2d) | 0 | |
| h = ((h ^ (z * 0x5bd1e995)) * 0x27d4eb2d) | 0 | |
| h ^= h >>> 16 | |
| return (h >>> 0) / 4294967296 | |
| } | |
| // ═══════════════════════════════════════════════════════════════ | |
| // NETHER BIOME TYPES | |
| // ═══════════════════════════════════════════════════════════════ | |
| enum NetherBiome { | |
| NetherWastes, | |
| SoulSandValley, | |
| CrimsonForest, | |
| WarpedForest, | |
| BasaltDeltas, | |
| } | |
| // ═══════════════════════════════════════════════════════════════ | |
| // NETHER GENERATOR | |
| // ═══════════════════════════════════════════════════════════════ | |
| export class NetherGenerator { | |
| private seed: number | |
| // Noise instances | |
| private noiseTerrain: ReturnType<typeof createNoise3D> | |
| private noiseTerrain2: ReturnType<typeof createNoise3D> | |
| private noiseCave: ReturnType<typeof createNoise3D> | |
| private noiseCave2: ReturnType<typeof createNoise3D> | |
| private noiseBiome1: ReturnType<typeof createNoise2D> | |
| private noiseBiome2: ReturnType<typeof createNoise2D> | |
| private noiseBiome3: ReturnType<typeof createNoise2D> | |
| private noiseOre: ReturnType<typeof createNoise3D> | |
| private noiseGlowstone: ReturnType<typeof createNoise3D> | |
| private noiseDetail: ReturnType<typeof createNoise2D> | |
| private noisePillar: ReturnType<typeof createNoise2D> | |
| constructor(seed: number) { | |
| this.seed = seed | |
| const rng = seededRandom(seed ^ 0xDEADBEEF) | |
| this.noiseTerrain = createNoise3D(rng) | |
| this.noiseTerrain2 = createNoise3D(rng) | |
| this.noiseCave = createNoise3D(rng) | |
| this.noiseCave2 = createNoise3D(rng) | |
| this.noiseBiome1 = createNoise2D(rng) | |
| this.noiseBiome2 = createNoise2D(rng) | |
| this.noiseBiome3 = createNoise2D(rng) | |
| this.noiseOre = createNoise3D(rng) | |
| this.noiseGlowstone = createNoise3D(rng) | |
| this.noiseDetail = createNoise2D(rng) | |
| this.noisePillar = createNoise2D(rng) | |
| } | |
| generateChunk(cx: number, cz: number): ChunkData { | |
| const blocks = new Uint8Array(CHUNK_WIDTH * NETHER_HEIGHT * CHUNK_WIDTH) | |
| const heightMap = new Uint16Array(CHUNK_WIDTH * CHUNK_WIDTH) | |
| // Phase 1: Compute biome and height map | |
| const biomeCache = new Array<NetherBiome>(CHUNK_WIDTH * CHUNK_WIDTH) | |
| for (let lx = 0; lx < CHUNK_WIDTH; lx++) { | |
| for (let lz = 0; lz < CHUNK_WIDTH; lz++) { | |
| const wx = cx * CHUNK_WIDTH + lx | |
| const wz = cz * CHUNK_WIDTH + lz | |
| const biomeIdx = lx + lz * CHUNK_WIDTH | |
| biomeCache[biomeIdx] = this.getBiome(wx, wz) | |
| // Height map = highest non-air block | |
| heightMap[biomeIdx] = 0 | |
| } | |
| } | |
| // Phase 2: Fill blocks | |
| for (let lx = 0; lx < CHUNK_WIDTH; lx++) { | |
| for (let lz = 0; lz < CHUNK_WIDTH; lz++) { | |
| const wx = cx * CHUNK_WIDTH + lx | |
| const wz = cz * CHUNK_WIDTH + lz | |
| const biomeIdx = lx + lz * CHUNK_WIDTH | |
| const biome = biomeCache[biomeIdx] | |
| for (let ly = 0; ly < NETHER_HEIGHT; ly++) { | |
| const blockId = this.getBlockAt(wx, ly, wz, biome) | |
| const idx = lx + lz * CHUNK_WIDTH + ly * CHUNK_WIDTH * CHUNK_WIDTH | |
| if (blockId !== BLOCK_AIR) { | |
| blocks[idx] = blockId | |
| heightMap[biomeIdx] = ly + 1 | |
| } | |
| } | |
| } | |
| } | |
| // Phase 3: Place glowstone clusters | |
| this.placeGlowstone(cx, cz, blocks, heightMap, biomeCache) | |
| // Phase 4: Place crimson/warped vegetation | |
| this.placeNetherVegetation(cx, cz, blocks, heightMap, biomeCache) | |
| // Phase 5: Place basalt columns in basalt deltas | |
| this.placeBasaltColumns(cx, cz, blocks, heightMap, biomeCache) | |
| return { | |
| position: { x: cx, z: cz }, | |
| blocks, | |
| heightMap, | |
| dirty: true, | |
| meshVersion: 0, | |
| } | |
| } | |
| // ─── Biome Determination ─────────────────────────────────── | |
| private getBiome(wx: number, wz: number): NetherBiome { | |
| const s = 0.004 // biome scale | |
| const v1 = this.noiseBiome1(wx * s, wz * s) | |
| const v2 = this.noiseBiome2(wx * s, wz * s) | |
| const v3 = this.noiseBiome3(wx * s * 1.5, wz * s * 1.5) | |
| // Classify into 5 biomes based on noise | |
| if (v1 < -0.3) { | |
| return NetherBiome.SoulSandValley | |
| } | |
| if (v1 > 0.3 && v2 > 0.1) { | |
| return NetherBiome.CrimsonForest | |
| } | |
| if (v1 > 0.3 && v2 <= 0.1) { | |
| return NetherBiome.WarpedForest | |
| } | |
| if (v3 > 0.4) { | |
| return NetherBiome.BasaltDeltas | |
| } | |
| return NetherBiome.NetherWastes | |
| } | |
| // ─── Block Placement ─────────────────────────────────────── | |
| private getBlockAt(wx: number, ly: number, wz: number, biome: NetherBiome): number { | |
| // Bedrock floor (y=0) and ceiling (y=127) | |
| if (ly <= 1) return BLOCK_BEDROCK | |
| if (ly >= NETHER_HEIGHT - 2) return BLOCK_BEDROCK | |
| // Random bedrock patches at floor/ceiling | |
| if (ly <= 4 && hashCoord(wx, ly, wz, this.seed) < (5 - ly) * 0.15) return BLOCK_BEDROCK | |
| if (ly >= NETHER_HEIGHT - 5 && hashCoord(wx, ly, wz, this.seed ^ 0xABCD) < (ly - (NETHER_HEIGHT - 5) + 1) * 0.15) return BLOCK_BEDROCK | |
| // Compute terrain density using 3D noise | |
| const density = this.computeDensity(wx, ly, wz, biome) | |
| // If density is below threshold, it's air (or lava) | |
| if (density < 0) { | |
| // Fill with lava below lava sea level | |
| if (ly <= NETHER_LAVA_LEVEL) { | |
| return BLOCK_LAVA | |
| } | |
| return BLOCK_AIR | |
| } | |
| // Solid terrain — determine block type based on biome and position | |
| return this.getBlockType(wx, ly, wz, biome) | |
| } | |
| private computeDensity(wx: number, ly: number, wz: number, biome: NetherBiome): number { | |
| // Base terrain shape | |
| const s = 0.02 | |
| const n1 = this.noiseTerrain(wx * s, ly * s * 0.8, wz * s) | |
| const n2 = this.noiseTerrain2(wx * s * 2, ly * s * 1.6, wz * s * 2) * 0.5 | |
| let density = n1 + n2 | |
| // Add larger-scale shape variation | |
| const largeScale = this.noiseDetail(wx * 0.005, wz * 0.005) * 0.3 | |
| density += largeScale | |
| // Carve caves | |
| const caveS = 0.04 | |
| const cave1 = this.noiseCave(wx * caveS, ly * caveS * 0.6, wz * caveS) | |
| const cave2 = this.noiseCave2(wx * caveS * 1.5, ly * caveS * 0.9, wz * caveS * 1.5) | |
| // Spaghetti caves: thin winding tunnels | |
| const spaghettiThreshold = 0.05 | |
| if (Math.abs(cave1) < spaghettiThreshold && Math.abs(cave2) < spaghettiThreshold * 1.5) { | |
| return -1 // carve out | |
| } | |
| // Cheese caves: larger spherical cavities | |
| if (cave1 * cave1 + cave2 * cave2 < 0.02) { | |
| return -1 | |
| } | |
| // Biome-specific density modifiers | |
| switch (biome) { | |
| case NetherBiome.SoulSandValley: | |
| // Slightly more open terrain | |
| density -= 0.08 | |
| break | |
| case NetherBiome.BasaltDeltas: | |
| // More jagged, denser terrain | |
| density += this.noisePillar(wx * 0.1, wz * 0.1) * 0.2 | |
| break | |
| case NetherBiome.CrimsonForest: | |
| case NetherBiome.WarpedForest: | |
| // Moderate openness | |
| density -= 0.03 | |
| break | |
| } | |
| // Height-based density: thin out near the floor ceiling and floor | |
| const floorFade = Math.min(1, ly / 10) | |
| const ceilingFade = Math.min(1, (NETHER_HEIGHT - ly) / 10) | |
| density *= floorFade * ceilingFade | |
| return density | |
| } | |
| private getBlockType(wx: number, ly: number, wz: number, biome: NetherBiome): number { | |
| // Check for ores first | |
| const oreVal = this.noiseOre(wx * 0.15, ly * 0.15, wz * 0.15) | |
| if (oreVal > 0.75) return BLOCK_NETHER_QUARTZ_ORE | |
| if (oreVal > 0.7) return BLOCK_NETHER_GOLD_ORE | |
| // Check if this is a surface block (has air above) | |
| // We'll check this via a simple heuristic | |
| const surfNoise = this.noiseTerrain(wx * 0.05, (ly + 1) * 0.05, wz * 0.05) | |
| const isNearSurface = surfNoise < 0.1 | |
| switch (biome) { | |
| case NetherBiome.SoulSandValley: | |
| // Surface: soul sand/soil, interior: soul sand/soil | |
| if (isNearSurface) { | |
| return hashCoord(wx, ly, wz, this.seed ^ 0x1234) < 0.5 ? BLOCK_SOUL_SAND : BLOCK_SOUL_SOIL | |
| } | |
| return hashCoord(wx, ly, wz, this.seed ^ 0x5678) < 0.3 ? BLOCK_SOUL_SAND : BLOCK_NETHERRACK | |
| case NetherBiome.CrimsonForest: | |
| // Surface: crimson nylium, interior: netherrack | |
| if (isNearSurface && ly > NETHER_LAVA_LEVEL) { | |
| return BLOCK_CRIMSON_NYLIUM | |
| } | |
| return BLOCK_NETHERRACK | |
| case NetherBiome.WarpedForest: | |
| // Surface: warped nylium, interior: netherrack | |
| if (isNearSurface && ly > NETHER_LAVA_LEVEL) { | |
| return BLOCK_WARPED_NYLIUM | |
| } | |
| return BLOCK_NETHERRACK | |
| case NetherBiome.BasaltDeltas: | |
| // Basalt and blackstone terrain | |
| if (isNearSurface) { | |
| return hashCoord(wx, ly, wz, this.seed ^ 0xBEEF) < 0.4 ? BLOCK_BLACKSTONE : BLOCK_BASALT | |
| } | |
| return hashCoord(wx, ly, wz, this.seed ^ 0xCAFE) < 0.3 ? BLOCK_BLACKSTONE : BLOCK_BASALT | |
| case NetherBiome.NetherWastes: | |
| default: | |
| // Standard netherrack with occasional gravel and magma | |
| if (isNearSurface && ly <= NETHER_LAVA_LEVEL + 2) { | |
| // Magma near lava | |
| if (hashCoord(wx, ly, wz, this.seed ^ 0xF1E0) < 0.1) { | |
| return BLOCK_MAGMA_BLOCK | |
| } | |
| } | |
| if (hashCoord(wx, ly, wz, this.seed ^ 0x6AA0) < 0.02) { | |
| return BLOCK_GRAVEL | |
| } | |
| return BLOCK_NETHERRACK | |
| } | |
| } | |
| // ─── Glowstone Clusters ──────────────────────────────────── | |
| private placeGlowstone( | |
| cx: number, cz: number, | |
| blocks: Uint8Array, heightMap: Uint16Array, | |
| _biomeCache: NetherBiome[] | |
| ): void { | |
| // Try to place glowstone clusters hanging from ceiling | |
| const rng = seededRandom((cx * 3419 + cz * 7919) ^ this.seed) | |
| for (let attempt = 0; attempt < 4; attempt++) { | |
| if (rng() > 0.4) continue | |
| const lx = Math.floor(rng() * 14) + 1 | |
| const lz = Math.floor(rng() * 14) + 1 | |
| const startHeight = NETHER_HEIGHT - 5 - Math.floor(rng() * 10) | |
| // Grow a cluster downward | |
| this.growGlowstone(blocks, lx, startHeight, lz, rng) | |
| } | |
| } | |
| private growGlowstone( | |
| blocks: Uint8Array, lx: number, ly: number, lz: number, | |
| rng: () => number, depth: number = 0 | |
| ): void { | |
| if (depth > 8 || ly < 5 || lx < 0 || lx >= CHUNK_WIDTH || lz < 0 || lz >= CHUNK_WIDTH) return | |
| const idx = lx + lz * CHUNK_WIDTH + ly * CHUNK_WIDTH * CHUNK_WIDTH | |
| if (blocks[idx] !== BLOCK_AIR && blocks[idx] !== 0) return | |
| blocks[idx] = BLOCK_GLOWSTONE | |
| // Continue growing downward and sometimes sideways | |
| const dir = rng() | |
| if (dir < 0.5) { | |
| this.growGlowstone(blocks, lx, ly - 1, lz, rng, depth + 1) | |
| } else if (dir < 0.65) { | |
| this.growGlowstone(blocks, lx + 1, ly - 1, lz, rng, depth + 1) | |
| } else if (dir < 0.8) { | |
| this.growGlowstone(blocks, lx - 1, ly - 1, lz, rng, depth + 1) | |
| } else if (dir < 0.9) { | |
| this.growGlowstone(blocks, lx, ly - 1, lz + 1, rng, depth + 1) | |
| } else { | |
| this.growGlowstone(blocks, lx, ly - 1, lz - 1, rng, depth + 1) | |
| } | |
| // Sometimes branch | |
| if (rng() < 0.3 && depth < 5) { | |
| const sideX = lx + (rng() < 0.5 ? 1 : -1) | |
| const sideZ = lz + (rng() < 0.5 ? 1 : -1) | |
| this.growGlowstone(blocks, sideX, ly, sideZ, rng, depth + 2) | |
| } | |
| } | |
| // ─── Nether Vegetation ───────────────────────────────────── | |
| private placeNetherVegetation( | |
| cx: number, cz: number, | |
| blocks: Uint8Array, heightMap: Uint16Array, | |
| biomeCache: NetherBiome[] | |
| ): void { | |
| for (let lx = 0; lx < CHUNK_WIDTH; lx++) { | |
| for (let lz = 0; lz < CHUNK_WIDTH; lz++) { | |
| const biomeIdx = lx + lz * CHUNK_WIDTH | |
| const biome = biomeCache[biomeIdx] | |
| const wx = cx * CHUNK_WIDTH + lx | |
| const wz = cz * CHUNK_WIDTH + lz | |
| // Find the surface (first air from top down) | |
| let surfaceY = -1 | |
| for (let ly = NETHER_HEIGHT - 3; ly > 0; ly--) { | |
| const idx = lx + lz * CHUNK_WIDTH + ly * CHUNK_WIDTH * CHUNK_WIDTH | |
| if (blocks[idx] !== BLOCK_AIR && blocks[idx] !== 0 && blocks[idx] !== BLOCK_LAVA) { | |
| surfaceY = ly | |
| break | |
| } | |
| } | |
| if (surfaceY < 0 || surfaceY >= NETHER_HEIGHT - 2) continue | |
| const aboveIdx = lx + lz * CHUNK_WIDTH + (surfaceY + 1) * CHUNK_WIDTH * CHUNK_WIDTH | |
| if (blocks[aboveIdx] !== BLOCK_AIR && blocks[aboveIdx] !== 0) continue | |
| const surfaceBlock = blocks[lx + lz * CHUNK_WIDTH + surfaceY * CHUNK_WIDTH * CHUNK_WIDTH] | |
| const h = hashCoord2(wx, wz, this.seed ^ 0x7E60) | |
| switch (biome) { | |
| case NetherBiome.CrimsonForest: | |
| if (surfaceBlock === BLOCK_CRIMSON_NYLIUM) { | |
| if (h < 0.04) { | |
| // Crimson fungus | |
| blocks[aboveIdx] = BLOCK_CRIMSON_FUNGUS | |
| } else if (h < 0.06) { | |
| // Crimson stem (small tree) | |
| this.placeCrimsonStem(blocks, lx, surfaceY + 1, lz) | |
| } | |
| } | |
| break | |
| case NetherBiome.WarpedForest: | |
| if (surfaceBlock === BLOCK_WARPED_NYLIUM) { | |
| if (h < 0.04) { | |
| // Warped fungus | |
| blocks[aboveIdx] = BLOCK_WARPED_FUNGUS | |
| } else if (h < 0.06) { | |
| // Warped stem (small tree) | |
| this.placeWarpedStem(blocks, lx, surfaceY + 1, lz) | |
| } | |
| } | |
| break | |
| case NetherBiome.NetherWastes: | |
| // Occasional crimson/warped in nether wastes | |
| if (h < 0.005) { | |
| blocks[aboveIdx] = BLOCK_CRIMSON_FUNGUS | |
| } else if (h < 0.01) { | |
| blocks[aboveIdx] = BLOCK_WARPED_FUNGUS | |
| } | |
| break | |
| } | |
| } | |
| } | |
| } | |
| private placeCrimsonStem(blocks: Uint8Array, lx: number, baseY: number, lz: number): void { | |
| const height = 3 + Math.floor(hashCoord(lx, baseY, lz, this.seed ^ 0xC1230) * 4) | |
| for (let dy = 0; dy < height && baseY + dy < NETHER_HEIGHT - 2; dy++) { | |
| const idx = lx + lz * CHUNK_WIDTH + (baseY + dy) * CHUNK_WIDTH * CHUNK_WIDTH | |
| blocks[idx] = BLOCK_CRIMSON_STEM | |
| } | |
| // Wart block on top | |
| const topY = baseY + height - 1 | |
| if (topY < NETHER_HEIGHT - 2) { | |
| const topIdx = lx + lz * CHUNK_WIDTH + topY * CHUNK_WIDTH * CHUNK_WIDTH | |
| blocks[topIdx] = BLOCK_NETHER_WART_BLOCK | |
| // Shroomlight | |
| if (topY + 1 < NETHER_HEIGHT - 2) { | |
| const shroomIdx = lx + lz * CHUNK_WIDTH + (topY + 1) * CHUNK_WIDTH * CHUNK_WIDTH | |
| if (blocks[shroomIdx] === BLOCK_AIR || blocks[shroomIdx] === 0) { | |
| blocks[shroomIdx] = BLOCK_SHROOMLIGHT | |
| } | |
| } | |
| } | |
| } | |
| private placeWarpedStem(blocks: Uint8Array, lx: number, baseY: number, lz: number): void { | |
| const height = 3 + Math.floor(hashCoord(lx, baseY, lz, this.seed ^ 0xA4560) * 4) | |
| for (let dy = 0; dy < height && baseY + dy < NETHER_HEIGHT - 2; dy++) { | |
| const idx = lx + lz * CHUNK_WIDTH + (baseY + dy) * CHUNK_WIDTH * CHUNK_WIDTH | |
| blocks[idx] = BLOCK_WARPED_STEM | |
| } | |
| // Warped wart block on top | |
| const topY = baseY + height - 1 | |
| if (topY < NETHER_HEIGHT - 2) { | |
| const topIdx = lx + lz * CHUNK_WIDTH + topY * CHUNK_WIDTH * CHUNK_WIDTH | |
| blocks[topIdx] = BLOCK_WARPED_WART_BLOCK | |
| // Shroomlight | |
| if (topY + 1 < NETHER_HEIGHT - 2) { | |
| const shroomIdx = lx + lz * CHUNK_WIDTH + (topY + 1) * CHUNK_WIDTH * CHUNK_WIDTH | |
| if (blocks[shroomIdx] === BLOCK_AIR || blocks[shroomIdx] === 0) { | |
| blocks[shroomIdx] = BLOCK_SHROOMLIGHT | |
| } | |
| } | |
| } | |
| } | |
| // ─── Basalt Columns ──────────────────────────────────────── | |
| private placeBasaltColumns( | |
| cx: number, cz: number, | |
| blocks: Uint8Array, heightMap: Uint16Array, | |
| biomeCache: NetherBiome[] | |
| ): void { | |
| for (let lx = 0; lx < CHUNK_WIDTH; lx++) { | |
| for (let lz = 0; lz < CHUNK_WIDTH; lz++) { | |
| const biomeIdx = lx + lz * CHUNK_WIDTH | |
| const biome = biomeCache[biomeIdx] | |
| if (biome !== NetherBiome.BasaltDeltas) continue | |
| const wx = cx * CHUNK_WIDTH + lx | |
| const wz = cz * CHUNK_WIDTH + lz | |
| const h = hashCoord2(wx, wz, this.seed ^ 0xB4A40) | |
| // Occasionally add basalt columns/pillars | |
| if (h < 0.08) { | |
| const columnHeight = 5 + Math.floor(hashCoord(wx, 32, wz, this.seed ^ 0xC0A0) * 20) | |
| for (let ly = NETHER_LAVA_LEVEL + 1; ly < NETHER_LAVA_LEVEL + 1 + columnHeight && ly < NETHER_HEIGHT - 3; ly++) { | |
| const idx = lx + lz * CHUNK_WIDTH + ly * CHUNK_WIDTH * CHUNK_WIDTH | |
| if (blocks[idx] === BLOCK_AIR || blocks[idx] === 0) { | |
| blocks[idx] = BLOCK_BASALT | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // ═══════════════════════════════════════════════════════════════ | |
| // END GENERATOR | |
| // ═══════════════════════════════════════════════════════════════ | |
| export class EndGenerator { | |
| private seed: number | |
| // Noise instances | |
| private noiseTerrain: ReturnType<typeof createNoise2D> | |
| private noiseDetail: ReturnType<typeof createNoise2D> | |
| private noiseIsland: ReturnType<typeof createNoise2D> | |
| constructor(seed: number) { | |
| this.seed = seed | |
| const rng = seededRandom(seed ^ 0xEDEDD00) | |
| this.noiseTerrain = createNoise2D(rng) | |
| this.noiseDetail = createNoise2D(rng) | |
| this.noiseIsland = createNoise2D(rng) | |
| } | |
| generateChunk(cx: number, cz: number): ChunkData { | |
| const blocks = new Uint8Array(CHUNK_WIDTH * END_HEIGHT * CHUNK_WIDTH) | |
| const heightMap = new Uint16Array(CHUNK_WIDTH * CHUNK_WIDTH) | |
| // Calculate world coordinates for chunk origin | |
| const chunkOriginX = cx * CHUNK_WIDTH | |
| const chunkOriginZ = cz * CHUNK_WIDTH | |
| // Check if any part of this chunk is within the island radius | |
| const minDist = this.minDistToOrigin(chunkOriginX, chunkOriginZ, CHUNK_WIDTH) | |
| if (minDist > END_TERRAIN_RADIUS + 20) { | |
| // Entirely outside the island — return empty chunk | |
| return { | |
| position: { x: cx, z: cz }, | |
| blocks, | |
| heightMap, | |
| dirty: true, | |
| meshVersion: 0, | |
| } | |
| } | |
| // Phase 1: Generate terrain | |
| for (let lx = 0; lx < CHUNK_WIDTH; lx++) { | |
| for (let lz = 0; lz < CHUNK_WIDTH; lz++) { | |
| const wx = chunkOriginX + lx | |
| const wz = chunkOriginZ + lz | |
| const biomeIdx = lx + lz * CHUNK_WIDTH | |
| const distFromOrigin = Math.sqrt(wx * wx + wz * wz) | |
| // Only generate terrain within the island radius | |
| if (distFromOrigin > END_TERRAIN_RADIUS) { | |
| heightMap[biomeIdx] = 0 | |
| continue | |
| } | |
| // Compute island height | |
| const islandHeight = this.computeIslandHeight(wx, wz, distFromOrigin) | |
| for (let ly = 0; ly < END_HEIGHT; ly++) { | |
| const idx = lx + lz * CHUNK_WIDTH + ly * CHUNK_WIDTH * CHUNK_WIDTH | |
| const blockId = this.getBlockAt(wx, ly, wz, distFromOrigin, islandHeight) | |
| if (blockId !== BLOCK_AIR) { | |
| blocks[idx] = blockId | |
| heightMap[biomeIdx] = ly + 1 | |
| } | |
| } | |
| } | |
| } | |
| // Phase 2: Place obsidian pillars | |
| this.placeObsidianPillars(cx, cz, blocks, heightMap) | |
| return { | |
| position: { x: cx, z: cz }, | |
| blocks, | |
| heightMap, | |
| dirty: true, | |
| meshVersion: 0, | |
| } | |
| } | |
| // ─── Island Height ───────────────────────────────────────── | |
| private computeIslandHeight(wx: number, wz: number, distFromOrigin: number): number { | |
| // Main island is a flat disc at y=64 with some noise variation | |
| const baseY = END_ISLAND_Y | |
| // Taper the edges | |
| if (distFromOrigin > END_ISLAND_RADIUS) { | |
| // Edge falloff | |
| const edgeDist = (distFromOrigin - END_ISLAND_RADIUS) / (END_TERRAIN_RADIUS - END_ISLAND_RADIUS) | |
| if (edgeDist >= 1) return 0 | |
| return baseY * (1 - edgeDist * edgeDist) | |
| } | |
| // Add some subtle height variation on the island surface | |
| const noise = this.noiseTerrain(wx * 0.02, wz * 0.02) * 4 | |
| const detail = this.noiseDetail(wx * 0.05, wz * 0.05) * 2 | |
| return baseY + noise + detail | |
| } | |
| // ─── Block Placement ─────────────────────────────────────── | |
| private getBlockAt( | |
| wx: number, ly: number, wz: number, | |
| distFromOrigin: number, islandHeight: number | |
| ): number { | |
| if (islandHeight <= 0) return BLOCK_AIR | |
| // The island is a solid mass of end stone from y=0 up to the island height | |
| // Only the top few layers are end stone; below is also end stone but with | |
| // some variation for visual interest | |
| if (ly > Math.floor(islandHeight)) return BLOCK_AIR | |
| // Top surface is end stone | |
| // Interior is also end stone, with slight variation | |
| // Bottom surface gets some subtle shape | |
| // Check if near the bottom of the island | |
| const bottomY = Math.max(0, END_ISLAND_Y - 8 - Math.floor(this.noiseIsland(wx * 0.03, wz * 0.03) * 4)) | |
| if (ly < bottomY) return BLOCK_AIR | |
| // Edge tapering | |
| if (distFromOrigin > END_ISLAND_RADIUS) { | |
| const edgeFactor = 1 - (distFromOrigin - END_ISLAND_RADIUS) / (END_TERRAIN_RADIUS - END_ISLAND_RADIUS) | |
| const effectiveTop = bottomY + (islandHeight - bottomY) * edgeFactor | |
| if (ly > Math.floor(effectiveTop)) return BLOCK_AIR | |
| } | |
| return BLOCK_END_STONE | |
| } | |
| // ─── Obsidian Pillars ────────────────────────────────────── | |
| private placeObsidianPillars( | |
| cx: number, cz: number, | |
| blocks: Uint8Array, heightMap: Uint16Array | |
| ): void { | |
| // Deterministic pillar positions based on seed | |
| // Pillars are scattered within ~40 blocks of origin | |
| const pillarSeed = this.seed ^ 0x71AA400 | |
| const rng = seededRandom(pillarSeed) | |
| // Generate ~10 pillar positions | |
| const pillarCount = 10 | |
| for (let i = 0; i < pillarCount; i++) { | |
| const pillarX = Math.floor(rng() * 80) - 40 | |
| const pillarZ = Math.floor(rng() * 80) - 40 | |
| const pillarRadius = Math.floor(rng() * 2) + 2 // 2-3 block radius | |
| const pillarHeight = Math.floor(rng() * 10) + 5 // 5-15 blocks tall | |
| // Check if this pillar intersects with this chunk | |
| const chunkMinX = cx * CHUNK_WIDTH | |
| const chunkMaxX = chunkMinX + CHUNK_WIDTH - 1 | |
| const chunkMinZ = cz * CHUNK_WIDTH | |
| const chunkMaxZ = chunkMinZ + CHUNK_WIDTH - 1 | |
| // Quick bounds check | |
| if (pillarX + pillarRadius < chunkMinX || pillarX - pillarRadius > chunkMaxX) continue | |
| if (pillarZ + pillarRadius < chunkMinZ || pillarZ - pillarRadius > chunkMaxZ) continue | |
| // Find the surface height at the pillar center | |
| const centerLX = pillarX - chunkMinX | |
| const centerLZ = pillarZ - chunkMinZ | |
| // Only place if we can find the surface | |
| let surfaceY = 0 | |
| if (centerLX >= 0 && centerLX < CHUNK_WIDTH && centerLZ >= 0 && centerLZ < CHUNK_WIDTH) { | |
| surfaceY = heightMap[centerLX + centerLZ * CHUNK_WIDTH] | |
| } else { | |
| // Compute surface height for the pillar center | |
| surfaceY = Math.floor(this.computeIslandHeight(pillarX, pillarZ, Math.sqrt(pillarX * pillarX + pillarZ * pillarZ))) | |
| } | |
| if (surfaceY <= 0) continue | |
| // Place the pillar | |
| for (let dx = -pillarRadius; dx <= pillarRadius; dx++) { | |
| for (let dz = -pillarRadius; dz <= pillarRadius; dz++) { | |
| if (dx * dx + dz * dz > pillarRadius * pillarRadius) continue | |
| const lx = pillarX + dx - chunkMinX | |
| const lz = pillarZ + dz - chunkMinZ | |
| if (lx < 0 || lx >= CHUNK_WIDTH || lz < 0 || lz >= CHUNK_WIDTH) continue | |
| for (let dy = 0; dy < pillarHeight; dy++) { | |
| const y = surfaceY + dy | |
| if (y >= END_HEIGHT) break | |
| const idx = lx + lz * CHUNK_WIDTH + y * CHUNK_WIDTH * CHUNK_WIDTH | |
| blocks[idx] = BLOCK_OBSIDIAN | |
| if (y + 1 > heightMap[lx + lz * CHUNK_WIDTH]) { | |
| heightMap[lx + lz * CHUNK_WIDTH] = y + 1 | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // ─── Utility ─────────────────────────────────────────────── | |
| private minDistToOrigin(chunkMinX: number, chunkMinZ: number, chunkSize: number): number { | |
| // Find the minimum distance from origin to any point in the chunk | |
| const nearestX = Math.max(chunkMinX, Math.min(0, chunkMinX + chunkSize - 1)) | |
| const nearestZ = Math.max(chunkMinZ, Math.min(0, chunkMinZ + chunkSize - 1)) | |
| return Math.sqrt(nearestX * nearestX + nearestZ * nearestZ) | |
| } | |
| } | |