feat: 伤害计算系统

This commit is contained in:
unanmed 2026-04-15 18:26:25 +08:00
parent 4c0960fff7
commit 0517da5000
8 changed files with 404 additions and 5 deletions

View File

@ -3,6 +3,7 @@ import { ITileLocator } from '@user/types';
import { import {
IAuraConverter, IAuraConverter,
IAuraView, IAuraView,
IDamageSystem,
IEnemy, IEnemy,
IEnemyAuraView, IEnemyAuraView,
IEnemyCommonQueryEffect, IEnemyCommonQueryEffect,
@ -53,6 +54,7 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
private readonly dirtyEnemy: Set<IEnemyView<TAttr>> = new Set(); private readonly dirtyEnemy: Set<IEnemyView<TAttr>> = new Set();
private mapDamage: IMapDamage<TAttr> | null = null; private mapDamage: IMapDamage<TAttr> | null = null;
private damageSystem: IDamageSystem<TAttr, unknown> | null = null;
readonly indexer: MapLocIndexer = new MapLocIndexer(); readonly indexer: MapLocIndexer = new MapLocIndexer();
private needUpdate: boolean = true; private needUpdate: boolean = true;
@ -174,6 +176,9 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
if (this.mapDamage) { if (this.mapDamage) {
this.mapDamage.deleteEnemy(view); this.mapDamage.deleteEnemy(view);
} }
if (this.damageSystem) {
this.damageSystem.deleteEnemy(view);
}
this.needTotallyRefresh.delete(view); this.needTotallyRefresh.delete(view);
this.dirtyEnemy.delete(view); this.dirtyEnemy.delete(view);
@ -253,6 +258,15 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
return this.mapDamage; 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( private convertSpecial(
special: ISpecial<any>, special: ISpecial<any>,
enemy: IReadonlyEnemy<TAttr>, enemy: IReadonlyEnemy<TAttr>,
@ -545,6 +559,10 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
this.buildupFinal(); this.buildupFinal();
} }
if (this.damageSystem) {
this.damageSystem.markAllDirty();
}
if (this.mapDamage) { if (this.mapDamage) {
this.mapDamage.refreshAll(); this.mapDamage.refreshAll();
} }
@ -553,6 +571,9 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
markDirty(view: IEnemyView<TAttr>): void { markDirty(view: IEnemyView<TAttr>): void {
if (!this.locatorViewMap.has(view)) return; if (!this.locatorViewMap.has(view)) return;
this.dirtyEnemy.add(view); this.dirtyEnemy.add(view);
if (this.damageSystem) {
this.damageSystem.markDirty(view);
}
} }
private refreshSpecialModifier( private refreshSpecialModifier(
@ -687,6 +708,10 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
this.dirtyEnemy.delete(view); this.dirtyEnemy.delete(view);
if (this.damageSystem) {
this.damageSystem.markDirty(view);
}
if (this.mapDamage) { if (this.mapDamage) {
this.mapDamage.markEnemyDirty(view); this.mapDamage.markEnemyDirty(view);
} }
@ -721,6 +746,9 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
this.needTotallyRefresh.clear(); this.needTotallyRefresh.clear();
this.requestedCommonContext.clear(); this.requestedCommonContext.clear();
this.dirtyEnemy.clear(); this.dirtyEnemy.clear();
if (this.damageSystem) {
this.damageSystem.markAllDirty();
}
if (this.mapDamage) { if (this.mapDamage) {
this.mapDamage.refreshAll(); this.mapDamage.refreshAll();
} }
@ -729,6 +757,7 @@ export class EnemyContext<TAttr> implements IEnemyContext<TAttr> {
destroy(): void { destroy(): void {
this.clear(); this.clear();
this.attachMapDamage(null); this.attachMapDamage(null);
this.damageSystem = null;
this.auraConverter.clear(); this.auraConverter.clear();
this.commonQueryMap.clear(); this.commonQueryMap.clear();
this.specialQueryEffects.clear(); this.specialQueryEffects.clear();

View 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
};
}
}

View File

@ -1,5 +1,6 @@
export * from './enemy'; export * from './enemy';
export * from './context'; export * from './context';
export * from './damage';
export * from './mapDamage'; export * from './mapDamage';
export * from './manager'; export * from './manager';
export * from './special'; export * from './special';

View File

@ -60,10 +60,12 @@ export class MapDamage<TAttr> implements IMapDamage<TAttr> {
/** 合并后伤害缓存,索引 -> 合并结果 */ /** 合并后伤害缓存,索引 -> 合并结果 */
private readonly reducedCache: Map<number, IMapDamageInfo> = new Map(); private readonly reducedCache: Map<number, IMapDamageInfo> = new Map();
constructor( /** 坐标索引对象 */
readonly context: IEnemyContext<TAttr>, private readonly indexer: IMapLocIndexer;
readonly indexer: IMapLocIndexer
) {} constructor(readonly context: IEnemyContext<TAttr>) {
this.indexer = context.indexer;
}
useConverter(converter: IMapDamageConverter<TAttr>): void { useConverter(converter: IMapDamageConverter<TAttr>): void {
this.converter = converter; this.converter = converter;

View File

@ -540,6 +540,117 @@ export interface IMapDamage<TAttr> {
//#endregion //#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 上下文 //#region 上下文
export interface IEnemyContext<TAttr> { export interface IEnemyContext<TAttr> {
@ -547,6 +658,8 @@ export interface IEnemyContext<TAttr> {
readonly width: number; readonly width: number;
/** 怪物上下文高度 */ /** 怪物上下文高度 */
readonly height: number; readonly height: number;
/** 此上下文使用的索引对象 */
readonly indexer: IMapLocIndexer;
/** /**
* *
@ -702,6 +815,17 @@ export interface IEnemyContext<TAttr> {
*/ */
getMapDamage(): IMapDamage<TAttr> | null; getMapDamage(): IMapDamage<TAttr> | null;
/**
*
* @param system
*/
attachDamageSystem(system: IDamageSystem<TAttr, unknown>): void;
/**
*
*/
getDamageSystem<THero>(): IDamageSystem<TAttr, THero> | null;
/** /**
* *
* *

View File

@ -3,6 +3,19 @@ import { IHeroState, HeroState } from './hero';
import { ILayerState, LayerState } from './map'; import { ILayerState, LayerState } from './map';
import { IRoleFaceBinder, RoleFaceBinder } from './common'; import { IRoleFaceBinder, RoleFaceBinder } from './common';
import { GameDataState } from './data'; 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 { export class CoreState implements ICoreState {
readonly layer: ILayerState; readonly layer: ILayerState;
@ -11,6 +24,7 @@ export class CoreState implements ICoreState {
readonly data: IGameDataState; readonly data: IGameDataState;
readonly idNumberMap: Map<string, number>; readonly idNumberMap: Map<string, number>;
readonly numberIdMap: Map<number, string>; readonly numberIdMap: Map<number, string>;
readonly enemyContext: IEnemyContext<IEnemyAttributes>;
constructor() { constructor() {
this.layer = new LayerState(); this.layer = new LayerState();
@ -19,6 +33,19 @@ export class CoreState implements ICoreState {
this.idNumberMap = new Map(); this.idNumberMap = new Map();
this.numberIdMap = new Map(); this.numberIdMap = new Map();
this.data = new GameDataState(); 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 { saveState(): IStateSaveData {

View File

@ -1,7 +1,7 @@
import { ILayerState } from './map'; import { ILayerState } from './map';
import { IHeroFollower, IHeroState } from './hero'; import { IHeroFollower, IHeroState } from './hero';
import { IRoleFaceBinder } from './common'; import { IRoleFaceBinder } from './common';
import { IEnemyManager } from '@user/data-base'; import { IEnemyContext, IEnemyManager } from '@user/data-base';
import { IEnemyAttributes } from './enemy/types'; import { IEnemyAttributes } from './enemy/types';
export interface IGameDataState { export interface IGameDataState {
@ -28,6 +28,9 @@ export interface ICoreState {
/** 图块数字到 id 的映射 */ /** 图块数字到 id 的映射 */
readonly numberIdMap: Map<number, string>; readonly numberIdMap: Map<number, string>;
/** 怪物上下文 */
readonly enemyContext: IEnemyContext<IEnemyAttributes>;
/** /**
* *
*/ */

View File

@ -161,6 +161,8 @@
"103": "Map damage reducer is missing, reduced map damage is unavailable.", "103": "Map damage reducer is missing, reduced map damage is unavailable.",
"104": "Enemy dirty marking failed since specific enemy is not in current context.", "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.", "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." "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
} }
} }