feat(spells): Phase E.5 CastSpellRequest + Spellbook/enchantment state
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>
This commit is contained in:
parent
2e3f9d7a04
commit
c95aedcd4a
5 changed files with 396 additions and 0 deletions
|
|
@ -264,6 +264,63 @@ public static class GameEvents
|
|||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)));
|
||||
}
|
||||
|
||||
// ── Spell enchantments ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 0x02C3 MagicRemoveEnchantment — (layerId, spellId).
|
||||
/// </summary>
|
||||
public readonly record struct MagicRemoveEnchantment(uint LayerId, uint SpellId);
|
||||
|
||||
public static MagicRemoveEnchantment? ParseMagicRemoveEnchantment(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 8) return null;
|
||||
return new MagicRemoveEnchantment(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)));
|
||||
}
|
||||
|
||||
/// <summary>0x01A8 MagicRemoveSpell — spell id removed from spellbook.</summary>
|
||||
public static uint? ParseMagicRemoveSpell(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 4) return null;
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 0x02C2 MagicUpdateEnchantment — the Enchantment blob. Full layout
|
||||
/// (ACE <c>Enchantment.Pack</c>) is ~80+ bytes of spell metadata +
|
||||
/// stat mods. We expose the first few fields that drive the enchant
|
||||
/// bar UI; the rest is available via the raw payload view.
|
||||
/// </summary>
|
||||
public readonly record struct EnchantmentSummary(
|
||||
uint SpellId,
|
||||
uint LayerId,
|
||||
float Duration,
|
||||
uint CasterGuid);
|
||||
|
||||
public static EnchantmentSummary? ParseMagicUpdateEnchantment(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
// Layout (ACE Enchantment.Pack):
|
||||
// u32 spellId
|
||||
// u32 layerId
|
||||
// f32 duration
|
||||
// u32 casterGuid
|
||||
// ... (stat mods, category, power, etc.)
|
||||
if (payload.Length < 16) return null;
|
||||
return new EnchantmentSummary(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(8)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(12)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 0x02C7 MagicDispelEnchantment — (layerId, spellId).
|
||||
/// Structure matches MagicRemoveEnchantment.
|
||||
/// </summary>
|
||||
public static MagicRemoveEnchantment? ParseMagicDispelEnchantment(ReadOnlySpan<byte> payload)
|
||||
=> ParseMagicRemoveEnchantment(payload);
|
||||
|
||||
// ── Appraise / identify ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>0x00C9 IdentifyObjectResponse header.</summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue