export const ATTACK_PRESENTATION_DURATION_SECONDS = 0.52; const WINDUP_END_SECONDS = 0.12; const STRIKE_END_SECONDS = 0.25; function smoothstep(progress: number): number { const clamped = Math.max(0, Math.min(1, progress)); return clamped * clamped * (3 - 2 * clamped); } /** * A one-shot presentation curve driven by an actual combat hit revision. * Negative values are the windup, positive values are the strike, and zero is * the locomotion pose. Keeping this pure lets the render rig layer the motion * over Stand/Run without putting animation timing into combat simulation. */ export function attackSwingAt(elapsedSeconds: number): number { if (!Number.isFinite(elapsedSeconds) || elapsedSeconds <= 0) return 0; if (elapsedSeconds >= ATTACK_PRESENTATION_DURATION_SECONDS) return 0; if (elapsedSeconds < WINDUP_END_SECONDS) { return -0.58 * smoothstep(elapsedSeconds / WINDUP_END_SECONDS); } if (elapsedSeconds < STRIKE_END_SECONDS) { const progress = smoothstep( (elapsedSeconds - WINDUP_END_SECONDS) / (STRIKE_END_SECONDS - WINDUP_END_SECONDS), ); return -0.58 + (1.05 + 0.58) * progress; } const progress = smoothstep( (elapsedSeconds - STRIKE_END_SECONDS) / (ATTACK_PRESENTATION_DURATION_SECONDS - STRIKE_END_SECONDS), ); return 1.05 * (1 - progress); }