feat: add inventory system and item pickups
This commit is contained in:
131
Assets/Scripts/Items/Inventory.cs
Normal file
131
Assets/Scripts/Items/Inventory.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Assets/Scripts/Items/ItemDefinition.cs
Normal file
38
Assets/Scripts/Items/ItemDefinition.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
Assets/Scripts/Items/ItemPickup.cs
Normal file
52
Assets/Scripts/Items/ItemPickup.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
107
Assets/Scripts/Life/CritterInteraction.cs
Normal file
107
Assets/Scripts/Life/CritterInteraction.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user