Files
MedievalShopkeeper/Assets/Scripts/CustomerNPC.cs

505 lines
16 KiB
C#
Raw Permalink Normal View History

2026-09-03 16:35:05 +01:00
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using EasyTalk.Display;
2026-09-03 16:35:05 +01:00
namespace MedievalShopkeeper
{
public enum NPCState
{
Entering,
Browsing,
Queueing,
CheckingOut,
Leaving,
Fleeing
}
public enum NPCIntention
{
Shopper,
Shoplifter,
Raider
2026-09-03 16:35:05 +01:00
}
[RequireComponent(typeof(NavMeshAgent))]
public class CustomerNPC : Interactable
{
[Header("NPC Data")]
public string npcName = "Peasant";
public float browseTimePerShelf = 2f;
[Header("Dialogue (EasyTalk)")]
[Tooltip("Assign a ConversationDisplay or WorldspaceConversationDisplay attached to this NPC prefab")]
public ConversationDisplay speechBubble;
public float bubbleShowDuration = 4f;
private Coroutine hideBubbleRoutine;
[Header("Effects")]
[Tooltip("VFX to play when the NPC is stunned (e.g. caught stealing or kicked out).")]
[SerializeField] private GameObject stunVFXPrefab;
2026-09-03 16:35:05 +01:00
private NPCState state;
public NPCIntention intention { get; private set; }
2026-09-03 16:35:05 +01:00
private NavMeshAgent agent;
2026-09-03 16:35:05 +01:00
private List<ItemData> basket = new List<ItemData>();
private List<ItemData> shoppingList = new List<ItemData>();
2026-09-03 16:35:05 +01:00
private ShopTill targetTill;
private bool isCaught = false;
2026-09-03 16:35:05 +01:00
private Animator animator;
private int speedHash;
private int isMovingHash;
private int isCarryingHash;
private float originalSpeed;
2026-09-03 16:35:05 +01:00
public override string InteractionPrompt
{
get
{
if (intention == NPCIntention.Shoplifter && state == NPCState.Fleeing && basket.Count > 0 && !isCaught)
{
return "Catch Thief!";
}
return $"Talk to {npcName}";
}
}
private void Awake()
2026-09-03 16:35:05 +01:00
{
agent = GetComponent<NavMeshAgent>();
if (agent != null) originalSpeed = agent.speed;
animator = GetComponentInChildren<Animator>();
if (animator != null)
{
speedHash = Animator.StringToHash("Speed");
isMovingHash = Animator.StringToHash("IsMoving");
isCarryingHash = Animator.StringToHash("IsCarrying");
animator.applyRootMotion = false;
}
}
private void LateUpdate()
{
if (speechBubble != null && speechBubble.gameObject.activeInHierarchy && Camera.main != null)
{
speechBubble.transform.forward = Camera.main.transform.forward;
}
}
public void PauseMovement()
{
if (agent != null && agent.isOnNavMesh)
{
agent.isStopped = true;
agent.velocity = Vector3.zero;
}
}
public void ResumeMovement()
{
if (agent != null && agent.isOnNavMesh)
{
agent.isStopped = false;
}
}
public void Initialize(NPCIntention newIntention, List<ItemData> newShoppingList)
{
intention = newIntention;
shoppingList = newShoppingList ?? new List<ItemData>();
2026-09-03 16:35:05 +01:00
state = NPCState.Entering;
if (GameManager.Instance != null)
{
GameManager.Instance.CustomerEntered();
}
if (AudioManager.Instance != null && (intention == NPCIntention.Shoplifter || intention == NPCIntention.Raider))
{
AudioManager.Instance.AddThreat();
}
if (intention == NPCIntention.Shoplifter) npcName = "Shady " + npcName;
if (intention == NPCIntention.Raider) npcName = "Angry " + npcName;
2026-09-03 16:35:05 +01:00
StartCoroutine(BrainRoutine());
}
private void OnDestroy()
{
if (AudioManager.Instance != null && (intention == NPCIntention.Shoplifter || intention == NPCIntention.Raider))
{
AudioManager.Instance.RemoveThreat();
}
}
2026-09-03 16:35:05 +01:00
private IEnumerator BrainRoutine()
{
if (ShopDoor.Instance != null)
{
agent.SetDestination(ShopDoor.Instance.InteractionTransform.position);
float timer = 0f;
while (timer < 10f)
{
if (!agent.pathPending && agent.pathStatus == NavMeshPathStatus.PathComplete && agent.remainingDistance < 1.5f)
{
break;
}
if (!agent.isStopped)
{
timer += Time.deltaTime;
}
yield return null;
}
}
2026-09-03 16:35:05 +01:00
state = NPCState.Browsing;
if (intention == NPCIntention.Raider)
{
yield return RaiderRoutine();
yield break;
}
List<ItemData> itemsSought = new List<ItemData>(shoppingList);
if (itemsSought.Count == 0) itemsSought.Add(null);
foreach (var desiredItem in itemsSought)
2026-09-03 16:35:05 +01:00
{
bool reached = false;
Shelf targetShelf = null;
2026-09-03 16:35:05 +01:00
Shelf[] allShelves = FindObjectsByType<Shelf>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
List<Shelf> validShelves = new List<Shelf>();
foreach (var s in allShelves)
2026-09-03 16:35:05 +01:00
{
if (!s.HasStock || s.isBroken) continue;
if (desiredItem != null)
{
if (s.AssignedItem == desiredItem) validShelves.Add(s);
}
else
{
validShelves.Add(s);
}
2026-09-03 16:35:05 +01:00
}
if (validShelves.Count == 0)
2026-09-03 16:35:05 +01:00
{
string logStr = desiredItem != null ? desiredItem.itemName : "anything";
Speak($"I can't seem to find any {logStr}...");
continue;
2026-09-03 16:35:05 +01:00
}
targetShelf = validShelves[Random.Range(0, validShelves.Count)];
Debug.Log($"[{npcName}] Found {targetShelf.gameObject.name} which has what I need. Heading there...");
2026-09-03 16:35:05 +01:00
agent.SetDestination(targetShelf.InteractionTransform.position);
float timer = 0f;
float timeout = 15f;
2026-09-03 16:35:05 +01:00
while (timer < timeout)
{
if (!agent.pathPending && agent.pathStatus == NavMeshPathStatus.PathComplete)
2026-09-03 16:35:05 +01:00
{
if (agent.remainingDistance < 1.5f)
2026-09-03 16:35:05 +01:00
{
reached = true;
break;
}
}
if (!agent.isStopped)
{
timer += Time.deltaTime;
}
2026-09-03 16:35:05 +01:00
yield return null;
}
if (reached)
{
Debug.Log($"[{npcName}] Arrived at {targetShelf.gameObject.name}. Browsing...");
2026-09-03 16:35:05 +01:00
yield return new WaitForSeconds(browseTimePerShelf);
if (targetShelf.TryTakeItem(out ItemData item))
{
Debug.Log($"[{npcName}] Took 1x {item.itemName}!");
2026-09-03 16:35:05 +01:00
basket.Add(item);
}
}
}
if (basket.Count > 0)
{
if (intention == NPCIntention.Shoplifter)
2026-09-03 16:35:05 +01:00
{
Debug.LogWarning($"[{npcName}] (Shoplifter) I'm stealing these and running for the door!");
LeaveShop();
2026-09-03 16:35:05 +01:00
}
else
{
targetTill = FindFirstObjectByType<ShopTill>();
if (targetTill != null)
{
state = NPCState.Queueing;
targetTill.JoinQueue(this);
}
else
{
Debug.LogWarning($"[{npcName}] No Till found! Dropping items and leaving.");
ReturnItemsAndLeave();
}
2026-09-03 16:35:05 +01:00
}
}
else
{
Speak("I couldn't find what I wanted.");
2026-09-03 16:35:05 +01:00
ReturnItemsAndLeave();
}
}
private IEnumerator RaiderRoutine()
{
Shelf[] allShelves = FindObjectsByType<Shelf>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
if (allShelves.Length > 0)
{
Shelf targetShelf = allShelves[Random.Range(0, allShelves.Length)];
Debug.LogWarning($"[{npcName}] (Raider) I'm going to smash {targetShelf.gameObject.name}!");
agent.SetDestination(targetShelf.InteractionTransform.position);
float timer = 0f;
while (timer < 10f)
{
if (!agent.pathPending && agent.pathStatus == NavMeshPathStatus.PathComplete && agent.remainingDistance < 1.5f)
{
yield return new WaitForSeconds(0.5f);
targetShelf.BreakShelf();
break;
}
if (!agent.isStopped)
{
timer += Time.deltaTime;
}
yield return null;
}
}
LeaveShop();
}
2026-09-03 16:35:05 +01:00
private void Update()
{
if (animator != null && agent != null)
{
float currentSpeed = agent.velocity.magnitude;
bool isMoving = agent.velocity.sqrMagnitude > 0.05f && !agent.isStopped && agent.hasPath;
animator.SetFloat(speedHash, currentSpeed);
animator.SetBool(isMovingHash, isMoving);
animator.SetBool(isCarryingHash, basket.Count > 0);
}
if (agent != null && agent.isStopped) return;
2026-09-03 16:35:05 +01:00
if (state == NPCState.Queueing && targetTill != null)
{
Vector3 qPos = targetTill.GetQueuePosition(this);
agent.SetDestination(qPos);
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()
{
if (targetTill != null && (state == NPCState.Queueing || state == NPCState.CheckingOut))
{
targetTill.LeaveQueue(this);
}
StartCoroutine(HitAndStunSequence("Fine, I'm leaving!"));
2026-09-03 16:35:05 +01:00
}
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 || state == NPCState.Fleeing) return;
state = (intention == NPCIntention.Shoplifter || intention == NPCIntention.Raider) ? NPCState.Fleeing : NPCState.Leaving;
2026-09-03 16:35:05 +01:00
StopAllCoroutines();
if (state == NPCState.Fleeing && agent != null)
{
agent.speed *= 1.5f; // Run away faster!
}
2026-09-03 16:35:05 +01:00
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;
}
if (!agent.isStopped)
{
timer += Time.deltaTime;
}
2026-09-03 16:35:05 +01:00
yield return null;
}
if (GameManager.Instance != null)
{
GameManager.Instance.CustomerLeft();
}
2026-09-03 16:35:05 +01:00
Destroy(gameObject);
}
public override void Interact(PlayerController player)
{
base.Interact(player);
if (intention == NPCIntention.Shoplifter && state == NPCState.Fleeing && basket.Count > 0 && !isCaught)
{
CatchThief();
return;
}
2026-09-03 16:35:05 +01:00
if (UIManager.Instance != null)
{
UIManager.Instance.OpenNPCMenu(this, player);
}
}
private void CatchThief()
{
isCaught = true;
Debug.Log($"<color=#00FF00>[Caught!] You caught {npcName} trying to steal {basket.Count} items!</color>");
StartCoroutine(HitAndStunSequence("Alright, alright! You caught me. Here!"));
}
private IEnumerator HitAndStunSequence(string dialogText)
{
PauseMovement();
PlayerController player = FindAnyObjectByType<PlayerController>();
if (player != null)
{
Vector3 lookPos = player.transform.position;
lookPos.y = transform.position.y;
transform.LookAt(lookPos);
Animator playerAnim = player.GetComponentInChildren<Animator>();
if (playerAnim != null) playerAnim.SetTrigger("Hit");
}
yield return new WaitForSeconds(0.4f);
if (animator != null)
{
animator.SetTrigger("Stunned");
}
if (stunVFXPrefab != null)
2026-09-03 16:35:05 +01:00
{
Instantiate(stunVFXPrefab, transform.position + Vector3.up * 1.5f, Quaternion.identity);
2026-09-03 16:35:05 +01:00
}
Speak(dialogText);
yield return new WaitForSeconds(1.5f);
ResumeMovement();
ReturnItemsAndLeave();
2026-09-03 16:35:05 +01:00
}
public void TalkTo()
{
if (state == NPCState.Browsing)
Speak("Just browsing, thank you!");
2026-09-03 16:35:05 +01:00
else if (state == NPCState.Queueing)
Speak("I am waiting to pay...");
else if (state == NPCState.Fleeing)
Speak("You'll never catch me!");
else
Speak("Hello shopkeeper!");
}
private void Speak(string text)
{
Debug.Log($"[{npcName}] '{text}'");
if (speechBubble != null)
2026-09-03 16:35:05 +01:00
{
speechBubble.Show();
speechBubble.SetCharacterName(npcName, npcName);
speechBubble.SetConversationText(text);
if (hideBubbleRoutine != null) StopCoroutine(hideBubbleRoutine);
hideBubbleRoutine = StartCoroutine(HideBubbleAfterDelay(bubbleShowDuration));
2026-09-03 16:35:05 +01:00
}
}
private IEnumerator HideBubbleAfterDelay(float delay)
{
yield return new WaitForSeconds(delay);
if (speechBubble != null && gameObject.activeInHierarchy)
2026-09-03 16:35:05 +01:00
{
speechBubble.Hide();
2026-09-03 16:35:05 +01:00
}
}
}
}