A Razor UI component for a map card shown in the game lobby. It displays a map thumbnail, minimap, title, description, player vote avatars, capacity chip, and navigation controls; it updates textures and computes a hash for UI change detection.
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.UI.Navigation;
@using Machines.Components;
@using Machines.Resources;
@namespace Machines.UI
@inherits Panel
@{
var flow = LobbyFlow.Current;
var displayMap = Map ?? flow?.LocalBrowsedMap;
var localReady = flow?.IsLocalReady ?? false;
var locked = !IsSide && localReady;
var poolCount = flow?.MapsForMode( flow?.SelectedMode ?? GameModeType.Race ).Count ?? 0;
var minimapTex = GetMinimapTexture();
// Host pick gets an outline so everyone sees what will launch.
var isHostPick = displayMap != null && flow != null && displayMap.ResourcePath == flow.SelectedMapIdent;
// Players browsing this map; their avatars sit on the card.
var voters = displayMap != null ? flow?.VotersFor( displayMap.ResourcePath ) : null;
var voteCount = voters?.Count ?? 0;
var totalPlayers = Connection.All.Count;
}
<div class="map-tile @(IsSide ? "is-side" : "is-hero") @(locked ? "locked" : "") @(isHostPick ? "host-pick" : "")" onclick=@OnCardClick>
<div class="photo">
<image @ref="ThumbImage" class="photo-img" />
@if ( displayMap?.Thumbnail == null )
{
<div class="photo-empty">NO IMAGE</div>
}
@if ( !IsSide && displayMap != null )
{
<div class="leaderboard-btn" onclick=@OpenLeaderboard>
<i>leaderboard</i>
</div>
}
@if ( voteCount > 0 )
{
<div class="vote-avatars">
@foreach ( var c in voters )
{
<div class="vote-avatar avatar-wrap @(c.IsHost ? "is-host" : "")">
<image class="avatar" src="avatar:@(c.SteamId)" />
<div class="avatar-fade"></div>
</div>
}
</div>
}
</div>
@if ( !IsSide && minimapTex != null )
{
<div class="minimap-overlay @(_minimapExpanded ? "expanded" : "")" onclick=@(() => _minimapExpanded = !_minimapExpanded)>
<image @ref="MinimapImage" class="minimap-img" />
</div>
}
<div class="caption">
<div class="title-row">
<div class="color-bar"></div>
<div class="title">@(displayMap?.Title ?? "No maps")</div>
</div>
@if ( !IsSide )
{
@if ( !string.IsNullOrEmpty( displayMap?.Description ) )
{
<div class="description">@displayMap.Description</div>
}
<div class="meta-row">
@if ( displayMap != null )
{
<span class="chip chip-capacity">@(displayMap.MinPlayers)@("-")@(displayMap.MaxPlayers) players</span>
}
</div>
}
</div>
@if ( !IsSide && poolCount > 1 )
{
<div class="cycle cycle-left">
<div class="chevron">‹</div>
<InputHint Action="MenuLeft" Dark=@(true) />
</div>
<div class="cycle cycle-right">
<InputHint Action="MenuRight" Dark=@(true) />
<div class="chevron">›</div>
</div>
}
</div>
@code
{
public MapResource Map { get; set; }
public bool IsSide { get; set; }
// Side card click selects that map; hero card is handled by its chevrons.
private void OnCardClick()
{
if ( IsSide )
LobbyFlow.Current?.SelectMap( Map );
}
// Navigate to the leaderboard for this map.
private void OpenLeaderboard()
{
var map = ResolvedMap;
if ( map == null )
return;
this.Navigate( $"/leaderboard?map={System.Uri.EscapeDataString( map.ResourcePath )}" );
}
public Sandbox.UI.Image ThumbImage { get; set; }
private MapResource ResolvedMap => Map ?? LobbyFlow.Current?.LocalBrowsedMap;
public Sandbox.UI.Image MinimapImage { get; set; }
// Track-layout PNG alongside the scene file (<scene>_minimap.png).
private bool _minimapExpanded;
private Texture _minimapTex;
private string _minimapFor;
private Texture GetMinimapTexture()
{
var scenePath = ResolvedMap?.Scene?.ResourcePath;
if ( scenePath != _minimapFor )
{
_minimapFor = scenePath;
_minimapExpanded = false;
_minimapTex = scenePath != null
? Texture.LoadFromFileSystem( scenePath.Replace( ".scene", "_minimap.png" ), FileSystem.Mounted, warnOnMissing: false )
: null;
}
return _minimapTex;
}
public override void Tick()
{
base.Tick();
if ( ThumbImage != null )
ThumbImage.Texture = ResolvedMap?.Thumbnail;
if ( MinimapImage != null )
MinimapImage.Texture = GetMinimapTexture();
}
protected override int BuildHash()
{
var flow = LobbyFlow.Current;
var localReady = flow?.IsLocalReady ?? false;
// Include voters so avatars refresh as players browse.
var voters = ResolvedMap != null ? flow?.VotersFor( ResolvedMap.ResourcePath ) : null;
var voteHash = voters?.Count ?? 0;
if ( voters != null )
{
foreach ( var c in voters )
voteHash = System.HashCode.Combine( voteHash, c.SteamId );
}
return System.HashCode.Combine(
System.HashCode.Combine( Map?.ResourcePath, IsSide, flow?.SelectedMode, flow?.SelectedMapIdent, flow?.State, localReady, Networking.IsHost ),
System.HashCode.Combine( GetMinimapTexture() != null, _minimapExpanded, voteHash, Connection.All.Count ) );
}
}