diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index 8fdf47f719..31e5d6c068 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -70,6 +70,7 @@ namespace Content.Client.Entry
"SpeedLoader",
"Hitscan",
"StunOnCollide",
+ "ExaminableDamage",
"RandomPottedPlant",
"Brain",
"CommunicationsConsole",
diff --git a/Content.Server/Damage/Components/ExaminableDamageComponent.cs b/Content.Server/Damage/Components/ExaminableDamageComponent.cs
new file mode 100644
index 0000000000..7f008b79cf
--- /dev/null
+++ b/Content.Server/Damage/Components/ExaminableDamageComponent.cs
@@ -0,0 +1,20 @@
+using Content.Shared.Damage.Prototypes;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Damage.Components;
+
+///
+/// This component shows entity damage severity when it is examined by player.
+///
+[RegisterComponent]
+public class ExaminableDamageComponent : Component
+{
+ public override string Name => "ExaminableDamage";
+
+ [DataField("messages", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))]
+ public string? MessagesProtoId;
+
+ public ExaminableDamagePrototype? MessagesProto;
+}
diff --git a/Content.Server/Damage/Systems/ExaminableDamageSystem.cs b/Content.Server/Damage/Systems/ExaminableDamageSystem.cs
new file mode 100644
index 0000000000..8916c12f82
--- /dev/null
+++ b/Content.Server/Damage/Systems/ExaminableDamageSystem.cs
@@ -0,0 +1,73 @@
+using System.Linq;
+using Content.Server.Damage.Components;
+using Content.Server.Destructible;
+using Content.Server.Destructible.Thresholds.Triggers;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.Examine;
+using Content.Shared.Rounding;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Damage.Systems;
+
+public class ExaminableDamageSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnExamine);
+ }
+
+ private void OnInit(EntityUid uid, ExaminableDamageComponent component, ComponentInit args)
+ {
+ if (component.MessagesProtoId == null)
+ return;
+ component.MessagesProto = _prototype.Index(component.MessagesProtoId);
+ }
+
+ private void OnExamine(EntityUid uid, ExaminableDamageComponent component, ExaminedEvent args)
+ {
+ if (component.MessagesProto == null)
+ return;
+
+ var messages = component.MessagesProto.Messages;
+ if (messages.Length == 0)
+ return;
+
+ var level = GetDamageLevel(uid, component);
+ var msg = Loc.GetString(messages[level]);
+ args.PushMarkup(msg);
+ }
+
+ private int GetDamageLevel(EntityUid uid, ExaminableDamageComponent? component = null,
+ DamageableComponent? damageable = null, DestructibleComponent? destructible = null)
+ {
+ if (!Resolve(uid, ref component, ref damageable, ref destructible))
+ return 0;
+
+ if (component.MessagesProto == null)
+ return 0;
+
+ var maxLevels = component.MessagesProto.Messages.Length - 1;
+ if (maxLevels <= 0)
+ return 0;
+
+ var trigger = (DamageTrigger?) destructible.Thresholds
+ .LastOrDefault(threshold => threshold.Trigger is DamageTrigger)?.Trigger;
+ if (trigger == null)
+ return 0;
+
+ var damage = damageable.TotalDamage;
+ var damageThreshold = trigger.Damage;
+ var fraction = damageThreshold == 0 ? 0f : (float) damage / damageThreshold;
+
+ var level = ContentHelpers.RoundToNearestLevels(fraction, 1, maxLevels);
+ return level;
+ }
+}
diff --git a/Content.Server/Window/WindowComponent.cs b/Content.Server/Window/WindowComponent.cs
index 150e00a0a8..44b5a3333f 100644
--- a/Content.Server/Window/WindowComponent.cs
+++ b/Content.Server/Window/WindowComponent.cs
@@ -1,116 +1,24 @@
using System;
-using Content.Server.Destructible;
-using Content.Server.Destructible.Thresholds.Triggers;
-using Content.Server.Popups;
-using Content.Shared.Audio;
-using Content.Shared.Damage;
-using Content.Shared.Examine;
-using Content.Shared.Interaction;
-using Content.Shared.Rounding;
using Content.Shared.Sound;
using Content.Shared.Window;
-using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Localization;
-using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager.Attributes;
-using Robust.Shared.Timing;
-using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.Window
{
[RegisterComponent]
[ComponentReference(typeof(SharedWindowComponent))]
-#pragma warning disable 618
- public class WindowComponent : SharedWindowComponent, IExamine, IInteractHand
-#pragma warning restore 618
+ public class WindowComponent : SharedWindowComponent
{
- [Dependency] private readonly IEntityManager _entMan = default!;
- [Dependency] private readonly IGameTiming _gameTiming = default!;
-
- [ViewVariables(VVAccess.ReadWrite)] private TimeSpan _lastKnockTime;
-
[DataField("knockDelay")]
[ViewVariables(VVAccess.ReadWrite)]
- private TimeSpan _knockDelay = TimeSpan.FromSeconds(0.5);
-
- [DataField("rateLimitedKnocking")]
- [ViewVariables(VVAccess.ReadWrite)] private bool _rateLimitedKnocking = true;
+ public TimeSpan KnockDelay = TimeSpan.FromSeconds(0.5);
[DataField("knockSound")]
- private SoundSpecifier _knockSound = new SoundPathSpecifier("/Audio/Effects/glass_knock.ogg");
+ public SoundSpecifier KnockSound = new SoundPathSpecifier("/Audio/Effects/glass_knock.ogg");
- void IExamine.Examine(FormattedMessage message, bool inDetailsRange)
- {
- if (!_entMan.TryGetComponent(Owner, out DamageableComponent? damageable) ||
- !_entMan.TryGetComponent(Owner, out DestructibleComponent? destructible))
- {
- return;
- }
-
- var damage = damageable.TotalDamage;
- DamageTrigger? trigger = null;
-
- // TODO: Pretend this does not exist until https://github.com/space-wizards/space-station-14/pull/2783 is merged
- foreach (var threshold in destructible.Thresholds)
- {
- if ((trigger = threshold.Trigger as DamageTrigger) != null)
- {
- break;
- }
- }
-
- if (trigger == null)
- {
- return;
- }
-
- var damageThreshold = trigger.Damage;
- var fraction = damage == 0 || damageThreshold == 0
- ? 0f
- : (float) damage / damageThreshold;
- var level = Math.Min(ContentHelpers.RoundToLevels(fraction, 1, 7), 5);
-
- switch (level)
- {
- case 0:
- message.AddText(Loc.GetString("comp-window-damaged-1"));
- break;
- case 1:
- message.AddText(Loc.GetString("comp-window-damaged-2"));
- break;
- case 2:
- message.AddText(Loc.GetString("comp-window-damaged-3"));
- break;
- case 3:
- message.AddText(Loc.GetString("comp-window-damaged-4"));
- break;
- case 4:
- message.AddText(Loc.GetString("comp-window-damaged-5"));
- break;
- case 5:
- message.AddText(Loc.GetString("comp-window-damaged-6"));
- break;
- }
- }
-
- bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs)
- {
- if (_rateLimitedKnocking && _gameTiming.CurTime < _lastKnockTime + _knockDelay)
- {
- return false;
- }
-
- SoundSystem.Play(
- Filter.Pvs(eventArgs.Target), _knockSound.GetSound(),
- _entMan.GetComponent(eventArgs.Target).Coordinates, AudioHelpers.WithVariation(0.05f));
- eventArgs.Target.PopupMessageEveryone(Loc.GetString("comp-window-knock"));
-
- _lastKnockTime = _gameTiming.CurTime;
-
- return true;
- }
+ [ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan LastKnockTime;
}
}
diff --git a/Content.Server/Window/WindowSystem.cs b/Content.Server/Window/WindowSystem.cs
new file mode 100644
index 0000000000..290d52bcb0
--- /dev/null
+++ b/Content.Server/Window/WindowSystem.cs
@@ -0,0 +1,44 @@
+using Content.Server.Popups;
+using Content.Shared.Audio;
+using Content.Shared.Interaction;
+using Robust.Shared.Audio;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Player;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Window;
+
+public class WindowSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInteractHand);
+ }
+
+ private void OnInteractHand(EntityUid uid, WindowComponent component, InteractHandEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ if (component.KnockDelay.TotalSeconds <= 0)
+ return;
+
+ if (_gameTiming.CurTime < component.LastKnockTime + component.KnockDelay)
+ return;
+
+ SoundSystem.Play(Filter.Pvs(args.Target), component.KnockSound.GetSound(),
+ Transform(args.Target).Coordinates, AudioHelpers.WithVariation(0.05f));
+
+ var msg = Loc.GetString("comp-window-knock");
+ _popupSystem.PopupEntity(msg, uid, Filter.Pvs(uid));
+
+ component.LastKnockTime = _gameTiming.CurTime;
+ args.Handled = true;
+ }
+}
diff --git a/Content.Shared/Damage/Prototypes/ExaminableDamagePrototype.cs b/Content.Shared/Damage/Prototypes/ExaminableDamagePrototype.cs
new file mode 100644
index 0000000000..3112c1fca1
--- /dev/null
+++ b/Content.Shared/Damage/Prototypes/ExaminableDamagePrototype.cs
@@ -0,0 +1,22 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Shared.Damage.Prototypes;
+
+///
+/// Prototype for examinable damage messages.
+///
+[Prototype("examinableDamage")]
+public class ExaminableDamagePrototype : IPrototype
+{
+ [DataField("id", required: true)]
+ public string ID { get; } = default!;
+
+ ///
+ /// List of damage messages IDs sorted by severity.
+ /// First one describes fully intact entity.
+ /// Last one describes almost destroyed.
+ ///
+ [DataField("messages")]
+ public string[] Messages = {};
+}
diff --git a/Resources/Prototypes/Damage/examine_messages.yml b/Resources/Prototypes/Damage/examine_messages.yml
new file mode 100644
index 0000000000..4523998094
--- /dev/null
+++ b/Resources/Prototypes/Damage/examine_messages.yml
@@ -0,0 +1,9 @@
+- type: examinableDamage
+ id: WindowMessages
+ messages:
+ - comp-window-damaged-1
+ - comp-window-damaged-2
+ - comp-window-damaged-3
+ - comp-window-damaged-4
+ - comp-window-damaged-5
+ - comp-window-damaged-6
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml b/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
index 9b2ffd85d6..87fbf1c0e7 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
@@ -41,6 +41,8 @@
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Glass
+ - type: ExaminableDamage
+ messages: WindowMessages
- type: Destructible
thresholds:
- trigger:
diff --git a/Resources/Prototypes/Entities/Structures/Windows/window.yml b/Resources/Prototypes/Entities/Structures/Windows/window.yml
index 727eac57c1..142676cde1 100644
--- a/Resources/Prototypes/Entities/Structures/Windows/window.yml
+++ b/Resources/Prototypes/Entities/Structures/Windows/window.yml
@@ -35,6 +35,8 @@
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Glass
+ - type: ExaminableDamage
+ messages: WindowMessages
- type: Repairable
- type: Destructible
thresholds:
@@ -70,6 +72,7 @@
visuals:
- type: DamageVisualizer
thresholds: [4, 8, 12]
+ damageDivisor: 2
trackAllDamage: true
damageOverlay:
sprite: Structures/Windows/cracks.rsi
@@ -111,6 +114,8 @@
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Glass
+ - type: ExaminableDamage
+ messages: WindowMessages
- type: Destructible
thresholds:
- trigger: