Files
MedievalShopkeeper/Assets/Scripts/CustomerNPC.cs
2026-09-03 16:35:05 +01:00

244 lines
7.8 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
namespace MedievalShopkeeper
{
public enum NPCState
{
Entering,
Browsing,
Queueing,
CheckingOut,
Leaving
}
[RequireComponent(typeof(NavMeshAgent))]
public class CustomerNPC : Interactable
{
[Header("NPC Data")]
public string npcName = "Peasant";
public float browseTimePerShelf = 2f;
private NPCState state;
private NavMeshAgent agent;
private List<ItemData> basket = new List<ItemData>();
private ShopTill targetTill;
public override string InteractionPrompt => $"Talk to {npcName}";
private void Start()
{
agent = GetComponent<NavMeshAgent>();
state = NPCState.Entering;
StartCoroutine(BrainRoutine());
}
private IEnumerator BrainRoutine()
{
state = NPCState.Browsing;
int itemsToBuy = Random.Range(1, 4);
for (int i = 0; i < itemsToBuy; i++)
{
// Find all shelves that ACTUALLY have items on them
Shelf[] allShelves = FindObjectsByType<Shelf>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
List<Shelf> stockedShelves = new List<Shelf>();
foreach (var shelf in allShelves)
{
if (shelf.HasStock) stockedShelves.Add(shelf);
}
if (stockedShelves.Count == 0)
{
Debug.Log($"[{npcName}] Looked around, but there are no shelves with stock right now.");
break;
}
Shelf targetShelf = stockedShelves[Random.Range(0, stockedShelves.Count)];
Debug.Log($"[{npcName}] Decided to check out {targetShelf.gameObject.name}...");
agent.SetDestination(targetShelf.InteractionTransform.position);
// Wait until we reach the shelf (or fail)
bool reached = false;
float timeout = 15f;
float timer = 0f;
while (timer < timeout)
{
if (!agent.pathPending)
{
if (agent.pathStatus == NavMeshPathStatus.PathComplete && agent.remainingDistance < 1.5f)
{
reached = true;
break;
}
if (agent.pathStatus == NavMeshPathStatus.PathPartial || agent.pathStatus == NavMeshPathStatus.PathInvalid)
{
Debug.LogWarning($"[{npcName}] Path to {targetShelf.gameObject.name} is completely blocked (Status: {agent.pathStatus}). Giving up on this shelf.");
break;
}
}
timer += Time.deltaTime;
yield return null;
}
if (reached)
{
Debug.Log($"[{npcName}] Successfully arrived at {targetShelf.gameObject.name}. Browsing...");
yield return new WaitForSeconds(browseTimePerShelf);
if (targetShelf.TryTakeItem(out ItemData item))
{
Debug.Log($"[{npcName}] Picked up 1x {item.itemName}!");
basket.Add(item);
}
else
{
Debug.Log($"[{npcName}] Bummer, someone took the last item before I could reach it.");
}
}
else
{
Debug.LogWarning($"[{npcName}] Timed out trying to reach {targetShelf.gameObject.name}. It might be physically unreachable.");
}
}
if (basket.Count > 0)
{
targetTill = FindFirstObjectByType<ShopTill>();
if (targetTill != null)
{
state = NPCState.Queueing;
targetTill.JoinQueue(this);
}
else
{
Debug.LogWarning($"[{npcName}] No ShopTill found! Dropping items and leaving.");
ReturnItemsAndLeave();
}
}
else
{
Debug.Log($"[{npcName}] Couldn't find what I wanted. Leaving.");
ReturnItemsAndLeave();
}
}
private void Update()
{
if (state == NPCState.Queueing && targetTill != null)
{
Vector3 qPos = targetTill.GetQueuePosition(this);
agent.SetDestination(qPos);
// Ensure they actually reached the queue spot and aren't stuck outside on a partial path
if (targetTill.IsFrontOfQueue(this) && !agent.pathPending && agent.pathStatus == NavMeshPathStatus.PathComplete && agent.remainingDistance < 1.5f)
{
state = NPCState.CheckingOut;
targetTill.PlaceItemsOnCounter(this, basket);
}
}
}
public List<ItemData> GetBasket()
{
return basket;
}
public void FinishCheckoutAndLeave()
{
basket.Clear();
LeaveShop();
}
public void KickOut()
{
Debug.Log($"[{npcName}] 'Fine, I'm leaving!'");
if (targetTill != null && (state == NPCState.Queueing || state == NPCState.CheckingOut))
{
targetTill.LeaveQueue(this);
}
ReturnItemsAndLeave();
}
private void ReturnItemsAndLeave()
{
if (ShopInventory.Instance != null)
{
foreach (var item in basket)
{
if (item != null) ShopInventory.Instance.AddStock(item, 1);
}
}
basket.Clear();
LeaveShop();
}
private void LeaveShop()
{
if (state == NPCState.Leaving) return;
state = NPCState.Leaving;
StopAllCoroutines();
if (ShopDoor.Instance != null)
{
agent.SetDestination(ShopDoor.Instance.InteractionTransform.position);
}
StartCoroutine(WaitAndDestroy());
}
private IEnumerator WaitAndDestroy()
{
float timeout = 15f;
float timer = 0f;
while (timer < timeout)
{
if (!agent.pathPending && agent.pathStatus == NavMeshPathStatus.PathComplete && agent.remainingDistance < 1.5f)
{
break;
}
timer += Time.deltaTime;
yield return null;
}
Destroy(gameObject);
}
public override void Interact(PlayerController player)
{
base.Interact(player);
if (UIManager.Instance != null)
{
UIManager.Instance.OpenNPCMenu(this, player);
}
else
{
Debug.Log($"[CustomerNPC] You interacted with {npcName}, but no UIManager is present.");
}
}
public void TalkTo()
{
if (state == NPCState.Browsing)
{
Debug.Log($"[{npcName}] 'Just browsing, thank you!'");
}
else if (state == NPCState.Queueing)
{
Debug.Log($"[{npcName}] 'I am waiting to pay...'");
}
else
{
Debug.Log($"[{npcName}] 'Hello shopkeeper!'");
}
}
}
}