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