Skip to content

Commit

Permalink
Add surgical cleanliness and sanitation
Browse files Browse the repository at this point in the history
  • Loading branch information
sowelipililimute committed Feb 2, 2025
1 parent 4a6ff66 commit 1798497
Show file tree
Hide file tree
Showing 19 changed files with 416 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Content.Client/_DV/Surgery/SurgeryCleanSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Content.Shared._DV.Surgery;

namespace Content.Client._DV.Surgery;

/// <summary>This gets the examine tooltip and sanitize verb predicted on the client so there's no pop-in after latency</summary>
public sealed class SurgeryCleanSystem : SharedSurgeryCleanSystem
{
public override bool TryStartCleaning(Entity<SurgeryCleansDirtComponent> ent, EntityUid user, EntityUid target)
{
return true;
}
}
86 changes: 86 additions & 0 deletions Content.Server/_DV/Surgery/SurgeryCleanSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using Content.Server.DoAfter;
using Content.Server.Popups;
using Content.Shared._DV.Surgery;
using Content.Shared.DoAfter;
using Content.Shared.Forensics;
using Content.Shared.Interaction;
using Content.Shared.Popups;

namespace Content.Server._DV.Surgery;

/// <summary>Responsible for handling the visual appearance of and sanitzation of items that can get dirty from surgery</summary>
public sealed class SurgeryCleanSystem : SharedSurgeryCleanSystem
{
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<SurgeryCleansDirtComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<SurgeryCleansDirtComponent, SurgeryCleanDirtDoAfterEvent>(FinishCleaning);

SubscribeLocalEvent<SurgeryDirtinessComponent, SurgeryCleanedEvent>(OnCleanDirt);
SubscribeLocalEvent<SurgeryCrossContaminationComponent, SurgeryCleanedEvent>(OnCleanDNA);
}

private void OnAfterInteract(Entity<SurgeryCleansDirtComponent> ent, ref AfterInteractEvent args)
{
if (args.Handled || !args.CanReach || args.Target == null)
return;

args.Handled = TryStartCleaning(ent, args.User, args.Target.Value);
}

public override bool TryStartCleaning(Entity<SurgeryCleansDirtComponent> ent, EntityUid user, EntityUid target)
{
var isDirty = (TryComp<SurgeryDirtinessComponent>(target, out var dirtiness) && dirtiness.Dirtiness > 0);
var isContaminated = (TryComp<SurgeryCrossContaminationComponent>(target, out var contamination) && contamination.DNAs.Count > 0);

if (!(isDirty || isContaminated))
{
_popup.PopupEntity(Loc.GetString("sanitization-cannot-clean", ("target", target)), user, user, PopupType.MediumCaution);
return false;
}

var cleanDelay = ent.Comp.CleanDelay;
var doAfterArgs = new DoAfterArgs(EntityManager, user, cleanDelay, new SurgeryCleanDirtDoAfterEvent(), ent, target: target, used: ent)
{
NeedHand = true,
BreakOnDamage = true,
BreakOnMove = true,
MovementThreshold = 0.01f,
DistanceThreshold = 1f,
};

_doAfter.TryStartDoAfter(doAfterArgs);
_popup.PopupEntity(Loc.GetString("sanitization-cleaning", ("target", target)), user, user);

return true;
}

private void FinishCleaning(Entity<SurgeryCleansDirtComponent> ent, ref SurgeryCleanDirtDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Target == null)
return;

var ev = new SurgeryCleanedEvent();
RaiseLocalEvent(args.Args.Target.Value, ref ev);

// daisychain to forensics because if you sterilise something youve almost definitely scrubbed all dna and fibers off of it
var daisyChainEvent = new CleanForensicsDoAfterEvent() { DoAfter = args.DoAfter };
RaiseLocalEvent(ent.Owner, daisyChainEvent);
}

private void OnCleanDirt(Entity<SurgeryDirtinessComponent> ent, ref SurgeryCleanedEvent args)
{
ent.Comp.Dirtiness = 0;
Dirty(ent);
}

