openvibers/breakroomsimulator Commits

Centralize UI pointer-lock focus; seal exit-door bay and entrance transom (Bug30)

OP OpenVibers committed 27 Jul 2026 · daaaea591d · 5 files, +42 −4

- uiFocus manager in input.js: opening inventory/settings/editor releases the
  pointer so the cursor can click; closing the last panel re-locks mouse-look
- Canvas clicks no longer re-lock (or crosshair-fire) while a panel is open;
  wheel doesn't zoom/cycle over panels; Escape closes the top panel
- Full-height backing panel behind the games-corner emergency exit door (the
  bay was open above the 2.4m slab, showing the lit hallway through the wall)
- Glass transom over the sealed south entrance leaves (same open-band defect)

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

Changes

ddiff --git a/public/js/editor.js b/public/js/editor.js
iindex 2817879..0b7004a 100644
---- a/public/js/editor.js
++++ b/public/js/editor.js
@@@ -2,6 +2,7 @@
// All changes go through the server (admin-validated) and broadcast live.
import * as THREE from 'three';
import { net } from './net.js';
++import { uiFocus } from './input.js';
import { PROP_KINDS } from './props.js';
export function initEditor({ scene, camera, props, toast }) {
@@@ -56,7 +57,7 @@ export function initEditor({ scene, camera, props, toast }) {
ed.active = on;
panel.classList.toggle('hidden', !on);
btn.style.background = on ? '#ff9900' : '';
-- document.exitPointerLock?.();
++ uiFocus('editor', on);
if (!on) { stopPlacing(); select(null); }
toast(on ? '🛠️ Editor on — changes save to the server for everyone' : 'Editor off', 1800);
}
ddiff --git a/public/js/input.js b/public/js/input.js
iindex ee6773c..cac0acb 100644
---- a/public/js/input.js
++++ b/public/js/input.js
@@@ -8,11 +8,35 @@ export const input = {
isTouch: false,
locked: false,
chatOpen: false,
++ uiOpen: false, // any UI panel (inventory/settings/editor) open
onAction: null, // set by main
onTap: null, // world tap (for board clicks on mobile)
};
++// ---------- UI focus manager ----------
++// Any open panel releases the pointer so the cursor can click UI; closing the
++// last panel re-locks mouse-look if the player had it before (works from
++// keydown/click handlers, which carry the user gesture relock needs).
++let lockEl = null;
++const uiStack = new Set();
++let relock = false;
++export function uiFocus(id, open) {
++ const had = uiStack.size > 0;
++ open ? uiStack.add(id) : uiStack.delete(id);
++ input.uiOpen = uiStack.size > 0;
++ if (input.uiOpen && !had) {
++ relock = input.locked;
++ document.exitPointerLock?.();
++ } else if (!input.uiOpen && had) {
++ if (relock && !input.isTouch) {
++ try { lockEl?.requestPointerLock?.()?.catch?.(() => {}); } catch {}
++ }
++ relock = false;
++ }
++}
++
export function initInput(canvas) {
++ lockEl = canvas;
input.isTouch = navigator.maxTouchPoints > 0 || 'ontouchstart' in window
|| matchMedia('(any-pointer: coarse)').matches;
if (input.isTouch) document.body.classList.add('touch');
@@@ -34,7 +58,7 @@ export function initInput(canvas) {
// ---------- mouse (also active on hybrid touch devices) ----------
canvas.addEventListener('click', (e) => {
-- if (Date.now() - (input.lastTouch || 0) < 600 || input.chatOpen) return;
++ if (Date.now() - (input.lastTouch || 0) < 600 || input.chatOpen || input.uiOpen) return;
if (!input.locked) canvas.requestPointerLock?.();
else if (input.onTap) input.onTap(innerWidth / 2, innerHeight / 2); // crosshair click
});
ddiff --git a/public/js/inventory.js b/public/js/inventory.js
iindex e00f0d7..7e30c9a 100644
---- a/public/js/inventory.js
++++ b/public/js/inventory.js
@@@ -2,6 +2,7 @@
// number keys to select, consumables / melee props / clothes. Persists via server.
import { net } from './net.js';
import { beep } from './minigames.js';
++import { uiFocus } from './input.js';
export const ITEMS = {
chips: { name: 'Chips', icon: '🍟', type: 'food' },
@@@ -172,6 +173,7 @@ export function initInventory({ me, onEquip, onWear, toast }) {
function toggle(open) {
inv.open = open ?? !inv.open;
invEl.classList.toggle('hidden', !inv.open);
++ uiFocus('inv', inv.open); // free the cursor while the panel is up
if (inv.open) renderInv();
return inv.open;
}
ddiff --git a/public/js/main.js b/public/js/main.js
iindex fa298f9..80b2677 100644
---- a/public/js/main.js
++++ b/public/js/main.js
@@@ -3,7 +3,7 @@ import * as THREE from 'three';
import { buildWorld, resolveCollisions, W } from './world.js';
import { makeAvatar, buildHeldMesh, SKINS, SHIRTS, HAIRS, HATS } from './avatar.js';
import { initInventory, ITEMS } from './inventory.js';
--import { input, initInput, keyMove } from './input.js';
++import { input, initInput, keyMove, uiFocus } from './input.js';
import { net, api, connect } from './net.js';
import { initMinigames, beep } from './minigames.js';
import { initProps } from './props.js';
@@@ -352,6 +352,11 @@ function start(token, user) {
addEventListener('keydown', (e) => {
if (e.code === 'Enter' && !input.chatOpen && !mg.inArcade() && started) { openChat(); e.preventDefault(); }
else if (e.code === 'Escape' && input.chatOpen) closeChat();
++ else if (e.code === 'Escape' && started) {
++ // Escape closes the top UI panel (inventory/settings) like any game menu
++ if (invApi.state.open) invApi.toggle(false);
++ else if (!$('settings').classList.contains('hidden')) { $('settings').classList.add('hidden'); uiFocus('settings', false); }
++ }
});
$('btn-chat').addEventListener('touchstart', (e) => { e.preventDefault(); input.chatOpen ? closeChat() : openChat(); }, { passive: false });
@@@ -474,6 +479,7 @@ function start(token, user) {
}
$('btn-settings').onclick = () => {
$('settings').classList.remove('hidden');
++ uiFocus('settings', true);
buildApRows();
document.querySelectorAll('.vest').forEach(b => b.classList.toggle('sel', b.dataset.vest === me.vest));
const s = me.stats;
@@@ -495,7 +501,7 @@ function start(token, user) {
$('set-inverty').onchange = (e) => { cfg.invertY = e.target.checked; saveCfg(); };
camera.fov = cfg.fov; camera.updateProjectionMatrix();
refreshCfgUI();
-- $('btn-close-settings').onclick = () => $('settings').classList.add('hidden');
++ $('btn-close-settings').onclick = () => { $('settings').classList.add('hidden'); uiFocus('settings', false); };
$('btn-logout').onclick = () => { localStorage.clear(); location.reload(); };
document.querySelectorAll('.vest').forEach(b => b.onclick = () => {
me.vest = b.dataset.vest;
@@@ -868,6 +874,7 @@ function start(token, user) {
};
addEventListener('wheel', (e) => {
++ if (input.uiOpen) return; // scrolling a panel shouldn't zoom/cycle
if (fp) {
if (cfg.wheel === 'hotbar') { // cycle hotbar
const s = invApi.state;
ddiff --git a/public/js/world.js b/public/js/world.js
iindex f7dba82..7cd1741 100644
---- a/public/js/world.js
++++ b/public/js/world.js
@@@ -130,6 +130,9 @@ function shell(scene) {
collide(-21.65, -17.1, 16.7, .6); collide(-10.85, -17.1, 1.7, .6);
collide(1, -17.1, 22, .6); collide(27.5, -17.1, 21, .6);
serviceHallway(scene);
++ // full-height backing panel in the exit-door bay — the gap above/around the
++ // 2.4 m door slab looked straight through into the lit hallway (Bug30)
++ scene.add(blocker(box(1.8, 6.2, .14, M.wallDark, -12.5, 3.1, -17.02)));
exitDoor(scene, -12.6, -16.85, 0);
// south (z=17): long glass run with parking outside (photos 1/2/4) and a second
// set of entry doors near the market end (panorama), wall far east
@@@ -1212,6 +1215,7 @@ function entrance(scene) {
scene.add(box(.3, 6.2, 1.2, M.wall, -38, 3.1, -2.6));
scene.add(box(.3, 6.2, 1.2, M.wall, -38, 3.1, 2.6));
scene.add(box(.3, 1.6, 6, M.wall, -38, 5.4, 0));
++ scene.add(box(.07, 1.9, 4.1, M.glass, -37.9, 3.72, 0)); // transom over the leaves
const signM = new THREE.MeshBasicMaterial({ map: emergencySignTexture() });
for (const s of [-1, 1]) {
const leaf = box(.07, 2.6, 1.9, M.glass, -37.9, 1.5, s * 1.0);