diff --git a/Content.Client/Disease/DiseaseMachineSystem.cs b/Content.Client/Disease/DiseaseMachineSystem.cs
new file mode 100644
index 0000000000..b910a07874
--- /dev/null
+++ b/Content.Client/Disease/DiseaseMachineSystem.cs
@@ -0,0 +1,29 @@
+using Robust.Client.GameObjects;
+using Content.Shared.Disease;
+
+namespace Content.Client.Disease
+{
+ ///
+ /// Controls client-side visuals for the
+ /// disease machines.
+ ///
+ public sealed class DiseaseMachineSystem : VisualizerSystem
+ {
+ protected override void OnAppearanceChange(EntityUid uid, DiseaseMachineVisualsComponent component, ref AppearanceChangeEvent args)
+ {
+ if (TryComp(uid, out SpriteComponent? sprite)
+ && args.Component.TryGetData(DiseaseMachineVisuals.IsOn, out bool isOn)
+ && args.Component.TryGetData(DiseaseMachineVisuals.IsRunning, out bool isRunning))
+ {
+ var state = isRunning ? component.RunningState : component.IdleState;
+ sprite.LayerSetVisible(DiseaseMachineVisualLayers.IsOn, isOn);
+ sprite.LayerSetState(DiseaseMachineVisualLayers.IsRunning, state);
+ }
+ }
+ }
+}
+public enum DiseaseMachineVisualLayers : byte
+{
+ IsOn,
+ IsRunning
+}
diff --git a/Content.Client/Disease/DiseaseMachineVisualsComponent.cs b/Content.Client/Disease/DiseaseMachineVisualsComponent.cs
new file mode 100644
index 0000000000..2194cd1c78
--- /dev/null
+++ b/Content.Client/Disease/DiseaseMachineVisualsComponent.cs
@@ -0,0 +1,15 @@
+namespace Content.Client.Disease;
+
+///
+/// Holds the idle and running state for machines to control
+/// playing animtions on the client.
+///
+[RegisterComponent]
+public sealed class DiseaseMachineVisualsComponent : Component
+{
+ [DataField("idleState", required: true)]
+ public string IdleState = default!;
+
+ [DataField("runningState", required: true)]
+ public string RunningState = default!;
+}
diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index 6dddd30c5e..13abf71dce 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -37,6 +37,10 @@ namespace Content.Client.Entry
"Healing",
"Material",
"RandomAppearance",
+ "DiseaseProtection",
+ "DiseaseDiagnoser",
+ "DiseaseVaccine",
+ "DiseaseVaccineCreator",
"Mineable",
"RangedMagazine",
"Ammo",
@@ -47,12 +51,15 @@ namespace Content.Client.Entry
"ResearchClient",
"IdCardConsole",
"ThermalRegulator",
+ "DiseaseMachineRunning",
+ "DiseaseMachine",
"AtmosFixMarker",
"CablePlacer",
"Drink",
"Food",
"DeployableBarrier",
"MagicMirror",
+ "DiseaseSwab",
"FloorTile",
"RandomInsulation",
"Electrified",
@@ -62,6 +69,7 @@ namespace Content.Client.Entry
"Bloodstream",
"TransformableContainer",
"Mind",
+ "DiseaseCarrier",
"StorageFill",
"Mop",
"Bucket",
@@ -122,6 +130,7 @@ namespace Content.Client.Entry
"RCD",
"RCDAmmo",
"CursedEntityStorage",
+ "DiseaseArtifact",
"Radio",
"GasArtifact",
"SentienceTarget",
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
index 7d7bbc1232..25165ae0de 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
@@ -3,6 +3,7 @@ using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Disease.Components;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
using Content.Shared.Damage;
@@ -35,7 +36,17 @@ namespace Content.Client.HealthAnalyzer.UI
text.Append($"{Loc.GetString("health-analyzer-window-entity-health-text", ("entityName", entityName))}\n");
- text.Append($"{Loc.GetString("health-analyzer-window-entity-damage-total-text", ("amount", damageable.TotalDamage))}\n");
+ /// Status Effects / Components
+ if (entities.HasComponent(msg.TargetEntity))
+ {
+ text.Append($"{Loc.GetString("disease-scanner-diseased")}\n");
+ }else
+ {
+ text.Append($"{Loc.GetString("disease-scanner-not-diseased")}\n");
+ }
+
+ /// Damage
+ text.Append($"\n{Loc.GetString("health-analyzer-window-entity-damage-total-text", ("amount", damageable.TotalDamage))}\n");
HashSet shownTypes = new();
diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs
index b715facfd7..1062314871 100644
--- a/Content.Server/Chat/Managers/ChatManager.cs
+++ b/Content.Server/Chat/Managers/ChatManager.cs
@@ -1,5 +1,3 @@
-using System;
-using System.Collections.Generic;
using System.Linq;
using System.Text;
using Content.Server.Administration.Logs;
@@ -10,6 +8,9 @@ using Content.Server.MoMMI;
using Content.Server.Players;
using Content.Server.Preferences.Managers;
using Content.Server.Radio.EntitySystems;
+using Content.Server.Disease;
+using Content.Server.Disease.Components;
+using Content.Shared.Disease.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration;
using Content.Shared.CCVar;
@@ -22,10 +23,6 @@ using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Localization;
-using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
@@ -208,6 +205,11 @@ namespace Content.Server.Chat.Managers
return;
}
+ if (_entManager.HasComponent(source) && _entManager.TryGetComponent(source,out var carrier))
+ {
+ EntitySystem.Get().SneezeCough(source, _random.Pick(carrier.Diseases), string.Empty);
+ }
+
if (MessageCharacterLimit(source, message))
{
return;
diff --git a/Content.Server/Chemistry/ReagentEffects/ChemCauseDisease.cs b/Content.Server/Chemistry/ReagentEffects/ChemCauseDisease.cs
new file mode 100644
index 0000000000..33a80795a4
--- /dev/null
+++ b/Content.Server/Chemistry/ReagentEffects/ChemCauseDisease.cs
@@ -0,0 +1,32 @@
+using Content.Shared.Chemistry.Reagent;
+using Content.Server.Disease;
+using Content.Shared.Disease;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using JetBrains.Annotations;
+
+namespace Content.Server.Chemistry.ReagentEffects
+{
+ ///
+ /// Default metabolism for medicine reagents.
+ ///
+ [UsedImplicitly]
+ public sealed class ChemCauseDisease : ReagentEffect
+ {
+ ///
+ /// Chance it has each tick to cause disease, between 0 and 1
+ ///
+ [DataField("causeChance")]
+ public float CauseChance = 0.15f;
+
+ ///
+ /// The disease to add.
+ ///
+ [DataField("disease", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string Disease = string.Empty;
+ public override void Effect(ReagentEffectArgs args)
+ {
+ EntitySystem.Get().TryAddDisease(null, null, Disease, args.SolutionEntity);
+ }
+ }
+}
diff --git a/Content.Server/Chemistry/ReagentEffects/ChemCureDisease.cs b/Content.Server/Chemistry/ReagentEffects/ChemCureDisease.cs
new file mode 100644
index 0000000000..3f34d9c254
--- /dev/null
+++ b/Content.Server/Chemistry/ReagentEffects/ChemCureDisease.cs
@@ -0,0 +1,25 @@
+using Content.Shared.Chemistry.Reagent;
+using Content.Server.Disease;
+using JetBrains.Annotations;
+
+namespace Content.Server.Chemistry.ReagentEffects
+{
+ ///
+ /// Default metabolism for medicine reagents.
+ ///
+ [UsedImplicitly]
+ public sealed class ChemCureDisease : ReagentEffect
+ {
+ ///
+ /// Chance it has each tick to cure a disease, between 0 and 1
+ ///
+ [DataField("cureChance")]
+ public float CureChance = 0.15f;
+
+ public override void Effect(ReagentEffectArgs args)
+ {
+ var ev = new CureDiseaseAttemptEvent(CureChance);
+ args.EntityManager.EventBus.RaiseLocalEvent(args.SolutionEntity, ev, false);
+ }
+ }
+}
diff --git a/Content.Server/Chemistry/ReagentEffects/HealthChange.cs b/Content.Server/Chemistry/ReagentEffects/HealthChange.cs
index 593bd1e1b0..5421aff454 100644
--- a/Content.Server/Chemistry/ReagentEffects/HealthChange.cs
+++ b/Content.Server/Chemistry/ReagentEffects/HealthChange.cs
@@ -1,7 +1,5 @@
using System.Text.Json.Serialization;
using Content.Shared.Chemistry.Reagent;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Serialization.Manager.Attributes;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
diff --git a/Content.Server/Disease/Components/DiseaseCarrierComponent.cs b/Content.Server/Disease/Components/DiseaseCarrierComponent.cs
new file mode 100644
index 0000000000..e9280235f3
--- /dev/null
+++ b/Content.Server/Disease/Components/DiseaseCarrierComponent.cs
@@ -0,0 +1,36 @@
+using System.Linq;
+using Content.Shared.Disease;
+
+namespace Content.Server.Disease.Components
+{
+ [RegisterComponent]
+ ///
+ /// Allows the enity to be infected with diseases.
+ /// Please use only on mobs.
+ ///
+ public sealed class DiseaseCarrierComponent : Component
+ {
+ ///
+ /// Shows the CURRENT diseases on the carrier
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public List Diseases = new();
+ ///
+ /// The carrier's resistance to disease
+ ///
+ [DataField("diseaseResist")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float DiseaseResist = 0f;
+ ///
+ /// Diseases the carrier has had, used for immunity.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public List PastDiseases = new();
+ ///
+ /// All the diseases the carrier has or has had.
+ /// Checked against when trying to add a disease
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public List AllDiseases => PastDiseases.Concat(Diseases).ToList();
+ }
+}
diff --git a/Content.Server/Disease/Components/DiseaseDiagnoserComponent.cs b/Content.Server/Disease/Components/DiseaseDiagnoserComponent.cs
new file mode 100644
index 0000000000..9b2acc8410
--- /dev/null
+++ b/Content.Server/Disease/Components/DiseaseDiagnoserComponent.cs
@@ -0,0 +1,9 @@
+namespace Content.Server.Disease.Components
+{
+ ///
+ /// To give the disease diagnosing machine specific behavior
+ ///
+ [RegisterComponent]
+ public sealed class DiseaseDiagnoserComponent : Component
+ {}
+}
diff --git a/Content.Server/Disease/Components/DiseaseMachineComponent.cs b/Content.Server/Disease/Components/DiseaseMachineComponent.cs
new file mode 100644
index 0000000000..e0535635a9
--- /dev/null
+++ b/Content.Server/Disease/Components/DiseaseMachineComponent.cs
@@ -0,0 +1,31 @@
+using Content.Shared.Disease;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Disease.Components
+{
+ [RegisterComponent]
+ ///
+ /// For shared behavior between both disease machines
+ ///
+ public sealed class DiseaseMachineComponent : Component
+ {
+ [DataField("delay")]
+ public float Delay = 5f;
+ ///
+ /// How much time we've accumulated processing
+ ///
+ [ViewVariables]
+ public float Accumulator = 0f;
+ ///
+ /// The disease prototype currently being diagnosed
+ ///
+ [ViewVariables]
+ public DiseasePrototype? Disease;
+ ///
+ /// What the machine will spawn
+ ///
+ [DataField("machineOutput", customTypeSerializer: typeof(PrototypeIdSerializer), required: true)]
+ public string MachineOutput = string.Empty;
+ }
+}
diff --git a/Content.Server/Disease/Components/DiseaseMachineRunningComponent.cs b/Content.Server/Disease/Components/DiseaseMachineRunningComponent.cs
new file mode 100644
index 0000000000..0315bfbfa5
--- /dev/null
+++ b/Content.Server/Disease/Components/DiseaseMachineRunningComponent.cs
@@ -0,0 +1,9 @@
+namespace Content.Server.Disease.Components
+{
+ ///
+ /// For EntityQuery to keep track of which machines are running
+ ///
+ [RegisterComponent]
+ public sealed class DiseaseMachineRunningComponent : Component
+ {}
+}
diff --git a/Content.Server/Disease/Components/DiseaseProtectionComponent.cs b/Content.Server/Disease/Components/DiseaseProtectionComponent.cs
new file mode 100644
index 0000000000..e99e4dacc5
--- /dev/null
+++ b/Content.Server/Disease/Components/DiseaseProtectionComponent.cs
@@ -0,0 +1,24 @@
+namespace Content.Server.Disease.Components
+{
+ ///
+ /// Value added to clothing to give its wearer
+ /// protection against infection from diseases
+ ///
+ [RegisterComponent]
+ public sealed class DiseaseProtectionComponent : Component
+ {
+ ///
+ /// Float value between 0 and 1, will be subtracted
+ /// from the infection chance (which is base 0.7)
+ /// Reference guide is a full biosuit w/gloves & mask
+ /// should add up to exactly 0.7
+ ///
+ [DataField("protection")]
+ public float Protection = 0.1f;
+ ///
+ /// Is the component currently being worn and affecting someone's disease
+ /// resistance? Making the unequip check not totally CBT
+ ///
+ public bool IsActive = false;
+ }
+}
diff --git a/Content.Server/Disease/Components/DiseaseSwabComponent.cs b/Content.Server/Disease/Components/DiseaseSwabComponent.cs
new file mode 100644
index 0000000000..8fb7c1e6d6
--- /dev/null
+++ b/Content.Server/Disease/Components/DiseaseSwabComponent.cs
@@ -0,0 +1,33 @@
+using System.Threading;
+using Content.Shared.Disease;
+
+namespace Content.Server.Disease.Components
+{
+ [RegisterComponent]
+ ///
+ /// For mouth swabs used to collect and process
+ /// disease samples.
+ ///
+ public sealed class DiseaseSwabComponent : Component
+ {
+ ///
+ /// How long it takes to swab someone.
+ ///
+ [DataField("swabDelay")]
+ [ViewVariables]
+ public float SwabDelay = 2f;
+ ///
+ /// If this swab has been used
+ ///
+ public bool Used = false;
+ ///
+ /// Token for interrupting swabbing do after.
+ ///
+ public CancellationTokenSource? CancelToken;
+ ///
+ /// The disease prototype currently on the swab
+ ///
+ [ViewVariables]
+ public DiseasePrototype? Disease;
+ }
+}
diff --git a/Content.Server/Disease/Components/DiseaseVaccineComponent.cs b/Content.Server/Disease/Components/DiseaseVaccineComponent.cs
new file mode 100644
index 0000000000..5e3e4f7f93
--- /dev/null
+++ b/Content.Server/Disease/Components/DiseaseVaccineComponent.cs
@@ -0,0 +1,33 @@
+using System.Threading;
+using Content.Shared.Disease;
+
+namespace Content.Server.Disease.Components
+{
+ [RegisterComponent]
+ ///
+ /// For disease vaccines
+ ///
+ public sealed class DiseaseVaccineComponent : Component
+ {
+ ///
+ /// How long it takes to inject someone
+ ///
+ [DataField("injectDelay")]
+ [ViewVariables]
+ public float InjectDelay = 2f;
+ ///
+ /// If this vaccine has been used
+ ///
+ public bool Used = false;
+ ///
+ /// Token for interrupting injection do after.
+ ///
+ public CancellationTokenSource? CancelToken;
+
+ ///
+ /// The disease prototype currently on the vaccine
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public DiseasePrototype? Disease;
+ }
+}
diff --git a/Content.Server/Disease/Components/DiseaseVaccineCreatorComponent.cs b/Content.Server/Disease/Components/DiseaseVaccineCreatorComponent.cs
new file mode 100644
index 0000000000..f8353cb965
--- /dev/null
+++ b/Content.Server/Disease/Components/DiseaseVaccineCreatorComponent.cs
@@ -0,0 +1,10 @@
+namespace Content.Server.Disease.Components
+{
+ ///
+ /// Controls disease machine behavior specific to the
+ /// vaccine creating machine
+ ///
+ [RegisterComponent]
+ public sealed class DiseaseVaccineCreatorComponent : Component
+ {}
+}
diff --git a/Content.Server/Disease/Cures/DiseaseBedrestCure.cs b/Content.Server/Disease/Cures/DiseaseBedrestCure.cs
new file mode 100644
index 0000000000..3f90385204
--- /dev/null
+++ b/Content.Server/Disease/Cures/DiseaseBedrestCure.cs
@@ -0,0 +1,33 @@
+using Content.Shared.Disease;
+using Content.Server.Buckle.Components;
+
+namespace Content.Server.Disease.Cures
+{
+ ///
+ /// Cures the disease after a certain amount of time
+ /// strapped.
+ ///
+ /// TODO: Revisit after bed pr merged
+ public sealed class DiseaseBedrestCure : DiseaseCure
+ {
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int Ticker = 0;
+ [DataField("maxLength", required: true)]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int MaxLength = 60;
+
+ public override bool Cure(DiseaseEffectArgs args)
+ {
+ if (!args.EntityManager.TryGetComponent(args.DiseasedEntity, out var buckle))
+ return false;
+ if (buckle.Buckled)
+ Ticker++;
+ return Ticker >= MaxLength;
+ }
+
+ public override string CureText()
+ {
+ return (Loc.GetString("diagnoser-cure-bedrest", ("time", MaxLength)));
+ }
+ }
+}
diff --git a/Content.Server/Disease/Cures/DiseaseBodyTemperatureCure.cs b/Content.Server/Disease/Cures/DiseaseBodyTemperatureCure.cs
new file mode 100644
index 0000000000..6afc92a2e4
--- /dev/null
+++ b/Content.Server/Disease/Cures/DiseaseBodyTemperatureCure.cs
@@ -0,0 +1,34 @@
+using Content.Server.Temperature.Components;
+using Content.Shared.Disease;
+
+namespace Content.Server.Disease.Cures
+{
+ ///
+ /// Cures the disease if temperature is within certain bounds.
+ ///
+ public sealed class DiseaseBodyTemperatureCure : DiseaseCure
+ {
+ [DataField("min")]
+ public float Min = 0;
+
+ [DataField("max")]
+ public float Max = float.MaxValue;
+ public override bool Cure(DiseaseEffectArgs args)
+ {
+ if (!args.EntityManager.TryGetComponent(args.DiseasedEntity, out TemperatureComponent temp))
+ return false;
+
+ return temp.CurrentTemperature > Min && temp.CurrentTemperature < float.MaxValue;
+ }
+
+ public override string CureText()
+ {
+ if (Min == 0)
+ return Loc.GetString("diagnoser-cure-temp-max", ("max", Math.Round(Max)));
+ if (Max == float.MaxValue)
+ return Loc.GetString("diagnoser-cure-temp-min", ("min", Math.Round(Min)));
+
+ return Loc.GetString("diagnoser-cure-temp-both", ("max", Math.Round(Max)), ("min", Math.Round(Min)));
+ }
+ }
+}
diff --git a/Content.Server/Disease/Cures/DiseaseJustWaitCure.cs b/Content.Server/Disease/Cures/DiseaseJustWaitCure.cs
new file mode 100644
index 0000000000..dc44de16ad
--- /dev/null
+++ b/Content.Server/Disease/Cures/DiseaseJustWaitCure.cs
@@ -0,0 +1,31 @@
+using Content.Shared.Disease;
+
+namespace Content.Server.Disease.Cures
+{
+ ///
+ /// Automatically removes the disease after a
+ /// certain amount of time.
+ ///
+ public sealed class DiseaseJustWaitCure : DiseaseCure
+ {
+ ///
+ /// All of these are in seconds
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int Ticker = 0;
+ [DataField("maxLength", required: true)]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int MaxLength = 150;
+
+ public override bool Cure(DiseaseEffectArgs args)
+ {
+ Ticker++;
+ return Ticker >= MaxLength;
+ }
+
+ public override string CureText()
+ {
+ return Loc.GetString("diagnoser-cure-wait", ("time", MaxLength));
+ }
+ }
+}
diff --git a/Content.Server/Disease/Cures/DiseaseReagentCure.cs b/Content.Server/Disease/Cures/DiseaseReagentCure.cs
new file mode 100644
index 0000000000..0568252768
--- /dev/null
+++ b/Content.Server/Disease/Cures/DiseaseReagentCure.cs
@@ -0,0 +1,38 @@
+using Content.Shared.Disease;
+using Content.Shared.FixedPoint;
+using Content.Server.Body.Components;
+
+namespace Content.Server.Disease.Cures
+{
+ ///
+ /// Cures the disease if a certain amount of reagent
+ /// is in the host's chemstream.
+ ///
+ public sealed class DiseaseReagentCure : DiseaseCure
+ {
+ [DataField("min")]
+ public FixedPoint2 Min = 5;
+ [DataField("reagent")]
+ public string? Reagent;
+
+ public override bool Cure(DiseaseEffectArgs args)
+ {
+ if (!args.EntityManager.TryGetComponent(args.DiseasedEntity, out var bloodstream))
+ return false;
+
+ var quant = FixedPoint2.Zero;
+ if (Reagent != null && bloodstream.ChemicalSolution.ContainsReagent(Reagent))
+ {
+ quant = bloodstream.ChemicalSolution.GetReagentQuantity(Reagent);
+ }
+ return quant >= Min;
+ }
+
+ public override string CureText()
+ {
+ if (Reagent == null)
+ return string.Empty;
+ return (Loc.GetString("diagnoser-cure-reagent", ("units", Min), ("reagent", Reagent)));
+ }
+ }
+}
diff --git a/Content.Server/Disease/DiseaseDiagnosisSystem.cs b/Content.Server/Disease/DiseaseDiagnosisSystem.cs
new file mode 100644
index 0000000000..148bdd8b89
--- /dev/null
+++ b/Content.Server/Disease/DiseaseDiagnosisSystem.cs
@@ -0,0 +1,415 @@
+using System.Threading;
+using Content.Server.Disease.Components;
+using Content.Shared.Disease;
+using Content.Shared.Disease.Components;
+using Content.Shared.Interaction;
+using Content.Shared.Inventory;
+using Content.Shared.Examine;
+using Content.Server.DoAfter;
+using Content.Server.Popups;
+using Content.Server.Hands.Components;
+using Content.Server.Nutrition.EntitySystems;
+using Content.Server.Paper;
+using Content.Server.Tools.Components;
+using Content.Server.Power.Components;
+using Robust.Shared.Random;
+using Robust.Shared.Player;
+using Robust.Shared.Audio;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Disease
+{
+ /// Everything that's about disease diangosis and machines is in here
+
+ public sealed class DiseaseDiagnosisSystem : EntitySystem
+ {
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly InventorySystem _inventorySystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent(OnExamined);
+ SubscribeLocalEvent(OnAfterInteractUsing);
+ SubscribeLocalEvent(OnAfterInteractUsingVaccine);
+ /// Visuals
+ SubscribeLocalEvent(OnPowerChanged);
+ /// Private Events
+ SubscribeLocalEvent(OnDiagnoserFinished);
+ SubscribeLocalEvent(OnVaccinatorFinished);
+ SubscribeLocalEvent(OnTargetSwabSuccessful);
+ SubscribeLocalEvent(OnSwabCancelled);
+ }
+
+ private Queue AddQueue = new();
+ private Queue RemoveQueue = new();
+
+ ///
+ /// This handles running disease machines
+ /// to handle their delay and visuals.
+ ///
+ public override void Update(float frameTime)
+ {
+ foreach (var uid in AddQueue)
+ EnsureComp(uid);
+
+ AddQueue.Clear();
+ foreach (var uid in RemoveQueue)
+ RemComp(uid);
+
+ RemoveQueue.Clear();
+
+ foreach (var (runningComp, diseaseMachine) in EntityQuery(false))
+ {
+ if (diseaseMachine.Accumulator < diseaseMachine.Delay)
+ {
+ diseaseMachine.Accumulator += frameTime;
+ return;
+ }
+
+ diseaseMachine.Accumulator = 0;
+ var ev = new DiseaseMachineFinishedEvent(diseaseMachine);
+ RaiseLocalEvent(diseaseMachine.Owner, ev, false);
+ RemoveQueue.Enqueue(diseaseMachine.Owner);
+ }
+ }
+
+ ///
+ /// Event Handlers
+ ///
+
+ ///
+ /// This handles using swabs on other people
+ /// and checks that the swab isn't already used
+ /// and the other person's mouth is accessible
+ /// and then adds a random disease from that person
+ /// to the swab if they have any
+ ///
+ private void OnAfterInteract(EntityUid uid, DiseaseSwabComponent swab, AfterInteractEvent args)
+ {
+ if (swab.CancelToken != null)
+ {
+ swab.CancelToken.Cancel();
+ swab.CancelToken = null;
+ return;
+ }
+ if (args.Target == null || !args.CanReach)
+ return;
+
+ if (!TryComp(args.Target, out var carrier))
+ return;
+
+ if (swab.Used)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("swab-already-used"), args.User, Filter.Entities(args.User));
+ return;
+ }
+
+ if (_inventorySystem.TryGetSlotEntity(args.Target.Value, "mask", out var maskUid) &&
+ EntityManager.TryGetComponent(maskUid, out var blocker) &&
+ blocker.Enabled)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("swab-mask-blocked", ("target", args.Target), ("mask", maskUid)), args.User, Filter.Entities(args.User));
+ return;
+ }
+
+ swab.CancelToken = new CancellationTokenSource();
+ _doAfterSystem.DoAfter(new DoAfterEventArgs(args.User, swab.SwabDelay, swab.CancelToken.Token, target: args.Target)
+ {
+ BroadcastFinishedEvent = new TargetSwabSuccessfulEvent(args.User, args.Target, swab, carrier),
+ BroadcastCancelledEvent = new SwabCancelledEvent(swab),
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ BreakOnStun = true,
+ NeedHand = true
+ });
+ }
+
+ ///
+ /// This handles the disease diagnoser machine up
+ /// until it's turned on. It has some slight
+ /// differences in checks from the vaccinator.
+ ///
+ private void OnAfterInteractUsing(EntityUid uid, DiseaseDiagnoserComponent component, AfterInteractUsingEvent args)
+ {
+ var machine = Comp(uid);
+ if (args.Handled || !args.CanReach)
+ return;
+
+ if (TryComp(uid, out var power) && !power.Powered)
+ return;
+
+ if (!HasComp(args.User) || HasComp(args.Used)) // Don't want to accidentally breach wrenching or whatever
+ return;
+
+ if (!TryComp(args.Used, out var swab))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("diagnoser-cant-use-swab", ("machine", uid), ("swab", args.Used)), uid, Filter.Entities(args.User));
+ return;
+ }
+ _popupSystem.PopupEntity(Loc.GetString("diagnoser-insert-swab", ("machine", uid), ("swab", args.Used)), uid, Filter.Entities(args.User));
+
+
+ machine.Disease = swab.Disease;
+ EntityManager.DeleteEntity(args.Used);
+
+ AddQueue.Enqueue(uid);
+ UpdateAppearance(uid, true, true);
+ SoundSystem.Play(Filter.Pvs(uid), "/Audio/Machines/diagnoser_printing.ogg", uid);
+ }
+
+ ///
+ /// This handles the vaccinator machine up
+ /// until it's turned on. It has some slight
+ /// differences in checks from the diagnoser.
+ ///
+ private void OnAfterInteractUsingVaccine(EntityUid uid, DiseaseVaccineCreatorComponent component, AfterInteractUsingEvent args)
+ {
+ if (args.Handled || !args.CanReach)
+ return;
+
+ if (TryComp(uid, out var power) && !power.Powered)
+ return;
+
+ if (!HasComp(args.User) || HasComp(args.Used)) //This check ensures tools don't break without yaml ordering jank
+ return;
+
+ if (!TryComp(args.Used, out var swab) || swab.Disease == null || !swab.Disease.Infectious)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("diagnoser-cant-use-swab", ("machine", uid), ("swab", args.Used)), uid, Filter.Entities(args.User));
+ return;
+ }
+ _popupSystem.PopupEntity(Loc.GetString("diagnoser-insert-swab", ("machine", uid), ("swab", args.Used)), uid, Filter.Entities(args.User));
+ var machine = Comp(uid);
+ machine.Disease = swab.Disease;
+ EntityManager.DeleteEntity(args.Used);
+
+ AddQueue.Enqueue(uid);
+ UpdateAppearance(uid, true, true);
+ SoundSystem.Play(Filter.Pvs(uid), "/Audio/Machines/vaccinator_running.ogg", uid);
+ }
+
+ ///
+ /// This handles swab examination text
+ /// so you can tell if they are used or not.
+ ///
+ private void OnExamined(EntityUid uid, DiseaseSwabComponent swab, ExaminedEvent args)
+ {
+ if (args.IsInDetailsRange)
+ {
+ if (swab.Used)
+ args.PushMarkup(Loc.GetString("swab-used"));
+ else
+ args.PushMarkup(Loc.GetString("swab-unused"));
+ }
+ }
+
+ ///
+ /// Helper functions
+ ///
+
+ ///
+ /// This assembles a disease report
+ /// With its basic details and
+ /// specific cures (i.e. not spaceacillin).
+ /// The cure resist field tells you how
+ /// effective spaceacillin etc will be.
+ ///
+ private FormattedMessage AssembleDiseaseReport(DiseasePrototype disease)
+ {
+ FormattedMessage report = new();
+ report.AddMarkup(Loc.GetString("diagnoser-disease-report-name", ("disease", disease.Name)));
+ report.PushNewline();
+
+ if (disease.Infectious)
+ {
+ report.AddMarkup(Loc.GetString("diagnoser-disease-report-infectious"));
+ report.PushNewline();
+ } else
+ {
+ report.AddMarkup(Loc.GetString("diagnoser-disease-report-not-infectious"));
+ report.PushNewline();
+ }
+ string cureResistLine = string.Empty;
+ cureResistLine += disease.CureResist switch
+ {
+ < 0f => Loc.GetString("diagnoser-disease-report-cureresist-none"),
+ <= 0.05f => Loc.GetString("diagnoser-disease-report-cureresist-low"),
+ <= 0.14f => Loc.GetString("diagnoser-disease-report-cureresist-medium"),
+ _ => Loc.GetString("diagnoser-disease-report-cureresist-high")
+ };
+ report.AddMarkup(cureResistLine);
+ report.PushNewline();
+
+ /// Add Cures
+ if (disease.Cures.Count == 0)
+ {
+ report.AddMarkup(Loc.GetString("diagnoser-no-cures"));
+ }
+ else
+ {
+ report.PushNewline();
+ report.AddMarkup(Loc.GetString("diagnoser-cure-has"));
+ report.PushNewline();
+
+ foreach (var cure in disease.Cures)
+ {
+ report.AddMarkup(cure.CureText());
+ report.PushNewline();
+ }
+ }
+
+ return report;
+ }
+ ///
+ /// Appearance stuff
+ ///
+
+ ///
+ /// Appearance helper function to
+ /// set the component's power and running states.
+ ///
+ private void UpdateAppearance(EntityUid uid, bool isOn, bool isRunning)
+ {
+ if (!TryComp(uid, out var appearance))
+ return;
+
+ appearance.SetData(DiseaseMachineVisuals.IsOn, isOn);
+ appearance.SetData(DiseaseMachineVisuals.IsRunning, isRunning);
+ }
+ ///
+ /// Makes sure the machine is visually off/on.
+ ///
+ private void OnPowerChanged(EntityUid uid, DiseaseMachineComponent component, PowerChangedEvent args)
+ {
+ UpdateAppearance(uid, args.Powered, false);
+ }
+ ///
+ /// Private events
+ ///
+
+ ///
+ /// Copies a disease prototype to the swab
+ /// after the doafter completes.
+ ///
+ private void OnTargetSwabSuccessful(TargetSwabSuccessfulEvent args)
+ {
+ if (args.Target == null)
+ return;
+
+ args.Swab.Used = true;
+ _popupSystem.PopupEntity(Loc.GetString("swab-swabbed", ("target", args.Target)), args.Target.Value, Filter.Entities(args.User));
+
+ if (args.Swab.Disease != null || args.Carrier.Diseases.Count == 0)
+ return;
+
+ args.Swab.Disease = _random.Pick(args.Carrier.Diseases);
+ }
+
+ ///
+ /// Cancels the swab doafter if needed.
+ ///
+ private static void OnSwabCancelled(SwabCancelledEvent args)
+ {
+ args.Swab.CancelToken = null;
+ }
+
+ ///
+ /// Prints a diagnostic report with its findings.
+ /// Also cancels the animation.
+ ///
+ private void OnDiagnoserFinished(EntityUid uid, DiseaseDiagnoserComponent component, DiseaseMachineFinishedEvent args)
+ {
+ var power = Comp(uid);
+ UpdateAppearance(uid, power.Powered, false);
+ // spawn a piece of paper.
+ var printed = EntityManager.SpawnEntity(args.Machine.MachineOutput, Transform(uid).Coordinates);
+
+ if (!TryComp(printed, out var paper))
+ return;
+
+ var reportTitle = string.Empty;
+ FormattedMessage contents = new();
+ if (args.Machine.Disease != null)
+ {
+ reportTitle = Loc.GetString("diagnoser-disease-report", ("disease", args.Machine.Disease.Name));
+ contents = AssembleDiseaseReport(args.Machine.Disease);
+ } else
+ {
+ reportTitle = Loc.GetString("diagnoser-disease-report-none");
+ contents.AddMarkup(Loc.GetString("diagnoser-disease-report-none-contents"));
+ }
+ MetaData(printed).EntityName = reportTitle;
+
+ paper.SetContent(contents.ToMarkup());
+ }
+
+ ///
+ /// Prints a vaccine that will vaccinate
+ /// against the disease on the inserted swab.
+ ///
+ private void OnVaccinatorFinished(EntityUid uid, DiseaseVaccineCreatorComponent component, DiseaseMachineFinishedEvent args)
+ {
+ var power = Comp(uid);
+ UpdateAppearance(uid, power.Powered, false);
+
+ // spawn a vaccine
+ var vaxx = EntityManager.SpawnEntity(args.Machine.MachineOutput, Transform(uid).Coordinates);
+
+ if (!TryComp(vaxx, out var vaxxComp))
+ return;
+
+ vaxxComp.Disease = args.Machine.Disease;
+ }
+
+ ///
+ /// Cancels the mouth-swabbing doafter
+ ///
+ private sealed class SwabCancelledEvent : EntityEventArgs
+ {
+ public readonly DiseaseSwabComponent Swab;
+ public SwabCancelledEvent(DiseaseSwabComponent swab)
+ {
+ Swab = swab;
+ }
+ }
+
+ ///
+ /// Fires if the doafter for swabbing someone's mouth succeeds
+ ///
+ private sealed class TargetSwabSuccessfulEvent : EntityEventArgs
+ {
+ public EntityUid User { get; }
+ public EntityUid? Target { get; }
+ public DiseaseSwabComponent Swab { get; }
+
+ public DiseaseCarrierComponent Carrier { get; }
+
+ public TargetSwabSuccessfulEvent(EntityUid user, EntityUid? target, DiseaseSwabComponent swab, DiseaseCarrierComponent carrier)
+ {
+ User = user;
+ Target = target;
+ Swab = swab;
+ Carrier = carrier;
+ }
+ }
+
+ ///
+ /// Fires when a disease machine is done
+ /// with its production delay and ready to
+ /// create a report or vaccine
+ ///
+ private sealed class DiseaseMachineFinishedEvent : EntityEventArgs
+ {
+ public DiseaseMachineComponent Machine {get;}
+ public DiseaseMachineFinishedEvent(DiseaseMachineComponent machine)
+ {
+ Machine = machine;
+ }
+ }
+ }
+}
+
diff --git a/Content.Server/Disease/DiseaseSystem.cs b/Content.Server/Disease/DiseaseSystem.cs
new file mode 100644
index 0000000000..2933e7471e
--- /dev/null
+++ b/Content.Server/Disease/DiseaseSystem.cs
@@ -0,0 +1,442 @@
+using System.Threading;
+using Content.Shared.Disease;
+using Content.Shared.Disease.Components;
+using Content.Server.Disease.Components;
+using Content.Server.Clothing.Components;
+using Content.Shared.MobState.Components;
+using Content.Shared.Examine;
+using Content.Shared.Inventory;
+using Content.Shared.Interaction;
+using Content.Server.Popups;
+using Content.Server.DoAfter;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Serialization.Manager;
+using Content.Shared.Inventory.Events;
+using Content.Server.Nutrition.EntitySystems;
+
+namespace Content.Server.Disease
+{
+
+ ///
+ /// Handles disease propagation & curing
+ ///
+ public sealed class DiseaseSystem : EntitySystem
+ {
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly ISerializationManager _serializationManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
+ [Dependency] private readonly InventorySystem _inventorySystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnTryCureDisease);
+ SubscribeLocalEvent(OnInteractDiseasedHand);
+ SubscribeLocalEvent(OnInteractDiseasedUsing);
+ SubscribeLocalEvent(OnEquipped);
+ SubscribeLocalEvent(OnUnequipped);
+ SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent(OnExamined);
+ /// Private events stuff
+ SubscribeLocalEvent(OnTargetVaxxSuccessful);
+ SubscribeLocalEvent(OnVaxxCancelled);
+ }
+
+ private Queue AddQueue = new();
+ private Queue<(DiseaseCarrierComponent carrier, DiseasePrototype disease)> CureQueue = new();
+
+ ///
+ /// First, adds or removes diseased component from the queues and clears them.
+ /// Then, iterates over every diseased component to check for their effects
+ /// and cures
+ ///
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ foreach (var entity in AddQueue)
+ EnsureComp(entity);
+ AddQueue.Clear();
+
+ foreach (var tuple in CureQueue)
+ {
+ if (tuple.carrier.Diseases.Count == 1) //This is reliable unlike testing Count == 0 right after removal for reasons I don't quite get
+ RemComp(tuple.carrier.Owner);
+ tuple.carrier.PastDiseases.Add(tuple.disease);
+ tuple.carrier.Diseases.Remove(tuple.disease);
+ }
+ CureQueue.Clear();
+
+ foreach (var (diseasedComp, carrierComp, mobState) in EntityQuery(false))
+ {
+ if (mobState.IsDead())
+ {
+ if (_random.Prob(0.005f * frameTime)) //Mean time to remove is 200 seconds per disease
+ CureDisease(carrierComp, _random.Pick(carrierComp.Diseases));
+ continue;
+ }
+
+ foreach(var disease in carrierComp.Diseases)
+ {
+ var args = new DiseaseEffectArgs(carrierComp.Owner, disease, EntityManager);
+ disease.Accumulator += frameTime;
+ if (disease.Accumulator >= disease.TickTime)
+ {
+ disease.Accumulator -= disease.TickTime;
+ foreach (var cure in disease.Cures)
+ {
+ if (cure.Cure(args))
+ CureDisease(carrierComp, disease);
+ }
+ foreach (var effect in disease.Effects)
+ {
+ if (_random.Prob(effect.Probability))
+ effect.Effect(args);
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Event Handlers
+ ///
+
+ ///
+ /// Used when something is trying to cure ANY disease on the target,
+ /// not for special disease interactions. Randomly
+ /// tries to cure every disease on the target.
+ ///
+ private void OnTryCureDisease(EntityUid uid, DiseaseCarrierComponent component, CureDiseaseAttemptEvent args)
+ {
+ foreach (var disease in component.Diseases)
+ {
+ var cureProb = ((args.CureChance / component.Diseases.Count) - disease.CureResist);
+ if (cureProb < 0)
+ return;
+ if (cureProb > 1)
+ {
+ CureDisease(component, disease);
+ return;
+ }
+ if (_random.Prob(cureProb))
+ {
+ CureDisease(component, disease);
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Called when a component with disease protection
+ /// is equipped so it can be added to the person's
+ /// total disease resistance
+ ///
+ private void OnEquipped(EntityUid uid, DiseaseProtectionComponent component, GotEquippedEvent args)
+ {
+ /// This only works on clothing
+ if (!TryComp(uid, out var clothing))
+ return;
+ /// Is the clothing in its actual slot?
+ if (!clothing.SlotFlags.HasFlag(args.SlotFlags))
+ return;
+ /// Give the user the component's disease resist
+ if(TryComp(args.Equipee, out var carrier))
+ carrier.DiseaseResist += component.Protection;
+ /// Set the component to active to the unequip check isn't CBT
+ component.IsActive = true;
+ }
+
+ ///
+ /// Called when a component with disease protection
+ /// is unequipped so it can be removed from the person's
+ /// total disease resistance
+ ///
+ private void OnUnequipped(EntityUid uid, DiseaseProtectionComponent component, GotUnequippedEvent args)
+ {
+ /// Only undo the resistance if it was affecting the user
+ if (!component.IsActive)
+ return;
+ if(TryComp(args.Equipee, out var carrier))
+ carrier.DiseaseResist -= component.Protection;
+ component.IsActive = false;
+ }
+
+ ///
+ /// Called when it's already decided a disease will be cured
+ /// so it can be safely queued up to be removed from the target
+ /// and added to past disease history (for immunity)
+ ///
+ private void CureDisease(DiseaseCarrierComponent carrier, DiseasePrototype disease)
+ {
+ var CureTuple = (carrier, disease);
+ CureQueue.Enqueue(CureTuple);
+ _popupSystem.PopupEntity(Loc.GetString("disease-cured"), carrier.Owner, Filter.Entities(carrier.Owner));
+ }
+
+ ///
+ /// Called when someone interacts with a diseased person with an empty hand
+ /// to check if they get infected
+ ///
+ private void OnInteractDiseasedHand(EntityUid uid, DiseasedComponent component, InteractHandEvent args)
+ {
+ if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target))
+ return;
+ InteractWithDiseased (args.Target, args.User);
+ }
+
+ ///
+ /// Called when someone interacts with a diseased person with any object
+ /// to check if they get infected
+ ///
+ private void OnInteractDiseasedUsing(EntityUid uid, DiseasedComponent component, InteractUsingEvent args)
+ {
+ InteractWithDiseased(args.Target, args.User);
+ }
+
+ ///
+ /// Called when a vaccine is used on someone
+ /// to handle the vaccination doafter
+ ///
+ private void OnAfterInteract(EntityUid uid, DiseaseVaccineComponent vaxx, AfterInteractEvent args)
+ {
+ if (vaxx.CancelToken != null)
+ {
+ vaxx.CancelToken.Cancel();
+ vaxx.CancelToken = null;
+ return;
+ }
+ if (args.Target == null)
+ return;
+
+ if (!args.CanReach)
+ return;
+
+ if (vaxx.CancelToken != null)
+ return;
+
+ if (!TryComp(args.Target, out var carrier))
+ return;
+
+ if (vaxx.Used)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("vaxx-already-used"), args.User, Filter.Entities(args.User));
+ return;
+ }
+
+ vaxx.CancelToken = new CancellationTokenSource();
+ _doAfterSystem.DoAfter(new DoAfterEventArgs(args.User, vaxx.InjectDelay, vaxx.CancelToken.Token, target: args.Target)
+ {
+ BroadcastFinishedEvent = new TargetVaxxSuccessfulEvent(args.User, args.Target, vaxx, carrier),
+ BroadcastCancelledEvent = new VaxxCancelledEvent(vaxx),
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ BreakOnStun = true,
+ NeedHand = true
+ });
+ }
+
+ ///
+ /// Called when a vaccine is examined.
+ /// Currently doesn't do much because
+ /// vaccines don't have unique art with a seperate
+ /// state visualizer.
+ ///
+ private void OnExamined(EntityUid uid, DiseaseVaccineComponent vaxx, ExaminedEvent args)
+ {
+ if (args.IsInDetailsRange)
+ {
+ if (vaxx.Used)
+ args.PushMarkup(Loc.GetString("vaxx-used"));
+ else
+ args.PushMarkup(Loc.GetString("vaxx-unused"));
+ }
+ }
+
+ ///
+ /// Helper functions
+ ///
+
+ ///
+ /// Tries to infect anyone that
+ /// interacts with a diseased person or body
+ ///
+ private void InteractWithDiseased(EntityUid diseased, EntityUid target)
+ {
+ if (!TryComp(target, out var carrier))
+ return;
+
+ var disease = _random.Pick(Comp(diseased).Diseases);
+ if (disease != null)
+ TryInfect(carrier, disease, 0.4f);
+ }
+
+ ///
+ /// Adds a disease to a target
+ /// if it's not already in their current
+ /// or past diseases. If you want this
+ /// to not be guaranteed you are looking
+ /// for TryInfect.
+ ///
+ public void TryAddDisease(DiseaseCarrierComponent? target, DiseasePrototype? addedDisease, string? diseaseName = null, EntityUid host = default!)
+ {
+ if (diseaseName != null && _prototypeManager.TryIndex(diseaseName, out DiseasePrototype? diseaseProto))
+ addedDisease = diseaseProto;
+
+ if (host != default!)
+ target = Comp(host);
+
+ if (target != null)
+ {
+ foreach (var disease in target.AllDiseases)
+ {
+ if (disease.ID == addedDisease?.ID) //ID because of the way protoypes work
+ return;
+ }
+ var freshDisease = _serializationManager.CreateCopy(addedDisease) ?? default!;
+ target.Diseases.Add(freshDisease);
+ AddQueue.Enqueue(target.Owner);
+ }
+ }
+
+ ///
+ /// Pits the infection chance against the
+ /// person's disease resistance and
+ /// rolls the dice to see if they get
+ /// the disease.
+ ///
+ public void TryInfect(DiseaseCarrierComponent carrier, DiseasePrototype? disease, float chance = 0.7f)
+ {
+ if(disease == null || !disease.Infectious)
+ return;
+ var infectionChance = chance - carrier.DiseaseResist;
+ if (infectionChance <= 0)
+ return;
+ if (_random.Prob(infectionChance))
+ TryAddDisease(carrier, disease);
+ }
+
+ ///
+ /// Plays a sneeze/cough popup if applicable
+ /// and then tries to infect anyone in range
+ /// if the snougher is not wearing a mask.
+ ///
+ public void SneezeCough(EntityUid uid, DiseasePrototype? disease, string snoughMessage, bool airTransmit = true, float infectionChance = 0.3f)
+ {
+ var xform = Comp(uid);
+ if (snoughMessage != string.Empty)
+ _popupSystem.PopupEntity(Loc.GetString(snoughMessage, ("person", uid)), uid, Filter.Pvs(uid));
+
+ if (disease == null || !disease.Infectious || airTransmit == false)
+ return;
+
+ if (_inventorySystem.TryGetSlotEntity(uid, "mask", out var maskUid) &&
+ EntityManager.TryGetComponent(maskUid, out var blocker) &&
+ blocker.Enabled)
+ return;
+
+ foreach (var entity in _lookup.GetEntitiesInRange(xform.MapID, xform.WorldPosition, 2f))
+ {
+ if (!_interactionSystem.InRangeUnobstructed(uid, entity))
+ continue;
+
+ if (TryComp(entity, out var carrier))
+ TryInfect(carrier, disease, 0.3f);
+ }
+ }
+
+ ///
+ /// Adds a disease to the carrier's
+ /// past diseases to give them immunity
+ /// IF they don't already have the disease.
+ ///
+ public void Vaccinate(DiseaseCarrierComponent carrier, DiseasePrototype disease)
+ {
+ foreach (var currentDisease in carrier.Diseases)
+ {
+ if (currentDisease.ID == disease.ID) //ID because of the way protoypes work
+ return;
+ }
+ carrier.PastDiseases.Add(disease);
+ }
+
+ ///
+ /// Private Events Stuff
+ ///
+
+ ///
+ /// Injects the vaccine into the target
+ /// if the doafter is completed
+ ///
+ private void OnTargetVaxxSuccessful(TargetVaxxSuccessfulEvent args)
+ {
+ if (args.Vaxx.Disease == null)
+ return;
+ Vaccinate(args.Carrier, args.Vaxx.Disease);
+ EntityManager.DeleteEntity(args.Vaxx.Owner);
+ }
+
+ ///
+ /// Cancels the vaccine doafter
+ ///
+ private static void OnVaxxCancelled(VaxxCancelledEvent args)
+ {
+ args.Vaxx.CancelToken = null;
+ }
+ /// These two are standard doafter stuff you can ignore
+ private sealed class VaxxCancelledEvent : EntityEventArgs
+ {
+ public readonly DiseaseVaccineComponent Vaxx;
+ public VaxxCancelledEvent(DiseaseVaccineComponent vaxx)
+ {
+ Vaxx = vaxx;
+ }
+ }
+ private sealed class TargetVaxxSuccessfulEvent : EntityEventArgs
+ {
+ public EntityUid User { get; }
+ public EntityUid? Target { get; }
+ public DiseaseVaccineComponent Vaxx { get; }
+ public DiseaseCarrierComponent Carrier { get; }
+ public TargetVaxxSuccessfulEvent(EntityUid user, EntityUid? target, DiseaseVaccineComponent vaxx, DiseaseCarrierComponent carrier)
+ {
+ User = user;
+ Target = target;
+ Vaxx = vaxx;
+ Carrier = carrier;
+ }
+ }
+ }
+
+ ///
+ /// This event is fired by chems
+ /// and other brute-force rather than
+ /// specific cures. It will roll the dice to attempt
+ /// to cure each disease on the target
+ ///
+ public sealed class CureDiseaseAttemptEvent : EntityEventArgs
+ {
+ public float CureChance { get; }
+ public CureDiseaseAttemptEvent(float cureChance)
+ {
+ CureChance = cureChance;
+ }
+ }
+
+ ///
+ /// Controls whether the snough is a sneeze, cough
+ /// or neither. If none, will not create
+ /// a popup. Mostly used for talking
+ ///
+ public enum SneezeCoughType
+ {
+ Sneeze,
+ Cough,
+ None
+ }
+}
diff --git a/Content.Server/Disease/Effects/DiseaseAdjustReagent.cs b/Content.Server/Disease/Effects/DiseaseAdjustReagent.cs
new file mode 100644
index 0000000000..26c180a887
--- /dev/null
+++ b/Content.Server/Disease/Effects/DiseaseAdjustReagent.cs
@@ -0,0 +1,46 @@
+using Content.Server.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.FixedPoint;
+using JetBrains.Annotations;
+using Content.Server.Body.Components;
+using Content.Shared.Disease;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Disease.Effects
+{
+ ///
+ /// Adds or removes reagents from the
+ /// host's chemstream.
+ ///
+ [UsedImplicitly]
+ public sealed class DiseaseAdjustReagent : DiseaseEffect
+ {
+ ///
+ /// The reagent ID to add or remove.
+ ///
+ [DataField("reagent", customTypeSerializer:typeof(PrototypeIdSerializer))]
+ public string? Reagent = null;
+
+ [DataField("amount", required: true)]
+ public FixedPoint2 Amount = default!;
+
+ public override void Effect(DiseaseEffectArgs args)
+ {
+ if (!args.EntityManager.TryGetComponent(args.DiseasedEntity, out var bloodstream))
+ return;
+
+ var stream = bloodstream.ChemicalSolution;
+ if (stream != null)
+ {
+ var solutionSys = args.EntityManager.EntitySysManager.GetEntitySystem();
+ if (Reagent != null)
+ {
+ if (Amount < 0 && stream.ContainsReagent(Reagent))
+ solutionSys.TryRemoveReagent(args.DiseasedEntity, stream, Reagent, -Amount);
+ if (Amount > 0)
+ solutionSys.TryAddReagent(args.DiseasedEntity, stream, Reagent, Amount, out _);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/Disease/Effects/DiseaseGenericStatusEffect.cs b/Content.Server/Disease/Effects/DiseaseGenericStatusEffect.cs
new file mode 100644
index 0000000000..af36ba9398
--- /dev/null
+++ b/Content.Server/Disease/Effects/DiseaseGenericStatusEffect.cs
@@ -0,0 +1,67 @@
+using Content.Shared.Disease;
+using Content.Shared.StatusEffect;
+using JetBrains.Annotations;
+
+namespace Content.Server.Disease.Effects
+{
+ ///
+ /// Adds a generic status effect to the entity.
+ /// Differs from the chem version in its defaults
+ /// to better facilitate adding components that
+ /// last the length of the disease.
+ ///
+ [UsedImplicitly]
+ public sealed class DiseaseGenericStatusEffect : DiseaseEffect
+ {
+ ///
+ /// The status effect key
+ /// Prevents other components from being with the same key
+ ///
+ [DataField("key", required: true)]
+ public string Key = default!;
+ ///
+ /// The component to add
+ ///
+ [DataField("component")]
+ public string Component = String.Empty;
+
+ [DataField("time")]
+ public float Time = 1.01f; /// I'm afraid if this was exact the key could get stolen by another thing
+
+ ///
+ /// true - refresh status effect time, false - accumulate status effect time
+ ///
+ [DataField("refresh")]
+ public bool Refresh = false;
+
+ ///
+ /// Should this effect add the status effect, remove time from it, or set its cooldown?
+ ///
+ [DataField("type")]
+ public StatusEffectDiseaseType Type = StatusEffectDiseaseType.Add;
+
+ public override void Effect(DiseaseEffectArgs args)
+ {
+ var statusSys = EntitySystem.Get();
+ if (Type == StatusEffectDiseaseType.Add && Component != String.Empty)
+ {
+ statusSys.TryAddStatusEffect(args.DiseasedEntity, Key, TimeSpan.FromSeconds(Time), Refresh, Component);
+ }
+ else if (Type == StatusEffectDiseaseType.Remove)
+ {
+ statusSys.TryRemoveTime(args.DiseasedEntity, Key, TimeSpan.FromSeconds(Time));
+ }
+ else if (Type == StatusEffectDiseaseType.Set)
+ {
+ statusSys.TrySetTime(args.DiseasedEntity, Key, TimeSpan.FromSeconds(Time));
+ }
+ }
+ }
+ /// See status effects for how these work
+ public enum StatusEffectDiseaseType
+ {
+ Add,
+ Remove,
+ Set
+ }
+}
diff --git a/Content.Server/Disease/Effects/DiseaseHealthChange.cs b/Content.Server/Disease/Effects/DiseaseHealthChange.cs
new file mode 100644
index 0000000000..af5a81cc88
--- /dev/null
+++ b/Content.Server/Disease/Effects/DiseaseHealthChange.cs
@@ -0,0 +1,21 @@
+using Content.Shared.Disease;
+using Content.Shared.Damage;
+using JetBrains.Annotations;
+
+namespace Content.Server.Disease.Effects
+{
+ ///
+ /// Deals or heals damage to the host
+ ///
+ [UsedImplicitly]
+ public sealed class DiseaseHealthChange : DiseaseEffect
+ {
+ [DataField("damage", required: true)]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public DamageSpecifier Damage = default!;
+ public override void Effect(DiseaseEffectArgs args)
+ {
+ EntitySystem.Get().TryChangeDamage(args.DiseasedEntity, Damage, true, false);
+ }
+ }
+}
diff --git a/Content.Server/Disease/Effects/DiseasePopUp.cs b/Content.Server/Disease/Effects/DiseasePopUp.cs
new file mode 100644
index 0000000000..beee93c52e
--- /dev/null
+++ b/Content.Server/Disease/Effects/DiseasePopUp.cs
@@ -0,0 +1,38 @@
+using Content.Shared.Disease;
+using Content.Shared.Popups;
+using Robust.Shared.Player;
+using JetBrains.Annotations;
+
+namespace Content.Server.Disease.Effects
+{
+ [UsedImplicitly]
+ ///
+ /// Plays a popup on the host's transform.
+ /// Supports passing the host's entity metadata
+ /// in PVS ones with {$person}
+ ///
+ public sealed class DiseasePopUp : DiseaseEffect
+ {
+ [DataField("message")]
+ public string Message = "disease-sick-generic";
+
+ [DataField("type")]
+ public PopupType Type = PopupType.Local;
+ public override void Effect(DiseaseEffectArgs args)
+ {
+ var popupSys = EntitySystem.Get();
+
+ if (Type == PopupType.Local)
+ popupSys.PopupEntity(Loc.GetString(Message), args.DiseasedEntity, Filter.Entities(args.DiseasedEntity));
+ else if (Type == PopupType.Pvs)
+ popupSys.PopupEntity(Loc.GetString(Message, ("person", args.DiseasedEntity)), args.DiseasedEntity, Filter.Pvs(args.DiseasedEntity));
+ }
+
+ }
+
+ public enum PopupType
+ {
+ Pvs,
+ Local
+ }
+}
diff --git a/Content.Server/Disease/Effects/DiseaseSnough.cs b/Content.Server/Disease/Effects/DiseaseSnough.cs
new file mode 100644
index 0000000000..ce4d4099be
--- /dev/null
+++ b/Content.Server/Disease/Effects/DiseaseSnough.cs
@@ -0,0 +1,30 @@
+using Content.Shared.Disease;
+using JetBrains.Annotations;
+
+namespace Content.Server.Disease
+{
+ [UsedImplicitly]
+
+ ///
+ /// Makes the diseased sneeze or cough
+ /// or neither.
+ ///
+ public sealed class DiseaseSnough : DiseaseEffect
+ {
+ ///
+ /// Message to play when snoughing
+ ///
+ [DataField("snoughMessage")]
+ public string SnoughMessage = "disease-sneeze";
+ ///
+ /// Whether to spread the disease throught he air
+ ///
+ [DataField("airTransmit")]
+ public bool AirTransmit = true;
+
+ public override void Effect(DiseaseEffectArgs args)
+ {
+ EntitySystem.Get().SneezeCough(args.DiseasedEntity, args.Disease, SnoughMessage, AirTransmit);
+ }
+ }
+}
diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs
index e70b305c39..d067ba81aa 100644
--- a/Content.Server/Entry/IgnoredComponents.cs
+++ b/Content.Server/Entry/IgnoredComponents.cs
@@ -17,6 +17,7 @@ namespace Content.Server.Entry
"ClientEntitySpawner",
"CharacterInfo",
"ItemCabinetVisuals",
+ "DiseaseMachineVisuals",
"HandheldGPS",
"PotencyVisuals"
};
diff --git a/Content.Server/Medical/Components/HealthAnalyzerComponent.cs b/Content.Server/Medical/Components/HealthAnalyzerComponent.cs
index b7ba852605..733c4e8745 100644
--- a/Content.Server/Medical/Components/HealthAnalyzerComponent.cs
+++ b/Content.Server/Medical/Components/HealthAnalyzerComponent.cs
@@ -1,7 +1,9 @@
using System.Threading;
using Content.Server.UserInterface;
using Content.Shared.MedicalScanner;
+using Content.Shared.Disease;
using Robust.Server.GameObjects;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Medical.Components
{
@@ -23,5 +25,19 @@ namespace Content.Server.Medical.Components
///
public CancellationTokenSource? CancelToken;
public BoundUserInterface? UserInterface => Owner.GetUIOrNull(HealthAnalyzerUiKey.Key);
+
+ ///
+ /// Is this actually going to give people the disease below
+ ///
+ [DataField("fake")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public bool Fake = false;
+
+ ///
+ /// The disease this will give people if Fake == true
+ ///
+ [DataField("disease", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string Disease = string.Empty;
}
}
diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs
index 1c68f48deb..891dc2df86 100644
--- a/Content.Server/Medical/HealthAnalyzerSystem.cs
+++ b/Content.Server/Medical/HealthAnalyzerSystem.cs
@@ -1,10 +1,13 @@
using System.Threading;
using Content.Server.DoAfter;
using Content.Server.Medical.Components;
+using Content.Server.Disease;
+using Content.Server.Popups;
using Content.Shared.Damage;
using Content.Shared.Interaction;
using Content.Shared.MobState.Components;
using Robust.Server.GameObjects;
+using Robust.Shared.Player;
using static Content.Shared.MedicalScanner.SharedHealthAnalyzerComponent;
namespace Content.Server.Medical
@@ -12,6 +15,7 @@ namespace Content.Server.Medical
public sealed class HealthAnalyzerSystem : EntitySystem
{
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
public override void Initialize()
{
@@ -64,6 +68,22 @@ namespace Content.Server.Medical
{
args.Component.CancelToken = null;
UpdateScannedUser(args.Component.Owner, args.User, args.Target, args.Component);
+ /// Below is for the traitor item
+ /// Piggybacking off another component's doafter is complete CBT so I gave up
+ /// and put it on the same component
+ if (!args.Component.Fake || args.Component.Disease == string.Empty || args.Target == null)
+ return;
+
+ EntitySystem.Get().TryAddDisease(null, null, args.Component.Disease, args.Target.Value);
+
+ if (args.User == args.Target)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("disease-scanner-gave-self", ("disease", args.Component.Disease)),
+ args.User, Filter.Entities(args.User));
+ return;
+ }
+ _popupSystem.PopupEntity(Loc.GetString("disease-scanner-gave-other", ("target", args.Target), ("disease", args.Component.Disease)),
+ args.User, Filter.Entities(args.User));
}
private void OpenUserInterface(EntityUid user, HealthAnalyzerComponent healthAnalyzer)
diff --git a/Content.Server/StationEvents/Events/DiseaseOutbreak.cs b/Content.Server/StationEvents/Events/DiseaseOutbreak.cs
new file mode 100644
index 0000000000..8a038030be
--- /dev/null
+++ b/Content.Server/StationEvents/Events/DiseaseOutbreak.cs
@@ -0,0 +1,62 @@
+using System.Linq;
+using Content.Server.Chat.Managers;
+using Content.Server.Disease.Components;
+using Content.Server.Disease;
+using Content.Shared.Disease;
+using Robust.Shared.Random;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.StationEvents.Events;
+///
+/// Infects a couple people
+/// with a random disease that isn't super deadly
+///
+public sealed class DiseaseOutbreak : StationEvent
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IChatManager _chatManager = default!;
+
+ ///
+ /// Disease prototypes I decided were not too deadly for a random event
+ ///
+ public readonly IReadOnlyList NotTooSeriousDiseases = new[]
+ {
+ "SpaceCold",
+ "VanAusdallsRobovirus",
+ "VentCough",
+ "AMIV"
+ };
+ public override string Name => "DiseaseOutbreak";
+ public override float Weight => WeightNormal;
+ protected override float EndAfter => 1.0f;
+ ///
+ /// Finds 2-5 random entities that can host diseases
+ /// and gives them a randomly selected disease.
+ /// They all get the same disease.
+ ///
+ public override void Startup()
+ {
+ base.Startup();
+
+ var targetList = _entityManager.EntityQuery().ToList();
+ _random.Shuffle(targetList);
+
+ var toInfect = _random.Next(2, 5);
+
+ var diseaseName = _random.Pick(NotTooSeriousDiseases);
+
+ if (!_prototypeManager.TryIndex(diseaseName, out DiseasePrototype? disease) || disease == null)
+ return;
+
+ foreach (var target in targetList)
+ {
+ if (toInfect-- == 0)
+ break;
+
+ EntitySystem.Get().TryAddDisease(target, disease);
+ }
+ _chatManager.DispatchStationAnnouncement(Loc.GetString("station-event-disease-outbreak-announcement"));
+ }
+}
diff --git a/Content.Server/StationEvents/Events/VentClog.cs b/Content.Server/StationEvents/Events/VentClog.cs
index 6f39e8d841..c1c4a1cac2 100644
--- a/Content.Server/StationEvents/Events/VentClog.cs
+++ b/Content.Server/StationEvents/Events/VentClog.cs
@@ -42,7 +42,7 @@ public sealed class VentClog : StationEvent
public readonly IReadOnlyList SafeishVentChemicals = new[]
{
"Water", "Iron", "Oxygen", "Tritium", "Plasma", "SulfuricAcid", "Blood", "SpaceDrugs", "SpaceCleaner", "Flour",
- "Nutriment", "Sugar", "SpaceLube", "Ethanol", "Mercury", "Ephedrine", "WeldingFuel"
+ "Nutriment", "Sugar", "SpaceLube", "Ethanol", "Mercury", "Ephedrine", "WeldingFuel", "VentCrud"
};
public override void Startup()
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/DiseaseArtifactComponent.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/DiseaseArtifactComponent.cs
new file mode 100644
index 0000000000..bcbe184030
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/DiseaseArtifactComponent.cs
@@ -0,0 +1,37 @@
+using Content.Shared.Disease;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
+///
+/// Spawn a random disease at regular intervals when artifact activated.
+///
+[RegisterComponent]
+public sealed class DiseaseArtifactComponent : Component
+{
+ public override string Name => "DiseaseArtifact";
+ ///
+ /// Disease the artifact will spawn
+ /// If empty, picks a random one from its list
+ ///
+ [DataField("disease", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string SpawnDisease = string.Empty;
+ ///
+ /// How far away it will check for people
+ /// If empty, picks a random one from its list
+ ///
+ [DataField("range")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float Range = 5f;
+ [ViewVariables(VVAccess.ReadWrite)]
+ public DiseasePrototype ResolveDisease = default!;
+ [ViewVariables(VVAccess.ReadWrite)]
+ public readonly IReadOnlyList ArtifactDiseases = new[]
+ {
+ "VanAusdallsRobovirus",
+ "OwOnavirus",
+ "BleedersBite",
+ "Ultragigacancer",
+ "AMIV"
+ };
+}
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/DiseaseArtifactSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/DiseaseArtifactSystem.cs
new file mode 100644
index 0000000000..520f953df4
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/DiseaseArtifactSystem.cs
@@ -0,0 +1,63 @@
+using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
+using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
+using Content.Shared.Disease;
+using Content.Server.Disease;
+using Content.Server.Disease.Components;
+using Robust.Shared.Random;
+using Robust.Shared.Prototypes;
+using Content.Shared.Interaction;
+
+namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems
+{
+ ///
+ /// Handles disease-producing artifacts
+ ///
+ public sealed class DiseaseArtifactSystem : EntitySystem
+ {
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnActivate);
+ }
+
+ ///
+ /// Makes sure this artifact is assigned a disease
+ ///
+ private void OnMapInit(EntityUid uid, DiseaseArtifactComponent component, MapInitEvent args)
+ {
+ if (component.SpawnDisease == string.Empty && component.ArtifactDiseases.Count != 0)
+ {
+ var diseaseName = _random.Pick(component.ArtifactDiseases);
+
+ component.SpawnDisease = diseaseName;
+ }
+
+ if (_prototypeManager.TryIndex(component.SpawnDisease, out DiseasePrototype? disease) && disease != null)
+ component.ResolveDisease = disease;
+ }
+
+ ///
+ /// When activated, blasts everyone in LOS within 3 tiles
+ /// with a high-probability disease infection attempt
+ ///
+ private void OnActivate(EntityUid uid, DiseaseArtifactComponent component, ArtifactActivatedEvent args)
+ {
+ var xform = Transform(uid);
+ foreach (var entity in _lookup.GetEntitiesInRange(xform.MapID, xform.WorldPosition, 3f))
+ {
+ if (!_interactionSystem.InRangeUnobstructed(uid, entity, 3f))
+ continue;
+
+ if (TryComp(entity, out var carrier))
+ EntitySystem.Get().TryInfect(carrier, component.ResolveDisease);
+ }
+ }
+ }
+}
+
diff --git a/Content.Shared/Chemistry/Reagent/ReagentEffectCondition.cs b/Content.Shared/Chemistry/Reagent/ReagentEffectCondition.cs
index ea5b2857de..13d1cb37ee 100644
--- a/Content.Shared/Chemistry/Reagent/ReagentEffectCondition.cs
+++ b/Content.Shared/Chemistry/Reagent/ReagentEffectCondition.cs
@@ -1,7 +1,5 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared.Chemistry.Reagent
{
diff --git a/Content.Shared/Disease/DiseaseCure.cs b/Content.Shared/Disease/DiseaseCure.cs
new file mode 100644
index 0000000000..e933c9e591
--- /dev/null
+++ b/Content.Shared/Disease/DiseaseCure.cs
@@ -0,0 +1,24 @@
+using JetBrains.Annotations;
+
+namespace Content.Shared.Disease
+{
+ [ImplicitDataDefinitionForInheritors]
+ [MeansImplicitUse]
+ public abstract class DiseaseCure
+ {
+ ///
+ /// This returns true if the disease should be cured
+ /// and false otherwise
+ ///
+ public abstract bool Cure(DiseaseEffectArgs args);
+
+ ///
+ /// This is used by the disease diangoser machine
+ /// to generate reports to tell people all of a disease's
+ /// special cures using in-game methods.
+ /// So it should return a localization string describing
+ /// the cure
+ ///
+ public abstract string CureText();
+ }
+}
diff --git a/Content.Shared/Disease/DiseaseEffect.cs b/Content.Shared/Disease/DiseaseEffect.cs
new file mode 100644
index 0000000000..bc40bfc978
--- /dev/null
+++ b/Content.Shared/Disease/DiseaseEffect.cs
@@ -0,0 +1,29 @@
+using JetBrains.Annotations;
+
+namespace Content.Shared.Disease
+{
+ [ImplicitDataDefinitionForInheritors]
+ [MeansImplicitUse]
+ public abstract class DiseaseEffect
+ {
+ ///
+ /// What's the chance, from 0 to 1, that this effect will occur?
+ ///
+ [DataField("probability")]
+ public float Probability = 1.0f;
+ ///
+ /// What effect the disease will have.
+ ///
+ public abstract void Effect(DiseaseEffectArgs args);
+ }
+ ///
+ /// What you have to work with in any disease effect/cure.
+ /// Includes an entity manager because it is out of scope
+ /// otherwise.
+ ///
+ public readonly record struct DiseaseEffectArgs(
+ EntityUid DiseasedEntity,
+ DiseasePrototype Disease,
+ IEntityManager EntityManager
+ );
+}
diff --git a/Content.Shared/Disease/DiseaseMachineVisuals.cs b/Content.Shared/Disease/DiseaseMachineVisuals.cs
new file mode 100644
index 0000000000..4254a436b9
--- /dev/null
+++ b/Content.Shared/Disease/DiseaseMachineVisuals.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Disease
+{
+ [Serializable, NetSerializable]
+ ///
+ /// Stores bools for if the machine is on
+ /// and if it's currently running.
+ /// Used for the visualizer
+ ///
+ public enum DiseaseMachineVisuals : byte
+ {
+ IsOn,
+ IsRunning
+ }
+}
diff --git a/Content.Shared/Disease/DiseasePrototype.cs b/Content.Shared/Disease/DiseasePrototype.cs
new file mode 100644
index 0000000000..d158045fa6
--- /dev/null
+++ b/Content.Shared/Disease/DiseasePrototype.cs
@@ -0,0 +1,68 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+///
+/// Diseases encompass everything from viruses to cancers to heart disease.
+/// It's not just a virology thing.
+///
+namespace Content.Shared.Disease
+{
+ [Prototype("disease")]
+ [DataDefinition]
+ public sealed class DiseasePrototype : IPrototype, IInheritingPrototype
+ {
+ [ViewVariables]
+ [DataField("id", required: true)]
+ public string ID { get; } = default!;
+
+ [DataField("name")]
+ public string Name { get; } = string.Empty;
+
+ [DataField("parent", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string? Parent { get; private set; }
+
+ [NeverPushInheritance]
+ [DataField("abstract")]
+ public bool Abstract { get; private set; }
+
+ ///
+ /// Controls how often a disease ticks.
+ ///
+ public float TickTime = 1f;
+
+ ///
+ /// Since disease isn't mapped to metabolism or anything,
+ /// it needs something to control its tickrate
+ ///
+ public float Accumulator = 0f;
+ ///
+ /// List of effects the disease has that will
+ /// run every second (by default anyway)
+ ///
+ [DataField("effects", serverOnly: true)]
+ public readonly List Effects = new(0);
+ ///
+ /// List of SPECIFIC CURES the disease has that will
+ /// be checked every second.
+ /// Stuff like spaceacillin operates outside this.
+ ///
+ [DataField("cures", serverOnly: true)]
+ public readonly List Cures = new(0);
+ ///
+ /// This flatly reduces the probabilty disease medicine
+ /// has to cure it every tick. Although, since spaceacillin is
+ /// used as a reference and it has 0.15 chance, this is
+ /// a base 33% reduction in cure chance
+ ///
+ [DataField("cureResist", serverOnly: true)]
+ public float CureResist = 0.05f;
+ ///
+ /// Whether the disease can infect other people.
+ /// Since this isn't just a virology thing, this
+ /// primary determines what sort of disease it is.
+ /// This also affects things like the vaccine machine.
+ /// You can't print a cancer vaccine
+ ///
+ [DataField("infectious", serverOnly: true)]
+ public bool Infectious = true;
+ }
+}
diff --git a/Content.Shared/Disease/DiseasedComponent.cs b/Content.Shared/Disease/DiseasedComponent.cs
new file mode 100644
index 0000000000..6ad89a790c
--- /dev/null
+++ b/Content.Shared/Disease/DiseasedComponent.cs
@@ -0,0 +1,12 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Disease.Components
+{
+ [NetworkedComponent]
+ [RegisterComponent]
+ /// This is added to anyone with at least 1 disease
+ /// and helps cull event subscriptions and entity queries
+ /// when they are not relevant.
+ public sealed class DiseasedComponent : Component
+ {}
+}
diff --git a/Resources/Audio/Machines/diagnoser_printing.ogg b/Resources/Audio/Machines/diagnoser_printing.ogg
new file mode 100644
index 0000000000..51d8863d91
Binary files /dev/null and b/Resources/Audio/Machines/diagnoser_printing.ogg differ
diff --git a/Resources/Audio/Machines/license.txt b/Resources/Audio/Machines/license.txt
new file mode 100644
index 0000000000..b81583c745
--- /dev/null
+++ b/Resources/Audio/Machines/license.txt
@@ -0,0 +1,3 @@
+diagnoser_printing.ogg taken from https://freesound.org/people/RobSp1derp1g/sounds/615419/ and edited
+
+vaccinator_running.ogg taken from https://freesound.org/people/RutgerMuller/sounds/365413/ and edited
diff --git a/Resources/Audio/Machines/vaccinator_running.ogg b/Resources/Audio/Machines/vaccinator_running.ogg
new file mode 100644
index 0000000000..604d385b8e
Binary files /dev/null and b/Resources/Audio/Machines/vaccinator_running.ogg differ
diff --git a/Resources/Locale/en-US/disease/diagnoser.ftl b/Resources/Locale/en-US/disease/diagnoser.ftl
new file mode 100644
index 0000000000..2b451af84a
--- /dev/null
+++ b/Resources/Locale/en-US/disease/diagnoser.ftl
@@ -0,0 +1,20 @@
+diagnoser-cant-use-swab = {CAPITALIZE(THE($machine))} rejects {THE($swab)}.
+diagnoser-insert-swab = You insert {THE($swab)} into {THE($machine)}.
+diagnoser-disease-report = Disease Report: {CAPITALIZE($disease)}
+diagnoser-disease-report-none = Bill of Good Health
+diagnoser-disease-report-none-contents = [color=green]No diseases were found in this sample.[/color]
+diagnoser-disease-report-name = Disease Name: {CAPITALIZE($disease)}
+diagnoser-disease-report-infectious = Infectious: [color=red]Yes[/color]
+diagnoser-disease-report-not-infectious = Infectious: [color=green]No[/color]
+diagnoser-disease-report-cureresist-none = Medication Resistance: [color=green]None[/color]
+diagnoser-disease-report-cureresist-low = Medication Resistance: [color=yellow]Low[/color]
+diagnoser-disease-report-cureresist-medium = Medication Resistance: [color=orange]Medium[/color]
+diagnoser-disease-report-cureresist-high = Medication Resistance: [color=red]High[/color]
+diagnoser-cure-none = The disease has no specific cures.
+diagnoser-cure-has = The disease has the following cures:
+diagnoser-cure-bedrest = Rest in bed for {$time} seconds.
+diagnoser-cure-reagent = Consume at least {$units}u of {$reagent}.
+diagnoser-cure-wait = It will go away on its own after {$time} seconds.
+diagnoser-cure-temp = Reach a body temperature below {$max}°K or above {$min}°K.
+diagnoser-cure-temp-min = Reach a body temperature above {$min}°K.
+diagnoser-cure-temp-max = Reach a body temperature below {$max}°K.
diff --git a/Resources/Locale/en-US/disease/disease.ftl b/Resources/Locale/en-US/disease/disease.ftl
new file mode 100644
index 0000000000..9a973a57a8
--- /dev/null
+++ b/Resources/Locale/en-US/disease/disease.ftl
@@ -0,0 +1,10 @@
+disease-cured = You feel a bit better.
+disease-sick-generic = You feel sick.
+disease-sneeze = {CAPITALIZE($person)} sneezes.
+disease-cough = {CAPITALIZE($person)} coughs.
+disease-screech = {CAPITALIZE($person)} screeches.
+disease-meow = {CAPITALIZE($person)} meows.
+disease-beep= {CAPITALIZE($person)} beeps.
+disease-eaten-inside = You feel like you're being eaten from the inside.
+disease-steal-compulsion = You want to steal things.
+disease-beat-chest-compulsion = {CAPITALIZE(THE($person))} beats {POSS-ADJ($person)} chest.
diff --git a/Resources/Locale/en-US/disease/scanner.ftl b/Resources/Locale/en-US/disease/scanner.ftl
new file mode 100644
index 0000000000..476ac86cf8
--- /dev/null
+++ b/Resources/Locale/en-US/disease/scanner.ftl
@@ -0,0 +1,4 @@
+disease-scanner-diseased = DISEASED
+disease-scanner-not-diseased = No diseases
+disease-scanner-gave-other = You gave {THE($target)} {CAPITALIZE($disease)}!
+disease-scanner-gave-self = You gave yourself {CAPITALIZE($disease)}! Congratulations!
diff --git a/Resources/Locale/en-US/disease/swab.ftl b/Resources/Locale/en-US/disease/swab.ftl
new file mode 100644
index 0000000000..92648a21ea
--- /dev/null
+++ b/Resources/Locale/en-US/disease/swab.ftl
@@ -0,0 +1,5 @@
+swab-already-used = You already used this swab.
+swab-swabbed = You swab {THE($target)}'s mouth.
+swab-mask-blocked = {CAPITALIZE(THE($target))} needs to take off {THE($mask)}.
+swab-used = It looks like it's been used.
+swab-unused = It's clean and ready to use.
diff --git a/Resources/Locale/en-US/disease/vaccine.ftl b/Resources/Locale/en-US/disease/vaccine.ftl
new file mode 100644
index 0000000000..867499a7ce
--- /dev/null
+++ b/Resources/Locale/en-US/disease/vaccine.ftl
@@ -0,0 +1,3 @@
+vaxx-already-used = You already used this vaccine.
+vaxx-used = It's spent.
+vaxx-unused = It hasn't been spent.
diff --git a/Resources/Locale/en-US/station-events/events/disease-outbreak.ftl b/Resources/Locale/en-US/station-events/events/disease-outbreak.ftl
new file mode 100644
index 0000000000..488cf0d0c8
--- /dev/null
+++ b/Resources/Locale/en-US/station-events/events/disease-outbreak.ftl
@@ -0,0 +1 @@
+station-event-disease-outbreak-announcement = Ship systems have detected that some crewmates have been infected with a disease.
diff --git a/Resources/Prototypes/Catalog/Fills/Boxes/medical.yml b/Resources/Prototypes/Catalog/Fills/Boxes/medical.yml
index 044071b499..5ed4deb0b1 100644
--- a/Resources/Prototypes/Catalog/Fills/Boxes/medical.yml
+++ b/Resources/Prototypes/Catalog/Fills/Boxes/medical.yml
@@ -57,7 +57,17 @@
layers:
- state: box
- state: latex
-
+
+- type: entity
+ name: mouth swab box
+ parent: BoxCardboard
+ id: BoxMouthSwab
+ components:
+ - type: StorageFill
+ contents:
+ - id: DiseaseSwab
+ amount: 30
+
- type: entity
name: body bag box
parent: BoxCardboard
diff --git a/Resources/Prototypes/Catalog/Fills/Crates/medical.yml b/Resources/Prototypes/Catalog/Fills/Crates/medical.yml
index 240e58d097..d59b411adc 100644
--- a/Resources/Prototypes/Catalog/Fills/Crates/medical.yml
+++ b/Resources/Prototypes/Catalog/Fills/Crates/medical.yml
@@ -19,6 +19,12 @@
amount: 2
- id: Gauze
amount: 2
+ - id: BoxLatex
+ amount: 1
+ - id: BoxSterile
+ amount: 1
+ - id: BoxMouthSwab
+ amount: 1
- type: entity
id: CrateChemistrySupplies
diff --git a/Resources/Prototypes/Catalog/Fills/Items/firstaidkits.yml b/Resources/Prototypes/Catalog/Fills/Items/firstaidkits.yml
index 6c1d7cf9b2..71f7c4b8ce 100644
--- a/Resources/Prototypes/Catalog/Fills/Items/firstaidkits.yml
+++ b/Resources/Prototypes/Catalog/Fills/Items/firstaidkits.yml
@@ -50,6 +50,8 @@
components:
- type: StorageFill
contents:
+ - id: SyringeSpaceacillin
+ amount: 1
- id: PillDylovene
amount: 3
diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
index f614c2d007..d832b66e31 100644
--- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
+++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
@@ -129,7 +129,7 @@
- type: StorageFill
contents:
- id: MedkitFilled
- - id: ClothingHandsGlovesLatex
+ - id: ClothingHandsGlovesNitrile
#- name: ClothingEyesHudMedical #Removed until working properly
# prob: 1
- id: ClothingHeadsetAltMedical
@@ -139,6 +139,9 @@
- id: ClothingMaskSterile
- id: ClothingHeadHelmetHardsuitMedical
- id: ClothingOuterHardsuitMedical
+ - id: DiagnoserMachineCircuitboard
+ - id: VaccinatorMachineCircuitboard
+ prob: 0.25
- id: Hypospray
- id: HandheldCrewMonitor
- id: DoorRemoteMedical
diff --git a/Resources/Prototypes/Catalog/Research/technologies.yml b/Resources/Prototypes/Catalog/Research/technologies.yml
index 71e68d0cfb..d0f1ab78d3 100644
--- a/Resources/Prototypes/Catalog/Research/technologies.yml
+++ b/Resources/Prototypes/Catalog/Research/technologies.yml
@@ -116,6 +116,8 @@
- ChemMasterMachineCircuitboard
- ChemDispenserMachineCircuitboard
- CrewMonitoringComputerCircuitboard
+ - VaccinatorMachineCircuitboard
+ - DiagnoserMachineCircuitboard
- HandheldCrewMonitor
# Security Technology Tree
diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml
index 66b84f1491..45752b0f16 100644
--- a/Resources/Prototypes/Catalog/uplink_catalog.yml
+++ b/Resources/Prototypes/Catalog/uplink_catalog.yml
@@ -284,3 +284,11 @@
category: Misc
itemId: ClothingBackpackDuffelSyndicatePyjamaBundle
price: 4
+
+- type: uplinkListing
+ id: UplinkGigacancerScanner
+ category: Misc
+ itemId: HandheldHealthAnalyzerGigacancer
+ listingName: Ultragigacancer Health Analyzer
+ description: Works like a normal health analyzer, other than giving everyone it scans ultragigacancer.
+ price: 5
diff --git a/Resources/Prototypes/Damage/modifier_sets.yml b/Resources/Prototypes/Damage/modifier_sets.yml
index 83e4cfc4f9..50e6d50b5c 100644
--- a/Resources/Prototypes/Damage/modifier_sets.yml
+++ b/Resources/Prototypes/Damage/modifier_sets.yml
@@ -52,7 +52,7 @@
Shock: 0
flatReductions:
Blunt: 5
-
+
- type: damageModifierSet
id: RGlass
coefficients:
@@ -83,6 +83,15 @@
Cold: 1.5
Poison: 0.8
+- type: damageModifierSet
+ id: Scale # Skin tougher, bones weaker, strong stomachs, cold-blooded (kindof)
+ coefficients:
+ Blunt: 1.1
+ Slash: 0.9
+ Cold: 1.5
+ Heat: 0.9
+ Poison: 0.9
+
# Represents which damage types should be modified
# in relation to how they cause bloodloss damage.
- type: damageModifierSet
diff --git a/Resources/Prototypes/Diseases/infectious.yml b/Resources/Prototypes/Diseases/infectious.yml
new file mode 100644
index 0000000000..78e02884da
--- /dev/null
+++ b/Resources/Prototypes/Diseases/infectious.yml
@@ -0,0 +1,138 @@
+- type: disease
+ id: SpaceCold
+ name: space cold
+ cureResist: 0
+ effects:
+ - !type:DiseaseAdjustReagent
+ probability: 0.2
+ reagent: Histamine
+ amount: 0.5
+ - !type:DiseasePopUp
+ probability: 0.025
+ - !type:DiseaseSnough
+ probability: 0.025
+ cures:
+ - !type:DiseaseBedrestCure
+ maxLength: 20
+ - !type:DiseaseJustWaitCure
+ maxLength: 400
+ - !type:DiseaseReagentCure
+ reagent: Ultravasculine
+### - !type:DiseaseReagentCure ### In Loving Memory, Lean
+### reagent: Lean ### 2022/03/12 - 2022/03/13
+
+- type: disease
+ id: VentCough
+ name: vent cough
+ effects:
+ - !type:DiseasePopUp
+ probability: 0.025
+ message: burning-insides
+ - !type:DiseaseSnough
+ probability: 0.025
+ snoughMessage: disease-cough
+ - !type:DiseaseHealthChange
+ probability: 0.015
+ damage:
+ groups:
+ Caustic: 1
+ cures:
+ - !type:DiseaseBedrestCure
+ maxLength: 30
+ - !type:DiseaseJustWaitCure
+ maxLength: 600
+ - !type:DiseaseReagentCure
+ reagent: SpaceCleaner
+
+- type: disease
+ id: VanAusdallsRobovirus
+ name: Van Ausdall's Robovirus
+ cureResist: 0.1
+ effects:
+ - !type:DiseaseAdjustReagent
+ probability: 0.025
+ reagent: Licoxide
+ amount: 0.5
+ - !type:DiseaseSnough
+ probability: 0.02
+ snoughMessage: disease-beep
+ cures:
+ - !type:DiseaseJustWaitCure
+ maxLength: 900
+ - !type:DiseaseReagentCure
+ reagent: BeepskySmash
+
+- type: disease
+ id: AMIV
+ name: AMIV
+ cureResist: 0.10
+ effects:
+ - !type:DiseasePopUp
+ probability: 0.015
+ type: Pvs
+ message: disease-beat-chest-compulsion
+ - !type:DiseasePopUp
+ probability: 0.03
+ message: disease-steal-compulsion
+ - !type:DiseaseSnough
+ probability: 0.02
+ snoughMessage: disease-screech
+ - !type:DiseaseGenericStatusEffect
+ probability: 0.3
+ key: Stutter
+ component: MonkeyAccent
+ - !type:DiseaseHealthChange
+ probability: 0.53
+ damage:
+ types:
+ Asphyxiation: 1
+ cures:
+ - !type:DiseaseJustWaitCure
+ maxLength: 1600
+ - !type:DiseaseReagentCure
+ reagent: BananaHonk
+
+- type: disease
+ id: BleedersBite
+ name: Bleeder's Bite
+ effects:
+ - !type:DiseaseAdjustReagent
+ reagent: TranexamicAcid
+ amount: -2.5
+ - !type:DiseaseHealthChange
+ probability: 0.015
+ damage:
+ types:
+ Piercing: 20
+ - !type:DiseasePopUp
+ probability: 0.05
+ message: disease-eaten-inside
+ cures:
+ - !type:DiseaseJustWaitCure
+ maxLength: 900
+ - !type:DiseaseBodyTemperatureCure
+ min: 360
+ - !type:DiseaseReagentCure
+ reagent: DemonsBlood
+
+- type: disease
+ id: OwOnavirus
+ name: OwOnavirus
+ cureResist: 0.25
+ effects:
+ - !type:DiseaseGenericStatusEffect
+ key: Stutter
+ component: OwOAccent
+ - !type:DiseaseAdjustReagent ## 20 / 0.013 / 60 is around 25 minutes before overdose (0.5u metabolize each tick)
+ probability: 0.513
+ reagent: Ephedrine
+ amount: 1
+ - !type:DiseaseSnough
+ probability: 0.02
+ snoughMessage: disease-meow
+ cures:
+ - !type:DiseaseBodyTemperatureCure
+ min: 420 ## Reachable with a flamer
+ - !type:DiseaseReagentCure
+ reagent: Theobromine
+ amount: 10
diff --git a/Resources/Prototypes/Diseases/noninfectious.yml b/Resources/Prototypes/Diseases/noninfectious.yml
new file mode 100644
index 0000000000..d3a2a173e8
--- /dev/null
+++ b/Resources/Prototypes/Diseases/noninfectious.yml
@@ -0,0 +1,18 @@
+- type: disease
+ id: Ultragigacancer
+ name: ultragigacancer
+ infectious: false
+ cureResist: 0.15
+ effects:
+ - !type:DiseaseHealthChange
+ probability: 0.5
+ damage:
+ types:
+ Cellular: 1
+ - !type:DiseasePopUp
+ probability: 0.03
+ cures:
+ - !type:DiseaseReagentCure
+ reagent: Phalanximine
+ min: 15
+### Once radiation is refactored I want it to have a small chance of giving you regular cancer
diff --git a/Resources/Prototypes/Entities/Clothing/Hands/base_clothinghands.yml b/Resources/Prototypes/Entities/Clothing/Hands/base_clothinghands.yml
index 99548af470..c549933051 100644
--- a/Resources/Prototypes/Entities/Clothing/Hands/base_clothinghands.yml
+++ b/Resources/Prototypes/Entities/Clothing/Hands/base_clothinghands.yml
@@ -7,3 +7,5 @@
state: icon
- type: Clothing
Slots: [gloves]
+ - type: DiseaseProtection
+ protection: 0.05
diff --git a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml
index 34a891dd06..2e512d0101 100644
--- a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml
+++ b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml
@@ -71,18 +71,33 @@
sprite: Clothing/Hands/Gloves/ihscombat.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/ihscombat.rsi
-
+#### Medical
- type: entity
parent: ClothingHandsBase
id: ClothingHandsGlovesLatex
name: latex gloves
- description: Thin sterile latex gloves.
+ description: Thin sterile latex gloves. Basic PPE for any doctor.
components:
- type: Sprite
sprite: Clothing/Hands/Gloves/latex.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/latex.rsi
+ - type: DiseaseProtection
+ protection: 0.1
+- type: entity
+ parent: ClothingHandsBase
+ id: ClothingHandsGlovesNitrile
+ name: nitrile gloves
+ description: High-quality nitrile gloves. Expensive medical PPE.
+ components:
+ - type: Sprite
+ sprite: Clothing/Hands/Gloves/Color/blue.rsi
+ - type: Clothing
+ sprite: Clothing/Hands/Gloves/Color/blue.rsi
+ - type: DiseaseProtection
+ protection: 0.15
+####
- type: entity
parent: ClothingHandsBase
id: ClothingHandsGlovesLeather
diff --git a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
index aac9e93bad..7a2573715d 100644
--- a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
+++ b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
@@ -48,6 +48,8 @@
- type: Tag
tags:
- HidesHair
+ - type: DiseaseProtection
+ protection: 0.05
- type: entity
abstract: true
diff --git a/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml b/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml
index 192e944fbc..5e26fb01bd 100644
--- a/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml
+++ b/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml
@@ -83,6 +83,8 @@
- type: PressureProtection
highPressureMultiplier: 0.80
lowPressureMultiplier: 55
+ - type: DiseaseProtection
+ protection: 0.15
- type: entity
parent: ClothingHeadHardsuitWithLightBase
diff --git a/Resources/Prototypes/Entities/Clothing/Head/hoods.yml b/Resources/Prototypes/Entities/Clothing/Head/hoods.yml
index 800755af89..d022adc6ba 100644
--- a/Resources/Prototypes/Entities/Clothing/Head/hoods.yml
+++ b/Resources/Prototypes/Entities/Clothing/Head/hoods.yml
@@ -9,9 +9,14 @@
sprite: Clothing/Head/Hoods/Bio/general.rsi
- type: Clothing
sprite: Clothing/Head/Hoods/Bio/general.rsi
+ - type: DiseaseProtection
+ protection: 0.15
+ - type: Tag
+ tags:
+ - HidesHair
- type: entity
- parent: ClothingHeadBase
+ parent: ClothingHeadHatHoodBioGeneral
id: ClothingHeadHatHoodBioCmo
name: bio hood
suffix: CMO
@@ -21,9 +26,11 @@
sprite: Clothing/Head/Hoods/Bio/cmo.rsi
- type: Clothing
sprite: Clothing/Head/Hoods/Bio/cmo.rsi
+ - type: DiseaseProtection
+ protection: 0.25
- type: entity
- parent: ClothingHeadBase
+ parent: ClothingHeadHatHoodBioGeneral
id: ClothingHeadHatHoodBioJanitor
name: bio hood
suffix: Janitor
@@ -34,8 +41,9 @@
- type: Clothing
sprite: Clothing/Head/Hoods/Bio/janitor.rsi
+
- type: entity
- parent: ClothingHeadBase
+ parent: ClothingHeadHatHoodBioGeneral
id: ClothingHeadHatHoodBioScientist
name: bio hood
suffix: Science
@@ -47,7 +55,7 @@
sprite: Clothing/Head/Hoods/Bio/scientist.rsi
- type: entity
- parent: ClothingHeadBase
+ parent: ClothingHeadHatHoodBioGeneral
id: ClothingHeadHatHoodBioSecurity
name: bio hood
suffix: Security
@@ -59,7 +67,7 @@
sprite: Clothing/Head/Hoods/Bio/security.rsi
- type: entity
- parent: ClothingHeadBase
+ parent: ClothingHeadHatHoodBioGeneral
id: ClothingHeadHatHoodBioVirology
name: bio hood
suffix: Virology
@@ -69,6 +77,8 @@
sprite: Clothing/Head/Hoods/Bio/virology.rsi
- type: Clothing
sprite: Clothing/Head/Hoods/Bio/virology.rsi
+ - type: DiseaseProtection
+ protection: 0.25
- type: entity
parent: ClothingHeadBase
diff --git a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml
index a932f5d072..ded3f0dea6 100644
--- a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml
+++ b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml
@@ -10,6 +10,8 @@
sprite: Clothing/Mask/gas.rsi
- type: BreathMask
- type: IngestionBlocker
+ - type: DiseaseProtection
+ protection: 0.05
- type: entity
parent: ClothingMaskBase
@@ -23,6 +25,8 @@
sprite: Clothing/Mask/breath.rsi
- type: BreathMask
- type: IngestionBlocker
+ - type: DiseaseProtection
+ protection: 0.05
- type: entity
parent: ClothingMaskBase
@@ -72,6 +76,8 @@
sprite: Clothing/Mask/sterile.rsi
- type: BreathMask
- type: IngestionBlocker
+ - type: DiseaseProtection
+ protection: 0.1
- type: entity
parent: ClothingMaskBase
diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml
index b76401693d..3638d7242f 100644
--- a/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml
+++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml
@@ -30,3 +30,4 @@
Piercing: 0.95
Heat: 0.90
Radiation: 0.25
+ - type: DiseaseProtection
diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/bio.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/bio.yml
index 082f02a872..1d39959dbc 100644
--- a/Resources/Prototypes/Entities/Clothing/OuterClothing/bio.yml
+++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/bio.yml
@@ -9,6 +9,8 @@
sprite: Clothing/OuterClothing/Bio/general.rsi
- type: Clothing
sprite: Clothing/OuterClothing/Bio/general.rsi
+ - type: DiseaseProtection
+ protection: 0.2
- type: entity
parent: ClothingOuterBase
@@ -21,6 +23,8 @@
sprite: Clothing/OuterClothing/Bio/cmo.rsi
- type: Clothing
sprite: Clothing/OuterClothing/Bio/cmo.rsi
+ - type: DiseaseProtection
+ protection: 0.3
- type: entity
parent: ClothingOuterBase
@@ -33,6 +37,8 @@
sprite: Clothing/OuterClothing/Bio/janitor.rsi
- type: Clothing
sprite: Clothing/OuterClothing/Bio/janitor.rsi
+ - type: DiseaseProtection
+ protection: 0.2
- type: entity
parent: ClothingOuterBase
@@ -45,6 +51,8 @@
sprite: Clothing/OuterClothing/Bio/scientist.rsi
- type: Clothing
sprite: Clothing/OuterClothing/Bio/scientist.rsi
+ - type: DiseaseProtection
+ protection: 0.2
- type: entity
parent: ClothingOuterBase
@@ -57,6 +65,8 @@
sprite: Clothing/OuterClothing/Bio/security.rsi
- type: Clothing
sprite: Clothing/OuterClothing/Bio/security.rsi
+ - type: DiseaseProtection
+ protection: 0.2
- type: entity
parent: ClothingOuterBase
@@ -69,3 +79,5 @@
sprite: Clothing/OuterClothing/Bio/virology.rsi
- type: Clothing
sprite: Clothing/OuterClothing/Bio/virology.rsi
+ - type: DiseaseProtection
+ protection: 0.3
diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml
index fd064fe293..95b30f26f1 100644
--- a/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml
+++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml
@@ -180,6 +180,8 @@
- type: PressureProtection
highPressureMultiplier: 0.75
lowPressureMultiplier: 100
+ - type: DiseaseProtection
+ protection: 0.2
- type: entity
parent: ClothingOuterHardsuitBase
diff --git a/Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml b/Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml
index 82c32cf1f4..b55b877127 100644
--- a/Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml
+++ b/Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml
@@ -89,6 +89,8 @@
sprite: Clothing/Uniforms/Jumpskirt/cmo.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpskirt/cmo.rsi
+ - type: DiseaseProtection
+ protection: 0.15
- type: entity
parent: ClothingUniformSkirtBase
@@ -199,6 +201,8 @@
sprite: Clothing/Uniforms/Jumpskirt/medical.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpskirt/medical.rsi
+ - type: DiseaseProtection
+ protection: 0.1
- type: entity
parent: ClothingUniformSkirtBase
@@ -221,6 +225,8 @@
sprite: Clothing/Uniforms/Jumpskirt/paramedic.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpskirt/paramedic.rsi
+ - type: DiseaseProtection
+ protection: 0.1
- type: entity
parent: ClothingUniformSkirtBase
diff --git a/Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml b/Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml
index a4d3de2a5b..f781ef4af0 100644
--- a/Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml
+++ b/Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml
@@ -161,6 +161,8 @@
sprite: Clothing/Uniforms/Jumpsuit/cmo.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpsuit/cmo.rsi
+ - type: DiseaseProtection
+ protection: 0.15
- type: entity
parent: ClothingUniformBase
@@ -293,6 +295,8 @@
sprite: Clothing/Uniforms/Jumpsuit/medical.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpsuit/medical.rsi
+ - type: DiseaseProtection
+ protection: 0.1
- type: entity
parent: ClothingUniformBase
@@ -315,6 +319,8 @@
sprite: Clothing/Uniforms/Jumpsuit/paramedic.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpsuit/paramedic.rsi
+ - type: DiseaseProtection
+ protection: 0.1
- type: entity
parent: ClothingUniformBase
@@ -618,7 +624,7 @@
sprite: Clothing/Uniforms/Jumpsuit/librarian.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpsuit/librarian.rsi
-
+
- type: entity
parent: ClothingUniformBase
id: ClothingUniformJumpsuitLawyerRed
@@ -629,7 +635,7 @@
sprite: Clothing/Uniforms/Jumpsuit/lawyerred.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpsuit/lawyerred.rsi
-
+
- type: entity
parent: ClothingUniformBase
id: ClothingUniformJumpsuitLawyerBlue
@@ -640,7 +646,7 @@
sprite: Clothing/Uniforms/Jumpsuit/lawyerblue.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpsuit/lawyerblue.rsi
-
+
- type: entity
parent: ClothingUniformBase
id: ClothingUniformJumpsuitLawyerBlack
@@ -651,7 +657,7 @@
sprite: Clothing/Uniforms/Jumpsuit/lawyerblack.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpsuit/lawyerblack.rsi
-
+
- type: entity
parent: ClothingUniformBase
id: ClothingUniformJumpsuitLawyerPurple
@@ -662,7 +668,7 @@
sprite: Clothing/Uniforms/Jumpsuit/lawyerpurple.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpsuit/lawyerpurple.rsi
-
+
- type: entity
parent: ClothingUniformBase
id: ClothingUniformJumpsuitPyjamaSyndicateBlack
diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/artifacts.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/artifacts.yml
index 6d277e0625..f57703c744 100644
--- a/Resources/Prototypes/Entities/Markers/Spawners/Random/artifacts.yml
+++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/artifacts.yml
@@ -18,6 +18,7 @@
- ColdArtifact
- RadiateArtifact
- GasArtifact
+ - DiseaseArtifact
chance: 1
- type: entity
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
index ffdf54aca9..921db4eed4 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
@@ -675,7 +675,7 @@
tags:
- Trash
- type: Recyclable
- - type: Actions
+ - type: Actions
# TODO: Remove CombatMode when Prototype Composition is added
- type: CombatMode
combatToggleAction:
@@ -686,6 +686,7 @@
autoPopulate: false
- type: Bloodstream
bloodMaxVolume: 50
+ - type: DiseaseCarrier #The other class lab animal and disease vector
- type: entity
@@ -707,7 +708,8 @@
crit: dead-1
dead: splat-1
- type: Bloodstream
- bloodMaxVolume: 50
+ bloodMaxVolume: 50
+ - type: DiseaseCarrier #Why doesn't this save if it's only on the parent wtf
- type: entity
@@ -730,6 +732,7 @@
dead: splat-2
- type: Bloodstream
bloodMaxVolume: 50
+ - type: DiseaseCarrier
- type: entity
@@ -775,6 +778,9 @@
interactFailureString: petting-failure-generic
- type: Bloodstream
bloodMaxVolume: 50
+ - type: Damageable
+ damageContainer: Biological
+ damageModifierSet: Scale
- type: entity
name: frog
@@ -947,6 +953,9 @@
interactFailureString: petting-failure-generic
- type: Bloodstream
bloodMaxVolume: 50
+ - type: Damageable
+ damageContainer: Biological
+ damageModifierSet: Scale
# Code unique spider prototypes or combine them all into one spider and get a
# random sprite state when you spawn it.
@@ -1162,7 +1171,7 @@
gender: epicene
- type: Bloodstream
bloodMaxVolume: 100
-
+
- type: entity
name: Renault
parent: MobFox
diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml
index e57268da98..fbb1c9e82d 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/human.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml
@@ -71,6 +71,7 @@
- SlowedDown
- Stutter
- Electrocution
+ - type: DiseaseCarrier
# Other
- type: Inventory
- type: Clickable
@@ -294,6 +295,7 @@
proper: true
- type: StandingState
+
- type: entity
save: false
name: Urist McHands
diff --git a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml
index 184164561f..4742db107d 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml
@@ -110,6 +110,25 @@
template: HumanoidTemplate
preset: HumanPreset
- type: LizardAccent
+ - type: DiseaseCarrier
+ diseaseResist: 0.1
+ - type: Damageable
+ damageContainer: Biological
+ damageModifierSet: Scale
+ - type: Temperature
+ heatDamageThreshold: 400
+ coldDamageThreshold: 285
+ currentTemperature: 310.15
+ specificHeat: 46
+ coldDamage:
+ types:
+ Cold : 1.1 #per second, scales with temperature & other constants
+ heatDamage:
+ types:
+ Heat : 0.9 #per second, scales with temperature & other constants
+ - type: MovementSpeedModifier
+ baseWalkSpeed : 2.7
+ baseSprintSpeed : 4.5
- type: entity
save: false
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
index 0d1818ec2d..e73b5aeb14 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
@@ -32,7 +32,7 @@
- type: entity
id: CircuitImprinterMachineCircuitboard
parent: BaseMachineCircuitboard
- name: Circuit Imprinter (Machine Board)
+ name: circuit imprinter machine board
components:
- type: MachineBoard
prototype: CircuitImprinter
@@ -48,7 +48,7 @@
- type: entity
id: UniformPrinterMachineCircuitboard
parent: BaseMachineCircuitboard
- name: Uniform Printer (Machine Board)
+ name: uniform printer machine board
components:
- type: MachineBoard
prototype: UniformPrinter
@@ -57,10 +57,41 @@
Manipulator: 1
Laser: 1
+- type: entity
+ id: VaccinatorMachineCircuitboard
+ parent: BaseMachineCircuitboard
+ name: vaccinator machine board
+ components:
+ - type: MachineBoard
+ prototype: Vaccinator
+ requirements:
+ MatterBin: 1
+ Manipulator: 1
+ materialRequirements:
+ Cable: 5
+ tagRequirements:
+ GlassBeaker:
+ Amount: 1
+ DefaultPrototype: Beaker
+ ExamineName: Glass Beaker
+
+- type: entity
+ id: DiagnoserMachineCircuitboard
+ parent: BaseMachineCircuitboard
+ name: diagnoser machine board
+ components:
+ - type: MachineBoard
+ prototype: DiseaseDiagnoser
+ requirements:
+ Manipulator: 1
+ Laser: 2
+ materialRequirements:
+ Cable: 5
+
- type: entity
id: ThermomachineFreezerMachineCircuitBoard
parent: BaseMachineCircuitboard
- name: Freezer Thermomachine (Machine Board)
+ name: freezer thermomachine machine board
description: Looks like you could use a screwdriver to change the board type.
components:
- type: MachineBoard
@@ -77,7 +108,7 @@
- type: entity
id: ThermomachineHeaterMachineCircuitBoard
parent: BaseMachineCircuitboard
- name: Heater Thermomachine (Machine Board)
+ name: heather thermomachine machine board
description: Looks like you could use a screwdriver to change the board type.
components:
- type: MachineBoard
@@ -208,7 +239,7 @@
- type: entity
parent: BaseMachineCircuitboard
id: DawInstrumentMachineCircuitboard
- name: Digital Audio Workstation (Machine Board)
+ name: digital audio workstation machine board
components:
- type: MachineBoard
prototype: DawInstrument
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/disease.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/disease.yml
new file mode 100644
index 0000000000..f69ca7a911
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/disease.yml
@@ -0,0 +1,30 @@
+- type: entity
+ parent: BaseItem
+ id: DiseaseSwab
+ name: mouth swab
+ description: Used to take saliva samples to test for diseases.
+ components:
+ - type: Item
+ size: 1
+ - type: Sprite
+ netsync: false
+ sprite: Objects/Specific/Medical/mouth_swab.rsi
+ state: icon
+ - type: Tag
+ tags:
+ - Recyclable
+ - type: DiseaseSwab
+
+- type: entity
+ parent: BaseItem
+ id: Vaccine
+ name: Vaccine
+ description: There's no way you don't already have an opinion on these.
+ components:
+ - type: Item
+ size: 3
+ - type: Sprite
+ sprite: Objects/Specific/Medical/medipen.rsi
+ netsync: false
+ state: salpen
+ - type: DiseaseVaccine
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml
index be16bda046..aaaabae819 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml
@@ -237,3 +237,16 @@
reagents:
- ReagentId: TranexamicAcid
Quantity: 15
+
+- type: entity
+ name: spaceacillin syringe
+ parent: Syringe
+ id: SyringeSpaceacillin
+ components:
+ - type: SolutionContainerManager
+ solutions:
+ injector:
+ maxVol: 15
+ reagents:
+ - ReagentId: Spaceacillin
+ Quantity: 15
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml
index 4427deef97..4a09a05415 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml
@@ -15,3 +15,23 @@
- key: enum.HealthAnalyzerUiKey.Key
type: HealthAnalyzerBoundUserInterface
- type: HealthAnalyzer
+
+- type: entity
+ parent: HandheldHealthAnalyzer
+ id: HandheldHealthAnalyzerGigacancer
+ suffix: gigacancer
+ components:
+ - type: HealthAnalyzer
+ fake: true
+ disease: Ultragigacancer
+
+## I know admins will want this
+- type: entity
+ parent: HandheldHealthAnalyzer
+ id: HandheldHealthAnalyzerOwOnavirus
+ name: OwOnavirus analyzer
+ suffix: admin abuse
+ components:
+ - type: HealthAnalyzer
+ fake: true
+ disease: OwOnavirus
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Xenoarchaeology/artifacts.yml b/Resources/Prototypes/Entities/Objects/Specific/Xenoarchaeology/artifacts.yml
index 288cfb861b..ea3c8569e4 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Xenoarchaeology/artifacts.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Xenoarchaeology/artifacts.yml
@@ -167,3 +167,10 @@
suffix: Gas
components:
- type: GasArtifact
+
+- type: entity
+ parent: BaseXenoArtifact
+ id: DiseaseArtifact
+ suffix: Disease
+ components:
+ - type: DiseaseArtifact
diff --git a/Resources/Prototypes/Entities/Structures/Machines/disease_diagnoser.yml b/Resources/Prototypes/Entities/Structures/Machines/disease_diagnoser.yml
new file mode 100644
index 0000000000..7a6f0a9bc5
--- /dev/null
+++ b/Resources/Prototypes/Entities/Structures/Machines/disease_diagnoser.yml
@@ -0,0 +1,24 @@
+- type: entity
+ id: DiseaseDiagnoser
+ parent: BaseMachinePowered
+ name: Disease Diagnoser Delta Extreme
+ description: A machine that analyzes disease samples.
+ placement:
+ mode: SnapgridCenter
+ components:
+ - type: Sprite
+ sprite: Structures/Machines/diagnoser.rsi
+ layers:
+ - state: icon
+ map: ["enum.DiseaseMachineVisualLayers.IsRunning"]
+ - state: unlit
+ shader: unshaded
+ map: ["enum.DiseaseMachineVisualLayers.IsOn"]
+ netsync: false
+ - type: DiseaseDiagnoser
+ - type: DiseaseMachine
+ machineOutput: Paper
+ - type: Appearance
+ - type: DiseaseMachineVisuals
+ idleState: icon
+ runningState: running
diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
index 2d24719701..c6699cb970 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
@@ -229,6 +229,9 @@
- ThermomachineFreezerMachineCircuitBoard
- CloningPodMachineCircuitboard
- MedicalScannerMachineCircuitboard
+ - CrewMonitoringComputerCircuitboard
+ - VaccinatorMachineCircuitboard
+ - DiagnoserMachineCircuitboard
- ChemMasterMachineCircuitboard
- ChemDispenserMachineCircuitboard
- HydroponicsTrayMachineCircuitboard
@@ -237,7 +240,6 @@
- ProtolatheMachineCircuitboard
- ReagentGrinderMachineCircuitboard
- UniformPrinterMachineCircuitboard
- - CrewMonitoringComputerCircuitboard
- ShuttleConsoleCircuitboard
- CircuitImprinterMachineCircuitboard
- DawInstrumentMachineCircuitboard
diff --git a/Resources/Prototypes/Entities/Structures/Machines/vaccinator.yml b/Resources/Prototypes/Entities/Structures/Machines/vaccinator.yml
new file mode 100644
index 0000000000..e01f792370
--- /dev/null
+++ b/Resources/Prototypes/Entities/Structures/Machines/vaccinator.yml
@@ -0,0 +1,26 @@
+- type: entity
+ id: Vaccinator
+ parent: BaseMachinePowered
+ name: Vaccinator
+ description: A machine that creates vaccines.
+ placement:
+ mode: SnapgridCenter
+ components:
+ - type: Sprite
+ sprite: Structures/Machines/vaccinator.rsi
+ layers:
+ - state: icon
+ map: ["enum.DiseaseMachineVisualLayers.IsRunning"]
+ - state: unlit
+ shader: unshaded
+ map: ["enum.DiseaseMachineVisualLayers.IsOn"]
+ netsync: false
+ - type: DiseaseVaccineCreator
+ - type: DiseaseMachine
+ machineOutput: Vaccine
+ - type: Appearance
+ - type: DiseaseMachineVisuals
+ idleState: icon
+ runningState: running
+ - type: Machine
+ board: VaccinatorMachineCircuitboard
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Tanks/tanks.yml b/Resources/Prototypes/Entities/Structures/Storage/Tanks/tanks.yml
index c3eceef59b..694f0c90e3 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Tanks/tanks.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Tanks/tanks.yml
@@ -86,4 +86,4 @@
# It is pressurized...
- type: ReagentTank
transferAmount: 100
-
+
diff --git a/Resources/Prototypes/Reagents/medicine.yml b/Resources/Prototypes/Reagents/medicine.yml
index 05f35eb685..f117282f8b 100644
--- a/Resources/Prototypes/Reagents/medicine.yml
+++ b/Resources/Prototypes/Reagents/medicine.yml
@@ -1,3 +1,11 @@
+- type: reagent
+ id: Cryptobiolin
+ name: cryptobiolin
+ group: Medicine
+ desc: Causes confusion and dizziness. This is essential to make Spaceacillin.
+ physicalDesc: fizzy
+ color: "#081a80"
+
- type: reagent
id: Dylovene
name: dylovene
@@ -308,8 +316,7 @@
- !type:HealthChange
damage:
types:
- # close enough to what it says
- Poison: -1
+ Cellular: -1
Radiation: 1
- type: reagent
@@ -340,6 +347,18 @@
groups:
Caustic: -5
+- type: reagent
+ id: Spaceacillin
+ name: spaceacillin
+ group: Medicine
+ desc: A theta-lactam antibiotic. A common and very useful medicine, effective against many diseases likely to be encountered in space. Slows progression of diseases.
+ physicalDesc: opaque
+ color: "#9942f5"
+ metabolisms:
+ Medicine:
+ effects:
+ - !type:ChemCureDisease
+
- type: reagent
id: Stellibinin
name: stellibinin
diff --git a/Resources/Prototypes/Reagents/toxins.yml b/Resources/Prototypes/Reagents/toxins.yml
index 4537e0af9f..6108fdc3fa 100644
--- a/Resources/Prototypes/Reagents/toxins.yml
+++ b/Resources/Prototypes/Reagents/toxins.yml
@@ -269,3 +269,20 @@
damage:
types:
Poison: 6
+
+- type: reagent
+ id: VentCrud
+ name: vent crud
+ desc: A jet black substance found in poorly maintained ventilation systems.
+ physicalDesc: sticky
+ color: "#000000"
+ metabolisms:
+ Poison:
+ effects:
+ - !type:HealthChange
+ damage:
+ types:
+ Poison: 2
+ - !type:ChemCauseDisease ##Since this mostly just comes from the event you won't ingest that much
+ causeChance: 0.6
+ disease: VentCough
diff --git a/Resources/Prototypes/Recipes/Lathes/electronics.yml b/Resources/Prototypes/Recipes/Lathes/electronics.yml
index 271abfa5e4..2e984d5bb0 100644
--- a/Resources/Prototypes/Recipes/Lathes/electronics.yml
+++ b/Resources/Prototypes/Recipes/Lathes/electronics.yml
@@ -163,6 +163,27 @@
Steel: 100
Glass: 900
+- type: latheRecipe
+ id: VaccinatorMachineCircuitboard
+ icon: Objects/Misc/module.rsi/id_mod.png
+ result: VaccinatorMachineCircuitboard
+ completetime: 100
+ materials:
+ Steel: 100
+ Glass: 900
+ Gold: 100
+
+- type: latheRecipe
+ id: DiagnoserMachineCircuitboard
+ icon: Objects/Misc/module.rsi/id_mod.png
+ result: DiagnoserMachineCircuitboard
+ completetime: 100
+ materials:
+ Steel: 100
+ Glass: 900
+ Gold: 100
+
+
- type: latheRecipe
id: ReagentGrinderMachineCircuitboard
icon: Objects/Misc/module.rsi/id_mod.png
diff --git a/Resources/Prototypes/Recipes/Reactions/medicine.yml b/Resources/Prototypes/Recipes/Reactions/medicine.yml
index 8ccab03e08..57e0ebc4f9 100644
--- a/Resources/Prototypes/Recipes/Reactions/medicine.yml
+++ b/Resources/Prototypes/Recipes/Reactions/medicine.yml
@@ -10,6 +10,18 @@
products:
Dylovene: 3
+- type: reaction
+ id: Cryptobiolin
+ reactants:
+ Potassium:
+ amount: 1
+ Oxygen:
+ amount: 1
+ Glucose:
+ amount: 1
+ products:
+ Cryptobiolin: 3
+
- type: reaction
id: Arithrazine
reactants:
@@ -288,3 +300,13 @@
amount: 1
products:
Siderlac: 2
+
+- type: reaction
+ id: Spaceacillin
+ reactants:
+ Cryptobiolin:
+ amount: 1
+ Inaprovaline:
+ amount: 1
+ products:
+ Spaceacillin: 2
diff --git a/Resources/Textures/Objects/Specific/Medical/mouth_swab.rsi/icon.png b/Resources/Textures/Objects/Specific/Medical/mouth_swab.rsi/icon.png
new file mode 100644
index 0000000000..aa10c2e9fe
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/mouth_swab.rsi/icon.png differ
diff --git a/Resources/Textures/Objects/Specific/Medical/mouth_swab.rsi/meta.json b/Resources/Textures/Objects/Specific/Medical/mouth_swab.rsi/meta.json
new file mode 100644
index 0000000000..437c1de1de
--- /dev/null
+++ b/Resources/Textures/Objects/Specific/Medical/mouth_swab.rsi/meta.json
@@ -0,0 +1,15 @@
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY",
+ "copyright": "Created by Willhudson#4576 (Discord user id: 935437363180613672) in the SS14 Discord",
+ "states": [
+ {
+ "name": "icon",
+ "directions": 1
+ }
+ ]
+}
diff --git a/Resources/Textures/Structures/Machines/diagnoser.rsi/icon.png b/Resources/Textures/Structures/Machines/diagnoser.rsi/icon.png
new file mode 100644
index 0000000000..4ae4891b9f
Binary files /dev/null and b/Resources/Textures/Structures/Machines/diagnoser.rsi/icon.png differ
diff --git a/Resources/Textures/Structures/Machines/diagnoser.rsi/meta.json b/Resources/Textures/Structures/Machines/diagnoser.rsi/meta.json
new file mode 100644
index 0000000000..094df25baf
--- /dev/null
+++ b/Resources/Textures/Structures/Machines/diagnoser.rsi/meta.json
@@ -0,0 +1,52 @@
+{
+ "version": 1,
+ "license": "CC-BY",
+ "copyright": "Created by Willhudson#4576 (Discord user id: 935437363180613672) in the SS14 Discord",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "unlit"
+ },
+ {
+ "name": "running",
+ "delays": [
+ [
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785,
+ 0.1785
+ ]
+ ]
+ }
+ ]
+}
diff --git a/Resources/Textures/Structures/Machines/diagnoser.rsi/running.png b/Resources/Textures/Structures/Machines/diagnoser.rsi/running.png
new file mode 100644
index 0000000000..101d5765d5
Binary files /dev/null and b/Resources/Textures/Structures/Machines/diagnoser.rsi/running.png differ
diff --git a/Resources/Textures/Structures/Machines/diagnoser.rsi/unlit.png b/Resources/Textures/Structures/Machines/diagnoser.rsi/unlit.png
new file mode 100644
index 0000000000..dcb7bf96ce
Binary files /dev/null and b/Resources/Textures/Structures/Machines/diagnoser.rsi/unlit.png differ
diff --git a/Resources/Textures/Structures/Machines/vaccinator.rsi/icon.png b/Resources/Textures/Structures/Machines/vaccinator.rsi/icon.png
new file mode 100644
index 0000000000..bb10cbf7bf
Binary files /dev/null and b/Resources/Textures/Structures/Machines/vaccinator.rsi/icon.png differ
diff --git a/Resources/Textures/Structures/Machines/vaccinator.rsi/meta.json b/Resources/Textures/Structures/Machines/vaccinator.rsi/meta.json
new file mode 100644
index 0000000000..82f9d0928a
--- /dev/null
+++ b/Resources/Textures/Structures/Machines/vaccinator.rsi/meta.json
@@ -0,0 +1,33 @@
+{
+ "version": 1,
+ "license": "CC-BY",
+ "copyright": "Created by Willhudson#4576 (Discord user id: 935437363180613672) in the SS14 Discord",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "unlit"
+ },
+ {
+ "name": "running",
+ "delays": [
+ [
+ 0.5555,
+ 0.5555,
+ 0.5555,
+ 0.5555,
+ 0.5555,
+ 0.5555,
+ 0.5555,
+ 0.5555,
+ 0.5555
+ ]
+ ]
+ }
+ ]
+}
diff --git a/Resources/Textures/Structures/Machines/vaccinator.rsi/running.png b/Resources/Textures/Structures/Machines/vaccinator.rsi/running.png
new file mode 100644
index 0000000000..79845fd350
Binary files /dev/null and b/Resources/Textures/Structures/Machines/vaccinator.rsi/running.png differ
diff --git a/Resources/Textures/Structures/Machines/vaccinator.rsi/unlit.png b/Resources/Textures/Structures/Machines/vaccinator.rsi/unlit.png
new file mode 100644
index 0000000000..65199b874c
Binary files /dev/null and b/Resources/Textures/Structures/Machines/vaccinator.rsi/unlit.png differ