~ruther/NosSmooth

f0507f74c29dcda0f857bb7401191db755b430fd — Rutherther 3 years ago a76434f
feat(combat): add combat operations
A Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs => Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs +90 -0
@@ 0,0 1,90 @@
//
//  CombatStateExtensions.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.Extensions.Combat.Errors;
using NosSmooth.Extensions.Combat.Operations;
using NosSmooth.Extensions.Combat.Policies;
using NosSmooth.Extensions.Pathfinding;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Game.Data.Items;
using OneOf.Types;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Extensions;

/// <summary>
/// Extension methods for <see cref="ICombatState"/>.
/// </summary>
public static class CombatStateExtensions
{
    /// <summary>
    /// Walk in the range of the given entity.
    /// </summary>
    /// <param name="state">The combat state.</param>
    /// <param name="walkManager">The walk manager.</param>
    /// <param name="entity">The entity.</param>
    /// <param name="range">The range distance to walk to.</param>
    public static void WalkInRange
    (
        this ICombatState state,
        WalkManager walkManager,
        IEntity entity,
        float range
    )
    {
        state.EnqueueOperation(new WalkInRangeOperation(walkManager, entity, range));
    }

    /// <summary>
    /// Walk to the given position.
    /// </summary>
    /// <param name="combatState">The combat state.</param>
    /// <param name="walkManager">The walk manager.</param>
    /// <param name="x">The target x coordinate.</param>
    /// <param name="y">The target y coordinate.</param>
    public static void WalkTo
    (
        this ICombatState combatState,
        WalkManager walkManager,
        short x,
        short y
    )
    {
        combatState.EnqueueOperation(new WalkOperation(walkManager, x, y));
    }

    /// <summary>
    /// Use the given skill.
    /// </summary>
    /// <param name="combatState">The combat state.</param>
    /// <param name="skill">The skill.</param>
    /// <param name="target">The target to use skill at.</param>
    public static void UseSkill(this ICombatState combatState, Skill skill, ILivingEntity target)
    {
        combatState.EnqueueOperation(new UseSkillOperation(skill, target));
    }

    /// <summary>
    /// Use primary skill.
    /// </summary>
    /// <param name="combatState">The combat state.</param>
    /// <param name="target">The target to use skill at.</param>
    public static void UsePrimarySkill(this ICombatState combatState, ILivingEntity target)
    {
        combatState.EnqueueOperation(new UsePrimarySkillOperation(target));
    }

    /// <summary>
    /// Use the given item.
    /// </summary>
    /// <param name="combatState">The combat state.</param>
    /// <param name="item">The item to use.</param>
    public static void UseItem(this ICombatState combatState, Item item)
    {
        combatState.EnqueueOperation(new UseItemOperation(item));
    }
}
\ No newline at end of file

A Extensions/NosSmooth.Extensions.Combat/ICombatState.cs => Extensions/NosSmooth.Extensions.Combat/ICombatState.cs +58 -0
@@ 0,0 1,58 @@
//
//  ICombatState.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.Extensions.Combat.Operations;
using NosSmooth.Game.Data.Entities;

namespace NosSmooth.Extensions.Combat;

/// <summary>
/// The combat technique state used for queuing operations and storing information.
/// </summary>
public interface ICombatState
{
    /// <summary>
    /// Gets the combat manager.
    /// </summary>
    public CombatManager CombatManager { get; }

    /// <summary>
    /// Gets the game.
    /// </summary>
    public Game.Game Game { get; }

    /// <summary>
    /// Gets the NosTale client.
    /// </summary>
    public INostaleClient Client { get; }

    /// <summary>
    /// Cancel the combat technique, quit the combat state.
    /// </summary>
    public void QuitCombat();

    /// <summary>
    /// Replace the current operation with this one.
    /// </summary>
    /// <param name="operation">The operation to use.</param>
    /// <param name="emptyQueue">Whether to empty the queue of the operations.</param>
    /// <param name="prependCurrentOperationToQueue">Whether to still use the current operation (true) after this one or discard it (false).</param>
    public void SetCurrentOperation
        (ICombatOperation operation, bool emptyQueue = false, bool prependCurrentOperationToQueue = false);

    /// <summary>
    /// Enqueue the operation at the end of the queue.
    /// </summary>
    /// <param name="operation">The operation to enqueue.</param>
    public void EnqueueOperation(ICombatOperation operation);

