diff --git a/Content.Client/VendingMachines/UI/VendingMachineItem.xaml b/Content.Client/VendingMachines/UI/VendingMachineItem.xaml
deleted file mode 100644
index 2ad19f7255f..00000000000
--- a/Content.Client/VendingMachines/UI/VendingMachineItem.xaml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs b/Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs
deleted file mode 100644
index a7212934fd8..00000000000
--- a/Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.VendingMachines.UI;
-
-[GenerateTypedNameReferences]
-public sealed partial class VendingMachineItem : BoxContainer
-{
- public VendingMachineItem(EntProtoId entProto, string text)
- {
- RobustXamlLoader.Load(this);
-
- ItemPrototype.SetPrototype(entProto);
-
- NameLabel.Text = text;
- }
-}
diff --git a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml
deleted file mode 100644
index 59e0b0d890b..00000000000
--- a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
deleted file mode 100644
index 0ea8fddae26..00000000000
--- a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
+++ /dev/null
@@ -1,256 +0,0 @@
-using System.Numerics;
-using Content.Client.UserInterface.Controls;
-using Content.Shared.VendingMachines;
-using Content.Shared.Cargo.Components;
-using Content.Shared.Stacks;
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.Controls;
-using Content.Shared._NF.Bank; // Frontier
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Prototypes;
-using Content.Shared.Chemistry.Components.SolutionManager;
-using Content.Shared.Chemistry.Reagent;
-using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
-using Robust.Client.UserInterface;
-using Content.Shared.IdentityManagement;
-using Robust.Client.Graphics;
-
-namespace Content.Client.VendingMachines.UI
-{
- [GenerateTypedNameReferences]
- public sealed partial class VendingMachineMenu : FancyWindow
- {
- [Dependency] private IPrototypeManager _prototypeManager = default!;
- [Dependency] private IEntityManager _entityManager = default!;
- private readonly Dictionary _dummies = [];
-
- public event Action? OnItemSelected;
-
- private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
-
- public VendingMachineMenu()
- {
- MinSize = SetSize = new Vector2(250, 150);
- RobustXamlLoader.Load(this);
- IoCManager.InjectDependencies(this);
-
- VendingContents.SearchBar = SearchBar;
- VendingContents.DataFilterCondition += DataFilterCondition;
- VendingContents.GenerateItem += GenerateButton;
- VendingContents.ItemKeyBindDown += (args, data) => OnItemSelected?.Invoke(args, data);
- }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
-
- // Don't clean up dummies during disposal or we'll just have to spawn them again
- if (!disposing)
- return;
-
- // Delete any dummy items we spawned
- foreach (var entity in _dummies.Values)
- {
- _entityManager.QueueDeleteEntity(entity);
- }
- _dummies.Clear();
- }
-
- private bool DataFilterCondition(string filter, ListData data)
- {
- if (data is not VendorItemsListData { ItemText: var text })
- return false;
-
- if (string.IsNullOrEmpty(filter))
- return true;
-
- return text.Contains(filter, StringComparison.CurrentCultureIgnoreCase);
- }
-
- private void GenerateButton(ListData data, ListContainerButton button)
- {
- if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text })
- return;
-
- button.AddChild(new VendingMachineItem(protoID, text));
-
- button.ToolTip = text;
- button.StyleBoxOverride = _styleBox;
- }
-
- ///
- /// Populates the list of available items on the vending machine interface
- /// and sets icons based on their prototypes
- ///
- public void Populate(List inventory, float priceModifier, int balance, int? cashSlotBalance, bool requiresCash) // Frontier: add balance, cashSlotBalance
- {
- UpdateBalance(balance); // Frontier
- UpdateCashSlotBalance(cashSlotBalance); // Frontier
-
- if (inventory.Count == 0 && VendingContents.Visible)
- {
- SearchBar.Visible = false;
- VendingContents.Visible = false;
-
- var outOfStockLabel = new Label()
- {
- Text = Loc.GetString("vending-machine-component-try-eject-out-of-stock"),
- Margin = new Thickness(4, 4),
- HorizontalExpand = true,
- VerticalAlignment = VAlignment.Stretch,
- HorizontalAlignment = HAlignment.Center
- };
-
- MainContainer.AddChild(outOfStockLabel);
-
- SetSizeAfterUpdate(outOfStockLabel.Text.Length, 0);
-
- return;
- }
-
- var longestEntry = string.Empty;
- var listData = new List();
-
- for (var i = 0; i < inventory.Count; i++)
- {
- var entry = inventory[i];
-
- if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
- continue;
-
- if (!_dummies.TryGetValue(entry.ID, out var dummy))
- {
- dummy = _entityManager.Spawn(entry.ID);
- _dummies.Add(entry.ID, dummy);
- }
-
- var itemName = Identity.Name(dummy, _entityManager);
- var cost = 0; // mono
- if (requiresCash) // frontier
- cost = GetPrice(entry, prototype, priceModifier);
-
- string itemText;
-
- // Frontier: unlimited vending
- if (entry.Amount != uint.MaxValue)
- itemText = $"[{BankSystemExtensions.ToSpesoString(cost)}] {itemName} [{entry.Amount}]";
- else
- itemText = $"[{BankSystemExtensions.ToSpesoString(cost)}] {itemName}";
- // End Frontier: unlimited vending
-
- if (itemText.Length > longestEntry.Length)
- longestEntry = itemText;
-
- listData.Add(new VendorItemsListData(prototype!.ID, itemText, i)); // Frontier: prototype(out var priceComponent, _entityManager.ComponentFactory))
- {
- if (priceComponent.Price != 0)
- {
- var price = (float)priceComponent.Price;
- cost = (int)(price * priceModifier);
- }
- else
- {
- if (prototype.TryGetComponent(out var stackPrice, _entityManager.ComponentFactory)
- && prototype.TryGetComponent(out var stack, _entityManager.ComponentFactory))
- {
- var price = stackPrice.Price * stack.Count;
- cost = (int)(price * priceModifier);
- }
- else
- cost = (int)(cost * priceModifier);
- }
- }
- else
- cost = (int)(cost * priceModifier);
-
- if (prototype != null && prototype.TryGetComponent(out var priceSolutions, _entityManager.ComponentFactory))
- {
- if (priceSolutions.Solutions != null)
- {
- foreach (var solution in priceSolutions.Solutions.Values)
- {
- foreach (var (reagent, quantity) in solution.Contents)
- {
- if (!_prototypeManager.TryIndex(reagent.Prototype,
- out var reagentProto))
- continue;
-
- // TODO check ReagentData for price information?
- var costReagent = quantity.Float() * reagentProto.PricePerUnit;
- cost += (int)(costReagent * priceModifier);
- }
- }
- }
- }
- // End Frontier: item pricing
-
- // Frontier: calculate vending price (this duplicates Content.Server.PricingSystem.GetVendPrice - this should be moved to Content.Shared if possible)
- if (prototype != null)
- {
- var price = 0.0;
-
- if (prototype.TryGetComponent(out var staticComp, _entityManager.ComponentFactory) && staticComp.VendPrice > 0.0)
- {
- price += staticComp.VendPrice;
- }
- else if (prototype.TryGetComponent(out var stackComp, _entityManager.ComponentFactory) && stackComp.VendPrice > 0.0)
- {
- price += stackComp.VendPrice;
- }
-
- // If there is anything that explicitly sets vending price - higher OR lower, override the base.
- if (price > 0.0)
- {
- cost = (int)price;
- }
- }
-
- return cost;
- // End Frontier
- }
-
- // Frontier
- public void UpdateBalance(int balance)
- {
- BalanceLabel.Text = BankSystemExtensions.ToSpesoString(balance);
- }
-
- public void UpdateCashSlotBalance(int? balance)
- {
- CashSlotControls.Visible = balance != null;
- if (balance != null)
- CashSlotLabel.Text = BankSystemExtensions.ToSpesoString(balance.Value);
- }
- // End Frontier
-
- private void SetSizeAfterUpdate(int longestEntryLength, int contentCount)
- {
- SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400),
- Math.Clamp(contentCount * 50, 150, 350));
- }
- }
-}
-
-public record VendorItemsListData(EntProtoId ItemProtoID, string ItemText, int ItemIndex) : ListData;
diff --git a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
index 6b83cccf6de..04719def2d1 100644
--- a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
+++ b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
@@ -1,9 +1,6 @@
-using Content.Client.UserInterface.Controls;
-using Content.Client.VendingMachines.UI;
+using Content.Client._Lua.VendingMachines; // LuaM
using Content.Shared.VendingMachines;
using Robust.Client.UserInterface;
-using Robust.Shared.Input;
-using System.Linq;
using Robust.Client.GameObjects;
using Content.Shared._NF.Bank.Components; // Frontier
using Content.Shared.Containers.ItemSlots; // Frontier
@@ -14,7 +11,7 @@ namespace Content.Client.VendingMachines
public sealed class VendingMachineBoundUserInterface : BoundUserInterface
{
[ViewVariables]
- private VendingMachineMenu? _menu;
+ private LuaVendingMachineWindow? _menu; // LuaM: VendingMachineMenu > LuaVendingMachineWindow
[ViewVariables]
private List _cachedInventory = new();
@@ -49,7 +46,7 @@ protected override void Open()
_mod = market.Mod;
// End Frontier
- _menu = this.CreateWindowCenteredLeft();
+ _menu = this.CreateWindowCenteredLeft(); // LuaM: VendingMachineMenu > LuaVendingMachineWindow
// Frontier: no exceptions
if (EntMan.TryGetComponent(Owner, out MetaDataComponent? meta))
_menu.Title = meta.EntityName;
@@ -62,6 +59,9 @@ protected override void Open()
public void Refresh()
{
+ if (_menu == null || !EntMan.HasComponent(Owner)) // LuaM
+ return; // LuaM
+
var system = EntMan.System();
_cachedInventory = system.GetAllInventory(Owner);
@@ -86,26 +86,13 @@ public void Refresh()
}
// End Frontier
- _menu?.Populate(_cachedInventory, _mod, _balance, cashSlotValue, _requiresCash); // Frontier: add _balance, mono: add _requiresCash
+ var enabled = vendingMachine is { Ejecting: false }; // LuaM
+ _menu?.Populate(_cachedInventory, enabled, _mod, _balance, cashSlotValue, _requiresCash); // Frontier: add _balance, mono: add _requiresCash // LuaM: add enabled
}
- private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
+ private void OnItemSelected(InventoryType type, string id) // LuaM: GUIBoundKeyEventArgs, ListData > InventoryType, string
{
- if (args.Function != EngineKeyFunctions.UIClick)
- return;
-
- if (data is not VendorItemsListData { ItemIndex: var itemIndex })
- return;
-
- if (_cachedInventory.Count == 0)
- return;
-
- var selectedItem = _cachedInventory.ElementAtOrDefault(itemIndex);
-
- if (selectedItem == null)
- return;
-
- SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
+ SendMessage(new VendingMachineEjectMessage(type, id));
}
protected override void Dispose(bool disposing)
diff --git a/Content.Client/_Lua/AmmoLoader/UI/MarqueeLabel.cs b/Content.Client/_Lua/AmmoLoader/UI/MarqueeLabel.cs
index 0c18c820f86..94c74b827be 100644
--- a/Content.Client/_Lua/AmmoLoader/UI/MarqueeLabel.cs
+++ b/Content.Client/_Lua/AmmoLoader/UI/MarqueeLabel.cs
@@ -1,9 +1,6 @@
-// LuaCorp - This file is licensed under AGPLv3
-// Copyright (c) 2026 LuaCorp Contributors
-// See AGPLv3.txt for details.
-
using System;
using System.Numerics;
+using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
@@ -15,10 +12,26 @@ public sealed class MarqueeLabel : Control
private const float ScrollSpeedPx = 28f;
private const float EndPauseSeconds = 1.25f;
private const float GapPx = 32f;
+ private const float StartPauseSeconds = 0.35f;
private readonly Label _label;
private float _offset;
private float _pauseRemaining;
private bool _scrollingForward = true;
+ private bool _scrollEnabled = true;
+ public bool ScrollEnabled
+ {
+ get => _scrollEnabled;
+ set
+ {
+ if (_scrollEnabled == value)
+ return;
+
+ _scrollEnabled = value;
+ ResetScroll();
+ if (value)
+ _pauseRemaining = StartPauseSeconds;
+ }
+ }
public string? Text
{
@@ -37,6 +50,12 @@ public Color? FontColorOverride
set => _label.FontColorOverride = value;
}
+ public Font? FontOverride
+ {
+ get => _label.FontOverride;
+ set => _label.FontOverride = value;
+ }
+
public MarqueeLabel()
{
RectClipContent = true;
@@ -78,6 +97,8 @@ protected override Vector2 ArrangeOverride(Vector2 finalSize)
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
+ if (!_scrollEnabled)
+ return;
var avail = Size.X;
var textWidth = _label.DesiredSize.X;
if (textWidth <= avail + 0.5f)
diff --git a/Content.Client/_Lua/Styles/LunaWindow.cs b/Content.Client/_Lua/Styles/LunaWindow.cs
new file mode 100644
index 00000000000..7644c0ea8dc
--- /dev/null
+++ b/Content.Client/_Lua/Styles/LunaWindow.cs
@@ -0,0 +1,18 @@
+using Content.Client.UserInterface.Controls;
+
+namespace Content.Client._Lua.Styles;
+
+[Virtual]
+public class LunaWindow : FancyWindow
+{
+ protected void ApplyLunaChrome()
+ {
+ LunaWindowStyle.ApplyWindowChrome(this);
+ }
+
+ protected override void OnThemeUpdated()
+ {
+ base.OnThemeUpdated();
+ LunaWindowStyle.ApplyWindowChrome(this);
+ }
+}
diff --git a/Content.Client/_Lua/Styles/LunaWindowStyle.cs b/Content.Client/_Lua/Styles/LunaWindowStyle.cs
new file mode 100644
index 00000000000..d8642943c3e
--- /dev/null
+++ b/Content.Client/_Lua/Styles/LunaWindowStyle.cs
@@ -0,0 +1,115 @@
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client._Lua.Styles;
+
+public static class LunaWindowStyle
+{
+ public static readonly Color FrameBg = Color.FromHex("#0B0F14");
+ public static readonly Color FrameBorder = Color.FromHex("#243041");
+ public static readonly Color TitleBarBg = Color.FromHex("#0E1218");
+ public static readonly Color PanelBg = Color.FromHex("#0E1218");
+ public static readonly Color PanelBorder = Color.FromHex("#2A3344");
+ public static readonly Color Divider = Color.FromHex("#1C2433");
+
+ public static readonly Color TextMuted = Color.FromHex("#7E8BA0");
+ public static readonly Color TextSecondary = Color.FromHex("#8A96A8");
+ public static readonly Color TextPrimary = Color.FromHex("#C5CEDB");
+
+ public static readonly Color Accent = Color.FromHex("#5EC8E8");
+ public static readonly Color AccentWarn = Color.FromHex("#E8A43A");
+ public static readonly Color AccentBad = Color.FromHex("#E07070");
+
+ private static Font? _fontSmall;
+ private static Font? _fontBody;
+ private static Font? _fontTitle;
+ private static Font? _fontFooter;
+
+ public static Font FontSmall => _fontSmall ??= Cache().NotoStack(size: 10);
+ public static Font FontBody => _fontBody ??= Cache().NotoStack(size: 12);
+ public static Font FontTitle => _fontTitle ??= Cache().NotoStack(variation: "Bold", size: 13);
+ public static Font FontFooter => _fontFooter ??= Cache().NotoStack(size: 8);
+
+ private static IResourceCache Cache() => IoCManager.Resolve();
+
+ public static StyleBoxFlat Box(Color background, Color border, float borderThickness = 1f) => new()
+ {
+ BackgroundColor = background,
+ BorderColor = border,
+ BorderThickness = new Thickness(borderThickness),
+ };
+
+ public static StyleBoxFlat Shell() => Box(FrameBg, FrameBorder);
+
+ public static StyleBoxFlat ThinDivider() => new()
+ {
+ BackgroundColor = Divider,
+ BorderColor = Color.Transparent,
+ BorderThickness = new Thickness(0),
+ ContentMarginTopOverride = 1,
+ ContentMarginBottomOverride = 1,
+ };
+
+ public static void ApplyWindowChrome(FancyWindow window)
+ {
+ if (window.ChildCount < 2)
+ return;
+
+ if (window.GetChild(0) is PanelContainer frame)
+ {
+ frame.ModulateSelfOverride = Color.White;
+ frame.PanelOverride = Box(FrameBg, FrameBorder);
+ }
+
+ if (window.GetChild(1) is not BoxContainer layout || layout.ChildCount < 2)
+ return;
+
+ var titleHost = layout.GetChild(0);
+ if (titleHost.ChildCount > 0 && titleHost.GetChild(0) is PanelContainer titlePanel)
+ {
+ titlePanel.ModulateSelfOverride = Color.White;
+ titlePanel.PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = TitleBarBg,
+ BorderColor = FrameBorder,
+ BorderThickness = new Thickness(0, 0, 0, 1),
+ };
+ }
+
+ if (titleHost.ChildCount > 1 &&
+ titleHost.GetChild(1) is BoxContainer titleRow &&
+ titleRow.ChildCount > 0 &&
+ titleRow.GetChild(0) is Label titleLabel)
+ {
+ titleLabel.FontOverride = FontTitle;
+ titleLabel.FontColorOverride = TextPrimary;
+ }
+
+ if (layout.GetChild(1) is PanelContainer divider)
+ divider.PanelOverride = ThinDivider();
+ }
+
+ public static void StyleDivider(PanelContainer panel) => panel.PanelOverride = ThinDivider();
+
+ public static void StyleMuted(Label label) => StyleSmall(label, TextMuted);
+
+ public static void StyleSecondary(Label label) => StyleSmall(label, TextSecondary);
+
+ public static void StyleValue(Label label) => StyleSmall(label, Accent);
+
+ public static void StyleFooter(Label label)
+ {
+ label.FontOverride = FontFooter;
+ label.FontColorOverride = TextMuted;
+ }
+
+ private static void StyleSmall(Label label, Color color)
+ {
+ label.AddStyleClass(StyleNano.StyleClassLabelSmall);
+ label.FontOverride = FontSmall;
+ label.FontColorOverride = color;
+ }
+}
diff --git a/Content.Client/_Lua/VendingMachines/LuaVendingMachineWindow.xaml b/Content.Client/_Lua/VendingMachines/LuaVendingMachineWindow.xaml
new file mode 100644
index 00000000000..f6e2d441ec4
--- /dev/null
+++ b/Content.Client/_Lua/VendingMachines/LuaVendingMachineWindow.xaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Lua/VendingMachines/LuaVendingMachineWindow.xaml.cs b/Content.Client/_Lua/VendingMachines/LuaVendingMachineWindow.xaml.cs
new file mode 100644
index 00000000000..93d71e7980e
--- /dev/null
+++ b/Content.Client/_Lua/VendingMachines/LuaVendingMachineWindow.xaml.cs
@@ -0,0 +1,225 @@
+using System.Linq;
+using Content.Client._Lua.Styles;
+using Content.Client._Lua.VendingMachines.UI;
+using Content.Shared._NF.Bank;
+using Content.Shared.Cargo.Components;
+using Content.Shared.Chemistry.Components.SolutionManager;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Stacks;
+using Content.Shared.VendingMachines;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client._Lua.VendingMachines;
+
+[GenerateTypedNameReferences]
+public sealed partial class LuaVendingMachineWindow : LunaWindow
+{
+ [Dependency] private IPrototypeManager _prototypeManager = default!;
+ [Dependency] private IComponentFactory _componentFactory = default!;
+
+ private readonly List<((InventoryType Type, string Id) Key, LuaVendingEntry Row)> _rows = new();
+
+ public event Action? OnItemSelected;
+
+ public LuaVendingMachineWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ ApplyLunaChrome();
+
+ Shell.PanelOverride = LunaWindowStyle.Shell();
+ LunaWindowStyle.StyleDivider(HeaderDivider);
+ LunaWindowStyle.StyleDivider(FooterDivider);
+ LunaWindowStyle.StyleMuted(BalanceCaption);
+ LunaWindowStyle.StyleValue(BalanceLabel);
+ LunaWindowStyle.StyleMuted(CashSlotCaption);
+ LunaWindowStyle.StyleValue(CashSlotLabel);
+ LunaWindowStyle.StyleSecondary(SearchLabel);
+ LunaWindowStyle.StyleMuted(EmptyLabel);
+ LunaWindowStyle.StyleFooter(FlavorLeft);
+ LunaWindowStyle.StyleFooter(FlavorRight);
+
+ Search.OnTextChanged += args => ApplySearchFilter(args.Text);
+ }
+
+ public void Populate(List inventory,
+ bool enabled,
+ float priceModifier,
+ int balance,
+ int? cashSlotBalance,
+ bool requiresCash)
+ {
+ UpdateBalance(balance);
+ UpdateCashSlotBalance(cashSlotBalance);
+
+ var hasItems = inventory.Count > 0;
+ Search.Visible = hasItems;
+ SearchLabel.Visible = hasItems;
+ CatalogScroll.Visible = hasItems;
+ EmptyLabel.Visible = !hasItems;
+
+ if (SameItems(inventory))
+ {
+ for (var i = 0; i < inventory.Count; i++)
+ {
+ if (_prototypeManager.TryIndex(inventory[i].ID, out var prototype))
+ ApplyRowState(_rows[i].Row, inventory[i], prototype, enabled, priceModifier, requiresCash);
+ }
+
+ return;
+ }
+
+ _rows.Clear();
+ Entries.DisposeAllChildren();
+
+ foreach (var entry in inventory)
+ {
+ if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
+ continue;
+
+ var row = CreateRow(entry, prototype);
+ ApplyRowState(row, entry, prototype, enabled, priceModifier, requiresCash);
+ _rows.Add(((entry.Type, entry.ID), row));
+ Entries.AddChild(row);
+ }
+
+ ApplySearchFilter(Search.Text);
+ }
+
+ public void UpdateBalance(int balance)
+ {
+ BalanceLabel.Text = BankSystemExtensions.ToSpesoString(balance);
+ }
+
+ public void UpdateCashSlotBalance(int? balance)
+ {
+ CashSlotControls.Visible = balance != null;
+ if (balance != null)
+ CashSlotLabel.Text = BankSystemExtensions.ToSpesoString(balance.Value);
+ }
+
+ private bool SameItems(List inventory)
+ {
+ if (_rows.Count != inventory.Count)
+ return false;
+
+ for (var i = 0; i < inventory.Count; i++)
+ {
+ if (_rows[i].Key != (inventory[i].Type, inventory[i].ID))
+ return false;
+ }
+
+ return true;
+ }
+
+ private LuaVendingEntry CreateRow(VendingMachineInventoryEntry entry, EntityPrototype prototype)
+ {
+ var row = new LuaVendingEntry();
+ row.Icon.SetPrototype(prototype.ID);
+ row.Panel.Title.Text = prototype.Name;
+
+ switch (entry.Type)
+ {
+ case InventoryType.Emagged:
+ row.Panel.BorderColor = LunaWindowStyle.AccentBad;
+ row.Panel.HoveredColor = LunaWindowStyle.AccentBad;
+ break;
+ case InventoryType.Contraband:
+ row.Panel.BorderColor = LunaWindowStyle.AccentWarn;
+ row.Panel.HoveredColor = LunaWindowStyle.AccentWarn;
+ break;
+ }
+
+ var name = prototype.Name;
+ var description = prototype.Description;
+ row.Panel.Button.TooltipDelay = 0;
+ row.Panel.Button.TooltipSupplier = _ =>
+ {
+ var tooltipText = new FormattedMessage();
+ tooltipText.AddText(name);
+ if (!string.IsNullOrWhiteSpace(description))
+ {
+ tooltipText.PushNewline();
+ tooltipText.AddText(description);
+ }
+
+ var tooltip = new Tooltip();
+ tooltip.SetMessage(tooltipText);
+ return tooltip;
+ };
+
+ var type = entry.Type;
+ var id = entry.ID;
+ row.Panel.Button.OnPressed += _ => OnItemSelected?.Invoke(type, id);
+ return row;
+ }
+
+ private void ApplyRowState(LuaVendingEntry row,
+ VendingMachineInventoryEntry entry,
+ EntityPrototype prototype,
+ bool enabled,
+ float priceModifier,
+ bool requiresCash)
+ {
+ var cost = requiresCash ? GetPrice(prototype, priceModifier) : 0; // Mono: free vending shows no price
+ row.Price.Text = BankSystemExtensions.ToSpesoString(cost);
+ row.Amount.Text = entry.Amount == uint.MaxValue ? string.Empty : entry.Amount.ToString(); // Frontier: unlimited vending
+ row.SetDisabled(!enabled || entry.Amount == 0);
+ }
+
+ private void ApplySearchFilter(string? text)
+ {
+ foreach (var (_, row) in _rows)
+ {
+ row.Visible = string.IsNullOrWhiteSpace(text) ||
+ (row.Panel.Title.Text?.Contains(text, StringComparison.CurrentCultureIgnoreCase) ?? false);
+ }
+ }
+
+ private int GetPrice(EntityPrototype prototype, float priceModifier)
+ {
+ var vendPrice = 0.0;
+ if (prototype.TryGetComponent(out var staticVend, _componentFactory) && staticVend.VendPrice > 0.0)
+ vendPrice = staticVend.VendPrice;
+ else if (prototype.TryGetComponent(out var stackVend, _componentFactory) && stackVend.VendPrice > 0.0)
+ vendPrice = stackVend.VendPrice;
+
+ if (vendPrice > 0.0)
+ return (int) vendPrice;
+
+ var cost = 20;
+ if (prototype.TryGetComponent(out var priceComponent, _componentFactory))
+ {
+ if (priceComponent.Price != 0)
+ cost = (int) ((float) priceComponent.Price * priceModifier);
+ else if (prototype.TryGetComponent(out var stackPrice, _componentFactory)
+ && prototype.TryGetComponent(out var stack, _componentFactory))
+ cost = (int) (stackPrice.Price * stack.Count * priceModifier);
+ else
+ cost = (int) (cost * priceModifier);
+ }
+ else
+ {
+ cost = (int) (cost * priceModifier);
+ }
+
+ if (prototype.TryGetComponent(out var solutions, _componentFactory)
+ && solutions.Solutions != null)
+ {
+ foreach (var solution in solutions.Solutions.Values)
+ {
+ foreach (var (reagent, quantity) in solution.Contents)
+ {
+ if (_prototypeManager.TryIndex(reagent.Prototype, out var reagentProto))
+ cost += (int) (quantity.Float() * reagentProto.PricePerUnit * priceModifier);
+ }
+ }
+ }
+
+ return cost;
+ }
+}
diff --git a/Content.Client/_Lua/VendingMachines/UI/LuaVendingButton.xaml b/Content.Client/_Lua/VendingMachines/UI/LuaVendingButton.xaml
new file mode 100644
index 00000000000..751b2b7edf5
--- /dev/null
+++ b/Content.Client/_Lua/VendingMachines/UI/LuaVendingButton.xaml
@@ -0,0 +1,3 @@
+
diff --git a/Content.Client/_Lua/VendingMachines/UI/LuaVendingButton.xaml.cs b/Content.Client/_Lua/VendingMachines/UI/LuaVendingButton.xaml.cs
new file mode 100644
index 00000000000..a6cfd5e94be
--- /dev/null
+++ b/Content.Client/_Lua/VendingMachines/UI/LuaVendingButton.xaml.cs
@@ -0,0 +1,23 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Lua.VendingMachines.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class LuaVendingButton : Button
+{
+ public event Action? OnDrawModeChanged;
+
+ public LuaVendingButton()
+ {
+ RobustXamlLoader.Load(this);
+ Label.Visible = false;
+ ClipText = true;
+ }
+
+ protected override void DrawModeChanged()
+ {
+ OnDrawModeChanged?.Invoke();
+ }
+}
diff --git a/Content.Client/_Lua/VendingMachines/UI/LuaVendingEntry.xaml b/Content.Client/_Lua/VendingMachines/UI/LuaVendingEntry.xaml
new file mode 100644
index 00000000000..0beafb7c627
--- /dev/null
+++ b/Content.Client/_Lua/VendingMachines/UI/LuaVendingEntry.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Lua/VendingMachines/UI/LuaVendingEntry.xaml.cs b/Content.Client/_Lua/VendingMachines/UI/LuaVendingEntry.xaml.cs
new file mode 100644
index 00000000000..9dec745d776
--- /dev/null
+++ b/Content.Client/_Lua/VendingMachines/UI/LuaVendingEntry.xaml.cs
@@ -0,0 +1,28 @@
+using Content.Client._Lua.Styles;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Lua.VendingMachines.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class LuaVendingEntry : Control
+{
+ public LuaVendingEntry()
+ {
+ RobustXamlLoader.Load(this);
+ Price.FontOverride = LunaWindowStyle.FontSmall;
+ Amount.FontOverride = LunaWindowStyle.FontSmall;
+ Panel.Title.FontOverride = LunaWindowStyle.FontBody;
+ SetDisabled(false);
+ }
+
+ public void SetDisabled(bool disabled)
+ {
+ Panel.Button.Disabled = disabled;
+ Amount.FontColorOverride = disabled ? LunaWindowStyle.AccentBad : LunaWindowStyle.TextSecondary;
+ Price.FontColorOverride = disabled ? LunaWindowStyle.TextMuted : LunaWindowStyle.Accent;
+ Panel.Title.FontColorOverride = disabled ? LunaWindowStyle.TextMuted : LunaWindowStyle.TextPrimary;
+ Panel.UpdateColor();
+ }
+}
diff --git a/Content.Client/_Lua/VendingMachines/UI/LuaVendingPanel.xaml b/Content.Client/_Lua/VendingMachines/UI/LuaVendingPanel.xaml
new file mode 100644
index 00000000000..74bd9dc1d46
--- /dev/null
+++ b/Content.Client/_Lua/VendingMachines/UI/LuaVendingPanel.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Lua/VendingMachines/UI/LuaVendingPanel.xaml.cs b/Content.Client/_Lua/VendingMachines/UI/LuaVendingPanel.xaml.cs
new file mode 100644
index 00000000000..9d30cc181bb
--- /dev/null
+++ b/Content.Client/_Lua/VendingMachines/UI/LuaVendingPanel.xaml.cs
@@ -0,0 +1,49 @@
+using Content.Client._Lua.Styles;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Lua.VendingMachines.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class LuaVendingPanel : Control
+{
+ private static readonly Color HoveredBg = Color.FromHex("#163040");
+ private static readonly Color DisabledBg = Color.FromHex("#1C2433");
+
+ public Color BorderColor = LunaWindowStyle.PanelBorder;
+ public Color HoveredColor = LunaWindowStyle.Accent;
+
+ public LuaVendingPanel()
+ {
+ RobustXamlLoader.Load(this);
+ Button.OnDrawModeChanged += UpdateColor;
+
+ Title.ScrollEnabled = false;
+ Button.OnMouseEntered += _ => Title.ScrollEnabled = true;
+ Button.OnMouseExited += _ => Title.ScrollEnabled = false;
+
+ UpdateColor();
+ }
+
+ public void UpdateColor()
+ {
+ var panel = (StyleBoxFlat) Panel.PanelOverride!;
+ if (Button.Disabled)
+ {
+ panel.BackgroundColor = DisabledBg;
+ panel.BorderColor = LunaWindowStyle.TextMuted;
+ return;
+ }
+
+ panel.BackgroundColor = Button.IsHovered ? HoveredBg : LunaWindowStyle.PanelBg;
+ panel.BorderColor = Button.IsHovered ? HoveredColor : BorderColor;
+ }
+
+ protected override void EnteredTree()
+ {
+ base.EnteredTree();
+ UpdateColor();
+ }
+}
diff --git a/Resources/Locale/en-US/_Lua/vending.ftl b/Resources/Locale/en-US/_Lua/vending.ftl
new file mode 100644
index 00000000000..052e2d86e76
--- /dev/null
+++ b/Resources/Locale/en-US/_Lua/vending.ftl
@@ -0,0 +1 @@
+lua-vending-search-label = Search
diff --git a/Resources/Locale/ru-RU/_Lua/vending.ftl b/Resources/Locale/ru-RU/_Lua/vending.ftl
new file mode 100644
index 00000000000..f4285a12715
--- /dev/null
+++ b/Resources/Locale/ru-RU/_Lua/vending.ftl
@@ -0,0 +1 @@
+lua-vending-search-label = Поиск