OverdriveShotgun.cs
using Sandbox;
using System;
namespace PaintballBaddies;

public static class OverdriveShotgun
{
    public const float Interval=.78f;
    public const string SoundPath="sound/vidya/weapons/shotgun/shotgun_shoot.sound";
    public static float IntervalFor(Scene scene,GameObject actor=null)=>MultiplayerSession.Find(scene)?.IsOverdrive==true ? (actor?.Components.Get<PaintballCombatant>()?.HasCrossbow==true ? .62f : actor.IsValid() && OverdriveLoadout.DualFor(actor) ? DualInterval : Interval) : PaintballFlight.ShotInterval;
    // Dual pistols: quicker alternating shots, a tight three-pellet group each.
    public const float DualInterval=.3f;
    public static void LocalKick(Scene scene,GameObject shooter)
    {
        if(!scene.Camera.IsValid() || shooter.Components.Get<PlayerController>()?.IsProxy!=false)return;
        if(shooter.Components.Get<PaintballCombatant>()?.HasCrossbow==true){scene.Camera.AddPunch(new Angles(-.65f,0,.1f),1,.12f,0);return;}
        scene.Camera.AddShake(.8f,35,.13f);
        scene.Camera.AddPunch(new Angles(-2,0,.3f),1,.17f,0);
    }
    public static void Fire(Scene scene,PaintballCombatant shooter,Vector3 origin,Vector3 aim,int seed)
    {
        var session=MultiplayerSession.Find(scene);
        if(!MultiplayerSession.Authority || !shooter.IsValid() || !shooter.AcceptHits || session?.IsOverdrive!=true || !session.InRound
            || shooter.Components.Get<CloseCombat>()?.Incapacitated==true)return;
        var eye=shooter.Components.Get<PlayerController>()?.EyePosition ?? shooter.WorldPosition+Vector3.Up*52;
        // Keep real walls authoritative, but do not mistake an enemy overlapping
        // the barrel for cover. Retract the launch to the eye for point-blank aim.
        if(PaintballFlight.Trace(scene,eye,origin,shooter.GameObject,hitEnemies:false).Hit)return;
        if((aim-eye).Length<100 || OverdriveEnemy.TraceIntact(scene.Trace.Ray(eye,origin)
            .UsePhysicsWorld(false).UseHitboxes(true).WithTag("overdrive_enemy")).Hit)origin=eye;
        if(shooter.HasCrossbow){SausageBolt.Fire(shooter,origin,aim);return;}
        var rotation=Rotation.LookAt((aim-origin).Normal);
        var damage=new Dictionary<(OverdriveEnemy,int),int>();
        var hitPoints=new Dictionary<(OverdriveEnemy,int),Vector3>();
        var concrete=new Dictionary<ConcreteChip,int>();
        var crockery=new Dictionary<RestaurantBreakable,(float Damage,Vector3 Point)>();
        var stacks=new Dictionary<OverdriveEnemy,Vector3>();
        OverdriveEnemy primaryEnemy=null;
        bool helper=shooter.Components.Get<NetworkBot>().IsValid();
        var hitDrones=new HashSet<PopRiotTarget>();
        var crashingAtStart=session.Scene.GetAllComponents<PopRiotTarget>().Where(t=>t.Crashing).ToHashSet();
        bool hitSomething=false,metalHit=false;Vector3 impact=aim,bodyImpact=aim;
        bool dual=OverdriveLoadout.DualFor(shooter.GameObject);
        int pellets=dual ? 3 : 9;
        var points=new Vector3[pellets];var normals=new Vector3[pellets];int surfaceMask=0;
        for(int i=0;i<pellets;i++)
        {
            float radius=i==0 ? 0 : (dual ? .02f : .038f)*MathF.Sqrt(i/(float)(pellets-1));
            float angle=i*2.399963f+seed*.71f;
            var direction=(rotation.Forward+rotation.Right*MathF.Cos(angle)*radius+rotation.Up*MathF.Sin(angle)*radius).Normal;
            var ray=scene.Trace.Ray(origin,origin+direction*1700).WithSurfaceMeshes().IgnoreGameObjectHierarchy(shooter.GameObject)
                .WithoutTags("paintball_actor","paintball_debris","overdrive_enemy");
            foreach(var shield in scene.GetAllComponents<ArenaShield>())ray=ray.IgnoreGameObjectHierarchy(shield.GameObject);
            var trace=ray.Run();
            var flesh=OverdriveEnemy.TraceIntact(scene.Trace.Ray(origin,origin+direction*1700).UsePhysicsWorld(false).UseHitboxes(true).WithTag("overdrive_enemy"));
            if(flesh.Hit && (!trace.Hit || flesh.Distance<trace.Distance))trace=flesh;
            // A carried plate stack in front of whatever the pellet reached.
            var stackOwner=OverdriveEnemy.TraceStack(scene,origin,direction,trace.Hit ? (trace.HitPosition-origin).Length : 1700,out var stackPoint);
            if(stackOwner.IsValid())
            {
                points[i]=stackPoint;normals[i]=-direction;
                if(!primaryEnemy.IsValid() || primaryEnemy==stackOwner)
                {primaryEnemy=stackOwner;stacks.TryAdd(stackOwner,stackPoint);hitSomething=true;impact=stackPoint;}
                continue;
            }
            if(SausageProjectile.TryIntercept(scene,origin,direction,trace.Hit ? (trace.HitPosition-origin).Length : 1700,shooter,out var intercepted))
            {points[i]=intercepted;normals[i]=-direction;hitSomething=true;impact=intercepted;continue;}
            points[i]=trace.Hit ? trace.HitPosition : origin+direction*1700;
            normals[i]=trace.Normal;
            if(!trace.Hit)continue;
            impact=trace.HitPosition;
            var enemy=OverdriveEnemy.Find(trace.GameObject);
            if(enemy.IsValid())
            {
                // A normal trigger pull has one enemy target, even if the
                // spread clips several neighbours. Only earned grenades add area damage.
                if(primaryEnemy.IsValid() && primaryEnemy!=enemy)continue;
                primaryEnemy=enemy;bodyImpact=impact;
                int part=enemy.Zone(impact,trace.Hitbox?.Bone?.Name);
                if(part!=0 && (enemy.MissingParts&part)!=0)continue;
                float falloff=((impact-origin).Length/1300).Clamp(0,1);
                int amount=(int)MathF.Round((dual ? 17-9*falloff : 14-10*falloff)*(shooter.HotShotSeconds>0 ? 1.4f : 1));
                if(helper)amount=Math.Max(1,(int)(amount*.38f));
                var key=(enemy,part);damage[key]=damage.GetValueOrDefault(key)+amount;hitPoints[key]=impact;hitSomething=true;
            }
            else
            {
                var drone=PopRiotTarget.Find(trace.GameObject);
                if(drone.IsValid() && (!drone.Crashing || crashingAtStart.Contains(drone)) && (!helper || hitDrones.Add(drone))){drone.Hit(impact,shooter);hitSomething=true;metalHit=true;}
                if(!drone.IsValid())surfaceMask|=1<<i;
                var dish=RestaurantBreakable.Find(trace.GameObject);
                if(dish.IsValid()){var prior=crockery.GetValueOrDefault(dish);crockery[dish]=(prior.Damage+14,impact);surfaceMask &= ~(1<<i);}
                var chip=trace.GameObject?.Components.Get<ConcreteChip>();
                if(chip.IsValid())concrete[chip]=concrete.GetValueOrDefault(chip)+14;
            }
        }
        // Snapshot the living so the shooter's hit confirmation can tell a kill
        // from a hit.
        var livingBefore=session.OverdriveEnemies.Where(e=>e.IsValid() && !e.Dead).ToArray();
        foreach(var hit in damage)
        {
            // A head hit represents the whole trigger pull. Other pellets from
            // that blast must not kill the first-hit survivor through body damage.
            if(hit.Key.Item2!=1 && damage.ContainsKey((hit.Key.Item1,1)))continue;
            if(hit.Key.Item2==0)hit.Key.Item1.BlastTorso(hit.Value);
            hit.Key.Item1.Damage(hit.Value,hit.Key.Item2,(hitPoints[hit.Key]-origin).Normal,shooter,hitPoints[hit.Key]);
        }
        // Resolve all pellets against the intact geometry before changing any
        // collision, so a blast cannot pass through its own newly opened hole.
        foreach(var hit in crockery)hit.Key.Hit(hit.Value.Damage,hit.Value.Point,shooter);
        foreach(var stack in stacks){int kind=stack.Key.ShootStack(shooter,stack.Value);if(kind>0)HitConfirm.Callout(shooter.GameObject,OverdriveEnemy.StackCallout(kind),session.RoundId);}
        foreach(var hit in concrete)hit.Key.Hit(hit.Value,(hit.Key.WorldPosition-origin).Normal,shooter);
        // Intercepted crockery already has its own shatter cue. Only actual
        // body hits get the soft impact layer; head bursts own their payoff.
        bool bodyHit=damage.Count>0 && !damage.Keys.Any(k=>k.Item1.Dead && (k.Item1.MissingParts&1)!=0);
        int kills=livingBefore.Count(e=>e.IsValid() && e.Dead);
        Effects(shooter.GameObject,origin,impact,hitSomething,metalHit,bodyHit,bodyImpact,false,session.RoundId,points,normals,surfaceMask,damage.Count>0 || kills>0,kills);
    }
    [Rpc.Broadcast(NetFlags.HostOnly)]
    private static async void Effects(GameObject shooter,Vector3 origin,Vector3 impact,bool hit,bool metalHit,bool bodyHit,Vector3 bodyImpact,bool explosive,int round,Vector3[] points,Vector3[] normals,int surfaceMask,bool enemyHit,int kills)
    {
        var scene=Game.ActiveScene;await GameTask.MainThread();
        if(!scene.IsValid() || scene!=Game.ActiveScene || MultiplayerSession.Find(scene) is not {} s || !s.IsOverdrive || s.RoundId!=round)return;
        // Dual pistols alternate: every other shot comes from the left gun.
        var loadout=shooter.IsValid() ? shooter.Components.Get<OverdriveLoadout>() : null;
        bool dual=loadout?.DualActive==true;
        if(dual)origin=loadout.NextDualMuzzle();
        var shot=dual && OverdriveAssets.PistolSound is not null ? Sound.Play(OverdriveAssets.PistolSound,origin) : Sound.Play(SoundPath,origin);
        if(shot is not null)shot.Pitch=Game.Random.Float(.95f,1.05f);
        if(!dual)FeelAudio.ShotgunLayers(scene,shooter,origin);
        ShotFlash.Spawn(scene,origin+(points[0]-origin).Normal*6,new Color(1,.72f,.38f)*3.2f,190,.07f);
        if(enemyHit)HitConfirm.Local(scene,shooter,kills>0,impact,kills);
        var body=shooter.IsValid() ? shooter.Components.Get<CitizenPlayerPresentation>()?.FootstepRenderer ?? shooter.Components.Get<CitizenOpponentPresentation>()?.FootstepRenderer : null;
        if(body.IsValid())body.Set("b_attack",true);
        ShotgunShellEjection.Spawn(scene,shooter,origin,(points[0]-origin).Normal);
        for(int i=0;i<points.Length;i++)
        {
            if(i%2==0)OverdriveShotEffects.Tracer(scene,origin,points[i]);
            if((surfaceMask&(1<<i))!=0)OverdriveShotEffects.Impact(scene,points[i],normals[i],i);
        }
        if(hit)
        {
            if(metalHit || explosive)DroneDamageFlare.Spawn(scene,impact,Color.Orange,explosive,round,explosive ? .5f : .25f);
            if(metalHit)DroneAudio.Play("damage",impact);
            else if(bodyHit)OverdriveAudio.Play("hit",bodyImpact);
        }
    }
}

