barkour/js/physics.js

95 lines
3.3 KiB
JavaScript
Raw Permalink Normal View History

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