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