swb_base/ViewModelHandler.cs

A viewmodel handler component for a weapon, responsible for positioning, rotating and animating the first-person weapon model and hands. It computes sway, visual recoil, aim/run/customize/jump animations, applies MW-style recoil effects, manages viewmodel FOV, and exposes console commands to tune a global viewmodel correction (position and rotation).

External DownloadFile AccessNetworking
using SWB.Shared;
using System;

namespace SWB.Base;

public class ViewModelHandler : Component
{
	public SkinnedModelRenderer ViewModelRenderer { get; set; }
	public SkinnedModelRenderer ViewModelHandsRenderer { get; set; }
	public Weapon Weapon { get; set; }
	public CameraComponent Camera { get; set; }
	public bool ShouldDraw { get; set; }

	// Editor
	public bool EditorMode { get; set; }
	public AngPos EditorOffset { get; set; }

	/// <summary>
	/// A whole-viewmodel angular correction, in the handler's own rot units.
	///
	/// ⚠️ STATIC AND LIVE-TUNABLE ON PURPOSE. This is a hunt for one number; a
	/// property on the weapon would need a prefab edit and a restart per attempt.
	/// Once it is right it gets baked and this goes away.
	/// </summary>
	// ⛔ 90 IS THE DEFAULT, NOT ZERO. Every ARC9 BO1 viewmodel is authored a quarter
	// turn from the axis SWB expects, so with no correction the weapon sits off
	// screen entirely. This was being typed in by hand every session because the
	// field is static and resets on restart — a correction the whole pack needs is
	// a default, not a console command.
	//
	// ⚠️ If weapons face the wrong way, this is -90; it is the only value in play.
	public static float VMYaw { get; set; } = 90f;
	public static float VMPitch { get; set; }
	public static float VMRoll { get; set; }

	/// <summary>Positional half of the correction. x = right, y = forward,
	/// z = up — the same axes the handler applies every other offset in.</summary>
	public static float VMX { get; set; }
	public static float VMY { get; set; }
	public static float VMZ { get; set; }

	/// <summary>Set the rotation: nz_vm_yaw &lt;yaw&gt; [pitch] [roll].</summary>
	[ConCmd( "nz_vm_yaw" )]
	public static void SetVMYaw( float yaw = 0f, float pitch = 0f, float roll = 0f )
	{
		VMYaw = yaw; VMPitch = pitch; VMRoll = roll;
		Report();
	}

	/// <summary>Set the position: nz_vm_pos &lt;x&gt; &lt;y&gt; &lt;z&gt;
	/// (right / forward / up).</summary>
	[ConCmd( "nz_vm_pos" )]
	public static void SetVMPos( float x = 0f, float y = 0f, float z = 0f )
	{
		VMX = x; VMY = y; VMZ = z;
		Report();
	}

	/// <summary>
	/// NUDGE all six: nz_vm_add &lt;yaw&gt; [pitch] [roll] [x] [y] [z].
	///
	/// ⚠️ The one you actually want while hunting a value — converging by typing
	/// absolutes means recomputing the total in your head every attempt.
	/// </summary>
	[ConCmd( "nz_vm_add" )]
	public static void AddVM( float yaw = 0f, float pitch = 0f, float roll = 0f,
		float x = 0f, float y = 0f, float z = 0f )
	{
		VMYaw += yaw; VMPitch += pitch; VMRoll += roll;
		VMX += x; VMY += y; VMZ += z;
		Report();
	}

	/// <summary>Print the current correction, ready to be baked.</summary>
	[ConCmd( "nz_vm" )]
	public static void Report()
	{
		Log.Info( $"[swb] viewmodel correction:  yaw {VMYaw:0.##}  pitch {VMPitch:0.##}"
			+ $"  roll {VMRoll:0.##}   x {VMX:0.##}  y {VMY:0.##}  z {VMZ:0.##}" );
		Log.Info( $"[swb]   nz_vm_yaw {VMYaw:0.##} {VMPitch:0.##} {VMRoll:0.##}"
			+ $"   ·   nz_vm_pos {VMX:0.##} {VMY:0.##} {VMZ:0.##}" );
	}

	/// <summary>Back to zero, for starting the hunt again.</summary>
	[ConCmd( "nz_vm_reset" )]
	public static void ResetVM()
	{
		VMYaw = VMPitch = VMRoll = VMX = VMY = VMZ = 0f;
		Report();
	}
	public float EditorFOV { get; set; }

	/// <summary>
	/// The player this viewmodel belongs to — NULL-SAFE, because it is routinely orphaned.
	///
	/// ⛔ THIS THREW EVERY FRAME AND TOOK THE WHOLE BODY DOWN WITH IT. A player body is CLONED
	/// (`NZPlayers.SpawnFor`), the clone carries a copy of the template's viewmodel object and this
	/// handler, and then `Disarm` deletes the weapon — leaving a handler whose `Weapon` is null.
	/// `Weapon.Owner` then threw in `OnUpdate` on every tick:
	///
	///     Exception when calling 'Update' on SWB.Base.ViewModelHandler
	///     Exception when calling 'OnDestroy' on SWB.Base.ViewModelHandler
	///       at SWB.Base.ViewModelHandler.get_player()
	///
	/// ⚠️ AND A THROWING `OnUpdate` DOES NOT FAIL QUIETLY — the rest of that object's update is
	/// abandoned, `PlayerController` among it, so the body is never drawn. Measured, both ends of
	/// one session: the body WITHOUT an orphaned viewmodel was visible, the body WITH one was
	/// invisible while its stranded hands were not.
	///
	/// ⚠️ `?.` RATHER THAN REMOVING THE ORPHAN. Three attempts to delete these objects each made
	/// bodies disappear; this cannot, because it removes nothing. It only stops an exception, and
	/// an orphan with a null owner now simply does nothing — `IsFirstPerson` on a null player is
	/// false, which is exactly the "not mine, do not draw" answer the handler already knows how to
	/// act on.
	/// </summary>
	IPlayerBase player => Weapon?.Owner;

	float animSpeed = 1;
	float weaponFOVSpeed = 1;

	// Target animation values
	Vector3 targetVectorPos;
	Vector3 targetVectorRot;
	float targetWeaponFOV = -1;

	// Finalized animation values
	Vector3 finalVectorPos;
	Rotation finalRot;
	float finalWeaponFOV;

	// Sway
	Rotation lastEyeRot;

