using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Sirenix.OdinInspector; using Sirenix.Utilities; using UnityEngine; using VOID.Generic.AI; using VOID.Generic.Dict; using VOID.Generic.Navigation; using VOID.Generic.Objects.Abstract; using VOID.Generic.Objects.Abstract.Events; using VOID.Generic.Objects.Unit; using VOID.Generic.Objects.Unit.Events; using VOID.Generic.Player; using VOID.Generic.Util; namespace VOID.Generic.Turn { /// /// Instantiated object that manages turn based combat in given area.
/// - Invokes events onTurn... /// - Automatically add any when in area
///
public class TurnManager : MonoBehaviour { [InfoBox("" + nameof(InitBefore) + " Initializing - still looking for objects in range" + "" + nameof(InitAfter) + " Initializing - found all objects in range" + "" + nameof(Running) + " Active" + "" + nameof(End) + " TurnManager finishing its job")] private enum TurnManagerState { InitBefore, InitAfter, Running, End } // State of this instance of TurnManager for internal purposes [ShowInInspector, ReadOnly] private TurnManagerState _currentState = TurnManagerState.InitBefore; // Participants in Queue - both units and non-units [ShowInInspector, ReadOnly] public List allNonUnitsInQueue { get; private set; } = new(); [ShowInInspector, ReadOnly] public List allUnitsInQueue { get; private set; } = new(); [ShowInInspector, ReadOnly] public List unitsInCurrentQueue { get; private set; } = new(); [ShowInInspector, ReadOnly] public UnitObject currentUnitTurn { get; private set; } // Bounds of TurnManager - everything inside should participate in combat private readonly Dictionary _turnManagerAreas = new(); // Events (for UI purposes) public event Action OnNextTurn; public event Action OnNextQueue; public event Action OnObjectEnter; public event Action OnObjectLeave; public event Action OnCurrentQueueOrderChange; public event Action OnNextQueueOrderChange; // States private bool _isOtherToMerge; public bool isForcedFight { get; private set; } /// /// Starts new TurnManager for given units.
/// - All units in range also will be added to turn combat ///
public static void BuildGameObject(IEnumerable units) { new GameObject(nameof(TurnManager)).AddComponent().Initialize(units); } /// public static void BuildGameObject(UnitObject unit) { BuildGameObject(new[] { unit }); } private void Initialize(IEnumerable units) { units.ForEach(ObjectEnter); StartCoroutine(WaitForFinishInitialize()); } private IEnumerator WaitForFinishInitialize() { // Because check for collision is after fixed update, we need to wait for that fixed update when there // will be no more new obejcts added to turn manager. After that we can start first turn. // If any object found within TurnManager bounds then state changes back to InitBefore in ObjectEnter(). // This process loops untill no more objects can be found! while (_currentState is TurnManagerState.InitBefore) { _currentState = TurnManagerState.InitAfter; yield return new WaitForFixedUpdate(); } // Checks if this turn manager was initialized manually or by meeting enemies // If fight is started manually it can be ended anytime // If fight is started by meeting enemy, then ending is possible only by killing or running isForcedFight = IsForcedFight(); UpdateNavMesh(); _currentState = TurnManagerState.Running; NextTurn(); } private void UpdateNavMesh() { var positions = allUnitsInQueue.Select(unit => unit.transform.position).ToArray(); var bounds = GeometryUtility.CalculateBounds(positions, transform.localToWorldMatrix); NavigationManager.current.UpdateNavMesh(bounds); } /// /// Ends current turn and proceed to start next one.
/// - Ends whole TurnManager if needed
/// - Manages events for units
/// - Restarts queue if all units had their turns ///
public void NextTurn() { // If somehow whole queue is empty - Destroy current turn manager if (allUnitsInQueue.IsNullOrEmpty()) { EndTurnManager(); return; } // Get next unit in queue var previousUnit = currentUnitTurn; unitsInCurrentQueue.Remove(previousUnit); currentUnitTurn = unitsInCurrentQueue.FirstOrDefault(); // Nobody did their turn - usually when starting new turn manager if (previousUnit) { var turnEvent = new TurnEvent { obj = previousUnit, turnManager = previousUnit.basicState.turnManager }; previousUnit.basicEvents.onTurnSelfEnd.Invoke(turnEvent); } // That was last unit in current queue, start next queue if (!currentUnitTurn) { NextQueue(); currentUnitTurn = unitsInCurrentQueue.First(); } OnNextTurn?.Invoke(); currentUnitTurn.basicEvents.onTurnSelfStart.Invoke(new TurnEvent { obj = currentUnitTurn, turnManager = this }); UpdateNavMesh(); StartCoroutine(SkipTurnIfNeeded()); } /// /// Skips 's turn if it cant end itself. /// Usually that means that its not controlled by Player nor AI. /// private IEnumerator SkipTurnIfNeeded() { if (!currentUnitTurn) yield break; var isPlayerUnit = currentUnitTurn == PlayerController.current.mainUnit || currentUnitTurn == PlayerController.current.subUnit; var isAIUnit = currentUnitTurn.GetComponent(); if (isPlayerUnit || isAIUnit) yield break; yield return new WaitForSeconds(0.5f); NextTurn(); } /// /// Starts next Queue for this and invokes events for all participants. /// private void NextQueue() { allNonUnitsInQueue.ForEach(obj => { var turnEvent = new TurnEvent { obj = obj, turnManager = this }; obj.basicEvents.onTurnSelfStart.Invoke(turnEvent); obj.basicEvents.onTurnSelfEnd.Invoke(turnEvent); obj.basicEvents.onTurnQueueStart.Invoke(turnEvent); obj.basicEvents.onTurnQueueEnd.Invoke(turnEvent); }); allUnitsInQueue.ForEach(unit => { var turnEvent = new TurnEvent { obj = unit, turnManager = this }; unit.basicEvents.onTurnQueueStart.Invoke(turnEvent); unit.basicEvents.onTurnQueueEnd.Invoke(turnEvent); }); unitsInCurrentQueue = allUnitsInQueue.ToList(); OnNextQueue?.Invoke(); } /// /// Try to end this TurnManager if possible. /// If participing units are hostile to each other then ending is not possible. /// public void EndTurnManager() { if (isForcedFight) return; _currentState = TurnManagerState.End; allNonUnitsInQueue.ForEach(obj => obj.basicEvents.onTurnQueueLeave.Invoke(new TurnEvent { obj = obj, turnManager = this })); allUnitsInQueue.ForEach(unit => unit.basicEvents.onTurnQueueLeave.Invoke(new TurnEvent { obj = unit, turnManager = this })); Destroy(gameObject); } /// /// Checks if there is any units with enemy attitude towards any other unit. /// /// TRUE if anyone is enemy private bool IsForcedFight() { for (var i = 0; i < allUnitsInQueue.Count - 1; i++) for (var j = i + 1; j < allUnitsInQueue.Count; j++) if (allUnitsInQueue[i].unitAttitude.GetAttitudeWith(allUnitsInQueue[j]) is AttitudeType.Hostile or AttitudeType.Cautious) return true; return false; } /// /// Merges two . /// private void MergeWith(TurnManager otherTurnManager) { if (!otherTurnManager) throw new Exception($"{nameof(TurnManager)}: Nothing to merge with!"); if (otherTurnManager == this) throw new Exception($"{nameof(TurnManager)}: Cant merge with itself!"); // First manager that will come here will flag other one for merging, so other one will do nothing if (_isOtherToMerge) return; otherTurnManager._isOtherToMerge = true; // Disable other so it wont process anything more - current one will do it instead otherTurnManager.gameObject.SetActive(false); // Get all units and objects from other turn manager and put them into current one otherTurnManager.unitsInCurrentQueue.Skip(1).ForEach(PutUnitInCurrentQueue); otherTurnManager.allUnitsInQueue.ForEach(PutUnitInQueue); otherTurnManager.allNonUnitsInQueue.ForEach(allNonUnitsInQueue.Add); // After merging two queues, there will be two units with their turn, need to decide who will be first var thisUnit = unitsInCurrentQueue.First(); var otherUnit = otherTurnManager.unitsInCurrentQueue.First(); if (thisUnit.unitData.subAttributes.initiative >= otherUnit.unitData.subAttributes.initiative) { // Other unit is second, need to update other unit unitsInCurrentQueue.Insert(1, otherUnit); otherUnit.OrderStop(); otherUnit.unitState.isSelfTurn = false; currentUnitTurn = unitsInCurrentQueue.First(); } else { // Other unit is first, need to update this unit unitsInCurrentQueue.Insert(0, otherUnit); thisUnit.OrderStop(); thisUnit.unitState.isSelfTurn = false; currentUnitTurn = unitsInCurrentQueue.First(); } // Also we need to inform all participants that their turn manager changed allUnitsInQueue.ForEach(unit => unit.basicEvents.onTurnQueueSwap.Invoke(new TurnEvent { obj = unit, turnManager = this })); allNonUnitsInQueue.ForEach(obj => obj.basicEvents.onTurnQueueSwap.Invoke(new TurnEvent { obj = obj, turnManager = this })); // Move all trigger areas from other to this manager otherTurnManager._turnManagerAreas.Values.ForEach(turnArea => turnArea.ChangeTurnManager(this)); otherTurnManager._turnManagerAreas.ForEach(other => _turnManagerAreas.Add(other.Key, other.Value)); // After merging destroy other turn manager Destroy(otherTurnManager.gameObject, 1f); } /// /// Given object will be added to this TurnManager. /// If this is UnitObject then TurnManagerArea will be expanded. /// Also entering object events will be handled. /// public void ObjectEnter(AbstractObject obj) { // If this object exists in CURRENT turn manager - do nothing if (allNonUnitsInQueue.Contains(obj) || allUnitsInQueue.Contains(obj)) return; // If this object exists in OTHER turn manager - merge with it if (obj.basicState.turnManager) { MergeWith(obj.basicState.turnManager); return; } // For initialize purpose - if any object found, wait for one more fixed update to probably get more if (_currentState is TurnManagerState.InitAfter) _currentState = TurnManagerState.InitBefore; if (obj is UnitObject unit) { PutUnitInCurrentQueue(unit); PutUnitInQueue(unit); CreateNewArea(unit); // Listen to unit's change of initiative - it can change queue order unit.unitEvents.onSubAttributeChange.AddListener(OnInitiativeChange); } else { allNonUnitsInQueue.Add(obj); } OnObjectEnter?.Invoke(obj); // Everytime someone enters fight check if whole turn manager can be ended manually or by killing/running if (_currentState is TurnManagerState.InitAfter && !isForcedFight) isForcedFight = !IsForcedFight(); obj.basicEvents.onTurnQueueEnter.Invoke(new TurnEvent { obj = obj, turnManager = this }); } private void OnInitiativeChange(SubAttributeChangeEvent subAttributeChangeEvent) { // We care only about initiative - it defines queue order if (subAttributeChangeEvent.type is not SubAttributeType.Initiative) return; // Even after initiative change there is possibility that someone else will have identical value... var beforeUnit = subAttributeChangeEvent.changedValue > 0 // ...when unit LOST initiative and someone else have same value then place it BEFORE them ? allUnitsInQueue.FirstOrDefault(unit => unit.unitData.subAttributes.initiative <= subAttributeChangeEvent.newValue) // ...when unit GAIN initiative and someone else have same value then place it AFTER them : allUnitsInQueue.FirstOrDefault(unit => unit.unitData.subAttributes.initiative < subAttributeChangeEvent.newValue); // NEXT QUEUE // Update unit's position in NEXT queue var nextQueueIndex = beforeUnit ? allUnitsInQueue.IndexOf(beforeUnit) : allUnitsInQueue.Count - 1; allUnitsInQueue.Remove(subAttributeChangeEvent.unit); allUnitsInQueue.Insert(nextQueueIndex, subAttributeChangeEvent.unit); OnNextQueueOrderChange?.Invoke(subAttributeChangeEvent.unit, nextQueueIndex); // That unit already done or is doing his turn - too late to update current queue if (!unitsInCurrentQueue.Contains(subAttributeChangeEvent.unit)) return; if (currentUnitTurn == subAttributeChangeEvent.unit) return; // CURRENT QUEUE // Update unit's position in CURRENT queue - never disturb turn of unit who currently do his turn var currentQueueIndex = beforeUnit ? Mathf.Max(unitsInCurrentQueue.IndexOf(beforeUnit), 1) : unitsInCurrentQueue.Count - 1; unitsInCurrentQueue.Remove(subAttributeChangeEvent.unit); unitsInCurrentQueue.Insert(currentQueueIndex, subAttributeChangeEvent.unit); OnCurrentQueueOrderChange?.Invoke(subAttributeChangeEvent.unit, currentQueueIndex); } private void PutUnitInCurrentQueue(UnitObject unit) { var currentQueueIndex = unitsInCurrentQueue.FindIndex(other => other.unitData.subAttributes.initiative < unit.unitData.subAttributes.initiative); if (currentQueueIndex is - 1) currentQueueIndex = unitsInCurrentQueue.Count; else if (currentQueueIndex is 0 && _currentState is not TurnManagerState.InitBefore) currentQueueIndex = 1; unitsInCurrentQueue.Insert(currentQueueIndex, unit); } private void PutUnitInQueue(UnitObject unit) { var queueIndex = allUnitsInQueue.FindIndex(other => other.unitData.subAttributes.initiative < unit.unitData.subAttributes.initiative); if (queueIndex is - 1) queueIndex = allUnitsInQueue.Count; allUnitsInQueue.Insert(queueIndex, unit); } private void CreateNewArea(UnitObject unit) { var range = unit.unitData.subAttributes.sight; _turnManagerAreas.Add(unit, TurnManagerArea.BuildGameObject(this, unit.transform, range)); } /// /// Given object will be taken out of this TurnManager. /// - TurnManagerArea attached to that unit will be removed. /// - Leaving object events will be handled. /// - Checks if this fight can be ended /// - If given unit had current turn then its turn will be skipped /// public void ObjectExit(AbstractObject obj) { obj.basicEvents.onTurnQueueLeave.Invoke(new TurnEvent { obj = obj, turnManager = this }); OnObjectLeave?.Invoke(obj); if (obj is UnitObject unit) { unitsInCurrentQueue.Remove(unit); allUnitsInQueue.Remove(unit); // Destroy turn collider attached to unit _turnManagerAreas[unit].gameObject.SetActive(false); Destroy(_turnManagerAreas[unit].gameObject); _turnManagerAreas.Remove(unit); // Stop listining to this unit unit.unitEvents.onSubAttributeChange.AddListener(OnInitiativeChange); // If fight was forced THEN check if there are units still willing to fight - if not end whole fight if (isForcedFight && !IsForcedFight()) { isForcedFight = false; EndTurnManager(); return; } if (unit == currentUnitTurn) { currentUnitTurn = null; NextTurn(); } } else { allNonUnitsInQueue.Remove(obj); } } public bool CanObjectExit(AbstractObject obj) { if (obj is not UnitObject unit) return true; return !unit.basicState.turnManager.allUnitsInQueue .Where(otherUnit => RangeUtil.IsInRange(unit, otherUnit, otherUnit.unitData.subAttributes.sight * 2)) .Any(otherUnit => otherUnit.unitAttitude.GetAttitudeWith(unit) <= AttitudeType.Cautious); } [Button] private void DEBUG_NEXT_TURN() { NextTurn(); } } }