Interface and abstract base for scripted vehicle maneuvers used by the testing harness. IManeuver defines lifecycle methods Start, Tick, Report and an optional TimingValue. ManeuverBase implements common plateau-detection state used to detect when acceleration has flattened for about 2 seconds and provides ResetPlateau and AccelStalled helper logic.
namespace VehicleProto;
/// <summary>
/// One scripted maneuver (the pilot decomposition). Each concrete
/// maneuver is a self-contained object that owns (1) its per-run phase state, (2) its drive profile
/// (<see cref="Tick"/> stages <see cref="DriveInputs"/> through the context — it never applies
/// forces), and (3) its own telemetry projection (<see cref="Report"/> — both the console log token
/// and the card row for each metric). Adding a maneuver is therefore LOCAL: a new class here, one
/// entry in <see cref="ManeuverRegistry"/>, and a spec file (docs/testing-harness.md §8).
/// </summary>
public interface IManeuver
{
/// <summary>The battery name (matches the spec's "maneuver" field and DriveManeuver key).</summary>
string Name { get; }
/// <summary>Reset this maneuver's per-run state at the start of a run.</summary>
void Start( ManeuverContext ctx );
/// <summary>Drive one tick by staging <see cref="ManeuverContext.Drive"/>. Return true when the
/// maneuver's OWN completion condition is met (else the run ends on maxRunS).</summary>
bool Tick( ManeuverContext ctx, float dt );
/// <summary>Fill the verdict's metric rows from <see cref="VehicleBridge"/> (the maneuver's frozen
/// telemetry fields). This is the SINGLE definition the console run-line and the card both use.</summary>
void Report( RunVerdict verdict );
/// <summary>Live value for the top-center timing widget while a run is in progress; null = this
/// maneuver isn't shown as a timed run (only launch/brake are).</summary>
string TimingValue( ManeuverContext ctx ) => null;
}
/// <summary>Shared base for maneuvers that need the accel-plateau detector (launch, top speed).
/// A car whose acceleration flattens for a ~2 s window is "topped out" — used to end launch/topspeed
/// runs instead of idling at full throttle to maxRunS. State is per-instance and re-armed by
/// <see cref="ResetPlateau"/> each run (the registry caches one instance per maneuver).</summary>
public abstract class ManeuverBase : IManeuver
{
public abstract string Name { get; }
public abstract void Start( ManeuverContext ctx );
public abstract bool Tick( ManeuverContext ctx, float dt );
public abstract void Report( RunVerdict verdict );
public virtual string TimingValue( ManeuverContext ctx ) => null;
bool _plateauMark;
float _plateauTime;
float _plateauSpeed;
protected void ResetPlateau() => _plateauMark = false;
protected bool AccelStalled( float runTime )
{
if ( !_plateauMark )
{
_plateauMark = true;
_plateauTime = runTime;
_plateauSpeed = VehicleBridge.SpeedMs;
return false;
}
if ( runTime - _plateauTime < 2f )
return false;
bool stalled = VehicleBridge.SpeedMs - _plateauSpeed < 0.5f;
_plateauTime = runTime;
_plateauSpeed = VehicleBridge.SpeedMs;
return stalled;
}
}