	/// <summary>
	/// How far the gun is swinging away from where the player is looking, this frame.
	///
	/// ⛔ EXPOSED SO NOTHING HAS TO RE-DERIVE IT. This is the whole of "how much a weapon swings
	/// when you turn": the eye rotation lags behind the real one at `swayspeed`, and that lag,
	/// scaled and clamped just below, is the swing. Any second copy of that calculation elsewhere is
	/// free to disagree with the gun actually on screen -- so readers take these instead.
	///
	/// ⚠️ PITCH, YAW, ROLL in degrees, matching `MathUtil.ToRotation`'s (x, y, z) convention.
	/// </summary>
	public Vector3 SwayRotOffset { get; private set; }

	/// <summary>
	/// The gun's positional swing, in the handler's own (right, forward, up) order — the order
	/// `WorldPosition +=` composes it in, NOT Source's (forward, left, up).
	/// </summary>
	public Vector3 SwayPosOffset { get; private set; }

	/// <summary>
	/// How far the lagged eye rotation is currently behind the real one, in degrees.
	///
	/// ⛔ THE INPUT TO THE WHOLE SWAY SYSTEM, and it tracks turn RATE, not turn DISTANCE. It grows
	/// while `swayspeed` cannot keep up and settles at (degrees per second / swayspeed) — so a
	/// steady turn holds a steady lag no matter how far round you go, and stopping collapses it.
	/// </summary>
	public Angles SwayLag { get; private set; }

	/// <summary>The rate the lagged eye rotation chases the real one — 5 at the hip, 20 aiming.</summary>
	public float SwaySpeed { get; private set; }

	/// <summary>The rate the pose is slerped toward its target, `10 * Weapon.AnimSpeed`.</summary>
	public float PoseSpeed => animSpeed;

	/// <summary>The furthest the gun will ever tilt from sway, in degrees.</summary>
	///
	/// ⚠️ NAMED SO THE FULL RANGE CAN BE ASKED FOR. Anything expressing itself as a FRACTION of the
	/// swing needs to know what the whole of it is, and a second copy of "4" written elsewhere would
	/// quietly stop matching the moment this is tuned.
	public const float SwayRotLimit = 4.0f;

	/// <summary>The furthest the gun will ever shift from sway, in units.</summary>
	public const float SwayPosLimit = 1.5f;

	/// <summary>
	/// Where the viewmodel would be standing this frame if it were not swaying.
	///
	/// ⛔ THE RAW SWAY NUMBERS ARE NOT WHAT YOU SEE, so anything that needs the gun's real on-screen
	/// motion has to be given this instead of rebuilding it. `SwayRotOffset` is a TARGET: it is
	/// clamped, then folded into a pose that is slerped toward at `animSpeed`, so what reaches the
	/// screen is smaller than the target, lags it, and answers a fast turn on a completely different
	/// curve than a slow one. A reader scaling the raw numbers cannot match that with any constant --
	/// measured against the gun it needed 1200% sideways and still had the wrong shape.
	///
	/// ⚠️ VISUAL RECOIL IS DELIBERATELY EXCLUDED, so a kick shows up as motion against this rather
	/// than cancelling out of it.
	/// </summary>
	public Transform SwayFreeTransform { get; private set; }

	// ⚠️ A SECOND COPY OF THE DAMPED POSE, run with the sway subtracted from its target and stepped
	// at the same `animSpeed` on the same frames. Anything cheaper -- a smoothed average of the real
	// pose, a fixed reference -- drifts during a sustained turn, because a gun held at constant sway
	// is indistinguishable from one at rest to a filter that only watches the output.
	Rotation finalRotNoSway;
	Vector3 finalVectorPosNoSway;

	// FOV
	float cachedFOV = -1f;
	float cachedVerticalFOV;

	// Jumping Animation
	float jumpTime;
	float landTime;

	// Aim animation
	float aimTime;

	// Helpful values
	Vector3 localVel;

	protected override void OnDestroy()
	{
		// ⚠️ AN ORPHAN HAS NOBODY TO GIVE THE FIELD OF VIEW BACK TO. This is the exact line the
		// `OnDestroy` exception in the logs came from.
		if ( player is null || !player.IsFirstPerson ) return;
		player.FieldOfView = Screen.CreateVerticalFieldOfView( Preferences.FieldOfView );
	}

	protected override void OnDisabled()
	{
		// Reinitialize all target values when enabled
		targetWeaponFOV = -1;
	}

	Vector3 _visualRecoil;      // pitch, yaw, roll — the TARGET the shot asked for, decaying
	Vector3 _tiltShown;         // what is actually drawn, chasing the target through a spring
	Vector3 _tiltVel;           // degrees per second, the spring's velocity
	float _visualPunch;         // units of backward drive still to settle
	float _visualRecoilRecovery = 0.15f;

	/// <summary>
	/// Kick the MODEL. Called on each shot; the pose settles back on its own.
	/// </summary>
	/// <param name="replace">
	/// Overwrite the outstanding kick instead of adding to it.
	///
	/// ⛔ ADDITIVE WAS THE ORIGINAL DESIGN AND IT IS WHAT MADE THE LEAN POSSIBLE AT ALL. Stacking
	/// makes a burst climb in the hands, which reads well — but it also means the direction of
	/// shot N is carried into shot N+1, so any bias in the sequence compounds instead of
	/// cancelling. Two separate fixes tried to remove the bias from the SEQUENCE; replacing
	/// removes the accumulation the bias needed.
	///
	/// ⚠️ IT COSTS THE CLIMB, DELIBERATELY. Requested as *"each shot going in a random direction,
	/// and recenter before the next shot"* — a crisp per-shot snap rather than a rising sway.
	/// `GlobalHandling.RecoilStability` switches back.
	/// </param>
	public void ApplyVisualRecoil( float up, float side, float roll, float punch, float recovery,
		bool replace = false )
	{
		var kick = new Vector3( up, side, roll );

		if ( replace )
		{
			_visualRecoil = kick;
			_visualPunch = punch;
		}
		else
		{
			_visualRecoil += kick;
			_visualPunch += punch;
		}

		_visualRecoilRecovery = recovery > 0 ? recovery : 0.15f;
	}

	/// <summary>
	/// The MW Base's gun springs (`NZombies.MwRecoilFx`), kicked from `Weapon.Shoot` while the MW look owns the gun.
	/// One per held gun, so a weapon swap starts at rest.
	/// </summary>
	public NZombies.MwGunRecoil MwGun { get; } = new();

