SWB integration component for NZPlayer implementing IPlayerBase. It forwards camera, movement, view and ammo queries to the player controller and NZAmmo components, handles parenting to bones, recoil offsets, and delegates damage to the shared Hp/Health component. It customizes AmmoCount/TakeAmmo to use per-weapon NZAmmo rather than pooled ammo.
using Sandbox;
using SWB.Shared;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// SWB INTEGRATION — the half of NZPlayer that Simple Weapon Base talks to.
///
/// `IPlayerBase` is SWB's designed extension point: its own doc says *"Implement
/// this interface on a `Component` to integrate with SWB"*. So this is the
/// supported path, not a workaround — and it is why `swb_player` is NOT
/// vendored: we already have a player, and two player controllers on one
/// GameObject is worse than either.
///
/// Most of what follows is a one-line forward to PlayerController. The two that
/// carry a real decision are AmmoCount and TakeAmmo.
/// </summary>
public sealed partial class NZPlayer : IPlayerBase
{
PlayerController Controller => Components.Get<PlayerController>();
// ── identity ─────────────────────────────────────────────────────────────
// ⚠️ No Id property here. `Component.Id` already satisfies IPlayerBase's
// `Guid Id { get; }`, and declaring one only shadowed it — two ids for one
// player, differing the moment anything compared them.
/// <summary>⚠️ Set by SWB when it builds the viewmodel camera; we only store
/// it. Nothing in nZombies reads it.</summary>
public CameraComponent ViewModelCamera { get; set; }
public CameraComponent Camera => Scene?.Camera;
/// <summary>⚠️ Always true for now. nZombies has no third-person mode, and
/// SWB uses this to decide whether to draw the viewmodel at all — returning
/// false here would hide every weapon.</summary>
/// <summary>
/// ⛔ IT REPORTS THE TRUTH NOW, AND THAT IS WHAT MAKES THIRD PERSON USABLE. Hardcoded `true`,
/// `ThirdPerson` produced a third-person camera with the first-person weapon still glued to the
/// screen — because `ViewModelHandler` reads THIS to decide whether to draw the viewmodel and
/// hands. One constant was the whole reason the built-in toggle was unbound.
///
/// ⚠️ NOTHING ELSE CHANGES IN NORMAL PLAY. `NZPlayer.ThirdPerson` is false unless a diagnostic
/// turned it on, so this reads `true` exactly as before.
/// </summary>
/// <summary>
/// Is THIS player being looked at from behind their own eyes?
///
/// ⛔ IT USED TO READ ONLY THE STATIC `NZPlayer.ThirdPerson`, WHICH IS A FACT ABOUT THE
/// CAMERA, NOT ABOUT A PLAYER. With one body that is the same thing; with two it answered
/// "yes, first person" for EVERY body on the machine — including the other player's — so
/// anything that draws a viewmodel, hides a world model or aims from the eye was being told
/// that somebody else's body was the one you are looking out of.
///
/// ⚠️ THE SAME MISTAKE AS `HideBodyInFirstPerson`, WHICH HID THE OTHER PLAYER ENTIRELY:
/// a "the local player is…" answer applied to a body that is not the local player. When
/// something reads right for one player and wrong for two, look for a static being asked a
/// per-player question.
/// </summary>
public bool IsFirstPerson => !NZPlayer.ThirdPerson && PlayerPresence.Mine( GameObject );
/// <summary>⚠️ Settable because SWB's bots set it. We have no bots, so it is
/// storage rather than behaviour.</summary>
public bool IsBot { get; set; }
// ── movement, straight off the controller ────────────────────────────────
public Vector3 Velocity => Controller.IsValid() ? Controller.Velocity : Vector3.Zero;
public bool IsCrouching => Controller.IsValid() && Controller.IsDucking;
public bool IsOnGround => Controller.IsValid() && Controller.IsOnGround;
public bool IsClimbingLadder => Controller.IsValid() && Controller.IsClimbing;
/// <summary>
/// ⚠️ NOT `Controller.RunSpeed` or an input check — SWB uses this to pick the
/// run animation and to disable firing, so it has to mean "actually moving at
/// run pace", not "the run key is down". A player sprinting into a wall is
/// not running.
/// </summary>
public bool IsRunning
=> Controller.IsValid()
&& WantsToRun
&& Controller.Velocity.WithZ( 0 ).Length > Controller.WalkSpeed * 1.1f;
/// <summary>
/// Is the player actually ASKING to run, right now?
///
/// ⛔ WITHOUT THIS, SPRINT SURVIVES A JUMP. Speed alone cannot express intent
/// while airborne: there is no ground friction, so horizontal velocity is
/// unchanged for the whole jump and letting go of sprint does nothing. You
/// land still above the threshold, and the weapon refuses to aim for the
/// fraction of a second it takes friction to slow you — which reads as the
/// game ignoring that you stopped running.
///
/// ⚠️ Velocity is still required (see above): a player sprinting into a wall
/// is holding the key but is not running. Intent AND motion, not either.
///
/// ⚠️ `RunByDefault` INVERTS the button — its own description: "if true then
/// the player will run by default, and holding AltMoveButton will switch to
/// walk". Hence the `!=` rather than a plain Input.Down.
/// </summary>
bool WantsToRun
{
get
{
if ( !Controller.IsValid() ) return false;
var button = Controller.AltMoveButton;
if ( string.IsNullOrEmpty( button ) ) return Controller.RunByDefault;
return Input.Down( button ) != Controller.RunByDefault;
}
}
public Angles EyeAngles => Controller.IsValid() ? Controller.EyeAngles : Angles.Zero;
public Vector3 EyePos => Controller.IsValid() ? Controller.EyePosition : WorldPosition;
/// <summary>
/// ⛔ DOWNED IS STILL ALIVE, AND STILL SHOOTING. I had this gated on !IsDown
/// on the reasoning that a downed player should not fire — which is wrong for
/// this game specifically: in nZombies you keep your pistol on the floor and
/// crawling-and-shooting is how a solo player earns their own revive.
///
/// ⚠️ So this means "not dead", nothing more. Whatever restricts a downed
/// player (crawl speed, no sprint) belongs in the down state, not in the
/// weapon base's idea of alive.
/// </summary>
public bool IsAlive => Hp is null || Hp.Current > 0f;
public float InputSensitivity
{
get => Controller.IsValid() ? Controller.LookSensitivity : 1f;
set { if ( Controller.IsValid() ) Controller.LookSensitivity = value; }
}
/// <summary>Field of view, for ADS zoom. Stored rather than pushed at the
/// camera every frame — SWB writes it, the camera reads it.</summary>
public float FieldOfView { get; set; } = 90f;
/// <summary>⚠️ Write-only in the interface. It drives the THIRD-person hold
/// pose, which nothing renders yet; stored so a future world model can use
/// it without SWB needing to change.</summary>
public HoldTypes HoldType { get; set; }
// ── ammo — ⛔ THE DECISION ───────────────────────────────────────────────
/// <summary>
/// ⛔ THE AMMO TYPE ARGUMENT IS DELIBERATELY IGNORED.
///
/// SWB, like most weapon bases, models reserve ammo as a POOL PER TYPE owned
/// by the player: every pistol you carry draws from one "pistol" pool.
/// **nZombies does not work that way.** Ammo is bought per weapon at that
/// weapon's wall-buy, Max Ammo refills every gun you are holding to its own
/// maximum, and an M1911 and a Python share nothing despite both being
/// pistols.
///
/// So the reserve lives on the WEAPON (see <see cref="NZAmmo"/>) and these
/// two methods resolve it from whichever weapon is asking. SWB never sees
/// the difference — it asks for ammo and gets the right number.
///
/// ⚠️ The alternative (adopting SWB's pooled model) was considered and
/// rejected: it would make Max Ammo mean "refill the pistol pool" and let a
/// wall-buy top up guns you are not carrying. Cheap to get right now,
/// expensive once the HUD and the wall-buys are wired to it.
/// </summary>
public int AmmoCount( string type )
{
var ammo = ActiveAmmo();
return ammo.IsValid() ? ammo.Reserve : 0;
}
/// <inheritdoc cref="AmmoCount"/>
public int TakeAmmo( string type, int amount )
{
var ammo = ActiveAmmo();
return ammo.IsValid() ? ammo.Take( amount ) : 0;
}
/// <summary>
/// The NZAmmo belonging to the weapon currently held.
///
/// ⚠️ Found through the ACTIVE SWB weapon rather than "the first NZAmmo under
/// the player" — carrying two guns, that would silently reload one from the
/// other's reserve, and the symptom (ammo draining from a holstered weapon)
/// is one nobody would think to look for.
/// </summary>
NZAmmo ActiveAmmo()
{
var weapon = Components
.GetAll<SWB.Base.Weapon>( FindMode.EverythingInSelfAndDescendants )
.FirstOrDefault( w => w.IsValid() && w.Active );
if ( !weapon.IsValid() ) return null;
var ammo = weapon.Components.Get<NZAmmo>();
// ⚠️ Said out loud. A weapon with no NZAmmo reports an empty reserve,
// which in game looks exactly like "this gun refuses to reload" — a
// missing component should not be diagnosed by feel.
if ( !ammo.IsValid() )
Log.Warning( $"[nz] weapon '{weapon.DisplayName}' has no NZAmmo component "
+ "— it will never reload. Add one beside the Weapon." );
return ammo;
}
// ⛔ `FillAllAmmo` DELETED. Its summary said "the Max Ammo powerup" and NOTHING CALLED IT —
// the powerup lives in `PowerupEffects.MaxAmmo`, which walks `NZInventory.Weapons` instead.
// A dead method is harmless; a dead method whose doc claims to BE a feature is not, because
// the next person to change Max Ammo edits this one and sees no effect. §12: grep for the
// READ, not the type.
//
// ⚠️ IT WAS ALSO THE ONLY CALLER OF `NZAmmo.Fill()`, whose own summary still reads
// "Max Ammo, and the wall-buy's ammo purchase" — the wall-buy half is live through other
// paths, so the method stays; only the Max Ammo claim was false.
//
// ⚠️ Left as a note rather than removed silently, the same way `MuleKickAugments.ApplyReserve`
// was: a deleted method with no explanation is an invitation to add it back.
// ── effects ──────────────────────────────────────────────────────────────
/// <summary>Recoil. SWB hands us an offset to add to the view.</summary>
public void ApplyEyeAnglesOffset( Angles offset )
{
var c = Controller;
if ( !c.IsValid() ) return;
c.EyeAngles += offset;
}
/// <summary>⚠️ Deliberately empty. The per-shot screen shake is `MwRecoilFx`'s — the
/// MW Base's rattle, through a camera modifier — fired from the same shot in
/// `Weapon.Shoot`; filling this in as well would shake every shot twice. Silent on
/// purpose: SWB calls it on every shot, so a warning here would be a log line per
/// bullet.</summary>
public void ShakeScreen( ScreenShake screenShake ) { }
/// <summary>
/// Third-person gesture for attacking / reloading.
///
/// ⚠️ A no-op for now, and deliberately silent for the same reason as
/// ShakeScreen: it fires on every shot. nZombies renders no third-person
/// body yet, so there is nothing to gesture with — when a world model
/// arrives this is where its reload and fire gestures hang.
/// </summary>
public void TriggerAnimation( Animations animation ) { }
/// <summary>
/// Parent an object to a bone — SWB uses it to hang world models and
/// attachments off the hands.
///
/// ⚠️ Deletes the object when the bone is missing, because that is what the
/// caller asks for by default: an unparented weapon would otherwise sit at
/// the world origin, visible to everyone, forever.
/// </summary>
public void ParentToBone( GameObject @object, string boneName,
bool deleteOnFail = true, Action<GameObject> onFail = null )
{
var renderer = Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants );
var bone = renderer.IsValid() ? renderer.GetBoneObject( boneName ) : null;
if ( bone is null )
{
onFail?.Invoke( @object );
if ( deleteOnFail ) @object?.Destroy();
return;
}
@object.SetParent( bone );
@object.LocalPosition = Vector3.Zero;
@object.LocalRotation = Rotation.Identity;
}
/// <summary>
/// ⚠️ FORWARDED TO Health, not implemented here. `IPlayerBase` extends
/// `Component.IDamageable`, and NZPlayer already delegates all its health to
/// the shared Health component that zombies use too — implementing damage a
/// second time here would give the player two health systems that disagree.
/// </summary>
public void OnDamage( in Sandbox.DamageInfo damage )
{
Hp?.OnDamage( damage );
}
}