using UnityEngine; using UnityEngine.AI; using System.Collections.Generic; using UnityEngine.UI; public class NPC : MonoBehaviour { public enum Job { Builder, Farmer, Soldier, Forester, Merchant, Miner, StoneWorker, Citizen } public Job job = Job.Citizen; // Added a simple state tracker to cleanly separate gathering and traveling public enum NPCState { Gathering, Delivering, Idle, Building } public NPCState currentState = NPCState.Idle; private Resource.ResourceType resourceToWorkWith, carryingResource; public Dictionary inventory = new Dictionary(); [SerializeField] int gatherAmount, inventoryCapacity; [SerializeField] NavMeshAgent navMeshAgent; [SerializeField] Animator animationController; [SerializeField] float movementSpeed, interactionRadius; [SerializeField] Image jobImg; [SerializeField] Sprite miner, forester, citizen, farmer, merchant, soldier, builder, stoneWorker; private Resource currentTargetResource; private Building currentTargetBuilding; // Cache the TownHall so we don't search for it every frame void Start() { navMeshAgent.speed = movementSpeed; SetJob(job); } void Update() { // 1. Evaluate if we need to switch from Gathering to Delivering if (currentState == NPCState.Gathering && !CanAddToInventory(gatherAmount)) { animationController.SetBool("IsHarvesting", false); currentTargetResource = null; currentState = NPCState.Delivering; } // 2. Execute the current state's logic switch (currentState) { case NPCState.Gathering: HandleGatheringPhase(); break; case NPCState.Delivering: HandleDeliveryPhase(); break; case NPCState.Idle: navMeshAgent.ResetPath(); animationController.SetBool("MovementInputHeld", false); animationController.SetFloat("MoveSpeed", 0f); if (CanAddToInventory(gatherAmount) && job != Job.Citizen) if (inventory.ContainsKey(resourceToWorkWith)) currentState = NPCState.Gathering; else DepositResources(); break; case NPCState.Building: HandleBuildingPhase(); break; } } private void HandleGatheringPhase() { if (currentTargetResource == null || currentTargetResource.resourceState != Resource.ResourceState.Full) { FindResources(); } else { float distanceToTarget = Vector3.Distance(transform.position, currentTargetResource.transform.position); if (distanceToTarget <= interactionRadius) { GatherResource(); } else { MoveToLocation(currentTargetResource.transform.position); } } } private void HandleDeliveryPhase() { // 1. Find a TownHall if we haven't cached one yet if (currentTargetBuilding == null) { Building[] activeBuildings = Object.FindObjectsByType(FindObjectsSortMode.None); foreach (Building building in activeBuildings) { if (building.GetBuildingState() == Building.BuildingState.Active && building.GetBuildingType() == Building.BuildingType.TownHall) { currentTargetBuilding = building; break; // Exit the loop once we find one } } // If there's no TownHall active on the map, stand idle if (currentTargetBuilding == null) { currentState = NPCState.Idle; return; } } // 2. Move towards the TownHall float distanceToTarget = Vector3.Distance(transform.position, currentTargetBuilding.transform.position); if (distanceToTarget <= interactionRadius) { DepositResources(); } else { MoveToLocation(currentTargetBuilding.transform.position); } } private void HandleBuildingPhase() { // 1. If we finished our building, or someone else finished it, look for a new one if (currentTargetBuilding == null || currentTargetBuilding.GetBuildingState() != Building.BuildingState.Unbuilt) { Building newJob = JobManager.Instance.RequestJob(Job.Builder); if (newJob != null) { currentTargetBuilding = newJob; } else { currentState = NPCState.Idle; return; } } // 2. Move towards the unfinished building float distanceToTarget = Vector3.Distance(transform.position, currentTargetBuilding.transform.position); if (distanceToTarget <= interactionRadius) { WorkOnBuilding(); } else { MoveToLocation(currentTargetBuilding.transform.position); } } private void WorkOnBuilding() { // Stop moving and look at the building navMeshAgent.ResetPath(); animationController.SetBool("MovementInputHeld", false); animationController.SetFloat("MoveSpeed", 0f); transform.LookAt(currentTargetBuilding.transform.position); // Tell the building to progress its construction (transitions from Unbuilt to Constructing on first call) currentTargetBuilding.StartBuilding(); // Play animation while building is constructing if (currentTargetBuilding.GetBuildingState() == Building.BuildingState.Constructing) { animationController.SetBool("IsHarvesting", true); } else { // Stop animation when building is complete animationController.SetBool("IsHarvesting", false); } } private void MoveToLocation(Vector3 destination) { navMeshAgent.SetDestination(destination); animationController.SetBool("MovementInputHeld", true); animationController.SetFloat("MoveSpeed", movementSpeed); } private void FindResources() { Resource[] resources = Object.FindObjectsByType(FindObjectsSortMode.None); Resource closestResource = null; float closestDistance = Mathf.Infinity; foreach (Resource res in resources) { if (res.resourceType == resourceToWorkWith && res.resourceState == Resource.ResourceState.Full) { float distance = Vector3.Distance(transform.position, res.transform.position); if (distance < closestDistance) { closestDistance = distance; closestResource = res; } } } if (closestResource != null) { currentTargetResource = closestResource; MoveToLocation(currentTargetResource.transform.position); } else { // If the map is empty, check our pockets. // If we have items, go deliver them. If empty, stand idle. if (GetTotalInventoryAmount() > 0) { currentState = NPCState.Delivering; } else { currentState = NPCState.Idle; } } } private void GatherResource() { navMeshAgent.ResetPath(); animationController.SetBool("MovementInputHeld", false); animationController.SetFloat("MoveSpeed", 0f); transform.LookAt(currentTargetResource.transform.position); animationController.SetBool("IsHarvesting", true); if (carryingResource != resourceToWorkWith) { carryingResource = resourceToWorkWith; } } private void Harvest() { if (currentTargetResource != null) { // Safety check: if full, stop hitting and trigger delivery on the next frame if (!CanAddToInventory(gatherAmount)) { animationController.SetBool("IsHarvesting", false); currentTargetResource = null; currentState = NPCState.Delivering; return; } currentTargetResource.RemoveResource(gatherAmount); AddToInventory(resourceToWorkWith, gatherAmount); if (currentTargetResource.GetResourceState() == false) { currentTargetResource = null; animationController.SetBool("IsHarvesting", false); } } else { animationController.SetBool("IsHarvesting", false); } } public void SetJob(Job job) { switch (job) { case Job.Builder: //resourceToWorkWith = Resource.ResourceType.Building; jobImg.sprite = builder; CancelTask(); Building buildingTarget = JobManager.Instance.RequestJob(job); if (buildingTarget != null) { currentTargetBuilding = buildingTarget; currentState = NPCState.Building; // Put them in the Building state } else { // No buildings need building right now. Stand by. currentState = NPCState.Idle; } break; case Job.Soldier: CancelTask(); break; case Job.Farmer: resourceToWorkWith = Resource.ResourceType.Food; CancelTask(); break; case Job.Forester: resourceToWorkWith = Resource.ResourceType.Wood; jobImg.sprite = forester; CancelTask(); break; case Job.Merchant: resourceToWorkWith = Resource.ResourceType.Money; CancelTask(); break; case Job.Miner: resourceToWorkWith = Resource.ResourceType.Metal; jobImg.sprite = miner; CancelTask(); break; case Job.StoneWorker: resourceToWorkWith = Resource.ResourceType.Stone; jobImg.sprite = stoneWorker; CancelTask(); break; case Job.Citizen: resourceToWorkWith = Resource.ResourceType.Population; jobImg.sprite = citizen; CancelTask(); break; default: break; } // Don't instantly teleport resources. Put the NPC into Delivery state. if (inventory.Count > 0 && carryingResource != resourceToWorkWith) { currentState = NPCState.Delivering; } carryingResource = resourceToWorkWith; this.job = job; } public Job GetJob() { return job; } private bool AddToInventory(Resource.ResourceType resourceToAdd, int amountToAdd) { if (amountToAdd <= 0) return true; if (!inventory.ContainsKey(resourceToAdd)) { inventory.Add(resourceToAdd, 0); } inventory[resourceToAdd] += amountToAdd; return true; } private bool CanAddToInventory(int amountToAdd) { return GetTotalInventoryAmount() + amountToAdd <= inventoryCapacity; } // Efficiency: Created a helper method so we aren't writing out the dictionary loop constantly private int GetTotalInventoryAmount() { int currentTotal = 0; foreach (int amount in inventory.Values) { currentTotal += amount; } return currentTotal; } private void DepositResources() { navMeshAgent.ResetPath(); animationController.SetBool("MovementInputHeld", false); animationController.SetFloat("MoveSpeed", 0f); if (currentTargetBuilding != null) { currentTargetBuilding.Interact(this); } // CRITICAL: You must clear the local inventory so CanAddToInventory() becomes true again. inventory.Clear(); currentTargetBuilding = null; currentState = NPCState.Gathering; } public Dictionary GetInventory() { return inventory; } private void CancelTask() { navMeshAgent.destination = transform.position; animationController.SetBool("MovementInputHeld", false); animationController.SetFloat("MoveSpeed", 0f); currentTargetResource = null; currentTargetBuilding = null; currentState = NPCState.Idle; } }