//
// 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 System.Diagnostics;
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;
/// <summary>
/// The combat manager that uses techniques to attack enemies.
/// </summary>
public class CombatManager : IStatefulEntity
{
private readonly ManagedNostaleClient _client;
private readonly Game.Game _game;
/// <summary>
/// Initializes a new instance of the <see cref="CombatManager"/> class.
/// </summary>
/// <param name="client">The NosTale client.</param>
/// <param name="game">The game.</param>
public CombatManager(ManagedNostaleClient client, Game.Game game)
{
_client = client;
_game = game;
}
/// <summary>
/// Enter into a combat state using the given technique.
/// </summary>
/// <param name="technique">The technique to use.</param>
/// <param name="ct">The cancellation token for cancelling the operation.</param>
/// <returns>A result that may or may not succeed.</returns>
public async Task<Result> EnterCombatAsync(ICombatTechnique technique, CancellationToken ct = default)
{
var combatState = new CombatState(_client, _game, this);
long? currentTarget = null;
long? previousTarget = null;
while (!(combatState.ShouldQuit && combatState.CanQuit) && !ct.IsCancellationRequested)
{
var commandResult = await _client.SendCommandAsync
(
new AttackCommand
(
currentTarget,
async (ct) =>
{
while (!(combatState.ShouldQuit && combatState.CanQuit) && currentTarget == previousTarget)
{
var iterationResult = await HandleAttackIterationAsync(combatState, technique, ct);
if (!iterationResult.IsSuccess)
{
var errorResult = technique.HandleError(combatState, Result.FromError(iterationResult));
if (!errorResult.IsSuccess)
{ // end the attack.
return errorResult;
}
}
var result = iterationResult.Entity;
if (!result.TargetChanged)
{
continue;
}
previousTarget = currentTarget;
currentTarget = result.TargetId;
}
return Result.FromSuccess();
}
),
ct
);
if (!commandResult.IsSuccess)
{
return commandResult;
}
previousTarget = currentTarget;
}
return Result.FromSuccess();
}
private async Task<Result<(bool TargetChanged, long? TargetId)>> HandleAttackIterationAsync
(CombatState combatState, ICombatTechnique technique, CancellationToken ct)
{
if (!technique.ShouldContinue(combatState))
{
combatState.QuitCombat();
}
// the operations need time for execution and/or
// wait.
await Task.Delay(50, ct);
var tasks = technique.HandlingQueueTypes
.Select(x => HandleTypeIterationAsync(x, combatState, technique, ct))
.ToArray();
var results = await Task.WhenAll(tasks);
var errors = results.Where(x => !x.IsSuccess).Cast<IResult>().ToArray();
return errors.Length switch
{
0 => results.FirstOrDefault
(x => x.Entity.TargetChanged, Result<(bool TargetChanged, long?)>.FromSuccess((false, null))),
1 => (Result<(bool, long?)>)errors[0],
_ => new AggregateError()
};
}
private async Task<Result<(bool TargetChanged, long? TargetId)>> HandleTypeIterationAsync
(
OperationQueueType queueType,
CombatState combatState,
ICombatTechnique technique,
CancellationToken ct
)
{
var currentOperation = combatState.GetCurrentOperation(queueType);
if (currentOperation?.IsFinished() ?? false)
{
var operationResult = await currentOperation.WaitForFinishedAsync(combatState, ct);
currentOperation.Dispose();
if (!operationResult.IsSuccess)
{
return Result<(bool, long?)>.FromError(operationResult);
}
currentOperation = null;
}
if (combatState.ShouldQuit)
{
if (currentOperation is not null && !currentOperation.IsExecuting())
{
currentOperation.Dispose();
currentOperation = null;
}
if (currentOperation is not null && currentOperation.MayBeCancelled)
{
currentOperation.Cancel();
}
}
if (currentOperation is null && !combatState.ShouldQuit)
{ // waiting for an operation.
currentOperation = combatState.NextOperation(queueType);
if (currentOperation is null)
{ // The operation is null and the step has to be obtained from the technique.
var stepResult = technique.HandleNextCombatStep(queueType, combatState);
if (!stepResult.IsSuccess)
{
return Result<(bool, long?)>.FromError(stepResult);
}
return Result<(bool, long?)>.FromSuccess((true, stepResult.Entity));
}
}
if (currentOperation is null)
{ // should quit, do nothing.
return (false, null);
}
if (!currentOperation.IsExecuting())
{ // not executing, check can be used, execute if can.
var canBeUsedResult = currentOperation.CanBeUsed(combatState);
if (canBeUsedResult is { IsSuccess: false, Error: not CannotBeUsedError })
{
return Result<(bool, long?)>.FromError(canBeUsedResult);
}
var canBeUsedError = canBeUsedResult.Error as CannotBeUsedError;
var canBeUsed = canBeUsedError?.Response ?? CanBeUsedResponse.CanBeUsed;
switch (canBeUsed)
{
case CanBeUsedResponse.WontBeUsable:
case CanBeUsedResponse.MustWait:
var waitingResult = technique.HandleWaiting
(queueType, combatState, currentOperation, canBeUsedError!);
if (!waitingResult.IsSuccess)
{
return Result<(bool, long?)>.FromError(waitingResult);
}
return Result<(bool, long?)>.FromSuccess((false, null));
case CanBeUsedResponse.CanBeUsed:
var executingResult = await currentOperation.BeginExecution(combatState, ct);
if (!executingResult.IsSuccess)
{
return Result<(bool, long?)>.FromError(executingResult);
}
break;
}
}
return (false, null);
}
}