Port of gmToolbarUI::HandleSelectionChanged (acclient_2013_pseudo_c.txt:198635). When the player selects a world object the action bar's bottom strip shows the object name + (for player/pet/attackable targets) a live Health meter; deselect clears it. Mana (#140) + stack slider deferred. - SelectedObjectController (new): clear-then-populate on selection change; sets name (UiText child, VitalsController pattern), overlay state (ObjectSelected / StackedItemSelected via UiDatElement.ActiveState), shows the health meter and sends QueryHealth for health targets. Subscribes via a delegate seam (no GameWindow coupling). - GameWindow: _selectedGuid field -> SelectedGuid property + SelectionChanged event (fires on actual change only); 3 write sites converted, reads untouched. All selection-write paths (LMB pick, Tab/Q, despawn-clear via Tick()) run on the render thread, so the event-driven UI mutation is single-threaded. - WorldSession.SendQueryHealth (0x01BF) — wraps SocialActions.BuildQueryHealth. - DatWidgetFactory.BuildMeter: handle the single-image toolbar meter shape (back-track on the element's own DirectState, fill on one Type-3 child). The sprites go in the TILE slot (DrawMode=Normal tiles to full bar geometry per UIElement_Meter::DrawChildren) — a left-cap assignment would gap/clamp a sub-140px sprite. Vitals 3-slice path unchanged. - ToolbarController.HiddenIds: A1 (health) now owned by SelectedObjectController; A2 (mana) + A4 (stack) stay hidden (deferred) so their dat back-tracks don't render as stray empty bars. Adversarial Opus review found + fixed: the mana-meter orphan (A2 left unhidden) and the meter tile-vs-cap render bug (C1). Divergence rows AP-46 (health gate approximation: IsLiveCreatureTarget vs IsPlayer||pet||attackable) + AP-47 (meter shown on select vs on UpdateHealth reply). Spec §5 corrected. Build + full test suite green (2,684 passed / 4 skipped). Health meter render fidelity (full-width fill + fraction mapping) pending the user's visual gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
2.6 KiB
C#
91 lines
2.6 KiB
C#
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);
|
|
}
|
|
|
|
[Fact]
|
|
public void SendQueryHealth_UsesRetailQueryHealthBuilder()
|
|
{
|
|
// Retail anchor: CM_Combat::Event_QueryHealth / gmToolbarUI::HandleSelectionChanged:198635
|
|
using var session = NewSession();
|
|
byte[]? captured = null;
|
|
session.GameActionCapture = body => captured = body;
|
|
|
|
session.SendQueryHealth(0x50000007u);
|
|
|
|
Assert.NotNull(captured);
|
|
Assert.Equal(SocialActions.BuildQueryHealth(1, 0x50000007u), captured);
|
|
}
|
|
}
|