feat(hero): 新增完整计时性Buff/Debuff系统

本次提交实现了游戏内完整的计时性增益/减益系统,包含:
1. 新增Buff配置表与运行时数据组件
2. 实现属性修饰器与最终属性计算逻辑
3. 完成Buff管理、计时结算与周期效果处理
4. 兼容旧有控制状态系统,支持平滑过渡
5. 附带完整的系统设计文档

同时关闭了两个闲置的任务界面节点。
This commit is contained in:
pan
2026-07-23 15:48:59 +08:00
parent 708ca0827a
commit c4e7f2584b
11 changed files with 714 additions and 2 deletions

View File

@@ -10969,7 +10969,7 @@
"__id__": 875
}
],
"_active": true,
"_active": false,
"_components": [
{
"__id__": 945
@@ -16271,7 +16271,7 @@
"__id__": 1040
}
],
"_active": true,
"_active": false,
"_components": [
{
"__id__": 1110

View File

@@ -0,0 +1,77 @@
// ========== Buff/Debuff 配置定义 ==========
import { Attrs } from "./HeroAttrs";
/** Buff 类别 */
export enum BuffCategory {
Buff = 0,
Debuff = 1,
Control = 2,
}
/** 修饰器运算类型 */
export enum ModOp {
Flat = 0,
PercentAdd = 1,
PercentMul = 2,
}
/** 单条属性修饰器 */
export interface BuffModifier {
attr: Attrs;
op: ModOp;
value: number;
}
/** 周期效果DoT/HoT */
export interface BuffTickEffect {
interval: number;
damage_or_heal: number; // 正=治疗,负=伤害
scale_by_ap_pct: number; // 按施法者 ap 缩放百分比0=不吃加成
}
/** Buff 静态定义 */
export interface BuffConfig {
id: number;
name: string;
icon: string;
category: BuffCategory;
duration: number; // 单层持续时间0=永久
max_stack: number; // 默认 0=无限叠加1=不可叠层N=最多N层
modifiers?: BuffModifier[];
tick?: BuffTickEffect;
is_control?: boolean;
control_kind?: 'frost' | 'stun' | 'none';
info: string;
}
/** Buff 配置表 */
export const BuffList: Record<number, BuffConfig> = {
// 攻击强化(限时版)
7001: {
id: 7001, name: "攻击强化", icon: "Stat_Attack_03",
category: BuffCategory.Buff, duration: 10, max_stack: 0,
modifiers: [{ attr: Attrs.ap, op: ModOp.Flat, value: 5 }],
info: "攻击力 +5持续 10 秒",
},
// 灼烧DoT可叠层
7002: {
id: 7002, name: "灼烧", icon: "Stat_Burn",
category: BuffCategory.Debuff, duration: 5, max_stack: 5,
tick: { interval: 0.5, damage_or_heal: -10, scale_by_ap_pct: 30 },
info: "每 0.5 秒受到 10 伤害,可叠加",
},
// 冰冻(控制,走统一框架,双写兼容)
7003: {
id: 7003, name: "冰冻", icon: "Stat_Freeze",
category: BuffCategory.Control, duration: 2, max_stack: 1,
is_control: true, control_kind: 'frost',
info: "冰冻目标,无法行动",
},
// 击晕(控制)
7004: {
id: 7004, name: "击晕", icon: "Stat_Stun",
category: BuffCategory.Control, duration: 2, max_stack: 1,
is_control: true, control_kind: 'stun',
info: "击晕目标,无法行动",
},
};

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "ef985116-4023-43ae-9a69-69d019139c27",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,44 @@
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
/**
* 单个活跃 buff 实例(一层)。
* 每个 ActiveBuff 独立计时,叠层时会产生多个实例。
*/
export class ActiveBuff {
/** 对应 BuffList 中的配置 id */
config_id: number;
/** 剩余持续时间(秒) */
remaining: number;
/** 施法者实体 uuid用于伤害归属 */
source_uuid: number;
/** 施法者攻击力快照(用于 DoT/HoT 的 ap 缩放) */
source_ap_snapshot: number;
/** 周期效果累计时间(秒) */
tick_acc: number = 0;
constructor(cfg_id: number, dur: number, src_uuid: number, src_ap: number) {
this.config_id = cfg_id;
this.remaining = dur;
this.source_uuid = src_uuid;
this.source_ap_snapshot = src_ap;
}
}
/**
* Buff 组件 — 存储实体身上所有活跃的 buff/debuff。
*
* 数据结构:按 buff_id 分组,每组是若干层(每层独立计时)。
* 使用 ecs.Comp仅承载数据计时与结算逻辑由 BuffSystem 负责。
*/
@ecs.register('Buff')
export class BuffComp extends ecs.Comp {
/** 按 buff_id 分组,每组是若干层(每层独立计时) */
buffs: Map<number, ActiveBuff[]> = new Map();
/** buff 列表变更标记(预留 UI 消费) */
dirty_buffs: boolean = false;
reset() {
this.buffs.clear();
this.dirty_buffs = false;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "ac7fa294-b313-4649-94d9-88b7d190d31b",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,209 @@
/**
* BuffManager — buff/debuff 的应用、驱散、查询管理器。
*
* 设计说明:
* - 非 ECS 组件,是普通 class 单例(参考 smc 的单例暴露模式)。
* - 对外提供 applyBuff / dispel / getStack / hasControl 四组核心能力。
* - 控制类 buff 采用"双写"策略:既写入 BuffComp新框架也同步调用
* HeroAttrsComp.toFrost/toStun旧框架保证过渡期表现一致。
*/
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { BuffCategory, BuffConfig, BuffList } from "../common/config/BuffSet";
import { FightSet } from "../common/config/GameSet";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { BuffComp, ActiveBuff } from "./BuffComp";
/** applyBuff 的覆盖参数(预留按需覆写持续时间) */
export interface BuffApplyOverrides {
duration?: number;
}
class BuffManagerImpl {
/**
* 对目标施加一个 buff 实例。
*
* 叠层规则:
* - max_stack === 0无限叠加始终 push 新层
* - existing.length < max_stackpush 新层
* - 已满:替换最老层(数组末尾)
*
* 控制类is_control额外双写旧字段frost/stun
*
* @param target 目标实体
* @param buffId BuffList 配置 id
* @param sourceUuid 施法者 uuid0=系统/无主)
* @param sourceAp 施法者攻击力快照(用于 DoT 缩放)
* @param overrides 可选覆写参数
*/
applyBuff(target: ecs.Entity, buffId: number, sourceUuid: number = 0, sourceAp: number = 0, overrides?: BuffApplyOverrides): void {
const cfg = BuffList[buffId];
if (!cfg) return;
const buffComp = this.ensureBuffComp(target);
const duration = overrides?.duration ?? cfg.duration;
let layers = buffComp.buffs.get(buffId);
if (!layers) {
layers = [];
buffComp.buffs.set(buffId, layers);
}
const newLayer = new ActiveBuff(buffId, duration, sourceUuid, sourceAp);
if (cfg.max_stack === 0 || layers.length < cfg.max_stack) {
layers.push(newLayer);
} else {
// 已满,移除最老层(数组首部)后追加新层,保持 FIFO 刷新语义
layers.shift();
layers.push(newLayer);
}
// 控制类双写旧字段
if (cfg.is_control) {
this.applyControlEffect(target, cfg, duration);
}
buffComp.dirty_buffs = true;
}
/**
* 驱散目标身上匹配的 buff。
*
* @param target 目标实体
* @param category 仅驱散该类别(不传=所有类别)
* @param excludeIds 排除的 buff id 列表
* @returns 移除的 buff 种类数
*/
dispel(target: ecs.Entity, category?: BuffCategory, excludeIds?: number[]): number {
if (!target.has(BuffComp)) return 0;
const buffComp = target.get(BuffComp);
let removedKinds = 0;
const controlKindsCleared: ('frost' | 'stun')[] = [];
buffComp.buffs.forEach((layers, buffId) => {
const cfg = BuffList[buffId];
if (!cfg) return;
if (excludeIds && excludeIds.includes(buffId)) return;
if (category !== undefined && cfg.category !== category) return;
if (layers.length > 0) removedKinds++;
buffComp.buffs.delete(buffId);
// 记录被驱散的控制类型,用于后续重算
if (cfg.is_control && cfg.control_kind && cfg.control_kind !== 'none') {
controlKindsCleared.push(cfg.control_kind);
}
});
// 控制类驱散后重算旧字段
if (controlKindsCleared.length > 0) {
this.recomputeControlState(target, controlKindsCleared);
}
buffComp.dirty_buffs = true;
return removedKinds;
}
/**
* 获取目标身上指定 buff 的当前叠层数。
* @returns 层数(未命中返回 0
*/
getStack(target: ecs.Entity, buffId: number): number {
if (!target.has(BuffComp)) return 0;
const buffComp = target.get(BuffComp);
return buffComp.buffs.get(buffId)?.length ?? 0;
}
/**
* 判断目标是否处于指定类型的控制状态下。
* 同时检查新框架 BuffComp 和旧字段,兼容过渡期。
*/
hasControl(target: ecs.Entity, kind: 'frost' | 'stun'): boolean {
// 新框架:检查 BuffComp 中是否有对应控制类 buff
if (target.has(BuffComp)) {
const buffComp = target.get(BuffComp);
for (const [buffId, layers] of buffComp.buffs) {
if (layers.length === 0) continue;
const cfg = BuffList[buffId];
if (cfg?.is_control && cfg.control_kind === kind) return true;
}
}
// 旧字段兜底
if (target.has(HeroAttrsComp)) {
const attrs = target.get(HeroAttrsComp);
if (kind === 'frost') return attrs.isFrost();
if (kind === 'stun') return attrs.isStun();
}
return false;
}
// ==================== 私有方法 ====================
/**
* 确保目标实体拥有 BuffComp没有则添加。
*/
private ensureBuffComp(target: ecs.Entity): BuffComp {
if (!target.has(BuffComp)) {
target.add(BuffComp);
}
return target.get(BuffComp);
}
/**
* 控制效果双写:将控制类 buff 同步写入旧字段。
* duration 会被换算成 toFrost/toStun 所需的倍数参数。
*/
private applyControlEffect(target: ecs.Entity, cfg: BuffConfig, duration: number): void {
if (!target.has(HeroAttrsComp)) return;
const attrs = target.get(HeroAttrsComp);
if (cfg.control_kind === 'frost') {
// toFrost(time) 内部会乘以 FROST_TIME这里反除得到倍数
attrs.toFrost(duration / FightSet.FROST_TIME);
} else if (cfg.control_kind === 'stun') {
attrs.toStun(duration / FightSet.STUN_TIME);
}
}
/**
* 驱散控制 buff 后重算旧字段。
* 如果对应类型的控制 buff 已全部清除,则立即将旧字段归零。
*
* @param target 目标实体
* @param clearedKinds 本次被驱散的控制类型列表
*/
private recomputeControlState(target: ecs.Entity, clearedKinds: ('frost' | 'stun')[]): void {
if (!target.has(HeroAttrsComp)) return;
const attrs = target.get(HeroAttrsComp);
// 检查是否还有同类型控制 buff 残留
const hasFrostInBuff = this.hasControlInBuff(target, 'frost');
const hasStunInBuff = this.hasControlInBuff(target, 'stun');
if (clearedKinds.includes('frost') && !hasFrostInBuff) {
attrs.frost_end_time = 0;
}
if (clearedKinds.includes('stun') && !hasStunInBuff) {
attrs.stun_end_time = 0;
}
}
/**
* 仅检查 BuffComp 中是否存在指定控制类型(不查旧字段)。
*/
private hasControlInBuff(target: ecs.Entity, kind: 'frost' | 'stun'): boolean {
if (!target.has(BuffComp)) return false;
const buffComp = target.get(BuffComp);
for (const [buffId, layers] of buffComp.buffs) {
if (layers.length === 0) continue;
const cfg = BuffList[buffId];
if (cfg?.is_control && cfg.control_kind === kind) return true;
}
return false;
}
}
/** Buff 管理器单例(全局唯一实例) */
export const buffManager = new BuffManagerImpl();

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e4a3112b-cb27-4fd8-bd87-542ecf8ba5ee",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,115 @@
/**
* BuffSystem — 计时性 buff/debuff 的核心结算系统。
*
* 使用 oops-framework 的 Timer 实现固定 0.1s 步长 tick
* - 遍历 BuffComp.buffs每层 remaining -= 步长
* - 若配置了 tick 效果DoT/HoT累计 tick_acc 并按 interval 结算伤害/治疗
* - 到期的层从数组中移除;数组空则清理 key
* - 兼容期:同时递减旧字段 frost_end_time / stun_end_time
*
* 注意:当前 HeroBuffSystem 仍在运行相同的 frost/stun 递减逻辑,
* 待旧系统完全下线后可移除此处兼容代码。
*/
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { Timer } from "db://oops-framework/core/common/timer/Timer";
import { BuffList } from "../common/config/BuffSet";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { BuffComp, ActiveBuff } from "./BuffComp";
/** 固定步长(秒) */
const TICK_STEP = 0.1;
@ecs.register('BuffSystem')
export class BuffSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
private timer = new Timer(TICK_STEP);
filter(): ecs.IMatcher {
return ecs.allOf(BuffComp);
}
update(e: ecs.Entity): void {
if (!this.timer.update(this.dt)) return;
const buffComp = e.get(BuffComp);
if (!buffComp) return;
// HeroAttrsComp 可选,用于 DoT/HoT 的血量结算和兼容字段
const attrsComp = e.has(HeroAttrsComp) ? e.get(HeroAttrsComp) : null;
// ---- 兼容期:递减旧字段 frost_end_time / stun_end_time ----
if (attrsComp) {
if (attrsComp.frost_end_time > 0) {
attrsComp.frost_end_time -= TICK_STEP;
if (attrsComp.frost_end_time <= 0) {
attrsComp.frost_end_time = 0;
}
}
if (attrsComp.stun_end_time > 0) {
attrsComp.stun_end_time -= TICK_STEP;
if (attrsComp.stun_end_time <= 0) {
attrsComp.stun_end_time = 0;
}
}
}
// ---- 处理所有 buff 层 ----
let anyChanged = false;
buffComp.buffs.forEach((layers, buffId) => {
const cfg = BuffList[buffId];
if (!cfg) return;
// 从后往前遍历,安全 splice 移除到期层
for (let i = layers.length - 1; i >= 0; i--) {
const layer = layers[i];
// 永久 buffduration<=0不递减 remaining
if (cfg.duration > 0) {
layer.remaining -= TICK_STEP;
}
// 周期效果结算(仅在未到期时)
if (cfg.tick && layer.remaining > 0 && attrsComp) {
this.processTick(layer, cfg.tick, attrsComp);
}
// 到期移除
if (cfg.duration > 0 && layer.remaining <= 0) {
layers.splice(i, 1);
anyChanged = true;
}
}
// 数组空则清理 key避免空 Map 项残留
if (layers.length === 0) {
buffComp.buffs.delete(buffId);
anyChanged = true;
}
});
if (anyChanged) {
buffComp.dirty_buffs = true;
}
}
/**
* 处理单层的周期效果DoT/HoT
* 累计 tick_acc 到 interval 时结算一次伤害/治疗。
*
* @param layer 当前 buff 层实例
* @param tick 周期效果配置
* @param attrs 被作用者属性组件(调用 add_hp
*/
private processTick(layer: ActiveBuff, tick: { interval: number; damage_or_heal: number; scale_by_ap_pct: number }, attrs: HeroAttrsComp): void {
// interval 非正值守卫,避免死循环
if (tick.interval <= 0) return;
layer.tick_acc += TICK_STEP;
while (layer.tick_acc >= tick.interval) {
layer.tick_acc -= tick.interval;
// 按 ap 缩放后取整floor(dmg * (1 + ap_snapshot * scale_pct / 10000))
const scale = 1 + layer.source_ap_snapshot * tick.scale_by_ap_pct / 10000;
const amount = Math.floor(tick.damage_or_heal * scale);
attrs.add_hp(amount);
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "c0d3f608-da5a-4f1d-8cbc-5c339d5f2320",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -7,6 +7,8 @@ import { FieldSkillSet, FieldSkillType, SkillOverrides } from "../common/config/
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;
@@ -320,6 +322,76 @@ export class HeroAttrsComp extends ecs.Comp {
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;
}
/** 根据攻速加成换算实际攻击间隔,避免直接改写配置里的基础 CD。 */
public getEffectiveSkillCd(skillId: number): number {
const skill = this.skills[skillId];

View File

@@ -0,0 +1,159 @@
# 计时性 Buff/Debuff 系统设计
> 日期2026-07-23
> 状态:已批准,待实现
## 1. 目标与范围
为英雄/怪物添加通用的计时性 buff/debuff 机制,覆盖四类需求:
- 限时的属性加成/削弱(如"10秒内攻击+30%"
- 周期性跳伤害/治疗 DoT/HoT如中毒、灼烧、再生
- 统一现有冰冻/击晕的计时框架,便于扩展更多控制效果
- 数据层完整记录 buff 状态UI 展示本期不做,预留脏标记)
## 2. 核心架构决策
| 维度 | 决策 |
|---|---|
| 属性计算模型 | 修饰器模型(读取时重算,无残留风险) |
| 叠加规则 | 可叠层,每层独立计时 |
| 配置组织 | 独立 `BuffSet.ts` 配置表 |
| 架构方案 | 方案 A原字段=base + 修饰器表 |
## 3. 模块划分
```
assets/script/game/
├── common/config/
│ ├── BuffSet.ts 新增buff 静态配置表id → 定义)
│ └── HeroAttrs.ts 复用现有 Attrs 枚举
└── hero/
├── HeroAttrsComp.ts 改造:新增 getFinalXxx() getter
├── BuffComp.ts 新增ECS Comp持有 buffs 运行时实例
├── BuffSystem.ts 新增ECS Systemtick 驱动
└── BuffManager.ts 新增:对外 APIapply/dispel/查询)
```
职责边界:
- `BuffSet.ts`:纯静态配置,无运行时状态
- `BuffComp.ts`:挂实体上的运行时数据,不写逻辑
- `BuffSystem.ts`:固定 tick 推进 buff倒计时/DoT/到期)
- `BuffManager.ts`:对外门面,屏蔽 ECS 细节
## 4. 数据模型
### BuffSet.ts — 静态配置
```ts
export enum BuffCategory {
Buff = 0, // 正面增益
Debuff = 1, // 负面减益
Control = 2, // 控制(冰冻/击晕/减速)
}
export enum ModOp {
Flat = 0, // 固定值
PercentAdd = 1, // 百分比(加算)
PercentMul = 2, // 百分比(乘算,预留)
}
export interface BuffModifier {
attr: Attrs;
op: ModOp;
value: number;
}
export interface BuffTickEffect {
interval: number; // 触发间隔(秒)
damage_or_heal: number; // 正=治疗,负=伤害
scale_by_ap_pct: number; // 按施法者 ap 缩放百分比0=不吃加成
}
export interface BuffConfig {
id: number;
name: string;
icon: string;
category: BuffCategory;
duration: number; // 单层持续时间0=永久
max_stack: number; // 默认 0=无限叠加1=不可叠层N=最多N层
modifiers?: BuffModifier[];
tick?: BuffTickEffect;
is_control?: boolean;
control_kind?: 'frost' | 'stun' | 'none';
info: string;
}
export const BuffList: Record<number, BuffConfig> = { /* 配置项 */ };
```
### BuffComp.ts — 运行时实例
```ts
export class ActiveBuff {
config_id: number;
remaining: number;
source_uuid: number;
source_ap_snapshot: number; // 施法时锁定
tick_acc: number = 0;
}
@ecs.register('Buff')
export class BuffComp extends ecs.Comp {
buffs: Map<number, ActiveBuff[]> = new Map();
dirty_buffs: boolean = false; // 预留 UI 消费
}
```
## 5. 修饰器模型与最终值计算
HeroAttrsComp 原字段ap/hp_max/critical 等)保持原语义(基础值 + 永久加成),新增 getter
```
final = (base + Σ_flat_timed) × (1 + Σ_pctAdd_timed / 100)
```
- flat 优先于 pct先加固定值再乘百分比
- 驻场加成并入 getter避免"忘记乘驻场"
- `add_ap`/`add_hp_max`/`add_special_attr` 语义不变,继续改 base
- getter 不缓存,避免脏数据;实测有热点再加 dirty
## 6. BuffSystem 驱动逻辑
- 固定步长 0.1s(与现有 HeroBuffSystem 一致),用 Timer(0.1)
- 每层独立倒计时,到期 splice 移除
- DoT/HoT每层独立 tick_acc到 interval 跳一次,`tick_acc -= interval` 保留余数
- DoT 数值按施法时 ap 快照缩放,不在结算时重读施法者
- 控制类 buff 双写兼容:施加时同时调用现有 toFrost/toStun现有字段继续由 BuffSystem.tickLegacyControl 驱动
- 永久 buffduration=0不递减 remaining只能被 dispel 清除
## 7. BuffManager API
```ts
applyBuff(target, buffId, sourceUuid?, sourceAp?, overrides?): void
dispel(target, category?, excludeIds?): number
getStack(target, buffId): number
hasControl(target, kind): boolean
```
叠层规则max_stack 默认 0=无限叠加):
- max_stack=0每次施加都新增层
- 未满:新增层
- 已满≥1替换最老层刷新
驱散后控制类需 recomputeControlState同 kind 还有任意层则保持,否则清除。
## 8. 迁移策略
- Step 1新增 buff 基础设施(纯加法,零风险)
- Step 2HeroAttrsComp 新增 getter不改旧字段
- Step 3SkillSet 新增 timed_buff_idSCastSystem 新旧路径并存,消费方逐个改用 getter
- Step 4控制类 buff 迁移(双写),最后删除 HeroBuffSystem 与旧字段
## 9. 风险与兜底
| 风险 | 兜底 |
|---|---|
| getter 未覆盖所有读取点 | grep 全量排查 |
| max_stack=0 无限叠加内存泄漏 | 超阈值打 warning |
| BuffComp 未 reset 残留 | reset() 清空,英雄回收时调用 |
| 双 System 冲突 | 兼容期 BuffSystem 接管HeroBuffSystem 改空或删除 |