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: