diff --git a/Content.Server/Medical/Stethoscope/Components/StethoscopeComponent.cs b/Content.Server/Medical/Stethoscope/Components/StethoscopeComponent.cs
deleted file mode 100644
index d7e971e953..0000000000
--- a/Content.Server/Medical/Stethoscope/Components/StethoscopeComponent.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Medical.Stethoscope.Components
-{
- ///
- /// Adds an innate verb when equipped to use a stethoscope.
- ///
- [RegisterComponent]
- public sealed partial class StethoscopeComponent : Component
- {
- public bool IsActive = false;
-
- [DataField("delay")]
- public float Delay = 2.5f;
-
- [DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer))]
- 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
index dfce294a73..0000000000
--- a/Content.Server/Medical/Stethoscope/Components/WearingStethoscopeComponent.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Threading;
-
-namespace Content.Server.Medical.Components
-{
- ///
- /// Used to let doctors use the stethoscope on people.
- ///
- [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
index b8304c562a..0000000000
--- a/Content.Server/Medical/Stethoscope/StethoscopeSystem.cs
+++ /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(OnEquipped);
- SubscribeLocalEvent(OnUnequipped);
- SubscribeLocalEvent>(AddStethoscopeVerb);
- SubscribeLocalEvent(OnGetActions);
- SubscribeLocalEvent(OnStethoscopeAction);
- SubscribeLocalEvent(OnDoAfter);
- }
-
- ///
- /// Add the component the verb event subs to if the equippee is wearing the stethoscope.
- ///
- private void OnEquipped(EntityUid uid, StethoscopeComponent component, ref ClothingGotEquippedEvent args)
- {
- component.IsActive = true;
-
- var wearingComp = EnsureComp(args.Wearer);
- wearingComp.Stethoscope = uid;
- }
-
- private void OnUnequipped(EntityUid uid, StethoscopeComponent component, ref ClothingGotUnequippedEvent args)
- {
- if (!component.IsActive)
- return;
-
- RemComp(args.Wearer);
- component.IsActive = false;
- }
-
- ///
- /// This is raised when someone with WearingStethoscopeComponent requests verbs on an item.
- /// It returns if the target is not a mob.
- ///
- private void AddStethoscopeVerb(EntityUid uid, WearingStethoscopeComponent component, GetVerbsEvent args)
- {
- if (!args.CanInteract || !args.CanAccess)
- return;
-
- if (!HasComp(args.Target))
- return;
-
- if (component.CancelToken != null)
- return;
-
- if (!TryComp(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);
- }
-
- ///
- /// 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.
- ///
- 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(target) || !TryComp(target, out var mobState) || _mobStateSystem.IsDead(target, mobState))
- {
- _popupSystem.PopupEntity(Loc.GetString("stethoscope-dead"), target, user);
- return;
- }
-
- if (!TryComp(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;
- }
- }
-}
diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs
index 94a32f5ef3..fada9822a3 100644
--- a/Content.Shared/Inventory/InventorySystem.Relay.cs
+++ b/Content.Shared/Inventory/InventorySystem.Relay.cs
@@ -67,6 +67,8 @@ public partial class InventorySystem
SubscribeLocalEvent>(RefRelayInventoryEvent);
SubscribeLocalEvent>(OnGetEquipmentVerbs);
+ SubscribeLocalEvent>(OnGetInnateVerbs);
+
}
protected void RefRelayInventoryEvent(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 args)
+ {
+ // Automatically relay stripping related verbs to all equipped clothing.
+ var ev = new InventoryRelayedEvent>(args);
+ var enumerator = new InventorySlotEnumerator(component, SlotFlags.WITHOUT_POCKET);
+ while (enumerator.NextItem(out var item))
+ {
+ RaiseLocalEvent(item, ev);
+ }
+ }
+
}
///
diff --git a/Content.Shared/Medical/Stethoscope/Components/StethoscopeComponent.cs b/Content.Shared/Medical/Stethoscope/Components/StethoscopeComponent.cs
new file mode 100644
index 0000000000..7f740ef39c
--- /dev/null
+++ b/Content.Shared/Medical/Stethoscope/Components/StethoscopeComponent.cs
@@ -0,0 +1,31 @@
+using Content.Shared.FixedPoint;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Medical.Stethoscope.Components;
+
+///
+/// Adds a verb and action that allows the user to listen to the entity's breathing.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class StethoscopeComponent : Component
+{
+ ///
+ /// Time between each use of the stethoscope.
+ ///
+ [DataField]
+ public TimeSpan Delay = TimeSpan.FromSeconds(1.75);
+
+ ///
+ /// Last damage that was measured. Used to indicate if breathing is improving or getting worse.
+ ///
+ [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
index 11ac8a2684..0000000000
--- a/Content.Shared/Medical/Stethoscope/StethoscopeActionEvent.cs
+++ /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
index 0000000000..01d61aa06e
--- /dev/null
+++ b/Content.Shared/Medical/Stethoscope/StethoscopeSystem.cs
@@ -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>>(AddStethoscopeVerb);
+ SubscribeLocalEvent(OnGetActions);
+ SubscribeLocalEvent(OnStethoscopeAction);
+ SubscribeLocalEvent(OnDoAfter);
+ }
+
+ private void OnGetActions(Entity ent, ref GetItemActionsEvent args)
+ {
+ args.AddAction(ref ent.Comp.ActionEntity, ent.Comp.Action);
+ }
+
+ private void OnStethoscopeAction(Entity ent, ref StethoscopeActionEvent args)
+ {
+ StartListening(ent, args.Target);
+ }
+
+ private void AddStethoscopeVerb(Entity ent, ref InventoryRelayedEvent> args)
+ {
+ if (!args.Args.CanInteract || !args.Args.CanAccess)
+ return;
+
+ if (!HasComp(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 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 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 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(target, out var mobState) ||
+ !TryComp(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;
diff --git a/Content.Shared/Medical/StethoscopeDoAfterEvent.cs b/Content.Shared/Medical/StethoscopeDoAfterEvent.cs
index aeb1c133cf..d3f3962958 100644
--- a/Content.Shared/Medical/StethoscopeDoAfterEvent.cs
+++ b/Content.Shared/Medical/StethoscopeDoAfterEvent.cs
@@ -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;
diff --git a/Content.Shared/Verbs/Verb.cs b/Content.Shared/Verbs/Verb.cs
index 5faca9bb06..207c739466 100644
--- a/Content.Shared/Verbs/Verb.cs
+++ b/Content.Shared/Verbs/Verb.cs
@@ -281,12 +281,11 @@ namespace Content.Shared.Verbs
}
///
- /// 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.
///
///
- /// 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.
///
[Serializable, NetSerializable]
public sealed class InnateVerb : Verb
diff --git a/Resources/Locale/en-US/health-examinable/stethoscope.ftl b/Resources/Locale/en-US/health-examinable/stethoscope.ftl
index decfd7795b..d4baf4cc93 100644
--- a/Resources/Locale/en-US/health-examinable/stethoscope.ftl
+++ b/Resources/Locale/en-US/health-examinable/stethoscope.ftl
@@ -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}
diff --git a/Resources/Prototypes/Entities/Clothing/Neck/misc.yml b/Resources/Prototypes/Entities/Clothing/Neck/misc.yml
index f712ec1b1d..26071b5146 100644
--- a/Resources/Prototypes/Entities/Clothing/Neck/misc.yml
+++ b/Resources/Prototypes/Entities/Clothing/Neck/misc.yml
@@ -32,17 +32,36 @@
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
@@ -69,18 +88,6 @@
- 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