openvibers/breakroomsimulator Commits
Performance pass + graphics settings + inventory/crafting cross links
Changes
@@@ -169,3 +169,8 @@ body.touch #hp-wrap { bottom: calc(174px + env(safe-area-inset-bottom)); }.craft-row .cn small { display: block; color: #8fa0b3; }.craft-row button { background: rgba(55,224,111,.15); border: 1px solid #37e06f; color: #b8f5cf; border-radius: 8px; padding: 6px 14px; cursor: pointer; font-family: inherit; }.craft-row button:disabled { background: rgba(255,255,255,.04); border-color: rgba(255,255,255,.15); color: #5d6b7a; cursor: default; }++++/* ---------- graphics/perf UI ---------- */++#fps-meter { position: absolute; top: 12px; left: 12px; z-index: 25; font: 700 13px ui-monospace, monospace; color: #37e06f; background: rgba(10,14,20,.55); padding: 4px 8px; border-radius: 8px; pointer-events: none; }++.xlink { background: rgba(53,224,255,.12); border: 1px solid rgba(53,224,255,.5); color: #9fe8ff; border-radius: 8px; padding: 4px 10px; cursor: pointer; font-family: inherit; font-size: .8rem; margin-left: 10px; }++.xlink:hover { background: rgba(53,224,255,.22); }@@@ -33,7 +33,10 @@ export function initLighting(scene, renderer, shadows = true) {const sun = new THREE.DirectionalLight(0xffeed8, 1.35);const moon = new THREE.DirectionalLight(0x8fa8d8, 0);sun.castShadow = true;-- sun.shadow.mapSize.set(2048, 2048);++ // shadow pass = the whole scene rendered again; refresh it every OTHER++ // frame — the sun moves slowly enough that nobody can tell++ renderer.shadowMap.autoUpdate = false;++ sun.shadow.mapSize.set(1024, 1024);sun.shadow.camera.left = -48; sun.shadow.camera.right = 48;sun.shadow.camera.top = 48; sun.shadow.camera.bottom = -48;sun.shadow.camera.near = 1; sun.shadow.camera.far = 240;@@@ -43,6 +46,7 @@ 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.++ 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],@@@ -51,6 +55,7 @@ export function initLighting(scene, renderer, shadows = true) {const pl = new THREE.PointLight(0xf6f4ea, i2, dist, 1.2);pl.position.set(x, y, z);scene.add(pl);++ interiorLights.push(pl);}enableShadows(scene); // everything built so far@@@ -92,7 +97,10 @@ export function initLighting(scene, renderer, shadows = true) {];let indoorK = 0;++ let frameN = 0;function update(dt, px, pz) {++ frameN++;++ if (renderer.shadowMap.enabled && frameN % 2 === 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();@@@ -133,12 +141,27 @@ export function initLighting(scene, renderer, shadows = true) {return t;}-- function setShadows(on) {-- renderer.shadowMap.enabled = on;++ function refreshMaterials() {scene.traverse(o => {if (o.material) (Array.isArray(o.material) ? o.material : [o.material]).forEach(m => { m.needsUpdate = true; });});}++ function setShadowQuality(q) { // 'off' | 'low' | 'high'++ renderer.shadowMap.enabled = q !== 'off';++ const size = q === 'high' ? 2048 : 1024;++ if (sun.shadow.mapSize.x !== size) {++ sun.shadow.mapSize.set(size, size);++ if (sun.shadow.map) { sun.shadow.map.dispose(); sun.shadow.map = null; }++ }++ renderer.shadowMap.needsUpdate = true;++ refreshMaterials();++ }++ function setExtraLights(on) { // interior fluorescents + street lamps + fire++ for (const pl of interiorLights) pl.visible = on;++ for (const nl of W.nightLights || []) nl.light.visible = on;++ for (const fl of W.extraLights || []) fl.visible = on;++ refreshMaterials();++ }-- return { update, setShadows, timeOfDay };++ return { update, setShadowQuality, setExtraLights, timeOfDay };}@@@ -94,6 +94,28 @@ function start(token, user) {JSON.parse(localStorage.getItem('brs-cfg') || '{}').shadows !== false);let editor = null;++ // ---- static-matrix freeze: the world is thousands of meshes that never++ // move; recomposing their matrices every frame is pure CPU waste. Freeze++ // everything except the known movers. ----++ {++ const dyn = new Set();++ const markDyn = (o) => o && o.traverse(x => dyn.add(x));++ W.cars?.forEach(c => markDyn(c.group));++ W.gates?.forEach(g => { markDyn(g.pl); markDyn(g.pr); });++ W.knockables?.forEach(k => markDyn(k.obj));++ W.dynamic.fireGroups?.forEach(markDyn);++ for (const id of ['a', 'b']) {++ markDyn(W.anchors[`pong-${id}`]?.ball);++ W.anchors[`c4-${id}`]?.discs?.forEach(markDyn);++ }++ markDyn(W.anchors.chess?.group);++ scene.traverse(o => {++ if (dyn.has(o) || o.isLight || o.isCamera) return;++ o.matrixAutoUpdate = false;++ o.updateMatrix();++ });++ }++// ---------- drivable cars (rigid-body vehicles — they roll and flip) ----------const carState = { driving: null }; // driving = W.cars entrylet carSendT = 0;@@@ -171,8 +193,9 @@ 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 },++ { wheel: 'zoom', sens: 1.0, fov: 70, invertY: false, shadows: true, shadowQ: null, rscale: 1, drawDist: 'far', xlights: true, fps: false },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));let myAvatar = makeAvatar(me.name, me.vest, me.guest, me.ap);enableShadows(myAvatar.group);@@@ -222,6 +245,13 @@ function start(token, user) {torch.position.set(.25, -.2, .1);torch.target.position.set(0, -.4, -12);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++) {++ const sp = new THREE.SpotLight(0xfff2cf, 0, 36, .52, .5, 1.1);++ scene.add(sp, sp.target);++ hlPool.push(sp);++ }let vmSwing = 0;// per-item viewmodel pose so props read correctly in first person (Bug10)const VM_POSE = {@@@ -924,6 +954,9 @@ function start(token, user) {uiFocus('craft', craftOpen);if (craftOpen) renderCraft();}++ // inventory ⇄ crafting cross links++ $('btn-inv-craft').onclick = () => { invApi.toggle(false); toggleCraft(true); };++ $('btn-craft-inv').onclick = () => { toggleCraft(false); invApi.toggle(true); };// ---------- base building: walls & floors with hp and upgrades ----------const buildMap = new Map(); // id -> {b, mesh, col, body, inter}@@@ -1282,14 +1315,53 @@ function start(token, user) {$('set-sens').value = cfg.sens;$('set-fov').value = cfg.fov;$('set-inverty').checked = cfg.invertY;-- $('set-shadows').checked = cfg.shadows !== false;++ for (const [id, v] of [['set-sh-off', 'off'], ['set-sh-low', 'low'], ['set-sh-high', 'high']])++ $(id).classList.toggle('sel', cfg.shadowQ === v);++ $('set-rscale').value = cfg.rscale;++ $('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-fps').checked = !!cfg.fps;};++ // ---- graphics application ----++ const baseDpr = Math.min(devicePixelRatio, 2);++ function applyGraphics() {++ daylight.setShadowQuality(cfg.shadowQ);++ renderer.setPixelRatio(baseDpr * cfg.rscale);++ renderer.setSize(innerWidth, innerHeight);++ const far = cfg.drawDist === 'near' ? 170 : 300;++ camera.far = far;++ camera.updateProjectionMatrix();++ scene.fog.far = Math.min(260, far - 15);++ scene.fog.near = cfg.drawDist === 'near' ? 60 : 90;++ daylight.setExtraLights(cfg.xlights !== false);++ $('fps-meter').classList.toggle('hidden', !cfg.fps);++ }++ for (const [id, v] of [['set-sh-off', 'off'], ['set-sh-low', 'low'], ['set-sh-high', 'high']])++ $(id).onclick = () => { cfg.shadowQ = v; cfg.shadows = v !== 'off'; saveCfg(); applyGraphics(); refreshCfgUI(); };++ $('set-rscale').oninput = (e) => { cfg.rscale = +e.target.value; saveCfg(); applyGraphics(); };++ $('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-fps').onchange = (e) => { cfg.fps = e.target.checked; saveCfg(); applyGraphics(); };++ applyGraphics();++ // fps counter (1s window)++ let fpsN = 0, fpsT = performance.now();++ function tickFps() {++ if (!cfg.fps) return;++ fpsN++;++ const now2 = performance.now();++ if (now2 - fpsT > 1000) {++ $('fps-meter').textContent = `${Math.round(fpsN * 1000 / (now2 - fpsT))} fps · ${renderer.info.render.calls} draws`;++ fpsN = 0;++ fpsT = now2;++ }++ }$('set-wheel-zoom').onclick = () => { cfg.wheel = 'zoom'; saveCfg(); refreshCfgUI(); };$('set-wheel-hotbar').onclick = () => { cfg.wheel = 'hotbar'; saveCfg(); refreshCfgUI(); };$('set-sens').oninput = (e) => { cfg.sens = +e.target.value; saveCfg(); };$('set-fov').oninput = (e) => { cfg.fov = +e.target.value; camera.fov = cfg.fov; camera.updateProjectionMatrix(); saveCfg(); };$('set-inverty').onchange = (e) => { cfg.invertY = e.target.checked; saveCfg(); };-- $('set-shadows').onchange = (e) => { cfg.shadows = e.target.checked; daylight.setShadows(cfg.shadows); saveCfg(); };camera.fov = cfg.fov; camera.updateProjectionMatrix();refreshCfgUI();$('btn-close-settings').onclick = () => { $('settings').classList.add('hidden'); uiFocus('settings', false); };@@@ -1763,21 +1835,22 @@ function start(token, user) {ghosts[bkind].rotation.y = gp.ry;}-- // headlights on driven cars, brighter after dark-- for (const e2 of phys.cars.values()) {-- const car = e2.car;-- if (car.driver != null) {-- if (!e2.hlLight) {-- const sp = new THREE.SpotLight(0xfff2cf, 0, 36, .52, .5, 1.1);-- sp.position.set(0, .85, 2.0);-- const tgt = new THREE.Object3D();-- tgt.position.set(0, .25, 15);-- car.group.add(sp, tgt);-- sp.target = tgt;-- e2.hlLight = sp;++ // headlights: a fixed POOL of 3 spotlights shared by driven cars — the++ // light count never changes, so shaders never recompile mid-game (the++ // old per-car lazy lights caused a full recompile hitch on every enter)++ {++ let hi = 0;++ for (const e2 of phys.cars.values()) {++ if (e2.car.driver == null || hi >= hlPool.length) continue;++ const hl2 = hlPool[hi++];++ if (hl2.parent !== e2.car.group) {++ e2.car.group.add(hl2, hl2.target);++ hl2.position.set(0, .85, 2.0);++ hl2.target.position.set(0, .25, 15);}-- e2.hlLight.intensity = .25 + (1 - (W.dayFactor ?? 1)) * 3.4;-- } else if (e2.hlLight) e2.hlLight.intensity = 0;++ hl2.intensity = .25 + (1 - (W.dayFactor ?? 1)) * 3.4;++ }++ for (; hi < hlPool.length; hi++) hlPool[hi].intensity = 0;}// flashlight follows your viewtorch.intensity = my.held === 'flashlight' ? .5 + (1 - (W.dayFactor ?? 1)) * 3.6 : 0;@@@ -1793,6 +1866,7 @@ function start(token, user) {if (sendTimer > .1) { sendTimer = 0; sendPos(); }renderer.render(scene, camera);++ tickFps();}frame();@@@ -357,7 +357,7 @@ export function initPhysics(scene) {}}if (playerPos) playerPush(playerPos.x, playerPos.y, playerPos.z, dt);-- world.step(1 / 60, dt, 3);++ world.step(1 / 60, dt, 2);// cars: visual group, walking collider, and E-prompt follow the bodyfor (const e of cars.values()) {const b = e.body, car = e.car;@@@ -2830,6 +2830,8 @@ function streets(scene) {const fireLight = new THREE.PointLight(0xff7722, 1.8, 11, 2);fireLight.position.set(0, 1.4, 0);fireG.add(fireLight);++ (W.extraLights ||= []).push(fireLight);++ (W.dynamic.fireGroups ||= []).push(fireG); // stays matrix-dynamic for the flame anim// rising smoke puffsconst smokeTex = TX.ct(64, 64, (g, w, h) => {const gr = g.createRadialGradient(w / 2, h / 2, 4, w / 2, h / 2, 30);@@@ -67,9 +67,11 @@<div id="dmg-flash"></div><div id="hitmark"></div><div id="headshot-tag">HEADSHOT</div>++ <div id="fps-meter" class="hidden"></div><!-- crafting (C) --><div id="craft-menu" class="hidden">++ <button id="btn-craft-inv" class="xlink" style="float:right">🎒 Inventory →</button><h3>🔨 Crafting</h3><div class="have" id="craft-have"></div><div id="craft-list"></div>@@@ -101,7 +103,7 @@<!-- ============ INVENTORY ============ --><div id="inventory" class="hidden"><div id="inv-panel">-- <div id="inv-head">🎒 INVENTORY <span>drag to move · 1-6 select · F use/swing · Tab close</span></div>++ <div id="inv-head">🎒 INVENTORY <span>drag to move · 1-6 select · F use/swing · Tab close</span><button id="btn-inv-craft" class="xlink">🔨 Crafting →</button></div><div id="inv-grid"></div><div id="inv-hot-label">HOTBAR</div><div id="inv-hotbar"></div>@@@ -134,7 +136,16 @@<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>-- <div class="set-row"><span>Shadows</span><input id="set-shadows" 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>
@@ -169,3 +169,8 @@ body.touch #hp-wrap { bottom: calc(174px + env(safe-area-inset-bottom)); }
@@ -169,3 +169,8 @@ body.touch #hp-wrap { bottom: calc(174px + env(safe-area-inset-bottom)); }
.craft-row .cn small { display: block; color: #8fa0b3; }
.craft-row .cn small { display: block; color: #8fa0b3; }
.craft-row button { background: rgba(55,224,111,.15); border: 1px solid #37e06f; color: #b8f5cf; border-radius: 8px; padding: 6px 14px; cursor: pointer; font-family: inherit; }
.craft-row button { background: rgba(55,224,111,.15); border: 1px solid #37e06f; color: #b8f5cf; border-radius: 8px; padding: 6px 14px; cursor: pointer; font-family: inherit; }
.craft-row button:disabled { background: rgba(255,255,255,.04); border-color: rgba(255,255,255,.15); color: #5d6b7a; cursor: default; }
.craft-row button:disabled { background: rgba(255,255,255,.04); border-color: rgba(255,255,255,.15); color: #5d6b7a; cursor: default; }
+
+/* ---------- graphics/perf UI ---------- */
+#fps-meter { position: absolute; top: 12px; left: 12px; z-index: 25; font: 700 13px ui-monospace, monospace; color: #37e06f; background: rgba(10,14,20,.55); padding: 4px 8px; border-radius: 8px; pointer-events: none; }
+.xlink { background: rgba(53,224,255,.12); border: 1px solid rgba(53,224,255,.5); color: #9fe8ff; border-radius: 8px; padding: 4px 10px; cursor: pointer; font-family: inherit; font-size: .8rem; margin-left: 10px; }
+.xlink:hover { background: rgba(53,224,255,.22); }
@@ -33,7 +33,10 @@ export function initLighting(scene, renderer, shadows = true) {
@@ -33,7 +33,10 @@ export function initLighting(scene, renderer, shadows = true) {
const sun = new THREE.DirectionalLight(0xffeed8, 1.35);
const sun = new THREE.DirectionalLight(0xffeed8, 1.35);
const moon = new THREE.DirectionalLight(0x8fa8d8, 0);
const moon = new THREE.DirectionalLight(0x8fa8d8, 0);
sun.castShadow = true;
sun.castShadow = true;
- sun.shadow.mapSize.set(2048, 2048);
+ // shadow pass = the whole scene rendered again; refresh it every OTHER
+ // frame — the sun moves slowly enough that nobody can tell
+ renderer.shadowMap.autoUpdate = false;
+ sun.shadow.mapSize.set(1024, 1024);
sun.shadow.camera.left = -48; sun.shadow.camera.right = 48;
sun.shadow.camera.left = -48; sun.shadow.camera.right = 48;
sun.shadow.camera.top = 48; sun.shadow.camera.bottom = -48;
sun.shadow.camera.top = 48; sun.shadow.camera.bottom = -48;
sun.shadow.camera.near = 1; sun.shadow.camera.far = 240;
sun.shadow.camera.near = 1; sun.shadow.camera.far = 240;
@@ -43,6 +46,7 @@ export function initLighting(scene, renderer, shadows = true) {
@@ -43,6 +46,7 @@ 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.
+ 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],
[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],
[77, 4.4, -5, .65, 26], [75, 4.2, 24, .65, 26],
@@ -51,6 +55,7 @@ export function initLighting(scene, renderer, shadows = true) {
@@ -51,6 +55,7 @@ export function initLighting(scene, renderer, shadows = true) {
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);
scene.add(pl);
scene.add(pl);
+ interiorLights.push(pl);
}
}
enableShadows(scene); // everything built so far
enableShadows(scene); // everything built so far
@@ -92,7 +97,10 @@ export function initLighting(scene, renderer, shadows = true) {
@@ -92,7 +97,10 @@ export function initLighting(scene, renderer, shadows = true) {
];
];
let indoorK = 0;
let indoorK = 0;
+ let frameN = 0;
function update(dt, px, pz) {
function update(dt, px, pz) {
+ frameN++;
+ if (renderer.shadowMap.enabled && frameN % 2 === 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();
@@ -133,12 +141,27 @@ export function initLighting(scene, renderer, shadows = true) {
@@ -133,12 +141,27 @@ export function initLighting(scene, renderer, shadows = true) {
return t;
return t;
}
}
- function setShadows(on) {
- renderer.shadowMap.enabled = on;
+ function refreshMaterials() {
scene.traverse(o => {
scene.traverse(o => {
if (o.material) (Array.isArray(o.material) ? o.material : [o.material]).forEach(m => { m.needsUpdate = true; });
if (o.material) (Array.isArray(o.material) ? o.material : [o.material]).forEach(m => { m.needsUpdate = true; });
});
});
}
}
+ function setShadowQuality(q) { // 'off' | 'low' | 'high'
+ renderer.shadowMap.enabled = q !== 'off';
+ const size = q === 'high' ? 2048 : 1024;
+ if (sun.shadow.mapSize.x !== size) {
+ sun.shadow.mapSize.set(size, size);
+ if (sun.shadow.map) { sun.shadow.map.dispose(); sun.shadow.map = null; }
+ }
+ renderer.shadowMap.needsUpdate = true;
+ refreshMaterials();
+ }
+ function setExtraLights(on) { // interior fluorescents + street lamps + fire
+ for (const pl of interiorLights) pl.visible = on;
+ for (const nl of W.nightLights || []) nl.light.visible = on;
+ for (const fl of W.extraLights || []) fl.visible = on;
+ refreshMaterials();
+ }
- return { update, setShadows, timeOfDay };
+ return { update, setShadowQuality, setExtraLights, timeOfDay };
}
}
@@ -94,6 +94,28 @@ function start(token, user) {
@@ -94,6 +94,28 @@ function start(token, user) {
JSON.parse(localStorage.getItem('brs-cfg') || '{}').shadows !== false);
JSON.parse(localStorage.getItem('brs-cfg') || '{}').shadows !== false);
let editor = null;
let editor = null;
+ // ---- static-matrix freeze: the world is thousands of meshes that never
+ // move; recomposing their matrices every frame is pure CPU waste. Freeze
+ // everything except the known movers. ----
+ {
+ const dyn = new Set();
+ const markDyn = (o) => o && o.traverse(x => dyn.add(x));
+ W.cars?.forEach(c => markDyn(c.group));
+ W.gates?.forEach(g => { markDyn(g.pl); markDyn(g.pr); });
+ W.knockables?.forEach(k => markDyn(k.obj));
+ W.dynamic.fireGroups?.forEach(markDyn);
+ for (const id of ['a', 'b']) {
+ markDyn(W.anchors[`pong-${id}`]?.ball);
+ W.anchors[`c4-${id}`]?.discs?.forEach(markDyn);
+ }
+ markDyn(W.anchors.chess?.group);
+ scene.traverse(o => {
+ if (dyn.has(o) || o.isLight || o.isCamera) return;
+ o.matrixAutoUpdate = false;
+ o.updateMatrix();
+ });
+ }
+
// ---------- drivable cars (rigid-body vehicles — they roll and flip) ----------
// ---------- drivable cars (rigid-body vehicles — they roll and flip) ----------
const carState = { driving: null }; // driving = W.cars entry
const carState = { driving: null }; // driving = W.cars entry
let carSendT = 0;
let carSendT = 0;
@@ -171,8 +193,9 @@ function start(token, user) {
@@ -171,8 +193,9 @@ 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 },
+ { wheel: 'zoom', sens: 1.0, fov: 70, invertY: false, shadows: true, shadowQ: null, rscale: 1, drawDist: 'far', xlights: true, fps: false },
JSON.parse(localStorage.getItem('brs-cfg') || '{}'));
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));
const saveCfg = () => localStorage.setItem('brs-cfg', JSON.stringify(cfg));
let myAvatar = makeAvatar(me.name, me.vest, me.guest, me.ap);
let myAvatar = makeAvatar(me.name, me.vest, me.guest, me.ap);
enableShadows(myAvatar.group);
enableShadows(myAvatar.group);
@@ -222,6 +245,13 @@ function start(token, user) {
@@ -222,6 +245,13 @@ function start(token, user) {
torch.position.set(.25, -.2, .1);
torch.position.set(.25, -.2, .1);
torch.target.position.set(0, -.4, -12);
torch.target.position.set(0, -.4, -12);
camera.add(torch, torch.target);
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++) {
+ const sp = new THREE.SpotLight(0xfff2cf, 0, 36, .52, .5, 1.1);
+ scene.add(sp, sp.target);
+ hlPool.push(sp);
+ }
let vmSwing = 0;
let vmSwing = 0;
// per-item viewmodel pose so props read correctly in first person (Bug10)
// per-item viewmodel pose so props read correctly in first person (Bug10)
const VM_POSE = {
const VM_POSE = {
@@ -924,6 +954,9 @@ function start(token, user) {
@@ -924,6 +954,9 @@ function start(token, user) {
uiFocus('craft', craftOpen);
uiFocus('craft', craftOpen);
if (craftOpen) renderCraft();
if (craftOpen) renderCraft();
}
}
+ // inventory ⇄ crafting cross links
+ $('btn-inv-craft').onclick = () => { invApi.toggle(false); toggleCraft(true); };
+ $('btn-craft-inv').onclick = () => { toggleCraft(false); invApi.toggle(true); };
// ---------- base building: walls & floors with hp and upgrades ----------
// ---------- base building: walls & floors with hp and upgrades ----------
const buildMap = new Map(); // id -> {b, mesh, col, body, inter}
const buildMap = new Map(); // id -> {b, mesh, col, body, inter}
@@ -1282,14 +1315,53 @@ function start(token, user) {
@@ -1282,14 +1315,53 @@ function start(token, user) {
$('set-sens').value = cfg.sens;
$('set-sens').value = cfg.sens;
$('set-fov').value = cfg.fov;
$('set-fov').value = cfg.fov;
$('set-inverty').checked = cfg.invertY;
$('set-inverty').checked = cfg.invertY;
- $('set-shadows').checked = cfg.shadows !== false;
+ for (const [id, v] of [['set-sh-off', 'off'], ['set-sh-low', 'low'], ['set-sh-high', 'high']])
+ $(id).classList.toggle('sel', cfg.shadowQ === v);
+ $('set-rscale').value = cfg.rscale;
+ $('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-fps').checked = !!cfg.fps;
};
};
+ // ---- graphics application ----
+ const baseDpr = Math.min(devicePixelRatio, 2);
+ function applyGraphics() {
+ daylight.setShadowQuality(cfg.shadowQ);
+ renderer.setPixelRatio(baseDpr * cfg.rscale);
+ renderer.setSize(innerWidth, innerHeight);
+ const far = cfg.drawDist === 'near' ? 170 : 300;
+ camera.far = far;
+ camera.updateProjectionMatrix();
+ scene.fog.far = Math.min(260, far - 15);
+ scene.fog.near = cfg.drawDist === 'near' ? 60 : 90;
+ daylight.setExtraLights(cfg.xlights !== false);
+ $('fps-meter').classList.toggle('hidden', !cfg.fps);
+ }
+ for (const [id, v] of [['set-sh-off', 'off'], ['set-sh-low', 'low'], ['set-sh-high', 'high']])
+ $(id).onclick = () => { cfg.shadowQ = v; cfg.shadows = v !== 'off'; saveCfg(); applyGraphics(); refreshCfgUI(); };
+ $('set-rscale').oninput = (e) => { cfg.rscale = +e.target.value; saveCfg(); applyGraphics(); };
+ $('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-fps').onchange = (e) => { cfg.fps = e.target.checked; saveCfg(); applyGraphics(); };
+ applyGraphics();
+ // fps counter (1s window)
+ let fpsN = 0, fpsT = performance.now();
+ function tickFps() {
+ if (!cfg.fps) return;
+ fpsN++;
+ const now2 = performance.now();
+ if (now2 - fpsT > 1000) {
+ $('fps-meter').textContent = `${Math.round(fpsN * 1000 / (now2 - fpsT))} fps · ${renderer.info.render.calls} draws`;
+ fpsN = 0;
+ fpsT = now2;
+ }
+ }
$('set-wheel-zoom').onclick = () => { cfg.wheel = 'zoom'; saveCfg(); refreshCfgUI(); };
$('set-wheel-zoom').onclick = () => { cfg.wheel = 'zoom'; saveCfg(); refreshCfgUI(); };
$('set-wheel-hotbar').onclick = () => { cfg.wheel = 'hotbar'; saveCfg(); refreshCfgUI(); };
$('set-wheel-hotbar').onclick = () => { cfg.wheel = 'hotbar'; saveCfg(); refreshCfgUI(); };
$('set-sens').oninput = (e) => { cfg.sens = +e.target.value; saveCfg(); };
$('set-sens').oninput = (e) => { cfg.sens = +e.target.value; saveCfg(); };
$('set-fov').oninput = (e) => { cfg.fov = +e.target.value; camera.fov = cfg.fov; camera.updateProjectionMatrix(); saveCfg(); };
$('set-fov').oninput = (e) => { cfg.fov = +e.target.value; camera.fov = cfg.fov; camera.updateProjectionMatrix(); saveCfg(); };
$('set-inverty').onchange = (e) => { cfg.invertY = e.target.checked; saveCfg(); };
$('set-inverty').onchange = (e) => { cfg.invertY = e.target.checked; saveCfg(); };
- $('set-shadows').onchange = (e) => { cfg.shadows = e.target.checked; daylight.setShadows(cfg.shadows); saveCfg(); };
camera.fov = cfg.fov; camera.updateProjectionMatrix();
camera.fov = cfg.fov; camera.updateProjectionMatrix();
refreshCfgUI();
refreshCfgUI();
$('btn-close-settings').onclick = () => { $('settings').classList.add('hidden'); uiFocus('settings', false); };
$('btn-close-settings').onclick = () => { $('settings').classList.add('hidden'); uiFocus('settings', false); };
@@ -1763,21 +1835,22 @@ function start(token, user) {
@@ -1763,21 +1835,22 @@ function start(token, user) {
ghosts[bkind].rotation.y = gp.ry;
ghosts[bkind].rotation.y = gp.ry;
}
}
- // headlights on driven cars, brighter after dark
- for (const e2 of phys.cars.values()) {
- const car = e2.car;
- if (car.driver != null) {
- if (!e2.hlLight) {
- const sp = new THREE.SpotLight(0xfff2cf, 0, 36, .52, .5, 1.1);
- sp.position.set(0, .85, 2.0);
- const tgt = new THREE.Object3D();
- tgt.position.set(0, .25, 15);
- car.group.add(sp, tgt);
- sp.target = tgt;
- e2.hlLight = sp;
+ // headlights: a fixed POOL of 3 spotlights shared by driven cars — the
+ // light count never changes, so shaders never recompile mid-game (the
+ // old per-car lazy lights caused a full recompile hitch on every enter)
+ {
+ let hi = 0;
+ for (const e2 of phys.cars.values()) {
+ if (e2.car.driver == null || hi >= hlPool.length) continue;
+ const hl2 = hlPool[hi++];
+ if (hl2.parent !== e2.car.group) {
+ e2.car.group.add(hl2, hl2.target);
+ hl2.position.set(0, .85, 2.0);
+ hl2.target.position.set(0, .25, 15);
}
}
- e2.hlLight.intensity = .25 + (1 - (W.dayFactor ?? 1)) * 3.4;
- } else if (e2.hlLight) e2.hlLight.intensity = 0;
+ hl2.intensity = .25 + (1 - (W.dayFactor ?? 1)) * 3.4;
+ }
+ for (; hi < hlPool.length; hi++) hlPool[hi].intensity = 0;
}
}
// 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;
@@ -1793,6 +1866,7 @@ function start(token, user) {
@@ -1793,6 +1866,7 @@ function start(token, user) {
if (sendTimer > .1) { sendTimer = 0; sendPos(); }
if (sendTimer > .1) { sendTimer = 0; sendPos(); }
renderer.render(scene, camera);
renderer.render(scene, camera);
+ tickFps();
}
}
frame();
frame();
@@ -357,7 +357,7 @@ export function initPhysics(scene) {
@@ -357,7 +357,7 @@ export function initPhysics(scene) {
}
}
}
}
if (playerPos) playerPush(playerPos.x, playerPos.y, playerPos.z, dt);
if (playerPos) playerPush(playerPos.x, playerPos.y, playerPos.z, dt);
- world.step(1 / 60, dt, 3);
+ world.step(1 / 60, dt, 2);
// cars: visual group, walking collider, and E-prompt follow the body
// cars: visual group, walking collider, and E-prompt follow the body
for (const e of cars.values()) {
for (const e of cars.values()) {
const b = e.body, car = e.car;
const b = e.body, car = e.car;
@@ -2830,6 +2830,8 @@ function streets(scene) {
@@ -2830,6 +2830,8 @@ function streets(scene) {
const fireLight = new THREE.PointLight(0xff7722, 1.8, 11, 2);
const fireLight = new THREE.PointLight(0xff7722, 1.8, 11, 2);
fireLight.position.set(0, 1.4, 0);
fireLight.position.set(0, 1.4, 0);
fireG.add(fireLight);
fireG.add(fireLight);
+ (W.extraLights ||= []).push(fireLight);
+ (W.dynamic.fireGroups ||= []).push(fireG); // stays matrix-dynamic for the flame anim
// rising smoke puffs
// rising smoke puffs
const smokeTex = TX.ct(64, 64, (g, w, h) => {
const smokeTex = TX.ct(64, 64, (g, w, h) => {
const gr = g.createRadialGradient(w / 2, h / 2, 4, w / 2, h / 2, 30);
const gr = g.createRadialGradient(w / 2, h / 2, 4, w / 2, h / 2, 30);
@@ -67,9 +67,11 @@
@@ -67,9 +67,11 @@
<div id="dmg-flash"></div>
<div id="dmg-flash"></div>
<div id="hitmark"></div>
<div id="hitmark"></div>
<div id="headshot-tag">HEADSHOT</div>
<div id="headshot-tag">HEADSHOT</div>
+ <div id="fps-meter" class="hidden"></div>
<!-- crafting (C) -->
<!-- crafting (C) -->
<div id="craft-menu" class="hidden">
<div id="craft-menu" class="hidden">
+ <button id="btn-craft-inv" class="xlink" style="float:right">🎒 Inventory →</button>
<h3>🔨 Crafting</h3>
<h3>🔨 Crafting</h3>
<div class="have" id="craft-have"></div>
<div class="have" id="craft-have"></div>
<div id="craft-list"></div>
<div id="craft-list"></div>
@@ -101,7 +103,7 @@
@@ -101,7 +103,7 @@
<!-- ============ INVENTORY ============ -->
<!-- ============ INVENTORY ============ -->
<div id="inventory" class="hidden">
<div id="inventory" class="hidden">
<div id="inv-panel">
<div id="inv-panel">
- <div id="inv-head">🎒 INVENTORY <span>drag to move · 1-6 select · F use/swing · Tab close</span></div>
+ <div id="inv-head">🎒 INVENTORY <span>drag to move · 1-6 select · F use/swing · Tab close</span><button id="btn-inv-craft" class="xlink">🔨 Crafting →</button></div>
<div id="inv-grid"></div>
<div id="inv-grid"></div>
<div id="inv-hot-label">HOTBAR</div>
<div id="inv-hot-label">HOTBAR</div>
<div id="inv-hotbar"></div>
<div id="inv-hotbar"></div>
@@ -134,7 +136,16 @@
@@ -134,7 +136,16 @@
<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>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>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>
<div class="set-row"><span>Invert Y</span><input id="set-inverty" type="checkbox"></div>
- <div class="set-row"><span>Shadows</span><input id="set-shadows" 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>