Files
pixelheros/assets/script/game/hero/HeroAttrsComp.ts
pan 1afa06efb8 refactor(attr system): 统一使用属性获取方法,让限时buff生效
1. 重构HeroAtkSystem中的暴击、击晕判定逻辑,改用getFinal方法获取抗性
2. 重构Skill.ts中的属性赋值逻辑,替换为对应的最终属性获取方法
3. 修复血量钳制逻辑,使用getFinalHpMax确保限时血量上限buff生效
4. 新增多个属性的最终值获取方法,统一处理限时修饰符
2026-07-23 16:55:33 +08:00

610 lines
23 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 { HeroDisVal, HeroInfo, HSkillInfo, HType, SkillTriggerType } from "../common/config/heroSet";
import { mLogger } from "../common/Logger";
import { Timer } from "db://oops-framework/core/common/timer/Timer";
import { FacSet, FightSet } from "../common/config/GameSet";
import { FieldSkillSet, FieldSkillType, SkillOverrides } from "../common/config/SkillSet";
import { smc } from "../common/SingletonModuleComp";
import { Attrs } from "../common/config/HeroAttrs";
import { FieldSkillHelper } from "./FieldSkillHelper";
import { BuffComp } from "./BuffComp";
import { BuffList, ModOp } from "../common/config/BuffSet";
@ecs.register('HeroAttrs')
export class HeroAttrsComp extends ecs.Comp {
public debugMode: boolean = false;
private static readonly percentRateThreshold = 1;
private static readonly minAttackCd = 0.05;
Ebus: any = null!
// ==================== 角色基础信息 ====================
hero_uuid: number = 1001;
hero_name: string = "hero";
lv: number = 1;
pool_lv: number = 1;
base_pool_lv: number = 1;
type: number = 0; // 0近战 1远程 2辅助
fac: number = 0; // 0:hero 1:monster
// ==================== 基础属性(有初始值) ====================
base_ap: number = 0; // 原始基础攻击(无任何加成)
base_hp: number = 0; // 原始基础血量(无任何加成)
ap: number = 0; // 基础攻击
hp: number = 100; // 基础血量
hp_max: number = 100; // 最大血量
speed: number = 100; // 基础移动速度
dis: number = 100; // 基础距离
shield: number = 0; // 当前护盾
// ==================== 攻击属性 (补充) ====================
skills: Record<number, HSkillInfo> = {};
// ==================== 触发类技能 ====================
[SkillTriggerType.Call]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.Dead]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.FStart]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.FEnd]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.Atking]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.Atked]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.Revive]?: { s_uuid: number, r_num: number, upr: number };
// ==================== 特殊属性 ====================
critical: number = 0; // 暴击率
critical_res: number = 0; // 暴击抗性
freeze_chance: number = 0; // 冰冻概率
freeze_res: number = 0; // 冰冻抗性
stun_chance: number = 0; // 击晕概率
stun_res: number = 0; // 击晕抗性
knockback_chance: number = 0; // 击退概率
knockback_distance: number = 0; // 击退距离强化
knockback_res: number = 0; // 击退抗性
crit_damage: number = 0; // 额外暴击伤害
puncture_chance: number = 0; // 穿透概率
wfuny: number = 0; // 风怒
revived_count: number = 0; // 已复活次数
invincible_time: number = 0;// 无敌时间
frost_end_time: number = 0;
stun_end_time: number = 0;
boom: boolean = false; // 自爆怪
// ==================== 脏标签标记 ====================
dirty_hp: boolean = false; // 血量变更标记
dirty_shield: boolean = false; // 护盾变更标记
dirty_lv: boolean = false; // 等级变更标记
// ==================== 技能距离缓存 ====================
maxSkillDistance: number = 0; // 最远技能攻击距离缓存受MP影响
minSkillDistance: number = 0; // 最近技能攻击距离缓存不受MP影响用于停止位置判断
// ==================== 阵型位置 ====================
posIndex: number = -1;
// ==================== 标记状态 ====================
is_dead: boolean = false;
is_count_dead: boolean = false;
is_atking: boolean = false; // 是否正在攻击
is_stop: boolean = false; // 是否正在停止
is_boss: boolean = false;
is_big_boss: boolean = false;
is_master: boolean = false;
is_friend: boolean = false;
is_kalami: boolean = false;
is_reviving: boolean = false; // 是否正在复活中
// ==================== 计数统计 ====================
atk_count: number = 0; // 攻击次数
atked_count: number = 0; // 被攻击次数
killed_count: number = 0;
combat_target_eid: number = -1;
enemy_in_cast_range: boolean = false;
start() {
}
// ==================== BUFF 系统初始化 ====================
/**
* 初始化角色的 buff debuff
* 从 HeroInfo 读取初始配置,建立属性系统
*/
initAttrs() {
this.frost_end_time = 0;
this.stun_end_time = 0;
}
/*******************基础属性管理********************/
add_hp(value: number) {
const oldHp = this.hp;
let addValue = value;
this.hp += addValue;
// 血量钳制使用 getFinalHpMax(),让限时 hp_max buff 也生效于血量上限
this.hp = Math.max(0, Math.min(this.hp, this.getFinalHpMax()));
this.dirty_hp = true; // ✅ 仅标记需要更新
if (this.debugMode) {
mLogger.log(this.debugMode, 'HeroAttrs', ` HP变更: ${this.hero_name}, 变化=${addValue.toFixed(1)}, ${oldHp.toFixed(1)} -> ${this.hp.toFixed(1)}`);
}
return addValue;
}
add_shield(value: number) {
const oldShield = this.shield;
const addValue = Math.max(0, Math.floor(value));
if (addValue <= 0) return;
this.shield += addValue;
this.shield = Math.min(this.shield, FightSet.SHIELD_MAX); // 限制护盾最大层数
if (this.shield < 0) this.shield = 0;
this.dirty_shield = true; // 标记护盾需要更新
if (this.debugMode) {
mLogger.log(this.debugMode, 'HeroAttrs', ` 护盾次数变更: ${this.hero_name}, 变化=${addValue}, ${Math.floor(oldShield)} -> ${Math.floor(this.shield)}`);
}
}
add_hp_max(value: number) {
this.hp_max += value
this.hp += value
this.dirty_hp = true; // ✅ 仅标记需要更新
return value
}
add_ap(value: number) {
this.ap += value
return value
}
/**
* 统一的特殊/固定属性数值增加方法
* @param attr_type 属性类型枚举
* @param value 增加的数值
*/
add_special_attr(attr_type: Attrs, value: number) {
// 利用枚举值(字符串)与类属性名一致的特性,动态访问并累加属性
const key = attr_type as keyof this;
// 确保目标属性存在且类型为数字,避免运行时错误
if (typeof this[key] === 'number') {
(this as any)[key] += value;
} else {
if (this.debugMode) {
mLogger.log(this.debugMode, 'HeroAttrs', `未找到对应数字属性或无法累加: attr_type=${attr_type}, value=${value}`);
}
}
}
toFrost(time: number = 1) {
const frostTime = FightSet.FROST_TIME * time;
this.frost_end_time = Math.max(this.frost_end_time, frostTime);
}
toStun(time: number = 1) {
const stunTime = FightSet.STUN_TIME * time;
this.stun_end_time = Math.max(this.stun_end_time, stunTime);
// 击晕时 CD 清零
for (const key in this.skills) {
const skill = this.skills[key];
if (skill) {
skill.ccd = 0;
}
}
}
updateCD(dt: number) {
// 如果处于冰冻状态,则技能 CD 暂停刷新
if (this.isFrost()) return;
// 如果处于击晕状态,则技能 CD 暂停刷新(且保持清零状态)
if (this.isStun()) {
for (const key in this.skills) {
const skill = this.skills[key];
if (skill) {
skill.ccd = 0;
}
}
return;
}
for (const key in this.skills) {
const skill = this.skills[key];
if (!skill) continue;
const actualCd = this.getEffectiveSkillCd(skill.uuid);
if (actualCd <= 0) {
skill.ccd = 0;
continue;
}
if (skill.ccd >= actualCd) {
skill.ccd = actualCd;
continue;
}
skill.ccd = Math.min(actualCd, skill.ccd + dt);
}
}
isFrost(): boolean {
return this.frost_end_time > 0
}
isStun(): boolean {
return this.stun_end_time > 0
}
getSkillLevel(skillId: number): number {
if (!skillId) return 0;
return this.skills[skillId]?.lv ?? 0;
}
getSkillIds(): number[] {
return Object.values(this.skills).map(skill => skill.uuid);
}
isSkillReady(skillId: number): boolean {
if (!skillId) return false;
const skill = this.skills[skillId];
if (!skill) return false;
const actualCd = this.getEffectiveSkillCd(skillId);
if (actualCd <= 0) return true;
return skill.ccd >= actualCd;
}
triggerSkillCD(skillId: number) {
if (!skillId) return;
const skill = this.skills[skillId];
if (!skill) return;
skill.ccd = 0;
}
getSkillCdProgress(skillId: number): number {
if (!skillId) return 1;
const skill = this.skills[skillId];
const actualCd = this.getEffectiveSkillCd(skillId);
if (!skill || actualCd <= 0) return 1;
return Math.max(0, Math.min(1, skill.ccd / actualCd));
}
getDisplaySkillCdProgress(): number {
const skillIds = this.getSkillIds();
const displaySkillId = skillIds[1] ?? skillIds[0] ?? 0;
return this.getSkillCdProgress(displaySkillId);
}
/** 将驻场配置值统一换算成百分比数值,兼容 0.2 和 20 两种写法。 */
public static getFieldPercentValue(type: FieldSkillType): number {
const rawValue = FieldSkillHelper.getFieldSkillTotalValue(type);
if (Math.abs(rawValue) <= HeroAttrsComp.percentRateThreshold) {
return rawValue * 100;
}
return rawValue;
}
/** 英雄实时暴击率 = 基础暴击率 + 驻场暴击率。 */
public getRuntimeCritical(): number {
if (this.fac !== FacSet.HERO) return this.critical;
return this.critical + HeroAttrsComp.getFieldPercentValue(FieldSkillType.HeroCrit);
}
/** 英雄实时冰冻率 = 基础冰冻率 + 驻场冰冻率。 */
public getRuntimeFreezeChance(): number {
if (this.fac !== FacSet.HERO) return this.freeze_chance;
return this.freeze_chance;
}
/** 英雄实时击晕率 = 基础击晕率 + 驻场击晕率。 */
public getRuntimeStunChance(): number {
if (this.fac !== FacSet.HERO) return this.stun_chance;
return this.stun_chance + HeroAttrsComp.getFieldPercentValue(FieldSkillType.HeroStun);
}
/** 英雄实时风怒概率 = 基础风怒 + 驻场风怒。 */
public getRuntimeWindFury(): number {
if (this.fac !== FacSet.HERO) return this.wfuny;
return this.wfuny + HeroAttrsComp.getFieldPercentValue(FieldSkillType.HeroWindFury);
}
/** 英雄实时穿透概率 = 基础穿透 + 驻场穿透。 */
public getRuntimePunctureChance(): number {
if (this.fac !== FacSet.HERO) return this.puncture_chance;
return this.puncture_chance + HeroAttrsComp.getFieldPercentValue(FieldSkillType.HeroPuncture);
}
/** 英雄实时暴击伤害 = 基础额外暴伤 + 驻场暴伤。 */
public getRuntimeCritDamageBonus(): number {
if (this.fac !== FacSet.HERO) return this.crit_damage;
return this.crit_damage + HeroAttrsComp.getFieldPercentValue(FieldSkillType.HeroCritDamage);
}
/** 攻速加成通过缩短普通攻击技能 CD 生效,正值越高,攻击越快。 */
public getRuntimeAttackSpeedBonus(): number {
if (this.fac !== FacSet.HERO) return 0;
return HeroAttrsComp.getFieldPercentValue(FieldSkillType.HeroSpeed);
}
/** 英雄实时攻击力 = 基础攻击 × (1 + 驻场攻击百分比)。 */
public getRuntimeAp(baseAp: number): number {
if (this.fac !== FacSet.HERO) return baseAp;
return baseAp * (1 + HeroAttrsComp.getFieldPercentValue(FieldSkillType.HeroAtk) / 100);
}
/** 英雄实时最大生命 = 基础生命 × (1 + 驻场生命百分比)。 */
public getRuntimeHp(baseHp: number): number {
if (this.fac !== FacSet.HERO) return baseHp;
return baseHp * (1 + HeroAttrsComp.getFieldPercentValue(FieldSkillType.HeroHp) / 100);
}
// ==================== 计时性 Buff 最终值 ====================
//
// 以下方法在驻场属性的基础上,叠加 BuffComp 中计时性 buff/debuff 的修饰器,
// 输出"最终生效值"。设计依据ECSComp 持有 ent 字段指向所属 Entity
// 因此可直接通过 this.ent.get(BuffComp) 取到同实体上的 buff 组件。
/**
* 聚合目标身上所有活跃 buff 对指定属性的计时性修饰。
* 每层 buff 独立贡献其配置中的 modifiers。
*
* @param attr 目标属性枚举
* @returns flatSum=Flat 修饰总和pctSum=百分比修饰总和(百分点)
*/
private aggregateTimedMods(attr: Attrs): { flatSum: number; pctSum: number } {
const result = { flatSum: 0, pctSum: 0 };
if (!this.ent) return result;
const buffComp = this.ent.get(BuffComp);
if (!buffComp || buffComp.buffs.size === 0) return result;
buffComp.buffs.forEach((layers, buffId) => {
const cfg = BuffList[buffId];
if (!cfg || !cfg.modifiers) return;
// 每层独立贡献,模拟叠层放大效果
for (let i = 0; i < layers.length; i++) {
for (const mod of cfg.modifiers) {
if (mod.attr !== attr) continue;
if (mod.op === ModOp.Flat) {
result.flatSum += mod.value;
} else {
// PercentAdd 与 PercentMul 暂统一按加法百分比聚合;
// PercentMul 真正的独立乘区尚未启用TODO: 启用时需改造为多乘区连乘
result.pctSum += mod.value;
}
}
}
});
return result;
}
/**
* 最终攻击力 = (基础攻击 + Flat 修饰) × (1 + 百分比修饰 / 100)。
* 融合驻场加成与计时性 buff 修饰。
*/
public getFinalAp(): number {
const runtimeAp = this.getRuntimeAp(this.ap);
const mods = this.aggregateTimedMods(Attrs.ap);
return (runtimeAp + mods.flatSum) * (1 + mods.pctSum / 100);
}
/**
* 最终最大生命 = (基础最大生命 + Flat 修饰) × (1 + 百分比修饰 / 100)。
* 融合驻场加成与计时性 buff 修饰。
*/
public getFinalHpMax(): number {
const runtimeHp = this.getRuntimeHp(this.hp_max);
const mods = this.aggregateTimedMods(Attrs.hp_max);
return (runtimeHp + mods.flatSum) * (1 + mods.pctSum / 100);
}
/**
* 最终暴击率 = 驻场实时暴击率 + Flat/百分比修饰之和。
* 暴击率是加法模型Flat 和 PercentAdd 均直接累加到百分点。
*/
public getFinalCritical(): number {
const base = this.getRuntimeCritical();
const mods = this.aggregateTimedMods(Attrs.critical);
return base + mods.flatSum + mods.pctSum;
}
/** 最终冰冻率 = 驻场实时冰冻率 + 限时修饰(加法模型) */
public getFinalFreezeChance(): number {
const base = this.getRuntimeFreezeChance();
const mods = this.aggregateTimedMods(Attrs.freeze_chance);
return base + mods.flatSum + mods.pctSum;
}
/** 最终击晕率 = 驻场实时击晕率 + 限时修饰(加法模型) */
public getFinalStunChance(): number {
const base = this.getRuntimeStunChance();
const mods = this.aggregateTimedMods(Attrs.stun_chance);
return base + mods.flatSum + mods.pctSum;
}
/** 最终击退概率 = 基础击退 + 限时修饰(加法模型) */
public getFinalKnockbackChance(): number {
const mods = this.aggregateTimedMods(Attrs.knockback_chance);
return this.knockback_chance + mods.flatSum + mods.pctSum;
}
/** 最终击退距离 = 基础击退距离 + 限时修饰(加法模型) */
public getFinalKnockbackDistance(): number {
const mods = this.aggregateTimedMods(Attrs.knockback_distance);
return this.knockback_distance + mods.flatSum + mods.pctSum;
}
/** 最终额外暴击伤害 = 驻场实时暴伤 + 限时修饰(加法模型) */
public getFinalCritDamage(): number {
const base = this.getRuntimeCritDamageBonus();
const mods = this.aggregateTimedMods(Attrs.critical_damage);
return base + mods.flatSum + mods.pctSum;
}
/** 最终穿透概率 = 驻场实时穿透 + 限时修饰(加法模型) */
public getFinalPunctureChance(): number {
const base = this.getRuntimePunctureChance();
const mods = this.aggregateTimedMods(Attrs.puncture_chance);
return base + mods.flatSum + mods.pctSum;
}
/** 最终风怒概率 = 驻场实时风怒 + 限时修饰(加法模型) */
public getFinalWindFury(): number {
const base = this.getRuntimeWindFury();
const mods = this.aggregateTimedMods(Attrs.wfuny);
return base + mods.flatSum + mods.pctSum;
}
/** 最终暴击抗性 = 基础 + 限时修饰(加法模型) */
public getFinalCriticalRes(): number {
const mods = this.aggregateTimedMods(Attrs.critical_res);
return this.critical_res + mods.flatSum + mods.pctSum;
}
/** 最终冰冻抗性 = 基础 + 限时修饰(加法模型) */
public getFinalFreezeRes(): number {
const mods = this.aggregateTimedMods(Attrs.freeze_res);
return this.freeze_res + mods.flatSum + mods.pctSum;
}
/** 最终击晕抗性 = 基础 + 限时修饰(加法模型) */
public getFinalStunRes(): number {
const mods = this.aggregateTimedMods(Attrs.stun_res);
return this.stun_res + mods.flatSum + mods.pctSum;
}
/** 最终击退抗性 = 基础 + 限时修饰(加法模型) */
public getFinalKnockbackRes(): number {
const mods = this.aggregateTimedMods(Attrs.knockback_res);
return this.knockback_res + mods.flatSum + mods.pctSum;
}
/** 根据攻速加成换算实际攻击间隔,避免直接改写配置里的基础 CD。 */
public getEffectiveSkillCd(skillId: number): number {
const skill = this.skills[skillId];
if (!skill) return 0;
if (skill.cd <= 0) return 0;
const speedBonus = this.getRuntimeAttackSpeedBonus();
if (speedBonus <= 0) return skill.cd;
const speedRate = 1 + speedBonus / 100;
return Math.max(HeroAttrsComp.minAttackCd, skill.cd / speedRate);
}
// ==================== 技能距离缓存管理 ====================
/**
* 更新技能距离缓存
* 在技能初始化、新增技能、MP变化时调用
* @param skillsComp 技能组件
*/
public updateSkillDistanceCache(): void {
const maxRange = this.dis;
let minRange = 0;
this.maxSkillDistance = maxRange;
this.minSkillDistance = minRange;
}
/**
* 获取缓存的最远技能攻击距离
* @returns 最远攻击距离
*/
public getCachedMaxSkillDistance(): number {
return this.maxSkillDistance;
}
/**
* 获取缓存的最近技能攻击距离
* @returns 最近攻击距离
*/
public getCachedMinSkillDistance(): number {
return this.minSkillDistance;
}
reset() {
// 重置为初始状态
this.hero_uuid = 1001;
this.hero_name = "hero";
this.lv = 1;
this.type = 0;
this.fac = 0;
this.base_ap = 0;
this.base_hp = 0;
this.ap = 0;
this.hp = 100;
this.hp_max = 100;
this.speed = 100;
this.dis = 100;
this.shield = 0;
// 重置新增属性
this.skills = {};
this.call = undefined;
this.dead = undefined;
this.fstart = undefined;
this.fend = undefined;
this.atking = undefined;
this.atked = undefined;
this.revive = undefined;
this.critical = 0;
this.critical_res = 0;
this.freeze_chance = 0;
this.freeze_res = 0;
this.stun_chance = 0;
this.stun_res = 0;
this.knockback_chance = 0;
this.knockback_distance = 0;
this.knockback_res = 0;
this.crit_damage = 0;
this.revived_count = 0;
this.invincible_time = 0;
this.puncture_chance = 0;
this.wfuny = 0;
this.boom = false;
this.frost_end_time = 0;
this.stun_end_time = 0;
// 重置技能距离缓存
this.maxSkillDistance = 0;
this.minSkillDistance = 0;
this.posIndex = -1;
this.is_dead = false;
this.is_count_dead = false;
this.is_atking = false;
this.is_stop = false;
this.is_boss = false;
this.is_big_boss = false;
this.is_friend = false;
this.is_kalami = false;
this.is_reviving = false;
this.atk_count = 0;
this.atked_count = 0;
this.killed_count = 0;
this.combat_target_eid = -1;
this.enemy_in_cast_range = false;
// 重置脏标签
this.dirty_hp = false;
this.dirty_shield = false;
this.dirty_lv = false;
}
}
@ecs.register('HeroBuffSystem')
export class HeroBuffSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
private timer = new Timer(0.1)
filter(): ecs.IMatcher {
return ecs.allOf(HeroAttrsComp);
}
update(e: ecs.Entity): void {
if (this.timer.update(this.dt)) {
const attrsComp = e.get(HeroAttrsComp);
if (attrsComp.frost_end_time > 0) {
attrsComp.frost_end_time -= 0.1;
if (attrsComp.frost_end_time <= 0) {
attrsComp.frost_end_time = 0;
}
}
if (attrsComp.stun_end_time > 0) {
attrsComp.stun_end_time -= 0.1;
if (attrsComp.stun_end_time <= 0) {
attrsComp.stun_end_time = 0;
}
}
}
void e;
}
}