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.
Babushka/scripts/CSharp/Common/Fight/FightHappening.cs

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
}