diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index 0c638a7..205f35e 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -31,7 +31,8 @@ public readonly record struct MovementResult( uint? TurnCommand, float? ForwardSpeed, float? SidestepSpeed, - float? TurnSpeed); + float? TurnSpeed, + float? JumpExtent = null); // non-null when a jump was triggered this frame /// /// Portal-space state for the player movement controller. @@ -94,6 +95,11 @@ public sealed class PlayerMovementController /// public float VerticalVelocity => _body.Velocity.Z; + // Jump charge state. + private bool _jumpCharging; + private float _jumpExtent; + private const float JumpChargeRate = 1.0f; // 0→1 over 1 second + // Previous frame's motion commands for change detection. private uint? _prevForwardCmd; private uint? _prevSidestepCmd; @@ -246,15 +252,33 @@ public sealed class PlayerMovementController // Rotation about Z does not affect the Z component, so world Vz == local Vz. _body.set_local_velocity(new Vector3(localX, localY, savedWorldVz)); - // ── 3. Jump ─────────────────────────────────────────────────────────── - if (input.Jump) + // ── 3. Jump (charged) ───────────────────────────────────────────────── + // Hold spacebar to charge (0→1 over JumpChargeRate seconds). + // Release to execute: jump(extent) validates + sets JumpExtent, + // then LeaveGround() applies the scaled velocity via get_leave_ground_velocity. + float? outJumpExtent = null; + + if (input.Jump && _body.OnWalkable) { - var jumpResult = _motion.jump(1.0f); + // Spacebar held and on the ground — accumulate charge. + if (!_jumpCharging) + { + _jumpCharging = true; + _jumpExtent = 0f; + } + _jumpExtent = MathF.Min(_jumpExtent + dt * JumpChargeRate, 1.0f); + } + else if (_jumpCharging) + { + // Spacebar released (or left ground during charge) — fire jump. + var jumpResult = _motion.jump(_jumpExtent); if (jumpResult == WeenieError.None) { - // jump() set_on_walkable(false); now apply the launch velocity. _motion.LeaveGround(); + outJumpExtent = _jumpExtent; } + _jumpCharging = false; + _jumpExtent = 0f; } // ── 4. Integrate physics (gravity, friction, sub-stepping) ──────────── @@ -384,6 +408,7 @@ public sealed class PlayerMovementController TurnCommand: outTurnCmd, ForwardSpeed: outForwardSpeed, SidestepSpeed: outSidestepSpeed, - TurnSpeed: outTurnSpeed); + TurnSpeed: outTurnSpeed, + JumpExtent: outJumpExtent); } } diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index a634772..28a7a7b 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1912,6 +1912,13 @@ public sealed class GameWindow : IDisposable forcePositionSequence: _liveSession.ForcePositionSequence); _liveSession.SendGameAction(body); } + + if (result.JumpExtent.HasValue) + { + // TODO: send jump packet to server (format needs research from holtburger). + // Local jump physics work without server acknowledgment for now. + Console.WriteLine($"jump: extent={result.JumpExtent.Value:F2}"); + } } // Update the player entity's animation cycle to match current motion. diff --git a/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs b/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs index 44c55c4..fe1e859 100644 --- a/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs +++ b/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs @@ -108,8 +108,11 @@ public class PlayerMovementControllerTests var controller = new PlayerMovementController(engine); controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001); - var input = new MovementInput(Jump: true); - controller.Update(0.016f, input); + // Charged jump: hold for a full charge (1s dt), then release to fire. + // A full charge gives enough Vz that the player clears the 0.05-unit + // ground-snap threshold within the same integration frame. + controller.Update(1.0f, new MovementInput(Jump: true)); // full charge + controller.Update(0.016f, new MovementInput(Jump: false)); // release → jump fires Assert.True(controller.IsAirborne); Assert.True(controller.VerticalVelocity > 0f); @@ -122,8 +125,9 @@ public class PlayerMovementControllerTests var controller = new PlayerMovementController(engine); controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001); - // Jump - controller.Update(0.016f, new MovementInput(Jump: true)); + // Charged jump: hold for a full charge, then release. + controller.Update(1.0f, new MovementInput(Jump: true)); // full charge + controller.Update(0.016f, new MovementInput(Jump: false)); // release → jump fires float z1 = controller.Position.Z; // A few frames of rising