MissionResultHud.cs
using Sandbox;
using Sandbox.UI;
using Sandbox.UI.Construct;
using SWB.Player;
// The in-mission HUD: a small objective banner up top while the job is
// running, and the big win/lose text once it's over.
//
// (Named for the result screen because that's all it did originally - the
// class name is wired into the scene by type, so renaming it would break
// that. It's the mission HUD now.)
//
// Add this component anywhere in the scene and assign Timer. It creates
// its own ScreenPanel (PanelComponent needs one on the same GameObject to
// render), so no other editor wiring is required.
public sealed class MissionResultHud : PanelComponent
{
[Property]
public MissionTimer Timer { get; set; }
Label _objectiveLabel;
Panel _resultWrapper;
Label _resultLabel;
protected override void OnStart()
{
Components.GetOrCreate<ScreenPanel>();
Panel.StyleSheet.Load( "/MissionResultHud.cs.scss" );
_objectiveLabel = Panel.Add.Label( "", "objective" );
// The result text needs a wrapper to centre against. Flex centring
// set directly on a Label positions that Label's children, and its
// text isn't a child panel - so the label has to be a child of
// something that does the centring.
_resultWrapper = Panel.Add.Panel( "resultWrapper" );
_resultLabel = _resultWrapper.Add.Label( "", "result" );
}
protected override void OnUpdate()
{
if ( Timer is null )
return;
UpdateObjective();
UpdateResult();
}
// Sits near the top of the screen so it doesn't cover the fight.
// Counts down while holding out, then switches to the extraction call
// once the exit opens.
void UpdateObjective()
{
var show = Timer.MissionInProgress;
_objectiveLabel.SetClass( "show", show );
if ( !show )
return;
var state = LocalState();
var escaped = state?.Current == MissionPlayerState.State.Escaped
|| (Timer.EscapeZone?.LocalPlayerEscaped ?? false);
var eliminated = state?.Current == MissionPlayerState.State.Eliminated;
if ( eliminated )
{
// Without this the view silently jumping into someone else's
// head just reads as a bug.
var watching = state.SpectateTarget?.GameObject?.Name;
_objectiveLabel.Text = string.IsNullOrWhiteSpace( watching )
? "ELIMINATED"
: $"ELIMINATED — SPECTATING {watching}";
}
else if ( escaped )
{
// You're out. Confirms the extraction actually paid, and gives
// you something to look at while waiting on teammates who
// haven't made it yet.
var reward = Timer.EscapeZone?.LastRewardPaid ?? 0;
_objectiveLabel.Text = $"EXTRACTED — +${reward}";
}
else if ( Timer.EscapeAvailable )
{
_objectiveLabel.Text = "ESCAPE ZONE OPEN — GET OUT";
}
else if ( Timer.RobberyStalled )
{
// Without this the frozen countdown just looks broken - the
// player has no way to know leaving is what stopped it.
_objectiveLabel.Text = "ROBBERY STALLED — GET BACK INSIDE";
}
else
{
// Whole seconds only; a ticking decimal is noise at a glance.
var remaining = Timer.TimeUntilEscape.CeilToInt();
_objectiveLabel.Text = $"HOLD THE STORE — {remaining}s";
}
// Deliberately blue rather than red: red already means MISSION
// FAILED on this same HUD, so reusing it for "the exit is open"
// would read as bad news at a glance. Green once you're actually
// out and paid.
var stalled = Timer.RobberyStalled && !escaped && !eliminated;
_objectiveLabel.SetClass( "escape", Timer.EscapeAvailable && !escaped && !eliminated );
_objectiveLabel.SetClass( "extracted", escaped && !eliminated );
_objectiveLabel.SetClass( "eliminated", eliminated );
_objectiveLabel.SetClass( "stalled", stalled );
}
static MissionPlayerState LocalState()
{
return PlayerBase.Local?.Components.Get<MissionPlayerState>();
}
void UpdateResult()
{
_resultWrapper.SetClass( "show", Timer.MissionEnded );
if ( !Timer.MissionEnded )
return;
_resultLabel.Text = Timer.Won ? "YOU WON" : "MISSION FAILED";
_resultLabel.SetClass( "won", Timer.Won );
_resultLabel.SetClass( "lost", !Timer.Won );
}
}