Compare commits
4 Commits
708ca0827a
...
1afa06efb8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1afa06efb8 | ||
|
|
2381812d06 | ||
|
|
9a5093d2a4 | ||
|
|
c4e7f2584b |
File diff suppressed because it is too large
Load Diff
77
assets/script/game/common/config/BuffSet.ts
Normal file
77
assets/script/game/common/config/BuffSet.ts
Normal 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: "击晕目标,无法行动",
|
||||
},
|
||||
};
|
||||
9
assets/script/game/common/config/BuffSet.ts.meta
Normal file
9
assets/script/game/common/config/BuffSet.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ef985116-4023-43ae-9a69-69d019139c27",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -15,12 +15,12 @@ export enum Attrs {
|
||||
dis = "dis", // 基础距离
|
||||
shield = "shield", // 当前护盾
|
||||
|
||||
// ==================== 攻击属性 ====================
|
||||
a_cd = "a_cd", // 攻击计时
|
||||
s_cd = "s_cd", // 技能计时
|
||||
a_cd_max = "a_cd_max", // 攻击CD
|
||||
s_cd_max = "s_cd_max", // 技能CD
|
||||
|
||||
// // ==================== 攻击属性 ====================
|
||||
// a_cd = "a_cd", // 攻击计时
|
||||
// s_cd = "s_cd", // 技能计时
|
||||
// a_cd_max = "a_cd_max", // 攻击CD
|
||||
// s_cd_max = "s_cd_max", // 技能CD
|
||||
|
||||
// ==================== 暴击与命中属性 ====================
|
||||
critical = "critical", // 暴击率
|
||||
critical_damage = "critical_damage", // 暴击伤害
|
||||
@@ -44,27 +44,27 @@ export enum Attrs {
|
||||
*/
|
||||
export interface GameScoreStats {
|
||||
score: number; // 基础得分
|
||||
|
||||
|
||||
// 战斗统计
|
||||
crt_count: number; // 暴击次数
|
||||
wf_count: number; // 风怒次数
|
||||
dod_count: number; // 闪避次数
|
||||
stun_count: number; // 击晕次数
|
||||
freeze_count: number; // 冰冻次数
|
||||
|
||||
|
||||
// 伤害统计
|
||||
total_dmg: number; // 总伤害
|
||||
atk_count: number; // 攻击次数 (用于计算平均伤害)
|
||||
avg_dmg: number; // 平均伤害
|
||||
thorns_dmg: number; // 反伤伤害
|
||||
crit_dmg_total: number; // 暴击伤害总额
|
||||
|
||||
|
||||
// 生存统计
|
||||
heal_total: number; // 治疗总量
|
||||
lifesteal_total: number;// 吸血总量
|
||||
shield_block_count: number; // 格挡次数
|
||||
dead_trigger_count: number; // 死亡触发次数
|
||||
|
||||
|
||||
// 资源统计
|
||||
exp_total: number; // 经验总数
|
||||
gold_total: number; // 金币总数
|
||||
|
||||
@@ -154,6 +154,7 @@ export interface SkillConfig {
|
||||
stun?: number, // 额外击晕概率
|
||||
bck?: number, // 额外击退概率
|
||||
buff_type?: Attrs, // Buff 类型 (单一职责)
|
||||
timed_buff_id?: number, // 计时性 buff 配置 id(指向 BuffList),存在时走 buffManager 而非永久 add_*
|
||||
call_hero?: number, // 召唤技能召唤英雄id(可选)
|
||||
is_accel?: boolean, // 是否逐渐加速飞行
|
||||
info: string, // 技能描述
|
||||
@@ -170,6 +171,7 @@ export interface SkillOverrides {
|
||||
stun?: number;
|
||||
bck?: number;
|
||||
buff_type?: Attrs;
|
||||
timed_buff_id?: number;
|
||||
call_hero?: number;
|
||||
is_accel?: boolean;
|
||||
}
|
||||
|
||||
44
assets/script/game/hero/BuffComp.ts
Normal file
44
assets/script/game/hero/BuffComp.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
9
assets/script/game/hero/BuffComp.ts.meta
Normal file
9
assets/script/game/hero/BuffComp.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ac7fa294-b313-4649-94d9-88b7d190d31b",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
209
assets/script/game/hero/BuffManager.ts
Normal file
209
assets/script/game/hero/BuffManager.ts
Normal 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_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();
|
||||
9
assets/script/game/hero/BuffManager.ts.meta
Normal file
9
assets/script/game/hero/BuffManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "e4a3112b-cb27-4fd8-bd87-542ecf8ba5ee",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
115
assets/script/game/hero/BuffSystem.ts
Normal file
115
assets/script/game/hero/BuffSystem.ts
Normal 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];
|
||||
|
||||
// 永久 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/script/game/hero/BuffSystem.ts.meta
Normal file
9
assets/script/game/hero/BuffSystem.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "c0d3f608-da5a-4f1d-8cbc-5c339d5f2320",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
|
||||
|
||||
// 暴击判定
|
||||
// 使用施法者的暴击率属性(damageEvent.Attrs 快照),- 被攻击者的暴击抗性属
|
||||
const criticalChance = (damageEvent.Attrs[Attrs.critical] || 0) - (TAttrsComp.critical_res || 0);
|
||||
const criticalChance = (damageEvent.Attrs[Attrs.critical] || 0) - TAttrsComp.getFinalCriticalRes();
|
||||
const isCrit = this.checkChance(criticalChance);
|
||||
|
||||
// 计算基础伤害
|
||||
@@ -187,7 +187,7 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
|
||||
const isFrost = !TAttrsComp.isFrost() && this.checkChance(freezeChance);
|
||||
|
||||
// 击晕判定
|
||||
const stunChance = (damageEvent.Attrs[Attrs.stun_chance] || 0) - (TAttrsComp.stun_res || 0);
|
||||
const stunChance = (damageEvent.Attrs[Attrs.stun_chance] || 0) - TAttrsComp.getFinalStunRes();
|
||||
const isStun = !TAttrsComp.isStun() && this.checkChance(stunChance);
|
||||
|
||||
// 击退判定
|
||||
|
||||
@@ -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;
|
||||
@@ -113,7 +115,8 @@ export class HeroAttrsComp extends ecs.Comp {
|
||||
const oldHp = this.hp;
|
||||
let addValue = value;
|
||||
this.hp += addValue;
|
||||
this.hp = Math.max(0, Math.min(this.hp, this.hp_max));
|
||||
// 血量钳制使用 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)}`);
|
||||
@@ -320,6 +323,147 @@ 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;
|
||||
}
|
||||
|
||||
/** 最终冰冻率 = 驻场实时冰冻率 + 限时修饰(加法模型) */
|
||||
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];
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SkillTriggerType } from "../common/config/heroSet";
|
||||
import { SkillTriggerHelper } from "./SkillTriggerHelper";
|
||||
import { MissionEconomy } from "../map/MissionEconomy";
|
||||
import { MissionHeroComp } from "../map/MissionHeroComp";
|
||||
import { buffManager } from "./BuffManager";
|
||||
|
||||
/**
|
||||
* ==================== 自动施法系统 ====================
|
||||
@@ -44,18 +45,18 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
oops.message.off(GameEvent.TriggerSkill, this.onTriggerSkill, this);
|
||||
}
|
||||
|
||||
private onTriggerSkill(event: string, args: {
|
||||
s_uuid: number,
|
||||
heroAttrs?: HeroAttrsComp,
|
||||
heroView?: HeroViewComp,
|
||||
private onTriggerSkill(event: string, args: {
|
||||
s_uuid: number,
|
||||
heroAttrs?: HeroAttrsComp,
|
||||
heroView?: HeroViewComp,
|
||||
triggerType?: string,
|
||||
isCardSkill?: boolean,
|
||||
card_lv?: number,
|
||||
targetPos?: Vec3,
|
||||
overrides?: any
|
||||
overrides?: any
|
||||
}) {
|
||||
if (!args || !args.s_uuid) return;
|
||||
|
||||
|
||||
// 卡牌技能直接触发
|
||||
if (args.isCardSkill) {
|
||||
this.forceCastCardSkill(args.s_uuid, args.card_lv || 1, args.targetPos || new Vec3(FightSet.CSKILL_START_X, FightSet.CSKILL_START_Y, 0), args.overrides);
|
||||
@@ -94,12 +95,12 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
|
||||
// 构造一个模拟的 HeroAttrsComp 用于数值计算,只包含基础卡牌伤害计算所需的属性
|
||||
const mockAttrs = new HeroAttrsComp();
|
||||
|
||||
|
||||
// 动态计算卡牌的虚拟攻击力:
|
||||
// 1. 根据卡牌等级给予基础成长(同英雄升级公式,基准设为 100)
|
||||
let baseAp = 100 * Math.pow(FightSet.HERO_LV_MULTIPLIER, cardLv - 1);
|
||||
let highestAp = baseAp;
|
||||
|
||||
|
||||
// 2. 获取场上最高攻击力的英雄,保证后期奶量/增益绝对够用
|
||||
for (const eid of smc.mission.heroGrid) {
|
||||
if (eid >= 0) {
|
||||
@@ -113,7 +114,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
}
|
||||
}
|
||||
|
||||
mockAttrs.ap = highestAp;
|
||||
mockAttrs.ap = highestAp;
|
||||
mockAttrs.critical = 0;
|
||||
mockAttrs.freeze_chance = 0;
|
||||
mockAttrs.stun_chance = 0;
|
||||
@@ -121,7 +122,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
mockAttrs.fac = FacSet.HERO;
|
||||
mockAttrs.type = HType.Long; // 假定为远程,拥有较长索敌范围
|
||||
mockAttrs.dis = 2000; // 给予全屏以上的索敌范围
|
||||
|
||||
|
||||
let targetPos: Vec3 | null = null;
|
||||
if (!isFriendly) {
|
||||
// 伪造一个 view 供找敌逻辑使用,位置为 spawnPos
|
||||
@@ -140,7 +141,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log("[SCastSystem] forceCastCardSkill: casting skill", s_uuid, "castTimes", castTimes, "targetPos", targetPos);
|
||||
for (let i = 0; i < castTimes; i++) {
|
||||
if (isFriendly) {
|
||||
@@ -165,7 +166,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
|
||||
const skill = ecs.getEntity<Skill>(Skill);
|
||||
const actualStartPos = this.resolveRepeatCastStartPos(startPos, castIndex);
|
||||
|
||||
|
||||
// 伪造一个简单的 heroView 供 Skill 初始化使用,只包含方向信息
|
||||
const mockView = {
|
||||
node: { scale: new Vec3(1, 1, 1), position: actualStartPos },
|
||||
@@ -192,7 +193,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
private isOutOfBattleBounds(x: number): boolean {
|
||||
return x < BoxSet.LETF_END || x > BoxSet.RIGHT_END;
|
||||
}
|
||||
|
||||
|
||||
/** 系统过滤器:仅处理英雄实体 */
|
||||
filter(): ecs.IMatcher {
|
||||
return this.getHeroMatcher();
|
||||
@@ -205,9 +206,9 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
* 3. 选取本帧可施放技能并执行施法
|
||||
*/
|
||||
update(e: ecs.Entity): void {
|
||||
if(!smc.mission.play ) return;
|
||||
if(smc.mission.pause) return
|
||||
if(!smc.mission.in_fight) return
|
||||
if (!smc.mission.play) return;
|
||||
if (smc.mission.pause) return
|
||||
if (!smc.mission.in_fight) return
|
||||
const heroAttrs = e.get(HeroAttrsComp);
|
||||
const heroView = e.get(HeroViewComp);
|
||||
if (!heroAttrs || !heroView || !heroView.node) return;
|
||||
@@ -231,7 +232,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
heroView.playReady("yellow");
|
||||
} else if (triggerType === 'dead') {
|
||||
heroView.playOther("dead");
|
||||
}else{
|
||||
} else {
|
||||
heroView.playOther('yellow')
|
||||
}
|
||||
|
||||
@@ -273,11 +274,11 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
const sUp = SkillUpList[s_uuid] ? SkillUpList[s_uuid] : SkillUpList[1001];
|
||||
const cNum = Math.min(2, Math.max(0, Math.floor(sUp.num ?? 0)));
|
||||
const castTimes = 1 + cNum;
|
||||
let val=""
|
||||
if(castTimes >1){
|
||||
val = "*"+castTimes.toString
|
||||
let val = ""
|
||||
if (castTimes > 1) {
|
||||
val = "*" + castTimes.toString
|
||||
}
|
||||
heroView.skill_name(val,s_uuid,triggerType)
|
||||
heroView.skill_name(val, s_uuid, triggerType)
|
||||
for (let i = 0; i < castTimes; i++) {
|
||||
if (!heroView.node || !heroView.node.isValid) return;
|
||||
if (isFriendly) {
|
||||
@@ -340,12 +341,12 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
const skillLv = castPlan.skillLv;
|
||||
const overrides = castPlan.overrides;
|
||||
let config = SkillSet[s_uuid];
|
||||
const sUp = SkillUpList[s_uuid] ? SkillUpList[s_uuid]:SkillUpList[1001];
|
||||
const sUp = SkillUpList[s_uuid] ? SkillUpList[s_uuid] : SkillUpList[1001];
|
||||
const cNum = Math.min(2, Math.max(0, Math.floor(sUp.num ?? 0)));
|
||||
if (!config) return;
|
||||
|
||||
|
||||
config = mergeSkillParams(config, overrides);
|
||||
|
||||
|
||||
//播放前摇技能动画
|
||||
heroView.playReady(config.readyAnm);
|
||||
//播放角色攻击动画
|
||||
@@ -363,7 +364,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
// 注意:这里仍然是基于时间的延迟,受帧率波动影响。
|
||||
// 若需精确同步,建议在动画中添加帧事件并在 HeroViewComp 中监听。
|
||||
const delay = config.ready > 0 ? config.ready : FightSet.SKILL_CAST_DELAY;
|
||||
|
||||
|
||||
heroView.scheduleOnce(() => {
|
||||
if (!smc.mission.play || smc.mission.pause || !smc.mission.in_fight) return;
|
||||
if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return;
|
||||
@@ -407,7 +408,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
* 创建技能实体(投射物/范围体等)。
|
||||
* 仅用于对敌伤害技能的实体化表现与碰撞伤害分发。
|
||||
*/
|
||||
private createSkillEntity(s_uuid: number, skillLv: number, caster: HeroViewComp,cAttrsComp: HeroAttrsComp, targetPos: Vec3, castIndex: number = 0, overrides?: SkillOverrides) {
|
||||
private createSkillEntity(s_uuid: number, skillLv: number, caster: HeroViewComp, cAttrsComp: HeroAttrsComp, targetPos: Vec3, castIndex: number = 0, overrides?: SkillOverrides) {
|
||||
if (!caster.node || !caster.node.isValid) return;
|
||||
const parent = caster.node.parent;
|
||||
if (!parent) return;
|
||||
@@ -443,12 +444,12 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
private applyFriendlySkillEffects(_s_uuid: number, _skillLv: number, config: SkillConfig, _heroView: HeroViewComp, _cAttrsComp: HeroAttrsComp, targets: HeroViewComp[], _targetPos: Vec3 | null, isCardSkill: boolean = false) {
|
||||
const kind = config.kind ?? SkillKind.Support;
|
||||
const sUp = SkillUpList[_s_uuid] ?? SkillUpList[1001];
|
||||
const sAp =config.ap+sUp.ap*_skillLv;
|
||||
const sHit=config.hit_count+sUp.hit_count*_skillLv;
|
||||
const sAp = config.ap + sUp.ap * _skillLv;
|
||||
const sHit = config.hit_count + sUp.hit_count * _skillLv;
|
||||
const applyTargets = kind === SkillKind.Heal
|
||||
? this.pickHealTargetsByMostMissingHp(targets, sHit)
|
||||
: this.pickRandomFriendlyTargets(targets, sHit);
|
||||
|
||||
|
||||
for (const target of applyTargets) {
|
||||
this.applyActualFriendlyEffect(target, kind, sAp, _cAttrsComp, config, sUp, _skillLv);
|
||||
}
|
||||
@@ -460,13 +461,13 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
if (!target.ent) return;
|
||||
const model = target.ent.get(HeroAttrsComp);
|
||||
if (!model || model.is_dead) return;
|
||||
|
||||
|
||||
if (config.endAnm && config.endAnm !== "") {
|
||||
target.playEnd(config.endAnm);
|
||||
}
|
||||
|
||||
if (kind === SkillKind.Heal && sAp !== 0) {
|
||||
const addHp = Math.floor(sAp*_cAttrsComp.ap/100);
|
||||
const addHp = Math.floor(sAp * _cAttrsComp.ap / 100);
|
||||
model.add_hp(addHp);
|
||||
target.health(addHp);
|
||||
if (_cAttrsComp.fac === FacSet.HERO) {
|
||||
@@ -483,20 +484,32 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
MissionEconomy.addCoin(addGold);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (config.buff_type !== undefined) {
|
||||
// 计时性 buff:配置了 timed_buff_id 时走 buffManager 限时路径,
|
||||
// 同一技能不应既永久加成又限时加成,故提前 return 跳过下方永久 add_* 逻辑
|
||||
if (config.timed_buff_id) {
|
||||
buffManager.applyBuff(
|
||||
target.ent,
|
||||
config.timed_buff_id,
|
||||
_cAttrsComp.hero_uuid,
|
||||
_cAttrsComp.ap
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseValue = config.ap;
|
||||
let upgradeValue = 0;
|
||||
|
||||
|
||||
// 根据 buff 类型选择对应的升级加成
|
||||
if (config.buff_type === Attrs.ap) upgradeValue = sUp.buff_ap || 0;
|
||||
else if (config.buff_type === Attrs.hp_max) upgradeValue = sUp.buff_hp || 0;
|
||||
else if (config.buff_type === Attrs.critical) upgradeValue = sUp.crt || 0;
|
||||
// 如果后续有冰冻、击晕等,在这里加上对应的 sUp 字段即可,如 sUp.frz / sUp.stun
|
||||
|
||||
|
||||
const totalBuffValue = baseValue + upgradeValue;
|
||||
|
||||
switch (config.buff_type){
|
||||
|
||||
switch (config.buff_type) {
|
||||
case Attrs.ap:
|
||||
model.add_ap(totalBuffValue);
|
||||
break;
|
||||
@@ -507,7 +520,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
// 除了 hp_max 和 ap,其他固定属性走统一的 add_special_attr 方法
|
||||
model.add_special_attr(config.buff_type, totalBuffValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,11 +582,11 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
private collectFriendlyTargetEids(fac: number, selfEid: number | undefined, includeSelf: boolean): number[] {
|
||||
const eids: number[] = [];
|
||||
const grid = fac === FacSet.HERO ? smc.mission.heroGrid : smc.mission.monGrid;
|
||||
|
||||
|
||||
for (const eid of grid) {
|
||||
if (eid >= 0) {
|
||||
if (!includeSelf && typeof selfEid === "number" && eid === selfEid) continue;
|
||||
|
||||
|
||||
const entity = ecs.getEntityByEid(eid);
|
||||
if (entity) {
|
||||
const model = entity.get(HeroAttrsComp);
|
||||
@@ -645,7 +658,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
// 怪物找英雄
|
||||
for (let col = 0; col < 2; col++) {
|
||||
// 列内顺序:怪物配置优先中路
|
||||
const rowOrder = [1, myRow, myRow === 0 ? 2 : 0];
|
||||
const rowOrder = [1, myRow, myRow === 0 ? 2 : 0];
|
||||
for (const row of rowOrder) {
|
||||
const idx = col * 3 + row;
|
||||
const eid = smc.mission.heroGrid[idx];
|
||||
@@ -741,10 +754,10 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
halfHeight = 40; // 如果没有 UITransform,给一个默认高度偏移
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const pos = target.node.position.clone();
|
||||
pos.y += halfHeight;
|
||||
|
||||
|
||||
// 至于最终投射物是否要飞出屏幕(例如线性弹道延长至 +-500),由 SMoveSystem 统一处理
|
||||
return pos;
|
||||
}
|
||||
|
||||
@@ -210,14 +210,14 @@ export class Skill extends ecs.Entity {
|
||||
const sBck = (config.bck ?? 0) + (SUp.bck * skill_lv);
|
||||
const sAp = config.ap + (SUp.ap * skill_lv);
|
||||
const sHit = config.hit_count + (SUp.hit_count * skill_lv);
|
||||
sDataCom.Attrs[Attrs.ap] = Math.floor(cAttrsComp.ap * sAp / 100); //技能的ap是百分值 需要/100 而且需要再最终计算总ap时再/100,不然会出现ap为90%变0
|
||||
sDataCom.Attrs[Attrs.critical] = cAttrsComp.getRuntimeCritical() + sCrt;
|
||||
sDataCom.Attrs[Attrs.critical_damage] = cAttrsComp.getRuntimeCritDamageBonus();
|
||||
sDataCom.Attrs[Attrs.freeze_chance] = cAttrsComp.getRuntimeFreezeChance() + sFrz;
|
||||
sDataCom.Attrs[Attrs.stun_chance] = cAttrsComp.getRuntimeStunChance() + sStun;
|
||||
sDataCom.Attrs[Attrs.knockback_chance] = cAttrsComp.knockback_chance + sBck;
|
||||
sDataCom.Attrs[Attrs.knockback_distance] = cAttrsComp.knockback_distance || 0;
|
||||
sDataCom.Attrs[Attrs.puncture_chance] = cAttrsComp.getRuntimePunctureChance(); // 初始化携带施法者的穿透概率
|
||||
sDataCom.Attrs[Attrs.ap] = Math.floor(cAttrsComp.getFinalAp() * sAp / 100); //技能的ap是百分值 需要/100 而且需要再最终计算总ap时再/100,不然会出现ap为90%变0
|
||||
sDataCom.Attrs[Attrs.critical] = cAttrsComp.getFinalCritical() + sCrt;
|
||||
sDataCom.Attrs[Attrs.critical_damage] = cAttrsComp.getFinalCritDamage();
|
||||
sDataCom.Attrs[Attrs.freeze_chance] = cAttrsComp.getFinalFreezeChance() + sFrz;
|
||||
sDataCom.Attrs[Attrs.stun_chance] = cAttrsComp.getFinalStunChance() + sStun;
|
||||
sDataCom.Attrs[Attrs.knockback_chance] = cAttrsComp.getFinalKnockbackChance() + sBck;
|
||||
sDataCom.Attrs[Attrs.knockback_distance] = cAttrsComp.getFinalKnockbackDistance();
|
||||
sDataCom.Attrs[Attrs.puncture_chance] = cAttrsComp.getFinalPunctureChance(); // 初始化携带施法者的穿透概率
|
||||
sDataCom.s_uuid = s_uuid
|
||||
sDataCom.skill_lv = Math.max(0, skill_lv);
|
||||
sDataCom.fac = cAttrsComp.fac
|
||||
|
||||
159
docs/superpowers/specs/2026-07-23-timed-buff-system-design.md
Normal file
159
docs/superpowers/specs/2026-07-23-timed-buff-system-design.md
Normal 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 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<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 驱动
|
||||
- 永久 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 改空或删除 |
|
||||
Reference in New Issue
Block a user