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