public sealed class OverdriveLoadout : Component
{
    // Spaghelli world model coordinates, fitted to Citizen's native hold_R.
    public static Vector3 ShotgunOffset => new(3.5f,0,-2);
    public static Vector3 SupportOffset => ShotgunOffset+new Vector3(7,1,3.3f);
    public static Vector3 MuzzleOffset => ShotgunOffset+new Vector3(20.1969f,0,6.226f);
    public Vector3 FittedOffset {get;private set;}=ShotgunOffset;
    // Tilt the shotgun about the trigger hand so the stock rides above and
    // outside the right forearm instead of passing through it.
    public static readonly Rotation ShotgunTilt=Rotation.From(new Angles(0,7,0));
    // Hand sits a little ahead of the trigger so the stock ends before the forearm.
    public static readonly Vector3 GripPoint=new(-13.1205f,-.6448f,2.252f);
    // Shouldered shotgun stance (stock at the shoulder, support hand on the
    // fore-end) in Sausage Survival; the crossbow and paintball keep the rifle hold.
    public static Sandbox.Citizen.CitizenAnimationHelper.HoldTypes HoldFor(GameObject actor)
        =>DualFor(actor) ? Sandbox.Citizen.CitizenAnimationHelper.HoldTypes.Pistol : Sandbox.Citizen.CitizenAnimationHelper.HoldTypes.Rifle;
    // Sausage Survival's main weapon is a pair of pistols (the crossbow stays).
    public static bool DualFor(GameObject actor)=>actor.IsValid() && MultiplayerSession.Find(actor.Scene)?.IsOverdrive==true && !Bow(actor) && OverdriveAssets.PistolModel is {IsError:false};
    // Pistol grip (where the hand bone sits) and muzzle, from the model's bounds.
    public static Vector3 PistolGrip{get{var b=OverdriveAssets.PistolModel.Bounds;return new(b.Mins.x+b.Size.x*.14f,b.Center.y,b.Mins.z+b.Size.z*.42f);}}
    // Middle of the 1911's grip handle (behind the trigger, lower half).
    public static Vector3 DualGrip{get{var b=OverdriveAssets.PistolModel.Bounds;return new(b.Mins.x+b.Size.x*DualPistols.GripBack,b.Center.y,b.Mins.z+b.Size.z*DualPistols.GripUp);}}
    public static Vector3 PistolMuzzle{get{var b=OverdriveAssets.PistolModel.Bounds;return new(b.Maxs.x,b.Center.y,b.Maxs.z-b.Size.z*.18f);}}
    private static bool Bow(GameObject actor)=>actor.Components.Get<PaintballCombatant>()?.HasCrossbow==true;
    public static Vector3 MuzzleFor(GameObject actor)=>MultiplayerSession.Find(actor.Scene)?.IsOverdrive==true ? (actor.Components.Get<OverdriveLoadout>()?.FittedOffset ?? ShotgunOffset)+(Bow(actor) ? new Vector3(9.3f,0,1.8f) : DualFor(actor) ? PistolMuzzle : ShotgunTilt*new Vector3(20.1969f,0,6.226f)) : PaintballMarker.MuzzleOffset;
    public static readonly Rotation CrossbowRotation=Rotation.FromYaw(90);
    public const float CrossbowScale=.75f;
    public static readonly Vector3 CrossbowGrip=new(0,12.4f,-2.4f);
    public static readonly Vector3 CrossbowSupport=new(0,3f,.8f);
    public static Vector3 SupportFor(GameObject actor)
        =>(actor.Components.Get<OverdriveLoadout>()?.FittedOffset ?? ShotgunOffset)
          +(Bow(actor) ? CrossbowRotation*(CrossbowSupport*CrossbowScale) : ShotgunTilt*new Vector3(5,1,3.3f));
    public float SwapSeconds {get;private set;}
    private int observedStage=-1;
    
