diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
index 159e6db8576..bdd4ced7520 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
@@ -50,6 +50,9 @@
+
+
+
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
index db30bfab6b5..231ebe27cfc 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
@@ -22,6 +22,7 @@
using Robust.Client.ResourceManagement;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
+using Content.Shared.ADT.Body.Allergies;
namespace Content.Client.HealthAnalyzer.UI
{
@@ -131,6 +132,11 @@ public void Populate(HealthAnalyzerScannedUserMessage msg)
DamageLabel.Text = damageable.TotalDamage.ToString();
+ // ADT-Tweak-Start
+ bool allergic = _entityManager.HasComponent(target.Value);
+ AllergyText.Visible = allergic;
+ // ADT-Tweak-End
+
// Alerts
var showAlerts = msg.Unrevivable == true || msg.Bleeding == true;
diff --git a/Content.Server/ADT/Body/AllergicEvents.cs b/Content.Server/ADT/Body/AllergicEvents.cs
new file mode 100644
index 00000000000..26b9b9ed947
--- /dev/null
+++ b/Content.Server/ADT/Body/AllergicEvents.cs
@@ -0,0 +1,16 @@
+using Content.Shared.Chemistry.Reagent;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.ADT.Body;
+
+///
+/// When allergy triggered by allergens in bloodstream.
+///
+[ByRefEvent]
+public struct AllergyTriggeredEvent;
+
+///
+/// When allergy stack has faded to zero.
+///
+[ByRefEvent]
+public struct AllergyFadedEvent;
\ No newline at end of file
diff --git a/Content.Server/ADT/Body/AllergicSystem.Shock.cs b/Content.Server/ADT/Body/AllergicSystem.Shock.cs
new file mode 100644
index 00000000000..22db302039c
--- /dev/null
+++ b/Content.Server/ADT/Body/AllergicSystem.Shock.cs
@@ -0,0 +1,66 @@
+using Content.Shared.ADT.Body.Allergies;
+using Content.Shared.ADT.Body.Allergies.Prototypes;
+using Content.Shared.EntityEffects;
+using Robust.Shared.Timing;
+using System.Linq;
+
+namespace Content.Server.ADT.Body;
+
+public sealed partial class AllergicSystem : EntitySystem
+{
+ private List _reactions = new();
+
+ [Dependency] private IGameTiming _timing = default!;
+ [Dependency] private SharedEntityEffectsSystem _effectSystem = default!;
+
+ public void InitializeShock()
+ {
+ SubscribeLocalEvent(Unshock);
+ _reactions = _proto.EnumeratePrototypes()
+ .OrderBy(r => r.StackThreshold)
+ .ToList();
+ }
+
+ private void Unshock(EntityUid uid, AllergicComponent allergic, ref AllergyFadedEvent ev)
+ {
+ allergic.NextShockEvent = TimeSpan.Zero;
+ }
+
+ private void AssignNextShockTiming(AllergicComponent allergic)
+ {
+ allergic.NextShockEvent = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(allergic.MinimumTimeTilNextShockEvent, allergic.MaximumTimeTilNextShockEvent));
+ }
+
+ private void UpdateShock(EntityUid uid, AllergicComponent allergic)
+ {
+ if (allergic.AllergyStack <= 0)
+ return;
+
+ if (allergic.NextShockEvent == TimeSpan.Zero)
+ {
+ AssignNextShockTiming(allergic);
+ return;
+ }
+
+ if (allergic.NextShockEvent > _timing.CurTime)
+ return;
+
+ AllergicReactionPrototype? picked = null;
+
+ foreach (var reaction in _reactions)
+ {
+ if (allergic.AllergyStack >= reaction.StackThreshold)
+ picked = reaction;
+ }
+
+ if (picked == null)
+ return;
+
+ foreach (var effect in picked.Effects)
+ {
+ _effectSystem.ApplyEffect(uid, effect);
+ }
+
+ AssignNextShockTiming(allergic);
+ }
+}
diff --git a/Content.Server/ADT/Body/AllergicSystem.Stacks.cs b/Content.Server/ADT/Body/AllergicSystem.Stacks.cs
new file mode 100644
index 00000000000..1a35fad3431
--- /dev/null
+++ b/Content.Server/ADT/Body/AllergicSystem.Stacks.cs
@@ -0,0 +1,71 @@
+using Content.Shared.ADT.Body.Allergies;
+
+namespace Content.Server.ADT.Body;
+
+public sealed partial class AllergicSystem : EntitySystem
+{
+ #region Events
+
+ private void IncrementStackOnTrigger(EntityUid uid, AllergicComponent allergic, ref AllergyTriggeredEvent ev)
+ {
+ AdjustAllergyStack(uid, allergic.StackGrow, allergic);
+ }
+
+ #endregion
+
+ #region Flow
+
+ private void UpdateStack(EntityUid uid, AllergicComponent allergic)
+ {
+ if (allergic.AllergyStack <= 0)
+ return;
+
+ if (allergic.NextStackFade > _timing.CurTime)
+ return;
+
+ float newStack = allergic.AllergyStack + allergic.StackFade;
+ SetAllergyStack(uid, newStack, allergic);
+
+ if (newStack <= 0)
+ {
+ var faded = new AllergyFadedEvent();
+ RaiseLocalEvent(uid, ref faded);
+ }
+
+ allergic.NextStackFade = allergic.NextStackFade + allergic.StackFadeRate;
+ }
+
+ #endregion
+
+ #region API
+
+ ///
+ /// Метод для изменения стака аллергии на относительное значение.
+ ///
+ ///
+ ///
+ ///
+ public void AdjustAllergyStack(EntityUid uid, float relativeStack, AllergicComponent? allergic = null)
+ {
+ if (!Resolve(uid, ref allergic))
+ return;
+
+ SetAllergyStack(uid, allergic.AllergyStack + relativeStack, allergic);
+ }
+
+ ///
+ /// Метод для изменения стака аллергии.
+ ///
+ ///
+ ///
+ ///
+ public void SetAllergyStack(EntityUid uid, float stack, AllergicComponent? allergic = null)
+ {
+ if (!Resolve(uid, ref allergic))
+ return;
+
+ allergic.AllergyStack = MathF.Min(MathF.Max(0, stack), allergic.MaximumStack);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/Content.Server/ADT/Body/AllergicSystem.cs b/Content.Server/ADT/Body/AllergicSystem.cs
new file mode 100644
index 00000000000..ce8c52880ad
--- /dev/null
+++ b/Content.Server/ADT/Body/AllergicSystem.cs
@@ -0,0 +1,117 @@
+using Content.Server.Body.Systems;
+using Content.Shared.ADT.Body.Allergies;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.EntityEffects.Effects;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using System.Linq;
+
+namespace Content.Server.ADT.Body;
+
+public sealed partial class AllergicSystem : EntitySystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+
+ private ProtoId _allergyDamageType = "Poison";
+ private DamageTypePrototype? _allergyDamageTypeProto;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnGetReagentEffects);
+
+ SubscribeLocalEvent(OnAllergyTriggered);
+
+ InitializeShock();
+
+ _allergyDamageTypeProto = _proto.Index(_allergyDamageType);
+ }
+
+ private void OnInit(EntityUid uid, AllergicComponent component, ComponentInit args)
+ {
+ component.Triggers = GetRandomAllergies(component.Min, component.Max);
+ }
+
+ ///
+ /// Метод для обработки метаболизма реагента.
+ /// При наличии аллергена, добавляет эффект изменения здоровья и вызывает AllergyTriggeredEvent.
+ ///
+ ///
+ ///
+ ///
+ private void OnGetReagentEffects(EntityUid uid, AllergicComponent component, ref GetReagentEffectsEvent ev)
+ {
+ if (!component.Triggers.Contains(ev.Reagent.Prototype))
+ return;
+
+ var damageSpecifier = new DamageSpecifier(_allergyDamageTypeProto!, 0.25);
+ var damageEffect = new HealthChange
+ {
+ Damage = damageSpecifier
+ };
+
+ AllergyTriggeredEvent triggered = new();
+ RaiseLocalEvent(uid, ref triggered);
+
+ ev.Effects = ev.Effects.Append(damageEffect).ToArray();
+ }
+
+ private void OnAllergyTriggered(EntityUid uid, AllergicComponent allergic, ref AllergyTriggeredEvent ev)
+ {
+ IncrementStackOnTrigger(uid, allergic, ref ev);
+ }
+
+ ///
+ /// Метод для получения рандомных аллергенов.
+ ///
+ /// Максимальное кол-во
+ /// Минимальное кол-во
+ ///
+ private List> GetRandomAllergies(int min, int max)
+ {
+ int reagentsCount = _proto.Count();
+
+ if (reagentsCount == 0)
+ return new();
+
+ int safeMin = Math.Clamp(min, 0, reagentsCount);
+ int safeMax = Math.Clamp(max, safeMin, reagentsCount);
+ int allergiesCount = _random.Next(safeMin, safeMax + 1);
+
+ var picked = new HashSet();
+ while (picked.Count < allergiesCount)
+ {
+ picked.Add(_random.Next(reagentsCount));
+ }
+
+ int index = 0;
+ List> allergies = new();
+
+ foreach (var proto in _proto.EnumeratePrototypes())
+ {
+ if (picked.Contains(index))
+ allergies.Add(proto.ID);
+ index++;
+ }
+
+ return allergies;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out var allergic))
+ {
+ UpdateStack(uid, allergic);
+ UpdateShock(uid, allergic);
+ }
+ }
+}
diff --git a/Content.Server/ADT/EntityEffects/AdjustAllergicStackEffectSystem.cs b/Content.Server/ADT/EntityEffects/AdjustAllergicStackEffectSystem.cs
new file mode 100644
index 00000000000..47f858e70a8
--- /dev/null
+++ b/Content.Server/ADT/EntityEffects/AdjustAllergicStackEffectSystem.cs
@@ -0,0 +1,19 @@
+using Content.Server.ADT.Body;
+using Content.Shared.ADT.Body.Allergies;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects;
+
+namespace Content.Server.ADT.EntityEffects;
+
+///
+/// Эффект для изменения значения стака аллергии.
+///
+public sealed partial class AdjustAllergicStackEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private AllergicSystem _allergic = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ _allergic.AdjustAllergyStack(entity, args.Effect.Amount * args.Scale);
+ }
+}
diff --git a/Content.Server/ADT/Medical/EntitySystems/DiseaseDiagnoserSystem.cs b/Content.Server/ADT/Medical/EntitySystems/DiseaseDiagnoserSystem.cs
new file mode 100644
index 00000000000..b0f65efc5c3
--- /dev/null
+++ b/Content.Server/ADT/Medical/EntitySystems/DiseaseDiagnoserSystem.cs
@@ -0,0 +1,172 @@
+using System.Linq;
+using System.Text;
+using Content.Server.Botany;
+using Content.Server.Power.EntitySystems;
+using Content.Shared.ADT.Medical;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Interaction;
+using Content.Shared.Paper;
+using Content.Shared.Popups;
+using Content.Shared.Power;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Server.ADT.Medical.EntitySystems;
+
+public sealed class DiseaseDiagnoserSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly PaperSystem _paperSystem = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnActivateInWorld);
+ SubscribeLocalEvent(OnRemoveAttempt);
+ SubscribeLocalEvent(OnPowerChanged);
+ }
+
+ #region Events
+
+ private void OnActivateInWorld(Entity entity, ref ActivateInWorldEvent args)
+ {
+ if (args.Handled || !args.Complex)
+ return;
+
+ TryStart(entity, args.User);
+ args.Handled = true;
+ }
+
+ private void OnRemoveAttempt(Entity ent, ref ContainerIsRemovingAttemptEvent args)
+ {
+ if (args.Container.ID == ent.Comp.ContainerId && ent.Comp.Working)
+ args.Cancel();
+ }
+
+ private void OnPowerChanged(Entity ent, ref PowerChangedEvent args)
+ {
+ if (!args.Powered)
+ Stop(ent);
+ }
+
+ #endregion
+
+ #region Lifecycle
+
+ public void TryStart(Entity entity, EntityUid? user)
+ {
+ var (uid, comp) = entity;
+ if (comp.Working)
+ return;
+
+ if (!HasPower(entity))
+ {
+ if (user != null)
+ _popup.PopupClient(Loc.GetString("solution-container-mixer-no-power"), entity, user.Value);
+ return;
+ }
+
+ if (!_container.TryGetContainer(uid, comp.ContainerId, out var container) || container.Count == 0)
+ {
+ if (user != null)
+ _popup.PopupClient(Loc.GetString("solution-container-mixer-popup-nothing-to-mix"), entity, user.Value);
+ return;
+ }
+
+ comp.Working = true;
+ comp.WorkingSoundEntity = _audio.PlayPvs(comp.WorkingSound, entity, comp.WorkingSound?.Params.WithLoop(true));
+ comp.WorkTimeEnd = _timing.CurTime + comp.WorkDuration;
+ _appearance.SetData(entity, DiseaseDiagnoserVisuals.Printing, true);
+ Dirty(uid, comp);
+ }
+
+ public void Stop(Entity entity)
+ {
+ var (uid, comp) = entity;
+ if (!comp.Working)
+ return;
+ _audio.Stop(comp.WorkingSoundEntity);
+ _appearance.SetData(entity, DiseaseDiagnoserVisuals.Printing, false);
+ comp.Working = false;
+ comp.WorkingSoundEntity = null;
+ Dirty(uid, comp);
+ }
+
+ public string GetReport(BotanySwabComponent swabComponent)
+ {
+ StringBuilder builder = new();
+
+ if (swabComponent.AllergicTriggers == null || swabComponent.AllergicTriggers.Count() == 0)
+ builder.Append(Loc.GetString("paper-allergy-prefix", ("allergies", Loc.GetString("paper-allergy-no"))));
+ else
+ {
+ List localizedNames = new();
+ foreach (ProtoId trigger in swabComponent.AllergicTriggers)
+ {
+ localizedNames.Add(_proto.Index(trigger).LocalizedName.ToLower());
+ }
+
+ builder.Append(Loc.GetString("paper-allergy-prefix", ("allergies", String.Join(", ", localizedNames))));
+ }
+
+ return builder.ToString();
+ }
+
+ public void Finish(Entity entity)
+ {
+ var (uid, comp) = entity;
+ if (!comp.Working)
+ return;
+ Stop(entity);
+
+ if (!TryComp(entity, out var reactionMixer)
+ || !_container.TryGetContainer(uid, comp.ContainerId, out var container))
+ return;
+
+ if (container.ContainedEntities.Count == 0)
+ return;
+
+ var swabEnt = container.ContainedEntities.First();
+ if (!TryComp(swabEnt, out var swabComponent))
+ return;
+
+ var printed = Spawn("DiagnosisReportPaper", Transform(uid).Coordinates);
+
+ if (TryComp(printed, out var paper))
+ {
+ _paperSystem.SetContent((printed, paper), GetReport(swabComponent));
+ }
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var comp))
+ {
+ if (!comp.Working)
+ continue;
+
+ if (_timing.CurTime < comp.WorkTimeEnd)
+ continue;
+
+ Finish((uid, comp));
+ }
+ }
+
+ #endregion
+
+ public bool HasPower(Entity entity)
+ {
+ return this.IsPowered(entity, EntityManager);
+ }
+}
diff --git a/Content.Server/Body/Systems/MetabolizerSystem.cs b/Content.Server/Body/Systems/MetabolizerSystem.cs
index b5b30ae74cf..116af09fafe 100644
--- a/Content.Server/Body/Systems/MetabolizerSystem.cs
+++ b/Content.Server/Body/Systems/MetabolizerSystem.cs
@@ -194,8 +194,15 @@ private void TryMetabolize(Entity
public SeedData? SeedData;
+
+ ///
+ /// Allergic triggers from players swabbed.
+ ///
+ // ADT-Tweak-Start
+ [DataField]
+ public List>? AllergicTriggers;
+ // ADT-Tweak-End
}
}
diff --git a/Content.Server/Botany/Systems/BotanySwabSystem.cs b/Content.Server/Botany/Systems/BotanySwabSystem.cs
index e8c7af92c27..abcf09db9e1 100644
--- a/Content.Server/Botany/Systems/BotanySwabSystem.cs
+++ b/Content.Server/Botany/Systems/BotanySwabSystem.cs
@@ -1,5 +1,8 @@
+using System.Linq;
using Content.Server.Botany.Components;
using Content.Server.Popups;
+using Content.Shared.ADT.Body.Allergies;
+using Content.Shared.Body.Components;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Interaction;
@@ -21,6 +24,16 @@ public override void Initialize()
SubscribeLocalEvent(OnDoAfter);
}
+ // ADT-Tweak-Start
+ ///
+ /// If swab was used.
+ ///
+ private bool IsUsed(BotanySwabComponent swab)
+ {
+ return swab.SeedData != null || swab.AllergicTriggers != null;
+ }
+ // ADT-Tweak-End
+
///
/// This handles swab examination text
/// so you can tell if they are used or not.
@@ -29,7 +42,7 @@ private void OnExamined(EntityUid uid, BotanySwabComponent swab, ExaminedEvent a
{
if (args.IsInDetailsRange)
{
- if (swab.SeedData != null)
+ if (IsUsed(swab)) // ADT-Tweak: IsUsed
args.PushMarkup(Loc.GetString("swab-used"));
else
args.PushMarkup(Loc.GetString("swab-unused"));
@@ -41,9 +54,28 @@ private void OnExamined(EntityUid uid, BotanySwabComponent swab, ExaminedEvent a
///
private void OnAfterInteract(EntityUid uid, BotanySwabComponent swab, AfterInteractEvent args)
{
- if (args.Target == null || !args.CanReach || !HasComp(args.Target))
+ // ADT-Tweak-Start
+ bool isTargetPlant = HasComp(args.Target);
+ bool isTargetBody = HasComp(args.Target);
+ // ADT-Tweak-End
+
+ if (args.Target == null || !args.CanReach || !isTargetPlant && !isTargetBody) // ADT-Tweak
return;
+ // ADT-Tweak-Start
+ if (isTargetPlant && swab.AllergicTriggers != null)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("botany-swab-unusable-plant"), args.User, args.User);
+ return;
+ }
+
+ if (isTargetBody && swab.SeedData != null)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("botany-swab-unusable-bio"), args.User, args.User);
+ return;
+ }
+ // ADT-Tweak-End
+
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, swab.SwabDelay, new BotanySwabDoAfterEvent(), uid, target: args.Target, used: uid)
{
Broadcast = true,
@@ -52,19 +84,17 @@ private void OnAfterInteract(EntityUid uid, BotanySwabComponent swab, AfterInter
});
}
+ // ADT-Tweak-Start
///
/// Save seed data or cross-pollenate.
///
- private void OnDoAfter(EntityUid uid, BotanySwabComponent swab, DoAfterEvent args)
+ private void OnPlantDoAfter(EntityUid uid, BotanySwabComponent swab, PlantHolderComponent plant, DoAfterEvent args)
{
- if (args.Cancelled || args.Handled || !TryComp(args.Args.Target, out var plant))
- return;
-
if (swab.SeedData == null)
{
// Pick up pollen
swab.SeedData = plant.Seed;
- _popupSystem.PopupEntity(Loc.GetString("botany-swab-from"), args.Args.Target.Value, args.Args.User);
+ _popupSystem.PopupEntity(Loc.GetString("botany-swab-from"), args.Args.Target!.Value, args.Args.User);
}
else
{
@@ -73,8 +103,45 @@ private void OnDoAfter(EntityUid uid, BotanySwabComponent swab, DoAfterEvent arg
return;
plant.Seed = _mutationSystem.Cross(swab.SeedData, old); // Cross-pollenate
swab.SeedData = old; // Transfer old plant pollen to swab
- _popupSystem.PopupEntity(Loc.GetString("botany-swab-to"), args.Args.Target.Value, args.Args.User);
+ _popupSystem.PopupEntity(Loc.GetString("botany-swab-to"), args.Args.Target!.Value, args.Args.User);
}
+ }
+ // ADT-Tweak-End
+
+ // ADT-Tweak-Start
+ ///
+ /// Save allergy triggers if exists.
+ ///
+ private void OnCreatureDoAfter(EntityUid uid, BotanySwabComponent swab, DoAfterEvent args)
+ {
+ if (TryComp(args.Args.Target!.Value, out var allergic))
+ if (swab.AllergicTriggers == null)
+ swab.AllergicTriggers = allergic.Triggers.ToList();
+ else
+ /** Snowball unique allergies on swab */
+ swab.AllergicTriggers = swab.AllergicTriggers.Concat(allergic.Triggers).Distinct().ToList();
+
+ /** Mark as used anyway */
+ if (swab.AllergicTriggers == null)
+ swab.AllergicTriggers = new();
+
+ _popupSystem.PopupEntity(Loc.GetString("botany-swab-used-bio"), args.Args.User, args.Args.User);
+ }
+ // ADT-Tweak-End
+
+ private void OnDoAfter(EntityUid uid, BotanySwabComponent swab, DoAfterEvent args)
+ {
+ if (args.Cancelled || args.Handled)
+ return;
+
+ // ADT-Tweak-Start
+ if (TryComp(args.Args.Target, out var plant))
+ OnPlantDoAfter(uid, swab, plant, args);
+ else if (HasComp(args.Args.Target))
+ OnCreatureDoAfter(uid, swab, args);
+ else
+ return;
+ // ADT-Tweak-End
args.Handled = true;
}
diff --git a/Content.Shared/ADT/Body/Allergies/AllergicComponent.cs b/Content.Shared/ADT/Body/Allergies/AllergicComponent.cs
new file mode 100644
index 00000000000..b4e55190ed9
--- /dev/null
+++ b/Content.Shared/ADT/Body/Allergies/AllergicComponent.cs
@@ -0,0 +1,87 @@
+using Content.Shared.Chemistry.Reagent;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.ADT.Body.Allergies;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class AllergicComponent : Component
+{
+ ///
+ /// Список реагентов-аллергенов.
+ ///
+ [DataField]
+ public List> Triggers = new();
+
+ ///
+ /// Время следующего события анафилактического шока.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ [DataField]
+ public TimeSpan NextShockEvent = TimeSpan.Zero;
+
+ ///
+ /// Минимальное время в секундах до следующего события анафилактического шока.
+ ///
+ [DataField]
+ public float MinimumTimeTilNextShockEvent = 5f;
+
+ ///
+ /// Максимальное время в секундах до следующего события анафилактического шока.
+ ///
+ [DataField]
+ public float MaximumTimeTilNextShockEvent = 10f;
+
+ ///
+ /// Стак аллергии.
+ ///
+ [DataField]
+ public float AllergyStack = 0f;
+
+ ///
+ /// Максимальный размер стака аллергии.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
+ public float MaximumStack = 50f;
+
+ ///
+ /// Значение затухания (декремента) стака.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
+ public float StackFade = -0.2f;
+
+ ///
+ /// Скорость затухания (декремента) стака.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
+ public TimeSpan StackFadeRate = TimeSpan.FromSeconds(5);
+
+ ///
+ /// Время следующего декремента стака.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ [DataField]
+ public TimeSpan NextStackFade = TimeSpan.Zero;
+
+ ///
+ /// Значение роста стака при метаболизме аллергена.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
+ public float StackGrow = 0.5f;
+
+ ///
+ /// Минимальное количество назначенных аллергенов при инициализации компонента.
+ ///
+ [DataField(readOnly: true)]
+ public int Min = 1;
+
+ ///
+ /// Максимальное количество назначенных аллергенов при инициализации компонента.
+ ///
+ [DataField(readOnly: true)]
+ public int Max = 3;
+}
diff --git a/Content.Shared/ADT/Body/Allergies/Prototypes/AllergicReactionPrototype.cs b/Content.Shared/ADT/Body/Allergies/Prototypes/AllergicReactionPrototype.cs
new file mode 100644
index 00000000000..8c3a6a40558
--- /dev/null
+++ b/Content.Shared/ADT/Body/Allergies/Prototypes/AllergicReactionPrototype.cs
@@ -0,0 +1,23 @@
+using Content.Shared.EntityEffects;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.ADT.Body.Allergies.Prototypes;
+
+///
+/// Прототип аллергической реакции.
+///
+/// Определяет, какие эффекты будут назначены при наступлении события аллергического шока,
+/// если стак аллергической реакции у энтити >= StackThreshold.
+///
+[Prototype("allergicReaction")]
+public sealed partial class AllergicReactionPrototype : IPrototype
+{
+ [IdDataField]
+ public string ID { get; private set; } = default!;
+
+ [DataField("effects")]
+ public EntityEffect[] Effects = default!;
+
+ [DataField("threshold", required: true)]
+ public float StackThreshold;
+}
diff --git a/Content.Shared/ADT/EntityEffects/AdjustAllergicStack.cs b/Content.Shared/ADT/EntityEffects/AdjustAllergicStack.cs
new file mode 100644
index 00000000000..d5f23a9a858
--- /dev/null
+++ b/Content.Shared/ADT/EntityEffects/AdjustAllergicStack.cs
@@ -0,0 +1,24 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityEffects.Effects;
+
+///
+public sealed partial class AdjustAllergicStack : EntityEffectBase
+{
+ ///
+ /// Amount we're adjusting stack by.
+ ///
+ [DataField]
+ public float Amount = 0f;
+
+ public override string? EntityEffectGuidebookText(
+ IPrototypeManager prototype,
+ IEntitySystemManager entSys)
+ {
+ return Loc.GetString(
+ "reagent-effect-guidebook-adjust-allergic-stack",
+ ("chance", Probability),
+ ("amount", MathF.Abs(Amount).ToString("0.00")),
+ ("positive", Amount <= 0));
+ }
+}
diff --git a/Content.Shared/ADT/EntityEffects/PurgeAllergies.cs b/Content.Shared/ADT/EntityEffects/PurgeAllergies.cs
new file mode 100644
index 00000000000..589ca06c391
--- /dev/null
+++ b/Content.Shared/ADT/EntityEffects/PurgeAllergies.cs
@@ -0,0 +1,50 @@
+using Content.Shared.ADT.Body.Allergies;
+using Content.Shared.Body.Components;
+using Content.Shared.Chemistry.Components.SolutionManager;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Reagent;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityEffects.Effects;
+
+///
+/// Эффект для удаления аллергий, аллергены которых присутствуют в крови.
+///
+public sealed partial class PurgeAllergiesEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (!TryComp(entity, out var bloodstreamComp))
+ return;
+
+ if (!TryComp(entity, out var solMan))
+ return;
+
+ (EntityUid uid, _) = entity;
+ ref List> allergicTriggers = ref entity.Comp.Triggers;
+
+ if (_solutionContainerSystem.TryGetSolution((uid, solMan), BloodstreamComponent.DefaultChemicalsSolutionName, out _, out var chemicalsSolution))
+ {
+ foreach (var (reagent, _) in chemicalsSolution.Contents)
+ {
+ if (allergicTriggers.Contains(reagent.Prototype))
+ allergicTriggers.Remove(reagent.Prototype);
+ }
+ }
+ }
+}
+
+///
+public sealed partial class PurgeAllergies : EntityEffectBase
+{
+ public override string? EntityEffectGuidebookText(
+ IPrototypeManager prototype,
+ IEntitySystemManager entSys)
+ {
+ return Loc.GetString(
+ "reagent-effect-guidebook-purge-allergies",
+ ("chance", Probability));
+ }
+}
\ No newline at end of file
diff --git a/Content.Shared/ADT/Medical/DiseaseDiagnoserComponent.cs b/Content.Shared/ADT/Medical/DiseaseDiagnoserComponent.cs
new file mode 100644
index 00000000000..09bbfde97b7
--- /dev/null
+++ b/Content.Shared/ADT/Medical/DiseaseDiagnoserComponent.cs
@@ -0,0 +1,35 @@
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Components;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.ADT.Medical;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class DiseaseDiagnoserComponent : Component
+{
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public string ContainerId = "swab";
+
+ [DataField, AutoNetworkedField]
+ public bool Working;
+
+ [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+ public TimeSpan WorkDuration;
+
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+ public TimeSpan WorkTimeEnd;
+
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier? WorkingSound;
+
+ [ViewVariables]
+ public Entity? WorkingSoundEntity;
+}
+
+[Serializable, NetSerializable]
+public enum DiseaseDiagnoserVisuals : byte
+{
+ Printing
+}
\ No newline at end of file
diff --git a/Resources/Locale/ru-RU/ADT/allergies/allergies.ftl b/Resources/Locale/ru-RU/ADT/allergies/allergies.ftl
new file mode 100644
index 00000000000..e2212c312e2
--- /dev/null
+++ b/Resources/Locale/ru-RU/ADT/allergies/allergies.ftl
@@ -0,0 +1,9 @@
+health-analyzer-window-entity-allergy-text = Имеется аллергия
+
+botany-swab-unusable-plant = Палочка непригодна для сбора пыльцы.
+botany-swab-unusable-bio = Палочка непригодна для взятия биологических образцов.
+
+botany-swab-used-bio = Вы собрали биологические образцы.
+
+paper-allergy-prefix = Аллергия: {$allergies}
+paper-allergy-no = нет
\ No newline at end of file
diff --git a/Resources/Locale/ru-RU/ADT/guidebook/chemistry/effects.ftl b/Resources/Locale/ru-RU/ADT/guidebook/chemistry/effects.ftl
index 417ff91e27e..820f3a36770 100644
--- a/Resources/Locale/ru-RU/ADT/guidebook/chemistry/effects.ftl
+++ b/Resources/Locale/ru-RU/ADT/guidebook/chemistry/effects.ftl
@@ -8,9 +8,28 @@ reagent-effect-guidebook-teleport =
[1] Вызвать
*[other] вызвать
} неконтролируемое перемещение
-
+reagent-effect-guidebook-purge-allergies =
+ { $chance ->
+ [1] Лечит
+ *[other] лечит
+ } аллергию
+reagent-effect-guidebook-adjust-allergic-stack =
+ { $chance ->
+ [1] { $positive ->
+ [true] Ослабляет
+ *[false] Ухудшает
+ }
+ *[other] { $positive ->
+ [true] ослабляет
+ *[false] ухудшает
+ }
+ } симптомы аллергии на {
+ $positive ->
+ [true] [color=green]{$amount}[/color]
+ *[false] [color=red]{$amount}[/color]
+ } ед.
reagent-effect-guidebook-wash-stamp-reaction =
{ $chance ->
[1] Смывает
*[other] смывают
- } печати с лица
\ No newline at end of file
+ } печати с лица
diff --git a/Resources/Locale/ru-RU/ADT/reagents/meta/medicine.ftl b/Resources/Locale/ru-RU/ADT/reagents/meta/medicine.ftl
index 2bec1b3a9bb..3ee8ef5e116 100644
--- a/Resources/Locale/ru-RU/ADT/reagents/meta/medicine.ftl
+++ b/Resources/Locale/ru-RU/ADT/reagents/meta/medicine.ftl
@@ -55,5 +55,8 @@ reagent-desc-miadinol = Хорошо помогает восстановить
reagent-name-devazigon = Девазигон
reagent-desc-devazigon = Антитоксин выведенный на основе нейротоксина, используется в устранении последствий сильного отравления.
+reagent-name-sanallergium = Саналлергин
+reagent-desc-sanallergium = Используется для перманентного снижения иммунной реакции пациента к аллергенам в крови.
+
reagent-name-diphalazidone = дифалазидон
reagent-desc-diphalazidone = Экспериментальный криогенный препарат, используемый для нестабильной регенерации гниющих тканей и мозгового вещества при помощи радиации.
diff --git a/Resources/Locale/ru-RU/ADT/traits/disabilities.ftl b/Resources/Locale/ru-RU/ADT/traits/disabilities.ftl
index 70199c66512..1d4740071f3 100644
--- a/Resources/Locale/ru-RU/ADT/traits/disabilities.ftl
+++ b/Resources/Locale/ru-RU/ADT/traits/disabilities.ftl
@@ -18,3 +18,6 @@ trait-unadapted-to-space-desc = Из-за особого строения ваш
trait-touchy-name = Зрительная агнозия
trait-touchy-desc = Вы определяете вещи и их внешний вид на ощупь. (examine работает только с теми предметами которые в пределах вашей досягаемости)
+
+trait-allergic-name = Аллергик
+trait-allergic-desc = У вас есть аллергия.
diff --git a/Resources/Prototypes/ADT/Medical/allergy.yml b/Resources/Prototypes/ADT/Medical/allergy.yml
new file mode 100644
index 00000000000..ace0035ff5d
--- /dev/null
+++ b/Resources/Prototypes/ADT/Medical/allergy.yml
@@ -0,0 +1,55 @@
+- type: allergicReaction
+ id: mildAllergicReaction
+ threshold: 5
+ effects:
+ - !type:Jitter
+ time: 5
+ probability: 0.3
+ - !type:Emote
+ emote: Cough
+ showInChat: true
+ probability: 0.3
+
+- type: allergicReaction
+ id: midAllergicReaction
+ threshold: 15
+ effects:
+ - !type:Jitter
+ time: 5
+ - !type:HealthChange
+ damage:
+ groups:
+ Airloss: -3
+ probability: 0.5
+ - !type:Emote
+ emote: Cough
+ showInChat: true
+ probability: 0.7
+
+- type: allergicReaction
+ id: highAllergicReaction
+ threshold: 25
+ effects:
+ - !type:Jitter
+ time: 5
+ - !type:HealthChange
+ damage:
+ groups:
+ Airloss: -5
+ - !type:Emote
+ emote: Cough
+ showInChat: true
+
+- type: allergicReaction
+ id: itsOverAllergicReaction
+ threshold: 35
+ effects:
+ - !type:Jitter
+ time: 5
+ - !type:HealthChange
+ damage:
+ groups:
+ Airloss: -10
+ - !type:Emote
+ emote: Cough
+ showInChat: true
diff --git a/Resources/Prototypes/ADT/Reagents/medicine.yml b/Resources/Prototypes/ADT/Reagents/medicine.yml
index c9722ba5528..49b54070290 100644
--- a/Resources/Prototypes/ADT/Reagents/medicine.yml
+++ b/Resources/Prototypes/ADT/Reagents/medicine.yml
@@ -643,6 +643,24 @@
Cold: 2
Bloodloss: 1
+- type: reagent
+ id: ADTSanallergium # Latin "Allegy healer" (source: ChatGPT)
+ name: reagent-name-sanallergium
+ group: Medicine
+ desc: reagent-desc-sanallergium
+ flavor: medicine
+ physicalDesc: reagent-physical-desc-skunky
+ color: "#7ba854"
+ worksOnTheDead: true
+ metabolisms:
+ Medicine:
+ effects:
+ - !type:PurgeAllergies
+ conditions:
+ - !type:ReagentCondition
+ reagent: ADTSanallergium
+ min: 10
+
- type: reagent
id: ADTDiphalazidone
name: reagent-name-diphalazidone
diff --git a/Resources/Prototypes/ADT/Recipes/Reactions/medicine.yml b/Resources/Prototypes/ADT/Recipes/Reactions/medicine.yml
index ffe6cebd15b..b0de8999e6c 100644
--- a/Resources/Prototypes/ADT/Recipes/Reactions/medicine.yml
+++ b/Resources/Prototypes/ADT/Recipes/Reactions/medicine.yml
@@ -254,6 +254,16 @@
products:
ADTDevazigon: 3
+- type: reaction
+ id: ADTSanallergium
+ reactants:
+ Diphenhydramine:
+ amount: 2
+ Dylovene:
+ amount: 1
+ products:
+ ADTSanallergium: 3
+
- type: reaction
id: ADTDiphalazidone
minTemp: 400
diff --git a/Resources/Prototypes/ADT/Traits/disabilities.yml b/Resources/Prototypes/ADT/Traits/disabilities.yml
index 2fb770f1f9f..483a3a6734e 100644
--- a/Resources/Prototypes/ADT/Traits/disabilities.yml
+++ b/Resources/Prototypes/ADT/Traits/disabilities.yml
@@ -82,3 +82,18 @@
quirk: true
speciesBlacklist:
- IPC
+
+- type: trait
+ id: Allergic
+ name: trait-allergic-name
+ description: trait-allergic-desc
+ category: Quirks
+ components:
+ - type: Allergic
+ cost: -1
+ quirk: true
+ speciesBlacklist:
+ - NovakidSpecies
+ - Diona
+ - SlimePerson
+ - IPC
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Structures/Machines/Medical/disease_diagnoser.yml b/Resources/Prototypes/Entities/Structures/Machines/Medical/disease_diagnoser.yml
index ad98f47e36a..56b0f04ede1 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/Medical/disease_diagnoser.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/Medical/disease_diagnoser.yml
@@ -17,6 +17,33 @@
- type: Appearance
- type: Machine
board: DiagnoserMachineCircuitboard
+ # ADT-Tweak-Start
+ - type: GenericVisualizer
+ visuals:
+ enum.DiseaseDiagnoserVisuals.Printing:
+ enum.DiseaseMachineVisualLayers.IsRunning:
+ True: {state: "running"}
+ False: {state: "icon"}
+ enum.PowerDeviceVisuals.Powered:
+ enum.DiseaseMachineVisualLayers.IsOn:
+ True: { visible: True }
+ False: { visible: False }
+ - type: ItemSlots
+ slots:
+ swab:
+ whitelist:
+ components:
+ - BotanySwab
+ - type: ContainerContainer
+ containers:
+ swab: !type:ContainerSlot
+ - type: DiseaseDiagnoser
+ workDuration: 5
+ workingSound:
+ path: /Audio/Machines/spinning.ogg
+ params:
+ volume: -4
+ # ADT-Tweak-End
- type: entity
name: disease diagnoser report
diff --git a/Resources/Prototypes/Reagents/medicine.yml b/Resources/Prototypes/Reagents/medicine.yml
index f6144ce3fc5..e87f05b4444 100644
--- a/Resources/Prototypes/Reagents/medicine.yml
+++ b/Resources/Prototypes/Reagents/medicine.yml
@@ -97,6 +97,10 @@
damage:
types:
Poison: -3
+ # ADT-Tweak-Start
+ - !type:AdjustAllergicStack
+ amount: -1
+ # ADT-Tweak-End
- type: reagent
id: Ethylredoxrazine