From 02382045ab6928572a5bc1ae3df7bac6ee4bda90 Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 6 Aug 2025 16:12:26 +0000 Subject: [PATCH 1/6] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index f48d043d40..403c5ddd11 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: murolem - changes: - - message: Building orientation on fences, diagonal grilles and the diagonal shuttle - wall is now respected. - type: Fix - id: 8319 - time: '2025-04-23T01:58:42.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/36488 - author: kosticia changes: - message: Added analog of donk-pockets for moth - moth-pockets @@ -3906,3 +3898,10 @@ id: 8831 time: '2025-08-06T13:00:32.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/38852 +- author: aada + changes: + - message: You can examine fuel tanks to see how much they're filled. + type: Add + id: 8832 + time: '2025-08-06T16:11:19.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/39362 From 91854e077624e19c698268b028a0dd4bd706121e Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:53:38 -0700 Subject: [PATCH 2/6] 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> --- .../Kitchen/EntitySystems/SharpSystem.cs | 2 +- .../NPC/Systems/NPCUtilitySystem.cs | 27 +- .../Components/MessyDrinkerComponent.cs | 10 +- .../Nutrition/EntitySystems/DrinkSystem.cs | 200 +------ .../EntitySystems/MessyDrinkerSystem.cs | 21 +- .../EntitySystems/SliceableFoodSystem.cs | 82 ++- .../EntitySystems/SmokingSystem.Vape.cs | 5 +- .../Polymorph/Systems/PolymorphSystem.cs | 11 - Content.Shared/Animals/WoolySystem.cs | 7 - .../Damage/Systems/SharedGodmodeSystem.cs | 7 + .../Inventory/InventorySystem.Relay.cs | 2 + .../Nutrition/Components/DrinkComponent.cs | 1 + .../Nutrition/Components/EdibleComponent.cs | 86 +++ .../Nutrition/Components/FoodComponent.cs | 4 +- .../Components/IngestionBlockerComponent.cs | 5 +- .../Nutrition/Components/OpenableComponent.cs | 4 +- .../Nutrition/Components/SealableComponent.cs | 4 +- .../Nutrition/Components/UtensilComponent.cs | 2 +- .../EntitySystems/FlavorProfileSystem.cs | 22 +- .../Nutrition/EntitySystems/FoodSystem.cs | 534 ++++-------------- .../EntitySystems/IngestionBlockerSystem.cs | 19 - .../EntitySystems/IngestionSystem.API.cs | 430 ++++++++++++++ .../EntitySystems/IngestionSystem.Blockers.cs | 159 ++++++ .../EntitySystems/IngestionSystem.Utensils.cs | 157 +++++ .../EntitySystems/IngestionSystem.cs | 531 +++++++++++++++++ .../Nutrition/EntitySystems/OpenableSystem.cs | 7 +- .../EntitySystems/SharedDrinkSystem.cs | 218 +++---- .../Nutrition/EntitySystems/UtensilSystem.cs | 73 --- Content.Shared/Nutrition/IngestionEvents.cs | 186 +++++- .../Nutrition/Prototypes/EdiblePrototype.cs | 54 ++ Content.Shared/Stacks/SharedStackSystem.cs | 48 ++ .../EntitySystems/SecretStashSystem.cs | 6 +- .../nutrition/components/drink-component.ftl | 18 - .../nutrition/components/food-component.ftl | 29 - .../nutrition/components/ingestion-system.ftl | 53 ++ .../components/openable-component.ftl | 3 + .../components/sealable-component.ftl | 2 + .../Body/Organs/Animal/ruminant.yml | 6 + .../Entities/Clothing/Head/misc.yml | 7 +- .../Prototypes/Entities/Effects/puddle.yml | 3 +- .../Prototypes/Entities/Mobs/NPCs/animals.yml | 4 +- .../Objects/Consumable/Drinks/drinks.yml | 5 +- .../Objects/Consumable/Food/burger.yml | 16 +- .../Objects/Consumable/Food/food_base.yml | 2 +- .../Objects/Consumable/Food/produce.yml | 34 +- .../Entities/Objects/Materials/materials.yml | 14 +- .../Entities/Objects/Misc/kudzu.yml | 5 +- .../Entities/Objects/Specific/chemistry.yml | 5 +- .../Entities/Objects/Tools/bucket.yml | 9 +- Resources/Prototypes/NPCs/utility_queries.yml | 4 +- Resources/Prototypes/Nutrition/edible.yml | 47 ++ Resources/Prototypes/tags.yml | 3 + 52 files changed, 2169 insertions(+), 1024 deletions(-) create mode 100644 Content.Shared/Nutrition/Components/EdibleComponent.cs delete mode 100644 Content.Shared/Nutrition/EntitySystems/IngestionBlockerSystem.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/IngestionSystem.Blockers.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/IngestionSystem.Utensils.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs create mode 100644 Content.Shared/Nutrition/Prototypes/EdiblePrototype.cs delete mode 100644 Resources/Locale/en-US/nutrition/components/food-component.ftl create mode 100644 Resources/Locale/en-US/nutrition/components/ingestion-system.ftl create mode 100644 Resources/Locale/en-US/nutrition/components/sealable-component.ftl create mode 100644 Resources/Prototypes/Nutrition/edible.yml diff --git a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs index cce5fb5bd3..0ba9d0990a 100644 --- a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs @@ -38,7 +38,7 @@ public sealed class SharpSystem : EntitySystem { base.Initialize(); - SubscribeLocalEvent(OnAfterInteract, before: [typeof(UtensilSystem)]); + SubscribeLocalEvent(OnAfterInteract, before: [typeof(IngestionSystem)]); SubscribeLocalEvent(OnDoAfter); SubscribeLocalEvent>(OnGetInteractionVerbs); diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs index 489ac6de55..6de26cd056 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -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(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(owner); @@ -194,15 +188,16 @@ public sealed class NPCUtilitySystem : EntitySystem if (avoidBadFood && HasComp(targetUid)) return 0f; + var nutrition = _ingestion.TotalNutrition(targetUid, owner); + if (nutrition <= 1.0f) + return 0f; + return 1f; } case DrinkValueCon: { - if (!TryComp(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; diff --git a/Content.Server/Nutrition/Components/MessyDrinkerComponent.cs b/Content.Server/Nutrition/Components/MessyDrinkerComponent.cs index 6a1a3a0319..5519c5d983 100644 --- a/Content.Server/Nutrition/Components/MessyDrinkerComponent.cs +++ b/Content.Server/Nutrition/Components/MessyDrinkerComponent.cs @@ -1,9 +1,11 @@ using Content.Shared.FixedPoint; +using Content.Shared.Nutrition.Prototypes; +using Robust.Shared.Prototypes; namespace Content.Server.Nutrition.Components; /// -/// 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. /// [RegisterComponent] public sealed partial class MessyDrinkerComponent : Component @@ -17,6 +19,12 @@ public sealed partial class MessyDrinkerComponent : Component [DataField] public FixedPoint2 SpillAmount = 1.0; + /// + /// The types of food prototypes we can spill + /// + [DataField] + public List> SpillableTypes = new List> { "Drink" }; + [DataField] public LocId? SpillMessagePopup; } diff --git a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs index 6e1824c843..1677f1d822 100644 --- a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs @@ -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(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(OnUse, before: [typeof(ServerInventorySystem)], after: [typeof(OpenableSystem)]); - SubscribeLocalEvent(AfterInteract); - SubscribeLocalEvent(OnDoAfter); - } - - /// - /// Get the total hydration factor contained in a drink's solution. - /// - 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(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 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 entity, ref UseInHandEvent args) - { - if (args.Handled) - return; - - args.Handled = TryDrink(args.User, args.User, entity.Comp, entity); } private void OnDrinkInit(Entity 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); } - - /// - /// Raised directed at a victim when someone has force fed them a drink. - /// - private void OnDoAfter(Entity entity, ref ConsumeDoAfterEvent args) - { - if (args.Handled || args.Cancelled || entity.Comp.Deleted) - return; - - if (!TryComp(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((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(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; - } } diff --git a/Content.Server/Nutrition/EntitySystems/MessyDrinkerSystem.cs b/Content.Server/Nutrition/EntitySystems/MessyDrinkerSystem.cs index f92318d0f7..dc8c11bb7f 100644 --- a/Content.Server/Nutrition/EntitySystems/MessyDrinkerSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/MessyDrinkerSystem.cs @@ -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(OnBeforeIngestDrink); + SubscribeLocalEvent(OnIngested); } - private void OnBeforeIngestDrink(Entity ent, ref BeforeIngestDrinkEvent ev) + private void OnIngested(Entity 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 _); } diff --git a/Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs b/Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs index 3ce285b06c..2a13d07797 100644 --- a/Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs @@ -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 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(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. /// - public EntityUid Slice(EntityUid uid, - EntityUid user, - SliceableFoodComponent? comp = null, - TransformComponent? transform = null) + public EntityUid Slice(Entity 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 slice, Solution solution) { - // Replace all reagents on prototype not just copying poisons (example: slices of eaten pizza should have less nutrition) - if (TryComp(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 entity, ref ComponentStartup args) { - var foodComp = EnsureComp(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(entity); _solutionContainer.EnsureSolution(entity.Owner, foodComp.Solution, out _); } } diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs index 5a269eace5..5de6a5a631 100644 --- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs +++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs @@ -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(args.Target) - || _foodSystem.IsMouthBlocked(args.Target.Value, args.User)) + || _ingestion.HasMouthAvailable(args.Target.Value, args.User) + ) { return; } diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs index 6b6284b18e..d0f8b4e8fc 100644 --- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs +++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs @@ -53,7 +53,6 @@ public sealed partial class PolymorphSystem : EntitySystem SubscribeLocalEvent(OnPolymorphActionEvent); SubscribeLocalEvent(OnRevertPolymorphActionEvent); - SubscribeLocalEvent(OnBeforeFullyEaten); SubscribeLocalEvent(OnBeforeFullySliced); SubscribeLocalEvent(OnDestruction); @@ -126,16 +125,6 @@ public sealed partial class PolymorphSystem : EntitySystem Revert((ent, ent)); } - private void OnBeforeFullyEaten(Entity ent, ref BeforeFullyEatenEvent args) - { - var (_, comp) = ent; - if (comp.Configuration.RevertOnEat) - { - args.Cancel(); - Revert((ent, ent)); - } - } - private void OnBeforeFullySliced(Entity ent, ref BeforeFullySlicedEvent args) { var (_, comp) = ent; diff --git a/Content.Shared/Animals/WoolySystem.cs b/Content.Shared/Animals/WoolySystem.cs index 734de2f34c..203def2257 100644 --- a/Content.Shared/Animals/WoolySystem.cs +++ b/Content.Shared/Animals/WoolySystem.cs @@ -23,7 +23,6 @@ public sealed class WoolySystem : EntitySystem { base.Initialize(); - SubscribeLocalEvent(OnBeforeFullyEaten); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnEntRemoved); } @@ -77,10 +76,4 @@ public sealed class WoolySystem : EntitySystem _solutionContainer.TryAddReagent(wooly.Solution.Value, wooly.ReagentId, wooly.Quantity, out _); } } - - private void OnBeforeFullyEaten(Entity ent, ref BeforeFullyEatenEvent args) - { - // don't want moths to delete goats after eating them - args.Cancel(); - } } diff --git a/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs b/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs index feb5dd4140..4bf762c479 100644 --- a/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs +++ b/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs @@ -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(OnBeforeStatusEffect); SubscribeLocalEvent(OnBeforeOldStatusEffect); SubscribeLocalEvent(OnBeforeStaminaDamage); + SubscribeLocalEvent(BeforeEdible); SubscribeLocalEvent(OnSlipAttempt); SubscribeLocalEvent(OnDestruction); } @@ -60,6 +62,11 @@ public abstract class SharedGodmodeSystem : EntitySystem args.Cancel(); } + private void BeforeEdible(Entity ent, ref IngestibleEvent args) + { + args.Cancelled = true; + } + public virtual void EnableGodmode(EntityUid uid, GodmodeComponent? godmode = null) { godmode ??= EnsureComp(uid); diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index fe3267a92f..f4a0ccb5de 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -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(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); + SubscribeLocalEvent(RefRelayInventoryEvent); // Eye/vision events SubscribeLocalEvent(RelayInventoryEvent); diff --git a/Content.Shared/Nutrition/Components/DrinkComponent.cs b/Content.Shared/Nutrition/Components/DrinkComponent.cs index 2211d58071..a4d1114379 100644 --- a/Content.Shared/Nutrition/Components/DrinkComponent.cs +++ b/Content.Shared/Nutrition/Components/DrinkComponent.cs @@ -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 diff --git a/Content.Shared/Nutrition/Components/EdibleComponent.cs b/Content.Shared/Nutrition/Components/EdibleComponent.cs new file mode 100644 index 0000000000..4fcd9770eb --- /dev/null +++ b/Content.Shared/Nutrition/Components/EdibleComponent.cs @@ -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; + +/// +/// This is used on an entity with a solution container to flag a specific solution as being able to have its +/// reagents consumed directly. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(IngestionSystem))] +public sealed partial class EdibleComponent : Component +{ + /// + /// Name of the solution that stores the consumable reagents + /// + [DataField] + public string Solution = "food"; + + /// + /// Should this entity be deleted when our solution is emptied? + /// + [DataField] + public bool DestroyOnEmpty = true; + + /// + /// Trash we spawn when eaten, will not spawn if the item isn't deleted when empty. + /// + [DataField] + public List Trash = new(); + + /// + /// How much of our solution is eaten on a do-after completion. Set to null to eat the whole thing. + /// + [DataField] + public FixedPoint2? TransferAmount = FixedPoint2.New(5); + + /// + /// Acceptable utensils to use + /// + [DataField] + public UtensilType Utensil = UtensilType.Fork; //There are more "solid" than "liquid" food + + /// + /// Do we need a utensil to access this solution? + /// + [DataField] + public bool UtensilRequired; + + /// + /// If this is set to true, food can only be eaten if you have a stomach with a + /// 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. + /// + [DataField] + public bool RequiresSpecialDigestion; + + /// + /// How long it takes to eat the food personally. + /// + [DataField] + public TimeSpan Delay = TimeSpan.FromSeconds(1f); + + /// + /// This is how many seconds it takes to force-feed someone this food. + /// Should probably be smaller for small items like pills. + /// + [DataField] + public TimeSpan ForceFeedDelay = TimeSpan.FromSeconds(3f); + + /// + /// For mobs that are food, requires killing them before eating. + /// + [DataField] + public bool RequireDead = true; + + /// + /// Verb, icon, and sound data for our edible. + /// + [DataField] + public ProtoId Edible = IngestionSystem.Food; +} diff --git a/Content.Shared/Nutrition/Components/FoodComponent.cs b/Content.Shared/Nutrition/Components/FoodComponent.cs index ce04569fcb..5f1ec41717 100644 --- a/Content.Shared/Nutrition/Components/FoodComponent.cs +++ b/Content.Shared/Nutrition/Components/FoodComponent.cs @@ -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. /// [DataField] - public LocId EatMessage = "food-nom"; + public LocId EatMessage = "edible-nom"; /// /// How long it takes to eat the food personally. diff --git a/Content.Shared/Nutrition/Components/IngestionBlockerComponent.cs b/Content.Shared/Nutrition/Components/IngestionBlockerComponent.cs index 803bf1f8b2..931d47838b 100644 --- a/Content.Shared/Nutrition/Components/IngestionBlockerComponent.cs +++ b/Content.Shared/Nutrition/Components/IngestionBlockerComponent.cs @@ -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. /// -[RegisterComponent, Access(typeof(FoodSystem), typeof(SharedDrinkSystem), typeof(IngestionBlockerSystem))] +[RegisterComponent, Access(typeof(IngestionSystem))] public sealed partial class IngestionBlockerComponent : Component { /// /// Is this component currently blocking consumption. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("enabled")] + [DataField] public bool Enabled { get; set; } = true; } diff --git a/Content.Shared/Nutrition/Components/OpenableComponent.cs b/Content.Shared/Nutrition/Components/OpenableComponent.cs index 58d6665c58..6a32c0bdeb 100644 --- a/Content.Shared/Nutrition/Components/OpenableComponent.cs +++ b/Content.Shared/Nutrition/Components/OpenableComponent.cs @@ -36,7 +36,7 @@ public sealed partial class OpenableComponent : Component /// Text shown when examining and its open. /// [DataField] - public LocId ExamineText = "drink-component-on-examine-is-opened"; + public LocId ExamineText = "openable-component-on-examine-is-opened"; /// /// 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. /// [DataField] - public LocId ClosedPopup = "drink-component-try-use-drink-not-open"; + public LocId ClosedPopup = "openable-component-try-use-closed"; /// /// Text to show in the verb menu for the "Open" action. diff --git a/Content.Shared/Nutrition/Components/SealableComponent.cs b/Content.Shared/Nutrition/Components/SealableComponent.cs index 1c2f732e7a..71a63f103b 100644 --- a/Content.Shared/Nutrition/Components/SealableComponent.cs +++ b/Content.Shared/Nutrition/Components/SealableComponent.cs @@ -22,11 +22,11 @@ public sealed partial class SealableComponent : Component /// Text shown when examining and the item's seal has not been broken. /// [DataField] - public LocId ExamineTextSealed = "drink-component-on-examine-is-sealed"; + public LocId ExamineTextSealed = "sealable-component-on-examine-is-sealed"; /// /// Text shown when examining and the item's seal has been broken. /// [DataField] - public LocId ExamineTextUnsealed = "drink-component-on-examine-is-unsealed"; + public LocId ExamineTextUnsealed = "sealable-component-on-examine-is-unsealed"; } diff --git a/Content.Shared/Nutrition/Components/UtensilComponent.cs b/Content.Shared/Nutrition/Components/UtensilComponent.cs index e8da588186..f3c4323592 100644 --- a/Content.Shared/Nutrition/Components/UtensilComponent.cs +++ b/Content.Shared/Nutrition/Components/UtensilComponent.cs @@ -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")] diff --git a/Content.Shared/Nutrition/EntitySystems/FlavorProfileSystem.cs b/Content.Shared/Nutrition/EntitySystems/FlavorProfileSystem.cs index 31384f3a18..e887486e93 100644 --- a/Content.Shared/Nutrition/EntitySystems/FlavorProfileSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/FlavorProfileSystem.cs @@ -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 entity, EntityUid user, Solution? solution) { - if (!Resolve(uid, ref flavorProfile, false)) + HashSet flavors = new(); + HashSet? 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(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); } diff --git a/Content.Shared/Nutrition/EntitySystems/FoodSystem.cs b/Content.Shared/Nutrition/EntitySystems/FoodSystem.cs index a8b5f7ac78..a599a1e74e 100644 --- a/Content.Shared/Nutrition/EntitySystems/FoodSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/FoodSystem.cs @@ -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; /// /// Handles feeding attempts both on yourself and on the target. /// +[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(OnUseFoodInHand, after: new[] { typeof(OpenableSystem), typeof(InventorySystem) }); SubscribeLocalEvent(OnFeedFood); + SubscribeLocalEvent>(AddEatVerb); - SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(OnInventoryIngestAttempt); + + SubscribeLocalEvent(OnBeforeFoodEaten); + SubscribeLocalEvent(OnFoodEaten); + SubscribeLocalEvent(OnFoodFullyEaten); + + SubscribeLocalEvent(OnGetUtensils); + + SubscribeLocalEvent(OnIsFoodDigestible); + + SubscribeLocalEvent(OnFood); + + SubscribeLocalEvent(OnGetEdibleType); + + SubscribeLocalEvent(OnBeforeFullySliced); } /// - /// Eat item + /// Eat or drink an item /// private void OnUseFoodInHand(Entity 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); } /// @@ -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); } - /// - /// Tries to feed the food item to the target entity - /// - public (bool Success, bool Handled) TryFeed(EntityUid user, EntityUid target, EntityUid food, FoodComponent foodComp) + private void AddEatVerb(Entity entity, ref GetVerbsEvent 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(target, out var body)) - return (false, false); + if (entity.Owner == user || !args.CanInteract || !args.CanAccess) + return; - if (HasComp(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((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(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(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 entity, ref ConsumeDoAfterEvent args) + private void OnBeforeFoodEaten(Entity 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(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((args.Target.Value, body), out var stomachs)) + private void OnFoodEaten(Entity 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? 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(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 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 entity, ref GetVerbsEvent ev) - { - if (entity.Owner == ev.User || - !ev.CanInteract || - !ev.CanAccess || - !TryComp(ev.User, out var body) || - !_body.TryGetBodyOrganEntityComps((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); - } - - /// - /// Returns true if the food item can be digested by the user. - /// - public bool IsDigestibleBy(EntityUid uid, EntityUid food, FoodComponent? foodComp = null) - { - if (!Resolve(food, ref foodComp, false)) - return false; - - if (!_body.TryGetBodyOrganEntityComps(uid, out var stomachs)) - return false; - - return IsDigestibleBy(food, foodComp, stomachs); - } - - /// - /// Returns true if has a that whitelists - /// this (or if they even have enough stomachs in the first place). - /// - private bool IsDigestibleBy(EntityUid food, FoodComponent component, List> 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 utensils, HandsComponent? hands = null) - { - utensils = new List(); - - 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(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; - } - - /// - /// Block ingestion attempts based on the equipped mask or head-wear - /// - private void OnInventoryIngestAttempt(Entity entity, ref IngestionAttemptEvent args) + private void OnFood(Entity 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); } - - /// - /// Check whether the target's mouth is blocked by equipment (masks or head-wear). - /// - /// The target whose equipment is checked - /// Optional entity that will receive an informative pop-up identifying the blocking - /// piece of equipment. - /// - public bool IsMouthBlocked(EntityUid uid, EntityUid? popupUid = null) + private void OnGetUtensils(Entity 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; } - /// - /// 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. - /// - 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 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 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 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); + } + } } } diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionBlockerSystem.cs b/Content.Shared/Nutrition/EntitySystems/IngestionBlockerSystem.cs deleted file mode 100644 index f9cd233948..0000000000 --- a/Content.Shared/Nutrition/EntitySystems/IngestionBlockerSystem.cs +++ /dev/null @@ -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(OnBlockerMaskToggled); - } - - private void OnBlockerMaskToggled(Entity ent, ref ItemMaskToggledEvent args) - { - ent.Comp.Enabled = !args.Mask.Comp.IsToggled; - } -} diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs new file mode 100644 index 0000000000..3a8ef333d7 --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs @@ -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; + +/// +/// Public API for Ingestion System so you can build your own form of ingestion system. +/// +public sealed partial class IngestionSystem +{ + // List of prototypes that other components or systems might want. + public static readonly ProtoId Food = "Food"; + public static readonly ProtoId 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 + + /// + /// An entity is trying to ingest another entity in Space Station 14!!! + /// + /// The entity who is eating. + /// The entity that is trying to be ingested. + /// Returns true if we are now ingesting the item. + public bool TryIngest(EntityUid user, EntityUid ingested) + { + return TryIngest(user, user, ingested); + } + + /// + /// Overload of TryIngest for if an entity is trying to make another entity ingest an entity + /// The entity who is trying to make this happen. + /// The entity who is being made to ingest something. + /// The entity that is trying to be ingested. + public bool TryIngest(EntityUid user, EntityUid target, EntityUid ingested) + { + return AttemptIngest(user, target, ingested, true); + } + + /// + /// Checks if we can ingest a given entity without actually ingesting it. + /// + /// The entity doing the ingesting. + /// The ingested entity. + /// Returns true if it's possible for the entity to ingest this item. + public bool CanIngest(EntityUid user, EntityUid ingested) + { + return AttemptIngest(user, user, ingested, false); + } + + /// + /// Check whether we have an open pie-hole that's in range. + /// + /// The one performing the action + /// The target whose mouth is checked + /// + public bool HasMouthAvailable(EntityUid user, EntityUid target) + { + return HasMouthAvailable(user, target, DefaultFlags); + } + + /// + /// 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; + } + + /// + /// The entity that is consuming + /// The entity that is being consumed + public bool CanConsume(EntityUid user, EntityUid ingested) + { + return CanConsume(user, user, ingested, out _, out _); + } + + /// + /// Checks if we can feed an edible solution from an entity to a target. + /// + /// The one doing the feeding + /// The one being fed. + /// The food item being eaten. + /// Returns true if the user can feed the target with the ingested entity + public bool CanConsume(EntityUid user, EntityUid target, EntityUid ingested) + { + return CanConsume(user, target, ingested, out _, out _); + } + + /// + /// The one doing the feeding + /// The one being fed. + /// The food item being eaten. + /// The solution we will be consuming from. + /// The time it takes us to eat this entity if any. + /// Returns true if the user can feed the target with the ingested entity and also returns a solution + public bool CanConsume(EntityUid user, + EntityUid target, + EntityUid ingested, + [NotNullWhen(true)] out Entity? 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 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 entity) + { + if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution)) + return FixedPoint2.Zero; + + return solution.Volume; + } + + public bool IsEmpty(Entity entity) + { + return EdibleVolume(entity) == FixedPoint2.Zero; + } + + /// + /// 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. + /// + /// The consumed entity + /// The entity doing the consuming + /// The amount of nutrition the consumable is worth + public float TotalNutrition(Entity entity, EntityUid consumer) + { + if (!CanIngest(consumer, entity)) + return 0f; + + return TotalNutrition(entity); + } + + /// + /// Gets the total metabolizable nutrition from an entity, assumes we can eat and metabolize it. + /// + /// The consumed entity + /// The amount of nutrition the consumable is worth + public float TotalNutrition(Entity 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(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; + } + + /// + /// 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. + /// + /// The consumed entity + /// The entity doing the consuming + /// The amount of hydration the consumable is worth + public float TotalHydration(Entity entity, EntityUid consumer) + { + if (!CanIngest(consumer, entity)) + return 0f; + + return TotalNutrition(entity); + } + + /// + /// Gets the total metabolizable hydration from an entity, assumes we can eat and metabolize it. + /// + /// The consumed entity + /// The amount of hydration the consumable is worth + public float TotalHydration(Entity 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(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 + + /// + /// Checks if the item is currently edible. + /// + /// Entity being ingested + /// The entity trying to make the ingestion happening, not necessarily the one eating + /// Solution we're returning + /// The time it takes us to eat this entity + public bool CanAccessSolution(Entity ingested, + EntityUid user, + [NotNullWhen(true)] out Entity? 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; + } + + /// + /// 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. + /// + 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 + + /// + /// Tries to get the ingestion verbs for a given user entity and ingestible entity + /// + /// The one getting the verbs who would be doing the eating. + /// Entity being ingested. + /// Edible prototype. + /// Verb we're returning. + /// Returns true if we generated a verb. + public bool TryGetIngestionVerb(EntityUid user, EntityUid ingested, [ForbidLiteral] ProtoId 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; + } + + /// + /// Returns the most accurate edible prototype for an entity if one exists. + /// + /// entity who's edible prototype we want + /// The best matching prototype if one exists. + public ProtoId? GetEdibleType(Entity 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 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 proto) + { + var prototype = _proto.Index(proto); + + return GetProtoNoun(prototype); + } + + public string GetProtoNoun(EdiblePrototype proto) + { + return Loc.GetString(proto.Noun); + } + + public string GetEdibleVerb(Entity 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 proto) + { + var prototype = _proto.Index(proto); + + return GetProtoVerb(prototype); + } + + public string GetProtoVerb(EdiblePrototype proto) + { + return Loc.GetString(proto.Verb); + } + + #endregion +} diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Blockers.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Blockers.cs new file mode 100644 index 0000000000..e1bd480bcc --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Blockers.cs @@ -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(OnUnremovableIngestion); + SubscribeLocalEvent(OnBlockerMaskToggled); + SubscribeLocalEvent(OnIngestionBlockerAttempt); + SubscribeLocalEvent>(OnIngestionBlockerAttempt); + + // Edible Event + SubscribeLocalEvent(OnEdible); + SubscribeLocalEvent(OnStorageEdible); + SubscribeLocalEvent(OnItemSlotsEdible); + SubscribeLocalEvent(OnOpenableEdible); + + // Digestion Events + SubscribeLocalEvent(OnEdibleIsDigestible); + SubscribeLocalEvent(OnDrainableIsDigestible); + SubscribeLocalEvent(OnPuddleIsDigestible); + + SubscribeLocalEvent(OnPillBeforeEaten); + } + + private void OnUnremovableIngestion(Entity 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 ent, ref ItemMaskToggledEvent args) + { + ent.Comp.Enabled = !args.Mask.Comp.IsToggled; + } + + private void OnIngestionBlockerAttempt(Entity entity, ref IngestionAttemptEvent args) + { + if (!args.Cancelled && entity.Comp.Enabled) + args.Cancelled = true; + } + + /// + /// Block ingestion attempts based on the equipped mask or head-wear + /// + private void OnIngestionBlockerAttempt(Entity entity, ref InventoryRelayedEvent args) + { + if (args.Args.Cancelled || !entity.Comp.Enabled) + return; + + args.Args.Cancelled = true; + args.Args.Blocker = entity; + } + + private void OnEdible(Entity 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 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 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 ent, ref EdibleEvent args) + { + if (_openable.IsClosed(ent, args.User, ent.Comp)) + args.Cancelled = true; + } + + private void OnEdibleIsDigestible(Entity ent, ref IsDigestibleEvent args) + { + if (ent.Comp.RequireDead && _mobState.IsAlive(ent)) + return; + + args.AddDigestible(ent.Comp.RequiresSpecialDigestion); + } + + /// + /// 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. + /// + private void OnDrainableIsDigestible(Entity ent, ref IsDigestibleEvent args) + { + args.UniversalDigestion(); + } + + private void OnPuddleIsDigestible(Entity ent, ref IsDigestibleEvent args) + { + args.UniversalDigestion(); + } + + /// + /// I mean you have to eat the *whole* pill no? + /// + private void OnPillBeforeEaten(Entity ent, ref BeforeIngestedEvent args) + { + if (args.Cancelled || args.Solution is not { } sol) + return; + + if (args.TryNewMinimum(sol.Volume)) + return; + + args.Cancelled = true; + } +} diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Utensils.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Utensils.cs new file mode 100644 index 0000000000..670fdc8dfb --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Utensils.cs @@ -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 _utensilsQuery; + + public void InitializeUtensils() + { + SubscribeLocalEvent(OnAfterInteract, after: new[] { typeof(ToolOpenableSystem) }); + + SubscribeLocalEvent(OnGetEdibleUtensils); + + _utensilsQuery = GetEntityQuery(); + } + + /// + /// Clicked with utensil + /// + private void OnAfterInteract(Entity 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 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); + } + + /// + /// Attempt to break the utensil after interaction. + /// + /// Utensil. + /// User of the utensil. + public void TryBreak(Entity 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); + } + + /// + /// Checks if we have the utensils required to eat a certain food item. + /// + /// Entity that is trying to eat. + /// The types of utensils we need. + /// The utensils needed to eat the food item. + /// True if we are able to eat the item. + public bool TryGetUtensils(Entity entity, EntityUid food, out List utensils) + { + var ev = new GetUtensilsEvent(); + RaiseLocalEvent(food, ref ev); + + return TryGetUtensils(entity, ev.Types, ev.RequiredTypes, out utensils); + } + + public bool TryGetUtensils(Entity entity, UtensilType types, UtensilType requiredTypes, out List utensils) + { + utensils = new List(); + + 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; + + } + + /// + /// 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. + /// + /// The entity doing the action who has the utensils. + /// The types of utensils we need. + /// Returns true if we have the utensils we need. + public bool HasRequiredUtensils(EntityUid entity, UtensilType types) + { + return TryGetUtensils(entity, types, types, out _); + } + + private void OnGetEdibleUtensils(Entity 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; + } +} diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs new file mode 100644 index 0000000000..cdd366ba50 --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs @@ -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; + +/// +/// 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. +/// +/// +/// This handles the ingestion of solutions and entities. +/// +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!; + + /// + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnEdibleInit); + + // Interactions + SubscribeLocalEvent(OnUseEdibleInHand, after: new[] { typeof(OpenableSystem), typeof(InventorySystem) }); + SubscribeLocalEvent(OnEdibleInteract, after: new[] { typeof(ToolOpenableSystem) }); + + // Generic Eating Handlers + SubscribeLocalEvent(OnBeforeIngested); + SubscribeLocalEvent(OnEdibleIngested); + SubscribeLocalEvent(OnFullyEaten); + + // Body Component eating handler + SubscribeLocalEvent(OnTryIngest); + SubscribeLocalEvent(OnEatingDoAfter); + + // Verbs + SubscribeLocalEvent>(AddEdibleVerbs); + SubscribeLocalEvent(OnSolutionContainerChanged); + + // Misc + SubscribeLocalEvent(OnAttemptShake); + SubscribeLocalEvent(OnBeforeFullySliced); + + InitializeBlockers(); + InitializeUtensils(); + } + + /// + /// Eat or drink an item + /// + private void OnUseEdibleInHand(Entity entity, ref UseInHandEvent ev) + { + if (ev.Handled) + return; + + ev.Handled = TryIngest(ev.User, entity); + } + + /// + /// Feed someone else + /// + private void OnEdibleInteract(Entity entity, ref AfterInteractEvent args) + { + if (args.Handled || args.Target == null || !args.CanReach) + return; + + args.Handled = TryIngest(args.User, args.Target.Value, entity); + } + + /// Raises events to see if it's possible to ingest + /// The entity who is trying to make this happen. + /// The entity who is being made to ingest something. + /// The entity that is trying to be ingested. + /// 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. + /// Returns true if we can ingest the item. + 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 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(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 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 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. + /// + /// 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. + /// + /// Entity being eaten + /// Stomachs available to digest + public bool IsDigestibleBy(EntityUid food, List> 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; + } + + /// + /// Generic method which takes a single stomach into account, and checks if a given food item passes a stomach whitelist. + /// + /// Entity being eaten + /// Stomachs that is attempting to digest. + public bool IsDigestibleBy(EntityUid food, Entity 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 entity, ref AttemptIngestEvent args) + { + var food = args.Ingested; + var forceFed = args.User != entity.Owner; + + if (!_body.TryGetBodyOrganEntityComps(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 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(entity!, out var stomachs)) + return; + + var forceFed = args.User != entity.Owner; + + var highestAvailable = FixedPoint2.Zero; + Entity? 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; + } + + /// + /// Gets the DoAfterArgs for the specific event + /// + /// Entity that is doing the action. + /// Entity that is eating. + /// Food entity we're trying to eat. + /// The time delay for our DoAfter + /// Returns true if it was able to successfully start the DoAfter + 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 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 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 entity, ref FullyEatenEvent args) + { + SpawnTrash(entity, args.User); + } + + private void OnBeforeFullySliced(Entity entity, ref BeforeFullySlicedEvent args) + { + SpawnTrash(entity, args.User); + } + + private void AddEdibleVerbs(Entity entity, ref GetVerbsEvent 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 entity, ref AttemptShakeEvent args) + { + if (IsEmpty(entity)) + args.Cancelled = true; + } +} diff --git a/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs b/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs index 2f276fa93d..ab462557d9 100644 --- a/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs @@ -121,11 +121,8 @@ public sealed partial class OpenableSystem : EntitySystem private void OnTransferAttempt(Entity 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 entity, ref AttemptShakeEvent args) diff --git a/Content.Shared/Nutrition/EntitySystems/SharedDrinkSystem.cs b/Content.Shared/Nutrition/EntitySystems/SharedDrinkSystem.cs index 66e4834d0d..303d94d55f 100644 --- a/Content.Shared/Nutrition/EntitySystems/SharedDrinkSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/SharedDrinkSystem.cs @@ -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(OnUseDrinkInHand, after: new[] { typeof(OpenableSystem), typeof(InventorySystem) }); + SubscribeLocalEvent(OnUseDrink); + SubscribeLocalEvent(OnAttemptShake); + SubscribeLocalEvent>(AddDrinkVerb); + + SubscribeLocalEvent(OnBeforeDrinkEaten); + SubscribeLocalEvent(OnDrinkEaten); + + SubscribeLocalEvent(OnDrink); + + SubscribeLocalEvent(OnIsDigestible); + + SubscribeLocalEvent(OnGetEdibleType); } protected void OnAttemptShake(Entity entity, ref AttemptShakeEvent args) @@ -46,38 +53,6 @@ public abstract partial class SharedDrinkSystem : EntitySystem args.Cancelled = true; } - private void AddDrinkVerb(Entity entity, ref GetVerbsEvent ev) - { - if (entity.Owner == ev.User || - !ev.CanInteract || - !ev.CanAccess || - !TryComp(ev.User, out var body) || - !_body.TryGetBodyOrganEntityComps((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 } /// - /// Tries to feed the drink item to the target entity + /// Eat or drink an item /// - protected bool TryDrink(EntityUid user, EntityUid target, DrinkComponent drink, EntityUid item) + private void OnUseDrinkInHand(Entity entity, ref UseInHandEvent ev) { - if (!HasComp(target)) - return false; + if (ev.Handled) + return; - if (!_body.TryGetBodyOrganEntityComps(target, out var stomachs)) - return false; + ev.Handled = _ingestion.TryIngest(ev.User, ev.User, entity); + } - if (_openable.IsClosed(item, user, predicted: true)) - return true; + /// + /// Feed someone else + /// + private void OnUseDrink(Entity 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 entity, ref GetVerbsEvent 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 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 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 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 ent, ref IsDigestibleEvent args) + { + // Anyone can drink from puddles on the floor! + args.UniversalDigestion(); + } + + private void OnGetEdibleType(Entity ent, ref GetEdibleTypeEvent args) + { + if (args.Type != null) + return; + + args.SetPrototype(IngestionSystem.Drink); } } diff --git a/Content.Shared/Nutrition/EntitySystems/UtensilSystem.cs b/Content.Shared/Nutrition/EntitySystems/UtensilSystem.cs index 63fe822186..e69de29bb2 100644 --- a/Content.Shared/Nutrition/EntitySystems/UtensilSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/UtensilSystem.cs @@ -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(OnAfterInteract, after: new[] { typeof(ItemSlotsSystem), typeof(ToolOpenableSystem) }); - } - - /// - /// Clicked with utensil - /// - private void OnAfterInteract(Entity 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 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); - } - - /// - /// Attempt to break the utensil after interaction. - /// - /// Utensil. - /// User of the utensil. - 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); - } - } -} diff --git a/Content.Shared/Nutrition/IngestionEvents.cs b/Content.Shared/Nutrition/IngestionEvents.cs index 685b08b1bd..27988c898d 100644 --- a/Content.Shared/Nutrition/IngestionEvents.cs +++ b/Content.Shared/Nutrition/IngestionEvents.cs @@ -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; /// -/// 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. /// -public sealed class IngestionAttemptEvent : CancellableEntityEventArgs +[ByRefEvent] +public record struct IngestibleEvent(bool Cancelled = false); + +/// +/// Raised on an entity with the to check if anything is stopping +/// another entity from consuming the delicious reagents stored inside. +/// +/// The entity trying to feed us to an entity. +[ByRefEvent] +public record struct EdibleEvent(EntityUid User) +{ + public Entity? Solution = null; + + public TimeSpan Time = TimeSpan.Zero; + + public bool Cancelled; +} + +/// +/// Raised when an entity is trying to ingest an entity to see if it has any component that can ingest it. +/// +/// Did a system successfully ingest this item? +/// The entity that is trying to feed and therefore raising the event +/// What are we trying to ingest? +/// Should we actually try and ingest? Or are we just testing if it's even possible +[ByRefEvent] +public record struct AttemptIngestEvent(EntityUid User, EntityUid Ingested, bool Ingest, bool Handled = false); + +/// +/// 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. +/// +[ByRefEvent] +public record struct IngestionAttemptEvent(SlotFlags TargetSlots, bool Cancelled = false) : IInventoryRelayEvent { /// /// 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 } /// -/// 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. /// -public sealed class BeforeFullyEatenEvent : CancellableEntityEventArgs +/// 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! +/// +[ByRefEvent] +public record struct IsDigestibleEvent() { - /// - /// The person that ate the food. - /// - 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; + } +} + +/// +/// Do After Event for trying to put food solution into stomach entity. +/// +[Serializable, NetSerializable] +public sealed partial class EatingDoAfterEvent : SimpleDoAfterEvent; + +/// +/// 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. +/// +/// The minimum amount we can transfer. +/// The maximum amount we can transfer. +/// The solution we are transferring. +[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); + +/// +/// Raised on an entity when it is being made to be eaten. +/// +/// Who is doing the action? +/// Who is doing the eating? +/// The solution we're currently eating. +/// Whether we're being fed by someone else, checkec enough I might as well pass it. +[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; } /// /// Raised directed at the food after finishing eating it and before it's deleted. /// [ByRefEvent] -public readonly record struct AfterFullyEatenEvent(EntityUid User) +public readonly record struct FullyEatenEvent(EntityUid User) { /// /// The entity that ate the food. @@ -35,6 +169,38 @@ public readonly record struct AfterFullyEatenEvent(EntityUid User) public readonly EntityUid User = User; } +/// +/// Returns a list of Utensils that can be used to consume the entity, as well as a list of required types. +/// +[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; + } +} + +/// +/// Tries to get the best fitting edible type for an entity. +/// +[ByRefEvent] +public record struct GetEdibleTypeEvent +{ + public ProtoId? Type { get; private set; } + + public void SetPrototype([ForbidLiteral] ProtoId proto) + { + Type = proto; + } +} + /// /// 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. diff --git a/Content.Shared/Nutrition/Prototypes/EdiblePrototype.cs b/Content.Shared/Nutrition/Prototypes/EdiblePrototype.cs new file mode 100644 index 0000000000..0f4c23846a --- /dev/null +++ b/Content.Shared/Nutrition/Prototypes/EdiblePrototype.cs @@ -0,0 +1,54 @@ +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Shared.Nutrition.Prototypes; + +/// +/// This stores unique data for an item that is edible, such as verbs, verb icons, verb names, sounds, ect. +/// +[Prototype] +public sealed partial class EdiblePrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; } = default!; + + /// + /// The sound we make when eaten. + /// + [DataField] + public SoundSpecifier UseSound = new SoundCollectionSpecifier("eating"); + + /// + /// The localization identifier for the ingestion message. + /// + [DataField] + public LocId Message; + + /// + /// Localization verb used when consuming this item. + /// + [DataField] + public LocId Verb; + + /// + /// Localization noun used when consuming this item. + /// + [DataField] + public LocId Noun; + + /// + /// What type of food are we, currently used for determining verbs and some checks. + /// + [DataField] + public LocId VerbName; + + /// + /// What type of food are we, currently used for determining verbs and some checks. + /// + [DataField] + public SpriteSpecifier? VerbIcon; + + +} diff --git a/Content.Shared/Stacks/SharedStackSystem.cs b/Content.Shared/Stacks/SharedStackSystem.cs index cd2f38d47f..912089379a 100644 --- a/Content.Shared/Stacks/SharedStackSystem.cs +++ b/Content.Shared/Stacks/SharedStackSystem.cs @@ -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(OnStackStarted); SubscribeLocalEvent(OnStackExamined); SubscribeLocalEvent(OnStackInteractUsing); + SubscribeLocalEvent(OnBeforeEaten); + SubscribeLocalEvent(OnEaten); _vvm.GetTypeHandler() .AddPath(nameof(StackComponent.Count), (_, comp) => comp.Count, SetCount); @@ -389,6 +392,51 @@ namespace Content.Shared.Stacks ) ); } + + private void OnBeforeEaten(Entity 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 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; + } } /// diff --git a/Content.Shared/Storage/EntitySystems/SecretStashSystem.cs b/Content.Shared/Storage/EntitySystems/SecretStashSystem.cs index 51615c5afa..3691f6e2b6 100644 --- a/Content.Shared/Storage/EntitySystems/SecretStashSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SecretStashSystem.cs @@ -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; /// 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(OnDestroyed); SubscribeLocalEvent(OnReclaimed); SubscribeLocalEvent(OnInteractUsing, after: new[] { typeof(ToolOpenableSystem), typeof(AnchorableSystem) }); - SubscribeLocalEvent(OnEaten); + SubscribeLocalEvent(OnFullyEaten); SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent>(OnGetVerb); } @@ -61,7 +63,7 @@ public sealed class SecretStashSystem : EntitySystem DropContentsAndAlert(entity, args.ReclaimerCoordinates); } - private void OnEaten(Entity entity, ref AfterFullyEatenEvent args) + private void OnFullyEaten(Entity entity, ref FullyEatenEvent args) { // TODO: When newmed is finished should do damage to teeth (Or something like that!) var damage = entity.Comp.DamageEatenItemInside; diff --git a/Resources/Locale/en-US/nutrition/components/drink-component.ftl b/Resources/Locale/en-US/nutrition/components/drink-component.ftl index ab458746dd..e69de29bb2 100644 --- a/Resources/Locale/en-US/nutrition/components/drink-component.ftl +++ b/Resources/Locale/en-US/nutrition/components/drink-component.ftl @@ -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 diff --git a/Resources/Locale/en-US/nutrition/components/food-component.ftl b/Resources/Locale/en-US/nutrition/components/food-component.ftl deleted file mode 100644 index 2247ef6fd4..0000000000 --- a/Resources/Locale/en-US/nutrition/components/food-component.ftl +++ /dev/null @@ -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)} diff --git a/Resources/Locale/en-US/nutrition/components/ingestion-system.ftl b/Resources/Locale/en-US/nutrition/components/ingestion-system.ftl new file mode 100644 index 0000000000..692100e61a --- /dev/null +++ b/Resources/Locale/en-US/nutrition/components/ingestion-system.ftl @@ -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)} diff --git a/Resources/Locale/en-US/nutrition/components/openable-component.ftl b/Resources/Locale/en-US/nutrition/components/openable-component.ftl index 3acc24cf53..786885e658 100644 --- a/Resources/Locale/en-US/nutrition/components/openable-component.ftl +++ b/Resources/Locale/en-US/nutrition/components/openable-component.ftl @@ -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! diff --git a/Resources/Locale/en-US/nutrition/components/sealable-component.ftl b/Resources/Locale/en-US/nutrition/components/sealable-component.ftl new file mode 100644 index 0000000000..e826e174ef --- /dev/null +++ b/Resources/Locale/en-US/nutrition/components/sealable-component.ftl @@ -0,0 +1,2 @@ +sealable-component-on-examine-is-sealed = The seal is intact. +sealable-component-on-examine-is-unsealed = The seal is broken. diff --git a/Resources/Prototypes/Body/Organs/Animal/ruminant.yml b/Resources/Prototypes/Body/Organs/Animal/ruminant.yml index 3b00e1a223..6b64aa4c1d 100644 --- a/Resources/Prototypes/Body/Organs/Animal/ruminant.yml +++ b/Resources/Prototypes/Body/Organs/Animal/ruminant.yml @@ -4,6 +4,12 @@ name: ruminant stomach categories: [ HideSpawnMenu ] components: + - type: Stomach + specialDigestible: + tags: + - Ruminant + - Wheat + - BananaPeel - type: SolutionContainerManager solutions: stomach: diff --git a/Resources/Prototypes/Entities/Clothing/Head/misc.yml b/Resources/Prototypes/Entities/Clothing/Head/misc.yml index be8727008a..96844d4017 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/misc.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/misc.yml @@ -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: diff --git a/Resources/Prototypes/Entities/Effects/puddle.yml b/Resources/Prototypes/Entities/Effects/puddle.yml index 1b84dcd7d0..cc3df59c55 100644 --- a/Resources/Prototypes/Entities/Effects/puddle.yml +++ b/Resources/Prototypes/Entities/Effects/puddle.yml @@ -201,7 +201,8 @@ - type: EdgeSpreader id: Puddle - type: StepTrigger - - type: Drink + - type: Edible + edible: Drink delay: 3 transferAmount: 1 solution: puddle diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 920468605f..da68ee109b 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -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: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml index 2d8fadfa43..4df720d541 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml index b226620953..6d17f4fbbb 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml @@ -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 - + diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/food_base.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/food_base.yml index ddacf71a03..fedce70e79 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/food_base.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/food_base.yml @@ -9,7 +9,7 @@ - type: FlavorProfile flavors: - food - - type: Food + - type: Edible - type: Sprite - type: StaticPrice price: 0 diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml index 93c48b69c2..721c2e3e38 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Materials/materials.yml b/Resources/Prototypes/Entities/Objects/Materials/materials.yml index 5e04ac55dc..a64566f258 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/materials.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/materials.yml @@ -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: diff --git a/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml b/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml index e35e915b09..6c01a2a8dd 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml index 3a7e44775f..70ea8080ae 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml @@ -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: [] diff --git a/Resources/Prototypes/Entities/Objects/Tools/bucket.yml b/Resources/Prototypes/Entities/Objects/Tools/bucket.yml index 992c3313e2..d77e6cd2b8 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/bucket.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/bucket.yml @@ -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: diff --git a/Resources/Prototypes/NPCs/utility_queries.yml b/Resources/Prototypes/NPCs/utility_queries.yml index 23ad7a59a1..3274bdf977 100644 --- a/Resources/Prototypes/NPCs/utility_queries.yml +++ b/Resources/Prototypes/NPCs/utility_queries.yml @@ -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 diff --git a/Resources/Prototypes/Nutrition/edible.yml b/Resources/Prototypes/Nutrition/edible.yml new file mode 100644 index 0000000000..1b29bbed69 --- /dev/null +++ b/Resources/Prototypes/Nutrition/edible.yml @@ -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 diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index 24e21742c3..5d34f0f30c 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -1199,6 +1199,9 @@ - type: Tag id: RollingPin +- type: Tag + id: Ruminant + - type: Tag id: SaltShaker From 4821bff9415bf027fdde9f9149f629a7bfe6aca1 Mon Sep 17 00:00:00 2001 From: Tao <56692749+TaoNewt@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:58:07 +0100 Subject: [PATCH 3/6] Fun with cardboard! (#37363) * learning??? * made card walls work, then made game unlaunchable (: * Still broken, added notes that I thought of while in bed * wall, door, table and chair are now bare min functional, yay * learnt why not to web edit... * added floors, walls and floors fully complete * added swords, shields, armour, helmets and arrows * added funny sound and cleanup small issues * cleanup * cleanup * credited myself * card to cardboard * fixed licence issue and meta thingy * adjusted arrow stam-damage * made card carpets more regular * simplified sprite, reduced stam damage * formatting fixes --------- Co-authored-by: beck-thompson --- Content.Shared/Physics/CollisionGroup.cs | 2 + Resources/Audio/Effects/attributions.yml | 10 ++ Resources/Audio/Effects/card_drag.ogg | Bin 0 -> 21803 bytes Resources/Audio/Items/Toys/card_tube_bonk.ogg | Bin 0 -> 8379 bytes Resources/Prototypes/Damage/modifier_sets.yml | 7 + .../Entities/Clothing/Head/helmets.yml | 22 +++ .../Entities/Clothing/OuterClothing/armor.yml | 19 ++ .../Prototypes/Entities/Objects/Fun/toys.yml | 38 ++++ .../Entities/Objects/Materials/materials.yml | 7 + .../Entities/Objects/Shields/shields.yml | 55 ++++++ .../Weapons/Guns/Projectiles/arrows.yml | 25 +++ .../Doors/MaterialDoors/material_doors.yml | 79 +++++++++ .../Structures/Furniture/Tables/tables.yml | 55 ++++++ .../Entities/Structures/Furniture/carpets.yml | 57 ++++++ .../Entities/Structures/Furniture/chairs.yml | 34 ++++ .../Entities/Structures/Walls/walls.yml | 71 ++++++++ .../Construction/Graphs/clothing/armor.yml | 28 +++ .../Construction/Graphs/furniture/seats.yml | 19 ++ .../Construction/Graphs/furniture/tables.yml | 18 ++ .../Graphs/structures/cardwall.yml | 25 +++ .../Construction/Graphs/structures/doors.yml | 21 +++ .../Graphs/weapons/card_shield.yml | 17 ++ .../Graphs/weapons/card_sword.yml | 14 ++ .../Graphs/weapons/improvised_arrow.yml | 19 ++ .../Recipes/Construction/clothing.yml | 16 ++ .../Recipes/Construction/furniture.yml | 24 +++ .../Recipes/Construction/structures.yml | 23 +++ .../Recipes/Construction/weapons.yml | 24 +++ .../card_helmet.rsi/equipped-HELMET-vox.png | Bin 0 -> 831 bytes .../card_helmet.rsi/equipped-HELMET.png | Bin 0 -> 852 bytes .../Head/Helmets/card_helmet.rsi/icon.png | Bin 0 -> 419 bytes .../Head/Helmets/card_helmet.rsi/meta.json | 22 +++ .../equipped-OUTERCLOTHING-vox.png | Bin 0 -> 1113 bytes .../equipped-OUTERCLOTHING.png | Bin 0 -> 1040 bytes .../Armor/card_armour.rsi/icon.png | Bin 0 -> 472 bytes .../Armor/card_armour.rsi/meta.json | 22 +++ .../Fun/card_sword.rsi/equipped-BACKPACK.png | Bin 0 -> 503 bytes .../card_sword.rsi/equipped-SUITSTORAGE.png | Bin 0 -> 503 bytes .../Objects/Fun/card_sword.rsi/icon.png | Bin 0 -> 486 bytes .../Fun/card_sword.rsi/inhand-left.png | Bin 0 -> 523 bytes .../Fun/card_sword.rsi/inhand-right.png | Bin 0 -> 503 bytes .../Objects/Fun/card_sword.rsi/meta.json | 30 ++++ .../Melee/shields.rsi/cardshield-icon.png | Bin 0 -> 425 bytes .../shields.rsi/cardshield-inhand-left.png | Bin 0 -> 844 bytes .../shields.rsi/cardshield-inhand-right.png | Bin 0 -> 818 bytes .../Weapons/Melee/shields.rsi/meta.json | 14 +- .../MineralDoors/card_door.rsi/closed.png | Bin 0 -> 446 bytes .../MineralDoors/card_door.rsi/closing.png | Bin 0 -> 968 bytes .../MineralDoors/card_door.rsi/meta.json | 57 ++++++ .../Doors/MineralDoors/card_door.rsi/open.png | Bin 0 -> 227 bytes .../MineralDoors/card_door.rsi/opening.png | Bin 0 -> 1005 bytes .../Carpets/card_carpet.rsi/carpet_0.png | Bin 0 -> 503 bytes .../Carpets/card_carpet.rsi/carpet_1.png | Bin 0 -> 502 bytes .../Carpets/card_carpet.rsi/carpet_2.png | Bin 0 -> 109 bytes .../Carpets/card_carpet.rsi/carpet_3.png | Bin 0 -> 502 bytes .../Carpets/card_carpet.rsi/carpet_4.png | Bin 0 -> 507 bytes .../Carpets/card_carpet.rsi/carpet_5.png | Bin 0 -> 503 bytes .../Carpets/card_carpet.rsi/carpet_6.png | Bin 0 -> 507 bytes .../Carpets/card_carpet.rsi/carpet_7.png | Bin 0 -> 545 bytes .../Carpets/card_carpet.rsi/full.png | Bin 0 -> 282 bytes .../Carpets/card_carpet.rsi/meta.json | 46 +++++ .../Furniture/Tables/card.rsi/full.png | Bin 0 -> 449 bytes .../Furniture/Tables/card.rsi/meta.json | 163 ++++++++++++++++++ .../Furniture/Tables/card.rsi/state_0.png | Bin 0 -> 658 bytes .../Furniture/Tables/card.rsi/state_1.png | Bin 0 -> 603 bytes .../Furniture/Tables/card.rsi/state_2.png | Bin 0 -> 658 bytes .../Furniture/Tables/card.rsi/state_3.png | Bin 0 -> 626 bytes .../Furniture/Tables/card.rsi/state_4.png | Bin 0 -> 534 bytes .../Furniture/Tables/card.rsi/state_5.png | Bin 0 -> 661 bytes .../Furniture/Tables/card.rsi/state_6.png | Bin 0 -> 581 bytes .../Furniture/Tables/card.rsi/state_7.png | Bin 0 -> 450 bytes .../Furniture/chairs.rsi/card-stool.png | Bin 0 -> 766 bytes .../Structures/Furniture/chairs.rsi/meta.json | 4 + .../Structures/Walls/card.rsi/card0.png | Bin 0 -> 745 bytes .../Structures/Walls/card.rsi/card1.png | Bin 0 -> 508 bytes .../Structures/Walls/card.rsi/card2.png | Bin 0 -> 745 bytes .../Structures/Walls/card.rsi/card3.png | Bin 0 -> 581 bytes .../Structures/Walls/card.rsi/card4.png | Bin 0 -> 514 bytes .../Structures/Walls/card.rsi/card5.png | Bin 0 -> 556 bytes .../Structures/Walls/card.rsi/card6.png | Bin 0 -> 604 bytes .../Structures/Walls/card.rsi/card7.png | Bin 0 -> 452 bytes .../Structures/Walls/card.rsi/full.png | Bin 0 -> 475 bytes .../Structures/Walls/card.rsi/meta.json | 46 +++++ 83 files changed, 1132 insertions(+), 1 deletion(-) create mode 100644 Resources/Audio/Effects/card_drag.ogg create mode 100644 Resources/Audio/Items/Toys/card_tube_bonk.ogg create mode 100644 Resources/Prototypes/Recipes/Construction/Graphs/structures/cardwall.yml create mode 100644 Resources/Prototypes/Recipes/Construction/Graphs/weapons/card_shield.yml create mode 100644 Resources/Prototypes/Recipes/Construction/Graphs/weapons/card_sword.yml create mode 100644 Resources/Textures/Clothing/Head/Helmets/card_helmet.rsi/equipped-HELMET-vox.png create mode 100644 Resources/Textures/Clothing/Head/Helmets/card_helmet.rsi/equipped-HELMET.png create mode 100644 Resources/Textures/Clothing/Head/Helmets/card_helmet.rsi/icon.png create mode 100644 Resources/Textures/Clothing/Head/Helmets/card_helmet.rsi/meta.json create mode 100644 Resources/Textures/Clothing/OuterClothing/Armor/card_armour.rsi/equipped-OUTERCLOTHING-vox.png create mode 100644 Resources/Textures/Clothing/OuterClothing/Armor/card_armour.rsi/equipped-OUTERCLOTHING.png create mode 100644 Resources/Textures/Clothing/OuterClothing/Armor/card_armour.rsi/icon.png create mode 100644 Resources/Textures/Clothing/OuterClothing/Armor/card_armour.rsi/meta.json create mode 100644 Resources/Textures/Objects/Fun/card_sword.rsi/equipped-BACKPACK.png create mode 100644 Resources/Textures/Objects/Fun/card_sword.rsi/equipped-SUITSTORAGE.png create mode 100644 Resources/Textures/Objects/Fun/card_sword.rsi/icon.png create mode 100644 Resources/Textures/Objects/Fun/card_sword.rsi/inhand-left.png create mode 100644 Resources/Textures/Objects/Fun/card_sword.rsi/inhand-right.png create mode 100644 Resources/Textures/Objects/Fun/card_sword.rsi/meta.json create mode 100644 Resources/Textures/Objects/Weapons/Melee/shields.rsi/cardshield-icon.png create mode 100644 Resources/Textures/Objects/Weapons/Melee/shields.rsi/cardshield-inhand-left.png create mode 100644 Resources/Textures/Objects/Weapons/Melee/shields.rsi/cardshield-inhand-right.png create mode 100644 Resources/Textures/Structures/Doors/MineralDoors/card_door.rsi/closed.png create mode 100644 Resources/Textures/Structures/Doors/MineralDoors/card_door.rsi/closing.png create mode 100644 Resources/Textures/Structures/Doors/MineralDoors/card_door.rsi/meta.json create mode 100644 Resources/Textures/Structures/Doors/MineralDoors/card_door.rsi/open.png create mode 100644 Resources/Textures/Structures/Doors/MineralDoors/card_door.rsi/opening.png create mode 100644 Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_0.png create mode 100644 Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_1.png create mode 100644 Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_2.png create mode 100644 Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_3.png create mode 100644 Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_4.png create mode 100644 Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_5.png create mode 100644 Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_6.png create mode 100644 Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_7.png create mode 100644 Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/full.png create mode 100644 Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/meta.json create mode 100644 Resources/Textures/Structures/Furniture/Tables/card.rsi/full.png create mode 100644 Resources/Textures/Structures/Furniture/Tables/card.rsi/meta.json create mode 100644 Resources/Textures/Structures/Furniture/Tables/card.rsi/state_0.png create mode 100644 Resources/Textures/Structures/Furniture/Tables/card.rsi/state_1.png create mode 100644 Resources/Textures/Structures/Furniture/Tables/card.rsi/state_2.png create mode 100644 Resources/Textures/Structures/Furniture/Tables/card.rsi/state_3.png create mode 100644 Resources/Textures/Structures/Furniture/Tables/card.rsi/state_4.png create mode 100644 Resources/Textures/Structures/Furniture/Tables/card.rsi/state_5.png create mode 100644 Resources/Textures/Structures/Furniture/Tables/card.rsi/state_6.png create mode 100644 Resources/Textures/Structures/Furniture/Tables/card.rsi/state_7.png create mode 100644 Resources/Textures/Structures/Furniture/chairs.rsi/card-stool.png create mode 100644 Resources/Textures/Structures/Walls/card.rsi/card0.png create mode 100644 Resources/Textures/Structures/Walls/card.rsi/card1.png create mode 100644 Resources/Textures/Structures/Walls/card.rsi/card2.png create mode 100644 Resources/Textures/Structures/Walls/card.rsi/card3.png create mode 100644 Resources/Textures/Structures/Walls/card.rsi/card4.png create mode 100644 Resources/Textures/Structures/Walls/card.rsi/card5.png create mode 100644 Resources/Textures/Structures/Walls/card.rsi/card6.png create mode 100644 Resources/Textures/Structures/Walls/card.rsi/card7.png create mode 100644 Resources/Textures/Structures/Walls/card.rsi/full.png create mode 100644 Resources/Textures/Structures/Walls/card.rsi/meta.json diff --git a/Content.Shared/Physics/CollisionGroup.cs b/Content.Shared/Physics/CollisionGroup.cs index db065d22b1..1f19184b50 100644 --- a/Content.Shared/Physics/CollisionGroup.cs +++ b/Content.Shared/Physics/CollisionGroup.cs @@ -78,6 +78,8 @@ public enum CollisionGroup WallLayer = Opaque | Impassable | HighImpassable | MidImpassable | LowImpassable | BulletImpassable | InteractImpassable, GlassLayer = Impassable | HighImpassable | MidImpassable | LowImpassable | BulletImpassable | InteractImpassable, HalfWallLayer = MidImpassable | LowImpassable, + FlimsyLayer = Opaque | HighImpassable | MidImpassable | LowImpassable | InteractImpassable, + // Allows people to interact past and target players inside of this SpecialWallLayer = Opaque | HighImpassable | MidImpassable | LowImpassable | BulletImpassable, diff --git a/Resources/Audio/Effects/attributions.yml b/Resources/Audio/Effects/attributions.yml index 3a7f18c7a3..9f85c4fb05 100644 --- a/Resources/Audio/Effects/attributions.yml +++ b/Resources/Audio/Effects/attributions.yml @@ -256,3 +256,13 @@ license: "CC-BY-4.0" copyright: "Clipped by FairlySadPanda (Github) from a sound created by CheChoDj (Freesound)" source: "https://freesound.org/people/CheChoDj/sounds/609353/" + +- files: [card_drag.ogg] + copyright: 'created by Tao7891' + license: CC0-1.0 + source: https://github.com/space-wizards/space-station-14/pull/37363 + +- files: [card_tube_bonk.ogg] + copyright: 'created by Crinkem on Freesound' + license: "CC0-1.0" + source: https://freesound.org/people/Crinkem/sounds/492027/ diff --git a/Resources/Audio/Effects/card_drag.ogg b/Resources/Audio/Effects/card_drag.ogg new file mode 100644 index 0000000000000000000000000000000000000000..8ecf632e8abb092b042de64b42dca3fac9518d25 GIT binary patch literal 21803 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 YS_1H>PWD5#Xqh+#0LKFs7yu3d0MA|vLjV8( literal 0 HcmV?d00001 diff --git a/Resources/Audio/Items/Toys/card_tube_bonk.ogg b/Resources/Audio/Items/Toys/card_tube_bonk.ogg new file mode 100644 index 0000000000000000000000000000000000000000..b300702c25c7addd53796def93a5e5ab1bb3bccd GIT binary patch literal 8379 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 JPDHLkV1f@Raku~g literal 0 HcmV?d00001 diff --git a/Resources/Textures/Clothing/Head/Helmets/card_helmet.rsi/equipped-HELMET.png b/Resources/Textures/Clothing/Head/Helmets/card_helmet.rsi/equipped-HELMET.png new file mode 100644 index 0000000000000000000000000000000000000000..0ff496c88e52a145584687526415748e295b11d3 GIT binary patch literal 852 zcmV-a1FQUrP)Px&4oO5oRCt{2nomm`K^VrLNWe|Ht~RDi)hHoIph9a4Em9EDQ;*f7M|1}7o_fa3Qo03iNlLuf8h{mwp7eeOYt<(L@>sgOrQ@EAn^KtpH9 z=C!^g!O|b{O8@}V7eup^7uM$*5MP=z!{aKna{b0LHK5@5R6}Adn|pq1-T0}23OEI5MA zolH-`+=WO;MK-TFdNfS5KSL3?6VT8ZV&H0o!W!D)+C31`=iUK6-yrsnPV9E6Qp4o5*S-+B z&FcELXJrA`E<&YB4Xf+heY=Pe1VIo4K@bE%5ClOG1VIoF72*1TZqMAd$nRU5d%)-T zN3(Ttfyc@t3>YQbrw4$|oecbvnaE80p4%{cOx{I<*}-~ z7UyS@R;^Gk3m}vLOKhmU3XzbCPal+|R<7gyYv!6u9ABC<{gMPVpx8UWS^!bFOJoe= z9KdA~8jeI*bvylFEI0yrG-SWL8c?7H6gxH^gldE=d%USL+pBNr%+&+XqExA&p);`7 z1%CTRuf^o^4RX&Xv{hFYUUt2Vxb4%&m*z~vILGAlGXTKJ;eOw=34$O9f*=TjAP9mW e2!bFUJbwVevs`-SeHXL<0000Px$T}ebiR9J=Wl|f6wU>L`L#)6C(vc^zK4X@B&@;h|&1N03%@6fjhy7(PD zbxN<{DIz3DV~i!M=1OTDWJ5^Pt&I-tcY62mKF{+T{NaU^l$4Y|CKIJeZ)l#pR}Q|- z$9n*p7gwR@g=)|nng^grBKcwk0CzHFYCrMnxEPilT4e2udPDORNu(g>f|{3=$&5{2 z95<->BI`;B>k2roi($>qzQnTSj_WQO@jC&4qiTukn_Kz=W9c2WQlfYFkcDb=IOCF9>0I97b^#B3Ib>6myurBl$4bJ%NJ3ciqR|=j$Hr% N002ovPDHLkV1fyYx^n;k literal 0 HcmV?d00001 diff --git a/Resources/Textures/Clothing/Head/Helmets/card_helmet.rsi/meta.json b/Resources/Textures/Clothing/Head/Helmets/card_helmet.rsi/meta.json new file mode 100644 index 0000000000..6027df662b --- /dev/null +++ b/Resources/Textures/Clothing/Head/Helmets/card_helmet.rsi/meta.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Sprites by TaoNewt (github)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-HELMET", + "directions": 4 + }, + { + "name": "equipped-HELMET-vox", + "directions": 4 + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Clothing/OuterClothing/Armor/card_armour.rsi/equipped-OUTERCLOTHING-vox.png b/Resources/Textures/Clothing/OuterClothing/Armor/card_armour.rsi/equipped-OUTERCLOTHING-vox.png new file mode 100644 index 0000000000000000000000000000000000000000..a0d570b468f8e335684272b73170e1524d08989d GIT binary patch literal 1113 zcmV-f1g86mP)Px(6G=otRCt{2n$K??W1;j zVYO8&Ws|<&-BvI(IF63bJ@x&i{ks~G^9_k%U~tr%I(D}eT-|7v&Hi4&`kw5f3c^YC z(9C6@007#Z9vUs(@@Jp))s2Q?UV+`#i~mP%270%Tp~0<55x@LcvVI7=o!;&HpY0O= z1B2t`wT7eP^Mu400N|ihKrZ_P2c-f4Kw=C>$LEQc*BS;s{JRE#22pS7(7Syo85zJV z!uC&k8e|tG^`?&6=|zH4eGy7VhFU3`wbP44y{UWdJLzWNtTp;QEitq^J)|YJ)IJJ= z^yjCIrwmxchQvT?!EkvNSkfX1)|(0r=+I0;Z`flJXV+%uQbJ_5%&T4+dXc z+tbu0rl~A)JEaA!R^wU%i|D;?U}D=Drm0P2pQbUMOaNR4>CzLqof5uECNVTRbERk8 zy#rF&Kz;fOK>N)#B*x%eh{6mY2-4v?=CvZ}G z{LHpzv z{rz9U(;{eaj;k9DqtS$7wGR2m}IwKp+qZ1OkCT;EETidYy!k z<$WJzky<=7I2x^)1}8BV`$4D^wNf_e0Rb4H01W01iq8?C%v&%qsveH+es@IN|$LxZcz; z_;e3|Gyz_{EnrM(OQb0PRnK)`d7QI7eDiK2xM=;z7zU zkT^}m&n^>jW`UBCt&0$0SAQas0N8Jk#{DL^$Y&TL3BWp1{~`Dk f2m}Jb|BZhEvb#cOAgjjO00000NkvXXu0mjf#r_1f literal 0 HcmV?d00001 diff --git a/Resources/Textures/Clothing/OuterClothing/Armor/card_armour.rsi/equipped-OUTERCLOTHING.png b/Resources/Textures/Clothing/OuterClothing/Armor/card_armour.rsi/equipped-OUTERCLOTHING.png new file mode 100644 index 0000000000000000000000000000000000000000..4605c87ea405f889a1dd5d325d6197e5f4a981e3 GIT binary patch literal 1040 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 co&JfN9~+-2 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Fun/card_sword.rsi/inhand-left.png b/Resources/Textures/Objects/Fun/card_sword.rsi/inhand-left.png new file mode 100644 index 0000000000000000000000000000000000000000..b5dc4c4116b90f9072368e0a41f8f84be0c571fd GIT binary patch literal 523 zcmV+m0`&cfP)Px$#Ysd#RCt{2+97X)Koke?*C|4piHXUn$%#57pMX>~sl@CHV5!CK3t;A2zfVwE zU)d~6vrJS>Ocbhaq9M6JO74z(M``{)4Oh7LhXVJA0001>O%{hDPa@Ixaj3|XNMv!S ze5`jW8y(A%#=b{71NR||LoxO}I+msKJ|oo)(|I6#&!fkOdwPBQP~Oj#Jc-2N_m7Tc zN!e(H*H&;A-#i3X(X6zGb^%AJ7E{DGM2LJ#700000xGI;{ z@m=>KuznG`q-`DF^%{fo&(j~hfe4U(xub)Gw-?5$m zj>^}`N$@?7x-?{1pMdRd@1!>Xy$FO}1gJA<$mr#DmPx$u}MThRCt{2+AD6uKoADtNs(nm1px+CLDdt41^NV5P$WZ>Ucl6$OM3wyAmKel zcwkUqK|z7-pe?AjiYg_3&8%Plhk{o7`2ETZ5dZ)Ha7p7ptH{@!W2(s4X&kVQX*_Ta zrXpX@U5B!w;2f5+qM*6!5ckFs5zV*;O5;F(etlC`6r}bCwsDOlTxr*F$Xh(p+xrLG z_$&jd$k(sW9vx3-k;O}DJz43Km)7bYaDB}Waq&bg5-{|WHyR~i3@2~Yp8Qx&-l&y; zF`c|oX#!ps@OOcO}-cpgT zciYXN7ronVO216+UINF{nRiB(4SoIq00000001y>xB3PoYyVZS_6OW@uWvxI_Fskg zU))tHNm8}{Dnt`dwUVUvhgnxi5Zmo0HN-BGAd-NlHN-BGz`n~I)nl7U5bF|aQ$w_w zgkE=v*+>FQ0;Ku|Y$T!8WRq~EU!FXY?ii6wLgNION!jQaxlRJN#v5R}Zy?Vv{VR4( tvTtD0#?UnJU+sH9@2*1t004l7egGIsKk3BICR_jj002ovPDHLkV1ksd;aC6w literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Fun/card_sword.rsi/meta.json b/Resources/Textures/Objects/Fun/card_sword.rsi/meta.json new file mode 100644 index 0000000000..dfbec476c3 --- /dev/null +++ b/Resources/Textures/Objects/Fun/card_sword.rsi/meta.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Sprites by TaoNewt (github)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + }, + { + "name": "equipped-BACKPACK", + "directions": 4 + }, + { + "name": "equipped-SUITSTORAGE", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Objects/Weapons/Melee/shields.rsi/cardshield-icon.png b/Resources/Textures/Objects/Weapons/Melee/shields.rsi/cardshield-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f73eb51293439bb81e951ae7c6442bb5e1f3bdb6 GIT binary patch literal 425 zcmV;a0apHrP)Px$V@X6oR9J=Wm9b94Fcd{^Pz6aKLMn)*EHEPRPy9#cuHD%fyY>?pyLG6EM zQWM&!B*=DEOJ!(d(zHn&D+rKsCtL45-}7wC25Qx+RqL0f6(u~jAJ)nF%zZ0m(0+Q( z^=QZ}a%$*e+`PL74?QveBn*BK;`aJV%wzdMSh%(n005jEZ&G>a!7{&rVnfT+5fO&7 zZ7d0rr=y|-`}GAhaKS?l2LISG2LPkl5@XBMcjANPxt2JXTCA+M%10Fb>4NtNxs6hOB>)Y1^i$0unn z+kGj3Gz4v&gRQuTshF1rNJ32K3jmwXh%lyPt^^Q6@D+zMo#8!LD4W-+^(XWNvXZ`| T(eFw?00000NkvXXu0mjfoOrav literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Weapons/Melee/shields.rsi/cardshield-inhand-left.png b/Resources/Textures/Objects/Weapons/Melee/shields.rsi/cardshield-inhand-left.png new file mode 100644 index 0000000000000000000000000000000000000000..bed0d999c07208d577e509b852bfb7c95d152413 GIT binary patch literal 844 zcmV-S1GD^zP)Px&21!IgRCt{2+P_Z|VHgMS?-7vR2}S6oij``RkU->zF2sq%-LUFFf`5WIICQR? zb#XB+Y_c{wlQ?#3LK6~Fnox?AOIx8OcQ3h1&Vk&e#iPA)?<)|W&(`+t>GM99_jxaQ zUw{xo2qAxt7H5@6el_Z0BE&(+c-L70Kynm@9;>=m!6;j zOv?fQgcTXGb%a4Bgh3_b{ywOL+yR{pu)Tq%dqqEefI|TecDK=ER3RCaw%0We%nw^u zTR#8zu0FPtz(?zdPZ^@<-zVYws!Wgk=ex7 z&K{36Jq0d6bJ7l{AvV7k=!_w`p6S@t4dWD6y^QCxlZXrtA(hTbsdQF)u`m-fRZm=c zfMJ~e^_^bb?|6-uR5~jqvI{b@in!^1Xn|6iB7BQZ8TgoCXb@A(t;AmCpKJ z(+%SkxqQ+6Y|AQ)UNJ@B)&!lT8#-%Q#&Y?h@2BrbrL&TAj}nQ2nHCPKN4!#P!L0yJ z(p57AW?E1pF#uE`J-$E*H6hE_X9>9FFyaq`CfRrAxk*Y4WAm*2Y7;0Ymw5JCtc zgb+f=P4oHvvc0-AFJv#zvAQ(RV-#H$6N-T%Dg z%A$SI=`ufrOabSBlt@~wjuV|>cJ5iAINS@M#>RPvY-_h%!O-3KrSIq71Kxi8CM8!E zZ9gZ%g3uX*Rs#}i-ve#E-RM<-2P%Mwp#Q25@c17g1u!j3co9Y)PWn1|N(do@TPx%>`6pHRCt{2+P_O1VHgMS?}^sjRpXC@SdF$NErNp9cBpg``VY9agJAbgcIc2f zLYKI7=_Yh>4RtPbbPH4vN(}g`iN__4r`)|E2kBrvlH7Y2&E0vCKA$nZdG2|i^L^jI z`v8OxLI@#*kpGbobOL@!tuEDhpC>neG|kykfG^2+<`>w zklNZ2T6g!W4YBcefnC6$y+AC+w(9}_!0g;}FT&V*JP|V+HN57! z0KMS{UgQxN*SEulT7<^ALAonCE`VJZmYn3E09-Bwzq`dW#vqr0B2JbQ|fZG7K)jH?b wb6gidV+`YuroG6;Ry3Y4J6H)Jgb?ri1^g2*3T*udr2qf`07*qoM6N<$f=j4<9{>OV literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Weapons/Melee/shields.rsi/meta.json b/Resources/Textures/Objects/Weapons/Melee/shields.rsi/meta.json index 2d2663c158..307cb4bb4a 100644 --- a/Resources/Textures/Objects/Weapons/Melee/shields.rsi/meta.json +++ b/Resources/Textures/Objects/Weapons/Melee/shields.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from https://github.com/Citadel-Station-13/Citadel-Station-13/commit/84223c65f5caf667a84f3c0f49bc2a41cdc6c4e3", + "copyright": "Taken from https://github.com/Citadel-Station-13/Citadel-Station-13/commit/84223c65f5caf667a84f3c0f49bc2a41cdc6c4e3, card shield sprites by TaoNewt (github)", "size": { "x": 32, "y": 32 @@ -51,6 +51,18 @@ "name": "ratvarian-inhand-left", "directions": 4 }, + { + "name": "cardshield-icon" + }, + { + "name": "cardshield-inhand-right", + "directions": 4 + }, + { + "name": "cardshield-inhand-left", + "directions": 4 + }, + { "name": "mirror-icon", "delays": [ diff --git a/Resources/Textures/Structures/Doors/MineralDoors/card_door.rsi/closed.png b/Resources/Textures/Structures/Doors/MineralDoors/card_door.rsi/closed.png new file mode 100644 index 0000000000000000000000000000000000000000..442e1df8b6cb40029de43732815fd777f13f83be GIT binary patch literal 446 zcmV;v0YUzWP)Px$cu7P-R9J=Wm(fmxFcgOW5uA=GY!Iu7%nOz!-YoIPNAYcZA0J7RWiD}&EChy| z%QnptyjfXAx|L9ZCj3{2ww&`(+LLo?j}P||0KB|;2*Me>_YaWP8Hy0~ZM6+JHUR*r zY;aaq+YoXZgfpSFtbb}dHi1fmTW#YP1Qjv$y^eYF-gKTi&c;$izd*JqgnHNSVX<6u zX_GqDPGWnpTyw_`iI_SZn?T6`U5Ne{K*PL(s$WP2P(>GNmc6xVQUOZ;1>7A-CXQ=m zmmOaRgW>_$+(1kn8uQ%jc4GT!I4%Z&r}~i!Wqa>M@BBXf-7}C>6h#1l&3qyiKzIjM zQ&(!>%m76}lzs*(fXsOysZRF>lO;RSjIAI%oTPFC0KmD~N*&i4w*Ua&9dXM&DFDay zotw|E&5t?X9qoNLH^6xKj7zN-Kd+ed>Px&f=NU{RCt{2oXu|2Koo^WVCpwMoNsd!`^YU2?0PyvD z1mnpWMAI3(e%C9313iDziCeW@Xx6LnV86BW9M5mnc5nXv>g^{ZFE5@R#q2Sgt;5gj zgXgC410L>6jKxfd{ARtnw4JHgyl6Vp;){Osa+zfOfWe#fs<#kCmdI~4DoaL9#pZSU zdVKzQ6E2gCA27IY@g~-pA3T0^02lKsh)&PF8fY~tH-`X`zY}g+Wy$8H@ojnFXI#v$ zr2Mimg=8cO09h1(L|ct}5LE7XVNn1Ql}aT5aIzHoM-%|E0&D{^ef)fk%Kx#rJpFd; z-bWz=IPA0m0HE6&pz!w=>w;|$J_;Ft?R7vNZQKf=?7;_}1CT`lNEF90T%VtKVNn3c zq5veiJRN#tQ2@xI0FXriAghq;0Gn!e9>U4I2;6Pyb}JtM0Ki_OR#@&@1aNx;=VkKT zpTDl9+pK&5gWeao8y=u@FNLopndl^pED8Ww6aca)0Ax`B$SUMjkXT~Em}kR-%8Wt= zV7sNKJI@whWlq3qd90f4S~7;nw_OtE=lSHz=7{`YC)`~64mO(3q}_I=te$+@zDfOj zk*_jO^pVEprOVfyugjO-ACS}*R{hUN+COhUU)4v}eyV(xS-cmOZN)k~<4f-k5PR*i zA!$n5U$vi~H7ChO()Qx9Se2hOm+h;{mGSBR0AzmV{Q-G8I%&OlrmTK`9v>>)y!Cc;u%izpg&$5QHr0JD}Se!2R;yyTyI<rN*DsPy7}b4SB|A-UEAQ0>M0(|)|qe!UB>?pzbD^!@gF8lc^`>F7CJ?LyJ@_z$K)QM?vFQ6O%0000B-94b;?6fJZfA1cc!>Q1~z{CM3&hQ!h)BU{u=as+tHH@i`9e$)p SDF*@_%HZkh=d#Wzp$PziFIYYR literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Doors/MineralDoors/card_door.rsi/opening.png b/Resources/Textures/Structures/Doors/MineralDoors/card_door.rsi/opening.png new file mode 100644 index 0000000000000000000000000000000000000000..c26d93225e12c15442d4acb7bab02f8db9fea0f0 GIT binary patch literal 1005 zcmVPx&r%6OXRCt{2oWX9>KoEu}l__mnk|t3D5-kU!O7u{v7tTBaCvH3iXP$xA;LJ0? zjSCWoAXOz2rBW4=rfI9Rpi1UI*4-?-_N=qJbZ#)1Wh}x}29iDErXWp~bfA`@_MSlI}<$fGOU)5Df^P$s^WO12Rqn`D7 z003Y(8l#q#>x*e&Lek7JuN5OQB4Uosi?Pd+C((AR(b~4>PJaTSI zXq$9>Xv*w%wu9-Sj{$<^dl4*6$GO-szYApDkoeZ+V*oMeEg+8nUF5qtKk;U!jJoJ8 z;Og|)r;pO=TJ3@s{mY=xI?#8=i*gNIeA?^KTRL2Yg$hAfzh}9tQa2v0Iy$kV$|~iYFaq&FG8(jo6mQ3 z{Yccq>iZ`1`2aQFp4XgON0*Nm^>zBLz7$aptM40rKA=GQ#^(d_7g# z9~k`*@bdu$fg3=KwgFUm{4%0#K&4WF(0Tl_!n+`1wAX@uO^0Wp^7tyBztZSZpl!hB zbX*)NkFT;zq)UPEYA976AHe;2K#}2N05RGI#Bm%vk6%`J3m`_@0IEEG8R0E}7;XSD z+yG*nq@#7%=H$cqC>Gut0AQDs_eoKF=zsecKbEpZ+W_jEz3Y@(=+v7H+UEmYg;aHQLge$)d|>o_ bqH6pFlBI{aB{0w*00000NkvXXu0mjf&>ZjR literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_0.png b/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_0.png new file mode 100644 index 0000000000000000000000000000000000000000..f165ae667316f0f00659063682b59fbd306bc0b5 GIT binary patch literal 503 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%zH+s4_hE&XX zJ8Q4sVFQuYd_iWX6&*^ci@4|R`s!?5&ix|<>TLt?2|0If4-5Bs{b9sH{qxL#P0uHzjsG#x;dYJAvEi^4v&D~ z&Yk;$W`1`{y=HufCxJU5nQa5>hK25p5p&9O_IXYHzg$|uzR<44p8bI5p@t-rFUKms zKhfn6K&WlZ$`qXVefsiy7WcNyof4|s&+q!a)c5wuti7$TE2q4<&tTl3+t4d{Ks zfBk0S<#9v3!OviBPj$WiYO8*q3)k24FFm0-@vnLU$hC&dH<)f52voTC@uJ=Hb|&eD zH+Pp$P+#$sah)5-Tb`u}4&SmHK=vO4`Oo?NP5oz^AD0;M%@g9Dt5jY0x8a@QZ}S6i zzk5DfQxJLeqTHfu|D1)`47!zeF>b7Dc&+|pmDiLv*Ac#HlVhzrz5g6@(wlGWTVozd Y9}Lc`USDuN2N;tKp00i_>zopr0PKI;n*aa+ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_1.png b/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_1.png new file mode 100644 index 0000000000000000000000000000000000000000..842e29f0f3daada0e9a6df618dc3828b0d3baf1b GIT binary patch literal 502 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%zH+Z@@hE&XX zJ8PrgVFQ7-^&O0gE}>ebO{{9$_Iawa#Vs~gHeP@5z>(P{uS2F8xn`|$3!Ji6CuQ|p zukg>7KlYjS{(kN~#oAiViuN zpDrGF%(mMotKWdJgh5K!fqlWj)ebApmDkjY2mX^6H<06P-e35gp{juKQ+4AytNV8= zUo$HuGQD6hQET9HSUk(SyJOFO8`(9FOD}r`h85LYTUNx~-LffW?bXkfx;O0^L8_8L z%3tktxVz=vTPNGV>QwRgR<59d~f>s7V!EGhAE7JW47c!uR)Nm{UHlvy fZ@ydfmCK%Wc3$niV>?~}Ba*?>)z4*}Q$iB})sWN% literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_2.png b/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_2.png new file mode 100644 index 0000000000000000000000000000000000000000..2975c479be7566a6c59a7aca4e20778aa850feb7 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|)ID7sLn`LH rJ;=xlwOr9UK@K`#+rw1POb(`njxgN@xNA?@buF literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_3.png b/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_3.png new file mode 100644 index 0000000000000000000000000000000000000000..842e29f0f3daada0e9a6df618dc3828b0d3baf1b GIT binary patch literal 502 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%zH+Z@@hE&XX zJ8PrgVFQ7-^&O0gE}>ebO{{9$_Iawa#Vs~gHeP@5z>(P{uS2F8xn`|$3!Ji6CuQ|p zukg>7KlYjS{(kN~#oAiViuN zpDrGF%(mMotKWdJgh5K!fqlWj)ebApmDkjY2mX^6H<06P-e35gp{juKQ+4AytNV8= zUo$HuGQD6hQET9HSUk(SyJOFO8`(9FOD}r`h85LYTUNx~-LffW?bXkfx;O0^L8_8L z%3tktxVz=vTPNGV>QwRgR<59d~f>s7V!EGhAE7JW47c!uR)Nm{UHlvy fZ@ydfmCK%Wc3$niV>?~}Ba*?>)z4*}Q$iB})sWN% literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_4.png b/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_4.png new file mode 100644 index 0000000000000000000000000000000000000000..7ecec5ee703ee6125a622fbd18d66a720d9f8d59 GIT binary patch literal 507 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%zw|crbhE&XX zJImVdu!BIGdIyuDh|7c+M`qF3yaR$kffowfwgfnxmV6NuP{@+!FkQ=J;U*FHjXJ02 zuDo;ep2y^;w>NyAdb8rw^Y6)WuNzs;l^Cr%f8Njfy8L}R_E|sLXEpzylw$Hls63y? z!qV=O(Dwu0S8NaQEMUDbgK-Oklnu9!`s1qE?`!`riK~-0cW_y<{(@k?X+ukrgOOC( zv!Bn-M9V(lG;R=cNM^ghlo7IbOUH>fFV~s&i>In)PCI?o^-@(#V3<+7&eN}tz1KQA ze+h4pbl_fK$eh7w@+xkD#_D^e@v(Q-1xQawt9*VdYwyLj?|ru#*4f;jQ)zX6cgH`; z1dy?37@`<@zgsDkTwyySe)_IP(NQIufLyv zzv{ohUs1TNz5?&LmPfvuqxkDq{Q=$u(n@z2uG=h_sa{e#H44R$^v2wu+e;6at+^(h c|Migm#P7Kkrt>BFfU(Ko>FVdQ&MBb@0Kg~RO#lD@ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_5.png b/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_5.png new file mode 100644 index 0000000000000000000000000000000000000000..3fd87a6de3b2115b382505077cd5d53f2b23b190 GIT binary patch literal 503 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%zH+s4_hE&XX zJIm1Tu!DfBc?Xjs3)7O2kU0wG4>(q8ys&VN(pdPEr$i(~M$vqMx9F^Z$gaf^)1IC) z*>So`*SBqZ+Vic_`TOSUzJGH=S=@5-%OyK)>_7L}?>Xl_=_kJ``+wEP-N92U*_9tW z`Qedzk9le&dkg0R<_j|ztQdIrah+KG!_{JC zxA6P_lKl>A`Cd5lv`&hWz3@(O*FB!23JO2d;jZAlx4C}b{!FL%&eGodwtKtJDgLyE zo1)g0-@17Z$2Wnk_v{}qz2FtpQ@FyP@tbpB?9wSu<}<1_@Hr$iy<(;eKh-* g#Jx9HUFEjFW{_L8)pM#LFeVv1UHx3vIVCg!09X3gO#lD@ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_6.png b/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_6.png new file mode 100644 index 0000000000000000000000000000000000000000..7ecec5ee703ee6125a622fbd18d66a720d9f8d59 GIT binary patch literal 507 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%zw|crbhE&XX zJImVdu!BIGdIyuDh|7c+M`qF3yaR$kffowfwgfnxmV6NuP{@+!FkQ=J;U*FHjXJ02 zuDo;ep2y^;w>NyAdb8rw^Y6)WuNzs;l^Cr%f8Njfy8L}R_E|sLXEpzylw$Hls63y? z!qV=O(Dwu0S8NaQEMUDbgK-Oklnu9!`s1qE?`!`riK~-0cW_y<{(@k?X+ukrgOOC( zv!Bn-M9V(lG;R=cNM^ghlo7IbOUH>fFV~s&i>In)PCI?o^-@(#V3<+7&eN}tz1KQA ze+h4pbl_fK$eh7w@+xkD#_D^e@v(Q-1xQawt9*VdYwyLj?|ru#*4f;jQ)zX6cgH`; z1dy?37@`<@zgsDkTwyySe)_IP(NQIufLyv zzv{ohUs1TNz5?&LmPfvuqxkDq{Q=$u(n@z2uG=h_sa{e#H44R$^v2wu+e;6at+^(h c|Migm#P7Kkrt>BFfU(Ko>FVdQ&MBb@0Kg~RO#lD@ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_7.png b/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/carpet_7.png new file mode 100644 index 0000000000000000000000000000000000000000..a6e84b4d43a18f16e8dcffd4f7f283b895012188 GIT binary patch literal 545 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%zZ+p5phE&XX zJKNUpu!De`dIyu@6b%6RI(3Jo&NW>-SBonzubJQuws~KZj@oA44yf1A7D8Jl1<@@{YH4?Tsfs zKFqqKOQt!5*TIlu!ClT3>P+3WhyONe*5&bx_xW1PJL=R zcYfL0-%~?aH^xDuuDP2sBG< zv^wyGW8Kt+5-ckJc@VBzko|ef@3;fsr_IRz^XlEqQofV-8R3TbioEAauY5SybXrt> z1Gj^@O9A7)@9aGa(cvdq^$wWMzi>Q>E5_kPd2^7;eAW;KD+ZGf=?)sz(d;b>mRtYv YIyW`h3ccy=1jador>mdKI;Vst0I5vrqyPW_ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/full.png b/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/full.png new file mode 100644 index 0000000000000000000000000000000000000000..43fbf2fbe7090a817b9bf86c0c533c04888c30b2 GIT binary patch literal 282 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}S3F%DLn2z= zPB-K`WWeKEAHd!cu-GI>P0jCs?+$MEhkAEfgiBtDXu3FZlr}e(`8;w7iT!gu;+*uG z{Ah*6r+LH2lqr=5e^VOyf0E5(AsS z6uawQVXEre6W=ul@h>>_^8>R%tn}N@bsHR7`S;vYW_VCxaIyUP;a{c)7mdKI;Vst090dhTmS$7 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/meta.json b/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/meta.json new file mode 100644 index 0000000000..b67903f65e --- /dev/null +++ b/Resources/Textures/Structures/Furniture/Carpets/card_carpet.rsi/meta.json @@ -0,0 +1,46 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Sprites by TaoNewt (github)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "full" + }, + { + "name": "carpet_0", + "directions": 4 + }, + { + "name": "carpet_1", + "directions": 4 + }, + { + "name": "carpet_2", + "directions": 4 + }, + { + "name": "carpet_3", + "directions": 4 + }, + { + "name": "carpet_4", + "directions": 4 + }, + { + "name": "carpet_5", + "directions": 4 + }, + { + "name": "carpet_6", + "directions": 4 + }, + { + "name": "carpet_7", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Structures/Furniture/Tables/card.rsi/full.png b/Resources/Textures/Structures/Furniture/Tables/card.rsi/full.png new file mode 100644 index 0000000000000000000000000000000000000000..dde5d4d9c704ad2bed14d3bee0109ed6c4187705 GIT binary patch literal 449 zcmV;y0Y3hTP)Px$dr3q=R9J=WmrYK>KorOSng#;OhvhS&jnPU13ln1E(s%$DT)t!BenXTh7hHdB+? zw3-bkcaZUva4;Ofi)Sz#F%a%zR{U5huAFQ_ZFGo=^V90eo~NG9K^Fn z1IS7Z*>WBUaf-u}IxIuO)x}vTGs1-0?)NPo6Ch&`K$``a>D~Hns6-B?1H5X_VK4xO ziG1Y<@z_2S<^ZVm)6qfS_`GDL25Q+5GfMze(nUF01vT`w^nlfQfK)1An*4-H9^Er; rG}GV401#U7c?bX~W>egJxKVupYKe-njBl=j00000NkvXXu0mjf?MJ^_ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Furniture/Tables/card.rsi/meta.json b/Resources/Textures/Structures/Furniture/Tables/card.rsi/meta.json new file mode 100644 index 0000000000..1f73612ec2 --- /dev/null +++ b/Resources/Textures/Structures/Furniture/Tables/card.rsi/meta.json @@ -0,0 +1,163 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Sprites by TaoNewt (github)", + "states": [ + { + "name": "full", + "delays": [ + [ + 1 + ] + ] + }, + { + "name": "state_0", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "state_1", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "state_2", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "state_3", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "state_4", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "state_5", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "state_6", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "state_7", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + } + ] +} diff --git a/Resources/Textures/Structures/Furniture/Tables/card.rsi/state_0.png b/Resources/Textures/Structures/Furniture/Tables/card.rsi/state_0.png new file mode 100644 index 0000000000000000000000000000000000000000..7234b7389046929135a093571a1c5c53f024e4a3 GIT binary patch literal 658 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Furniture/Tables/card.rsi/state_6.png b/Resources/Textures/Structures/Furniture/Tables/card.rsi/state_6.png new file mode 100644 index 0000000000000000000000000000000000000000..16cd343dfc2b283b93f34dc8cfe098072b13a3ad GIT binary patch literal 581 zcmV-L0=oT)P)Px$|4BqaRCt{2n>$a!P!z`xN_-6<4~0ZrG$xLG6IM)I-JCi*Iyu1V2XJFDeganq z!zN2*C=8+$dG$e>)PbZiP7U{@|65p>hJ-1C!OwejozmUt)N#!uq{^?AK z+=WM%H}@%(yc;S9{|7_>x&Reyv#%UuZ3TFf50*GSe109 z#1dz11rQPK=8u$PthE3RL;$(~G)i-6BBG{N_Yd;>p(iE-b3((>m>Q35<=h;kIf8te z-(=_pN2thQ5l|@?sl0ai0q6qIhyZi}XhZVUy=1#mSd5XeseT>zTI3lNfya#u#HRehD?(VZcdhDaforDO1fUB*0}~LJ`Gd>`IfAVEZE~#csv=_mx&SmH09^o@#0!A0(Kofa zUnv(?T3?)54qY{#XLn`LH zoxRbE*-^wbKBb#Qv6V~EWGc(0h+q0*b*)?fC%m4YIoV0g(^2YRk%Kn8SJL~O)VjOl? zXs+s>)W*Hw<(7uGug==KEoXI6U@T#f;#|Od!Gl4zVHp?W*@~bLbE};N=X<$l#GUPa z<+b!LyST&UYQ}YOx8AIuAHRQp+SR}7;`8m_FYT0iQF$@#Z(Y{$z~J2+f)mWVcki5+ z8YcV3z@~S`z1H870lO9ka=&mkSTz6prQh2dSS~PTs5J07CS3j3^P6Px%xJg7oRCt{2n$d0&K@^66=qfBy6wrpaVuDyAH8o9**E~XBpij{U@hN-(FTLz_ zW74EaNlGY*g-|hSLTjNy7%#SLfx_;Y*#V@S?`pC;GylJbVPb!&kDm`cHB|Mzv?=-q#Y8@vMl=$ha96-sT>}6-kKTH{ zylSIZKBIK6tZ%G7bIOC-GWtRP_JHn11Ko>;j;^G20RRE@L8c`D^QPPUuH1Iv1K@Y$ z`3gY)zrV;R2jD(DJV0Cp0DD#)#0+)pLMV>b@48k~UixOj0z^-XE-3)xmJ=&uI^|kL z&vHlAw|aeTbK?3>g}e^{fX1(;bM29N8-Qf|@lgG>L=3?Az^O2*@1`xQPaXlS_Vv)U zmFQEaT&)k!#MOEOW;jIYn#8ESo3^YzX$P^lvnBG`H1@w92n|kUxpZbgJu9 z*eraI^8)S}VjGNCdUjs$KMF+u!6AC`ZKY~^Jai;!-K4F&Bd>y$$sv+V0Ii=x*9Scrv6NJwqyW?^ zCx`{pSaF$rj93cSe>+m*DhOd~P5L3a&2zM_+T?sCPlCESl8w9yl#wWlQ~*ZL2+Rwe zKF9%>_6CQ_I3F+9H}I`=tkfSl03SBj0RX4f@rROLy|9nW92}LDjQH^p35W4!a~nUZ zWoaMe02~~Z?wk=AME98hs*h~`9h#Sa91pTSq}~PG^7Ffetk{10dM57zLq5I~efe1W wWl-z+fN7iyn6?id0o00=ock>D^16Ne0|xI%9=o9WBLDyZ07*qoM6N<$f|=4_Hvj+t literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Furniture/chairs.rsi/meta.json b/Resources/Textures/Structures/Furniture/chairs.rsi/meta.json index 435b686c72..1616f665c8 100644 --- a/Resources/Textures/Structures/Furniture/chairs.rsi/meta.json +++ b/Resources/Textures/Structures/Furniture/chairs.rsi/meta.json @@ -102,6 +102,10 @@ { "name": "xeno-chair", "directions": 4 + }, + { + "name": "card-stool", + "directions": 4 } ] } diff --git a/Resources/Textures/Structures/Walls/card.rsi/card0.png b/Resources/Textures/Structures/Walls/card.rsi/card0.png new file mode 100644 index 0000000000000000000000000000000000000000..aca7f7cc5d87714e6f94d2e1ce9f11378f8f061e GIT binary patch literal 745 zcmVPx%qe(f|QBAi!Ku!^5lWq1rK(3JNyN}cZ z^aQ;@ccUyQi_ndw5W*lr6ePNc+?vT4GT8XDRR30h2A2k=@rX|Gu6~XU6vF0;pjxomZXQHZPo6(jc9exO5)yKmgrpzm7^2 z<5lyWv3gsPzCQ zRs<+=dR3}R`-7DQP?uN{pv+%dwfg&^9osN(gs}igcW>#v?#|~+-#$1GgfwU#0cFBJ zu18BNzYkGhWevzr13*6&T8$dg{=kM=D_9YrsAjJ#bco>?NvG|N2-cqiR_*OirZdFN zD#UR7WIC{Lx(MdQ@!>(pSb+DPkEleOEoMGE2>thIFbKdN02_(`tX_a3LIk)rsHQ?6 zA|XTodjM>x0SEOY_bcq0I-XTTc)SOfvs17JfxHMVJwz&sPUvye6D5s=>+cby995g_XW ztw!y!UeK(r6v6hH0=fWlYdq}_kaXJGbSO7ZY)1%P0JT6W8z+j`RzDG40AN{!vd8C5 b4^RIAJ&@ZPQUH+d00000NkvXXu0mjftz}1( literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Walls/card.rsi/card1.png b/Resources/Textures/Structures/Walls/card.rsi/card1.png new file mode 100644 index 0000000000000000000000000000000000000000..f0550405f672a6f65feda518ab380dd9fbb50506 GIT binary patch literal 508 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%zw|TlahE&XX zJA1$1p#YI~{$s_SE`B$B9@NcYo%dxhf8{#~;J7XL+?C!mGTz+cov=oa8{J(F{ ek%P(2l6So5ec97tb^tIs89ZJ6T-G@yGywq172E6p literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Walls/card.rsi/card2.png b/Resources/Textures/Structures/Walls/card.rsi/card2.png new file mode 100644 index 0000000000000000000000000000000000000000..aca7f7cc5d87714e6f94d2e1ce9f11378f8f061e GIT binary patch literal 745 zcmVPx%qe(f|QBAi!Ku!^5lWq1rK(3JNyN}cZ z^aQ;@ccUyQi_ndw5W*lr6ePNc+?vT4GT8XDRR30h2A2k=@rX|Gu6~XU6vF0;pjxomZXQHZPo6(jc9exO5)yKmgrpzm7^2 z<5lyWv3gsPzCQ zRs<+=dR3}R`-7DQP?uN{pv+%dwfg&^9osN(gs}igcW>#v?#|~+-#$1GgfwU#0cFBJ zu18BNzYkGhWevzr13*6&T8$dg{=kM=D_9YrsAjJ#bco>?NvG|N2-cqiR_*OirZdFN zD#UR7WIC{Lx(MdQ@!>(pSb+DPkEleOEoMGE2>thIFbKdN02_(`tX_a3LIk)rsHQ?6 zA|XTodjM>x0SEOY_bcq0I-XTTc)SOfvs17JfxHMVJwz&sPUvye6D5s=>+cby995g_XW ztw!y!UeK(r6v6hH0=fWlYdq}_kaXJGbSO7ZY)1%P0JT6W8z+j`RzDG40AN{!vd8C5 b4^RIAJ&@ZPQUH+d00000NkvXXu0mjftz}1( literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Walls/card.rsi/card3.png b/Resources/Textures/Structures/Walls/card.rsi/card3.png new file mode 100644 index 0000000000000000000000000000000000000000..e9760a8a0ddc68dd4bf1b05830c1210c1e66e869 GIT binary patch literal 581 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%z|9iSPhE&XX zJImLb$x-6Cd@{dS-c4@isjO;(Y>fhQ|8mGGe{gnr-WU7&UiynC&tE8&2y}j$P!vxw~tGx?>k(1 zgY91W^%Zf8ukxwb#WGz{T*6e&?ZU_E-yLQCNx-wH$QVM+{h8KeXo{1|wn9ri}eylu)bv8UmIxw9Fce$$aIhRJ?aXZv~su5DCW@aLD^ z<>F=RiyaS%6_gdN(5+v6_PD%q^nuHtyTcBx`1~iX-XU(Y^V6)}kV{Y7(wS$ohFnse z+%4$1`NQP}91iLYZY%}TH+Qh)22s7qo zA)8jhHGWOA#(8Ij1w}v2T|G=@t6Pr literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Walls/card.rsi/card4.png b/Resources/Textures/Structures/Walls/card.rsi/card4.png new file mode 100644 index 0000000000000000000000000000000000000000..a51edee25dedee28439a6702983ea38419f8e2e4 GIT binary patch literal 514 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%z_jtNEhE&XX zJIm0E*-@nJ{|T|cp4OIzBOHnz9*Q?Acs71v`6hIJ;f>q3BC4746U24Ix)U}pa5Fz5 z%KVY(qGuV~{-d?=I}NPs_npZ}`yA(L5dQb@O$E7C*E2I~*Y3J<6rmwhmA*=LNt$YouivIf-e|9iP!KA^AgRD0D_bvQfz1Q3O z($AM4bJAC@%bcZSlV)?$q@&%-ZRNW6N$+{=7HBWHeot-r^pFN!hh(M~3?^y~d=SQp zhGWmzEcY&9V45czx2PT0WeJ9zMpky9&?P(Wt9^jcYV=b@P+Av?u6wVq=aG^Vq=c7sw}>8 i)AV1p`ONsjuhN$*gPNw!neYi1sSKX3elF{r5}E+QGu*WR literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Walls/card.rsi/card5.png b/Resources/Textures/Structures/Walls/card.rsi/card5.png new file mode 100644 index 0000000000000000000000000000000000000000..6741c5e94e61f81911b3d02c2a744b14d4f97198 GIT binary patch literal 556 zcmV+{0@MA8P)Px$=1D|BRCt{2nlVqpKp2K!Ob0aJU<`?3Oo%bW!NkScpWx`?>TdLRNcx!4D@!4u#HNZ-N#=IQ$aH-HRYk|Jc(8K}Swcm~km8L%o*wWjs*#Q8pj z&wyY6Er2E%KntJ=2G9a%a$5&P)mk>=WHQPRjK*=|aUVra8l2|Rl5}@{F->z`zt>GX z-fDN8HMrm<1NL{et>^4kK9hfFcYH`#8E=1q$#CH7R+&=xuRt(>7C;jWpaswb184y> z&Ia^*-G5sHNFf8b$$;&A1KvOEd--oUSpr)G-J6%BoAZ;z{(^d=ndPx%7D+@wRCt{2o6kzaKoG{Kl0y~K{{*B}h$5weAc$ul!J|*$UGzB=AHs`A@d2C9_WPZnx&3t@Jb|*WV41qa0*crQhj~aH3tWu7a zm$H$^`hy`Io!?|+;u|u9RZuSGq45`>(KUb#Knp;VdIR8-V1QN1#k$`xEn3Wuq~oSO z1GF+AP7R<6tqefG2A~C?fg2Ea_E!zu05og>T0qO`K;uh&OA`^D9UqR@Rw^{|+{k16 z!H`-`hupqLmrX}ngAC9N*x%hkh9@EoUI8?005Z7y`i=}wM4SO^02`2WKz}eyI^C+nW=wKXs?C3=O+RFTFR} z@X|;daC`qa^H?3SK*=l6*jN?bGimKF7xP3-M}>r$QDVTfmR3;uqPM@pnEND%==a4T z^~<2N0ad$BhH0tL0)aCCEdUK0fEIv;4L}P(lX?S`Rf3{gAnv|lQYbOt^GkeS@k6ui z@BnCUp6KU)fl1e^c0JPg(~GPB{l7rihKgZUgkz&*o(Q-BVTk4oRo@?}Uj^6zv;Z`0 z09pVVHUKREjka}wh>_|oj0000pWc?Ln`LH zon`39*H@*|Jm;#3UvrrVv@K(ZvKVsVi~Wh z4{I>l+5N5l`JVk8Px$l}SWFR9J=WSHW(AKoET;d$2}8pxHx9584x6{Q$qBf7AczCvl=2pqEWV1Y@>~!+Ns?OLNev4#zSf1GNoloP#BqG+ySO zyA%LGWa2zQMbs8_s)MC@aX)t{h$!yOTy?VI&HW&P_w;xcS5?BXO!#43+>^3+U5O{- zQE|Q7ACxf_4@)w5Gq*57#Io}wk+3PMa(DX!sBM5HS@CmdT>K{lzeSmwPIVB+GFu%i zqEpY{I9snRxER~Gza1Hxnr5%^SW(Rny8x@>Ry0%0N+Ol zUqbybhBtGS?Z^ignSd--)z*F(7fUMPTdMd#hN`pD Date: Wed, 6 Aug 2025 16:59:14 +0000 Subject: [PATCH 4/6] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 403c5ddd11..1286c1efd1 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: kosticia - changes: - - message: Added analog of donk-pockets for moth - moth-pockets - type: Add - id: 8320 - time: '2025-04-23T05:36:55.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/34517 - author: AgentSmithRadio changes: - message: New food crates are now available for purchase from the automated trade @@ -3905,3 +3898,10 @@ id: 8832 time: '2025-08-06T16:11:19.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/39362 +- author: Tao7891 + changes: + - message: Craftable cardboard structures and equipment. + type: Add + id: 8833 + time: '2025-08-06T16:58:07.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/37363 From fdf39dbffb8573dfe3014f0e55eb3879142ee203 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Wed, 6 Aug 2025 19:23:05 +0200 Subject: [PATCH 5/6] add scale:multiplyvector toolshed command (#39424) --- .../Toolshed/Commands/Misc/ScaleCommand.cs | 13 +++++++++++++ .../Locale/en-US/commands/toolshed-commands.ftl | 2 ++ 2 files changed, 15 insertions(+) diff --git a/Content.Server/Toolshed/Commands/Misc/ScaleCommand.cs b/Content.Server/Toolshed/Commands/Misc/ScaleCommand.cs index ab8cdf7ed9..67bd21737f 100644 --- a/Content.Server/Toolshed/Commands/Misc/ScaleCommand.cs +++ b/Content.Server/Toolshed/Commands/Misc/ScaleCommand.cs @@ -41,6 +41,19 @@ public sealed class ScaleCommand : ToolshedCommand } } + [CommandImplementation("multiplyvector")] + public IEnumerable Multiply([PipedArgument] IEnumerable input, Vector2 factor) + { + _scaleVisuals ??= GetSys(); + + foreach (var ent in input) + { + var scale = _scaleVisuals.GetSpriteScale(ent) * factor; + _scaleVisuals.SetSpriteScale(ent, scale); + yield return ent; + } + } + [CommandImplementation("multiplywithfixture")] public IEnumerable MultiplyWithFixture([PipedArgument] IEnumerable input, float factor) { diff --git a/Resources/Locale/en-US/commands/toolshed-commands.ftl b/Resources/Locale/en-US/commands/toolshed-commands.ftl index 08732fabac..cc5c03d52b 100644 --- a/Resources/Locale/en-US/commands/toolshed-commands.ftl +++ b/Resources/Locale/en-US/commands/toolshed-commands.ftl @@ -102,5 +102,7 @@ command-description-scale-get = Get an entity's sprite scale as set by ScaleVisualsComponent. Does not include any changes directly made in the SpriteComponent. command-description-scale-multiply = Multiply an entity's sprite size with a certain factor (without changing its fixture). +command-description-scale-multiplyvector = + Multiply an entity's sprite size with a certain 2d vector (without changing its fixture). command-description-scale-multiplywithfixture = Multiply an entity's sprite size with a certain factor (including its fixture). From ffccef2358100a88ad476adee8655a21bea9b6b4 Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 6 Aug 2025 17:24:13 +0000 Subject: [PATCH 6/6] Automatic changelog update --- Resources/Changelog/Admin.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Resources/Changelog/Admin.yml b/Resources/Changelog/Admin.yml index bc7f16288a..fca3c513f7 100644 --- a/Resources/Changelog/Admin.yml +++ b/Resources/Changelog/Admin.yml @@ -1308,5 +1308,12 @@ Entries: id: 159 time: '2025-08-06T15:09:51.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/39421 +- author: slarticodefast + changes: + - message: Added the "scale:multiplyvector" toolshed command. + type: Add + id: 160 + time: '2025-08-06T17:23:06.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/39424 Name: Admin Order: 2