+++ /dev/null
-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;
- }
-}
+++ /dev/null
-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!;
- }
-}
+++ /dev/null
-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;
- }
- }
-}
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
}
}
+ 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>
--- /dev/null
+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;
+}
+
+++ /dev/null
-using Content.Shared.Actions;
-
-namespace Content.Shared.Medical.Stethoscope;
-
-public sealed partial class StethoscopeActionEvent : EntityTargetActionEvent
-{
-}
--- /dev/null
+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;
namespace Content.Shared.Medical;
[Serializable, NetSerializable]
-public sealed partial class StethoscopeDoAfterEvent : SimpleDoAfterEvent
-{
-}
+public sealed partial class StethoscopeDoAfterEvent : SimpleDoAfterEvent;
}
/// <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
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}
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