From c5234a389e51f0fca6b49cbaf04aef9d91c1c5d4 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Fri, 20 Jan 2023 20:01:18 +0100 Subject: [PATCH] feat(combat): add compound operation to simplify using skills --- .../Operations/CompoundOperation.cs | 133 ++++++++++++++++++ .../Techniques/SimpleAttackTechnique.cs | 75 +++++----- 2 files changed, 166 insertions(+), 42 deletions(-) create mode 100644 Extensions/NosSmooth.Extensions.Combat/Operations/CompoundOperation.cs diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/CompoundOperation.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/CompoundOperation.cs new file mode 100644 index 0000000..e0fefc0 --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Combat/Operations/CompoundOperation.cs @@ -0,0 +1,133 @@ +// +// CompoundOperation.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics; +using Remora.Results; + +namespace NosSmooth.Extensions.Combat.Operations; + +/// +/// An operation made from multiple operations +/// that should execute right after each other. +/// +public class CompoundOperation : ICombatOperation +{ + private readonly ICombatOperation[] _operations; + private readonly OperationQueueType _queueType; + private Task? _compoundOperation; + + /// + /// Initializes a new instance of the class. + /// + /// The operations to execute. + /// The queue type. + public CompoundOperation + (OperationQueueType queueType = OperationQueueType.TotalControl, params ICombatOperation[] operations) + { + if (operations.Length == 0) + { + throw new ArgumentNullException(nameof(operations), "The compound operation needs at least one operation."); + } + + _operations = operations; + _queueType = queueType; + } + + /// + public void Dispose() + { + foreach (var operation in _operations) + { + operation.Dispose(); + } + } + + /// + public OperationQueueType QueueType { get; } + + /// + public Task BeginExecution(ICombatState combatState, CancellationToken ct = default) + { + if (_compoundOperation is not null) + { + return Task.FromResult(Result.FromSuccess()); + } + + _compoundOperation = 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 (_compoundOperation is null) + { + throw new UnreachableException(); + } + + return await _compoundOperation; + } + + /// + public bool IsExecuting() + => _compoundOperation is not null && !IsFinished(); + + /// + public bool IsFinished() + => _compoundOperation?.IsCompleted ?? false; + + /// + public Result CanBeUsed(ICombatState combatState) + => _operations[0].CanBeUsed(combatState); + + private async Task UseAsync(ICombatState combatState, CancellationToken ct) + { + foreach (var operation in _operations) + { + CanBeUsedResponse canBeUsed = CanBeUsedResponse.MustWait; + + while (canBeUsed != CanBeUsedResponse.CanBeUsed) + { + var canBeUsedResult = operation.CanBeUsed(combatState); + if (!canBeUsedResult.IsDefined(out canBeUsed)) + { + return Result.FromError(canBeUsedResult); + } + + if (canBeUsed == CanBeUsedResponse.WontBeUsable) + { + return new GenericError("Won't be usable."); + } + + await Task.Delay(10, ct); + } + + var result = await operation.BeginExecution(combatState, ct); + if (!result.IsSuccess) + { + return result; + } + + result = await operation.WaitForFinishedAsync(combatState, ct); + if (!result.IsSuccess) + { + return result; + } + } + + return Result.FromSuccess(); + } +} \ 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 16df380..8d99c4e 100644 --- a/Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs +++ b/Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs @@ -35,7 +35,6 @@ public class SimpleAttackTechnique : ICombatTechnique private readonly ISkillSelector _skillSelector; private readonly IItemSelector _itemSelector; - private Skill? _currentSkill; private ILivingEntity? _target; /// @@ -119,41 +118,34 @@ public class SimpleAttackTechnique : ICombatTechnique return new CharacterNotInitializedError(); } - if (_currentSkill is null) + var skills = state.Game.Skills; + if (skills is null) { - var skills = state.Game.Skills; - if (skills is null) - { - return new CharacterNotInitializedError("Skills"); - } + return new CharacterNotInitializedError("Skills"); + } - var characterMp = character.Mp?.Amount ?? 0; - var usableSkills = skills.AllSkills - .Where - ( - x => x.Info is not null && x.Info.HitType != HitType.AlliesInZone - && x.Info.SkillType == SkillType.Player - ) - .Where(x => !x.IsOnCooldown && characterMp >= (x.Info?.MpCost ?? long.MaxValue)); - - var skillResult = _skillSelector.GetSelectedSkill(usableSkills); - if (!skillResult.IsSuccess) - { - if (skillResult.Error is SkillNotFoundError) - { - return _target.Id; - } + var characterMp = character.Mp?.Amount ?? 0; + var usableSkills = skills.AllSkills + .Where + ( + x => x.Info is not null && x.Info.HitType != HitType.AlliesInZone + && x.Info.SkillType == SkillType.Player + ) + .Where(x => !x.IsOnCooldown && characterMp >= (x.Info?.MpCost ?? long.MaxValue)); - return Result.FromError(skillResult); + var skillResult = _skillSelector.GetSelectedSkill(usableSkills); + if (!skillResult.IsDefined(out var currentSkill)) + { + if (skillResult.Error is SkillNotFoundError) + { + return _target.Id; } - _currentSkill = skillResult.Entity; + return Result.FromError(skillResult); } - if (_currentSkill.Info is null) + if (currentSkill.Info is null) { - var currentSkill = _currentSkill; - _currentSkill = null; return new MissingInfoError("skill", currentSkill.SkillVNum); } @@ -167,23 +159,22 @@ public class SimpleAttackTechnique : ICombatTechnique return new EntityNotFoundError(); } - var range = _currentSkill.Info.Range; - if (_currentSkill.Info.TargetType == TargetType.Self && _currentSkill.Info.HitType == HitType.EnemiesInZone - && _currentSkill.Info.AttackType != AttackType.Dash) - { - range = _currentSkill.Info.ZoneRange; - } - - if (!character.Position.Value.IsInRange(_target.Position.Value, range)) - { - state.WalkInRange(_walkManager, _target, range); - } - else + var range = currentSkill.Info.Range; + if (currentSkill.Info.TargetType == TargetType.Self && currentSkill.Info.HitType == HitType.EnemiesInZone + && currentSkill.Info.AttackType != AttackType.Dash) { - state.UseSkill(_skillsApi, _currentSkill, _target); - _currentSkill = null; + range = currentSkill.Info.ZoneRange; } + state.EnqueueOperation + ( + new CompoundOperation + ( + OperationQueueType.TotalControl, + new WalkInRangeOperation(_walkManager, _target, range), + new UseSkillOperation(_skillsApi, currentSkill, character, _target) + ) + ); return _target.Id; } -- 2.48.1