diff --git a/idea.md b/idea.md index 9df705b..bc29949 100644 --- a/idea.md +++ b/idea.md @@ -116,3 +116,4 @@ dam4.png ---- 存档 59 [] 自定义状态栏,通过申请空间进行布局 [x] 复写 api,rewrite() [x] 对 vnode 进行简单的包装,提供出显示文字、显示图片等 api 以及修改 css 的 api +[] mapDamage 注册 diff --git a/public/project/items.js b/public/project/items.js index 72d7201..d8733de 100644 --- a/public/project/items.js +++ b/public/project/items.js @@ -1233,7 +1233,7 @@ var items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a = "cls": "constants", "name": "成就", "canUseItemEffect": "true", - "useItemEffect": "Mota.require('var', 'mainUi')open('achievement');", + "useItemEffect": "Mota.require('var', 'mainUi').open('achievement');", "text": "可以查看成就" }, "I662": { diff --git a/src/core/render/item.ts b/src/core/render/item.ts index 5e354f6..56338a5 100644 --- a/src/core/render/item.ts +++ b/src/core/render/item.ts @@ -103,6 +103,26 @@ export interface IRenderChildable { sortChildren(): void; } +interface IRenderFrame { + /** + * 在下一帧渲染之前执行函数,常用于渲染前数据更新,理论上不应当用于渲染,不保证运行顺序 + * @param fn 执行的函数 + */ + requestBeforeFrame(fn: () => void): void; + + /** + * 在下一帧渲染之后执行函数,理论上不应当用于渲染,不保证运行顺序 + * @param fn 执行的函数 + */ + requestAfterFrame(fn: () => void): void; + + /** + * 在下一帧渲染时执行函数,理论上应当只用于渲染(即{@link RenderItem.update}方法),且不保证运行顺序 + * @param fn 执行的函数 + */ + requestRenderFrame(fn: () => void): void; +} + interface RenderItemEvent { beforeUpdate: (item?: RenderItem) => void; afterUpdate: (item?: RenderItem) => void; @@ -110,9 +130,18 @@ interface RenderItemEvent { afterRender: () => void; } +const beforeFrame: (() => void)[] = []; +const afterFrame: (() => void)[] = []; +const renderFrame: (() => void)[] = []; + export abstract class RenderItem extends EventEmitter - implements IRenderCache, IRenderUpdater, IRenderAnchor, IRenderConfig + implements + IRenderCache, + IRenderUpdater, + IRenderAnchor, + IRenderConfig, + IRenderFrame { /** 渲染的全局ticker */ static ticker: Ticker = new Ticker(); @@ -227,6 +256,18 @@ export abstract class RenderItem this.parent?.sortChildren?.(); } + requestBeforeFrame(fn: () => void): void { + beforeFrame.push(fn); + } + + requestAfterFrame(fn: () => void): void { + afterFrame.push(fn); + } + + requestRenderFrame(fn: () => void): void { + renderFrame.push(fn); + } + /** * 隐藏这个元素 */ @@ -261,6 +302,24 @@ export abstract class RenderItem } } +RenderItem.ticker.add(() => { + if (beforeFrame.length > 0) { + const toEmit = beforeFrame.slice(); + beforeFrame.splice(0); + toEmit.forEach(v => v()); + } + if (renderFrame.length > 0) { + const toEmit = renderFrame.slice(); + renderFrame.splice(0); + toEmit.forEach(v => v()); + } + if (afterFrame.length > 0) { + const toEmit = afterFrame.slice(); + afterFrame.splice(0); + toEmit.forEach(v => v()); + } +}); + interface RenderEvent { animateFrame: (frame: number, time: number) => void; } diff --git a/src/core/render/preset/block.ts b/src/core/render/preset/block.ts index 983bae5..31209f5 100644 --- a/src/core/render/preset/block.ts +++ b/src/core/render/preset/block.ts @@ -16,6 +16,12 @@ interface BlockData { restHeight: number; } +/** + * 简单分块缓存类,内容包含元素与分块两种,其中元素是最小单元,分块是缓存单元。 + * 拿楼层举例,假如我将楼层按照13x13划分缓存,那么元素就是每个图块,而分块就是这13x13的缓存分块。 + * 为方便区分,在相关函数的注释最后,都会有`xx -> yy`的说明, + * 其中xx说明传入的数据是元素还是分块的数据,而yy表示其返回值或转换为的值 + */ export class BlockCacher extends EventEmitter { /** 区域宽度 */ width: number; @@ -47,6 +53,7 @@ export class BlockCacher extends EventEmitter { this.height = height; this.blockSize = size; this.cacheDepth = depth; + this.split(); } /** @@ -103,7 +110,7 @@ export class BlockCacher extends EventEmitter { } /** - * 清除指定块的索引 + * 清除指定块的索引(分块->void) * @param index 要清除的缓存索引 * @param deep 清除哪些深度的缓存,至多31位二进制数,例如填0b111就是清除前三层的索引 */ @@ -117,7 +124,7 @@ export class BlockCacher extends EventEmitter { } /** - * 清空指定索引的缓存,与 {@link clearCache} 不同的是,这里会直接清空对应索引的缓存,而不是指定分块的缓存 + * 清空指定索引的缓存,与 {@link clearCache} 不同的是,这里会直接清空对应索引的缓存,而不是指定分块的缓存(分块->void) */ clearCacheByIndex(index: number) { this.cache.delete(index); @@ -131,14 +138,14 @@ export class BlockCacher extends EventEmitter { } /** - * 根据分块的横纵坐标获取其索引 + * 根据分块的横纵坐标获取其索引(分块->分块) */ getIndex(x: number, y: number) { return x + y * this.blockData.width; } /** - * 根据元素位置获取分块索引(注意是单个元素的位置,而不是分块的位置) + * 根据元素位置获取分块索引(注意是单个元素的位置,而不是分块的位置)(元素->分块) */ getIndexByLoc(x: number, y: number) { return this.getIndex( @@ -148,7 +155,7 @@ export class BlockCacher extends EventEmitter { } /** - * 根据块的索引获取其位置 + * 根据块的索引获取其位置(分块->分块) */ getBlockXYByIndex(index: number): LocArr { const width = this.blockData.width; @@ -156,29 +163,47 @@ export class BlockCacher extends EventEmitter { } /** - * 获取一个元素位置所在的分块位置(即使它不在任何分块内) + * 获取一个元素位置所在的分块位置(即使它不在任何分块内)(元素->分块) */ getBlockXY(x: number, y: number): LocArr { return [Math.floor(x / this.blockSize), Math.floor(y / this.blockSize)]; } /** - * 根据分块坐标与deep获取一个分块的精确索引 + * 根据分块坐标与deep获取一个分块的精确索引(分块->分块) */ getPreciseIndex(x: number, y: number, deep: number) { return (x + y * this.blockSize) * this.cacheDepth + deep; } /** - * 根据元素坐标及deep获取元素所在块的精确索引 + * 根据元素坐标及deep获取元素所在块的精确索引(元素->分块) */ getPreciseIndexByLoc(x: number, y: number, deep: number) { return this.getPreciseIndex(...this.getBlockXY(x, y), deep); } /** - * 更新一个区域内的缓存 + * 更新指定元素区域内的缓存(注意坐标是元素坐标,而非分块坐标)(元素->分块) * @param deep 缓存清除深度,默认全部清空 + * @returns 更新区域内的所有有关分块索引 + */ + updateElementArea( + x: number, + y: number, + width: number, + height: number, + deep: number = 2 ** 31 - 1 + ) { + const [bx, by] = this.getBlockXY(x, y); + const [ex, ey] = this.getBlockXY(x + width, y + height); + return this.updateArea(bx, by, ex - bx, ey - by, deep); + } + + /** + * 更新指定分块区域内的缓存(注意坐标是分块坐标,而非元素坐标)(分块->分块) + * @param deep 缓存清除深度,默认全部清空 + * @returns 更新区域内的所有分块索引 */ updateArea( x: number, @@ -187,13 +212,15 @@ export class BlockCacher extends EventEmitter { height: number, deep: number = 2 ** 31 - 1 ) { - this.getIndexOf(x, y, width, height).forEach(v => { + const blocks = this.getIndexOf(x, y, width, height); + blocks.forEach(v => { this.clearCache(v, deep); }); + return blocks; } /** - * 获取一个区域内包含的所有分块索引 + * 传入分块坐标与范围,获取该区域内包含的所有分块索引(分块->分块) */ getIndexOf(x: number, y: number, width: number, height: number) { const res = new Set(); @@ -204,12 +231,43 @@ export class BlockCacher extends EventEmitter { for (let nx = sx; nx < ex; nx++) { for (let ny = sy; ny < ey; ny++) { - const index = this.getIndexByLoc(nx, ny); - if (res.has(index)) continue; + const index = this.getIndex(nx, ny); res.add(index); } } return res; } + + /** + * 传入元素坐标与范围,获取该区域内包含的所有分块索引(元素->分块) + */ + getIndexOfElement(x: number, y: number, width: number, height: number) { + const [bx, by] = this.getBlockXY(x, y); + const [ex, ey] = this.getBlockXY(x + width, y + height); + return this.getIndexOf(bx, by, ex - bx, ey - by); + } + + /** + * 根据分块索引,获取这个分块所在区域的元素矩形范围(左上角横纵坐标及右下角横纵坐标)(分块->元素) + * @param block 分块索引 + */ + getRectOfIndex(block: number) { + const [x, y] = this.getBlockXYByIndex(block); + return this.getRectOfBlockXY(x, y); + } + + /** + * 根据分块坐标,获取这个分块所在区域的元素矩形范围(左上角横纵坐标及右下角横纵坐标)(分块->元素) + * @param x 分块横坐标 + * @param y 分块纵坐标 + */ + getRectOfBlockXY(x: number, y: number) { + return [ + x * this.blockSize, + y * this.blockSize, + (x + 1) * this.blockSize, + (y + 1) * this.blockSize + ]; + } } diff --git a/src/core/render/preset/damage.ts b/src/core/render/preset/damage.ts new file mode 100644 index 0000000..b4c965a --- /dev/null +++ b/src/core/render/preset/damage.ts @@ -0,0 +1,443 @@ +import { logger } from '@/core/common/logger'; +import { LayerGroupFloorBinder } from './floor'; +import { + calNeedRenderOf, + ILayerGroupRenderExtends, + Layer, + LayerGroup +} from './layer'; +import { Sprite } from '../sprite'; +import { BlockCacher } from './block'; +import type { + DamageEnemy, + EnemyCollection, + MapDamage +} from '@/game/enemy/damage'; +import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; +import { Camera } from '../camera'; +import { isNil } from 'lodash-es'; +import { getDamageColor } from '@/plugin/utils'; +import { transformCanvas } from '../item'; + +const ensureFloorDamage = Mota.require('fn', 'ensureFloorDamage'); + +export class FloorDamageExtends implements ILayerGroupRenderExtends { + id: string = 'floor-damage'; + + floorBinder!: LayerGroupFloorBinder; + group!: LayerGroup; + sprite!: Damage; + + /** + * 立刻刷新伤害渲染 + */ + update(floor: FloorIds) { + if (!this.sprite) return; + const map = core.status.maps[floor]; + this.sprite.setMapSize(map.width, map.height); + ensureFloorDamage(floor); + const enemy = core.status.maps[floor].enemy; + console.log(enemy); + + this.sprite.updateCollection(enemy); + } + + /** + * 创建显伤层 + */ + private create() { + if (this.sprite) return; + const sprite = new Damage(); + this.group.appendChild(sprite); + this.sprite = sprite; + } + + private onUpdate = (floor: FloorIds) => { + this.update(floor); + }; + + private onSetBlock = (x: number, y: number, floor: FloorIds) => { + if (floor !== this.sprite.enemy?.floorId) return; + this.sprite.updateEnemyOn(x, y); + }; + + /** + * 进行楼层更新监听 + */ + private listen() { + this.floorBinder.on('update', this.onUpdate); + this.floorBinder.on('setBlock', this.onSetBlock); + } + + awake(group: LayerGroup): void { + group.requestBeforeFrame(() => { + const ex = group.getExtends('floor-binder'); + if (ex instanceof LayerGroupFloorBinder) { + this.floorBinder = ex; + this.group = group; + this.create(); + this.listen(); + } else { + logger.warn( + 17, + `Floor-damage extends needs 'floor-binder' extends as dependency.` + ); + group.removeExtends('floor-damage'); + } + }); + } + + onDestroy(group: LayerGroup): void { + this.floorBinder.off('update', this.onUpdate); + this.floorBinder.off('setBlock', this.onSetBlock); + } +} + +interface DamageRenderable { + x: number; + y: number; + align: CanvasTextAlign; + baseline: CanvasTextBaseline; + text: string; + color: CanvasStyle; + font?: string; + stroke?: CanvasStyle; + strokeWidth?: number; +} + +export class Damage extends Sprite { + mapWidth: number = 0; + mapHeight: number = 0; + + block: BlockCacher; + /** 键表示分块索引,值表示在这个分块上的渲染信息(当然实际渲染位置可以不在这个分块上) */ + renderable: Map> = new Map(); + + /** 当前渲染怪物列表 */ + enemy?: EnemyCollection; + /** 每个分块中包含的怪物集合 */ + blockData: Map> = new Map(); + /** 单元格大小 */ + cellSize: number = 32; + + /** 伤害渲染层 */ + damageMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); + /** 默认伤害字体 */ + font: string = "14px 'normal'"; + /** 默认描边样式,当伤害文字不存在描边属性时会使用此属性 */ + strokeStyle: CanvasStyle = '#000'; + /** 默认描边宽度 */ + strokeWidth: number = 2; + + /** 单个点的懒更新 */ + private needUpdateBlock: boolean = false; + /** 要懒更新的所有分块 */ + private needUpdateBlocks: Set = new Set(); + + constructor() { + super(); + + this.block = new BlockCacher(0, 0, core._WIDTH_, 1); + this.type = 'absolute'; + 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(); + }); + } + + /** + * 设置地图大小,后面应紧跟更新怪物列表 + */ + setMapSize(width: number, height: number) { + this.mapWidth = width; + this.mapHeight = height; + this.enemy = void 0; + this.blockData.clear(); + this.renderable.clear(); + this.block.size(width, height); + + // 预留blockData + const w = this.block.blockData.width; + const h = this.block.blockData.height; + const num = w * h; + for (let i = 0; i < num; i++) { + this.blockData.set(i, new Map()); + this.renderable.set(i, new Set()); + } + } + + /** + * 更新怪物列表。更新后,{@link Damage.enemy} 会丢失原来的怪物列表引用,换为传入的列表引用 + * @param enemy 怪物列表 + */ + updateCollection(enemy: EnemyCollection) { + if (this.enemy === enemy) return; + this.enemy = enemy; + this.blockData.forEach(v => v.clear()); + this.renderable.forEach(v => v.clear()); + this.block.clearAllCache(); + + enemy.list.forEach(v => { + if (isNil(v.x) || isNil(v.y)) return; + const index = this.block.getIndexByLoc(v.x, v.y); + this.blockData.get(index)?.set(v.y * this.mapWidth + v.x, v); + }); + this.updateBlocks(); + + this.update(this); + } + + /** + * 更新指定矩形区域内的渲染信息 + * @param x 左上角横坐标 + * @param y 左上角纵坐标 + * @param width 宽度 + * @param height 高度 + */ + updateRenderable(x: number, y: number, width: number, height: number) { + this.updateBlocks(this.block.updateElementArea(x, y, width, height)); + } + + /** + * 更新指定分块 + * @param blocks 要更新的分块集合 + */ + updateBlocks(blocks?: Set) { + if (blocks) { + blocks.forEach(v => this.updateBlock(v)); + } else { + this.blockData.forEach((_, k) => this.updateBlock(k, false)); + this.extractAllMapDamage(); + } + this.update(this); + } + + /** + * 更新指定位置的怪物信息 + */ + updateEnemyOn(x: number, y: number) { + const enemy = this.enemy?.get(x, y); + const block = this.block.getIndexByLoc(x, y); + const data = this.blockData.get(block); + const index = x + y * this.mapWidth; + if (!data) return; + if (!enemy) { + data.delete(index); + } else { + data.set(index, enemy); + } + this.update(this); + + // 渲染懒更新,优化性能表现 + if (!this.needUpdateBlock) { + this.requestBeforeFrame(() => { + this.needUpdateBlock = false; + this.needUpdateBlocks.forEach(v => this.updateBlock(v, false)); + // todo: 阻击夹域等地图伤害检测是否必要更新,例如不包含阻击夹域的怪就不必要更新这个怪物信息 + this.extractAllMapDamage(); + }); + this.needUpdateBlock = true; + } + } + + /** + * 更新单个分块 + * @param block 更新的分块 + * @param map 是否更新地图伤害 + */ + private updateBlock(block: number, map: boolean = true) { + const data = this.blockData.get(block); + if (!data) return; + this.block.clearCache(block, 1); + const renderable = this.renderable.get(block)!; + data.forEach(v => this.extract(v, renderable)); + if (map) this.extractMapDamage(block, renderable); + } + + /** + * 将怪物解析为renderable的伤害 + * @param enemy 怪物 + * @param block 怪物所属分块 + */ + private extract(enemy: DamageEnemy, block: Set) { + if (enemy.progress !== 4) return; + const x = enemy.x!; + const y = enemy.y!; + const { damage } = enemy.calDamage(); + const cri = enemy.calCritical(1)[0]?.atkDelta ?? Infinity; + const dam1: DamageRenderable = { + align: 'left', + baseline: 'alphabetic', + text: isFinite(damage) ? core.formatBigNumber(damage, true) : '???', + color: getDamageColor(damage), + 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 + }; + block.add(dam1).add(dam2); + } + + /** + * 解析指定分块的地图伤害 + * @param block 分块索引 + */ + private extractMapDamage(block: number, renderable: Set) { + if (!this.enemy) return; + const damage = this.enemy.mapDamage; + const [sx, sy, ex, ey] = this.block.getRectOfIndex(block); + for (let x = sx; x < ex; x++) { + for (let y = sy; y < ey; y++) { + const loc = `${x},${y}`; + const dam = damage[loc]; + if (!dam) continue; + this.pushMapDamage(x, y, renderable, dam); + } + } + } + + /** + * 解析所有地图伤害 + */ + private extractAllMapDamage() { + // todo: 测试性能,这样真的会更快吗?或许能更好的优化?或者是根本不需要这个函数? + if (!this.enemy) return; + for (const [loc, enemy] of Object.entries(this.enemy.mapDamage)) { + const [sx, sy] = loc.split(','); + const x = Number(sx); + const y = Number(sy); + const block = this.renderable.get(this.block.getIndexByLoc(x, y))!; + this.pushMapDamage(x, y, block, enemy); + } + } + + private pushMapDamage( + x: number, + y: number, + block: Set, + dam: MapDamage + ) { + // todo: 这个应当可以自定义,通过地图伤害注册实现 + 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 + }; + block.add(mapDam); + } + + /** + * 计算需要渲染哪些块 + */ + 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); + } + } + + /** + * 渲染伤害层 + * @param camera 摄像机 + */ + renderDamage(camera: Camera) { + const { ctx } = this.damageMap; + ctx.save(); + transformCanvas(this.damageMap, camera, true); + + const { res: render } = this.calNeedRender(camera); + 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) { + ctx.drawImage(cache, px, py, size, size); + return; + } + + console.time('damage1'); + // 否则依次渲染并写入缓存 + const temp = new MotaOffscreenCanvas2D(); + temp.setHD(true); + temp.setAntiAliasing(true); + temp.withGameScale(true); + temp.size(size, size); + const { ctx: ct } = temp; + console.timeEnd('damage1'); + + console.time('damage2'); + const render = this.renderable.get(v); + render?.forEach(v => { + if (!v) return; + ct.fillStyle = v.color; + ct.textAlign = v.align; + ct.textBaseline = v.baseline; + ct.font = v.font ?? this.font; + ct.strokeStyle = v.stroke ?? this.strokeStyle; + ct.lineWidth = v.strokeWidth ?? this.strokeWidth; + ct.strokeText(v.text, v.x, v.y); + ct.fillText(v.text, v.x, v.y); + }); + console.timeEnd('damage2'); + + console.time('damage3'); + ctx.drawImage(temp.canvas, px, py, size, size); + block.cache.set(v, temp.canvas); + console.timeEnd('damage3'); + }); + ctx.restore(); + } +} diff --git a/src/core/render/preset/floor.ts b/src/core/render/preset/floor.ts new file mode 100644 index 0000000..0d4e857 --- /dev/null +++ b/src/core/render/preset/floor.ts @@ -0,0 +1,269 @@ +import EventEmitter from 'eventemitter3'; +import { + FloorLayer, + ILayerGroupRenderExtends, + ILayerRenderExtends, + Layer, + LayerGroup +} from './layer'; +import { texture } from '../cache'; + +const hook = Mota.require('var', 'hook'); + +hook.on('setBlock', (x, y, floor, block) => { + const isNow = floor === core.status.floorId; + LayerGroupFloorBinder.activedBinder.forEach(v => { + if (floor === v.floor || (isNow && v.bindThisFloor)) { + v.setBlock('event', block, x, y); + } + }); + LayerFloorBinder.listenedBinder.forEach(v => { + if (v.layer.layer === 'event') { + if (v.floor === floor || (isNow && v.bindThisFloor)) { + v.setBlock(block, x, y); + } + } + }); +}); +hook.on('changingFloor', floor => { + // 潜在隐患:如果putRenderData改成异步,那么会变成两帧后才能真正刷新并渲染 + // 考虑到楼层转换一般不会同时执行很多次,因此这里改为立刻更新 + LayerGroupFloorBinder.activedBinder.forEach(v => { + if (v.bindThisFloor) v.updateBindData(); + }); + LayerFloorBinder.listenedBinder.forEach(v => { + if (v.bindThisFloor) v.updateBindData(); + }); +}); + +interface LayerGroupBinderEvent { + update: [floor: FloorIds]; + setBlock: [x: number, y: number, floor: FloorIds, block: AllNumbers]; +} + +/** + * 楼层绑定拓展,用于LayerGroup,将楼层数据传输到渲染系统。 + * 添加后,会自动在LayerGroup包含的子Layer上添加LayerFloorBinder拓展,用于后续处理。 + * 当移除这个拓展时,其附属的所有子拓展也会一并被移除。 + */ +export class LayerGroupFloorBinder + extends EventEmitter + implements ILayerGroupRenderExtends +{ + id: string = 'floor-binder'; + + bindThisFloor: boolean = true; + floor?: FloorIds; + group!: LayerGroup; + + /** 附属的子LayerFloorBinder拓展 */ + layerBinders: Set = new Set(); + + private needUpdate: boolean = false; + + static activedBinder: Set = new Set(); + + /** + * 绑定楼层为当前楼层,并跟随变化 + */ + bindThis() { + this.floor = void 0; + this.bindThisFloor = true; + this.updateBind(); + } + + /** + * 绑定楼层为指定楼层 + * @param floorId 楼层id + */ + bindFloor(floorId: FloorIds) { + this.bindThisFloor = false; + this.floor = floorId; + this.updateBind(); + } + + /** + * 在下一帧进行绑定数据更新 + */ + updateBind() { + if (this.needUpdate) return; + this.needUpdate = true; + this.group.requestBeforeFrame(() => { + this.needUpdate = false; + this.updateBindData(); + }); + } + + /** + * 立刻进行数据绑定更新 + */ + updateBindData() { + this.layerBinders.forEach(v => { + v.updateBindData(); + }); + + const floor = this.bindThisFloor ? core.status.floorId : this.floor!; + this.emit('update', floor); + } + + /** + * 设置图块 + */ + setBlock(layer: FloorLayer, block: AllNumbers, x: number, y: number) { + const ex = this.group + .getLayer(layer) + ?.getExtends('floor-binder') as LayerFloorBinder; + if (!ex) return; + ex.setBlock(block, x, y); + + const floor = this.bindThisFloor ? core.status.floorId : this.floor!; + this.emit('setBlock', x, y, floor, block); + } + + private checkLayerExtends(layer: Layer) { + const ex = layer.getExtends('floor-binder'); + if (!ex) { + const extend = new LayerFloorBinder(this); + layer.extends(extend); + this.layerBinders.add(extend); + } else { + if (ex instanceof LayerFloorBinder) { + ex.setParent(this); + this.layerBinders.add(ex); + } + } + } + + awake(group: LayerGroup) { + this.group = group; + for (const layer of group.layers.values()) { + this.checkLayerExtends(layer); + } + LayerGroupFloorBinder.activedBinder.add(this); + } + + onLayerAdd(group: LayerGroup, layer: Layer): void { + this.checkLayerExtends(layer); + } + + onDestroy(group: LayerGroup) { + LayerGroupFloorBinder.activedBinder.delete(this); + group.layers.forEach(v => { + v.removeExtends('floor-binder'); + }); + this.removeAllListeners(); + } +} + +/** + * 楼层绑定拓展,用于Layer的楼层渲染。 + * 注意,如果目标Layer是LayerGroup的子元素,那么会自动检测父元素是否包含LayerGroupFloorBinder拓展, + * 如果包含,那么会自动将此拓展附加至父元素的拓展。当父元素的拓展被移除时,此拓展也会一并被移除。 + */ +export class LayerFloorBinder implements ILayerRenderExtends { + id: string = 'floor-binder'; + + parent?: LayerGroupFloorBinder; + layer!: Layer; + bindThisFloor: boolean = true; + floor?: FloorIds; + + static listenedBinder: Set = new Set(); + + private needUpdate: boolean = false; + + constructor(parent?: LayerGroupFloorBinder) { + this.parent = parent; + } + + /** + * 绑定楼层为当前楼层,并跟随变化 + */ + bindThis() { + this.floor = void 0; + this.bindThisFloor = true; + } + + /** + * 绑定楼层为指定楼层 + * @param floorId 楼层id + */ + bindFloor(floorId: FloorIds) { + this.bindThisFloor = false; + this.floor = floorId; + } + + /** + * 设置这个拓展附属至的父拓展(LayerGroupFloorBinder拓展) + * @param parent 父拓展 + */ + setParent(parent?: LayerGroupFloorBinder) { + this.parent = parent; + this.checkListen(); + } + + private checkListen() { + if (this.parent) LayerFloorBinder.listenedBinder.delete(this); + else LayerFloorBinder.listenedBinder.add(this); + } + + /** + * 在下一帧进行绑定数据更新 + */ + updateBind() { + if (this.needUpdate) return; + this.needUpdate = true; + this.layer.requestBeforeFrame(() => { + this.needUpdate = false; + this.updateBindData(); + }); + } + + /** + * 设置图块 + */ + setBlock(block: AllNumbers, x: number, y: number) { + this.layer.putRenderData([block], 1, x, y); + } + + /** + * 立刻更新绑定数据,而非下一帧 + */ + updateBindData() { + const floor = this.bindThisFloor ? core.status.floorId : this.floor; + if (!floor) return; + core.extractBlocks(floor); + const map = core.status.maps[floor]; + this.layer.setMapSize(map.width, map.height); + if (this.layer.layer === 'event') { + const m = map.map; + this.layer.putRenderData(m.flat(), map.width, 0, 0); + } else { + const m = core.maps._getBgFgMapArray(this.layer.layer!, floor); + this.layer.putRenderData(m.flat(), map.width, 0, 0); + } + if (this.layer.layer === 'bg') { + // 别忘了背景图块 + this.layer.setBackground(texture.idNumberMap[map.defaultGround]); + } + } + + awake(layer: Layer) { + this.layer = layer; + if (!this.parent) { + const group = layer.parent; + if (group instanceof LayerGroup) { + const ex = group.getExtends('floor-binder'); + if (ex instanceof LayerGroupFloorBinder) { + this.parent = ex; + } + } + } + this.checkListen(); + } + + onDestroy(layer: Layer) { + LayerFloorBinder.listenedBinder.delete(this); + this.parent?.layerBinders.delete(this); + } +} diff --git a/src/core/render/preset/hero.ts b/src/core/render/preset/hero.ts new file mode 100644 index 0000000..4ed898f --- /dev/null +++ b/src/core/render/preset/hero.ts @@ -0,0 +1,3 @@ +import { ILayerRenderExtends } from './layer'; + +export class HeroRenderer implements ILayerRenderExtends {} diff --git a/src/core/render/preset/layer.ts b/src/core/render/preset/layer.ts index 69f73c4..817d4aa 100644 --- a/src/core/render/preset/layer.ts +++ b/src/core/render/preset/layer.ts @@ -8,17 +8,84 @@ import { logger } from '@/core/common/logger'; import { AutotileRenderable, RenderableData, texture } from '../cache'; import { glMatrix } from 'gl-matrix'; import { BlockCacher } from './block'; -import { isNil } from 'lodash-es'; -import { getDamageColor } from '@/plugin/utils'; -import type { DamageEnemy, EnemyCollection } from '@/game/enemy/damage'; -type FloorLayer = 'bg' | 'bg2' | 'event' | 'fg' | 'fg2'; -type LayerGroupPreset = 'defaults' | 'noDamage'; +export interface ILayerGroupRenderExtends { + /** 拓展的唯一标识符 */ + readonly id: string; + + /** + * 当拓展被激活时执行的函数(一般就是拓展加载至目标LayerGroup实例时立刻执行) + * @param group 目标LayerGroup实例 + */ + awake?(group: LayerGroup): void; + + /** + * 当一个Layer层级被添加时执行的函数 + * @param group 目标LayerGroup实例 + * @param layer 添加的Layer层实例 + */ + onLayerAdd?(group: LayerGroup, layer: Layer): void; + + /** + * 当一个Layer层级被移除时执行的函数 + * @param group 目标LayerGroup实例 + * @param layer 移除的Layer层实例 + */ + onLayerRemove?(group: LayerGroup, layer: Layer): void; + + /** + * 当一个Layer层级从显示到隐藏的状态切换时执行的函数 + * @param group 目标LayerGroup实例 + * @param layer 隐藏的Layer层实例 + */ + onLayerHide?(group: LayerGroup, layer: Layer): void; + + /** + * 当一个Layer层级从隐藏到显示状态切换时执行的函数 + * @param group 目标LayerGroup实例 + * @param layer 显示的Layer层实例 + */ + onLayerShow?(group: LayerGroup, layer: Layer): void; + + /** + * 当执行 {@link LayerGroup.emptyLayer} 时执行的函数,即清空所有挂载的Layer时执行的函数 + * @param group 目标LayerGroup实例 + */ + onEmptyLayer?(group: LayerGroup): void; + + /** + * 当渲染数据更新时执行的函数 + * @param group 目标LayerGroup实例 + * @param floor 数据更新的楼层 + */ + // onDataUpdate?: (group: LayerGroup, floor: FloorIds) => void; + + /** + * 当帧动画更新时执行的函数,例如从第一帧变成第二帧时 + * @param group 目标LayerGroup实例 + * @param frame 当前帧数 + */ + onFrameUpdate?(group: LayerGroup, frame: number): void; + + /** + * 当绑定楼层时执行的函数 + * @param group 目标LayerGroup实例 + * @param floor 绑定的楼层id,为空时表示绑定为当前楼层 + */ + // onBindFloor?: (group: LayerGroup, floor?: FloorIds) => void; + + /** + * 当拓展被取消挂载时执行的函数(LayerGroup被销毁,拓展被移除等) + * @param group 目标LayerGroup实例 + */ + onDestroy?(group: LayerGroup): void; +} + +export type FloorLayer = 'bg' | 'bg2' | 'event' | 'fg' | 'fg2'; -const layers: FloorLayer[] = ['bg', 'bg2', 'event', 'fg', 'fg2']; export class LayerGroup extends Container { /** 地图组列表 */ - static list: Set = new Set(); + // static list: Set = new Set(); cellSize: number = 32; blockSize: number = core._WIDTH_; @@ -28,13 +95,14 @@ export class LayerGroup extends Container { /** 是否绑定了当前层 */ bindThisFloor: boolean = false; /** 伤害显示层 */ - damage?: Damage; + // damage?: Damage; /** 地图显示层 */ - layers: Set = new Set(); + layers: Map = new Map(); private needRender?: NeedRenderData; + private extend: Map = new Map(); - constructor(floor?: FloorIds) { + constructor() { super(); this.setHD(true); @@ -45,9 +113,39 @@ export class LayerGroup extends Container { this.releaseNeedRender(); }); - this.usePreset('defaults'); - LayerGroup.list.add(this); - this.bindFloor(floor); + // this.usePreset('defaults'); + // LayerGroup.list.add(this); + // this.bindFloor(floor); + } + + /** + * 添加渲染拓展,可以将渲染拓展理解为一类插件,通过指定的函数在对应时刻执行一些函数, + * 来达到执行自己想要的功能的效果。例如样板自带的勇士渲染、伤害渲染等都由此实现。 + * 具体能干什么参考 {@link ILayerGroupRenderExtends} + * @param ex 渲染拓展对象 + */ + extends(ex: ILayerGroupRenderExtends) { + this.extend.set(ex.id, ex); + ex.awake?.(this); + } + + /** + * 移除一个渲染拓展 + * @param id 要移除的拓展 + */ + removeExtends(id: string) { + const ex = this.extend.get(id); + if (!ex) return; + this.extend.delete(id); + ex.onDestroy?.(this); + } + + /** + * 获取一个已装载的拓展 + * @param id 拓展id + */ + getExtends(id: string) { + return this.extend.get(id); } /** @@ -59,7 +157,7 @@ export class LayerGroup extends Container { this.layers.forEach(v => { v.block.setBlockSize(size); }); - this.damage?.block.setBlockSize(size); + // this.damage?.block.setBlockSize(size); } /** @@ -74,35 +172,39 @@ export class LayerGroup extends Container { * 使用预设显示模式,注意切换后所有的旧Layer实例会被摧毁 * @param preset 预设名称 */ - usePreset(preset: LayerGroupPreset) { - this.emptyLayer(); + // usePreset(preset: LayerGroupPreset) { + // this.emptyLayer(); - const child = layers.map((v, i) => { - const layer = new Layer(); - layer.bindLayer(v); - layer.setZIndex(i * 10); - this.layers.add(layer); - return layer; - }); - this.appendChild(...child); - if (preset === 'defaults') { - const damage = new Damage(this.floorId); - this.appendChild(damage); - this.damage = damage; - damage.setZIndex(60); - } - } + // 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); + // } + // } /** * 清空所有层 */ emptyLayer() { - this.removeChild(...this.layers); - if (this.damage) this.removeChild(this.damage); - this.damage?.destroy(); + this.removeChild(...this.layers.values()); + // if (this.damage) this.removeChild(this.damage); + // this.damage?.destroy(); this.layers.forEach(v => v.destroy()); this.layers.clear(); - this.damage = void 0; + // this.damage = void 0; + + for (const ex of this.extend.values()) { + ex.onEmptyLayer?.(this); + } } /** @@ -111,33 +213,43 @@ export class LayerGroup extends Container { */ addLayer(layer: FloorLayer) { const l = new Layer(); - l.bindLayer(layer); - this.layers.add(l); + // l.bindLayer(layer); + l.layer = layer; + if (l.layer) this.layers.set(l.layer, l); this.appendChild(l); + + for (const ex of this.extend.values()) { + ex.onLayerAdd?.(this, l); + } + return l; } /** * 移除指定层 - * @param layer 要移除的层,可以是Layer实例,也可以是字符串。如果是字符串,那么会移除所有符合的层 + * @param layer 要移除的层,可以是Layer实例,也可以是字符串 */ removeLayer(layer: FloorLayer | Layer) { + let ins: Layer | undefined; if (typeof layer === 'string') { - const toRemove: Layer[] = []; - this.layers.forEach(v => { - if (v.layer === layer) { - toRemove.push(v); - } - }); - toRemove.forEach(v => { - v.destroy(); - this.layers.delete(v); - }); - this.removeChild(...toRemove); + const la = this.layers.get(layer); + if (!la) return; + this.removeChild(la); + this.layers.delete(layer); + la.destroy(); + ins = la; } else { - if (this.layers.delete(layer)) { - layer.destroy(); + const arr = [...this.layers]; + const la = arr.find(v => v[1] === layer)?.[0]; + if (la && this.layers.delete(la)) { this.removeChild(layer); + layer.destroy(); + ins = layer; + } + } + if (ins) { + for (const ex of this.extend.values()) { + ex.onLayerRemove?.(this, ins); } } } @@ -147,7 +259,7 @@ export class LayerGroup extends Container { * @param layer 地图层 */ getLayer(layer: FloorLayer) { - return [...this.layers].filter(v => v.layer === layer); + return this.layers.get(layer); } /** @@ -155,9 +267,13 @@ export class LayerGroup extends Container { * @param layer 要隐藏的层 */ hideLayer(layer: FloorLayer) { - this.layers.forEach(v => { - if (v.layer === layer) v.hide(); - }); + const la = this.getLayer(layer); + if (!la) return; + la.hide(); + + for (const ex of this.extend.values()) { + ex.onLayerHide?.(this, la); + } } /** @@ -165,49 +281,53 @@ export class LayerGroup extends Container { * @param layer 要显示的层 */ showLayer(layer: FloorLayer) { - this.layers.forEach(v => { - if (v.layer === layer) v.show(); - }); + const la = this.getLayer(layer); + if (!la) return; + la.show(); + + for (const ex of this.extend.values()) { + ex.onLayerShow?.(this, la); + } } /** * 绑定当前楼层 */ - bindThis() { - this.bindThisFloor = true; - this.updateFloor(); - } + // bindThis() { + // this.bindThisFloor = true; + // this.updateFloor(); + // } /** * 绑定楼层信息 * @param floor 绑定楼层,不填时表示绑定为当前楼层 */ - bindFloor(floor?: FloorIds) { - if (!floor) { - this.bindThisFloor = true; - } else { - this.floorId = floor; - } - this.updateFloor(); - } + // 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); - } + // 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); + // } /** * 缓存计算应该渲染的块 @@ -231,33 +351,33 @@ export class LayerGroup extends Container { /** * 添加伤害显示层,并将显示层返回,如果已经添加,则会返回已经添加的显示层 */ - 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; - } + // 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; - } - } + // 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); - } + // updateDamage(x: number, y: number, width: number, height: number) { + // this.damage?.updateRenderable(x, y, width, height); + // } /** * 更新动画帧 @@ -266,50 +386,58 @@ export class LayerGroup extends Container { this.layers.forEach(v => { v.cache(v.using); }); - this.damage?.cache(this.damage.using); + // this.damage?.cache(this.damage.using); this.update(this); + + for (const ex of this.extend.values()) { + ex.onFrameUpdate?.(this, RenderItem.animatedFrame % 4); + } } destroy(): void { + for (const ex of this.extend.values()) { + ex.onDestroy?.(this); + } super.destroy(); - LayerGroup.list.delete(this); + // LayerGroup.list.delete(this); } } 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); - } - }); -}); +// 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); +// } +// }); +// }); -renderEmits.on('animateFrame', () => { - LayerGroup.list.forEach(v => { - v.updateFrameAnimate(); - }); -}); +// todo: animate frame. +// renderEmits.on('animateFrame', () => { +// LayerGroup.list.forEach(v => { +// v.updateFrameAnimate(); +// }); +// }); -function calNeedRenderOf( +export function calNeedRenderOf( camera: Camera, cell: number, block: BlockCacher @@ -454,7 +582,148 @@ function calNeedRenderOf( return { res, back }; } +export interface ILayerRenderExtends { + /** 拓展的唯一标识符 */ + readonly id: string; + + /** + * 当拓展被激活时执行的函数(一般就是拓展加载至目标Layer实例时立刻执行) + * @param layer 目标Layer实例 + */ + awake?(layer: Layer): void; + + /** + * 当楼层的背景图块被设置时执行的函数 + * @param layer 目标Layer实例 + * @param background 设置为的背景图块数字 + */ + onBackgroundSet?(layer: Layer, background: AllNumbers): void; + + // onBackgroundBind?: (layer: Layer, floorId: FloorIds) => void; + + /** + * 当背景图块图片被生成时执行的函数 + * @param layer 目标Layer实例 + * @param images 生成出的背景图块的单个分块图像,数组是因为背景图块可能是多帧图块 + */ + onBackgroundGenerated?(layer: Layer, images: HTMLCanvasElement[]): void; + + /** + * 当修改渲染数据时执行的函数,参见 {@link Layer.putRenderData} + * @param layer 目标Layer实例 + * @param data 扁平化的数据信息 + * @param width 数据宽度 + * @param x 数据左上角横坐标 + * @param y 数据左上角纵坐标 + * @param calAutotile 是否重新计算自动元件的连接情况 + */ + onDataPut?( + layer: Layer, + data: number[], + width: number, + x: number, + y: number, + calAutotile: boolean + ): void; + + /** + * 当更新某个区域内的大怪物renderable信息时执行的函数 + * @param layer 目标Layer实例 + * @param x 左上角横坐标 + * @param y 左上角纵坐标 + * @param width 区域宽度 + * @param height 区域高度 + * @param images 最终的大怪物renderable信息,等同于 {@link Layer.bigImages} + */ + onBigImagesUpdate?( + layer: Layer, + x: number, + y: number, + width: number, + height: number, + images: Map + ): void; + + /** + * 当计算完成区域内自动元件连接信息时执行的函数 + * @param layer 目标Layer实例 + * @param x 左上角横坐标 + * @param y 左上角纵坐标 + * @param width 区域宽度 + * @param height 区域高度 + * @param autotiles 计算出的自动元件连接信息,等同于 {@link Layer.autotiles} + */ + onAutotilesCaled?( + layer: Layer, + x: number, + y: number, + width: number, + height: number, + 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实例 + * @param width 地图宽度 + * @param height 地图高度 + */ + onMapResize?(layer: Layer, width: number, height: number): void; + + /** + * 当更新指定区域的分块缓存时执行的函数 + * @param layer 目标Layer实例 + * @param blocks 更新区域内包含的分块索引 + * @param x 区域的图格左上角横坐标 + * @param y 区域的图格右上角横坐标 + * @param width 区域的图格宽度 + * @param height 区域的图格高度 + */ + onBlocksUpdate?( + layer: Layer, + blocks: Set, + x: number, + y: number, + width: number, + height: number + ): void; + + /** + * 当更新移动层的渲染信息是执行的函数 + * @param layer 目标Layer实例 + * @param renderable 移动层的渲染信息(包含大怪物) + */ + onMovingUpdate?(layer: Layer, renderable: LayerMovingRenderable[]): void; + + /** + * 在地图渲染之前执行的函数 + * @param layer 目标Layer实例 + * @param camera 渲染的摄像头信息 + * @param need 需要渲染的分块信息 + */ + onBeforeRender?(layer: Layer, camera: Camera, need: NeedRenderData): void; + + /** + * 在地图渲染之后执行的函数 + * @param layer 目标Layer实例 + * @param camera 渲染的摄像头信息 + * @param need 需要渲染的分块信息 + */ + onAfterRender?(layer: Layer, camera: Camera, need: NeedRenderData): void; + + /** + * 当拓展被取消挂载时执行的函数(Layer被销毁,拓展被移除等) + * @param layer 目标Layer实例 + */ + onDestroy?(layer: Layer): void; +} + interface LayerCacheItem { + // todo: 删掉这个属性 floorId?: FloorIds; canvas: HTMLCanvasElement; } @@ -466,9 +735,9 @@ interface LayerMovingRenderable extends RenderableData { } interface NeedRenderData { - /** 需要渲染的地图内容 */ + /** 需要渲染的分块索引 */ res: Set; - /** 需要渲染的背景内容 */ + /** 需要渲染的背景的左上角横纵坐标,因为背景是可能渲染在地图之外的,所以不能使用分块索引的形式存储 */ back: [x: number, y: number][]; } @@ -499,9 +768,9 @@ interface MovingBlock { steps: MovingStep[]; /** 当前正在执行哪一步 */ index: number; - /** 目标横坐标 */ + /** 当前横坐标 */ x: number; - /** 目标纵坐标 */ + /** 当前纵坐标 */ y: number; /** 渲染信息 */ render: RenderableData | AutotileRenderable; @@ -559,6 +828,8 @@ export class Layer extends Container { /** 下一此渲染时是否需要更新移动层的渲染信息 */ needUpdateMoving: boolean = false; + private extend: Map = new Map(); + constructor() { super('absolute'); @@ -598,6 +869,36 @@ export class Layer extends Container { }); } + /** + * 添加渲染拓展,可以将渲染拓展理解为一类插件,通过指定的函数在对应时刻执行一些函数, + * 来达到执行自己想要的功能的效果。例如样板自带的勇士渲染、伤害渲染等都由此实现。 + * 具体能干什么参考 {@link ILayerRenderExtends} + * @param ex 渲染拓展对象 + */ + extends(ex: ILayerRenderExtends) { + this.extend.set(ex.id, ex); + ex.awake?.(this); + } + + /** + * 移除一个渲染拓展 + * @param id 要移除的拓展 + */ + removeExtends(id: string) { + const ex = this.extend.get(id); + if (!ex) return; + this.extend.delete(id); + ex.onDestroy?.(this); + } + + /** + * 获取一个已装载的拓展 + * @param id 拓展id + */ + getExtends(id: string) { + return this.extend.get(id); + } + /** * 判断一个点是否在地图范围内 * @param x 横坐标 @@ -646,6 +947,10 @@ export class Layer extends Container { setBackground(background: AllNumbers) { this.background = background; this.generateBackground(); + + for (const ex of this.extend.values()) { + ex.onBackgroundSet?.(this, background); + } } /** @@ -694,6 +999,10 @@ export class Layer extends Container { this.backImage.push(canvas.canvas); } + + for (const ex of this.extend.values()) { + ex.onBackgroundGenerated?.(this, this.backImage); + } } /** @@ -725,22 +1034,46 @@ export class Layer extends Container { ); if (this.isRectOutside(x, y, width, height)) return; } - for (let nx = 0; nx < width; nx++) { - for (let ny = 0; ny < height; ny++) { - const dx = nx + x; - const dy = ny + y; - if (this.isPointOutside(dx, dy)) { - continue; + // 特判特殊情况-全地图更新 + if ( + x === 0 && + y === 0 && + width === this.mapWidth && + height === this.mapHeight + ) { + // 为了不丢失引用,需要先清空,然后填充,不能直接赋值 + this.renderData.splice(0); + this.renderData.push(...data); + } else if (data.length === 1) { + // 特判单个图块的情况 + const index = x + y + this.mapWidth; + this.renderData[index] = data[0]; + } else { + // 限定更新区域 + const startX = Math.max(0, x); + const startY = Math.max(0, y); + const endX = Math.min(this.mapWidth, width); + const endY = Math.min(this.mapHeight, height); + for (let nx = startX; nx < endX; nx++) { + for (let ny = startY; ny < endY; ny++) { + // dx和dy表示数据在传入的data中的位置 + const dx = nx - x; + const dy = ny - y; + const index = dx + dy * width; + const indexData = nx + nx * this.mapWidth; + this.renderData[indexData] = data[index]; } - const index = nx + ny * width; - const indexData = dx + dy * this.mapWidth; - this.renderData[indexData] = data[index]; } } + // todo: 异步优化,到下一帧再更新 if (calAutotile) this.calAutotiles(x, y, width, height); this.updateBlocks(x, y, width, height); this.updateBigImages(x, y, width, height); this.update(this); + + for (const ex of this.extend.values()) { + ex.onDataPut?.(this, data, width, x, y, calAutotile); + } } /** @@ -769,10 +1102,14 @@ export class Layer extends Container { } this.needUpdateMoving = true; + + for (const ex of this.extend.values()) { + ex.onBigImagesUpdate?.(this, x, y, width, height, this.bigImages); + } } /** - * 计算自动元件的连接信息 + * 计算自动元件的连接信息(会丢失autotiles属性的引用) */ calAutotiles(x: number, y: number, width: number, height: number) { const ex = x + width; @@ -864,6 +1201,10 @@ export class Layer extends Container { check(nx, ny, nx, ny + 1, 0b00000100, 0b01000000); } } + + for (const ex of this.extend.values()) { + ex.onAutotilesCaled?.(this, x, y, width, height, this.autotiles); + } } /** @@ -871,42 +1212,42 @@ 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(); - } + // 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(); - } + // 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); - } - } + // 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,以保证渲染正常进行 + * 设置地图大小,会清空渲染数据(且丢失引用),因此后面应当紧跟 putRenderData,以保证渲染正常进行 * @param width 地图宽度 * @param height 地图高度 */ @@ -916,18 +1257,32 @@ export class Layer extends Container { this.renderData = Array(width * height).fill(0); this.autotiles = {}; this.block.size(width, height); + + for (const ex of this.extend.values()) { + ex.onMapResize?.(this, width, height); + } } /** * 给定一个矩形,更新其包含的块信息,注意由于自动元件的存在,实际判定范围会大一圈 - * @param x 左上角横坐标 - * @param y 左上角纵坐标 - * @param width 宽度 - * @param height 高度 + * @param x 图格的左上角横坐标 + * @param y 图格的左上角纵坐标 + * @param width 横向有多少个图格 + * @param height 纵向有多少个图格 */ updateBlocks(x: number, y: number, width: number, height: number) { - this.block.updateArea(x, y, width, height, Layer.FRAME_ALL); + const blocks = this.block.updateElementArea( + x, + y, + width, + height, + Layer.FRAME_ALL + ); this.update(this); + + for (const ex of this.extend.values()) { + ex.onBlocksUpdate?.(this, blocks, x, y, width, height); + } } /** @@ -969,6 +1324,10 @@ 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); + } } /** @@ -981,9 +1340,15 @@ export class Layer extends Container { if (this.needUpdateMoving) this.updateMovingRenderable(); + for (const ex of this.extend.values()) { + ex.onBeforeRender?.(this, camera, need); + } this.renderBack(camera, need); this.renderStatic(camera, need); this.renderMoving(camera); + for (const ex of this.extend.values()) { + ex.onAfterRender?.(this, camera, need); + } } /** @@ -1078,7 +1443,7 @@ export class Layer extends Container { if (num === 0 || num === 17) continue; const data = texture.getRenderable(num); if (!data || data.bigImage) continue; - const f = frame % data.frame; + const f = (frame - 1) % data.frame; const i = data.animate === -1 ? frame === 4 && data.frame === 3 @@ -1184,7 +1549,7 @@ export class Layer extends Container { * @param fn 移动函数,传入一个完成度(范围0-1),返回一个三元素数组,表示横纵格子坐标,可以是小数。 * 第三个元素表示图块纵深,一般图块的纵深就是其纵坐标,当地图上有大怪物时,此举可以辅助渲染, * 否则可能会导致移动过程中与大怪物的层级关系不正确,比如全在大怪物身后。注意不建议频繁改动这个值, - * 因为此举会导致层级的重新排序,降低渲染性能。当移动结束时,会对最终位置取整得到移动后的坐标 + * 因为此举会导致层级的重新排序,降低渲染性能。 * @param time 移动总时长 * @param relative 是否是相对模式 */ @@ -1205,340 +1570,347 @@ export class Layer extends Container { // todo return Promise.resolve(); } + + destroy(): void { + for (const ex of this.extend.values()) { + ex.onDestroy?.(this); + } + super.destroy(); + } } -interface DamageRenderable { - x: number; - y: number; - align: CanvasTextAlign; - baseline: CanvasTextBaseline; - text: string; - color: CanvasStyle; -} +// interface DamageRenderable { +// x: number; +// y: number; +// align: CanvasTextAlign; +// baseline: CanvasTextBaseline; +// text: string; +// color: CanvasStyle; +// } -export class Damage extends Sprite { - floorId?: FloorIds; +// export class Damage extends Sprite { +// floorId?: FloorIds; - mapWidth: number = 0; - mapHeight: number = 0; +// mapWidth: number = 0; +// mapHeight: number = 0; - /** 键表示格子索引,值表示在这个格子上的渲染信息(当然实际渲染位置可以不在这个格子上) */ - renderable: Map = new Map(); - block: BlockCacher; - /** 记录所有需要重新计算伤害的分块,这样可以不用一次性计算全地图的伤害,从而优化性能 */ - needUpdateBlock: Set = new Set(); +// /** 键表示格子索引,值表示在这个格子上的渲染信息(当然实际渲染位置可以不在这个格子上) */ +// renderable: Map = new Map(); +// block: BlockCacher; +// /** 记录所有需要重新计算伤害的分块,这样可以不用一次性计算全地图的伤害,从而优化性能 */ +// needUpdateBlock: Set = new Set(); - cellSize: number = 32; +// cellSize: number = 32; - /** 伤害渲染层,渲染至之后再渲染到目标层 */ - damageMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); - /** 字体 */ - font: string = "14px 'normal'"; - /** 描边样式 */ - strokeColor: CanvasStyle = '#000'; - /** 描边粗细 */ - strokeWidth: number = 2; +// /** 伤害渲染层,渲染至之后再渲染到目标层 */ +// damageMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); +// /** 字体 */ +// font: string = "14px 'normal'"; +// /** 描边样式 */ +// strokeColor: CanvasStyle = '#000'; +// /** 描边粗细 */ +// strokeWidth: number = 2; - constructor(floor?: FloorIds) { - super(); +// 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.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(); - }); - } +// 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); - } +// /** +// * 更新楼层信息 +// */ +// 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 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; +// /** +// * 根据需要更新的区域更新显示信息,注意调用前需要保证怪物信息是最新的,也就是要在计算过怪物信息后才能调用这个 +// * @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); +// 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); +// 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; +// 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); - } - } +// 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][] = []; +// 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]); +// 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); - } - } - }); +// 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); - }); - }); +// 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); - } - } +// 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); - } +// /** +// * 更新指定区域内的渲染信息,注意调用前需要保证怪物信息是最新的,也就是要在计算过怪物信息后才能调用这个 +// */ +// 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); - } +// /** +// * 向渲染列表添加渲染内容,应当在 `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]; +// 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; - } +// 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); - } +// 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); - } +// 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); - } - } +// /** +// * 计算需要渲染哪些块 +// */ +// 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; +// /** +// * 渲染伤害值 +// */ +// renderDamage(camera: Camera) { +// if (!this.floorId) return; - const { ctx } = this.damageMap; - ctx.save(); - transformCanvas(this.damageMap, camera, true); +// 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 { 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 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 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); +// 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); - }); - } - } +// 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(); - } -} +// 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 2eaaac5..5d7849a 100644 --- a/src/core/render/render.ts +++ b/src/core/render/render.ts @@ -3,7 +3,9 @@ import { MotaCanvas2D, MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { Camera } from './camera'; import { Container } from './container'; import { RenderItem, transformCanvas, withCacheRender } from './item'; -import { Layer, LayerGroup } from './preset/layer'; +import { FloorLayer, Layer, LayerGroup } from './preset/layer'; +import { LayerGroupFloorBinder } from './preset/floor'; +import { FloorDamageExtends } from './preset/damage'; export class MotaRenderer extends Container { static list: Set = new Set(); @@ -65,6 +67,7 @@ export class MotaRenderer extends Container { * 渲染游戏画面 */ render() { + console.time(); const { canvas, ctx } = this.target; const camera = this.camera; this.emit('beforeRender'); @@ -91,12 +94,13 @@ export class MotaRenderer extends Container { }); }); this.emit('afterRender'); + console.timeEnd(); } update(item?: RenderItem) { if (this.needUpdate) return; this.needUpdate = true; - requestAnimationFrame(() => { + this.requestRenderFrame(() => { this.cache(this.using); this.needUpdate = false; this.refresh(item); @@ -141,7 +145,16 @@ Mota.require('var', 'hook').once('reset', () => { render.mount(); const layer = new LayerGroup(); - layer.addDamage(); + + ['bg', 'bg2', 'event', 'fg', 'fg2'].forEach(v => { + layer.addLayer(v as FloorLayer); + }); + + const binder = new LayerGroupFloorBinder(); + const damage = new FloorDamageExtends(); + layer.extends(binder); + layer.extends(damage); + binder.bindThis(); render.appendChild(layer); camera.move(240, 240); diff --git a/src/game/enemy/damage.ts b/src/game/enemy/damage.ts index 851a24e..4483e99 100644 --- a/src/game/enemy/damage.ts +++ b/src/game/enemy/damage.ts @@ -1,4 +1,4 @@ -import { getHeroStatusOf, getHeroStatusOn } from '@/game/hero'; +import { getHeroStatusOf, getHeroStatusOn } from '@/game/state/hero'; import { Range, RangeCollection } from '@/plugin/game/range'; import { checkV2, @@ -44,7 +44,7 @@ interface DamageInfo { skill?: number; } -interface MapDamage { +export interface MapDamage { damage: number; type: Set; mockery?: LocArr[]; @@ -100,6 +100,7 @@ export class EnemyCollection implements RangeCollection { list: DamageEnemy[] = []; range: Range = new Range(this); + // todo: 改成Map mapDamage: Record = {}; haloList: HaloData[] = []; @@ -353,7 +354,7 @@ export class DamageEnemy { * 伤害计算进度,0 -> 预平衡光环 -> 1 -> 计算没有光环的属性 -> 2 -> provide inject 光环 * -> 3 -> 计算光环加成 -> 4 -> 计算完毕 */ - private progress: number = 0; + progress: number = 0; constructor( enemy: Enemy, @@ -656,6 +657,7 @@ export class DamageEnemy { * 计算怪物伤害 */ calDamage(hero: Partial = core.status.hero) { + // todo: 缓存怪物伤害 const enemy = this.getRealInfo(); return this.calEnemyDamageOf(hero, enemy); } @@ -831,6 +833,7 @@ export class DamageEnemy { num: number = 1, hero: Partial = core.status.hero ): CriticalDamageDelta[] { + // todo: 缓存临界 const origin = this.calDamage(hero); const seckill = this.getSeckillAtk(); return this.calCriticalWith(num, seckill, origin, hero); diff --git a/src/game/enemy/special.ts b/src/game/enemy/special.ts index 48a413b..f9b63c1 100644 --- a/src/game/enemy/special.ts +++ b/src/game/enemy/special.ts @@ -1,4 +1,4 @@ -import { getHeroStatusOn } from '@/game/hero'; +import { getHeroStatusOn } from '@/game/state/hero'; import { EnemyInfo } from './damage'; export interface SpecialDeclaration {