deepfryer 2.0 (#5043)

* was it worth the hype

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* minor stuff

* multi-ingredient recipe support

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* oil transfer

* explosions and fire

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* make the yaml linter happy

* YML recipes

* typo ops

* review

* Apply suggestions from code review

Milon told me to.

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Stop-Signs <stopsign221@gmail.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
This commit is contained in:
Milon 2026-01-06 19:41:29 +01:00 committed by GitHub
parent 4b4c2ef973
commit fb98bfe575
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 1588 additions and 1970 deletions

View File

@ -174,7 +174,13 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
private void GenerateCookTime(FoodRecipePrototype recipe)
{
var msg = new FormattedMessage();
msg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-cook-time", ("time", recipe.CookTime)));
// DeltaV - Deep fryer formatting
var locId = recipe.DeepFried
? "guideboook-microwave-fry-time"
: "guidebook-microwave-cook-time-deltav";
msg.AddMarkupOrThrow(Loc.GetString(locId, ("time", recipe.CookTime)));
// End DV
msg.Pop();
CookTimeLabel.SetMessage(msg);

View File

@ -1,11 +0,0 @@
using Content.Shared.Kitchen.Components;
using Content.Shared.Nyanotrasen.Kitchen.Components;
namespace Content.Client.Kitchen.Components
{
[RegisterComponent]
//Unnecessary item: [ComponentReference(typeof(SharedDeepFriedComponent))]
public sealed partial class DeepFriedComponent : SharedDeepFriedComponent
{
}
}

View File

@ -1,11 +0,0 @@
using Content.Shared.Kitchen.Components;
using Content.Shared.Nyanotrasen.Kitchen.Components;
namespace Content.Client.Kitchen.Components
{
[RegisterComponent]
// Unnecessary line: [ComponentReference(typeof(SharedDeepFryerComponent))]
public sealed partial class DeepFryerComponent : SharedDeepFryerComponent
{
}
}

View File

@ -1,64 +0,0 @@
using Content.Shared.Nyanotrasen.Kitchen.UI;
using Robust.Client.GameObjects;
namespace Content.Client.Nyanotrasen.Kitchen.UI
{
public sealed class DeepFryerBoundUserInterface : BoundUserInterface
{
private DeepFryerWindow? _window;
private NetEntity[] _entities = default!;
public DeepFryerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
base.Open();
_window = new DeepFryerWindow();
_window.OnClose += Close;
_window.ItemList.OnItemSelected += args =>
{
SendMessage(new DeepFryerRemoveItemMessage(_entities[args.ItemIndex]));
};
_window.InsertItem.OnPressed += _ =>
{
SendMessage(new DeepFryerInsertItemMessage());
};
_window.ScoopVat.OnPressed += _ =>
{
SendMessage(new DeepFryerScoopVatMessage());
};
_window.ClearSlag.OnPressed += args =>
{
SendMessage(new DeepFryerClearSlagMessage());
};
_window.RemoveAllItems.OnPressed += _ =>
{
SendMessage(new DeepFryerRemoveAllItemsMessage());
};
_window.OpenCentered();
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window == null)
return;
if (state is not DeepFryerBoundUserInterfaceState cast)
return;
_entities = cast.ContainedEntities;
_window.UpdateState(cast);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_window?.Dispose();
}
}
}

View File

@ -1,67 +0,0 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'deep-fryer-window-title'}"
MinSize="600 400"
>
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
<BoxContainer
Orientation="Vertical"
HorizontalExpand="True"
>
<Label
Text="{Loc 'deep-fryer-label-baskets'}"
Align="Left"/>
<ItemList Name="ItemList"
Access="Public"
VerticalExpand="True"
HorizontalExpand="True"
SelectMode="Button">
</ItemList>
</BoxContainer>
<BoxContainer
Orientation="Vertical"
Margin="8 0"
MinSize="200 0"
>
<Label Text="{Loc 'deep-fryer-label-oil-level'}"/>
<ProgressBar Name="OilLevel"
HorizontalExpand="True"
MinValue="0"
MaxValue="1"
Page="0"
Value="1">
</ProgressBar>
<Label Text="{Loc 'deep-fryer-label-oil-purity'}"/>
<ProgressBar Name="OilPurity"
HorizontalExpand="True"
MinValue="0"
MaxValue="1"
Page="0"
Value="1">
</ProgressBar>
<Button Name="InsertItem"
Access="Public"
TextAlign="Center"
HorizontalExpand="True"
Text="{Loc 'deep-fryer-button-insert-item'}"
ToolTip="{Loc 'deep-fryer-button-insert-item-tooltip'}"/>
<Button Name="ScoopVat"
Access="Public"
TextAlign="Center"
HorizontalExpand="True"
Text="{Loc 'deep-fryer-button-scoop-vat'}"
ToolTip="{Loc 'deep-fryer-button-scoop-vat-tooltip'}"/>
<Button Name="ClearSlag"
Access="Public"
TextAlign="Center"
HorizontalExpand="True"
Text="{Loc 'deep-fryer-button-clear-slag'}"
ToolTip="{Loc 'deep-fryer-button-clear-slag-tooltip'}"/>
<Button Name="RemoveAllItems"
Access="Public"
TextAlign="Center"
HorizontalExpand="True"
Text="{Loc 'deep-fryer-button-remove-all-items'}"
ToolTip="{Loc 'deep-fryer-button-remove-all-items-tooltip'}"/>
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@ -1,72 +0,0 @@
using System.Numerics;
using Content.Shared.Nyanotrasen.Kitchen.UI;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Nyanotrasen.Kitchen.UI
{
[GenerateTypedNameReferences]
[Access(typeof(DeepFryerBoundUserInterface))]
public sealed partial class DeepFryerWindow : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
private static readonly Color WarningColor = Color.FromHsv(new Vector4(0.0f, 1.0f, 0.8f, 1.0f));
public DeepFryerWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
public void UpdateState(DeepFryerBoundUserInterfaceState state)
{
OilLevel.Value = (float) state.OilLevel;
OilPurity.Value = (float) state.OilPurity;
if (state.OilPurity < state.FryingOilThreshold)
{
if (OilPurity.ForegroundStyleBoxOverride == null)
{
OilPurity.ForegroundStyleBoxOverride = new StyleBoxFlat();
var oilPurityStyle = (StyleBoxFlat) OilPurity.ForegroundStyleBoxOverride;
oilPurityStyle.BackgroundColor = WarningColor;
}
}
else
{
OilPurity.ForegroundStyleBoxOverride = null;
}
ItemList.Clear();
foreach (var netEntity in state.ContainedEntities)
{
var entity = _entityManager.GetEntity(netEntity);
if (_entityManager.Deleted(entity))
continue;
// Duplicated from MicrowaveBoundUserInterface.cs: keep an eye on that file for when it changes.
Texture? texture;
if (_entityManager.TryGetComponent(entity, out IconComponent? iconComponent))
{
texture = _entityManager.System<SpriteSystem>().GetIcon(iconComponent);
}
else if (_entityManager.TryGetComponent(entity, out SpriteComponent? spriteComponent))
{
texture = spriteComponent.Icon?.Default;
}
else
{
continue;
}
ItemList.AddItem(_entityManager.GetComponent<MetaDataComponent>(entity).EntityName, texture);
}
}
}
}

View File

@ -1,75 +0,0 @@
using System.Linq;
using Robust.Client.GameObjects;
using static Robust.Client.GameObjects.SpriteComponent;
using Content.Client.Kitchen.Components;
using Content.Shared.Clothing;
using Content.Shared.Hands;
using Content.Shared.Kitchen.Components;
using Content.Shared.Nyanotrasen.Kitchen.Components;
namespace Content.Client.Kitchen.Visualizers;
public sealed class DeepFriedVisualizerSystem : VisualizerSystem<DeepFriedComponent>
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
private readonly static string ShaderName = "Crispy";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DeepFriedComponent, HeldVisualsUpdatedEvent>(OnHeldVisualsUpdated);
SubscribeLocalEvent<DeepFriedComponent, EquipmentVisualsUpdatedEvent>(OnEquipmentVisualsUpdated);
}
protected override void OnAppearanceChange(EntityUid uid, DeepFriedComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!_appearance.TryGetData(uid, DeepFriedVisuals.Fried, out bool isFried, args.Component))
return;
for (var i = 0; i < args.Sprite.AllLayers.Count(); ++i)
args.Sprite.LayerSetShader(i, ShaderName);
}
private void OnHeldVisualsUpdated(EntityUid uid, DeepFriedComponent component, HeldVisualsUpdatedEvent args)
{
if (args.RevealedLayers.Count == 0)
{
return;
}
if (!TryComp(args.User, out SpriteComponent? sprite))
return;
foreach (var key in args.RevealedLayers)
{
if (!sprite.LayerMapTryGet(key, out var index) || sprite[index] is not Layer layer)
continue;
sprite.LayerSetShader(index, ShaderName);
}
}
private void OnEquipmentVisualsUpdated(EntityUid uid, DeepFriedComponent component, EquipmentVisualsUpdatedEvent args)
{
if (args.RevealedLayers.Count == 0)
{
return;
}
if (!TryComp(args.Equipee, out SpriteComponent? sprite))
return;
foreach (var key in args.RevealedLayers)
{
if (!sprite.LayerMapTryGet(key, out var index) || sprite[index] is not Layer layer)
continue;
sprite.LayerSetShader(index, ShaderName);
}
}
}

View File

@ -0,0 +1,5 @@
using Content.Shared._DV.Kitchen.Systems;
namespace Content.Client._DV.Kitchen;
public sealed class DeepFryerSystem : SharedDeepFryerSystem;

View File

@ -1,11 +1,8 @@
using Robust.Client.GameObjects;
using Content.Client.Chemistry.Visualizers;
using Content.Client.Kitchen.Components;
using Content.Shared._DV.Kitchen.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Kitchen.Components;
using Content.Shared.Nyanotrasen.Kitchen.Components;
using Robust.Client.GameObjects;
namespace Content.Client.Kitchen.Visualizers;
namespace Content.Client._DV.Kitchen;
public sealed class DeepFryerVisualizerSystem : VisualizerSystem<DeepFryerComponent>
{
@ -22,4 +19,3 @@ public sealed class DeepFryerVisualizerSystem : VisualizerSystem<DeepFryerCompon
scvComponent.FillBaseName = isBubbling ? "on-" : "off-";
}
}

View File

