From c4e7f2584b8def7a81e3b665506a915d24a18560 Mon Sep 17 00:00:00 2001 From: pan Date: Thu, 23 Jul 2026 15:48:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(hero):=20=E6=96=B0=E5=A2=9E=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E8=AE=A1=E6=97=B6=E6=80=A7Buff/Debuff=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交实现了游戏内完整的计时性增益/减益系统,包含: 1. 新增Buff配置表与运行时数据组件 2. 实现属性修饰器与最终属性计算逻辑 3. 完成Buff管理、计时结算与周期效果处理 4. 兼容旧有控制状态系统,支持平滑过渡 5. 附带完整的系统设计文档 同时关闭了两个闲置的任务界面节点。 --- assets/resources/gui/element/mission.prefab | 4 +- assets/script/game/common/config/BuffSet.ts | 77 +++++++ .../script/game/common/config/BuffSet.ts.meta | 9 + assets/script/game/hero/BuffComp.ts | 44 ++++ assets/script/game/hero/BuffComp.ts.meta | 9 + assets/script/game/hero/BuffManager.ts | 209 ++++++++++++++++++ assets/script/game/hero/BuffManager.ts.meta | 9 + assets/script/game/hero/BuffSystem.ts | 115 ++++++++++ assets/script/game/hero/BuffSystem.ts.meta | 9 + assets/script/game/hero/HeroAttrsComp.ts | 72 ++++++ .../2026-07-23-timed-buff-system-design.md | 159 +++++++++++++ 11 files changed, 714 insertions(+), 2 deletions(-) create mode 100644 assets/script/game/common/config/BuffSet.ts create mode 100644 assets/script/game/common/config/BuffSet.ts.meta create mode 100644 assets/script/game/hero/BuffComp.ts create mode 100644 assets/script/game/hero/BuffComp.ts.meta create mode 100644 assets/script/game/hero/BuffManager.ts create mode 100644 assets/script/game/hero/BuffManager.ts.meta create mode 100644 assets/script/game/hero/BuffSystem.ts create mode 100644 assets/script/game/hero/BuffSystem.ts.meta create mode 100644 docs/superpowers/specs/2026-07-23-timed-buff-system-design.md diff --git a/assets/resources/gui/element/mission.prefab b/assets/resources/gui/element/mission.prefab index 58af7077..2f3b8b07 100644 --- a/assets/resources/gui/element/mission.prefab +++ b/assets/resources/gui/element/mission.prefab @@ -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 diff --git a/assets/script/game/common/config/BuffSet.ts b/assets/script/game/common/config/BuffSet.ts new file mode 100644 index 00000000..328d97e3 --- /dev/null +++ b/assets/script/game/common/config/BuffSet.ts @@ -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 = { + // 攻击强化(限时版) + 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: "击晕目标,无法行动", + }, +}; diff --git a/assets/script/game/common/config/BuffSet.ts.meta b/assets/script/game/common/config/BuffSet.ts.meta new file mode 100644 index 00000000..b8a8541b --- /dev/null +++ b/assets/script/game/common/config/BuffSet.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "ef985116-4023-43ae-9a69-69d019139c27", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/script/game/hero/BuffComp.ts b/assets/script/game/hero/BuffComp.ts new file mode 100644 index 00000000..1880dd83 --- /dev/null +++ b/assets/script/game/hero/BuffComp.ts @@ -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 = new Map(); + /** buff 列表变更标记(预留 UI 消费) */ + dirty_buffs: boolean = false; + + reset() { + this.buffs.clear(); + this.dirty_buffs = false; + } +} diff --git a/assets/script/game/hero/BuffComp.ts.meta b/assets/script/game/hero/BuffComp.ts.meta new file mode 100644 index 00000000..b295daff --- /dev/null +++ b/assets/script/game/hero/BuffComp.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "ac7fa294-b313-4649-94d9-88b7d190d31b", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/script/game/hero/BuffManager.ts b/assets/script/game/hero/BuffManager.ts new file mode 100644 index 00000000..469ce0b2 --- /dev/null +++ b/assets/script/game/hero/BuffManager.ts @@ -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_stack:push 新层 + * - 已满:替换最老层(数组末尾) + * + * 控制类(is_control)额外双写旧字段(frost/stun)。 + * + * @param target 目标实体 + * @param buffId BuffList 配置 id + * @param sourceUuid 施法者 uuid(0=系统/无主) + * @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(); diff --git a/assets/script/game/hero/BuffManager.ts.meta b/assets/script/game/hero/BuffManager.ts.meta new file mode 100644 index 00000000..67b546ca --- /dev/null +++ b/assets/script/game/hero/BuffManager.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "e4a3112b-cb27-4fd8-bd87-542ecf8ba5ee", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/script/game/hero/BuffSystem.ts b/assets/script/game/hero/BuffSystem.ts new file mode 100644 index 00000000..5798a819 --- /dev/null +++ b/assets/script/game/hero/BuffSystem.ts @@ -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]; + + // 永久 buff(duration<=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); + } + } +} diff --git a/assets/script/game/hero/BuffSystem.ts.meta b/assets/script/game/hero/BuffSystem.ts.meta new file mode 100644 index 00000000..faca4614 --- /dev/null +++ b/assets/script/game/hero/BuffSystem.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "c0d3f608-da5a-4f1d-8cbc-5c339d5f2320", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/script/game/hero/HeroAttrsComp.ts b/assets/script/game/hero/HeroAttrsComp.ts index a47c99f1..f237b75b 100644 --- a/assets/script/game/hero/HeroAttrsComp.ts +++ b/assets/script/game/hero/HeroAttrsComp.ts @@ -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]; diff --git a/docs/superpowers/specs/2026-07-23-timed-buff-system-design.md b/docs/superpowers/specs/2026-07-23-timed-buff-system-design.md new file mode 100644 index 00000000..874fceca --- /dev/null +++ b/docs/superpowers/specs/2026-07-23-timed-buff-system-design.md @@ -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 System,tick 驱动 + └── BuffManager.ts 新增:对外 API(apply/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 = { /* 配置项 */ }; +``` + +### 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 = 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 驱动 +- 永久 buff(duration=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 2:HeroAttrsComp 新增 getter,不改旧字段 +- Step 3:SkillSet 新增 timed_buff_id,SCastSystem 新旧路径并存,消费方逐个改用 getter +- Step 4:控制类 buff 迁移(双写),最后删除 HeroBuffSystem 与旧字段 + +## 9. 风险与兜底 + +| 风险 | 兜底 | +|---|---| +| getter 未覆盖所有读取点 | grep 全量排查 | +| max_stack=0 无限叠加内存泄漏 | 超阈值打 warning | +| BuffComp 未 reset 残留 | reset() 清空,英雄回收时调用 | +| 双 System 冲突 | 兼容期 BuffSystem 接管,HeroBuffSystem 改空或删除 |