openvibers/breakroomsimulator Commits

Optimization pass 2 + discoverable settings (gear button, graphics first)

OP OpenVibers committed 30 Jul 2026 · 2fcabe0652 · 4 files, +59 −39

- Lights 22 -> 16 at defaults: interior fluorescents merged 8 -> 5 wider
  lights, street lamps emit real light from alternating heads only (3 of 5),
  headlight pool 3 -> 2. Every light is evaluated per-fragment on every
  draw in a forward renderer — count is the whole game.
- Device pixel ratio capped at 1.5 (dpr-2/3 phone screens were rendering
  up to 4x the fragments for no visible gain at this art style).
- Antialiasing toggle (MSAA off = big win on tiled/mobile GPUs; applies via
  quick auto-reload since it's a context-creation flag).
- Shadow refresh every 3rd frame (was every 2nd).
- Avatar animation LOD: other players further than 55m position-lerp only,
  skipping the full limb/pose/bubble math.
- Interactable proximity scan runs every 3rd frame instead of every frame.
- Settings discoverability: new gear button in the topbar (tooltip'd, next
  to the locker vest), panel retitled 'Settings & Locker' with the Graphics
  section moved to the very top.

Note on server-side: hits/deaths, build hp, prop caps and position bounds
were already validated server-side; the remaining client cost is rendering
+ local physics, which can't move off-client without adding input latency —
this pass shrinks them instead.

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

Changes

ddiff --git a/public/js/lighting.js b/public/js/lighting.js
iindex 233f9e9..5a2eb99 100644
---- a/public/js/lighting.js
++++ b/public/js/lighting.js
@@@ -46,11 +46,13 @@ export function initLighting(scene, renderer, shadows = true) {
// interior fluorescents — the lights stay on all night (Bug44: the facility
// went pitch black after dark). World coords: cafeteria, security, lockers,
// service hallway.
++ // every light here is evaluated per-fragment on every draw (forward
++ // renderer) — fewer, wider lights beat many narrow ones
const interiorLights = [];
for (const [x, y, z, i2, dist] of [
-- [67, 4.6, 55, .75, 30], [67, 4.6, 83, .75, 30], [83, 4.6, 55, .75, 30], [83, 4.6, 83, .75, 30],
-- [77, 4.4, -5, .65, 26], [75, 4.2, 24, .65, 26],
-- [55.2, 2.8, 55, .55, 22], [55.2, 2.8, 83, .55, 22],
++ [70, 4.6, 60, .85, 42], [80, 4.6, 82, .85, 42],
++ [77, 4.4, -5, .7, 30], [75, 4.2, 24, .65, 26],
++ [55.2, 2.8, 69, .6, 40],
]) {
const pl = new THREE.PointLight(0xf6f4ea, i2, dist, 1.2);
pl.position.set(x, y, z);
@@@ -100,7 +102,7 @@ export function initLighting(scene, renderer, shadows = true) {
let frameN = 0;
function update(dt, px, pz) {
frameN++;
-- if (renderer.shadowMap.enabled && frameN % 2 === 0) renderer.shadowMap.needsUpdate = true;
++ if (renderer.shadowMap.enabled && frameN % 3 === 0) renderer.shadowMap.needsUpdate = true;
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();
ddiff --git a/public/js/main.js b/public/js/main.js
iindex 3ed97c3..d7c5630 100644
---- a/public/js/main.js
++++ b/public/js/main.js
@@@ -64,9 +64,10 @@ function start(token, user) {
const me = { name: user.name, vest: user.vest, guest: user.guest, admin: !!user.admin, ap: user.ap || null, inv: user.inv, hotbar: user.hotbar, stats: user.stats || {} };
// ---------- three ----------
++ const cfg0 = JSON.parse(localStorage.getItem('brs-cfg') || '{}'); // pre-renderer options
const canvas = $('game');
-- const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });
-- renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
++ const renderer = new THREE.WebGLRenderer({ canvas, antialias: cfg0.aa !== false, powerPreference: 'high-performance' });
++ renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5)); // >1.5 dpr is wasted fragments on this art style
renderer.setSize(innerWidth, innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
@@@ -193,7 +194,7 @@ function start(token, user) {
let fp = false; // first-person mode
// ---------- persistent game settings ----------
const cfg = Object.assign(
-- { wheel: 'zoom', sens: 1.0, fov: 70, invertY: false, shadows: true, shadowQ: null, rscale: 1, drawDist: 'far', xlights: true, fps: false },
++ { wheel: 'zoom', sens: 1.0, fov: 70, invertY: false, shadows: true, shadowQ: null, rscale: 1, drawDist: 'far', xlights: true, fps: false, aa: true },
JSON.parse(localStorage.getItem('brs-cfg') || '{}'));
cfg.shadowQ ||= cfg.shadows === false ? 'off' : 'low'; // migrate the old checkbox
const saveCfg = () => localStorage.setItem('brs-cfg', JSON.stringify(cfg));
@@@ -247,7 +248,7 @@ function start(token, user) {
camera.add(torch, torch.target);
// headlight pool (created up-front — stable light count, no shader hitches)
const hlPool = [];
-- for (let i = 0; i < 3; i++) {
++ for (let i = 0; i < 2; i++) {
const sp = new THREE.SpotLight(0xfff2cf, 0, 36, .52, .5, 1.1);
scene.add(sp, sp.target);
hlPool.push(sp);
@@@ -1299,7 +1300,7 @@ function start(token, user) {
});
}
}
-- $('btn-settings').onclick = () => {
++ const openSettings = () => {
$('settings').classList.remove('hidden');
uiFocus('settings', true);
buildApRows();
@@@ -1309,6 +1310,8 @@ function start(token, user) {
? 'Guest session — create an account to save stats.'
: `🏓 Pong wins: <b>${s.pongWins || 0}</b> · 🔴 Connect 4 wins: <b>${s.c4Wins || 0}</b> · ♟️ Chess wins: <b>${s.chessWins || 0}</b>`;
};
++ $('btn-settings').onclick = openSettings;
++ $('btn-settings2').onclick = openSettings; // the ⚙️ — same panel, graphics on top
const refreshCfgUI = () => {
$('set-wheel-zoom').classList.toggle('sel', cfg.wheel === 'zoom');
$('set-wheel-hotbar').classList.toggle('sel', cfg.wheel === 'hotbar');
@@@ -1321,10 +1324,11 @@ function start(token, user) {
$('set-dd-near').classList.toggle('sel', cfg.drawDist === 'near');
$('set-dd-far').classList.toggle('sel', cfg.drawDist !== 'near');
$('set-xlights').checked = cfg.xlights !== false;
++ $('set-aa').checked = cfg.aa !== false;
$('set-fps').checked = !!cfg.fps;
};
// ---- graphics application ----
-- const baseDpr = Math.min(devicePixelRatio, 2);
++ const baseDpr = Math.min(devicePixelRatio, 1.5);
function applyGraphics() {
daylight.setShadowQuality(cfg.shadowQ);
renderer.setPixelRatio(baseDpr * cfg.rscale);
@@@ -1343,6 +1347,12 @@ function start(token, user) {
$('set-dd-near').onclick = () => { cfg.drawDist = 'near'; saveCfg(); applyGraphics(); refreshCfgUI(); };
$('set-dd-far').onclick = () => { cfg.drawDist = 'far'; saveCfg(); applyGraphics(); refreshCfgUI(); };
$('set-xlights').onchange = (e) => { cfg.xlights = e.target.checked; saveCfg(); applyGraphics(); };
++ $('set-aa').onchange = (e) => { // MSAA can only change at context creation
++ cfg.aa = e.target.checked;
++ saveCfg();
++ toast('🎨 Antialiasing applies after a quick reload…', 1800);
++ setTimeout(() => location.reload(), 900);
++ };
$('set-fps').onchange = (e) => { cfg.fps = e.target.checked; saveCfg(); applyGraphics(); };
applyGraphics();
// fps counter (1s window)
@@@ -1527,7 +1537,7 @@ function start(token, user) {
// ---------- movement & camera ----------
const clock = new THREE.Clock();
-- let sendTimer = 0, lastSent = '';
++ let sendTimer = 0, lastSent = '', frameCount = 0;
function sendPos(force = false) {
const d = [+my.x.toFixed(2), +my.y.toFixed(2), +my.z.toFixed(2), +my.ry.toFixed(2), my.anim];
const s = d.join(',');
@@@ -1683,7 +1693,7 @@ function start(token, user) {
myAvatar.airborne = !my.onGround && !my.seat;
myAvatar.animate(dt);
-- // others interpolate
++ // others interpolate (full limb animation only within 55m — LOD)
for (const o of others.values()) {
const g = o.avatar.group, t = o.target;
g.position.x += (t.x - g.position.x) * Math.min(1, dt * 10);
@@@ -1693,7 +1703,8 @@ function start(token, user) {
while (dr > Math.PI) dr -= Math.PI * 2;
while (dr < -Math.PI) dr += Math.PI * 2;
g.rotation.y += dr * Math.min(1, dt * 10);
-- o.avatar.animate(dt);
++ const ddx = g.position.x - my.x, ddz = g.position.z - my.z;
++ if (ddx * ddx + ddz * ddz < 3025) o.avatar.animate(dt);
}
if (carState.driving && !fp) my.dist = Math.max(my.dist, 6.5);
@@@ -1858,7 +1869,9 @@ function start(token, user) {
daylight.update(dt, my.x, my.z);
W.dynamic.fires?.forEach(f => f.update(dt));
-- scanInteractables();
++ // interactable scan every 3rd frame — a prompt appearing 30ms late is
++ // invisible; scanning a few hundred entries per frame is not
++ if ((frameCount = (frameCount + 1) % 3) === 0) scanInteractables();
mg.update(dt);
// network send 10Hz
ddiff --git a/public/js/world.js b/public/js/world.js
iindex 3b0a0c0..13646fa 100644
---- a/public/js/world.js
++++ b/public/js/world.js
@@@ -2695,11 +2695,14 @@ function streets(scene) {
lamp.add(cyl(.07, .09, 6.4, M.blackMetal, 0, 3.2, 0, 8));
lamp.add(box(1.6, .08, .12, M.blackMetal, .8, 6.35, 0));
lamp.add(box(.5, .1, .22, lampM, 1.5, 6.28, 0));
-- // real pool of light on the road after dark
-- const glow = new THREE.PointLight(0xffe9b0, 0, 16, 1.4);
-- glow.position.set(1.5, 6.1, 0);
-- lamp.add(glow);
-- W.nightLights.push({ light: glow, night: 1.5 });
++ // real light pools on ALTERNATING lamps only — every light costs every
++ // fragment of every draw; the in-between lamps get emissive heads only
++ if (lz === -30 || lz === 50 || lz === 126) {
++ const glow = new THREE.PointLight(0xffe9b0, 0, 20, 1.3);
++ glow.position.set(1.5, 6.1, 0);
++ lamp.add(glow);
++ W.nightLights.push({ light: glow, night: 1.7 });
++ }
lamp.position.set(132.2, 0, lz);
scene.add(lamp);
const c = collide(132.2, lz, .3, .3);
ddiff --git a/public/play.html b/public/play.html
iindex 0e1f210..0f0afb8 100644
---- a/public/play.html
++++ b/public/play.html
@@@ -49,7 +49,8 @@
<div id="clock-pill">⏱ <span id="break-timer">15:00</span></div>
<button id="btn-inv">🎒</button>
<button id="btn-fp">📷</button>
-- <button id="btn-settings">🦺</button>
++ <button id="btn-settings" title="Locker — outfit & stats">🦺</button>
++ <button id="btn-settings2" title="Settings — graphics & controls">⚙️</button>
<button id="btn-fullscreen">⛶</button>
</div>
@@@ -113,8 +114,26 @@
<!-- ============ SETTINGS ============ -->
<div id="settings" class="overlay hidden">
<div class="panel">
-- <h2>Locker</h2>
-- <p style="margin-top:4px">Character</p>
++ <h2>⚙️ Settings &amp; Locker</h2>
++ <p style="margin-top:4px">Graphics</p>
++ <div class="set-row"><span>Shadows</span>
++ <div class="seg"><button id="set-sh-off" class="segbtn">Off</button><button id="set-sh-low" class="segbtn">Low</button><button id="set-sh-high" class="segbtn">High</button></div>
++ </div>
++ <div class="set-row"><span>Render scale</span><input id="set-rscale" type="range" min="0.5" max="1" step="0.05"></div>
++ <div class="set-row"><span>Draw distance</span>
++ <div class="seg"><button id="set-dd-near" class="segbtn">Near</button><button id="set-dd-far" class="segbtn">Far</button></div>
++ </div>
++ <div class="set-row"><span>Extra lights</span><input id="set-xlights" type="checkbox"></div>
++ <div class="set-row"><span>Antialiasing</span><input id="set-aa" type="checkbox"></div>
++ <div class="set-row"><span>FPS counter</span><input id="set-fps" type="checkbox"></div>
++ <p style="margin-top:6px">Game settings</p>
++ <div class="set-row"><span>Scroll wheel</span>
++ <div class="seg"><button id="set-wheel-zoom" class="segbtn">Zoom</button><button id="set-wheel-hotbar" class="segbtn">Hotbar</button></div>
++ </div>
++ <div class="set-row"><span>Sensitivity</span><input id="set-sens" type="range" min="0.4" max="2.2" step="0.1"></div>
++ <div class="set-row"><span>FOV</span><input id="set-fov" type="range" min="60" max="100" step="1"></div>
++ <div class="set-row"><span>Invert Y</span><input id="set-inverty" type="checkbox"></div>
++ <p style="margin-top:6px">Character</p>
<div class="ap-row"><span>Skin</span><div id="ap-skin" class="swatches"></div></div>
<div class="ap-row"><span>Shirt</span><div id="ap-shirt" class="swatches"></div></div>
<div class="ap-row"><span>Hair</span><div id="ap-hair" class="swatches"></div></div>
@@@ -129,23 +148,6 @@
<button class="vest" data-vest="none" style="--c:#555">no vest</button>
</div>
<div id="stats-box"></div>
-- <p style="margin-top:6px">Game settings</p>
-- <div class="set-row"><span>Scroll wheel</span>
-- <div class="seg"><button id="set-wheel-zoom" class="segbtn">Zoom</button><button id="set-wheel-hotbar" class="segbtn">Hotbar</button></div>
-- </div>
-- <div class="set-row"><span>Sensitivity</span><input id="set-sens" type="range" min="0.4" max="2.2" step="0.1"></div>
-- <div class="set-row"><span>FOV</span><input id="set-fov" type="range" min="60" max="100" step="1"></div>
-- <div class="set-row"><span>Invert Y</span><input id="set-inverty" type="checkbox"></div>
-- <p style="margin-top:6px">Graphics</p>
-- <div class="set-row"><span>Shadows</span>
-- <div class="seg"><button id="set-sh-off" class="segbtn">Off</button><button id="set-sh-low" class="segbtn">Low</button><button id="set-sh-high" class="segbtn">High</button></div>
-- </div>
-- <div class="set-row"><span>Render scale</span><input id="set-rscale" type="range" min="0.5" max="1" step="0.05"></div>
-- <div class="set-row"><span>Draw distance</span>
-- <div class="seg"><button id="set-dd-near" class="segbtn">Near</button><button id="set-dd-far" class="segbtn">Far</button></div>
-- </div>
-- <div class="set-row"><span>Extra lights</span><input id="set-xlights" type="checkbox"></div>
-- <div class="set-row"><span>FPS counter</span><input id="set-fps" type="checkbox"></div>
<button id="btn-close-settings" class="btn-primary">Back to break</button>
<button id="btn-logout" class="btn-ghost">Badge out (log out)</button>
</div>