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.
155 lines
5.9 KiB
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);
|
||
|
|
}
|