You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
386 lines
12 KiB
386 lines
12 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Runtime.ExceptionServices;
|
|
using System.Threading.Tasks;
|
|
using Babushka.scripts.CSharp.Common.Util;
|
|
using Godot;
|
|
|
|
namespace Babushka.scripts.CSharp.Common.Fight;
|
|
|
|
public partial class FightHappening : Node
|
|
{
|
|
/*
|
|
To get a visual overview of the FightHappening state machine, refer to the graph on miro:
|
|
https://miro.com/app/board/uXjVK8YEprM=/?moveToWidget=3458764640805655262&cot=14
|
|
*/
|
|
|
|
#region Internal Types
|
|
|
|
public enum FightState
|
|
{
|
|
None,
|
|
FightStartAnim,
|
|
FightersEnter,
|
|
FightersEnterAnim,
|
|
NextFighter,
|
|
StateCheck,
|
|
InputActionSelect,
|
|
ActionCheckDetails,
|
|
InputActionDetail,
|
|
ActionExecute,
|
|
ActionAnim,
|
|
EnemyActionSelect,
|
|
PlayerWin,
|
|
EnemyWin,
|
|
}
|
|
|
|
public class FightersEnterStaging
|
|
{
|
|
public required List<FightWorld.Fighter> enteringAllyFighters;
|
|
public required List<FightWorld.Fighter> enteringEnemyFighters;
|
|
|
|
public bool HasAnyToExecute()
|
|
{
|
|
return enteringAllyFighters.Any() || enteringEnemyFighters.Any();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Settings
|
|
|
|
private const float StartAnimationTime = 1;
|
|
private const float FightersEnterAnimationTime = 1;
|
|
|
|
#endregion
|
|
|
|
#region Shortcuts
|
|
|
|
private static FightWorld.FightHappeningData HappeningData =>
|
|
FightWorld.Instance.fightHappeningData ?? throw new NoFightHappeningException();
|
|
|
|
private static FightWorld.Fighter CurrentFighter => HappeningData.fighterTurn.Current;
|
|
|
|
#endregion
|
|
|
|
#region Events
|
|
|
|
[Signal]
|
|
public delegate void SignalTransitionFromStateEventHandler(FightState state);
|
|
|
|
[Signal]
|
|
public delegate void SignalTransitionStateEventHandler(FightState from, FightState to);
|
|
|
|
[Signal]
|
|
public delegate void SignalTransitionToStateEventHandler(FightState state);
|
|
|
|
#endregion
|
|
|
|
#region Singleton
|
|
|
|
public static FightHappening Instance = null!;
|
|
|
|
private void SetupInstance()
|
|
{
|
|
Instance = this;
|
|
}
|
|
|
|
#endregion
|
|
|
|
public override void _Ready()
|
|
{
|
|
SetupInstance();
|
|
StartFight();
|
|
}
|
|
|
|
#region Public Methods
|
|
|
|
public void StartFight()
|
|
{
|
|
RequireState(FightState.None);
|
|
ChangeState(FightState.FightStartAnim);
|
|
}
|
|
|
|
public void ActionSelect(FighterAction action)
|
|
{
|
|
RequireState(FightState.InputActionSelect);
|
|
HappeningData.actionStaging = action;
|
|
action.Reset();
|
|
ChangeState(FightState.ActionCheckDetails);
|
|
}
|
|
|
|
public void DetailFilled()
|
|
{
|
|
RequireState(FightState.InputActionDetail);
|
|
ChangeState(FightState.ActionCheckDetails);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region State Machine
|
|
|
|
private bool _inTransition = false;
|
|
private FightState? _changeToAfterTransition = null;
|
|
|
|
private void ChangeState(FightState nextState)
|
|
{
|
|
_changeToAfterTransition = null;
|
|
if (_inTransition)
|
|
{
|
|
_changeToAfterTransition = nextState;
|
|
return;
|
|
}
|
|
|
|
_inTransition = true;
|
|
TransitionFromState();
|
|
var lastState = HappeningData.fightState;
|
|
HappeningData.fightState = nextState;
|
|
TransitionToState(nextState);
|
|
|
|
EmitSignalSignalTransitionFromState(lastState);
|
|
EmitSignalSignalTransitionState(lastState, nextState);
|
|
EmitSignalSignalTransitionToState(nextState);
|
|
_inTransition = false;
|
|
|
|
if (_changeToAfterTransition.HasValue)
|
|
{
|
|
ChangeState(_changeToAfterTransition.Value);
|
|
}
|
|
}
|
|
|
|
private void TransitionFromState()
|
|
{
|
|
// fixed behaviour
|
|
switch (HappeningData.fightState)
|
|
{
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
private void TransitionToState(FightState nextState)
|
|
{
|
|
// fixed behaviour
|
|
switch (HappeningData.fightState)
|
|
{
|
|
case FightState.FightStartAnim:
|
|
AdvanceToStateInSeconds(FightState.FightersEnter, StartAnimationTime);
|
|
break;
|
|
case FightState.FightersEnter:
|
|
HappeningData.fightersEnterStaging = StageFightersEnter();
|
|
if (HappeningData.fightersEnterStaging.HasAnyToExecute())
|
|
{
|
|
ExecuteFightersEnter();
|
|
ChangeState(FightState.FightersEnterAnim);
|
|
}
|
|
else
|
|
{
|
|
ChangeState(FightState.NextFighter);
|
|
}
|
|
|
|
break;
|
|
case FightState.FightersEnterAnim:
|
|
AdvanceToStateInSeconds(FightState.NextFighter, FightersEnterAnimationTime);
|
|
break;
|
|
case FightState.NextFighter:
|
|
ExecuteNextFighter();
|
|
ChangeState(FightState.StateCheck);
|
|
break;
|
|
case FightState.StateCheck:
|
|
// restest action staging and fighter enter staging
|
|
HappeningData.actionStaging = null;
|
|
HappeningData.fightersEnterStaging = null;
|
|
|
|
if (!FightWorld.Instance.allyFighters.IsAlive())
|
|
{
|
|
ChangeState(FightState.EnemyWin);
|
|
}
|
|
else if (HappeningData.enemyGroup.AreAllDead())
|
|
{
|
|
ChangeState(FightState.PlayerWin);
|
|
}
|
|
else if (CurrentFighter.actionPointsLeft <= 0)
|
|
{
|
|
ChangeState(FightState.FightersEnter);
|
|
}
|
|
else if (CurrentFighter.IsInFormation(HappeningData.enemyFighterFormation))
|
|
{
|
|
ChangeState(FightState.EnemyActionSelect);
|
|
}
|
|
else
|
|
{
|
|
ChangeState(FightState.InputActionSelect);
|
|
}
|
|
|
|
break;
|
|
case FightState.InputActionSelect:
|
|
// wait for player input
|
|
break;
|
|
case FightState.ActionCheckDetails:
|
|
RequireNotNull(HappeningData.actionStaging);
|
|
|
|
if (ActionAbort())
|
|
ChangeState(FightState.InputActionSelect);
|
|
else if (ActionNeededDetail())
|
|
ChangeState(FightState.InputActionDetail);
|
|
else
|
|
ChangeState(FightState.ActionExecute);
|
|
break;
|
|
case FightState.InputActionDetail:
|
|
// wait for player input
|
|
break;
|
|
case FightState.EnemyActionSelect:
|
|
HappeningData.actionStaging = CurrentFighter.AutoSelectAction();
|
|
ChangeState(FightState.ActionExecute);
|
|
break;
|
|
case FightState.ActionExecute:
|
|
ExecuteAction();
|
|
ChangeState(FightState.ActionAnim);
|
|
break;
|
|
case FightState.ActionAnim:
|
|
var actionTime = GetActionAnimationEnd();
|
|
if (actionTime.IsType<float>())
|
|
{
|
|
AdvanceToStateInSeconds(FightState.StateCheck, actionTime);
|
|
}
|
|
else
|
|
{
|
|
_ = AdvanceToStateWhenDone(FightState.StateCheck, actionTime);
|
|
}
|
|
|
|
break;
|
|
case FightState.EnemyWin:
|
|
// TODO: remove and find proper solution
|
|
ReviveVesna();
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Game Logic
|
|
|
|
private FightersEnterStaging StageFightersEnter()
|
|
{
|
|
// ally
|
|
var enteringAllyFighters = new List<FightWorld.Fighter>();
|
|
var allyFighters = FightWorld.Instance.allyFighters;
|
|
if (!allyFighters.vesnaFighter.IsInFormation(HappeningData.allyFighterFormation))
|
|
{
|
|
enteringAllyFighters.Add(allyFighters.vesnaFighter);
|
|
}
|
|
|
|
// enemy
|
|
const int totalEnemySpace = 3;
|
|
var enemySpaceLeft = HappeningData.enemyFighterFormation.GetEmptySlotCount();
|
|
|
|
return new FightersEnterStaging
|
|
{
|
|
enteringAllyFighters = enteringAllyFighters,
|
|
enteringEnemyFighters = HappeningData.enemyGroup.fighters
|
|
.WhereIsAlive()
|
|
.WhereIsNotInFormation(HappeningData.enemyFighterFormation)
|
|
.Take(enemySpaceLeft)
|
|
.ToList()
|
|
};
|
|
}
|
|
|
|
private void ExecuteFightersEnter()
|
|
{
|
|
Debug.Assert(HappeningData.fightersEnterStaging != null);
|
|
foreach (var fighter in HappeningData.fightersEnterStaging.enteringAllyFighters)
|
|
{
|
|
var emptySlotIndex = HappeningData.allyFighterFormation.GetFirstEmptySlot();
|
|
if (emptySlotIndex < 0) throw new Exception("No empty slot for ally fighter to enter");
|
|
HappeningData.allyFighterFormation.SetFighterAtPosition(emptySlotIndex, fighter);
|
|
HappeningData.fighterTurn.AddAsLast(fighter);
|
|
}
|
|
|
|
foreach (var fighter in HappeningData.fightersEnterStaging.enteringEnemyFighters)
|
|
{
|
|
var emptySlotIndex = HappeningData.enemyFighterFormation.GetFirstEmptySlot();
|
|
if (emptySlotIndex < 0) throw new Exception("No empty slot for enemy fighter to enter");
|
|
HappeningData.enemyFighterFormation.SetFighterAtPosition(emptySlotIndex, fighter);
|
|
HappeningData.fighterTurn.AddAsLast(fighter);
|
|
}
|
|
}
|
|
|
|
private void ExecuteNextFighter()
|
|
{
|
|
HappeningData.fighterTurn.Next();
|
|
CurrentFighter.actionPointsLeft = FightWorld.Fighter.MaxActionPoints;
|
|
}
|
|
|
|
private void ExecuteAction()
|
|
{
|
|
Debug.Assert(HappeningData.actionStaging != null);
|
|
HappeningData.actionStaging.ExecuteAction();
|
|
CurrentFighter.actionPointsLeft -= HappeningData.actionStaging.GetActionPointCost();
|
|
}
|
|
|
|
private Variant<float, Func<bool>> GetActionAnimationEnd()
|
|
{
|
|
Debug.Assert(HappeningData.actionStaging != null);
|
|
return HappeningData.actionStaging.GetAnimationEnd();
|
|
}
|
|
|
|
private bool ActionAbort()
|
|
{
|
|
Debug.Assert(HappeningData.actionStaging != null);
|
|
return HappeningData.actionStaging.MarkedForAbort();
|
|
}
|
|
|
|
private bool ActionNeededDetail()
|
|
{
|
|
Debug.Assert(HappeningData.actionStaging != null);
|
|
return HappeningData.actionStaging.NextDetail();
|
|
}
|
|
|
|
// TODO: remove
|
|
private void ReviveVesna()
|
|
{
|
|
var vesnaFighter = FightWorld.Instance.allyFighters.vesnaFighter;
|
|
vesnaFighter.health = vesnaFighter.maxHealth;
|
|
GD.Print("Vesna has been revived. This is for the current prototype only");
|
|
}
|
|
|
|
#endregion // Game Logic
|
|
|
|
#region Utility
|
|
|
|
private void RequireState(params FightState[] states)
|
|
{
|
|
if (states.Contains(HappeningData.fightState))
|
|
return;
|
|
|
|
throw new Exception(
|
|
$"Can not call this Method while in state {HappeningData.fightState}. Only available in {string.Join(" ,", states)}");
|
|
}
|
|
|
|
private void RequireNotNull(Object? o)
|
|
{
|
|
if (o != null)
|
|
return;
|
|
|
|
throw new Exception("Object must not be null to call this method");
|
|
}
|
|
|
|
private void AdvanceToStateInSeconds(FightState nextState, float seconds)
|
|
{
|
|
FightWorld.Instance.GetTree().CreateTimer(seconds).Timeout += () => ChangeState(nextState);
|
|
}
|
|
|
|
private async Task AdvanceToStateWhenDone(FightState nextState, Func<bool> isDone)
|
|
{
|
|
while (!isDone())
|
|
{
|
|
await FightWorld.Instance.ToSignal(FightWorld.Instance.GetTree(), SceneTree.SignalName.ProcessFrame);
|
|
}
|
|
|
|
ChangeState(nextState);
|
|
}
|
|
|
|
#endregion
|
|
} |