- Isometric canvas RTS vs dinosaur waves (fan demake of Repterra) - Economy: houses/taxes, farms, foresters, quarries; colonist staffing - Power grid: generators extend build range; brownout + recovery - Defense: walls/gates, watchtowers (AA), cannon towers (ground-only) - Taming: Primal Pen + Tamers collar weakened dinos; pets obey commands - Breeding: tamed pairs incubate eggs at the pen; hatchlings grow up - 7 dino species incl. flying Pteranodons and lake-raiding Suchomimus - Telegraphed waves with direction arrows; day-15 final horde; 3 difficulties - Day/night cycle, fog of war, minimap, synth audio, 1x-3x speeds - Save/Load/Continue + dawn autosave (full JSON state snapshots) - Tests: 80-assertion headless suite, browser boot + E2E, balance harness
147 lines
5.6 KiB
JavaScript
147 lines
5.6 KiB
JavaScript
/* =========================================================
|
|
* 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 };
|
|
})();
|