using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Unity.VisualScripting; using UnityEngine; using UnityEngine.AI; using UnityEngine.Rendering.UI; public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } public List playerCharacters = new List(); public List enemyCharacters = new List(); [Header("Decision Controller")] public PlayerDecisionController playerDecisionController; private Dictionary enemySelectedTargets = new Dictionary(); public bool isContinuousRunActive = false; // Indicates if the game is in continuous run mode private const string ENEMY_LAYER_NAME = "Enemies"; private int enemyLayerMask; private bool isActionExecutionPhaseActive; private bool waitingForAdvanceInput = false; private void Awake() { if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); // Initialize PlayerDecisionController if not set if (playerDecisionController == null) { playerDecisionController = FindFirstObjectByType(); if (playerDecisionController == null) { GameObject pdcGO = new GameObject("PlayerDecisionController"); playerDecisionController = pdcGO.AddComponent(); } } int enemyLayer = LayerMask.NameToLayer(ENEMY_LAYER_NAME); if (enemyLayer == -1) { Debug.LogError($"Layer '{ENEMY_LAYER_NAME}' not found. Please create it in Project Settings > Tags and Layers, and assign your enemy prefabs to it."); // Disable functionality or handle error appropriately } else { enemyLayerMask = 1 << enemyLayer; } } else { Destroy(gameObject); return; // Avoid further initialization if this is not the singleton instance } } internal void StartBattle(List placedPlayerCharacters, List placedEnemyCharacters) { playerCharacters = placedPlayerCharacters; enemyCharacters = placedEnemyCharacters; // Refresh PlayerDecisionController with current player characters if (playerDecisionController != null) { playerDecisionController.RefreshPlayerCharacters(); playerDecisionController.SetEnabled(false); // Disable initially until decision phase } // Start the battle logic here EnemiesDecideAction(); StartCoroutine(MainBattleLoopCoroutine()); } private IEnumerator MainBattleLoopCoroutine() { while (true) // Main battle loop { // --- Player Decision Phase --- Time.timeScale = 1f; // Ensure normal time scale for player decision phase // Check for completed movements before turn starts CheckAndResetCompletedMovements(); // Keep existing actions, only allow new decisions for characters with no action playerDecisionController.UpdateVisualStates(); // Update colors: pink for no action, green for incomplete playerDecisionController.SetEnabled(true); // Show lines for any existing incomplete actions playerDecisionController.ShowActiveActionLines(); // Wait for all players to make their initial decisions (only needed at turn start) while (!playerDecisionController.AllCharactersHaveActions()) { yield return null; // Wait for player input through PlayerDecisionController } // Disable input during execution phase and hide action lines playerDecisionController.SetEnabled(false); playerDecisionController.HideActiveActionLines(); // --- Pause point after decision (if not in continuous run mode) --- if (!isContinuousRunActive) { PauseGame(); yield return new WaitUntil(() => Time.timeScale > 0 || isContinuousRunActive); } if (Time.timeScale == 0 && isContinuousRunActive) { ResumeGame(); } // --- Action execution phase --- // yield return StartCoroutine(ExecutePlayerActionsCoroutine()); yield return StartCoroutine(ExecuteAllActionsSimultaneously()); // TODO: add ement action execution here for a full turn if (BattleUIManager.Instance != null) { BattleUIManager.Instance.SetButtonsInteractable(true); } EnemiesDecideAction(); // Enemies decide their actions after players have executed their actions // --- Post-Actions: Loop or Pause --- if (isContinuousRunActive) { if (Time.timeScale > 0) { yield return new WaitForSeconds(0.5f); // Brief delay if running } } else { PauseGame(); } // Check for battle end conditions if (playerCharacters.Count == 0 || enemyCharacters.Count == 0) { PauseGame(); if (BattleUIManager.Instance != null) { BattleUIManager.Instance.DisableBattleControls(); // Disable UI controls } // Handle battle end logic here (e.g., show results, reset game state, etc.) break; // Exit the loop if battle is over } } } public List GetAllCharacters() { return playerCharacters.Concat(enemyCharacters).ToList(); } private IEnumerator ExecuteAllActionsSimultaneously() { isActionExecutionPhaseActive = true; var allActions = new Dictionary(); var movementActions = new Dictionary(); // Collect player actions from Character components foreach (GameObject playerGO in playerCharacters) { Character character = playerGO.GetComponent(); if (character != null && character.actionData.hasTarget) { if (character.actionData.targetEnemy != null) { allActions[playerGO] = character.actionData.targetEnemy; } else if (character.actionData.targetPosition != Vector3.zero) { movementActions[playerGO] = character.actionData.targetPosition; } } } foreach (var action in enemySelectedTargets) { allActions[action.Key] = action.Value; } int playerActionsCount = allActions.Count - enemySelectedTargets.Count + movementActions.Count; enemySelectedTargets.Clear(); // Clear enemy targets as well if (allActions.Count == 0 && movementActions.Count == 0) { isActionExecutionPhaseActive = false; yield break; } // Configure All NavMeshAgents before starting movement foreach (var action in allActions) { GameObject actor = action.Key; if (actor == null) { continue; } Character character = actor.GetComponent(); float attackRange = character != null ? character.GetAttackRange() : 2.0f; NavMeshAgent agent = actor.GetComponent(); if (agent != null) { // For melee weapons (range <= 2), get much closer. For ranged, stay back more. float stoppingDistance = attackRange <= 2 ? 0.2f : Mathf.Max(0.5f, attackRange - 1.0f); agent.stoppingDistance = stoppingDistance; // agent.obstacleAvoidanceType = ObstacleAvoidanceType.HighQualityObstacleAvoidance; agent.avoidancePriority = UnityEngine.Random.Range(0, 100); // Start movement GameObject target = action.Value; if (target != null) { agent.SetDestination(target.transform.position); agent.isStopped = false; } } else { Debug.LogWarning($"NavMeshAgent not found or disabled on {actor.name}. Cannot move towards target."); } } // Handle pure movement actions (no enemy target) foreach (var movement in movementActions) { GameObject playerGO = movement.Key; Vector3 targetPosition = movement.Value; NavMeshAgent agent = playerGO.GetComponent(); if (agent != null) { agent.SetDestination(targetPosition); agent.isStopped = false; } } // handle movement duration based on mode // if (allActions.Count > 0 || movementActions.Count > 0) { yield return StartCoroutine(MovementPhaseWithTargetTracking(allActions, movementActions, 1.0f)); } // else // { // // If there are only attacks (no movement), still wait for 1 second // yield return new WaitForSeconds(1.0f); // } if (!isContinuousRunActive) { // // // Check for completed movement actions before showing lines CheckAndResetCompletedMovements(); // Enable input and show lines for actions during pause - allow re-picking actions playerDecisionController.UpdateVisualStates(); // Update colors playerDecisionController.SetEnabled(true); // Allow action changes playerDecisionController.ShowActiveActionLines(); PauseGame(); waitingForAdvanceInput = true; yield return new WaitUntil(() => !waitingForAdvanceInput); // Disable input when advancing playerDecisionController.SetEnabled(false); ResumeGame(); yield return new WaitForSeconds(1.0f); } foreach (var action in allActions) { GameObject actor = action.Key; GameObject target = action.Value; if (actor == null || target == null) continue; Character character = actor.GetComponent(); float attackRange = character != null ? character.GetAttackRange() : 2.0f; float distance = Vector3.Distance(actor.transform.position, target.transform.position); if (distance <= attackRange) { // Execute the character's attack action character.ExecuteAction(); } else { } } // Check movement completion and reset action if reached (movement actions are one-time) foreach (var movement in movementActions) { GameObject actor = movement.Key; Vector3 targetPosition = movement.Value; if (actor == null) continue; Character character = actor.GetComponent(); if (character != null) { // Use horizontal distance only, ignore Y differences Vector3 actorPos = actor.transform.position; Vector2 actorPos2D = new Vector2(actorPos.x, actorPos.z); Vector2 targetPos2D = new Vector2(targetPosition.x, targetPosition.z); float horizontalDistance = Vector2.Distance(actorPos2D, targetPos2D); if (horizontalDistance <= 1.2f) // Movement completion threshold { character.actionData.Reset(); character.SetVisualState(ActionDecisionState.NoAction); } } } isActionExecutionPhaseActive = false; BattleUIManager.Instance.SetButtonsInteractable(true); } private IEnumerator MovementPhaseWithTargetTracking(Dictionary allActions, Dictionary movementActions, float movementDuration) { float elapsedTime = 0f; // Store witch agents are still moving (not in attack range) Dictionary activeMovements = new Dictionary(); Dictionary agentAttackRanges = new Dictionary(); // Initialize active movements for attack actions foreach (var action in allActions) { GameObject actor = action.Key; GameObject target = action.Value; if (actor == null || target == null) continue; NavMeshAgent agent = actor.GetComponent(); if (agent != null && agent.enabled) { // Get the character's attack range Character character = actor.GetComponent(); float attackRange = character != null ? character.GetAttackRange() : 2.0f; // Configure agent for proper pathfinding around obstacles agent.obstacleAvoidanceType = ObstacleAvoidanceType.HighQualityObstacleAvoidance; agent.avoidancePriority = UnityEngine.Random.Range(0, 100); // Random priority to avoid clustering activeMovements[agent] = target; agentAttackRanges[agent] = attackRange; } } // Initialize active movements for pure movement actions foreach (var movement in movementActions) { GameObject actor = movement.Key; Vector3 targetPosition = movement.Value; if (actor == null) continue; NavMeshAgent agent = actor.GetComponent(); if (agent != null && agent.enabled) { // For movement actions, use a small stopping distance to get closer float arrivalRange = 1.2f; // Configure agent for proper pathfinding around obstacles agent.obstacleAvoidanceType = ObstacleAvoidanceType.HighQualityObstacleAvoidance; agent.avoidancePriority = UnityEngine.Random.Range(0, 100); agent.stoppingDistance = 0.1f; // Get very close to the target // Create a dummy target GameObject at the position for consistency with the tracking system GameObject dummyTarget = new GameObject("MovementTarget"); dummyTarget.transform.position = targetPosition; activeMovements[agent] = dummyTarget; agentAttackRanges[agent] = arrivalRange; } } while (elapsedTime < movementDuration && Time.timeScale > 0 && activeMovements.Count > 0) { var agentsToRemove = new List(); foreach (var movement in activeMovements) { NavMeshAgent agent = movement.Key; GameObject target = movement.Value; float attackRange = agentAttackRanges[agent]; if (agent == null || target == null || agent.gameObject == null) { agentsToRemove.Add(agent); continue; } float distanceToTarget = Vector3.Distance(agent.transform.position, target.transform.position); if (distanceToTarget <= attackRange) { agent.isStopped = true; agent.velocity = Vector3.zero; agentsToRemove.Add(agent); // If this is a movement target (dummy), check if we should reset the action if (target.name == "MovementTarget") { Character character = agent.GetComponent(); if (character != null && character.actionData.targetPosition != Vector3.zero) { character.actionData.Reset(); character.SetVisualState(ActionDecisionState.NoAction); } } } else if (!agent.isStopped) { // For movement targets, go directly to the position if (target.name == "MovementTarget") { agent.SetDestination(target.transform.position); // Check if agent has reached destination or is very close but stopped moving if (agent.pathStatus == NavMeshPathStatus.PathComplete && (!agent.hasPath || agent.remainingDistance < 0.1f)) { Character character = agent.GetComponent(); if (character != null) { Vector3 actorPos = agent.transform.position; Vector3 targetPos = character.actionData.targetPosition; Vector2 actorPos2D = new Vector2(actorPos.x, actorPos.z); Vector2 targetPos2D = new Vector2(targetPos.x, targetPos.z); float horizontalDistance = Vector2.Distance(actorPos2D, targetPos2D); if (horizontalDistance <= 1.2f) { agent.isStopped = true; agentsToRemove.Add(agent); character.actionData.Reset(); character.SetVisualState(ActionDecisionState.NoAction); } } } } else { // Update destination to track moving target, but stop just outside attack range Vector3 directionToTarget = (target.transform.position - agent.transform.position).normalized; Vector3 stoppingPosition = target.transform.position - directionToTarget * (attackRange * 0.9f); agent.SetDestination(stoppingPosition); } // Check if agent is stuck or can't reach destination if (agent.pathStatus == NavMeshPathStatus.PathInvalid) { Debug.LogWarning($"{agent.gameObject.name} cannot find path to {target.name}"); agent.SetDestination(target.transform.position); } } } foreach (var agent in agentsToRemove) { activeMovements.Remove(agent); agentAttackRanges.Remove(agent); } yield return null; // wait one frame elapsedTime += Time.deltaTime; } // Stop any remaining moving agents and clean up dummy targets foreach (var movement in activeMovements) { if (movement.Key != null && movement.Key.enabled) { movement.Key.isStopped = true; movement.Key.velocity = Vector3.zero; } // Clean up dummy movement targets if (movement.Value != null && movement.Value.name == "MovementTarget") { Destroy(movement.Value); } } } public void AdvanceAction() { // Hide action lines when advancing if (playerDecisionController != null) { playerDecisionController.HideActiveActionLines(); } if (waitingForAdvanceInput) { waitingForAdvanceInput = false; } else if (Time.timeScale == 0 && !isActionExecutionPhaseActive) { ResumeGame(); } else { } } public void PauseGame() { Time.timeScale = 0f; // Pause the game BattleUIManager.Instance.SetButtonsInteractable(true); } public void ResumeGame() { Time.timeScale = 1f; // Resume the game } private void EnemiesDecideAction() { enemySelectedTargets.Clear(); foreach (var enemy in enemyCharacters) { if (enemy == null) { Debug.LogWarning("Found null enemy in enemyCharacters list"); continue; } if (playerCharacters.Count == 0) { Debug.LogWarning("No player characters available for enemies to target"); continue; } // Logic for enemy action decision GameObject closestPlayer = null; float minDistance = float.MaxValue; List alivePlayers = playerCharacters.Where(player => !player.GetComponent().IsDead).ToList(); foreach (var player in alivePlayers) { if (player == null) continue; // Safety check float distance = Vector3.Distance(enemy.transform.position, player.transform.position); if (distance < minDistance) { minDistance = distance; closestPlayer = player; } } if (closestPlayer != null) { // Set the enemy's action data to attack the target Character enemyCharacter = enemy.GetComponent(); if (enemyCharacter != null) { enemyCharacter.actionData.SetAttackTarget(closestPlayer); } enemySelectedTargets[enemy] = closestPlayer; // Assign the closest player as the target } else { Debug.LogWarning($"{enemy.name} could not find a valid target"); } enemy.GetComponent().SetVisualState(ActionDecisionState.EnemyAction); } } private void CheckAndResetCompletedMovements() { foreach (GameObject playerGO in playerCharacters) { Character character = playerGO.GetComponent(); if (character != null && character.actionData.targetPosition != Vector3.zero && character.actionData.targetEnemy == null) { // This is a movement action, check if completed using horizontal distance only Vector3 playerPos = playerGO.transform.position; Vector3 targetPos = character.actionData.targetPosition; // Use only X and Z coordinates (ignore Y differences) Vector2 playerPos2D = new Vector2(playerPos.x, playerPos.z); Vector2 targetPos2D = new Vector2(targetPos.x, targetPos.z); float horizontalDistance = Vector2.Distance(playerPos2D, targetPos2D); if (horizontalDistance <= 1.2f) // Threshold for "close enough" { character.actionData.Reset(); character.SetVisualState(ActionDecisionState.NoAction); } } } } }