This commit is contained in:
Joe 2026-08-14 01:46:09 +00:00 committed by GitHub
commit eb60ae8019
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1573 additions and 4 deletions

View File

@ -80,12 +80,14 @@ public static class StylesheetHelpers
public static MutableSelector HorizontalAlignment(this MutableSelector selector, Control.HAlignment val)
{
return selector.Prop(nameof(Control.HorizontalExpand), val);
// DeltaV - Fix HorizontalAlignment being misapplied to HorizontalExpand
return selector.Prop(nameof(Control.HorizontalAlignment), val);
}
public static MutableSelector VerticalAlignment(this MutableSelector selector, Control.VAlignment val)
{
return selector.Prop(nameof(Control.VerticalExpand), val);
// DeltaV - Fix VerticalAlignment being misapplied to VerticalExpand
return selector.Prop(nameof(Control.VerticalAlignment), val);
}
public static MutableSelector AlignMode(this MutableSelector selector, Label.AlignMode mode)

View File

@ -0,0 +1,49 @@
using System.Linq;
using Content.Client._DV.Kitchen.UI;
using Content.Shared._DV.Kitchen.BUI;
using Content.Shared._DV.Kitchen.Systems;
using Robust.Client.Player;
using Robust.Client.UserInterface;
namespace Content.Client._DV.Kitchen.BUI;
public sealed class DeepFryerBoundUserInterface : BoundUserInterface
{
private DeepFryerWindow? _window;
[Dependency]
private IPlayerManager _player = default!;
public DeepFryerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<DeepFryerWindow>();
_window.OnFoodItemPressed += (item) =>
{
SendPredictedMessage(new DeepFryerTryEjectItemMessage(EntMan.GetNetEntity(item), EntMan.GetNetEntity(_player.LocalEntity)));
};
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window is null || state is not DeepFryerBoundUserInterfaceState deepFryerState)
return;
_window.OilQuality = deepFryerState.OilQuality;
_window.SolutionColor = deepFryerState.SolutionColor;
_window.MinimumVolume = deepFryerState.MinimumVolume;
_window.SolutionVolume = deepFryerState.SolutionVolume;
_window.SolutionMaxVolume = deepFryerState.SolutionMaxVolume;
_window.CookingItems = [..deepFryerState.CookingItems.Select(EntMan.GetEntity)];
_window.IsPowered = deepFryerState.IsPowered;
_window.Capacity = deepFryerState.Capacity;
}
}

View File

@ -0,0 +1,18 @@
<controls:FryerBaskets
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client._DV.Kitchen.UI.Controls">
<ScrollContainer VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
ReserveScrollbarSpace="True">
<BoxContainer Name="ContentsContainer"
StyleClasses="FryerBasketsContainer"
Orientation="Horizontal"
Align="Begin"
VerticalAlignment="Top">
</BoxContainer>
</ScrollContainer>
</controls:FryerBaskets>

View File

@ -0,0 +1,193 @@
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.Kitchen.UI.Controls;
[GenerateTypedNameReferences]
public sealed partial class FryerBaskets : PanelContainer
{
public const string ContentContainerStyleClass = "FryerBasketsContainer";
[Dependency]
private readonly IEntityManager _entityManager = default!;
private readonly SpriteSystem _sprite;
private readonly List<FryerItemButton> _itemControls = [];
private readonly EntityQuery<MetaDataComponent> _metadataQuery;
/// <summary>
/// Event raised when pressing a <see cref="FryerItemButton"/> that <b>does</b> have an assigned <see cref="FryerItemButton.ItemEntity"/>.
/// </summary>
[PublicAPI]
public event Action<EntityUid>? OnFoodItemPressed;
/// <summary>
/// Event raised when pressing a <see cref="FryerItemButton"/> that <b>does not</b> have an assigned <see cref="FryerItemButton.ItemEntity"/>.
/// </summary>
[PublicAPI]
public event Action? OnEmptyItemPressed;
/// <summary>
/// The maximum number of items that can be inside the deep fryer.
/// </summary>
[PublicAPI]
public int Capacity
{
get;
set
{
if (field == value)
return;
field = value;
BindValues();
}
} = 3;
/// <summary>
/// The read-only list of <see cref="EntityUid"/>s inside the deep fryer.
/// </summary>
[PublicAPI]
public IReadOnlyList<EntityUid> Contents
{
get;
set
{
if (Equals(field, value))
return;
field = value;
BindValues();
}
} = [];
/// <summary>
/// When true, buttons for empty fryer item slots will be shown.
/// </summary>
[PublicAPI]
public bool ShowEmptyBaskets
{
get;
set
{
if (field == value)
return;
field = value;
BindValues();
}
}
/// <summary>
/// The optional <see cref="LocId"/> of the text to display in empty fryer item buttons.
/// </summary>
[PublicAPI]
public LocId? EmptyBasketText
{
get;
set
{
if (field == value)
return;
field = value;
BindValues();
}
}
/// <summary>
/// The <see cref="BoxContainer.Margin"/> of the container which holds the fryer item buttons.
/// </summary>
[PublicAPI]
public Thickness InnerMargin
{
get => ContentsContainer.Margin;
set => ContentsContainer.Margin = value;
}
public FryerBaskets()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_sprite = _entityManager.System<SpriteSystem>();
_metadataQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
}
private void BindValues()
{
var expectedButtons = Math.Max(Capacity, Contents.Count);
while (_itemControls.Count < expectedButtons)
{
var newBtn = new FryerItemButton();
newBtn.OnItemPressed += OnItemButtonPressed;
_itemControls.Add(newBtn);
ContentsContainer.AddChild(newBtn);
}
for (var i = 0; i < _itemControls.Count; i++)
{
var fryerItemButton = _itemControls[i];
if (i < Contents.Count)
{
fryerItemButton.ItemEntity = Contents[i];
fryerItemButton.ItemTexture = TryGetTextureForEntity(Contents[i], out var texture) ? texture : null;
fryerItemButton.Align = BoxContainer.AlignMode.Begin;
fryerItemButton.Text = _metadataQuery.TryGetComponent(Contents[i], out var metadata) ? metadata.EntityName : null;
fryerItemButton.Visible = true;
}
else
{
fryerItemButton.ItemEntity = null;
fryerItemButton.ItemTexture = null;
fryerItemButton.Align = BoxContainer.AlignMode.Center;
fryerItemButton.Text = Loc.GetString(EmptyBasketText ?? "deep-fryer-ui-empty");
fryerItemButton.Visible = ShowEmptyBaskets;
}
}
}
private void OnItemButtonPressed(FryerItemButton.ButtonPressedEventArgs args)
{
if(args.IsEmpty)
{
OnEmptyItemPressed?.Invoke();
}
else
{
OnFoodItemPressed?.Invoke(args.ItemEntity.Value);
}
}
private bool TryGetTextureForEntity(EntityUid ent, [NotNullWhen(true)] out Texture? texture)
{
texture = null;
if (_entityManager.Deleted(ent))
return false;
if (_entityManager.TryGetComponent<IconComponent>(ent, out var icon))
{
texture = _sprite.GetIcon(icon);
return true;
}
if (_entityManager.TryGetComponent<SpriteComponent>(ent, out var sprite) && sprite.Icon is not null)
{
texture = sprite.Icon.Default;
return true;
}
return false;
}
}

