barkour/js/mapgen_arena.js
w1n5t0n f57c1f16f8 feat: top-down 'Dogpit' arena mode (dota-like view)
Second game mode selectable at dog select (host-authoritative online):
- js/sim_arena.js: top-down sim, same export surface as sim.js — x/y plane
  plus a z hop axis (Space / comma); KOs by knocking dogs into abyss pits
- js/mapgen_arena.js: mirrored symmetric arenas — walls, pits, center
  clearing, guaranteed spawn-to-center corridors, crystals/spikes/launch
  pads, mirrored saw pairs; fallback pit pair since pits are the only KO
  mechanic
- specials re-imagined per dog: zoom dash, ram + hop-slam, zip-leash
  slingshot, blink, hover (floats over pits/saws/spikes)
- render: arena tiles with wall shadows + void pits, top-down dog drawing
  with z-lift over a ground shadow; new fall event/sfx
- B.HOP input bit so 'up' can mean north in top-down (Space also jumps in
  side view); mode toggle UI + mode carried in start/rematch net messages

Tested: arena mapgen determinism/symmetry/connectivity + chaotic matches +
pit/hop scenarios headless; local chaos + full P2P arena match (host mode
flip syncs to client) in two headless browsers with zero page errors.
2026-06-10 15:23:22 +02:00

156 lines
6.6 KiB
JavaScript

