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

@ -64,6 +64,7 @@ public sealed class RuntimeCharacterState : IDisposable
/// </summary>
private int _runSkillBase = -1;
private int _jumpSkillBase = -1;
private PlayerSkillMath.AugmentationBonuses _movementSkillAugmentations;
public RuntimeCharacterState(SpellTable? spellTable = null)
{
@ -142,13 +143,14 @@ public sealed class RuntimeCharacterState : IDisposable
&& MovementSkills.PlayerKillerStatus == -1
&& MovementSkills.LastPkAttackTimestamp is null
&& _runSkillBase == -1
&& _jumpSkillBase == -1);
&& _jumpSkillBase == -1
&& _movementSkillAugmentations == default);
}
/// <summary>
/// Campaign P Slice P1 (2026-07-30): stores the pre-<c>EnchantSkill</c>
/// base run/jump skill (PlayerDescription's formulaBonus+init+ranks)
/// and pushes the vitae/enchantment-adjusted result into
/// and pushes the augmentation/vitae/enchantment-adjusted result into
/// <see cref="MovementSkills"/> — the SAME call shape
/// <c>LiveSessionEventRouter</c>'s pre-P1 <c>onSkillsUpdated</c> callback
/// already used (<c>MovementSkills.Update(runSkill, jumpSkill)</c>), now
@ -165,6 +167,22 @@ public sealed class RuntimeCharacterState : IDisposable
RecomputeMovementSkills();
}
/// <summary>
/// Installs the player-quality augmentation terms consumed by retail
/// <c>CACQualities::InqRunRate</c>/<c>InqJumpVelocity</c>. The object
/// table remains authoritative for live PropertyInt updates; Runtime
/// retains only this immutable derived snapshot.
/// </summary>
public void UpdateMovementSkillAugmentations(
PlayerSkillMath.AugmentationBonuses augmentations)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_movementSkillAugmentations == augmentations)
return;
_movementSkillAugmentations = augmentations;
RecomputeMovementSkills();
}
/// <summary>
/// Re-derives the adjusted run/jump skill from the stored base plus the
/// CURRENT spellbook state (vitae + skill enchantments) — matching
@ -178,10 +196,10 @@ public sealed class RuntimeCharacterState : IDisposable
private void RecomputeMovementSkills()
{
int run = _runSkillBase >= 0
? ApplySkillEnchantments(_runSkillBase, RunSkillId)
? CalculateMovementSkill(_runSkillBase, RunSkillId)
: -1;
int jump = _jumpSkillBase >= 0
? ApplySkillEnchantments(_jumpSkillBase, JumpSkillId)
? CalculateMovementSkill(_jumpSkillBase, JumpSkillId)
: -1;
// #266 apparatus (permanent, low-volume — fires only on skill-base or
// enchantment changes, the [snap] class): the full stat-chain state at
@ -196,14 +214,19 @@ public sealed class RuntimeCharacterState : IDisposable
MovementSkills.Update(run, jump);
}
private int ApplySkillEnchantments(int baseSkill, uint skillId)
private int CalculateMovementSkill(int baseSkill, uint skillId)
{
EnchantmentMath.VitalMod mod = Spellbook.GetSkillMod(skillId);
float adjusted = baseSkill * mod.Multiplier + mod.Additive;
// CEnchantmentRegistry::EnchantSkill pc 416240: floor to 0 below
// 0.5, then truncate (retail _ftol2, a C-style cast).
if (adjusted < 0.5f) adjusted = 0f;
return (int)adjusted;
float vitae = EnchantmentMath.GetVitaeMultiplier(
Spellbook.ActiveEnchantments);
uint advancementClass = LocalPlayer.GetSkill(skillId)?.Status ?? 0u;
return PlayerSkillMath.Calculate(
baseSkill,
skillId,
advancementClass,
_movementSkillAugmentations,
mod,
vitae).EffectiveLevel;
}
private void OnEnchantmentsChangedForMovement() => RecomputeMovementSkills();
@ -331,6 +354,7 @@ public sealed class RuntimeCharacterState : IDisposable
Try(Options.ResetSession, ref failures);
_runSkillBase = -1;
_jumpSkillBase = -1;
_movementSkillAugmentations = default;
Try(MovementSkills.ResetSession, ref failures);
if (failures is not null)
{
@ -355,6 +379,7 @@ public sealed class RuntimeCharacterState : IDisposable
Try(Options.ResetSession, ref failures);
_runSkillBase = -1;
_jumpSkillBase = -1;
_movementSkillAugmentations = default;
Try(MovementSkills.ResetSession, ref failures);
}
finally