Skip to content

Add player-specific capsule physics shape creation#598

Open
tracygardner wants to merge 12 commits intomainfrom
claude/fix-physics-capsule-YrFd2
Open

Add player-specific capsule physics shape creation#598
tracygardner wants to merge 12 commits intomainfrom
claude/fix-physics-capsule-YrFd2

Conversation

@tracygardner
Copy link
Copy Markdown
Contributor

@tracygardner tracygardner commented May 1, 2026

Summary

This PR introduces a new createPlayerCapsuleFromBoundingBox method to handle physics capsule generation specifically for player characters, with improved constraints to prevent degenerate shapes for wide or flat meshes.

Key Changes

  • New player capsule creation method (createPlayerCapsuleFromBoundingBox): Creates physics capsules with a capped radius that ensures the cylindrical section is always at least 1/3 of the total height, preventing issues with aircraft-like or flat meshes
  • Physics shape type expansion: Added "PLAYER_CAPSULE" as a new physics shape type in setPhysicsShape, allowing models to opt into the player-specific capsule behavior
  • Lazy capsule initialization: Modified movement system to automatically initialize player capsules on-demand if they're missing, with a pending flag to prevent duplicate requests
  • Refactored capsule application: Made applyCapsuleToRoot accept a configurable creation function parameter, enabling reuse of the capsule application logic for both standard and player-specific capsules

Implementation Details

  • The player capsule includes a small shrink amount (0.01 units) to prevent physics overlap issues
  • Radius is constrained to Math.min(Math.min(width, depth) / 2, height / 3) to maintain reasonable proportions
  • Capsule metadata is stored on the mesh for later reference by the movement system
  • The lazy initialization in movement.js ensures player capsules are created when needed without requiring explicit setup

https://claude.ai/code/session_019iH8iKivgCCsF3m8MpYcsc

Summary by CodeRabbit

  • New Features

    • Added player-specific capsule generation with explicit metadata to improve character collision sizing.
  • Bug Fixes

    • More robust player physics initialization with asynchronous setup when shape data is missing.
    • Movement and grounding improved: self-collision ignored during probes, longer air "coyote" window, and refined step detection/step-up behavior for smoother, more stable movement.

Wide/flat meshes (e.g. airplane) produce a degenerate sphere via the existing
createCapsuleFromBoundingBox because radius >= height/2. A separate
createPlayerCapsuleFromBoundingBox caps radius at height/3, ensuring the
cylinder section is always present and the capsule is narrow enough for
slope traversal.

moveForward now detects a missing physicsCapsule and automatically switches
the mesh to PLAYER_CAPSULE shape on first use, via a one-time async call
guarded by _playerCapsulePending.

https://claude.ai/code/session_019iH8iKivgCCsF3m8MpYcsc
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

Warning

Rate limit exceeded

@tracygardner has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 51 minutes and 10 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a49b9386-dc1f-4715-a9fd-64e71fbe204f

📥 Commits

Reviewing files that changed from the base of the PR and between f166430 and 2aa0970.

📒 Files selected for processing (1)
  • api/movement.js
📝 Walkthrough

Walkthrough

Adds a player-specific capsule generator (clamped radius) and persists player-capsule metadata; defers/async-initializes player capsule from movement when physics/capsule data is missing or degenerate; and exposes an injectable capsule factory plus a "PLAYER_CAPSULE" path in physics shape creation. (≤50 words)

Changes

