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);
}
}