Player/ShoveGuard.cs

Player component that prevents players being violently ejected or pushed through walls by zombies or geometry, clamps horizontal velocities, handles containment (undoes frame-crossings through vertical surfaces), and applies fall damage based on downward speed.

NetworkingFile Access
using Sandbox;
using System;
using System.Linq;

namespace NZombies;

/// <summary>
/// Stops a zombie punting the player across the room, stops the wall at the other end hurting
/// them for it, and stops the push putting them THROUGH the wall.
/// </summary>
///
/// ⛔ THE CRUSH IS THE THIRD JOB AND IT WAS THE ONE THAT SOFTLOCKED PEOPLE. Reported as
/// *"zombies can push me through a barricade/invisible wall — effectively softlocking the
/// player"*. Clamping the speed could never have fixed it, because the speed is not what moves
/// them: a keyframed capsule pressing a dynamic body against a static wall leaves the solver with
/// an overlap it MUST resolve, and once the player's centre passes the wall's midplane the
/// shortest way out is the far side. It happens at walking pace, which is why `Ceiling` never
/// saw it.
///
/// ⛔ ZOMBIES STAY SOLID, WHICH IS WHY THIS EXISTS AT ALL. The clean fix for "sent flying" is a
/// `zombie`/`player` row in the collision matrix set to Ignore — one line, no component, no
/// per-frame work. It was rejected on purpose: zombies would stop body-blocking, and being unable
/// to shoulder your way out of a corner is the tension the mode is built on. Keeping them solid
/// means keeping the shove, so the shove is clamped instead of prevented.
///
/// ⛔ THE SHOVE IS A PHYSICS ARTEFACT, NOT A DESIGNED FORCE. A zombie's capsule has no Rigidbody,
/// so it is KEYFRAMED — infinite mass as far as the solver is concerned. When one walks into the
/// player's dynamic body the only way out of the overlap is to move the player, and the speed it
/// leaves behind scales with how fast the zombie was going. That is why this got worse today:
/// pests now run up to 562 u/s.
///
/// ⚠️ GATED ON A ZOMBIE ACTUALLY BEING IN CONTACT, so nothing else that moves the player fast is
/// touched. The shrieker's launch, Oberon's pull and a PhD dive are all DELIBERATE and all survive
/// this untouched — a blanket speed cap would have quietly neutered three features while fixing
/// one bug.
public sealed class ShoveGuard : Component
{
	/// <summary>
	/// The fastest a zombie may leave the player travelling, horizontally.
	/// </summary>
	///
	/// ⚠️ ABOVE A FULLY-AUGMENTED SPRINT (538) ON PURPOSE. This must never slow a player down
	/// under their own power — it is a ceiling on being THROWN, not a speed limit. Anything at or
	/// below what the player can produce themselves would make sprinting into a zombie feel like
	/// running into treacle, which is a worse bug than the one being fixed.
	public static float Ceiling { get; set; } = 560f;

	/// <summary>How close a zombie has to be to count as touching.</summary>
	///
	/// ⚠️ GENEROUS. The capsule is 18 wide and the player's is about the same, so contact happens
	/// somewhere near 40; 64 covers the frame where the solver has already pushed them apart and
	/// the damage has not landed yet.
	public static float ContactRange { get; set; } = 64f;

	/// <summary>
	/// The fastest the player may travel horizontally from ANY cause. No contact test.
	/// </summary>
	///
	/// ⛔ GEOMETRY EJECTIONS ARE THE OTHER HALF, AND THE ZOMBIE GATE MISSED THEM ENTIRELY.
	/// Reported as *"invisible walls or barricades... they usually send me flying and that's
	/// probably what kills me"*. A static collider rebuilt while the player is inside it — a
	/// barricade re-running `BuildCollider` on repair, a wall manager laying its boxes out again —
	/// leaves the solver one way out of the overlap and it takes it hard. No zombie is involved, so
	/// `Ceiling` never saw it.
	///
	/// ⚠️ 1000 IS ABOVE EVERYTHING THE GAME DELIBERATELY DOES, WHICH IS WHY IT CAN BE
	/// UNCONDITIONAL. Measured rather than guessed: the shrieker's `PulseShove` is 420 horizontal
	/// and ADDS to the player's own velocity, so a sprinting player shoved by one reaches about
	/// 958. PhD's slam and the banana springboard are vertical only, and this clamps the HORIZONTAL
	/// component alone, so neither is touched at any value.
	///
	/// ⚠️ IT IS A BACKSTOP, NOT A FIX. Whatever collider is ejecting the player still is; this
	/// stops the ejection being fatal. The two clamps are counted separately so the next one says
	/// whether a zombie or geometry did it.
	public static float HardCeiling { get; set; } = 1000f;

	/// <summary>Velocity at the start of this frame, before anything clamped it.</summary>
	public Vector3 LastVelocity { get; private set; }

	/// <summary>Times the zombie clamp has fired this session. `nz_shove`.</summary>
	public static int Clamped { get; private set; }

	/// <summary>Times the unconditional clamp has fired — i.e. geometry. `nz_shove`.</summary>
	public static int HardClamped { get; private set; }

	/// <summary>Fastest horizontal speed seen this session, for tuning the two ceilings.</summary>
	public static float Peak { get; private set; }

	/// <summary>Our own fall damage. Off leaves the player unable to be hurt by landing at all.</summary>
	public static bool FallDamage { get; set; } = true;

	/// <summary>
	/// Downward speed at touchdown below which landing costs nothing.
	/// </summary>
	///
	/// ⚠️ ABOVE A JUMP, AND THAT SETS THE FLOOR. `PlayerController.JumpSpeed` is 300, so a jump
	/// lands at about 300 and anything at or under that must be free or the game would hurt you
	/// for moving. 450 is roughly a 126-unit drop at s&box's gravity — call it two player heights
	/// before the first point of damage.
	public static float FallSafeSpeed { get; set; } = 450f;

	/// <summary>Downward speed at which a landing costs a full health bar.</summary>
	///
	/// ⚠️ ABOUT A 900-UNIT DROP. Basalt's high walkway to its low one is ~516 units, which arrives
	/// at roughly 908 and takes about 61% of the bar — survivable, and expensive enough that the
	/// drop is a decision rather than a shortcut.
	public static float FallLethalSpeed { get; set; } = 1200f;

	/// <summary>Landings that have cost health this session. `nz_shove`.</summary>
	public static int Fell { get; private set; }

	// ── containment ──────────────────────────────────────────────────────────

	/// <summary>
	/// Undo any frame that took the player through a solid vertical surface. `nz_containment`.
	/// </summary>
	public static bool Containment { get; set; } = true;

	/// <summary>
	/// Height above the player's origin the containment ray runs at.
	/// </summary>
	///
	/// ⚠️ CHEST HEIGHT, AND AWAY FROM BOTH ENDS OF THE CAPSULE. At the feet it would graze every
	/// step and kerb; at the head it would graze door frames. 36 is inside the body on a standing
	/// player and still inside it on a ducking one, which is what keeps a crouch-slide under a
	/// pipe from reading as a wall.
	public static float ProbeHeight { get; set; } = 36f;

	/// <summary>
	/// A move longer than this in one frame is not judged. It is a teleport, not a crossing.
	/// </summary>
	///
	/// ⛔ WITHOUT IT THE TELEPORTER WOULD FIGHT THIS GUARD AND WIN ONCE PER FRAME. `Teleporter`
	/// writes `WorldPosition` outright, as does a respawn, and the straight line from where the
	/// player was to where they now are runs through half the map. 128 is well past what any frame
	/// of ordinary movement covers — a 1000 u/s sprint at 60fps is 17 units, and even a 120ms
	/// hitch at that speed is 120 — so nothing a player does under their own power reaches it.
	public static float BigJump { get; set; } = 128f;

	/// <summary>Crossings undone this session. `nz_shove`.</summary>
	public static int Crossings { get; private set; }

	Vector3 _clear;
	bool _hasClear;
	float _nextSay;

	float _fallSpeed;
	bool _wasOnGround = true;

	/// <summary>The next landing costs nothing: set by a springboard's launch (<see cref="SpareNextLanding"/>).</summary>
	bool _spareLanding;

	/// <summary>
	/// A springboard just threw this player up, so their next landing costs nothing (`SpringboardSpot.SafeLanding`).
	///
	/// ⚠️ IT ALSO STARTS THE FLIGHT CLEAN: the fall measured so far is dropped, and the player counts as already on the
	/// ground this frame. A pad that throws them again as they touch down launches in the same frame as that landing, and
	/// this way the landing neither charges the old flight nor spends the new flight's pardon on a zero-speed touch.
	/// </summary>
	public void SpareNextLanding()
	{
		_spareLanding = true;
		_fallSpeed = 0f;
		_wasOnGround = true;
	}

