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

View file

@ -200,27 +200,27 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
SubscribeToRecompute<ClientObject>(
h => inventory.Objects.ObjectAdded += h,
h => inventory.Objects.ObjectAdded -= h,
() => { RecomputeBurden(inventory, character); RecomputePvpStatus(inventory, character); });
() => RecomputePlayerQualities(inventory, character));
SubscribeToRecompute<ClientObject>(
h => inventory.Objects.ObjectUpdated += h,
h => inventory.Objects.ObjectUpdated -= h,
() => { RecomputeBurden(inventory, character); RecomputePvpStatus(inventory, character); });
() => RecomputePlayerQualities(inventory, character));
SubscribeToRecompute<ClientObject>(
h => inventory.Objects.ObjectRemoved += h,
h => inventory.Objects.ObjectRemoved -= h,
() => { RecomputeBurden(inventory, character); RecomputePvpStatus(inventory, character); });
() => RecomputePlayerQualities(inventory, character));
SubscribeToRecompute<ClientObjectMove>(
h => inventory.Objects.ObjectMoved += h,
h => inventory.Objects.ObjectMoved -= h,
() => { RecomputeBurden(inventory, character); RecomputePvpStatus(inventory, character); });
() => RecomputePlayerQualities(inventory, character));
SubscribeToRecompute<uint>(
h => inventory.Objects.ContainerContentsReplaced += h,
h => inventory.Objects.ContainerContentsReplaced -= h,
() => { RecomputeBurden(inventory, character); RecomputePvpStatus(inventory, character); });
() => RecomputePlayerQualities(inventory, character));
SubscribeParameterless(
h => inventory.Objects.Cleared += h,
h => inventory.Objects.Cleared -= h,
() => { RecomputeBurden(inventory, character); RecomputePvpStatus(inventory, character); });
() => RecomputePlayerQualities(inventory, character));
Subscribe<LocalPlayerState.AttributeKind>(
h => character.Character.LocalPlayer.AttributeChanged += h,
h => character.Character.LocalPlayer.AttributeChanged -= h,
@ -365,7 +365,8 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
/// </summary>
private static void RecomputeBurden(
LiveInventorySessionBindings inventory,
LiveCharacterSessionBindings character)
LiveCharacterSessionBindings character,
bool notify = true)
{
uint player = inventory.PlayerGuid();
ClientObject? playerObject = inventory.Objects.Get(player);
@ -381,6 +382,22 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
: inventory.Objects.SumCarriedBurden(player);
float load = EncumbranceSystem.Load(capacity, burden);
character.Character.MovementSkills.UpdateBurden(load);
if (notify)
character.OnMovementStatsUpdated?.Invoke();
}
private static void RecomputePlayerQualities(
LiveInventorySessionBindings inventory,
LiveCharacterSessionBindings character)
{
RecomputeBurden(inventory, character, notify: false);
RecomputePvpStatus(inventory, character, notify: false);
uint player = inventory.PlayerGuid();
PropertyBundle properties = inventory.Objects.Get(player)?.Properties
?? character.Character.LocalPlayer.Properties;
character.Character.UpdateMovementSkillAugmentations(
PlayerSkillMath.AugmentationBonuses.FromProperties(properties));
character.OnMovementStatsUpdated?.Invoke();
}
@ -397,7 +414,8 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
/// </summary>
private static void RecomputePvpStatus(
LiveInventorySessionBindings inventory,
LiveCharacterSessionBindings character)
LiveCharacterSessionBindings character,
bool notify = true)
{
uint player = inventory.PlayerGuid();
ClientObject? playerObject = inventory.Objects.Get(player);
@ -415,7 +433,8 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
character.Character.MovementSkills.UpdatePlayerKillerStatus(
pkStatus,
lastPkAttackTimestamp);
character.OnMovementStatsUpdated?.Invoke();
if (notify)
character.OnMovementStatsUpdated?.Invoke();
}
/// <summary>