// 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) }; }