A networked mine item component. It can be activated by a car to place a mine behind the vehicle, aligns to ground, spawns on the network, expires after a lifetime, and triggers a spinout and explosion FX when another car (not the owner during self-immunity) touches it, awarding score to the dropper.
using Machines.Player;
using Machines.UI;
namespace Machines.Items;
/// <summary>
/// Dropped mine that spins out cars; dropper has brief self-immunity.
/// </summary>
public sealed class MineHazard : Component, Component.ITriggerListener, IPickupItem, IMinimapBlip
{
Color IMinimapBlip.BlipColor => new( 1f, 0.4f, 0.15f );
string IMinimapBlip.BlipClass => "item mine";
int IMinimapBlip.BlipPriority => 1;
/// <summary>
/// Distance behind the dropper to place the mine.
/// </summary>
[Property]
public float SpawnBack { get; set; } = 70f;
/// <summary>
/// Height above the drop point for the downward ground trace.
/// </summary>
[Property]
public float TraceUp { get; set; } = 128f;
/// <summary>
/// Height above the surface the mine rests at.
/// </summary>
[Property]
public float SurfaceOffset { get; set; } = 4f;
/// <summary>
/// Self-immunity window after dropping (seconds).
/// </summary>
[Property]
public float SelfImmunity { get; set; } = 1.5f;
/// <summary>
/// Seconds before the mine self-destructs if untouched.
/// </summary>
[Property]
public float Lifetime { get; set; } = 30f;
/// <summary>
/// Seconds the victim spins out for.
/// </summary>
[Property]
public float SpinDuration { get; set; } = 1.5f;
/// <summary>
/// FX spawned when the mine goes off.
/// </summary>
[Property]
public GameObject ExplosionEffect { get; set; }
private Car _owner;
private float _droppedAt;
private float _expireAt;
private bool _triggered;
public bool Activate( Car owner )
{
// Can't deploy mid-air.
if ( !owner.Movement.IsValid() || !owner.Movement.IsGrounded )
return false;
_owner = owner;
_droppedAt = Time.Now;
// Drop behind car and trace down to find the surface normal.
var rot = Rotation.FromYaw( owner.Movement.Yaw );
var dropPoint = owner.WorldPosition - rot.Forward * SpawnBack;
var traceStart = dropPoint + Vector3.Up * TraceUp;
// Exclude players/cars; don't filter by tag since the track isn't reliably tagged "world".
var tr = Scene.Trace
.Ray( traceStart, traceStart + Vector3.Down * (TraceUp * 4f) )
.WithoutTags( "player", "car", "trigger" )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
if ( tr.Hit )
{
// Align to the surface: up = hit normal, forward = car heading projected onto surface.
var forwardOnSurface = rot.Forward - tr.Normal * Vector3.Dot( rot.Forward, tr.Normal );
if ( forwardOnSurface.IsNearlyZero() )
forwardOnSurface = rot.Right;
WorldPosition = tr.HitPosition + tr.Normal * SurfaceOffset;
WorldRotation = Rotation.LookAt( forwardOnSurface.Normal, tr.Normal );
}
else
{
WorldPosition = dropPoint + Vector3.Up * SurfaceOffset;
WorldRotation = rot;
}
GameObject.NetworkSpawn();
return true;
}
private bool IsAuthority => !Networking.IsActive || GameObject.Network.IsOwner;
protected override void OnStart()
{
_expireAt = Time.Now + Lifetime;
}
protected override void OnFixedUpdate()
{
if ( IsAuthority && Time.Now >= _expireAt )
GameObject.Destroy();
}
public void OnTriggerEnter( Collider other )
{
if ( !IsAuthority || _triggered )
return;
var car = other.GameObject.GetComponentInParent<Car>();
if ( !car.IsValid() )
return;
// Self-immunity window so the dropper doesn't immediately trigger it.
if ( car == _owner && Time.Now - _droppedAt < SelfImmunity )
return;
_triggered = true;
car.Spinout?.Spin( SpinDuration, (car.WorldPosition - WorldPosition).WithZ( 0f ) );
// Credit the dropper, but not for tripping their own mine.
if ( car != _owner )
{
_owner?.Score?.RpcAdd( "Mine Hit", 50 );
_owner?.Score?.RpcRecordHit( "mine-hits" );
}
SpawnExplosionFx( WorldPosition );
GameObject.Destroy();
}
public void OnTriggerExit( Collider other ) { }
[Rpc.Broadcast]
private void SpawnExplosionFx( Vector3 point )
{
if ( ExplosionEffect.IsValid() )
{
ExplosionEffect.Clone( new CloneConfig
{
Transform = new Transform( point ),
StartEnabled = true
} );
}
}
}