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.
166 lines
6.7 KiB
166 lines
6.7 KiB
import { createAbilityContext } from '../combat/AbilityContext';
|
|
import type { BattleServices } from '../combat/BattleServices';
|
|
import type { EffectSpec } from '../config/BattleConfigTypes';
|
|
import { BattleEventFactory } from '../events/BattleEvents';
|
|
import { EffectRegistry, ConditionRegistry } from './EffectRegistry';
|
|
import { TagComp, HealthComp, BuffContainerComp } from '../ecs/Components';
|
|
import { traceCombatDamage } from 'db://assets/mx/tools/CombatDamageTrace';
|
|
const BUILTIN_FLAG = '__mx_battle_builtinEffectsRegistered__';
|
|
|
|
/** 注册内置 Effect / Condition(BattleSession 构造时调用一次) */
|
|
export function registerBuiltinBattleEffects(): void {
|
|
const g = globalThis as unknown as Record<string, boolean | undefined>;
|
|
if (g[BUILTIN_FLAG]) return;
|
|
g[BUILTIN_FLAG] = true;
|
|
|
|
ConditionRegistry.register('HasTargetInRange', (services, ctx) => {
|
|
const def = services.config.getAbility(ctx.abilityId);
|
|
if (!def?.targeting) return true;
|
|
const targets = services.targets.resolve(ctx, def.targeting);
|
|
return targets.length > 0;
|
|
});
|
|
|
|
ConditionRegistry.register('HasTag', (services, ctx, params) => {
|
|
const tag = params.tag as string;
|
|
for (const id of ctx.targets) {
|
|
const tags = services.world.getComponent(id, TagComp);
|
|
if (tags?.tags.includes(tag)) return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
EffectRegistry.register('DealDamage', (services, ctx, params) => {
|
|
traceCombatDamage('effectDealDamage', {
|
|
caster: ctx.caster,
|
|
abilityId: ctx.abilityId,
|
|
targetCount: ctx.targets.length,
|
|
targets: ctx.targets.slice(0, 5),
|
|
});
|
|
// 每目标独立:AttackBefore → calc(含暴击) → 扣血 → HitAfter / atk_after
|
|
const targets = [...ctx.targets];
|
|
for (const targetId of targets) {
|
|
services.damage.dealToTarget(ctx.caster, targetId, params, ctx);
|
|
}
|
|
});
|
|
|
|
EffectRegistry.register('Heal', (services, ctx, params) => {
|
|
const amount = (params.amount as number) ?? 0;
|
|
for (const targetId of ctx.targets) {
|
|
const health = services.world.getComponent(targetId, HealthComp);
|
|
if (health) {
|
|
health.hp = Math.min(health.maxHp, health.hp + amount);
|
|
}
|
|
}
|
|
});
|
|
|
|
EffectRegistry.register('ApplyBuff', (services, ctx, params) => {
|
|
const buffId = params.buffId as string;
|
|
const stacks = (params.stacks as number) ?? 1;
|
|
const chanceWan = params.chanceWan as number | undefined;
|
|
if (chanceWan != null && chanceWan < 10000) {
|
|
if (ctx.rng.next() >= chanceWan / 10000) return;
|
|
}
|
|
const duration = params.duration as number | undefined;
|
|
for (const targetId of ctx.targets) {
|
|
services.buffs.add(targetId, buffId, ctx.caster, stacks, duration != null ? { duration } : undefined);
|
|
}
|
|
});
|
|
|
|
EffectRegistry.register('RemoveBuff', (services, ctx, params) => {
|
|
const buffId = params.buffId as string;
|
|
for (const targetId of ctx.targets) {
|
|
const container = services.world.getComponent(targetId, BuffContainerComp);
|
|
if (!container) continue;
|
|
container.buffs = container.buffs.filter(b => b.buffId !== buffId);
|
|
container.refreshDebuff4Flag();
|
|
}
|
|
});
|
|
|
|
EffectRegistry.register('SpawnProjectile', (services, ctx, params) => {
|
|
const speed = (params.speed as number) ?? 400;
|
|
const hitEffects: EffectSpec[] = (params.hitEffects as EffectSpec[]) ?? [
|
|
{ type: 'DealDamage', params: { scale: 1 } },
|
|
];
|
|
const options = {
|
|
mode: params.mode as 'homing' | 'linear' | undefined,
|
|
direction: params.direction as { dr: number; dc: number } | undefined,
|
|
travel: params.travel as number | undefined,
|
|
pierce: params.pierce as number | null | undefined,
|
|
prefab: params.prefab as string | undefined,
|
|
};
|
|
for (const targetId of ctx.targets) {
|
|
services.projectiles.spawn(
|
|
ctx.caster,
|
|
targetId,
|
|
hitEffects,
|
|
speed,
|
|
(params.scale as number) ?? 1,
|
|
params.damageType as string,
|
|
options,
|
|
);
|
|
}
|
|
});
|
|
|
|
EffectRegistry.register('TriggerAbility', (services, ctx, params) => {
|
|
const abilityId = params.abilityId as string;
|
|
const child = createAbilityContext(services, ctx.caster, abilityId, [...ctx.targets]);
|
|
child.parent = ctx;
|
|
const def = services.config.getAbility(abilityId);
|
|
if (!def) return;
|
|
for (const event of def.timeline) {
|
|
services.clock.schedule(event.at / 1000, () => {
|
|
services.effects.executeAll(child, event.effects);
|
|
});
|
|
}
|
|
});
|
|
|
|
EffectRegistry.register('ModifyAttribute', (services, ctx, params) => {
|
|
const targets = ctx.targets.length ? ctx.targets : [ctx.caster];
|
|
const group = (params.group as string) ?? ctx.abilityId;
|
|
for (const id of targets) {
|
|
services.attributes.addModifier(id, {
|
|
attr: params.attr as string,
|
|
op: (params.op as 'Add' | 'Mul' | 'Override') ?? 'Add',
|
|
value: (params.value as number) ?? 0,
|
|
source: ctx.abilityId,
|
|
group,
|
|
remain: params.duration as number,
|
|
});
|
|
}
|
|
});
|
|
|
|
EffectRegistry.register('DispatchEvent', (services, _ctx, params) => {
|
|
services.abilities.fireEvent(params.event as string);
|
|
});
|
|
|
|
EffectRegistry.register('PlayPresentation', (services, ctx, params) => {
|
|
const cueId = (params.cue as string) ?? '';
|
|
if (!cueId) {
|
|
return;
|
|
}
|
|
const castInstanceId = ctx.vars.get('__castInstanceId') ?? 0;
|
|
const comboAttack = ctx.vars.get('__comboAttack');
|
|
services.events.emit(BattleEventFactory.presentationCue({
|
|
caster: ctx.caster,
|
|
abilityId: ctx.abilityId,
|
|
cueId,
|
|
params: {
|
|
...params,
|
|
...(comboAttack ? { comboAttack: 1 } : {}),
|
|
},
|
|
atMs: (params.at as number) ?? 0,
|
|
castInstanceId,
|
|
targetEntityIds: [...ctx.targets],
|
|
}));
|
|
});
|
|
}
|
|
|
|
/** 重置内置注册状态(单元测试用) */
|
|
export function resetBuiltinRegistrationForTests(): void {
|
|
(globalThis as unknown as Record<string, boolean | undefined>)[BUILTIN_FLAG] = false;
|
|
EffectRegistry.clear();
|
|
ConditionRegistry.clear();
|
|
}
|
|
|
|
// 显式标注 services 类型用于 handler 签名推断(无运行时影响)
|
|
export type { BattleServices };
|
|
|