Files
heros/assets/script/game/hero/HeroAtkSystem.ts
panw 2dc43b5b81 refactor(HeroAtkSystem): 重构伤害计算逻辑以提高可维护性
将原有的伤害计算逻辑拆分为更清晰的步骤,引入applyPR方法统一处理伤害加成和抗性计算
根据技能类型(DType)应用对应的元素伤害计算
使用防御和魔防的百分比减免公式替代原有的固定值减免
2025-11-21 10:28:14 +08:00

466 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { FacSet } from "../common/config/GameSet";
import { Attrs } from "../common/config/HeroAttrs";
import { FightSet } from "../common/config/GameSet";
import { SkillSet, DType } from "../common/config/SkillSet";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { DamageQueueComp, DamageEvent, DamageQueueHelper } from "./DamageQueueComp";
import { smc } from "../common/SingletonModuleComp";
/** 最终伤害数据接口
* 用于封装一次攻击计算的所有结果数据
* @property damage - 最终造成的伤害值(已考虑所有加成和减免)
* @property isCrit - 是否为暴击攻击
* @property isDodge - 是否被闪避闪避时damage为0
*/
interface FinalData {
damage: number;
isCrit: boolean;
isDodge: boolean;
}
/**
* 英雄攻击系统 - 伤害处理核心系统
*
* 职责:
* 1. 处理所有伤害事件的计算和分发
* 2. 管理伤害队列的处理流程
* 3. 协调视图层的表现更新
*
* 重要概念:
* - damageEvent.Attrs: 施法者属性快照(创建技能时保存)
* - targetAttrs: 被攻击者实时属性
* - 属性来源规范:攻击判定用施法者,防御判定用被攻击者
*/
@ecs.register('HeroAtkSystem')
export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
private debugMode: boolean = false; // 是否启用调试模式
/**
* 过滤器:处理拥有伤害队列的实体
*/
filter(): ecs.IMatcher {
return ecs.allOf(HeroAttrsComp, DamageQueueComp);
}
/**
* 系统更新(每帧调用)
* 处理伤害队列中的所有伤害事件
*/
update(e: ecs.Entity): void {
if(!smc.mission.play || smc.mission.pause) return;
const model = e.get(HeroAttrsComp);
const damageQueue = e.get(DamageQueueComp);
if (!model || !damageQueue || damageQueue.isEmpty()) return;
// 标记正在处理
damageQueue.isProcessing = true;
// 处理队列中的所有伤害事件
let processedCount = 0;
while (!damageQueue.isEmpty()) {
const damageEvent = damageQueue.getNextDamageEvent();
if (!damageEvent) break;
// 处理单个伤害事件
const FDData = this.doAttack(e, damageEvent);
processedCount++;
damageQueue.processedCount++;
if (this.debugMode) {
const casterName = damageEvent.caster?.ent?.get(HeroAttrsComp)?.hero_name || "未知";
const casterUuid = damageEvent.caster?.ent?.get(HeroAttrsComp)?.hero_uuid || 0;
console.log(`[HeroAtkSystem] 英雄${model.hero_name} (uuid: ${model.hero_uuid}) 受到 ${casterName}(uuid: ${casterUuid})的 伤害 ${FDData.damage},${FDData.isCrit?"暴击":"普通"}攻击技能ID ${damageEvent.s_uuid}`);
}
// 如果目标已死亡,停止处理后续伤害
if (model.is_dead) {
if (this.debugMode) {
console.log(`[HeroAtkSystem] ${model.hero_name} 已死亡,停止处理剩余伤害`);
}
damageQueue.clear(); // 清空剩余伤害
break;
}
}
// 如果队列已空,移除伤害队列组件
if (damageQueue.isEmpty()) {
e.remove(DamageQueueComp);
if (this.debugMode && processedCount > 0) {
console.log(`[HeroAtkSystem] ${model.hero_name} 伤害队列处理完成,共处理 ${processedCount} 个伤害事件`);
}
}
}
/**
* 执行攻击计算 - 核心伤害计算逻辑
*
* 属性使用规范(重要!):
*
* ✅ 正确使用施法者属性damageEvent.Attrs - 快照):
* - CRITICAL: 暴击率判定
* - CRITICAL_DMG: 暴击伤害加成
* - BACK_CHANCE: 击退概率
* - HIT: 命中率(用于闪避计算)
* - AP/MAP: 攻击力(基础伤害计算)
*
* ✅ 正确使用被攻击者属性targetAttrs - 实时):
* - DODGE: 闪避率(用于闪避计算)
* - SHIELD_MAX: 护盾最大值(护盾吸收)
* - hp: 当前生命值(伤害应用)
* - 各种抗性属性(预留扩展)
*
* ❌ 错误使用案例(已修复):
* - 不要混用施法者和被攻击者的属性进行同一计算
* - 暴击伤害应该基于施法者的暴击伤害属性
*
* @param target 目标实体(被攻击者)
* @param damageEvent 伤害事件数据(包含施法者信息和属性快照)
* @returns 最终伤害数据(包含伤害值、暴击标记、闪避标记)
*/
private doAttack(target: ecs.Entity, damageEvent: DamageEvent): FinalData {
const targetAttrs = target.get(HeroAttrsComp);
const targetView = target.get(HeroViewComp);
let reDate:FinalData={
damage:0,
isCrit:false,
isDodge:false,
}
if (!targetAttrs || targetAttrs.is_dead) return reDate;
const caster = damageEvent.caster;
const attackerModel = caster?.ent?.get(HeroAttrsComp);
// 获取技能配置
const skillConf = SkillSet[damageEvent.s_uuid];
if (!skillConf) return reDate;
// 触发被攻击事件
this.onAttacked(target);
// 闪避判定
// 闪避成功概率 = 被攻击者闪避率 - 施法者命中率
// targetAttrs.Attrs[Attrs.DODGE]: 被攻击者的实时闪避属性
// damageEvent.Attrs[Attrs.HIT]: 施法者在技能创建时的命中属性快照
const isDodge =this.checkChance((targetAttrs.Attrs[Attrs.DODGE]-damageEvent.Attrs[Attrs.HIT]) || 0);
if (isDodge) {
// TODO: 触发闪避视图表现
reDate.isDodge=true;
return reDate;
}
// 暴击判定
// 使用施法者的暴击率属性damageEvent.Attrs 快照)
const isCrit = this.checkChance(damageEvent.Attrs[Attrs.CRITICAL]);
if (isCrit) attackerModel?.clearTalBuffByAttr(Attrs.CRITICAL);
// 计算伤害
let damage = this.dmgCount(damageEvent.Attrs,targetAttrs.Attrs,damageEvent.s_uuid);
if (isCrit) {
// 暴击伤害计算
// 使用施法者的暴击伤害加成属性damageEvent.Attrs 快照)
// 公式:最终伤害 = 基础伤害 * (1 + 系统暴击倍率 + 施法者暴击伤害加成)
const casterCritDmg = damageEvent.Attrs[Attrs.CRITICAL_DMG] || 0;
damage = Math.floor(damage * (1 + (FightSet.CRIT_DAMAGE + casterCritDmg) / 100));
reDate.isCrit=true;
}
// 伤害计算考虑易伤等debuff
damage = this.calculateDamage(targetAttrs, damage);
// 护盾吸收
damage =Math.floor(this.absorbShield(targetAttrs, damage))
if (damage <= 0) return reDate;
// 应用伤害到数据层
targetAttrs.hp -= damage;
targetAttrs.atked_count++;
// 击退判定
// 使用施法者的击退概率属性damageEvent.Attrs 快照)
// 击退成功后需要清理施法者的相关天赋buff
const isBack = this.checkChance(damageEvent.Attrs[Attrs.BACK_CHANCE] || 0);
if (isBack) attackerModel?.clearTalBuffByAttr(Attrs.BACK_CHANCE);
// ✅ 触发视图层表现(伤害数字、受击动画、后退)
if (targetView) targetView.do_atked(damage, isCrit, damageEvent.s_uuid, isBack);
// 检查死亡
if (targetAttrs.hp <= 0) {
this.doDead(target);
// ✅ 触发死亡视图表现
if (targetView) {
targetView.do_dead();
}
}
if (this.debugMode) {
console.log(`[HeroAtkSystem] ${targetAttrs.hero_name} 受到 ${damage} 点伤害 (暴击: ${isCrit})`);
}
reDate.damage=damage;
return reDate;
}
/**
* 基础伤害计算
* 根据技能配置和施法者属性计算基础伤害值
*
* @param CAttrs 施法者属性快照(包含攻击力等关键属性)
* @param s_uuid 技能ID用于获取技能配置
* @returns 基础伤害值(未考虑暴击、抗性等因素)
*
* @todo 后续可以扩展更多伤害计算公式:
* - 技能倍率加成
* - 元素伤害计算
* - 真实伤害/魔法伤害/物理伤害区分
*/
private dmgCount(CAttrs:any,TAttrs:any,s_uuid:number){
let sConf = SkillSet[s_uuid];
if (!sConf) return 0;
let apBase = (sConf.ap||0)*CAttrs[Attrs.AP]/100;
let mapBase = (sConf.map||0)*CAttrs[Attrs.MAP]/100;
const def = (TAttrs[Attrs.DEF]||0);
const mdef = (TAttrs[Attrs.MDEF]||0);
const apRed = def / (def + FightSet.DEF_C);
const mapRed = mdef / (mdef + FightSet.MDEF_C);
let apAfter = Math.floor(apBase * (1 - apRed));
let mapAfter = Math.floor(mapBase * (1 - mapRed));
apAfter = this.applyPR(apAfter, CAttrs[Attrs.PHYS_POWER]||0, TAttrs[Attrs.PHYS_RES]||0);
mapAfter = this.applyPR(mapAfter, CAttrs[Attrs.MAGIC_POWER]||0, TAttrs[Attrs.MAGIC_RES]||0);
switch (sConf.DType) {
case DType.ICE:
mapAfter = this.applyPR(mapAfter, CAttrs[Attrs.ICE_POWER]||0, TAttrs[Attrs.ICE_RES]||0);
break;
case DType.FIRE:
mapAfter = this.applyPR(mapAfter, CAttrs[Attrs.FIRE_POWER]||0, TAttrs[Attrs.FIRE_RES]||0);
break;
case DType.WIND:
mapAfter = this.applyPR(mapAfter, CAttrs[Attrs.WIND_POWER]||0, TAttrs[Attrs.WIND_RES]||0);
break;
}
let total = apAfter + mapAfter;
total = Math.floor(total * (1 - ((TAttrs[Attrs.DAMAGE_REDUCTION]||0)/100)));
return Math.max(0,total);
}
private applyPR(base: number, power: number, res: number): number {
return Math.floor(base * (1 + (power/100)) * (1 - (res/100)));
}
/**
* 处理角色死亡
*
* 死亡处理流程:
* 1. 标记死亡状态is_dead = true
* 2. 触发死亡事件onDeath
* 3. 记录调试信息(如启用调试模式)
*
* @param entity 死亡的实体
*
* @important 死亡状态一旦设置,该实体将不再处理新的伤害事件
* 这确保了死亡逻辑的单一性和一致性
*/
private doDead(entity: ecs.Entity): void {
const model = entity.get(HeroAttrsComp);
if (!model || model.is_dead) return;
model.is_dead = true;
// 触发死亡事件
this.onDeath(entity);
if (this.debugMode) {
console.log(`[HeroAtkSystem] ${model.hero_name} 死亡`);
}
}
/**
* 统一概率判定方法
*
* 用于所有概率相关的判定:
* - 暴击率判定
* - 闪避率判定
* - 击退概率判定
* - 其他特殊效果概率
*
* @param rate 概率值0-100的百分比
* @returns true-判定成功false-判定失败
*
* @example
* ```typescript
* // 10%概率触发
* if (this.checkChance(10)) {
* // 触发特殊效果
* }
* ```
*
* @important 概率为0或负数时直接返回false避免不必要的随机数计算
*/
private checkChance(rate: number): boolean {
if (rate <= 0) return false;
const r = Math.random() * 100;
return r < rate;
}
/**
* 伤害计算考虑易伤等debuff
*
* 预留的伤害计算扩展点,用于处理:
* - 被攻击者的易伤状态(增加受到伤害)
* - 被攻击者的伤害减免状态(减少受到伤害)
* - 元素抗性计算
* - 真实伤害/魔法伤害/物理伤害的类型区分
*
* @param model 被攻击者的属性组件(包含抗性、易伤等状态)
* @param baseDamage 基础伤害值
* @returns 最终伤害值(经过各种加成和减免后的结果)
*/
private calculateDamage(model: HeroAttrsComp, baseDamage: number): number {
// 这里可以添加易伤等debuff的计算逻辑
// 例如如果目标有易伤buff增加受到的伤害
return baseDamage;
}
/**
* 护盾吸收伤害
*
* 护盾吸收逻辑:
* 1. 如果护盾值 >= 伤害值完全吸收剩余伤害为0
* 2. 如果护盾值 < 伤害值:部分吸收,剩余伤害 = 原伤害 - 护盾值
* 3. 护盾被击破时,重置护盾最大值属性
*
* @param model 被攻击者的属性组件(包含当前护盾值)
* @param damage 原始伤害值
* @returns 剩余伤害值(护盾吸收后的结果)
*/
private absorbShield(model: HeroAttrsComp, damage: number): number {
if (model.shield <= 0) return damage;
if (model.shield >= damage) {
model.shield -= damage;
if (model.shield <= 0) {
model.shield = 0;
model.Attrs[Attrs.SHIELD_MAX] = 0;
}
return 0;
} else {
const remainingDamage = damage - model.shield;
model.shield = 0;
model.Attrs[Attrs.SHIELD_MAX] = 0;
return remainingDamage;
}
}
/**
* 被攻击时触发的事件
*
* 预留的扩展点,用于处理被攻击时的特殊逻辑:
* - 触发反伤效果(荆棘光环等)
* - 触发被攻击天赋(如受击回血、受击反击等)
* - 触发特殊状态(如受伤狂暴、受伤护盾等)
*
* @param entity 被攻击的实体
*
* @todo 当前对怪物实体直接返回,后续可以根据需求扩展怪物的被攻击逻辑
*/
private onAttacked(entity: ecs.Entity): void {
const model = entity.get(HeroAttrsComp);
if (!model || model.is_dead) return;
// 这里可以添加被攻击时的特殊处理逻辑
if (model.fac === FacSet.MON) return;
// 例如:触发某些天赋效果、反击逻辑等
}
/**
* 死亡时触发的事件
*
* 根据实体阵营类型处理不同的死亡逻辑:
*
* - FacSet.MON怪物触发掉落逻辑
* - 延迟执行掉落,避免阻塞主逻辑
* - 可以扩展:经验值计算、任务进度等
*
* - FacSet.HERO英雄触发英雄死亡特殊处理
* - 游戏结束判定
* - 复活机制检查
* - 死亡惩罚/奖励
*
* @param entity 死亡的实体
*
* @important 死亡事件应该幂等,避免重复触发
*/
private onDeath(entity: ecs.Entity): void {
const model = entity.get(HeroAttrsComp);
if (!model) return;
if (model.fac === FacSet.MON) {
// 怪物死亡处理
this.scheduleDrop(entity);
} else if (model.fac === FacSet.HERO) {
// 英雄死亡处理
this.scheduleHeroDeath(entity);
}
}
/**
* 延迟执行掉落逻辑
*
* 采用延迟执行的原因:
* 1. 避免在伤害计算过程中阻塞主线程
* 2. 给死亡动画播放留出时间
* 3. 可以批量处理多个掉落,优化性能
*
* @param entity 死亡的怪物实体
*
* @todo 具体实现可以包括:
* - 根据怪物等级计算基础掉落
* - 幸运值影响掉落品质
* - 特殊事件(双倍掉落、稀有掉落等)
* - 掉落物在场景中的生成位置计算
*/
private scheduleDrop(entity: ecs.Entity): void {
// 这里可以添加掉落逻辑
// 例如:延迟一段时间后生成掉落物品
}
/**
* 延迟执行英雄死亡逻辑
*
* 英雄死亡的特殊处理,比普通怪物复杂:
*
* 处理内容包括:
* - 检查复活次数和复活条件
* - 触发游戏结束界面(如适用)
* - 记录死亡统计信息
* - 处理死亡惩罚(经验损失、装备损坏等)
*
* @param entity 死亡的英雄实体
*
* @important 英雄死亡通常需要玩家交互,所以必须延迟处理
* 给玩家足够的反馈时间和操作空间
*/
private scheduleHeroDeath(entity: ecs.Entity): void {
// 这里可以添加英雄死亡的特殊处理
// 例如:触发游戏结束、复活机制等
}
/**
* 启用调试模式
*/
enableDebug() {
this.debugMode = true;
}
/**
* 禁用调试模式
*/
disableDebug() {
this.debugMode = false;
}
}