using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using Xunit;
namespace AcDream.Core.Tests.Physics;
// ─────────────────────────────────────────────────────────────────────────────
// MotionInterpreterJumpFamilyTests — R3-W3 (closes J5, J6, J7-interp-side,
// J16-epsilons). Pins per docs/research/2026-07-02-r3-motioninterp/W0-pins.md
// (A1, A2, A5, A6, A10) and r3-motioninterp-decomp.md §3a-3h.
//
// Source addresses tested:
// FUN_00527a50 (0x00527a50) jump_charge_is_allowed @304935
// FUN_005281c0 (0x005281c0) charge_jump @305448
// FUN_00527aa0 (0x00527aa0) get_jump_v_z @304953
// FUN_005280c0 (0x005280c0) get_leave_ground_velocity @305404
// FUN_005282b0 (0x005282b0) jump_is_allowed (full chain)
// FUN_00528780 (0x00528780) jump @305792
// ─────────────────────────────────────────────────────────────────────────────
/// Fake WeenieObject for jump-family test isolation.
file sealed class FakeWeenie : IWeenieObject
{
public float RunRate = 1.0f;
public float JumpVz = 10.0f;
public bool CanJumpResult = true;
public bool InqRunRateResult = true;
public bool InqJumpVelocityResult = true;
/// Controls the JumpStaminaCost virtual (vtable +0x44 per the
/// raw 305549-305556 shape): true = affordable (pass), false = 0x47.
public bool JumpStaminaCostResult = true;
public int JumpStaminaCostCalls;
public bool InqJumpVelocity(float extent, out float vz)
{
vz = JumpVz * extent;
return InqJumpVelocityResult;
}
public bool InqRunRate(out float rate)
{
rate = RunRate;
return InqRunRateResult;
}
public bool CanJump(float extent) => CanJumpResult;
public bool JumpStaminaCost(float extent, out int cost)
{
JumpStaminaCostCalls++;
cost = 0;
return JumpStaminaCostResult;
}
}
public sealed class MotionInterpreterJumpFamilyTests
{
// ── helpers ───────────────────────────────────────────────────────────────
private static PhysicsBody MakeGrounded()
{
var body = new PhysicsBody
{
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
};
body.TransientState = TransientStateFlags.Contact
| TransientStateFlags.OnWalkable
| TransientStateFlags.Active;
return body;
}
private static PhysicsBody MakeAirborne()
{
var body = new PhysicsBody
{
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
};
body.TransientState = TransientStateFlags.Active;
return body;
}
private static MotionInterpreter MakeInterp(PhysicsBody? body = null, IWeenieObject? weenie = null)
{
body ??= MakeGrounded();
return new MotionInterpreter(body, weenie);
}
// =========================================================================
// jump_charge_is_allowed — 0x00527a50 @304935
//
// Raw (r3-motioninterp-decomp.md §3b):
// weenie_obj = this->weenie_obj;
// if (weenie_obj != 0 && weenie_obj->vtable->CanJump(this->jump_extent) == 0)
// return 0x49;
// forward_command = this->interpreted_state.forward_command;
// if (forward_command != 0x40000008
// && (forward_command <= 0x41000011 || forward_command > 0x41000014))
// return 0;
// return 0x48;
// =========================================================================
[Fact]
public void JumpChargeIsAllowed_NoWeenie_ReadyForward_ReturnsNone()
{
var interp = MakeInterp();
interp.InterpretedState.ForwardCommand = MotionCommand.Ready;
var result = interp.JumpChargeIsAllowed(0.5f);
Assert.Equal(WeenieError.None, result);
}
[Fact]
public void JumpChargeIsAllowed_WeenieCanJumpFalse_ReturnsCantJumpLoadedDown()
{
var weenie = new FakeWeenie { CanJumpResult = false };
var interp = MakeInterp(weenie: weenie);
interp.InterpretedState.ForwardCommand = MotionCommand.Ready;
var result = interp.JumpChargeIsAllowed(0.5f);
Assert.Equal(WeenieError.CantJumpLoadedDown, result);
}
[Fact]
public void JumpChargeIsAllowed_Fallen_ReturnsYouCantJumpFromThisPosition()
{
var interp = MakeInterp();
interp.InterpretedState.ForwardCommand = MotionCommand.Fallen; // 0x40000008
var result = interp.JumpChargeIsAllowed(0.5f);
Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result);
}
[Theory]
[InlineData(MotionCommand.Crouch)] // 0x41000012
[InlineData(MotionCommand.Sitting)] // 0x41000013
[InlineData(MotionCommand.Sleeping)] // 0x41000014
public void JumpChargeIsAllowed_CrouchSitSleepRange_ReturnsYouCantJumpFromThisPosition(uint forward)
{
var interp = MakeInterp();
interp.InterpretedState.ForwardCommand = forward;
var result = interp.JumpChargeIsAllowed(0.5f);
Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result);
}
[Fact]
public void JumpChargeIsAllowed_CrouchLowerBoundExact_Passes()
{
// forward_command <= 0x41000011 passes (the gate is a strict
// inequality on the LOWER bound: forward_command > 0x41000011 is
// required to even consider the block). 0x41000011 == CrouchLowerBound
// itself must PASS (not blocked) — verbatim boundary.
var interp = MakeInterp();
interp.InterpretedState.ForwardCommand = MotionCommand.CrouchLowerBound; // 0x41000011
var result = interp.JumpChargeIsAllowed(0.5f);
Assert.Equal(WeenieError.None, result);
}
[Fact]
public void JumpChargeIsAllowed_SleepUpperBoundExact_Blocked()
{
// forward_command > 0x41000014 passes; == 0x41000014 (Sleeping) is
// still inside the blocked range (already covered above), but this
// pins the boundary explicitly: 0x41000015 (one past Sleeping) must
// PASS.
var interp = MakeInterp();
interp.InterpretedState.ForwardCommand = MotionCommand.CrouchUpperExclusive; // 0x41000015
var result = interp.JumpChargeIsAllowed(0.5f);
Assert.Equal(WeenieError.None, result);
}
[Fact]
public void JumpChargeIsAllowed_Falling_Passes()
{
// Falling (0x40000015) is NOT Fallen (0x40000008) — must pass this
// gate (though jump_is_allowed's separate ground check blocks
// mid-air jumps by a different mechanism — A1 adjudication).
var interp = MakeInterp();
interp.InterpretedState.ForwardCommand = MotionCommand.Falling;
var result = interp.JumpChargeIsAllowed(0.5f);
Assert.Equal(WeenieError.None, result);
}
[Fact]
public void JumpChargeIsAllowed_CanJumpCheckedBeforePostureCheck()
{
// Raw order: CanJump gate FIRST, forward_command gate SECOND. A
// weenie that refuses CanJump returns 0x49 even in a posture that
// would otherwise pass.
var weenie = new FakeWeenie { CanJumpResult = false };
var interp = MakeInterp(weenie: weenie);
interp.InterpretedState.ForwardCommand = MotionCommand.Falling; // would pass posture gate
var result = interp.JumpChargeIsAllowed(0.5f);
Assert.Equal(WeenieError.CantJumpLoadedDown, result);
}
// =========================================================================
// charge_jump — 0x005281c0 @305448 (closes J6)
//
// Raw (r3-motioninterp-decomp.md §3e):
// weenie_obj = this->weenie_obj;
// if (weenie_obj != 0 && weenie_obj->vtable->CanJump(this->jump_extent) == 0)
// return 0x49;
// forward_command = this->interpreted_state.forward_command;
// if (forward_command == 0x40000008
// || (forward_command > 0x41000011 && forward_command <= 0x41000014))
// return 0x48;
// transient_state = physics_obj->transient_state;
// if ((transient_state & 1) != 0 && (transient_state & 2) != 0
// && forward_command == 0x41000003
// && interpreted_state.sidestep_command == 0
// && interpreted_state.turn_command == 0)
// standing_longjump = 1;
// return 0;
// =========================================================================
[Fact]
public void ChargeJump_GroundedIdle_ArmsStandingLongJump()
{
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.StandingLongJump = false;
// Default interpreted state: ForwardCommand=Ready, SideStep=0, Turn=0.
var result = interp.ChargeJump();
Assert.Equal(WeenieError.None, result);
Assert.True(interp.StandingLongJump, "grounded + idle must arm StandingLongJump");
}
[Fact]
public void ChargeJump_GroundedButMoving_DoesNotArmStandingLongJump()
{
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.StandingLongJump = false;
interp.InterpretedState.ForwardCommand = MotionCommand.RunForward;
var result = interp.ChargeJump();
Assert.Equal(WeenieError.None, result);
Assert.False(interp.StandingLongJump, "must not arm while moving forward");
}
[Fact]
public void ChargeJump_GroundedIdleButSidestepping_DoesNotArmStandingLongJump()
{
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.StandingLongJump = false;
interp.InterpretedState.ForwardCommand = MotionCommand.Ready;
interp.InterpretedState.SideStepCommand = MotionCommand.SideStepRight;
var result = interp.ChargeJump();
Assert.Equal(WeenieError.None, result);
Assert.False(interp.StandingLongJump, "must not arm while sidestepping");
}
[Fact]
public void ChargeJump_GroundedIdleButTurning_DoesNotArmStandingLongJump()
{
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.StandingLongJump = false;
interp.InterpretedState.ForwardCommand = MotionCommand.Ready;
interp.InterpretedState.TurnCommand = MotionCommand.TurnRight;
var result = interp.ChargeJump();
Assert.Equal(WeenieError.None, result);
Assert.False(interp.StandingLongJump, "must not arm while turning");
}
[Fact]
public void ChargeJump_Airborne_DoesNotArmStandingLongJump()
{
var body = MakeAirborne();
var interp = MakeInterp(body);
interp.StandingLongJump = false;
var result = interp.ChargeJump();
Assert.Equal(WeenieError.None, result);
Assert.False(interp.StandingLongJump, "must not arm while airborne (no Contact+OnWalkable)");
}
[Fact]
public void ChargeJump_WeenieBlocksCanJump_ReturnsCantJumpLoadedDown_NoArm()
{
var weenie = new FakeWeenie { CanJumpResult = false };
var body = MakeGrounded();
var interp = MakeInterp(body, weenie);
interp.StandingLongJump = false;
var result = interp.ChargeJump();
Assert.Equal(WeenieError.CantJumpLoadedDown, result);
Assert.False(interp.StandingLongJump);
}
[Fact]
public void ChargeJump_Fallen_ReturnsYouCantJumpFromThisPosition_NoArm()
{
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.InterpretedState.ForwardCommand = MotionCommand.Fallen;
interp.StandingLongJump = false;
var result = interp.ChargeJump();
Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result);
Assert.False(interp.StandingLongJump);
}
[Fact]
public void ChargeJump_CrouchRange_ReturnsYouCantJumpFromThisPosition_NoArm()
{
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.InterpretedState.ForwardCommand = MotionCommand.Sitting;
interp.StandingLongJump = false;
var result = interp.ChargeJump();
Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result);
Assert.False(interp.StandingLongJump);
}
// ── J6 regression pin: contact_allows_move no longer arms StandingLongJump ──
[Fact]
public void ContactAllowsMove_GroundedAndIdle_DoesNotArmStandingLongJump()
{
// J6: the S2a-flagged misattribution is DELETED. Only ChargeJump
// arms StandingLongJump now — contact_allows_move must have no side
// effect on this flag at all, in either direction.
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.StandingLongJump = false;
interp.contact_allows_move(MotionCommand.WalkForward);
Assert.False(interp.StandingLongJump,
"contact_allows_move must never arm StandingLongJump (J6 — moved to ChargeJump exclusively)");
}
[Fact]
public void ContactAllowsMove_GroundedAndIdle_DoesNotClearPreArmedStandingLongJump()
{
// contact_allows_move must not touch the flag at all — verify it
// doesn't clear a flag armed by a prior ChargeJump call either.
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.StandingLongJump = true;
interp.contact_allows_move(MotionCommand.WalkForward);
Assert.True(interp.StandingLongJump,
"contact_allows_move must not clear an externally-armed StandingLongJump flag");
}
// =========================================================================
// get_jump_v_z — 0x00527aa0 @304953 (A5; closes J16-epsilons)
// Epsilon: 0.000199999995f (NOT the old 0.001 JumpExtentEpsilon).
// =========================================================================
[Fact]
public void GetJumpVZ_EpsilonIsRetailLiteral()
{
Assert.Equal(0.000199999995f, MotionInterpreter.JumpVzEpsilon);
}
[Fact]
public void GetJumpVZ_JustBelowEpsilon_ReturnsZero()
{
var weenie = new FakeWeenie { JumpVz = 10.0f };
var interp = MakeInterp(weenie: weenie);
interp.JumpExtent = 0.0001f; // < 0.000199999995f
Assert.Equal(0f, interp.GetJumpVZ(), precision: 6);
}
[Fact]
public void GetJumpVZ_JustAboveEpsilon_DelegatesToWeenie()
{
var weenie = new FakeWeenie { JumpVz = 10.0f };
var interp = MakeInterp(weenie: weenie);
interp.JumpExtent = 0.0003f; // > 0.000199999995f
float vz = interp.GetJumpVZ();
// Not the zero-fallback: delegates to InqJumpVelocity(extent, ...).
Assert.Equal(weenie.JumpVz * 0.0003f, vz, precision: 4);
}
[Fact]
public void GetJumpVZ_ExactlyOldWrongEpsilon_0_001_IsAboveRetailEpsilon_NotZero()
{
// Regression pin: the OLD (wrong) epsilon was 0.001. At extent =
// 0.0005 the old code would have returned 0 (0.0005 < 0.001), but
// retail's epsilon (0.0002) means 0.0005 must NOT hit the zero path.
var weenie = new FakeWeenie { JumpVz = 10.0f };
var interp = MakeInterp(weenie: weenie);
interp.JumpExtent = 0.0005f;
float vz = interp.GetJumpVZ();
Assert.NotEqual(0f, vz);
Assert.Equal(weenie.JumpVz * 0.0005f, vz, precision: 4);
}
[Fact]
public void GetJumpVZ_NoWeenie_ReturnsDefault()
{
var interp = MakeInterp();
interp.JumpExtent = 0.5f;
Assert.Equal(MotionInterpreter.DefaultJumpVz, interp.GetJumpVZ(), precision: 4);
}
[Fact]
public void GetJumpVZ_ExtentClampsAtMax1()
{
var weenie = new FakeWeenie { JumpVz = 10.0f };
var interp = MakeInterp(weenie: weenie);
interp.JumpExtent = 5.0f; // over-clamped
float vz = interp.GetJumpVZ();
Assert.Equal(10.0f, vz, precision: 4);
}
[Fact]
public void GetJumpVZ_WeenieRefusesInqJumpVelocity_ReturnsZero()
{
var weenie = new FakeWeenie { InqJumpVelocityResult = false };
var interp = MakeInterp(weenie: weenie);
interp.JumpExtent = 0.5f;
Assert.Equal(0f, interp.GetJumpVZ(), precision: 5);
}
// =========================================================================
// get_leave_ground_velocity — 0x005280c0 @305404 (A6; closes J16-epsilons)
// =========================================================================
[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.GetLeaveGroundVelocity();
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_TrulyZero_FallsBackToWorldVelocityGlobalToLocal()
{
// A6: fallback fires ONLY when |x| AND |y| AND |z| are ALL <
// epsilon. When it fires, ALL THREE components (including the
// just-computed jump v_z) are overwritten with
// globaltolocal(m_velocityVector) — the existing
// Quaternion.Inverse(Orientation) transform IS global->local; keep.
var body = MakeGrounded();
body.Velocity = new Vector3(2.0f, 3.0f, 0f); // nonzero WORLD velocity (momentum)
body.Orientation = Quaternion.Identity; // identity: local == global for this pin
var interp = new MotionInterpreter(body)
{
JumpExtent = 0f, // below epsilon -> get_jump_v_z returns 0
};
// Default interpreted state = Ready (no walk/run/sidestep command) -> XY also 0.
var vel = interp.GetLeaveGroundVelocity();
// Fallback must have fired: velocity now reflects body.Velocity
// (global->local via inverse-identity == identity here), NOT the
// all-zero get_state_velocity()/get_jump_v_z() composition.
Assert.Equal(2.0f, vel.X, precision: 4);
Assert.Equal(3.0f, vel.Y, precision: 4);
Assert.Equal(0.0f, vel.Z, precision: 4);
}
[Fact]
public void GetLeaveGroundVelocity_TrulyZero_NoWorldVelocity_StaysZero()
{
var body = MakeGrounded();
body.Velocity = Vector3.Zero;
var interp = new MotionInterpreter(body)
{
JumpExtent = 0f,
};
var vel = interp.GetLeaveGroundVelocity();
Assert.Equal(0f, vel.X, precision: 5);
Assert.Equal(0f, vel.Y, precision: 5);
Assert.Equal(0f, vel.Z, precision: 5);
}
[Fact]
public void GetLeaveGroundVelocity_NonzeroXOnly_DoesNotTriggerFallback()
{
// A6: the fallback requires ALL THREE axes below epsilon. A
// sidestep-only launch (nonzero X, zero Y, zero jump Z) must NOT
// trigger the momentum fallback — it should keep the computed X.
var body = MakeGrounded();
body.Velocity = new Vector3(99f, 99f, 99f); // if fallback wrongly fired, this would show
var interp = new MotionInterpreter(body)
{
JumpExtent = 0f, // jump v_z stays 0
};
interp.InterpretedState.SideStepCommand = MotionCommand.SideStepRight;
interp.InterpretedState.SideStepSpeed = 1.0f;
var vel = interp.GetLeaveGroundVelocity();
// X must be the sidestep-derived value, not 99 (fallback did not fire).
Assert.NotEqual(99f, vel.X);
Assert.True(vel.X > 0f, "sidestep should have produced nonzero X from get_state_velocity");
}
[Fact]
public void GetLeaveGroundVelocity_EpsilonBoundary_JustBelow_TriggersFallback()
{
var body = MakeGrounded();
body.Velocity = new Vector3(5f, 0f, 0f);
body.Orientation = Quaternion.Identity;
var interp = new MotionInterpreter(body)
{
JumpExtent = 0.0001f, // < 0.000199999995f -> get_jump_v_z() == 0
};
// Default interpreted state -> XY == 0 too. All three axes are
// exactly at the "essentially zero" edge -> fallback fires.
var vel = interp.GetLeaveGroundVelocity();
Assert.Equal(5f, vel.X, precision: 4);
}
// =========================================================================
// jump_is_allowed — 0x005282b0 (A2, A10; closes J5)
//
// Raw entry shape (r3-motioninterp-decomp.md §3h):
// if (physics_obj != 0) {
// if (weenie != 0) eax_2 = weenie->IsCreature();
// if (weenie != 0 && eax_2 == 0) // non-creature weenie
// goto shared_gate;
// if (physics_obj == 0 || (state bit 0x400) == 0) // gravity-state off
// goto shared_gate;
// if (Contact && OnWalkable) // grounded
// goto shared_gate;
// }
// return 0x24;
//
// shared_gate:
// if (IsFullyConstrained) return 0x47;
// head = pending_motions.head_;
// if (head != 0) eax_6 = head.jump_error_code;
// if (head == 0 || eax_6 == 0) {
// eax_6 = jump_charge_is_allowed();
// if (eax_6 == 0) {
// eax_7 = motion_allows_jump(interpreted_state.forward_command);
// if (eax_7 != 0) return eax_7;
// if (weenie_obj_1 == 0) return eax_7; // == 0 here
// eax_6 = 0x47;
// if (weenie_obj_1->JumpStaminaCost(extent, &cost) != 0)
// return eax_7; // == 0 (success)
// // JumpStaminaCost returned 0 (false) -> falls through, eax_6 stays 0x47
// }
// }
// return eax_6;
// =========================================================================
[Fact]
public void JumpIsAllowed_NullPhysicsObj_ReturnsNotGrounded()
{
var interp = new MotionInterpreter();
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.NotGrounded, result);
}
[Fact]
public void JumpIsAllowed_CreatureWeenie_Airborne_GravityState_ReturnsNotGrounded()
{
var body = MakeAirborne();
var interp = MakeInterp(body);
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.NotGrounded, result);
}
[Fact]
public void JumpIsAllowed_CreatureWeenie_Grounded_ReturnsNone()
{
var body = MakeGrounded();
var interp = MakeInterp(body);
var result = interp.jump_is_allowed(0.5f, out int cost);
Assert.Equal(WeenieError.None, result);
Assert.Equal(0, cost);
}
[Fact]
public void JumpIsAllowed_NonCreatureWeenie_Airborne_SkipsGroundGate_ReturnsNone()
{
// weenie present && !IsCreature() -> skip the ground check entirely,
// go straight to the shared gate. Airborne here must NOT block.
var weenie = new FakeWeenie();
var body = MakeAirborne();
var interp = MakeInterp(body, weenie);
// Non-creature: use an explicit fake that overrides IsCreature.
var nonCreature = new NonCreatureFakeWeenie();
interp.WeenieObj = nonCreature;
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.None, result);
}
[Fact]
public void JumpIsAllowed_NoWeenie_GravityStateOff_SkipsGroundGate_ReturnsNone()
{
// "no weenie" -> weenie_obj == 0 branch is fine either way (the
// `weenie != 0 && eax_2 == 0` non-creature-skip only applies when a
// weenie exists); but the SEPARATE gravity-state-off skip
// (state bit 0x400 clear) also reaches the shared gate regardless
// of weenie presence. Pin: no gravity flag -> shared gate, not
// blocked outright.
var body = new PhysicsBody
{
State = PhysicsStateFlags.None, // gravity flag clear
TransientState = TransientStateFlags.None,
};
var interp = MakeInterp(body);
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.None, result);
}
[Fact]
public void JumpIsAllowed_IsFullyConstrained_ReturnsGeneralMovementFailure()
{
var body = MakeGrounded();
body.IsFullyConstrained = true;
var interp = MakeInterp(body);
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.GeneralMovementFailure, result); // 0x47
}
[Fact]
public void JumpIsAllowed_IsFullyConstrained_BeatsPendingHeadPeek()
{
// A2 ordering: IsFullyConstrained is checked BEFORE the pending-head
// peek. Even if the head carries a DIFFERENT nonzero error, the
// IsFullyConstrained code (0x47) must win.
var body = MakeGrounded();
body.IsFullyConstrained = true;
var interp = MakeInterp(body);
interp.AddToQueue(0, MotionCommand.WalkForward, (uint)WeenieError.CantJumpLoadedDown);
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.GeneralMovementFailure, result);
}
[Fact]
public void JumpIsAllowed_PendingHeadNonzeroError_ShortCircuitsChain()
{
// A2: the peek fires WHENEVER the queue is non-empty (no Count>1
// gate). A nonzero head.JumpErrorCode is returned VERBATIM, and the
// charge/motion/stamina chain is skipped entirely (proven via the
// weenie's CanJump/JumpStaminaCost never being consulted for THIS
// code path — CanJump would return CantJumpLoadedDown some other
// way, so instead assert JumpStaminaCost, which only the fallthrough
// chain calls, is never invoked).
var weenie = new FakeWeenie();
var body = MakeGrounded();
var interp = MakeInterp(body, weenie);
interp.AddToQueue(0, MotionCommand.WalkForward, (uint)WeenieError.YouCantJumpFromThisPosition);
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result);
Assert.Equal(0, weenie.JumpStaminaCostCalls);
}
[Fact]
public void JumpIsAllowed_PendingHeadZeroError_FallsThroughToChain()
{
// A2: head exists but JumpErrorCode == 0 -> chain still runs.
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.AddToQueue(0, MotionCommand.WalkForward, jumpErrorCode: 0);
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.None, result);
}
[Fact]
public void JumpIsAllowed_EmptyQueue_FallsThroughToChain()
{
var body = MakeGrounded();
var interp = MakeInterp(body);
// No AddToQueue call -> head is null -> chain runs normally.
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.None, result);
}
[Fact]
public void JumpIsAllowed_ChargeBlocked_Fallen_ReturnsYouCantJumpFromThisPosition()
{
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.InterpretedState.ForwardCommand = MotionCommand.Fallen;
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result);
}
[Fact]
public void JumpIsAllowed_ChargeBlocked_WeenieCanJumpFalse_ReturnsCantJumpLoadedDown()
{
var weenie = new FakeWeenie { CanJumpResult = false };
var body = MakeGrounded();
var interp = MakeInterp(body, weenie);
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.CantJumpLoadedDown, result);
}
[Fact]
public void JumpIsAllowed_MotionAllowsJumpBlocks_ReturnsYouCantJumpFromThisPosition()
{
// jump_charge_is_allowed passes (Ready forward command), but the
// SEPARATE motion_allows_jump(forward_command) check blocks — use a
// forward command in the [0x4000001e, 0x40000039] blocklist band
// that jump_charge_is_allowed does NOT gate on (only Fallen/Crouch
// range does), so this pins the double-check shape.
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.InterpretedState.ForwardCommand = 0x40000020u; // inside [0x4000001e, 0x40000039]
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result);
}
[Fact]
public void JumpIsAllowed_NoWeenie_PassesChain_ReturnsNone()
{
// motion_allows_jump passes and weenie_obj == null -> return eax_7
// (== 0) directly, skipping the JumpStaminaCost consult entirely.
var body = MakeGrounded();
var interp = MakeInterp(body); // no weenie
var result = interp.jump_is_allowed(0.5f, out int cost);
Assert.Equal(WeenieError.None, result);
Assert.Equal(0, cost);
}
[Fact]
public void JumpIsAllowed_WeenieJumpStaminaCostRefuses_ReturnsGeneralMovementFailure()
{
var weenie = new FakeWeenie { JumpStaminaCostResult = false };
var body = MakeGrounded();
var interp = MakeInterp(body, weenie);
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.GeneralMovementFailure, result); // 0x47
Assert.Equal(1, weenie.JumpStaminaCostCalls);
}
[Fact]
public void JumpIsAllowed_WeenieJumpStaminaCostAffords_ReturnsNone()
{
var weenie = new FakeWeenie { JumpStaminaCostResult = true };
var body = MakeGrounded();
var interp = MakeInterp(body, weenie);
var result = interp.jump_is_allowed(0.5f, out _);
Assert.Equal(WeenieError.None, result);
Assert.Equal(1, weenie.JumpStaminaCostCalls);
}
/// Fake weenie whose IsCreature() returns false, for the
/// non-creature ground-gate-skip pin.
private sealed class NonCreatureFakeWeenie : IWeenieObject
{
public bool InqJumpVelocity(float extent, out float vz) { vz = 10f; return true; }
public bool InqRunRate(out float rate) { rate = 1f; return true; }
public bool CanJump(float extent) => true;
public bool IsCreature() => false;
}
// =========================================================================
// jump — 0x00528780 @305792 (closes J7-interp-side)
// =========================================================================
[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()
{
var body = MakeAirborne();
var interp = MakeInterp(body);
var result = interp.jump(0.5f);
Assert.Equal(WeenieError.NotGrounded, result);
}
[Fact]
public void Jump_WeenieBlocksJump_ClearsStandingLongJump()
{
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, "a failed jump attempt cancels any pending StandingLongJump");
}
[Fact]
public void Jump_NullPhysicsObj_ReturnsNoPhysicsObject()
{
var interp = new MotionInterpreter();
var result = interp.jump(0.5f);
Assert.Equal(WeenieError.NoPhysicsObject, result);
}
[Fact]
public void Jump_Success_DoesNotClearStandingLongJump()
{
// Only the FAILURE path clears StandingLongJump (raw 305800:
// `this->standing_longjump = 0;` sits inside the `if (result != 0)`
// branch only). A successful jump leaves whatever value the flag
// already had.
var body = MakeGrounded();
var interp = MakeInterp(body);
interp.StandingLongJump = true;
var result = interp.jump(0.5f);
Assert.Equal(WeenieError.None, result);
Assert.True(interp.StandingLongJump, "success path must not touch StandingLongJump");
}
[Fact]
public void Jump_CallsInterruptCurrentMovementSeam()
{
// J7 / "Adjacent findings": jump() ALWAYS calls
// interrupt_current_movement first (raw @305800), regardless of
// outcome. Register no-op seam until R4's cancel_moveto exists.
var body = MakeGrounded();
var interp = MakeInterp(body);
bool called = false;
interp.InterruptCurrentMovement = () => called = true;
interp.jump(0.5f);
Assert.True(called, "jump() must invoke the InterruptCurrentMovement seam");
}
[Fact]
public void Jump_CallsInterruptCurrentMovementSeam_EvenOnFailure()
{
var body = MakeAirborne();
var interp = MakeInterp(body);
bool called = false;
interp.InterruptCurrentMovement = () => called = true;
interp.jump(0.5f);
Assert.True(called, "jump() calls interrupt_current_movement before jump_is_allowed, unconditionally");
}
// =========================================================================
// IWeenieObject.IsThePlayer — default false; PlayerWeenie true (for W4's
// A3 dispatch gate; no consumer yet in W3).
// =========================================================================
[Fact]
public void IWeenieObject_IsThePlayer_DefaultsFalse()
{
// Default interface members are only reachable through the
// interface type, not the concrete implementer — call via
// IWeenieObject to exercise the DIM (not a compile error site).
IWeenieObject weenie = new FakeWeenie();
Assert.False(weenie.IsThePlayer());
}
[Fact]
public void PlayerWeenie_IsThePlayer_ReturnsTrue()
{
var weenie = new PlayerWeenie();
Assert.True(weenie.IsThePlayer());
}
}