	/// <summary>
	/// The MW Base's gun motion on this frame's pose: its springs (`MwGun`) and the part of the view punch the base
	/// shows on the gun rather than on the camera.
	///
	/// ⚠️ ABOUT THE EYE, NOT ABOUT A PIVOT ON THE GUN. The base's viewmodel origin IS the eye and every turn it makes
	/// (`RotateAroundAxis`) swings the gun about it, so a pitch reads as the gun rising on screen, not as the muzzle
	/// tipping in place. Our own tilt pivots on the gun on purpose (*"it rotates around the player"*); this is the
	/// other look, and only while it is asked for.
	///
	/// ⚠️ THE SCREEN RATTLE IS NOT HERE. It goes on this gun's CAMERA (`MwRecoilFx.GunCamera`, shaped by
	/// `MwRecoilView`), so the gun and the world rattle as one picture, as they do in the base.
	/// </summary>
	void ApplyMwLook()
	{
		if ( !NZombies.MwRecoilFx.On ) return;

		NZombies.MwRecoilFx.GunCamera = Camera;

		var dt = MathF.Min( Time.Delta, 0.034f );

		var aimed = Weapon.IsAiming && !Weapon.IsReloading ? 1f : 0f;
		var aim = MathX.Lerp( NZombies.MwRecoilFx.Aim, aimed, MathF.Min( 1f, 18f * dt ) );
		NZombies.MwRecoilFx.Aim = aim;

		MwGun.Step( dt, aim );
		MwGun.Output( aim, out var turn, out var move );

		var eye = Camera.WorldPosition;
		var cam = Camera.WorldRotation;

		WorldPosition += cam.Right * move.x + cam.Forward * move.y + cam.Up * move.z;

		// the base's (pitch up, yaw left, roll) is our (pitch down, yaw left, roll)
		var local = Rotation.From( -turn.x, turn.y, turn.z ) * NZombies.MwRecoilFx.PunchOnGun( aim );

		var q = cam * local * cam.Inverse;
		WorldRotation = q * WorldRotation;
		WorldPosition = eye + q * (WorldPosition - eye);
	}

