Completes the client-side spell loop on top of Phase F.1. Player can
send cast requests; spellbook + active-enchantment state mirrors what
the server broadcasts.
Wire layer:
- CastSpellRequest (C→S, inside 0xF7B1 GameAction):
- BuildUntargeted (0x0048): self-buffs, recalls, heal-self — 16 bytes.
- BuildTargeted (0x004A): projectile attacks, target buffs/debuffs — 20 bytes.
- GameEvents parsers added:
- 0x02C1 MagicUpdateSpell: spell-id → spellbook.
- 0x01A8 MagicRemoveSpell.
- 0x02C2 MagicUpdateEnchantment: spellId + layerId + duration + casterGuid
(summary head; full stat-mod body deferred).
- 0x02C3 MagicRemoveEnchantment: (layerId, spellId).
- 0x02C7 MagicDispelEnchantment: same shape.
Core layer:
- Spellbook: learned-spell set + active-enchantment-by-layer dict
with events (SpellLearned, SpellForgotten, EnchantmentAdded,
EnchantmentRemoved). Duplicate learn is idempotent. Same-layer
add refreshes duration. Purge fires per-record remove for UI
cleanup.
- ActiveEnchantmentRecord: (SpellId, LayerId, Duration, CasterGuid).
Tests (10 new):
- CastSpellRequest untargeted (16 bytes) + targeted (20 bytes) wire encoding.
- GameEvents: MagicUpdateSpell, MagicUpdateEnchantment,
MagicRemoveEnchantment round-trip.
- Spellbook: learn idempotent, forget, add/refresh enchantment,
remove fires event, purge-all clears + fires per-record.
Build green, 555 tests pass (up from 544).
Ref: r01 §2 (wire casts), §3 (cast state machine), §5 (stacking rules).
Ref: r08 §3 opcodes 0x0048/0x004A, §4 opcodes 0x01A8/0x02C1-0x02C8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
64 lines
2.1 KiB
C#
64 lines
2.1 KiB
C#
using System.Buffers.Binary;
|
|
|
|
namespace AcDream.Core.Net.Messages;
|
|
|
|
/// <summary>
|
|
/// Outbound <c>Magic_CastSpell</c> GameActions — targeted and untargeted
|
|
/// variants. From r01 §2 + r08 §3 opcodes 0x0048 / 0x004A.
|
|
///
|
|
/// <para>
|
|
/// Wire layouts (inside the <c>0xF7B1</c> GameAction envelope):
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Untargeted (<c>0x0048</c>):
|
|
/// <code>
|
|
/// u32 0xF7B1
|
|
/// u32 gameActionSequence
|
|
/// u32 0x0048
|
|
/// u32 spellId
|
|
/// </code>
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Targeted (<c>0x004A</c>):
|
|
/// <code>
|
|
/// u32 0xF7B1
|
|
/// u32 gameActionSequence
|
|
/// u32 0x004A
|
|
/// u32 targetGuid
|
|
/// u32 spellId
|
|
/// </code>
|
|
/// </para>
|
|
/// </summary>
|
|
public static class CastSpellRequest
|
|
{
|
|
public const uint GameActionEnvelope = 0xF7B1u;
|
|
public const uint UntargetedSubOpcode = 0x0048u;
|
|
public const uint TargetedSubOpcode = 0x004Au;
|
|
|
|
/// <summary>Build an untargeted cast (buff self, recall, self-heal, etc).</summary>
|
|
public static byte[] BuildUntargeted(uint gameActionSequence, uint spellId)
|
|
{
|
|
byte[] body = new byte[16];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), gameActionSequence);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), UntargetedSubOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), spellId);
|
|
return body;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build a targeted cast (projectile attacks, target buffs, debuffs).
|
|
/// </summary>
|
|
public static byte[] BuildTargeted(uint gameActionSequence, uint targetGuid, uint spellId)
|
|
{
|
|
byte[] body = new byte[20];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), gameActionSequence);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), TargetedSubOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), targetGuid);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), spellId);
|
|
return body;
|
|
}
|
|
}
|