This commit is contained in:
2026-07-09 21:33:33 +02:00
commit 84a2a365f3
2364 changed files with 950134 additions and 0 deletions
@@ -0,0 +1,442 @@
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
{
/// <summary>
/// Instantiated object that manages turn based combat in given area.<br/>
/// - Invokes <see cref="AbstractObjectEvents"/> events <b>onTurn...</b>
/// - Automatically add any <see cref="AbstractObject"/> when in area<br/>
/// </summary>
public class TurnManager : MonoBehaviour
{
[InfoBox("<u><b>" + nameof(InitBefore) + "</b></u> Initializing - still looking for objects in range" +
"<u><b>" + nameof(InitAfter) + "</b></u> Initializing - found all objects in range" +
"<u><b>" + nameof(Running) + "</b></u> Active" +
"<u><b>" + nameof(End) + "</b></u> 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<AbstractObject> allNonUnitsInQueue { get; private set; } = new();
[ShowInInspector, ReadOnly] public List<UnitObject> allUnitsInQueue { get; private set; } = new();
[ShowInInspector, ReadOnly] public List<UnitObject> 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<UnitObject, TurnManagerArea> _turnManagerAreas = new();
// Events (for UI purposes)
public event Action OnNextTurn;
public event Action OnNextQueue;
public event Action<AbstractObject> OnObjectEnter;
public event Action<AbstractObject> OnObjectLeave;
public event Action<UnitObject, int> OnCurrentQueueOrderChange;
public event Action<UnitObject, int> OnNextQueueOrderChange;
// States
private bool _isOtherToMerge;
public bool isForcedFight { get; private set; }
/// <summary>
/// Starts new TurnManager for given units.<br/>
/// - All units in range also will be added to turn combat
/// </summary>
public static void BuildGameObject(IEnumerable<UnitObject> units)
{
new GameObject(nameof(TurnManager)).AddComponent<TurnManager>().Initialize(units);
}
/// <inheritdoc cref="BuildGameObject(IEnumerable{UnitObject})"/>
public static void BuildGameObject(UnitObject unit)
{
BuildGameObject(new[] { unit });
}
private void Initialize(IEnumerable<UnitObject> 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);
}
/// <summary>
/// Ends current turn and proceed to start next one.<br/>
/// - Ends whole TurnManager if needed<br/>
/// - Manages events for units<br/>
/// - Restarts queue if all units had their turns
/// </summary>
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());
}
/// <summary>
/// Skips <see cref="UnitObject"/>'s turn if it cant end itself.
/// Usually that means that its not controlled by Player nor AI.
/// </summary>
private IEnumerator SkipTurnIfNeeded()
{
if (!currentUnitTurn) yield break;
var isPlayerUnit = currentUnitTurn == PlayerController.current.mainUnit || currentUnitTurn == PlayerController.current.subUnit;
var isAIUnit = currentUnitTurn.GetComponent<UnitAI>();
if (isPlayerUnit || isAIUnit) yield break;
yield return new WaitForSeconds(0.5f);
NextTurn();
}
/// <summary>
/// Starts next Queue for this <see cref="TurnManager"/> and invokes events for all participants.
/// </summary>
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();
}
/// <summary>
/// Try to end this TurnManager if possible.
/// If participing units are hostile to each other then ending is not possible.
/// </summary>
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);
}
/// <summary>
/// Checks if there is any units with enemy attitude towards any other unit.
/// </summary>
/// <returns>TRUE if anyone is enemy</returns>
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;
}
/// <summary>
/// Merges two <see cref="TurnManager"/>.
/// </summary>
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);
}
/// <summary>
/// Given object will be added to this TurnManager.
/// If this is UnitObject then TurnManagerArea will be expanded.
/// Also entering object events will be handled.
/// </summary>
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));
}
/// <summary>
/// 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
/// </summary>
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();
}
}
}