diff --git a/Content.Server/Zombies/ActiveZombieComponent.cs b/Content.Server/Zombies/ActiveZombieComponent.cs
new file mode 100644
index 0000000000..0d79672bd1
--- /dev/null
+++ b/Content.Server/Zombies/ActiveZombieComponent.cs
@@ -0,0 +1,31 @@
+namespace Content.Server.Zombies;
+
+[RegisterComponent]
+public sealed class ActiveZombieComponent : Component
+{
+ ///
+ /// The chance that on a random attempt
+ /// that a zombie will do a groan
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float GroanChance = 0.2f;
+
+ ///
+ /// Minimum time between groans
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float GroanCooldown = 2;
+
+ ///
+ /// The length of time between each zombie's random groan
+ /// attempt.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float RandomGroanAttempt = 5;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float LastDamageGroanCooldown = 0f;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float Accumulator = 0f;
+}
diff --git a/Content.Server/Zombies/ZombieComponent.cs b/Content.Server/Zombies/ZombieComponent.cs
index ba301783e8..7b00e87dfc 100644
--- a/Content.Server/Zombies/ZombieComponent.cs
+++ b/Content.Server/Zombies/ZombieComponent.cs
@@ -17,14 +17,14 @@ namespace Content.Server.Zombies
///
/// The baseline infection chance you have if you are completely nude
///
- [ViewVariables]
+ [ViewVariables(VVAccess.ReadWrite)]
public float MaxZombieInfectionChance = 0.75f;
///
/// The minimum infection chance possible. This is simply to prevent
/// being invincible by bundling up.
///
- [ViewVariables]
+ [ViewVariables(VVAccess.ReadWrite)]
public float MinZombieInfectionChance = 0.1f;
[ViewVariables(VVAccess.ReadWrite)]
@@ -33,19 +33,19 @@ namespace Content.Server.Zombies
///
/// The skin color of the zombie
///
- [ViewVariables, DataField("skinColor")]
+ [DataField("skinColor")]
public Color SkinColor = new(0.45f, 0.51f, 0.29f);
///
/// The eye color of the zombie
///
- [ViewVariables, DataField("eyeColor")]
+ [DataField("eyeColor")]
public Color EyeColor = new(0.96f, 0.13f, 0.24f);
///
/// The attack arc of the zombie
///
- [ViewVariables, DataField("attackArc", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ [DataField("attackArc", customTypeSerializer: typeof(PrototypeIdSerializer))]
public string AttackArc = "claw";
///
diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs
index 79d50a7724..e5c0e43268 100644
--- a/Content.Server/Zombies/ZombieSystem.cs
+++ b/Content.Server/Zombies/ZombieSystem.cs
@@ -8,8 +8,11 @@ using Content.Shared.Chemistry.Components;
using Content.Shared.MobState.Components;
using Content.Server.Disease;
using Content.Shared.Inventory;
+using Content.Shared.MobState;
using Content.Server.Inventory;
using Robust.Shared.Prototypes;
+using Content.Server.Speech;
+using Content.Server.Chat.Systems;
using Content.Shared.Movement.Systems;
using Content.Shared.Damage;
@@ -22,6 +25,8 @@ namespace Content.Server.Zombies
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
[Dependency] private readonly ZombifyOnDeathSystem _zombify = default!;
[Dependency] private readonly ServerInventorySystem _inv = default!;
+ [Dependency] private readonly VocalSystem _vocal = default!;
+ [Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
@@ -30,9 +35,25 @@ namespace Content.Server.Zombies
base.Initialize();
SubscribeLocalEvent(OnMeleeHit);
+ SubscribeLocalEvent(OnMobState);
+ SubscribeLocalEvent(OnDamage);
SubscribeLocalEvent(OnRefreshSpeed);
}
+ private void OnMobState(EntityUid uid, ZombieComponent component, MobStateChangedEvent args)
+ {
+ if (args.CurrentMobState == DamageState.Alive)
+ EnsureComp(uid);
+ else
+ RemComp(uid);
+ }
+
+ private void OnDamage(EntityUid uid, ActiveZombieComponent component, DamageChangedEvent args)
+ {
+ if (args.DamageIncreased)
+ DoGroan(uid, component);
+ }
+
private void OnRefreshSpeed(EntityUid uid, ZombieComponent component, RefreshMovementSpeedModifiersEvent args)
{
var mod = component.ZombieMovementSpeedDebuff;
@@ -96,13 +117,13 @@ namespace Content.Server.Zombies
if (HasComp(entity))
args.BonusDamage = -args.BaseDamage * zombieComp.OtherZombieDamageCoefficient;
- if ((mobState.IsDead() || mobState.IsCritical())
+ if ((mobState.CurrentState == DamageState.Dead || mobState.CurrentState == DamageState.Critical)
&& !HasComp(entity))
{
_zombify.ZombifyEntity(entity);
args.BonusDamage = -args.BaseDamage;
}
- else if (mobState.IsAlive()) //heals when zombies bite live entities
+ else if (mobState.CurrentState == DamageState.Alive) //heals when zombies bite live entities
{
var healingSolution = new Solution();
healingSolution.AddReagent("Bicaridine", 1.00); //if OP, reduce/change chem
@@ -110,5 +131,39 @@ namespace Content.Server.Zombies
}
}
}
+
+ public void DoGroan(EntityUid uid, ActiveZombieComponent component)
+ {
+ if (component.LastDamageGroanCooldown > 0)
+ return;
+
+ if (_robustRandom.Prob(0.5f)) //this message is never seen by players so it just says this for admins
+ _chat.TrySendInGameICMessage(uid, "[automated zombie groan]", InGameICChatType.Speak, false);
+ else
+ _vocal.TryScream(uid);
+
+ 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;
+
+ //either do a random accent line or scream
+ DoGroan(zombiecomp.Owner, zombiecomp);
+ }
+ }
}
}
diff --git a/Content.Server/Zombies/ZombifyOnDeathSystem.cs b/Content.Server/Zombies/ZombifyOnDeathSystem.cs
index dee705dd07..491c5f1013 100644
--- a/Content.Server/Zombies/ZombifyOnDeathSystem.cs
+++ b/Content.Server/Zombies/ZombifyOnDeathSystem.cs
@@ -30,6 +30,10 @@ using Content.Shared.Zombies;
using Content.Shared.Popups;
using Content.Server.Atmos.Miasma;
using Content.Server.IdentityManagement;
+using Content.Shared.Audio;
+using Content.Shared.Sound;
+using Robust.Shared.Random;
+using Content.Server.Speech;
using Content.Shared.Movement.Systems;
namespace Content.Server.Zombies
@@ -111,6 +115,11 @@ namespace Content.Server.Zombies
RemComp(target);
AddComp(target);
+ var vocal = EnsureComp(target);
+ var scream = new SoundCollectionSpecifier ("ZombieScreams");
+ vocal.FemaleScream = scream;
+ vocal.MaleScream = scream;
+
///This is the actual damage of the zombie. We assign the visual appearance
///and range here because of stuff we'll find out later
var melee = EnsureComp(target);
diff --git a/Resources/Audio/Voice/Zombie/attributions.yml b/Resources/Audio/Voice/Zombie/attributions.yml
new file mode 100644
index 0000000000..9fa56bc389
--- /dev/null
+++ b/Resources/Audio/Voice/Zombie/attributions.yml
@@ -0,0 +1,12 @@
+- files: ["zombie-1.ogg"]
+ license: "CC-BY-NC-SA-3.0"
+ copyright: "Zombie 1 by Under7dude. Converted from MP3 to OGG."
+ source: "https://freesound.org/people/Under7dude/sounds/163440/"
+- files: ["zombie-2.ogg"]
+ license: "CC-BY-NC-SA-3.0"
+ copyright: "Zombie gargles by Breviceps. Converted from MP3 to OGG."
+ source: "https://freesound.org/people/Breviceps/sounds/445983/"
+- files: ["zombie-3.ogg"]
+ license: "CC-BY-NC-SA-3.0"
+ copyright: "Zombie Snarl by gneube. Converted from MP3 to OGG."
+ source: "https://freesound.org/people/gneube/sounds/315844/"
\ No newline at end of file
diff --git a/Resources/Audio/Voice/Zombie/zombie-1.ogg b/Resources/Audio/Voice/Zombie/zombie-1.ogg
new file mode 100644
index 0000000000..59d3b54662
Binary files /dev/null and b/Resources/Audio/Voice/Zombie/zombie-1.ogg differ
diff --git a/Resources/Audio/Voice/Zombie/zombie-2.ogg b/Resources/Audio/Voice/Zombie/zombie-2.ogg
new file mode 100644
index 0000000000..41eaa17bcc
Binary files /dev/null and b/Resources/Audio/Voice/Zombie/zombie-2.ogg differ
diff --git a/Resources/Audio/Voice/Zombie/zombie-3.ogg b/Resources/Audio/Voice/Zombie/zombie-3.ogg
new file mode 100644
index 0000000000..a03fff0719
Binary files /dev/null and b/Resources/Audio/Voice/Zombie/zombie-3.ogg differ
diff --git a/Resources/Locale/en-US/accent/accents.ftl b/Resources/Locale/en-US/accent/accents.ftl
index 54f4be86a5..8ffadc33fd 100644
--- a/Resources/Locale/en-US/accent/accents.ftl
+++ b/Resources/Locale/en-US/accent/accents.ftl
@@ -40,6 +40,9 @@ accent-words-zombie-1 = Gruaahhhh...
accent-words-zombie-2 = Mmuaaaa..
accent-words-zombie-3 = Braainnssss...
accent-words-zombie-4 = Grrrrr...
+accent-words-zombie-5 = Ouuaahhhhh...
+accent-words-zombie-6 = Graaaaaooohhlll...
+accent-words-zombie-7 = Brainsss... Braaaiiinnsss..
# Generic Aggressive
accent-words-generic-aggressive-1 = Grr!
diff --git a/Resources/Prototypes/SoundCollections/screams.yml b/Resources/Prototypes/SoundCollections/screams.yml
index 1033215005..c5507a0582 100644
--- a/Resources/Prototypes/SoundCollections/screams.yml
+++ b/Resources/Prototypes/SoundCollections/screams.yml
@@ -26,3 +26,10 @@
- /Audio/Voice/Human/science_scream4.ogg
- /Audio/Voice/Human/science_scream5.ogg
- /Audio/Voice/Human/science_scream6.ogg
+
+- type: soundCollection
+ id: ZombieScreams
+ files:
+ - /Audio/Voice/Zombie/zombie-1.ogg
+ - /Audio/Voice/Zombie/zombie-2.ogg
+ - /Audio/Voice/Zombie/zombie-3.ogg
\ No newline at end of file
diff --git a/Resources/Prototypes/accents.yml b/Resources/Prototypes/accents.yml
index ae32d9371f..b02dc30790 100644
--- a/Resources/Prototypes/accents.yml
+++ b/Resources/Prototypes/accents.yml
@@ -54,6 +54,9 @@
- accent-words-zombie-2
- accent-words-zombie-3
- accent-words-zombie-4
+ - accent-words-zombie-5
+ - accent-words-zombie-6
+ - accent-words-zombie-7
- type: accent
id: genericAggressive