feat(physics): Campaign P P1 - stat-coupled movement (burden/stamina/vitae)
Ports the retail CACQualities/EncumbranceSystem/MovementSystem chain (named-retail decomp pc 256393/412901-414050/416169-416320/695958+) so PlayerWeenie's run rate, jump height, jump permission, and jump stamina cost are real functions of burden, current stamina, and vitae/skill enchantments instead of stubs. Core: - New EncumbranceSystem.cs (delegates to the already-verified BurdenMath formulas — one source of truth for the burden HUD and movement physics) and MovementSystem.cs (GetRunRate/GetJumpHeight/ JumpStaminaCost/GetJumpPower, decomp-cited; ACE cross-referenced where BN dropped the general-case arithmetic entirely). - PlayerWeenie rewritten as the CACQualities-shaped composition: CanJump gates on burden (<2.0 load, UN-8 — x87 polarity resolved by plausibility, Ghidra MCP unavailable this slice), JumpStaminaCost returns the real ceil((load+0.5)*power*8+2) cost and always affords it (matches decomp — retail's own function never refuses; "weak" jump comes entirely from the stamina==0 skill-zeroing gate inside InqRunRate/InqJumpVelocity, not a hard refusal), SetStamina wires a null="unknown, don't gate" sentinel preserving every pre-P1 test. - EnchantmentMath.GetMod gained an optional StatModType flag filter (GetSkillMod convenience wrapper) so the SAME vitae/family-stacking machinery already used for vital-max buffs now also answers "what's the vitae+skill-enchantment-adjusted Run/Jump skill" — reusing the M3 active-enchantment state, not a new engine. Runtime: - RuntimeCharacterState now stores the pre-EnchantSkill base run/jump skill and recomputes the adjusted value (vitae first, then matching Skill-flagged buffs, floor 0.5, truncate) on every base push AND on every Spellbook.EnchantmentsChanged notification — a vitae change alone moves the produced rate without a fresh PlayerDescription. - RuntimeMovementSkillState extended with Burden/CurrentStamina (RuntimeMovementSkillProjection.ApplyTo pushes both through the existing seam); LiveSessionEventRouter recomputes burden from the same Strength+aug-property+EncumbranceVal inputs the burden HUD already assembles (reacting to the same ClientObjectTable events) and pushes current stamina from LocalPlayerState vital updates. - Wires the previously dead-lettered ReportExhaustion() R3-W4 seam: LiveSessionRuntimeFactory's OnMovementStatsUpdated callback re- applies the current snapshot to the live controller and forces an immediate movement re-evaluation on any skill/burden/stamina change. Register: retires TS-5 (CanJump/JumpStaminaCost stubs) and AP-25 (no vitae in pushed skill). Adds AP-127 (two minor unmodeled retail bonus properties + the stamina-buff-adjusts-local-copy nuance, deliberately out of the bounded "run/jump query path only" scope) and UN-8 (the CanJump x87 polarity call, flagged for a future Ghidra MCP confirmation pass). Extends TS-23 (PlayerKillerStatus not parsed) to cover JumpStaminaCost's new pk parameter, hardcoded false pending P3. Full pseudocode + retail citations + the vitae/skill-level finding in docs/research/2026-07-30-stat-coupled-movement-pseudocode.md. Release suite: Core.Tests 3977/2 skips, Runtime.Tests 425/0 skips, App.Tests 3968/3 skips — all green. (One pre-existing, unrelated Debug- only flake in LandblockBuildOriginTests reproduces on the pre-P1 baseline and passes in Release; not touched here.) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3a4782048e
commit
9355ddcec6
20 changed files with 1714 additions and 74 deletions
|
|
@ -1,18 +1,56 @@
|
|||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// IWeenieObject implementation for the local player. Provides skill-based
|
||||
/// run rate and jump velocity calculations.
|
||||
/// IWeenieObject implementation for the local player — the C# analogue of
|
||||
/// retail's <c>CACQualities</c> "qualities DB" composition (named-retail
|
||||
/// decomp pc 412901-414050; ACCWeenieObject's own CanJump/JumpStaminaCost/
|
||||
/// InqRunRate/InqJumpVelocity/InqMaxRunRate are thin delegations to exactly
|
||||
/// this object, gated on <c>IsThePlayer()</c> — pc 406512+, confirming this
|
||||
/// query family only ever reaches the LOCAL player's weenie).
|
||||
///
|
||||
/// Formulas from decompiled acclient.exe, cross-referenced against
|
||||
/// ACE MovementSystem.GetRunRate and MovementSystem.GetJumpHeight.
|
||||
/// <para>
|
||||
/// Campaign P Slice P1 (2026-07-30): burden, current stamina, and the
|
||||
/// vitae/enchantment-adjusted run/jump skill are now real, pushed inputs —
|
||||
/// see <c>docs/research/2026-07-30-stat-coupled-movement-pseudocode.md</c>.
|
||||
/// <c>_runSkill</c>/<c>_jumpSkill</c> arrive ALREADY <c>EnchantSkill</c>-adjusted
|
||||
/// (vitae + skill enchantments folded in by
|
||||
/// <c>AcDream.Runtime.Gameplay.RuntimeCharacterState.RecomputeMovementSkills</c>)
|
||||
/// — this class stays a pure formula consumer with no Spellbook/enchantment
|
||||
/// dependency, matching retail's OWN split between <c>CACQualities</c> (the
|
||||
/// query surface) and <c>CEnchantmentRegistry</c> (the buff aggregator it
|
||||
/// calls into internally).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Formulas from decompiled acclient.exe (<see cref="MovementSystem"/> /
|
||||
/// <see cref="EncumbranceSystem"/>), cross-referenced against ACE
|
||||
/// <c>MovementSystem.GetRunRate</c>/<c>GetJumpHeight</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class PlayerWeenie : IWeenieObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Retail <c>CACQualities::CanJump</c>'s hard burden gate (0x00591b50,
|
||||
/// pc 412907) — x87 mush, polarity resolved by domain plausibility
|
||||
/// (register row UN-8; Ghidra MCP was unavailable to confirm). Chosen
|
||||
/// to coincide with <see cref="EncumbranceSystem.LoadMod"/>'s own floor.
|
||||
/// </summary>
|
||||
public const float CanJumpLoadThreshold = 2.0f;
|
||||
|
||||
private int _runSkill;
|
||||
private int _jumpSkill;
|
||||
private float _burden;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>AttributeCache::InqAttribute2nd(ATTR2ND_STAMINA=4)</c>'s
|
||||
/// current-stamina reading, consulted by <c>InqRunRate</c>/
|
||||
/// <c>InqJumpVelocity</c> to zero the effective skill when exhausted
|
||||
/// (pc 413824-413898, 413902-413979). <c>null</c> = never pushed
|
||||
/// (matches every pre-P1 caller/test — no gating, today's behavior
|
||||
/// unchanged); any non-negative value including 0 is a real reading.
|
||||
/// </summary>
|
||||
private uint? _currentStamina;
|
||||
|
||||
public PlayerWeenie(int runSkill = 0, int jumpSkill = 0, float burden = 0f)
|
||||
{
|
||||
_runSkill = runSkill;
|
||||
|
|
@ -26,74 +64,91 @@ public sealed class PlayerWeenie : IWeenieObject
|
|||
_jumpSkill = jumpSkill;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the retail <c>InqLoad</c>-equivalent burden/capacity ratio
|
||||
/// (0.0 unencumbered .. ~3.0 severely overloaded). Runtime computes this
|
||||
/// from Strength + augmentation property 0xE6 + EncumbranceVal property
|
||||
/// 5 (the same inputs <c>IndicatorBarController.UpdateBurden</c> already
|
||||
/// assembles) and pushes it — see the pseudocode doc §9.
|
||||
/// </summary>
|
||||
public void SetBurden(float burden) => _burden = burden;
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the current-stamina reading feeding the zero-skill gate in
|
||||
/// <see cref="InqRunRate"/>/<see cref="InqJumpVelocity"/>. Pass
|
||||
/// <c>null</c> to return to "unknown, don't gate" (matches construction
|
||||
/// default).
|
||||
/// </summary>
|
||||
public void SetStamina(uint? currentStamina) => _currentStamina = currentStamina;
|
||||
|
||||
public bool InqRunRate(out float rate)
|
||||
{
|
||||
rate = GetRunRate(_burden, _runSkill);
|
||||
int effectiveSkill = _currentStamina == 0 ? 0 : _runSkill;
|
||||
rate = MovementSystem.GetRunRate(_burden, effectiveSkill);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool InqJumpVelocity(float extent, out float vz)
|
||||
{
|
||||
float height = GetJumpHeight(_burden, _jumpSkill, extent);
|
||||
int effectiveSkill = _currentStamina == 0 ? 0 : _jumpSkill;
|
||||
float height = MovementSystem.GetJumpHeight(_burden, effectiveSkill, extent);
|
||||
vz = MathF.Sqrt(height * 19.6f);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CanJump(float extent) => true; // burden/stamina checks deferred
|
||||
/// <summary>
|
||||
/// Retail <c>CACQualities::CanJump</c> (0x00591b50): refuses only past
|
||||
/// <see cref="CanJumpLoadThreshold"/> (200% load) — see the class doc's
|
||||
/// UN-8 note. TS-5 retired: this was previously an unconditional
|
||||
/// <c>true</c>.
|
||||
/// </summary>
|
||||
public bool CanJump(float extent) => _burden < CanJumpLoadThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// R3-W3 (W0-pins.md A3): the local player's weenie is THE player.
|
||||
/// Feeds W4's <c>apply_current_movement</c>/<c>ReportExhaustion</c>
|
||||
/// dual-dispatch gate — no consumer yet in W3.
|
||||
/// dual-dispatch gate.
|
||||
/// </summary>
|
||||
public bool IsThePlayer() => true;
|
||||
|
||||
/// <summary>
|
||||
/// TS-5 (extended): stamina cost gating deferred pending stat plumbing —
|
||||
/// always affordable, cost 0. Matches <see cref="CanJump"/>'s existing
|
||||
/// always-true stance.
|
||||
/// Retail <c>CACQualities::JumpStaminaCost</c> (0x00591b90, pc 412949,
|
||||
/// FULLY READABLE): computes the real cost via
|
||||
/// <see cref="MovementSystem.JumpStaminaCost"/> and returns <c>true</c>
|
||||
/// unconditionally (once burden is knowable, which it always is for the
|
||||
/// local player) — retail's own function never exercises the "can't
|
||||
/// afford" false path; see the pseudocode doc §4/§7. TS-5 retired: this
|
||||
/// was previously a zero-cost stub. <c>pk</c> is hardcoded <c>false</c>
|
||||
/// pending TS-23 (PlayerKillerStatus parsing, Campaign P Slice P3).
|
||||
/// </summary>
|
||||
public bool JumpStaminaCost(float extent, out int cost)
|
||||
{
|
||||
cost = 0;
|
||||
cost = MovementSystem.JumpStaminaCost(extent, _burden, pk: false);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RunRate = (burdenMod * (runSkill / (runSkill + 200)) * 11 + 4) / 4
|
||||
/// Capped at 4.5 when runSkill >= 800.
|
||||
/// Source: decompiled + ACE MovementSystem.GetRunRate
|
||||
/// RunRate = (burdenMod * (runSkill / (runSkill + 200)) * 11 + 4) / 4.
|
||||
/// Capped at 4.5 when runSkill >= 800. Thin forwarder to
|
||||
/// <see cref="MovementSystem.GetRunRate"/> — kept for source
|
||||
/// compatibility with existing golden-value tests.
|
||||
/// </summary>
|
||||
public static float GetRunRate(float burden, int runSkill)
|
||||
{
|
||||
if (runSkill >= 800) return 18f / 4f;
|
||||
float loadMod = GetBurdenMod(burden);
|
||||
return (loadMod * ((float)runSkill / (runSkill + 200) * 11f) + 4f) / 4f;
|
||||
}
|
||||
public static float GetRunRate(float burden, int runSkill) =>
|
||||
MovementSystem.GetRunRate(burden, runSkill);
|
||||
|
||||
/// <summary>
|
||||
/// JumpHeight = burdenMod * (jumpSkill / (jumpSkill + 1300) * 22.2 + 0.05) * extent
|
||||
/// Clamped to minimum 0.35m.
|
||||
/// Source: decompiled + ACE MovementSystem.GetJumpHeight
|
||||
/// JumpHeight = burdenMod * (jumpSkill / (jumpSkill + 1300) * 22.2 + 0.05) * extent,
|
||||
/// clamped to minimum 0.35m. Thin forwarder to
|
||||
/// <see cref="MovementSystem.GetJumpHeight"/> — kept for source
|
||||
/// compatibility with existing golden-value tests.
|
||||
/// </summary>
|
||||
public static float GetJumpHeight(float burden, int jumpSkill, float extent)
|
||||
{
|
||||
extent = Math.Clamp(extent, 0f, 1f);
|
||||
float loadMod = GetBurdenMod(burden);
|
||||
float height = loadMod * ((float)jumpSkill / (jumpSkill + 1300f) * 22.2f + 0.05f) * extent;
|
||||
return MathF.Max(height, 0.35f);
|
||||
}
|
||||
public static float GetJumpHeight(float burden, int jumpSkill, float extent) =>
|
||||
MovementSystem.GetJumpHeight(burden, jumpSkill, extent);
|
||||
|
||||
/// <summary>
|
||||
/// Encumbrance modifier: 1.0 when unloaded, linearly decreasing to 0 at 200%.
|
||||
/// Source: decompiled + ACE EncumbranceSystem.GetBurdenMod
|
||||
/// Encumbrance modifier: 1.0 when unloaded, linearly decreasing to 0 at
|
||||
/// 200%. Thin forwarder to <see cref="EncumbranceSystem.LoadMod"/> —
|
||||
/// kept for source compatibility with existing golden-value tests.
|
||||
/// </summary>
|
||||
public static float GetBurdenMod(float burden)
|
||||
{
|
||||
if (burden < 1f) return 1f;
|
||||
if (burden < 2f) return 2f - burden;
|
||||
return 0f;
|
||||
}
|
||||
public static float GetBurdenMod(float burden) => EncumbranceSystem.LoadMod(burden);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue