A game system and console commands that implement invisible springboard pads which launch the local player straight up when they land on a pad. It reads springboard definitions from ActiveConfig.Current.Springboards each tick, detects whether the local player is standing on a pad, and replaces the player's vertical velocity to a configured strength. Also provides console commands to place, list and clear pads in the editor.
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
namespace NZombies;
/// <summary>
/// SPRINGBOARDS — the map's invisible pads that fling a player straight up (`MapConfig.Springboards`, Q > Placeables >
/// Transportation). Banana Colada's springboard (`Placeable.TickSpringboard`) made part of the map: no uses, no clock, nothing
/// drawn, and its strength and radius set per pad (the user, 2026-10-01: *"its like the banana colada springboard, but
/// invisible and i can set how much strength it has and the radius"*).
///
/// ⛔ EACH MACHINE THROWS ONLY ITS OWN PLAYER (`NZPlayer.Local`). A body is driven by the machine that owns it, so a launch
/// written to somebody else's copy would fight the transform arriving from its owner (`ShoveGuard`'s reason). Every machine
/// has the config, so every machine knows the pads.
///
/// ⚠️ IT THROWS ON LANDING ON THE PAD, NOT ON CROSSING ITS EDGE. The perk's pad fires on entering its volume, so it also caught
/// a player coming back down 72 units above it, and threw them again in mid-air. Here a player has to touch the floor inside
/// the circle, as on a real springboard: walk on, or land on it, and up you go; land on it again and it throws you again.
///
/// ⚠️ A SYSTEM, NOT A MANAGER: there is nothing to build, so there is nothing for `NZGame.ShowConfig` to rebuild. It reads the
/// config every frame, and a pad placed in Creative works at once.
/// </summary>
public sealed class SpringboardSystem : GameObjectSystem<SpringboardSystem>
{
public SpringboardSystem( Scene scene ) : base( scene )
{
Listen( Stage.StartUpdate, 0, Tick, "nz.springboard" );
}
static float? _padHeight;
/// <summary>
/// How far above or below a pad a player's feet may be and still count as on it. 48.
///
/// ⚠️ UNDER A FLOOR'S HEIGHT, so a pad does not throw someone standing on a walkway above it, and over a stair step, so a
/// pad laid on a slope or a lumpy displacement still catches the whole of its circle.
/// </summary>
public static float PadHeight { get => _padHeight ?? 48f; set => _padHeight = value; }
/// <summary>
/// The pad the local player stood on last frame, or -1.
///
/// ⚠️ ONE THROW PER TOUCH. The launch fires when this changes to a pad, so a player the launch could not lift (a low
/// ceiling) is not thrown again every frame; they step off and back on.
/// </summary>
int _onPad = -1;
void Tick()
{
var scene = Scene;
if ( !scene.IsValid() || scene.IsEditor ) return;
var list = ActiveConfig.Current?.Springboards;
if ( list is null || list.Count == 0 ) { _onPad = -1; return; }
var player = NZPlayer.Local;
if ( !player.IsValid() || !player.GameObject.Active || player.IsDown || player.IsOutOfRound ) { _onPad = -1; return; }
// ⚠️ A FROZEN BODY IS LEFT ALONE: noclip keeps the controller but stops the body's motion (`Noclip.Detach`), and a
// launch there would only nudge a flying player 12 units up
var c = player.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
if ( !c.IsValid() || !c.Enabled || !c.IsOnGround || !c.Body.IsValid() || !c.Body.MotionEnabled )
{
_onPad = -1;
return;
}
var pad = PadAt( list, player.WorldPosition );
if ( pad == _onPad ) return;
_onPad = pad;
if ( pad >= 0 && list[pad].Strength > 0f ) Launch( player, c, list[pad], pad );
}
/// <summary>
/// The index of the pad whose circle a point stands in, the nearest middle if several overlap, or -1. A pad of strength 0
/// counts, so the tool can still find it to re-set or remove; it just throws nobody.
/// </summary>
public static int PadAt( List<SpringboardSpot> list, Vector3 at )
{
var best = -1;
var bestD = float.MaxValue;
for ( var i = 0; i < (list?.Count ?? 0); i++ )
{
var s = list[i];
if ( s is null ) continue;
var d = at - s.Position;
var flat = d.WithZ( 0f ).Length;
if ( flat > s.Radius || MathF.Abs( d.z ) > PadHeight || flat >= bestD ) continue;
bestD = flat;
best = i;
}
return best;
}
/// <summary>
/// Throw the local player straight up, the perk's way (`Placeable.Launch`): lifted off the floor first, or the controller
/// cancels the upward speed and re-snaps them to it, then the body's vertical speed replaced, never added to, so every
/// throw from a pad reaches the same height.
/// </summary>
static void Launch( NZPlayer player, PlayerController c, SpringboardSpot s, int index )
{
var body = c.Body;
if ( !body.IsValid() ) return;
player.WorldPosition += Vector3.Up * Placeable.GroundBreak;
body.Velocity = body.Velocity.WithZ( s.Strength );
ShoveGuard.Of( player )?.NoteLaunch();
if ( s.SafeLanding ) ShoveGuard.Of( player )?.SpareNextLanding();
Log.Info( $"[nz-spring] springboard #{index} threw you at {s.Strength:0} u/s"
+ $" (about {ApexHeight( s.Strength, player.Scene ):0} units up)"
+ ( s.SafeLanding ? "" : " — ⚠ fall damage on the landing" ) );
}
/// <summary>
/// How high a throw at this speed goes, in units: v² ÷ 2g, from the scene's own gravity (800 when it has none).
///
/// ⚠️ THE SAME FOR EVERY THROW. The launch replaces the vertical speed rather than adding to it, so a run-up or a jump onto
/// the pad changes how far a player travels, not how high.
/// </summary>
public static float ApexHeight( float speed, Scene scene )
{
var g = MathF.Abs( scene?.PhysicsWorld?.Gravity.z ?? 0f );
if ( g < 1f ) g = 800f;
return speed * speed / ( 2f * g );
}
}
/// <summary>
/// Console access to the springboards, so one can be placed, listed and cleared without clicking (MCP can't click).
/// </summary>
public static class SpringboardCommands
{
static MapEditor Editor
=> Game.ActiveScene?.GetAllComponents<MapEditor>().FirstOrDefault();
/// <summary>
/// `nz_springboard [strength] [radius]` — one at your feet, with the tool panel's numbers, or these. In Creative.
/// </summary>
[ConCmd( "nz_springboard" )]
public static void Place( float strength = -1f, float radius = -1f )
{
var ed = Editor;
var p = NZPlayer.Local;
if ( !ed.IsValid() || !p.IsValid() ) { Log.Warning( "[nz-spring] no editor or player — in Creative only" ); return; }
if ( strength >= 0f ) ed.SpringboardStrength = strength;
if ( radius > 0f ) ed.SpringboardRadius = radius;
ed.AddSpringboardAt( p.WorldPosition, Vector3.Up );
}
/// <summary>`nz_springboard_list` — every pad, with its strength, radius and how high it throws.</summary>
[ConCmd( "nz_springboard_list" )]
public static void List()
{
var list = ActiveConfig.Current?.Springboards;
if ( list is null || list.Count == 0 )
{
Log.Info( "[nz-spring] none placed — Q > Placeables > Transportation > Springboard, or nz_springboard" );
return;
}
for ( var i = 0; i < list.Count; i++ )
{
var s = list[i];
Log.Info( $"[nz-spring] [{i}] {s.Position} {s.Strength:0} u/s"
+ $" (about {SpringboardSystem.ApexHeight( s.Strength, Game.ActiveScene ):0} up) radius {s.Radius:0}"
+ ( s.SafeLanding ? " safe landing" : " ⚠ fall damage" ) );
}
}
/// <summary>`nz_springboard_clear` — remove them all.</summary>
[ConCmd( "nz_springboard_clear" )]
public static void Clear()
{
var list = ActiveConfig.Current?.Springboards;
var n = list?.Count ?? 0;
list?.Clear();
Log.Info( $"[nz-spring] removed {n}" );
}
}