private void OnCleanDNA(Entity<SurgeryCrossContaminationComponent> ent, ref SurgeryCleanedEvent args)
{
ent.Comp.DNAs.Clear();
}
}
10 changes: 10 additions & 0 deletions Content.Server/_DV/Surgery/SurgeryCrossContaminationComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Content.Server._DV.Surgery;

/// <summary>Component that allows an entity to be cross contamined from being used in surgery</summary>
[RegisterComponent]
public sealed partial class SurgeryCrossContaminationComponent : Component
{
/// <summary>Patient DNAs that are present on this dirtied tool</summary>
[DataField]
public HashSet<string> DNAs = new();
}
113 changes: 113 additions & 0 deletions Content.Server/_Shitmed/Medical/Surgery/SurgerySystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared._DV.Surgery; // DeltaV: expanded anesthesia
using Content.Server.Forensics; // DeltaV: surgery cross contamination
using Content.Server._DV.Surgery; // DeltaV: surgery cross contamination
using Content.Shared.FixedPoint; // DeltaV: surgery cross contamination
using Content.Shared.Damage.Prototypes; // DeltaV: surgery cross contamination
using Content.Shared._Shitmed.Medical.Surgery;
using Content.Shared._Shitmed.Medical.Surgery.Conditions;
using Content.Shared._Shitmed.Medical.Surgery.Effects.Step;
Expand Down Expand Up @@ -40,13 +44,15 @@ public sealed class SurgerySystem : SharedSurgerySystem
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly RottingSystem _rot = default!;
[Dependency] private readonly BlindableSystem _blindableSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!; // DeltaV: surgery cross contamination

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<SurgeryToolComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
SubscribeLocalEvent<SurgeryTargetComponent, SurgeryStepDamageEvent>(OnSurgeryStepDamage);
SubscribeLocalEvent<SurgeryTargetComponent, SurgeryDirtinessEvent>(OnSurgerySanitation); // DeltaV: surgery cross contamination
// You might be wondering "why aren't we using StepEvent for these two?" reason being that StepEvent fires off regardless of success on the previous functions
// so this would heal entities even if you had a used or incorrect organ.
SubscribeLocalEvent<SurgerySpecialDamageChangeEffectComponent, SurgeryStepDamageChangeEvent>(OnSurgerySpecialDamageChange);
Expand Down Expand Up @@ -141,6 +147,113 @@ private void OnUtilityVerb(Entity<SurgeryToolComponent> ent, ref GetVerbsEvent<U
private void OnSurgeryStepDamage(Entity<SurgeryTargetComponent> ent, ref SurgeryStepDamageEvent args) =>
SetDamage(args.Body, args.Damage, args.PartMultiplier, args.User, args.Part);

// Begin DeltaV: surgery cross contamination
private FixedPoint2 Dirtiness(EntityUid entity)
{
var comp = EnsureComp<SurgeryDirtinessComponent>(entity);
return comp.Dirtiness;
}

private HashSet<string> CrossContaminants(EntityUid entity)
{
var comp = EnsureComp<SurgeryCrossContaminationComponent>(entity);
return comp.DNAs;
}

public FixedPoint2 TotalDirtiness(EntityUid user, List<EntityUid> tools, Entity<DnaComponent, SurgeryContaminableComponent> target)
{
var total = FixedPoint2.Zero;
var dnas = new HashSet<string>();

if (HasComp<SurgerySelfDirtyComponent>(user))
{
total += Dirtiness(user);
dnas.UnionWith(CrossContaminants(user));
}
else
{
if (_inventory.TryGetSlotEntity(user, "gloves", out var glovesEntity))
{
total += Dirtiness(glovesEntity.Value);
dnas.UnionWith(CrossContaminants(glovesEntity.Value));
}

foreach (var tool in tools)
{
total += Dirtiness(tool);
dnas.UnionWith(CrossContaminants(tool));
}
}

dnas.Remove(target.Comp1.DNA);

return total + dnas.Count * target.Comp2.CrossContaminationDirtinessLevel;
}

public FixedPoint2 DamageToBeDealt(Entity<SurgeryContaminableComponent> ent, FixedPoint2 dirtiness)
{
if (ent.Comp.DirtinessThreshold > dirtiness)
{
return 0;
}

var exceedsAmount = (dirtiness - ent.Comp.DirtinessThreshold).Float();
var additionalDamage = (1f / ent.Comp.InverseDamageCoefficient.Float()) * (exceedsAmount * exceedsAmount);

return FixedPoint2.New(additionalDamage) + ent.Comp.BaseDamage;
}

private void OnSurgerySanitation(Entity<SurgeryTargetComponent> target, ref Content.Shared._DV.Surgery.SurgeryDirtinessEvent args)
{
if (!TryComp<SurgeryContaminableComponent>(target.Owner, out var contaminableComp))
return;

if (!TryComp<DnaComponent>(target.Owner, out var dnaComp))
return;

var dirtiness = TotalDirtiness(args.User, args.Tools, new(target.Owner, dnaComp, contaminableComp));
var damage = DamageToBeDealt(new(target.Owner, contaminableComp), dirtiness);

if (damage > 0)
{
var sepsis = new DamageSpecifier(_prototypes.Index<DamageTypePrototype>("Poison"), damage);
SetDamage(target.Owner, sepsis, 0.5f, args.User, args.Part);
}

if (!TryComp<SurgeryStepDirtinessComponent>(args.Step, out var surgicalStepDirtiness))
return;

if (HasComp<SurgerySelfDirtyComponent>(args.User))
{
var userDirtiness = EnsureComp<SurgeryDirtinessComponent>(args.User);
userDirtiness.Dirtiness += surgicalStepDirtiness.ToolDirtiness;

var userContamination = EnsureComp<SurgeryCrossContaminationComponent>(args.User);
userContamination.DNAs.Add(dnaComp.DNA);

return;
}

if (_inventory.TryGetSlotEntity(args.User, "gloves", out var glovesEntity))
{
var glovesDirtiness = EnsureComp<SurgeryDirtinessComponent>(glovesEntity.Value);
glovesDirtiness.Dirtiness += surgicalStepDirtiness.GloveDirtiness;

var glovesContamination = EnsureComp<SurgeryCrossContaminationComponent>(glovesEntity.Value);
glovesContamination.DNAs.Add(dnaComp.DNA);
}
foreach (var tool in args.Tools)
{
var toolDirtiness = EnsureComp<SurgeryDirtinessComponent>(tool);
toolDirtiness.Dirtiness += surgicalStepDirtiness.ToolDirtiness;

var toolContamination = EnsureComp<SurgeryCrossContaminationComponent>(tool);
toolContamination.DNAs.Add(dnaComp.DNA);
}
}

// End DeltaV: surgery cross contamination

private void OnSurgeryDamageChange(Entity<SurgeryDamageChangeEffectComponent> ent, ref SurgeryStepEvent args) // DeltaV
{
var damageChange = ent.Comp.Damage;
Expand Down
53 changes: 53 additions & 0 deletions Content.Shared/_DV/Surgery/SharedSurgeryCleanSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Content.Shared.Examine;
using Content.Shared.Verbs;
using Robust.Shared.Utility;

namespace Content.Shared._DV.Surgery;

public abstract class SharedSurgeryCleanSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<SurgeryDirtinessComponent, ExaminedEvent>(OnDirtyExamined);
SubscribeLocalEvent<SurgeryCleansDirtComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
}

private void OnUtilityVerb(Entity<SurgeryCleansDirtComponent> ent, ref GetVerbsEvent<UtilityVerb> args)
{
if (!args.CanInteract || !args.CanAccess)
return;

var user = args.User;
var target = args.Target;

var verb = new UtilityVerb()
{
Act = () => TryStartCleaning(ent, user, target),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/bubbles.svg.192dpi.png")),
Text = Loc.GetString(Loc.GetString("sanitization-verb-text")),
Message = Loc.GetString(Loc.GetString("sanitization-verb-message")),
// we daisychain to forensics here so we shouldn't leave forensic traces of our own
DoContactInteraction = false
};

args.Verbs.Add(verb);
}

private void OnDirtyExamined(Entity<SurgeryDirtinessComponent> ent, ref ExaminedEvent args)
{
var description = ent.Comp.Dirtiness.Int() switch {
<= 0 => "surgery-cleanliness-0",
<= 20 => "surgery-cleanliness-1",
<= 40 => "surgery-cleanliness-2",
<= 60 => "surgery-cleanliness-3",
<= 80 => "surgery-cleanliness-4",
_ => "surgery-cleanliness-5",
};

args.PushMarkup(Loc.GetString(description));
}

public abstract bool TryStartCleaning(Entity<SurgeryCleansDirtComponent> ent, EntityUid user, EntityUid target);
}
7 changes: 7 additions & 0 deletions Content.Shared/_DV/Surgery/SurgeryCleanDirtDoAfterEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;

