diff --git a/src/core/index.ts b/src/core/index.ts index 075f320..f8137df 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -72,6 +72,7 @@ import { RenderAdapter } from './render/adapter'; import { getMainRenderer } from './render'; import { Layer } from './render/preset/layer'; import { LayerGroupFloorBinder } from './render/preset/floor'; +import { HeroKeyMover } from './main/action/move'; // ----- 类注册 Mota.register('class', 'AudioPlayer', AudioPlayer); @@ -162,6 +163,9 @@ Mota.register('module', 'Render', { Layer, LayerGroupFloorBinder }); +Mota.register('module', 'Action', { + HeroKeyMover +}); main.renderLoaded = true; Mota.require('var', 'hook').emit('renderLoaded'); diff --git a/src/core/main/action/move.ts b/src/core/main/action/move.ts new file mode 100644 index 0000000..8fac7cc --- /dev/null +++ b/src/core/main/action/move.ts @@ -0,0 +1,159 @@ +import { KeyCode } from '@/plugin/keyCodes'; +import { Hotkey, HotkeyData } from '../custom/hotkey'; +import type { HeroMover, IMoveController } from '@/game/state/move'; +import { Ticker } from 'mutate-animate'; +import { mainScope } from '../init/hotkey'; + +type MoveKey = Record; +type MoveKeyConfig = Record; + +export class HeroKeyMover { + /** 当前按下的键 */ + private pressedKey: Set = new Set(); + /** 当前的移动方向 */ + private moveDir: Dir = 'down'; + /** 当前是否正在使用按键移动 */ + private moving: boolean = false; + /** 当前移动的控制器 */ + private controller?: IMoveController; + + /** 按键接续ticker */ + private ticker = new Ticker(); + + /** 当前移动实例绑定的热键 */ + hotkey: Hotkey; + /** 当前热键的移动按键信息 */ + hotkeyData: MoveKey; + /** 移动实例 */ + mover: HeroMover; + /** 移动可触发的作用域 */ + scope: symbol = mainScope; + + constructor(hotkey: Hotkey, mover: HeroMover, config?: MoveKeyConfig) { + this.hotkey = hotkey; + this.mover = mover; + hotkey.on('press', this.onPressKey); + hotkey.on('release', this.onReleaseKey); + + const data = hotkey.data; + + this.hotkeyData = { + left: data[config?.left ?? 'moveLeft'], + right: data[config?.right ?? 'moveRight'], + up: data[config?.up ?? 'moveUp'], + down: data[config?.down ?? 'moveDown'] + }; + + this.ticker.add(() => { + if (!this.moving) { + if (this.pressedKey.size > 0) { + const dir = [...this.pressedKey].at(-1); + if (!dir) return; + this.moveDir = dir; + this.tryStartMove(); + } + } + }); + } + + private onPressKey = (code: KeyCode) => { + if (code === this.hotkeyData.left.key) this.press('left'); + else if (code === this.hotkeyData.right.key) this.press('right'); + else if (code === this.hotkeyData.up.key) this.press('up'); + else if (code === this.hotkeyData.down.key) this.press('down'); + }; + + private onReleaseKey = (code: KeyCode) => { + if (code === this.hotkeyData.left.key) this.release('left'); + else if (code === this.hotkeyData.right.key) this.release('right'); + else if (code === this.hotkeyData.up.key) this.release('up'); + else if (code === this.hotkeyData.down.key) this.release('down'); + }; + + /** + * 设置按键触发作用域 + */ + setScope(scope: symbol) { + this.scope = scope; + } + + /** + * 按下某个方向键 + * @param dir 移动方向 + */ + press(dir: Dir) { + if (this.hotkey.scope !== this.scope || core.status.lockControl) return; + this.pressedKey.add(dir); + this.moveDir = dir; + if (!this.moving) { + this.tryStartMove(); + } + } + + /** + * 松开方向键 + * @param dir 移动方向 + */ + release(dir: Dir) { + this.pressedKey.delete(dir); + if (this.pressedKey.size > 0) { + this.moveDir = [...this.pressedKey][0]; + } else { + this.endMove(); + } + } + + /** + * 尝试开始移动 + * @returns 是否成功开始移动 + */ + tryStartMove() { + if (this.moving || core.status.lockControl) return false; + + this.mover.oneStep(this.moveDir); + const controller = this.mover.startMove(); + if (!controller) return; + + this.controller = controller; + controller.onEnd.then(() => { + this.moving = false; + this.controller = void 0; + this.mover.off('stepEnd', this.onStepEnd); + }); + this.moving = true; + + this.mover.on('stepEnd', this.onStepEnd); + return true; + } + + /** + * 停止本次按键移动 + */ + endMove() { + this.controller?.stop(); + } + + private onStepEnd = () => { + const con = this.controller; + if (!con) return; + if (!this.moving) { + con.stop(); + return; + } + + if (this.pressedKey.size > 0) { + if (con.queue.length === 0) { + con.push({ type: 'dir', value: this.moveDir }); + } + } else { + con.stop(); + } + }; + + destroy() { + this.hotkey.off('press', this.onPressKey); + this.hotkey.off('release', this.onReleaseKey); + this.mover.off('stepEnd', this.onStepEnd); + this.ticker.destroy(); + } +} diff --git a/src/core/main/custom/danmaku.ts b/src/core/main/custom/danmaku.ts index a53dd12..45dd45d 100644 --- a/src/core/main/custom/danmaku.ts +++ b/src/core/main/custom/danmaku.ts @@ -251,7 +251,7 @@ export class Danmaku extends EventEmitter { } else { logger.error( 8, - `Post not allowed css danmaku. Allow info: ${allow.join(',')}` + `Post danmaku with not allowed css. Info: ${allow.join(',')}` ); } } diff --git a/src/core/main/custom/hotkey.ts b/src/core/main/custom/hotkey.ts index 899fe03..9df772c 100644 --- a/src/core/main/custom/hotkey.ts +++ b/src/core/main/custom/hotkey.ts @@ -34,7 +34,7 @@ interface RegisterHotkeyData extends Partial { defaults: KeyCode; } -interface HotkeyData extends Required { +export interface HotkeyData extends Required { key: KeyCode; emits: Map; group?: string; diff --git a/src/core/render/preset/hero.ts b/src/core/render/preset/hero.ts index 29432e7..197ddea 100644 --- a/src/core/render/preset/hero.ts +++ b/src/core/render/preset/hero.ts @@ -5,10 +5,10 @@ import { logger } from '@/core/common/logger'; import EventEmitter from 'eventemitter3'; import { texture } from '../cache'; import { TimingFn } from 'mutate-animate'; -import { backDir } from '@/plugin/game/utils'; import { isNil } from 'lodash-es'; -type HeroMovingStatus = 'stop' | 'moving' | 'moving-as'; +// type HeroMovingStatus = 'stop' | 'moving' | 'moving-as'; +// export const enum HeroMovingStatus {} interface HeroRenderEvent { stepEnd: []; @@ -31,25 +31,27 @@ export class HeroRenderer renderable?: LayerMovingRenderable; layer!: Layer; - // hero?: Hero; - - /** 勇士移动状态 */ - status: HeroMovingStatus = 'stop'; /** 当前移动帧数 */ movingFrame: number = 0; + /** 是否正在移动 */ + moving: boolean = false; + /** 是否正在播放动画,与移动分开以实现原地踏步 */ + animate: boolean = false; /** 勇士移动速度 */ speed: number = 100; - /** 当前勇士朝向 */ - // todo: 删了这个属性 - dir: Dir = 'down'; + + /** 当前的移动方向 */ + moveDir: Dir2 = 'down'; + /** 当前移动的勇士显示方向 */ + showDir: Dir = 'down'; + /** 帧动画是否反向播放,例如后退时就应该设为true */ + animateReverse: boolean = false; /** 勇士移动定时器id */ private moveId: number = -1; /** 上一次帧数切换的时间 */ private lastFrameTime: number = 0; - /** 当前的移动方向 */ - moveDir: Move2 = 'down'; /** 上一步走到格子上的时间 */ private lastStepTime: number = 0; /** 执行当前步移动的Promise */ @@ -63,8 +65,6 @@ export class HeroRenderer stepDir: Dir2 = 'down'; /** 每步的格子增量 */ private stepDelta: Loc = { x: 0, y: 1 }; - /** 动画显示的方向,用于适配后退 */ - // private animateDir: Dir = 'down'; /** * 设置勇士所用的图片资源 @@ -76,6 +76,10 @@ export class HeroRenderer this.layer.update(this.layer); } + /** + * 设置移动的移动速度 + * @param speed 移动速度 + */ setMoveSpeed(speed: number) { this.speed = speed; } @@ -102,7 +106,7 @@ export class HeroRenderer zIndex: core.status.hero.loc.y, autotile: false, bigImage: true, - render: this.getRenderFromDir(this.moveDir), + render: this.getRenderFromDir(this.showDir), animate: 0 }; } @@ -111,53 +115,90 @@ export class HeroRenderer * 根据方向获取勇士的裁切信息 * @param dir 方向 */ - getRenderFromDir(dir: Move2): [number, number, number, number][] { + getRenderFromDir(dir: Dir): [number, number, number, number][] { if (!this.cellWidth || !this.cellHeight) return []; - let resolved: Dir2; - if (dir === 'forward') resolved = this.dir; - else if (dir === 'backward') resolved = backDir(this.dir); - else resolved = dir; - const index = texture.characterDirection[resolved]; + const index = texture.characterDirection[dir]; const y = index * this.cellHeight; const res: [number, number, number, number][] = [0, 1, 2, 3].map(v => { return [v * this.cellWidth!, y, this.cellWidth!, this.cellHeight!]; }); - if (dir === 'backward') return res.reverse(); + if (this.animateReverse) return res.reverse(); else return res; } /** - * 设置当前勇士 - * @param hero 绑定的勇士 + * 开始勇士帧动画 */ - // setHero(hero: Hero) { - // this.hero = hero; - // } + startAnimate() { + this.animate = true; + this.lastFrameTime = Date.now(); + } /** - * 准备开始移动 + * 结束勇士帧动画 */ - readyMove() { - if (this.status !== 'stop') return; - this.status = 'moving'; - this.lastFrameTime = Date.now(); + endAnimate() { + this.animate = false; + this.resetRenderable(false); + } + + /** + * 设置帧动画是否反向播放 + * @param reverse 帧动画是否反向播放 + */ + setAnimateReversed(reverse: boolean) { + this.animateReverse = reverse; + this.resetRenderable(true); + } + + /** + * 设置勇士的动画显示朝向 + * @param dir 显示朝向 + */ + setAnimateDir(dir: Dir) { + if (dir !== this.showDir) { + this.showDir = dir; + this.resetRenderable(true); + } + } + + /** + * 重置renderable状态,恢复至第一帧状态 + * @param getInfo 是否重新获取渲染信息 + */ + resetRenderable(getInfo: boolean) { + this.movingFrame = 0; + console.trace(); + + if (this.renderable) { + this.renderable.animate = 0; + if (getInfo) { + this.renderable.render = this.getRenderFromDir(this.showDir); + } + } + this.layer.update(this.layer); + } + + /** + * 勇士帧动画定时器 + */ + private animateTick(time: number) { + if (!this.animate) return; + if (time - this.lastFrameTime > this.speed) { + this.lastFrameTime = time; + this.movingFrame++; + this.movingFrame %= 4; + if (this.renderable) this.renderable.animate = this.movingFrame; + } + this.layer.update(this.layer); } /** * 勇士移动定时器 */ private moveTick(time: number) { - if (this.status !== 'moving') return; - if (!this.renderable) { - this.emit('stepEnd'); - return; - } - - if (time - this.lastFrameTime > this.speed) { - this.lastFrameTime = time; - this.movingFrame++; - this.movingFrame %= 4; - } + if (!this.moving) return; + if (!this.renderable) return; const progress = (time - this.lastStepTime) / this.speed; @@ -174,7 +215,6 @@ export class HeroRenderer this.renderable.y = ry; } this.emit('moveTick', this.renderable.x, this.renderable.y); - this.renderable.animate = this.movingFrame; this.layer.update(this.layer); } @@ -182,18 +222,23 @@ export class HeroRenderer * 进行下一步的移动准备,设置移动信息 */ private step() { - if (this.moveDir === 'backward') this.stepDir = backDir(this.stepDir); - else if (this.moveDir !== 'forward') this.stepDir = this.moveDir; this.lastStepTime = Date.now(); this.stepDelta = core.utils.scan2[this.stepDir]; this.turn(this.stepDir); } + /** + * 准备开始移动 + */ + readyMove() { + this.moving = true; + } + /** * 移动勇士 */ - move(dir: Move2): Promise { - if (this.status !== 'moving') { + move(dir: Dir2): Promise { + if (!this.moving) { logger.error( 12, `Cannot move while status is not 'moving'. Call 'readyMove' first.` @@ -202,16 +247,16 @@ export class HeroRenderer } this.moveDir = dir; - if (this.moveDetached) return this.moveDetached; else { this.step(); - return (this.moveDetached = new Promise(resolve => { + this.moveDetached = new Promise(res => { this.once('stepEnd', () => { this.moveDetached = void 0; - resolve(); + res(); }); - })); + }); + return this.moveDetached; } } @@ -219,14 +264,13 @@ export class HeroRenderer * 结束勇士的移动过程 */ endMove(): Promise { - if (this.status !== 'moving') return Promise.reject(); + if (!this.moving) return Promise.reject(); if (this.moveEnding) return this.moveEnding; else { const promise = new Promise(resolve => { this.once('stepEnd', () => { this.moveEnding = void 0; - this.status = 'stop'; - this.movingFrame = 0; + this.moving = false; const { x, y } = core.status.hero.loc; this.setHeroLoc(x, y); this.render(); @@ -259,7 +303,7 @@ export class HeroRenderer this.stepDir = dir; if (!this.renderable) return; - this.renderable.render = this.getRenderFromDir(this.moveDir); + this.renderable.render = this.getRenderFromDir(this.showDir); this.layer.update(this.layer); } @@ -291,9 +335,8 @@ export class HeroRenderer * 因为此举会导致层级的重新排序,降低渲染性能。 */ moveAs(x: number, y: number, time: number, fn: TimingFn<3>): Promise { - if (this.status !== 'stop') return Promise.reject(); + if (!this.moving) return Promise.reject(); if (!this.renderable) return Promise.reject(); - this.status = 'moving-as'; let nowZIndex = fn(0)[2]; let startTime = Date.now(); return new Promise(res => { @@ -307,18 +350,15 @@ export class HeroRenderer this.renderable.y = ny; this.renderable.zIndex = nz; if (nz !== nowZIndex) { - this.layer.movingRenderable.sort( - (a, b) => a.zIndex - b.zIndex - ); + this.layer.sortMovingRenderable(); } this.emit('moveTick', this.renderable.x, this.renderable.y); this.layer.update(this.layer); }, time, () => { - this.status = 'stop'; + this.moving = false; if (!this.renderable) return res(); - this.renderable.animate = 0; this.renderable.x = x; this.renderable.y = y; this.emit('moveTick', this.renderable.x, this.renderable.y); @@ -334,7 +374,7 @@ export class HeroRenderer */ render() { if (!this.renderable) return; - if (this.status === 'stop') { + if (!this.animate) { this.renderable.animate = -1; } else { this.renderable.animate = this.movingFrame; @@ -346,7 +386,9 @@ export class HeroRenderer this.layer = layer; adapter.add(this); this.moveId = layer.delegateTicker(() => { - this.moveTick(Date.now()); + const time = Date.now(); + this.animateTick(time); + this.moveTick(time); }); } @@ -380,10 +422,6 @@ adapter.receive( return item.moveAs(x, y, time, fn); } ); -adapter.receive('setMoveSpeed', (item, speed: number) => { - item.setMoveSpeed(speed); - return Promise.resolve(); -}); adapter.receive('setHeroLoc', (item, x?: number, y?: number) => { item.setHeroLoc(x, y); return Promise.resolve(); @@ -392,11 +430,28 @@ adapter.receive('turn', (item, dir: Dir2) => { item.turn(dir); return Promise.resolve(); }); -adapter.receive('setImage', (item, image: SizedCanvasImageSource) => { + +// 同步适配函数,这些函数用于同步设置信息等 +adapter.receiveSync('setImage', (item, image: SizedCanvasImageSource) => { item.setImage(image); - return Promise.resolve(); }); -// 同步fallback,用于适配现在的样板,之后会删除 +adapter.receiveSync('setMoveSpeed', (item, speed: number) => { + item.setMoveSpeed(speed); +}); +adapter.receiveSync('setAnimateReversed', (item, reverse: boolean) => { + item.setAnimateReversed(reverse); +}); +adapter.receiveSync('startAnimate', item => { + item.startAnimate(); +}); +adapter.receiveSync('endAnimate', item => { + item.endAnimate(); +}); +adapter.receiveSync('setAnimateDir', (item, dir: Dir) => { + item.setAnimateDir(dir); +}); + +// 不分同步fallback,用于适配现在的样板,之后会删除 adapter.receiveSync('setHeroLoc', (item, x?: number, y?: number) => { item.setHeroLoc(x, y); }); diff --git a/src/core/render/preset/layer.ts b/src/core/render/preset/layer.ts index 2cab014..f5395f0 100644 --- a/src/core/render/preset/layer.ts +++ b/src/core/render/preset/layer.ts @@ -1123,6 +1123,13 @@ export class Layer extends Container { for (const ex of this.extend.values()) { ex.onMovingUpdate?.(this, this.movingRenderable); } + this.sortMovingRenderable(); + } + + /** + * 对移动层按照z坐标排序 + */ + sortMovingRenderable() { this.movingRenderable.sort((a, b) => a.zIndex - b.zIndex); } @@ -1277,7 +1284,7 @@ export class Layer extends Container { * 渲染移动/大怪物层 */ protected renderMoving(transform: Transform) { - const frame = (RenderItem.animatedFrame % 4) + 1; + const frame = RenderItem.animatedFrame; const cell = this.cellSize; const halfCell = cell / 2; const { ctx } = this.movingMap; diff --git a/src/game/index.ts b/src/game/index.ts index 2715dd2..20b18ad 100644 --- a/src/game/index.ts +++ b/src/game/index.ts @@ -11,6 +11,7 @@ import * as miscMechanism from './mechanism/misc'; import * as study from './mechanism/study'; import { registerPresetState } from './state/preset'; import { ItemState } from './state/item'; +import { HeroMover, ObjectMoverBase } from './state/move'; // ----- 类注册 Mota.register('class', 'DamageEnemy', damage.DamageEnemy); @@ -34,7 +35,9 @@ Mota.register('module', 'Mechanism', { Study: study }); Mota.register('module', 'State', { - ItemState + ItemState, + HeroMover, + ObjectMoverBase }); main.loading = loading; diff --git a/src/game/state/move.ts b/src/game/state/move.ts new file mode 100644 index 0000000..777de45 --- /dev/null +++ b/src/game/state/move.ts @@ -0,0 +1,397 @@ +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 { KeyCode } from '@/plugin/keyCodes'; + +interface MoveStepDir { + type: 'dir'; + value: Move2; +} + +interface MoveStepSpeed { + type: 'speed'; + value: number; +} + +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; + + /** + * 当本次移动开始时执行的函数 + * @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; + + private getMoveDir(move: Move2): Dir2 { + if (move === 'forward') return this.moveDir; + if (move === 'backward') return backDir(this.moveDir); + return move; + } + + /** + * 开始移动,这个操作后,本次移动将不能通过{@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()); + this.emit('moveStart', queue); + await this.onMoveStart(controller); + 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); + const code = await this.onStepStart(step, controller); + await this.onStepEnd(step, code, controller); + } else { + this.moveSpeed = step.value; + } + this.emit('stepEnd', step); + } + this.moving = false; + this.emit('moveEnd'); + + await this.onMoveEnd(controller); + }; + + const onEnd = start(); + const controller: IMoveController = { + done: false, + onEnd, + queue, + push(...step) { + queue.push(...step); + }, + stop() { + stopMove = true; + return onEnd; + } + }; + + 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); + } +} + +interface CanMoveStatus { + /** 由CannotIn和CannotOut计算出的信息,不可移动时不会触发触发器 */ + canMove: boolean; + /** 由图块的noPass计算出的信息,不可移动时会触发触发器 */ + noPass: boolean; +} + +const enum HeroMoveCode { + Step, + Stop, + /** 不能移动,并撞击前面一格的图块,触发其触发器 */ + Hit, + /** 不能移动,同时当前格有CannotOut,或目标格有CannotIn,不会触发前面一格的触发器 */ + CannotMove +} + +export class HeroMover extends ObjectMoverBase { + /** 勇士渲染适配器,用于等待动画等操作 */ + static adapter?: RenderAdapter; + /** 视角适配器 */ + static viewport?: RenderAdapter; + + /** 当前移动是否忽略地形 */ + private ignoreTerrain: boolean = false; + /** 当前移动是否不计入录像 */ + private noRoute: boolean = false; + /** 当前移动是否是在lockControl条件下开始的 */ + private inLockControl: boolean = false; + + override startMove( + ignoreTerrain: boolean = false, + noRoute: boolean = false, + inLockControl: boolean = false + ): IMoveController | null { + this.ignoreTerrain = ignoreTerrain; + this.noRoute = noRoute; + this.inLockControl = inLockControl; + return super.startMove(); + } + + protected async onMoveStart(controller: IMoveController): Promise { + const adapter = HeroMover.adapter; + if (!adapter) return; + await adapter.all('readyMove'); + adapter.sync('startAnimate'); + } + + protected async onMoveEnd(controller: IMoveController): Promise { + const adapter = HeroMover.adapter; + if (!adapter) return; + await adapter.all('endMove'); + adapter.sync('endAnimate'); + } + + protected async onStepStart( + step: MoveStepDir, + controller: IMoveController + ): Promise { + const showDir = toDir(this.moveDir); + core.setHeroLoc('direction', showDir); + const adapter = HeroMover.adapter; + adapter?.sync('setAnimateDir', showDir); + + const { x, y } = core.status.hero.loc; + const { x: nx, y: ny } = this.nextLoc(x, y, this.moveDir); + + if (!this.inLockControl && core.status.lockControl) { + controller.stop(); + return HeroMoveCode.Stop; + } + + if (!this.ignoreTerrain || !this.noRoute) { + this.moveDir = showDir; + } + + 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) { + controller.stop(); + return; + } + + // 本次移动正常完成 + if (code === HeroMoveCode.Step) { + core.setHeroLoc('x', nx, true); + core.setHeroLoc('y', ny, true); + + const direction = core.getHeroLoc('direction'); + core.control._moveAction_popAutomaticRoute(); + if (!this.noRoute) core.status.route.push(direction); + + core.moveOneStep(); + core.checkRouteFolding(); + } + } + + /** + * 移动动画 + * @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); + 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 }; + } +} + +// 初始化基础勇士移动实例 +export const heroMover = new HeroMover(); +loading.once('coreInit', () => { + // 注册按键操作 + Mota.r(() => { + const { HeroKeyMover } = Mota.require('module', 'Action'); + const gameKey = Mota.require('var', 'gameKey'); + const keyMover = new HeroKeyMover(gameKey, heroMover); + }); +}); + +// 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'); + HeroMover.adapter = adapter; + HeroMover.viewport = viewport; +}); diff --git a/src/game/state/utils.ts b/src/game/state/utils.ts new file mode 100644 index 0000000..d658847 --- /dev/null +++ b/src/game/state/utils.ts @@ -0,0 +1,35 @@ +const dirMap: Record = { + down: 'down', + left: 'left', + leftdown: 'left', + leftup: 'left', + right: 'right', + rightdown: 'right', + rightup: 'right', + up: 'up' +}; + +/** + * 将八方向转换为四方向,一般用于斜角移动时的方向显示 + * @param dir 八方向 + */ +export function toDir(dir: Dir2): Dir { + return dirMap[dir]; +} + +const backDirMap: Record = { + up: 'down', + down: 'up', + left: 'right', + right: 'left', + leftup: 'rightdown', + rightup: 'leftdown', + leftdown: 'rightup', + rightdown: 'leftup' +}; + +export function backDir(dir: Dir): Dir; +export function backDir(dir: Dir2): Dir2; +export function backDir(dir: Dir2): Dir2 { + return backDirMap[dir]; +} diff --git a/src/game/system.ts b/src/game/system.ts index a5592a4..22ab4ba 100644 --- a/src/game/system.ts +++ b/src/game/system.ts @@ -37,6 +37,8 @@ import type { RenderAdapter } from '@/core/render/adapter'; import type { ItemState } from './state/item'; import type { Layer } from '@/core/render/preset/layer'; import type { LayerGroupFloorBinder } from '@/core/render/preset/floor'; +import type { HeroKeyMover } from '@/core/main/action/move'; +import type { HeroMover, ObjectMoverBase } from './state/move'; interface ClassInterface { // 渲染进程与游戏进程通用 @@ -121,6 +123,11 @@ interface ModuleInterface { }; State: { ItemState: typeof ItemState; + HeroMover: typeof HeroMover; + ObjectMoverBase: typeof ObjectMoverBase; + }; + Action: { + HeroKeyMover: typeof HeroKeyMover; }; } diff --git a/src/plugin/game/fallback.ts b/src/plugin/game/fallback.ts index def3dbc..6a053c5 100644 --- a/src/plugin/game/fallback.ts +++ b/src/plugin/game/fallback.ts @@ -50,7 +50,7 @@ export function init() { } let moving: boolean = false; - let stopChian: boolean = false; + let stopChain: boolean = false; let moveDir: Dir; let stepDir: Dir; let moveEnding: Promise = Promise.resolve([]); @@ -65,24 +65,24 @@ export function init() { let portal: boolean = false; const pressedArrow: Set = new Set(); - Mota.r(() => { - const gameKey = Mota.require('var', 'gameKey'); - const { moveUp, moveDown, moveLeft, moveRight } = gameKey.data; - const symbol = Symbol.for('@key_main'); - gameKey.on('press', code => { - if (core.status.lockControl || gameKey.scope !== symbol) return; - if (code === moveUp.key) onMoveKeyDown('up'); - if (code === moveDown.key) onMoveKeyDown('down'); - if (code === moveLeft.key) onMoveKeyDown('left'); - if (code === moveRight.key) onMoveKeyDown('right'); - }); - gameKey.on('release', code => { - if (code === moveUp.key) onMoveKeyUp('up'); - if (code === moveDown.key) onMoveKeyUp('down'); - if (code === moveLeft.key) onMoveKeyUp('left'); - if (code === moveRight.key) onMoveKeyUp('right'); - }); - }); + // Mota.r(() => { + // const gameKey = Mota.require('var', 'gameKey'); + // const { moveUp, moveDown, moveLeft, moveRight } = gameKey.data; + // const symbol = Symbol.for('@key_main'); + // gameKey.on('press', code => { + // if (core.status.lockControl || gameKey.scope !== symbol) return; + // if (code === moveUp.key) onMoveKeyDown('up'); + // if (code === moveDown.key) onMoveKeyDown('down'); + // if (code === moveLeft.key) onMoveKeyDown('left'); + // if (code === moveRight.key) onMoveKeyDown('right'); + // }); + // gameKey.on('release', code => { + // if (code === moveUp.key) onMoveKeyUp('up'); + // if (code === moveDown.key) onMoveKeyUp('down'); + // if (code === moveLeft.key) onMoveKeyUp('left'); + // if (code === moveRight.key) onMoveKeyUp('right'); + // }); + // }); function onMoveKeyDown(type: Dir) { pressedArrow.add(type); @@ -91,15 +91,15 @@ export function init() { stepDir = moveDir; readyMove(); } - if (moving && stopChian) { - stopChian = false; + if (moving && stopChain) { + stopChain = false; } } function onMoveKeyUp(type: Dir) { pressedArrow.delete(type); if (pressedArrow.size === 0) { - stopChian = true; + stopChain = true; } else { const arr = [...pressedArrow]; moveDir = arr[0]; @@ -127,7 +127,7 @@ export function init() { } await adapter.all('readyMove'); moving = true; - stopChian = false; + stopChain = false; startHeroMoveChain(ignoreTerrain, noRoute, inLockControl, callback); } @@ -143,7 +143,7 @@ export function init() { return Promise.resolve(); } else { if (moving) { - stopChian = true; + stopChain = true; moveEnding = adapter.all('endMove'); await moveEnding; moveEmit.emit('moveEnd'); @@ -161,7 +161,9 @@ export function init() { function continueAfterEnd() { requestAnimationFrame(() => { if (pressedArrow.size === 0 || moving) { - stopChian = true; + // console.log(6); + + stopChain = true; return; } if (checkCanMoveStatus()) { @@ -185,8 +187,8 @@ export function init() { if (!adapter) { return Promise.resolve(); } else { - while (moving || !stopChian) { - if (stopChian || (!inLockControl && core.status.lockControl)) { + while (moving || !stopChain) { + if (stopChain || (!inLockControl && core.status.lockControl)) { break; } @@ -215,7 +217,7 @@ export function init() { } endMove(); - stopChian = false; + stopChain = false; } } @@ -297,6 +299,30 @@ export function init() { callback?.(); } + async function moveHeroAs(step: DiredLoc[]) { + if (moving) return; + // console.log(step); + + let now = step.shift(); + if (!now) return; + + stepDir = now.direction; + await readyMove(); + while (now) { + if (!moving) break; + stepDir = now.direction; + + await new Promise(res => { + moveEmit.once('stepEnd', () => { + now = step.shift(); + if (!now) endMove(); + console.log(now?.direction); + res(); + }); + }); + } + } + // ----- 移动 - 传送门 function checkPortal() { const map = BluePalace.portalMap.get(core.status.floorId); @@ -437,8 +463,8 @@ export function init() { } }); - const step0 = steps.find(v => !v.startsWith('speed')) as Move2; - return { steps, start: step0 }; + const step0 = moveSteps.find(v => !v.startsWith('speed')) as Move2; + return { steps: moveSteps, start: step0 }; } /** @@ -495,7 +521,7 @@ export function init() { control.prototype.moveAction = async function (callback?: () => void) { // await endMove(false); moveEmit.once('stepEnd', () => { - stopChian = true; + stopChain = true; endMove(false); }); readyMove(false, true, true, callback); @@ -570,14 +596,14 @@ export function init() { readyMove(true, true, true); while (++pointer < len) { const dir = moveSteps[pointer]; - if (dir === 'backward') moveDir = backDir(moveDir); + if (dir === 'backward') stepDir = backDir(stepDir); else if (dir.startsWith('speed')) { const speed = parseInt(dir.slice(6)); list.forEach(v => v.setMoveSpeed(speed)); } else if (dir !== 'forward') { - moveDir = dir as Dir; + stepDir = dir as Dir; } - const { x, y } = core.utils.scan[moveDir]; + const { x, y } = core.utils.scan[stepDir]; nx += x; ny += y; await new Promise(res => { @@ -617,7 +643,9 @@ export function init() { control.prototype.waitHeroToStop = function (callback?: () => void) { core.stopAutomaticRoute(); core.clearContinueAutomaticRoute(); - moveEnding.then(() => { + + stopChain = true; + stepEnding.then(() => { callback?.(); }); }; @@ -639,7 +667,8 @@ export function init() { moveDir = direction; stepDir = direction; await readyMove(); - stopChian = true; + + stopChain = true; callback?.(); }; @@ -647,13 +676,52 @@ export function init() { events.prototype.setHeroIcon = function (name: ImageIds) { const img = core.material.images.images[name]; if (!img) return; - adapters['hero-adapter']?.all('setImage', img); + adapters['hero-adapter']?.sync('setImage', img); }; control.prototype.isMoving = function () { return moving; }; + ////// 设置自动寻路路线 ////// + control.prototype.setAutomaticRoute = function ( + destX: number, + destY: number, + stepPostfix: DiredLoc[] + ) { + if (!core.status.played || core.status.lockControl) return; + if (this._setAutomaticRoute_isMoving(destX, destY)) return; + if (this._setAutomaticRoute_isTurning(destX, destY, stepPostfix)) + return; + if ( + this._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; + this._setAutomaticRoute_drawRoute(moveStep); + this._setAutomaticRoute_setAutoSteps(moveStep); + // 立刻移动 + // core.setAutoHeroMove(); + console.log(moveStep); + + moveHeroAs(moveStep.slice()); + }; + hook.on('reset', () => { moveDir = core.status.hero.loc.direction; stepDir = moveDir; @@ -868,10 +936,11 @@ export function init() { for (const step of moveSteps) { if (step === 'backward') stepDir = backDir(stepDir); - if (step.startsWith('speed')) { + else if (step.startsWith('speed')) { time = parseInt(step.slice(6)); continue; - } + } else stepDir = step as Dir2; + const { x, y } = core.utils.scan2[stepDir]; const tx = nx + x; const ty = ny + y; @@ -983,8 +1052,10 @@ export function init() { destY: number, ignoreSteps: number ) { - adapters.viewport?.all('mutateTo', destX, destY); - return this.controldata.moveDirectly(destX, destY, ignoreSteps); + const data = this.controldata; + const success = data.moveDirectly(destX, destY, ignoreSteps); + if (success) adapters.viewport?.all('mutateTo', destX, destY); + return success; }; }); diff --git a/src/plugin/utils.ts b/src/plugin/utils.ts index 62c4160..0f8ed0d 100644 --- a/src/plugin/utils.ts +++ b/src/plugin/utils.ts @@ -2,7 +2,7 @@ import { message } from 'ant-design-vue'; import { MessageApi } from 'ant-design-vue/lib/message'; import { isNil } from 'lodash-es'; import { Animation, sleep, TimingFn } from 'mutate-animate'; -import { ref } from 'vue'; +import { Ref, ref } from 'vue'; import { EVENT_KEY_CODE_MAP, KeyCode } from './keyCodes'; import axios from 'axios'; import { decompressFromBase64 } from 'lz-string'; diff --git a/src/types/status.d.ts b/src/types/status.d.ts index 4f9cb66..8c18611 100644 --- a/src/types/status.d.ts +++ b/src/types/status.d.ts @@ -641,7 +641,7 @@ interface InitGameStatus { /** * 自动寻路状态 */ - automaticRoute: DeepReadonly; + automaticRoute: AutomaticRouteStatus; /** * 按键按下的时间,用于判定双击