    /// <summary>
    /// Remove the operations by the given filter.
    /// </summary>
    /// <param name="filter">Called for each operation, should return true if it should be removed.</param>
    public void RemoveOperations(Func<ICombatOperation, bool> filter);
}
\ No newline at end of file

A Extensions/NosSmooth.Extensions.Combat/Operations/CanBeUsedResponse.cs => Extensions/NosSmooth.Extensions.Combat/Operations/CanBeUsedResponse.cs +28 -0
@@ 0,0 1,28 @@
//
//  CanBeUsedResponse.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.

namespace NosSmooth.Extensions.Combat.Operations;

/// <summary>
/// A response to <see cref="ICombatOperation"/> CanBeUsed method.
/// </summary>
public enum CanBeUsedResponse
{
    /// <summary>
    /// The operation may be used right awayt.
    /// </summary>
    CanBeUsed,

    /// <summary>
    /// The operation will be usable after some amount of time.
    /// </summary>
    MustWait,

    /// <summary>
    /// The operation won't be usable. (ie. missing arrows).
    /// </summary>
    WontBeUsable
}
\ No newline at end of file

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

namespace NosSmooth.Extensions.Combat.Operations;

/// <summary>
/// A combat operation used in <see cref="ICombatTechnique"/> that can be used as one step.
/// </summary>
public interface ICombatOperation
{
    /// <summary>
    /// Checks whether the operation can currently be used.
    /// </summary>
    /// <remarks>
    /// Ie. if the operation is to use a skill, it will return true only if the skill is not on a cooldown,
    /// the character has enough mana and is not stunned.
    /// </remarks>
    /// <param name="combatState">The combat state.</param>
    /// <returns>Whether the operation can be used right away.</returns>
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState);

    /// <summary>
    /// Use the operation, if possible.
    /// </summary>
    /// <remarks>
    /// Should block until the operation is finished.
    /// </remarks>
    /// <param name="combatState">The combat state.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not succeed.</returns>
    public Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default);
}
\ No newline at end of file

A Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs +26 -0
@@ 0,0 1,26 @@
//
//  UseItemOperation.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.Game.Data.Items;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Operations;

// TODO: first inventory has to be made
public record UseItemOperation(Item Item) : ICombatOperation
{
    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    {
        throw new NotImplementedException();
    }

    /// <inheritdoc />
    public Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
    {
        throw new NotImplementedException();
    }
}
\ No newline at end of file

A Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs +72 -0
@@ 0,0 1,72 @@
//
//  UsePrimarySkillOperation.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.Extensions.Combat.Errors;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Packets.Client.Battle;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Operations;

/// <summary>
/// An operation that uses the primary skill of the character.
/// </summary>
public record UsePrimarySkillOperation(ILivingEntity Target) : ICombatOperation
{
    private UseSkillOperation? _useSkillOperation;

    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    {
        if (_useSkillOperation is null)
        {
            var primarySkillResult = GetPrimarySkill(combatState);
            if (!primarySkillResult.IsDefined(out var primarySkill))
            {
                return Result<CanBeUsedResponse>.FromError(primarySkillResult);
            }

            _useSkillOperation = new UseSkillOperation(primarySkill, Target);
        }

        return _useSkillOperation.CanBeUsed(combatState);
    }

    /// <inheritdoc />
    public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct)
    {
        if (_useSkillOperation is null)
        {
            var primarySkillResult = GetPrimarySkill(combatState);
            if (!primarySkillResult.IsDefined(out var primarySkill))
            {
                return Result.FromError(primarySkillResult);
            }

            _useSkillOperation = new UseSkillOperation(primarySkill, Target);
        }

        return await _useSkillOperation.UseAsync(combatState, ct);
    }

    private Result<Skill> GetPrimarySkill(ICombatState combatState)
    {
        var character = combatState.Game.Character;
        if (character is null)
        {
            return new CharacterNotInitializedError();
        }

        var skills = character.Skills;
        if (skills is null)
        {
            return new CharacterNotInitializedError("Skills");
        }

        return skills.PrimarySkill;
    }
}
\ No newline at end of file

A Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs +77 -0
@@ 0,0 1,77 @@
//
//  UseSkillOperation.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.Xml.XPath;
using NosSmooth.Extensions.Combat.Errors;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Packets.Client.Battle;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Operations;

/// <summary>
/// A combat operation to use a skill.
/// </summary>
/// <param name="Skill">The skill to use.</param>
/// <param name="Target">The target entity to use the skill at.</param>
public record UseSkillOperation(Skill Skill, ILivingEntity Target) : ICombatOperation
{
    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    {
        if (Skill.Info is null)
        {
            return new MissingInfoError("skill", Skill.SkillVNum);
        }

        var character = combatState.Game.Character;
        if (character is not null && 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 Skill.IsOnCooldown ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
    }

    /// <inheritdoc />
    public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
    {
        if (Skill.Info is null)
        {
            return new MissingInfoError("skill", Skill.SkillVNum);
        }

        // TODO: support for area skills, support skills that use x, y coordinates (like dashes or teleports)
        var sendResponse = await combatState.Client.SendPacketAsync
        (
            new UseSkillPacket
            (
                Skill.Info.CastId,
                Target.Type,
                Target.Id,
                null,
                null
            ),
            ct
        );

        if (!sendResponse.IsSuccess)
        {
            return sendResponse;
        }

        var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
        combatState.CombatManager.RegisterSkillCancellationToken(linkedSource);
        await Task.Delay(Skill.Info.CastTime * 200 * 5, linkedSource.Token);
        combatState.CombatManager.UnregisterSkillCancellationToken(linkedSource);

        return Result.FromSuccess();
    }
}
\ No newline at end of file

A Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs +84 -0
@@ 0,0 1,84 @@
//
//  WalkInRangeOperation.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.Extensions.Combat.Errors;
using NosSmooth.Extensions.Pathfinding;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Game.Data.Info;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Operations;

/// <summary>
/// A combat operation that walks into a given range of an entity.
/// </summary>
/// <param name="WalkManager">The walk manager.</param>
/// <param name="Entity">The entity to walk to.</param>
/// <param name="Distance">The maximal distance from the entity.</param>
public record WalkInRangeOperation
(
    WalkManager WalkManager,
    IEntity Entity,
    float Distance
) : ICombatOperation
{
    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    {
        var character = combatState.Game.Character;
        if (character is null)
        {
            return new CharacterNotInitializedError();
        }

        return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
    }

    /// <inheritdoc />
    public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
    {
        var character = combatState.Game.Character;
        if (character is null)
        {
            return new CharacterNotInitializedError();
        }

        var distance = Distance;
        while (distance >= 1)
        {
            var position = Entity.Position;
            if (position is null)
            {
                return new GenericError("Entity's position is not initialized.");
            }

            var currentPosition = character.Position;
            if (currentPosition is null)
            {
                return new CharacterNotInitializedError("Position");
            }

            var closePosition = GetClosePosition(currentPosition.Value, position.Value, distance);
            var walkResult = await WalkManager.GoToAsync(closePosition.X, closePosition.Y, ct);
            if (!walkResult.IsSuccess && walkResult.Error is NotFoundError)
            {
                distance--;
                continue;
            }

            return walkResult;
        }

        return Result.FromSuccess();
    }

    private Position GetClosePosition(Position start, Position target, double distance)
    {
        var diff = start - target;
        var diffLength = Math.Sqrt(diff.DistanceSquared(Position.Zero));
        return target + ((distance / diffLength) * diff);
    }
}
\ No newline at end of file

A Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs +38 -0
@@ 0,0 1,38 @@
//
//  WalkOperation.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.Extensions.Combat.Errors;
using NosSmooth.Extensions.Pathfinding;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Operations;

/// <summary>
/// A combat operation that walks to the target.
/// </summary>
/// <param name="WalkManager">The walk manager.</param>
/// <param name="X">The x coordinate to walk to.</param>
/// <param name="Y">The y coordinate to walk to.</param>
public record WalkOperation(WalkManager WalkManager, short X, short Y) : ICombatOperation
{
    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    {
        var character = combatState.Game.Character;
        if (character is null)
        {
            return new CharacterNotInitializedError();
        }

        return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
    }

    /// <inheritdoc />
    public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
    {
        return await WalkManager.GoToAsync(X, Y, ct);
    }
}
\ No newline at end of file

Do not follow this link