cirnogodot/Scripts/UI/VendingMachineShopUi.cs

447 lines
No EOL
13 KiB
C#

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();
}
}