diff --git a/Content.Client/Alerts/ClientAlertsSystem.cs b/Content.Client/Alerts/ClientAlertsSystem.cs index 525ef1f018fc9c..c5ec254c0ccc5e 100644 --- a/Content.Client/Alerts/ClientAlertsSystem.cs +++ b/Content.Client/Alerts/ClientAlertsSystem.cs @@ -2,6 +2,7 @@ using Content.Shared.Alert; using JetBrains.Annotations; using Robust.Client.Player; +using Robust.Shared.GameStates; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -24,8 +25,7 @@ public override void Initialize() SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); - - SubscribeLocalEvent(ClientAlertsHandleState); + SubscribeLocalEvent(OnHandleState); } protected override void LoadPrototypes() { @@ -47,17 +47,22 @@ public IReadOnlyDictionary? ActiveAlerts } } - protected override void AfterShowAlert(Entity alerts) + private void OnHandleState(Entity alerts, ref ComponentHandleState args) { + if (args.Current is not AlertComponentState cast) + return; + + alerts.Comp.Alerts = cast.Alerts; + UpdateHud(alerts); } - protected override void AfterClearAlert(Entity alerts) + protected override void AfterShowAlert(Entity alerts) { UpdateHud(alerts); } - private void ClientAlertsHandleState(Entity alerts, ref AfterAutoHandleStateEvent args) + protected override void AfterClearAlert(Entity alerts) { UpdateHud(alerts); } diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs index f0b7ffbe119909..81c9a409a3b403 100644 --- a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs +++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs @@ -23,6 +23,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow { private readonly IEntityManager _entManager; private readonly SpriteSystem _spriteSystem; + private readonly SharedNavMapSystem _navMapSystem; private EntityUid? _owner; private NetEntity? _trackedEntity; @@ -42,19 +43,32 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow private const float SilencingDuration = 2.5f; + // Colors + private Color _wallColor = new Color(64, 64, 64); + private Color _tileColor = new Color(28, 28, 28); + private Color _monitorBlipColor = Color.Cyan; + private Color _untrackedEntColor = Color.DimGray; + private Color _regionBaseColor = new Color(154, 154, 154); + private Color _inactiveColor = StyleNano.DisabledFore; + private Color _statusTextColor = StyleNano.GoodGreenFore; + private Color _goodColor = Color.LimeGreen; + private Color _warningColor = new Color(255, 182, 72); + private Color _dangerColor = new Color(255, 67, 67); + public AtmosAlertsComputerWindow(AtmosAlertsComputerBoundUserInterface userInterface, EntityUid? owner) { RobustXamlLoader.Load(this); _entManager = IoCManager.Resolve(); _spriteSystem = _entManager.System(); + _navMapSystem = _entManager.System(); // Pass the owner to nav map _owner = owner; NavMap.Owner = _owner; // Set nav map colors - NavMap.WallColor = new Color(64, 64, 64); - NavMap.TileColor = Color.DimGray * NavMap.WallColor; + NavMap.WallColor = _wallColor; + NavMap.TileColor = _tileColor; // Set nav map grid uid var stationName = Loc.GetString("atmos-alerts-window-unknown-location"); @@ -179,6 +193,9 @@ public void UpdateUI(EntityCoordinates? consoleCoords, AtmosAlertsComputerEntry[ // Add tracked entities to the nav map foreach (var device in console.AtmosDevices) { + if (!device.NetEntity.Valid) + continue; + if (!NavMap.Visible) continue; @@ -209,7 +226,7 @@ public void UpdateUI(EntityCoordinates? consoleCoords, AtmosAlertsComputerEntry[ if (consoleCoords != null && consoleUid != null) { var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png"))); - var blip = new NavMapBlip(consoleCoords.Value, texture, Color.Cyan, true, false); + var blip = new NavMapBlip(consoleCoords.Value, texture, _monitorBlipColor, true, false); NavMap.TrackedEntities[consoleUid.Value] = blip; } @@ -258,7 +275,7 @@ public void UpdateUI(EntityCoordinates? consoleCoords, AtmosAlertsComputerEntry[ VerticalAlignment = VAlignment.Center, }; - label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", StyleNano.GoodGreenFore.ToHexNoAlpha()))); + label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", _statusTextColor.ToHexNoAlpha()))); AlertsTable.AddChild(label); } @@ -270,6 +287,34 @@ public void UpdateUI(EntityCoordinates? consoleCoords, AtmosAlertsComputerEntry[ else MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-alerts", ("value", activeAlarmCount))); + // Update sensor regions + NavMap.RegionOverlays.Clear(); + var prioritizedRegionOverlays = new Dictionary(); + + if (_owner != null && + _entManager.TryGetComponent(_owner, out var xform) && + _entManager.TryGetComponent(xform.GridUid, out var navMap)) + { + var regionOverlays = _navMapSystem.GetNavMapRegionOverlays(_owner.Value, navMap, AtmosAlertsComputerUiKey.Key); + + foreach (var (regionOwner, regionOverlay) in regionOverlays) + { + var alarmState = GetAlarmState(regionOwner); + + if (!TryGetSensorRegionColor(regionOwner, alarmState, out var regionColor)) + continue; + + regionOverlay.Color = regionColor; + + var priority = (_trackedEntity == regionOwner) ? 999 : (int)alarmState; + prioritizedRegionOverlays.Add(regionOverlay, priority); + } + + // Sort overlays according to their priority + var sortedOverlays = prioritizedRegionOverlays.OrderBy(x => x.Value).Select(x => x.Key).ToList(); + NavMap.RegionOverlays = sortedOverlays; + } + // Auto-scroll re-enable if (_autoScrollAwaitsUpdate) { @@ -290,7 +335,7 @@ private void AddTrackedEntityToNavMap(AtmosAlertsDeviceNavMapData metaData, Atmo var coords = _entManager.GetCoordinates(metaData.NetCoordinates); if (_trackedEntity != null && _trackedEntity != metaData.NetEntity) - color *= Color.DimGray; + color *= _untrackedEntColor; var selectable = true; var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color, _trackedEntity == metaData.NetEntity, selectable); @@ -298,6 +343,24 @@ private void AddTrackedEntityToNavMap(AtmosAlertsDeviceNavMapData metaData, Atmo NavMap.TrackedEntities[metaData.NetEntity] = blip; } + private bool TryGetSensorRegionColor(NetEntity regionOwner, AtmosAlarmType alarmState, out Color color) + { + color = Color.White; + + var blip = GetBlipTexture(alarmState); + + if (blip == null) + return false; + + // Color the region based on alarm state and entity tracking + color = blip.Value.Item2 * _regionBaseColor; + + if (_trackedEntity != null && _trackedEntity != regionOwner) + color *= _untrackedEntColor; + + return true; + } + private void UpdateUIEntry(AtmosAlertsComputerEntry entry, int index, Control table, AtmosAlertsComputerComponent console, AtmosAlertsFocusDeviceData? focusData = null) { // Make new UI entry if required @@ -534,13 +597,13 @@ private AtmosAlarmType GetAlarmState(NetEntity netEntity) switch (alarmState) { case AtmosAlarmType.Invalid: - output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), StyleNano.DisabledFore); break; + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), _inactiveColor); break; case AtmosAlarmType.Normal: - output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), Color.LimeGreen); break; + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), _goodColor); break; case AtmosAlarmType.Warning: - output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), new Color(255, 182, 72)); break; + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), _warningColor); break; case AtmosAlarmType.Danger: - output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), new Color(255, 67, 67)); break; + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), _dangerColor); break; } return output; diff --git a/Content.Client/Commands/ShowHealthBarsCommand.cs b/Content.Client/Commands/ShowHealthBarsCommand.cs index 0811f9666378d5..6ea9d06c8c3df0 100644 --- a/Content.Client/Commands/ShowHealthBarsCommand.cs +++ b/Content.Client/Commands/ShowHealthBarsCommand.cs @@ -1,6 +1,8 @@ +using Content.Shared.Damage.Prototypes; using Content.Shared.Overlays; using Robust.Client.Player; using Robust.Shared.Console; +using Robust.Shared.Prototypes; using System.Linq; namespace Content.Client.Commands; @@ -34,7 +36,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) { var showHealthBarsComponent = new ShowHealthBarsComponent { - DamageContainers = args.ToList(), + DamageContainers = args.Select(arg => new ProtoId(arg)).ToList(), HealthStatusIcon = null, NetSyncEnabled = false }; diff --git a/Content.Client/Effects/ColorFlashEffectSystem.cs b/Content.Client/Effects/ColorFlashEffectSystem.cs index 956c9465244200..b584aa9ad1bd35 100644 --- a/Content.Client/Effects/ColorFlashEffectSystem.cs +++ b/Content.Client/Effects/ColorFlashEffectSystem.cs @@ -124,6 +124,10 @@ private void OnColorFlashEffect(ColorFlashEffectEvent ev) continue; } + var targetEv = new GetFlashEffectTargetEvent(ent); + RaiseLocalEvent(ent, ref targetEv); + ent = targetEv.Target; + EnsureComp(ent, out comp); comp.NetSyncEnabled = false; comp.Color = sprite.Color; @@ -132,3 +136,9 @@ private void OnColorFlashEffect(ColorFlashEffectEvent ev) } } } + +/// +/// Raised on an entity to change the target for a color flash effect. +/// +[ByRefEvent] +public record struct GetFlashEffectTargetEvent(EntityUid Target); diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml index 19d00a0bbf8b1d..aae8785b1fe108 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml @@ -47,8 +47,7 @@ - + diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs index d61267d002c6b5..fd3615d59f5897 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs @@ -110,18 +110,29 @@ public void Populate(HealthAnalyzerScannedUserMessage msg) // Alerts - AlertsDivider.Visible = msg.Bleeding == true; - AlertsContainer.Visible = msg.Bleeding == true; + var showAlerts = msg.Unrevivable == true || msg.Bleeding == true; - if (msg.Bleeding == true) - { + AlertsDivider.Visible = showAlerts; + AlertsContainer.Visible = showAlerts; + + if (showAlerts) AlertsContainer.DisposeAllChildren(); - AlertsContainer.AddChild(new Label + + if (msg.Unrevivable == true) + AlertsContainer.AddChild(new RichTextLabel + { + Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"), + Margin = new Thickness(0, 4), + MaxWidth = 300 + }); + + if (msg.Bleeding == true) + AlertsContainer.AddChild(new RichTextLabel { Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"), - FontColorOverride = Color.Red, + Margin = new Thickness(0, 4), + MaxWidth = 300 }); - } // Damage Groups diff --git a/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.cs b/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.cs index f8cb04bddea3b5..632ad8de4ac68c 100644 --- a/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.cs +++ b/Content.Client/Info/PlaytimeStats/PlaytimeStatsEntry.cs @@ -1,4 +1,4 @@ -using Content.Shared.Localizations; // SS220 Playtime Format Fix +using Content.Shared.Localizations; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; @@ -17,7 +17,7 @@ public PlaytimeStatsEntry(string role, TimeSpan playtime, StyleBox styleBox) RoleLabel.Text = role; Playtime = playtime; // store the TimeSpan value directly - PlaytimeLabel.Text = ContentLocalizationManager.FormatPlaytime(playtime); // convert to string for display // SS220 Playtime Format Fix + PlaytimeLabel.Text = ContentLocalizationManager.FormatPlaytime(playtime); // convert to string for display BackgroundColorPanel.PanelOverride = styleBox; } diff --git a/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.cs b/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.cs index abfac3bf6cb02f..98241b2ccab38a 100644 --- a/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.cs +++ b/Content.Client/Info/PlaytimeStats/PlaytimeStatsWindow.cs @@ -104,7 +104,7 @@ private void PopulatePlaytimeData() { var overallPlaytime = _jobRequirementsManager.FetchOverallPlaytime(); - OverallPlaytimeLabel.Text = Loc.GetString("ui-playtime-overall", ("time", overallPlaytime)); // SS220 Playtime Format Fix + OverallPlaytimeLabel.Text = Loc.GetString("ui-playtime-overall", ("time", overallPlaytime)); var rolePlaytimes = _jobRequirementsManager.FetchPlaytimeByRoles(); diff --git a/Content.Client/Mapping/MappingScreen.xaml b/Content.Client/Mapping/MappingScreen.xaml index 9cc3e734f0e9ee..bad492e7e41ffa 100644 --- a/Content.Client/Mapping/MappingScreen.xaml +++ b/Content.Client/Mapping/MappingScreen.xaml @@ -8,7 +8,7 @@ VerticalExpand="False" VerticalAlignment="Bottom" HorizontalAlignment="Center"> - @@ -82,5 +82,5 @@ - + diff --git a/Content.Client/Mapping/MappingScreen.xaml.cs b/Content.Client/Mapping/MappingScreen.xaml.cs index 46c0e51fad69b4..20e2528a44003e 100644 --- a/Content.Client/Mapping/MappingScreen.xaml.cs +++ b/Content.Client/Mapping/MappingScreen.xaml.cs @@ -197,7 +197,6 @@ private void RefreshList() public override void SetChatSize(Vector2 size) { - ScreenContainer.DesiredSplitCenter = size.X; ScreenContainer.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize; } diff --git a/Content.Client/Overlays/ShowThirstIconsSystem.cs b/Content.Client/Overlays/ShowThirstIconsSystem.cs index e494ba732137c1..9fc64165c56abb 100644 --- a/Content.Client/Overlays/ShowThirstIconsSystem.cs +++ b/Content.Client/Overlays/ShowThirstIconsSystem.cs @@ -22,6 +22,6 @@ private void OnGetStatusIconsEvent(EntityUid uid, ThirstComponent component, ref return; if (_thirst.TryGetStatusIconPrototype(component, out var iconPrototype)) - ev.StatusIcons.Add(iconPrototype); // SS220 Thirst hud and hunger hud fix + ev.StatusIcons.Add(iconPrototype); } } diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs index c97110b208e58f..d2ac0cdefdc131 100644 --- a/Content.Client/Physics/Controllers/MoverController.cs +++ b/Content.Client/Physics/Controllers/MoverController.cs @@ -1,9 +1,12 @@ +using Content.Shared.Alert; +using Content.Shared.CCVar; using Content.Shared.Movement.Components; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Systems; using Robust.Client.GameObjects; using Robust.Client.Physics; using Robust.Client.Player; +using Robust.Shared.Configuration; using Robust.Shared.Physics.Components; using Robust.Shared.Player; using Robust.Shared.Timing; @@ -14,6 +17,8 @@ public sealed class MoverController : SharedMoverController { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; public override void Initialize() { @@ -135,4 +140,15 @@ protected override bool CanSound() { return _timing is { IsFirstTimePredicted: true, InSimulation: true }; } + + public override void SetSprinting(Entity entity, ushort subTick, bool walking) + { + // Logger.Info($"[{_gameTiming.CurTick}/{subTick}] Sprint: {enabled}"); + base.SetSprinting(entity, subTick, walking); + + if (walking && _cfg.GetCVar(CCVars.ToggleWalk)) + _alerts.ShowAlert(entity, WalkingAlert, showCooldown: false, autoRemove: false); + else + _alerts.ClearAlert(entity, WalkingAlert); + } } diff --git a/Content.Client/Pinpointer/NavMapSystem.Regions.cs b/Content.Client/Pinpointer/NavMapSystem.Regions.cs new file mode 100644 index 00000000000000..4cc775418ecd19 --- /dev/null +++ b/Content.Client/Pinpointer/NavMapSystem.Regions.cs @@ -0,0 +1,303 @@ +using Content.Shared.Atmos; +using Content.Shared.Pinpointer; +using System.Linq; + +namespace Content.Client.Pinpointer; + +public sealed partial class NavMapSystem +{ + private (AtmosDirection, Vector2i, AtmosDirection)[] _regionPropagationTable = + { + (AtmosDirection.East, new Vector2i(1, 0), AtmosDirection.West), + (AtmosDirection.West, new Vector2i(-1, 0), AtmosDirection.East), + (AtmosDirection.North, new Vector2i(0, 1), AtmosDirection.South), + (AtmosDirection.South, new Vector2i(0, -1), AtmosDirection.North), + }; + + public override void Update(float frameTime) + { + // To prevent compute spikes, only one region is flood filled per frame + var query = AllEntityQuery(); + + while (query.MoveNext(out var ent, out var entNavMapRegions)) + FloodFillNextEnqueuedRegion(ent, entNavMapRegions); + } + + private void FloodFillNextEnqueuedRegion(EntityUid uid, NavMapComponent component) + { + if (!component.QueuedRegionsToFlood.Any()) + return; + + var regionOwner = component.QueuedRegionsToFlood.Dequeue(); + + // If the region is no longer valid, flood the next one in the queue + if (!component.RegionProperties.TryGetValue(regionOwner, out var regionProperties) || + !regionProperties.Seeds.Any()) + { + FloodFillNextEnqueuedRegion(uid, component); + return; + } + + // Flood fill the region, using the region seeds as starting points + var (floodedTiles, floodedChunks) = FloodFillRegion(uid, component, regionProperties); + + // Combine the flooded tiles into larger rectangles + var gridCoords = GetMergedRegionTiles(floodedTiles); + + // Create and assign the new region overlay + var regionOverlay = new NavMapRegionOverlay(regionProperties.UiKey, gridCoords) + { + Color = regionProperties.Color + }; + + component.RegionOverlays[regionOwner] = regionOverlay; + + // To reduce unnecessary future flood fills, we will track which chunks have been flooded by a region owner + + // First remove an old assignments + if (component.RegionOwnerToChunkTable.TryGetValue(regionOwner, out var oldChunks)) + { + foreach (var chunk in oldChunks) + { + if (component.ChunkToRegionOwnerTable.TryGetValue(chunk, out var oldOwners)) + { + oldOwners.Remove(regionOwner); + component.ChunkToRegionOwnerTable[chunk] = oldOwners; + } + } + } + + // Now update with the new assignments + component.RegionOwnerToChunkTable[regionOwner] = floodedChunks; + + foreach (var chunk in floodedChunks) + { + if (!component.ChunkToRegionOwnerTable.TryGetValue(chunk, out var owners)) + owners = new(); + + owners.Add(regionOwner); + component.ChunkToRegionOwnerTable[chunk] = owners; + } + } + + private (HashSet, HashSet) FloodFillRegion(EntityUid uid, NavMapComponent component, NavMapRegionProperties regionProperties) + { + if (!regionProperties.Seeds.Any()) + return (new(), new()); + + var visitedChunks = new HashSet(); + var visitedTiles = new HashSet(); + var tilesToVisit = new Stack(); + + foreach (var regionSeed in regionProperties.Seeds) + { + tilesToVisit.Push(regionSeed); + + while (tilesToVisit.Count > 0) + { + // If the max region area is hit, exit + if (visitedTiles.Count > regionProperties.MaxArea) + return (new(), new()); + + // Pop the top tile from the stack + var current = tilesToVisit.Pop(); + + // If the current tile position has already been visited, + // or is too far away from the seed, continue + if ((regionSeed - current).Length > regionProperties.MaxRadius) + continue; + + if (visitedTiles.Contains(current)) + continue; + + // Determine the tile's chunk index + var chunkOrigin = SharedMapSystem.GetChunkIndices(current, ChunkSize); + var relative = SharedMapSystem.GetChunkRelative(current, ChunkSize); + var idx = GetTileIndex(relative); + + // Extract the tile data + if (!component.Chunks.TryGetValue(chunkOrigin, out var chunk)) + continue; + + var flag = chunk.TileData[idx]; + + // If the current tile is entirely occupied, continue + if ((FloorMask & flag) == 0) + continue; + + if ((WallMask & flag) == WallMask) + continue; + + if ((AirlockMask & flag) == AirlockMask) + continue; + + // Otherwise the tile can be added to this region + visitedTiles.Add(current); + visitedChunks.Add(chunkOrigin); + + // Determine if we can propagate the region into its cardinally adjacent neighbors + // To propagate to a neighbor, movement into the neighbors closest edge must not be + // blocked, and vice versa + + foreach (var (direction, tileOffset, reverseDirection) in _regionPropagationTable) + { + if (!RegionCanPropagateInDirection(chunk, current, direction)) + continue; + + var neighbor = current + tileOffset; + var neighborOrigin = SharedMapSystem.GetChunkIndices(neighbor, ChunkSize); + + if (!component.Chunks.TryGetValue(neighborOrigin, out var neighborChunk)) + continue; + + visitedChunks.Add(neighborOrigin); + + if (!RegionCanPropagateInDirection(neighborChunk, neighbor, reverseDirection)) + continue; + + tilesToVisit.Push(neighbor); + } + } + } + + return (visitedTiles, visitedChunks); + } + + private bool RegionCanPropagateInDirection(NavMapChunk chunk, Vector2i tile, AtmosDirection direction) + { + var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize); + var idx = GetTileIndex(relative); + var flag = chunk.TileData[idx]; + + if ((FloorMask & flag) == 0) + return false; + + var directionMask = 1 << (int)direction; + var wallMask = (int)direction << (int)NavMapChunkType.Wall; + var airlockMask = (int)direction << (int)NavMapChunkType.Airlock; + + if ((wallMask & flag) > 0) + return false; + + if ((airlockMask & flag) > 0) + return false; + + return true; + } + + private List<(Vector2i, Vector2i)> GetMergedRegionTiles(HashSet tiles) + { + if (!tiles.Any()) + return new(); + + var x = tiles.Select(t => t.X); + var minX = x.Min(); + var maxX = x.Max(); + + var y = tiles.Select(t => t.Y); + var minY = y.Min(); + var maxY = y.Max(); + + var matrix = new int[maxX - minX + 1, maxY - minY + 1]; + + foreach (var tile in tiles) + { + var a = tile.X - minX; + var b = tile.Y - minY; + + matrix[a, b] = 1; + } + + return GetMergedRegionTiles(matrix, new Vector2i(minX, minY)); + } + + private List<(Vector2i, Vector2i)> GetMergedRegionTiles(int[,] matrix, Vector2i offset) + { + var output = new List<(Vector2i, Vector2i)>(); + + var rows = matrix.GetLength(0); + var cols = matrix.GetLength(1); + + var dp = new int[rows, cols]; + var coords = (new Vector2i(), new Vector2i()); + var maxArea = 0; + + var count = 0; + + while (!IsArrayEmpty(matrix)) + { + count++; + + if (count > rows * cols) + break; + + // Clear old values + dp = new int[rows, cols]; + coords = (new Vector2i(), new Vector2i()); + maxArea = 0; + + // Initialize the first row of dp + for (int j = 0; j < cols; j++) + { + dp[0, j] = matrix[0, j]; + } + + // Calculate dp values for remaining rows + for (int i = 1; i < rows; i++) + { + for (int j = 0; j < cols; j++) + dp[i, j] = matrix[i, j] == 1 ? dp[i - 1, j] + 1 : 0; + } + + // Find the largest rectangular area seeded for each position in the matrix + for (int i = 0; i < rows; i++) + { + for (int j = 0; j < cols; j++) + { + int minWidth = dp[i, j]; + + for (int k = j; k >= 0; k--) + { + if (dp[i, k] <= 0) + break; + + minWidth = Math.Min(minWidth, dp[i, k]); + var currArea = Math.Max(maxArea, minWidth * (j - k + 1)); + + if (currArea > maxArea) + { + maxArea = currArea; + coords = (new Vector2i(i - minWidth + 1, k), new Vector2i(i, j)); + } + } + } + } + + // Save the recorded rectangle vertices + output.Add((coords.Item1 + offset, coords.Item2 + offset)); + + // Removed the tiles covered by the rectangle from matrix + for (int i = coords.Item1.X; i <= coords.Item2.X; i++) + { + for (int j = coords.Item1.Y; j <= coords.Item2.Y; j++) + matrix[i, j] = 0; + } + } + + return output; + } + + private bool IsArrayEmpty(int[,] matrix) + { + for (int i = 0; i < matrix.GetLength(0); i++) + { + for (int j = 0; j < matrix.GetLength(1); j++) + { + if (matrix[i, j] == 1) + return false; + } + } + + return true; + } +} diff --git a/Content.Client/Pinpointer/NavMapSystem.cs b/Content.Client/Pinpointer/NavMapSystem.cs index 9aeb792a429f5e..47469d4ea79a64 100644 --- a/Content.Client/Pinpointer/NavMapSystem.cs +++ b/Content.Client/Pinpointer/NavMapSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Shared.Pinpointer; using Robust.Shared.GameStates; @@ -16,6 +17,7 @@ private void OnHandleState(EntityUid uid, NavMapComponent component, ref Compone { Dictionary modifiedChunks; Dictionary beacons; + Dictionary regions; switch (args.Current) { @@ -23,6 +25,8 @@ private void OnHandleState(EntityUid uid, NavMapComponent component, ref Compone { modifiedChunks = delta.ModifiedChunks; beacons = delta.Beacons; + regions = delta.Regions; + foreach (var index in component.Chunks.Keys) { if (!delta.AllChunks!.Contains(index)) @@ -35,6 +39,8 @@ private void OnHandleState(EntityUid uid, NavMapComponent component, ref Compone { modifiedChunks = state.Chunks; beacons = state.Beacons; + regions = state.Regions; + foreach (var index in component.Chunks.Keys) { if (!state.Chunks.ContainsKey(index)) @@ -47,13 +53,54 @@ private void OnHandleState(EntityUid uid, NavMapComponent component, ref Compone return; } + // Update region data and queue new regions for flooding + var prevRegionOwners = component.RegionProperties.Keys.ToList(); + var validRegionOwners = new List(); + + component.RegionProperties.Clear(); + + foreach (var (regionOwner, regionData) in regions) + { + if (!regionData.Seeds.Any()) + continue; + + component.RegionProperties[regionOwner] = regionData; + validRegionOwners.Add(regionOwner); + + if (component.RegionOverlays.ContainsKey(regionOwner)) + continue; + + if (component.QueuedRegionsToFlood.Contains(regionOwner)) + continue; + + component.QueuedRegionsToFlood.Enqueue(regionOwner); + } + + // Remove stale region owners + var regionOwnersToRemove = prevRegionOwners.Except(validRegionOwners); + + foreach (var regionOwnerRemoved in regionOwnersToRemove) + RemoveNavMapRegion(uid, component, regionOwnerRemoved); + + // Modify chunks foreach (var (origin, chunk) in modifiedChunks) { var newChunk = new NavMapChunk(origin); Array.Copy(chunk, newChunk.TileData, chunk.Length); component.Chunks[origin] = newChunk; + + // If the affected chunk intersects one or more regions, re-flood them + if (!component.ChunkToRegionOwnerTable.TryGetValue(origin, out var affectedOwners)) + continue; + + foreach (var affectedOwner in affectedOwners) + { + if (!component.QueuedRegionsToFlood.Contains(affectedOwner)) + component.QueuedRegionsToFlood.Enqueue(affectedOwner); + } } + // Refresh beacons component.Beacons.Clear(); foreach (var (nuid, beacon) in beacons) { diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs index 413b41c36a6f43..90c2680c4a79f9 100644 --- a/Content.Client/Pinpointer/UI/NavMapControl.cs +++ b/Content.Client/Pinpointer/UI/NavMapControl.cs @@ -48,6 +48,7 @@ public partial class NavMapControl : MapGridControl public List<(Vector2, Vector2)> TileLines = new(); public List<(Vector2, Vector2)> TileRects = new(); public List<(Vector2[], Color)> TilePolygons = new(); + public List RegionOverlays = new(); // Default colors public Color WallColor = new(102, 217, 102); @@ -228,7 +229,7 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args) { if (!blip.Selectable) continue; - + var currentDistance = (_transformSystem.ToMapCoordinates(blip.Coordinates).Position - worldPosition).Length(); if (closestDistance < currentDistance || currentDistance * MinimapScale > MaxSelectableDistance) @@ -319,6 +320,22 @@ protected override void Draw(DrawingHandleScreen handle) } } + // Draw region overlays + if (_grid != null) + { + foreach (var regionOverlay in RegionOverlays) + { + foreach (var gridCoords in regionOverlay.GridCoords) + { + var positionTopLeft = ScalePosition(new Vector2(gridCoords.Item1.X, -gridCoords.Item1.Y) - new Vector2(offset.X, -offset.Y)); + var positionBottomRight = ScalePosition(new Vector2(gridCoords.Item2.X + _grid.TileSize, -gridCoords.Item2.Y - _grid.TileSize) - new Vector2(offset.X, -offset.Y)); + + var box = new UIBox2(positionTopLeft, positionBottomRight); + handle.DrawRect(box, regionOverlay.Color); + } + } + } + // Draw map lines if (TileLines.Any()) { diff --git a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs index 01c526fc32660e..d88d74d685b614 100644 --- a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs +++ b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs @@ -1,11 +1,14 @@ using Content.Client.Lock.Visualizers; using Content.Client.Storage.Visualizers; using Content.Client.Wires; +using Content.Client.Effects; +using Content.Client.Smoking; using Content.Shared.Chemistry.Components; using Content.Shared.Polymorph.Components; using Content.Shared.Polymorph.Systems; using Content.Shared.VendingMachines; using Robust.Client.GameObjects; +using Robust.Shared.Player; namespace Content.Client.Polymorph.Systems; @@ -14,14 +17,20 @@ public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem [Dependency] private readonly SharedAppearanceSystem _appearance = default!; private EntityQuery _appearanceQuery; + private EntityQuery _spriteQuery; public override void Initialize() { base.Initialize(); _appearanceQuery = GetEntityQuery(); + _spriteQuery = GetEntityQuery(); SubscribeLocalEvent(OnHandleState); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnGetFlashEffectTargetEvent); } private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) @@ -29,9 +38,30 @@ private void OnHandleState(Entity ent, ref AfterAuto CopyComp(ent); CopyComp(ent); CopyComp(ent); + CopyComp(ent); // reload appearance to hopefully prevent any invisible layers if (_appearanceQuery.TryComp(ent, out var appearance)) _appearance.QueueUpdate(ent, appearance); } + + private void OnStartup(Entity ent, ref ComponentStartup args) + { + if (!_spriteQuery.TryComp(ent, out var sprite)) + return; + + ent.Comp.WasVisible = sprite.Visible; + sprite.Visible = false; + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + if (_spriteQuery.TryComp(ent, out var sprite)) + sprite.Visible = ent.Comp.WasVisible; + } + + private void OnGetFlashEffectTargetEvent(Entity ent, ref GetFlashEffectTargetEvent args) + { + args.Target = ent.Comp.Disguise; + } } diff --git a/Content.Client/UserInterface/Controls/RecordedSplitContainer.cs b/Content.Client/UserInterface/Controls/RecordedSplitContainer.cs deleted file mode 100644 index fd217bc7e860bf..00000000000000 --- a/Content.Client/UserInterface/Controls/RecordedSplitContainer.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Numerics; -using Robust.Client.UserInterface.Controls; - -namespace Content.Client.UserInterface.Controls; - -/// -/// A split container that performs an action when the split resizing is finished. -/// -public sealed class RecordedSplitContainer : SplitContainer -{ - public double? DesiredSplitCenter; - - protected override Vector2 ArrangeOverride(Vector2 finalSize) - { - if (ResizeMode == SplitResizeMode.RespectChildrenMinSize - && DesiredSplitCenter != null - && !finalSize.Equals(Vector2.Zero)) - { - SplitFraction = (float) DesiredSplitCenter.Value; - - if (!Size.Equals(Vector2.Zero)) - { - DesiredSplitCenter = null; - } - } - - return base.ArrangeOverride(finalSize); - } -} diff --git a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml index 7f1d1bcd5b19a1..653302fae4c1fd 100644 --- a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml +++ b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml @@ -14,7 +14,7 @@ VerticalExpand="False" VerticalAlignment="Bottom" HorizontalAlignment="Center"> - + @@ -26,7 +26,7 @@ - + @@ -36,5 +36,5 @@ - + diff --git a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs index e04d377d321586..2892ca44254d50 100644 --- a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs +++ b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs @@ -40,7 +40,6 @@ private void ResizeActionContainer() public override void SetChatSize(Vector2 size) { - ScreenContainer.DesiredSplitCenter = size.X; ScreenContainer.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize; } } diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleButtonsBox.xaml b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleButtonsBox.xaml new file mode 100644 index 00000000000000..32d611e771723d --- /dev/null +++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleButtonsBox.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEntry.xaml.cs b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleButtonsBox.xaml.cs similarity index 86% rename from Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEntry.xaml.cs rename to Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleButtonsBox.xaml.cs index fc53cc72ae6a8e..7df02434160af2 100644 --- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRolesEntry.xaml.cs +++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleButtonsBox.xaml.cs @@ -10,20 +10,17 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles { [GenerateTypedNameReferences] - public sealed partial class GhostRolesEntry : BoxContainer + public sealed partial class GhostRoleButtonsBox : BoxContainer { private SpriteSystem _spriteSystem; public event Action? OnRoleSelected; public event Action? OnRoleFollow; - public GhostRolesEntry(string name, string description, bool hasAccess, FormattedMessage? reason, IEnumerable roles, SpriteSystem spriteSystem) + public GhostRoleButtonsBox(bool hasAccess, FormattedMessage? reason, IEnumerable roles, SpriteSystem spriteSystem) { RobustXamlLoader.Load(this); _spriteSystem = spriteSystem; - Title.Text = name; - Description.SetMessage(description); - foreach (var role in roles) { var button = new GhostRoleEntryButtons(role); diff --git a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleEntryButtons.xaml b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleEntryButtons.xaml index ffde5d69f764d3..05c52deef1643e 100644 --- a/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleEntryButtons.xaml +++ b/Content.Client/UserInterface/Systems/Ghost/Controls/Roles/GhostRoleEntryButtons.xaml @@ -1,15 +1,15 @@  + Orientation="Horizontal" + HorizontalAlignment="Stretch">