* 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:
Rane
2022-03-13 21:02:55 -04:00
committed by GitHub
parent ce01e53579
commit bb9ad4259c
96 changed files with 2555 additions and 39 deletions

View File

@@ -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;

View 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);
}
}
}

View 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);
}
}
}

View File

@@ -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;

View 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();
}
}

View File

@@ -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
{}
}

View 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;
}
}

View File

@@ -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
{}
}

View File

@@ -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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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
{}
}

View 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)));
}
}
}

View 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)));
}
}
}

View 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));
}
}
}

View 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)));
}
}
}

View 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;
}
}
}
}

View 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
}
}

View 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 _);
}
}
}
}
}

View 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
}
}

View 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);
}
}
}

View 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
}
}

View 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);
}
}
}

View File

@@ -17,6 +17,7 @@ namespace Content.Server.Entry
"ClientEntitySpawner",
"CharacterInfo",
"ItemCabinetVisuals",
"DiseaseMachineVisuals",
"HandheldGPS",
"PotencyVisuals"
};

View File

@@ -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;
}
}

View File

@@ -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)

View 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"));
}
}

View File

@@ -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()

View File

@@ -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"
};
}

View File

@@ -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);
}
}
}
}