Skip to content

Commit 80bfcbb

Browse files
authored
Prevent partial smellers from detecting cloaked entities (#5557)
## Short description <!-- What do you propose to change with your PR? --> Make partial smellers (vulpkanins) unable to detect cloaked entities using stealth. This was an unintended feature for partial smellers. ## Why we need to add this <!-- What is the reason for adding these changes? Please post links to Discussions as well as Bug Reports here. Please describe how this will change the game balance. --> The scent system was designed to do things _**like**_ catching cloaked entities. I want scent perception to be a strong pull to the future K9 implementation. However, giving this to vulps has presented periodic balance concerns when it comes to this. Recently, I've noticed umbrae vampires and other cloaked antags being immediately outed by their scent trail by vulpkanin security. That's not exactly what we want out of the scent system. It's okay for the K9 to do because there's gonna be like one of them and their ability set will be limited. But for anyone to roll vulp and invalidate the entire stealth kit is unhealthy. ## Media (Video/Screenshots) <!-- If your PR contains in-game changes you must provide screenshots/videos of the changes. --> https://github.com/user-attachments/assets/db09283d-519c-474f-8655-c3ed46391450 ## Checks <!-- check boxes for faster reviewing of your PR --> - [X] I do not require assistance to complete the PR. - [X] Before posting/requesting review of a PR, I have verified that the changes work. - [X] I have added screenshots/videos of the changes, or this PR does not change in-game mechanics. - [X] I affirm that my changes are licensed under the [MIT License](https://github.com/ss14Starlight/space-station-14/blob/Starlight/LICENSE.TXT) and grant permission for use in this repository under its conditions. **Changelog** :cl: Sparlight - tweak: Partial smellers (vulpkanins) can no longer detect scent emissions from cloaked entities. <!-- If you want the players to know about changes made in this PR, specify them using the template outside the comment. Short and informative. :cl: STARLIGHT TEAM - add: Added Starlight. - remove: Removed SS13. - tweak: Changed SS14. - fix: Fixed Rinary. -->
1 parent 0867b94 commit 80bfcbb

3 files changed

Lines changed: 48 additions & 34 deletions

File tree

Content.Client/_Starlight/Scent/Systems/ScentTrackingSystem.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,10 @@ private void ApplyFilter(Entity<ScentMarkerComponent> ent, SmellerComponent smel
123123
return;
124124

125125
var visible = !IsPerceptionBlocked(smeller) &&
126-
!(smeller.Perception == ScentPerception.Partial && ent.Comp.ContainedIn != null && !ent.Comp.WasDead) &&
127-
!(smeller.Perception == ScentPerception.Partial && IsOutsideOwnEnclosure(ent, enclosure)) &&
128-
(smeller.TrackedScentId == null || ent.Comp.ScentId == smeller.TrackedScentId);
126+
!(smeller.Perception == ScentPerception.Partial && ent.Comp.WasCloaked) &&
127+
!(smeller.Perception == ScentPerception.Partial && ent.Comp.ContainedIn != null && !ent.Comp.WasDead) &&
128+
!(smeller.Perception == ScentPerception.Partial && IsOutsideOwnEnclosure(ent, enclosure)) &&
129+
(smeller.TrackedScentId == null || ent.Comp.ScentId == smeller.TrackedScentId);
129130
_sprite.SetVisible((ent.Owner, sprite), visible);
130131
}
131132

Content.Server/_Starlight/Scent/Systems/ScentSystem.cs

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
using Content.Shared.Nutrition.EntitySystems;
2525
using Content.Shared.Popups;
2626
using Content.Shared.Storage.Components;
27+
using Content.Shared.Stealth;
28+
using Content.Shared.Stealth.Components;
2729
using Content.Shared.Tag;
2830
using Content.Shared.Verbs;
2931
using Content.Shared.Zombies;
@@ -36,20 +38,21 @@
3638

3739
namespace Content.Server._Starlight.Scent.Systems;
3840

39-
public sealed class ScentSystem : SharedScentSystem
41+
public sealed partial class ScentSystem : SharedScentSystem
4042
{
41-
[Dependency] private readonly IRobustRandom _random = default!;
42-
[Dependency] private readonly IGameTiming _timing = default!;
43-
[Dependency] private readonly SharedEyeSystem _eye = default!;
44-
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
45-
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
46-
[Dependency] private readonly PopupSystem _popup = default!;
47-
[Dependency] private readonly SharedAudioSystem _audio = default!;
48-
[Dependency] private readonly SharedTransformSystem _transform = default!;
49-
[Dependency] private readonly IPrototypeManager _prototype = default!;
50-
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
51-
[Dependency] private readonly InventorySystem _inventory = default!;
52-
[Dependency] private readonly TagSystem _tags = default!;
43+
[Dependency] private IRobustRandom _random = default!;
44+
[Dependency] private IGameTiming _timing = default!;
45+
[Dependency] private SharedEyeSystem _eye = default!;
46+
[Dependency] private SharedUserInterfaceSystem _ui = default!;
47+
[Dependency] private DoAfterSystem _doAfterSystem = default!;
48+
[Dependency] private PopupSystem _popup = default!;
49+
[Dependency] private SharedAudioSystem _audio = default!;
50+
[Dependency] private SharedTransformSystem _transform = default!;
51+
[Dependency] private IPrototypeManager _prototype = default!;
52+
[Dependency] private AtmosphereSystem _atmosphere = default!;
53+
[Dependency] private InventorySystem _inventory = default!;
54+
[Dependency] private TagSystem _tags = default!;
55+
[Dependency] private SharedStealthSystem _stealth = default!;
5356

5457
private const string ScentMarkerPrototype = "ScentMarker";
5558
private const int ScentIdByteLength = 8;
@@ -170,7 +173,7 @@ private void OnSniffMenuClosed(Entity<SmellerComponent> ent, ref BoundUIClosedEv
170173

171174
private void OnSmellerMove(Entity<SmellerComponent> ent, ref MoveEvent args)
172175
{
173-
if (ent.Comp.SniffTarget is not { } target || !TryComp<TransformComponent>(target, out var targetXform))
176+
if (ent.Comp.SniffTarget is not { } target || !TryComp(target, out TransformComponent? targetXform))
174177
return;
175178

176179
if (_transform.InRange(args.NewPosition, targetXform.Coordinates, ent.Comp.SniffRange))
@@ -181,17 +184,15 @@ private void OnSmellerMove(Entity<SmellerComponent> ent, ref MoveEvent args)
181184
}
182185

183186
// Zombies shouldn't be able to hunt survivors by scent.
184-
private void OnSmellerZombified(Entity<SmellerComponent> ent, ref EntityZombifiedEvent args)
185-
{
187+
private void OnSmellerZombified(Entity<SmellerComponent> ent, ref EntityZombifiedEvent args) =>
186188
RemComp<SmellerComponent>(ent.Owner);
187-
}
188189

189190
private void OnTrackMessage(EntityUid uid, SmellerComponent component, ScentSniffTrackMessage args)
190191
{
191192
if (component.SniffTarget is not { } target || !Exists(target))
192193
return;
193194

194-
if (!TryComp<TransformComponent>(uid, out var xform) || !TryComp<TransformComponent>(target, out var targetXform))
195+
if (!TryComp(uid, out TransformComponent? xform) || !TryComp(target, out TransformComponent? targetXform))
195196
return;
196197

197198
if (!_transform.InRange(xform.Coordinates, targetXform.Coordinates, component.SniffRange))
@@ -420,7 +421,7 @@ public override void Update(float frameTime)
420421

421422
scent.NextEmitTime = now + RollEmitDelay(scent);
422423

423-
if (TryComp<TransformComponent>(uid, out var xform))
424+
if (TryComp(uid, out TransformComponent? xform))
424425
EmitScent((uid, scent, xform), scentId);
425426
}
426427
}
@@ -466,6 +467,7 @@ private void EmitScent(Entity<ScentComponent, TransformComponent> ent, string sc
466467
markerComp.TotalDuration = decayTime;
467468
markerComp.ContainedIn = GetAirtightContainer(xform);
468469
markerComp.WasDead = MobState.IsDead(uid);
470+
markerComp.WasCloaked = IsCloaked(uid);
469471
Dirty(marker, markerComp);
470472

471473
var despawn = Comp<TimedDespawnComponent>(marker);
@@ -497,7 +499,7 @@ private bool IsSealed(EntityUid uid)
497499
if (TryComp<EntityStorageComponent>(parent, out var storage) && storage.Airtight)
498500
return parent;
499501

500-
if (!TryComp<TransformComponent>(parent, out var parentXform))
502+
if (!TryComp(parent, out TransformComponent? parentXform))
501503
break;
502504

503505
parent = parentXform.ParentUid;
@@ -506,17 +508,20 @@ private bool IsSealed(EntityUid uid)
506508
return null;
507509
}
508510

511+
// Same threshold the game already uses to decide an entity is too hidden to examine
512+
// (StealthComponent.ExamineThreshold), reused here instead of picking a new cutoff.
513+
private bool IsCloaked(EntityUid uid) =>
514+
TryComp<StealthComponent>(uid, out var stealth) &&
515+
stealth.Enabled &&
516+
_stealth.GetVisibility(uid, stealth) <= stealth.ExamineThreshold;
517+
509518
// Suppressed everywhere in the gas pipe network except at a terminus (vent/scrubber), where
510519
// the scent is genuinely reaching open air.
511-
private bool IsHiddenVentCrawl(EntityUid uid)
512-
{
513-
if (!TryComp<BeingVentCrawlComponent>(uid, out var ventCrawl))
514-
return false;
515-
516-
return !TryComp<VentCrawlHolderComponent>(ventCrawl.Holder, out var holder) ||
517-
holder.CurrentTube is not { } tube ||
518-
!HasComp<VentCrawlEntryComponent>(tube);
519-
}
520+
private bool IsHiddenVentCrawl(EntityUid uid) =>
521+
TryComp<BeingVentCrawlComponent>(uid, out var ventCrawl) &&
522+
(!TryComp<VentCrawlHolderComponent>(ventCrawl.Holder, out var holder) ||
523+
holder.CurrentTube is not { } tube ||
524+
!HasComp<VentCrawlEntryComponent>(tube));
520525

521526
// Only merges into our own chain tail, never any other nearby marker. Revisiting an old spot
522527
// would otherwise rewrite the trail's visit order.
@@ -526,7 +531,7 @@ private bool TryMergeIntoExisting(Entity<ScentComponent, TransformComponent> ent
526531

527532
if (scent.LastMarkerEntity is not { } tail ||
528533
!TryComp<ScentMarkerComponent>(tail, out var marker) ||
529-
!TryComp<TransformComponent>(tail, out var tailXform))
534+
!TryComp(tail, out TransformComponent? tailXform))
530535
{
531536
return false;
532537
}
@@ -541,6 +546,7 @@ private bool TryMergeIntoExisting(Entity<ScentComponent, TransformComponent> ent
541546
marker.TotalDuration = decayTime;
542547
marker.ContainedIn = GetAirtightContainer(xform);
543548
marker.WasDead = MobState.IsDead(uid);
549+
marker.WasCloaked = IsCloaked(uid);
544550
Dirty(tail, marker);
545551

546552
if (TryComp<TimedDespawnComponent>(tail, out var despawn))

Content.Shared/_Starlight/Scent/Components/ScentMarkerComponent.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ public sealed partial class ScentMarkerComponent : Component
2121
public TimeSpan ExpiresAt;
2222

2323
/// <summary>
24-
// How pooled this marker is, 0-1. Maps to alpha/scale client-side.
24+
/// How pooled this marker is, 0-1. Maps to alpha/scale client-side.
25+
/// </summary>
2526
/// </summary>
2627
[DataField, AutoNetworkedField]
2728
public float Strength;
@@ -44,4 +45,10 @@ public sealed partial class ScentMarkerComponent : Component
4445
/// </summary>
4546
[DataField, AutoNetworkedField]
4647
public bool WasDead;
48+
/// <summary>
49+
/// Whether the emitter was cloaked (StealthComponent, hidden past its ExamineThreshold) at
50+
/// the moment of this emission.
51+
/// </summary>
52+
[DataField, AutoNetworkedField]
53+
public bool WasCloaked;
4754
}

0 commit comments

Comments
 (0)