import type { World, EntityId } from 'db://assets/mx/module/ecs'; import type { ISystem } from 'db://assets/mx/module/ecs'; import { BuffContainerComp, MovementComp, isBajiaoBlowRetreating, isYanSoulClaimed, type BuffInstance } from '../ecs/Components'; import type { IConfigProvider } from '../config/IConfigProvider'; import type { EffectExecutor } from './EffectExecutor'; import type { BattleServices } from './BattleServices'; import { createAbilityContext } from './AbilityContext'; import { BattleEventFactory } from '../events/BattleEvents'; /** Buff 系统:时间型 / 次数型 / 周期 tick */ export class BuffSystem implements ISystem { readonly priority = 20; enabled = true; constructor( private readonly _world: World, private readonly _config: IConfigProvider, private readonly _effects: EffectExecutor, private readonly _services: BattleServices, ) {} /** * @param target 目标实体 ID * @param buffId Buff 配置 ID * @param caster 施加者实体 ID * @param stacks 叠加层数,默认 1 * @param opts.duration 覆盖 BuffDef.duration(秒) */ add( target: EntityId, buffId: string, caster: EntityId, stacks = 1, opts?: { duration?: number }, ): void { if (!this._world.isAlive(target)) return; const targetMove = this._world.getComponent(target, MovementComp); // 出生跳出保护:攻击附带的 buff 也不落地 if (targetMove?.spawnProtected) return; if (isBajiaoBlowRetreating(targetMove)) return; // 阎王拘魂归属:伸爪/囚禁期间不接受任何 buff if (isYanSoulClaimed(targetMove)) return; const def = this._config.getBuff(buffId); if (!def) return; let container = this._world.getComponent(target, BuffContainerComp); if (!container) { container = new BuffContainerComp(); this._world.addComponent(target, container); } // 负面 buff 同 category 只保留 1 个:移除旧的(新替换旧) if (def.polarity === 'negative' && def.category) { const removed = container.buffs.filter(b => { if (b.buffId === buffId) return true; const existingDef = this._config.getBuff(b.buffId); return existingDef?.category === def.category && existingDef?.polarity === 'negative'; }); for (const b of removed) { this._cleanupBuff(target, b.buffId); if (b.buffId !== buffId) { this._services.events.emit(BattleEventFactory.buffRemoved(target, b.buffId)); } } container.buffs = container.buffs.filter(b => !removed.includes(b)); container.refreshDebuff4Flag(); } const duration = opts?.duration ?? def.duration; const isCharge = def.maxCharges != null; let inst = container.buffs.find(b => b.buffId === buffId); if (inst) { const maxStacks = def.maxStacks ?? 99; inst.stacks = Math.min(maxStacks, inst.stacks + stacks); if (isCharge) { inst.charges = Math.min(def.maxCharges!, (inst.charges ?? 0) + stacks); } else { inst.remain = duration; } } else { const newInst: BuffInstance = isCharge ? { buffId, stacks, remain: Infinity, caster, charges: Math.min(def.maxCharges!, stacks), tickAccum: 0 } : { buffId, stacks, remain: duration, caster, tickAccum: 0 }; container.buffs.push(newInst); } container.refreshDebuff4Flag(); this._runOnApply(caster, target, buffId); this._services.events.emit(BattleEventFactory.buffApplied( target, buffId, isCharge ? Infinity : duration, caster, )); } /** * @param target 目标实体 ID * @param tag Buff ID 或标签 * @returns 是否持有该 Buff */ hasTag(target: EntityId, tag: string): boolean { const container = this._world.getComponent(target, BuffContainerComp); return !!container?.buffs.some(b => b.buffId === tag); } /** * 消耗次数型 Buff 的生效次数;非次数型返回 false。 * @param target 目标实体 ID * @param buffId Buff 配置 ID * @param count 消耗次数,默认 1 * @returns 是否成功消耗(且 Buff 仍存在) */ consumeCharge(target: EntityId, buffId: string, count = 1): boolean { const container = this._world.getComponent(target, BuffContainerComp); if (!container) return false; const inst = container.buffs.find(b => b.buffId === buffId); if (!inst || inst.charges == null) return false; inst.charges -= count; if (inst.charges <= 0) { container.buffs = container.buffs.filter(b => b !== inst); container.refreshDebuff4Flag(); const def = this._config.getBuff(buffId); if (def?.removeGroup) { this._services.attributes.removeModifiersByGroup(target, def.removeGroup); } return false; } return true; } /** * 消耗实体上所有"每次攻击消耗"型 Buff 的 1 次生效次数,移除已耗尽的并清理关联 modifier。 * 在普攻/技能每次触发时调用,用于"临时(次数)"型 buff 的衰减。 * @param target 目标实体 ID */ consumeAttackBuffs(target: EntityId): void { const container = this._world.getComponent(target, BuffContainerComp); if (!container) return; const remain: BuffInstance[] = []; for (const b of container.buffs) { const def = this._config.getBuff(b.buffId); // 非consumeOnAttack的buff不消耗 if (!def?.consumeOnAttack || b.charges == null) { remain.push(b); continue; } b.charges -= 1; if (b.charges > 0) { remain.push(b); } else { // 次数耗尽:按 removeGroup 清理属性 modifier if (def.removeGroup) { this._services.attributes.removeModifiersByGroup(target, def.removeGroup); } } } container.buffs = remain; container.refreshDebuff4Flag(); } /** @param dt 帧间隔(秒),递减 Buff 剩余时间 / 累计 tick */ tick(dt: number): void { this._world.view(BuffContainerComp).forEach((id, container) => { if (!container.buffs.length) return; const buffs = container.buffs; let write = 0; for (let i = 0; i < buffs.length; i++) { const b = buffs[i]; const def = this._config.getBuff(b.buffId); if (!def) continue; const isCharge = b.charges != null; if (!isCharge) { b.remain -= dt; if (b.remain <= 0) { this._cleanupBuff(id, b.buffId); this._services.events.emit(BattleEventFactory.buffRemoved(id, b.buffId)); continue; } } if (def.tickInterval && def.tickEffects && def.tickEffects.length) { b.tickAccum = (b.tickAccum ?? 0) + dt; while (b.tickAccum >= def.tickInterval) { b.tickAccum -= def.tickInterval; this._runTickEffects(b, id); } } buffs[write++] = b; } buffs.length = write; container.refreshDebuff4Flag(); }); } private _cleanupBuff(target: EntityId, buffId: string): void { const def = this._config.getBuff(buffId); if (def?.removeGroup) { this._services.attributes.removeModifiersByGroup(target, def.removeGroup); } } private _runOnApply(caster: EntityId, target: EntityId, buffId: string): void { const def = this._config.getBuff(buffId)!; if (!def.onApplyEffects || !def.onApplyEffects.length) return; const ctx = createAbilityContext(this._services, caster, buffId, [target]); for (const spec of def.onApplyEffects) { this._effects.execute(ctx, spec); } } private _runTickEffects(inst: BuffInstance, target: EntityId): void { const def = this._config.getBuff(inst.buffId); if (!def?.tickEffects) return; const ctx = createAbilityContext(this._services, inst.caster, inst.buffId, [target]); for (const spec of def.tickEffects) { this._effects.execute(ctx, spec); } } }