From d0ff2f1dc5c65aed836868af4f87be14450e91c3 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Tue, 27 Aug 2024 18:34:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/render/adapter.ts | 27 ++- src/core/render/index.ts | 3 + src/core/render/preset/animate.ts | 262 ++++++++++++++++++++++++++++++ src/core/render/preset/floor.ts | 4 +- src/core/render/preset/hero.ts | 20 +-- src/plugin/game/fallback.ts | 87 +++++++++- src/types/util.d.ts | 5 +- 7 files changed, 391 insertions(+), 17 deletions(-) create mode 100644 src/core/render/preset/animate.ts diff --git a/src/core/render/adapter.ts b/src/core/render/adapter.ts index 29d37ec..7ff4abd 100644 --- a/src/core/render/adapter.ts +++ b/src/core/render/adapter.ts @@ -1,5 +1,6 @@ type AdapterFunction = (item: T, ...params: any[]) => Promise; type SyncAdapterFunction = (item: T, ...params: any[]) => any; +type GlobalAdapterFunction = (...params: any[]) => Promise; /** * 渲染适配器,用作渲染层与数据层沟通的桥梁,用于在数据层等待渲染层执行,常用与动画等。 @@ -16,6 +17,7 @@ export class RenderAdapter { private execute: Map> = new Map(); private syncExecutes: Map> = new Map(); + private globalExecutes: Map = new Map(); constructor(id: string) { this.id = id; @@ -36,11 +38,20 @@ export class RenderAdapter { this.items.delete(item); } + /** + * 定义一个类似于静态函数的函数,只会调用一次,不会对每个元素执行 + * @param id 函数名称 + * @param fn 执行的函数 + */ + receiveGlobal(id: string, fn: GlobalAdapterFunction) { + this.globalExecutes.set(id, fn); + } + /** * 设置执行函数 * @param fn 对于每个元素执行的函数 */ - recieve(id: string, fn: AdapterFunction): void { + receive(id: string, fn: AdapterFunction): void { this.execute.set(id, fn); } @@ -48,7 +59,7 @@ export class RenderAdapter { * 设置同步执行函数 * @param fn 对于每个元素执行的函数 */ - recieveSync(id: string, fn: SyncAdapterFunction): void { + receiveSync(id: string, fn: SyncAdapterFunction): void { this.syncExecutes.set(id, fn); } @@ -96,6 +107,18 @@ export class RenderAdapter { } } + /** + * 调用一个全局函数 + */ + global(id: string, ...params: any[]): Promise { + const execute = this.globalExecutes.get(id); + if (!execute) { + return Promise.reject(); + } else { + return execute(...params); + } + } + /** * 销毁这个adapter */ diff --git a/src/core/render/index.ts b/src/core/render/index.ts index bc202e2..3582955 100644 --- a/src/core/render/index.ts +++ b/src/core/render/index.ts @@ -6,6 +6,7 @@ import { LayerGroup, FloorLayer } from './preset/layer'; import { MotaRenderer } from './render'; import { LayerShadowExtends } from '../fx/shadow'; import { LayerGroupFilter } from '@/plugin/fx/gameCanvas'; +import { LayerGroupAnimate } from './preset/animate'; let main: MotaRenderer; @@ -31,12 +32,14 @@ Mota.require('var', 'loading').once('loaded', () => { const door = new LayerDoorAnimate(); const shadow = new LayerShadowExtends(); const filter = new LayerGroupFilter(); + const animate = new LayerGroupAnimate(); layer.extends(damage); layer.extends(detail); layer.extends(filter); layer.getLayer('event')?.extends(hero); layer.getLayer('event')?.extends(door); layer.getLayer('event')?.extends(shadow); + layer.extends(animate); render.appendChild(layer); }); diff --git a/src/core/render/preset/animate.ts b/src/core/render/preset/animate.ts new file mode 100644 index 0000000..f002d0b --- /dev/null +++ b/src/core/render/preset/animate.ts @@ -0,0 +1,262 @@ +import { logger } from '@/core/common/logger'; +import { RenderAdapter } from '../adapter'; +import { Sprite } from '../sprite'; +import { HeroRenderer } from './hero'; +import { ILayerGroupRenderExtends, LayerGroup } from './layer'; +import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; +import { transformCanvas } from '../item'; + +export class LayerGroupAnimate implements ILayerGroupRenderExtends { + static animateList: Set = new Set(); + id: string = 'animate'; + + group!: LayerGroup; + hero!: HeroRenderer; + animate!: Animate; + + private animation: Set = new Set(); + + /** + * 绘制一个跟随勇士的动画 + * @param name 动画id + */ + async drawHeroAnimate(name: AnimationIds) { + const animate = this.animate.animate(name, 0, 0); + this.updatePosition(animate); + await this.animate.draw(animate); + this.animation.delete(animate); + } + + private updatePosition(animate: AnimateData) { + if (!this.hero.renderable) return; + const { x, y } = this.hero.renderable; + const cell = this.group.cellSize; + const half = cell / 2; + animate.centerX = x * cell + half; + animate.centerY = y * cell + half; + } + + private onMoveTick = (x: number, y: number) => { + const cell = this.group.cellSize; + const half = cell / 2; + const ax = x * cell + half; + const ay = y * cell + half; + this.animation.forEach(v => { + v.centerX = ax; + v.centerY = ay; + }); + }; + + private listen() { + this.hero.on('moveTick', this.onMoveTick); + } + + awake(group: LayerGroup): void { + this.group = group; + const ex = group.getLayer('event')?.getExtends('floor-hero'); + if (ex instanceof HeroRenderer) { + this.hero = ex; + this.animate = new Animate(); + this.animate.size(group.width, group.height); + this.animate.setHD(true); + this.animate.setZIndex(100); + group.appendChild(this.animate); + LayerGroupAnimate.animateList.add(this); + this.listen(); + } else { + logger.error( + 14, + `Animate extends needs 'floor-hero' extends as dependency.` + ); + group.removeExtends('animate'); + } + } + + onDestroy(group: LayerGroup): void { + this.hero.off('moveTick', this.onMoveTick); + LayerGroupAnimate.animateList.delete(this); + } +} + +interface AnimateData { + obj: globalThis.Animate; + /** 第一帧是全局第几帧 */ + readonly start: number; + /** 当前是第几帧 */ + index: number; + /** 是否需要播放音频 */ + sound: boolean; + centerX: number; + centerY: number; + onEnd?: () => void; + readonly absolute: boolean; +} + +export class Animate extends Sprite { + /** 绝对位置的动画 */ + private absoluteAnimates: Set = new Set(); + /** 静态位置的动画 */ + private staticAnimates: Set = new Set(); + + private delegation: number; + private frame: number = 0; + private lastTime: number = 0; + + constructor() { + super('absolute', false); + + this.setRenderFn((canvas, transform) => { + const { ctx } = canvas; + ctx.save(); + this.drawAnimates(this.absoluteAnimates, canvas); + transformCanvas(canvas, transform); + this.drawAnimates(this.staticAnimates, canvas); + ctx.restore(); + }); + + this.delegation = this.delegateTicker(time => { + if (time - this.lastTime < 50) return; + this.lastTime = time; + this.frame++; + if ( + this.absoluteAnimates.size > 0 || + this.staticAnimates.size > 0 + ) { + this.update(this); + } + }); + + adapter.add(this); + } + + private drawAnimates( + data: Set, + canvas: MotaOffscreenCanvas2D + ) { + if (data.size === 0) return; + const { ctx } = canvas; + const toDelete = new Set(); + data.forEach(v => { + const obj = v.obj; + const index = v.index; + const frame = obj.frames[index]; + const ratio = obj.ratio; + if (!v.sound) { + const se = (index % obj.frame) + 1; + core.playSound(v.obj.se[se], v.obj.pitch[se]); + v.sound = true; + } + const centerX = v.centerX; + const centerY = v.centerY; + + frame.forEach(v => { + const img = obj.images[v.index]; + if (!img) return; + + const realWidth = (img.width * ratio * v.zoom) / 100; + const realHeight = (img.height * ratio * v.zoom) / 100; + ctx.globalAlpha = v.opacity / 255; + + const cx = centerX + v.x; + const cy = centerY + v.y; + + const ix = -realWidth / 2; + const iy = -realHeight / 2; + const angle = v.angle ? (-v.angle * Math.PI) / 180 : 0; + + ctx.save(); + ctx.translate(cx, cy); + if (v.mirror) { + ctx.scale(-1, 1); + } + ctx.rotate(angle); + ctx.drawImage(img, ix, iy, realWidth, realHeight); + ctx.restore(); + }); + const now = this.frame - v.start; + if (now !== v.index) v.sound = true; + v.index = now; + if (v.index === v.obj.frame) { + toDelete.add(v); + } + }); + toDelete.forEach(v => { + data.delete(v); + v.onEnd?.(); + }); + } + + /** + * 创建一个可以被执行的动画 + * @param name 动画名称 + * @param absolute 是否是绝对定位,绝对定位不会受到transform的影响 + */ + animate( + name: AnimationIds, + x: number, + y: number, + absolute: boolean = false + ) { + const animate = core.material.animates[name]; + const data: AnimateData = { + index: 0, + start: this.frame, + obj: animate, + centerX: x, + centerY: y, + absolute, + sound: false + }; + return data; + } + + /** + * 绘制动画,动画结束时兑现返回的Promise + * @param animate 动画信息 + * @returns + */ + draw(animate: AnimateData): Promise { + return new Promise(res => { + if (animate.absolute) { + this.absoluteAnimates.add(animate); + } else { + this.staticAnimates.add(animate); + } + animate.onEnd = () => { + res(); + }; + }); + } + + /** + * 根据动画名称、坐标、定位绘制动画 + * @param name 动画名称 + * @param absolute 是否是绝对定位 + */ + drawAnimate( + name: AnimationIds, + x: number, + y: number, + absolute: boolean = false + ) { + return this.draw(this.animate(name, x, y, absolute)); + } + + destroy(): void { + super.destroy(); + this.removeTicker(this.delegation); + adapter.remove(this); + } +} + +const adapter = new RenderAdapter('animate'); +adapter.receive('drawAnimate', (item, name, x, y, absolute) => { + return item.drawAnimate(name, x, y, absolute); +}); +adapter.receiveGlobal('drawHeroAnimate', name => { + const execute: Promise[] = []; + LayerGroupAnimate.animateList.forEach(v => { + execute.push(v.drawHeroAnimate(name)); + }); + return Promise.all(execute); +}); diff --git a/src/core/render/preset/floor.ts b/src/core/render/preset/floor.ts index fc24c0f..15247be 100644 --- a/src/core/render/preset/floor.ts +++ b/src/core/render/preset/floor.ts @@ -381,9 +381,9 @@ export class LayerDoorAnimate implements ILayerRenderExtends { } const doorAdapter = new RenderAdapter('door-animate'); -doorAdapter.recieve('openDoor', (item, block: Block) => { +doorAdapter.receive('openDoor', (item, block: Block) => { return item.openDoor(block); }); -doorAdapter.recieve('closeDoor', (item, block: Block) => { +doorAdapter.receive('closeDoor', (item, block: Block) => { return item.closeDoor(block); }); diff --git a/src/core/render/preset/hero.ts b/src/core/render/preset/hero.ts index feb44ef..8f6321e 100644 --- a/src/core/render/preset/hero.ts +++ b/src/core/render/preset/hero.ts @@ -359,42 +359,42 @@ export class HeroRenderer } const adapter = new RenderAdapter('hero-adapter'); -adapter.recieve('readyMove', item => { +adapter.receive('readyMove', item => { item.readyMove(); return Promise.resolve(); }); -adapter.recieve('move', (item, dir: Dir) => { +adapter.receive('move', (item, dir: Dir) => { return item.move(dir); }); -adapter.recieve('endMove', item => { +adapter.receive('endMove', item => { return item.endMove(); }); -adapter.recieve( +adapter.receive( 'moveAs', (item, x: number, y: number, time: number, fn: TimingFn<3>) => { return item.moveAs(x, y, time, fn); } ); -adapter.recieve('setMoveSpeed', (item, speed: number) => { +adapter.receive('setMoveSpeed', (item, speed: number) => { item.setMoveSpeed(speed); return Promise.resolve(); }); -adapter.recieve('setHeroLoc', (item, x?: number, y?: number) => { +adapter.receive('setHeroLoc', (item, x?: number, y?: number) => { item.setHeroLoc(x, y); return Promise.resolve(); }); -adapter.recieve('turn', (item, dir: Dir2) => { +adapter.receive('turn', (item, dir: Dir2) => { item.turn(dir); return Promise.resolve(); }); -adapter.recieve('setImage', (item, image: SizedCanvasImageSource) => { +adapter.receive('setImage', (item, image: SizedCanvasImageSource) => { item.setImage(image); return Promise.resolve(); }); // 同步fallback,用于适配现在的样板,之后会删除 -adapter.recieveSync('setHeroLoc', (item, x?: number, y?: number) => { +adapter.receiveSync('setHeroLoc', (item, x?: number, y?: number) => { item.setHeroLoc(x, y); }); -adapter.recieveSync('turn', (item, dir: Dir2) => { +adapter.receiveSync('turn', (item, dir: Dir2) => { item.turn(dir); }); diff --git a/src/plugin/game/fallback.ts b/src/plugin/game/fallback.ts index ea98060..c2f0ce4 100644 --- a/src/plugin/game/fallback.ts +++ b/src/plugin/game/fallback.ts @@ -1,26 +1,30 @@ import type { RenderAdapter } from '@/core/render/adapter'; +import type { LayerGroupAnimate } from '@/core/render/preset/animate'; import type { LayerDoorAnimate } from '@/core/render/preset/floor'; import type { HeroRenderer } from '@/core/render/preset/hero'; -import { hook } from '@/game/game'; interface Adapters { 'hero-adapter'?: RenderAdapter; 'door-animate'?: RenderAdapter; + animate?: RenderAdapter; } const adapters: Adapters = {}; export function init() { const hook = Mota.require('var', 'hook'); + const loading = Mota.require('var', 'loading'); let fallbackIds: number = 1e8; if (!main.replayChecking && main.mode === 'play') { const Adapter = Mota.require('module', 'Render').RenderAdapter; const hero = Adapter.get('hero-adapter'); const doorAnimate = Adapter.get('door-animate'); + const animate = Adapter.get('animate'); adapters['hero-adapter'] = hero; adapters['door-animate'] = doorAnimate; + adapters['animate'] = animate; } let moving: boolean = false; @@ -391,6 +395,87 @@ export function init() { this._openDoor_animate(block, x, y, callback); } }; + + // ----- animate & hero animate + ////// 绘制动画 ////// + maps.prototype.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?.(); + }); + }; + + maps.prototype.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?.(); + }); + + // 开始绘制 + // var animate = core.material.animates[name]; + // animate.se = animate.se || {}; + // if (typeof animate.se == 'string') animate.se = { 1: animate.se }; + + // var id = setTimeout(null); + // core.status.animateObjs.push({ + // name: name, + // id: id, + // animate: animate, + // hero: true, + // index: 0, + // callback: callback + // }); + + // return id; + }; + }); + + 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 ??= {}; + } }); return { readyMove, endMove, move }; diff --git a/src/types/util.d.ts b/src/types/util.d.ts index 44c380d..e1f2f73 100644 --- a/src/types/util.d.ts +++ b/src/types/util.d.ts @@ -753,7 +753,7 @@ type EventValuePreffix = interface Animate { /** - * 动画的帧数s + * 动画的帧数 */ frame: number; @@ -775,7 +775,8 @@ interface Animate { /** * 音效 */ - se: string; + se: any; + pitch: any; } type Save = DeepReadonly<{