From 6f4bb040e407a19d549fab9329cdb42ed410a067 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Sat, 15 Oct 2022 17:15:25 +1300 Subject: [PATCH] Misc stealth and box changes (#11809) * git mv * Disable shader while box is open * Hide entity menu / prevent examine * fix recursion fix recursion fix recursion fix recursion * Better visibility checks * min and max visibility fields * fix reference point --- .../ContextMenu/UI/EntityMenuPresenter.cs | 21 ++++++- Content.Client/Examine/ExamineSystem.cs | 18 ++++-- Content.Client/Stealth/StealthSystem.cs | 57 +++++++++++++------ Content.Client/Verbs/VerbSystem.cs | 24 ++++++-- .../CardboardBox/CardboardBoxSystem.cs | 19 ++++++- ...xComponent.cs => CardboardBoxComponent.cs} | 0 Content.Shared/Examine/ExamineSystemShared.cs | 33 +++++++++-- Content.Shared/Examine/ExaminerComponent.cs | 12 ++-- .../Stealth/Components/StealthComponent.cs | 41 +++++++++++-- Content.Shared/Stealth/SharedStealthSystem.cs | 45 +++++++++++++-- .../Entities/Mobs/Player/observer.yml | 2 +- .../Structures/Storage/Closets/big_boxes.yml | 2 + 12 files changed, 222 insertions(+), 52 deletions(-) rename Content.Shared/CardboardBox/Components/{SharedCardboardBoxComponent.cs => CardboardBoxComponent.cs} (100%) diff --git a/Content.Client/ContextMenu/UI/EntityMenuPresenter.cs b/Content.Client/ContextMenu/UI/EntityMenuPresenter.cs index 33da153343..3d9577d397 100644 --- a/Content.Client/ContextMenu/UI/EntityMenuPresenter.cs +++ b/Content.Client/ContextMenu/UI/EntityMenuPresenter.cs @@ -7,6 +7,7 @@ using Content.Client.Verbs; using Content.Client.Viewport; using Content.Shared.CCVar; using Content.Shared.CombatMode; +using Content.Shared.Examine; using Content.Shared.Input; using Robust.Client.GameObjects; using Robust.Client.Graphics; @@ -20,6 +21,7 @@ using Robust.Shared.Input; using Robust.Shared.Input.Binding; using Robust.Shared.IoC; using Robust.Shared.Log; +using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Timing; @@ -47,6 +49,7 @@ namespace Content.Client.ContextMenu.UI private readonly VerbSystem _verbSystem; private readonly ExamineSystem _examineSystem; + private readonly TransformSystem _xform; private readonly SharedCombatModeSystem _combatMode; /// @@ -64,6 +67,7 @@ namespace Content.Client.ContextMenu.UI _verbSystem = verbSystem; _examineSystem = _entityManager.EntitySysManager.GetEntitySystem(); _combatMode = _entityManager.EntitySysManager.GetEntitySystem(); + _xform = _entityManager.EntitySysManager.GetEntitySystem(); _cfg.OnValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged, true); @@ -191,9 +195,24 @@ namespace Content.Client.ContextMenu.UI var ignoreFov = !_eyeManager.CurrentEye.DrawFov || (_verbSystem.Visibility & MenuVisibility.NoFov) == MenuVisibility.NoFov; + _entityManager.TryGetComponent(player, out ExaminerComponent? examiner); + var xformQuery = _entityManager.GetEntityQuery(); + foreach (var entity in Elements.Keys.ToList()) { - if (_entityManager.Deleted(entity) || !ignoreFov && !_examineSystem.CanExamine(player, entity)) + if (!xformQuery.TryGetComponent(entity, out var xform)) + { + // entity was deleted + RemoveEntity(entity); + continue; + } + + if (ignoreFov) + continue; + + var pos = new MapCoordinates(_xform.GetWorldPosition(xform, xformQuery), xform.MapID); + + if (!_examineSystem.CanExamine(player, pos, e => e == player || e == entity, entity, examiner)) RemoveEntity(entity); } } diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs index 6164df123e..a87dff2019 100644 --- a/Content.Client/Examine/ExamineSystem.cs +++ b/Content.Client/Examine/ExamineSystem.cs @@ -63,13 +63,23 @@ namespace Content.Client.Examine base.Shutdown(); } - public override bool CanExamine(EntityUid examiner, MapCoordinates target, Ignored? predicate = null) + public override bool CanExamine(EntityUid examiner, MapCoordinates target, Ignored? predicate = null, EntityUid? examined = null, ExaminerComponent? examinerComp = null) { - var b = _eyeManager.GetWorldViewbounds(); - if (!b.Contains(target.Position)) + if (!Resolve(examiner, ref examinerComp, false)) return false; - return base.CanExamine(examiner, target, predicate); + if (examinerComp.SkipChecks) + return true; + + if (examinerComp.CheckInRangeUnOccluded) + { + // TODO fix this. This should be using the examiner's eye component, not eye manager. + var b = _eyeManager.GetWorldViewbounds(); + if (!b.Contains(target.Position)) + return false; + } + + return base.CanExamine(examiner, target, predicate, examined, examinerComp); } private bool HandleExamine(in PointerInputCmdHandler.PointerInputCmdArgs args) diff --git a/Content.Client/Stealth/StealthSystem.cs b/Content.Client/Stealth/StealthSystem.cs index 0e116f07a0..6ceff19576 100644 --- a/Content.Client/Stealth/StealthSystem.cs +++ b/Content.Client/Stealth/StealthSystem.cs @@ -22,35 +22,48 @@ public sealed class StealthSystem : SharedStealthSystem SubscribeLocalEvent(OnShaderRender); } - protected override void OnInit(EntityUid uid, StealthComponent component, ComponentInit args) + public override void SetEnabled(EntityUid uid, bool value, StealthComponent? component = null) { - base.OnInit(uid, component, args); - if (!TryComp(uid, out SpriteComponent? sprite)) + if (!Resolve(uid, ref component) || component.Enabled == value) return; - sprite.PostShader = _shader; - sprite.GetScreenTexture = true; - sprite.RaiseShaderEvent = true; + base.SetEnabled(uid, value, component); + SetShader(uid, value, component); + } + + private void SetShader(EntityUid uid, bool enabled, StealthComponent? component = null, SpriteComponent? sprite = null) + { + if (!Resolve(uid, ref component, ref sprite, false)) + return; + + sprite.Color = Color.White; + sprite.PostShader = enabled ? _shader : null; + sprite.GetScreenTexture = enabled; + sprite.RaiseShaderEvent = enabled; + + if (!enabled) + { + if (component.HadOutline) + AddComp(uid); + return; + } if (TryComp(uid, out InteractionOutlineComponent? outline)) { - RemComp(uid, outline); + RemCompDeferred(uid, outline); component.HadOutline = true; } } + protected override void OnInit(EntityUid uid, StealthComponent component, ComponentInit args) + { + base.OnInit(uid, component, args); + SetShader(uid, component.Enabled, component); + } + private void OnRemove(EntityUid uid, StealthComponent component, ComponentRemove args) { - if (!TryComp(uid, out SpriteComponent? sprite)) - return; - - sprite.PostShader = null; - sprite.GetScreenTexture = false; - sprite.RaiseShaderEvent = false; - sprite.Color = Color.White; - - if (component.HadOutline) - AddComp(uid); + SetShader(uid, false, component); } private void OnShaderRender(EntityUid uid, StealthComponent component, BeforePostShaderRenderEvent args) @@ -61,9 +74,17 @@ public sealed class StealthSystem : SharedStealthSystem // So we need to use relative screen coordinates. The reference frame we use is the parent's position on screen. // this ensures that if the Stealth is not moving relative to the parent, its relative screen position remains // unchanged. - var parentXform = Transform(Transform(uid).ParentUid); + var parent = Transform(uid).ParentUid; + if (!parent.IsValid()) + return; // should never happen, but lets not kill the client. + var parentXform = Transform(parent); var reference = args.Viewport.WorldToLocal(parentXform.WorldPosition); + reference.X = -reference.X; var visibility = GetVisibility(uid, component); + + // actual visual visibility effect is limited to +/- 1. + visibility = Math.Clamp(visibility, -1f, 1f); + _shader.SetParameter("reference", reference); _shader.SetParameter("visibility", visibility); diff --git a/Content.Client/Verbs/VerbSystem.cs b/Content.Client/Verbs/VerbSystem.cs index a1b9e3b277..2a7ccf4862 100644 --- a/Content.Client/Verbs/VerbSystem.cs +++ b/Content.Client/Verbs/VerbSystem.cs @@ -108,18 +108,34 @@ namespace Content.Client.Verbs ? Visibility : Visibility | MenuVisibility.NoFov; + + // Get entities + List entities; + // Do we have to do FoV checks? if ((visibility & MenuVisibility.NoFov) == 0) { var entitiesUnderMouse = gameScreenBase.GetEntitiesUnderPosition(targetPos); bool Predicate(EntityUid e) => e == player || entitiesUnderMouse.Contains(e); + + // first check the general location. if (!_examineSystem.CanExamine(player.Value, targetPos, Predicate)) return false; - } - // Get entities - var entities = _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize) - .ToList(); + TryComp(player.Value, out ExaminerComponent? examiner); + + // Then check every entity + entities = new(); + foreach (var ent in _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize)) + { + if (_examineSystem.CanExamine(player.Value, targetPos, Predicate, ent, examiner)) + entities.Add(ent); + } + } + else + { + entities = _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize).ToList(); + } if (entities.Count == 0) return false; diff --git a/Content.Server/CardboardBox/CardboardBoxSystem.cs b/Content.Server/CardboardBox/CardboardBoxSystem.cs index 6b30bf0013..50ee49a24e 100644 --- a/Content.Server/CardboardBox/CardboardBoxSystem.cs +++ b/Content.Server/CardboardBox/CardboardBoxSystem.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Content.Shared.CardboardBox.Components; using Content.Server.Storage.Components; using Content.Shared.CardboardBox; @@ -6,6 +6,8 @@ using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Robust.Shared.Player; using Robust.Shared.Timing; +using Content.Shared.Stealth.Components; +using Content.Shared.Stealth; namespace Content.Server.CardboardBox; @@ -14,12 +16,14 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedMoverController _mover = default!; [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedStealthSystem _stealth = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnBeforeStorageClosed); SubscribeLocalEvent(AfterStorageOpen); + SubscribeLocalEvent(AfterStorageClosed); } private void OnBeforeStorageClosed(EntityUid uid, CardboardBoxComponent component, StorageBeforeCloseEvent args) @@ -57,5 +61,18 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem } component.Mover = null; + + // If this box has a stealth/chameleon effect, disable the stealth effect while the box is open. + _stealth.SetEnabled(uid, false); + } + + private void AfterStorageClosed(EntityUid uid, CardboardBoxComponent component, StorageAfterCloseEvent args) + { + // If this box has a stealth/chameleon effect, enable the stealth effect. + if (TryComp(uid, out StealthComponent? stealth)) + { + _stealth.SetVisibility(uid, stealth.MaxVisibility, stealth); + _stealth.SetEnabled(uid, true, stealth); + } } } diff --git a/Content.Shared/CardboardBox/Components/SharedCardboardBoxComponent.cs b/Content.Shared/CardboardBox/Components/CardboardBoxComponent.cs similarity index 100% rename from Content.Shared/CardboardBox/Components/SharedCardboardBoxComponent.cs rename to Content.Shared/CardboardBox/Components/CardboardBoxComponent.cs diff --git a/Content.Shared/Examine/ExamineSystemShared.cs b/Content.Shared/Examine/ExamineSystemShared.cs index 2541be7cfa..74e5a838d2 100644 --- a/Content.Shared/Examine/ExamineSystemShared.cs +++ b/Content.Shared/Examine/ExamineSystemShared.cs @@ -67,16 +67,28 @@ namespace Content.Shared.Examine public bool CanExamine(EntityUid examiner, EntityUid examined) { return !Deleted(examined) && CanExamine(examiner, EntityManager.GetComponent(examined).MapPosition, - entity => entity == examiner || entity == examined); + entity => entity == examiner || entity == examined, examined); } [Pure] - public virtual bool CanExamine(EntityUid examiner, MapCoordinates target, Ignored? predicate = null) + public virtual bool CanExamine(EntityUid examiner, MapCoordinates target, Ignored? predicate = null, EntityUid? examined = null, ExaminerComponent? examinerComp = null) { - if (!EntityManager.TryGetComponent(examiner, out ExaminerComponent? examinerComponent)) + if (!Resolve(examiner, ref examinerComp, false)) return false; - if (!examinerComponent.DoRangeCheck) + // Ghosts and admins skip examine checks. + if (examinerComp.SkipChecks) + return true; + + if (examined != null) + { + var ev = new ExamineAttemptEvent(examiner); + RaiseLocalEvent(examined.Value, ev); + if (ev.Cancelled) + return false; + } + + if (!examinerComp.CheckInRangeUnOccluded) return true; if (EntityManager.GetComponent(examiner).MapID != target.MapId) @@ -326,4 +338,17 @@ namespace Content.Shared.Examine PushMessage(msg); } } + + /// + /// Event raised directed at an entity that someone is attempting to examine + /// + public sealed class ExamineAttemptEvent : CancellableEntityEventArgs + { + public readonly EntityUid Examiner; + + public ExamineAttemptEvent(EntityUid examiner) + { + Examiner = examiner; + } + } } diff --git a/Content.Shared/Examine/ExaminerComponent.cs b/Content.Shared/Examine/ExaminerComponent.cs index 7f9dce7caa..3a931d961f 100644 --- a/Content.Shared/Examine/ExaminerComponent.cs +++ b/Content.Shared/Examine/ExaminerComponent.cs @@ -7,13 +7,11 @@ namespace Content.Shared.Examine public sealed class ExaminerComponent : Component { [ViewVariables(VVAccess.ReadWrite)] - [DataField("DoRangeCheck")] - private bool _doRangeCheck = true; + [DataField("skipChecks")] + public bool SkipChecks = false; - /// - /// Whether to do a distance check on examine. - /// If false, the user can theoretically examine from infinitely far away. - /// - public bool DoRangeCheck => _doRangeCheck; + [ViewVariables(VVAccess.ReadWrite)] + [DataField("checkInRangeUnOccluded")] + public bool CheckInRangeUnOccluded = true; } } diff --git a/Content.Shared/Stealth/Components/StealthComponent.cs b/Content.Shared/Stealth/Components/StealthComponent.cs index b41f05c9ae..d97e76b9fb 100644 --- a/Content.Shared/Stealth/Components/StealthComponent.cs +++ b/Content.Shared/Stealth/Components/StealthComponent.cs @@ -12,6 +12,12 @@ namespace Content.Shared.Stealth.Components; [Access(typeof(SharedStealthSystem))] public sealed class StealthComponent : Component { + /// + /// Whether or not the stealth effect should currently be applied. + /// + [DataField("enabled")] + public bool Enabled = true; + /// /// Whether or not the entity previously had an interaction outline prior to cloaking. /// @@ -19,12 +25,21 @@ public sealed class StealthComponent : Component public bool HadOutline; /// - /// Last set level of visibility. Ranges from 1 (fully visible) and -1 (fully hidden). To get the actual current - /// visibility, use + /// Minimum visibility before the entity becomes unexaminable (and thus no longer appears on context menus). + /// + [DataField("examineThreshold")] + public readonly float ExamineThreshold = 0.5f; + + /// + /// Last set level of visibility. The visual effect ranges from 1 (fully visible) and -1 (fully hidden). Values + /// outside of this range simply act as a buffer for the visual effect (i.e., a delay before turning invisible). To + /// get the actual current visibility, use /// [DataField("lastVisibility")] [Access(typeof(SharedStealthSystem), Other = AccessPermissions.None)] - public float LastVisibility; + public float LastVisibility = 1; + /// /// Time at which was set. Null implies the entity is currently paused and not @@ -44,17 +59,31 @@ public sealed class StealthComponent : Component /// [DataField("movementVisibilityRate")] public readonly float MovementVisibilityRate = 0.2f; + + /// + /// Minimum visibility. Note that the visual effect caps out at -1, but this value is allowed to be larger or smaller. + /// + [DataField("minVisibility")] + public readonly float MinVisibility = -1f; + + /// + /// Maximum visibility. Note that the visual effect caps out at +1, but this value is allowed to be larger or smaller. + /// + [DataField("maxVisibility")] + public readonly float MaxVisibility = 1.5f; } [Serializable, NetSerializable] public sealed class StealthComponentState : ComponentState { - public float Visibility; - public TimeSpan? LastUpdated; + public readonly float Visibility; + public readonly TimeSpan? LastUpdated; + public readonly bool Enabled; - public StealthComponentState(float stealthLevel, TimeSpan? lastUpdated) + public StealthComponentState(float stealthLevel, TimeSpan? lastUpdated, bool enabled) { Visibility = stealthLevel; LastUpdated = lastUpdated; + Enabled = enabled; } } diff --git a/Content.Shared/Stealth/SharedStealthSystem.cs b/Content.Shared/Stealth/SharedStealthSystem.cs index 582bd7b57d..9390d6eb22 100644 --- a/Content.Shared/Stealth/SharedStealthSystem.cs +++ b/Content.Shared/Stealth/SharedStealthSystem.cs @@ -1,3 +1,4 @@ +using Content.Shared.Examine; using Content.Shared.Stealth.Components; using Robust.Shared.GameStates; using Robust.Shared.Timing; @@ -17,6 +18,35 @@ public abstract class SharedStealthSystem : EntitySystem SubscribeLocalEvent(OnMove); SubscribeLocalEvent(OnPaused); SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnExamine); + } + + private void OnExamine(EntityUid uid, StealthComponent component, ExamineAttemptEvent args) + { + if (!component.Enabled || GetVisibility(uid, component) > component.ExamineThreshold) + return; + + // Don't block examine for owner or children of the cloaked entity. + // Containers and the like should already block examining, so not bothering to check for occluding containers. + var source = args.Examiner; + do + { + if (source == uid) + return; + source = Transform(source).ParentUid; + } + while (source.IsValid()); + + args.Cancel(); + } + + public virtual void SetEnabled(EntityUid uid, bool value, StealthComponent? component = null) + { + if (!Resolve(uid, ref component, false) || component.Enabled == value) + return; + + component.Enabled = value; + Dirty(component); } private void OnPaused(EntityUid uid, StealthComponent component, EntityPausedEvent args) @@ -44,7 +74,7 @@ public abstract class SharedStealthSystem : EntitySystem private void OnStealthGetState(EntityUid uid, StealthComponent component, ref ComponentGetState args) { - args.State = new StealthComponentState(component.LastVisibility, component.LastUpdated); + args.State = new StealthComponentState(component.LastVisibility, component.LastUpdated, component.Enabled); } private void OnStealthHandleState(EntityUid uid, StealthComponent component, ref ComponentHandleState args) @@ -52,6 +82,7 @@ public abstract class SharedStealthSystem : EntitySystem if (args.Current is not StealthComponentState cast) return; + SetEnabled(uid, cast.Enabled, component); component.LastVisibility = cast.Visibility; component.LastUpdated = cast.LastUpdated; } @@ -83,7 +114,7 @@ public abstract class SharedStealthSystem : EntitySystem component.LastUpdated = _timing.CurTime; } - component.LastVisibility = Math.Clamp(component.LastVisibility + delta, -1f, 1f); + component.LastVisibility = Math.Clamp(component.LastVisibility + delta, component.MinVisibility, component.MaxVisibility); Dirty(component); } @@ -96,7 +127,7 @@ public abstract class SharedStealthSystem : EntitySystem if (!Resolve(uid, ref component)) return; - component.LastVisibility = value; + component.LastVisibility = Math.Clamp(value, component.MinVisibility, component.MaxVisibility); if (component.LastUpdated != null) component.LastUpdated = _timing.CurTime; @@ -107,16 +138,18 @@ public abstract class SharedStealthSystem : EntitySystem /// Gets the current visibility from the /// Use this instead of getting LastVisibility from the component directly. /// - /// Returns a calculation that accounts for any stealth change that happened since last update, otherwise returns based on if it can resolve the component. + /// Returns a calculation that accounts for any stealth change that happened since last update, otherwise + /// returns based on if it can resolve the component. Note that the returned value may be larger than the components + /// maximum stealth value if it is currently disabled. public float GetVisibility(EntityUid uid, StealthComponent? component = null) { - if (!Resolve(uid, ref component)) + if (!Resolve(uid, ref component) || !component.Enabled) return 1; if (component.LastUpdated == null) return component.LastVisibility; var deltaTime = _timing.CurTime - component.LastUpdated.Value; - return Math.Clamp(component.LastVisibility + (float) deltaTime.TotalSeconds * component.PassiveVisibilityRate, -1f, 1f); + return Math.Clamp(component.LastVisibility + (float) deltaTime.TotalSeconds * component.PassiveVisibilityRate, component.MinVisibility, component.MaxVisibility); } } diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index 8cbfe3faf9..e45f6868c4 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -27,7 +27,7 @@ - type: Input context: "ghost" - type: Examiner - DoRangeCheck: false + skipChecks: true - type: Ghost - type: IntrinsicRadio channels: diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml index 0cc5ef792a..e39a1a03e9 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml @@ -49,6 +49,7 @@ - type: entity id: StealthBox + suffix: stealth parent: BaseBigBox name: cardboard box #it's still just a box description: Kept ya waiting, huh? @@ -74,6 +75,7 @@ - type: entity id: GhostBox parent: StealthBox + suffix: name: ghost box description: Beware! components: