diff --git a/src/scenes/game_world.tscn b/src/scenes/game_world.tscn index dbde1d2..702840a 100644 --- a/src/scenes/game_world.tscn +++ b/src/scenes/game_world.tscn @@ -41,5 +41,3 @@ color = Color(0.671875, 0.671875, 0.671875, 1) [node name="AudioStreamPlayer2D" type="AudioStreamPlayer2D" parent="." unique_id=1141138343] stream = ExtResource("6_6c6v5") -autoplay = true -stream_paused = true diff --git a/src/scenes/inventory_ui.tscn b/src/scenes/inventory_ui.tscn new file mode 100644 index 0000000..a4bfffa --- /dev/null +++ b/src/scenes/inventory_ui.tscn @@ -0,0 +1,188 @@ +[gd_scene format=3 uid="uid://cxs0ybxk2blth"] + +[ext_resource type="Script" uid="uid://vm6intetgl40" path="res://scripts/inventory_ui.gd" id="1_inventory_ui"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_selection"] +bg_color = Color(0, 0, 0, 0) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(1, 1, 0, 1) + +[node name="InventoryUI" type="CanvasLayer" unique_id=-1294967296] +layer = 150 +script = ExtResource("1_inventory_ui") + +[node name="InventoryContainer" type="Control" parent="." unique_id=-294967296] +layout_mode = 3 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 0 +grow_vertical = 0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="InventoryContainer" unique_id=935107028] +layout_mode = 1 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -610.0 +offset_top = -226.0 +grow_horizontal = 0 +grow_vertical = 0 + +[node name="Background" type="ColorRect" parent="InventoryContainer/MarginContainer" unique_id=705032704] +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 1 +color = Color(0.1, 0.1, 0.1, 0.85) + +[node name="VBoxContainer" type="VBoxContainer" parent="InventoryContainer/MarginContainer" unique_id=1015792177] +layout_mode = 2 + +[node name="HBox" type="HBoxContainer" parent="InventoryContainer/MarginContainer/VBoxContainer" unique_id=1705032704] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="StatsPanel" type="VBoxContainer" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox" unique_id=-1589934592] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +theme_override_constants/separation = 5 + +[node name="StatsLabel" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel" unique_id=-589934592] +layout_mode = 2 +theme_override_font_sizes/font_size = 14 +text = "Stats" + +[node name="StatsHBox" type="HBoxContainer" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel" unique_id=410065408] +layout_mode = 2 +theme_override_constants/separation = 5 + +[node name="LabelBaseStats" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox" unique_id=1410065408] +layout_mode = 2 +theme_override_font_sizes/font_size = 10 +text = "Level + +HP +MP + +STR +DEX +END +INT +WIS +LCK" + +[node name="LabelBaseStatsValue" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox" unique_id=-1884901888] +layout_mode = 2 +theme_override_font_sizes/font_size = 10 +text = "1 + +30/30 +20/20 + +10 +10 +10 +10 +10 +10" +horizontal_alignment = 2 + +[node name="LabelDerivedStats" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox" unique_id=-884901888] +layout_mode = 2 +theme_override_font_sizes/font_size = 10 +text = "XP +Coin + + + +DMG +DEF +MovSpd +AtkSpd +Sight +SpellAmp +Crit%" + +[node name="LabelDerivedStatsValue" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox" unique_id=115098112] +layout_mode = 2 +theme_override_font_sizes/font_size = 10 +text = "0/100 +0 + + + +2.0 +2.0 +2.1 +1.4 +7.0 +5.0 +12.0%" +horizontal_alignment = 2 + +[node name="InventoryPanel" type="VBoxContainer" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox" unique_id=1115098112] +custom_minimum_size = Vector2(400, 0) +layout_mode = 2 +theme_override_constants/separation = 5 + +[node name="EquipmentLabel" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=2115098112] +layout_mode = 2 +theme_override_font_sizes/font_size = 14 +text = "Equipment" + +[node name="EquipmentSpacer" type="Control" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=3000000001] +custom_minimum_size = Vector2(0, 8) +layout_mode = 2 + +[node name="EquipmentPanel" type="GridContainer" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=-1179869184] +layout_mode = 2 +theme_override_constants/h_separation = 15 +theme_override_constants/v_separation = 15 +columns = 3 + +[node name="EquipmentBottomSpacer" type="Control" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=3000000002] +custom_minimum_size = Vector2(0, 8) +layout_mode = 2 + +[node name="InventoryLabel" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=-179869184] +layout_mode = 2 +theme_override_font_sizes/font_size = 14 +text = "Inventory" + +[node name="InventoryScroll" type="ScrollContainer" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=820130816] +custom_minimum_size = Vector2(380, 120) +layout_mode = 2 + +[node name="InventoryVBox" type="VBoxContainer" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll" unique_id=1820130816] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/separation = -4 + +[node name="SelectionRectangle" type="Panel" parent="InventoryContainer/MarginContainer/VBoxContainer" unique_id=-1474836480] +z_index = 100 +custom_minimum_size = Vector2(38, 38) +layout_mode = 2 +mouse_filter = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_selection") + +[node name="InfoPanel" type="VBoxContainer" parent="InventoryContainer/MarginContainer/VBoxContainer" unique_id=-474836480] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 + +[node name="InfoLabel" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/InfoPanel" unique_id=525163520] +custom_minimum_size = Vector2(300, 64) +layout_mode = 2 +size_flags_vertical = 3 +theme_override_font_sizes/font_size = 10 +vertical_alignment = 1 +autowrap_mode = 3 diff --git a/src/scripts/game_world.gd b/src/scripts/game_world.gd index 0360c37..0ea05a4 100644 --- a/src/scripts/game_world.gd +++ b/src/scripts/game_world.gd @@ -2091,17 +2091,19 @@ func _create_chat_ui(): push_error("GameWorld: Failed to instantiate chat_ui.tscn!") func _create_inventory_ui(): - # Create inventory UI programmatically (using CanvasLayer) - var inventory_ui_script = load("res://scripts/inventory_ui.gd") - if not inventory_ui_script: - push_error("GameWorld: Could not load inventory_ui.gd script!") + # Load inventory UI scene + var inventory_ui_scene = load("res://scenes/inventory_ui.tscn") + if not inventory_ui_scene: + push_error("GameWorld: Could not load inventory_ui.tscn scene!") return - var inventory_ui = CanvasLayer.new() - inventory_ui.set_script(inventory_ui_script) - inventory_ui.name = "InventoryUI" - add_child(inventory_ui) - print("GameWorld: Inventory UI created and added to scene tree") + var inventory_ui = inventory_ui_scene.instantiate() + if inventory_ui: + inventory_ui.name = "InventoryUI" + add_child(inventory_ui) + print("GameWorld: Inventory UI scene instantiated and added to scene tree") + else: + push_error("GameWorld: Failed to instantiate inventory_ui.tscn!") func _send_player_join_message(peer_id: int, player_info: Dictionary): # Send a chat message when a player joins diff --git a/src/scripts/inventory_ui.gd b/src/scripts/inventory_ui.gd index ed3cd42..b02836d 100644 --- a/src/scripts/inventory_ui.gd +++ b/src/scripts/inventory_ui.gd @@ -2,6 +2,8 @@ extends CanvasLayer # Minimalistic Inventory UI with Stats Panel # Toggle with Tab key, shows stats, equipment slots and inventory items +# Uses inventory_slot graphics like inspiration inventory system +# Supports keyboard navigation with arrow keys var is_open: bool = false var local_player: Node = null @@ -11,22 +13,30 @@ var selected_item: Item = null # Selected inventory item var selected_slot: String = "" # Selected equipment slot name var selected_type: String = "" # "item" or "equipment" -# UI Nodes -var container: Control = null -var stats_panel: Control = null -var equipment_panel: Control = null -var inventory_grid: GridContainer = null -var scroll_container: ScrollContainer = null +# Navigation tracking (for keyboard navigation) +var inventory_selection_row: int = 0 # Current inventory row (0-based) +var inventory_selection_col: int = 0 # Current inventory column (0-based) +var equipment_selection_index: int = 0 # Current equipment slot index (0-5: mainhand, offhand, headgear, armour, boots, accessory) -# Stats labels -var label_base_stats: Label = null -var label_base_stats_value: Label = null -var label_derived_stats: Label = null -var label_derived_stats_value: Label = null +# UI Nodes (from scene) +@onready var container: Control = $InventoryContainer +@onready var stats_panel: Control = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel +@onready var equipment_panel: GridContainer = $InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/EquipmentPanel +@onready var scroll_container: ScrollContainer = $InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll +@onready var inventory_grid: VBoxContainer = $InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll/InventoryVBox +@onready var selection_rectangle: Panel = $InventoryContainer/MarginContainer/VBoxContainer/SelectionRectangle +@onready var info_panel: Control = $InventoryContainer/MarginContainer/VBoxContainer/InfoPanel +@onready var info_label: Label = $InventoryContainer/MarginContainer/VBoxContainer/InfoPanel/InfoLabel +@onready var label_base_stats: Label = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStats +@onready var label_base_stats_value: Label = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStatsValue +@onready var label_derived_stats: Label = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStats +@onready var label_derived_stats_value: Label = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStatsValue # Store button/item mappings for selection highlighting var inventory_buttons: Dictionary = {} # item -> button var equipment_buttons: Dictionary = {} # slot_name -> button +var inventory_items_list: Array = [] # Flat list of items for navigation +var inventory_rows_list: Array = [] # List of HBoxContainers (rows) # Equipment slot buttons var equipment_slots: Dictionary = { @@ -37,17 +47,101 @@ var equipment_slots: Dictionary = { "boots": null, "accessory": null } +var equipment_slots_list: Array = ["mainhand", "offhand", "headgear", "armour", "boots", "accessory"] # Order for navigation + +# StyleBoxes for inventory slots (like inspiration system) +var style_box_hover: StyleBox = null +var style_box_focused: StyleBox = null +var style_box_pressed: StyleBox = null +var style_box_empty: StyleBox = null +var quantity_font: Font = null + +# Selection animation +var selection_animation_time: float = 0.0 func _ready(): # Set layer to be above game but below chat layer = 150 - # Create UI structure - _setup_ui() + # Load styleboxes for inventory slots (like inspiration system) + _setup_styleboxes() + + # Setup fonts for labels + _setup_fonts() + + # Create equipment slot buttons (dynamically) + _create_equipment_slots() + + # Setup selection rectangle (already in scene, just configure it) + _setup_selection_rectangle() # Find local player call_deferred("_find_local_player") +func _setup_styleboxes(): + # Create styleboxes similar to inspiration inventory system + var slot_texture = preload("res://assets/gfx/ui/inventory_slot_kenny_white.png") + if not slot_texture: + # Fallback if texture doesn't exist + slot_texture = null + + style_box_empty = StyleBoxEmpty.new() + + if slot_texture: + style_box_hover = StyleBoxTexture.new() + style_box_hover.texture = slot_texture + + style_box_focused = StyleBoxTexture.new() + style_box_focused.texture = slot_texture + + style_box_pressed = StyleBoxTexture.new() + style_box_pressed.texture = slot_texture + else: + # Fallback to empty styleboxes if texture not found + style_box_hover = StyleBoxEmpty.new() + style_box_focused = StyleBoxEmpty.new() + style_box_pressed = StyleBoxEmpty.new() + + # Load quantity font (dmg_numbers.png) + if ResourceLoader.exists("res://assets/fonts/dmg_numbers.png"): + quantity_font = load("res://assets/fonts/dmg_numbers.png") + +func _setup_fonts(): + # Setup fonts for labels (standard_font.png) + var standard_font_resource = null + if ResourceLoader.exists("res://assets/fonts/standard_font.png"): + standard_font_resource = load("res://assets/fonts/standard_font.png") + if standard_font_resource: + # Stats panel labels + if label_base_stats: + label_base_stats.add_theme_font_override("font", standard_font_resource) + if label_base_stats_value: + label_base_stats_value.add_theme_font_override("font", standard_font_resource) + if label_derived_stats: + label_derived_stats.add_theme_font_override("font", standard_font_resource) + if label_derived_stats_value: + label_derived_stats_value.add_theme_font_override("font", standard_font_resource) + + # Info label + if info_label: + info_label.add_theme_font_override("font", standard_font_resource) + + # Equipment and Inventory labels + var eq_label = container.get_node_or_null("MarginContainer/VBoxContainer/HBox/InventoryPanel/EquipmentLabel") + if eq_label: + eq_label.add_theme_font_override("font", standard_font_resource) + var inv_label = container.get_node_or_null("MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryLabel") + if inv_label: + inv_label.add_theme_font_override("font", standard_font_resource) + var stats_label = container.get_node_or_null("MarginContainer/VBoxContainer/HBox/StatsPanel/StatsLabel") + if stats_label: + stats_label.add_theme_font_override("font", standard_font_resource) + +func _setup_selection_rectangle(): + # Selection rectangle is already in scene, just ensure it's configured correctly + if selection_rectangle: + selection_rectangle.visible = false + func _find_local_player(): # Find the local player var game_world = get_tree().get_first_node_in_group("game_world") @@ -66,157 +160,6 @@ func _find_local_player(): _update_ui() _update_stats() -func _setup_ui(): - # Main container (anchored to bottom-left, larger to fit stats + inventory) - container = Control.new() - container.name = "InventoryContainer" - container.set_anchors_preset(Control.PRESET_BOTTOM_LEFT) - container.offset_left = 10 # 10px padding from left - container.offset_right = 10 + 620 # Width 620px (stats 200px + inventory 400px + gap 20px) - container.offset_top = -350 # Height 340px (350 - 10) - container.offset_bottom = -10 # 10px padding from bottom - container.visible = false - add_child(container) - - # Background panel - var background = ColorRect.new() - background.name = "Background" - background.color = Color(0.1, 0.1, 0.1, 0.85) - background.mouse_filter = Control.MOUSE_FILTER_IGNORE - container.add_child(background) - background.set_anchors_preset(Control.PRESET_FULL_RECT) - - # HBox for stats and inventory side by side - var hbox = HBoxContainer.new() - hbox.name = "HBox" - container.add_child(hbox) - hbox.set_anchors_preset(Control.PRESET_FULL_RECT) - hbox.add_theme_constant_override("separation", 10) - - # Stats panel (left side) - _create_stats_panel() - hbox.add_child(stats_panel) - - # Inventory/Equipment panel (right side) - var inv_panel = VBoxContainer.new() - inv_panel.name = "InventoryPanel" - inv_panel.custom_minimum_size = Vector2(400, 0) - hbox.add_child(inv_panel) - inv_panel.add_theme_constant_override("separation", 5) - - # Equipment label - var eq_label = Label.new() - eq_label.text = "Equipment" - eq_label.add_theme_font_size_override("font_size", 14) - var standard_font_resource = null - if ResourceLoader.exists("res://assets/fonts/standard_font.png"): - standard_font_resource = load("res://assets/fonts/standard_font.png") - if standard_font_resource: - eq_label.add_theme_font_override("font", standard_font_resource) - inv_panel.add_child(eq_label) - - equipment_panel = GridContainer.new() - equipment_panel.name = "EquipmentPanel" - equipment_panel.columns = 3 - equipment_panel.add_theme_constant_override("h_separation", 5) - equipment_panel.add_theme_constant_override("v_separation", 5) - inv_panel.add_child(equipment_panel) - - # Create equipment slot buttons - _create_equipment_slots() - - # Inventory label - var inv_label = Label.new() - inv_label.text = "Inventory" - inv_label.add_theme_font_size_override("font_size", 14) - if ResourceLoader.exists("res://assets/fonts/standard_font.png"): - standard_font_resource = load("res://assets/fonts/standard_font.png") - if standard_font_resource: - inv_label.add_theme_font_override("font", standard_font_resource) - inv_panel.add_child(inv_label) - - # Scroll container for inventory - scroll_container = ScrollContainer.new() - scroll_container.name = "InventoryScroll" - scroll_container.custom_minimum_size = Vector2(380, 120) - inv_panel.add_child(scroll_container) - - # Inventory grid - inventory_grid = GridContainer.new() - inventory_grid.name = "InventoryGrid" - inventory_grid.columns = 6 - inventory_grid.add_theme_constant_override("h_separation", 3) - inventory_grid.add_theme_constant_override("v_separation", 3) - scroll_container.add_child(inventory_grid) - -func _create_stats_panel(): - stats_panel = VBoxContainer.new() - stats_panel.name = "StatsPanel" - stats_panel.custom_minimum_size = Vector2(200, 0) - - # Stats label - var stats_label = Label.new() - stats_label.text = "Stats" - stats_label.add_theme_font_size_override("font_size", 14) - var standard_font_resource = null - if ResourceLoader.exists("res://assets/fonts/standard_font.png"): - standard_font_resource = load("res://assets/fonts/standard_font.png") - if standard_font_resource: - stats_label.add_theme_font_override("font", standard_font_resource) - stats_panel.add_child(stats_label) - - # HBox for stats columns - var stats_hbox = HBoxContainer.new() - stats_hbox.name = "StatsHBox" - stats_panel.add_child(stats_hbox) - stats_hbox.add_theme_constant_override("separation", 5) - - # Base stats label - label_base_stats = Label.new() - label_base_stats.name = "LabelBaseStats" - label_base_stats.text = "Level\n\nHP\nMP\n\nSTR\nDEX\nEND\nINT\nWIS\nLCK" - label_base_stats.add_theme_font_size_override("font_size", 10) - if ResourceLoader.exists("res://assets/fonts/standard_font.png"): - var font_resource = load("res://assets/fonts/standard_font.png") - if font_resource: - label_base_stats.add_theme_font_override("font", font_resource) - stats_hbox.add_child(label_base_stats) - - # Base stats values label - label_base_stats_value = Label.new() - label_base_stats_value.name = "LabelBaseStatsValue" - label_base_stats_value.text = "1\n\n30/30\n20/20\n\n10\n10\n10\n10\n10\n10" - label_base_stats_value.add_theme_font_size_override("font_size", 10) - label_base_stats_value.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT - if ResourceLoader.exists("res://assets/fonts/standard_font.png"): - var font_resource = load("res://assets/fonts/standard_font.png") - if font_resource: - label_base_stats_value.add_theme_font_override("font", font_resource) - stats_hbox.add_child(label_base_stats_value) - - # Derived stats label - label_derived_stats = Label.new() - label_derived_stats.name = "LabelDerivedStats" - label_derived_stats.text = "XP\nCoin\n\n\n\nDMG\nDEF\nMovSpd\nAtkSpd\nSight\nSpellAmp\nCrit%" - label_derived_stats.add_theme_font_size_override("font_size", 10) - if ResourceLoader.exists("res://assets/fonts/standard_font.png"): - var font_resource = load("res://assets/fonts/standard_font.png") - if font_resource: - label_derived_stats.add_theme_font_override("font", font_resource) - stats_hbox.add_child(label_derived_stats) - - # Derived stats values label - label_derived_stats_value = Label.new() - label_derived_stats_value.name = "LabelDerivedStatsValue" - label_derived_stats_value.text = "0/100\n0\n\n\n\n2.0\n2.0\n2.1\n1.4\n7.0\n5.0\n12.0%" - label_derived_stats_value.add_theme_font_size_override("font_size", 10) - label_derived_stats_value.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT - if ResourceLoader.exists("res://assets/fonts/standard_font.png"): - var font_resource = load("res://assets/fonts/standard_font.png") - if font_resource: - label_derived_stats_value.add_theme_font_override("font", font_resource) - stats_hbox.add_child(label_derived_stats_value) - func _update_stats(): if not local_player or not local_player.character_stats: return @@ -248,7 +191,7 @@ func _update_stats(): func _create_equipment_slots(): # Equipment slot order: mainhand, offhand, headgear, armour, boots, accessory var slot_names = ["mainhand", "offhand", "headgear", "armour", "boots", "accessory"] - var slot_labels = ["Weapon", "Shield", "Head", "Armour", "Boots", "Accessory"] + var slot_labels = ["Main-hand", "Off-hand", "Head", "Armour", "Boots", "Accessory"] for i in range(slot_names.size()): var slot_name = slot_names[i] @@ -264,32 +207,73 @@ func _create_equipment_slots(): label.text = slot_label label.add_theme_font_size_override("font_size", 10) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + var standard_font_resource = null if ResourceLoader.exists("res://assets/fonts/standard_font.png"): var font_resource = load("res://assets/fonts/standard_font.png") if font_resource: label.add_theme_font_override("font", font_resource) slot_container.add_child(label) - # Button + # Button (use styleboxes like inspiration system) var button = Button.new() button.name = slot_name + "_btn" - button.custom_minimum_size = Vector2(60, 60) - button.flat = true + button.custom_minimum_size = Vector2(24, 24) # Smaller like inspiration (24x24 instead of 60x60) + button.size = Vector2(24, 24) + if style_box_empty: + button.add_theme_stylebox_override("normal", style_box_empty) + if style_box_hover: + button.add_theme_stylebox_override("hover", style_box_hover) + if style_box_focused: + button.add_theme_stylebox_override("focus", style_box_focused) + if style_box_pressed: + button.add_theme_stylebox_override("pressed", style_box_pressed) + button.flat = false # Use styleboxes instead of flat button.connect("pressed", _on_equipment_slot_pressed.bind(slot_name)) slot_container.add_child(button) equipment_slots[slot_name] = button equipment_buttons[slot_name] = button +func _has_equipment_in_slot(slot_name: String) -> bool: + # Check if there's an item equipped in this slot + if not local_player or not local_player.character_stats: + return false + return local_player.character_stats.equipment[slot_name] != null + +func _find_next_filled_equipment_slot(start_index: int, direction: int) -> int: + # Find next filled equipment slot, or -1 if none found + var current_index = start_index + for _i in range(equipment_slots_list.size()): + current_index += direction + if current_index < 0: + current_index = equipment_slots_list.size() - 1 + elif current_index >= equipment_slots_list.size(): + current_index = 0 + + var slot_name = equipment_slots_list[current_index] + if _has_equipment_in_slot(slot_name): + return current_index + return -1 + func _on_equipment_slot_pressed(slot_name: String): if not local_player or not local_player.character_stats: return + # Only select if there's an item equipped + if not _has_equipment_in_slot(slot_name): + return + # Select this slot selected_slot = slot_name selected_item = local_player.character_stats.equipment[slot_name] selected_type = "equipment" if selected_item else "" + + # Update navigation position + if slot_name in equipment_slots_list: + equipment_selection_index = equipment_slots_list.find(slot_name) + _update_selection_highlight() + _update_selection_rectangle() func _update_selection_highlight(): # Reset all button styles @@ -304,30 +288,66 @@ func _update_selection_highlight(): var highlight = button.get_node_or_null("Highlight") if highlight: highlight.queue_free() + +func _update_selection_rectangle(): + # Update visual selection rectangle position and visibility + if not selection_rectangle: + return + + var target_button: Button = null + var target_position: Vector2 = Vector2.ZERO + var should_show: bool = false + + # Get the parent of selection_rectangle (VBoxContainer) to calculate relative positions + var selection_parent = selection_rectangle.get_parent() + if not selection_parent: + selection_rectangle.visible = false + return - # Highlight selected equipment slot if selected_type == "equipment" and selected_slot != "": - var button = equipment_buttons.get(selected_slot) - if button: - var highlight = ColorRect.new() - highlight.name = "Highlight" - highlight.color = Color(1.0, 1.0, 0.0, 0.6) - highlight.mouse_filter = Control.MOUSE_FILTER_IGNORE - highlight.z_index = 10 - button.add_child(highlight) - highlight.set_anchors_preset(Control.PRESET_FULL_RECT) + # Show rectangle on equipment slot (only if it has an item) + if _has_equipment_in_slot(selected_slot): + target_button = equipment_buttons.get(selected_slot) + if target_button and target_button.is_inside_tree(): + # Get button position relative to selection_rectangle's parent (VBoxContainer) + var button_global_pos = target_button.global_position + var parent_global_pos = selection_parent.global_position + target_position = button_global_pos - parent_global_pos + should_show = true + elif selected_type == "item" and inventory_selection_row >= 0 and inventory_selection_row < inventory_rows_list.size(): + # Show rectangle on inventory item + var row = inventory_rows_list[inventory_selection_row] + if row and inventory_selection_col >= 0 and inventory_selection_col < row.get_child_count(): + target_button = row.get_child(inventory_selection_col) as Button + if target_button and target_button.is_inside_tree(): + # Get button position relative to selection_rectangle's parent (VBoxContainer) + var button_global_pos = target_button.global_position + var parent_global_pos = selection_parent.global_position + target_position = button_global_pos - parent_global_pos + should_show = true - # Highlight selected inventory item - if selected_type == "item" and selected_item: - var button = inventory_buttons.get(selected_item) - if button: - var highlight = ColorRect.new() - highlight.name = "Highlight" - highlight.color = Color(1.0, 1.0, 0.0, 0.6) - highlight.mouse_filter = Control.MOUSE_FILTER_IGNORE - highlight.z_index = 10 - button.add_child(highlight) - highlight.set_anchors_preset(Control.PRESET_FULL_RECT) + # Only show and position if we have a valid target + if should_show and target_button: + selection_rectangle.visible = true + selection_rectangle.position = target_position + selection_rectangle.size = Vector2(38, 38) + else: + selection_rectangle.visible = false + +func _process(delta): + if is_open and selection_rectangle and selection_rectangle.visible: + # Animate selection rectangle border color + selection_animation_time += delta * 2.0 # Speed of animation + # Animate between yellow and orange + var color1 = Color.YELLOW + var color2 = Color(1.0, 0.7, 0.0) # Orange-yellow + var t = (sin(selection_animation_time) + 1.0) / 2.0 # 0 to 1 + var animated_color = color1.lerp(color2, t) + + # Update border color + var stylebox = selection_rectangle.get_theme_stylebox("panel") as StyleBoxFlat + if stylebox: + stylebox.border_color = animated_color func _update_ui(): if not local_player or not local_player.character_stats: @@ -335,8 +355,16 @@ func _update_ui(): var char_stats = local_player.character_stats + # Debug: Print inventory contents + print("InventoryUI: Updating UI - inventory size: ", char_stats.inventory.size()) + for i in range(char_stats.inventory.size()): + var item = char_stats.inventory[i] + print(" Item ", i, ": ", item.item_name if item else "null") + # Clear button mappings inventory_buttons.clear() + inventory_items_list.clear() + inventory_rows_list.clear() # Update equipment slots for slot_name in equipment_slots.keys(): @@ -351,39 +379,56 @@ func _update_ui(): var equipped_item = char_stats.equipment[slot_name] if equipped_item: var sprite = Sprite2D.new() - var texture_path = "" - var use_equipment_path = false - - if slot_name == "mainhand" or slot_name == "offhand": - texture_path = equipped_item.spritePath - use_equipment_path = false - else: - texture_path = equipped_item.equipmentPath if equipped_item.equipmentPath != "" else equipped_item.spritePath - use_equipment_path = (equipped_item.equipmentPath != "") + # Equipment slots should ONLY use spritePath (items_n_shit.png), NOT equipmentPath + # equipmentPath is for character rendering only! + var texture_path = equipped_item.spritePath var texture = load(texture_path) if texture: sprite.texture = texture - if use_equipment_path: - sprite.hframes = 35 - sprite.vframes = 8 - else: - sprite.hframes = equipped_item.spriteFrames.x if equipped_item.spriteFrames.x > 0 else 20 - sprite.vframes = equipped_item.spriteFrames.y if equipped_item.spriteFrames.y > 0 else 14 - sprite.frame = equipped_item.spriteFrame - sprite.scale = Vector2(1.2, 1.2) + sprite.hframes = equipped_item.spriteFrames.x if equipped_item.spriteFrames.x > 0 else 20 + sprite.vframes = equipped_item.spriteFrames.y if equipped_item.spriteFrames.y > 0 else 14 + sprite.frame = equipped_item.spriteFrame + sprite.centered = false # Like inspiration system + sprite.position = Vector2(4, 4) # Like inspiration system + sprite.scale = Vector2(2.0, 2.0) # 2x size as requested button.add_child(sprite) - # Update inventory grid + # Update inventory grid - clear existing HBoxContainers for child in inventory_grid.get_children(): child.queue_free() + # Add inventory items using HBoxContainers (like inspiration system) + var current_hbox: HBoxContainer = null + var items_per_row = 10 # Items per row (like inspiration system - they use 10 per HBox) + var items_in_current_row = 0 + for item in char_stats.inventory: - var button = Button.new() - button.custom_minimum_size = Vector2(60, 60) - button.flat = true - button.connect("pressed", _on_inventory_item_pressed.bind(item)) + # Create new HBoxContainer if needed + if current_hbox == null or items_in_current_row >= items_per_row: + current_hbox = HBoxContainer.new() + current_hbox.add_theme_constant_override("separation", 0) # No separation like inspiration + inventory_grid.add_child(current_hbox) + inventory_rows_list.append(current_hbox) + items_in_current_row = 0 + # Create button with styleboxes (like inspiration system) + var button = Button.new() + button.custom_minimum_size = Vector2(24, 24) # Smaller like inspiration (24x24) + button.size = Vector2(24, 24) + if style_box_empty: + button.add_theme_stylebox_override("normal", style_box_empty) + if style_box_hover: + button.add_theme_stylebox_override("hover", style_box_hover) + if style_box_focused: + button.add_theme_stylebox_override("focus", style_box_focused) + if style_box_pressed: + button.add_theme_stylebox_override("pressed", style_box_pressed) + button.flat = false # Use styleboxes + button.connect("pressed", _on_inventory_item_pressed.bind(item)) + current_hbox.add_child(button) + + # Add item sprite (like inspiration system - positioned at 4,4 with centered=false) if item.spritePath != "": var sprite = Sprite2D.new() var texture = load(item.spritePath) @@ -392,17 +437,264 @@ func _update_ui(): sprite.hframes = item.spriteFrames.x if item.spriteFrames.x > 0 else 20 sprite.vframes = item.spriteFrames.y if item.spriteFrames.y > 0 else 14 sprite.frame = item.spriteFrame - sprite.scale = Vector2(1.2, 1.2) + sprite.centered = false # Like inspiration system + sprite.position = Vector2(4, 4) # Like inspiration system + sprite.scale = Vector2(2.0, 2.0) # 2x size as requested button.add_child(sprite) - inventory_grid.add_child(button) + # Add quantity label if item can have multiple (like inspiration system) + if item.can_have_multiple_of and item.quantity > 1: + var quantity_label = Label.new() + quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + quantity_label.size = Vector2(24, 24) + quantity_label.custom_minimum_size = Vector2(0, 0) + quantity_label.position = Vector2(10, 2) + quantity_label.text = str(item.quantity) + if quantity_font: + quantity_label.add_theme_font_override("font", quantity_font) + quantity_label.add_theme_font_size_override("font_size", 8) + quantity_label.scale = Vector2(0.5, 0.5) + button.add_child(quantity_label) + inventory_buttons[item] = button + inventory_items_list.append(item) + items_in_current_row += 1 - # Update selection highlight - _update_selection_highlight() + # Clamp selection to valid range + if inventory_selection_row >= inventory_rows_list.size(): + inventory_selection_row = max(0, inventory_rows_list.size() - 1) + if inventory_selection_row >= 0 and inventory_selection_row < inventory_rows_list.size(): + var row = inventory_rows_list[inventory_selection_row] + if inventory_selection_col >= row.get_child_count(): + inventory_selection_col = max(0, row.get_child_count() - 1) - # Update stats - _update_stats() + # Update selection + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + +func _update_selection_from_navigation(): + # Update selected_item/selected_slot based on navigation position + if selected_type == "equipment" and equipment_selection_index >= 0 and equipment_selection_index < equipment_slots_list.size(): + var slot_name = equipment_slots_list[equipment_selection_index] + if _has_equipment_in_slot(slot_name): + selected_slot = slot_name + if local_player and local_player.character_stats: + selected_item = local_player.character_stats.equipment[slot_name] + else: + # Empty slot - switch to inventory + selected_type = "item" + selected_slot = "" + selected_item = null + if inventory_rows_list.size() > 0: + inventory_selection_row = 0 + inventory_selection_col = 0 + _update_selection_from_navigation() + elif selected_type == "item" and inventory_selection_row >= 0 and inventory_selection_row < inventory_rows_list.size(): + var row = inventory_rows_list[inventory_selection_row] + if inventory_selection_col >= 0 and inventory_selection_col < row.get_child_count(): + var item_index = inventory_selection_row * 10 + inventory_selection_col + if item_index >= 0 and item_index < inventory_items_list.size(): + selected_item = inventory_items_list[item_index] + selected_slot = "" + +func _format_item_info(item: Item) -> String: + # Format item description, stats modifiers, and controls + var text = "" + + # Description + if item.description != "": + text += item.description + else: + text += item.item_name + + text += "\n\n" + + # Stats modifiers + var stat_lines = [] + if item.modifiers.has("str"): + stat_lines.append("STR: +%d" % item.modifiers["str"]) + if item.modifiers.has("dex"): + stat_lines.append("DEX: +%d" % item.modifiers["dex"]) + if item.modifiers.has("end"): + stat_lines.append("END: +%d" % item.modifiers["end"]) + if item.modifiers.has("int"): + stat_lines.append("INT: +%d" % item.modifiers["int"]) + if item.modifiers.has("wis"): + stat_lines.append("WIS: +%d" % item.modifiers["wis"]) + if item.modifiers.has("lck"): + stat_lines.append("LCK: +%d" % item.modifiers["lck"]) + if item.modifiers.has("dmg"): + stat_lines.append("DMG: +%d" % item.modifiers["dmg"]) + if item.modifiers.has("def"): + stat_lines.append("DEF: +%d" % item.modifiers["def"]) + if item.modifiers.has("hp"): + stat_lines.append("HP: +%d" % item.modifiers["hp"]) + if item.modifiers.has("mp"): + stat_lines.append("MP: +%d" % item.modifiers["mp"]) + if item.modifiers.has("maxhp"): + stat_lines.append("MAXHP: +%d" % item.modifiers["maxhp"]) + if item.modifiers.has("maxmp"): + stat_lines.append("MAXMP: +%d" % item.modifiers["maxmp"]) + + if stat_lines.size() > 0: + text += "\n".join(stat_lines) + text += "\n\n" + + # Controls + if item.item_type == Item.ItemType.Equippable: + if selected_type == "equipment": + text += "Press F to unequip" + else: + text += "Press F to equip" + elif item.item_type == Item.ItemType.Restoration: + text += "Press F to consume" + + if selected_type == "item": + text += "\nPress E to drop" + + return text + +func _update_info_panel(): + # Update info panel based on selected item + if not info_label: + return + + if selected_item: + info_label.text = _format_item_info(selected_item) + else: + info_label.text = "" + +func _navigate_inventory(direction: String): + # Handle navigation within inventory + var items_per_row = 10 + + match direction: + "left": + if inventory_selection_col > 0: + inventory_selection_col -= 1 + else: + # Wrap to end of previous row + if inventory_selection_row > 0: + inventory_selection_row -= 1 + var row = inventory_rows_list[inventory_selection_row] + inventory_selection_col = row.get_child_count() - 1 + "right": + if inventory_selection_row < inventory_rows_list.size(): + var row = inventory_rows_list[inventory_selection_row] + if inventory_selection_col < row.get_child_count() - 1: + inventory_selection_col += 1 + else: + # Wrap to start of next row + if inventory_selection_row < inventory_rows_list.size() - 1: + inventory_selection_row += 1 + inventory_selection_col = 0 + "up": + if inventory_selection_row > 0: + inventory_selection_row -= 1 + # Clamp column to valid range + var row = inventory_rows_list[inventory_selection_row] + if inventory_selection_col >= row.get_child_count(): + inventory_selection_col = row.get_child_count() - 1 + else: + # Move to equipment slots (only if there are filled slots) + var next_equip_index = _find_next_filled_equipment_slot(-1, 1) # Start from end, go forward + if next_equip_index >= 0: + selected_type = "equipment" + equipment_selection_index = next_equip_index + selected_slot = equipment_slots_list[next_equip_index] + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + return + "down": + if inventory_selection_row < inventory_rows_list.size() - 1: + inventory_selection_row += 1 + # Clamp column to valid range + var row = inventory_rows_list[inventory_selection_row] + if inventory_selection_col >= row.get_child_count(): + inventory_selection_col = row.get_child_count() - 1 + # Can't go down from inventory (already at bottom) + + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + +func _navigate_equipment(direction: String): + # Handle navigation within equipment slots (only filled slots) + # Equipment layout: 3 columns, 2 rows + # Row 1: mainhand(0), offhand(1), headgear(2) + # Row 2: armour(3), boots(4), accessory(5) + + match direction: + "left": + var next_index = _find_next_filled_equipment_slot(equipment_selection_index, -1) + if next_index >= 0: + equipment_selection_index = next_index + "right": + var next_index = _find_next_filled_equipment_slot(equipment_selection_index, 1) + if next_index >= 0: + equipment_selection_index = next_index + "up": + # Find next filled slot in row above (same column) + var current_row = int(equipment_selection_index / 3) + var current_col = equipment_selection_index % 3 + if current_row > 0: + var target_index = (current_row - 1) * 3 + current_col + var target_slot = equipment_slots_list[target_index] + if _has_equipment_in_slot(target_slot): + equipment_selection_index = target_index + else: + # Skip to next filled slot in that row + var next_index = _find_next_filled_equipment_slot(target_index - 1, 1) + if next_index >= 0 and next_index < 3: # Make sure it's in row 0 + equipment_selection_index = next_index + # Can't go up from equipment (already at top) + "down": + # Find next filled slot in row below (same column), or move to inventory + var current_row = int(equipment_selection_index / 3) + var current_col = equipment_selection_index % 3 + if current_row < 1: + var target_index = (current_row + 1) * 3 + current_col + var target_slot = equipment_slots_list[target_index] + if _has_equipment_in_slot(target_slot): + equipment_selection_index = target_index + else: + # No filled slot below, move to inventory + selected_type = "item" + inventory_selection_row = 0 + inventory_selection_col = current_col + # Clamp to valid range + if inventory_rows_list.size() > 0: + var inv_row = inventory_rows_list[0] + if inventory_selection_col >= inv_row.get_child_count(): + inventory_selection_col = inv_row.get_child_count() - 1 + else: + inventory_selection_col = 0 + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + return + else: + # Already at bottom row, move to inventory + selected_type = "item" + inventory_selection_row = 0 + inventory_selection_col = current_col + # Clamp to valid range + if inventory_rows_list.size() > 0: + var inv_row = inventory_rows_list[0] + if inventory_selection_col >= inv_row.get_child_count(): + inventory_selection_col = inv_row.get_child_count() - 1 + else: + inventory_selection_col = 0 + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + return + + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() func _on_inventory_item_pressed(item: Item): if not local_player or not local_player.character_stats: @@ -411,7 +703,16 @@ func _on_inventory_item_pressed(item: Item): selected_item = item selected_slot = "" selected_type = "item" + + # Update navigation position + var item_index = inventory_items_list.find(item) + if item_index >= 0: + var items_per_row = 10 + inventory_selection_row = int(item_index / items_per_row) + inventory_selection_col = item_index % items_per_row + _update_selection_highlight() + _update_selection_rectangle() func _on_character_changed(_char: CharacterStats): _update_ui() @@ -430,6 +731,26 @@ func _input(event): if not is_open: return + # Arrow key navigation + if event is InputEventKey and event.pressed and not event.echo: + var direction = "" + if event.keycode == KEY_LEFT: + direction = "left" + elif event.keycode == KEY_RIGHT: + direction = "right" + elif event.keycode == KEY_UP: + direction = "up" + elif event.keycode == KEY_DOWN: + direction = "down" + + if direction != "": + if selected_type == "equipment": + _navigate_equipment(direction) + else: + _navigate_inventory(direction) + get_viewport().set_input_as_handled() + return + # F key: Unequip/equip items if event is InputEventKey and event.keycode == KEY_F and event.pressed and not event.echo: _handle_f_key() @@ -452,22 +773,82 @@ func _handle_f_key(): var equipped_item = char_stats.equipment[selected_slot] if equipped_item: char_stats.unequip_item(equipped_item) - selected_item = null - selected_slot = "" - selected_type = "" + # After unequipping, if all equipment is empty, go to inventory + var has_any_equipment = false + for slot in equipment_slots_list: + if _has_equipment_in_slot(slot): + has_any_equipment = true + break + + if not has_any_equipment: + # No equipment left, go to inventory + selected_type = "item" + selected_slot = "" + selected_item = null + if inventory_rows_list.size() > 0: + inventory_selection_row = 0 + inventory_selection_col = 0 + else: + selected_type = "" + else: + # Find next filled equipment slot + var next_index = _find_next_filled_equipment_slot(equipment_selection_index, 1) + if next_index >= 0: + equipment_selection_index = next_index + selected_slot = equipment_slots_list[next_index] + selected_item = char_stats.equipment[selected_slot] + else: + # No more equipment, go to inventory + selected_type = "item" + selected_slot = "" + selected_item = null + if inventory_rows_list.size() > 0: + inventory_selection_row = 0 + inventory_selection_col = 0 + _update_selection_from_navigation() + _update_selection_rectangle() return if selected_type == "item" and selected_item: if selected_item.item_type == Item.ItemType.Equippable and selected_item.equipment_type != Item.EquipmentType.NONE: + # Remember which slot the item will be equipped to + var target_slot_name = "" + match selected_item.equipment_type: + Item.EquipmentType.MAINHAND: + target_slot_name = "mainhand" + Item.EquipmentType.OFFHAND: + target_slot_name = "offhand" + Item.EquipmentType.HEADGEAR: + target_slot_name = "headgear" + Item.EquipmentType.ARMOUR: + target_slot_name = "armour" + Item.EquipmentType.BOOTS: + target_slot_name = "boots" + Item.EquipmentType.ACCESSORY: + target_slot_name = "accessory" + + # Check if this is the last item in inventory (before equipping) + var was_last_item = char_stats.inventory.size() == 1 + char_stats.equip_item(selected_item) - selected_item = null - selected_slot = "" - selected_type = "" + + # If this was the last item, set selection state BEFORE _update_ui() + # so that _update_selection_from_navigation() works correctly + if was_last_item and target_slot_name != "": + if target_slot_name in equipment_slots_list: + selected_type = "equipment" + selected_slot = target_slot_name + equipment_selection_index = equipment_slots_list.find(target_slot_name) + selected_item = char_stats.equipment[target_slot_name] + + _update_ui() + + # Update selection rectangle and info panel + _update_selection_rectangle() + _update_info_panel() elif selected_item.item_type == Item.ItemType.Restoration: _use_consumable_item(selected_item) - selected_item = null - selected_slot = "" - selected_type = "" + _update_info_panel() func _use_consumable_item(item: Item): if not local_player or not local_player.character_stats: @@ -539,6 +920,32 @@ func _open_inventory(): _lock_player_controls(true) _update_ui() + # Initialize selection - prefer inventory, but if empty, check equipment + if inventory_rows_list.size() > 0: + selected_type = "item" + inventory_selection_row = 0 + inventory_selection_col = 0 + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + else: + # No inventory items, try equipment + var first_filled_slot = _find_next_filled_equipment_slot(-1, 1) + if first_filled_slot >= 0: + selected_type = "equipment" + equipment_selection_index = first_filled_slot + selected_slot = equipment_slots_list[first_filled_slot] + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + else: + # Nothing to select + selected_type = "" + if selection_rectangle: + selection_rectangle.visible = false + if info_label: + info_label.text = "" + if not local_player: _find_local_player() @@ -550,6 +957,9 @@ func _close_inventory(): if container: container.visible = false + if selection_rectangle: + selection_rectangle.visible = false + selected_item = null selected_slot = "" selected_type = "" diff --git a/src/scripts/inventory_ui_refactored.gd b/src/scripts/inventory_ui_refactored.gd new file mode 100644 index 0000000..6039be2 --- /dev/null +++ b/src/scripts/inventory_ui_refactored.gd @@ -0,0 +1,943 @@ +extends CanvasLayer + +# Minimalistic Inventory UI with Stats Panel +# Toggle with Tab key, shows stats, equipment slots and inventory items +# Uses inventory_slot graphics like inspiration inventory system +# Supports keyboard navigation with arrow keys + +var is_open: bool = false +var local_player: Node = null + +# Selection tracking +var selected_item: Item = null # Selected inventory item +var selected_slot: String = "" # Selected equipment slot name +var selected_type: String = "" # "item" or "equipment" + +# Navigation tracking (for keyboard navigation) +var inventory_selection_row: int = 0 # Current inventory row (0-based) +var inventory_selection_col: int = 0 # Current inventory column (0-based) +var equipment_selection_index: int = 0 # Current equipment slot index (0-5: mainhand, offhand, headgear, armour, boots, accessory) + +# UI Nodes (from scene) +@onready var container: Control = $InventoryContainer +@onready var stats_panel: Control = $InventoryContainer/HBox/StatsPanel +@onready var equipment_panel: GridContainer = $InventoryContainer/HBox/InventoryPanel/EquipmentPanel +@onready var scroll_container: ScrollContainer = $InventoryContainer/HBox/InventoryPanel/InventoryScroll +@onready var inventory_grid: VBoxContainer = $InventoryContainer/HBox/InventoryPanel/InventoryScroll/InventoryVBox +@onready var selection_rectangle: Panel = $InventoryContainer/SelectionRectangle +@onready var info_panel: Control = $InventoryContainer/InfoPanel +@onready var info_label: Label = $InventoryContainer/InfoPanel/InfoLabel +@onready var label_base_stats: Label = $InventoryContainer/HBox/StatsPanel/StatsHBox/LabelBaseStats +@onready var label_base_stats_value: Label = $InventoryContainer/HBox/StatsPanel/StatsHBox/LabelBaseStatsValue +@onready var label_derived_stats: Label = $InventoryContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStats +@onready var label_derived_stats_value: Label = $InventoryContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStatsValue + +# Store button/item mappings for selection highlighting +var inventory_buttons: Dictionary = {} # item -> button +var equipment_buttons: Dictionary = {} # slot_name -> button +var inventory_items_list: Array = [] # Flat list of items for navigation +var inventory_rows_list: Array = [] # List of HBoxContainers (rows) + +# Equipment slot buttons +var equipment_slots: Dictionary = { + "mainhand": null, + "offhand": null, + "headgear": null, + "armour": null, + "boots": null, + "accessory": null +} +var equipment_slots_list: Array = ["mainhand", "offhand", "headgear", "armour", "boots", "accessory"] # Order for navigation + +# StyleBoxes for inventory slots (like inspiration system) +var style_box_hover: StyleBox = null +var style_box_focused: StyleBox = null +var style_box_pressed: StyleBox = null +var style_box_empty: StyleBox = null +var quantity_font: Font = null + +# Selection animation +var selection_animation_time: float = 0.0 + +func _ready(): + # Set layer to be above game but below chat + layer = 150 + + # Load styleboxes for inventory slots (like inspiration system) + _setup_styleboxes() + + # Setup fonts for labels + _setup_fonts() + + # Create equipment slot buttons (dynamically) + _create_equipment_slots() + + # Setup selection rectangle (already in scene, just configure it) + _setup_selection_rectangle() + + # Find local player + call_deferred("_find_local_player") + +func _setup_styleboxes(): + # Create styleboxes similar to inspiration inventory system + var slot_texture = preload("res://assets/gfx/ui/inventory_slot_kenny_white.png") + if not slot_texture: + # Fallback if texture doesn't exist + slot_texture = null + + style_box_empty = StyleBoxEmpty.new() + + if slot_texture: + style_box_hover = StyleBoxTexture.new() + style_box_hover.texture = slot_texture + + style_box_focused = StyleBoxTexture.new() + style_box_focused.texture = slot_texture + + style_box_pressed = StyleBoxTexture.new() + style_box_pressed.texture = slot_texture + else: + # Fallback to empty styleboxes if texture not found + style_box_hover = StyleBoxEmpty.new() + style_box_focused = StyleBoxEmpty.new() + style_box_pressed = StyleBoxEmpty.new() + + # Load quantity font (dmg_numbers.png) + if ResourceLoader.exists("res://assets/fonts/dmg_numbers.png"): + quantity_font = load("res://assets/fonts/dmg_numbers.png") + +func _setup_fonts(): + # Setup fonts for labels (standard_font.png) + var standard_font_resource = null + if ResourceLoader.exists("res://assets/fonts/standard_font.png"): + standard_font_resource = load("res://assets/fonts/standard_font.png") + if standard_font_resource: + # Stats panel labels + if label_base_stats: + label_base_stats.add_theme_font_override("font", standard_font_resource) + if label_base_stats_value: + label_base_stats_value.add_theme_font_override("font", standard_font_resource) + if label_derived_stats: + label_derived_stats.add_theme_font_override("font", standard_font_resource) + if label_derived_stats_value: + label_derived_stats_value.add_theme_font_override("font", standard_font_resource) + + # Info label + if info_label: + info_label.add_theme_font_override("font", standard_font_resource) + + # Equipment and Inventory labels + var eq_label = container.get_node_or_null("HBox/InventoryPanel/EquipmentLabel") + if eq_label: + eq_label.add_theme_font_override("font", standard_font_resource) + var inv_label = container.get_node_or_null("HBox/InventoryPanel/InventoryLabel") + if inv_label: + inv_label.add_theme_font_override("font", standard_font_resource) + var stats_label = container.get_node_or_null("HBox/StatsPanel/StatsLabel") + if stats_label: + stats_label.add_theme_font_override("font", standard_font_resource) + +func _setup_selection_rectangle(): + # Selection rectangle is already in scene, just ensure it's configured correctly + if selection_rectangle: + selection_rectangle.visible = false + +func _find_local_player(): + # Find the local player + var game_world = get_tree().get_first_node_in_group("game_world") + if game_world: + var player_manager = game_world.get_node_or_null("PlayerManager") + if player_manager: + var local_players = player_manager.get_local_players() + if local_players.size() > 0: + local_player = local_players[0] + if local_player and local_player.character_stats: + # Connect to character_changed signal + if local_player.character_stats.character_changed.is_connected(_on_character_changed): + local_player.character_stats.character_changed.disconnect(_on_character_changed) + local_player.character_stats.character_changed.connect(_on_character_changed) + # Initial update + _update_ui() + _update_stats() + +func _update_stats(): + if not local_player or not local_player.character_stats: + return + + var char_stats = local_player.character_stats + + # Update base stats + label_base_stats_value.text = str(char_stats.level) + "\n\n" + \ + str(int(char_stats.hp)) + "/" + str(int(char_stats.maxhp)) + "\n" + \ + str(int(char_stats.mp)) + "/" + str(int(char_stats.maxmp)) + "\n\n" + \ + str(char_stats.baseStats.str) + "\n" + \ + str(char_stats.baseStats.dex) + "\n" + \ + str(char_stats.baseStats.end) + "\n" + \ + str(char_stats.baseStats.int) + "\n" + \ + str(char_stats.baseStats.wis) + "\n" + \ + str(char_stats.baseStats.lck) + + # Update derived stats + label_derived_stats_value.text = str(int(char_stats.xp)) + "/" + str(int(char_stats.xp_to_next_level)) + "\n" + \ + str(char_stats.coin) + "\n\n\n\n" + \ + str(char_stats.damage) + "\n" + \ + str(char_stats.defense) + "\n" + \ + str(char_stats.move_speed) + "\n" + \ + str(char_stats.attack_speed) + "\n" + \ + str(char_stats.sight) + "\n" + \ + str(char_stats.spell_amp) + "\n" + \ + str(char_stats.crit_chance) + "%" + +func _create_equipment_slots(): + # Equipment slot order: mainhand, offhand, headgear, armour, boots, accessory + var slot_names = ["mainhand", "offhand", "headgear", "armour", "boots", "accessory"] + var slot_labels = ["Main-hand", "Off-hand", "Head", "Armour", "Boots", "Accessory"] + + for i in range(slot_names.size()): + var slot_name = slot_names[i] + var slot_label = slot_labels[i] + + # Container for slot + var slot_container = VBoxContainer.new() + slot_container.name = slot_name + "_slot" + equipment_panel.add_child(slot_container) + + # Label + var label = Label.new() + label.text = slot_label + label.add_theme_font_size_override("font_size", 10) + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + var standard_font_resource = null + if ResourceLoader.exists("res://assets/fonts/standard_font.png"): + var font_resource = load("res://assets/fonts/standard_font.png") + if font_resource: + label.add_theme_font_override("font", font_resource) + slot_container.add_child(label) + + # Button (use styleboxes like inspiration system) + var button = Button.new() + button.name = slot_name + "_btn" + button.custom_minimum_size = Vector2(24, 24) # Smaller like inspiration (24x24 instead of 60x60) + button.size = Vector2(24, 24) + if style_box_empty: + button.add_theme_stylebox_override("normal", style_box_empty) + if style_box_hover: + button.add_theme_stylebox_override("hover", style_box_hover) + if style_box_focused: + button.add_theme_stylebox_override("focus", style_box_focused) + if style_box_pressed: + button.add_theme_stylebox_override("pressed", style_box_pressed) + button.flat = false # Use styleboxes instead of flat + button.connect("pressed", _on_equipment_slot_pressed.bind(slot_name)) + slot_container.add_child(button) + + equipment_slots[slot_name] = button + equipment_buttons[slot_name] = button + +func _has_equipment_in_slot(slot_name: String) -> bool: + # Check if there's an item equipped in this slot + if not local_player or not local_player.character_stats: + return false + return local_player.character_stats.equipment[slot_name] != null + +func _find_next_filled_equipment_slot(start_index: int, direction: int) -> int: + # Find next filled equipment slot, or -1 if none found + var current_index = start_index + for _i in range(equipment_slots_list.size()): + current_index += direction + if current_index < 0: + current_index = equipment_slots_list.size() - 1 + elif current_index >= equipment_slots_list.size(): + current_index = 0 + + var slot_name = equipment_slots_list[current_index] + if _has_equipment_in_slot(slot_name): + return current_index + return -1 + +func _on_equipment_slot_pressed(slot_name: String): + if not local_player or not local_player.character_stats: + return + + # Only select if there's an item equipped + if not _has_equipment_in_slot(slot_name): + return + + # Select this slot + selected_slot = slot_name + selected_item = local_player.character_stats.equipment[slot_name] + selected_type = "equipment" if selected_item else "" + + # Update navigation position + if slot_name in equipment_slots_list: + equipment_selection_index = equipment_slots_list.find(slot_name) + + _update_selection_highlight() + _update_selection_rectangle() + +func _update_selection_highlight(): + # Reset all button styles + for button in equipment_buttons.values(): + if button: + var highlight = button.get_node_or_null("Highlight") + if highlight: + highlight.queue_free() + + for button in inventory_buttons.values(): + if button: + var highlight = button.get_node_or_null("Highlight") + if highlight: + highlight.queue_free() + +func _update_selection_rectangle(): + # Update visual selection rectangle position and visibility + if not selection_rectangle or not container: + return + + var target_button: Button = null + var target_position: Vector2 = Vector2.ZERO + + if selected_type == "equipment" and selected_slot != "": + # Show rectangle on equipment slot (only if it has an item) + if _has_equipment_in_slot(selected_slot): + target_button = equipment_buttons.get(selected_slot) + if target_button and target_button.is_inside_tree(): + # Get button position relative to container + var button_global_pos = target_button.global_position + var container_global_pos = container.global_position + target_position = button_global_pos - container_global_pos + selection_rectangle.visible = true + else: + selection_rectangle.visible = false + else: + selection_rectangle.visible = false + elif selected_type == "item" and inventory_selection_row < inventory_rows_list.size(): + # Show rectangle on inventory item + var row = inventory_rows_list[inventory_selection_row] + if row and inventory_selection_col < row.get_child_count(): + target_button = row.get_child(inventory_selection_col) as Button + if target_button and target_button.is_inside_tree(): + # Get button position relative to container + var button_global_pos = target_button.global_position + var container_global_pos = container.global_position + target_position = button_global_pos - container_global_pos + selection_rectangle.visible = true + else: + selection_rectangle.visible = false + else: + selection_rectangle.visible = false + else: + selection_rectangle.visible = false + + if target_button and selection_rectangle.visible: + selection_rectangle.position = target_position + # Ensure size is correct (48x48 to match sprite scale) + selection_rectangle.size = Vector2(48, 48) + +func _process(delta): + if is_open and selection_rectangle and selection_rectangle.visible: + # Animate selection rectangle border color + selection_animation_time += delta * 2.0 # Speed of animation + # Animate between yellow and orange + var color1 = Color.YELLOW + var color2 = Color(1.0, 0.7, 0.0) # Orange-yellow + var t = (sin(selection_animation_time) + 1.0) / 2.0 # 0 to 1 + var animated_color = color1.lerp(color2, t) + + # Update border color + var stylebox = selection_rectangle.get_theme_stylebox("panel") as StyleBoxFlat + if stylebox: + stylebox.border_color = animated_color + +func _update_ui(): + if not local_player or not local_player.character_stats: + return + + var char_stats = local_player.character_stats + + # Debug: Print inventory contents + print("InventoryUI: Updating UI - inventory size: ", char_stats.inventory.size()) + for i in range(char_stats.inventory.size()): + var item = char_stats.inventory[i] + print(" Item ", i, ": ", item.item_name if item else "null") + + # Clear button mappings + inventory_buttons.clear() + inventory_items_list.clear() + inventory_rows_list.clear() + + # Update equipment slots + for slot_name in equipment_slots.keys(): + var button = equipment_slots[slot_name] + if not button: + continue + + # Clear existing children (sprite, highlight) + for child in button.get_children(): + child.queue_free() + + var equipped_item = char_stats.equipment[slot_name] + if equipped_item: + var sprite = Sprite2D.new() + # Equipment slots should ONLY use spritePath (items_n_shit.png), NOT equipmentPath + # equipmentPath is for character rendering only! + var texture_path = equipped_item.spritePath + + var texture = load(texture_path) + if texture: + sprite.texture = texture + sprite.hframes = equipped_item.spriteFrames.x if equipped_item.spriteFrames.x > 0 else 20 + sprite.vframes = equipped_item.spriteFrames.y if equipped_item.spriteFrames.y > 0 else 14 + sprite.frame = equipped_item.spriteFrame + sprite.centered = false # Like inspiration system + sprite.position = Vector2(4, 4) # Like inspiration system + sprite.scale = Vector2(2.0, 2.0) # 2x size as requested + button.add_child(sprite) + + # Update inventory grid - clear existing HBoxContainers + for child in inventory_grid.get_children(): + child.queue_free() + + # Add inventory items using HBoxContainers (like inspiration system) + var current_hbox: HBoxContainer = null + var items_per_row = 10 # Items per row (like inspiration system - they use 10 per HBox) + var items_in_current_row = 0 + + for item in char_stats.inventory: + # Create new HBoxContainer if needed + if current_hbox == null or items_in_current_row >= items_per_row: + current_hbox = HBoxContainer.new() + current_hbox.add_theme_constant_override("separation", 0) # No separation like inspiration + inventory_grid.add_child(current_hbox) + inventory_rows_list.append(current_hbox) + items_in_current_row = 0 + + # Create button with styleboxes (like inspiration system) + var button = Button.new() + button.custom_minimum_size = Vector2(24, 24) # Smaller like inspiration (24x24) + button.size = Vector2(24, 24) + if style_box_empty: + button.add_theme_stylebox_override("normal", style_box_empty) + if style_box_hover: + button.add_theme_stylebox_override("hover", style_box_hover) + if style_box_focused: + button.add_theme_stylebox_override("focus", style_box_focused) + if style_box_pressed: + button.add_theme_stylebox_override("pressed", style_box_pressed) + button.flat = false # Use styleboxes + button.connect("pressed", _on_inventory_item_pressed.bind(item)) + current_hbox.add_child(button) + + # Add item sprite (like inspiration system - positioned at 4,4 with centered=false) + if item.spritePath != "": + var sprite = Sprite2D.new() + var texture = load(item.spritePath) + if texture: + sprite.texture = texture + sprite.hframes = item.spriteFrames.x if item.spriteFrames.x > 0 else 20 + sprite.vframes = item.spriteFrames.y if item.spriteFrames.y > 0 else 14 + sprite.frame = item.spriteFrame + sprite.centered = false # Like inspiration system + sprite.position = Vector2(4, 4) # Like inspiration system + sprite.scale = Vector2(2.0, 2.0) # 2x size as requested + button.add_child(sprite) + + # Add quantity label if item can have multiple (like inspiration system) + if item.can_have_multiple_of and item.quantity > 1: + var quantity_label = Label.new() + quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + quantity_label.size = Vector2(24, 24) + quantity_label.custom_minimum_size = Vector2(0, 0) + quantity_label.position = Vector2(10, 2) + quantity_label.text = str(item.quantity) + if quantity_font: + quantity_label.add_theme_font_override("font", quantity_font) + quantity_label.add_theme_font_size_override("font_size", 8) + quantity_label.scale = Vector2(0.5, 0.5) + button.add_child(quantity_label) + + inventory_buttons[item] = button + inventory_items_list.append(item) + items_in_current_row += 1 + + # Clamp selection to valid range + if inventory_selection_row >= inventory_rows_list.size(): + inventory_selection_row = max(0, inventory_rows_list.size() - 1) + if inventory_selection_row >= 0 and inventory_selection_row < inventory_rows_list.size(): + var row = inventory_rows_list[inventory_selection_row] + if inventory_selection_col >= row.get_child_count(): + inventory_selection_col = max(0, row.get_child_count() - 1) + + # Update selection + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + +func _update_selection_from_navigation(): + # Update selected_item/selected_slot based on navigation position + if selected_type == "equipment" and equipment_selection_index >= 0 and equipment_selection_index < equipment_slots_list.size(): + var slot_name = equipment_slots_list[equipment_selection_index] + if _has_equipment_in_slot(slot_name): + selected_slot = slot_name + if local_player and local_player.character_stats: + selected_item = local_player.character_stats.equipment[slot_name] + else: + # Empty slot - switch to inventory + selected_type = "item" + selected_slot = "" + selected_item = null + if inventory_rows_list.size() > 0: + inventory_selection_row = 0 + inventory_selection_col = 0 + _update_selection_from_navigation() + elif selected_type == "item" and inventory_selection_row >= 0 and inventory_selection_row < inventory_rows_list.size(): + var row = inventory_rows_list[inventory_selection_row] + if inventory_selection_col >= 0 and inventory_selection_col < row.get_child_count(): + var item_index = inventory_selection_row * 10 + inventory_selection_col + if item_index >= 0 and item_index < inventory_items_list.size(): + selected_item = inventory_items_list[item_index] + selected_slot = "" + +func _format_item_info(item: Item) -> String: + # Format item description, stats modifiers, and controls + var text = "" + + # Description + if item.description != "": + text += item.description + else: + text += item.item_name + + text += "\n\n" + + # Stats modifiers + var stat_lines = [] + if item.modifiers.has("str"): + stat_lines.append("STR: +%d" % item.modifiers["str"]) + if item.modifiers.has("dex"): + stat_lines.append("DEX: +%d" % item.modifiers["dex"]) + if item.modifiers.has("end"): + stat_lines.append("END: +%d" % item.modifiers["end"]) + if item.modifiers.has("int"): + stat_lines.append("INT: +%d" % item.modifiers["int"]) + if item.modifiers.has("wis"): + stat_lines.append("WIS: +%d" % item.modifiers["wis"]) + if item.modifiers.has("lck"): + stat_lines.append("LCK: +%d" % item.modifiers["lck"]) + if item.modifiers.has("dmg"): + stat_lines.append("DMG: +%d" % item.modifiers["dmg"]) + if item.modifiers.has("def"): + stat_lines.append("DEF: +%d" % item.modifiers["def"]) + if item.modifiers.has("hp"): + stat_lines.append("HP: +%d" % item.modifiers["hp"]) + if item.modifiers.has("mp"): + stat_lines.append("MP: +%d" % item.modifiers["mp"]) + if item.modifiers.has("maxhp"): + stat_lines.append("MAXHP: +%d" % item.modifiers["maxhp"]) + if item.modifiers.has("maxmp"): + stat_lines.append("MAXMP: +%d" % item.modifiers["maxmp"]) + + if stat_lines.size() > 0: + text += "\n".join(stat_lines) + text += "\n\n" + + # Controls + if item.item_type == Item.ItemType.Equippable: + if selected_type == "equipment": + text += "Press F to unequip" + else: + text += "Press F to equip" + elif item.item_type == Item.ItemType.Restoration: + text += "Press F to consume" + + if selected_type == "item": + text += "\nPress E to drop" + + return text + +func _update_info_panel(): + # Update info panel based on selected item + if not info_label: + return + + if selected_item: + info_label.text = _format_item_info(selected_item) + else: + info_label.text = "" + +func _navigate_inventory(direction: String): + # Handle navigation within inventory + var items_per_row = 10 + + match direction: + "left": + if inventory_selection_col > 0: + inventory_selection_col -= 1 + else: + # Wrap to end of previous row + if inventory_selection_row > 0: + inventory_selection_row -= 1 + var row = inventory_rows_list[inventory_selection_row] + inventory_selection_col = row.get_child_count() - 1 + "right": + if inventory_selection_row < inventory_rows_list.size(): + var row = inventory_rows_list[inventory_selection_row] + if inventory_selection_col < row.get_child_count() - 1: + inventory_selection_col += 1 + else: + # Wrap to start of next row + if inventory_selection_row < inventory_rows_list.size() - 1: + inventory_selection_row += 1 + inventory_selection_col = 0 + "up": + if inventory_selection_row > 0: + inventory_selection_row -= 1 + # Clamp column to valid range + var row = inventory_rows_list[inventory_selection_row] + if inventory_selection_col >= row.get_child_count(): + inventory_selection_col = row.get_child_count() - 1 + else: + # Move to equipment slots (only if there are filled slots) + var next_equip_index = _find_next_filled_equipment_slot(-1, 1) # Start from end, go forward + if next_equip_index >= 0: + selected_type = "equipment" + equipment_selection_index = next_equip_index + selected_slot = equipment_slots_list[next_equip_index] + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + return + "down": + if inventory_selection_row < inventory_rows_list.size() - 1: + inventory_selection_row += 1 + # Clamp column to valid range + var row = inventory_rows_list[inventory_selection_row] + if inventory_selection_col >= row.get_child_count(): + inventory_selection_col = row.get_child_count() - 1 + # Can't go down from inventory (already at bottom) + + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + +func _navigate_equipment(direction: String): + # Handle navigation within equipment slots (only filled slots) + # Equipment layout: 3 columns, 2 rows + # Row 1: mainhand(0), offhand(1), headgear(2) + # Row 2: armour(3), boots(4), accessory(5) + + match direction: + "left": + var next_index = _find_next_filled_equipment_slot(equipment_selection_index, -1) + if next_index >= 0: + equipment_selection_index = next_index + "right": + var next_index = _find_next_filled_equipment_slot(equipment_selection_index, 1) + if next_index >= 0: + equipment_selection_index = next_index + "up": + # Find next filled slot in row above (same column) + var current_row = equipment_selection_index / 3 + var current_col = equipment_selection_index % 3 + if current_row > 0: + var target_index = (current_row - 1) * 3 + current_col + var target_slot = equipment_slots_list[target_index] + if _has_equipment_in_slot(target_slot): + equipment_selection_index = target_index + else: + # Skip to next filled slot in that row + var next_index = _find_next_filled_equipment_slot(target_index - 1, 1) + if next_index >= 0 and next_index < 3: # Make sure it's in row 0 + equipment_selection_index = next_index + # Can't go up from equipment (already at top) + "down": + # Find next filled slot in row below (same column), or move to inventory + var current_row = equipment_selection_index / 3 + var current_col = equipment_selection_index % 3 + if current_row < 1: + var target_index = (current_row + 1) * 3 + current_col + var target_slot = equipment_slots_list[target_index] + if _has_equipment_in_slot(target_slot): + equipment_selection_index = target_index + else: + # No filled slot below, move to inventory + selected_type = "item" + inventory_selection_row = 0 + inventory_selection_col = current_col + # Clamp to valid range + if inventory_rows_list.size() > 0: + var inv_row = inventory_rows_list[0] + if inventory_selection_col >= inv_row.get_child_count(): + inventory_selection_col = inv_row.get_child_count() - 1 + else: + inventory_selection_col = 0 + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + return + else: + # Already at bottom row, move to inventory + selected_type = "item" + inventory_selection_row = 0 + inventory_selection_col = current_col + # Clamp to valid range + if inventory_rows_list.size() > 0: + var inv_row = inventory_rows_list[0] + if inventory_selection_col >= inv_row.get_child_count(): + inventory_selection_col = inv_row.get_child_count() - 1 + else: + inventory_selection_col = 0 + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + return + + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + +func _on_inventory_item_pressed(item: Item): + if not local_player or not local_player.character_stats: + return + + selected_item = item + selected_slot = "" + selected_type = "item" + + # Update navigation position + var item_index = inventory_items_list.find(item) + if item_index >= 0: + var items_per_row = 10 + inventory_selection_row = item_index / items_per_row + inventory_selection_col = item_index % items_per_row + + _update_selection_highlight() + _update_selection_rectangle() + +func _on_character_changed(_char: CharacterStats): + _update_ui() + _update_stats() + +func _input(event): + # Toggle with Tab key + if event is InputEventKey and event.keycode == KEY_TAB and event.pressed and not event.echo: + if is_open: + _close_inventory() + else: + _open_inventory() + get_viewport().set_input_as_handled() + return + + if not is_open: + return + + # Arrow key navigation + if event is InputEventKey and event.pressed and not event.echo: + var direction = "" + if event.keycode == KEY_LEFT: + direction = "left" + elif event.keycode == KEY_RIGHT: + direction = "right" + elif event.keycode == KEY_UP: + direction = "up" + elif event.keycode == KEY_DOWN: + direction = "down" + + if direction != "": + if selected_type == "equipment": + _navigate_equipment(direction) + else: + _navigate_inventory(direction) + get_viewport().set_input_as_handled() + return + + # F key: Unequip/equip items + if event is InputEventKey and event.keycode == KEY_F and event.pressed and not event.echo: + _handle_f_key() + get_viewport().set_input_as_handled() + return + + # E key: Drop selected item + if event is InputEventKey and event.keycode == KEY_E and event.pressed and not event.echo: + _handle_e_key() + get_viewport().set_input_as_handled() + return + +func _handle_f_key(): + if not local_player or not local_player.character_stats: + return + + var char_stats = local_player.character_stats + + if selected_type == "equipment" and selected_slot != "": + var equipped_item = char_stats.equipment[selected_slot] + if equipped_item: + char_stats.unequip_item(equipped_item) + # After unequipping, if all equipment is empty, go to inventory + var has_any_equipment = false + for slot in equipment_slots_list: + if _has_equipment_in_slot(slot): + has_any_equipment = true + break + + if not has_any_equipment: + # No equipment left, go to inventory + selected_type = "item" + selected_slot = "" + selected_item = null + if inventory_rows_list.size() > 0: + inventory_selection_row = 0 + inventory_selection_col = 0 + else: + selected_type = "" + else: + # Find next filled equipment slot + var next_index = _find_next_filled_equipment_slot(equipment_selection_index, 1) + if next_index >= 0: + equipment_selection_index = next_index + selected_slot = equipment_slots_list[next_index] + selected_item = char_stats.equipment[selected_slot] + else: + # No more equipment, go to inventory + selected_type = "item" + selected_slot = "" + selected_item = null + if inventory_rows_list.size() > 0: + inventory_selection_row = 0 + inventory_selection_col = 0 + _update_selection_from_navigation() + _update_selection_rectangle() + return + + if selected_type == "item" and selected_item: + if selected_item.item_type == Item.ItemType.Equippable and selected_item.equipment_type != Item.EquipmentType.NONE: + char_stats.equip_item(selected_item) + # After equipping, selection might move to equipment slot + _update_ui() + _update_selection_rectangle() + elif selected_item.item_type == Item.ItemType.Restoration: + _use_consumable_item(selected_item) + _update_info_panel() + +func _use_consumable_item(item: Item): + if not local_player or not local_player.character_stats: + return + + var char_stats = local_player.character_stats + + if item.modifiers.has("hp"): + var hp_heal = item.modifiers["hp"] + if local_player.has_method("heal"): + local_player.heal(hp_heal) + if item.modifiers.has("mp"): + var mana_amount = item.modifiers["mp"] + char_stats.restore_mana(mana_amount) + + var index = char_stats.inventory.find(item) + if index >= 0: + char_stats.inventory.remove_at(index) + char_stats.character_changed.emit(char_stats) + + print(local_player.name, " used item: ", item.item_name) + +func _handle_e_key(): + if not local_player or not local_player.character_stats: + return + + if selected_type != "item" or not selected_item: + return + + var char_stats = local_player.character_stats + + if not selected_item in char_stats.inventory: + return + + var index = char_stats.inventory.find(selected_item) + if index >= 0: + char_stats.inventory.remove_at(index) + + var drop_position = local_player.global_position + Vector2(randf_range(-20, 20), randf_range(-20, 20)) + var game_world = get_tree().get_first_node_in_group("game_world") + var entities_node = null + if game_world: + entities_node = game_world.get_node_or_null("Entities") + + if entities_node: + var loot = ItemLootHelper.spawn_item_loot(selected_item, drop_position, entities_node, game_world) + if loot: + if local_player.has_method("get_multiplayer_authority"): + var player_peer_id = local_player.get_multiplayer_authority() + loot.set_meta("dropped_by_peer_id", player_peer_id) + loot.set_meta("drop_time", Time.get_ticks_msec()) + + selected_item = null + selected_slot = "" + selected_type = "" + + char_stats.character_changed.emit(char_stats) + + print(local_player.name, " dropped item") + +func _open_inventory(): + if is_open: + return + + is_open = true + if container: + container.visible = true + + _lock_player_controls(true) + _update_ui() + + # Initialize selection - prefer inventory, but if empty, check equipment + if inventory_rows_list.size() > 0: + selected_type = "item" + inventory_selection_row = 0 + inventory_selection_col = 0 + _update_selection_from_navigation() + _update_selection_rectangle() + else: + # No inventory items, try equipment + var first_filled_slot = _find_next_filled_equipment_slot(-1, 1) + if first_filled_slot >= 0: + selected_type = "equipment" + equipment_selection_index = first_filled_slot + selected_slot = equipment_slots_list[first_filled_slot] + _update_selection_from_navigation() + _update_selection_rectangle() + _update_info_panel() + else: + # Nothing to select + selected_type = "" + selection_rectangle.visible = false + if info_label: + info_label.text = "" + + if not local_player: + _find_local_player() + +func _close_inventory(): + if not is_open: + return + + is_open = false + if container: + container.visible = false + + if selection_rectangle: + selection_rectangle.visible = false + + selected_item = null + selected_slot = "" + selected_type = "" + + _lock_player_controls(false) + +func _lock_player_controls(lock: bool): + var game_world = get_tree().get_first_node_in_group("game_world") + if game_world: + var player_manager = game_world.get_node_or_null("PlayerManager") + if player_manager: + var local_players = player_manager.get_local_players() + for player in local_players: + player.controls_disabled = lock diff --git a/src/scripts/inventory_ui_refactored.gd.uid b/src/scripts/inventory_ui_refactored.gd.uid new file mode 100644 index 0000000..6cfb0aa --- /dev/null +++ b/src/scripts/inventory_ui_refactored.gd.uid @@ -0,0 +1 @@ +uid://ch6orv505wr0b diff --git a/src/scripts/loot.gd b/src/scripts/loot.gd index b1ca6f1..ad661b5 100644 --- a/src/scripts/loot.gd +++ b/src/scripts/loot.gd @@ -504,24 +504,14 @@ func _process_pickup_on_server(player: Node): player.character_stats.add_item(item) print(name, " picked up item: ", item.item_name, " (added to inventory)") elif item.item_type == Item.ItemType.Restoration: - # Consumable item - use immediately + # Consumable item - add to inventory (use with F key in inventory) if player.character_stats: - # Apply modifiers (hp, mp, etc.) - if item.modifiers.has("hp"): - var hp_heal = item.modifiers["hp"] - if player.has_method("heal"): - player.heal(hp_heal) - if item.modifiers.has("mp"): - var mana_amount = item.modifiers["mp"] - player.character_stats.restore_mana(mana_amount) - - # TODO: Handle other modifiers (dodge_chance, res_all, etc.) - these would need duration tracking - - print(name, " used item: ", item.item_name) + player.character_stats.add_item(item) + print(name, " picked up item: ", item.item_name, " (added to inventory)") - # Show floating text with item name + # Show floating text with item name (uppercase) var items_texture = load(item.spritePath) - var display_text = item.item_name + var display_text = item.item_name.to_upper() # Always uppercase var text_color = Color.WHITE # Color code based on item type diff --git a/src/scripts/player.gd b/src/scripts/player.gd index 617d62d..6418a71 100644 --- a/src/scripts/player.gd +++ b/src/scripts/player.gd @@ -599,6 +599,18 @@ func _apply_appearance_to_sprites(): func _on_character_changed(_char: CharacterStats): # Update appearance when character stats change (e.g., equipment) _apply_appearance_to_sprites() + + # Sync equipment changes to other clients + if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): + # Sync equipment to all clients + var equipment_data = {} + for slot_name in character_stats.equipment.keys(): + var item = character_stats.equipment[slot_name] + if item: + equipment_data[slot_name] = item.save() # Serialize item data + else: + equipment_data[slot_name] = null + _sync_equipment.rpc(equipment_data) func _get_player_color() -> Color: # Legacy function - now returns white (no color tint) @@ -782,18 +794,20 @@ func _physics_process(delta): if is_dead: return - # Skip input if controls are disabled (e.g., when player reached exit) - if controls_disabled: - velocity = velocity.lerp(Vector2.ZERO, delta * 8.0) # Slow down movement - return - - # Handle knockback timer + # Handle knockback timer (always handle knockback, even when controls are disabled) if is_knocked_back: knockback_time += delta if knockback_time >= knockback_duration: is_knocked_back = false knockback_time = 0.0 + # Skip input if controls are disabled (e.g., when inventory is open) + # But still allow knockback to continue (handled above) + if controls_disabled: + if not is_knocked_back: + velocity = velocity.lerp(Vector2.ZERO, delta * 8.0) # Slow down movement + return + # Check if being held by someone var being_held_by_someone = false for other_player in get_tree().get_nodes_in_group("player"): @@ -2556,6 +2570,29 @@ func _sync_stats_update(kills_count: int, coins_count: int): character_stats.coin = coins_count print(name, " stats synced from server: kills=", kills_count, " coins=", coins_count) +@rpc("any_peer", "reliable") +func _sync_equipment(equipment_data: Dictionary): + # Client receives equipment update from server + # Update equipment to match other players + # Only process if we're not the authority (remote player) + if is_multiplayer_authority(): + return # Authority ignores this (it's the sender) + + if not character_stats: + return + + # Update equipment from data + for slot_name in equipment_data.keys(): + var item_data = equipment_data[slot_name] + if item_data != null: + character_stats.equipment[slot_name] = Item.new(item_data) + else: + character_stats.equipment[slot_name] = null + + # Update appearance + _apply_appearance_to_sprites() + print(name, " equipment synced from server") + func heal(amount: float): if is_dead: return