	protected override void OnUpdate()
	{
		// ⛔ AN ORPHANED HANDLER STOPS HERE, AND STOPS QUIETLY. Everything below assumes a player;
		// with none, the honest answer is "this viewmodel belongs to nobody, so draw nothing" —
		// and, critically, RETURNING is what keeps the exception from killing the rest of this
		// object's update, which is what was hiding the body.
		if ( player is null )
		{
			if ( ViewModelRenderer.IsValid() ) ViewModelRenderer.Enabled = false;
			if ( ViewModelHandsRenderer.IsValid() ) ViewModelHandsRenderer.Enabled = false;
			return;
		}

		// ⛔ AND WITHOUT ITS CAMERA IT CANNOT POSITION ANYTHING EITHER. `nz_vm_clear` destroys
		// viewmodel cameras deliberately, and the handler then threw on `Camera.WorldPosition`
		// every frame — hundreds of times a second, measured:
		//
		//     [115] Exception when calling 'Update' on SWB.Base.ViewModelHandler
		//     [224] Exception when calling 'Update' on SWB.Base.ViewModelHandler
		//
		// ⚠️ A DIAGNOSTIC COMMAND MUST NOT SET THE LOG ON FIRE. That flood buries the very lines
		// the command was run to produce, which is the opposite of what a diagnostic is for.
		if ( !Camera.IsValid() )
		{
			if ( ViewModelRenderer.IsValid() ) ViewModelRenderer.Enabled = false;
			if ( ViewModelHandsRenderer.IsValid() ) ViewModelHandsRenderer.Enabled = false;
			return;
		}

		var renderType = ShouldDraw ? ModelRenderer.ShadowRenderType.Off : ModelRenderer.ShadowRenderType.ShadowsOnly;
		ViewModelRenderer.Enabled = player.IsFirstPerson;
		ViewModelRenderer.RenderType = renderType;

		if ( ViewModelHandsRenderer is not null )
		{
			ViewModelHandsRenderer.Enabled = player.IsFirstPerson;
			ViewModelHandsRenderer.RenderType = renderType;
		}

		if ( !player.IsFirstPerson ) return;

		// For particles & lighting
		Camera.WorldPosition = Scene.Camera.WorldPosition;
		Camera.WorldRotation = Scene.Camera.WorldRotation;

		if ( targetWeaponFOV == -1 )
		{
			targetWeaponFOV = Weapon.ViewModelFOV;
			finalWeaponFOV = Weapon.ViewModelFOV;
		}

		// Skinned modelrenderer has issues with following parent
		WorldPosition = Camera.WorldPosition;
		WorldRotation = Camera.WorldRotation;

		// Pos + FOV
		finalVectorPos = finalVectorPos.LerpTo( targetVectorPos, animSpeed * RealTime.SmoothDelta );
		// ⚠️ SAME LINE, SAME animSpeed, SAME FRAME — the two only stay comparable while they are
		// stepped identically. `animSpeed` is rewritten further down, so this cannot be deferred.
		finalVectorPosNoSway = finalVectorPosNoSway.LerpTo( targetVectorPos - SwayPosOffset, animSpeed * RealTime.SmoothDelta );
		finalWeaponFOV = MathX.LerpTo( finalWeaponFOV, targetWeaponFOV, weaponFOVSpeed * animSpeed * RealTime.SmoothDelta );

		// Angles
		//
		// ⛔ THE CORRECTION IS COMPOSED AS A ROTATION, NOT SUMMED INTO THE EULER.
		//
		// I first added it to `targetVectorRot`, which is a Vector3 of
		// (pitch, yaw, roll) awaiting conversion. **Euler composition is not
		// commutative**: adding 90 to yaw before the conversion changes how the
		// pitch and roll components are then interpreted, so every OTHER offset —
		// aim, run, tuck — silently changed meaning. Reported exactly: *"when i
		// run the weapon is supposed to turn to the left, but now its rotating
		// counter clockwise"*. The run offset was fine; my correction had
		// re-mapped its axes.
		//
		// Multiplied on the end instead, it is a fixed reorientation of the model
		// in its own frame and leaves every authored offset meaning what it did.
		var targetRot = MathUtil.ToRotation( targetVectorRot )
			* Rotation.From( VMPitch, VMYaw, VMRoll );
		finalRot = finalRot.SlerpTo( targetRot, animSpeed * RealTime.SmoothDelta );

		// ⚠️ Euler subtraction, matching the Euler ADDITION the sway was applied with — undoing it
		// the way it was done is what keeps the two poses differing by exactly the sway.
		var targetRotNoSway = MathUtil.ToRotation( targetVectorRot - SwayRotOffset )
			* Rotation.From( VMPitch, VMYaw, VMRoll );
		finalRotNoSway = finalRotNoSway.SlerpTo( targetRotNoSway, animSpeed * RealTime.SmoothDelta );

		// Reset Speed
		animSpeed = 10 * Weapon.AnimSpeed;

		// ── visual recoil ───────────────────────────────────────────────────
		//
		// ⚠️ Decayed BEFORE composing, and folded into finalRot/finalVectorPos
		// rather than applied afterwards — the pose is built in the weapon's own
		// space, so adding a world-space nudge after the fact would drift as you
		// turn. This rides along with the pose instead.
		// ⛔ NEVER WRITE VISUAL RECOIL INTO finalRot / finalVectorPos. Those are
		// PERSISTENT FIELDS carried across frames and slerped toward the target
		// pose — multiplying a kick into them compounds it into the weapon's base
		// pose permanently, so the gun creeps further every shot and never comes
		// back. That is a drift that no amount of decay can undo, because the
		// decaying value was already baked into the state it decays from.
		//
		// ⚠️ Apply as LOCALS at composition time instead. The kick then genuinely
		// is transient: it exists only for the frame it is drawn in.
		var vrRot = finalRot;
		var vrPos = finalVectorPos;

		// ⛔ THE KICK'S ROTATION IS NO LONGER FOLDED INTO `vrRot`, AND THAT IS THE FIX FOR
		// *"it rotates around the player."* `vrRot` is the frame the authored position offset is
		// STEPPED ALONG, a few lines below — so a kick multiplied in here did not turn the gun,
		// it turned the ruler the gun is measured with, and swung it on an arc centred on the eye.
		// A rotation about the point you are looking FROM is very nearly pure screen-space
		// translation: the weapon slid sideways and barely appeared to turn at all.
		//
		// ⚠️ IT IS HELD AND APPLIED AFTER THE POSITION INSTEAD, about a pivot on the weapon. The
		// decay still happens here, because that is per-frame state and has to tick exactly once.
		var recoilTilt = Rotation.Identity;
		var hasTilt = false;

		if ( _visualRecoil.LengthSquared > 0.0001f || MathF.Abs( _visualPunch ) > 0.001f )
		{
			var rate = (_visualRecoilRecovery > 0 ? RealTime.SmoothDelta / _visualRecoilRecovery : 1f).Clamp( 0f, 1f );
			_visualRecoil = Vector3.Lerp( _visualRecoil, Vector3.Zero, rate );
			_visualPunch = MathX.Lerp( _visualPunch, 0f, rate );

			// ⚠️ THE PUNCH STAYS A POSITION TERM AND STAYS HERE, because it is a translation —
			// sliding the gun back at the eye is the same motion wherever it is composed.
			vrPos.y -= _visualPunch;   // y = forward, so minus drives it back at the eye
		}

		// ── SOFTNESS: THE DRAWN TILT CHASES THE KICK INSTEAD OF BEING IT ──────────────
		//
		// ⛔ THE KICK USED TO ARRIVE IN ONE FRAME, WHICH IS INFINITE ACCELERATION. Only the
		// return was smoothed, so the gun teleported to full tilt and then eased home — hard on,
		// soft off. `_visualRecoil` is now the TARGET and `_tiltShown` is what gets drawn.
		//
		// ⚠️ A DAMPED SPRING, NOT A SECOND LERP, because a lerp has only one control and the
		// request has two: *"how quickly it accelerates AND stops."* Stiffness sets the first,
		// damping the second, and they are independent — which a single rate cannot be.
		//
		// ⚠️ SEMI-IMPLICIT EULER: velocity first, then position FROM THE NEW VELOCITY. The
		// explicit form (position from the old velocity) feeds energy into a spring instead of
		// taking it out, so a stiff setting would oscillate wider every frame until the gun span.
		// One reordered line is the whole difference between stable and divergent.
		//
		// ⚠️ AND `dt` IS CAPPED. A hitch of a quarter second through a stiff spring is a step
		// far past the target, which the next frame answers with a bigger step back. Clamping
		// trades exactness during a stall — where nothing is being judged anyway — for never
		// exploding.
		var soften = NZombies.GlobalHandling.VisualTiltSoften;

		if ( soften <= 0.001f )
		{
			_tiltShown = _visualRecoil;
			_tiltVel = Vector3.Zero;
		}
		else if ( _visualRecoil.LengthSquared > 1e-8f
			|| _tiltShown.LengthSquared > 1e-8f
			|| _tiltVel.LengthSquared > 1e-8f )
		{
			// ⛔ THIS BLOCK MUST KEEP RUNNING AFTER THE TARGET REACHES ZERO, which is why it
			// tests the spring's own state as well. Gating it on the target alone would freeze the
			// model at whatever tilt it happened to be holding the moment the target expired —
			// a gun stuck permanently askew, and only on the shots that ended mid-motion.
			var dt = MathF.Min( RealTime.SmoothDelta, 0.05f );
			var damp = MathF.Max( 0.05f, NZombies.GlobalHandling.VisualTiltDamp );

			// ⚠️ 4/t, NOT 1/t, SO THE KNOB MEANS WHAT IT SAYS. A critically damped spring covers
			// 90% of its distance in about 4/omega seconds, so omega = 1/t would have made "0.05"
			// deliver a 0.2s rise — four times slower than the number claims, and slower than the
			// 0.075s between shots at 800 rpm. Verified by simulating the integrator.
			var omega = 4f / soften;

			// ⛔ SUB-STEPPED, BECAUSE THIS INTEGRATOR HAS A STABILITY LIMIT AND THE KNOB DRIVES
			// STRAIGHT THROUGH IT. Semi-implicit Euler on a spring needs roughly
			// `2 * damp * omega * h < 2`; at soften 0.02 and 30 fps the real numbers were 3.3, and
			// a simulation of this exact loop ran away to 4.9e5 degrees in a third of a second.
			// Shipping a knob whose useful range explodes the gun is not a knob.
			//
			// ⚠️ SO THE STEP IS SPLIT UNTIL IT IS SAFE rather than the setting being refused.
			// Sixteen is far more than the default needs (six at 60 fps) and costs a few float
			// operations in the worst case.
			var hMax = 0.5f / (omega * MathF.Max( 1f, 2f * damp ));
			var steps = Math.Clamp( (int)MathF.Ceiling( dt / hMax ), 1, 16 );
			var h = dt / steps;

			// ⚠️ AND IF SIXTEEN STILL IS NOT ENOUGH, THE STIFFNESS IS CAPPED RATHER THAN THE
			// SIMULATION ABANDONED. That only happens for an extreme setting at a bad frame rate,
			// and the honest failure is "as sharp as this frame rate can carry" — not a weapon
			// that spins off the screen on the one machine that dropped frames.
			if ( h > hMax ) omega = 0.5f / (h * MathF.Max( 1f, 2f * damp ));

			var stiffness = omega * omega;
			var friction = 2f * damp * omega;

			for ( var i = 0; i < steps; i++ )
			{
				_tiltVel += ((_visualRecoil - _tiltShown) * stiffness - _tiltVel * friction) * h;
				_tiltShown += _tiltVel * h;
			}

			if ( _tiltShown.LengthSquared < 1e-8f && _tiltVel.LengthSquared < 1e-8f )
			{
				_tiltShown = Vector3.Zero;
				_tiltVel = Vector3.Zero;
			}
		}

		// ⚠️ REPORTED EVERY FRAME, INCLUDING THE ONE WHERE IT REACHES ZERO, which is what lets
		// the probe print a peak and then stop. Gating this on a non-zero tilt would mean the
		// summary line never fires.
		NZombies.TiltProbe.Frame( _tiltShown );

		if ( _tiltShown.LengthSquared > 1e-8f )
		{
			// The tilt as a player reads it: pitch raises the muzzle, yaw swings it across, roll
			// turns it about the barrel. Plain VIEW space, nothing model-specific.
			var viewTilt = Rotation.From( -_tiltShown.x, _tiltShown.y, _tiltShown.z );

			// ⛔ REBASED OUT OF THE MODEL'S FRAME, AND WITHOUT THIS TWO OF THE THREE AXES ARE THE
			// WRONG ONES. `vrRot` carries the VMYaw = 90 correction documented below, which maps the
			// viewmodel's Right onto the CAMERA's Forward. Applied in that frame:
			//
			//     pitch turns about the model's right  = the camera's FORWARD -> you get a ROLL
			//     yaw   turns about the model's up     = the camera's up      -> you get a yaw
			//     roll  turns about the model's forward = the camera's LEFT   -> you get a PITCH
			//
			// So pitch and roll were swapped and only yaw was ever right. Reported exactly that
			// way: *"the vertical part does basically nothing, the side one is very good."* Side
			// is yaw, and yaw is the one axis the correction leaves alone.
			//
			// ⚠️ A SIMILARITY TRANSFORM RATHER THAN SWAPPING THE COMPONENTS BY HAND. Both fix the
			// axes; only this one also gets the SIGNS right without a derivation that has to be
			// redone if the correction ever changes. `WorldRotation` is `camera * vrRot` by the
			// time this is applied, so `vrRot.Inverse * viewTilt * vrRot` composes to
			// `camera * viewTilt * vrRot` — the tilt in camera space, then the model correction,
			// which is the order that was wanted all along.
			//
			// ⚠️ AND IT LEAVES A TUNED SIDE VALUE ALONE. Yaw maps to yaw under the transform, so
			// the horizontal lean is bit-for-bit what it was; only the two broken axes move.
			recoilTilt = vrRot.Inverse * viewTilt * vrRot;
			hasTilt = true;
		}

		// Change the angles and positions of the viewmodel with the new vectors
		//
		// ⛔ THE AXES BELOW ARE THE VIEWMODEL'S, NOT THE CAMERA'S, AND READING THEM LITERALLY GIVES
		// THE WRONG ANSWER FOR EVERY AUTHORED OFFSET. `WorldRotation` has just been multiplied by
		// vrRot, which carries the VMYaw = 90 model correction -- and a quarter turn in yaw maps the
		// viewmodel's Right onto the CAMERA's Forward:
		//
		//     Forward (1,0,0) -> (0,1,0) = Left        Right (0,-1,0) -> (1,0,0) = Forward
		//
		// So `Pos.x` -- the offset editor's X slider, and slot 0 of AimAnimData / ViewModelOffset /
		// RunAnimData -- is DEPTH, negative being toward the eye. `Pos.y` is sideways. The line
		// says x * Right and means x * forward, which is exactly backwards from how it reads.
		WorldRotation *= vrRot;
		// Position has to be set after rotation!
		WorldPosition += vrPos.z * WorldRotation.Up + vrPos.y * WorldRotation.Forward + vrPos.x * WorldRotation.Right;

		// ── THE KICK, NOW THAT THE GUN IS WHERE IT BELONGS ──────────────────────────
		//
		// ⚠️ ROTATE-ABOUT-A-POINT, DONE ON THE OBJECT RATHER THAN ON THE FRAME. Turning the model
		// in place means the pivot must not move, so the origin is swung around it by exactly the
		// amount the orientation turned. `delta` is that turn expressed in WORLD space, which is
		// what `new * old.Inverse` gives — `recoilTilt` itself lives in the viewmodel's local
		// frame, and that frame carries the VMYaw = 90 correction, so using it directly on a world
		// vector would rotate about the wrong axes entirely.
		//
		// ⚠️ THE BOUNDS ARE THE MODEL'S OWN, so "the centre" follows whatever is in your hands
		// rather than a constant that would suit one gun and miss 489 others. They lag the pose by
		// a frame, which at these angles is invisible.
		//
		// ⚠️ AND IT APPLIES TO THE OLD RANDOM-DIRECTION PATH TOO, not only the follow-the-kick
		// one: both arrive as `_visualRecoil`, so `nz_recoil_tilt 0` gets the fixed pivot as well.
		// The bug was never about which direction the kick pointed.
		if ( hasTilt )
		{
			var pivot = WorldPosition;
			var blend = NZombies.GlobalHandling.VisualPivot;

			if ( blend != 0f && ViewModelRenderer.IsValid() )
			{
				// ⛔ WRITTEN OUT RATHER THAN `Vector3.Lerp`, SO IT CAN EXTRAPOLATE. Lerp clamps its
				// fraction to 0..1, so every value past 1 would have silently landed on the model's
				// centre — a slider that moves and does nothing, which is the exact failure this
				// panel keeps being asked to fix. Past 1 the pivot walks out beyond the centre
				// toward the muzzle, which is where a gun pivoting in two hands actually turns:
				// far enough out and the STOCK swings rather than the barrel.
				pivot += (ViewModelRenderer.Bounds.Center - pivot) * blend;
			}

			var before = WorldRotation;
			WorldRotation *= recoilTilt;

			var delta = WorldRotation * before.Inverse;
			WorldPosition = pivot + delta * (WorldPosition - pivot);
		}

		// ⚠️ THE MW BASE'S GUN MOTION goes on last, about the eye, and does nothing while that look is off.
		ApplyMwLook();

		// ⛔ COMPOSED IN THE SAME ORDER AS THE REAL POSE ABOVE — rotation first, then position in the
		// ROTATED frame. Composing it any other way makes the difference between the two include an
		// error that grows with the pose, not just the sway.
		var freeRot = Camera.WorldRotation * finalRotNoSway;
		var freePos = Camera.WorldPosition
			+ finalVectorPosNoSway.z * freeRot.Up
			+ finalVectorPosNoSway.y * freeRot.Forward
			+ finalVectorPosNoSway.x * freeRot.Right;
		SwayFreeTransform = new Transform( freePos, freeRot );

		if ( finalWeaponFOV != cachedFOV )
		{
			cachedFOV = finalWeaponFOV;
			cachedVerticalFOV = Screen.CreateVerticalFieldOfView( finalWeaponFOV );
		}
		Camera.FieldOfView = cachedVerticalFOV;

		// Initialize the target vectors for this frame
		targetVectorPos = Vector3.Zero;
		targetVectorRot = Vector3.Zero;
		targetWeaponFOV = Weapon.ViewModelFOV;

		// Editor mode
		if ( EditorMode )
		{
			targetVectorRot += MathUtil.ToVector3( EditorOffset.Angle );
			targetVectorPos += EditorOffset.Pos;
			targetWeaponFOV = EditorFOV;
			return;
		}

		// ⛔ WHOLE-VIEWMODEL YAW CORRECTION — live-tunable with nz_vm_yaw.
		//
		// User's observation, which no source-side edit explained: *"it's as if
		// it's rotated 90 degrees around the player… the positions we created are
		// still all proportional to each other but to the RIGHT of the player, not
		// the front."* Offsets proportionally intact but the whole frame turned is
		// a rotation of the VIEWMODEL, not of a bone — a bone would distort the
		// relationships between the poses, and these are preserved.
		//
		// ⚠️ Applied to targetVectorRot, in the same units and the same place the
		// aim/run offsets are, so it composes with them instead of fighting them.
		// Dial it with `nz_vm_yaw <deg>` and bake the value once it is right.
		// ⚠️ POSITION here, rotation LOWER DOWN — see the note at ToRotation.
		targetVectorPos += new Vector3( VMX, VMY, VMZ );

		// ⛔ THE HIP OFFSET IS THE IDLE POSE, NOT A BASE ADDED TO EVERYTHING.
		//
		// This is the correction that actually mattered. The three offsets are
		// each measured ABSOLUTELY in the offset editor — you drag the gun to
		// where you want it and read the numbers off. SWB, however, ADDS
		// AimAnimData and RunAnimData on top of whatever pose is current. For
		// SWB's own weapons those are the same thing, because their base pose is
		// zero (the model is authored in place). For a PORTED model the base is
		// not zero, so adding hip underneath ADS gave hip+ads — a position nobody
		// measured and nowhere near either value.
		//
		// So it applies only while the gun is at rest. Aiming and running each
		// own the pose outright, exactly as they were dialled in.
		//
		// ⚠️ No snap on transition: targetVector is lerped toward, not assigned,
		// so dropping this term blends out over the same frames the aim pose
		// blends in.
		// ⛔ MIRRORS THE CONDITIONS THAT ACTUALLY CLAIM THE POSE, NOT AN APPROXIMATION OF THEM.
		//
		// This read `Weapon.IsAiming` alone, while HandleIronAnimation applies the aim pose only
		// when `IsAiming && !IsReloading && AimAnimData != Zero`. Reloading with the aim button
		// held therefore satisfied NEITHER branch: hip stood down because the gun was aiming, aim
		// stood down because the gun was reloading, and the viewmodel fell back to the raw model
		// origin for the whole reload — so the offset dialled in the editor visibly stopped
		// applying the moment a reload started.
		//
		// ⚠️ The AimAnimData check closes the same hole for a second case: a weapon with no ADS
		// pose authored lost its hip pose the instant it aimed, for exactly the same reason.
		bool posedElsewhere = AimOwnsPose
			|| (Weapon.RunAnimData != AngPos.Zero
				&& (Weapon.ShouldTuckVar || Weapon.IsRunning));

		if ( !posedElsewhere )
		{
			targetVectorRot += MathUtil.ToVector3( Weapon.ViewModelOffset.Angle );
			targetVectorPos += Weapon.ViewModelOffset.Pos;
		}


		// I'm sure there's something already that does this for me, but I spend an hour
		// searching through the wiki and a bunch of other garbage and couldn't find anything...
		// So I'm doing it manually. Problem solved.
		var eyeRot = player.EyeAngles.ToRotation();
		localVel = new Vector3( eyeRot.Right.Dot( player.Velocity ), eyeRot.Forward.Dot( player.Velocity ), player.Velocity.z );

		HandleIdleAnimation();
		HandleWalkAnimation();
		HandleJumpAnimation();

		// Tucking
		if ( Weapon.RunAnimData != AngPos.Zero && Weapon.ShouldTuckVar )
		{
			var animationCompletion = 1f;

			if ( !player.IsClimbingLadder )
				animationCompletion = Math.Min( 1, ((Weapon.TuckRange - Weapon.TuckDist) / Weapon.TuckRange) + 0.5f );

			targetVectorPos += Weapon.RunAnimData.Pos * animationCompletion;
			targetVectorRot += MathUtil.ToVector3( Weapon.RunAnimData.Angle * animationCompletion );
			return;
		}

		HandleSwayAnimation();
		HandleIronAnimation();
		HandleSprintAnimation();
		HandleCustomizeAnimation();
	}

