Files
deepseek 8fbe70d0b0 Repterra Web — full game: base building, power grid, taming & breeding, aquatic raiders, day/night, save/load
- 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
2026-08-23 07:00:23 +00:00

191 lines
6.6 KiB
JavaScript

/* =========================================================
* REPRTERRA WEB — utils.js
* Math, RNG, A* pathfinding, spatial hash.
* ========================================================= */
'use strict';
window.RTS = window.RTS || {};
RTS.util = (function () {
const U = {};
// ---------- math ----------
U.clamp = (v, a, b) => v < a ? a : (v > b ? b : v);
U.lerp = (a, b, t) => a + (b - a) * t;
U.dist2 = (ax, ay, bx, by) => { const dx = ax - bx, dy = ay - by; return dx * dx + dy * dy; };
U.dist = (ax, ay, bx, by) => Math.sqrt(U.dist2(ax, ay, bx, by));
U.angleTo = (ax, ay, bx, by) => Math.atan2(by - ay, bx - ax);
U.angleLerp = (a, b, t) => {
let d = (b - a) % (Math.PI * 2);
if (d > Math.PI) d -= Math.PI * 2;
if (d < -Math.PI) d += Math.PI * 2;
return a + d * t;
};
// Mulberry32 seeded RNG
U.makeRng = function (seed) {
let s = seed >>> 0;
const rng = function () {
s |= 0; s = (s + 0x6D2B79F5) | 0;
let t = Math.imul(s ^ (s >>> 15), 1 | s);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
rng.range = (a, b) => a + rng() * (b - a);
rng.int = (a, b) => Math.floor(rng.range(a, b + 1));
rng.pick = (arr) => arr[Math.floor(rng() * arr.length)];
// save/load support (does not alter the sequence)
rng.state = () => s >>> 0;
rng.setState = (v) => { s = v >>> 0; };
return rng;
};
U.rng = U.makeRng(Date.now() & 0xffffffff);
// ---------- grid helpers ----------
U.idx = (x, y, W) => y * W + x;
U.inBounds = (x, y, W, H) => x >= 0 && y >= 0 && x < W && y < H;
// ---------- spatial hash ----------
U.SpatialHash = function (cellSize, W, H) {
this.cs = cellSize;
this.cw = Math.ceil(W / cellSize);
this.ch = Math.ceil(H / cellSize);
this.buckets = new Array(this.cw * this.ch);
for (let i = 0; i < this.buckets.length; i++) this.buckets[i] = [];
};
U.SpatialHash.prototype.clear = function () {
for (let i = 0; i < this.buckets.length; i++) this.buckets[i].length = 0;
};
U.SpatialHash.prototype._key = function (x, y) {
const cx = U.clamp(Math.floor(x / this.cs), 0, this.cw - 1);
const cy = U.clamp(Math.floor(y / this.cs), 0, this.ch - 1);
return cy * this.cw + cx;
};
U.SpatialHash.prototype.insert = function (e) {
this.buckets[this._key(e.x, e.y)].push(e);
};
// iterate entities within radius r of (x,y); cb(entity) -> truthy stops
U.SpatialHash.prototype.eachNear = function (x, y, r, cb) {
const minx = U.clamp(Math.floor((x - r) / this.cs), 0, this.cw - 1);
const maxx = U.clamp(Math.floor((x + r) / this.cs), 0, this.cw - 1);
const miny = U.clamp(Math.floor((y - r) / this.cs), 0, this.ch - 1);
const maxy = U.clamp(Math.floor((y + r) / this.cs), 0, this.ch - 1);
const r2 = r * r;
for (let cy = miny; cy <= maxy; cy++) {
for (let cx = minx; cx <= maxx; cx++) {
const b = this.buckets[cy * this.cw + cx];
for (let i = 0; i < b.length; i++) {
const e = b[i];
if (U.dist2(x, y, e.x, e.y) <= r2) { if (cb(e)) return true; }
}
}
}
return false;
};
U.SpatialHash.prototype.nearest = function (x, y, r, filter) {
let best = null, bd = Infinity;
this.eachNear(x, y, r, (e) => {
if (filter && !filter(e)) return false;
const d = U.dist2(x, y, e.x, e.y);
if (d < bd) { bd = d; best = e; }
return false;
});
return best;
};
// ---------- A* on blocked grid ----------
// blocked: Uint8Array(W*H), 1 = blocked. 8-directional, no corner cutting.
U.findPath = function (sx, sy, tx, ty, W, H, blocked, maxNodes) {
sx |= 0; sy |= 0; tx |= 0; ty |= 0;
if (!U.inBounds(tx, ty, W, H)) return null;
if (blocked[U.idx(tx, ty, W)]) {
// find nearest free tile to target
let found = false;
outer:
for (let rr = 1; rr <= 3; rr++) {
for (let dy = -rr; dy <= rr; dy++) for (let dx = -rr; dx <= rr; dx++) {
const nx = tx + dx, ny = ty + dy;
if (U.inBounds(nx, ny, W, H) && !blocked[U.idx(nx, ny, W)]) { tx = nx; ty = ny; found = true; break outer; }
}
}
if (!found) return null;
}
if (sx === tx && sy === ty) return [];
maxNodes = maxNodes || 6000;
const N = W * H;
const gScore = new Float32Array(N).fill(Infinity);
const cameFrom = new Int32Array(N).fill(-1);
const closed = new Uint8Array(N);
const open = []; // binary heap of [f, nodeIdx]
const push = (f, n) => {
open.push([f, n]);
let i = open.length - 1;
while (i > 0) {
const p = (i - 1) >> 1;
if (open[p][0] <= open[i][0]) break;
const t = open[p]; open[p] = open[i]; open[i] = t; i = p;
}
};
const pop = () => {
const top = open[0];
const last = open.pop();
if (open.length) {
open[0] = last;
let i = 0;
for (;;) {
const l = 2 * i + 1, r = l + 1;
let m = i;
if (l < open.length && open[l][0] < open[m][0]) m = l;
if (r < open.length && open[r][0] < open[m][0]) m = r;
if (m === i) break;
const t = open[m]; open[m] = open[i]; open[i] = t; i = m;
}
}
return top;
};
const startI = U.idx(sx, sy, W), goalI = U.idx(tx, ty, W);
const h = (x, y) => { const dx = Math.abs(x - tx), dy = Math.abs(y - ty); return (dx + dy) * 0.99 + Math.min(dx, dy) * 0.42; };
gScore[startI] = 0;
push(h(sx, sy), startI);
let nodes = 0;
const DIRS = [[1,0,1],[ -1,0,1],[0,1,1],[0,-1,1],[1,1,1.42],[1,-1,1.42],[-1,1,1.42],[-1,-1,1.42]];
while (open.length && nodes < maxNodes) {
const [, cur] = pop();
if (cur === goalI) break;
if (closed[cur]) continue;
closed[cur] = 1;
nodes++;
const cx = cur % W, cy = (cur / W) | 0;
for (let k = 0; k < 8; k++) {
const nx = cx + DIRS[k][0], ny = cy + DIRS[k][1];
if (!U.inBounds(nx, ny, W, H)) continue;
const ni = U.idx(nx, ny, W);
if (blocked[ni] || closed[ni]) continue;
if (k >= 4) { // no corner cutting through blocked tiles
if (blocked[U.idx(cx + DIRS[k][0], cy, W)] || blocked[U.idx(cx, cy + DIRS[k][1], W)]) continue;
}
const ng = gScore[cur] + DIRS[k][2];
if (ng < gScore[ni]) {
gScore[ni] = ng;
cameFrom[ni] = cur;
push(ng + h(nx, ny), ni);
}
}
}
if (cameFrom[goalI] === -1 && goalI !== startI) return null;
// rebuild
const path = [];
let c = goalI;
while (c !== startI && c !== -1) {
path.push({ x: (c % W) + 0.5, y: ((c / W) | 0) + 0.5 });
c = cameFrom[c];
}
path.reverse();
return path;
};
return U;
})();