From b3448ab0d23ad84d228a0b033c2013a2f48cb378 Mon Sep 17 00:00:00 2001 From: Zack Backmen Date: Fri, 22 Mar 2024 12:43:13 +0300 Subject: [PATCH 1/5] Disease --- .../Components/DiseaseDiagnoserComponent.cs | 7 + .../Components/DiseaseMachineComponent.cs | 36 ++ .../Components/DiseaseProtectionComponent.cs | 23 + .../Components/DiseaseSwabComponent.cs | 26 + .../Components/DiseaseVaccineComponent.cs | 27 + .../DiseaseVaccineCreatorComponent.cs | 38 ++ .../Disease/Cures/DiseaseBedrestCure.cs | 46 ++ .../Cures/DiseaseBodyTemperatureCure.cs | 37 ++ .../Disease/Cures/DiseaseCureSystem.cs | 40 ++ .../Disease/Cures/DiseaseJustWaitCure.cs | 36 ++ .../Disease/Cures/DiseaseReagentCure.cs | 50 ++ .../Backmen/Disease/DiseaseDiagnosisSystem.cs | 350 ++++++++++++ .../Backmen/Disease/DiseaseSystem.cs | 501 ++++++++++++++++++ .../Disease/Effects/DiseaseAddComponent.cs | 42 ++ .../Disease/Effects/DiseaseAdjustReagent.cs | 51 ++ .../Effects/DiseaseGenericStatusEffect.cs | 73 +++ .../Disease/Effects/DiseaseHealthChange.cs | 26 + .../Disease/Effects/DiseasePolymorph.cs | 48 ++ .../Backmen/Disease/Effects/DiseasePopUp.cs | 40 ++ .../Backmen/Disease/Effects/DiseaseSnough.cs | 34 ++ .../Backmen/Disease/Effects/DiseaseVomit.cs | 27 + .../Backmen/Disease/Effects/EffectSystem.cs | 58 ++ .../Disease/Server/DiseaseServerComponent.cs | 13 + .../Backmen/Disease/VaccineSystem.cs | 316 +++++++++++ .../Disease/DiseaseCarrierComponent.cs | 59 +++ Content.Shared/Backmen/Disease/DiseaseCure.cs | 34 ++ .../Backmen/Disease/DiseaseEffect.cs | 41 ++ .../Disease/DiseaseMachineRunningComponent.cs | 13 + .../Backmen/Disease/DiseaseMachineVisuals.cs | 15 + .../Backmen/Disease/DiseasePrototype.cs | 82 +++ .../Disease/DiseaseVaccineCreatorMessages.cs | 75 +++ .../Backmen/Disease/DiseasedComponent.cs | 13 + .../Disease/Effects/SharedEffectSystem.cs | 6 + .../Disease/Events/AttemptSneezeCoughEvent.cs | 6 + .../Disease/Events/VaccineDoAfterEvent.cs | 9 + .../Backmen/Disease/Swab/SwabEvents.cs | 14 + 36 files changed, 2312 insertions(+) create mode 100644 Content.Server/Backmen/Disease/Components/DiseaseDiagnoserComponent.cs create mode 100644 Content.Server/Backmen/Disease/Components/DiseaseMachineComponent.cs create mode 100644 Content.Server/Backmen/Disease/Components/DiseaseProtectionComponent.cs create mode 100644 Content.Server/Backmen/Disease/Components/DiseaseSwabComponent.cs create mode 100644 Content.Server/Backmen/Disease/Components/DiseaseVaccineComponent.cs create mode 100644 Content.Server/Backmen/Disease/Components/DiseaseVaccineCreatorComponent.cs create mode 100644 Content.Server/Backmen/Disease/Cures/DiseaseBedrestCure.cs create mode 100644 Content.Server/Backmen/Disease/Cures/DiseaseBodyTemperatureCure.cs create mode 100644 Content.Server/Backmen/Disease/Cures/DiseaseCureSystem.cs create mode 100644 Content.Server/Backmen/Disease/Cures/DiseaseJustWaitCure.cs create mode 100644 Content.Server/Backmen/Disease/Cures/DiseaseReagentCure.cs create mode 100644 Content.Server/Backmen/Disease/DiseaseDiagnosisSystem.cs create mode 100644 Content.Server/Backmen/Disease/DiseaseSystem.cs create mode 100644 Content.Server/Backmen/Disease/Effects/DiseaseAddComponent.cs create mode 100644 Content.Server/Backmen/Disease/Effects/DiseaseAdjustReagent.cs create mode 100644 Content.Server/Backmen/Disease/Effects/DiseaseGenericStatusEffect.cs create mode 100644 Content.Server/Backmen/Disease/Effects/DiseaseHealthChange.cs create mode 100644 Content.Server/Backmen/Disease/Effects/DiseasePolymorph.cs create mode 100644 Content.Server/Backmen/Disease/Effects/DiseasePopUp.cs create mode 100644 Content.Server/Backmen/Disease/Effects/DiseaseSnough.cs create mode 100644 Content.Server/Backmen/Disease/Effects/DiseaseVomit.cs create mode 100644 Content.Server/Backmen/Disease/Effects/EffectSystem.cs create mode 100644 Content.Server/Backmen/Disease/Server/DiseaseServerComponent.cs create mode 100644 Content.Server/Backmen/Disease/VaccineSystem.cs create mode 100644 Content.Shared/Backmen/Disease/DiseaseCarrierComponent.cs create mode 100644 Content.Shared/Backmen/Disease/DiseaseCure.cs create mode 100644 Content.Shared/Backmen/Disease/DiseaseEffect.cs create mode 100644 Content.Shared/Backmen/Disease/DiseaseMachineRunningComponent.cs create mode 100644 Content.Shared/Backmen/Disease/DiseaseMachineVisuals.cs create mode 100644 Content.Shared/Backmen/Disease/DiseasePrototype.cs create mode 100644 Content.Shared/Backmen/Disease/DiseaseVaccineCreatorMessages.cs create mode 100644 Content.Shared/Backmen/Disease/DiseasedComponent.cs create mode 100644 Content.Shared/Backmen/Disease/Effects/SharedEffectSystem.cs create mode 100644 Content.Shared/Backmen/Disease/Events/AttemptSneezeCoughEvent.cs create mode 100644 Content.Shared/Backmen/Disease/Events/VaccineDoAfterEvent.cs create mode 100644 Content.Shared/Backmen/Disease/Swab/SwabEvents.cs diff --git a/Content.Server/Backmen/Disease/Components/DiseaseDiagnoserComponent.cs b/Content.Server/Backmen/Disease/Components/DiseaseDiagnoserComponent.cs new file mode 100644 index 00000000000..4b438b0da61 --- /dev/null +++ b/Content.Server/Backmen/Disease/Components/DiseaseDiagnoserComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Backmen.Disease.Components; + +[RegisterComponent] +public sealed partial class DiseaseDiagnoserComponent : Component +{ + +} diff --git a/Content.Server/Backmen/Disease/Components/DiseaseMachineComponent.cs b/Content.Server/Backmen/Disease/Components/DiseaseMachineComponent.cs new file mode 100644 index 00000000000..9a140aae11c --- /dev/null +++ b/Content.Server/Backmen/Disease/Components/DiseaseMachineComponent.cs @@ -0,0 +1,36 @@ +using Content.Shared.Backmen.Disease; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Backmen.Disease.Components; + +/// +/// For shared behavior between both disease machines +/// +[RegisterComponent] +public sealed partial class DiseaseMachineComponent : Component +{ + [DataField("delay")] + public float Delay = 5f; + /// + /// How much time we've accumulated processing + /// + [DataField("accumulator")] + public float Accumulator = 0f; + + /// + /// Prototypes queued. + /// + public int Queued = 0; + + /// + /// 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/Backmen/Disease/Components/DiseaseProtectionComponent.cs b/Content.Server/Backmen/Disease/Components/DiseaseProtectionComponent.cs new file mode 100644 index 00000000000..4fecc286dfa --- /dev/null +++ b/Content.Server/Backmen/Disease/Components/DiseaseProtectionComponent.cs @@ -0,0 +1,23 @@ +namespace Content.Server.Backmen.Disease.Components; + +/// +/// Value added to clothing to give its wearer +/// protection against infection from diseases +/// +[RegisterComponent] +public sealed partial 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/Backmen/Disease/Components/DiseaseSwabComponent.cs b/Content.Server/Backmen/Disease/Components/DiseaseSwabComponent.cs new file mode 100644 index 00000000000..9591fde8e7d --- /dev/null +++ b/Content.Server/Backmen/Disease/Components/DiseaseSwabComponent.cs @@ -0,0 +1,26 @@ +using Content.Shared.Backmen.Disease; + +namespace Content.Server.Backmen.Disease.Components; + +/// +/// For mouth swabs used to collect and process +/// disease samples. +/// +[RegisterComponent] +public sealed partial class DiseaseSwabComponent : Component +{ + /// + /// How long it takes to swab someone. + /// + [DataField("swabDelay")] + public float SwabDelay = 2f; + /// + /// If this swab has been used + /// + public bool Used = false; + /// + /// The disease prototype currently on the swab + /// + [ViewVariables] + public DiseasePrototype? Disease; +} diff --git a/Content.Server/Backmen/Disease/Components/DiseaseVaccineComponent.cs b/Content.Server/Backmen/Disease/Components/DiseaseVaccineComponent.cs new file mode 100644 index 00000000000..046d775fea7 --- /dev/null +++ b/Content.Server/Backmen/Disease/Components/DiseaseVaccineComponent.cs @@ -0,0 +1,27 @@ +using Content.Shared.Backmen.Disease; + +namespace Content.Server.Backmen.Disease.Components; + +/// +/// For disease vaccines +/// + +[RegisterComponent] +public sealed partial class DiseaseVaccineComponent : Component +{ + /// + /// How long it takes to inject someone + /// + [DataField("injectDelay")] + public float InjectDelay = 2f; + /// + /// If this vaccine has been used + /// + public bool Used = false; + + /// + /// The disease prototype currently on the vaccine + /// + [ViewVariables(VVAccess.ReadWrite)] + public DiseasePrototype? Disease; +} diff --git a/Content.Server/Backmen/Disease/Components/DiseaseVaccineCreatorComponent.cs b/Content.Server/Backmen/Disease/Components/DiseaseVaccineCreatorComponent.cs new file mode 100644 index 00000000000..1c43855c61d --- /dev/null +++ b/Content.Server/Backmen/Disease/Components/DiseaseVaccineCreatorComponent.cs @@ -0,0 +1,38 @@ +using Content.Server.Backmen.Disease.Server; +using Content.Shared.Construction.Prototypes; +using Robust.Shared.Audio; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Backmen.Disease.Components; + +[RegisterComponent] +public sealed partial class DiseaseVaccineCreatorComponent : Component +{ + public DiseaseServerComponent? DiseaseServer = null; + + /// + /// Biomass cost per vaccine, scaled off of the machine part. (So T1 parts effectively reduce the default to 4.) + /// Reduced by the part rating. + /// + [DataField("BaseBiomassCost")] + public int BaseBiomassCost = 5; + + /// + /// Current biomass cost, derived from the above. + /// + public int BiomassCost = 4; + + /// + /// The machine part that reduces biomass cost. + /// + [DataField("machinePartCost", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartCost = "Manipulator"; + + /// + /// Current vaccines queued. + /// + public int Queued = 0; + + [DataField("runningSound")] + public SoundSpecifier RunningSoundPath = new SoundPathSpecifier("/Audio/Machines/vaccinator_running.ogg"); +} diff --git a/Content.Server/Backmen/Disease/Cures/DiseaseBedrestCure.cs b/Content.Server/Backmen/Disease/Cures/DiseaseBedrestCure.cs new file mode 100644 index 00000000000..90e0c4da47e --- /dev/null +++ b/Content.Server/Backmen/Disease/Cures/DiseaseBedrestCure.cs @@ -0,0 +1,46 @@ +using Content.Server.Bed.Components; +using Content.Shared.Backmen.Disease; +using Content.Shared.Bed.Sleep; +using Content.Shared.Buckle.Components; + +namespace Content.Server.Backmen.Disease.Cures; + +public sealed partial class DiseaseBedrestCure : DiseaseCure +{ + [ViewVariables(VVAccess.ReadWrite)] + public int Ticker = 0; + + /// How many extra ticks you get for sleeping. + [DataField("sleepMultiplier")] + public int SleepMultiplier = 3; + + [DataField("maxLength", required: true)] + [ViewVariables(VVAccess.ReadWrite)] + public int MaxLength = 60; + + public override string CureText() + { + return (Loc.GetString("diagnoser-cure-bedrest", ("time", MaxLength), ("sleep", (MaxLength / SleepMultiplier)))); + } +} + +public sealed partial class DiseaseCureSystem +{ + private void DiseaseBedrestCure(DiseaseCureArgs args, DiseaseBedrestCure ds) + { + if (!TryComp(args.DiseasedEntity, out var buckle) || + !HasComp(buckle.BuckledTo)) + return; + + var ticks = 1; + if (HasComp(args.DiseasedEntity)) + ticks *= ds.SleepMultiplier; + + if (buckle.Buckled) + ds.Ticker += ticks; + if (ds.Ticker >= ds.MaxLength) + { + _disease.CureDisease(args.DiseasedEntity, args.Disease); + } + } +} diff --git a/Content.Server/Backmen/Disease/Cures/DiseaseBodyTemperatureCure.cs b/Content.Server/Backmen/Disease/Cures/DiseaseBodyTemperatureCure.cs new file mode 100644 index 00000000000..402fe41149b --- /dev/null +++ b/Content.Server/Backmen/Disease/Cures/DiseaseBodyTemperatureCure.cs @@ -0,0 +1,37 @@ +using Content.Server.Temperature.Components; +using Content.Shared.Backmen.Disease; + +namespace Content.Server.Backmen.Disease.Cures; + +public sealed partial class DiseaseBodyTemperatureCure : DiseaseCure +{ + [DataField("min")] + public float Min = 0; + + [DataField("max")] + public float Max = 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))); + } +} + +public sealed partial class DiseaseCureSystem +{ + private void DiseaseBodyTemperatureCure(DiseaseCureArgs args, DiseaseBodyTemperatureCure ds) + { + if (!TryComp(args.DiseasedEntity, out var temp)) + return; + + if(temp.CurrentTemperature > ds.Min && temp.CurrentTemperature < float.MaxValue) + { + _disease.CureDisease(args.DiseasedEntity, args.Disease); + } + } +} diff --git a/Content.Server/Backmen/Disease/Cures/DiseaseCureSystem.cs b/Content.Server/Backmen/Disease/Cures/DiseaseCureSystem.cs new file mode 100644 index 00000000000..19d382733bd --- /dev/null +++ b/Content.Server/Backmen/Disease/Cures/DiseaseCureSystem.cs @@ -0,0 +1,40 @@ +using Content.Shared.Backmen.Disease; + +namespace Content.Server.Backmen.Disease.Cures; + +public sealed partial class DiseaseCureSystem : EntitySystem +{ + [Dependency] private readonly DiseaseSystem _disease = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(DoGenericCure); + } + + private void DoGenericCure(DiseaseCureArgs args) + { + if(args.Handled) + return; + + switch (args.DiseaseCure) + { + case DiseaseBedrestCure ds1: + args.Handled = true; + DiseaseBedrestCure(args, ds1); + return; + case DiseaseBodyTemperatureCure ds2: + args.Handled = true; + DiseaseBodyTemperatureCure(args, ds2); + return; + case DiseaseJustWaitCure ds3: + args.Handled = true; + DiseaseJustWaitCure(args, ds3); + return; + case DiseaseReagentCure ds4: + args.Handled = true; + DiseaseReagentCure(args, ds4); + return; + } + } +} diff --git a/Content.Server/Backmen/Disease/Cures/DiseaseJustWaitCure.cs b/Content.Server/Backmen/Disease/Cures/DiseaseJustWaitCure.cs new file mode 100644 index 00000000000..332add09432 --- /dev/null +++ b/Content.Server/Backmen/Disease/Cures/DiseaseJustWaitCure.cs @@ -0,0 +1,36 @@ +using Content.Shared.Backmen.Disease; + +namespace Content.Server.Backmen.Disease.Cures; + +/// +/// Automatically removes the disease after a +/// certain amount of time. +/// +public sealed partial 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 string CureText() + { + return Loc.GetString("diagnoser-cure-wait", ("time", MaxLength)); + } +} + +public sealed partial class DiseaseCureSystem +{ + private void DiseaseJustWaitCure(DiseaseCureArgs args, DiseaseJustWaitCure ds) + { + ds.Ticker++; + if (ds.Ticker >= ds.MaxLength) + { + _disease.CureDisease(args.DiseasedEntity, args.Disease); + } + } +} diff --git a/Content.Server/Backmen/Disease/Cures/DiseaseReagentCure.cs b/Content.Server/Backmen/Disease/Cures/DiseaseReagentCure.cs new file mode 100644 index 00000000000..a3b45078a4a --- /dev/null +++ b/Content.Server/Backmen/Disease/Cures/DiseaseReagentCure.cs @@ -0,0 +1,50 @@ +using Content.Server.Body.Components; +using Content.Shared.Backmen.Disease; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Server.Backmen.Disease.Cures; + +/// +/// Cures the disease if a certain amount of reagent +/// is in the host's chemstream. +/// +public sealed partial class DiseaseReagentCure : DiseaseCure +{ + [DataField("min")] + public FixedPoint2 Min = 5; + [DataField("reagent")] + public ReagentId? Reagent; + + public override string CureText() + { + var prototypeMan = IoCManager.Resolve(); + if (Reagent == null || !prototypeMan.TryIndex(Reagent.Value.Prototype, out var reagentProt)) + return string.Empty; + return (Loc.GetString("diagnoser-cure-reagent", ("units", Min), ("reagent", reagentProt.LocalizedName))); + } +} + +public sealed partial class DiseaseCureSystem +{ + private void DiseaseReagentCure(DiseaseCureArgs args, DiseaseReagentCure ds) + { + if (!TryComp(args.DiseasedEntity, out var bloodstream) + || bloodstream.ChemicalSolution == null) + return; + + var chemicalSolution = bloodstream.ChemicalSolution.Value; + + var quant = FixedPoint2.Zero; + if (ds.Reagent != null && chemicalSolution.Comp.Solution.ContainsReagent(ds.Reagent.Value)) + { + quant = chemicalSolution.Comp.Solution.GetReagentQuantity(ds.Reagent.Value); + } + + if (quant >= ds.Min) + { + _disease.CureDisease(args.DiseasedEntity, args.Disease); + } + } +} diff --git a/Content.Server/Backmen/Disease/DiseaseDiagnosisSystem.cs b/Content.Server/Backmen/Disease/DiseaseDiagnosisSystem.cs new file mode 100644 index 00000000000..58a715e2c0f --- /dev/null +++ b/Content.Server/Backmen/Disease/DiseaseDiagnosisSystem.cs @@ -0,0 +1,350 @@ +using System.Linq; +using Content.Server.Backmen.Disease.Components; +using Content.Server.Backmen.Disease.Server; +using Content.Server.Nutrition.Components; +using Content.Server.Paper; +using Content.Server.Popups; +using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; +using Content.Server.Station.Systems; +using Content.Shared.Backmen.Disease; +using Content.Shared.Backmen.Disease.Swab; +using Content.Shared.DoAfter; +using Content.Shared.Examine; +using Content.Shared.Hands.Components; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Inventory; +using Content.Shared.Tools.Components; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Backmen.Disease; + +public sealed class DiseaseDiagnosisSystem : EntitySystem +{ + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly PaperSystem _paperSystem = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedAudioSystem _sharedSoundSystem = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnAfterInteractUsing); + SubscribeLocalEvent(OnAfterInteractUsingVaccine); + // Visuals + SubscribeLocalEvent(OnPowerChanged); + // Private Events + SubscribeLocalEvent(OnDiagnoserFinished); + SubscribeLocalEvent(OnSwabDoAfter); + } + + /// + /// This handles running disease machines + /// to handle their delay and visuals. + /// + public override void Update(float frameTime) + { + var q = EntityQueryEnumerator(); + while (q.MoveNext(out var owner, out _, out var diseaseMachine)) + { + diseaseMachine.Accumulator += frameTime; + + while (diseaseMachine.Accumulator >= diseaseMachine.Delay) + { + diseaseMachine.Accumulator -= diseaseMachine.Delay; + var ev = new DiseaseMachineFinishedEvent(diseaseMachine, true); + RaiseLocalEvent(owner, ev); + RemCompDeferred(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, ref AfterInteractEvent args) + { + if (args.Target == null || !args.CanReach || !HasComp(args.Target)) + return; + + if (swab.Used) + { + _popupSystem.PopupEntity(Loc.GetString("swab-already-used"), args.User, args.User); + return; + } + + if (_inventorySystem.TryGetSlotEntity(args.Target.Value, "mask", out var maskUid) && + TryComp(maskUid, out var blocker) && + blocker.Enabled) + { + _popupSystem.PopupEntity(Loc.GetString("swab-mask-blocked", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("mask", maskUid)), args.User, args.User); + return; + } + + _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, swab.SwabDelay, new DiseaseSwabDoAfterEvent(), uid, target: args.Target, used: uid) + { + BreakOnMove = 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 (HasComp(uid) || !this.IsPowered(uid, EntityManager)) + 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, args.User); + return; + } + _popupSystem.PopupEntity(Loc.GetString("machine-insert-item", ("machine", uid), ("item", args.Used), ("user", args.User)), uid, args.User); + + + machine.Disease = swab.Disease; + QueueDel(args.Used); + + EnsureComp(uid); + UpdateAppearance(uid, true, true); + _sharedSoundSystem.PlayPvs("/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 (HasComp(uid) || !this.IsPowered(uid, EntityManager)) + 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, args.User); + return; + } + _popupSystem.PopupEntity(Loc.GetString("machine-insert-item", ("machine", uid), ("item", args.Used), ("user", args.User)), uid, args.User); + var machine = Comp(uid); + machine.Disease = swab.Disease; + EntityManager.DeleteEntity(args.Used); + + EnsureComp(uid); + UpdateAppearance(uid, true, true); + _sharedSoundSystem.PlayPvs("/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, ref ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + if (swab.Used) + args.PushMarkup(Loc.GetString("swab-used")); + else + args.PushMarkup(Loc.GetString("swab-unused")); + } + + /// + /// 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(); + var diseaseName = Loc.GetString(disease.Name); + report.AddMarkup(Loc.GetString("diagnoser-disease-report-name", ("disease", diseaseName))); + 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; + } + + public bool ServerHasDisease(Entity server, DiseasePrototype disease) + { + return server.Comp.Diseases.Any(serverDisease => serverDisease.ID == disease.ID); + } + + /// + /// Appearance stuff + /// + + /// + /// Appearance helper function to + /// set the component's power and running states. + /// + public void UpdateAppearance(EntityUid uid, bool isOn, bool isRunning) + { + if (!TryComp(uid, out var appearance)) + return; + + _appearance.SetData(uid, DiseaseMachineVisuals.IsOn, isOn, appearance); + _appearance.SetData(uid, DiseaseMachineVisuals.IsRunning, isRunning, appearance); + } + /// + /// Makes sure the machine is visually off/on. + /// + private void OnPowerChanged(EntityUid uid, DiseaseMachineComponent component, ref PowerChangedEvent args) + { + UpdateAppearance(uid, args.Powered, false); + } + + /// + /// Copies a disease prototype to the swab + /// after the doafter completes. + /// + private void OnSwabDoAfter(EntityUid uid, DiseaseSwabComponent component, DoAfterEvent args) + { + if (args.Handled || args.Cancelled || !TryComp(args.Args.Target, out var carrier) || !TryComp(args.Args.Used, out var swab)) + return; + + swab.Used = true; + _popupSystem.PopupEntity(Loc.GetString("swab-swabbed", ("target", Identity.Entity(args.Args.Target.Value, EntityManager))), args.Args.Target.Value, args.Args.User); + + if (swab.Disease != null || carrier.Diseases.Count == 0) + return; + + swab.Disease = _random.Pick(carrier.Diseases); + } + + /// + /// Prints a diagnostic report with its findings. + /// Also cancels the animation. + /// + private void OnDiagnoserFinished(EntityUid uid, DiseaseDiagnoserComponent component, DiseaseMachineFinishedEvent args) + { + var isPowered = this.IsPowered(uid, EntityManager); + UpdateAppearance(uid, isPowered, false); + // spawn a piece of paper. + var printed = Spawn(args.Machine.MachineOutput, Transform(uid).Coordinates); + + if (!TryComp(printed, out var paper)) + return; + + string reportTitle; + FormattedMessage contents = new(); + if (args.Machine.Disease != null) + { + var diseaseName = Loc.GetString(args.Machine.Disease.Name); + reportTitle = Loc.GetString("diagnoser-disease-report", ("disease", diseaseName)); + contents = AssembleDiseaseReport(args.Machine.Disease); + + var known = false; + + var q = EntityQueryEnumerator(); + while (q.MoveNext(out var owner, out var server)) + { + if (_stationSystem.GetOwningStation(owner) != _stationSystem.GetOwningStation(uid)) + continue; + + if (ServerHasDisease((owner, server), args.Machine.Disease)) + { + known = true; + } + else + { + server.Diseases.Add(args.Machine.Disease); + } + } + + if (!known) + { + Spawn(ResearchDisk5000, Transform(uid).Coordinates); + } + } + else + { + reportTitle = Loc.GetString("diagnoser-disease-report-none"); + contents.AddMarkup(Loc.GetString("diagnoser-disease-report-none-contents")); + } + _metaData.SetEntityName(printed,reportTitle); + + _paperSystem.SetContent(printed, contents.ToMarkup(), paper); + } + + [ValidatePrototypeId] + private const string ResearchDisk5000 = "ResearchDisk5000"; +} diff --git a/Content.Server/Backmen/Disease/DiseaseSystem.cs b/Content.Server/Backmen/Disease/DiseaseSystem.cs new file mode 100644 index 00000000000..74347d0ee55 --- /dev/null +++ b/Content.Server/Backmen/Disease/DiseaseSystem.cs @@ -0,0 +1,501 @@ +using Content.Server.Backmen.Disease.Components; +using Content.Server.Body.Systems; +using Content.Server.Chat.Systems; +using Content.Server.Nutrition.Components; +using Content.Server.Popups; +using Content.Shared.Backmen.Disease; +using Content.Shared.Backmen.Disease.Events; +using Content.Shared.Clothing.Components; +using Content.Shared.DoAfter; +using Content.Shared.Examine; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Rejuvenate; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Threading; +using Robust.Shared.Utility; + +namespace Content.Server.Backmen.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 SharedDoAfterSystem _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!; + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly ChatSystem _chatSystem = default!; + [Dependency] private readonly IParallelManager _parallel = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnTryCureDisease); + SubscribeLocalEvent(OnRejuvenate); + SubscribeLocalEvent(OnContactInteraction); + SubscribeLocalEvent(OnEntitySpeak); + SubscribeLocalEvent(OnEquipped); + SubscribeLocalEvent(OnUnequipped); + // Handling stuff from other systems + SubscribeLocalEvent(OnApplyMetabolicMultiplier); + + _carrierQuery = GetEntityQuery(); + } + + private EntityQuery _carrierQuery; + + private readonly HashSet _addQueue = new(); + private readonly HashSet<(Entity 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) + { + if (TerminatingOrDeleted(entity)) + continue; + + EnsureComp(entity); + } + _addQueue.Clear(); + + foreach (var (carrier, disease) in _cureQueue) + { + if (carrier.Comp.Diseases.Count > 0) //This is reliable unlike testing Count == 0 right after removal for reasons I don't quite get + RemCompDeferred(carrier); + carrier.Comp.PastDiseases.Add(disease.ID); + carrier.Comp.Diseases.Remove(disease); + } + _cureQueue.Clear(); + + var q = EntityQueryEnumerator(); + while (q.MoveNext(out var owner, out _, out var carrierComp, out var mobState)) + { + DebugTools.Assert(carrierComp.Diseases.Count > 0); + /* + if (_mobStateSystem.IsDead(owner, mobState)) + { + if (_random.Prob(0.005f * frameTime)) //Mean time to remove is 200 seconds per disease + CureDisease((owner, carrierComp), _random.Pick(carrierComp.Diseases)); + + continue; + } +*/ + _parallel.ProcessNow(new DiseaseJob + { + System = this, + Owner = (owner, carrierComp) + }, carrierComp.Diseases.Count); + } + } + + private record struct DiseaseJob : IParallelRobustJob + { + public DiseaseSystem System { get; init; } + public Entity Owner { get; init; } + public float FrameTime { get; init; } + public void Execute(int index) + { + System.Process(Owner, FrameTime, index); + } + } + + private void Process(Entity owner, float frameTime, int i) + { + var disease = owner.Comp.Diseases[i]; + disease.Accumulator += frameTime; + disease.TotalAccumulator += frameTime; + + if (disease.Accumulator < disease.TickTime) + return; + + // if the disease is on the silent disease list, don't do effects + var doEffects = owner.Comp.CarrierDiseases?.Contains(disease.ID) != true; + disease.Accumulator -= disease.TickTime; + + var stage = 0; //defaults to stage 0 because you should always have one + var lastThreshold = 0f; + + for (var j = 0; j < disease.Stages.Count; j++) + { + if (!(disease.TotalAccumulator >= disease.Stages[j]) || !(disease.Stages[j] > lastThreshold)) + continue; + lastThreshold = disease.Stages[j]; + stage = j; + } + + foreach (var cure in disease.Cures) + { + if (!cure.Stages.Contains(stage)) + continue; + var args = new DiseaseCureArgs(owner, disease.ID, cure); + QueueLocalEvent(args); + } + + if (!doEffects) + return; + + foreach (var effect in disease.Effects) + { + if (!effect.Stages.Contains(stage) || !_random.Prob(effect.Probability)) + continue; + var args = new DiseaseEffectArgs(owner, disease.ID, effect); + QueueLocalEvent(args); + } + } + /// + /// Event Handlers + /// + + /// + /// Fill in the natural immunities of this entity. + /// + private void OnInit(EntityUid uid, DiseaseCarrierComponent component, ComponentInit args) + { + if (component.NaturalImmunities == null || component.NaturalImmunities.Count == 0) + return; + + foreach (var immunity in component.NaturalImmunities) + { + component.PastDiseases.Add(immunity); + } + } + + /// + /// 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(Entity ent, ref CureDiseaseAttemptEvent args) + { + foreach (var disease in ent.Comp.Diseases) + { + var cureProb = ((args.CureChance / ent.Comp.Diseases.Count) - disease.CureResist); + if (cureProb < 0) + return; + if (cureProb > 1) + { + CureDisease(ent, disease); + return; + } + if (_random.Prob(cureProb)) + { + CureDisease(ent, disease); + return; + } + } + } + + private void OnRejuvenate(EntityUid uid, DiseaseCarrierComponent component, RejuvenateEvent args) + { + CureAllDiseases(uid, component); + } + + /// + /// 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.Slots.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) + /// + public void CureDisease(Entity carrier, DiseasePrototype disease) + { + _cureQueue.Add((carrier, disease)); + _popupSystem.PopupEntity(Loc.GetString("disease-cured"), carrier.Owner, carrier.Owner); + } + public void CureDisease(Entity carrier, ProtoId disease) + { + CureDisease(carrier, _prototypeManager.Index(disease)); + } + + public void CureAllDiseases(EntityUid uid, DiseaseCarrierComponent? carrier = null) + { + if (!Resolve(uid, ref carrier)) + return; + + foreach (var disease in carrier.Diseases) + { + CureDisease((uid,carrier), disease); + } + } + + /// + /// When a diseased person interacts with something, check infection. + /// + private void OnContactInteraction(EntityUid uid, DiseasedComponent component, ContactInteractionEvent args) + { + InteractWithDiseased(uid, args.Other); + } + + private void OnEntitySpeak(EntityUid uid, DiseasedComponent component, EntitySpokeEvent args) + { + if (TryComp(uid, out var carrier)) + { + SneezeCough(uid, _random.Pick(carrier.Diseases), string.Empty); + } + } + + /// + /// Called when a vaccine is used on someone + /// to handle the vaccination doafter + /// + private void OnAfterInteract(EntityUid uid, DiseaseVaccineComponent vaxx, AfterInteractEvent args) + { + if (args.Target == null || !args.CanReach || args.Handled) + return; + + args.Handled = true; + + if (vaxx.Used) + { + _popupSystem.PopupEntity(Loc.GetString("vaxx-already-used"), args.User, args.User); + return; + } + + var ev = new VaccineDoAfterEvent(); + var doAfterArgs = new DoAfterArgs(EntityManager, args.User, vaxx.InjectDelay, ev, uid, target: args.Target, used: uid) + { + BreakOnMove = true, + NeedHand = true + }; + + _doAfterSystem.TryStartDoAfter(doAfterArgs); + } + + /// + /// 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")); + } + } + + + private void OnApplyMetabolicMultiplier(EntityUid uid, DiseaseCarrierComponent component, ApplyMetabolicMultiplierEvent args) + { + if (args.Apply) + { + foreach (var disease in component.Diseases) + { + disease.TickTime *= args.Multiplier; + return; + } + } + foreach (var disease in component.Diseases) + { + disease.TickTime /= args.Multiplier; + if (disease.Accumulator >= disease.TickTime) + disease.Accumulator = disease.TickTime; + } + } + + /// + /// Helper functions + /// + + /// + /// Tries to infect anyone that + /// interacts with a diseased person or body + /// + private void InteractWithDiseased(EntityUid diseased, EntityUid target, DiseaseCarrierComponent? diseasedCarrier = null) + { + if (!Resolve(diseased, ref diseasedCarrier, false) || + diseasedCarrier.Diseases.Count == 0 || + !TryComp(target, out var carrier)) + return; + + var disease = _random.Pick(diseasedCarrier.Diseases); + TryInfect((target,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(EntityUid host, DiseasePrototype addedDisease, DiseaseCarrierComponent? target = null) + { + if (!Resolve(host, ref target, false)) + return; + + foreach (var disease in target.AllDiseases) + { + if (disease == addedDisease.ID) //ID because of the way protoypes work + return; + } + + var freshDisease = _serializationManager.CreateCopy(addedDisease, notNullableOverride: true); + + //if (freshDisease == null) + // return; + + target.Diseases.Add(freshDisease); + _addQueue.Add(host); + } + + public void TryAddDisease(EntityUid host, string? addedDisease, DiseaseCarrierComponent? target = null) + { + if (addedDisease == null || !_prototypeManager.TryIndex(addedDisease, out var added)) + return; + + TryAddDisease(host, added, target); + } + + /// + /// Pits the infection chance against the + /// person's disease resistance and + /// rolls the dice to see if they get + /// the disease. + /// + /// The target of the disease + /// The disease to apply + /// % chance of the disease being applied, before considering resistance + /// Bypass the disease's infectious trait. + public void TryInfect(Entity carrier, DiseasePrototype? disease, float chance = 0.7f, bool forced = false) + { + if(disease == null || !forced && !disease.Infectious) + return; + var infectionChance = chance - carrier.Comp.DiseaseResist; + if (infectionChance <= 0) + return; + if (_random.Prob(infectionChance)) + TryAddDisease(carrier.Owner, disease, carrier); + } + + public void TryInfect(Entity carrier, ProtoId? disease, float chance = 0.7f, bool forced = false) + { + if (disease == null || !_prototypeManager.TryIndex(disease, out var d)) + return; + + TryInfect(carrier, d, chance, forced); + } + + /// + /// Raises an event for systems to cancel the snough if needed + /// Plays a sneeze/cough sound and popup if applicable + /// and then tries to infect anyone in range + /// if the snougher is not wearing a mask. + /// + public bool SneezeCough(EntityUid uid, DiseasePrototype? disease, string emoteId, bool airTransmit = true, TransformComponent? xform = null) + { + if (!Resolve(uid, ref xform)) + return false; + + if (_mobStateSystem.IsDead(uid)) + return false; + + var attemptSneezeCoughEvent = new AttemptSneezeCoughEvent(emoteId); + RaiseLocalEvent(uid, attemptSneezeCoughEvent); + if (attemptSneezeCoughEvent.Cancelled) + return false; + + _chatSystem.TryEmoteWithChat(uid, emoteId); + + if (disease is not { Infectious: true } || !airTransmit) + return true; + + if (_inventorySystem.TryGetSlotEntity(uid, "mask", out var maskUid) && + EntityManager.TryGetComponent(maskUid, out var blocker) && + blocker.Enabled) + return true; + + foreach (var entity in _lookup.GetEntitiesInRange(_transform.GetMapCoordinates(uid, xform), 2f)) + { + if (!_carrierQuery.TryGetComponent(entity, out var carrier) || + !_interactionSystem.InRangeUnobstructed(uid, entity)) + continue; + + TryInfect((entity,carrier), disease, 0.3f); + } + return true; + } + + } + +/// +/// 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(float cureChance) : EntityEventArgs +{ + public float CureChance { get; } = 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/Backmen/Disease/Effects/DiseaseAddComponent.cs b/Content.Server/Backmen/Disease/Effects/DiseaseAddComponent.cs new file mode 100644 index 00000000000..6f02e72b260 --- /dev/null +++ b/Content.Server/Backmen/Disease/Effects/DiseaseAddComponent.cs @@ -0,0 +1,42 @@ +using Content.Shared.Backmen.Disease; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; + +namespace Content.Server.Backmen.Disease.Effects; + +/// +/// Adds a component to the diseased entity +/// +[UsedImplicitly] +public sealed partial class DiseaseAddComponent : DiseaseEffect +{ + /// + /// The component that is added at the end of build up + /// + [DataField("components")] + public ComponentRegistry Components = new(); +} + +public sealed partial class DiseaseEffectSystem +{ + [Dependency] private readonly ISerializationManager _serialization = default!; + + private void DiseaseAddComponent(DiseaseEffectArgs args, DiseaseAddComponent ds) + { + if (ds.Components.Count == 0) + return; + + var uid = args.DiseasedEntity; + + foreach (var compReg in ds.Components.Values) + { + var compType = compReg.Component.GetType(); + + if (HasComp(uid, compType)) + continue; + var comp = (Component) _serialization.CreateCopy(compReg.Component, notNullableOverride: true); + AddComp(uid, comp, true); + } + } +} diff --git a/Content.Server/Backmen/Disease/Effects/DiseaseAdjustReagent.cs b/Content.Server/Backmen/Disease/Effects/DiseaseAdjustReagent.cs new file mode 100644 index 00000000000..b7d10240ee8 --- /dev/null +++ b/Content.Server/Backmen/Disease/Effects/DiseaseAdjustReagent.cs @@ -0,0 +1,51 @@ +using Content.Server.Body.Components; +using Content.Server.Chemistry.Containers.EntitySystems; +using Content.Shared.Backmen.Disease; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Backmen.Disease.Effects; + +/// +/// Adds or removes reagents from the +/// host's chemstream. +/// +[UsedImplicitly] +public sealed partial class DiseaseAdjustReagent : DiseaseEffect +{ + /// + /// The reagent ID to add or remove. + /// + [DataField("reagent")] + public ReagentId? Reagent = null; + + [DataField("amount", required: true)] + public FixedPoint2 Amount = default!; +} + +public sealed partial class DiseaseEffectSystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + + private void DiseaseAdjustReagent(DiseaseEffectArgs args, DiseaseAdjustReagent ds) + { + if (ds.Reagent == null) + return; + + if (!TryComp(args.DiseasedEntity, out var bloodstream)) + return; + + var stream = bloodstream.ChemicalSolution; + if (stream == null) + return; + + if (ds.Amount < 0 && stream.Value.Comp.Solution.ContainsReagent(ds.Reagent.Value)) + _solutionContainer.RemoveReagent(stream.Value,ds.Reagent.Value, -ds.Amount); + if (ds.Amount > 0) + _solutionContainer.TryAddReagent(stream.Value, ds.Reagent.Value, ds.Amount, out _); + } +} diff --git a/Content.Server/Backmen/Disease/Effects/DiseaseGenericStatusEffect.cs b/Content.Server/Backmen/Disease/Effects/DiseaseGenericStatusEffect.cs new file mode 100644 index 00000000000..068b546477b --- /dev/null +++ b/Content.Server/Backmen/Disease/Effects/DiseaseGenericStatusEffect.cs @@ -0,0 +1,73 @@ +using Content.Shared.Backmen.Disease; +using Content.Shared.StatusEffect; +using JetBrains.Annotations; + +namespace Content.Server.Backmen.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 partial 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 = ""; + + [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; +} + +/// See status effects for how these work +public enum StatusEffectDiseaseType +{ + Add, + Remove, + Set +} + +public sealed partial class DiseaseEffectSystem +{ + [Dependency] private readonly StatusEffectsSystem _effectsSystem = default!; + + private void DiseaseGenericStatusEffect(DiseaseEffectArgs args, DiseaseGenericStatusEffect ds) + { + if (ds.Type == StatusEffectDiseaseType.Add && ds.Component != "") + { + _effectsSystem.TryAddStatusEffect(args.DiseasedEntity, ds.Key, TimeSpan.FromSeconds(ds.Time), ds.Refresh, ds.Component); + } + else if (ds.Type == StatusEffectDiseaseType.Remove) + { + _effectsSystem.TryRemoveTime(args.DiseasedEntity, ds.Key, TimeSpan.FromSeconds(ds.Time)); + } + else if (ds.Type == StatusEffectDiseaseType.Set) + { + _effectsSystem.TrySetTime(args.DiseasedEntity, ds.Key, TimeSpan.FromSeconds(ds.Time)); + } + } +} diff --git a/Content.Server/Backmen/Disease/Effects/DiseaseHealthChange.cs b/Content.Server/Backmen/Disease/Effects/DiseaseHealthChange.cs new file mode 100644 index 00000000000..10864d2dad0 --- /dev/null +++ b/Content.Server/Backmen/Disease/Effects/DiseaseHealthChange.cs @@ -0,0 +1,26 @@ +using Content.Shared.Backmen.Disease; +using Content.Shared.Damage; +using JetBrains.Annotations; + +namespace Content.Server.Backmen.Disease.Effects; + +/// +/// Deals or heals damage to the host +/// +[UsedImplicitly] +public sealed partial class DiseaseHealthChange : DiseaseEffect +{ + [DataField("damage", required: true)] + [ViewVariables(VVAccess.ReadWrite)] + public DamageSpecifier Damage = default!; +} + +public sealed partial class DiseaseEffectSystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + + private void DiseaseHealthChange(DiseaseEffectArgs args, DiseaseHealthChange ds) + { + _damageable.TryChangeDamage(args.DiseasedEntity, ds.Damage, true, false); + } +} diff --git a/Content.Server/Backmen/Disease/Effects/DiseasePolymorph.cs b/Content.Server/Backmen/Disease/Effects/DiseasePolymorph.cs new file mode 100644 index 00000000000..3e0658ee578 --- /dev/null +++ b/Content.Server/Backmen/Disease/Effects/DiseasePolymorph.cs @@ -0,0 +1,48 @@ +using Content.Server.Polymorph.Systems; +using Content.Shared.Audio; +using Content.Shared.Backmen.Disease; +using Content.Shared.Polymorph; +using Content.Shared.Popups; +using JetBrains.Annotations; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Backmen.Disease.Effects; + +[UsedImplicitly] +public sealed partial class DiseasePolymorph : DiseaseEffect +{ + [DataField("polymorphId", required: true)] + [ViewVariables(VVAccess.ReadWrite)] + public ProtoId PolymorphId = default!; + + [DataField("polymorphSound")] + [ViewVariables(VVAccess.ReadWrite)] + public SoundSpecifier? PolymorphSound; + + [DataField("polymorphMessage")] + [ViewVariables(VVAccess.ReadWrite)] + public string? PolymorphMessage; +} + +public sealed partial class DiseaseEffectSystem +{ + [Dependency] private readonly PolymorphSystem _polymorph = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + private void DiseasePolymorph(DiseaseEffectArgs args, DiseasePolymorph ds) + { + var polyUid = _polymorph.PolymorphEntity(args.DiseasedEntity, ds.PolymorphId); + + if (ds.PolymorphSound != null && polyUid != null) + { + _audio.PlayPvs(ds.PolymorphSound, polyUid.Value, AudioParams.Default.WithVariation(0.2f)); + } + if (ds.PolymorphMessage != null && polyUid != null) + _popup.PopupEntity(Loc.GetString(ds.PolymorphMessage), polyUid.Value, polyUid.Value, PopupType.Large); + } +} diff --git a/Content.Server/Backmen/Disease/Effects/DiseasePopUp.cs b/Content.Server/Backmen/Disease/Effects/DiseasePopUp.cs new file mode 100644 index 00000000000..451fe6be269 --- /dev/null +++ b/Content.Server/Backmen/Disease/Effects/DiseasePopUp.cs @@ -0,0 +1,40 @@ +using Content.Shared.Backmen.Disease; +using Content.Shared.IdentityManagement; +using Content.Shared.Popups; +using JetBrains.Annotations; + +namespace Content.Server.Backmen.Disease.Effects; + +/// +/// Plays a popup on the host's transform. +/// Supports passing the host's entity metadata +/// in PVS ones with {$person} +/// +[UsedImplicitly] +public sealed partial class DiseasePopUp : DiseaseEffect +{ + [DataField("message")] + public string Message = "disease-sick-generic"; + + [DataField("type")] + public PopupRecipients Type = PopupRecipients.Local; + + [DataField("visualType")] + public PopupType VisualType = PopupType.Small; +} +public enum PopupRecipients +{ + Pvs, + Local +} + +public sealed partial class DiseaseEffectSystem +{ + private void DiseasePopUp(DiseaseEffectArgs args, DiseasePopUp ds) + { + if (ds.Type == PopupRecipients.Local) + _popup.PopupEntity(Loc.GetString(ds.Message), args.DiseasedEntity, args.DiseasedEntity, ds.VisualType); + else if (ds.Type == PopupRecipients.Pvs) + _popup.PopupEntity(Loc.GetString(ds.Message, ("person", Identity.Entity(args.DiseasedEntity, EntityManager))), args.DiseasedEntity, ds.VisualType); + } +} diff --git a/Content.Server/Backmen/Disease/Effects/DiseaseSnough.cs b/Content.Server/Backmen/Disease/Effects/DiseaseSnough.cs new file mode 100644 index 00000000000..2333684ae64 --- /dev/null +++ b/Content.Server/Backmen/Disease/Effects/DiseaseSnough.cs @@ -0,0 +1,34 @@ +using Content.Shared.Backmen.Disease; +using Content.Shared.Chat.Prototypes; +using JetBrains.Annotations; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Backmen.Disease.Effects; + +/// +/// Makes the diseased sneeze or cough +/// or neither. +/// +[UsedImplicitly] +public sealed partial class DiseaseSnough : DiseaseEffect +{ + /// + /// Emote to play when snoughing + /// + [DataField("emote", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] + public string EmoteId = String.Empty; + + /// + /// Whether to spread the disease through the air + /// + [DataField("airTransmit")] + public bool AirTransmit = true; +} +public sealed partial class DiseaseEffectSystem +{ + [Dependency] private readonly DiseaseSystem _disease = default!; + private void DiseaseSnough(DiseaseEffectArgs args, DiseaseSnough ds) + { + _disease.SneezeCough(args.DiseasedEntity, _prototypeManager.Index(args.Disease), ds.EmoteId, ds.AirTransmit); + } +} diff --git a/Content.Server/Backmen/Disease/Effects/DiseaseVomit.cs b/Content.Server/Backmen/Disease/Effects/DiseaseVomit.cs new file mode 100644 index 00000000000..ef727076140 --- /dev/null +++ b/Content.Server/Backmen/Disease/Effects/DiseaseVomit.cs @@ -0,0 +1,27 @@ +using Content.Server.Medical; +using Content.Shared.Backmen.Disease; +using JetBrains.Annotations; + +namespace Content.Server.Backmen.Disease.Effects; + +/// +/// Forces you to vomit. +/// +[UsedImplicitly] +public sealed partial class DiseaseVomit : DiseaseEffect +{ + /// How many units of thirst to add each time we vomit + [DataField("thirstAmount")] + public float ThirstAmount = -40f; + /// How many units of hunger to add each time we vomit + [DataField("hungerAmount")] + public float HungerAmount = -40f; +} +public sealed partial class DiseaseEffectSystem +{ + [Dependency] private readonly VomitSystem _vomit = default!; + private void DiseaseVomit(DiseaseEffectArgs args, DiseaseVomit ds) + { + _vomit.Vomit(args.DiseasedEntity, ds.ThirstAmount, ds.HungerAmount); + } +} diff --git a/Content.Server/Backmen/Disease/Effects/EffectSystem.cs b/Content.Server/Backmen/Disease/Effects/EffectSystem.cs new file mode 100644 index 00000000000..7420ab56a86 --- /dev/null +++ b/Content.Server/Backmen/Disease/Effects/EffectSystem.cs @@ -0,0 +1,58 @@ +using Content.Shared.Backmen.Disease; +using Content.Shared.Backmen.Disease.Effects; +using Robust.Shared.Prototypes; + +namespace Content.Server.Backmen.Disease.Effects; + +public sealed partial class DiseaseEffectSystem : SharedDiseaseEffectSystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(DoGenericEffect); + } + + private void DoGenericEffect(DiseaseEffectArgs args) + { + if(args.Handled) + return; + + switch (args.DiseaseEffect) + { + case DiseaseAddComponent ds1: + args.Handled = true; + DiseaseAddComponent(args, ds1); + return; + case DiseaseAdjustReagent ds2: + args.Handled = true; + DiseaseAdjustReagent(args, ds2); + return; + case DiseaseGenericStatusEffect ds3: + args.Handled = true; + DiseaseGenericStatusEffect(args, ds3); + return; + case DiseaseHealthChange ds4: + args.Handled = true; + DiseaseHealthChange(args, ds4); + return; + case DiseasePolymorph ds5: + args.Handled = true; + DiseasePolymorph(args, ds5); + return; + case DiseasePopUp ds6: + args.Handled = true; + DiseasePopUp(args, ds6); + return; + case DiseaseSnough ds7: + args.Handled = true; + DiseaseSnough(args, ds7); + return; + case DiseaseVomit ds8: + args.Handled = true; + DiseaseVomit(args, ds8); + return; + } + } +} diff --git a/Content.Server/Backmen/Disease/Server/DiseaseServerComponent.cs b/Content.Server/Backmen/Disease/Server/DiseaseServerComponent.cs new file mode 100644 index 00000000000..cd13d613c34 --- /dev/null +++ b/Content.Server/Backmen/Disease/Server/DiseaseServerComponent.cs @@ -0,0 +1,13 @@ +using Content.Shared.Backmen.Disease; + +namespace Content.Server.Backmen.Disease.Server; + +[RegisterComponent] +public sealed partial class DiseaseServerComponent : Component +{ + /// + /// Which diseases this server has information on. + /// + [ViewVariables(VVAccess.ReadWrite)] + public List Diseases = new(); +} diff --git a/Content.Server/Backmen/Disease/VaccineSystem.cs b/Content.Server/Backmen/Disease/VaccineSystem.cs new file mode 100644 index 00000000000..fa836d7b65b --- /dev/null +++ b/Content.Server/Backmen/Disease/VaccineSystem.cs @@ -0,0 +1,316 @@ +using System.Linq; +using Content.Server.Backmen.Disease.Components; +using Content.Server.Backmen.Disease.Server; +using Content.Server.DoAfter; +using Content.Server.Nutrition.Components; +using Content.Server.Paper; +using Content.Server.Popups; +using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; +using Content.Server.Research.Systems; +using Content.Server.Station.Systems; +using Content.Shared.Backmen.Disease; +using Content.Shared.Backmen.Disease.Events; +using Content.Shared.Backmen.Disease.Swab; +using Content.Shared.DoAfter; +using Content.Shared.Examine; +using Content.Shared.Hands.Components; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Inventory; +using Content.Shared.Materials; +using Content.Shared.Research.Components; +using Content.Shared.Tag; +using Content.Shared.Tools.Components; +using Content.Shared.UserInterface; +using Robust.Server.Audio; +using Robust.Server.GameObjects; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Backmen.Disease; + +public sealed class VaccineSystem : EntitySystem +{ + [Dependency] private readonly DiseaseDiagnosisSystem _diseaseDiagnosisSystem = default!; + [Dependency] private readonly SharedMaterialStorageSystem _storageSystem = default!; + [Dependency] private readonly UserInterfaceSystem _uiSys = default!; + [Dependency] private readonly ResearchSystem _research = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly AudioSystem _audioSystem = default!; + [Dependency] private readonly TagSystem _tagSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + + public override void Initialize() + { + base.Initialize(); + + Subs.BuiEvents(VaccineMachineUiKey.Key, subscriber => + { + subscriber.Event(OnCreateVaccineMessageReceived); + subscriber.Event(OnServerSelected); + subscriber.Event(OnServerDeselected); + subscriber.Event(OnSyncRequest); + subscriber.Event(OpenServerList); + }); + SubscribeLocalEvent(AfterUIOpen); + SubscribeLocalEvent(OnVaccinatorFinished); + SubscribeLocalEvent(OnVaccinatorAmountChanged); + SubscribeLocalEvent(OnResearchRegistrationChanged); + + // vaccines, the item + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnExamined); + + SubscribeLocalEvent(OnDoAfter); + } + + /// + /// This handles running disease machines + /// to handle their delay and visuals. + /// + public override void Update(float frameTime) + { + var q = EntityQueryEnumerator(); + while (q.MoveNext(out var owner, out _, out var diseaseMachine)) + { + diseaseMachine.Accumulator += frameTime; + + while (diseaseMachine.Accumulator >= diseaseMachine.Delay) + { + diseaseMachine.Accumulator -= diseaseMachine.Delay; + var ev = new DiseaseMachineFinishedEvent(diseaseMachine, true); + RaiseLocalEvent(owner, ev); + RemCompDeferred(owner); + } + } + } + + private void OnResearchRegistrationChanged(EntityUid uid, DiseaseVaccineCreatorComponent component, ref ResearchRegistrationChangedEvent args) + { + component.DiseaseServer = TryComp(args.Server, out var diseaseServer) ? diseaseServer : null; + } + + /// + /// Creates a vaccine, if possible, when sent a UI message to do so. + /// + private void OnCreateVaccineMessageReceived(EntityUid uid, DiseaseVaccineCreatorComponent component, CreateVaccineMessage args) + { + if (HasComp(uid) || !this.IsPowered(uid, EntityManager)) + return; + + if (_storageSystem.GetMaterialAmount(uid, "Biomass") < component.BiomassCost * args.Amount) + return; + + if (!_prototypeManager.TryIndex(args.Disease, out var disease)) + return; + + if (!disease.Infectious) + return; + + component.Queued = args.Amount; + QueueNext(uid, component, disease); + UpdateUserInterfaceState(uid, component, true); + } + private void QueueNext(EntityUid uid, DiseaseVaccineCreatorComponent component, DiseasePrototype disease, DiseaseMachineComponent? machine = null) + { + if (!Resolve(uid, ref machine)) + return; + + machine.Disease = disease; + EnsureComp(uid); + _diseaseDiagnosisSystem.UpdateAppearance(uid, true, true); + _audioSystem.PlayPvs(component.RunningSoundPath, uid); + } + + /// + /// Prints a vaccine that will vaccinate + /// against the disease on the inserted swab. + /// + private void OnVaccinatorFinished(EntityUid uid, DiseaseVaccineCreatorComponent component, DiseaseMachineFinishedEvent args) + { + _diseaseDiagnosisSystem.UpdateAppearance(uid, this.IsPowered(uid, EntityManager), false); + + if (!_storageSystem.TryChangeMaterialAmount(uid, "Biomass", (0 - component.BiomassCost))) + return; + + // spawn a vaccine + var vaxx = Spawn(args.Machine.MachineOutput, Transform(uid).Coordinates); + + if (args.Machine.Disease == null) + return; + + _metaData.SetEntityName(vaxx, Loc.GetString("vaccine-name", ("disease", args.Machine.Disease.Name))); + _metaData.SetEntityDescription(vaxx, Loc.GetString("vaccine-desc", ("disease", args.Machine.Disease.Name))); + + if (!TryComp(vaxx, out var vaxxComp)) + return; + + vaxxComp.Disease = args.Machine.Disease; + + component.Queued--; + if (component.Queued > 0) + { + args.Dequeue = false; + QueueNext(uid, component, args.Machine.Disease, args.Machine); + UpdateUserInterfaceState(uid, component); + } + else + { + UpdateUserInterfaceState(uid, component, false); + } + } + + private void OnVaccinatorAmountChanged(EntityUid uid, DiseaseVaccineCreatorComponent component, ref MaterialAmountChangedEvent args) + { + UpdateUserInterfaceState(uid, component); + } + + private void OnServerSelected(EntityUid uid, DiseaseVaccineCreatorComponent component, ResearchClientServerSelectedMessage args) + { + if (!_research.TryGetServerById(args.ServerId, out var serverUid, out var serverComponent)) + return; + + if (!TryComp(serverUid, out var diseaseServer)) + return; + + component.DiseaseServer = diseaseServer; + UpdateUserInterfaceState(uid, component); + } + + private void OnServerDeselected(EntityUid uid, DiseaseVaccineCreatorComponent component, ResearchClientServerDeselectedMessage args) + { + component.DiseaseServer = null; + UpdateUserInterfaceState(uid, component); + } + + private void OnSyncRequest(EntityUid uid, DiseaseVaccineCreatorComponent component, VaccinatorSyncRequestMessage args) + { + UpdateUserInterfaceState(uid, component); + } + + private void OpenServerList(EntityUid uid, DiseaseVaccineCreatorComponent component, VaccinatorServerSelectionMessage args) + { + _uiSys.TryOpen(uid, ResearchClientUiKey.Key, args.Session); + } + + private void AfterUIOpen(EntityUid uid, DiseaseVaccineCreatorComponent component, AfterActivatableUIOpenEvent args) + { + UpdateUserInterfaceState(uid, component); + } + + public void UpdateUserInterfaceState(EntityUid uid, DiseaseVaccineCreatorComponent? component = null, bool? overrideLocked = null) + { + if (!Resolve(uid, ref component)) + return; + + var biomass = _storageSystem.GetMaterialAmount(uid, "Biomass"); + + var diseases = new List<(string id, string name)>(); + var hasServer = false; + + if (component.DiseaseServer != null) + { + foreach (var disease in component.DiseaseServer.Diseases) + { + if (!disease.Infectious) + continue; + + diseases.Add((disease.ID, disease.Name)); + } + + hasServer = true; + } + + var state = new VaccineMachineUpdateState(biomass, component.BiomassCost, diseases, overrideLocked ?? HasComp(uid), hasServer); + _uiSys.TrySetUiState(uid, VaccineMachineUiKey.Key, state); + } + + /// + /// Called when a vaccine is used on someone + /// to handle the vaccination doafter + /// + private void OnAfterInteract(EntityUid uid, DiseaseVaccineComponent vaxx, AfterInteractEvent args) + { + if (args.Target == null || !args.CanReach) + return; + + if (vaxx.Used) + { + _popupSystem.PopupEntity(Loc.GetString("vaxx-already-used"), args.User, args.User); + return; + } + + var ev = new VaccineDoAfterEvent(); + var doAfterArgs = new DoAfterArgs(EntityManager, args.User, vaxx.InjectDelay, ev, uid, target: args.Target, used: uid) + { + BreakOnMove = true, + NeedHand = true + }; + + _doAfterSystem.TryStartDoAfter(doAfterArgs); + } + + /// + /// 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")); + } + } + /// + /// 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.ID); + } + + private void OnDoAfter(EntityUid uid, DiseaseVaccineComponent component, VaccineDoAfterEvent args) + { + if (args.Handled || args.Cancelled || !TryComp(args.Args.Target, out var carrier) || component.Disease == null) + return; + + Vaccinate(carrier, component.Disease); + QueueDel(uid); + args.Handled = true; + } +} + +/// +/// Fires when a disease machine is done +/// with its production delay and ready to +/// create a report or vaccine +/// +public sealed class DiseaseMachineFinishedEvent : EntityEventArgs +{ + public DiseaseMachineComponent Machine {get;} + public bool Dequeue = true; + public DiseaseMachineFinishedEvent(DiseaseMachineComponent machine, bool dequeue) + { + Machine = machine; + Dequeue = dequeue; + } +} diff --git a/Content.Shared/Backmen/Disease/DiseaseCarrierComponent.cs b/Content.Shared/Backmen/Disease/DiseaseCarrierComponent.cs new file mode 100644 index 00000000000..e032c9df5c8 --- /dev/null +++ b/Content.Shared/Backmen/Disease/DiseaseCarrierComponent.cs @@ -0,0 +1,59 @@ +using System.Linq; +using Content.Shared.Backmen.Disease; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; + +namespace Content.Shared.Backmen.Disease; + +/// +/// Allows the entity to be infected with diseases. +/// Please use only on mobs. +/// + +[RegisterComponent] +[NetworkedComponent] +public sealed partial 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 IReadOnlyList> AllDiseases => PastDiseases.Concat(Diseases.Select(x=>(ProtoId)x.ID)).ToList(); + + /// + /// A list of diseases which the entity does not + /// exhibit direct symptoms from. They still transmit + /// these diseases, just without symptoms. + /// + [DataField("carrierDiseases")] + public HashSet>? CarrierDiseases; + + /// + /// When this component is initialized, + /// these diseases will be added to past diseases, + /// rendering them immune. + /// + [DataField("naturalImmunities")] + public List>? NaturalImmunities; +} diff --git a/Content.Shared/Backmen/Disease/DiseaseCure.cs b/Content.Shared/Backmen/Disease/DiseaseCure.cs new file mode 100644 index 00000000000..a7e77a2fe54 --- /dev/null +++ b/Content.Shared/Backmen/Disease/DiseaseCure.cs @@ -0,0 +1,34 @@ +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Backmen.Disease; + +[ImplicitDataDefinitionForInheritors] +[MeansImplicitUse] +public abstract partial class DiseaseCure +{ + /// + /// What stages the cure applies to. + /// probably should be all, but go wild + /// + [DataField("stages")] + public HashSet Stages { get; private set; } = [0]; + + /// + /// 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(); +} + +public sealed class DiseaseCureArgs( + Entity diseasedEntity, + ProtoId disease, + DiseaseCure diseaseCure) + : DiseaseArgs(diseasedEntity, disease) +{ + public readonly DiseaseCure DiseaseCure = diseaseCure; +} diff --git a/Content.Shared/Backmen/Disease/DiseaseEffect.cs b/Content.Shared/Backmen/Disease/DiseaseEffect.cs new file mode 100644 index 00000000000..359559736c6 --- /dev/null +++ b/Content.Shared/Backmen/Disease/DiseaseEffect.cs @@ -0,0 +1,41 @@ +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Backmen.Disease; + +[ImplicitDataDefinitionForInheritors] +[MeansImplicitUse] +public abstract partial class DiseaseEffect : HandledEntityEventArgs +{ + /// + /// What's the chance, from 0 to 1, that this effect will occur? + /// + [DataField("probability")] + public float Probability = 1.0f; + /// + /// What stages this effect triggers on + /// + [DataField("stages")] + public HashSet Stages { get; private set; } = [0]; +} + +public abstract class DiseaseArgs(Entity diseasedEntity, ProtoId disease) + : HandledEntityEventArgs +{ + public readonly Entity DiseasedEntity = diseasedEntity; + public readonly ProtoId Disease = disease; +} + +/// +/// What you have to work with in any disease effect/cure. +/// Includes an entity manager because it is out of scope +/// otherwise. +/// +public sealed class DiseaseEffectArgs( + Entity diseasedEntity, + ProtoId disease, + DiseaseEffect diseaseEffect) + : DiseaseArgs(diseasedEntity, disease) +{ + public readonly DiseaseEffect DiseaseEffect = diseaseEffect; +} diff --git a/Content.Shared/Backmen/Disease/DiseaseMachineRunningComponent.cs b/Content.Shared/Backmen/Disease/DiseaseMachineRunningComponent.cs new file mode 100644 index 00000000000..7617f3b98fd --- /dev/null +++ b/Content.Shared/Backmen/Disease/DiseaseMachineRunningComponent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Disease; + +/// +/// For EntityQuery to keep track of which machines are running +/// +[RegisterComponent] +[NetworkedComponent] +public sealed partial class DiseaseMachineRunningComponent : Component +{ + +} diff --git a/Content.Shared/Backmen/Disease/DiseaseMachineVisuals.cs b/Content.Shared/Backmen/Disease/DiseaseMachineVisuals.cs new file mode 100644 index 00000000000..c205c23c5f1 --- /dev/null +++ b/Content.Shared/Backmen/Disease/DiseaseMachineVisuals.cs @@ -0,0 +1,15 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Backmen.Disease; + +/// +/// Stores bools for if the machine is on +/// and if it's currently running. +/// Used for the visualizer +/// +[Serializable, NetSerializable] +public enum DiseaseMachineVisuals : byte +{ + IsOn, + IsRunning +} diff --git a/Content.Shared/Backmen/Disease/DiseasePrototype.cs b/Content.Shared/Backmen/Disease/DiseasePrototype.cs new file mode 100644 index 00000000000..83d00a830a6 --- /dev/null +++ b/Content.Shared/Backmen/Disease/DiseasePrototype.cs @@ -0,0 +1,82 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; + +namespace Content.Shared.Backmen.Disease; + +/// +/// Diseases encompass everything from viruses to cancers to heart disease. +/// It's not just a virology thing. +/// +[Prototype("disease")] +[DataDefinition] +public sealed partial class DiseasePrototype : IPrototype, IInheritingPrototype +{ + [ViewVariables] + [IdDataField] + public string ID { get; private set; } = default!; + + [DataField("name")] + public string Name { get; private set; } = string.Empty; + + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] + public string[]? Parents { get; private set; } + + [NeverPushInheritance] + [AbstractDataField] + public bool Abstract { get; private set; } + + /// + /// Controls how often a disease ticks. + /// + [ViewVariables] + public float TickTime = 1f; + + /// + /// Since disease isn't mapped to metabolism or anything, + /// it needs something to control its tickrate + /// + public float Accumulator = 0f; + /// + /// Since accumulator is reset with TickTime, this just tracks + /// the total amount of time a disease has been present. + /// + public float TotalAccumulator = 0f; + /// + /// Stores all the separate stages of the disease plus the time + /// thresholds for their activation + /// int: the disease stage (0 for baseline, 1, 2, etc.) + /// float: the time it takes for the stage to begin. + /// + [DataField("stages", serverOnly: true)] + public List Stages { get; private set; } = new() { 0f }; + /// + /// List of effects the disease has that will + /// run every second (by default anyway) + /// + [DataField("effects", serverOnly: true)] + public List Effects { get; private set; } = 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 List Cures { get; private set; } = 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/Backmen/Disease/DiseaseVaccineCreatorMessages.cs b/Content.Shared/Backmen/Disease/DiseaseVaccineCreatorMessages.cs new file mode 100644 index 00000000000..1f05b8504eb --- /dev/null +++ b/Content.Shared/Backmen/Disease/DiseaseVaccineCreatorMessages.cs @@ -0,0 +1,75 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Backmen.Disease; + +[NetSerializable, Serializable] +public enum VaccineMachineUiKey : byte +{ + Key, +} + +/// +/// Sent to the server when the client chooses a vaccine to print. +/// +[Serializable, NetSerializable] +public sealed class CreateVaccineMessage : BoundUserInterfaceMessage +{ + public string Disease; + public int Amount; + + public CreateVaccineMessage(string disease, int amount) + { + Disease = disease; + Amount = amount; + } +} + +/// +/// Just manual UI update. +/// +[Serializable, NetSerializable] +public sealed class VaccinatorSyncRequestMessage : BoundUserInterfaceMessage +{ + public VaccinatorSyncRequestMessage() + { + + } +} + +[Serializable, NetSerializable] +public sealed class VaccineMachineUpdateState : BoundUserInterfaceState +{ + public int Biomass; + + public int BiomassCost; + + public List<(string id, string name)> Diseases; + + public bool Locked; + + public bool HasServer; + + public VaccineMachineUpdateState(int biomass, int biomassCost, List<(string id, string name)> diseases, bool locked, bool hasServer) + { + Biomass = biomass; + BiomassCost = biomassCost; + Diseases = diseases; + Locked = locked; + HasServer = hasServer; + } +} + +[Serializable, NetSerializable] +public sealed class VaccineMachineBoundInterfaceState : BoundUserInterfaceState +{ + +} + +/// +/// Sent to the server to open the ResearchClient UI. +/// +[Serializable, NetSerializable] +public sealed class VaccinatorServerSelectionMessage : BoundUserInterfaceMessage +{ + +} diff --git a/Content.Shared/Backmen/Disease/DiseasedComponent.cs b/Content.Shared/Backmen/Disease/DiseasedComponent.cs new file mode 100644 index 00000000000..1e66e05c384 --- /dev/null +++ b/Content.Shared/Backmen/Disease/DiseasedComponent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Backmen.Disease; + +/// This is added to anyone with at least 1 disease +/// and helps cull event subscriptions and entity queries +/// when they are not relevant. +[RegisterComponent] +[NetworkedComponent] +public sealed partial class DiseasedComponent : Component +{ + +} diff --git a/Content.Shared/Backmen/Disease/Effects/SharedEffectSystem.cs b/Content.Shared/Backmen/Disease/Effects/SharedEffectSystem.cs new file mode 100644 index 00000000000..b03aece1e0c --- /dev/null +++ b/Content.Shared/Backmen/Disease/Effects/SharedEffectSystem.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.Backmen.Disease.Effects; + +public abstract class SharedDiseaseEffectSystem : EntitySystem +{ + +} diff --git a/Content.Shared/Backmen/Disease/Events/AttemptSneezeCoughEvent.cs b/Content.Shared/Backmen/Disease/Events/AttemptSneezeCoughEvent.cs new file mode 100644 index 00000000000..3a84d4104a6 --- /dev/null +++ b/Content.Shared/Backmen/Disease/Events/AttemptSneezeCoughEvent.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.Backmen.Disease.Events; + +public sealed class AttemptSneezeCoughEvent(string? EmoteId) : CancellableEntityEventArgs +{ + public string? EmoteId { get; } = EmoteId; +} diff --git a/Content.Shared/Backmen/Disease/Events/VaccineDoAfterEvent.cs b/Content.Shared/Backmen/Disease/Events/VaccineDoAfterEvent.cs new file mode 100644 index 00000000000..df7897666a3 --- /dev/null +++ b/Content.Shared/Backmen/Disease/Events/VaccineDoAfterEvent.cs @@ -0,0 +1,9 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Backmen.Disease.Events; + +[Serializable, NetSerializable] +public sealed partial class VaccineDoAfterEvent : SimpleDoAfterEvent +{ +} diff --git a/Content.Shared/Backmen/Disease/Swab/SwabEvents.cs b/Content.Shared/Backmen/Disease/Swab/SwabEvents.cs new file mode 100644 index 00000000000..ca220dc580a --- /dev/null +++ b/Content.Shared/Backmen/Disease/Swab/SwabEvents.cs @@ -0,0 +1,14 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Backmen.Disease.Swab; + +[Serializable, NetSerializable] +public sealed partial class DiseaseSwabDoAfterEvent : SimpleDoAfterEvent +{ +} + +[Serializable, NetSerializable] +public sealed partial class BotanySwabDoAfterEvent : SimpleDoAfterEvent +{ +} From 24d6a9dcf3852f08f2aa60e490e7848ae4417468 Mon Sep 17 00:00:00 2001 From: Zack Backmen Date: Fri, 22 Mar 2024 13:46:43 +0300 Subject: [PATCH 2/5] upd --- .../Backmen/Disease/DiseaseMachineSystem.cs | 32 ++ .../Disease/DiseaseMachineVisualsComponent.cs | 15 + .../UI/VaccineMachineBoundUserInterface.cs | 67 ++++ .../Disease/UI/VaccineMachineMenu.xaml | 50 +++ .../Disease/UI/VaccineMachineMenu.xaml.cs | 136 ++++++++ .../Tests/Backmen/Disease/TryAddDisease.cs | 47 +++ .../Backmen/Disease/DiseaseSystem.cs | 3 +- .../StationEvents/DiseaseOutbreakRule.cs | 66 ++++ .../DiseaseOutbreakRuleComponent.cs | 26 ++ .../Backmen/Diseases/infectious.yml | 312 ++++++++++++++++++ .../Backmen/Diseases/noninfectious.yml | 67 ++++ .../Prototypes/Backmen/GameRules/disease.yml | 10 + .../Prototypes/Entities/Mobs/Species/base.yml | 1 + .../Machines/Medical/disease_diagnoser.yml | 4 + .../Machines/Medical/vaccinator.yml | 17 + .../Entities/Structures/Machines/research.yml | 1 + 16 files changed, 853 insertions(+), 1 deletion(-) create mode 100644 Content.Client/Backmen/Disease/DiseaseMachineSystem.cs create mode 100644 Content.Client/Backmen/Disease/DiseaseMachineVisualsComponent.cs create mode 100644 Content.Client/Backmen/Disease/UI/VaccineMachineBoundUserInterface.cs create mode 100644 Content.Client/Backmen/Disease/UI/VaccineMachineMenu.xaml create mode 100644 Content.Client/Backmen/Disease/UI/VaccineMachineMenu.xaml.cs create mode 100644 Content.IntegrationTests/Tests/Backmen/Disease/TryAddDisease.cs create mode 100644 Content.Server/Backmen/Disease/StationEvents/DiseaseOutbreakRule.cs create mode 100644 Content.Server/Backmen/Disease/StationEvents/DiseaseOutbreakRuleComponent.cs create mode 100644 Resources/Prototypes/Backmen/Diseases/infectious.yml create mode 100644 Resources/Prototypes/Backmen/Diseases/noninfectious.yml create mode 100644 Resources/Prototypes/Backmen/GameRules/disease.yml diff --git a/Content.Client/Backmen/Disease/DiseaseMachineSystem.cs b/Content.Client/Backmen/Disease/DiseaseMachineSystem.cs new file mode 100644 index 00000000000..c8d947a437c --- /dev/null +++ b/Content.Client/Backmen/Disease/DiseaseMachineSystem.cs @@ -0,0 +1,32 @@ +using Content.Client.Medical; +using Content.Shared.Backmen.Disease; +using Robust.Client.GameObjects; + +namespace Content.Client.Backmen.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 (args.Sprite == null) + return; + + if (AppearanceSystem.TryGetData(uid, DiseaseMachineVisuals.IsOn, out var isOn, args.Component) + && AppearanceSystem.TryGetData(uid, DiseaseMachineVisuals.IsRunning, out var isRunning, args.Component)) + { + var state = isRunning ? component.RunningState : component.IdleState; + args.Sprite.LayerSetVisible(DiseaseMachineVisualLayers.IsOn, isOn); + args.Sprite.LayerSetState(DiseaseMachineVisualLayers.IsRunning, state); + } + } +} + +public enum DiseaseMachineVisualLayers : byte +{ + IsOn, + IsRunning +} diff --git a/Content.Client/Backmen/Disease/DiseaseMachineVisualsComponent.cs b/Content.Client/Backmen/Disease/DiseaseMachineVisualsComponent.cs new file mode 100644 index 00000000000..a7624d202e0 --- /dev/null +++ b/Content.Client/Backmen/Disease/DiseaseMachineVisualsComponent.cs @@ -0,0 +1,15 @@ +namespace Content.Client.Backmen.Disease; + +/// +/// Holds the idle and running state for machines to control +/// playing animtions on the client. +/// +[RegisterComponent] +public sealed partial class DiseaseMachineVisualsComponent : Component +{ + [DataField("idleState", required: true)] + public string IdleState = default!; + + [DataField("runningState", required: true)] + public string RunningState = default!; +} diff --git a/Content.Client/Backmen/Disease/UI/VaccineMachineBoundUserInterface.cs b/Content.Client/Backmen/Disease/UI/VaccineMachineBoundUserInterface.cs new file mode 100644 index 00000000000..9cc96a24a3f --- /dev/null +++ b/Content.Client/Backmen/Disease/UI/VaccineMachineBoundUserInterface.cs @@ -0,0 +1,67 @@ +using Content.Shared.Backmen.Disease; +using JetBrains.Annotations; + +namespace Content.Client.Backmen.Disease.UI; + +[UsedImplicitly] +public sealed class VaccineMachineBoundUserInterface : BoundUserInterface +{ + private VaccineMachineMenu? _machineMenu; + + public VaccineMachineBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + base.Open(); + + _machineMenu = new VaccineMachineMenu(this); + + _machineMenu.OnClose += Close; + + _machineMenu.OnServerSelectionButtonPressed += _ => + { + SendMessage(new VaccinatorServerSelectionMessage()); + }; + + _machineMenu.OpenCentered(); + _machineMenu?.PopulateBiomass(Owner); + } + + public void CreateVaccineMessage(string disease, int amount) + { + SendMessage(new CreateVaccineMessage(disease, amount)); + } + + public void RequestSync() + { + SendMessage(new VaccinatorSyncRequestMessage()); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + switch (state) + { + case VaccineMachineUpdateState msg: + _machineMenu?.UpdateLocked(msg.Locked); + _machineMenu?.PopulateDiseases(msg.Diseases); + _machineMenu?.PopulateBiomass(Owner); + _machineMenu?.UpdateCost(msg.BiomassCost); + _machineMenu?.UpdateServerConnection(msg.HasServer); + break; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (!disposing) + return; + + _machineMenu?.Dispose(); + } +} diff --git a/Content.Client/Backmen/Disease/UI/VaccineMachineMenu.xaml b/Content.Client/Backmen/Disease/UI/VaccineMachineMenu.xaml new file mode 100644 index 00000000000..6d0653912a0 --- /dev/null +++ b/Content.Client/Backmen/Disease/UI/VaccineMachineMenu.xaml @@ -0,0 +1,50 @@ + + + + + + +