diff --git a/Content.Server/Animals/Components/EggLayerComponent.cs b/Content.Server/Animals/Components/EggLayerComponent.cs index a0f7de676e..899bc97f47 100644 --- a/Content.Server/Animals/Components/EggLayerComponent.cs +++ b/Content.Server/Animals/Components/EggLayerComponent.cs @@ -1,3 +1,4 @@ +using Content.Server.Animals.Systems; using Content.Shared.Storage; using Robust.Shared.Audio; using Robust.Shared.Prototypes; @@ -9,44 +10,47 @@ namespace Content.Server.Animals.Components; /// It also grants an action to players who are controlling these entities, allowing them to do it manually. /// -[RegisterComponent] +[RegisterComponent, Access(typeof(EggLayerSystem)), AutoGenerateComponentPause] public sealed partial class EggLayerComponent : Component { + /// + /// The item that gets laid/spawned, retrieved from animal prototype. + /// + [DataField(required: true)] + public List EggSpawn = new(); + + /// + /// Player action. + /// [DataField] public EntProtoId EggLayAction = "ActionAnimalLayEgg"; - /// - /// The amount of nutrient consumed on update. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float HungerUsage = 60f; + [DataField] + public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg"); /// /// Minimum cooldown used for the automatic egg laying. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float EggLayCooldownMin = 60f; /// /// Maximum cooldown used for the automatic egg laying. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float EggLayCooldownMax = 120f; /// - /// Set during component init. + /// The amount of nutrient consumed on update. /// - [ViewVariables(VVAccess.ReadWrite)] - public float CurrentEggLayCooldown; - - [DataField(required: true), ViewVariables(VVAccess.ReadWrite)] - public List EggSpawn = default!; - [DataField] - public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg"); - - [DataField] - public float AccumulatedFrametime; + public float HungerUsage = 60f; [DataField] public EntityUid? Action; + + /// + /// When to next try to produce. + /// + [DataField, AutoPausedField] + public TimeSpan NextGrowth = TimeSpan.Zero; } diff --git a/Content.Server/Animals/Components/UdderComponent.cs b/Content.Server/Animals/Components/UdderComponent.cs deleted file mode 100644 index 620f4572a7..0000000000 --- a/Content.Server/Animals/Components/UdderComponent.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Content.Server.Animals.Systems; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.FixedPoint; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; - -namespace Content.Server.Animals.Components - -/// -/// Lets an entity produce milk. Uses hunger if present. -/// -{ - [RegisterComponent, Access(typeof(UdderSystem))] - internal sealed partial class UdderComponent : Component - { - /// - /// The reagent to produce. - /// - [DataField, ViewVariables(VVAccess.ReadOnly)] - public ProtoId ReagentId = "Milk"; - - /// - /// The name of . - /// - [DataField, ViewVariables(VVAccess.ReadOnly)] - public string SolutionName = "udder"; - - /// - /// The solution to add reagent to. - /// - [DataField] - public Entity? Solution = null; - - /// - /// The amount of reagent to be generated on update. - /// - [DataField, ViewVariables(VVAccess.ReadOnly)] - public FixedPoint2 QuantityPerUpdate = 25; - - /// - /// The amount of nutrient consumed on update. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float HungerUsage = 10f; - - /// - /// How long to wait before producing. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public TimeSpan GrowthDelay = TimeSpan.FromMinutes(1); - - /// - /// When to next try to produce. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] - public TimeSpan NextGrowth = TimeSpan.FromSeconds(0); - } -} diff --git a/Content.Server/Animals/Systems/EggLayerSystem.cs b/Content.Server/Animals/Systems/EggLayerSystem.cs index 55d63808a4..3e552f1b38 100644 --- a/Content.Server/Animals/Systems/EggLayerSystem.cs +++ b/Content.Server/Animals/Systems/EggLayerSystem.cs @@ -7,15 +7,15 @@ using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Storage; using Robust.Server.Audio; -using Robust.Server.GameObjects; using Robust.Shared.Player; using Robust.Shared.Random; +using Robust.Shared.Timing; namespace Content.Server.Animals.Systems; /// -/// Gives ability to produce eggs, produces endless if the -/// owner has no HungerComponent +/// Gives the ability to lay eggs/other things; +/// produces endlessly if the owner does not have a HungerComponent. /// public sealed class EggLayerSystem : EntitySystem { @@ -23,6 +23,7 @@ public sealed class EggLayerSystem : EntitySystem [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly HungerSystem _hunger = default!; + [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly MobStateSystem _mobState = default!; @@ -37,7 +38,6 @@ public sealed class EggLayerSystem : EntitySystem public override void Update(float frameTime) { base.Update(frameTime); - var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var eggLayer)) { @@ -45,13 +45,17 @@ public sealed class EggLayerSystem : EntitySystem if (HasComp(uid)) continue; - eggLayer.AccumulatedFrametime += frameTime; - - if (eggLayer.AccumulatedFrametime < eggLayer.CurrentEggLayCooldown) + if (_timing.CurTime < eggLayer.NextGrowth) continue; - eggLayer.AccumulatedFrametime -= eggLayer.CurrentEggLayCooldown; - eggLayer.CurrentEggLayCooldown = _random.NextFloat(eggLayer.EggLayCooldownMin, eggLayer.EggLayCooldownMax); + // Randomize next growth time for more organic egglaying. + eggLayer.NextGrowth += TimeSpan.FromSeconds(_random.NextFloat(eggLayer.EggLayCooldownMin, eggLayer.EggLayCooldownMax)); + + if (_mobState.IsDead(uid)) + continue; + + // Hungerlevel check/modification is done in TryLayEgg() + // so it's used for player controlled chickens as well. TryLayEgg(uid, eggLayer); } @@ -60,11 +64,12 @@ public sealed class EggLayerSystem : EntitySystem private void OnMapInit(EntityUid uid, EggLayerComponent component, MapInitEvent args) { _actions.AddAction(uid, ref component.Action, component.EggLayAction); - component.CurrentEggLayCooldown = _random.NextFloat(component.EggLayCooldownMin, component.EggLayCooldownMax); + component.NextGrowth = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(component.EggLayCooldownMin, component.EggLayCooldownMax)); } private void OnEggLayAction(EntityUid uid, EggLayerComponent egglayer, EggLayInstantActionEvent args) { + // Cooldown is handeled by ActionAnimalLayEgg in types.yml. args.Handled = TryLayEgg(uid, egglayer); } @@ -76,7 +81,7 @@ public sealed class EggLayerSystem : EntitySystem if (_mobState.IsDead(uid)) return false; - // Allow infinitely laying eggs if they can't get hungry + // Allow infinitely laying eggs if they can't get hungry. if (TryComp(uid, out var hunger)) { if (hunger.CurrentHunger < egglayer.HungerUsage) diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs index c9a71c5358..1514d580dd 100644 --- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs +++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs @@ -2,7 +2,6 @@ using Content.Server.Actions; using Content.Server.Humanoid; using Content.Server.Inventory; using Content.Server.Mind.Commands; -using Content.Server.Nutrition; using Content.Server.Polymorph.Components; using Content.Shared.Actions; using Content.Shared.Buckle; @@ -13,6 +12,7 @@ using Content.Shared.IdentityManagement; using Content.Shared.Mind; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.Nutrition; using Content.Shared.Polymorph; using Content.Shared.Popups; using Robust.Server.Audio; diff --git a/Content.Shared/Animals/UdderComponent.cs b/Content.Shared/Animals/UdderComponent.cs new file mode 100644 index 0000000000..d2767b0896 --- /dev/null +++ b/Content.Shared/Animals/UdderComponent.cs @@ -0,0 +1,57 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Animals; + +/// +/// Gives the ability to produce a solution; +/// produces endlessly if the owner does not have a HungerComponent. +/// +[RegisterComponent, AutoGenerateComponentState, AutoGenerateComponentPause, NetworkedComponent] +public sealed partial class UdderComponent : Component +{ + /// + /// The reagent to produce. + /// + [DataField, AutoNetworkedField] + public ProtoId ReagentId = new(); + + /// + /// The name of . + /// + [DataField] + public string SolutionName = "udder"; + + /// + /// The solution to add reagent to. + /// + [DataField, ViewVariables(VVAccess.ReadOnly)] + public Entity? Solution = null; + + /// + /// The amount of reagent to be generated on update. + /// + [DataField, AutoNetworkedField] + public FixedPoint2 QuantityPerUpdate = 25; + + /// + /// The amount of nutrient consumed on update. + /// + [DataField, AutoNetworkedField] + public float HungerUsage = 10f; + + /// + /// How long to wait before producing. + /// + [DataField, AutoNetworkedField] + public TimeSpan GrowthDelay = TimeSpan.FromMinutes(1); + + /// + /// When to next try to produce. + /// + [DataField, AutoPausedField, Access(typeof(UdderSystem))] + public TimeSpan NextGrowth = TimeSpan.Zero; +} diff --git a/Content.Server/Animals/Systems/UdderSystem.cs b/Content.Shared/Animals/UdderSystem.cs similarity index 62% rename from Content.Server/Animals/Systems/UdderSystem.cs rename to Content.Shared/Animals/UdderSystem.cs index 452ba54d6e..cb6e5b307f 100644 --- a/Content.Server/Animals/Systems/UdderSystem.cs +++ b/Content.Shared/Animals/UdderSystem.cs @@ -1,8 +1,7 @@ -using Content.Server.Animals.Components; -using Content.Server.Popups; -using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.DoAfter; +using Content.Shared.Examine; using Content.Shared.IdentityManagement; using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition.Components; @@ -12,18 +11,17 @@ using Content.Shared.Udder; using Content.Shared.Verbs; using Robust.Shared.Timing; -namespace Content.Server.Animals.Systems; - +namespace Content.Shared.Animals; /// -/// Gives ability to produce milkable reagents, produces endless if the -/// owner has no HungerComponent +/// Gives the ability to produce milkable reagents; +/// produces endlessly if the owner does not have a HungerComponent. /// -internal sealed class UdderSystem : EntitySystem +public sealed class UdderSystem : EntitySystem { [Dependency] private readonly HungerSystem _hunger = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; @@ -31,26 +29,37 @@ internal sealed class UdderSystem : EntitySystem { base.Initialize(); + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent>(AddMilkVerb); SubscribeLocalEvent(OnDoAfter); + SubscribeLocalEvent(OnExamine); + } + + private void OnMapInit(EntityUid uid, UdderComponent component, MapInitEvent args) + { + component.NextGrowth = _timing.CurTime + component.GrowthDelay; } public override void Update(float frameTime) { base.Update(frameTime); - var query = EntityQueryEnumerator(); - var now = _timing.CurTime; while (query.MoveNext(out var uid, out var udder)) { - if (now < udder.NextGrowth) + if (_timing.CurTime < udder.NextGrowth) continue; - udder.NextGrowth = now + udder.GrowthDelay; + udder.NextGrowth += udder.GrowthDelay; if (_mobState.IsDead(uid)) continue; + if (!_solutionContainerSystem.ResolveSolution(uid, udder.SolutionName, ref udder.Solution, out var solution)) + continue; + + if (solution.AvailableVolume == 0) + continue; + // Actually there is food digestion so no problem with instant reagent generation "OnFeed" if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger)) { @@ -61,9 +70,6 @@ internal sealed class UdderSystem : EntitySystem _hunger.ModifyHunger(uid, -udder.HungerUsage, hunger); } - if (!_solutionContainerSystem.ResolveSolution(uid, udder.SolutionName, ref udder.Solution)) - continue; - //TODO: toxins from bloodstream !? _solutionContainerSystem.TryAddReagent(udder.Solution.Value, udder.ReagentId, udder.QuantityPerUpdate, out _); } @@ -99,7 +105,7 @@ internal sealed class UdderSystem : EntitySystem var quantity = solution.Volume; if (quantity == 0) { - _popupSystem.PopupEntity(Loc.GetString("udder-system-dry"), entity.Owner, args.Args.User); + _popupSystem.PopupClient(Loc.GetString("udder-system-dry"), entity.Owner, args.Args.User); return; } @@ -109,7 +115,7 @@ internal sealed class UdderSystem : EntitySystem var split = _solutionContainerSystem.SplitSolution(entity.Comp.Solution.Value, quantity); _solutionContainerSystem.TryAddSolution(targetSoln.Value, split); - _popupSystem.PopupEntity(Loc.GetString("udder-system-success", ("amount", quantity), ("target", Identity.Entity(args.Args.Used.Value, EntityManager))), entity.Owner, + _popupSystem.PopupClient(Loc.GetString("udder-system-success", ("amount", quantity), ("target", Identity.Entity(args.Args.Used.Value, EntityManager))), entity.Owner, args.Args.User, PopupType.Medium); } @@ -134,4 +140,50 @@ internal sealed class UdderSystem : EntitySystem }; args.Verbs.Add(verb); } + + /// + /// Defines the text provided on examine. + /// Changes depending on the amount of hunger the target has. + /// + private void OnExamine(Entity entity, ref ExaminedEvent args) + { + + var entityIdentity = Identity.Entity(args.Examined, EntityManager); + + string message; + + // Check if the target has hunger, otherwise return not hungry. + if (!TryComp(entity, out var hunger)) + { + message = Loc.GetString("udder-system-examine-none", ("entity", entityIdentity)); + args.PushMarkup(message); + return; + } + + // Choose the correct examine string based on HungerThreshold. + switch (_hunger.GetHungerThreshold(hunger)) + { + case >= HungerThreshold.Overfed: + message = Loc.GetString("udder-system-examine-overfed", ("entity", entityIdentity)); + break; + + case HungerThreshold.Okay: + message = Loc.GetString("udder-system-examine-okay", ("entity", entityIdentity)); + break; + + case HungerThreshold.Peckish: + message = Loc.GetString("udder-system-examine-hungry", ("entity", entityIdentity)); + break; + + // There's a final hunger threshold called "dead" but animals don't actually die so we'll re-use this. + case <= HungerThreshold.Starving: + message = Loc.GetString("udder-system-examine-starved", ("entity", entityIdentity)); + break; + + default: + return; + } + + args.PushMarkup(message); + } } diff --git a/Content.Server/Animals/Components/WoolyComponent.cs b/Content.Shared/Animals/WoolyComponent.cs similarity index 66% rename from Content.Server/Animals/Components/WoolyComponent.cs rename to Content.Shared/Animals/WoolyComponent.cs index c09c6f5e08..1dfe523001 100644 --- a/Content.Server/Animals/Components/WoolyComponent.cs +++ b/Content.Shared/Animals/WoolyComponent.cs @@ -1,23 +1,22 @@ -using Content.Server.Animals.Systems; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Server.Animals.Components; +namespace Content.Shared.Animals; /// -/// Lets an entity produce wool fibers. Uses hunger if present. +/// Gives the ability to produce wool fibers; +/// produces endlessly if the owner does not have a HungerComponent. /// - -[RegisterComponent, Access(typeof(WoolySystem))] +[RegisterComponent, AutoGenerateComponentState, AutoGenerateComponentPause, NetworkedComponent] public sealed partial class WoolyComponent : Component { /// /// The reagent to grow. /// - [DataField, ViewVariables(VVAccess.ReadOnly)] + [DataField, AutoNetworkedField] public ProtoId ReagentId = "Fiber"; /// @@ -29,30 +28,30 @@ public sealed partial class WoolyComponent : Component /// /// The solution to add reagent to. /// - [DataField] + [DataField, ViewVariables(VVAccess.ReadOnly)] public Entity? Solution; /// /// The amount of reagent to be generated on update. /// - [DataField, ViewVariables(VVAccess.ReadOnly)] + [DataField, AutoNetworkedField] public FixedPoint2 Quantity = 25; /// /// The amount of nutrient consumed on update. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public float HungerUsage = 10f; /// /// How long to wait before growing wool. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public TimeSpan GrowthDelay = TimeSpan.FromMinutes(1); /// /// When to next try growing wool. /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] - public TimeSpan NextGrowth = TimeSpan.FromSeconds(0); + [DataField, AutoPausedField, Access(typeof(WoolySystem))] + public TimeSpan NextGrowth = TimeSpan.Zero; } diff --git a/Content.Server/Animals/Systems/WoolySystem.cs b/Content.Shared/Animals/WoolySystem.cs similarity index 74% rename from Content.Server/Animals/Systems/WoolySystem.cs rename to Content.Shared/Animals/WoolySystem.cs index ef0ba086ea..b7e0f52982 100644 --- a/Content.Server/Animals/Systems/WoolySystem.cs +++ b/Content.Shared/Animals/WoolySystem.cs @@ -1,16 +1,15 @@ -using Content.Server.Animals.Components; -using Content.Server.Nutrition; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Mobs.Systems; +using Content.Shared.Nutrition; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; using Robust.Shared.Timing; -namespace Content.Server.Animals.Systems; +namespace Content.Shared.Animals; /// -/// Gives ability to produce fiber reagents, produces endless if the -/// owner has no HungerComponent +/// Gives ability to produce fiber reagents; +/// produces endlessly if the owner has no HungerComponent. /// public sealed class WoolySystem : EntitySystem { @@ -24,6 +23,12 @@ public sealed class WoolySystem : EntitySystem base.Initialize(); SubscribeLocalEvent(OnBeforeFullyEaten); + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(EntityUid uid, WoolyComponent component, MapInitEvent args) + { + component.NextGrowth = _timing.CurTime + component.GrowthDelay; } public override void Update(float frameTime) @@ -31,17 +36,22 @@ public sealed class WoolySystem : EntitySystem base.Update(frameTime); var query = EntityQueryEnumerator(); - var now = _timing.CurTime; while (query.MoveNext(out var uid, out var wooly)) { - if (now < wooly.NextGrowth) + if (_timing.CurTime < wooly.NextGrowth) continue; - wooly.NextGrowth = now + wooly.GrowthDelay; + wooly.NextGrowth += wooly.GrowthDelay; if (_mobState.IsDead(uid)) continue; + if (!_solutionContainer.ResolveSolution(uid, wooly.SolutionName, ref wooly.Solution, out var solution)) + continue; + + if (solution.AvailableVolume == 0) + continue; + // Actually there is food digestion so no problem with instant reagent generation "OnFeed" if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger)) { @@ -52,9 +62,6 @@ public sealed class WoolySystem : EntitySystem _hunger.ModifyHunger(uid, -wooly.HungerUsage, hunger); } - if (!_solutionContainer.ResolveSolution(uid, wooly.SolutionName, ref wooly.Solution)) - continue; - _solutionContainer.TryAddReagent(wooly.Solution.Value, wooly.ReagentId, wooly.Quantity, out _); } } diff --git a/Content.Server/Nutrition/IngestionEvents.cs b/Content.Shared/Nutrition/IngestionEvents.cs similarity index 96% rename from Content.Server/Nutrition/IngestionEvents.cs rename to Content.Shared/Nutrition/IngestionEvents.cs index ae1d22fb71..488605522a 100644 --- a/Content.Server/Nutrition/IngestionEvents.cs +++ b/Content.Shared/Nutrition/IngestionEvents.cs @@ -1,4 +1,4 @@ -namespace Content.Server.Nutrition; +namespace Content.Shared.Nutrition; /// /// Raised directed at the consumer when attempting to ingest something. diff --git a/Resources/Locale/en-US/animals/udder/udder-system.ftl b/Resources/Locale/en-US/animals/udder/udder-system.ftl index 8479ae08bf..959a4fef59 100644 --- a/Resources/Locale/en-US/animals/udder/udder-system.ftl +++ b/Resources/Locale/en-US/animals/udder/udder-system.ftl @@ -5,3 +5,9 @@ udder-system-success = You fill {THE($target)} with {$amount}u from the udder. udder-system-dry = The udder is dry. udder-system-verb-milk = Milk + +udder-system-examine-overfed = {CAPITALIZE(SUBJECT($entity))} looks stuffed! +udder-system-examine-okay = {CAPITALIZE(SUBJECT($entity))} looks content. +udder-system-examine-hungry = {CAPITALIZE(SUBJECT($entity))} looks hungry. +udder-system-examine-starved = {CAPITALIZE(SUBJECT($entity))} looks starved! +udder-system-examine-none = {CAPITALIZE(SUBJECT($entity))} seems not to get hungry.