	/// <summary>A springboard just threw this player (`SpringboardSystem.Launch`), safe landing or not.</summary>
	public void NoteLaunch() => _sinceLaunch = 0f;

	/// <summary>Since the last springboard throw: an "ejection" inside <see cref="LaunchWindow"/> of one is the throw hitting something.</summary>
	TimeSince _sinceLaunch = 999f;

	const float LaunchWindow = 0.5f;

	/// <summary>The guard on a player, or null.</summary>
	public static ShoveGuard Of( NZPlayer player )
		=> player.IsValid()
			? player.Components.Get<ShoveGuard>( FindMode.EverythingInSelf )
			: null;

	/// <summary>
	/// Turn the engine's impact damage off. We do our own, from downward speed alone.
	/// </summary>
	///
	/// ⛔ s&box DOES IMPACT DAMAGE ITSELF, AND THAT IS WHAT WAS KILLING PEOPLE. `Rigidbody` ships
	/// with `EnableImpactDamage` true and `MinImpactDamageSpeed` **500** — and a fully-augmented
	/// sprint is 538. So running into a wall under your own power was already over the line before
	/// anything shoved you, and being ejected by a collider was far over it.
	///
	/// ⛔ THE ENGINE'S VERSION CANNOT BE MADE DIRECTIONAL, WHICH IS THE WHOLE REASON IT GOES. It
	/// reads an impact SPEED and knows nothing about which way the player was going, so any
	/// threshold high enough to ignore a wall is also high enough to ignore most falls. An earlier
	/// attempt tied `MinImpactDamageSpeed` to `HardCeiling` and kept it on; that traded one wrong
	/// behaviour for a duller one. Off, and replaced by something that only looks down.
	///
	/// ⚠️ IN CODE RATHER THAN THE PREFAB, for the reason `NZPlayer.OnStart` gives about tagging:
	/// it keeps the setting with the rest of the player's setup and survives the prefab being
	/// rebuilt, which a value typed into the inspector does not.
	void DisableEngineImpact()
	{
		var body = Components.Get<PlayerController>( FindMode.EverythingInSelf )?.Body;
		if ( !body.IsValid() ) return;

		body.EnableImpactDamage = false;
	}

	protected override void OnStart() => DisableEngineImpact();

	protected override void OnUpdate()
	{
		// ⛔ THE MACHINE THAT OWNS THE BODY, AND ONLY IT. Every machine holds a copy of every
		// player; clamping a proxy's velocity would fight the transform arriving from its owner
		// and show up as a stutter on somebody else's screen.
		if ( Networking.IsActive && !PlayerPresence.Mine( GameObject ) ) return;

		var body = Components.Get<PlayerController>( FindMode.EverythingInSelf )?.Body;
		if ( !body.IsValid() ) return;

		var v = body.Velocity;

		// ⚠️ RECORDED BEFORE THE CLAMP, because the impact test wants the speed the player was
		// ACTUALLY carrying when they met the wall — which is the number this method is about to
		// take away.
		LastVelocity = v;

		// ⚠️ BEFORE THE SPEED CLAMP, BECAUSE THE CLAMP RETURNS EARLY. Three of them, in fact —
		// the common case is a player under every ceiling, and a containment check placed after
		// them would run on almost no frames at all.
		KeepInside( body );

		// ⛔ ONLY THE DOWNWARD COMPONENT EVER BECOMES DAMAGE, which is the point of doing this
		// ourselves. The engine's impact damage reads a speed and cannot tell a fall from a crash;
		// this watches `-v.z` while airborne and keeps the WORST of it, so a landing is judged on
		// how hard the player was actually coming down rather than on how fast they happened to be
		// travelling when they touched something.
		//
		// ⚠️ THE PEAK, NOT THE LAST FRAME. Touchdown can be read a frame after the solver has
		// already arrested the fall, and reading that frame would score a 900 u/s drop as nothing.
		var c = Components.Get<PlayerController>( FindMode.EverythingInSelf );
		var onGround = c.IsValid() && c.IsOnGround;

		if ( !onGround )
		{
			if ( -v.z > _fallSpeed ) _fallSpeed = -v.z;
		}
		else
		{
			if ( !_wasOnGround ) Land( _fallSpeed );
			_fallSpeed = 0f;
		}

		_wasOnGround = onGround;

		var flat = v.WithZ( 0f );
		var speed = flat.Length;

		if ( speed > Peak ) Peak = speed;
		if ( speed <= Ceiling ) return;

		// ⛔ THE ZOMBIE CLAMP FIRST, BECAUSE IT IS THE TIGHTER ONE. A zombie in contact is held to
		// `Ceiling`; anything else only has to stay under `HardCeiling`, which is loose enough that
		// a shrieker can still throw a sprinting player across a room.
		if ( ZombieInContact() )
		{
			body.Velocity = flat.Normal * Ceiling + Vector3.Up * v.z;
			Clamped++;
			return;
		}

		// ⛔ THE PLAYER'S OWN SLIDE IS NOT AN EJECTION (2026-10-03). Stamin-Up's Slide Boost with Banana Colada's coasting launches
		// a slide at 1,001 u/s, one over `HardCeiling`, so every frame of every boosted slide was clamped and logged as "something
		// rebuilt a collider": 12,902 of the round-88 game's 39,682 log lines, a warning nobody could act on. The slide says how
		// fast it is going (`Slide.OwnSpeed`); up to that, with a margin for a downhill run, is the player's own doing. A real
		// ejection is still clamped, to whichever of the two is higher, and still said out loud.
		var limit = MathF.Max( HardCeiling, OwnSpeed() * OwnSpeedMargin );
		if ( speed <= limit ) return;

		// ⚠️ SAID OUT LOUD EVERY TIME, because this one is the symptom of a collider bug that is
		// still there. A silent backstop would let the real cause go unnoticed for good.
		// ⚠️ UNLESS A SPRINGBOARD JUST THREW THEM (2026-10-05). Then it is the throw: Defocus's pad #0 sends a player straight up
		// at 2550 u/s, they are going sideways at 1387-1900 within 20 ms, and back on the pad to be thrown again within half a
		// second, every time in every log tonight. Something above the pad turns the throw, and the old wording sent the reader
		// looking for a collider bug that is not there. Clamped all the same.
		Log.Warning( _sinceLaunch < LaunchWindow
			? $"[nz-shove] thrown sideways at {speed:0} u/s {(float)_sinceLaunch * 1000f:0} ms after a springboard launch"
				+ $" — clamped to {limit:0}; the throw hit something above the pad (check its placement)"
			: $"[nz-shove] ejected at {speed:0} u/s with no zombie in contact"
				+ $" — clamped to {limit:0}; something rebuilt a collider inside the player" );

		body.Velocity = flat.Normal * limit + Vector3.Up * v.z;
		HardClamped++;
	}

	/// <summary>How far over its own speed a slide may run before the guard calls it an ejection: slopes, and the solver's rounding.</summary>
	const float OwnSpeedMargin = 1.1f;

	Slide _slide;

	/// <summary>The player's own horizontal speed right now (`Slide.OwnSpeed`): 0 when not sliding.</summary>
	float OwnSpeed()
	{
		if ( !_slide.IsValid() ) _slide = Components.Get<Slide>( FindMode.EverythingInSelf );
		return _slide.IsValid() ? _slide.OwnSpeed : 0f;
	}

	/// <summary>
	/// Put the player back if the last frame took them through a wall.
	/// </summary>
	///
	/// ⛔ A ZERO-RADIUS RAY BETWEEN TWO CONSECUTIVE POSITIONS, AND THE ZERO RADIUS IS THE WHOLE
	/// TRICK. The player's CENTRE can never come closer to a face than the capsule's radius, so a
	/// ray between two centres cannot touch a wall the player merely slid along, hugged, fell past
	/// or rounded the corner of. The only way it hits one is if the centre actually went through
	/// it — which is the bug, stated exactly. A swept hull would have had to be shrunk by guesswork
	/// and would still have clipped every corner.
	///
	/// ⛔ VERTICAL SURFACES ONLY. A ray between two frames of a step-up or a ramp can graze the
	/// floor it is climbing, and undoing that would make stairs unusable. Nothing in the report is
	/// about falling through a floor, so a mostly-upright normal is ignored.
	///
	/// ⚠️ IT DOES NOT CARE HOW THE PLAYER GOT THERE. A crush that takes ten frames and a pop that
	/// takes one both end with a wall between them and where they were, so both are caught — and
	/// so is anything else that ever does this, including the collider-rebuild ejection the hard
	/// ceiling only ever back-stopped.
	///
	/// ⚠️ THE ANCHOR IS ONE FRAME OLD, so putting them back is a correction of a few units and
	/// does not read as a teleport. While a zombie keeps pressing, it simply happens again — the
	/// player feels a wall that holds instead of a wall that gives way.
	///
	/// ⛔ NOCLIP IS EXEMPT AND HAS TO BE. Flying through geometry is the entire feature; the anchor
	/// is dropped while it is on so the first frame after landing starts clean rather than
	/// yanking the flier back to wherever they took off from.
	void KeepInside( Rigidbody body )
	{
		if ( !Containment ) return;

		if ( Components.Get<Noclip>( FindMode.EverythingInSelf )?.Flying ?? false )
		{
			_hasClear = false;
			return;
		}

		var here = WorldPosition;

		if ( !_hasClear )
		{
			_clear = here;
			_hasClear = true;
			return;
		}

		var moved = here.Distance( _clear );
		if ( moved < 0.01f ) return;

		if ( moved > BigJump )
		{
			_clear = here;
			return;
		}

		var up = Vector3.Up * ProbeHeight;

		// ⚠️ THE BULLET IGNORE LIST IS DELIBERATELY NOT USED. Invisible walls and barricades both
		// carry `PassBullets`, and they are the two things this exists to detect — filtering on
		// that list would have made the guard blind to exactly the surfaces in the report.
		var tr = Scene.Trace.Ray( _clear + up, here + up )
			.WithoutTags( "player", "zombie", "ragdoll", "trigger" )
			.IgnoreGameObjectHierarchy( GameObject )
			.Run();

		// ⚠️ THE ANCHOR ITSELF IS INSIDE SOMETHING — a collider was rebuilt around it. Do not
		// advance it (the new spot is no better) and do not send the player back into it either;
		// the hard ceiling and the solver will push them out, and the next clear frame re-anchors.
		if ( tr.StartedSolid ) return;

		if ( !tr.Hit || MathF.Abs( tr.Normal.z ) >= 0.5f )
		{
			_clear = here;
			return;
		}

		WorldPosition = _clear;

		// ⚠️ HORIZONTAL SPEED GOES, VERTICAL STAYS. Whatever was pushing them into the wall is
		// still there; leaving the inward velocity would spend the next frame doing it again. The
		// fall is not part of the crush, and taking it would cancel a drop the player is owed
		// damage for.
		body.Velocity = Vector3.Up * body.Velocity.z;

		Crossings++;

		// ⚠️ RATE-LIMITED, because a player held against a wall by a crowd triggers this on
		// consecutive frames and a line per frame would bury everything else in the console.
		if ( Time.Now >= _nextSay )
		{
			_nextSay = Time.Now + 1f;
			Log.Warning( $"[nz-shove] put back {moved:0} units — crossed"
				+ $" {(tr.GameObject.IsValid() ? tr.GameObject.Name : "a wall")}"
				+ $" ({Crossings} this session)" );
		}
	}