    private int shownMode=-1;
    private GameObject visual,leftVisual;
    private bool nextLeft;
    private float kickRight,kickLeft;
    public bool DualActive=>shownMode==2 && visual.IsValid() && leftVisual.IsValid() && visual.Enabled;
    public Vector3 LeftMuzzle=>leftVisual.IsValid() ? leftVisual.WorldTransform.PointToWorld(PistolMuzzle) : WorldPosition;
    public Vector3 RightMuzzle=>visual.IsValid() ? visual.WorldTransform.PointToWorld(PistolMuzzle) : WorldPosition;
    /// <summary>Alternates the firing gun and kicks it; returns its muzzle.</summary>
    public Vector3 NextDualMuzzle()
    {
        nextLeft=!nextLeft;
        if(nextLeft && leftVisual.IsValid() && leftVisual.Enabled){kickLeft=1;return LeftMuzzle;}
        kickRight=1;return RightMuzzle;
    }
    public GameObject Visual=>visual;
    public GameObject LeftVisual=>leftVisual;
    public bool ShowingShotgun=>visual.IsValid() && visual.Enabled && shownMode==0;
    private ModelRenderer original;
    protected override void OnUpdate()
    {
        bool active=MultiplayerSession.Find(Scene)?.IsOverdrive==true;
        var actor=Components.Get<PaintballCombatant>();bool bow=actor?.HasCrossbow==true;
        int stage=actor?.EquippedWeapon ?? 0;
        SwapSeconds=MathF.Max(0,SwapSeconds-Time.Delta);
        if(active && stage!=observedStage)
        {
            if(observedStage>=0){SwapSeconds=1.5f;KitchenCombatAudio.Play("swap",WorldPosition+Vector3.Up*50);}
            observedStage=stage;
        }
        if(active){_=OverdriveAssets.Load();_=OverdriveAssets.LoadPistols();}
        var anchor=Components.Get<PaintballMarker>()?.PresentationAnchor ?? Components.Get<ArenaOpponent>()?.PresentationAnchor;
        if(active && anchor.IsValid() && !visual.IsValid() && OverdriveAssets.Ready)
        {
            original=anchor.Components.Get<ModelRenderer>();
            visual=new GameObject(anchor,true,"Spaghelli shotgun"){NetworkMode=NetworkMode.Never,LocalPosition=ShotgunOffset};
            visual.Components.Create<ModelRenderer>().Model=Model.Load(OverdriveAssets.ShotgunPath);
        }
        bool dual=active && DualFor(GameObject);
        int mode=bow ? 1 : dual ? 2 : 0;
        var visualRenderer=visual.IsValid() ? visual.Components.Get<ModelRenderer>() : null;
        if(visual.IsValid() && (mode!=shownMode || !visualRenderer.IsValid()))
        {
            // A presentation rebuild/hotload can retain the anchor while its
            // renderer is gone. Restore it even if the selected weapon is unchanged.
            if(!visualRenderer.IsValid())visualRenderer=visual.Components.Create<ModelRenderer>();
            shownMode=mode;visual.Name=bow ? "Rust crossbow" : dual ? "Right 1911" : "Spaghelli shotgun";
            visualRenderer.Model=bow ? Model.Load(OverdriveAssets.CrossbowPath) : dual ? OverdriveAssets.PistolModel : Model.Load(OverdriveAssets.ShotgunPath);
            visual.LocalRotation=bow ? CrossbowRotation : dual ? Rotation.Identity : ShotgunTilt;
            visual.LocalScale=new Vector3(bow ? CrossbowScale : 1);
            if(dual && !leftVisual.IsValid())
            {
                // Parented to the character so close-camera hiding covers it too.
                leftVisual=new GameObject(GameObject,true,"Left 1911"){NetworkMode=NetworkMode.Never};
                leftVisual.Components.Create<ModelRenderer>().Model=OverdriveAssets.PistolModel;
            }
            if(leftVisual.IsValid())leftVisual.Enabled=dual;
        }
        var melee=Components.Get<CloseCombat>();
        bool visible=active && actor?.Eliminated!=true && melee?.Incapacitated!=true;
        bool shield=Scene.GetAllComponents<ArenaShield>().Any(x=>x.Owner==actor && !x.Stowed);
        if(leftVisual.IsValid())leftVisual.Enabled=visible && dual && !shield;
        if(original.IsValid())original.Enabled=!active;
        if(visual.IsValid())visual.Enabled=visible;
        kickRight=MathF.Max(0,kickRight-Time.Delta*7);kickLeft=MathF.Max(0,kickLeft-Time.Delta*7);
    }
    protected override void OnPreRender()
    {
        // Fit both guns to the same evaluated animation frame. Updating only
        // the right gun through its bone parent made the left one lag behind.
        bool active=MultiplayerSession.Find(Scene)?.IsOverdrive==true;
        bool bow=Bow(GameObject),dual=active && DualFor(GameObject);
        var anchor=Components.Get<PaintballMarker>()?.PresentationAnchor ?? Components.Get<ArenaOpponent>()?.PresentationAnchor;
        var body=Components.Get<CitizenPlayerPresentation>()?.FootstepRenderer ?? Components.Get<CitizenOpponentPresentation>()?.FootstepRenderer;
        if(active && visual.IsValid() && anchor.IsValid() && body.IsValid() && body.TryGetBoneTransform("hand_R",out var hand))
        {
            // Keep the same trigger contact when the native hold changes for a shield.
            FittedOffset=anchor.WorldTransform.PointToLocal(hand.Position)-(bow ? CrossbowRotation*(CrossbowGrip*CrossbowScale) : dual ? PistolGrip : ShotgunTilt*GripPoint);
            visual.LocalPosition=FittedOffset;
            if(dual)
            {
                // Each pistol is placed in world space: level, pointing along the
                // arms, with the middle of its grip handle inside the opening of
                // the closed fist. Short muzzle flip on its shots.
                var aim=body.TryGetBoneTransform("hold_R",out var holdR) ? holdR.Rotation : anchor.WorldRotation;
                if(DualPistols.Palm(body,"R",out var palmR))
                {
                    var rot=aim*Rotation.FromPitch(-8*kickRight*kickRight);
                    visual.WorldRotation=rot;visual.WorldPosition=palmR-rot*DualGrip;
                    FittedOffset=anchor.WorldTransform.PointToLocal(visual.WorldPosition);
                }
                else{FittedOffset=-CitizenMarkerGrip.MarkerOffset;visual.LocalPosition=FittedOffset;visual.LocalRotation=Rotation.Identity;}
                if(!leftVisual.IsValid())
                {
                    leftVisual=new GameObject(GameObject,true,"Left 1911"){NetworkMode=NetworkMode.Never};
                    leftVisual.Components.Create<ModelRenderer>().Model=OverdriveAssets.PistolModel;
                }
                if(leftVisual.Parent!=GameObject)leftVisual.Parent=GameObject;
                if(DualPistols.Palm(body,"L",out var palmL))
                {
                    var leftAim=body.TryGetBoneTransform("hold_L",out var holdL) ? holdL.Rotation : aim;
                    var rot=leftAim*Rotation.FromPitch(-8*kickLeft*kickLeft);
                    leftVisual.WorldRotation=rot;leftVisual.WorldPosition=palmL-rot*DualGrip;
                }
            }
            else if(!bow)visual.LocalRotation=ShotgunTilt;
        }
    }
    protected override void OnDestroy(){if(visual.IsValid())visual.Destroy();if(leftVisual.IsValid())leftVisual.Destroy();if(original.IsValid())original.Enabled=true;}
}

