// 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 ---------- function generateWorld(seedIn, w, h, onProgress) { worldSeed = seedIn >>> 0; alloc(w, h); RS = (worldSeed ^ 0x9E3779B9) | 1; const rng = mulberry32(worldSeed); const nSurf = makeNoise(rng), nDetail = makeNoise(rng), nLayer = makeNoise(rng), nPocket = makeNoise(rng), nPocket2 = makeNoise(rng); const nCoal = makeNoise(rng), nCu = makeNoise(rng), nFe = makeNoise(rng), nCave = makeNoise(rng), nCave2 = makeNoise(rng), nCave3 = makeNoise(rng), nWater = makeNoise(rng); const rng2 = mulberry32(worldSeed ^ 0x5bd1e995); // separate stream so older worlds keep their layout const nSn = makeNoise(rng2), nZn = makeNoise(rng2), nAg = makeNoise(rng2), nAu = makeNoise(rng2); const rng3 = mulberry32(worldSeed ^ 0x27d4eb2d); // the shapes of the deep rock layers const nLay2 = makeNoise(rng3), nWarp = makeNoise(rng3), nIntr = makeNoise(rng3), nDike = makeNoise(rng3), nMagC = makeNoise(rng3); const spawnX = w >> 1; // Layout: a third of the height is sky. The shallow layers keep a fixed depth (see shallowD), // the deep ones stretch to fill the rest, and the air cools gently enough to build tall. surfBase = Math.round(h * 0.3); underD = h - surfBase; shallowD = Math.min(140, Math.round(underD * 0.3)); lapse = 0.22; const base = surfBase; const U = depthY; // y at a fraction of the underground depth for (let x = 0; x < w; x++) { let s = base + fbm(nSurf, x / 230, 0.3, 4) * 46 + nDetail(x / 38, 4.2) * 5; const ds = Math.abs(x - spawnX); if (ds < 60) { const k = 1 - ds / 60; s = s * (1 - k * 0.75) + base * k * 0.75; } surfY[x] = Math.round(s); dirtY[x] = surfY[x] + Math.round(14 + nLayer(x / 60, 1.5) * 7); // the deep layers rise and sink in long swells as well as short wiggles, more in deeper worlds // (a shared fold moves them together, and each also has its own swells) const fold = fbm(nLay2, x / 520, 3.3, 3); const gA = underD * 0.16, bA = underD * 0.12, mA = underD * 0.06; graniteY[x] = U(0.5) + Math.round(fold * gA * 1.8 + fbm(nLay2, x / 170, 9.1, 3) * gA + nLayer(x / 60, 9.1) * 10); basaltY[x] = U(0.72) + Math.round(fold * bA * 1.6 + fbm(nLay2, x / 150, 17.3, 3) * bA + nLayer(x / 50, 17.3) * 8); magmaY[x] = U(0.905) + Math.round(fold * mA * 1.4 + fbm(nLay2, x / 110, 23.7, 3) * mA + nLayer(x / 40, 23.7) * 5); // and stay in order, with room between them graniteY[x] = Math.max(graniteY[x], dirtY[x] + 60); basaltY[x] = Math.max(basaltY[x], graniteY[x] + 40); magmaY[x] = Math.min(h - 12, Math.max(magmaY[x], basaltY[x] + 30)); } // Landmarks: a lake on one side of spawn and a desert on the other, then mountains, lakes, // deserts and hills spaced out toward both edges. The first one past the lake is always a // mountain tall enough to reach the freezing air and hold snow. const lakeSide = rng() < 0.5 ? -1 : 1; const feats = [{ k: 'lake', x: Math.round(spawnX + lakeSide * (170 + rng() * 80)) }, { k: 'desert', x: Math.round(spawnX - lakeSide * (250 + rng() * 90)) }]; for (const side of [lakeSide, -lakeSide]) { let d = 600 + rng() * 90, prev = side === lakeSide ? '' : 'desert'; for (;;) { const x = Math.round(spawnX + side * d); if (x < 140 || x > w - 140) break; // never the same kind twice in a row let k = 'mountain'; if (prev) { const ks = ['lake', 'desert', 'mountain', 'hills'].filter(q => q !== prev), wt = { lake: 3, desert: 2.5, mountain: 2.5, hills: 2 }; let r = rng() * ks.reduce((a, q) => a + wt[q], 0); k = ks.find(q => (r -= wt[q]) < 0) || ks[0]; } feats.push({ k, x }); prev = k; d += 340 + rng() * 260; } } if (!feats.some(f => f.k === 'mountain')) feats.push({ k: 'mountain', x: lakeSide > 0 ? w - 160 : 160 }); const maxMt = Math.max(60, Math.min(surfBase - 80, 200)); for (const f of feats) { if (f.k !== 'mountain' && f.k !== 'hills') continue; const mt = f.k === 'mountain', sig = mt ? 55 + rng() * 30 : 45 + rng() * 35, hgt = mt ? maxMt * (0.8 + rng() * 0.2) : 35 + rng() * 35; for (let x = Math.max(0, Math.round(f.x - sig * 3.2)); x < Math.min(w, f.x + sig * 3.2); x++) { const g = Math.exp(-0.5 * ((x - f.x) / sig) ** 2); if (g < 0.01) continue; surfY[x] = Math.round(surfY[x] - hgt * g - nDetail(x / 9, 5.7) * 5 * g); if (mt) dirtY[x] = surfY[x] + Math.round(3 + (1 - g) * 11); else dirtY[x] = Math.min(dirtY[x], surfY[x] + 20); } } // lakes: a basin dug below the lowest point of its rim, filled to just above it const lakeTop = new Int16Array(w).fill(h), noTree = new Uint8Array(w), desAt = new Uint8Array(w); for (const f of feats) { if (f.k !== 'lake') continue; const lw = Math.round(90 + rng() * 40), deep = 22 + rng() * 8; let lip = 1e9; for (let x = f.x - lw; x <= f.x + lw; x++) if (x >= 0 && x < w) lip = Math.min(lip, surfY[x]); for (let x = f.x - lw; x <= f.x + lw; x++) { if (x < 0 || x >= w) continue; const u = (x - f.x) / lw, depth = Math.max(0, 1 - u * u) * deep; if (depth > 0) { surfY[x] = Math.max(surfY[x], Math.round(lip + depth)); dirtY[x] = Math.max(dirtY[x], surfY[x] + 8); } lakeTop[x] = lip + 2; } for (let x = Math.max(0, f.x - lw - 6); x <= Math.min(w - 1, f.x + lw + 6); x++) noTree[x] = 1; } for (const f of feats) { if (f.k !== 'desert') continue; const dw = Math.round(90 + rng() * 40); for (let x = Math.max(0, f.x - dw); x <= Math.min(w - 1, f.x + dw); x++) if (Math.abs(x - f.x) < dw - Math.abs(nDetail(x / 20, 3.3)) * 10) desAt[x] = 1; } computeAmbient(); for (let y = 0; y < h; y++) { const fy = depthU(y); // fraction of the underground depth (negative in the sky) for (let x = 0; x < w; x++) { const i = y * w + x; const sy = surfY[x]; let m = EMPTY; if (y < sy) { if (y >= lakeTop[x]) m = WATER; } else { const d = y - sy; const inDes = desAt[x] === 1; if (y < dirtY[x]) m = d === 0 && !inDes ? GRASS : DIRT; else if (y < graniteY[x] + nWarp(x / 70, y / 45) * 26 + nWarp(x / 22 + 40, y / 15) * 8 + nPocket(x / 9, y / 9) * 3) m = STONE; else if (y < basaltY[x] + nWarp(x / 64 + 50, y / 40) * 22 + nWarp(x / 20 + 90, y / 14) * 7 + nPocket(x / 7, y / 7) * 3) m = GRANITE; else m = BASALT; // granite bulges up into the stone in rounded masses, and stone is caught in the granite in lumps if (m === STONE && y > graniteY[x] - 100 && nIntr(x / 46, y / 34) > 0.24 + (graniteY[x] - y) / 100 * 0.3) m = GRANITE; else if (m === GRANITE && y < basaltY[x] - 20 && nIntr(x / 38 + 70, y / 30) < -0.36) m = STONE; void nDike; // (kept so the noises after it stay the same) if (inDes && d < 12 + nDetail(x / 15, 8.1) * 4) m = SAND; // pockets const pk = nPocket(x / 14, y / 11), pk2 = nPocket2(x / 18, y / 13); if (m === DIRT && d > 2 && pk2 > 0.42) m = CLAY; else if (m === STONE) { if (d < 150 && pk > 0.52) m = DIRT; else if (d < 110 && pk2 > 0.5) m = CLAY; else if (pk < -0.56) m = GRAVEL; else if (pk2 < -0.6 && d < 220) m = SAND; } // ores if (m === STONE || m === DIRT || m === GRANITE || m === BASALT || m === CLAY) { const host = m; // Leave the shallow quarter unchanged; deeper rock has broader, richer lenses. const rich = Math.min(1, Math.max(0, (fy - 0.25) / 0.6)); const c = nCoal(x / 13, y / 9); if ((d > 6 && fy < 0.45 && c > 0.5 - 0.55 * rich && m !== CLAY) || (fy >= 0.45 && (m === STONE || m === GRANITE) && c > 0.62 - 0.3 * rich) || (m === BASALT && fy > 0.6 && c > 0.58 - 0.4 * rich)) m = COALV; else { const cu = nCu(x / 9, y / 8); const cuBand = d > 14 && fy < 0.48; if ((cuBand && (m === STONE || m === DIRT) && cu > (d < 140 ? 0.5 : 0.56) - 0.65 * rich) || (fy > 0.48 && fy < 0.8 && (m === STONE || m === GRANITE) && cu > 0.6 - 0.46 * rich)) m = CUV; else { // iron and zinc are harder than the stone around them, so they come in fewer, chunkier // lenses rather than a scatter of specks that a stone bit would leave in the way const fe = nFe(x / 24, y / 13); if (fy > 0.28 && (m === STONE || m === GRANITE || m === BASALT) && fe > (m === STONE ? 0.55 : 0.47) - 0.55 * rich) m = FEV; // rarer ores in deeper bands: tin, then zinc, silver in granite, gold deepest else if (((fy > 0.15 && fy < 0.55 && m === STONE) || (fy >= 0.55 && fy < 0.8 && (m === STONE || m === GRANITE))) && nSn(x / 8, y / 7) > 0.62 + Math.max(0, fy - 0.55) * 0.4 - 0.425 * rich) m = SNV; else if (fy > 0.35 && fy < 0.78 && (m === STONE || m === GRANITE) && nZn(x / 22, y / 13) > 0.6 - 0.49 * rich) m = ZNV; else if (fy > 0.5 && fy < 0.88 && m === GRANITE && nAg(x / 7, y / 6) > 0.66 - 0.32 * rich) m = AGV; else if (fy > 0.68 && (m === GRANITE || m === BASALT) && nAu(x / 6, y / 5) > 0.7 - 0.18 * rich) m = AUV; } } // no ore where the rock is naturally hot enough to light or melt it: it would burn or run forever if (m !== host && (IGN[m] ? AMB[y] > IGN[m] - 120 : MELT[m] && AMB[y] > MELT[m] - 200)) m = host; } // caves if (d > 9) { const t1 = Math.abs(fbm(nCave, x / 130, y / 80, 2)); const big = fy > 0.2 ? fbm(nCave2, x / 95, y / 64, 3) : -1; const small = nCave3(x / 26, y / 20); const depthK = Math.min(1, (d - 9) / 50); if (t1 < 0.06 * depthK || big > 0.34 || small > 0.6 - (fy > 0.35 ? 0.05 : 0)) m = EMPTY; } const my = magmaY[x] + nWarp(x / 40 + 120, y / 20) * 12; if (y >= my) m = (nPocket(x / 12, y / 6) > 0.45 && y < my + 6) ? BASALT : MAGMA; // pockets of magma sit in the basalt just above the magma sea else if (m === BASALT && y > my - 50 && nMagC(x / 40, y / 22) > 0.3 + (my - y) / 50 * 0.3) m = MAGMA; if (y >= h - 2) m = BASALT; // aquifers fill the low parts of shallow caves if (m === EMPTY && d > 20 && fy < 0.46) { const wl = nWater(x / 140, 0.5); if (wl > 0.15 && nWater(x / 40, y / 40) > -0.1 && ((y + Math.floor(wl * 40)) % 23) > 13) m = WATER; } } mat[i] = m; if (m !== EMPTY && m !== WATER && m !== MAGMA) flags[i] = F_NAT; else if (m === WATER || m === MAGMA) flags[i] = F_NAT; temp[i] = m === MAGMA ? Math.max(AMB[y], 1180) : AMB[y]; if (PH[m] === PH_LIQUID && FRZ[m]) life[i] = 255; } if (onProgress && (y & 63) === 0) onProgress(y / h * 0.7); } clearHardSpecks(false); // (tree bases are collected after the trees below) // snow lies on the high ground wherever the air is below freezing for (let x = 1; x < w - 1; x++) { const sy = surfY[x]; if (sy < 3 || AMB[sy - 1] > 0) continue; const i0 = sy * w + x; if (mat[i0] === GRASS) mat[i0] = DIRT; const depth = Math.abs(surfY[x + 1] - surfY[x - 1]) <= 3 ? 3 : 1; for (let k = 1; k <= depth; k++) { const i = (sy - k) * w + x; if (mat[i] === EMPTY) { mat[i] = SNOW; flags[i] = F_NAT; temp[i] = AMB[sy - k]; } } } // trees for (let x = 12; x < w - 12;) { const sy = surfY[x]; const ok = mat[sy * w + x] === GRASS && mat[sy * w + x + 1] === GRASS && !noTree[x] && Math.abs(x - spawnX) > 6 && !(x > spawnX + 14 && x < spawnX + 72); // none where the dock goes if (ok && rng() < 0.7) { const th = 9 + Math.floor(rng() * 8); const setW = (xx, yy) => { if (xx < 0 || yy < 1 || xx >= w) return; const i = yy * w + xx; if (mat[i] === EMPTY) bgm[i] = WOOD; }; for (let k = 1; k <= th; k++) for (let dx = 0; dx < 2; dx++) setW(x + dx, sy - k); // two branches for (const side of [-1, 1]) { const by = sy - th + 3 + Math.floor(rng() * 3); for (let k = 1; k <= 3; k++) setW(x + (side < 0 ? -k : 1 + k), by - (k >> 1)); } // lumpy canopy made of a few overlapping blobs const blobs = [[0.5, -th - 1, 6 + rng() * 2.5], [-3.5, -th + 1, 4 + rng() * 2], [4.5, -th + 1, 4 + rng() * 2], [0.5, -th - 5, 4 + rng() * 2]]; for (const [bx, by, r] of blobs) { const cx = x + bx, cy = sy + by; for (let yy = Math.floor(cy - r); yy <= cy + r; yy++) for (let xx = Math.floor(cx - r); xx <= cx + r; xx++) { if (xx < 0 || yy < 1 || xx >= w) continue; const ex = (xx - cx) / r, ey = (yy - cy) / (r * 0.85); if (ex * ex + ey * ey <= 1 && hash2(xx, yy, worldSeed + 7) % 9 !== 0) { const i = yy * w + xx; if (mat[i] === EMPTY && !bgm[i]) bgm[i] = LEAVES; } } } x += 16 + Math.floor(rng() * 20); } else x += 4; } // springs: in cave side walls just above the cave floor { const cands = []; for (let y = 0; y < h - 4; y++) { if (depthU(y) > 0.45) break; for (let x = 2; x < w - 2; x++) { const i = y * w + x; if (!(flags[i] & F_NAT) || PH[mat[i]] !== PH_SOLID || mat[i] === LEAVES || mat[i] === WOOD) continue; if (y - surfY[x] < 16) continue; for (const dx of [-1, 1]) { const e = i + dx; if (mat[e] !== EMPTY) continue; // floor 2-4 cells below the open cell, and solid above the spring if (PH[mat[e + w]] === PH_EMPTY && PH[mat[e + 2 * w]] !== PH_EMPTY && PH[mat[i - w]] === PH_SOLID && PH[mat[i + w]] === PH_SOLID) cands.push(i); } } } const picks = []; const near = cands.slice().sort((a, b) => Math.abs(a % w - spawnX) - Math.abs(b % w - spawnX)); for (const c of near) { if (picks.length >= 2) break; if (picks.every(p => Math.abs(p % w - c % w) > 30)) picks.push(c); } for (let k = 0; k < 400 && picks.length < 10 && cands.length; k++) { const c = cands[Math.floor(rng() * cands.length)]; if (picks.every(p => Math.abs(p % w - c % w) > 50 || Math.abs(((p / w) | 0) - ((c / w) | 0)) > 40)) picks.push(c); } // Springs were taken out of the game (rain refills the world instead). The spots are still // drawn above so every seed keeps making the same world it always did. void picks; } fillNoise(); if (onProgress) onProgress(0.75); // settle loose material awake.fill(3); for (let k = 0; k < 260; k++) { tick++; tickStamp = (tickStamp % 255) + 1; simMove(); let any = 0; for (let c = 0; c < NCH; c++) if (awake[c]) { awake[c]--; any++; } if (!any) break; if (onProgress && (k & 31) === 0) onProgress(0.75 + 0.25 * k / 260); } // re-ambient after settling, then start clean for (let i = 0; i < N; i++) { const m = mat[i]; if (m !== MAGMA) temp[i] = AMB[(i / w) | 0]; life[i] = PH[m] === PH_LIQUID && FRZ[m] ? 255 : 0; } tick = 0; findTreeBases(); placeDock(spawnX + 34); hot.fill(0); awake.fill(0); updateRoof(); resetEconomy(); resetWeather(); weather.base = worldWater(); bot.x = spawnX - 2; bot.y = surfY[spawnX] - 14; bot.vx = 0; bot.vy = 0; while (botOverlapSolid(bot.x, bot.y) && bot.y > 2) bot.y -= 1; bag = new Int32Array(NMAT); bagTotal = 0; player.bitTier = 0; player.tankTier = 0; player.lens = false; player.flask = false; seen = new Uint8Array(NMAT); ledger = {}; stats.dug = stats.moved = stats.placed = stats.ignitions = stats.cast = stats.bars = 0; EV.length = 0; fogInit(); fogLook(bot.x + bot.w / 2, bot.y + bot.h / 2); if (onProgress) onProgress(1); }