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