diff --git a/Content.Client/_Scp/Blinking/BlinkingSystem.cs b/Content.Client/_Scp/Blinking/BlinkingSystem.cs index c06e3f6fb96..83308c451ff 100644 --- a/Content.Client/_Scp/Blinking/BlinkingSystem.cs +++ b/Content.Client/_Scp/Blinking/BlinkingSystem.cs @@ -1,10 +1,12 @@ using Content.Client._Scp.Shaders.Common; using Content.Shared._Scp.Blinking; +using Content.Shared.Alert; using Robust.Client.Audio; using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Audio; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Client._Scp.Blinking; @@ -13,6 +15,7 @@ public sealed class BlinkingSystem : SharedBlinkingSystem { [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly CompatibilityModeActiveWarningSystem _compatibility = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IOverlayManager _overlayMan = default!; [Dependency] private readonly IGameTiming _timing = default!; @@ -25,6 +28,7 @@ public sealed class BlinkingSystem : SharedBlinkingSystem private BlinkingOverlay _overlay = default!; private const float DefaultAnimationDuration = 0.4f; + private ProtoId? _localBlinkingAlert; public override void Initialize() { @@ -42,6 +46,29 @@ public override void Initialize() _overlay.OnAnimationFinished += SetDefaultAnimationDuration; } + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (_player.LocalEntity is not { Valid: true } localEntity) + return; + + if (!BlinkableQuery.TryComp(localEntity, out var blinkable)) + { + if (_localBlinkingAlert.HasValue) + _alerts.ClearAlert(localEntity, _localBlinkingAlert.Value); + + _localBlinkingAlert = null; + return; + } + + if (_localBlinkingAlert.HasValue && _localBlinkingAlert.Value != blinkable.BlinkingAlert) + _alerts.ClearAlert(localEntity, _localBlinkingAlert.Value); + + _localBlinkingAlert = blinkable.BlinkingAlert; + UpdateAlert((localEntity, blinkable)); + } + protected override void OnOpenedEyes(Entity ent, ref EntityOpenedEyesEvent args) { base.OnOpenedEyes(ent, ref args); @@ -66,6 +93,11 @@ private void OnAttached(Entity ent, ref LocalPlayerAttachedE private void OnDetached(Entity ent, ref LocalPlayerDetachedEvent args) { + if (_localBlinkingAlert.HasValue) + _alerts.ClearAlert(ent.Owner, _localBlinkingAlert.Value); + + _localBlinkingAlert = null; + if (!_overlayMan.HasOverlay()) return; @@ -84,7 +116,7 @@ private void OnEyesStateChanged(EntityEyesStateChanged ev) var ent = GetEntity(ev.NetEntity); - if (!TryComp(ent, out var blinkable)) + if (!BlinkableQuery.TryComp(ent, out var blinkable)) return; if (ev.NewState == EyesState.Closed) @@ -195,4 +227,23 @@ private void SetDefaultAnimationDuration() { _overlay.AnimationDuration = _compatibility.ShouldUseShaders ? DefaultAnimationDuration : 0f; } + + /// + /// Актуализирует иконку моргания справа у панели чата игрока + /// + private void UpdateAlert(Entity ent) + { + // Если в данный момент глаза закрыты, то выставляем иконку с закрытым глазом + if (IsBlind(ent.AsNullable())) + { + _alerts.ShowAlert(ent.Owner, ent.Comp.BlinkingAlert, 4); + return; + } + + var timeToNextBlink = ent.Comp.NextBlink - _timing.CurTime; + var denom = MathF.Max(0.001f, (float)(ent.Comp.BlinkingInterval.TotalSeconds - ent.Comp.BlinkingDuration.TotalSeconds)); + var severity = (short) Math.Clamp(4 - (float) timeToNextBlink.TotalSeconds / denom * 4, 0, 4); + + _alerts.ShowAlert(ent.Owner, ent.Comp.BlinkingAlert, severity); + } } diff --git a/Content.Client/_Scp/Fear/FearSystem.cs b/Content.Client/_Scp/Fear/FearSystem.cs index f043a213250..47f413b31a3 100644 --- a/Content.Client/_Scp/Fear/FearSystem.cs +++ b/Content.Client/_Scp/Fear/FearSystem.cs @@ -1,5 +1,4 @@ -using Content.Shared._Scp.Fear; -using Content.Shared._Scp.Fear.Components; +using Content.Shared._Scp.Fear.Components; using Content.Shared._Scp.Fear.Systems; using Content.Shared._Sunrise.Heartbeat; using Robust.Client.Audio; diff --git a/Content.Client/_Scp/Scp939/Scp939HudSystem.Overlay.cs b/Content.Client/_Scp/Scp939/Scp939HudSystem.Overlay.cs new file mode 100644 index 00000000000..e14458dfc95 --- /dev/null +++ b/Content.Client/_Scp/Scp939/Scp939HudSystem.Overlay.cs @@ -0,0 +1,50 @@ +using Content.Shared._Scp.Scp939; +using Robust.Client.Graphics; +using Robust.Shared.Player; + +namespace Content.Client._Scp.Scp939; + +public sealed partial class Scp939HudSystem +{ + [Dependency] private readonly IOverlayManager _overlayManager = default!; + + private void InitializeOverlay() + { + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + } + + private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args) + { + _scp939Component = ent.Comp; + AddOverlays(); + } + + private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args) + { + _scp939Component = null; + } + + private void AddOverlays() + { + if (_overlaysPresented) + return; + + _overlayManager.AddOverlay(_setAlphaOverlay); + _overlayManager.AddOverlay(_resetAlphaOverlay); + + _overlaysPresented = true; + } + + private void RemoveOverlays() + { + if (!_overlaysPresented) + return; + + _overlayManager.RemoveOverlay(_setAlphaOverlay); + _overlayManager.RemoveOverlay(_resetAlphaOverlay); + + CachedBaseAlphas.Clear(); + _overlaysPresented = false; + } +} diff --git a/Content.Client/_Scp/Scp939/Scp939HudSystem.Visibility.cs b/Content.Client/_Scp/Scp939/Scp939HudSystem.Visibility.cs new file mode 100644 index 00000000000..dc111bf2bfe --- /dev/null +++ b/Content.Client/_Scp/Scp939/Scp939HudSystem.Visibility.cs @@ -0,0 +1,162 @@ +using Content.Shared._Scp.Scp939; +using Content.Shared.Standing; +using Content.Shared.Throwing; +using Content.Shared.Weapons.Melee.Events; +using Robust.Shared.Physics.Events; +using Robust.Shared.Random; + +namespace Content.Client._Scp.Scp939; + +public sealed partial class Scp939HudSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + private void InitializeVisibility() + { + SubscribeLocalEvent((Entity ent, ref StartCollideEvent args) + => OnCollide(ent, args.OtherEntity)); + SubscribeLocalEvent((Entity ent, ref EndCollideEvent args) + => OnCollide(ent, args.OtherEntity)); + SubscribeLocalEvent(OnVisibilityStateUpdated); + + SubscribeLocalEvent(OnMove); + + SubscribeLocalEvent(OnThrow); + SubscribeLocalEvent(OnStood); + SubscribeLocalEvent(OnMeleeAttack); + SubscribeLocalEvent(OnDown); + } + + private void OnVisibilityStateUpdated(Entity ent, ref AfterAutoHandleStateEvent args) + { + if (ent.Comp.LastHandledVisibilityResetCounter == ent.Comp.VisibilityResetCounter) + return; + + ent.Comp.LastHandledVisibilityResetCounter = ent.Comp.VisibilityResetCounter; + ent.Comp.VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!IsActive) + return; + + _lastUpdateTime += frameTime; + if (_lastUpdateTime < UpdateInterval) + return; + + var delta = _lastUpdateTime; + _lastUpdateTime = 0f; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out _, out var visibilityComponent)) + { + if (visibilityComponent.VisibilityAcc >= visibilityComponent.HideTime) + continue; + + visibilityComponent.VisibilityAcc = MathF.Min(visibilityComponent.VisibilityAcc + delta, visibilityComponent.HideTime); + } + } + + private void OnMove(Entity ent, ref MoveEvent args) + { + if (!IsActive) + return; + + // В зависимости от наличие защит или проблем со зрением у 939 изменяется то, насколько хорошо мы видим жертву + if (ModifyAcc(ent.Comp, out var modifier)) // Если зрение затруднено + { + ent.Comp.VisibilityAcc *= modifier; + } + else if (_scp939ProtectionQuery.HasComp(ent)) // Если имеется защита(тихое хождение) + { + return; + } + else // Если со зрением все ок + { + ent.Comp.VisibilityAcc = 0; + } + + if (!_movementSpeedQuery.TryComp(ent, out var speedModifierComponent) + || !_physicsQuery.TryComp(ent, out var physicsComponent)) + { + return; + } + + var currentVelocity = physicsComponent.LinearVelocity.Length(); + + if (speedModifierComponent.BaseWalkSpeed > currentVelocity) + ent.Comp.VisibilityAcc = ent.Comp.HideTime / 2f; + } + + + private void OnCollide(Entity ent, EntityUid otherEntity) + { + if (!IsActive) + return; + + if (!HasComp(otherEntity)) + return; + + MobDidSomething(ent); + } + + private void OnThrow(Entity ent, ref ThrowEvent args) + { + if (!IsActive) + return; + + MobDidSomething(ent); + } + + private void OnStood(Entity ent, ref StoodEvent args) + { + if (!IsActive) + return; + + MobDidSomething(ent); + } + + private void OnMeleeAttack(Entity ent, ref MeleeAttackEvent args) + { + if (!IsActive) + return; + + MobDidSomething(ent); + } + + private void OnDown(Entity ent, ref DownedEvent args) + { + if (!IsActive) + return; + + MobDidSomething(ent); + } + + private void MobDidSomething(Entity ent) + { + ent.Comp.VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; + } + + // TODO: Переделать под статус эффект и добавить его в панель статус эффектов, а то непонятно игруну + /// + /// Если вдруг собачка плохо видит + /// + private bool ModifyAcc(ActiveScp939VisibilityComponent visibilityComponent, out int modifier) + { + // 1 = отсутствие модификатора + modifier = 1; + + if (_scp939Component == null) + return false; + + if (!_scp939Component.PoorEyesight) + return false; + + modifier = _random.Next(visibilityComponent.MinValue, visibilityComponent.MaxValue); + + return true; + } +} diff --git a/Content.Client/_Scp/Scp939/Scp939HudSystem.cs b/Content.Client/_Scp/Scp939/Scp939HudSystem.cs index f1a99bc2acb..a0136d8844b 100644 --- a/Content.Client/_Scp/Scp939/Scp939HudSystem.cs +++ b/Content.Client/_Scp/Scp939/Scp939HudSystem.cs @@ -1,74 +1,75 @@ -using System.Diagnostics.CodeAnalysis; using Content.Client.Overlays; using Content.Client.SSDIndicator; using Content.Client.Stealth; using Content.Shared._Scp.Scp939; using Content.Shared._Scp.Scp939.Protection; using Content.Shared.Examine; +using Content.Shared.Inventory.Events; using Content.Shared.Movement.Components; -using Content.Shared.Standing; using Content.Shared.StatusIcon.Components; -using Content.Shared.Throwing; -using Content.Shared.Weapons.Melee.Events; -using Content.Shared.Weapons.Ranged.Events; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Events; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; namespace Content.Client._Scp.Scp939; -public sealed class Scp939HudSystem : EquipmentHudSystem +public sealed partial class Scp939HudSystem : EquipmentHudSystem { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; - private ShaderInstance _shaderInstance = default!; - private readonly Dictionary _shaderCache = new(); + internal readonly List<(Entity Ent, float BaseAlpha)> CachedBaseAlphas = new(64); + + private Scp939SetAlphaOverlay _setAlphaOverlay = default!; + private Scp939ResetAlphaOverlay _resetAlphaOverlay = default!; // TODO: Выделить значения плохого зрения в отдельный компонент, не связанный с 939 private Scp939Component? _scp939Component; + private EntityQuery _eyeQuery; + private EntityQuery _scp939ProtectionQuery; + private EntityQuery _movementSpeedQuery; + private EntityQuery _physicsQuery; + + private bool _overlaysPresented; private float _lastUpdateTime; - private const float UpdateInterval = 0.05f; // Обновляем каждые n секунды + + private const float UpdateInterval = 0.05f; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent((Entity ent, ref StartCollideEvent args) => OnCollide(ent, args.OtherEntity)); - SubscribeLocalEvent((Entity ent, ref EndCollideEvent args) => OnCollide(ent, args.OtherEntity)); + InitializeVisibility(); + InitializeOverlay(); - #region Visibility + SubscribeLocalEvent(OnGetStatusIcons, after: [typeof(SSDIndicatorSystem)] ); + SubscribeLocalEvent(OnExamine); - SubscribeLocalEvent(OnMove); + _eyeQuery = GetEntityQuery(); + _scp939ProtectionQuery = GetEntityQuery(); + _movementSpeedQuery = GetEntityQuery(); + _physicsQuery = GetEntityQuery(); - SubscribeLocalEvent(OnThrow); - SubscribeLocalEvent(OnStood); - SubscribeLocalEvent(OnMeleeAttack); + _setAlphaOverlay = new(); + _resetAlphaOverlay = new(); - #endregion - - SubscribeLocalEvent(BeforeRender); - SubscribeLocalEvent(OnGetStatusIcons, after: new []{typeof(SSDIndicatorSystem)}); - SubscribeLocalEvent(OnExamine); - - SubscribeLocalEvent(OnPlayerAttached); - SubscribeLocalEvent(OnPlayerDetached); + UpdatesAfter.Add(typeof(StealthSystem)); + } - SubscribeLocalEvent(OnEntityTerminating); + public override void Shutdown() + { + RestoreCachedBaseAlphas(); + RemoveOverlays(); - _shaderInstance = _prototypeManager.Index("Hide").Instance(); + _setAlphaOverlay.Dispose(); + _resetAlphaOverlay.Dispose(); - UpdatesAfter.Add(typeof(StealthSystem)); + base.Shutdown(); } - private void OnExamine(Entity ent, ref ExamineAttemptEvent args) + private void OnExamine(Entity ent, ref ExamineAttemptEvent args) { if (!IsActive) return; @@ -79,12 +80,9 @@ private void OnExamine(Entity ent, ref ExamineAttempt args.Cancel(); } - private void OnGetStatusIcons(Entity ent, ref GetStatusIconsEvent args) + private void OnGetStatusIcons(Entity ent, ref GetStatusIconsEvent args) { - // Олежа чурка - var playerEntity = _playerManager.LocalSession?.AttachedEntity; - - if (!HasComp(playerEntity)) + if (!IsActive) return; var visibility = GetVisibility(ent); @@ -93,136 +91,53 @@ private void OnGetStatusIcons(Entity ent, ref GetStat args.StatusIcons.Clear(); } - protected override void DeactivateInternal() + protected override void UpdateInternal(RefreshEquipmentHudEvent args) { - base.DeactivateInternal(); - - var query = EntityQueryEnumerator(); - - while (query.MoveNext(out _, out _, out var spriteComponent)) - { - spriteComponent.PostShader = null; - } - } - - #region Visibility - - private void OnCollide(Entity ent, EntityUid otherEntity) - { - if (!HasComp(otherEntity)) - return; - - MobDidSomething(ent); - } - - private void OnThrow(Entity ent, ref ThrowEvent args) - { - MobDidSomething(ent); - } - - private void OnStood(Entity ent, ref StoodEvent args) - { - MobDidSomething(ent); - } - - private void OnMeleeAttack(Entity ent, ref MeleeAttackEvent args) - { - MobDidSomething(ent); - } - - private void MobDidSomething(Entity ent) - { - ent.Comp.VisibilityAcc = 0.001f; - Dirty(ent); - } - - private void OnMove(Entity ent, ref MoveEvent args) - { - // В зависимости от наличие защит или проблем со зрением у 939 изменяется то, насколько хорошо мы видим жертву - if (ModifyAcc(ent.Comp, out var modifier)) // Если зрение затруднено - { - ent.Comp.VisibilityAcc *= modifier; - } - else if (HasComp(ent)) // Если имеется защита(тихое хождение) - { - return; - } - else // Если со зрением все ок - { - ent.Comp.VisibilityAcc = 0; - } - - if (!TryComp(ent, out var speedModifierComponent) - || !TryComp(ent, out var physicsComponent)) - { - return; - } + base.UpdateInternal(args); - var currentVelocity = physicsComponent.LinearVelocity.Length(); - - if (speedModifierComponent.BaseWalkSpeed > currentVelocity) - { - ent.Comp.VisibilityAcc = ent.Comp.HideTime / 2f; - } + _scp939Component = args.Components.Count > 0 ? args.Components[0] : null; + AddOverlays(); } - #endregion - - private void OnPlayerAttached(Entity ent, ref PlayerAttachedEvent args) + protected override void DeactivateInternal() { - _scp939Component = ent.Comp; - } + base.DeactivateInternal(); - private void OnPlayerDetached(Entity ent, ref PlayerDetachedEvent args) - { _scp939Component = null; - } + _lastUpdateTime = 0f; - private void OnEntityTerminating(Entity ent, ref EntityTerminatingEvent args) - { - _shaderCache.Remove(ent); + RestoreCachedBaseAlphas(); + RemoveOverlays(); } - public override void Update(float frameTime) + internal bool CanDraw(in OverlayDrawArgs args) { - base.Update(frameTime); - if (!IsActive) - return; - - _lastUpdateTime += frameTime; - if (_lastUpdateTime < UpdateInterval) - return; + return false; - _lastUpdateTime = 0f; + if (_playerManager.LocalEntity is not { } player) + return false; - var query = EntityQueryEnumerator(); + if (!_eyeQuery.TryComp(player, out var eye)) + return false; - while (query.MoveNext(out var uid, out var spriteComponent, out var visibilityComponent)) - { - // Обновляем только если нужно - if (!_shaderCache.TryGetValue(uid, out var shader)) - { - shader = _shaderInstance.Duplicate(); - _shaderCache[uid] = shader; - UpdateVisibility(spriteComponent, shader); - } - - if (visibilityComponent.VisibilityAcc < 3f) - visibilityComponent.VisibilityAcc += frameTime; - } + return args.Viewport.Eye == eye.Eye; } - private static void UpdateVisibility(SpriteComponent spriteComponent, ShaderInstance shader) + internal void RestoreCachedBaseAlphas() { - spriteComponent.Color = Color.White; - spriteComponent.GetScreenTexture = true; - spriteComponent.RaiseShaderEvent = true; + foreach (var (ent, baseAlpha) in CachedBaseAlphas) + { + if (!EntityManager.EntityExists(ent)) + continue; - spriteComponent.PostShader = shader; + _sprite.SetColor(ent.AsNullable(), ent.Comp.Color.WithAlpha(baseAlpha)); + } + + CachedBaseAlphas.Clear(); } - private static float GetVisibility(Entity ent) + internal static float GetVisibility(Entity ent) { var acc = ent.Comp.VisibilityAcc; @@ -231,30 +146,4 @@ private static float GetVisibility(Entity ent) return Math.Clamp(1f - (acc / ent.Comp.HideTime), 0f, 1f); } - - private static void BeforeRender(Entity ent, ref BeforePostShaderRenderEvent args) - { - var visibility = GetVisibility(ent); - args.Sprite.PostShader?.SetParameter("visibility", visibility); - } - - // TODO: Переделать под статус эффект и добавить его в панель статус эффектов, а то непонятно игруну - /// - /// Если вдруг собачка плохо видит - /// - private bool ModifyAcc(Scp939VisibilityComponent visibilityComponent, [NotNullWhen(true)] out int modifier) - { - // 1 = отсутствие модификатора - modifier = 1; - - if (_scp939Component == null) - return false; - - if (!_scp939Component.PoorEyesight) - return false; - - modifier = _random.Next(visibilityComponent.MinValue, visibilityComponent.MaxValue); - - return true; - } } diff --git a/Content.Client/_Scp/Scp939/Scp939ResetAlphaOverlay.cs b/Content.Client/_Scp/Scp939/Scp939ResetAlphaOverlay.cs new file mode 100644 index 00000000000..2eb83579c89 --- /dev/null +++ b/Content.Client/_Scp/Scp939/Scp939ResetAlphaOverlay.cs @@ -0,0 +1,30 @@ +using Robust.Client.Graphics; +using Robust.Shared.Enums; + +namespace Content.Client._Scp.Scp939; + +public sealed class Scp939ResetAlphaOverlay : Overlay +{ + [Dependency] private readonly IEntityManager _ent = default!; + + private readonly Scp939HudSystem _hud; + + public override OverlaySpace Space => OverlaySpace.WorldSpace; + + public Scp939ResetAlphaOverlay() + { + IoCManager.InjectDependencies(this); + + _hud = _ent.System(); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + return _hud.CachedBaseAlphas.Count > 0 && _hud.CanDraw(in args); + } + + protected override void Draw(in OverlayDrawArgs args) + { + _hud.RestoreCachedBaseAlphas(); + } +} diff --git a/Content.Client/_Scp/Scp939/Scp939SetAlphaOverlay.cs b/Content.Client/_Scp/Scp939/Scp939SetAlphaOverlay.cs new file mode 100644 index 00000000000..d081f9afa4b --- /dev/null +++ b/Content.Client/_Scp/Scp939/Scp939SetAlphaOverlay.cs @@ -0,0 +1,55 @@ +using Content.Shared._Scp.Scp939; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.Enums; + +namespace Content.Client._Scp.Scp939; + +public sealed class Scp939SetAlphaOverlay : Overlay +{ + [Dependency] private readonly IEntityManager _ent = default!; + + private readonly Scp939HudSystem _hud; + private readonly TransformSystem _transform; + private readonly SpriteSystem _sprite; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities; + + public Scp939SetAlphaOverlay() + { + IoCManager.InjectDependencies(this); + + _hud = _ent.System(); + _transform = _ent.System(); + _sprite = _ent.System(); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + return _hud.CanDraw(in args); + } + + protected override void Draw(in OverlayDrawArgs args) + { + _hud.CachedBaseAlphas.Clear(); + + var query = _ent.EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var visibility, out var sprite, out var xform)) + { + if (xform.MapID != args.MapId || !sprite.Visible) + continue; + + var worldPosition = _transform.GetWorldPosition(xform); + if (!args.WorldBounds.Contains(worldPosition)) + continue; + + var targetAlpha = Scp939HudSystem.GetVisibility((uid, visibility)); + if (MathF.Abs(sprite.Color.A - targetAlpha) <= 0.01f) + continue; + + var entity = (uid, sprite); + _hud.CachedBaseAlphas.Add((entity, sprite.Color.A)); + _sprite.SetColor(entity, sprite.Color.WithAlpha(targetAlpha)); + } + } +} diff --git a/Content.Client/_Scp/Shaders/Common/Grain/GrainOverlaySystem.cs b/Content.Client/_Scp/Shaders/Common/Grain/GrainOverlaySystem.cs index 9e6fb4b11eb..a2f8dd79780 100644 --- a/Content.Client/_Scp/Shaders/Common/Grain/GrainOverlaySystem.cs +++ b/Content.Client/_Scp/Shaders/Common/Grain/GrainOverlaySystem.cs @@ -20,7 +20,7 @@ public override void Initialize() Overlay = new GrainOverlay(); - SubscribeLocalEvent(OnAdditionalStrengthChanged); + SubscribeLocalEvent(OnAdditionalStrengthChanged); _cfg.OnValueChanged(ScpCCVars.GrainToggleOverlay, ToggleGrainOverlay); _cfg.OnValueChanged(ScpCCVars.GrainStrength, SetBaseStrength); @@ -44,7 +44,7 @@ protected override void OnPlayerAttached(Entity ent, ref SetBaseStrength(_cfg.GetCVar(ScpCCVars.GrainStrength)); } - private void OnAdditionalStrengthChanged(Entity ent, ref ShaderAdditionalStrengthChanged args) + private void OnAdditionalStrengthChanged(Entity ent, ref AfterAutoHandleStateEvent args) { if (_player.LocalEntity != ent) return; diff --git a/Content.Client/_Scp/Shaders/Common/Vignette/VignetteOverlaySystem.cs b/Content.Client/_Scp/Shaders/Common/Vignette/VignetteOverlaySystem.cs index 3cabadc1d1b..51f78a9539c 100644 --- a/Content.Client/_Scp/Shaders/Common/Vignette/VignetteOverlaySystem.cs +++ b/Content.Client/_Scp/Shaders/Common/Vignette/VignetteOverlaySystem.cs @@ -17,11 +17,11 @@ public override void Initialize() DisableOnCompatibilityMode = false; Overlay = new VignetteOverlay(); - SubscribeLocalEvent(OnAdditionalStrengthChanged); + SubscribeLocalEvent(OnAdditionalStrengthChanged); } private void OnAdditionalStrengthChanged(Entity ent, - ref ShaderAdditionalStrengthChanged args) + ref AfterAutoHandleStateEvent args) { if (_player.LocalEntity != ent) return; diff --git a/Content.Server/_Scp/Fear/FearSystem.EntityEffects.cs b/Content.Server/_Scp/Fear/FearSystem.EntityEffects.cs index b6437dbd826..e011994bc14 100644 --- a/Content.Server/_Scp/Fear/FearSystem.EntityEffects.cs +++ b/Content.Server/_Scp/Fear/FearSystem.EntityEffects.cs @@ -14,6 +14,5 @@ private void InitializeEntityEffects() private void OnExecuteCalmDown(Entity ent, ref EntityEffectEvent args) { ent.Comp.NextTimeDecreaseFearLevel -= args.Effect.SpeedUpBy; - Dirty(ent); } } diff --git a/Content.Server/_Scp/Fear/FearSystem.Fears.cs b/Content.Server/_Scp/Fear/FearSystem.Fears.cs index 1e99ee4e4e2..44172aea89c 100644 --- a/Content.Server/_Scp/Fear/FearSystem.Fears.cs +++ b/Content.Server/_Scp/Fear/FearSystem.Fears.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; using Content.Server._Scp.Shaders.Highlighting; -using Content.Server._Sunrise.Mood; +using Content.Shared._Scp.Blinking; using Content.Shared._Scp.Fear; using Content.Shared._Scp.Fear.Components; using Content.Shared._Scp.Fear.Components.Fears; @@ -52,13 +52,21 @@ private void OnMobStateChanged(MobStateChangedEvent ev) var toggleUsed = new ItemToggledEvent(false, activated, null); RaiseLocalEvent(ev.Target, ref toggleUsed); + // Если activated = true, значит человек умер. + // Поэтому код ниже требует true, так как реализует логику для смерти. if (!activated) return; - var whoSaw = _watching.GetAllEntitiesVisibleTo(ev.Target); + using var realWatchers = ListPoolEntity.Rent(); + if (!_watching.TryGetWatchers(ev.Target, realWatchers.Value, flags: LookupFlags.Dynamic)) + return; - foreach (var uid in whoSaw) + foreach (var uid in realWatchers.Value) { + // Убийца не будет печалиться смерти убитого + if (uid.Owner == ev.Origin) + continue; + AddNegativeMoodEffect(uid, MoodSomeoneDiedOnMyEyes); } } @@ -80,7 +88,7 @@ private void UpdateHemophobia() continue; _hemophobiaBloodList.Clear(); - var bloodAmount = _helpers.GetAroundSolutionVolume(uid, hemophobia.Reagent, in _hemophobiaBloodList); + var bloodAmount = _helpers.GetAroundSolutionVolume(uid, hemophobia.Reagent, _hemophobiaBloodList); var requiredBloodAmount = hemophobia.BloodRequiredPerState[fear.State]; if (bloodAmount <= requiredBloodAmount) diff --git a/Content.Server/_Scp/Fear/FearSystem.Gameplay.cs b/Content.Server/_Scp/Fear/FearSystem.Gameplay.cs index f6cbe9391cb..09d113fde29 100644 --- a/Content.Server/_Scp/Fear/FearSystem.Gameplay.cs +++ b/Content.Server/_Scp/Fear/FearSystem.Gameplay.cs @@ -11,38 +11,10 @@ namespace Content.Server._Scp.Fear; public sealed partial class FearSystem { [Dependency] private readonly ChatSystem _chat = default!; - [Dependency] private readonly StandingStateSystem _standing = default!; [Dependency] private readonly IRobustRandom _random = default!; private static readonly ProtoId ScreamProtoId = "Scream"; - private void InitializeGameplay() - { - // TODO: Перенести это на отдельный компонент, чтобы не перебирать всех потенциально пугающихся. - SubscribeLocalEvent(OnMove); - } - - /// - /// Обрабатывает событие хождения. - /// Реализует случайное падение во время сильного страха - /// - private void OnMove(Entity ent, ref MoveInputEvent args) - { - if (ent.Comp.State < ent.Comp.FallOffRequiredState) - return; - - if (_timing.CurTime < ent.Comp.FallOffNextCheckTime) - return; - - var percentNormalized = PercentToNormalized(ent.Comp.FallOffChance); - SetNextFallOffTime(ent); // Даже если не прокнет, то время все равно должно устанавливаться - - if (!_random.Prob(percentNormalized)) - return; - - _standing.Down(ent, force: true); - } - /// /// Пытается закричать, если увиденный объект настолько страшный. /// @@ -55,12 +27,4 @@ protected override void TryScream(Entity ent) _chat.TryEmoteWithChat(ent, ScreamProtoId); } - - /// - /// Устанавливает следующее время возможности запнуться. - /// - private void SetNextFallOffTime(Entity ent) - { - ent.Comp.FallOffNextCheckTime = _timing.CurTime + ent.Comp.FallOffCheckInterval; - } } diff --git a/Content.Server/_Scp/Fear/FearSystem.SoundEffects.cs b/Content.Server/_Scp/Fear/FearSystem.SoundEffects.cs index 2264e26207a..82e0f24cb61 100644 --- a/Content.Server/_Scp/Fear/FearSystem.SoundEffects.cs +++ b/Content.Server/_Scp/Fear/FearSystem.SoundEffects.cs @@ -77,10 +77,12 @@ protected override void StartBreathing(Entity e var audio = _audio.PlayGlobal(BreathingSound, ent, audioParams); ent.Comp.BreathingAudioStream = audio?.Entity; + DirtyField(ent, ent.Comp, nameof(FearActiveSoundEffectsComponent.BreathingAudioStream)); } - private void StopBreathing(Entity ent) + protected override void StopBreathing(Entity ent) { ent.Comp.BreathingAudioStream = _audio.Stop(ent.Comp.BreathingAudioStream); + DirtyField(ent, ent.Comp, nameof(FearActiveSoundEffectsComponent.BreathingAudioStream)); } } diff --git a/Content.Server/_Scp/Fear/FearSystem.Traits.cs b/Content.Server/_Scp/Fear/FearSystem.Traits.cs index 8c53935263b..1d71f53622a 100644 --- a/Content.Server/_Scp/Fear/FearSystem.Traits.cs +++ b/Content.Server/_Scp/Fear/FearSystem.Traits.cs @@ -31,11 +31,10 @@ private void OnStuporFearStateChanged(Entity ent, ref FearS if (args.NewState < ent.Comp.RequiredState) return; - var normalizedChance = PercentToNormalized(ent.Comp.Chance); - if (!_random.Prob(normalizedChance)) + if (!_random.Prob(ent.Comp.Chance)) return; - _statusEffects.TryAddStatusEffectDuration(ent, FearStuporComponent.StatusEffect, ent.Comp.StuporTime); + _statusEffects.TryAddStatusEffectDuration(ent, ent.Comp.StatusEffect, ent.Comp.StuporTime); } private void OnStutteringFearStateChanged(Entity ent, ref FearStateChangedEvent args) @@ -63,10 +62,9 @@ private void OnFaintingFearStateChanged(Entity ent, ref F if (args.NewState < ent.Comp.RequiredState) return; - var percentNormalized = PercentToNormalized(ent.Comp.Chance); - if (!_random.Prob(percentNormalized)) + if (!_random.Prob(ent.Comp.Chance)) return; - _statusEffects.TryAddStatusEffectDuration(ent, FearFaintingComponent.StatusEffect, ent.Comp.Time); + _statusEffects.TryAddStatusEffectDuration(ent, ent.Comp.StatusEffect, ent.Comp.Time); } } diff --git a/Content.Server/_Scp/Fear/FearSystem.cs b/Content.Server/_Scp/Fear/FearSystem.cs index afe71f1ba63..327507f0ae2 100644 --- a/Content.Server/_Scp/Fear/FearSystem.cs +++ b/Content.Server/_Scp/Fear/FearSystem.cs @@ -1,8 +1,8 @@ -using System.Linq; -using Content.Shared._Scp.Fear; +using Content.Shared._Scp.Fear; using Content.Shared._Scp.Fear.Components; using Content.Shared._Scp.Fear.Systems; using Content.Shared._Sunrise.Mood; +using Content.Shared.GameTicking; using Content.Shared.Mobs.Components; using Content.Shared.Rejuvenate; using Robust.Shared.Timing; @@ -13,8 +13,6 @@ public sealed partial class FearSystem : SharedFearSystem { [Dependency] private readonly IGameTiming _timing = default!; - private EntityQuery _activeFearEffects; - private static readonly TimeSpan CalmDownCheckCooldown = TimeSpan.FromSeconds(1f); private TimeSpan _nextCalmDownCheck = TimeSpan.Zero; @@ -22,13 +20,12 @@ public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnCleanUp); + InitializeSoundEffects(); InitializeFears(); - InitializeGameplay(); InitializeTraits(); InitializeEntityEffects(); - - _activeFearEffects = GetEntityQuery(); } public override void Update(float frameTime) @@ -49,13 +46,13 @@ private void UpdateCalmDown() // Проходимся по людям с компонентом страха и уменьшаем уровень страха со временем while (query.MoveNext(out var uid, out var fear, out var mob)) { - if (!_mob.IsAlive(uid, mob)) + if (fear.State == FearState.None) continue; - if (fear.State == FearState.None) + if (!_mob.IsAlive(uid, mob)) continue; - if (fear.NextTimeDecreaseFearLevel > _timing.CurTime) + if (_timing.CurTime < fear.NextTimeDecreaseFearLevel) continue; var entity = (uid, fear); @@ -75,16 +72,9 @@ private void UpdateCalmDown() /// public bool TryCalmDown(Entity ent) { - // Немного костыль, но это означает, что мы прямо сейчас испытываем какие-то приколы со страхом - // И пугаемся чего-то в данный момент. Значит мы не должны успокаиваться. - if (_activeFearEffects.HasComp(ent)) - return false; - - var visibleFearSources = _watching.GetAllVisibleTo(ent.Owner, ent.Comp.SeenBlockerLevel); - // Проверка на то, что мы в данный момент не смотрим на какую-то страшную сущность. // Нельзя успокоиться, когда мы смотрим на источник страха. - if (visibleFearSources.Any()) + if (_watching.TryGetAnyEntitiesVisibleTo(ent.Owner, ent.Comp.SeenBlockerLevel)) return false; var newFearState = GetDecreasedLevel(ent.Comp.State); @@ -124,10 +114,8 @@ private void RemoveMoodEffects(EntityUid uid) RaiseLocalEvent(uid, new MoodRemoveEffectEvent(MoodHemophobicBleeding)); } - protected override void Clear() + private void OnCleanUp(RoundRestartCleanupEvent args) { - base.Clear(); - _nextHemophobiaCheck = TimeSpan.Zero; _nextCalmDownCheck = TimeSpan.Zero; } diff --git a/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessComponent.cs b/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessComponent.cs index ae57cfcc523..f4dec26d28f 100644 --- a/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessComponent.cs +++ b/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessComponent.cs @@ -7,5 +7,5 @@ public sealed partial class ArtifactScp096MadnessComponent : Component public float Radius = 12f; [DataField] - public float Percent = 70f; + public float Percent = 0.7f; } diff --git a/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs b/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs index e432cbcfd91..e073a27543f 100644 --- a/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs +++ b/Content.Server/_Scp/Research/Artifacts/Effects/_ScpSpecific/Scp096/Madness/ArtifactScp096MadnessSystem.cs @@ -1,9 +1,9 @@ using System.Linq; using Content.Server._Scp.Scp096; using Content.Server._Sunrise.Helpers; +using Content.Shared._Scp.Blinking; using Content.Shared._Scp.Scp096.Main.Components; using Content.Shared._Scp.ScpMask; -using Content.Shared._Scp.Watching; using Content.Shared._Sunrise.Helpers; using Content.Shared.Xenoarchaeology.Artifact; using Content.Shared.Xenoarchaeology.Artifact.XAE; @@ -15,7 +15,7 @@ public sealed class ArtifactScp096MadnessSystem : BaseXAESystem ent, if (!_helpers.TryGetFirst(out var scp096)) return; - var targets = _watching.GetWatchers(ent.Owner) + var targets = _lookup.GetEntitiesInRange(Transform(scp096.Value).Coordinates, ent.Comp.Radius, LookupFlags.Dynamic) .ToList() .ShuffleRobust(_random) .TakePercentage(ent.Comp.Percent); @@ -37,5 +37,7 @@ protected override void OnActivated(Entity ent, // TODO: Пофиксить разрыв маски много раз _scpMask.TryTear(scp096.Value); } + + // TODO: Звук } } diff --git a/Content.Server/_Scp/Scp173/Scp173System.cs b/Content.Server/_Scp/Scp173/Scp173System.cs index c070cb8ca0a..f595135cf6a 100644 --- a/Content.Server/_Scp/Scp173/Scp173System.cs +++ b/Content.Server/_Scp/Scp173/Scp173System.cs @@ -103,7 +103,7 @@ private void OnStructureDamage(Entity uid, ref Scp173DamageStru return; } - if (Watching.IsWatched(uid.Owner)) + if (Watching.IsWatchedByAny(uid)) { var message = Loc.GetString("scp173-fast-movement-too-many-watchers"); _popup.PopupEntity(message, uid, uid, PopupType.LargeCaution); @@ -185,7 +185,7 @@ private void OnClog(Entity ent, ref Scp173ClogAction args) return; } - if (Watching.IsWatched(ent.Owner)) + if (Watching.IsWatchedByAny(ent)) { var message = Loc.GetString("scp173-fast-movement-too-many-watchers"); _popup.PopupEntity(message, ent, ent, PopupType.LargeCaution); @@ -253,7 +253,7 @@ private void OnFastMovement(Entity ent, ref Scp173FastMovementA return; } - if (Watching.IsWatched(ent.Owner, out var watchersCount) && watchersCount > ent.Comp.MaxWatchers) + if (Watching.TryGetWatchers(ent, out var watchersCount) && watchersCount > ent.Comp.MaxWatchers) { var message = Loc.GetString("scp173-fast-movement-too-many-watchers"); _popup.PopupEntity(message, ent, ent, PopupType.LargeCaution); diff --git a/Content.Server/_Scp/Scp939/Scp939System.Actions.cs b/Content.Server/_Scp/Scp939/Scp939System.Actions.cs index f84644780a6..fd97893262d 100644 --- a/Content.Server/_Scp/Scp939/Scp939System.Actions.cs +++ b/Content.Server/_Scp/Scp939/Scp939System.Actions.cs @@ -1,5 +1,6 @@ using Content.Server.Chat.Systems; using Content.Server.Examine; +using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Scp939; using Content.Shared._Scp.ScpMask; using Content.Shared._Sunrise.TTS; @@ -7,7 +8,6 @@ using Content.Shared.Chat; using Content.Shared.Coordinates.Helpers; using Content.Shared.IdentityManagement; -using Content.Shared.Mobs.Components; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -28,8 +28,6 @@ private void InitializeActions() SubscribeLocalEvent(OnSleepAction); SubscribeLocalEvent(OnGasAction); SubscribeLocalEvent(OnMimic); - - SubscribeLocalEvent(OnEntitySpoke); } private void OnSleepAction(Entity ent, ref Scp939SleepAction args) @@ -93,15 +91,19 @@ private void OnMimic(Entity ent, ref Scp939MimicActionEvent arg /// /// Запоминание последних сказанных возле 939 слов /// - private void OnEntitySpoke(Entity ent, ref EntitySpokeEvent args) + private void TryRememberPhrase(Entity ent, string message) { - var query = _entityLookup.GetEntitiesInRange(Transform(ent).Coordinates, 16f); + using var scp939Set = HashSetPoolEntity.Rent(); + _entityLookup.GetEntitiesInRange(Transform(ent).Coordinates, 16f, scp939Set.Value, LookupFlags.Dynamic | LookupFlags.Approximate); + if (scp939Set.Value.Count == 0) + return; + string? voicePrototype = null; if (TryComp(ent, out var ttsComponent)) voicePrototype = ttsComponent.VoicePrototypeId; - foreach (var scp in query) + foreach (var scp in scp939Set.Value) { if (!_examine.InRangeUnOccluded(ent, scp)) continue; @@ -113,8 +115,7 @@ private void OnEntitySpoke(Entity ent, ref EntitySpokeEvent a } var username = Identity.Name(ent, EntityManager); - scp.Comp.RememberedMessages.TryAdd(args.Message, new(username, voicePrototype)); - Dirty(scp); + scp.Comp.RememberedMessages.TryAdd(message, new(username, voicePrototype)); } } } diff --git a/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs b/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs index b2421d6bb4c..e8817310dbc 100644 --- a/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs +++ b/Content.Server/_Scp/Scp939/Scp939System.Visibility.cs @@ -1,4 +1,3 @@ -using Content.Server.Chat.Systems; using Content.Server.Popups; using Content.Shared._Scp.Scp939; using Content.Shared.Chat; @@ -6,8 +5,8 @@ using Content.Shared.Item; using Content.Shared.Mobs.Components; using Content.Shared.Popups; -using Content.Shared.Standing; using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.Map; using Robust.Shared.Timing; namespace Content.Server._Scp.Scp939; @@ -17,16 +16,26 @@ public sealed partial class Scp939System [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly IGameTiming _timing = default!; + private static readonly TimeSpan VisibilityRefreshInterval = TimeSpan.FromSeconds(0.2f); + + private TimeSpan _nextVisibilityRefresh = TimeSpan.Zero; + private readonly HashSet _visibilityActiveTargets = []; + private readonly HashSet> _visibilityCandidates = []; + private readonly List _visibilityRemovalQueue = []; + + private EntityQuery _activeQuery; + private void InitializeVisibility() { SubscribeLocalEvent(OnMobStartup); - SubscribeLocalEvent(OnTargetSpoke); - SubscribeLocalEvent(OnTargetEmote); - SubscribeLocalEvent(OnDown); + SubscribeLocalEvent(OnTargetSpoke); + SubscribeLocalEvent(OnTargetEmote); SubscribeLocalEvent(OnShot); SubscribeLocalEvent(OnFlash); + + _activeQuery = GetEntityQuery(); } private void OnFlash(Entity ent, ref AfterFlashedEvent args) @@ -37,25 +46,24 @@ private void OnFlash(Entity ent, ref AfterFlashedEvent args) var message = Loc.GetString("scp939-flashed", ("time", ent.Comp.PoorEyesightTime)); _popup.PopupEntity(message, ent, ent, PopupType.MediumCaution); - Dirty(ent); + DirtyFields(ent, + ent.Comp, + null, + nameof(Scp939Component.PoorEyesight), + nameof(Scp939Component.PoorEyesightTimeStart)); } - private void OnTargetEmote(Entity ent, ref EmoteEvent args) - { - MobDidSomething(ent); - } - - private void OnDown(Entity ent, ref DownedEvent args) + private void OnTargetEmote(Entity ent, ref EmoteEvent args) { MobDidSomething(ent); } private void OnShot(Entity ent, ref GunShotEvent args) { - if (!TryComp(args.User, out var scp939VisibilityComponent)) + if (!_activeQuery.TryComp(args.User, out var visibilityComponent)) return; - MobDidSomething((args.User, scp939VisibilityComponent)); + MobDidSomething((args.User, visibilityComponent)); } private void OnMobStartup(Entity ent, ref ComponentStartup args) @@ -63,21 +71,98 @@ private void OnMobStartup(Entity ent, ref ComponentStartup ar if (HasComp(ent)) return; - var visibilityComponent = EnsureComp(ent); - visibilityComponent.VisibilityAcc = 0.001f; - - Dirty(ent, visibilityComponent); + EnsureComp(ent); } - private void OnTargetSpoke(Entity ent, ref EntitySpokeEvent args) + private void OnTargetSpoke(Entity ent, ref EntitySpokeEvent args) { MobDidSomething(ent); + TryRememberPhrase(ent, args.Message); } - // TODO: Перенести на клиент? - private void MobDidSomething(Entity ent) + private void MobDidSomething(Entity ent) { - ent.Comp.VisibilityAcc = 0.001f; - Dirty(ent); + ent.Comp.VisibilityResetCounter++; + DirtyField(ent, ent.Comp, nameof(ActiveScp939VisibilityComponent.VisibilityResetCounter)); + } + + private void UpdateVisibilityTargets() + { + if (_timing.CurTime < _nextVisibilityRefresh) + return; + + _nextVisibilityRefresh = _timing.CurTime + VisibilityRefreshInterval; + _visibilityActiveTargets.Clear(); + + var scpQuery = EntityQueryEnumerator(); + while (scpQuery.MoveNext(out var uid, out var scp939, out var xform)) + { + if (xform.MapID == MapId.Nullspace) + continue; + + _visibilityCandidates.Clear(); + _entityLookup.GetEntitiesInRange(xform.Coordinates, + scp939.VisibilityActivationRange, + _visibilityCandidates, + LookupFlags.Dynamic | LookupFlags.Approximate); + + foreach (var target in _visibilityCandidates) + { + if (target.Owner == uid) + continue; + + _visibilityActiveTargets.Add(target); + EnsureActiveVisibility(target); + } + } + + _visibilityRemovalQueue.Clear(); + + var activeQuery = EntityQueryEnumerator(); + while (activeQuery.MoveNext(out var uid, out _)) + { + if (_visibilityActiveTargets.Contains(uid)) + continue; + + _visibilityRemovalQueue.Add(uid); + } + + foreach (var uid in _visibilityRemovalQueue) + { + RemComp(uid); + } + + _visibilityCandidates.Clear(); + _visibilityRemovalQueue.Clear(); + } + + private void EnsureActiveVisibility(Entity ent) + { + if (!_activeQuery.TryComp(ent, out var active)) + { + active = AddComp(ent); + active.HideTime = ent.Comp.HideTime; + active.MinValue = ent.Comp.MinValue; + active.MaxValue = ent.Comp.MaxValue; + return; + } + + if (!MathHelper.CloseTo(active.HideTime, ent.Comp.HideTime)) + { + active.HideTime = ent.Comp.HideTime; + DirtyField(ent, active, nameof(ActiveScp939VisibilityComponent.HideTime)); + } + + if (active.MinValue != ent.Comp.MinValue) + { + active.MinValue = ent.Comp.MinValue; + DirtyField(ent, active, nameof(ActiveScp939VisibilityComponent.MinValue)); + } + + if (active.MaxValue != ent.Comp.MaxValue) + { + active.MaxValue = ent.Comp.MaxValue; + DirtyField(ent, active, nameof(ActiveScp939VisibilityComponent.MaxValue)); + } } } diff --git a/Content.Server/_Scp/Scp939/Scp939System.cs b/Content.Server/_Scp/Scp939/Scp939System.cs index 596bc383283..f671db25756 100644 --- a/Content.Server/_Scp/Scp939/Scp939System.cs +++ b/Content.Server/_Scp/Scp939/Scp939System.cs @@ -66,19 +66,15 @@ public override void Update(float frameTime) { base.Update(frameTime); - // Все 939, что спят - var querySleeping = EntityQueryEnumerator(); + UpdateVisibilityTargets(); - // Обработка лечения 939 во сне + var querySleeping = EntityQueryEnumerator(); while (querySleeping.MoveNext(out var uid, out var scp939Component, out _)) { _damageableSystem.TryChangeDamage(uid, scp939Component.HibernationHealingRate * frameTime); } - // Просто все 939 var querySimple = EntityQueryEnumerator(); - - // Обработка плохого зрения 939 while (querySimple.MoveNext(out var uid, out var scp939Component)) { if (!scp939Component.PoorEyesight) @@ -94,7 +90,11 @@ public override void Update(float frameTime) scp939Component.PoorEyesight = false; scp939Component.PoorEyesightTimeStart = null; - Dirty(uid, scp939Component); + DirtyFields(uid, + scp939Component, + null, + nameof(Scp939Component.PoorEyesight), + nameof(Scp939Component.PoorEyesightTimeStart)); } } } diff --git a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.EyeClosing.cs b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.EyeClosing.cs index 66ee5110936..e88caf38734 100644 --- a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.EyeClosing.cs +++ b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.EyeClosing.cs @@ -30,7 +30,7 @@ private void InitializeEyeClosing() #region Event handlers - private void OnShutdown(Entity ent, ref ComponentShutdown _) + private void OnShutdown(Entity ent, ref ComponentShutdown args) { _actions.RemoveAction(ent.Owner, ent.Comp.EyeToggleActionEntity); @@ -101,7 +101,7 @@ private void OnTryingSleep(Entity ent, ref TryingToSleepEven /// private void OnHumanoidClosedEyes(Entity ent, ref EntityClosedEyesEvent args) { - if (!TryComp(ent, out var blinkableComponent)) + if (!BlinkableQuery.TryComp(ent, out var blinkableComponent)) return; blinkableComponent.CachedEyesColor = ent.Comp.EyeColor; @@ -114,7 +114,7 @@ private void OnHumanoidClosedEyes(Entity ent, ref E /// private void OnHumanoidOpenedEyes(Entity ent, ref EntityOpenedEyesEvent args) { - if (!TryComp(ent, out var blinkableComponent)) + if (!BlinkableQuery.TryComp(ent, out var blinkableComponent)) return; if (blinkableComponent.CachedEyesColor == null) @@ -152,7 +152,7 @@ public bool TrySetEyelids(Entity ent, bool useEffects = false, TimeSpan? customBlinkDuration = null) { - if (!Resolve(ent, ref ent.Comp)) + if (!BlinkableQuery.Resolve(ent, ref ent.Comp)) return false; if (ent.Comp.State == newState) @@ -192,7 +192,7 @@ public bool CanToggleEyes(Entity ent, EyesState newState) /// True -> закрыты, False -> открыты public bool AreEyesClosed(Entity ent) { - if (!Resolve(ent.Owner, ref ent.Comp, false)) + if (!BlinkableQuery.Resolve(ent.Owner, ref ent.Comp, false)) return false; // Мб одна из проверок лишняя, но ладно пусть будет. Не сильная потеря производительности @@ -207,7 +207,7 @@ public bool AreEyesClosed(Entity ent) /// public bool AreEyesClosedManually(Entity ent) { - if (!Resolve(ent.Owner, ref ent.Comp, false)) + if (!BlinkableQuery.Resolve(ent.Owner, ref ent.Comp, false)) return false; if (ent.Comp.State == EyesState.Opened) @@ -265,9 +265,17 @@ private void SetEyelids(Entity ent, nameof(BlinkableComponent.NextOpenEyesRequiresEffects)); if (newState == EyesState.Closed) - RaiseLocalEvent(ent, new EntityClosedEyesEvent(manual, useEffects, customBlinkDuration)); + { + var closedEvent = new EntityClosedEyesEvent(manual, useEffects, customBlinkDuration); + RaiseLocalEvent(ent, ref closedEvent); + } + else - RaiseLocalEvent(ent, new EntityOpenedEyesEvent(manual, useEffects || openEyesRequiresEffects, customBlinkDuration)); + { + var openedEvent = + new EntityOpenedEyesEvent(manual, useEffects || openEyesRequiresEffects, customBlinkDuration); + RaiseLocalEvent(ent, ref openedEvent); + } if (predicted) RaiseLocalEvent(ent, new EntityEyesStateChanged(oldState, newState, manual)); diff --git a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs index 9d0b0ec5563..e0dc50ecfbe 100644 --- a/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs +++ b/Content.Shared/_Scp/Blinking/SharedBlinkingSystem.cs @@ -1,9 +1,8 @@ using System.Linq; -using Content.Shared._Scp.Scp096.Main.Components; +using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Scp173; using Content.Shared._Scp.Watching; using Content.Shared._Sunrise.Random; -using Content.Shared.Alert; using Content.Shared.Mobs; using Content.Shared.Mobs.Systems; using Robust.Shared.Network; @@ -20,11 +19,12 @@ public abstract partial class SharedBlinkingSystem : EntitySystem { [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly EyeWatchingSystem _watching = default!; - [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly RandomPredictedSystem _random = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly INetManager _net = default!; + protected EntityQuery BlinkableQuery; + public override void Initialize() { base.Initialize(); @@ -35,6 +35,8 @@ public override void Initialize() SubscribeLocalEvent(OnMobStateChanged); InitializeEyeClosing(); + + BlinkableQuery = GetEntityQuery(); } #region Event handlers @@ -107,8 +109,6 @@ public override void Update(float frameTime) { var blinkableEntity = (uid, blinkableComponent); - UpdateAlert(blinkableEntity); - if (TryOpenEyes(blinkableEntity)) continue; @@ -120,7 +120,7 @@ public override void Update(float frameTime) private bool TryBlink(Entity ent, TimeSpan? customDuration = null) { - if (!Resolve(ent.Owner, ref ent.Comp)) + if (!BlinkableQuery.Resolve(ent.Owner, ref ent.Comp)) return false; if (_timing.CurTime < ent.Comp.NextBlink) @@ -142,7 +142,7 @@ private bool TryBlink(Entity ent, TimeSpan? customDuration /// Предугадывается ли клиентом этот вызов метода? Если нет, отправляет клиенту стейт с сервера. public void SetNextBlink(Entity ent, TimeSpan interval, TimeSpan? variance = null, bool predicted = true) { - if (!Resolve(ent, ref ent.Comp)) + if (!BlinkableQuery.Resolve(ent, ref ent.Comp)) return; if (!variance.HasValue) @@ -162,7 +162,7 @@ public void SetNextBlink(Entity ent, TimeSpan interval, Tim public void ResetBlink(Entity ent, bool useVariance = true, bool predicted = true) { - if (!Resolve(ent, ref ent.Comp)) + if (!BlinkableQuery.Resolve(ent, ref ent.Comp)) return; // Если useVariance == false, то variance = 0 @@ -183,7 +183,7 @@ public void ResetBlink(Entity ent, bool useVariance = true, /// public bool IsBlind(Entity ent, bool useTimeCompensation = false) { - if (!Resolve(ent, ref ent.Comp, false)) + if (!BlinkableQuery.Resolve(ent, ref ent.Comp, false)) return false; // Специально для сцп173. Он должен начинать остановку незадолго до того, как у людей откроются глаза @@ -200,7 +200,7 @@ public void ForceBlind(Entity ent, TimeSpan duration, bool if (_mobState.IsIncapacitated(ent)) return; - if (!Resolve(ent, ref ent.Comp)) + if (!BlinkableQuery.Resolve(ent, ref ent.Comp)) return; // Если у персонажа уже закрыты глаза, то обновляем время @@ -226,25 +226,6 @@ private TimeSpan GetBlinkVariance(Entity ent) #endregion - /// - /// Актуализирует иконку моргания справа у панели чата игрока - /// - protected void UpdateAlert(Entity ent) - { - // Если в данный момент глаза закрыты, то выставляем иконку с закрытым глазом - if (IsBlind(ent.AsNullable())) - { - _alerts.ShowAlert(ent.Owner, ent.Comp.BlinkingAlert, 4); - return; - } - - var timeToNextBlink = ent.Comp.NextBlink - _timing.CurTime; - var denom = MathF.Max(0.001f, (float)(ent.Comp.BlinkingInterval.TotalSeconds - ent.Comp.BlinkingDuration.TotalSeconds)); - var severity = (short) Math.Clamp(4 - (float) timeToNextBlink.TotalSeconds / denom * 4, 0, 4); - - _alerts.ShowAlert(ent.Owner, ent.Comp.BlinkingAlert, severity); - } - /// /// Проверяет, есть ли рядом с игроком Scp, использующий механики зрения /// @@ -255,27 +236,25 @@ protected void UpdateAlert(Entity ent) protected bool IsScpNearby(EntityUid player) { // Получаем всех Scp с механиками зрения, которые видят игрока - var allScp173InView = _watching.GetAllVisibleTo(player); - var allScp096InView = _watching.GetAllVisibleTo(player); + using var scp173List = ListPoolEntity.Rent(); + if (!_watching.TryGetAllEntitiesVisibleTo(player, scp173List.Value, flags: LookupFlags.Dynamic | LookupFlags.Approximate)) + return false; - return allScp173InView.Any(e => _watching.CanBeWatched(player, e)) - || allScp096InView.Any(e => _watching.CanBeWatched(player, e)); + return scp173List.Value.Any(e => _watching.CanBeWatched(player, e)); } } -public sealed class EntityOpenedEyesEvent(bool manual = false, bool useEffects = false, TimeSpan? customNextTimeBlinkInterval = null) : EntityEventArgs -{ - public readonly bool Manual = manual; - public readonly bool UseEffects = useEffects; - public readonly TimeSpan? CustomNextTimeBlinkInterval = customNextTimeBlinkInterval; -} - -public sealed class EntityClosedEyesEvent(bool manual = false, bool useEffects = false, TimeSpan? customBlinkDuration = null) : EntityEventArgs -{ - public readonly bool Manual = manual; - public readonly bool UseEffects = useEffects; - public readonly TimeSpan? CustomBlinkDuration = customBlinkDuration; -} +[ByRefEvent] +public readonly record struct EntityOpenedEyesEvent( + bool Manual = false, + bool UseEffects = false, + TimeSpan? CustomNextTimeBlinkInterval = null); + +[ByRefEvent] +public readonly record struct EntityClosedEyesEvent( + bool Manual = false, + bool UseEffects = false, + TimeSpan? CustomBlinkDuration = null); [Serializable, NetSerializable] public sealed class EntityEyesStateChanged(EyesState oldState, EyesState newState, bool manual = false, bool useEffects = false, NetEntity? netEntity = null) : EntityEventArgs diff --git a/Content.Shared/_Scp/Fear/Components/ActiveCloseFearComponent.cs b/Content.Shared/_Scp/Fear/Components/ActiveCloseFearComponent.cs new file mode 100644 index 00000000000..3b47b0502d2 --- /dev/null +++ b/Content.Shared/_Scp/Fear/Components/ActiveCloseFearComponent.cs @@ -0,0 +1,15 @@ +namespace Content.Shared._Scp.Fear.Components; + +/// +/// Runtime-компонент активного "страха от близости". +/// Существует только пока у сущности реально активны close-fear эффекты. +/// +[RegisterComponent] +public sealed partial class ActiveCloseFearComponent : Component +{ + /// + /// Источник страха, который сейчас применяется к сущности. + /// + [ViewVariables] + public EntityUid Source; +} diff --git a/Content.Shared/_Scp/Fear/Components/ActiveFearFallOffComponent.cs b/Content.Shared/_Scp/Fear/Components/ActiveFearFallOffComponent.cs new file mode 100644 index 00000000000..b588852cad3 --- /dev/null +++ b/Content.Shared/_Scp/Fear/Components/ActiveFearFallOffComponent.cs @@ -0,0 +1,28 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Scp.Fear.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +public sealed partial class ActiveFearFallOffComponent : Component +{ + /// + /// Шанс упасть при хождении во время страха постигшего + /// + [DataField, AutoNetworkedField] + public float FallOffChance = 0.03f; + + /// + /// Время между проверками на возможность запнуться. + /// + [DataField] + public TimeSpan FallOffCheckInterval = TimeSpan.FromSeconds(0.3f); + + /// + /// Время следующей проверки на возможность запнуться при высоком уровне страха. + /// + [AutoNetworkedField, ViewVariables, AutoPausedField] + public TimeSpan? FallOffNextCheckTime; + + [DataField] + public TimeSpan FallOffTime = TimeSpan.FromSeconds(0.5f); +} diff --git a/Content.Shared/_Scp/Fear/Components/FearActiveSoundEffectsComponent.cs b/Content.Shared/_Scp/Fear/Components/FearActiveSoundEffectsComponent.cs index f2cdf368bb9..a14c145a34e 100644 --- a/Content.Shared/_Scp/Fear/Components/FearActiveSoundEffectsComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/FearActiveSoundEffectsComponent.cs @@ -6,7 +6,7 @@ namespace Content.Shared._Scp.Fear.Components; /// /// Компонент, отвечающий за звуковые эффекты страха при приближении к источнику. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] public sealed partial class FearActiveSoundEffectsComponent : Component { /// @@ -61,5 +61,4 @@ public sealed partial class FearActiveSoundEffectsComponent : Component public EntityUid? BreathingAudioStream; #endregion - } diff --git a/Content.Shared/_Scp/Fear/Components/FearComponent.cs b/Content.Shared/_Scp/Fear/Components/FearComponent.cs index 345600baf20..441e372066a 100644 --- a/Content.Shared/_Scp/Fear/Components/FearComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/FearComponent.cs @@ -8,7 +8,7 @@ namespace Content.Shared._Scp.Fear.Components; /// Компонент, отвечающий за возможность пугаться. /// Обрабатывает уровни страха и хранит текущий. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] public sealed partial class FearComponent : Component { /// @@ -143,19 +143,7 @@ public sealed partial class FearComponent : Component /// Шанс упасть при хождении во время страха постигшего /// [DataField, AutoNetworkedField] - public float FallOffChance = 3f; // 3% - - /// - /// Время следующей проверки на возможность запнуться при высоком уровне страха. - /// - [ViewVariables] - public TimeSpan FallOffNextCheckTime = TimeSpan.Zero; - - /// - /// Время между проверками на возможность запнуться. - /// - [DataField] - public TimeSpan FallOffCheckInterval = TimeSpan.FromSeconds(0.3f); + public float FallOffChance = 0.03f; /// /// Какой уровень страха нужен, чтобы у человека появился адреналин. diff --git a/Content.Shared/_Scp/Fear/Components/FearSourceComponent.cs b/Content.Shared/_Scp/Fear/Components/FearSourceComponent.cs index dba3e333c57..192a75423d1 100644 --- a/Content.Shared/_Scp/Fear/Components/FearSourceComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/FearSourceComponent.cs @@ -9,7 +9,7 @@ namespace Content.Shared._Scp.Fear.Components; /// Сущность с этим компонентом будет являться источников страха для игрока. /// Здесь настраивается, какой уровень страха будет вызван у игрока при разных обстоятельствах. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] public sealed partial class FearSourceComponent : Component { [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] diff --git a/Content.Shared/_Scp/Fear/Components/Fears/HemophobiaComponent.cs b/Content.Shared/_Scp/Fear/Components/Fears/HemophobiaComponent.cs index d5ef761bc87..1b0b65f10b2 100644 --- a/Content.Shared/_Scp/Fear/Components/Fears/HemophobiaComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/Fears/HemophobiaComponent.cs @@ -8,7 +8,7 @@ namespace Content.Shared._Scp.Fear.Components.Fears; /// /// Компонент, отвечающий за страх перед кровью /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] public sealed partial class HemophobiaComponent : Component { /// diff --git a/Content.Shared/_Scp/Fear/Components/Traits/FearFaintingComponent.cs b/Content.Shared/_Scp/Fear/Components/Traits/FearFaintingComponent.cs index 2c32b4cc1db..afa361dd4ed 100644 --- a/Content.Shared/_Scp/Fear/Components/Traits/FearFaintingComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/Traits/FearFaintingComponent.cs @@ -12,20 +12,21 @@ public sealed partial class FearFaintingComponent : Component /// /// Требуемый уровень страха для падения в обморок /// - [DataField, ViewVariables] + [DataField] public FearState RequiredState = FearState.Fear; /// /// Время, которое персонаж проведет в обмороке /// - [DataField, ViewVariables] + [DataField] public TimeSpan Time = TimeSpan.FromSeconds(20f); /// /// Шанс упасть в обморок при достижении /// - [DataField, ViewVariables] - public float Chance = 30f; + [DataField] + public float Chance = 0.3f; - public static readonly EntProtoId StatusEffect = "StatusEffectForcedSleeping"; + [DataField] + public EntProtoId StatusEffect = "StatusEffectForcedSleeping"; } diff --git a/Content.Shared/_Scp/Fear/Components/Traits/FearStuporComponent.cs b/Content.Shared/_Scp/Fear/Components/Traits/FearStuporComponent.cs index b2638618c3e..7a3a95e5e58 100644 --- a/Content.Shared/_Scp/Fear/Components/Traits/FearStuporComponent.cs +++ b/Content.Shared/_Scp/Fear/Components/Traits/FearStuporComponent.cs @@ -9,14 +9,15 @@ namespace Content.Shared._Scp.Fear.Components.Traits; [RegisterComponent, NetworkedComponent] public sealed partial class FearStuporComponent : Component { - [DataField, ViewVariables] + [DataField] public FearState RequiredState = FearState.Fear; - [DataField, ViewVariables] - public float Chance = 10f; + [DataField] + public float Chance = 0.1f; - [DataField, ViewVariables] + [DataField] public TimeSpan StuporTime = TimeSpan.FromSeconds(10f); - public static readonly EntProtoId StatusEffect = "StatusEffectFearStupor"; + [DataField] + public EntProtoId StatusEffect = "StatusEffectFearStupor"; } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.CloseFear.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.CloseFear.cs new file mode 100644 index 00000000000..8e4f671a20a --- /dev/null +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.CloseFear.cs @@ -0,0 +1,144 @@ +using Content.Shared._Scp.Fear.Components; +using Content.Shared._Scp.Proximity; +using Content.Shared._Scp.Shaders.Grain; +using Content.Shared._Scp.Shaders.Vignette; + +namespace Content.Shared._Scp.Fear.Systems; + +public abstract partial class SharedFearSystem +{ + private EntityQuery _activeCloseFearQuery; + + private void InitializeCloseFear() + { + SubscribeLocalEvent(OnActiveProximityShutdown); + + _activeCloseFearQuery = GetEntityQuery(); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + UpdateCloseFear(); + } + + private void UpdateCloseFear() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var proximity, out var fear)) + { + SyncCloseFear((uid, fear), (uid, proximity)); + } + } + + private void SyncCloseFear(Entity ent, Entity proximity) + { + if (!_mobState.IsAlive(ent)) + { + ClearCloseFear(ent); + return; + } + + if (!_fearSourceQuery.TryComp(proximity.Comp.Receiver, out var source)) + { + ClearCloseFear(ent); + return; + } + + if (source.UponComeCloser == FearState.None) + { + ClearCloseFear(ent); + return; + } + + if (source.PhobiaType.HasValue && !ent.Comp.Phobias.Contains(source.PhobiaType.Value)) + { + ClearCloseFear(ent); + return; + } + + var blockerLevel = _proximity.GetLightOfSightBlockerLevel(proximity.Comp.Receiver, ent); + if (blockerLevel > ent.Comp.ProximityBlockerLevel) + { + ClearCloseFear(ent); + return; + } + + if (!_watching.IsWatchedBy(proximity.Comp.Receiver, ent, checkProximity: false, useFov: false, checkBlinking: false)) + { + ClearCloseFear(ent); + return; + } + + var sourceCoords = Transform(proximity.Comp.Receiver).Coordinates; + var targetCoords = Transform(ent).Coordinates; + if (!sourceCoords.TryDistance(EntityManager, targetCoords, out var range)) + { + ClearCloseFear(ent); + return; + } + + if (range > proximity.Comp.CloseRange) + { + ClearCloseFear(ent); + return; + } + + var hadActive = _activeCloseFearQuery.TryComp(ent, out var activeCloseFear); + activeCloseFear ??= EnsureComp(ent); + var sourceChanged = hadActive && activeCloseFear.Source != proximity.Comp.Receiver; + + if (!hadActive) + { + AddNegativeMoodEffect(ent, MoodSourceClose); + } + else if (sourceChanged) + { + RemoveSoundEffects(ent.Owner); + } + + activeCloseFear.Source = proximity.Comp.Receiver; + + StartEffects(ent, source.PlayHeartbeatSound, source.PlayBreathingSound); + + if (ent.Comp.State < source.UponComeCloser) + TrySetFearLevel(ent.AsNullable(), source.UponComeCloser); + + RecalculateEffectsStrength(ent.Owner, range, proximity.Comp.CloseRange); + + SetRangeBasedShaderStrength( + ent.Owner, + range, + proximity.Comp.CloseRange, + source.GrainShaderStrength, + blockerLevel, + ent.Comp); + + SetRangeBasedShaderStrength( + ent.Owner, + range, + proximity.Comp.CloseRange, + source.VignetteShaderStrength, + blockerLevel, + ent.Comp); + } + + private void OnActiveProximityShutdown(Entity ent, ref ComponentShutdown args) + { + if (!_fearQuery.TryComp(ent, out var fear)) + return; + + ClearCloseFear((ent.Owner, fear)); + } + + private void ClearCloseFear(Entity ent) + { + RemComp(ent); + + SetFearBasedShaderStrength(ent); + + RemoveSoundEffects(ent.Owner); + RemoveCloseFearMood(ent.Owner); + } +} diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs index 0de7aee9aae..39ed91ba066 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Fears.cs @@ -22,11 +22,16 @@ private void OnHemophobiaInit(Entity ent, ref ComponentStar .OrderBy(kv => kv.Value) .ToList(); - if (!TryComp(ent, out var fearComponent)) + if (!_fearQuery.TryComp(ent, out var fearComponent)) + { + Log.Warning($"Found entity {ToPrettyString(ent)} with {nameof(HemophobiaComponent)} but without {nameof(FearComponent)}! {nameof(HemophobiaComponent)} will be deleted"); + RemComp(ent); + return; + } fearComponent.Phobias.Add(ent.Comp.Phobia); - Dirty(ent, fearComponent); + DirtyField(ent, fearComponent, nameof(FearComponent.Phobias)); } /// @@ -35,13 +40,10 @@ private void OnHemophobiaInit(Entity ent, ref ComponentStar /// private void OnHemophobiaShutdown(Entity ent, ref ComponentShutdown args) { - if (TerminatingOrDeleted(ent)) - return; - - if (!TryComp(ent, out var fearComponent)) + if (!_fearQuery.TryComp(ent, out var fearComponent)) return; fearComponent.Phobias.Remove(ent.Comp.Phobia); - Dirty(ent, fearComponent); + DirtyField(ent, fearComponent, nameof(FearComponent.Phobias)); } } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Gameplay.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Gameplay.cs index e35385a3841..199314d82ee 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Gameplay.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Gameplay.cs @@ -1,10 +1,12 @@ using Content.Shared._Scp.Fear.Components; -using Content.Shared._Scp.Fear.Components.Traits; using Content.Shared._Scp.Weapons.Ranged; using Content.Shared._Sunrise.Mood; +using Content.Shared._Sunrise.Random; using Content.Shared.Drunk; using Content.Shared.Jittering; +using Content.Shared.Standing; using Content.Shared.StatusEffectNew; +using Content.Shared.Stunnable; using Robust.Shared.Prototypes; namespace Content.Shared._Scp.Fear.Systems; @@ -13,6 +15,9 @@ public abstract partial class SharedFearSystem { [Dependency] private readonly StatusEffectsSystem _effects = default!; [Dependency] private readonly SharedJitteringSystem _jittering = default!; + [Dependency] private readonly StandingStateSystem _standing = default!; + [Dependency] private readonly SharedStunSystem _stun = default!; + [Dependency] private readonly RandomPredictedSystem _random = default!; private const float BaseJitteringAmplitude = 1f; private const float BaseJitteringFrequency = 4f; @@ -35,6 +40,30 @@ public abstract partial class SharedFearSystem private void InitializeGameplay() { _drunkQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnFallOffMapInit); + SubscribeLocalEvent(OnFallOffMove); + } + + private void OnFallOffMapInit(Entity ent, ref MapInitEvent args) + { + SetNextFallOffTime(ent); + } + + private void OnFallOffMove(Entity ent, ref MoveEvent args) + { + if (_timing.CurTime < ent.Comp.FallOffNextCheckTime) + return; + + SetNextFallOffTime(ent); // Даже если не прокнет, то время все равно должно устанавливаться + + if (!_random.ProbForEntity(ent, ent.Comp.FallOffChance)) + return; + + if (_standing.IsDown(ent.Owner)) + return; + + _stun.TryAddParalyzeDuration(ent.Owner, ent.Comp.FallOffTime); } /// @@ -68,10 +97,7 @@ private void SetSpreadParameters(EntityUid uid, float angleIncrease, float maxAn private void ManageJitter(Entity ent) { // При ступоре и обмороке персонаж не должен трястись - if (_effects.HasStatusEffect(ent, FearStuporComponent.StatusEffect)) - return; - - if (_effects.HasStatusEffect(ent, FearFaintingComponent.StatusEffect)) + if (_fearFaintingQuery.HasComp(ent) || _fearStuporQuery.HasComp(ent)) return; if (MathHelper.CloseTo(ent.Comp.BaseJitterTime, 0f)) @@ -155,4 +181,30 @@ private float GetDrunkModifier(EntityUid uid) } protected virtual void TryScream(Entity ent) {} + + private void ManageFallOff(Entity ent) + { + if (ent.Comp.State >= ent.Comp.FallOffRequiredState) + { + if (HasComp(ent)) + return; + + var comp = EnsureComp(ent); + comp.FallOffChance = ent.Comp.FallOffChance; + Dirty(ent, comp); + } + else + { + RemComp(ent); + } + } + + /// + /// Устанавливает следующее время возможности запнуться. + /// + private void SetNextFallOffTime(Entity ent) + { + ent.Comp.FallOffNextCheckTime = _timing.CurTime + ent.Comp.FallOffCheckInterval; + Dirty(ent); + } } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs index 5dbb0d18911..b5be92ead45 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Helpers.cs @@ -1,15 +1,11 @@ -using System.Threading; -using Content.Shared._Scp.Fear.Components; +using Content.Shared._Scp.Fear.Components; using Content.Shared._Scp.Helpers; using Content.Shared._Scp.Shaders; -using Timer = Robust.Shared.Timing.Timer; namespace Content.Shared._Scp.Fear.Systems; public abstract partial class SharedFearSystem { - private static CancellationTokenSource _restartToken = new (); - private const int MinPossibleValue = (int) FearState.None; private const int MaxPossibleValue = (int) FearState.Terror; @@ -21,10 +17,11 @@ public abstract partial class SharedFearSystem /// private void HighLightAllVisibleFears(Entity ent) { - var visibleFearSources = - _watching.GetAllEntitiesVisibleTo(ent.Owner, ent.Comp.SeenBlockerLevel); + using var fearSources = ListPoolEntity.Rent(); + if (!_watching.TryGetWatchingTargets(ent.Owner, fearSources.Value, ent.Comp.SeenBlockerLevel)) + return; - foreach (var source in visibleFearSources) + foreach (var source in fearSources.Value) { if (source.Comp.UponSeenState != ent.Comp.State) continue; @@ -124,17 +121,6 @@ public static int GetGenericFearBasedModifier(FearState state, int scale = Gener protected void SetNextCalmDownTime(Entity ent) { ent.Comp.NextTimeDecreaseFearLevel = _timing.CurTime + ent.Comp.TimeToDecreaseFearLevel; - Dirty(ent); - } - - protected void RemoveComponentAfter(EntityUid ent, float removeAfter) where T : IComponent - { - Timer.Spawn(TimeSpan.FromSeconds(removeAfter), () => RemComp(ent), _restartToken.Token); - } - - protected void RemoveComponentAfter(EntityUid ent, TimeSpan removeAfter) where T : IComponent - { - Timer.Spawn(removeAfter, () => RemComp(ent), _restartToken.Token); } /// @@ -165,18 +151,4 @@ protected void RemoveComponentAfter(EntityUid ent, TimeSpan removeAfter) wher _ => null, }; - - /// - /// Преобразует процент из человеческого формата в probный. - /// - protected static float PercentToNormalized(float percent) - { - return Math.Clamp(percent / 100f, 0f, 1f); - } - - protected virtual void Clear() - { - _restartToken.Cancel(); - _restartToken = new(); - } } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.SoundEffects.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.SoundEffects.cs index 6bb94e495fb..25ef79214b2 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.SoundEffects.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.SoundEffects.cs @@ -15,6 +15,7 @@ public abstract partial class SharedFearSystem public const float MaximumAdditionalVolume = 16f; protected virtual void StartBreathing(Entity ent) {} + protected virtual void StopBreathing(Entity ent) {} protected virtual void StartHeartBeat(Entity ent) {} @@ -32,17 +33,32 @@ protected virtual void PlayFearStateSound(Entity ent, FearState o /// Проигрывать звук дыхания? private void StartEffects(EntityUid uid, bool playHeartbeatSound, bool playBreathingSound) { - if (HasComp(uid)) - return; + var existed = TryComp(uid, out var effects); + effects ??= EnsureComp(uid); + + var heartbeatChanged = effects.PlayHeartbeatSound != playHeartbeatSound; + var breathingChanged = effects.PlayBreathingSound != playBreathingSound; - var effects = EnsureComp(uid); effects.PlayHeartbeatSound = playHeartbeatSound; effects.PlayBreathingSound = playBreathingSound; - Dirty(uid, effects); + if (!existed || heartbeatChanged || breathingChanged) + { + DirtyFields(uid, + effects, + null, + nameof(FearActiveSoundEffectsComponent.PlayHeartbeatSound), + nameof(FearActiveSoundEffectsComponent.PlayBreathingSound)); + } + + if (!existed || (heartbeatChanged && playHeartbeatSound)) + StartHeartBeat((uid, effects)); + + if (!existed || (breathingChanged && playBreathingSound)) + StartBreathing((uid, effects)); - StartBreathing((uid, effects)); - StartHeartBeat((uid, effects)); + if (existed && breathingChanged && !playBreathingSound) + StopBreathing((uid, effects)); } /// @@ -57,18 +73,31 @@ private void RecalculateEffectsStrength(Entity var cooldown = CalculateStrength(currentRange, maxRange, HeartBeatMinimumCooldown, HeartBeatMaximumCooldown); var currentPitch = CalculateStrength(currentRange, maxRange, HeartBeatMinimumPitch, HeartBeatMaximumPitch); + var currentCooldown = TimeSpan.FromSeconds(cooldown); + + if (MathF.Abs(ent.Comp.AdditionalVolume - volume) < 0.05f + && MathF.Abs(ent.Comp.Pitch - currentPitch) < 0.01f + && Math.Abs((ent.Comp.NextHeartbeatCooldown - currentCooldown).TotalMilliseconds) < 25) + { + return; + } ent.Comp.AdditionalVolume = volume; ent.Comp.Pitch = currentPitch; - ent.Comp.NextHeartbeatCooldown = TimeSpan.FromSeconds(cooldown); - - Dirty(ent); + ent.Comp.NextHeartbeatCooldown = currentCooldown; + + DirtyFields(ent, + ent.Comp, + null, + nameof(FearActiveSoundEffectsComponent.AdditionalVolume), + nameof(FearActiveSoundEffectsComponent.Pitch), + nameof(FearActiveSoundEffectsComponent.NextHeartbeatCooldown)); } /// /// Убирает все звуковые эффекты. /// - private void RemoveEffects(EntityUid uid) + private void RemoveSoundEffects(EntityUid uid) { RemComp(uid); } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Traits.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Traits.cs index 431b91e8bb7..f31615c399c 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Traits.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.Traits.cs @@ -1,9 +1,15 @@ -namespace Content.Shared._Scp.Fear.Systems; +using Content.Shared._Scp.Fear.Components.Traits; + +namespace Content.Shared._Scp.Fear.Systems; public abstract partial class SharedFearSystem { + private EntityQuery _fearStuporQuery; + private EntityQuery _fearFaintingQuery; + private void InitializeTraits() { - + _fearStuporQuery = GetEntityQuery(); + _fearFaintingQuery = GetEntityQuery(); } } diff --git a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs index ecfee58a2ea..66a24b0c2c1 100644 --- a/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs +++ b/Content.Shared/_Scp/Fear/Systems/SharedFearSystem.cs @@ -8,7 +8,6 @@ using Content.Shared._Scp.Watching; using Content.Shared._Sunrise.Mood; using Content.Shared.Examine; -using Content.Shared.GameTicking; using Content.Shared.Mobs.Systems; using Content.Shared.Rejuvenate; using Robust.Shared.Timing; @@ -30,20 +29,20 @@ public abstract partial class SharedFearSystem : EntitySystem [Dependency] private readonly EyeWatchingSystem _watching = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedShaderStrengthSystem _shaderStrength = default!; + [Dependency] private readonly ProximitySystem _proximity = default!; [Dependency] private readonly IGameTiming _timing = default!; private const string MoodSourceClose = "FearSourceClose"; private const string MoodFearSourceSeen = "FearSourceSeen"; - private EntityQuery _fears; + private EntityQuery _fearSourceQuery; + private EntityQuery _fearQuery; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnEntityLookedAt); - SubscribeLocalEvent(OnProximityInRange); - SubscribeLocalEvent(OnProximityNotInRange); SubscribeLocalEvent(OnFearStateChanged); @@ -52,13 +51,13 @@ public override void Initialize() SubscribeLocalEvent(OnExamine); - SubscribeLocalEvent(_ => Clear()); - InitializeFears(); InitializeGameplay(); InitializeTraits(); + InitializeCloseFear(); - _fears = GetEntityQuery(); + _fearSourceQuery = GetEntityQuery(); + _fearQuery = GetEntityQuery(); } /// @@ -67,16 +66,13 @@ public override void Initialize() /// private void OnEntityLookedAt(Entity ent, ref EntityLookedAtEvent args) { - if (!_timing.IsFirstTimePredicted) - return; - if (!_mobState.IsAlive(ent)) return; // Проверка на видимость. // Это нужно, чтобы можно было не пугаться через стекло, например. // Это будет использовано, например, у ученых, которые 100 лет видели сцп через стекла и не должны пугаться. - if (args.BlockerLevel > ent.Comp.SeenBlockerLevel) + if (!_proximity.IsRightType(args.Target, ent, ent.Comp.SeenBlockerLevel)) return; if (!args.Target.Comp.AlreadyLookedAt.TryGetValue(GetNetEntity(ent), out var lastSeenTime)) @@ -88,7 +84,7 @@ private void OnEntityLookedAt(Entity ent, ref EntityLookedAtEvent && _timing.CurTime < lastSeenTime + ent.Comp.TimeToGetScaredAgainOnLookAt) return; - if (!_fears.TryComp(args.Target, out var source)) + if (!_fearSourceQuery.TryComp(args.Target, out var source)) return; if (source.UponSeenState == FearState.None) @@ -110,101 +106,17 @@ private void OnEntityLookedAt(Entity ent, ref EntityLookedAtEvent HighLightAllVisibleFears(ent); } - /// - /// Обрабатывает событие, когда игрок находится вблизи с источником страха. - /// Нужен, чтобы включить разные страшные эффекты. - /// - private void OnProximityInRange(Entity ent, ref ProximityInRangeTargetEvent args) - { - if (!_timing.IsFirstTimePredicted) - return; - - if (!_mobState.IsAlive(ent)) - return; - - // Проверка на видимость. - // Это нужно, чтобы можно было не пугаться через стекло, например. - // Это будет использовано, например, у ученых, которые 100 лет видели сцп через стекла и не должны пугаться. - if (args.Type > ent.Comp.ProximityBlockerLevel) - return; - - if (!_fears.TryComp(args.Receiver, out var source)) - return; - - if (source.UponComeCloser == FearState.None) - return; - - // Если в списке фобий персонажа нет фобии, которой является источник, то мы ее не боимся - if (source.PhobiaType.HasValue && !ent.Comp.Phobias.Contains(source.PhobiaType.Value)) - return; - - // Проверка на зрение, чтобы можно было закрыть глазки и было не страшно - if (!_watching.SimpleIsWatchedBy(args.Receiver, [ent])) - return; - - // Если текущий уровень страха выше, чем тот, что мы хотим поставить, - // то мы не должны его ставить. - if (ent.Comp.State < source.UponComeCloser) - TrySetFearLevel(ent.AsNullable(), source.UponComeCloser); - - StartEffects(ent, source.PlayHeartbeatSound, source.PlayBreathingSound); - RecalculateEffectsStrength(ent.Owner, args.Range, args.CloseRange); - - SetRangeBasedShaderStrength(ent.Owner, - args.Range, - args.CloseRange, - source.GrainShaderStrength, - args.Type, - ent.Comp); - - SetRangeBasedShaderStrength(ent.Owner, - args.Range, - args.CloseRange, - source.VignetteShaderStrength, - args.Type, - ent.Comp); - - AddNegativeMoodEffect(ent, MoodSourceClose); - } - - /// - /// Обрабатывает событие, когда сущность НЕ находится рядом с источником страха. - /// Нужен, чтобы выключить эффекты от источника страха. - /// - private void OnProximityNotInRange(Entity ent, ref ProximityNotInRangeTargetEvent args) - { - // Оказывается этот метод фиксит серверные проблемы, - // из-за которых изменение уровня страха на сервере не меняет параметры оверлеев на клиенте. - // Благодаря тому, что оно вызывается постоянно, это создает костыль, который закрывает проблему. Вот оно как. - // TODO: Избавиться от этого костыля когда-нибудь и реализовать Net версию изменения уровня страха - if (!_timing.IsFirstTimePredicted) - return; - - if (!_mobState.IsAlive(ent)) - return; - - // Как только игрок отходит от источника страха он должен перестать бояться - // Но значения шейдера от уровня страха должны продолжать действовать, что и учитывает метод - SetShaderStrength(ent.Owner, ent.Comp, 0f); - SetShaderStrength(ent.Owner, ent.Comp, 0f); - - RemoveEffects(ent); - RaiseLocalEvent(ent, new MoodRemoveEffectEvent(MoodSourceClose)); - } - /// /// Обрабатывает событие изменения уровня страха у персонажа. /// private void OnFearStateChanged(Entity ent, ref FearStateChangedEvent args) { - if (!_timing.IsFirstTimePredicted) - return; - PlayFearStateSound(ent, args.OldState); // Добавляем геймплейные проблемы, завязанный на уровне страха ManageShootingProblems(ent); ManageStateBasedMood(ent); + ManageFallOff(ent); // Проверка на то, что уровень понизился -> мы успокоились. // Геймплейные штуки ниже не нужно триггерить. @@ -218,20 +130,12 @@ private void OnFearStateChanged(Entity ent, ref FearStateChangedE protected virtual void OnShutdown(Entity ent, ref ComponentShutdown args) { - _shaderStrength.TrySetAdditionalStrength(ent.Owner, 0f); - _shaderStrength.TrySetAdditionalStrength(ent.Owner, 0f); - - RemoveEffects(ent.Owner); - - RaiseLocalEvent(ent, new MoodRemoveEffectEvent(MoodFearSourceSeen)); - RaiseLocalEvent(ent, new MoodRemoveEffectEvent(MoodSourceClose)); + CleanupFear(ent); } protected virtual void OnRejuvenate(Entity ent, ref RejuvenateEvent args) { - TrySetFearLevel(ent.AsNullable(), FearState.None); - - RaiseLocalEvent(ent, new MoodRemoveEffectEvent(MoodFearSourceSeen)); + ResetFear(ent); } /// @@ -275,7 +179,7 @@ public bool TrySetFearLevel(Entity ent, FearState state) SetFearBasedShaderStrength(entity); SetNextCalmDownTime(entity); - Dirty(ent); + DirtyField(ent, ent.Comp, nameof(FearComponent.State)); return true; } @@ -294,7 +198,31 @@ private void SetFearBasedShaderStrength(Entity ent) ent.Comp.CurrentFearBasedShaderStrength[nameof(GrainOverlayComponent)] = grainStrength; ent.Comp.CurrentFearBasedShaderStrength[nameof(VignetteOverlayComponent)] = vignetteStrength; - Dirty(ent); + DirtyField(ent, ent.Comp, nameof(FearComponent.CurrentFearBasedShaderStrength)); + } + + private void ResetFear(Entity ent) + { + TrySetFearLevel(ent.AsNullable(), FearState.None); + CleanupFear(ent); + } + + private void CleanupFear(Entity ent) + { + ClearCloseFear(ent); + + RemoveSeenFearMood(ent); + WipeMood(ent); + } + + private void RemoveSeenFearMood(EntityUid uid) + { + RaiseLocalEvent(uid, new MoodRemoveEffectEvent(MoodFearSourceSeen)); + } + + private void RemoveCloseFearMood(EntityUid uid) + { + RaiseLocalEvent(uid, new MoodRemoveEffectEvent(MoodSourceClose)); } /// diff --git a/Content.Shared/_Scp/Helpers/CollectionPool.cs b/Content.Shared/_Scp/Helpers/CollectionPool.cs new file mode 100644 index 00000000000..1a073bf58d7 --- /dev/null +++ b/Content.Shared/_Scp/Helpers/CollectionPool.cs @@ -0,0 +1,190 @@ +using System.Runtime.CompilerServices; + +namespace Content.Shared._Scp.Helpers; + +/// +/// Provides a static object pool for collections to minimize garbage collection allocations. +/// +/// The type of the collection being pooled. Must implement . +/// The type of the elements contained in the collection. +public static class CollectionPool + where TCollection : class, ICollection +{ + private static readonly Stack Pool = new(); + private static Func? _factory; + + /// + /// Configures the factory function used to instantiate new collections when the pool is empty. + /// + /// The delegate used to create new instances of . + /// Thrown when the provided is null. + public static void Configure(Func factory) + { + _factory = factory ?? throw new InvalidOperationException("Factory cannot be null"); + } + + /// + /// Creates a new collection using the configured factory. + /// + /// A new instance of . + /// Thrown if the pool has not been configured via . + private static TCollection Create() + { + if (_factory is null) + { + throw new InvalidOperationException( + $"CollectionPool<{typeof(TCollection).Name}, {typeof(T).Name}> " + + $"is not configured. Call Configure(factory) before use."); + } + + return _factory(); + } + + /// + /// Rents a collection from the pool. If the pool is empty, a new collection is created. + /// + /// A disposable wrapper. Use within a statement to automatically return the collection to the pool. + public static PooledCollection Rent() + { + return Pool.TryPop(out var collection) + ? new PooledCollection(collection) + : new PooledCollection(Create()); + } + + /// + /// Returns a collection to the pool. Clears the collection before storing it. + /// Collections will be dropped and garbage collected if the pool is full (>= 512 items) or if the collection capacity exceeds 2048. + /// + /// The collection to return to the pool. + internal static void Return(TCollection collection) + { + if (Pool.Count >= 512) + return; + + if (collection is List list && list.Capacity > 2048) + return; + + if (collection is HashSet hashSet && hashSet.Capacity > 2048) + return; + + collection.Clear(); + Pool.Push(collection); + } + + /// + /// An allocation-free disposable wrapper around a rented collection. + /// + public struct PooledCollection : IDisposable + { + private TCollection? _value; + private bool _disposed; + + /// + /// Gets the underlying rented collection. + /// + /// Thrown if accessed after the collection has been returned to the pool. + public TCollection Value + { + get + { + if (_disposed) + throw new InvalidOperationException("The collection has already been returned to the pool."); + + return _value!; + } + } + + internal PooledCollection(TCollection value) + { + _value = value; + _disposed = false; + } + + /// + /// Returns the underlying collection to the pool and marks this wrapper as disposed. + /// + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + + if (_value is not null) + { + Return(_value); + _value = default; + } + } + } +} + +/// +/// A pre-configured pool specifically for instances. +/// +/// The type of elements in the list. +public static class ListPool +{ + static ListPool() + { + CollectionPool, T>.Configure(() => new List()); + } + + /// + /// Rents a list from the pool. + /// + /// A disposable wrapper containing the rented list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CollectionPool, T>.PooledCollection Rent() + => CollectionPool, T>.Rent(); +} + +/// +/// A helper pool for renting lists of entities with specific components. +/// +/// The component type associated with the entity. +public static class ListPoolEntity where T : IComponent +{ + /// + /// Rents a list of from the pool. + /// + /// A disposable wrapper containing the rented entity list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CollectionPool>, Entity>.PooledCollection Rent() + => ListPool>.Rent(); +} + +/// +/// A pre-configured pool specifically for instances. +/// +/// The type of elements in the hash set. +public static class HashSetPool +{ + static HashSetPool() + { + CollectionPool, T>.Configure(() => new HashSet()); + } + + /// + /// Rents a hash set from the pool. + /// + /// A disposable wrapper containing the rented hash set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CollectionPool, T>.PooledCollection Rent() + => CollectionPool, T>.Rent(); +} + +/// +/// A helper pool for renting hash sets of entities with specific components. +/// +/// The component type associated with the entity. +public static class HashSetPoolEntity where T : IComponent +{ + /// + /// Rents a hash set of from the pool. + /// + /// A disposable wrapper containing the rented entity hash set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CollectionPool>, Entity>.PooledCollection Rent() + => HashSetPool>.Rent(); +} diff --git a/Content.Shared/_Scp/Helpers/ScpHelpers.cs b/Content.Shared/_Scp/Helpers/ScpHelpers.cs index 9cfdff50daf..56acb1dddfa 100644 --- a/Content.Shared/_Scp/Helpers/ScpHelpers.cs +++ b/Content.Shared/_Scp/Helpers/ScpHelpers.cs @@ -7,8 +7,6 @@ namespace Content.Shared._Scp.Helpers; -// TODO: Использовать оптимизации GC после внедрения их в EyeWatchingSystem - public sealed class ScpHelpers : EntitySystem { [Dependency] private readonly EyeWatchingSystem _watching = default!; @@ -19,19 +17,20 @@ public sealed class ScpHelpers : EntitySystem /// public FixedPoint2 GetAroundSolutionVolume(EntityUid uid, ProtoId reagent, - in List puddleList, + List puddleList, LineOfSightBlockerLevel lineOfSight = LineOfSightBlockerLevel.Transparent) { - FixedPoint2 total = 0; - var puddles = _watching.GetAllEntitiesVisibleTo(uid, lineOfSight); + using var puddles = ListPoolEntity.Rent(); + if (!_watching.TryGetAllEntitiesVisibleTo(uid, puddles.Value, lineOfSight, LookupFlags.Static | LookupFlags.Approximate)) + return FixedPoint2.Zero; - foreach (var puddle in puddles) + FixedPoint2 total = 0; + foreach (var puddle in puddles.Value) { if (!puddle.Comp.Solution.HasValue) continue; var solution = puddle.Comp.Solution.Value.Comp.Solution; - foreach (var (reagentId, quantity) in solution.Contents) { if (reagentId.Prototype != reagent) @@ -53,10 +52,12 @@ public FixedPoint2 GetAroundSolutionVolume(EntityUid uid, ProtoId reagent, LineOfSightBlockerLevel lineOfSight = LineOfSightBlockerLevel.Transparent) { - FixedPoint2 total = 0; - var puddles = _watching.GetAllEntitiesVisibleTo(uid, lineOfSight); + using var puddles = ListPoolEntity.Rent(); + if (!_watching.TryGetAllEntitiesVisibleTo(uid, puddles.Value, lineOfSight, LookupFlags.Static | LookupFlags.Approximate)) + return FixedPoint2.Zero; - foreach (var puddle in puddles) + FixedPoint2 total = 0; + foreach (var puddle in puddles.Value) { if (!puddle.Comp.Solution.HasValue) continue; @@ -80,10 +81,12 @@ public bool IsAroundSolutionVolumeGreaterThan(EntityUid uid, FixedPoint2 required, LineOfSightBlockerLevel lineOfSight = LineOfSightBlockerLevel.Transparent) { - FixedPoint2 total = 0; - var puddles = _watching.GetAllEntitiesVisibleTo(uid, lineOfSight); + using var puddles = ListPoolEntity.Rent(); + if (!_watching.TryGetAllEntitiesVisibleTo(uid, puddles.Value, lineOfSight, LookupFlags.Static | LookupFlags.Approximate)) + return false; - foreach (var puddle in puddles) + FixedPoint2 total = 0; + foreach (var puddle in puddles.Value) { if (!puddle.Comp.Solution.HasValue) continue; diff --git a/Content.Shared/_Scp/Proximity/ActiveProximityTargetComponent.cs b/Content.Shared/_Scp/Proximity/ActiveProximityTargetComponent.cs new file mode 100644 index 00000000000..10c8938d5fd --- /dev/null +++ b/Content.Shared/_Scp/Proximity/ActiveProximityTargetComponent.cs @@ -0,0 +1,22 @@ +namespace Content.Shared._Scp.Proximity; + +/// +/// Runtime-компонент, который существует только пока цель находится рядом +/// хотя бы с одним . +/// Хранит выбранный dominant receiver для дальнейшей логики. +/// +[RegisterComponent] +public sealed partial class ActiveProximityTargetComponent : Component +{ + /// + /// Текущий dominant receiver, рядом с которым находится цель. + /// + [ViewVariables] + public EntityUid Receiver; + + /// + /// Эффективный радиус receiver, внутри которого находится цель. + /// + [ViewVariables] + public float CloseRange; +} diff --git a/Content.Shared/_Scp/Proximity/ProximityReceiverComponent.cs b/Content.Shared/_Scp/Proximity/ProximityReceiverComponent.cs index c5c3326c74c..038fccf6de0 100644 --- a/Content.Shared/_Scp/Proximity/ProximityReceiverComponent.cs +++ b/Content.Shared/_Scp/Proximity/ProximityReceiverComponent.cs @@ -12,12 +12,15 @@ public sealed partial class ProximityReceiverComponent : Component /// /// На каком расстоянии будут вызываться ивент? /// - [DataField, AutoNetworkedField, ViewVariables] + [DataField, AutoNetworkedField] public float CloseRange = 3f; /// - [DataField, ViewVariables, AutoNetworkedField] + [DataField, AutoNetworkedField] public LineOfSightBlockerLevel RequiredLineOfSight = LineOfSightBlockerLevel.Transparent; + + [DataField] + public LookupFlags Flags = LookupFlags.Uncontained | LookupFlags.Approximate; } /// @@ -51,33 +54,29 @@ public enum LineOfSightBlockerLevel /// Ивент, вызываемый при приближении к . /// Вызывается на сущности, к которой приблизились. /// -/// Цель, которая приблизилась -/// Текущее расстояние сущности до цели -/// Расстояние, на котором начинается триггер ивента -/// Фактический уровень видимости -public sealed class ProximityInRangeReceiverEvent(EntityUid target, float range, float closeRange, LineOfSightBlockerLevel type) : EntityEventArgs -{ - public readonly EntityUid Target = target; - public readonly float Range = range; - public readonly float CloseRange = closeRange; - public readonly LineOfSightBlockerLevel Type = type; -} +/// Цель, которая приблизилась +/// Текущее расстояние сущности до цели +/// Расстояние, на котором начинается триггер ивента +/// Фактический уровень видимости +[ByRefEvent] +public readonly record struct ProximityInRangeReceiverEvent(EntityUid Target, + float Range, + float CloseRange, + LineOfSightBlockerLevel Type); /// /// Ивент, вызываемый при приближении к . /// Вызывается на цели, которая приблизилась. /// -/// Сущность, к которой приблизились -/// Текущее расстояние цели до сущности -/// Расстояние, на котором начинается триггер ивента -/// Фактический уровень видимости -public sealed class ProximityInRangeTargetEvent(EntityUid receiver, float range, float closeRange, LineOfSightBlockerLevel type) : EntityEventArgs -{ - public readonly EntityUid Receiver = receiver; - public readonly float Range = range; - public readonly float CloseRange = closeRange; - public readonly LineOfSightBlockerLevel Type = type; -} +/// Сущность, к которой приблизились +/// Текущее расстояние цели до сущности +/// Расстояние, на котором начинается триггер ивента +/// Фактический уровень видимости +[ByRefEvent] +public readonly record struct ProximityInRangeTargetEvent(EntityUid Receiver, + float Range, + float CloseRange, + LineOfSightBlockerLevel Type); /// /// Ивент, вызываемый, когда сущность отсутствует рядом с любым . @@ -85,4 +84,31 @@ public sealed class ProximityInRangeTargetEvent(EntityUid receiver, float range, /// Служит, чтобы убирать какие-то эффекты, вызванные ивента приближения. /// [ByRefEvent] -public record struct ProximityNotInRangeTargetEvent; +public readonly record struct ProximityNotInRangeTargetEvent; + +/// +/// Ивент, вызываемый на цели, когда рядом впервые появляется dominant proximity receiver. +/// +[ByRefEvent] +public readonly record struct ProximityTargetEnteredEvent( + EntityUid Receiver, + float Range, + float CloseRange, + LineOfSightBlockerLevel Type); + +/// +/// Ивент, вызываемый на цели, когда она перестает находиться рядом с dominant proximity receiver. +/// +[ByRefEvent] +public readonly record struct ProximityTargetExitedEvent(EntityUid Receiver); + +/// +/// Ивент, вызываемый на цели, когда ее dominant proximity receiver сменился. +/// +[ByRefEvent] +public readonly record struct ProximityTargetReceiverChangedEvent( + EntityUid OldReceiver, + EntityUid NewReceiver, + float Range, + float CloseRange, + LineOfSightBlockerLevel Type); diff --git a/Content.Shared/_Scp/Proximity/ProximitySystem.cs b/Content.Shared/_Scp/Proximity/ProximitySystem.cs index 508be5d62cf..9a55f9d4dad 100644 --- a/Content.Shared/_Scp/Proximity/ProximitySystem.cs +++ b/Content.Shared/_Scp/Proximity/ProximitySystem.cs @@ -28,12 +28,12 @@ public sealed class ProximitySystem : EntitySystem [Dependency] private readonly IGameTiming _timing = default!; private static readonly TimeSpan ProximitySearchCooldown = TimeSpan.FromSeconds(0.05f); - private static TimeSpan _nextSearchTime = TimeSpan.Zero; + private TimeSpan _nextSearchTime = TimeSpan.Zero; // Оптимизации аллокации памяти - private static readonly HashSet> Targets = []; - private static readonly HashSet PossibleNotInRange = []; - private static readonly HashSet AllTargets = []; + private readonly HashSet> _targets = []; + private readonly Dictionary _currentMatches = []; + private readonly Dictionary _nextMatches = []; private const float JustUselessNumber = 30f; @@ -54,33 +54,33 @@ public sealed class ProximitySystem : EntitySystem "SecureUraniumWindoor", ]; + private EntityQuery _activeProximityQuery; private EntityQuery _insideQuery; + private EntityQuery _xformQuery; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(_ => Clean()); - - SubscribeLocalEvent(AddToTargets); - SubscribeLocalEvent(RemoveFromTargets); - SubscribeLocalEvent(RemoveFromTargets); + SubscribeLocalEvent(OnRoundRestartCleanup); + _activeProximityQuery = GetEntityQuery(); _insideQuery = GetEntityQuery(); + _xformQuery = GetEntityQuery(); } - private static void Clean() + private void OnRoundRestartCleanup(RoundRestartCleanupEvent args) { _nextSearchTime = TimeSpan.Zero; - AllTargets.Clear(); - } - - #region All targets population + _currentMatches.Clear(); + _nextMatches.Clear(); - private static void AddToTargets(Entity ent, ref T args) => AllTargets.Add(ent); - private static void RemoveFromTargets(Entity ent, ref T args) => AllTargets.Remove(ent); - - #endregion + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _)) + { + RemCompDeferred(uid); + } + } public override void Update(float frameTime) { @@ -93,42 +93,117 @@ public override void Update(float frameTime) if (_timing.CurTime < _nextSearchTime) return; - PossibleNotInRange.Clear(); - PossibleNotInRange.UnionWith(AllTargets); + _nextMatches.Clear(); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var receiver, out var xform)) { - Targets.Clear(); - _lookup.GetEntitiesInRange(xform.Coordinates, receiver.CloseRange, Targets); + _targets.Clear(); + _lookup.GetEntitiesInRange(xform.Coordinates, receiver.CloseRange, _targets, receiver.Flags); - foreach (var target in Targets) + foreach (var target in _targets) { - if (!IsRightType(uid, target, receiver.RequiredLineOfSight, out var lightOfSightBlockerLevel)) + if (!_xformQuery.TryComp(target, out var targetXform)) continue; - var targetCoords = Transform(target).Coordinates; + var targetCoords = targetXform.Coordinates; if (!xform.Coordinates.TryDistance(EntityManager, _transform, targetCoords, out var range)) continue; - var receiverEvent = new ProximityInRangeReceiverEvent(target, range, receiver.CloseRange, lightOfSightBlockerLevel); - RaiseLocalEvent(uid, receiverEvent); + if (range > receiver.CloseRange) + continue; + + if (!IsRightType(uid, target, receiver.RequiredLineOfSight, out var lightOfSightBlockerLevel)) + continue; - var targetEvent = new ProximityInRangeTargetEvent(uid, range, receiver.CloseRange, lightOfSightBlockerLevel); - RaiseLocalEvent(target, targetEvent); + var candidate = new ProximityMatch(uid, receiver.CloseRange, range, lightOfSightBlockerLevel); - PossibleNotInRange.Remove(target); + if (_nextMatches.TryGetValue(target, out var current) && !IsBetter(candidate, current)) + continue; + + _nextMatches[target] = candidate; } } - foreach (var target in PossibleNotInRange) + ApplyDelta(); + + _nextSearchTime = _timing.CurTime + ProximitySearchCooldown; + } + + private void ApplyDelta() + { + foreach (var (target, current) in _currentMatches) { - var notInRangeEvent = new ProximityNotInRangeTargetEvent(); - RaiseLocalEvent(target, ref notInRangeEvent); + if (_nextMatches.ContainsKey(target)) + continue; + + if (Deleted(target)) + continue; + + if (_activeProximityQuery.HasComp(target)) + RemCompDeferred(target); + + var exited = new ProximityTargetExitedEvent(current.Receiver); + RaiseLocalEvent(target, ref exited); } - _nextSearchTime = _timing.CurTime + ProximitySearchCooldown; + foreach (var (target, next) in _nextMatches) + { + if (Deleted(target)) + continue; + + var hadCurrent = _currentMatches.TryGetValue(target, out var current); + var proximity = EnsureComp(target); + + proximity.Receiver = next.Receiver; + proximity.CloseRange = next.CloseRange; + + if (!hadCurrent) + { + var entered = new ProximityTargetEnteredEvent(next.Receiver, next.Range, next.CloseRange, next.BlockerLevel); + RaiseLocalEvent(target, ref entered); + continue; + } + + if (current.Receiver != next.Receiver) + { + var changed = new ProximityTargetReceiverChangedEvent( + current.Receiver, + next.Receiver, + next.Range, + next.CloseRange, + next.BlockerLevel); + + RaiseLocalEvent(target, ref changed); + } + } + + _currentMatches.Clear(); + foreach (var (target, next) in _nextMatches) + { + _currentMatches[target] = next; + } + } + + private static bool IsBetter(ProximityMatch candidate, ProximityMatch current) + { + var candidateNormalized = candidate.Range / MathF.Max(candidate.CloseRange, float.Epsilon); + var currentNormalized = current.Range / MathF.Max(current.CloseRange, float.Epsilon); + + if (candidateNormalized < currentNormalized) + return true; + + if (candidateNormalized > currentNormalized) + return false; + + if (candidate.BlockerLevel < current.BlockerLevel) + return true; + + if (candidate.BlockerLevel > current.BlockerLevel) + return false; + + return candidate.Receiver.CompareTo(current.Receiver) < 0; } @@ -195,7 +270,7 @@ private bool InRangeUnobstructed(Entity first, EntityИмеется ли рядом такая сущность или нет public bool IsNearby(EntityUid uid, float range, LineOfSightBlockerLevel level = LineOfSightBlockerLevel.None) where T : IComponent { - return _lookup.GetEntitiesInRange(Transform(uid).Coordinates, range) + return _lookup.GetEntitiesInRange(Transform(uid).Coordinates, range, LookupFlags.Uncontained | LookupFlags.Approximate) .Any(e => IsRightType(uid, e, level, out _)); } @@ -214,4 +289,10 @@ public bool IsNearby(EntityUid uid, float range, HashSet> buffer, L return buffer.Any(e => IsRightType(uid, e, level, out _)); } + + private readonly record struct ProximityMatch( + EntityUid Receiver, + float CloseRange, + float Range, + LineOfSightBlockerLevel BlockerLevel); } diff --git a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Target.cs b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Target.cs index 41674f9d19c..bf31f98027e 100644 --- a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Target.cs +++ b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Target.cs @@ -190,7 +190,7 @@ private bool IsTargetSeeScp096(EntityUid viewer, Entity scp, bo return false; // Проверяет, не слеп ли персонаж - if (_watching.IsEyeBlinded(viewer, scp, false) && !ignoreBlinded) + if (!_watching.CanSee(viewer, scp, false) && !ignoreBlinded) return false; // Если игнорируем угол, то считаем, что смотрящий видит 096 diff --git a/Content.Shared/_Scp/Scp106/Systems/SharedScp106System.Phantom.cs b/Content.Shared/_Scp/Scp106/Systems/SharedScp106System.Phantom.cs index 0210cdc4edd..8a6fe663694 100644 --- a/Content.Shared/_Scp/Scp106/Systems/SharedScp106System.Phantom.cs +++ b/Content.Shared/_Scp/Scp106/Systems/SharedScp106System.Phantom.cs @@ -101,7 +101,7 @@ private void OnExamined(Entity ent, ref ExaminedEvent ar if (!_mob.IsAlive(args.Examiner)) return; - if (!_watching.SimpleIsWatchedBy(ent.Owner, [args.Examiner])) + if (!_watching.IsWatchedBy(ent.Owner, args.Examiner)) return; // Ликвидируйся diff --git a/Content.Shared/_Scp/Scp173/SharedScp173System.cs b/Content.Shared/_Scp/Scp173/SharedScp173System.cs index 8d9bd65a644..cb0275be7a2 100644 --- a/Content.Shared/_Scp/Scp173/SharedScp173System.cs +++ b/Content.Shared/_Scp/Scp173/SharedScp173System.cs @@ -1,12 +1,12 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Numerics; using Content.Shared._Scp.Blinking; using Content.Shared._Scp.Containment.Cage; +using Content.Shared._Scp.Helpers; +using Content.Shared._Scp.Proximity; using Content.Shared._Scp.Watching; using Content.Shared.ActionBlocker; using Content.Shared.DoAfter; -using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Movement.Events; using Content.Shared.Physics; @@ -18,30 +18,28 @@ namespace Content.Shared._Scp.Scp173; +// TODO: Выделить логику блокировки движения при смотрении в отдельную систему со своим компонентом. public abstract class SharedScp173System : EntitySystem { [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] private readonly SharedBlinkingSystem _blinking = default!; [Dependency] private readonly ActionBlockerSystem _blocker = default!; [Dependency] protected readonly EyeWatchingSystem Watching = default!; - [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; - protected static readonly TimeSpan ReagentCheckInterval = TimeSpan.FromSeconds(1); + protected static readonly TimeSpan ReagentCheckInterval = TimeSpan.FromSeconds(1f); public const float ContainmentRoomSearchRadius = 8f; + private EntityQuery _insideQuery; + private EntityQuery _scpCageQuery; + public override void Initialize() { base.Initialize(); - SubscribeLocalEvent((uid, _, args) => - { - if (Watching.IsWatched(uid)) - args.Cancel(); - }); + SubscribeLocalEvent(OnAttackAttempt); SubscribeLocalEvent(OnDirectionAttempt); SubscribeLocalEvent(OnMoveAttempt); @@ -50,20 +48,50 @@ public override void Initialize() SubscribeLocalEvent(OnStartedBlind); SubscribeLocalEvent(OnBlind); + + _insideQuery = GetEntityQuery(); + _scpCageQuery = GetEntityQuery(); } #region Movement - private void OnDirectionAttempt(Entity ent, ref ChangeDirectionAttemptEvent args) + private void OnAttackAttempt(Entity ent, ref AttackAttemptEvent args) { - if (Watching.IsWatched(ent.Owner) && !IsInScpCage(ent, out _)) + if (IsInScpCage(ent, out _)) + { + args.Cancel(); + return; + } + + if (Watching.IsWatchedByAny(ent, useTimeCompensation: true)) + { args.Cancel(); + return; + } + } + + private void OnDirectionAttempt(Entity ent, ref ChangeDirectionAttemptEvent args) + { + // В клетке можно двигаться + if (IsInScpCage(ent, out _)) + return; + + if (Watching.IsWatchedByAny(ent, useTimeCompensation: true)) + return; + + args.Cancel(); } private void OnMoveAttempt(Entity ent, ref UpdateCanMoveEvent args) { - if (Watching.IsWatched(ent.Owner) && !IsInScpCage(ent, out _)) - args.Cancel(); + // В клетке можно двигаться + if (IsInScpCage(ent, out _)) + return; + + if (!Watching.IsWatchedByAny(ent, useTimeCompensation: true)) + return; + + args.Cancel(); } private void OnMoveInput(Entity ent, ref MoveInputEvent args) @@ -123,11 +151,13 @@ private void OnBlind(Entity ent, ref Scp173StartBlind args) public void BlindEveryoneInRange(EntityUid scp, TimeSpan time, bool predicted = true) { - var eyes = Watching.GetWatchers(scp); + using var blinkableList = ListPoolEntity.Rent(); + if (!Watching.TryGetAllEntitiesVisibleTo(scp, blinkableList.Value, flags: LookupFlags.Dynamic | LookupFlags.Approximate)) + return; - foreach (var eye in eyes) + foreach (var eye in blinkableList.Value) { - _blinking.ForceBlind(eye, time, predicted); + _blinking.ForceBlind(eye.AsNullable(), time, predicted); } // TODO: Add sound. @@ -140,8 +170,8 @@ public bool IsInScpCage(EntityUid uid, [NotNullWhen(true)] out EntityUid? storag { storage = null; - if (TryComp(uid, out var insideEntityStorageComponent) && - HasComp(insideEntityStorageComponent.Storage)) + if (_insideQuery.TryComp(uid, out var insideEntityStorageComponent) && + _scpCageQuery.HasComp(insideEntityStorageComponent.Storage)) { storage = insideEntityStorageComponent.Storage; return true; @@ -153,11 +183,12 @@ public bool IsInScpCage(EntityUid uid, [NotNullWhen(true)] out EntityUid? storag /// /// Находится ли 173 в своей камере. Проверяется по наличию рядом спавнера работы /// - /// TODO: Оптимизировать, использовав EntityQueryEnumerator и ранний выход public bool IsContained(EntityUid uid) { - return _lookup.GetEntitiesInRange(Transform(uid).Coordinates, ContainmentRoomSearchRadius) - .Any(entity => _interaction.InRangeUnobstructed(uid, entity.Owner, ContainmentRoomSearchRadius)); + return Watching.TryGetAnyEntitiesVisibleTo(uid, + LineOfSightBlockerLevel.None, + LookupFlags.Sensors | LookupFlags.Sundries, + ContainmentRoomSearchRadius); } private bool CanBlind(EntityUid uid, bool showPopups = true) @@ -178,7 +209,7 @@ private bool CanBlind(EntityUid uid, bool showPopups = true) return false; } - if (!IsWatched(uid, out var watchers)) + if (!Watching.TryGetWatchers(uid, out var watchers)) { if (showPopups) _popup.PopupClient(Loc.GetString("scp173-blind-failed-too-few-watchers"), uid, uid); @@ -186,7 +217,7 @@ private bool CanBlind(EntityUid uid, bool showPopups = true) return false; } - if (watchers.Count <= 3) + if (watchers <= 3) { if (showPopups) _popup.PopupClient(Loc.GetString("scp173-blind-failed-too-few-watchers"), uid, uid); @@ -197,17 +228,6 @@ private bool CanBlind(EntityUid uid, bool showPopups = true) return true; } - public bool IsWatched(EntityUid target, out HashSet viewers) - { - var watchers = Watching.GetWatchers(target); - - viewers = watchers - .Where(eye => Watching.CanBeWatched(eye, target)) - .ToHashSet(); - - return viewers.Count != 0; - } - #endregion #region Jump Helpers diff --git a/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs b/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs new file mode 100644 index 00000000000..d28eeba9bf6 --- /dev/null +++ b/Content.Shared/_Scp/Scp939/ActiveScp939VisibilityComponent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Scp.Scp939; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true, fieldDeltas: true)] +public sealed partial class ActiveScp939VisibilityComponent : Component +{ + [ViewVariables] + public float VisibilityAcc = Scp939VisibilityComponent.InitialVisibilityAcc; + + [AutoNetworkedField] + public uint VisibilityResetCounter; + + [DataField, AutoNetworkedField] + public float HideTime = Scp939VisibilityComponent.DefaultHideTime; + + [DataField, AutoNetworkedField] + public int MinValue = Scp939VisibilityComponent.DefaultMinValue; + + [DataField, AutoNetworkedField] + public int MaxValue = Scp939VisibilityComponent.DefaultMaxValue; + + [NonSerialized] + public uint LastHandledVisibilityResetCounter; +} diff --git a/Content.Shared/_Scp/Scp939/Scp939Component.cs b/Content.Shared/_Scp/Scp939/Scp939Component.cs index 9f57ec9ec0e..3b6052b1f0b 100644 --- a/Content.Shared/_Scp/Scp939/Scp939Component.cs +++ b/Content.Shared/_Scp/Scp939/Scp939Component.cs @@ -5,16 +5,16 @@ namespace Content.Shared._Scp.Scp939; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] public sealed partial class Scp939Component : Component { - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public Solution SmokeSolution = new("АМН-С227", 40); - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float SmokeDuration = 30.0f; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public int SmokeSpreadRadius = 10; [DataField] @@ -28,10 +28,10 @@ public sealed partial class Scp939Component : Component "Scp939Sleep", }; - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float HibernationDuration = 60.0f; + [DataField] + public float HibernationDuration = 60f; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public DamageSpecifier HibernationHealingRate = new() { DamageDict = new() @@ -63,6 +63,9 @@ public sealed partial class Scp939Component : Component [AutoNetworkedField] public TimeSpan? PoorEyesightTimeStart; // Когда начали плохо видеть + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float VisibilityActivationRange = 20f; + #endregion [DataField] diff --git a/Content.Shared/_Scp/Scp939/Scp939VisabilityComponent.cs b/Content.Shared/_Scp/Scp939/Scp939VisabilityComponent.cs index d75ad4f7b5d..94a85140e37 100644 --- a/Content.Shared/_Scp/Scp939/Scp939VisabilityComponent.cs +++ b/Content.Shared/_Scp/Scp939/Scp939VisabilityComponent.cs @@ -1,22 +1,19 @@ -using Robust.Shared.GameStates; - namespace Content.Shared._Scp.Scp939; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent] public sealed partial class Scp939VisibilityComponent : Component { - [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public float VisibilityAcc; - - public readonly float HideTime = 2.5f; - - #region PoorEyesight + public const float InitialVisibilityAcc = 0.001f; + public const float DefaultHideTime = 2.5f; + public const int DefaultMinValue = 40; + public const int DefaultMaxValue = 400; - [DataField, ViewVariables(VVAccess.ReadWrite)] - public int MinValue = 40; + [DataField] + public float HideTime = DefaultHideTime; - [DataField, ViewVariables(VVAccess.ReadWrite)] - public int MaxValue = 400; + [DataField] + public int MinValue = DefaultMinValue; - #endregion + [DataField] + public int MaxValue = DefaultMaxValue; } diff --git a/Content.Shared/_Scp/Shaders/Grain/GrainOverlayComponent.cs b/Content.Shared/_Scp/Shaders/Grain/GrainOverlayComponent.cs index 082cea2e7e7..4380f74c6e9 100644 --- a/Content.Shared/_Scp/Shaders/Grain/GrainOverlayComponent.cs +++ b/Content.Shared/_Scp/Shaders/Grain/GrainOverlayComponent.cs @@ -7,7 +7,7 @@ namespace Content.Shared._Scp.Shaders.Grain; /// Компонент, отвечающий за параметры шейдера зернистости. /// Наличие компонента необходимо для работы шейдера. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true, true)] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] public sealed partial class GrainOverlayComponent : Component, IShaderStrength { /// diff --git a/Content.Shared/_Scp/Shaders/SharedShaderStrengthSystem.cs b/Content.Shared/_Scp/Shaders/SharedShaderStrengthSystem.cs index b01ba15f9ae..13832775298 100644 --- a/Content.Shared/_Scp/Shaders/SharedShaderStrengthSystem.cs +++ b/Content.Shared/_Scp/Shaders/SharedShaderStrengthSystem.cs @@ -13,7 +13,8 @@ public sealed class SharedShaderStrengthSystem : EntitySystem /// Сущность, к которой будет применено значение /// Значение, которое будет установлено /// Получилось/Не получилось - public bool TrySetBaseStrength(Entity ent, float value) where T : IComponent, IShaderStrength + public bool TrySetBaseStrength(Entity ent, float value) + where T : IComponent, IShaderStrength { // Базовая сила это чисто клиентский параметр // Поэтому ее задавать только на клиенте @@ -35,7 +36,8 @@ public bool TrySetBaseStrength(Entity ent, float value) where T : ICompon /// Значение, которое будет установлено /// Возвращаемый компонент с параметрами /// Получилось/Не получилось - public bool TrySetBaseStrength(Entity ent, float value, [NotNullWhen(true)] out T? component) where T : Component, IShaderStrength + public bool TrySetBaseStrength(Entity ent, float value, [NotNullWhen(true)] out T? component) + where T : Component, IShaderStrength { component = null; @@ -59,19 +61,15 @@ public bool TrySetBaseStrength(Entity ent, float value, [NotNullWhen(true /// Сущность, к которой будет применено значение /// Значение, которое будет установлено /// Получилось/Не получилось - public bool TrySetAdditionalStrength(Entity ent, float value) where T : IComponent, IShaderStrength + public bool TrySetAdditionalStrength(Entity ent, float value) + where T : IComponent, IShaderStrength { if (!Resolve(ent, ref ent.Comp)) return false; ent.Comp.AdditionalStrength = value; - - var ev = new ShaderAdditionalStrengthChanged(); - RaiseLocalEvent(ent, ref ev); + Dirty(ent); return true; } } - -[ByRefEvent] -public record struct ShaderAdditionalStrengthChanged(); diff --git a/Content.Shared/_Scp/Shaders/Vignette/VignetteOverlayComponent.cs b/Content.Shared/_Scp/Shaders/Vignette/VignetteOverlayComponent.cs index 830cd000aef..5e4bc739dd4 100644 --- a/Content.Shared/_Scp/Shaders/Vignette/VignetteOverlayComponent.cs +++ b/Content.Shared/_Scp/Shaders/Vignette/VignetteOverlayComponent.cs @@ -2,7 +2,7 @@ namespace Content.Shared._Scp.Shaders.Vignette; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true, true)] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] public sealed partial class VignetteOverlayComponent : Component, IShaderStrength { /// diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs new file mode 100644 index 00000000000..f84b83170bf --- /dev/null +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Base.cs @@ -0,0 +1,260 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared._Scp.Blinking; +using Content.Shared._Scp.Helpers; +using Content.Shared._Scp.Proximity; +using Content.Shared._Scp.Watching.FOV; +using Content.Shared.Eye.Blinding.Systems; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Storage.Components; + +namespace Content.Shared._Scp.Watching; + +public sealed partial class EyeWatchingSystem +{ + [Dependency] private readonly SharedBlinkingSystem _blinking = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly FieldOfViewSystem _fov = default!; + + private EntityQuery _mobStateQuery; + private EntityQuery _insideStorageQuery; + private EntityQuery _blinkableQuery; + + private void InitializeApi() + { + _mobStateQuery = GetEntityQuery(); + _insideStorageQuery = GetEntityQuery(); + _blinkableQuery = GetEntityQuery(); + } + + /// + /// Получает и возвращает всех сущности в радиусе видимости для цели. + /// По сути является аналогом , но использует проверку на линию видимости. + /// + /// + /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. + /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.). + /// + /// + /// Цель, для которой ищем сущности в радиусе видимости + /// Список всех, кто находится в радиусе видимости + /// Требуемая прозрачность линии видимости. + /// Список флагов для поиска целей в + /// Если нужно использовать другой радиус поиска, отличный от + /// Компонент, который должны иметь все сущности в радиусе видимости + /// Удалось ли найти хоть кого-то + public bool TryGetAllEntitiesVisibleTo( + Entity ent, + List> potentialWatchers, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + float? rangeOverride = null) + where T : IComponent + { + using var searchSet = HashSetPoolEntity.Rent(); + return TryGetAllEntitiesVisibleTo(ent, potentialWatchers, searchSet.Value, type, flags, rangeOverride); + } + + /// + /// Получает и возвращает всех сущности в радиусе видимости для цели. + /// По сути является аналогом , но использует проверку на линию видимости. + /// + /// + /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. + /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.). + /// + /// + /// Цель, для которой ищем сущности в радиусе видимости + /// Список всех, кто находится в радиусе видимости + /// Заранее заготовленный список, который будет использоваться в + /// Требуемая прозрачность линии видимости. + /// Список флагов для поиска целей в + /// Если нужно использовать другой радиус поиска, отличный от + /// Компонент, который должны иметь все сущности в радиусе видимости + /// Удалось ли найти хоть кого-то + private bool TryGetAllEntitiesVisibleTo( + Entity ent, + List> potentialWatchers, + HashSet> searchSet, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + float? rangeOverride = null) + where T : IComponent + { + if (!Resolve(ent.Owner, ref ent.Comp)) + return false; + + searchSet.Clear(); + _lookup.GetEntitiesInRange(ent.Comp.Coordinates, rangeOverride ?? SeeRange, searchSet, flags); + + foreach (var target in searchSet) + { + if (!IsInProximity(ent, target, type)) + continue; + + potentialWatchers.Add(target); + } + + return potentialWatchers.Count != 0; + } + + /// + /// Проверяет, есть ли хоть одна сущность в радиусе видимости цели. + /// По сути является аналогом , но использует проверку на линию видимости. + /// + /// + /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. + /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.). + /// + /// + /// Цель, для которой ищем сущности в радиусе видимости + /// Требуемая прозрачность линии видимости. + /// Список флагов для поиска целей в + /// Если нужно использовать другой радиус поиска, отличный от + /// Компонент, который должны иметь все сущности в радиусе видимости + /// Удалось ли найти хоть кого-то + public bool TryGetAnyEntitiesVisibleTo( + Entity viewer, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + float? rangeOverride = null) + where T : IComponent + { + using var searchSet = HashSetPoolEntity.Rent(); + if (!TryGetAnyEntitiesVisibleTo(viewer, out _, searchSet.Value, type, flags, rangeOverride)) + return false; + + return true; + } + + /// + /// Получает и возвращает первую сущность в радиусе видимости цели. + /// По сути является аналогом , но использует проверку на линию видимости. + /// + /// + /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. + /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.). + /// + /// + /// Цель, для которой ищем сущности в радиусе видимости + /// Первая попавшаяся сущность в радиусе видимости цели + /// Требуемая прозрачность линии видимости. + /// Список флагов для поиска целей в + /// Если нужно использовать другой радиус поиска, отличный от + /// Компонент, который должны иметь все сущности в радиусе видимости + /// Удалось ли найти хоть кого-то + public bool TryGetAnyEntitiesVisibleTo( + Entity viewer, + [NotNullWhen(true)] out Entity? firstVisible, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + float? rangeOverride = null) + where T : IComponent + { + firstVisible = null; + + using var searchSet = HashSetPoolEntity.Rent(); + if (!TryGetAnyEntitiesVisibleTo(viewer, out var first, searchSet.Value, type, flags, rangeOverride)) + return false; + + firstVisible = first; + return true; + } + + /// + /// Получает и возвращает первую сущность в радиусе видимости цели. + /// По сути является аналогом , но использует проверку на линию видимости. + /// + /// + /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. + /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.). + /// + /// + /// Цель, для которой ищем сущности в радиусе видимости + /// Первая попавшаяся сущность в радиусе видимости цели + /// Заранее заготовленный список, который будет использоваться в + /// Требуемая прозрачность линии видимости. + /// Список флагов для поиска целей в + /// Если нужно использовать другой радиус поиска, отличный от + /// Компонент, который должны иметь все сущности в радиусе видимости + /// Удалось ли найти хоть кого-то + private bool TryGetAnyEntitiesVisibleTo( + Entity viewer, + [NotNullWhen(true)] out Entity? firstVisible, + HashSet> searchSet, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + float? rangeOverride = null) + where T : IComponent + { + firstVisible = null; + + if (!Resolve(viewer.Owner, ref viewer.Comp)) + return false; + + searchSet.Clear(); + _lookup.GetEntitiesInRange(viewer.Comp.Coordinates, rangeOverride ?? SeeRange, searchSet, flags); + + foreach (var target in searchSet) + { + if (!IsInProximity(viewer, target, type)) + continue; + + firstVisible = target; + return true; + } + + return false; + } + + /// + /// Проверяет, правильный ли между сущностями тип видимости. + /// + private bool IsInProximity(EntityUid ent, EntityUid target, LineOfSightBlockerLevel type) + { + if (target == ent) + return false; + + if (!_proximity.IsRightType(ent, target, type, out _)) + return false; + + return true; + } + + /// + /// Проверка на то, может ли смотрящий видеть цель + /// + /// Смотрящий + /// Цель, которую проверяем + /// Применять ли проверку на поле зрения? + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? + /// Если нужно использовать другой угол поля зрения + /// Видит ли смотрящий цель + public bool CanSee(Entity viewer, + EntityUid target, + bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, + float? fovOverride = null) + { + if (_mobState.IsIncapacitated(viewer)) + return false; + + // Проверяем, видит ли смотрящий цель + if (useFov && !_fov.IsInFov(viewer.Owner, target, fovOverride)) + return false; // Если не видит, то не считаем его как смотрящего + + if (checkBlinking && _blinking.IsBlind(viewer, useTimeCompensation)) + return false; + + var canSeeAttempt = new CanSeeAttemptEvent(); + RaiseLocalEvent(viewer, canSeeAttempt); + + if (canSeeAttempt.Blind) + return false; + + return true; + } +} diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs new file mode 100644 index 00000000000..3abe349b67b --- /dev/null +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watched.cs @@ -0,0 +1,208 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared._Scp.Blinking; +using Content.Shared._Scp.Helpers; +using Content.Shared._Scp.Proximity; + +namespace Content.Shared._Scp.Watching; + +public sealed partial class EyeWatchingSystem +{ + /// + /// Получает всех зрителей для конкретной сущности, которые подходят заданным условиям. + /// + /// Цель, для которой ищутся зрители + /// Количество зрителей + /// Требуемый тип линии видимости + /// Флаги для поиска зрителей + /// Будет ли проверять тип линии видимости + /// Будет ли проверять FOV зрителя + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? + /// Если нужно использовать другой угол для FOV зрителя + /// Найден ли хоть один зритель + public bool TryGetWatchers(EntityUid target, + [NotNullWhen(true)] out int? watchers, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + bool checkProximity = true, + bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, + float? fovOverride = null) + { + watchers = null; + + using var realWatchers = ListPoolEntity.Rent(); + if (!TryGetWatchers(target, realWatchers.Value, type, flags, checkProximity, useFov, useTimeCompensation, checkBlinking, fovOverride)) + return false; + + watchers = realWatchers.Value.Count; + return true; + } + + /// + /// Получает всех зрителей для конкретной сущности, которые подходят заданным условиям. + /// + /// Цель, для которой ищутся зрители + /// Список зрителей, который будет наполнен методом + /// Требуемый тип линии видимости + /// Флаги для поиска зрителей + /// Будет ли проверять тип линии видимости + /// Будет ли проверять FOV зрителя + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? + /// Если нужно использовать другой угол для FOV зрителя + /// Найден ли хоть один зритель + public bool TryGetWatchers(EntityUid target, + List> realWatchers, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + bool checkProximity = true, + bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, + float? fovOverride = null) + { + using var potentialWatchers = HashSetPoolEntity.Rent(); + _lookup.GetEntitiesInRange(Transform(target).Coordinates, SeeRange, potentialWatchers.Value, flags); + + return TryGetWatchersFrom(target, + realWatchers, + potentialWatchers.Value, + type, + checkProximity, + useFov, + useTimeCompensation, + checkBlinking, + fovOverride); + } + + /// + /// Получает всех зрителей для конкретной сущности из заранее заготовленного списка потенциальных зрителей, которые подходят заданным условиям. + /// + /// Цель, для которой ищутся зрители + /// Список зрителей, который будет наполнен методом + /// Заранее заготовленный список потенциальных зрителей, среди которых будет поиск + /// Требуемый тип линии видимости + /// Будет ли проверять тип линии видимости + /// Будет ли проверять FOV зрителя + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? + /// Если нужно использовать другой угол для FOV зрителя + /// Найден ли хоть один зритель + public bool TryGetWatchersFrom(EntityUid target, + List> realWatchers, + ICollection> potentialWatchers, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + bool checkProximity = true, + bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, + float? fovOverride = null) + { + foreach (var viewer in potentialWatchers) + { + if (!IsWatchedBy(target, viewer, type, checkProximity, useFov, useTimeCompensation, checkBlinking, fovOverride)) + continue; + + realWatchers.Add(viewer); + } + + return realWatchers.Count != 0; + } + + /// + /// Проверяет, есть ли хоть один зритель для целевой сущности. + /// Более оптимизированный вариант, который прерывает свое выполнение при найденном результате + /// + /// Цель для поиска зрителей + /// Требуемый тип линии видимости + /// Флаги для поиска зрителей в радиусе видимости + /// Будет ли проверяться тип линии видимости? + /// Будет ли проверять FOV зрителя? + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? + /// Если нужно использовать другой угол FOV зрителя + /// Найден ли хоть один зритель для цели + public bool IsWatchedByAny(EntityUid target, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + bool checkProximity = true, + bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, + float? fovOverride = null) + { + using var potentialWatchers = HashSetPoolEntity.Rent(); + _lookup.GetEntitiesInRange(Transform(target).Coordinates, SeeRange, potentialWatchers.Value, flags); + + foreach (var viewer in potentialWatchers.Value) + { + if (!IsWatchedBy(target, viewer, type, useFov, checkProximity, useTimeCompensation, checkBlinking, fovOverride)) + continue; + + return true; + } + + return false; + } + + /// + /// Проверяет, смотри ли потенциальный зритель на цель. + /// + /// Цель для проверки + /// Потенциальный зритель, который проверяется + /// Требуемый тип линии видимости + /// Будет ли проверяться тип линии видимости + /// Будет ли проверять FOV зрителя + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? + /// Если нужно задать другой угол FOV зрителя + /// Смотрит ли потенциальный зритель на цель. + public bool IsWatchedBy(EntityUid target, + EntityUid potentialViewer, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + bool checkProximity = true, + bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, + float? fovOverride = null) + { + if (!CanBeWatched(potentialViewer, target)) + return false; + + if (checkProximity && !IsInProximity(potentialViewer, target, type)) + return false; + + if (!CanSee(potentialViewer, target, useFov, useTimeCompensation, checkBlinking, fovOverride)) + return false; + + return true; + } + + /// + /// Проверяет, может ли цель вообще быть увидена смотрящим + /// + /// + /// Проверка заключается в поиске базовых компонентов, без которых Watching система не будет работать + /// + /// Смотрящий, который в теории может увидеть цель + /// Цель, которую мы проверяем на возможность быть увиденной смотрящим + /// Да/нет + public bool CanBeWatched(EntityUid viewer, EntityUid target) + { + if (!_blinkableQuery.HasComp(viewer)) + return false; + + if (viewer == target) + return false; + + if (_insideStorageQuery.HasComp(viewer)) + return false; + + if (_mobStateQuery.TryComp(viewer, out var mobState) && _mobState.IsIncapacitated(viewer, mobState)) + return false; + + return true; + } +} diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs new file mode 100644 index 00000000000..6e13bac8e8b --- /dev/null +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.Watching.cs @@ -0,0 +1,90 @@ +using Content.Shared._Scp.Helpers; +using Content.Shared._Scp.Proximity; + +namespace Content.Shared._Scp.Watching; + +public sealed partial class EyeWatchingSystem +{ + /// + /// Получает и возвращает на кого в данный момент смотрит зритель. + /// + /// Зритель, для которого идут проверки + /// Список целей, в который метод занесет всех, на кого смотрит зритель + /// Требуемый тип линии видимости + /// Флаги для поиска целей + /// Будет ли проверяться тип линии видимости + /// Будет ли проверяться FOV зрителя + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? + /// Если нужно поставить другой угол FOV зрителя + /// Компонент, который должен быть у целей + /// Найдена ли хоть одна цель + public bool TryGetWatchingTargets(EntityUid watcher, + List> targets, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Approximate, + bool checkProximity = true, + bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, + float? fovOverride = null) + where T : IComponent + { + using var potentialTargets = HashSetPoolEntity.Rent(); + _lookup.GetEntitiesInRange(Transform(watcher).Coordinates, SeeRange, potentialTargets.Value, flags); + + return TryGetWatchingTargetsFrom(watcher, + targets, + potentialTargets.Value, + type, + checkProximity, + useFov, + useTimeCompensation, + checkBlinking, + fovOverride); + } + + /// + /// Получает и возвращает на кого в данный момент смотрит зритель. + /// Использует заранее заготовленный список целей для поиска реальных целей + /// + /// Зритель, для которого идут проверки + /// Список целей, в который метод занесет всех, на кого смотрит зритель + /// Требуемый тип линии видимости + /// Заранее заготовленный список целей из которых будет производиться поиск. + /// Будет ли проверяться тип линии видимости + /// Будет ли проверяться FOV зрителя + /// Будет ли использоваться компенсация времени? Нужно для передвижения SCP-173 + /// Будет ли проводиться проверка на моргание? + /// Если нужно поставить другой угол FOV зрителя + /// Компонент, который должен быть у целей + /// Найдена ли хоть одна цель + public bool TryGetWatchingTargetsFrom(EntityUid watcher, + List> targets, + ICollection> potentialTargets, + LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent, + bool checkProximity = true, + bool useFov = true, + bool useTimeCompensation = false, + bool checkBlinking = true, + float? fovOverride = null) + where T : IComponent + { + foreach (var target in potentialTargets) + { + if (!IsWatchedBy(target, + watcher, + type, + checkProximity, + useFov, + useTimeCompensation, + checkBlinking, + fovOverride)) + continue; + + targets.Add(target); + } + + return targets.Count != 0; + } +} diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs deleted file mode 100644 index 9ebfc9766c9..00000000000 --- a/Content.Shared/_Scp/Watching/EyeWatchingSystem.API.cs +++ /dev/null @@ -1,213 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Content.Shared._Scp.Blinking; -using Content.Shared._Scp.Proximity; -using Content.Shared._Scp.Watching.FOV; -using Content.Shared.Eye.Blinding.Systems; -using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; -using Content.Shared.Storage.Components; - -namespace Content.Shared._Scp.Watching; - -// TODO: Оптимизации Garbage Collector посредством возможности передать заранее готовый список сущностей. -// Вместо создания каждый раз нового система будет использовать список, который заготовлен заранее и будет очищаться перед повторным использованием. - -public sealed partial class EyeWatchingSystem -{ - [Dependency] private readonly SharedBlinkingSystem _blinking = default!; - [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly FieldOfViewSystem _fov = default!; - - private EntityQuery _mobStateQuery; - private EntityQuery _insideStorageQuery; - - /// - /// Проверяет, смотрит ли кто-то на указанную цель - /// - /// Цель, которую проверяем - /// Нужно ли проверять поле зрения - /// Если нужно использовать другой угол обзора, отличный от стандартного - /// Смотрит ли на цель хоть кто-то - public bool IsWatched(Entity ent, bool useFov = true, float? fovOverride = null) - { - return IsWatched(ent, out _, useFov, fovOverride); - } - - /// - /// Проверяет, смотрит ли кто-то на указанную цель - /// - /// Цель, которую проверяем - /// Количество смотрящих - /// Нужно ли проверять поле зрения - /// Если нужно использовать другой угол обзора, отличный от стандартного - /// Смотрит ли на цель хоть кто-то - public bool IsWatched(EntityUid ent, [NotNullWhen(true)] out int? watchersCount, bool useFov = true, float? fovOverride = null) - { - var eyes = GetWatchers(ent); - - var isWatched = IsWatchedBy(ent, eyes, out int count , useFov, fovOverride); - watchersCount = count; - - return isWatched; - } - - /// - /// Получает и возвращает всех потенциально смотрящих на указанную цель. - /// - /// - /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. - /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.) - /// - /// Цель, для которой ищем потенциальных смотрящих - /// Список всех, кто потенциально видит цель - public IEnumerable GetWatchers(Entity ent) - { - return GetAllVisibleTo(ent); - } - - /// - /// Получает и возвращает всех потенциально смотрящих на указанную цель. - /// - /// - /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. - /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.) - /// - /// Цель, для которой ищем потенциальных смотрящих\ - /// Требуемая прозрачность линии видимости. - /// Список всех, кто потенциально видит цель - public IEnumerable GetAllVisibleTo(Entity ent, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent) - where T : IComponent - { - return GetAllEntitiesVisibleTo(ent, type) - .Select(e => e.Owner); - } - - /// - /// Получает и возвращает всех потенциально смотрящих на указанную цель. - /// - /// - /// В методе нет проверок на дополнительные состояния, такие как моргание/закрыты ли глаза/поле зрения т.п. - /// Единственная проверка - можно ли физически увидеть цель(т.е. не закрыта ли она стеной и т.п.) - /// - /// Цель, для которой ищем потенциальных смотрящих\ - /// Требуемая прозрачность линии видимости. - /// Список всех, кто потенциально видит цель - public IEnumerable> GetAllEntitiesVisibleTo(Entity ent, LineOfSightBlockerLevel type = LineOfSightBlockerLevel.Transparent) - where T : IComponent - { - if (!Resolve(ent.Owner, ref ent.Comp)) - return []; - - return _lookup.GetEntitiesInRange(ent.Comp.Coordinates, SeeRange) - .Where(eye => _proximity.IsRightType(ent, eye, type, out _)) - .Where(e => e.Owner != ent.Owner); - } - - /// - /// Проверяет, смотрят ли переданные сущности на указанную цель - /// - /// Цель - /// Список сущностей для проверки - /// Количество смотрящих - /// Нужно ли проверять, находится ли цель в поле зрения сущности - /// Если нужно перезаписать угол поля зрения - /// Смотрит ли хоть кто-то на цель - public bool IsWatchedBy(EntityUid target, IEnumerable watchers, out int watchersCount, bool useFov = true, float? fovOverride = null) - { - var isWatched = IsWatchedBy(target, watchers, out IEnumerable viewers, useFov, fovOverride); - watchersCount = viewers.Count(); - - return isWatched; - } - - /// - /// Проверяет, смотрят ли переданные сущности на указанную цель. Передает список всех сущностей, что действительно смотрят на цель - /// - /// Цель - /// Список сущностей для проверки - /// Список всех сущностей, что действительно смотрят на цель - /// Нужно ли проверять, находится ли цель в поле зрения сущности - /// Если нужно перезаписать угол поля зрения - /// Смотрит ли хоть кто-то на цель - public bool IsWatchedBy(EntityUid target, IEnumerable watchers, out IEnumerable viewers, bool useFov = true, float? fovOverride = null) - { - viewers = watchers - .Where(eye => CanBeWatched(eye, target)) - .Where(eye => !IsEyeBlinded(eye, target, useFov, fovOverride)); - - return viewers.Any(); - } - - /// - /// Простая проверка на то, видят ли переданную сущность другие сущности. - /// Вместо проверки на интервальное моргание используется проверка на мануальное закрытие глаз. - /// - /// Сущность, на которую смотрят - /// Смотрящие - /// Смотри ли хоть кто-нибудь из переданных - public bool SimpleIsWatchedBy(EntityUid target, IEnumerable watchers) - { - var viewers = watchers - .Where(eye => CanBeWatched(eye, target)) - .Where(eye => !_blinking.AreEyesClosedManually(eye)); - - return viewers.Any(); - } - - /// - /// Проверяет, может ли цель вообще быть увидена смотрящим - /// - /// - /// Проверка заключается в поиске базовых компонентов, без которых Watching система не будет работать - /// - /// Смотрящий, который в теории может увидеть цель - /// Цель, которую мы проверяем на возможность быть увиденной смотрящим - /// Да/нет - public bool CanBeWatched(Entity viewer, EntityUid target) - { - if (!Resolve(viewer.Owner, ref viewer.Comp, false)) - return false; - - if (viewer.Owner == target) - return false; - - if (_insideStorageQuery.HasComp(viewer)) - return false; - - if (_mobStateQuery.TryComp(viewer, out var mobState) && _mobState.IsIncapacitated(viewer, mobState)) - return false; - - return true; - } - - /// - /// Проверка на то, может ли смотрящий видеть цель - /// - /// Смотрящий - /// Цель, которую проверяем - /// Применять ли проверку на поле зрения? - /// Если нужно использовать другой угол поля зрения - /// Видит ли смотрящий цель - public bool IsEyeBlinded(Entity viewer, EntityUid target, bool useFov = true, float? fovOverride = null) - { - if (_mobState.IsIncapacitated(viewer)) - return true; - - // Проверяем, видит ли смотрящий цель - if (useFov && !_fov.IsInFov(viewer.Owner, target, fovOverride)) - return true; // Если не видит, то не считаем его как смотрящего - - if (_blinking.IsBlind(viewer, true)) - return true; - - var canSeeAttempt = new CanSeeAttemptEvent(); - RaiseLocalEvent(viewer, canSeeAttempt); - - if (canSeeAttempt.Blind) - return true; - - return false; - } -} diff --git a/Content.Shared/_Scp/Watching/EyeWatchingSystem.Events.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.Events.cs new file mode 100644 index 00000000000..217df44ed0e --- /dev/null +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.Events.cs @@ -0,0 +1,32 @@ +using Robust.Shared; +using Robust.Shared.Configuration; + +namespace Content.Shared._Scp.Watching; + +public sealed partial class EyeWatchingSystem +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + + private void InitializeEvents() + { + SubscribeLocalEvent(OnMapInit); + + _cfg.OnValueChanged(CVars.NetMaxUpdateRange, OnPvsRageChanged, true); + } + + private void ShutdownEvents() + { + _cfg.UnsubValueChanged(CVars.NetMaxUpdateRange, OnPvsRageChanged); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + SetNextTime(ent); + } + + private void OnPvsRageChanged(float newRange) + { + // Потому что игрок в середине экрана, а SeeRange работает как радиус. + SeeRange = newRange / 2f; + } +} diff --git a/Content.Shared/_Scp/Watching/EyeWatching.cs b/Content.Shared/_Scp/Watching/EyeWatchingSystem.cs similarity index 61% rename from Content.Shared/_Scp/Watching/EyeWatching.cs rename to Content.Shared/_Scp/Watching/EyeWatchingSystem.cs index b44ed214c1f..86eaa8291ac 100644 --- a/Content.Shared/_Scp/Watching/EyeWatching.cs +++ b/Content.Shared/_Scp/Watching/EyeWatchingSystem.cs @@ -1,6 +1,6 @@ -using Content.Shared._Scp.Proximity; -using Content.Shared.Mobs.Components; -using Content.Shared.Storage.Components; +using Content.Shared._Scp.Blinking; +using Content.Shared._Scp.Helpers; +using Content.Shared._Scp.Proximity; using Robust.Shared.Timing; namespace Content.Shared._Scp.Watching; @@ -14,21 +14,23 @@ public sealed partial class EyeWatchingSystem : EntitySystem [Dependency] private readonly ProximitySystem _proximity = default!; [Dependency] private readonly IGameTiming _timing = default!; - private static readonly TimeSpan WatchingCheckInterval = TimeSpan.FromSeconds(0.3f); - - public const float SeeRange = 16f; + /// + /// Радиус, в котором сущности могут увидеть друг друга. + /// + [ViewVariables] + public float SeeRange { get; private set; } = 16f; public override void Initialize() { - SubscribeLocalEvent(OnMapInit); - - _mobStateQuery = GetEntityQuery(); - _insideStorageQuery = GetEntityQuery(); + InitializeApi(); + InitializeEvents(); } - private void OnMapInit(Entity ent, ref MapInitEvent args) + public override void Shutdown() { - SetNextTime(ent); + base.Shutdown(); + + ShutdownEvents(); } /// @@ -39,9 +41,6 @@ public override void Update(float frameTime) { base.Update(frameTime); - if (!_timing.IsFirstTimePredicted) - return; - var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var watchingComponent)) { @@ -49,96 +48,112 @@ public override void Update(float frameTime) continue; // Все потенциально возможные смотрящие. Среди них те, что прошли фаст-чек из самых простых проверок - var potentialViewers = GetWatchers(uid); + using var potentialWatchers = ListPoolEntity.Rent(); + if (!TryGetAllEntitiesVisibleTo(uid, potentialWatchers.Value)) + { + SetNextTime(watchingComponent); + Dirty(uid, watchingComponent); + + continue; + } // Вызываем ивенты на потенциально смотрящих. Без особых проверок // Полезно в коде, который уже использует подобные проверки или не требует этого - foreach (var potentialViewer in potentialViewers) + foreach (var potentialViewer in potentialWatchers.Value) { + var simpleViewerEvent = new SimpleEntityLookedAtEvent((uid, watchingComponent)); + var simpleTargetEvent = new SimpleEntitySeenEvent(potentialViewer); + // За подробностями какой ивент для чего навести мышку на название ивента - RaiseLocalEvent(potentialViewer, new SimpleEntityLookedAtEvent((uid, watchingComponent))); - RaiseLocalEvent(uid, new SimpleEntitySeenEvent(potentialViewer)); + RaiseLocalEvent(potentialViewer, ref simpleViewerEvent); + RaiseLocalEvent(uid, ref simpleTargetEvent); + } + + // Если требуются только Simple ивенты, то нет смысла делать дальнейшие действия. + if (watchingComponent.SimpleMode) + { + SetNextTime(watchingComponent); + Dirty(uid, watchingComponent); + + continue; } // Проверяет всех потенциальных смотрящих на то, действительно ли они видят цель. // Каждый потенциально смотрящий проходит полный комплекс проверок. // Выдает полный список всех сущностей, кто действительно видит цель - if (!IsWatchedBy(uid, potentialViewers, viewers: out var viewers)) + using var realWatchers = ListPoolEntity.Rent(); + if (!TryGetWatchersFrom(uid, realWatchers.Value, potentialWatchers.Value, checkProximity: false)) + { + SetNextTime(watchingComponent); + Dirty(uid, watchingComponent); + continue; + } // Вызываем ивент на смотрящем, говорящие, что он действительно видит цель - foreach (var viewer in viewers) + foreach (var viewer in realWatchers.Value) { var netViewer = GetNetEntity(viewer); var firstTime = !watchingComponent.AlreadyLookedAt.ContainsKey(netViewer); - var blockerLevel = _proximity.GetLightOfSightBlockerLevel(viewer, uid); // Небольшая заглушка для удобства работы с ивентами. // Использовать firstTime не очень удобно, поэтому в качестве дополнительного способа определения будет TimeSpan.Zero watchingComponent.AlreadyLookedAt[netViewer] = TimeSpan.Zero; // За подробностями какой ивент для чего навести мышку на название ивента - RaiseLocalEvent(viewer, new EntityLookedAtEvent((uid, watchingComponent), firstTime, blockerLevel)); - RaiseLocalEvent(uid, new EntitySeenEvent(viewer, firstTime, blockerLevel)); + var viewerEvent = new EntityLookedAtEvent((uid, watchingComponent), firstTime); + var targetEvent = new EntitySeenEvent(viewer, firstTime); + + RaiseLocalEvent(viewer, ref viewerEvent); + RaiseLocalEvent(uid, ref targetEvent); // Добавляет смотрящего в список уже смотревших, чтобы позволить системам манипулировать этим // И предотвращать эффект, если игрок смотрит не первый раз или не так давно watchingComponent.AlreadyLookedAt[netViewer] = _timing.CurTime; } - Dirty(uid, watchingComponent); SetNextTime(watchingComponent); + Dirty(uid, watchingComponent); } } + /// + /// Устанавливает время следующей проверки видимости + /// private void SetNextTime(WatchingTargetComponent component) { - component.NextTimeWatchedCheck = _timing.CurTime + WatchingCheckInterval; + component.NextTimeWatchedCheck = _timing.CurTime + component.WatchingCheckInterval; } } /// /// Ивент вызываемый на смотрящем, передающий информации, что он посмотрел на кого-то /// -/// Цель, на которую посмотрели -/// Видим ли мы цель в первый раз -/// Линия видимости между смотрящим и целью, подробнее -public sealed class EntityLookedAtEvent(Entity target, bool firstTime, LineOfSightBlockerLevel blockerLevel) : EntityEventArgs -{ - public readonly Entity Target = target; - public readonly bool IsSeenFirstTime = firstTime; - public readonly LineOfSightBlockerLevel BlockerLevel = blockerLevel; -} +/// Цель, на которую посмотрели +/// Видим ли мы цель в первый раз +[ByRefEvent] +public readonly record struct EntityLookedAtEvent(Entity Target, bool FirstTime); /// /// Ивент вызываемый на цели, передающий информации, что на нее кто-то посмотрел /// -/// Смотрящий, который увидел цель -/// Видим ли мы цель в первый раз -/// Линия видимости между смотрящим и целью, подробнее -public sealed class EntitySeenEvent(EntityUid viewer, bool firstTime, LineOfSightBlockerLevel blockerLevel) : EntityEventArgs -{ - public readonly EntityUid Viewer = viewer; - public readonly bool IsSeenFirstTime = firstTime; - public readonly LineOfSightBlockerLevel BlockerLevel = blockerLevel; -} +/// Смотрящий, который увидел цель +/// Видим ли мы цель в первый раз +[ByRefEvent] +public readonly record struct EntitySeenEvent(EntityUid Viewer, bool FirstTime); /// /// Простой ивент, говорящий, что смотрящий посмотрел на цель. /// Вызывается до прохождения различных проверок на смотрящем. Если вдруг требуются собственная ручная проверка /// -/// Цель, на которую посмотри -public sealed class SimpleEntityLookedAtEvent(Entity target) : EntityEventArgs -{ - public readonly Entity Target = target; -} +/// Цель, на которую посмотри +[ByRefEvent] +public readonly record struct SimpleEntityLookedAtEvent(Entity Target); /// /// Простой ивент, говорящий, что на цель кто-то посмотрел. /// Вызывается до прохождения различных проверок на цели. Если вдруг требуются собственная ручная проверка /// -/// Смотрящий -public sealed class SimpleEntitySeenEvent(EntityUid viewer) : EntityEventArgs -{ - public readonly EntityUid Viewer = viewer; -} +/// Смотрящий +[ByRefEvent] +public readonly record struct SimpleEntitySeenEvent(EntityUid Viewer); diff --git a/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs b/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs index 21f3fe85c85..da92a6db574 100644 --- a/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs +++ b/Content.Shared/_Scp/Watching/WatchingTargetComponent.cs @@ -6,7 +6,7 @@ namespace Content.Shared._Scp.Watching; /// Компонент-маркер, который позволяет системе смотрения включить владельца в обработку /// Это позволит вызывать ивенты на владельце, когда на него кто-то посмотрит /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] public sealed partial class WatchingTargetComponent : Component { /// @@ -16,5 +16,24 @@ public sealed partial class WatchingTargetComponent : Component [AutoNetworkedField] public Dictionary AlreadyLookedAt = new(); - public TimeSpan NextTimeWatchedCheck; + /// + /// Время между проверками зрения + /// + [DataField] + public TimeSpan WatchingCheckInterval = TimeSpan.FromSeconds(0.2f); + + /// + /// Время следующей проверки зрения + /// + [AutoNetworkedField, AutoPausedField, ViewVariables] + public TimeSpan? NextTimeWatchedCheck; + + /// + /// Будет ли система обрабатывать только простые ивенты, исключая комплексные проверки и ивенты после них? + /// + /// + /// Используется, когда другие системы вручную обрабатывают данные, чтобы исключить двойную работу + /// + [DataField] + public bool SimpleMode; } diff --git a/Resources/Prototypes/_Scp/Alerts/alerts.yml b/Resources/Prototypes/_Scp/Alerts/alerts.yml index 6317641b73d..ce9e16bdd43 100644 --- a/Resources/Prototypes/_Scp/Alerts/alerts.yml +++ b/Resources/Prototypes/_Scp/Alerts/alerts.yml @@ -15,6 +15,7 @@ description: alert-desc-blinking minSeverity: 0 maxSeverity: 4 + clientHandled: true - type: alert id: Scp106LifeEssence diff --git a/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml b/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml index a8e73c7f29c..5945c4142b1 100644 --- a/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml +++ b/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml @@ -71,6 +71,8 @@ canStandingState: true - type: FearSource uponSeenState: Fear + - type: WatchingTarget + simpleMode: true - type: ShowBlinkable - type: ScpAnnounceOnSpawn text: scp096-announce-on-spawn diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml index 6a5ea003419..1aed21a3b06 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml @@ -65,7 +65,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: SecurityCommanderGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml index 202a8866b95..ec42f204c1a 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml @@ -59,7 +59,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: ExternalAdministrativeZoneCommandantGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml index fd2f30a10e3..78920db7053 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml @@ -54,7 +54,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: ExternalAdministrativeZoneOfficerGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml index de8fc66ffd3..33f51739002 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml @@ -43,7 +43,7 @@ Fear: 2.2 Terror: 5 baseJitterTime: 3 - fallOffChance: 2 + fallOffChance: 0.02 - type: startingGear id: JuniorExternalAdministrativeZoneOfficerGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml index 203577a659c..67ca4c5ca1b 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml @@ -54,7 +54,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: SeniorExternalAdministrativeZoneOfficerGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml index d3fcaa47cb1..07a3cf01b76 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml @@ -60,7 +60,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: HeavyContainmentZoneCommandantGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml index 496326553e2..914e026f047 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml @@ -54,7 +54,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: HeavyContainmentZoneOfficerGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml index 8b5b9b3f360..d991ca9b174 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml @@ -43,7 +43,7 @@ Fear: 2.2 Terror: 5 baseJitterTime: 3 - fallOffChance: 2 + fallOffChance: 0.02 - type: startingGear id: JuniorHeavyContainmentZoneOfficerGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml index b83ff2489c4..965c40a0e8b 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml @@ -54,7 +54,7 @@ Fear: 60 Terror: 350 baseJitterTime: 2 - fallOffChance: 1 + fallOffChance: 0.01 - type: startingGear id: SeniorHeavyContainmentZoneOfficerGear