AI/ActionSystem/Actions/Guest/SabotageAction.cs
namespace HC3;
public sealed class SabotageAction : AgentAction
{
[Sync( SyncFlags.FromHost )]
private BasicRide Target { get; set; }
private Guest Guest => Agent as Guest;
public override float Score()
{
if ( !Guest.IsValid() || Guest.Delinquency < 0.85f ) return 0f;
if ( Target.IsValid() ) return 300f;
// Pick random available ride without allocating filtered array
BasicRide candidate = null;
int count = 0;
foreach ( var b in Scene.GetAll<BasicRide>() )
{
if ( !b.IsAvailable() || b.Durability <= 0f || b.IsBroken )
continue;
// Reservoir sampling: each valid item has equal probability
count++;
if ( Game.Random.Next( count ) == 0 )
candidate = b;
}
if ( count == 0 ) return 0f;
Target = candidate;
return Target.IsValid() ? 300f : 0f;
}
public override void StartAction()
{
Controller.ClearNavigation();
}
public override Status TickAction()
{
if ( !Target.IsValid() )
return Status.Failure;
var targetPos = Target.WorldPosition;
if ( !GridManager.IsWalkable( Agent.WorldPosition, targetPos ) )
{
Target = null;
return Status.Failure;
}
if ( !Controller.Navigate( targetPos ) )
return Status.Running;
Guest.CommitCrime( 0.5f );
// TODO: maybe they stand there for a while breaking it with an animation?
// For now, let's just have it fuck the durability up a bit.
Target.Damage( Game.Random.Float( 0.3f, 0.8f ) );
Target = null;
return Status.Success;
}
public override void StopAction()
{
Target = null;
}
public override ActionDisplayInfo? GetDisplay()
{
if ( !Target.IsValid() ) return null;
var dist = Agent.WorldPosition.Distance( Target.WorldPosition );
var progress = 1f - (dist / 1500f).Clamp( 0f, 1f );
return new ActionDisplayInfo(
"visibility_off",
$"Sabotage",
progress
);
}
}