// Top-down "Dogpit" arena generation. Mirrored left↔right (dota-style fairness):
// border walls, wall blobs, abyss pits, a cleared center circle, guaranteed
// corridors spawn→center, crystals/spikes/launch pads, patrolling saws.
import { TILE, GW, GH, W, T } from './consts.js';
import { makeRng } from './rng.js';
export function generateArena(seed) {
const r = makeRng(seed);
const tiles = new Uint8Array(GW * GH);
const hp = {};
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];
const HALF = GW >> 1;
// --- border walls (2 thick — nothing leaves the pit) ---
for (let x = 0; x < GW; x++) { set(x, 0, T.SOLID); set(x, 1, T.SOLID); set(x, GH - 1, T.SOLID); set(x, GH - 2, T.SOLID); }
for (let y = 0; y < GH; y++) { set(0, y, T.SOLID); set(1, y, T.SOLID); set(GW - 1, y, T.SOLID); set(GW - 2, y, T.SOLID); }
// --- wall blobs on the left half ---
const nWalls = r.int(8, 12);
for (let i = 0; i < nWalls; i++) {
const w = r.int(2, 6), h = r.int(2, 5);
const x = r.int(3, HALF - 2 - w), y = r.int(3, GH - 4 - h);
for (let yy = y; yy < y + h; yy++) for (let xx = x; xx < x + w; xx++) set(xx, yy, T.SOLID);
}
// --- abyss pits (the kill mechanic: knock dogs in) ---
const wantPits = r.int(2, 4);
let pitBlobs = 0, pitTries = 0;
while (pitBlobs < wantPits && pitTries++ < 150) {
const w = r.int(2, 4), h = r.int(2, 4);
const x = r.int(4, HALF - 3 - w), y = r.int(4, GH - 6 - h);
let clear = true;
for (let yy = y - 1; yy < y + h + 1 && clear; yy++)
for (let xx = x - 1; xx < x + w + 1; xx++) if (get(xx, yy) === T.SOLID) { clear = false; break; }
if (!clear) continue;
for (let yy = y; yy < y + h; yy++) for (let xx = x; xx < x + w; xx++) set(xx, yy, T.PIT);
pitBlobs++;
}
// --- mirror left → right ---
for (let y = 0; y < GH; y++) for (let x = 0; x < HALF; x++) tiles[y * GW + (GW - 1 - x)] = tiles[y * GW + x];
// --- cleared center circle (the contested middle) ---
const ccx = GW / 2 - 0.5, ccy = GH / 2 - 0.5;
for (let y = 2; y < GH - 2; y++) for (let x = 2; x < GW - 2; x++) {
if (Math.hypot(x - ccx, y - ccy) < 7) set(x, y, T.EMPTY);
}
// --- spawns in the four corners, 3x3 cleared ---
const spawnT = [[5, 5], [GW - 6, 5], [5, GH - 6], [GW - 6, GH - 6]];
for (const [sx, sy] of spawnT)
for (let y = sy - 1; y <= sy + 1; y++) for (let x = sx - 1; x <= sx + 1; x++) set(x, y, T.EMPTY);
const nearSpawn = (x, y) => spawnT.some(([sx, sy]) => Math.abs(sx - x) < 5 && Math.abs(sy - y) < 5);
// --- guaranteed L-corridors spawn→center (2 wide) for connectivity ---
const carve = (x, y) => { if (x > 1 && x < GW - 2 && y > 1 && y < GH - 2) set(x, y, T.EMPTY); };
const mx = GW >> 1, my = GH >> 1;
for (const [sx, sy] of spawnT) {
for (let x = Math.min(sx, mx); x <= Math.max(sx, mx); x++) { carve(x, sy); carve(x, sy + 1); }
for (let y = Math.min(sy, my); y <= Math.max(sy, my); y++) { carve(mx, y); carve(mx + 1, y); }
}
// pits are the only KO mechanic — if blob placement + corridor carving left
// too few, stamp a guaranteed mirrored pair clear of corridors and spawns
let pitTiles = 0;
for (const t of tiles) if (t === T.PIT) pitTiles++;
if (pitTiles < 6) {
for (let y = 12; y <= 13; y++) for (let x = HALF - 12; x <= HALF - 10; x++) {
set(x, y, T.PIT);
set(GW - 1 - x, y, T.PIT);
}
}
// --- decorations, placed on the left and mirrored for fairness ---
const mirrorSet = (x, y, v, crystal) => {
set(x, y, v);
set(GW - 1 - x, y, v);
if (crystal) { hp[y * GW + x] = 3; hp[y * GW + (GW - 1 - x)] = 3; }
};
const floorAt = (x, y) => get(x, y) === T.EMPTY;
const wallAdjacent = (x, y) =>
get(x + 1, y) === T.SOLID || get(x - 1, y) === T.SOLID || get(x, y + 1) === T.SOLID || get(x, y - 1) === T.SOLID;
// crystals hug wall faces
let placed = 0, tries = 0;
while (placed < 7 && tries++ < 300) {
const x = r.int(3, HALF - 2), y = r.int(3, GH - 4);
if (!floorAt(x, y) || !wallAdjacent(x, y) || nearSpawn(x, y)) continue;
if (!floorAt(GW - 1 - x, y)) continue; // keep the mirror twin valid too
mirrorSet(x, y, T.CRYSTAL, true);
placed++;
}
// spike patches on open floor
let spikes = 0; tries = 0;
while (spikes < r.int(2, 4) && tries++ < 200) {
const x = r.int(4, HALF - 3), y = r.int(4, GH - 5);
if (!floorAt(x, y) || !floorAt(x + 1, y) || nearSpawn(x, y)) continue;
mirrorSet(x, y, T.SPIKE); mirrorSet(x + 1, y, T.SPIKE);
spikes++;
}
// launch pads (walkable; fling you into a high hop over pits)
let pads = 0; tries = 0;
while (pads < r.int(2, 3) && tries++ < 200) {
const x = r.int(4, HALF - 3), y = r.int(4, GH - 5);
if (!floorAt(x, y) || nearSpawn(x, y)) continue;
mirrorSet(x, y, T.PADTOP);
pads++;
}
// --- spawn pixel coords ---
const spawns = spawnT.map(([x, y]) => ({ x: x * TILE + TILE / 2, y: y * TILE + TILE / 2 }));
// --- patrolling saws, mirrored pairs, paths clear of walls/pits/spawns ---
const blocked = (t) => t === T.SOLID || t === T.CRYSTAL || t === T.PIT;
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 (blocked(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, 2);
for (let i = 0; i < nSaws; i++) {
for (let t = 0; t < 30; t++) {
const y = r.int(5, GH - 8) * TILE;
const x0 = r.int(4, HALF - 14) * TILE, x1 = x0 + r.int(8, 12) * TILE;
let pts, loop = false;
if (r.chance(0.5)) {
pts = [{ x: x0, y }, { x: x1, y }];
} else {
const y2 = Math.min((GH - 4) * TILE, y + r.int(4, 8) * TILE);
pts = [{ x: x0, y }, { x: x1, y }, { x: x1, y: y2 }, { x: x0, y: y2 }];
loop = true;
}
if (!sawPathOk(pts, loop)) continue;
const speed = 1.1 + r.f() * 1.1, prog = r.f();
saws.push({ pts, speed, prog, x: pts[0].x, y: pts[0].y, loop });
saws.push({ pts: pts.map(pt => ({ x: W - pt.x, y: pt.y })), speed, prog, x: W - pts[0].x, y: pts[0].y, loop });
break;
}
}
return { seed, tiles, hp, spawns, saws, pal: r.int(0, 3) };
}