//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.GameLogic;
using System.Collections.Concurrent;
using System.Diagnostics;
///
/// A state machine which is dynamically built, based on a .
/// For creation, use the factory method .
///
public sealed class ComboStateMachine : StateMachine
{
private static readonly ConcurrentDictionary StateCache = new();
private readonly TimeSpan _maximumCompletionTime;
private DateTime _lastStartedCombo;
///
/// Initializes a new instance of the class.
///
/// The initial state.
/// The final state.
/// The maximum completion time for combos.
private ComboStateMachine(ComboState initial, ComboState final, TimeSpan maximumCompletionTime)
: base(initial)
{
this._maximumCompletionTime = maximumCompletionTime;
this.InitialState = initial;
this.FinalState = final;
}
///
/// Gets or sets the final state.
/// When this state is achieved, the state machine will proceed to the
/// to handle the next combo attempt.
///
public ComboState FinalState { get; set; }
///
/// Gets or sets the initial state.
///
public ComboState InitialState { get; set; }
///
/// Creates the specified combo definition, based on the .
///
/// The combo definition.
/// The created ..
public static ComboStateMachine Create(SkillComboDefinition comboDefinition)
{
var states = GetOrCreateStates(comboDefinition);
return new ComboStateMachine(states.Initial, states.Final, comboDefinition.MaximumCompletionTime);
}
///
/// Registers the skill to trigger potential state advancements.
///
/// The performed skill.
/// , if the combo completed; otherwise, .
public async ValueTask RegisterSkillAsync(Skill skill)
{
if (DateTime.UtcNow - this._lastStartedCombo > this._maximumCompletionTime)
{
// If it took to long, reset to initial state.
await this.TryAdvanceToAsync(this.InitialState).ConfigureAwait(false);
}
var nextPossibleSkillState = this.CurrentState?.PossibleTransitions?.OfType().FirstOrDefault(t => t.RequiredSkill == skill.GetBaseSkill());
if (nextPossibleSkillState is null)
{
// If it's the wrong skill, reset to initial state.
await this.TryAdvanceToAsync(this.InitialState).ConfigureAwait(false);
return false;
}
if (this.CurrentState == this.InitialState)
{
this._lastStartedCombo = DateTime.UtcNow;
}
await this.TryAdvanceToAsync(nextPossibleSkillState).ConfigureAwait(false);
var canComplete = this.CurrentState?.PossibleTransitions?.Contains(this.FinalState) ?? false;
if (canComplete && await this.TryAdvanceToAsync(this.FinalState).ConfigureAwait(false))
{
await this.TryAdvanceToAsync(this.InitialState).ConfigureAwait(false);
return true;
}
return false;
}
private static (ComboState Initial, ComboState Final) GetOrCreateStates(SkillComboDefinition comboDefinition)
{
return StateCache.GetOrAdd(comboDefinition, BuildStates);
}
private static (ComboState Initial, ComboState Final) BuildStates(SkillComboDefinition comboDefinition)
{
var initialState = new ComboState(Guid.NewGuid(), null) { Name = "Initial", PossibleTransitions = new List() };
var finalState = new ComboState(Guid.NewGuid(), null) { Name = "Finished", PossibleTransitions = new List { initialState } };
var statesPerStep = new Dictionary>();
foreach (var groupedSteps in comboDefinition.Steps.GroupBy(s => s.Order).OrderByDescending(s => s.Key))
{
foreach (var step in groupedSteps)
{
var stepState = new ComboState(Guid.NewGuid(), step.Skill);
stepState.Name = $"Step {step.Order}: {step.Skill?.Name}";
stepState.PossibleTransitions = new List();
stepState.PossibleTransitions.Add(initialState);
if (step.Order == 1)
{
initialState.PossibleTransitions.Add(stepState);
}
if (step.IsFinalStep)
{
stepState.PossibleTransitions.Add(finalState);
}
else
{
if (statesPerStep.TryGetValue(step.Order + 1, out var nextSteps))
{
nextSteps
.OfType()
.Where(p => p.RequiredSkill != step.Skill)
.ForEach(stepState.PossibleTransitions.Add);
}
else
{
Debug.Fail("Inconsistent combo data");
}
}
if (!statesPerStep.TryGetValue(step.Order, out var list))
{
list = new List();
statesPerStep[step.Order] = list;
}
list.Add(stepState);
}
}
return (initialState, finalState);
}
}