merge: #267 vitae character-panel display (attributes vitae-immune per retail; skill dual parentheticals)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-30 18:14:48 +02:00
commit 2493f24c63
12 changed files with 984 additions and 108 deletions

View file

@ -219,6 +219,150 @@ public sealed class EnchantmentMathTests
Assert.Equal(1.5f, mod.Multiplier, precision: 3);
}
// ── Issue #267 — Attribute/Skill domain filter + EnchantAttribute/EnchantSkill goldens ──
[Fact]
public void GetMod_RequiredType_ExcludesCrossDomainKeyCollision()
{
// A Strength buff (Attribute, key=1) must NOT leak into a MaxHealth
// (SecondAtt, key=1) computation just because the numeric key collides.
var table = LoadTable((50u, "Strength Buff", 0u));
var enchantments = new[]
{
MakeTypedMultRecord(spellId: 50, layer: 1, statKey: 1u,
statModType: (uint)EnchantmentMath.EnchantmentTypeFlag.Attribute, val: 1.5f),
};
var secondAttMod = EnchantmentMath.GetMod(enchantments, table, statKey: 1u,
EnchantmentMath.EnchantmentTypeFlag.SecondAtt);
Assert.Equal(EnchantmentMath.VitalMod.Identity, secondAttMod);
var attributeMod = EnchantmentMath.GetMod(enchantments, table, statKey: 1u,
EnchantmentMath.EnchantmentTypeFlag.Attribute);
Assert.Equal(1.5f, attributeMod.Multiplier, precision: 3);
}
[Fact]
public void GetMod_IncludeVitaeFalse_ExcludesVitaeEvenWhenActive()
{
// Primary attributes (EnchantAttribute) never reference the vitae
// singleton in retail — includeVitae:false must fully exclude it.
var table = LoadTable((60u, "Vitae", 0u));
var enchantments = new[]
{
MakeVitaeRecord(spellId: 60, layer: 1, statKey: 0u, val: 0.67f),
};
var mod = EnchantmentMath.GetMod(enchantments, table, statKey: 1u,
EnchantmentMath.EnchantmentTypeFlag.Attribute, includeVitae: false);
Assert.Equal(EnchantmentMath.VitalMod.Identity, mod);
}
[Fact]
public void EnchantAttribute_NoMods_ReturnsBaseTruncated()
{
Assert.Equal(200, EnchantmentMath.EnchantAttribute(EnchantmentMath.VitalMod.Identity, 200u));
}
[Fact]
public void EnchantAttribute_Buff_AppliesMultiplierAndTruncates()
{
// 200 base with a +10% buff (unrelated to vitae — attributes are
// vitae-immune) → 220.
var mod = new EnchantmentMath.VitalMod(1.1f, 0f);
Assert.Equal(220, EnchantmentMath.EnchantAttribute(mod, 200u));
}
[Fact]
public void EnchantAttribute_FloorsAtOne_WhenBaseBelowTenAndDebuffed()
{
// Base 5 (< 10) crushed by a 0.1 multiplier would compute to 0.5,
// but retail floors small attributes at 1 rather than letting them
// hit zero (0x00594570: `< 0xa` branch floors at 1f).
var mod = new EnchantmentMath.VitalMod(0.1f, 0f);
Assert.Equal(1, EnchantmentMath.EnchantAttribute(mod, 5u));
}
[Fact]
public void EnchantAttribute_FloorsAtTen_WhenBaseAtOrAboveTenAndDebuffed()
{
// Base 50 (>= 10) crushed by a 0.1 multiplier would compute to 5,
// but retail floors at 10 for this base range.
var mod = new EnchantmentMath.VitalMod(0.1f, 0f);
Assert.Equal(10, EnchantmentMath.EnchantAttribute(mod, 50u));
}
[Fact]
public void EnchantSkill_ThirtyThreePercentVitae_MatchesGoldenValue()
{
// 33% vitae penalty on a base-303 skill: 303 * 0.67 = 203.01 -> 203.
var mod = new EnchantmentMath.VitalMod(0.67f, 0f);
Assert.Equal(203, EnchantmentMath.EnchantSkill(mod, 303u));
}
[Fact]
public void EnchantSkill_BuffPlusVitaeComposition_MatchesGoldenValue()
{
// Base 300, vitae 0.67 (33%) composed with a +50 additive buff:
// (300 * 0.67) + 50 = 201 + 50 = 251.
var mod = new EnchantmentMath.VitalMod(0.67f, 50f);
Assert.Equal(251, EnchantmentMath.EnchantSkill(mod, 300u));
}
[Fact]
public void EnchantSkill_ZeroFloorsBelowHalf()
{
// A crushing vitae/debuff combination that drops the result under
// 0.5 floors to 0 rather than truncating to a stray small value.
var mod = new EnchantmentMath.VitalMod(0.001f, 0f);
Assert.Equal(0, EnchantmentMath.EnchantSkill(mod, 10u));
}
[Fact]
public void GetVitaeMultiplier_NoVitae_ReturnsOne()
{
var enchantments = new[]
{
MakeMultRecord(spellId: 1, layer: 1, statKey: 1u, val: 1.5f),
};
Assert.Equal(1.0f, EnchantmentMath.GetVitaeMultiplier(enchantments));
}
[Fact]
public void GetVitaeMultiplier_WithVitae_ReturnsItsValue()
{
var enchantments = new[]
{
MakeVitaeRecord(spellId: 1, layer: 1, statKey: 0u, val: 0.67f),
MakeMultRecord(spellId: 2, layer: 2, statKey: 1u, val: 1.5f), // must not affect vitae isolation
};
Assert.Equal(0.67f, EnchantmentMath.GetVitaeMultiplier(enchantments), precision: 3);
}
[Fact]
public void SkillVitaeModifier_ThirtyThreePercent_MatchesGoldenValue()
{
// Base 303 skill, 33% vitae: truncate(303 * 0.67) - 303 = 203 - 303 = -100.
// This is the user-reported oracle example: "(-100)".
var enchantments = new[]
{
MakeVitaeRecord(spellId: 1, layer: 1, statKey: 0u, val: 0.67f),
};
Assert.Equal(-100, EnchantmentMath.SkillVitaeModifier(enchantments, baseValue: 303u));
}
[Fact]
public void SkillVitaeModifier_NoVitae_ReturnsZero()
{
Assert.Equal(0, EnchantmentMath.SkillVitaeModifier(
System.Array.Empty<ActiveEnchantmentRecord>(), baseValue: 303u));
}
private static ActiveEnchantmentRecord MakeTypedMultRecord(
uint spellId, uint layer, uint statKey, uint statModType, float val) =>
new(spellId, layer, 60f, 0u, StatModType: statModType, StatModKey: statKey,
StatModValue: val, Bucket: 1u);
private static ActiveEnchantmentRecord MakeMultRecord(uint spellId, uint layer, uint statKey, float val) =>
new(spellId, layer, 60f, 0u, StatModType: 0, StatModKey: statKey, StatModValue: val, Bucket: 1u);
@ -235,7 +379,7 @@ public sealed class EnchantmentMathTests
uint spellId, uint layer, uint skillId, float val) =>
new(
spellId, layer, 60f, 0u,
StatModType: EnchantmentMath.EnchantmentTypeFlag.Skill,
StatModType: (uint)EnchantmentMath.EnchantmentTypeFlag.Skill,
StatModKey: skillId,
StatModValue: val,
Bucket: 1u);
@ -244,7 +388,7 @@ public sealed class EnchantmentMathTests
uint spellId, uint layer, uint skillId, float val) =>
new(
spellId, layer, 60f, 0u,
StatModType: EnchantmentMath.EnchantmentTypeFlag.Skill,
StatModType: (uint)EnchantmentMath.EnchantmentTypeFlag.Skill,
StatModKey: skillId,
StatModValue: val,
Bucket: 2u);
@ -253,8 +397,8 @@ public sealed class EnchantmentMathTests
public void EnchantmentTypeFlag_Skill_MatchesAceEnchantmentTypeFlags()
{
// ACE.Entity.Enum.EnchantmentTypeFlags.Skill = 0x0000010.
Assert.Equal(0x0000010u, EnchantmentMath.EnchantmentTypeFlag.Skill);
Assert.Equal(0x0000002u, EnchantmentMath.EnchantmentTypeFlag.SecondAtt);
Assert.Equal(0x0000010u, (uint)EnchantmentMath.EnchantmentTypeFlag.Skill);
Assert.Equal(0x0000002u, (uint)EnchantmentMath.EnchantmentTypeFlag.SecondAtt);
}
[Fact]
@ -304,7 +448,7 @@ public sealed class EnchantmentMathTests
{
new ActiveEnchantmentRecord(
53u, 1u, 60f, 0u,
StatModType: EnchantmentMath.EnchantmentTypeFlag.SecondAtt,
StatModType: (uint)EnchantmentMath.EnchantmentTypeFlag.SecondAtt,
StatModKey: 24u,
StatModValue: 1.5f,
Bucket: 1u),