@ -24,7 +24,7 @@ public sealed class WizdenContentFreeze
var protoMan = server.ProtoMan;
var recipesCount = protoMan.Count<FoodRecipePrototype>();
var recipesLimit = 268; // DeltaV - was 218
var recipesLimit = 269; // DeltaV - was 218
if (recipesCount > recipesLimit)
{

View File

@ -1,24 +0,0 @@
using Content.Shared.Kitchen.Components;
using Content.Shared.Nyanotrasen.Kitchen.Components;
namespace Content.Server.Kitchen.Components
{
[RegisterComponent]
//This line appears to be deprecated. [ComponentReference(typeof(SharedDeepFriedComponent))]
public sealed partial class DeepFriedComponent : SharedDeepFriedComponent
{
/// <summary>
/// What is the item's base price multiplied by?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("priceCoefficient")]
public float PriceCoefficient { get; set; } = 1.0f;
/// <summary>
/// What was the entity's original name before any modification?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("originalName")]
public string? OriginalName { get; set; }
}
}

View File

@ -1,191 +0,0 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Construction.Prototypes;
using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition;
using Content.Shared.Nyanotrasen.Kitchen;
using Content.Shared.Nyanotrasen.Kitchen.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Nyanotrasen.Kitchen.Components;
// TODO: move to shared and get rid of SharedDeepFryerComponent
[RegisterComponent, Access(typeof(SharedDeepfryerSystem))]
public sealed partial class DeepFryerComponent : SharedDeepFryerComponent
{
// There are three levels to how the deep fryer treats entities.
//
// 1. An entity can be rejected by the blacklist and be untouched by
// anything other than heat damage.
//
// 2. An entity can be deep-fried but not turned into an edible. The
// change will be mostly cosmetic. Any entity that does not match
// the blacklist will fall into this category.
//
// 3. An entity can be deep-fried and turned into something edible. The
// change will permit the item to be permanently destroyed by eating
// it.
/// <summary>
/// When will the deep fryer layer on the next stage of crispiness?
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextFryTime;
/// <summary>
/// How much waste needs to be added at the next update interval?
/// </summary>
[DataField]
public FixedPoint2 WasteToAdd = FixedPoint2.Zero;
/// <summary>
/// How often are items in the deep fryer fried?
/// </summary>
[DataField]
public TimeSpan FryInterval = TimeSpan.FromSeconds(5);
/// <summary>
/// What entities cannot be deep-fried no matter what?
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
/// <summary>
/// What entities can be deep-fried into being edible?
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// What are over-cooked and burned entities turned into?
/// </summary>
/// <remarks>
/// To prevent unwanted destruction of items, only food can be turned
/// into this.
/// </remarks>
[DataField]
public EntProtoId? CharredPrototype;
/// <summary>
/// What reagents are considered valid cooking oils?
/// </summary>
[DataField]
public HashSet<ProtoId<ReagentPrototype>> FryingOils = new();
/// <summary>
/// What reagents are added to tasty deep-fried food?
/// </summary>
[DataField]
public List<ReagentQuantity> GoodReagents = new();
/// <summary>
/// What reagents are added to terrible deep-fried food?
/// </summary>
[DataField]
public List<ReagentQuantity> BadReagents = new();
/// <summary>
/// What reagents replace every 1 unit of oil spent on frying?
/// </summary>
[DataField]
public List<ReagentQuantity> WasteReagents = new();
/// <summary>
/// What flavors go well with deep frying?
/// </summary>
[DataField]
public HashSet<ProtoId<FlavorPrototype>> GoodFlavors = new();
/// <summary>
/// What flavors don't go well with deep frying?
/// </summary>
[DataField]
public HashSet<ProtoId<FlavorPrototype>> BadFlavors = new();
/// <summary>
/// How much is the price coefficiency of a food changed for each good flavor?
/// </summary>
[DataField]
public float GoodFlavorPriceBonus = 0.2f;
/// <summary>
/// How much is the price coefficiency of a food changed for each bad flavor?
/// </summary>
[DataField]
public float BadFlavorPriceMalus = -0.3f;
/// <summary>
/// What is the name of the solution container for the fryer's oil?
/// </summary>
[DataField]
public string SolutionName = "vat_oil";
// TODO: Entity<SolutionComponent>
public Solution Solution = default!;
/// <summary>
/// What is the name of the entity container for items inside the deep fryer?
/// </summary>
[DataField("storage")]
public string StorageName = "vat_entities";
public BaseContainer Storage = default!;
/// <summary>
/// How much solution should be imparted based on an item's size?
/// </summary>
[DataField]
public FixedPoint2 SolutionSizeCoefficient = 1f;
/// <summary>
/// What's the maximum amount of solution that should ever be imparted?
/// </summary>
[DataField]
public FixedPoint2 SolutionSplitMax = 10f;
/// <summary>
/// What percent of the fryer's solution has to be oil in order for it to fry?
/// </summary>
/// <remarks>
/// The chef will have to clean it out occasionally, and if too much
/// non-oil reagents are added, the vat will have to be drained.
/// </remarks>
[DataField]
public FixedPoint2 FryingOilThreshold = 0.5f;
/// <summary>
/// What is the bare minimum number of oil units to prevent the fryer
/// from unsafe operation?
/// </summary>
[DataField]
public FixedPoint2 SafeOilVolume = 10f;
/// <summary>
/// What is the temperature of the vat when the deep fryer is powered?
/// </summary>
[DataField]
public float PoweredTemperature = 550.0f;
/// <summary>
/// How many entities can this deep fryer hold?
/// </summary>
[DataField]
public int StorageMaxEntities = 4;
/// <summary>
/// What sound is played when an item is inserted into hot oil?
/// </summary>
[DataField]
public SoundSpecifier SoundInsertItem = new SoundPathSpecifier("/Audio/Nyanotrasen/Machines/deepfryer_basket_add_item.ogg");
/// <summary>
/// What sound is played when an item is removed?
/// </summary>
[DataField]
public SoundSpecifier SoundRemoveItem = new SoundPathSpecifier("/Audio/Nyanotrasen/Machines/deepfryer_basket_remove_item.ogg");
}

View File

@ -1,5 +0,0 @@
namespace Content.Server.Kitchen.Components
{
[RegisterComponent]
public sealed partial class ProfessionalChefComponent : Component {}
}

View File

@ -1,186 +0,0 @@
using System.Text;
using Content.Server.Atmos.Components;
using Content.Server.Body.Components;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Kitchen.Components;
using Content.Server.Nutrition.Components;
using Content.Server.Nyanotrasen.Kitchen.Components;
using Content.Shared.Body.Components;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Buckle.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs.Components;
using Content.Shared.NPC;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nyanotrasen.Kitchen.Components;
using Content.Shared.Paper;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Nyanotrasen.Kitchen.EntitySystems;
public sealed partial class DeepFryerSystem
{
private HashSet<ProtoId<FlavorPrototype>> _badFlavors = new();
private HashSet<ProtoId<FlavorPrototype>> _goodFlavors = new();
/// <summary>
/// Make an item look deep-fried.
/// </summary>
private void MakeCrispy(EntityUid item)
{
EnsureComp<AppearanceComponent>(item);
EnsureComp<DeepFriedComponent>(item);
_appearanceSystem.SetData(item, DeepFriedVisuals.Fried, true);
}
/// <summary>
/// Turn a dead mob into food.
/// </summary>
/// <remarks>
/// This is meant to be an irreversible process, similar to gibbing.
/// </remarks>
public bool TryMakeMobIntoFood(EntityUid mob, MobStateComponent mobStateComponent, bool force = false)
{
// Don't do anything to mobs until they're dead.
if (force || _mobStateSystem.IsDead(mob, mobStateComponent))
{
RemComp<ActiveNPCComponent>(mob);
RemComp<AtmosExposedComponent>(mob);
RemComp<BarotraumaComponent>(mob);
RemComp<BuckleComponent>(mob);
RemComp<GhostTakeoverAvailableComponent>(mob);
RemComp<PerishableComponent>(mob);
RemComp<RespiratorComponent>(mob);
RemComp<RottingComponent>(mob);
// Ensure it's Food here, so it passes the whitelist.
var mobFoodComponent = EnsureComp<EdibleComponent>(mob);
_solutionContainerSystem.EnsureSolution(mob, mobFoodComponent.Solution, out var alreadyHadFood);
if (!_solutionContainerSystem.TryGetSolution(mob, mobFoodComponent.Solution, out var mobFoodSolution))
return false;
// This line here is mainly for mice, because they have a food
// component that mirrors how much blood they have, which is
// used for the reagent grinder.
if (alreadyHadFood)
_solutionContainerSystem.RemoveAllSolution(mobFoodSolution.Value);
if (TryComp<BloodstreamComponent>(mob, out var bloodstreamComponent) && bloodstreamComponent.ChemicalSolution != null)
{
// Fry off any blood into protein.
var bloodSolution = bloodstreamComponent.BloodSolution;
var solPresent = bloodSolution!.Value.Comp.Solution.Volume;
_solutionContainerSystem.RemoveReagent(bloodSolution.Value, "Blood", FixedPoint2.MaxValue);
var bloodRemoved = solPresent - bloodSolution.Value.Comp.Solution.Volume;
var proteinQuantity = bloodRemoved * BloodToProteinRatio;
mobFoodSolution.Value.Comp.Solution.MaxVolume += proteinQuantity;
_solutionContainerSystem.TryAddReagent(mobFoodSolution.Value, "Protein", proteinQuantity);
// This is a heuristic. If you had blood, you might just taste meaty.
if (bloodRemoved > FixedPoint2.Zero)
EnsureComp<FlavorProfileComponent>(mob).Flavors.Add(MobFlavorMeat);
// Bring in whatever chemicals they had in them too.
mobFoodSolution.Value.Comp.Solution.MaxVolume +=
bloodstreamComponent.ChemicalSolution.Value.Comp.Solution.Volume;
_solutionContainerSystem.AddSolution(mobFoodSolution.Value,
bloodstreamComponent.ChemicalSolution.Value.Comp.Solution);
}
return true;
}
return false;
}
/// <summary>
/// Make an item actually edible.
/// </summary>
private void MakeEdible(EntityUid uid, DeepFryerComponent component, EntityUid item, FixedPoint2 solutionQuantity)
{
if (!TryComp<DeepFriedComponent>(item, out var deepFriedComponent))
{
_sawmill.Error($"{ToPrettyString(item)} is missing the DeepFriedComponent before being made Edible.");
return;
}
// Remove any components that wouldn't make sense anymore.
RemComp<ButcherableComponent>(item);
if (TryComp<PaperComponent>(item, out var paperComponent))
{
var stringBuilder = new StringBuilder();
for (var i = 0; i < paperComponent.Content.Length; ++i)
{
var uchar = paperComponent.Content.Substring(i, 1);
if (uchar == "\n" || _random.Prob(0.4f))
stringBuilder.Append(uchar);
else
stringBuilder.Append("x");
}
paperComponent.Content = stringBuilder.ToString();
}
var foodComponent = EnsureComp<EdibleComponent>(item);
var extraSolution = new Solution();
if (TryComp(item, out FlavorProfileComponent? flavorProfileComponent))
{
_goodFlavors.Clear();
_goodFlavors.IntersectWith(component.GoodFlavors);
_badFlavors.Clear();
_badFlavors.IntersectWith(component.BadFlavors);
deepFriedComponent.PriceCoefficient = Math.Max(0.01f,
1.0f
+ _goodFlavors.Count * component.GoodFlavorPriceBonus
- _badFlavors.Count * component.BadFlavorPriceMalus);
if (_goodFlavors.Count > 0)
{
foreach (var reagent in component.GoodReagents)
{
extraSolution.AddReagent(reagent.Reagent.ToString(), reagent.Quantity * _goodFlavors.Count);
// Mask the taste of "medicine."
flavorProfileComponent.IgnoreReagents.Add(reagent.Reagent.ToString());
}
}
if (_badFlavors.Count > 0)
{
foreach (var reagent in component.BadReagents)
{
extraSolution.AddReagent(reagent.Reagent.ToString(), reagent.Quantity * _badFlavors.Count);
}
}
}
else
{
flavorProfileComponent = EnsureComp<FlavorProfileComponent>(item);
// TODO: Default flavor?
}
// Make sure there's enough room for the fryer solution.
var foodSolution = _solutionContainerSystem.EnsureSolution(item, foodComponent.Solution);
if (!_solutionContainerSystem.TryGetSolution(item, foodSolution.Name, out var foodContainer))
return;
// The solution quantity is used to give the fried food an extra
// buffer too, to support injectables or condiments.
foodSolution.MaxVolume = 2 * solutionQuantity + foodSolution.Volume + extraSolution.Volume;
_solutionContainerSystem.AddSolution(foodContainer.Value,
component.Solution.SplitSolution(solutionQuantity));
_solutionContainerSystem.AddSolution(foodContainer.Value, extraSolution);
_solutionContainerSystem.UpdateChemicals(foodContainer.Value);
}
}

View File

@ -1,91 +0,0 @@
using Content.Server.Kitchen.Components;
using Content.Server.Nyanotrasen.Kitchen.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Item;
using Content.Shared.Nyanotrasen.Kitchen.UI;
using Content.Shared.Storage;
using Content.Shared.Tools.Components;
namespace Content.Server.Nyanotrasen.Kitchen.EntitySystems;
public sealed partial class DeepFryerSystem
{
public bool CanInsertItem(EntityUid uid, DeepFryerComponent component, EntityUid item)
{
// Keep this consistent with the checks in TryInsertItem.
return HasComp<ItemComponent>(item) &&
!HasComp<StorageComponent>(item) &&
component.Storage.ContainedEntities.Count < component.StorageMaxEntities;
}
private bool TryInsertItem(EntityUid uid, DeepFryerComponent component, EntityUid user, EntityUid item)
{
if (!HasComp<ItemComponent>(item))
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-interact-using-not-item"),
uid,
user);
return false;
}
if (HasComp<StorageComponent>(item))
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-storage-no-fit",
("item", item)),
uid,
user);
return false;
}
if (component.Storage.ContainedEntities.Count >= component.StorageMaxEntities)
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-storage-full"),
uid,
user);
return false;
}
if (!_handsSystem.TryDropIntoContainer(user, item, component.Storage))
return false;
AfterInsert(uid, component, item);
_adminLogManager.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(user)} put {ToPrettyString(item)} inside {ToPrettyString(uid)}.");
return true;
}
private void OnInteractUsing(EntityUid uid, DeepFryerComponent component, InteractUsingEvent args)
{
if (args.Handled)
return;
// By default, allow entities with SolutionTransfer or Tool
// components to perform their usual actions. Inserting them (if
// the chef really wants to) will be supported through the UI.
if (HasComp<SolutionTransferComponent>(args.Used) ||
HasComp<ToolComponent>(args.Used))
return;
if (TryInsertItem(uid, component, args.User, args.Used))
args.Handled = true;
}
private void OnInsertItem(EntityUid uid, DeepFryerComponent component, DeepFryerInsertItemMessage args)
{
var user = args.Actor;
if (!TryComp<HandsComponent>(user, out var handsComponent) ||
_handsSystem.GetActiveItem((user, handsComponent)) is not { } item)
return;
TryInsertItem(uid, component, user, item);
}
}

View File

@ -1,110 +0,0 @@
using System.Linq;
using Content.Server.Kitchen.Components;
using Content.Server.Nyanotrasen.Kitchen.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint;
using Content.Shared.Popups;
using Robust.Shared.Player;
namespace Content.Server.Nyanotrasen.Kitchen.EntitySystems;
public sealed partial class DeepFryerSystem
{
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var component in EntityManager.EntityQuery<DeepFryerComponent>())
{
var uid = component.Owner;
if (_gameTimingSystem.CurTime < component.NextFryTime ||
!_powerReceiverSystem.IsPowered(uid))
{
continue;
}
UpdateNextFryTime(uid, component);
if (!_solutionContainerSystem.TryGetSolution(uid, component.Solution.Name, out var solution))
continue;
// Heat the vat solution and contained entities.
_solutionContainerSystem.SetTemperature(solution.Value, component.PoweredTemperature);
foreach (var item in component.Storage.ContainedEntities)
CookItem(uid, component, item);
// Do something bad if there's enough heat but not enough oil.
var oilVolume = GetOilVolume(uid, component);
if (oilVolume < component.SafeOilVolume)
{
foreach (var item in component.Storage.ContainedEntities.ToArray())
BurnItem(uid, component, item);
if (oilVolume > FixedPoint2.Zero)
{
component.Solution.RemoveAllSolution();
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-oil-volume-low",
("deepFryer", uid)),
uid,
PopupType.SmallCaution);
continue;
}
}
// We only alert the chef that there's a problem with oil purity
// if there's anything to cook beyond this point.
if (!component.Storage.ContainedEntities.Any())
{
continue;
}
if (GetOilPurity(uid, component) < component.FryingOilThreshold)
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-oil-purity-low",
("deepFryer", uid)),
uid,
Filter.Pvs(uid, PvsWarningRange),
true);
continue;
}
foreach (var item in component.Storage.ContainedEntities.ToArray())
DeepFry(uid, component, item);
// After the round of frying, replace the spent oil with a
// waste product.
if (component.WasteToAdd > FixedPoint2.Zero)
{
foreach (var reagent in component.WasteReagents)
component.Solution.AddReagent(reagent.Reagent.ToString(), reagent.Quantity * component.WasteToAdd);
component.WasteToAdd = FixedPoint2.Zero;
_solutionContainerSystem.UpdateChemicals(solution.Value, true);
}
UpdateUserInterface(uid, component);
}
}
private void UpdateAmbientSound(EntityUid uid, DeepFryerComponent component)
{
_ambientSoundSystem.SetAmbience(uid, HasBubblingOil(uid, component));
}
private void UpdateNextFryTime(EntityUid uid, DeepFryerComponent component)
{
component.NextFryTime = _gameTimingSystem.CurTime + component.FryInterval;
}
}

