mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-01-19 12:49:25 +08:00
refactor: 地图渲染与伤害渲染
This commit is contained in:
parent
bb656f8169
commit
c7660e93c7
1
idea.md
1
idea.md
@ -116,3 +116,4 @@ dam4.png ---- 存档 59
|
||||
[] 自定义状态栏,通过申请空间进行布局
|
||||
[x] 复写 api,rewrite()
|
||||
[x] 对 vnode 进行简单的包装,提供出显示文字、显示图片等 api 以及修改 css 的 api
|
||||
[] mapDamage 注册
|
||||
|
@ -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": {
|
||||
|
@ -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<RenderItemEvent>
|
||||
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;
|
||||
}
|
||||
|
@ -16,6 +16,12 @@ interface BlockData {
|
||||
restHeight: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单分块缓存类,内容包含元素与分块两种,其中元素是最小单元,分块是缓存单元。
|
||||
* 拿楼层举例,假如我将楼层按照13x13划分缓存,那么元素就是每个图块,而分块就是这13x13的缓存分块。
|
||||
* 为方便区分,在相关函数的注释最后,都会有`xx -> yy`的说明,
|
||||
* 其中xx说明传入的数据是元素还是分块的数据,而yy表示其返回值或转换为的值
|
||||
*/
|
||||
export class BlockCacher<T> extends EventEmitter<BlockCacherEvent> {
|
||||
/** 区域宽度 */
|
||||
width: number;
|
||||
@ -47,6 +53,7 @@ export class BlockCacher<T> extends EventEmitter<BlockCacherEvent> {
|
||||
this.height = height;
|
||||
this.blockSize = size;
|
||||
this.cacheDepth = depth;
|
||||
this.split();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,7 +110,7 @@ export class BlockCacher<T> extends EventEmitter<BlockCacherEvent> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定块的索引
|
||||
* 清除指定块的索引(分块->void)
|
||||
* @param index 要清除的缓存索引
|
||||
* @param deep 清除哪些深度的缓存,至多31位二进制数,例如填0b111就是清除前三层的索引
|
||||
*/
|
||||
@ -117,7 +124,7 @@ export class BlockCacher<T> extends EventEmitter<BlockCacherEvent> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空指定索引的缓存,与 {@link clearCache} 不同的是,这里会直接清空对应索引的缓存,而不是指定分块的缓存
|
||||
* 清空指定索引的缓存,与 {@link clearCache} 不同的是,这里会直接清空对应索引的缓存,而不是指定分块的缓存(分块->void)
|
||||
*/
|
||||
clearCacheByIndex(index: number) {
|
||||
this.cache.delete(index);
|
||||
@ -131,14 +138,14 @@ export class BlockCacher<T> extends EventEmitter<BlockCacherEvent> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分块的横纵坐标获取其索引
|
||||
* 根据分块的横纵坐标获取其索引(分块->分块)
|
||||
*/
|
||||
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<T> extends EventEmitter<BlockCacherEvent> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据块的索引获取其位置
|
||||
* 根据块的索引获取其位置(分块->分块)
|
||||
*/
|
||||
getBlockXYByIndex(index: number): LocArr {
|
||||
const width = this.blockData.width;
|
||||
@ -156,29 +163,47 @@ export class BlockCacher<T> extends EventEmitter<BlockCacherEvent> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个元素位置所在的分块位置(即使它不在任何分块内)
|
||||
* 获取一个元素位置所在的分块位置(即使它不在任何分块内)(元素->分块)
|
||||
*/
|
||||
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<T> extends EventEmitter<BlockCacherEvent> {
|
||||
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<number>();
|
||||
@ -204,12 +231,43 @@ export class BlockCacher<T> extends EventEmitter<BlockCacherEvent> {
|
||||
|
||||
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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
443
src/core/render/preset/damage.ts
Normal file
443
src/core/render/preset/damage.ts
Normal file
@ -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<HTMLCanvasElement>;
|
||||
/** 键表示分块索引,值表示在这个分块上的渲染信息(当然实际渲染位置可以不在这个分块上) */
|
||||
renderable: Map<number, Set<DamageRenderable>> = new Map();
|
||||
|
||||
/** 当前渲染怪物列表 */
|
||||
enemy?: EnemyCollection;
|
||||
/** 每个分块中包含的怪物集合 */
|
||||
blockData: Map<number, Map<number, DamageEnemy>> = 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<number> = 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<number>) {
|
||||
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<DamageRenderable>) {
|
||||
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<DamageRenderable>) {
|
||||
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<DamageRenderable>,
|
||||
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();
|
||||
}
|
||||
}
|
269
src/core/render/preset/floor.ts
Normal file
269
src/core/render/preset/floor.ts
Normal file
@ -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<LayerGroupBinderEvent>
|
||||
implements ILayerGroupRenderExtends
|
||||
{
|
||||
id: string = 'floor-binder';
|
||||
|
||||
bindThisFloor: boolean = true;
|
||||
floor?: FloorIds;
|
||||
group!: LayerGroup;
|
||||
|
||||
/** 附属的子LayerFloorBinder拓展 */
|
||||
layerBinders: Set<LayerFloorBinder> = new Set();
|
||||
|
||||
private needUpdate: boolean = false;
|
||||
|
||||
static activedBinder: Set<LayerGroupFloorBinder> = 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<LayerFloorBinder> = 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);
|
||||
}
|
||||
}
|
3
src/core/render/preset/hero.ts
Normal file
3
src/core/render/preset/hero.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { ILayerRenderExtends } from './layer';
|
||||
|
||||
export class HeroRenderer implements ILayerRenderExtends {}
|
File diff suppressed because it is too large
Load Diff
@ -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<MotaRenderer> = 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);
|
||||
|
@ -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<string>;
|
||||
mockery?: LocArr[];
|
||||
@ -100,6 +100,7 @@ export class EnemyCollection implements RangeCollection<DamageEnemy> {
|
||||
list: DamageEnemy[] = [];
|
||||
|
||||
range: Range<DamageEnemy> = new Range(this);
|
||||
// todo: 改成Map<number, MapDamage>
|
||||
mapDamage: Record<string, MapDamage> = {};
|
||||
haloList: HaloData[] = [];
|
||||
|
||||
@ -353,7 +354,7 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
|
||||
* 伤害计算进度,0 -> 预平衡光环 -> 1 -> 计算没有光环的属性 -> 2 -> provide inject 光环
|
||||
* -> 3 -> 计算光环加成 -> 4 -> 计算完毕
|
||||
*/
|
||||
private progress: number = 0;
|
||||
progress: number = 0;
|
||||
|
||||
constructor(
|
||||
enemy: Enemy<T>,
|
||||
@ -656,6 +657,7 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
|
||||
* 计算怪物伤害
|
||||
*/
|
||||
calDamage(hero: Partial<HeroStatus> = core.status.hero) {
|
||||
// todo: 缓存怪物伤害
|
||||
const enemy = this.getRealInfo();
|
||||
return this.calEnemyDamageOf(hero, enemy);
|
||||
}
|
||||
@ -831,6 +833,7 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
|
||||
num: number = 1,
|
||||
hero: Partial<HeroStatus> = core.status.hero
|
||||
): CriticalDamageDelta[] {
|
||||
// todo: 缓存临界
|
||||
const origin = this.calDamage(hero);
|
||||
const seckill = this.getSeckillAtk();
|
||||
return this.calCriticalWith(num, seckill, origin, hero);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getHeroStatusOn } from '@/game/hero';
|
||||
import { getHeroStatusOn } from '@/game/state/hero';
|
||||
import { EnemyInfo } from './damage';
|
||||
|
||||
export interface SpecialDeclaration {
|
||||
|
Loading…
Reference in New Issue
Block a user