	protected virtual void HandleIdleAnimation()
	{
		// No swaying if aiming
		if ( Weapon.IsAiming )
			return;

		// Perform a "breathing" animation
		var breatheTime = RealTime.Now * 2.0f;
		targetVectorPos -= new Vector3( MathF.Cos( breatheTime / 4.0f ) / 8.0f, 0.0f, -MathF.Cos( breatheTime / 4.0f ) / 32.0f );
		targetVectorRot -= new Vector3( MathF.Cos( breatheTime / 5.0f ), MathF.Cos( breatheTime / 4.0f ), MathF.Cos( breatheTime / 7.0f ) );

		// Crouching animation
		if ( player.IsCrouching && player.IsOnGround )
			targetVectorPos += new Vector3( -1.0f, -1.0f, 0.5f );
	}

	protected virtual void HandleWalkAnimation()
	{
		var breatheTime = RealTime.Now * 16.0f;
		var walkSpeed = new Vector3( player.Velocity.x, player.Velocity.y, 0.0f ).Length;
		var maxWalkSpeed = 200.0f;
		var roll = 0.0f;
		var yaw = 0.0f;

		// Check if on the ground
		if ( !player.IsOnGround )
			return;

		// ⚠️ AN MW BASE GUN'S SPRINT CLIP IS ITS RUN (`NZombies.MwRunBob`, 2026-10-02): this bob fought its own cadence
		if ( NZombies.MwRunBob.Skip( Weapon ) )
			return;

		// Check if sprinting
		if ( player.IsRunning )
		{
			breatheTime = RealTime.Now * 18.0f;
			maxWalkSpeed = 100.0f;
		}

		// Check for sideways velocity to sway the gun slightly
		if ( Weapon.IsAiming || localVel.x > 0.0f )
			roll = -7.0f * (localVel.x / maxWalkSpeed);
		else if ( localVel.x < 0.0f )
			yaw = 3.0f * (localVel.x / maxWalkSpeed);

		// Check if ADS & firing
		if ( Weapon.IsAiming && Weapon.TimeSincePrimaryShoot < 0.1f )
		{
			targetVectorRot -= new Vector3( 0, 0, roll );
			return;
		}

		// Perform walk cycle
		targetVectorPos -= new Vector3( (-MathF.Cos( breatheTime / 2.0f ) / 5.0f) * walkSpeed / maxWalkSpeed - yaw / 4.0f, 0.0f, 0.0f );
		targetVectorRot -= new Vector3( (Math.Clamp( MathF.Cos( breatheTime ), -0.3f, 0.3f ) * 2.0f) * walkSpeed / maxWalkSpeed, (-MathF.Cos( breatheTime / 2.0f ) * 1.2f) * walkSpeed / maxWalkSpeed - yaw * 1.5f, roll );
	}


	protected virtual void HandleSwayAnimation()
	{
		var swayspeed = 5;

		// Fix the sway faster if we're ironsighting
		if ( Weapon.IsAiming )
			swayspeed = 20;

		SwaySpeed = swayspeed;

		// Lerp the eye position
		lastEyeRot = Rotation.Lerp( lastEyeRot, player.Camera.WorldRotation, swayspeed * RealTime.SmoothDelta );

		// Calculate the difference between our current eye angles and old (lerped) eye angles
		var angDif = player.Camera.WorldRotation.Angles() - lastEyeRot.Angles();
		angDif = new Angles( angDif.pitch, MathX.RadianToDegree( MathF.Atan2( MathF.Sin( MathX.DegreeToRadian( angDif.yaw ) ), MathF.Cos( MathX.DegreeToRadian( angDif.yaw ) ) ) ), 0 );

		SwayLag = angDif;

		// Perform sway
		//
		// ⚠️ Lifted into locals ONLY so they can be published — the arithmetic and the magic numbers
		// are unchanged. 0.04 units and 0.2 degrees per degree of lag, capped at 1.5 units and 4
		// degrees, are what decide how far the gun swings.
		var posSway = new Vector3( Math.Clamp( angDif.yaw * 0.04f, -SwayPosLimit, SwayPosLimit ), 0.0f, Math.Clamp( angDif.pitch * 0.04f, -SwayPosLimit, SwayPosLimit ) );
		var rotSway = new Vector3( Math.Clamp( angDif.pitch * 0.2f, -SwayRotLimit, SwayRotLimit ), Math.Clamp( angDif.yaw * 0.2f, -SwayRotLimit, SwayRotLimit ), 0.0f );

		// ⚠️ PUBLISHED EVEN WHEN SUPPRESSED, and zeroed rather than skipped: SwayFreeTransform is
		// computed by SUBTRACTING these from the target, so leaving a stale non-zero offset behind
		// would make the ADS dot read a sway the gun is no longer doing.
		if ( !Weapon.UseSway )
		{
			posSway = Vector3.Zero;
			rotSway = Vector3.Zero;
		}

		targetVectorPos += posSway;
		targetVectorRot += rotSway;

		SwayPosOffset = posSway;
		SwayRotOffset = rotSway;
	}


