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_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(() => new VitalsVM(null!)); } }