~ruther/NosSmooth

aa41cfec89339a8103d5b7dd38e25776c8da3e03 — František Boháček 2 years ago 2043a7d
feat(combat): make waiting return an error with information about waiting information
M Extensions/NosSmooth.Extensions.Combat/CombatManager.cs => Extensions/NosSmooth.Extensions.Combat/CombatManager.cs +5 -3
@@ 169,17 169,19 @@ public class CombatManager : IStatefulEntity
        if (!currentOperation.IsExecuting())
        { // not executing, check can be used, execute if can.
            var canBeUsedResult = currentOperation.CanBeUsed(combatState);
            if (!canBeUsedResult.IsDefined(out var canBeUsed))
            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:
                    return new UnusableOperationError(currentOperation);
                case CanBeUsedResponse.MustWait:
                    var waitingResult = technique.HandleWaiting(queueType, combatState, currentOperation);
                    var waitingResult = technique.HandleWaiting(queueType, combatState, currentOperation, canBeUsedError!);

                    if (!waitingResult.IsSuccess)
                    {

A Extensions/NosSmooth.Extensions.Combat/Errors/CharacterCannotAttackError.cs => Extensions/NosSmooth.Extensions.Combat/Errors/CharacterCannotAttackError.cs +12 -0
@@ 0,0 1,12 @@
//
//  CharacterCannotAttackError.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 Remora.Results;

namespace NosSmooth.Extensions.Combat.Errors;

public record CharacterCannotAttackError()
    : ResultError("The character cannot currently attack (is stunned, under debuff)");
\ No newline at end of file

A Extensions/NosSmooth.Extensions.Combat/Errors/CharacterCannotMoveError.cs => Extensions/NosSmooth.Extensions.Combat/Errors/CharacterCannotMoveError.cs +12 -0
@@ 0,0 1,12 @@
//
//  CharacterCannotMoveError.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 Remora.Results;

namespace NosSmooth.Extensions.Combat.Errors;

public record CharacterCannotMoveError()
    : ResultError("The character cannot currently move (is stunned, under debuff)");
\ No newline at end of file

A Extensions/NosSmooth.Extensions.Combat/Errors/NotEnoughManaError.cs => Extensions/NosSmooth.Extensions.Combat/Errors/NotEnoughManaError.cs +12 -0
@@ 0,0 1,12 @@
//
//  NotEnoughManaError.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 Remora.Results;

namespace NosSmooth.Extensions.Combat.Errors;

public record NotEnoughManaError(long CurrentMana, long NeededMana)
    : ResultError($"The character (with {CurrentMana} mp) does not have enough mana ({NeededMana} mp) for the given operation.");
\ No newline at end of file

A Extensions/NosSmooth.Extensions.Combat/Errors/TargetDeadError.cs => Extensions/NosSmooth.Extensions.Combat/Errors/TargetDeadError.cs +12 -0
@@ 0,0 1,12 @@
//
//  TargetDeadError.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 Remora.Results;

namespace NosSmooth.Extensions.Combat.Errors;

public record TargetDeadError()
    : ResultError("The target is already dead.");
\ No newline at end of file

A Extensions/NosSmooth.Extensions.Combat/Operations/CannotBeUsedError.cs => Extensions/NosSmooth.Extensions.Combat/Operations/CannotBeUsedError.cs +12 -0
@@ 0,0 1,12 @@
//
//  CannotBeUsedError.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 Remora.Results;

namespace NosSmooth.Extensions.Combat.Operations;

public record CannotBeUsedError(CanBeUsedResponse Response, IResultError? UnderlyingError)
    : ResultError($"The given operation cannot move forward ({Response}). {UnderlyingError?.Message}");
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Combat/Operations/CompoundOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/CompoundOperation.cs +14 -7
@@ 5,6 5,7 @@
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics;
using NosSmooth.Extensions.Combat.Techniques;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Operations;


@@ 15,6 16,7 @@ namespace NosSmooth.Extensions.Combat.Operations;
/// </summary>
public class CompoundOperation : ICombatOperation
{
    private readonly ICombatTechnique _technique;
    private readonly ICombatOperation[] _operations;
    private readonly OperationQueueType _queueType;
    private Task<Result>? _compoundOperation;


@@ 22,16 24,18 @@ public class CompoundOperation : ICombatOperation
    /// <summary>
    /// Initializes a new instance of the <see cref="CompoundOperation"/> class.
    /// </summary>
    /// <param name="operations">The operations to execute.</param>
    /// <param name="technique">The combat technique used for calling HandleWaiting.</param>
    /// <param name="queueType">The queue type.</param>
    /// <param name="operations">The operations to execute.</param>
    public CompoundOperation
        (OperationQueueType queueType = OperationQueueType.TotalControl, params ICombatOperation[] operations)
        (ICombatTechnique technique, OperationQueueType queueType = OperationQueueType.TotalControl, params ICombatOperation[] operations)
    {
        if (operations.Length == 0)
        {
            throw new ArgumentNullException(nameof(operations), "The compound operation needs at least one operation.");
        }

        _technique = technique;
        _operations = operations;
        _queueType = queueType;
    }


@@ 90,7 94,7 @@ public class CompoundOperation : ICombatOperation
        => _compoundOperation?.IsCompleted ?? false;

    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    public Result CanBeUsed(ICombatState combatState)
        => _operations[0].CanBeUsed(combatState);

    private async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct)


@@ 102,14 106,17 @@ public class CompoundOperation : ICombatOperation
            while (canBeUsed != CanBeUsedResponse.CanBeUsed)
            {
                var canBeUsedResult = operation.CanBeUsed(combatState);
                if (!canBeUsedResult.IsDefined(out canBeUsed))
                if (canBeUsedResult is { IsSuccess: false, Error: not CannotBeUsedError })
                {
                    return Result.FromError(canBeUsedResult);
                    return canBeUsedResult;
                }

                if (canBeUsed == CanBeUsedResponse.WontBeUsable)
                var error = canBeUsedResult.Error as CannotBeUsedError;
                canBeUsed = error?.Response ?? CanBeUsedResponse.CanBeUsed;

                if (canBeUsed != CanBeUsedResponse.CanBeUsed)
                {
                    return new GenericError("Won't be usable.");
                    _technique.HandleWaiting(QueueType, combatState, this, error!);
                }

                await Task.Delay(10, ct);

M Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs +1 -9
@@ 15,14 15,6 @@ namespace NosSmooth.Extensions.Combat.Operations;
/// </summary>
public interface ICombatOperation : IDisposable
{
    // 1. wait for CanBeUsed
    // 2. use OnlyExecute
    // 3. periodically check IsFinished
    // 4. Finished
    // 5. Call Dispose
    // 6. Go to next operation in queue
    // go to step 1

    /// <summary>
    /// Gets the queue type the operation belongs to.
    /// </summary>


@@ 70,5 62,5 @@ public interface ICombatOperation : IDisposable
    /// </remarks>
    /// <param name="combatState">The combat state.</param>
    /// <returns>Whether the operation can be used right away.</returns>
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState);
    public Result CanBeUsed(ICombatState combatState);
}
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs +2 -2
@@ 66,8 66,8 @@ public record UseItemOperation(InventoryItem Item) : ICombatOperation
        => _useItemOperation?.IsCompleted ?? false;

    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
        => CanBeUsedResponse.CanBeUsed;
    public Result CanBeUsed(ICombatState combatState)
        => Result.FromSuccess();

    /// <inheritdoc />
    public void Dispose()

M Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs +24 -8
@@ 5,7 5,6 @@
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics;
using System.Xml.XPath;
using NosSmooth.Core.Contracts;
using NosSmooth.Data.Abstractions.Enums;
using NosSmooth.Data.Abstractions.Infos;


@@ 16,8 15,6 @@ using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Game.Errors;
using NosSmooth.Game.Events.Battle;
using NosSmooth.Packets;
using NosSmooth.Packets.Client.Battle;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Operations;


@@ 103,7 100,7 @@ public record UseSkillOperation
        => _contract?.HasReachedState(UseSkillStates.CharacterRestored) ?? false;

    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    public Result CanBeUsed(ICombatState combatState)
    {
        if (Skill.Info is null)
        {


@@ 113,18 110,37 @@ public record UseSkillOperation
        var character = combatState.Game.Character;
        if (Target.Hp is not null && Target.Hp.Amount is not null && Target.Hp.Amount == 0)
        {
            return CanBeUsedResponse.WontBeUsable;
            return new CannotBeUsedError(CanBeUsedResponse.WontBeUsable, new TargetDeadError());
        }

        if (character is not null && character.Mp is not null && character.Mp.Amount is not null)
        if (character is null)
        {
            return new CharacterNotInitializedError();
        }

        if (character.CantAttack)
        {
            return new CannotBeUsedError(CanBeUsedResponse.MustWait, new CharacterCannotAttackError());
        }

        if (character.Mp is not null && character.Mp.Amount is not null)
        {
            if (character.Mp.Amount < Skill.Info.MpCost)
            { // The character is in combat, mp won't restore.
                return CanBeUsedResponse.WontBeUsable;
                return new CannotBeUsedError
                (
                    CanBeUsedResponse.WontBeUsable,
                    new NotEnoughManaError(character.Mp.Amount.Value, Skill.Info.MpCost)
                );
            }
        }

        return Skill.IsOnCooldown ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
        if (Skill.IsOnCooldown)
        {
            return new CannotBeUsedError(CanBeUsedResponse.MustWait, new SkillOnCooldownError(Skill));
        }

        return Result.FromSuccess();
    }

    private Result<IContract<SkillUsedEvent, UseSkillStates>> ContractSkill(ISkillInfo info)

M Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs +7 -2
@@ 76,7 76,7 @@ public record WalkInRangeOperation
        => _walkInRangeOperation?.IsCompleted ?? false;

    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    public Result CanBeUsed(ICombatState combatState)
    {
        var character = combatState.Game.Character;
        if (character is null)


@@ 84,7 84,12 @@ public record WalkInRangeOperation
            return new CharacterNotInitializedError();
        }

        return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
        if (character.CantMove)
        {
            return new CannotBeUsedError(CanBeUsedResponse.MustWait, new CharacterCannotMoveError());
        }

        return Result.FromSuccess();
    }

    private async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)

M Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs +7 -2
@@ 66,7 66,7 @@ public record WalkOperation(WalkManager WalkManager, short X, short Y) : ICombat
        => _walkOperation?.IsCompleted ?? false;

    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    public Result CanBeUsed(ICombatState combatState)
    {
        var character = combatState.Game.Character;
        if (character is null)


@@ 74,7 74,12 @@ public record WalkOperation(WalkManager WalkManager, short X, short Y) : ICombat
            return new CharacterNotInitializedError();
        }

        return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
        if (character.CantMove)
        {
            return new CannotBeUsedError(CanBeUsedResponse.MustWait, new CharacterCannotMoveError());
        }

        return Result.FromSuccess();
    }

    private Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)

