import type { RenderAdapter } from '@motajs/render'; import type { TimingFn } from 'mutate-animate'; import { BlockMover, heroMoveCollection, MoveStep } from '@user/data-state'; import { hook, loading } from '@user/data-base'; import { Patch, PatchClass } from '@motajs/legacy-common'; import type { HeroRenderer, LayerDoorAnimate, LayerGroupAnimate, Layer, FloorViewport, LayerFloorBinder, LayerGroup } from '@user/client-modules'; // 向后兼容用,会充当两个版本间过渡的作用 interface Adapters { 'hero-adapter'?: RenderAdapter; 'door-animate'?: RenderAdapter; animate?: RenderAdapter; layer?: RenderAdapter; viewport?: RenderAdapter; } const adapters: Adapters = {}; export function initFallback() { let fallbackIds: number = 1e8; if (!main.replayChecking && main.mode === 'play') { const Adapter = Mota.require('@motajs/render').RenderAdapter; const hero = Adapter.get('hero-adapter'); const doorAnimate = Adapter.get('door-animate'); const animate = Adapter.get('animate'); const layer = Adapter.get('layer'); const viewport = Adapter.get('viewport'); adapters['hero-adapter'] = hero; adapters['door-animate'] = doorAnimate; adapters['animate'] = animate; adapters['layer'] = layer; adapters['viewport'] = viewport; } const { mover: heroMover } = heroMoveCollection; // ----- 工具函数 /** * 根据事件中给出的移动数组解析出全部的移动步骤 */ function getMoveSteps(steps: string[]) { const moveSteps: string[] = []; steps.forEach(v => { const [type, number] = v.split(':'); if (!number) moveSteps.push(type); else { if (type === 'speed') moveSteps.push(v); else { moveSteps.push(...Array(Number(number)).fill(type)); } } }); return moveSteps; } function setHeroDirection(dir: Dir) { heroMover.setFaceDir(dir); heroMover.setMoveDir(dir); } /** * 生成跳跃函数 */ function generateJumpFn(dx: number, dy: number): TimingFn<3> { const distance = Math.hypot(dx, dy); const peak = 3 + distance; return (progress: number) => { const x = dx * progress; const y = progress * dy + (progress ** 2 - progress) * peak; return [x, y, Math.ceil(y)]; }; } Mota.r(() => { // ----- 引入 const { MotaRenderer: Renderer } = Mota.require('@motajs/render'); const { Camera } = Mota.require('@user/client-modules'); const Animation = Mota.require('MutateAnimate'); const patch = new Patch(PatchClass.Control); const patch2 = new Patch(PatchClass.Events); const patch3 = new Patch(PatchClass.Maps); //#region 勇士移动相关 patch.add('moveAction', async function (callback?: () => void) { heroMover.clearMoveQueue(); heroMover.oneStep('forward'); const lock = core.status.lockControl; const controller = heroMover.startMove(false, true, lock); controller?.onEnd.then(() => { callback?.(); }); heroMover.once('stepEnd', () => { controller?.stop(); }); }); patch.add('_moveAction_moving', () => {}); patch2.add( '_action_moveAction', function (data: any, x: number, y: number, prefix: any) { if (core.canMoveHero()) { const nx = core.nextX(), ny = core.nextY(); // 检查noPass决定是撞击还是移动 if (core.noPass(nx, ny)) { core.insertAction([{ type: 'trigger', loc: [nx, ny] }]); } else { // 先移动一格,然后尝试触发事件 core.insertAction([ { type: 'function', function: 'function() { core.moveAction(core.doAction); }', async: true }, { type: '_label' } ]); } } core.doAction(); } ); patch2.add( 'eventMoveHero', async function ( steps: string[], time: number = 500, callback?: () => void ) { if (heroMover.moving) return; const moveSteps = getMoveSteps(steps); const resolved = moveSteps.map(v => { if (v.startsWith('speed')) { return { type: 'speed', value: Number(v.slice(6)) }; } else { return { type: 'dir', value: v as Move2 }; } }); const start: MoveStep = { type: 'speed', value: time }; heroMover.insertMove(...[start, ...resolved]); const controller = heroMover.startMove(true, true, true, false); if (!controller) { callback?.(); return; } controller.onEnd.then(() => { callback?.(); }); const animate = fallbackIds++; core.animateFrame.lastAsyncId = animate; core.animateFrame.asyncId[animate] = controller.stop; } ); patch.add( 'setHeroLoc', function ( name: 'x' | 'y' | 'direction', value: number | Dir, noGather?: boolean ) { if (!core.status.hero) return; // @ts-ignore core.status.hero.loc[name] = value; if ((name === 'x' || name === 'y') && !noGather) { core.control.gatherFollowers(); } if (name === 'direction') { adapters['hero-adapter']?.sync('turn', value); adapters['hero-adapter']?.sync('setAnimateDir', value); setHeroDirection(value as Dir); } else if (name === 'x') { // 为了防止逆天样板出问题 core.bigmap.posX = value as number; adapters['hero-adapter']?.sync('setHeroLoc', value); } else { // 为了防止逆天样板出问题 core.bigmap.posY = value as number; adapters['hero-adapter']?.sync('setHeroLoc', void 0, value); } } ); patch.add('waitHeroToStop', function (callback?: () => void) { core.stopAutomaticRoute(); core.clearContinueAutomaticRoute(); heroMover.controller?.stop(); if (callback) { core.status.replay.animate = true; core.lockControl(); core.status.automaticRoute.moveDirectly = false; setTimeout( function () { core.status.replay.animate = false; callback(); }, core.status.replay.speed === 24 ? 1 : 30 ); } }); patch.add( 'moveHero', async function ( direction?: Dir, callback?: () => void, noRoute: boolean = false ) { if (heroMover.moving) return; heroMover.clearMoveQueue(); heroMover.oneStep(direction ?? 'forward'); const lock = core.status.lockControl; const controller = heroMover.startMove(false, noRoute, lock); controller?.onEnd.then(() => { callback?.(); }); heroMover.once('stepEnd', () => { controller?.stop(); }); } ); patch2.add('setHeroIcon', function (name: ImageIds) { const img = core.material.images.images[name]; if (!img) return; core.status.hero.image = name; adapters['hero-adapter']?.sync('setImage', img); }); patch.add('isMoving', function () { return heroMover.moving; }); patch.add( 'setAutomaticRoute', function (destX: number, destY: number, stepPostfix: DiredLoc[]) { if (heroMover.moving) return; if (!core.status.played || core.status.lockControl) return; if (core.control._setAutomaticRoute_isMoving(destX, destY)) return; if ( core.control._setAutomaticRoute_isTurning( destX, destY, stepPostfix ) ) return; if ( core.control._setAutomaticRoute_clickMoveDirectly( destX, destY, stepPostfix ) ) return; // 找寻自动寻路路线 const moveStep = core.automaticRoute(destX, destY); if ( moveStep.length == 0 && (destX != core.status.hero.loc.x || destY != core.status.hero.loc.y || stepPostfix.length == 0) ) return; moveStep.push(...stepPostfix); core.status.automaticRoute.destX = destX; core.status.automaticRoute.destY = destY; core.control._setAutomaticRoute_drawRoute(moveStep); core.control._setAutomaticRoute_setAutoSteps(moveStep); // ??? core.setAutoHeroMove(); // 执行移动 const steps: MoveStep[] = moveStep.map(v => { return { type: 'dir', value: v.direction }; }); heroMover.clearMoveQueue(); heroMover.insertMove(...steps); heroMover.startMove(); } ); //#region 开关门 patch2.add( 'openDoor', function ( x: number, y: number, needKey: boolean, callback?: () => void ) { const block = core.getBlock(x, y); core.saveAndStopAutomaticRoute(); if (!core.events._openDoor_check(block, x, y, needKey)) { const locked = core.status.lockControl; core.waitHeroToStop(function () { if (!locked) core.unlockControl(); if (callback) callback(); }); return; } if (core.status.replay.speed === 24) { core.status.replay.animate = true; core.removeBlock(x, y); setTimeout(function () { core.status.replay.animate = false; hook.emit( 'afterOpenDoor', block.event.id as AllIdsOf<'animates'>, x, y ); if (callback) callback(); }, 1); // +1是为了录像检测系统 } else { const locked = core.status.lockControl; core.lockControl(); core.status.replay.animate = true; core.removeBlock(x, y); const cb = () => { core.maps._removeBlockFromMap( core.status.floorId, block ); if (!locked) core.unlockControl(); core.status.replay.animate = false; hook.emit( 'afterOpenDoor', block.event.id as AllIdsOf<'animates'>, x, y ); callback?.(); }; adapters['door-animate']?.all('openDoor', block).then(cb); const animate = fallbackIds++; core.animateFrame.lastAsyncId = animate; core.animateFrame.asyncId[animate] = cb; // this._openDoor_animate(block, x, y, callback); } } ); patch2.add( 'closeDoor', function (x: number, y: number, id: AllIds, callback?: () => void) { id = id || ''; if ( // @ts-ignore (core.material.icons.animates[id] == null && // @ts-ignore core.material.icons.npc48[id] == null) || core.getBlock(x, y) != null ) { if (callback) callback(); return; } const block = core.getBlockById(id); const doorInfo = (block.event || {}).doorInfo; if (!doorInfo) { if (callback) callback(); return; } core.playSound(doorInfo.closeSound); const locked = core.status.lockControl; core.lockControl(); core.status.replay.animate = true; const cb = function () { if (!locked) core.unlockControl(); core.status.replay.animate = false; core.setBlock(id, x, y); core.showBlock(x, y); callback?.(); }; if (core.status.replay.speed === 24) { cb(); } else { adapters['door-animate'] ?.all('closeDoor', block) .then(() => { cb(); }); const animate = fallbackIds++; core.animateFrame.lastAsyncId = animate; core.animateFrame.asyncId[animate] = cb; core.events._openDoor_animate(block, x, y, callback); } } ); //#region 动画 patch3.add( 'drawAnimate', function ( name: AnimationIds, x: number, y: number, alignWindow?: boolean, callback?: () => void ) { // @ts-ignore name = core.getMappedName(name); // 正在播放录像:不显示动画 if ( core.isReplaying() || !core.material.animates[name] || x == null || y == null ) { if (callback) callback(); return -1; } adapters.animate ?.all( 'drawAnimate', name, x * 32 + 16, y * 32 + 16, alignWindow ?? false ) .then(() => { callback?.(); }); } ); patch3.add( 'drawHeroAnimate', function (name: AnimationIds, callback?: () => void) { // @ts-ignore name = core.getMappedName(name); // 正在播放录像或动画不存在:不显示动画 if (core.isReplaying() || !core.material.animates[name]) { if (callback) callback(); return -1; } adapters.animate?.global('drawHeroAnimate', name).then(() => { callback?.(); }); } ); patch3.add( 'moveBlock', async function ( x: number, y: number, steps: string[], time: number = 500, keep: boolean = false, callback?: () => void ) { if (!steps || steps.length === 0) { callback?.(); return; } const block = core.getBlock(x, y); if (!block) { callback?.(); return; } const mover = new BlockMover( x, y, core.status.floorId, 'event' ); const moveSteps = getMoveSteps(steps); const resolved = moveSteps.map(v => { if (v.startsWith('speed')) { return { type: 'speed', value: Number(v.slice(6)) }; } else { return { type: 'dir', value: v as Move2 }; } }); const start: MoveStep = { type: 'speed', value: time }; mover.insertMove(...[start, ...resolved]); const controller = mover.startMove(); if (controller) { await controller.onEnd; } if (!keep) { core.removeBlock(mover.x, mover.y); } callback?.(); } ); patch3.add( 'jumpBlock', async function ( sx: number, sy: number, ex: number, ey: number, time: number = 500, keep: boolean = false, callback?: () => void ) { const block = core.getBlock(sx, sy); if (!block) { callback?.(); return; } time /= core.status.replay.speed; if (core.status.replay.speed === 24) time = 1; const dx = ex - sx; const dy = ey - sy; const fn = generateJumpFn(dx, dy); const list = adapters.layer?.items ?? []; const items = [...list].filter(v => { if (v.layer !== 'event') return false; const ex = v.getExtends('floor-binder') as LayerFloorBinder; if (!ex) return false; return ex.getFloor() === core.status.floorId; }); const width = core.status.thisMap.width; const index = sx + sy * width; const promise = Promise.all( items.map(v => { return v.moveAs(index, ex, ey, fn, time, keep); }) ); core.updateStatusBar(); core.removeBlock(sx, sy); await promise; if (keep) { core.setBlock(block.id, ex, ey); } core.updateStatusBar(); callback?.(); } ); patch2.add( 'jumpHero', async function ( ex: number, ey: number, time: number = 500, callback?: () => void ) { if (heroMover.moving) return; const sx = core.getHeroLoc('x'); const sy = core.getHeroLoc('y'); adapters.viewport?.all('mutateTo', ex, ey, time); const locked = core.status.lockControl; core.lockControl(); const list = adapters['hero-adapter']?.items ?? []; const items = [...list]; time /= core.status.replay.speed; if (core.status.replay.speed === 24) time = 1; const fn = generateJumpFn(ex - sx, ey - sy); await Promise.all( items.map(v => { if (!v.renderable) return Promise.reject(); return v.layer.moveRenderable( v.renderable, sx, sy, fn, time ); }) ); if (!locked) core.unlockControl(); core.setHeroLoc('x', ex); core.setHeroLoc('y', ey); callback?.(); } ); //#region 视角处理 patch.add( 'moveDirectly', function (destX: number, destY: number, ignoreSteps: number) { const data = core.control.controldata; const success = data.moveDirectly(destX, destY, ignoreSteps); if (success) adapters.viewport?.all('mutateTo', destX, destY); return success; } ); patch.add( 'moveViewport', function ( x: number, y: number, _moveMode: EaseMode, time: number = 1, callback?: () => void ) { const main = Renderer.get('render-main'); const layer = main?.getElementById('layer-main') as LayerGroup; if (!layer) return; const camera = Camera.for(layer); camera.clearOperation(); const translate = camera.addTranslate(); const animateTime = time / Math.max(core.status.replay.speed, 1); const animate = new Animation.Animation(); animate .absolute() .time(1) .mode(Animation.linear()) .move(core.bigmap.offsetX, core.bigmap.offsetY); animate.time(animateTime).move(x * 32, y * 32); camera.applyTranslateAnimation( translate, animate, animateTime + 50 ); camera.transform = layer.camera; const end = () => { core.bigmap.offsetX = x * 32; core.bigmap.offsetY = y * 32; camera.destroy(); callback?.(); }; const timeout = window.setTimeout(end, animateTime + 50); const id = fallbackIds++; core.animateFrame.lastAsyncId = id; core.animateFrame.asyncId[id] = () => { end(); clearTimeout(timeout); }; } ); }); loading.once('loaded', () => { for (const animate of Object.values(core.material.animates)) { animate.se ??= {}; if (typeof animate.se === 'string') { animate.se = { 1: animate.se }; } animate.pitch ??= {}; } }); loading.once('coreInit', () => { const moveAction = new Set(['up', 'down', 'left', 'right']); // 复写录像的移动 core.registerReplayAction('move', action => { if (moveAction.has(action)) { if (!heroMover.moving) { heroMover.startMove(); } if (!heroMover.controller) { return false; } heroMover.controller.push({ type: 'dir', value: action as Dir }); heroMover.controller.onEnd.then(() => { core.replay(); }); return true; } else { return false; } }); }); }