openvibers/breakroomsimulator Commits

Bug55: ceiling collision — heads bonk on overhead geometry instead of clipping in

OP OpenVibers committed 31 Jul 2026 · 802ef54203 · 2 files, +48 −4

- phys.ceilingAt: lowest overhead underside above the feet (frozen + dynamic
  props + cars, fading doors excluded); movement clamps head height against it
  and zeroes upward velocity with a *donk* — jumping into a slanted panel now
  stops flush at the surface like Source
- groundAt guards against acquiring a surface approached from below while
  AIRBORNE (the thin-slab jump-warp); grounded step-ups onto low ledges are
  exempt since the ceiling clamp makes the grounded warp impossible
- Crouching lowers head clearance, so you can duck under lower pieces

Co-Authored-By: Claude Fable 5 <[email protected]>

Changes

ddiff --git a/public/js/main.js b/public/js/main.js
iindex 93f3315..0aaa5c4 100644
---- a/public/js/main.js
++++ b/public/js/main.js
@@@ -2170,7 +2170,7 @@ function start(token, user) {
: hsp > 5.2 ? 'run' : hsp > .5 ? 'walk' : 'idle';
// gravity — against the walkable surface under our feet (props, ramps,
// car roofs), not just the world floor
-- const grd = phys.groundAt(my.x, my.z, my.y);
++ const grd = phys.groundAt(my.x, my.z, my.y, !my.onGround);
const gY = grd.y;
if (my.onGround && gY > my.y && gY - my.y <= .5) { my.y = gY; my.vy = 0; } // step/ramp up
my.vy -= 16 * dt;
@@@ -2182,6 +2182,15 @@ function start(token, user) {
}
my.y = gY; my.vy = 0; my.onGround = true;
} else if (my.y > gY + .03) my.onGround = false;
++ // ceiling: heads bonk on overhead geometry instead of clipping into it
++ {
++ const ceilY = phys.ceilingAt(my.x, my.z, my.y);
++ const headH = my.crouch ? 1.25 : 1.72;
++ if (ceilY != null && my.y + headH > ceilY) {
++ my.y = Math.max(gY, ceilY - headH);
++ if (my.vy > 0) { my.vy = 0; beep(200, .04, 'square', .05); } // *donk*
++ }
++ }
// standing on a moving prop/car = moving platform: ride it (source-style)
if (my.onGround && grd.e && !grd.e.frozen) {
const bv = grd.e.body.velocity;
ddiff --git a/public/js/physics.js b/public/js/physics.js
iindex 1af7d57..cafb0e5 100644
---- a/public/js/physics.js
++++ b/public/js/physics.js
@@@ -270,7 +270,7 @@ export function initPhysics(scene) {
// highest walkable surface under (x,z) reachable from foot height py.
// Returns {y, e}: e is the entity you'd be standing on (null = world floor),
// so the walker can ride moving props like source-engine platforms.
-- function groundAt(x, z, py) {
++ function groundAt(x, z, py, airborne = false) {
let g = 0, ge = null;
const consider = (body, e) => {
body.updateAABB();
@@@ -279,12 +279,47 @@ export function initPhysics(scene) {
z < bb.lowerBound.z - .05 || z > bb.upperBound.z + .05) return;
if (bb.lowerBound.y > py + STEP + 1.2) return; // floating way overhead
const h = surfaceYAt(body, x, z);
-- if (h != null && h <= py + STEP && h > g) { g = h; ge = e; }
++ if (h == null || h > py + STEP || h <= g) return;
++ // approached from BELOW while airborne? if a downward-facing surface
++ // sits well above our feet, we're UNDER the piece — its top is not our
++ // ground (jumping beneath a thin slab used to warp you through onto it).
++ // Grounded step-ups skip this: the ceiling clamp already makes the
++ // grounded warp geometrically impossible, and thin ledges must step.
++ if (airborne && h - py > .3) {
++ wRay.from.set(x, py + .03, z);
++ wRay.to.set(x, h - .02, z);
++ wRes.reset();
++ wRay.intersectBody(body, wRes);
++ if (wRes.hasHit && wRes.hitPointWorld.y - py > .28) return;
++ }
++ g = h; ge = e;
};
for (const e of props.values()) consider(e.body, e);
for (const e of cars.values()) consider(e.body, e);
return { y: g, e: ge };
}
++ // lowest overhead surface (an underside) above the feet at (x,z), or null —
++ // source-style head collision: jumps BONK instead of clipping into geometry
++ function ceilingAt(x, z, feetY) {
++ let c = null;
++ const consider = (e) => {
++ const b = e.body;
++ if (!b.collisionResponse) return; // fading door
++ b.updateAABB();
++ const bb = b.aabb;
++ if (x < bb.lowerBound.x - .05 || x > bb.upperBound.x + .05 ||
++ z < bb.lowerBound.z - .05 || z > bb.upperBound.z + .05) return;
++ if (bb.upperBound.y <= feetY + .25 || bb.lowerBound.y > feetY + 2.5) return;
++ wRay.from.set(x, feetY + .25, z);
++ wRay.to.set(x, feetY + 2.5, z);
++ wRes.reset();
++ wRay.intersectBody(b, wRes);
++ if (wRes.hasHit && (c == null || wRes.hitPointWorld.y < c)) c = wRes.hitPointWorld.y;
++ };
++ for (const e of props.values()) consider(e);
++ for (const e of cars.values()) consider(e);
++ return c;
++ }
// landing on a physics prop presses it down — the source-engine thud
function stomp(e, vy) {
if (!e || e.frozen || e.isCar) return;
@@@ -793,7 +828,7 @@ export function initPhysics(scene) {
return {
world, props, cars, add, remove, claim, applyState, applyCarState, sendState, sendCarState,
drive, step, smack, raycast, yawBody, rotateBody, setQuatAnchored, grabLocal, anchorWorld,
-- addStatic, removeStatic, setFrozen, addCar, groundAt, resolvePlayer, clipMove, stomp, PHYS_KINDS,
++ addStatic, removeStatic, setFrozen, addCar, groundAt, ceilingAt, resolvePlayer, clipMove, stomp, PHYS_KINDS,
setMyId(id) { myId = id; },
};
}