Conveyance/PatrolMover.cs
using Sandbox;
using System.Collections.Generic;
/// <summary>
/// Moves the GameObject between a sequence of waypoint GameObjects.
/// Designed to sit alongside (not inside) DragonTurret — the mover handles where it is,
/// the turret handles where it's looking and what it's shooting.
/// </summary>
public sealed class PatrolMover : Component
{
/// <summary>
/// Ordered list of waypoint GameObjects to visit. Only the WorldPosition is used —
/// the waypoints can be empty GameObjects placed in the scene for visual reference.
/// Need at least 2 waypoints for anything to happen.
/// </summary>
[Property] public List<GameObject> Waypoints { get; set; } = new();
/// <summary>
/// How fast the mover travels between waypoints, in units/second.
/// </summary>
[Property, Range( 50, 1000 )] public float Speed { get; set; } = 200f;
/// <summary>
/// How close the mover needs to get to a waypoint before switching to the next.
/// Too small and floating-point error means it might never "arrive"; too large and
/// it cuts corners.
/// </summary>
[Property, Range( 1, 100 )] public float ArrivalRadius { get; set; } = 20f;
/// <summary>
/// Patrol behaviour at the end of the waypoint list.
/// PingPong: A→B→A→B (reverses direction at each end).
/// Loop: A→B→C→A→B→C (jumps back to start after last).
/// </summary>
[Property] public PatrolMode Mode { get; set; } = PatrolMode.PingPong;
/// <summary>
/// If true, the GameObject smoothly rotates to face its direction of travel.
/// Disable this if something else is controlling rotation (e.g. a turret aiming at the player —
/// but in our case the turret only rotates the Gun child, not the root, so leaving this on is fine).
/// </summary>
[Property] public bool FaceDirectionOfTravel { get; set; } = true;
/// <summary>
/// Degrees per second the body rotates to face travel direction.
/// </summary>
[Property, Range( 30, 720 )] public float TurnRateDegPerSec { get; set; } = 90f;
public enum PatrolMode
{
PingPong,
Loop,
}
// === Runtime ===
private int currentWaypointIndex = 0;
private int direction = 1; // +1 forward through list, -1 backwards (only used in PingPong)
protected override void OnUpdate()
{
if ( Waypoints == null || Waypoints.Count < 2 )
return;
var target = Waypoints[currentWaypointIndex];
if ( !target.IsValid() )
return;
var toTarget = target.WorldPosition - WorldPosition;
var distance = toTarget.Length;
// Arrived at this waypoint — pick the next one.
if ( distance < ArrivalRadius )
{
AdvanceWaypoint();
return;
}
// Move toward the waypoint at the configured speed, capped at the remaining distance
// so we don't overshoot in a single frame on a slow machine.
var moveDir = toTarget.Normal;
var moveStep = Speed * Time.Delta;
if ( moveStep > distance ) moveStep = distance;
WorldPosition += moveDir * moveStep;
// Optionally rotate the body to face direction of travel.
if ( FaceDirectionOfTravel )
{
var desiredRotation = Rotation.LookAt( moveDir );
var maxStepDeg = TurnRateDegPerSec * Time.Delta;
WorldRotation = WorldRotation.Clamp( desiredRotation, maxStepDeg );
}
}
/// <summary>
/// Choose the next waypoint index according to the patrol mode.
/// </summary>
private void AdvanceWaypoint()
{
switch ( Mode )
{
case PatrolMode.Loop:
currentWaypointIndex = (currentWaypointIndex + 1) % Waypoints.Count;
break;
case PatrolMode.PingPong:
currentWaypointIndex += direction;
// Bounce off either end of the list.
if ( currentWaypointIndex >= Waypoints.Count )
{
currentWaypointIndex = Waypoints.Count - 2;
direction = -1;
}
else if ( currentWaypointIndex < 0 )
{
currentWaypointIndex = 1;
direction = 1;
}
break;
}
}
}