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/script/battle/skill/SkillPresentationBridge.ts

155 lines
5.9 KiB

3 weeks ago
import type { BattleSession } from 'db://assets/mx/module/battle';
import { createAbilityContext } from 'db://assets/mx/module/battle/combat/AbilityContext';
import type { EntityId } from 'db://assets/mx/module/ecs';
import { isActiveAbilityId, isNormalAbilityId } from '../config/weapon1/Weapon1BattleIds';
import type {
SkillAttackFlowSnapshot,
SkillDealDamageParams,
SkillDeliveryMode,
SkillPresentationContext,
SkillPresentationCueInput,
} from './SkillPresentationTypes';
/** abilityId → Skill.id */
export function parseBaseAtkIdFromAbilityId(abilityId: string): number {
const m = abilityId.match(/_(?:norm|ult|hero|weapon|monster)_(\d+)(?:_e\d+)?$/);
return m ? Number(m[1]) : 0;
}
/** deferred 切片推断投递方式 */
export function inferDeliveryMode(
pendingStages: SkillAttackFlowSnapshot['pendingStages'],
): SkillDeliveryMode {
if (pendingStages.some(s => s.effectTypes.includes('SpawnProjectile'))) return 'projectile';
if (pendingStages.length === 0) return 'instant';
return 'melee';
}
/** PresentationCue → SkillPresentationContext(含 hit/dealDamage/finish/cancel) */
export function buildSkillPresentationContext(
session: BattleSession,
cue: SkillPresentationCueInput,
): SkillPresentationContext {
const { presentation, world, damage } = session;
const pending = presentation.getPending(cue.castInstanceId);
const baseAtkId = parseBaseAtkIdFromAbilityId(cue.abilityId);
const pendingStages = (pending?.slices ?? []).slice(pending?.nextIndex ?? 0).map((s, i) => ({
stageIndex: s.index ?? pending!.nextIndex + i,
effectTypes: s.effects.map(e => e.type),
}));
const flow: SkillAttackFlowSnapshot = {
abilityId: cue.abilityId,
caster: cue.caster,
level: pending?.ctx.level ?? 1,
castInstanceId: cue.castInstanceId,
cueId: cue.cueId,
baseAtkId,
pendingStages,
vars: pending?.ctx.vars ?? new Map(),
};
const deliveryMode = inferDeliveryMode(pendingStages);
let targets = [...(pending?.ctx.targets ?? cue.targetEntityIds)];
let cancelled = false;
const ctx: SkillPresentationContext = {
flow,
caster: cue.caster,
abilityId: cue.abilityId,
cueId: cue.cueId,
castInstanceId: cue.castInstanceId,
level: flow.level,
baseAtkId,
deliveryMode,
params: { ...(cue.params ?? {}) },
get targets() { return targets; },
set targets(v: EntityId[]) { targets = [...v]; },
get isAlive() {
return !cancelled && world.isAlive(cue.caster);
},
hit() {
if (!ctx.isAlive) return false;
presentation.updateTargets(cue.castInstanceId, targets);
const ok = presentation.hit(cue.castInstanceId);
return ok;
},
dealDamage(target: EntityId, params?: SkillDealDamageParams) {
if (!ctx.isAlive) return false;
if (!world.isAlive(target)) return false;
const pendingCtx = presentation.getPending(cue.castInstanceId)?.ctx;
const abilityCtx = pendingCtx
? {
...pendingCtx,
targets: [target],
vars: new Map(pendingCtx.vars),
}
: createAbilityContext(session, cue.caster, cue.abilityId, [target]);
abilityCtx.level = flow.level;
abilityCtx.targets = [target];
// 与 DealDamage 相同:每怪独立 before → calc → after
// 能力专属 hook 来自本次 cue.params;词条 hook 在 AtkHookComp
// 出生保护等导致 0 伤时返回 false,避免碰撞 once/pierce 误记命中
const amount = damage.dealToTarget(cue.caster, target, {
scale: params?.scale ?? 1,
damageType: params?.damageType,
skillAtkBuff1: (ctx.params.buff1 as string) ?? '',
skillAtkBuff2: (ctx.params.buff2 as string) ?? '',
...(params?.percentMaxHpWan != null && params.percentMaxHpWan > 0
? { percentMaxHpWan: params.percentMaxHpWan }
: {}),
...(params?.additivePercentMaxHpWan != null && params.additivePercentMaxHpWan > 0
? { additivePercentMaxHpWan: params.additivePercentMaxHpWan }
: {}),
...(params?.allowCrit === false
? { allowCrit: false }
: params?.allowCrit === true || ctx.params.allowCrit === true
? { allowCrit: true }
: {}),
}, abilityCtx);
return amount > 0;
},
finish() {
if (cancelled) return;
if (!world.isAlive(cue.caster)) {
ctx.cancel('caster_dead');
return;
}
presentation.finish(cue.castInstanceId);
},
releasePresentationLock() {
if (cancelled) return;
presentation.releaseLock(cue.castInstanceId);
},
cancel(reason?: string) {
if (cancelled) return;
cancelled = true;
presentation.cancel(cue.castInstanceId);
if (reason) {
console.debug('[SkillPresentation] cancel', cue.castInstanceId, reason);
}
},
setTargets(next: EntityId[]) {
targets = [...next];
presentation.updateTargets(cue.castInstanceId, targets);
},
applyHitEffects() {
return ctx.hit();
},
completePresentation() {
ctx.finish();
},
};
return ctx;
}
/** 校验 abilityId 是否为已知 base_atk 映射 */
export function isKnownTowerAbilityId(abilityId: string): boolean {
const id = parseBaseAtkIdFromAbilityId(abilityId);
if (id <= 0) return false;
return isNormalAbilityId(abilityId) || isActiveAbilityId(abilityId);
}