Diseases (#7057)
* Disease system first pass * Renamed HealthChange * First working version of diseases (wtf???) * Fix the cursed yaml initialization * Pop-Up effect * Generic status effect * Create copy of prototype * CureDiseaseEffect * Disease resistance * Spaceacillin * Nerf spaceacillin now that we know it works * Sneezing, Coughing, Snoughing * Fix queuing, prevent future issues * Disease protection * Disease outbreak event * Disease Reagent Cure * Chem cause disease effect * Disease artifacts * Try infect when interacting with diseased * Diseases don't have to be infectious * Talking without a mask does a snough * Temperature cure * Bedrest * DiseaseAdjustReagent * Tweak how disease statuses work to be a bit less shit * A few more diseases * Natural immunity (can't get the same disease twice) * Polished up some diseases, touched up spaceacillin production * Rebalanced transmission * Edit a few diseases, make disease cures support a minimum value * Nitrile gloves, more disease protection sources * Health scanner shows diseased status * Clean up disease system * Traitor item * Mouth swabs * Disease diagnoser machine * Support for clean samples * Vaccines + fixes * Pass on disease resistant clothes * More work on non-infectious diseases & vaccines * Handle dead bodies * Added the relatively CBT visualizer * Pass over diseases and their populators * Comment stuff * Readability cleanup * Add printing sound to diagnoser, fix printing bug * vaccinator sound, seal up some classes * Make disease protection equip detection not shit (thanks whoever wrote addaccentcomponent) * Mirror review * More review stuff * More mirror review stuff * Refactor snoughing * Redid report creator * Fix snough messages, new vaccinator sound * Mirror review naming * Woops, forgot the artifact * Add recipes and fills * Rebalance space cold and robovirus * Give lizarb disease interaction stuff * Tweak some stuff and move things around * Add diseases to mice (since animal vectors are interesting and can be used to make vaccines) * Remove unused reagent
This commit is contained in:
@@ -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<DiseasedComponent>(source) && _entManager.TryGetComponent<DiseaseCarrierComponent>(source,out var carrier))
|
||||
{
|
||||
EntitySystem.Get<DiseaseSystem>().SneezeCough(source, _random.Pick(carrier.Diseases), string.Empty);
|
||||
}
|
||||
|
||||
if (MessageCharacterLimit(source, message))
|
||||
{
|
||||
return;
|
||||
|
||||
32
Content.Server/Chemistry/ReagentEffects/ChemCauseDisease.cs
Normal file
32
Content.Server/Chemistry/ReagentEffects/ChemCauseDisease.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Default metabolism for medicine reagents.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class ChemCauseDisease : ReagentEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// Chance it has each tick to cause disease, between 0 and 1
|
||||
/// </summary>
|
||||
[DataField("causeChance")]
|
||||
public float CauseChance = 0.15f;
|
||||
|
||||
/// <summary>
|
||||
/// The disease to add.
|
||||
/// </summary>
|
||||
[DataField("disease", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>))]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string Disease = string.Empty;
|
||||
public override void Effect(ReagentEffectArgs args)
|
||||
{
|
||||
EntitySystem.Get<DiseaseSystem>().TryAddDisease(null, null, Disease, args.SolutionEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Content.Server/Chemistry/ReagentEffects/ChemCureDisease.cs
Normal file
25
Content.Server/Chemistry/ReagentEffects/ChemCureDisease.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Server.Disease;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Chemistry.ReagentEffects
|
||||
{
|
||||
/// <summary>
|
||||
/// Default metabolism for medicine reagents.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class ChemCureDisease : ReagentEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// Chance it has each tick to cure a disease, between 0 and 1
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
36
Content.Server/Disease/Components/DiseaseCarrierComponent.cs
Normal file
36
Content.Server/Disease/Components/DiseaseCarrierComponent.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Disease;
|
||||
|
||||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
/// <summary>
|
||||
/// Allows the enity to be infected with diseases.
|
||||
/// Please use only on mobs.
|
||||
/// </summary>
|
||||
public sealed class DiseaseCarrierComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the CURRENT diseases on the carrier
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public List<DiseasePrototype> Diseases = new();
|
||||
/// <summary>
|
||||
/// The carrier's resistance to disease
|
||||
/// </summary>
|
||||
[DataField("diseaseResist")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float DiseaseResist = 0f;
|
||||
/// <summary>
|
||||
/// Diseases the carrier has had, used for immunity.
|
||||
/// <summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public List<DiseasePrototype> PastDiseases = new();
|
||||
/// <summary>
|
||||
/// All the diseases the carrier has or has had.
|
||||
/// Checked against when trying to add a disease
|
||||
/// <summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public List<DiseasePrototype> AllDiseases => PastDiseases.Concat(Diseases).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// To give the disease diagnosing machine specific behavior
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DiseaseDiagnoserComponent : Component
|
||||
{}
|
||||
}
|
||||
31
Content.Server/Disease/Components/DiseaseMachineComponent.cs
Normal file
31
Content.Server/Disease/Components/DiseaseMachineComponent.cs
Normal file
@@ -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]
|
||||
/// <summary>
|
||||
/// For shared behavior between both disease machines
|
||||
/// </summary>
|
||||
public sealed class DiseaseMachineComponent : Component
|
||||
{
|
||||
[DataField("delay")]
|
||||
public float Delay = 5f;
|
||||
/// <summary>
|
||||
/// How much time we've accumulated processing
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public float Accumulator = 0f;
|
||||
/// <summary>
|
||||
/// The disease prototype currently being diagnosed
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public DiseasePrototype? Disease;
|
||||
/// <summary>
|
||||
/// What the machine will spawn
|
||||
/// </summary>
|
||||
[DataField("machineOutput", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>), required: true)]
|
||||
public string MachineOutput = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// For EntityQuery to keep track of which machines are running
|
||||
/// <summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DiseaseMachineRunningComponent : Component
|
||||
{}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Value added to clothing to give its wearer
|
||||
/// protection against infection from diseases
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DiseaseProtectionComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
[DataField("protection")]
|
||||
public float Protection = 0.1f;
|
||||
/// <summary>
|
||||
/// Is the component currently being worn and affecting someone's disease
|
||||
/// resistance? Making the unequip check not totally CBT
|
||||
/// </summary>
|
||||
public bool IsActive = false;
|
||||
}
|
||||
}
|
||||
33
Content.Server/Disease/Components/DiseaseSwabComponent.cs
Normal file
33
Content.Server/Disease/Components/DiseaseSwabComponent.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Threading;
|
||||
using Content.Shared.Disease;
|
||||
|
||||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
/// <summary>
|
||||
/// For mouth swabs used to collect and process
|
||||
/// disease samples.
|
||||
/// </summary>
|
||||
public sealed class DiseaseSwabComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// How long it takes to swab someone.
|
||||
/// </summary>
|
||||
[DataField("swabDelay")]
|
||||
[ViewVariables]
|
||||
public float SwabDelay = 2f;
|
||||
/// <summary>
|
||||
/// If this swab has been used
|
||||
/// </summary>
|
||||
public bool Used = false;
|
||||
/// <summary>
|
||||
/// Token for interrupting swabbing do after.
|
||||
/// </summary>
|
||||
public CancellationTokenSource? CancelToken;
|
||||
/// <summary>
|
||||
/// The disease prototype currently on the swab
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public DiseasePrototype? Disease;
|
||||
}
|
||||
}
|
||||
33
Content.Server/Disease/Components/DiseaseVaccineComponent.cs
Normal file
33
Content.Server/Disease/Components/DiseaseVaccineComponent.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Threading;
|
||||
using Content.Shared.Disease;
|
||||
|
||||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
/// <summary>
|
||||
/// For disease vaccines
|
||||
/// </summary>
|
||||
public sealed class DiseaseVaccineComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// How long it takes to inject someone
|
||||
/// </summary>
|
||||
[DataField("injectDelay")]
|
||||
[ViewVariables]
|
||||
public float InjectDelay = 2f;
|
||||
/// <summary>
|
||||
/// If this vaccine has been used
|
||||
/// </summary>
|
||||
public bool Used = false;
|
||||
/// <summary>
|
||||
/// Token for interrupting injection do after.
|
||||
/// </summary>
|
||||
public CancellationTokenSource? CancelToken;
|
||||
|
||||
/// <summary>
|
||||
/// The disease prototype currently on the vaccine
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public DiseasePrototype? Disease;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls disease machine behavior specific to the
|
||||
/// vaccine creating machine
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DiseaseVaccineCreatorComponent : Component
|
||||
{}
|
||||
}
|
||||
33
Content.Server/Disease/Cures/DiseaseBedrestCure.cs
Normal file
33
Content.Server/Disease/Cures/DiseaseBedrestCure.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Content.Shared.Disease;
|
||||
using Content.Server.Buckle.Components;
|
||||
|
||||
namespace Content.Server.Disease.Cures
|
||||
{
|
||||
/// <summary>
|
||||
/// Cures the disease after a certain amount of time
|
||||
/// strapped.
|
||||
/// </summary>
|
||||
/// 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<BuckleComponent>(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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Content.Server/Disease/Cures/DiseaseBodyTemperatureCure.cs
Normal file
34
Content.Server/Disease/Cures/DiseaseBodyTemperatureCure.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Content.Server.Temperature.Components;
|
||||
using Content.Shared.Disease;
|
||||
|
||||
namespace Content.Server.Disease.Cures
|
||||
{
|
||||
/// <summary>
|
||||
/// Cures the disease if temperature is within certain bounds.
|
||||
/// </summary>
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Content.Server/Disease/Cures/DiseaseJustWaitCure.cs
Normal file
31
Content.Server/Disease/Cures/DiseaseJustWaitCure.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Content.Shared.Disease;
|
||||
|
||||
namespace Content.Server.Disease.Cures
|
||||
{
|
||||
/// <summary>
|
||||
/// Automatically removes the disease after a
|
||||
/// certain amount of time.
|
||||
/// </summary>
|
||||
public sealed class DiseaseJustWaitCure : DiseaseCure
|
||||
{
|
||||
/// <summary>
|
||||
/// All of these are in seconds
|
||||
/// </summary>
|
||||
[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));
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Content.Server/Disease/Cures/DiseaseReagentCure.cs
Normal file
38
Content.Server/Disease/Cures/DiseaseReagentCure.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Content.Shared.Disease;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Server.Body.Components;
|
||||
|
||||
namespace Content.Server.Disease.Cures
|
||||
{
|
||||
/// <summary>
|
||||
/// Cures the disease if a certain amount of reagent
|
||||
/// is in the host's chemstream.
|
||||
/// </summary>
|
||||
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<BloodstreamComponent>(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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
415
Content.Server/Disease/DiseaseDiagnosisSystem.cs
Normal file
415
Content.Server/Disease/DiseaseDiagnosisSystem.cs
Normal file
@@ -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<DiseaseSwabComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<DiseaseSwabComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<DiseaseDiagnoserComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
|
||||
SubscribeLocalEvent<DiseaseVaccineCreatorComponent, AfterInteractUsingEvent>(OnAfterInteractUsingVaccine);
|
||||
/// Visuals
|
||||
SubscribeLocalEvent<DiseaseMachineComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
/// Private Events
|
||||
SubscribeLocalEvent<DiseaseDiagnoserComponent, DiseaseMachineFinishedEvent>(OnDiagnoserFinished);
|
||||
SubscribeLocalEvent<DiseaseVaccineCreatorComponent, DiseaseMachineFinishedEvent>(OnVaccinatorFinished);
|
||||
SubscribeLocalEvent<TargetSwabSuccessfulEvent>(OnTargetSwabSuccessful);
|
||||
SubscribeLocalEvent<SwabCancelledEvent>(OnSwabCancelled);
|
||||
}
|
||||
|
||||
private Queue<EntityUid> AddQueue = new();
|
||||
private Queue<EntityUid> RemoveQueue = new();
|
||||
|
||||
/// <summary>
|
||||
/// This handles running disease machines
|
||||
/// to handle their delay and visuals.
|
||||
/// </summary>
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
foreach (var uid in AddQueue)
|
||||
EnsureComp<DiseaseMachineRunningComponent>(uid);
|
||||
|
||||
AddQueue.Clear();
|
||||
foreach (var uid in RemoveQueue)
|
||||
RemComp<DiseaseMachineRunningComponent>(uid);
|
||||
|
||||
RemoveQueue.Clear();
|
||||
|
||||
foreach (var (runningComp, diseaseMachine) in EntityQuery<DiseaseMachineRunningComponent, DiseaseMachineComponent>(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
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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<DiseaseCarrierComponent>(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<IngestionBlockerComponent>(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
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This handles the disease diagnoser machine up
|
||||
/// until it's turned on. It has some slight
|
||||
/// differences in checks from the vaccinator.
|
||||
/// </summary>
|
||||
private void OnAfterInteractUsing(EntityUid uid, DiseaseDiagnoserComponent component, AfterInteractUsingEvent args)
|
||||
{
|
||||
var machine = Comp<DiseaseMachineComponent>(uid);
|
||||
if (args.Handled || !args.CanReach)
|
||||
return;
|
||||
|
||||
if (TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
|
||||
return;
|
||||
|
||||
if (!HasComp<HandsComponent>(args.User) || HasComp<ToolComponent>(args.Used)) // Don't want to accidentally breach wrenching or whatever
|
||||
return;
|
||||
|
||||
if (!TryComp<DiseaseSwabComponent>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This handles the vaccinator machine up
|
||||
/// until it's turned on. It has some slight
|
||||
/// differences in checks from the diagnoser.
|
||||
/// </summary>
|
||||
private void OnAfterInteractUsingVaccine(EntityUid uid, DiseaseVaccineCreatorComponent component, AfterInteractUsingEvent args)
|
||||
{
|
||||
if (args.Handled || !args.CanReach)
|
||||
return;
|
||||
|
||||
if (TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
|
||||
return;
|
||||
|
||||
if (!HasComp<HandsComponent>(args.User) || HasComp<ToolComponent>(args.Used)) //This check ensures tools don't break without yaml ordering jank
|
||||
return;
|
||||
|
||||
if (!TryComp<DiseaseSwabComponent>(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<DiseaseMachineComponent>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This handles swab examination text
|
||||
/// so you can tell if they are used or not.
|
||||
/// </summary>
|
||||
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
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// Appearance helper function to
|
||||
/// set the component's power and running states.
|
||||
/// </summary>
|
||||
private void UpdateAppearance(EntityUid uid, bool isOn, bool isRunning)
|
||||
{
|
||||
if (!TryComp<AppearanceComponent>(uid, out var appearance))
|
||||
return;
|
||||
|
||||
appearance.SetData(DiseaseMachineVisuals.IsOn, isOn);
|
||||
appearance.SetData(DiseaseMachineVisuals.IsRunning, isRunning);
|
||||
}
|
||||
/// <summary>
|
||||
/// Makes sure the machine is visually off/on.
|
||||
/// </summary>
|
||||
private void OnPowerChanged(EntityUid uid, DiseaseMachineComponent component, PowerChangedEvent args)
|
||||
{
|
||||
UpdateAppearance(uid, args.Powered, false);
|
||||
}
|
||||
///
|
||||
/// Private events
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// Copies a disease prototype to the swab
|
||||
/// after the doafter completes.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the swab doafter if needed.
|
||||
/// </summary>
|
||||
private static void OnSwabCancelled(SwabCancelledEvent args)
|
||||
{
|
||||
args.Swab.CancelToken = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints a diagnostic report with its findings.
|
||||
/// Also cancels the animation.
|
||||
/// </summary>
|
||||
private void OnDiagnoserFinished(EntityUid uid, DiseaseDiagnoserComponent component, DiseaseMachineFinishedEvent args)
|
||||
{
|
||||
var power = Comp<ApcPowerReceiverComponent>(uid);
|
||||
UpdateAppearance(uid, power.Powered, false);
|
||||
// spawn a piece of paper.
|
||||
var printed = EntityManager.SpawnEntity(args.Machine.MachineOutput, Transform(uid).Coordinates);
|
||||
|
||||
if (!TryComp<PaperComponent>(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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints a vaccine that will vaccinate
|
||||
/// against the disease on the inserted swab.
|
||||
/// <summary>
|
||||
private void OnVaccinatorFinished(EntityUid uid, DiseaseVaccineCreatorComponent component, DiseaseMachineFinishedEvent args)
|
||||
{
|
||||
var power = Comp<ApcPowerReceiverComponent>(uid);
|
||||
UpdateAppearance(uid, power.Powered, false);
|
||||
|
||||
// spawn a vaccine
|
||||
var vaxx = EntityManager.SpawnEntity(args.Machine.MachineOutput, Transform(uid).Coordinates);
|
||||
|
||||
if (!TryComp<DiseaseVaccineComponent>(vaxx, out var vaxxComp))
|
||||
return;
|
||||
|
||||
vaxxComp.Disease = args.Machine.Disease;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the mouth-swabbing doafter
|
||||
/// </summary>
|
||||
private sealed class SwabCancelledEvent : EntityEventArgs
|
||||
{
|
||||
public readonly DiseaseSwabComponent Swab;
|
||||
public SwabCancelledEvent(DiseaseSwabComponent swab)
|
||||
{
|
||||
Swab = swab;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires if the doafter for swabbing someone's mouth succeeds
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires when a disease machine is done
|
||||
/// with its production delay and ready to
|
||||
/// create a report or vaccine
|
||||
/// </summary>
|
||||
private sealed class DiseaseMachineFinishedEvent : EntityEventArgs
|
||||
{
|
||||
public DiseaseMachineComponent Machine {get;}
|
||||
public DiseaseMachineFinishedEvent(DiseaseMachineComponent machine)
|
||||
{
|
||||
Machine = machine;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
442
Content.Server/Disease/DiseaseSystem.cs
Normal file
442
Content.Server/Disease/DiseaseSystem.cs
Normal file
@@ -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
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Handles disease propagation & curing
|
||||
/// </summary>
|
||||
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<DiseaseCarrierComponent, CureDiseaseAttemptEvent>(OnTryCureDisease);
|
||||
SubscribeLocalEvent<DiseasedComponent, InteractHandEvent>(OnInteractDiseasedHand);
|
||||
SubscribeLocalEvent<DiseasedComponent, InteractUsingEvent>(OnInteractDiseasedUsing);
|
||||
SubscribeLocalEvent<DiseaseProtectionComponent, GotEquippedEvent>(OnEquipped);
|
||||
SubscribeLocalEvent<DiseaseProtectionComponent, GotUnequippedEvent>(OnUnequipped);
|
||||
SubscribeLocalEvent<DiseaseVaccineComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<DiseaseVaccineComponent, ExaminedEvent>(OnExamined);
|
||||
/// Private events stuff
|
||||
SubscribeLocalEvent<TargetVaxxSuccessfulEvent>(OnTargetVaxxSuccessful);
|
||||
SubscribeLocalEvent<VaxxCancelledEvent>(OnVaxxCancelled);
|
||||
}
|
||||
|
||||
private Queue<EntityUid> AddQueue = new();
|
||||
private Queue<(DiseaseCarrierComponent carrier, DiseasePrototype disease)> CureQueue = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
foreach (var entity in AddQueue)
|
||||
EnsureComp<DiseasedComponent>(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<DiseasedComponent>(tuple.carrier.Owner);
|
||||
tuple.carrier.PastDiseases.Add(tuple.disease);
|
||||
tuple.carrier.Diseases.Remove(tuple.disease);
|
||||
}
|
||||
CureQueue.Clear();
|
||||
|
||||
foreach (var (diseasedComp, carrierComp, mobState) in EntityQuery<DiseasedComponent, DiseaseCarrierComponent, MobStateComponent>(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
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a component with disease protection
|
||||
/// is equipped so it can be added to the person's
|
||||
/// total disease resistance
|
||||
/// </summary>
|
||||
private void OnEquipped(EntityUid uid, DiseaseProtectionComponent component, GotEquippedEvent args)
|
||||
{
|
||||
/// This only works on clothing
|
||||
if (!TryComp<ClothingComponent>(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<DiseaseCarrierComponent>(args.Equipee, out var carrier))
|
||||
carrier.DiseaseResist += component.Protection;
|
||||
/// Set the component to active to the unequip check isn't CBT
|
||||
component.IsActive = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a component with disease protection
|
||||
/// is unequipped so it can be removed from the person's
|
||||
/// total disease resistance
|
||||
/// </summary>
|
||||
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<DiseaseCarrierComponent>(args.Equipee, out var carrier))
|
||||
carrier.DiseaseResist -= component.Protection;
|
||||
component.IsActive = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when someone interacts with a diseased person with an empty hand
|
||||
/// to check if they get infected
|
||||
/// </summary>
|
||||
private void OnInteractDiseasedHand(EntityUid uid, DiseasedComponent component, InteractHandEvent args)
|
||||
{
|
||||
if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target))
|
||||
return;
|
||||
InteractWithDiseased (args.Target, args.User);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when someone interacts with a diseased person with any object
|
||||
/// to check if they get infected
|
||||
/// </summary>
|
||||
private void OnInteractDiseasedUsing(EntityUid uid, DiseasedComponent component, InteractUsingEvent args)
|
||||
{
|
||||
InteractWithDiseased(args.Target, args.User);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a vaccine is used on someone
|
||||
/// to handle the vaccination doafter
|
||||
/// </summary>
|
||||
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<DiseaseCarrierComponent>(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
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a vaccine is examined.
|
||||
/// Currently doesn't do much because
|
||||
/// vaccines don't have unique art with a seperate
|
||||
/// state visualizer.
|
||||
/// </summary>
|
||||
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
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// Tries to infect anyone that
|
||||
/// interacts with a diseased person or body
|
||||
/// </summary>
|
||||
private void InteractWithDiseased(EntityUid diseased, EntityUid target)
|
||||
{
|
||||
if (!TryComp<DiseaseCarrierComponent>(target, out var carrier))
|
||||
return;
|
||||
|
||||
var disease = _random.Pick(Comp<DiseaseCarrierComponent>(diseased).Diseases);
|
||||
if (disease != null)
|
||||
TryInfect(carrier, disease, 0.4f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<DiseaseCarrierComponent>(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pits the infection chance against the
|
||||
/// person's disease resistance and
|
||||
/// rolls the dice to see if they get
|
||||
/// the disease.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a sneeze/cough popup if applicable
|
||||
/// and then tries to infect anyone in range
|
||||
/// if the snougher is not wearing a mask.
|
||||
/// </summary>
|
||||
public void SneezeCough(EntityUid uid, DiseasePrototype? disease, string snoughMessage, bool airTransmit = true, float infectionChance = 0.3f)
|
||||
{
|
||||
var xform = Comp<TransformComponent>(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<IngestionBlockerComponent>(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<DiseaseCarrierComponent>(entity, out var carrier))
|
||||
TryInfect(carrier, disease, 0.3f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a disease to the carrier's
|
||||
/// past diseases to give them immunity
|
||||
/// IF they don't already have the disease.
|
||||
/// <summary>
|
||||
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
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// Injects the vaccine into the target
|
||||
/// if the doafter is completed
|
||||
/// </summary>
|
||||
private void OnTargetVaxxSuccessful(TargetVaxxSuccessfulEvent args)
|
||||
{
|
||||
if (args.Vaxx.Disease == null)
|
||||
return;
|
||||
Vaccinate(args.Carrier, args.Vaxx.Disease);
|
||||
EntityManager.DeleteEntity(args.Vaxx.Owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the vaccine doafter
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public sealed class CureDiseaseAttemptEvent : EntityEventArgs
|
||||
{
|
||||
public float CureChance { get; }
|
||||
public CureDiseaseAttemptEvent(float cureChance)
|
||||
{
|
||||
CureChance = cureChance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether the snough is a sneeze, cough
|
||||
/// or neither. If none, will not create
|
||||
/// a popup. Mostly used for talking
|
||||
/// </summary>
|
||||
public enum SneezeCoughType
|
||||
{
|
||||
Sneeze,
|
||||
Cough,
|
||||
None
|
||||
}
|
||||
}
|
||||
46
Content.Server/Disease/Effects/DiseaseAdjustReagent.cs
Normal file
46
Content.Server/Disease/Effects/DiseaseAdjustReagent.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds or removes reagents from the
|
||||
/// host's chemstream.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class DiseaseAdjustReagent : DiseaseEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// The reagent ID to add or remove.
|
||||
/// </summary>
|
||||
[DataField("reagent", customTypeSerializer:typeof(PrototypeIdSerializer<ReagentPrototype>))]
|
||||
public string? Reagent = null;
|
||||
|
||||
[DataField("amount", required: true)]
|
||||
public FixedPoint2 Amount = default!;
|
||||
|
||||
public override void Effect(DiseaseEffectArgs args)
|
||||
{
|
||||
if (!args.EntityManager.TryGetComponent<BloodstreamComponent>(args.DiseasedEntity, out var bloodstream))
|
||||
return;
|
||||
|
||||
var stream = bloodstream.ChemicalSolution;
|
||||
if (stream != null)
|
||||
{
|
||||
var solutionSys = args.EntityManager.EntitySysManager.GetEntitySystem<SolutionContainerSystem>();
|
||||
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 _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Content.Server/Disease/Effects/DiseaseGenericStatusEffect.cs
Normal file
67
Content.Server/Disease/Effects/DiseaseGenericStatusEffect.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using Content.Shared.Disease;
|
||||
using Content.Shared.StatusEffect;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Disease.Effects
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class DiseaseGenericStatusEffect : DiseaseEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// The status effect key
|
||||
/// Prevents other components from being with the same key
|
||||
/// </summary>
|
||||
[DataField("key", required: true)]
|
||||
public string Key = default!;
|
||||
/// <summary>
|
||||
/// The component to add
|
||||
/// </summary>
|
||||
[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
|
||||
|
||||
/// <remarks>
|
||||
/// true - refresh status effect time, false - accumulate status effect time
|
||||
/// </remarks>
|
||||
[DataField("refresh")]
|
||||
public bool Refresh = false;
|
||||
|
||||
/// <summary>
|
||||
/// Should this effect add the status effect, remove time from it, or set its cooldown?
|
||||
/// </summary>
|
||||
[DataField("type")]
|
||||
public StatusEffectDiseaseType Type = StatusEffectDiseaseType.Add;
|
||||
|
||||
public override void Effect(DiseaseEffectArgs args)
|
||||
{
|
||||
var statusSys = EntitySystem.Get<StatusEffectsSystem>();
|
||||
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
|
||||
}
|
||||
}
|
||||
21
Content.Server/Disease/Effects/DiseaseHealthChange.cs
Normal file
21
Content.Server/Disease/Effects/DiseaseHealthChange.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Content.Shared.Disease;
|
||||
using Content.Shared.Damage;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Disease.Effects
|
||||
{
|
||||
/// <summary>
|
||||
/// Deals or heals damage to the host
|
||||
/// </summary>
|
||||
[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<DamageableSystem>().TryChangeDamage(args.DiseasedEntity, Damage, true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Content.Server/Disease/Effects/DiseasePopUp.cs
Normal file
38
Content.Server/Disease/Effects/DiseasePopUp.cs
Normal file
@@ -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]
|
||||
/// <summary>
|
||||
/// Plays a popup on the host's transform.
|
||||
/// Supports passing the host's entity metadata
|
||||
/// in PVS ones with {$person}
|
||||
/// </summary>
|
||||
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<SharedPopupSystem>();
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
30
Content.Server/Disease/Effects/DiseaseSnough.cs
Normal file
30
Content.Server/Disease/Effects/DiseaseSnough.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Content.Shared.Disease;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Disease
|
||||
{
|
||||
[UsedImplicitly]
|
||||
|
||||
/// <summary>
|
||||
/// Makes the diseased sneeze or cough
|
||||
/// or neither.
|
||||
/// </summary>
|
||||
public sealed class DiseaseSnough : DiseaseEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// Message to play when snoughing
|
||||
/// </summary>
|
||||
[DataField("snoughMessage")]
|
||||
public string SnoughMessage = "disease-sneeze";
|
||||
/// <summary>
|
||||
/// Whether to spread the disease throught he air
|
||||
/// </summary>
|
||||
[DataField("airTransmit")]
|
||||
public bool AirTransmit = true;
|
||||
|
||||
public override void Effect(DiseaseEffectArgs args)
|
||||
{
|
||||
EntitySystem.Get<DiseaseSystem>().SneezeCough(args.DiseasedEntity, args.Disease, SnoughMessage, AirTransmit);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ namespace Content.Server.Entry
|
||||
"ClientEntitySpawner",
|
||||
"CharacterInfo",
|
||||
"ItemCabinetVisuals",
|
||||
"DiseaseMachineVisuals",
|
||||
"HandheldGPS",
|
||||
"PotencyVisuals"
|
||||
};
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public CancellationTokenSource? CancelToken;
|
||||
public BoundUserInterface? UserInterface => Owner.GetUIOrNull(HealthAnalyzerUiKey.Key);
|
||||
|
||||
/// <summary>
|
||||
/// Is this actually going to give people the disease below
|
||||
/// </summary>
|
||||
[DataField("fake")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Fake = false;
|
||||
|
||||
/// <summary>
|
||||
/// The disease this will give people if Fake == true
|
||||
/// </summary>
|
||||
[DataField("disease", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>))]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string Disease = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DiseaseSystem>().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)
|
||||
|
||||
62
Content.Server/StationEvents/Events/DiseaseOutbreak.cs
Normal file
62
Content.Server/StationEvents/Events/DiseaseOutbreak.cs
Normal file
@@ -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;
|
||||
/// <summary>
|
||||
/// Infects a couple people
|
||||
/// with a random disease that isn't super deadly
|
||||
/// </summary>
|
||||
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!;
|
||||
|
||||
/// <summary>
|
||||
/// Disease prototypes I decided were not too deadly for a random event
|
||||
/// </summary>
|
||||
public readonly IReadOnlyList<string> NotTooSeriousDiseases = new[]
|
||||
{
|
||||
"SpaceCold",
|
||||
"VanAusdallsRobovirus",
|
||||
"VentCough",
|
||||
"AMIV"
|
||||
};
|
||||
public override string Name => "DiseaseOutbreak";
|
||||
public override float Weight => WeightNormal;
|
||||
protected override float EndAfter => 1.0f;
|
||||
/// <summary>
|
||||
/// Finds 2-5 random entities that can host diseases
|
||||
/// and gives them a randomly selected disease.
|
||||
/// They all get the same disease.
|
||||
/// </summary>
|
||||
public override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
var targetList = _entityManager.EntityQuery<DiseaseCarrierComponent>().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<DiseaseSystem>().TryAddDisease(target, disease);
|
||||
}
|
||||
_chatManager.DispatchStationAnnouncement(Loc.GetString("station-event-disease-outbreak-announcement"));
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public sealed class VentClog : StationEvent
|
||||
public readonly IReadOnlyList<string> 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()
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using Content.Shared.Disease;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
|
||||
/// <summary>
|
||||
/// Spawn a random disease at regular intervals when artifact activated.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DiseaseArtifactComponent : Component
|
||||
{
|
||||
public override string Name => "DiseaseArtifact";
|
||||
/// <summary>
|
||||
/// Disease the artifact will spawn
|
||||
/// If empty, picks a random one from its list
|
||||
/// </summary>
|
||||
[DataField("disease", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>))]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string SpawnDisease = string.Empty;
|
||||
/// <summary>
|
||||
/// How far away it will check for people
|
||||
/// If empty, picks a random one from its list
|
||||
/// </summary>
|
||||
[DataField("range")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Range = 5f;
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public DiseasePrototype ResolveDisease = default!;
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public readonly IReadOnlyList<string> ArtifactDiseases = new[]
|
||||
{
|
||||
"VanAusdallsRobovirus",
|
||||
"OwOnavirus",
|
||||
"BleedersBite",
|
||||
"Ultragigacancer",
|
||||
"AMIV"
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles disease-producing artifacts
|
||||
/// </summary>
|
||||
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<DiseaseArtifactComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<DiseaseArtifactComponent, ArtifactActivatedEvent>(OnActivate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes sure this artifact is assigned a disease
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When activated, blasts everyone in LOS within 3 tiles
|
||||
/// with a high-probability disease infection attempt
|
||||
/// </summary>
|
||||
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<DiseaseCarrierComponent>(entity, out var carrier))
|
||||
EntitySystem.Get<DiseaseSystem>().TryInfect(carrier, component.ResolveDisease);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user