diff --git a/Content.Client/Administration/AdminNameOverlay.cs b/Content.Client/Administration/AdminNameOverlay.cs index 205e9757e6..9fb93e926c 100644 --- a/Content.Client/Administration/AdminNameOverlay.cs +++ b/Content.Client/Administration/AdminNameOverlay.cs @@ -2,7 +2,9 @@ using System.Linq; using System.Numerics; using Content.Client.Administration.Systems; using Content.Client.Stylesheets; +using Content.Shared.Administration; using Content.Shared.CCVar; +using Content.Shared.Ghost; using Content.Shared.Mind; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; @@ -26,12 +28,15 @@ internal sealed class AdminNameOverlay : Overlay private bool _overlaySymbols; private bool _overlayPlaytime; private bool _overlayStartingJob; + private float _ghostFadeDistance; + private float _ghostHideDistance; + private int _overlayStackMax; + private float _overlayMergeDistance; //TODO make this adjustable via GUI private readonly ProtoId[] _filter = ["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"]; private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic"); - private readonly Color _antagColorClassic = Color.OrangeRed; public AdminNameOverlay( AdminSystem system, @@ -48,7 +53,7 @@ internal sealed class AdminNameOverlay : Overlay _entityLookup = entityLookup; _userInterfaceManager = userInterfaceManager; ZIndex = 200; - // Setting this to a specific font would break the antag symbols + // Setting these to a specific ttf would break the antag symbols _font = resourceCache.NotoStack(); _fontBold = resourceCache.NotoStack(variation: "Bold"); @@ -56,6 +61,10 @@ internal sealed class AdminNameOverlay : Overlay config.OnValueChanged(CCVars.AdminOverlaySymbols, (show) => { _overlaySymbols = show; }, true); config.OnValueChanged(CCVars.AdminOverlayPlaytime, (show) => { _overlayPlaytime = show; }, true); config.OnValueChanged(CCVars.AdminOverlayStartingJob, (show) => { _overlayStartingJob = show; }, true); + config.OnValueChanged(CCVars.AdminOverlayGhostHideDistance, (f) => { _ghostHideDistance = f; }, true); + config.OnValueChanged(CCVars.AdminOverlayGhostFadeDistance, (f) => { _ghostFadeDistance = f; }, true); + config.OnValueChanged(CCVars.AdminOverlayStackMax, (i) => { _overlayStackMax = i; }, true); + config.OnValueChanged(CCVars.AdminOverlayMergeDistance, (f) => { _overlayMergeDistance = f; }, true); } public override OverlaySpace Space => OverlaySpace.ScreenSpace; @@ -63,58 +72,114 @@ internal sealed class AdminNameOverlay : Overlay protected override void Draw(in OverlayDrawArgs args) { var viewport = args.WorldAABB; + var colorDisconnected = Color.White; + var uiScale = _userInterfaceManager.RootControl.UIScale; + var lineoffset = new Vector2(0f, 14f) * uiScale; + var drawnOverlays = new List<(Vector2,Vector2)>() ; // A saved list of the overlays already drawn - foreach (var playerInfo in _system.PlayerList) + // Get all player positions before drawing overlays, so they can be sorted before iteration + var sortable = new List<(PlayerInfo, Box2, EntityUid, Vector2)>(); + foreach (var info in _system.PlayerList) { - var entity = _entityManager.GetEntity(playerInfo.NetEntity); + var entity = _entityManager.GetEntity(info.NetEntity); - // Otherwise the entity can not exist yet - if (entity == null || !_entityManager.EntityExists(entity)) - { + // If entity does not exist or is on a different map, skip + if (entity == null + || !_entityManager.EntityExists(entity) + || _entityManager.GetComponent(entity.Value).MapID != args.MapId) continue; - } - - // if not on the same map, continue - if (_entityManager.GetComponent(entity.Value).MapID != args.MapId) - { - continue; - } var aabb = _entityLookup.GetWorldAABB(entity.Value); - - // if not on screen, continue + // if not on screen, skip if (!aabb.Intersects(in viewport)) - { continue; - } - var uiScale = _userInterfaceManager.RootControl.UIScale; - var lineoffset = new Vector2(0f, 14f) * uiScale; - var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center + - new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec( - aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f); + // Get on-screen coordinates of player + var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center); + sortable.Add((info, aabb, entity.Value, screenCoordinates)); + } + + // Draw overlays for visible players, starting from the top of the screen + foreach (var info in sortable.OrderBy(s => s.Item4.Y).ToList()) + { + var playerInfo = info.Item1; + var aabb = info.Item2; + var entity = info.Item3; + var screenCoordinatesCenter = info.Item4; + //the center position is kept separately, for simpler position comparison later + var centerOffset = new Vector2(28f, -18f) * uiScale; + var screenCoordinates = screenCoordinatesCenter + centerOffset; + var alpha = 1f; + + //TODO make a smarter system where the starting offset can be modified by the predicted position and size of already-drawn overlays/stacks? var currentOffset = Vector2.Zero; + // Ghosts near the cursor are made transparent/invisible + // TODO would be "cheaper" if playerinfo already contained a ghost bool, this gets called every frame for every onscreen player! + if (_entityManager.HasComponent(entity)) + { + // We want the map positions here, so we don't have to worry about resolution and such shenanigans + var mobPosition = aabb.Center; + var mousePosition = _eyeManager + .ScreenToMap(_userInterfaceManager.MousePositionScaled.Position * uiScale) + .Position; + var dist = Vector2.Distance(mobPosition, mousePosition); + if (dist < _ghostHideDistance) + continue; + + alpha = Math.Clamp((dist - _ghostHideDistance) / (_ghostFadeDistance - _ghostHideDistance), 0f, 1f); + colorDisconnected.A = alpha; + } + + // If the new overlay text block is within merge distance of any previous ones + // merge them into a stack so they don't hide each other + var stack = drawnOverlays.FindAll(x => + Vector2.Distance(_eyeManager.ScreenToMap(x.Item1).Position, aabb.Center) <= _overlayMergeDistance); + if (stack.Count > 0) + { + screenCoordinates = stack.First().Item1 + centerOffset; + // Replacing this overlay's coordinates for the later save with the stack root's coordinates + // so that other overlays don't try to stack to these coordinates + screenCoordinatesCenter = stack.First().Item1; + + var i = 1; + foreach (var s in stack) + { + // additional entries after maximum stack size is reached will be drawn over the last entry + if (i <= _overlayStackMax - 1) + currentOffset = lineoffset + s.Item2 ; + i++; + } + } + // Character name - args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White); + var color = Color.Aquamarine; + color.A = alpha; + args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? color : colorDisconnected); currentOffset += lineoffset; // Username - args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White); + color = Color.Yellow; + color.A = alpha; + args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? color : colorDisconnected); currentOffset += lineoffset; // Playtime if (!string.IsNullOrEmpty(playerInfo.PlaytimeString) && _overlayPlaytime) { - args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.PlaytimeString, uiScale, playerInfo.Connected ? Color.Orange : Color.White); + color = Color.Orange; + color.A = alpha; + args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.PlaytimeString, uiScale, playerInfo.Connected ? color : colorDisconnected); currentOffset += lineoffset; } // Job if (!string.IsNullOrEmpty(playerInfo.StartingJob) && _overlayStartingJob) { - args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? Color.GreenYellow : Color.White); + color = Color.GreenYellow; + color.A = alpha; + args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? color : colorDisconnected); currentOffset += lineoffset; } @@ -127,7 +192,9 @@ internal sealed class AdminNameOverlay : Overlay ("symbol", symbol), ("name", _antagLabelClassic)) : _antagLabelClassic; - args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, _antagColorClassic); + color = Color.OrangeRed; + color.A = alpha; + args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color); currentOffset += lineoffset; } // Role Type @@ -138,11 +205,14 @@ internal sealed class AdminNameOverlay : Overlay var label = _overlaySymbols ? Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", role)) : role; - var color = playerInfo.RoleProto.Color; - + color = playerInfo.RoleProto.Color; + color.A = alpha; args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color); currentOffset += lineoffset; } + + //Save the coordinates and size of the text block, for stack merge check + drawnOverlays.Add((screenCoordinatesCenter, currentOffset)); } } } diff --git a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml index 88bfdbd19e..449c16ad91 100644 --- a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml +++ b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml @@ -3,7 +3,6 @@ xmlns:ui="clr-namespace:Content.Client.Options.UI"> - diff --git a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs index 9bd41e3550..d2e56d80ed 100644 --- a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs @@ -8,6 +8,13 @@ namespace Content.Client.Options.UI.Tabs; [GenerateTypedNameReferences] public sealed partial class AdminOptionsTab : Control { + private const float OverlayMergeMin = 0.05f; + private const float OverlayMergeMax = 0.95f; + private const int OverlayGhostFadeMin = 0; + private const int OverlayGhostFadeMax = 10; + private const int OverlayGhostHideMin = 0; + private const int OverlayGhostHideMax = 5; + public AdminOptionsTab() { RobustXamlLoader.Load(this); @@ -22,6 +29,24 @@ public sealed partial class AdminOptionsTab : Control Control.AddOptionCheckBox(CCVars.AdminOverlayStartingJob, EnableOverlayStartingJobCheckBox); Control.Initialize(); + + Control.AddOptionPercentSlider( + CCVars.AdminOverlayMergeDistance, + OverlayMergeDistanceSlider, + OverlayMergeMin, + OverlayMergeMax); + + Control.AddOptionSlider( + CCVars.AdminOverlayGhostFadeDistance, + OverlayGhostFadeSlider, + OverlayGhostFadeMin, + OverlayGhostFadeMax); + + Control.AddOptionSlider( + CCVars.AdminOverlayGhostHideDistance, + OverlayGhostHideSlider, + OverlayGhostHideMin, + OverlayGhostHideMax); } } diff --git a/Content.Shared/CCVar/CCVars.Interface.cs b/Content.Shared/CCVar/CCVars.Interface.cs index 168d2319bf..85e06def61 100644 --- a/Content.Shared/CCVar/CCVars.Interface.cs +++ b/Content.Shared/CCVar/CCVars.Interface.cs @@ -78,4 +78,29 @@ public sealed partial class CCVars /// public static readonly CVarDef AdminOverlaySymbols = CVarDef.Create("ui.admin_overlay_symbols", true, CVar.CLIENTONLY | CVar.ARCHIVE); + + /// + /// The range (in tiles) around the cursor within which the admin overlays of ghosts start to fade out + /// + public static readonly CVarDef AdminOverlayGhostFadeDistance = + CVarDef.Create("ui.admin_overlay_ghost_fade_distance", 6, CVar.CLIENTONLY | CVar.ARCHIVE); + + /// + /// The range (in tiles) around the cursor within which the admin overlays of ghosts disappear + /// + public static readonly CVarDef AdminOverlayGhostHideDistance = + CVarDef.Create("ui.admin_overlay_ghost_hide_distance", 2, CVar.CLIENTONLY | CVar.ARCHIVE); + + /// + /// The maximum range (in tiles) at which admin overlay entries still merge to form a stack + /// Recommended to keep under 1, otherwise the overlays of people sitting next to each other will stack + /// + public static readonly CVarDef AdminOverlayMergeDistance = + CVarDef.Create("ui.admin_overlay_merge_distance", 0.33f, CVar.CLIENTONLY | CVar.ARCHIVE); + + /// + /// The maximum size that an overlay stack can reach. Additional overlays will be superimposed over the last one. + /// + public static readonly CVarDef AdminOverlayStackMax = + CVarDef.Create("ui.admin_overlay_stack_max", 3, CVar.CLIENTONLY | CVar.ARCHIVE); } diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index 413ae2f695..35ae20ec3c 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -348,3 +348,6 @@ ui-options-enable-classic-overlay = Revert overlay to classic mode ui-options-enable-overlay-symbols = Add antag symbol to text ui-options-enable-overlay-playtime = Show playtime ui-options-enable-overlay-starting-job = Show starting job +ui-options-overlay-merge-distance = Stack merge distance +ui-options-overlay-ghost-fade-distance = Ghost overlay fade range from mouse +ui-options-overlay-ghost-hide-distance = Ghost overlay hide range from mouse