View File

@ -0,0 +1,22 @@
<controls:FryerItemButton
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client._DV.Kitchen.UI.Controls"
VerticalAlignment="Stretch"
SetSize="150 150"
TrackingTooltip="True">
<BoxContainer Name="Container"
Orientation="Vertical"
Margin="5"
Align="Begin">
<TextureRect Name="ItemIcon"
HorizontalAlignment="Center"
TextureScale="2 2"
Stretch="KeepAspectCentered" />
<RichTextLabel Name="TextLabel"
RectClipContent="True"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"/>
</BoxContainer>
</controls:FryerItemButton>

View File

@ -0,0 +1,106 @@
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.Kitchen.UI.Controls;
[GenerateTypedNameReferences]
public sealed partial class FryerItemButton : ContainerButton
{
/// <summary>
/// Event raised when pressing this button. See <see cref="ButtonPressedEventArgs"/>.
/// </summary>
[PublicAPI]
public event Action<ButtonPressedEventArgs>? OnItemPressed;
/// <summary>
/// The <see cref="EntityUid"/> of the item associated with this button, if any.
/// </summary>
[PublicAPI]
public EntityUid? ItemEntity
{
get;
set
{
if (field == value)
return;
field = value;
BindValues();
}
}
/// <summary>
/// The texture of the item icon. This can be left null for a text-only button.
/// </summary>
[PublicAPI]
public Texture? ItemTexture
{
get => ItemIcon.Texture;
set => ItemIcon.Texture = value;
}
/// <summary>
/// The text to display on the button (e.g., the item's name). This is optional and can be left null for an icon-only button.
/// </summary>
[PublicAPI]
public string? Text
{
get => TextLabel.Text;
set => TextLabel.Text = value;
}
/// <summary>
/// The alignment of the button's contents along it's orientation axis.
/// </summary>
[PublicAPI]
public BoxContainer.AlignMode Align
{
get => Container.Align;
set => Container.Align = value;
}
public FryerItemButton()
{
RobustXamlLoader.Load(this);
OnPressed += OnButtonPressed;
}
private void OnButtonPressed(ButtonEventArgs args)
{
OnItemPressed?.Invoke(new ButtonPressedEventArgs(this, args.Event, ItemEntity));
}
private void BindValues()
{
ToolTip = ItemEntity.HasValue ? Loc.GetString("deep-fryer-eject-item", ("item", ItemEntity)) : null;
HideTooltip();
}
/// <summary>
/// Event arguments for the <see cref="FryerItemButton.OnItemPressed"/> event.
/// </summary>
/// <param name="button"></param>
/// <param name="args"></param>
/// <param name="itemEntity"></param>
public sealed class ButtonPressedEventArgs(BaseButton button, GUIBoundKeyEventArgs args, EntityUid? itemEntity) : ButtonEventArgs(button, args)
{
/// <summary>
/// The <see cref="EntityUid"/> of the item associated with this button, if any.
/// </summary>
[PublicAPI]
public EntityUid? ItemEntity { get; } = itemEntity;
/// <summary>
/// Whether the button is empty (i.e., has no item entity).
/// </summary>
[PublicAPI]
[MemberNotNullWhen(false, nameof(ItemEntity))]
public bool IsEmpty => ItemEntity is null;
}
}

View File

