Refactor: merge duplicated rules, split the largest files by topic (t21)

- One helper each for wall and mold refunds, tube edges and lanes, pipe text, piece dimensions,
  font lookup, reach circles and surface averaging.
- previews, smooth, notebook, materials, worldgen and selection split into topic files
  (preview-drill, preview-selection, smooth-shaders, notebook-market, notebook-blueprints,
  material-tables, worldgen-helpers, selection-pickers, selection-panels).
- Every suite's output identical apart from timings; seeds make the same worlds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 22:35:12 -04:00
co-authored by Claude Opus 5.5
parent 9937683a2c
commit 7fa1056e09
28 changed files with 1036 additions and 953 deletions
+10 -3
View File
@@ -71,6 +71,15 @@ function blockIntegrity(b) {
void v;
return n;
}
// One cell of a wall or mold being taken back: metal is counted toward bars (returns 1), other
// walls give their rubble back.
function removeWallCell(i, wall) {
const m = mat[i];
if (BAR_OF[m] && PH[wall] === PH_ITEM) { setCell(i, EMPTY, temp[i], 0); return 1; }
if (YLD[m] && bagAdd(YLD[m], 1, false) === 0) { setCell(i, YLD[m], temp[i], 0); droppedLast++; } // bag full: it crumbles in place
else setCell(i, EMPTY, temp[i], 0);
return 0;
}
function removeBlock(bx, by) {
const b = by * BW + bx, code = blockP[b], v = blockV[b];
if (!code) return 0;
@@ -82,9 +91,7 @@ function removeBlock(bx, by) {
const m = mat[i], p = part[i];
if (code === BP_WALL) {
if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m] && m !== SPRING) {
if (BAR_OF[m] && PH[v] === PH_ITEM) { metal++; setCell(i, EMPTY, temp[i], 0); }
else if (YLD[m] && bagAdd(YLD[m], 1, false) === 0) { setCell(i, YLD[m], temp[i], 0); droppedLast++; } // bag full: it crumbles in place
else setCell(i, EMPTY, temp[i], 0);
metal += removeWallCell(i, v);
n++;
}
} else if (isPipeCode(code)) {
+15 -8
View File
@@ -62,13 +62,19 @@ function entTubeMove(e, x, y, tubeOnly = false) {
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, yy)) return false;
e.x = e.ix = x; e.y = e.iy = y; e.vx = e.vy = 0; return true;
}
function entTubeLane(y, iy) { return Math.max(y & ~3, Math.min((y & ~3) + 2, iy)); }
function entTubeEdge(e, dx, dy, k) {
const x = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), y = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k);
return x < 0 || y < 0 || x >= W || y >= H ? -1 : y * W + x;
}
function entTubeEnter(e, dx, dy) {
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
const xx = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), yy = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k);
if (xx < 0 || yy < 0 || xx >= W || yy >= H) continue;
const p = part[yy * W + xx]; if (p < P_TU || p > P_TR || (!dx && TDX[p]) || dx * TDX[p] + dy * TDY[p] < 0) continue;
const i = entTubeEdge(e, dx, dy, k);
if (i < 0) continue;
const xx = i % W, yy = (i / W) | 0;
const p = part[i]; if (p < P_TU || p > P_TR || (!dx && TDX[p]) || dx * TDX[p] + dy * TDY[p] < 0) continue;
// Vertical screws require the full width; horizontal screws use the nearest two-row lane.
const x = (xx & ~3), y = Math.max(yy & ~3, Math.min((yy & ~3) + 2, e.iy));
const x = (xx & ~3), y = entTubeLane(yy, e.iy);
if (entTubeMove(e, x, TDX[p] || dy ? y : e.iy, true)) return true;
}
return false;
@@ -80,14 +86,15 @@ function entTubeTick(e, p) {
const dx = TDX[p], dy = TDY[p];
let x = e.ix + dx, y = e.iy + dy, next = 0, ax = 0, ay = 0;
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
const xx = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), yy = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k);
if (xx < 0 || yy < 0 || xx >= W || yy >= H) return;
const q = part[yy * W + xx];
const i = entTubeEdge(e, dx, dy, k);
if (i < 0) return;
const xx = i % W, yy = (i / W) | 0;
const q = part[i];
if (q >= P_TU && q <= P_TR && q !== p) { next = q; ax = xx; ay = yy; break; }
}
if (next) {
if (TDX[next] === -dx && TDY[next] === -dy) return;
x = ax & ~3; y = Math.max(ay & ~3, Math.min((ay & ~3) + 2, e.iy));
x = ax & ~3; y = entTubeLane(ay, e.iy);
if (entTubeMove(e, x, y, true)) stats.moved++;
return;
}
+49
View File
@@ -0,0 +1,49 @@
// Material tables: typed properties, casting groups, flags, parts and tool tiers.
// Typed property tables
const PH = new Uint8Array(NMAT), DENS = new Float32Array(NMAT), KC = new Float32Array(NMAT), ICAP = new Float32Array(NMAT),
HARD = new Uint8Array(NMAT), YLD = new Uint8Array(NMAT), MELT = new Float32Array(NMAT), MELTTO = new Uint8Array(NMAT),
SMELTP = new Float32Array(NMAT), FRZ = new Float32Array(NMAT), FRZTO = new Uint8Array(NMAT), BOIL = new Float32Array(NMAT),
BOILTO = new Uint8Array(NMAT), CONDT = new Float32Array(NMAT), CONDTO = new Uint8Array(NMAT), IGN = new Float32Array(NMAT),
FLAME = new Float32Array(NMAT), FLAMEF = new Float32Array(NMAT), FUEL = new Uint8Array(NMAT), BURNTO = new Uint8Array(NMAT),
ASHP = new Float32Array(NMAT), FLUID = new Float32Array(NMAT), SPREAD = new Uint8Array(NMAT), STICK = new Float32Array(NMAT),
ALPHA = new Float32Array(NMAT), LIQT = new Float32Array(NMAT), BAGOK = new Uint8Array(NMAT), PARTMAT = new Uint8Array(NMAT),
RELAX = new Float32Array(NMAT), COLR = new Uint8Array(NMAT), COLG = new Uint8Array(NMAT), COLB = new Uint8Array(NMAT),
CVAR = new Uint8Array(NMAT);
for (let i = 0; i < NMAT; i++) {
const d = MD[i];
PH[i] = d.ph; DENS[i] = d.dens; KC[i] = d.k; ICAP[i] = 1 / d.cap; HARD[i] = d.hard; YLD[i] = d.yld;
MELT[i] = d.melt; MELTTO[i] = d.meltTo; SMELTP[i] = d.smelt; FRZ[i] = d.freeze; FRZTO[i] = d.freezeTo;
BOIL[i] = d.boil; BOILTO[i] = d.boilTo; CONDT[i] = d.condT; CONDTO[i] = d.condTo; IGN[i] = d.ign; FLAME[i] = d.flame;
FLAMEF[i] = d.flameF; FUEL[i] = d.fuel; BURNTO[i] = d.burnTo; ASHP[i] = d.ashP; FLUID[i] = d.fluid; SPREAD[i] = d.spread;
STICK[i] = d.stick; ALPHA[i] = d.alpha; LIQT[i] = d.liqT; BAGOK[i] = d.bag ? 1 : 0; PARTMAT[i] = d.partMat ? 1 : 0;
COLR[i] = d.col[0]; COLG[i] = d.col[1]; COLB[i] = d.col[2]; CVAR[i] = d.v;
RELAX[i] = d.ph === PH_LIQUID ? 0.0006 : d.ph === PH_POWDER ? 0.0005 : d.ph === PH_GAS ? 0.003 : 0.0004;
}
// what molds cast (molten metals, and molten glass) and what they set into
const CASTMETAL = new Uint8Array(NMAT); for (const m of [MCU, MFE, MST, MSN, MZN, MAG, MAU, MBZ, MBS, MGL]) CASTMETAL[m] = 1;
// tier as a drill bit or tank (0: too soft to be a bit)
const METAL_TIER = new Int8Array(NMAT).fill(-1); METAL_TIER[COPPER] = 1; METAL_TIER[IRON] = 2; METAL_TIER[STEEL] = 3;
METAL_TIER[TIN] = 0; METAL_TIER[GLASS] = 0; METAL_TIER[ZINC] = 0; METAL_TIER[SILVER] = 0; METAL_TIER[GOLD] = 0; METAL_TIER[BRONZE] = 1; METAL_TIER[BRASS] = 1;
const SOLIDMETAL = new Uint8Array(NMAT); for (const m of [COPPER, IRON, STEEL, TIN, ZINC, SILVER, GOLD, BRONZE, BRASS, GLASS]) SOLIDMETAL[m] = 1;
// ---------- Flags & parts ----------
const F_NAT = 1, F_CAST = 2, F_BURN = 4;
const P_NONE = 0, P_TU = 1, P_TD = 2, P_TL = 3, P_TR = 4, P_GRATE = 5, P_GOPEN = 6, P_WHEEL = 7, P_INTAKE = 8, P_FILTER = 9, P_PIPE = 10;
const TDX = [0, 0, 0, -1, 1], TDY = [0, -1, 1, 0, 0];
// ---------- Tool tiers ----------
// reach: how far from the bot the drill works. Harder rock takes longer, and
// every new bit cuts faster and further.
const BIT_TIERS = [
{ name: 'Stone bit', hard: 3, speed: 0.7, reach: 16 },
{ name: 'Copper bit', hard: 4, speed: 1.0, reach: 24 },
{ name: 'Iron bit', hard: 5, speed: 1.5, reach: 34 },
{ name: 'Steel bit', hard: 6, speed: 2.2, reach: 46 },
];
const TANK_TIERS = [
{ name: 'Canvas sack', cap: 1500 },
{ name: 'Copper tank', cap: 4000 },
{ name: 'Iron tank', cap: 10000 },
{ name: 'Steel tank', cap: 25000 },
];
const REACH = 64;
+1 -50
View File
@@ -1,4 +1,4 @@
// Material ids, the material table (def), per-material property arrays, cell flags and part
// Material definitions: ids, properties and metal families.
// ids, and drill/tank tiers.
// ---------- Material ids ----------
const EMPTY = 0, GRASS = 1, DIRT = 2, LDIRT = 3, SAND = 4, GRAVEL = 5, CLAY = 6, STONE = 7, GRANITE = 8, BASALT = 9,
@@ -117,52 +117,3 @@ def(BARSB, { name: 'Stone block', ph: PH_ITEM, col: [172, 166, 154], v: 0, dens:
def(PIPE, { name: 'Pipe', col: [176, 112, 72], v: 4, k: 0.04, cap: 1.5, hard: 4, melt: 1085, meltTo: MCU, partMat: true });
def(DOCKW, { name: 'Trade dock', col: [70, 82, 96], v: 6, k: 0.01, cap: 2, hard: 9, yld: 0, partMat: true, desc: 'The claim office\'s trade dock. Whatever drops through the grate in its floor is sold.' });
def(HULL, { name: 'Machine', col: [84, 92, 102], v: 6, k: 0.03, cap: 1.5, hard: 4, melt: 1085, meltTo: MCU, partMat: true });
// Typed property tables
const PH = new Uint8Array(NMAT), DENS = new Float32Array(NMAT), KC = new Float32Array(NMAT), ICAP = new Float32Array(NMAT),
HARD = new Uint8Array(NMAT), YLD = new Uint8Array(NMAT), MELT = new Float32Array(NMAT), MELTTO = new Uint8Array(NMAT),
SMELTP = new Float32Array(NMAT), FRZ = new Float32Array(NMAT), FRZTO = new Uint8Array(NMAT), BOIL = new Float32Array(NMAT),
BOILTO = new Uint8Array(NMAT), CONDT = new Float32Array(NMAT), CONDTO = new Uint8Array(NMAT), IGN = new Float32Array(NMAT),
FLAME = new Float32Array(NMAT), FLAMEF = new Float32Array(NMAT), FUEL = new Uint8Array(NMAT), BURNTO = new Uint8Array(NMAT),
ASHP = new Float32Array(NMAT), FLUID = new Float32Array(NMAT), SPREAD = new Uint8Array(NMAT), STICK = new Float32Array(NMAT),
ALPHA = new Float32Array(NMAT), LIQT = new Float32Array(NMAT), BAGOK = new Uint8Array(NMAT), PARTMAT = new Uint8Array(NMAT),
RELAX = new Float32Array(NMAT), COLR = new Uint8Array(NMAT), COLG = new Uint8Array(NMAT), COLB = new Uint8Array(NMAT),
CVAR = new Uint8Array(NMAT);
for (let i = 0; i < NMAT; i++) {
const d = MD[i];
PH[i] = d.ph; DENS[i] = d.dens; KC[i] = d.k; ICAP[i] = 1 / d.cap; HARD[i] = d.hard; YLD[i] = d.yld;
MELT[i] = d.melt; MELTTO[i] = d.meltTo; SMELTP[i] = d.smelt; FRZ[i] = d.freeze; FRZTO[i] = d.freezeTo;
BOIL[i] = d.boil; BOILTO[i] = d.boilTo; CONDT[i] = d.condT; CONDTO[i] = d.condTo; IGN[i] = d.ign; FLAME[i] = d.flame;
FLAMEF[i] = d.flameF; FUEL[i] = d.fuel; BURNTO[i] = d.burnTo; ASHP[i] = d.ashP; FLUID[i] = d.fluid; SPREAD[i] = d.spread;
STICK[i] = d.stick; ALPHA[i] = d.alpha; LIQT[i] = d.liqT; BAGOK[i] = d.bag ? 1 : 0; PARTMAT[i] = d.partMat ? 1 : 0;
COLR[i] = d.col[0]; COLG[i] = d.col[1]; COLB[i] = d.col[2]; CVAR[i] = d.v;
RELAX[i] = d.ph === PH_LIQUID ? 0.0006 : d.ph === PH_POWDER ? 0.0005 : d.ph === PH_GAS ? 0.003 : 0.0004;
}
// what molds cast (molten metals, and molten glass) and what they set into
const CASTMETAL = new Uint8Array(NMAT); for (const m of [MCU, MFE, MST, MSN, MZN, MAG, MAU, MBZ, MBS, MGL]) CASTMETAL[m] = 1;
// tier as a drill bit or tank (0: too soft to be a bit)
const METAL_TIER = new Int8Array(NMAT).fill(-1); METAL_TIER[COPPER] = 1; METAL_TIER[IRON] = 2; METAL_TIER[STEEL] = 3;
METAL_TIER[TIN] = 0; METAL_TIER[GLASS] = 0; METAL_TIER[ZINC] = 0; METAL_TIER[SILVER] = 0; METAL_TIER[GOLD] = 0; METAL_TIER[BRONZE] = 1; METAL_TIER[BRASS] = 1;
const SOLIDMETAL = new Uint8Array(NMAT); for (const m of [COPPER, IRON, STEEL, TIN, ZINC, SILVER, GOLD, BRONZE, BRASS, GLASS]) SOLIDMETAL[m] = 1;
// ---------- Flags & parts ----------
const F_NAT = 1, F_CAST = 2, F_BURN = 4;
const P_NONE = 0, P_TU = 1, P_TD = 2, P_TL = 3, P_TR = 4, P_GRATE = 5, P_GOPEN = 6, P_WHEEL = 7, P_INTAKE = 8, P_FILTER = 9, P_PIPE = 10;
const TDX = [0, 0, 0, -1, 1], TDY = [0, -1, 1, 0, 0];
// ---------- Tool tiers ----------
// reach: how far from the bot the drill works. Harder rock takes longer, and
// every new bit cuts faster and further.
const BIT_TIERS = [
{ name: 'Stone bit', hard: 3, speed: 0.7, reach: 16 },
{ name: 'Copper bit', hard: 4, speed: 1.0, reach: 24 },
{ name: 'Iron bit', hard: 5, speed: 1.5, reach: 34 },
{ name: 'Steel bit', hard: 6, speed: 2.2, reach: 46 },
];
const TANK_TIERS = [
{ name: 'Canvas sack', cap: 1500 },
{ name: 'Copper tank', cap: 4000 },
{ name: 'Iron tank', cap: 10000 },
{ name: 'Steel tank', cap: 25000 },
];
const REACH = 64;
+1 -3
View File
@@ -309,9 +309,7 @@ function removeMold(mo) {
for (const i of walls) {
const m = mat[i];
if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m]) {
if (BAR_OF[m] && PH[mo.wall] === PH_ITEM) { metal++; setCell(i, EMPTY, temp[i], 0); }
else if (YLD[m] && bagAdd(YLD[m], 1, false) === 0) { setCell(i, YLD[m], temp[i], 0); droppedLast++; }
else setCell(i, EMPTY, temp[i], 0);
metal += removeWallCell(i, mo.wall);
}
}
if (metal) giveOrDrop(BAR_OF[WALL_OF[mo.wall]] || BARCU, Math.floor(metal / GRAINS_PER_BAR), mo.x + 8, mo.y);
+2 -1
View File
@@ -67,8 +67,9 @@ function depthU(y) {
}
// the y at a fraction u of the underground depth (the inverse of depthU)
function depthY(u) { return Math.round(surfBase + (!shallowD ? u * underD : u <= 0.3 ? u / 0.3 * shallowD : shallowD + (u - 0.3) / 0.7 * (underD - shallowD))); }
function meanSurface() { let s = 0; for (let x = 0; x < W; x++) s += surfY[x]; return s / W; }
function computeAmbient() {
let sm = 0; for (let x = 0; x < W; x++) sm += surfY[x]; sm /= W;
const sm = meanSurface();
for (let y = 0; y < H; y++) {
let a = 18;
if (y > sm) a += Math.min(60, (y - sm) * 0.04);
+61
View File
@@ -0,0 +1,61 @@
// World generation helpers: noise and removal of tiny hard rock specks.
// ---------- Perlin noise ----------
function makeNoise(rng) {
const perm = new Uint8Array(512), gx = new Float32Array(256), gy = new Float32Array(256);
const p = []; for (let i = 0; i < 256; i++) p.push(i);
for (let i = 255; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); const t = p[i]; p[i] = p[j]; p[j] = t; }
for (let i = 0; i < 512; i++) perm[i] = p[i & 255];
for (let i = 0; i < 256; i++) { const a = rng() * Math.PI * 2; gx[i] = Math.cos(a); gy[i] = Math.sin(a); }
return function (x, y) {
const xi = Math.floor(x), yi = Math.floor(y), xf = x - xi, yf = y - yi;
const X = xi & 255, Y = yi & 255;
const p0 = perm[Y], p1 = perm[Y + 1];
const h00 = perm[X + p0], h10 = perm[X + 1 + p0], h01 = perm[X + p1], h11 = perm[X + 1 + p1];
const n00 = gx[h00] * xf + gy[h00] * yf, n10 = gx[h10] * (xf - 1) + gy[h10] * yf;
const n01 = gx[h01] * xf + gy[h01] * (yf - 1), n11 = gx[h11] * (xf - 1) + gy[h11] * (yf - 1);
const u = xf * xf * xf * (xf * (xf * 6 - 15) + 10), v = yf * yf * yf * (yf * (yf * 6 - 15) + 10);
return (n00 + u * (n10 - n00) + v * (n01 - n00) + u * v * (n00 - n10 - n01 + n11)) * 1.41;
};
}
function fbm(n, x, y, oct) { let s = 0, a = 1, f = 1, t = 0; for (let o = 0; o < oct; o++) { s += n(x * f, y * f) * a; t += a; a *= 0.5; f *= 2; } return s / t; }
// Small specks of rock harder than the rock around them (a few grains of iron ore in stone, a
// chip of granite, a sliver of basalt) are not worth having: a bit too weak to break them leaves
// them hanging in the way. Any such piece of SPECK_MIN grains or fewer becomes the softer rock
// around it. Bigger veins and pockets stay. live: the world is in play (an older save).
const SPECK_MIN = 30;
function clearHardSpecks(live) {
const seen = new Uint8Array(N), st = new Int32Array(N), cells = new Int32Array(SPECK_MIN + 1);
let n = 0;
for (const h0 of [6, 5, 4]) {
seen.fill(0);
const mem = (i) => { const m = mat[i]; return (flags[i] & F_NAT) !== 0 && PH[m] === PH_SOLID && HARD[m] >= h0 && HARD[m] <= 6 && YLD[m] !== 0 && !PARTMAT[m]; };
const soft = (i) => { const m = mat[i]; return (flags[i] & F_NAT) !== 0 && PH[m] === PH_SOLID && HARD[m] >= 1 && HARD[m] < h0 && YLD[m] !== 0 && !PARTMAT[m]; };
for (let i0 = 0; i0 < N; i0++) {
if (seen[i0] || !mem(i0)) continue;
let sp = 0, sz = 0, x0 = W, y0 = H, x1 = 0, y1 = 0;
const hosts = new Map();
st[sp++] = i0; seen[i0] = 1;
while (sp) {
const i = st[--sp], x = i % W, y = (i / W) | 0;
if (sz <= SPECK_MIN) cells[sz] = i;
sz++;
if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y;
for (const j of [x > 0 ? i - 1 : -1, x < W - 1 ? i + 1 : -1, i - W, i + W]) {
if (j < 0 || j >= N || seen[j]) continue;
if (mem(j)) { seen[j] = 1; st[sp++] = j; }
else if (soft(j)) hosts.set(mat[j], (hosts.get(mat[j]) || 0) + 1);
}
}
if (sz > SPECK_MIN) continue;
// a speck left floating (in a cave, or dug round) takes the rock nearest round it
if (!hosts.size) for (let y = Math.max(0, y0 - 5); y <= Math.min(H - 1, y1 + 5); y++) for (let x = Math.max(0, x0 - 5); x <= Math.min(W - 1, x1 + 5); x++) { const j = y * W + x; if (soft(j)) hosts.set(mat[j], (hosts.get(mat[j]) || 0) + 1); }
let host = 0, best = 0;
for (const [m, c] of hosts) if (c > best) { best = c; host = m; }
if (!host) continue;
for (let k = 0; k < sz; k++) { const i = cells[k]; if (live) setCell(i, host, temp[i], F_NAT); else mat[i] = host; }
n++;
}
}
return n;
}
+1 -61
View File
@@ -1,64 +1,4 @@
// World generation: noise, layers, ores, caves, water, trees, and clearing tiny hard specks.
// ---------- Perlin noise ----------
function makeNoise(rng) {
const perm = new Uint8Array(512), gx = new Float32Array(256), gy = new Float32Array(256);
const p = []; for (let i = 0; i < 256; i++) p.push(i);
for (let i = 255; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); const t = p[i]; p[i] = p[j]; p[j] = t; }
for (let i = 0; i < 512; i++) perm[i] = p[i & 255];
for (let i = 0; i < 256; i++) { const a = rng() * Math.PI * 2; gx[i] = Math.cos(a); gy[i] = Math.sin(a); }
return function (x, y) {
const xi = Math.floor(x), yi = Math.floor(y), xf = x - xi, yf = y - yi;
const X = xi & 255, Y = yi & 255;
const p0 = perm[Y], p1 = perm[Y + 1];
const h00 = perm[X + p0], h10 = perm[X + 1 + p0], h01 = perm[X + p1], h11 = perm[X + 1 + p1];
const n00 = gx[h00] * xf + gy[h00] * yf, n10 = gx[h10] * (xf - 1) + gy[h10] * yf;
const n01 = gx[h01] * xf + gy[h01] * (yf - 1), n11 = gx[h11] * (xf - 1) + gy[h11] * (yf - 1);
const u = xf * xf * xf * (xf * (xf * 6 - 15) + 10), v = yf * yf * yf * (yf * (yf * 6 - 15) + 10);
return (n00 + u * (n10 - n00) + v * (n01 - n00) + u * v * (n00 - n10 - n01 + n11)) * 1.41;
};
}
function fbm(n, x, y, oct) { let s = 0, a = 1, f = 1, t = 0; for (let o = 0; o < oct; o++) { s += n(x * f, y * f) * a; t += a; a *= 0.5; f *= 2; } return s / t; }
// Small specks of rock harder than the rock around them (a few grains of iron ore in stone, a
// chip of granite, a sliver of basalt) are not worth having: a bit too weak to break them leaves
// them hanging in the way. Any such piece of SPECK_MIN grains or fewer becomes the softer rock
// around it. Bigger veins and pockets stay. live: the world is in play (an older save).
const SPECK_MIN = 30;
function clearHardSpecks(live) {
const seen = new Uint8Array(N), st = new Int32Array(N), cells = new Int32Array(SPECK_MIN + 1);
let n = 0;
for (const h0 of [6, 5, 4]) {
seen.fill(0);
const mem = (i) => { const m = mat[i]; return (flags[i] & F_NAT) !== 0 && PH[m] === PH_SOLID && HARD[m] >= h0 && HARD[m] <= 6 && YLD[m] !== 0 && !PARTMAT[m]; };
const soft = (i) => { const m = mat[i]; return (flags[i] & F_NAT) !== 0 && PH[m] === PH_SOLID && HARD[m] >= 1 && HARD[m] < h0 && YLD[m] !== 0 && !PARTMAT[m]; };
for (let i0 = 0; i0 < N; i0++) {
if (seen[i0] || !mem(i0)) continue;
let sp = 0, sz = 0, x0 = W, y0 = H, x1 = 0, y1 = 0;
const hosts = new Map();
st[sp++] = i0; seen[i0] = 1;
while (sp) {
const i = st[--sp], x = i % W, y = (i / W) | 0;
if (sz <= SPECK_MIN) cells[sz] = i;
sz++;
if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y;
for (const j of [x > 0 ? i - 1 : -1, x < W - 1 ? i + 1 : -1, i - W, i + W]) {
if (j < 0 || j >= N || seen[j]) continue;
if (mem(j)) { seen[j] = 1; st[sp++] = j; }
else if (soft(j)) hosts.set(mat[j], (hosts.get(mat[j]) || 0) + 1);
}
}
if (sz > SPECK_MIN) continue;
// a speck left floating (in a cave, or dug round) takes the rock nearest round it
if (!hosts.size) for (let y = Math.max(0, y0 - 5); y <= Math.min(H - 1, y1 + 5); y++) for (let x = Math.max(0, x0 - 5); x <= Math.min(W - 1, x1 + 5); x++) { const j = y * W + x; if (soft(j)) hosts.set(mat[j], (hosts.get(mat[j]) || 0) + 1); }
let host = 0, best = 0;
for (const [m, c] of hosts) if (c > best) { best = c; host = m; }
if (!host) continue;
for (let k = 0; k < sz; k++) { const i = cells[k]; if (live) setCell(i, host, temp[i], F_NAT); else mat[i] = host; }
n++;
}
}
return n;
}
// World generation: staged terrain, ore, caves and placement, with synchronous and async entry points.
// ---------- World generation ----------
function* worldSteps(seedIn, w, h) {
yield [0, 'Shaping the land'];