using Chickensoft.AutoInject; using Chickensoft.GodotNodeInterfaces; using Chickensoft.Introspection; using GameJamDungeon; using Godot; using System.Linq; using System.Threading.Tasks; public interface IInventoryMenu : IControl { public void PopulateItems(); public void ClearItems(); } [Meta(typeof(IAutoNode))] public partial class InventoryMenu : Control, IInventoryMenu { public override void _Notification(int what) => this.Notify(what); [Dependency] public IGameRepo GameRepo => this.DependOn(); [Dependency] public IGame Game => this.DependOn(); // Player Info [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!; public void OnReady() { UseButton.Pressed += UseButtonPressed; ThrowButton.Pressed += ThrowButtonPressed; DropButton.Pressed += DropButtonPressed; } 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(); public void PopulateItems() { PopulateInventory(); PopulatePlayerInfo(); } public void ClearItems() { foreach (var item in ItemSlots) ItemsPage.RemoveChildEx(item); HideUserActionPrompt(); } public void PopulatePlayerInfo() { FloorLabel.Text = $"Level {GameRepo.CurrentFloor:D2}"; var currentLevel = GameRepo.PlayerStatInfo.Value.CurrentLevel; CurrentLevelLabel.Text = $"Level {currentLevel:D2}"; var currentHP = GameRepo.PlayerStatInfo.Value.CurrentHP; var maxHP = GameRepo.PlayerStatInfo.Value.MaximumHP; HPValue.Text = $"{currentHP}/{maxHP}"; var currentVT = GameRepo.PlayerStatInfo.Value.CurrentVT; var maxVT = GameRepo.PlayerStatInfo.Value.MaximumVT; VTValue.Text = $"{currentVT}/{maxVT}"; var currentAttack = GameRepo.PlayerStatInfo.Value.CurrentAttack; var maxAttack = GameRepo.PlayerStatInfo.Value.MaxAttack; ATKValue.Text = $"{currentAttack}/{maxAttack}"; var currentDefense = GameRepo.PlayerStatInfo.Value.CurrentDefense; var maxDefense = GameRepo.PlayerStatInfo.Value.MaxDefense; DEFValue.Text = $"{currentDefense}/{maxDefense}"; var atkBonus = GameRepo.PlayerStatInfo.Value.BonusAttack; var defBonus = GameRepo.PlayerStatInfo.Value.BonusDefense; ATKBonusLabel.Text = atkBonus != 0 ? $"{atkBonus:+0;-#}" : "..."; DEFBonusLabel.Text = defBonus != 0 ? $"{defBonus:+0;-#}" : "..."; // TODO: Change font style when EXP Bonus effect is active var currentExp = GameRepo.PlayerStatInfo.Value.CurrentEXP; var expToNextLevel = GameRepo.PlayerStatInfo.Value.EXPToNextLevel; EXPValue.Text = $"{currentExp}/{expToNextLevel}"; if (ItemSlots.Any()) { var item = ItemSlots.ElementAt(_currentIndex).Item; ItemDescriptionTitle.Text = $"{item.Info.Name}"; ItemEffectLabel.Text = $"{item.Info.Description}"; } } public override void _UnhandledInput(InputEvent @event) { if (ItemSlots.Length == 0) return; var inventory = GameRepo.InventoryItems.Value; if (@event.IsActionPressed(GameInputs.UiRight) && _currentPageNumber == InventoryPageNumber.FirstPage && inventory.Count > _itemsPerPage) ChangeInventoryPage(InventoryPageNumber.SecondPage); if (@event.IsActionPressed(GameInputs.UiLeft) && _currentPageNumber == InventoryPageNumber.SecondPage) ChangeInventoryPage(InventoryPageNumber.FirstPage); if (@event.IsActionPressed(GameInputs.UiDown)) { SetToUnselectedStyle(ItemSlots.ElementAt(_currentIndex)); _currentIndex = new[] { _currentIndex + 1, _itemsPerPage - 1, ItemSlots.Length - 1 }.Min(); SetToSelectedStyle(ItemSlots.ElementAt(_currentIndex)); } if (@event.IsActionPressed(GameInputs.UiUp)) { SetToUnselectedStyle(ItemSlots.ElementAt(_currentIndex)); _currentIndex = new[] { _currentIndex - 1, 0 }.Max(); SetToSelectedStyle(ItemSlots.ElementAt(_currentIndex)); } if (@event.IsActionPressed(GameInputs.UiAccept)) { if (ItemSlots.Length == 0 || GetViewport().GuiGetFocusOwner() is Button) return; DisplayUserActionPrompt(); } } private void DisplayUserActionPrompt() { ItemDescriptionTitle.Hide(); ItemEffectLabel.Hide(); UseItemPrompt.Show(); UseButton.Show(); ThrowButton.Show(); DropButton.Show(); var currentItem = ItemSlots.ElementAt(_currentIndex).Item; if (currentItem is IEquipable equipable) { var isEquipped = GameRepo.IsItemEquipped(equipable); UseButton.Text = isEquipped ? "Unequip" : "Equip"; ThrowButton.Disabled = isEquipped ? true : false; ThrowButton.FocusMode = isEquipped ? FocusModeEnum.None : FocusModeEnum.All; DropButton.Disabled = isEquipped ? true : false; DropButton.FocusMode = isEquipped ? FocusModeEnum.None : FocusModeEnum.All; } else { UseButton.Text = "Use"; } UseButton.GrabFocus(); } private async Task HideUserActionPrompt() { ItemDescriptionTitle.Show(); ItemEffectLabel.Show(); UseItemPrompt.Hide(); UseButton.Hide(); ThrowButton.Hide(); DropButton.Hide(); } private async void ChangeInventoryPage(InventoryPageNumber pageToChangeTo) { ClearItems(); await ToSignal(GetTree().CreateTimer(0.1f), "timeout"); _currentIndex = 0; _currentPageNumber = pageToChangeTo; PopulateItems(); } private void PopulateInventory() { var inventory = GameRepo.InventoryItems.Value; var numberOfItemsToDisplay = _currentPageNumber == InventoryPageNumber.FirstPage ? Mathf.Min(inventory.Count, _itemsPerPage) : Mathf.Min(inventory.Count - _itemsPerPage, _itemsPerPage); var indexToStart = _currentPageNumber == InventoryPageNumber.FirstPage ? 0 : _itemsPerPage - 1; ForwardArrow.Text = ""; BackArrow.Text = ""; if (_currentPageNumber == InventoryPageNumber.FirstPage && inventory.Count > _itemsPerPage) { ForwardArrow.Text = "►"; BackArrow.Text = ""; } if (_currentPageNumber == InventoryPageNumber.SecondPage) { ForwardArrow.Text = ""; BackArrow.Text = "◄"; } for (var i = 0; i < numberOfItemsToDisplay; i++) { var item = inventory.ElementAt(i + indexToStart); var itemScene = GD.Load(ITEM_SLOT_SCENE); var itemSlot = itemScene.Instantiate(); itemSlot.Item = item; ItemsPage.AddChildEx(itemSlot); if (itemSlot.Item is IEquipable equipable && GameRepo.IsItemEquipped(equipable)) itemSlot.SetEquippedItemStyle(); } if (ItemSlots.Any()) { ItemSlots.ElementAt(_currentIndex).SetSelectedItemStyle(); if (ItemSlots.ElementAt(_currentIndex).Item is IEquipable equipable && GameRepo.IsItemEquipped(equipable)) ItemSlots.ElementAt(_currentIndex).SetEquippedSelectedItemStyle(); } } private async void SetToUnselectedStyle(IItemSlot itemSlot) { await ToSignal(GetTree().CreateTimer(0.1f), "timeout"); itemSlot.SetItemStyle(); if (itemSlot.Item is IEquipable equipable && GameRepo.IsItemEquipped(equipable)) itemSlot.SetEquippedItemStyle(); } private async void SetToSelectedStyle(IItemSlot itemSlot) { await ToSignal(GetTree().CreateTimer(0.1f), "timeout"); itemSlot.SetSelectedItemStyle(); if (itemSlot.Item is IEquipable newEquipable && GameRepo.IsItemEquipped(newEquipable)) itemSlot.SetEquippedSelectedItemStyle(); ItemDescriptionTitle.Text = $"{itemSlot.Item.Info.Name}"; ItemEffectLabel.Text = $"{itemSlot.Item.Info.Description}"; } private async void EquipOrUnequipItem() { var itemSlot = ItemSlots[_currentIndex]; await ToSignal(GetTree().CreateTimer(0.2f), "timeout"); if (itemSlot.Item is IEquipable equipableItem) { if (GameRepo.IsItemEquipped(equipableItem)) { GameRepo.UnequipItem(equipableItem); itemSlot.SetSelectedItemStyle(); } else { GameRepo.EquipItem(equipableItem); itemSlot.SetEquippedSelectedItemStyle(); } UpdateStatBonusInfo(); } } private void UpdateStatBonusInfo() { var atkBonus = GameRepo.PlayerStatInfo.Value.BonusAttack; ATKBonusLabel.Text = atkBonus != 0 ? $"{atkBonus:+0;-#}" : "..."; var defBonus = GameRepo.PlayerStatInfo.Value.BonusDefense; DEFBonusLabel.Text = defBonus != 0 ? $"{defBonus:+0;-#}" : "..."; var currentHP = GameRepo.PlayerStatInfo.Value.CurrentHP; var maxHP = GameRepo.PlayerStatInfo.Value.MaximumHP; HPValue.Text = $"{currentHP}/{maxHP}"; var currentVT = GameRepo.PlayerStatInfo.Value.CurrentVT; var maxVT = GameRepo.PlayerStatInfo.Value.MaximumVT; VTValue.Text = $"{currentVT}/{maxVT}"; } private async void UseButtonPressed() { var currentItem = ItemSlots[_currentIndex].Item; if (currentItem is IEquipable) EquipOrUnequipItem(); if (currentItem is ConsumableItem consumable) consumable.Use(); // TODO: Replace with animation player (for visual effects/sound effects?) await ToSignal(GetTree().CreateTimer(0.25f), "timeout"); await HideUserActionPrompt(); PopulatePlayerInfo(); } private async void ThrowButtonPressed() { var currentItem = ItemSlots[_currentIndex].Item; if (_currentIndex >= ItemSlots.Length - 1) _currentIndex--; if (_currentIndex <= 0) _currentIndex = 0; Game.ToggleInventory(); currentItem.Throw(); } private async void DropButtonPressed() { var currentItem = ItemSlots[_currentIndex].Item; if (_currentIndex >= ItemSlots.Length - 1) _currentIndex--; if (_currentIndex <= 0) _currentIndex = 0; Game.ToggleInventory(); currentItem.Drop(); } private enum InventoryPageNumber { FirstPage, SecondPage } }