396 lines
13 KiB
C#
396 lines
13 KiB
C#
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<Resource.ResourceType, int> inventory = new Dictionary<Resource.ResourceType, int>();
|
|
|
|
[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<Building>(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<Resource>(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<Resource.ResourceType, int> 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;
|
|
}
|
|
} |