namespace Content.Shared._DV.Surgery;

[Serializable, NetSerializable]
public sealed partial class SurgeryCleanDirtDoAfterEvent : SimpleDoAfterEvent;
5 changes: 5 additions & 0 deletions Content.Shared/_DV/Surgery/SurgeryCleanedEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace Content.Shared._DV.Surgery;

/// <summary>Event fired when an object is sterilised for surgery</summary>
[ByRefEvent]
public record struct SurgeryCleanedEvent;
16 changes: 16 additions & 0 deletions Content.Shared/_DV/Surgery/SurgeryCleansDirt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Robust.Shared.GameStates;

namespace Content.Shared._DV.Surgery;

/// <summary>
/// For items that can clean up surgical dirtiness
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class SurgeryCleansDirtComponent : Component
{
/// <summary>
/// How long it takes to wipe prints/blood/etc. off of things using this entity
/// </summary>
[DataField]
public float CleanDelay = 24.0f;
}
30 changes: 30 additions & 0 deletions Content.Shared/_DV/Surgery/SurgeryContaminableComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Content.Shared._Shitmed.Medical.Surgery;
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;

namespace Content.Shared._DV.Surgery;

/// <summary>Component that indicates how an entity should respond to unsanitary surgery conditions</summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
public sealed partial class SurgeryContaminableComponent : Component
{
/// <summary>How much cross contamination should increase dirtiness per incompatible DNA</summary>
[DataField]
public FixedPoint2 CrossContaminationDirtinessLevel = 40.0;

/// <summary>The level of dirtiness above which toxin damage will be dealt</summary>
[DataField]
public FixedPoint2 DirtinessThreshold = 50.0;

/// <summary>The base amount of toxin damage to deal above the threshold</summary>
[DataField]
public FixedPoint2 BaseDamage = 2.0;

/// <summary>The inverse of the coefficient to scale the toxin damage by</summary>
[DataField]
public FixedPoint2 InverseDamageCoefficient = 250.0;

/// <summary>The upper limit on how much toxin damage can be dealt in a single step</summary>
[DataField]
public FixedPoint2 ToxinStepLimit = 15.0;
}
15 changes: 15 additions & 0 deletions Content.Shared/_DV/Surgery/SurgeryDirtinessComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Content.Shared._Shitmed.Medical.Surgery;
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;

namespace Content.Shared._DV.Surgery;

/// <summary>Component that allows an entity to take on dirtiness from being used in surgery</summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedSurgeryCleanSystem), typeof(SharedSurgerySystem))]
public sealed partial class SurgeryDirtinessComponent : Component
{
/// <summary>The level of dirtiness this component represents; above 50 is usually where consequences start to happen</summary>
[DataField]
[AutoNetworkedField]
public FixedPoint2 Dirtiness = 0.0;
}
7 changes: 7 additions & 0 deletions Content.Shared/_DV/Surgery/SurgeryDirtinessEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Robust.Shared.Prototypes;

namespace Content.Shared._DV.Surgery;

/// <summary>Handled by the server when a surgery step is completed in order to deal with sanitization concerns</summary>
[ByRefEvent]
public record struct SurgeryDirtinessEvent(EntityUid User, EntityUid Part, List<EntityUid> Tools, EntityUid Step);
Loading

0 comments on commit 1798497

Please sign in to comment.