diff --git a/Content.Server/Animals/Components/EggLayerComponent.cs b/Content.Server/Animals/Components/EggLayerComponent.cs
new file mode 100644
index 0000000000..84faa0ef27
--- /dev/null
+++ b/Content.Server/Animals/Components/EggLayerComponent.cs
@@ -0,0 +1,55 @@
+using Content.Shared.Actions;
+using Content.Shared.Actions.ActionTypes;
+using Content.Shared.Sound;
+using Content.Shared.Storage;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Animals.Components;
+
+///
+/// This component handles animals which lay eggs (or some other item) on a timer, using up hunger to do so.
+/// It also grants an action to players who are controlling these entities, allowing them to do it manually.
+///
+[RegisterComponent]
+public sealed class EggLayerComponent : Component
+{
+ [DataField("eggLayAction", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string EggLayAction = "AnimalLayEgg";
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("hungerUsage")]
+ public float HungerUsage = 60f;
+
+ ///
+ /// Minimum cooldown used for the automatic egg laying.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("eggLayCooldownMin")]
+ public float EggLayCooldownMin = 60f;
+
+ ///
+ /// Maximum cooldown used for the automatic egg laying.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("eggLayCooldownMax")]
+ public float EggLayCooldownMax = 120f;
+
+ ///
+ /// Set during component init.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float CurrentEggLayCooldown;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("eggSpawn", required: true)]
+ public List EggSpawn = default!;
+
+ [DataField("eggLaySound")]
+ public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg");
+
+ [DataField("accumulatedFrametime")]
+ public float AccumulatedFrametime;
+}
+
+public sealed class EggLayInstantActionEvent : InstantActionEvent {}
diff --git a/Content.Server/Animals/Systems/EggLayerSystem.cs b/Content.Server/Animals/Systems/EggLayerSystem.cs
new file mode 100644
index 0000000000..32fdb0bc24
--- /dev/null
+++ b/Content.Server/Animals/Systems/EggLayerSystem.cs
@@ -0,0 +1,95 @@
+using Content.Server.Actions;
+using Content.Server.Animals.Components;
+using Content.Server.Nutrition.Components;
+using Content.Server.Popups;
+using Content.Shared.Actions.ActionTypes;
+using Content.Shared.Storage;
+using Robust.Server.GameObjects;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Animals.Systems;
+
+public sealed class EggLayerSystem : EntitySystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly ActionsSystem _actions = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnComponentInit);
+ SubscribeLocalEvent(OnEggLayAction);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ foreach (var eggLayer in EntityQuery())
+ {
+ // Players should be using the action.
+ if (HasComp(eggLayer.Owner))
+ return;
+
+ eggLayer.AccumulatedFrametime += frameTime;
+
+ if (eggLayer.AccumulatedFrametime < eggLayer.CurrentEggLayCooldown)
+ continue;
+
+ eggLayer.AccumulatedFrametime -= eggLayer.CurrentEggLayCooldown;
+ eggLayer.CurrentEggLayCooldown = _random.NextFloat(eggLayer.EggLayCooldownMin, eggLayer.EggLayCooldownMax);
+
+ TryLayEgg(eggLayer.Owner, eggLayer);
+ }
+ }
+
+ private void OnComponentInit(EntityUid uid, EggLayerComponent component, ComponentInit args)
+ {
+ if (!_prototype.TryIndex(component.EggLayAction, out var action))
+ return;
+
+ _actions.AddAction(uid, new InstantAction(action), uid);
+ component.CurrentEggLayCooldown = _random.NextFloat(component.EggLayCooldownMin, component.EggLayCooldownMax);
+ }
+
+ private void OnEggLayAction(EntityUid uid, EggLayerComponent component, EggLayInstantActionEvent args)
+ {
+ args.Handled = TryLayEgg(uid, component);
+ }
+
+ public bool TryLayEgg(EntityUid uid, EggLayerComponent? component)
+ {
+ if (!Resolve(uid, ref component))
+ return false;
+
+ // Allow infinitely laying eggs if they can't get hungry
+ if (TryComp(uid, out var hunger))
+ {
+ if (hunger.CurrentHunger < component.HungerUsage)
+ {
+ _popup.PopupEntity(Loc.GetString("action-popup-lay-egg-too-hungry"), uid, Filter.Entities(uid));
+ return false;
+ }
+
+ hunger.CurrentHunger -= component.HungerUsage;
+ }
+
+ foreach (var ent in EntitySpawnCollection.GetSpawns(component.EggSpawn, _random))
+ {
+ Spawn(ent, Transform(uid).Coordinates);
+ }
+
+ // Sound + popups
+ SoundSystem.Play(component.EggLaySound.GetSound(), Filter.Pvs(uid), uid, component.EggLaySound.Params);
+ _popup.PopupEntity(Loc.GetString("action-popup-lay-egg-user"), uid, Filter.Entities(uid));
+ _popup.PopupEntity(Loc.GetString("action-popup-lay-egg-others", ("entity", uid)), uid, Filter.PvsExcept(uid));
+
+ return true;
+ }
+}
diff --git a/Content.Shared/Storage/EntitySpawnEntry.cs b/Content.Shared/Storage/EntitySpawnEntry.cs
index 9748f8dc30..2efbea010f 100644
--- a/Content.Shared/Storage/EntitySpawnEntry.cs
+++ b/Content.Shared/Storage/EntitySpawnEntry.cs
@@ -12,12 +12,14 @@ namespace Content.Shared.Storage;
[DataDefinition]
public struct EntitySpawnEntry : IPopulateDefaultValues
{
+ [ViewVariables(VVAccess.ReadWrite)]
[DataField("id", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
public string PrototypeId;
///
/// The probability that an item will spawn. Takes decimal form so 0.05 is 5%, 0.50 is 50% etc.
///
+ [ViewVariables(VVAccess.ReadWrite)]
[DataField("prob")] public float SpawnProbability;
///
@@ -41,8 +43,10 @@ public struct EntitySpawnEntry : IPopulateDefaultValues
///
///
///
+ [ViewVariables(VVAccess.ReadWrite)]
[DataField("orGroup")] public string? GroupId;
+ [ViewVariables(VVAccess.ReadWrite)]
[DataField("amount")] public int Amount;
///
@@ -50,6 +54,7 @@ public struct EntitySpawnEntry : IPopulateDefaultValues
/// If this is lesser or equal to , it will spawn exactly.
/// Otherwise, it chooses a random value between and on spawn.
///
+ [ViewVariables(VVAccess.ReadWrite)]
[DataField("maxAmount")] public int MaxAmount;
public void PopulateDefaultValues()
diff --git a/Resources/Audio/Effects/licenses.txt b/Resources/Audio/Effects/licenses.txt
index ee7381179e..18145786fc 100644
--- a/Resources/Audio/Effects/licenses.txt
+++ b/Resources/Audio/Effects/licenses.txt
@@ -37,3 +37,5 @@ The following sounds are taken from TGstation github (licensed under CC by 3.0):
demon_consume.ogg: taken at https://github.com/tgstation/tgstation/commit/d4f678a1772007ff8d7eddd21cf7218c8e07bfc0
demon_dies.ogg: taken at https://github.com/tgstation/tgstation/commit/d4f678a1772007ff8d7eddd21cf7218c8e07bfc0
+
+pop.ogg licensed under CC0 1.0 by mirrorcult
\ No newline at end of file
diff --git a/Resources/Audio/Effects/pop.ogg b/Resources/Audio/Effects/pop.ogg
new file mode 100644
index 0000000000..97b7468e19
Binary files /dev/null and b/Resources/Audio/Effects/pop.ogg differ
diff --git a/Resources/Locale/en-US/accent/accents.ftl b/Resources/Locale/en-US/accent/accents.ftl
index 603505e780..54f4be86a5 100644
--- a/Resources/Locale/en-US/accent/accents.ftl
+++ b/Resources/Locale/en-US/accent/accents.ftl
@@ -46,3 +46,15 @@ accent-words-generic-aggressive-1 = Grr!
accent-words-generic-aggressive-2 = Rrrr!
accent-words-generic-aggressive-3 = Grr...
accent-words-generic-aggressive-4 = Grrow!!
+
+# Duck
+accent-words-duck-1 = Quack!
+accent-words-duck-2 = Quack.
+accent-words-duck-3 = Quack?
+accent-words-duck-4 = Quack quack!
+
+# Chicken
+accent-words-chicken-1 = Cluck!
+accent-words-chicken-2 = Cluck.
+accent-words-chicken-3 = Cluck?
+accent-words-chicken-4 = Cluck cluck!
diff --git a/Resources/Locale/en-US/actions/actions/egg-lay.ftl b/Resources/Locale/en-US/actions/actions/egg-lay.ftl
new file mode 100644
index 0000000000..404eeee7d0
--- /dev/null
+++ b/Resources/Locale/en-US/actions/actions/egg-lay.ftl
@@ -0,0 +1,6 @@
+action-name-lay-egg = Lay egg
+action-description-lay-egg = Uses hunger to lay an egg.
+
+action-popup-lay-egg-user = You lay an egg.
+action-popup-lay-egg-others = {CAPITALIZE(THE($entity))} lays an egg.
+action-popup-lay-egg-too-hungry = You need more food before you can lay another egg!
diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml
index 29a85ce3e3..d30a32993c 100644
--- a/Resources/Prototypes/Actions/types.yml
+++ b/Resources/Prototypes/Actions/types.yml
@@ -69,3 +69,11 @@
icon: Objects/Weapons/Melee/shields.rsi/teleriot-icon.png
iconOn: Objects/Weapons/Melee/shields.rsi/teleriot-on.png
event: !type:ToggleActionEvent
+
+- type: instantAction
+ id: AnimalLayEgg
+ name: action-name-lay-egg
+ description: action-description-lay-egg
+ icon: Objects/Consumable/Food/egg.rsi/icon.png
+ useDelay: 60
+ serverEvent: !type:EggLayInstantActionEvent
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
index 0b7cb19938..2aa24672df 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
@@ -193,6 +193,13 @@
path: /Audio/Animals/chicken_cluck_happy.ogg
- type: Bloodstream
bloodMaxVolume: 100
+ - type: EggLayer
+ eggSpawn:
+ - id: FoodEgg
+ - type: ReplacementAccent
+ accent: chicken
+ - type: SentienceTarget
+ flavorKind: organic
- type: entity
name: mallard duck #Quack
@@ -237,6 +244,13 @@
path: /Audio/Animals/duck_quack_happy.ogg
- type: Bloodstream
bloodMaxVolume: 100
+ - type: EggLayer
+ eggSpawn:
+ - id: FoodEgg
+ - type: ReplacementAccent
+ accent: duck
+ - type: SentienceTarget
+ flavorKind: organic
- type: entity
name: white duck #Quack
diff --git a/Resources/Prototypes/accents.yml b/Resources/Prototypes/accents.yml
index e728a6fc82..ae32d9371f 100644
--- a/Resources/Prototypes/accents.yml
+++ b/Resources/Prototypes/accents.yml
@@ -62,3 +62,19 @@
- accent-words-generic-aggressive-2
- accent-words-generic-aggressive-3
- accent-words-generic-aggressive-4
+
+- type: accent
+ id: duck
+ words:
+ - accent-words-duck-1
+ - accent-words-duck-2
+ - accent-words-duck-3
+ - accent-words-duck-4
+
+- type: accent
+ id: chicken
+ words:
+ - accent-words-chicken-1
+ - accent-words-chicken-2
+ - accent-words-chicken-3
+ - accent-words-chicken-4