Animals obey conservation of matter unless they are undead (#21922)

This commit is contained in:
Sirionaut
2023-12-11 04:20:41 +01:00
committed by GitHub
parent 07d8b14af0
commit c095b7cd4a
7 changed files with 212 additions and 161 deletions

View File

@@ -1,7 +1,6 @@
using Content.Shared.Storage; using Content.Shared.Storage;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Animals.Components; namespace Content.Server.Animals.Components;
@@ -9,28 +8,29 @@ namespace Content.Server.Animals.Components;
/// This component handles animals which lay eggs (or some other item) on a timer, using up hunger to do so. /// This component handles animals which lay eggs (or some other item) on a timer, using up hunger to do so.
/// It also grants an action to players who are controlling these entities, allowing them to do it manually. /// It also grants an action to players who are controlling these entities, allowing them to do it manually.
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent]
public sealed partial class EggLayerComponent : Component public sealed partial class EggLayerComponent : Component
{ {
[DataField("eggLayAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] [DataField]
public string EggLayAction = "ActionAnimalLayEgg"; public EntProtoId EggLayAction = "ActionAnimalLayEgg";
[ViewVariables(VVAccess.ReadWrite)] /// <summary>
[DataField("hungerUsage")] /// The amount of nutrient consumed on update.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float HungerUsage = 60f; public float HungerUsage = 60f;
/// <summary> /// <summary>
/// Minimum cooldown used for the automatic egg laying. /// Minimum cooldown used for the automatic egg laying.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField("eggLayCooldownMin")]
public float EggLayCooldownMin = 60f; public float EggLayCooldownMin = 60f;
/// <summary> /// <summary>
/// Maximum cooldown used for the automatic egg laying. /// Maximum cooldown used for the automatic egg laying.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField("eggLayCooldownMax")]
public float EggLayCooldownMax = 120f; public float EggLayCooldownMax = 120f;
/// <summary> /// <summary>
@@ -39,14 +39,13 @@ public sealed partial class EggLayerComponent : Component
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public float CurrentEggLayCooldown; public float CurrentEggLayCooldown;
[ViewVariables(VVAccess.ReadWrite)] [DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
[DataField("eggSpawn", required: true)]
public List<EntitySpawnEntry> EggSpawn = default!; public List<EntitySpawnEntry> EggSpawn = default!;
[DataField("eggLaySound")] [DataField]
public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg"); public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg");
[DataField("accumulatedFrametime")] [DataField]
public float AccumulatedFrametime; public float AccumulatedFrametime;
[DataField] public EntityUid? Action; [DataField] public EntityUid? Action;

View File

@@ -1,9 +1,15 @@
using Content.Server.Animals.Systems; using Content.Server.Animals.Systems;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Animals.Components namespace Content.Server.Animals.Components
/// <summary>
/// Lets an entity produce milk. Uses hunger if present.
/// </summary>
{ {
[RegisterComponent, Access(typeof(UdderSystem))] [RegisterComponent, Access(typeof(UdderSystem))]
internal sealed partial class UdderComponent : Component internal sealed partial class UdderComponent : Component
@@ -11,31 +17,37 @@ namespace Content.Server.Animals.Components
/// <summary> /// <summary>
/// The reagent to produce. /// The reagent to produce.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadOnly)] [DataField, ViewVariables(VVAccess.ReadOnly)]
[DataField("reagentId", customTypeSerializer:typeof(PrototypeIdSerializer<ReagentPrototype>))] public ProtoId<ReagentPrototype> ReagentId = "Milk";
public string ReagentId = "Milk";
/// <summary> /// <summary>
/// The solution to add reagent to. /// The solution to add reagent to.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadOnly)] [DataField, ViewVariables(VVAccess.ReadOnly)]
[DataField("targetSolution")] public string Solution = "udder";
public string TargetSolutionName = "udder";
/// <summary> /// <summary>
/// The amount of reagent to be generated on update. /// The amount of reagent to be generated on update.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadOnly)] [DataField, ViewVariables(VVAccess.ReadOnly)]
[DataField("quantity")] public FixedPoint2 QuantityPerUpdate = 25;
public FixedPoint2 QuantityPerUpdate = 1;
/// <summary> /// <summary>
/// The time between updates (in seconds). /// The amount of nutrient consumed on update.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadOnly)] [DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField("updateRate")] public float HungerUsage = 10f;
public float UpdateRate = 5;
public float AccumulatedFrameTime; /// <summary>
/// How long to wait before producing.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public TimeSpan GrowthDelay = TimeSpan.FromMinutes(1);
/// <summary>
/// When to next try to produce.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan NextGrowth = TimeSpan.FromSeconds(0);
} }
} }

View File

@@ -4,29 +4,38 @@ using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Animals.Components;
/// <summary> /// <summary>
/// Lets an animal grow a wool solution when not hungry. /// Lets an entity produce wool fibers. Uses hunger if present.
/// </summary> /// </summary>
[RegisterComponent, Access(typeof(WoolySystem))] [RegisterComponent, Access(typeof(WoolySystem))]
public sealed partial class WoolyComponent : Component public sealed partial class WoolyComponent : Component
{ {
/// <summary> /// <summary>
/// What reagent to grow. /// The reagent to grow.
/// </summary> /// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadOnly)]
public ProtoId<ReagentPrototype> ReagentId = "Fiber"; public ProtoId<ReagentPrototype> ReagentId = "Fiber";
/// <summary> /// <summary>
/// How much wool to grow at every growth cycle. /// The solution to add reagent to.
/// </summary> /// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadOnly)]
public string Solution = "wool";
/// <summary>
/// The amount of reagent to be generated on update.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public FixedPoint2 Quantity = 25; public FixedPoint2 Quantity = 25;
/// <summary> /// <summary>
/// What solution to add the wool reagent to. /// The amount of nutrient consumed on update.
/// </summary> /// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadWrite)]
public string Solution = "wool"; public float HungerUsage = 10f;
/// <summary> /// <summary>
/// How long to wait before growing wool. /// How long to wait before growing wool.

View File

@@ -2,6 +2,7 @@ using Content.Server.Actions;
using Content.Server.Animals.Components; using Content.Server.Animals.Components;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Shared.Actions.Events; using Content.Shared.Actions.Events;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Storage; using Content.Shared.Storage;
@@ -12,6 +13,10 @@ using Robust.Shared.Random;
namespace Content.Server.Animals.Systems; namespace Content.Server.Animals.Systems;
/// <summary>
/// Gives ability to produce eggs, produces endless if the
/// owner has no HungerComponent
/// </summary>
public sealed class EggLayerSystem : EntitySystem public sealed class EggLayerSystem : EntitySystem
{ {
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
@@ -19,6 +24,7 @@ public sealed class EggLayerSystem : EntitySystem
[Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly HungerSystem _hunger = default!; [Dependency] private readonly HungerSystem _hunger = default!;
[Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -57,35 +63,38 @@ public sealed class EggLayerSystem : EntitySystem
component.CurrentEggLayCooldown = _random.NextFloat(component.EggLayCooldownMin, component.EggLayCooldownMax); component.CurrentEggLayCooldown = _random.NextFloat(component.EggLayCooldownMin, component.EggLayCooldownMax);
} }
private void OnEggLayAction(EntityUid uid, EggLayerComponent component, EggLayInstantActionEvent args) private void OnEggLayAction(EntityUid uid, EggLayerComponent egglayer, EggLayInstantActionEvent args)
{ {
args.Handled = TryLayEgg(uid, component); args.Handled = TryLayEgg(uid, egglayer);
} }
public bool TryLayEgg(EntityUid uid, EggLayerComponent? component) public bool TryLayEgg(EntityUid uid, EggLayerComponent? egglayer)
{ {
if (!Resolve(uid, ref component)) if (!Resolve(uid, ref egglayer))
return false;
if (_mobState.IsDead(uid))
return false; return false;
// Allow infinitely laying eggs if they can't get hungry // Allow infinitely laying eggs if they can't get hungry
if (TryComp<HungerComponent>(uid, out var hunger)) if (TryComp<HungerComponent>(uid, out var hunger))
{ {
if (hunger.CurrentHunger < component.HungerUsage) if (hunger.CurrentHunger < egglayer.HungerUsage)
{ {
_popup.PopupEntity(Loc.GetString("action-popup-lay-egg-too-hungry"), uid, uid); _popup.PopupEntity(Loc.GetString("action-popup-lay-egg-too-hungry"), uid, uid);
return false; return false;
} }
_hunger.ModifyHunger(uid, -component.HungerUsage, hunger); _hunger.ModifyHunger(uid, -egglayer.HungerUsage, hunger);
} }
foreach (var ent in EntitySpawnCollection.GetSpawns(component.EggSpawn, _random)) foreach (var ent in EntitySpawnCollection.GetSpawns(egglayer.EggSpawn, _random))
{ {
Spawn(ent, Transform(uid).Coordinates); Spawn(ent, Transform(uid).Coordinates);
} }
// Sound + popups // Sound + popups
_audio.PlayPvs(component.EggLaySound, uid); _audio.PlayPvs(egglayer.EggLaySound, uid);
_popup.PopupEntity(Loc.GetString("action-popup-lay-egg-user"), uid, uid); _popup.PopupEntity(Loc.GetString("action-popup-lay-egg-user"), uid, uid);
_popup.PopupEntity(Loc.GetString("action-popup-lay-egg-others", ("entity", uid)), uid, Filter.PvsExcept(uid), true); _popup.PopupEntity(Loc.GetString("action-popup-lay-egg-others", ("entity", uid)), uid, Filter.PvsExcept(uid), true);

View File

@@ -4,23 +4,28 @@ using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Udder; using Content.Shared.Udder;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Timing;
namespace Content.Server.Animals.Systems;
namespace Content.Server.Animals.Systems
{
/// <summary> /// <summary>
/// Gives ability to living beings with acceptable hunger level to produce milkable reagents. /// Gives ability to produce milkable reagents, produces endless if the
/// owner has no HungerComponent
/// </summary> /// </summary>
internal sealed class UdderSystem : EntitySystem internal sealed class UdderSystem : EntitySystem
{ {
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly HungerSystem _hunger = default!; [Dependency] private readonly HungerSystem _hunger = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -32,40 +37,44 @@ namespace Content.Server.Animals.Systems
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(frameTime);
var query = EntityQueryEnumerator<UdderComponent>(); var query = EntityQueryEnumerator<UdderComponent>();
var now = _timing.CurTime;
while (query.MoveNext(out var uid, out var udder)) while (query.MoveNext(out var uid, out var udder))
{ {
udder.AccumulatedFrameTime += frameTime; if (now < udder.NextGrowth)
continue;
while (udder.AccumulatedFrameTime > udder.UpdateRate) udder.NextGrowth = now + udder.GrowthDelay;
{
udder.AccumulatedFrameTime -= udder.UpdateRate; if (_mobState.IsDead(uid))
continue;
// Actually there is food digestion so no problem with instant reagent generation "OnFeed" // Actually there is food digestion so no problem with instant reagent generation "OnFeed"
if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger)) if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger))
{ {
// Is there enough nutrition to produce reagent? // Is there enough nutrition to produce reagent?
if (_hunger.GetHungerThreshold(hunger) < HungerThreshold.Peckish) if (_hunger.GetHungerThreshold(hunger) < HungerThreshold.Okay)
continue; continue;
_hunger.ModifyHunger(uid, -udder.HungerUsage, hunger);
} }
if (!_solutionContainerSystem.TryGetSolution(uid, udder.TargetSolutionName, if (!_solutionContainerSystem.TryGetSolution(uid, udder.Solution, out var solution))
out var solution))
continue; continue;
//TODO: toxins from bloodstream !? //TODO: toxins from bloodstream !?
_solutionContainerSystem.TryAddReagent(uid, solution, udder.ReagentId, _solutionContainerSystem.TryAddReagent(uid, solution, udder.ReagentId, udder.QuantityPerUpdate, out _);
udder.QuantityPerUpdate, out var accepted);
}
} }
} }
private void AttemptMilk(EntityUid uid, EntityUid userUid, EntityUid containerUid, UdderComponent? udder = null) private void AttemptMilk(Entity<UdderComponent?> udder, EntityUid userUid, EntityUid containerUid)
{ {
if (!Resolve(uid, ref udder)) if (!Resolve(udder, ref udder.Comp))
return; return;
var doargs = new DoAfterArgs(EntityManager, userUid, 5, new MilkingDoAfterEvent(), uid, uid, used: containerUid) var doargs = new DoAfterArgs(EntityManager, userUid, 5, new MilkingDoAfterEvent(), udder, udder, used: containerUid)
{ {
BreakOnUserMove = true, BreakOnUserMove = true,
BreakOnDamage = true, BreakOnDamage = true,
@@ -81,7 +90,7 @@ namespace Content.Server.Animals.Systems
if (args.Cancelled || args.Handled || args.Args.Used == null) if (args.Cancelled || args.Handled || args.Args.Used == null)
return; return;
if (!_solutionContainerSystem.TryGetSolution(uid, component.TargetSolutionName, out var solution)) if (!_solutionContainerSystem.TryGetSolution(uid, component.Solution, out var solution))
return; return;
if (!_solutionContainerSystem.TryGetRefillableSolution(args.Args.Used.Value, out var targetSolution)) if (!_solutionContainerSystem.TryGetRefillableSolution(args.Args.Used.Value, out var targetSolution))
@@ -116,7 +125,7 @@ namespace Content.Server.Animals.Systems
{ {
Act = () => Act = () =>
{ {
AttemptMilk(uid, args.User, args.Using.Value, component); AttemptMilk(uid, args.User, args.Using.Value);
}, },
Text = Loc.GetString("udder-system-verb-milk"), Text = Loc.GetString("udder-system-verb-milk"),
Priority = 2 Priority = 2
@@ -124,4 +133,3 @@ namespace Content.Server.Animals.Systems
args.Verbs.Add(verb); args.Verbs.Add(verb);
} }
} }
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Animals.Components; using Content.Server.Animals.Components;
using Content.Server.Nutrition; using Content.Server.Nutrition;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Nutrition.EntitySystems;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -8,13 +9,14 @@ using Robust.Shared.Timing;
namespace Content.Server.Animals.Systems; namespace Content.Server.Animals.Systems;
/// <summary> /// <summary>
/// Handles regeneration of an animal's wool solution when not hungry. /// Gives ability to produce fiber reagents, produces endless if the
/// Shearing is not currently possible so the only use is for moths to eat. /// owner has no HungerComponent
/// </summary> /// </summary>
public sealed class WoolySystem : EntitySystem public sealed class WoolySystem : EntitySystem
{ {
[Dependency] private readonly HungerSystem _hunger = default!; [Dependency] private readonly HungerSystem _hunger = default!;
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
public override void Initialize() public override void Initialize()
@@ -28,23 +30,32 @@ public sealed class WoolySystem : EntitySystem
{ {
base.Update(frameTime); base.Update(frameTime);
var query = EntityQueryEnumerator<WoolyComponent, HungerComponent>(); var query = EntityQueryEnumerator<WoolyComponent>();
var now = _timing.CurTime; var now = _timing.CurTime;
while (query.MoveNext(out var uid, out var comp, out var hunger)) while (query.MoveNext(out var uid, out var wooly))
{ {
if (now < comp.NextGrowth) if (now < wooly.NextGrowth)
continue; continue;
comp.NextGrowth = now + comp.GrowthDelay; wooly.NextGrowth = now + wooly.GrowthDelay;
if (_mobState.IsDead(uid))
continue;
// Actually there is food digestion so no problem with instant reagent generation "OnFeed"
if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger))
{
// Is there enough nutrition to produce reagent? // Is there enough nutrition to produce reagent?
if (_hunger.GetHungerThreshold(hunger) < HungerThreshold.Peckish) if (_hunger.GetHungerThreshold(hunger) < HungerThreshold.Okay)
continue; continue;
if (!_solutionContainer.TryGetSolution(uid, comp.Solution, out var solution)) _hunger.ModifyHunger(uid, -wooly.HungerUsage, hunger);
}
if (!_solutionContainer.TryGetSolution(uid, wooly.Solution, out var solution))
continue; continue;
_solutionContainer.TryAddReagent(uid, solution, comp.ReagentId, comp.Quantity, out _); _solutionContainer.TryAddReagent(uid, solution, wooly.ReagentId, wooly.Quantity, out _);
} }
} }

View File

@@ -25,6 +25,7 @@ using Content.Shared.Mobs;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Content.Shared.Nutrition.AnimalHusbandry;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Roles; using Content.Shared.Roles;
@@ -96,11 +97,13 @@ namespace Content.Server.Zombies
var zombiecomp = AddComp<ZombieComponent>(target); var zombiecomp = AddComp<ZombieComponent>(target);
//we need to basically remove all of these because zombies shouldn't //we need to basically remove all of these because zombies shouldn't
//get diseases, breath, be thirst, be hungry, or die in space //get diseases, breath, be thirst, be hungry, die in space or have offspring
RemComp<RespiratorComponent>(target); RemComp<RespiratorComponent>(target);
RemComp<BarotraumaComponent>(target); RemComp<BarotraumaComponent>(target);
RemComp<HungerComponent>(target); RemComp<HungerComponent>(target);
RemComp<ThirstComponent>(target); RemComp<ThirstComponent>(target);
RemComp<ReproductiveComponent>(target);
RemComp<ReproductivePartnerComponent>(target);
//funny voice //funny voice
var accentType = "zombie"; var accentType = "zombie";