	/// <summary>
	/// Charge for a landing, from the speed the player was coming down at.
	/// </summary>
	///
	/// ⛔ THROUGH `OnDamage`, NOT `Apply`, AND THAT IS WHAT KEEPS PhD FLOPPER WORKING. The perk is
	/// defined as "immune to anything that is not a zombie" and that test lives on the `OnDamage`
	/// path; `Apply` is the route a zombie's swipe takes and deliberately bypasses it. Fall damage
	/// sent through `Apply` would hurt a player wearing the one perk that exists to stop it.
	///
	/// ⚠️ THE ATTACKER IS THE PLAYER THEMSELVES, which is how the friendly-fire guard already
	/// expects self-damage to arrive — `Health.OnDamage` excludes `attacker == GameObject` from
	/// that check precisely so your own grenade and your own fall still land.
	///
	/// ⚠️ TAGGED `"fall"` so `Armor.Bypasses` lets it through. Plates are for being shot at; a
	/// player should not walk off a roof and have their armour absorb it.
	///
	/// ⚠️ LINEAR BETWEEN THE TWO SPEEDS, and at least one point once it applies at all. A curve
	/// would be a second thing to tune with no way to feel the difference, and a landing that
	/// crosses the threshold and takes zero reads as the system being broken.
	void Land( float down )
	{
		// ⚠️ A SPRINGBOARD'S PARDON IS SPENT BY THE FIRST LANDING, SOFT OR HARD. Kept past a soft one — thrown onto a ledge —
		// it would forgive the next fall that had nothing to do with any springboard.
		var spared = _spareLanding;
		_spareLanding = false;

		if ( !FallDamage || down <= FallSafeSpeed ) return;

		if ( spared )
		{
			Log.Info( $"[nz-shove] landed at {down:0} u/s off a springboard — no damage (safe landing)" );
			return;
		}

		var player = Components.Get<NZPlayer>( FindMode.EverythingInSelf );
		if ( !player.IsValid() || player.IsDown ) return;

		var hp = player.Components.Get<Health>( FindMode.EverythingInSelfAndAncestors );
		if ( !hp.IsValid() ) return;

		var span = MathF.Max( 1f, FallLethalSpeed - FallSafeSpeed );
		var t = MathX.Clamp( (down - FallSafeSpeed) / span, 0f, 1f );
		// ⚠️ OF THE MATCH'S MAX HEALTH (the lobby's Difficulty, 2026-10-05), so a lethal fall is lethal on any difficulty
		var dmg = MathF.Max( 1f, t * Difficulty.MaxHealth );

		var tags = new TagSet();
		tags.Add( "fall" );

		hp.OnDamage( new DamageInfo
		{
			Damage = dmg,
			Attacker = GameObject,
			Position = WorldPosition,
			Tags = tags,
		} );

		Fell++;
		Log.Info( $"[nz-shove] landed at {down:0} u/s — {dmg:0} damage"
			+ $" ({FallSafeSpeed:0} free, {FallLethalSpeed:0} fatal)" );
	}

	/// <summary>
	/// Is a living zombie close enough to be the thing that just moved us?
	/// </summary>
	///
	/// ⚠️ ONLY ASKED WHEN THE PLAYER IS ALREADY OVER THE CEILING, which is almost never — so the
	/// scene walk costs nothing in the normal case. Putting the distance test first would run it
	/// every frame for every player against every zombie in the round.
	///
	/// ⚠️ CORPSES DO NOT COUNT. A ragdoll already ignores the player in the collision matrix, so
	/// one lying nearby is not what pushed them and would only widen the window.
	bool ZombieInContact()
	{
		var scene = Scene ?? Game.ActiveScene;
		if ( !scene.IsValid() ) return false;

		var reach = ContactRange * ContactRange;
		var here = WorldPosition;

		return scene.GetAllComponents<ZombieAI>()
			.Any( z => z.IsValid()
				&& z.State != ZombieState.Dead
				&& z.WorldPosition.DistanceSquared( here ) <= reach );
	}

	/// <summary>
	/// `nz_containment [0|1] [probe] [jump]` — the wall guard, and what it has caught.
	/// </summary>
	[ConCmd( "nz_containment" )]
	public static void ContainmentCmd( int on = -1, float probe = 0f, float jump = 0f )
	{
		if ( on >= 0 ) Containment = on != 0;
		if ( probe > 0f ) ProbeHeight = probe;
		if ( jump > 0f ) BigJump = jump;

		Log.Info( $"[nz-shove] containment {(Containment ? "on" : "OFF")}"
			+ $" · probe {ProbeHeight:0} up · teleports above {BigJump:0} units"
			+ $" · {Crossings} crossing(s) undone" );
	}