	/// <summary>
	/// Does the aim pose own the viewmodel this frame.
	/// </summary>
	///
	/// ⛔ ONE EXPRESSION, READ IN BOTH PLACES. `HandleIronAnimation` applies the aim pose and the
	/// hip-offset branch stands down for it; when those two conditions drifted apart, aiming during
	/// a reload satisfied NEITHER and the viewmodel dropped to the raw model origin for the whole
	/// reload. They cannot drift if there is only one of them.
	///
	/// ⚠️ `AimHold` BYPASSES `IsReloading`, WHICH IS THE POINT. The sight editor has to hold the
	/// weapon at the sights while the gun does whatever it does — and the Prisma recharges its clip
	/// continuously, so "wait until it is not reloading" is not a state a person can tune in.
	///
	/// ⚠️ IT DOES NOT BYPASS `AimAnimData != Zero`. That is the weapon saying it has no aim pose at
	/// all, and forcing one would hand the viewmodel a zero offset while ALSO standing the hip
	/// offset down — the gun would jump to the model origin, which is the exact bug above.
	public bool AimOwnsPose => Weapon.IsValid() && Weapon.AimAnimData != AngPos.Zero
		&& (NZombies.SckPartsRig.AimHold || (Weapon.IsAiming && !Weapon.IsReloading));

	protected virtual void HandleIronAnimation()
	{
		if ( AimOwnsPose )
		{
			var speedMod = 1f;
			if ( aimTime == 0 )
			{
				aimTime = RealTime.Now;
			}

			var timeDiff = RealTime.Now - aimTime;

			// Mod only while actively scoping
			if ( Weapon.IsScoping || (!Weapon.IsScoping && timeDiff < 0.2f) )
			{
				speedMod = timeDiff * 10;
			}

			// ⛔ DEADSHOT'S ADS SPEED IS APPLIED HERE. Its multiplier had no reader
			// outside WeaponStatsPanel, so a third of the perk was doing nothing.
			//
			// ⚠️ INSIDE THE AIMING BRANCH ONLY, never on the `animSpeed` reset that
			// runs every frame. That reset is the rate for EVERY viewmodel pose — idle,
			// run, crouch, reload — and scaling it there would speed the whole viewmodel
			// up. Deadshot buys a faster aim-in, not a gun that animates at double time.
			//
			// ⚠️ AIM-IN ONLY, not aim-out: the `else` branch keeps its own rate. The
			// perk is sold as "aim down sights faster" and lowering the gun quicker is
			// not part of it.
			//
			// ⚠️ AND THE FOV RATE WITH IT. The pose and the zoom are one visual event
			// to the player; speeding the model up while the FOV kept its own rate would
			// have the gun arrive at the shoulder before the sight picture caught up,
			// which looks like a bug rather than a faster aim.
			//
			// ⚠️ THE TECH NODES RIDE THE SAME LOCAL, rather than getting their own
			// multiply further down. `adsSpeed` is what both the pose rate and the FOV
			// rate below read, so folding them in here is what keeps the two moving
			// together for a weapon that has a node, Deadshot, or both.
			//
			// ⛔ AND THEY COME THROUGH `AdsSpeedFactor`, NOT A LOCAL `Factor` CALL. That
			// accessor is Quickdraw x Emplacement in one place, because this rate has a
			// second home in PlayerCameraHandler and reading the nodes separately in the
			// two files is what let Quickdraw ship half-wired. Emplacement's x0.25 would
			// desync the gun pose from the world zoom by FOUR times.
			//
			// ⚠️ The magnitudes stay in WeaponTech.cs. `nz_tech` prints that catalogue,
			// so a number typed at this call site would make the printed table a liar.
			var adsSpeed = NZombies.PerkEffects.AimSpeedMultiplierFor( Weapon )
				* NZombies.TechEffects.AdsSpeedFactor( Weapon );

			animSpeed = 10 * Weapon.AnimSpeed * speedMod * adsSpeed;
			targetVectorPos += Weapon.AimAnimData.Pos;
			targetVectorRot += MathUtil.ToVector3( Weapon.AimAnimData.Angle );

			if ( Weapon.AimInfo.ViewModelFOV > 0 )
				targetWeaponFOV = Weapon.AimInfo.ViewModelFOV;

			weaponFOVSpeed = Weapon.AimInfo.AimInFOVSpeed * adsSpeed;
		}
		else
		{
			aimTime = 0;
			targetWeaponFOV = Weapon.ViewModelFOV;
		}
	}

	protected virtual void HandleSprintAnimation()
	{
		if ( Weapon.IsRunning && Weapon.RunAnimData != AngPos.Zero && !Weapon.IsCustomizing )
		{
			targetVectorPos += Weapon.RunAnimData.Pos;
			targetVectorRot += MathUtil.ToVector3( Weapon.RunAnimData.Angle );
		}
	}


	protected virtual void HandleCustomizeAnimation()
	{
		if ( Weapon.IsCustomizing && Weapon.CustomizeAnimData != AngPos.Zero )
		{
			targetVectorPos += Weapon.CustomizeAnimData.Pos;
			targetVectorRot += MathUtil.ToVector3( Weapon.CustomizeAnimData.Angle );
		}
	}

	protected virtual void HandleJumpAnimation()
	{
		// If we're not on the ground, reset the landing animation time
		if ( !player.IsOnGround )
			landTime = RealTime.Now + 0.31f;

		// Reset the timers once they elapse
		if ( landTime < RealTime.Now && landTime != 0.0f )
		{
			landTime = 0.0f;
			jumpTime = 0.0f;
		}

		// If we jumped, start the animation
		if ( Input.Down( InputButtonHelper.Jump ) && jumpTime == 0.0f )
		{
			jumpTime = RealTime.Now + 0.31f;
			landTime = 0.0f;
		}

		// If we're not ironsighting, do a fancy jump animation
		if ( !Weapon.IsAiming )
		{
			if ( jumpTime > RealTime.Now )
			{
				// If we jumped, do a curve upwards
				var f = 0.31f - (jumpTime - RealTime.Now);
				var xx = MathUtil.BezierY( f, 0.0f, -4.0f, 0.0f );
				var yy = 0.0f;
				var zz = MathUtil.BezierY( f, 0.0f, -2.0f, -5.0f );
				var pt = MathUtil.BezierY( f, 0.0f, -4.36f, 10.0f );
				var yw = xx;
				var rl = MathUtil.BezierY( f, 0.0f, -10.82f, -5.0f );
				targetVectorPos += new Vector3( xx, yy, zz ) / 4.0f;
				targetVectorRot += new Vector3( pt, yw, rl ) / 4.0f;
				animSpeed = 20.0f;
			}
			else if ( !player.IsOnGround )
			{
				// Shaking while falling
				var breatheTime = RealTime.Now * 30.0f;
				targetVectorPos += new Vector3( MathF.Cos( breatheTime / 2.0f ) / 16.0f, 0.0f, -5.0f + (MathF.Sin( breatheTime / 3.0f ) / 16.0f) ) / 4.0f;
				targetVectorRot += new Vector3( 10.0f - (MathF.Sin( breatheTime / 3.0f ) / 4.0f), MathF.Cos( breatheTime / 2.0f ) / 4.0f, -5.0f ) / 4.0f;
				animSpeed = 20.0f;
			}
			else if ( landTime > RealTime.Now )
			{
				// If we landed, do a fancy curve downwards
				var f = landTime - RealTime.Now;
				var xx = MathUtil.BezierY( f, 0.0f, -4.0f, 0.0f );
				var yy = 0.0f;
				var zz = MathUtil.BezierY( f, 0.0f, -2.0f, -5.0f );
				var pt = MathUtil.BezierY( f, 0.0f, -4.36f, 10.0f );
				var yw = xx;
				var rl = MathUtil.BezierY( f, 0.0f, -10.82f, -5.0f );
				targetVectorPos += new Vector3( xx, yy, zz ) / 2.0f;
				targetVectorRot += new Vector3( pt, yw, rl ) / 2.0f;
				animSpeed = 20.0f;
			}
		}
		else
			targetVectorPos += new Vector3( 0.0f, 0.0f, Math.Clamp( localVel.z / 1000.0f, -1.0f, 1.0f ) );
	}
}