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 class FightHappening { /* 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, } private class FightersEnterStaging { public required List enteringAllyFighters; public required List enteringEnemyFighters; public bool HasAnyToExecute() { return enteringAllyFighters.Count != 0 || enteringEnemyFighters.Count != 0; } } #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.fighterStack.Current; #endregion #region Events public event Action? transitionFromState; public event Action? transitionState; public event Action? transitionToState; #endregion #region Staging private FightersEnterStaging? _fightersEnterStaging; private FighterAction? _actionStaging; #endregion #region Public Methods public void StartFight() { RequireState(FightState.None); ChangeState(FightState.FightStartAnim); } #endregion #region State Machine private void ChangeState(FightState nextState) { TransitionFromState(); var lastState = HappeningData.fightState; HappeningData.fightState = nextState; TransitionFromToState(nextState, lastState); TransitionToState(nextState); } private void TransitionFromState() { // fixed behaviour switch (HappeningData.fightState) { default: break; } // notify everyone else transitionFromState?.Invoke(HappeningData.fightState); } private void TransitionFromToState(FightState nextState, FightState lastState) { transitionState?.Invoke(lastState, nextState); } private void TransitionToState(FightState nextState) { // notify everyone else transitionToState?.Invoke(nextState); // fixed behaviour switch (HappeningData.fightState) { case FightState.FightStartAnim: AdvanceToStateInSeconds(FightState.FightersEnter, StartAnimationTime); break; case FightState.FightersEnter: _fightersEnterStaging = StageFightersEnter(); if (_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 _actionStaging = null; if ( /*TODO: are all allys dead*/ false) { ChangeState(FightState.EnemyWin); } else if (HappeningData.enemyGroup.AreAllDead()) { ChangeState(FightState.PlayerWin); } else if (CurrentFighter.actionsLeft <= 0) { ChangeState(FightState.FightersEnter); } else if (CurrentFighter.isEnemy) { ChangeState(FightState.EnemyActionSelect); } else { ChangeState(FightState.InputActionSelect); } break; case FightState.InputActionSelect: // wait for player input break; case FightState.ActionCheckDetails: if (ActionAbort()) ChangeState(FightState.InputActionSelect); else if (ActionNeededDetail() != null) ChangeState(FightState.InputActionDetail); else ChangeState(FightState.ActionExecute); break; case FightState.InputActionDetail: // wait for player input break; case FightState.EnemyActionSelect: _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; default: break; } } #endregion #region Game Logic private FightersEnterStaging StageFightersEnter() { // ally var enteringAllyFighters = new List(); //TODO // enemy const int totalEnemySpace = 3; var enemySpaceLeft = totalEnemySpace - HappeningData.enemyGroup.GetEnteredAmount(); var enterEnemyFighters = new List(); for (var i = 0; i < enemySpaceLeft; i++) { if (HappeningData.enemyGroup.TryGetFirstUnenteredFighter(out var fighter)) { enterEnemyFighters.Add(fighter); } } return new FightersEnterStaging { enteringAllyFighters = enteringAllyFighters, enteringEnemyFighters = enterEnemyFighters }; } private void ExecuteFightersEnter() { Debug.Assert(_fightersEnterStaging != null); foreach (var fighter in _fightersEnterStaging.enteringAllyFighters) { fighter.entered = true; HappeningData.fighterStack.AddAsLast(fighter); } foreach (var fighter in _fightersEnterStaging.enteringEnemyFighters) { fighter.entered = true; HappeningData.fighterStack.AddAsLast(fighter); } } private void ExecuteNextFighter() { HappeningData.fighterStack.Next(); } private void ExecuteAction() { Debug.Assert(_actionStaging != null); _actionStaging.ExecuteAction(); } private Variant> GetActionAnimationEnd() { Debug.Assert(_actionStaging != null); return _actionStaging.GetAnimationEnd(); } private bool ActionAbort() { Debug.Assert(_actionStaging != null); return _actionStaging.MarkedForAbort(); } private FighterAction.FighterActionDetail? ActionNeededDetail() { Debug.Assert(_actionStaging != null); return _actionStaging.NeededDetail(); } #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 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 }