CatStation/Content.Client/Wall/Systems/WallMountVisibilitySystem.cs
B_Kirill c17429daa4
Directional visibility for wall-mounted entities (#43476)
* Directional visibility for wall-mounted entities

* Fix cache

* Revert

* InteractionIgnoreAnchoredInTileComponent + yml

* Main system

* Move it

* Directional arrow

* Add fade

* retry

* Split "Directional arrow"

* fix

* small thing

* Split "InteractionIgnoreAnchoredInTileComponent"

* correct description

* I hate the online editor

* retry

* ...

* Apply suggestions from code review

Co-authored-by: Jessica M <jessica@jessicamaybe.com>

* Additional comment

* cleanup

---------

Co-authored-by: Jessica M <jessica@jessicamaybe.com>
Co-authored-by: AJCM-git <ajcm_mail@disroot.org>
2026-07-19 04:42:51 +00:00

187 lines
5.7 KiB
C#

using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Tag;
using Content.Shared.Wall;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Configuration;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Wall.Systems;
/// <summary>
/// Manages the directional visibility overlay for wall-mounted entities.
/// </summary>
public sealed partial class WallMountVisibilitySystem : EntitySystem
{
[Dependency] private IConfigurationManager _cfg = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private IOverlayManager _overlay = default!;
[Dependency] private SharedMapSystem _map = default!;
[Dependency] private SpriteSystem _sprite = default!;
[Dependency] private TagSystem _tag = default!;
[Dependency] private TransformSystem _xform = default!;
[Dependency] private WallMountTreeSystem _tree = default!;
[Dependency] private EntityQuery<MapGridComponent> _gridQuery = default!;
[Dependency] private EntityQuery<SpriteComponent> _spriteQuery = default!;
/// <summary>
/// Tags that block visibility when present on the same tile.
/// </summary>
private static readonly ProtoId<TagPrototype>[] BlockingTags = ["Wall"];
/// <summary>
/// Caches for whether a tile has any blocking entity.
/// </summary>
private readonly Dictionary<(EntityUid Grid, Vector2i Tile), bool> _tileCache = [];
private WallMountVisibilityOverlay _overlayInstance = default!;
/// <summary>
/// Whether directional visibility is currently enabled.
/// </summary>
internal bool DirectionalVisibilityEnabled = true;
public override void Initialize()
{
base.Initialize();
_overlayInstance = new WallMountVisibilityOverlay(_timing, _map, _sprite, _xform, _tree, this, _gridQuery, _spriteQuery);
Subs.CVar(_cfg, CCVars.WallMountDirectionalVisibility, OnDirectionalVisibilityChanged, true);
}
public override void Shutdown()
{
base.Shutdown();
_overlay.RemoveOverlay(_overlayInstance);
}
private void OnDirectionalVisibilityChanged(bool enabled)
{
DirectionalVisibilityEnabled = enabled;
if (enabled)
_overlay.AddOverlay(_overlayInstance);
else
{
_overlay.RemoveOverlay(_overlayInstance);
SetAllVisible(true);
}
}
/// <summary>
/// Invalidates tile cache when anchor state changes for a blocking entity.
/// </summary>
[SubscribeLocalEvent]
private void OnTagAnchorChanged(Entity<TagComponent> ent, ref AnchorStateChangedEvent args)
{
if (!_tag.HasAnyTag(ent.Comp, BlockingTags))
return;
var xform = args.Transform;
if (xform.GridUid is not { } gridUid)
return;
if (!_gridQuery.TryGetComponent(gridUid, out var grid))
return;
var tile = _map.TileIndicesFor(gridUid, grid, xform.Coordinates);
_tileCache.Remove((gridUid, tile));
}
/// <summary>
/// Makes the entity visible again on component shutdown.
/// </summary>
[SubscribeLocalEvent]
private void OnWallMountShutdown(Entity<WallMountComponent> ent, ref ComponentShutdown args)
{
if (TerminatingOrDeleted(ent))
return;
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
_sprite.SetVisible((ent, sprite), true);
}
/// <summary>
/// Makes the entity visible again if directional visibility is disabled for this mount.
/// </summary>
[SubscribeLocalEvent]
private void OnWallMountAfterHandleState(Entity<WallMountComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (ent.Comp.DirectionalVisibility)
return;
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
_sprite.SetVisible((ent, sprite), true);
}
/// <summary>
/// Removes all cached entries for a grid that is being removed.
/// </summary>
[SubscribeLocalEvent]
private void OnGridRemoval(GridRemovalEvent ev)
{
foreach (var key in _tileCache.Keys.Where(k => k.Grid == ev.EntityUid).ToList())
{
_tileCache.Remove(key);
}
}
/// <summary>
/// Clears tile cache and resets all wall-mount visibility on round restart.
/// </summary>
[SubscribeNetworkEvent]
private void OnRoundRestart(RoundRestartCleanupEvent ev)
{
_tileCache.Clear();
SetAllVisible(true);
}
/// <summary>
/// Forces all wall-mount entities to become visible or hidden.
/// </summary>
internal void SetAllVisible(bool visible)
{
var query = AllEntityQuery<WallMountComponent, SpriteComponent>();
while (query.MoveNext(out var uid, out _, out var sprite))
{
_sprite.SetVisible((uid, sprite), visible);
}
}
/// <summary>
/// Checks whether the tile contains any anchored blocking entity.
/// </summary>
internal bool IsTileBlocked(EntityUid gridUid, Vector2i tile, EntityUid? ignoreUid = null)
{
if (!_gridQuery.TryGetComponent(gridUid, out var grid))
return false;
var key = (gridUid, tile);
if (_tileCache.TryGetValue(key, out var cached))
return cached;
var enumerator = _map.GetAnchoredEntitiesEnumerator(gridUid, grid, tile);
while (enumerator.MoveNext(out var anchored))
{
if (anchored == ignoreUid)
continue;
if (!_tag.HasAnyTag(anchored.Value, BlockingTags))
continue;
return _tileCache[key] = true;
}
return _tileCache[key] = false;
}
}