]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Cleanup and small update to the stethoscope! (#36210)
authorbeck-thompson <107373427+beck-thompson@users.noreply.github.com>
Mon, 31 Mar 2025 02:27:08 +0000 (19:27 -0700)
committerGitHub <noreply@github.com>
Mon, 31 Mar 2025 02:27:08 +0000 (22:27 -0400)
* First commit

* Address most of the review!

Content.Server/Medical/Stethoscope/Components/StethoscopeComponent.cs [deleted file]
Content.Server/Medical/Stethoscope/Components/WearingStethoscopeComponent.cs [deleted file]
Content.Server/Medical/Stethoscope/StethoscopeSystem.cs [deleted file]
Content.Shared/Inventory/InventorySystem.Relay.cs
Content.Shared/Medical/Stethoscope/Components/StethoscopeComponent.cs [new file with mode: 0644]
Content.Shared/Medical/Stethoscope/StethoscopeActionEvent.cs [deleted file]
Content.Shared/Medical/Stethoscope/StethoscopeSystem.cs [new file with mode: 0644]
Content.Shared/Medical/StethoscopeDoAfterEvent.cs
Content.Shared/Verbs/Verb.cs
Resources/Locale/en-US/health-examinable/stethoscope.ftl
Resources/Prototypes/Entities/Clothing/Neck/misc.yml

diff --git a/Content.Server/Medical/Stethoscope/Components/StethoscopeComponent.cs b/Content.Server/Medical/Stethoscope/Components/StethoscopeComponent.cs
deleted file mode 100644 (file)
index d7e971e..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Medical.Stethoscope.Components
-{
-    /// <summary>
-    /// Adds an innate verb when equipped to use a stethoscope.
-    /// </summary>
-    [RegisterComponent]
-    public sealed partial class StethoscopeComponent : Component
-    {
-        public bool IsActive = false;
-
-        [DataField("delay")]
-        public float Delay = 2.5f;
-
-        [DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-        public string Action = "ActionStethoscope";
-
-        [DataField("actionEntity")] public EntityUid? ActionEntity;
-    }
-}
diff --git a/Content.Server/Medical/Stethoscope/Components/WearingStethoscopeComponent.cs b/Content.Server/Medical/Stethoscope/Components/WearingStethoscopeComponent.cs
deleted file mode 100644 (file)
index dfce294..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Threading;
-
-namespace Content.Server.Medical.Components
-{
-    /// <summary>
-    /// Used to let doctors use the stethoscope on people.
-    /// </summary>
-    [RegisterComponent]
-    public sealed partial class WearingStethoscopeComponent : Component
-    {
-        public CancellationTokenSource? CancelToken;
-
-        [DataField("delay")]
-        public float Delay = 2.5f;
-
-        public EntityUid Stethoscope = default!;
-    }
-}
diff --git a/Content.Server/Medical/Stethoscope/StethoscopeSystem.cs b/Content.Server/Medical/Stethoscope/StethoscopeSystem.cs
deleted file mode 100644 (file)
index b8304c5..0000000
+++ /dev/null
@@ -1,153 +0,0 @@
-using Content.Server.Body.Components;
-using Content.Server.Medical.Components;
-using Content.Server.Medical.Stethoscope.Components;
-using Content.Server.Popups;
-using Content.Shared.Actions;
-using Content.Shared.Clothing;
-using Content.Shared.Damage;
-using Content.Shared.DoAfter;
-using Content.Shared.FixedPoint;
-using Content.Shared.Medical;
-using Content.Shared.Medical.Stethoscope;
-using Content.Shared.Mobs.Components;
-using Content.Shared.Mobs.Systems;
-using Content.Shared.Verbs;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Medical.Stethoscope
-{
-    public sealed class StethoscopeSystem : EntitySystem
-    {
-        [Dependency] private readonly PopupSystem _popupSystem = default!;
-        [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
-        [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
-
-        public override void Initialize()
-        {
-            base.Initialize();
-            SubscribeLocalEvent<StethoscopeComponent, ClothingGotEquippedEvent>(OnEquipped);
-            SubscribeLocalEvent<StethoscopeComponent, ClothingGotUnequippedEvent>(OnUnequipped);
-            SubscribeLocalEvent<WearingStethoscopeComponent, GetVerbsEvent<InnateVerb>>(AddStethoscopeVerb);
-            SubscribeLocalEvent<StethoscopeComponent, GetItemActionsEvent>(OnGetActions);
-            SubscribeLocalEvent<StethoscopeComponent, StethoscopeActionEvent>(OnStethoscopeAction);
-            SubscribeLocalEvent<StethoscopeComponent, StethoscopeDoAfterEvent>(OnDoAfter);
-        }
-
-        /// <summary>
-        /// Add the component the verb event subs to if the equippee is wearing the stethoscope.
-        /// </summary>
-        private void OnEquipped(EntityUid uid, StethoscopeComponent component, ref ClothingGotEquippedEvent args)
-        {
-            component.IsActive = true;
-
-            var wearingComp = EnsureComp<WearingStethoscopeComponent>(args.Wearer);
-            wearingComp.Stethoscope = uid;
-        }
-
-        private void OnUnequipped(EntityUid uid, StethoscopeComponent component, ref ClothingGotUnequippedEvent args)
-        {
-            if (!component.IsActive)
-                return;
-
-            RemComp<WearingStethoscopeComponent>(args.Wearer);
-            component.IsActive = false;
-        }
-
-        /// <summary>
-        /// This is raised when someone with WearingStethoscopeComponent requests verbs on an item.
-        /// It returns if the target is not a mob.
-        /// </summary>
-        private void AddStethoscopeVerb(EntityUid uid, WearingStethoscopeComponent component, GetVerbsEvent<InnateVerb> args)
-        {
-            if (!args.CanInteract || !args.CanAccess)
-                return;
-
-            if (!HasComp<MobStateComponent>(args.Target))
-                return;
-
-            if (component.CancelToken != null)
-                return;
-
-            if (!TryComp<StethoscopeComponent>(component.Stethoscope, out var stetho))
-                return;
-
-            InnateVerb verb = new()
-            {
-                Act = () =>
-                {
-                    StartListening(component.Stethoscope, uid, args.Target, stetho); // start doafter
-                },
-                Text = Loc.GetString("stethoscope-verb"),
-                Icon = new SpriteSpecifier.Rsi(new ("Clothing/Neck/Misc/stethoscope.rsi"), "icon"),
-                Priority = 2
-            };
-            args.Verbs.Add(verb);
-        }
-
-
-        private void OnStethoscopeAction(EntityUid uid, StethoscopeComponent component, StethoscopeActionEvent args)
-        {
-            StartListening(uid, args.Performer, args.Target, component);
-        }
-
-        private void OnGetActions(EntityUid uid, StethoscopeComponent component, GetItemActionsEvent args)
-        {
-            args.AddAction(ref component.ActionEntity, component.Action);
-        }
-
-        // construct the doafter and start it
-        private void StartListening(EntityUid scope, EntityUid user, EntityUid target, StethoscopeComponent comp)
-        {
-            _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, comp.Delay, new StethoscopeDoAfterEvent(), scope, target: target, used: scope)
-            {
-                NeedHand = true,
-                BreakOnMove = true,
-            });
-        }
-
-        private void OnDoAfter(EntityUid uid, StethoscopeComponent component, DoAfterEvent args)
-        {
-            if (args.Handled || args.Cancelled || args.Args.Target == null)
-                return;
-
-            ExamineWithStethoscope(args.Args.User, args.Args.Target.Value);
-        }
-
-        /// <summary>
-        /// Return a value based on the total oxyloss of the target.
-        /// Could be expanded in the future with reagent effects etc.
-        /// The loc lines are taken from the goon wiki.
-        /// </summary>
-        public void ExamineWithStethoscope(EntityUid user, EntityUid target)
-        {
-            // The mob check seems a bit redundant but (1) they could conceivably have lost it since when the doafter started and (2) I need it for .IsDead()
-            if (!HasComp<RespiratorComponent>(target) || !TryComp<MobStateComponent>(target, out var mobState) || _mobStateSystem.IsDead(target, mobState))
-            {
-                _popupSystem.PopupEntity(Loc.GetString("stethoscope-dead"), target, user);
-                return;
-            }
-
-            if (!TryComp<DamageableComponent>(target, out var damage))
-                return;
-            // these should probably get loc'd at some point before a non-english fork accidentally breaks a bunch of stuff that does this
-            if (!damage.Damage.DamageDict.TryGetValue("Asphyxiation", out var value))
-                return;
-
-            var message = GetDamageMessage(value);
-
-            _popupSystem.PopupEntity(Loc.GetString(message), target, user);
-        }
-
-        private string GetDamageMessage(FixedPoint2 totalOxyloss)
-        {
-            var msg = (int) totalOxyloss switch
-            {
-                < 20 => "stethoscope-normal",
-                < 60 => "stethoscope-hyper",
-                < 80 => "stethoscope-irregular",
-                _ => "stethoscope-fucked"
-            };
-            return msg;
-        }
-    }
-}
index 94a32f5ef3b46d3f91b264585d32697b87825012..fada9822a39177b81b7a2ee2bc30a844cf435a54 100644 (file)
@@ -67,6 +67,8 @@ public partial class InventorySystem
         SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowCriminalRecordIconsComponent>>(RefRelayInventoryEvent);
 
         SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<EquipmentVerb>>(OnGetEquipmentVerbs);
+        SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<InnateVerb>>(OnGetInnateVerbs);
+
     }
 
     protected void RefRelayInventoryEvent<T>(EntityUid uid, InventoryComponent component, ref T args) where T : IInventoryRelayEvent
@@ -121,6 +123,17 @@ public partial class InventorySystem
         }
     }
 
+    private void OnGetInnateVerbs(EntityUid uid, InventoryComponent component, GetVerbsEvent<InnateVerb> args)
+    {
+        // Automatically relay stripping related verbs to all equipped clothing.
+        var ev = new InventoryRelayedEvent<GetVerbsEvent<InnateVerb>>(args);
+        var enumerator = new InventorySlotEnumerator(component, SlotFlags.WITHOUT_POCKET);
+        while (enumerator.NextItem(out var item))
+        {
+            RaiseLocalEvent(item, ev);
+        }
+    }
+
 }
 
 /// <summary>
diff --git a/Content.Shared/Medical/Stethoscope/Components/StethoscopeComponent.cs b/Content.Shared/Medical/Stethoscope/Components/StethoscopeComponent.cs
new file mode 100644 (file)
index 0000000..7f740ef
--- /dev/null
@@ -0,0 +1,31 @@
+using Content.Shared.FixedPoint;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Medical.Stethoscope.Components;
+
+/// <summary>
+///     Adds a verb and action that allows the user to listen to the entity's breathing.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class StethoscopeComponent : Component
+{
+    /// <summary>
+    ///     Time between each use of the stethoscope.
+    /// </summary>
+    [DataField]
+    public TimeSpan Delay = TimeSpan.FromSeconds(1.75);
+
+    /// <summary>
+    ///     Last damage that was measured. Used to indicate if breathing is improving or getting worse.
+    /// </summary>
+    [DataField]
+    public FixedPoint2? LastMeasuredDamage;
+
+    [DataField]
+    public EntProtoId Action = "ActionStethoscope";
+
+    [DataField]
+    public EntityUid? ActionEntity;
+}
+
diff --git a/Content.Shared/Medical/Stethoscope/StethoscopeActionEvent.cs b/Content.Shared/Medical/Stethoscope/StethoscopeActionEvent.cs
deleted file mode 100644 (file)
index 11ac8a2..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-using Content.Shared.Actions;
-
-namespace Content.Shared.Medical.Stethoscope;
-
-public sealed partial class StethoscopeActionEvent : EntityTargetActionEvent
-{
-}
diff --git a/Content.Shared/Medical/Stethoscope/StethoscopeSystem.cs b/Content.Shared/Medical/Stethoscope/StethoscopeSystem.cs
new file mode 100644 (file)
index 0000000..01d61aa
--- /dev/null
@@ -0,0 +1,148 @@
+using Content.Shared.Actions;
+using Content.Shared.Damage;
+using Content.Shared.DoAfter;
+using Content.Shared.FixedPoint;
+using Content.Shared.Inventory;
+using Content.Shared.Medical.Stethoscope.Components;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Verbs;
+using Robust.Shared.Containers;
+
+namespace Content.Shared.Medical.Stethoscope;
+
+public sealed class StethoscopeSystem : EntitySystem
+{
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private readonly MobStateSystem _mobState = default!;
+    [Dependency] private readonly SharedContainerSystem _container = default!;
+
+    // The damage type to "listen" for with the stethoscope.
+    private const string DamageToListenFor = "Asphyxiation";
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<StethoscopeComponent, InventoryRelayedEvent<GetVerbsEvent<InnateVerb>>>(AddStethoscopeVerb);
+        SubscribeLocalEvent<StethoscopeComponent, GetItemActionsEvent>(OnGetActions);
+        SubscribeLocalEvent<StethoscopeComponent, StethoscopeActionEvent>(OnStethoscopeAction);
+        SubscribeLocalEvent<StethoscopeComponent, StethoscopeDoAfterEvent>(OnDoAfter);
+    }
+
+    private void OnGetActions(Entity<StethoscopeComponent> ent, ref GetItemActionsEvent args)
+    {
+        args.AddAction(ref ent.Comp.ActionEntity, ent.Comp.Action);
+    }
+
+    private void OnStethoscopeAction(Entity<StethoscopeComponent> ent, ref StethoscopeActionEvent args)
+    {
+        StartListening(ent, args.Target);
+    }
+
+    private void AddStethoscopeVerb(Entity<StethoscopeComponent> ent, ref InventoryRelayedEvent<GetVerbsEvent<InnateVerb>> args)
+    {
+        if (!args.Args.CanInteract || !args.Args.CanAccess)
+            return;
+
+        if (!HasComp<MobStateComponent>(args.Args.Target))
+            return;
+
+        var target = args.Args.Target;
+
+        InnateVerb verb = new()
+        {
+            Act = () => StartListening(ent, target),
+            Text = Loc.GetString("stethoscope-verb"),
+            IconEntity = GetNetEntity(ent),
+            Priority = 2,
+        };
+        args.Args.Verbs.Add(verb);
+    }
+
+    private void StartListening(Entity<StethoscopeComponent> ent, EntityUid target)
+    {
+        if (!_container.TryGetContainingContainer((ent, null, null), out var container))
+            return;
+
+        _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, container.Owner, ent.Comp.Delay, new StethoscopeDoAfterEvent(), ent, target: target, used: ent)
+        {
+            DuplicateCondition = DuplicateConditions.SameEvent,
+            BreakOnMove = true,
+            Hidden = true,
+            BreakOnHandChange = false,
+        });
+    }
+
+    private void OnDoAfter(Entity<StethoscopeComponent> ent, ref StethoscopeDoAfterEvent args)
+    {
+        var target = args.Target;
+
+        if (args.Handled || target == null || args.Cancelled)
+        {
+            ent.Comp.LastMeasuredDamage = null;
+            return;
+        }
+
+        ExamineWithStethoscope(ent, args.Args.User, target.Value);
+
+        args.Repeat = true;
+    }
+
+    private void ExamineWithStethoscope(Entity<StethoscopeComponent> stethoscope, EntityUid user, EntityUid target)
+    {
+        // TODO: Add check for respirator component when it gets moved to shared.
+        // If the mob is dead or cannot asphyxiation damage, the popup shows nothing.
+        if (!TryComp<MobStateComponent>(target, out var mobState)                        ||
+            !TryComp<DamageableComponent>(target, out var damageComp) ||
+            _mobState.IsDead(target, mobState)                                           ||
+            !damageComp.Damage.DamageDict.TryGetValue(DamageToListenFor, out var asphyxDmg))
+        {
+            _popup.PopupPredicted(Loc.GetString("stethoscope-nothing"), target, user);
+            stethoscope.Comp.LastMeasuredDamage = null;
+            return;
+        }
+
+        var absString = GetAbsoluteDamageString(asphyxDmg);
+
+        // Don't show the change if this is the first time listening.
+        if (stethoscope.Comp.LastMeasuredDamage == null)
+        {
+            _popup.PopupPredicted(absString, target, user);
+        }
+        else
+        {
+            var deltaString = GetDeltaDamageString(stethoscope.Comp.LastMeasuredDamage.Value, asphyxDmg);
+            _popup.PopupPredicted(Loc.GetString("stethoscope-combined-status", ("absolute", absString), ("delta", deltaString)), target, user);
+        }
+
+        stethoscope.Comp.LastMeasuredDamage = asphyxDmg;
+    }
+
+    private string GetAbsoluteDamageString(FixedPoint2 asphyxDmg)
+    {
+        var msg = (int) asphyxDmg switch
+        {
+            < 10 => "stethoscope-normal",
+            < 30 => "stethoscope-raggedy",
+            < 60 => "stethoscope-hyper",
+            < 80 => "stethoscope-irregular",
+            _    => "stethoscope-fucked",
+        };
+        return Loc.GetString(msg);
+    }
+
+    private string GetDeltaDamageString(FixedPoint2 lastDamage, FixedPoint2 currentDamage)
+    {
+        if (lastDamage > currentDamage)
+            return Loc.GetString("stethoscope-delta-improving");
+        if (lastDamage < currentDamage)
+            return Loc.GetString("stethoscope-delta-worsening");
+        return Loc.GetString("stethoscope-delta-steady");
+    }
+
+}
+
+public sealed partial class StethoscopeActionEvent : EntityTargetActionEvent;
index aeb1c133cf74a498d1ab486392f94f74ae2534b1..d3f3962958744dfbaa654ae0b3d135fb66c1b3e9 100644 (file)
@@ -4,6 +4,4 @@ using Robust.Shared.Serialization;
 namespace Content.Shared.Medical;
 
 [Serializable, NetSerializable]
