diff --git a/src/core/index.ts b/src/core/index.ts index 54cf632c..e63d081 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -75,6 +75,7 @@ import { HeroKeyMover } from './main/action/move'; import { Camera } from './render/camera'; import * as Animation from 'mutate-animate'; import './render/index'; +import * as RenderUtils from './render/utils'; // ----- 类注册 Mota.register('class', 'AudioPlayer', AudioPlayer); @@ -163,7 +164,8 @@ Mota.register('module', 'Render', { RenderAdapter, Layer, LayerGroupFloorBinder, - Camera + Camera, + Utils: RenderUtils }); Mota.register('module', 'Action', { HeroKeyMover diff --git a/src/core/render/camera.ts b/src/core/render/camera.ts index 2161888..c1e3ff4 100644 --- a/src/core/render/camera.ts +++ b/src/core/render/camera.ts @@ -1,7 +1,8 @@ -import { Animation, Transition } from 'mutate-animate'; +import { Animation, TimingFn, Transition } from 'mutate-animate'; import { RenderItem } from './item'; import { logger } from '../common/logger'; import { Transform } from './transform'; +import EventEmitter from 'eventemitter3'; interface CameraTranslate { readonly type: 'translate'; @@ -26,7 +27,11 @@ interface CameraScale { type CameraOperation = CameraTranslate | CameraScale | CameraRotate; -export class Camera { +interface CameraEvent { + destroy: []; +} + +export class Camera extends EventEmitter { /** 当前绑定的渲染元素 */ readonly binded: RenderItem; /** 目标变换矩阵,默认与 `this.binded.transform` 同引用 */ @@ -62,6 +67,8 @@ export class Camera { } constructor(item: RenderItem) { + super(); + this.binded = item; this.delegation = item.delegateTicker(() => this.tick()); @@ -324,6 +331,13 @@ export class Camera { this.applyAnimation(time, update); } + /** + * 停止所有动画 + */ + stopAllAnimates() { + this.animationIds.forEach(v => this.binded.removeTicker(v)); + } + /** * 摧毁这个摄像机,当绑定元素被摧毁之后摄像机会一并摧毁,如果这个摄像机不使用了,一定要将它摧毁 */ @@ -331,5 +345,257 @@ export class Camera { this.binded.removeTicker(this.delegation); this.animationIds.forEach(v => this.binded.removeTicker(v)); Camera.cameraMap.delete(this.binded); + this.emit('destroy'); + } +} + +interface CameraAnimationBase { + type: string; + time: number; + start: number; +} + +interface TranslateAnimation extends CameraAnimationBase { + type: 'translate'; + timing: TimingFn; + x: number; + y: number; +} + +interface TranslateAsAnimation extends CameraAnimationBase { + type: 'translateAs'; + timing: TimingFn<2>; + time: number; +} + +interface RotateAnimation extends CameraAnimationBase { + type: 'rotate'; + timing: TimingFn; + angle: number; + time: number; +} + +interface ScaleAnimation extends CameraAnimationBase { + type: 'scale'; + timing: TimingFn; + scale: number; + time: number; +} + +type CameraAnimationData = + | TranslateAnimation + | TranslateAsAnimation + | RotateAnimation + | ScaleAnimation; + +interface CameraAnimationExecution { + data: CameraAnimationData[]; + animation: Animation; +} + +interface CameraAnimationEvent { + animate: [ + operation: CameraOperation, + execution: CameraAnimationExecution, + item: CameraAnimationData + ]; +} + +export class CameraAnimation extends EventEmitter { + camera: Camera; + + /** 动画开始时刻 */ + private startTime: number = 0; + /** 动画结束时刻 */ + private endTime: number = 0; + /** 委托ticker的id */ + private delegation: number; + /** 动画是否开始 */ + private started: boolean = false; + + /** 每个摄像机操作的动画映射 */ + private animateMap: Map = + new Map(); + + constructor(camera: Camera) { + super(); + + this.camera = camera; + this.delegation = camera.binded.delegateTicker(this.tick); + } + + private tick = () => { + if (!this.started) return; + const now = Date.now(); + const time = now - this.startTime; + if (now - this.startTime > this.endTime + 50) { + this.destroy(); + return; + } + this.animateMap.forEach((exe, ope) => { + const data = exe.data; + if (data.length === 0) return; + const item = data[0]; + if (item.time < time) { + this.executeAnimate(exe, item); + data.shift(); + this.emit('animate', ope, exe, item); + } + }); + }; + + private executeAnimate( + execution: CameraAnimationExecution, + animate: CameraAnimationData + ) { + if (animate.type === 'translateAs') { + const ani = this.ensureAnimate(execution); + ani.time(animate.time).moveAs(animate.timing); + } else if (animate.type === 'translate') { + const ani = this.ensureAnimate(execution); + const { x, y, time, timing } = animate; + ani.mode(timing).time(time).move(x, y); + } else if (animate.type === 'rotate') { + const ani = this.ensureAnimate(execution); + const { angle, time, timing } = animate; + ani.mode(timing).time(time).rotate(angle); + } else { + const ani = this.ensureAnimate(execution); + const { scale, time, timing } = animate; + ani.mode(timing).time(time).scale(scale); + } + } + + private ensureAnimate(execution: CameraAnimationExecution) { + if (execution.animation) return execution.animation; + const ani = new Animation(); + execution.animation = ani; + return ani; + } + + private ensureOperation(operation: CameraOperation) { + if (!this.animateMap.has(operation)) { + const data: CameraAnimationExecution = { + data: [], + animation: new Animation() + }; + this.animateMap.set(operation, data); + return data; + } else { + return this.animateMap.get(operation)!; + } + } + + /** + * 添加一个平移动画 + * @param operation 摄像机操作对象 + * @param x 目标横坐标 + * @param y 目标纵坐标 + * @param time 动画时长 + * @param start 动画开始时间 + * @param timing 动画的缓动函数 + */ + translate( + operation: CameraTranslate, + x: number, + y: number, + time: number, + start: number, + timing: TimingFn + ) { + const exe = this.ensureOperation(operation); + const data: TranslateAnimation = { + type: 'translate', + timing, + x, + y, + time, + start + }; + exe.data.push(data); + } + + /** + * 添加一个旋转动画 + * @param operation 摄像机操作 + * @param angle 目标旋转弧度,单位弧度 + * @param time 动画时长 + * @param start 动画开始时间 + * @param timing 动画的缓动函数 + */ + rotate( + operation: CameraRotate, + angle: number, + time: number, + start: number, + timing: TimingFn + ) { + const exe = this.ensureOperation(operation); + const data: RotateAnimation = { + type: 'rotate', + timing, + angle, + time, + start + }; + exe.data.push(data); + } + + /** + * 添加一个缩放动画 + * @param operation 摄像机操作 + * @param scale 目标缩放倍率 + * @param time 动画时长 + * @param start 动画开始时间 + * @param timing 动画的缓动函数 + */ + scale( + operation: CameraScale, + scale: number, + time: number, + start: number, + timing: TimingFn + ) { + const exe = this.ensureOperation(operation); + const data: ScaleAnimation = { + type: 'scale', + timing, + scale, + time, + start + }; + exe.data.push(data); + } + + /** + * 开始执行这个动画 + */ + start() { + if (this.started) return; + this.startTime = Date.now(); + this.started = true; + let endTime = 0; + this.animateMap.forEach((exe, ope) => { + const data = exe.data; + data.sort((a, b) => a.start - b.start); + const end = data.at(-1); + if (!end) return; + const t = end.start + end.time; + if (t > endTime) endTime = t; + + if (ope.type === 'translate') { + this.camera.applyTranslateAnimation(ope, exe.animation, t + 50); + } else if (ope.type === 'rotate') { + this.camera.applyRotateAnimation(ope, exe.animation, t + 50); + } else { + this.camera.applyScaleAnimation(ope, exe.animation, t + 50); + } + }); + this.endTime = endTime + this.startTime; + } + + destroy() { + this.camera.binded.removeTicker(this.delegation); + this.camera.stopAllAnimates(); } } diff --git a/src/core/render/preset/viewport.ts b/src/core/render/preset/viewport.ts index 4de854e..dadfe0e 100644 --- a/src/core/render/preset/viewport.ts +++ b/src/core/render/preset/viewport.ts @@ -102,6 +102,11 @@ export class FloorViewport implements ILayerGroupRenderExtends { */ enable() { this.enabled = true; + const { x, y } = core.status.hero.loc; + const { x: nx, y: ny } = this.transform; + this.nx = nx; + this.ny = ny; + this.mutateTo(x, y); } /** @@ -381,6 +386,12 @@ adapter.receive('setPosition', (item, x, y) => { item.setPosition(x, y); return Promise.resolve(); }); +adapter.receiveSync('disable', item => { + item.disable(); +}); +adapter.receiveSync('enable', item => { + item.enable(); +}); const hook = Mota.require('var', 'hook'); hook.on('changingFloor', (_, loc) => { diff --git a/src/core/render/utils.ts b/src/core/render/utils.ts new file mode 100644 index 0000000..34a5b73 --- /dev/null +++ b/src/core/render/utils.ts @@ -0,0 +1,14 @@ +import { RenderAdapter } from './adapter'; +import { FloorViewport } from './preset/viewport'; + +export function disableViewport() { + const adapter = RenderAdapter.get('viewport'); + if (!adapter) return; + adapter.sync('disable'); +} + +export function enableViewport() { + const adapter = RenderAdapter.get('viewport'); + if (!adapter) return; + adapter.sync('disable'); +} diff --git a/src/game/system.ts b/src/game/system.ts index 44b0c0c..f1ad5c2 100644 --- a/src/game/system.ts +++ b/src/game/system.ts @@ -40,6 +40,7 @@ import type { HeroKeyMover } from '@/core/main/action/move'; import type { BlockMover, HeroMover, ObjectMoverBase } from './state/move'; import type { Camera } from '@/core/render/camera'; import type * as Animation from 'mutate-animate'; +import type * as RenderUtils from '@/core/render/utils'; interface ClassInterface { // 渲染进程与游戏进程通用 @@ -122,6 +123,7 @@ interface ModuleInterface { Layer: typeof Layer; LayerGroupFloorBinder: typeof LayerGroupFloorBinder; Camera: typeof Camera; + Utils: typeof RenderUtils; }; State: { ItemState: typeof ItemState; @@ -130,7 +132,7 @@ interface ModuleInterface { ObjectMoverBase: typeof ObjectMoverBase; heroMoveCollection: { mover: HeroMover; - keyMover: HeroKeyMover; + keyMover?: HeroKeyMover; }; }; Action: { @@ -168,7 +170,6 @@ interface PluginInterface { chase_g: typeof import('../plugin/game/chase'); skill_g: typeof import('../plugin/game/skill'); towerBoss_g: typeof import('../plugin/game/towerBoss'); - rewrite_g: typeof import('../plugin/game/fx/rewrite'); itemDetail_g: typeof import('../plugin/game/fx/itemDetail'); checkBlock_g: typeof import('../plugin/game/enemy/checkblock'); }