feat(player): port retail augmentation stat chain

This commit is contained in:
Erik 2026-07-31 08:08:23 +02:00
parent 0cb60d98a0
commit 461a1fb7b4
22 changed files with 953 additions and 138 deletions

View file

@ -28,22 +28,15 @@ namespace AcDream.Core.Spells;
/// <para>
/// <b>Vitae</b> (death penalty) is a singleton on
/// <c>CEnchantmentRegistry._vitae</c>, applied multiplicatively after
/// the buff lists. We don't yet wire it through.
/// the buff lists.
/// </para>
///
/// <para>
/// <b>Current implementation status:</b> the aggregator iterates
/// <see cref="Spellbook.ActiveEnchantments"/> and applies
/// <see cref="SpellTable"/> family-stacking deduplication, but
/// **returns identity (1.0, 0.0) for stat modifiers** because our
/// <see cref="ActiveEnchantmentRecord"/> doesn't yet carry the
/// <c>StatMod (type/key/val)</c> triad — that requires extending
/// <c>ParseMagicUpdateEnchantment</c> to read the full Enchantment
/// payload (60-64 bytes per holtburger
/// <c>messages/magic/types.rs</c>) and storing it on the record.
/// Filed as ISSUES.md #12. Once that lands, the aggregator's
/// `effectiveMult * mod.Val` and `additive + mod.Val` paths fire and
/// the Vitals HUD percent gap closes.
/// <b>Current implementation status:</b> the aggregator consumes the same
/// complete <c>StatMod (type/key/value)</c> record shape from both the
/// PlayerDescription snapshot and live <c>MagicUpdateEnchantment</c>
/// (0x02C2), applies retail family stacking, then evaluates the selected
/// attribute, secondary attribute, or skill domain.
/// </para>
///
/// <para>
@ -167,9 +160,8 @@ public static class EnchantmentMath
// Bucket 2 (Additive): additive += ench.StatModValue
// Bucket 4 (Vitae): multiplier *= ench.StatModValue (post-pass)
// Bucket 8 (Cooldown): skipped (doesn't affect vital max)
// Records without StatMod data (StatModKey == null) — e.g.
// those from older MagicUpdateEnchantment events that don't
// yet parse the full payload — contribute nothing.
// Records without StatMod data (StatModKey == null) are valid for
// non-stat enchantment classes and contribute nothing here.
float multiplier = 1.0f;
float additive = 0.0f;
float vitae = 1.0f;
@ -283,10 +275,17 @@ public static class EnchantmentMath
public static int SkillVitaeModifier(
IEnumerable<ActiveEnchantmentRecord> enchantments,
uint baseValue)
=> SkillVitaeModifier(GetVitaeMultiplier(enchantments), baseValue);
/// <summary>
/// Value overload for callers that already captured the registry's vitae
/// singleton. Keeping the truncation here prevents the movement and UI
/// paths from growing subtly different copies of retail's calculation.
/// </summary>
public static int SkillVitaeModifier(float vitaeMultiplier, uint baseValue)
{
float vitae = GetVitaeMultiplier(enchantments);
if (vitae == 1.0f) return 0;
return (int)(baseValue * vitae) - (int)baseValue;
if (vitaeMultiplier == 1.0f) return 0;
return (int)(baseValue * vitaeMultiplier) - (int)baseValue;
}
/// <summary>

View file

@ -267,11 +267,9 @@ public sealed class Spellbook
}
/// <summary>
/// Issue #7 / #12 — accept a fully-populated record from
/// <c>PlayerDescription</c>'s enchantment block (which carries
/// the StatMod triad + bucket). Used when the wire-format extension
/// gives us the full per-enchantment payload, rather than the
/// 4-field summary from <c>MagicUpdateEnchantment</c>.
/// Accept the canonical fully-populated enchantment record shared by
/// <c>PlayerDescription</c> and live <c>MagicUpdateEnchantment</c>
/// (0x02C2), including the StatMod triad and bucket classification.
/// </summary>
public void OnEnchantmentAdded(ActiveEnchantmentRecord record)
{