M Extensions/NosSmooth.Extensions.Combat/Techniques/ICombatTechnique.cs => Extensions/NosSmooth.Extensions.Combat/Techniques/ICombatTechnique.cs +2 -1
@@ 51,8 51,9 @@ public interface ICombatTechnique
    /// <param name="queueType">The type of the operation.</param>
    /// <param name="state">The combat state.</param>
    /// <param name="operation">The operation that needs waiting.</param>
    /// <param name="error">The error received from the operation.</param>
    /// <returns>A result that may or may not have succeeded. In case of an error, <see cref="HandleError"/> will be called with the error.</returns>
    public Result HandleWaiting(OperationQueueType queueType, ICombatState state, ICombatOperation operation);
    public Result HandleWaiting(OperationQueueType queueType, ICombatState state, ICombatOperation operation, CannotBeUsedError error);

    /// <summary>
    /// Handles an arbitrary error.

M Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs => Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs +14 -5
@@ 4,7 4,6 @@
//  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.Data.Abstractions.Enums;
using NosSmooth.Extensions.Combat.Errors;
using NosSmooth.Extensions.Combat.Extensions;


@@ 12,9 11,7 @@ using NosSmooth.Extensions.Combat.Operations;
using NosSmooth.Extensions.Combat.Selectors;
using NosSmooth.Extensions.Pathfinding;
using NosSmooth.Game.Apis.Safe;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Game.Data.Inventory;
using NosSmooth.Game.Extensions;
using Remora.Results;



@@ 83,8 80,19 @@ public class SimpleAttackTechnique : ICombatTechnique
    }

    /// <inheritdoc />
    public Result HandleWaiting(OperationQueueType queueType, ICombatState state, ICombatOperation operation)
    { // does not do anything, just wait.
    public Result HandleWaiting(OperationQueueType queueType, ICombatState state, ICombatOperation operation, CannotBeUsedError cannotBeUsedError)
    {
        if (cannotBeUsedError.UnderlyingError is TargetDeadError)
        {
            state.RemoveCurrentOperation(queueType, true);
            return Result.FromSuccess();
        }

        if (cannotBeUsedError.Response == CanBeUsedResponse.WontBeUsable)
        {
            return cannotBeUsedError;
        }

        return Result.FromSuccess();
    }



@@ 172,6 180,7 @@ public class SimpleAttackTechnique : ICombatTechnique
        (
            new CompoundOperation
            (
                this,
                OperationQueueType.TotalControl,
                new WalkInRangeOperation(_walkManager, _target, range),
                new UseSkillOperation(_skillsApi, currentSkill, character, _target)

Do not follow this link