]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add AutoEmote comp/system, updates to zombie code (#13932)
author0x6273 <0x40@keemail.me>
Thu, 2 Mar 2023 19:23:56 +0000 (20:23 +0100)
committerGitHub <noreply@github.com>
Thu, 2 Mar 2023 19:23:56 +0000 (11:23 -0800)
* 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 [new file with mode: 0644]
Content.Server/Chat/Systems/AutoEmoteSystem.cs [new file with mode: 0644]
Content.Server/Zombies/ActiveZombieComponent.cs
Content.Server/Zombies/ZombieSystem.cs
Content.Server/Zombies/ZombifyOnDeathSystem.cs
Content.Shared/Chat/Prototypes/AutoEmotePrototype.cs [new file with mode: 0644]
Resources/Prototypes/Voice/auto_emotes.yml [new file with mode: 0644]

diff --git a/Content.Server/Chat/AutoEmoteComponent.cs b/Content.Server/Chat/AutoEmoteComponent.cs
new file mode 100644 (file)
index 0000000..5a4211c
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Causes an entity to automatically emote at a set interval.
+/// </summary>
+[RegisterComponent, Access(typeof(AutoEmoteSystem))]
+public sealed class AutoEmoteComponent : Component
+{
+    /// <summary>
+    /// A set of emotes that the entity will preform.
+    /// <see cref="AutoEmotePrototype"/>
+    /// </summary>
+    [DataField("emotes", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AutoEmotePrototype>)), ViewVariables(VVAccess.ReadOnly)]
+    public HashSet<string> Emotes = new HashSet<string>();
+
+    /// <summary>
+    /// A dictionary storing the time of the next emote attempt for each emote.
+    /// Uses AutoEmotePrototype IDs as keys.
+    /// <summary>
+    [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<string, TimeSpan> EmoteTimers = new Dictionary<string, TimeSpan>();
+
+    /// <summary>
+    /// Time of the next emote. Redundant, but avoids having to iterate EmoteTimers each update.
+    /// </summary>
+    [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 (file)
index 0000000..7dd4a98
--- /dev/null
@@ -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<AutoEmoteComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<AutoEmoteComponent, EntityUnpausedEvent>(OnUnpaused);
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        var curTime = _gameTiming.CurTime;
+        foreach (var autoEmote in EntityQuery<AutoEmoteComponent>())
+        {
+            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<AutoEmotePrototype>(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;
+    }
+
+    /// <summary>
+    /// Try to add an emote to the entity, which will be performed at an interval.
+    /// </summary>
+    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;
+    }
+
+    /// <summary>
+    /// Stop preforming an emote.
+    /// </summary>
+    public bool RemoveEmote(EntityUid uid, string autoEmotePrototypeId, AutoEmoteComponent? autoEmote = null)
+    {
+        if (!Resolve(uid, ref autoEmote, logMissing: false))
+            return false;
+
+        DebugTools.Assert(_prototypeManager.HasIndex<AutoEmotePrototype>(autoEmotePrototypeId), "Prototype not found. Did you make a typo?");
+
+        if (!autoEmote.EmoteTimers.Remove(autoEmotePrototypeId))
+            return false;
+
+        autoEmote.NextEmoteTime = autoEmote.EmoteTimers.Values.Min();
+        return true;
+    }
+
+    /// <summary>
+    /// Reset the timer for a specific emote, or return false if it doesn't exist.
+    /// </summary>
+    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<AutoEmotePrototype>(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;
+    }
+}
index 3e18ac397d007c458a8f3a6d2d30cd4f734b73eb..f6584330eaac35325381b9aa2d63124eaf8aa4f1 100644 (file)
@@ -1,34 +1,33 @@
 namespace Content.Server.Zombies;
 
+/// <summary>
+/// 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.
+/// </summary>
 [RegisterComponent]
 public sealed class ActiveZombieComponent : Component
 {
     /// <summary>
-    /// The chance that on a random attempt
-    /// that a zombie will do a groan
+    /// What emote to preform.
     /// </summary>
     [ViewVariables(VVAccess.ReadWrite)]
-    public float GroanChance = 0.2f;
+    public string GroanEmoteId = "Scream";
 
     /// <summary>
-    /// Minimum time between groans
+    /// Minimum time between groans.
     /// </summary>
     [ViewVariables(VVAccess.ReadWrite)]
-    public float GroanCooldown = 2;
+    public TimeSpan DamageGroanCooldown = TimeSpan.FromSeconds(2);
 
     /// <summary>
-    /// The length of time between each zombie's random groan
-    /// attempt.
+    /// Chance to groan.
     /// </summary>
-    [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;
 
+    /// <summary>
+    /// The last time the zombie groaned from taking damage.
+    /// </summary>
     [ViewVariables(VVAccess.ReadWrite)]
-    public float Accumulator = 0f;
+    public TimeSpan LastDamageGroan = TimeSpan.Zero;
 }
index ebc9f079677e5d11a1f60e5d91cdb627cf1f0449..181b14a439b17490aa1ec25201e4b6abdc6c3f55 100644 (file)
@@ -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<ActiveZombieComponent, DamageChangedEvent>(OnDamage);
             SubscribeLocalEvent<ActiveZombieComponent, AttemptSneezeCoughEvent>(OnSneeze);
             SubscribeLocalEvent<ActiveZombieComponent, TryingToSleepEvent>(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<ActiveZombieComponent>(uid);
+
+                // Random groaning
+                EnsureComp<AutoEmoteComponent>(uid);
+                _autoEmote.AddEmote(uid, "ZombieGroan");
+            }
             else
+            {
+                // Stop groaning when damaged
                 RemComp<ActiveZombieComponent>(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<ActiveZombieComponent>())
-            {
-                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;
         }
 
         /// <summary>
index 32272801dd5806203c21cb373226ed792ca0c3df..49b95f422befb9f51a2e891de0505a8f74f03aef 100644 (file)
@@ -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 (file)
index 0000000..99f3845
--- /dev/null
@@ -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
+{
+    /// <inheritdoc/>
+    [IdDataField]
+    public string ID { get; } = default!;
+
+    /// <summary>
+    /// The ID of the emote prototype.
+    /// </summary>
+    [DataField("emote", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EmotePrototype>))]
+    public string EmoteId = String.Empty;
+
+    /// <summary>
+    /// How often an attempt at the emote will be made.
+    /// </summary>
+    [DataField("interval", required: true)]
+    public TimeSpan Interval;
+
+    /// <summary>
+    /// Probability of performing the emote each interval.
+    /// <summary>
+    [DataField("chance")]
+    public float Chance = 1;
+
+    /// <summary>
+    /// Also send the emote in chat.
+    /// <summary>
+    [DataField("withChat")]
+    public bool WithChat = true;
+
+    /// <summary>
+    /// Hide the chat message from the chat window, only showing the popup.
+    /// This does nothing if WithChat is false.
+    /// <summary>
+    [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 (file)
index 0000000..5bfa295
--- /dev/null
@@ -0,0 +1,7 @@
+# Zombie
+- type: autoEmote
+  id: ZombieGroan
+  emote: Scream
+  interval: 5.0
+  chance: 0.1
+  withChat: false