Cohort / File(s) Summary
Player capsule helper
api/mesh.js
Adds createPlayerCapsuleFromBoundingBox(mesh, scene) which computes capsule endpoints from bounding box/cylinder, clamps radius to at most height/3, creates the physics capsule, and writes mesh.metadata.physicsCapsule with radius, height, baseY, localCenter, and isPlayerCapsule: true. Also updates createCapsuleFromBoundingBox to set isPlayerCapsule: false.
Movement validation & deferred init
api/movement.js
moveForward tolerates missing/degenerate physics capsule: if model.physics or mesh.metadata.physicsCapsule is absent or not a player capsule, schedules an async flock.setPhysicsShape(..., "PLAYER_CAPSULE") once via model._playerCapsulePending and returns early that frame. Ground/step probes now ignore the model's own body (ignoredBodies: [model.physics]); coyote time increased to 250ms; step-up probe limited to tall capsules (cap.height > stepHeight * 3), uses cap.localCenter.y for probe origin, and only applies step boost when hit normal dot up < 0.5.
Physics shape factory dispatch
api/physics.js
setPhysicsShape accepts an optional factory (createFn) so capsule creation can use a custom factory; adds a "PLAYER_CAPSULE" branch that uses flock.createPlayerCapsuleFromBoundingBox via the factory parameter while preserving existing "CAPSULE" behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Movement as Movement
    participant Physics as Physics Engine
    participant Mesh as Mesh Factory

    Movement->>Movement: moveForward(model)
    Movement->>Movement: check model.physics & mesh.metadata.physicsCapsule
    alt capsule missing or degenerate
        Movement->>Physics: setPhysicsShape(model, "PLAYER_CAPSULE")
        Physics->>Mesh: createPlayerCapsuleFromBoundingBox(mesh, scene)
        Mesh->>Mesh: compute radius,height,baseY,localCenter (radius ≤ height/3)
        Mesh-->>Physics: return PhysicsShapeCapsule
        Physics-->>Movement: shape applied (async)
        Movement->>Movement: return early this frame (retry next update)
    else capsule valid
        Movement->>Movement: enforce vertical constraint
        Movement->>Physics: ground/step shapeCast (ignoredBodies: [model.physics])
        Movement->>Movement: apply movement/step logic
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Poem

🐰 I nudged the mesh with careful paw,

Clamped my radius, knew the law,
If physics waits, I'll patient be,
One frame pause — then up the lea,
Capsule snug, I bound and hop with glee! 🎩✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and clearly summarizes the main change: adding a new player-specific capsule physics shape creation system. The title is concise, specific, and accurately reflects the primary functionality being introduced.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-physics-capsule-YrFd2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 51 minutes and 10 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 1, 2026

Deploying flockxr with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2aa0970
Status: ✅  Deploy successful!
Preview URL: https://766b0d1b.flockxr.pages.dev
Branch Preview URL: https://claude-fix-physics-capsule-y.flockxr.pages.dev

View logs

claude added 6 commits May 1, 2026 07:19
The CAPSULE case in setPhysicsShape now defaults to
createPlayerCapsuleFromBoundingBox, so explicit user calls like
setPhysicsShape(player, "CAPSULE") also get the height/3 radius cap.
Previously the airplane produced a degenerate sphere (radius > height/2)
even when the user set the physics shape explicitly.

Removes the PLAYER_CAPSULE internal type — CAPSULE now always uses
the correct formula since setPhysicsShape capsules are exclusively
for player characters.

https://claude.ai/code/session_019iH8iKivgCCsF3m8MpYcsc
The early return on !model.physics prevented the auto-capsule path from
ever running when setPhysicsShape had not been called explicitly. GLB root
nodes often fail MESH shape creation, leaving model.physics null.

Include !model.physics in the capsule condition so the auto-switch fires
regardless. ensureVerticalConstraint moved to after the capsule check
since it requires physics to exist.

https://claude.ai/code/session_019iH8iKivgCCsF3m8MpYcsc
setupMesh calls createCapsuleFromBoundingBox for every loaded model,
which degenerates to a sphere for wide/flat meshes like the airplane.
Switch to createPlayerCapsuleFromBoundingBox (radius capped at height/3)
so the default capsule is always a proper tall shape suitable for movement.

https://claude.ai/code/session_019iH8iKivgCCsF3m8MpYcsc
Revert setupMesh and applyCapsuleToRoot defaults to createCapsuleFromBoundingBox
so non-player objects keep the standard capsule shape.

moveForward now detects a degenerate capsule (height <= 2*radius, no cylinder
section) as well as missing physics/capsule, and upgrades to PLAYER_CAPSULE
on first call only. PLAYER_CAPSULE is an internal shape type in setPhysicsShape
that uses createPlayerCapsuleFromBoundingBox (radius capped at height/3).

https://claude.ai/code/session_019iH8iKivgCCsF3m8MpYcsc
restitution: 0.5 caused micro-bounces at slope crests — the capsule
briefly went airborne, cutting forward control (airControlFactor=0),
producing the stuck-at-top-of-slope behaviour.