View File

@ -1,732 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Audio;
using Content.Server.Cargo.Systems;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Construction;
using Content.Server.DoAfter;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Kitchen.Components;
using Content.Server.Nutrition;
using Content.Server.Nutrition.Components;
using Content.Server.Nyanotrasen.Kitchen.Components;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Storage.EntitySystems;
using Content.Server.Temperature.Systems;
using Content.Server.UserInterface;
using Content.Shared.Cargo;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Construction;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Database;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.EntityEffects;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Item;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Events;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nyanotrasen.Kitchen;
using Content.Shared.Nyanotrasen.Kitchen.Components;
using Content.Shared.Nyanotrasen.Kitchen.UI;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Temperature.Components;
using Content.Shared.Throwing;
using Content.Shared.UserInterface;
using Content.Shared.Whitelist;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Nyanotrasen.Kitchen.EntitySystems;
public sealed partial class DeepFryerSystem : SharedDeepfryerSystem
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
[Dependency] private readonly IGameTiming _gameTimingSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly SolutionTransferSystem _solutionTransferSystem = default!;
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly TemperatureSystem _temperature = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly AmbientSoundSystem _ambientSoundSystem = default!;
[Dependency] private readonly MetaDataSystem _metaDataSystem = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
private static readonly string CookingDamageType = "Heat";
private static readonly float CookingDamageAmount = 10.0f;
private static readonly float PvsWarningRange = 0.5f;
private static readonly float ThrowMissChance = 0.25f;
private static readonly int MaximumCrispiness = 2;
private static readonly float BloodToProteinRatio = 0.1f;
private static readonly string MobFlavorMeat = "meaty";
private static readonly AudioParams
AudioParamsInsertRemove = new(0.5f, 1f, 5f, 1.5f, 1f, false, 0f, 0.2f);
private ISawmill _sawmill = default!;
public override void Initialize()
{
base.Initialize();
_sawmill = Logger.GetSawmill("deepfryer");
SubscribeLocalEvent<DeepFryerComponent, ComponentInit>(OnInitDeepFryer);
SubscribeLocalEvent<DeepFryerComponent, PowerChangedEvent>(OnPowerChange);
SubscribeLocalEvent<DeepFryerComponent, MachineDeconstructedEvent>(OnDeconstruct);
SubscribeLocalEvent<DeepFryerComponent, DestructionEventArgs>(OnDestruction);
SubscribeLocalEvent<DeepFryerComponent, ThrowHitByEvent>(OnThrowHitBy);
SubscribeLocalEvent<DeepFryerComponent, SolutionChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<DeepFryerComponent, ContainerRelayMovementEntityEvent>(OnRelayMovement);
SubscribeLocalEvent<DeepFryerComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<DeepFryerComponent, BeforeActivatableUIOpenEvent>(OnBeforeActivatableUIOpen);
SubscribeLocalEvent<DeepFryerComponent, DeepFryerRemoveItemMessage>(OnRemoveItem);
SubscribeLocalEvent<DeepFryerComponent, DeepFryerInsertItemMessage>(OnInsertItem);
SubscribeLocalEvent<DeepFryerComponent, DeepFryerScoopVatMessage>(OnScoopVat);
SubscribeLocalEvent<DeepFryerComponent, DeepFryerClearSlagMessage>(OnClearSlagStart);
SubscribeLocalEvent<DeepFryerComponent, DeepFryerRemoveAllItemsMessage>(OnRemoveAllItems);
SubscribeLocalEvent<DeepFryerComponent, ClearSlagDoAfterEvent>(OnClearSlag);
SubscribeLocalEvent<DeepFriedComponent, ComponentInit>(OnInitDeepFried);
SubscribeLocalEvent<DeepFriedComponent, ExaminedEvent>(OnExamineFried);
SubscribeLocalEvent<DeepFriedComponent, PriceCalculationEvent>(OnPriceCalculation);
SubscribeLocalEvent<DeepFriedComponent, FoodSlicedEvent>(OnSliceDeepFried);
}
private void UpdateUserInterface(EntityUid uid, DeepFryerComponent component)
{
var state = new DeepFryerBoundUserInterfaceState(
GetOilLevel(uid, component),
GetOilPurity(uid, component),
component.FryingOilThreshold,
EntityManager.GetNetEntityArray(component.Storage.ContainedEntities.ToArray()));
_uiSystem.SetUiState(uid, DeepFryerUiKey.Key, state);
}
/// <summary>
/// Does the deep fryer have hot oil?
/// </summary>
/// <remarks>
/// This is mainly for audio.
/// </remarks>
private bool HasBubblingOil(EntityUid uid, DeepFryerComponent component)
{
return _powerReceiverSystem.IsPowered(uid) && GetOilVolume(uid, component) > FixedPoint2.Zero;
}
/// <summary>
/// Returns how much total oil is in the vat.
/// </summary>
public FixedPoint2 GetOilVolume(EntityUid uid, DeepFryerComponent component)
{
var oilVolume = FixedPoint2.Zero;
foreach (var reagent in component.Solution)
{
if (component.FryingOils.Contains(reagent.Reagent.ToString()))
oilVolume += reagent.Quantity;
}
return oilVolume;
}
/// <summary>
/// Returns how much total waste is in the vat.
/// </summary>
public FixedPoint2 GetWasteVolume(EntityUid uid, DeepFryerComponent component)
{
var wasteVolume = FixedPoint2.Zero;
foreach (var reagent in component.WasteReagents)
{
wasteVolume += component.Solution.GetReagentQuantity(reagent.Reagent);
}
return wasteVolume;
}
/// <summary>
/// Returns a percentage of how much of the total solution is usable oil.
/// </summary>
public FixedPoint2 GetOilPurity(EntityUid uid, DeepFryerComponent component)
{
if (component.Solution.Volume == 0) return 0;
return GetOilVolume(uid, component) / component.Solution.Volume;
}
/// <summary>
/// Returns a percentage of how much of the total volume is usable oil.
/// </summary>
public FixedPoint2 GetOilLevel(EntityUid uid, DeepFryerComponent component)
{
return GetOilVolume(uid, component) / component.Solution.MaxVolume;
}
/// <summary>
/// This takes care of anything that would happen to an item with or
/// without enough oil.
/// </summary>
private void CookItem(EntityUid uid, DeepFryerComponent component, EntityUid item)
{
if (TryComp<TemperatureComponent>(item, out var tempComp))
{
// Push the temperature towards what it should be but no higher.
var delta = (component.PoweredTemperature - tempComp.CurrentTemperature) * _temperature.GetHeatCapacity(item, tempComp);
if (delta > 0f)
_temperature.ChangeHeat(item, delta, false, tempComp);
}
if (TryComp<SolutionContainerManagerComponent>(item, out var solutions) && solutions.Solutions != null)
{
foreach (var (_, solution) in solutions.Solutions)
{
if(_solutionContainerSystem.TryGetSolution(item, solution.Name, out var solutionRef))
_solutionContainerSystem.SetTemperature(solutionRef!.Value, component.PoweredTemperature);
}
}
// Damage non-food items and mobs.
if ((!HasComp<EdibleComponent>(item) || HasComp<MobStateComponent>(item)) &&
TryComp<DamageableComponent>(item, out var damageableComponent))
{
var damage = new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>(CookingDamageType),
CookingDamageAmount);
var result = _damageableSystem.TryChangeDamage(item, damage, origin: uid);
if (result)
{
// TODO: Smoke, waste, sound, or some indication.
}
}
}
/// <summary>
/// Destroy a food item and replace it with a charred mess.
/// </summary>
private void BurnItem(EntityUid uid, DeepFryerComponent component, EntityUid item)
{
if (HasComp<EdibleComponent>(item) &&
!HasComp<MobStateComponent>(item) &&
MetaData(item).EntityPrototype?.ID != component.CharredPrototype)
{
var charred = Spawn(component.CharredPrototype, Transform(uid).Coordinates);
_containerSystem.Insert(charred, component.Storage);
Del(item);
}
}
private void UpdateDeepFriedName(EntityUid uid, DeepFriedComponent component)
{
if (component.OriginalName == null)
return;
switch (component.Crispiness)
{
case 0:
// Already handled at OnInitDeepFried.
break;
case 1:
_metaDataSystem.SetEntityName(uid, Loc.GetString("deep-fried-crispy-item",
("entity", component.OriginalName)));
break;
default:
_metaDataSystem.SetEntityName(uid, Loc.GetString("deep-fried-burned-item",
("entity", component.OriginalName)));
break;
}
}
/// <summary>
/// Try to deep fry a single item, which can
/// - be cancelled by other systems, or
/// - fail due to the blacklist, or
/// - give it a crispy shader, and possibly also
/// - turn it into food.
/// </summary>
private void DeepFry(EntityUid uid, DeepFryerComponent component, EntityUid item)
{
if (MetaData(item).EntityPrototype?.ID == component.CharredPrototype)
return;
// This item has already been deep-fried, and now it's progressing
// into another stage.
if (TryComp<DeepFriedComponent>(item, out var deepFriedComponent))
{
// TODO: Smoke, waste, sound, or some indication.
deepFriedComponent.Crispiness += 1;
if (deepFriedComponent.Crispiness > MaximumCrispiness)
{
BurnItem(uid, component, item);
return;
}
UpdateDeepFriedName(item, deepFriedComponent);
return;
}
// Allow entity systems to conditionally forbid an attempt at deep-frying.
var attemptEvent = new DeepFryAttemptEvent(uid);
RaiseLocalEvent(item, attemptEvent);
if (attemptEvent.Cancelled)
return;
// The attempt event is allowed to go first before the blacklist check,
// just in case the attempt is relevant to any system in the future.
//
// The blacklist overrides all.
if (component.Blacklist != null && _whitelistSystem.IsWhitelistPass(component.Blacklist, item))
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-blacklist-item-failed",
("item", item), ("deepFryer", uid)),
uid,
Filter.Pvs(uid, PvsWarningRange),
true);
return;
}
var beingEvent = new BeingDeepFriedEvent(uid, item);
RaiseLocalEvent(item, beingEvent);
// It's important to check for the MobStateComponent so we know
// it's actually a mob, because functions like
// MobStateSystem.IsAlive will return false if the entity lacks the
// component.
if (TryComp<MobStateComponent>(item, out var mobStateComponent))
{
if (!TryMakeMobIntoFood(item, mobStateComponent))
return;
}
MakeCrispy(item);
var itemComponent = Comp<ItemComponent>(item);
// Determine how much solution to spend on this item.
var solutionQuantity = FixedPoint2.Min(
component.Solution.Volume,
itemComponent.Size.Id switch
{
"Tiny" => 1,
"Small" => 5,
"Medium" => 10,
"Large" => 15,
"Huge" => 30,
"Ginormous" => 50,
_ => 10
} * component.SolutionSizeCoefficient);
if (component.Whitelist != null && _whitelistSystem.IsWhitelistPass(component.Whitelist, item) ||
beingEvent.TurnIntoFood)
MakeEdible(uid, component, item, solutionQuantity);
else
component.Solution.RemoveSolution(solutionQuantity);
component.WasteToAdd += solutionQuantity;
}
private void OnInitDeepFryer(EntityUid uid, DeepFryerComponent component, ComponentInit args)
{
component.Storage =
_containerSystem.EnsureContainer<Container>(uid, component.StorageName, out var containerExisted);
if (!containerExisted)
_sawmill.Warning(
$"{ToPrettyString(uid)} did not have a {component.StorageName} container. It has been created.");
component.Solution =
_solutionContainerSystem.EnsureSolution(uid, component.SolutionName, out var solutionExisted);
if (!solutionExisted)
_sawmill.Warning(
$"{ToPrettyString(uid)} did not have a {component.SolutionName} solution container. It has been created.");
}
/// <summary>
/// Make sure the UI and interval tracker are updated anytime something
/// is inserted into one of the baskets.
/// </summary>
/// <remarks>
/// This is used instead of EntInsertedIntoContainerMessage so charred
/// items can be inserted into the deep fryer without triggering this
/// event.
/// </remarks>
private void AfterInsert(EntityUid uid, DeepFryerComponent component, EntityUid item)
{
if (HasBubblingOil(uid, component))
_audioSystem.PlayPvs(component.SoundInsertItem, uid, AudioParamsInsertRemove);
UpdateNextFryTime(uid, component);
UpdateUserInterface(uid, component);
}
private void OnPowerChange(EntityUid uid, DeepFryerComponent component, ref PowerChangedEvent args)
{
_appearanceSystem.SetData(uid, DeepFryerVisuals.Bubbling, args.Powered);
UpdateNextFryTime(uid, component);
UpdateAmbientSound(uid, component);
}
private void OnDeconstruct(EntityUid uid, DeepFryerComponent component, MachineDeconstructedEvent args)
{
// The EmptyOnMachineDeconstruct component handles the entity container for us.
_puddleSystem.TrySpillAt(uid, component.Solution, out var _);
}
private void OnDestruction(EntityUid uid, DeepFryerComponent component, DestructionEventArgs args)
{
_containerSystem.EmptyContainer(component.Storage, true);
}
/// <summary>
/// Allow thrown items to land in a basket.
/// </summary>
private void OnThrowHitBy(EntityUid uid, DeepFryerComponent component, ThrowHitByEvent args)
{
// Chefs never miss this. :)
var missChance = HasComp<ProfessionalChefComponent>(args.Component.Thrower) ? 0f : ThrowMissChance;
if (!CanInsertItem(uid, component, args.Thrown) ||
_random.Prob(missChance) ||
!_containerSystem.Insert(args.Thrown, component.Storage))
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-thrown-missed"),
uid);
if (args.Component.Thrower != null)
{
_adminLogManager.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(args.Component.Thrower.Value)} threw {ToPrettyString(args.Thrown)} at {ToPrettyString(uid)}, and it missed.");
}
return;
}
if (GetOilVolume(uid, component) < component.SafeOilVolume)
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-thrown-hit-oil-low"),
uid);
}
else
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-thrown-hit-oil"),
uid);
}
if (args.Component.Thrower != null)
{
_adminLogManager.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(args.Component.Thrower.Value)} threw {ToPrettyString(args.Thrown)} at {ToPrettyString(uid)}, and it landed inside.");
}
AfterInsert(uid, component, args.Thrown);
}
private void OnSolutionChange(EntityUid uid, DeepFryerComponent component, SolutionChangedEvent args)
{
UpdateUserInterface(uid, component);
UpdateAmbientSound(uid, component);
}
private void OnRelayMovement(EntityUid uid, DeepFryerComponent component,
ref ContainerRelayMovementEntityEvent args)
{
if (!_containerSystem.Remove(args.Entity, component.Storage, destination: Transform(uid).Coordinates))
return;
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-entity-escape",
("victim", Identity.Entity(args.Entity, EntityManager)),
("deepFryer", uid)),
uid,
PopupType.SmallCaution);
}
private void OnBeforeActivatableUIOpen(EntityUid uid, DeepFryerComponent component,
BeforeActivatableUIOpenEvent args)
{
UpdateUserInterface(uid, component);
}
private void OnRemoveItem(EntityUid uid, DeepFryerComponent component, DeepFryerRemoveItemMessage args)
{
var removedItem = EntityManager.GetEntity(args.Item);
if (removedItem.Valid)
{
//JJ Comment - This line should be unnecessary. Some issue is keeping the UI from updating when converting straight to a Burned Mess while the UI is still open. To replicate, put a Raw Meat in the fryer with no oil in it. Wait until it sputters with no effect. It should transform to Burned Mess, but doesn't.
if (!_containerSystem.Remove(removedItem, component.Storage))
return;
var user = args.Actor;
_handsSystem.TryPickupAnyHand(user, removedItem);
_adminLogManager.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(user)} took {ToPrettyString(args.Item)} out of {ToPrettyString(uid)}.");
_audioSystem.PlayPvs(component.SoundRemoveItem, uid, AudioParamsInsertRemove);
UpdateUserInterface(component.Owner, component);
}
}
/// <summary>
/// This is a helper function for ScoopVat and ClearSlag.
/// </summary>
private bool TryGetActiveHandSolutionContainer(
EntityUid fryer,
EntityUid user,
[NotNullWhen(true)] out EntityUid? heldItem,
[NotNullWhen(true)] out Entity<SolutionComponent>? solution,
out FixedPoint2 transferAmount)
{
heldItem = null;
solution = null;
transferAmount = FixedPoint2.Zero;
if (!TryComp<HandsComponent>(user, out var handsComponent))
return false;
heldItem = _handsSystem.GetActiveItem(user);
if (heldItem == null ||
!TryComp<SolutionTransferComponent>(heldItem, out var solutionTransferComponent) ||
!_solutionContainerSystem.TryGetRefillableSolution(heldItem.Value, out var solEnt, out var _) ||
!solutionTransferComponent.CanReceive)
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-need-liquid-container-in-hand"),
fryer,
user);
return false;
}
solution = solEnt;
transferAmount = solutionTransferComponent.TransferAmount;
return true;
}
private void OnScoopVat(EntityUid uid, DeepFryerComponent component, DeepFryerScoopVatMessage args)
{
var user = args.Actor;
if (user == null ||
!TryGetActiveHandSolutionContainer(uid, user, out var heldItem, out var heldSolution,
out var transferAmount))
return;
if (!_solutionContainerSystem.TryGetSolution(component.Owner, component.Solution.Name, out var solution))
return;
_solutionTransferSystem.Transfer(new SolutionTransferData(user,
uid,
solution.Value,
heldItem.Value,
heldSolution.Value,
transferAmount));
// UI update is not necessary here, because the solution change event handles it.
}
private void OnClearSlagStart(EntityUid uid, DeepFryerComponent component, DeepFryerClearSlagMessage args)
{
var user = args.Actor;
if (!TryGetActiveHandSolutionContainer(uid, user, out var heldItem, out var heldSolution,
out var transferAmount))
return;
var wasteVolume = GetWasteVolume(uid, component);
if (wasteVolume == FixedPoint2.Zero)
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-oil-no-slag"),
uid,
user);
return;
}
var delay = TimeSpan.FromSeconds(Math.Clamp((float) wasteVolume * 0.1f, 1f, 5f));
var ev = new ClearSlagDoAfterEvent(heldSolution.Value.Comp.Solution, transferAmount);
//JJ Comment - not sure I have DoAfterArgs configured correctly.
var doAfterArgs = new DoAfterArgs(EntityManager, user, delay, ev, uid, uid, heldItem)
{
BreakOnDamage = true,
BreakOnMove = true,
MovementThreshold = 0.25f,
NeedHand = true
};
_doAfterSystem.TryStartDoAfter(doAfterArgs);
}
[Obsolete("Obsolete")]
private void OnRemoveAllItems(EntityUid uid, DeepFryerComponent component, DeepFryerRemoveAllItemsMessage args)
{
if (component.Storage.ContainedEntities.Count == 0)
return;
_containerSystem.EmptyContainer(component.Storage);
var user = args.Actor;
_adminLogManager.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(user)} removed all items from {ToPrettyString(uid)}.");
_audioSystem.PlayPvs(component.SoundRemoveItem, uid, AudioParamsInsertRemove);
UpdateUserInterface(component.Owner, component);
}
private void OnClearSlag(EntityUid uid, DeepFryerComponent component, ClearSlagDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Used == null)
return;
FixedPoint2 reagentCount = component.WasteReagents.Count();
var removingSolution = new Solution();
foreach (var reagent in component.WasteReagents)
{
var removed = component.Solution.RemoveReagent(reagent.Reagent.ToString(), args.Amount / reagentCount);
removingSolution.AddReagent(reagent.Reagent.ToString(), removed);
}
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution))
return;
if (!_solutionContainerSystem.TryGetSolution(args.Used!.Value, args.Solution.Name, out var targetSolution))
return;
_solutionContainerSystem.UpdateChemicals(solution.Value);
_solutionContainerSystem.TryMixAndOverflow(targetSolution.Value, removingSolution,
args.Solution.MaxVolume, out var _);
}
private void OnInitDeepFried(EntityUid uid, DeepFriedComponent component, ComponentInit args)
{
var meta = MetaData(uid);
component.OriginalName = meta.EntityName;
_metaDataSystem.SetEntityName(uid, Loc.GetString("deep-fried-crispy-item", ("entity", meta.EntityName)));
}
private void OnExamineFried(EntityUid uid, DeepFriedComponent component, ExaminedEvent args)
{
switch (component.Crispiness)
{
case 0:
args.PushMarkup(Loc.GetString("deep-fried-crispy-item-examine"));
break;
case 1:
args.PushMarkup(Loc.GetString("deep-fried-fried-item-examine"));
break;
default:
args.PushMarkup(Loc.GetString("deep-fried-burned-item-examine"));
break;
}
}
private void OnPriceCalculation(EntityUid uid, DeepFriedComponent component, ref PriceCalculationEvent args)
{
args.Price *= component.PriceCoefficient;
}
private void OnSliceDeepFried(EntityUid uid, DeepFriedComponent component, FoodSlicedEvent args)
{
MakeCrispy(args.Slice);
// Copy relevant values to the slice.
var sourceDeepFriedComponent = Comp<DeepFriedComponent>(args.Food);
var sliceDeepFriedComponent = Comp<DeepFriedComponent>(args.Slice);
sliceDeepFriedComponent.Crispiness = sourceDeepFriedComponent.Crispiness;
sliceDeepFriedComponent.PriceCoefficient = sourceDeepFriedComponent.PriceCoefficient;
UpdateDeepFriedName(args.Slice, sliceDeepFriedComponent);
// TODO: Flavor profiles aren't copied to the slices. This should
// probably be handled on upstream, but for now let's assume the
// oil of the deep fryer is overpowering enough for this small
// hack. This is likely the only place where it would be useful.
if (TryComp<FlavorProfileComponent>(args.Food, out var sourceFlavorProfileComponent) &&
TryComp<FlavorProfileComponent>(args.Slice, out var sliceFlavorProfileComponent))
{
sliceFlavorProfileComponent.Flavors.UnionWith(sourceFlavorProfileComponent.Flavors);
sliceFlavorProfileComponent.IgnoreReagents.UnionWith(sourceFlavorProfileComponent.IgnoreReagents);
}
}
}
public sealed class DeepFryAttemptEvent : CancellableEntityEventArgs
{
public EntityUid DeepFryer { get; }
public DeepFryAttemptEvent(EntityUid deepFryer)
{
DeepFryer = deepFryer;
}
}
public sealed class BeingDeepFriedEvent : EntityEventArgs
{
public EntityUid DeepFryer { get; }
public EntityUid Item { get; }
public bool TurnIntoFood { get; set; }
public BeingDeepFriedEvent(EntityUid deepFryer, EntityUid item)
{
DeepFryer = deepFryer;
Item = item;
}
}

