1946 lines
77 KiB
GDScript
1946 lines
77 KiB
GDScript
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
|
|
var is_updating_ui: bool = false # Prevent recursive UI updates
|
|
|
|
# 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"
|
|
var is_first_open: bool = true # Track if this is the first time opening
|
|
|
|
# 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/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel
|
|
@onready var equipment_panel: GridContainer = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/EquipmentPanel
|
|
@onready var scroll_container: ScrollContainer = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll
|
|
@onready var inventory_grid: VBoxContainer = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll/InventoryVBox
|
|
@onready var selection_rectangle: Panel = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/SelectionRectangle
|
|
@onready var info_panel: Control = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/InfoPanel
|
|
@onready var info_label: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/InfoPanel/InfoLabel
|
|
@onready var stats_label: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsLabel
|
|
@onready var label_base_stats: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStats
|
|
@onready var label_base_stats_value: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStatsValue
|
|
@onready var label_derived_stats: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStats
|
|
@onready var label_derived_stats_value: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStatsValue
|
|
@onready var sfx_potion: AudioStreamPlayer2D = $SfxPotion
|
|
@onready var sfx_food: AudioStreamPlayer2D = $SfxFood
|
|
@onready var sfx_armour: AudioStreamPlayer2D = $SfxArmour
|
|
@onready var tab_row: HBoxContainer = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/TabRow
|
|
@onready var inventory_tab_btn: Button = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/TabRow/InventoryTab
|
|
@onready var spell_book_tab_btn: Button = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/TabRow/SpellBookTab
|
|
@onready var spell_book_panel: Control = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/SpellBookPanel
|
|
@onready var inventory_hbox: HBoxContainer = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox
|
|
|
|
# Bar layout constants (align X/Y + bar across rows)
|
|
const _BAR_WIDTH: int = 100
|
|
const _BAR_VALUE_MIN_WIDTH: int = 44 # "999/999"
|
|
const _BAR_LABEL_MIN_WIDTH: int = 52 # "Weight:", "Exp:", "HP:", "MP:", "Coin:"
|
|
|
|
# Weight UI elements (created programmatically)
|
|
var weight_container: HBoxContainer = null
|
|
var weight_label: Label = null
|
|
var weight_value_label: Label = null
|
|
var weight_progress_bar: ProgressBar = null
|
|
|
|
# Exp UI elements (like weight)
|
|
var exp_container: HBoxContainer = null
|
|
var exp_label: Label = null
|
|
var exp_value_label: Label = null
|
|
var exp_progress_bar: ProgressBar = null
|
|
|
|
# HP / MP bar elements
|
|
var hp_container: HBoxContainer = null
|
|
var hp_label: Label = null
|
|
var hp_value_label: Label = null
|
|
var hp_progress_bar: ProgressBar = null
|
|
var mp_container: HBoxContainer = null
|
|
var mp_label: Label = null
|
|
var mp_value_label: Label = null
|
|
var mp_progress_bar: ProgressBar = null
|
|
|
|
# Coin UI elements ("Coin:" + 6-frame sprite + "X N")
|
|
var coin_container: HBoxContainer = null
|
|
var coin_label: Label = null
|
|
var coin_sprite: Sprite2D = null
|
|
var coin_value_label: Label = null
|
|
var coin_anim_time: float = 0.0
|
|
|
|
# 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)
|
|
|
|
# Level-up stat allocation (when pending_level_up)
|
|
var level_up_label: Label = null
|
|
var level_up_stat_buttons: Array = [] # Buttons for STR, DEX, INT, END, WIS, LCK, PER
|
|
var level_up_stat_container: HBoxContainer = null
|
|
var selected_level_up_stat_index: int = -1
|
|
const STAT_DESCRIPTIONS: Dictionary = {
|
|
"str": "STR (Strength): Increases melee and bow damage. Raises carry capacity so you can hold more items before becoming encumbered.",
|
|
"dex": "DEX (Dexterity): Improves dodge chance and hit chance. Makes you move and attack faster.",
|
|
"int": "INT (Intelligence): Boosts spell damage (flames, frost, heal). Increases max mana and vision range.",
|
|
"end": "END (Endurance): Increases max HP. Each point raises your maximum health.",
|
|
"wis": "WIS (Wisdom): Improves mana regeneration and resistances to certain effects.",
|
|
"lck": "LCK (Luck): Increases critical hit chance. Critical hits deal bonus damage and partially ignore defense.",
|
|
"per": "PER (Perception): Improves trap detection and perception. Helps you spot hazards and secrets."
|
|
}
|
|
|
|
# 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
|
|
|
|
# Tab: "inventory" or "spell_book"
|
|
var _current_tab: String = "inventory"
|
|
|
|
func _ready():
|
|
# Set layer to be above game but below chat
|
|
layer = 150
|
|
|
|
# Load styleboxes for inventory slots (like inspiration system)
|
|
_setup_styleboxes()
|
|
|
|
# Create equipment slot buttons (dynamically)
|
|
_create_equipment_slots()
|
|
|
|
# Create HP/MP bars, then weight, exp, coin (order in stats panel)
|
|
_create_hp_ui()
|
|
_create_mp_ui()
|
|
_create_weight_ui()
|
|
_create_exp_ui()
|
|
_create_coin_ui()
|
|
|
|
# Level-up stat allocation UI (label + stat buttons)
|
|
_setup_level_up_ui()
|
|
|
|
# Setup selection rectangle (already in scene, just configure it)
|
|
_setup_selection_rectangle()
|
|
|
|
# Spell Book / Inventory tabs
|
|
if inventory_tab_btn:
|
|
inventory_tab_btn.pressed.connect(_on_inventory_tab_pressed)
|
|
if spell_book_tab_btn:
|
|
spell_book_tab_btn.pressed.connect(_on_spell_book_tab_pressed)
|
|
|
|
# Find local player
|
|
call_deferred("_find_local_player")
|
|
|
|
func _setup_styleboxes():
|
|
# Create styleboxes exactly like inspiration inventory system
|
|
var selected_tex = preload("res://assets/gfx/ui/inventory_slot_kenny_white.png")
|
|
|
|
style_box_empty = StyleBoxEmpty.new()
|
|
|
|
if selected_tex:
|
|
# Scale factor for the slot background (1.5x to match larger item sprites)
|
|
# Since StyleBoxTexture doesn't support texture_scale, we use expand_margin
|
|
# to make the texture fill more space
|
|
# For 1.5x scale on a 36px button (which was 24px originally), we need to expand
|
|
# The button is now 36px, so to make a 24px texture appear 1.5x (36px), we use negative margins
|
|
# Actually, let's use smaller positive margins to avoid clipping
|
|
var margin_scale = 3.0 # Smaller margin to avoid clipping in upper left corner
|
|
|
|
style_box_hover = StyleBoxTexture.new()
|
|
style_box_hover.texture = selected_tex
|
|
style_box_hover.expand_margin_left = margin_scale
|
|
style_box_hover.expand_margin_top = margin_scale
|
|
style_box_hover.expand_margin_right = margin_scale
|
|
style_box_hover.expand_margin_bottom = margin_scale
|
|
|
|
style_box_focused = StyleBoxTexture.new()
|
|
style_box_focused.texture = selected_tex
|
|
style_box_focused.expand_margin_left = margin_scale
|
|
style_box_focused.expand_margin_top = margin_scale
|
|
style_box_focused.expand_margin_right = margin_scale
|
|
style_box_focused.expand_margin_bottom = margin_scale
|
|
|
|
style_box_pressed = StyleBoxTexture.new()
|
|
style_box_pressed.texture = selected_tex
|
|
style_box_pressed.expand_margin_left = margin_scale
|
|
style_box_pressed.expand_margin_top = margin_scale
|
|
style_box_pressed.expand_margin_right = margin_scale
|
|
style_box_pressed.expand_margin_bottom = margin_scale
|
|
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_selection_rectangle():
|
|
# Selection rectangle is already in scene, just ensure it's configured correctly
|
|
if selection_rectangle:
|
|
selection_rectangle.visible = false
|
|
# Ensure it's on top and visible
|
|
selection_rectangle.z_index = 100
|
|
selection_rectangle.z_as_relative = false
|
|
selection_rectangle.mouse_filter = Control.MOUSE_FILTER_IGNORE # Don't block mouse input
|
|
# Ensure it's on top
|
|
selection_rectangle.z_index = 100
|
|
selection_rectangle.z_as_relative = 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 (for inventory/equipment changes)
|
|
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)
|
|
# Connect to mana_changed signal (for mana updates only - don't rebuild UI)
|
|
if local_player.character_stats.mana_changed.is_connected(_on_mana_changed):
|
|
local_player.character_stats.mana_changed.disconnect(_on_mana_changed)
|
|
local_player.character_stats.mana_changed.connect(_on_mana_changed)
|
|
# Connect to health_changed signal (for HP updates only - don't rebuild UI)
|
|
if local_player.character_stats.health_changed.is_connected(_on_health_changed):
|
|
local_player.character_stats.health_changed.disconnect(_on_health_changed)
|
|
local_player.character_stats.health_changed.connect(_on_health_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 race/class in stats label
|
|
if stats_label:
|
|
var race_text = char_stats.race
|
|
stats_label.text = "Stats - " + race_text
|
|
|
|
# Level-up UI: "Level X - LEVEL UP" in green, stat allocation buttons
|
|
var pending = char_stats.pending_level_up and char_stats.pending_stat_points > 0
|
|
if level_up_label:
|
|
level_up_label.visible = pending
|
|
if pending:
|
|
level_up_label.text = "Level " + str(char_stats.level) + " - LEVEL UP"
|
|
level_up_label.add_theme_color_override("font_color", Color(0.2, 1.0, 0.4))
|
|
if level_up_stat_container:
|
|
level_up_stat_container.visible = pending
|
|
|
|
# Base stats: Level, STR, DEX, END, INT, WIS, LCK, PER (HP/MP are bars below)
|
|
if label_base_stats:
|
|
label_base_stats.text = "Level\n\nSTR\nDEX\nEND\nINT\nWIS\nLCK\nPER"
|
|
if label_base_stats_value:
|
|
label_base_stats_value.text = str(char_stats.level) + "\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) + "\n" + \
|
|
str(char_stats.baseStats.get("per", 10))
|
|
|
|
# Derived stats: DMG, DEF, MovSpd, AtkSpd, Sight, SpellAmp, Crit%, Dodge% (XP/Coin moved to exp meter & coin UI)
|
|
if label_derived_stats:
|
|
label_derived_stats.text = "DMG\nDEF\nMovSpd\nAtkSpd\nSight\nSpellAmp\nCrit%\nDodge%"
|
|
if label_derived_stats_value:
|
|
label_derived_stats_value.text = "%.1f\n%.1f\n%.2f\n%.2f\n%.1f\n%.1f\n%.1f%%\n%.1f%%" % [
|
|
char_stats.damage,
|
|
char_stats.defense,
|
|
char_stats.move_speed,
|
|
char_stats.attack_speed,
|
|
char_stats.sight,
|
|
char_stats.spell_amp,
|
|
char_stats.crit_chance,
|
|
char_stats.dodge_chance * 100.0
|
|
]
|
|
|
|
# HP bar
|
|
if hp_progress_bar and hp_value_label:
|
|
hp_progress_bar.max_value = max(1.0, char_stats.maxhp)
|
|
hp_progress_bar.value = char_stats.hp
|
|
hp_value_label.text = str(int(char_stats.hp)) + "/" + str(int(char_stats.maxhp))
|
|
|
|
# MP bar
|
|
if mp_progress_bar and mp_value_label:
|
|
mp_progress_bar.max_value = max(1.0, char_stats.maxmp)
|
|
mp_progress_bar.value = char_stats.mp
|
|
mp_value_label.text = str(int(char_stats.mp)) + "/" + str(int(char_stats.maxmp))
|
|
|
|
# Exp meter (like weight)
|
|
if exp_progress_bar and exp_value_label:
|
|
var xp = char_stats.xp
|
|
var xp_next = char_stats.xp_to_next_level
|
|
exp_progress_bar.max_value = max(1.0, xp_next)
|
|
exp_progress_bar.value = xp
|
|
exp_value_label.text = str(int(xp)) + "/" + str(int(xp_next))
|
|
var fill_exp = StyleBoxFlat.new()
|
|
fill_exp.bg_color = Color(0.55, 0.35, 0.95)
|
|
exp_progress_bar.add_theme_stylebox_override("fill", fill_exp)
|
|
|
|
# Coin: "Coin:" + 6-frame sprite + "X <count>"
|
|
if coin_value_label:
|
|
coin_value_label.text = "X " + str(char_stats.coin)
|
|
|
|
# Weight progress bar
|
|
if weight_progress_bar and weight_value_label:
|
|
var current_weight = char_stats.get_total_weight()
|
|
var max_weight = char_stats.get_carrying_capacity()
|
|
weight_progress_bar.max_value = max_weight
|
|
weight_progress_bar.value = current_weight
|
|
weight_value_label.text = str(int(current_weight)) + "/" + str(int(max_weight))
|
|
var weight_ratio = current_weight / max_weight
|
|
var fill_style = StyleBoxFlat.new()
|
|
if weight_ratio < 0.7:
|
|
fill_style.bg_color = Color(0.6, 0.8, 0.3)
|
|
elif weight_ratio < 0.9:
|
|
fill_style.bg_color = Color(0.9, 0.8, 0.2)
|
|
else:
|
|
fill_style.bg_color = Color(0.9, 0.3, 0.2)
|
|
weight_progress_bar.add_theme_stylebox_override("fill", fill_style)
|
|
|
|
func _setup_level_up_ui() -> void:
|
|
if not stats_panel:
|
|
return
|
|
level_up_label = Label.new()
|
|
level_up_label.name = "LevelUpLabel"
|
|
level_up_label.add_theme_font_size_override("font_size", 14)
|
|
level_up_label.add_theme_color_override("font_color", Color(0.2, 1.0, 0.4))
|
|
if ResourceLoader.exists("res://assets/fonts/standard_font.png"):
|
|
var fr = load("res://assets/fonts/standard_font.png")
|
|
if fr:
|
|
level_up_label.add_theme_font_override("font", fr)
|
|
level_up_label.visible = false
|
|
stats_panel.add_child(level_up_label)
|
|
stats_panel.move_child(level_up_label, 1)
|
|
level_up_stat_container = HBoxContainer.new()
|
|
level_up_stat_container.name = "LevelUpStatButtons"
|
|
level_up_stat_container.add_theme_constant_override("separation", 4)
|
|
level_up_stat_container.visible = false
|
|
for stat_name in CharacterStats.LEVEL_UP_STAT_NAMES:
|
|
var btn = Button.new()
|
|
btn.name = "LevelUp_" + stat_name
|
|
btn.text = stat_name.to_upper()
|
|
btn.custom_minimum_size = Vector2(32, 24)
|
|
btn.flat = true
|
|
btn.add_theme_color_override("font_color", Color(0.85, 0.85, 0.85))
|
|
btn.add_theme_color_override("font_hover_color", Color(0.4, 1.0, 0.5))
|
|
btn.add_theme_color_override("font_focus_color", Color(0.4, 1.0, 0.5))
|
|
if ResourceLoader.exists("res://assets/fonts/standard_font.png"):
|
|
var fr = load("res://assets/fonts/standard_font.png")
|
|
if fr:
|
|
btn.add_theme_font_override("font", fr)
|
|
btn.add_theme_font_size_override("font_size", 9)
|
|
btn.focus_mode = Control.FOCUS_ALL
|
|
btn.pressed.connect(_on_level_up_stat_pressed.bind(stat_name))
|
|
btn.mouse_entered.connect(_on_level_up_stat_hover_entered.bind(stat_name))
|
|
btn.mouse_exited.connect(_on_level_up_stat_hover_exited)
|
|
btn.gui_input.connect(_on_level_up_stat_gui_input.bind(stat_name, btn))
|
|
btn.focus_entered.connect(_on_level_up_stat_focus_entered.bind(stat_name))
|
|
level_up_stat_container.add_child(btn)
|
|
level_up_stat_buttons.append(btn)
|
|
stats_panel.add_child(level_up_stat_container)
|
|
stats_panel.move_child(level_up_stat_container, 2)
|
|
|
|
func _on_level_up_stat_pressed(stat_name: String) -> void:
|
|
if not local_player or not local_player.character_stats:
|
|
return
|
|
if not _can_use_inventory():
|
|
return
|
|
if local_player.character_stats.allocate_stat_point(stat_name):
|
|
if sfx_armour:
|
|
sfx_armour.play()
|
|
_update_stats()
|
|
if not local_player.character_stats.pending_level_up:
|
|
selected_type = "equipment"
|
|
selected_level_up_stat_index = -1
|
|
var next_index = _find_next_filled_equipment_slot(-1, 1)
|
|
if next_index >= 0:
|
|
equipment_selection_index = next_index
|
|
selected_slot = equipment_slots_list[next_index]
|
|
selected_item = local_player.character_stats.equipment[selected_slot]
|
|
else:
|
|
selected_slot = ""
|
|
selected_item = null
|
|
if inventory_items_list.size() > 0:
|
|
selected_type = "item"
|
|
inventory_selection_row = 0
|
|
inventory_selection_col = 0
|
|
_update_ui()
|
|
|
|
func _on_level_up_stat_hover_entered(stat_name: String) -> void:
|
|
if info_label and stat_name in STAT_DESCRIPTIONS:
|
|
info_label.text = STAT_DESCRIPTIONS[stat_name]
|
|
|
|
func _on_level_up_stat_hover_exited() -> void:
|
|
if not info_label:
|
|
return
|
|
var fc = get_viewport().gui_get_focus_owner()
|
|
if level_up_stat_container and fc and is_instance_valid(level_up_stat_container) and fc.get_parent() == level_up_stat_container:
|
|
var idx = level_up_stat_buttons.find(fc)
|
|
if idx >= 0 and idx < CharacterStats.LEVEL_UP_STAT_NAMES.size():
|
|
var sn = CharacterStats.LEVEL_UP_STAT_NAMES[idx]
|
|
if sn in STAT_DESCRIPTIONS:
|
|
info_label.text = STAT_DESCRIPTIONS[sn]
|
|
return
|
|
_update_info_panel()
|
|
|
|
func _on_level_up_stat_focus_entered(stat_name: String) -> void:
|
|
if info_label and stat_name in STAT_DESCRIPTIONS:
|
|
info_label.text = STAT_DESCRIPTIONS[stat_name]
|
|
|
|
func _on_level_up_stat_gui_input(event: InputEvent, stat_name: String, _btn: Button) -> void:
|
|
if event is InputEventKey and event.pressed and not event.echo:
|
|
if event.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER or event.keycode == KEY_SPACE:
|
|
_on_level_up_stat_pressed(stat_name)
|
|
|
|
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
|
|
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 size increased to accommodate 1.5x scaled texture (24 * 1.5 = 36)
|
|
button.custom_minimum_size = Vector2(36, 36) # Increased to fit 1.5x scaled texture
|
|
button.size = Vector2(36, 36)
|
|
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.focus_mode = Control.FOCUS_ALL # Allow button to receive focus
|
|
button.size_flags_horizontal = 0
|
|
button.size_flags_vertical = 0
|
|
button.connect("pressed", _on_equipment_slot_pressed.bind(slot_name))
|
|
button.connect("gui_input", _on_equipment_slot_gui_input.bind(slot_name))
|
|
# Connect focus_entered like inspiration system (for keyboard navigation)
|
|
if local_player and local_player.character_stats:
|
|
var equipped_item = local_player.character_stats.equipment[slot_name]
|
|
if equipped_item:
|
|
button.connect("focus_entered", _on_equipment_slot_pressed.bind(slot_name))
|
|
slot_container.add_child(button)
|
|
|
|
equipment_slots[slot_name] = button
|
|
equipment_buttons[slot_name] = button
|
|
|
|
func _style_bar_font(lbl: Label) -> void:
|
|
lbl.add_theme_font_size_override("font_size", 10)
|
|
if ResourceLoader.exists("res://assets/fonts/standard_font.png"):
|
|
var fr = load("res://assets/fonts/standard_font.png")
|
|
if fr:
|
|
lbl.add_theme_font_override("font", fr)
|
|
|
|
func _make_progress_bar_background() -> StyleBoxFlat:
|
|
var bg = StyleBoxFlat.new()
|
|
bg.bg_color = Color(0.2, 0.2, 0.2, 0.8)
|
|
bg.border_color = Color(0.4, 0.4, 0.4)
|
|
bg.set_border_width_all(1)
|
|
return bg
|
|
|
|
func _create_bar_row(p_name: String, p_label_text: String) -> Dictionary:
|
|
var row = HBoxContainer.new()
|
|
row.name = p_name
|
|
row.add_theme_constant_override("separation", 4)
|
|
var left = Label.new()
|
|
left.text = p_label_text
|
|
left.custom_minimum_size.x = _BAR_LABEL_MIN_WIDTH
|
|
_style_bar_font(left)
|
|
var spacer = Control.new()
|
|
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
var value_lbl = Label.new()
|
|
value_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
|
value_lbl.custom_minimum_size.x = _BAR_VALUE_MIN_WIDTH
|
|
_style_bar_font(value_lbl)
|
|
var bar = ProgressBar.new()
|
|
bar.custom_minimum_size = Vector2(_BAR_WIDTH, 12)
|
|
bar.show_percentage = false
|
|
bar.add_theme_stylebox_override("background", _make_progress_bar_background())
|
|
row.add_child(left)
|
|
row.add_child(spacer)
|
|
row.add_child(value_lbl)
|
|
row.add_child(bar)
|
|
stats_panel.add_child(row)
|
|
return {"container": row, "label": left, "value_label": value_lbl, "progress_bar": bar}
|
|
|
|
func _create_hp_ui():
|
|
if not stats_panel:
|
|
return
|
|
var d = _create_bar_row("HPContainer", "HP:")
|
|
hp_container = d.container
|
|
hp_label = d.label
|
|
hp_value_label = d.value_label
|
|
hp_progress_bar = d.progress_bar
|
|
var fill = StyleBoxFlat.new()
|
|
fill.bg_color = Color(0.85, 0.2, 0.2)
|
|
hp_progress_bar.add_theme_stylebox_override("fill", fill)
|
|
|
|
func _create_mp_ui():
|
|
if not stats_panel:
|
|
return
|
|
var d = _create_bar_row("MPContainer", "MP:")
|
|
mp_container = d.container
|
|
mp_label = d.label
|
|
mp_value_label = d.value_label
|
|
mp_progress_bar = d.progress_bar
|
|
var fill = StyleBoxFlat.new()
|
|
fill.bg_color = Color(0.25, 0.45, 0.9)
|
|
mp_progress_bar.add_theme_stylebox_override("fill", fill)
|
|
|
|
func _create_weight_ui():
|
|
if not stats_panel:
|
|
return
|
|
var d = _create_bar_row("WeightContainer", "Weight:")
|
|
weight_container = d.container
|
|
weight_label = d.label
|
|
weight_value_label = d.value_label
|
|
weight_progress_bar = d.progress_bar
|
|
var fill = StyleBoxFlat.new()
|
|
fill.bg_color = Color(0.6, 0.8, 0.3)
|
|
weight_progress_bar.add_theme_stylebox_override("fill", fill)
|
|
|
|
func _create_exp_ui():
|
|
if not stats_panel:
|
|
return
|
|
var d = _create_bar_row("ExpContainer", "Exp:")
|
|
exp_container = d.container
|
|
exp_label = d.label
|
|
exp_value_label = d.value_label
|
|
exp_progress_bar = d.progress_bar
|
|
var fill = StyleBoxFlat.new()
|
|
fill.bg_color = Color(0.55, 0.35, 0.95)
|
|
exp_progress_bar.add_theme_stylebox_override("fill", fill)
|
|
|
|
func _create_coin_ui():
|
|
if not stats_panel:
|
|
return
|
|
coin_container = HBoxContainer.new()
|
|
coin_container.name = "CoinContainer"
|
|
coin_container.add_theme_constant_override("separation", 4)
|
|
coin_label = Label.new()
|
|
coin_label.name = "CoinLabel"
|
|
coin_label.text = "Coin:"
|
|
coin_label.custom_minimum_size.x = _BAR_LABEL_MIN_WIDTH
|
|
_style_bar_font(coin_label)
|
|
coin_container.add_child(coin_label)
|
|
var coin_spacer = Control.new()
|
|
coin_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
coin_container.add_child(coin_spacer)
|
|
var coin_wrap = Control.new()
|
|
coin_wrap.custom_minimum_size = Vector2(16, 16)
|
|
coin_sprite = Sprite2D.new()
|
|
coin_sprite.name = "CoinSprite"
|
|
var tex = load("res://assets/gfx/pickups/gold_coin.png") as Texture2D
|
|
if tex:
|
|
coin_sprite.texture = tex
|
|
coin_sprite.hframes = 6
|
|
coin_sprite.vframes = 1
|
|
coin_sprite.frame = 0
|
|
coin_sprite.centered = false
|
|
# Scale down to fit; texture may be multi-frame
|
|
var tw = tex.get_width() / 6.0
|
|
var th = tex.get_height()
|
|
if tw > 0 and th > 0:
|
|
var s = min(16.0 / tw, 16.0 / th)
|
|
coin_sprite.scale = Vector2(s, s)
|
|
coin_wrap.add_child(coin_sprite)
|
|
coin_container.add_child(coin_wrap)
|
|
coin_value_label = Label.new()
|
|
coin_value_label.name = "CoinValueLabel"
|
|
coin_value_label.text = "X 0"
|
|
coin_value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
|
_style_bar_font(coin_value_label)
|
|
coin_container.add_child(coin_value_label)
|
|
stats_panel.add_child(coin_container)
|
|
|
|
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 _can_use_inventory() -> bool:
|
|
# Block equip/unequip/use/drop/level-up when dead
|
|
if not local_player:
|
|
return false
|
|
if "is_dead" in local_player and local_player.is_dead:
|
|
return false
|
|
if "is_processing_death" in local_player and local_player.is_processing_death:
|
|
return false
|
|
return true
|
|
|
|
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
|
|
|
|
# Prevent updates during UI refresh (prevents infinite loops from focus_entered)
|
|
if is_updating_ui:
|
|
return
|
|
|
|
# Only select if there's an item equipped (same as arrow-key navigation)
|
|
if not _has_equipment_in_slot(slot_name):
|
|
return
|
|
|
|
# Select this slot (equivalent to arrow-key selecting this equipment)
|
|
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()
|
|
_update_info_panel()
|
|
|
|
func _on_equipment_slot_gui_input(event: InputEvent, slot_name: String):
|
|
# Handle double-click to unequip
|
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
|
if event.double_click and event.pressed:
|
|
# Double-click detected - unequip the item
|
|
selected_slot = slot_name
|
|
selected_item = local_player.character_stats.equipment[slot_name] if local_player and local_player.character_stats else null
|
|
selected_type = "equipment" if selected_item else ""
|
|
|
|
if selected_type == "equipment" and selected_slot != "":
|
|
# Use the same logic as F key to unequip
|
|
_handle_f_key()
|
|
|
|
func _update_selection_highlight():
|
|
# This function is kept for compatibility but now uses _update_selection_rectangle()
|
|
_update_selection_rectangle()
|
|
|
|
# Removed _clear_button_highlight and _apply_button_highlight - using focus system instead
|
|
|
|
func _update_selection_rectangle():
|
|
# Update visual selection indicator - use button focus like inspiration system
|
|
# Hide the old selection rectangle
|
|
if selection_rectangle:
|
|
selection_rectangle.visible = false
|
|
|
|
# Find and focus the selected button (like inspiration system uses grab_focus())
|
|
var target_button: Button = null
|
|
|
|
if selected_type == "equipment" and selected_slot != "":
|
|
# Focus equipment slot (only if it has an item)
|
|
if _has_equipment_in_slot(selected_slot):
|
|
target_button = equipment_buttons.get(selected_slot)
|
|
elif selected_type == "item" and inventory_selection_row >= 0 and inventory_selection_row < inventory_rows_list.size():
|
|
# Focus 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
|
|
|
|
# Grab focus on selected button (this will automatically show the focus stylebox)
|
|
if target_button:
|
|
if target_button.is_inside_tree():
|
|
# Don't grab focus if button already has focus (prevents infinite loops)
|
|
if target_button.has_focus():
|
|
return
|
|
|
|
# Ensure button is visible and ready
|
|
if not target_button.visible:
|
|
target_button.visible = true
|
|
# Wait a frame to ensure button is fully ready for focus
|
|
await get_tree().process_frame
|
|
# Check again if already focused (might have changed during await)
|
|
if target_button.has_focus():
|
|
return
|
|
# Ensure button can receive focus
|
|
if target_button.focus_mode == Control.FOCUS_NONE:
|
|
target_button.focus_mode = Control.FOCUS_ALL
|
|
# Try direct grab_focus (only if not already focused)
|
|
if not target_button.has_focus():
|
|
target_button.grab_focus()
|
|
# Also use call_deferred as backup to ensure it's set
|
|
target_button.call_deferred("grab_focus")
|
|
print("InventoryUI: Focus grabbed on button - has_focus: ", target_button.has_focus())
|
|
else:
|
|
# If not in tree yet, wait a frame and try again
|
|
await get_tree().process_frame
|
|
if target_button.is_inside_tree():
|
|
target_button.grab_focus()
|
|
target_button.call_deferred("grab_focus")
|
|
print("InventoryUI: Focus grabbed on button (after wait)")
|
|
else:
|
|
print("InventoryUI: Button still not in tree after wait")
|
|
else:
|
|
print("InventoryUI: No button to focus - selected_type: ", selected_type)
|
|
|
|
func _process(delta):
|
|
if is_open:
|
|
# Animate selection highlight border color on selected button
|
|
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)
|
|
|
|
# Find the selected button and update its highlight color
|
|
var selected_button: Button = null
|
|
if selected_type == "equipment" and selected_slot != "":
|
|
if _has_equipment_in_slot(selected_slot):
|
|
selected_button = equipment_buttons.get(selected_slot)
|
|
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 row and inventory_selection_col >= 0 and inventory_selection_col < row.get_child_count():
|
|
selected_button = row.get_child(inventory_selection_col) as Button
|
|
|
|
if selected_button and selected_button.has_meta("highlight_stylebox"):
|
|
var stylebox = selected_button.get_meta("highlight_stylebox") as StyleBoxFlat
|
|
if stylebox:
|
|
stylebox.border_color = animated_color
|
|
|
|
# Animate 6-frame coin sprite
|
|
if coin_sprite and coin_sprite.hframes >= 6:
|
|
coin_anim_time += delta * 10.0
|
|
coin_sprite.frame = int(coin_anim_time) % 6
|
|
|
|
func _update_ui():
|
|
if not local_player or not local_player.character_stats:
|
|
return
|
|
|
|
# Prevent recursive updates
|
|
if is_updating_ui:
|
|
return
|
|
is_updating_ui = true
|
|
|
|
var char_stats = local_player.character_stats
|
|
|
|
# Ensure containers don't clip their children (allows expand_margin to show properly)
|
|
if scroll_container:
|
|
scroll_container.clip_contents = false # Allow buttons to extend beyond scroll bounds
|
|
if inventory_grid:
|
|
inventory_grid.clip_contents = false # Allow buttons to extend beyond grid bounds
|
|
if equipment_panel:
|
|
equipment_panel.clip_contents = false # Allow buttons to extend beyond grid bounds
|
|
|
|
# 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()
|
|
|
|
# Wait for old buttons to be fully freed before creating new ones
|
|
await get_tree().process_frame
|
|
|
|
# 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
|
|
ItemDatabase.apply_item_colors_to_sprite(sprite, equipped_item)
|
|
button.add_child(sprite)
|
|
|
|
# Add quantity label if item can have multiple (like arrows, bombs)
|
|
var show_qty = equipped_item.can_have_multiple_of and (equipped_item.quantity > 1 or equipped_item.weapon_type == Item.WeaponType.BOMB)
|
|
if show_qty:
|
|
var quantity_label = Label.new()
|
|
quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
|
quantity_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
|
quantity_label.size = Vector2(36, 36)
|
|
quantity_label.custom_minimum_size = Vector2(36, 36)
|
|
quantity_label.position = Vector2(0, 0)
|
|
quantity_label.text = str(equipped_item.quantity)
|
|
# Use dmg_numbers.png font (same as inventory items)
|
|
var dmg_font_resource = load("res://assets/fonts/dmg_numbers.png")
|
|
if dmg_font_resource:
|
|
var font_file = FontFile.new()
|
|
font_file.font_data = dmg_font_resource
|
|
quantity_label.add_theme_font_override("font", font_file)
|
|
quantity_label.add_theme_font_size_override("font_size", 16)
|
|
quantity_label.add_theme_color_override("font_color", Color.GREEN)
|
|
quantity_label.z_index = 100 # High z-index to show above item sprite
|
|
quantity_label.z_as_relative = false # Absolute z-index
|
|
button.add_child(quantity_label)
|
|
|
|
# Update inventory grid - clear existing HBoxContainers
|
|
for child in inventory_grid.get_children():
|
|
child.queue_free()
|
|
|
|
# Wait for old buttons to be fully freed before creating new ones
|
|
await get_tree().process_frame
|
|
|
|
# Add inventory items using HBoxContainers (like inspiration system)
|
|
var current_hbox: HBoxContainer = null
|
|
var items_per_row = 8 # Items per row (3 rows = 24 total items max)
|
|
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
|
|
# Ensure HBoxContainer doesn't clip child buttons
|
|
current_hbox.clip_contents = false
|
|
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 size increased to accommodate 1.5x scaled texture (24 * 1.5 = 36)
|
|
button.custom_minimum_size = Vector2(36, 36) # Increased to fit 1.5x scaled texture
|
|
button.size = Vector2(36, 36)
|
|
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.focus_mode = Control.FOCUS_ALL # Allow button to receive focus
|
|
button.connect("pressed", _on_inventory_item_pressed.bind(item))
|
|
button.connect("gui_input", _on_inventory_item_gui_input.bind(item))
|
|
# Connect focus_entered like inspiration system (for keyboard navigation)
|
|
# Note: focus_entered will trigger when we call grab_focus(), but _on_inventory_item_pressed
|
|
# just updates selection state, so it should be safe
|
|
button.connect("focus_entered", _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
|
|
ItemDatabase.apply_item_colors_to_sprite(sprite, item)
|
|
button.add_child(sprite)
|
|
|
|
# Add quantity label if item quantity > 1 (show for all stacked items)
|
|
if item.quantity > 1:
|
|
var quantity_label = Label.new()
|
|
quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
|
quantity_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
|
quantity_label.size = Vector2(36, 36)
|
|
quantity_label.custom_minimum_size = Vector2(36, 36)
|
|
quantity_label.position = Vector2(0, 0)
|
|
quantity_label.text = str(item.quantity)
|
|
# Use dmg_numbers.png font (same as damage_number.gd)
|
|
var dmg_font_resource = load("res://assets/fonts/dmg_numbers.png")
|
|
if dmg_font_resource:
|
|
var font_file = FontFile.new()
|
|
font_file.font_data = dmg_font_resource
|
|
quantity_label.add_theme_font_override("font", font_file)
|
|
quantity_label.add_theme_font_size_override("font_size", 16)
|
|
quantity_label.add_theme_color_override("font_color", Color.GREEN)
|
|
quantity_label.z_index = 100 # High z-index to show above item sprite
|
|
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 only if selected_type is already set (don't auto-update during initialization)
|
|
# Don't call _set_selection() here if we already have a valid selection - it will reset to 0,0
|
|
# Only call it if selection is empty or invalid
|
|
var should_reset_selection = false
|
|
if selected_type == "":
|
|
should_reset_selection = true
|
|
elif selected_type == "item":
|
|
# Check if current selection is still valid
|
|
if inventory_selection_row < 0 or inventory_selection_row >= inventory_rows_list.size():
|
|
should_reset_selection = true
|
|
elif 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 or inventory_selection_col >= row.get_child_count():
|
|
should_reset_selection = true
|
|
|
|
if should_reset_selection:
|
|
_set_selection()
|
|
elif selected_type != "":
|
|
# Selection is valid, just update it
|
|
_update_selection_from_navigation()
|
|
_update_selection_rectangle()
|
|
_update_info_panel()
|
|
|
|
# Reset update flag
|
|
is_updating_ui = false
|
|
|
|
func _set_selection():
|
|
# NOW check for items AFTER UI is updated
|
|
# Initialize selection - prefer inventory, but if empty, check equipment
|
|
# Only set initial selection if not already set, or if current selection is invalid
|
|
var needs_initial_selection = false
|
|
if selected_type == "":
|
|
needs_initial_selection = true
|
|
elif selected_type == "item":
|
|
# Check if current selection is still valid
|
|
if inventory_selection_row >= inventory_rows_list.size() or inventory_selection_row < 0:
|
|
needs_initial_selection = true
|
|
elif 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() or inventory_selection_col < 0:
|
|
needs_initial_selection = true
|
|
|
|
# Check if we have inventory items
|
|
if inventory_rows_list.size() > 0 and inventory_items_list.size() > 0:
|
|
if needs_initial_selection:
|
|
# Only reset to 0 if we need initial selection
|
|
selected_type = "item"
|
|
inventory_selection_row = 0
|
|
inventory_selection_col = 0
|
|
# Ensure selection is set correctly (preserves existing selection if valid)
|
|
_update_selection_from_navigation()
|
|
# Debug: Print selection state
|
|
print("InventoryUI: Selection - type: ", selected_type, " row: ", inventory_selection_row, " col: ", inventory_selection_col, " item: ", selected_item)
|
|
# Now set focus - buttons should be ready
|
|
await _update_selection_rectangle() # Await to ensure focus is set
|
|
_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]
|
|
# Ensure selection is set correctly
|
|
_update_selection_from_navigation()
|
|
# Debug: Print selection state
|
|
print("InventoryUI: Initial selection - type: ", selected_type, " slot: ", selected_slot, " item: ", selected_item)
|
|
# Now set focus - buttons should be ready
|
|
await _update_selection_rectangle() # Await to ensure focus is set
|
|
_update_info_panel()
|
|
else:
|
|
# Nothing to select (only print this AFTER UI is updated)
|
|
selected_type = ""
|
|
if selection_rectangle:
|
|
selection_rectangle.visible = false
|
|
if info_label:
|
|
info_label.text = ""
|
|
print("InventoryUI: No items to select")
|
|
pass
|
|
|
|
func _update_selection_from_navigation():
|
|
# Update selected_item/selected_slot based on navigation position
|
|
# Early return if selected_type is not set yet (prevents errors during initialization)
|
|
if selected_type == "":
|
|
print("InventoryUI: _update_selection_from_navigation() - selected_type is empty, skipping")
|
|
return
|
|
|
|
print("InventoryUI: _update_selection_from_navigation() - selected_type: ", selected_type, " inventory_rows_list.size(): ", inventory_rows_list.size(), " inventory_items_list.size(): ", inventory_items_list.size())
|
|
|
|
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:
|
|
selected_item = null
|
|
print("InventoryUI: Selected equipment slot: ", slot_name, " item: ", selected_item)
|
|
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]
|
|
print("InventoryUI: Checking item selection - row: ", inventory_selection_row, " col: ", inventory_selection_col, " row exists: ", row != null, " row child count: ", row.get_child_count() if row else 0)
|
|
if row and inventory_selection_col >= 0 and inventory_selection_col < row.get_child_count():
|
|
var items_per_row = 8 # Must match the items_per_row used when building rows
|
|
var item_index = inventory_selection_row * items_per_row + inventory_selection_col
|
|
print("InventoryUI: Calculated item_index: ", item_index, " inventory_items_list.size(): ", inventory_items_list.size())
|
|
if item_index >= 0 and item_index < inventory_items_list.size():
|
|
selected_item = inventory_items_list[item_index]
|
|
selected_slot = ""
|
|
print("InventoryUI: Selected inventory item: ", selected_item.item_name if selected_item else "null")
|
|
else:
|
|
selected_item = null
|
|
selected_slot = ""
|
|
print("InventoryUI: item_index out of range!")
|
|
else:
|
|
selected_item = null
|
|
selected_slot = ""
|
|
print("InventoryUI: Row or column invalid!")
|
|
else:
|
|
print("InventoryUI: selected_type invalid or row out of range!")
|
|
|
|
func _format_item_info(item: Item) -> String:
|
|
# Format item description, stats modifiers, and controls
|
|
var text = ""
|
|
|
|
# Item name (always show)
|
|
text += item.item_name
|
|
|
|
# Description
|
|
if item.description != "":
|
|
text += "\n" + item.description
|
|
|
|
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 += ", ".join(stat_lines)
|
|
text += "\n\n"
|
|
|
|
# Weight
|
|
var item_weight = item.weight
|
|
if item.can_have_multiple_of:
|
|
item_weight *= item.quantity
|
|
text += "Weight: %.1f" % item_weight
|
|
text += "\n\n"
|
|
|
|
# Controls
|
|
if item.weapon_type == Item.WeaponType.SPELLBOOK:
|
|
if local_player and local_player.character_stats:
|
|
var spell_id = local_player.character_stats._tome_to_spell_id(item) if local_player.character_stats.has_method("_tome_to_spell_id") else ""
|
|
if spell_id != "" and spell_id in local_player.character_stats.learnt_spells:
|
|
text += "Already learnt."
|
|
else:
|
|
text += "Press F to learn spell"
|
|
else:
|
|
text += "Press F to learn spell"
|
|
elif 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"
|
|
|
|
# Only show "Press E to drop" for inventory items, not equipment
|
|
if selected_type == "item":
|
|
text += ", Press E to drop"
|
|
|
|
return text
|
|
|
|
func _update_info_panel():
|
|
# Update info panel based on selected item
|
|
if not info_label:
|
|
print("InventoryUI: _update_info_panel() - info_label is null!")
|
|
return
|
|
|
|
print("InventoryUI: _update_info_panel() - selected_item: ", selected_item, " selected_type: ", selected_type)
|
|
if selected_item:
|
|
info_label.text = _format_item_info(selected_item)
|
|
print("InventoryUI: Info panel text set: ", info_label.text.substr(0, 50) if info_label.text.length() > 50 else info_label.text)
|
|
else:
|
|
info_label.text = ""
|
|
print("InventoryUI: Info panel text cleared (no selected_item)")
|
|
|
|
func _navigate_inventory(direction: String):
|
|
# Handle navigation within inventory
|
|
match direction:
|
|
"left":
|
|
if inventory_selection_col > 0:
|
|
inventory_selection_col -= 1
|
|
else:
|
|
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
|
|
elif local_player and local_player.character_stats and local_player.character_stats.pending_level_up and local_player.character_stats.pending_stat_points > 0 and level_up_stat_buttons.size() > 0:
|
|
selected_type = "level_up_stat"
|
|
selected_level_up_stat_index = 0
|
|
selected_slot = ""
|
|
selected_item = null
|
|
level_up_stat_buttons[0].call_deferred("grab_focus")
|
|
if info_label and CharacterStats.LEVEL_UP_STAT_NAMES.size() > 0:
|
|
var sn = CharacterStats.LEVEL_UP_STAT_NAMES[0]
|
|
if sn in STAT_DESCRIPTIONS:
|
|
info_label.text = STAT_DESCRIPTIONS[sn]
|
|
_update_selection_rectangle()
|
|
return
|
|
"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
|
|
elif local_player and local_player.character_stats and local_player.character_stats.pending_level_up and local_player.character_stats.pending_stat_points > 0 and level_up_stat_buttons.size() > 0:
|
|
selected_type = "level_up_stat"
|
|
selected_level_up_stat_index = 0
|
|
selected_slot = ""
|
|
selected_item = null
|
|
level_up_stat_buttons[0].call_deferred("grab_focus")
|
|
if info_label and CharacterStats.LEVEL_UP_STAT_NAMES.size() > 0:
|
|
var sn = CharacterStats.LEVEL_UP_STAT_NAMES[0]
|
|
if sn in STAT_DESCRIPTIONS:
|
|
info_label.text = STAT_DESCRIPTIONS[sn]
|
|
return
|
|
"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), or go to stats
|
|
var current_row: int = floor(equipment_selection_index / 3.0)
|
|
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:
|
|
var next_index = _find_next_filled_equipment_slot(target_index - 1, 1)
|
|
if next_index >= 0 and next_index < 3:
|
|
equipment_selection_index = next_index
|
|
elif local_player and local_player.character_stats and local_player.character_stats.pending_level_up and local_player.character_stats.pending_stat_points > 0 and level_up_stat_buttons.size() > 0:
|
|
selected_type = "level_up_stat"
|
|
selected_level_up_stat_index = 0
|
|
selected_slot = ""
|
|
selected_item = null
|
|
level_up_stat_buttons[0].call_deferred("grab_focus")
|
|
if info_label and CharacterStats.LEVEL_UP_STAT_NAMES.size() > 0:
|
|
var sn = CharacterStats.LEVEL_UP_STAT_NAMES[0]
|
|
if sn in STAT_DESCRIPTIONS:
|
|
info_label.text = STAT_DESCRIPTIONS[sn]
|
|
return
|
|
"down":
|
|
# Find next filled slot in row below (same column), or move to inventory
|
|
var current_row: int = floor(equipment_selection_index / 3.0)
|
|
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 (only if inventory has items)
|
|
if inventory_rows_list.size() > 0 and inventory_items_list.size() > 0:
|
|
selected_type = "item"
|
|
inventory_selection_row = 0
|
|
inventory_selection_col = current_col
|
|
# Clamp to valid range
|
|
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
|
|
_update_selection_from_navigation()
|
|
_update_selection_rectangle()
|
|
_update_info_panel()
|
|
return
|
|
# No inventory items, stay on equipment
|
|
else:
|
|
# Already at bottom row, move to inventory (only if inventory has items)
|
|
if inventory_rows_list.size() > 0 and inventory_items_list.size() > 0:
|
|
selected_type = "item"
|
|
inventory_selection_row = 0
|
|
inventory_selection_col = current_col
|
|
# Clamp to valid range
|
|
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
|
|
_update_selection_from_navigation()
|
|
_update_selection_rectangle()
|
|
_update_info_panel()
|
|
return
|
|
# No inventory items, stay on equipment
|
|
|
|
_update_selection_from_navigation()
|
|
_update_selection_rectangle()
|
|
_update_info_panel()
|
|
|
|
func _navigate_level_up_stats(direction: String) -> void:
|
|
var n = level_up_stat_buttons.size()
|
|
if n == 0:
|
|
return
|
|
match direction:
|
|
"left":
|
|
selected_level_up_stat_index = (selected_level_up_stat_index - 1 + n) % n
|
|
level_up_stat_buttons[selected_level_up_stat_index].call_deferred("grab_focus")
|
|
if info_label and selected_level_up_stat_index < CharacterStats.LEVEL_UP_STAT_NAMES.size():
|
|
var sn = CharacterStats.LEVEL_UP_STAT_NAMES[selected_level_up_stat_index]
|
|
if sn in STAT_DESCRIPTIONS:
|
|
info_label.text = STAT_DESCRIPTIONS[sn]
|
|
"right":
|
|
if selected_level_up_stat_index >= n - 1:
|
|
# Past last stat: go to equipment
|
|
selected_type = "equipment"
|
|
selected_level_up_stat_index = -1
|
|
selected_slot = ""
|
|
selected_item = null
|
|
var next_index = _find_next_filled_equipment_slot(-1, 1)
|
|
if next_index >= 0:
|
|
equipment_selection_index = next_index
|
|
selected_slot = equipment_slots_list[next_index]
|
|
selected_item = local_player.character_stats.equipment[selected_slot] if local_player and local_player.character_stats else null
|
|
_update_selection_from_navigation()
|
|
_update_selection_rectangle()
|
|
_update_info_panel()
|
|
return
|
|
selected_level_up_stat_index += 1
|
|
level_up_stat_buttons[selected_level_up_stat_index].call_deferred("grab_focus")
|
|
if info_label and selected_level_up_stat_index < CharacterStats.LEVEL_UP_STAT_NAMES.size():
|
|
var sn = CharacterStats.LEVEL_UP_STAT_NAMES[selected_level_up_stat_index]
|
|
if sn in STAT_DESCRIPTIONS:
|
|
info_label.text = STAT_DESCRIPTIONS[sn]
|
|
"up":
|
|
pass
|
|
"down":
|
|
selected_type = "equipment"
|
|
selected_level_up_stat_index = -1
|
|
var next_index = _find_next_filled_equipment_slot(-1, 1)
|
|
if next_index >= 0:
|
|
equipment_selection_index = next_index
|
|
selected_slot = equipment_slots_list[next_index]
|
|
selected_item = local_player.character_stats.equipment[selected_slot] if local_player and local_player.character_stats else null
|
|
_update_selection_from_navigation()
|
|
_update_selection_rectangle()
|
|
_update_info_panel()
|
|
return
|
|
# Don't call _update_info_panel - we've set stat description above
|
|
|
|
func _on_inventory_item_pressed(item: Item):
|
|
if not local_player or not local_player.character_stats:
|
|
return
|
|
|
|
# Prevent updates during UI refresh (prevents infinite loops from focus_entered)
|
|
if is_updating_ui:
|
|
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: int = 8
|
|
inventory_selection_row = floor(item_index / float(items_per_row))
|
|
inventory_selection_col = item_index % items_per_row
|
|
|
|
_update_selection_highlight()
|
|
_update_selection_rectangle()
|
|
_update_info_panel() # Show item description on single-click
|
|
|
|
func _on_inventory_item_gui_input(event: InputEvent, item: Item):
|
|
# Handle double-click to equip/consume and right-click to drop
|
|
if event is InputEventMouseButton:
|
|
if event.button_index == MOUSE_BUTTON_LEFT and event.double_click and event.pressed:
|
|
# Double-click detected - equip or consume the item
|
|
selected_item = item
|
|
selected_slot = ""
|
|
selected_type = "item"
|
|
|
|
# Update navigation position first
|
|
var item_index = inventory_items_list.find(item)
|
|
if item_index >= 0:
|
|
var items_per_row: int = 8
|
|
inventory_selection_row = floor(item_index / float(items_per_row))
|
|
inventory_selection_col = item_index % items_per_row
|
|
|
|
# Use the same logic as F key to equip/consume
|
|
_handle_f_key()
|
|
elif event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
|
|
# Right-click detected - drop the item
|
|
selected_item = item
|
|
selected_slot = ""
|
|
selected_type = "item"
|
|
|
|
# Update navigation position first
|
|
var item_index = inventory_items_list.find(item)
|
|
if item_index >= 0:
|
|
var items_per_row: int = 8
|
|
inventory_selection_row = floor(item_index / float(items_per_row))
|
|
inventory_selection_col = item_index % items_per_row
|
|
|
|
# Use the same logic as E key to drop
|
|
_handle_e_key()
|
|
|
|
func _on_mana_changed(new_mana: float, max_mana: float):
|
|
# Update only MP bar and label when mana changes (don't rebuild entire UI)
|
|
if mp_progress_bar and mp_value_label:
|
|
mp_progress_bar.max_value = max(1.0, max_mana)
|
|
mp_progress_bar.value = new_mana
|
|
mp_value_label.text = str(int(new_mana)) + "/" + str(int(max_mana))
|
|
|
|
func _on_health_changed(new_hp: float, max_hp: float):
|
|
# Update only HP bar and label when health changes (don't rebuild entire UI)
|
|
if hp_progress_bar and hp_value_label:
|
|
hp_progress_bar.max_value = max(1.0, max_hp)
|
|
hp_progress_bar.value = new_hp
|
|
hp_value_label.text = str(int(new_hp)) + "/" + str(int(max_hp))
|
|
|
|
func _on_character_changed(_char: CharacterStats):
|
|
# Always update stats when character changes (even if inventory is closed)
|
|
# Equipment changes affect max HP/MP which should be reflected everywhere
|
|
_update_stats()
|
|
|
|
# Only update UI if inventory is open (prevents unnecessary updates)
|
|
if not is_open:
|
|
return
|
|
# Prevent recursive updates
|
|
if is_updating_ui:
|
|
return
|
|
_update_ui()
|
|
|
|
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
|
|
|
|
# B key: switch to Spell Book tab when inventory is open
|
|
if event is InputEventKey and event.keycode == KEY_B and event.pressed and not event.echo:
|
|
_current_tab = "spell_book" if _current_tab == "inventory" else "inventory"
|
|
_apply_tab_visibility()
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
|
|
# Arrow key navigation (use ui_left/right/up/down so keybindings work)
|
|
var direction = ""
|
|
var skip_repeat = event is InputEventKey and event.echo
|
|
if not skip_repeat and event.is_action_pressed("ui_left"):
|
|
direction = "left"
|
|
elif not skip_repeat and event.is_action_pressed("ui_right"):
|
|
direction = "right"
|
|
elif not skip_repeat and event.is_action_pressed("ui_up"):
|
|
direction = "up"
|
|
elif not skip_repeat and event.is_action_pressed("ui_down"):
|
|
direction = "down"
|
|
if direction != "":
|
|
if _current_tab == "spell_book":
|
|
pass
|
|
elif selected_type == "level_up_stat":
|
|
_navigate_level_up_stats(direction)
|
|
elif 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
|
|
|
|
# Attack: Allocate level-up stat when a stat button is focused
|
|
if event.is_action_pressed("attack") and not event.is_echo():
|
|
var fc = get_viewport().gui_get_focus_owner()
|
|
if level_up_stat_container and fc and is_instance_valid(level_up_stat_container) and fc.get_parent() == level_up_stat_container:
|
|
var idx = level_up_stat_buttons.find(fc)
|
|
if idx >= 0 and idx < CharacterStats.LEVEL_UP_STAT_NAMES.size():
|
|
_on_level_up_stat_pressed(CharacterStats.LEVEL_UP_STAT_NAMES[idx])
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
|
|
func _handle_f_key():
|
|
if not local_player or not local_player.character_stats:
|
|
return
|
|
if not _can_use_inventory():
|
|
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:
|
|
# Tome in offhand: use (F) to learn spell and consume tome
|
|
if equipped_item.weapon_type == Item.WeaponType.SPELLBOOK and char_stats.has_method("learn_spell_from_tome"):
|
|
if char_stats.learn_spell_from_tome(equipped_item):
|
|
_update_ui()
|
|
_update_selection_from_navigation()
|
|
_update_selection_rectangle()
|
|
_update_info_panel()
|
|
if sfx_armour:
|
|
sfx_armour.play()
|
|
return
|
|
char_stats.unequip_item(equipped_item)
|
|
# Play armour sound when unequipping
|
|
if sfx_armour:
|
|
sfx_armour.play()
|
|
# 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:
|
|
# Tome in inventory: use (F) to learn spell and consume tome
|
|
if selected_item.weapon_type == Item.WeaponType.SPELLBOOK and char_stats.has_method("learn_spell_from_tome"):
|
|
if char_stats.learn_spell_from_tome(selected_item):
|
|
var current_item_index = inventory_selection_row * 8 + inventory_selection_col
|
|
_update_ui()
|
|
if current_item_index < char_stats.inventory.size():
|
|
inventory_selection_row = current_item_index / 8
|
|
inventory_selection_col = current_item_index % 8
|
|
elif char_stats.inventory.size() > 0:
|
|
inventory_selection_row = 0
|
|
inventory_selection_col = 0
|
|
_update_selection_from_navigation()
|
|
_update_selection_rectangle()
|
|
_update_info_panel()
|
|
if sfx_armour:
|
|
sfx_armour.play()
|
|
return
|
|
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"
|
|
|
|
# Remember current item position before equipping
|
|
var items_per_row = 8
|
|
var current_item_index = inventory_selection_row * items_per_row + inventory_selection_col
|
|
|
|
# Check if target slot has an item (will be placed back in inventory)
|
|
var slot_has_item = char_stats.equipment[target_slot_name] != null
|
|
|
|
# Check if this is the last item in inventory (before equipping)
|
|
var was_last_item = char_stats.inventory.size() == 1
|
|
|
|
# Equip the item, placing old item at the same position if slot had an item
|
|
var insert_index = current_item_index if slot_has_item else -1
|
|
char_stats.equip_item(selected_item, insert_index)
|
|
|
|
# Play armour sound when equipping
|
|
if sfx_armour:
|
|
sfx_armour.play()
|
|
|
|
# Update UI first
|
|
_update_ui()
|
|
|
|
# If slot had an item, keep selection at the same position (old item is now there)
|
|
if slot_has_item and current_item_index < char_stats.inventory.size():
|
|
# Keep selection at the same position
|
|
selected_type = "item"
|
|
selected_slot = ""
|
|
# Recalculate row/col from index (may have changed if rows shifted)
|
|
inventory_selection_row = floor(current_item_index / float(items_per_row))
|
|
inventory_selection_col = current_item_index % items_per_row
|
|
# Ensure row/col are within bounds
|
|
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_from_navigation()
|
|
elif was_last_item and target_slot_name != "":
|
|
# Last item was equipped, move selection to equipment slot
|
|
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_selection_from_navigation()
|
|
else:
|
|
# Item was removed, try to keep selection at same position if possible
|
|
if current_item_index < char_stats.inventory.size():
|
|
# Item at next position moved up, keep selection there
|
|
selected_type = "item"
|
|
selected_slot = ""
|
|
inventory_selection_row = floor(current_item_index / float(items_per_row))
|
|
inventory_selection_col = current_item_index % items_per_row
|
|
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_from_navigation()
|
|
elif current_item_index > 0:
|
|
# Move to previous position (current_item_index - 1) if current is out of bounds
|
|
var previous_index = current_item_index - 1
|
|
inventory_selection_row = floor(previous_index / float(items_per_row))
|
|
inventory_selection_col = previous_index % items_per_row
|
|
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)
|
|
selected_type = "item"
|
|
selected_slot = ""
|
|
_update_selection_from_navigation()
|
|
else:
|
|
# Previous position is also out of bounds, move to equipment if available
|
|
selected_type = ""
|
|
selected_slot = ""
|
|
selected_item = null
|
|
_update_selection_from_navigation()
|
|
else:
|
|
# No items left, move to equipment if available
|
|
selected_type = ""
|
|
selected_slot = ""
|
|
selected_item = null
|
|
_update_selection_from_navigation()
|
|
|
|
# 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)
|
|
_update_info_panel()
|
|
|
|
func _use_consumable_item(item: Item):
|
|
if not local_player or not local_player.character_stats:
|
|
return
|
|
if not _can_use_inventory():
|
|
return
|
|
|
|
var char_stats = local_player.character_stats
|
|
|
|
# Remember current item position before consuming
|
|
var items_per_row = 8
|
|
var current_item_index = inventory_selection_row * items_per_row + inventory_selection_col
|
|
|
|
# Determine if it's a potion or food based on item name
|
|
var is_potion = "potion" in item.item_name.to_lower()
|
|
|
|
# Play appropriate sound
|
|
if is_potion:
|
|
if sfx_potion:
|
|
sfx_potion.play()
|
|
else:
|
|
# Food item
|
|
if sfx_food:
|
|
sfx_food.play()
|
|
|
|
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)
|
|
# Dodge potion (and any consumable with dodge_chance + duration): apply temporary dodge buff
|
|
if item.modifiers.has("dodge_chance") and item.duration > 0:
|
|
char_stats.add_buff_dodge_chance(item.modifiers["dodge_chance"], item.duration)
|
|
|
|
var index = char_stats.inventory.find(item)
|
|
if index >= 0:
|
|
char_stats.inventory.remove_at(index)
|
|
char_stats.character_changed.emit(char_stats)
|
|
|
|
# Update UI first
|
|
_update_ui()
|
|
|
|
# Try to keep selection at the same position if possible
|
|
if current_item_index < char_stats.inventory.size():
|
|
# Item at next position moved up, keep selection there
|
|
selected_type = "item"
|
|
selected_slot = ""
|
|
inventory_selection_row = floor(current_item_index / float(items_per_row))
|
|
inventory_selection_col = current_item_index % items_per_row
|
|
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_from_navigation()
|
|
_update_selection_rectangle()
|
|
elif current_item_index > 0:
|
|
# Move to previous position if current is out of bounds
|
|
current_item_index = char_stats.inventory.size() - 1
|
|
inventory_selection_row = floor(current_item_index / float(items_per_row))
|
|
inventory_selection_col = current_item_index % items_per_row
|
|
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_from_navigation()
|
|
_update_selection_rectangle()
|
|
else:
|
|
# No items left, clear selection
|
|
selected_type = ""
|
|
selected_slot = ""
|
|
selected_item = null
|
|
_update_selection_from_navigation()
|
|
_update_selection_rectangle()
|
|
|
|
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 not _can_use_inventory():
|
|
return
|
|
|
|
if selected_type != "item" or not selected_item:
|
|
return
|
|
|
|
var char_stats = local_player.character_stats
|
|
|
|
# Play armour sound when dropping equipment
|
|
if selected_item.item_type == Item.ItemType.Equippable:
|
|
if sfx_armour:
|
|
sfx_armour.play()
|
|
|
|
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")
|
|
|
|
# In multiplayer, clients need to request server to spawn loot
|
|
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
|
|
# Client: send drop request to server
|
|
if game_world and game_world.has_method("_request_item_drop"):
|
|
game_world._request_item_drop.rpc_id(1, selected_item.save(), drop_position, local_player.get_multiplayer_authority())
|
|
else:
|
|
# Fallback: try to spawn locally (won't sync)
|
|
if entities_node:
|
|
var loot = ItemLootHelper.spawn_item_loot(selected_item, drop_position, entities_node, game_world)
|
|
if loot and 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())
|
|
else:
|
|
# Server or single-player: spawn directly
|
|
if entities_node:
|
|
var loot = ItemLootHelper.spawn_item_loot(selected_item, drop_position, entities_node, game_world)
|
|
if loot and 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
|
|
|
|
# Workaround: On first open, immediately close and reopen to ensure proper initialization
|
|
if is_first_open:
|
|
is_first_open = false
|
|
is_open = true
|
|
if container:
|
|
container.visible = true
|
|
_lock_player_controls(true)
|
|
_update_ui()
|
|
# Wait a frame
|
|
await get_tree().process_frame
|
|
# Close immediately
|
|
_close_inventory()
|
|
# Wait a frame
|
|
await get_tree().process_frame
|
|
# Now reopen properly (will continue with normal flow below)
|
|
|
|
is_open = true
|
|
if container:
|
|
container.visible = true
|
|
|
|
_lock_player_controls(true)
|
|
|
|
# Reset selection state BEFORE updating UI (so _update_ui doesn't try to update selection)
|
|
selected_type = ""
|
|
selected_item = null
|
|
selected_slot = ""
|
|
|
|
_update_ui()
|
|
_update_stats()
|
|
_apply_tab_visibility()
|
|
|
|
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 _on_inventory_tab_pressed():
|
|
_current_tab = "inventory"
|
|
# Reset selection so first item gets selected (same as TAB-open flow)
|
|
selected_type = ""
|
|
selected_item = null
|
|
selected_slot = ""
|
|
inventory_selection_row = 0
|
|
inventory_selection_col = 0
|
|
_apply_tab_visibility()
|
|
_update_ui()
|
|
|
|
func _on_spell_book_tab_pressed():
|
|
_current_tab = "spell_book"
|
|
_apply_tab_visibility()
|
|
|
|
func _apply_tab_visibility():
|
|
var show_inv = (_current_tab == "inventory")
|
|
if inventory_hbox:
|
|
inventory_hbox.visible = show_inv
|
|
if spell_book_panel:
|
|
spell_book_panel.visible = not show_inv
|
|
if not show_inv and spell_book_panel.has_method("set_character_stats"):
|
|
if local_player and local_player.character_stats:
|
|
spell_book_panel.set_character_stats(local_player.character_stats)
|
|
if spell_book_panel.has_method("refresh"):
|
|
spell_book_panel.refresh()
|
|
if selection_rectangle:
|
|
selection_rectangle.visible = show_inv
|
|
if info_panel:
|
|
info_panel.visible = show_inv
|
|
|
|
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
|
|
if lock:
|
|
# Stop movement immediately when opening inventory (physics may have already run this frame)
|
|
player.velocity = Vector2.ZERO
|