Player capsule now uses restitution: 0 on the body and
friction: 0 on the shape material so it slides cleanly over
terrain polygons without bouncing or gripping edges.

https://claude.ai/code/session_019iH8iKivgCCsF3m8MpYcsc
…on change

Ground check and step probe shape casts had ignoredBodies: [] so they were
colliding with the player's own physics body. For wide/short player capsules
(box, airplane) the self-hit normal comes back horizontal → grounded=false
→ forward control cut → bumping/stalling. Character capsules happened to
produce a near-vertical self-hit normal so the bug was masked.

Fix: pass [model.physics] as ignoredBodies in both the ground check and
step probe casts so only terrain and other objects are detected.

Also reverts the restitution/friction change from the previous commit —
it was wrong; character meshes work fine with the existing restitution.

https://claude.ai/code/session_019iH8iKivgCCsF3m8MpYcsc
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/movement.js (1)

1-1: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Address Prettier formatting issue flagged by CI.

The pipeline reports a Prettier check failure. Run the formatter to resolve before merge:

npx prettier --write api/movement.js
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/movement.js` at line 1, CI reports a Prettier formatting failure in
api/movement.js (the file declaring the top-level variable let flock;); fix it
by running the project formatter (e.g., npx prettier --write api/movement.js) or
applying the same Prettier rules in your editor to reformat the file so it
passes the Prettier check, then re-run the Prettier/CI check and commit the
changed formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/movement.js`:
- Around line 18-23: The code sets model._playerCapsulePending=true and calls
flock.setPhysicsShape(modelName, "PLAYER_CAPSULE") but never clears the flag if
the promise rejects, which can permanently block re-initialization; change the
promise handling on flock.setPhysicsShape(...) in the block that sets
model._playerCapsulePending so the flag is cleared in a finally handler (or via
.then(...).catch(...).finally(...)) regardless of success or failure, ensuring
delete model._playerCapsulePending always runs and allowing retries on transient
errors.

---

Outside diff comments:
In `@api/movement.js`:
- Line 1: CI reports a Prettier formatting failure in api/movement.js (the file
declaring the top-level variable let flock;); fix it by running the project
formatter (e.g., npx prettier --write api/movement.js) or applying the same
Prettier rules in your editor to reformat the file so it passes the Prettier
check, then re-run the Prettier/CI check and commit the changed formatting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1393be7d-9a01-4d7d-99ab-fba6eee9b04b

📥 Commits

Reviewing files that changed from the base of the PR and between 1aabb57 and 8bed98b.

📒 Files selected for processing (1)
  • api/movement.js