	/// <summary>
	/// `nz_unstuck` — put the local player back somewhere the game knows about.
	/// </summary>
	///
	/// ⛔ THE NAVMESH FIRST, NOT THE GUARD'S OWN ANCHOR. Somebody who has been outside the map for
	/// a while has an anchor out there with them — it follows every clear frame, and out in the
	/// void every frame is clear. The mesh is the only description of the playable world that
	/// cannot have followed them out, and `GetClosestPoint` returns the nearest point ON it from
	/// wherever they are, which is the definition of where they should be standing.
	///
	/// ⚠️ MANUAL, AND STAYING MANUAL. An automatic version needs a test for "out of bounds" that
	/// is right every time — and a false positive teleports a player who was fine, mid-fight,
	/// which is worse than the bug. Containment above is the automatic half; this is the hatch.
	[ConCmd( "nz_unstuck" )]
	public static void UnstuckCmd()
	{
		var p = NZPlayer.Local;
		if ( !p.IsValid() ) { Log.Info( "[nz] unstuck: no local player" ); return; }

		var scene = p.Scene ?? Game.ActiveScene;
		var here = p.WorldPosition;
		var g = Of( p );

		var target = scene?.NavMesh?.GetClosestPoint( here );

		if ( !target.HasValue && g.IsValid() && g._hasClear )
			target = g._clear;

		if ( !target.HasValue )
		{
			var spawns = ActiveConfig.Current?.PlayerSpawns;
			if ( spawns is not null && spawns.Count > 0 )
				target = spawns.OrderBy( sp => sp.Position.DistanceSquared( here ) )
					.First().Position;
		}

		if ( !target.HasValue )
		{
			Log.Warning( "[nz] unstuck: no navmesh and no player spawns — nowhere to send you" );
			return;
		}

		// ⚠️ A LITTLE ABOVE IT. A navmesh point sits ON the floor, and dropping a capsule exactly
		// there starts it half inside the surface it is standing on.
		var to = target.Value + Vector3.Up * 8f;
		p.WorldPosition = to;

		var body = p.Components.Get<PlayerController>( FindMode.EverythingInSelf )?.Body;
		if ( body.IsValid() ) body.Velocity = Vector3.Zero;

		// ⚠️ RE-ANCHORED, or the containment guard would read the move as a crossing on the very
		// next frame and undo the rescue.
		if ( g.IsValid() )
		{
			g._clear = to;
			g._hasClear = true;
		}

		Log.Info( $"[nz] unstuck — moved {here.Distance( to ):0} units" );
	}

	/// <summary>`nz_shove` — the knobs and what they have caught.</summary>
	[ConCmd( "nz_shove" )]
	/// ⚠️ THE FALL NUMBERS ARE THE ONES WORTH MOVING, so they are arguments rather than a rebuild.
	/// `free` and `fatal` are a feel pair — how far you may drop for nothing, and how far kills —
	/// and neither can be settled by reading them. `nz_shove 0 0 450 1200` restores the defaults.
	public static void Report( float ceiling = 0f, float hard = 0f,
		float free = 0f, float fatal = 0f )
	{
		if ( ceiling > 0f ) Ceiling = ceiling;
		if ( hard > 0f ) HardCeiling = hard;
		if ( free > 0f ) FallSafeSpeed = free;
		if ( fatal > 0f ) FallLethalSpeed = fatal;

		var p = NZPlayer.Local;
		var g = Of( p );

		Log.Info( $"[nz-shove] ceiling {Ceiling:0} u/s · contact {ContactRange:0}"
			+ " u/s" );
		var body = p.IsValid()
			? p.Components.Get<PlayerController>( FindMode.EverythingInSelf )?.Body
			: null;

		Log.Info( $"[nz-shove] hard ceiling {HardCeiling:0} u/s · peak seen {Peak:0} u/s"
			+ (body.IsValid()
				? $" · engine impact threshold {body.MinImpactDamageSpeed:0}"
					+ $" (damage {(body.EnableImpactDamage ? "on" : "off")})"
				: "") );
		Log.Info( $"[nz-shove] fall damage {(FallDamage ? "on" : "OFF")}"
			+ $" · free under {FallSafeSpeed:0} · fatal at {FallLethalSpeed:0}"
			+ $" · {Fell} costly landing(s)" );
		Log.Info( $"[nz-shove] containment {(Containment ? "on" : "OFF")}"
			+ $" · {Crossings} crossing(s) undone · nz_unstuck if one ever gets through" );
		Log.Info( $"[nz-shove] zombie clamps {Clamped} · geometry clamps {HardClamped}"
			+ (g.IsValid() ? $" · now {g.LastVelocity.Length:0} u/s" : " · no guard on this player") );

		// ⚠️ WHICH COUNTER MOVED IS THE DIAGNOSIS. Zombie clamps mean the horde is shoving;
		// geometry clamps mean a collider was rebuilt inside the player, which is a different bug
		// wearing the same symptom.
		if ( HardClamped > 0 )
			Log.Warning( "[nz-shove] geometry has ejected this player — a barricade or invisible"
				+ " wall rebuilt its collider while they were standing in it" );
	}
}