View File

@ -55,6 +55,7 @@ public sealed partial class RandomizedCandySystem : EntitySystem
_metaData.SetEntityName(uid, $"{candyFlavor.Name} {meta.EntityName}", meta);
_metaData.SetEntityDescription(uid, $"{meta.EntityDescription} {GetExamineFluff(candyFlavor.Flavors)}");
Dirty(uid, meta);
Dirty(uid, flavorProfile);
}
// this technically duplicates code from FlavorProfileSystem but what we would need to call

View File

@ -0,0 +1,677 @@
using System.Linq;
using Content.Shared._DV.Kitchen;
using Content.Shared._DV.Kitchen.Components;
using Content.Shared._DV.Kitchen.Systems;
using Content.Shared.Audio;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Throwing;
using Content.Shared.Trigger.Systems;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server._DV.Kitchen;
public sealed class DeepFryerSystem : SharedDeepFryerSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedPowerReceiverSystem _power = default!;
[Dependency] private readonly TriggerSystem _trigger = default!;
/// <summary>
/// The trigger key used when non-frying oil reagents are added to the fryer
/// </summary>
public const string WrongReagentTriggerKey = "reaction";
private readonly List<EntityUid> _itemsToComplete = new();
private readonly List<EntityUid> _itemsToBurn = new();
private readonly HashSet<EntityUid> _processedItems = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DeepFryerComponent, EntInsertedIntoContainerMessage>(OnItemInserted);
SubscribeLocalEvent<DeepFryerComponent, EntRemovedFromContainerMessage>(OnItemRemoved);
SubscribeLocalEvent<DeepFryerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<DeepFryerComponent, SolutionTransferredEvent>(OnSolutionTransferred);
SubscribeLocalEvent<DeepFryerComponent, ThrowHitByEvent>(OnThrowHitBy);
}
private void OnSolutionTransferred(Entity<DeepFryerComponent> ent, ref SolutionTransferredEvent args)
{
// Only restore quality when oil is being added TO the fryer (not removed from it)
if (args.To != ent.Owner)
return;
// Get the fryer's solution to check what reagents are now in it
if (Solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var solution))
{
// Check if any reagents in the solution are NOT valid frying oils
var hasInvalidReagent = false;
foreach (var reagent in solution.Contents)
{
if (!ent.Comp.FryingOils.Contains(reagent.Reagent.Prototype))
{
hasInvalidReagent = true;
break;
}
}
// If we found an invalid reagent, trigger the reaction
if (hasInvalidReagent)
{
_trigger.Trigger(ent, args.User, WrongReagentTriggerKey);
// Don't restore oil quality if we're triggering an explosion
return;
}
}
// Restore oil quality based on the amount transferred
var qualityRestored = (float)args.Amount * ent.Comp.OilQualityRestorationPerUnit;
ent.Comp.OilQuality = Math.Min(1.0f, ent.Comp.OilQuality + qualityRestored);
Dirty(ent);
}
private void OnPowerChanged(Entity<DeepFryerComponent> ent, ref PowerChangedEvent args)
{
UpdateAppearance(ent);
}
private void OnThrowHitBy(Entity<DeepFryerComponent> ent, ref ThrowHitByEvent args)
{
if (args.Component.Thrower is not { } thrower || !CanInsertItem(ent, args.Thrown, out _))
return;
if (!HasComp<ProfessionalChefComponent>(thrower) && _random.Prob(ent.Comp.MissChance))
{
// Item missed! Let it continue with normal throw physics
Popup.PopupEntity(Loc.GetString("deep-fryer-throw-miss", ("item", args.Thrown)), ent, thrower);
return;
}
// Success! Insert the item
if (TryInsertItem(ent, args.Thrown, thrower))
Popup.PopupEntity(Loc.GetString("deep-fryer-throw-success", ("item", args.Thrown)), ent, thrower);
}
private void OnItemInserted(Entity<DeepFryerComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (args.Container.ID != ent.Comp.ContainerName)
return;
if (!_container.TryGetContainer(ent, ent.Comp.ContainerName, out var container))
return;
// First, check if this new item completes any multi-ingredient recipes with items already in the fryer
var completedMultiRecipe = TryFindAndUpgradeToMultiRecipe(ent, container);
if (completedMultiRecipe == null)
{
// No multi-recipe was completed, so assign this item its best single-ingredient recipe
var singleRecipe = FindBestRecipeForItem(args.Entity);
ent.Comp.CookingItems[args.Entity] = new CookingItem(singleRecipe, _timing.CurTime);
}
UpdateAppearance(ent);
}
private void OnItemRemoved(Entity<DeepFryerComponent> ent, ref EntRemovedFromContainerMessage args)
{
// Only process items in the fryer basket
if (args.Container.ID != ent.Comp.ContainerName)
return;
// Remove from cooking tracking
ent.Comp.CookingItems.Remove(args.Entity);
UpdateAppearance(ent);
}
/// <summary>
/// Updates the visual appearance of the deep fryer based on power and cooking state
/// </summary>
private void UpdateAppearance(Entity<DeepFryerComponent> ent)
{
var isBubbling = false;
// Check if the fryer is powered and has items
if (_power.IsPowered(ent.Owner)
&& ent.Comp.CookingItems.Count > 0
&& HasEnoughOil(ent))
{
isBubbling = true;
}
UpdateAmbience(ent, isBubbling);
_appearance.SetData(ent, DeepFryerVisuals.Bubbling, isBubbling);
}
private void UpdateAmbience(Entity<DeepFryerComponent> ent, bool value)
{
_ambientSound.SetAmbience(ent, value);
}
/// <summary>
/// Finds the best recipe for a single item.
/// Prioritizes multi-ingredient recipes (returns null so item waits), then single-ingredient recipes.
/// Returns null if item is only in multi-ingredient recipes or has no recipe at all.
/// </summary>
private ProtoId<DeepFryerRecipePrototype>? FindBestRecipeForItem(EntityUid item)
{
var itemProto = MetaData(item).EntityPrototype?.ID;
if (itemProto == null)
return null;
ProtoId<DeepFryerRecipePrototype>? singleIngredientRecipe = null;
// Look through all deep fryer recipes
foreach (var deepFryerRecipe in _prototype.EnumeratePrototypes<DeepFryerRecipePrototype>())
{
// Get the base microwave recipe
if (!_prototype.Resolve(deepFryerRecipe.BaseRecipe, out var microwaveRecipe))
continue;
// Count total solid ingredients
FixedPoint2 totalIngredients = 0;
var hasThisItem = false;
foreach (var (ingredientId, count) in microwaveRecipe.IngredientsSolids)
{
totalIngredients += count;
if (ingredientId == itemProto)
hasThisItem = true;
}
if (!hasThisItem)
continue;
if (totalIngredients == 1)
{
// This is a single-ingredient recipe
singleIngredientRecipe = deepFryerRecipe.ID;
}
}
// Return the single-ingredient recipe (may be null)
return singleIngredientRecipe;
}
/// <summary>
/// Checks if the newly inserted item completes any multi-ingredient recipe with existing items.
/// If so, upgrades all involved items to use that recipe.
/// Returns the recipe if one was found and upgraded, null otherwise.
/// </summary>
private ProtoId<DeepFryerRecipePrototype>? TryFindAndUpgradeToMultiRecipe(
Entity<DeepFryerComponent> ent,
BaseContainer container)
{
// Look through all multi-ingredient recipes to see if any are now complete
foreach (var deepFryerRecipe in _prototype.EnumeratePrototypes<DeepFryerRecipePrototype>())
{
// Get the base microwave recipe
if (!_prototype.Resolve(deepFryerRecipe.BaseRecipe, out var microwaveRecipe))
continue;
// Count total solid ingredients
FixedPoint2 totalIngredients = 0;
foreach (var (_, count) in microwaveRecipe.IngredientsSolids)
{
totalIngredients += count;
}
// Skip single-ingredient recipes
if (totalIngredients <= 1)
continue;
// Check if all ingredients for this multi-ingredient recipe are present
var ingredients = GetIngredientsForRecipe(deepFryerRecipe.ID, container);
if (ingredients == null)
continue;
// Check if ingredients are within tolerance
if (!AreIngredientsWithinTolerance(ent, ingredients))
continue;
// We found a complete multi-ingredient recipe within tolerance!
// Upgrade all ingredients to use this recipe
// Find the earliest start time among all ingredients
var earliestTime = _timing.CurTime;
foreach (var (ingredientUid, _) in ingredients)
{
if (ent.Comp.CookingItems.TryGetValue(ingredientUid, out var existingItem))
{
if (existingItem.TimeStarted < earliestTime)
earliestTime = existingItem.TimeStarted;
}
}
// Assign the multi-ingredient recipe to all ingredients with synchronized start time
foreach (var (ingredientUid, _) in ingredients)
{
ent.Comp.CookingItems[ingredientUid] = new CookingItem(deepFryerRecipe.ID, earliestTime);
}
return deepFryerRecipe.ID;
}
return null;
}
/// <summary>
/// Tries to get all ingredients needed for a specific recipe from the fryer.
/// Returns null if not all ingredients are present.
/// </summary>
private Dictionary<EntityUid, string>? GetIngredientsForRecipe(
ProtoId<DeepFryerRecipePrototype> recipeId,
BaseContainer container)
{
if (!_prototype.TryIndex(recipeId, out var deepFryerRecipe))
return null;
if (!_prototype.Resolve(deepFryerRecipe.BaseRecipe, out var microwaveRecipe))
return null;
var neededIngredients = new Dictionary<string, FixedPoint2>();
foreach (var (ingredient, count) in microwaveRecipe.IngredientsSolids)
{
neededIngredients[ingredient] = count;
}
var foundIngredients = new Dictionary<EntityUid, string>();
// Check each item in the fryer
foreach (var itemUid in container.ContainedEntities)
{
var itemProto = MetaData(itemUid).EntityPrototype?.ID;
if (itemProto == null)
continue;
// If this item is one of the needed ingredients
if (neededIngredients.TryGetValue(itemProto, out var needed) && needed > 0)
{
foundIngredients[itemUid] = itemProto;
neededIngredients[itemProto] -= 1;
}
}
// Check if we found all required ingredients
foreach (var (_, count) in neededIngredients)
{
if (count > 0)
return null; // Missing some ingredients
}
return foundIngredients;
}
/// <summary>
/// Checks if all ingredients for a multi-ingredient recipe are within the cooking time tolerance
/// </summary>
private bool AreIngredientsWithinTolerance(
Entity<DeepFryerComponent> ent,
Dictionary<EntityUid, string> ingredients)
{
if (ingredients.Count <= 1)
return true;
TimeSpan? earliest = null;
TimeSpan? latest = null;
foreach (var (ingredientUid, _) in ingredients)
{
if (!ent.Comp.CookingItems.TryGetValue(ingredientUid, out var cookingItem))
continue;
if (earliest == null || cookingItem.TimeStarted < earliest)
earliest = cookingItem.TimeStarted;
if (latest == null || cookingItem.TimeStarted > latest)
latest = cookingItem.TimeStarted;
}
if (earliest == null || latest == null)
return false;
var timeDifference = latest.Value - earliest.Value;
return timeDifference <= ent.Comp.CookingTolerance;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_itemsToComplete.Clear();
_itemsToBurn.Clear();
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<DeepFryerComponent>();
while (query.MoveNext(out var uid, out var fryer))
{
// Skip if not powered
if (!_power.IsPowered(uid))
continue;
// Skip if no oil
if (!HasEnoughOil((uid, fryer)))
continue;
// Get the container
if (!_container.TryGetContainer(uid, fryer.ContainerName, out var container))
continue;
_processedItems.Clear();
// Process each cooking item
foreach (var (itemUid, cookingItem) in fryer.CookingItems.ToList())
{
// Skip if already processed as part of a multi-ingredient recipe
if (_processedItems.Contains(itemUid))
continue;
var elapsedTime = curTime - cookingItem.TimeStarted;
// If the item is already marked as burning
if (cookingItem.IsBurning)
{
if (cookingItem.Recipe is { } burningRecipe)
{
if (!_prototype.TryIndex(burningRecipe, out var deepFryerRecipe))
continue;
var burnTime = deepFryerRecipe.BurnTime;
if (elapsedTime >= burnTime)
{
_itemsToBurn.Add(itemUid);
}
}
continue;
}
// Item is cooking (not burning yet)
if (cookingItem.Recipe is { } recipe)
{
if (!_prototype.TryIndex(recipe, out var deepFryerRecipe))
continue;
if (!_prototype.Resolve(deepFryerRecipe.BaseRecipe, out var microwaveRecipe))
continue;
var cookTime = TimeSpan.FromSeconds(microwaveRecipe.CookTime);
// Check if this is part of a multi-ingredient recipe
var multiIngredients = GetIngredientsForRecipe(recipe, container);
if (multiIngredients is { Count: > 1 })
{
// This is a multi-ingredient recipe
// Check if all ingredients are still within tolerance
if (!AreIngredientsWithinTolerance((uid, fryer), multiIngredients))
continue;
// Find the earliest start time
var earliestStart = TimeSpan.MaxValue;
foreach (var (ingredientUid, _) in multiIngredients)
{
// TryGetValue in Update my beloved
// Only like one deep fryer per map so it's gonna be fine probably
if (!fryer.CookingItems.TryGetValue(ingredientUid, out var ingredientCookingItem))
continue;
if (ingredientCookingItem.TimeStarted < earliestStart)
earliestStart = ingredientCookingItem.TimeStarted;
}
// Check if enough time has passed since the earliest ingredient
var earliestElapsed = curTime - earliestStart;
if (earliestElapsed < cookTime)
continue;
{
// Mark the first item for completion (it will handle all ingredients)
_itemsToComplete.Add(itemUid);
// Mark all ingredients as processed
foreach (var (ingredientUid, _) in multiIngredients)
{
_processedItems.Add(ingredientUid);
}
}
// Note: If ingredients are outside tolerance, they keep their current recipes
// and will be handled individually (single-ingredient recipes will complete, items without recipes will burn)
}
else
{
// Single-ingredient recipe, proceed normally
if (elapsedTime >= cookTime)
{
_itemsToComplete.Add(itemUid);
}
}
}
else
{
// Item has no recipe assigned - it should burn after BaseBurnTime
if (elapsedTime >= fryer.BaseBurnTime)
{
_itemsToBurn.Add(itemUid);
}
}
}
// Complete cooking for finished items
foreach (var itemUid in _itemsToComplete)
{
CompleteCooking((uid, fryer), itemUid, container);
}
// Burn items that have been cooking too long or have no recipe
foreach (var itemUid in _itemsToBurn)
{
BurnItem((uid, fryer), itemUid, container);
}
}
}
/// <summary>
/// Completes cooking for an item (or multi-ingredient recipe), transforming it into the result
/// </summary>
private void CompleteCooking(Entity<DeepFryerComponent> ent, EntityUid item, BaseContainer container)
{
if (!ent.Comp.CookingItems.TryGetValue(item, out var cookingItem))
return;
if (cookingItem.Recipe is not { } recipe)
return;
if (!_prototype.TryIndex(recipe, out var deepFryerRecipe))
return;
// Get the base microwave recipe for result
if (!_prototype.Resolve(deepFryerRecipe.BaseRecipe, out var microwaveRecipe))
return;
// Get all ingredients for this recipe
var recipeIngredients = GetIngredientsForRecipe(recipe, container);
var isMultiIngredient = recipeIngredients is { Count: > 1 };
// Check if we should burn the item due to foul oil
var qualityLevel = GetOilQualityLevel(ent.Comp.OilQuality);
if (qualityLevel == OilQuality.Foul && _random.Prob(ent.Comp.FoulOilBurnChance))
{
// For multi-ingredient recipes, burn all ingredients
if (isMultiIngredient)
{
foreach (var (ingredientUid, _) in recipeIngredients!)
{
BurnItem(ent, ingredientUid, container, recipe: recipe);
}
return;
}
// Force burn the single item
BurnItem(ent, item, container, recipe: recipe);
return;
}
var xform = Transform(ent);
var coords = Xform.GetMapCoordinates((ent, xform));
// For multi-ingredient recipes, remove ALL ingredients
if (isMultiIngredient)
{
// Delete all ingredients
foreach (var (ingredientUid, _) in recipeIngredients!)
{
ent.Comp.CookingItems.Remove(ingredientUid);
_container.Remove(ingredientUid, container);
QueueDel(ingredientUid);
}
}
else
{
// Single ingredient recipe
ent.Comp.CookingItems.Remove(item);
_container.Remove(item, container);
QueueDel(item);
}
// Spawn the result (from the microwave recipe)
var result = Spawn(microwaveRecipe.Result, coords);
// Transfer solution from fryer to food (includes oil AND any contaminants!)
TransferOilToFood(ent, result, deepFryerRecipe.OilConsumption);
// Add flavors based on oil quality
AddOilQualityFlavors(result, ent.Comp, qualityLevel);
// Degrade oil quality
DegradeOilQuality(ent);
// Try to put it back in the fryer
if (!_container.Insert(result, container))
{
// If we can't insert it (container full?), just leave it at the fryer's location
Xform.SetCoordinates(result, xform, Xform.GetMoverCoordinates(ent, xform));
}
else
{
// Track the result and start burning timer
ent.Comp.CookingItems[result] = new CookingItem(cookingItem.Recipe, _timing.CurTime, isBurning: true);
}
// Show a popup
Popup.PopupEntity(Loc.GetString("deep-fryer-item-finished", ("item", result)), ent, PopupType.Medium);
_audio.PlayPvs(ent.Comp.FinishedCookingSound, ent);
}
/// <summary>
/// Burns an item - uses recipe's BurnedResult if available, otherwise uses BaseBurnedResult
/// </summary>
private void BurnItem(Entity<DeepFryerComponent> ent, EntityUid item, BaseContainer container, ProtoId<DeepFryerRecipePrototype>? recipe = null)
{
EntProtoId? burnedEntity = null;
// If we were never explicitly given a recipe, then see if there's one
if (!recipe.HasValue && ent.Comp.CookingItems.TryGetValue(item, out var cookingItem) && cookingItem.Recipe is { } foundRecipe)
recipe = foundRecipe;
// Try to get the recipe, if we were given one or if we found one
if (_prototype.TryIndex(recipe, out var deepFryerRecipe))
burnedEntity = deepFryerRecipe.BurnedResult;
// finally, if we don't have a BurnedResult from a recipe, just default to BaseBurnedResult
if (!burnedEntity.HasValue)
burnedEntity = ent.Comp.BaseBurnedResult;
// Remove the item from tracking and container
ent.Comp.CookingItems.Remove(item);
_container.Remove(item, container);
// Delete the original item
QueueDel(item);
// Spawn the burned result on top of the fryer
Spawn(burnedEntity, Xform.GetMoverCoordinates(ent));
// Degrade oil quality even when burning
DegradeOilQuality(ent);
// Show a danger popup
Popup.PopupEntity(Loc.GetString("deep-fryer-item-burned", ("item", item)), ent, PopupType.MediumCaution);
_audio.PlayPvs(ent.Comp.FinishedBurningSound, ent);
}
/// <summary>
/// Adds flavors to the cooked item based on the current oil quality
/// </summary>
private void AddOilQualityFlavors(EntityUid result, DeepFryerComponent fryer, OilQuality qualityLevel)
{
// Get or create the FlavorProfile component
var flavorProfile = EnsureComp<FlavorProfileComponent>(result);
// Get the flavors for this quality level
if (!fryer.OilQualityFlavors.TryGetValue(qualityLevel, out var flavors))
return;
// Add each flavor to the profile
foreach (var flavor in flavors)
{
flavorProfile.Flavors.Add(flavor);
}
Dirty(result, flavorProfile);
}
/// <summary>
/// Degrades the oil quality after cooking an item
/// </summary>
private void DegradeOilQuality(Entity<DeepFryerComponent> ent)
{
// Calculate degradation multiplier based on oil volume
var degradationMultiplier = CalculateOilDegradationMultiplier(ent);
// Reduce oil quality with the multiplier applied
var degradationAmount = ent.Comp.OilDegradationPerRecipe * degradationMultiplier;
ent.Comp.OilQuality = Math.Max(0f, ent.Comp.OilQuality - degradationAmount);
// Mark as dirty to sync to clients
Dirty(ent);
}
/// <summary>
/// Transfers solution from the fryer into the food solution.
/// Transfers ALL reagents proportionally - if someone added bleach to the fryer, enjoy your bleach-fried food!
/// </summary>
private void TransferOilToFood(Entity<DeepFryerComponent> ent, EntityUid food, FixedPoint2 amount)
{
// Get the fryer's solution
if (!Solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var fryerSolution))
return;
// Get the food solution
if (!Solution.TryGetSolution(food, "food", out var foodSolutionEnt, out _))
return;
// Split the desired amount from the fryer - this takes ALL reagents proportionally!
// If the fryer has 80% oil and 20% bleach, the food gets 80% oil and 20% bleach too!
var transferredSolution = fryerSolution.SplitSolution(amount);
// Add the split solution to the food
Solution.AddSolution(foodSolutionEnt.Value, transferredSolution);
}
}

