From: 0x6273 <0x40@keemail.me> Date: Thu, 2 Mar 2023 19:23:56 +0000 (+0100) Subject: Add AutoEmote comp/system, updates to zombie code (#13932) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=72269c7a77dae912c8631acebd8a4b0925111f47;p=space-station-14.git 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 --- 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