A Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs => Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs +90 -0
@@ 0,0 1,90 @@
+//
+// CombatStateExtensions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Combat.Operations;
+using NosSmooth.Extensions.Combat.Policies;
+using NosSmooth.Extensions.Pathfinding;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Items;
+using OneOf.Types;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Extensions;
+
+/// <summary>
+/// Extension methods for <see cref="ICombatState"/>.
+/// </summary>
+public static class CombatStateExtensions
+{
+ /// <summary>
+ /// Walk in the range of the given entity.
+ /// </summary>
+ /// <param name="state">The combat state.</param>
+ /// <param name="walkManager">The walk manager.</param>
+ /// <param name="entity">The entity.</param>
+ /// <param name="range">The range distance to walk to.</param>
+ public static void WalkInRange
+ (
+ this ICombatState state,
+ WalkManager walkManager,
+ IEntity entity,
+ float range
+ )
+ {
+ state.EnqueueOperation(new WalkInRangeOperation(walkManager, entity, range));
+ }
+
+ /// <summary>
+ /// Walk to the given position.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="walkManager">The walk manager.</param>
+ /// <param name="x">The target x coordinate.</param>
+ /// <param name="y">The target y coordinate.</param>
+ public static void WalkTo
+ (
+ this ICombatState combatState,
+ WalkManager walkManager,
+ short x,
+ short y
+ )
+ {
+ combatState.EnqueueOperation(new WalkOperation(walkManager, x, y));
+ }
+
+ /// <summary>
+ /// Use the given skill.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="skill">The skill.</param>
+ /// <param name="target">The target to use skill at.</param>
+ public static void UseSkill(this ICombatState combatState, Skill skill, ILivingEntity target)
+ {
+ combatState.EnqueueOperation(new UseSkillOperation(skill, target));
+ }
+
+ /// <summary>
+ /// Use primary skill.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="target">The target to use skill at.</param>
+ public static void UsePrimarySkill(this ICombatState combatState, ILivingEntity target)
+ {
+ combatState.EnqueueOperation(new UsePrimarySkillOperation(target));
+ }
+
+ /// <summary>
+ /// Use the given item.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="item">The item to use.</param>
+ public static void UseItem(this ICombatState combatState, Item item)
+ {
+ combatState.EnqueueOperation(new UseItemOperation(item));
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/ICombatState.cs => Extensions/NosSmooth.Extensions.Combat/ICombatState.cs +58 -0
@@ 0,0 1,58 @@
+//
+// ICombatState.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Client;
+using NosSmooth.Extensions.Combat.Operations;
+using NosSmooth.Game.Data.Entities;
+
+namespace NosSmooth.Extensions.Combat;
+
+/// <summary>
+/// The combat technique state used for queuing operations and storing information.
+/// </summary>
+public interface ICombatState
+{
+ /// <summary>
+ /// Gets the combat manager.
+ /// </summary>
+ public CombatManager CombatManager { get; }
+
+ /// <summary>
+ /// Gets the game.
+ /// </summary>
+ public Game.Game Game { get; }
+
+ /// <summary>
+ /// Gets the NosTale client.
+ /// </summary>
+ public INostaleClient Client { get; }
+
+ /// <summary>
+ /// Cancel the combat technique, quit the combat state.
+ /// </summary>
+ public void QuitCombat();
+
+ /// <summary>
+ /// Replace the current operation with this one.
+ /// </summary>
+ /// <param name="operation">The operation to use.</param>
+ /// <param name="emptyQueue">Whether to empty the queue of the operations.</param>
+ /// <param name="prependCurrentOperationToQueue">Whether to still use the current operation (true) after this one or discard it (false).</param>
+ public void SetCurrentOperation
+ (ICombatOperation operation, bool emptyQueue = false, bool prependCurrentOperationToQueue = false);
+
+ /// <summary>
+ /// Enqueue the operation at the end of the queue.
+ /// </summary>
+ /// <param name="operation">The operation to enqueue.</param>
+ public void EnqueueOperation(ICombatOperation operation);
+
+ /// <summary>
+ /// Remove the operations by the given filter.
+ /// </summary>
+ /// <param name="filter">Called for each operation, should return true if it should be removed.</param>
+ public void RemoveOperations(Func<ICombatOperation, bool> filter);
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Operations/CanBeUsedResponse.cs => Extensions/NosSmooth.Extensions.Combat/Operations/CanBeUsedResponse.cs +28 -0
@@ 0,0 1,28 @@
+//
+// CanBeUsedResponse.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+/// <summary>
+/// A response to <see cref="ICombatOperation"/> CanBeUsed method.
+/// </summary>
+public enum CanBeUsedResponse
+{
+ /// <summary>
+ /// The operation may be used right awayt.
+ /// </summary>
+ CanBeUsed,
+
+ /// <summary>
+ /// The operation will be usable after some amount of time.
+ /// </summary>
+ MustWait,
+
+ /// <summary>
+ /// The operation won't be usable. (ie. missing arrows).
+ /// </summary>
+ WontBeUsable
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs +38 -0
@@ 0,0 1,38 @@
+//
+// ICombatOperation.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Techniques;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+/// <summary>
+/// A combat operation used in <see cref="ICombatTechnique"/> that can be used as one step.
+/// </summary>
+public interface ICombatOperation
+{
+ /// <summary>
+ /// Checks whether the operation can currently be used.
+ /// </summary>
+ /// <remarks>
+ /// Ie. if the operation is to use a skill, it will return true only if the skill is not on a cooldown,
+ /// the character has enough mana and is not stunned.
+ /// </remarks>
+ /// <param name="combatState">The combat state.</param>
+ /// <returns>Whether the operation can be used right away.</returns>
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState);
+
+ /// <summary>
+ /// Use the operation, if possible.
+ /// </summary>
+ /// <remarks>
+ /// Should block until the operation is finished.
+ /// </remarks>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not succeed.</returns>
+ public Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default);
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs +26 -0
@@ 0,0 1,26 @@
+//
+// UseItemOperation.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Game.Data.Items;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+// TODO: first inventory has to be made
+public record UseItemOperation(Item Item) : ICombatOperation
+{
+ /// <inheritdoc />
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs +72 -0
@@ 0,0 1,72 @@
+//
+// UsePrimarySkillOperation.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Packets.Client.Battle;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+/// <summary>
+/// An operation that uses the primary skill of the character.
+/// </summary>
+public record UsePrimarySkillOperation(ILivingEntity Target) : ICombatOperation
+{
+ private UseSkillOperation? _useSkillOperation;
+
+ /// <inheritdoc />
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ {
+ if (_useSkillOperation is null)
+ {
+ var primarySkillResult = GetPrimarySkill(combatState);
+ if (!primarySkillResult.IsDefined(out var primarySkill))
+ {
+ return Result<CanBeUsedResponse>.FromError(primarySkillResult);
+ }
+
+ _useSkillOperation = new UseSkillOperation(primarySkill, Target);
+ }
+
+ return _useSkillOperation.CanBeUsed(combatState);
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct)
+ {
+ if (_useSkillOperation is null)
+ {
+ var primarySkillResult = GetPrimarySkill(combatState);
+ if (!primarySkillResult.IsDefined(out var primarySkill))
+ {
+ return Result.FromError(primarySkillResult);
+ }
+
+ _useSkillOperation = new UseSkillOperation(primarySkill, Target);
+ }
+
+ return await _useSkillOperation.UseAsync(combatState, ct);
+ }
+
+ private Result<Skill> GetPrimarySkill(ICombatState combatState)
+ {
+ var character = combatState.Game.Character;
+ if (character is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ var skills = character.Skills;
+ if (skills is null)
+ {
+ return new CharacterNotInitializedError("Skills");
+ }
+
+ return skills.PrimarySkill;
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs +77 -0
@@ 0,0 1,77 @@
+//
+// UseSkillOperation.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Xml.XPath;
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Packets.Client.Battle;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+/// <summary>
+/// A combat operation to use a skill.
+/// </summary>
+/// <param name="Skill">The skill to use.</param>
+/// <param name="Target">The target entity to use the skill at.</param>
+public record UseSkillOperation(Skill Skill, ILivingEntity Target) : ICombatOperation
+{
+ /// <inheritdoc />
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ {
+ if (Skill.Info is null)
+ {
+ return new MissingInfoError("skill", Skill.SkillVNum);
+ }
+
+ var character = combatState.Game.Character;
+ if (character is not null && character.Mp is not null && character.Mp.Amount is not null)
+ {
+ if (character.Mp.Amount < Skill.Info.MpCost)
+ { // The character is in combat, mp won't restore.
+ return CanBeUsedResponse.WontBeUsable;
+ }
+ }
+
+ return Skill.IsOnCooldown ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
+ {
+ if (Skill.Info is null)
+ {
+ return new MissingInfoError("skill", Skill.SkillVNum);
+ }
+
+ // TODO: support for area skills, support skills that use x, y coordinates (like dashes or teleports)
+ var sendResponse = await combatState.Client.SendPacketAsync
+ (
+ new UseSkillPacket
+ (
+ Skill.Info.CastId,
+ Target.Type,
+ Target.Id,
+ null,
+ null
+ ),
+ ct
+ );
+
+ if (!sendResponse.IsSuccess)
+ {
+ return sendResponse;
+ }
+
+ var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
+ combatState.CombatManager.RegisterSkillCancellationToken(linkedSource);
+ await Task.Delay(Skill.Info.CastTime * 200 * 5, linkedSource.Token);
+ combatState.CombatManager.UnregisterSkillCancellationToken(linkedSource);
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs +84 -0
@@ 0,0 1,84 @@
+//
+// WalkInRangeOperation.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Pathfinding;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+/// <summary>
+/// A combat operation that walks into a given range of an entity.
+/// </summary>
+/// <param name="WalkManager">The walk manager.</param>
+/// <param name="Entity">The entity to walk to.</param>
+/// <param name="Distance">The maximal distance from the entity.</param>
+public record WalkInRangeOperation
+(
+ WalkManager WalkManager,
+ IEntity Entity,
+ float Distance
+) : ICombatOperation
+{
+ /// <inheritdoc />
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ {
+ var character = combatState.Game.Character;
+ if (character is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
+ {
+ var character = combatState.Game.Character;
+ if (character is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ var distance = Distance;
+ while (distance >= 1)
+ {
+ var position = Entity.Position;
+ if (position is null)
+ {
+ return new GenericError("Entity's position is not initialized.");
+ }
+
+ var currentPosition = character.Position;
+ if (currentPosition is null)
+ {
+ return new CharacterNotInitializedError("Position");
+ }
+
+ var closePosition = GetClosePosition(currentPosition.Value, position.Value, distance);
+ var walkResult = await WalkManager.GoToAsync(closePosition.X, closePosition.Y, ct);
+ if (!walkResult.IsSuccess && walkResult.Error is NotFoundError)
+ {
+ distance--;
+ continue;
+ }
+
+ return walkResult;
+ }
+
+ return Result.FromSuccess();
+ }
+
+ private Position GetClosePosition(Position start, Position target, double distance)
+ {
+ var diff = start - target;
+ var diffLength = Math.Sqrt(diff.DistanceSquared(Position.Zero));
+ return target + ((distance / diffLength) * diff);
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs +38 -0
@@ 0,0 1,38 @@
+//
+// WalkOperation.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Pathfinding;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+/// <summary>
+/// A combat operation that walks to the target.
+/// </summary>
+/// <param name="WalkManager">The walk manager.</param>
+/// <param name="X">The x coordinate to walk to.</param>
+/// <param name="Y">The y coordinate to walk to.</param>
+public record WalkOperation(WalkManager WalkManager, short X, short Y) : ICombatOperation
+{
+ /// <inheritdoc />
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ {
+ var character = combatState.Game.Character;
+ if (character is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
+ {
+ return await WalkManager.GoToAsync(X, Y, ct);
+ }
+}<
\ No newline at end of file