feat(inventory): hotbar + inventory UI + 5 crafting recipes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
50
scripts/inventory/CraftingRecipes.gd
Normal file
50
scripts/inventory/CraftingRecipes.gd
Normal file
@@ -0,0 +1,50 @@
|
||||
class_name CraftingRecipes
|
||||
extends RefCounted
|
||||
|
||||
static var RECIPES: Array = [
|
||||
{
|
||||
"name": "Lampe bioluminescente",
|
||||
"inputs": [{"item_id": 5, "count": 2}, {"item_id": 6, "count": 1}],
|
||||
"output": {"item_id": 100, "count": 1}
|
||||
},
|
||||
{
|
||||
"name": "Harpon",
|
||||
"inputs": [{"item_id": 3, "count": 2}, {"item_id": 7, "count": 2}],
|
||||
"output": {"item_id": 101, "count": 1}
|
||||
},
|
||||
{
|
||||
"name": "Bulle d'air",
|
||||
"inputs": [{"item_id": 6, "count": 3}, {"item_id": 8, "count": 1}],
|
||||
"output": {"item_id": 102, "count": 1}
|
||||
},
|
||||
{
|
||||
"name": "Algue cuisinee",
|
||||
"inputs": [{"item_id": 6, "count": 2}],
|
||||
"output": {"item_id": 103, "count": 2}
|
||||
},
|
||||
{
|
||||
"name": "Armure ecailles",
|
||||
"inputs": [{"item_id": 4, "count": 4}, {"item_id": 7, "count": 2}],
|
||||
"output": {"item_id": 104, "count": 1}
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
static func find_available_recipes(inventory: Inventory) -> Array:
|
||||
var available: Array = []
|
||||
for i: int in range(RECIPES.size()):
|
||||
if inventory.has_items(RECIPES[i]["inputs"]):
|
||||
available.append(i)
|
||||
return available
|
||||
|
||||
|
||||
static func craft(inventory: Inventory, recipe_index: int) -> bool:
|
||||
if recipe_index < 0 or recipe_index >= RECIPES.size():
|
||||
return false
|
||||
var recipe: Dictionary = RECIPES[recipe_index]
|
||||
if not inventory.consume_items(recipe["inputs"]):
|
||||
return false
|
||||
var leftover: int = inventory.add_item(recipe["output"]["item_id"], recipe["output"]["count"])
|
||||
if leftover > 0:
|
||||
push_warning("CraftingRecipes: craft overflow — %d items lost" % leftover)
|
||||
return true
|
||||
93
scripts/inventory/Inventory.gd
Normal file
93
scripts/inventory/Inventory.gd
Normal file
@@ -0,0 +1,93 @@
|
||||
class_name Inventory
|
||||
extends RefCounted
|
||||
|
||||
const MAX_STACK: int = 64
|
||||
const TOTAL_SLOTS: int = 36
|
||||
const HOTBAR_SIZE: int = 9
|
||||
|
||||
signal inventory_changed
|
||||
|
||||
var slots: Array = []
|
||||
var selected_hotbar: int = 0
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
slots.resize(TOTAL_SLOTS)
|
||||
for i: int in range(TOTAL_SLOTS):
|
||||
slots[i] = null
|
||||
|
||||
|
||||
func add_item(item_id: int, count: int = 1) -> int:
|
||||
var remaining: int = count
|
||||
# First pass: stack into existing slots
|
||||
for i: int in range(TOTAL_SLOTS):
|
||||
if remaining <= 0:
|
||||
break
|
||||
if slots[i] != null and slots[i]["item_id"] == item_id:
|
||||
var space: int = MAX_STACK - slots[i]["count"]
|
||||
if space > 0:
|
||||
var add_amount: int = mini(space, remaining)
|
||||
slots[i]["count"] += add_amount
|
||||
remaining -= add_amount
|
||||
# Second pass: fill empty slots
|
||||
for i: int in range(TOTAL_SLOTS):
|
||||
if remaining <= 0:
|
||||
break
|
||||
if slots[i] == null:
|
||||
var add_amount: int = mini(MAX_STACK, remaining)
|
||||
slots[i] = {"item_id": item_id, "count": add_amount}
|
||||
remaining -= add_amount
|
||||
inventory_changed.emit()
|
||||
return remaining
|
||||
|
||||
|
||||
func remove_item_from_slot(slot_index: int, count: int = 1) -> bool:
|
||||
if slot_index < 0 or slot_index >= TOTAL_SLOTS:
|
||||
return false
|
||||
if slots[slot_index] == null:
|
||||
return false
|
||||
if slots[slot_index]["count"] < count:
|
||||
return false
|
||||
slots[slot_index]["count"] -= count
|
||||
if slots[slot_index]["count"] <= 0:
|
||||
slots[slot_index] = null
|
||||
inventory_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func get_selected_item() -> Variant:
|
||||
return slots[selected_hotbar]
|
||||
|
||||
|
||||
func scroll_hotbar(direction: int) -> void:
|
||||
selected_hotbar = (selected_hotbar + direction + HOTBAR_SIZE) % HOTBAR_SIZE
|
||||
inventory_changed.emit()
|
||||
|
||||
|
||||
func has_items(requirements: Array) -> bool:
|
||||
for req: Dictionary in requirements:
|
||||
var needed: int = req["count"]
|
||||
for i: int in range(TOTAL_SLOTS):
|
||||
if slots[i] != null and slots[i]["item_id"] == req["item_id"]:
|
||||
needed -= slots[i]["count"]
|
||||
if needed > 0:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func consume_items(requirements: Array) -> bool:
|
||||
if not has_items(requirements):
|
||||
return false
|
||||
for req: Dictionary in requirements:
|
||||
var to_consume: int = req["count"]
|
||||
for i: int in range(TOTAL_SLOTS):
|
||||
if to_consume <= 0:
|
||||
break
|
||||
if slots[i] != null and slots[i]["item_id"] == req["item_id"]:
|
||||
var take: int = mini(slots[i]["count"], to_consume)
|
||||
slots[i]["count"] -= take
|
||||
to_consume -= take
|
||||
if slots[i]["count"] <= 0:
|
||||
slots[i] = null
|
||||
inventory_changed.emit()
|
||||
return true
|
||||
279
scripts/inventory/InventoryUI.gd
Normal file
279
scripts/inventory/InventoryUI.gd
Normal 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
|
||||
65
scripts/inventory/ItemDatabase.gd
Normal file
65
scripts/inventory/ItemDatabase.gd
Normal file
@@ -0,0 +1,65 @@
|
||||
extends Node
|
||||
|
||||
const ITEM_NAMES: Dictionary = {
|
||||
0: "Air",
|
||||
1: "Water",
|
||||
2: "Sand",
|
||||
3: "Rock",
|
||||
4: "Coral Rouge",
|
||||
5: "Coral Bleu",
|
||||
6: "Kelp",
|
||||
7: "Epave",
|
||||
8: "Glace",
|
||||
9: "Bedrock",
|
||||
100: "Lampe Bio",
|
||||
101: "Harpon",
|
||||
102: "Bulle d'air",
|
||||
103: "Algue cuisinee",
|
||||
104: "Armure ecailles",
|
||||
}
|
||||
|
||||
const ITEM_COLORS: Dictionary = {
|
||||
0: Color(0, 0, 0, 0),
|
||||
1: Color(0.1, 0.3, 0.8, 0.5),
|
||||
2: Color(0.76, 0.70, 0.50),
|
||||
3: Color(0.45, 0.45, 0.45),
|
||||
4: Color(0.9, 0.2, 0.2),
|
||||
5: Color(0.2, 0.4, 0.9),
|
||||
6: Color(0.1, 0.6, 0.1),
|
||||
7: Color(0.55, 0.35, 0.15),
|
||||
8: Color(0.7, 0.9, 1.0),
|
||||
9: Color(0.15, 0.15, 0.15),
|
||||
100: Color(1.0, 0.95, 0.2),
|
||||
101: Color(0.6, 0.6, 0.6),
|
||||
102: Color(0.2, 0.9, 0.9),
|
||||
103: Color(0.05, 0.4, 0.05),
|
||||
104: Color(0.1, 0.55, 0.5),
|
||||
}
|
||||
|
||||
const PLACEABLE_IDS: Array = [2, 3, 4, 5, 6, 7, 8]
|
||||
const CONSUMABLE_IDS: Array = [102, 103]
|
||||
const TOOL_IDS: Array = [100, 101, 104]
|
||||
|
||||
|
||||
func get_item_name(id: int) -> String:
|
||||
if ITEM_NAMES.has(id):
|
||||
return ITEM_NAMES[id]
|
||||
return "Inconnu"
|
||||
|
||||
|
||||
func get_item_color(id: int) -> Color:
|
||||
if ITEM_COLORS.has(id):
|
||||
return ITEM_COLORS[id]
|
||||
return Color(0.5, 0.5, 0.5)
|
||||
|
||||
|
||||
func is_placeable(id: int) -> bool:
|
||||
return id in PLACEABLE_IDS
|
||||
|
||||
|
||||
func is_consumable(id: int) -> bool:
|
||||
return id in CONSUMABLE_IDS
|
||||
|
||||
|
||||
func is_tool(id: int) -> bool:
|
||||
return id in TOOL_IDS
|
||||
Reference in New Issue
Block a user