Part of the Barricade class that determines which window a zombie spawner feeds and exactly where a zombie from that spawner should stand. It computes nearest window within reach, which side of the run the spawner is on, determines standing points on floor or navmesh, checks line-of-sight and traces against scene geometry, and exposes a console command listing spawn-window mappings.
using Sandbox;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// A SPAWNER AT A WINDOW: which window it feeds, and where on that window's own side a zombie from it stands (the user,
/// 2026-10-01: *"zombies not getting behind the barricade when spawning ... make it so they are moved to the barricade after
/// spawning, on the side nearest to their spawn point"*). Used by `ZombieCommands.SpawnAt( atWindow: true )`.
///
/// ⛔ WHY THEY ENDED UP IN FRONT OF THE BOARDS. A spawn closet behind a window is barely bigger than a zombie (Defocus: about
/// 68×68 and 79 high), and in half of Defocus's the navmesh does not reach at all (`nz_barr_cross`: 16 of 32 windows have no
/// crossing). The spawn snap (`ZombieAI.NavGround`) then found the nearest mesh, which was the room on the other side of the
/// boards, and the agent held the zombie there.
///
/// ⛔ THE SIDE IS THE SPAWNER'S, NOT THE NEAREST PLAYER'S. `FarSideOf` guesses which side is outside from where the players
/// are. A spawner placed behind a window says it outright.
/// </summary>
public sealed partial class Barricade
{
static float? _spawnWindowReach;
/// <summary>
/// How far from a window's run a spawner may stand and still feed that window, flat, in units. 96. The vertical band is
/// the barricade's own (`InReach`, 64), so a spawner on the floor above or below feeds nothing here.
///
/// ⚠️ Defocus's spawners stand 25 to 41 units from their windows. 96 takes those with room to spare, and stays short of
/// a window across a corridor.
/// </summary>
public static float SpawnWindowReach { get => _spawnWindowReach ?? 96f; set => _spawnWindowReach = value; }
/// <summary>The window a spawner feeds: the nearest run within <see cref="SpawnWindowReach"/> on its own level, or null.</summary>
public static Barricade WindowFor( Vector3 spawner )
{
Barricade best = null;
var bestDist = float.MaxValue;
foreach ( var b in All )
{
if ( !b.IsValid() || !b.InReach( spawner, SpawnWindowReach ) ) continue;
var d = b.DistanceToRun( spawner );
if ( d >= bestDist ) continue;
bestDist = d;
best = b;
}
return best;
}
/// <summary>Which side of the run a spawner is on, +1 or -1; on the line counts as +1.</summary>
int SpawnSide( Vector3 spawner ) => PlaneDistance( spawner ) < 0f ? -1 : 1;
/// <summary>
/// Where a zombie from a spawner at this window stands: on the spawner's side of the run, <see cref="CrossOffset"/> out
/// from its middle, inside the reach a zombie tears from. With a crossing, that is the crossing's own landing point on that
/// side (on the navmesh). Without one it is that point on the floor, and <paramref name="onMesh"/> says whether the
/// navmesh reaches it there. Null when there is no floor for it on the spawner's level, or a wall stands between: the
/// caller then spawns as before.
///
/// ⚠️ OFF THE MESH IS NOT A FAILURE. A zombie there is held where it stands (`ZombieAI.ParkedAt`) until the boards are
/// down, then climbs through to <see cref="RoomSideFor"/>.
/// </summary>
public Vector3? SpawnSideFor( Vector3 spawner, out bool onMesh )
{
onMesh = false;
var side = SpawnSide( spawner );
if ( CrossValid )
{
onMesh = true;
return PlaneDistance( CrossA ) * side > 0f ? CrossA : CrossB;
}
var stand = FloorAt( MidRun() + RunNormal.WithZ( 0f ).Normal * side * CrossOffset, spawner.z );
if ( stand is not Vector3 at ) return null;
// ⚠️ ONLY A STAND THE BOARDS CAN BE TORN FROM (`InReach`, its vertical band too): a zombie held anywhere else would
// stand there for good
if ( !InReach( at, CrossOffset + 1f ) ) return null;
// ⚠️ NOT THROUGH A WALL: the stand and the spawner share the closet, or this window is not the one it feeds
var scene = Scene ?? Game.ActiveScene;
var wall = scene.Trace.Ray( spawner + Vector3.Up * 36f, at + Vector3.Up * 36f ).WithoutTags( StandIgnores ).Run();
if ( wall.Hit && !wall.StartedSolid && wall.Distance < spawner.Distance( at ) - 4f ) return null;
// ⚠️ THE MESH, ONLY ON THIS SIDE. A box around the stand reaches the room's mesh past the boards, and a point there
// would put the zombie back in front of them, which is the bug.
var near = scene.NavMesh?.GetClosestPoint( new BBox( at - new Vector3( 16f, 16f, 24f ), at + new Vector3( 16f, 16f, 24f ) ) );
if ( near is Vector3 mesh && PlaneDistance( mesh ) * side > 4f )
{
onMesh = true;
return mesh;
}
return at;
}
/// <summary>
/// Where a zombie held on a spawner's side climbs to: the run's other side, <see cref="CrossOffset"/> in, on the navmesh of
/// its own level. With a crossing, the crossing's landing point there.
/// </summary>
public Vector3 RoomSideFor( Vector3 spawner )
{
var side = SpawnSide( spawner );
if ( CrossValid ) return PlaneDistance( CrossA ) * side > 0f ? CrossB : CrossA;
var at = MidRun() - RunNormal.WithZ( 0f ).Normal * side * CrossOffset;
var floor = FloorAt( at, spawner.z ) ?? at.WithZ( spawner.z );
// ⚠️ KEPT ONLY IF IT STAYED IN THE ROOM. `NavGround` looks 64 to the side, and the closet is closer than that
var mesh = ZombieAI.NavGround( Scene ?? Game.ActiveScene, floor );
return PlaneDistance( mesh ) * side < 0f ? mesh : floor;
}
Vector3 MidRun() => (RunA + RunB) * 0.5f;
/// <summary>
/// What the stand's two traces see through: bodies, so only the map decides (`FloorAt`, and the wall test in `SpawnSideFor`).
///
/// ⛔ A ZOMBIE ALREADY ON THE STAND MADE THE WINDOW REFUSE THE NEXT ONE (2026-10-05). Every zombie from a window is held on
/// the same stand, and both traces hit bodies: the floor trace starts 40 up, inside the first zombie's capsule
/// (`StartedSolid`), and the wall trace hits its side. So while one waited at the boards, the next from that spawner got no
/// stand, fell back to the bare spawner, and in a closet the navmesh doesn't reach the agent could take it to the closet's
/// roof. That fits the busiest closets on Defocus filling their roofs (the user: *"small changes can decide where it
/// spawns"*).
/// </summary>
internal static readonly string[] StandIgnores = { "player", "zombie", "corpse", "ragdoll", "trigger" };
/// <summary>The floor under a point, from just above the sill as `Snap` reads it, if it is within 32 of the given level.</summary>
Vector3? FloorAt( Vector3 at, float level )
{
var scene = Scene ?? Game.ActiveScene;
if ( !scene.IsValid() ) return null;
var top = at.WithZ( level + 40f );
var tr = scene.Trace.Ray( top, top + Vector3.Down * 120f ).WithoutTags( StandIgnores ).Run();
if ( !tr.Hit || tr.StartedSolid || MathF.Abs( tr.HitPosition.z - level ) > 32f ) return null;
return tr.HitPosition;
}
/// <summary>
/// `nz_spawn_windows` — every zombie spawner: the window it feeds, and whether its zombies stand there on the navmesh or
/// are held there off it. Read only.
/// </summary>
[ConCmd( "nz_spawn_windows" )]
public static void SpawnWindowsCmd()
{
var spawns = ActiveConfig.Current?.ZombieSpawns;
if ( spawns is null || spawns.Count == 0 ) { Log.Info( "[nz-window] no zombie spawns placed" ); return; }
int at = 0, held = 0, none = 0;
for ( var i = 0; i < spawns.Count; i++ )
{
var p = spawns[i].Position;
var w = WindowFor( p );
if ( w is null )
{
none++;
Log.Info( $"[nz-window] spawn #{i} {p:0}: no window within {SpawnWindowReach:0} on its level — spawns where it stands" );
continue;
}
var stand = w.SpawnSideFor( p, out var onMesh );
if ( stand is not Vector3 s )
{
none++;
Log.Info( $"[nz-window] spawn #{i} {p:0}: window #{w.Index} {w.DistanceToRun( p ):0}u away, but no floor for a stand"
+ " on its side — spawns where it stands" );
continue;
}
at++;
if ( !onMesh ) held++;
Log.Info( $"[nz-window] spawn #{i} {p:0}: window #{w.Index} {w.DistanceToRun( p ):0}u away"
+ ( w.CrossValid ? ", crossing built" : ", NO crossing" )
+ $" — stands at {s:0}" + ( onMesh ? " on the navmesh" : ", HELD there off the navmesh until the boards are down" ) );
}
Log.Info( $"[nz-window] {at} of {spawns.Count} spawners are at a window ({held} of them held off the navmesh),"
+ $" {none} spawn where they stand" );
}
}