You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

216 lines
8.7 KiB

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);
}
}
}