openvibers/breakroomsimulator Commits

Fix sideways car hitbox, bright 24/7 interiors at night, dropped-item float/lay-flat

OP OpenVibers committed 28 Jul 2026 · 2c202513f2 · 4 files, +50 −13

- The car chassis cannon box had its LENGTH on the X axis, but cars are
  modeled long-axis-Z — the physics hitbox has been 90 degrees across the
  car since rigid bodies landed (masked at spawn because parked cars face
  +X). Chassis and cabin boxes now width-x / length-z.
- Bug46/47: when the player is inside the facility (cafeteria, security,
  lockers, hallway, bathrooms rects), hemisphere/ambient hold ~82% of
  daytime level regardless of hour — the 24/7 fluorescents. Sun, sky, fog,
  stars and outdoor lamps unaffected; step outside and it's still night.
- Dropped items: held-item meshes are modeled around the hand grip (a broom
  extends ~1m below origin) so drops sank through the floor. Drops now lay
  tall items flat, float the bounding box just above ground, and the drop
  point is collision-resolved so it can't land inside a wall. Server sweeps
  any out-of-world drops at load.

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

Changes

ddiff --git a/public/js/lighting.js b/public/js/lighting.js
iindex aee2388..2ad8d34 100644
---- a/public/js/lighting.js
++++ b/public/js/lighting.js
@@@ -79,7 +79,22 @@ export function initLighting(scene, renderer, shadows = true) {
const sky = new THREE.Color();
const smooth = (a, b, x) => { const k = Math.max(0, Math.min(1, (x - a) / (b - a))); return k * k * (3 - 2 * k); };
-- function update(px, pz) {
++ // the facility runs 24/7: when the player is inside, ambient light stays
++ // near daytime levels no matter the hour (Bug46/47 — interiors went black).
++ // Outside the windows it's still night: sun, sky, fog and stars are
++ // untouched by this.
++ const INDOORS = [
++ [58, 92, 31, 107], // cafeteria / games / market
++ [62, 92, -17, 17], // security lobby
++ [58, 92, 17, 31], // locker hall
++ [52.5, 58, 39, 99], // service hallway
++ [48, 53, 63, 82], // bathrooms
++ ];
++ let indoorK = 0;
++
++ function update(dt, px, pz) {
++ const inside = INDOORS.some(([x0, x1, z0, z1]) => px >= x0 && px <= x1 && pz >= z0 && pz <= z1) ? 1 : 0;
++ indoorK += (inside - indoorK) * Math.min(1, (dt || .016) * 4);
const t = timeOfDay();
const elev = -Math.cos(t * Math.PI * 2); // -1 midnight … +1 noon
const az = t * Math.PI * 2;
@@@ -99,10 +114,11 @@ export function initLighting(scene, renderer, shadows = true) {
moon.position.set(px - Math.sin(az) * 70, Math.max(10, -elev * 80), pz - Math.cos(az) * 35);
moon.target.position.set(px, 0, pz);
-- hemi.intensity = .3 + .75 * d;
-- hemi.color.copy(hemiNight).lerp(hemiDay, d);
-- hemi.groundColor.copy(gndNight).lerp(gndDay, d);
-- amb.intensity = .08 + .16 * d;
++ const dl = Math.max(d, indoorK * .82); // indoor light floor: fluorescents
++ hemi.intensity = .3 + .75 * dl;
++ hemi.color.copy(hemiNight).lerp(hemiDay, dl);
++ hemi.groundColor.copy(gndNight).lerp(gndDay, dl);
++ amb.intensity = .08 + .16 * dl;
stars.material.opacity = (1 - d) * .95;
ddiff --git a/public/js/main.js b/public/js/main.js
iindex ed6ecf9..7ee5b32 100644
---- a/public/js/main.js
++++ b/public/js/main.js
@@@ -589,15 +589,30 @@ function start(token, user) {
function dropItemInWorld(id, n = 1) {
if (nearFire()) { burnItems(id, n); return; } // dropped into the barrel
const fx = -Math.sin(my.yaw), fz = -Math.cos(my.yaw);
-- net.send({ t: 'drop', op: 'add', item: id, n, x: +(my.x + fx * 1.3).toFixed(2), y: .35, z: +(my.z + fz * 1.3).toFixed(2) });
++ // resolve against the walls so a drop can't land inside (or beyond) one
++ const [dx2, dz2] = resolveCollisions(my.x + fx * 1.3, my.z + fz * 1.3, .3);
++ net.send({ t: 'drop', op: 'add', item: id, n, x: +dx2.toFixed(2), y: .35, z: +dz2.toFixed(2) });
}
function addWorldDrop(d) {
removeWorldDrop(d.id);
-- const mesh = buildHeldMesh(d.item);
-- if (!mesh) return;
++ const held = buildHeldMesh(d.item);
++ if (!held) return;
const group = new THREE.Group();
-- mesh.scale.setScalar(1.4);
++ const mesh = new THREE.Group(); // spin/bob pivot
++ held.scale.setScalar(1.4);
++ mesh.add(held);
group.add(mesh);
++ // held-item meshes are modeled around the hand grip — a broom extends a
++ // meter BELOW its origin, so dropped ones sank through the floor (or,
++ // lifted naively, hovered head-high). Lay tall items flat, then float
++ // the bounding box just above the ground.
++ let bb = new THREE.Box3().setFromObject(held);
++ if (bb.max.y - bb.min.y > .7) {
++ held.rotation.x = Math.PI / 2;
++ held.updateMatrixWorld(true);
++ bb = new THREE.Box3().setFromObject(held);
++ }
++ const lift = Math.max(.28, .16 - bb.min.y);
const sh = new THREE.Mesh(new THREE.CircleGeometry(.16, 12), new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: .2, depthWrite: false }));
sh.rotation.x = -Math.PI / 2;
sh.position.y = .012;
@@@ -608,7 +623,7 @@ function start(token, user) {
const def = ITEMS[d.item];
const it = { id: `drop-${d.id}`, type: 'dropitem', x: d.x, z: d.z, r: 1.2, label: `Pick up ${def.icon} ${def.name}${d.n > 1 ? ' ×' + d.n : ''}`, data: { drop: d.id } };
W.interactables.push(it);
-- worldDrops.set(d.id, { group, mesh, inter: it, t: Math.random() * 6, baseY: Math.max(.3, d.y || .35) });
++ worldDrops.set(d.id, { group, mesh, inter: it, t: Math.random() * 6, baseY: lift });
}
function removeWorldDrop(id) {
const e = worldDrops.get(id);
@@@ -1384,7 +1399,7 @@ function start(token, user) {
// flashlight follows your view
torch.intensity = my.held === 'flashlight' ? .5 + (1 - (W.dayFactor ?? 1)) * 3.6 : 0;
-- daylight.update(my.x, my.z);
++ daylight.update(dt, my.x, my.z);
W.dynamic.fires?.forEach(f => f.update(dt));
scanInteractables();
ddiff --git a/public/js/physics.js b/public/js/physics.js
iindex 5b1acfe..123cab4 100644
---- a/public/js/physics.js
++++ b/public/js/physics.js
@@@ -148,8 +148,11 @@ export function initPhysics(scene) {
mass: 180, material: carMat,
angularDamping: .6, linearDamping: .12,
});
-- body.addShape(new CANNON.Box(new CANNON.Vec3(hl, CAR_HH, hw)));
-- body.addShape(new CANNON.Box(new CANNON.Vec3(hl * .52, .24, hw * .86)), new CANNON.Vec3(0, CAR_HH + .2, -.1));
++ // cars are long along local Z (forward): width on x, LENGTH on z — the
++ // box used to be built the other way round, leaving the hitbox 90° across
++ // the car
++ body.addShape(new CANNON.Box(new CANNON.Vec3(hw, CAR_HH, hl)));
++ body.addShape(new CANNON.Box(new CANNON.Vec3(hw * .86, .24, hl * .52)), new CANNON.Vec3(0, CAR_HH + .2, -.2));
body.position.set(car.x, CAR_HH + .05, car.z);
body.quaternion.setFromEuler(0, car.ry, 0);
body.sleepSpeedLimit = .35;
ddiff --git a/server/server.js b/server/server.js
iindex 4b0f9ab..0782137 100644
---- a/server/server.js
++++ b/server/server.js
@@@ -106,6 +106,9 @@ for (const [cid, c] of Object.entries(savedWorld.cars || {})) {
let nextPhysId = [...props.keys()].reduce((m, id) => Math.max(m, Number(String(id).slice(2)) || 0), 0) + 1;
// dropped items lying on the floor — minecraft style, persisted like all else
const drops = new Map(Object.entries(savedWorld.drops || {})); // id -> {id, item, n, x, y, z}
++for (const [id, d] of drops) { // sweep anything that ever ended up outside the world
++ if (!Number.isFinite(d?.x) || !Number.isFinite(d?.z) || !(d.y >= 0 && d.y <= 8)) drops.delete(id);
++}
let nextDropId = [...drops.keys()].reduce((m, id) => Math.max(m, Number(String(id).slice(1)) || 0), 0) + 1;
// permanent marker tags — the "I was here" layer. They outlive everyone.
const tags = Array.isArray(savedWorld.tags) ? savedWorld.tags : [];