View File

@ -46,6 +46,12 @@ namespace Content.Shared.Kitchen
[DataField]
public bool SecretRecipe = false;
/// <summary>
/// DeltaV: Changes the guidebook formatting to "Fry for"
/// </summary>
[DataField]
public bool DeepFried;
/// <summary>
/// Count the number of ingredients in a recipe for sorting the recipe list.
/// This makes sure that where ingredient lists overlap, the more complex

View File

@ -3,13 +3,15 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Nutrition.Components;
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState] // DV - Deep Fryers
public sealed partial class FlavorProfileComponent : Component
{
/// <summary>
/// Localized string containing the base flavor of this entity.
/// </summary>
[DataField]
public HashSet<string> Flavors { get; private set; } = new();
[AutoNetworkedField] // DV - Deep Fryers
public HashSet<string> Flavors = new(); // DV remove setter
/// <summary>
/// Reagent IDs to ignore when processing this flavor profile. Defaults to nutriment.

View File

@ -1,29 +0,0 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Robust.Shared.Serialization;
namespace Content.Shared.Nyanotrasen.Kitchen
{
[Serializable, NetSerializable]
public sealed partial class ClearSlagDoAfterEvent : DoAfterEvent
{
[DataField("solution", required: true)]
public Solution Solution = default!;
[DataField("amount", required: true)]
public FixedPoint2 Amount;
private ClearSlagDoAfterEvent()
{
}
public ClearSlagDoAfterEvent(Solution solution, FixedPoint2 amount)
{
Solution = solution;
Amount = amount;
}
public override DoAfterEvent Clone() => this;
}
}

View File

@ -1,22 +0,0 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Nyanotrasen.Kitchen.Components
{
[NetworkedComponent]
public abstract partial class SharedDeepFriedComponent : Component
{
/// <summary>
/// How deep-fried is this item?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("crispiness")]
public int Crispiness { get; set; }
}
[Serializable, NetSerializable]
public enum DeepFriedVisuals : byte
{
Fried,
}
}

View File

@ -1,12 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Nyanotrasen.Kitchen.Components
{
public abstract partial class SharedDeepFryerComponent : Component { }
[Serializable, NetSerializable]
public enum DeepFryerVisuals : byte
{
Bubbling,
}
}

View File

@ -1,5 +0,0 @@
namespace Content.Shared.Nyanotrasen.Kitchen;
public abstract class SharedDeepfryerSystem : EntitySystem
{
}

View File

@ -1,67 +0,0 @@
using Content.Shared.FixedPoint;
using Robust.Shared.Serialization;
namespace Content.Shared.Nyanotrasen.Kitchen.UI
{
[Serializable, NetSerializable]
public sealed class DeepFryerBoundUserInterfaceState : BoundUserInterfaceState
{
public readonly FixedPoint2 OilLevel;
public readonly FixedPoint2 OilPurity;
public readonly FixedPoint2 FryingOilThreshold;
public readonly NetEntity[] ContainedEntities;
public DeepFryerBoundUserInterfaceState(
FixedPoint2 oilLevel,
FixedPoint2 oilPurity,
FixedPoint2 fryingOilThreshold,
NetEntity[] containedEntities)
{
OilLevel = oilLevel;
OilPurity = oilPurity;
FryingOilThreshold = fryingOilThreshold;
ContainedEntities = containedEntities;
}
}
[Serializable, NetSerializable]
public sealed class DeepFryerRemoveItemMessage : BoundUserInterfaceMessage
{
public readonly NetEntity Item;
public DeepFryerRemoveItemMessage(NetEntity item)
{
Item = item;
}
}
[Serializable, NetSerializable]
public sealed class DeepFryerInsertItemMessage : BoundUserInterfaceMessage
{
public DeepFryerInsertItemMessage() { }
}
[Serializable, NetSerializable]
public sealed class DeepFryerScoopVatMessage : BoundUserInterfaceMessage
{
public DeepFryerScoopVatMessage() { }
}
[Serializable, NetSerializable]
public sealed class DeepFryerClearSlagMessage : BoundUserInterfaceMessage
{
public DeepFryerClearSlagMessage() { }
}
[Serializable, NetSerializable]
public sealed class DeepFryerRemoveAllItemsMessage : BoundUserInterfaceMessage
{
public DeepFryerRemoveAllItemsMessage() { }
}
[NetSerializable, Serializable]
public enum DeepFryerUiKey
{
Key
}
}

View File

@ -0,0 +1,191 @@
using Content.Shared._DV.Kitchen.Systems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared._DV.Kitchen.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedDeepFryerSystem))]
public sealed partial class DeepFryerComponent : Component
{
/// <summary>
/// The <see cref="EntityWhitelist"/> of items that fit into the deep fryer
/// </summary>
[DataField]
public EntityWhitelist Whitelist = new();
/// <summary>
/// The <see cref="EntityWhitelist"/> of items that will never fit into the deep fryer, even if on the whitelist
/// </summary>
[DataField]
public EntityWhitelist Blacklist = new();
/// <summary>
/// The <see cref="Solution"/> of cooking oil
/// </summary>
[DataField]
public string Solution = "vat_oil";
/// <summary>
/// The allowed reagents that will not cause a reaction when inserted
/// </summary>
[DataField]
public List<ProtoId<ReagentPrototype>> FryingOils = new();
/// <summary>
/// The container that holds items being fried
/// </summary>
[DataField]
public string ContainerName = "fryer_basket";
/// <summary>
/// Maximum number of items that can be in the fryer at once
/// </summary>
[DataField]
public int MaxItems = 3;
/// <summary>
/// Minimum amount of oil required to fry items (in units)
/// </summary>
[DataField]
public FixedPoint2 MinimumOilVolume = 25;
/// <summary>
/// Tracks cooking progress for each item in the fryer
/// Key: EntityUid of the item being fried
/// Value: CookingItem struct containing recipe and time
/// </summary>
[DataField]
public Dictionary<EntityUid, CookingItem> CookingItems = new();
/// <summary>
/// The <see cref="SoundSpecifier"/> that will play once an item finishes cooking
/// </summary>
[DataField]
public SoundSpecifier FinishedCookingSound = new SoundPathSpecifier("/Audio/Machines/Nuke/angry_beep.ogg");
/// <summary>
/// The <see cref="SoundSpecifier"/> that will play once an item finishes burning
/// </summary>
[DataField]
public SoundSpecifier FinishedBurningSound = new SoundPathSpecifier("/Audio/Effects/drop.ogg");
/// <summary>
/// Current oil quality as a value from 0.0 (0%) to 1.0 (100%)
/// </summary>
[DataField, AutoNetworkedField]
public float OilQuality = 1.0f;
/// <summary>
/// How much the oil quality degrades per recipe cooked (as a percentage, e.g., 0.05 = 5%)
/// </summary>
[DataField]
public float OilDegradationPerRecipe = 0.05f;
/// <summary>
/// Degradation multiplier when at minimum oil volume (higher = faster degradation with less oil)
/// </summary>
[DataField]
public float MinOilVolumeDegradationMultiplier = 4.0f;
/// <summary>
/// How much quality is restored per unit of oil added (e.g., 0.01 = 1% quality per unit)
/// </summary>
[DataField]
public float OilQualityRestorationPerUnit = 0.01f;
/// <summary>
/// Chance (0.0 to 1.0) that BurnedResult will spawn when using Foul quality oil
/// </summary>
[DataField]
public float FoulOilBurnChance = 0.3f;
/// <summary>
/// Maps oil quality levels to the flavors that should be added to cooked items
/// </summary>
[DataField]
public Dictionary<OilQuality, List<ProtoId<FlavorPrototype>>> OilQualityFlavors = new();
/// <summary>
/// Time tolerance for multi-ingredient recipes (ingredients must be inserted within this window)
/// </summary>
[DataField]
public TimeSpan CookingTolerance = TimeSpan.FromSeconds(5);
/// <summary>
/// The time it will take for ingredients to start burning if not a part of any recipe
/// </summary>
[DataField]
public TimeSpan BaseBurnTime = TimeSpan.FromSeconds(30);
/// <summary>
/// The default result for when something burns
/// </summary>
[DataField]
public EntProtoId BaseBurnedResult = "FoodBadRecipe";
/// <summary>
/// Chance (0.0 to 1.0) that a thrown item will miss the fryer and land nearby instead.
/// Professional chefs always have a 0% miss chance regardless of this value.
/// </summary>
/// <seealso cref="ProfessionalChefComponent"/>
[DataField]
public float MissChance = 0.25f;
}
/// <summary>
/// Tracks an item being cooked in the deep fryer
/// </summary>
[DataDefinition]
public partial record struct CookingItem
{
/// <summary>
/// The deep fryer recipe being used to cook this item
/// </summary>
[DataField]
public ProtoId<DeepFryerRecipePrototype>? Recipe;
/// <summary>
/// When this item started cooking or burning
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan TimeStarted;
/// <summary>
/// Whether this item has finished cooking and is now burning
/// </summary>
[DataField]
public bool IsBurning;
public CookingItem(ProtoId<DeepFryerRecipePrototype>? recipe, TimeSpan timeStarted, bool isBurning = false)
{
Recipe = recipe;
TimeStarted = timeStarted;
IsBurning = isBurning;
}
}
/// <summary>
/// Represents the quality level of oil in the deep fryer
/// </summary>
[Serializable, NetSerializable]
public enum OilQuality : byte
{
Pristine, // >= 90%
Clean, // >= 70%
Used, // >= 50%
Dirty, // >= 30%
Foul // >= 0%
}
[Serializable, NetSerializable]
public enum DeepFryerVisuals : byte
{
Bubbling
}

