5 dogs with distinct movement physics, seeded procedural arenas with traps and mineable powerups, smash-style knockback/stocks combat, local 2P and P2P netplay (PeerJS room keys), procedural art and sfx, headless sim smoke tests.
94 lines
3.3 KiB
JavaScript
94 lines
3.3 KiB
JavaScript
// Tile-grid AABB physics. Entities are {x, y, w, h, vx, vy} with x,y = center.
|
|
import { TILE, GW, GH, T } from './consts.js';
|
|
|
|
export const idx = (x, y) => y * GW + x;
|
|
|
|
export function tileAt(map, tx, ty) {
|
|
if (tx < 0 || tx >= GW || ty < 0 || ty >= GH) return T.EMPTY;
|
|
return map.tiles[ty * GW + tx];
|
|
}
|
|
|
|
export const isSolid = (t) => t === T.SOLID || t === T.CRYSTAL || t === T.PAD;
|
|
|
|
export function solidAtPx(map, px, py) {
|
|
return isSolid(tileAt(map, Math.floor(px / TILE), Math.floor(py / TILE)));
|
|
}
|
|
|
|
export function boxFree(map, cx, cy, w, h) {
|
|
const x0 = Math.floor((cx - w / 2) / TILE), x1 = Math.floor((cx + w / 2 - 0.01) / TILE);
|
|
const y0 = Math.floor((cy - h / 2) / TILE), y1 = Math.floor((cy + h / 2 - 0.01) / TILE);
|
|
for (let y = y0; y <= y1; y++)
|
|
for (let x = x0; x <= x1; x++)
|
|
if (isSolid(tileAt(map, x, y))) return false;
|
|
return true;
|
|
}
|
|
|
|
// Move with per-axis collision resolution. opt.drop: fall through one-way platforms.
|
|
// Velocities must stay below TILE per tick (we cap MAXFALL/dashes accordingly).
|
|
export function moveEntity(e, map, opt = {}) {
|
|
const res = { ground: false, wall: 0, ceil: false };
|
|
const hw = e.w / 2, hh = e.h / 2;
|
|
|
|
// X axis
|
|
e.x += e.vx;
|
|
{
|
|
const y0 = Math.floor((e.y - hh) / TILE), y1 = Math.floor((e.y + hh - 0.01) / TILE);
|
|
if (e.vx > 0) {
|
|
const tx = Math.floor((e.x + hw - 0.01) / TILE);
|
|
for (let ty = y0; ty <= y1; ty++) {
|
|
if (isSolid(tileAt(map, tx, ty))) { e.x = tx * TILE - hw; e.vx = 0; res.wall = 1; break; }
|
|
}
|
|
} else if (e.vx < 0) {
|
|
const tx = Math.floor((e.x - hw) / TILE);
|
|
for (let ty = y0; ty <= y1; ty++) {
|
|
if (isSolid(tileAt(map, tx, ty))) { e.x = (tx + 1) * TILE + hw; e.vx = 0; res.wall = -1; break; }
|
|
}
|
|
}
|
|
}
|
|
|
|
// Y axis
|
|
const oldBottom = e.y + hh;
|
|
e.y += e.vy;
|
|
{
|
|
const x0 = Math.floor((e.x - hw) / TILE), x1 = Math.floor((e.x + hw - 0.01) / TILE);
|
|
if (e.vy > 0) {
|
|
const ty = Math.floor((e.y + hh - 0.01) / TILE);
|
|
for (let tx = x0; tx <= x1; tx++) {
|
|
const t = tileAt(map, tx, ty);
|
|
const top = ty * TILE;
|
|
if (isSolid(t) || (t === T.PLAT && !opt.drop && oldBottom <= top + 0.01)) {
|
|
e.y = top - hh; e.vy = 0; res.ground = true; break;
|
|
}
|
|
}
|
|
} else if (e.vy < 0) {
|
|
const ty = Math.floor((e.y - hh) / TILE);
|
|
for (let tx = x0; tx <= x1; tx++) {
|
|
if (isSolid(tileAt(map, tx, ty))) { e.y = (ty + 1) * TILE + hh; e.vy = 0; res.ceil = true; break; }
|
|
}
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// True if the tiles directly under the entity's feet are one-way platform (and nothing solid).
|
|
export function standingOnPlat(map, p) {
|
|
const ty = Math.floor((p.y + p.h / 2 + 1) / TILE);
|
|
const x0 = Math.floor((p.x - p.w / 2) / TILE), x1 = Math.floor((p.x + p.w / 2 - 0.01) / TILE);
|
|
let plat = false;
|
|
for (let tx = x0; tx <= x1; tx++) {
|
|
const t = tileAt(map, tx, ty);
|
|
if (isSolid(t)) return false;
|
|
if (t === T.PLAT) plat = true;
|
|
}
|
|
return plat;
|
|
}
|
|
|
|
// Coarse raycast against solid tiles. Returns {x, y, d} of the hit point or null.
|
|
export function raycast(map, x, y, dx, dy, maxDist) {
|
|
const step = 4, n = Math.ceil(maxDist / step);
|
|
for (let i = 1; i <= n; i++) {
|
|
const px = x + dx * step * i, py = y + dy * step * i;
|
|
if (solidAtPx(map, px, py)) return { x: px, y: py, d: step * i };
|
|
}
|
|
return null;
|
|
}
|