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.