using System.Collections.Generic; using UnityEngine; /// /// Inventory system with a toggleable HUD (press I) and right-click context menu. /// Attach to the Player GameObject alongside WeaponManager. /// public class Inventory : MonoBehaviour { // ─── Item data ──────────────────────────────────────────────────── [System.Serializable] public class InventoryEntry { public ItemDefinition definition; public string itemName; public Sprite icon; public int count; public bool isEquipped; public InventoryEntry(string name, Sprite icon) { this.itemName = name; this.icon = icon; this.count = 1; this.isEquipped = false; } public InventoryEntry(ItemDefinition def) { this.definition = def; this.itemName = def.itemName; this.icon = def.icon; this.count = 1; this.isEquipped = false; } public bool _isWeaponSlot; public bool IsWeapon => (definition != null && definition.type == ItemDefinition.ItemType.Weapon) || _isWeaponSlot; // True for weapons AND equippable misc items public bool IsEquippable => IsWeapon || (definition != null && definition.isEquippable); public string DisplayName => definition != null ? definition.itemName : itemName; } public List items = new List(); // ─── HUD config ─────────────────────────────────────────────────── [Header("HUD")] public KeyCode toggleKey = KeyCode.I; public KeyCode equipKey = KeyCode.F; public int columns = 4; public int maxSlots = 20; [Header("HUD Colours")] public Color colBackground = new Color(0.05f, 0.05f, 0.05f, 0.92f); public Color colPanel = new Color(0.10f, 0.10f, 0.10f, 0.95f); public Color colSlotEmpty = new Color(0.18f, 0.18f, 0.18f, 1.00f); public Color colSlotFilled = new Color(0.22f, 0.28f, 0.22f, 1.00f); public Color colSlotHover = new Color(0.35f, 0.45f, 0.35f, 1.00f); public Color colSlotSelected = new Color(0.45f, 0.75f, 0.45f, 1.00f); public Color colSlotEquipped = new Color(0.60f, 0.40f, 0.10f, 1.00f); public Color colBorder = new Color(0.40f, 0.65f, 0.40f, 1.00f); public Color colText = new Color(0.85f, 0.95f, 0.85f, 1.00f); public Color colDim = new Color(0.50f, 0.60f, 0.50f, 1.00f); public Color colAccent = new Color(0.50f, 0.90f, 0.50f, 1.00f); public Color colWeaponAccent = new Color(0.95f, 0.75f, 0.20f, 1.00f); [Header("Context Menu Colours")] public Color colCtxBg = new Color(0.08f, 0.10f, 0.08f, 0.98f); public Color colCtxBorder = new Color(0.40f, 0.65f, 0.40f, 1.00f); public Color colCtxHover = new Color(0.20f, 0.35f, 0.20f, 1.00f); public Color colCtxText = new Color(0.85f, 0.95f, 0.85f, 1.00f); public Color colCtxDestructive = new Color(0.85f, 0.25f, 0.20f, 1.00f); // ─── Private state ──────────────────────────────────────────────── private bool _open = false; private int _selectedIndex = 0; private int _hoveredIndex = -1; private WeaponManager _weaponManager; // ─── Context menu state ─────────────────────────────────────────── private bool _ctxOpen = false; private int _ctxItemIndex = -1; private Vector2 _ctxPos; private const float kCtxItemH = 28f; private const float kCtxWidth = 140f; private const float kCtxPadX = 10f; // Context menu action labels — built per item private struct CtxAction { public string label; public bool isDestructive; public System.Action callback; } private List _ctxActions = new List(); // ─── Layout constants ───────────────────────────────────────────── private const float kSlotSize = 80f; private const float kSlotPad = 8f; private const float kPanelPad = 20f; private const float kHeaderH = 40f; private const float kDetailPanelH = 120f; // ───────────────────────────────────────────────────────────────── void Start() { _weaponManager = GetComponent(); if (_weaponManager != null) Invoke(nameof(SyncStartingWeapons), 0.1f); } void SyncStartingWeapons() { if (_weaponManager == null) return; foreach (var slot in _weaponManager.slots) { if (slot.definition != null) AddItem(slot.definition, autoEquip: false); else AddItemRaw(slot.itemName, null, isWeaponSlot: true); } SyncEquippedState(); } // ─── Public API ─────────────────────────────────────────────────── public void AddItem(ItemDefinition def, bool autoEquip = false) { if (def == null) return; InventoryEntry existing = items.Find(e => e.definition == def); if (existing != null) { if (def.type != ItemDefinition.ItemType.Weapon) existing.count++; Debug.Log($"[Inventory] Already have: {def.itemName}"); } else { var entry = new InventoryEntry(def); items.Add(entry); Debug.Log($"[Inventory] Picked up: {def.itemName}"); if (def.type == ItemDefinition.ItemType.Weapon) { _weaponManager?.AddWeapon(def); if (autoEquip) EquipItem(items.Count - 1); } } } public void AddItem(string itemName, Sprite icon = null) => AddItemRaw(itemName, icon, false); private void AddItemRaw(string itemName, Sprite icon, bool isWeaponSlot) { InventoryEntry existing = items.Find(e => e.itemName == itemName && e.definition == null); if (existing != null) { existing.count++; return; } var entry = new InventoryEntry(itemName, icon); entry._isWeaponSlot = isWeaponSlot; items.Add(entry); } public bool RemoveItem(string itemName, int amount = 1) { InventoryEntry entry = items.Find(e => e.DisplayName == itemName); if (entry == null) return false; entry.count -= amount; if (entry.count <= 0) items.Remove(entry); return true; } public bool HasItem(string itemName) => items.Exists(e => e.DisplayName == itemName); public int CountOf(string itemName) { var e = items.Find(x => x.DisplayName == itemName); return e != null ? e.count : 0; } public bool IsOpen => _open; // ─── Equip / Unequip ────────────────────────────────────────────── void EquipItem(int index) { if (index < 0 || index >= items.Count) return; InventoryEntry entry = items[index]; if (!entry.IsEquippable) return; if (entry.IsWeapon) { // Unequip other weapons foreach (var e in items) { if (e.IsWeapon) e.isEquipped = false; } entry.isEquipped = true; _weaponManager?.EquipByName(entry.DisplayName); } else { // Toggle equip for misc equippables (boots, etc.) entry.isEquipped = true; } Debug.Log($"[Inventory] Equipped: {entry.DisplayName}"); } void UnequipItem(int index) { if (index < 0 || index >= items.Count) return; InventoryEntry entry = items[index]; if (!entry.IsEquippable || !entry.isEquipped) return; entry.isEquipped = false; if (entry.IsWeapon && _weaponManager != null) { var slot = _weaponManager.slots.Find(s => s.itemName == entry.DisplayName); if (slot != null) slot.instance.SetActive(false); _weaponManager.Unequip(); } Debug.Log($"[Inventory] Unequipped: {entry.DisplayName}"); } void DropItem(int index) { if (index < 0 || index >= items.Count) return; InventoryEntry entry = items[index]; if (entry.isEquipped) UnequipItem(index); items.RemoveAt(index); if (_selectedIndex >= items.Count) _selectedIndex = items.Count - 1; Debug.Log($"[Inventory] Dropped: {entry.DisplayName}"); // TODO: optionally spawn the pickup back in the world here } void SyncEquippedState() { if (_weaponManager == null) return; string active = _weaponManager.ActiveWeaponName; foreach (var e in items) { // Only sync weapon equipped state — leave misc equippables alone if (e.IsWeapon) e.isEquipped = (e.DisplayName == active); } } // ─── Context menu builder ───────────────────────────────────────── void OpenContextMenu(int itemIndex, Vector2 screenPos) { if (itemIndex < 0 || itemIndex >= items.Count) return; _ctxItemIndex = itemIndex; _ctxActions.Clear(); InventoryEntry entry = items[itemIndex]; if (entry.IsEquippable) { if (entry.isEquipped) { int captured = itemIndex; _ctxActions.Add(new CtxAction { label = "Unequip", isDestructive = false, callback = () => UnequipItem(captured) }); } else { int captured = itemIndex; _ctxActions.Add(new CtxAction { label = "Equip", isDestructive = false, callback = () => EquipItem(captured) }); } } // Consumables / misc could have a "Use" action here in future // For now everyone gets Inspect (just selects and focuses the detail panel) { int captured = itemIndex; _ctxActions.Add(new CtxAction { label = "Inspect", isDestructive = false, callback = () => _selectedIndex = captured }); } { int captured = itemIndex; _ctxActions.Add(new CtxAction { label = "Drop", isDestructive = true, callback = () => DropItem(captured) }); } // Clamp so the menu doesn't go off screen float menuH = _ctxActions.Count * kCtxItemH + 8f; float clampedX = Mathf.Min(screenPos.x, Screen.width - kCtxWidth - 4f); float clampedY = Mathf.Min(screenPos.y, Screen.height - menuH - 4f); _ctxPos = new Vector2(clampedX, clampedY); _ctxOpen = true; } void CloseContextMenu() { _ctxOpen = false; _ctxItemIndex = -1; } // ─── Toggle ─────────────────────────────────────────────────────── void Update() { if (Input.GetKeyDown(toggleKey)) { CloseContextMenu(); SetOpen(!_open); } if (_open && Input.GetKeyDown(equipKey)) { if (_selectedIndex < items.Count && items[_selectedIndex].IsWeapon) EquipItem(_selectedIndex); CloseContextMenu(); } if (!_open) SyncEquippedState(); } void SetOpen(bool open) { _open = open; SyncEquippedState(); Cursor.lockState = open ? CursorLockMode.None : CursorLockMode.Locked; Cursor.visible = open; } // ─── GUI ────────────────────────────────────────────────────────── void OnGUI() { if (!_open) return; int rows = Mathf.CeilToInt((float)maxSlots / columns); float gridW = columns * (kSlotSize + kSlotPad) - kSlotPad; float gridH = rows * (kSlotSize + kSlotPad) - kSlotPad; float panelW = gridW + kPanelPad * 2f; float panelH = kHeaderH + kPanelPad + gridH + kPanelPad + kDetailPanelH + kPanelPad; float panelX = (Screen.width - panelW) * 0.5f; float panelY = (Screen.height - panelH) * 0.5f; Vector2 mouse = Event.current.mousePosition; // ── Click outside context menu to close it ──────────────────── if (_ctxOpen && Event.current.type == EventType.MouseDown) { Rect ctxRect = GetCtxRect(); if (!ctxRect.Contains(mouse)) CloseContextMenu(); } // ── Backdrop ────────────────────────────────────────────────── DrawRect(new Rect(0, 0, Screen.width, Screen.height), new Color(0, 0, 0, 0.55f)); // ── Main panel ──────────────────────────────────────────────── DrawBorderedBox(new Rect(panelX, panelY, panelW, panelH), colPanel, colBorder, 2f); GUIStyle headerStyle = MakeStyle(18, FontStyle.Bold, colAccent, TextAnchor.MiddleCenter); GUI.Label(new Rect(panelX, panelY + 8f, panelW, 28f), "[ INVENTORY ]", headerStyle); DrawRect(new Rect(panelX + 10f, panelY + kHeaderH - 2f, panelW - 20f, 1f), colBorder); if (_weaponManager != null && _weaponManager.ActiveWeaponName != "") { GUIStyle activeStyle = MakeStyle(11, FontStyle.Normal, colWeaponAccent, TextAnchor.MiddleRight); GUI.Label(new Rect(panelX, panelY + 10f, panelW - 10f, 20f), $"EQUIPPED: {_weaponManager.ActiveWeaponName}", activeStyle); } float gx = panelX + kPanelPad; float gy = panelY + kHeaderH + kPanelPad; _hoveredIndex = -1; // ── Slot grid ───────────────────────────────────────────────── for (int i = 0; i < maxSlots; i++) { int col = i % columns; int row = i / columns; float sx = gx + col * (kSlotSize + kSlotPad); float sy = gy + row * (kSlotSize + kSlotPad); Rect slotRect = new Rect(sx, sy, kSlotSize, kSlotSize); bool hasItem = i < items.Count; bool isHover = slotRect.Contains(mouse) && !_ctxOpen; bool isSel = (i == _selectedIndex); bool isEquipped = hasItem && items[i].isEquipped; bool isWeapon = hasItem && items[i].IsWeapon; if (isHover) _hoveredIndex = i; // Left-click: select if (isHover && Event.current.type == EventType.MouseDown && Event.current.button == 0 && hasItem) { _selectedIndex = i; CloseContextMenu(); } // Left double-click: equip weapon if (isHover && Event.current.type == EventType.MouseDown && Event.current.button == 0 && Event.current.clickCount == 2 && isWeapon) EquipItem(i); // Right-click: open context menu if (isHover && Event.current.type == EventType.MouseDown && Event.current.button == 1 && hasItem) { _selectedIndex = i; OpenContextMenu(i, mouse); } // Slot colour Color slotCol; if (isEquipped) slotCol = colSlotEquipped; else if (isSel) slotCol = colSlotSelected; else if (isHover) slotCol = colSlotHover; else if (hasItem) slotCol = colSlotFilled; else slotCol = colSlotEmpty; Color borderCol = isEquipped ? colWeaponAccent : (isSel ? colAccent : colBorder); DrawBorderedBox(slotRect, slotCol, borderCol, (isSel || isEquipped) ? 2f : 1f); if (!hasItem) continue; InventoryEntry entry = items[i]; // Icon or letter placeholder if (entry.icon != null) { float ip = 10f; GUI.DrawTexture(new Rect(sx + ip, sy + ip, kSlotSize - ip * 2f, kSlotSize - 28f), entry.icon.texture, ScaleMode.ScaleToFit, true); } else { Color lc = isEquipped ? colWeaponAccent : (isSel ? Color.white : colAccent); GUI.Label(new Rect(sx, sy + 8f, kSlotSize, kSlotSize - 24f), entry.DisplayName.Substring(0, 1).ToUpper(), MakeStyle(26, FontStyle.Bold, lc, TextAnchor.MiddleCenter)); } // Weapon tag if (isWeapon) { Color tc = isEquipped ? colWeaponAccent : new Color(0.6f, 0.5f, 0.2f, 1f); GUI.Label(new Rect(sx + 3f, sy + 2f, 40f, 12f), isEquipped ? "EQUIP" : "WPN", MakeStyle(8, FontStyle.Bold, tc, TextAnchor.UpperLeft)); } // Right-click hint on hovered slot if (isHover) { GUIStyle hint = MakeStyle(8, FontStyle.Normal, new Color(0.6f, 0.6f, 0.6f, 0.9f), TextAnchor.LowerRight); GUI.Label(new Rect(sx + 2f, sy + 2f, kSlotSize - 4f, kSlotSize - 4f), "RMB ▾", hint); } // Item name string dn = entry.DisplayName.Length > 10 ? entry.DisplayName.Substring(0, 9) + "…" : entry.DisplayName; Color nc = isEquipped ? colWeaponAccent : (isSel ? Color.white : colText); GUI.Label(new Rect(sx + 2f, sy + kSlotSize - 28f, kSlotSize - 4f, 16f), dn, MakeStyle(9, FontStyle.Bold, nc, TextAnchor.LowerCenter)); // Count badge if (entry.count > 1) { DrawRect(new Rect(sx + kSlotSize - 22f, sy + kSlotSize - 18f, 20f, 16f), new Color(0, 0, 0, 0.7f)); GUI.Label(new Rect(sx + kSlotSize - 22f, sy + kSlotSize - 18f, 19f, 16f), $"x{entry.count}", MakeStyle(10, FontStyle.Bold, Color.white, TextAnchor.LowerRight)); } } // ── Detail panel ────────────────────────────────────────────── float detailY = gy + gridH + kPanelPad; Rect detailRect = new Rect(panelX + kPanelPad, detailY, panelW - kPanelPad * 2f, kDetailPanelH - kPanelPad); DrawBorderedBox(detailRect, new Color(0.08f, 0.08f, 0.08f, 1f), colBorder, 1f); if (_selectedIndex < items.Count) { InventoryEntry sel = items[_selectedIndex]; float dy = detailRect.y + 8f; float dx = detailRect.x + 10f; float dw = detailRect.width - 20f; GUI.Label(new Rect(dx, dy, dw, 16f), "SELECTED", MakeStyle(10, FontStyle.Normal, colDim, TextAnchor.UpperLeft)); GUI.Label(new Rect(dx, dy + 16f, dw, 22f), sel.DisplayName, MakeStyle(14, FontStyle.Bold, colText, TextAnchor.UpperLeft)); string desc = (sel.definition != null && sel.definition.description != "") ? sel.definition.description : (sel.IsWeapon ? "Weapon" : "Item"); GUI.Label(new Rect(dx, dy + 38f, dw * 0.65f, 30f), desc, MakeStyle(10, FontStyle.Normal, colDim, TextAnchor.UpperLeft)); if (sel.IsEquippable) { Color wc = sel.isEquipped ? colWeaponAccent : colDim; string tag = sel.isEquipped ? "▶ EQUIPPED" : "right-click or [F] to equip"; GUI.Label(new Rect(dx, dy + 68f, dw, 18f), tag, MakeStyle(11, FontStyle.Bold, wc, TextAnchor.UpperLeft)); } else if (sel.count > 1) { GUI.Label(new Rect(dx, dy + 68f, dw, 18f), $"Quantity: {sel.count}", MakeStyle(11, FontStyle.Bold, colAccent, TextAnchor.UpperLeft)); } // Equip / Unequip button bottom-right of detail panel if (sel.IsEquippable) { Rect btnRect = new Rect(detailRect.x + detailRect.width - 110f, detailRect.y + detailRect.height - 34f, 100f, 26f); if (sel.isEquipped) { DrawBorderedBox(btnRect, new Color(0.4f, 0.15f, 0.05f, 1f), colWeaponAccent, 1f); GUI.Label(btnRect, "[ F ] UNEQUIP", MakeStyle(11, FontStyle.Bold, Color.white, TextAnchor.MiddleCenter)); if (GUI.Button(btnRect, "", GUIStyle.none)) UnequipItem(_selectedIndex); } else { DrawBorderedBox(btnRect, colSlotEquipped, colWeaponAccent, 1f); GUI.Label(btnRect, "[ F ] EQUIP", MakeStyle(11, FontStyle.Bold, Color.white, TextAnchor.MiddleCenter)); if (GUI.Button(btnRect, "", GUIStyle.none)) EquipItem(_selectedIndex); } } } else { GUI.Label(detailRect, "No item selected", MakeStyle(11, FontStyle.Normal, colDim, TextAnchor.MiddleCenter)); } // Hints GUIStyle hintStyle = MakeStyle(10, FontStyle.Normal, colDim, TextAnchor.LowerRight); GUI.Label(new Rect(panelX, panelY + panelH - 18f, panelW - 8f, 16f), "[ I ] close | RMB slot for options", hintStyle); // ── Context menu (drawn last so it's on top) ────────────────── if (_ctxOpen) DrawContextMenu(mouse); } // ─── Context menu draw ──────────────────────────────────────────── Rect GetCtxRect() { float menuH = _ctxActions.Count * kCtxItemH + 8f; return new Rect(_ctxPos.x, _ctxPos.y, kCtxWidth, menuH); } void DrawContextMenu(Vector2 mouse) { float menuH = _ctxActions.Count * kCtxItemH + 8f; Rect bgRect = new Rect(_ctxPos.x, _ctxPos.y, kCtxWidth, menuH); // Shadow DrawRect(new Rect(bgRect.x + 3f, bgRect.y + 3f, bgRect.width, bgRect.height), new Color(0f, 0f, 0f, 0.45f)); // Background + border DrawBorderedBox(bgRect, colCtxBg, colCtxBorder, 1.5f); // Header strip — item name if (_ctxItemIndex >= 0 && _ctxItemIndex < items.Count) { string title = items[_ctxItemIndex].DisplayName; DrawRect(new Rect(bgRect.x + 1.5f, bgRect.y + 1.5f, bgRect.width - 3f, 20f), new Color(0.15f, 0.22f, 0.15f, 1f)); GUI.Label(new Rect(bgRect.x + kCtxPadX, bgRect.y + 3f, kCtxWidth - kCtxPadX * 2f, 16f), title.ToUpper(), MakeStyle(9, FontStyle.Bold, colWeaponAccent, TextAnchor.MiddleLeft)); } // Action rows for (int i = 0; i < _ctxActions.Count; i++) { float rowY = bgRect.y + 22f + i * kCtxItemH; Rect rowRect = new Rect(bgRect.x + 1.5f, rowY, bgRect.width - 3f, kCtxItemH); bool hover = rowRect.Contains(mouse); if (hover) DrawRect(rowRect, colCtxHover); Color textColor = _ctxActions[i].isDestructive ? (hover ? Color.white : colCtxDestructive) : (hover ? Color.white : colCtxText); GUI.Label(new Rect(bgRect.x + kCtxPadX, rowY + 2f, kCtxWidth - kCtxPadX * 2f, kCtxItemH - 4f), _ctxActions[i].label, MakeStyle(12, FontStyle.Normal, textColor, TextAnchor.MiddleLeft)); // Separator line between items if (i < _ctxActions.Count - 1) DrawRect(new Rect(bgRect.x + 6f, rowY + kCtxItemH - 1f, bgRect.width - 12f, 1f), new Color(1f, 1f, 1f, 0.06f)); // Click to fire action if (hover && Event.current.type == EventType.MouseDown && Event.current.button == 0) { _ctxActions[i].callback?.Invoke(); CloseContextMenu(); Event.current.Use(); } } } // ─── Style / Draw helpers ───────────────────────────────────────── static GUIStyle MakeStyle(int size, FontStyle style, Color color, TextAnchor align) { var s = new GUIStyle(); s.fontSize = size; s.fontStyle = style; s.normal.textColor = color; s.alignment = align; return s; } static void DrawRect(Rect r, Color c) { Color prev = GUI.color; GUI.color = c; GUI.DrawTexture(r, Texture2D.whiteTexture); GUI.color = prev; } static void DrawBorderedBox(Rect r, Color fill, Color border, float thickness) { DrawRect(r, border); DrawRect(new Rect(r.x + thickness, r.y + thickness, r.width - thickness * 2f, r.height - thickness * 2f), fill); } }