Controller that tracks and transitions a game tempo state machine. It stores current TempoPhase, when the phase started, and relax progress, and decides phase changes based on world snapshot, elapsed time, and pressure thresholds from a TempoConfiguration.
namespace SboxDirector;
public readonly record struct TempoState(TempoPhase Phase, double PhaseStarted, float RelaxStartProgress);
public readonly record struct TempoTransition(TempoPhase Previous, TempoPhase Current, string Reason);
public sealed class TempoController
{
readonly TempoConfiguration _configuration;
public TempoPhase Phase { get; private set; } = TempoPhase.Relax;
public double PhaseStarted { get; private set; }
public float RelaxStartProgress { get; private set; }
public TempoController(TempoConfiguration configuration, double startTime = 0, float startProgress = 0)
{ _configuration = configuration; configuration.Validate(); PhaseStarted = startTime; RelaxStartProgress = startProgress; }
public TempoController(TempoConfiguration configuration, TempoState state) : this(configuration, state.PhaseStarted, state.RelaxStartProgress) { Phase = state.Phase; }
public TempoState State => new(Phase, PhaseStarted, RelaxStartProgress);
public void AlignStart(double time, float progress)
{
PhaseStarted = time; if (Phase == TempoPhase.Relax) RelaxStartProgress = progress;
}
public TempoTransition? Update(WorldSnapshot world, float maximumInstantaneousPressure)
{
var elapsed = world.Time - PhaseStarted;
return Phase switch
{
TempoPhase.BuildUp when elapsed >= _configuration.BuildUpMinimumSeconds && maximumInstantaneousPressure > _configuration.PeakThreshold => Transition(TempoPhase.SustainPeak, world, "buildup timer ready and team pressure crossed peak threshold"),
TempoPhase.SustainPeak when elapsed >= _configuration.SustainPeakSeconds => Transition(TempoPhase.PeakFade, world, "sustain timer expired"),
TempoPhase.PeakFade when !world.CombatActive && maximumInstantaneousPressure < _configuration.RelaxThreshold => Transition(TempoPhase.Relax, world, "combat cleared and team pressure fell below relax threshold"),
TempoPhase.Relax when elapsed >= _configuration.RelaxMaximumSeconds => Transition(TempoPhase.BuildUp, world, "maximum relax time expired"),
TempoPhase.Relax when elapsed >= _configuration.RelaxMinimumSeconds && world.TeamProgress - RelaxStartProgress >= _configuration.RelaxMaximumProgressTravel => Transition(TempoPhase.BuildUp, world, "team traveled the configured relax distance"),
_ => null
};
}
TempoTransition Transition(TempoPhase next, WorldSnapshot world, string reason)
{
var previous = Phase; Phase = next; PhaseStarted = world.Time;
if (next == TempoPhase.Relax) RelaxStartProgress = world.TeamProgress;
return new(previous, next, reason);
}
}