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.
217 lines
8.7 KiB
217 lines
8.7 KiB
|
3 weeks ago
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|