extends CanvasLayer # Minimalistic Inventory UI with Stats Panel # Toggle with Tab key, shows stats, equipment slots and inventory items 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" # 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 # 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 # Store button/item mappings for selection highlighting var inventory_buttons: Dictionary = {} # item -> button var equipment_buttons: Dictionary = {} # slot_name -> button # Equipment slot buttons var equipment_slots: Dictionary = { "mainhand": null, "offhand": null, "headgear": null, "armour": null, "boots": null, "accessory": null } func _ready(): # Set layer to be above game but below chat layer = 150 # Create UI structure _setup_ui() # Find local player call_deferred("_find_local_player") 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 _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 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 = ["Weapon", "Shield", "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 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 var button = Button.new() button.name = slot_name + "_btn" button.custom_minimum_size = Vector2(60, 60) button.flat = true 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 _on_equipment_slot_pressed(slot_name: String): if not local_player or not local_player.character_stats: 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_selection_highlight() 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() # 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) # 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) func _update_ui(): if not local_player or not local_player.character_stats: return var char_stats = local_player.character_stats # Clear button mappings inventory_buttons.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() 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 != "") 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) button.add_child(sprite) # Update inventory grid for child in inventory_grid.get_children(): child.queue_free() 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)) 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.scale = Vector2(1.2, 1.2) button.add_child(sprite) inventory_grid.add_child(button) inventory_buttons[item] = button # Update selection highlight _update_selection_highlight() # Update stats _update_stats() 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_selection_highlight() 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 # 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) selected_item = null selected_slot = "" selected_type = "" 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) selected_item = null selected_slot = "" selected_type = "" elif selected_item.item_type == Item.ItemType.Restoration: _use_consumable_item(selected_item) selected_item = null selected_slot = "" selected_type = "" 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() if not local_player: _find_local_player() func _close_inventory(): if not is_open: return is_open = false if container: container.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