From 73478a1ad111b2ce4f17171774173b2fb7d226f3 Mon Sep 17 00:00:00 2001
From: Rane <60792108+Elijahrane@users.noreply.github.com>
Date: Sun, 5 Jun 2022 21:37:29 -0400
Subject: [PATCH] Port stethoscopes + innate verbs from nyano (#8228)
* port stethoscopes from nyanotrasen
* remove mono crash wtf
* don't touch puddle
* Switch to using action
* both verb and action
* Address reviews
---
.../Components/StethoscopeComponent.cs | 28 +++
.../Components/WearingStethoscopeComponent.cs | 18 ++
.../Medical/Stethoscope/StethoscopeSystem.cs | 199 ++++++++++++++++++
Content.Shared/Verbs/SharedVerbSystem.cs | 7 +
Content.Shared/Verbs/Verb.cs | 19 ++
.../en-US/health-examinable/stethoscope.ftl | 6 +
.../Entities/Clothing/Neck/misc.yml | 3 +-
7 files changed, 279 insertions(+), 1 deletion(-)
create mode 100644 Content.Server/Medical/Stethoscope/Components/StethoscopeComponent.cs
create mode 100644 Content.Server/Medical/Stethoscope/Components/WearingStethoscopeComponent.cs
create mode 100644 Content.Server/Medical/Stethoscope/StethoscopeSystem.cs
create mode 100644 Resources/Locale/en-US/health-examinable/stethoscope.ftl
diff --git a/Content.Server/Medical/Stethoscope/Components/StethoscopeComponent.cs b/Content.Server/Medical/Stethoscope/Components/StethoscopeComponent.cs
new file mode 100644
index 0000000000..49b926e72d
--- /dev/null
+++ b/Content.Server/Medical/Stethoscope/Components/StethoscopeComponent.cs
@@ -0,0 +1,28 @@
+using System.Threading;
+using Content.Shared.Actions.ActionTypes;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Medical.Components
+{
+ ///
+ /// Adds an innate verb when equipped to use a stethoscope.
+ ///
+ [RegisterComponent]
+ public sealed class StethoscopeComponent : Component
+ {
+ public bool IsActive = false;
+
+ public CancellationTokenSource? CancelToken;
+
+ [DataField("delay")]
+ public float Delay = 2.5f;
+
+ public EntityTargetAction Action = new()
+ {
+ Icon = new SpriteSpecifier.Texture(new ResourcePath("Clothing/Neck/Misc/stethoscope.rsi/icon.png")),
+ Name = "stethoscope-verb",
+ Priority = -1,
+ Event = new StethoscopeActionEvent(),
+ };
+ }
+}
diff --git a/Content.Server/Medical/Stethoscope/Components/WearingStethoscopeComponent.cs b/Content.Server/Medical/Stethoscope/Components/WearingStethoscopeComponent.cs
new file mode 100644
index 0000000000..64f7694bbc
--- /dev/null
+++ b/Content.Server/Medical/Stethoscope/Components/WearingStethoscopeComponent.cs
@@ -0,0 +1,18 @@
+using System.Threading;
+
+namespace Content.Server.Medical.Components
+{
+ ///
+ /// Used to let doctors use the stethoscope on people.
+ ///
+ [RegisterComponent]
+ public sealed 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
new file mode 100644
index 0000000000..fd86bae88e
--- /dev/null
+++ b/Content.Server/Medical/Stethoscope/StethoscopeSystem.cs
@@ -0,0 +1,199 @@
+using System.Threading;
+using Content.Shared.Verbs;
+using Content.Shared.Inventory.Events;
+using Content.Shared.MobState.Components;
+using Content.Shared.FixedPoint;
+using Content.Shared.Damage;
+using Content.Shared.Actions;
+using Content.Server.Clothing.Components;
+using Content.Server.Medical.Components;
+using Content.Server.Popups;
+using Content.Server.Body.Components;
+using Content.Server.DoAfter;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Medical
+{
+ public sealed class StethoscopeSystem : EntitySystem
+ {
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnEquipped);
+ SubscribeLocalEvent(OnUnequipped);
+ SubscribeLocalEvent>(AddStethoscopeVerb);
+ SubscribeLocalEvent(OnGetActions);
+ SubscribeLocalEvent(OnStethoscopeAction);
+ SubscribeLocalEvent(OnListenSuccess);
+ SubscribeLocalEvent(OnListenCancelled);
+ }
+
+ ///
+ /// Add the component the verb event subs to if the equippee is wearing the stethoscope.
+ ///
+ private void OnEquipped(EntityUid uid, StethoscopeComponent component, GotEquippedEvent args)
+ {
+ if (!TryComp(uid, out var clothing))
+ return;
+ // Is the clothing in its actual slot?
+ if (!clothing.SlotFlags.HasFlag(args.SlotFlags))
+ return;
+
+ component.IsActive = true;
+
+ var wearingComp = EnsureComp(args.Equipee);
+ wearingComp.Stethoscope = uid;
+ }
+
+ private void OnUnequipped(EntityUid uid, StethoscopeComponent component, GotUnequippedEvent args)
+ {
+ if (!component.IsActive)
+ return;
+
+ RemComp(args.Equipee);
+ 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(uid, args.Target, stetho); // start doafter
+ },
+ Text = Loc.GetString("stethoscope-verb"),
+ IconTexture = "Clothing/Neck/Misc/stethoscope.rsi/icon.png",
+ Priority = 2
+ };
+ args.Verbs.Add(verb);
+ }
+
+
+ private void OnStethoscopeAction(EntityUid uid, StethoscopeComponent component, StethoscopeActionEvent args)
+ {
+ StartListening(args.Performer, args.Target, component);
+ }
+
+ private void OnGetActions(EntityUid uid, StethoscopeComponent component, GetItemActionsEvent args)
+ {
+ args.Actions.Add(component.Action);
+ }
+
+ // doafter succeeded / failed
+ private void OnListenSuccess(ListenSuccessfulEvent ev)
+ {
+ ev.Component.CancelToken = null;
+ ExamineWithStethoscope(ev.User, ev.Target);
+ }
+
+ private void OnListenCancelled(ListenCancelledEvent ev)
+ {
+ if (ev.Component == null)
+ return;
+ ev.Component.CancelToken = null;
+ }
+ // construct the doafter and start it
+ private void StartListening(EntityUid user, EntityUid target, StethoscopeComponent comp)
+ {
+ comp.CancelToken = new CancellationTokenSource();
+ _doAfterSystem.DoAfter(new DoAfterEventArgs(user, comp.Delay, comp.CancelToken.Token, target: target)
+ {
+ BroadcastFinishedEvent = new ListenSuccessfulEvent(user, target, comp),
+ BroadcastCancelledEvent = new ListenCancelledEvent(user, comp),
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ BreakOnStun = true,
+ NeedHand = true
+ });
+ }
+
+ ///
+ /// 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) || mobState.IsDead())
+ {
+ _popupSystem.PopupEntity(Loc.GetString("stethoscope-dead"), target, Filter.Entities(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, Filter.Entities(user));
+ }
+
+ private string GetDamageMessage(FixedPoint2 totalOxyloss)
+ {
+ var msg = (int) totalOxyloss switch
+ {
+ < 20 => "stethoscope-normal",
+ < 60 => "stethoscope-hyper",
+ < 80 => "stethoscope-irregular",
+ _ => "stethoscope-fucked"
+ };
+ return msg;
+ }
+
+ // events for the doafter
+ private sealed class ListenSuccessfulEvent : EntityEventArgs
+ {
+ public EntityUid User;
+ public EntityUid Target;
+ public StethoscopeComponent Component;
+
+ public ListenSuccessfulEvent(EntityUid user, EntityUid target, StethoscopeComponent component)
+ {
+ User = user;
+ Target = target;
+ Component = component;
+ }
+ }
+
+ private sealed class ListenCancelledEvent : EntityEventArgs
+ {
+ public EntityUid Uid;
+ public StethoscopeComponent Component;
+
+ public ListenCancelledEvent(EntityUid uid, StethoscopeComponent component)
+ {
+ Uid = uid;
+ Component = component;
+ }
+ }
+
+ }
+
+ public sealed class StethoscopeActionEvent : EntityTargetActionEvent {}
+}
diff --git a/Content.Shared/Verbs/SharedVerbSystem.cs b/Content.Shared/Verbs/SharedVerbSystem.cs
index 0b8a93ec6d..217bc66c48 100644
--- a/Content.Shared/Verbs/SharedVerbSystem.cs
+++ b/Content.Shared/Verbs/SharedVerbSystem.cs
@@ -102,6 +102,13 @@ namespace Content.Shared.Verbs
verbs.UnionWith(verbEvent.Verbs);
}
+ if (types.Contains(typeof(InnateVerb)))
+ {
+ var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract, canAccess);
+ RaiseLocalEvent(user, verbEvent);
+ verbs.UnionWith(verbEvent.Verbs);
+ }
+
if (types.Contains(typeof(AlternativeVerb)))
{
var verbEvent = new GetVerbsEvent(user, target, @using, hands, canInteract, canAccess);
diff --git a/Content.Shared/Verbs/Verb.cs b/Content.Shared/Verbs/Verb.cs
index f23c68abff..587b1ea122 100644
--- a/Content.Shared/Verbs/Verb.cs
+++ b/Content.Shared/Verbs/Verb.cs
@@ -215,6 +215,7 @@ namespace Content.Shared.Verbs
{ typeof(Verb) },
{ typeof(InteractionVerb) },
{ typeof(UtilityVerb) },
+ { typeof(InnateVerb)},
{ typeof(AlternativeVerb) },
{ typeof(ActivationVerb) },
{ typeof(ExamineVerb) }
@@ -262,6 +263,24 @@ namespace Content.Shared.Verbs
}
}
+ ///
+ /// This is for verbs facilitated by components on the user.
+ /// 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.
+ ///
+ [Serializable, NetSerializable]
+ public sealed class InnateVerb : Verb
+ {
+ public override int TypePriority => 3;
+ public InnateVerb() : base()
+ {
+ TextStyleClass = InteractionVerb.DefaultTextStyleClass;
+ }
+ }
+
///
/// Verbs for alternative-interactions.
///
diff --git a/Resources/Locale/en-US/health-examinable/stethoscope.ftl b/Resources/Locale/en-US/health-examinable/stethoscope.ftl
new file mode 100644
index 0000000000..decfd7795b
--- /dev/null
+++ b/Resources/Locale/en-US/health-examinable/stethoscope.ftl
@@ -0,0 +1,6 @@
+stethoscope-verb = Listen with stethoscope
+stethoscope-dead = You hear nothing.
+stethoscope-normal = You hear normal 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.
diff --git a/Resources/Prototypes/Entities/Clothing/Neck/misc.yml b/Resources/Prototypes/Entities/Clothing/Neck/misc.yml
index 4f50f0d418..d0f98dd5ea 100644
--- a/Resources/Prototypes/Entities/Clothing/Neck/misc.yml
+++ b/Resources/Prototypes/Entities/Clothing/Neck/misc.yml
@@ -19,6 +19,7 @@
sprite: Clothing/Neck/Misc/stethoscope.rsi
- type: Clothing
sprite: Clothing/Neck/Misc/stethoscope.rsi
+ - type: Stethoscope
- type: entity
parent: ClothingNeckBase
@@ -40,4 +41,4 @@
- type: Sprite
sprite: Clothing/Neck/Misc/lawyerbadge.rsi
- type: Clothing
- sprite: Clothing/Neck/Misc/lawyerbadge.rsi
+ sprite: Clothing/Neck/Misc/lawyerbadge.rsi