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 enteringAllyFighters; public required List 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; RemoveDeadFighters(); 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 (ShouldActionAbort()) ChangeState(FightState.InputActionSelect); else if (DoesActionNeededDetail()) 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()) { 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(); 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> GetActionAnimationEnd() { Debug.Assert(HappeningData.actionStaging != null); return HappeningData.actionStaging.GetAnimationEnd(); } private bool ShouldActionAbort() { Debug.Assert(HappeningData.actionStaging != null); return HappeningData.actionStaging.ShouldAbort(); } private bool DoesActionNeededDetail() { 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"); } private void RemoveDeadFighters() { foreach (var f in HappeningData.fighterTurn) { if (f.IsDead()) { HappeningData.fighterTurn.Remove(f); } } } #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 isDone) { while (!isDone()) { await FightWorld.Instance.ToSignal(FightWorld.Instance.GetTree(), SceneTree.SignalName.ProcessFrame); } ChangeState(nextState); } #endregion }