using AcDream.Core.Player; namespace AcDream.Core.Tests.Player; /// /// Tests for — per-vital + per-attribute /// cache populated from PlayerDescription's attribute block (ids /// 1..=6 primary attrs, 7..=9 vitals) and from /// PrivateUpdateVital(Current) deltas (ids 1..=6 in their /// alternate role as wire-opcode Vital ids). /// /// /// Max formula tested here: vital.(ranks+start) + attribute_contribution /// with retail coefficients (Endurance/2 for Health, Endurance for Stamina, /// Self for Mana). See class doc. /// /// public sealed class LocalPlayerStateTests { [Fact] public void Defaults_AllVitalsNull_PercentsNull() { var s = new LocalPlayerState(); Assert.Null(s.Get(LocalPlayerState.VitalKind.Health)); Assert.Null(s.Get(LocalPlayerState.VitalKind.Stamina)); Assert.Null(s.Get(LocalPlayerState.VitalKind.Mana)); Assert.Null(s.HealthPercent); Assert.Null(s.StaminaPercent); Assert.Null(s.ManaPercent); } [Theory] // Wire opcode IDs (PrivateUpdateVital + PrivateUpdateVitalCurrent), // ACE Vital enum: Undef=0, MaxHealth=1, Health=2, MaxStamina=3, Stamina=4, MaxMana=5, Mana=6. [InlineData(1u, LocalPlayerState.VitalKind.Health)] [InlineData(2u, LocalPlayerState.VitalKind.Health)] [InlineData(3u, LocalPlayerState.VitalKind.Stamina)] [InlineData(4u, LocalPlayerState.VitalKind.Stamina)] [InlineData(5u, LocalPlayerState.VitalKind.Mana)] [InlineData(6u, LocalPlayerState.VitalKind.Mana)] // PlayerDescription attribute-block IDs. [InlineData(7u, LocalPlayerState.VitalKind.Health)] [InlineData(8u, LocalPlayerState.VitalKind.Stamina)] [InlineData(9u, LocalPlayerState.VitalKind.Mana)] public void VitalIdToKind_MapsBothIdSystems_ToSameKind(uint vitalId, LocalPlayerState.VitalKind expected) { Assert.Equal(expected, LocalPlayerState.VitalIdToKind(vitalId)); } [Theory] [InlineData(0u)] [InlineData(10u)] [InlineData(99u)] public void VitalIdToKind_ReturnsNull_ForUnknownId(uint vitalId) { Assert.Null(LocalPlayerState.VitalIdToKind(vitalId)); } [Theory] [InlineData(1u, LocalPlayerState.AttributeKind.Strength)] [InlineData(2u, LocalPlayerState.AttributeKind.Endurance)] [InlineData(3u, LocalPlayerState.AttributeKind.Quickness)] [InlineData(4u, LocalPlayerState.AttributeKind.Coordination)] [InlineData(5u, LocalPlayerState.AttributeKind.Focus)] [InlineData(6u, LocalPlayerState.AttributeKind.Self)] public void AttributeIdToKind_MapsPrimaryAttrIds(uint atType, LocalPlayerState.AttributeKind expected) { Assert.Equal(expected, LocalPlayerState.AttributeIdToKind(atType)); } [Theory] [InlineData(0u)] [InlineData(7u)] // Vitals are not primary attrs in this lookup. [InlineData(99u)] public void AttributeIdToKind_ReturnsNull_ForNonPrimaryIds(uint atType) { Assert.Null(LocalPlayerState.AttributeIdToKind(atType)); } [Fact] public void OnVitalUpdate_PopulatesSnapshot_FromFullMessage() { var s = new LocalPlayerState(); s.OnVitalUpdate(vitalId: 4u, ranks: 100u, start: 120u, xp: 50000u, current: 180u); var stam = s.Get(LocalPlayerState.VitalKind.Stamina); Assert.NotNull(stam); Assert.Equal(100u, stam!.Value.Ranks); Assert.Equal(120u, stam.Value.Start); Assert.Equal(50000u, stam.Value.Xp); Assert.Equal(180u, stam.Value.Current); } [Fact] public void OnAttributeUpdate_PopulatesSnapshot() { var s = new LocalPlayerState(); s.OnAttributeUpdate(atType: 2u, ranks: 50u, start: 100u, xp: 12345u); var endurance = s.GetAttribute(LocalPlayerState.AttributeKind.Endurance); Assert.NotNull(endurance); Assert.Equal(50u, endurance!.Value.Ranks); Assert.Equal(100u, endurance.Value.Start); Assert.Equal(12345u, endurance.Value.Xp); // Current = ranks + start. Assert.Equal(150u, endurance.Value.Current); } [Fact] public void HealthPercent_UsesEnduranceContribution_DividedByTwo() { // Endurance.current = ranks(50) + start(150) = 200. Contribution = 100. // Health vital: ranks=0 start=0 cur=80. MaxApprox = 0 + 100 = 100. Percent = 0.8. var s = new LocalPlayerState(); s.OnAttributeUpdate(atType: 2u, ranks: 50u, start: 150u, xp: 0u); // Endurance s.OnVitalUpdate(vitalId: 7u, ranks: 0u, start: 0u, xp: 0u, current: 80u); // Health (PD id) Assert.Equal(100u, s.GetMaxApprox(LocalPlayerState.VitalKind.Health)); Assert.Equal(0.8f, s.HealthPercent!.Value, precision: 3); } [Fact] public void StaminaPercent_UsesEnduranceContribution_FullValue() { // Endurance.current = 200, Health/2=100. Stamina takes full Endurance=200. // Stamina vital: ranks=0 start=0 cur=150. MaxApprox = 0 + 200 = 200. Percent = 0.75. var s = new LocalPlayerState(); s.OnAttributeUpdate(atType: 2u, ranks: 50u, start: 150u, xp: 0u); s.OnVitalUpdate(vitalId: 8u, ranks: 0u, start: 0u, xp: 0u, current: 150u); Assert.Equal(200u, s.GetMaxApprox(LocalPlayerState.VitalKind.Stamina)); Assert.Equal(0.75f, s.StaminaPercent!.Value, precision: 3); } [Fact] public void ManaPercent_UsesSelfContribution() { // Self.current = 100. Mana vital: ranks=20 start=80 cur=100. MaxApprox = 100 + 100 = 200. var s = new LocalPlayerState(); s.OnAttributeUpdate(atType: 6u, ranks: 50u, start: 50u, xp: 0u); // Self s.OnVitalUpdate(vitalId: 9u, ranks: 20u, start: 80u, xp: 0u, current: 100u); // Mana Assert.Equal(200u, s.GetMaxApprox(LocalPlayerState.VitalKind.Mana)); Assert.Equal(0.5f, s.ManaPercent!.Value, precision: 3); } [Fact] public void Percent_ZeroWhenAttributeAndVitalBothZero() { // Without any attribute or vital ranks, MaxApprox=0 → percent null // (no /0). Vital received but no useful information. var s = new LocalPlayerState(); s.OnVitalUpdate(vitalId: 8u, ranks: 0u, start: 0u, xp: 0u, current: 0u); Assert.Null(s.StaminaPercent); } [Fact] public void Percent_ClampsToOne_WhenCurrentExceedsMax() { // Endurance contribution = 100/1 = 100. Stamina ranks+start = 0. // MaxApprox = 100. Current = 150 → ratio 1.5 → clamps to 1.0. var s = new LocalPlayerState(); s.OnAttributeUpdate(atType: 2u, ranks: 50u, start: 50u, xp: 0u); s.OnVitalUpdate(vitalId: 8u, ranks: 0u, start: 0u, xp: 0u, current: 150u); Assert.Equal(1f, s.StaminaPercent!.Value); } [Fact] public void OnVitalCurrent_UpdatesOnlyCurrent_LeavesRanksStartXpAlone() { var s = new LocalPlayerState(); s.OnAttributeUpdate(atType: 2u, ranks: 50u, start: 150u, xp: 0u); s.OnVitalUpdate(vitalId: 8u, ranks: 0u, start: 0u, xp: 50000u, current: 180u); s.OnVitalCurrent(vitalId: 8u, current: 90u); var stam = s.Get(LocalPlayerState.VitalKind.Stamina)!.Value; Assert.Equal(0u, stam.Ranks); Assert.Equal(0u, stam.Start); Assert.Equal(50000u, stam.Xp); Assert.Equal(90u, stam.Current); // Endurance.current = 200; MaxApprox = 200; percent = 90/200 = 0.45. Assert.Equal(0.45f, s.StaminaPercent!.Value, precision: 3); } [Fact] public void OnVitalCurrent_NoOp_WhenNoFullUpdateYet() { var s = new LocalPlayerState(); s.OnVitalCurrent(vitalId: 4u, current: 90u); Assert.Null(s.Get(LocalPlayerState.VitalKind.Stamina)); Assert.Null(s.StaminaPercent); } [Fact] public void Changed_FiresOnFullVitalUpdate_WithCorrectKind() { var s = new LocalPlayerState(); var seen = new List(); s.Changed += k => seen.Add(k); s.OnVitalUpdate(vitalId: 2u, ranks: 1u, start: 1u, xp: 0u, current: 1u); s.OnVitalUpdate(vitalId: 4u, ranks: 1u, start: 1u, xp: 0u, current: 1u); s.OnVitalUpdate(vitalId: 6u, ranks: 1u, start: 1u, xp: 0u, current: 1u); Assert.Equal(new[] { LocalPlayerState.VitalKind.Health, LocalPlayerState.VitalKind.Stamina, LocalPlayerState.VitalKind.Mana, }, seen); } [Fact] public void AttributeChanged_FiresOnPrimaryAttrUpdate() { var s = new LocalPlayerState(); var seen = new List(); s.AttributeChanged += k => seen.Add(k); s.OnAttributeUpdate(atType: 2u, ranks: 1u, start: 1u, xp: 0u); // Endurance s.OnAttributeUpdate(atType: 6u, ranks: 1u, start: 1u, xp: 0u); // Self Assert.Equal(new[] { LocalPlayerState.AttributeKind.Endurance, LocalPlayerState.AttributeKind.Self, }, seen); } [Fact] public void OnAttributeUpdate_DoesNotAffectVitals_DirectlyButRefreshesPercent() { var s = new LocalPlayerState(); s.OnVitalUpdate(vitalId: 8u, ranks: 0u, start: 0u, xp: 0u, current: 100u); // Pre-attribute: percent null because MaxApprox = 0. Assert.Null(s.StaminaPercent); s.OnAttributeUpdate(atType: 2u, ranks: 50u, start: 150u, xp: 0u); // Endurance.current = 200 // Now MaxApprox = 0 + 200 = 200; percent = 100/200 = 0.5. Assert.Equal(0.5f, s.StaminaPercent!.Value, precision: 3); } }