83 lines
5.2 KiB
JavaScript
83 lines
5.2 KiB
JavaScript
// 已核对的剧情落脚点。控制事件的位置不一定是角色演出的位置。
|
|
// Map181 EV002 第 5 页将玩家送到 Map182 (11,11),随后由 EV003 自动执行
|
|
// 298/299 -> 305。EV003 本身放在 (0,0),仅用于控制剧情。
|
|
// 此表只有结构坐标,不含台词;发布版可直接共用,无需本地解包数据。
|
|
function storyPosition(trigger) {
|
|
const { x, y } = trigger;
|
|
const entries = [
|
|
{ map: 182, event: 3, page: 0, stories: [298, 299, 305], from: [0, 0], to: [11, 11] },
|
|
// 原游戏 Map006 EV017 第 1 页的入场位置。
|
|
{ map: 9, event: 10, page: 5, stories: [449], from: [0, 0], to: [11, 9] },
|
|
// 原游戏 Map301 EV005 第 1 页的村落入口。
|
|
{ map: 296, event: 34, page: 5, stories: [449], from: [4, 0], to: [28, 13] },
|
|
// 原游戏 Map004 EV005 第 5 页的梦境入场位置。
|
|
{ map: 254, event: 2, page: 0, stories: [467], from: [0, 0], to: [6, 5] },
|
|
];
|
|
const entry = entries.find(e => Number(trigger.mapId) === e.map && Number(trigger.eventId) === e.event &&
|
|
trigger.pageIndex !== null && trigger.pageIndex !== undefined && Number(trigger.pageIndex) === e.page &&
|
|
e.stories.includes(Number(trigger.storyId)) &&
|
|
x !== null && y !== null && x !== '' && y !== '' && Number(x) === e.from[0] && Number(y) === e.from[1]);
|
|
if (entry) {
|
|
return { x: entry.to[0], y: entry.to[1], positionNote: `使用原游戏入场位置;剧情控制事件位于 (${entry.from.join(',')})` };
|
|
}
|
|
return { x, y };
|
|
}
|
|
// 在游戏运行时调用;只读取当前地图与碰撞,不加载/导出台词或解包文件。
|
|
function chooseStoryPosition({ map, player, data, eventId, pageIndex, storyId, sameMap, previous, preferred, requested }) {
|
|
const event = data.events?.[eventId];
|
|
const containsStory = page => page.list?.some(c => c.code === 357 && c.parameters?.[1] === 'call_story' && Number(c.parameters[3]?.id) === Number(storyId));
|
|
const page = event?.pages?.[pageIndex] || event?.pages?.find(containsStory);
|
|
const hasBody = pg => !!(pg?.image?.characterName || pg?.image?.tileId || pg?.list?.some(c => c.code === 357 && c.parameters?.[1] === 'ai' && Number(c.parameters[3]?.battleObject) > 0));
|
|
if (!page || ![3, 4].includes(page.trigger) || hasBody(page)) return { ok: true, ...requested, controller: false };
|
|
if (!preferred && (requested.x !== event.x || requested.y !== event.y)) preferred = requested;
|
|
|
|
const tw = map.tileWidth(), th = map.tileHeight(), step = Math.max(8, Math.floor(th / 4));
|
|
let probes = 0;
|
|
const floor = (x, y, depth) => {
|
|
x = Math.round(x); y = Math.round(y);
|
|
if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0 || x >= map.width() || y >= map.height()) return null;
|
|
for (let dy = 0; dy <= Math.min(depth * th, (map.height() - y) * th); dy += step) {
|
|
if (++probes > 800) return null;
|
|
const hits = (player.hitPreview((x - player._x) * tw, (y - player._y) * th + dy) || []).filter(o => o?.terrain === true);
|
|
if (!hits.length) continue;
|
|
if (hits.some(o => o.type === 2)) return null;
|
|
return { x, y: y + Math.max(0, Math.floor((dy - step) / th)) };
|
|
}
|
|
return null;
|
|
};
|
|
// 同图已站在正常场景里时,不因点击控制事件而把人重新送到角落。
|
|
let pick = sameMap && previous ? floor(previous.x, previous.y, 1) : null;
|
|
let reason = pick ? '保留当前可落地位置' : '';
|
|
if (!pick && preferred) { pick = floor(preferred.x, preferred.y, 2); if (pick) reason = '使用已核实的入场位置'; }
|
|
const events = (map.events() || []).filter(e => e && e.eventId() !== Number(eventId));
|
|
const refs = new Set((page.list || []).filter(c => [203, 205, 213].includes(c.code)).map(c => Number(c.parameters?.[0])).filter(id => id > 0));
|
|
const ordered = [
|
|
...events.filter(e => refs.has(e.eventId())),
|
|
...events.filter(e => e.characterName?.()),
|
|
...events.filter(e => e.event()?.pages?.some(hasBody)),
|
|
];
|
|
const seen = new Set();
|
|
for (const ev of ordered) {
|
|
if (pick || probes >= 600) break;
|
|
if (seen.has(ev)) continue;
|
|
seen.add(ev);
|
|
for (const dx of [-1, 1, -2, 2]) {
|
|
pick = floor(ev._x + dx, Math.max(0, ev._y - 1), 4);
|
|
if (pick) { reason = '按场景人物附近的安全地面定位'; break; }
|
|
}
|
|
}
|
|
// 场景人物不可用时,仅在地图内部做有上限的地形扫描,不猜测原作站位。
|
|
if (!pick) {
|
|
const xs = Array.from({ length: Math.min(map.width(), 31) }, (_, i) => Math.round((i + 0.5) * map.width() / Math.min(map.width(), 31)))
|
|
.sort((a, b) => Math.abs(a - map.width() / 2) - Math.abs(b - map.width() / 2));
|
|
for (const x of xs) {
|
|
if (probes >= 800) break;
|
|
pick = floor(x, 0, map.height());
|
|
if (pick) { reason = '按当前地图的安全地面定位'; break; }
|
|
}
|
|
}
|
|
if (!pick) return { ok: false, controller: true, error: '这是无形剧情控制事件,未找到可确认的安全落脚点;已保留悬浮,请手动定位后再运行过场' };
|
|
return { ok: true, ...pick, controller: true, positionNote: reason };
|
|
}
|
|
module.exports = { storyPosition, chooseStoryPosition };
|