barkour/js/mapgen.js

210 lines
8.5 KiB
JavaScript
Raw Permalink Normal View History

// 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) ---
// The player box is 18px wide (~1.5 tiles), so a valid spawn column needs
// its neighbours' ground no higher than its own, or the dog spawns embedded.
const spawnCols = [];
const colOk = (x) =>
x >= 1 && x < GW - 1 && !spawnCols.includes(x) &&
hs[x] <= GH - 2 && hs[x - 1] <= GH - 2 && hs[x + 1] <= GH - 2;
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 (!colOk(x) && tries++ < 90) x = (x + 1) % (GW - 4) + 2; // skip pits/steps
if (colOk(x)) 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);
// pixel spawn points sit above the highest ground of the 3-column footprint
const spawns = spawnCols.map(x => {
const surf = Math.min(hs[x - 1] ?? GH, hs[x] <= GH ? hs[x] : GH - 4, hs[x + 1] ?? GH);
return { x: x * TILE + TILE / 2, y: surf * TILE - 10 };
});
// --- 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++) {
if (hs[x] > GH) continue; // don't hang tower slabs over pit columns
for (let y = hs[x] - th; y < hs[x]; 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 ---
// A path is rejected if it grinds through terrain (invisible through-wall
// hits) or sweeps within kill radius of a spawn point.
const solidT = (t) => t === T.SOLID || t === T.CRYSTAL || t === T.PAD;
const sawPathOk = (pts, loop) => {
const nSegs = loop ? pts.length : pts.length - 1;
for (let s = 0; s < nSegs; s++) {
const a = pts[s], b = pts[(s + 1) % pts.length];
const len = Math.hypot(b.x - a.x, b.y - a.y) || 1;
for (let d = 0; d <= len; d += 6) {
const px = a.x + (b.x - a.x) * d / len, py = a.y + (b.y - a.y) * d / len;
for (const [ox, oy] of [[0, 0], [12, 0], [-12, 0], [0, 12], [0, -12]]) {
if (solidT(get(Math.floor((px + ox) / TILE), Math.floor((py + oy) / TILE)))) return false;
}
for (const sp of spawns) if (Math.hypot(sp.x - px, sp.y - py) < 30) return false;
}
}
return true;
};
const saws = [];
const nSaws = r.int(1, 3);
for (let i = 0; i < nSaws; i++) {
for (let t = 0; t < 30; t++) {
const y = (r.int(10, 26)) * TILE;
const x0 = r.int(8, GW - 30) * TILE, x1 = x0 + r.int(12, 20) * TILE;
let pts, loop = false;
if (r.chance(0.5)) {
pts = [{ x: x0, y }, { x: x1, y }];
} else {
const y2 = y + r.int(4, 8) * TILE; // rectangle loop
pts = [{ x: x0, y }, { x: x1, y }, { x: x1, y: y2 }, { x: x0, y: y2 }];
loop = true;
}
if (!sawPathOk(pts, loop)) continue;
saws.push({ pts, speed: 1.1 + r.f() * 1.1, prog: r.f(), x: x0, y, loop });
break;
}
}
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 };
}