PhysicalOpponentHitChecks.cs
using Sandbox;
namespace PaintballBaddies;
/// <summary>Opt-in projectile integration check, not a human difficulty assessment.</summary>
public sealed class PhysicalOpponentHitChecks : Component
{
[Property] public bool CheckFeedbackReset {get;set;}
private float doneTime;
private bool resetChecked;
[Property] public bool FullRound { get; set; }
private PlayerController player;
private PaintballMarker marker;
private ArenaMatch match;
private float elapsed;
private bool started,done;
private int movingHits;
private bool sawHitFeedback,sawEliminationFeedback;
private readonly Dictionary<ArenaOpponent,(int Hits,float Speed)> samples=new();
protected override void OnStart()
{
player=Scene.GetAllComponents<PlayerController>().First();
marker=player.Components.Get<PaintballMarker>();
match=player.Components.Get<ArenaMatch>();
}
protected override void OnUpdate()
{
if(done)
{
doneTime+=Time.Delta;
if(CheckFeedbackReset && !resetChecked && doneTime>.6f)
{
marker.ResetTraining();resetChecked=true;
Log.Info($"FEEDBACK_RESET {(marker.HitFlash==0 && marker.EliminationFlash==0 ? "PASS":"FAIL")} after accepted physical elimination");
}
return;
}
elapsed+=Time.Delta;
if(!started)
{
if(elapsed<1)return;
match.StartRound();
// Only the unattended player's durability changes. Opponent health,
// navigation, cover, shooting and projectile collisions stay live.
match.PlayerState.HitLimit=1000;
player.UseInputControls=false;player.UseCameraControls=false;
marker.AcceptInput=false;started=true;
return;
}
sawHitFeedback |= marker.HitFlash>0;
sawEliminationFeedback |= marker.EliminationFlash>0;
var allBots=Scene.GetAllComponents<ArenaOpponent>().ToArray();
foreach(var bot in allBots)
{
var hits=bot.Components.Get<PaintballCombatant>().PaintHits;
if(samples.TryGetValue(bot,out var prior) && hits>prior.Hits && prior.Speed>20)
movingHits+=hits-prior.Hits;
samples[bot]=(hits,bot.MovementSpeed);
}
var bots=allBots.Where(b=>b.Components.Get<PaintballCombatant>()?.Eliminated==false).ToArray();
if((FullRound ? !match.InRound : match.OpponentsRemaining<3) || elapsed>(FullRound ? 55 : 35))
{
done=true;
var success=FullRound ? match.Status=="VICTORY" && marker.Hits==9 : match.OpponentsRemaining<3 && marker.Hits>=3;
Log.Info($"PHYSICAL_BOT {(success && movingHits>0 && sawHitFeedback && sawEliminationFeedback ? "PASS":"FAIL")} fullRound={FullRound} status={match.Status} remaining={match.OpponentsRemaining} hits={marker.Hits} shots={marker.Shots} movingTargetHits={movingHits} hitFeedback={sawHitFeedback} eliminationFeedback={sawEliminationFeedback} elapsed={elapsed}");
return;
}
if(marker.Ammo==0)marker.Reload();
foreach(var bot in bots.OrderBy(b=>(b.WorldPosition-player.WorldPosition).Length))
{
var shape=bot.Components.Get<BoxCollider>();
var aim=bot.WorldPosition+Vector3.Up*shape.Center.z;
var ray=Scene.Trace.Ray(marker.Muzzle,aim).IgnoreGameObjectHierarchy(player.GameObject).Run();
if(!ray.Hit || PaintballCombatant.Find(ray.GameObject)!=bot.Components.Get<PaintballCombatant>())continue;
var flight=(aim-marker.Muzzle).Length/2283;
aim+=bot.WorldRotation.Forward*bot.MovementSpeed*flight;
player.EyeAngles=Rotation.LookAt(aim-player.EyePosition).Angles();
marker.FireAt(aim);
break;
}
}
}