View File

@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Kitchen.Components;
/// <summary>
/// Players with this component will never miss a throw into the deep fryer's basket.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ProfessionalChefComponent : Component;

View File

@ -0,0 +1,46 @@
using Content.Shared.FixedPoint;
using Content.Shared.Kitchen;
using Robust.Shared.Prototypes;
namespace Content.Shared._DV.Kitchen;
/// <summary>
/// The base recipe for the deep fryer.
/// </summary>
/// <remarks>
/// This only handles the deep-fryer related stuff like oil consumption and burning result,
/// while the <see cref="FoodRecipePrototype"/> handles the recipe and results themselves.
/// This is done because while FoodRecipe is not generic and hardcoded to microwaves,
/// we can still reuse it for guidebook generation without duping the code.
/// </remarks>
[Prototype]
public sealed class DeepFryerRecipePrototype : IPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// The <see cref="FoodRecipePrototype"/> to inherit from
/// </summary>
[DataField(required: true)]
public ProtoId<FoodRecipePrototype> BaseRecipe;
/// <summary>
/// How long will it take for this food to burn once it finishes cooking?
/// </summary>
[DataField]
public TimeSpan BurnTime = TimeSpan.FromSeconds(30);
/// <summary>
/// How many units of oil will this recipe use up?
/// </summary>
[DataField]
public FixedPoint2 OilConsumption = 2;
/// <summary>
/// The <see cref="EntityPrototype"/> this recipe will spawn when it finishes burning.
/// </summary>
[DataField]
public EntProtoId BurnedResult = "FoodBadRecipe";
}

