using Content.Server.Administration.Logs; using Content.Server.Popups; using Content.Shared.Database; using Content.Shared.IdentityManagement; using Content.Shared.Interaction.Components; using Content.Shared.Mind.Components; using Content.Shared.Mobs.Systems; using Content.Shared.NameModifier.EntitySystems; using Content.Shared.Nutrition.AnimalHusbandry; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Nutrition.Prototypes; using Content.Shared.Storage; using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Server.Nutrition.EntitySystems; /// /// This handles logic and interactions related to /// public sealed partial class AnimalHusbandrySystem : EntitySystem { [Dependency] private EntityLookupSystem _entityLookup = default!; [Dependency] private SatiationSystem _satiation = default!; [Dependency] private IAdminLogManager _adminLog = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private IRobustRandom _random = default!; [Dependency] private MobStateSystem _mobState = default!; [Dependency] private PopupSystem _popup = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private NameModifierSystem _nameMod = default!; private readonly HashSet _failedAttempts = new(); private readonly HashSet _birthQueue = new(); /// public override void Initialize() { SubscribeLocalEvent(OnMindAdded); SubscribeLocalEvent(OnRefreshNameModifiers); } // 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 OnRefreshNameModifiers(Entity entity, ref RefreshNameModifiersEvent args) { // This check may seem redundant, but it makes sure that the prefix is removed before the component is removed if (_timing.CurTime < entity.Comp.InfantEndTime) args.AddModifier("infant-name-prefix"); } /// /// 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 = new HashSet>(); _entityLookup.GetEntitiesInRange(xform.Coordinates, component.BreedRange, partners); if (partners.Count >= component.Capacity) return false; 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); if (TryComp(uid, out var satiation)) { _satiation.ModifyValue((uid, satiation), SatiationSystem.Hunger, -component.HungerPerBirth); } if (TryComp(partner, out var partnerSatiation)) { _satiation.ModifyValue((partner, partnerSatiation), SatiationSystem.Hunger, -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 no satiations, no limit to reproduction if (!TryComp(uid, out var satiation)) return true; if (component.MinHungerThreshold is { } minHunger) { if (!satiation.Has(SatiationSystem.Hunger) || _satiation.IsValueInRange((uid, satiation), SatiationSystem.Hunger, below: minHunger)) return false; } // Check minimum thirst requirement if (component.MinThirstThreshold is { } minThirst) { if (!satiation.Has(SatiationSystem.Thirst) || _satiation.IsValueInRange((uid, satiation), SatiationSystem.Thirst, below: minThirst)) 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 _whitelistSystem.IsWhitelistPass(component.PartnerWhitelist, 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); if (!_transform.TryGetMapOrGridCoordinates(uid, out var spawnPosition)) return; var spawns = EntitySpawnCollection.GetSpawns(component.Offspring, _random); foreach (var spawn in spawns) { var offspring = Spawn(spawn, spawnPosition.Value.Offset(_random.NextVector2(0.3f))); if (component.MakeOffspringInfant) { var infant = AddComp(offspring); infant.InfantEndTime = _timing.CurTime + infant.InfantDuration; // Make sure the name prefix is applied _nameMod.RefreshNameModifiers(offspring); } _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); _birthQueue.Clear(); _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); // Make sure the name prefix gets removed _nameMod.RefreshNameModifiers(uid); } } }