@ -0,0 +1,79 @@
<kitchenUi:DeepFryerWindow
xmlns="https://spacestation14.io"
xmlns:kitchenUi="clr-namespace:Content.Client._DV.Kitchen.UI"
xmlns:dv="clr-namespace:Content.Client._DV.UserInterfaces.Controls"
xmlns:kitchenControls="clr-namespace:Content.Client._DV.Kitchen.UI.Controls"
Resizable="True"
MouseFilter="Pass"
MinSize="385 310"
SetSize="550 310">
<PanelContainer StyleClasses="BackgroundPanelOpenLeft" />
<BoxContainer Name="WindowPadding"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
Margin="10">
<BoxContainer Orientation="Horizontal" SeparationOverride="5" SetHeight="60" Margin="0 0 0 10">
<!-- Header Area -->
<BoxContainer Orientation="Horizontal" VerticalAlignment="Center" Margin="32 0 0 0">
<Label Text="{Loc deep-fryer-ui-power-text}" VerticalAlignment="Center" />
<dv:StatusLight
Name="PowerIndicator"
SetSize="24 24"
Margin="10"
ActiveColorOverride="#FF000066"
VerticalAlignment="Center"
IsOn="True" />
</BoxContainer>
<Label StyleClasses="LabelHeading"
HorizontalExpand="True"
Align="Center"
VerticalAlignment="Center"
Text="{Loc deep-fryer-ui-title}" />
<BoxContainer Orientation="Horizontal" VerticalAlignment="Center" Margin="0 0 10 0">
<dv:StatusLight
Name="OilIndicator"
StyleClasses="BlinkingAnimation:Slow"
SetSize="24 24"
Margin="10"
ActiveColorOverride="#FFFF0066"
VerticalAlignment="Center"
IsOn="True" />
<Label Text="{Loc deep-fryer-ui-oil-text}" VerticalAlignment="Center" />
</BoxContainer>
<TextureButton Name="CloseButton"
SetSize="22 22"
StyleClasses="windowCloseButton"
VerticalAlignment="Center" />
</BoxContainer>
<Control Name="BasketControl"
VerticalExpand="True"
HorizontalExpand="True"
TrackingTooltip="True"
MouseFilter="Stop">
<dv:AdvancedProgressBar Name="OilMeter"
VerticalExpand="True"
HorizontalExpand="True"
Orientation="BottomToTop"
InsideMargin="5" />
<kitchenControls:FryerBaskets Name="FryerBaskets"
VerticalAlignment="Top"
ShowEmptyBaskets="True"
MinHeight="180"
InnerMargin="15 15 15 5"
Margin="20"/>
</Control>
</BoxContainer>
</kitchenUi:DeepFryerWindow>

View File

@ -0,0 +1,248 @@
using System.Numerics;
using Content.Client._DV.Kitchen.UI.Controls;
using Content.Shared._DV.Kitchen.Components;
using Content.Shared._DV.Kitchen.Systems;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
namespace Content.Client._DV.Kitchen.UI;
[GenerateTypedNameReferences]
public sealed partial class DeepFryerWindow : BaseWindow
{
private const int DragMarginSize = 7;
[Dependency]
private readonly IEntityManager _entityManager = default!;
private readonly SharedDeepFryerSystem _deepFryer;
private FormattedMessage? _oilQualityTooltip;
/// <summary>
/// Event raised when pressing a <see cref="FryerItemButton"/> with an assigned <see cref="FryerItemButton.ItemEntity"/>.
/// </summary>
[PublicAPI]
public event Action<EntityUid>? OnFoodItemPressed;
/// <summary>
/// Event raised when pressing a <see cref="FryerItemButton"/> without an assigned <see cref="FryerItemButton.ItemEntity"/>.
/// </summary>
[PublicAPI]
public event Action? OnEmptyItemPressed;
/// <summary>
/// The quality value of the oil.
/// </summary>
[PublicAPI]
public float OilQuality
{
get;
set
{
if (MathHelper.CloseTo(field, value))
return;
field = value;
BindValues();
}
} = 1.0f;
/// <summary>
/// The minimum volume of the deep fryer's oil solution to allow cooking.
/// </summary>
[PublicAPI]
public FixedPoint2 MinimumVolume
{
get;
set
{
if (field == value)
return;
field = value;
BindValues();
}
}
/// <summary>
/// The current color of the deep fryer's oil solution.
/// </summary>
[PublicAPI]
public Color SolutionColor
{
get;
set
{
if (field == value)
return;
field = value;
BindValues();
}
} = Color.Yellow;
/// <summary>
/// The current volume of the deep fryer's oil solution.
/// </summary>
[PublicAPI]
public FixedPoint2 SolutionVolume
{
get;
set
{
if (field == value)
return;
field = value;
BindValues();
}
}
/// <summary>
/// The maximum volume of the deep fryer's oil solution.
/// </summary>
[PublicAPI]
public FixedPoint2 SolutionMaxVolume
{
get;
set
{
if (field == value)
return;
field = value;
BindValues();
}
}
/// <summary>
/// The maximum number of items that can be inside the deep fryer.
/// </summary>
[PublicAPI]
public int Capacity
{
get;
set
{
if (field == value)
return;
field = value;
BindValues();
}
} = 3;
/// <summary>
/// Whether the deep fryer is powered or not.
/// </summary>
[PublicAPI]
public bool IsPowered
{
get;
set
{
field = value;
BindValues();
}
}
/// <summary>
/// The entities contained in the deep fryer.
/// </summary>
[PublicAPI]
public IReadOnlyList<EntityUid> CookingItems { get; set; } = [];
/// <summary>
/// The <see cref="OilQuality"/> level of the oil, as determined by <see cref="SharedDeepFryerSystem"/>.
/// </summary>
private OilQuality OilQualityCategory => SharedDeepFryerSystem.GetOilQualityLevel(OilQuality);
public DeepFryerWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_deepFryer = _entityManager.System<SharedDeepFryerSystem>();
BasketControl.TooltipSupplier = BuildOilQualityTooltip;
CloseButton.OnPressed += _ => Close();
FryerBaskets.OnEmptyItemPressed += () => OnEmptyItemPressed?.Invoke();
FryerBaskets.OnFoodItemPressed += entity => OnFoodItemPressed?.Invoke(entity);
BindValues();
}
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
{
var mode = DragMode.Move;
if (Resizable)
{
if (relativeMousePos.Y < DragMarginSize)
{
mode = DragMode.Top;
}
else if (relativeMousePos.Y > Size.Y - DragMarginSize)
{
mode = DragMode.Bottom;
}
if (relativeMousePos.X < DragMarginSize)
{
mode |= DragMode.Left;
}
else if (relativeMousePos.X > Size.X - DragMarginSize)
{
mode |= DragMode.Right;
}
}
return mode;
}
private (Color color, string labelName) GetOilQualityInfo() => _deepFryer.GetOilQualityInfo(OilQualityCategory);
private Tooltip? BuildOilQualityTooltip(Control control)
{
if (SolutionVolume.Value == 0 || _oilQualityTooltip is null) return null;
var tooltip = new Tooltip
{
Tracking = true
};
tooltip.SetMessage(_oilQualityTooltip);
return tooltip;
}
private void BindValues()
{
PowerIndicator.IsOn = IsPowered;
OilMeter.MaximumValue = SolutionMaxVolume.Value;
OilMeter.Value = SolutionVolume.Value;
OilMeter.ForegroundColorOverride = SolutionColor.WithAlpha(0.8f);
OilIndicator.IsOn = IsPowered && SolutionVolume < MinimumVolume;
OilIndicator.ToolTip = OilIndicator.IsOn ? Loc.GetString("deep-fryer-ui-oil-tooltip", ("units", MinimumVolume)) : null;
var (color, labelName) = GetOilQualityInfo();
_oilQualityTooltip = FormattedMessage.FromMarkupPermissive(Loc.GetString("deep-fryer-oil-quality-examine",
("color", color.ToHex()),
("state", labelName)));
BasketControl.HideTooltip();
FryerBaskets.Contents = CookingItems;
FryerBaskets.Capacity = Capacity;
}
}

View File

@ -0,0 +1,54 @@
using Content.Client._DV.UserInterfaces.Controls;
using Content.Client.Stylesheets;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using static Content.Client.Stylesheets.StylesheetHelpers;
namespace Content.Client._DV.Stylesheets.Sheetlets;
[CommonSheetlet]
public sealed class AdvancedProgressBarSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet
{
public override StyleRule[] GetRules(T sheet, object config)
{
var colorable = new StyleBoxFlat(Color.White);
return
[
E<AdvancedProgressBar>()
.ParentOf(E<PanelContainer>())
.Panel(colorable),
E<AdvancedProgressBar>()
.Prop(AdvancedProgressBar.StylePropertyBackgroundColor, sheet.PrimaryPalette.BackgroundDark)
.Prop(AdvancedProgressBar.StylePropertyForegroundColor, sheet.PrimaryPalette.Base),
E<AdvancedProgressBar>()
.Pseudo(AdvancedProgressBar.StylePseudoClassLeftToRight)
.ParentOf(E<PanelContainer>().Class(AdvancedProgressBar.StyleClassForegroundPanelContainer))
.HorizontalAlignment(Control.HAlignment.Left)
.VerticalAlignment(Control.VAlignment.Stretch),
E<AdvancedProgressBar>()
.Pseudo(AdvancedProgressBar.StylePseudoClassRightToLeft)
.ParentOf(E<PanelContainer>().Class(AdvancedProgressBar.StyleClassForegroundPanelContainer))
.HorizontalAlignment(Control.HAlignment.Right)
.VerticalAlignment(Control.VAlignment.Stretch),
E<AdvancedProgressBar>()
.Pseudo(AdvancedProgressBar.StylePseudoClassTopToBottom)
.ParentOf(E<PanelContainer>().Class(AdvancedProgressBar.StyleClassForegroundPanelContainer))
.HorizontalAlignment(Control.HAlignment.Stretch)
.VerticalAlignment(Control.VAlignment.Top),
E<AdvancedProgressBar>()
.Pseudo(AdvancedProgressBar.StylePseudoClassBottomToTop)
.ParentOf(E<PanelContainer>().Class(AdvancedProgressBar.StyleClassForegroundPanelContainer))
.HorizontalAlignment(Control.HAlignment.Stretch)
.VerticalAlignment(Control.VAlignment.Bottom),
];
}
}

View File

@ -0,0 +1,42 @@
using Content.Client._DV.Kitchen.UI.Controls;
using Content.Client.Stylesheets;
using Content.Client.Stylesheets.SheetletConfigs;
using Content.Client.Stylesheets.Sheetlets;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using static Content.Client.Stylesheets.StylesheetHelpers;
namespace Content.Client._DV.Stylesheets.Sheetlets;
[CommonSheetlet]
public sealed class DeepFryerSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet, IButtonConfig, IIconConfig
{
public override StyleRule[] GetRules(T sheet, object config)
{
var itemWrapperBox = new StyleBoxFlat(sheet.SecondaryPalette.BackgroundDark.WithAlpha(0.5f));
var buttonBox = new StyleBoxFlat(sheet.SecondaryPalette.BackgroundLight.WithAlpha(0.90f));
var rules = new List<StyleRule>
{
E<FryerBaskets>()
.Panel(itemWrapperBox),
E<BoxContainer>()
.Class(FryerBaskets.ContentContainerStyleClass)
.Prop(BoxContainer.StylePropertySeparation, 5),
E<FryerItemButton>()
.Box(buttonBox),
E<FryerItemButton>()
.ParentOf(E<BoxContainer>())
.Prop(BoxContainer.StylePropertySeparation, 5),
};
ButtonSheetlet<T>
.MakeButtonRules<FryerItemButton>(rules, sheet.SecondaryPalette, null);
return rules.ToArray();
}
}

View File

