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>
Implements the inbound GameEvent routing layer — the single biggest
network-protocol gap per r08 (94 sub-opcodes, zero handled before).
WorldSession now detects 0xF7B0, parses the 16-byte header (guid +
gameEventSequence + eventType), and forwards to a pluggable
GameEventDispatcher.
Added:
- GameEventEnvelope record + TryParse with layout from
ACE GameEventMessage.cs.
- GameEventType enum: all 94 S→C sub-opcodes from
ACE.Server.Network.GameEvent.GameEventType, named per ACE conventions.
- GameEventDispatcher: handler registry + unhandled-counts bag for
diagnostics ("which server events are firing that we don't parse?").
Handlers invoked synchronously on the decode thread; thrown exceptions
are swallowed + logged to stderr so one bad handler can't take down
the packet loop.
- GameEvents parsers: ChannelBroadcast, Tell, TransientMessage,
PopupString, WeenieError (+ WithString), UpdateHealth, PingResponse,
MagicUpdateSpell. Each returns a typed record or null on malformed
payload. String16L helper matches the existing CharacterList pattern
(u16 length + ASCII bytes + 4-byte pad).
- WorldSession.GameEvents property exposing the dispatcher so
GameWindow / UI / chat can register handlers at startup.
Wired into WorldSession.ProcessDatagram: new `else if (op ==
GameEventEnvelope.Opcode)` branch with TryParse + Dispatch.
Tests (13 new):
- Envelope: valid round-trip, wrong outer opcode, too-short body.
- Dispatcher: handler invoked, unhandled count, exception isolation,
unregister + rollover to unhandled.
- Event parsers: ChannelBroadcast, Tell, UpdateHealth, WeenieError,
Transient, MagicUpdateSpell.
Total: 521 tests pass (up from 508).
With this dispatcher in place, Phase F.2 (items + appraise), F.3 (combat
+ damage), F.4 (spell cast state machine), chat UI, allegiance, quest
tracker — all of which depend on GameEvent handling — are unblocked.
Ref: r08 §4 (GameEvent sub-opcode table), §2 (envelope wire shape).
Ref: ACE GameEventMessage.cs / GameEventType.cs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>