mirror of
https://github.com/motajs/template.git
synced 2026-05-02 12:23:13 +08:00
feat: 伤害计算系统
This commit is contained in:
parent
4c0960fff7
commit
0517da5000
@ -3,6 +3,7 @@ import { ITileLocator } from '@user/types';
|
||||
import {
|
||||
IAuraConverter,
|
||||
IAuraView,
|
||||
IDamageSystem,
|
||||
IEnemy,
|
||||
IEnemyAuraView,
|
||||
IEnemyCommonQueryEffect,
|
||||
@ -53,6 +54,7 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
|
||||
private readonly dirtyEnemy: Set<IEnemyView<TAttr>> = new Set();
|
||||
|
||||
private mapDamage: IMapDamage<TAttr> | null = null;
|
||||
private damageSystem: IDamageSystem<TAttr, unknown> | null = null;
|
||||
readonly indexer: MapLocIndexer = new MapLocIndexer();
|
||||
|
||||
private needUpdate: boolean = true;
|
||||
@ -174,6 +176,9 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
|
||||
if (this.mapDamage) {
|
||||
this.mapDamage.deleteEnemy(view);
|
||||
}
|
||||
if (this.damageSystem) {
|
||||
this.damageSystem.deleteEnemy(view);
|
||||
}
|
||||
|
||||
this.needTotallyRefresh.delete(view);
|
||||
this.dirtyEnemy.delete(view);
|
||||
@ -253,6 +258,15 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
|
||||
return this.mapDamage;
|
||||
}
|
||||
|
||||
attachDamageSystem(system: IDamageSystem<TAttr, unknown>): void {
|
||||
this.damageSystem = system;
|
||||
system.markAllDirty();
|
||||
}
|
||||
|
||||
getDamageSystem<THero>(): IDamageSystem<TAttr, THero> | null {
|
||||
return this.damageSystem as IDamageSystem<TAttr, THero> | null;
|
||||
}
|
||||
|
||||
private convertSpecial(
|
||||
special: ISpecial<any>,
|
||||
enemy: IReadonlyEnemy<TAttr>,
|
||||
@ -545,6 +559,10 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
|
||||
this.buildupFinal();
|
||||
}
|
||||
|
||||
if (this.damageSystem) {
|
||||
this.damageSystem.markAllDirty();
|
||||
}
|
||||
|
||||
if (this.mapDamage) {
|
||||
this.mapDamage.refreshAll();
|
||||
}
|
||||
@ -553,6 +571,9 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
|
||||
markDirty(view: IEnemyView<TAttr>): void {
|
||||
if (!this.locatorViewMap.has(view)) return;
|
||||
this.dirtyEnemy.add(view);
|
||||
if (this.damageSystem) {
|
||||
this.damageSystem.markDirty(view);
|
||||
}
|
||||
}
|
||||
|
||||
private refreshSpecialModifier(
|
||||
@ -687,6 +708,10 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
|
||||
|
||||
this.dirtyEnemy.delete(view);
|
||||
|
||||
if (this.damageSystem) {
|
||||
this.damageSystem.markDirty(view);
|
||||
}
|
||||
|
||||
if (this.mapDamage) {
|
||||
this.mapDamage.markEnemyDirty(view);
|
||||
}
|
||||
@ -721,6 +746,9 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
|
||||
this.needTotallyRefresh.clear();
|
||||
this.requestedCommonContext.clear();
|
||||
this.dirtyEnemy.clear();
|
||||
if (this.damageSystem) {
|
||||
this.damageSystem.markAllDirty();
|
||||
}
|
||||
if (this.mapDamage) {
|
||||
this.mapDamage.refreshAll();
|
||||
}
|
||||
@ -729,6 +757,7 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
|
||||
destroy(): void {
|
||||
this.clear();
|
||||
this.attachMapDamage(null);
|
||||
this.damageSystem = null;
|
||||
this.auraConverter.clear();
|
||||
this.commonQueryMap.clear();
|
||||
this.specialQueryEffects.clear();
|
||||
|
||||
211
packages-user/data-base/src/enemy/damage.ts
Normal file
211
packages-user/data-base/src/enemy/damage.ts
Normal file
@ -0,0 +1,211 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import {
|
||||
CriticalableHeroStatus,
|
||||
IDamageCalculator,
|
||||
IDamageSystem,
|
||||
IEnemyContext,
|
||||
IEnemyCritical,
|
||||
IEnemyDamageInfo,
|
||||
IEnemyView,
|
||||
IReadonlyEnemy
|
||||
} from './types';
|
||||
|
||||
interface ICriticalSearchResult {
|
||||
/** 此临界点的属性值 */
|
||||
readonly value: number;
|
||||
/** 此临界点的伤害信息 */
|
||||
readonly info: IEnemyDamageInfo;
|
||||
}
|
||||
|
||||
export class DamageSystem<TAttr, THero> implements IDamageSystem<TAttr, THero> {
|
||||
/** 当前正在使用的计算器 */
|
||||
private calculator: IDamageCalculator<TAttr, THero> | null = null;
|
||||
/** 当前勇士属性 */
|
||||
private heroStatus: Readonly<THero> | null = null;
|
||||
/** 怪物伤害缓存 */
|
||||
private readonly cache: Map<IEnemyView<TAttr>, IEnemyDamageInfo> =
|
||||
new Map();
|
||||
|
||||
constructor(readonly context: IEnemyContext<TAttr>) {}
|
||||
|
||||
useCalculator(calculator: IDamageCalculator<TAttr, THero>): void {
|
||||
this.calculator = calculator;
|
||||
this.markAllDirty();
|
||||
}
|
||||
|
||||
getCalculator(): IDamageCalculator<TAttr, THero> | null {
|
||||
return this.calculator;
|
||||
}
|
||||
|
||||
bindHeroStatus(hero: Readonly<THero>): void {
|
||||
this.heroStatus = hero;
|
||||
this.markAllDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝勇士属性
|
||||
*/
|
||||
private cloneHeroStatus(): THero | null {
|
||||
if (!this.heroStatus) return null;
|
||||
else return structuredClone(this.heroStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在修改勇士属性的情况下计算怪物伤害
|
||||
* @param enemy 怪物属性
|
||||
* @param attribute 修改的属性键名
|
||||
* @param value 修改为的属性值
|
||||
* @returns
|
||||
*/
|
||||
private calculateDamageWithModified(
|
||||
enemy: IReadonlyEnemy<TAttr>,
|
||||
attribute: CriticalableHeroStatus<THero>,
|
||||
value: number
|
||||
): IEnemyDamageInfo {
|
||||
const hero = this.cloneHeroStatus()!;
|
||||
// @ts-expect-error 之后会进行修复
|
||||
hero[attribute] = value;
|
||||
return this.calculator!.calculate(hero, enemy);
|
||||
}
|
||||
|
||||
getDamageInfo(enemy: IEnemyView<TAttr>): IEnemyDamageInfo | null {
|
||||
if (!this.heroStatus) {
|
||||
logger.warn(107);
|
||||
return null;
|
||||
}
|
||||
if (!this.calculator) {
|
||||
logger.warn(106);
|
||||
return null;
|
||||
}
|
||||
const hero = this.cloneHeroStatus()!;
|
||||
|
||||
const cached = this.cache.get(enemy);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const info = this.calculator.calculate(hero, enemy.getComputedEnemy());
|
||||
this.cache.set(enemy, info);
|
||||
return info;
|
||||
}
|
||||
|
||||
markDirty(enemy: IEnemyView<TAttr>): void {
|
||||
this.cache.delete(enemy);
|
||||
}
|
||||
|
||||
deleteEnemy(enemy: IEnemyView<TAttr>): void {
|
||||
this.cache.delete(enemy);
|
||||
}
|
||||
|
||||
markAllDirty(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
*calculateCritical(
|
||||
view: IEnemyView<TAttr>,
|
||||
attribute: CriticalableHeroStatus<THero>,
|
||||
precision: number
|
||||
): Generator<IEnemyCritical, void, void> {
|
||||
if (!this.heroStatus) {
|
||||
logger.warn(107);
|
||||
return;
|
||||
}
|
||||
if (!this.calculator) {
|
||||
logger.warn(106);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentInfo = this.getDamageInfo(view);
|
||||
if (!currentInfo) return;
|
||||
|
||||
const enemy = view.getComputedEnemy();
|
||||
const hero = this.cloneHeroStatus()!;
|
||||
const currentValue = hero[attribute] as number;
|
||||
|
||||
const upperLimit = Math.floor(
|
||||
this.calculator.getCriticalLimit(hero, enemy, attribute)
|
||||
);
|
||||
|
||||
if (currentValue >= upperLimit) return;
|
||||
|
||||
const maxIterations = Math.max(0, Math.floor(precision));
|
||||
let baseValue = currentValue;
|
||||
let baseInfo = currentInfo;
|
||||
|
||||
while (baseValue < upperLimit) {
|
||||
const next = this.findNextCritical(
|
||||
enemy,
|
||||
attribute,
|
||||
baseValue,
|
||||
upperLimit,
|
||||
baseInfo.damage,
|
||||
maxIterations
|
||||
);
|
||||
if (!next) return;
|
||||
|
||||
yield {
|
||||
nextValue: next.value,
|
||||
baseValue: currentValue,
|
||||
nextDiff: next.value - currentValue,
|
||||
baseInfo: currentInfo,
|
||||
info: next.info,
|
||||
damageDiff: next.info.damage - currentInfo.damage
|
||||
};
|
||||
|
||||
baseValue = next.value;
|
||||
baseInfo = next.info;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算下一个临界点
|
||||
* @param enemy 怪物对象
|
||||
* @param attribute 勇士属性名
|
||||
* @param currentValue 当前勇士属性值
|
||||
* @param upperLimit 二分上界
|
||||
* @param referenceDamage 参考伤害值
|
||||
* @param maxIterations 最大迭代数量
|
||||
*/
|
||||
private findNextCritical(
|
||||
enemy: IReadonlyEnemy<TAttr>,
|
||||
attribute: CriticalableHeroStatus<THero>,
|
||||
currentValue: number,
|
||||
upperLimit: number,
|
||||
referenceDamage: number,
|
||||
maxIterations: number
|
||||
): ICriticalSearchResult | null {
|
||||
let left = currentValue;
|
||||
let right = upperLimit;
|
||||
let rightInfo = this.calculateDamageWithModified(
|
||||
enemy,
|
||||
attribute,
|
||||
right
|
||||
);
|
||||
|
||||
if (rightInfo.damage >= referenceDamage) return null;
|
||||
|
||||
let iter = 0;
|
||||
while (iter < maxIterations) {
|
||||
const middle = Math.floor((left + right) / 2);
|
||||
const middleInfo = this.calculateDamageWithModified(
|
||||
enemy,
|
||||
attribute,
|
||||
middle
|
||||
);
|
||||
if (middleInfo.damage < referenceDamage) {
|
||||
right = middle;
|
||||
rightInfo = middleInfo;
|
||||
} else {
|
||||
left = middle;
|
||||
}
|
||||
if (right - left <= 1) break;
|
||||
|
||||
iter++;
|
||||
}
|
||||
|
||||
return {
|
||||
value: right,
|
||||
info: rightInfo
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
export * from './enemy';
|
||||
export * from './context';
|
||||
export * from './damage';
|
||||
export * from './mapDamage';
|
||||
export * from './manager';
|
||||
export * from './special';
|
||||
|
||||
@ -60,10 +60,12 @@ export class MapDamage<TAttr> implements IMapDamage<TAttr> {
|
||||
/** 合并后伤害缓存,索引 -> 合并结果 */
|
||||
private readonly reducedCache: Map<number, IMapDamageInfo> = new Map();
|
||||
|
||||
constructor(
|
||||
readonly context: IEnemyContext<TAttr>,
|
||||
readonly indexer: IMapLocIndexer
|
||||
) {}
|
||||
/** 坐标索引对象 */
|
||||
private readonly indexer: IMapLocIndexer;
|
||||
|
||||
constructor(readonly context: IEnemyContext<TAttr>) {
|
||||
this.indexer = context.indexer;
|
||||
}
|
||||
|
||||
useConverter(converter: IMapDamageConverter<TAttr>): void {
|
||||
this.converter = converter;
|
||||
|
||||
@ -540,6 +540,117 @@ export interface IMapDamage<TAttr> {
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 伤害系统
|
||||
|
||||
export interface IEnemyDamageInfo {
|
||||
/** 战斗伤害值 */
|
||||
readonly damage: number;
|
||||
/** 战斗回合数 */
|
||||
readonly turn: number;
|
||||
}
|
||||
|
||||
export interface IEnemyCritical {
|
||||
/** 此临界点中指定勇士属性的值 */
|
||||
readonly nextValue: number;
|
||||
/** 当前勇士指定属性的值 */
|
||||
readonly baseValue: number;
|
||||
/** 此临界点中指定勇士数值的值与当前值的差,即 `nextValue - baseValue` */
|
||||
readonly nextDiff: number;
|
||||
/** 当前状态下怪物的伤害信息 */
|
||||
readonly baseInfo: IEnemyDamageInfo;
|
||||
/** 此临界点下怪物的伤害信息 */
|
||||
readonly info: IEnemyDamageInfo;
|
||||
/** 此临界点的伤害值与当前伤害值的差 */
|
||||
readonly damageDiff: number;
|
||||
}
|
||||
|
||||
export type CriticalableHeroStatus<THero> = keyof {
|
||||
[P in keyof THero as THero[P] extends number ? P : never]: unknown;
|
||||
};
|
||||
|
||||
export interface IDamageCalculator<TAttr, THero> {
|
||||
/**
|
||||
* 计算战斗伤害信息
|
||||
* @param hero 勇士信息
|
||||
* @param enemy 怪物信息
|
||||
*/
|
||||
calculate(
|
||||
hero: Readonly<THero>,
|
||||
enemy: IReadonlyEnemy<TAttr>
|
||||
): IEnemyDamageInfo;
|
||||
|
||||
/**
|
||||
* 获取临界计算的上界
|
||||
* @param hero 勇士信息
|
||||
* @param enemy 怪物信息
|
||||
* @param attribute 勇士的临界属性
|
||||
*/
|
||||
getCriticalLimit(
|
||||
hero: Readonly<THero>,
|
||||
enemy: IReadonlyEnemy<TAttr>,
|
||||
attribute: CriticalableHeroStatus<THero>
|
||||
): number;
|
||||
}
|
||||
|
||||
export interface IDamageSystem<TAttr, THero> {
|
||||
/** 伤害系统所属的上下文 */
|
||||
readonly context: IEnemyContext<TAttr>;
|
||||
|
||||
/**
|
||||
* 设置当前伤害计算系统使用的伤害计算器
|
||||
* @param calculator 伤害计算器
|
||||
*/
|
||||
useCalculator(calculator: IDamageCalculator<TAttr, THero>): void;
|
||||
|
||||
/**
|
||||
* 获取当前使用的伤害计算器
|
||||
*/
|
||||
getCalculator(): IDamageCalculator<TAttr, THero> | null;
|
||||
|
||||
/**
|
||||
* 绑定勇士信息
|
||||
* @param hero 勇士信息
|
||||
*/
|
||||
bindHeroStatus(hero: Readonly<THero>): void;
|
||||
|
||||
/**
|
||||
* 获取战斗伤害信息
|
||||
* @param enemy 怪物视图
|
||||
*/
|
||||
getDamageInfo(enemy: IEnemyView<TAttr>): IEnemyDamageInfo | null;
|
||||
|
||||
/**
|
||||
* 将指定的怪物标记为脏
|
||||
* @param enemy 怪物视图
|
||||
*/
|
||||
markDirty(enemy: IEnemyView<TAttr>): void;
|
||||
|
||||
/**
|
||||
* 删除指定的怪物
|
||||
* @param enemy 怪物视图
|
||||
*/
|
||||
deleteEnemy(enemy: IEnemyView<TAttr>): void;
|
||||
|
||||
/**
|
||||
* 将所有怪物标记为脏
|
||||
*/
|
||||
markAllDirty(): void;
|
||||
|
||||
/**
|
||||
* 计算怪物在指定勇士属性下的临界
|
||||
* @param enemy 怪物视图
|
||||
* @param attribute 计算临界的目标勇士属性,比如计算攻击临界、自定义属性的临界等等
|
||||
* @param precision 临界计算精度,表示会进行多少次二分计算,一般填写 `12-16` 之间的数即可
|
||||
*/
|
||||
calculateCritical(
|
||||
enemy: IEnemyView<TAttr>,
|
||||
attribute: CriticalableHeroStatus<THero>,
|
||||
precision: number
|
||||
): Generator<IEnemyCritical, void, void>;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 上下文
|
||||
|
||||
export interface IEnemyContext<TAttr> {
|
||||
@ -547,6 +658,8 @@ export interface IEnemyContext<TAttr> {
|
||||
readonly width: number;
|
||||
/** 怪物上下文高度 */
|
||||
readonly height: number;
|
||||
/** 此上下文使用的索引对象 */
|
||||
readonly indexer: IMapLocIndexer;
|
||||
|
||||
/**
|
||||
* 调整上下文尺寸,并清空当前上下文中的所有怪物与状态
|
||||
@ -702,6 +815,17 @@ export interface IEnemyContext<TAttr> {
|
||||
*/
|
||||
getMapDamage(): IMapDamage<TAttr> | null;
|
||||
|
||||
/**
|
||||
* 绑定伤害计算系统
|
||||
* @param system 伤害系统
|
||||
*/
|
||||
attachDamageSystem(system: IDamageSystem<TAttr, unknown>): void;
|
||||
|
||||
/**
|
||||
* 获取当前绑定的伤害计算系统
|
||||
*/
|
||||
getDamageSystem<THero>(): IDamageSystem<TAttr, THero> | null;
|
||||
|
||||
/**
|
||||
* 重建当前上下文中的全部怪物计算结果
|
||||
*
|
||||
|
||||
@ -3,6 +3,19 @@ import { IHeroState, HeroState } from './hero';
|
||||
import { ILayerState, LayerState } from './map';
|
||||
import { IRoleFaceBinder, RoleFaceBinder } from './common';
|
||||
import { GameDataState } from './data';
|
||||
import {
|
||||
DamageSystem,
|
||||
EnemyContext,
|
||||
IEnemyContext,
|
||||
MapDamage
|
||||
} from '@user/data-base';
|
||||
import { IEnemyAttributes } from './enemy/types';
|
||||
import {
|
||||
CommonAuraConverter,
|
||||
GuardAuraConverter,
|
||||
MainMapDamageConverter,
|
||||
MainMapDamageReducer
|
||||
} from './enemy';
|
||||
|
||||
export class CoreState implements ICoreState {
|
||||
readonly layer: ILayerState;
|
||||
@ -11,6 +24,7 @@ export class CoreState implements ICoreState {
|
||||
readonly data: IGameDataState;
|
||||
readonly idNumberMap: Map<string, number>;
|
||||
readonly numberIdMap: Map<number, string>;
|
||||
readonly enemyContext: IEnemyContext<IEnemyAttributes>;
|
||||
|
||||
constructor() {
|
||||
this.layer = new LayerState();
|
||||
@ -19,6 +33,19 @@ export class CoreState implements ICoreState {
|
||||
this.idNumberMap = new Map();
|
||||
this.numberIdMap = new Map();
|
||||
this.data = new GameDataState();
|
||||
|
||||
// 怪物上下文初始化
|
||||
const enemyContext = new EnemyContext<IEnemyAttributes>();
|
||||
const damageSystem = new DamageSystem(enemyContext);
|
||||
const mapDamage = new MapDamage(enemyContext);
|
||||
mapDamage.useConverter(new MainMapDamageConverter());
|
||||
mapDamage.useReducer(new MainMapDamageReducer());
|
||||
enemyContext.attachDamageSystem(damageSystem);
|
||||
enemyContext.attachMapDamage(mapDamage);
|
||||
enemyContext.registerAuraConverter(new CommonAuraConverter());
|
||||
enemyContext.registerAuraConverter(new GuardAuraConverter());
|
||||
enemyContext.resize(core._WIDTH_, core._HEIGHT_);
|
||||
this.enemyContext = enemyContext;
|
||||
}
|
||||
|
||||
saveState(): IStateSaveData {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ILayerState } from './map';
|
||||
import { IHeroFollower, IHeroState } from './hero';
|
||||
import { IRoleFaceBinder } from './common';
|
||||
import { IEnemyManager } from '@user/data-base';
|
||||
import { IEnemyContext, IEnemyManager } from '@user/data-base';
|
||||
import { IEnemyAttributes } from './enemy/types';
|
||||
|
||||
export interface IGameDataState {
|
||||
@ -28,6 +28,9 @@ export interface ICoreState {
|
||||
/** 图块数字到 id 的映射 */
|
||||
readonly numberIdMap: Map<number, string>;
|
||||
|
||||
/** 怪物上下文 */
|
||||
readonly enemyContext: IEnemyContext<IEnemyAttributes>;
|
||||
|
||||
/**
|
||||
* 保存状态
|
||||
*/
|
||||
|
||||
@ -161,6 +161,8 @@
|
||||
"103": "Map damage reducer is missing, reduced map damage is unavailable.",
|
||||
"104": "Enemy dirty marking failed since specific enemy is not in current context.",
|
||||
"105": "No specific map damage view stored, which seems like an internal bug of map damage system.",
|
||||
"106": "Damage calculator is missing, damage calculation is unavailable.",
|
||||
"107": "Hero status is not bound, damage calculation is unavailable.",
|
||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user