@ -0,0 +1,89 @@
using Content.Client._DV.UserInterfaces.Controls;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Robust.Client.Animations;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Animations;
using static Content.Client.Stylesheets.StylesheetHelpers;
namespace Content.Client._DV.Stylesheets.Sheetlets;
[CommonSheetlet]
public sealed class StatusLightSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet
{
private const string DefaultBaseLightResPath = "/Textures/Interface/WireHacking/light_off_base.svg.96dpi.png";
private const string DefaultActiveLightResPath = "/Textures/Interface/WireHacking/light_on_base.svg.96dpi.png";
private readonly Animation _blinkingFastAnimation = new()
{
Length = TimeSpan.FromSeconds(0.2),
AnimationTracks =
{
new AnimationTrackControlProperty
{
Property = nameof(Control.Modulate),
InterpolationMode = AnimationInterpolationMode.Linear,
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(Color.White, 0f),
new AnimationTrackProperty.KeyFrame(Color.Transparent, 0.1f),
new AnimationTrackProperty.KeyFrame(Color.White, 0.1f)
}
}
}
};
private readonly Animation _blinkingSlowAnimation = new()
{
Length = TimeSpan.FromSeconds(0.8),
AnimationTracks =
{
new AnimationTrackControlProperty
{
Property = nameof(Control.Modulate),
InterpolationMode = AnimationInterpolationMode.Linear,
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(Color.White, 0f),
new AnimationTrackProperty.KeyFrame(Color.White, 0.3f),
new AnimationTrackProperty.KeyFrame(Color.Transparent, 0.1f),
new AnimationTrackProperty.KeyFrame(Color.Transparent, 0.3f),
new AnimationTrackProperty.KeyFrame(Color.White, 0.1f),
}
}
}
};
public override StyleRule[] GetRules(T sheet, object config)
{
return
[
E<StatusLight>()
.Prop(StatusLight.StylePropertyBlinkingAnimation, false)
.Prop(StatusLight.StylePropertyActiveColor, Color.Green.WithAlpha(0.3f))
.Prop(StatusLight.StylePropertyBaseColor, Color.FromHex("#202020")),
E<StatusLight>()
.ParentOf(E<TextureRect>().Class(StatusLight.StyleClassBaseLight))
.Prop(TextureRect.StylePropertyTexture, ResCache.GetTexture(DefaultBaseLightResPath)),
E<StatusLight>()
.ParentOf(E<TextureRect>().Class(StatusLight.StyleClassActiveLight))
.Prop(TextureRect.StylePropertyTexture, ResCache.GetTexture(DefaultActiveLightResPath)),
/* Animation Classes */
E<StatusLight>()
.Class(StatusLight.StyleClassFastBlinking)
.Pseudo(StatusLight.StylePseudoClassIsOn)
.Prop(StatusLight.StylePropertyBlinkingAnimation, _blinkingFastAnimation),
E<StatusLight>()
.Class(StatusLight.StyleClassSlowBlinking)
.Pseudo(StatusLight.StylePseudoClassIsOn)
.Prop(StatusLight.StylePropertyBlinkingAnimation, _blinkingSlowAnimation)
];
}
}

View File

@ -0,0 +1,8 @@
<dv:AdvancedProgressBar
xmlns="https://spacestation14.io"
xmlns:dv="clr-namespace:Content.Client._DV.UserInterfaces.Controls">
<PanelContainer Name="Background"
StyleClasses="BackgroundPanelContainer" />
<PanelContainer Name="Foreground"
StyleClasses="ForegroundPanelContainer"/>
</dv:AdvancedProgressBar>

View File

@ -0,0 +1,298 @@
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client._DV.UserInterfaces.Controls;
[GenerateTypedNameReferences]
public sealed partial class AdvancedProgressBar : Control
{
/// <summary>
/// Style Class for targeting this control's Background <see cref="PanelContainer"/>
/// </summary>
[PublicAPI]
public const string StyleClassBackgroundPanelContainer = "BackgroundPanelContainer";
/// <summary>
/// Style class for targeting this control's Foreground <see cref="PanelContainer"/>
/// </summary>
[PublicAPI]
public const string StyleClassForegroundPanelContainer = "ForegroundPanelContainer";
/// <summary>
/// Style Property for setting the background color of the progress bar.
/// </summary>
[PublicAPI]
public const string StylePropertyBackgroundColor = "Modulate:Background";
/// <summary>
/// Style Property for setting the foreground color of the progress bar.
/// </summary>
[PublicAPI]
public const string StylePropertyForegroundColor = "Modulate:Foreground";
/// <summary>
/// Style Pseudo Class for the current orientation of the progress bar.
/// </summary>
[PublicAPI]
public const string StylePseudoClassLeftToRight = ":LeftToRight";
/// <summary>
/// Style Pseudo Class for the current orientation of the progress bar.
/// </summary>
[PublicAPI]
public const string StylePseudoClassRightToLeft = ":RightToLeft";
/// <summary>
/// Style Pseudo Class for the current orientation of the progress bar.
/// </summary>
[PublicAPI]
public const string StylePseudoClassTopToBottom = ":TopToBottom";
/// <summary>
/// Style Pseudo Class for the current orientation of the progress bar.
/// </summary>
[PublicAPI]
public const string StylePseudoClassBottomToTop = ":BottomToTop";
/// <summary>
/// Style Pseudo Class for when the progress bar's value is at its minimum.
/// </summary>
[PublicAPI]
public const string StylePseudoClassEmpty = ":Empty";
/// <summary>
/// Style Pseudo Class for when the progress bar's value is at its maximum.
/// </summary>
[PublicAPI]
public const string StylePseudoClassFull = ":Full";
/// <summary>
/// Style Pseudo Class for when the progress bar's percentage is in the range of [1%, 25%).
/// </summary>
[PublicAPI]
public const string StylePseudoClassFirstQuartile = ":FirstQuartile";
/// <summary>
/// Style Pseudo Class for when the progress bar's percentage is in the range of [25%, 50%).
/// </summary>
[PublicAPI]
public const string StylePseudoClassSecondQuartile = ":SecondQuartile";
/// <summary>
/// Style Pseudo Class for when the progress bar's percentage is in the range of [50%, 75%).
/// </summary>
[PublicAPI]
public const string StylePseudoClassThirdQuartile = ":ThirdQuartile";
/// <summary>
/// Style Pseudo Class for when the progress bar's percentage is in the range of [75%, 100%).
/// </summary>
[PublicAPI]
public const string StylePseudoClassFourthQuartile = ":FourthQuartile";
public enum BarOrientation : byte
{
RightToLeft,
LeftToRight,
TopToBottom,
BottomToTop
}
[PublicAPI]
public BarOrientation Orientation
{
get;
set
{
if (field == value) return;
field = value;
UpdateStylePseudoClasses();
}
} = BarOrientation.LeftToRight;
[PublicAPI]
public int MinimumValue
{
get;
set
{
if (field == value) return;
field = value;
UpdateStylePseudoClasses();
}
} = 0;
[PublicAPI]
public int MaximumValue
{
get;
set
{
if (field == value) return;
field = value;
UpdateStylePseudoClasses();
}
} = 100;
[PublicAPI]
public int Value
{
get;
set
{
if (field == value) return;
field = value;
UpdateStylePseudoClasses();
}
} = 0;
[PublicAPI]
public float Percent => MaximumValue == 0 ? 0 : (float) (Value - MinimumValue) / MaximumValue;
[PublicAPI]
public Color? ForegroundColorOverride
{
get;
set
{
if (field == value) return;
field = value;
InvalidateStyleSheet();
}
}
[PublicAPI]
public Color? BackgroundColorOverride
{
get;
set
{
if (field == value) return;
field = value;
InvalidateStyleSheet();
}
}
[PublicAPI]
public Thickness InsideMargin
{
get => Foreground.Margin;
set => Foreground.Margin = value;
}
public AdvancedProgressBar()
{
RobustXamlLoader.Load(this);
UpdateStylePseudoClasses();
}
protected override void StylePropertiesChanged()
{
Foreground.ModulateSelfOverride = GetActualForegroundColor();
Background.ModulateSelfOverride = GetActualBackgroundColor();
base.StylePropertiesChanged();
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
// Returning zero here makes it so we can reliably use this control's width/height to scale the size of the "bar"
return Vector2.Zero;
}
protected override void FrameUpdate(FrameEventArgs args)
{
switch (Orientation)
{
default:
case BarOrientation.LeftToRight:
case BarOrientation.RightToLeft:
Foreground.SetWidth = (Width - InsideMargin.SumHorizontal) * Percent;
Foreground.SetHeight = float.NaN;
break;
case BarOrientation.TopToBottom:
case BarOrientation.BottomToTop:
Foreground.SetWidth = float.NaN;
Foreground.SetHeight = (Height - InsideMargin.SumVertical) * Percent;
break;
}
base.FrameUpdate(args);
}
private void UpdateStylePseudoClasses()
{
var pseudo = Orientation switch
{
BarOrientation.LeftToRight => StylePseudoClassLeftToRight,
BarOrientation.RightToLeft => StylePseudoClassRightToLeft,
BarOrientation.TopToBottom => StylePseudoClassTopToBottom,
BarOrientation.BottomToTop => StylePseudoClassBottomToTop,
_ => throw new ArgumentOutOfRangeException()
};
SetOnlyStylePseudoClass(pseudo);
Value = Math.Clamp(Value, MinimumValue, MaximumValue);
if (Value == MinimumValue)
{
AddStylePseudoClass(StylePseudoClassEmpty);
}
else if (Value == MaximumValue)
{
AddStylePseudoClass(StylePseudoClassFull);
}
else
{
switch (Percent)
{
case < 0.25f:
AddStylePseudoClass(StylePseudoClassFirstQuartile);
break;
case < 0.5f:
AddStylePseudoClass(StylePseudoClassSecondQuartile);
break;
case < 0.75f:
AddStylePseudoClass(StylePseudoClassThirdQuartile);
break;
default:
AddStylePseudoClass(StylePseudoClassFourthQuartile);
break;
}
}
InvalidateStyleSheet();
}
private Color? GetActualBackgroundColor()
{
if (BackgroundColorOverride is not null) return BackgroundColorOverride.Value;
if (TryGetStyleProperty<Color>(StylePropertyBackgroundColor, out var backgroundColor)) return backgroundColor;
return null;
}
private Color? GetActualForegroundColor()
{
if (ForegroundColorOverride is not null) return ForegroundColorOverride.Value;
if (TryGetStyleProperty<Color>(StylePropertyForegroundColor, out var foregroundColor)) return foregroundColor;
return null;
}
}

View File

@ -0,0 +1,24 @@
<controls:StatusLight
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client._DV.UserInterfaces.Controls"
MouseFilter="Pass">
<TextureRect Name="BaseLight"
StyleClasses="StatusLight.BaseLight"
TextureScale="2 2"
Stretch="KeepAspectCentered"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" />
<TextureRect Name="ActiveLight"
StyleClasses="StatusLight.ActiveLight"
TextureScale="2 2"
Stretch="KeepAspectCentered"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" />
</controls:StatusLight>

View File

@ -0,0 +1,230 @@
using JetBrains.Annotations;
using Robust.Client.Animations;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.UserInterfaces.Controls;
[GenerateTypedNameReferences]
public sealed partial class StatusLight : Control
{
private const string BlinkingAnimationKey = "Blinking";
/// <summary>
/// Style Class for the Status Light which sets the blinking animation to On 100ms, Off 100ms
/// </summary>
[PublicAPI]
public const string StyleClassFastBlinking = "BlinkingAnimation:Fast";
/// <summary>
/// Style Class for the Status Light which sets the blinking animation to On 400ms, Off 400ms
/// </summary>
[PublicAPI]
public const string StyleClassSlowBlinking = "BlinkingAnimation:Slow";
/// <summary>
/// Style Class for targeting this control's Active Light <see cref="TextureRect"/>.
/// </summary>
[PublicAPI]
public const string StyleClassActiveLight = "StatusLight.ActiveLight";
/// <summary>
/// Style Class for targeting this control's Base Light <see cref="TextureRect"/>.
/// </summary>
[PublicAPI]
public const string StyleClassBaseLight = "StatusLight.BaseLight";
/// <summary>
/// Style Pseudo Class for when the light is on. This is mutually exclusive with <see cref="StylePseudoClassIsOff"/>.
/// </summary>
[PublicAPI]
public const string StylePseudoClassIsOn = ":IsOn";
/// <summary>
/// Style Pseudo Class for when the light is off. This is mutually exclusive with <see cref="StylePseudoClassIsOn"/>.
/// </summary>
[PublicAPI]
public const string StylePseudoClassIsOff = ":IsOff";
/// <summary>
/// Style Property for setting the blinking animation.
/// </summary>
[PublicAPI]
public const string StylePropertyBlinkingAnimation = "Animation:Blinking";
/// <summary>
/// Style Property for setting the active color of the light. This is the color shown when the light is on.
/// </summary>
[PublicAPI]
public const string StylePropertyActiveColor = "Modulate:Light";
/// <summary>
/// Style Property for setting the base color of the light. This is the color shown when the light is off.
/// </summary>
[PublicAPI]
public const string StylePropertyBaseColor = "Modulate:Base";
/// <summary>
/// Whether the indicator should be lit.
/// </summary>
[PublicAPI]
public bool IsOn
{
get;
set
{
if (field == value) return;
field = value;
UpdateStylePseudoClasses();
}
}
/// <summary>
/// The color of the indicator when it is on.
/// </summary>
[PublicAPI]
public Color? ActiveColorOverride
{
get;
set
{
if (field == value) return;
field = value;
InvalidateStyleSheet();
}
}
/// <summary>
/// The base color of the indicator, also shown when it is off.
/// </summary>
[PublicAPI]
public Color? BaseColorOverride
{
get;
set
{
if (field == value) return;
field = value;
InvalidateStyleSheet();
}
}
/// <summary>
/// The <see cref="Texture"/> used by the Active Light.
/// </summary>
[PublicAPI]
public Texture? ActiveLightTextureOverride
{
get => ActiveLight.Texture;
set => ActiveLight.Texture = value;
}
/// <summary>
/// The <see cref="Texture"/> used by the Base Light.
/// </summary>
[PublicAPI]
public Texture? BaseLightTextureOverride
{
get => BaseLight.Texture;
set => BaseLight.Texture = value;
}
/// <summary>
/// The blinking animation to use for the Active Light.
/// </summary>
[PublicAPI]
public Animation? BlinkingAnimationOverride
{
get;
set
{
if (field == value) return;
field = value;
InvalidateStyleSheet();
}
}
public StatusLight()
{
RobustXamlLoader.Load(this);
UpdateStylePseudoClasses();
ActiveLight.AnimationCompleted += ActiveLightOnAnimationCompleted;
}
protected override void StylePropertiesChanged()
{
if (GetActualBaseColor() is var baseColor) BaseLight.ModulateSelfOverride = baseColor;
if (GetActualActiveColor() is var activeColor) ActiveLight.ModulateSelfOverride = activeColor;
SetActiveLightAnimation();
base.StylePropertiesChanged();
}
private void UpdateStylePseudoClasses()
{
SetOnlyStylePseudoClass(IsOn ? StylePseudoClassIsOn : StylePseudoClassIsOff);
ActiveLight.Visible = IsOn;
InvalidateStyleSheet();
}
/// <summary>
/// Reset the blinking animation based on the current style and overrides. If there is no blinking animation, stop any currently playing animation.
/// </summary>
private void SetActiveLightAnimation()
{
if (GetActualBlinkingAnimation() is not { } blinkingAnimation)
{
ActiveLight.StopAnimation(BlinkingAnimationKey);
}
else if (!ActiveLight.HasRunningAnimation(BlinkingAnimationKey))
{
ActiveLight.PlayAnimation(blinkingAnimation, BlinkingAnimationKey);
}
}
/// <summary>
/// Handler for the AnimationCompleted delegate.
/// </summary>
/// <param name="animationId"></param>
private void ActiveLightOnAnimationCompleted(string animationId)
{
SetActiveLightAnimation();
}
private Animation? GetActualBlinkingAnimation()
{
if (BlinkingAnimationOverride is not null) return BlinkingAnimationOverride;
if (TryGetStyleProperty<Animation>(StylePropertyBlinkingAnimation, out var blinkingAnimation)) return blinkingAnimation;
return null;
}
private Color GetActualActiveColor()
{
if (ActiveColorOverride is not null) return ActiveColorOverride.Value;
if (TryGetStyleProperty<Color>(StylePropertyActiveColor, out var activeColor)) return activeColor;
return Color.White;
}
private Color GetActualBaseColor()
{
if (BaseColorOverride is not null) return BaseColorOverride.Value;
if (TryGetStyleProperty<Color>(StylePropertyBaseColor, out var baseColor)) return baseColor;
return Color.White;
}
}

View File

