using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using EasyTalk.Display; namespace MedievalShopkeeper { public enum NPCState { Entering, Browsing, Queueing, CheckingOut, Leaving, Fleeing } public enum NPCIntention { Shopper, Shoplifter, Raider } [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; private NPCState state; public NPCIntention intention { get; private set; } private NavMeshAgent agent; private List basket = new List(); private List shoppingList = new List(); private ShopTill targetTill; private bool isCaught = false; private Animator animator; private int speedHash; private int isMovingHash; private int isCarryingHash; private float originalSpeed; 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() { agent = GetComponent(); if (agent != null) originalSpeed = agent.speed; animator = GetComponentInChildren(); 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 newShoppingList) { intention = newIntention; shoppingList = newShoppingList ?? new List(); 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; StartCoroutine(BrainRoutine()); } private void OnDestroy() { if (AudioManager.Instance != null && (intention == NPCIntention.Shoplifter || intention == NPCIntention.Raider)) { AudioManager.Instance.RemoveThreat(); } } 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; } } state = NPCState.Browsing; if (intention == NPCIntention.Raider) { yield return RaiderRoutine(); yield break; } List itemsSought = new List(shoppingList); if (itemsSought.Count == 0) itemsSought.Add(null); foreach (var desiredItem in itemsSought) { bool reached = false; Shelf targetShelf = null; Shelf[] allShelves = FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None); List validShelves = new List(); foreach (var s in allShelves) { if (!s.HasStock || s.isBroken) continue; if (desiredItem != null) { if (s.AssignedItem == desiredItem) validShelves.Add(s); } else { validShelves.Add(s); } } if (validShelves.Count == 0) { string logStr = desiredItem != null ? desiredItem.itemName : "anything"; Speak($"I can't seem to find any {logStr}..."); continue; } targetShelf = validShelves[Random.Range(0, validShelves.Count)]; Debug.Log($"[{npcName}] Found {targetShelf.gameObject.name} which has what I need. Heading there..."); agent.SetDestination(targetShelf.InteractionTransform.position); float timer = 0f; float timeout = 15f; while (timer < timeout) { if (!agent.pathPending && agent.pathStatus == NavMeshPathStatus.PathComplete) { if (agent.remainingDistance < 1.5f) { reached = true; break; } } if (!agent.isStopped) { timer += Time.deltaTime; } yield return null; } if (reached) { Debug.Log($"[{npcName}] Arrived at {targetShelf.gameObject.name}. Browsing..."); yield return new WaitForSeconds(browseTimePerShelf); if (targetShelf.TryTakeItem(out ItemData item)) { Debug.Log($"[{npcName}] Took 1x {item.itemName}!"); basket.Add(item); } } } if (basket.Count > 0) { if (intention == NPCIntention.Shoplifter) { Debug.LogWarning($"[{npcName}] (Shoplifter) I'm stealing these and running for the door!"); LeaveShop(); } else { targetTill = FindFirstObjectByType(); if (targetTill != null) { state = NPCState.Queueing; targetTill.JoinQueue(this); } else { Debug.LogWarning($"[{npcName}] No Till found! Dropping items and leaving."); ReturnItemsAndLeave(); } } } else { Speak("I couldn't find what I wanted."); ReturnItemsAndLeave(); } } private IEnumerator RaiderRoutine() { Shelf[] allShelves = FindObjectsByType(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(); } 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; 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 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!")); } 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; StopAllCoroutines(); if (state == NPCState.Fleeing && agent != null) { agent.speed *= 1.5f; // Run away faster! } 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; } yield return null; } if (GameManager.Instance != null) { GameManager.Instance.CustomerLeft(); } Destroy(gameObject); } public override void Interact(PlayerController player) { base.Interact(player); if (intention == NPCIntention.Shoplifter && state == NPCState.Fleeing && basket.Count > 0 && !isCaught) { CatchThief(); return; } if (UIManager.Instance != null) { UIManager.Instance.OpenNPCMenu(this, player); } } private void CatchThief() { isCaught = true; Debug.Log($"[Caught!] You caught {npcName} trying to steal {basket.Count} items!"); StartCoroutine(HitAndStunSequence("Alright, alright! You caught me. Here!")); } private IEnumerator HitAndStunSequence(string dialogText) { PauseMovement(); PlayerController player = FindAnyObjectByType(); if (player != null) { Vector3 lookPos = player.transform.position; lookPos.y = transform.position.y; transform.LookAt(lookPos); Animator playerAnim = player.GetComponentInChildren(); if (playerAnim != null) playerAnim.SetTrigger("Hit"); } yield return new WaitForSeconds(0.4f); if (animator != null) { animator.SetTrigger("Stunned"); } if (stunVFXPrefab != null) { Instantiate(stunVFXPrefab, transform.position + Vector3.up * 1.5f, Quaternion.identity); } Speak(dialogText); yield return new WaitForSeconds(1.5f); ResumeMovement(); ReturnItemsAndLeave(); } public void TalkTo() { if (state == NPCState.Browsing) Speak("Just browsing, thank you!"); 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) { speechBubble.Show(); speechBubble.SetCharacterName(npcName, npcName); speechBubble.SetConversationText(text); if (hideBubbleRoutine != null) StopCoroutine(hideBubbleRoutine); hideBubbleRoutine = StartCoroutine(HideBubbleAfterDelay(bubbleShowDuration)); } } private IEnumerator HideBubbleAfterDelay(float delay) { yield return new WaitForSeconds(delay); if (speechBubble != null && gameObject.activeInHierarchy) { speechBubble.Hide(); } } } }