feat(R3-W3): verbatim jump family — charge gates, jump_is_allowed chain, epsilon fixes; StandingLongJump misattribution retired (closes J5, J6, J7-interp-side, J16-epsilons)
JumpChargeIsAllowed 0x00527a50 (CanJump→0x49 before posture; Fallen + Crouch..Sleeping→0x48), ChargeJump 0x005281c0 — now the ONLY place standing_longjump arms (grounded Contact+OnWalkable + forward Ready + no sidestep/turn, raw @305453-305466); the S2a-flagged side effect inside contact_allows_move is DELETED (J6) with regression pins that contact checks never touch the flag (the funnel's StandingLongJump branch is local-player scope; the controller wires ChargeJump in W4/W6 — remotes never charge client-side, no interim regression). jump_is_allowed 0x005282b0 full chain replaces the 15-line approximation: IsFullyConstrained→0x47 (new PhysicsBody stub, TS-35) BEFORE the pending-head peek (A2 ordering, pinned); head peek fires whenever the queue is non-empty (no ACE Count>1 gate; nonzero jump_error_code short-circuits the whole chain — test proves JumpStaminaCost is never consulted); retail entry shape verbatim (non-creature-weenie and gravity-off skip the ground gate; null physics obj → 0x24 NOT 8 per A10); charge → MotionAllowsJump(fwd) double-check → JumpStaminaCost gate (new IWeenieObject member, always-affordable PlayerWeenie stub — TS-5 extended). GetJumpVZ/GetLeaveGroundVelocity: epsilon corrected to retail's 0.000199999995f (was 0.001 — regression-pinned at extent 0.0005); A5 shape (clamp 1.0, weenie-null 10.0f); A6 momentum fallback fires only when ALL THREE components are sub-epsilon and overwrites all three with global→local(m_velocityVector). Jump 0x00528780: InterruptCurrentMovement no-op seam (TS-36, →R4 cancel_moveto), fires unconditionally; standing_longjump cleared ONLY on failure (success keeps it — pinned). IWeenieObject.IsThePlayer() (PlayerWeenie true) lands for W4's A3 dispatch. 51 new tests + 1 re-pin; superseded pre-W3 jump tests removed. Full suite: 3,623 passed. Registers: TS-5 extended, TS-35/36/37 added. Implemented by a dedicated agent against the W0-pinned spec; arming gates + entry shape verified against the quoted raw text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
371679915e
commit
af4764443f
7 changed files with 1383 additions and 352 deletions
|
|
@ -423,220 +423,20 @@ public sealed class MotionInterpreterTests
|
|||
}
|
||||
|
||||
// =========================================================================
|
||||
// jump (FUN_00529390)
|
||||
// jump (FUN_00529390) / get_jump_v_z (FUN_005286b0) /
|
||||
// get_leave_ground_velocity (FUN_00528cd0) / jump_is_allowed (FUN_00528ec0)
|
||||
//
|
||||
// R3-W3 (closes J5, J6, J7-interp-side, J16-epsilons): the full jump
|
||||
// family (jump/GetJumpVZ/GetLeaveGroundVelocity/jump_is_allowed) plus
|
||||
// jump_charge_is_allowed/ChargeJump now live in
|
||||
// MotionInterpreterJumpFamilyTests.cs, ported against the verbatim
|
||||
// decomp (r3-motioninterp-decomp.md §3a-3h) and W0-pins.md (A2/A5/A6/A10)
|
||||
// instead of this file's pre-W3 approximation-era pins (wrong 0.001
|
||||
// epsilon, jump_is_allowed(extent, int) shape, no IsFullyConstrained /
|
||||
// pending-head-peek / stamina chain). See that file for the full gate
|
||||
// tables and epsilon-boundary pins.
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void Jump_Grounded_SetsJumpExtentAndLeavesWalkable()
|
||||
{
|
||||
var body = MakeGrounded();
|
||||
var interp = MakeInterp(body);
|
||||
|
||||
var result = interp.jump(0.5f);
|
||||
|
||||
Assert.Equal(WeenieError.None, result);
|
||||
Assert.Equal(0.5f, interp.JumpExtent, precision: 5);
|
||||
Assert.False(body.OnWalkable, "Body should no longer be on walkable after jump");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jump_Airborne_ReturnsNotGrounded()
|
||||
{
|
||||
// R3-W1 renumber (A10): airborne is 0x24 (NotGrounded), not 0x48
|
||||
// (0x48 is now reserved for the motion_allows_jump blocklist).
|
||||
var body = MakeAirborne();
|
||||
var interp = MakeInterp(body);
|
||||
|
||||
var result = interp.jump(0.5f);
|
||||
|
||||
Assert.Equal(WeenieError.NotGrounded, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jump_WeenieBlocksJump_ClearStandingLongJump()
|
||||
{
|
||||
var weenie = new FakeWeenie { CanJumpResult = false };
|
||||
var body = MakeGrounded();
|
||||
var interp = MakeInterp(body, weenie);
|
||||
interp.StandingLongJump = true;
|
||||
|
||||
var result = interp.jump(0.5f);
|
||||
|
||||
Assert.Equal(WeenieError.CantJumpLoadedDown, result);
|
||||
Assert.False(interp.StandingLongJump);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jump_NullPhysicsObj_ReturnsNoPhysicsObject()
|
||||
{
|
||||
var interp = new MotionInterpreter();
|
||||
|
||||
var result = interp.jump(0.5f);
|
||||
|
||||
Assert.Equal(WeenieError.NoPhysicsObject, result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// get_jump_v_z (FUN_005286b0)
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void GetJumpVz_ZeroExtent_ReturnsZero()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
interp.JumpExtent = 0f;
|
||||
|
||||
Assert.Equal(0f, interp.get_jump_v_z(), precision: 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetJumpVz_NoWeenie_ReturnsDefault()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
interp.JumpExtent = 0.5f;
|
||||
|
||||
Assert.Equal(MotionInterpreter.DefaultJumpVz, interp.get_jump_v_z(), precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetJumpVz_WithWeenie_DelegatesToInqJumpVelocity()
|
||||
{
|
||||
var weenie = new FakeWeenie { JumpVz = 8.0f };
|
||||
var interp = MakeInterp(weenie: weenie);
|
||||
interp.JumpExtent = 1.0f;
|
||||
|
||||
float vz = interp.get_jump_v_z();
|
||||
|
||||
// FakeWeenie returns JumpVz * extent = 8.0 * 1.0 = 8.0
|
||||
Assert.Equal(8.0f, vz, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetJumpVz_ExtentClampsAt1()
|
||||
{
|
||||
var weenie = new FakeWeenie { JumpVz = 10.0f };
|
||||
var interp = MakeInterp(weenie: weenie);
|
||||
interp.JumpExtent = 5.0f; // over-clamped
|
||||
|
||||
float vz = interp.get_jump_v_z();
|
||||
|
||||
// Should clamp extent to 1.0: FakeWeenie returns 10.0 * 1.0 = 10.0
|
||||
Assert.Equal(10.0f, vz, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetJumpVz_WeenieReturnsFailure_ReturnsZero()
|
||||
{
|
||||
var weenie = new FakeWeenie { InqJumpVelocityResult = false };
|
||||
var interp = MakeInterp(weenie: weenie);
|
||||
interp.JumpExtent = 0.5f;
|
||||
|
||||
Assert.Equal(0f, interp.get_jump_v_z(), precision: 5);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// get_leave_ground_velocity (FUN_00528cd0)
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void GetLeaveGroundVelocity_WalkingForward_HasPositiveYAndZ()
|
||||
{
|
||||
var weenie = new FakeWeenie { JumpVz = 10.0f };
|
||||
var body = MakeGrounded();
|
||||
var interp = new MotionInterpreter(body, weenie)
|
||||
{
|
||||
JumpExtent = 1.0f,
|
||||
};
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward;
|
||||
interp.InterpretedState.ForwardSpeed = 1.0f;
|
||||
|
||||
var vel = interp.get_leave_ground_velocity();
|
||||
|
||||
Assert.True(vel.Y > 0f, "Y velocity should be positive when walking forward");
|
||||
Assert.True(vel.Z > 0f, "Z (jump) velocity should be positive");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLeaveGroundVelocity_Standing_ZeroExtent_ReturnsZeroXY()
|
||||
{
|
||||
var body = MakeGrounded();
|
||||
var interp = new MotionInterpreter(body)
|
||||
{
|
||||
JumpExtent = 0f, // below epsilon
|
||||
};
|
||||
// Default interpreted state = Ready (no walk/run command)
|
||||
|
||||
var vel = interp.get_leave_ground_velocity();
|
||||
|
||||
Assert.Equal(0f, vel.X, precision: 5);
|
||||
Assert.Equal(0f, vel.Y, precision: 5);
|
||||
Assert.Equal(0f, vel.Z, precision: 5);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// jump_is_allowed (FUN_00528ec0)
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void JumpIsAllowed_Grounded_ReturnsNone()
|
||||
{
|
||||
var interp = MakeInterp(MakeGrounded());
|
||||
|
||||
var result = interp.jump_is_allowed(0.5f, 0);
|
||||
|
||||
Assert.Equal(WeenieError.None, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JumpIsAllowed_Airborne_ReturnsNotGrounded()
|
||||
{
|
||||
var interp = MakeInterp(MakeAirborne());
|
||||
|
||||
var result = interp.jump_is_allowed(0.5f, 0);
|
||||
|
||||
Assert.Equal(WeenieError.NotGrounded, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JumpIsAllowed_WeenieBlocks_ReturnsCantJumpLoadedDown()
|
||||
{
|
||||
var weenie = new FakeWeenie { CanJumpResult = false };
|
||||
var interp = MakeInterp(MakeGrounded(), weenie);
|
||||
|
||||
var result = interp.jump_is_allowed(0.5f, 0);
|
||||
|
||||
Assert.Equal(WeenieError.CantJumpLoadedDown, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JumpIsAllowed_NoGravityFlag_ReturnsNotGrounded()
|
||||
{
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
State = PhysicsStateFlags.None, // no gravity
|
||||
TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
|
||||
};
|
||||
var interp = MakeInterp(body);
|
||||
|
||||
// No gravity → must be airborne-style
|
||||
var result = interp.jump_is_allowed(0.5f, 0);
|
||||
|
||||
Assert.Equal(WeenieError.NotGrounded, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JumpIsAllowed_NullPhysicsObj_ReturnsNotGrounded()
|
||||
{
|
||||
// A10 note: jump_is_allowed returns 0x24 (NotGrounded), NOT 8, when
|
||||
// physics_obj == null — the "8 = no physics obj" convention that
|
||||
// holds everywhere else in CMotionInterp does not hold here.
|
||||
var interp = new MotionInterpreter();
|
||||
|
||||
var result = interp.jump_is_allowed(0.5f, 0);
|
||||
|
||||
Assert.Equal(WeenieError.NotGrounded, result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// contact_allows_move (FUN_00528dd0)
|
||||
// =========================================================================
|
||||
|
|
@ -741,8 +541,15 @@ public sealed class MotionInterpreterTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ContactAllowsMove_GroundedAndIdle_SetsStandingLongJump()
|
||||
public void ContactAllowsMove_GroundedAndIdle_DoesNotSetStandingLongJump()
|
||||
{
|
||||
// R3-W3 (closes J6): the S2a-flagged misattribution is DELETED.
|
||||
// Retail arms StandingLongJump ONLY in charge_jump (0x005281c0) —
|
||||
// contact_allows_move (0x00528240) never reads or writes it. See
|
||||
// MotionInterpreterJumpFamilyTests.ContactAllowsMove_GroundedAndIdle_DoesNotArmStandingLongJump
|
||||
// for the full regression pin (both arm and no-clear directions)
|
||||
// and ChargeJump_GroundedIdle_ArmsStandingLongJump for where the
|
||||
// arming now actually happens.
|
||||
var body = MakeGrounded();
|
||||
var interp = MakeInterp(body);
|
||||
// All interpreted commands at default (Ready, no sidestep, no turn)
|
||||
|
|
@ -750,7 +557,7 @@ public sealed class MotionInterpreterTests
|
|||
|
||||
interp.contact_allows_move(MotionCommand.WalkForward);
|
||||
|
||||
Assert.True(interp.StandingLongJump, "Should set StandingLongJump when grounded and idle");
|
||||
Assert.False(interp.StandingLongJump, "contact_allows_move must never touch StandingLongJump (J6)");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue