M Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs => Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs +6 -1
@@ 65,7 65,12 @@ public static class CombatStateExtensions
/// <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));
+ if (combatState.Game.Character is null)
+ {
+ throw new InvalidOperationException("The character is not initialized.");
+ }
+
+ combatState.EnqueueOperation(new UseSkillOperation(skill, combatState.Game.Character, target));
}
/// <summary>
M Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs +12 -2
@@ 30,7 30,12 @@ public record UsePrimarySkillOperation(ILivingEntity Target) : ICombatOperation
return Result<CanBeUsedResponse>.FromError(primarySkillResult);
}
- _useSkillOperation = new UseSkillOperation(primarySkill, Target);
+ if (combatState.Game.Character is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ _useSkillOperation = new UseSkillOperation(primarySkill, combatState.Game.Character, Target);
}
return _useSkillOperation.CanBeUsed(combatState);
@@ 47,7 52,12 @@ public record UsePrimarySkillOperation(ILivingEntity Target) : ICombatOperation
return Result.FromError(primarySkillResult);
}
- _useSkillOperation = new UseSkillOperation(primarySkill, Target);
+ if (combatState.Game.Character is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ _useSkillOperation = new UseSkillOperation(primarySkill, combatState.Game.Character, Target);
}
return await _useSkillOperation.UseAsync(combatState, ct);
M Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs +51 -10
@@ 4,10 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 System.Xml.XPath;
+using NosSmooth.Data.Abstractions.Enums;
+using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Extensions.Combat.Errors;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Entities;
+using NosSmooth.Packets;
using NosSmooth.Packets.Client.Battle;
using Remora.Results;
@@ 17,8 21,9 @@ namespace NosSmooth.Extensions.Combat.Operations;
/// A combat operation to use a skill.
/// </summary>
/// <param name="Skill">The skill to use.</param>
+/// <param name="Caster">The caster entity that is using the skill.</param>
/// <param name="Target">The target entity to use the skill at.</param>
-public record UseSkillOperation(Skill Skill, ILivingEntity Target) : ICombatOperation
+public record UseSkillOperation(Skill Skill, ILivingEntity Caster, ILivingEntity Target) : ICombatOperation
{
/// <inheritdoc />
public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
@@ 48,19 53,11 @@ public record UseSkillOperation(Skill Skill, ILivingEntity Target) : ICombatOper
return new MissingInfoError("skill", Skill.SkillVNum);
}
- // TODO: support for area skills, support skills that use x, y coordinates (like dashes or teleports)
using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
await combatState.CombatManager.RegisterSkillCancellationTokenAsync(linkedSource, ct);
var sendResponse = await combatState.Client.SendPacketAsync
(
- new UseSkillPacket
- (
- Skill.Info.CastId,
- Target.Type,
- Target.Id,
- null,
- null
- ),
+ CreateSkillUsePacket(Skill.Info),
ct
);
@@ 84,4 81,48 @@ public record UseSkillOperation(Skill Skill, ILivingEntity Target) : ICombatOper
return Result.FromSuccess();
}
+
+ private IPacket CreateSkillUsePacket(ISkillInfo info)
+ {
+ switch (info.TargetType)
+ {
+ case TargetType.SelfOrTarget: // a buff?
+ case TargetType.Self:
+ return CreateSelfTargetedSkillPacket(info);
+ case TargetType.NoTarget: // area skill?
+ return CreateAreaSkillPacket(info);
+ case TargetType.Target:
+ return CreateTargetedSkillPacket(info);
+ }
+
+ 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
+ );
}=
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/Responders/SkillUseResponder.cs => Extensions/NosSmooth.Extensions.Combat/Responders/SkillUseResponder.cs +13 -4
@@ 1,5 1,5 @@
//
-// SuResponder.cs
+// 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.
@@ 17,27 17,36 @@ namespace NosSmooth.Extensions.Combat.Responders;
public class SuResponder : IPacketResponder<SuPacket>, IPacketResponder<BsPacket>
{
private readonly CombatManager _combatManager;
+ private readonly Game.Game _game;
/// <summary>
/// Initializes a new instance of the <see cref="SuResponder"/> class.
/// </summary>
/// <param name="combatManager">The combat manager.</param>
- public SuResponder(CombatManager combatManager)
+ /// <param name="game">The game.</param>
+ public SuResponder(CombatManager combatManager, Game.Game game)
{
_combatManager = combatManager;
+ _game = game;
}
/// <inheritdoc />
public Task<Result> Respond(PacketEventArgs<SuPacket> packetArgs, CancellationToken ct = default)
{
- _combatManager.CancelSkillTokensAsync(ct);
+ if (packetArgs.Packet.CasterEntityId == _game.Character?.Id)
+ {
+ _combatManager.CancelSkillTokensAsync(ct);
+ }
return Task.FromResult(Result.FromSuccess());
}
/// <inheritdoc />
public Task<Result> Respond(PacketEventArgs<BsPacket> packetArgs, CancellationToken ct = default)
{
- _combatManager.CancelSkillTokensAsync(ct);
+ if (packetArgs.Packet.CasterEntityId == _game.Character?.Id)
+ {
+ _combatManager.CancelSkillTokensAsync(ct);
+ }
return Task.FromResult(Result.FromSuccess());
}
}=
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs => Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs +9 -1
@@ 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.Data.Abstractions.Enums;
using NosSmooth.Extensions.Combat.Errors;
using NosSmooth.Extensions.Combat.Extensions;
using NosSmooth.Extensions.Combat.Operations;
@@ 95,6 96,7 @@ public class SimpleAttackTechnique : ICombatTechnique
var characterMp = character.Mp?.Amount ?? 0;
var usableSkills = new[] { skills.PrimarySkill, skills.SecondarySkill }
.Concat(skills.OtherSkills)
+ .Where(x => x.Info is not null && x.Info.HitType != HitType.AlliesInZone)
.Where(x => !x.IsOnCooldown && characterMp >= (x.Info?.MpCost ?? long.MaxValue));
var skillResult = _skillSelector.GetSelectedSkill(usableSkills);
@@ 128,7 130,13 @@ public class SimpleAttackTechnique : ICombatTechnique
return new EntityNotFoundError();
}
- if (!character.Position.Value.IsInRange(_target.Position.Value, _currentSkill.Info.Range))
+ var range = _currentSkill.Info.Range;
+ if (_currentSkill.Info.TargetType == TargetType.Self && _currentSkill.Info.HitType == HitType.EnemiesInZone)
+ {
+ range = _currentSkill.Info.ZoneRange;
+ }
+
+ if (!character.Position.Value.IsInRange(_target.Position.Value, range))
{
state.WalkInRange(_walkManager, _target, _currentSkill.Info.Range);
}