openvibers/breakroomsimulator Commits
Fix physgun freeze (lost function), shadow wedges, zombie behavior, sleeper combat parity
Changes
@@@ -64,6 +64,10 @@ export function initInventory({ me, onEquip, onWear, onDropItem, toast }) {clearTimeout(saveT);saveT = setTimeout(() => net.send({ t: 'inv', inv: inv.slots, hotbar: inv.hotbar }), 400);};++ const flush = () => { // fire the pending save NOW (tab closing)++ clearTimeout(saveT);++ net.send({ t: 'inv', inv: inv.slots, hotbar: inv.hotbar });++ };// ---------- DOM ----------const hotbarEl = document.getElementById('hotbar');@@@ -288,5 +292,5 @@ export function initInventory({ me, onEquip, onWear, onDropItem, toast }) {if (el?.dataset?.bar !== undefined) select(+el.dataset.bar);});-- return { add, select, useSelected, dropSelected, count, consume, toggle, selectedItem, restore, state: inv };++ return { add, select, useSelected, dropSelected, count, consume, toggle, selectedItem, restore, flush, state: inv };}@@@ -119,10 +119,13 @@ export function initLighting(scene, renderer, shadows = true) {scene.background.copy(sky);scene.fog.color.copy(sky);-- sun.intensity = 1.4 * d;++ // no direct sun indoors: interiors are lit by the fluorescents, and sun++ // spilling "through the roof" painted giant moving shadow-boundary wedges++ sun.intensity = 1.4 * d * (1 - indoorK * .92);sun.color.copy(sunWarm).lerp(sunNoon, smooth(.05, .45, elev));-- sun.position.set(px + Math.sin(az) * 80, Math.max(4, elev * 90), pz + Math.cos(az) * 40);-- sun.target.position.set(px, 0, pz);++ const qx = Math.round(px / 4) * 4, qz = Math.round(pz / 4) * 4; // quantized follow = less boundary swim++ sun.position.set(qx + Math.sin(az) * 80, Math.max(4, elev * 90), qz + Math.cos(az) * 40);++ sun.target.position.set(qx, 0, qz);moon.intensity = .22 * (1 - d);moon.position.set(px - Math.sin(az) * 70, Math.max(10, -elev * 80), pz - Math.cos(az) * 35);@@@ -757,7 +757,7 @@ function start(token, user) {root.add(zz);enableShadows(root);scene.add(root);-- sleeperMap.set(s.key, { root, zz, t: Math.random() * 6 });++ sleeperMap.set(s.key, { root, zz, av, t: Math.random() * 6 });}function removeSleeper(key) {const e = sleeperMap.get(key);@@@ -767,6 +767,10 @@ function start(token, user) {}net.on('sleep', (m) => addSleeper(m.s));net.on('wake', (m) => removeSleeper(m.key));++ net.on('sleephurt', (m) => {++ sleeperMap.get(m.key)?.av?.flash?.();++ if (m.by === myId) hitmarker(false);++ });// ---------- dropped items on the floor (minecraft style) ----------const worldDrops = new Map(); // id -> {group, mesh, inter, t}@@@ -900,7 +904,29 @@ function start(token, user) {const skin = M2(0x6a9c4e, .9), shirt = M2(0x3a3f46, .95);g.add(bx(.44, .62, .26, shirt, 0, 1.12, 0));head = bx(.3, .3, .3, skin, 0, 1.62, 0);++ // ember eyes so they read at night++ const eyeM = new THREE.MeshStandardMaterial({ color: 0xff7a2a, emissive: 0xff7a2a, emissiveIntensity: 2 });++ head.add(bx(.05, .04, .02, eyeM, -.07, .03, .16));++ head.add(bx(.05, .04, .02, eyeM, .07, .03, .16));g.add(head);++ // a lit torch in hand — see them coming++ const torchG = new THREE.Group();++ torchG.add(bx(.05, .4, .05, M2(0x5a4028, .9), 0, 0, 0));++ const flameM = new THREE.MeshStandardMaterial({ color: 0xffb42e, emissive: 0xff8a1c, emissiveIntensity: 2.6 });++ torchG.add(bx(.1, .16, .1, flameM, 0, .27, 0));++ const glow = new THREE.Sprite(new THREE.SpriteMaterial({++ map: ct(64, 64, (gg, w2, h2) => {++ const gr = gg.createRadialGradient(w2 / 2, h2 / 2, 3, w2 / 2, h2 / 2, 30);++ gr.addColorStop(0, 'rgba(255,190,90,.9)'); gr.addColorStop(1, 'rgba(255,140,30,0)');++ gg.fillStyle = gr; gg.fillRect(0, 0, w2, h2);++ }),++ transparent: true, blending: THREE.AdditiveBlending, depthWrite: false,++ }));++ glow.scale.set(1.1, 1.1, 1);++ glow.position.y = .3;++ torchG.add(glow);++ torchG.position.set(.42, 1.25, .3);++ g.add(torchG);for (const ax of [-.3, .3]) { // arms out front, classicconst arm = bx(.11, .11, .55, skin, ax, 1.3, .34);arms.push(arm); g.add(arm);@@@ -929,7 +955,29 @@ function start(token, user) {if (!n) return;scene.remove(n.group);npcMap.delete(id);-- if (burn) beep(190, .3, 'sawtooth', .08);++ if (burn) { // minecraft sunrise: they go up in flames++ beep(190, .3, 'sawtooth', .08);++ const fl = new THREE.Sprite(new THREE.SpriteMaterial({++ map: ct(64, 96, (gg, w2, h2) => {++ const gr = gg.createLinearGradient(0, h2, 0, 0);++ gr.addColorStop(0, 'rgba(255,120,20,.95)'); gr.addColorStop(.7, 'rgba(255,200,60,.5)'); gr.addColorStop(1, 'rgba(255,240,160,0)');++ gg.fillStyle = gr; gg.fillRect(0, 0, w2, h2);++ }),++ transparent: true, blending: THREE.AdditiveBlending, depthWrite: false,++ }));++ fl.position.set(n.group.position.x, 1.1, n.group.position.z);++ fl.scale.set(1.2, 2, 1);++ scene.add(fl);++ let life = 1;++ const fade = () => {++ life -= .06;++ fl.material.opacity = Math.max(0, life);++ fl.position.y += .05;++ if (life > 0) requestAnimationFrame(fade);++ else scene.remove(fl);++ };++ fade();++ }}net.on('npcs', (m) => {const seen = new Set();@@@ -1224,6 +1272,11 @@ function start(token, user) {});// ---------- physgun: hold LMB to grab · wheel push/pull · R spin · X delete ----------++ const muzzleV = new THREE.Vector3();++ function myBeamFrom() { // the beam starts at the GUN, gmod-style++ if (fp) return camera.localToWorld(muzzleV.set(.31, -.26, -.75)); // viewmodel gun tip++ return myAvatar.heldAnchor.getWorldPosition(muzzleV);++ }const pgState = { held: null, dist: 0, lmb: false, missT: 0 };const pgRay = new THREE.Raycaster();function pgEquipped() { return my.held === 'physgun'; }@@@ -1392,6 +1445,7 @@ function start(token, user) {}});++ addEventListener('pagehide', () => { try { invApi.flush(); } catch {} });connect(token, {onOpen: () => {},onClose: (e) => {@@@ -218,7 +218,12 @@ setInterval(() => { // 5Hz herd-and-horde tickfor (const p of players.values()) { const dd = Math.hypot(p.x - n.x, p.z - n.z); if (dd < bd) { bd = dd; best = p; } }if (best) {n.ry = Math.atan2(best.x - n.x, best.z - n.z);-- if (bd > 1.4) { n.x += Math.sin(n.ry) * sp * step; n.z += Math.cos(n.ry) * sp * step; }++ if (bd > 1.4) {++ const nx2 = n.x + Math.sin(n.ry) * sp * step, nz2 = n.z + Math.cos(n.ry) * sp * step;++ // the facility is lit — zombies won't set foot inside++ const INDOORS = [[58, 92, 31, 107], [62, 92, -17, 17], [58, 92, 17, 31], [52.5, 58, 39, 99], [48, 53, 63, 82]];++ if (!INDOORS.some(([x0, x1, z0, z1]) => nx2 >= x0 && nx2 <= x1 && nz2 >= z0 && nz2 <= z1)) { n.x = nx2; n.z = nz2; }++ }else if (Date.now() - n.atkT > 1200) {n.atkT = Date.now();best.hp = (best.hp ?? 100) - 8;@@@ -672,7 +677,11 @@ function handle(p, m) {if (!s) return;if (Math.hypot(p.x - s.x, p.z - s.z) > maxRange) return;s.hp = (s.hp ?? 40) - dmg; // sleepers are fragile-- if (s.hp > 0) { worldDirty = true; return; }++ if (s.hp > 0) {++ worldDirty = true;++ broadcast({ t: 'sleephurt', key, by: p.id }); // red flash + hitmarker++ return;++ }sleepers.delete(key);spillInventory(key, s.x, s.z);stats.kills++;
@@ -64,6 +64,10 @@ export function initInventory({ me, onEquip, onWear, onDropItem, toast }) {
@@ -64,6 +64,10 @@ export function initInventory({ me, onEquip, onWear, onDropItem, toast }) {
clearTimeout(saveT);
clearTimeout(saveT);
saveT = setTimeout(() => net.send({ t: 'inv', inv: inv.slots, hotbar: inv.hotbar }), 400);
saveT = setTimeout(() => net.send({ t: 'inv', inv: inv.slots, hotbar: inv.hotbar }), 400);
};
};
+ const flush = () => { // fire the pending save NOW (tab closing)
+ clearTimeout(saveT);
+ net.send({ t: 'inv', inv: inv.slots, hotbar: inv.hotbar });
+ };
// ---------- DOM ----------
// ---------- DOM ----------
const hotbarEl = document.getElementById('hotbar');
const hotbarEl = document.getElementById('hotbar');
@@ -288,5 +292,5 @@ export function initInventory({ me, onEquip, onWear, onDropItem, toast }) {
@@ -288,5 +292,5 @@ export function initInventory({ me, onEquip, onWear, onDropItem, toast }) {
if (el?.dataset?.bar !== undefined) select(+el.dataset.bar);
if (el?.dataset?.bar !== undefined) select(+el.dataset.bar);
});
});
- return { add, select, useSelected, dropSelected, count, consume, toggle, selectedItem, restore, state: inv };
+ return { add, select, useSelected, dropSelected, count, consume, toggle, selectedItem, restore, flush, state: inv };
}
}
@@ -119,10 +119,13 @@ export function initLighting(scene, renderer, shadows = true) {
@@ -119,10 +119,13 @@ export function initLighting(scene, renderer, shadows = true) {
scene.background.copy(sky);
scene.background.copy(sky);
scene.fog.color.copy(sky);
scene.fog.color.copy(sky);
- sun.intensity = 1.4 * d;
+ // no direct sun indoors: interiors are lit by the fluorescents, and sun
+ // spilling "through the roof" painted giant moving shadow-boundary wedges
+ sun.intensity = 1.4 * d * (1 - indoorK * .92);
sun.color.copy(sunWarm).lerp(sunNoon, smooth(.05, .45, elev));
sun.color.copy(sunWarm).lerp(sunNoon, smooth(.05, .45, elev));
- sun.position.set(px + Math.sin(az) * 80, Math.max(4, elev * 90), pz + Math.cos(az) * 40);
- sun.target.position.set(px, 0, pz);
+ const qx = Math.round(px / 4) * 4, qz = Math.round(pz / 4) * 4; // quantized follow = less boundary swim
+ sun.position.set(qx + Math.sin(az) * 80, Math.max(4, elev * 90), qz + Math.cos(az) * 40);
+ sun.target.position.set(qx, 0, qz);
moon.intensity = .22 * (1 - d);
moon.intensity = .22 * (1 - d);
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);
@@ -757,7 +757,7 @@ function start(token, user) {
@@ -757,7 +757,7 @@ function start(token, user) {
root.add(zz);
root.add(zz);
enableShadows(root);
enableShadows(root);
scene.add(root);
scene.add(root);
- sleeperMap.set(s.key, { root, zz, t: Math.random() * 6 });
+ sleeperMap.set(s.key, { root, zz, av, t: Math.random() * 6 });
}
}
function removeSleeper(key) {
function removeSleeper(key) {
const e = sleeperMap.get(key);
const e = sleeperMap.get(key);
@@ -767,6 +767,10 @@ function start(token, user) {
@@ -767,6 +767,10 @@ function start(token, user) {
}
}
net.on('sleep', (m) => addSleeper(m.s));
net.on('sleep', (m) => addSleeper(m.s));
net.on('wake', (m) => removeSleeper(m.key));
net.on('wake', (m) => removeSleeper(m.key));
+ net.on('sleephurt', (m) => {
+ sleeperMap.get(m.key)?.av?.flash?.();
+ if (m.by === myId) hitmarker(false);
+ });
// ---------- dropped items on the floor (minecraft style) ----------
// ---------- dropped items on the floor (minecraft style) ----------
const worldDrops = new Map(); // id -> {group, mesh, inter, t}
const worldDrops = new Map(); // id -> {group, mesh, inter, t}
@@ -900,7 +904,29 @@ function start(token, user) {
@@ -900,7 +904,29 @@ function start(token, user) {
const skin = M2(0x6a9c4e, .9), shirt = M2(0x3a3f46, .95);
const skin = M2(0x6a9c4e, .9), shirt = M2(0x3a3f46, .95);
g.add(bx(.44, .62, .26, shirt, 0, 1.12, 0));
g.add(bx(.44, .62, .26, shirt, 0, 1.12, 0));
head = bx(.3, .3, .3, skin, 0, 1.62, 0);
head = bx(.3, .3, .3, skin, 0, 1.62, 0);
+ // ember eyes so they read at night
+ const eyeM = new THREE.MeshStandardMaterial({ color: 0xff7a2a, emissive: 0xff7a2a, emissiveIntensity: 2 });
+ head.add(bx(.05, .04, .02, eyeM, -.07, .03, .16));
+ head.add(bx(.05, .04, .02, eyeM, .07, .03, .16));
g.add(head);
g.add(head);
+ // a lit torch in hand — see them coming
+ const torchG = new THREE.Group();
+ torchG.add(bx(.05, .4, .05, M2(0x5a4028, .9), 0, 0, 0));
+ const flameM = new THREE.MeshStandardMaterial({ color: 0xffb42e, emissive: 0xff8a1c, emissiveIntensity: 2.6 });
+ torchG.add(bx(.1, .16, .1, flameM, 0, .27, 0));
+ const glow = new THREE.Sprite(new THREE.SpriteMaterial({
+ map: ct(64, 64, (gg, w2, h2) => {
+ const gr = gg.createRadialGradient(w2 / 2, h2 / 2, 3, w2 / 2, h2 / 2, 30);
+ gr.addColorStop(0, 'rgba(255,190,90,.9)'); gr.addColorStop(1, 'rgba(255,140,30,0)');
+ gg.fillStyle = gr; gg.fillRect(0, 0, w2, h2);
+ }),
+ transparent: true, blending: THREE.AdditiveBlending, depthWrite: false,
+ }));
+ glow.scale.set(1.1, 1.1, 1);
+ glow.position.y = .3;
+ torchG.add(glow);
+ torchG.position.set(.42, 1.25, .3);
+ g.add(torchG);
for (const ax of [-.3, .3]) { // arms out front, classic
for (const ax of [-.3, .3]) { // arms out front, classic
const arm = bx(.11, .11, .55, skin, ax, 1.3, .34);
const arm = bx(.11, .11, .55, skin, ax, 1.3, .34);
arms.push(arm); g.add(arm);
arms.push(arm); g.add(arm);
@@ -929,7 +955,29 @@ function start(token, user) {
@@ -929,7 +955,29 @@ function start(token, user) {
if (!n) return;
if (!n) return;
scene.remove(n.group);
scene.remove(n.group);
npcMap.delete(id);
npcMap.delete(id);
- if (burn) beep(190, .3, 'sawtooth', .08);
+ if (burn) { // minecraft sunrise: they go up in flames
+ beep(190, .3, 'sawtooth', .08);
+ const fl = new THREE.Sprite(new THREE.SpriteMaterial({
+ map: ct(64, 96, (gg, w2, h2) => {
+ const gr = gg.createLinearGradient(0, h2, 0, 0);
+ gr.addColorStop(0, 'rgba(255,120,20,.95)'); gr.addColorStop(.7, 'rgba(255,200,60,.5)'); gr.addColorStop(1, 'rgba(255,240,160,0)');
+ gg.fillStyle = gr; gg.fillRect(0, 0, w2, h2);
+ }),
+ transparent: true, blending: THREE.AdditiveBlending, depthWrite: false,
+ }));
+ fl.position.set(n.group.position.x, 1.1, n.group.position.z);
+ fl.scale.set(1.2, 2, 1);
+ scene.add(fl);
+ let life = 1;
+ const fade = () => {
+ life -= .06;
+ fl.material.opacity = Math.max(0, life);
+ fl.position.y += .05;
+ if (life > 0) requestAnimationFrame(fade);
+ else scene.remove(fl);
+ };
+ fade();
+ }
}
}
net.on('npcs', (m) => {
net.on('npcs', (m) => {
const seen = new Set();
const seen = new Set();
@@ -1224,6 +1272,11 @@ function start(token, user) {
@@ -1224,6 +1272,11 @@ function start(token, user) {
});
});
// ---------- physgun: hold LMB to grab · wheel push/pull · R spin · X delete ----------
// ---------- physgun: hold LMB to grab · wheel push/pull · R spin · X delete ----------
+ const muzzleV = new THREE.Vector3();
+ function myBeamFrom() { // the beam starts at the GUN, gmod-style
+ if (fp) return camera.localToWorld(muzzleV.set(.31, -.26, -.75)); // viewmodel gun tip
+ return myAvatar.heldAnchor.getWorldPosition(muzzleV);
+ }
const pgState = { held: null, dist: 0, lmb: false, missT: 0 };
const pgState = { held: null, dist: 0, lmb: false, missT: 0 };
const pgRay = new THREE.Raycaster();
const pgRay = new THREE.Raycaster();
function pgEquipped() { return my.held === 'physgun'; }
function pgEquipped() { return my.held === 'physgun'; }
@@ -1392,6 +1445,7 @@ function start(token, user) {
@@ -1392,6 +1445,7 @@ function start(token, user) {
}
}
});
});
+ addEventListener('pagehide', () => { try { invApi.flush(); } catch {} });
connect(token, {
connect(token, {
onOpen: () => {},
onOpen: () => {},
onClose: (e) => {
onClose: (e) => {
@@ -218,7 +218,12 @@ setInterval(() => { // 5Hz herd-and-horde tick
@@ -218,7 +218,12 @@ setInterval(() => { // 5Hz herd-and-horde tick
for (const p of players.values()) { const dd = Math.hypot(p.x - n.x, p.z - n.z); if (dd < bd) { bd = dd; best = p; } }
for (const p of players.values()) { const dd = Math.hypot(p.x - n.x, p.z - n.z); if (dd < bd) { bd = dd; best = p; } }
if (best) {
if (best) {
n.ry = Math.atan2(best.x - n.x, best.z - n.z);
n.ry = Math.atan2(best.x - n.x, best.z - n.z);
- if (bd > 1.4) { n.x += Math.sin(n.ry) * sp * step; n.z += Math.cos(n.ry) * sp * step; }
+ if (bd > 1.4) {
+ const nx2 = n.x + Math.sin(n.ry) * sp * step, nz2 = n.z + Math.cos(n.ry) * sp * step;
+ // the facility is lit — zombies won't set foot inside
+ const INDOORS = [[58, 92, 31, 107], [62, 92, -17, 17], [58, 92, 17, 31], [52.5, 58, 39, 99], [48, 53, 63, 82]];
+ if (!INDOORS.some(([x0, x1, z0, z1]) => nx2 >= x0 && nx2 <= x1 && nz2 >= z0 && nz2 <= z1)) { n.x = nx2; n.z = nz2; }
+ }
else if (Date.now() - n.atkT > 1200) {
else if (Date.now() - n.atkT > 1200) {
n.atkT = Date.now();
n.atkT = Date.now();
best.hp = (best.hp ?? 100) - 8;
best.hp = (best.hp ?? 100) - 8;
@@ -672,7 +677,11 @@ function handle(p, m) {
@@ -672,7 +677,11 @@ function handle(p, m) {
if (!s) return;
if (!s) return;
if (Math.hypot(p.x - s.x, p.z - s.z) > maxRange) return;
if (Math.hypot(p.x - s.x, p.z - s.z) > maxRange) return;
s.hp = (s.hp ?? 40) - dmg; // sleepers are fragile
s.hp = (s.hp ?? 40) - dmg; // sleepers are fragile
- if (s.hp > 0) { worldDirty = true; return; }
+ if (s.hp > 0) {
+ worldDirty = true;
+ broadcast({ t: 'sleephurt', key, by: p.id }); // red flash + hitmarker
+ return;
+ }
sleepers.delete(key);
sleepers.delete(key);
spillInventory(key, s.x, s.z);
spillInventory(key, s.x, s.z);
stats.kills++;
stats.kills++;