openvibers/breakroomsimulator Commits
Fix sideways car hitbox, bright 24/7 interiors at night, dropped-item float/lay-flat
Changes
@@@ -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 noonconst 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;@@@ -589,15 +589,30 @@ function start(token, user) {function dropItemInWorld(id, n = 1) {if (nearFire()) { burnItems(id, n); return; } // dropped into the barrelconst 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 viewtorch.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();@@@ -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;@@@ -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 elseconst 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 : [];
@@ -79,7 +79,22 @@ export function initLighting(scene, renderer, shadows = true) {
@@ -79,7 +79,22 @@ export function initLighting(scene, renderer, shadows = true) {
const sky = new THREE.Color();
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); };
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 t = timeOfDay();
const elev = -Math.cos(t * Math.PI * 2); // -1 midnight … +1 noon
const elev = -Math.cos(t * Math.PI * 2); // -1 midnight … +1 noon
const az = t * Math.PI * 2;
const az = t * Math.PI * 2;
@@ -99,10 +114,11 @@ export function initLighting(scene, renderer, shadows = true) {
@@ -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.position.set(px - Math.sin(az) * 70, Math.max(10, -elev * 80), pz - Math.cos(az) * 35);
moon.target.position.set(px, 0, pz);
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;
stars.material.opacity = (1 - d) * .95;
@@ -589,15 +589,30 @@ function start(token, user) {
@@ -589,15 +589,30 @@ function start(token, user) {
function dropItemInWorld(id, n = 1) {
function dropItemInWorld(id, n = 1) {
if (nearFire()) { burnItems(id, n); return; } // dropped into the barrel
if (nearFire()) { burnItems(id, n); return; } // dropped into the barrel
const fx = -Math.sin(my.yaw), fz = -Math.cos(my.yaw);
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) {
function addWorldDrop(d) {
removeWorldDrop(d.id);
removeWorldDrop(d.id);
- const mesh = buildHeldMesh(d.item);
- if (!mesh) return;
+ const held = buildHeldMesh(d.item);
+ if (!held) return;
const group = new THREE.Group();
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);
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 }));
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.rotation.x = -Math.PI / 2;
sh.position.y = .012;
sh.position.y = .012;
@@ -608,7 +623,7 @@ function start(token, user) {
@@ -608,7 +623,7 @@ function start(token, user) {
const def = ITEMS[d.item];
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 } };
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);
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) {
function removeWorldDrop(id) {
const e = worldDrops.get(id);
const e = worldDrops.get(id);
@@ -1384,7 +1399,7 @@ function start(token, user) {
@@ -1384,7 +1399,7 @@ function start(token, user) {
// flashlight follows your view
// flashlight follows your view
torch.intensity = my.held === 'flashlight' ? .5 + (1 - (W.dayFactor ?? 1)) * 3.6 : 0;
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));
W.dynamic.fires?.forEach(f => f.update(dt));
scanInteractables();
scanInteractables();
@@ -148,8 +148,11 @@ export function initPhysics(scene) {
@@ -148,8 +148,11 @@ export function initPhysics(scene) {
mass: 180, material: carMat,
mass: 180, material: carMat,
angularDamping: .6, linearDamping: .12,
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.position.set(car.x, CAR_HH + .05, car.z);
body.quaternion.setFromEuler(0, car.ry, 0);
body.quaternion.setFromEuler(0, car.ry, 0);
body.sleepSpeedLimit = .35;
body.sleepSpeedLimit = .35;
@@ -106,6 +106,9 @@ for (const [cid, c] of Object.entries(savedWorld.cars || {})) {
@@ -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;
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
// 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}
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;
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.
// permanent marker tags — the "I was here" layer. They outlive everyone.
const tags = Array.isArray(savedWorld.tags) ? savedWorld.tags : [];
const tags = Array.isArray(savedWorld.tags) ? savedWorld.tags : [];