/// <summary>Shouldered shotgun stance: the stock sits in the right shoulder
/// pocket, pointing where the character aims. The right hand is placed on the
/// grip by IK (the gun rides on the hand), the left hand already reaches the
/// fore-end, and the arms bend naturally instead of cradling the gun low.</summary>
public static class ShotgunShoulder
{
    // Stock position relative to the right shoulder joint (engine units).
    // Forward keeps the right hand out in front so the arm stays visible.
    public static float ShoulderForward=4.5f,ShoulderInward=1.5f,ShoulderDrop=1.5f;
    public static bool Enabled=false;
    public static bool Apply(GameObject actor,SkinnedModelRenderer body,Sandbox.Citizen.CitizenAnimationHelper animation,GameObject target,Rotation aim)
    {
        var loadout=actor.Components.Get<OverdriveLoadout>();
        // Disabled: Citizen's arm IK folds the realistic arms badly in this pose.
        if(!Enabled || loadout is null || !loadout.ShowingShotgun || !target.IsValid())return false;
        if(animation.IkRightHand.IsValid() && animation.IkRightHand!=target)return false;
        var model=loadout.Visual.Components.Get<ModelRenderer>()?.Model;
        if(model is null || !body.TryGetBoneTransform("hand_R",out var hand) || !body.TryGetBoneTransform("arm_upper_R",out var shoulder))
        {if(animation.IkRightHand==target)animation.IkRightHand=null;return false;}
        var gun=loadout.Visual.WorldTransform;var b=model.Bounds;
        // The gun is rigid on the hand, so its pose relative to the hand is constant.
        var gunInHand=hand.Rotation.Inverse*gun.Rotation;
        var butt=gun.PointToWorld(new Vector3(b.Mins.x+1.5f,b.Center.y,b.Center.z));
        var buttInHand=hand.Rotation.Inverse*(butt-hand.Position);
        var handRotation=aim*gunInHand.Inverse;
        // Tucked toward the chest so the shorter support arm can reach the fore-end
        // with a soft elbow instead of locking straight.
        var pocket=shoulder.Position+aim.Forward*ShoulderForward+aim.Left*ShoulderInward-Vector3.Up*ShoulderDrop;
        target.WorldRotation=handRotation;
        target.WorldPosition=pocket-handRotation*buttInHand;
        animation.IkRightHand=target;
        return true;
    }
}

