Files
OGG/Assets/Scripts/Inventory.cs

633 lines
26 KiB
C#

using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Inventory system with a toggleable HUD (press I) and right-click context menu.
/// Attach to the Player GameObject alongside WeaponManager.
/// </summary>
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<InventoryEntry> items = new List<InventoryEntry>();
// ─── 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<CtxAction> _ctxActions = new List<CtxAction>();
// ─── 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<WeaponManager>();
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);
}
}