fix: airborne jump refusal fires at RELEASE, not press (user retail

gate; supersedes CH round-1 item A)

The user's retail description matches the decomp exactly:
charge_jump @0x005281c0 has NO grounded check - it refuses only 0x49
(CanJump encumbrance) and 0x48 (fallen/crouch-family forward commands).
Pressing jump while airborne begins the powerbar and charges normally.
The 0x24 "You can't jump while in the air" comes exclusively from the
RELEASE path (ClientCombatSystem::DoJump @0x0056B110 ->
CMotionInterp::jump -> jump_is_allowed, whose airborne 0x24 our port
already carries test-pinned). A charge held through landing executes a
normal jump on the grounded release.

PlayerMovementController's input orchestration now mirrors
CommenceJump/DoJump:
- Press edge: ChargeJump() decides; a refused charge (0x48/0x49)
  reports and never begins the bar (retail's jump_pending stays 0).
  The invented airborne press-edge 0x24 report (CH user-gate round 1
  item A - added when the press/release split was not yet known) is
  deleted; CommenceJump's in-air fallback text is unreachable with a
  faithful charge_jump.
- Hold: accumulates grounded OR airborne; leaving the ground mid-charge
  no longer force-fires the jump.
- Release: fires jump(); an airborne release refuses 0x24 there.

Tests: the round-1 press-edge test is replaced by two release-semantics
tests (airborne release reports once; held-through-landing grounded
release jumps silently). Runtime 1,619, App 4,987/3, Core jump family
159.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-14 08:31:22 +02:00
parent 88cdfdc3c7
commit 1390f9477d
2 changed files with 87 additions and 56 deletions

View file

@ -2535,26 +2535,42 @@ public sealed class PlayerMovementController
float? outJumpExtent = null;
Vector3? outJumpVelocity = null;
if (input.Jump && _body.OnWalkable)
if (input.Jump && !_jumpCharging && !_prevJumpHeld)
{
// Spacebar held and on the ground — accumulate charge.
if (!_jumpCharging)
// Press edge — ClientCombatSystem::CommenceJump @0x0056AF90.
// R3-W6 (map R1): retail's charge_jump fires at charge START
// (SmartBox/input boundary 0x0056afac) — the ONLY place
// StandingLongJump arms (grounded + Ready + no sidestep/turn).
//
// 2026-08-14 user retail gate: charge_jump @0x005281c0 has NO
// grounded check — it refuses only 0x49 (CanJump encumbrance)
// and 0x48 (fallen/crouch-family commands). Pressing jump while
// AIRBORNE charges the bar normally; retail's 0x24 "You can't
// jump while in the air" comes exclusively from the RELEASE
// path's jump_is_allowed (DoJump @0x0056B110 → CMotionInterp::
// jump), so charging through the air and releasing after
// landing executes a normal jump. This supersedes the CH
// user-gate round 1 item A press-edge 0x24 report — CommenceJump
// DOES carry an in-air fallback text, but with a faithful
// charge_jump (0/0x48/0x49 only) that arm is unreachable.
//
// A REFUSED charge reports and never begins the bar
// (jump_pending stays 0 in retail — no powerbar, no charge).
WeenieError chargeResult = _motion.ChargeJump();
if (chargeResult == WeenieError.None)
{
_jumpCharging = true;
_jumpExtent = 0f;
// R3-W6 (map R1): retail's charge_jump fires at charge
// START (SmartBox/input boundary 0x0056afac) — the ONLY
// place StandingLongJump arms (grounded + Ready + no
// sidestep/turn). Never called by production code before
// this line despite the W3 port.
//
// Campaign CH slice CH2: the return value used to be
// discarded — a refused charge (CantJumpLoadedDown /
// YouCantJumpFromThisPosition) silently drained the power
// bar with no explanation. Report it exactly as
// ClientCombatSystem::CommenceJump @0x0056AF90 does.
ReportJumpRefusal(_motion.ChargeJump());
}
else
{
ReportJumpRefusal(chargeResult);
}
}
if (input.Jump && _jumpCharging)
{
// Spacebar held — accumulate charge (grounded OR airborne).
float chargeRate = _motion.InterpretedState.CurrentStyle
== AcDream.Core.Combat.CombatInputPlanner.DualWieldCombatStyle
? DualWieldJumpChargeRate
@ -2563,7 +2579,8 @@ public sealed class PlayerMovementController
}
else if (_jumpCharging)
{
// Spacebar released (or left ground during charge) — fire jump.
// Spacebar RELEASED — fire jump (DoJump @0x0056B110). An
// airborne release refuses 0x24 through jump_is_allowed below.
var jumpResult = _motion.jump(_jumpExtent);
if (jumpResult == WeenieError.None)
{
@ -2603,20 +2620,6 @@ public sealed class PlayerMovementController
_jumpCharging = false;
_jumpExtent = 0f;
}
else if (input.Jump && !_prevJumpHeld && !_body.OnWalkable)
{
// Campaign CH user-gate round 1, item A: the whole jump block
// above only ever evaluates `input.Jump` inside
// `input.Jump && _body.OnWalkable` (charge) or `_jumpCharging`
// (fire/refuse) — pressing jump while airborne and NOT already
// charging never reached either branch, so retail's 0x24 "You
// can't jump while in the air" (jump_is_allowed via
// ClientCombatSystem::DoJump @0x0056B110) could never fire live.
// Report it exactly like the grounded refusals above, gated to
// the press EDGE only (see _prevJumpHeld) so holding space
// in-air raises exactly one report, not one per frame.
ReportJumpRefusal(WeenieError.NotGrounded);
}
// Campaign CH user-gate round 2, item 1 (TEMPORARY): per-tick trace
// bracketing every frame where jump is (or was) held, so the probe