567 lines
18 KiB
GDScript
567 lines
18 KiB
GDScript
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
|