Files
pixelheros/assets/script/game/hero/HeroAtkSystem.ts
panw cd0004e37c feat(护盾): 将护盾机制从数值吸收改为次数免疫
- 护盾技能配置的 `ap` 字段现在表示免疫次数而非攻击力百分比
- 修改护盾计算逻辑,每次受到攻击消耗1点护盾值
- 更新日志和注释以反映新的护盾行为
2026-03-27 15:22:16 +08:00

432 lines
15 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 } from "../common/config/SkillSet";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { DamageQueueComp, DamageEvent } from "./DamageQueueComp";
import { smc } from "../common/SingletonModuleComp";
import { mLogger } from "../common/Logger";
/** 最终伤害数据接口
* 用于封装一次攻击计算的所有结果数据
* @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} 个伤害事件`);
}
}
}
/**
* 执行攻击计算 - 核心伤害计算逻辑
*
* @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 isCrit = this.checkChance(damageEvent.Attrs[Attrs.critical]);
// 计算基础伤害
let damage = this.dmgCount(damageEvent,TAttrsComp);
mLogger.log(this.debugMode, 'HeroAtkSystem', " dmgCount",damage)
if (isCrit) {
damage = Math.floor(damage * (1 + FightSet.CRIT_DAMAGE / 100));
reDate.isCrit=true;
}
mLogger.log(this.debugMode, 'HeroAtkSystem', " after crit",damage)
// 护盾吸收
const shieldResult = this.absorbShield(TAttrsComp, damage);
damage = shieldResult.remainingDamage;
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 (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 isBack = this.checkChance((damageEvent.Attrs[Attrs.back_chance] || 0));
const freezeChance = damageEvent.Attrs[Attrs.freeze_chance] || 0;
const isFrost = !TAttrsComp.isFrost() && this.checkChance(freezeChance);
// ✅ 触发视图层表现(伤害数字、受击动画、后退,冰冻)
if (targetView) {
targetView.do_atked(damage, isCrit, damageEvent.s_uuid, isBack);
targetView.playEnd(skillConf.endAnm);
if (isFrost) {
TAttrsComp.toFrost();
targetView.in_iced(TAttrsComp.frost_end_time);
}
}
// 检查死亡
if (TAttrsComp.hp <= 0) {
// 复活机制:如果玩家属性内的复活属性值>=1 则执行复活,原地50%血量复活
if (TAttrsComp.revive_count >= 1) {
TAttrsComp.revive_count--;
TAttrsComp.is_reviving = true; // 标记为正在复活
// 触发死亡动画(假死)
if (targetView) {
targetView.do_dead();
// 延迟1秒复活
targetView.scheduleRevive(1.0);
}
mLogger.log(this.debugMode, 'HeroAtkSystem', ` Hero waiting to revive! Lives left: ${TAttrsComp.revive_count}`);
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;
}
/**
* 处理角色死亡
*
* 死亡处理流程:
* 1. 标记死亡状态is_dead = true
* 2. 触发死亡事件onDeath
* 3. 记录调试信息(如启用调试模式)
*
* @param entity 死亡的实体
*
* @important 死亡状态一旦设置,该实体将不再处理新的伤害事件
* 这确保了死亡逻辑的单一性和一致性
*/
private doDead(entity: ecs.Entity): void {
const TAttrsComp = entity.get(HeroAttrsComp);
if (!TAttrsComp || TAttrsComp.is_dead) return;
TAttrsComp.is_dead = true;
// 触发死亡事件
this.onDeath(entity);
if (this.debugMode) {
mLogger.log(this.debugMode, 'HeroAtkSystem', ` ${TAttrsComp.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;
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.shield_max = 0;
}
TAttrsComp.dirty_shield = true;
mLogger.log(this.debugMode, 'HeroAtkSystem', ` 护盾抵挡1次伤害剩余次数 ${TAttrsComp.shield}`);
return {remainingDamage: 0, absorbedDamage: 1};
}
/**
* 被攻击时触发的事件
*
* 预留的扩展点,用于处理被攻击时的特殊逻辑:
* - 触发反伤效果(荆棘光环等)
* - 触发被攻击天赋(如受击回血、受击反击等)
* - 触发特殊状态(如受伤狂暴、受伤护盾等)
*
* @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);
} else if (TAttrsComp.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;
}
}