update Tue 07/14/2026 16:45:47.62
This commit is contained in:
@@ -121,7 +121,8 @@ function scheduleRemove(uid) {
|
||||
const e = activeGhosts.get(uid);
|
||||
if (!e) return removeGhost(uid);
|
||||
// let the client-side crossfade (driven by rec.until) finish, then remove
|
||||
const wait = Math.max(0, e.rec.until - (Date.now() + clockOffset)) + (e.rec.crossfade || 3) * 1000;
|
||||
const until = e.rec.until ?? (Date.now() + clockOffset); // residents removed by config change: fade now
|
||||
const wait = Math.max(0, until - (Date.now() + clockOffset)) + (e.rec.crossfade || 3) * 1000;
|
||||
setTimeout(() => removeGhost(uid), Math.min(wait, 8000));
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ export function ghostTransform(rec, nowMs, out) {
|
||||
const base = new THREE.Vector3(...rec.pos);
|
||||
const b = rec.behavior || { type: 'static' };
|
||||
|
||||
if (b.type === 'wander') {
|
||||
if (b.type === 'path') {
|
||||
pathTransform(b, t, out);
|
||||
} else if (b.type === 'wander') {
|
||||
const s = b.seed || 1;
|
||||
const R = b.radius ?? 60; // cm
|
||||
const v = b.speed ?? 0.15;
|
||||
@@ -44,10 +46,51 @@ export function ghostTransform(rec, nowMs, out) {
|
||||
out.rotationY = 0.25 * Math.sin(t * 0.3); // slow idle sway
|
||||
}
|
||||
|
||||
// crossfade opacity: fade in on spawn, fade out approaching `until`
|
||||
// crossfade opacity: fade in on spawn; permanent residents (until == null) never fade out
|
||||
const fade = (rec.crossfade || 3) * 1000;
|
||||
const inA = Math.min(1, (nowMs - rec.spawnedAt) / fade);
|
||||
const outA = Math.min(1, Math.max(0, (rec.until - nowMs) / fade));
|
||||
const outA = rec.until == null ? 1 : Math.min(1, Math.max(0, (rec.until - nowMs) / fade));
|
||||
out.opacity = Math.min(inA, outA);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Walk a waypoint path at constant speed (cm/s), turning to face travel direction.
|
||||
* Deterministic: position is a pure function of elapsed time, so all viewers agree.
|
||||
* b: { points:[[x,y,z],...], mode:'loop'|'pingpong', speed, phase, seed } */
|
||||
function pathTransform(b, t, out) {
|
||||
const raw = b.points || [];
|
||||
if (raw.length < 2) { out.position.set(...(raw[0] || [0, 0, 0])); out.rotationY = 0; return; }
|
||||
const pts = b.mode === 'loop' ? [...raw, raw[0]] : raw;
|
||||
|
||||
// cumulative segment lengths
|
||||
const cum = [0];
|
||||
for (let i = 1; i < pts.length; i++) {
|
||||
const dx = pts[i][0] - pts[i - 1][0], dy = pts[i][1] - pts[i - 1][1], dz = pts[i][2] - pts[i - 1][2];
|
||||
cum.push(cum[i - 1] + Math.hypot(dx, dy, dz));
|
||||
}
|
||||
const total = cum[cum.length - 1] || 1;
|
||||
|
||||
const wrap = (d) => {
|
||||
if (b.mode === 'pingpong') { const m = ((d % (2 * total)) + 2 * total) % (2 * total); return m < total ? m : 2 * total - m; }
|
||||
return ((d % total) + total) % total;
|
||||
};
|
||||
const sample = (d, v) => {
|
||||
let i = 1; while (i < cum.length - 1 && cum[i] < d) i++;
|
||||
const f = (d - cum[i - 1]) / Math.max(cum[i] - cum[i - 1], 1e-6);
|
||||
v.set(
|
||||
pts[i - 1][0] + (pts[i][0] - pts[i - 1][0]) * f,
|
||||
pts[i - 1][1] + (pts[i][1] - pts[i - 1][1]) * f,
|
||||
pts[i - 1][2] + (pts[i][2] - pts[i - 1][2]) * f);
|
||||
return v;
|
||||
};
|
||||
|
||||
const d = wrap(t * (b.speed ?? 8) + (b.phase || 0));
|
||||
sample(d, out.position);
|
||||
// face where we're heading: sample a few cm ahead so corners turn smoothly
|
||||
const ahead = sample(wrap(t * (b.speed ?? 8) + (b.phase || 0) + 6), _look);
|
||||
const dx = ahead.x - out.position.x, dz = ahead.z - out.position.z;
|
||||
if (dx * dx + dz * dz > 1e-4) out.rotationY = Math.atan2(dx, dz);
|
||||
// gentle float so walking still reads ghostly
|
||||
out.position.y += 2 * Math.sin(t * 1.9 + (b.seed || 0));
|
||||
}
|
||||
const _look = new THREE.Vector3();
|
||||
|
||||
Reference in New Issue
Block a user