Debody Food and Drink Systems, Combine Food and Drink into One System. (#39031)
* Shelve * 22 file diff * What if it was just better * Hold that thought * Near final Commit, then YAML hell * 95% done with cs * Working Commit * Final Commit (Before reviews tear it apart and kill me) * Add a really stupid comment. * KILL * EXPLODE TEST FAILS WITH MY MIND * I hate it here * TACTICAL NUCLEAR STRIKE * Wait what the fuck was I doing? * Comments * Me when I'm stupid * Food doesn't need solutions * API improvements with some API weirdness * Move non-API out of API * Better comment * Fixes and spelling mistakes * Final fixes * Final fixes for real... * Kill food and drink localization files because I hate them. * Water droplet fix * Utensil fixes * Fix verb priority (It should've been 2) * A few minor localization fixes * merge conflict and stuff * MERGE CONFLICT NUCLEAR WAR!!! * Cleanup --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
02382045ab
commit
91854e0776
@@ -38,7 +38,7 @@ public sealed class SharpSystem : EntitySystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SharpComponent, AfterInteractEvent>(OnAfterInteract, before: [typeof(UtensilSystem)]);
|
||||
SubscribeLocalEvent<SharpComponent, AfterInteractEvent>(OnAfterInteract, before: [typeof(IngestionSystem)]);
|
||||
SubscribeLocalEvent<SharpComponent, SharpDoAfterEvent>(OnDoAfter);
|
||||
|
||||
SubscribeLocalEvent<ButcherableComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs);
|
||||
|
||||
@@ -44,9 +44,9 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
[Dependency] private readonly ContainerSystem _container = default!;
|
||||
[Dependency] private readonly DrinkSystem _drink = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly FoodSystem _food = default!;
|
||||
[Dependency] private readonly HandsSystem _hands = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly IngestionSystem _ingestion = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||
[Dependency] private readonly OpenableSystem _openable = default!;
|
||||
@@ -174,14 +174,8 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
{
|
||||
case FoodValueCon:
|
||||
{
|
||||
if (!TryComp<FoodComponent>(targetUid, out var food))
|
||||
return 0f;
|
||||
|
||||
// mice can't eat unpeeled bananas, need monkey's help
|
||||
if (_openable.IsClosed(targetUid))
|
||||
return 0f;
|
||||
|
||||
if (!_food.IsDigestibleBy(owner, targetUid, food))
|
||||
// do we have a mouth available? Is the food item opened?
|
||||
if (!_ingestion.CanConsume(owner, targetUid))
|
||||
return 0f;
|
||||
|
||||
var avoidBadFood = !HasComp<IgnoreBadFoodComponent>(owner);
|
||||
@@ -194,15 +188,16 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
if (avoidBadFood && HasComp<BadFoodComponent>(targetUid))
|
||||
return 0f;
|
||||
|
||||
var nutrition = _ingestion.TotalNutrition(targetUid, owner);
|
||||
if (nutrition <= 1.0f)
|
||||
return 0f;
|
||||
|
||||
return 1f;
|
||||
}
|
||||
case DrinkValueCon:
|
||||
{
|
||||
if (!TryComp<DrinkComponent>(targetUid, out var drink))
|
||||
return 0f;
|
||||
|
||||
// can't drink closed drinks
|
||||
if (_openable.IsClosed(targetUid))
|
||||
// can't drink closed drinks and can't drink with a mask on...
|
||||
if (!_ingestion.CanConsume(owner, targetUid))
|
||||
return 0f;
|
||||
|
||||
// only drink when thirsty
|
||||
@@ -214,7 +209,9 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
return 0f;
|
||||
|
||||
// needs to have something that will satiate thirst, mice wont try to drink 100% pure mutagen.
|
||||
var hydration = _drink.TotalHydration(targetUid, drink);
|
||||
// We don't check if the solution is metabolizable cause all drinks should be currently.
|
||||
// If that changes then simply use the other overflow.
|
||||
var hydration = _ingestion.TotalHydration(targetUid);
|
||||
if (hydration <= 1.0f)
|
||||
return 0f;
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Nutrition.Prototypes;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Nutrition.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Entities with this component occasionally spill some of their drink when drinking.
|
||||
/// Entities with this component occasionally spill some of the solution they're ingesting.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class MessyDrinkerComponent : Component
|
||||
@@ -17,6 +19,12 @@ public sealed partial class MessyDrinkerComponent : Component
|
||||
[DataField]
|
||||
public FixedPoint2 SpillAmount = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// The types of food prototypes we can spill
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<ProtoId<EdiblePrototype>> SpillableTypes = new List<ProtoId<EdiblePrototype>> { "Drink" };
|
||||
|
||||
[DataField]
|
||||
public LocId? SpillMessagePopup;
|
||||
}
|
||||
|
||||
@@ -1,50 +1,16 @@
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.Forensics;
|
||||
using Content.Server.Inventory;
|
||||
using Content.Server.Nutrition.Events;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.EntityEffects.Effects;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
|
||||
namespace Content.Server.Nutrition.EntitySystems;
|
||||
|
||||
public sealed class DrinkSystem : SharedDrinkSystem
|
||||
{
|
||||
[Dependency] private readonly BodySystem _body = default!;
|
||||
[Dependency] private readonly FoodSystem _food = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly OpenableSystem _openable = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly PuddleSystem _puddle = default!;
|
||||
[Dependency] private readonly ReactiveSystem _reaction = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly StomachSystem _stomach = default!;
|
||||
[Dependency] private readonly ForensicsSystem _forensics = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -55,59 +21,6 @@ public sealed class DrinkSystem : SharedDrinkSystem
|
||||
SubscribeLocalEvent<DrinkComponent, ComponentInit>(OnDrinkInit);
|
||||
// run before inventory so for bucket it always tries to drink before equipping (when empty)
|
||||
// run after openable so its always open -> drink
|
||||
SubscribeLocalEvent<DrinkComponent, UseInHandEvent>(OnUse, before: [typeof(ServerInventorySystem)], after: [typeof(OpenableSystem)]);
|
||||
SubscribeLocalEvent<DrinkComponent, AfterInteractEvent>(AfterInteract);
|
||||
SubscribeLocalEvent<DrinkComponent, ConsumeDoAfterEvent>(OnDoAfter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the total hydration factor contained in a drink's solution.
|
||||
/// </summary>
|
||||
public float TotalHydration(EntityUid uid, DrinkComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp))
|
||||
return 0f;
|
||||
|
||||
if (!_solutionContainer.TryGetSolution(uid, comp.Solution, out _, out var solution))
|
||||
return 0f;
|
||||
|
||||
var total = 0f;
|
||||
foreach (var quantity in solution.Contents)
|
||||
{
|
||||
var reagent = _proto.Index<ReagentPrototype>(quantity.Reagent.Prototype);
|
||||
if (reagent.Metabolisms == null)
|
||||
continue;
|
||||
|
||||
foreach (var entry in reagent.Metabolisms.Values)
|
||||
{
|
||||
foreach (var effect in entry.Effects)
|
||||
{
|
||||
// ignores any effect conditions, just cares about how much it can hydrate
|
||||
if (effect is SatiateThirst thirst)
|
||||
{
|
||||
total += thirst.HydrationFactor * quantity.Quantity.Float();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private void AfterInteract(Entity<DrinkComponent> entity, ref AfterInteractEvent args)
|
||||
{
|
||||
if (args.Handled || args.Target == null || !args.CanReach)
|
||||
return;
|
||||
|
||||
args.Handled = TryDrink(args.User, args.Target.Value, entity.Comp, entity);
|
||||
}
|
||||
|
||||
private void OnUse(Entity<DrinkComponent> entity, ref UseInHandEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = TryDrink(args.User, args.User, entity.Comp, entity);
|
||||
}
|
||||
|
||||
private void OnDrinkInit(Entity<DrinkComponent> entity, ref ComponentInit args)
|
||||
@@ -147,115 +60,4 @@ public sealed class DrinkSystem : SharedDrinkSystem
|
||||
var drainAvailable = DrinkVolume(uid, component);
|
||||
_appearance.SetData(uid, FoodVisuals.Visual, drainAvailable.Float(), appearance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed at a victim when someone has force fed them a drink.
|
||||
/// </summary>
|
||||
private void OnDoAfter(Entity<DrinkComponent> entity, ref ConsumeDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || entity.Comp.Deleted)
|
||||
return;
|
||||
|
||||
if (!TryComp<BodyComponent>(args.Target, out var body))
|
||||
return;
|
||||
|
||||
if (args.Used is null || !_solutionContainer.TryGetSolution(args.Used.Value, args.Solution, out var soln, out var solution))
|
||||
return;
|
||||
|
||||
if (_openable.IsClosed(args.Used.Value, args.Target.Value, predicted: true))
|
||||
return;
|
||||
|
||||
// TODO this should really be checked every tick.
|
||||
if (_food.IsMouthBlocked(args.Target.Value))
|
||||
return;
|
||||
|
||||
// TODO this should really be checked every tick.
|
||||
if (!_interaction.InRangeUnobstructed(args.User, args.Target.Value))
|
||||
return;
|
||||
|
||||
var transferAmount = FixedPoint2.Min(entity.Comp.TransferAmount, solution.Volume);
|
||||
var drained = _solutionContainer.SplitSolution(soln.Value, transferAmount);
|
||||
var forceDrink = args.User != args.Target;
|
||||
|
||||
args.Handled = true;
|
||||
if (transferAmount <= 0)
|
||||
return;
|
||||
|
||||
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>((args.Target.Value, body), out var stomachs))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString(forceDrink ? "drink-component-try-use-drink-cannot-drink-other" : "drink-component-try-use-drink-had-enough"), args.Target.Value, args.User);
|
||||
|
||||
if (HasComp<RefillableSolutionComponent>(args.Target.Value))
|
||||
{
|
||||
_puddle.TrySpillAt(args.User, drained, out _);
|
||||
return;
|
||||
}
|
||||
|
||||
_solutionContainer.Refill(args.Target.Value, soln.Value, drained);
|
||||
return;
|
||||
}
|
||||
|
||||
var firstStomach = stomachs.FirstOrNull(stomach => _stomach.CanTransferSolution(stomach.Owner, drained, stomach.Comp1));
|
||||
|
||||
//All stomachs are full or can't handle whatever solution we have.
|
||||
if (firstStomach == null)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough"), args.Target.Value, args.Target.Value);
|
||||
|
||||
if (forceDrink)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough-other"), args.Target.Value, args.User);
|
||||
_puddle.TrySpillAt(args.Target.Value, drained, out _);
|
||||
}
|
||||
else
|
||||
_solutionContainer.TryAddSolution(soln.Value, drained);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var flavors = args.FlavorMessage;
|
||||
|
||||
if (forceDrink)
|
||||
{
|
||||
var targetName = Identity.Entity(args.Target.Value, EntityManager);
|
||||
var userName = Identity.Entity(args.User, EntityManager);
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("drink-component-force-feed-success", ("user", userName), ("flavors", flavors)), args.Target.Value, args.Target.Value);
|
||||
|
||||
_popup.PopupEntity(
|
||||
Loc.GetString("drink-component-force-feed-success-user", ("target", targetName)),
|
||||
args.User, args.User);
|
||||
|
||||
// log successful forced drinking
|
||||
_adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity.Owner):user} forced {ToPrettyString(args.User):target} to drink {ToPrettyString(entity.Owner):drink}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupEntity(
|
||||
Loc.GetString("drink-component-try-use-drink-success-slurp-taste", ("flavors", flavors)), args.User,
|
||||
args.User);
|
||||
_popup.PopupEntity(
|
||||
Loc.GetString("drink-component-try-use-drink-success-slurp"), args.User, Filter.PvsExcept(args.User), true);
|
||||
|
||||
// log successful voluntary drinking
|
||||
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} drank {ToPrettyString(entity.Owner):drink}");
|
||||
}
|
||||
|
||||
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-2f).WithVariation(0.25f));
|
||||
|
||||
var beforeDrinkEvent = new BeforeIngestDrinkEvent(entity.Owner, drained, forceDrink);
|
||||
RaiseLocalEvent(args.Target.Value, ref beforeDrinkEvent);
|
||||
|
||||
_forensics.TransferDna(entity, args.Target.Value);
|
||||
|
||||
_reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion);
|
||||
|
||||
if (drained.Volume == 0)
|
||||
return;
|
||||
|
||||
_stomach.TryTransferSolution(firstStomach.Value.Owner, drained, firstStomach.Value.Comp1);
|
||||
|
||||
if (!forceDrink && solution.Volume > 0)
|
||||
args.Repeat = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.Nutrition.Components;
|
||||
using Content.Server.Nutrition.Events;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
@@ -8,24 +9,30 @@ namespace Content.Server.Nutrition.EntitySystems;
|
||||
|
||||
public sealed class MessyDrinkerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly PuddleSystem _puddle = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IngestionSystem _ingestion = default!;
|
||||
[Dependency] private readonly PuddleSystem _puddle = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<MessyDrinkerComponent, BeforeIngestDrinkEvent>(OnBeforeIngestDrink);
|
||||
SubscribeLocalEvent<MessyDrinkerComponent, IngestingEvent>(OnIngested);
|
||||
}
|
||||
|
||||
private void OnBeforeIngestDrink(Entity<MessyDrinkerComponent> ent, ref BeforeIngestDrinkEvent ev)
|
||||
private void OnIngested(Entity<MessyDrinkerComponent> ent, ref IngestingEvent ev)
|
||||
{
|
||||
if (ev.Solution.Volume <= ent.Comp.SpillAmount)
|
||||
if (ev.Split.Volume <= ent.Comp.SpillAmount)
|
||||
return;
|
||||
|
||||
var proto = _ingestion.GetEdibleType(ev.Food);
|
||||
|
||||
if (proto == null || !ent.Comp.SpillableTypes.Contains(proto.Value))
|
||||
return;
|
||||
|
||||
// Cannot spill if you're being forced to drink.
|
||||
if (ev.Forced)
|
||||
if (ev.ForceFed)
|
||||
return;
|
||||
|
||||
if (!_random.Prob(ent.Comp.SpillChance))
|
||||
@@ -34,7 +41,7 @@ public sealed class MessyDrinkerSystem : EntitySystem
|
||||
if (ent.Comp.SpillMessagePopup != null)
|
||||
_popup.PopupEntity(Loc.GetString(ent.Comp.SpillMessagePopup), ent, ent, PopupType.MediumCaution);
|
||||
|
||||
var split = ev.Solution.SplitSolution(ent.Comp.SpillAmount);
|
||||
var split = ev.Split.SplitSolution(ent.Comp.SpillAmount);
|
||||
|
||||
_puddle.TrySpillAt(ent, split, out _);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ public sealed class SliceableFoodSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedDestructibleSystem _destroy = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
@@ -64,31 +65,27 @@ public sealed class SliceableFoodSystem : EntitySystem
|
||||
if (args.Cancelled || args.Handled || args.Args.Target == null)
|
||||
return;
|
||||
|
||||
if (TrySliceFood(entity, args.User, args.Used, entity.Comp))
|
||||
if (TrySliceFood(entity.Owner, args.User, args.Used))
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private bool TrySliceFood(EntityUid uid,
|
||||
private bool TrySliceFood(Entity<TransformComponent?, SliceableFoodComponent?, EdibleComponent?> entity,
|
||||
EntityUid user,
|
||||
EntityUid? usedItem,
|
||||
SliceableFoodComponent? component = null,
|
||||
FoodComponent? food = null,
|
||||
TransformComponent? transform = null)
|
||||
EntityUid? usedItem)
|
||||
{
|
||||
if (!Resolve(uid, ref component, ref food, ref transform) ||
|
||||
string.IsNullOrEmpty(component.Slice))
|
||||
if (!Resolve(entity, ref entity.Comp1, ref entity.Comp2, ref entity.Comp3) || string.IsNullOrEmpty(entity.Comp2.Slice))
|
||||
return false;
|
||||
|
||||
if (!_solutionContainer.TryGetSolution(uid, food.Solution, out var soln, out var solution))
|
||||
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp3.Solution, out var soln, out var solution))
|
||||
return false;
|
||||
|
||||
if (!TryComp<UtensilComponent>(usedItem, out var utensil) || (utensil.Types & UtensilType.Knife) == 0)
|
||||
return false;
|
||||
|
||||
var sliceVolume = solution.Volume / FixedPoint2.New(component.TotalCount);
|
||||
for (int i = 0; i < component.TotalCount; i++)
|
||||
var sliceVolume = solution.Volume / FixedPoint2.New(entity.Comp2.TotalCount);
|
||||
for (int i = 0; i < entity.Comp2.TotalCount; i++)
|
||||
{
|
||||
var sliceUid = Slice(uid, user, component, transform);
|
||||
var sliceUid = Slice(entity, user);
|
||||
|
||||
var lostSolution =
|
||||
_solutionContainer.SplitSolution(soln.Value, sliceVolume);
|
||||
@@ -97,11 +94,11 @@ public sealed class SliceableFoodSystem : EntitySystem
|
||||
FillSlice(sliceUid, lostSolution);
|
||||
}
|
||||
|
||||
_audio.PlayPvs(component.Sound, transform.Coordinates, AudioParams.Default.WithVolume(-2));
|
||||
_audio.PlayPvs(entity.Comp2.Sound, entity.Comp1.Coordinates, AudioParams.Default.WithVolume(-2));
|
||||
var ev = new SliceFoodEvent();
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
|
||||
DeleteFood(uid, user, food);
|
||||
DeleteFood(entity, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -109,19 +106,16 @@ public sealed class SliceableFoodSystem : EntitySystem
|
||||
/// Create a new slice in the world and returns its entity.
|
||||
/// The solutions must be set afterwards.
|
||||
/// </summary>
|
||||
public EntityUid Slice(EntityUid uid,
|
||||
EntityUid user,
|
||||
SliceableFoodComponent? comp = null,
|
||||
TransformComponent? transform = null)
|
||||
public EntityUid Slice(Entity<TransformComponent?, SliceableFoodComponent?> entity, EntityUid user)
|
||||
{
|
||||
if (!Resolve(uid, ref comp, ref transform))
|
||||
if (!Resolve(entity, ref entity.Comp1, ref entity.Comp2))
|
||||
return EntityUid.Invalid;
|
||||
|
||||
var sliceUid = Spawn(comp.Slice, _transform.GetMapCoordinates(uid));
|
||||
var sliceUid = Spawn(entity.Comp2.Slice, _transform.GetMapCoordinates((entity, entity.Comp1)));
|
||||
|
||||
// try putting the slice into the container if the food being sliced is in a container!
|
||||
// this lets you do things like slice a pizza up inside of a hot food cart without making a food-everywhere mess
|
||||
_transform.DropNextTo(sliceUid, (uid, transform));
|
||||
_transform.DropNextTo(sliceUid, entity);
|
||||
_transform.SetLocalRotation(sliceUid, 0);
|
||||
|
||||
if (!_container.IsEntityOrParentInContainer(sliceUid))
|
||||
@@ -134,7 +128,7 @@ public sealed class SliceableFoodSystem : EntitySystem
|
||||
return sliceUid;
|
||||
}
|
||||
|
||||
private void DeleteFood(EntityUid uid, EntityUid user, FoodComponent foodComp)
|
||||
private void DeleteFood(EntityUid uid, EntityUid user)
|
||||
{
|
||||
var ev = new BeforeFullySlicedEvent
|
||||
{
|
||||
@@ -144,38 +138,32 @@ public sealed class SliceableFoodSystem : EntitySystem
|
||||
if (ev.Cancelled)
|
||||
return;
|
||||
|
||||
var dev = new DestructionEventArgs();
|
||||
RaiseLocalEvent(uid, dev);
|
||||
|
||||
// Locate the sliced food and spawn its trash
|
||||
foreach (var trash in foodComp.Trash)
|
||||
{
|
||||
var trashUid = Spawn(trash, _transform.GetMapCoordinates(uid));
|
||||
|
||||
// try putting the trash in the food's container too, to be consistent with slice spawning?
|
||||
_transform.DropNextTo(trashUid, uid);
|
||||
_transform.SetLocalRotation(trashUid, 0);
|
||||
}
|
||||
|
||||
QueueDel(uid);
|
||||
_destroy.DestroyEntity(uid);
|
||||
}
|
||||
|
||||
private void FillSlice(EntityUid sliceUid, Solution solution)
|
||||
private void FillSlice(Entity<EdibleComponent?> slice, Solution solution)
|
||||
{
|
||||
// Replace all reagents on prototype not just copying poisons (example: slices of eaten pizza should have less nutrition)
|
||||
if (TryComp<FoodComponent>(sliceUid, out var sliceFoodComp) &&
|
||||
_solutionContainer.TryGetSolution(sliceUid, sliceFoodComp.Solution, out var itsSoln, out var itsSolution))
|
||||
{
|
||||
_solutionContainer.RemoveAllSolution(itsSoln.Value);
|
||||
if (!Resolve(slice, ref slice.Comp, false))
|
||||
return;
|
||||
|
||||
var lostSolutionPart = solution.SplitSolution(itsSolution.AvailableVolume);
|
||||
_solutionContainer.TryAddSolution(itsSoln.Value, lostSolutionPart);
|
||||
}
|
||||
// Replace all reagents on prototype not just copying poisons (example: slices of eaten pizza should have less nutrition)
|
||||
if (!_solutionContainer.TryGetSolution(slice.Owner, slice.Comp.Solution, out var itsSoln, out var itsSolution))
|
||||
return;
|
||||
|
||||
_solutionContainer.RemoveAllSolution(itsSoln.Value);
|
||||
|
||||
var lostSolutionPart = solution.SplitSolution(itsSolution.AvailableVolume);
|
||||
_solutionContainer.TryAddSolution(itsSoln.Value, lostSolutionPart);
|
||||
}
|
||||
|
||||
private void OnComponentStartup(Entity<SliceableFoodComponent> entity, ref ComponentStartup args)
|
||||
{
|
||||
var foodComp = EnsureComp<FoodComponent>(entity);
|
||||
// TODO: When Food Component is fully kill delete this awful method
|
||||
// This exists just to make tests fail I guess, awesome!
|
||||
// If you're here because your test just failed, make sure that:
|
||||
// Your food has the edible component
|
||||
// The solution listed in the edible component exists
|
||||
var foodComp = EnsureComp<EdibleComponent>(entity);
|
||||
_solutionContainer.EnsureSolution(entity.Owner, foodComp.Solution, out _);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
[Dependency] private readonly FoodSystem _foodSystem = default!;
|
||||
[Dependency] private readonly IngestionSystem _ingestion = default!;
|
||||
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
|
||||
@@ -42,7 +42,8 @@ namespace Content.Server.Nutrition.EntitySystems
|
||||
if (!args.CanReach
|
||||
|| !_solutionContainerSystem.TryGetRefillableSolution(entity.Owner, out _, out var solution)
|
||||
|| !HasComp<BloodstreamComponent>(args.Target)
|
||||
|| _foodSystem.IsMouthBlocked(args.Target.Value, args.User))
|
||||
|| _ingestion.HasMouthAvailable(args.Target.Value, args.User)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ public sealed partial class PolymorphSystem : EntitySystem
|
||||
SubscribeLocalEvent<PolymorphableComponent, PolymorphActionEvent>(OnPolymorphActionEvent);
|
||||
SubscribeLocalEvent<PolymorphedEntityComponent, RevertPolymorphActionEvent>(OnRevertPolymorphActionEvent);
|
||||
|
||||
SubscribeLocalEvent<PolymorphedEntityComponent, BeforeFullyEatenEvent>(OnBeforeFullyEaten);
|
||||
SubscribeLocalEvent<PolymorphedEntityComponent, BeforeFullySlicedEvent>(OnBeforeFullySliced);
|
||||
SubscribeLocalEvent<PolymorphedEntityComponent, DestructionEventArgs>(OnDestruction);
|
||||
|
||||
@@ -126,16 +125,6 @@ public sealed partial class PolymorphSystem : EntitySystem
|
||||
Revert((ent, ent));
|
||||
}
|
||||
|
||||
private void OnBeforeFullyEaten(Entity<PolymorphedEntityComponent> ent, ref BeforeFullyEatenEvent args)
|
||||
{
|
||||
var (_, comp) = ent;
|
||||
if (comp.Configuration.RevertOnEat)
|
||||
{
|
||||
args.Cancel();
|
||||
Revert((ent, ent));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBeforeFullySliced(Entity<PolymorphedEntityComponent> ent, ref BeforeFullySlicedEvent args)
|
||||
{
|
||||
var (_, comp) = ent;
|
||||
|
||||
@@ -23,7 +23,6 @@ public sealed class WoolySystem : EntitySystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<WoolyComponent, BeforeFullyEatenEvent>(OnBeforeFullyEaten);
|
||||
SubscribeLocalEvent<WoolyComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<WoolyComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
|
||||
}
|
||||
@@ -77,10 +76,4 @@ public sealed class WoolySystem : EntitySystem
|
||||
_solutionContainer.TryAddReagent(wooly.Solution.Value, wooly.ReagentId, wooly.Quantity, out _);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBeforeFullyEaten(Entity<WoolyComponent> ent, ref BeforeFullyEatenEvent args)
|
||||
{
|
||||
// don't want moths to delete goats after eating them
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Events;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Rejuvenate;
|
||||
using Content.Shared.Slippery;
|
||||
@@ -24,6 +25,7 @@ public abstract class SharedGodmodeSystem : EntitySystem
|
||||
SubscribeLocalEvent<GodmodeComponent, BeforeStatusEffectAddedEvent>(OnBeforeStatusEffect);
|
||||
SubscribeLocalEvent<GodmodeComponent, BeforeOldStatusEffectAddedEvent>(OnBeforeOldStatusEffect);
|
||||
SubscribeLocalEvent<GodmodeComponent, BeforeStaminaDamageEvent>(OnBeforeStaminaDamage);
|
||||
SubscribeLocalEvent<GodmodeComponent, IngestibleEvent>(BeforeEdible);
|
||||
SubscribeLocalEvent<GodmodeComponent, SlipAttemptEvent>(OnSlipAttempt);
|
||||
SubscribeLocalEvent<GodmodeComponent, DestructionAttemptEvent>(OnDestruction);
|
||||
}
|
||||
@@ -60,6 +62,11 @@ public abstract class SharedGodmodeSystem : EntitySystem
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void BeforeEdible(Entity<GodmodeComponent> ent, ref IngestibleEvent args)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
public virtual void EnableGodmode(EntityUid uid, GodmodeComponent? godmode = null)
|
||||
{
|
||||
godmode ??= EnsureComp<GodmodeComponent>(uid);
|
||||
|
||||
@@ -19,6 +19,7 @@ using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Overlays;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Radio;
|
||||
@@ -72,6 +73,7 @@ public partial class InventorySystem
|
||||
SubscribeLocalEvent<InventoryComponent, FlashAttemptEvent>(RefRelayInventoryEvent);
|
||||
SubscribeLocalEvent<InventoryComponent, WieldAttemptEvent>(RefRelayInventoryEvent);
|
||||
SubscribeLocalEvent<InventoryComponent, UnwieldAttemptEvent>(RefRelayInventoryEvent);
|
||||
SubscribeLocalEvent<InventoryComponent, IngestionAttemptEvent>(RefRelayInventoryEvent);
|
||||
|
||||
// Eye/vision events
|
||||
SubscribeLocalEvent<InventoryComponent, CanSeeAttemptEvent>(RelayInventoryEvent);
|
||||
|
||||
@@ -5,6 +5,7 @@ using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Nutrition.Components;
|
||||
|
||||
[Obsolete("Migration to Content.Shared.Nutrition.Components.EdibleComponent is required")]
|
||||
[NetworkedComponent, AutoGenerateComponentState]
|
||||
[RegisterComponent, Access(typeof(SharedDrinkSystem))]
|
||||
public sealed partial class DrinkComponent : Component
|
||||
|
||||
86
Content.Shared/Nutrition/Components/EdibleComponent.cs
Normal file
86
Content.Shared/Nutrition/Components/EdibleComponent.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Nutrition.Prototypes;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Nutrition.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used on an entity with a solution container to flag a specific solution as being able to have its
|
||||
/// reagents consumed directly.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(IngestionSystem))]
|
||||
public sealed partial class EdibleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the solution that stores the consumable reagents
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string Solution = "food";
|
||||
|
||||
/// <summary>
|
||||
/// Should this entity be deleted when our solution is emptied?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool DestroyOnEmpty = true;
|
||||
|
||||
/// <summary>
|
||||
/// Trash we spawn when eaten, will not spawn if the item isn't deleted when empty.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<EntProtoId> Trash = new();
|
||||
|
||||
/// <summary>
|
||||
/// How much of our solution is eaten on a do-after completion. Set to null to eat the whole thing.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public FixedPoint2? TransferAmount = FixedPoint2.New(5);
|
||||
|
||||
/// <summary>
|
||||
/// Acceptable utensils to use
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public UtensilType Utensil = UtensilType.Fork; //There are more "solid" than "liquid" food
|
||||
|
||||
/// <summary>
|
||||
/// Do we need a utensil to access this solution?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool UtensilRequired;
|
||||
|
||||
/// <summary>
|
||||
/// If this is set to true, food can only be eaten if you have a stomach with a
|
||||
/// <see cref="StomachComponent.SpecialDigestible"/> that includes this entity in its whitelist,
|
||||
/// rather than just being digestible by anything that can eat food.
|
||||
/// Whitelist the food component to allow eating of normal food.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool RequiresSpecialDigestion;
|
||||
|
||||
/// <summary>
|
||||
/// How long it takes to eat the food personally.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan Delay = TimeSpan.FromSeconds(1f);
|
||||
|
||||
/// <summary>
|
||||
/// This is how many seconds it takes to force-feed someone this food.
|
||||
/// Should probably be smaller for small items like pills.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan ForceFeedDelay = TimeSpan.FromSeconds(3f);
|
||||
|
||||
/// <summary>
|
||||
/// For mobs that are food, requires killing them before eating.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool RequireDead = true;
|
||||
|
||||
/// <summary>
|
||||
/// Verb, icon, and sound data for our edible.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<EdiblePrototype> Edible = IngestionSystem.Food;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Nutrition.Components;
|
||||
|
||||
[Obsolete("Migration to Content.Shared.Nutrition.Components.EdibleComponent is required")]
|
||||
[RegisterComponent, Access(typeof(FoodSystem), typeof(FoodSequenceSystem))]
|
||||
public sealed partial class FoodComponent : Component
|
||||
{
|
||||
@@ -53,7 +53,7 @@ public sealed partial class FoodComponent : Component
|
||||
/// The localization identifier for the eat message. Needs a "food" entity argument passed to it.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId EatMessage = "food-nom";
|
||||
public LocId EatMessage = "edible-nom";
|
||||
|
||||
/// <summary>
|
||||
/// How long it takes to eat the food personally.
|
||||
|
||||
@@ -9,13 +9,12 @@ namespace Content.Shared.Nutrition.Components;
|
||||
/// In the event that more head-wear & mask functionality is added (like identity systems, or raising/lowering of
|
||||
/// masks), then this component might become redundant.
|
||||
/// </remarks>
|
||||
[RegisterComponent, Access(typeof(FoodSystem), typeof(SharedDrinkSystem), typeof(IngestionBlockerSystem))]
|
||||
[RegisterComponent, Access(typeof(IngestionSystem))]
|
||||
public sealed partial class IngestionBlockerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Is this component currently blocking consumption.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("enabled")]
|
||||
[DataField]
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public sealed partial class OpenableComponent : Component
|
||||
/// Text shown when examining and its open.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId ExamineText = "drink-component-on-examine-is-opened";
|
||||
public LocId ExamineText = "openable-component-on-examine-is-opened";
|
||||
|
||||
/// <summary>
|
||||
/// The locale id for the popup shown when IsClosed is called and closed. Needs a "owner" entity argument passed to it.
|
||||
@@ -44,7 +44,7 @@ public sealed partial class OpenableComponent : Component
|
||||
/// It's still generic enough that you should change it if you make openable non-drinks, i.e. unwrap it first, peel it first.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId ClosedPopup = "drink-component-try-use-drink-not-open";
|
||||
public LocId ClosedPopup = "openable-component-try-use-closed";
|
||||
|
||||
/// <summary>
|
||||
/// Text to show in the verb menu for the "Open" action.
|
||||
|
||||
@@ -22,11 +22,11 @@ public sealed partial class SealableComponent : Component
|
||||
/// Text shown when examining and the item's seal has not been broken.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId ExamineTextSealed = "drink-component-on-examine-is-sealed";
|
||||
public LocId ExamineTextSealed = "sealable-component-on-examine-is-sealed";
|
||||
|
||||
/// <summary>
|
||||
/// Text shown when examining and the item's seal has been broken.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId ExamineTextUnsealed = "drink-component-on-examine-is-unsealed";
|
||||
public LocId ExamineTextUnsealed = "sealable-component-on-examine-is-unsealed";
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Nutrition.Components
|
||||
{
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(UtensilSystem))]
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(IngestionSystem))]
|
||||
public sealed partial class UtensilComponent : Component
|
||||
{
|
||||
[DataField("types")]
|
||||
|
||||
@@ -19,22 +19,30 @@ public sealed class FlavorProfileSystem : EntitySystem
|
||||
|
||||
private int FlavorLimit => _configManager.GetCVar(CCVars.FlavorLimit);
|
||||
|
||||
public string GetLocalizedFlavorsMessage(EntityUid uid, EntityUid user, Solution solution,
|
||||
FlavorProfileComponent? flavorProfile = null)
|
||||
public string GetLocalizedFlavorsMessage(Entity<FlavorProfileComponent?> entity, EntityUid user, Solution? solution)
|
||||
{
|
||||
if (!Resolve(uid, ref flavorProfile, false))
|
||||
HashSet<string> flavors = new();
|
||||
HashSet<string>? ignore = null;
|
||||
|
||||
if (Resolve(entity, ref entity.Comp, false))
|
||||
{
|
||||
return Loc.GetString(BackupFlavorMessage);
|
||||
flavors = entity.Comp.Flavors;
|
||||
ignore = entity.Comp.IgnoreReagents;
|
||||
}
|
||||
|
||||
var flavors = new HashSet<string>(flavorProfile.Flavors);
|
||||
flavors.UnionWith(GetFlavorsFromReagents(solution, FlavorLimit - flavors.Count, flavorProfile.IgnoreReagents));
|
||||
|
||||
if (solution != null)
|
||||
flavors.UnionWith(GetFlavorsFromReagents(solution, FlavorLimit - flavors.Count, ignore));
|
||||
|
||||
var ev = new FlavorProfileModificationEvent(user, flavors);
|
||||
|
||||
RaiseLocalEvent(ev);
|
||||
RaiseLocalEvent(uid, ev);
|
||||
RaiseLocalEvent(entity, ev);
|
||||
RaiseLocalEvent(user, ev);
|
||||
|
||||
if (flavors.Count == 0)
|
||||
return Loc.GetString(BackupFlavorMessage);
|
||||
|
||||
return FlavorsToFlavorMessage(flavors);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,58 +1,36 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Organ;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Forensics;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Components;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles feeding attempts both on yourself and on the target.
|
||||
/// </summary>
|
||||
[Obsolete("Migration to Content.Shared.Nutrition.EntitySystems.IngestionSystem is required")]
|
||||
public sealed class FoodSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedBodySystem _body = default!;
|
||||
[Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
|
||||
[Dependency] private readonly IngestionSystem _ingestion = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly OpenableSystem _openable = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly ReactiveSystem _reaction = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedStackSystem _stack = default!;
|
||||
[Dependency] private readonly StomachSystem _stomach = default!;
|
||||
[Dependency] private readonly UtensilSystem _utensil = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
|
||||
public const float MaxFeedDistance = 1.0f;
|
||||
|
||||
@@ -60,25 +38,35 @@ public sealed class FoodSystem : EntitySystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
// TODO add InteractNoHandEvent for entities like mice.
|
||||
// run after openable for wrapped/peelable foods
|
||||
SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand, after: new[] { typeof(OpenableSystem), typeof(InventorySystem) });
|
||||
SubscribeLocalEvent<FoodComponent, AfterInteractEvent>(OnFeedFood);
|
||||
|
||||
SubscribeLocalEvent<FoodComponent, GetVerbsEvent<AlternativeVerb>>(AddEatVerb);
|
||||
SubscribeLocalEvent<FoodComponent, ConsumeDoAfterEvent>(OnDoAfter);
|
||||
SubscribeLocalEvent<InventoryComponent, IngestionAttemptEvent>(OnInventoryIngestAttempt);
|
||||
|
||||
SubscribeLocalEvent<FoodComponent, BeforeIngestedEvent>(OnBeforeFoodEaten);
|
||||
SubscribeLocalEvent<FoodComponent, IngestedEvent>(OnFoodEaten);
|
||||
SubscribeLocalEvent<FoodComponent, FullyEatenEvent>(OnFoodFullyEaten);
|
||||
|
||||
SubscribeLocalEvent<FoodComponent, GetUtensilsEvent>(OnGetUtensils);
|
||||
|
||||
SubscribeLocalEvent<FoodComponent, IsDigestibleEvent>(OnIsFoodDigestible);
|
||||
|
||||
SubscribeLocalEvent<FoodComponent, EdibleEvent>(OnFood);
|
||||
|
||||
SubscribeLocalEvent<FoodComponent, GetEdibleTypeEvent>(OnGetEdibleType);
|
||||
|
||||
SubscribeLocalEvent<FoodComponent, BeforeFullySlicedEvent>(OnBeforeFullySliced);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eat item
|
||||
/// Eat or drink an item
|
||||
/// </summary>
|
||||
private void OnUseFoodInHand(Entity<FoodComponent> entity, ref UseInHandEvent ev)
|
||||
{
|
||||
if (ev.Handled)
|
||||
return;
|
||||
|
||||
var result = TryFeed(ev.User, ev.User, entity, entity.Comp);
|
||||
ev.Handled = result.Handled;
|
||||
ev.Handled = _ingestion.TryIngest(ev.User, ev.User, entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -89,271 +77,98 @@ public sealed class FoodSystem : EntitySystem
|
||||
if (args.Handled || args.Target == null || !args.CanReach)
|
||||
return;
|
||||
|
||||
var result = TryFeed(args.User, args.Target.Value, entity, entity.Comp);
|
||||
args.Handled = result.Handled;
|
||||
args.Handled = _ingestion.TryIngest(args.User, args.Target.Value, entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to feed the food item to the target entity
|
||||
/// </summary>
|
||||
public (bool Success, bool Handled) TryFeed(EntityUid user, EntityUid target, EntityUid food, FoodComponent foodComp)
|
||||
private void AddEatVerb(Entity<FoodComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
//Suppresses eating yourself and alive mobs
|
||||
if (food == user || (_mobState.IsAlive(food) && foodComp.RequireDead))
|
||||
return (false, false);
|
||||
var user = args.User;
|
||||
|
||||
// Target can't be fed or they're already eating
|
||||
if (!TryComp<BodyComponent>(target, out var body))
|
||||
return (false, false);
|
||||
if (entity.Owner == user || !args.CanInteract || !args.CanAccess)
|
||||
return;
|
||||
|
||||
if (HasComp<UnremoveableComponent>(food))
|
||||
return (false, false);
|
||||
if (!_ingestion.TryGetIngestionVerb(user, entity, IngestionSystem.Food, out var verb))
|
||||
return;
|
||||
|
||||
if (_openable.IsClosed(food, user, predicted: true))
|
||||
return (false, true);
|
||||
|
||||
if (!_solutionContainer.TryGetSolution(food, foodComp.Solution, out _, out var foodSolution))
|
||||
return (false, false);
|
||||
|
||||
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>((target, body), out var stomachs))
|
||||
return (false, false);
|
||||
|
||||
// Check for special digestibles
|
||||
if (!IsDigestibleBy(food, foodComp, stomachs))
|
||||
return (false, false);
|
||||
|
||||
if (!TryGetRequiredUtensils(user, foodComp, out _))
|
||||
return (false, false);
|
||||
|
||||
// Check for used storage on the food item
|
||||
if (TryComp<StorageComponent>(food, out var storageState) && storageState.Container.ContainedEntities.Any())
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
|
||||
return (false, true);
|
||||
}
|
||||
|
||||
// Checks for used item slots
|
||||
if (TryComp<ItemSlotsComponent>(food, out var itemSlots))
|
||||
{
|
||||
if (itemSlots.Slots.Any(slot => slot.Value.HasItem))
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
|
||||
return (false, true);
|
||||
}
|
||||
}
|
||||
|
||||
var flavors = _flavorProfile.GetLocalizedFlavorsMessage(food, user, foodSolution);
|
||||
|
||||
if (GetUsesRemaining(food, foodComp) <= 0)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("food-system-try-use-food-is-empty", ("entity", food)), user, user);
|
||||
DeleteAndSpawnTrash(foodComp, food, user);
|
||||
return (false, true);
|
||||
}
|
||||
|
||||
if (IsMouthBlocked(target, user))
|
||||
return (false, true);
|
||||
|
||||
if (!_interaction.InRangeUnobstructed(user, food, popup: true))
|
||||
return (false, true);
|
||||
|
||||
if (!_interaction.InRangeUnobstructed(user, target, MaxFeedDistance, popup: true))
|
||||
return (false, true);
|
||||
|
||||
// TODO make do-afters account for fixtures in the range check.
|
||||
if (!_transform.GetMapCoordinates(user).InRange(_transform.GetMapCoordinates(target), MaxFeedDistance))
|
||||
{
|
||||
var message = Loc.GetString("interaction-system-user-interaction-cannot-reach");
|
||||
_popup.PopupClient(message, user, user);
|
||||
return (false, true);
|
||||
}
|
||||
|
||||
var forceFeed = user != target;
|
||||
if (forceFeed)
|
||||
{
|
||||
var userName = Identity.Entity(user, EntityManager);
|
||||
_popup.PopupEntity(Loc.GetString("food-system-force-feed", ("user", userName)),
|
||||
user, target);
|
||||
|
||||
// logging
|
||||
_adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to eat {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// log voluntary eating
|
||||
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is eating {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}");
|
||||
}
|
||||
|
||||
var doAfterArgs = new DoAfterArgs(EntityManager,
|
||||
user,
|
||||
forceFeed ? foodComp.ForceFeedDelay : foodComp.Delay,
|
||||
new ConsumeDoAfterEvent(foodComp.Solution, flavors),
|
||||
eventTarget: food,
|
||||
target: target,
|
||||
used: food)
|
||||
{
|
||||
BreakOnHandChange = false,
|
||||
BreakOnMove = forceFeed,
|
||||
BreakOnDamage = true,
|
||||
MovementThreshold = 0.01f,
|
||||
DistanceThreshold = MaxFeedDistance,
|
||||
// do-after will stop if item is dropped when trying to feed someone else
|
||||
// or if the item started out in the user's own hands
|
||||
NeedHand = forceFeed || _hands.IsHolding(user, food),
|
||||
};
|
||||
|
||||
_doAfter.TryStartDoAfter(doAfterArgs);
|
||||
return (true, true);
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
private void OnDoAfter(Entity<FoodComponent> entity, ref ConsumeDoAfterEvent args)
|
||||
private void OnBeforeFoodEaten(Entity<FoodComponent> food, ref BeforeIngestedEvent args)
|
||||
{
|
||||
if (args.Cancelled || args.Handled || entity.Comp.Deleted || args.Target == null)
|
||||
if (args.Cancelled || args.Solution is not { } solution)
|
||||
return;
|
||||
|
||||
if (!TryComp<BodyComponent>(args.Target.Value, out var body))
|
||||
return;
|
||||
// Set it to transfer amount if it exists, otherwise eat the whole volume if possible.
|
||||
args.Transfer = food.Comp.TransferAmount ?? solution.Volume;
|
||||
}
|
||||
|
||||
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>((args.Target.Value, body), out var stomachs))
|
||||
private void OnFoodEaten(Entity<FoodComponent> entity, ref IngestedEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (args.Used is null || !_solutionContainer.TryGetSolution(args.Used.Value, args.Solution, out var soln, out var solution))
|
||||
return;
|
||||
|
||||
if (!TryGetRequiredUtensils(args.User, entity.Comp, out var utensils))
|
||||
return;
|
||||
|
||||
// TODO this should really be checked every tick.
|
||||
if (IsMouthBlocked(args.Target.Value))
|
||||
return;
|
||||
|
||||
// TODO this should really be checked every tick.
|
||||
if (!_interaction.InRangeUnobstructed(args.User, args.Target.Value))
|
||||
return;
|
||||
|
||||
var forceFeed = args.User != args.Target;
|
||||
|
||||
args.Handled = true;
|
||||
var transferAmount = entity.Comp.TransferAmount != null ? FixedPoint2.Min((FixedPoint2) entity.Comp.TransferAmount, solution.Volume) : solution.Volume;
|
||||
|
||||
var split = _solutionContainer.SplitSolution(soln.Value, transferAmount);
|
||||
_audio.PlayPredicted(entity.Comp.UseSound, args.Target, args.User, AudioParams.Default.WithVolume(-1f).WithVariation(0.20f));
|
||||
|
||||
// Get the stomach with the highest available solution volume
|
||||
var highestAvailable = FixedPoint2.Zero;
|
||||
Entity<StomachComponent>? stomachToUse = null;
|
||||
foreach (var ent in stomachs)
|
||||
var flavors = _flavorProfile.GetLocalizedFlavorsMessage(entity.Owner, args.Target, args.Split);
|
||||
|
||||
if (args.ForceFed)
|
||||
{
|
||||
var owner = ent.Owner;
|
||||
if (!_stomach.CanTransferSolution(owner, split, ent.Comp1))
|
||||
continue;
|
||||
|
||||
if (!_solutionContainer.ResolveSolution(owner, StomachSystem.DefaultSolutionName, ref ent.Comp1.Solution, out var stomachSol))
|
||||
continue;
|
||||
|
||||
if (stomachSol.AvailableVolume <= highestAvailable)
|
||||
continue;
|
||||
|
||||
stomachToUse = ent;
|
||||
highestAvailable = stomachSol.AvailableVolume;
|
||||
}
|
||||
|
||||
// No stomach so just popup a message that they can't eat.
|
||||
if (stomachToUse == null)
|
||||
{
|
||||
_solutionContainer.TryAddSolution(soln.Value, split);
|
||||
_popup.PopupClient(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other", ("target", args.Target.Value)) : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
_reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion);
|
||||
_stomach.TryTransferSolution(stomachToUse!.Value.Owner, split, stomachToUse);
|
||||
|
||||
var flavors = args.FlavorMessage;
|
||||
|
||||
if (forceFeed)
|
||||
{
|
||||
var targetName = Identity.Entity(args.Target.Value, EntityManager);
|
||||
var targetName = Identity.Entity(args.Target, EntityManager);
|
||||
var userName = Identity.Entity(args.User, EntityManager);
|
||||
_popup.PopupEntity(Loc.GetString("food-system-force-feed-success", ("user", userName), ("flavors", flavors)), entity.Owner, entity.Owner);
|
||||
_popup.PopupEntity(Loc.GetString("edible-force-feed-success", ("user", userName), ("verb", _ingestion.GetProtoVerb(IngestionSystem.Food)), ("flavors", flavors)), entity, entity);
|
||||
|
||||
_popup.PopupClient(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)), args.User, args.User);
|
||||
_popup.PopupClient(Loc.GetString("edible-force-feed-success-user", ("target", targetName), ("verb", _ingestion.GetProtoVerb(IngestionSystem.Food))), args.User, args.User);
|
||||
|
||||
// log successful force feed
|
||||
_adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity.Owner):user} forced {ToPrettyString(args.User):target} to eat {ToPrettyString(entity.Owner):food}");
|
||||
// log successful forced feeding
|
||||
_adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):user} forced {ToPrettyString(args.User):target} to eat {ToPrettyString(entity):food}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString(entity.Comp.EatMessage, ("food", entity.Owner), ("flavors", flavors)), args.User, args.User);
|
||||
|
||||
// log successful voluntary eating
|
||||
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity.Owner):food}");
|
||||
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity):food}");
|
||||
}
|
||||
|
||||
_audio.PlayPredicted(entity.Comp.UseSound, args.Target.Value, args.User, entity.Comp.UseSound.Params.WithVolume(-1f).WithVariation(0.20f));
|
||||
|
||||
// Try to break all used utensils
|
||||
foreach (var utensil in utensils)
|
||||
// BREAK OUR UTENSILS
|
||||
if (_ingestion.TryGetUtensils(args.User, entity, out var utensils))
|
||||
{
|
||||
_utensil.TryBreak(utensil, args.User);
|
||||
}
|
||||
|
||||
args.Repeat = !forceFeed;
|
||||
|
||||
if (TryComp<StackComponent>(entity, out var stack))
|
||||
{
|
||||
//Not deleting whole stack piece will make troubles with grinding object
|
||||
if (stack.Count > 1)
|
||||
foreach (var utensil in utensils)
|
||||
{
|
||||
_stack.SetCount(entity.Owner, stack.Count - 1);
|
||||
_solutionContainer.TryAddSolution(soln.Value, split);
|
||||
return;
|
||||
_ingestion.TryBreak(utensil, args.User);
|
||||
}
|
||||
}
|
||||
else if (GetUsesRemaining(entity.Owner, entity.Comp) > 0)
|
||||
|
||||
if (_ingestion.GetUsesRemaining(entity, entity.Comp.Solution, args.Split.Volume) > 0)
|
||||
{
|
||||
// Leave some of the consumer's DNA on the consumed item...
|
||||
var ev = new TransferDnaEvent
|
||||
{
|
||||
Donor = args.Target,
|
||||
Recipient = entity,
|
||||
CanDnaBeCleaned = false,
|
||||
};
|
||||
RaiseLocalEvent(args.Target, ref ev);
|
||||
|
||||
args.Repeat = !args.ForceFed;
|
||||
return;
|
||||
}
|
||||
|
||||
// don't try to repeat if its being deleted
|
||||
args.Repeat = false;
|
||||
DeleteAndSpawnTrash(entity.Comp, entity.Owner, args.User);
|
||||
// Food is always destroyed...
|
||||
args.Destroy = true;
|
||||
}
|
||||
|
||||
public void DeleteAndSpawnTrash(FoodComponent component, EntityUid food, EntityUid user)
|
||||
private void OnFoodFullyEaten(Entity<FoodComponent> food, ref FullyEatenEvent args)
|
||||
{
|
||||
var ev = new BeforeFullyEatenEvent
|
||||
{
|
||||
User = user
|
||||
};
|
||||
RaiseLocalEvent(food, ev);
|
||||
if (ev.Cancelled)
|
||||
if (food.Comp.Trash.Count == 0)
|
||||
return;
|
||||
|
||||
var attemptEv = new DestructionAttemptEvent();
|
||||
RaiseLocalEvent(food, attemptEv);
|
||||
if (attemptEv.Cancelled)
|
||||
return;
|
||||
|
||||
var afterEvent = new AfterFullyEatenEvent(user);
|
||||
RaiseLocalEvent(food, ref afterEvent);
|
||||
|
||||
var dev = new DestructionEventArgs();
|
||||
RaiseLocalEvent(food, dev);
|
||||
|
||||
if (component.Trash.Count == 0)
|
||||
{
|
||||
PredictedQueueDel(food);
|
||||
return;
|
||||
}
|
||||
|
||||
//We're empty. Become trash.
|
||||
//cache some data as we remove food, before spawning trash and passing it to the hand.
|
||||
|
||||
var position = _transform.GetMapCoordinates(food);
|
||||
var trashes = component.Trash;
|
||||
var tryPickup = _hands.IsHolding(user, food, out _);
|
||||
var trashes = food.Comp.Trash;
|
||||
var tryPickup = _hands.IsHolding(args.User, food, out _);
|
||||
|
||||
PredictedDel(food);
|
||||
foreach (var trash in trashes)
|
||||
{
|
||||
var spawnedTrash = EntityManager.PredictedSpawn(trash, position);
|
||||
@@ -362,192 +177,77 @@ public sealed class FoodSystem : EntitySystem
|
||||
if (tryPickup)
|
||||
{
|
||||
// Put the trash in the user's hand
|
||||
_hands.TryPickupAnyHand(user, spawnedTrash);
|
||||
_hands.TryPickupAnyHand(args.User, spawnedTrash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddEatVerb(Entity<FoodComponent> entity, ref GetVerbsEvent<AlternativeVerb> ev)
|
||||
{
|
||||
if (entity.Owner == ev.User ||
|
||||
!ev.CanInteract ||
|
||||
!ev.CanAccess ||
|
||||
!TryComp<BodyComponent>(ev.User, out var body) ||
|
||||
!_body.TryGetBodyOrganEntityComps<StomachComponent>((ev.User, body), out var stomachs))
|
||||
return;
|
||||
|
||||
// have to kill mouse before eating it
|
||||
if (_mobState.IsAlive(entity) && entity.Comp.RequireDead)
|
||||
return;
|
||||
|
||||
// only give moths eat verb for clothes since it would just fail otherwise
|
||||
if (!IsDigestibleBy(entity, entity.Comp, stomachs))
|
||||
return;
|
||||
|
||||
var user = ev.User;
|
||||
AlternativeVerb verb = new()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
TryFeed(user, user, entity, entity.Comp);
|
||||
},
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/cutlery.svg.192dpi.png")),
|
||||
Text = Loc.GetString("food-system-verb-eat"),
|
||||
Priority = -1
|
||||
};
|
||||
|
||||
ev.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the food item can be digested by the user.
|
||||
/// </summary>
|
||||
public bool IsDigestibleBy(EntityUid uid, EntityUid food, FoodComponent? foodComp = null)
|
||||
{
|
||||
if (!Resolve(food, ref foodComp, false))
|
||||
return false;
|
||||
|
||||
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(uid, out var stomachs))
|
||||
return false;
|
||||
|
||||
return IsDigestibleBy(food, foodComp, stomachs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if <paramref name="stomachs"/> has a <see cref="StomachComponent.SpecialDigestible"/> that whitelists
|
||||
/// this <paramref name="food"/> (or if they even have enough stomachs in the first place).
|
||||
/// </summary>
|
||||
private bool IsDigestibleBy(EntityUid food, FoodComponent component, List<Entity<StomachComponent, OrganComponent>> stomachs)
|
||||
{
|
||||
var digestible = true;
|
||||
|
||||
// Does the mob have enough stomachs?
|
||||
if (stomachs.Count < component.RequiredStomachs)
|
||||
return false;
|
||||
|
||||
// Run through the mobs' stomachs
|
||||
foreach (var ent in stomachs)
|
||||
{
|
||||
// Find a stomach with a SpecialDigestible
|
||||
if (ent.Comp1.SpecialDigestible == null)
|
||||
continue;
|
||||
// Check if the food is in the whitelist
|
||||
if (_whitelistSystem.IsWhitelistPass(ent.Comp1.SpecialDigestible, food))
|
||||
return true;
|
||||
|
||||
// If their diet is whitelist exclusive, then they cannot eat anything but what follows their whitelisted tags. Else, they can eat their tags AND human food.
|
||||
if (ent.Comp1.IsSpecialDigestibleExclusive)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (component.RequiresSpecialDigestion)
|
||||
return false;
|
||||
|
||||
return digestible;
|
||||
}
|
||||
|
||||
private bool TryGetRequiredUtensils(EntityUid user, FoodComponent component,
|
||||
out List<EntityUid> utensils, HandsComponent? hands = null)
|
||||
{
|
||||
utensils = new List<EntityUid>();
|
||||
|
||||
if (component.Utensil == UtensilType.None)
|
||||
return true;
|
||||
|
||||
if (!Resolve(user, ref hands, false))
|
||||
return true; //mice
|
||||
|
||||
var usedTypes = UtensilType.None;
|
||||
|
||||
foreach (var item in _hands.EnumerateHeld((user, hands)))
|
||||
{
|
||||
// Is utensil?
|
||||
if (!TryComp<UtensilComponent>(item, out var utensil))
|
||||
continue;
|
||||
|
||||
if ((utensil.Types & component.Utensil) != 0 && // Acceptable type?
|
||||
(usedTypes & utensil.Types) != utensil.Types) // Type is not used already? (removes usage of identical utensils)
|
||||
{
|
||||
// Add to used list
|
||||
usedTypes |= utensil.Types;
|
||||
utensils.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
// If "required" field is set, try to block eating without proper utensils used
|
||||
if (component.UtensilRequired && (usedTypes & component.Utensil) != component.Utensil)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("food-you-need-to-hold-utensil", ("utensil", component.Utensil ^ usedTypes)), user, user);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Block ingestion attempts based on the equipped mask or head-wear
|
||||
/// </summary>
|
||||
private void OnInventoryIngestAttempt(Entity<InventoryComponent> entity, ref IngestionAttemptEvent args)
|
||||
private void OnFood(Entity<FoodComponent> food, ref EdibleEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
IngestionBlockerComponent? blocker;
|
||||
if (args.Cancelled || args.Solution != null)
|
||||
return;
|
||||
|
||||
if (_inventory.TryGetSlotEntity(entity.Owner, "mask", out var maskUid) &&
|
||||
TryComp(maskUid, out blocker) &&
|
||||
blocker.Enabled)
|
||||
if (food.Comp.UtensilRequired && !_ingestion.HasRequiredUtensils(args.User, food.Comp.Utensil))
|
||||
{
|
||||
args.Blocker = maskUid;
|
||||
args.Cancel();
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_inventory.TryGetSlotEntity(entity.Owner, "head", out var headUid) &&
|
||||
TryComp(headUid, out blocker) &&
|
||||
blocker.Enabled)
|
||||
{
|
||||
args.Blocker = headUid;
|
||||
args.Cancel();
|
||||
}
|
||||
// Check this last
|
||||
_solutionContainer.TryGetSolution(food.Owner, food.Comp.Solution, out args.Solution);
|
||||
args.Time += TimeSpan.FromSeconds(food.Comp.Delay);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the target's mouth is blocked by equipment (masks or head-wear).
|
||||
/// </summary>
|
||||
/// <param name="uid">The target whose equipment is checked</param>
|
||||
/// <param name="popupUid">Optional entity that will receive an informative pop-up identifying the blocking
|
||||
/// piece of equipment.</param>
|
||||
/// <returns></returns>
|
||||
public bool IsMouthBlocked(EntityUid uid, EntityUid? popupUid = null)
|
||||
private void OnGetUtensils(Entity<FoodComponent> entity, ref GetUtensilsEvent args)
|
||||
{
|
||||
var attempt = new IngestionAttemptEvent();
|
||||
RaiseLocalEvent(uid, attempt, false);
|
||||
if (attempt.Cancelled && attempt.Blocker != null && popupUid != null)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("food-system-remove-mask", ("entity", attempt.Blocker.Value)),
|
||||
uid, popupUid.Value);
|
||||
}
|
||||
if (entity.Comp.Utensil == UtensilType.None)
|
||||
return;
|
||||
|
||||
return attempt.Cancelled;
|
||||
if (entity.Comp.UtensilRequired)
|
||||
args.AddRequiredTypes(entity.Comp.Utensil);
|
||||
else
|
||||
args.Types |= entity.Comp.Utensil;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the number of bites this food has left, based on how much food solution there is and how much of it to eat per bite.
|
||||
/// </summary>
|
||||
public int GetUsesRemaining(EntityUid uid, FoodComponent? comp = null)
|
||||
// TODO: When DrinkComponent and FoodComponent are properly obseleted, make the IsDigestionBools in IngestionSystem private again.
|
||||
private void OnIsFoodDigestible(Entity<FoodComponent> ent, ref IsDigestibleEvent args)
|
||||
{
|
||||
if (!Resolve(uid, ref comp))
|
||||
return 0;
|
||||
if (ent.Comp.RequireDead && _mobState.IsAlive(ent))
|
||||
return;
|
||||
|
||||
if (!_solutionContainer.TryGetSolution(uid, comp.Solution, out _, out var solution) || solution.Volume == 0)
|
||||
return 0;
|
||||
args.AddDigestible(ent.Comp.RequiresSpecialDigestion);
|
||||
}
|
||||
|
||||
// eat all in 1 go, so non empty is 1 bite
|
||||
if (comp.TransferAmount == null)
|
||||
return 1;
|
||||
private void OnGetEdibleType(Entity<FoodComponent> ent, ref GetEdibleTypeEvent args)
|
||||
{
|
||||
if (args.Type != null)
|
||||
return;
|
||||
|
||||
return Math.Max(1, (int) Math.Ceiling((solution.Volume / (FixedPoint2) comp.TransferAmount).Float()));
|
||||
args.SetPrototype(IngestionSystem.Food);
|
||||
}
|
||||
|
||||
private void OnBeforeFullySliced(Entity<FoodComponent> food, ref BeforeFullySlicedEvent args)
|
||||
{
|
||||
if (food.Comp.Trash.Count == 0)
|
||||
return;
|
||||
|
||||
var position = _transform.GetMapCoordinates(food);
|
||||
var trashes = food.Comp.Trash;
|
||||
var tryPickup = _hands.IsHolding(args.User, food, out _);
|
||||
|
||||
foreach (var trash in trashes)
|
||||
{
|
||||
var spawnedTrash = EntityManager.PredictedSpawn(trash, position);
|
||||
|
||||
// If the user is holding the item
|
||||
if (tryPickup)
|
||||
{
|
||||
// Put the trash in the user's hand
|
||||
_hands.TryPickupAnyHand(args.User, spawnedTrash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
public sealed class IngestionBlockerSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<IngestionBlockerComponent, ItemMaskToggledEvent>(OnBlockerMaskToggled);
|
||||
}
|
||||
|
||||
private void OnBlockerMaskToggled(Entity<IngestionBlockerComponent> ent, ref ItemMaskToggledEvent args)
|
||||
{
|
||||
ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
|
||||
}
|
||||
}
|
||||
430
Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs
Normal file
430
Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs
Normal file
@@ -0,0 +1,430 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.EntityEffects.Effects;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.Prototypes;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Public API for Ingestion System so you can build your own form of ingestion system.
|
||||
/// </summary>
|
||||
public sealed partial class IngestionSystem
|
||||
{
|
||||
// List of prototypes that other components or systems might want.
|
||||
public static readonly ProtoId<EdiblePrototype> Food = "Food";
|
||||
public static readonly ProtoId<EdiblePrototype> Drink = "Drink";
|
||||
|
||||
public const float MaxFeedDistance = 1.0f; // We should really have generic interaction ranges like short, medium, long and use those instead...
|
||||
// BodySystem has no way of telling us where the mouth is so we're making some assumptions.
|
||||
public const SlotFlags DefaultFlags = SlotFlags.HEAD | SlotFlags.MASK;
|
||||
|
||||
#region Ingestion
|
||||
|
||||
/// <summary>
|
||||
/// An entity is trying to ingest another entity in Space Station 14!!!
|
||||
/// </summary>
|
||||
/// <param name="user">The entity who is eating.</param>
|
||||
/// <param name="ingested">The entity that is trying to be ingested.</param>
|
||||
/// <returns>Returns true if we are now ingesting the item.</returns>
|
||||
public bool TryIngest(EntityUid user, EntityUid ingested)
|
||||
{
|
||||
return TryIngest(user, user, ingested);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TryIngest(EntityUid,EntityUid)"/>
|
||||
/// <summary>Overload of TryIngest for if an entity is trying to make another entity ingest an entity</summary>
|
||||
/// <param name="user">The entity who is trying to make this happen.</param>
|
||||
/// <param name="target">The entity who is being made to ingest something.</param>
|
||||
/// <param name="ingested">The entity that is trying to be ingested.</param>
|
||||
public bool TryIngest(EntityUid user, EntityUid target, EntityUid ingested)
|
||||
{
|
||||
return AttemptIngest(user, target, ingested, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if we can ingest a given entity without actually ingesting it.
|
||||
/// </summary>
|
||||
/// <param name="user">The entity doing the ingesting.</param>
|
||||
/// <param name="ingested">The ingested entity.</param>
|
||||
/// <returns>Returns true if it's possible for the entity to ingest this item.</returns>
|
||||
public bool CanIngest(EntityUid user, EntityUid ingested)
|
||||
{
|
||||
return AttemptIngest(user, user, ingested, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether we have an open pie-hole that's in range.
|
||||
/// </summary>
|
||||
/// <param name="user">The one performing the action</param>
|
||||
/// <param name="target">The target whose mouth is checked</param>
|
||||
/// <returns></returns>
|
||||
public bool HasMouthAvailable(EntityUid user, EntityUid target)
|
||||
{
|
||||
return HasMouthAvailable(user, target, DefaultFlags);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="HasMouthAvailable(EntityUid, EntityUid)"/>
|
||||
/// Overflow which takes custom flags for a mouth being blocked, in case the entity has a mouth not on the face.
|
||||
public bool HasMouthAvailable(EntityUid user, EntityUid target, SlotFlags flags)
|
||||
{
|
||||
if (!_transform.GetMapCoordinates(user).InRange(_transform.GetMapCoordinates(target), MaxFeedDistance))
|
||||
{
|
||||
var message = Loc.GetString("interaction-system-user-interaction-cannot-reach");
|
||||
_popup.PopupClient(message, user, user);
|
||||
return false;
|
||||
}
|
||||
|
||||
var attempt = new IngestionAttemptEvent(flags);
|
||||
RaiseLocalEvent(target, ref attempt);
|
||||
|
||||
if (!attempt.Cancelled)
|
||||
return true;
|
||||
|
||||
if (attempt.Blocker != null)
|
||||
_popup.PopupClient(Loc.GetString("ingestion-remove-mask", ("entity", attempt.Blocker.Value)), target, user);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="CanConsume(EntityUid,EntityUid)"/>
|
||||
/// <param name="user">The entity that is consuming</param>
|
||||
/// <param name="ingested">The entity that is being consumed</param>
|
||||
public bool CanConsume(EntityUid user, EntityUid ingested)
|
||||
{
|
||||
return CanConsume(user, user, ingested, out _, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if we can feed an edible solution from an entity to a target.
|
||||
/// </summary>
|
||||
/// <param name="user">The one doing the feeding</param>
|
||||
/// <param name="target">The one being fed.</param>
|
||||
/// <param name="ingested">The food item being eaten.</param>
|
||||
/// <returns>Returns true if the user can feed the target with the ingested entity</returns>
|
||||
public bool CanConsume(EntityUid user, EntityUid target, EntityUid ingested)
|
||||
{
|
||||
return CanConsume(user, target, ingested, out _, out _);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="CanConsume(EntityUid,EntityUid,EntityUid)"/>
|
||||
/// <param name="user">The one doing the feeding</param>
|
||||
/// <param name="target">The one being fed.</param>
|
||||
/// <param name="ingested">The food item being eaten.</param>
|
||||
/// <param name="solution">The solution we will be consuming from.</param>
|
||||
/// <param name="time">The time it takes us to eat this entity if any.</param>
|
||||
/// <returns>Returns true if the user can feed the target with the ingested entity and also returns a solution</returns>
|
||||
public bool CanConsume(EntityUid user,
|
||||
EntityUid target,
|
||||
EntityUid ingested,
|
||||
[NotNullWhen(true)] out Entity<SolutionComponent>? solution,
|
||||
out TimeSpan? time)
|
||||
{
|
||||
solution = null;
|
||||
time = null;
|
||||
|
||||
if (!HasMouthAvailable(user, target))
|
||||
return false;
|
||||
|
||||
// If we don't have the tools to eat we can't eat.
|
||||
return CanAccessSolution(ingested, user, out solution, out time);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EdibleComponent
|
||||
|
||||
public void SpawnTrash(Entity<EdibleComponent> entity, EntityUid user)
|
||||
{
|
||||
if (entity.Comp.Trash.Count == 0)
|
||||
return;
|
||||
|
||||
var position = _transform.GetMapCoordinates(entity);
|
||||
var trashes = entity.Comp.Trash;
|
||||
var tryPickup = _hands.IsHolding(user, entity, out _);
|
||||
|
||||
foreach (var trash in trashes)
|
||||
{
|
||||
var spawnedTrash = EntityManager.PredictedSpawn(trash, position);
|
||||
|
||||
// If the user is holding the item
|
||||
if (tryPickup)
|
||||
{
|
||||
// Put the trash in the user's hand
|
||||
_hands.TryPickupAnyHand(user, spawnedTrash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedPoint2 EdibleVolume(Entity<EdibleComponent> entity)
|
||||
{
|
||||
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution))
|
||||
return FixedPoint2.Zero;
|
||||
|
||||
return solution.Volume;
|
||||
}
|
||||
|
||||
public bool IsEmpty(Entity<EdibleComponent> entity)
|
||||
{
|
||||
return EdibleVolume(entity) == FixedPoint2.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total metabolizable nutrition from an entity, checks first if we can metabolize it.
|
||||
/// If we can't then it's not worth any nutrition.
|
||||
/// </summary>
|
||||
/// <param name="entity">The consumed entity</param>
|
||||
/// <param name="consumer">The entity doing the consuming</param>
|
||||
/// <returns>The amount of nutrition the consumable is worth</returns>
|
||||
public float TotalNutrition(Entity<EdibleComponent?> entity, EntityUid consumer)
|
||||
{
|
||||
if (!CanIngest(consumer, entity))
|
||||
return 0f;
|
||||
|
||||
return TotalNutrition(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total metabolizable nutrition from an entity, assumes we can eat and metabolize it.
|
||||
/// </summary>
|
||||
/// <param name="entity">The consumed entity</param>
|
||||
/// <returns>The amount of nutrition the consumable is worth</returns>
|
||||
public float TotalNutrition(Entity<EdibleComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return 0f;
|
||||
|
||||
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution))
|
||||
return 0f;
|
||||
|
||||
var total = 0f;
|
||||
foreach (var quantity in solution.Contents)
|
||||
{
|
||||
var reagent = _proto.Index<ReagentPrototype>(quantity.Reagent.Prototype);
|
||||
if (reagent.Metabolisms == null)
|
||||
continue;
|
||||
|
||||
foreach (var entry in reagent.Metabolisms.Values)
|
||||
{
|
||||
foreach (var effect in entry.Effects)
|
||||
{
|
||||
// ignores any effect conditions, just cares about how much it can hydrate
|
||||
if (effect is SatiateHunger hunger)
|
||||
{
|
||||
total += hunger.NutritionFactor * quantity.Quantity.Float();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total metabolizable hydration from an entity, checks first if we can metabolize it.
|
||||
/// If we can't then it's not worth any hydration.
|
||||
/// </summary>
|
||||
/// <param name="entity">The consumed entity</param>
|
||||
/// <param name="consumer">The entity doing the consuming</param>
|
||||
/// <returns>The amount of hydration the consumable is worth</returns>
|
||||
public float TotalHydration(Entity<EdibleComponent?> entity, EntityUid consumer)
|
||||
{
|
||||
if (!CanIngest(consumer, entity))
|
||||
return 0f;
|
||||
|
||||
return TotalNutrition(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total metabolizable hydration from an entity, assumes we can eat and metabolize it.
|
||||
/// </summary>
|
||||
/// <param name="entity">The consumed entity</param>
|
||||
/// <returns>The amount of hydration the consumable is worth</returns>
|
||||
public float TotalHydration(Entity<EdibleComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return 0f;
|
||||
|
||||
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution))
|
||||
return 0f;
|
||||
|
||||
var total = 0f;
|
||||
foreach (var quantity in solution.Contents)
|
||||
{
|
||||
var reagent = _proto.Index<ReagentPrototype>(quantity.Reagent.Prototype);
|
||||
if (reagent.Metabolisms == null)
|
||||
continue;
|
||||
|
||||
foreach (var entry in reagent.Metabolisms.Values)
|
||||
{
|
||||
foreach (var effect in entry.Effects)
|
||||
{
|
||||
// ignores any effect conditions, just cares about how much it can hydrate
|
||||
if (effect is SatiateThirst thirst)
|
||||
{
|
||||
total += thirst.HydrationFactor * quantity.Quantity.Float();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Solutions
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the item is currently edible.
|
||||
/// </summary>
|
||||
/// <param name="ingested">Entity being ingested</param>
|
||||
/// <param name="user">The entity trying to make the ingestion happening, not necessarily the one eating</param>
|
||||
/// <param name="solution">Solution we're returning</param>
|
||||
/// <param name="time">The time it takes us to eat this entity</param>
|
||||
public bool CanAccessSolution(Entity<SolutionContainerManagerComponent?> ingested,
|
||||
EntityUid user,
|
||||
[NotNullWhen(true)] out Entity<SolutionComponent>? solution,
|
||||
out TimeSpan? time)
|
||||
{
|
||||
solution = null;
|
||||
time = null;
|
||||
|
||||
if (!Resolve(ingested, ref ingested.Comp))
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("ingestion-try-use-is-empty", ("entity", ingested)), ingested, user);
|
||||
return false;
|
||||
}
|
||||
|
||||
var ev = new EdibleEvent(user);
|
||||
RaiseLocalEvent(ingested, ref ev);
|
||||
|
||||
solution = ev.Solution;
|
||||
time = ev.Time;
|
||||
|
||||
return !ev.Cancelled && solution != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimate the number of bites this food has left, based on how much food solution there is and how much of it to eat per bite.
|
||||
/// </summary>
|
||||
public int GetUsesRemaining(EntityUid uid, string solutionName, FixedPoint2 splitVol)
|
||||
{
|
||||
if (!_solutionContainer.TryGetSolution(uid, solutionName, out _, out var solution) || solution.Volume == 0)
|
||||
return 0;
|
||||
|
||||
return Math.Max(1, (int) Math.Ceiling((solution.Volume / splitVol).Float()));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edible Types
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the ingestion verbs for a given user entity and ingestible entity
|
||||
/// </summary>
|
||||
/// <param name="user">The one getting the verbs who would be doing the eating.</param>
|
||||
/// <param name="ingested">Entity being ingested.</param>
|
||||
/// <param name="type">Edible prototype.</param>
|
||||
/// <param name="verb">Verb we're returning.</param>
|
||||
/// <returns>Returns true if we generated a verb.</returns>
|
||||
public bool TryGetIngestionVerb(EntityUid user, EntityUid ingested, [ForbidLiteral] ProtoId<EdiblePrototype> type, [NotNullWhen(true)] out AlternativeVerb? verb)
|
||||
{
|
||||
verb = null;
|
||||
|
||||
// We want to see if we can ingest this item, but we don't actually want to ingest it.
|
||||
if (!CanIngest(user, ingested))
|
||||
return false;
|
||||
|
||||
var proto = _proto.Index(type);
|
||||
|
||||
verb = new()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
TryIngest(user, user, ingested);
|
||||
},
|
||||
Icon = proto.VerbIcon,
|
||||
Text = Loc.GetString(proto.VerbName),
|
||||
Priority = 2
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the most accurate edible prototype for an entity if one exists.
|
||||
/// </summary>
|
||||
/// <param name="entity">entity who's edible prototype we want</param>
|
||||
/// <returns>The best matching prototype if one exists.</returns>
|
||||
public ProtoId<EdiblePrototype>? GetEdibleType(Entity<EdibleComponent?> entity)
|
||||
{
|
||||
if (Resolve(entity, ref entity.Comp, false))
|
||||
return entity.Comp.Edible;
|
||||
|
||||
var ev = new GetEdibleTypeEvent();
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
|
||||
return ev.Type;
|
||||
}
|
||||
|
||||
public string GetEdibleNoun(Entity<EdibleComponent?> entity)
|
||||
{
|
||||
if (Resolve(entity, ref entity.Comp, false))
|
||||
return GetProtoVerb(entity.Comp.Edible);
|
||||
|
||||
var ev = new GetEdibleTypeEvent();
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
|
||||
if (ev.Type == null)
|
||||
return Loc.GetString("edible-noun-edible");
|
||||
|
||||
return GetProtoNoun(ev.Type.Value);
|
||||
}
|
||||
|
||||
public string GetProtoNoun([ForbidLiteral] ProtoId<EdiblePrototype> proto)
|
||||
{
|
||||
var prototype = _proto.Index(proto);
|
||||
|
||||
return GetProtoNoun(prototype);
|
||||
}
|
||||
|
||||
public string GetProtoNoun(EdiblePrototype proto)
|
||||
{
|
||||
return Loc.GetString(proto.Noun);
|
||||
}
|
||||
|
||||
public string GetEdibleVerb(Entity<EdibleComponent?> entity)
|
||||
{
|
||||
if (Resolve(entity, ref entity.Comp, false))
|
||||
return GetProtoVerb(entity.Comp.Edible);
|
||||
|
||||
var ev = new GetEdibleTypeEvent();
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
|
||||
if (ev.Type == null)
|
||||
return Loc.GetString("edible-verb-edible");
|
||||
|
||||
return GetProtoVerb(ev.Type.Value);
|
||||
}
|
||||
|
||||
public string GetProtoVerb([ForbidLiteral] ProtoId<EdiblePrototype> proto)
|
||||
{
|
||||
var prototype = _proto.Index(proto);
|
||||
|
||||
return GetProtoVerb(prototype);
|
||||
}
|
||||
|
||||
public string GetProtoVerb(EdiblePrototype proto)
|
||||
{
|
||||
return Loc.GetString(proto.Verb);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Fluids.Components;
|
||||
using Content.Shared.Interaction.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Storage;
|
||||
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
public sealed partial class IngestionSystem
|
||||
{
|
||||
[Dependency] private readonly OpenableSystem _openable = default!;
|
||||
|
||||
public void InitializeBlockers()
|
||||
{
|
||||
SubscribeLocalEvent<UnremoveableComponent, IngestibleEvent>(OnUnremovableIngestion);
|
||||
SubscribeLocalEvent<IngestionBlockerComponent, ItemMaskToggledEvent>(OnBlockerMaskToggled);
|
||||
SubscribeLocalEvent<IngestionBlockerComponent, IngestionAttemptEvent>(OnIngestionBlockerAttempt);
|
||||
SubscribeLocalEvent<IngestionBlockerComponent, InventoryRelayedEvent<IngestionAttemptEvent>>(OnIngestionBlockerAttempt);
|
||||
|
||||
// Edible Event
|
||||
SubscribeLocalEvent<EdibleComponent, EdibleEvent>(OnEdible);
|
||||
SubscribeLocalEvent<StorageComponent, EdibleEvent>(OnStorageEdible);
|
||||
SubscribeLocalEvent<ItemSlotsComponent, EdibleEvent>(OnItemSlotsEdible);
|
||||
SubscribeLocalEvent<OpenableComponent, EdibleEvent>(OnOpenableEdible);
|
||||
|
||||
// Digestion Events
|
||||
SubscribeLocalEvent<EdibleComponent, IsDigestibleEvent>(OnEdibleIsDigestible);
|
||||
SubscribeLocalEvent<DrainableSolutionComponent, IsDigestibleEvent>(OnDrainableIsDigestible);
|
||||
SubscribeLocalEvent<PuddleComponent, IsDigestibleEvent>(OnPuddleIsDigestible);
|
||||
|
||||
SubscribeLocalEvent<PillComponent, BeforeIngestedEvent>(OnPillBeforeEaten);
|
||||
}
|
||||
|
||||
private void OnUnremovableIngestion(Entity<UnremoveableComponent> entity, ref IngestibleEvent args)
|
||||
{
|
||||
// If we can't remove it we probably shouldn't be able to eat it.
|
||||
// TODO: Separate glue and Unremovable component.
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnBlockerMaskToggled(Entity<IngestionBlockerComponent> ent, ref ItemMaskToggledEvent args)
|
||||
{
|
||||
ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
|
||||
}
|
||||
|
||||
private void OnIngestionBlockerAttempt(Entity<IngestionBlockerComponent> entity, ref IngestionAttemptEvent args)
|
||||
{
|
||||
if (!args.Cancelled && entity.Comp.Enabled)
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Block ingestion attempts based on the equipped mask or head-wear
|
||||
/// </summary>
|
||||
private void OnIngestionBlockerAttempt(Entity<IngestionBlockerComponent> entity, ref InventoryRelayedEvent<IngestionAttemptEvent> args)
|
||||
{
|
||||
if (args.Args.Cancelled || !entity.Comp.Enabled)
|
||||
return;
|
||||
|
||||
args.Args.Cancelled = true;
|
||||
args.Args.Blocker = entity;
|
||||
}
|
||||
|
||||
private void OnEdible(Entity<EdibleComponent> entity, ref EdibleEvent args)
|
||||
{
|
||||
if (args.Cancelled || args.Solution != null)
|
||||
return;
|
||||
|
||||
if (entity.Comp.UtensilRequired && !HasRequiredUtensils(args.User, entity.Comp.Utensil))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check this last
|
||||
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out args.Solution) || IsEmpty(entity) && !entity.Comp.DestroyOnEmpty)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
|
||||
_popup.PopupClient(Loc.GetString("ingestion-try-use-is-empty", ("entity", entity)), entity, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
// Time is additive because I said so.
|
||||
args.Time += entity.Comp.Delay;
|
||||
}
|
||||
|
||||
private void OnStorageEdible(Entity<StorageComponent> ent, ref EdibleEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
if (!ent.Comp.Container.ContainedEntities.Any())
|
||||
return;
|
||||
|
||||
args.Cancelled = true;
|
||||
|
||||
_popup.PopupClient(Loc.GetString("edible-has-used-storage", ("food", ent), ("verb", GetEdibleVerb(ent.Owner))), args.User, args.User);
|
||||
}
|
||||
|
||||
private void OnItemSlotsEdible(Entity<ItemSlotsComponent> ent, ref EdibleEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
if (!ent.Comp.Slots.Any(slot => slot.Value.HasItem))
|
||||
return;
|
||||
|
||||
args.Cancelled = true;
|
||||
|
||||
_popup.PopupClient(Loc.GetString("edible-has-used-storage", ("food", ent), ("verb", GetEdibleVerb(ent.Owner))), args.User, args.User);
|
||||
}
|
||||
|
||||
private void OnOpenableEdible(Entity<OpenableComponent> ent, ref EdibleEvent args)
|
||||
{
|
||||
if (_openable.IsClosed(ent, args.User, ent.Comp))
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnEdibleIsDigestible(Entity<EdibleComponent> ent, ref IsDigestibleEvent args)
|
||||
{
|
||||
if (ent.Comp.RequireDead && _mobState.IsAlive(ent))
|
||||
return;
|
||||
|
||||
args.AddDigestible(ent.Comp.RequiresSpecialDigestion);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Both of these assume that having this component means there's nothing stopping you from slurping up
|
||||
/// pure reagent juice with absolutely nothing to stop you.
|
||||
/// </remarks>
|
||||
private void OnDrainableIsDigestible(Entity<DrainableSolutionComponent> ent, ref IsDigestibleEvent args)
|
||||
{
|
||||
args.UniversalDigestion();
|
||||
}
|
||||
|
||||
private void OnPuddleIsDigestible(Entity<PuddleComponent> ent, ref IsDigestibleEvent args)
|
||||
{
|
||||
args.UniversalDigestion();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// I mean you have to eat the *whole* pill no?
|
||||
/// </remarks>
|
||||
private void OnPillBeforeEaten(Entity<PillComponent> ent, ref BeforeIngestedEvent args)
|
||||
{
|
||||
if (args.Cancelled || args.Solution is not { } sol)
|
||||
return;
|
||||
|
||||
if (args.TryNewMinimum(sol.Volume))
|
||||
return;
|
||||
|
||||
args.Cancelled = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Tools.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
public sealed partial class IngestionSystem
|
||||
{
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private EntityQuery<UtensilComponent> _utensilsQuery;
|
||||
|
||||
public void InitializeUtensils()
|
||||
{
|
||||
SubscribeLocalEvent<UtensilComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(ToolOpenableSystem) });
|
||||
|
||||
SubscribeLocalEvent<EdibleComponent, GetUtensilsEvent>(OnGetEdibleUtensils);
|
||||
|
||||
_utensilsQuery = GetEntityQuery<UtensilComponent>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clicked with utensil
|
||||
/// </summary>
|
||||
private void OnAfterInteract(Entity<UtensilComponent> entity, ref AfterInteractEvent ev)
|
||||
{
|
||||
if (ev.Handled || ev.Target == null || !ev.CanReach)
|
||||
return;
|
||||
|
||||
ev.Handled = TryUseUtensil(ev.User, ev.Target.Value, entity);
|
||||
}
|
||||
|
||||
public bool TryUseUtensil(EntityUid user, EntityUid target, Entity<UtensilComponent> utensil)
|
||||
{
|
||||
var ev = new GetUtensilsEvent();
|
||||
RaiseLocalEvent(target, ref ev);
|
||||
|
||||
//Prevents food usage with a wrong utensil
|
||||
if ((ev.Types & utensil.Comp.Types) == 0)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("ingestion-try-use-wrong-utensil", ("verb", GetEdibleVerb(target)),("food", target), ("utensil", utensil.Owner)), user, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_interactionSystem.InRangeUnobstructed(user, target, popup: true))
|
||||
return true;
|
||||
|
||||
return TryIngest(user, user, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to break the utensil after interaction.
|
||||
/// </summary>
|
||||
/// <param name="entity">Utensil.</param>
|
||||
/// <param name="userUid">User of the utensil.</param>
|
||||
public void TryBreak(Entity<UtensilComponent?> entity, EntityUid userUid)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return;
|
||||
|
||||
// TODO: Once we have predicted randomness delete this for something sane...
|
||||
var seed = SharedRandomExtensions.HashCodeCombine(new() {(int)_timing.CurTick.Value, GetNetEntity(entity).Id, GetNetEntity(userUid).Id });
|
||||
var rand = new System.Random(seed);
|
||||
|
||||
if (!rand.Prob(entity.Comp.BreakChance))
|
||||
return;
|
||||
|
||||
_audio.PlayPredicted(entity.Comp.BreakSound, userUid, userUid, AudioParams.Default.WithVolume(-2f));
|
||||
// Not prediced because no random predicted
|
||||
PredictedDel(entity.Owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if we have the utensils required to eat a certain food item.
|
||||
/// </summary>
|
||||
/// <param name="entity">Entity that is trying to eat.</param>
|
||||
/// <param name="food">The types of utensils we need.</param>
|
||||
/// <param name="utensils">The utensils needed to eat the food item.</param>
|
||||
/// <returns>True if we are able to eat the item.</returns>
|
||||
public bool TryGetUtensils(Entity<HandsComponent?> entity, EntityUid food, out List<EntityUid> utensils)
|
||||
{
|
||||
var ev = new GetUtensilsEvent();
|
||||
RaiseLocalEvent(food, ref ev);
|
||||
|
||||
return TryGetUtensils(entity, ev.Types, ev.RequiredTypes, out utensils);
|
||||
}
|
||||
|
||||
public bool TryGetUtensils(Entity<HandsComponent?> entity, UtensilType types, UtensilType requiredTypes, out List<EntityUid> utensils)
|
||||
{
|
||||
utensils = new List<EntityUid>();
|
||||
|
||||
var required = requiredTypes != UtensilType.None;
|
||||
|
||||
// Why are we even here? Just to suffer?
|
||||
if (types == UtensilType.None)
|
||||
return true;
|
||||
|
||||
// If you don't have hands you can eat anything I guess.
|
||||
if (!Resolve(entity, ref entity.Comp, false)) // You aren't allowed to eat with your hands in this hellish dystopia.
|
||||
return true;
|
||||
|
||||
var usedTypes = UtensilType.None;
|
||||
|
||||
foreach (var item in _hands.EnumerateHeld(entity))
|
||||
{
|
||||
// Is utensil?
|
||||
if (!_utensilsQuery.TryComp(item, out var utensil))
|
||||
continue;
|
||||
|
||||
// Do we have a new and unused utensil type?
|
||||
if ((utensil.Types & types) == 0 || (usedTypes & utensil.Types) == utensil.Types)
|
||||
continue;
|
||||
|
||||
// Add to used list
|
||||
usedTypes |= utensil.Types;
|
||||
utensils.Add(item);
|
||||
}
|
||||
|
||||
// If "required" field is set, try to block eating without proper utensils used
|
||||
if (!required || (usedTypes & requiredTypes) == requiredTypes)
|
||||
return true;
|
||||
|
||||
_popup.PopupClient(Loc.GetString("ingestion-you-need-to-hold-utensil", ("utensil", requiredTypes ^ usedTypes)), entity, entity);
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if you have the required utensils based on a list of types.
|
||||
/// Note it is assumed if you're calling this method that you need utensils.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity doing the action who has the utensils.</param>
|
||||
/// <param name="types">The types of utensils we need.</param>
|
||||
/// <returns>Returns true if we have the utensils we need.</returns>
|
||||
public bool HasRequiredUtensils(EntityUid entity, UtensilType types)
|
||||
{
|
||||
return TryGetUtensils(entity, types, types, out _);
|
||||
}
|
||||
|
||||
private void OnGetEdibleUtensils(Entity<EdibleComponent> entity, ref GetUtensilsEvent args)
|
||||
{
|
||||
if (entity.Comp.Utensil == UtensilType.None)
|
||||
return;
|
||||
|
||||
if (entity.Comp.UtensilRequired)
|
||||
args.AddRequiredTypes(entity.Comp.Utensil);
|
||||
else
|
||||
args.Types |= entity.Comp.Utensil;
|
||||
}
|
||||
}
|
||||
531
Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs
Normal file
531
Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs
Normal file
@@ -0,0 +1,531 @@
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Organ;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Forensics;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Tools.EntitySystems;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
/// <remarks>
|
||||
/// I was warned about puddle system, I knew the risks with body system, but food and drink system?
|
||||
/// Food and Drink system was a sleeping titan, and I walked directly into it's gaping maw.
|
||||
/// Between copy-pasted code, strange reliance on systems, being a pillar of chemistry for some reason,
|
||||
/// nothing could've prepared me for the horror that I had to endure. I saw the signs, comments of those who
|
||||
/// turned back, code that was made to be "just good enough" the fact that I got soaped by soap.yml, but I
|
||||
/// ignored them and pressed on.
|
||||
/// Let this remark be a reminder to those who come after, that I was here, and that I vanquished a great beast.
|
||||
/// Let young little contributors rest easy at night not knowing the horrible system that once lived beneath the
|
||||
/// bedrock of the codebase they now commit to.
|
||||
/// </remarks>
|
||||
/// <summary>
|
||||
/// This handles the ingestion of solutions and entities.
|
||||
/// </summary>
|
||||
public sealed partial class IngestionSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
[Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
// Body Component Dependencies
|
||||
[Dependency] private readonly SharedBodySystem _body = default!;
|
||||
[Dependency] private readonly ReactiveSystem _reaction = default!;
|
||||
[Dependency] private readonly StomachSystem _stomach = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<EdibleComponent, ComponentInit>(OnEdibleInit);
|
||||
|
||||
// Interactions
|
||||
SubscribeLocalEvent<EdibleComponent, UseInHandEvent>(OnUseEdibleInHand, after: new[] { typeof(OpenableSystem), typeof(InventorySystem) });
|
||||
SubscribeLocalEvent<EdibleComponent, AfterInteractEvent>(OnEdibleInteract, after: new[] { typeof(ToolOpenableSystem) });
|
||||
|
||||
// Generic Eating Handlers
|
||||
SubscribeLocalEvent<EdibleComponent, BeforeIngestedEvent>(OnBeforeIngested);
|
||||
SubscribeLocalEvent<EdibleComponent, IngestedEvent>(OnEdibleIngested);
|
||||
SubscribeLocalEvent<EdibleComponent, FullyEatenEvent>(OnFullyEaten);
|
||||
|
||||
// Body Component eating handler
|
||||
SubscribeLocalEvent<BodyComponent, AttemptIngestEvent>(OnTryIngest);
|
||||
SubscribeLocalEvent<BodyComponent, EatingDoAfterEvent>(OnEatingDoAfter);
|
||||
|
||||
// Verbs
|
||||
SubscribeLocalEvent<EdibleComponent, GetVerbsEvent<AlternativeVerb>>(AddEdibleVerbs);
|
||||
SubscribeLocalEvent<EdibleComponent, SolutionContainerChangedEvent>(OnSolutionContainerChanged);
|
||||
|
||||
// Misc
|
||||
SubscribeLocalEvent<EdibleComponent, AttemptShakeEvent>(OnAttemptShake);
|
||||
SubscribeLocalEvent<EdibleComponent, BeforeFullySlicedEvent>(OnBeforeFullySliced);
|
||||
|
||||
InitializeBlockers();
|
||||
InitializeUtensils();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eat or drink an item
|
||||
/// </summary>
|
||||
private void OnUseEdibleInHand(Entity<EdibleComponent> entity, ref UseInHandEvent ev)
|
||||
{
|
||||
if (ev.Handled)
|
||||
return;
|
||||
|
||||
ev.Handled = TryIngest(ev.User, entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Feed someone else
|
||||
/// </summary>
|
||||
private void OnEdibleInteract(Entity<EdibleComponent> entity, ref AfterInteractEvent args)
|
||||
{
|
||||
if (args.Handled || args.Target == null || !args.CanReach)
|
||||
return;
|
||||
|
||||
args.Handled = TryIngest(args.User, args.Target.Value, entity);
|
||||
}
|
||||
|
||||
/// <summary>Raises events to see if it's possible to ingest </summary>
|
||||
/// <param name="user">The entity who is trying to make this happen.</param>
|
||||
/// <param name="target">The entity who is being made to ingest something.</param>
|
||||
/// <param name="ingested">The entity that is trying to be ingested.</param>
|
||||
/// <param name="ingest">Bool that determines whethere this is a Try or a Can effectively.
|
||||
/// When set to true, it tries to ingest, when false it checks if we can.</param>
|
||||
/// <returns>Returns true if we can ingest the item.</returns>
|
||||
private bool AttemptIngest(EntityUid user, EntityUid target, EntityUid ingested, bool ingest)
|
||||
{
|
||||
var eatEv = new IngestibleEvent();
|
||||
RaiseLocalEvent(ingested, ref eatEv);
|
||||
|
||||
if (eatEv.Cancelled)
|
||||
return false;
|
||||
|
||||
var ingestionEv = new AttemptIngestEvent(user, ingested, ingest);
|
||||
RaiseLocalEvent(target, ref ingestionEv);
|
||||
|
||||
return ingestionEv.Handled;
|
||||
}
|
||||
|
||||
private void OnEdibleInit(Entity<EdibleComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
// TODO: When Food and Drink component are kill make sure to nuke both TryComps and just have it update appearance...
|
||||
// Beakers, Soap and other items have drainable, and we should be able to eat that solution...
|
||||
// If I could make drainable properly support sound effects and such I'd just have it use TryIngest itself
|
||||
// Does this exist just to make tests fail? That way you have the proper yaml???
|
||||
if (TryComp<DrainableSolutionComponent>(entity, out var existingDrainable))
|
||||
entity.Comp.Solution = existingDrainable.Solution;
|
||||
|
||||
UpdateAppearance(entity);
|
||||
|
||||
if (TryComp(entity, out RefillableSolutionComponent? refillComp))
|
||||
refillComp.Solution = entity.Comp.Solution;
|
||||
}
|
||||
|
||||
#region Appearance System
|
||||
|
||||
public void UpdateAppearance(Entity<EdibleComponent, AppearanceComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp2, false))
|
||||
return;
|
||||
|
||||
var drainAvailable = EdibleVolume(entity);
|
||||
_appearance.SetData(entity, FoodVisuals.Visual, drainAvailable.Float(), entity.Comp2);
|
||||
}
|
||||
|
||||
private void OnSolutionContainerChanged(Entity<EdibleComponent> entity, ref SolutionContainerChangedEvent args)
|
||||
{
|
||||
UpdateAppearance(entity);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region BodySystem
|
||||
|
||||
// TODO: The IsDigestibleBy bools should be API but they're too specific to the BodySystem to be API. Requires BodySystem rework.
|
||||
/// <summary>
|
||||
/// Generic method which takes a list of stomachs, and checks if a given food item passes any stomach's whitelist
|
||||
/// in a given list of stomachs.
|
||||
/// </summary>
|
||||
/// <param name="food">Entity being eaten</param>
|
||||
/// <param name="stomachs">Stomachs available to digest</param>
|
||||
public bool IsDigestibleBy(EntityUid food, List<Entity<StomachComponent, OrganComponent>> stomachs)
|
||||
{
|
||||
var ev = new IsDigestibleEvent();
|
||||
RaiseLocalEvent(food, ref ev);
|
||||
|
||||
if (!ev.Digestible)
|
||||
return false;
|
||||
|
||||
if (ev.Universal)
|
||||
return true;
|
||||
|
||||
if (ev.SpecialDigestion)
|
||||
{
|
||||
foreach (var ent in stomachs)
|
||||
{
|
||||
// We need one stomach that can digest our special food.
|
||||
if (_whitelistSystem.IsWhitelistPass(ent.Comp1.SpecialDigestible, food))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var ent in stomachs)
|
||||
{
|
||||
// We need one stomach that can digest normal food.
|
||||
if (ent.Comp1.SpecialDigestible == null
|
||||
|| !ent.Comp1.IsSpecialDigestibleExclusive
|
||||
|| _whitelistSystem.IsWhitelistPass(ent.Comp1.SpecialDigestible, food))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find a stomach that can digest our food then it doesn't exist.
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic method which takes a single stomach into account, and checks if a given food item passes a stomach whitelist.
|
||||
/// </summary>
|
||||
/// <param name="food">Entity being eaten</param>
|
||||
/// <param name="stomach">Stomachs that is attempting to digest.</param>
|
||||
public bool IsDigestibleBy(EntityUid food, Entity<StomachComponent, OrganComponent> stomach)
|
||||
{
|
||||
var ev = new IsDigestibleEvent();
|
||||
RaiseLocalEvent(food, ref ev);
|
||||
|
||||
if (!ev.Digestible)
|
||||
return false;
|
||||
|
||||
if (ev.Universal)
|
||||
return true;
|
||||
|
||||
if (ev.SpecialDigestion)
|
||||
return _whitelistSystem.IsWhitelistPass(stomach.Comp1.SpecialDigestible, food);
|
||||
|
||||
if (stomach.Comp1.SpecialDigestible == null || !stomach.Comp1.IsSpecialDigestibleExclusive || _whitelistSystem.IsWhitelistPass(stomach.Comp1.SpecialDigestible, food))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnTryIngest(Entity<BodyComponent> entity, ref AttemptIngestEvent args)
|
||||
{
|
||||
var food = args.Ingested;
|
||||
var forceFed = args.User != entity.Owner;
|
||||
|
||||
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(entity!, out var stomachs))
|
||||
return;
|
||||
|
||||
// Can we digest the specific item we're trying to eat?
|
||||
if (!IsDigestibleBy(args.Ingested, stomachs))
|
||||
{
|
||||
if (forceFed)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("ingestion-cant-digest-other", ("target", entity), ("entity", food)), entity, args.User);
|
||||
}
|
||||
else
|
||||
_popup.PopupClient(Loc.GetString("ingestion-cant-digest", ("entity", food)), entity, entity);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Exit early if we're just trying to get verbs
|
||||
if (!args.Ingest)
|
||||
{
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if despite being able to digest the item something is blocking us from eating.
|
||||
if (!CanConsume(args.User, entity, args.Ingested, out var solution, out var time))
|
||||
return;
|
||||
|
||||
if (!_doAfter.TryStartDoAfter(GetEdibleDoAfterArgs(args.User, entity, food, time ?? TimeSpan.Zero)))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
var foodSolution = solution.Value.Comp.Solution;
|
||||
|
||||
if (forceFed)
|
||||
{
|
||||
var userName = Identity.Entity(args.User, EntityManager);
|
||||
_popup.PopupEntity(Loc.GetString("edible-force-feed", ("user", userName), ("verb", GetEdibleVerb(food))), args.User, entity);
|
||||
|
||||
// logging
|
||||
_adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(args.User):user} is forcing {ToPrettyString(entity):target} to eat {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// log voluntary eating
|
||||
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(entity):target} is eating {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEatingDoAfter(Entity<BodyComponent> entity, ref EatingDoAfterEvent args)
|
||||
{
|
||||
if (args.Cancelled || args.Handled || entity.Comp.Deleted || args.Target == null)
|
||||
return;
|
||||
|
||||
var food = args.Target.Value;
|
||||
|
||||
var blockerEv = new IngestibleEvent();
|
||||
RaiseLocalEvent(food, ref blockerEv);
|
||||
|
||||
if (blockerEv.Cancelled)
|
||||
return;
|
||||
|
||||
if (!CanConsume(args.User, entity, food, out var solution, out _))
|
||||
return;
|
||||
|
||||
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(entity!, out var stomachs))
|
||||
return;
|
||||
|
||||
var forceFed = args.User != entity.Owner;
|
||||
|
||||
var highestAvailable = FixedPoint2.Zero;
|
||||
Entity<StomachComponent>? stomachToUse = null;
|
||||
foreach (var ent in stomachs)
|
||||
{
|
||||
var owner = ent.Owner;
|
||||
if (!_solutionContainer.ResolveSolution(owner, StomachSystem.DefaultSolutionName, ref ent.Comp1.Solution, out var stomachSol))
|
||||
continue;
|
||||
|
||||
if (stomachSol.AvailableVolume <= highestAvailable)
|
||||
continue;
|
||||
|
||||
if (!IsDigestibleBy(food, ent))
|
||||
continue;
|
||||
|
||||
stomachToUse = ent;
|
||||
highestAvailable = stomachSol.AvailableVolume;
|
||||
}
|
||||
|
||||
// All stomachs are full or we have no stomachs
|
||||
if (stomachToUse == null)
|
||||
{
|
||||
// Very long
|
||||
_popup.PopupClient(Loc.GetString("ingestion-you-cannot-ingest-any-more", ("verb", GetEdibleVerb(food))), entity, entity);
|
||||
if (!forceFed)
|
||||
return;
|
||||
|
||||
_popup.PopupClient(Loc.GetString("ingestion-other-cannot-ingest-any-more", ("target", entity), ("verb", GetEdibleVerb(food))), args.Target.Value, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
var beforeEv = new BeforeIngestedEvent(FixedPoint2.Zero, highestAvailable, solution.Value.Comp.Solution);
|
||||
RaiseLocalEvent(food, ref beforeEv);
|
||||
RaiseLocalEvent(entity, ref beforeEv);
|
||||
|
||||
if (beforeEv.Cancelled || beforeEv.Min > beforeEv.Max)
|
||||
{
|
||||
// Very long x2
|
||||
_popup.PopupClient(Loc.GetString("ingestion-you-cannot-ingest-any-more", ("verb", GetEdibleVerb(food))), entity, entity);
|
||||
if (!forceFed)
|
||||
return;
|
||||
|
||||
_popup.PopupClient(Loc.GetString("ingestion-other-cannot-ingest-any-more", ("target", entity), ("verb", GetEdibleVerb(food))), args.Target.Value, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
var transfer = FixedPoint2.Clamp(beforeEv.Transfer, beforeEv.Min, beforeEv.Max);
|
||||
|
||||
var split = _solutionContainer.SplitSolution(solution.Value, transfer);
|
||||
|
||||
var ingestEv = new IngestingEvent(food, split, forceFed);
|
||||
RaiseLocalEvent(entity, ref ingestEv);
|
||||
|
||||
_reaction.DoEntityReaction(entity, split, ReactionMethod.Ingestion);
|
||||
|
||||
// Everything is good to go item has been successfuly eaten
|
||||
var afterEv = new IngestedEvent(args.User, entity, split, forceFed);
|
||||
RaiseLocalEvent(food, ref afterEv);
|
||||
|
||||
if (afterEv.Refresh)
|
||||
_solutionContainer.TryAddSolution(solution.Value, split);
|
||||
|
||||
_stomach.TryTransferSolution(stomachToUse.Value.Owner, split, stomachToUse);
|
||||
|
||||
if (!afterEv.Destroy)
|
||||
{
|
||||
args.Repeat = afterEv.Repeat;
|
||||
return;
|
||||
}
|
||||
|
||||
var ev = new DestructionAttemptEvent();
|
||||
RaiseLocalEvent(food, ev);
|
||||
if (ev.Cancelled)
|
||||
return;
|
||||
|
||||
// Tell the food that it's time to die.
|
||||
var finishedEv = new FullyEatenEvent(args.User);
|
||||
RaiseLocalEvent(food, ref finishedEv);
|
||||
|
||||
var eventArgs = new DestructionEventArgs();
|
||||
RaiseLocalEvent(food, eventArgs);
|
||||
|
||||
PredictedDel(food);
|
||||
|
||||
// Don't try to repeat if its being deleted
|
||||
args.Repeat = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DoAfterArgs for the specific event
|
||||
/// </summary>
|
||||
/// <param name="user">Entity that is doing the action.</param>
|
||||
/// <param name="target">Entity that is eating.</param>
|
||||
/// <param name="food">Food entity we're trying to eat.</param>
|
||||
/// <param name="delay">The time delay for our DoAfter</param>
|
||||
/// <returns>Returns true if it was able to successfully start the DoAfter</returns>
|
||||
private DoAfterArgs GetEdibleDoAfterArgs(EntityUid user, EntityUid target, EntityUid food, TimeSpan delay = default)
|
||||
{
|
||||
var forceFeed = user != target;
|
||||
|
||||
var doAfterArgs = new DoAfterArgs(EntityManager, user, delay, new EatingDoAfterEvent(), target, food)
|
||||
{
|
||||
BreakOnHandChange = false,
|
||||
BreakOnMove = forceFeed,
|
||||
BreakOnDamage = true,
|
||||
MovementThreshold = 0.01f,
|
||||
DistanceThreshold = MaxFeedDistance,
|
||||
// do-after will stop if item is dropped when trying to feed someone else
|
||||
// or if the item started out in the user's own hands
|
||||
NeedHand = forceFeed || _hands.IsHolding(user, food),
|
||||
};
|
||||
|
||||
return doAfterArgs;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void OnBeforeIngested(Entity<EdibleComponent> food, ref BeforeIngestedEvent args)
|
||||
{
|
||||
if (args.Cancelled || args.Solution is not { } solution)
|
||||
return;
|
||||
|
||||
// Set it to transfer amount if it exists, otherwise eat the whole volume if possible.
|
||||
args.Transfer = food.Comp.TransferAmount ?? solution.Volume;
|
||||
}
|
||||
|
||||
private void OnEdibleIngested(Entity<EdibleComponent> entity, ref IngestedEvent args)
|
||||
{
|
||||
// This is a lot but there wasn't really a way to separate this from the EdibleComponent otherwise I would've moved it.
|
||||
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
var edible = _proto.Index(entity.Comp.Edible);
|
||||
|
||||
_audio.PlayPredicted(edible.UseSound, args.Target, args.User);
|
||||
|
||||
var flavors = _flavorProfile.GetLocalizedFlavorsMessage(entity.Owner, args.Target, args.Split);
|
||||
|
||||
if (args.ForceFed)
|
||||
{
|
||||
var targetName = Identity.Entity(args.Target, EntityManager);
|
||||
var userName = Identity.Entity(args.User, EntityManager);
|
||||
_popup.PopupEntity(Loc.GetString("edible-force-feed-success", ("user", userName), ("verb", edible.Verb), ("flavors", flavors)), entity, entity);
|
||||
|
||||
_popup.PopupClient(Loc.GetString("edible-force-feed-success-user", ("target", targetName), ("verb", edible.Verb)), args.User, args.User);
|
||||
|
||||
// log successful forced feeding
|
||||
_adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):user} forced {ToPrettyString(args.User):target} to eat {ToPrettyString(entity):food}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString(edible.Message, ("food", entity.Owner), ("flavors", flavors)), args.User, args.User);
|
||||
|
||||
// log successful voluntary eating
|
||||
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity):food}");
|
||||
}
|
||||
|
||||
// BREAK OUR UTENSILS
|
||||
if (TryGetUtensils(args.User, entity, out var utensils))
|
||||
{
|
||||
foreach (var utensil in utensils)
|
||||
{
|
||||
TryBreak(utensil, args.User);
|
||||
}
|
||||
}
|
||||
|
||||
// This also prevents us from repeating if it's empty
|
||||
if (!IsEmpty(entity))
|
||||
{
|
||||
// Leave some of the consumer's DNA on the consumed item...
|
||||
var ev = new TransferDnaEvent
|
||||
{
|
||||
Donor = args.Target,
|
||||
Recipient = entity,
|
||||
CanDnaBeCleaned = false,
|
||||
};
|
||||
RaiseLocalEvent(args.Target, ref ev);
|
||||
|
||||
args.Repeat = !args.ForceFed;
|
||||
return;
|
||||
}
|
||||
|
||||
args.Destroy = entity.Comp.DestroyOnEmpty;
|
||||
}
|
||||
|
||||
private void OnFullyEaten(Entity<EdibleComponent> entity, ref FullyEatenEvent args)
|
||||
{
|
||||
SpawnTrash(entity, args.User);
|
||||
}
|
||||
|
||||
private void OnBeforeFullySliced(Entity<EdibleComponent> entity, ref BeforeFullySlicedEvent args)
|
||||
{
|
||||
SpawnTrash(entity, args.User);
|
||||
}
|
||||
|
||||
private void AddEdibleVerbs(Entity<EdibleComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
var user = args.User;
|
||||
|
||||
if (entity.Owner == user || !args.CanInteract || !args.CanAccess)
|
||||
return;
|
||||
|
||||
if (!TryGetIngestionVerb(user, entity, entity.Comp.Edible, out var verb))
|
||||
return;
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
private void OnAttemptShake(Entity<EdibleComponent> entity, ref AttemptShakeEvent args)
|
||||
{
|
||||
if (IsEmpty(entity))
|
||||
args.Cancelled = true;
|
||||
}
|
||||
}
|
||||
@@ -121,11 +121,8 @@ public sealed partial class OpenableSystem : EntitySystem
|
||||
|
||||
private void OnTransferAttempt(Entity<OpenableComponent> ent, ref SolutionTransferAttemptEvent args)
|
||||
{
|
||||
if (!ent.Comp.Opened)
|
||||
{
|
||||
// message says its just for drinks, shouldn't matter since you typically dont have a food that is openable and can be poured out
|
||||
args.Cancel(Loc.GetString("drink-component-try-use-drink-not-open", ("owner", ent.Owner)));
|
||||
}
|
||||
if (ent.Comp.Opened)
|
||||
args.Cancel(Loc.GetString(ent.Comp.ClosedPopup, ("owner", ent.Owner)));
|
||||
}
|
||||
|
||||
private void OnAttemptShake(Entity<OpenableComponent> entity, ref AttemptShakeEvent args)
|
||||
|
||||
@@ -1,34 +1,28 @@
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Forensics;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
[Obsolete("Migration to Content.Shared.Nutrition.EntitySystems.IngestionSystem is required")]
|
||||
public abstract partial class SharedDrinkSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedBodySystem _body = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
|
||||
[Dependency] private readonly FoodSystem _food = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly OpenableSystem _openable = default!;
|
||||
[Dependency] private readonly IngestionSystem _ingestion = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
|
||||
@@ -36,8 +30,21 @@ public abstract partial class SharedDrinkSystem : EntitySystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DrinkComponent, UseInHandEvent>(OnUseDrinkInHand, after: new[] { typeof(OpenableSystem), typeof(InventorySystem) });
|
||||
SubscribeLocalEvent<DrinkComponent, AfterInteractEvent>(OnUseDrink);
|
||||
|
||||
SubscribeLocalEvent<DrinkComponent, AttemptShakeEvent>(OnAttemptShake);
|
||||
|
||||
SubscribeLocalEvent<DrinkComponent, GetVerbsEvent<AlternativeVerb>>(AddDrinkVerb);
|
||||
|
||||
SubscribeLocalEvent<DrinkComponent, BeforeIngestedEvent>(OnBeforeDrinkEaten);
|
||||
SubscribeLocalEvent<DrinkComponent, IngestedEvent>(OnDrinkEaten);
|
||||
|
||||
SubscribeLocalEvent<DrinkComponent, EdibleEvent>(OnDrink);
|
||||
|
||||
SubscribeLocalEvent<DrinkComponent, IsDigestibleEvent>(OnIsDigestible);
|
||||
|
||||
SubscribeLocalEvent<DrinkComponent, GetEdibleTypeEvent>(OnGetEdibleType);
|
||||
}
|
||||
|
||||
protected void OnAttemptShake(Entity<DrinkComponent> entity, ref AttemptShakeEvent args)
|
||||
@@ -46,38 +53,6 @@ public abstract partial class SharedDrinkSystem : EntitySystem
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void AddDrinkVerb(Entity<DrinkComponent> entity, ref GetVerbsEvent<AlternativeVerb> ev)
|
||||
{
|
||||
if (entity.Owner == ev.User ||
|
||||
!ev.CanInteract ||
|
||||
!ev.CanAccess ||
|
||||
!TryComp<BodyComponent>(ev.User, out var body) ||
|
||||
!_body.TryGetBodyOrganEntityComps<StomachComponent>((ev.User, body), out var stomachs))
|
||||
return;
|
||||
|
||||
// Make sure the solution exists
|
||||
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var solution))
|
||||
return;
|
||||
|
||||
// no drinking from living drinks, have to kill them first.
|
||||
if (_mobState.IsAlive(entity))
|
||||
return;
|
||||
|
||||
var user = ev.User;
|
||||
AlternativeVerb verb = new()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
TryDrink(user, user, entity.Comp, entity);
|
||||
},
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/drink.svg.192dpi.png")),
|
||||
Text = Loc.GetString("drink-system-verb-drink"),
|
||||
Priority = 2
|
||||
};
|
||||
|
||||
ev.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
protected FixedPoint2 DrinkVolume(EntityUid uid, DrinkComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
@@ -98,72 +73,123 @@ public abstract partial class SharedDrinkSystem : EntitySystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to feed the drink item to the target entity
|
||||
/// Eat or drink an item
|
||||
/// </summary>
|
||||
protected bool TryDrink(EntityUid user, EntityUid target, DrinkComponent drink, EntityUid item)
|
||||
private void OnUseDrinkInHand(Entity<DrinkComponent> entity, ref UseInHandEvent ev)
|
||||
{
|
||||
if (!HasComp<BodyComponent>(target))
|
||||
return false;
|
||||
if (ev.Handled)
|
||||
return;
|
||||
|
||||
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(target, out var stomachs))
|
||||
return false;
|
||||
ev.Handled = _ingestion.TryIngest(ev.User, ev.User, entity);
|
||||
}
|
||||
|
||||
if (_openable.IsClosed(item, user, predicted: true))
|
||||
return true;
|
||||
/// <summary>
|
||||
/// Feed someone else
|
||||
/// </summary>
|
||||
private void OnUseDrink(Entity<DrinkComponent> entity, ref AfterInteractEvent args)
|
||||
{
|
||||
if (args.Handled || args.Target == null || !args.CanReach)
|
||||
return;
|
||||
|
||||
if (!_solutionContainer.TryGetSolution(item, drink.Solution, out _, out var drinkSolution) || drinkSolution.Volume <= 0)
|
||||
args.Handled = _ingestion.TryIngest(args.User, args.Target.Value, entity);
|
||||
}
|
||||
|
||||
private void AddDrinkVerb(Entity<DrinkComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
var user = args.User;
|
||||
|
||||
if (entity.Owner == user || !args.CanInteract || !args.CanAccess)
|
||||
return;
|
||||
|
||||
if (!_ingestion.TryGetIngestionVerb(user, entity, IngestionSystem.Drink, out var verb))
|
||||
return;
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
private void OnBeforeDrinkEaten(Entity<DrinkComponent> food, ref BeforeIngestedEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
// Set it to transfer amount if it exists, otherwise eat the whole volume if possible.
|
||||
args.Transfer = food.Comp.TransferAmount;
|
||||
}
|
||||
|
||||
private void OnDrinkEaten(Entity<DrinkComponent> entity, ref IngestedEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
_audio.PlayPredicted(entity.Comp.UseSound, args.Target, args.User, AudioParams.Default.WithVolume(-2f).WithVariation(0.25f));
|
||||
|
||||
var flavors = _flavorProfile.GetLocalizedFlavorsMessage(entity.Owner, args.Target, args.Split);
|
||||
|
||||
if (args.ForceFed)
|
||||
{
|
||||
if (drink.IgnoreEmpty)
|
||||
return false;
|
||||
var targetName = Identity.Entity(args.Target, EntityManager);
|
||||
var userName = Identity.Entity(args.User, EntityManager);
|
||||
|
||||
_popup.PopupClient(Loc.GetString("drink-component-try-use-drink-is-empty", ("entity", item)), item, user);
|
||||
return true;
|
||||
}
|
||||
_popup.PopupEntity(Loc.GetString("edible-force-feed-success", ("user", userName), ("verb", _ingestion.GetProtoVerb(IngestionSystem.Drink)), ("flavors", flavors)), entity, entity);
|
||||
|
||||
if (_food.IsMouthBlocked(target, user))
|
||||
return true;
|
||||
_popup.PopupClient(Loc.GetString("edible-force-feed-success-user", ("target", targetName), ("verb", _ingestion.GetProtoVerb(IngestionSystem.Drink))), args.User, args.User);
|
||||
|
||||
if (!_interaction.InRangeUnobstructed(user, item, popup: true))
|
||||
return true;
|
||||
|
||||
var forceDrink = user != target;
|
||||
|
||||
if (forceDrink)
|
||||
{
|
||||
var userName = Identity.Entity(user, EntityManager);
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)), user, target);
|
||||
|
||||
// logging
|
||||
_adminLogger.Add(LogType.ForceFeed, LogImpact.High, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to drink {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
|
||||
// log successful forced drinking
|
||||
_adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity.Owner):user} forced {ToPrettyString(args.User):target} to drink {ToPrettyString(entity.Owner):drink}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// log voluntary drinking
|
||||
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is drinking {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
|
||||
_popup.PopupClient(Loc.GetString("edible-slurp", ("flavors", flavors)), args.User, args.User);
|
||||
_popup.PopupEntity(Loc.GetString("edible-slurp"), args.User, Filter.PvsExcept(args.User), true);
|
||||
|
||||
// log successful voluntary drinking
|
||||
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} drank {ToPrettyString(entity.Owner):drink}");
|
||||
}
|
||||
|
||||
var flavors = _flavorProfile.GetLocalizedFlavorsMessage(user, drinkSolution);
|
||||
if (_ingestion.GetUsesRemaining(entity, entity.Comp.Solution, args.Split.Volume) <= 0)
|
||||
return;
|
||||
|
||||
var doAfterEventArgs = new DoAfterArgs(EntityManager,
|
||||
user,
|
||||
forceDrink ? drink.ForceFeedDelay : drink.Delay,
|
||||
new ConsumeDoAfterEvent(drink.Solution, flavors),
|
||||
eventTarget: item,
|
||||
target: target,
|
||||
used: item)
|
||||
// Leave some of the consumer's DNA on the consumed item...
|
||||
var ev = new TransferDnaEvent
|
||||
{
|
||||
BreakOnHandChange = false,
|
||||
BreakOnMove = forceDrink,
|
||||
BreakOnDamage = true,
|
||||
MovementThreshold = 0.01f,
|
||||
DistanceThreshold = 1.0f,
|
||||
// do-after will stop if item is dropped when trying to feed someone else
|
||||
// or if the item started out in the user's own hands
|
||||
NeedHand = forceDrink || _hands.IsHolding(user, item),
|
||||
Donor = args.Target,
|
||||
Recipient = entity,
|
||||
CanDnaBeCleaned = false,
|
||||
};
|
||||
RaiseLocalEvent(args.Target, ref ev);
|
||||
|
||||
_doAfter.TryStartDoAfter(doAfterEventArgs);
|
||||
return true;
|
||||
args.Repeat = !args.ForceFed;
|
||||
}
|
||||
|
||||
private void OnDrink(Entity<DrinkComponent> drink, ref EdibleEvent args)
|
||||
{
|
||||
if (args.Cancelled || args.Solution != null)
|
||||
return;
|
||||
|
||||
if (!_solutionContainer.TryGetSolution(drink.Owner, drink.Comp.Solution, out args.Solution) || IsEmpty(drink))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
|
||||
_popup.PopupClient(Loc.GetString("ingestion-try-use-is-empty", ("entity", drink)), drink, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
args.Time += TimeSpan.FromSeconds(drink.Comp.Delay);
|
||||
}
|
||||
|
||||
private void OnIsDigestible(Entity<DrinkComponent> ent, ref IsDigestibleEvent args)
|
||||
{
|
||||
// Anyone can drink from puddles on the floor!
|
||||
args.UniversalDigestion();
|
||||
}
|
||||
|
||||
private void OnGetEdibleType(Entity<DrinkComponent> ent, ref GetEdibleTypeEvent args)
|
||||
{
|
||||
if (args.Type != null)
|
||||
return;
|
||||
|
||||
args.SetPrototype(IngestionSystem.Drink);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Tools.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Shared.Nutrition.EntitySystems;
|
||||
|
||||
public sealed class UtensilSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly FoodSystem _foodSystem = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<UtensilComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(ItemSlotsSystem), typeof(ToolOpenableSystem) });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clicked with utensil
|
||||
/// </summary>
|
||||
private void OnAfterInteract(Entity<UtensilComponent> entity, ref AfterInteractEvent ev)
|
||||
{
|
||||
if (ev.Handled || ev.Target == null || !ev.CanReach)
|
||||
return;
|
||||
|
||||
var result = TryUseUtensil(ev.User, ev.Target.Value, entity);
|
||||
ev.Handled = result.Handled;
|
||||
}
|
||||
|
||||
public (bool Success, bool Handled) TryUseUtensil(EntityUid user, EntityUid target, Entity<UtensilComponent> utensil)
|
||||
{
|
||||
if (!TryComp(target, out FoodComponent? food))
|
||||
return (false, false);
|
||||
|
||||
//Prevents food usage with a wrong utensil
|
||||
if ((food.Utensil & utensil.Comp.Types) == 0)
|
||||
{
|
||||
_popupSystem.PopupClient(Loc.GetString("food-system-wrong-utensil", ("food", target), ("utensil", utensil.Owner)), user, user);
|
||||
return (false, true);
|
||||
}
|
||||
|
||||
if (!_interactionSystem.InRangeUnobstructed(user, target, popup: true))
|
||||
return (false, true);
|
||||
|
||||
return _foodSystem.TryFeed(user, user, target, food);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to break the utensil after interaction.
|
||||
/// </summary>
|
||||
/// <param name="uid">Utensil.</param>
|
||||
/// <param name="userUid">User of the utensil.</param>
|
||||
public void TryBreak(EntityUid uid, EntityUid userUid, UtensilComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
if (_robustRandom.Prob(component.BreakChance))
|
||||
{
|
||||
_audio.PlayPredicted(component.BreakSound, userUid, userUid, AudioParams.Default.WithVolume(-2f));
|
||||
Del(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,52 @@
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.Prototypes;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Nutrition;
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed at the consumer when attempting to ingest something.
|
||||
/// Raised on an entity that is trying to be ingested to see if it has universal blockers preventing it from being
|
||||
/// ingested.
|
||||
/// </summary>
|
||||
public sealed class IngestionAttemptEvent : CancellableEntityEventArgs
|
||||
[ByRefEvent]
|
||||
public record struct IngestibleEvent(bool Cancelled = false);
|
||||
|
||||
/// <summary>
|
||||
/// Raised on an entity with the <see cref="EdibleComponent"/> to check if anything is stopping
|
||||
/// another entity from consuming the delicious reagents stored inside.
|
||||
/// </summary>
|
||||
/// <param name="User">The entity trying to feed us to an entity.</param>
|
||||
[ByRefEvent]
|
||||
public record struct EdibleEvent(EntityUid User)
|
||||
{
|
||||
public Entity<SolutionComponent>? Solution = null;
|
||||
|
||||
public TimeSpan Time = TimeSpan.Zero;
|
||||
|
||||
public bool Cancelled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an entity is trying to ingest an entity to see if it has any component that can ingest it.
|
||||
/// </summary>
|
||||
/// <param name="Handled">Did a system successfully ingest this item?</param>
|
||||
/// <param name="User">The entity that is trying to feed and therefore raising the event</param>
|
||||
/// <param name="Ingested">What are we trying to ingest?</param>
|
||||
/// <param name="Ingest">Should we actually try and ingest? Or are we just testing if it's even possible </param>
|
||||
[ByRefEvent]
|
||||
public record struct AttemptIngestEvent(EntityUid User, EntityUid Ingested, bool Ingest, bool Handled = false);
|
||||
|
||||
/// <summary>
|
||||
/// Raised on an entity that is consuming another entity to see if there is anything attached to the entity
|
||||
/// that is preventing it from doing the consumption.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct IngestionAttemptEvent(SlotFlags TargetSlots, bool Cancelled = false) : IInventoryRelayEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The equipment that is blocking consumption. Should only be non-null if the event was canceled.
|
||||
@@ -12,22 +55,113 @@ public sealed class IngestionAttemptEvent : CancellableEntityEventArgs
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed at the food after finishing eating a food before it's deleted.
|
||||
/// Cancel this if you want to do something special before a food is deleted.
|
||||
/// Raised on an entity that is trying to be digested, aka turned from an entity into reagents.
|
||||
/// Returns its digestive properties or how difficult it is to convert to reagents.
|
||||
/// </summary>
|
||||
public sealed class BeforeFullyEatenEvent : CancellableEntityEventArgs
|
||||
/// <remarks>This method is currently needed for backwards compatibility with food and drink component.
|
||||
/// It also might be needed in the event items like trash and plushies have their edible component removed.
|
||||
/// There's no way to know whether this event will be made obsolete or not after Food and Drink Components
|
||||
/// are removed until after a proper body and digestion rework. Oh well!
|
||||
/// </remarks>
|
||||
[ByRefEvent]
|
||||
public record struct IsDigestibleEvent()
|
||||
{
|
||||
/// <summary>
|
||||
/// The person that ate the food.
|
||||
/// </summary>
|
||||
public EntityUid User;
|
||||
public bool Digestible = false;
|
||||
|
||||
public bool SpecialDigestion = false;
|
||||
|
||||
// If this is true, SpecialDigestion will be ignored
|
||||
public bool Universal = false;
|
||||
|
||||
// If it requires special digestion then it has to be digestible...
|
||||
public void AddDigestible(bool special)
|
||||
{
|
||||
SpecialDigestion = special;
|
||||
Digestible = true;
|
||||
}
|
||||
|
||||
// This should only be used for if you're trying to drink pure reagents from a puddle or cup or something...
|
||||
public void UniversalDigestion()
|
||||
{
|
||||
Universal = true;
|
||||
Digestible = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do After Event for trying to put food solution into stomach entity.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class EatingDoAfterEvent : SimpleDoAfterEvent;
|
||||
|
||||
/// <summary>
|
||||
/// We use this to determine if an entity should abort giving up its reagents at the last minute,
|
||||
/// as well as specifying how much of its reagents it should give up including minimums and maximums.
|
||||
/// If minimum exceeds the maximum, the event will abort.
|
||||
/// </summary>
|
||||
/// <param name="Min">The minimum amount we can transfer.</param>
|
||||
/// <param name="Max">The maximum amount we can transfer.</param>
|
||||
/// <param name="Solution">The solution we are transferring.</param>
|
||||
[ByRefEvent]
|
||||
public record struct BeforeIngestedEvent(FixedPoint2 Min, FixedPoint2 Max, Solution? Solution)
|
||||
{
|
||||
// How much we would like to transfer, gets clamped by Min and Max.
|
||||
public FixedPoint2 Transfer;
|
||||
|
||||
// Whether this event, and therefore eat attempt, should be cancelled.
|
||||
public bool Cancelled;
|
||||
|
||||
public bool TryNewMinimum(FixedPoint2 newMin)
|
||||
{
|
||||
if (newMin > Max)
|
||||
return false;
|
||||
|
||||
Min = newMin;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryNewMaximum(FixedPoint2 newMax)
|
||||
{
|
||||
if (newMax < Min)
|
||||
return false;
|
||||
|
||||
Min = newMax;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public record struct IngestingEvent(EntityUid Food, Solution Split, bool ForceFed);
|
||||
|
||||
/// <summary>
|
||||
/// Raised on an entity when it is being made to be eaten.
|
||||
/// </summary>
|
||||
/// <param name="User">Who is doing the action?</param>
|
||||
/// <param name="Target">Who is doing the eating?</param>
|
||||
/// <param name="Split">The solution we're currently eating.</param>
|
||||
/// <param name="ForceFed">Whether we're being fed by someone else, checkec enough I might as well pass it.</param>
|
||||
[ByRefEvent]
|
||||
public record struct IngestedEvent(EntityUid User, EntityUid Target, Solution Split, bool ForceFed)
|
||||
{
|
||||
// Should we refill the solution now that we've eaten it?
|
||||
// This bool basically only exists because of stackable system.
|
||||
public bool Refresh;
|
||||
|
||||
// Should we destroy the ingested entity?
|
||||
public bool Destroy;
|
||||
|
||||
// Has this eaten event been handled? Used to prevent duplicate flavor popups and sound effects.
|
||||
public bool Handled;
|
||||
|
||||
// Should we try eating again?
|
||||
public bool Repeat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed at the food after finishing eating it and before it's deleted.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct AfterFullyEatenEvent(EntityUid User)
|
||||
public readonly record struct FullyEatenEvent(EntityUid User)
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity that ate the food.
|
||||
@@ -35,6 +169,38 @@ public readonly record struct AfterFullyEatenEvent(EntityUid User)
|
||||
public readonly EntityUid User = User;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of Utensils that can be used to consume the entity, as well as a list of required types.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct GetUtensilsEvent()
|
||||
{
|
||||
public UtensilType Types = UtensilType.None;
|
||||
|
||||
public UtensilType RequiredTypes = UtensilType.None;
|
||||
|
||||
// Forces you to add to both lists if a utensil is required.
|
||||
public void AddRequiredTypes(UtensilType type)
|
||||
{
|
||||
RequiredTypes |= type;
|
||||
Types |= type;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the best fitting edible type for an entity.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct GetEdibleTypeEvent
|
||||
{
|
||||
public ProtoId<EdiblePrototype>? Type { get; private set; }
|
||||
|
||||
public void SetPrototype([ForbidLiteral] ProtoId<EdiblePrototype> proto)
|
||||
{
|
||||
Type = proto;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed at the food being sliced before it's deleted.
|
||||
/// Cancel this if you want to do something special before a food is deleted.
|
||||
|
||||
54
Content.Shared/Nutrition/Prototypes/EdiblePrototype.cs
Normal file
54
Content.Shared/Nutrition/Prototypes/EdiblePrototype.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Nutrition.Prototypes;
|
||||
|
||||
/// <summary>
|
||||
/// This stores unique data for an item that is edible, such as verbs, verb icons, verb names, sounds, ect.
|
||||
/// </summary>
|
||||
[Prototype]
|
||||
public sealed partial class EdiblePrototype : IPrototype
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[IdDataField]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The sound we make when eaten.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier UseSound = new SoundCollectionSpecifier("eating");
|
||||
|
||||
/// <summary>
|
||||
/// The localization identifier for the ingestion message.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId Message;
|
||||
|
||||
/// <summary>
|
||||
/// Localization verb used when consuming this item.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId Verb;
|
||||
|
||||
/// <summary>
|
||||
/// Localization noun used when consuming this item.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId Noun;
|
||||
|
||||
/// <summary>
|
||||
/// What type of food are we, currently used for determining verbs and some checks.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId VerbName;
|
||||
|
||||
/// <summary>
|
||||
/// What type of food are we, currently used for determining verbs and some checks.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SpriteSpecifier? VerbIcon;
|
||||
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Content.Shared.Examine;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using JetBrains.Annotations;
|
||||
@@ -37,6 +38,8 @@ namespace Content.Shared.Stacks
|
||||
SubscribeLocalEvent<StackComponent, ComponentStartup>(OnStackStarted);
|
||||
SubscribeLocalEvent<StackComponent, ExaminedEvent>(OnStackExamined);
|
||||
SubscribeLocalEvent<StackComponent, InteractUsingEvent>(OnStackInteractUsing);
|
||||
SubscribeLocalEvent<StackComponent, BeforeIngestedEvent>(OnBeforeEaten);
|
||||
SubscribeLocalEvent<StackComponent, IngestedEvent>(OnEaten);
|
||||
|
||||
_vvm.GetTypeHandler<StackComponent>()
|
||||
.AddPath(nameof(StackComponent.Count), (_, comp) => comp.Count, SetCount);
|
||||
@@ -389,6 +392,51 @@ namespace Content.Shared.Stacks
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void OnBeforeEaten(Entity<StackComponent> eaten, ref BeforeIngestedEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
if (args.Solution is not { } sol)
|
||||
return;
|
||||
|
||||
// If the entity is empty and is a lingering entity we can't eat from it.
|
||||
if (eaten.Comp.Count <= 0)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
Edible stacked items is near completely evil so we must choose one of the following:
|
||||
- Option 1: Eat the entire solution each bite and reduce the stack by 1.
|
||||
- Option 2: Multiply the solution eaten by the stack size.
|
||||
- Option 3: Divide the solution consumed by stack size.
|
||||
The easiest and safest option is and always will be Option 1 otherwise we risk reagent deletion or duplication.
|
||||
That is why we cancel if we cannot set the minimum to the entire volume of the solution.
|
||||
*/
|
||||
if(args.TryNewMinimum(sol.Volume))
|
||||
return;
|
||||
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnEaten(Entity<StackComponent> eaten, ref IngestedEvent args)
|
||||
{
|
||||
if (!Use(eaten, 1))
|
||||
return;
|
||||
|
||||
// We haven't eaten the whole stack yet or are unable to eat it completely.
|
||||
if (eaten.Comp.Count > 0 || eaten.Comp.Lingering)
|
||||
{
|
||||
args.Refresh = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Here to tell the food system to do destroy stuff.
|
||||
args.Destroy = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,6 +8,7 @@ using Content.Shared.Interaction;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Materials;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Tools.EntitySystems;
|
||||
@@ -25,6 +26,7 @@ namespace Content.Shared.Storage.EntitySystems;
|
||||
/// </summary>
|
||||
public sealed class SecretStashSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IngestionSystem _ingestion = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
|
||||
@@ -41,7 +43,7 @@ public sealed class SecretStashSystem : EntitySystem
|
||||
SubscribeLocalEvent<SecretStashComponent, DestructionEventArgs>(OnDestroyed);
|
||||
SubscribeLocalEvent<SecretStashComponent, GotReclaimedEvent>(OnReclaimed);
|
||||
SubscribeLocalEvent<SecretStashComponent, InteractUsingEvent>(OnInteractUsing, after: new[] { typeof(ToolOpenableSystem), typeof(AnchorableSystem) });
|
||||
SubscribeLocalEvent<SecretStashComponent, AfterFullyEatenEvent>(OnEaten);
|
||||
SubscribeLocalEvent<SecretStashComponent, FullyEatenEvent>(OnFullyEaten);
|
||||
SubscribeLocalEvent<SecretStashComponent, InteractHandEvent>(OnInteractHand);
|
||||
SubscribeLocalEvent<SecretStashComponent, GetVerbsEvent<InteractionVerb>>(OnGetVerb);
|
||||
}
|
||||
@@ -61,7 +63,7 @@ public sealed class SecretStashSystem : EntitySystem
|
||||
DropContentsAndAlert(entity, args.ReclaimerCoordinates);
|
||||
}
|
||||
|
||||
private void OnEaten(Entity<SecretStashComponent> entity, ref AfterFullyEatenEvent args)
|
||||
private void OnFullyEaten(Entity<SecretStashComponent> entity, ref FullyEatenEvent args)
|
||||
{
|
||||
// TODO: When newmed is finished should do damage to teeth (Or something like that!)
|
||||
var damage = entity.Comp.DamageEatenItemInside;
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
drink-component-on-use-is-empty = {CAPITALIZE(THE($owner))} is empty!
|
||||
drink-component-on-examine-is-opened = [color=yellow]Opened[/color]
|
||||
drink-component-on-examine-is-sealed = The seal is intact.
|
||||
drink-component-on-examine-is-unsealed = The seal is broken.
|
||||
drink-component-try-use-drink-not-open = Open {$owner} first!
|
||||
drink-component-try-use-drink-is-empty = {CAPITALIZE(THE($entity))} is empty!
|
||||
drink-component-try-use-drink-cannot-drink = You can't drink anything!
|
||||
drink-component-try-use-drink-had-enough = You can't drink more!
|
||||
drink-component-try-use-drink-cannot-drink-other = They can't drink anything!
|
||||
drink-component-try-use-drink-had-enough-other = They can't drink more!
|
||||
drink-component-try-use-drink-success-slurp = Slurp
|
||||
drink-component-try-use-drink-success-slurp-taste = Slurp. {$flavors}
|
||||
drink-component-force-feed = {CAPITALIZE(THE($user))} is trying to make you drink something!
|
||||
drink-component-force-feed-success = {CAPITALIZE(THE($user))} forced you to drink something! {$flavors}
|
||||
drink-component-force-feed-success-user = You successfully feed {THE($target)}
|
||||
|
||||
|
||||
drink-system-verb-drink = Drink
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
|
||||
### Interaction Messages
|
||||
|
||||
# When trying to eat food without the required utensil... but you gotta hold it
|
||||
food-you-need-to-hold-utensil = You need to be holding {INDEFINITE($utensil)} {$utensil} to eat that!
|
||||
|
||||
food-nom = Nom. {$flavors}
|
||||
food-swallow = You swallow { THE($food) }. {$flavors}
|
||||
|
||||
food-has-used-storage = You cannot eat { THE($food) } with an item stored inside.
|
||||
|
||||
food-system-remove-mask = You need to take off the {$entity} first.
|
||||
|
||||
## System
|
||||
|
||||
food-system-you-cannot-eat-any-more = You can't eat any more!
|
||||
food-system-you-cannot-eat-any-more-other = {CAPITALIZE(SUBJECT($target))} can't eat any more!
|
||||
food-system-try-use-food-is-empty = {CAPITALIZE(THE($entity))} is empty!
|
||||
food-system-wrong-utensil = You can't eat {THE($food)} with {INDEFINITE($utensil)} {$utensil}.
|
||||
food-system-cant-digest = You can't digest {THE($entity)}!
|
||||
food-system-cant-digest-other = {CAPITALIZE(SUBJECT($target))} can't digest {THE($entity)}!
|
||||
|
||||
food-system-verb-eat = Eat
|
||||
|
||||
## Force feeding
|
||||
|
||||
food-system-force-feed = {CAPITALIZE(THE($user))} is trying to feed you something!
|
||||
food-system-force-feed-success = {CAPITALIZE(THE($user))} forced you to eat something! {$flavors}
|
||||
food-system-force-feed-success-user = You successfully feed {THE($target)}
|
||||
@@ -0,0 +1,53 @@
|
||||
### Interaction Messages
|
||||
|
||||
# System
|
||||
|
||||
## When trying to ingest without the required utensil... but you gotta hold it
|
||||
ingestion-you-need-to-hold-utensil = You need to be holding {INDEFINITE($utensil)} {$utensil} to eat that!
|
||||
|
||||
ingestion-try-use-is-empty = {CAPITALIZE(THE($entity))} is empty!
|
||||
ingestion-try-use-wrong-utensil = You can't {$verb} {THE($food)} with {INDEFINITE($utensil)} {$utensil}.
|
||||
|
||||
ingestion-remove-mask = You need to take off the {$entity} first.
|
||||
|
||||
## Failed Ingestion
|
||||
|
||||
ingestion-you-cannot-ingest-any-more = You can't {$verb} any more!
|
||||
ingestion-other-cannot-ingest-any-more = {CAPITALIZE(SUBJECT($target))} can't {$verb} any more!
|
||||
|
||||
ingestion-cant-digest = You can't digest {THE($entity)}!
|
||||
ingestion-cant-digest-other = {CAPITALIZE(SUBJECT($target))} can't digest {THE($entity)}!
|
||||
|
||||
## Action Verbs, not to be confused with Verbs
|
||||
|
||||
ingestion-verb-food = Eat
|
||||
ingestion-verb-drink = Drink
|
||||
|
||||
# Edible Component
|
||||
|
||||
edible-nom = Nom. {$flavors}
|
||||
edible-slurp = Slurp. {$flavors}
|
||||
edible-swallow = You swallow { THE($food) }
|
||||
edible-gulp = Gulp. {$flavors}
|
||||
|
||||
edible-has-used-storage = You cannot {$verb} { THE($food) } with an item stored inside.
|
||||
|
||||
## Nouns
|
||||
|
||||
edible-noun-edible = edible
|
||||
edible-noun-food = food
|
||||
edible-noun-drink = drink
|
||||
edible-noun-pill = pill
|
||||
|
||||
## Verbs
|
||||
|
||||
edible-verb-edible = ingest
|
||||
edible-verb-food = eat
|
||||
edible-verb-drink = drink
|
||||
edible-verb-pill = swallow
|
||||
|
||||
## Force feeding
|
||||
|
||||
edible-force-feed = {CAPITALIZE(THE($user))} is trying to make you {$verb} something!
|
||||
edible-force-feed-success = {CAPITALIZE(THE($user))} forced you to {$verb} something! {$flavors}
|
||||
edible-force-feed-success-user = You successfully feed {THE($target)}
|
||||
@@ -1,2 +1,5 @@
|
||||
openable-component-verb-open = Open
|
||||
openable-component-verb-close = Close
|
||||
|
||||
openable-component-on-examine-is-opened = [color=yellow]Opened[/color]
|
||||
openable-component-try-use-closed = Open {$owner} first!
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
sealable-component-on-examine-is-sealed = The seal is intact.
|
||||
sealable-component-on-examine-is-unsealed = The seal is broken.
|
||||
@@ -4,6 +4,12 @@
|
||||
name: ruminant stomach
|
||||
categories: [ HideSpawnMenu ]
|
||||
components:
|
||||
- type: Stomach
|
||||
specialDigestible:
|
||||
tags:
|
||||
- Ruminant
|
||||
- Wheat
|
||||
- BananaPeel
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
stomach:
|
||||
|
||||
@@ -315,15 +315,16 @@
|
||||
- type: Clothing
|
||||
slots:
|
||||
- HEAD
|
||||
- type: Food
|
||||
- type: Edible
|
||||
edible: Drink
|
||||
solution: drink
|
||||
useSound: /Audio/Items/drink.ogg
|
||||
eatMessage: drink-component-try-use-drink-success-slurp
|
||||
delay: 0.5
|
||||
forceFeedDelay: 1.5
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
- water
|
||||
- type: DrainableSolution
|
||||
solution: drink
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
drink:
|
||||
|
||||
@@ -201,7 +201,8 @@
|
||||
- type: EdgeSpreader
|
||||
id: Puddle
|
||||
- type: StepTrigger
|
||||
- type: Drink
|
||||
- type: Edible
|
||||
edible: Drink
|
||||
delay: 3
|
||||
transferAmount: 1
|
||||
solution: puddle
|
||||
|
||||
@@ -1054,10 +1054,10 @@
|
||||
growthDelay: 20
|
||||
- type: ExaminableHunger
|
||||
- type: Wooly
|
||||
- type: Food
|
||||
- type: Edible
|
||||
destroyOnEmpty: false
|
||||
solution: wool
|
||||
requiresSpecialDigestion: true
|
||||
# Wooly prevents eating wool deleting the goat so its fine
|
||||
requireDead: false
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
solution: drink
|
||||
- type: SolutionTransfer
|
||||
canChangeTransferAmount: true
|
||||
- type: Drink
|
||||
- type: Edible
|
||||
edible: Drink
|
||||
solution: drink
|
||||
destroyOnEmpty: false
|
||||
- type: Sprite
|
||||
state: icon
|
||||
- type: MeleeWeapon
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
id: FoodBreadBunBottom
|
||||
parent: FoodBreadSliceBase
|
||||
name: bottom bun
|
||||
description: It's time to start building the burger tower.
|
||||
description: It's time to start building the burger tower.
|
||||
components:
|
||||
- type: Item
|
||||
size: Normal #patch until there is an adequate resizing system in place
|
||||
size: Normal #patch until there is an adequate resizing system in place
|
||||
- type: Food
|
||||
- type: Sprite
|
||||
drawdepth: Mobs
|
||||
@@ -83,7 +83,7 @@
|
||||
- type: FoodSequenceElement
|
||||
entries:
|
||||
Burger: BunTopBurger
|
||||
|
||||
|
||||
# Base
|
||||
|
||||
- type: entity
|
||||
@@ -95,8 +95,6 @@
|
||||
flavors:
|
||||
- bun
|
||||
- meaty
|
||||
- type: Food
|
||||
transferAmount: 5
|
||||
- type: Sprite
|
||||
sprite: Objects/Consumable/Food/burger.rsi
|
||||
- type: SolutionContainerManager
|
||||
@@ -499,7 +497,7 @@
|
||||
- ReagentId: Vitamin
|
||||
Quantity: 8
|
||||
- ReagentId: Sulfur # What you get for eating something with a flare in it
|
||||
Quantity: 5
|
||||
Quantity: 5
|
||||
- type: Tag
|
||||
tags:
|
||||
- Meat
|
||||
@@ -691,7 +689,7 @@
|
||||
description: An elusive rib shaped burger with limited availability across the galaxy. Not as good as you remember it.
|
||||
components:
|
||||
- type: Food
|
||||
trash:
|
||||
trash:
|
||||
- FoodKebabSkewer
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
@@ -702,7 +700,7 @@
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
food:
|
||||
maxVol: 30
|
||||
maxVol: 30
|
||||
reagents:
|
||||
- ReagentId: Nutriment
|
||||
Quantity: 11
|
||||
@@ -987,4 +985,4 @@
|
||||
- type: Tag
|
||||
tags:
|
||||
- Meat
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
- food
|
||||
- type: Food
|
||||
- type: Edible
|
||||
- type: Sprite
|
||||
- type: StaticPrice
|
||||
price: 0
|
||||
|
||||
@@ -10,13 +10,16 @@
|
||||
- type: Sprite
|
||||
state: produce
|
||||
# let cows eat raw produce like wheat and oats
|
||||
- type: Food
|
||||
requiredStomachs: 2
|
||||
- type: Edible
|
||||
requiresSpecialDigestion: true
|
||||
- type: Produce
|
||||
- type: PotencyVisuals
|
||||
- type: Appearance
|
||||
- type: Extractable
|
||||
grindableSolutionName: food
|
||||
- type: Tag
|
||||
tags:
|
||||
- Ruminant
|
||||
|
||||
# For produce that can be immediately eaten
|
||||
|
||||
@@ -57,6 +60,7 @@
|
||||
- type: Tag
|
||||
tags:
|
||||
- Wheat
|
||||
- Ruminant
|
||||
|
||||
- type: entity
|
||||
name: meatwheat bushel
|
||||
@@ -176,6 +180,7 @@
|
||||
- type: Tag
|
||||
tags:
|
||||
- Vegetable
|
||||
- Ruminant
|
||||
|
||||
- type: entity
|
||||
name: tower-cap log
|
||||
@@ -323,7 +328,7 @@
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
- banana
|
||||
- type: Food
|
||||
- type: Edible
|
||||
trash:
|
||||
- TrashBananaPeel
|
||||
- type: SolutionContainerManager
|
||||
@@ -365,7 +370,7 @@
|
||||
flavors:
|
||||
- banana
|
||||
- nothing
|
||||
- type: Food
|
||||
- type: Edible
|
||||
trash:
|
||||
- TrashMimanaPeel
|
||||
- type: SolutionContainerManager
|
||||
@@ -437,6 +442,7 @@
|
||||
- Recyclable
|
||||
- Trash
|
||||
- BananaPeel
|
||||
- Ruminant
|
||||
- WhitelistChameleon
|
||||
- HamsterWearable
|
||||
- type: SolutionContainerManager
|
||||
@@ -449,7 +455,7 @@
|
||||
- type: Extractable
|
||||
grindableSolutionName: food
|
||||
- type: SpaceGarbage
|
||||
- type: Food
|
||||
- type: Edible
|
||||
requiresSpecialDigestion: true
|
||||
- type: Clothing
|
||||
sprite: Objects/Specific/Hydroponics/banana.rsi
|
||||
@@ -1190,7 +1196,7 @@
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
- corn
|
||||
- type: Food
|
||||
- type: Edible
|
||||
trash:
|
||||
- FoodCornTrash
|
||||
- type: SolutionContainerManager
|
||||
@@ -1895,7 +1901,7 @@
|
||||
sprite: Objects/Specific/Hydroponics/gatfruit.rsi
|
||||
- type: Produce
|
||||
seedId: gatfruit
|
||||
- type: Food
|
||||
- type: Edible
|
||||
trash:
|
||||
- WeaponRevolverPython
|
||||
- type: Tag
|
||||
@@ -1930,7 +1936,7 @@
|
||||
heldPrefix: produce
|
||||
- type: Produce
|
||||
seedId: realCapfruit
|
||||
- type: Food
|
||||
- type: Edible
|
||||
trash:
|
||||
- RevolverCapGun
|
||||
- type: Tag
|
||||
@@ -1949,7 +1955,7 @@
|
||||
components:
|
||||
- type: Produce
|
||||
seedId: fakeCapfruit
|
||||
- type: Food
|
||||
- type: Edible
|
||||
trash:
|
||||
- RevolverCapGunFake
|
||||
|
||||
@@ -2353,7 +2359,7 @@
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
- bungo
|
||||
- type: Food
|
||||
- type: Edible
|
||||
trash:
|
||||
- FoodBungoPit
|
||||
- type: SolutionContainerManager
|
||||
@@ -2588,7 +2594,7 @@
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
- cotton
|
||||
- type: Food
|
||||
- type: Edible
|
||||
requiresSpecialDigestion: true
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
@@ -2620,7 +2626,7 @@
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
- pyrotton
|
||||
- type: Food
|
||||
- type: Edible
|
||||
requiresSpecialDigestion: true
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
@@ -2654,7 +2660,7 @@
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
- cherry
|
||||
- type: Food
|
||||
- type: Edible
|
||||
trash:
|
||||
- TrashCherryPit
|
||||
- type: SolutionContainerManager
|
||||
@@ -2729,7 +2735,7 @@
|
||||
heldPrefix: produce
|
||||
- type: Produce
|
||||
seedId: anomalyBerry
|
||||
- type: Food
|
||||
- type: Edible
|
||||
trash:
|
||||
- EffectAnomalyFloraBulb # Random loot
|
||||
- type: SolutionContainerManager
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
- state: cloth_3
|
||||
map: ["base"]
|
||||
- type: Appearance
|
||||
- type: Food
|
||||
- type: Edible
|
||||
requiresSpecialDigestion: true
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
@@ -192,7 +192,7 @@
|
||||
- type: Construction
|
||||
graph: Durathread
|
||||
node: MaterialDurathread
|
||||
- type: Food
|
||||
- type: Edible
|
||||
requiresSpecialDigestion: true
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
@@ -421,7 +421,7 @@
|
||||
- state: cotton_3
|
||||
map: ["base"]
|
||||
- type: Appearance
|
||||
- type: Food
|
||||
- type: Edible
|
||||
requiresSpecialDigestion: true
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
@@ -480,7 +480,7 @@
|
||||
- state: pyrotton_3
|
||||
map: ["base"]
|
||||
- type: Appearance
|
||||
- type: Food
|
||||
- type: Edible
|
||||
requiresSpecialDigestion: true
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
@@ -540,7 +540,7 @@
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
- banana
|
||||
- type: Food
|
||||
- type: Edible
|
||||
trash:
|
||||
- TrashBananiumPeel
|
||||
- type: BadFood
|
||||
@@ -592,7 +592,7 @@
|
||||
- type: Stack
|
||||
count: 50
|
||||
stackType: WebSilk
|
||||
- type: Food
|
||||
- type: Edible
|
||||
requiresSpecialDigestion: true
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
@@ -735,7 +735,7 @@
|
||||
- state: cotton_3
|
||||
map: ["base"]
|
||||
- type: Appearance
|
||||
- type: Food
|
||||
- type: Edible
|
||||
- type: BadFood
|
||||
- type: SolutionContainerManager
|
||||
solutions:
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
components:
|
||||
- IgnoreKudzu
|
||||
- type: Food
|
||||
requiredStomachs: 2 # ruminants have 4 stomachs but i dont care to give them literally 4 stomachs. 2 is good
|
||||
requiresSpecialDigestion: true
|
||||
delay: 0.5
|
||||
- type: FlavorProfile
|
||||
flavors:
|
||||
@@ -101,6 +101,9 @@
|
||||
reagents:
|
||||
- ReagentId: Nutriment
|
||||
Quantity: 2
|
||||
- type: Tag
|
||||
tags:
|
||||
- Ruminant
|
||||
|
||||
- type: entity
|
||||
id: WeakKudzu
|
||||
|
||||
@@ -602,12 +602,11 @@
|
||||
size: Tiny
|
||||
sprite: Objects/Specific/Chemistry/pills.rsi
|
||||
- type: Pill
|
||||
- type: Food
|
||||
- type: Edible
|
||||
delay: 0.6
|
||||
forceFeedDelay: 2
|
||||
transferAmount: null
|
||||
eatMessage: food-swallow
|
||||
useSound: /Audio/Items/pill.ogg
|
||||
edible: Pill
|
||||
- type: BadFood
|
||||
- type: FlavorProfile
|
||||
ignoreReagents: []
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
- type: entity
|
||||
parent: BaseItem
|
||||
parent: DrinkBase
|
||||
id: Bucket
|
||||
name: bucket
|
||||
description: It's a boring old bucket.
|
||||
components:
|
||||
- type: Drink
|
||||
solution: bucket
|
||||
ignoreEmpty: true
|
||||
- type: Clickable
|
||||
- type: Edible
|
||||
edible: Drink
|
||||
solution: bucket
|
||||
destroyOnEmpty: false
|
||||
- type: Sprite
|
||||
sprite: Objects/Tools/bucket.rsi
|
||||
layers:
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
query:
|
||||
- !type:ComponentQuery
|
||||
components:
|
||||
- type: Food
|
||||
- type: Edible
|
||||
considerations:
|
||||
- !type:TargetIsAliveCon
|
||||
curve: !type:InverseBoolCurve
|
||||
@@ -50,7 +50,7 @@
|
||||
query:
|
||||
- !type:ComponentQuery
|
||||
components:
|
||||
- type: Drink
|
||||
- type: Edible
|
||||
considerations:
|
||||
- !type:TargetIsAliveCon
|
||||
curve: !type:InverseBoolCurve
|
||||
|
||||
47
Resources/Prototypes/Nutrition/edible.yml
Normal file
47
Resources/Prototypes/Nutrition/edible.yml
Normal file
@@ -0,0 +1,47 @@
|
||||
# If you add a new prototype, you may want to consider adding it to IngestionSystem.API for other systems to use.
|
||||
# But only if other systems/components might want it.
|
||||
|
||||
# Food
|
||||
|
||||
- type: edible
|
||||
id: Food
|
||||
useSound: !type:SoundCollectionSpecifier
|
||||
params:
|
||||
variation: 0.2
|
||||
volume: -1
|
||||
collection: eating # I think this *should* grab the sound specifier...
|
||||
message: edible-nom
|
||||
verb: edible-verb-food
|
||||
noun: edible-noun-food
|
||||
verbName: ingestion-verb-food
|
||||
verbIcon: /Textures/Interface/VerbIcons/cutlery.svg.192dpi.png
|
||||
|
||||
# Drink
|
||||
|
||||
- type: edible
|
||||
id: Drink
|
||||
useSound: !type:SoundPathSpecifier
|
||||
params:
|
||||
variation: 0.25
|
||||
volume: -2
|
||||
path: /Audio/Items/drink.ogg
|
||||
message: edible-slurp
|
||||
verb: edible-verb-drink
|
||||
noun: edible-noun-drink
|
||||
verbName: ingestion-verb-drink
|
||||
verbIcon: /Textures/Interface/VerbIcons/drink.svg.192dpi.png
|
||||
|
||||
# Pills!
|
||||
|
||||
- type: edible
|
||||
id: Pill
|
||||
useSound: !type:SoundPathSpecifier
|
||||
params:
|
||||
variation: 0.2
|
||||
volume: -1
|
||||
path: /Audio/Items/pill.ogg
|
||||
message: edible-swallow
|
||||
verb: edible-verb-pill
|
||||
noun: edible-noun-pill
|
||||
verbName: ingestion-verb-food
|
||||
verbIcon: /Textures/Interface/VerbIcons/cutlery.svg.192dpi.png
|
||||
@@ -1199,6 +1199,9 @@
|
||||
- type: Tag
|
||||
id: RollingPin
|
||||
|
||||
- type: Tag
|
||||
id: Ruminant
|
||||
|
||||
- type: Tag
|
||||
id: SaltShaker
|
||||
|
||||
|
||||
Reference in New Issue
Block a user