Cleanup and small update to the stethoscope! (#36210)

* First commit

* Address most of the review!
This commit is contained in:
beck-thompson
2025-03-30 19:27:08 -07:00
committed by GitHub
parent 66e2b0ab64
commit fc0a6dfdb3
11 changed files with 225 additions and 220 deletions

View File

@@ -1,22 +0,0 @@
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;
}
}

View File

@@ -1,18 +0,0 @@
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!;
}
}

View File

@@ -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<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;
}
}
}

View File

@@ -67,6 +67,8 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowCriminalRecordIconsComponent>>(RefRelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowCriminalRecordIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<EquipmentVerb>>(OnGetEquipmentVerbs); SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<EquipmentVerb>>(OnGetEquipmentVerbs);
SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<InnateVerb>>(OnGetInnateVerbs);
} }
protected void RefRelayInventoryEvent<T>(EntityUid uid, InventoryComponent component, ref T args) where T : IInventoryRelayEvent protected void RefRelayInventoryEvent<T>(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<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> /// <summary>

View File

@@ -0,0 +1,31 @@
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;
}

View File

@@ -1,7 +0,0 @@
using Content.Shared.Actions;
namespace Content.Shared.Medical.Stethoscope;
public sealed partial class StethoscopeActionEvent : EntityTargetActionEvent
{
}

View File

@@ -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<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;

View File

@@ -4,6 +4,4 @@ using Robust.Shared.Serialization;
namespace Content.Shared.Medical; namespace Content.Shared.Medical;
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed partial class StethoscopeDoAfterEvent : SimpleDoAfterEvent public sealed partial class StethoscopeDoAfterEvent : SimpleDoAfterEvent;
{
}

View File

@@ -281,12 +281,11 @@ namespace Content.Shared.Verbs
} }
/// <summary> /// <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. /// Verbs from clothing, species, etc. rather than a held item.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Add a component to the user's entity and sub to the get verbs event /// This will get relayed to all clothing (Not pockets) through an inventory relay event.
/// and it'll appear in the verbs menu on any target.
/// </remarks> /// </remarks>
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class InnateVerb : Verb public sealed class InnateVerb : Verb

View File

@@ -1,6 +1,15 @@
stethoscope-verb = Listen with stethoscope stethoscope-verb = Listen with stethoscope
stethoscope-dead = You hear nothing.
stethoscope-nothing = You don't hear anything.
stethoscope-normal = You hear normal breathing. stethoscope-normal = You hear normal breathing.
stethoscope-raggedy = You hear raggedy breathing.
stethoscope-hyper = You hear hyperventilation. stethoscope-hyper = You hear hyperventilation.
stethoscope-irregular = You hear hyperventilation with an irregular pattern. stethoscope-irregular = You hear hyperventilation with an irregular pattern.
stethoscope-fucked = You hear twitchy, labored breathing interspersed with short gasps. 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}

View File

@@ -32,17 +32,36 @@
path: /Audio/Items/flashlight_off.ogg path: /Audio/Items/flashlight_off.ogg
- type: entity - type: entity
parent: ClothingNeckBase parent: Clothing
id: ClothingNeckStethoscope id: ClothingNeckStethoscope
name: stethoscope 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. 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: components:
- type: Item
size: Small
- type: Sprite - type: Sprite
sprite: Clothing/Neck/Misc/stethoscope.rsi sprite: Clothing/Neck/Misc/stethoscope.rsi
state: icon
- type: Clothing - type: Clothing
sprite: Clothing/Neck/Misc/stethoscope.rsi sprite: Clothing/Neck/Misc/stethoscope.rsi
quickEquip: true
slots:
- neck
- type: Stethoscope - 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 - type: entity
parent: ClothingNeckBase parent: ClothingNeckBase
id: ClothingNeckBling id: ClothingNeckBling
@@ -69,18 +88,6 @@
- type: TypingIndicatorClothing - type: TypingIndicatorClothing
proto: lawyer 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 - type: entity
parent: ClothingNeckBase parent: ClothingNeckBase
id: Dinkystar id: Dinkystar