using UnityEngine; using System.Collections; using System.Collections.Generic; using System.Linq; public class MonsterFormationGroup : MonoBehaviour { public MonsterData monsterData; public Vector3 anchorPosition; public float spacing = 1.5f; public float frontRowOffset = 2f; public float cellSize = 5f; public float attackRange = 6f; public float moveDelay = 1f; public float stopAfterDistance = 5f; public float stopDuration = 2f; public float attackInterval = 1.5f; public float attackRepeatDelay = 3f; // Attack range consistency private const float ATTACK_RANGE_BUFFER = 0.5f; private float effectiveAttackRange => attackRange + ATTACK_RANGE_BUFFER; public List frontRow = new(); public List backRow = new(); public List frontRowPositions = new(); private Transform player; public bool isChasing = false; public bool hasDetectedPlayer = false; // Track if player has been detected private float distanceTravelled = 0f; private Coroutine attackLoopCoroutine; private TeamCohesionManager cohesionManager; private void Start() { StartCoroutine(DelayedStart()); } private IEnumerator DelayedStart() { while (UIUpdater.Instance == null) yield return null; // attend que UIUpdater existe player = GameObject.FindWithTag("Player")?.transform; cohesionManager = FindFirstObjectByType(); if (GetComponent() == null) { SphereCollider trigger = gameObject.AddComponent(); trigger.isTrigger = true; trigger.radius = 20f; } } private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) { hasDetectedPlayer = true; if (!isChasing && attackLoopCoroutine == null) { StartChase(); } } } private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) { hasDetectedPlayer = false; if (attackLoopCoroutine != null) { StopCoroutine(attackLoopCoroutine); attackLoopCoroutine = null; } isChasing = false; } } private void OnTriggerStay(Collider other) { if (other.CompareTag("Player")) { hasDetectedPlayer = true; } } public void Spawn() { int total = monsterData.monstersPerGroup; for (int i = 0; i < total; i++) { int row = i < 3 ? 0 : 1; int indexInRow = row == 0 ? i : i - 3; float xOffset = 0f; if (row == 0) { xOffset = (indexInRow - 1) * spacing; } else { if (monsterData.monstersPerGroup == 5 && indexInRow < 2) xOffset = (indexInRow - 0.5f) * spacing; else xOffset = (indexInRow - 1.5f) * spacing; } float zOffset = -row * spacing + (row == 0 ? frontRowOffset : 0); Vector3 offset = new Vector3(xOffset, 0, zOffset); Vector3 rotatedOffset = RotateOffset(offset, monsterData.defaultOrientation); Vector3 spawnPos = anchorPosition + rotatedOffset; Quaternion rotation = GetRotationFromOrientation(monsterData.defaultOrientation); GameObject m = Instantiate(monsterData.prefab, spawnPos, rotation, transform); MonsterController ctrl = m.GetComponent(); ctrl.data = monsterData; ctrl.formationGroup = this; if (row == 0) { frontRow.Add(ctrl); frontRowPositions.Add(spawnPos); } else { backRow.Add(ctrl); } } } public void NotifyMonsterDeath(MonsterController dead) { Vector3 deadPosition = dead.transform.position; if (frontRow.Contains(dead)) { int index = frontRow.IndexOf(dead); frontRow.RemoveAt(index); if (backRow.Count > 0) { MonsterController replacement = GetClosestBacklinerTo(deadPosition); backRow.Remove(replacement); frontRow.Insert(index, replacement); StartCoroutine(MoveReplacementWithDelay(replacement, deadPosition, 2f)); } } else { backRow.Remove(dead); } } IEnumerator MoveReplacementWithDelay(MonsterController replacement, Vector3 destination, float delay) { yield return new WaitForSeconds(delay); replacement.MoveTo(destination); replacement.FaceTarget(player.position); } public void StartChase() { if (!isChasing) { if (UIUpdater.Instance == null || UIUpdater.Instance.IsReady == false) { StartCoroutine(WaitForUIThenChase()); return; } StartCoroutine(ChaseRoutine()); } } IEnumerator WaitForUIThenChase() { while (UIUpdater.Instance == null || UIUpdater.Instance.IsReady == false) { yield return null; } StartCoroutine(ChaseRoutine()); } IEnumerator ChaseRoutine() { isChasing = true; distanceTravelled = 0f; while (isChasing && hasDetectedPlayer) { if (!hasDetectedPlayer) { isChasing = false; yield break; } float distanceToPlayer = Vector3.Distance(transform.position, player.position); if (distanceToPlayer <= attackRange || IsAdjacentToPlayer()) { isChasing = false; attackLoopCoroutine = StartCoroutine(LoopAttack()); yield break; } Vector3 dirToPlayer = (player.position - transform.position).normalized; Vector3 step = new Vector3(Mathf.Round(dirToPlayer.x), 0, Mathf.Round(dirToPlayer.z)) * cellSize; MoveGroupBy(step); distanceTravelled += cellSize + 1f; if (distanceTravelled >= stopAfterDistance) { yield return new WaitForSeconds(stopDuration); distanceTravelled = 0f; } else { yield return new WaitForSeconds(moveDelay); } } isChasing = false; } void MoveGroupBy(Vector3 step) { foreach (var monster in frontRow.Concat(backRow)) { Vector3 targetPos = monster.transform.position + step; monster.MoveTo(targetPos); monster.FaceTarget(player.position); } } private void Update() { if (!hasDetectedPlayer) return; if (player == null) return; if (isChasing || attackLoopCoroutine != null) return; if (!isChasing && attackLoopCoroutine == null) { bool anyMonsterInAttackRange = false; if (frontRow.Count > 0) { foreach (var monster in frontRow) { if (monster != null) { float monsterToPlayerDist = Vector3.Distance(monster.transform.position, player.position); if (monsterToPlayerDist <= effectiveAttackRange) { anyMonsterInAttackRange = true; break; } } } } else { return; } if (anyMonsterInAttackRange) { attackLoopCoroutine = StartCoroutine(LoopAttack()); } else { StartChase(); } } } bool IsPlayerInRange() { return Vector3.Distance(transform.position, player.position) <= attackRange; } IEnumerator LoopAttack() { while (true) { if (GameManager.Instance != null && GameManager.Instance.IsPlayerDead()) { attackLoopCoroutine = null; yield break; } if (!hasDetectedPlayer) { attackLoopCoroutine = null; yield break; } if (cohesionManager == null || cohesionManager.groupMembers.Count == 0) { attackLoopCoroutine = null; yield break; } if (frontRow.Count == 0 && backRow.Count == 0) { attackLoopCoroutine = null; yield break; } bool anyInRange = frontRow.Any(monster => monster != null && Vector3.Distance(monster.transform.position, player.position) <= effectiveAttackRange); if (!anyInRange) { attackLoopCoroutine = null; yield break; } foreach (var monster in frontRow.ToList()) { if (monster == null) continue; if (monsterData.attackType == MonsterData.AttackType.Melee) { float distanceToPlayer = Vector3.Distance(monster.transform.position, player.position); if (distanceToPlayer > effectiveAttackRange) { continue; } monster.StopMove(); monster.FaceTarget(player.position, instant: true); if (!monster.IsFacingTarget(player.position, 45f)) { continue; } monster.Attack(); var closest = cohesionManager.groupMembers .OrderBy(charac => Vector3.Distance(monster.transform.position, new Vector3(charac.gridX, 0, charac.gridY))) .FirstOrDefault(); if (closest == null) continue; var uiController = UIUpdater.Instance?.GetUIForCharacterByName(closest.characterName); if (uiController != null) { int damage = monster.data.attackDamage; closest.currentHP -= damage; closest.currentHP = Mathf.Max(0, closest.currentHP); uiController.UpdateHPBar(); uiController.ShowDamageOnCard(damage); if (closest.currentHP <= 0) { uiController.HandleCharacterDeath(); } } yield return new WaitForSeconds(attackInterval); } } yield return new WaitForSeconds(attackRepeatDelay); } } MonsterController GetClosestBacklinerTo(Vector3 position) { MonsterController closest = null; float minDist = float.MaxValue; foreach (var b in backRow) { float d = Vector3.Distance(b.transform.position, position); if (d < minDist) { minDist = d; closest = b; } } return closest; } bool IsAdjacentToPlayer() { foreach (var monster in frontRow.Concat(backRow)) { if (Vector3.Distance(monster.transform.position, player.position) <= cellSize + 0.5f) return true; } return false; } Vector3 RotateOffset(Vector3 offset, MonsterData.Orientation direction) { switch (direction) { case MonsterData.Orientation.North: return offset; case MonsterData.Orientation.South: return new Vector3(-offset.x, 0, -offset.z); case MonsterData.Orientation.East: return new Vector3(offset.z, 0, -offset.x); case MonsterData.Orientation.West: return new Vector3(-offset.z, 0, offset.x); default: return offset; } } Quaternion GetRotationFromOrientation(MonsterData.Orientation orientation) { switch (orientation) { case MonsterData.Orientation.North: return Quaternion.Euler(0, 0, 0); case MonsterData.Orientation.South: return Quaternion.Euler(0, 180, 0); case MonsterData.Orientation.East: return Quaternion.Euler(0, 90, 0); case MonsterData.Orientation.West: return Quaternion.Euler(0, 270, 0); default: return Quaternion.identity; } } }