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:
parent
4b4c2ef973
commit
fb98bfe575
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared._DV.Kitchen.Systems;
|
||||
|
||||
namespace Content.Client._DV.Kitchen;
|
||||
|
||||
public sealed class DeepFryerSystem : SharedDeepFryerSystem;
|
||||
|
|
@ -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-";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
namespace Content.Server.Kitchen.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class ProfessionalChefComponent : Component {}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
namespace Content.Shared.Nyanotrasen.Kitchen;
|
||||
|
||||
public abstract class SharedDeepfryerSystem : EntitySystem
|
||||
{
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -37,6 +37,7 @@
|
|||
- OtherRecipes
|
||||
- MedicinalRecipes
|
||||
- SecretRecipes
|
||||
- DeepFriedRecipes # DeltaV
|
||||
|
||||
- type: guideEntry
|
||||
id: PizzaRecipes
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
- type: guideEntry
|
||||
id: DeepFriedRecipes
|
||||
name: guide-entry-deepfried-recipes
|
||||
text: "/ServerInfo/Guidebook/_DV/Service/DeepFriedRecipes.xml"
|
||||
filterEnabled: True
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<Document>
|
||||
|
||||
# Deep-Fried Recipes
|
||||
|
||||
Microwaves can't cook these, you need to use a deep fryer.
|
||||
|
||||
<GuideMicrowaveGroupEmbed Group="DeepFried"/>
|
||||
|
||||
</Document>
|
||||
Loading…
Reference in New Issue