/** * 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 private noiseTerrain2: ReturnType private noiseCave: ReturnType private noiseCave2: ReturnType private noiseBiome1: ReturnType private noiseBiome2: ReturnType private noiseBiome3: ReturnType private noiseOre: ReturnType private noiseGlowstone: ReturnType private noiseDetail: ReturnType private noisePillar: ReturnType 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(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 private noiseDetail: ReturnType private noiseIsland: ReturnType 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) } }