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 } from "../common/config/SkillSet"; import { HeroAttrsComp } from "./HeroAttrsComp"; import { HeroViewComp } from "./HeroViewComp"; import { DamageQueueComp, DamageEvent } from "./DamageQueueComp"; import { smc } from "../common/SingletonModuleComp"; import { HeroInfo } from "../common/config/heroSet"; import { oops } from "db://oops-framework/core/Oops"; import { GameEvent } from "../common/config/GameEvent"; import { SkillTriggerType } from "../common/config/heroSet"; import { FieldSkillType } from "../common/config/SkillSet"; import { mLogger } from "../common/Logger"; import { SkillTriggerHelper } from "./SkillTriggerHelper"; import { getMonsterGoldDrop } from "../map/RogueConfig"; import { MissionEconomy } from "../map/MissionEconomy"; /** 最终伤害数据接口 * 用于封装一次攻击计算的所有结果数据 * @property damage - 最终造成的伤害值(已考虑所有加成和减免) * @property isCrit - 是否为暴击攻击 */ interface FinalData { damage: number; isCrit: boolean; } /** * 英雄攻击系统 - 伤害处理核心系统 * * 职责: * 1. 处理所有伤害事件的计算和分发 * 2. 管理伤害队列的处理流程 * 3. 协调视图层的表现更新 * * 重要概念: * - damageEvent.Attrs: 施法者属性快照(创建技能时保存) * - TAttrsComp: 被攻击者实时属性 * - 属性来源规范:攻击判定用施法者,防御判定用被攻击者 */ @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) return if (smc.mission.pause) return const TAttrsComp = e.get(HeroAttrsComp) const damageQueue = e.get(DamageQueueComp) if (!TAttrsComp || !damageQueue || damageQueue.isEmpty()) return; // 标记正在处理 damageQueue.isProcessing = true; // 处理队列中的所有伤害事件 let processedCount = 0; while (!damageQueue.isEmpty()) { const damageEvent = damageQueue.getNextDamageEvent(); if (!damageEvent) break; // 处理单个伤害事件 this.doAttack(e, damageEvent); processedCount++; damageQueue.processedCount++; // 如果目标已死亡,停止处理后续伤害 if (TAttrsComp.is_dead) { mLogger.log(this.debugMode, 'HeroAtkSystem', ` ${TAttrsComp.hero_name} 已死亡,停止处理剩余伤害`); damageQueue.clear(); // 清空剩余伤害 break; } } // 如果队列已空,移除伤害队列组件 if (damageQueue.isEmpty()) { e.remove(DamageQueueComp); if (processedCount > 0) { mLogger.log(this.debugMode, 'HeroAtkSystem', ` ${TAttrsComp.hero_name} 伤害队列处理完成,共处理 ${processedCount} 个伤害事件`); } } } /** 检查并触发受击附加技能 (atked) */ private checkAndTriggerAtkedSkills(heroAttrs: HeroAttrsComp, heroView: HeroViewComp) { SkillTriggerHelper.trigger(SkillTriggerType.Atked, heroAttrs, heroView); } /** * 执行攻击计算 - 核心伤害计算逻辑 * * @param target 目标实体(被攻击者) * @param damageEvent 伤害事件数据(包含施法者信息和属性快照) * @returns 最终伤害数据(包含伤害值、暴击标记、闪避标记) */ private doAttack(target: ecs.Entity, damageEvent: DamageEvent): FinalData { const TAttrsComp = target.get(HeroAttrsComp); const targetView = target.get(HeroViewComp); let reDate: FinalData = { damage: 0, isCrit: false, } if (!TAttrsComp || TAttrsComp.is_dead || TAttrsComp.is_reviving) return reDate; const casterEid = damageEvent.casterEid; // 获取技能配置 const skillConf = SkillSet[damageEvent.s_uuid]; if (!skillConf) return reDate; // 触发被攻击事件 this.onAttacked(target); // 暴击判定 // 使用施法者的暴击率属性(damageEvent.Attrs 快照),- 被攻击者的暴击抗性属 const criticalChance = (damageEvent.Attrs[Attrs.critical] || 0) - TAttrsComp.getFinalCriticalRes(); const isCrit = this.checkChance(criticalChance); // 计算基础伤害 let damage = this.dmgCount(damageEvent, TAttrsComp); mLogger.log(this.debugMode, 'HeroAtkSystem', " dmgCount", damage) if (isCrit) { const critDamageBonus = damageEvent.Attrs[Attrs.critical_damage] || 0; damage = Math.floor(damage * (1 + (FightSet.CRIT_DAMAGE + critDamageBonus) / 100)); reDate.isCrit = true; if (damageEvent.Attrs.fac === FacSet.HERO) { // 【评分系统 - 输出分】统计暴击次数与暴击造成的总伤害 smc.vmdata.scores.crt_count++; smc.vmdata.scores.crit_dmg_total += damage; } } mLogger.log(this.debugMode, 'HeroAtkSystem', " after crit", damage) // 护盾吸收 const shieldResult = this.absorbShield(TAttrsComp, damage); damage = shieldResult.remainingDamage; if (shieldResult.absorbedDamage > 0) { // 【评分系统 - 防御分】统计护盾成功抵挡伤害的次数 smc.vmdata.scores.shield_block_count += 1; } mLogger.log(this.debugMode, 'HeroAtkSystem', " after shield", damage) // 显示护盾格挡飘字 if (shieldResult.absorbedDamage > 0 && targetView) { targetView.shield_tip(shieldResult.absorbedDamage); } if (damage <= 0) return reDate; // TAttrsComp.hp -= damage; // 应用伤害到数据层 TAttrsComp.add_hp(-damage); // 使用 add_hp 以触发 dirty_hp 和 UI 更新 if (damageEvent.Attrs.fac === FacSet.HERO) { // 【评分系统 - 输出分】统计团队造成的总伤害以及单次最高伤害记录 smc.vmdata.scores.total_dmg += damage; if (damage > smc.vmdata.scores.highest_dmg) { smc.vmdata.scores.highest_dmg = damage; } } // 受击计数:只有实际造成伤害才 +1(被格挡不计数) TAttrsComp.atked_count++; this.checkAndTriggerAtkedSkills(TAttrsComp, targetView); // 受击者产生击退效果 // if (damage > 0 && targetView) { // targetView.back(); // } mLogger.log(this.debugMode, 'HeroAtkSystem', ` 英雄${TAttrsComp.hero_name} (uuid: ${TAttrsComp.hero_uuid}) 受到 eid:${casterEid} 的 伤害 ${damage},${isCrit ? "暴击" : "普通"}攻击,技能ID ${damageEvent.s_uuid}`); // 冰冻判定 const freezeChance = (damageEvent.Attrs[Attrs.freeze_chance] || 0) - (TAttrsComp.freeze_res || 0); const isFrost = !TAttrsComp.isFrost() && this.checkChance(freezeChance); // 击晕判定 const stunChance = (damageEvent.Attrs[Attrs.stun_chance] || 0) - TAttrsComp.getFinalStunRes(); const isStun = !TAttrsComp.isStun() && this.checkChance(stunChance); // 击退判定 const knockbackChance = (damageEvent.Attrs[Attrs.knockback_chance] || 0) - (TAttrsComp.knockback_res || 0); const isKnockback = this.checkChance(knockbackChance); // ✅ 触发视图层表现(伤害数字、受击动画、冰冻、击晕、击退) if (targetView) { targetView.do_atked(damage, isCrit, damageEvent.s_uuid, false); targetView.playEnd(skillConf.endAnm); if (isFrost) { TAttrsComp.toFrost(); targetView.in_iced(FightSet.FROST_TIME); if (damageEvent.Attrs.fac === FacSet.HERO) { smc.vmdata.scores.freeze_count++; } } if (isStun) { TAttrsComp.toStun(); targetView.in_stun(FightSet.STUN_TIME); if (damageEvent.Attrs.fac === FacSet.HERO) { smc.vmdata.scores.stun_count++; } } if (isKnockback) { targetView.back(damageEvent.Attrs[Attrs.knockback_distance] || 0); } } // 检查死亡 if (TAttrsComp.hp <= 0) { // 先触发死亡技能(如亡语),不管后续是否续命都应触发 this.triggerDeadSkills(target); // 濒死回血(revive 系):不是真正复活,而是血量归零瞬间消耗 1 次续命次数, // 按比例回血继续战斗。次数每回合重置(见 MissionComp.healAllHeroes)。 let canRevive = false; let maxReviveCount = 0; let reviveHpPercent = 50; // 默认恢复50% let reviveSkillConf: any = null; // 提取出来的复活技能配置 if (TAttrsComp.runtime_revive) { const reviveConf = TAttrsComp.runtime_revive; maxReviveCount = reviveConf.r_num + Math.floor((TAttrsComp.lv - 1) * reviveConf.upr); if (TAttrsComp.revived_count < maxReviveCount) { canRevive = true; // 从配置表中读取对应的复活技能配置,ap 字段代表恢复生命值的百分比 reviveSkillConf = SkillSet[reviveConf.s_uuid]; if (reviveSkillConf && reviveSkillConf.ap) { reviveHpPercent = reviveSkillConf.ap; } } } if (canRevive) { TAttrsComp.revived_count++; TAttrsComp.is_reviving = true; // 标记为正在复活 // 根据技能配置恢复生命值 TAttrsComp.hp = Math.floor(TAttrsComp.hp_max * (reviveHpPercent / 100)); TAttrsComp.dirty_hp = true; // 触发复活动画 if (targetView && reviveSkillConf) { targetView.playReady(reviveSkillConf.readyAnm); targetView.skill_name('', reviveSkillConf.uuid, SkillTriggerType.Revive); // 延迟 0.5 秒后恢复状态,让特效有时间播放,同时也能体现“正在复活”的过程 targetView.scheduleOnce(() => { if (targetView.node && targetView.node.isValid) { targetView.alive(); } }, 0.5); } else if (targetView) { targetView.alive(); } mLogger.log(this.debugMode, 'HeroAtkSystem', ` Hero waiting to revive! Lives revived: ${TAttrsComp.revived_count}/${maxReviveCount}, Hp restored: ${reviveHpPercent}%`); return reDate; } this.doDead(target); // ✅ 触发死亡视图表现 if (targetView) { targetView.do_dead(); } } mLogger.log(this.debugMode, 'HeroAtkSystem', ` ${TAttrsComp.hero_name} 受到 ${damage} 点伤害 (暴击: ${isCrit})`); reDate.damage = damage; return reDate; } /** * 详细伤害计算核心方法 * * 计算流程: * 1. 获取技能配置 * 2. 计算原始物理伤害 * 3. 应用最终伤害减免 * 4. 确保伤害值非负 * * @param CAttrs 施法者属性快照对象,包含所有攻击力、属性加成等战斗属性 * @param TAttrs 目标属性对象,包含所有防御力、抗性等防御属性 * @param s_uuid 技能ID,用于获取技能配置信息和伤害类型 * @returns 经过完整计算后的最终伤害值(未考虑暴击) * * @important 注意事项: */ private dmgCount(damageEvent: DamageEvent, TAttrsComp: HeroAttrsComp) { // 1. 获取技能配置 - 如果技能不存在,直接返回0伤害 const CAttrs = damageEvent.Attrs; const TAttrs = TAttrsComp; let sConf = SkillSet[damageEvent.s_uuid]; if (!sConf) return 0; mLogger.log(this.debugMode, 'HeroAtkSystem', ` 伤害处理对象`, CAttrs, TAttrs); // 2. 计算原始物理伤害和魔法伤害 // 物理伤害基础值 = 技能物理倍率 * (施法者物理攻击力 + 额外伤害) / 100 * 额外伤害比例 let apBase = (sConf.ap || 0) * (CAttrs[Attrs.ap] + damageEvent.ext_dmg) / 100 * damageEvent.dmg_ratio; mLogger.log(this.debugMode, 'HeroAtkSystem', ` 物理伤害基础值: ${apBase}, 技能ap=${sConf.ap},施法者物理攻击力: ${CAttrs[Attrs.ap]},} 额外伤害:${damageEvent.ext_dmg}, 额外伤害比例:${damageEvent.dmg_ratio}`); // 4. 确保伤害值非负 let total = Math.max(0, apBase); if (this.debugMode) mLogger.log(this.debugMode, 'HeroAtkSystem', ` 最终伤害: ${total} (Base: ${apBase}`); return total; } /** * 触发死亡技能(如亡语) * 不管是否复活,只要血量归零都触发 */ private triggerDeadSkills(entity: ecs.Entity): void { const TAttrsComp = entity.get(HeroAttrsComp); if (!TAttrsComp) return; const view = entity.get(HeroViewComp); if (view) { SkillTriggerHelper.trigger(SkillTriggerType.Dead, TAttrsComp, view); // 【新增】仅英雄阵营派发全局死亡事件(怪物死亡会误触发海量卡牌效果) if (TAttrsComp.fac === FacSet.HERO) { oops.message.dispatchEvent(GameEvent.HeroDead, { eid: entity.eid }); } } } /** * 处理角色死亡 * * 死亡处理流程: * 1. 标记死亡状态(is_dead = true) * 2. 英雄阵营:快照死亡状态到 smc.mission.dead_heroes(供后续复活机制重新召唤) * 3. 触发死亡事件(onDeath) * * @param entity 死亡的实体 * * @important 死亡状态一旦设置,该实体将不再处理新的伤害事件 * 英雄为真实死亡:视图动画结束后由 HeroViewComp.realDead 销毁实体 */ private doDead(entity: ecs.Entity): void { const TAttrsComp = entity.get(HeroAttrsComp); if (!TAttrsComp || TAttrsComp.is_dead) return; TAttrsComp.is_dead = true; // 英雄真实死亡:记录快照,供后续复活机制(重新召唤)消费 if (TAttrsComp.fac === FacSet.HERO) { this.recordDeadHero(TAttrsComp); } // 触发死亡事件 this.onDeath(entity); if (this.debugMode) { mLogger.log(this.debugMode, 'HeroAtkSystem', ` ${TAttrsComp.hero_name} 死亡`); } } /** * 快照死亡英雄的静态/成长数据到 smc.mission.dead_heroes。 * 仅记录重新召唤所需字段,不含战斗内临时状态(buff、技能 CD 等)。 * * @param attrs 死亡英雄的属性组件 */ private recordDeadHero(attrs: HeroAttrsComp): void { smc.mission.dead_heroes.push({ uuid: attrs.hero_uuid, lv: attrs.lv, card_lv: attrs.card_lv, posIndex: attrs.posIndex, wave: Math.max(1, smc.vmdata?.mission_data?.level ?? 1), }); mLogger.log(this.debugMode, 'HeroAtkSystem', ` 死亡英雄已记录: ${attrs.hero_name}(uuid=${attrs.hero_uuid}) lv=${attrs.lv} pos=${attrs.posIndex} wave=${smc.vmdata?.mission_data?.level}, 当前共 ${smc.mission.dead_heroes.length} 个`); } /** * 统一概率判定方法 * * 用于所有概率相关的判定: * - 暴击率判定 * - 闪避率判定 * - 击退概率判定 * - 其他特殊效果概率 * * @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; if (rate >= 70) rate = 70 const r = Math.random() * 100; return r < rate; } /** * 护盾吸收伤害 * * 护盾吸收逻辑: * 1. 护盾值大于 0 时,抵挡本次伤害(免伤) * 2. 每次抵挡消耗 1 点护盾值 * 3. 护盾归零时,重置护盾最大值属性 * * @param TAttrsComp 被攻击者的属性组件(包含当前护盾值) * @param damage 原始伤害值 * @returns {remainingDamage, absorbedDamage} 剩余伤害值和被格挡的伤害值 */ private absorbShield(TAttrsComp: HeroAttrsComp, damage: number): { remainingDamage: number, absorbedDamage: number } { if (TAttrsComp.shield <= 0) { mLogger.log(this.debugMode, 'HeroAtkSystem', " 护盾值小于等于0,无法吸收伤害"); return { remainingDamage: damage, absorbedDamage: 0 }; } TAttrsComp.shield = Math.max(0, TAttrsComp.shield - 1); if (TAttrsComp.shield <= 0) { TAttrsComp.shield = 0; } TAttrsComp.dirty_shield = true; mLogger.log(this.debugMode, 'HeroAtkSystem', ` 护盾抵挡1次伤害,剩余次数 ${TAttrsComp.shield}`); return { remainingDamage: 0, absorbedDamage: damage }; } /** * 被攻击时触发的事件 * * 预留的扩展点,用于处理被攻击时的特殊逻辑: * - 触发反伤效果(荆棘光环等) * - 触发被攻击天赋(如受击回血、受击反击等) * - 触发特殊状态(如受伤狂暴、受伤护盾等) * * @param entity 被攻击的实体 * * @todo 当前对怪物实体直接返回,后续可以根据需求扩展怪物的被攻击逻辑 */ private onAttacked(entity: ecs.Entity): void { const TAttrsComp = entity.get(HeroAttrsComp); if (!TAttrsComp || TAttrsComp.is_dead) return; TAttrsComp.atked_count++; // 这里可以添加被攻击时的特殊处理逻辑 if (TAttrsComp.fac === FacSet.MON) return; // 例如:触发某些天赋效果、反击逻辑等 } /** * 死亡时触发的事件 * * 根据实体阵营类型处理不同的死亡逻辑: * * - FacSet.MON(怪物):触发掉落逻辑 * - 延迟执行掉落,避免阻塞主逻辑 * - 可以扩展:经验值计算、任务进度等 * * - FacSet.HERO(英雄):触发英雄死亡特殊处理 * - 游戏结束判定 * - 复活机制检查 * - 死亡惩罚/奖励 * * @param entity 死亡的实体 * * @important 死亡事件应该幂等,避免重复触发 */ private onDeath(entity: ecs.Entity): void { const TAttrsComp = entity.get(HeroAttrsComp); if (!TAttrsComp) return; if (TAttrsComp.fac === FacSet.MON) { // 怪物死亡处理 this.scheduleDrop(entity); // oops.message 全局事件总线:怪物死亡(连杀计数等表现层消费) oops.message.dispatchEvent(GameEvent.MonDead, { isBoss: !!TAttrsComp.is_boss, worldPos: entity.get(HeroViewComp)?.node?.worldPosition?.clone() ?? null, }); } else if (TAttrsComp.fac === FacSet.HERO) { // 英雄死亡处理 this.scheduleHeroDeath(entity); } } /** * 怪物死亡掉落金币 * * 实现说明: * - 通过 hero_uuid 查 HeroInfo 获取 monType * - 按怪物类型查 MonsterGoldSet 获取固定金币掉落数(Boss 有独立的高额奖励) * - 调用 MissionEconomy.addCoin 统一发放金币(会自动触发 UI 更新和评分统计) * * @param entity 死亡的怪物实体 */ private scheduleDrop(entity: ecs.Entity): void { const TAttrsComp = entity.get(HeroAttrsComp); if (!TAttrsComp) return; // 通过 hero_uuid 查配置获取怪物类型 const info = HeroInfo[TAttrsComp.hero_uuid]; const monType = info?.monType ?? 0; const gold = getMonsterGoldDrop(monType, !!TAttrsComp.is_boss); if (gold <= 0) return; MissionEconomy.addCoin(gold); // oops.message 全局事件总线:金币飞行表现(账务已即时结算,飞行纯视觉) oops.message.dispatchEvent(GameEvent.CoinFly, { worldPos: entity.get(HeroViewComp)?.node?.worldPosition?.clone() ?? null, gold, isBoss: !!TAttrsComp.is_boss, }); mLogger.log(this.debugMode, 'HeroAtkSystem', ` ${TAttrsComp.hero_name} 死亡掉落金币 ${gold}(monType=${monType}, isBoss=${TAttrsComp.is_boss})`); } /** * 延迟执行英雄死亡逻辑 * * 英雄死亡的特殊处理,比普通怪物复杂: * * 处理内容包括: * - 检查复活次数和复活条件 * - 触发游戏结束界面(如适用) * - 记录死亡统计信息 * - 处理死亡惩罚(经验损失、装备损坏等) * * @param entity 死亡的英雄实体 * * @important 英雄死亡通常需要玩家交互,所以必须延迟处理 * 给玩家足够的反馈时间和操作空间 */ private scheduleHeroDeath(entity: ecs.Entity): void { // 这里可以添加英雄死亡的特殊处理 // 例如:触发游戏结束、复活机制等 } /** * 启用调试模式 */ enableDebug() { this.debugMode = true; } /** * 禁用调试模式 */ disableDebug() { this.debugMode = false; } }