@ -1,5 +1,6 @@
using System.Linq;
using Content.Shared._DV.Kitchen;
using Content.Shared._DV.Kitchen.BUI;
using Content.Shared._DV.Kitchen.Components;
using Content.Shared._DV.Kitchen.Systems;
using Content.Shared.Audio;
@ -11,6 +12,7 @@ using Content.Shared.Power;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Throwing;
using Content.Shared.Trigger.Systems;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
@ -30,6 +32,7 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedPowerReceiverSystem _power = default!;
[Dependency] private readonly TriggerSystem _trigger = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
/// <summary>
/// The trigger key used when non-frying oil reagents are added to the fryer
@ -46,11 +49,24 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem
SubscribeLocalEvent<DeepFryerComponent, EntInsertedIntoContainerMessage>(OnItemInserted);
SubscribeLocalEvent<DeepFryerComponent, EntRemovedFromContainerMessage>(OnItemRemoved);
SubscribeLocalEvent<DeepFryerComponent, BoundUIOpenedEvent>(OnDeepFryerUIOpened);
SubscribeLocalEvent<DeepFryerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<DeepFryerComponent, SolutionContainerChangedEvent>(OnSolutionChanged);
SubscribeLocalEvent<DeepFryerComponent, SolutionTransferredEvent>(OnSolutionTransferred);
SubscribeLocalEvent<DeepFryerComponent, ThrowHitByEvent>(OnThrowHitBy);
}
private void OnSolutionChanged(Entity<DeepFryerComponent> ent, ref SolutionContainerChangedEvent args)
{
// This event also fires for drinking the oil (which does not count as transferring from the solution)
UpdateUserInterfaceState(ent);
}
private void OnDeepFryerUIOpened(Entity<DeepFryerComponent> ent, ref BoundUIOpenedEvent args)
{
UpdateUserInterfaceState(ent);
}
private void OnSolutionTransferred(Entity<DeepFryerComponent> ent, ref SolutionTransferredEvent args)
{
// Only restore quality when oil is being added TO the fryer (not removed from it)
@ -91,6 +107,7 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem
{
UpdateAppearance(ent);
ResetCookingItemsStartTime(ent);
UpdateUserInterfaceState(ent);
}
private void OnThrowHitBy(Entity<DeepFryerComponent> ent, ref ThrowHitByEvent args)
@ -129,6 +146,7 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem
}
UpdateAppearance(ent);
UpdateUserInterfaceState(ent);
}
private void OnItemRemoved(Entity<DeepFryerComponent> ent, ref EntRemovedFromContainerMessage args)
@ -141,6 +159,7 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem
ent.Comp.CookingItems.Remove(args.Entity);
UpdateAppearance(ent);
UpdateUserInterfaceState(ent);
}
/// <summary>
@ -167,6 +186,30 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem
_ambientSound.SetAmbience(ent, value);
}
private void UpdateUserInterfaceState(Entity<DeepFryerComponent> ent)
{
if (!Solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var solution))
return;
if (_uiSystem.HasUi(ent, DeepFryerUiKey.DeepFryer))
{
_uiSystem.SetUiState(ent.Owner, DeepFryerUiKey.DeepFryer, new DeepFryerBoundUserInterfaceState
{
IsPowered = _power.IsPowered(ent.Owner),
OilQuality = ent.Comp.OilQuality,
CookingItems = [..ent.Comp.CookingItems.Keys.Select(item => EntityManager.GetNetEntity(item))],
Capacity = ent.Comp.MaxItems,
MinimumVolume = ent.Comp.MinimumOilVolume,
SolutionMaxVolume = solution.MaxVolume,
SolutionVolume = solution.Volume,
SolutionColor = solution.GetColor(_prototype),
});
}
}
/// <summary>
/// Finds the best recipe for a single item.
/// Prioritizes multi-ingredient recipes (returns null so item waits), then single-ingredient recipes.

View File

@ -0,0 +1,23 @@
using Content.Shared.FixedPoint;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.Kitchen.BUI;
[NetSerializable, Serializable]
public sealed class DeepFryerBoundUserInterfaceState : BoundUserInterfaceState
{
public required NetEntity[] CookingItems { get; set; }
public float OilQuality { get; set; }
public FixedPoint2 MinimumVolume { get; set; }
public FixedPoint2 SolutionVolume { get; set; }
public FixedPoint2 SolutionMaxVolume { get; set; }
public int Capacity { get; set; }
public Color SolutionColor { get; set; }
public bool IsPowered { get; set; }
}

View File

@ -28,6 +28,8 @@ public abstract class SharedDeepFryerSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<DeepFryerComponent, DeepFryerTryEjectItemMessage>(OnUiItemEjected);
SubscribeLocalEvent<DeepFryerComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<DeepFryerComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<DeepFryerComponent, DeepFryerInsertDoAfterEvent>(OnInsertDoAfter);
@ -129,6 +131,17 @@ public abstract class SharedDeepFryerSystem : EntitySystem
}
}
private void OnUiItemEjected(Entity<DeepFryerComponent> ent, ref DeepFryerTryEjectItemMessage args)
{
if (!TryGetEntity(args.Item, out var itemEnt))
return;
if (!TryGetEntity(args.User, out var userEnt))
return;
TryEjectItem(ent, itemEnt.Value, userEnt.Value);
}
/// <summary>
/// Attempts to eject an item from the deep fryer
/// </summary>
@ -219,7 +232,7 @@ public abstract class SharedDeepFryerSystem : EntitySystem
if (!_container.Insert(item, container))
return false;
Popup.PopupClient(Loc.GetString("deep-fryer-insert-item", ("item", item)), ent, user ?? EntityUid.Invalid);
Popup.PopupClient(Loc.GetString("deep-fryer-insert-item-success", ("item", item)), ent, user ?? EntityUid.Invalid);
return true;
}
@ -318,3 +331,17 @@ public abstract class SharedDeepFryerSystem : EntitySystem
[Serializable, NetSerializable]
public sealed partial class DeepFryerInsertDoAfterEvent : SimpleDoAfterEvent;
[Serializable, NetSerializable]
public sealed class DeepFryerTryEjectItemMessage(NetEntity item, NetEntity? user) : BoundUserInterfaceMessage
{
public NetEntity Item { get; set; } = item;
public NetEntity? User { get; set; } = user;
}
[Serializable, NetSerializable]
public enum DeepFryerUiKey : byte
{
DeepFryer
}

View File

@ -4,7 +4,8 @@ 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-insert-item = Insert {THE($item)}
deep-fryer-insert-item-success = 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!
@ -20,3 +21,10 @@ deep-fryer-oil-quality-used = used
deep-fryer-oil-quality-dirty = dirty
deep-fryer-oil-quality-foul = foul
deep-fryer-oil-quality-unknown = unknown
## UI Text
deep-fryer-ui-title = Mr. Fry
deep-fryer-ui-power-text = Power
deep-fryer-ui-oil-text = Add Oil
deep-fryer-ui-oil-tooltip = At least {$units}u of oil must be in the fryer.
deep-fryer-ui-empty = [bold]Empty Basket[/bold]

View File

@ -125,6 +125,12 @@
node: machineFrame
- type: ApcPowerReceiver
powerLoad: 2000
- type: ActivatableUI
key: enum.DeepFryerUiKey.DeepFryer
- type: UserInterface
interfaces:
enum.DeepFryerUiKey.DeepFryer:
type: DeepFryerBoundUserInterface
- type: Machine
board: DeepFryerMachineCircuitboard
- type: EmptyOnMachineDeconstruct