UI component (Razor) for milestone and event overlays in a 2D game. Displays an animated icon, title, subtitle, description, progress bar, plays sounds, queues multiple messages, pauses/resumes the game timer, and auto-dismisses after a duration.
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox
<root>
@* Milestone messages *@
@if (_active && _milestone != null)
{
<div class="ms-backdrop" onclick=@Dismiss>
<div class="ms-box">
@* Animated icon *@
<div class="ms-icon" style="color:@_milestone.AccentColor;">
@_milestone.Icon
</div>
@* Subtitle — wave number *@
<div class="ms-subtitle" style="color:@_milestone.AccentColor;">
@_milestone.Subtitle
</div>
@* Big title *@
<div class="ms-title">@_milestone.Title</div>
@* Description — what changes *@
<div class="ms-desc">@_milestone.Description</div>
@* Auto-dismiss progress bar *@
<div class="ms-progress-track">
<div class="ms-progress-fill"
style="animation-duration:@_duration.ToString("F1")s;">
</div>
</div>
<div class="ms-hint">TAP TO CONTINUE</div>
</div>
</div>
}
</root>
@code {
public static MilestoneOverlay Instance { get; private set; }
bool _active = false;
WaveMilestone _milestone = null;
float _duration = 4f;
float _shownAt = 0f;
public bool IsActive => _active;
protected override void OnStart() => Instance = this;
protected override void OnUpdate()
{
if (!_active) return;
// Auto-dismiss after duration
if (Time.Now - _shownAt >= _duration)
Dismiss();
}
// Called from ChainReactionGame when wave changes
public void ShowIfMilestone(int wave)
{
var milestone = MilestoneCatalog.GetForWave(wave);
if (milestone == null) return;
Sound.Play("replaybutton_ha");
ShowMilestone(milestone, 4f);
}
void Dismiss()
{
if (!_active) return;
_active = false;
_milestone = null;
if (_queue.Count > 0)
{
// Show next queued milestone
var (next, dur) = _queue.Dequeue();
_milestone = next;
_duration = dur;
_active = true;
_shownAt = Time.Now;
// Timer stays paused
StateHasChanged();
return;
}
// Nothing queued — resume timer
ChainReactionGame.Instance?.ResumeTimer();
StateHasChanged();
}
// Another type of message (i.e : lost a heart -> message card if we have a heart consumable or pet's heart)
public void ShowMessage(string message, int wave, float duration = 2.5f)
{
ShowMilestone(new WaveMilestone {
Icon = "💔",
Title = message,
Subtitle = wave > 0 ? $"Wave {wave}" : "",
Description = "",
AccentColor = "rgba(248,113,113,0.90)"
}, duration);
}
string _customMessage = "";
protected override int BuildHash() =>
System.HashCode.Combine(_active, _milestone?.Title ?? "", _queue.Count);
// ------------------------------------------------------------------- //
// --------------------------- RANDOM EVENTS ------------------------- //
// ------------------------------------------------------------------- //
public void ShowEvent(RandomEventType type, int wave)
{
ShowMilestone(BuildEventMilestone(type, wave), 3.5f);
}
// Queue for multiple overlays
public Queue<(WaveMilestone milestone, float duration)> _queue = new();
void ShowMilestone(WaveMilestone milestone, float duration)
{
if (_active)
{
// Queue it instead of showing immediately
_queue.Enqueue((milestone, duration));
ChainReactionGame.Instance?.PauseTimer();
Log.Info($"Queued: {milestone.Title}");
return;
}
ChainReactionGame.Instance?.PauseTimer();
_milestone = milestone;
_duration = duration;
_active = true;
_shownAt = Time.Now;
Sound.Play("UI_Button");
StateHasChanged();
}
public void ShowBoss(int wave, int hp, int moveCooldown)
{
string moveText = moveCooldown == 1 ? "moves every wave"
: $"moves every {moveCooldown} waves";
ShowMilestone(new WaveMilestone {
Icon = "👑",
Title = "BOSS INCOMING!",
Subtitle = $"Wave {wave}",
Description = $"{hp} HP · {moveText} · Multipliers capped while alive.",
AccentColor = "rgba(251,191,36,0.90)"
}, 4.5f);
// PauseTimer is handled inside ShowMilestone now
Sound.Play("boss_chest_spawn");
}
WaveMilestone BuildEventMilestone(RandomEventType type, int wave) => type switch
{
RandomEventType.BonusWave => new WaveMilestone { Icon = "🎁", Title = "BONUS WAVE!",
Subtitle = $"Wave {wave}", Description = "This wave: 3× score, no lives can be lost.",
AccentColor = "rgba(52,211,153,0.90)" },
RandomEventType.FragmentRain => new WaveMilestone { Icon = "💠", Title = "FRAGMENT RAIN!",
Subtitle = $"Wave {wave}", Description = "+15 fragments added to your total instantly.",
AccentColor = "rgba(96,165,250,0.90)" },
RandomEventType.TimerFreeze => new WaveMilestone { Icon = "❄️", Title = "TIME FREEZE!",
Subtitle = $"Wave {wave}", Description = "Timer is frozen for 8 seconds this wave.",
AccentColor = "rgba(147,197,253,0.90)" },
RandomEventType.DoubleDrop => new WaveMilestone { Icon = "✨", Title = "DOUBLE DROP!",
Subtitle = $"Wave {wave}", Description = "All chests reward double coins this wave.",
AccentColor = "rgba(251,191,36,0.90)" },
RandomEventType.ChestBonanza => new WaveMilestone { Icon = "🎁", Title = "CHEST BONANZA!",
Subtitle = $"Wave {wave}", Description = "3 extra chests spawned this wave!",
AccentColor = "rgba(52,211,153,0.90)" },
RandomEventType.GoldRush => new WaveMilestone { Icon = "💎", Title = "GOLD RUSH!",
Subtitle = $"Wave {wave}", Description = "Only gem chests this wave. All worth 5× score.",
AccentColor = "rgba(251,191,36,0.90)" },
RandomEventType.CursedWave => new WaveMilestone { Icon = "💀", Title = "CURSED WAVE",
Subtitle = $"Wave {wave}", Description = "All chests require one extra hit to destroy.",
AccentColor = "rgba(239,68,68,0.90)" },
RandomEventType.SpeedRound => new WaveMilestone { Icon = "⚡", Title = "SPEED ROUND!",
Subtitle = $"Wave {wave}", Description = "Timer is halved this wave. Move fast!",
AccentColor = "rgba(251,191,36,0.90)" },
RandomEventType.GhostBombs => new WaveMilestone { Icon = "👻", Title = "GHOST BOMBS!",
Subtitle = $"Wave {wave}", Description = "Your bombs fire twice this wave.",
AccentColor = "rgba(167,139,250,0.90)" },
RandomEventType.BossRush => new WaveMilestone { Icon = "👑", Title = "BOSS RUSH!",
Subtitle = $"Wave {wave}", Description = "Three mini-bosses instead of normal chests.",
AccentColor = "rgba(239,68,68,0.90)" },
RandomEventType.MysteryWave => new WaveMilestone { Icon = "❓", Title = "MYSTERY WAVE",
Subtitle = $"Wave {wave}", Description = "Chest types are hidden until you hit them.",
AccentColor = "rgba(139,92,246,0.90)" },
RandomEventType.BombJam => new WaveMilestone { Icon = "🔧", Title = "BOMB JAM",
Subtitle = $"Wave {wave}", Description = "One of your bomb slots is locked this wave.",
AccentColor = "rgba(239,68,68,0.90)" },
RandomEventType.MirrorWave => new WaveMilestone {
Icon = "🪞", Title = "MIRROR WAVE!",
Subtitle = $"Wave {wave}",
Description = "The grid is mirrored horizontally this wave. Left is right, right is left!",
AccentColor = "rgba(147,197,253,0.90)"
},
RandomEventType.Wave100 => new WaveMilestone {
Wave = 100,
Title = "CENTURY",
Description = "Wave 100 — you're in the top 1% of players. Legendary.",
},
RandomEventType.Wave200 => new WaveMilestone {
Wave = 200,
Title = "UNSTOPPABLE",
Description = "Wave 200. Nobody gets here. You're something else.",
},
_ => new WaveMilestone { Icon = "⚡", Title = "SPECIAL WAVE!",
Subtitle = $"Wave {wave}", Description = "Something unexpected is happening...",
AccentColor = "rgba(167,139,250,0.90)" }
};
}