// // CombatManager.cs // // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using NosSmooth.Core.Client; using NosSmooth.Core.Commands.Attack; using NosSmooth.Core.Stateful; using NosSmooth.Extensions.Combat.Errors; using NosSmooth.Extensions.Combat.Operations; using NosSmooth.Extensions.Combat.Techniques; using Remora.Results; namespace NosSmooth.Extensions.Combat; /// /// The combat manager that uses techniques to attack enemies. /// public class CombatManager : IStatefulEntity { private readonly List _tokenSource; private readonly Semaphore _semaphore; private readonly INostaleClient _client; private readonly Game.Game _game; /// /// Initializes a new instance of the class. /// /// The NosTale client. /// The game. public CombatManager(INostaleClient client, Game.Game game) { _semaphore = new Semaphore(1, 1); _tokenSource = new List(); _client = client; _game = game; } /// /// Enter into a combat state using the given technique. /// /// The technique to use. /// The cancellation token for cancelling the operation. /// A result that may or may not succeed. public async Task EnterCombatAsync(ICombatTechnique technique, CancellationToken ct = default) { var combatState = new CombatState(_client, _game, this); long? currentTarget = null; long? previousTarget = null; while (!combatState.ShouldQuit) { var commandResult = await _client.SendCommandAsync ( new AttackCommand ( currentTarget, async (c) => { while (!combatState.ShouldQuit && currentTarget == previousTarget) { if (!technique.ShouldContinue(combatState)) { combatState.QuitCombat(); continue; } var operation = combatState.NextOperation(); if (operation is null) { // The operation is null and the step has to be obtained from the technique. var stepResult = technique.HandleCombatStep(combatState); if (!stepResult.IsSuccess) { return Result.FromError(stepResult); } previousTarget = currentTarget; currentTarget = stepResult.Entity; operation = combatState.NextOperation(); } if (operation is null) { // The operation could be null just because there is currently not a skill to be used etc. await Task.Delay(5, ct); continue; } Result responseResult; while ((responseResult = operation.CanBeUsed(combatState)).IsSuccess && responseResult.Entity == CanBeUsedResponse.MustWait) { // TODO: wait for just some amount of time await Task.Delay(5, ct); } if (!responseResult.IsSuccess) { return Result.FromError(responseResult); } if (responseResult.Entity == CanBeUsedResponse.WontBeUsable) { return new UnusableOperationError(operation); } var usageResult = await operation.UseAsync(combatState, ct); if (!usageResult.IsSuccess) { var errorHandleResult = technique.HandleError(combatState, operation, usageResult); if (!errorHandleResult.IsSuccess) { return errorHandleResult; } } } return Result.FromSuccess(); } ), ct ); if (!commandResult.IsSuccess) { return commandResult; } previousTarget = currentTarget; } return Result.FromSuccess(); } /// /// Register the given cancellation token source to be cancelled on skill use/cancel. /// /// The token source to register. public void RegisterSkillCancellationToken(CancellationTokenSource tokenSource) { _semaphore.WaitOne(); _tokenSource.Add(tokenSource); _semaphore.Release(); } /// /// Unregister the given cancellation token registered using . /// /// The token source to unregister. public void UnregisterSkillCancellationToken(CancellationTokenSource tokenSource) { _semaphore.WaitOne(); _tokenSource.Remove(tokenSource); _semaphore.Release(); } /// /// Cancel all of the skill tokens. /// internal void CancelSkillTokens() { _semaphore.WaitOne(); foreach (var tokenSource in _tokenSource) { try { tokenSource.Cancel(); } catch { // ignored } } _tokenSource.Clear(); _semaphore.Release(); } }