feat: add inventory system and item pickups

This commit is contained in:
Alexa Amundson
2025-11-25 14:52:01 -06:00
parent ffc2842bd0
commit a5f61489de
4 changed files with 328 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace BlackRoad.Worldbuilder.Items
{
/// <summary>
/// Simple stack-based inventory for the player.
/// </summary>
public class Inventory : MonoBehaviour
{
[Serializable]
public class Slot
{
public ItemDefinition item;
public int count;
}
[Header("Slots")]
[SerializeField] private int maxSlots = 16;
[SerializeField] private List<Slot> slots = new List<Slot>();
public int MaxSlots => maxSlots;
public IReadOnlyList<Slot> Slots => slots;
private void Awake()
{
// Ensure we have a fixed size list
while (slots.Count < maxSlots)
{
slots.Add(new Slot());
}
}
/// <summary>
/// Try to add a quantity of an item. Returns how many were actually added.
/// </summary>
public int AddItem(ItemDefinition item, int amount)
{
if (item == null || amount <= 0) return 0;
int remaining = amount;
// 1) Fill existing stacks
for (int i = 0; i < slots.Count && remaining > 0; i++)
{
var slot = slots[i];
if (slot.item == item && slot.count < item.MaxStack)
{
int space = item.MaxStack - slot.count;
int add = Mathf.Min(space, remaining);
slot.count += add;
remaining -= add;
}
}
// 2) Use empty slots
for (int i = 0; i < slots.Count && remaining > 0; i++)
{
var slot = slots[i];
if (slot.item == null || slot.count <= 0)
{
int add = Mathf.Min(item.MaxStack, remaining);
slot.item = item;
slot.count = add;
remaining -= add;
}
}
int added = amount - remaining;
return added;
}
/// <summary>
/// Removes up to `amount` items; returns actual removed count.
/// </summary>
public int RemoveItem(ItemDefinition item, int amount)
{
if (item == null || amount <= 0) return 0;
int remaining = amount;
for (int i = 0; i < slots.Count && remaining > 0; i++)
{
var slot = slots[i];
if (slot.item == item && slot.count > 0)
{
int remove = Mathf.Min(slot.count, remaining);
slot.count -= remove;
remaining -= remove;
if (slot.count <= 0)
{
slot.item = null;
slot.count = 0;
}
}
}
return amount - remaining;
}
public int GetCount(ItemDefinition item)
{
if (item == null) return 0;
int total = 0;
foreach (var slot in slots)
{
if (slot.item == item)
total += slot.count;
}
return total;
}
/// <summary>
/// Finds any food item with non-zero count, returns it or null.
/// </summary>
public ItemDefinition GetAnyFoodItem()
{
foreach (var slot in slots)
{
if (slot.item != null && slot.item.IsFood && slot.count > 0)
return slot.item;
}
return null;
}
}
}

View File

@@ -0,0 +1,38 @@
using UnityEngine;
namespace BlackRoad.Worldbuilder.Items
{
/// <summary>
/// ScriptableObject describing a generic item type.
/// </summary>
[CreateAssetMenu(
fileName = "ItemDefinition",
menuName = "BlackRoad/Worldbuilder/ItemDefinition",
order = 0)]
public class ItemDefinition : ScriptableObject
{
[Header("Identity")]
[SerializeField] private string itemId = "item.berry";
[SerializeField] private string displayName = "Berry";
[Header("Visual")]
[SerializeField] private Sprite icon;
[SerializeField] private Color iconTint = Color.white;
[Header("Stacking")]
[SerializeField] private int maxStack = 99;
[Header("Tags")]
[Tooltip("True if this item can be used as critter food.")]
[SerializeField] private bool isFood = false;
[SerializeField] private float nutritionValue = 0.25f; // how much hunger to reduce
public string ItemId => itemId;
public string DisplayName => displayName;
public Sprite Icon => icon;
public Color IconTint => iconTint;
public int MaxStack => maxStack;
public bool IsFood => isFood;
public float NutritionValue => nutritionValue;
}
}

View File

