/* ========================================================= * REPRTERRA WEB — world.js * Procedural map generation (per run, like Repterra): * grass/dirt/water, forest & rock deposits, dino caves. * ========================================================= */ 'use strict'; window.RTS = window.RTS || {}; RTS.world = (function () { const U = RTS.util; const W = RTS.CONFIG.WORLD.W; const H = RTS.CONFIG.WORLD.H; // value noise with bilinear interp + octaves function makeNoise(rng, size) { const g = new Float32Array(size * size); for (let i = 0; i < g.length; i++) g[i] = rng(); return function (x, y) { const xi = Math.floor(x), yi = Math.floor(y); const xf = x - xi, yf = y - yi; const sx = xf * xf * (3 - 2 * xf), sy = yf * yf * (3 - 2 * yf); const x0 = ((xi % size) + size) % size, y0 = ((yi % size) + size) % size; const x1 = (x0 + 1) % size, y1 = (y0 + 1) % size; const a = U.lerp(g[y0 * size + x0], g[y0 * size + x1], sx); const b = U.lerp(g[y1 * size + x0], g[y1 * size + x1], sx); return U.lerp(a, b, sy); }; } function generate(seed) { const rng = U.makeRng(seed || ((Date.now() / 1000) | 0)); const T = { W, H, terrain: new Uint8Array(W * H), // 0 grass, 1 grass2, 2 dirt, 3 water tree: new Uint8Array(W * H), // wood units on tile (0..8) treeMax: new Uint8Array(W * H), rock: new Uint8Array(W * H), // stone units rockMax: new Uint8Array(W * H), variant: new Float32Array(W * H), // visual noise }; const n1 = makeNoise(rng, 16), n2 = makeNoise(rng, 32), n3 = makeNoise(rng, 64); // --- base terrain + water --- for (let y = 0; y < H; y++) { for (let x = 0; x < W; x++) { const i = y * W + x; let h = n1(x / 11, y / 11) * 0.55 + n2(x / 5.5, y / 5.5) * 0.3 + n3(x / 2.6, y / 2.6) * 0.15; T.variant[i] = n3(x * 1.7, y * 1.7); // keep center playable const cx = x - W / 2, cy = y - H / 2; const dc = Math.sqrt(cx * cx + cy * cy); h += Math.max(0, (dc - 9)) * 0.004; // slight rise away from base if (h < 0.30 && dc > 10) { T.terrain[i] = 3; continue; } // lake T.terrain[i] = h < 0.42 ? 1 : (h < 0.47 ? 2 : 0); if (rng() < 0.02 && T.terrain[i] === 1) T.terrain[i] = 2; } } const clearArea = (cx, cy, r) => { for (let y = Math.max(0, cy - r); y <= Math.min(H - 1, cy + r); y++) for (let x = Math.max(0, cx - r); x <= Math.min(W - 1, cx + r); x++) { const i = y * W + x; if (U.dist(x, y, cx, cy) <= r) { T.tree[i] = 0; T.rock[i] = 0; } } }; const unwaterArea = (cx, cy, r) => { for (let y = Math.max(0, cy - r); y <= Math.min(H - 1, cy + r); y++) for (let x = Math.max(0, cx - r); x <= Math.min(W - 1, cx + r); x++) { const i = y * W + x; if (U.dist(x, y, cx, cy) <= r && T.terrain[i] === 3) T.terrain[i] = 1; } }; // --- HQ site: near center --- const hq = { x: Math.floor(W / 2), y: Math.floor(H / 2) }; while (T.terrain[hq.y * W + hq.x] === 3) hq.x--; unwaterArea(hq.x, hq.y, 9); clearArea(hq.x, hq.y, 7); // --- forests: blobby clusters --- const forests = []; const nForest = 14; for (let f = 0; f < nForest; f++) { const fx = rng.int(4, W - 5), fy = rng.int(4, H - 5); if (U.dist(fx, fy, hq.x, hq.y) < 9) continue; const fr = rng.range(2.5, 4.8); forests.push({ fx, fy, fr }); for (let y = Math.max(0, fy - 6); y <= Math.min(H - 1, fy + 6); y++) for (let x = Math.max(0, fx - 6); x <= Math.min(W - 1, fx + 6); x++) { const i = y * W + x; const d = U.dist(x, y, fx, fy) + n3(x / 2.2, y / 2.2) * 1.6; if (d < fr && T.terrain[i] !== 3 && !(Math.abs(x - hq.x) < 7 && Math.abs(y - hq.y) < 7)) { T.treeMax[i] = 8; T.tree[i] = 8; T.rock[i] = 0; } } } // --- rocks: scattered outcrops --- const nRock = 12; for (let f = 0; f < nRock; f++) { const rx = rng.int(4, W - 5), ry = rng.int(4, H - 5); if (U.dist(rx, ry, hq.x, hq.y) < 10) continue; const rr = rng.range(1.6, 3.0); for (let y = Math.max(0, ry - 4); y <= Math.min(H - 1, ry + 4); y++) for (let x = Math.max(0, rx - 4); x <= Math.min(W - 1, rx + 4); x++) { const i = y * W + x; const d = U.dist(x, y, rx, ry) + n3(x / 1.8, y / 1.8) * 1.2; if (d < rr && T.terrain[i] !== 3 && !(Math.abs(x - hq.x) < 8 && Math.abs(y - hq.y) < 8)) { T.rockMax[i] = 10; T.rock[i] = 10; T.tree[i] = 0; T.treeMax[i] = 0; } } } // guarantee some starting resources within reach of HQ const ensureNear = (field, fieldMax, want) => { let placed = 0, guard = 0; while (placed < want && guard++ < 400) { const ang = rng() * Math.PI * 2; const dd = rng.range(4.5, 8.5); const x = Math.round(hq.x + Math.cos(ang) * dd); const y = Math.round(hq.y + Math.sin(ang) * dd); if (!U.inBounds(x, y, W, H)) continue; const i = y * W + x; if (T.terrain[i] === 3) { T.terrain[i] = 1; } // small patch for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) { const j = (y + dy) * W + (x + dx); if (!U.inBounds(x + dx, y + dy, W, H)) continue; if (T.terrain[j] !== 3 && rng() < 0.75 && !T.rock[j]) { field[j] = fieldMax[j] = field === T.tree ? 8 : 10; } } placed++; } }; ensureNear(T.tree, T.treeMax, 4); ensureNear(T.rock, T.rockMax, 3); return { tiles: T, hq, seed, rng }; } return { generate }; })();