using Chickensoft.AutoInject; using Chickensoft.GodotNodeInterfaces; using Chickensoft.Introspection; using Godot; using System.Linq; using System.Threading.Tasks; using Zennysoft.Ma.Adapter; namespace Zennysoft.Game.Ma; public interface IInventoryMenu : IControl { public Task RefreshInventoryScreen(); public Task DisplayMessage(string message); public void RemoveItem(InventoryItem item); } [Meta(typeof(IAutoNode))] public partial class InventoryMenu : Control, IInventoryMenu { public override void _Notification(int what) => this.Notify(what); [Dependency] private IGameRepo _gameRepo => this.DependOn(); [Dependency] public IGame Game => this.DependOn(); [Dependency] public IPlayer Player => this.DependOn(); [Dependency] public IMap _map => this.DependOn(); [Dependency] public IGameEventDepot GameEventDepot => this.DependOn(); private InventoryPageNumber _currentPageNumber = InventoryPageNumber.FirstPage; private string ITEM_SLOT_SCENE = "res://src/inventory_menu/ItemSlot.tscn"; private const int _itemsPerPage = 10; private int _currentIndex = 0; private IItemSlot[] ItemSlots => ItemsPage.GetChildren().OfType().ToArray(); #region Control Nodes [Node] public Label FloorLabel { get; set; } = default!; [Node] public Label CurrentLevelLabel { get; set; } = default!; [Node] public Label EXPValue { get; set; } = default!; [Node] public Label HPValue { get; set; } = default!; [Node] public Label HPBonusLabel { get; set; } = default!; [Node] public Label VTValue { get; set; } = default!; [Node] public Label VTBonusLabel { get; set; } = default!; [Node] public Label ATKValue { get; set; } = default!; [Node] public Label ATKBonusLabel { get; set; } = default!; [Node] public Label DEFValue { get; set; } = default!; [Node] public Label DEFBonusLabel { get; set; } = default!; [Node] public Label ItemDescriptionTitle { get; set; } = default!; [Node] public Label ItemEffectLabel { get; set; } = default!; // Item Menu [Node] public Label BackArrow { get; set; } = default!; [Node] public Label ForwardArrow { get; set; } = default!; [Node] public Control ItemsPage { get; set; } = default!; // User Prompt Menu [Node] public Label UseItemPrompt { get; set; } = default!; [Node] public Button UseButton { get; set; } = default!; [Node] public Button ThrowButton { get; set; } = default!; [Node] public Button DropButton { get; set; } = default!; [Node] public AnimationPlayer AnimationPlayer { get; set; } = default!; #endregion public void OnReady() { UseButton.Pressed += UseButtonPressed; ThrowButton.Pressed += ThrowButtonPressed; DropButton.Pressed += DropButtonPressed; } public void OnResolved() { Player.Stats.CurrentHP.Sync += CurrentHP_Sync; Player.Stats.MaximumHP.Sync += MaximumHP_Sync; Player.Stats.CurrentVT.Sync += CurrentVT_Sync; Player.Stats.MaximumVT.Sync += MaximumVT_Sync; Player.Stats.CurrentAttack.Sync += CurrentAttack_Sync; Player.Stats.MaxAttack.Sync += MaxAttack_Sync; Player.Stats.CurrentDefense.Sync += CurrentDefense_Sync; Player.Stats.MaxDefense.Sync += MaxDefense_Sync; Player.Stats.CurrentExp.Sync += CurrentExp_Sync; Player.Stats.ExpToNextLevel.Sync += ExpToNextLevel_Sync; Player.Stats.CurrentLevel.Sync += CurrentLevel_Sync; Player.Stats.BonusAttack.Sync += BonusAttack_Sync; Player.Stats.BonusDefense.Sync += BonusDefense_Sync; SetProcessInput(false); } public async Task DisplayMessage(string message) { SetProcessInput(false); await HideUserActionPrompt(); await ShowInventoryInfo(); ItemEffectLabel.Text = message; await ToSignal(GetTree().CreateTimer(1f), "timeout"); await RefreshInventoryScreen(); SetProcessInput(true); } private void BonusAttack_Sync(int bonus) { ATKBonusLabel.Text = $"{bonus:+0;-#;\\.\\.\\.}"; DEFBonusLabel.Text = $"{Player.Stats.BonusDefense.Value:+0;-#;\\.\\.\\.}"; } private void BonusDefense_Sync(int bonus) { ATKBonusLabel.Text = $"{Player.Stats.BonusAttack.Value:+0;-#;\\.\\.\\.}"; DEFBonusLabel.Text = $"{bonus:+0;-#;\\.\\.\\.}"; } private void CurrentLevel_Sync(int obj) => CurrentLevelLabel.Text = $"Level {obj:D2}"; private void ExpToNextLevel_Sync(int obj) => EXPValue.Text = $"{Player.Stats.CurrentExp.Value}/{obj}"; private void CurrentExp_Sync(double obj) => EXPValue.Text = $"{obj}/{Player.Stats.ExpToNextLevel.Value}"; private void MaxDefense_Sync(int obj) => DEFValue.Text = $"{Player.Stats.CurrentDefense.Value}/{obj}"; private void CurrentDefense_Sync(int obj) => DEFValue.Text = $"{obj}/{Player.Stats.MaxDefense.Value}"; private void MaxAttack_Sync(int obj) => ATKValue.Text = $"{Player.Stats.CurrentAttack.Value}/{obj}"; private void CurrentAttack_Sync(int obj) => ATKValue.Text = $"{obj}/{Player.Stats.MaxAttack.Value}"; private void MaximumVT_Sync(int obj) => VTValue.Text = $"{Player.Stats.CurrentVT.Value}/{obj}"; private void CurrentVT_Sync(int obj) => VTValue.Text = $"{obj}/{Player.Stats.MaximumVT.Value}"; private void MaximumHP_Sync(int obj) => HPValue.Text = $"{Player.Stats.CurrentHP.Value}/{obj}"; private void CurrentHP_Sync(int obj) => HPValue.Text = $"{obj}/{Player.Stats.MaximumHP.Value}"; public async Task RefreshInventoryScreen() { await ClearItems(); PopulateInventory(); PopulatePlayerInfo(); await HideUserActionPrompt(); await ShowInventoryInfo(); } #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed public override void _Input(InputEvent @event) { var inventory = Player.Inventory; if (@event.IsActionPressed(GameInputs.UiCancel)) { if (UseButton.HasFocus() || DropButton.HasFocus() || ThrowButton.HasFocus()) { HideUserActionPrompt(); ShowInventoryInfo(); GameEventDepot.OnMenuBackedOut(); } else { _gameRepo.CloseInventory(); } } if (ItemSlots.Length == 0 || UseButton.HasFocus() || DropButton.HasFocus() || ThrowButton.HasFocus()) return; if (@event.IsActionPressed(GameInputs.UiRight) && _currentPageNumber == InventoryPageNumber.FirstPage && inventory.Items.Count > _itemsPerPage) ChangeInventoryPage(InventoryPageNumber.SecondPage); if (@event.IsActionPressed(GameInputs.UiLeft) && _currentPageNumber == InventoryPageNumber.SecondPage) ChangeInventoryPage(InventoryPageNumber.FirstPage); if (@event.IsActionPressed(GameInputs.UiDown)) { var oldIndex = _currentIndex; var newIndex = new[] { _currentIndex + 1, _itemsPerPage - 1, ItemSlots.Length - 1 }.Min(); if (oldIndex == newIndex) return; SetToUnselectedStyle(ItemSlots.ElementAt(oldIndex)); SetToSelectedStyle(ItemSlots.ElementAt(newIndex)); GameEventDepot.OnMenuScrolled(); _currentIndex = newIndex; } if (@event.IsActionPressed(GameInputs.UiUp)) { var oldIndex = _currentIndex; var newIndex = new[] { _currentIndex - 1, 0 }.Max(); if (oldIndex == newIndex) return; SetToUnselectedStyle(ItemSlots.ElementAt(oldIndex)); SetToSelectedStyle(ItemSlots.ElementAt(newIndex)); GameEventDepot.OnMenuScrolled(); _currentIndex = newIndex; } if (@event.IsActionPressed(GameInputs.UiAccept)) { DisplayUserActionPrompt(); } if (@event.IsActionPressed(GameInputs.InventorySort)) { inventory.Sort(); if (_currentIndex > inventory.Items.Count - 1) _currentIndex = inventory.Items.Count - 1; GameEventDepot.OnInventorySorted(); RefreshInventoryScreen(); } } #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed public async void RemoveItem(InventoryItem item) { Player.Inventory.Remove(item); if (_currentIndex >= ItemSlots.Length - 1) _currentIndex--; if (_currentIndex <= 0) _currentIndex = 0; } private void PopulateItems() { PopulateInventory(); PopulatePlayerInfo(); } private async Task ClearItems() { foreach (var item in ItemSlots) ItemsPage.RemoveChildEx(item); ItemDescriptionTitle.Text = string.Empty; ItemEffectLabel.Text = string.Empty; } private void PopulatePlayerInfo() { FloorLabel.Text = $"Floor {_map.CurrentFloorNumber:D2}"; if (ItemSlots.Length != 0) { var item = ItemSlots.ElementAt(_currentIndex).Item; ItemDescriptionTitle.Text = $"{item.ItemName}"; ItemEffectLabel.Text = $"{item.Description}"; } } private void DisplayUserActionPrompt() { ItemDescriptionTitle.Hide(); ItemEffectLabel.Hide(); UseItemPrompt.Show(); UseButton.Show(); ThrowButton.Show(); DropButton.Show(); var currentItem = ItemSlots.ElementAt(_currentIndex).Item; if (currentItem is EquipableItem equipable) { UseButton.Text = equipable.IsEquipped ? "Unequip" : "Equip"; ThrowButton.Disabled = equipable.IsEquipped; ThrowButton.FocusMode = equipable.IsEquipped ? FocusModeEnum.None : FocusModeEnum.All; DropButton.Disabled = equipable.IsEquipped; DropButton.FocusMode = equipable.IsEquipped ? FocusModeEnum.None : FocusModeEnum.All; } else { UseButton.Text = "Use"; } UseButton.CallDeferred(MethodName.GrabFocus); } private async Task HideUserActionPrompt() { UseItemPrompt.Hide(); UseButton.Hide(); ThrowButton.Hide(); DropButton.Hide(); UseButton.ReleaseFocus(); ThrowButton.ReleaseFocus(); DropButton.ReleaseFocus(); } private async Task ShowInventoryInfo() { ItemDescriptionTitle.Show(); ItemEffectLabel.Show(); } private async Task ChangeInventoryPage(InventoryPageNumber pageToChangeTo) { await ClearItems(); await ToSignal(GetTree().CreateTimer(0.1f), "timeout"); _currentIndex = 0; _currentPageNumber = pageToChangeTo; await RefreshInventoryScreen(); GameEventDepot.OnMenuScrolled(); } private async void PopulateInventory() { var inventory = Player.Inventory; var numberOfItemsToDisplay = _currentPageNumber == InventoryPageNumber.FirstPage ? Mathf.Min(inventory.Items.Count, _itemsPerPage) : Mathf.Min(inventory.Items.Count - _itemsPerPage, _itemsPerPage); var indexToStart = _currentPageNumber == InventoryPageNumber.FirstPage ? 0 : _itemsPerPage; ForwardArrow.Text = ""; BackArrow.Text = ""; if (_currentPageNumber == InventoryPageNumber.FirstPage && inventory.Items.Count > _itemsPerPage) { ForwardArrow.Text = "►"; BackArrow.Text = ""; } if (_currentPageNumber == InventoryPageNumber.SecondPage) { ForwardArrow.Text = ""; BackArrow.Text = "◄"; } for (var i = 0; i < numberOfItemsToDisplay; i++) { var item = inventory.Items.ElementAt(i + indexToStart); var itemScene = GD.Load(ITEM_SLOT_SCENE); var itemSlot = itemScene.Instantiate(); itemSlot.Item = item; ItemsPage.AddChildEx(itemSlot); if (itemSlot.Item is EquipableItem equipable && equipable.IsEquipped) itemSlot.SetEquippedItemStyle(); } if (ItemSlots.Length != 0) { ItemSlots.ElementAt(_currentIndex).SetSelectedItemStyle(); if (ItemSlots.ElementAt(_currentIndex).Item is EquipableItem equipable && equipable.IsEquipped) ItemSlots.ElementAt(_currentIndex).SetEquippedSelectedItemStyle(); } } private async Task SetToUnselectedStyle(IItemSlot itemSlot) { await ToSignal(GetTree().CreateTimer(0.1f), "timeout"); itemSlot.SetItemStyle(); if (itemSlot.Item is EquipableItem equipable && equipable.IsEquipped) itemSlot.SetEquippedItemStyle(); } private async Task SetToSelectedStyle(IItemSlot itemSlot) { await ToSignal(GetTree().CreateTimer(0.1f), "timeout"); itemSlot.SetSelectedItemStyle(); ItemDescriptionTitle.Text = $"{itemSlot.Item.ItemName}"; ItemEffectLabel.Text = $"{itemSlot.Item.Description}"; } private async Task EquipOrUnequipItem() { var itemSlot = ItemSlots[_currentIndex]; if (itemSlot.Item is EquipableItem equipableItem) { if (equipableItem.IsEquipped) { ItemEffectLabel.Text = $"{itemSlot.Item.GetType()} unequipped."; Player.Unequip(equipableItem); itemSlot.SetSelectedItemStyle(); if (equipableItem.ItemTag == ItemTag.BreaksOnChange) Player.Inventory.Remove(equipableItem); } else { ItemEffectLabel.Text = $"{itemSlot.Item.GetType()} equipped."; Player.Equip(equipableItem); itemSlot.SetEquippedSelectedItemStyle(); } RefreshUIAfterUserSelection(); } } private async void UseButtonPressed() { UseButton.Disabled = true; var currentItem = ItemSlots[_currentIndex].Item; if (currentItem is EquipableItem) await EquipOrUnequipItem(); else await Game.UseItem(currentItem); RefreshUIAfterUserSelection(); UseButton.Disabled = false; } private async void ThrowButtonPressed() { var currentItem = ItemSlots[_currentIndex].Item; Game.ThrowItem(currentItem); Player.Inventory.Remove(currentItem); if (_currentIndex >= ItemSlots.Length - 1) _currentIndex--; if (_currentIndex <= 0) _currentIndex = 0; _gameRepo.CloseInventory(); } private async void DropButtonPressed() { var currentItem = ItemSlots[_currentIndex].Item; Game.DropItem(currentItem); Player.Inventory.Remove(currentItem); if (_currentIndex >= ItemSlots.Length - 1) _currentIndex--; if (_currentIndex <= 0) _currentIndex = 0; _gameRepo.CloseInventory(); } private async void RefreshUIAfterUserSelection() { SetProcessInput(false); await HideUserActionPrompt(); await ShowInventoryInfo(); await RefreshInventoryScreen(); await ToSignal(GetTree().CreateTimer(1f), "timeout"); SetProcessInput(true); } private enum InventoryPageNumber { FirstPage, SecondPage } }