Merge 5d49458da0 into fd5b16abb3
This commit is contained in:
commit
eb60ae8019
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue