179 lines
7.1 KiB
JavaScript
179 lines
7.1 KiB
JavaScript
|
|
// Procedural arena generation. Fully seeded — both peers generate the same map
|
||
|
|
// from the match seed; only the host simulates, the seed just saves bandwidth.
|
||
|
|
import { TILE, GW, GH, T } from './consts.js';
|
||
|
|
import { makeRng } from './rng.js';
|
||
|
|
|
||
|
|
export const PALETTES = [
|
||
|
|
{ name: 'midnight', sky: ['#0b1026', '#1b2845'], hill: '#141d3d', solid: '#3a4466', top: '#5a6e9e', plat: '#c0863f', spike: '#aab4cc', crystal: '#7df9ff', pad: '#ffd166', star: true },
|
||
|
|
{ name: 'sunset', sky: ['#2d1b4e', '#b4496b'], hill: '#451f55', solid: '#5d4157', top: '#ec9a5e', plat: '#7b4b94', spike: '#f3d9c9', crystal: '#ffe066', pad: '#9ef01a', star: false },
|
||
|
|
{ name: 'forest', sky: ['#0e2a1f', '#1f5840'], hill: '#123524', solid: '#4a3b2a', top: '#69a14a', plat: '#8a6b3d', spike: '#d8e0c0', crystal: '#ff7edb', pad: '#ffe45e', star: false },
|
||
|
|
{ name: 'candy', sky: ['#341a4f', '#7b2d8b'], hill: '#4b2266', solid: '#9b5d9e', top: '#f5b0cb', plat: '#54d6d6', spike: '#fff0f5', crystal: '#a6ff4d', pad: '#ff9f1c', star: true },
|
||
|
|
];
|
||
|
|
|
||
|
|
export function generateMap(seed) {
|
||
|
|
const r = makeRng(seed);
|
||
|
|
const tiles = new Uint8Array(GW * GH);
|
||
|
|
const hp = {}; // crystal tile hp, keyed by tile index
|
||
|
|
const set = (x, y, v) => { if (x >= 0 && x < GW && y >= 0 && y < GH) tiles[y * GW + x] = v; };
|
||
|
|
const get = (x, y) => (x < 0 || x >= GW || y < 0 || y >= GH) ? T.SOLID : tiles[y * GW + x];
|
||
|
|
|
||
|
|
// --- ground heightmap (stepped terrain) ---
|
||
|
|
let h = GH - 5 + r.int(-1, 1);
|
||
|
|
const hs = []; // surface row per column; > GH means pit
|
||
|
|
for (let x = 0; x < GW; x++) {
|
||
|
|
if (x % 4 === 0) h += r.int(-2, 2);
|
||
|
|
h = Math.max(GH - 12, Math.min(GH - 3, h));
|
||
|
|
hs.push(h);
|
||
|
|
for (let y = h; y < GH; y++) set(x, y, T.SOLID);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- pits into the abyss ---
|
||
|
|
const nPits = r.int(1, 2);
|
||
|
|
for (let i = 0; i < nPits; i++) {
|
||
|
|
const gx = r.int(12, GW - 18), gw = r.int(3, 4);
|
||
|
|
for (let x = gx; x < gx + gw; x++) {
|
||
|
|
for (let y = 0; y < GH; y++) if (tiles[y * GW + x] === T.SOLID) set(x, y, T.EMPTY);
|
||
|
|
hs[x] = GH + 9;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- spawn columns (picked early so hazards can avoid them) ---
|
||
|
|
const spawnCols = [];
|
||
|
|
const wanted = [0.12, 0.38, 0.62, 0.88].map(f => Math.floor(GW * f) + r.int(-3, 3));
|
||
|
|
for (let sx of wanted) {
|
||
|
|
let x = Math.max(2, Math.min(GW - 3, sx));
|
||
|
|
let tries = 0;
|
||
|
|
while (hs[x] > GH - 2 && tries++ < 20) x = (x + 1) % (GW - 4) + 2; // skip pits
|
||
|
|
if (hs[x] <= GH - 2) spawnCols.push(x);
|
||
|
|
}
|
||
|
|
if (spawnCols.length < 2) spawnCols.push(4, GW - 5); // paranoid fallback
|
||
|
|
const nearSpawn = (x) => spawnCols.some(sx => Math.abs(sx - x) < 5);
|
||
|
|
|
||
|
|
// --- wall-jump towers rising from the ground ---
|
||
|
|
const nTowers = r.int(2, 3);
|
||
|
|
for (let i = 0; i < nTowers; i++) {
|
||
|
|
const tx = r.int(8, GW - 9);
|
||
|
|
if (hs[tx] > GH || nearSpawn(tx)) continue;
|
||
|
|
const th = r.int(5, 9), tw = r.int(1, 2);
|
||
|
|
for (let x = tx; x < tx + tw; x++)
|
||
|
|
for (let y = hs[tx] - th; y < hs[tx]; y++) set(x, y, T.SOLID);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- floating islands ---
|
||
|
|
const islands = [];
|
||
|
|
const nIsl = r.int(3, 5);
|
||
|
|
for (let i = 0; i < nIsl; i++) {
|
||
|
|
const iw = r.int(4, 8), ih = r.int(2, 3);
|
||
|
|
const ix = r.int(4, GW - iw - 4), iy = r.int(8, 22);
|
||
|
|
let clear = true;
|
||
|
|
for (let x = ix - 1; x < ix + iw + 1 && clear; x++)
|
||
|
|
for (let y = iy - 3; y < iy + ih + 3; y++) if (get(x, y) !== T.EMPTY) { clear = false; break; }
|
||
|
|
if (!clear) continue;
|
||
|
|
islands.push({ ix, iy, iw, ih });
|
||
|
|
for (let x = ix; x < ix + iw; x++)
|
||
|
|
for (let y = iy; y < iy + ih; y++) set(x, y, T.SOLID);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- one-way platforms ---
|
||
|
|
const nPlat = r.int(6, 9);
|
||
|
|
for (let i = 0; i < nPlat; i++) {
|
||
|
|
const pw = r.int(4, 7), px = r.int(3, GW - pw - 3), py = r.int(7, GH - 16);
|
||
|
|
let ok = true;
|
||
|
|
for (let x = px - 1; x < px + pw + 1 && ok; x++)
|
||
|
|
for (let y = py - 2; y < py + 3; y++) if (get(x, y) !== T.EMPTY) { ok = false; break; }
|
||
|
|
if (ok) for (let x = px; x < px + pw; x++) set(x, py, T.PLAT);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- crystals: outcrops sitting on surfaces (mine them for powerups) ---
|
||
|
|
let placed = 0, tries = 0;
|
||
|
|
while (placed < 10 && tries++ < 300) {
|
||
|
|
const onIsland = islands.length > 0 && r.chance(0.4);
|
||
|
|
let x, surf;
|
||
|
|
if (onIsland) {
|
||
|
|
const isl = r.pick(islands);
|
||
|
|
x = isl.ix + r.int(0, isl.iw - 1); surf = isl.iy;
|
||
|
|
} else {
|
||
|
|
x = r.int(2, GW - 3); surf = hs[x];
|
||
|
|
if (surf > GH - 2) continue;
|
||
|
|
}
|
||
|
|
if (nearSpawn(x)) continue;
|
||
|
|
if (get(x, surf - 1) !== T.EMPTY || get(x, surf - 2) !== T.EMPTY) continue;
|
||
|
|
set(x, surf - 1, T.CRYSTAL); hp[(surf - 1) * GW + x] = 3;
|
||
|
|
if (r.chance(0.3) && get(x, surf - 3) === T.EMPTY) { set(x, surf - 2, T.CRYSTAL); hp[(surf - 2) * GW + x] = 3; }
|
||
|
|
placed++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- bounce pads (solid bumps on the ground) ---
|
||
|
|
let pads = 0; tries = 0;
|
||
|
|
while (pads < r.int(2, 3) + 1 && tries++ < 100) {
|
||
|
|
const x = r.int(3, GW - 4), surf = hs[x];
|
||
|
|
if (surf > GH - 2 || nearSpawn(x)) continue;
|
||
|
|
if (get(x, surf - 1) !== T.EMPTY || get(x, surf - 2) !== T.EMPTY) continue;
|
||
|
|
set(x, surf - 1, T.PAD);
|
||
|
|
pads++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- spike patches on the ground surface ---
|
||
|
|
let patches = 0; tries = 0;
|
||
|
|
while (patches < r.int(2, 4) && tries++ < 100) {
|
||
|
|
const x0 = r.int(3, GW - 8), w = r.int(2, 3);
|
||
|
|
let ok = true;
|
||
|
|
for (let x = x0; x < x0 + w; x++)
|
||
|
|
if (hs[x] > GH - 2 || nearSpawn(x) || get(x, hs[x] - 1) !== T.EMPTY) ok = false;
|
||
|
|
if (!ok) continue;
|
||
|
|
for (let x = x0; x < x0 + w; x++) set(x, hs[x] - 1, T.SPIKE);
|
||
|
|
patches++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- rail saws: hazards moving along fixed paths through open air ---
|
||
|
|
const saws = [];
|
||
|
|
const nSaws = r.int(1, 3);
|
||
|
|
for (let i = 0; i < nSaws; i++) {
|
||
|
|
const y = (r.int(10, 26)) * TILE;
|
||
|
|
const x0 = r.int(8, GW - 30) * TILE, x1 = x0 + r.int(12, 20) * TILE;
|
||
|
|
if (r.chance(0.5)) {
|
||
|
|
saws.push({ pts: [{ x: x0, y }, { x: x1, y }], speed: 1.1 + r.f() * 1.1, prog: r.f(), x: x0, y });
|
||
|
|
} else {
|
||
|
|
const y2 = y + r.int(4, 8) * TILE; // rectangle loop
|
||
|
|
saws.push({ pts: [{ x: x0, y }, { x: x1, y }, { x: x1, y: y2 }, { x: x0, y: y2 }], speed: 1.2 + r.f(), prog: r.f(), x: x0, y, loop: true });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- spawn points (pixel coords, centered above surface) ---
|
||
|
|
const spawns = spawnCols.map(x => {
|
||
|
|
let surf = hs[x] <= GH ? hs[x] : GH - 4;
|
||
|
|
return { x: x * TILE + TILE / 2, y: surf * TILE - 10 };
|
||
|
|
});
|
||
|
|
|
||
|
|
return { seed, tiles, hp, spawns, saws, pal: r.int(0, PALETTES.length - 1) };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Position of a saw along its (possibly looping) polyline at progress t in [0,1).
|
||
|
|
export function sawPos(saw, prog) {
|
||
|
|
const pts = saw.pts;
|
||
|
|
const segs = [];
|
||
|
|
let total = 0;
|
||
|
|
const n = saw.loop ? pts.length : pts.length * 2 - 2; // ping-pong for open paths
|
||
|
|
const at = (i) => {
|
||
|
|
if (saw.loop) return pts[i % pts.length];
|
||
|
|
const m = (pts.length - 1) * 2;
|
||
|
|
const j = i % m;
|
||
|
|
return j < pts.length ? pts[j] : pts[m - j];
|
||
|
|
};
|
||
|
|
for (let i = 0; i < n; i++) {
|
||
|
|
const a = at(i), b = at(i + 1);
|
||
|
|
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
||
|
|
segs.push({ a, b, len });
|
||
|
|
total += len;
|
||
|
|
}
|
||
|
|
let d = ((prog % 1) + 1) % 1 * total;
|
||
|
|
for (const s of segs) {
|
||
|
|
if (d <= s.len) {
|
||
|
|
const f = s.len ? d / s.len : 0;
|
||
|
|
return { x: s.a.x + (s.b.x - s.a.x) * f, y: s.a.y + (s.b.y - s.a.y) * f, total };
|
||
|
|
}
|
||
|
|
d -= s.len;
|
||
|
|
}
|
||
|
|
return { x: pts[0].x, y: pts[0].y, total };
|
||
|
|
}
|