diff --git a/idea.md b/idea.md index bc29949..995d8cb 100644 --- a/idea.md +++ b/idea.md @@ -117,3 +117,4 @@ dam4.png ---- 存档 59 [x] 复写 api,rewrite() [x] 对 vnode 进行简单的包装,提供出显示文字、显示图片等 api 以及修改 css 的 api [] mapDamage 注册 +[] Box 组件右下角添加 resize 按钮 diff --git a/src/core/render/adapter.ts b/src/core/render/adapter.ts new file mode 100644 index 0000000..2e6290b --- /dev/null +++ b/src/core/render/adapter.ts @@ -0,0 +1,88 @@ +type AdapterFunction = (item: T, ...params: any[]) => Promise; + +/** + * 渲染适配器,用作渲染层与数据层沟通的桥梁,用于在数据层等待渲染层执行,常用与动画等。 + * 例如移动图块,移动图块操作先由数据层执行,删除原有图块,然后通知渲染层进行移动动画, + * 而移动动画是耗时的,因此需要通过本类进行适配,实现等待移动动画执行完毕,然后进行后续操作 + */ +export class RenderAdapter { + static adapters: Map> = new Map(); + + /** 所有元素的集合 */ + items: Set = new Set(); + /** 适配器的id */ + id: string; + + private execute: Map> = new Map(); + + constructor(id: string) { + this.id = id; + RenderAdapter.adapters.set(id, this); + } + + /** + * 添加一个元素 + */ + add(item: T) { + this.items.add(item); + } + + /** + * 移除一个元素 + */ + remove(item: T) { + this.items.delete(item); + } + + /** + * 设置执行函数 + * @param fn 对于每个元素执行的函数 + */ + recieve(id: string, fn: AdapterFunction): void { + this.execute.set(id, fn); + } + + /** + * 对所有元素执行函数,当所有元素都运行完毕后兑现,类似于Promise.all + * @returns 包含每个元素运行结果的数组 + */ + all(fn: string, ...params: any[]): Promise { + const execute = this.execute.get(fn); + if (!execute) { + return Promise.reject(); + } else { + return Promise.all( + [...this.items].map(v => execute!(v, ...params)) + ); + } + } + + /** + * 对所有元素执行函数,当任意一个元素运行完毕后兑现,类似于Promise.any + * @returns 最先运行完毕的元素的结果 + */ + any(fn: string, ...params: any[]): Promise { + const execute = this.execute.get(fn); + if (!execute) { + return Promise.reject(); + } else { + return Promise.any( + [...this.items].map(v => execute!(v, ...params)) + ); + } + } + + /** + * 销毁这个adapter + */ + destroy() { + RenderAdapter.adapters.delete(this.id); + } + + /** + * 获取适配器 + */ + static get(id: string): RenderAdapter | undefined { + return this.adapters.get(id); + } +} diff --git a/src/core/render/cache.ts b/src/core/render/cache.ts index b6807ad..ad90fe2 100644 --- a/src/core/render/cache.ts +++ b/src/core/render/cache.ts @@ -44,7 +44,7 @@ interface TextureRequire { interface RenderableDataBase { /** 图块的总帧数 */ frame: number; - /** 对应图块属性的动画帧数,-1表示没有设定 */ + /** 对应图块属性的动画帧数,-1表示没有设定,0表示第一帧 */ animate: number; /** 是否是大怪物 */ bigImage: boolean; @@ -76,6 +76,15 @@ class TextureCache extends EventEmitter { renderable: Map = new Map(); /** 自动元件额外连接信息,用于对非自身图块进行连接 */ autoConn: Map> = new Map(); + /** 行走图朝向绑定 */ + characterDirection: Record = { + down: 0, + left: 1, + right: 2, + up: 3 + }; + /** 行走图转向顺序 */ + characterTurn: Dir[] = ['up', 'right', 'down', 'left']; constructor() { super(); diff --git a/src/core/render/item.ts b/src/core/render/item.ts index 8647659..e4b48cd 100644 --- a/src/core/render/item.ts +++ b/src/core/render/item.ts @@ -2,7 +2,7 @@ import { isNil } from 'lodash-es'; import { EventEmitter } from '../common/eventEmitter'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { Camera } from './camera'; -import { Ticker } from 'mutate-animate'; +import { Ticker, TickerFn } from 'mutate-animate'; export type RenderFunction = ( canvas: MotaOffscreenCanvas2D, @@ -123,6 +123,25 @@ interface IRenderFrame { requestRenderFrame(fn: () => void): void; } +interface IRenderTickerSupport { + /** + * 委托ticker,让其在指定时间范围内每帧执行对应函数,超过时间后自动删除 + * @param fn 每帧执行的函数 + * @param time 函数持续时间,不填代表不会自动删除,需要手动删除 + * @param end 持续时间结束后执行的函数 + * @returns 委托id,可用于删除 + */ + delegateTicker(fn: TickerFn, time?: number, end?: () => void): number; + + /** + * 移除ticker函数 + * @param id 函数id,也就是{@link IRenderTickerSupport.delegateTicker}的返回值 + * @param callEnd 是否调用结束函数,即{@link IRenderTickerSupport.delegateTicker}的end参数 + * @returns 是否删除成功,比如对应ticker不存在,就是删除失败 + */ + removeTicker(id: number, callEnd?: boolean): boolean; +} + interface RenderItemEvent { beforeUpdate: (item?: RenderItem) => void; afterUpdate: (item?: RenderItem) => void; @@ -130,10 +149,16 @@ interface RenderItemEvent { afterRender: () => void; } +interface TickerDelegation { + fn: TickerFn; + endFn?: () => void; +} + const beforeFrame: (() => void)[] = []; const afterFrame: (() => void)[] = []; const renderFrame: (() => void)[] = []; +// todo: 添加模型变换 export abstract class RenderItem extends EventEmitter implements @@ -141,12 +166,17 @@ export abstract class RenderItem IRenderUpdater, IRenderAnchor, IRenderConfig, - IRenderFrame + IRenderFrame, + IRenderTickerSupport { /** 渲染的全局ticker */ static ticker: Ticker = new Ticker(); /** 包括但不限于怪物、npc、自动元件的动画帧数 */ static animatedFrame: number = 0; + /** ticker委托映射 */ + static tickerMap: Map = new Map(); + /** ticker委托id */ + static tickerId: number = 0; zIndex: number = 0; @@ -263,6 +293,32 @@ export abstract class RenderItem renderFrame.push(fn); } + delegateTicker(fn: TickerFn, time?: number, end?: () => void): number { + const id = RenderItem.tickerId++; + if (typeof time === 'number' && time === 0) return id; + const delegation: TickerDelegation = { + fn, + endFn: end + }; + RenderItem.tickerMap.set(id, delegation); + RenderItem.ticker.add(fn); + if (typeof time === 'number' && time < 2147438647 && time > 0) { + setTimeout(() => { + RenderItem.ticker.remove(fn); + end?.(); + }, time); + } + return id; + } + + removeTicker(id: number, callEnd: boolean = true): boolean { + const delegation = RenderItem.tickerMap.get(id); + if (!delegation) return false; + RenderItem.ticker.remove(delegation.fn); + if (callEnd) delegation.endFn?.(); + return true; + } + /** * 隐藏这个元素 */ diff --git a/src/core/render/preset/damage.ts b/src/core/render/preset/damage.ts index cbc428f..8e99e9c 100644 --- a/src/core/render/preset/damage.ts +++ b/src/core/render/preset/damage.ts @@ -37,7 +37,6 @@ export class FloorDamageExtends implements ILayerGroupRenderExtends { this.sprite.setMapSize(map.width, map.height); ensureFloorDamage(floor); const enemy = core.status.maps[floor].enemy; - console.log(enemy); this.sprite.updateCollection(enemy); } @@ -386,7 +385,7 @@ export class Damage extends Sprite { * @param camera 摄像机 */ renderDamage(camera: Camera) { - console.time('damage'); + // console.time('damage'); const { ctx } = this.damageMap; ctx.save(); transformCanvas(this.damageMap, camera, true); @@ -435,6 +434,6 @@ export class Damage extends Sprite { block.cache.set(v, temp.canvas); }); ctx.restore(); - console.timeEnd('damage'); + // console.timeEnd('damage'); } } diff --git a/src/core/render/preset/floor.ts b/src/core/render/preset/floor.ts index 0d4e857..416a42e 100644 --- a/src/core/render/preset/floor.ts +++ b/src/core/render/preset/floor.ts @@ -39,6 +39,7 @@ hook.on('changingFloor', floor => { interface LayerGroupBinderEvent { update: [floor: FloorIds]; setBlock: [x: number, y: number, floor: FloorIds, block: AllNumbers]; + floorChange: [floor: FloorIds]; } /** @@ -86,7 +87,7 @@ export class LayerGroupFloorBinder * 在下一帧进行绑定数据更新 */ updateBind() { - if (this.needUpdate) return; + if (this.needUpdate || !this.group) return; this.needUpdate = true; this.group.requestBeforeFrame(() => { this.needUpdate = false; @@ -182,6 +183,7 @@ export class LayerFloorBinder implements ILayerRenderExtends { bindThis() { this.floor = void 0; this.bindThisFloor = true; + this.updateBind(); } /** @@ -191,6 +193,7 @@ export class LayerFloorBinder implements ILayerRenderExtends { bindFloor(floorId: FloorIds) { this.bindThisFloor = false; this.floor = floorId; + this.updateBind(); } /** diff --git a/src/core/render/preset/hero.ts b/src/core/render/preset/hero.ts index 4ed898f..e66dc5d 100644 --- a/src/core/render/preset/hero.ts +++ b/src/core/render/preset/hero.ts @@ -1,3 +1,266 @@ -import { ILayerRenderExtends } from './layer'; +import { ILayerRenderExtends, Layer, LayerMovingRenderable } from './layer'; +import { SizedCanvasImageSource } from './misc'; +import { RenderAdapter } from '../adapter'; +import { logger } from '@/core/common/logger'; +import EventEmitter from 'eventemitter3'; +import { texture } from '../cache'; -export class HeroRenderer implements ILayerRenderExtends {} +type HeroMovingStatus = 'stop' | 'moving'; + +interface HeroRenderEvent { + stepEnd: []; +} + +export class HeroRenderer + extends EventEmitter + implements ILayerRenderExtends +{ + id: string = 'floor-hero'; + + /** 勇士的图片资源 */ + image?: SizedCanvasImageSource; + cellWidth?: number; + cellHeight?: number; + + /** 勇士的渲染信息 */ + renderable?: LayerMovingRenderable; + layer!: Layer; + + // hero?: Hero; + + /** 勇士移动状态 */ + status: HeroMovingStatus = 'stop'; + /** 当前移动帧数 */ + movingFrame: number = 0; + + /** 勇士移动速度 */ + speed: number = 100; + + /** 勇士移动定时器id */ + private moveId: number = -1; + /** 上一次帧数切换的时间 */ + private lastFrameTime: number = 0; + /** 当前的移动方向 */ + private moveDir: Dir = 'down'; + /** 上一步走到格子上的时间 */ + private lastStepTime: number = 0; + /** 是否已经执行了当前步移动 */ + private moveDetached?: Promise; + /** + * 这一步的移动方向,与{@link moveDir}不同的是,在这一步走完之前,它都不会变, + * 当这一步走完之后,才会将其设置为{@link moveDir}的值 + */ + private stepDir: Dir = 'down'; + /** 每步的格子增量 */ + private stepDelta: Loc = { x: 0, y: 1 }; + + /** + * 设置勇士所用的图片资源 + * @param image 图片资源 + */ + setImage(image: SizedCanvasImageSource) { + this.image = image; + this.split(); + this.layer.update(this.layer); + } + + setMoveSpeed(speed: number) { + this.speed = speed; + } + + /** + * 分割勇士图像,生成renderable信息 + */ + split() { + this.cellWidth = this.image!.width / 4; + this.cellHeight = this.image!.height / 4; + this.generateRenderable(); + } + + /** + * 生成渲染信息 + */ + generateRenderable() { + if (!this.image) return; + this.renderable = { + image: this.image, + frame: 4, + x: core.status.hero.loc.x, + y: core.status.hero.loc.y, + zIndex: core.status.hero.loc.y, + autotile: false, + bigImage: true, + render: this.getRenderFromDir(this.moveDir), + animate: 0 + }; + } + + /** + * 根据方向获取勇士的裁切信息 + * @param dir 方向 + */ + getRenderFromDir(dir: Dir): [number, number, number, number][] { + if (!this.cellWidth || !this.cellHeight) return []; + const index = texture.characterDirection[dir]; + const y = index * this.cellHeight; + return [0, 1, 2, 3].map(v => { + return [v * this.cellWidth!, y, this.cellWidth!, this.cellHeight!]; + }); + } + + /** + * 设置当前勇士 + * @param hero 绑定的勇士 + */ + // setHero(hero: Hero) { + // this.hero = hero; + // } + + /** + * 准备开始移动 + */ + readyMove() { + if (this.status !== 'stop') return; + this.status = 'moving'; + this.lastFrameTime = Date.now(); + } + + /** + * 勇士移动定时器 + */ + private moveTick(time: number) { + if (this.status !== 'moving') return; + if (!this.renderable) return; + + if (time - this.lastFrameTime > this.speed) { + this.lastFrameTime = time; + this.movingFrame++; + this.movingFrame %= 4; + } + + const progress = (time - this.lastStepTime) / this.speed; + const { x: dx, y: dy } = this.stepDelta; + const { x, y } = core.status.hero.loc; + if (progress >= 1) { + this.renderable.x = x + dx; + this.renderable.y = y + dy; + this.emit('stepEnd'); + } else { + const rx = dx * progress + x; + const ry = dy * progress + y; + this.renderable.x = rx; + this.renderable.y = ry; + } + this.renderable.animate = this.movingFrame; + this.layer.update(this.layer); + } + + /** + * 进行下一步的移动准备,设置移动信息 + */ + private step() { + this.stepDir = this.moveDir; + this.lastStepTime = Date.now(); + this.stepDelta = core.utils.scan[this.stepDir]; + this.turn(this.stepDir); + } + + /** + * 移动勇士 + */ + move(dir: Dir): Promise { + if (this.status !== 'moving') { + logger.error( + 12, + `Cannot move while status is not 'moving'. Call 'readyMove' first.` + ); + return Promise.reject(); + } + + this.moveDir = dir; + + if (this.moveDetached) return this.moveDetached; + else { + this.step(); + return (this.moveDetached = new Promise(resolve => { + this.moveDetached = void 0; + this.once('stepEnd', resolve); + })); + } + } + + /** + * 结束勇士的移动过程 + */ + endMove(): Promise { + if (this.status !== 'moving') return Promise.reject(); + return new Promise(resolve => { + this.once('stepEnd', () => { + this.status = 'stop'; + this.movingFrame = 0; + this.layer.removeTicker(this.moveId); + this.render(); + resolve(); + }); + }); + } + + /** + * 勇士转向,不填表示顺时针转一个方向 + * @param dir 移动方向 + */ + turn(dir?: Dir): void { + if (!dir) { + const index = texture.characterTurn.indexOf(this.moveDir) + 1; + const length = texture.characterTurn.length; + const next = texture.characterTurn[index % length]; + return this.turn(next); + } + this.moveDir = dir; + if (!this.renderable) return; + this.renderable.render = this.getRenderFromDir(this.moveDir); + this.layer.update(this.layer); + } + + /** + * 渲染勇士 + */ + render() { + if (!this.renderable) return; + if (this.status === 'stop') { + this.renderable.animate = -1; + } else { + this.renderable.animate = this.movingFrame; + } + this.layer.update(this.layer); + } + + awake(layer: Layer): void { + this.layer = layer; + adapter.add(this); + this.moveId = layer.delegateTicker(() => { + this.moveTick(Date.now()); + }); + } + + onDestroy(layer: Layer): void { + adapter.remove(this); + layer.removeTicker(this.moveId); + } + + onMovingUpdate(layer: Layer, renderable: LayerMovingRenderable[]): void { + if (this.renderable) renderable.push(this.renderable); + } +} + +const adapter = new RenderAdapter('hero-adapter'); +adapter.recieve('readyMove', item => { + item.readyMove(); + return Promise.resolve(); +}); +adapter.recieve('move', (item, dir: Dir) => { + return item.move(dir); +}); +adapter.recieve('endMove', item => { + return item.endMove(); +}); diff --git a/src/core/render/preset/layer.ts b/src/core/render/preset/layer.ts index e12df2a..603d620 100644 --- a/src/core/render/preset/layer.ts +++ b/src/core/render/preset/layer.ts @@ -168,29 +168,6 @@ export class LayerGroup extends Container { this.cellSize = size; } - /** - * 使用预设显示模式,注意切换后所有的旧Layer实例会被摧毁 - * @param preset 预设名称 - */ - // usePreset(preset: LayerGroupPreset) { - // this.emptyLayer(); - - // const child = layers.map((v, i) => { - // const layer = new Layer(); - // layer.bindLayer(v); - // layer.setZIndex(i * 10); - // this.layers.set(v, layer); - // return layer; - // }); - // this.appendChild(...child); - // if (preset === 'defaults') { - // const damage = new Damage(this.floorId); - // this.appendChild(damage); - // this.damage = damage; - // damage.setZIndex(60); - // } - // } - /** * 清空所有层 */ @@ -290,45 +267,6 @@ export class LayerGroup extends Container { } } - /** - * 绑定当前楼层 - */ - // bindThis() { - // this.bindThisFloor = true; - // this.updateFloor(); - // } - - /** - * 绑定楼层信息 - * @param floor 绑定楼层,不填时表示绑定为当前楼层 - */ - // bindFloor(floor?: FloorIds) { - // if (!floor) { - // this.bindThisFloor = true; - // } else { - // this.floorId = floor; - // } - // this.updateFloor(); - // } - - /** - * 更新地图信息 - */ - // updateFloor() { - // if (this.bindThisFloor) { - // this.floorId = core.status.floorId; - // } - // const floor = this.floorId; - // if (!floor) return; - // this.layers.forEach(v => { - // v.bindData(floor); - // if (v.layer === 'bg') { - // v.bindBackground(floor); - // } - // }); - // // this.damage?.bindFloor(floor); - // } - /** * 缓存计算应该渲染的块 * @param camera 摄像机 @@ -348,37 +286,6 @@ export class LayerGroup extends Container { this.needRender = void 0; } - /** - * 添加伤害显示层,并将显示层返回,如果已经添加,则会返回已经添加的显示层 - */ - // addDamage() { - // if (!this.damage) { - // const damage = new Damage(); - // this.appendChild(damage); - // this.damage = damage; - // if (this.floorId) damage.bindFloor(this.floorId); - // return damage; - // } - // return this.damage; - // } - - /** - * 移除伤害显示层 - */ - // removeDamage() { - // if (this.damage) { - // this.removeChild(this.damage); - // this.damage = void 0; - // } - // } - - /** - * 更新指定区域内的伤害渲染信息 - */ - // updateDamage(x: number, y: number, width: number, height: number) { - // this.damage?.updateRenderable(x, y, width, height); - // } - /** * 更新动画帧 */ @@ -405,31 +312,6 @@ export class LayerGroup extends Container { const hook = Mota.require('var', 'hook'); -// hook.on('changingFloor', floorId => { -// LayerGroup.list.forEach(v => { -// if (v.floorId === floorId || v.bindThisFloor) v.updateFloor(); -// }); -// }); -// hook.on('setBlock', (x, y, floorId, block) => { -// LayerGroup.list.forEach(v => { -// if (v.floorId === floorId) { -// v.updateDamage(x, y, 1, 1); -// v.layers.forEach(v => { -// if (v.layer === 'event') { -// v.putRenderData([block], 1, x, y); -// } -// }); -// } -// }); -// }); -// hook.on('statusBarUpdate', () => { -// LayerGroup.list.forEach(v => { -// if (v.floorId) { -// v.damage?.bindFloor(v.floorId); -// } -// }); -// }); - // todo: animate frame. // renderEmits.on('animateFrame', () => { // LayerGroup.list.forEach(v => { @@ -662,10 +544,6 @@ export interface ILayerRenderExtends { autotiles: Record ): void; - // onDataBind?: (layer: Layer, floor: FloorIds, name?: FloorLayer) => void; - // onLayerBind?: (layer: Layer, name: FloorLayer) => void; - // onDataUpdate?: (layer: Layer, data: number[]) => void; - /** * 当地图大小修改时执行的函数 * @param layer 目标Layer实例 @@ -695,7 +573,7 @@ export interface ILayerRenderExtends { /** * 当更新移动层的渲染信息是执行的函数 * @param layer 目标Layer实例 - * @param renderable 移动层的渲染信息(包含大怪物) + * @param renderable 移动层的渲染信息(包含大怪物),未排序 */ onMovingUpdate?(layer: Layer, renderable: LayerMovingRenderable[]): void; @@ -728,7 +606,7 @@ interface LayerCacheItem { canvas: HTMLCanvasElement; } -interface LayerMovingRenderable extends RenderableData { +export interface LayerMovingRenderable extends RenderableData { zIndex: number; x: number; y: number; @@ -800,6 +678,7 @@ export class Layer extends Container { floorId?: FloorIds; /** 渲染的层 */ layer?: FloorLayer; + // todo: renderable分块存储,优化循环绘制性能 /** 渲染数据 */ renderData: number[] = []; /** 自动元件的连接信息,键表示图块在渲染数据中的索引,值表示连接信息,是个8位二进制 */ @@ -823,6 +702,7 @@ export class Layer extends Container { moving: MovingBlock[] = []; /** 大怪物渲染信息 */ bigImages: Map = new Map(); + // todo: 是否需要桶排? /** 移动层的渲染信息 */ movingRenderable: LayerMovingRenderable[] = []; /** 下一此渲染时是否需要更新移动层的渲染信息 */ @@ -1207,45 +1087,6 @@ export class Layer extends Container { } } - /** - * 绑定渲染楼层 - * @param floor 楼层id - * @param layer 渲染的层数,例如是背景层还是事件层等 - */ - // bindData(floor: FloorIds, layer?: FloorLayer) { - // this.floorId = floor; - // if (layer) this.layer = layer; - // const f = core.status.maps[floor]; - // this.mapWidth = f.width; - // this.mapHeight = f.height; - // this.block.size(f.width, f.height); - // this.updateDataFromFloor(); - // } - - /** - * 绑定显示层 - * @param layer 绑定的层 - */ - // bindLayer(layer: FloorLayer) { - // this.layer = layer; - // this.updateDataFromFloor(); - // } - - /** - * 从地图数据更新渲染数据,要求已经绑定渲染楼层,否则无事发生 - */ - // updateDataFromFloor() { - // if (!this.floorId || !this.layer) return; - // const floor = core.status.maps[this.floorId]; - // if (this.layer === 'event') { - // const map = floor.map; - // this.putRenderData(map.flat(), floor.width, 0, 0); - // } else { - // const map = core.maps._getBgFgMapArray(this.layer, this.floorId); - // this.putRenderData(map.flat(), floor.width, 0, 0); - // } - // } - /** * 设置地图大小,会清空渲染数据(且丢失引用),因此后面应当紧跟 putRenderData,以保证渲染正常进行 * @param width 地图宽度 @@ -1323,11 +1164,11 @@ export class Layer extends Container { }); } }); - this.movingRenderable.sort((a, b) => a.zIndex - b.zIndex); for (const ex of this.extend.values()) { ex.onMovingUpdate?.(this, this.movingRenderable); } + this.movingRenderable.sort((a, b) => a.zIndex - b.zIndex); } /** @@ -1499,26 +1340,34 @@ export class Layer extends Container { ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.translate(core._PX_ / 2, core._PY_ / 2); ctx.transform(a, b, c, d, e, f); - const r = - Math.max(a, b, c, d) ** 2 * Math.max(core._PX_, core._PY_) * 2; + const max1 = Math.max(a, b, c, d) ** 2; + const max2 = Math.max(core._PX_, core._PY_) * 2; + const r = (max1 * max2) ** 2; this.movingRenderable.forEach(v => { - const { x, y, image, frame: blockFrame, render } = v; - const f = frame % 4; - const i = frame === 4 && blockFrame === 3 ? 1 : f; + const { x, y, image, frame: blockFrame, render, animate } = v; + const ff = frame % 4; + const i = + animate === -1 + ? frame === 4 && blockFrame === 3 + ? 1 + : ff + : animate; const [sx, sy, w, h] = render[i]; const px = x * cell - w / 2 + halfCell; const py = y * cell - h + cell; const ex = px + w; const ey = py + h; + if ( - (px - e) ** 2 > r || - (py - f) ** 2 > r || - (ex - e) ** 2 > r || - (ey - f) ** 2 > r + (px + e) ** 2 > r || + (py + f) ** 2 > r || + (ex + e) ** 2 > r || + (ey + f) ** 2 > r ) { return; } + ctx.drawImage(image, sx, sy, w, h, px, py, w, h); }); @@ -1578,339 +1427,3 @@ export class Layer extends Container { super.destroy(); } } - -// interface DamageRenderable { -// x: number; -// y: number; -// align: CanvasTextAlign; -// baseline: CanvasTextBaseline; -// text: string; -// color: CanvasStyle; -// } - -// export class Damage extends Sprite { -// floorId?: FloorIds; - -// mapWidth: number = 0; -// mapHeight: number = 0; - -// /** 键表示格子索引,值表示在这个格子上的渲染信息(当然实际渲染位置可以不在这个格子上) */ -// renderable: Map = new Map(); -// block: BlockCacher; -// /** 记录所有需要重新计算伤害的分块,这样可以不用一次性计算全地图的伤害,从而优化性能 */ -// needUpdateBlock: Set = new Set(); - -// cellSize: number = 32; - -// /** 伤害渲染层,渲染至之后再渲染到目标层 */ -// damageMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); -// /** 字体 */ -// font: string = "14px 'normal'"; -// /** 描边样式 */ -// strokeColor: CanvasStyle = '#000'; -// /** 描边粗细 */ -// strokeWidth: number = 2; - -// constructor(floor?: FloorIds) { -// super(); - -// this.block = new BlockCacher(0, 0, core._WIDTH_, 1); -// this.type = 'absolute'; -// if (floor) this.bindFloor(floor); -// this.size(core._PX_, core._PY_); -// this.damageMap.withGameScale(true); -// this.damageMap.setHD(true); -// this.damageMap.setAntiAliasing(true); -// this.damageMap.size(core._PX_, core._PY_); - -// this.setRenderFn((canvas, camera) => { -// const { ctx } = canvas; -// const { width, height } = canvas.canvas; -// ctx.save(); -// ctx.imageSmoothingEnabled = false; -// this.renderDamage(camera); -// ctx.setTransform(1, 0, 0, 1, 0, 0); -// ctx.drawImage(this.damageMap.canvas, 0, 0, width, height); -// ctx.restore(); -// }); -// } - -// /** -// * 更新楼层信息 -// */ -// updateFloor(): void { -// const floor = this.floorId; -// if (!floor) return; -// core.extractBlocks(floor); -// const f = core.status.maps[floor]; -// this.updateRenderable(0, 0, f.width, f.height); -// } - -// /** -// * 绑定显示楼层 -// * @param floor 绑定的楼层 -// */ -// bindFloor(floor: FloorIds) { -// this.floorId = floor; -// core.extractBlocks(this.floorId); -// const f = core.status.maps[this.floorId]; -// this.mapWidth = f.width; -// this.mapHeight = f.height; -// this.block.size(f.width, f.height); -// this.updateFloor(); -// } - -// /** -// * 根据需要更新的区域更新显示信息,注意调用前需要保证怪物信息是最新的,也就是要在计算过怪物信息后才能调用这个 -// * @param block 要更新的区域 -// */ -// updateRenderableBlock(block: Set) { -// if (!this.floorId) return; -// const size = this.block.blockSize; - -// Mota.require('fn', 'ensureFloorDamage')(this.floorId); -// const col = core.status.maps[this.floorId].enemy; -// const obj = core.getMapBlocksObj(this.floorId); - -// if (block.size === 1) { -// // 如果是单个分块,直接进行更新 -// const index = [...block.values()][0]; -// if (!this.needUpdateBlock.has(index)) return; -// this.needUpdateBlock.delete(index); - -// const [x, y] = this.block.getBlockXYByIndex(index); -// const sx = x * size; -// const sy = y * size; -// const ex = sx + size; -// const ey = sy + size; - -// for (let ny = sy; ny < ey; ny++) { -// for (let nx = sx; nx < ex; nx++) { -// const index = nx + ny * this.mapWidth; -// this.renderable.delete(index); -// this.pushMapDamage(obj, col, nx, ny); -// } -// } - -// col.range -// .scan('rect', { x: sx, y: sy, w: size, h: size }) -// .forEach(v => { -// if (isNil(v.x) || isNil(v.y)) return; -// this.pushEnemyDamage(v, v.x, v.y); -// }); -// } else { -// // 否则使用 X 扫描线的方式,获取每个y坐标对应的最小最大x坐标,从而可以更快地找出在范围内的怪物 -// const xyMap: Map = new Map(); -// const toEmitArea: [number, number, number, number][] = []; - -// block.forEach(v => { -// if (!this.needUpdateBlock.has(v)) return; -// this.needUpdateBlock.delete(v); -// const [x, y] = this.block.getBlockXYByIndex(v); -// const sx = x * size; -// const sy = y * size; -// const ex = sx + size; -// const ey = sy + size; -// toEmitArea.push([sx, sy, size, size]); - -// for (let ny = sy; ny < ey; ny++) { -// let arr = xyMap.get(ny); -// if (!arr) { -// arr = [sx, ex]; -// xyMap.set(ny, arr); -// } else { -// if (sx < arr[0]) arr[0] = sx; -// if (ex > arr[1]) arr[1] = ex; -// } -// for (let nx = sx; nx < ex; nx++) { -// const index = nx + ny * this.mapWidth; -// this.renderable.delete(index); -// this.pushMapDamage(obj, col, x, y); -// } -// } -// }); - -// xyMap.forEach(([sx, ex], y) => { -// col.list.forEach(v => { -// if (isNil(v.x) || isNil(v.y)) return; -// if (v.y !== y || v.x < sx || v.x >= ex) return; -// this.pushEnemyDamage(v, v.x, v.y); -// }); -// }); - -// toEmitArea.forEach(v => { -// this.emit('dataUpdate', v[0], v[1], v[2], v[3]); -// }); -// this.update(this); -// } -// } - -// /** -// * 更新指定区域内的渲染信息,注意调用前需要保证怪物信息是最新的,也就是要在计算过怪物信息后才能调用这个 -// */ -// updateRenderable(x: number, y: number, width: number, height: number) { -// if (!this.floorId) return; -// this.block.getIndexOf(x, y, width, height).forEach(v => { -// this.block.clearCache(v, 1); -// this.needUpdateBlock.add(v); -// }); -// this.update(this); -// } - -// /** -// * 向渲染列表添加渲染内容,应当在 `dataUpdate` 的事件中进行调用,其他位置不应当直接调用 -// */ -// pushDamageRenderable(x: number, y: number, ...data: DamageRenderable[]) { -// const index = x + y * this.mapWidth; -// let arr = this.renderable.get(index); -// if (!arr) { -// arr = []; -// this.renderable.set(index, arr); -// } -// arr.push(...data); -// } - -// private pushMapDamage( -// obj: Record, -// col: EnemyCollection, -// x: number, -// y: number -// ) { -// const loc = `${x},${y}` as LocString; -// const dam = col.mapDamage[loc]; - -// if (!dam || obj[loc]?.event.noPass) return; -// let text = ''; -// let color = '#fa3'; -// if (dam.damage > 0) { -// text = core.formatBigNumber(dam.damage, true); -// } else if (dam.mockery) { -// dam.mockery.sort((a, b) => -// a[0] === b[0] ? a[1] - b[1] : a[0] - b[0] -// ); -// const [tx, ty] = dam.mockery[0]; -// const dir = x > tx ? '←' : x < tx ? '→' : y > ty ? '↑' : '↓'; -// text = '嘲' + dir; -// color = '#fd4'; -// } else if (dam.hunt) { -// text = '猎'; -// color = '#fd4'; -// } else { -// return; -// } - -// const mapDam: DamageRenderable = { -// align: 'center', -// baseline: 'middle', -// text, -// color, -// x: x * this.cellSize + this.cellSize / 2, -// y: y * this.cellSize + this.cellSize / 2 -// }; -// this.pushDamageRenderable(x, y, mapDam); -// } - -// private pushEnemyDamage(enemy: DamageEnemy, x: number, y: number) { -// const dam = enemy.calDamage().damage; -// const cri = enemy.calCritical(1)[0]?.atkDelta ?? Infinity; -// const dam1: DamageRenderable = { -// align: 'left', -// baseline: 'alphabetic', -// text: !isFinite(dam) ? '???' : core.formatBigNumber(dam, true), -// color: getDamageColor(dam), -// x: x * this.cellSize + 1, -// y: y * this.cellSize + this.cellSize - 1 -// }; -// const dam2: DamageRenderable = { -// align: 'left', -// baseline: 'alphabetic', -// text: !isFinite(cri) ? '?' : core.formatBigNumber(cri, true), -// color: '#fff', -// x: x * this.cellSize + 1, -// y: y * this.cellSize + this.cellSize - 11 -// }; -// this.pushDamageRenderable(x, y, dam1, dam2); -// } - -// /** -// * 计算需要渲染哪些块 -// */ -// calNeedRender(camera: Camera) { -// if (this.parent instanceof LayerGroup) { -// // 如果处于地图组中,每个地图的渲染区域应该是一样的,因此可以缓存优化 -// return this.parent.cacheNeedRender(camera, this.block); -// } else if (this.parent instanceof Layer) { -// // 如果是地图的子元素,直接调用Layer的计算函数 -// return this.parent.calNeedRender(camera); -// } else { -// return calNeedRenderOf(camera, this.cellSize, this.block); -// } -// } - -// /** -// * 渲染伤害值 -// */ -// renderDamage(camera: Camera) { -// if (!this.floorId) return; - -// const { ctx } = this.damageMap; -// ctx.save(); -// transformCanvas(this.damageMap, camera, true); - -// const { res: render } = this.calNeedRender(camera); -// this.updateRenderableBlock(render); -// const block = this.block; -// const cell = this.cellSize; -// const size = cell * block.blockSize; -// render.forEach(v => { -// const [x, y] = block.getBlockXYByIndex(v); -// const bx = x * block.blockSize; -// const by = y * block.blockSize; -// const px = bx * cell; -// const py = by * cell; - -// // 检查有没有缓存 -// const cache = block.cache.get(v * block.cacheDepth); -// if (cache && cache.floorId === this.floorId) { -// ctx.drawImage(cache.canvas, px, py, size, size); -// return; -// } - -// // 否则依次渲染并写入缓存 -// const temp = new MotaOffscreenCanvas2D(); -// temp.setHD(true); -// temp.setAntiAliasing(true); -// temp.withGameScale(true); -// temp.size(size, size); -// const { ctx: ct } = temp; -// ct.font = this.font; -// ct.strokeStyle = this.strokeColor; -// ct.lineWidth = this.strokeWidth; - -// const ex = bx + block.blockSize; -// const ey = by + block.blockSize; -// for (let nx = bx; nx < ex; nx++) { -// for (let ny = by; ny < ey; ny++) { -// const index = nx + ny * block.blockSize; -// const render = this.renderable.get(index); - -// render?.forEach(v => { -// if (!v) return; -// ct.fillStyle = v.color; -// ct.textAlign = v.align; -// ct.textBaseline = v.baseline; -// ct.strokeText(v.text, v.x, v.y); -// ct.fillText(v.text, v.x, v.y); -// }); -// } -// } - -// ct.drawImage(temp.canvas, px, py, size, size); -// block.cache.set(v * block.cacheDepth, { -// canvas: temp.canvas, -// floorId: this.floorId -// }); -// }); -// ctx.restore(); -// } -// } diff --git a/src/core/render/render.ts b/src/core/render/render.ts index 93f95a6..268616e 100644 --- a/src/core/render/render.ts +++ b/src/core/render/render.ts @@ -6,6 +6,7 @@ import { RenderItem, transformCanvas, withCacheRender } from './item'; import { FloorLayer, Layer, LayerGroup } from './preset/layer'; import { LayerGroupFloorBinder } from './preset/floor'; import { FloorDamageExtends } from './preset/damage'; +import { HeroRenderer } from './preset/hero'; export class MotaRenderer extends Container { static list: Set = new Set(); @@ -67,7 +68,7 @@ export class MotaRenderer extends Container { * 渲染游戏画面 */ render() { - console.time(); + // console.time(); const { canvas, ctx } = this.target; const camera = this.camera; this.emit('beforeRender'); @@ -94,7 +95,7 @@ export class MotaRenderer extends Container { }); }); this.emit('afterRender'); - console.timeEnd(); + // console.timeEnd(); } update(item?: RenderItem) { @@ -152,11 +153,20 @@ Mota.require('var', 'hook').once('reset', () => { const binder = new LayerGroupFloorBinder(); const damage = new FloorDamageExtends(); + const hero = new HeroRenderer(); layer.extends(binder); layer.extends(damage); + layer.getLayer('event')?.extends(hero); binder.bindThis(); render.appendChild(layer); + layer.requestAfterFrame(() => { + hero.setImage(core.material.images.images['hero2.png']); + }); + layer.delegateTicker(() => { + hero.turn(); + }, 10000); + camera.move(240, 240); render.update(); diff --git a/src/game/state/hero.ts b/src/game/state/hero.ts index 435d57e..13d59ff 100644 --- a/src/game/state/hero.ts +++ b/src/game/state/hero.ts @@ -365,6 +365,7 @@ export class Hero x: number; y: number; floorId: FloorIds; + dir: Dir2; readonly items: Map, number> = new Map(); readonly id: string; @@ -384,6 +385,7 @@ export class Hero this.y = y; this.floorId = floorId; this.state = state; + this.dir = 'down'; } /** diff --git a/src/types/control.d.ts b/src/types/control.d.ts index b6b1d48..6fa5d7c 100644 --- a/src/types/control.d.ts +++ b/src/types/control.d.ts @@ -294,12 +294,14 @@ interface Control { setAutomaticRoute(destX: number, destY: number, stepPostfix: Loc[]): void; /** + * @deprecated * 连续行走 * @param steps 压缩的步伐数组,每项表示朝某方向走多少步 */ setAutoHeroMove(steps: CompressedStep[]): void; /** + * @deprecated * 设置行走的效果动画 */ setHeroMoveInterval(callback?: () => any): void; @@ -310,6 +312,7 @@ interface Control { moveOneStep(callback?: () => any): void; /** + * @deprecated * 尝试前进一步,如果面前不可被踏入就会直接触发该点事件 * @example core.moveAction(core.doAction); // 尝试前进一步,然后继续事件处理 * @param callback 走一步后的回调函数 @@ -317,6 +320,7 @@ interface Control { moveAction(callback?: () => void): void; /** + * @deprecated * 连续前进,不撞南墙不回头 * @example core.moveHero(); // 连续前进 * @param direction 移动的方向,不设置就是勇士当前的方向 @@ -325,11 +329,13 @@ interface Control { moveHero(direction?: Dir, callback?: () => void): void; /** + * @deprecated * 当前是否正在移动 */ isMoving(): boolean; /** + * @deprecated * 停止勇士的一切行动并等待勇士停下 * @example core.waitHeroToStop(core.vibrate); // 等待勇士停下,然后视野左右抖动1秒 * @param callback 勇士停止后的回调函数 @@ -337,6 +343,7 @@ interface Control { waitHeroToStop(callback?: () => void): void; /** + * @deprecated * 主角转向并计入录像,不会导致跟随者聚集,会导致视野重置到以主角为中心 * @example core.turnHero(); // 主角顺时针旋转,即单击主角或按下Z键的效果 * @param direction 主角的新朝向,可为up, down, left, right, :left, :right, :back七种之一,不填视为:right @@ -360,6 +367,7 @@ interface Control { tryMoveDirectly(destX: number, destY: number): boolean; /** + * @deprecated * 绘制主角和跟随者并重置视野到以主角为中心 * @example core.drawHero(); // 原地绘制主角的静止帧 * @param status 绘制状态 @@ -373,6 +381,7 @@ interface Control { ): void; /** + * @deprecated * 改变勇士的不透明度 * @param opacity 要设置成的不透明度 * @param moveMode 动画的缓动模式