From 972cf4714e11a5760eeb8c316be34625a2d240f7 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Tue, 17 Jan 2023 22:53:04 +0100 Subject: [PATCH] feat(combat): update operations to be non-blocking, add support for handling waiting --- .../CombatManager.cs | 166 ++++++++++++------ .../CombatState.cs | 100 +++++++---- .../Extensions/CombatStateExtensions.cs | 19 +- .../Extensions/ServiceCollectionExtensions.cs | 4 - .../ICombatState.cs | 31 ++++ .../OperationQueueType.cs | 73 ++++++++ .../Operations/ICombatOperation.cs | 58 ++++-- .../Operations/UseItemOperation.cs | 60 ++++++- .../Operations/UsePrimarySkillOperation.cs | 77 -------- .../Operations/UseSkillOperation.cs | 136 +++++++------- .../Operations/WalkInRangeOperation.cs | 60 ++++++- .../Operations/WalkOperation.cs | 56 +++++- .../Policies/EnemyPolicy.cs | 1 + .../Policies/UseItemPolicy.cs | 12 +- .../Responders/CancelResponder.cs | 35 ---- .../Responders/SkillUseResponder.cs | 52 ------ .../Selectors/IItemSelector.cs | 2 +- .../Selectors/InventoryItem.cs | 14 ++ .../Techniques/ICombatTechnique.cs | 27 ++- .../Techniques/SimpleAttackTechnique.cs | 90 +++++++++- 20 files changed, 709 insertions(+), 364 deletions(-) create mode 100644 Extensions/NosSmooth.Extensions.Combat/OperationQueueType.cs delete mode 100644 Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs delete mode 100644 Extensions/NosSmooth.Extensions.Combat/Responders/CancelResponder.cs delete mode 100644 Extensions/NosSmooth.Extensions.Combat/Responders/SkillUseResponder.cs create mode 100644 Extensions/NosSmooth.Extensions.Combat/Selectors/InventoryItem.cs diff --git a/Extensions/NosSmooth.Extensions.Combat/CombatManager.cs b/Extensions/NosSmooth.Extensions.Combat/CombatManager.cs index 84c2cca5a947b867e4aa38df2979de5e206ccbf2..5f49ce560fbb48c8105198b3acfed322393c4bee 100644 --- a/Extensions/NosSmooth.Extensions.Combat/CombatManager.cs +++ b/Extensions/NosSmooth.Extensions.Combat/CombatManager.cs @@ -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 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> 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().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> 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); + } + /// /// Register the given cancellation token source to be cancelled on skill use/cancel. /// diff --git a/Extensions/NosSmooth.Extensions.Combat/CombatState.cs b/Extensions/NosSmooth.Extensions.Combat/CombatState.cs index e65461a81e9a98ce797d585cf5b39672d24bbaba..96e6f685640e448ceaca36293b7560aeb7a1a9ad 100644 --- a/Extensions/NosSmooth.Extensions.Combat/CombatState.cs +++ b/Extensions/NosSmooth.Extensions.Combat/CombatState.cs @@ -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; /// internal class CombatState : ICombatState { - private readonly LinkedList _operations; - private ICombatOperation? _currentOperation; + private readonly Dictionary> _operations; + private readonly Dictionary _currentOperations; /// /// Initializes a new instance of the class. @@ -28,7 +27,8 @@ internal class CombatState : ICombatState Client = client; Game = game; CombatManager = combatManager; - _operations = new LinkedList(); + _operations = new Dictionary>(); + _currentOperations = new Dictionary(); } /// @@ -46,62 +46,98 @@ internal class CombatState : ICombatState public INostaleClient Client { get; } /// - public void QuitCombat() - { - ShouldQuit = true; - } + public bool IsWaitingOnOperation => _currentOperations.Any(x => !x.Value.IsExecuting()); /// - /// Make a step in the queue. + /// Move to next operation, if available. /// - /// The current operation, if any. - public ICombatOperation? NextOperation() + /// The queue type to move to next operation in. + /// Next operation, if any. + 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; + } + + /// + public IReadOnlyList GetWaitingForOperations() + { + return _currentOperations.Values.Where(x => !x.IsExecuting()).ToList(); + } + + /// + public ICombatOperation? GetCurrentOperation(OperationQueueType queueType) + => _currentOperations.GetValueOrDefault(queueType); + + /// + public bool IsExecutingOperation(OperationQueueType queueType, [NotNullWhen(true)] out ICombatOperation? operation) + { + operation = GetCurrentOperation(queueType); + return operation is not null && operation.IsExecuting(); + } + + /// + public void QuitCombat() + { + ShouldQuit = true; } /// 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(); + } 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; } /// public void EnqueueOperation(ICombatOperation operation) { - _operations.AddLast(operation); + var type = operation.QueueType; + + if (!_operations.ContainsKey(type)) + { + _operations[type] = new LinkedList(); + } + + _operations[type].AddLast(operation); } /// public void RemoveOperations(Func 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 diff --git a/Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs b/Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs index 5a8ae5ca4d31ba9832d371dca5e1d11c4a3643c9..a98ba933ea17f5e53dcf4979a9006172cddb5b89 100644 --- a/Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs +++ b/Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs @@ -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. /// /// The combat state. + /// The skills api for using a skill. /// The skill. /// The target to use skill at. - 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)); - } - - /// - /// Use primary skill. - /// - /// The combat state. - /// The target to use skill at. - public static void UsePrimarySkill(this ICombatState combatState, ILivingEntity target) - { - combatState.EnqueueOperation(new UsePrimarySkillOperation(target)); + combatState.EnqueueOperation(new UseSkillOperation(skillsApi, skill, combatState.Game.Character, target)); } /// @@ -88,7 +81,7 @@ public static class CombatStateExtensions /// /// The combat state. /// The item to use. - public static void UseItem(this ICombatState combatState, Item item) + public static void UseItem(this ICombatState combatState, InventoryItem item) { combatState.EnqueueOperation(new UseItemOperation(item)); } diff --git a/Extensions/NosSmooth.Extensions.Combat/Extensions/ServiceCollectionExtensions.cs b/Extensions/NosSmooth.Extensions.Combat/Extensions/ServiceCollectionExtensions.cs index eebcf9d3015d866270072a75779fe18bcdc67847..c4d595eb8b060073c7d0a89dbd3925a30e51b7d3 100644 --- a/Extensions/NosSmooth.Extensions.Combat/Extensions/ServiceCollectionExtensions.cs +++ b/Extensions/NosSmooth.Extensions.Combat/Extensions/ServiceCollectionExtensions.cs @@ -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() - .AddPacketResponder() .AddSingleton(); } } \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/ICombatState.cs b/Extensions/NosSmooth.Extensions.Combat/ICombatState.cs index 97fd99d6b43ffeda77e86380dd85fdd6605cff3b..0397f62471511e685def9e62aa154db516982e0c 100644 --- a/Extensions/NosSmooth.Extensions.Combat/ICombatState.cs +++ b/Extensions/NosSmooth.Extensions.Combat/ICombatState.cs @@ -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; @@ -30,6 +31,36 @@ public interface ICombatState /// public INostaleClient Client { get; } + /// + /// Gets whether there is an operation that cannot be used + /// and we must wait for it to be usable. + /// + public bool IsWaitingOnOperation { get; } + + /// + /// Get the operations the state is waiting for to to be usable. + /// + /// The operations needed to wait for. + public IReadOnlyList GetWaitingForOperations(); + + /// + /// Gets the current operation of the given queue type. + /// + /// The queue type to get the current operation of. + /// The operation of the given queue, if any. + public ICombatOperation? GetCurrentOperation(OperationQueueType queueType); + + /// + /// Checks whether an operation is being executed in the given queue. + /// + /// + /// If not, either waiting for the operation or there is no operation enqueued. + /// + /// The type of queue to look at. + /// The operation currently being executed. + /// Whether an operation is being executed. + public bool IsExecutingOperation(OperationQueueType queueType, [NotNullWhen(true)] out ICombatOperation? operation); + /// /// Cancel the combat technique, quit the combat state. /// diff --git a/Extensions/NosSmooth.Extensions.Combat/OperationQueueType.cs b/Extensions/NosSmooth.Extensions.Combat/OperationQueueType.cs new file mode 100644 index 0000000000000000000000000000000000000000..271b29e9dc7fff20fe898a4cba6632cf069b1c13 --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Combat/OperationQueueType.cs @@ -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; + +/// +/// A type classifying operations into multiple sequential queues. +/// +public enum OperationQueueType +{ + /// + /// A total control of controls is needed (walking, sitting - recovering, using a skill). + /// + TotalControl, + + /// + /// An item is being used. + /// + Item, + + /// + /// Any operation that should be executed sequentially with other operations. + /// + Reserved1, + + /// + /// Any operation that should be executed sequentially with other operations. + /// + Reserved2, + + /// + /// Any operation that should be executed sequentially with other operations. + /// + Reserved3, + + /// + /// Any operation that should be executed sequentially with other operations. + /// + Reserved4, + + /// + /// Any operation that should be executed sequentially with other operations. + /// + Reserved5, + + /// + /// Any operation that should be executed sequentially with other operations. + /// + Reserved6, + + /// + /// Any operation that should be executed sequentially with other operations. + /// + Reserved7, + + /// + /// Any operation that should be executed sequentially with other operations. + /// + Reserved8, + + /// + /// Any operation that should be executed sequentially with other operations. + /// + Reserved9, + + /// + /// Any operation that should be executed sequentially with other operations. + /// + Reserved10, +} \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs index d99c81a3ae85be53f02d9a46a3b50f98e52902b3..412f1847b75c17a48814e2600e1c749b2406334e 100644 --- a/Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs +++ b/Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs @@ -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; /// /// A combat operation used in that can be used as one step. /// -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 + /// - /// Checks whether the operation can currently be used. + /// Gets the queue type the operation belongs to. /// /// - /// 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. /// + public OperationQueueType QueueType { get; } + + /// + /// Begin the execution without waiting for the finished state. + /// /// The combat state. - /// Whether the operation can be used right away. - public Result CanBeUsed(ICombatState combatState); + /// The cancellation token used for cancelling the operation. + /// A result that may or may not have succeeded. + public Task BeginExecution(ICombatState combatState, CancellationToken ct = default); + + /// + /// Asynchronously wait for finished state. + /// + /// The combat state. + /// The cancellation token used for cancelling the operation. + /// A representing the asynchronous operation. + public Task WaitForFinishedAsync(ICombatState combatState, CancellationToken ct = default); + + /// + /// Checks whether the operation is currently being executed. + /// + /// Whether the operation is being executed. + public bool IsExecuting(); + + /// + /// Checks whether the operation is done. + /// + /// Whether the operation is finished. + public bool IsFinished(); /// - /// Use the operation, if possible. + /// Checks whether the operation can currently be used. /// /// - /// 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. /// /// The combat state. - /// The cancellation token for cancelling the operation. - /// A result that may or may not succeed. - public Task UseAsync(ICombatState combatState, CancellationToken ct = default); + /// Whether the operation can be used right away. + public Result CanBeUsed(ICombatState combatState); } \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs index 9d3fefedfea41a1468a7e5a276138d4f8b03b712..6d468e8c44af013b2b7995721e1537400b0e6ad5 100644 --- a/Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs +++ b/Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs @@ -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. /// /// The item to use. -public record UseItemOperation(Item Item) : ICombatOperation +public record UseItemOperation(InventoryItem Item) : ICombatOperation { + private Task? _useItemOperation; + /// - public Result CanBeUsed(ICombatState combatState) + public OperationQueueType QueueType => OperationQueueType.Item; + + /// + public Task 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()); + } + + /// + public Task 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; } /// - public Task UseAsync(ICombatState combatState, CancellationToken ct = default) + public bool IsExecuting() + => _useItemOperation is not null && !IsFinished(); + + /// + public bool IsFinished() + => _useItemOperation?.IsCompleted ?? false; + + /// + public Result CanBeUsed(ICombatState combatState) + => CanBeUsedResponse.CanBeUsed; + + /// + public void Dispose() { - throw new NotImplementedException(); + _useItemOperation?.Dispose(); } } \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs deleted file mode 100644 index 4d3894122e6ce99d932085374c9e0222dd22cfcb..0000000000000000000000000000000000000000 --- a/Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs +++ /dev/null @@ -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; - -/// -/// An operation that uses the primary skill of the character. -/// -public record UsePrimarySkillOperation(ILivingEntity Target) : ICombatOperation -{ - private UseSkillOperation? _useSkillOperation; - - /// - public Result CanBeUsed(ICombatState combatState) - { - 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 _useSkillOperation.CanBeUsed(combatState); - } - - /// - public async Task 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 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 diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs index 897364b042c0cdde534c99b9dceb0a00f1b4bbbe..a464e073667a24d14b1c099c1ed418c90b7fdfe9 100644 --- a/Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs +++ b/Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs @@ -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; /// /// A combat operation to use a skill. /// +/// The skills api used for executing the skills. /// The skill to use. /// The caster entity that is using the skill. /// The target entity to use the skill at. -public record UseSkillOperation(Skill Skill, ILivingEntity Caster, ILivingEntity Target) : ICombatOperation +public record UseSkillOperation(NostaleSkillsApi SkillsApi, Skill Skill, ILivingEntity Caster, ILivingEntity Target) : ICombatOperation { + private IContract? _contract; + /// - public Result CanBeUsed(ICombatState combatState) + public OperationQueueType QueueType => OperationQueueType.TotalControl; + + /// + public async Task 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; } /// - public async Task UseAsync(ICombatState combatState, CancellationToken ct = default) + public async Task 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); + } + + /// + public bool IsExecuting() + => _contract is not null && _contract.CurrentState > UseSkillStates.None && !IsFinished(); + + /// + public bool IsFinished() + => _contract?.CurrentState == UseSkillStates.CharacterRestored; - if (!sendResponse.IsSuccess) + /// + public Result 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> 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 - ); + /// + public void Dispose() + { + _contract?.Unregister(); + } } \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs index c0b37be9022c4c1efbf642657742ab391133e785..4ac553010178cc58d220d260aa0d0e3ed22267c1 100644 --- a/Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs +++ b/Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs @@ -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? _walkInRangeOperation; + + /// + public OperationQueueType QueueType => OperationQueueType.TotalControl; + + /// + public Task 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()); + } + + /// + public async Task 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; + } + + /// + public bool IsExecuting() + => _walkInRangeOperation is not null && !IsFinished(); + + /// + public bool IsFinished() + => _walkInRangeOperation?.IsCompleted ?? false; + /// public Result CanBeUsed(ICombatState combatState) { @@ -39,8 +87,7 @@ public record WalkInRangeOperation return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed; } - /// - public async Task UseAsync(ICombatState combatState, CancellationToken ct = default) + private async Task 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); } + + /// + public void Dispose() + { + _walkInRangeOperation?.Dispose(); + } } \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs index cdaef76f918bce28a111e7b69dc41843409d4ae4..10a61798eb0d85dc1eac7216f7d405943db0a070 100644 --- a/Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs +++ b/Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs @@ -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; /// The y coordinate to walk to. public record WalkOperation(WalkManager WalkManager, short X, short Y) : ICombatOperation { + private Task? _walkOperation; + + /// + public OperationQueueType QueueType => OperationQueueType.TotalControl; + + /// + public Task 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()); + } + + /// + public async Task 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; + } + + /// + public bool IsExecuting() + => _walkOperation is not null && !IsFinished(); + + /// + public bool IsFinished() + => _walkOperation?.IsCompleted ?? false; + /// public Result CanBeUsed(ICombatState combatState) { @@ -30,7 +77,12 @@ public record WalkOperation(WalkManager WalkManager, short X, short Y) : ICombat return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed; } - /// - public Task UseAsync(ICombatState combatState, CancellationToken ct = default) + private Task UseAsync(ICombatState combatState, CancellationToken ct = default) => WalkManager.GoToAsync(X, Y, true, ct); + + /// + public void Dispose() + { + _walkOperation?.Dispose(); + } } \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Policies/EnemyPolicy.cs b/Extensions/NosSmooth.Extensions.Combat/Policies/EnemyPolicy.cs index 1d971b9d0e4bf39b715b6c94536f123d6f927304..171ffbf1240fa6a88ef85e230c79389f3af62dc3 100644 --- a/Extensions/NosSmooth.Extensions.Combat/Policies/EnemyPolicy.cs +++ b/Extensions/NosSmooth.Extensions.Combat/Policies/EnemyPolicy.cs @@ -35,6 +35,7 @@ public record EnemyPolicy { targets = targets.Where(x => MonsterVNums.Contains(x.VNum)); } + targets = targets.ToArray(); if (!targets.Any()) { diff --git a/Extensions/NosSmooth.Extensions.Combat/Policies/UseItemPolicy.cs b/Extensions/NosSmooth.Extensions.Combat/Policies/UseItemPolicy.cs index 0150451e22a03063d9f326c56a15beeb4a90b9a6..eba629503851c31a7c44b9d5b5d789f6a48c2e8e 100644 --- a/Extensions/NosSmooth.Extensions.Combat/Policies/UseItemPolicy.cs +++ b/Extensions/NosSmooth.Extensions.Combat/Policies/UseItemPolicy.cs @@ -30,7 +30,7 @@ public record UseItemPolicy ) : IItemSelector { /// - public Result GetSelectedItem(ICombatState combatState, ICollection possibleItems) + public Result GetSelectedItem(ICombatState combatState, ICollection 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().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().FirstOrDefault + (x => UseManaItemsVNums.Contains(x?.Item.Item?.ItemVNum ?? -1)); if (item is not null) { - return item; + return item.Value; } } diff --git a/Extensions/NosSmooth.Extensions.Combat/Responders/CancelResponder.cs b/Extensions/NosSmooth.Extensions.Combat/Responders/CancelResponder.cs deleted file mode 100644 index 210932dc8a95cd6c0a7bb855ed4b23facf6e6168..0000000000000000000000000000000000000000 --- a/Extensions/NosSmooth.Extensions.Combat/Responders/CancelResponder.cs +++ /dev/null @@ -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; - -/// -/// Responds to cancel packet. -/// -public class CancelResponder : IPacketResponder -{ - private readonly CombatManager _combatManager; - - /// - /// Initializes a new instance of the class. - /// - /// The combat manager. - public CancelResponder(CombatManager combatManager) - { - _combatManager = combatManager; - } - - /// - public Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) - { - Task.Run(() => _combatManager.CancelSkillTokensAsync(default)); - return Task.FromResult(Result.FromSuccess()); - } -} \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Responders/SkillUseResponder.cs b/Extensions/NosSmooth.Extensions.Combat/Responders/SkillUseResponder.cs deleted file mode 100644 index 85a5bae1225f3b7179eeb9cf3c4609e03bd54fe9..0000000000000000000000000000000000000000 --- a/Extensions/NosSmooth.Extensions.Combat/Responders/SkillUseResponder.cs +++ /dev/null @@ -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; - -/// -/// Responds to su packet. -/// -public class SuResponder : IPacketResponder, IPacketResponder -{ - private readonly CombatManager _combatManager; - private readonly Game.Game _game; - - /// - /// Initializes a new instance of the class. - /// - /// The combat manager. - /// The game. - public SuResponder(CombatManager combatManager, Game.Game game) - { - _combatManager = combatManager; - _game = game; - } - - /// - public Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) - { - if (packetArgs.Packet.CasterEntityId == _game.Character?.Id) - { - Task.Run(() => _combatManager.CancelSkillTokensAsync(default)); - } - return Task.FromResult(Result.FromSuccess()); - } - - /// - public Task Respond(PacketEventArgs 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 diff --git a/Extensions/NosSmooth.Extensions.Combat/Selectors/IItemSelector.cs b/Extensions/NosSmooth.Extensions.Combat/Selectors/IItemSelector.cs index a02637c86a1e1725444a03635d12399f2d6b2dc7..41ac53e87ae584dcaa52161843763b9467a89452 100644 --- a/Extensions/NosSmooth.Extensions.Combat/Selectors/IItemSelector.cs +++ b/Extensions/NosSmooth.Extensions.Combat/Selectors/IItemSelector.cs @@ -20,7 +20,7 @@ public interface IItemSelector /// The combat state. /// The items that may be used. /// The selected item, or an error. - public Result GetSelectedItem(ICombatState combatState, ICollection possibleItems); + public Result GetSelectedItem(ICombatState combatState, ICollection possibleItems); /// /// Gets whether currently an item should be used. diff --git a/Extensions/NosSmooth.Extensions.Combat/Selectors/InventoryItem.cs b/Extensions/NosSmooth.Extensions.Combat/Selectors/InventoryItem.cs new file mode 100644 index 0000000000000000000000000000000000000000..48020062a8874bce602fbbb8525db1b57fedbcca --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Combat/Selectors/InventoryItem.cs @@ -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 diff --git a/Extensions/NosSmooth.Extensions.Combat/Techniques/ICombatTechnique.cs b/Extensions/NosSmooth.Extensions.Combat/Techniques/ICombatTechnique.cs index e75c22a0a3e7a22267ecffe8b5ce26ddbc70473b..1c7ebdc14579103683d388984174d789682a2435 100644 --- a/Extensions/NosSmooth.Extensions.Combat/Techniques/ICombatTechnique.cs +++ b/Extensions/NosSmooth.Extensions.Combat/Techniques/ICombatTechnique.cs @@ -17,6 +17,15 @@ namespace NosSmooth.Extensions.Combat.Techniques; /// public interface ICombatTechnique { + /// + /// Gets the types this technique may handle. + /// + /// + /// will be called only for queue types + /// from this collection. + /// + public IReadOnlyList HandlingQueueTypes { get; } + /// /// Should check whether the technique should process more steps or quit the combat. /// @@ -26,23 +35,33 @@ public interface ICombatTechnique /// /// Handle one step that should enqueue an operation. + /// Enqueue only operation of the given queue type. /// /// /// If error is returned, the combat will be cancelled. /// + /// The type of the operation to enqueue. /// The combat state. /// An id of the current target entity or an error. - public Result HandleCombatStep(ICombatState state); + public Result HandleNextCombatStep(OperationQueueType queueType, ICombatState state); + + /// + /// Handle waiting for an operation. + /// + /// The type of the operation. + /// The combat state. + /// The operation that needs waiting. + /// A result that may or may not have succeeded. In case of an error, will be called with the error. + public Result HandleWaiting(OperationQueueType queueType, ICombatState state, ICombatOperation operation); /// - /// Handles an error from . + /// Handles an arbitrary error. /// /// /// If an error is returned, the combat will be cancelled. /// /// The combat state. - /// The combat operation that returned an error. /// The errorful result. /// A result that may or may not succeed. - public Result HandleError(ICombatState state, ICombatOperation operation, Result result); + public Result HandleError(ICombatState state, Result result); } \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs b/Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs index 9d1d9a05671b2ebfdff138918b81182683e8dd26..22c39cf8d9732a842a09ccdd436b49ea9377fcd0 100644 --- a/Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs +++ b/Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs @@ -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; /// 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,20 +42,29 @@ public class SimpleAttackTechnique : ICombatTechnique /// Initializes a new instance of the class. /// /// The target entity id. + /// The skills api. /// The walk manager. /// The skill selector. + /// The item selector. public SimpleAttackTechnique ( long targetId, + NostaleSkillsApi skillsApi, WalkManager walkManager, - ISkillSelector skillSelector + ISkillSelector skillSelector, + IItemSelector itemSelector ) { _targetId = targetId; + _skillsApi = skillsApi; _walkManager = walkManager; _skillSelector = skillSelector; + _itemSelector = itemSelector; } + /// + public IReadOnlyList HandlingQueueTypes => _handlingTypes; + /// public bool ShouldContinue(ICombatState state) { @@ -60,7 +79,18 @@ public class SimpleAttackTechnique : ICombatTechnique } /// - public Result HandleCombatStep(ICombatState state) + public Result HandleWaiting(OperationQueueType queueType, ICombatState state, ICombatOperation operation) + { // does not do anything, just wait. + return Result.FromSuccess(); + } + + /// + public Result HandleError(ICombatState state, Result result) + { // no handling of errors is done + return result; + } + + private Result 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 HandleItem(ICombatState state) + { + var shouldUseItemResult = _itemSelector.ShouldUseItem(state); + if (!shouldUseItemResult.IsDefined(out var shouldUseItem)) + { + return Result.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.FromError(itemResult); + } + + state.UseItem(item); + return _targetId; + } + /// - public Result HandleError(ICombatState state, ICombatOperation operation, Result result) + public Result 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