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
98
src/AcDream.Core/Physics/MovementSystem.cs
Normal file
98
src/AcDream.Core/Physics/MovementSystem.cs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
using System;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>MovementSystem</c> (named-retail decomp pc 695958+): the pure
|
||||
/// formula layer <c>PlayerWeenie</c> (the CACQualities-shaped composition)
|
||||
/// calls once burden, skill, and stamina inputs are assembled. See
|
||||
/// <c>docs/research/2026-07-30-stat-coupled-movement-pseudocode.md</c> §6.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>x87-mush disclosure:</b> <see cref="GetRunRate"/>'s general-case
|
||||
/// arithmetic and <see cref="JumpStaminaCost"/>'s <c>pk!=0</c> branch are
|
||||
/// entirely dropped by the BN decompiler (not partially garbled — the
|
||||
/// operand expressions never survive translation, unlike the polarity-only
|
||||
/// ambiguities elsewhere in this port). Both are cross-referenced against
|
||||
/// <c>references/ACE/Source/ACE.Server/Physics/Animation/MovementSystem.cs</c>,
|
||||
/// which already matched this exact acdream port's PRE-P1 code
|
||||
/// (<c>PlayerWeenie.GetRunRate</c>/<c>GetJumpHeight</c> already cited "decompiled
|
||||
/// + ACE MovementSystem" before this slice) — no behavior change for the
|
||||
/// formulas already live; only the retail-named surface and the 3rd/4th
|
||||
/// <c>scaling</c> parameters (every known call site passes <c>1f</c>) are new.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class MovementSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// <c>MovementSystem::GetRunRate</c> 0x006b0950. Retail-verified 800-skill
|
||||
/// cap (<c>InqMaxRunRate</c> passes skill=9999 to reach this cap); general
|
||||
/// case ACE-cross-referenced (BN dropped the arithmetic, see class doc).
|
||||
/// </summary>
|
||||
public static float GetRunRate(float burden, int runSkill, float scaling = 1f)
|
||||
{
|
||||
if (runSkill >= 800)
|
||||
return 18f / 4f;
|
||||
|
||||
float loadMod = EncumbranceSystem.LoadMod(burden);
|
||||
return ((loadMod * ((float)runSkill / (runSkill + 200) * 11f) + 4f) / scaling) / 4f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>MovementSystem::GetJumpHeight</c> 0x006b09b0. Fully readable except
|
||||
/// the extent-clamp micro-branch (x87 mush, ACE's <c>Math.Clamp(power,0,1)</c>
|
||||
/// is the tiebreaker — matches the pre-P1 acdream port unchanged).
|
||||
/// </summary>
|
||||
public static float GetJumpHeight(
|
||||
float burden,
|
||||
int jumpSkill,
|
||||
float power,
|
||||
float scaling = 1f)
|
||||
{
|
||||
power = Math.Clamp(power, 0f, 1f);
|
||||
float loadMod = EncumbranceSystem.LoadMod(burden);
|
||||
float result = loadMod
|
||||
* ((float)jumpSkill / (jumpSkill + 1300f) * 22.2f + 0.05f)
|
||||
* power
|
||||
/ scaling;
|
||||
return result < 0.35f ? 0.35f : result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>MovementSystem::JumpStaminaCost</c> 0x006b0a40. The <c>pk==0</c>
|
||||
/// branch is fully readable: <c>ceil((load + 0.5) * power * 8 + 2)</c> —
|
||||
/// note <c>load</c> (not <c>power</c>) carries the <c>+0.5</c>; the
|
||||
/// campaign plan's shorthand had the operands swapped (see pseudocode
|
||||
/// doc §6). The <c>pk!=0</c> branch is entirely dropped by BN; ACE's
|
||||
/// <c>(power+1.0)*100.0</c> is the tiebreaker. <paramref name="pk"/> is
|
||||
/// hardcoded <c>false</c> at every P1 call site pending TS-23
|
||||
/// (PlayerKillerStatus parsing, Campaign P Slice P3) — ported here for
|
||||
/// signature completeness only.
|
||||
/// </summary>
|
||||
public static int JumpStaminaCost(float power, float burden, bool pk)
|
||||
{
|
||||
if (pk)
|
||||
return (int)((power + 1.0f) * 100.0f);
|
||||
|
||||
return (int)Math.Ceiling((burden + 0.5f) * power * 8f + 2f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>MovementSystem::GetJumpPower</c> — the algebraic inverse of
|
||||
/// <see cref="JumpStaminaCost"/>, solving for the extent affordable at a
|
||||
/// given stamina. Present in ACE (uncommented, live utility) but its
|
||||
/// retail call site is the charge-power-meter UI outside
|
||||
/// <c>CMotionInterp</c> (0x0056afac, out of R3/P1 scope — see
|
||||
/// <c>ChargeJump</c>'s doc comment in <c>MotionInterpreter.cs</c>). Not
|
||||
/// consumed by P1; ported for signature completeness and to leave the
|
||||
/// formula available for the charge-meter follow-up without a second
|
||||
/// decomp pass.
|
||||
/// </summary>
|
||||
public static float GetJumpPower(uint stamina, float burden, bool pk)
|
||||
{
|
||||
if (pk)
|
||||
return stamina / 100.0f - 1.0f;
|
||||
|
||||
return (stamina - 2.0f) / (burden * 8.0f + 4.0f);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue