Files
kidgo/tools/lib-reach.mjs
T

170 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 共享可达性 BFS 核心:solve.mjs 与 reach-diag.mjs 共用
// 用真实物理常量模拟走/落/跳(含二段跳时机、空中转向、松键短跳)
export const TILE = 32, PW = 11, PH = 21;
export const GRAV = 0.25, GRAV_FALL = 0.32, GRAV_DJ = 0.35, MAXFALL = 9, JUMP = 6.6, DJUMP = 6.7, HSPD = 2.6, DJ_INERTIA = 0;
export function setupGlobals() {
const handlers = {};
const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); };
globalThis.addEventListener = addL;
globalThis.window = { addEventListener: addL };
return handlers;
}
export function makeSim(L) {
// 水:近似为可站立
for (let y = 0; y < L.h; y++)
for (let x = 0; x < L.w; x++)
if (L.tiles[y][x] === 6) L.tiles[y][x] = 1;
// 移动平台:整条路径当桥(乐观近似)
const br = [];
for (const m of L.movers) {
const xs = m.path.map(p => p.x).concat([m.x]);
const ys = m.path.map(p => p.y).concat([m.y]);
const x0 = Math.min(...xs), x1 = Math.max(...xs) + m.w;
const y0 = Math.min(...ys), y1 = Math.max(...ys) + m.h;
br.push({ x: x0, y: y0, w: x1 - x0, h: y1 - y0 });
}
function solidAt(x, y, w, h, down, prevBottom) {
const x0 = Math.floor(x / TILE), x1 = Math.floor((x + w - 0.001) / TILE);
const y0 = Math.floor(y / TILE), y1 = Math.floor((y + h - 0.001) / TILE);
for (let ty = y0; ty <= y1; ty++) for (let tx = x0; tx <= x1; tx++) {
const t = L.tileAt(tx, ty);
if (t === 7) { if (down && prevBottom !== undefined && prevBottom <= ty * TILE + 6) return true; continue; }
if (L.isSolidTile(t, tx, ty)) return true;
}
for (const b of br) if (x < b.x + b.w && x + w > b.x && y < b.y + b.h && y + h > b.y) return true;
return false;
}
function canStand(fx, fy) {
if (fx < 0 || fx + PW > L.pxW) return false;
if (solidAt(fx, fy - PH, PW, PH, false)) return false;
if (!solidAt(fx, fy - PH + 1, PW, PH, true, fy - 1)) return false;
if (L.deadlyRect(fx, fy - PH, PW, PH)) return false;
return true;
}
const overlap = (a, b) => a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
function jump(fx, fy, vx0, djump, flip, rel, targets) {
let x = fx, y = fy - PH, vy = -JUMP, vx = vx0, t = 0, used = false, djUp = false;
while (t++ < 240) {
// 与 player.js 一致:上升中按二段 = 直接 -DJUMP;下落中按只抵消 DJ_INERTIA 比例的下落速度
if (djump && !used && t === djump) { vy = Math.max(vy, 0) * DJ_INERTIA - DJUMP; used = true; djUp = vy < 0; }
if (flip && t === flip) vx = -vx;
if (rel && t === rel && vy < -3) vy = -3;
vy = Math.min(vy + (vy < 0 ? (djUp ? GRAV_DJ : GRAV) : GRAV_FALL), MAXFALL);
if (djUp && vy >= 0) djUp = false;
// 真实行为(player.js):本帧被墙挡住只是没推动,按住方向键下一帧 vx 仍为 ±3。
// 绝不能把 vx 永久清零,否则"贴着墙上升、越过台面边缘后再横移"这条经典路线会丢失。
if (vx !== 0) {
const nx = x + vx;
if (!solidAt(nx, y, PW, PH, false)) x = nx;
}
const prevBottom = y + PH;
let rem = Math.abs(vy), dy = Math.sign(vy), landed = false;
while (rem > 0.0001) {
const m = Math.min(1, rem), ny = y + dy * m;
if (!solidAt(x, ny, PW, PH, dy > 0, prevBottom)) { y = ny; rem -= m; }
else { if (dy > 0) landed = true; vy = 0; break; }
}
if (L.deadlyRect(x, y, PW, PH)) return null;
if (targets) {
const box = { x, y, w: PW, h: PH };
for (const tg of targets) if (!tg.hit && overlap(box, tg.rect)) tg.hit = true;
}
if (y > L.pxH + 120 || y < -240) return null;
if (landed) return { fx: x, fy: y + PH };
}
return null;
}
function fall(fx, fy, dir, targets) {
let x = fx + dir * 3, y = fy - PH, vy = 0, t = 0;
if (solidAt(x, y, PW, PH, false)) return null;
while (t++ < 400) {
vy = Math.min(vy + (vy < 0 ? GRAV : GRAV_FALL), MAXFALL);
const prevBottom = y + PH;
let rem = Math.abs(vy), landed = false;
while (rem > 0.0001) {
const m = Math.min(1, rem), ny = y + m;
if (!solidAt(x, ny, PW, PH, true, prevBottom)) { y = ny; rem -= m; }
else { landed = true; break; }
}
if (L.deadlyRect(x, y, PW, PH)) return null;
if (targets) {
const box = { x, y, w: PW, h: PH };
for (const tg of targets) if (!tg.hit && overlap(box, tg.rect)) tg.hit = true;
}
if (y > L.pxH + 120) return null;
if (landed) return { fx: x, fy: y + PH };
}
return null;
}
return { solidAt, canStand, jump, fall, overlap };
}
/** BFS 可达性:targets=[{rect,type,...}],onState 回调每个新状态 */
export const DEF_DJ = [0, 6, 12, 18, 24, 30];
// 2026-10-23:加 rel=5/6。vgap 夹缝(缝高 32px、盒高 21px)要求短跳 apex 精确落在 32~43px,
// 且上升早期要躲开地刺左坡:rel=4 apex 35.7 但 t=7 蹭坡 2.6px,rel=6 apex 44.7 但 t=11 擦垂刺尖 0.6px,
// 只有 rel=5(apex 38.9)同时满足(j2 x16 夹缝实证);j7 顶层 x52 夹缝则要 rel=6
export const DEF_REL = [0, 4, 5, 6, 8, 14];
export function reach(L, { DJSET = DEF_DJ, RELSET = DEF_REL, targets = [], onState = null, maxStates = 40000 } = {}) {
const sim = makeSim(L);
let sx = L.spawn.x, sy = L.spawn.y + PH, start = null;
for (let k = 0; k < 400; k++) {
if (sim.canStand(sx, sy + k)) { start = { fx: sx, fy: sy + k }; break; }
if (sy + k > L.pxH) break;
}
if (!start) return { err: '出生点下方找不到落脚点', states: 0, targets };
// 3px 状态桶:6px 桶会把"贴右腰 1~3px"的起拍/落点窗量化吞掉(j2 连续三段因此误判不可达)
// fy 规范化:jump/fall 返回的落点常比地面高零点几 px(如 1535.5),与精确落点 1536 同桶——
// 先占桶者赢,导致走链被错位几 px 的跳链顶掉,芝麻距离的跳跃窗口翻转(加 rel=6 后 j2 状态数反而暴跌的根因)。
// 真实游戏里重力下一帧即把玩家按到底(地面顶 = 32 的倍数),故按 canStand 门控 snapping
const key = s => `${Math.round(s.fx / 3)},${Math.round(s.fy / 3)}`;
const canon = (fx, fy) => {
const r = Math.round(fy / TILE) * TILE;
return (r !== fy && sim.canStand(fx, r)) ? r : fy;
};
start.fy = canon(start.fx, start.fy);
const seen = new Set([key(start)]);
const q = [start];
let n = 0;
while (q.length && n < maxStates) {
const s = q.shift(); n++;
const box = { x: s.fx, y: s.fy - PH, w: PW, h: PH };
for (const tg of targets) if (!tg.hit && sim.overlap(box, tg.rect)) tg.hit = true;
const push = r => {
if (!r || !sim.canStand(r.fx, r.fy)) return;
r.fy = canon(r.fx, r.fy);
const k = key(r);
if (seen.has(k)) return;
seen.add(k); q.push(r);
if (onState) onState(r);
};
for (const d of [-HSPD, HSPD]) push(sim.canStand(s.fx + d, s.fy) ? { fx: s.fx + d, fy: s.fy } : null);
for (const d of [-1, 1]) push(sim.fall(s.fx, s.fy, d, targets));
for (const vx of [-HSPD, 0, HSPD]) for (const dj of DJSET) for (const rel of RELSET) {
if (dj && rel) continue;
push(sim.jump(s.fx, s.fy, vx, dj, 0, rel, targets));
if (vx !== 0) push(sim.jump(s.fx, s.fy, vx, dj, 22, rel, targets));
}
}
return { states: seen.size, targets };
}
export function makeTargets(L) {
const targets = [];
for (const e of L.entities) {
if (e.type === 'goal' || e.type === 'save' || e.type === 'apple')
targets.push({ type: e.type, idx: e.idx, no: e.no, rect: { x: e.x, y: e.y, w: e.w, h: e.h }, hit: false });
}
return targets;
}