import EventEmitter from 'eventemitter3'; import { backDir, toDir } from './utils'; import { loading } from '@user/data-base'; import type { HeroKeyMover } from '@user/client-modules'; import { sleep } from '@motajs/common'; import { state } from '..'; import { fromDirectionString } from '@user/data-common'; // todo: 转身功能 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 speed = step.value; 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); } } // todo: refactor interface CanMoveStatus { /** 由CannotIn和CannotOut计算出的信息,不可移动时不会触发触发器 */ canMove: boolean; /** 由图块的noPass计算出的信息,不可移动时会触发触发器 */ noPass: boolean; } const enum HeroMoveCode { Step, Stop, /** 不能移动,并撞击前面一格的图块,触发其触发器 */ Hit, /** 不能移动,同时当前格有CannotOut,或目标格有CannotIn,不会触发前面一格的触发器 */ CannotMove } export class HeroMover extends ObjectMoverBase { /** 当前移动是否忽略地形 */ private ignoreTerrain: boolean = false; /** 当前移动是否不计入录像 */ private noRoute: boolean = false; /** 当前移动是否是在lockControl条件下开始的 */ private inLockControl: boolean = false; /** 是否会在特殊时刻进行自动存档 */ private autoSave: boolean = false; /** 本次移动开始时的移动速度 */ private beforeMoveSpeed: number = 100; 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 (!dam || !nextDam) return; // 可以在这里判断地图伤害,并进行自动存档,例如在进入或离开地图伤害时存档 // if (dam.damage > 0 || nextDam.damage > 0) { // core.autosave() // } } protected async onMoveStart(controller: IMoveController): Promise { this.beforeMoveSpeed = this.moveSpeed; if (!core.isReplaying() || core.status.replay.speed <= 12) { state.hero.mover.startMove(); } // 这里要检查前面那一格能不能走,不能走则不触发平滑视角,以避免撞墙上视角卡住 if (!this.ignoreTerrain) { const { x, y } = core.status.hero.loc; const firstDir = controller.queue.find( v => v.type === 'dir' )?.value; if (firstDir && firstDir !== 'backward' && firstDir !== 'forward') { const data = this.checkCanMove(x, y, toDir(firstDir as Dir)); if (data.canMove && !data.noPass) { // viewport.sync('startMove'); } } } else { // viewport.sync('startMove'); } } protected async onMoveEnd(controller: IMoveController): Promise { this.moveSpeed = this.beforeMoveSpeed; this.onSetMoveSpeed(this.moveSpeed, controller); await state.hero.mover.endMove(); // viewport.sync('endMove'); core.clearContinueAutomaticRoute(); core.stopAutomaticRoute(); } protected async onStepStart( _step: MoveStepDir, controller: IMoveController ): Promise { const showDir = toDir(this.faceDir); const dir4Move = toDir(this.moveDir); 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 = dir4Move; } const dir = this.moveDir; if (!this.ignoreTerrain) { const { noPass, canMove } = this.checkCanMove(x, y, dir4Move); if (!canMove) { return HeroMoveCode.CannotMove; } // 不能移动 if (noPass) { 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.faceDir); // 前方不能移动 if (code === HeroMoveCode.CannotMove || code === HeroMoveCode.Hit) { controller.stop(); this.onCannotMove(showDir); if (code === HeroMoveCode.Hit) { core.trigger(nx, ny); } core.checkRouteFolding(); return; } // 本次移动停止 if (code === HeroMoveCode.Stop) { core.clearContinueAutomaticRoute(); core.stopAutomaticRoute(); controller.stop(); return; } // 本次移动正常完成 if (code === HeroMoveCode.Step) { 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); // 中毒处理 if (core.hasFlag('poison')) { core.status.hero.hp -= core.values.poisonDamage; if (core.status.hero.hp <= 0) { core.status.hero.hp = 0; core.events.lose(); } core.updateStatusBar(); } core.moveOneStep(); core.checkRouteFolding(); } } } protected onSetMoveSpeed( speed: number, _controller: IMoveController ): void { this.moveSpeed = speed; } /** * 移动动画 * @param x 目标横坐标 * @param y 目标纵坐标 * @param _showDir 显示方向 * @param moveDir 移动方向 */ private async moveAnimate( x: number, y: number, _showDir: Dir, moveDir: Dir2 ) { // const viewport = HeroMover.viewport; // if (!viewport) return; const replay = core.status.replay.speed; const speed = replay === 24 ? 1 : this.moveSpeed / replay; // viewport.all('moveTo', x, y, speed * 1.6); const replaying = core.isReplaying(); if (replaying) { if (core.status.replay.speed > 12) { await state.hero.mover.endMove(); await sleep(speed); state.hero.mover.setPosition(x, y); } else { state.hero.mover.startMove(); await state.hero.mover.move( fromDirectionString(moveDir), this.moveSpeed / core.status.replay.speed ); } } else { state.hero.mover.startMove(); await state.hero.mover.move( fromDirectionString(moveDir), this.moveSpeed ); } } /** * 当前方一格不能走时,执行的函数 * @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 }; } } 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('@user/client-modules'); const { gameKey } = Mota.require('@motajs/system'); const keyMover = new HeroKeyMover(gameKey, heroMover); heroMoveCollection.keyMover = keyMover; }); });