From f0507f74c29dcda0f857bb7401191db755b430fd Mon Sep 17 00:00:00 2001 From: Rutherther Date: Fri, 18 Feb 2022 23:33:20 +0100 Subject: [PATCH] feat(combat): add combat operations --- .../Extensions/CombatStateExtensions.cs | 90 +++++++++++++++++++ .../ICombatState.cs | 58 ++++++++++++ .../Operations/CanBeUsedResponse.cs | 28 ++++++ .../Operations/ICombatOperation.cs | 38 ++++++++ .../Operations/UseItemOperation.cs | 26 ++++++ .../Operations/UsePrimarySkillOperation.cs | 72 +++++++++++++++ .../Operations/UseSkillOperation.cs | 77 ++++++++++++++++ .../Operations/WalkInRangeOperation.cs | 84 +++++++++++++++++ .../Operations/WalkOperation.cs | 38 ++++++++ 9 files changed, 511 insertions(+) create mode 100644 Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs create mode 100644 Extensions/NosSmooth.Extensions.Combat/ICombatState.cs create mode 100644 Extensions/NosSmooth.Extensions.Combat/Operations/CanBeUsedResponse.cs create mode 100644 Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs create mode 100644 Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs create mode 100644 Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs create mode 100644 Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs create mode 100644 Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs create mode 100644 Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs diff --git a/Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs b/Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs new file mode 100644 index 0000000..829d5a6 --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs @@ -0,0 +1,90 @@ +// +// CombatStateExtensions.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Extensions.Combat.Errors; +using NosSmooth.Extensions.Combat.Operations; +using NosSmooth.Extensions.Combat.Policies; +using NosSmooth.Extensions.Pathfinding; +using NosSmooth.Game.Data.Characters; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Items; +using OneOf.Types; +using Remora.Results; + +namespace NosSmooth.Extensions.Combat.Extensions; + +/// +/// Extension methods for . +/// +public static class CombatStateExtensions +{ + /// + /// Walk in the range of the given entity. + /// + /// The combat state. + /// The walk manager. + /// The entity. + /// The range distance to walk to. + public static void WalkInRange + ( + this ICombatState state, + WalkManager walkManager, + IEntity entity, + float range + ) + { + state.EnqueueOperation(new WalkInRangeOperation(walkManager, entity, range)); + } + + /// + /// Walk to the given position. + /// + /// The combat state. + /// The walk manager. + /// The target x coordinate. + /// The target y coordinate. + public static void WalkTo + ( + this ICombatState combatState, + WalkManager walkManager, + short x, + short y + ) + { + combatState.EnqueueOperation(new WalkOperation(walkManager, x, y)); + } + + /// + /// Use the given skill. + /// + /// The combat state. + /// The skill. + /// The target to use skill at. + public static void UseSkill(this ICombatState combatState, Skill skill, ILivingEntity target) + { + combatState.EnqueueOperation(new UseSkillOperation(skill, 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)); + } + + /// + /// Use the given item. + /// + /// The combat state. + /// The item to use. + public static void UseItem(this ICombatState combatState, Item item) + { + combatState.EnqueueOperation(new UseItemOperation(item)); + } +} \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/ICombatState.cs b/Extensions/NosSmooth.Extensions.Combat/ICombatState.cs new file mode 100644 index 0000000..97fd99d --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Combat/ICombatState.cs @@ -0,0 +1,58 @@ +// +// ICombatState.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Core.Client; +using NosSmooth.Extensions.Combat.Operations; +using NosSmooth.Game.Data.Entities; + +namespace NosSmooth.Extensions.Combat; + +/// +/// The combat technique state used for queuing operations and storing information. +/// +public interface ICombatState +{ + /// + /// Gets the combat manager. + /// + public CombatManager CombatManager { get; } + + /// + /// Gets the game. + /// + public Game.Game Game { get; } + + /// + /// Gets the NosTale client. + /// + public INostaleClient Client { get; } + + /// + /// Cancel the combat technique, quit the combat state. + /// + public void QuitCombat(); + + /// + /// Replace the current operation with this one. + /// + /// The operation to use. + /// Whether to empty the queue of the operations. + /// Whether to still use the current operation (true) after this one or discard it (false). + public void SetCurrentOperation + (ICombatOperation operation, bool emptyQueue = false, bool prependCurrentOperationToQueue = false); + + /// + /// Enqueue the operation at the end of the queue. + /// + /// The operation to enqueue. + public void EnqueueOperation(ICombatOperation operation); + + /// + /// Remove the operations by the given filter. + /// + /// Called for each operation, should return true if it should be removed. + public void RemoveOperations(Func filter); +} \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/CanBeUsedResponse.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/CanBeUsedResponse.cs new file mode 100644 index 0000000..c9b556d --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Combat/Operations/CanBeUsedResponse.cs @@ -0,0 +1,28 @@ +// +// CanBeUsedResponse.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace NosSmooth.Extensions.Combat.Operations; + +/// +/// A response to CanBeUsed method. +/// +public enum CanBeUsedResponse +{ + /// + /// The operation may be used right awayt. + /// + CanBeUsed, + + /// + /// The operation will be usable after some amount of time. + /// + MustWait, + + /// + /// The operation won't be usable. (ie. missing arrows). + /// + WontBeUsable +} \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs new file mode 100644 index 0000000..d99c81a --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs @@ -0,0 +1,38 @@ +// +// ICombatOperation.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Extensions.Combat.Techniques; +using Remora.Results; + +namespace NosSmooth.Extensions.Combat.Operations; + +/// +/// A combat operation used in that can be used as one step. +/// +public interface ICombatOperation +{ + /// + /// Checks whether the operation can currently be used. + /// + /// + /// 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. + /// Whether the operation can be used right away. + public Result CanBeUsed(ICombatState combatState); + + /// + /// Use the operation, if possible. + /// + /// + /// Should block until the operation is finished. + /// + /// 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); +} \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs new file mode 100644 index 0000000..f307e9c --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs @@ -0,0 +1,26 @@ +// +// UseItemOperation.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Game.Data.Items; +using Remora.Results; + +namespace NosSmooth.Extensions.Combat.Operations; + +// TODO: first inventory has to be made +public record UseItemOperation(Item Item) : ICombatOperation +{ + /// + public Result CanBeUsed(ICombatState combatState) + { + throw new NotImplementedException(); + } + + /// + public Task UseAsync(ICombatState combatState, CancellationToken ct = default) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs new file mode 100644 index 0000000..b302a8e --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs @@ -0,0 +1,72 @@ +// +// UsePrimarySkillOperation.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Extensions.Combat.Errors; +using NosSmooth.Game.Data.Characters; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Packets.Client.Battle; +using Remora.Results; + +namespace NosSmooth.Extensions.Combat.Operations; + +/// +/// 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); + } + + _useSkillOperation = new UseSkillOperation(primarySkill, 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); + } + + _useSkillOperation = new UseSkillOperation(primarySkill, Target); + } + + return await _useSkillOperation.UseAsync(combatState, ct); + } + + private Result GetPrimarySkill(ICombatState combatState) + { + var character = combatState.Game.Character; + if (character is null) + { + return new CharacterNotInitializedError(); + } + + var skills = character.Skills; + if (skills is null) + { + return new CharacterNotInitializedError("Skills"); + } + + return skills.PrimarySkill; + } +} \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs new file mode 100644 index 0000000..cec4553 --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs @@ -0,0 +1,77 @@ +// +// UseSkillOperation.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Xml.XPath; +using NosSmooth.Extensions.Combat.Errors; +using NosSmooth.Game.Data.Characters; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Packets.Client.Battle; +using Remora.Results; + +namespace NosSmooth.Extensions.Combat.Operations; + +/// +/// A combat operation to use a skill. +/// +/// The skill to use. +/// The target entity to use the skill at. +public record UseSkillOperation(Skill Skill, ILivingEntity Target) : ICombatOperation +{ + /// + public Result CanBeUsed(ICombatState combatState) + { + if (Skill.Info is null) + { + return new MissingInfoError("skill", Skill.SkillVNum); + } + + var character = combatState.Game.Character; + if (character is not null && character.Mp is not null && character.Mp.Amount is not null) + { + if (character.Mp.Amount < Skill.Info.MpCost) + { // The character is in combat, mp won't restore. + return CanBeUsedResponse.WontBeUsable; + } + } + + return Skill.IsOnCooldown ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed; + } + + /// + public async Task UseAsync(ICombatState combatState, CancellationToken ct = default) + { + if (Skill.Info is null) + { + return new MissingInfoError("skill", Skill.SkillVNum); + } + + // TODO: support for area skills, support skills that use x, y coordinates (like dashes or teleports) + var sendResponse = await combatState.Client.SendPacketAsync + ( + new UseSkillPacket + ( + Skill.Info.CastId, + Target.Type, + Target.Id, + null, + null + ), + ct + ); + + if (!sendResponse.IsSuccess) + { + return sendResponse; + } + + var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(ct); + combatState.CombatManager.RegisterSkillCancellationToken(linkedSource); + await Task.Delay(Skill.Info.CastTime * 200 * 5, linkedSource.Token); + combatState.CombatManager.UnregisterSkillCancellationToken(linkedSource); + + return Result.FromSuccess(); + } +} \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs new file mode 100644 index 0000000..052448c --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs @@ -0,0 +1,84 @@ +// +// WalkInRangeOperation.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Extensions.Combat.Errors; +using NosSmooth.Extensions.Pathfinding; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Info; +using Remora.Results; + +namespace NosSmooth.Extensions.Combat.Operations; + +/// +/// A combat operation that walks into a given range of an entity. +/// +/// The walk manager. +/// The entity to walk to. +/// The maximal distance from the entity. +public record WalkInRangeOperation +( + WalkManager WalkManager, + IEntity Entity, + float Distance +) : ICombatOperation +{ + /// + public Result CanBeUsed(ICombatState combatState) + { + var character = combatState.Game.Character; + if (character is null) + { + return new CharacterNotInitializedError(); + } + + return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed; + } + + /// + public async Task UseAsync(ICombatState combatState, CancellationToken ct = default) + { + var character = combatState.Game.Character; + if (character is null) + { + return new CharacterNotInitializedError(); + } + + var distance = Distance; + while (distance >= 1) + { + var position = Entity.Position; + if (position is null) + { + return new GenericError("Entity's position is not initialized."); + } + + var currentPosition = character.Position; + if (currentPosition is null) + { + return new CharacterNotInitializedError("Position"); + } + + var closePosition = GetClosePosition(currentPosition.Value, position.Value, distance); + var walkResult = await WalkManager.GoToAsync(closePosition.X, closePosition.Y, ct); + if (!walkResult.IsSuccess && walkResult.Error is NotFoundError) + { + distance--; + continue; + } + + return walkResult; + } + + return Result.FromSuccess(); + } + + private Position GetClosePosition(Position start, Position target, double distance) + { + var diff = start - target; + var diffLength = Math.Sqrt(diff.DistanceSquared(Position.Zero)); + return target + ((distance / diffLength) * diff); + } +} \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs b/Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs new file mode 100644 index 0000000..7408bd7 --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs @@ -0,0 +1,38 @@ +// +// WalkOperation.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Extensions.Combat.Errors; +using NosSmooth.Extensions.Pathfinding; +using Remora.Results; + +namespace NosSmooth.Extensions.Combat.Operations; + +/// +/// A combat operation that walks to the target. +/// +/// The walk manager. +/// The x coordinate to walk to. +/// The y coordinate to walk to. +public record WalkOperation(WalkManager WalkManager, short X, short Y) : ICombatOperation +{ + /// + public Result CanBeUsed(ICombatState combatState) + { + var character = combatState.Game.Character; + if (character is null) + { + return new CharacterNotInitializedError(); + } + + return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed; + } + + /// + public async Task UseAsync(ICombatState combatState, CancellationToken ct = default) + { + return await WalkManager.GoToAsync(X, Y, ct); + } +} \ No newline at end of file -- 2.49.0