diff --git a/Content.Client/Wall/Components/WallMountTreeComponent.cs b/Content.Client/Wall/Components/WallMountTreeComponent.cs deleted file mode 100644 index 01fce349c8..0000000000 --- a/Content.Client/Wall/Components/WallMountTreeComponent.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Content.Shared.Wall; -using Robust.Shared.ComponentTrees; -using Robust.Shared.Physics; - -namespace Content.Client.Wall.Components; - -[RegisterComponent] -public sealed partial class WallMountTreeComponent : Component, IComponentTreeComponent -{ - public DynamicTree> Tree { get; set; } = default!; -} diff --git a/Content.Client/Wall/Systems/WallMountTreeSystem.cs b/Content.Client/Wall/Systems/WallMountTreeSystem.cs deleted file mode 100644 index 93398750d7..0000000000 --- a/Content.Client/Wall/Systems/WallMountTreeSystem.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Numerics; -using Content.Client.Wall.Components; -using Content.Shared.Wall; -using Robust.Client.GameObjects; -using Robust.Shared.ComponentTrees; -using Robust.Shared.Physics; - -namespace Content.Client.Wall.Systems; - -public sealed partial class WallMountTreeSystem : ComponentTreeSystem -{ - [Dependency] private SpriteSystem _sprite = default!; - - protected override bool DoFrameUpdate => true; - protected override bool DoTickUpdate => false; - protected override bool Recursive => false; - - protected override Box2 ExtractAabb(in ComponentTreeEntry entry, Vector2 pos, Angle rot) - { - return _sprite.CalculateBounds((entry.Uid, Comp(entry.Uid)), pos, rot, default).CalcBoundingBox(); - } -} diff --git a/Content.Client/Wall/Systems/WallMountVisibilitySystem.cs b/Content.Client/Wall/Systems/WallMountVisibilitySystem.cs deleted file mode 100644 index 39f1c8be78..0000000000 --- a/Content.Client/Wall/Systems/WallMountVisibilitySystem.cs +++ /dev/null @@ -1,187 +0,0 @@ -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; - -/// -/// Manages the directional visibility overlay for wall-mounted entities. -/// -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 _gridQuery = default!; - [Dependency] private EntityQuery _spriteQuery = default!; - - /// - /// Tags that block visibility when present on the same tile. - /// - private static readonly ProtoId[] BlockingTags = ["Wall"]; - - /// - /// Caches for whether a tile has any blocking entity. - /// - private readonly Dictionary<(EntityUid Grid, Vector2i Tile), bool> _tileCache = []; - - private WallMountVisibilityOverlay _overlayInstance = default!; - - /// - /// Whether directional visibility is currently enabled. - /// - 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); - } - } - - /// - /// Invalidates tile cache when anchor state changes for a blocking entity. - /// - [SubscribeLocalEvent] - private void OnTagAnchorChanged(Entity 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)); - } - - /// - /// Makes the entity visible again on component shutdown. - /// - [SubscribeLocalEvent] - private void OnWallMountShutdown(Entity ent, ref ComponentShutdown args) - { - if (TerminatingOrDeleted(ent)) - return; - - if (!TryComp(ent, out var sprite)) - return; - - _sprite.SetVisible((ent, sprite), true); - } - - /// - /// Makes the entity visible again if directional visibility is disabled for this mount. - /// - [SubscribeLocalEvent] - private void OnWallMountAfterHandleState(Entity ent, ref AfterAutoHandleStateEvent args) - { - if (ent.Comp.DirectionalVisibility) - return; - - if (!TryComp(ent, out var sprite)) - return; - - _sprite.SetVisible((ent, sprite), true); - } - - /// - /// Removes all cached entries for a grid that is being removed. - /// - [SubscribeLocalEvent] - private void OnGridRemoval(GridRemovalEvent ev) - { - foreach (var key in _tileCache.Keys.Where(k => k.Grid == ev.EntityUid).ToList()) - { - _tileCache.Remove(key); - } - } - - /// - /// Clears tile cache and resets all wall-mount visibility on round restart. - /// - [SubscribeNetworkEvent] - private void OnRoundRestart(RoundRestartCleanupEvent ev) - { - _tileCache.Clear(); - SetAllVisible(true); - } - - /// - /// Forces all wall-mount entities to become visible or hidden. - /// - internal void SetAllVisible(bool visible) - { - var query = AllEntityQuery(); - while (query.MoveNext(out var uid, out _, out var sprite)) - { - _sprite.SetVisible((uid, sprite), visible); - } - } - - /// - /// Checks whether the tile contains any anchored blocking entity. - /// - 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; - } -} diff --git a/Content.Client/Wall/WallMountFadeState.cs b/Content.Client/Wall/WallMountFadeState.cs deleted file mode 100644 index f8c7b7478c..0000000000 --- a/Content.Client/Wall/WallMountFadeState.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Robust.Client.GameObjects; - -namespace Content.Client.Wall; - -/// -/// Tracks fade progress for a wall-mounted entity. -/// -internal struct WallMountFadeState -{ - public float OriginalAlpha; - public float CurrentAlpha; - public float TargetAlpha; - - /// - /// Sprite alpha after applying the fade multiplier. - /// - public readonly float EffectiveAlpha => OriginalAlpha * CurrentAlpha; - - /// - /// Whether the entity should be rendered as visible at the current fade level. - /// - public readonly bool IsVisible => CurrentAlpha > 0f; - - /// - /// Creates a new fade state snapped immediately to the given target alpha. - /// - public static WallMountFadeState Snapped(float originalAlpha, float targetAlpha) - { - return new() - { - OriginalAlpha = originalAlpha, - CurrentAlpha = targetAlpha, - TargetAlpha = targetAlpha, - }; - } - - /// - /// Moves one step towards . - /// - public void StepTowards(float fadeStep) - { - if (MathHelper.CloseTo(CurrentAlpha, TargetAlpha)) - return; - - var delta = TargetAlpha - CurrentAlpha; - var step = MathF.Sign(delta) * MathF.Min(MathF.Abs(delta), fadeStep); - CurrentAlpha = Math.Clamp(CurrentAlpha + step, 0f, 1f); - } -} - -/// -/// Per-viewport fade state for wall-mounted entities. -/// -internal sealed class ViewportFadeState(SpriteSystem sprite, EntityQuery spriteQuery) : IDisposable -{ - private readonly SpriteSystem _sprite = sprite; - private readonly EntityQuery _spriteQuery = spriteQuery; - - /// - /// Fade states for entities tracked in this viewport. - /// - public readonly Dictionary FadeStates = []; - - /// - /// Entities visible this frame. - /// - public readonly HashSet SeenThisFrame = []; - - /// - /// Whether FOV was enabled during the previous frame. - /// - public bool WasFovEnabled = true; - - public void Dispose() - { - foreach (var (uid, state) in FadeStates) - { - if (!_spriteQuery.TryGetComponent(uid, out var sprite)) - continue; - - _sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(state.OriginalAlpha)); - _sprite.SetVisible((uid, sprite), true); - } - - FadeStates.Clear(); - SeenThisFrame.Clear(); - } -} diff --git a/Content.Client/Wall/WallMountVisibilityOverlay.cs b/Content.Client/Wall/WallMountVisibilityOverlay.cs deleted file mode 100644 index c2c3e1dc7d..0000000000 --- a/Content.Client/Wall/WallMountVisibilityOverlay.cs +++ /dev/null @@ -1,242 +0,0 @@ -using System.Linq; -using System.Numerics; -using Content.Client.Graphics; -using Content.Client.Wall.Systems; -using Content.Shared.Wall; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; -using Robust.Shared.Enums; -using Robust.Shared.Graphics; -using Robust.Shared.Map.Components; -using Robust.Shared.Timing; - -namespace Content.Client.Wall; - -/// -/// Renders wall-mounted entities conditionally based on their facing arc relative to the viewport's eye. -/// -public sealed partial class WallMountVisibilityOverlay( - IGameTiming timing, - SharedMapSystem map, - SpriteSystem sprite, - TransformSystem xform, - WallMountTreeSystem tree, - WallMountVisibilitySystem visibility, - EntityQuery gridQuery, - EntityQuery spriteQuery) : Overlay -{ - private readonly IGameTiming _timing = timing; - private readonly SharedMapSystem _map = map; - private readonly SpriteSystem _sprite = sprite; - private readonly TransformSystem _xform = xform; - private readonly WallMountTreeSystem _tree = tree; - private readonly WallMountVisibilitySystem _visibility = visibility; - - private readonly EntityQuery _gridQuery = gridQuery; - private readonly EntityQuery _spriteQuery = spriteQuery; - - /// - /// Caches instances per viewport. - /// - private readonly OverlayResourceCache _fadeCache = new(); - - /// - /// Original sprite alphas, shared across viewports to avoid capturing values modified by another viewport. - /// - private readonly Dictionary _originalAlphas = []; - - /// - /// Alpha change per second during fade. - /// - private const float FadeSpeed = 9f; - - public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities; - - protected override void Draw(in OverlayDrawArgs args) - { - if (!_visibility.DirectionalVisibilityEnabled) - return; - - if (args.Viewport.Eye is not { } eye) - return; - - var viewportState = _fadeCache.GetForViewport(args.Viewport, _ => new ViewportFadeState(_sprite, _spriteQuery)); - - if (!eye.DrawFov) - { - HandleFovDisabled(args, viewportState); - return; - } - - viewportState.WasFovEnabled = true; - - var fadeStep = FadeSpeed * (float)_timing.FrameTime.TotalSeconds; - var matrix = args.Viewport.GetWorldToLocalMatrix(); - - viewportState.SeenThisFrame.Clear(); - ProcessVisibleEntities(args, eye, matrix, fadeStep, viewportState); - - // Remove entities that left the viewport this frame. - foreach (var uid in viewportState.FadeStates.Keys.Except(viewportState.SeenThisFrame).ToList()) - { - RemoveTrackedEntity(uid, viewportState); - } - - ApplyFadeToVisibleEntities(viewportState); - } - - /// - /// When FOV gets disabled, clears fade state and restores sprites. - /// On subsequent frames, keeps wall-mounts visible and restores alpha modified by other viewports. - /// - private void HandleFovDisabled(in OverlayDrawArgs args, ViewportFadeState viewportState) - { - if (viewportState.WasFovEnabled) - { - ClearViewportFadeState(viewportState); - viewportState.WasFovEnabled = false; - } - - // Restore alpha modified by other viewports. - foreach (var entity in _tree.QueryAabb(args.MapId, args.WorldBounds)) - { - var uid = entity.Uid; - if (!_spriteQuery.TryGetComponent(uid, out var sprite)) - continue; - - if (_originalAlphas.Remove(uid, out var origAlpha)) - _sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(origAlpha)); - - _sprite.SetVisible((uid, sprite), true); - } - } - - /// - /// Updates fade state for all wall-mounted entities in the viewport. - /// - private void ProcessVisibleEntities(in OverlayDrawArgs args, IEye eye, Matrix3x2 matrix, float fadeStep, ViewportFadeState viewportState) - { - foreach (var entity in _tree.QueryAabb(args.MapId, args.WorldBounds)) - { - var (wallmount, xform) = entity; - var uid = entity.Uid; - - if (!_spriteQuery.TryGetComponent(uid, out var sprite)) - continue; - - // Capture original alpha before any viewport modifies it. - if (!_originalAlphas.TryGetValue(uid, out var originalAlpha)) - _originalAlphas[uid] = originalAlpha = sprite.Color.A; - - viewportState.SeenThisFrame.Add(uid); - - var targetAlpha = ComputeTargetAlpha(uid, wallmount, xform, eye, matrix); - UpdateFadeState(uid, originalAlpha, targetAlpha, fadeStep, viewportState); - } - } - - /// - /// Returns 1 if the entity is within its facing arc relative to the eye, 0 otherwise. - /// - private float ComputeTargetAlpha(EntityUid uid, WallMountComponent wallmount, TransformComponent xform, IEye eye, Matrix3x2 matrix) - { - if (!wallmount.DirectionalVisibility || wallmount.Arc >= Math.Tau) - return 1f; - - if (xform.GridUid is not { } gridUid || !_gridQuery.TryGetComponent(gridUid, out var grid)) - return 1f; - - var tile = _map.TileIndicesFor(gridUid, grid, xform.Coordinates); - if (!_visibility.IsTileBlocked(gridUid, tile, uid)) - return 1f; - - var (pos, rot) = _xform.GetWorldPositionRotation(xform); - var facingAngle = rot + eye.Rotation + wallmount.Direction; - - var entityScreenPos = Vector2.Transform(pos, matrix); - var eyeScreenPos = Vector2.Transform(eye.Position.Position, matrix); - var toEntity = entityScreenPos - eyeScreenPos; - var eyeToEntityAngle = (toEntity with { X = -toEntity.X }).ToWorldAngle(); - - var angleDiff = Angle.ShortestDistance(eyeToEntityAngle, facingAngle); - return Math.Abs(angleDiff) < wallmount.Arc / 2 ? 1f : 0f; - } - - /// - /// Applies the current fade state to all entities seen this frame. - /// - private void ApplyFadeToVisibleEntities(ViewportFadeState viewportState) - { - foreach (var uid in viewportState.SeenThisFrame) - { - if (!viewportState.FadeStates.TryGetValue(uid, out var state)) - continue; - - if (!_spriteQuery.TryGetComponent(uid, out var sprite)) - { - RemoveTrackedEntity(uid, viewportState); - continue; - } - - _sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(state.EffectiveAlpha)); - _sprite.SetVisible((uid, sprite), state.IsVisible); - } - } - - /// - /// Updates fade target for a tracked entity. Newly seen entities snap immediately without fading. - /// - private static void UpdateFadeState(EntityUid uid, float originalAlpha, float targetAlpha, float fadeStep, ViewportFadeState viewportState) - { - if (viewportState.FadeStates.TryGetValue(uid, out var state)) - { - state.TargetAlpha = targetAlpha; - state.StepTowards(fadeStep); - viewportState.FadeStates[uid] = state; - return; - } - - viewportState.FadeStates[uid] = WallMountFadeState.Snapped(originalAlpha, targetAlpha); - } - - private void RestoreSprite(EntityUid uid, WallMountFadeState state) - { - if (!_spriteQuery.TryGetComponent(uid, out var sprite)) - return; - - _sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(state.OriginalAlpha)); - _sprite.SetVisible((uid, sprite), true); - } - - /// - /// Removes an entity from fade tracking and restores its sprite. - /// - private void RemoveTrackedEntity(EntityUid uid, ViewportFadeState viewportState) - { - if (viewportState.FadeStates.Remove(uid, out var state)) - RestoreSprite(uid, state); - - _originalAlphas.Remove(uid); - } - - /// - /// Clears all fade tracking for a viewport and restores sprites. - /// - private void ClearViewportFadeState(ViewportFadeState viewportState) - { - foreach (var (uid, state) in viewportState.FadeStates) - { - RestoreSprite(uid, state); - _originalAlphas.Remove(uid); - } - - viewportState.FadeStates.Clear(); - viewportState.SeenThisFrame.Clear(); - } - - protected override void DisposeBehavior() - { - _fadeCache.Dispose(); - base.DisposeBehavior(); - } -} diff --git a/Content.Shared/CCVar/CCVars.Game.cs b/Content.Shared/CCVar/CCVars.Game.cs index 342cf783ce..16fcac7385 100644 --- a/Content.Shared/CCVar/CCVars.Game.cs +++ b/Content.Shared/CCVar/CCVars.Game.cs @@ -435,10 +435,4 @@ public sealed partial class CCVars /// public static readonly CVarDef NewCharacterJobs = CVarDef.Create("game.new_character_jobs", "Passenger", CVar.REPLICATED); - - /// - /// Determines whether wall-mounted entities are hidden when viewed from outside their facing arc. - /// - public static readonly CVarDef WallMountDirectionalVisibility = - CVarDef.Create("game.wallmount_directional_visibility", true, CVar.SERVER | CVar.REPLICATED); } diff --git a/Content.Shared/Wall/WallMountComponent.cs b/Content.Shared/Wall/WallMountComponent.cs index d7d1b04391..765dd5aff2 100644 --- a/Content.Shared/Wall/WallMountComponent.cs +++ b/Content.Shared/Wall/WallMountComponent.cs @@ -1,45 +1,28 @@ -using Robust.Shared.ComponentTrees; using Robust.Shared.GameStates; -using Robust.Shared.Physics; namespace Content.Shared.Wall; /// -/// Marks an entity as wall-mounted. -/// Allows interaction through other anchored entities on the same tile within the . -/// Hides the sprite when viewed from outside that arc. +/// This component enables an entity to ignore some obstructions for interaction checks. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)] -public sealed partial class WallMountComponent : Component, IComponentTreeEntry +/// +/// This will only exempt anchored entities that intersect the wall-mount. Additionally, this exemption will apply +/// in a limited arc, providing basic functionality for directional wall mounts. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class WallMountComponent : Component { /// - /// Range of angles where interaction through other anchored entities on the same tile is allowed. - /// Bigger is more permissive. + /// Range of angles for which the exemption applies. Bigger is more permissive. /// - [DataField, AutoNetworkedField] + [ViewVariables(VVAccess.ReadWrite)] + [DataField("arc"), AutoNetworkedField] public Angle Arc = new(MathF.PI); /// - /// The direction the allowed angle range faces, relative to the entity's rotation. - /// Defaults to south. + /// The direction in which the exemption arc is facing, relative to the entity's rotation. Defaults to south. /// - [DataField, AutoNetworkedField] + [ViewVariables(VVAccess.ReadWrite)] + [DataField("direction"), AutoNetworkedField] public Angle Direction = Angle.Zero; - - /// - /// If true, the sprite is only visible from within the facing . - /// - [DataField, AutoNetworkedField] - public bool DirectionalVisibility = true; - - [ViewVariables] - public EntityUid? TreeUid { get; set; } - - [ViewVariables] - public DynamicTree>? Tree { get; set; } - - [ViewVariables] - public bool AddToTree => Arc < Math.Tau && DirectionalVisibility; - - public bool TreeUpdateQueued { get; set; } }