170 lines
7.5 KiB
JavaScript
170 lines
7.5 KiB
JavaScript
// 共享可达性 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;
|
||
}
|