@@ -0,0 +1,52 @@
using UnityEngine;
using BlackRoad.Worldbuilder.Interaction;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace BlackRoad.Worldbuilder.Items
{
/// <summary>
/// World object that can be picked up as an item and added to the player's inventory.
/// </summary>
public class ItemPickup : Interactable
{
[Header("Item")]
[SerializeField] private ItemDefinition item;
[SerializeField] private int amount = 1;
public override void Interact(GameObject interactor)
{
if (item == null) return;
var inventory = interactor.GetComponent<Inventory>();
if (inventory == null)
{
Debug.LogWarning("[ItemPickup] Interactor has no Inventory.");
return;
}
int added = inventory.AddItem(item, amount);
if (added > 0)
{
// Could play sound, VFX, etc. For now, just destroy.
Destroy(gameObject);
}
}
#if UNITY_EDITOR
private void OnValidate()
{
// Set display name/verb for Interactable UI
if (item != null)
{
var so = new SerializedObject(this);
so.FindProperty("displayName").stringValue = item.DisplayName;
so.FindProperty("verb").stringValue = "Pick up";
so.ApplyModifiedPropertiesWithoutUndo();
}
}
#endif
}
}

View File

@@ -0,0 +1,107 @@
using UnityEngine;
using BlackRoad.Worldbuilder.Interaction;
using BlackRoad.Worldbuilder.Items;
namespace BlackRoad.Worldbuilder.Life
{
/// <summary>
/// Allows the player to feed and "pet" critters.
/// Feeding uses items from the player's Inventory if available.
/// Petting increases trust only.
/// </summary>
public class CritterInteraction : Interactable
{
[Header("Needs & Behaviour")]
[SerializeField] private CritterNeeds needs;
[SerializeField] private CritterAgent agent;
[Header("Trust")]
[Range(0f, 1f)]
[SerializeField] private float trust = 0f;
[SerializeField] private float trustIncreaseOnPet = 0.1f;
[SerializeField] private float trustIncreaseOnFeed = 0.15f;
[Header("Visual Feedback")]
[SerializeField] private Color feedColor = Color.green;
[SerializeField] private Color petColor = Color.cyan;
public float Trust => trust;
private void Awake()
{
if (needs == null)
needs = GetComponent<CritterNeeds>();
if (agent == null)
agent = GetComponent<CritterAgent>();
}
public override void Interact(GameObject interactor)
{
var inventory = interactor.GetComponent<Inventory>();
// If hungry and we have inventory & food, feed
if (needs != null && needs.IsHungry && inventory != null)
{
var foodItem = inventory.GetAnyFoodItem();
if (foodItem != null && foodItem.IsFood)
{
int removed = inventory.RemoveItem(foodItem, 1);
if (removed > 0)
{
Feed(foodItem);
return;
}
}
}
// Otherwise just pet
Pet();
}
private void Feed(ItemDefinition item)
{
if (needs != null && item.IsFood)
{
// Use item nutrition to reduce hunger
float newHunger = Mathf.Clamp01(needs.Hunger - item.NutritionValue);
needs.SetHunger(newHunger);
}
trust = Mathf.Clamp01(trust + trustIncreaseOnFeed);
StartCoroutine(BobAnimation(feedColor));
}
private void Pet()
{
trust = Mathf.Clamp01(trust + trustIncreaseOnPet);
StartCoroutine(BobAnimation(petColor));
}
private System.Collections.IEnumerator BobAnimation(Color color)
{
Renderer rend = GetComponentInChildren<Renderer>();
Color original = rend != null ? rend.material.color : Color.white;
float t = 0f;
Vector3 basePos = transform.position;
while (t < 0.4f)
{
t += Time.deltaTime;
float bob = Mathf.Sin(t * Mathf.PI * 4f) * 0.05f;
transform.position = basePos + Vector3.up * bob;
if (rend != null)
{
rend.material.color = Color.Lerp(original, color, t / 0.4f);
}
yield return null;
}
transform.position = basePos;
if (rend != null)
rend.material.color = original;
}
}
}