-public sealed partial class StethoscopeDoAfterEvent : SimpleDoAfterEvent
-{
-}
+public sealed partial class StethoscopeDoAfterEvent : SimpleDoAfterEvent;
index 5faca9bb06771b3524aa110f2c5a175e3abc712b..207c7394668413b300e4844fb35e825abc0942bb 100644 (file)
@@ -281,12 +281,11 @@ namespace Content.Shared.Verbs
     }
 
     /// <summary>
-    ///     This is for verbs facilitated by components on the user.
+    ///     This is for verbs facilitated by components on the user or their clothing.
     ///     Verbs from clothing, species, etc. rather than a held item.
     /// </summary>
     /// <remarks>
-    ///     Add a component to the user's entity and sub to the get verbs event
-    ///     and it'll appear in the verbs menu on any target.
+    ///     This will get relayed to all clothing (Not pockets) through an inventory relay event.
     /// </remarks>
     [Serializable, NetSerializable]
     public sealed class InnateVerb : Verb
index decfd7795b332a00d1a0a87e0bfc1870d7732aac..d4baf4cc93fe3c5aaca0df826c7051c8e638ec3b 100644 (file)
@@ -1,6 +1,15 @@
 stethoscope-verb = Listen with stethoscope
-stethoscope-dead = You hear nothing.
+
+stethoscope-nothing = You don't hear anything.
+
 stethoscope-normal = You hear normal breathing.
+stethoscope-raggedy = You hear raggedy breathing.
 stethoscope-hyper = You hear hyperventilation.
 stethoscope-irregular = You hear hyperventilation with an irregular pattern.
 stethoscope-fucked = You hear twitchy, labored breathing interspersed with short gasps.
+
+stethoscope-delta-steady = It's steady.
+stethoscope-delta-improving = It's improving.
+stethoscope-delta-worsening = It's getting worse.
+
+stethoscope-combined-status = {$absolute} {$delta}
index f712ec1b1df92d6b5e44bfb5b975d26bd996e530..26071b51468d7ba1be515fd058cadc44cd9c0f8f 100644 (file)
       path: /Audio/Items/flashlight_off.ogg
 
 - type: entity
-  parent: ClothingNeckBase
+  parent: Clothing
   id: ClothingNeckStethoscope
   name: stethoscope
   description: An outdated medical apparatus for listening to the sounds of the human body. It also makes you look like you know what you're doing.
   components:
+  - type: Item
+    size: Small
   - type: Sprite
     sprite: Clothing/Neck/Misc/stethoscope.rsi
+    state: icon
   - type: Clothing
     sprite: Clothing/Neck/Misc/stethoscope.rsi
+    quickEquip: true
+    slots:
+    - neck
   - type: Stethoscope
 
+- type: entity
+  id: ActionStethoscope
+  name: Listen with stethoscope
+  components:
+  - type: EntityTargetAction
+    icon:
+      sprite: Clothing/Neck/Misc/stethoscope.rsi
+      state: icon
+    event: !type:StethoscopeActionEvent
+    checkCanInteract: false
+    priority: -1
+    itemIconStyle: BigAction
+
 - type: entity
   parent: ClothingNeckBase
   id: ClothingNeckBling
   - type: TypingIndicatorClothing
     proto: lawyer
 
-- type: entity
-  id: ActionStethoscope
-  name: Listen with stethoscope
-  components:
-  - type: EntityTargetAction
-    icon:
-      sprite: Clothing/Neck/Misc/stethoscope.rsi
-      state: icon
-    event: !type:StethoscopeActionEvent
-    checkCanInteract: false
-    priority: -1
-
 - type: entity
   parent: ClothingNeckBase
   id: Dinkystar