Raw byte decode of MovementSystem::GetRunRate (0x006b0950, PDB-paired
binary): fild skill; fcom [800f]; fnstsw; test ah, 0x44; jp general —
the C2/C3 parity idiom whose 18/4 fall-through executes ONLY at
skill == 800 exactly. ACE read this as >= 800 ('max run speed?') and
Campaign P P1 inherited that misread when BN dropped the arithmetic,
flat-lining every maxed character at 4.5 (retail-true ~3.70, +21%) and
erasing the vitae differential (both 10200 and 15225 sat above 800).
The [stat-chain] live capture proved the enchant chain correct end to
end (vitae 0.67 -> eff run 10200 -> controller), isolating the formula.
General path byte-verified: (loadMod*(skill/(skill+200)*11)+4)/scaling/4.
InqMaxRunRate's skill=9999 probe gets ~3.6961, not 4.5.
Golden tests pin the 799/800/801 straddle and the maxed-skill vitae
differential; pseudocode doc §6 carries the decode plus a do-not-
reimport-ACE warning. Complete Release suite: 10,025 passed / 5 skips.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
149 lines
5.7 KiB
C#
149 lines
5.7 KiB
C#
using System;
|
|
using AcDream.Core.Physics;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Physics;
|
|
|
|
/// <summary>
|
|
/// Golden-value tables for retail <c>MovementSystem</c>
|
|
/// (docs/research/2026-07-30-stat-coupled-movement-pseudocode.md §6),
|
|
/// Campaign P Slice P1.
|
|
/// </summary>
|
|
public class MovementSystemTests
|
|
{
|
|
[Fact]
|
|
public void GetRunRate_Skill800Sentinel_IsExactEqualityOnly()
|
|
{
|
|
// Retail 0x006b0950 byte decode (#266): fcom [800f]; test ah, 0x44;
|
|
// jp — the 18/4 return executes ONLY when skill == 800 exactly.
|
|
// 799 and 801 take the general formula on either side of it.
|
|
Assert.Equal(4.5f, MovementSystem.GetRunRate(0f, 800), precision: 5);
|
|
Assert.Equal(3.1994f, MovementSystem.GetRunRate(0f, 799), precision: 3);
|
|
Assert.Equal(3.2005f, MovementSystem.GetRunRate(0f, 801), precision: 3);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetRunRate_MaxedSkills_UseGeneralFormula_VitaeDifferentiates()
|
|
{
|
|
// #266 regression: +Acdream at 33% vitae (eff 10200) vs +Je at 5%
|
|
// vitae (eff 14463) — retail runs them within ~0.4% of each other
|
|
// (~3.70 vs ~3.71), NOT at a flat 4.5. The old ">= 800 cap"
|
|
// misread made both saturate identically and erased vitae entirely.
|
|
// Tolerance asserts (not precision:) — 3.7125 straddles a 3-decimal
|
|
// rounding boundary, the same xunit footgun as the AP-7 case.
|
|
Assert.True(MathF.Abs(MovementSystem.GetRunRate(0f, 10200) - 3.6971f) < 1e-3f);
|
|
Assert.True(MathF.Abs(MovementSystem.GetRunRate(0f, 14463) - 3.7125f) < 1e-3f);
|
|
// InqMaxRunRate's skill=9999 probe: general formula, not 4.5.
|
|
Assert.True(MathF.Abs(MovementSystem.GetRunRate(0f, 9999) - 3.6961f) < 1e-3f);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetRunRate_Skill200_MatchesFormula()
|
|
{
|
|
// (1.0 * (200/400 * 11) + 4) / 4 = (5.5 + 4) / 4 = 2.375
|
|
Assert.Equal(2.375f, MovementSystem.GetRunRate(0f, 200), precision: 3);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetRunRate_Skill0_ReturnsBase()
|
|
{
|
|
Assert.Equal(1.0f, MovementSystem.GetRunRate(0f, 0), precision: 3);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0f, 1f)] // unencumbered
|
|
[InlineData(0.99f, 1f)] // just under 100%
|
|
[InlineData(1.0f, 1f)] // exactly 100% — still full effectiveness
|
|
[InlineData(1.5f, 0.5f)] // knee: halfway to 200%
|
|
[InlineData(2.0f, 0f)] // knee floor
|
|
[InlineData(3.0f, 0f)] // fully overloaded
|
|
public void GetRunRate_LoadKnees_ScaleLinearly(float burden, float expectedLoadMod)
|
|
{
|
|
// At runSkill=200: rate = (loadMod * 5.5 + 4) / 4.
|
|
float expected = (expectedLoadMod * 5.5f + 4f) / 4f;
|
|
Assert.Equal(expected, MovementSystem.GetRunRate(burden, 200), precision: 3);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetJumpHeight_FullExtent_Skill100_MatchesFormula()
|
|
{
|
|
// height = 1.0 * (100/1400 * 22.2 + 0.05) * 1.0 = 1.636...
|
|
float expected = (100f / 1400f * 22.2f + 0.05f);
|
|
Assert.Equal(expected, MovementSystem.GetJumpHeight(0f, 100, 1.0f), precision: 3);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0f)]
|
|
[InlineData(0.5f)]
|
|
[InlineData(1.0f)]
|
|
public void GetJumpHeight_ExtentScalesLinearly(float extent)
|
|
{
|
|
float unscaled = 100f / 1400f * 22.2f + 0.05f;
|
|
float expected = System.Math.Max(unscaled * extent, 0.35f);
|
|
Assert.Equal(expected, MovementSystem.GetJumpHeight(0f, 100, extent), precision: 3);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetJumpHeight_ClampsExtentAbove1()
|
|
{
|
|
Assert.Equal(
|
|
MovementSystem.GetJumpHeight(0f, 100, 1.0f),
|
|
MovementSystem.GetJumpHeight(0f, 100, 5.0f),
|
|
precision: 5);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetJumpHeight_ClampsExtentBelow0()
|
|
{
|
|
Assert.Equal(0.35f, MovementSystem.GetJumpHeight(0f, 100, -3.0f), precision: 5);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetJumpHeight_ZeroSkill_FloorsAt0Point35()
|
|
{
|
|
Assert.Equal(0.35f, MovementSystem.GetJumpHeight(0f, 0, 1.0f), precision: 5);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0f)]
|
|
[InlineData(1.0f)]
|
|
[InlineData(1.5f)]
|
|
[InlineData(2.0f)]
|
|
[InlineData(3.0f)]
|
|
public void GetJumpHeight_AtHeavyLoad_NeverGoesBelowFloor(float burden)
|
|
{
|
|
Assert.True(MovementSystem.GetJumpHeight(burden, 100, 1.0f) >= 0.35f);
|
|
}
|
|
|
|
[Theory]
|
|
// ceil((load + 0.5) * power * 8 + 2) — verbatim decomp, pc 696014-696024.
|
|
[InlineData(0f, 0f, 2)] // ceil((0+0.5)*0*8+2) = ceil(2) = 2
|
|
[InlineData(0f, 1f, 6)] // ceil((0+0.5)*1*8+2) = ceil(4+2) = 6
|
|
[InlineData(1f, 1f, 14)] // ceil((1+0.5)*1*8+2) = ceil(12+2) = 14
|
|
[InlineData(2f, 1f, 22)] // ceil((2+0.5)*1*8+2) = ceil(20+2) = 22
|
|
[InlineData(0.3f, 0.7f, 7)] // ceil((0.3+0.5)*0.7*8+2) = ceil(4.48+2) = ceil(6.48) = 7
|
|
public void JumpStaminaCost_NonPk_CeilsCorrectly(float burden, float power, int expected)
|
|
{
|
|
Assert.Equal(expected, MovementSystem.JumpStaminaCost(power, burden, pk: false));
|
|
}
|
|
|
|
[Fact]
|
|
public void JumpStaminaCost_Pk_UsesAceTiebreakerFormula()
|
|
{
|
|
// ACE tiebreaker (pk!=0 branch entirely dropped by BN): (power+1.0)*100.0
|
|
Assert.Equal(150, MovementSystem.JumpStaminaCost(0.5f, burden: 1f, pk: true));
|
|
Assert.Equal(100, MovementSystem.JumpStaminaCost(0f, burden: 1f, pk: true));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetJumpPower_IsAlgebraicInverseOfJumpStaminaCost()
|
|
{
|
|
// Not consumed by P1 (ported for signature completeness); sanity-check
|
|
// the inverse relationship still holds for the non-pk formula shape.
|
|
float burden = 1f;
|
|
uint stamina = 20u;
|
|
float power = MovementSystem.GetJumpPower(stamina, burden, pk: false);
|
|
// (stamina - 2) / (burden*8 + 4) = 18 / 12 = 1.5
|
|
Assert.Equal(1.5f, power, precision: 3);
|
|
}
|
|
}
|