1. 新增两个反伤相关的buff配置:固定反伤7027和比例反伤7028 2. 新增两个支援技能:荆棘护盾和荆棘比例增益 3. 为HeroAttrsComp新增获取最终反伤属性的方法,支持限时修饰词计算 4. 重构英雄反伤伤害计算逻辑,使用新的属性获取方法
759 lines
33 KiB
TypeScript
759 lines
33 KiB
TypeScript
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, BuffTarget } from "../common/config/SkillSet";
|
||
|
||
import { mLogger } from "../common/Logger";
|
||
import { SkillTriggerHelper } from "./SkillTriggerHelper";
|
||
import { getMonsterGoldDrop } from "../map/RogueConfig";
|
||
import { MissionEconomy } from "../map/MissionEconomy";
|
||
import { MoveComp } from "./MoveComp";
|
||
import { buffManager, BuffApplyOverrides } from "./BuffManager";
|
||
import { DEBUFF_ICON_MAP, VULNERABLE_BUFF_ID, BLEED_BUFF_ID, BLEED_CRIT_PER_STACK, BuffList } from "../common/config/BuffSet";
|
||
import { BuffComp } from "./BuffComp";
|
||
|
||
/** 最终伤害数据接口
|
||
* 用于封装一次攻击计算的所有结果数据
|
||
* @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;
|
||
// DoT 事件:dot_dmg>0 时伤害为固定 tick 值,无对应 SkillSet 配置(s_uuid 位置是 buffId)
|
||
const isDot = (damageEvent.dot_dmg ?? 0) > 0;
|
||
// DoT 事件的 Attrs 来自 ActiveBuff.source_attrs_snapshot,可能为空,兜底空对象避免取属性报错
|
||
if (isDot && !damageEvent.Attrs) damageEvent.Attrs = {};
|
||
|
||
// 获取技能配置
|
||
const skillConf = SkillSet[damageEvent.s_uuid];
|
||
if (!skillConf && !isDot) return reDate;
|
||
|
||
// 触发被攻击事件
|
||
this.onAttacked(target);
|
||
|
||
// 暴击判定
|
||
// 使用施法者的暴击率属性(damageEvent.Attrs 快照)- 被攻击者的暴击抗性 + 目标出血层数附加的被暴击率
|
||
const criticalChance = (damageEvent.Attrs[Attrs.critical] || 0) - TAttrsComp.getFinalCriticalRes() + this.getBleedCritBonus(target);
|
||
const isCrit = this.checkChance(criticalChance);
|
||
|
||
// 计算基础伤害:DoT 直接用固定 tick 值,普攻走 dmgCount 公式
|
||
let damage = isDot ? (damageEvent.dot_dmg ?? 0) : 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)
|
||
// 易伤附加:目标每层易伤 buff 附加固定伤害,被易伤抗性百分比减免;普攻与 DoT 均生效
|
||
damage += this.calcVulnerableBonus(target, TAttrsComp);
|
||
mLogger.log(this.debugMode, 'HeroAtkSystem', " after vulnerable", damage)
|
||
// 护盾数值减免(1 点护盾吸收 1 点伤害,扣完为止,溢出部分继续扣血)
|
||
const shieldResult = this.absorbShield(TAttrsComp, damage);
|
||
damage = shieldResult.remainingDamage;
|
||
if (shieldResult.absorbedDamage > 0) {
|
||
// 【评分系统 - 防御分】统计护盾累计吸收的伤害量
|
||
smc.vmdata.scores.shield_block_count += Math.floor(shieldResult.absorbedDamage);
|
||
}
|
||
mLogger.log(this.debugMode, 'HeroAtkSystem', " after shield", damage)
|
||
// 显示护盾减免飘字
|
||
if (shieldResult.absorbedDamage > 0 && targetView) {
|
||
targetView.shield_tip(shieldResult.absorbedDamage);
|
||
}
|
||
if (damage <= 0) return reDate;
|
||
|
||
// 防御减伤(除法递减模型,护盾吸收之后、扣血之前结算)
|
||
damage = TAttrsComp.applyDefense(damage);
|
||
mLogger.log(this.debugMode, 'HeroAtkSystem', " after defense", damage)
|
||
|
||
// 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(被格挡不计数);DoT 不计受击、不触发受击技
|
||
if (!isDot) {
|
||
TAttrsComp.atked_count++;
|
||
this.checkAndTriggerAtkedSkills(TAttrsComp, targetView);
|
||
}
|
||
|
||
// 反伤结算:实际受到伤害后,目标将自身 thorns_dmg 反弹给攻击者
|
||
// 反伤值 = 实际受到伤害 * thorns_rate / 100 + thorns_dmg(固定值+比例混合)
|
||
// DoT 不触发反伤(持续伤害类效果通常不触发荆棘)
|
||
if (!isDot && damage > 0) {
|
||
const thornsDmg = Math.floor(damage * TAttrsComp.getFinalThornsRate() / 100) + TAttrsComp.getFinalThornsDmg();
|
||
if (thornsDmg > 0) {
|
||
this.applyThornsDamage(casterEid, thornsDmg);
|
||
}
|
||
}
|
||
|
||
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 paralyzeChance = damageEvent.Attrs[Attrs.paralyze_chance] || 0;
|
||
const isParalyze = !TAttrsComp.isParalyze() && this.checkChance(paralyzeChance);
|
||
|
||
// ✅ 触发视图层表现(伤害数字、受击动画、冰冻、击晕、击退)
|
||
if (targetView) {
|
||
if (isDot) {
|
||
// DoT:只飘伤害数字 + 刷新血条,不播受击动画/音效/击退;
|
||
// s_uuid 位置是 buffId,据此激活 loss_life/hp/debuff 下的对应 debuff 图标
|
||
targetView.showDamage(damage, isCrit, DEBUFF_ICON_MAP[damageEvent.s_uuid] ?? "");
|
||
} else {
|
||
// 击退概率判定:概率来自技能配置 bck_chance(heroSet overrides 可覆写),未配置视为 0 不击退。
|
||
// 不复用 checkChance:其 70% 上限封顶会把 100% 击退(近战普攻 6004)削成 70%
|
||
const knockbackChance = damageEvent.Attrs[Attrs.knockback_skill_chance] || 0;
|
||
const isKnockback = knockbackChance >= 100 || (knockbackChance > 0 && Math.random() * 100 < knockbackChance);
|
||
|
||
// 击退距离增量 = 施法者击退强化(原击退概率)- 被攻击者击退抗性 + 击退距离强化(仅命中击退时生效)
|
||
const knockbackBonus = (damageEvent.Attrs[Attrs.knockback_chance] || 0) - (TAttrsComp.getFinalKnockbackRes() || 0);
|
||
const knockbackExtra = Math.max(0, knockbackBonus + (damageEvent.Attrs[Attrs.knockback_distance] || 0));
|
||
|
||
targetView.do_atked(damage, isCrit, damageEvent.s_uuid, isKnockback ? knockbackExtra : 0);
|
||
targetView.playEnd(skillConf.endAnm);
|
||
|
||
// 击退位移下沉到 MoveSystem:直接写入 knockbackVelocity,由 MoveSystem 每帧衰减并叠加位移
|
||
if (isKnockback) {
|
||
const moveComp = target.get(MoveComp);
|
||
if (moveComp) {
|
||
const dist = FightSet.BACK_RANG + knockbackExtra;
|
||
const isHero = TAttrsComp.fac === FacSet.HERO;
|
||
// 击退初速度:0.1 秒时间常数衰减,总位移约等于 dist
|
||
const knockbackSpeed = dist * 16;
|
||
moveComp.knockbackVelocity = isHero ? -knockbackSpeed : knockbackSpeed;
|
||
}
|
||
}
|
||
}
|
||
|
||
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 (isParalyze) {
|
||
TAttrsComp.toParalyze();
|
||
}
|
||
}
|
||
|
||
// 命中附带 buff:仅普攻路径;buff_target 缺省挂被命中者,显式配置可改为挂施法者/全体友方/全体敌方
|
||
// 施法者 ap/完整属性快照取自伤害事件,供 BuffSystem tick 缩放与暴击/冰冻/击晕判定
|
||
if (!isDot && skillConf.timed_buff_id) {
|
||
const buffChance = skillConf.buff_chance ?? 100;
|
||
if (buffChance >= 100 || Math.random() * 100 < buffChance) {
|
||
const buffOverrides: BuffApplyOverrides = { source_attrs: damageEvent.Attrs };
|
||
if (skillConf.buff_value !== undefined) buffOverrides.value = skillConf.buff_value;
|
||
if (skillConf.buff_duration !== undefined) buffOverrides.duration = skillConf.buff_duration;
|
||
for (const buffTarget of this.resolveBuffTargets(target, casterEid, TAttrsComp.fac, skillConf.buff_target)) {
|
||
buffManager.applyBuff(buffTarget, skillConf.timed_buff_id, casterEid, damageEvent.Attrs[Attrs.ap] || 0, buffOverrides);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 命中概率触发额外技能(proc_skill):配置由施法技能实体快照自合并后配置(含 heroSet overrides/lv_entries),
|
||
// 触发走 SkillTriggerHelper/forceCastTriggerSkill 通道(无视 CD、无施法前摇),与 atking 同一通道。
|
||
// 防递归:被触发的技能经 forceCastTriggerSkill 不再产生 proc_skill 伤害事件之外的新事件,且不会累加 atk_count。
|
||
if (!isDot && damageEvent.proc_skill) {
|
||
const proc = damageEvent.proc_skill;
|
||
const procChance = proc.chance ?? 100;
|
||
if (procChance >= 100 || Math.random() * 100 < procChance) {
|
||
const caster = ecs.getEntityByEid(casterEid);
|
||
if (caster) {
|
||
const casterAttrs = caster.get(HeroAttrsComp);
|
||
const casterView = caster.get(HeroViewComp);
|
||
if (casterAttrs && casterView && !casterAttrs.is_dead && !casterAttrs.is_reviving) {
|
||
// proc_skill.overrides 透传给被触发技能(如自定义 ap 伤害),与被触发技能自身 overrides 叠加(本次传入优先)
|
||
SkillTriggerHelper.dispatchSinglePublic(proc.s_uuid, casterAttrs, casterView, SkillTriggerType.Atking, proc.overrides);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// 检查死亡
|
||
if (TAttrsComp.hp <= 0) {
|
||
// 先触发死亡技能(如亡语),不管后续是否续命都应触发
|
||
this.triggerDeadSkills(target);
|
||
|
||
// 死亡计数 +1(用于 dead 触发统计)
|
||
TAttrsComp.death_count++;
|
||
|
||
// 从 runtime_dead 中提取复活配置(revive_hp / revive_count)
|
||
let canRevive = false;
|
||
let maxReviveCount = 0;
|
||
let reviveHpPercent = 0;
|
||
let reviveSkillConf: any = null;
|
||
if (TAttrsComp.runtime_dead) {
|
||
for (const trig of TAttrsComp.runtime_dead) {
|
||
const reviveHp = trig.overrides?.revive_hp;
|
||
const reviveCount = trig.overrides?.revive_count ?? 0;
|
||
if (reviveHp !== undefined && reviveCount > 0) {
|
||
maxReviveCount = reviveCount;
|
||
if (TAttrsComp.revived_count < maxReviveCount) {
|
||
canRevive = true;
|
||
reviveHpPercent = reviveHp;
|
||
reviveSkillConf = SkillSet[trig.s_uuid];
|
||
}
|
||
break; // 只取第一个复活配置
|
||
}
|
||
}
|
||
}
|
||
|
||
if (canRevive) {
|
||
TAttrsComp.revived_count++;
|
||
TAttrsComp.is_reviving = true; // 标记为正在复活
|
||
// 根据 dead 触发配置恢复生命值
|
||
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,
|
||
rarity: attrs.rarity,
|
||
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. 1 点护盾吸收 1 点伤害
|
||
* 2. 护盾扣减实际吸收量,溢出部分继续结算到血量
|
||
*
|
||
* @param TAttrsComp 被攻击者的属性组件(包含当前护盾值)
|
||
* @param damage 原始伤害值
|
||
* @returns {remainingDamage, absorbedDamage} 剩余伤害值和被护盾减免的伤害值
|
||
*/
|
||
/**
|
||
* 目标出血层数附加的被暴击率(每层 +BLEED_CRIT_PER_STACK 百分点)。
|
||
* @returns 附加的被暴击率(百分点)
|
||
*/
|
||
private getBleedCritBonus(target: ecs.Entity): number {
|
||
if (!target.has(BuffComp)) return 0;
|
||
const layers = target.get(BuffComp).buffs.get(BLEED_BUFF_ID);
|
||
return (layers?.length ?? 0) * BLEED_CRIT_PER_STACK;
|
||
}
|
||
|
||
/**
|
||
* 计算易伤附加伤害:汇总目标每层易伤 buff 的数值(value_override 优先),再被易伤抗性百分比减免。
|
||
* @returns 附加伤害值(向下取整,非负)
|
||
*/
|
||
private calcVulnerableBonus(target: ecs.Entity, TAttrsComp: HeroAttrsComp): number {
|
||
if (!target.has(BuffComp)) return 0;
|
||
const layers = target.get(BuffComp).buffs.get(VULNERABLE_BUFF_ID);
|
||
if (!layers || layers.length === 0) return 0;
|
||
const cfg = BuffList[VULNERABLE_BUFF_ID];
|
||
const baseVal = cfg?.modifiers?.[0]?.value ?? 0;
|
||
let sum = 0;
|
||
for (const layer of layers) {
|
||
sum += layer.value_override !== undefined ? layer.value_override : baseVal;
|
||
}
|
||
if (sum <= 0) return 0;
|
||
// 易伤抗性:百分比减免(vulnerable_res=30 即减免 30%)
|
||
const res = TAttrsComp.getFinalVulnerableRes();
|
||
return Math.max(0, Math.floor(sum * (1 - res / 100)));
|
||
}
|
||
|
||
private absorbShield(TAttrsComp: HeroAttrsComp, damage: number): { remainingDamage: number, absorbedDamage: number } {
|
||
if (TAttrsComp.shield <= 0) {
|
||
return { remainingDamage: damage, absorbedDamage: 0 };
|
||
}
|
||
const absorbed = Math.min(TAttrsComp.shield, damage);
|
||
TAttrsComp.shield = Math.max(0, TAttrsComp.shield - absorbed);
|
||
TAttrsComp.dirty_shield = true;
|
||
return { remainingDamage: damage - absorbed, absorbedDamage: absorbed };
|
||
}
|
||
|
||
/**
|
||
* 对攻击者施加反伤伤害
|
||
*
|
||
* 反伤为固定数值伤害,直接扣减攻击者 HP,不走暴击/防御/护盾等常规结算
|
||
* (反伤通常是真实伤害或固定伤害,避免嵌套结算导致无限循环)
|
||
*
|
||
* @param casterEid 攻击者实体 ID
|
||
* @param thornsDmg 反伤伤害值
|
||
*/
|
||
private applyThornsDamage(casterEid: number, thornsDmg: number): void {
|
||
const caster = ecs.getEntityByEid(casterEid);
|
||
if (!caster) return;
|
||
|
||
const casterAttrs = caster.get(HeroAttrsComp);
|
||
if (!casterAttrs || casterAttrs.is_dead || casterAttrs.is_reviving) return;
|
||
|
||
// 反伤直接扣血,不经过护盾/防御结算(避免反伤-反伤无限循环)
|
||
casterAttrs.add_hp(-thornsDmg);
|
||
|
||
// 反伤计入评分统计(仅英雄阵营的反伤被统计,怪物反伤通常源于英雄技能/天赋)
|
||
if (casterAttrs.fac === FacSet.HERO) {
|
||
smc.vmdata.scores.thorns_dmg += thornsDmg;
|
||
}
|
||
|
||
mLogger.log(this.debugMode, 'HeroAtkSystem', ` 反伤: ${casterAttrs.hero_name} 受到 ${thornsDmg} 点反伤伤害`);
|
||
|
||
// 反伤致死检查
|
||
if (casterAttrs.hp <= 0) {
|
||
this.triggerDeadSkills(caster);
|
||
casterAttrs.death_count++;
|
||
this.doDead(caster);
|
||
const casterView = caster.get(HeroViewComp);
|
||
if (casterView) {
|
||
casterView.do_dead();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析 timed_buff_id 的施加目标实体列表。
|
||
* 缺省(undefined / BuffTarget.Hit)返回被命中者;显式配置可切换到施法者自身、全体友方或全体敌方。
|
||
* @param hitTarget 被命中实体(伤害事件的 target)
|
||
* @param casterEid 施法者 eid
|
||
* @param targetFac 被命中者阵营(用于推导敌我网格)
|
||
* @param buffTarget 技能配置上的目标覆写
|
||
*/
|
||
private resolveBuffTargets(hitTarget: ecs.Entity, casterEid: number, targetFac: number, buffTarget?: BuffTarget): ecs.Entity[] {
|
||
if (buffTarget === undefined || buffTarget === BuffTarget.Hit) {
|
||
return [hitTarget];
|
||
}
|
||
if (buffTarget === BuffTarget.Self) {
|
||
const caster = ecs.getEntityByEid(casterEid);
|
||
return caster ? [caster] : [];
|
||
}
|
||
// 全体友方/敌方:以施法者阵营为基准(命中者必为敌方,施法方网格即友方)
|
||
const casterFac = targetFac === FacSet.HERO ? FacSet.MON : FacSet.HERO;
|
||
const grid = buffTarget === BuffTarget.Ally
|
||
? (casterFac === FacSet.HERO ? smc.mission.heroGrid : smc.mission.monGrid)
|
||
: (casterFac === FacSet.HERO ? smc.mission.monGrid : smc.mission.heroGrid);
|
||
const targets: ecs.Entity[] = [];
|
||
for (const eid of grid) {
|
||
if (eid < 0) continue;
|
||
const ent = ecs.getEntityByEid(eid);
|
||
if (!ent) continue;
|
||
const attrs = ent.get(HeroAttrsComp);
|
||
if (attrs && !attrs.is_dead && !attrs.is_reviving) targets.push(ent);
|
||
}
|
||
return targets;
|
||
}
|
||
|
||
/**
|
||
* 被攻击时触发的事件
|
||
*
|
||
* 当前已实现的逻辑:
|
||
* - 反伤效果:目标通过 thorns_dmg 属性对攻击者造成固定反伤(在 doAttack 中结算)
|
||
*
|
||
* 预留的扩展点:
|
||
* - 触发被攻击天赋(如受击回血、受击反击等)
|
||
* - 触发特殊状态(如受伤狂暴、受伤护盾等)
|
||
*
|
||
* @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;
|
||
}
|
||
|
||
|
||
}
|