From 716fcaa4a3b8a43f3bb2d2808db4f648ee210041 Mon Sep 17 00:00:00 2001
From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Date: Sun, 9 Jul 2023 18:36:37 -0400
Subject: [PATCH] Animal Husbandry (#17140)
* Animal Husbandry
* suffixes and VV
* sanitize
* fix fails
---
.../Nutrition/EntitySystems/InfantSystem.cs | 34 +++
.../EntitySystems/AnimalHusbandrySystem.cs | 253 ++++++++++++++++++
.../AnimalHusbandry/InfantComponent.cs | 42 +++
.../AnimalHusbandry/ReproductiveComponent.cs | 102 +++++++
.../ReproductivePartnerComponent.cs | 12 +
.../nutrition/components/animal-husbandry.ftl | 3 +
.../Prototypes/Entities/Mobs/NPCs/animals.yml | 98 +++++++
.../Prototypes/Entities/Mobs/NPCs/pets.yml | 2 +
Resources/Prototypes/tags.yml | 15 ++
9 files changed, 561 insertions(+)
create mode 100644 Content.Client/Nutrition/EntitySystems/InfantSystem.cs
create mode 100644 Content.Server/Nutrition/EntitySystems/AnimalHusbandrySystem.cs
create mode 100644 Content.Shared/Nutrition/AnimalHusbandry/InfantComponent.cs
create mode 100644 Content.Shared/Nutrition/AnimalHusbandry/ReproductiveComponent.cs
create mode 100644 Content.Shared/Nutrition/AnimalHusbandry/ReproductivePartnerComponent.cs
create mode 100644 Resources/Locale/en-US/nutrition/components/animal-husbandry.ftl
diff --git a/Content.Client/Nutrition/EntitySystems/InfantSystem.cs b/Content.Client/Nutrition/EntitySystems/InfantSystem.cs
new file mode 100644
index 0000000000..dbda75c58f
--- /dev/null
+++ b/Content.Client/Nutrition/EntitySystems/InfantSystem.cs
@@ -0,0 +1,34 @@
+using Content.Shared.Nutrition.AnimalHusbandry;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Nutrition.EntitySystems;
+
+///
+/// This handles visuals for
+///
+public sealed class InfantSystem : EntitySystem
+{
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnStartup);
+ SubscribeLocalEvent(OnShutdown);
+ }
+
+ private void OnStartup(EntityUid uid, InfantComponent component, ComponentStartup args)
+ {
+ if (!TryComp(uid, out var sprite))
+ return;
+
+ component.DefaultScale = sprite.Scale;
+ sprite.Scale = component.VisualScale;
+ }
+
+ private void OnShutdown(EntityUid uid, InfantComponent component, ComponentShutdown args)
+ {
+ if (!TryComp(uid, out var sprite))
+ return;
+
+ sprite.Scale = component.DefaultScale;
+ }
+}
diff --git a/Content.Server/Nutrition/EntitySystems/AnimalHusbandrySystem.cs b/Content.Server/Nutrition/EntitySystems/AnimalHusbandrySystem.cs
new file mode 100644
index 0000000000..1a9167182f
--- /dev/null
+++ b/Content.Server/Nutrition/EntitySystems/AnimalHusbandrySystem.cs
@@ -0,0 +1,253 @@
+using Content.Server.Administration.Logs;
+using Content.Server.Interaction.Components;
+using Content.Server.Mind.Components;
+using Content.Server.Nutrition.Components;
+using Content.Server.Popups;
+using Content.Shared.Database;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Nutrition.AnimalHusbandry;
+using Content.Shared.Nutrition.Components;
+using Content.Shared.Nutrition.EntitySystems;
+using Content.Shared.Storage;
+using Robust.Server.GameObjects;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Nutrition.EntitySystems;
+
+///
+/// This handles logic and interactions related to
+///
+public sealed class AnimalHusbandrySystem : EntitySystem
+{
+ [Dependency] private readonly IAdminLogManager _adminLog = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
+ [Dependency] private readonly HungerSystem _hunger = default!;
+ [Dependency] private readonly MetaDataSystem _metaData = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+
+ private readonly HashSet _failedAttempts = new();
+
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnUnpaused);
+ SubscribeLocalEvent(OnMindAdded);
+ SubscribeLocalEvent(OnInfantUnpaused);
+ SubscribeLocalEvent(OnInfantStartup);
+ SubscribeLocalEvent(OnInfantShutdown);
+ }
+
+ private void OnUnpaused(EntityUid uid, ReproductiveComponent component, ref EntityUnpausedEvent args)
+ {
+ component.NextBreedAttempt += args.PausedTime;
+ }
+
+ private void OnInfantUnpaused(EntityUid uid, InfantComponent component, ref EntityUnpausedEvent args)
+ {
+ component.InfantEndTime += args.PausedTime;
+ }
+
+ // we express EZ-pass terminate the pregnancy if a player takes the role
+ private void OnMindAdded(EntityUid uid, ReproductiveComponent component, MindAddedMessage args)
+ {
+ component.Gestating = false;
+ component.GestationEndTime = null;
+ }
+
+ private void OnInfantStartup(EntityUid uid, InfantComponent component, ComponentStartup args)
+ {
+ var meta = MetaData(uid);
+ component.OriginalName = meta.EntityName;
+ _metaData.SetEntityName(uid, Loc.GetString("infant-name-prefix", ("name", meta.EntityName)), meta);
+ }
+
+ private void OnInfantShutdown(EntityUid uid, InfantComponent component, ComponentShutdown args)
+ {
+ _metaData.SetEntityName(uid, component.OriginalName);
+ }
+
+ ///
+ /// Attempts to breed the entity with a valid
+ /// partner nearby.
+ ///
+ public bool TryReproduceNearby(EntityUid uid, ReproductiveComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return false;
+
+ var xform = Transform(uid);
+ var partners = _entityLookup.GetComponentsInRange(xform.Coordinates, component.BreedRange);
+ foreach (var comp in partners)
+ {
+ var partner = comp.Owner;
+ if (TryReproduce(uid, partner, component))
+ return true;
+
+ // exit early if a valid attempt failed
+ if (_failedAttempts.Contains(uid))
+ return false;
+ }
+ return false;
+ }
+
+ ///
+ /// Attempts to breed an entity with
+ /// the specified partner.
+ ///
+ public bool TryReproduce(EntityUid uid, EntityUid partner, ReproductiveComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return false;
+
+ if (uid == partner)
+ return false;
+
+ if (!CanReproduce(uid, component))
+ return false;
+
+ if (!IsValidPartner(uid, partner, component))
+ return false;
+
+ // if the partner is valid, yet it fails the random check
+ // invalidate the entity from further attempts this tick
+ // in order to reduce total possible pairs.
+ if (!_random.Prob(component.BreedChance))
+ {
+ _failedAttempts.Add(uid);
+ _failedAttempts.Add(partner);
+ return false;
+ }
+
+ // this is kinda wack but it's the only sound associated with most animals
+ if (TryComp(uid, out var interactionPopup))
+ _audio.PlayPvs(interactionPopup.InteractSuccessSound, uid);
+
+ _hunger.ModifyHunger(uid, -component.HungerPerBirth);
+ _hunger.ModifyHunger(partner, -component.HungerPerBirth);
+
+ component.GestationEndTime = _timing.CurTime + component.GestationDuration;
+ component.Gestating = true;
+ _adminLog.Add(LogType.Action, $"{ToPrettyString(uid)} (carrier) and {ToPrettyString(partner)} (partner) successfully bred.");
+ return true;
+ }
+
+ ///
+ /// Checks if an entity satisfies
+ /// the conditions to be able to breed.
+ ///
+ public bool CanReproduce(EntityUid uid, ReproductiveComponent? component = null)
+ {
+ if (_failedAttempts.Contains(uid))
+ return false;
+
+ if (Resolve(uid, ref component, false) && component.Gestating)
+ return false;
+
+ if (HasComp(uid))
+ return false;
+
+ if (_mobState.IsIncapacitated(uid))
+ return false;
+
+ if (TryComp(uid, out var hunger) && _hunger.GetHungerThreshold(hunger) < HungerThreshold.Okay)
+ return false;
+
+ if (TryComp(uid, out var thirst) && thirst.CurrentThirstThreshold < ThirstThreshold.Okay)
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Checks if a given entity is a valid partner.
+ /// Does not include the random check, for sane API reasons.
+ ///
+ public bool IsValidPartner(EntityUid uid, EntityUid partner, ReproductiveComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return false;
+
+ if (!CanReproduce(partner))
+ return false;
+
+ return component.PartnerWhitelist.IsValid(partner);
+ }
+
+ ///
+ /// Gives birth to offspring and
+ /// resets the parent entity.
+ ///
+ public void Birth(EntityUid uid, ReproductiveComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ // this is kinda wack but it's the only sound associated with most animals
+ if (TryComp(uid, out var interactionPopup))
+ _audio.PlayPvs(interactionPopup.InteractSuccessSound, uid);
+
+ var xform = Transform(uid);
+ var spawns = EntitySpawnCollection.GetSpawns(component.Offspring, _random);
+ foreach (var spawn in spawns)
+ {
+ var offspring = Spawn(spawn, xform.Coordinates.Offset(_random.NextVector2(0.3f)));
+ if (component.MakeOffspringInfant)
+ {
+ var infant = AddComp(offspring);
+ infant.InfantEndTime = _timing.CurTime + infant.InfantDuration;
+ }
+ _adminLog.Add(LogType.Action, $"{ToPrettyString(uid)} gave birth to {ToPrettyString(offspring)}.");
+ }
+
+ _popup.PopupEntity(Loc.GetString(component.BirthPopup, ("parent", Identity.Entity(uid, EntityManager))), uid);
+
+ component.Gestating = false;
+ component.GestationEndTime = null;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ HashSet birthQueue = new();
+ _failedAttempts.Clear();
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var reproductive))
+ {
+ if (reproductive.GestationEndTime != null && _timing.CurTime >= reproductive.GestationEndTime)
+ {
+ birthQueue.Add(uid);
+ }
+
+ if (_timing.CurTime < reproductive.NextBreedAttempt)
+ continue;
+ reproductive.NextBreedAttempt += _random.Next(reproductive.MinBreedAttemptInterval, reproductive.MaxBreedAttemptInterval);
+
+ // no.
+ if (HasComp(uid) || TryComp(uid, out var mind) && mind.HasMind)
+ continue;
+
+ TryReproduceNearby(uid, reproductive);
+ }
+
+ foreach (var queued in birthQueue)
+ {
+ Birth(queued);
+ }
+
+ var infantQuery = EntityQueryEnumerator();
+ while (infantQuery.MoveNext(out var uid, out var infant))
+ {
+ if (_timing.CurTime < infant.InfantEndTime)
+ continue;
+ RemCompDeferred(uid, infant);
+ }
+ }
+}
diff --git a/Content.Shared/Nutrition/AnimalHusbandry/InfantComponent.cs b/Content.Shared/Nutrition/AnimalHusbandry/InfantComponent.cs
new file mode 100644
index 0000000000..0baeab5251
--- /dev/null
+++ b/Content.Shared/Nutrition/AnimalHusbandry/InfantComponent.cs
@@ -0,0 +1,42 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Nutrition.AnimalHusbandry;
+
+///
+/// This is used for marking entities as infants.
+/// Infants have half the size, visually, and cannot breed.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed class InfantComponent : Component
+{
+ ///
+ /// How long the entity remains an infant.
+ ///
+ [DataField("infantDuration")]
+ public TimeSpan InfantDuration = TimeSpan.FromMinutes(3);
+
+ ///
+ /// The base scale of the entity
+ ///
+ [DataField("defaultScale")]
+ public Vector2 DefaultScale = Vector2.One;
+
+ ///
+ /// The size difference of the entity while it's an infant.
+ ///
+ [DataField("visualScale")]
+ public Vector2 VisualScale = new(.5f, .5f);
+
+ ///
+ /// When the entity will stop being an infant.
+ ///
+ [DataField("infantEndTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
+ public TimeSpan InfantEndTime;
+
+ ///
+ /// The entity's name before the "baby" prefix is added.
+ ///
+ [DataField("originalName")]
+ public string OriginalName = string.Empty;
+}
diff --git a/Content.Shared/Nutrition/AnimalHusbandry/ReproductiveComponent.cs b/Content.Shared/Nutrition/AnimalHusbandry/ReproductiveComponent.cs
new file mode 100644
index 0000000000..4cb1499d4f
--- /dev/null
+++ b/Content.Shared/Nutrition/AnimalHusbandry/ReproductiveComponent.cs
@@ -0,0 +1,102 @@
+using Content.Shared.Nutrition.Components;
+using Content.Shared.Storage;
+using Content.Shared.Whitelist;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Nutrition.AnimalHusbandry;
+
+///
+/// This is used for simple animal husbandry. Entities with this component,
+/// given they are next to a particular entity that fulfills a whitelist,
+/// can create several "child" entities.
+///
+[RegisterComponent]
+public sealed class ReproductiveComponent : Component
+{
+ ///
+ /// The next time when breeding will be attempted.
+ ///
+ [DataField("nextBreedAttempt", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan NextBreedAttempt;
+
+ ///
+ /// Minimum length between each attempt to breed.
+ ///
+ [DataField("minBreedAttemptInterval"), ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan MinBreedAttemptInterval = TimeSpan.FromSeconds(45);
+
+ ///
+ /// Maximum length between each attempt to breed.
+ ///
+ [DataField("maxBreedAttemptInterval"), ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan MaxBreedAttemptInterval = TimeSpan.FromSeconds(60);
+
+ ///
+ /// How close to a partner an entity must be in order to breed.
+ /// Unrealistically long.
+ ///
+ [DataField("breedRange"), ViewVariables(VVAccess.ReadWrite)]
+ public float BreedRange = 3f;
+
+ ///
+ /// The chance that, on a given attempt,
+ /// for each valid partner, the entity will breed.
+ ///
+ [DataField("breedChance"), ViewVariables(VVAccess.ReadWrite)]
+ public float BreedChance = 0.15f;
+
+ ///
+ /// Entity prototypes for what type of
+ /// offspring can be produced by this entity.
+ ///
+ [DataField("offspring", required: true)]
+ public List Offspring = default!;
+
+ ///
+ /// Whether or not this entity has bred successfully
+ /// and will produce offspring imminently
+ ///
+ [DataField("gestating")]
+ public bool Gestating;
+
+ ///
+ /// When gestation will end.
+ /// Null if is false
+ ///
+ [DataField("gestationEndTime"), ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan? GestationEndTime;
+
+ ///
+ /// How long it takes the entity after breeding
+ /// to produce offspring
+ ///
+ [DataField("gestationDuration"), ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan GestationDuration = TimeSpan.FromMinutes(1.5);
+
+ ///
+ /// How much hunger is consumed when an entity
+ /// gives birth. A balancing tool to require feeding.
+ ///
+ [DataField("hungerPerBirth"), ViewVariables(VVAccess.ReadWrite)]
+ public float HungerPerBirth = 75f;
+
+ ///
+ /// Popup shown when an entity gives birth.
+ /// Configurable for things like laying eggs.
+ ///
+ [DataField("birthPopup"), ViewVariables(VVAccess.ReadWrite)]
+ public string BirthPopup = "reproductive-birth-popup";
+
+ ///
+ /// Whether or not the offspring should be made into "infants".
+ ///
+ [DataField("makeOffspringInfant"), ViewVariables(VVAccess.ReadWrite)]
+ public bool MakeOffspringInfant = true;
+
+ ///
+ /// An entity whitelist for what entities
+ /// can be this one's partner.
+ ///
+ [DataField("partnerWhitelist", required: true)]
+ public EntityWhitelist PartnerWhitelist = default!;
+}
diff --git a/Content.Shared/Nutrition/AnimalHusbandry/ReproductivePartnerComponent.cs b/Content.Shared/Nutrition/AnimalHusbandry/ReproductivePartnerComponent.cs
new file mode 100644
index 0000000000..e18646b065
--- /dev/null
+++ b/Content.Shared/Nutrition/AnimalHusbandry/ReproductivePartnerComponent.cs
@@ -0,0 +1,12 @@
+namespace Content.Shared.Nutrition.AnimalHusbandry;
+
+///
+/// This is used for denoting entities which are
+/// valid partners for .
+/// This functions outside of the whitelist.
+///
+[RegisterComponent]
+public sealed class ReproductivePartnerComponent : Component
+{
+
+}
diff --git a/Resources/Locale/en-US/nutrition/components/animal-husbandry.ftl b/Resources/Locale/en-US/nutrition/components/animal-husbandry.ftl
new file mode 100644
index 0000000000..6ca108b653
--- /dev/null
+++ b/Resources/Locale/en-US/nutrition/components/animal-husbandry.ftl
@@ -0,0 +1,3 @@
+infant-name-prefix = baby {$name}
+reproductive-birth-popup = {CAPITALIZE(THE($parent))} gave birth!
+reproductive-laid-egg-popup = {CAPITALIZE(THE($parent))} lays an egg!
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
index fd70dc7896..90658fcc66 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
@@ -164,6 +164,22 @@
- MobMask
layer:
- MobLayer
+ - type: Tag
+ tags:
+ - DoorBumpOpener
+ - Chicken
+ - type: Reproductive
+ breedChance: 0.05
+ birthPopup: reproductive-laid-egg-popup
+ makeOffspringInfant: false
+ partnerWhitelist:
+ tags:
+ - Chicken
+ offspring:
+ - id: FoodEggChickenFertilized
+ maxAmount: 3
+ - type: ReproductivePartner
+ - type: Appearance
- type: DamageStateVisuals
states:
Alive:
@@ -195,6 +211,21 @@
factions:
- Passive
+- type: entity
+ id: FoodEggChickenFertilized
+ parent: FoodEgg
+ suffix: Fertilized, Chicken
+ components:
+ - type: Timer
+ - type: TimedSpawner
+ prototypes:
+ - MobChicken
+ intervalSeconds: 20
+ minimumEntitiesSpawned: 1
+ maximumEntitiesSpawned: 1
+ - type: TimedDespawn #delete the egg after the chicken spawns
+ lifetime: 21
+
- type: entity
name: mallard duck #Quack
parent: SimpleMobBase
@@ -218,6 +249,22 @@
- MobMask
layer:
- MobLayer
+ - type: Tag
+ tags:
+ - DoorBumpOpener
+ - Duck
+ - type: Reproductive
+ breedChance: 0.05
+ birthPopup: reproductive-laid-egg-popup
+ makeOffspringInfant: false
+ partnerWhitelist:
+ tags:
+ - Duck
+ offspring:
+ - id: FoodEggDuckFertilized
+ maxAmount: 3
+ - type: ReproductivePartner
+ - type: Appearance
- type: DamageStateVisuals
states:
Alive:
@@ -281,6 +328,23 @@
Dead:
Base: dead-2
+- type: entity
+ id: FoodEggDuckFertilized
+ parent: FoodEgg
+ suffix: Fertilized, Duck
+ components:
+ - type: Timer
+ - type: TimedSpawner
+ prototypes:
+ - MobDuckMallard
+ - MobDuckWhite
+ - MobDuckBrown
+ intervalSeconds: 20
+ minimumEntitiesSpawned: 1
+ maximumEntitiesSpawned: 1
+ - type: TimedDespawn #delete the egg after the chicken spawns
+ lifetime: 21
+
- type: entity
name: butterfly
parent: SimpleMobBase
@@ -343,6 +407,17 @@
- map: ["enum.DamageStateVisualLayers.Base"]
state: cow
sprite: Mobs/Animals/cow.rsi
+ - type: Tag
+ tags:
+ - DoorBumpOpener
+ - Cow
+ - type: Reproductive
+ partnerWhitelist:
+ tags:
+ - Cow
+ offspring:
+ - id: MobCow
+ - type: ReproductivePartner
- type: Physics
- type: Fixtures
fixtures:
@@ -464,6 +539,18 @@
- MobMask
layer:
- MobLayer
+ - type: Tag
+ tags:
+ - DoorBumpOpener
+ - Goat
+ - type: Reproductive
+ partnerWhitelist:
+ tags:
+ - Goat
+ offspring:
+ - id: MobGoat
+ - type: ReproductivePartner
+ - type: Appearance
- type: DamageStateVisuals
states:
Alive:
@@ -2114,6 +2201,17 @@
- type: Inventory
speciesId: pig
templateId: pet
+ - type: Tag
+ tags:
+ - DoorBumpOpener
+ - Pig
+ - type: Reproductive
+ partnerWhitelist:
+ tags:
+ - Pig
+ offspring:
+ - id: MobPig
+ - type: ReproductivePartner
- type: InventorySlots
- type: Strippable
- type: UserInterface
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml
index 3974cb079a..3f4b4280e8 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml
@@ -411,6 +411,8 @@
- type: Tag
tags:
- CannotSuicide
+ - DoorBumpOpener
+ - Pig
- type: entity
name: Renault
diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml
index 208e99dd1f..4785edadd0 100644
--- a/Resources/Prototypes/tags.yml
+++ b/Resources/Prototypes/tags.yml
@@ -149,6 +149,9 @@
- type: Tag
id: Catwalk
+- type: Tag
+ id: Chicken
+
- type: Tag
id: Cigarette
@@ -176,6 +179,9 @@
- type: Tag #Ohioans die happy
id: Corn
+- type: Tag
+ id: Cow
+
- type: Tag
id: Crayon
@@ -263,6 +269,9 @@
- type: Tag
id: DroneUsable
+- type: Tag
+ id: Duck
+
- type: Tag
id: Ectoplasm
@@ -344,6 +353,9 @@
- type: Tag
id: GlassShard
+- type: Tag
+ id: Goat
+
- type: Tag
id: Grenade
@@ -607,6 +619,9 @@
- type: Tag
id: Pie
+- type: Tag
+ id: Pig
+
- type: Tag
id: PillCanister