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