init
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user