M Extensions/NosSmooth.Extensions.Combat/CombatManager.cs => Extensions/NosSmooth.Extensions.Combat/CombatManager.cs +115 -51
@@ 4,6 4,7 @@
// 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;
@@ 61,65 62,26 @@ public class CombatManager : IStatefulEntity
{
while (!combatState.ShouldQuit && currentTarget == previousTarget)
{
- if (!technique.ShouldContinue(combatState))
- {
- combatState.QuitCombat();
- continue;
- }
-
- var operation = combatState.NextOperation();
+ var iterationResult = await HandleAttackIterationAsync(combatState, technique, ct);
- 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;
+ if (!iterationResult.IsSuccess)
+ {
+ var errorResult = technique.HandleError(combatState, Result.FromError(iterationResult));
- if (previousTarget != currentTarget)
- {
- continue;
+ if (!errorResult.IsSuccess)
+ { // end the attack.
+ return errorResult;
}
-
- 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<CanBeUsedResponse> 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)
+ var result = iterationResult.Entity;
+ if (!result.TargetChanged)
{
- return new UnusableOperationError(operation);
+ continue;
}
- var usageResult = await operation.UseAsync(combatState, ct);
- if (!usageResult.IsSuccess)
- {
- var errorHandleResult = technique.HandleError(combatState, operation, usageResult);
- if (!errorHandleResult.IsSuccess)
- {
- return errorHandleResult;
- }
- }
+ previousTarget = currentTarget;
+ currentTarget = result.TargetId;
}
return Result.FromSuccess();
@@ 138,6 100,108 @@ public class CombatManager : IStatefulEntity
return Result.FromSuccess();
}
+ private async Task<Result<(bool TargetChanged, long? TargetId)>> HandleAttackIterationAsync
+ (CombatState combatState, ICombatTechnique technique, CancellationToken ct)
+ {
+ if (!technique.ShouldContinue(combatState))
+ {
+ combatState.QuitCombat();
+ return Result<(bool, long?)>.FromSuccess((false, null));
+ }
+
+ // 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 (currentOperation is null)
+ { // 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.IsExecuting())
+ { // not executing, check can be used, execute if can.
+ var canBeUsedResult = currentOperation.CanBeUsed(combatState);
+ if (!canBeUsedResult.IsDefined(out var canBeUsed))
+ {
+ return Result<(bool, long?)>.FromError(canBeUsedResult);
+ }
+
+ switch (canBeUsed)
+ {
+ case CanBeUsedResponse.WontBeUsable:
+ return new UnusableOperationError(currentOperation);
+ case CanBeUsedResponse.MustWait:
+ var waitingResult = technique.HandleWaiting(queueType, combatState, currentOperation);
+
+ 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);
+ }
+
/// <summary>
/// Register the given cancellation token source to be cancelled on skill use/cancel.
/// </summary>
M Extensions/NosSmooth.Extensions.Combat/CombatState.cs => Extensions/NosSmooth.Extensions.Combat/CombatState.cs +68 -32
@@ 4,18 4,17 @@
// 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;
+using System.Diagnostics.CodeAnalysis;
using NosSmooth.Core.Client;
using NosSmooth.Extensions.Combat.Operations;
-using NosSmooth.Game.Data.Entities;
namespace NosSmooth.Extensions.Combat;
/// <inheritdoc />
internal class CombatState : ICombatState
{
- private readonly LinkedList<ICombatOperation> _operations;
- private ICombatOperation? _currentOperation;
+ private readonly Dictionary<OperationQueueType, LinkedList<ICombatOperation>> _operations;
+ private readonly Dictionary<OperationQueueType, ICombatOperation> _currentOperations;
/// <summary>
/// Initializes a new instance of the <see cref="CombatState"/> class.
@@ 28,7 27,8 @@ internal class CombatState : ICombatState
Client = client;
Game = game;
CombatManager = combatManager;
- _operations = new LinkedList<ICombatOperation>();
+ _operations = new Dictionary<OperationQueueType, LinkedList<ICombatOperation>>();
+ _currentOperations = new Dictionary<OperationQueueType, ICombatOperation>();
}
/// <summary>
@@ 46,62 46,98 @@ internal class CombatState : ICombatState
public INostaleClient Client { get; }
/// <inheritdoc/>
- public void QuitCombat()
- {
- ShouldQuit = true;
- }
+ public bool IsWaitingOnOperation => _currentOperations.Any(x => !x.Value.IsExecuting());
/// <summary>
- /// Make a step in the queue.
+ /// Move to next operation, if available.
/// </summary>
- /// <returns>The current operation, if any.</returns>
- public ICombatOperation? NextOperation()
+ /// <param name="queueType">The queue type to move to next operation in.</param>
+ /// <returns>Next operation, if any.</returns>
+ public ICombatOperation? NextOperation(OperationQueueType queueType)
{
- var operation = _currentOperation = _operations.First?.Value;
- if (operation is not null)
+ if (_operations.ContainsKey(queueType))
{
- _operations.RemoveFirst();
+ var nextOperation = _operations[queueType].FirstOrDefault();
+
+ if (nextOperation is not null)
+ {
+ _currentOperations[queueType] = nextOperation;
+ return nextOperation;
+ }
}
- return operation;
+ return null;
+ }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<ICombatOperation> GetWaitingForOperations()
+ {
+ return _currentOperations.Values.Where(x => !x.IsExecuting()).ToList();
+ }
+
+ /// <inheritdoc/>
+ public ICombatOperation? GetCurrentOperation(OperationQueueType queueType)
+ => _currentOperations.GetValueOrDefault(queueType);
+
+ /// <inheritdoc/>
+ public bool IsExecutingOperation(OperationQueueType queueType, [NotNullWhen(true)] out ICombatOperation? operation)
+ {
+ operation = GetCurrentOperation(queueType);
+ return operation is not null && operation.IsExecuting();
+ }
+
+ /// <inheritdoc/>
+ public void QuitCombat()
+ {
+ ShouldQuit = true;
}
/// <inheritdoc/>
public void SetCurrentOperation
(ICombatOperation operation, bool emptyQueue = false, bool prependCurrentOperationToQueue = false)
{
- var current = _currentOperation;
- _currentOperation = operation;
+ var type = operation.QueueType;
+
+ if (!_operations.ContainsKey(type))
+ {
+ _operations[type] = new LinkedList<ICombatOperation>();
+ }
if (emptyQueue)
{
- _operations.Clear();
+ _operations[type].Clear();
+ }
+
+ if (prependCurrentOperationToQueue)
+ {
+ _operations[type].AddFirst(operation);
+ return;
}
- if (prependCurrentOperationToQueue && current is not null)
+ if (_currentOperations.ContainsKey(type))
{
- _operations.AddFirst(current);
+ _currentOperations[type].Dispose();
}
+
+ _currentOperations[type] = operation;
}
/// <inheritdoc/>
public void EnqueueOperation(ICombatOperation operation)
{
- _operations.AddLast(operation);
+ var type = operation.QueueType;
+
+ if (!_operations.ContainsKey(type))
+ {
+ _operations[type] = new LinkedList<ICombatOperation>();
+ }
+
+ _operations[type].AddLast(operation);
}
/// <inheritdoc/>
public void RemoveOperations(Func<ICombatOperation, bool> filter)
{
- var node = _operations.First;
- while (node != null)
- {
- var next = node.Next;
- if (filter(node.Value))
- {
- _operations.Remove(node);
- }
- node = next;
- }
+ throw new NotImplementedException();
}
}=
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs => Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs +6 -13
@@ 7,7 7,9 @@
using NosSmooth.Extensions.Combat.Errors;
using NosSmooth.Extensions.Combat.Operations;
using NosSmooth.Extensions.Combat.Policies;
+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.Items;
@@ 61,26 63,17 @@ public static class CombatStateExtensions
/// Use the given skill.
/// </summary>
/// <param name="combatState">The combat state.</param>
+ /// <param name="skillsApi">The skills api for using a skill.</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)
+ public static void UseSkill(this ICombatState combatState, NostaleSkillsApi skillsApi, Skill skill, ILivingEntity target)
{
if (combatState.Game.Character is null)
{
throw new InvalidOperationException("The character is not initialized.");
}
- combatState.EnqueueOperation(new UseSkillOperation(skill, combatState.Game.Character, 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));
+ combatState.EnqueueOperation(new UseSkillOperation(skillsApi, skill, combatState.Game.Character, target));
}
/// <summary>
@@ 88,7 81,7 @@ public static class CombatStateExtensions
/// </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)
+ public static void UseItem(this ICombatState combatState, InventoryItem item)
{
combatState.EnqueueOperation(new UseItemOperation(item));
}
M Extensions/NosSmooth.Extensions.Combat/Extensions/ServiceCollectionExtensions.cs => Extensions/NosSmooth.Extensions.Combat/Extensions/ServiceCollectionExtensions.cs +0 -4
@@ 5,8 5,6 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using Microsoft.Extensions.DependencyInjection;
-using NosSmooth.Core.Extensions;
-using NosSmooth.Extensions.Combat.Responders;
namespace NosSmooth.Extensions.Combat.Extensions;
@@ 23,8 21,6 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddNostaleCombat(this IServiceCollection serviceCollection)
{
return serviceCollection
- .AddPacketResponder<CancelResponder>()
- .AddPacketResponder<SuResponder>()
.AddSingleton<CombatManager>();
}
}=
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/ICombatState.cs => Extensions/NosSmooth.Extensions.Combat/ICombatState.cs +31 -0
@@ 4,6 4,7 @@
// 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.CodeAnalysis;
using NosSmooth.Core.Client;
using NosSmooth.Extensions.Combat.Operations;
using NosSmooth.Game.Data.Entities;
@@ 31,6 32,36 @@ public interface ICombatState
public INostaleClient Client { get; }
/// <summary>
+ /// Gets whether there is an operation that cannot be used
+ /// and we must wait for it to be usable.
+ /// </summary>
+ public bool IsWaitingOnOperation { get; }
+
+ /// <summary>
+ /// Get the operations the state is waiting for to to be usable.
+ /// </summary>
+ /// <returns>The operations needed to wait for.</returns>
+ public IReadOnlyList<ICombatOperation> GetWaitingForOperations();
+
+ /// <summary>
+ /// Gets the current operation of the given queue type.
+ /// </summary>
+ /// <param name="queueType">The queue type to get the current operation of.</param>
+ /// <returns>The operation of the given queue, if any.</returns>
+ public ICombatOperation? GetCurrentOperation(OperationQueueType queueType);
+
+ /// <summary>
+ /// Checks whether an operation is being executed in the given queue.
+ /// </summary>
+ /// <remarks>
+ /// If not, either waiting for the operation or there is no operation enqueued.
+ /// </remarks>
+ /// <param name="queueType">The type of queue to look at.</param>
+ /// <param name="operation">The operation currently being executed.</param>
+ /// <returns>Whether an operation is being executed.</returns>
+ public bool IsExecutingOperation(OperationQueueType queueType, [NotNullWhen(true)] out ICombatOperation? operation);
+
+ /// <summary>
/// Cancel the combat technique, quit the combat state.
/// </summary>
public void QuitCombat();
A Extensions/NosSmooth.Extensions.Combat/OperationQueueType.cs => Extensions/NosSmooth.Extensions.Combat/OperationQueueType.cs +73 -0
@@ 0,0 1,73 @@
+//
+// OperationQueueType.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;
+
+/// <summary>
+/// A type classifying operations into multiple sequential queues.
+/// </summary>
+public enum OperationQueueType
+{
+ /// <summary>
+ /// A total control of controls is needed (walking, sitting - recovering, using a skill).
+ /// </summary>
+ TotalControl,
+
+ /// <summary>
+ /// An item is being used.
+ /// </summary>
+ Item,
+
+ /// <summary>
+ /// Any operation that should be executed sequentially with other <see cref="Reserved1"/> operations.
+ /// </summary>
+ Reserved1,
+
+ /// <summary>
+ /// Any operation that should be executed sequentially with other <see cref="Reserved2"/> operations.
+ /// </summary>
+ Reserved2,
+
+ /// <summary>
+ /// Any operation that should be executed sequentially with other <see cref="Reserved3"/> operations.
+ /// </summary>
+ Reserved3,
+
+ /// <summary>
+ /// Any operation that should be executed sequentially with other <see cref="Reserved4"/> operations.
+ /// </summary>
+ Reserved4,
+
+ /// <summary>
+ /// Any operation that should be executed sequentially with other <see cref="Reserved5"/> operations.
+ /// </summary>
+ Reserved5,
+
+ /// <summary>
+ /// Any operation that should be executed sequentially with other <see cref="Reserved6"/> operations.
+ /// </summary>
+ Reserved6,
+
+ /// <summary>
+ /// Any operation that should be executed sequentially with other <see cref="Reserved7"/> operations.
+ /// </summary>
+ Reserved7,
+
+ /// <summary>
+ /// Any operation that should be executed sequentially with other <see cref="Reserved8"/> operations.
+ /// </summary>
+ Reserved8,
+
+ /// <summary>
+ /// Any operation that should be executed sequentially with other <see cref="Reserved9"/> operations.
+ /// </summary>
+ Reserved9,
+
+ /// <summary>
+ /// Any operation that should be executed sequentially with other <see cref="Reserved10"/> operations.
+ /// </summary>
+ Reserved10,
+}<
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs +47 -11
@@ 4,6 4,7 @@
// 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.Contracts;
using NosSmooth.Extensions.Combat.Techniques;
using Remora.Results;
@@ 12,27 13,62 @@ 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
+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>
- /// Checks whether the operation can currently be used.
+ /// Gets the queue type the operation belongs to.
/// </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.
+ /// Used for distinguishing what operations may run simultaneously.
+ /// For example items may be used simultaneous to attacking. Attacking
+ /// may not be simultaneous to walking.
/// </remarks>
+ public OperationQueueType QueueType { get; }
+
+ /// <summary>
+ /// Begin the execution without waiting for the finished state.
+ /// </summary>
/// <param name="combatState">The combat state.</param>
- /// <returns>Whether the operation can be used right away.</returns>
- public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState);
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> BeginExecution(ICombatState combatState, CancellationToken ct = default);
+
+ /// <summary>
+ /// Asynchronously wait for finished state.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ public Task<Result> WaitForFinishedAsync(ICombatState combatState, CancellationToken ct = default);
+
+ /// <summary>
+ /// Checks whether the operation is currently being executed.
+ /// </summary>
+ /// <returns>Whether the operation is being executed.</returns>
+ public bool IsExecuting();
+
+ /// <summary>
+ /// Checks whether the operation is done.
+ /// </summary>
+ /// <returns>Whether the operation is finished.</returns>
+ public bool IsFinished();
/// <summary>
- /// Use the operation, if possible.
+ /// Checks whether the operation can currently be used.
/// </summary>
/// <remarks>
- /// Should block until the operation is finished.
+ /// 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>
- /// <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);
+ /// <returns>Whether the operation can be used right away.</returns>
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState);
}=
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs +54 -6
@@ 4,7 4,12 @@
// 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 System.Diagnostics;
+using NosSmooth.Data.Abstractions.Enums;
+using NosSmooth.Extensions.Combat.Selectors;
+using NosSmooth.Game.Data.Inventory;
+using NosSmooth.Game.Extensions;
+using NosSmooth.Packets.Client.Inventory;
using Remora.Results;
namespace NosSmooth.Extensions.Combat.Operations;
@@ 13,17 18,60 @@ namespace NosSmooth.Extensions.Combat.Operations;
/// A combat operation to use an item.
/// </summary>
/// <param name="Item">The item to use.</param>
-public record UseItemOperation(Item Item) : ICombatOperation
+public record UseItemOperation(InventoryItem Item) : ICombatOperation
{
+ private Task<Result>? _useItemOperation;
+
/// <inheritdoc />
- public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ public OperationQueueType QueueType => OperationQueueType.Item;
+
+ /// <inheritdoc />
+ public Task<Result> BeginExecution(ICombatState combatState, CancellationToken ct = default)
+ {
+ if (_useItemOperation is not null)
+ {
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ _useItemOperation = Task.Run(
+ () => combatState.Client.SendPacketAsync(new UseItemPacket(Item.Bag.Convert(), Item.Item.Slot), ct),
+ ct
+ );
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ /// <inheritdoc />
+ public Task<Result> WaitForFinishedAsync(ICombatState combatState, CancellationToken ct = default)
{
- throw new NotImplementedException();
+ if (IsFinished())
+ {
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ BeginExecution(combatState, ct);
+ if (_useItemOperation is null)
+ {
+ throw new UnreachableException();
+ }
+
+ return _useItemOperation;
}
/// <inheritdoc />
- public Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
+ public bool IsExecuting()
+ => _useItemOperation is not null && !IsFinished();
+
+ /// <inheritdoc />
+ public bool IsFinished()
+ => _useItemOperation?.IsCompleted ?? false;
+
+ /// <inheritdoc />
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ => CanBeUsedResponse.CanBeUsed;
+
+ /// <inheritdoc />
+ public void Dispose()
{
- throw new NotImplementedException();
+ _useItemOperation?.Dispose();
}
}=
\ No newline at end of file
D Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs +0 -77
@@ 1,77 0,0 @@
-//
-// 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);
- }
-
- if (combatState.Game.Character is null)
- {
- return new CharacterNotInitializedError();
- }
-
- _useSkillOperation = new UseSkillOperation(primarySkill, combatState.Game.Character, 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);
- }
-
- if (combatState.Game.Character is null)
- {
- return new CharacterNotInitializedError();
- }
-
- _useSkillOperation = new UseSkillOperation(primarySkill, combatState.Game.Character, Target);
- }
-
- return await _useSkillOperation.UseAsync(combatState, ct);
- }
-
- private Result<Skill> GetPrimarySkill(ICombatState combatState)
- {
- var skills = combatState.Game.Skills;
-
- if (skills is null)
- {
- return new CharacterNotInitializedError("Skills");
- }
-
- return skills.PrimarySkill;
- }
-}>
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs +75 -61
@@ 6,11 6,16 @@
using System.Diagnostics;
using System.Xml.XPath;
+using NosSmooth.Core.Contracts;
using NosSmooth.Data.Abstractions.Enums;
using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Game.Apis.Safe;
+using NosSmooth.Game.Contracts;
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;
@@ 20,109 25,118 @@ namespace NosSmooth.Extensions.Combat.Operations;
/// <summary>
/// A combat operation to use a skill.
/// </summary>
+/// <param name="SkillsApi">The skills api used for executing the skills.</param>
/// <param name="Skill">The skill to use.</param>
/// <param name="Caster">The caster entity that is using the skill.</param>
/// <param name="Target">The target entity to use the skill at.</param>
-public record UseSkillOperation(Skill Skill, ILivingEntity Caster, ILivingEntity Target) : ICombatOperation
+public record UseSkillOperation(NostaleSkillsApi SkillsApi, Skill Skill, ILivingEntity Caster, ILivingEntity Target) : ICombatOperation
{
+ private IContract<SkillUsedEvent, UseSkillStates>? _contract;
+
/// <inheritdoc />
- public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ public OperationQueueType QueueType => OperationQueueType.TotalControl;
+
+ /// <inheritdoc />
+ public async Task<Result> BeginExecution(ICombatState combatState, CancellationToken ct = default)
{
+ if (_contract is not null)
+ {
+ return Result.FromSuccess();
+ }
+
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 (Target.Position is null)
{
- if (character.Mp.Amount < Skill.Info.MpCost)
- { // The character is in combat, mp won't restore.
- return CanBeUsedResponse.WontBeUsable;
- }
+ return new NotInitializedError("target's position");
}
- return Skill.IsOnCooldown ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
+ var contractResult = ContractSkill(Skill.Info);
+ if (!contractResult.IsDefined(out var contract))
+ {
+ return Result.FromError(contractResult);
+ }
+
+ _contract = contract;
+ var executed = await _contract.OnlyExecuteAsync(ct);
+ _contract.Register();
+
+ return executed;
}
/// <inheritdoc />
- public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
+ public async Task<Result> WaitForFinishedAsync(ICombatState combatState, CancellationToken ct = default)
{
- if (Skill.Info is null)
+ var result = await BeginExecution(combatState, ct);
+ if (!result.IsSuccess)
{
- return new MissingInfoError("skill", Skill.SkillVNum);
+ return result;
+ }
+
+ if (_contract is null)
+ {
+ throw new UnreachableException();
}
- using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
- await combatState.CombatManager.RegisterSkillCancellationTokenAsync(linkedSource, ct);
- var sendResponse = await combatState.Client.SendPacketAsync
- (
- CreateSkillUsePacket(Skill.Info),
- ct
- );
+ var waitResult = await _contract.WaitForAsync(UseSkillStates.CharacterRestored, ct: ct);
+ return waitResult.IsSuccess ? Result.FromSuccess() : Result.FromError(waitResult);
+ }
+
+ /// <inheritdoc />
+ public bool IsExecuting()
+ => _contract is not null && _contract.CurrentState > UseSkillStates.None && !IsFinished();
+
+ /// <inheritdoc />
+ public bool IsFinished()
+ => _contract?.CurrentState == UseSkillStates.CharacterRestored;
- if (!sendResponse.IsSuccess)
+ /// <inheritdoc />
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ {
+ if (Skill.Info is null)
{
- await combatState.CombatManager.UnregisterSkillCancellationTokenAsync(linkedSource, ct);
- return sendResponse;
+ return new MissingInfoError("skill", Skill.SkillVNum);
}
- try
+ var character = combatState.Game.Character;
+ if (Target.Hp is not null && Target.Hp.Amount is not null && Target.Hp.Amount == 0)
{
- // wait 10 times the cast delay in case su is not received.
- await Task.Delay(Skill.Info.CastTime * 1000, linkedSource.Token);
+ return CanBeUsedResponse.WontBeUsable;
}
- catch (TaskCanceledException)
+
+ if (character is not null && character.Mp is not null && character.Mp.Amount is not null)
{
- // ignored
+ if (character.Mp.Amount < Skill.Info.MpCost)
+ { // The character is in combat, mp won't restore.
+ return CanBeUsedResponse.WontBeUsable;
+ }
}
- await combatState.CombatManager.UnregisterSkillCancellationTokenAsync(linkedSource, ct);
- await Task.Delay(1000, ct);
- return Result.FromSuccess();
+ return Skill.IsOnCooldown ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
}
- private IPacket CreateSkillUsePacket(ISkillInfo info)
+ private Result<IContract<SkillUsedEvent, UseSkillStates>> ContractSkill(ISkillInfo info)
{
switch (info.TargetType)
{
case TargetType.SelfOrTarget: // a buff?
case TargetType.Self:
- return CreateSelfTargetedSkillPacket(info);
+ return SkillsApi.ContractUseSkillOnCharacter(Skill);
case TargetType.NoTarget: // area skill?
- return CreateAreaSkillPacket(info);
+ return SkillsApi.ContractUseSkillAt(Skill, Target.Position!.Value.X, Target.Position.Value.Y);
case TargetType.Target:
- return CreateTargetedSkillPacket(info);
+ return SkillsApi.ContractUseSkillOn(Skill, Target);
}
throw new UnreachableException();
}
- private IPacket CreateAreaSkillPacket(ISkillInfo info)
- => new UseAOESkillPacket
- (
- info.CastId,
- Target.Position!.Value.X,
- Target.Position.Value.Y
- );
-
- private IPacket CreateTargetedSkillPacket(ISkillInfo info)
- => new UseSkillPacket
- (
- info.CastId,
- Target.Type,
- Target.Id,
- null,
- null
- );
-
- private IPacket CreateSelfTargetedSkillPacket(ISkillInfo info)
- => new UseSkillPacket
- (
- info.CastId,
- Caster.Type,
- Caster.Id,
- null,
- null
- );
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ _contract?.Unregister();
+ }
}=
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs +57 -3
@@ 4,12 4,14 @@
// 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.Extensions.Combat.Errors;
using NosSmooth.Extensions.Pathfinding;
using NosSmooth.Extensions.Pathfinding.Errors;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Game.Data.Info;
using NosSmooth.Game.Errors;
+using NosSmooth.Packets.Client.Inventory;
using Remora.Results;
namespace NosSmooth.Extensions.Combat.Operations;
@@ 27,6 29,52 @@ public record WalkInRangeOperation
float Distance
) : ICombatOperation
{
+ private Task<Result>? _walkInRangeOperation;
+
+ /// <inheritdoc />
+ public OperationQueueType QueueType => OperationQueueType.TotalControl;
+
+ /// <inheritdoc />
+ public Task<Result> BeginExecution(ICombatState combatState, CancellationToken ct = default)
+ {
+ if (_walkInRangeOperation is not null)
+ {
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ _walkInRangeOperation = Task.Run
+ (
+ () => UseAsync(combatState, ct),
+ ct
+ );
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> WaitForFinishedAsync(ICombatState combatState, CancellationToken ct = default)
+ {
+ if (IsFinished())
+ {
+ return Result.FromSuccess();
+ }
+
+ await BeginExecution(combatState, ct);
+ if (_walkInRangeOperation is null)
+ {
+ throw new UnreachableException();
+ }
+
+ return await _walkInRangeOperation;
+ }
+
+ /// <inheritdoc />
+ public bool IsExecuting()
+ => _walkInRangeOperation is not null && !IsFinished();
+
+ /// <inheritdoc />
+ public bool IsFinished()
+ => _walkInRangeOperation?.IsCompleted ?? false;
+
/// <inheritdoc />
public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
{
@@ 39,8 87,7 @@ public record WalkInRangeOperation
return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
}
- /// <inheritdoc />
- public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
+ private async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
{
var character = combatState.Game.Character;
if (character is null)
@@ 75,7 122,8 @@ public record WalkInRangeOperation
}
using var goToCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
- var walkResultTask = WalkManager.GoToAsync(closePosition.X, closePosition.Y, true, goToCancellationTokenSource.Token);
+ var walkResultTask = WalkManager.GoToAsync
+ (closePosition.X, closePosition.Y, true, goToCancellationTokenSource.Token);
while (!walkResultTask.IsCompleted)
{
@@ 129,4 177,10 @@ public record WalkInRangeOperation
var diffLength = Math.Sqrt(diff.DistanceSquared(Position.Zero));
return target + ((distance / diffLength) * diff);
}
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ _walkInRangeOperation?.Dispose();
+ }
}=
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs +54 -2
@@ 4,6 4,7 @@
// 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.Extensions.Combat.Errors;
using NosSmooth.Extensions.Pathfinding;
using Remora.Results;
@@ 18,6 19,52 @@ namespace NosSmooth.Extensions.Combat.Operations;
/// <param name="Y">The y coordinate to walk to.</param>
public record WalkOperation(WalkManager WalkManager, short X, short Y) : ICombatOperation
{
+ private Task<Result>? _walkOperation;
+
+ /// <inheritdoc />
+ public OperationQueueType QueueType => OperationQueueType.TotalControl;
+
+ /// <inheritdoc />
+ public Task<Result> BeginExecution(ICombatState combatState, CancellationToken ct = default)
+ {
+ if (_walkOperation is not null)
+ {
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ _walkOperation = Task.Run
+ (
+ () => UseAsync(combatState, ct),
+ ct
+ );
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> WaitForFinishedAsync(ICombatState combatState, CancellationToken ct = default)
+ {
+ if (IsFinished())
+ {
+ return Result.FromSuccess();
+ }
+
+ await BeginExecution(combatState, ct);
+ if (_walkOperation is null)
+ {
+ throw new UnreachableException();
+ }
+
+ return await _walkOperation;
+ }
+
+ /// <inheritdoc />
+ public bool IsExecuting()
+ => _walkOperation is not null && !IsFinished();
+
+ /// <inheritdoc />
+ public bool IsFinished()
+ => _walkOperation?.IsCompleted ?? false;
+
/// <inheritdoc />
public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
{
@@ 30,7 77,12 @@ public record WalkOperation(WalkManager WalkManager, short X, short Y) : ICombat
return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
}
- /// <inheritdoc />
- public Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
+ private Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
=> WalkManager.GoToAsync(X, Y, true, ct);
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ _walkOperation?.Dispose();
+ }
}=
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/Policies/EnemyPolicy.cs => Extensions/NosSmooth.Extensions.Combat/Policies/EnemyPolicy.cs +1 -0
@@ 35,6 35,7 @@ public record EnemyPolicy
{
targets = targets.Where(x => MonsterVNums.Contains(x.VNum));
}
+ targets = targets.ToArray();
if (!targets.Any())
{
M Extensions/NosSmooth.Extensions.Combat/Policies/UseItemPolicy.cs => Extensions/NosSmooth.Extensions.Combat/Policies/UseItemPolicy.cs +7 -5
@@ 30,7 30,7 @@ public record UseItemPolicy
) : IItemSelector
{
/// <inheritdoc />
- public Result<Item> GetSelectedItem(ICombatState combatState, ICollection<Item> possibleItems)
+ public Result<InventoryItem> GetSelectedItem(ICombatState combatState, ICollection<InventoryItem> possibleItems)
{
var character = combatState.Game.Character;
if (character is null)
@@ 40,19 40,21 @@ public record UseItemPolicy
if (ShouldUseHpItem(character))
{
- var item = possibleItems.FirstOrDefault(x => UseHealthItemsVNums.Contains(x.ItemVNum));
+ var item = possibleItems.Cast<InventoryItem?>().FirstOrDefault
+ (x => UseHealthItemsVNums.Contains(x?.Item.Item?.ItemVNum ?? -1));
if (item is not null)
{
- return item;
+ return item.Value;
}
}
if (ShouldUseMpItem(character))
{
- var item = possibleItems.FirstOrDefault(x => UseManaItemsVNums.Contains(x.ItemVNum));
+ var item = possibleItems.Cast<InventoryItem?>().FirstOrDefault
+ (x => UseManaItemsVNums.Contains(x?.Item.Item?.ItemVNum ?? -1));
if (item is not null)
{
- return item;
+ return item.Value;
}
}
D Extensions/NosSmooth.Extensions.Combat/Responders/CancelResponder.cs => Extensions/NosSmooth.Extensions.Combat/Responders/CancelResponder.cs +0 -35
@@ 1,35 0,0 @@
-//
-// CancelResponder.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.Packets;
-using NosSmooth.Packets.Server.Skills;
-using Remora.Results;
-
-namespace NosSmooth.Extensions.Combat.Responders;
-
-/// <summary>
-/// Responds to cancel packet.
-/// </summary>
-public class CancelResponder : IPacketResponder<CancelPacket>
-{
- private readonly CombatManager _combatManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CancelResponder"/> class.
- /// </summary>
- /// <param name="combatManager">The combat manager.</param>
- public CancelResponder(CombatManager combatManager)
- {
- _combatManager = combatManager;
- }
-
- /// <inheritdoc />
- public Task<Result> Respond(PacketEventArgs<CancelPacket> packetArgs, CancellationToken ct = default)
- {
- Task.Run(() => _combatManager.CancelSkillTokensAsync(default));
- return Task.FromResult(Result.FromSuccess());
- }
-}>
\ No newline at end of file
D Extensions/NosSmooth.Extensions.Combat/Responders/SkillUseResponder.cs => Extensions/NosSmooth.Extensions.Combat/Responders/SkillUseResponder.cs +0 -52
@@ 1,52 0,0 @@
-//
-// SkillUseResponder.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.Packets;
-using NosSmooth.Packets;
-using NosSmooth.Packets.Server.Battle;
-using Remora.Results;
-
-namespace NosSmooth.Extensions.Combat.Responders;
-
-/// <summary>
-/// Responds to su packet.
-/// </summary>
-public class SuResponder : IPacketResponder<SuPacket>, IPacketResponder<BsPacket>
-{
- private readonly CombatManager _combatManager;
- private readonly Game.Game _game;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SuResponder"/> class.
- /// </summary>
- /// <param name="combatManager">The combat manager.</param>
- /// <param name="game">The game.</param>
- public SuResponder(CombatManager combatManager, Game.Game game)
- {
- _combatManager = combatManager;
- _game = game;
- }
-
- /// <inheritdoc />
- public Task<Result> Respond(PacketEventArgs<SuPacket> packetArgs, CancellationToken ct = default)
- {
- if (packetArgs.Packet.CasterEntityId == _game.Character?.Id)
- {
- Task.Run(() => _combatManager.CancelSkillTokensAsync(default));
- }
- return Task.FromResult(Result.FromSuccess());
- }
-
- /// <inheritdoc />
- public Task<Result> Respond(PacketEventArgs<BsPacket> packetArgs, CancellationToken ct = default)
- {
- if (packetArgs.Packet.CasterEntityId == _game.Character?.Id)
- {
- Task.Run(() => _combatManager.CancelSkillTokensAsync(default));
- }
- return Task.FromResult(Result.FromSuccess());
- }
-}>
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/Selectors/IItemSelector.cs => Extensions/NosSmooth.Extensions.Combat/Selectors/IItemSelector.cs +1 -1
@@ 20,7 20,7 @@ public interface IItemSelector
/// <param name="combatState">The combat state.</param>
/// <param name="possibleItems">The items that may be used.</param>
/// <returns>The selected item, or an error.</returns>
- public Result<Item> GetSelectedItem(ICombatState combatState, ICollection<Item> possibleItems);
+ public Result<InventoryItem> GetSelectedItem(ICombatState combatState, ICollection<InventoryItem> possibleItems);
/// <summary>
/// Gets whether currently an item should be used.
A Extensions/NosSmooth.Extensions.Combat/Selectors/InventoryItem.cs => Extensions/NosSmooth.Extensions.Combat/Selectors/InventoryItem.cs +14 -0
@@ 0,0 1,14 @@
+//
+// InventoryItem.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.CodeAnalysis;
+using NosSmooth.Data.Abstractions.Enums;
+using NosSmooth.Game.Data.Inventory;
+
+namespace NosSmooth.Extensions.Combat.Selectors;
+
+[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Fix")]
+public record struct InventoryItem(BagType Bag, InventorySlot Item);<
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/Techniques/ICombatTechnique.cs => Extensions/NosSmooth.Extensions.Combat/Techniques/ICombatTechnique.cs +23 -4
@@ 18,6 18,15 @@ namespace NosSmooth.Extensions.Combat.Techniques;
public interface ICombatTechnique
{
/// <summary>
+ /// Gets the types this technique may handle.
+ /// </summary>
+ /// <remarks>
+ /// <see cref="HandleNextCombatStep"/> will be called only for queue types
+ /// from this collection.
+ /// </remarks>
+ public IReadOnlyList<OperationQueueType> HandlingQueueTypes { get; }
+
+ /// <summary>
/// Should check whether the technique should process more steps or quit the combat.
/// </summary>
/// <param name="state">The combat state.</param>
@@ 26,23 35,33 @@ public interface ICombatTechnique
/// <summary>
/// Handle one step that should enqueue an operation.
+ /// Enqueue only operation of the given queue type.
/// </summary>
/// <remarks>
/// If error is returned, the combat will be cancelled.
/// </remarks>
+ /// <param name="queueType">The type of the operation to enqueue.</param>
/// <param name="state">The combat state.</param>
/// <returns>An id of the current target entity or an error.</returns>
- public Result<long?> HandleCombatStep(ICombatState state);
+ public Result<long?> HandleNextCombatStep(OperationQueueType queueType, ICombatState state);
+
+ /// <summary>
+ /// Handle waiting for an operation.
+ /// </summary>
+ /// <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>
+ /// <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);
/// <summary>
- /// Handles an error from <see cref="ICombatOperation.UseAsync"/>.
+ /// Handles an arbitrary error.
/// </summary>
/// <remarks>
/// If an error is returned, the combat will be cancelled.
/// </remarks>
/// <param name="state">The combat state.</param>
- /// <param name="operation">The combat operation that returned an error.</param>
/// <param name="result">The errorful result.</param>
/// <returns>A result that may or may not succeed.</returns>
- public Result HandleError(ICombatState state, ICombatOperation operation, Result result);
+ public Result HandleError(ICombatState state, Result result);
}=
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs => Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs +83 -7
@@ 4,14 4,17 @@
// 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;
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 Remora.Results;
namespace NosSmooth.Extensions.Combat.Techniques;
@@ 21,9 24,16 @@ namespace NosSmooth.Extensions.Combat.Techniques;
/// </summary>
public class SimpleAttackTechnique : ICombatTechnique
{
+ private static OperationQueueType[] _handlingTypes = new[]
+ {
+ OperationQueueType.Item, OperationQueueType.TotalControl
+ };
+
private readonly long _targetId;
+ private readonly NostaleSkillsApi _skillsApi;
private readonly WalkManager _walkManager;
private readonly ISkillSelector _skillSelector;
+ private readonly IItemSelector _itemSelector;
private Skill? _currentSkill;
private ILivingEntity? _target;
@@ 32,21 42,30 @@ public class SimpleAttackTechnique : ICombatTechnique
/// Initializes a new instance of the <see cref="SimpleAttackTechnique"/> class.
/// </summary>
/// <param name="targetId">The target entity id.</param>
+ /// <param name="skillsApi">The skills api.</param>
/// <param name="walkManager">The walk manager.</param>
/// <param name="skillSelector">The skill selector.</param>
+ /// <param name="itemSelector">The item selector.</param>
public SimpleAttackTechnique
(
long targetId,
+ NostaleSkillsApi skillsApi,
WalkManager walkManager,
- ISkillSelector skillSelector
+ ISkillSelector skillSelector,
+ IItemSelector itemSelector
)
{
_targetId = targetId;
+ _skillsApi = skillsApi;
_walkManager = walkManager;
_skillSelector = skillSelector;
+ _itemSelector = itemSelector;
}
/// <inheritdoc />
+ public IReadOnlyList<OperationQueueType> HandlingQueueTypes => _handlingTypes;
+
+ /// <inheritdoc />
public bool ShouldContinue(ICombatState state)
{
var map = state.Game.CurrentMap;
@@ 60,7 79,18 @@ public class SimpleAttackTechnique : ICombatTechnique
}
/// <inheritdoc />
- public Result<long?> HandleCombatStep(ICombatState state)
+ public Result HandleWaiting(OperationQueueType queueType, ICombatState state, ICombatOperation operation)
+ { // does not do anything, just wait.
+ return Result.FromSuccess();
+ }
+
+ /// <inheritdoc />
+ public Result HandleError(ICombatState state, Result result)
+ { // no handling of errors is done
+ return result;
+ }
+
+ private Result<long?> HandleTotalControl(ICombatState state)
{
var map = state.Game.CurrentMap;
if (map is null)
@@ 94,8 124,7 @@ public class SimpleAttackTechnique : ICombatTechnique
}
var characterMp = character.Mp?.Amount ?? 0;
- var usableSkills = new[] { skills.PrimarySkill, skills.SecondarySkill }
- .Concat(skills.OtherSkills)
+ var usableSkills = skills.OtherSkills
.Where(x => x.Info is not null && x.Info.HitType != HitType.AlliesInZone)
.Where(x => !x.IsOnCooldown && characterMp >= (x.Info?.MpCost ?? long.MaxValue));
@@ 142,16 171,63 @@ public class SimpleAttackTechnique : ICombatTechnique
}
else
{
- state.UseSkill(_currentSkill, _target);
+ state.UseSkill(_skillsApi, _currentSkill, _target);
_currentSkill = null;
}
return _target.Id;
}
+ private Result<long?> HandleItem(ICombatState state)
+ {
+ var shouldUseItemResult = _itemSelector.ShouldUseItem(state);
+ if (!shouldUseItemResult.IsDefined(out var shouldUseItem))
+ {
+ return Result<long?>.FromError(shouldUseItemResult);
+ }
+
+ if (!shouldUseItem)
+ {
+ return _targetId;
+ }
+
+ var inventory = state.Game.Inventory;
+ if (inventory is null)
+ {
+ return _targetId;
+ }
+
+ var main = inventory.GetBag(BagType.Main)
+ .Where(x => x is { Amount: > 0, Item.Info.Type: ItemType.Potion })
+ .Select(x => new InventoryItem(BagType.Main, x));
+ var etc = inventory.GetBag(BagType.Etc)
+ .Where(x => x is { Amount: > 0, Item.Info.Type: ItemType.Food or ItemType.Snack })
+ .Select(x => new InventoryItem(BagType.Etc, x));
+
+ var possibleItems = main.Concat(etc).ToList();
+
+ var itemResult = _itemSelector.GetSelectedItem(state, possibleItems);
+
+ if (!itemResult.IsDefined(out var item))
+ {
+ return Result<long?>.FromError(itemResult);
+ }
+
+ state.UseItem(item);
+ return _targetId;
+ }
+
/// <inheritdoc />
- public Result HandleError(ICombatState state, ICombatOperation operation, Result result)
+ public Result<long?> HandleNextCombatStep(OperationQueueType queueType, ICombatState state)
{
- return result;
+ switch (queueType)
+ {
+ case OperationQueueType.Item:
+ return HandleItem(state);
+ case OperationQueueType.TotalControl:
+ return HandleTotalControl(state);
+ }
+
+ throw new InvalidOperationException("SimpleAttackTechnique supports only Item and TotalControl queue types.");
}
}=
\ No newline at end of file