193 lines
7.2 KiB
TypeScript
193 lines
7.2 KiB
TypeScript
// trees, part of the terrain pass. where they grow can't depend on which chunk generates first, so it only
|
|
// depends on the seed: every CELL x CELL cell has one candidate spot and a random priority, and a candidate becomes a
|
|
// tree only if no other candidate within MIN_DISTANCE has a higher priority (then its biome may still say no). that
|
|
// spreads trees out like poisson disk sampling: never closer than MIN_DISTANCE, but without lining up in a grid.
|
|
// a tree is placed by the chunk its trunk is in, leaves that reach into the next chunk go through the spills like any
|
|
// other block there
|
|
import { Alea } from "@paulaboks/rng";
|
|
import { CHUNK_HEIGHT, CHUNK_SIZE } from "$/common/constants.ts";
|
|
|
|
const CELL = 4;
|
|
// trunks are at least this far apart
|
|
const MIN_DISTANCE = 4;
|
|
// how many cells out a candidate can be and still be within MIN_DISTANCE
|
|
const REACH = Math.ceil(MIN_DISTANCE / CELL);
|
|
// the widest a canopy layer gets
|
|
const MAX_CANOPY = 3;
|
|
|
|
export type TreeKind = "oak" | "big_oak" | "spruce" | "jungle" | "acacia";
|
|
|
|
// how likely each biome's cells are to have a tree, and which kinds grow there
|
|
export const BIOME_TREES: Record<string, { chance: number; kinds: TreeKind[] }> = {
|
|
"bworld:forest": { chance: 0.85, kinds: ["oak", "oak", "big_oak"] },
|
|
"bworld:dark_forest": { chance: 1, kinds: ["big_oak", "big_oak", "oak"] },
|
|
"bworld:plains": { chance: 0.05, kinds: ["oak"] },
|
|
"bworld:meadow": { chance: 0.04, kinds: ["oak"] },
|
|
"bworld:swamp": { chance: 0.4, kinds: ["big_oak"] },
|
|
"bworld:river": { chance: 0.05, kinds: ["oak"] },
|
|
"bworld:taiga": { chance: 0.75, kinds: ["spruce"] },
|
|
"bworld:snowy_taiga": { chance: 0.6, kinds: ["spruce"] },
|
|
"bworld:snowy_plains": { chance: 0.03, kinds: ["spruce"] },
|
|
"bworld:snowy_slopes": { chance: 0.06, kinds: ["spruce"] },
|
|
"bworld:snowy_cliffs": { chance: 0.1, kinds: ["spruce"] },
|
|
"bworld:alpine_highlands": { chance: 0.2, kinds: ["spruce", "spruce", "oak"] },
|
|
"bworld:yosemite_cliffs": { chance: 0.2, kinds: ["spruce", "oak"] },
|
|
"bworld:stony_spires": { chance: 0.12, kinds: ["spruce"] },
|
|
"bworld:savanna": { chance: 0.15, kinds: ["acacia", "acacia", "oak"] },
|
|
"bworld:shattered_savanna": { chance: 0.12, kinds: ["acacia"] },
|
|
"bworld:jungle": { chance: 1, kinds: ["jungle", "jungle", "big_oak"] },
|
|
"bworld:skylands": { chance: 0.3, kinds: ["oak", "big_oak"] },
|
|
};
|
|
|
|
export interface TreeSite {
|
|
x: number;
|
|
z: number;
|
|
// seeded from the cell, what's left of it picks the tree
|
|
rng: Alea;
|
|
}
|
|
|
|
interface Candidate extends TreeSite {
|
|
priority: number;
|
|
}
|
|
|
|
// every cell's candidate, from the seed and the cell alone
|
|
function candidate(seed: string, cell_x: number, cell_z: number): Candidate {
|
|
const rng = new Alea(`${seed}_tree_${cell_x}_${cell_z}`);
|
|
const x = cell_x * CELL + Math.floor(rng.next() * CELL);
|
|
const z = cell_z * CELL + Math.floor(rng.next() * CELL);
|
|
return { x, z, priority: rng.next(), rng };
|
|
}
|
|
|
|
// the spots in the chunk where a tree may grow, before biome and ground are checked
|
|
export function tree_sites(seed: string, chunk_x: number, chunk_z: number): TreeSite[] {
|
|
const cells = CHUNK_SIZE / CELL;
|
|
const first_x = chunk_x * cells - REACH;
|
|
const first_z = chunk_z * cells - REACH;
|
|
const size = cells + 2 * REACH;
|
|
const candidates: Candidate[] = [];
|
|
for (let cz = 0; cz < size; cz++) {
|
|
for (let cx = 0; cx < size; cx++) {
|
|
candidates.push(candidate(seed, first_x + cx, first_z + cz));
|
|
}
|
|
}
|
|
|
|
const sites: TreeSite[] = [];
|
|
for (let cz = REACH; cz < REACH + cells; cz++) {
|
|
for (let cx = REACH; cx < REACH + cells; cx++) {
|
|
const site = candidates[cz * size + cx];
|
|
let wins = true;
|
|
for (let dz = -REACH; dz <= REACH && wins; dz++) {
|
|
for (let dx = -REACH; dx <= REACH; dx++) {
|
|
const other = candidates[(cz + dz) * size + cx + dx];
|
|
if (other === site) continue;
|
|
const distance_sq = (other.x - site.x) ** 2 + (other.z - site.z) ** 2;
|
|
if (distance_sq < MIN_DISTANCE * MIN_DISTANCE && other.priority > site.priority) {
|
|
wins = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (wins) sites.push(site);
|
|
}
|
|
}
|
|
return sites;
|
|
}
|
|
|
|
// log replaces anything that isn't ground, leaves only fill air
|
|
export type PlaceBlock = (x: number, y: number, z: number, block: "log" | "leaves") => void;
|
|
|
|
// grows a tree with its trunk's bottom at x, y, z. returns false when it wouldn't fit under the top of the world
|
|
export function grow_tree(kind: TreeKind, x: number, y: number, z: number, rng: Alea, place: PlaceBlock) {
|
|
const random = (min: number, max: number) => min + Math.floor(rng.next() * (max - min + 1));
|
|
const trunk = (height: number) => {
|
|
for (let i = 0; i < height; i++) place(x, y + i, z, "log");
|
|
};
|
|
// a square layer of leaves, its corners left out at random like minecraft's
|
|
const layer = (ly: number, radius: number, corners: boolean) => {
|
|
for (let dx = -radius; dx <= radius; dx++) {
|
|
for (let dz = -radius; dz <= radius; dz++) {
|
|
const corner = Math.abs(dx) === radius && Math.abs(dz) === radius;
|
|
if (corner && radius > 0 && (!corners || rng.next() < 0.5)) continue;
|
|
place(x + dx, ly, z + dz, "leaves");
|
|
}
|
|
}
|
|
};
|
|
|
|
let top: number;
|
|
switch (kind) {
|
|
case "oak": {
|
|
const height = random(4, 6);
|
|
top = y + height + 1;
|
|
if (top >= CHUNK_HEIGHT - 1) return false;
|
|
trunk(height);
|
|
layer(y + height - 2, 2, true);
|
|
layer(y + height - 1, 2, true);
|
|
layer(y + height, 1, true);
|
|
layer(y + height + 1, 1, false);
|
|
break;
|
|
}
|
|
case "big_oak": {
|
|
const height = random(6, 8);
|
|
top = y + height + 1;
|
|
if (top >= CHUNK_HEIGHT - 1) return false;
|
|
trunk(height);
|
|
layer(y + height - 3, 2, true);
|
|
layer(y + height - 2, 3, false);
|
|
layer(y + height - 1, 3, true);
|
|
layer(y + height, 2, true);
|
|
layer(y + height + 1, 1, false);
|
|
break;
|
|
}
|
|
case "spruce": {
|
|
const height = random(7, 10);
|
|
top = y + height + 1;
|
|
if (top >= CHUNK_HEIGHT - 1) return false;
|
|
trunk(height);
|
|
// a cone of layers getting wider going down, every other one narrower, like minecraft's spruce
|
|
place(x, y + height + 1, z, "leaves");
|
|
layer(y + height, 1, false);
|
|
let radius = 1;
|
|
for (let ly = y + height - 1; ly >= y + 2; ly--) {
|
|
radius = radius >= 2 + Math.floor((y + height - ly) / 4) ? 1 : radius + 1;
|
|
layer(ly, Math.min(radius, MAX_CANOPY), false);
|
|
}
|
|
break;
|
|
}
|
|
case "jungle": {
|
|
const height = random(9, 13);
|
|
top = y + height + 1;
|
|
if (top >= CHUNK_HEIGHT - 1) return false;
|
|
trunk(height);
|
|
layer(y + height - 2, 3, false);
|
|
layer(y + height - 1, 3, true);
|
|
layer(y + height, 2, true);
|
|
layer(y + height + 1, 1, false);
|
|
break;
|
|
}
|
|
case "acacia": {
|
|
// a short trunk that leans one way at the top, under a flat, wide canopy
|
|
const height = random(4, 5);
|
|
top = y + height + 2;
|
|
if (top >= CHUNK_HEIGHT - 1) return false;
|
|
trunk(height);
|
|
const lean_x = random(-1, 1);
|
|
const lean_z = lean_x === 0 ? (rng.next() < 0.5 ? -1 : 1) : 0;
|
|
const cx = x + lean_x;
|
|
const cz = z + lean_z;
|
|
place(cx, y + height, cz, "log");
|
|
for (let dx = -MAX_CANOPY; dx <= MAX_CANOPY; dx++) {
|
|
for (let dz = -MAX_CANOPY; dz <= MAX_CANOPY; dz++) {
|
|
if (Math.abs(dx) + Math.abs(dz) <= 4 && !(Math.abs(dx) === 3 && Math.abs(dz) === 3)) {
|
|
place(cx + dx, y + height + 1, cz + dz, "leaves");
|
|
}
|
|
if (Math.abs(dx) + Math.abs(dz) <= 2) {
|
|
place(cx + dx, y + height + 2, cz + dz, "leaves");
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|