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; } /** * 详细伤害计算核心方法 * * 这是整个战斗系统中最核心的伤害计算方法,负责根据施法者属性、目标属性和技能配置 * 计算最终的基础伤害值。该方法采用多阶段的伤害计算公式,综合考虑了物理和魔法伤害 * 以及各种属性加成和抗性减免。 * * 计算流程: * 1. 获取技能配置 * 2. 计算原始物理伤害和魔法伤害 * 3. 应用防御减免 * 4. 应用物理/魔法攻击力和抗性修正 * 5. 应用元素属性加成和抗性修正 * 6. 应用最终伤害减免 * 7. 确保伤害值非负 * * @param CAttrs 施法者属性快照对象,包含所有攻击力、属性加成等战斗属性 * @param TAttrs 目标属性对象,包含所有防御力、抗性等防御属性 * @param s_uuid 技能ID,用于获取技能配置信息和伤害类型 * @returns 经过完整计算后的最终伤害值(未考虑暴击) * * @important 注意事项: * - 此方法计算的是基础伤害,暴击计算在外部处理 * - 所有除法和乘法计算后都进行取整操作,确保游戏中的伤害值为整数 * - 元素伤害只应用于魔法伤害部分 */ private dmgCount(CAttrs:any,TAttrs:any,s_uuid:number){ // 1. 获取技能配置 - 如果技能不存在,直接返回0伤害 let sConf = SkillSet[s_uuid]; if (!sConf) return 0; // 2. 计算原始物理伤害和魔法伤害 // 物理伤害基础值 = 技能物理倍率 * 施法者物理攻击力 / 100 let apBase = (sConf.ap||0)*CAttrs[Attrs.AP]/100; // 魔法伤害基础值 = 技能魔法倍率 * 施法者魔法攻击力 / 100 let mapBase = (sConf.map||0)*CAttrs[Attrs.MAP]/100; // 3. 获取目标防御属性 const def = (TAttrs[Attrs.DEF]||0); // 目标物理防御 const mdef = (TAttrs[Attrs.MDEF]||0); // 目标魔法防御 // 4. 计算防御减免系数(采用公式:防御/(防御+常数),确保防御值不会导致伤害减到0) const apRed = def / (def + FightSet.DEF_C); // 物理防御减免系数 const mapRed = mdef / (mdef + FightSet.MDEF_C); // 魔法防御减免系数 // 5. 应用防御减免到基础伤害,向下取整 let apAfter = Math.floor(apBase * (1 - apRed)); // 物理伤害 - 防御减免 let mapAfter = Math.floor(mapBase * (1 - mapRed)); // 魔法伤害 - 防御减免 // 6. 应用物理/魔法攻击力和抗性修正 // 物理伤害修正:基础伤害 * (1 + 物理攻击力加成%) * (1 - 目标物理抗性%) apAfter = this.applyPR(apAfter, CAttrs[Attrs.PHYS_POWER]||0, TAttrs[Attrs.PHYS_RES]||0); // 魔法伤害修正:基础伤害 * (1 + 魔法攻击力加成%) * (1 - 目标魔法抗性%) mapAfter = this.applyPR(mapAfter, CAttrs[Attrs.MAGIC_POWER]||0, TAttrs[Attrs.MAGIC_RES]||0); // 7. 根据技能元素类型,应用元素属性加成和抗性修正 switch (sConf.DType) { case DType.ICE: // 冰系伤害修正:魔法伤害 * (1 + 冰系攻击力加成%) * (1 - 目标冰系抗性%) mapAfter = this.applyPR(mapAfter, CAttrs[Attrs.ICE_POWER]||0, TAttrs[Attrs.ICE_RES]||0); break; case DType.FIRE: // 火系伤害修正:魔法伤害 * (1 + 火系攻击力加成%) * (1 - 目标火系抗性%) mapAfter = this.applyPR(mapAfter, CAttrs[Attrs.FIRE_POWER]||0, TAttrs[Attrs.FIRE_RES]||0); break; case DType.WIND: // 风系伤害修正:魔法伤害 * (1 + 风系攻击力加成%) * (1 - 目标风系抗性%) mapAfter = this.applyPR(mapAfter, CAttrs[Attrs.WIND_POWER]||0, TAttrs[Attrs.WIND_RES]||0); break; } // 8. 计算最终总伤害(物理伤害 + 魔法伤害) let total = apAfter + mapAfter; // 9. 应用最终伤害减免效果(如特殊天赋、buff等提供的减免) total = Math.floor(total * (1 - ((TAttrs[Attrs.DAMAGE_REDUCTION]||0)/100))); // 10. 确保伤害值非负,返回最终伤害 return Math.max(0,total); } /** * 应用攻击力加成和抗性减免的通用计算方法 * * 这是一个核心的伤害修正计算方法,用于处理各种类型的攻击力加成和抗性减免。 * 该方法采用乘法叠加的方式,分别应用攻击者的加成属性和目标的抗性属性。 * * 计算公式: * 最终值 = 基础值 × (1 + 攻击力加成% / 100) × (1 - 抗性% / 100) * * 适用场景: * - 物理攻击力加成和物理抗性减免 * - 魔法攻击力加成和魔法抗性减免 * - 元素攻击力加成和元素抗性减免 * - 其他需要同时考虑加成和减免的属性修正 * * 计算逻辑说明: * 1. 首先将百分比转换为小数形式(除以100) * 2. 应用攻击者的加成:基础值 × (1 + 加成系数) * 3. 应用目标的抗性:上一步结果 × (1 - 抗性系数) * 4. 向下取整,确保结果为整数 * * @param base 基础值(通常是经过防御减免后的伤害值) * @param power 攻击者的攻击力加成值(百分比形式,如50表示50%) * @param res 目标的抗性值(百分比形式,如30表示30%) * @returns 经过攻击力加成和抗性减免后的最终值(向下取整) * * @important 注意事项: * - 当抗性值大于100时,可能导致最终值为负数或零 * - 所有计算结果会向下取整,确保游戏中的数值为整数 * - 此方法可以被多次调用,以叠加不同类型的加成和减免 */ private applyPR(base: number, power: number, res: number): number { // 计算公式:基础值 × (1 + 攻击力加成%) × (1 - 抗性%) // 1. 将百分比转换为小数:power/100 和 res/100 // 2. 应用攻击力加成:1 + (power/100) // 3. 应用抗性减免:1 - (res/100) // 4. 最终计算并向下取整 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; } }