Editor/WeaponImporter/Core/Setup/ActionTiming.cs
#nullable enable annotations

using WeaponImporter.Core.Analysis;
using WeaponImporter.Core.Weapon;

namespace WeaponImporter.Core.Setup;

/// <summary>
/// How long each action lasts at runtime (the WeaponHold action table): the mapped weapon clip's
/// length, else a sensible default. Everything that plans over an action's normalized time
/// (contact planning, checks, previews) uses this so it matches the game.
/// </summary>
public static class ActionTiming
{
    public static float Seconds(WeaponSetup setup, WeaponAsset? asset, AnimationRole role)
    {
        // The character drives actions it triggers itself; the weapon clip is stretched to fit.
        // Shell-reload parts keep their own clip lengths (the reload is as long as its shells).
        if (AnimationRoles.GraphTrigger(role) is not null && !AnimationRoles.IsShellReload(role) && setup.ActionSeconds.TryGetValue(role, out var measured) && measured > 0.05f)
            return measured;
        if (setup.WeaponAnimations.TryGetValue(role, out var binding) && asset?.FindClip(binding.Clip) is { Duration: > 0.01f } clip)
            return MathF.Max(clip.Duration, 0.05f);
        return DefaultSeconds(role);
    }

    public static float DefaultSeconds(AnimationRole role) => role switch
    {
        AnimationRole.Fire or AnimationRole.AdsFire or AnimationRole.FireEmpty => 0.25f,
        AnimationRole.Reload or AnimationRole.TacticalReload or AnimationRole.EmptyReload => 2.2f,
        AnimationRole.Draw => 0.8f,
        AnimationRole.Holster => 0.6f,
        AnimationRole.Melee => 0.8f,
        AnimationRole.Bolt => 0.9f,
        AnimationRole.ReloadStart or AnimationRole.ReloadEnd => 0.6f,
        AnimationRole.ReloadInsert => 0.5f,
        _ => 1f,
    };

    /// <summary>Roles whose support-hand contacts are planned from the character's animation.</summary>
    public static readonly AnimationRole[] ReloadRoles = { AnimationRole.Reload, AnimationRole.TacticalReload, AnimationRole.EmptyReload };
}