import { ERenderItemEvent, RenderItem, MotaOffscreenCanvas2D, Transform, transformCanvas } from '@motajs/render-core'; import { logger } from '@motajs/common'; import EventEmitter from 'eventemitter3'; import { isNil } from 'lodash-es'; import { BlockCacher, CanvasCacheItem, ICanvasCacheItem, calNeedRenderOf, ILayerGroupRenderExtends, Layer, LayerGroup, LayerGroupFloorBinder, tagMap } from '@motajs/render'; import { IDamageEnemy, IEnemyCollection, MapDamage } from '@motajs/types'; import { UserEnemyInfo } from '@user/data-state'; /** * 根据伤害大小获取颜色 * @param damage 伤害大小 */ export function getDamageColor(damage: number): string { if (typeof damage !== 'number') return '#f00'; if (damage === 0) return '#2f2'; if (damage < 0) return '#7f7'; if (damage < core.status.hero.hp / 3) return '#fff'; if (damage < (core.status.hero.hp * 2) / 3) return '#ff4'; if (damage < core.status.hero.hp) return '#f93'; return '#f22'; } interface EFloorDamageEvent { update: [floor: FloorIds]; } export class FloorDamageExtends extends EventEmitter implements ILayerGroupRenderExtends { id: string = 'floor-damage'; floorBinder!: LayerGroupFloorBinder; group!: LayerGroup; sprite!: Damage; /** * 立刻刷新伤害渲染 */ update(floor: FloorIds) { if (!this.sprite || !floor) return; const map = core.status.maps[floor]; this.sprite.setMapSize(map.width, map.height); const { ensureFloorDamage } = Mota.require('@user/data-state'); ensureFloorDamage(floor); const enemy = core.status.maps[floor].enemy; this.sprite.updateCollection(enemy); this.emit('update', floor); } /** * 创建显伤层 */ private create() { if (this.sprite) return; const sprite = new Damage(); sprite.setZIndex(80); this.group.appendChild(sprite); this.sprite = sprite; } private onUpdate = (floor: FloorIds) => { if (!this.floorBinder.bindThisFloor) { const { ensureFloorDamage } = Mota.require('@user/data-state'); ensureFloorDamage(floor); core.status.maps[floor].enemy.calRealAttribute(); } this.update(floor); }; // private onSetBlock = (x: number, y: number, floor: FloorIds) => { // this.sprite.enemy?.once('extract', () => { // if (floor !== this.sprite.enemy?.floorId) return; // this.sprite.updateBlocks(); // }); // if (!this.floorBinder.bindThisFloor) { // this.sprite.enemy?.extract(); // } // }; /** * 进行楼层更新监听 */ private listen() { this.floorBinder.on('update', this.onUpdate); // this.floorBinder.on('setBlock', this.onSetBlock); } awake(group: LayerGroup): void { const ex = group.getExtends('floor-binder'); if (ex instanceof LayerGroupFloorBinder) { this.floorBinder = ex; this.group = group; this.create(); this.listen(); } else { logger.warn(17); group.removeExtends('floor-damage'); } } onDestroy(_group: LayerGroup): void { this.floorBinder.off('update', this.onUpdate); // this.floorBinder.off('setBlock', this.onSetBlock); } } export interface DamageRenderable { x: number; y: number; align: CanvasTextAlign; baseline: CanvasTextBaseline; text: string; color: CanvasStyle; font?: string; stroke?: CanvasStyle; strokeWidth?: number; } export interface EDamageEvent extends ERenderItemEvent { setMapSize: [width: number, height: number]; beforeDamageRender: [need: Set, transform: Transform]; updateBlocks: [blocks: Set]; dirtyUpdate: [block: number]; } export class Damage extends RenderItem { mapWidth: number = 0; mapHeight: number = 0; block: BlockCacher; /** 键表示分块索引,值表示在这个分块上的渲染信息(当然实际渲染位置可以不在这个分块上) */ renderable: Map> = new Map(); /** 当前渲染怪物列表 */ enemy?: IEnemyCollection; /** 每个分块中包含的怪物集合 */ blockData: Map> = new Map(); /** 单元格大小 */ cellSize: number = 32; /** 默认伤害字体 */ font: string = '300 9px Verdana'; /** 默认描边样式,当伤害文字不存在描边属性时会使用此属性 */ strokeStyle: CanvasStyle = '#000'; /** 默认描边宽度 */ strokeWidth: number = 2; /** 要懒更新的所有分块 */ private dirtyBlocks: Set = new Set(); constructor() { super('absolute', false, true); this.block = new BlockCacher(0, 0, core._WIDTH_, 1); this.type = 'absolute'; this.size(core._PX_, core._PY_); this.setHD(true); this.setAntiAliasing(true); } protected render( canvas: MotaOffscreenCanvas2D, transform: Transform ): void { this.renderDamage(canvas, transform); } private onExtract = () => { if (this.enemy) this.updateCollection(this.enemy); }; /** * 设置地图大小,后面应紧跟更新怪物列表 */ 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()); this.dirtyBlocks.add(i); } this.emit('setMapSize', width, height); } /** * 设置每个图块的大小 */ setCellSize(size: number) { this.cellSize = size; this.update(); } /** * 更新怪物列表。更新后,{@link Damage.enemy} 会丢失原来的怪物列表引用,换为传入的列表引用 * @param enemy 怪物列表 */ updateCollection(enemy: IEnemyCollection) { if (this.enemy !== enemy) { this.enemy?.off('calculated', this.onExtract); enemy.on('calculated', this.onExtract); } this.enemy = enemy; this.blockData.forEach(v => v.clear()); this.renderable.forEach(v => v.clear()); this.block.clearAllCache(); const w = this.block.blockData.width; const h = this.block.blockData.height; const num = w * h; for (let i = 0; i < num; i++) { this.dirtyBlocks.add(i); } 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 要更新的分块集合 * @param map 是否更新地图伤害 */ updateBlocks(blocks?: Set) { if (blocks) { blocks.forEach(v => this.dirtyBlocks.add(v)); this.emit('updateBlocks', blocks); } else { this.blockData.forEach((_v, i) => { this.dirtyBlocks.add(i); }); this.emit('updateBlocks', new Set(this.blockData.keys())); } 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); // 渲染懒更新,优化性能表现 this.dirtyBlocks.add(block); } /** * 更新单个分块 * @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)!; renderable.clear(); data.forEach(v => this.extract(v, renderable)); if (map) this.extractMapDamage(block, renderable); } /** * 将怪物解析为renderable的伤害 * @param enemy 怪物 * @param block 怪物所属分块 */ private extract(enemy: IDamageEnemy, 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 real = enemy.getRealInfo() as UserEnemyInfo; 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); const hasHorn = real.special.has(33); if (real.special.has(8) && real.togetherNum && !hasHorn) { const dam3: DamageRenderable = { align: 'right', baseline: 'top', text: real.togetherNum.toString(), color: '#09FF5B', x: x * this.cellSize + this.cellSize - 1, y: y * this.cellSize + 2, strokeWidth: 3 }; block.add(dam3); } if (real.special.has(30)) { const dam4: DamageRenderable = { align: 'left', baseline: 'top', text: '乾', color: '#FDCD0B', x: x * this.cellSize + 1, y: y * this.cellSize + 2, strokeWidth: 2, font: '500 10px Verdana' }; block.add(dam4); } if (enemy.col && hasHorn) { const dam5: DamageRenderable = { align: 'right', baseline: 'top', text: enemy.col.list.size.toString(), color: '#fff866', x: x * this.cellSize + this.cellSize - 1, y: y * this.cellSize + 2, strokeWidth: 3 }; block.add(dam5); } } /** * 解析指定分块的地图伤害 * @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'; let font = '300 9px Verdana'; 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'; font = '500 11px Verdana'; } else if (dam.hunt) { text = '猎'; color = '#fd4'; font = '500 11px Verdana'; } else { return; } const mapDam: DamageRenderable = { align: 'center', baseline: 'middle', text, color, font, x: x * this.cellSize + this.cellSize / 2, y: y * this.cellSize + this.cellSize / 2 }; block.add(mapDam); } /** * 计算需要渲染哪些块 */ calNeedRender(transform: Transform) { if (this.parent instanceof LayerGroup) { // 如果处于地图组中,每个地图的渲染区域应该是一样的,因此可以缓存优化 return this.parent.cacheNeedRender(transform, this.block); } else if (this.parent instanceof Layer) { // 如果是地图的子元素,直接调用Layer的计算函数 return this.parent.calNeedRender(transform); } else { return calNeedRenderOf(transform, this.cellSize, this.block); } } /** * 渲染伤害层 * @param transform 变换矩阵 */ renderDamage(canvas: MotaOffscreenCanvas2D, transform: Transform) { // console.time('damage'); const { ctx } = canvas; transformCanvas(canvas, transform); const render = this.calNeedRender(transform); const block = this.block; const cell = this.cellSize; const size = cell * block.blockSize; this.emit('beforeDamageRender', render, transform); 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; // todo: 是否真的需要缓存 // 检查有没有缓存 const cache = block.cache.get(v); if (cache && cache.symbol === cache.canvas.symbol) { ctx.drawImage(cache.canvas.canvas, px, py, size, size); return; } if (this.dirtyBlocks.has(v)) { this.updateBlock(v, true); } this.emit('dirtyUpdate', v); // 否则依次渲染并写入缓存 const temp = block.cache.get(v)?.canvas ?? this.requireCanvas(); temp.clear(); temp.setHD(true); temp.setAntiAliasing(true); temp.withGameScale(true); temp.size(size, size); const { ctx: ct } = temp; ct.translate(-px, -py); 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); }); ctx.drawImage(temp.canvas, px, py, size, size); block.cache.set(v, new CanvasCacheItem(temp, temp.symbol)); }); ctx.restore(); // console.timeEnd('damage'); } protected handleProps( key: string, _prevValue: any, nextValue: any ): boolean { switch (key) { case 'mapWidth': if (!this.assertType(nextValue, 'number', key)) return false; this.setMapSize(nextValue, this.mapHeight); return true; case 'mapHeight': if (!this.assertType(nextValue, 'number', key)) return false; this.setMapSize(this.mapWidth, nextValue); return true; case 'cellSize': if (!this.assertType(nextValue, 'number', key)) return false; this.setCellSize(nextValue); return true; case 'enemy': if (!this.assertType(nextValue, 'object', key)) return false; this.updateCollection(nextValue); return true; case 'font': if (!this.assertType(nextValue, 'string', key)) return false; this.font = nextValue; this.update(); return true; case 'strokeStyle': this.strokeStyle = nextValue; this.update(); return true; case 'strokeWidth': if (!this.assertType(nextValue, 'number', key)) return false; this.strokeWidth = nextValue; this.update(); return true; } return false; } destroy(): void { super.destroy(); this.block.destroy(); this.enemy?.off('extract', this.onExtract); } } // 注册为内部元素 tagMap.register('damage', (_0, _1, _props) => { return new Damage(); }); // const adapter = new RenderAdapter('damage');