MonsterFormationGroup.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. using UnityEngine;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. public class MonsterFormationGroup : MonoBehaviour
  6. {
  7. public MonsterData monsterData;
  8. public Vector3 anchorPosition;
  9. public float spacing = 1.5f;
  10. public float frontRowOffset = 2f;
  11. public float cellSize = 5f;
  12. public float attackRange = 6f;
  13. public float moveDelay = 1f;
  14. public float stopAfterDistance = 5f;
  15. public float stopDuration = 2f;
  16. public float attackInterval = 1.5f;
  17. public float attackRepeatDelay = 3f;
  18. // Attack range consistency
  19. private const float ATTACK_RANGE_BUFFER = 0.5f;
  20. private float effectiveAttackRange => attackRange + ATTACK_RANGE_BUFFER;
  21. public List<MonsterController> frontRow = new();
  22. public List<MonsterController> backRow = new();
  23. public List<Vector3> frontRowPositions = new();
  24. private Transform player;
  25. public bool isChasing = false;
  26. public bool hasDetectedPlayer = false; // Track if player has been detected
  27. private float distanceTravelled = 0f;
  28. private Coroutine attackLoopCoroutine;
  29. private TeamCohesionManager cohesionManager;
  30. private void Start()
  31. {
  32. StartCoroutine(DelayedStart());
  33. }
  34. private IEnumerator DelayedStart()
  35. {
  36. while (UIUpdater.Instance == null)
  37. yield return null; // attend que UIUpdater existe
  38. player = GameObject.FindWithTag("Player")?.transform;
  39. cohesionManager = FindFirstObjectByType<TeamCohesionManager>();
  40. if (GetComponent<Collider>() == null)
  41. {
  42. SphereCollider trigger = gameObject.AddComponent<SphereCollider>();
  43. trigger.isTrigger = true;
  44. trigger.radius = 20f;
  45. }
  46. }
  47. private void OnTriggerEnter(Collider other)
  48. {
  49. if (other.CompareTag("Player"))
  50. {
  51. hasDetectedPlayer = true;
  52. Debug.Log("[MonsterGroup] Player entered detection zone");
  53. // Only start chase if not already chasing or attacking
  54. if (!isChasing && attackLoopCoroutine == null)
  55. {
  56. StartChase();
  57. }
  58. }
  59. }
  60. private void OnTriggerExit(Collider other)
  61. {
  62. if (other.CompareTag("Player"))
  63. {
  64. hasDetectedPlayer = false;
  65. Debug.Log("[MonsterGroup] Player exited detection zone");
  66. // Stop attack loop if player leaves
  67. if (attackLoopCoroutine != null)
  68. {
  69. StopCoroutine(attackLoopCoroutine);
  70. attackLoopCoroutine = null;
  71. }
  72. // Reset chase state
  73. isChasing = false;
  74. }
  75. }
  76. private void OnTriggerStay(Collider other)
  77. {
  78. if (other.CompareTag("Player"))
  79. {
  80. hasDetectedPlayer = true;
  81. }
  82. }
  83. public void Spawn()
  84. {
  85. int total = monsterData.monstersPerGroup;
  86. for (int i = 0; i < total; i++)
  87. {
  88. int row = i < 3 ? 0 : 1;
  89. int indexInRow = row == 0 ? i : i - 3;
  90. float xOffset = 0f;
  91. if (row == 0)
  92. {
  93. xOffset = (indexInRow - 1) * spacing;
  94. }
  95. else
  96. {
  97. if (monsterData.monstersPerGroup == 5 && indexInRow < 2)
  98. xOffset = (indexInRow - 0.5f) * spacing;
  99. else
  100. xOffset = (indexInRow - 1.5f) * spacing;
  101. }
  102. float zOffset = -row * spacing + (row == 0 ? frontRowOffset : 0);
  103. Vector3 offset = new Vector3(xOffset, 0, zOffset);
  104. Vector3 rotatedOffset = RotateOffset(offset, monsterData.defaultOrientation);
  105. Vector3 spawnPos = anchorPosition + rotatedOffset;
  106. Quaternion rotation = GetRotationFromOrientation(monsterData.defaultOrientation);
  107. GameObject m = Instantiate(monsterData.prefab, spawnPos, rotation, transform);
  108. MonsterController ctrl = m.GetComponent<MonsterController>();
  109. ctrl.data = monsterData;
  110. ctrl.formationGroup = this;
  111. if (row == 0)
  112. {
  113. frontRow.Add(ctrl);
  114. frontRowPositions.Add(spawnPos);
  115. }
  116. else
  117. {
  118. backRow.Add(ctrl);
  119. }
  120. }
  121. }
  122. public void NotifyMonsterDeath(MonsterController dead)
  123. {
  124. Debug.Log("Boucle NotifyMonster");
  125. Vector3 deadPosition = dead.transform.position;
  126. if (frontRow.Contains(dead))
  127. {
  128. Debug.Log("FrontRow ...");
  129. int index = frontRow.IndexOf(dead);
  130. frontRow.RemoveAt(index);
  131. if (backRow.Count > 0)
  132. {
  133. Debug.Log("BackRow");
  134. MonsterController replacement = GetClosestBacklinerTo(deadPosition);
  135. backRow.Remove(replacement);
  136. frontRow.Insert(index, replacement);
  137. StartCoroutine(MoveReplacementWithDelay(replacement, deadPosition, 2f)); // 👈 délai de 2s
  138. }
  139. }
  140. else
  141. {
  142. Debug.Log("BackRow Dead");
  143. backRow.Remove(dead);
  144. }
  145. }
  146. IEnumerator MoveReplacementWithDelay(MonsterController replacement, Vector3 destination, float delay)
  147. {
  148. yield return new WaitForSeconds(delay);
  149. replacement.MoveTo(destination);
  150. replacement.FaceTarget(player.position);
  151. }
  152. public void StartChase()
  153. {
  154. if (!isChasing)
  155. {
  156. // Ajout de vérification avant de démarrer le combat
  157. if (UIUpdater.Instance == null || UIUpdater.Instance.IsReady == false)
  158. {
  159. Debug.Log("[MonsterGroup] UI pas encore prête, attente du setup...");
  160. StartCoroutine(WaitForUIThenChase());
  161. return;
  162. }
  163. Debug.Log("[MonsterGroup] StartChase() appelé");
  164. StartCoroutine(ChaseRoutine());
  165. }
  166. }
  167. IEnumerator WaitForUIThenChase()
  168. {
  169. // Attend que UIUpdater soit prêt
  170. while (UIUpdater.Instance == null || UIUpdater.Instance.IsReady == false)
  171. {
  172. yield return null;
  173. }
  174. Debug.Log("[MonsterGroup] UI prête, démarrage de la chasse !");
  175. StartCoroutine(ChaseRoutine());
  176. }
  177. IEnumerator ChaseRoutine()
  178. {
  179. isChasing = true;
  180. distanceTravelled = 0f;
  181. Debug.Log("[MonsterGroup] Chase routine started");
  182. while (isChasing && hasDetectedPlayer)
  183. {
  184. // Check if player left detection zone
  185. if (!hasDetectedPlayer)
  186. {
  187. Debug.Log("[MonsterGroup] Player left detection zone during chase");
  188. isChasing = false;
  189. yield break;
  190. }
  191. float distanceToPlayer = Vector3.Distance(transform.position, player.position);
  192. // Reached attack range
  193. if (distanceToPlayer <= attackRange || IsAdjacentToPlayer())
  194. {
  195. Debug.Log("[MonsterGroup] Reached attack range, starting combat");
  196. isChasing = false;
  197. attackLoopCoroutine = StartCoroutine(LoopAttack());
  198. yield break;
  199. }
  200. // Continue chasing
  201. Vector3 dirToPlayer = (player.position - transform.position).normalized;
  202. Vector3 step = new Vector3(Mathf.Round(dirToPlayer.x), 0, Mathf.Round(dirToPlayer.z)) * cellSize;
  203. MoveGroupBy(step);
  204. distanceTravelled += cellSize + 1f;
  205. // Tactical pause every 5 meters
  206. if (distanceTravelled >= stopAfterDistance)
  207. {
  208. Debug.Log("[MonsterGroup] Tactical pause after moving 5m");
  209. yield return new WaitForSeconds(stopDuration);
  210. distanceTravelled = 0f;
  211. }
  212. else
  213. {
  214. yield return new WaitForSeconds(moveDelay);
  215. }
  216. }
  217. Debug.Log("[MonsterGroup] Chase routine ended");
  218. isChasing = false;
  219. }
  220. void MoveGroupBy(Vector3 step)
  221. {
  222. foreach (var monster in frontRow.Concat(backRow))
  223. {
  224. Vector3 targetPos = monster.transform.position + step;
  225. monster.MoveTo(targetPos);
  226. monster.FaceTarget(player.position);
  227. }
  228. }
  229. private void Update()
  230. {
  231. // Only act if player is in detection zone
  232. if (!hasDetectedPlayer) return;
  233. // If player not assigned yet, wait
  234. if (player == null) return;
  235. // Prevent interference with active states
  236. if (isChasing || attackLoopCoroutine != null) return;
  237. // If not currently doing anything and player is detected
  238. if (!isChasing && attackLoopCoroutine == null)
  239. {
  240. // Check if any front row monster is in attack range
  241. bool anyMonsterInAttackRange = false;
  242. float closestMonsterDistance = float.MaxValue;
  243. if (frontRow.Count > 0)
  244. {
  245. foreach (var monster in frontRow)
  246. {
  247. if (monster != null)
  248. {
  249. float monsterToPlayerDist = Vector3.Distance(monster.transform.position, player.position);
  250. if (monsterToPlayerDist < closestMonsterDistance)
  251. {
  252. closestMonsterDistance = monsterToPlayerDist;
  253. }
  254. if (monsterToPlayerDist <= effectiveAttackRange)
  255. {
  256. anyMonsterInAttackRange = true;
  257. }
  258. }
  259. }
  260. }
  261. else
  262. {
  263. Debug.LogWarning("[MonsterGroup] Update: No front row monsters available!");
  264. return;
  265. }
  266. if (anyMonsterInAttackRange)
  267. {
  268. // Monsters are close enough to attack
  269. Debug.Log($"[MonsterGroup] Update: Monsters in attack range (closest: {closestMonsterDistance:F1}m), starting attack loop");
  270. attackLoopCoroutine = StartCoroutine(LoopAttack());
  271. }
  272. else
  273. {
  274. // No monsters in attack range, should chase player
  275. Debug.Log($"[MonsterGroup] Update: Monsters out of attack range (closest: {closestMonsterDistance:F1}m), starting chase");
  276. StartChase();
  277. }
  278. }
  279. }
  280. bool IsPlayerInRange()
  281. {
  282. return Vector3.Distance(transform.position, player.position) <= attackRange;
  283. }
  284. IEnumerator LoopAttack()
  285. {
  286. Debug.Log("[MonsterGroup] Attack loop started");
  287. while (true)
  288. {
  289. // Check if game manager says player is dead (prevents attacks during death sequence)
  290. if (GameManager.Instance != null && GameManager.Instance.IsPlayerDead())
  291. {
  292. Debug.Log("[MonsterGroup] Player is dead, stopping attack");
  293. attackLoopCoroutine = null;
  294. yield break;
  295. }
  296. // Check if player left detection zone completely
  297. if (!hasDetectedPlayer)
  298. {
  299. Debug.Log("[MonsterGroup] Player left detection zone, stopping attack");
  300. attackLoopCoroutine = null;
  301. yield break;
  302. }
  303. // Check if any party members are still alive
  304. if (cohesionManager == null || cohesionManager.groupMembers.Count == 0)
  305. {
  306. Debug.Log("[MonsterGroup] No party members alive, stopping attack");
  307. attackLoopCoroutine = null;
  308. yield break;
  309. }
  310. // Check if any monsters are still alive
  311. if (frontRow.Count == 0 && backRow.Count == 0)
  312. {
  313. Debug.Log("[MonsterGroup] All monsters dead, stopping attack");
  314. attackLoopCoroutine = null;
  315. yield break;
  316. }
  317. // Check if any monster is in attack range
  318. bool anyInRange = frontRow.Any(monster =>
  319. monster != null && Vector3.Distance(monster.transform.position, player.position) <= effectiveAttackRange);
  320. if (!anyInRange)
  321. {
  322. Debug.Log("[MonsterGroup] Player moved out of attack range - stopping attack loop");
  323. attackLoopCoroutine = null;
  324. // Log state for debugging
  325. Debug.Log($"[MonsterGroup] State after attack stop: hasDetectedPlayer={hasDetectedPlayer}, isChasing={isChasing}, attackLoopCoroutine={attackLoopCoroutine}");
  326. if (hasDetectedPlayer)
  327. {
  328. Debug.Log("[MonsterGroup] Player still detected - Update() will handle re-engagement");
  329. }
  330. else
  331. {
  332. Debug.Log("[MonsterGroup] Player no longer detected - will wait for re-entry");
  333. }
  334. yield break;
  335. }
  336. // Attack with each front row monster
  337. foreach (var monster in frontRow.ToList()) // ToList to avoid modification during iteration
  338. {
  339. if (monster == null) continue;
  340. if (monsterData.attackType == MonsterData.AttackType.Melee)
  341. {
  342. float distanceToPlayer = Vector3.Distance(monster.transform.position, player.position);
  343. if (distanceToPlayer > effectiveAttackRange)
  344. {
  345. continue;
  346. }
  347. monster.StopMove();
  348. monster.FaceTarget(player.position, instant: true);
  349. // Verify monster is facing player (required for melee)
  350. if (!monster.IsFacingTarget(player.position, 45f))
  351. {
  352. Debug.Log($"[Combat] {monster.name} not facing player, skipping");
  353. continue;
  354. }
  355. monster.Attack();
  356. // Find the closest character on the party grid
  357. var closest = cohesionManager.groupMembers
  358. .OrderBy(charac => Vector3.Distance(monster.transform.position, new Vector3(charac.gridX, 0, charac.gridY)))
  359. .FirstOrDefault();
  360. if (closest == null)
  361. {
  362. Debug.LogWarning("[Combat] No valid target found!");
  363. continue;
  364. }
  365. Debug.Log($"[Combat] Target: {closest.characterName}");
  366. // Apply damage to the character
  367. var uiController = UIUpdater.Instance?.GetUIForCharacterByName(closest.characterName);
  368. if (uiController != null)
  369. {
  370. int damage = monster.data.attackDamage;
  371. closest.currentHP -= damage;
  372. closest.currentHP = Mathf.Max(0, closest.currentHP);
  373. uiController.UpdateHPBar();
  374. uiController.ShowDamageOnCard(damage);
  375. Debug.Log($"[Combat] {monster.name} dealt {damage} damage to {closest.characterName} ({closest.currentHP}/{closest.maxHP} HP remaining)");
  376. // Check if character died
  377. if (closest.currentHP <= 0)
  378. {
  379. Debug.Log($"[Combat] {closest.characterName} has been defeated!");
  380. uiController.HandleCharacterDeath();
  381. }
  382. }
  383. else
  384. {
  385. Debug.LogWarning($"[Combat] UI not found for {closest.characterName}");
  386. }
  387. yield return new WaitForSeconds(attackInterval);
  388. }
  389. }
  390. yield return new WaitForSeconds(attackRepeatDelay);
  391. Debug.Log("[MonsterGroup] Starting next attack round...");
  392. }
  393. }
  394. MonsterController GetClosestBacklinerTo(Vector3 position)
  395. {
  396. MonsterController closest = null;
  397. float minDist = float.MaxValue;
  398. foreach (var b in backRow)
  399. {
  400. float d = Vector3.Distance(b.transform.position, position);
  401. if (d < minDist)
  402. {
  403. minDist = d;
  404. closest = b;
  405. }
  406. }
  407. return closest;
  408. }
  409. bool IsAdjacentToPlayer()
  410. {
  411. foreach (var monster in frontRow.Concat(backRow))
  412. {
  413. if (Vector3.Distance(monster.transform.position, player.position) <= cellSize + 0.5f)
  414. return true;
  415. }
  416. return false;
  417. }
  418. Vector3 RotateOffset(Vector3 offset, MonsterData.Orientation direction)
  419. {
  420. switch (direction)
  421. {
  422. case MonsterData.Orientation.North: return offset;
  423. case MonsterData.Orientation.South: return new Vector3(-offset.x, 0, -offset.z);
  424. case MonsterData.Orientation.East: return new Vector3(offset.z, 0, -offset.x);
  425. case MonsterData.Orientation.West: return new Vector3(-offset.z, 0, offset.x);
  426. default: return offset;
  427. }
  428. }
  429. Quaternion GetRotationFromOrientation(MonsterData.Orientation orientation)
  430. {
  431. switch (orientation)
  432. {
  433. case MonsterData.Orientation.North: return Quaternion.Euler(0, 0, 0);
  434. case MonsterData.Orientation.South: return Quaternion.Euler(0, 180, 0);
  435. case MonsterData.Orientation.East: return Quaternion.Euler(0, 90, 0);
  436. case MonsterData.Orientation.West: return Quaternion.Euler(0, 270, 0);
  437. default: return Quaternion.identity;
  438. }
  439. }
  440. }