442 lines
19 KiB
C#
442 lines
19 KiB
C#
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();
|
|
}
|
|
}
|
|
} |