openvibers/breakroomsimulator Commits
Optimization pass 2 + discoverable settings (gear button, graphics first)
Changes
@@@ -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 onesconst 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();@@@ -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 optionsconst 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 stylerenderer.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 checkboxconst 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 topconst 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@@@ -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);@@@ -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 & 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>
@@ -46,11 +46,13 @@ export function initLighting(scene, renderer, shadows = true) {
@@ -46,11 +46,13 @@ export function initLighting(scene, renderer, shadows = true) {
// interior fluorescents — the lights stay on all night (Bug44: the facility
// interior fluorescents — the lights stay on all night (Bug44: the facility
// went pitch black after dark). World coords: cafeteria, security, lockers,
// went pitch black after dark). World coords: cafeteria, security, lockers,
// service hallway.
// service hallway.
+ // every light here is evaluated per-fragment on every draw (forward
+ // renderer) — fewer, wider lights beat many narrow ones
const interiorLights = [];
const interiorLights = [];
for (const [x, y, z, i2, dist] of [
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);
const pl = new THREE.PointLight(0xf6f4ea, i2, dist, 1.2);
pl.position.set(x, y, z);
pl.position.set(x, y, z);
@@ -100,7 +102,7 @@ export function initLighting(scene, renderer, shadows = true) {
@@ -100,7 +102,7 @@ export function initLighting(scene, renderer, shadows = true) {
let frameN = 0;
let frameN = 0;
function update(dt, px, pz) {
function update(dt, px, pz) {
frameN++;
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;
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);
indoorK += (inside - indoorK) * Math.min(1, (dt || .016) * 4);
const t = timeOfDay();
const t = timeOfDay();
@@ -64,9 +64,10 @@ function start(token, user) {
@@ -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 || {} };
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 ----------
// ---------- three ----------
+ const cfg0 = JSON.parse(localStorage.getItem('brs-cfg') || '{}'); // pre-renderer options
const canvas = $('game');
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.setSize(innerWidth, innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
const scene = new THREE.Scene();
@@ -193,7 +194,7 @@ function start(token, user) {
@@ -193,7 +194,7 @@ function start(token, user) {
let fp = false; // first-person mode
let fp = false; // first-person mode
// ---------- persistent game settings ----------
// ---------- persistent game settings ----------
const cfg = Object.assign(
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') || '{}'));
JSON.parse(localStorage.getItem('brs-cfg') || '{}'));
cfg.shadowQ ||= cfg.shadows === false ? 'off' : 'low'; // migrate the old checkbox
cfg.shadowQ ||= cfg.shadows === false ? 'off' : 'low'; // migrate the old checkbox
const saveCfg = () => localStorage.setItem('brs-cfg', JSON.stringify(cfg));
const saveCfg = () => localStorage.setItem('brs-cfg', JSON.stringify(cfg));
@@ -247,7 +248,7 @@ function start(token, user) {
@@ -247,7 +248,7 @@ function start(token, user) {
camera.add(torch, torch.target);
camera.add(torch, torch.target);
// headlight pool (created up-front — stable light count, no shader hitches)
// headlight pool (created up-front — stable light count, no shader hitches)
const hlPool = [];
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);
const sp = new THREE.SpotLight(0xfff2cf, 0, 36, .52, .5, 1.1);
scene.add(sp, sp.target);
scene.add(sp, sp.target);
hlPool.push(sp);
hlPool.push(sp);
@@ -1299,7 +1300,7 @@ function start(token, user) {
@@ -1299,7 +1300,7 @@ function start(token, user) {
});
});
}
}
}
}
- $('btn-settings').onclick = () => {
+ const openSettings = () => {
$('settings').classList.remove('hidden');
$('settings').classList.remove('hidden');
uiFocus('settings', true);
uiFocus('settings', true);
buildApRows();
buildApRows();
@@ -1309,6 +1310,8 @@ function start(token, user) {
@@ -1309,6 +1310,8 @@ function start(token, user) {
? 'Guest session — create an account to save stats.'
? '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>`;
: `🏓 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 = () => {
const refreshCfgUI = () => {
$('set-wheel-zoom').classList.toggle('sel', cfg.wheel === 'zoom');
$('set-wheel-zoom').classList.toggle('sel', cfg.wheel === 'zoom');
$('set-wheel-hotbar').classList.toggle('sel', cfg.wheel === 'hotbar');
$('set-wheel-hotbar').classList.toggle('sel', cfg.wheel === 'hotbar');
@@ -1321,10 +1324,11 @@ function start(token, user) {
@@ -1321,10 +1324,11 @@ function start(token, user) {
$('set-dd-near').classList.toggle('sel', cfg.drawDist === 'near');
$('set-dd-near').classList.toggle('sel', cfg.drawDist === 'near');
$('set-dd-far').classList.toggle('sel', cfg.drawDist !== 'near');
$('set-dd-far').classList.toggle('sel', cfg.drawDist !== 'near');
$('set-xlights').checked = cfg.xlights !== false;
$('set-xlights').checked = cfg.xlights !== false;
+ $('set-aa').checked = cfg.aa !== false;
$('set-fps').checked = !!cfg.fps;
$('set-fps').checked = !!cfg.fps;
};
};
// ---- graphics application ----
// ---- graphics application ----
- const baseDpr = Math.min(devicePixelRatio, 2);
+ const baseDpr = Math.min(devicePixelRatio, 1.5);
function applyGraphics() {
function applyGraphics() {
daylight.setShadowQuality(cfg.shadowQ);
daylight.setShadowQuality(cfg.shadowQ);
renderer.setPixelRatio(baseDpr * cfg.rscale);
renderer.setPixelRatio(baseDpr * cfg.rscale);
@@ -1343,6 +1347,12 @@ function start(token, user) {
@@ -1343,6 +1347,12 @@ function start(token, user) {
$('set-dd-near').onclick = () => { cfg.drawDist = 'near'; saveCfg(); applyGraphics(); refreshCfgUI(); };
$('set-dd-near').onclick = () => { cfg.drawDist = 'near'; saveCfg(); applyGraphics(); refreshCfgUI(); };
$('set-dd-far').onclick = () => { cfg.drawDist = 'far'; 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-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(); };
$('set-fps').onchange = (e) => { cfg.fps = e.target.checked; saveCfg(); applyGraphics(); };
applyGraphics();
applyGraphics();
// fps counter (1s window)
// fps counter (1s window)
@@ -1527,7 +1537,7 @@ function start(token, user) {
@@ -1527,7 +1537,7 @@ function start(token, user) {
// ---------- movement & camera ----------
// ---------- movement & camera ----------
const clock = new THREE.Clock();
const clock = new THREE.Clock();
- let sendTimer = 0, lastSent = '';
+ let sendTimer = 0, lastSent = '', frameCount = 0;
function sendPos(force = false) {
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 d = [+my.x.toFixed(2), +my.y.toFixed(2), +my.z.toFixed(2), +my.ry.toFixed(2), my.anim];
const s = d.join(',');
const s = d.join(',');
@@ -1683,7 +1693,7 @@ function start(token, user) {
@@ -1683,7 +1693,7 @@ function start(token, user) {
myAvatar.airborne = !my.onGround && !my.seat;
myAvatar.airborne = !my.onGround && !my.seat;
myAvatar.animate(dt);
myAvatar.animate(dt);
- // others interpolate
+ // others interpolate (full limb animation only within 55m — LOD)
for (const o of others.values()) {
for (const o of others.values()) {
const g = o.avatar.group, t = o.target;
const g = o.avatar.group, t = o.target;
g.position.x += (t.x - g.position.x) * Math.min(1, dt * 10);
g.position.x += (t.x - g.position.x) * Math.min(1, dt * 10);
@@ -1693,7 +1703,8 @@ function start(token, user) {
@@ -1693,7 +1703,8 @@ function start(token, user) {
while (dr > Math.PI) dr -= Math.PI * 2;
while (dr > Math.PI) dr -= Math.PI * 2;
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);
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);
if (carState.driving && !fp) my.dist = Math.max(my.dist, 6.5);
@@ -1858,7 +1869,9 @@ function start(token, user) {
@@ -1858,7 +1869,9 @@ function start(token, user) {
daylight.update(dt, 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();
+ // 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);
mg.update(dt);
// network send 10Hz
// network send 10Hz
@@ -2695,11 +2695,14 @@ function streets(scene) {
@@ -2695,11 +2695,14 @@ function streets(scene) {
lamp.add(cyl(.07, .09, 6.4, M.blackMetal, 0, 3.2, 0, 8));
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(1.6, .08, .12, M.blackMetal, .8, 6.35, 0));
lamp.add(box(.5, .1, .22, lampM, 1.5, 6.28, 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);
lamp.position.set(132.2, 0, lz);
scene.add(lamp);
scene.add(lamp);
const c = collide(132.2, lz, .3, .3);
const c = collide(132.2, lz, .3, .3);
@@ -49,7 +49,8 @@
@@ -49,7 +49,8 @@
<div id="clock-pill">⏱ <span id="break-timer">15:00</span></div>
<div id="clock-pill">⏱ <span id="break-timer">15:00</span></div>
<button id="btn-inv">🎒</button>
<button id="btn-inv">🎒</button>
<button id="btn-fp">📷</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>
<button id="btn-fullscreen">⛶</button>
</div>
</div>
@@ -113,8 +114,26 @@
@@ -113,8 +114,26 @@
<!-- ============ SETTINGS ============ -->
<!-- ============ SETTINGS ============ -->
<div id="settings" class="overlay hidden">
<div id="settings" class="overlay hidden">
<div class="panel">
<div class="panel">
- <h2>Locker</h2>
- <p style="margin-top:4px">Character</p>
+ <h2>⚙️ Settings & 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>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>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>
<div class="ap-row"><span>Hair</span><div id="ap-hair" class="swatches"></div></div>
@@ -129,23 +148,6 @@
@@ -129,23 +148,6 @@
<button class="vest" data-vest="none" style="--c:#555">no vest</button>
<button class="vest" data-vest="none" style="--c:#555">no vest</button>
</div>
</div>
<div id="stats-box"></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-close-settings" class="btn-primary">Back to break</button>
<button id="btn-logout" class="btn-ghost">Badge out (log out)</button>
<button id="btn-logout" class="btn-ghost">Badge out (log out)</button>
</div>
</div>