import EventEmitter from 'eventemitter3'; import { backDir, toDir } from './utils'; import { loading } from '../game'; import type { RenderAdapter } from '@/core/render/adapter'; import type { HeroRenderer } from '@/core/render/preset/hero'; import type { FloorViewport } from '@/core/render/preset/viewport'; import type { HeroKeyMover } from '@/core/main/action/move'; import type { FloorLayer, Layer, LayerMovingRenderable } from '@/core/render/preset/layer'; import type { LayerFloorBinder } from '@/core/render/preset/floor'; import { BluePalace } from '../mechanism/misc'; interface MoveStepDir { type: 'dir'; value: Move2; } interface MoveStepSpeed { type: 'speed'; value: number; } export type MoveStep = MoveStepDir | MoveStepSpeed; export interface IMoveController { /** 本次移动是否完成 */ done: boolean; /** 当前移动队列 */ queue: MoveStep[]; /** 当本次移动完成时兑现 */ onEnd: Promise; /** * 停止本次移动,停止后将不再能继续移动 */ stop(): Promise; /** * 向队列末尾添加移动信息 * @param step 移动信息 */ push(...step: MoveStep[]): void; } interface EObjectMovingEvent { stepEnd: [step: MoveStep]; moveEnd: []; moveStart: [queue: MoveStep[]]; } export abstract class ObjectMoverBase extends EventEmitter { /** 移动队列 */ private moveQueue: MoveStep[] = []; /** 当前的移动速度 */ moveSpeed: number = 100; /** 当前的移动方向 */ moveDir: Dir2 = 'down'; /** 当前是否正在移动 */ moving: boolean = false; /** 面朝方向 */ faceDir: Dir2 = 'down'; /** 当前的控制对象 */ controller?: IMoveController; /** * 当本次移动开始时执行的函数 * @param controller 本次移动的控制对象 */ protected abstract onMoveStart(controller: IMoveController): Promise; /** * 当本次移动结束时执行的函数 * @param controller 本次移动的控制对象 */ protected abstract onMoveEnd(controller: IMoveController): Promise; /** * 当这一步开始移动时执行的函数,可以用来播放移动动画等 * @param step 这一步的移动信息 * @param controller 本次移动的控制对象 * @returns 一个Promise,其值表示本次移动的代码,并将其值传入这一步移动后的操作,即{@link onStepEnd} */ protected abstract onStepStart( step: MoveStepDir, controller: IMoveController ): Promise; /** * 当这一步移动结束后执行的函数(即{@link onStepStart}成功兑现后),可以用于设置物体坐标等 * @param step 这一步的移动信息 * @param code 本步移动的代码,即onMoveStart的返回值 * @param controller 本次移动的控制对象 * @returns 一个Promise,当其兑现时,本步移动全部完成,将会开始下一步的移动 */ protected abstract onStepEnd( step: MoveStepDir, code: number, controller: IMoveController ): Promise; /** * 当移动速度被设置时执行的函数 * @param speed 设置为的移动速度 * @param controller 本次移动的控制对象 */ protected abstract onSetMoveSpeed( speed: number, controller: IMoveController ): void; private getMoveDir(move: Move2): Dir2 { if (move === 'forward') return this.moveDir; if (move === 'backward') return backDir(this.faceDir); return move; } private getFaceDir(move: Move2): Dir2 { if (move === 'forward' || move === 'backward') return this.faceDir; return move; } /** * 设置当前面朝方向,移动中设置无效 * @param dir 面朝方向 */ setFaceDir(dir: Dir2) { if (!this.moving) this.faceDir = dir; } /** * 设置当前移动方向,移动中设置无效 * @param dir 移动方向 */ setMoveDir(dir: Dir2) { if (!this.moving) this.moveDir = dir; } /** * 开始移动,这个操作后,本次移动将不能通过{@link insertMove}添加移动步骤, * 只能通过{@link IMoveController.push}添加,而通过前者添加的移动步骤会在下次移动生效 */ startMove(): IMoveController | null { if (this.moving) return null; const queue = this.moveQueue.slice(); this.clearMoveQueue(); this.moving = true; let stopMove = false; const start = async () => { // 等待宏任务执行完成,不然controller会在首次调用中未定义 await new Promise(res => res()); await this.onMoveStart(controller); this.emit('moveStart', queue); while (queue.length > 0) { if (stopMove || !this.moving) break; const step = queue.shift(); if (!step) break; if (step.type === 'dir') { this.moveDir = this.getMoveDir(step.value); this.faceDir = this.getFaceDir(step.value); const code = await this.onStepStart(step, controller); await this.onStepEnd(step, code, controller); } else { const replay = core.status.replay.speed; const speed = replay === 24 ? 1 : step.value / replay; this.moveSpeed = speed; this.onSetMoveSpeed(speed, controller); } this.emit('stepEnd', step); } this.moving = false; this.emit('moveEnd'); await this.onMoveEnd(controller); }; const onEnd = start().then(() => { this.controller = void 0; controller.done = true; }); const controller: IMoveController = { done: false, onEnd, queue, push(...step) { queue.push(...step); }, stop() { stopMove = true; return onEnd; } }; this.controller = controller; return controller; } /** * 向移动队列末尾插入移动实例 * @param move 移动实例 */ insertMove(...move: MoveStep[]) { this.moveQueue.push(...move); } /** * 清空移动队列 */ clearMoveQueue() { this.moveQueue = []; } /** * 向移动队列末尾插入一个移动操作 * @param step 移动方向 */ oneStep(step: Move2) { this.moveQueue.push({ type: 'dir', value: step }); } /** * 按照传入的数组移动物体,插入至移动队列末尾 * @param steps 移动步骤 */ moveAs(steps: MoveStep[]) { this.moveQueue.push(...steps); } } const enum BlockMoveCode { Step } export class BlockMover extends ObjectMoverBase { /** 楼层渲染适配器,用于显示动画 */ static adapter?: RenderAdapter; x: number; y: number; floorId: FloorIds; layer: FloorLayer; /** 本次移动中需要进行动画移动的楼层渲染组件 */ private layerItems: Layer[] = []; /** 本次移动过程中的移动renderable实例 */ private renderable?: LayerMovingRenderable; /** 本次移动的图块id */ private blockNum: number = 0; constructor( x: number, y: number, floorId: FloorIds, layer: FloorLayer, dir: Dir = 'down' ) { super(); this.x = x; this.y = y; this.floorId = floorId; this.moveDir = dir; this.layer = layer; } /** * 绑定移动点 * @param x 绑定点横坐标 * @param y 绑定点纵坐标 * @param floorId 绑定点楼层 * @returns 是否绑定成功,例如如果当前绑定点正在移动,那么就会绑定失败 */ bind( x: number, y: number, floorId: FloorIds, layer: FloorLayer, dir: Dir = 'down' ) { if (this.moving) return false; this.x = x; this.y = y; this.floorId = floorId; this.moveDir = dir; this.layer = layer; return true; } protected async onMoveStart(controller: IMoveController): Promise { const adapter = BlockMover.adapter; if (adapter) { const list = adapter.items; const items = [...list].filter(v => { if (v.layer !== this.layer) return false; const ex = v.getExtends('floor-binder') as LayerFloorBinder; if (!ex) return false; return ex.getFloor() === core.status.floorId; }); this.layerItems = items; } let blockNum: number = 0; if (this.layer === 'event') { blockNum = core.status.maps[this.floorId].map[this.y][this.x]; } else { const array = core.maps._getBgFgMapArray(this.layer, this.floorId); blockNum = array[this.y][this.x]; } this.blockNum = blockNum; Mota.r(() => { const { Layer } = Mota.require('module', 'Render'); const r = Layer.getMovingRenderable(blockNum, this.x, this.y); if (r) { this.renderable = r; this.layerItems.forEach(v => { v.moving.add(r); }); } }); if (this.layer === 'event') { core.removeBlock(this.x, this.y, this.floorId); } } protected async onMoveEnd(controller: IMoveController): Promise { if (this.renderable) { this.layerItems.forEach(v => { v.moving.delete(this.renderable!); }); } this.layerItems = []; this.renderable = void 0; if (this.layer === 'event') { core.setBlock(this.blockNum as AllNumbers, this.x, this.y); } } protected async onStepStart( step: MoveStepDir, controller: IMoveController ): Promise { await this.moveAnimate(step); const { x: dx, y: dy } = core.utils.scan2[this.moveDir]; this.x += dx; this.y += dy; return BlockMoveCode.Step; } protected async onStepEnd( step: MoveStepDir, code: BlockMoveCode, controller: IMoveController ): Promise {} protected onSetMoveSpeed( speed: number, controller: IMoveController ): void {} private moveAnimate(step: MoveStepDir) { const layer = this.layerItems[0]; if (!layer) return; if (!this.renderable) return; const data = this.renderable; const fx = this.x; const fy = this.y; const { x: dx, y: dy } = core.utils.scan2[this.moveDir]; const start = Date.now(); const time = this.moveSpeed; return new Promise(res => { layer.delegateTicker( () => { const now = Date.now() - start; const progress = now / time; data.x = fx + dx * progress; data.y = fy + dy * progress; this.layerItems.forEach(v => { v.update(v); }); }, this.moveSpeed, () => { data.x = fx + dx; data.y = fy + dy; data.zIndex = fy + dy; res(); } ); }); } } interface CanMoveStatus { /** 由CannotIn和CannotOut计算出的信息,不可移动时不会触发触发器 */ canMove: boolean; /** 由图块的noPass计算出的信息,不可移动时会触发触发器 */ noPass: boolean; } interface PortalStatus { /** 下一步是否会步入传送门 */ portal: boolean; /** 传送门会传到哪 */ data?: BluePalace.PortalTo; } const enum HeroMoveCode { Step, Stop, /** 不能移动,并撞击前面一格的图块,触发其触发器 */ Hit, /** 不能移动,同时当前格有CannotOut,或目标格有CannotIn,不会触发前面一格的触发器 */ CannotMove, /** 进入传送门 */ Portal } export class HeroMover extends ObjectMoverBase { /** 勇士渲染适配器,用于等待动画等操作 */ static adapter?: RenderAdapter; /** 视角适配器 */ static viewport?: RenderAdapter; /** 当前移动是否忽略地形 */ private ignoreTerrain: boolean = false; /** 当前移动是否不计入录像 */ private noRoute: boolean = false; /** 当前移动是否是在lockControl条件下开始的 */ private inLockControl: boolean = false; /** 是否会在特殊时刻进行自动存档 */ private autoSave: boolean = false; /** 本次移动开始时的移动速度 */ private beforeMoveSpeed: number = 100; /** 这一步的传送门信息 */ private portalData?: BluePalace.PortalTo; override startMove( ignoreTerrain: boolean = false, noRoute: boolean = false, inLockControl: boolean = false, autoSave: boolean = false ): IMoveController | null { if (this.moving) return null; this.ignoreTerrain = ignoreTerrain; this.noRoute = noRoute; this.inLockControl = inLockControl; this.autoSave = autoSave; return super.startMove(); } private checkAutoSave(x: number, y: number, nx: number, ny: number) { const index = `${x},${y}`; const nIndex = `${nx},${ny}`; const map = core.status.thisMap.enemy.mapDamage; const dam = map[index]; const nextDam = map[nIndex]; if (nextDam?.mockery || (!dam?.hunt && nextDam?.hunt)) { core.autosave(); return true; } } protected async onMoveStart(controller: IMoveController): Promise { this.beforeMoveSpeed = this.moveSpeed; const adapter = HeroMover.adapter; if (!adapter) return; await adapter.all('readyMove'); adapter.sync('startAnimate'); } protected async onMoveEnd(controller: IMoveController): Promise { this.moveSpeed = this.beforeMoveSpeed; this.onSetMoveSpeed(this.moveSpeed, controller); const adapter = HeroMover.adapter; if (!adapter) return; await adapter.all('endMove'); adapter.sync('endAnimate'); core.clearContinueAutomaticRoute(); core.stopAutomaticRoute(); } protected async onStepStart( step: MoveStepDir, controller: IMoveController ): Promise { const showDir = toDir(this.faceDir); core.setHeroLoc('direction', showDir); const { x, y } = core.status.hero.loc; const { x: nx, y: ny } = this.nextLoc(x, y, this.moveDir); if (this.autoSave && !this.ignoreTerrain) { this.checkAutoSave(x, y, nx, ny); } if (!this.inLockControl && core.status.lockControl) { controller.stop(); return HeroMoveCode.Stop; } if (!this.ignoreTerrain || !this.noRoute) { this.moveDir = showDir; } // 检查传送门 if (!this.ignoreTerrain) { const { portal, data } = this.checkPortal(x, y, showDir); if (portal && data) { this.portalData = data; await this.renderHeroSwap(data); return HeroMoveCode.Portal; } } const dir = this.moveDir; if (!this.ignoreTerrain) { const { noPass, canMove } = this.checkCanMove(x, y, showDir); // 不能移动 if (noPass || !canMove) { if (!canMove) return HeroMoveCode.CannotMove; else return HeroMoveCode.Hit; } } // 可以移动,显示移动动画 await this.moveAnimate(nx, ny, showDir, dir); return HeroMoveCode.Step; } protected async onStepEnd( step: MoveStepDir, code: HeroMoveCode, controller: IMoveController ): Promise { const { x, y } = core.status.hero.loc; const { x: nx, y: ny } = this.nextLoc(x, y, this.moveDir); const showDir = toDir(this.moveDir); // 前方不能移动 if (code === HeroMoveCode.CannotMove || code === HeroMoveCode.Hit) { controller.stop(); this.onCannotMove(showDir); if (code === HeroMoveCode.Hit) { core.trigger(nx, ny); } return; } // 本次移动停止 if (code === HeroMoveCode.Stop) { core.clearContinueAutomaticRoute(); core.stopAutomaticRoute(); controller.stop(); return; } // 本次移动正常完成 if (code === HeroMoveCode.Step || code === HeroMoveCode.Portal) { if (code === HeroMoveCode.Portal) { const data = this.portalData; if (!data) return; core.setHeroLoc('x', data.x); core.setHeroLoc('y', data.y); core.setHeroLoc('direction', data.dir); } else { core.setHeroLoc('x', nx, true); core.setHeroLoc('y', ny, true); } if (!this.ignoreTerrain) { const direction = core.getHeroLoc('direction'); core.control._moveAction_popAutomaticRoute(); if (!this.noRoute) core.status.route.push(direction); core.moveOneStep(); core.checkRouteFolding(); } } } protected onSetMoveSpeed(speed: number, controller: IMoveController): void { const adapter = HeroMover.adapter; if (!adapter) return; adapter.sync('setMoveSpeed', speed); } /** * 移动动画 * @param x 目标横坐标 * @param y 目标纵坐标 * @param showDir 显示方向 * @param moveDir 移动方向 */ private async moveAnimate( x: number, y: number, showDir: Dir, moveDir: Dir2 ) { const adapter = HeroMover.adapter; const viewport = HeroMover.viewport; if (!adapter || !viewport) return; viewport.all('moveTo', x, y); adapter.sync('setAnimateDir', showDir); await adapter.all('move', moveDir); } /** * 当前方一格不能走时,执行的函数 * @param dir 移动方向 */ private onCannotMove(dir: Dir) { if (!this.noRoute) core.status.route.push(dir); core.status.automaticRoute.moveStepBeforeStop = []; core.status.automaticRoute.lastDirection = dir; if (core.status.automaticRoute.moveStepBeforeStop.length == 0) { core.clearContinueAutomaticRoute(); core.stopAutomaticRoute(); } } /** * 检查前方是否可以移动 * @param x 当前横坐标 * @param y 当前纵坐标 * @param dir 移动方向 */ private checkCanMove(x: number, y: number, dir: Dir): CanMoveStatus { const { x: nx, y: ny } = this.nextLoc(x, y, dir); const noPass = core.noPass(nx, ny); const canMove = core.canMoveHero(x, y, dir); return { noPass, canMove }; } /** * 获取前方一格的坐标 * @param x 当前横坐标 * @param y 当前纵坐标 * @param dir 移动方向 */ private nextLoc(x: number, y: number, dir: Dir2): Loc { const { x: dx, y: dy } = core.utils.scan2[dir]; const nx = x + dx; const ny = y + dy; return { x: nx, y: ny }; } /** * 检查前方一格是否会步入传送门 * @param x 横坐标 * @param y 纵坐标 * @param dir 移动方向 */ private checkPortal(x: number, y: number, dir: Dir): PortalStatus { const map = BluePalace.portalMap.get(core.status.floorId); if (!map) { return { portal: false }; } const width = core.status.thisMap.width; const index = x + y * width; const data = map?.get(index); if (!data) { return { portal: false }; } const to = data[dir]; if (to) { return { portal: true, data: to }; } return { portal: false }; } private renderHeroSwap(data: BluePalace.PortalTo) { const adapter = HeroMover.adapter; if (!adapter) return; const list = adapter.items; const { x: tx, y: ty, dir: toDir } = data; const { x, y, direction } = core.status.hero.loc; const { x: dx, y: dy } = core.utils.scan[direction]; const { x: tdx } = core.utils.scan[toDir]; const promises = [...list].map(v => { if (!v.renderable) return; const renderable = { ...v.renderable }; renderable.render = v.getRenderFromDir(toDir); renderable.zIndex = ty; const heroDir = v.moveDir; const width = v.renderable.render[0][2]; const height = v.renderable.render[0][3]; const cell = v.layer.cellSize; const restHeight = height - cell; if (!width || !height) return; const originFrom = structuredClone(v.renderable.render); const originTo = structuredClone(renderable.render); v.layer.moving.add(renderable); v.layer.requestUpdateMoving(); const start = Date.now(); return new Promise(res => { const tick = () => { const now = Date.now(); const progress = (now - start) / this.moveSpeed; const clipWidth = cell * progress; const clipHeight = cell * progress; const beforeWidth = width - clipWidth; const beforeHeight = height - clipHeight; v.renderable!.x = x; v.renderable!.y = y; if (heroDir === 'left' || heroDir === 'right') { v.renderable!.x = x + (clipWidth / 2 / cell) * dx; v.renderable!.render.forEach((v, i) => { v[2] = beforeWidth; if (heroDir === 'left') { v[0] = originFrom[i][0] + clipWidth; } }); } else { v.renderable!.render.forEach((v, i) => { v[3] = beforeHeight; if (heroDir === 'up') { v[1] = originFrom[i][1] + clipHeight + restHeight; } }); } renderable.x = tx; renderable.y = ty; if (toDir === 'left' || toDir === 'right') { renderable.x = tx + (clipWidth / 2 / cell - 0.5) * tdx; renderable.render.forEach((v, i) => { v[2] = clipWidth; if (toDir === 'right') { v[0] = originTo[i][0] + beforeWidth; } }); } else { if (toDir === 'down') renderable.y = ty - 1 + progress; renderable.render.forEach((v, i) => { v[3] = clipHeight + restHeight; if (toDir === 'down') { v[1] = originTo[i][1] + clipHeight + restHeight; v[3] = clipHeight; } }); } }; v.layer.delegateTicker(tick, this.moveSpeed, () => { v.renderable!.render = originFrom; v.setAnimateDir(data.dir); v.layer.moving.delete(renderable); v.layer.requestUpdateMoving(); res(); }); }); }); return Promise.all(promises); } } interface HeroMoveCollection { mover: HeroMover; keyMover?: HeroKeyMover; } // 初始化基础勇士移动实例 const heroMover = new HeroMover(); export const heroMoveCollection: HeroMoveCollection = { mover: heroMover }; loading.once('coreInit', () => { // 注册按键操作 Mota.r(() => { const { HeroKeyMover } = Mota.require('module', 'Action'); const gameKey = Mota.require('var', 'gameKey'); const keyMover = new HeroKeyMover(gameKey, heroMover); heroMoveCollection.keyMover = keyMover; }); }); // Adapter初始化 loading.once('coreInit', () => { if (main.replayChecking || main.mode === 'editor') return; const Adapter = Mota.require('module', 'Render').RenderAdapter; const adapter = Adapter.get('hero-adapter'); const viewport = Adapter.get('viewport'); const layerAdapter = Adapter.get('layer'); HeroMover.adapter = adapter; HeroMover.viewport = viewport; BlockMover.adapter = layerAdapter; });