feat(combat): Phase L.1c add outbound combat actions
This commit is contained in:
parent
29afc94b94
commit
25b9616703
5 changed files with 176 additions and 5 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
|
using AcDream.Core.Combat;
|
||||||
using AcDream.Core.Net.Cryptography;
|
using AcDream.Core.Net.Cryptography;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Net.Packets;
|
using AcDream.Core.Net.Packets;
|
||||||
|
|
@ -909,6 +910,48 @@ public sealed class WorldSession : IDisposable
|
||||||
SendGameAction(body);
|
SendGameAction(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Send retail ChangeCombatMode (0x0053).</summary>
|
||||||
|
public void SendChangeCombatMode(CombatMode mode)
|
||||||
|
{
|
||||||
|
uint seq = NextGameActionSequence();
|
||||||
|
byte[] body = CharacterActions.BuildChangeCombatMode(
|
||||||
|
seq,
|
||||||
|
(CharacterActions.CombatMode)(uint)mode);
|
||||||
|
SendGameAction(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Send retail TargetedMeleeAttack (0x0008).</summary>
|
||||||
|
public void SendMeleeAttack(uint targetGuid, AttackHeight attackHeight, float powerLevel)
|
||||||
|
{
|
||||||
|
uint seq = NextGameActionSequence();
|
||||||
|
byte[] body = AttackTargetRequest.BuildMelee(
|
||||||
|
seq,
|
||||||
|
targetGuid,
|
||||||
|
(uint)attackHeight,
|
||||||
|
powerLevel);
|
||||||
|
SendGameAction(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Send retail TargetedMissileAttack (0x000A).</summary>
|
||||||
|
public void SendMissileAttack(uint targetGuid, AttackHeight attackHeight, float accuracyLevel)
|
||||||
|
{
|
||||||
|
uint seq = NextGameActionSequence();
|
||||||
|
byte[] body = AttackTargetRequest.BuildMissile(
|
||||||
|
seq,
|
||||||
|
targetGuid,
|
||||||
|
(uint)attackHeight,
|
||||||
|
accuracyLevel);
|
||||||
|
SendGameAction(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Send retail CancelAttack (0x01B7).</summary>
|
||||||
|
public void SendCancelAttack()
|
||||||
|
{
|
||||||
|
uint seq = NextGameActionSequence();
|
||||||
|
byte[] body = AttackTargetRequest.BuildCancel(seq);
|
||||||
|
SendGameAction(body);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase I.6: send a TurbineChat <c>RequestSendToRoomById</c> to a
|
/// Phase I.6: send a TurbineChat <c>RequestSendToRoomById</c> to a
|
||||||
/// global community room (General / Trade / LFG / Roleplay /
|
/// global community room (General / Trade / LFG / Roleplay /
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,17 @@ namespace AcDream.Core.Combat;
|
||||||
// Full research: docs/research/deepdives/r02-combat-system.md
|
// Full research: docs/research/deepdives/r02-combat-system.md
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Flags]
|
||||||
public enum CombatMode
|
public enum CombatMode
|
||||||
{
|
{
|
||||||
Undef = 0,
|
Undef = 0,
|
||||||
NonCombat = 1,
|
NonCombat = 0x01,
|
||||||
Melee = 2,
|
Melee = 0x02,
|
||||||
Missile = 3,
|
Missile = 0x04,
|
||||||
Magic = 4,
|
Magic = 0x08,
|
||||||
Peaceful = 5,
|
|
||||||
|
ValidCombat = NonCombat | Melee | Missile | Magic,
|
||||||
|
CombatCombat = Melee | Missile | Magic,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum AttackHeight
|
public enum AttackHeight
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ public sealed class CombatState
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<uint, float> _healthByGuid = new();
|
private readonly ConcurrentDictionary<uint, float> _healthByGuid = new();
|
||||||
|
|
||||||
|
public CombatMode CurrentMode { get; private set; } = CombatMode.NonCombat;
|
||||||
|
|
||||||
/// <summary>Fires when a target's health percent changes (from UpdateHealth).</summary>
|
/// <summary>Fires when a target's health percent changes (from UpdateHealth).</summary>
|
||||||
public event Action<uint /*guid*/, float /*percent*/>? HealthChanged;
|
public event Action<uint /*guid*/, float /*percent*/>? HealthChanged;
|
||||||
|
|
||||||
|
|
@ -60,6 +62,9 @@ public sealed class CombatState
|
||||||
/// <summary>The server accepted the attack and the power bar/animation can begin.</summary>
|
/// <summary>The server accepted the attack and the power bar/animation can begin.</summary>
|
||||||
public event Action? AttackCommenced;
|
public event Action? AttackCommenced;
|
||||||
|
|
||||||
|
/// <summary>The locally requested or server-confirmed combat mode changed.</summary>
|
||||||
|
public event Action<CombatMode>? CombatModeChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fires when the server confirms the player landed a killing blow
|
/// Fires when the server confirms the player landed a killing blow
|
||||||
/// (GameEvent <c>KillerNotification (0x01AD)</c>). Event payload is
|
/// (GameEvent <c>KillerNotification (0x01AD)</c>). Event payload is
|
||||||
|
|
@ -97,6 +102,15 @@ public sealed class CombatState
|
||||||
HealthChanged?.Invoke(targetGuid, healthPercent);
|
HealthChanged?.Invoke(targetGuid, healthPercent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetCombatMode(CombatMode mode)
|
||||||
|
{
|
||||||
|
if (CurrentMode == mode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CurrentMode = mode;
|
||||||
|
CombatModeChanged?.Invoke(mode);
|
||||||
|
}
|
||||||
|
|
||||||
public void OnVictimNotification(
|
public void OnVictimNotification(
|
||||||
string attackerName, uint attackerGuid, uint damageType, uint damage,
|
string attackerName, uint attackerGuid, uint damageType, uint damage,
|
||||||
uint hitQuadrant, uint critical, uint attackType)
|
uint hitQuadrant, uint critical, uint attackType)
|
||||||
|
|
|
||||||
77
tests/AcDream.Core.Net.Tests/WorldSessionCombatTests.cs
Normal file
77
tests/AcDream.Core.Net.Tests/WorldSessionCombatTests.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
using System.Net;
|
||||||
|
using AcDream.Core.Combat;
|
||||||
|
using AcDream.Core.Net;
|
||||||
|
using AcDream.Core.Net.Messages;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Net.Tests;
|
||||||
|
|
||||||
|
public sealed class WorldSessionCombatTests
|
||||||
|
{
|
||||||
|
private static WorldSession NewSession()
|
||||||
|
{
|
||||||
|
var ep = new IPEndPoint(IPAddress.Loopback, 65000);
|
||||||
|
return new WorldSession(ep);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SendChangeCombatMode_UsesSequenceAndRetailModeValue()
|
||||||
|
{
|
||||||
|
using var session = NewSession();
|
||||||
|
byte[]? captured = null;
|
||||||
|
session.GameActionCapture = body => captured = body;
|
||||||
|
|
||||||
|
session.SendChangeCombatMode(CombatMode.Magic);
|
||||||
|
|
||||||
|
Assert.NotNull(captured);
|
||||||
|
Assert.Equal(CharacterActions.BuildChangeCombatMode(
|
||||||
|
1,
|
||||||
|
CharacterActions.CombatMode.Magic), captured);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SendMeleeAttack_UsesRetailMeleeBuilder()
|
||||||
|
{
|
||||||
|
using var session = NewSession();
|
||||||
|
byte[]? captured = null;
|
||||||
|
session.GameActionCapture = body => captured = body;
|
||||||
|
|
||||||
|
session.SendMeleeAttack(0x50000002u, AttackHeight.High, 0.75f);
|
||||||
|
|
||||||
|
Assert.NotNull(captured);
|
||||||
|
Assert.Equal(AttackTargetRequest.BuildMelee(
|
||||||
|
1,
|
||||||
|
0x50000002u,
|
||||||
|
(uint)AttackHeight.High,
|
||||||
|
0.75f), captured);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SendMissileAttack_UsesRetailMissileBuilder()
|
||||||
|
{
|
||||||
|
using var session = NewSession();
|
||||||
|
byte[]? captured = null;
|
||||||
|
session.GameActionCapture = body => captured = body;
|
||||||
|
|
||||||
|
session.SendMissileAttack(0x50000003u, AttackHeight.Low, 0.5f);
|
||||||
|
|
||||||
|
Assert.NotNull(captured);
|
||||||
|
Assert.Equal(AttackTargetRequest.BuildMissile(
|
||||||
|
1,
|
||||||
|
0x50000003u,
|
||||||
|
(uint)AttackHeight.Low,
|
||||||
|
0.5f), captured);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SendCancelAttack_UsesRetailCancelBuilder()
|
||||||
|
{
|
||||||
|
using var session = NewSession();
|
||||||
|
byte[]? captured = null;
|
||||||
|
session.GameActionCapture = body => captured = body;
|
||||||
|
|
||||||
|
session.SendCancelAttack();
|
||||||
|
|
||||||
|
Assert.NotNull(captured);
|
||||||
|
Assert.Equal(AttackTargetRequest.BuildCancel(1), captured);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,40 @@ public sealed class CombatStateTests
|
||||||
Assert.Equal(1f, state.GetHealthPercent(0xDEAD));
|
Assert.Equal(1f, state.GetHealthPercent(0xDEAD));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CombatMode_UsesRetailAceBitValues()
|
||||||
|
{
|
||||||
|
Assert.Equal(1, (int)CombatMode.NonCombat);
|
||||||
|
Assert.Equal(2, (int)CombatMode.Melee);
|
||||||
|
Assert.Equal(4, (int)CombatMode.Missile);
|
||||||
|
Assert.Equal(8, (int)CombatMode.Magic);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetCombatMode_TracksCurrentMode_AndFiresEvent()
|
||||||
|
{
|
||||||
|
var state = new CombatState();
|
||||||
|
CombatMode? seen = null;
|
||||||
|
state.CombatModeChanged += mode => seen = mode;
|
||||||
|
|
||||||
|
state.SetCombatMode(CombatMode.Missile);
|
||||||
|
|
||||||
|
Assert.Equal(CombatMode.Missile, state.CurrentMode);
|
||||||
|
Assert.Equal(CombatMode.Missile, seen);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnCombatCommenceAttack_FiresAttackCommenced()
|
||||||
|
{
|
||||||
|
var state = new CombatState();
|
||||||
|
bool seen = false;
|
||||||
|
state.AttackCommenced += () => seen = true;
|
||||||
|
|
||||||
|
state.OnCombatCommenceAttack();
|
||||||
|
|
||||||
|
Assert.True(seen);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OnVictimNotification_FiresDamageTaken()
|
public void OnVictimNotification_FiresDamageTaken()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue