From 72269c7a77dae912c8631acebd8a4b0925111f47 Mon Sep 17 00:00:00 2001
From: 0x6273 <0x40@keemail.me>
Date: Thu, 2 Mar 2023 20:23:56 +0100
Subject: [PATCH] Add AutoEmote comp/system, updates to zombie code (#13932)
* Add AutoEmote comp/system
* Reduce groan chance so it's the same as before
Old code did 0.2 and then 0.5, now it's just one Prob(0.1)
* Fix typo, curTime var, don't log Resolve
* Maybe fix pausing?
* Fix mistake
* Update NextEmoteTime if an auto emote is removed
* Fix stuff
Get CurTime outside update loop
Use MapInit instead of ComponentInit
Fix a typo in a comment
Debug assert prototype ID in RemoveEmote
Do += PausedTime in OnUnpaused
Add prototype as arg to ResetTimer to avoid an indexing
---
Content.Server/Chat/AutoEmoteComponent.cs | 32 +++++
.../Chat/Systems/AutoEmoteSystem.cs | 134 ++++++++++++++++++
.../Zombies/ActiveZombieComponent.cs | 31 ++--
Content.Server/Zombies/ZombieSystem.cs | 61 ++++----
.../Zombies/ZombifyOnDeathSystem.cs | 2 -
.../Chat/Prototypes/AutoEmotePrototype.cs | 43 ++++++
Resources/Prototypes/Voice/auto_emotes.yml | 7 +
7 files changed, 256 insertions(+), 54 deletions(-)
create mode 100644 Content.Server/Chat/AutoEmoteComponent.cs
create mode 100644 Content.Server/Chat/Systems/AutoEmoteSystem.cs
create mode 100644 Content.Shared/Chat/Prototypes/AutoEmotePrototype.cs
create mode 100644 Resources/Prototypes/Voice/auto_emotes.yml
diff --git a/Content.Server/Chat/AutoEmoteComponent.cs b/Content.Server/Chat/AutoEmoteComponent.cs
new file mode 100644
index 0000000000..5a4211c2de
--- /dev/null
+++ b/Content.Server/Chat/AutoEmoteComponent.cs
@@ -0,0 +1,32 @@
+namespace Content.Server.Chat;
+
+using Content.Server.Chat.Systems;
+using Content.Shared.Chat.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
+
+///
+/// Causes an entity to automatically emote at a set interval.
+///
+[RegisterComponent, Access(typeof(AutoEmoteSystem))]
+public sealed class AutoEmoteComponent : Component
+{
+ ///
+ /// A set of emotes that the entity will preform.
+ ///
+ ///
+ [DataField("emotes", customTypeSerializer: typeof(PrototypeIdHashSetSerializer)), ViewVariables(VVAccess.ReadOnly)]
+ public HashSet Emotes = new HashSet();
+
+ ///
+ /// A dictionary storing the time of the next emote attempt for each emote.
+ /// Uses AutoEmotePrototype IDs as keys.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)] //TODO: make this a datafield and (de)serialize values as time offsets when https://github.com/space-wizards/RobustToolbox/issues/3768 is fixed
+ public Dictionary EmoteTimers = new Dictionary();
+
+ ///
+ /// Time of the next emote. Redundant, but avoids having to iterate EmoteTimers each update.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ public TimeSpan NextEmoteTime = TimeSpan.MaxValue;
+}
diff --git a/Content.Server/Chat/Systems/AutoEmoteSystem.cs b/Content.Server/Chat/Systems/AutoEmoteSystem.cs
new file mode 100644
index 0000000000..7dd4a98a43
--- /dev/null
+++ b/Content.Server/Chat/Systems/AutoEmoteSystem.cs
@@ -0,0 +1,134 @@
+namespace Content.Server.Chat.Systems;
+
+using System.Linq;
+using Content.Shared.Chat.Prototypes;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+public sealed class AutoEmoteSystem : 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(OnMapInit);
+ SubscribeLocalEvent(OnUnpaused);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var curTime = _gameTiming.CurTime;
+ foreach (var autoEmote in EntityQuery())
+ {
+ var uid = autoEmote.Owner;
+
+ if (autoEmote.NextEmoteTime > curTime)
+ continue;
+
+ foreach ((var key, var time) in autoEmote.EmoteTimers)
+ {
+ if (time > curTime)
+ continue;
+
+ var autoEmotePrototype = _prototypeManager.Index(key);
+ ResetTimer(uid, key, autoEmote, autoEmotePrototype);
+
+ if (!_random.Prob(autoEmotePrototype.Chance))
+ continue;
+
+ if (autoEmotePrototype.WithChat)
+ {
+ _chatSystem.TryEmoteWithChat(uid, autoEmotePrototype.EmoteId, autoEmotePrototype.HiddenFromChatWindow);
+ }
+ else
+ {
+ _chatSystem.TryEmoteWithoutChat(uid, autoEmotePrototype.EmoteId);
+ }
+ }
+ }
+ }
+
+ private void OnMapInit(EntityUid uid, AutoEmoteComponent autoEmote, MapInitEvent args)
+ {
+ // Start timers
+ foreach (var autoEmotePrototypeId in autoEmote.Emotes)
+ {
+ ResetTimer(uid, autoEmotePrototypeId, autoEmote);
+ }
+ }
+
+ private void OnUnpaused(EntityUid uid, AutoEmoteComponent autoEmote, ref EntityUnpausedEvent args)
+ {
+ foreach (var key in autoEmote.EmoteTimers.Keys)
+ {
+ autoEmote.EmoteTimers[key] += args.PausedTime;
+ }
+ autoEmote.NextEmoteTime += args.PausedTime;
+ }
+
+ ///
+ /// Try to add an emote to the entity, which will be performed at an interval.
+ ///
+ public bool AddEmote(EntityUid uid, string autoEmotePrototypeId, AutoEmoteComponent? autoEmote = null)
+ {
+ if (!Resolve(uid, ref autoEmote, logMissing: false))
+ return false;
+
+ if (autoEmote.Emotes.Contains(autoEmotePrototypeId))
+ return false;
+
+ autoEmote.Emotes.Add(autoEmotePrototypeId);
+ ResetTimer(uid, autoEmotePrototypeId, autoEmote);
+
+ return true;
+ }
+
+ ///
+ /// Stop preforming an emote.
+ ///
+ public bool RemoveEmote(EntityUid uid, string autoEmotePrototypeId, AutoEmoteComponent? autoEmote = null)
+ {
+ if (!Resolve(uid, ref autoEmote, logMissing: false))
+ return false;
+
+ DebugTools.Assert(_prototypeManager.HasIndex(autoEmotePrototypeId), "Prototype not found. Did you make a typo?");
+
+ if (!autoEmote.EmoteTimers.Remove(autoEmotePrototypeId))
+ return false;
+
+ autoEmote.NextEmoteTime = autoEmote.EmoteTimers.Values.Min();
+ return true;
+ }
+
+ ///
+ /// Reset the timer for a specific emote, or return false if it doesn't exist.
+ ///
+ public bool ResetTimer(EntityUid uid, string autoEmotePrototypeId, AutoEmoteComponent? autoEmote = null, AutoEmotePrototype? autoEmotePrototype = null)
+ {
+ if (!Resolve(uid, ref autoEmote))
+ return false;
+
+ if (!autoEmote.Emotes.Contains(autoEmotePrototypeId))
+ return false;
+
+ autoEmotePrototype ??= _prototypeManager.Index(autoEmotePrototypeId);
+
+ var curTime = _gameTiming.CurTime;
+ var time = curTime + autoEmotePrototype.Interval;
+ autoEmote.EmoteTimers[autoEmotePrototypeId] = time;
+
+ if (autoEmote.NextEmoteTime > time || autoEmote.NextEmoteTime <= curTime)
+ autoEmote.NextEmoteTime = time;
+
+ return true;
+ }
+}
diff --git a/Content.Server/Zombies/ActiveZombieComponent.cs b/Content.Server/Zombies/ActiveZombieComponent.cs
index 3e18ac397d..f6584330ea 100644
--- a/Content.Server/Zombies/ActiveZombieComponent.cs
+++ b/Content.Server/Zombies/ActiveZombieComponent.cs
@@ -1,34 +1,33 @@
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
{
///
- /// The chance that on a random attempt
- /// that a zombie will do a groan
+ /// What emote to preform.
///
[ViewVariables(VVAccess.ReadWrite)]
- public float GroanChance = 0.2f;
+ public string GroanEmoteId = "Scream";
///
- /// Minimum time between groans
+ /// Minimum time between groans.
///
[ViewVariables(VVAccess.ReadWrite)]
- public float GroanCooldown = 2;
+ public TimeSpan DamageGroanCooldown = TimeSpan.FromSeconds(2);
///
- /// The length of time between each zombie's random groan
- /// attempt.
+ /// Chance to groan.
///
- [ViewVariables(VVAccess.ReadWrite)]
- public float RandomGroanAttempt = 5;
-
- [ViewVariables(VVAccess.ReadWrite)]
- public string GroanEmoteId = "Scream";
-
- [ViewVariables(VVAccess.ReadWrite)]
- public float LastDamageGroanCooldown = 0f;
+ public float DamageGroanChance = 0.5f;
+ ///
+ /// The last time the zombie groaned from taking damage.
+ ///
[ViewVariables(VVAccess.ReadWrite)]
- public float Accumulator = 0f;
+ public TimeSpan LastDamageGroan = TimeSpan.Zero;
}
diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs
index ebc9f07967..181b14a439 100644
--- a/Content.Server/Zombies/ZombieSystem.cs
+++ b/Content.Server/Zombies/ZombieSystem.cs
@@ -1,5 +1,6 @@
using System.Linq;
using Content.Server.Body.Systems;
+using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Cloning;
using Content.Server.Disease;
@@ -7,14 +8,10 @@ using Content.Server.Disease.Components;
using Content.Server.Drone.Components;
using Content.Server.Humanoid;
using Content.Server.Inventory;
-using Content.Server.Speech;
using Content.Shared.Bed.Sleep;
using Content.Shared.Chemistry.Components;
-using Content.Server.Chat.Systems;
using Content.Server.Emoting.Systems;
using Content.Server.Speech.EntitySystems;
-using Content.Shared.Movement.Systems;
-using Content.Shared.Bed.Sleep;
using Content.Shared.Damage;
using Content.Shared.Disease.Events;
using Content.Shared.Inventory;
@@ -24,6 +21,7 @@ using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Zombies;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
+using Robust.Shared.Timing;
namespace Content.Server.Zombies
{
@@ -34,6 +32,8 @@ namespace Content.Server.Zombies
[Dependency] private readonly ZombifyOnDeathSystem _zombify = default!;
[Dependency] private readonly ServerInventorySystem _inv = default!;
[Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
@@ -52,7 +52,6 @@ namespace Content.Server.Zombies
SubscribeLocalEvent(OnDamage);
SubscribeLocalEvent(OnSneeze);
SubscribeLocalEvent(OnSleepAttempt);
-
}
private void OnSleepAttempt(EntityUid uid, ActiveZombieComponent component, ref TryingToSleepEvent args)
@@ -77,16 +76,30 @@ 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);
+
+ // Random groaning
+ EnsureComp(uid);
+ _autoEmote.AddEmote(uid, "ZombieGroan");
+ }
else
+ {
+ // Stop groaning when damaged
RemComp(uid);
+
+ // Stop random groaning
+ _autoEmote.RemoveEmote(uid, "ZombieGroan");
+ }
}
private void OnDamage(EntityUid uid, ActiveZombieComponent component, DamageChangedEvent args)
{
if (args.DamageIncreased)
- DoGroan(uid, component);
+ AttemptDamageGroan(uid, component);
}
private void OnSneeze(EntityUid uid, ActiveZombieComponent component, ref AttemptSneezeCoughEvent args)
@@ -166,40 +179,16 @@ namespace Content.Server.Zombies
}
}
- private void DoGroan(EntityUid uid, ActiveZombieComponent component)
+ private void AttemptDamageGroan(EntityUid uid, ActiveZombieComponent component)
{
- if (component.LastDamageGroanCooldown > 0)
+ if (component.LastDamageGroan + component.DamageGroanCooldown > _gameTiming.CurTime)
return;
- if (_robustRandom.Prob(0.5f)) //this message is never seen by players so it just says this for admins
- // What? Is this REALLY the best way we have of letting admins know there are zombies in a round?
- // [automated maintainer groan]
- _chat.TrySendInGameICMessage(uid, "[automated zombie groan]", InGameICChatType.Speak, false);
- else
- _chat.TryEmoteWithoutChat(uid, component.GroanEmoteId);
-
- component.LastDamageGroanCooldown = component.GroanCooldown;
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- foreach (var zombiecomp in EntityQuery())
- {
- zombiecomp.Accumulator += frameTime;
- zombiecomp.LastDamageGroanCooldown -= frameTime;
-
- if (zombiecomp.Accumulator < zombiecomp.RandomGroanAttempt)
- continue;
- zombiecomp.Accumulator -= zombiecomp.RandomGroanAttempt;
-
- if (!_robustRandom.Prob(zombiecomp.GroanChance))
- continue;
+ if (_robustRandom.Prob(component.DamageGroanChance))
+ return;
- //either do a random accent line or scream
- DoGroan(zombiecomp.Owner, zombiecomp);
- }
+ _chat.TryEmoteWithoutChat(uid, component.GroanEmoteId);
+ component.LastDamageGroan = _gameTiming.CurTime;
}
///
diff --git a/Content.Server/Zombies/ZombifyOnDeathSystem.cs b/Content.Server/Zombies/ZombifyOnDeathSystem.cs
index 32272801dd..49b95f422b 100644
--- a/Content.Server/Zombies/ZombifyOnDeathSystem.cs
+++ b/Content.Server/Zombies/ZombifyOnDeathSystem.cs
@@ -4,7 +4,6 @@ using Content.Server.Disease.Components;
using Content.Server.Body.Components;
using Content.Server.Atmos.Components;
using Content.Server.Nutrition.Components;
-using Robust.Shared.Player;
using Content.Server.Popups;
using Content.Server.Speech.Components;
using Content.Server.Body.Systems;
@@ -29,7 +28,6 @@ using Content.Shared.Humanoid;
using Content.Shared.Mobs;
using Content.Shared.Movement.Systems;
using Content.Shared.Weapons.Melee;
-using Robust.Shared.Audio;
namespace Content.Server.Zombies
{
diff --git a/Content.Shared/Chat/Prototypes/AutoEmotePrototype.cs b/Content.Shared/Chat/Prototypes/AutoEmotePrototype.cs
new file mode 100644
index 0000000000..99f3845fcb
--- /dev/null
+++ b/Content.Shared/Chat/Prototypes/AutoEmotePrototype.cs
@@ -0,0 +1,43 @@
+namespace Content.Shared.Chat.Prototypes;
+
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+[Prototype("autoEmote")]
+public sealed class AutoEmotePrototype : IPrototype
+{
+ ///
+ [IdDataField]
+ public string ID { get; } = default!;
+
+ ///
+ /// The ID of the emote prototype.
+ ///
+ [DataField("emote", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string EmoteId = String.Empty;
+
+ ///
+ /// How often an attempt at the emote will be made.
+ ///
+ [DataField("interval", required: true)]
+ public TimeSpan Interval;
+
+ ///
+ /// Probability of performing the emote each interval.
+ ///
+ [DataField("chance")]
+ public float Chance = 1;
+
+ ///
+ /// Also send the emote in chat.
+ ///
+ [DataField("withChat")]
+ public bool WithChat = true;
+
+ ///
+ /// 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;
+}
diff --git a/Resources/Prototypes/Voice/auto_emotes.yml b/Resources/Prototypes/Voice/auto_emotes.yml
new file mode 100644
index 0000000000..5bfa295d01
--- /dev/null
+++ b/Resources/Prototypes/Voice/auto_emotes.yml
@@ -0,0 +1,7 @@
+# Zombie
+- type: autoEmote
+ id: ZombieGroan
+ emote: Scream
+ interval: 5.0
+ chance: 0.1
+ withChat: false
--
2.52.0