mirror of
https://gitlab.com/MaddoScientisto/cirnogodot.git
synced 2026-06-01 11:15:33 +00:00
Implemented vending machine
This commit is contained in:
parent
d78daf4e18
commit
e5ffb0cf94
32 changed files with 3074 additions and 1992 deletions
447
Scripts/UI/VendingMachineShopUi.cs
Normal file
447
Scripts/UI/VendingMachineShopUi.cs
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cirno.Scripts.Actors._3D;
|
||||
using Cirno.Scripts.Resources;
|
||||
using Cirno.Scripts.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cirno.Scripts.UI;
|
||||
|
||||
public partial class VendingMachineShopUi : CanvasLayer
|
||||
{
|
||||
private sealed class AmmoPurchasePlan
|
||||
{
|
||||
public required VendingMachine3D.RuntimeShopEntry Entry { get; init; }
|
||||
public required int BundleCount { get; init; }
|
||||
public required int TotalPrice { get; init; }
|
||||
}
|
||||
|
||||
[Signal]
|
||||
public delegate void ClosedEventHandler();
|
||||
|
||||
[Export] public Label CreditsValueLabel { get; private set; }
|
||||
[Export] public ItemList ShopItemList { get; private set; }
|
||||
[Export] public TextureRect PreviewTexture { get; private set; }
|
||||
[Export] public Label ItemNameLabel { get; private set; }
|
||||
[Export] public Label DescriptionLabel { get; private set; }
|
||||
[Export] public Label PriceValueLabel { get; private set; }
|
||||
[Export] public Label StockValueLabel { get; private set; }
|
||||
[Export] public Label OwnedValueLabel { get; private set; }
|
||||
[Export] public Label MaxValueLabel { get; private set; }
|
||||
[Export] public Button AllAmmoButton { get; private set; }
|
||||
[Export] public Button BuyButton { get; private set; }
|
||||
[Export] public Button CancelButton { get; private set; }
|
||||
[Export] public Button CloseButton { get; private set; }
|
||||
|
||||
[Export] public StringName CancelActionName { get; private set; } = "ui_cancel";
|
||||
[Export] public bool AutoFitToViewport { get; private set; }
|
||||
[Export] public Vector2 ShopPanelDesignSize { get; private set; } = new(720f, 380f);
|
||||
[Export(PropertyHint.None, "suffix:px")] public float ViewportMargin { get; private set; } = 24f;
|
||||
|
||||
private readonly List<VendingMachine3D.RuntimeShopEntry> _entries = [];
|
||||
private VendingMachine3D _machine;
|
||||
private bool _isClosing;
|
||||
private bool _isReady;
|
||||
private int _selectedIndex = -1;
|
||||
private PanelContainer _rootPanel;
|
||||
|
||||
public void Init(VendingMachine3D machine)
|
||||
{
|
||||
_machine = machine;
|
||||
|
||||
if (_isReady)
|
||||
{
|
||||
RefreshUi();
|
||||
}
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
ProcessMode = ProcessModeEnum.Always;
|
||||
|
||||
_isReady = true;
|
||||
_rootPanel = GetNode<PanelContainer>("Overlay/PanelContainer");
|
||||
|
||||
CloseButton.Pressed += CloseShop;
|
||||
CancelButton.Pressed += CloseShop;
|
||||
BuyButton.Pressed += BuySelectedItem;
|
||||
AllAmmoButton.Pressed += BuyAllAmmo;
|
||||
ShopItemList.ItemSelected += OnItemSelected;
|
||||
ShopItemList.ItemActivated += OnItemActivated;
|
||||
|
||||
if (AutoFitToViewport && GetViewport() != null)
|
||||
{
|
||||
GetViewport().SizeChanged += OnViewportSizeChanged;
|
||||
}
|
||||
|
||||
FitToViewport();
|
||||
|
||||
if (InventoryManager.Instance != null)
|
||||
{
|
||||
InventoryManager.Instance.ItemAdded += OnInventoryChanged;
|
||||
InventoryManager.Instance.ItemRemoved += OnInventoryChanged;
|
||||
}
|
||||
|
||||
RefreshUi();
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (AutoFitToViewport && GetViewport() != null)
|
||||
{
|
||||
GetViewport().SizeChanged -= OnViewportSizeChanged;
|
||||
}
|
||||
|
||||
if (InventoryManager.Instance != null)
|
||||
{
|
||||
InventoryManager.Instance.ItemAdded -= OnInventoryChanged;
|
||||
InventoryManager.Instance.ItemRemoved -= OnInventoryChanged;
|
||||
}
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (_isClosing || GameStateManager.Instance?.GameState is not GameState.Shop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.IsActionJustPressed(CancelActionName))
|
||||
{
|
||||
CloseShop();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInventoryChanged(LootItem item, int currentAmount)
|
||||
{
|
||||
RefreshUi();
|
||||
}
|
||||
|
||||
private void OnInventoryChanged(string itemKey, int currentAmount)
|
||||
{
|
||||
RefreshUi();
|
||||
}
|
||||
|
||||
private void RefreshUi()
|
||||
{
|
||||
FitToViewport();
|
||||
PopulateList();
|
||||
UpdateCredits();
|
||||
UpdateAllAmmoButton();
|
||||
UpdateSelectionDetails();
|
||||
|
||||
if (_entries.Count > 0)
|
||||
{
|
||||
if (_selectedIndex < 0)
|
||||
{
|
||||
SelectEntry(0, true);
|
||||
}
|
||||
|
||||
CallDeferred(MethodName.GrabListFocus);
|
||||
}
|
||||
}
|
||||
|
||||
private void GrabListFocus()
|
||||
{
|
||||
ShopItemList?.GrabFocus();
|
||||
}
|
||||
|
||||
private void OnViewportSizeChanged()
|
||||
{
|
||||
FitToViewport();
|
||||
}
|
||||
|
||||
private void FitToViewport()
|
||||
{
|
||||
if (!AutoFitToViewport || _rootPanel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var viewport = GetViewport();
|
||||
if (viewport == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var viewportSize = viewport.GetVisibleRect().Size;
|
||||
var availableWidth = Mathf.Max(1f, viewportSize.X - (ViewportMargin * 2f));
|
||||
var availableHeight = Mathf.Max(1f, viewportSize.Y - (ViewportMargin * 2f));
|
||||
var scale = Mathf.Min(1f, Mathf.Min(availableWidth / ShopPanelDesignSize.X, availableHeight / ShopPanelDesignSize.Y));
|
||||
var scaledSize = ShopPanelDesignSize * scale;
|
||||
|
||||
_rootPanel.SetAnchorsPreset(Control.LayoutPreset.TopLeft);
|
||||
_rootPanel.Size = ShopPanelDesignSize;
|
||||
_rootPanel.PivotOffset = Vector2.Zero;
|
||||
_rootPanel.Scale = new Vector2(scale, scale);
|
||||
_rootPanel.Position = (viewportSize - scaledSize) * 0.5f;
|
||||
}
|
||||
|
||||
private void PopulateList()
|
||||
{
|
||||
_entries.Clear();
|
||||
ShopItemList.Clear();
|
||||
|
||||
if (_machine != null)
|
||||
{
|
||||
_entries.AddRange(_machine.GetEntries());
|
||||
}
|
||||
|
||||
for (var index = 0; index < _entries.Count; index++)
|
||||
{
|
||||
var entry = _entries[index];
|
||||
var item = entry.Item;
|
||||
if (item == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var displayName = GetDisplayName(item);
|
||||
var stockLabel = entry.Unlimited ? "INF" : entry.RemainingQuantity.ToString();
|
||||
var row = $"{displayName} {item.Price}c [{stockLabel}]";
|
||||
|
||||
ShopItemList.AddItem(row, item.InventorySprite, true);
|
||||
ShopItemList.SetItemTooltip(index, item.ItemDescription.ToString());
|
||||
}
|
||||
|
||||
if (_entries.Count == 0)
|
||||
{
|
||||
_selectedIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedIndex = Math.Clamp(_selectedIndex, 0, _entries.Count - 1);
|
||||
ShopItemList.Select(_selectedIndex);
|
||||
}
|
||||
|
||||
private void UpdateCredits()
|
||||
{
|
||||
var credits = InventoryManager.Instance?.GetItemCount("CREDITS") ?? 0;
|
||||
CreditsValueLabel.Text = credits.ToString();
|
||||
}
|
||||
|
||||
private void UpdateAllAmmoButton()
|
||||
{
|
||||
var plans = BuildAllAmmoPlan(out var totalPrice, out var canPurchase);
|
||||
|
||||
AllAmmoButton.Text = plans.Count == 0 ? "ALL AMMO" : $"ALL AMMO ({totalPrice}c)";
|
||||
AllAmmoButton.Disabled = !canPurchase;
|
||||
}
|
||||
|
||||
private void UpdateSelectionDetails()
|
||||
{
|
||||
if (!TryGetSelectedEntry(out var entry))
|
||||
{
|
||||
PreviewTexture.Texture = null;
|
||||
ItemNameLabel.Text = "No Stock";
|
||||
DescriptionLabel.Text = "This vending machine has no shop entries configured.";
|
||||
PriceValueLabel.Text = "--";
|
||||
StockValueLabel.Text = "--";
|
||||
OwnedValueLabel.Text = "--";
|
||||
MaxValueLabel.Text = "--";
|
||||
BuyButton.Text = "BUY";
|
||||
BuyButton.Disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var item = entry.Item;
|
||||
var currentCount = GetCurrentCount(item);
|
||||
var canBuy = CanBuy(entry);
|
||||
|
||||
PreviewTexture.Texture = item.LargePreviewSprite ?? item.InventorySprite;
|
||||
ItemNameLabel.Text = item.ItemName.ToString();
|
||||
DescriptionLabel.Text = item.ItemDescription.ToString();
|
||||
PriceValueLabel.Text = item.Price.ToString();
|
||||
StockValueLabel.Text = entry.Unlimited ? "Unlimited" : entry.RemainingQuantity.ToString();
|
||||
OwnedValueLabel.Text = currentCount.ToString();
|
||||
MaxValueLabel.Text = item.Max.ToString();
|
||||
BuyButton.Text = $"BUY ({item.Price}c)";
|
||||
BuyButton.Disabled = !canBuy;
|
||||
}
|
||||
|
||||
private void OnItemSelected(long index)
|
||||
{
|
||||
SelectEntry((int)index);
|
||||
}
|
||||
|
||||
private void OnItemActivated(long index)
|
||||
{
|
||||
SelectEntry((int)index);
|
||||
BuySelectedItem();
|
||||
}
|
||||
|
||||
private void SelectEntry(int index, bool forceFocus = false)
|
||||
{
|
||||
if (_entries.Count == 0)
|
||||
{
|
||||
_selectedIndex = -1;
|
||||
UpdateSelectionDetails();
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedIndex = Math.Clamp(index, 0, _entries.Count - 1);
|
||||
ShopItemList.Select(_selectedIndex);
|
||||
UpdateSelectionDetails();
|
||||
|
||||
if (forceFocus)
|
||||
{
|
||||
ShopItemList.GrabFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetSelectedEntry(out VendingMachine3D.RuntimeShopEntry entry)
|
||||
{
|
||||
if (_selectedIndex < 0 || _selectedIndex >= _entries.Count)
|
||||
{
|
||||
entry = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
entry = _entries[_selectedIndex];
|
||||
return entry?.Item != null;
|
||||
}
|
||||
|
||||
private bool CanBuy(VendingMachine3D.RuntimeShopEntry entry)
|
||||
{
|
||||
var item = entry.Item;
|
||||
if (item == null || InventoryManager.Instance == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentCount = GetCurrentCount(item);
|
||||
if (currentCount >= item.Max)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return entry.CanPurchase()
|
||||
&& InventoryManager.Instance.CanAddItem(item.ItemKey.ToString())
|
||||
&& InventoryManager.Instance.GetItemCount("CREDITS") >= item.Price;
|
||||
}
|
||||
|
||||
private void BuySelectedItem()
|
||||
{
|
||||
if (!TryGetSelectedEntry(out var entry) || !CanBuy(entry) || InventoryManager.Instance == null)
|
||||
{
|
||||
RefreshUi();
|
||||
return;
|
||||
}
|
||||
|
||||
_machine.TryConsumeStock(entry.Item.ItemKey);
|
||||
InventoryManager.Instance.RemoveItem("CREDITS", entry.Item.Price);
|
||||
InventoryManager.Instance.AddItem(entry.Item);
|
||||
|
||||
RefreshUi();
|
||||
}
|
||||
|
||||
private void BuyAllAmmo()
|
||||
{
|
||||
if (InventoryManager.Instance == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var plans = BuildAllAmmoPlan(out var totalPrice, out var canPurchase);
|
||||
if (!canPurchase || plans.Count == 0)
|
||||
{
|
||||
RefreshUi();
|
||||
return;
|
||||
}
|
||||
|
||||
InventoryManager.Instance.RemoveItem("CREDITS", totalPrice);
|
||||
|
||||
foreach (var plan in plans)
|
||||
{
|
||||
_machine.TryConsumeStock(plan.Entry.Item.ItemKey, plan.BundleCount);
|
||||
|
||||
for (var purchaseIndex = 0; purchaseIndex < plan.BundleCount; purchaseIndex++)
|
||||
{
|
||||
InventoryManager.Instance.AddItem(plan.Entry.Item);
|
||||
}
|
||||
}
|
||||
|
||||
RefreshUi();
|
||||
}
|
||||
|
||||
private List<AmmoPurchasePlan> BuildAllAmmoPlan(out int totalPrice, out bool canPurchase)
|
||||
{
|
||||
totalPrice = 0;
|
||||
canPurchase = false;
|
||||
|
||||
if (InventoryManager.Instance == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var plans = new List<AmmoPurchasePlan>();
|
||||
|
||||
foreach (var entry in _entries.Where(entry => entry.Item?.Item == ItemTypes.Ammo))
|
||||
{
|
||||
if (!InventoryManager.Instance.TryGetItem(entry.Item.ItemKey.ToString(), out var ownedItem))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ownedItem.Count >= entry.Item.Max)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var bundleSize = Math.Max(1, entry.Item.Amount);
|
||||
var missingAmount = entry.Item.Max - ownedItem.Count;
|
||||
var bundleCount = Mathf.CeilToInt(missingAmount / (float)bundleSize);
|
||||
|
||||
if (!entry.CanPurchase(bundleCount))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var price = bundleCount * entry.Item.Price;
|
||||
plans.Add(new AmmoPurchasePlan
|
||||
{
|
||||
Entry = entry,
|
||||
BundleCount = bundleCount,
|
||||
TotalPrice = price,
|
||||
});
|
||||
totalPrice += price;
|
||||
}
|
||||
|
||||
canPurchase = plans.Count > 0 && InventoryManager.Instance.GetItemCount("CREDITS") >= totalPrice;
|
||||
return plans;
|
||||
}
|
||||
|
||||
private int GetCurrentCount(LootItem item)
|
||||
{
|
||||
return InventoryManager.Instance?.GetItemCount(item.ItemKey.ToString()) ?? 0;
|
||||
}
|
||||
|
||||
private void CloseShop()
|
||||
{
|
||||
if (_isClosing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isClosing = true;
|
||||
|
||||
if (GameStateManager.Instance != null)
|
||||
{
|
||||
GameStateManager.Instance.ChangeState(GameState.Playing);
|
||||
}
|
||||
|
||||
EmitSignal(SignalName.Closed);
|
||||
QueueFree();
|
||||
}
|
||||
|
||||
private static string GetDisplayName(LootItem item)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(item.ShortName.ToString()))
|
||||
{
|
||||
return item.ShortName.ToString();
|
||||
}
|
||||
|
||||
return item.ItemName.ToString();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue