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