Comment thread api/movement.js
Comment on lines +18 to +23
if (!model._playerCapsulePending) {
model._playerCapsulePending = true;
flock.setPhysicsShape(modelName, "PLAYER_CAPSULE").then(() => {
delete model._playerCapsulePending;
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing error handling: rejected promise leaves player unable to move.

If setPhysicsShape rejects, the .then() callback never executes, _playerCapsulePending remains true, and subsequent frames skip re-initialization. The player would be permanently stuck.

Use .finally() to ensure the flag is cleared regardless of outcome, allowing retry on transient failures.

Proposed fix
       if (!model._playerCapsulePending) {
         model._playerCapsulePending = true;
-        flock.setPhysicsShape(modelName, "PLAYER_CAPSULE").then(() => {
-          delete model._playerCapsulePending;
-        });
+        flock.setPhysicsShape(modelName, "PLAYER_CAPSULE").finally(() => {
+          delete model._playerCapsulePending;
+        });
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/movement.js` around lines 18 - 23, The code sets
model._playerCapsulePending=true and calls flock.setPhysicsShape(modelName,
"PLAYER_CAPSULE") but never clears the flag if the promise rejects, which can
permanently block re-initialization; change the promise handling on
flock.setPhysicsShape(...) in the block that sets model._playerCapsulePending so
the flag is cleared in a finally handler (or via
.then(...).catch(...).finally(...)) regardless of success or failure, ensuring
delete model._playerCapsulePending always runs and allowing retries on transient
errors.

claude added 4 commits May 1, 2026 10:18
The height<=2*radius check had edge cases where the airplane's bounding
box orientation meant the default capsule wasn't detected as degenerate,
leaving it as a sphere even though it should be switched.

createPlayerCapsuleFromBoundingBox now sets cap.isPlayerCapsule=true.
createCapsuleFromBoundingBox sets it false. moveForward switches any
mesh whose cap.isPlayerCapsule is not true — reliably catches all cases
regardless of capsule proportions.

https://claude.ai/code/session_019iH8iKivgCCsF3m8MpYcsc
The horizontal step probe was boosting the capsule upward whenever it hit
any surface ahead, including sloped terrain faces when descending. This
launched the capsule airborne (killing forward control via airControlFactor=0)
and caused it to stall at slope crests.

Fix: check the hit surface normal before allowing a step-up boost.
Downward slope faces have a normal with upDot > 0.5 (pointing mostly upward),
so they are skipped. Only near-vertical surfaces (upDot < 0.5) — actual
ledges — trigger the boost.

https://claude.ai/code/session_019iH8iKivgCCsF3m8MpYcsc
The step-up probe started at model.position + 0.05. For humanoid characters
localCenter.y ≈ capsuleHeight/2 so model.position.y sits at ground level —
the probe fires just above ground as intended. But for wide/flat meshes
(airplane) localCenter.y ≈ 0 so model.position.y is at the capsule centre
(~0.3m above ground), putting the probe at 0.35m — high enough to hit the
face of a downward slope and trigger a false step-up boost.

Fix: compute the actual ground Y from model.position.y adjusted by
(cap.height/2 - cap.localCenter.y), then anchor the probe 0.05m above
that. The probe now fires near ground level for all capsule shapes,
so a downward slope (whose face is at or below ground) is no longer hit.

https://claude.ai/code/session_019iH8iKivgCCsF3m8MpYcsc
Short capsules (height <= stepHeight * 3, e.g. airplane at ~0.6m) don't
benefit from the step-up boost — their hemispherical base naturally rides
over small bumps. The probe was firing on slope-crest transition faces and
micro-bumps, repeatedly boosting the capsule airborne and stalling it at
the top of downward slopes.

Tall capsules (characters at ~1.8m) still use the probe because they need
help stepping over kerbs and low ledges.

Also increased coyoteTimeMs 120→250ms so briefly airborne states on slope
transitions don't immediately cut forward control.

https://claude.ai/code/session_019iH8iKivgCCsF3m8MpYcsc
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/mesh.js`:
- Around line 16-27: The capsule dimension math can still yield zero/near-zero
volumes for flat or thin meshes; clamp radius with a small epsilon (e.g., const
EPS = 1e-3) after computing radius and then ensure adjustedHeight is at least 2
* radius + EPS before computing cylinderHeight. Concretely: after computing
radius in the mesh sizing block, set radius = Math.max(radius, EPS); compute
adjustedHeight = Math.max(0, height - shrinkAmount) and then enforce
adjustedHeight = Math.max(adjustedHeight, 2 * radius + EPS) before computing
cylinderHeight = adjustedHeight - 2 * radius (and still Math.max(0, ...) if
desired). Update the symbols radius, adjustedHeight, shrinkAmount, and
cylinderHeight accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 086e6a51-c661-4241-a28e-00e8a1269dec

📥 Commits

Reviewing files that changed from the base of the PR and between 8bed98b and f166430.

📒 Files selected for processing (2)
  • api/mesh.js
  • api/movement.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/movement.js

Comment thread api/mesh.js
Comment on lines +16 to +27
const height = (localMax.y - localMin.y) * Math.abs(mesh.scaling.y);
const width = (localMax.x - localMin.x) * Math.abs(mesh.scaling.x);
const depth = (localMax.z - localMin.z) * Math.abs(mesh.scaling.z);

// Cap radius so the cylinder section is always at least height/3 tall,
// preventing degenerate sphere shapes for wide/flat meshes like aircraft.
const radius = Math.min(Math.min(width, depth) / 2, height / 3);

const shrinkAmount = 0.01;
const adjustedHeight = Math.max(0, height - shrinkAmount);
const cylinderHeight = Math.max(0, adjustedHeight - 2 * radius);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clamp minimum capsule dimensions to avoid zero-volume player shapes

Lines 22 and 25-27 can still produce a zero/near-zero capsule for perfectly flat or ultra-thin meshes, which weakens/loses collisions on the exact path meant to handle those models. Add an epsilon floor for radius and ensure adjustedHeight >= 2 * radius + epsilon.

Proposed fix
+    const EPSILON = 1e-4;
-    const height = (localMax.y - localMin.y) * Math.abs(mesh.scaling.y);
-    const width = (localMax.x - localMin.x) * Math.abs(mesh.scaling.x);
-    const depth = (localMax.z - localMin.z) * Math.abs(mesh.scaling.z);
+    const height = Math.max(
+      (localMax.y - localMin.y) * Math.abs(mesh.scaling.y),
+      EPSILON,
+    );
+    const width = Math.max(
+      (localMax.x - localMin.x) * Math.abs(mesh.scaling.x),
+      EPSILON,
+    );
+    const depth = Math.max(
+      (localMax.z - localMin.z) * Math.abs(mesh.scaling.z),
+      EPSILON,
+    );

-    const radius = Math.min(Math.min(width, depth) / 2, height / 3);
+    const radius = Math.max(
+      EPSILON,
+      Math.min(Math.min(width, depth) / 2, height / 3),
+    );

     const shrinkAmount = 0.01;
-    const adjustedHeight = Math.max(0, height - shrinkAmount);
+    const adjustedHeight = Math.max(2 * radius + EPSILON, height - shrinkAmount);
     const cylinderHeight = Math.max(0, adjustedHeight - 2 * radius);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const height = (localMax.y - localMin.y) * Math.abs(mesh.scaling.y);
const width = (localMax.x - localMin.x) * Math.abs(mesh.scaling.x);
const depth = (localMax.z - localMin.z) * Math.abs(mesh.scaling.z);
// Cap radius so the cylinder section is always at least height/3 tall,
// preventing degenerate sphere shapes for wide/flat meshes like aircraft.
const radius = Math.min(Math.min(width, depth) / 2, height / 3);
const shrinkAmount = 0.01;
const adjustedHeight = Math.max(0, height - shrinkAmount);
const cylinderHeight = Math.max(0, adjustedHeight - 2 * radius);
const EPSILON = 1e-4;
const height = Math.max(
(localMax.y - localMin.y) * Math.abs(mesh.scaling.y),
EPSILON,
);
const width = Math.max(
(localMax.x - localMin.x) * Math.abs(mesh.scaling.x),
EPSILON,
);
const depth = Math.max(
(localMax.z - localMin.z) * Math.abs(mesh.scaling.z),
EPSILON,
);
// Cap radius so the cylinder section is always at least height/3 tall,
// preventing degenerate sphere shapes for wide/flat meshes like aircraft.
const radius = Math.max(
EPSILON,
Math.min(Math.min(width, depth) / 2, height / 3),
);
const shrinkAmount = 0.01;
const adjustedHeight = Math.max(2 * radius + EPSILON, height - shrinkAmount);
const cylinderHeight = Math.max(0, adjustedHeight - 2 * radius);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/mesh.js` around lines 16 - 27, The capsule dimension math can still yield
zero/near-zero volumes for flat or thin meshes; clamp radius with a small
epsilon (e.g., const EPS = 1e-3) after computing radius and then ensure
adjustedHeight is at least 2 * radius + EPS before computing cylinderHeight.
Concretely: after computing radius in the mesh sizing block, set radius =
Math.max(radius, EPS); compute adjustedHeight = Math.max(0, height -
shrinkAmount) and then enforce adjustedHeight = Math.max(adjustedHeight, 2 *
radius + EPS) before computing cylinderHeight = adjustedHeight - 2 * radius (and
still Math.max(0, ...) if desired). Update the symbols radius, adjustedHeight,
shrinkAmount, and cylinderHeight accordingly.

When grounded, Havok applies small upward correction velocities to resolve
capsule-floor penetration. These were being preserved by clampedVertical and
causing visible bouncing, especially for short capsules (e.g. airplane) where
the bounce is large relative to capsule height. Characters were unaffected
because the same absolute bounce is negligible at their scale.

Fix: zero the vertical velocity when grounded and the upward speed is below
1.0 m/s (typical solver correction range). Velocities above 1.0 m/s are
preserved as deliberate upward movement (jumps, boosts).

https://claude.ai/code/session_019iH8iKivgCCsF3m8MpYcsc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants