using System; using System.Collections.Generic; using System.Linq; using Cirno.Scripts.Interactables; using Cirno.Scripts.Resources; using Cirno.Scripts.UI; using Cirno.Scripts.Utils; using Godot; namespace Cirno.Scripts.Actors._3D; [Tool] public partial class VendingMachine3D : Interactable3D { public sealed class RuntimeShopEntry { public RuntimeShopEntry(VendingShopEntry entry) { Entry = entry; RemainingQuantity = Math.Max(0, entry.StartingQuantity); } public VendingShopEntry Entry { get; } public LootItem Item => Entry.Item; public bool Unlimited => Entry.Unlimited; public int RemainingQuantity { get; private set; } public bool CanPurchase(int purchaseCount = 1) { return purchaseCount <= 0 || Unlimited || RemainingQuantity >= purchaseCount; } public bool TryConsume(int purchaseCount = 1) { if (purchaseCount <= 0 || Unlimited) { return true; } if (RemainingQuantity < purchaseCount) { return false; } RemainingQuantity -= purchaseCount; return true; } } [Signal] public delegate void ShopOpenedEventHandler(); [Signal] public delegate void ShopClosedEventHandler(); [Export] public PackedScene ShopUiScene { get; private set; } = ResourceLoader.Load("res://Scenes/HUD/VendingMachineShopUi.tscn"); [Export] public VendingShopDefinition ShopDefinition { get; private set; } = ResourceLoader.Load("res://Resources/Shops/VendingMachine_Default.tres"); private readonly List _runtimeEntries = []; private VendingMachineShopUi _activeShopUi; private bool _runtimeInitialized; public override void _Ready() { AddToGroup("Interactable"); if (Engine.IsEditorHint()) { return; } EnsureRuntimeEntries(); } public override bool CanActivate() { if (Engine.IsEditorHint()) { return false; } EnsureRuntimeEntries(); return !IsShopOpen() && ShopUiScene != null && _runtimeEntries.Count > 0 && GameStateManager.Instance?.GameState is not GameState.Shop; } public override bool Activate(ActivationType activationType = ActivationType.Toggle) { if (Engine.IsEditorHint()) { return false; } var typeToUse = activationType is ActivationType.Toggle ? ActivationType.Use : activationType; if (typeToUse is not ActivationType.Use) { return false; } if (!MeetsRequirements() || !CanActivate()) { return false; } EnsureRuntimeEntries(); if (GameStateManager.Instance == null) { return false; } GameStateManager.Instance.ChangeState(GameState.Shop); _activeShopUi = ShopUiScene.Instantiate(); _activeShopUi.Init(this); _activeShopUi.Closed += OnShopUiClosed; var parent = GetTree().CurrentScene ?? GetTree().Root; parent.AddChild(_activeShopUi); EmitSignal(SignalName.ShopOpened); return true; } public override void _ExitTree() { if (IsInstanceValid(_activeShopUi)) { _activeShopUi.Closed -= OnShopUiClosed; _activeShopUi.QueueFree(); } _activeShopUi = null; } public IReadOnlyList GetEntries() { EnsureRuntimeEntries(); return _runtimeEntries; } public RuntimeShopEntry FindEntry(StringName itemKey) { EnsureRuntimeEntries(); return _runtimeEntries.FirstOrDefault(entry => entry.Item?.ItemKey == itemKey); } public bool TryConsumeStock(StringName itemKey, int purchaseCount = 1) { var entry = FindEntry(itemKey); return entry?.TryConsume(purchaseCount) ?? false; } // public void _func_godot_apply_properties(Godot.Collections.Dictionary props) // { // if (props.TryGetValue("shop_definition_path", out var shopDefinitionPath)) // { // var path = shopDefinitionPath.AsString(); // if (!string.IsNullOrWhiteSpace(path)) // { // var loadedDefinition = ResourceLoader.Load(path); // if (loadedDefinition != null) // { // ShopDefinition = loadedDefinition; // _runtimeInitialized = false; // } // else // { // GD.PrintErr($"Failed to load vending shop definition from '{path}'"); // } // } // } // } private void EnsureRuntimeEntries() { if (_runtimeInitialized) { return; } _runtimeInitialized = true; _runtimeEntries.Clear(); if (ShopDefinition == null) { return; } foreach (var entry in ShopDefinition.Entries.Where(entry => entry?.Item != null)) { _runtimeEntries.Add(new RuntimeShopEntry(entry)); } } private bool IsShopOpen() { return IsInstanceValid(_activeShopUi); } private void OnShopUiClosed() { if (IsInstanceValid(_activeShopUi)) { _activeShopUi.Closed -= OnShopUiClosed; } _activeShopUi = null; EmitSignal(SignalName.ShopClosed); } }