openvibers/breakroomsimulator Commits
Source-style player-prop physics: real pushing, ground drag, no more weird shoves
Changes
@@@ -2045,7 +2045,7 @@ function start(token, user) {input.jumpQueued = false;if (carState.driving || carState.riding) { /* Space is the handbrake while driving */ }else if (mg.myPongTable && !my.seat) mg.swing();-- else if (my.onGround && !my.seat) { my.vy = my.crouch ? 4.2 : 5.4; my.onGround = false; my.crouch = false; beep(520, .04, 'sine', .05); }++ else if (my.onGround && !my.seat) { my.vy = my.crouch ? 4.6 : 6.1; my.onGround = false; my.crouch = false; beep(520, .04, 'sine', .05); } // source-height: a crate is jumpableelse if (my.seat) standUp();}@@@ -2103,6 +2103,10 @@ function start(token, user) {my.vel.x += wx * add; my.vel.z += wz * add;}let nx = my.x + my.vel.x * dt, nz = my.z + my.vel.z * dt;++ // pre-clip velocity: the shove force on props must survive wall-clipping++ // (walking into a blocking crate zeroes my.vel, but you're still pushing)++ my.wishVel = my.wishVel || { x: 0, z: 0 };++ my.wishVel.x = my.vel.x; my.wishVel.z = my.vel.z;W.feetY = my.y; // surface-probing colliders judge "climbable" from foot height[nx, nz] = resolveCollisions(nx, nz, .34);[nx, nz] = phys.resolvePlayer(nx, nz, my.y, .34, pgState.held); // props shove people@@@ -2121,14 +2125,26 @@ 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 gY = phys.groundAt(my.x, my.z, my.y);-- if (my.onGround && gY > my.y && gY - my.y <= .68) { my.y = gY; my.vy = 0; } // step/ramp up++ const grd = phys.groundAt(my.x, my.z, my.y);++ const gY = grd.y;++ if (my.onGround && gY > my.y && gY - my.y <= .5) { my.y = gY; my.vy = 0; } // step/ramp upmy.vy -= 16 * dt;my.y += my.vy * dt;if (my.y <= gY + 1e-4 && my.vy <= 0) {-- if (!my.onGround && my.vy < -5) beep(140, .05, 'sine', .05); // landing thud++ if (!my.onGround) { // touchdown++ if (my.vy < -5) beep(140, .05, 'sine', .05); // landing thud++ if (my.vy < -3) phys.stomp(grd.e, my.vy); // press the prop you landed on++ }my.y = gY; my.vy = 0; my.onGround = true;} else if (my.y > gY + .03) my.onGround = false;++ // 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;++ if (Math.abs(bv.x) + Math.abs(bv.z) > .02) {++ my.x += bv.x * dt;++ my.z += bv.z * dt;++ }++ }// walked away from pong table?if (mg.myPongTable) {// anchors from the rotated cafeteria group carry world coords in wx/wz@@@ -179,10 +179,17 @@ export function initPhysics(scene) {const world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.81, 0) });world.broadphase = new CANNON.SAPBroadphase(world);world.allowSleep = true;++ // crisper contacts: more solver iterations + tighter tolerance = stable++ // stacks and less spongy pushing (the "havok" feel is mostly solver quality)++ world.solver.iterations = 14;++ world.solver.tolerance = .01;const groundMat = new CANNON.Material('ground');const propMat = new CANNON.Material('prop');const bouncyMat = new CANNON.Material('bouncy');-- world.addContactMaterial(new CANNON.ContactMaterial(groundMat, propMat, { friction: .45, restitution: .25 }));++ // ground-prop friction is EXACTLY 0, like the cars: cannon's friction solver++ // freezes heavy resting bodies and lets light ones ice-skate — believable++ // sliding friction is applied manually in step() instead++ world.addContactMaterial(new CANNON.ContactMaterial(groundMat, propMat, { friction: 0, restitution: .25 }));world.addContactMaterial(new CANNON.ContactMaterial(propMat, propMat, { friction: .4, restitution: .3 }));world.addContactMaterial(new CANNON.ContactMaterial(groundMat, bouncyMat, { friction: .3, restitution: .72 }));world.addContactMaterial(new CANNON.ContactMaterial(propMat, bouncyMat, { friction: .3, restitution: .6 }));@@@ -259,22 +266,31 @@ export function initPhysics(scene) {wRay.intersectBody(body, wRes);return wRes.hasHit ? wRes.hitPointWorld.y : null; // null: (x,z) misses the actual shape}-- const STEP = .68; // max walk-up height — one box, not one crate-- // highest walkable surface under (x,z) reachable from foot height py++ const STEP = .5; // max walk-up height (~source 18u): stairs/ramps yes, boxes no — jump those++ // 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) {-- let g = 0;-- const consider = (body) => {++ let g = 0, ge = null;++ const consider = (body, e) => {body.updateAABB();const bb = body.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.lowerBound.y > py + STEP + 1.2) return; // floating way overheadconst h = surfaceYAt(body, x, z);-- if (h != null && h <= py + STEP && h > g) g = h;++ if (h != null && h <= py + STEP && h > g) { g = h; ge = e; }};-- for (const e of props.values()) consider(e.body);-- for (const e of cars.values()) consider(e.body);-- return g;++ 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 };++ }++ // landing on a physics prop presses it down — the source-engine thud++ function stomp(e, vy) {++ if (!e || e.frozen || e.isCar) return;++ if (e.grabbedBy && e.grabbedBy !== myId) return;++ claim(e);++ e.body.applyImpulse(V(0, Math.max(-40, vy * Math.min(e.body.mass, 6) * .55), 0));}// players are solid: shove the walking capsule out of dynamic props so a// physgunned crate PUSHES people instead of clipping through them@@@ -481,21 +497,37 @@ export function initPhysics(scene) {});}-- // shove props out of the way as you walk through them (and take ownership)-- function playerPush(px, py, pz, dt) {++ // shove props out of the way as you walk into them (and take ownership).++ // Source-style rules: only while GROUNDED and actually moving, never a prop++ // you're landing on or standing on, force scales inversely with mass — a++ // box skids away, a heavy crate mostly stands its ground.++ function playerPush(p, dt) {++ if (!p.onGround) return; // no mid-air kicking: jumps land ON props now++ const wv = p.wishVel || p.vel || { x: 0, z: 0 };++ const spd = Math.hypot(wv.x || 0, wv.z || 0);++ if (spd < .4) return; // standing still shouldn't vibrate the furniture++ const px = p.x, py = p.y, pz = p.z;for (const e of props.values()) {if (e.frozen) continue;if (e.grabbedBy && e.grabbedBy !== myId) continue; // don't fight the holder's streamconst b = e.body;-- if (py > b.aabb.upperBound.y - .25) continue; // standing on it, not into it++ if (py > b.aabb.upperBound.y - .45) continue; // landing zone / standing on itconst dx = b.position.x - px, dz = b.position.z - pz;const d = Math.hypot(dx, dz);const rad = .34 + (e.kind === 'crate' ? .62 : .42);if (d < rad && b.position.y < py + 1.6) {-- const nx = d > .01 ? dx / d : 1, nz = d > .01 ? dz / d : 0;++ const dot = (dx * wv.x + dz * wv.z) / ((d || .01) * spd);++ if (dot < .25) continue; // only props you're walking INTOclaim(e);-- // impulse at the center of mass (a relative point would add torque)-- b.applyImpulse(new CANNON.Vec3(nx * 3.2 * dt * 60 * .12, .3, nz * 3.2 * dt * 60 * .12));++ // velocity-space push (same trick as the cars): cannon's friction++ // solver eats forces AND small impulses on ground-resting bodies, so++ // we steer the velocity directly — boxes skid ahead of you, heavy++ // crates creep, and the solver still owns collisions and toppling++ const tv = Math.min(spd, 4.8) * Math.min(1, 4 / b.mass) * .75;++ const k = Math.min(1, dt * 14);++ b.velocity.x += ((wv.x / spd) * tv - b.velocity.x) * k;++ b.velocity.z += ((wv.z / spd) * tv - b.velocity.z) * k;++ e.pushT = performance.now(); // ground drag stands down while actively pushed}}}@@@ -542,8 +574,8 @@ export function initPhysics(scene) {e.body.wakeUp();}}-- if (playerPos) playerPush(playerPos.x, playerPos.y, playerPos.z, dt);-- world.step(1 / 60, dt, 2);++ if (playerPos) playerPush(playerPos, dt);++ world.step(1 / 60, dt, 3);// cars: visual group, walking collider, and E-prompt follow the bodyfor (const e of cars.values()) {const b = e.body, car = e.car;@@@ -581,6 +613,23 @@ export function initPhysics(scene) {b.velocity.set(0, 0, 0);b.angularVelocity.set(0, 0, 0);}++ // depenetration insurance: with zero ground friction, a prop expelled++ // from inside geometry can be launched — clamp runaway horizontal speed++ {++ const hv = Math.hypot(b.velocity.x, b.velocity.z);++ if (hv > 24) { b.velocity.x *= 24 / hv; b.velocity.z *= 24 / hv; }++ }++ // manual sliding friction: cannon's friction is weak on light bodies++ // (they ice-skate) and sticky on heavy ones — apply believable ground++ // drag ourselves. Balls keep rolling; held props are the holder's problem.++ if (!e.frozen && !e.grabbedBy && b.sleepState !== CANNON.Body.SLEEPING &&++ Math.abs(b.velocity.y) < .7 && b.position.y < 1.4 &&++ (!e.pushT || performance.now() - e.pushT > 130)) {++ const drag = (e.kind === 'ball' || e.kind === 'melon') ? .55 : .06;++ const f = Math.pow(drag, dt);++ b.velocity.x *= f;++ b.velocity.z *= f;++ }// interpolated pose: fixed-step positions quantize to 60Hz and read as// chop against an unaligned rAF — cannon keeps smooth in-between valuese.mesh.position.copy(b.interpolatedPosition);@@@ -684,7 +733,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, PHYS_KINDS,++ addStatic, removeStatic, setFrozen, addCar, groundAt, resolvePlayer, stomp, PHYS_KINDS,setMyId(id) { myId = id; },};}@@@ -3056,7 +3056,7 @@ export function resolveCollisions(x, z, r) {// Probe at the contact point on the box, not the player center.if (c.hAt && x >= c.x0 - .34 && x <= c.x1 + .34 && z >= c.z0 - .34 && z <= c.z1 + .34) {const h = c.hAt(Math.max(c.x0, Math.min(x, c.x1)), Math.max(c.z0, Math.min(z, c.z1)));-- if (h == null || h - (W.feetY || 0) <= .68) continue;++ if (h == null || h - (W.feetY || 0) <= .5) continue;}const nx = Math.max(c.x0, Math.min(x, c.x1));const nz = Math.max(c.z0, Math.min(z, c.z1));
@@ -2045,7 +2045,7 @@ function start(token, user) {
@@ -2045,7 +2045,7 @@ function start(token, user) {
input.jumpQueued = false;
input.jumpQueued = false;
if (carState.driving || carState.riding) { /* Space is the handbrake while driving */ }
if (carState.driving || carState.riding) { /* Space is the handbrake while driving */ }
else if (mg.myPongTable && !my.seat) mg.swing();
else if (mg.myPongTable && !my.seat) mg.swing();
- else if (my.onGround && !my.seat) { my.vy = my.crouch ? 4.2 : 5.4; my.onGround = false; my.crouch = false; beep(520, .04, 'sine', .05); }
+ else if (my.onGround && !my.seat) { my.vy = my.crouch ? 4.6 : 6.1; my.onGround = false; my.crouch = false; beep(520, .04, 'sine', .05); } // source-height: a crate is jumpable
else if (my.seat) standUp();
else if (my.seat) standUp();
}
}
@@ -2103,6 +2103,10 @@ function start(token, user) {
@@ -2103,6 +2103,10 @@ function start(token, user) {
my.vel.x += wx * add; my.vel.z += wz * add;
my.vel.x += wx * add; my.vel.z += wz * add;
}
}
let nx = my.x + my.vel.x * dt, nz = my.z + my.vel.z * dt;
let nx = my.x + my.vel.x * dt, nz = my.z + my.vel.z * dt;
+ // pre-clip velocity: the shove force on props must survive wall-clipping
+ // (walking into a blocking crate zeroes my.vel, but you're still pushing)
+ my.wishVel = my.wishVel || { x: 0, z: 0 };
+ my.wishVel.x = my.vel.x; my.wishVel.z = my.vel.z;
W.feetY = my.y; // surface-probing colliders judge "climbable" from foot height
W.feetY = my.y; // surface-probing colliders judge "climbable" from foot height
[nx, nz] = resolveCollisions(nx, nz, .34);
[nx, nz] = resolveCollisions(nx, nz, .34);
[nx, nz] = phys.resolvePlayer(nx, nz, my.y, .34, pgState.held); // props shove people
[nx, nz] = phys.resolvePlayer(nx, nz, my.y, .34, pgState.held); // props shove people
@@ -2121,14 +2125,26 @@ function start(token, user) {
@@ -2121,14 +2125,26 @@ function start(token, user) {
: hsp > 5.2 ? 'run' : hsp > .5 ? 'walk' : 'idle';
: hsp > 5.2 ? 'run' : hsp > .5 ? 'walk' : 'idle';
// gravity — against the walkable surface under our feet (props, ramps,
// gravity — against the walkable surface under our feet (props, ramps,
// car roofs), not just the world floor
// car roofs), not just the world floor
- const gY = phys.groundAt(my.x, my.z, my.y);
- if (my.onGround && gY > my.y && gY - my.y <= .68) { my.y = gY; my.vy = 0; } // step/ramp up
+ const grd = phys.groundAt(my.x, my.z, my.y);
+ 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;
my.vy -= 16 * dt;
my.y += my.vy * dt;
my.y += my.vy * dt;
if (my.y <= gY + 1e-4 && my.vy <= 0) {
if (my.y <= gY + 1e-4 && my.vy <= 0) {
- if (!my.onGround && my.vy < -5) beep(140, .05, 'sine', .05); // landing thud
+ if (!my.onGround) { // touchdown
+ if (my.vy < -5) beep(140, .05, 'sine', .05); // landing thud
+ if (my.vy < -3) phys.stomp(grd.e, my.vy); // press the prop you landed on
+ }
my.y = gY; my.vy = 0; my.onGround = true;
my.y = gY; my.vy = 0; my.onGround = true;
} else if (my.y > gY + .03) my.onGround = false;
} else if (my.y > gY + .03) my.onGround = false;
+ // 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;
+ if (Math.abs(bv.x) + Math.abs(bv.z) > .02) {
+ my.x += bv.x * dt;
+ my.z += bv.z * dt;
+ }
+ }
// walked away from pong table?
// walked away from pong table?
if (mg.myPongTable) {
if (mg.myPongTable) {
// anchors from the rotated cafeteria group carry world coords in wx/wz
// anchors from the rotated cafeteria group carry world coords in wx/wz
@@ -179,10 +179,17 @@ export function initPhysics(scene) {
@@ -179,10 +179,17 @@ export function initPhysics(scene) {
const world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.81, 0) });
const world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.81, 0) });
world.broadphase = new CANNON.SAPBroadphase(world);
world.broadphase = new CANNON.SAPBroadphase(world);
world.allowSleep = true;
world.allowSleep = true;
+ // crisper contacts: more solver iterations + tighter tolerance = stable
+ // stacks and less spongy pushing (the "havok" feel is mostly solver quality)
+ world.solver.iterations = 14;
+ world.solver.tolerance = .01;
const groundMat = new CANNON.Material('ground');
const groundMat = new CANNON.Material('ground');
const propMat = new CANNON.Material('prop');
const propMat = new CANNON.Material('prop');
const bouncyMat = new CANNON.Material('bouncy');
const bouncyMat = new CANNON.Material('bouncy');
- world.addContactMaterial(new CANNON.ContactMaterial(groundMat, propMat, { friction: .45, restitution: .25 }));
+ // ground-prop friction is EXACTLY 0, like the cars: cannon's friction solver
+ // freezes heavy resting bodies and lets light ones ice-skate — believable
+ // sliding friction is applied manually in step() instead
+ world.addContactMaterial(new CANNON.ContactMaterial(groundMat, propMat, { friction: 0, restitution: .25 }));
world.addContactMaterial(new CANNON.ContactMaterial(propMat, propMat, { friction: .4, restitution: .3 }));
world.addContactMaterial(new CANNON.ContactMaterial(propMat, propMat, { friction: .4, restitution: .3 }));
world.addContactMaterial(new CANNON.ContactMaterial(groundMat, bouncyMat, { friction: .3, restitution: .72 }));
world.addContactMaterial(new CANNON.ContactMaterial(groundMat, bouncyMat, { friction: .3, restitution: .72 }));
world.addContactMaterial(new CANNON.ContactMaterial(propMat, bouncyMat, { friction: .3, restitution: .6 }));
world.addContactMaterial(new CANNON.ContactMaterial(propMat, bouncyMat, { friction: .3, restitution: .6 }));
@@ -259,22 +266,31 @@ export function initPhysics(scene) {
@@ -259,22 +266,31 @@ export function initPhysics(scene) {
wRay.intersectBody(body, wRes);
wRay.intersectBody(body, wRes);
return wRes.hasHit ? wRes.hitPointWorld.y : null; // null: (x,z) misses the actual shape
return wRes.hasHit ? wRes.hitPointWorld.y : null; // null: (x,z) misses the actual shape
}
}
- const STEP = .68; // max walk-up height — one box, not one crate
- // highest walkable surface under (x,z) reachable from foot height py
+ const STEP = .5; // max walk-up height (~source 18u): stairs/ramps yes, boxes no — jump those
+ // 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) {
- let g = 0;
- const consider = (body) => {
+ let g = 0, ge = null;
+ const consider = (body, e) => {
body.updateAABB();
body.updateAABB();
const bb = body.aabb;
const bb = body.aabb;
if (x < bb.lowerBound.x - .05 || x > bb.upperBound.x + .05 ||
if (x < bb.lowerBound.x - .05 || x > bb.upperBound.x + .05 ||
z < bb.lowerBound.z - .05 || z > bb.upperBound.z + .05) return;
z < bb.lowerBound.z - .05 || z > bb.upperBound.z + .05) return;
if (bb.lowerBound.y > py + STEP + 1.2) return; // floating way overhead
if (bb.lowerBound.y > py + STEP + 1.2) return; // floating way overhead
const h = surfaceYAt(body, x, z);
const h = surfaceYAt(body, x, z);
- if (h != null && h <= py + STEP && h > g) g = h;
+ if (h != null && h <= py + STEP && h > g) { g = h; ge = e; }
};
};
- for (const e of props.values()) consider(e.body);
- for (const e of cars.values()) consider(e.body);
- return g;
+ 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 };
+ }
+ // landing on a physics prop presses it down — the source-engine thud
+ function stomp(e, vy) {
+ if (!e || e.frozen || e.isCar) return;
+ if (e.grabbedBy && e.grabbedBy !== myId) return;
+ claim(e);
+ e.body.applyImpulse(V(0, Math.max(-40, vy * Math.min(e.body.mass, 6) * .55), 0));
}
}
// players are solid: shove the walking capsule out of dynamic props so a
// players are solid: shove the walking capsule out of dynamic props so a
// physgunned crate PUSHES people instead of clipping through them
// physgunned crate PUSHES people instead of clipping through them
@@ -481,21 +497,37 @@ export function initPhysics(scene) {
@@ -481,21 +497,37 @@ export function initPhysics(scene) {
});
});
}
}
- // shove props out of the way as you walk through them (and take ownership)
- function playerPush(px, py, pz, dt) {
+ // shove props out of the way as you walk into them (and take ownership).
+ // Source-style rules: only while GROUNDED and actually moving, never a prop
+ // you're landing on or standing on, force scales inversely with mass — a
+ // box skids away, a heavy crate mostly stands its ground.
+ function playerPush(p, dt) {
+ if (!p.onGround) return; // no mid-air kicking: jumps land ON props now
+ const wv = p.wishVel || p.vel || { x: 0, z: 0 };
+ const spd = Math.hypot(wv.x || 0, wv.z || 0);
+ if (spd < .4) return; // standing still shouldn't vibrate the furniture
+ const px = p.x, py = p.y, pz = p.z;
for (const e of props.values()) {
for (const e of props.values()) {
if (e.frozen) continue;
if (e.frozen) continue;
if (e.grabbedBy && e.grabbedBy !== myId) continue; // don't fight the holder's stream
if (e.grabbedBy && e.grabbedBy !== myId) continue; // don't fight the holder's stream
const b = e.body;
const b = e.body;
- if (py > b.aabb.upperBound.y - .25) continue; // standing on it, not into it
+ if (py > b.aabb.upperBound.y - .45) continue; // landing zone / standing on it
const dx = b.position.x - px, dz = b.position.z - pz;
const dx = b.position.x - px, dz = b.position.z - pz;
const d = Math.hypot(dx, dz);
const d = Math.hypot(dx, dz);
const rad = .34 + (e.kind === 'crate' ? .62 : .42);
const rad = .34 + (e.kind === 'crate' ? .62 : .42);
if (d < rad && b.position.y < py + 1.6) {
if (d < rad && b.position.y < py + 1.6) {
- const nx = d > .01 ? dx / d : 1, nz = d > .01 ? dz / d : 0;
+ const dot = (dx * wv.x + dz * wv.z) / ((d || .01) * spd);
+ if (dot < .25) continue; // only props you're walking INTO
claim(e);
claim(e);
- // impulse at the center of mass (a relative point would add torque)
- b.applyImpulse(new CANNON.Vec3(nx * 3.2 * dt * 60 * .12, .3, nz * 3.2 * dt * 60 * .12));
+ // velocity-space push (same trick as the cars): cannon's friction
+ // solver eats forces AND small impulses on ground-resting bodies, so
+ // we steer the velocity directly — boxes skid ahead of you, heavy
+ // crates creep, and the solver still owns collisions and toppling
+ const tv = Math.min(spd, 4.8) * Math.min(1, 4 / b.mass) * .75;
+ const k = Math.min(1, dt * 14);
+ b.velocity.x += ((wv.x / spd) * tv - b.velocity.x) * k;
+ b.velocity.z += ((wv.z / spd) * tv - b.velocity.z) * k;
+ e.pushT = performance.now(); // ground drag stands down while actively pushed
}
}
}
}
}
}
@@ -542,8 +574,8 @@ export function initPhysics(scene) {
@@ -542,8 +574,8 @@ export function initPhysics(scene) {
e.body.wakeUp();
e.body.wakeUp();
}
}
}
}
- if (playerPos) playerPush(playerPos.x, playerPos.y, playerPos.z, dt);
- world.step(1 / 60, dt, 2);
+ if (playerPos) playerPush(playerPos, dt);
+ world.step(1 / 60, dt, 3);
// cars: visual group, walking collider, and E-prompt follow the body
// cars: visual group, walking collider, and E-prompt follow the body
for (const e of cars.values()) {
for (const e of cars.values()) {
const b = e.body, car = e.car;
const b = e.body, car = e.car;
@@ -581,6 +613,23 @@ export function initPhysics(scene) {
@@ -581,6 +613,23 @@ export function initPhysics(scene) {
b.velocity.set(0, 0, 0);
b.velocity.set(0, 0, 0);
b.angularVelocity.set(0, 0, 0);
b.angularVelocity.set(0, 0, 0);
}
}
+ // depenetration insurance: with zero ground friction, a prop expelled
+ // from inside geometry can be launched — clamp runaway horizontal speed
+ {
+ const hv = Math.hypot(b.velocity.x, b.velocity.z);
+ if (hv > 24) { b.velocity.x *= 24 / hv; b.velocity.z *= 24 / hv; }
+ }
+ // manual sliding friction: cannon's friction is weak on light bodies
+ // (they ice-skate) and sticky on heavy ones — apply believable ground
+ // drag ourselves. Balls keep rolling; held props are the holder's problem.
+ if (!e.frozen && !e.grabbedBy && b.sleepState !== CANNON.Body.SLEEPING &&
+ Math.abs(b.velocity.y) < .7 && b.position.y < 1.4 &&
+ (!e.pushT || performance.now() - e.pushT > 130)) {
+ const drag = (e.kind === 'ball' || e.kind === 'melon') ? .55 : .06;
+ const f = Math.pow(drag, dt);
+ b.velocity.x *= f;
+ b.velocity.z *= f;
+ }
// interpolated pose: fixed-step positions quantize to 60Hz and read as
// interpolated pose: fixed-step positions quantize to 60Hz and read as
// chop against an unaligned rAF — cannon keeps smooth in-between values
// chop against an unaligned rAF — cannon keeps smooth in-between values
e.mesh.position.copy(b.interpolatedPosition);
e.mesh.position.copy(b.interpolatedPosition);
@@ -684,7 +733,7 @@ export function initPhysics(scene) {
@@ -684,7 +733,7 @@ export function initPhysics(scene) {
return {
return {
world, props, cars, add, remove, claim, applyState, applyCarState, sendState, sendCarState,
world, props, cars, add, remove, claim, applyState, applyCarState, sendState, sendCarState,
drive, step, smack, raycast, yawBody, rotateBody, setQuatAnchored, grabLocal, anchorWorld,
drive, step, smack, raycast, yawBody, rotateBody, setQuatAnchored, grabLocal, anchorWorld,
- addStatic, removeStatic, setFrozen, addCar, groundAt, resolvePlayer, PHYS_KINDS,
+ addStatic, removeStatic, setFrozen, addCar, groundAt, resolvePlayer, stomp, PHYS_KINDS,
setMyId(id) { myId = id; },
setMyId(id) { myId = id; },
};
};
}
}
@@ -3056,7 +3056,7 @@ export function resolveCollisions(x, z, r) {
@@ -3056,7 +3056,7 @@ export function resolveCollisions(x, z, r) {
// Probe at the contact point on the box, not the player center.
// Probe at the contact point on the box, not the player center.
if (c.hAt && x >= c.x0 - .34 && x <= c.x1 + .34 && z >= c.z0 - .34 && z <= c.z1 + .34) {
if (c.hAt && x >= c.x0 - .34 && x <= c.x1 + .34 && z >= c.z0 - .34 && z <= c.z1 + .34) {
const h = c.hAt(Math.max(c.x0, Math.min(x, c.x1)), Math.max(c.z0, Math.min(z, c.z1)));
const h = c.hAt(Math.max(c.x0, Math.min(x, c.x1)), Math.max(c.z0, Math.min(z, c.z1)));
- if (h == null || h - (W.feetY || 0) <= .68) continue;
+ if (h == null || h - (W.feetY || 0) <= .5) continue;
}
}
const nx = Math.max(c.x0, Math.min(x, c.x1));
const nx = Math.max(c.x0, Math.min(x, c.x1));
const nz = Math.max(c.z0, Math.min(z, c.z1));
const nz = Math.max(c.z0, Math.min(z, c.z1));