View File

@ -0,0 +1,320 @@
using Content.Shared._DV.Kitchen.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Whitelist;
using Content.Shared.Verbs;
using Content.Shared.Hands.EntitySystems;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.Kitchen.Systems;
public abstract class SharedDeepFryerSystem : EntitySystem
{
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] protected readonly SharedSolutionContainerSystem Solution = default!;
[Dependency] protected readonly SharedTransformSystem Xform = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DeepFryerComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<DeepFryerComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<DeepFryerComponent, DeepFryerInsertDoAfterEvent>(OnInsertDoAfter);
SubscribeLocalEvent<DeepFryerComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerbs);
SubscribeLocalEvent<DeepFryerComponent, ExaminedEvent>(OnExamined);
}
private void OnComponentInit(Entity<DeepFryerComponent> ent, ref ComponentInit args)
{
_container.EnsureContainer<Container>(ent, ent.Comp.ContainerName);
}
private void OnExamined(Entity<DeepFryerComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (Solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var solution) && solution.Volume <= 0)
return;
var qualityLevel = GetOilQualityLevel(ent.Comp.OilQuality);
var (color, labelName) = GetOilQualityInfo(qualityLevel);
args.PushMarkup(Loc.GetString("deep-fryer-oil-quality-examine",
("color", color.ToHex()),
("state", labelName)));
}
private void OnInteractUsing(Entity<DeepFryerComponent> ent, ref InteractUsingEvent args)
{
if (args.Handled)
return;
// Check if the item can be inserted
if (!CanInsertItem(ent, args.Used, out var reason))
{
Popup.PopupClient(reason, ent, args.User);
return;
}
args.Handled = true;
// Start a do-after for inserting the item
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, 1f, new DeepFryerInsertDoAfterEvent(), ent, target: ent, used: args.Used)
{
BreakOnMove = true,
BreakOnDamage = true,
BlockDuplicate = true,
NeedHand = true
};
_doAfter.TryStartDoAfter(doAfterArgs);
}
private void OnInsertDoAfter(Entity<DeepFryerComponent> ent, ref DeepFryerInsertDoAfterEvent args)
{
if (args.Cancelled || args.Handled || args.Used == null)
return;
// Re-check if we can still insert (things might have changed)
if (!CanInsertItem(ent, args.Used.Value, out var reason))
{
Popup.PopupClient(reason, ent, args.User);
return;
}
// Insert the item
if (TryInsertItem(ent, args.Used.Value, args.User))
{
args.Handled = true;
}
}
private void OnGetAlternativeVerbs(Entity<DeepFryerComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
if (!_container.TryGetContainer(ent, ent.Comp.ContainerName, out var container))
return;
var user = args.User;
// Create an eject verb for each item in the basket
foreach (var item in container.ContainedEntities)
{
var itemName = Name(item);
var itemUid = item;
var verb = new AlternativeVerb
{
Text = Loc.GetString("deep-fryer-eject-item", ("item", itemName)),
Category = VerbCategory.Eject,
Act = () => TryEjectItem(ent, itemUid, user),
Priority = 1
};
args.Verbs.Add(verb);
}
}
/// <summary>
/// Attempts to eject an item from the deep fryer
/// </summary>
[PublicAPI]
public bool TryEjectItem(Entity<DeepFryerComponent> ent, EntityUid item, EntityUid user)
{
if (!_container.TryGetContainer(ent, ent.Comp.ContainerName, out var container))
return false;
if (!container.Contains(item))
return false;
// Remove from container
if (!_container.Remove(item, container))
return false;
// Try to put in user's hands, otherwise drop at fryer location
if (!_hands.TryPickupAnyHand(user, item))
{
var xform = Transform(ent);
Xform.SetCoordinates(item, xform.Coordinates);
}
Popup.PopupClient(Loc.GetString("deep-fryer-eject-item-success", ("item", item)), ent, user);
return true;
}
/// <summary>
/// Checks if an item can be inserted into the deep fryer
/// </summary>
[PublicAPI]
public bool CanInsertItem(Entity<DeepFryerComponent> ent, EntityUid item, out string reason)
{
reason = string.Empty;
// Skip the popup entirely if we're transferring solutions
if (HasComp<SolutionTransferComponent>(item))
return false;
// Check blacklist first since it should override
if (_whitelist.IsBlacklistPass(ent.Comp.Blacklist, item))
{
reason = Loc.GetString("deep-fryer-blacklist-item");
return false;
}
// Check whitelist
if (!_whitelist.IsWhitelistPass(ent.Comp.Whitelist, item))
{
reason = Loc.GetString("deep-fryer-not-food");
return false;
}
// Get the container
if (!_container.TryGetContainer(ent, ent.Comp.ContainerName, out var container))
{
reason = Loc.GetString("deep-fryer-no-container");
return false;
}
// Check if the fryer is full
if (container.ContainedEntities.Count >= ent.Comp.MaxItems)
{
reason = Loc.GetString("deep-fryer-full");
return false;
}
// Check if there's enough oil
if (!HasEnoughOil(ent))
{
reason = Loc.GetString("deep-fryer-insufficient-oil");
return false;
}
return true;
}
/// <summary>
/// Attempts to insert an item into the deep fryer
/// </summary>
[PublicAPI]
public bool TryInsertItem(Entity<DeepFryerComponent> ent, EntityUid item, EntityUid? user)
{
if (!_container.TryGetContainer(ent, ent.Comp.ContainerName, out var container))
return false;
// Insert the item
if (!_container.Insert(item, container))
return false;
Popup.PopupClient(Loc.GetString("deep-fryer-insert-item", ("item", item)), ent, user ?? EntityUid.Invalid);
return true;
}
/// <summary>
/// Checks if the fryer has enough oil to fry items
/// </summary>
protected bool HasEnoughOil(Entity<DeepFryerComponent> ent)
{
if (!Solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var solution))
return false;
// Check if there's enough total volume
if (solution.Volume < ent.Comp.MinimumOilVolume)
return false;
// Check if any of the reagents in the solution are valid frying oils
foreach (var reagent in solution.Contents)
{
if (ent.Comp.FryingOils.Contains(reagent.Reagent.Prototype))
return true;
}
return false;
}
/// <summary>
/// Gets the number of items currently in the fryer
/// </summary>
[PublicAPI]
public int GetItemCount(Entity<DeepFryerComponent> ent)
{
if (!_container.TryGetContainer(ent, ent.Comp.ContainerName, out var container))
return 0;
return container.ContainedEntities.Count;
}
/// <summary>
/// Determines the oil quality level from the quality value
/// </summary>
[PublicAPI]
public static OilQuality GetOilQualityLevel(float quality)
{
return quality switch
{
>= 0.9f => OilQuality.Pristine,
>= 0.7f => OilQuality.Clean,
>= 0.5f => OilQuality.Used,
>= 0.3f => OilQuality.Dirty,
_ => OilQuality.Foul
};
}
/// <summary>
/// Calculates the oil degradation multiplier based on current oil volume
/// Less oil = faster degradation
/// </summary>
protected float CalculateOilDegradationMultiplier(Entity<DeepFryerComponent> ent)
{
if (!Solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var solution))
return 1.0f;
var maxVolume = solution.MaxVolume;
var currentVolume = solution.Volume;
var minVolume = ent.Comp.MinimumOilVolume;
// If we don't have enough oil range, just return 1.0
if (maxVolume <= minVolume)
return 1.0f;
// Linear interpolation between 1.0x (at max volume) and MinOilVolumeDegradationMultiplier (at min volume)
// 1.0 + (maxVolume - currentVolume) / (maxVolume - minVolume) * (multiplier - 1.0)
var volumeRatio = (float)((maxVolume - currentVolume) / (maxVolume - minVolume));
var multiplier = 1.0f + volumeRatio * (ent.Comp.MinOilVolumeDegradationMultiplier - 1.0f);
return Math.Max(1.0f, multiplier);
}
/// <summary>
/// Gets the color and label name for a given oil quality level
/// </summary>
[PublicAPI]
public (Color color, string labelName) GetOilQualityInfo(OilQuality quality)
{
return quality switch
{
OilQuality.Pristine => (Color.Green, Loc.GetString("deep-fryer-oil-quality-pristine")),
OilQuality.Clean => (Color.White, Loc.GetString("deep-fryer-oil-quality-clean")),
OilQuality.Used => (Color.Yellow, Loc.GetString("deep-fryer-oil-quality-used")),
OilQuality.Dirty => (Color.Orange, Loc.GetString("deep-fryer-oil-quality-dirty")),
OilQuality.Foul => (Color.Red, Loc.GetString("deep-fryer-oil-quality-foul")),
_ => (Color.White, Loc.GetString("deep-fryer-oil-quality-unknown"))
};
}
}
[Serializable, NetSerializable]
public sealed partial class DeepFryerInsertDoAfterEvent : SimpleDoAfterEvent;

View File

@ -84,6 +84,13 @@ flavor-complex-affogato = like boozy ice cream
flavor-complex-five-oclock = like hard tea
flavor-complex-mliko = like a prank
## DeltaV deep fryer
flavor-base-crispy = crispy
flavor-base-stale = stale
flavor-base-burnt = burnt
flavor-base-rancid = rancid
flavor-base-awful = awful
candy-flavor-profile = This one is supposed to taste {$flavor}.
candy-flavor-profile-multiple = This one is supposed to taste {$flavors} and {$lastFlavor}.
candy-flavor-profile-unknown = You have no idea what this one is supposed to taste like.

View File

@ -0,0 +1,7 @@
guideboook-microwave-fry-time = Fry for [bold]{$time}[/bold] seconds
guidebook-microwave-cook-time-deltav = Microwave for
{ $time ->
[0] Instant
[1] [bold]1[/bold] second
*[other] [bold]{$time}[/bold] seconds
}

View File

@ -38,3 +38,5 @@ guide-entry-trade-station = Trade Station
guide-entry-cargo = Logistics
guide-entry-frequently-used-chemicals = Frequently Used Chemicals
guide-entry-deepfried-recipes = Deep-Fried

View File

@ -0,0 +1,22 @@
## General interactions
deep-fryer-blacklist-item = You can't fry that!
deep-fryer-not-food = That's not something you can fry!
deep-fryer-no-container = The fryer basket is missing!
deep-fryer-full = The fryer is full!
deep-fryer-insufficient-oil = There's not enough oil in the fryer!
deep-fryer-insert-item = You insert {THE($item)} into the deep fryer.
deep-fryer-eject-item = Eject {THE($item)}
deep-fryer-eject-item-success = You eject {THE($item)} from the fryer.
deep-fryer-item-finished = {CAPITALIZE(THE($item))} has finished cooking!
deep-fryer-item-burned = {CAPITALIZE(THE($item))} burns to a crisp!
deep-fryer-throw-miss = {CAPITALIZE(THE($item))} misses the fryer and bounces off!
deep-fryer-throw-success = You toss {THE($item)} into the fryer!
## Oil quality
deep-fryer-oil-quality-examine = The oil looks [color={$color}]{$state}[/color].
deep-fryer-oil-quality-pristine = pristine
deep-fryer-oil-quality-clean = clean
deep-fryer-oil-quality-used = used
deep-fryer-oil-quality-dirty = dirty
deep-fryer-oil-quality-foul = foul
deep-fryer-oil-quality-unknown = unknown

View File

@ -6,3 +6,6 @@ reagent-desc-tomatosauce = Tomato with salt and herbs.
reagent-name-bechamel = bechamel
reagent-desc-bechamel = A classic white sauce common to several cultures.
reagent-name-oil-ghee = ghee
reagent-desc-oil-ghee = Thick and translucent.

View File

