// fills a chunk with the overworld: the terrain's density sampled on a coarse grid and interpolated (minecraft's // noise cells), caves cut out of it, water up to sea level, then biomes and their surface blocks import { CHUNK_AREA, CHUNK_HEIGHT, CHUNK_SIZE, SEA_LEVEL } from "$/common/constants.ts"; import { OctaveNoise2D } from "./noise.ts"; import { OverworldTerrain, TerrainColumn } from "./terrain.ts"; import { BIOME_TREES, grow_tree, tree_sites } from "./trees.ts"; // density is sampled every CELL_WIDTH blocks across and CELL_HEIGHT up, caves every CAVE_CELL_HEIGHT up since // tunnels are thinner than terrain features const CELL_WIDTH = 4; const CELL_HEIGHT = 8; const CAVE_CELL_HEIGHT = 4; const CORNERS = CHUNK_SIZE / CELL_WIDTH + 1; const TERRAIN_LEVELS = CHUNK_HEIGHT / CELL_HEIGHT + 1; const CAVE_LEVELS = CHUNK_HEIGHT / CAVE_CELL_HEIGHT + 1; // surface blocks only go this far below the ground's height, so cave floors stay stone const SURFACE_REACH = 20; // a column is a cliff when its ground is this much higher or lower than a neighbor's const STEEP = 3.5; // snow covers the tops of everything above this, give or take const SNOW_LINE = SEA_LEVEL + 112; // the blocks the overworld is made of, as numeric ids export interface OverworldBlocks { stone: number; dirt: number; grass: number; sand: number; snow: number; water: number; log: number; leaves: number; } type Palette = "stone" | "dirt" | "grass" | "sand" | "snow"; // how a biome covers its ground, like minecraft's surface rules interface Surface { top: Palette; filler: Palette; filler_depth: number; // the ground's cover under water underwater_top: Palette; underwater_filler: Palette; // cliffs show bare stone instead of top bare_cliffs: boolean; // stripes of sand, stone and dirt down the cliffs, like badlands strata?: boolean; } const GRASSY: Surface = { top: "grass", filler: "dirt", filler_depth: 3, underwater_top: "dirt", underwater_filler: "dirt", bare_cliffs: true, }; const SANDY: Surface = { top: "sand", filler: "sand", filler_depth: 4, underwater_top: "sand", underwater_filler: "sand", bare_cliffs: false, }; const SNOWY: Surface = { ...GRASSY, top: "snow" }; const STONY: Surface = { ...GRASSY, top: "stone", filler: "stone", underwater_top: "stone", underwater_filler: "stone", }; const SEA_FLOOR: Surface = { ...SANDY, filler_depth: 3 }; // every biome the overworld has, and its surface. names follow minecraft's and terralith's export const BIOMES: Record = { "bworld:deep_ocean": SEA_FLOOR, "bworld:deep_frozen_ocean": SEA_FLOOR, "bworld:deep_lukewarm_ocean": SEA_FLOOR, "bworld:ocean": SEA_FLOOR, "bworld:frozen_ocean": SEA_FLOOR, "bworld:warm_ocean": SEA_FLOOR, "bworld:river": { ...GRASSY, underwater_top: "sand", underwater_filler: "sand" }, "bworld:frozen_river": { ...SNOWY, underwater_top: "sand", underwater_filler: "sand" }, "bworld:beach": SANDY, "bworld:snowy_beach": { ...SANDY, top: "snow" }, "bworld:stony_shore": STONY, "bworld:plains": GRASSY, "bworld:meadow": GRASSY, "bworld:forest": GRASSY, "bworld:dark_forest": GRASSY, "bworld:swamp": { ...GRASSY, bare_cliffs: false }, "bworld:taiga": GRASSY, "bworld:snowy_plains": SNOWY, "bworld:snowy_taiga": SNOWY, "bworld:savanna": GRASSY, "bworld:jungle": GRASSY, "bworld:desert": SANDY, "bworld:alpine_highlands": GRASSY, "bworld:snowy_slopes": SNOWY, "bworld:stony_peaks": STONY, "bworld:jagged_peaks": { ...STONY, top: "snow" }, "bworld:frozen_peaks": { ...SNOWY, filler: "stone" }, "bworld:yosemite_cliffs": GRASSY, "bworld:snowy_cliffs": SNOWY, "bworld:painted_mountains": { ...SANDY, filler_depth: 2, bare_cliffs: true, strata: true }, "bworld:stony_spires": GRASSY, "bworld:shattered_savanna": GRASSY, "bworld:skylands": GRASSY, }; // picks a column's biome from its climate and shape, like minecraft's multi noise biome source export function pick_biome(column: TerrainColumn, surface_y: number, steep: boolean): string { const { continentalness: c, erosion: e, pv, temperature: t, humidity: h } = column.climate; const frozen = t < -0.65; const cold = t < -0.3; const warm = t > 0.2; const hot = t > 0.55; if (column.island && surface_y >= column.island.bottom) { return "bworld:skylands"; } if (surface_y < SEA_LEVEL - 1) { if (pv < -0.7 && c > -0.12) return frozen ? "bworld:frozen_river" : "bworld:river"; if (c < -0.45) { return frozen ? "bworld:deep_frozen_ocean" : hot ? "bworld:deep_lukewarm_ocean" : "bworld:deep_ocean"; } return frozen ? "bworld:frozen_ocean" : hot ? "bworld:warm_ocean" : "bworld:ocean"; } if (c < -0.04 && surface_y <= SEA_LEVEL + 4) { return steep ? "bworld:stony_shore" : cold ? "bworld:snowy_beach" : "bworld:beach"; } if (pv < -0.8 && c > -0.12 && surface_y <= SEA_LEVEL + 1) { return frozen ? "bworld:frozen_river" : "bworld:river"; } const above = surface_y - SEA_LEVEL; if (above > 105) { if (cold) return "bworld:frozen_peaks"; return column.jaggedness > 0.35 ? "bworld:jagged_peaks" : "bworld:stony_peaks"; } if (column.plateau > 0.5) { if (hot && h < 0.1) return "bworld:painted_mountains"; return cold ? "bworld:snowy_cliffs" : "bworld:yosemite_cliffs"; } if (column.shattered > 0.5) { return warm ? "bworld:shattered_savanna" : "bworld:stony_spires"; } if (above > 60) { return cold ? "bworld:snowy_slopes" : "bworld:alpine_highlands"; } if (above > 30 && e > -0.2 && !cold && h > -0.3) { return "bworld:meadow"; } if (frozen) return h > 0 ? "bworld:snowy_taiga" : "bworld:snowy_plains"; if (cold) return h > 0 ? "bworld:taiga" : "bworld:plains"; if (hot) return h < -0.2 ? "bworld:desert" : h < 0.4 ? "bworld:savanna" : "bworld:jungle"; if (warm) return h < -0.3 ? "bworld:savanna" : h > 0.6 ? "bworld:jungle" : "bworld:forest"; if (h > 0.5 && e > 0.4) return "bworld:swamp"; return h < -0.35 ? "bworld:plains" : h < 0.45 ? "bworld:forest" : "bworld:dark_forest"; } interface SeedGenerators { terrain: OverworldTerrain; snow_line: OctaveNoise2D; strata: OctaveNoise2D; } const generators = new Map(); function generators_for(seed: string): SeedGenerators { let found = generators.get(seed); if (!found) { found = { terrain: new OverworldTerrain(seed), snow_line: new OctaveNoise2D(seed, "snow_line", 96, [1, 1]), strata: new OctaveNoise2D(seed, "strata", 128, [1]), }; generators.set(seed, found); } return found; } // fills blocks (indexed y * CHUNK_AREA + z * CHUNK_SIZE + x) and each column's surface height and biome. // blocks trees put in other chunks go to spill export function generate_overworld( blocks: Uint32Array, heights: Int32Array, biomes: string[], chunk_x: number, chunk_z: number, seed: string, ids: OverworldBlocks, spill: (x: number, y: number, z: number, block: number) => void, ) { const { terrain, snow_line, strata } = generators_for(seed); const x0 = chunk_x * CHUNK_SIZE; const z0 = chunk_z * CHUNK_SIZE; // density at the corners of every cell const corner_columns: TerrainColumn[] = []; for (let cz = 0; cz < CORNERS; cz++) { for (let cx = 0; cx < CORNERS; cx++) { corner_columns.push(terrain.column(x0 + cx * CELL_WIDTH, z0 + cz * CELL_WIDTH)); } } const solid = new Float32Array(CORNERS * CORNERS * TERRAIN_LEVELS); const caves = new Float32Array(CORNERS * CORNERS * CAVE_LEVELS); for (let i = 0; i < corner_columns.length; i++) { const column = corner_columns[i]; const x = x0 + (i % CORNERS) * CELL_WIDTH; const z = z0 + Math.floor(i / CORNERS) * CELL_WIDTH; for (let level = 0; level < TERRAIN_LEVELS; level++) { solid[i * TERRAIN_LEVELS + level] = terrain.density(column, x, level * CELL_HEIGHT, z); } // caves only matter below the ground, skip the sky above it const cave_top = column.height + CAVE_CELL_HEIGHT; for (let level = 0; level < CAVE_LEVELS; level++) { const y = level * CAVE_CELL_HEIGHT; caves[i * CAVE_LEVELS + level] = y > cave_top ? 1 : terrain.cave(column, x, y, z); } } // every column of the chunk plus a ring around it, for how steep the ground is const columns: TerrainColumn[] = []; const ring = CHUNK_SIZE + 2; for (let z = -1; z <= CHUNK_SIZE; z++) { for (let x = -1; x <= CHUNK_SIZE; x++) { columns.push(terrain.column(x0 + x, z0 + z)); } } const column_at = (x: number, z: number) => columns[(z + 1) * ring + x + 1]; for (let z = 0; z < CHUNK_SIZE; z++) { for (let x = 0; x < CHUNK_SIZE; x++) { fill_column(x, z); } } grow_trees(); function grow_trees() { const place = (x: number, y: number, z: number, block: "log" | "leaves") => { if (y < 0 || y >= CHUNK_HEIGHT) return; const lx = x - x0; const lz = z - z0; if (lx < 0 || lx >= CHUNK_SIZE || lz < 0 || lz >= CHUNK_SIZE) { // the other chunk only takes it where it has air spill(x, y, z, ids[block]); return; } const i = y * CHUNK_AREA + lz * CHUNK_SIZE + lx; const current = blocks[i]; if (current === 0 || (block === "log" && current === ids.leaves)) { blocks[i] = ids[block]; } }; for (const site of tree_sites(seed, chunk_x, chunk_z)) { const column = (site.z - z0) * CHUNK_SIZE + (site.x - x0); const trees = BIOME_TREES[biomes[column]]; if (!trees || site.rng.next() >= trees.chance) continue; // on soil with air above it, so never under water or on bare rock const ground_y = heights[column]; const ground = blocks[ground_y * CHUNK_AREA + column]; const above = blocks[(ground_y + 1) * CHUNK_AREA + column]; if ((ground !== ids.grass && ground !== ids.dirt && ground !== ids.snow) || above !== 0) continue; const kind = trees.kinds[Math.floor(site.rng.next() * trees.kinds.length)]; grow_tree(kind, site.x, ground_y + 1, site.z, site.rng, place); } } function fill_column(x: number, z: number) { const cell_x = Math.min(Math.floor(x / CELL_WIDTH), CORNERS - 2); const cell_z = Math.min(Math.floor(z / CELL_WIDTH), CORNERS - 2); const tx = (x - cell_x * CELL_WIDTH) / CELL_WIDTH; const tz = (z - cell_z * CELL_WIDTH) / CELL_WIDTH; const c00 = cell_z * CORNERS + cell_x; const c10 = c00 + 1; const c01 = c00 + CORNERS; const c11 = c01 + 1; const w00 = (1 - tx) * (1 - tz); const w10 = tx * (1 - tz); const w01 = (1 - tx) * tz; const w11 = tx * tz; // one column through the corner grid, blended between its four corners const blend = (grid: Float32Array, levels: number, level: number) => grid[c00 * levels + level] * w00 + grid[c10 * levels + level] * w10 + grid[c01 * levels + level] * w01 + grid[c11 * levels + level] * w11; const is_solid = new Uint8Array(CHUNK_HEIGHT); for (let y = 0; y < CHUNK_HEIGHT; y++) { const level = Math.min(Math.floor(y / CELL_HEIGHT), TERRAIN_LEVELS - 2); const t = (y - level * CELL_HEIGHT) / CELL_HEIGHT; let density = blend(solid, TERRAIN_LEVELS, level) * (1 - t) + blend(solid, TERRAIN_LEVELS, level + 1) * t; if (density > 0) { const cave_level = Math.min(Math.floor(y / CAVE_CELL_HEIGHT), CAVE_LEVELS - 2); const ct = (y - cave_level * CAVE_CELL_HEIGHT) / CAVE_CELL_HEIGHT; const cave = blend(caves, CAVE_LEVELS, cave_level) * (1 - ct) + blend(caves, CAVE_LEVELS, cave_level + 1) * ct; density = Math.min(density, cave); } is_solid[y] = density > 0 ? 1 : 0; } const column = column_at(x, z); let surface_y = CHUNK_HEIGHT - 1; while (surface_y > 0 && !is_solid[surface_y]) surface_y--; const ground = column.height; const neighbors = [column_at(x - 1, z), column_at(x + 1, z), column_at(x, z - 1), column_at(x, z + 1)]; const steep = neighbors.some((neighbor) => Math.abs(neighbor.height - ground) >= STEEP); const biome = pick_biome(column, surface_y, steep); const surface = BIOMES[biome]; const snow_y = SNOW_LINE + snow_line.sample(x0 + x, z0 + z) * 10; const strata_offset = strata.sample(x0 + x, z0 + z) * 4; const island_bottom = column.island ? column.island.bottom - 2 : Infinity; heights[z * CHUNK_SIZE + x] = surface_y; biomes[z * CHUNK_SIZE + x] = biome; const index = (y: number) => y * CHUNK_AREA + z * CHUNK_SIZE + x; // solid blocks since the last air going down, 0 is a block with air on top let depth = -1; // nothing but air (and sky islands) above so far: water fills it up to sea level let open_sky = true; // whether the ground's surface is under water, for its cover let underwater = false; for (let y = CHUNK_HEIGHT - 1; y >= 0; y--) { if (!is_solid[y]) { depth = -1; if (open_sky && y < SEA_LEVEL) { blocks[index(y)] = ids.water; } continue; } depth += 1; const in_island = y >= island_bottom; if (open_sky && !in_island) { open_sky = false; underwater = y < SEA_LEVEL - 1; } // surface rules reach the ground and anything above it, not cave floors let block: Palette = "stone"; if (y >= ground - SURFACE_REACH || in_island) { const wet = underwater && !in_island; if (surface.strata && depth > 0 && y > SEA_LEVEL) { block = strata_block(y + strata_offset); } else if (depth === 0) { if (wet) block = surface.underwater_top; else if (y >= snow_y && !in_island) block = steep ? "stone" : "snow"; else block = steep && surface.bare_cliffs ? "stone" : surface.top; } else if (depth <= surface.filler_depth) { block = wet ? surface.underwater_filler : surface.filler; } } blocks[index(y)] = ids[block]; } } } // the bands down a painted mountain's cliffs function strata_block(y: number): Palette { const band = ((Math.floor(y / 3) % 6) + 6) % 6; return band === 1 || band === 4 ? "stone" : band === 3 ? "dirt" : "sand"; }