extends Control ## D-065: Inventory grid — 3x3 slots, bottom-right, 40x40px icons. ## No empty slots displayed — icons appear only when items are carried. ## 1-9 hotkeys for direct slot access. Reads from GameState.player_inventory. ## Lives on UILayer (z-layer 7). signal item_selected(slot: int, item_id: int) const SLOT_SIZE := 40 const SLOT_GAP := 4 const GRID_COLS := 3 const GRID_ROWS := 3 const MAX_SLOTS := 9 const SLOT_BG := Color(0.1, 0.1, 0.14, 0.6) const SLOT_BORDER := Color(0.3, 0.3, 0.38, 0.8) const SLOT_ACTIVE := Color("#c8d0e0") const SLOT_TEXT := Color("#c8d0e0") const SLOT_TEXT_DIM := Color("#8b8ba0") const HOTKEY_SIZE := 10 var _slots: Array = [] # [{item_id, name, slot}] from GameState var _slot_nodes: Array[Control] = [] var _selected_slot: int = -1 func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE _build_grid() visible = false func _build_grid() -> void: for node in _slot_nodes: if is_instance_valid(node): node.queue_free() _slot_nodes.clear() var grid_w: float = GRID_COLS * SLOT_SIZE + (GRID_COLS - 1) * SLOT_GAP var grid_h: float = GRID_ROWS * SLOT_SIZE + (GRID_ROWS - 1) * SLOT_GAP # Position grid at bottom-right with margin custom_minimum_size = Vector2(grid_w, grid_h) size = Vector2(grid_w, grid_h) func update_from_state() -> void: _slots = GameState.player_inventory.duplicate() if _slots.is_empty(): visible = false return visible = true queue_redraw() func _draw() -> void: if _slots.is_empty(): return for item in _slots: var slot_idx: int = item.get("slot", 0) if slot_idx < 0 or slot_idx >= MAX_SLOTS: continue var col: int = slot_idx % GRID_COLS var row: int = slot_idx / GRID_COLS var pos := Vector2(col * (SLOT_SIZE + SLOT_GAP), row * (SLOT_SIZE + SLOT_GAP)) var rect := Rect2(pos, Vector2(SLOT_SIZE, SLOT_SIZE)) # Slot background draw_rect(rect, SLOT_BG) # Border draw_rect(rect, SLOT_BORDER, false, 1.0) # Selected highlight if slot_idx == _selected_slot: draw_rect(rect, SLOT_ACTIVE, false, 2.0) # Item name (truncated, centered) var item_name: String = item.get("name", "?") if item_name.length() > 5: item_name = item_name.substr(0, 4) + "." var font := ThemeDB.fallback_font var font_size := 11 var text_size := font.get_string_size(item_name, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size) var text_pos := ( pos + Vector2((SLOT_SIZE - text_size.x) / 2.0, SLOT_SIZE / 2.0 + text_size.y / 4.0) ) draw_string(font, text_pos, item_name, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, SLOT_TEXT) # Hotkey number (top-left corner) var hotkey := str(slot_idx + 1) draw_string( font, pos + Vector2(3, HOTKEY_SIZE + 2), hotkey, HORIZONTAL_ALIGNMENT_LEFT, -1, HOTKEY_SIZE, SLOT_TEXT_DIM ) func _unhandled_input(event: InputEvent) -> void: if not visible: return if event is InputEventKey and event.pressed and not event.echo: var key: int = event.physical_keycode # 1-9 hotkeys (Key_1 = 49, Key_9 = 57) if key >= KEY_1 and key <= KEY_9: var slot_idx: int = key - KEY_1 _select_slot(slot_idx) get_viewport().set_input_as_handled() func _select_slot(slot_idx: int) -> void: # Find item in this slot for item in _slots: if item.get("slot", -1) == slot_idx: _selected_slot = slot_idx item_selected.emit(slot_idx, item.get("item_id", -1)) queue_redraw() return # No item in slot — deselect _selected_slot = -1 queue_redraw() # -- Public API --------------------------------------------------------------- func get_slot_count() -> int: return _slots.size() func get_selected_slot() -> int: return _selected_slot func get_item_at_slot(slot_idx: int) -> Dictionary: for item in _slots: if item.get("slot", -1) == slot_idx: return item return {}