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