feat(inventory): hotbar + inventory UI + 5 crafting recipes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Floppyrj45
2026-04-19 17:16:13 +02:00
parent af94645d4c
commit d39a55dc45
7 changed files with 524 additions and 3 deletions

View File

@@ -0,0 +1,279 @@
extends CanvasLayer
const SLOT_SIZE: int = 48
const SLOT_MARGIN: int = 4
var inventory: Inventory = null
var _hotbar_slots: Array = []
var _inv_slots: Array = []
var _inv_window: Control = null
var _recipe_list: VBoxContainer = null
var _is_open: bool = false
func _ready() -> void:
_build_hotbar()
_build_inventory_window()
func setup(inv: Inventory) -> void:
inventory = inv
inventory.inventory_changed.connect(_refresh)
_refresh()
func _build_hotbar() -> void:
var hotbar_root := HBoxContainer.new()
hotbar_root.name = "HotbarRoot"
hotbar_root.add_theme_constant_override("separation", SLOT_MARGIN)
var total_width: int = SLOT_SIZE * 9 + SLOT_MARGIN * 8
var anchor_node := Control.new()
anchor_node.name = "HotbarAnchor"
anchor_node.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
anchor_node.custom_minimum_size = Vector2(total_width, SLOT_SIZE + 8)
anchor_node.set_offset(SIDE_BOTTOM, -8)
anchor_node.set_offset(SIDE_TOP, -(SLOT_SIZE + 16))
add_child(anchor_node)
hotbar_root.set_anchors_preset(Control.PRESET_CENTER)
hotbar_root.position = Vector2(-total_width / 2.0, 0)
anchor_node.add_child(hotbar_root)
for i: int in range(9):
var slot := _make_slot_panel(i)
hotbar_root.add_child(slot)
_hotbar_slots.append(slot)
func _make_slot_panel(index: int) -> PanelContainer:
var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(SLOT_SIZE, SLOT_SIZE)
panel.set_meta("slot_index", index)
var style_normal := StyleBoxFlat.new()
style_normal.bg_color = Color(0.05, 0.1, 0.2, 0.85)
style_normal.border_color = Color(0.0, 0.8, 0.8)
style_normal.set_border_width_all(2)
style_normal.set_corner_radius_all(3)
panel.add_theme_stylebox_override("panel", style_normal)
panel.set_meta("style_normal", style_normal)
var style_selected := StyleBoxFlat.new()
style_selected.bg_color = Color(0.05, 0.1, 0.2, 0.85)
style_selected.border_color = Color(1.0, 0.9, 0.1)
style_selected.set_border_width_all(3)
style_selected.set_corner_radius_all(3)
panel.set_meta("style_selected", style_selected)
var vbox := VBoxContainer.new()
vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
panel.add_child(vbox)
var color_rect := ColorRect.new()
color_rect.name = "ColorRect"
color_rect.custom_minimum_size = Vector2(SLOT_SIZE - 8, SLOT_SIZE - 18)
color_rect.color = Color(0, 0, 0, 0)
vbox.add_child(color_rect)
var count_label := Label.new()
count_label.name = "CountLabel"
count_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
count_label.add_theme_font_size_override("font_size", 10)
count_label.text = ""
vbox.add_child(count_label)
return panel
func _build_inventory_window() -> void:
_inv_window = Control.new()
_inv_window.name = "InventoryWindow"
_inv_window.set_anchors_preset(Control.PRESET_FULL_RECT)
_inv_window.visible = false
add_child(_inv_window)
# Dim background
var bg := ColorRect.new()
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
bg.color = Color(0, 0, 0, 0.55)
_inv_window.add_child(bg)
# Main HBox: left = slots, right = craft panel
var hbox := HBoxContainer.new()
hbox.set_anchors_preset(Control.PRESET_CENTER)
hbox.add_theme_constant_override("separation", 16)
_inv_window.add_child(hbox)
# Center it
hbox.set_offset(SIDE_LEFT, -360)
hbox.set_offset(SIDE_RIGHT, 360)
hbox.set_offset(SIDE_TOP, -240)
hbox.set_offset(SIDE_BOTTOM, 240)
# Left panel: inventory grid + hotbar row
var left_vbox := VBoxContainer.new()
left_vbox.add_theme_constant_override("separation", 8)
hbox.add_child(left_vbox)
var inv_label := Label.new()
inv_label.text = "Inventaire"
inv_label.add_theme_font_size_override("font_size", 14)
left_vbox.add_child(inv_label)
# 3 rows × 9 cols = 27 slots (indices 9..35)
var inv_grid := GridContainer.new()
inv_grid.columns = 9
inv_grid.add_theme_constant_override("h_separation", SLOT_MARGIN)
inv_grid.add_theme_constant_override("v_separation", SLOT_MARGIN)
left_vbox.add_child(inv_grid)
for i: int in range(9, 36):
var slot := _make_slot_panel(i)
inv_grid.add_child(slot)
_inv_slots.append(slot)
# Separator
var sep := HSeparator.new()
left_vbox.add_child(sep)
var hbar_label := Label.new()
hbar_label.text = "Hotbar"
hbar_label.add_theme_font_size_override("font_size", 12)
left_vbox.add_child(hbar_label)
# Hotbar row inside inventory (mirror of bottom hotbar)
var hotbar_grid := HBoxContainer.new()
hotbar_grid.add_theme_constant_override("separation", SLOT_MARGIN)
left_vbox.add_child(hotbar_grid)
for i: int in range(9):
var slot := _make_slot_panel(i)
hotbar_grid.add_child(slot)
_inv_slots.append(slot) # also tracked here for refresh
# Right panel: craft
var right_vbox := VBoxContainer.new()
right_vbox.custom_minimum_size = Vector2(200, 0)
right_vbox.add_theme_constant_override("separation", 6)
hbox.add_child(right_vbox)
var craft_label := Label.new()
craft_label.text = "Craft"
craft_label.add_theme_font_size_override("font_size", 14)
right_vbox.add_child(craft_label)
var scroll := ScrollContainer.new()
scroll.custom_minimum_size = Vector2(200, 380)
right_vbox.add_child(scroll)
_recipe_list = VBoxContainer.new()
_recipe_list.add_theme_constant_override("separation", 6)
scroll.add_child(_recipe_list)
func _refresh() -> void:
if inventory == null:
return
_refresh_hotbar_slots()
if _is_open:
_refresh_inv_slots()
_refresh_recipes()
func _refresh_hotbar_slots() -> void:
for i: int in range(_hotbar_slots.size()):
_update_slot_visual(_hotbar_slots[i], i)
func _refresh_inv_slots() -> void:
# _inv_slots contains: 27 inv slots (9..35) + 9 hotbar mirror (0..8)
for i: int in range(27):
_update_slot_visual(_inv_slots[i], i + 9)
for i: int in range(9):
_update_slot_visual(_inv_slots[27 + i], i)
func _update_slot_visual(panel: PanelContainer, slot_index: int) -> void:
var is_selected: bool = (slot_index == inventory.selected_hotbar and slot_index < 9)
if is_selected:
panel.add_theme_stylebox_override("panel", panel.get_meta("style_selected"))
panel.scale = Vector2(1.1, 1.1)
else:
panel.add_theme_stylebox_override("panel", panel.get_meta("style_normal"))
panel.scale = Vector2(1.0, 1.0)
var slot_data: Variant = inventory.slots[slot_index]
var color_rect: ColorRect = panel.get_node("VBoxContainer/ColorRect")
var count_label: Label = panel.get_node("VBoxContainer/CountLabel")
if slot_data == null:
color_rect.color = Color(0, 0, 0, 0)
count_label.text = ""
else:
color_rect.color = ItemDatabase.get_item_color(slot_data["item_id"])
if slot_data["count"] > 1:
count_label.text = str(slot_data["count"])
else:
count_label.text = ""
func _refresh_recipes() -> void:
for child in _recipe_list.get_children():
child.queue_free()
for i: int in range(CraftingRecipes.RECIPES.size()):
var recipe: Dictionary = CraftingRecipes.RECIPES[i]
var can_craft: bool = inventory.has_items(recipe["inputs"])
var recipe_panel := VBoxContainer.new()
recipe_panel.add_theme_constant_override("separation", 2)
_recipe_list.add_child(recipe_panel)
var name_label := Label.new()
name_label.text = recipe["name"]
name_label.add_theme_font_size_override("font_size", 11)
recipe_panel.add_child(name_label)
# Inputs summary
var inputs_text: String = ""
for inp: Dictionary in recipe["inputs"]:
inputs_text += "%s x%d " % [ItemDatabase.get_item_name(inp["item_id"]), inp["count"]]
var inp_label := Label.new()
inp_label.text = inputs_text.strip_edges()
inp_label.add_theme_font_size_override("font_size", 9)
inp_label.modulate = Color(0.7, 0.7, 0.7)
recipe_panel.add_child(inp_label)
var craft_btn := Button.new()
craft_btn.text = "Creer"
craft_btn.disabled = not can_craft
var idx: int = i
craft_btn.pressed.connect(func() -> void: _on_craft_pressed(idx))
recipe_panel.add_child(craft_btn)
var sep := HSeparator.new()
_recipe_list.add_child(sep)
func _on_craft_pressed(recipe_index: int) -> void:
CraftingRecipes.craft(inventory, recipe_index)
func _input(event: InputEvent) -> void:
if event.is_action_pressed("toggle_inventory"):
_toggle_inventory()
get_viewport().set_input_as_handled()
func _toggle_inventory() -> void:
_is_open = not _is_open
_inv_window.visible = _is_open
if _is_open:
get_tree().paused = true
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_refresh_inv_slots()
_refresh_recipes()
else:
get_tree().paused = false
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED