swb_base/Weapon.BodyReload.cs

Partial Weapon class methods to notify third-person bodies about reload steps and completion. It computes reload step counts and durations for magazine vs shell-by-shell reloads and sends NZNet.PlayerReload messages with phase, start time and total length; tracks whether a shell reload is open.

Networking
using NZombies;
using System;

namespace SWB.Base;

/// <summary>
/// THE RELOAD ON THE PLAYER'S BODY, FOR EVERY SCREEN (2026-10-05). The user: *"I want the reload animations to also be seen in
/// third person"*.
///
/// ⛔ IT WAS ONE BARE `b_reload`, SENT BEFORE THE RELOAD HAD A LENGTH. `StartReload` sent it ahead of the timing, so the body's
/// clip played at its authored 1.67 s whatever the gun took (0.6 s with Speed Cola, 4 s on an LMG); every round of a shotgun
/// re-sent the same gesture to a graph that answers it once; and nothing ever told the body a reload had ENDED. The body's left
/// hand was also pinned to the handguard every frame (`GunGrip.Hands`), so the reload's own left hand never moved.
///
/// ⚠️ NOW EACH STEP OF A RELOAD GOES OUT WITH ITS LENGTH (`NZNet.PlayerReload`) and the body fits its clip to it
/// (`ThirdPersonWeapon.PlayReload`, `speed_reload`). A magazine reload is one step. A round-at-a-time reload is its opening, a
/// step per round, and its closing — the shotgun graph's own round loop. The gun only reports; what each step looks like is
/// decided on each machine by the hold that body is in.
///
/// ⚠️ THE OWNER'S MACHINE ONLY, like the rest of the weapon (`NetworkMode.Never`); the steps travel owner → everyone.
/// </summary>
public partial class Weapon
{
	/// <summary>
	/// The bodies were told a round-at-a-time reload is open (its opening went out, its closing has not): the next round is an
	/// insert, and a closing is owed. Forgotten when a new one begins (`OnShellReload`).
	/// </summary>
	bool _bodyShellOpen;

	/// <summary>
	/// A reload step has begun, <paramref name="seconds"/> long: the timer `StartReload` has just set. Called from there, the first
	/// line that knows the length.
	/// </summary>
	void BodyReloadStep( NZPlayer nz, float seconds, float reloadSpeed )
	{
		if ( !nz.IsValid() ) return;

		var id = nz.GameObject.Id;

		if ( !ShellReloading )
		{
			NZNet.PlayerReload( id, (int)ThirdPersonWeapon.ReloadPhase.Magazine, seconds, seconds );
			return;
		}

		if ( _bodyShellOpen )
		{
			NZNet.PlayerReload( id, (int)ThirdPersonWeapon.ReloadPhase.ShellInsert, seconds, seconds );
			return;
		}

		_bodyShellOpen = true;

		// ⚠️ THE WHOLE RELOAD'S LENGTH GOES WITH THE OPENING, ESTIMATED: a hold with no round loop (a revolver, a tube-fed rifle)
		// plays its one reload clip across all of it rather than once per round. This step loads a round itself
		// (`OnShellReload`), so the rest are the inserts after it, then the closing clip if the gun has one. The rounds are counted
		// the way `OnShellReloadFinish` counts them: Overfill's worth, one insert for a whole-load gun, never more than the reserve.
		var over = OverfillRounds();
		var rounds = ClassTechWholeLoad ? 1 : over >= 0 ? over : ClassTechClip( Primary.ClipSize ) - Primary.Ammo;
		if ( Primary.InfiniteAmmo != InfiniteAmmoType.reserve ) rounds = Math.Min( rounds, Owner.AmmoCount( Primary.AmmoType ) );
		rounds = Math.Max( 1, rounds );

		var insert = ShellReloadInsertTime / Math.Max( 0.01f, reloadSpeed );
		var total = seconds + (rounds - 1) * insert + Math.Max( 0f, ShellReloadEndTime );

		NZNet.PlayerReload( id, (int)ThirdPersonWeapon.ReloadPhase.ShellStart, seconds, total );
	}

	/// <summary>
	/// The reload is over on the gun. A round-at-a-time reload closing (<paramref name="seconds"/> = its closing clip, 0 when it
	/// has none), or a magazine reload stopped part way (<paramref name="cancelled"/>: the knife, a power-up). A magazine reload
	/// that simply finished sends nothing, because its clip was fitted to end with it.
	///
	/// ⚠️ SENT ONCE. `AsyncShellReloadEnd` calls this as its closing clip starts and `CancelShellReload` again when it is done; the
	/// second finds nothing open. `CancelShellReload` also runs on reloads that were never open, which find nothing either.
	/// </summary>
	void BodyReloadEnded( float seconds, bool cancelled )
	{
		var shell = _bodyShellOpen;
		_bodyShellOpen = false;

		if ( !shell && !cancelled ) return;

		var nz = Components.Get<NZPlayer>( FindMode.InAncestors | FindMode.Enabled );
		if ( !nz.IsValid() ) return;

		var phase = shell ? ThirdPersonWeapon.ReloadPhase.ShellEnd : ThirdPersonWeapon.ReloadPhase.Cancel;
		NZNet.PlayerReload( nz.GameObject.Id, (int)phase, seconds, seconds );
	}
}