Target/WeaponAnimGraph.cs
#nullable enable
namespace HumanoidHandRetargeter.Target;

public enum WeaponAction { Idle, Fire, Reload, ReloadEmpty, Deploy, Holster }
public sealed record WeaponGraphClip(WeaponAction Action,string Sequence);

/// <summary>Deterministic weapon action graph. One evaluated stream drives weapon and custom hand bones.</summary>
public static class WeaponAnimGraph
{
    public static WeaponAction? Classify(string name)
    {
        var value=name.ToLowerInvariant();
        if(value.EndsWith("_delta")||value.Contains("additive"))return null;
        if(value.StartsWith("reload"))return value.Contains("empty")?WeaponAction.ReloadEmpty:WeaponAction.Reload;
        if(value.StartsWith("deploy"))return WeaponAction.Deploy;
        if(value.StartsWith("holster"))return WeaponAction.Holster;
        if(value.StartsWith("idle"))return WeaponAction.Idle;
        if(value is "fire" or "shoot" or "attack"||value.StartsWith("fire_")||value.StartsWith("shoot_")||value.StartsWith("attack_"))return WeaponAction.Fire;
        return null;
    }

    public static string Create(string modelPath,IReadOnlyList<WeaponGraphClip> clips)
    {
        var actions=clips.ToDictionary(c=>c.Action);
        if(!actions.ContainsKey(WeaponAction.Idle))throw new ArgumentException("A weapon graph requires an idle sequence.");
        foreach(var clip in clips)ArgumentException.ThrowIfNullOrWhiteSpace(clip.Sequence);
        var doc=Kv3.Parse(HandAnimGraph.Create(actions[WeaponAction.Idle].Sequence,modelPath));
        var root=(KvObject)doc.Root;
        var nodes=(KvArray)((KvObject)root["m_nodeManager"])["m_nodes"];
        var rootNode=(KvObject)((KvObject)nodes.Items[0])["value"];
        rootNode["m_inputConnection"]=Connection(100);
        var keptRoot=nodes.Items[0];nodes.Items.Clear();nodes.Items.Add(keptRoot);
        var parameters=new[]{"b_attack","b_reload","b_empty","b_deploy","b_holster","b_deploy_skip"};
        var ids=parameters.Select((p,i)=>(p,id:500+i)).ToDictionary(p=>p.p,p=>p.id);
        ((KvObject)root["m_pParameterList"])["m_Parameters"]=Array(parameters.Select(p=>Object(
            ("_class",Text("CBoolAnimParameter")),("m_name",Text(p)),("m_id",Id(ids[p])),
            ("m_previewButton",Text("ANIMPARAM_BUTTON_NONE")),("m_bUseMostRecentValue",Bool(false)),
            ("m_bAutoReset",Bool(p is "b_attack" or "b_reload" or "b_deploy")),("m_bDefaultValue",Bool(false)))));
        KvObject Condition(string parameter,bool value=true)=>Object(("_class",Text("CParameterAnimCondition")),
            ("m_comparisonOp",Number(0)),("m_paramID",Id(ids[parameter])),
            ("m_comparisonValue",Object(("m_nType",Number(1)),("m_data",Bool(value)))));
        KvObject Finished()=>Object(("_class",Text("CFinishedCondition")),("m_comparisonOp",Number(0)),
            ("m_option",Text("FinishedConditionOption_OnAlmostFinished")),("m_bIsFinished",Bool(true)));
        KvObject Transition(WeaponAction destination,params KvValue[] conditions)=>Object(
            ("_class",Text("CAnimStateTransition")),("m_conditions",Array(conditions)),
            ("m_blendDuration",new KvDouble(.06)),("m_destState",Id(200+(int)destination)),
            ("m_bReset",Bool(true)),("m_resetCycleOption",Text("Beginning")),("m_flFixedCycleValue",new KvDouble(0)),
            ("m_bBlendCycle",Bool(false)),("m_blendCurve",Object(("m_vControlPoint1",Point(.5,0)),("m_vControlPoint2",Point(.5,1)))),
            ("m_bForceFootPlant",Bool(false)),("m_bDisabled",Bool(false)),("m_bRandomTimeBetween",Bool(false)),
            ("m_flRandomTimeStart",new KvDouble(0)),("m_flRandomTimeEnd",new KvDouble(0)));
        var states=new KvArray();
        var initial=actions.ContainsKey(WeaponAction.Deploy)?WeaponAction.Deploy:WeaponAction.Idle;
        foreach(var pair in actions.OrderBy(p=>p.Key))
        {
            var action=pair.Key;var sequenceId=300+(int)action;var transitions=new KvArray();
            if(action==WeaponAction.Holster)
                transitions.Items.Add(Transition(actions.ContainsKey(WeaponAction.Deploy)?WeaponAction.Deploy:WeaponAction.Idle,Condition("b_holster",false)));
            else
            {
                if(actions.ContainsKey(WeaponAction.Holster))transitions.Items.Add(Transition(WeaponAction.Holster,Condition("b_holster")));
                if(action is WeaponAction.Idle or WeaponAction.Fire)
                {
                    if(actions.ContainsKey(WeaponAction.ReloadEmpty))transitions.Items.Add(Transition(WeaponAction.ReloadEmpty,Condition("b_reload"),Condition("b_empty")));
                    if(actions.ContainsKey(WeaponAction.Reload))transitions.Items.Add(actions.ContainsKey(WeaponAction.ReloadEmpty)
                        ?Transition(WeaponAction.Reload,Condition("b_reload"),Condition("b_empty",false)):Transition(WeaponAction.Reload,Condition("b_reload")));
                    if(actions.ContainsKey(WeaponAction.Fire))transitions.Items.Add(Transition(WeaponAction.Fire,Condition("b_attack")));
                    if(actions.ContainsKey(WeaponAction.Deploy))transitions.Items.Add(Transition(WeaponAction.Deploy,Condition("b_deploy")));
                }
                if(action==WeaponAction.Deploy)transitions.Items.Add(Transition(WeaponAction.Idle,Condition("b_deploy_skip")));
                if(action!=WeaponAction.Idle)transitions.Items.Add(Transition(WeaponAction.Idle,Finished()));
            }
            states.Items.Add(Object(("_class",Text("CAnimState")),("m_transitions",transitions),("m_tags",new KvArray()),("m_tagBehaviors",new KvArray()),
                ("m_name",Text(action.ToString())),("m_inputConnection",Connection(sequenceId)),("m_stateID",Id(200+(int)action)),
                ("m_position",Point((int)action*180,0)),("m_bIsStartState",Bool(action==initial)),("m_bIsEndtState",Bool(false)),
                ("m_bIsPassthrough",Bool(false)),("m_bIsRootMotionExclusive",Bool(false)),("m_bAlwaysEvaluate",Bool(false))));
            nodes.Items.Add(Node(sequenceId,Object(("_class",Text("CSequenceAnimNode")),("m_sName",Text(action.ToString())),("m_vecPosition",Point(-450,(int)action*150)),
                ("m_nNodeID",Id(sequenceId)),("m_sNote",Text("Generated weapon and hand animation")),("m_tagSpans",new KvArray()),
                ("m_sequenceName",Text(pair.Value.Sequence)),("m_playbackSpeed",new KvDouble(1)),("m_bLoop",Bool(action==WeaponAction.Idle)))));
        }
        nodes.Items.Add(Node(100,Object(("_class",Text("CStateMachineAnimNode")),("m_sName",Text("Weapon actions")),("m_vecPosition",Point(-100,0)),
            ("m_nNodeID",Id(100)),("m_sNote",Text("Single clock for weapon and custom hands")),("m_states",states),
            ("m_bBlockWaningTags",Bool(false)),("m_bLockStateWhenWaning",Bool(false)))));
        return Kv3.Serialize(doc);
    }
    static KvObject Object(params (string Key,KvValue Value)[] fields){var result=new KvObject();foreach(var (key,value) in fields)result[key]=value;return result;}
    static KvArray Array(IEnumerable<KvValue> items){var result=new KvArray();result.Items.AddRange(items);return result;}
    static KvString Text(string value)=>new(value);
    static KvLong Number(long value)=>new(value);
    static KvBool Bool(bool value)=>new(value);
    static KvObject Id(int value)=>Object(("m_id",Number(value)));
    static KvArray Point(double x,double y)=>Array(new KvValue[]{new KvDouble(x),new KvDouble(y)});
    static KvObject Node(int id,KvObject value)=>Object(("key",Id(id)),("value",value));
    static KvObject Connection(int node)=>Object(("m_nodeID",Id(node)),("m_outputID",Object(("m_id",Number(4294967295)))));
}