106 lines
3.7 KiB
TypeScript
106 lines
3.7 KiB
TypeScript
import {
|
|
getCharacterAnimationDurationMs,
|
|
getSequenceDurationMs,
|
|
getSkillCasterAnimation,
|
|
getSkillDelivery,
|
|
resolveSequenceFrames,
|
|
} from '../../data/characterCombat';
|
|
import type {
|
|
Character,
|
|
CharacterSkillDefinition,
|
|
CombatVisualEffect,
|
|
} from '../../types';
|
|
|
|
const RANGED_MONSTER_FOOT_LOCK_OFFSET_Y = -56;
|
|
|
|
function createCombatEffectId() {
|
|
return `combat-effect-${Math.random().toString(36).slice(2, 10)}`;
|
|
}
|
|
|
|
export function getSkillReleaseDelayMs(character: Character, skill: CharacterSkillDefinition) {
|
|
if (typeof skill.releaseDelayMs === 'number') return skill.releaseDelayMs;
|
|
const animationDuration = getCharacterAnimationDurationMs(character, getSkillCasterAnimation(skill));
|
|
return Math.min(260, Math.max(120, Math.round(animationDuration * 0.45)));
|
|
}
|
|
|
|
export function buildSkillEffects(
|
|
attacker: {
|
|
character: Character;
|
|
xMeters: number;
|
|
origin: 'player' | 'monster';
|
|
facing: 'left' | 'right';
|
|
monsterId?: string;
|
|
},
|
|
target: {
|
|
xMeters: number;
|
|
origin: 'player' | 'monster';
|
|
monsterId?: string;
|
|
},
|
|
skill: CharacterSkillDefinition,
|
|
) {
|
|
const phases = {
|
|
cast: [] as CombatVisualEffect[],
|
|
travel: [] as CombatVisualEffect[],
|
|
impact: [] as CombatVisualEffect[],
|
|
castDurationMs: 0,
|
|
travelDurationMs: 0,
|
|
impactDurationMs: 0,
|
|
};
|
|
|
|
const deliveryRanged = getSkillDelivery(skill) === 'ranged';
|
|
|
|
for (const effect of skill.effects ?? []) {
|
|
const frames = resolveSequenceFrames(attacker.character, effect.sequence);
|
|
if (frames.length === 0) continue;
|
|
|
|
const durationMs = effect.durationMs ?? getSequenceDurationMs(effect.sequence, frames.length);
|
|
const origin = effect.anchor === 'target' ? target : attacker;
|
|
const isProjectile =
|
|
effect.motion === 'projectile'
|
|
|| (effect.phase === 'travel' && deliveryRanged);
|
|
const fallbackProjectileStartOffsetX = attacker.facing === 'right' ? 18 : -18;
|
|
const startOffsetX = effect.startOffsetX ?? (isProjectile ? fallbackProjectileStartOffsetX : 0);
|
|
const endOffsetX = effect.endOffsetX ?? (isProjectile ? 0 : startOffsetX);
|
|
const startAnchorOffsetY = !isProjectile
|
|
&& deliveryRanged
|
|
&& origin.origin === 'monster'
|
|
? RANGED_MONSTER_FOOT_LOCK_OFFSET_Y
|
|
: 0;
|
|
const endAnchorOffsetY = isProjectile
|
|
&& deliveryRanged
|
|
&& target.origin === 'monster'
|
|
? RANGED_MONSTER_FOOT_LOCK_OFFSET_Y
|
|
: startAnchorOffsetY;
|
|
const instance: CombatVisualEffect = {
|
|
id: createCombatEffectId(),
|
|
frames,
|
|
fps: effect.sequence.fps ?? 10,
|
|
startX: isProjectile ? attacker.xMeters : origin.xMeters,
|
|
endX: isProjectile ? target.xMeters : origin.xMeters,
|
|
startOrigin: isProjectile ? attacker.origin : origin.origin,
|
|
endOrigin: isProjectile ? target.origin : origin.origin,
|
|
startMonsterId: isProjectile ? attacker.monsterId : origin.monsterId,
|
|
endMonsterId: isProjectile ? target.monsterId : origin.monsterId,
|
|
startAnchorOffsetY,
|
|
endAnchorOffsetY,
|
|
startOffsetX,
|
|
endOffsetX,
|
|
startYOffset: effect.startYOffset ?? 56,
|
|
endYOffset: effect.endYOffset ?? effect.startYOffset ?? 56,
|
|
durationMs,
|
|
sizePx: effect.sizePx ?? 96,
|
|
scale: effect.scale ?? 1,
|
|
facing: attacker.facing,
|
|
zIndex: effect.phase === 'impact' ? 28 : 24,
|
|
traveling: isProjectile,
|
|
};
|
|
|
|
phases[effect.phase].push(instance);
|
|
if (effect.phase === 'cast') phases.castDurationMs = Math.max(phases.castDurationMs, durationMs);
|
|
if (effect.phase === 'travel') phases.travelDurationMs = Math.max(phases.travelDurationMs, durationMs);
|
|
if (effect.phase === 'impact') phases.impactDurationMs = Math.max(phases.impactDurationMs, durationMs);
|
|
}
|
|
|
|
return phases;
|
|
}
|