using System.Linq; using System.Numerics; using Content.Client.Message; using RoundEndPlayerInfo = Content.Shared.GameTicking.RoundEndMessageEvent.RoundEndPlayerInfo; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Shared.Utility; using static Robust.Client.UserInterface.Controls.BoxContainer; namespace Content.Client.RoundEnd; /// /// Window displaying round end information including player manifest. /// public sealed partial class RoundEndSummaryWindow : DefaultWindow { [Dependency] private IEntityManager _entityManager = default!; public int RoundId; private readonly RoundEndPlayerInfo[] _playersInfo; private GridContainer _playerGrid = null!; private readonly List _sortButtons = []; private string _searchText = string.Empty; private enum SortField { ICName, Role, PlayerType, OOCName } private SortField _currentSortField = SortField.PlayerType; private bool _sortDescending; public RoundEndSummaryWindow(string gm, string roundEnd, TimeSpan roundTimeSpan, int roundId, RoundEndPlayerInfo[] info) { IoCManager.InjectDependencies(this); _playersInfo = info; MinSize = SetSize = new Vector2(720, 580); Title = Loc.GetString("round-end-summary-window-title"); // The round end window is split into two tabs, one about the round stats // and the other is a list of RoundEndPlayerInfo for each player. // This tab would be a good place for things like: "x many people died.", // "clown slipped the crew x times.", "x shots were fired this round.", etc. // Also, good for serious info. RoundId = roundId; var roundEndTabs = new TabContainer(); roundEndTabs.AddChild(MakeRoundEndSummaryTab(gm, roundEnd, roundTimeSpan, roundId)); roundEndTabs.AddChild(MakePlayerManifestTab()); ContentsContainer.AddChild(roundEndTabs); OpenCenteredRight(); MoveToFront(); } private static BoxContainer MakeRoundEndSummaryTab(string gamemode, string roundEnd, TimeSpan roundDuration, int roundId) { var roundEndSummaryTab = new BoxContainer { Orientation = LayoutOrientation.Vertical, Name = Loc.GetString("round-end-summary-window-round-end-summary-tab-title") }; var roundEndSummaryContainerScrollbox = new ScrollContainer { VerticalExpand = true, Margin = new Thickness(10) }; var roundEndSummaryContainer = new BoxContainer { Orientation = LayoutOrientation.Vertical }; //Gamemode Name var gamemodeLabel = new RichTextLabel(); var gamemodeMessage = new FormattedMessage(); gamemodeMessage.AddMarkupOrThrow(Loc.GetString("round-end-summary-window-round-id-label", ("roundId", roundId))); gamemodeMessage.AddText(" "); gamemodeMessage.AddMarkupOrThrow(Loc.GetString("round-end-summary-window-gamemode-name-label", ("gamemode", gamemode))); gamemodeLabel.SetMessage(gamemodeMessage); roundEndSummaryContainer.AddChild(gamemodeLabel); //Duration var roundTimeLabel = new RichTextLabel(); roundTimeLabel.SetMarkup(Loc.GetString("round-end-summary-window-duration-label", ("hours", roundDuration.Hours), ("minutes", roundDuration.Minutes), ("seconds", roundDuration.Seconds))); roundEndSummaryContainer.AddChild(roundTimeLabel); //Round end text if (!string.IsNullOrEmpty(roundEnd)) { var roundEndLabel = new RichTextLabel(); roundEndLabel.SetMarkup(roundEnd); roundEndSummaryContainer.AddChild(roundEndLabel); } roundEndSummaryContainerScrollbox.AddChild(roundEndSummaryContainer); roundEndSummaryTab.AddChild(roundEndSummaryContainerScrollbox); return roundEndSummaryTab; } private BoxContainer MakePlayerManifestTab() { var playerManifestTab = new BoxContainer { Orientation = LayoutOrientation.Vertical, Name = Loc.GetString("round-end-summary-window-player-manifest-tab-title") }; // Search container var searchContainer = new BoxContainer { Orientation = LayoutOrientation.Horizontal, Margin = new Thickness(10, 10, 10, 5) }; var searchLabel = new Label { Text = "Filter: ", VerticalAlignment = VAlignment.Center, MinSize = new Vector2(40, 1) }; var searchBar = new LineEdit { PlaceHolder = Loc.GetString("round-end-summary-window-player-manifest-tab-search-placeholder"), HorizontalExpand = true, MinSize = new Vector2(200, 1) }; searchBar.OnTextChanged += OnSearchTextChanged; searchContainer.AddChild(searchLabel); searchContainer.AddChild(searchBar); playerManifestTab.AddChild(searchContainer); // Header with sort buttons var headerContainer = new BoxContainer { Orientation = LayoutOrientation.Horizontal, Margin = new Thickness(10, 5, 10, 5) }; var icNameButton = CreateSortButton("round-end-summary-window-player-manifest-tab-sort-character", SortField.ICName); var roleButton = CreateSortButton("round-end-summary-window-player-manifest-tab-sort-role", SortField.Role); var playerTypeButton = CreateSortButton("round-end-summary-window-player-manifest-tab-sort-player-type", SortField.PlayerType); var oocNameButton = CreateSortButton("round-end-summary-window-player-manifest-tab-sort-player", SortField.OOCName); playerTypeButton.SetSortIndicator(true); headerContainer.AddChild(icNameButton); // Add small spacer between buttons headerContainer.AddChild(new Control { MinSize = new Vector2(5, 1), HorizontalExpand = false }); headerContainer.AddChild(roleButton); // Add small spacer between buttons headerContainer.AddChild(new Control { MinSize = new Vector2(5, 1), HorizontalExpand = false }); headerContainer.AddChild(playerTypeButton); // Add small spacer between buttons headerContainer.AddChild(new Control { MinSize = new Vector2(5, 1), HorizontalExpand = false }); headerContainer.AddChild(oocNameButton); playerManifestTab.AddChild(headerContainer); var scrollContainer = new ScrollContainer { VerticalExpand = true, Margin = new Thickness(10, 0, 10, 10), }; _playerGrid = new GridContainer { Columns = 6, // Player Sprite,IC Name,Role,Player Type,OOC Name HorizontalExpand = true, }; RefreshPlayerList(); scrollContainer.AddChild(_playerGrid); playerManifestTab.AddChild(scrollContainer); return playerManifestTab; } private SortButton CreateSortButton(string text, SortField field) { var button = new SortButton(Loc.GetString(text), field); button.OnPressed += _ => SortBy(field); _sortButtons.Add(button); return button; } /// /// Handles sorting by the specified field, toggling direction if the same field is clicked /// private void SortBy(SortField field) { if (_currentSortField == field) { _sortDescending = !_sortDescending; } else { _currentSortField = field; _sortDescending = false; } foreach (var button in _sortButtons) { button.SetSortIndicator(button.Field == _currentSortField, _sortDescending); } RefreshPlayerList(); } /// /// Refreshes the player list grid by clearing it and repopulating with sorted player data /// private void RefreshPlayerList() { _playerGrid.RemoveAllChildren(); var sortedPlayers = GetSortedPlayers(); foreach (var playerInfo in sortedPlayers) { AddPlayerRow(playerInfo); } } /// /// Adds a single player row to the grid with all columns (sprite, IC name, role, player type, OOC name) /// private void AddPlayerRow(RoundEndPlayerInfo playerInfo) { // Player Sprite column if (playerInfo.PlayerNetEntity != null) { _playerGrid.AddChild(new SpriteView(playerInfo.PlayerNetEntity.Value, _entityManager) { OverrideDirection = Direction.South, VerticalAlignment = VAlignment.Center, SetSize = new Vector2(32, 32), }); } else { _playerGrid.AddChild(new Control { SetSize = new Vector2(32, 32), }); } // IC Name column var icNameLabel = new Label { Text = playerInfo.PlayerICName ?? playerInfo.PlayerOOCName, VerticalAlignment = VAlignment.Center, HorizontalExpand = true, ClipText = true }; // Apply color coding for antagonists if (playerInfo.Antag) { icNameLabel.FontColorOverride = Color.Red; } _playerGrid.AddChild(icNameLabel); _playerGrid.AddChild(new Control { SetSize = new Vector2(32, 32), }); // Role column var roleLabel = new Label { Text = playerInfo.Observer ? "-" : Loc.GetString(playerInfo.Role), VerticalAlignment = VAlignment.Center, HorizontalExpand = true, ClipText = true }; _playerGrid.AddChild(roleLabel); // Player Type column var playerTypeLabel = new Label { Text = GetPlayerTypeText(playerInfo), VerticalAlignment = VAlignment.Center, HorizontalExpand = true, ClipText = true }; // Apply color coding based on player type if (playerInfo.Antag) { playerTypeLabel.FontColorOverride = Color.Red; } else if (playerInfo.Observer) { playerTypeLabel.FontColorOverride = Color.Gray; } _playerGrid.AddChild(playerTypeLabel); // OOC Name column var oocNameLabel = new Label { Text = playerInfo.PlayerOOCName, VerticalAlignment = VAlignment.Center, HorizontalExpand = true, ClipText = true }; _playerGrid.AddChild(oocNameLabel); } /// /// Gets the player type text for a player based on their observer and antagonist flags /// private static string GetPlayerTypeText(RoundEndPlayerInfo playerInfo) { if (playerInfo.Observer) return Loc.GetString("round-end-summary-window-player-manifest-tab-sort-player-type-observer"); if (playerInfo.Antag) return Loc.GetString("round-end-summary-window-player-manifest-tab-sort-player-type-antag"); return Loc.GetString("round-end-summary-window-player-manifest-tab-sort-player-type-crew"); } private IEnumerable GetSortedPlayers() { // First filter players based on search text var filteredPlayers = string.IsNullOrEmpty(_searchText) ? _playersInfo : _playersInfo.Where(PlayerMatchesSearch); static string GetIcKey(RoundEndPlayerInfo p) => (p.PlayerICName ?? p.PlayerOOCName).ToLowerInvariant(); static string GetOocKey(RoundEndPlayerInfo p) => p.PlayerOOCName.ToLowerInvariant(); static string GetRoleKey(RoundEndPlayerInfo p) => (p.Observer ? "zzz_observer" : p.Role).ToLowerInvariant(); static int GetPlayerTypeSortKey(RoundEndPlayerInfo p) => p.Antag ? 1 : p.Observer ? 3 : 2; return _currentSortField switch { SortField.ICName => ApplySort(filteredPlayers, GetIcKey, _sortDescending), SortField.OOCName => ApplySort(filteredPlayers, GetOocKey, _sortDescending), SortField.Role => ApplySort(filteredPlayers, GetRoleKey, _sortDescending), SortField.PlayerType => ApplySort(filteredPlayers, GetPlayerTypeSortKey, _sortDescending), _ => filteredPlayers }; } private static IEnumerable ApplySort( IEnumerable players, Func primaryKey, bool descending) { static string SecondaryKey(RoundEndPlayerInfo p) => (p.PlayerICName ?? p.PlayerOOCName).ToLowerInvariant(); return descending ? players.OrderByDescending(primaryKey).ThenByDescending(SecondaryKey) : players.OrderBy(primaryKey).ThenBy(SecondaryKey); } /// /// Gets a sort key for player type to ensure consistent ordering: Antagonist -> Crew -> Observer /// /// /// Checks if a player matches the current search filter /// private bool PlayerMatchesSearch(RoundEndPlayerInfo playerInfo) { if (string.IsNullOrEmpty(_searchText)) return true; // Search in character name (IC name) if (!string.IsNullOrEmpty(playerInfo.PlayerICName) && playerInfo.PlayerICName.Contains(_searchText, StringComparison.OrdinalIgnoreCase)) return true; // Search in player name (OOC name) if (!string.IsNullOrEmpty(playerInfo.PlayerOOCName) && playerInfo.PlayerOOCName.Contains(_searchText, StringComparison.OrdinalIgnoreCase)) return true; // Search in role if (!string.IsNullOrEmpty(playerInfo.Role)) { if (playerInfo.Role.Contains(_searchText, StringComparison.OrdinalIgnoreCase) || Loc.GetString(playerInfo.Role).Contains(_searchText, StringComparison.OrdinalIgnoreCase)) return true; } // Search in player type var playerType = GetPlayerTypeText(playerInfo); if (playerType.Contains(_searchText, StringComparison.OrdinalIgnoreCase)) return true; // Search for "Observer" when they are observers if (playerInfo.Observer && "observer".Contains(_searchText, StringComparison.OrdinalIgnoreCase)) return true; return false; } /// /// Handles search text changes and refreshes the player list /// private void OnSearchTextChanged(LineEdit.LineEditEventArgs args) { _searchText = args.Text; RefreshPlayerList(); } private sealed class SortButton : Button { public SortField Field { get; } private readonly Label _sortIndicator; public SortButton(string text, SortField field) { Field = field; HorizontalExpand = true; var container = new BoxContainer { Orientation = LayoutOrientation.Horizontal, HorizontalExpand = true }; var label = new Label { Text = text, HorizontalExpand = true }; _sortIndicator = new Label { Text = "", HorizontalAlignment = HAlignment.Right, MinSize = new Vector2(15, 1) }; container.AddChild(label); container.AddChild(_sortIndicator); AddChild(container); } public void SetSortIndicator(bool active, bool descending = false) { if (!active) { _sortIndicator.Text = ""; return; } _sortIndicator.Text = descending ? "▼" : "▲"; } } }