acdream/tests/AcDream.UI.Abstractions.Tests/VitalsVMTests.cs
Erik 05f6222865 fix(ui): complete live targeted healing flow
Route ACE server commands through the existing chat path, bind the retail paperdoll hit mask instead of its obscured viewport, and prefer authoritative private health vitals. Record the user-confirmed live gate and pin the production DAT widget type in tests.
2026-07-11 07:58:59 +02:00

153 lines
5 KiB
C#

using AcDream.Core.Combat;
using AcDream.Core.Player;
using AcDream.UI.Abstractions.Panels.Vitals;
namespace AcDream.UI.Abstractions.Tests;
public sealed class VitalsVMTests
{
[Fact]
public void HealthPercent_ReturnsCombatStateValue_AfterUpdateHealth()
{
var combat = new CombatState();
uint guid = 0x5000_0042u;
combat.OnUpdateHealth(guid, 0.42f);
var vm = new VitalsVM(combat);
vm.SetLocalPlayerGuid(guid);
Assert.Equal(0.42f, vm.HealthPercent, precision: 3);
}
[Fact]
public void HealthPercent_LocalPrivateVitalOverridesStaleCombatPercent()
{
var combat = new CombatState();
uint guid = 0x5000_0042u;
combat.OnUpdateHealth(guid, 1f);
var local = new LocalPlayerState();
local.OnVitalUpdate(vitalId: 2u, ranks: 50u, start: 50u, xp: 0u, current: 1u);
var vm = new VitalsVM(combat, local);
vm.SetLocalPlayerGuid(guid);
Assert.Equal(0.01f, vm.HealthPercent, precision: 3);
}
[Fact]
public void HealthPercent_ReadsCurrentOnlyPrivateVitalDeltaWithoutStaleCache()
{
var local = new LocalPlayerState();
local.OnVitalUpdate(vitalId: 2u, ranks: 50u, start: 50u, xp: 0u, current: 100u);
var vm = new VitalsVM(new CombatState(), local);
local.OnVitalCurrent(vitalId: 2u, current: 25u);
Assert.Equal(0.25f, vm.HealthPercent, precision: 3);
}
[Fact]
public void HealthPercent_ReturnsOne_WhenGuidUnknown()
{
var combat = new CombatState();
var vm = new VitalsVM(combat);
// No SetLocalPlayerGuid call — defaults to 0 which CombatState has never seen.
Assert.Equal(1f, vm.HealthPercent);
}
[Fact]
public void HealthPercent_ReturnsOne_WhenGuidSetButNeverUpdated()
{
var combat = new CombatState();
var vm = new VitalsVM(combat);
vm.SetLocalPlayerGuid(0xDEAD_BEEFu);
Assert.Equal(1f, vm.HealthPercent);
}
[Fact]
public void StaminaPercent_IsNull_WhenNoLocalPlayerStateProvided()
{
// Back-compat with the original D.2a constructor — when no
// LocalPlayerState is wired, Stam/Mana remain null and
// VitalsPanel skips drawing those bars.
var vm = new VitalsVM(new CombatState());
Assert.Null(vm.StaminaPercent);
}
[Fact]
public void ManaPercent_IsNull_WhenNoLocalPlayerStateProvided()
{
var vm = new VitalsVM(new CombatState());
Assert.Null(vm.ManaPercent);
}
[Fact]
public void StaminaPercent_FromLocalPlayerState_AfterVitalUpdate()
{
// Issue #5 — once a LocalPlayerState is wired and the server has
// sent a PrivateUpdateVital for Stamina (vital=4), the Stam bar
// surfaces the correct percent without any VM-level caching.
// ranks=50, start=50, current=80 → MaxApprox=100 → percent=0.8.
var local = new LocalPlayerState();
local.OnVitalUpdate(vitalId: 4u, ranks: 50u, start: 50u, xp: 0u, current: 80u);
var vm = new VitalsVM(new CombatState(), local);
Assert.Equal(0.8f, vm.StaminaPercent!.Value, precision: 3);
}
[Fact]
public void ManaPercent_FromLocalPlayerState_AfterVitalUpdate()
{
// ranks=20, start=80, current=25 → MaxApprox=100 → percent=0.25.
var local = new LocalPlayerState();
local.OnVitalUpdate(vitalId: 6u, ranks: 20u, start: 80u, xp: 0u, current: 25u);
var vm = new VitalsVM(new CombatState(), local);
Assert.Equal(0.25f, vm.ManaPercent!.Value, precision: 3);
}
[Fact]
public void Vm_ReadsThroughToLocalPlayerState_NoStaleCache()
{
// Verify the VM doesn't snapshot — every property access reads
// live so a server delta picks up next frame without any explicit
// refresh call.
var local = new LocalPlayerState();
var vm = new VitalsVM(new CombatState(), local);
Assert.Null(vm.StaminaPercent); // no data yet
local.OnVitalUpdate(vitalId: 4u, ranks: 50u, start: 50u, xp: 0u, current: 50u);
local.OnVitalUpdate(vitalId: 6u, ranks: 50u, start: 50u, xp: 0u, current: 50u);
Assert.Equal(0.5f, vm.StaminaPercent!.Value, precision: 3);
Assert.Equal(0.5f, vm.ManaPercent!.Value, precision: 3);
}
[Fact]
public void SetLocalPlayerGuid_ReroutesHealthLookup_WithoutStaleCache()
{
// Simulate the realistic GameWindow flow: VM is constructed pre-login
// with GUID=0, then SetLocalPlayerGuid is called at EnterWorld.
var combat = new CombatState();
uint playerGuid = 0x5003_E219u;
combat.OnUpdateHealth(playerGuid, 0.75f);
var vm = new VitalsVM(combat);
// Before SetLocalPlayerGuid — reads GUID=0 → returns safe 1.0.
Assert.Equal(1f, vm.HealthPercent);
vm.SetLocalPlayerGuid(playerGuid);
Assert.Equal(0.75f, vm.HealthPercent, precision: 3);
}
[Fact]
public void Constructor_ThrowsOnNullCombat()
{
Assert.Throws<ArgumentNullException>(() => new VitalsVM(null!));
}
}