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.
 
 
 
 
AdLike2048/assets/mx/module/battle/registry/registerBuiltin.ts

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