Villager.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. using UnityEngine;
  2. using UnityEngine.InputSystem;
  3. using System.Collections.Generic;
  4. using System;
  5. [System.Serializable]
  6. public enum JobType
  7. {
  8. None, // Unemployed - transports goods
  9. Woodcutter, // Cuts trees
  10. Stonecutter, // Mines stone
  11. Farmer, // Grows food
  12. Builder // Constructs buildings
  13. }
  14. [System.Serializable]
  15. public enum VillagerState
  16. {
  17. Idle,
  18. MovingToWork,
  19. Working,
  20. MovingToDeliver,
  21. Delivering,
  22. Transporting,
  23. BuildingConstruction, // Working on a building construction site
  24. BuildingField // Farmer building a field
  25. }
  26. [System.Serializable]
  27. public class Experience
  28. {
  29. public float woodcutting = 1f;
  30. public float stonecutting = 1f;
  31. public float farming = 1f;
  32. public float building = 1f;
  33. public float GetExperienceForJob(JobType job)
  34. {
  35. return job switch
  36. {
  37. JobType.Woodcutter => woodcutting,
  38. JobType.Stonecutter => stonecutting,
  39. JobType.Farmer => farming,
  40. JobType.Builder => building,
  41. _ => 1f
  42. };
  43. }
  44. public void AddExperience(JobType job, float amount)
  45. {
  46. switch (job)
  47. {
  48. case JobType.Woodcutter:
  49. woodcutting += amount;
  50. break;
  51. case JobType.Stonecutter:
  52. stonecutting += amount;
  53. break;
  54. case JobType.Farmer:
  55. farming += amount;
  56. break;
  57. case JobType.Builder:
  58. building += amount;
  59. break;
  60. }
  61. }
  62. }
  63. public class Villager : MonoBehaviour
  64. {
  65. [Header("Villager Info")]
  66. public string villagerName;
  67. public JobType currentJob = JobType.None;
  68. public VillagerState state = VillagerState.Idle;
  69. [Header("Experience")]
  70. public Experience experience = new Experience();
  71. [Header("Work Settings")]
  72. public float workSpeed = 1f;
  73. public float moveSpeed = 3f;
  74. public float workRange = 2f;
  75. [Header("Current Task")]
  76. public GameObject targetWorkplace;
  77. public GameObject targetDelivery;
  78. public ResourceType carryingResource;
  79. public int carryingAmount = 0;
  80. public int maxCarryCapacity = 10;
  81. [Header("Construction")]
  82. public ConstructionSite assignedConstructionSite;
  83. public Field assignedField;
  84. public GameObject homeBuilding; // For farmers living in farmhouses
  85. [Header("Visual")]
  86. public Renderer villagerRenderer;
  87. public GameObject selectionIndicator;
  88. private Vector3 targetPosition;
  89. private bool isSelected = false;
  90. private float workTimer = 0f;
  91. private VillagerManager villagerManager;
  92. public static event Action<Villager> OnVillagerSelected;
  93. public static event Action<Villager, JobType> OnJobChanged;
  94. void Start()
  95. {
  96. villagerManager = FindFirstObjectByType<VillagerManager>();
  97. if (villagerRenderer == null)
  98. villagerRenderer = GetComponent<Renderer>();
  99. if (selectionIndicator != null)
  100. selectionIndicator.SetActive(false);
  101. // Random starting experience
  102. experience.woodcutting = UnityEngine.Random.Range(0.8f, 1.5f);
  103. experience.stonecutting = UnityEngine.Random.Range(0.8f, 1.5f);
  104. experience.farming = UnityEngine.Random.Range(0.8f, 1.5f);
  105. experience.building = UnityEngine.Random.Range(0.8f, 1.5f);
  106. }
  107. void Update()
  108. {
  109. UpdateBehavior();
  110. }
  111. void UpdateBehavior()
  112. {
  113. switch (state)
  114. {
  115. case VillagerState.Idle:
  116. HandleIdleState();
  117. break;
  118. case VillagerState.MovingToWork:
  119. HandleMovingToWork();
  120. break;
  121. case VillagerState.Working:
  122. HandleWorking();
  123. break;
  124. case VillagerState.MovingToDeliver:
  125. HandleMovingToDeliver();
  126. break;
  127. case VillagerState.Delivering:
  128. HandleDelivering();
  129. break;
  130. case VillagerState.Transporting:
  131. HandleTransporting();
  132. break;
  133. case VillagerState.BuildingConstruction:
  134. HandleConstruction();
  135. break;
  136. case VillagerState.BuildingField:
  137. HandleFieldBuilding();
  138. break;
  139. }
  140. }
  141. void HandleIdleState()
  142. {
  143. if (currentJob == JobType.None)
  144. {
  145. // Unemployed villagers stay idle until assigned a job
  146. // They can be selected and assigned jobs via drag-and-drop
  147. return;
  148. }
  149. else
  150. {
  151. // Look for work appropriate to job
  152. FindWork();
  153. }
  154. }
  155. void HandleMovingToWork()
  156. {
  157. if (MoveToTarget(targetPosition))
  158. {
  159. state = VillagerState.Working;
  160. workTimer = 0f;
  161. // Start harvesting on the resource node
  162. if (targetWorkplace != null)
  163. {
  164. ResourceNode node = targetWorkplace.GetComponent<ResourceNode>();
  165. if (node != null)
  166. {
  167. node.StartHarvesting(this);
  168. }
  169. }
  170. }
  171. }
  172. void HandleWorking()
  173. {
  174. if (targetWorkplace == null)
  175. {
  176. state = VillagerState.Idle;
  177. return;
  178. }
  179. workTimer += Time.deltaTime * GetWorkEfficiency();
  180. float workDuration = GetWorkDuration();
  181. if (workTimer >= workDuration)
  182. {
  183. // Actually harvest resources from the node
  184. ResourceNode node = targetWorkplace.GetComponent<ResourceNode>();
  185. if (node != null)
  186. {
  187. int requestedAmount = GetProducedAmount();
  188. int harvestedAmount = node.HarvestResources(requestedAmount, GetWorkEfficiency());
  189. carryingAmount = harvestedAmount;
  190. carryingResource = node.resourceType;
  191. if (harvestedAmount > 0)
  192. {
  193. // Stop harvesting before leaving
  194. node.StopHarvesting();
  195. CompleteWork();
  196. }
  197. else
  198. {
  199. // No resources left, stop harvesting and find new workplace
  200. node.StopHarvesting();
  201. targetWorkplace = null;
  202. state = VillagerState.Idle;
  203. }
  204. }
  205. else
  206. {
  207. // Fallback for non-node workplaces
  208. CompleteWork();
  209. }
  210. }
  211. }
  212. void HandleMovingToDeliver()
  213. {
  214. if (MoveToTarget(targetPosition))
  215. {
  216. state = VillagerState.Delivering;
  217. }
  218. }
  219. void HandleDelivering()
  220. {
  221. if (carryingAmount > 0)
  222. {
  223. DeliverResources();
  224. }
  225. // Always transition back to idle after delivery attempt
  226. state = VillagerState.Idle;
  227. targetPosition = transform.position; // Clear target
  228. }
  229. void HandleTransporting()
  230. {
  231. // Similar to delivering but for transport tasks
  232. if (MoveToTarget(targetPosition))
  233. {
  234. DeliverResources();
  235. state = VillagerState.Idle;
  236. }
  237. }
  238. bool MoveToTarget(Vector3 target)
  239. {
  240. Vector3 direction = (target - transform.position).normalized;
  241. transform.position += direction * moveSpeed * Time.deltaTime;
  242. return Vector3.Distance(transform.position, target) < 0.5f;
  243. }
  244. void FindWork()
  245. {
  246. // Builders should look for construction sites, not resource nodes
  247. if (currentJob == JobType.Builder)
  248. {
  249. FindAvailableConstructionSite();
  250. return;
  251. }
  252. // Farmers should check if they need to plant a field
  253. if (currentJob == JobType.Farmer && homeBuilding != null)
  254. {
  255. // Check if there are fewer than 2 fields near the farmhouse
  256. Field[] nearbyFields = FindObjectsOfType<Field>();
  257. int fieldsNearHome = 0;
  258. foreach (Field field in nearbyFields)
  259. {
  260. if (field.farmhouse == homeBuilding)
  261. fieldsNearHome++;
  262. }
  263. // Plant a new field if needed
  264. if (fieldsNearHome < 2)
  265. {
  266. PlantNewField();
  267. return;
  268. }
  269. }
  270. // If villager already has a specific workplace assigned, use it
  271. if (targetWorkplace != null)
  272. {
  273. targetPosition = targetWorkplace.transform.position;
  274. state = VillagerState.MovingToWork;
  275. return;
  276. }
  277. // Otherwise, find appropriate workplace based on job
  278. GameObject workplace = villagerManager.FindWorkplaceForJob(currentJob, transform.position);
  279. if (workplace != null)
  280. {
  281. targetWorkplace = workplace;
  282. targetPosition = workplace.transform.position;
  283. state = VillagerState.MovingToWork;
  284. }
  285. }
  286. void FindTransportTask()
  287. {
  288. // Look for resources that need to be moved
  289. // This is simplified - in a real game you'd have a more complex task system
  290. var townHall = GameObject.FindWithTag("TownHall");
  291. if (townHall != null && carryingAmount == 0)
  292. {
  293. // Simple transport: move to a random resource and bring it to town hall
  294. state = VillagerState.Transporting;
  295. }
  296. }
  297. void CompleteWork()
  298. {
  299. // carryingResource and carryingAmount are already set by HandleWorking if harvesting from a node
  300. // Only set them here if not already set (for jobs that don't use ResourceNode)
  301. if (carryingAmount == 0)
  302. {
  303. ResourceType producedResource = GetProducedResource();
  304. int amount = GetProducedAmount();
  305. carryingResource = producedResource;
  306. carryingAmount = amount;
  307. }
  308. // Add experience
  309. experience.AddExperience(currentJob, 0.1f);
  310. // Find delivery target (usually TownHall)
  311. GameObject townHall = GameObject.FindWithTag("TownHall");
  312. if (townHall != null)
  313. {
  314. targetDelivery = townHall;
  315. targetPosition = townHall.transform.position;
  316. state = VillagerState.MovingToDeliver;
  317. }
  318. else
  319. {
  320. // Deliver immediately if no town hall found
  321. DeliverResources();
  322. state = VillagerState.Idle;
  323. }
  324. }
  325. void DeliverResources()
  326. {
  327. if (carryingAmount > 0)
  328. {
  329. Debug.Log($"{villagerName} delivering {carryingAmount} {carryingResource} to storage");
  330. GameManager.Instance.resourceManager.AddResource(carryingResource, carryingAmount);
  331. carryingAmount = 0;
  332. carryingResource = ResourceType.None;
  333. }
  334. }
  335. ResourceType GetProducedResource()
  336. {
  337. return currentJob switch
  338. {
  339. JobType.Woodcutter => ResourceType.Wood,
  340. JobType.Stonecutter => ResourceType.Stone,
  341. JobType.Farmer => ResourceType.Food,
  342. JobType.Builder => ResourceType.None, // Builders don't produce resources
  343. _ => ResourceType.None
  344. };
  345. }
  346. int GetProducedAmount()
  347. {
  348. float efficiency = GetWorkEfficiency();
  349. int baseAmount = UnityEngine.Random.Range(1, 4);
  350. int producedAmount = Mathf.RoundToInt(baseAmount * efficiency);
  351. // Cap maximum production per work cycle to prevent resource explosion
  352. return Mathf.Clamp(producedAmount, 1, 10);
  353. }
  354. float GetWorkEfficiency()
  355. {
  356. float baseEfficiency = experience.GetExperienceForJob(currentJob) * workSpeed;
  357. // Cap efficiency to prevent resource explosion (max 3x efficiency)
  358. return Mathf.Clamp(baseEfficiency, 0.5f, 3f);
  359. }
  360. float GetWorkDuration()
  361. {
  362. return 3f / GetWorkEfficiency(); // Base 3 seconds, modified by efficiency
  363. }
  364. public void SetJob(JobType newJob)
  365. {
  366. currentJob = newJob;
  367. state = VillagerState.Idle;
  368. // Clear previous workplace assignment when changing jobs
  369. if (currentJob == JobType.None)
  370. {
  371. targetWorkplace = null;
  372. }
  373. OnJobChanged?.Invoke(this, newJob);
  374. // Update visual appearance based on job
  375. UpdateAppearance();
  376. }
  377. public void AssignToSpecificWorkplace(GameObject workplace)
  378. {
  379. targetWorkplace = workplace;
  380. if (workplace != null)
  381. {
  382. targetPosition = workplace.transform.position;
  383. state = VillagerState.MovingToWork;
  384. Debug.Log($"{villagerName} assigned to specific workplace: {workplace.name}");
  385. }
  386. }
  387. void UpdateAppearance()
  388. {
  389. if (villagerRenderer != null)
  390. {
  391. Color jobColor = currentJob switch
  392. {
  393. JobType.Woodcutter => Color.green,
  394. JobType.Stonecutter => Color.gray,
  395. JobType.Farmer => Color.yellow,
  396. JobType.Builder => Color.blue,
  397. _ => Color.white
  398. };
  399. villagerRenderer.material.color = jobColor;
  400. }
  401. }
  402. public void SetSelected(bool selected)
  403. {
  404. isSelected = selected;
  405. if (selectionIndicator != null)
  406. selectionIndicator.SetActive(selected);
  407. Debug.Log($"Villager {villagerName} SetSelected: {selected}");
  408. if (selected)
  409. {
  410. Debug.Log($"Villager {villagerName} triggering OnVillagerSelected event");
  411. OnVillagerSelected?.Invoke(this);
  412. }
  413. }
  414. void OnMouseDown()
  415. {
  416. // Use Physics raycast instead of OnMouseDown for better reliability
  417. if (Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame)
  418. {
  419. if (GameManager.Instance?.selectionManager != null)
  420. {
  421. GameManager.Instance.selectionManager.SelectVillager(this);
  422. }
  423. }
  424. }
  425. public string GetStatusText()
  426. {
  427. string status = $"{villagerName}\nJob: {currentJob}\nState: {state}";
  428. if (carryingAmount > 0)
  429. status += $"\nCarrying: {carryingAmount} {carryingResource}";
  430. if (currentJob == JobType.None)
  431. status += "\n[Click and drag to assign job]";
  432. if (assignedConstructionSite != null)
  433. status += $"\nBuilding: {assignedConstructionSite.buildingName}";
  434. if (assignedField != null)
  435. status += $"\nField: {assignedField.fieldSize}";
  436. return status;
  437. }
  438. // Construction methods
  439. public void AssignToConstruction(ConstructionSite site)
  440. {
  441. assignedConstructionSite = site;
  442. site.AssignBuilder(this);
  443. state = VillagerState.BuildingConstruction;
  444. targetPosition = site.transform.position;
  445. Debug.Log($"{villagerName} assigned to build {site.buildingName}");
  446. }
  447. public void AssignToFieldConstruction(Field field)
  448. {
  449. assignedField = field;
  450. state = VillagerState.BuildingField;
  451. targetPosition = field.transform.position;
  452. Debug.Log($"{villagerName} assigned to build {field.fieldSize} field");
  453. }
  454. void FindAvailableConstructionSite()
  455. {
  456. if (assignedConstructionSite != null) return;
  457. // Find nearest construction site that needs a builder
  458. ConstructionSite[] sites = FindObjectsOfType<ConstructionSite>();
  459. ConstructionSite nearestSite = null;
  460. float nearestDistance = float.MaxValue;
  461. foreach (var site in sites)
  462. {
  463. if (site.assignedBuilder == null && !site.isComplete)
  464. {
  465. float distance = Vector3.Distance(transform.position, site.transform.position);
  466. if (distance < nearestDistance)
  467. {
  468. nearestDistance = distance;
  469. nearestSite = site;
  470. }
  471. }
  472. }
  473. if (nearestSite != null)
  474. {
  475. AssignToConstruction(nearestSite);
  476. Debug.Log($"{villagerName} found construction site: {nearestSite.buildingName}");
  477. }
  478. }
  479. public void SetHomeBuilding(GameObject building)
  480. {
  481. homeBuilding = building;
  482. Debug.Log($"{villagerName} home set to {(building != null ? building.name : "none")}");
  483. }
  484. void HandleConstruction()
  485. {
  486. if (assignedConstructionSite == null)
  487. {
  488. state = VillagerState.Idle;
  489. return;
  490. }
  491. // Move to construction site
  492. float distance = Vector3.Distance(transform.position, assignedConstructionSite.transform.position);
  493. if (distance > workRange)
  494. {
  495. transform.position = Vector3.MoveTowards(transform.position, assignedConstructionSite.transform.position, moveSpeed * Time.deltaTime);
  496. }
  497. else
  498. {
  499. // Work on construction
  500. assignedConstructionSite.AdvanceConstruction(Time.deltaTime * GetWorkEfficiency());
  501. // Add building experience
  502. experience.AddExperience(JobType.Builder, 0.1f * Time.deltaTime);
  503. if (assignedConstructionSite.isComplete)
  504. {
  505. Debug.Log($"{villagerName} completed construction of {assignedConstructionSite.buildingName}");
  506. assignedConstructionSite = null;
  507. // Return to builder's workshop
  508. if (targetWorkplace != null)
  509. {
  510. targetPosition = targetWorkplace.transform.position;
  511. state = VillagerState.MovingToWork;
  512. }
  513. else
  514. {
  515. state = VillagerState.Idle;
  516. }
  517. }
  518. }
  519. }
  520. void HandleFieldBuilding()
  521. {
  522. if (assignedField == null)
  523. {
  524. state = VillagerState.Idle;
  525. return;
  526. }
  527. // Move to field location
  528. float distance = Vector3.Distance(transform.position, assignedField.transform.position);
  529. if (distance > workRange)
  530. {
  531. transform.position = Vector3.MoveTowards(transform.position, assignedField.transform.position, moveSpeed * Time.deltaTime);
  532. }
  533. else
  534. {
  535. // Work on field construction
  536. assignedField.AdvanceConstruction(Time.deltaTime * GetWorkEfficiency());
  537. // Add farming experience
  538. experience.AddExperience(JobType.Farmer, 0.1f * Time.deltaTime);
  539. if (!assignedField.isUnderConstruction)
  540. {
  541. Debug.Log($"{villagerName} completed field construction");
  542. assignedField = null;
  543. state = VillagerState.Idle;
  544. }
  545. }
  546. }
  547. void PlantNewField()
  548. {
  549. if (homeBuilding == null) return;
  550. // Find a position near the farmhouse to plant a field
  551. Vector3 farmhousePos = homeBuilding.transform.position;
  552. Vector2 randomCircle = UnityEngine.Random.insideUnitCircle * 8f; // 8 units away
  553. Vector3 fieldPosition = farmhousePos + new Vector3(randomCircle.x, 0, randomCircle.y);
  554. fieldPosition.y = 0.1f; // Slightly above ground
  555. // Create the field
  556. GameObject fieldObj = GameObject.CreatePrimitive(PrimitiveType.Cube);
  557. fieldObj.name = $"Field_{homeBuilding.name}";
  558. fieldObj.tag = "Farm";
  559. fieldObj.transform.position = fieldPosition;
  560. Field field = fieldObj.AddComponent<Field>();
  561. field.farmhouse = homeBuilding;
  562. field.fieldSize = FieldSize.Small;
  563. field.assignedFarmer = this;
  564. // Assign this field to the farmer to build
  565. assignedField = field;
  566. state = VillagerState.BuildingField;
  567. Debug.Log($"{villagerName} started planting a new field near {homeBuilding.name}");
  568. }
  569. }