/// <summary>Akimbo pose: the left hand and pistol sit beside the right ones,
/// one shoulder-width to the left, both guns pointing straight ahead. The
/// sideways axis comes from the hips (the torso twists in the pistol pose).</summary>
public static class DualPistols
{
    public readonly record struct Plane(Vector3 Center,Vector3 Normal,float Width);
    public static Plane Frame(SkinnedModelRenderer body)
    {
        var n=body.WorldRotation.Left;var c=body.WorldPosition;float width=14;
        if(body.TryGetBoneTransform("leg_upper_L",out var hl) && body.TryGetBoneTransform("leg_upper_R",out var hr))
        {var d=(hl.Position-hr.Position).WithZ(0);if(d.Length>1){n=d.Normal;c=(hl.Position+hr.Position)/2;}}
        if(body.TryGetBoneTransform("arm_upper_L",out var sl) && body.TryGetBoneTransform("arm_upper_R",out var sr))
            width=((sl.Position-sr.Position).Length*.9f).Clamp(9,20);
        return new(c,n,width);
    }
    public static float GripBack=.25f,GripUp=.37f,PalmAlong=.75f,PalmPull=.35f;
    static bool palmLogged;
    /// <summary>Centre of the closed fist: most of the way from the wrist to the
    /// middle knuckle, where a gun grip passes through the curled fingers.</summary>
    public static bool Palm(SkinnedModelRenderer body,string side,out Vector3 palm)
    {
        palm=default;
        if(!body.TryGetBoneTransform("hand_"+side,out var wrist))return false;
        // Centre of the hole the curled fingers make: the knuckle row, the
        // middle and tip joints of the curled fingers, pulled toward the palm.
        var pts=new List<Vector3>();
        foreach(var f in new[]{"index","middle","ring"})
            for(int j=0;j<3;j++)
            {
                var name=$"finger_{f}_{j}_{side}";
                if(body.TryGetBoneTransform(name,out var bone))pts.Add(bone.Position);
            }
        if(pts.Count<3)
        {
            if(!palmLogged){palmLogged=true;Log.Warning("DUAL_PALM finger bones missing");}
            return false;
        }
        var curl=pts.Aggregate(Vector3.Zero,(x,y)=>x+y)/pts.Count;
        var knuckles=body.TryGetBoneTransform("finger_middle_0_"+side,out var k0) ? k0.Position : curl;
        palm=Vector3.Lerp(curl,Vector3.Lerp(wrist.Position,knuckles,PalmAlong),PalmPull);
        if(!palmLogged){palmLogged=true;Log.Info($"DUAL_PALM bones={pts.Count} wristToOpening={(palm-wrist.Position).Length:0.0}");}
        return true;
    }
    // Two-handed akimbo: the pair of hands is centred on the line from the
    // breastbone along the aim, hands a little narrower than the shoulders,
    // elbows soft (never locked straight), both guns parallel on the aim.
    public static float Reach=.86f,Droop=4,Spacing=.8f,MaxReach=.95f;
    static readonly Dictionary<SkinnedModelRenderer,Rotation> aims=new();
    /// <summary>Where this character is aiming (set by the presentation each frame).</summary>
    public static Rotation AimFor(SkinnedModelRenderer body)=>body.IsValid() && aims.TryGetValue(body,out var r) ? r : Aim(body);
    public static void BothHands(SkinnedModelRenderer body,Sandbox.Citizen.CitizenAnimationHelper animation,GameObject right,GameObject left,float aimYaw,float aimPitch,float lift=0)
    {
        if(!right.IsValid() || !left.IsValid())return;
        if(!body.TryGetBoneTransform("hand_R",out var handR) || !body.TryGetBoneTransform("hand_L",out var handL)
            || !body.TryGetBoneTransform("hold_R",out var holdR) || !body.TryGetBoneTransform("hold_L",out var holdL)
            || !body.TryGetBoneTransform("arm_upper_R",out var shR) || !body.TryGetBoneTransform("arm_upper_L",out var shL)
            || !body.TryGetBoneTransform("arm_lower_R",out var elR) || !body.TryGetBoneTransform("arm_lower_L",out var elL))return;
        if(aims.Count>64)aims.Clear();
        var aim=Rotation.FromYaw(aimYaw)*Rotation.FromPitch(aimPitch.Clamp(-35,35));
        aims[body]=aim;
        var dir=aim*Rotation.FromPitch(Droop);
        float armR=(elR.Position-shR.Position).Length+(handR.Position-elR.Position).Length;
        float armL=(elL.Position-shL.Position).Length+(handL.Position-elL.Position).Length;
        var chest=(shL.Position+shR.Position)/2;
        float half=(shL.Position-shR.Position).Length*.5f*Spacing;
        var center=chest+dir.Forward*MathF.Min(armR,armL)*Reach+Vector3.Up*lift;
        var flatLeft=Rotation.FromYaw(aimYaw).Left;
        var rightPos=Reachable(shR.Position,center-flatLeft*half,armR);
        var leftPos=Reachable(shL.Position,center+flatLeft*half,armL);
        // Wrists: turn each hand so its grip frame (hold bone, which the pistol
        // model is authored against) matches the gun: palm against the side of
        // the grip, fingers wrapped around the front, thumb up, knuckles on aim.
        var gun=aim;
        var relR=handR.Rotation.Inverse*holdR.Rotation;
        var relL=handL.Rotation.Inverse*holdL.Rotation;
        right.WorldPosition=rightPos;right.WorldRotation=gun*relR.Inverse;
        left.WorldPosition=leftPos;left.WorldRotation=gun*relL.Inverse;
        animation.IkRightHand=right;animation.IkLeftHand=left;
    }
    // Keeps a wrist target inside the arm's reach so the elbow never hyperextends.
    static Vector3 Reachable(Vector3 shoulder,Vector3 target,float arm)
    {
        var d=target-shoulder;float max=arm*MaxReach;
        return d.Length>max ? shoulder+d.Normal*max : target;
    }
    /// <summary>Level aim along the arms (shoulders to hands, averaged).</summary>
    public static Rotation Aim(SkinnedModelRenderer body)
    {
        var dir=Vector3.Zero;
        foreach(var side in new[]{"R","L"})
            if(body.TryGetBoneTransform("arm_upper_"+side,out var sh) && body.TryGetBoneTransform("hand_"+side,out var h))dir+=(h.Position-sh.Position).WithZ(0).Normal;
        if(dir.Length<.1f)dir=body.WorldRotation.Forward;
        return Rotation.LookAt(dir.WithZ(0).Normal,Vector3.Up);
    }
    public static Vector3 MirrorDirection(Plane p,Vector3 dir)=>dir-2*Vector3.Dot(dir,p.Normal)*p.Normal;
    public static Rotation MirrorRotation(Plane p,Rotation r)=>Rotation.LookAt(MirrorDirection(p,r.Forward),MirrorDirection(p,r.Up));
    // Correction for the left hand bone's own axes, if the wrist reads twisted.
    public static Angles LeftHandFix=new(0,0,180);
    public static Transform LeftHandTarget(Plane p,Transform rightHand)
        =>new Transform(rightHand.Position+p.Normal*p.Width,MirrorRotation(p,rightHand.Rotation)*Rotation.From(LeftHandFix));
    public static void LeftHand(SkinnedModelRenderer body,Sandbox.Citizen.CitizenAnimationHelper animation,GameObject target)
    {
        if(!target.IsValid() || !body.TryGetBoneTransform("hand_R",out var hand))return;
        var p=Frame(body);
        if(body.TryGetBoneTransform("hold_R",out var holdR) && body.TryGetBoneTransform("hand_L",out var handL) && body.TryGetBoneTransform("hold_L",out var holdL))
        {
            // Want hold_L parallel to hold_R (both guns pointing the same way),
            // one shoulder-width to the left. Solve back to the wrist target.
            var relRot=handL.Rotation.Inverse*holdL.Rotation;
            var relPos=handL.Rotation.Inverse*(holdL.Position-handL.Position);
            var wantRot=holdR.Rotation;
            var wantPos=holdR.Position+p.Normal*p.Width;
            var handRot=wantRot*relRot.Inverse;
            target.WorldRotation=handRot;
            target.WorldPosition=wantPos-handRot*relPos;
        }
        else
        {
            var t=LeftHandTarget(p,hand);
            target.WorldPosition=t.Position;target.WorldRotation=t.Rotation;
        }
        animation.IkLeftHand=target;
    }
}