A Barricade helper partial class that computes which side of a window/run a point is on and adjusts item or zombie drop positions to the near or far side. It finds nearest players and runs, projects points onto the run plane, optionally snaps drops to floor, and provides ResolveDrop and NearestForDrop utilities.
using Sandbox;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// Putting a dropped thing on the PLAYER'S side of a window.
///
/// ⛔ THE PROBLEM: drops spawn where the zombie died, and a zombie killed while
/// tearing boards or climbing through dies OUTSIDE. The powerup or the salvage then
/// sits in the street behind an intact window — visible, unreachable, and a direct
/// punishment for keeping the barricade boarded.
///
/// ⚠️ IT IS A POSITION FILTER, NOT A CONDITION ON THE ZOMBIE'S STATE. Asking "was
/// this zombie attacking a window" needs the AI state, and the state is wrong for
/// exactly the cases that matter: a zombie shot mid-vault is Vaulting, one shot a
/// frame after stepping through is Chasing, and one killed by a Nuke is whatever it
/// happened to be doing. Asking "is this POINT next to a window, and on the wrong
/// side" is true for all three and needs no state at all.
///
/// ⚠️ EVERY PIECE OF GEOMETRY HERE IS BORROWED, NOT REDERIVED. `RunNormal` already
/// answers "which way does this window face" from the object's own transform, and
/// `InReach` already answers "am I at this barricade" including its vertical band. A
/// first draft of this file computed the plane normal with its own cross product and
/// defined its own `SideOf`; that collided with the existing one (CS0111) — which was
/// the compiler pointing out the duplication before it could become a second,
/// disagreeing definition.
/// </summary>
public sealed partial class Barricade
{
/// <summary>How far past the window the drop is placed, in units.
///
/// ⚠️ Not zero. Landing a pickup exactly in the window plane leaves it half
/// inside the frame, where its pickup radius overlaps geometry the player cannot
/// stand in.</summary>
public static float DropInset { get; set; } = 40f;
/// <summary>Horizontal reach for "this death happened at a window".
///
/// ⚠️ Wider than the zombies' `BarricadeReach`, because a zombie shot mid-vault is
/// further from the run than one standing at it tearing boards — and that is the
/// case this exists for.</summary>
public static float DropReach { get; set; } = 96f;
/// <summary>How far down to look for a floor under the corrected point.</summary>
public static float DropFloorDrop { get; set; } = 512f;
/// <summary>
/// How far a point is from the run's plane, in UNITS, signed by side.
///
/// ⚠️ NOT `SideOf`. That returns a cross-product magnitude which scales with run
/// length, so it answers "which side" but its value is in no particular unit —
/// useless for "is it already 40 units clear". This projects onto the normal
/// instead, so the number is a real distance and `DropInset` means what it says.
/// </summary>
public float PlaneDistance( Vector3 point )
{
var n = RunNormal.WithZ( 0f );
if ( n.LengthSquared < 0.01f ) return 0f;
var centre = (RunA + RunB) * 0.5f;
return Vector3.Dot( (point - centre).WithZ( 0f ), n.Normal );
}
/// <summary>+1 / -1 for the side a point is on, 0 when it is in the plane.
///
/// ⚠️ A dead band in real units, which is what `PlaneDistance` buys. A zombie
/// standing exactly in the window plane would otherwise flip side frame to frame
/// on floating-point noise.</summary>
public int DropSideOf( Vector3 point )
{
var d = PlaneDistance( point );
if ( MathF.Abs( d ) < 1f ) return 0;
return d > 0f ? 1 : -1;
}
/// <summary>
/// Move a drop point to the near side of this window, or leave it alone if it is
/// already there.
///
/// ⚠️ "The player's side" is decided by the NEAREST PLAYER, not the first one in
/// the scene — the multiplayer rule, and here also the only reading that means
/// anything: four players can be at four windows.
///
/// ⚠️ DOWNED PLAYERS STILL COUNT. A downed player is lying on the inside and their
/// position answers the question being asked. Excluding them would send the drop
/// outside precisely when the team most needs it.
/// </summary>
public Vector3 NearSideOf( Vector3 at ) => SideOf( at, +1, snapToFloor: true );
/// <summary>
/// The same correction, pushed to the side AWAY from the player — the outside of the window.
/// </summary>
///
/// ⛔ FOR A ZOMBIE TEARING AT IT, WHICH IS THE MIRROR OF THE DROP CASE. A kill at a window has
/// to end up INSIDE or the reward is behind intact boards; a zombie attacking one has to stand
/// OUTSIDE or it is reaching through a wall it has not opened yet, and the player can be hit
/// through boards that are still up.
///
/// ⚠️ IT DOES NOT SNAP TO THE FLOOR, and that is the one real difference. A powerup has no
/// physics and would hang at sill height, so the drop version traces down. A zombie has an
/// agent and a capsule and is already standing on something — tracing it down would drop one
/// on a raised walkway off the edge it was standing on.
public Vector3 FarSideOf( Vector3 at ) => SideOf( at, -1, snapToFloor: false );
Vector3 SideOf( Vector3 at, int sign, bool snapToFloor )
{
var n = RunNormal.WithZ( 0f );
if ( n.LengthSquared < 0.01f ) return at;
n = n.Normal;
var scene = Scene ?? Game.ActiveScene;
if ( !scene.IsValid() ) return at;
var player = scene.GetAllComponents<NZPlayer>()
.Where( p => p.IsValid() )
.OrderBy( p => p.WorldPosition.DistanceSquared( at ) )
.FirstOrDefault();
if ( !player.IsValid() ) return at;
var side = DropSideOf( player.WorldPosition ) * sign;
if ( side == 0 ) return at;
// Already far enough onto the wanted side — nothing to correct, and moving it anyway
// would teleport things that were never wrong.
//
// ⚠️ THIS IS ALSO WHAT MAKES IT SAFE TO CALL EVERY SWING. A zombie re-enters the attacking
// state once per swing; without the early out it would be re-positioned each time, which
// is a visible twitch on something standing still.
if ( PlaneDistance( at ) * side >= DropInset ) return at;
// ⚠️ ANCHORED TO THE CLOSEST POINT ON THE RUN, not to the midpoint. A barricade
// can be a wide double window; a zombie killed at one end should drop at that
// end, not slide to the centre of the opening.
var a = RunA.WithZ( 0f );
var b = RunB.WithZ( 0f );
var ab = b - a;
var t = ab.LengthSquared < 0.01f
? 0f
: Math.Clamp( Vector3.Dot( at.WithZ( 0f ) - a, ab ) / ab.LengthSquared, 0f, 1f );
var onRun = a + ab * t;
var moved = (onRun + n * (side * DropInset)).WithZ( at.z );
// ⛔ TRACED DOWN, BECAUSE A CLIMBING ZOMBIE DIES IN MID-AIR. The powerup spawn
// has no physics — it parks the model at the position given — so a drop from a
// zombie halfway through the window would hang at sill height with nothing
// under it.
if ( !snapToFloor ) return moved;
var from = moved + Vector3.Up * 64f;
var tr = scene.Trace.Ray( from, from + Vector3.Down * DropFloorDrop ).Run();
return tr.Hit ? tr.HitPosition : moved;
}
/// <summary>
/// The barricade a drop at this point should be pulled through, or null.
///
/// ⚠️ GATED BY `InReach`, the same test the repair prompt and the tear check use,
/// so "close enough to be at this window" means one thing across the whole
/// feature — including its vertical band, which matters here more than anywhere:
/// without it a kill on the floor below a window would be dragged through it.
///
/// ⚠️ InReach GATES, DistanceToRun ORDERS — the split the class already documents
/// on InReach itself.
///
/// ⚠️ IT DOES NOT CARE WHETHER THE WINDOW IS BOARDED. An open barricade is still a
/// hole in a wall with a wrong side to it.
/// </summary>
public static Barricade NearestForDrop( Vector3 at, float reach )
{
Barricade best = null;
var bestDist = float.MaxValue;
foreach ( var b in All )
{
if ( !b.IsValid() ) continue;
if ( !b.InReach( at, reach ) ) continue;
var d = b.DistanceToRun( at );
if ( d >= bestDist ) continue;
bestDist = d;
best = b;
}
return best;
}
/// <summary>
/// Where a drop from a death at <paramref name="at"/> should actually land.
/// Returns the point unchanged when no window is involved.
///
/// ⛔ THE ONE ENTRY POINT, AND THERE ARE TWO CALLERS. `PowerupDrops.RollOnDeath`
/// and `PickupDrops.RollOnDeath` (salvage, plates, Vulture) both fire from the same
/// death and both need this. If a third drop system appears it calls this too — a
/// second copy of "is there a window here" would disagree about the reach and one
/// of them would start dropping through walls.
/// </summary>
public static Vector3 ResolveDrop( Vector3 at )
=> NearestForDrop( at, DropReach ) is { } wall ? wall.NearSideOf( at ) : at;
}