@ -1,47 +0,0 @@
## DeepFryer Entity
deep-fryer-blacklist-item-failed = {CAPITALIZE(THE($item))} fails to be covered in oil.
deep-fryer-oil-purity-low = {CAPITALIZE(THE($deepFryer))} sputters to no effect.
deep-fryer-oil-volume-low = {CAPITALIZE(THE($deepFryer))} burns and spews smoke!
deep-fryer-oil-no-slag = There's no slag to drain.
deep-fryer-storage-full = All of the baskets are full.
deep-fryer-storage-no-fit = {CAPITALIZE(THE($item))} won't fit inside one of the baskets.
deep-fryer-interact-using-not-item = That doesn't seem to be an item.
deep-fryer-need-liquid-container-in-hand = You need to first hold a liquid container like a beaker or bowl in your active hand.
deep-fryer-thrown-missed = Missed!
deep-fryer-thrown-hit-oil = Plop!
deep-fryer-thrown-hit-oil-low = Plonk!
deep-fryer-entity-escape = {CAPITALIZE(THE($victim))} leaps out of {THE($deepFryer)}!
## DeepFryer UI
deep-fryer-window-title = Deep Fryer
deep-fryer-label-baskets = Baskets
deep-fryer-label-oil-level = Oil Level
deep-fryer-label-oil-purity = Oil Purity
deep-fryer-button-insert-item = Insert Item
deep-fryer-button-insert-item-tooltip = Place your held item into one of the deep fryer baskets.
deep-fryer-button-scoop-vat = Scoop Vat
deep-fryer-button-scoop-vat-tooltip = Scoop out some liquid from the oil vat. You need to hold a liquid container for this.
deep-fryer-button-clear-slag = Clear Slag
deep-fryer-button-clear-slag-tooltip = Clear out some waste from the oil vat. You need to hold a liquid container for this.
deep-fryer-button-remove-all-items = Remove All Items
deep-fryer-button-remove-all-items-tooltip = Remove all of the items from the deep fryer baskets at once.
## DeepFriedComponent
deep-fried-crispy-item = crispy {$entity}
deep-fried-crispy-item-examine = It's covered in a crispy, oily texture.
deep-fried-fried-item = deep-fried {$entity}
deep-fried-fried-item-examine = It's covered in a thick, crispy layer.
deep-fried-burned-item = burned {$entity}
deep-fried-burned-item-examine = It's blackened with char.
reagent-name-oil-ghee = ghee
reagent-desc-oil-ghee = Thick and translucent.

View File

@ -37,6 +37,7 @@
- OtherRecipes
- MedicinalRecipes
- SecretRecipes
- DeepFriedRecipes # DeltaV
- type: guideEntry
id: PizzaRecipes

View File

@ -1341,77 +1341,79 @@
FoodPotato: 1
FoodCheeseSlice: 1
- type: microwaveMealRecipe
id: RecipeFries
name: space fries recipe
result: FoodMealFries
time: 15
group: Savory
reagents:
TableSalt: 5
solids:
FoodPotato: 1
# Begin DV removals for deep fryer
#- type: microwaveMealRecipe
# id: RecipeFries
# name: space fries recipe
# result: FoodMealFries
# time: 15
# group: Savory
# reagents:
# TableSalt: 5
# solids:
# FoodPotato: 1
- type: microwaveMealRecipe
id: RecipeCheesyFries
name: cheesy fries recipe
result: FoodMealFriesCheesy
time: 15
group: Savory
reagents:
TableSalt: 5
solids:
FoodPotato: 1
FoodCheeseSlice: 1
#- type: microwaveMealRecipe
# id: RecipeCheesyFries
# name: cheesy fries recipe
# result: FoodMealFriesCheesy
# time: 15
# group: Savory
# reagents:
# TableSalt: 5
# solids:
# FoodPotato: 1
# FoodCheeseSlice: 1
- type: microwaveMealRecipe
id: RecipeCarrotFries
name: carrot fries recipe
result: FoodMealFriesCarrot
time: 15
group: Savory
reagents:
TableSalt: 5
solids:
FoodCarrot: 1
#- type: microwaveMealRecipe
# id: RecipeCarrotFries
# name: carrot fries recipe
# result: FoodMealFriesCarrot
# time: 15
# group: Savory
# reagents:
# TableSalt: 5
# solids:
# FoodCarrot: 1
- type: microwaveMealRecipe
id: RecipeNachos
name: nachos recipe
result: FoodMealNachos
time: 10
group: Savory
reagents:
TableSalt: 1
solids:
FoodDoughTortillaFlat: 1
FoodPlateSmall: 1
#- type: microwaveMealRecipe
# id: RecipeNachos
# name: nachos recipe
# result: FoodMealNachos
# time: 10
# group: Savory
# reagents:
# TableSalt: 1
# solids:
# FoodDoughTortillaFlat: 1
# FoodPlateSmall: 1
- type: microwaveMealRecipe
id: RecipeNachosCheesy
name: cheesy nachos recipe
result: FoodMealNachosCheesy
time: 10
group: Savory
reagents:
TableSalt: 1
solids:
FoodCheeseSlice: 1
FoodDoughTortillaFlat: 1
FoodPlateSmall: 1
#- type: microwaveMealRecipe
# id: RecipeNachosCheesy
# name: cheesy nachos recipe
# result: FoodMealNachosCheesy
# time: 10
# group: Savory
# reagents:
# TableSalt: 1
# solids:
# FoodCheeseSlice: 1
# FoodDoughTortillaFlat: 1
# FoodPlateSmall: 1
- type: microwaveMealRecipe
id: RecipeNachosCuban
name: cuban nachos recipe
result: FoodMealNachosCuban
time: 10
group: Savory
reagents:
Ketchup: 5
solids:
FoodChiliPepper: 1
FoodDoughTortillaFlat: 1
FoodPlateSmall: 1
#- type: microwaveMealRecipe
# id: RecipeNachosCuban
# name: cuban nachos recipe
# result: FoodMealNachosCuban
# time: 10
# group: Savory
# reagents:
# Ketchup: 5
# solids:
# FoodChiliPepper: 1
# FoodDoughTortillaFlat: 1
# FoodPlateSmall: 1
# End DV Removals
- type: microwaveMealRecipe
id: RecipePopcorn

View File

@ -10,9 +10,9 @@
netsync: false
sprite: Nyanotrasen/Structures/Machines/deep_fryer.rsi
layers:
- state: off-0
- map: ["enum.SolutionContainerLayers.Fill"]
state: off-1
- state: off-0
- map: ["enum.SolutionContainerLayers.Fill"]
state: off-1
- type: AmbientSound
volume: -4
range: 5
@ -22,13 +22,27 @@
- type: SolutionContainerVisuals
maxFillLevels: 8
fillBaseName: off-
- type: Anchorable
- type: Pullable
- type: Rotatable
rotateWhilePulling: false
- type: Physics
bodyType: Static
- type: Climbable
- type: Explosive # Weak explosion in a very small radius. Ignites surrounding entities.
explosionType: FireBomb
totalIntensity: 25
intensitySlope: 5
maxIntensity: 3
canCreateVacuum: false
- type: ExplodeOnTrigger
keysIn:
- reaction
- type: SmokeOnTrigger
keysIn:
- reaction
duration: 10
spreadAmount: 30
smokePrototype: TearGasSmokeWhite
solution:
reagents:
- ReagentId: TearGas # burning oil is probably toxic and not good for your eyes
Quantity: 20
- type: Fixtures
fixtures:
fix1:
@ -43,68 +57,40 @@
- type: DeepFryer
blacklist:
components:
# The classic.
- NukeDisk
# SliceableFood is handled easily enough, but there's not much to be
# gained by doing special handling for Stacks, especially since most
# of the items that use Stack aren't even remotely edible.
- Stack
- Openable
whitelist:
components:
# It's what meat is.
- BodyPart
# Some clothes, shoes, uniforms.
- Butcherable
# It's already food.
- Food
- Seed
# A good source of fiber.
- Paper
# May as well let actual garbage get turned into food.
- SpaceGarbage
tags:
- Recyclable
- Trash
- Document
charredPrototype: FoodBadRecipe
goodReagents:
- ReagentId: Omnizine
Quantity: 1
badReagents:
- ReagentId: Toxin
Quantity: 2
wasteReagents:
- ReagentId: Charcoal
Quantity: 0.5
- ReagentId: Ash
Quantity: 0.5
# What food items are actually going to be improved by deep-frying?
# This is based on flavor profiles.
goodFlavors:
- bread
- bun
- donk
- dough
- fishy
- meaty
- pasta
- potatoes
- tofu
- onion
- mushroom
badFlavors:
- acid
- chocolate
- gunpowder
- minty
- raisins
- tea
- terrible
- Edible
solution: vat_oil
fryingOils:
- Cornoil
- OilGhee
- OilOlive
- Cornoil
- OilGhee
- OilOlive
containerName: fryer_basket
minimumOilVolume: 25
oilQuality: 1.0
oilDegradationPerRecipe: 0.05
foulOilBurnChance: 0.3
oilQualityFlavors:
Pristine:
- savory
- crispy
Clean:
- oily
Used:
- oily
- stale
Dirty:
- burnt
- bitter
- rancid
Foul:
- burnt
- bitter
- rancid
- awful
- type: SolutionContainerManager
solutions:
vat_oil:
@ -121,12 +107,6 @@
edible: Drink
solution: vat_oil
- type: Appearance
- type: ActivatableUI
key: enum.DeepFryerUiKey.Key
- type: UserInterface
interfaces:
enum.DeepFryerUiKey.Key:
type: DeepFryerBoundUserInterface
- type: Destructible
thresholds:
- trigger:
@ -143,15 +123,15 @@
- !type:ChangeConstructionNodeBehavior
node: machineFrame
- type: ApcPowerReceiver
powerLoad: 300
powerLoad: 2000
- type: Machine
board: DeepFryerMachineCircuitboard
- type: EmptyOnMachineDeconstruct
containers:
- vat_entities
- fryer_basket
- type: ContainerContainer
containers:
vat_entities: !type:Container
fryer_basket: !type:Container
machine_board: !type:Container
machine_parts: !type:Container
- type: PowerSwitch

View File

@ -227,3 +227,28 @@
id: mliko
flavorType: Complex
description: flavor-complex-mliko
- type: flavor
id: crispy
flavorType: Base
description: flavor-base-crispy
- type: flavor
id: stale
flavorType: Base
description: flavor-base-stale
- type: flavor
id: burnt
flavorType: Base
description: flavor-base-burnt
- type: flavor
id: rancid
flavorType: Base
description: flavor-base-rancid
- type: flavor
id: awful
flavorType: Base
description: flavor-base-awful

View File

@ -0,0 +1,5 @@
- type: guideEntry
id: DeepFriedRecipes
name: guide-entry-deepfried-recipes
text: "/ServerInfo/Guidebook/_DV/Service/DeepFriedRecipes.xml"
filterEnabled: True

View File

@ -0,0 +1,114 @@
- type: microwaveMealRecipe
id: RecipeFries
name: space fries recipe
result: FoodMealFries
secretRecipe: true
time: 15
group: DeepFried
deepFried: true
solids:
FoodPotato: 1
- type: deepFryerRecipe
id: DeepFryerRecipeFries
baseRecipe: RecipeFries
burnTime: 10
- type: microwaveMealRecipe
id: RecipeCheesyFries
name: cheesy fries recipe
result: FoodMealFriesCheesy
secretRecipe: true
time: 10
group: DeepFried
deepFried: true
solids:
FoodPotato: 1
FoodCheeseSlice: 1
- type: deepFryerRecipe
id: DeepFryerRecipeCheesyFries
baseRecipe: RecipeCheesyFries
burnTime: 10
- type: microwaveMealRecipe
id: RecipeCarrotFries
name: carrot fries recipe
result: FoodMealFriesCarrot
secretRecipe: true
time: 15
group: DeepFried
deepFried: true
solids:
FoodCarrot: 1
- type: deepFryerRecipe
id: DeepFryerRecipeCarrotFries
baseRecipe: RecipeCarrotFries
burnTime: 10
- type: microwaveMealRecipe
id: RecipeNachos # Renamed to tortilla chips (nachos have cheese)
name: nachos recipe
result: FoodMealNachos
secretRecipe: true
time: 10
group: DeepFried
deepFried: true
solids:
FoodDoughTortillaFlat: 1
- type: deepFryerRecipe
id: DeepFryerRecipeTortillaChips
baseRecipe: RecipeNachos
burnTime: 15
- type: microwaveMealRecipe
id: RecipeNachosCheesy
name: cheesy nachos recipe
result: FoodMealNachosCheesy
secretRecipe: true
time: 10
group: DeepFried
deepFried: true
solids:
FoodCheeseSlice: 1
FoodDoughTortillaFlat: 1
- type: deepFryerRecipe
id: DeepFryerRecipeNachosCheesy
baseRecipe: RecipeNachosCheesy
burnTime: 15
- type: microwaveMealRecipe
id: RecipeNachosCuban
name: cuban nachos recipe
result: FoodMealNachosCuban
secretRecipe: true
time: 10
group: DeepFried
deepFried: true
solids:
FoodChiliPepper: 1
FoodDoughTortillaFlat: 1
- type: deepFryerRecipe
id: DeepFryerRecipeNachosCuban
baseRecipe: RecipeNachosCuban
burnTime: 15
- type: microwaveMealRecipe
id: RecipeFriedChicken
name: fried chicken recipe
result: FoodMeatChickenFried
secretRecipe: true
time: 30
group: DeepFried
deepFried: true
solids:
FoodMeatChicken: 1
- type: deepFryerRecipe
id: DeepFryerRecipeFriedChicken
baseRecipe: RecipeFriedChicken
burnTime: 45

View File

@ -49,4 +49,7 @@ This is the latest published version of NanoTrasen Central Command Kitchen de Cu
## Secret
<GuideMicrowaveGroupEmbed Group="Secret"/>
## Deep-Fried
<GuideMicrowaveGroupEmbed Group="DeepFried"/>
</Document>

View File

@ -0,0 +1,9 @@
<Document>
# Deep-Fried Recipes
Microwaves can't cook these, you need to use a deep fryer.
<GuideMicrowaveGroupEmbed Group="DeepFried"/>
</Document>