From 21e5aea8ca0a47ad1f062fea2b9563280aa3b773 Mon Sep 17 00:00:00 2001
From: 0x6273 <0x40@keemail.me>
Date: Thu, 23 Mar 2023 15:52:46 +0100
Subject: [PATCH] Add EmoteOnDamage comp/system for zombies (#14371)
---
Content.Server/Chat/EmoteOnDamageComponent.cs | 51 ++++++++++++
.../Chat/Systems/EmoteOnDamageSystem.cs | 82 +++++++++++++++++++
.../Zombies/ActiveZombieComponent.cs | 33 --------
Content.Server/Zombies/ZombieSystem.cs | 35 ++------
.../Zombies/ZombifyOnDeathSystem.cs | 23 +++++-
5 files changed, 162 insertions(+), 62 deletions(-)
create mode 100644 Content.Server/Chat/EmoteOnDamageComponent.cs
create mode 100644 Content.Server/Chat/Systems/EmoteOnDamageSystem.cs
delete mode 100644 Content.Server/Zombies/ActiveZombieComponent.cs
diff --git a/Content.Server/Chat/EmoteOnDamageComponent.cs b/Content.Server/Chat/EmoteOnDamageComponent.cs
new file mode 100644
index 0000000000..f2c7dc5b7e
--- /dev/null
+++ b/Content.Server/Chat/EmoteOnDamageComponent.cs
@@ -0,0 +1,51 @@
+namespace Content.Server.Chat;
+
+using Content.Server.Chat.Systems;
+using Content.Shared.Chat.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
+
+///
+/// Causes an entity to automatically emote when taking damage.
+///
+[RegisterComponent, Access(typeof(EmoteOnDamageSystem))]
+public sealed class EmoteOnDamageComponent : Component
+{
+ ///
+ /// Chance of preforming an emote when taking damage and not on cooldown.
+ ///
+ [DataField("emoteChance"), ViewVariables(VVAccess.ReadWrite)]
+ public float EmoteChance = 0.5f;
+
+ ///
+ /// A set of emotes that will be randomly picked from.
+ ///
+ ///
+ [DataField("emotes", customTypeSerializer: typeof(PrototypeIdHashSetSerializer)), ViewVariables(VVAccess.ReadWrite)]
+ public HashSet Emotes = new();
+
+ ///
+ /// Also send the emote in chat.
+ ///
+ [DataField("withChat"), ViewVariables(VVAccess.ReadWrite)]
+ public bool WithChat = false;
+
+ ///
+ /// Hide the chat message from the chat window, only showing the popup.
+ /// This does nothing if WithChat is false.
+ ///
+ [DataField("hiddenFromChatWindow")]
+ public bool HiddenFromChatWindow = false;
+
+ ///
+ /// The simulation time of the last emote preformed due to taking damage.
+ ///
+ [DataField("lastEmoteTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan LastEmoteTime = TimeSpan.Zero;
+
+ ///
+ /// The cooldown between emotes.
+ ///
+ [DataField("emoteCooldown"), ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan EmoteCooldown = TimeSpan.FromSeconds(2);
+}
diff --git a/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs b/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs
new file mode 100644
index 0000000000..ae14399c25
--- /dev/null
+++ b/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs
@@ -0,0 +1,82 @@
+namespace Content.Server.Chat.Systems;
+
+using Content.Shared.Chat.Prototypes;
+using Content.Shared.Damage;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+public sealed class EmoteOnDamageSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly ChatSystem _chatSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnUnpaused);
+ SubscribeLocalEvent(OnDamage);
+ }
+
+ private void OnUnpaused(EntityUid uid, EmoteOnDamageComponent emoteOnDamage, ref EntityUnpausedEvent args)
+ {
+ emoteOnDamage.LastEmoteTime += args.PausedTime;
+ }
+
+ private void OnDamage(EntityUid uid, EmoteOnDamageComponent emoteOnDamage, DamageChangedEvent args)
+ {
+ if (!args.DamageIncreased)
+ return;
+
+ if (emoteOnDamage.LastEmoteTime + emoteOnDamage.EmoteCooldown > _gameTiming.CurTime)
+ return;
+
+ if (emoteOnDamage.Emotes.Count == 0)
+ return;
+
+ if (!_random.Prob(emoteOnDamage.EmoteChance))
+ return;
+
+ var emote = _random.Pick(emoteOnDamage.Emotes);
+ if (emoteOnDamage.WithChat)
+ {
+ _chatSystem.TryEmoteWithChat(uid, emote, emoteOnDamage.HiddenFromChatWindow);
+ }
+ else
+ {
+ _chatSystem.TryEmoteWithoutChat(uid,emote);
+ }
+
+ emoteOnDamage.LastEmoteTime = _gameTiming.CurTime;
+ }
+
+ ///
+ /// Try to add an emote to the entity, which will be performed at an interval.
+ ///
+ public bool AddEmote(EntityUid uid, string emotePrototypeId, EmoteOnDamageComponent? emoteOnDamage = null)
+ {
+ if (!Resolve(uid, ref emoteOnDamage, logMissing: false))
+ return false;
+
+ DebugTools.Assert(_prototypeManager.HasIndex(emotePrototypeId), "Prototype not found. Did you make a typo?");
+
+ return emoteOnDamage.Emotes.Add(emotePrototypeId);
+ }
+
+ ///
+ /// Stop preforming an emote.
+ ///
+ public bool RemoveEmote(EntityUid uid, string emotePrototypeId, EmoteOnDamageComponent? emoteOnDamage = null)
+ {
+ if (!Resolve(uid, ref emoteOnDamage, logMissing: false))
+ return false;
+
+ DebugTools.Assert(_prototypeManager.HasIndex(emotePrototypeId), "Prototype not found. Did you make a typo?");
+
+ return emoteOnDamage.Emotes.Remove(emotePrototypeId);
+ }
+}
diff --git a/Content.Server/Zombies/ActiveZombieComponent.cs b/Content.Server/Zombies/ActiveZombieComponent.cs
deleted file mode 100644
index f6584330ea..0000000000
--- a/Content.Server/Zombies/ActiveZombieComponent.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-namespace Content.Server.Zombies;
-
-///
-/// Indicates a zombie that is "alive", i.e not crit/dead.
-/// Causes it to emote when damaged.
-/// TODO: move this to generic EmoteWhenDamaged comp/system.
-///
-[RegisterComponent]
-public sealed class ActiveZombieComponent : Component
-{
- ///
- /// What emote to preform.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public string GroanEmoteId = "Scream";
-
- ///
- /// Minimum time between groans.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public TimeSpan DamageGroanCooldown = TimeSpan.FromSeconds(2);
-
- ///
- /// Chance to groan.
- ///
- public float DamageGroanChance = 0.5f;
-
- ///
- /// The last time the zombie groaned from taking damage.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public TimeSpan LastDamageGroan = TimeSpan.Zero;
-}
diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs
index 181b14a439..eafc6a54e0 100644
--- a/Content.Server/Zombies/ZombieSystem.cs
+++ b/Content.Server/Zombies/ZombieSystem.cs
@@ -12,7 +12,6 @@ using Content.Shared.Bed.Sleep;
using Content.Shared.Chemistry.Components;
using Content.Server.Emoting.Systems;
using Content.Server.Speech.EntitySystems;
-using Content.Shared.Damage;
using Content.Shared.Disease.Events;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
@@ -33,6 +32,7 @@ namespace Content.Server.Zombies
[Dependency] private readonly ServerInventorySystem _inv = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
+ [Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
@@ -49,12 +49,11 @@ namespace Content.Server.Zombies
SubscribeLocalEvent(OnMeleeHit);
SubscribeLocalEvent(OnMobState);
SubscribeLocalEvent(OnZombieCloning);
- SubscribeLocalEvent(OnDamage);
- SubscribeLocalEvent(OnSneeze);
- SubscribeLocalEvent(OnSleepAttempt);
+ SubscribeLocalEvent(OnSneeze);
+ SubscribeLocalEvent(OnSleepAttempt);
}
- private void OnSleepAttempt(EntityUid uid, ActiveZombieComponent component, ref TryingToSleepEvent args)
+ private void OnSleepAttempt(EntityUid uid, ZombieComponent component, ref TryingToSleepEvent args)
{
args.Cancelled = true;
}
@@ -76,11 +75,11 @@ namespace Content.Server.Zombies
private void OnMobState(EntityUid uid, ZombieComponent component, MobStateChangedEvent args)
{
- //BUG: this won't work when an entity becomes a zombie some other way, such as admin smite
if (args.NewMobState == MobState.Alive)
{
// Groaning when damaged
- EnsureComp(uid);
+ EnsureComp(uid);
+ _emoteOnDamage.AddEmote(uid, "Scream");
// Random groaning
EnsureComp(uid);
@@ -89,20 +88,14 @@ namespace Content.Server.Zombies
else
{
// Stop groaning when damaged
- RemComp(uid);
+ _emoteOnDamage.RemoveEmote(uid, "Scream");
// Stop random groaning
_autoEmote.RemoveEmote(uid, "ZombieGroan");
}
}
- private void OnDamage(EntityUid uid, ActiveZombieComponent component, DamageChangedEvent args)
- {
- if (args.DamageIncreased)
- AttemptDamageGroan(uid, component);
- }
-
- private void OnSneeze(EntityUid uid, ActiveZombieComponent component, ref AttemptSneezeCoughEvent args)
+ private void OnSneeze(EntityUid uid, ZombieComponent component, ref AttemptSneezeCoughEvent args)
{
args.Cancelled = true;
}
@@ -179,18 +172,6 @@ namespace Content.Server.Zombies
}
}
- private void AttemptDamageGroan(EntityUid uid, ActiveZombieComponent component)
- {
- if (component.LastDamageGroan + component.DamageGroanCooldown > _gameTiming.CurTime)
- return;
-
- if (_robustRandom.Prob(component.DamageGroanChance))
- return;
-
- _chat.TryEmoteWithoutChat(uid, component.GroanEmoteId);
- component.LastDamageGroan = _gameTiming.CurTime;
- }
-
///
/// This is the function to call if you want to unzombify an entity.
///
diff --git a/Content.Server/Zombies/ZombifyOnDeathSystem.cs b/Content.Server/Zombies/ZombifyOnDeathSystem.cs
index 9202e7fcef..7cf12b1328 100644
--- a/Content.Server/Zombies/ZombifyOnDeathSystem.cs
+++ b/Content.Server/Zombies/ZombifyOnDeathSystem.cs
@@ -26,8 +26,11 @@ using Content.Server.Humanoid;
using Content.Server.IdentityManagement;
using Content.Shared.Humanoid;
using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Weapons.Melee;
+using Content.Server.Chat;
+using Content.Server.Chat.Systems;
namespace Content.Server.Zombies
{
@@ -47,6 +50,8 @@ namespace Content.Server.Zombies
[Dependency] private readonly HumanoidAppearanceSystem _sharedHuApp = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
+ [Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
+ [Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
[Dependency] private readonly IChatManager _chatMan = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
@@ -65,7 +70,7 @@ namespace Content.Server.Zombies
if (args.NewMobState == MobState.Dead ||
args.NewMobState == MobState.Critical)
{
- ZombifyEntity(uid);
+ ZombifyEntity(uid, args.Component);
}
}
@@ -81,12 +86,15 @@ namespace Content.Server.Zombies
/// rewrite this, but this is how it shall lie eternal. Turn back now.
/// -emo
///
- public void ZombifyEntity(EntityUid target)
+ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null)
{
//Don't zombfiy zombies
if (HasComp(target))
return;
+ if (!Resolve(target, ref mobState, logMissing: false))
+ return;
+
//you're a real zombie now, son.
var zombiecomp = AddComp(target);
@@ -117,6 +125,17 @@ namespace Content.Server.Zombies
melee.Range = 1.5f;
Dirty(melee);
+ if (mobState.CurrentState == MobState.Alive)
+ {
+ // Groaning when damaged
+ EnsureComp(target);
+ _emoteOnDamage.AddEmote(target, "Scream");
+
+ // Random groaning
+ EnsureComp(target);
+ _autoEmote.AddEmote(target, "ZombieGroan");
+ }
+
//We have specific stuff for humanoid zombies because they matter more
if (TryComp(target, out var huApComp)) //huapcomp
{