feat(combat): Phase E.4 AttackTargetRequest + combat notification pipeline
Completes the client-side combat loop: send attacks, receive server's damage broadcasts, maintain per-entity health state for HP bars + damage floaters. All atop Phase F.1's GameEvent dispatcher. Wire layer: - AttackTargetRequest (0x0008 C→S, inside 0xF7B1): targetGuid + powerLevel + accuracyLevel + attackHeight. 28-byte body. - GameEvents parsers for all combat notifications from r08 §4: - VictimNotification (0x01AC) — you got hit, full details - KillerNotification (0x01AD) — you killed X - AttackerNotification (0x01B1) — you hit X for Y (damage%) - DefenderNotification (0x01B2) — X hit you - EvasionAttackerNotification (0x01B3) — X evaded - EvasionDefenderNotification (0x01B4) — you evaded X - AttackDone (0x01A7) — attack sequence completed Core layer: - CombatState: per-entity health-percent cache + typed events (HealthChanged, DamageTaken, DamageDealtAccepted, EvadedIncoming, MissedOutgoing, AttackDone). Each event carries enough detail for the UI to render damage floaters, HP bars, and a combat log panel. Server is authoritative; client only mirrors state. The server computes damage (armor, resist, crit, hit-chance); the client only displays results. Predictive UI like "estimated damage at 0.75 power" still works via the existing CombatMath helper class that was in the scaffold (r02 §5 formulas). Tests (13 new): - AttackTargetRequest byte-exact wire encoding - VictimNotification / AttackerNotification / EvasionAttacker / AttackDone round-trip parse. - CombatState: UpdateHealth caches + fires, Victim fires DamageTaken, Attacker fires DamageDealt, Evasion routes to right event, AttackDone carries sequence+error, Clear resets cache. Build green, 544 tests pass (up from 532). Ref: r02 §7 (wire formats), r08 §4 (event payloads), ACE GameEvent*Notification.cs families. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2561f5599f
commit
2e3f9d7a04
5 changed files with 516 additions and 0 deletions
|
|
@ -145,6 +145,125 @@ public static class GameEvents
|
|||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||||
}
|
||||
|
||||
// ── Combat notifications ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>0x01AC VictimNotification — "you got hit for X".</summary>
|
||||
public readonly record struct VictimNotification(
|
||||
string AttackerName,
|
||||
uint AttackerGuid,
|
||||
uint DamageType,
|
||||
uint Damage,
|
||||
uint HitQuadrant,
|
||||
uint Critical,
|
||||
uint AttackType);
|
||||
|
||||
public static VictimNotification? ParseVictimNotification(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
int pos = 0;
|
||||
try
|
||||
{
|
||||
string name = ReadString16L(payload, ref pos);
|
||||
if (payload.Length - pos < 24) return null;
|
||||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
uint damageType = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
uint damage = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
uint quad = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
uint crit = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
uint atkType = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
return new VictimNotification(name, guid, damageType, damage, quad, crit, atkType);
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>0x01AD KillerNotification — "you killed X".</summary>
|
||||
public readonly record struct KillerNotification(string VictimName, uint VictimGuid);
|
||||
|
||||
public static KillerNotification? ParseKillerNotification(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
int pos = 0;
|
||||
try
|
||||
{
|
||||
string name = ReadString16L(payload, ref pos);
|
||||
if (payload.Length - pos < 4) return null;
|
||||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos));
|
||||
return new KillerNotification(name, guid);
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>0x01B1 AttackerNotification — "you hit X for Y%".</summary>
|
||||
public readonly record struct AttackerNotification(
|
||||
string DefenderName,
|
||||
uint DamageType,
|
||||
uint Damage,
|
||||
float DamagePercent);
|
||||
|
||||
public static AttackerNotification? ParseAttackerNotification(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
int pos = 0;
|
||||
try
|
||||
{
|
||||
string name = ReadString16L(payload, ref pos);
|
||||
if (payload.Length - pos < 12) return null;
|
||||
uint damageType = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
uint damage = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
float pct = BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(pos)); pos += 4;
|
||||
return new AttackerNotification(name, damageType, damage, pct);
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>0x01B2 DefenderNotification — "X hit you for Y".</summary>
|
||||
public readonly record struct DefenderNotification(
|
||||
string AttackerName,
|
||||
uint AttackerGuid,
|
||||
uint DamageType,
|
||||
uint Damage,
|
||||
uint HitQuadrant,
|
||||
uint Critical);
|
||||
|
||||
public static DefenderNotification? ParseDefenderNotification(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
int pos = 0;
|
||||
try
|
||||
{
|
||||
string name = ReadString16L(payload, ref pos);
|
||||
if (payload.Length - pos < 20) return null;
|
||||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
uint dtype = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
uint dmg = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
uint quad = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
uint crit = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||||
return new DefenderNotification(name, guid, dtype, dmg, quad, crit);
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>0x01B3 EvasionAttackerNotification — "X evaded".</summary>
|
||||
public static string? ParseEvasionAttackerNotification(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
int pos = 0;
|
||||
try { return ReadString16L(payload, ref pos); } catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>0x01B4 EvasionDefenderNotification — "you evaded X".</summary>
|
||||
public static string? ParseEvasionDefenderNotification(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
int pos = 0;
|
||||
try { return ReadString16L(payload, ref pos); } catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>0x01A7 AttackDone — (attackSequence, weenieError).</summary>
|
||||
public readonly record struct AttackDone(uint AttackSequence, uint WeenieError);
|
||||
|
||||
public static AttackDone? ParseAttackDone(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 8) return null;
|
||||
return new AttackDone(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)));
|
||||
}
|
||||
|
||||
// ── Appraise / identify ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>0x00C9 IdentifyObjectResponse header.</summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue