mirror of
https://gitlab.com/MaddoScientisto/cirnogodot.git
synced 2026-06-01 08:45:33 +00:00
447 lines
No EOL
13 KiB
C#
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();
|
|
}
|
|
} |