Conveyance/PeachCannon.cs
using Sandbox;
/// <summary>
/// Environment Peach Cannon — lobs GIANT Moonlight Peaches in a ballistic arc.
/// Fires GiantPeachPrefab (PeachProjectile with IsGiant=true).
/// Supports Autonomous tracking or PlayerTriggered via TriggerFire().
/// </summary>
[Title( "Peach Cannon" )]
[Category( "Game / Peach" )]
public sealed class PeachCannon : Component
{
public enum CannonMode { Autonomous, PlayerTriggered }
// ── Wiring ────────────────────────────────────────────────────────────────
[Property] public GameObject GiantPeachPrefab { get; set; }
[Property] public GameObject Muzzle { get; set; }
[Property] public GameObject BarrelPivot { get; set; }
// ── Targeting ─────────────────────────────────────────────────────────────
[Property] public string TargetTag { get; set; } = "player";
[Property, Range( 200f, 8000f )] public float DetectionRange { get; set; } = 3000f;
[Property, Range( 10f, 360f )] public float TurnRate { get; set; } = 35f;
// ── Firing ────────────────────────────────────────────────────────────────
[Property, Range( 2f, 30f )] public float FireInterval { get; set; } = 7f;
[Property, Range( 200f, 2000f)] public float PeachSpeed { get; set; } = 650f;
[Property, Range( 50f, 2000f )] public float PeachMass { get; set; } = 500f;
[Property, Range( 0f, 5f )] public float PeachHealth { get; set; } = 0f;
// ── Ballistics ────────────────────────────────────────────────────────────
[Property] public bool UseBallisticAim { get; set; } = true;
[Property, Range( 1, 5 )] public int BallisticIterations { get; set; } = 3;
// ── Mode ──────────────────────────────────────────────────────────────────
[Property] public CannonMode OperationMode { get; set; } = CannonMode.Autonomous;
// ── Runtime ───────────────────────────────────────────────────────────────
private TimeSince _timeSinceFired;
private GameObject _currentTarget;
private bool _pendingTriggeredFire = false;
/// <summary>Call from a pressure plate / lever to fire one shot (PlayerTriggered mode).</summary>
public void TriggerFire() => _pendingTriggeredFire = true;
protected override void OnUpdate()
{
if ( IsProxy ) return;
_currentTarget = FindNearestTarget();
if ( _currentTarget.IsValid() && BarrelPivot.IsValid() )
TrackTarget( _currentTarget );
bool shouldFire = OperationMode switch
{
CannonMode.Autonomous => _timeSinceFired >= FireInterval && _currentTarget.IsValid() && IsAimed(),
CannonMode.PlayerTriggered => _pendingTriggeredFire && _currentTarget.IsValid(),
_ => false
};
if ( shouldFire )
{
FireGiantPeach();
_timeSinceFired = 0;
_pendingTriggeredFire = false;
}
}
// ── Targeting ─────────────────────────────────────────────────────────────
private GameObject FindNearestTarget()
{
GameObject best = null;
float bestDistSq = DetectionRange * DetectionRange;
foreach ( var obj in Scene.GetAllObjects( true ) )
{
if ( !obj.Tags.Has( TargetTag ) ) continue;
float d = (obj.WorldPosition - WorldPosition).LengthSquared;
if ( d < bestDistSq ) { bestDistSq = d; best = obj; }
}
return best;
}
private Vector3 GetAimPoint( GameObject target )
{
var r = target.Components.Get<ModelRenderer>( FindMode.EnabledInSelfAndDescendants );
return r.IsValid() ? r.Bounds.Center : target.WorldPosition;
}
private Vector3 ComputeBallisticAimDirection( Vector3 targetPos )
{
var muzzlePos = Muzzle.IsValid() ? Muzzle.WorldPosition : BarrelPivot.WorldPosition;
float gravity = -Scene.PhysicsWorld.Gravity.z;
if ( gravity <= 0f ) return (targetPos - muzzlePos).Normal;
var aimPoint = targetPos;
for ( int i = 0; i < BallisticIterations; i++ )
{
float dist = (aimPoint - muzzlePos).Length;
float tof = dist / PeachSpeed;
float drop = 0.5f * gravity * tof * tof;
aimPoint = targetPos + Vector3.Up * drop;
}
return (aimPoint - muzzlePos).Normal;
}
private void TrackTarget( GameObject target )
{
var aimDir = UseBallisticAim && Muzzle.IsValid()
? ComputeBallisticAimDirection( GetAimPoint( target ) )
: (GetAimPoint( target ) - BarrelPivot.WorldPosition).Normal;
var desired = Rotation.LookAt( aimDir );
BarrelPivot.WorldRotation = BarrelPivot.WorldRotation.Clamp( desired, TurnRate * Time.Delta );
}
private bool IsAimed()
{
if ( !BarrelPivot.IsValid() || !_currentTarget.IsValid() ) return false;
var aimDir = UseBallisticAim && Muzzle.IsValid()
? ComputeBallisticAimDirection( GetAimPoint( _currentTarget ) )
: (GetAimPoint( _currentTarget ) - BarrelPivot.WorldPosition).Normal;
return Vector3.Dot( BarrelPivot.WorldRotation.Forward, aimDir ) > 0.97f;
}
// ── Fire ──────────────────────────────────────────────────────────────────
private void FireGiantPeach()
{
if ( !GiantPeachPrefab.IsValid() || !Muzzle.IsValid() ) return;
var peach = GiantPeachPrefab.Clone( Muzzle.WorldPosition, Muzzle.WorldRotation );
peach.NetworkSpawn();
var rb = peach.Components.Get<Rigidbody>( FindMode.EnabledInSelfAndDescendants );
if ( rb.IsValid() )
{
rb.MassOverride = PeachMass;
rb.Velocity = Muzzle.WorldRotation.Forward * PeachSpeed;
}
var proj = peach.Components.Get<PeachProjectile>( FindMode.EnabledInSelfAndDescendants );
if ( proj.IsValid() )
{
proj.IsGiant = true;
proj.MaxHealth = PeachHealth;
}
Sound.Play( "sounds/explosion_near", WorldPosition );
}
}