diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj
index 183e06394f9f..0b372159d220 100644
--- a/Content.Client/Content.Client.csproj
+++ b/Content.Client/Content.Client.csproj
@@ -26,5 +26,13 @@
+
+
+
+
+
+ MSBuild:Compile
+
+
diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index 4d710806f9c7..0afe3fd26a3d 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -164,6 +164,7 @@ public static class IgnoredComponents
"PressureSiphon",
"PipeHeater",
"AtmosDevice",
+ "Sanity",
"SignalReceiver",
"SignalSwitch",
"SignalTransmitter",
diff --git a/Content.Client/Sanity/ClientMobSanityComponent.cs b/Content.Client/Sanity/ClientMobSanityComponent.cs
new file mode 100644
index 000000000000..1f913de2756e
--- /dev/null
+++ b/Content.Client/Sanity/ClientMobSanityComponent.cs
@@ -0,0 +1,13 @@
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Content.Shared.Sanity;
+using Content.Client.Sanity.UI;
+
+namespace Content.Client.Sanity
+{
+ public class ClientMobSanityComponent : SharedMobSanityComponent
+ {
+ }
+}
diff --git a/Content.Client/Sanity/ClientSanitySystem.cs b/Content.Client/Sanity/ClientSanitySystem.cs
new file mode 100644
index 000000000000..0140a717e039
--- /dev/null
+++ b/Content.Client/Sanity/ClientSanitySystem.cs
@@ -0,0 +1,34 @@
+using Content.Shared.Sanity;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using System;
+using System.Collections.Generic;
+using Content.Client.Sanity.UI;
+using Robust.Shared.Network;
+using Robust.Shared.Players;
+using Robust.Client.Player;
+using Robust.Shared.IoC;
+
+namespace Content.Client.Sanity
+{
+ public sealed class ClientSanitySystem : SharedSanitySystem
+ {
+ [Dependency] IPlayerManager _playerManager = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+
+
+ public void NotifyUI()
+ {
+ EntityUid? playerUID = _playerManager?.LocalPlayer?.ControlledEntity?.Uid;
+ if (playerUID is not null)
+ {
+ RaiseNetworkEvent(new SanityCloseUI((EntityUid)playerUID));
+ }
+ }
+
+ }
+}
diff --git a/Content.Client/Sanity/UI/SanityInterface.cs b/Content.Client/Sanity/UI/SanityInterface.cs
new file mode 100644
index 000000000000..86bfc18d69dc
--- /dev/null
+++ b/Content.Client/Sanity/UI/SanityInterface.cs
@@ -0,0 +1,59 @@
+using Robust.Client.GameObjects;
+using Robust.Shared.GameObjects;
+using Content.Client.Sanity;
+using Content.Shared.Sanity;
+using System;
+
+namespace Content.Client.Sanity.UI
+{
+ ///
+ ///
+ ///
+ public class SanityBoundUserInterface : BoundUserInterface
+ {
+ private SanityWindow? _window;
+
+ public SanityBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new SanityWindow();
+ if (State != null)
+ UpdateState(State);
+
+ _window.OpenCentered();
+
+ _window.OnClose += Close;
+ _window.OnClose += NotifySystem;
+
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+ if (_window == null || state is not SanityBoundUserInterfaceState cast)
+ return;
+
+ _window.UpdateData(cast.Sanity);
+ }
+
+ protected void NotifySystem()
+ {
+ EntitySystem.Get().NotifyUI();
+ }
+
+
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing) return;
+ _window?.Dispose();
+ }
+ }
+
+}
diff --git a/Content.Client/Sanity/UI/SanityWindow.xaml b/Content.Client/Sanity/UI/SanityWindow.xaml
new file mode 100644
index 000000000000..9c708f81e579
--- /dev/null
+++ b/Content.Client/Sanity/UI/SanityWindow.xaml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/Content.Client/Sanity/UI/SanityWindow.xaml.cs b/Content.Client/Sanity/UI/SanityWindow.xaml.cs
new file mode 100644
index 000000000000..8f322e448c19
--- /dev/null
+++ b/Content.Client/Sanity/UI/SanityWindow.xaml.cs
@@ -0,0 +1,27 @@
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+using Robust.Client.GameObjects;
+using Robust.Client.GameStates;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Client.Utility;
+
+
+
+namespace Content.Client.Sanity.UI
+{
+ [GenerateTypedNameReferences]
+ public partial class SanityWindow : SS14Window
+ {
+ public SanityWindow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void UpdateData(int sanity)
+ {
+ SanityLineEdit.Text = sanity.ToString();
+ }
+ }
+}
diff --git a/Content.Server/Alert/Click/SanityMenu.cs b/Content.Server/Alert/Click/SanityMenu.cs
new file mode 100644
index 000000000000..7a032925c0b7
--- /dev/null
+++ b/Content.Server/Alert/Click/SanityMenu.cs
@@ -0,0 +1,25 @@
+using Content.Shared.Alert;
+using JetBrains.Annotations;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Content.Server.Sanity;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Network;
+using Robust.Server.Player;
+using Robust.Server.GameObjects;
+
+namespace Content.Server.Alert.Click
+{
+ [UsedImplicitly]
+ [DataDefinition]
+ public class SanityMenu : IAlertClick
+ {
+ public void AlertClicked(ClickAlertEventArgs args)
+ {
+
+ if (args.Player.TryGetComponent(out MobSanityComponent? sanityComponent) && args.Player.TryGetComponent(out ActorComponent? actorComponent))
+ {
+ EntitySystem.Get().OpenUI(sanityComponent, actorComponent.PlayerSession);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Sanity/MobSanityComponent.Designer.cs b/Content.Server/Sanity/MobSanityComponent.Designer.cs
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/Content.Server/Sanity/MobSanityComponent.cs b/Content.Server/Sanity/MobSanityComponent.cs
new file mode 100644
index 000000000000..965522faf0e9
--- /dev/null
+++ b/Content.Server/Sanity/MobSanityComponent.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using Content.Server.Alert;
+using Content.Shared.Alert;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Robust.Shared.GameObjects.Components;
+using Robust.Server.GameObjects;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Server.Player;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.ViewVariables;
+using Content.Shared.Sanity;
+using Robust.Shared.Players;
+
+namespace Content.Server.Sanity
+{
+ [RegisterComponent]
+ public class MobSanityComponent : SharedMobSanityComponent
+ {
+
+ ///
+ /// Sanity variables
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public bool Updating = false;
+
+ public int Sanity
+ {
+ get => _sanity;
+ set
+ {
+ if(_sanity + value > MaxSanity)
+ {
+ _sanity = MaxSanity;
+ return;
+ }
+ if(_sanity + value < 1)
+ {
+ _sanity = 0;
+ return;
+ }
+ _sanity = value;
+ }
+ }
+
+ private int _sanity = 0;
+ ///
+ /// How much sanity points are given per second, this rounds itself , but losses are aceptable.
+ ///
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float SanityGainPerSecond = 0.1f;
+ ///
+ /// Maximum sanity to lose per cycle. Can be influenced
+ ///
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int MaxSanityDecayPerCycle = 20;
+ ///
+ /// This divides max sanity to show the sanity hud element sprite, automatically updated when max sanity changes , do not touch.
+ ///
+
+ [ViewVariables(VVAccess.ReadOnly)]
+ public int SanitySteps = 20;
+
+ ///
+ /// This is the maximum sanity , the more a player has the bigger the buffer between the stages becomes
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int MaxSanity
+ {
+ get => _maxSanity;
+ set
+ {
+ _maxSanity = value;
+ // 6 Sprites so far.
+ SanitySteps = _maxSanity / 6;
+ }
+ }
+
+ private int _maxSanity = 100;
+
+ ///
+ /// Insight variables
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int Insight
+ {
+ get => _insight;
+ set
+ {
+ if(_insight + value > RequiredInsight)
+ {
+ InsightPoints += (_insight + value) / RequiredInsight;
+ _insight = (_insight + value) % RequiredInsight;
+ return;
+ }
+ _insight = value;
+ }
+
+ }
+
+
+ private int _insight = 0;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int RequiredInsight = 100;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int InsightPoints = 0;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float PassiveInsightGainPerSecond = 0.75f;
+
+ ///
+ /// Rest variables
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int Rest
+ {
+ get => _rest;
+ set
+ {
+ if(_rest + value > RestRequired)
+ {
+ RestPoints += (_rest + value) / RestRequired;
+ _rest = (_rest + value) % RestRequired;
+ return;
+ }
+ _rest = value;
+ }
+ }
+
+
+ private int _rest = 0;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int RestPoints = 0;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int RestRequired = 100;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float RestGainPerSecond = 0.75f;
+
+
+ }
+}
diff --git a/Content.Server/Sanity/SanitySystem.cs b/Content.Server/Sanity/SanitySystem.cs
new file mode 100644
index 000000000000..9b7f3c225aa4
--- /dev/null
+++ b/Content.Server/Sanity/SanitySystem.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Robust.Server.GameObjects;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.Components;
+using Robust.Server.Placement;
+using Robust.Server.Player;
+using Content.Shared.Alert;
+using Content.Server.Alert;
+using Content.Server.UserInterface;
+using Content.Shared.Sanity;
+using Content.Shared.Eui;
+using Robust.Shared.Players;
+using System.Net;
+using Robust.Shared.Network;
+using JetBrains.Annotations;
+using Content.Shared.Popups;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Interaction;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+
+namespace Content.Server.Sanity
+{
+ public sealed class SanitySystem : SharedSanitySystem
+ {
+ [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
+ public HashSet SanityCompsToTick = new();
+ public float TimeAccumulator = 0.0f;
+ public float TimeBetweenTicks = 15.0f;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnDelete);
+ SubscribeNetworkEvent(CloseUI);
+ }
+
+ public void OnInit(EntityUid uid, MobSanityComponent component, ComponentInit args)
+ {
+ SanityCompsToTick.Add(component);
+ }
+ public void OnDelete(EntityUid uid, MobSanityComponent component, ComponentRemove args)
+ {
+ SanityCompsToTick.Remove(component);
+ }
+
+ public void CloseUI(SanityCloseUI msg)
+ {
+ MobSanityComponent? component = null;
+ if(!Resolve(msg.PlayerUID, ref component))
+ {
+ return;
+ }
+ component.Updating = false;
+ }
+ public void OpenUI(MobSanityComponent component, IPlayerSession player)
+ {
+ component.Updating = true;
+ component.Owner.GetUIOrNull(SanityUiKey.Key)?.Open(player);
+ _userInterfaceSystem.TrySetUiState(component.Owner.Uid, SanityUiKey.Key, new SanityBoundUserInterfaceState(
+ component.Insight, component.Sanity, component.Rest));
+ }
+
+ public override void Update(float frameTime)
+ {
+ TimeAccumulator += frameTime;
+ if (TimeAccumulator < TimeBetweenTicks)
+ {
+ TimeAccumulator += frameTime;
+ return;
+ }
+ TimeAccumulator = 0.0f;
+
+ foreach (MobSanityComponent component in SanityCompsToTick)
+ {
+ component.Sanity += Math.Max((int)(component.SanityGainPerSecond * TimeBetweenTicks), 0);
+ if (!component.Owner.TryGetComponent(out ServerAlertsComponent? alerts))
+ {
+ continue;
+ }
+ alerts.ShowAlert(AlertType.MobSanity, (short)(component.Sanity/component.SanitySteps));
+
+ if(component.Updating)
+ {
+ _userInterfaceSystem.TrySetUiState(component.Owner.Uid, SanityUiKey.Key, new SanityBoundUserInterfaceState(
+ component.Insight, component.Sanity, component.Rest));
+ }
+
+ }
+ }
+ }
+}
+
+
+
diff --git a/Content.Shared/Alert/AlertType.cs b/Content.Shared/Alert/AlertType.cs
index a4962a1d36fa..3e5198030791 100644
--- a/Content.Shared/Alert/AlertType.cs
+++ b/Content.Shared/Alert/AlertType.cs
@@ -1,4 +1,4 @@
-namespace Content.Shared.Alert
+namespace Content.Shared.Alert
{
///
/// Every category of alert. Corresponds to category field in alert prototypes defined in YML
@@ -12,7 +12,8 @@ public enum AlertCategory
Health,
Piloting,
Hunger,
- Thirst
+ Thirst,
+ Sanity
}
///
@@ -51,7 +52,8 @@ public enum AlertType : byte
Debug3,
Debug4,
Debug5,
- Debug6
+ Debug6,
+ MobSanity
}
}
diff --git a/Content.Shared/Alert/IAlertClick.cs b/Content.Shared/Alert/IAlertClick.cs
index 614c9ee6a03c..cafa82c1e139 100644
--- a/Content.Shared/Alert/IAlertClick.cs
+++ b/Content.Shared/Alert/IAlertClick.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Robust.Shared.GameObjects;
namespace Content.Shared.Alert
diff --git a/Content.Shared/Content.Shared.csproj b/Content.Shared/Content.Shared.csproj
index 86e05845c9fa..54e2fd1524ee 100644
--- a/Content.Shared/Content.Shared.csproj
+++ b/Content.Shared/Content.Shared.csproj
@@ -29,6 +29,9 @@
+
+
+
diff --git a/Content.Shared/Sanity/SanityBreakdowns/BreakdownCrying.cs b/Content.Shared/Sanity/SanityBreakdowns/BreakdownCrying.cs
new file mode 100644
index 000000000000..fd536260e99a
--- /dev/null
+++ b/Content.Shared/Sanity/SanityBreakdowns/BreakdownCrying.cs
@@ -0,0 +1,37 @@
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Players;
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
+using Robust.Shared.ViewVariables;
+using System.Collections.Generic;
+using Content.Shared.Construction.Conditions;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.Utility;
+using Content.Shared.Sanity.Prototypes;
+using Content.Shared.Popups;
+
+
+
+namespace Content.Shared.Sanity.Breakdown.Crying
+{
+ public class BreakdownCrying : ISanityBreakdown
+ {
+ public void Start(SanityBreakdownEventArgs args)
+ {
+ args.Player.PopupMessage(args.Player, "You can't take it anymore , you burst into tears");
+ }
+
+ public void End(SanityBreakdownEventArgs args)
+ {
+ args.Player.PopupMessage(args.Player, "You feel the crying stop. You accept things as they are now.");
+ }
+
+ public void ApplyEffects(SanityBreakdownEventArgs args)
+ {
+ args.Player.PopupMessage(args.Player, "Cries");
+ }
+ }
+}
diff --git a/Content.Shared/Sanity/SanityPrototypes.cs b/Content.Shared/Sanity/SanityPrototypes.cs
new file mode 100644
index 000000000000..fd86d368456a
--- /dev/null
+++ b/Content.Shared/Sanity/SanityPrototypes.cs
@@ -0,0 +1,66 @@
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Players;
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
+using Robust.Shared.ViewVariables;
+using System.Collections.Generic;
+using Content.Shared.Construction.Conditions;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Sanity.Prototypes
+{
+ [Serializable, NetSerializable]
+
+ [Prototype("sanitybreakdown")]
+ public class SanityBreakdownPrototype : IPrototype
+ {
+ [DataField("name")]
+ public ISanityBreakdown Name { get; } = default!;
+
+ [DataField("ID", required: true)]
+ public string ID { get; } = default!;
+
+ [DataField("duration")]
+ public float Duration { get; } = 120.0f;
+
+ [DataField("canoccurat")]
+
+ public int CanOccurAt { get; } = 30;
+
+ [DataField("imposeddelay")]
+
+ public float ImposedDelay { get; } = 300.0f;
+
+ [DataField("reoccuring")]
+
+ public bool Reoccuring { get; } = false;
+
+ [DataField("reoccuringdelay")]
+
+ public float ReoccuringInterval { get; } = 10.0f;
+
+
+
+ }
+
+ public interface ISanityBreakdown
+ {
+ void Start(SanityBreakdownEventArgs args)
+ {
+ }
+
+ void End(SanityBreakdownEventArgs args)
+ {
+ }
+
+ void ApplyEffects(SanityBreakdownEventArgs args)
+ {
+
+ }
+ }
+
+}
diff --git a/Content.Shared/Sanity/SharedMobSanityComponent.cs b/Content.Shared/Sanity/SharedMobSanityComponent.cs
new file mode 100644
index 000000000000..48151290160e
--- /dev/null
+++ b/Content.Shared/Sanity/SharedMobSanityComponent.cs
@@ -0,0 +1,82 @@
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Players;
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
+using Robust.Shared.ViewVariables;
+using System.Collections.Generic;
+using Content.Shared.Construction.Conditions;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Sanity
+{
+ public class SharedMobSanityComponent : Component
+ {
+ public override string Name => "Sanity";
+ }
+
+ public class SanityBreakdownEventArgs : EventArgs
+ {
+ ///
+ /// Player that the breakdown is acting upon
+ ///
+ public readonly IEntity Player;
+
+ public SanityBreakdownEventArgs(IEntity player)
+ {
+ Player = player;
+ }
+ }
+}
+
+public enum SanityUiKey
+ {
+ Key,
+ }
+
+ [Serializable, NetSerializable]
+ public class SanityOpenUI : EntityEventArgs
+ {
+ public int Insight { get; }
+ public int Sanity { get; }
+ public int Rest { get; }
+
+ public EntityUid PlayerUID { get; }
+ public SanityOpenUI(int insight, int sanity, int rest, EntityUid playerUID)
+ {
+ Insight = insight;
+ Sanity = sanity;
+ Rest = rest;
+ PlayerUID = playerUID;
+ }
+ }
+ [Serializable, NetSerializable]
+ public class SanityBoundUserInterfaceState : BoundUserInterfaceState
+ {
+ public int Insight { get; }
+ public int Sanity { get; }
+ public int Rest { get; }
+ public SanityBoundUserInterfaceState(int insight, int sanity, int rest)
+ {
+ Insight = insight;
+ Sanity = sanity;
+ Rest = rest;
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public class SanityCloseUI : EntityEventArgs
+ {
+ public EntityUid PlayerUID { get; }
+
+ public SanityCloseUI(EntityUid playerUID)
+ {
+ PlayerUID = playerUID;
+ }
+ }
+}
+
+
diff --git a/Content.Shared/Sanity/SharedSanitySystem.cs b/Content.Shared/Sanity/SharedSanitySystem.cs
new file mode 100644
index 000000000000..16cf138fde96
--- /dev/null
+++ b/Content.Shared/Sanity/SharedSanitySystem.cs
@@ -0,0 +1,8 @@
+using Robust.Shared.GameObjects;
+
+namespace Content.Shared.Sanity
+{
+ public abstract class SharedSanitySystem : EntitySystem
+ {
+ }
+}
diff --git a/Resources/Locale/en-US/sanity/sanity_UI.ftl b/Resources/Locale/en-US/sanity/sanity_UI.ftl
new file mode 100644
index 000000000000..aafe101d694f
--- /dev/null
+++ b/Resources/Locale/en-US/sanity/sanity_UI.ftl
@@ -0,0 +1 @@
+sanity-current-amount = Sanity
diff --git a/Resources/Prototypes/Actions/actions.yml b/Resources/Prototypes/Alerts/Actions/actions.yml
similarity index 100%
rename from Resources/Prototypes/Actions/actions.yml
rename to Resources/Prototypes/Alerts/Actions/actions.yml
diff --git a/Resources/Prototypes/Actions/item_actions.yml b/Resources/Prototypes/Alerts/Actions/item_actions.yml
similarity index 100%
rename from Resources/Prototypes/Actions/item_actions.yml
rename to Resources/Prototypes/Alerts/Actions/item_actions.yml
diff --git a/Resources/Prototypes/Actions/magboots.yml b/Resources/Prototypes/Alerts/Actions/magboots.yml
similarity index 100%
rename from Resources/Prototypes/Actions/magboots.yml
rename to Resources/Prototypes/Alerts/Actions/magboots.yml
diff --git a/Resources/Prototypes/Actions/spells.yml b/Resources/Prototypes/Alerts/Actions/spells.yml
similarity index 100%
rename from Resources/Prototypes/Actions/spells.yml
rename to Resources/Prototypes/Alerts/Actions/spells.yml
diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml
index a4fbd86d5c4b..e0fdf8625498 100644
--- a/Resources/Prototypes/Alerts/alerts.yml
+++ b/Resources/Prototypes/Alerts/alerts.yml
@@ -5,6 +5,7 @@
id: BaseAlertOrder
order:
- category: Health
+ - category: Sanity
- alertType: Fire
- alertType: Handcuffed
- category: Buckled
@@ -231,3 +232,15 @@
icon: /Textures/Interface/Alerts/human_health.rsi/health6.png
name: Debug6
description: Debug
+
+- type: alert
+ alertType: MobSanity
+ category: Sanity
+ onClick: !type:SanityMenu { }
+ icon:
+ sprite: /Textures/Interface/Alerts/sanity.rsi
+ state: sanity
+ name: Sanity
+ description: How nuts are you right now?
+ minSeverity: 0
+ maxSeverity: 5
diff --git a/Resources/Prototypes/Entities/Mobs/Player/human.yml b/Resources/Prototypes/Entities/Mobs/Player/human.yml
index 8f5ef460c7d1..30e4e797f0ee 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/human.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml
@@ -17,6 +17,7 @@
- CombatMode
- Disarm
- HumanScream
+ - type: Sanity
- type: Eye
- type: CameraRecoil
- type: Examiner
diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml
index b7b4e66d8a74..97db1d5bfe72 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/human.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml
@@ -262,6 +262,8 @@
interfaces:
- key: enum.StrippingUiKey.Key
type: StrippableBoundUserInterface
+ - key: enum.SanityUiKey.Key
+ type: SanityBoundUserInterface
- type: Puller
- type: Butcherable
meat: FoodMeat
diff --git a/Resources/Prototypes/Sanity/Breakdowns/BreakdownCrying.yaml b/Resources/Prototypes/Sanity/Breakdowns/BreakdownCrying.yaml
new file mode 100644
index 000000000000..f587ccafaa76
--- /dev/null
+++ b/Resources/Prototypes/Sanity/Breakdowns/BreakdownCrying.yaml
@@ -0,0 +1,8 @@
+
+- type: sanitybreakdown
+ name: !type BreakdownCrying
+ duration: 200f
+ canoccurat: 40
+ imposeddelay: 0
+ reoccuring: true
+ reoccuringinterval: 10f
diff --git a/Resources/Textures/Interface/Alerts/sanity.rsi/meta.json b/Resources/Textures/Interface/Alerts/sanity.rsi/meta.json
new file mode 100644
index 000000000000..a47c0f06b5e4
--- /dev/null
+++ b/Resources/Textures/Interface/Alerts/sanity.rsi/meta.json
@@ -0,0 +1,29 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "https://github.com/discordia-space/CEV-Eris/pull/4028",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "sanity0"
+ },
+ {
+ "name": "sanity1"
+ },
+ {
+ "name": "sanity2"
+ },
+ {
+ "name": "sanity3"
+ },
+ {
+ "name": "sanity4"
+ },
+ {
+ "name": "sanity5"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Interface/Alerts/sanity.rsi/sanity0.png b/Resources/Textures/Interface/Alerts/sanity.rsi/sanity0.png
new file mode 100644
index 000000000000..660a962bec08
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/sanity.rsi/sanity0.png differ
diff --git a/Resources/Textures/Interface/Alerts/sanity.rsi/sanity1.png b/Resources/Textures/Interface/Alerts/sanity.rsi/sanity1.png
new file mode 100644
index 000000000000..455b667762de
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/sanity.rsi/sanity1.png differ
diff --git a/Resources/Textures/Interface/Alerts/sanity.rsi/sanity2.png b/Resources/Textures/Interface/Alerts/sanity.rsi/sanity2.png
new file mode 100644
index 000000000000..8a612392d0b9
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/sanity.rsi/sanity2.png differ
diff --git a/Resources/Textures/Interface/Alerts/sanity.rsi/sanity3.png b/Resources/Textures/Interface/Alerts/sanity.rsi/sanity3.png
new file mode 100644
index 000000000000..9253df845cd5
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/sanity.rsi/sanity3.png differ
diff --git a/Resources/Textures/Interface/Alerts/sanity.rsi/sanity4.png b/Resources/Textures/Interface/Alerts/sanity.rsi/sanity4.png
new file mode 100644
index 000000000000..e12e54236a44
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/sanity.rsi/sanity4.png differ
diff --git a/Resources/Textures/Interface/Alerts/sanity.rsi/sanity5.png b/Resources/Textures/Interface/Alerts/sanity.rsi/sanity5.png
new file mode 100644
index 000000000000..ba61cf1ea30b
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/sanity.rsi/sanity5.png differ