@oddessentials/agent-guild 1.30.1 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agent-guild.mjs +52 -30
- package/bin/native/win32-arm64/agent-guild-report.exe +0 -0
- package/bin/native/win32-x64/agent-guild-report.exe +0 -0
- package/bin/windows/arm64/agent-guild-start.exe +0 -0
- package/bin/windows/x64/agent-guild-start.exe +0 -0
- package/docs/api.md +64 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/package.json +1 -1
- package/src/manager/command-resolver.mjs +4 -2
- package/src/manager/server.mjs +10 -5
- package/src/manager/session-hooks.mjs +2 -1
- package/src/manager/session.mjs +85 -4
- package/src/manager/shell-env.mjs +4 -2
- package/src/mcp/server.mjs +77 -4
- package/web/index.html +1 -1
- package/web/yard/art.json +1 -1
- package/web/yard/model.mjs +133 -0
- package/web/yard/renderer.js +85 -24
- package/web/yard/view.js +20 -1
- package/web/yard/yard.css +1 -0
package/web/yard/model.mjs
CHANGED
|
@@ -434,6 +434,139 @@ export function clipFor(id, mode, n) {
|
|
|
434
434
|
if (mode === 'work') return n % 3 === 2 && h % 2 ? set[1] : set[0];
|
|
435
435
|
return n % 3 === 2 ? set[1 + (h % (set.length - 1))] : set[0];
|
|
436
436
|
}
|
|
437
|
+
export const VIBE = {
|
|
438
|
+
// Must stay inside the ground every world keeps clear (Clearance.live_distance in agent-guild-builders).
|
|
439
|
+
roam: { radius: 13.2, half: 12.5, front: 30.5, split: 3, margin: .9, edge: 1.2, head: 2.4 },
|
|
440
|
+
flight: { height: [8.5, 12], lap: [26, 44], reach: [.45, 1], scale: 1.3, top: 2 },
|
|
441
|
+
};
|
|
442
|
+
export function vibeFrame(aspect, top = 0, bottom = 0) {
|
|
443
|
+
const { target, zoom } = overviewFor(aspect), height = CAMERA.height / zoom, width = height * aspect, { edge } = VIBE.roam;
|
|
444
|
+
return { target, right: [edge - width / 2, width / 2 - edge], up: [(bottom - .5) * height + edge, (.5 - top) * height - edge] };
|
|
445
|
+
}
|
|
446
|
+
export function inVibeFrame(frame, x, y, z) {
|
|
447
|
+
const { right, up } = viewBasis(), d = [x - frame.target[0], y - frame.target[1], z - frame.target[2]];
|
|
448
|
+
const r = dot(d, right), u = dot(d, up);
|
|
449
|
+
return r >= frame.right[0] && r <= frame.right[1] && u >= frame.up[0] && u <= frame.up[1];
|
|
450
|
+
}
|
|
451
|
+
function unit(value) {
|
|
452
|
+
let h = hash(value);
|
|
453
|
+
h = Math.imul(h ^ (h >>> 16), 0x45d9f3b); h = Math.imul(h ^ (h >>> 16), 0x45d9f3b);
|
|
454
|
+
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
455
|
+
}
|
|
456
|
+
// Clip the visible ground polygon to an axis-aligned interval. Destination
|
|
457
|
+
// geometry only: this neither builds nor checks a route.
|
|
458
|
+
function vibeClip(points, axis, low, high) {
|
|
459
|
+
for (const [edge, sign] of [[low, 1], [high, -1]]) {
|
|
460
|
+
const next = [];
|
|
461
|
+
for (let i = 0; i < points.length; i++) {
|
|
462
|
+
const a = points[i], b = points[(i + 1) % points.length], da = sign * (a[axis] - edge), db = sign * (b[axis] - edge);
|
|
463
|
+
if (da >= 0) next.push(a);
|
|
464
|
+
if ((da < 0) !== (db < 0)) { const t = da / (da - db); next.push({ x: a.x + t * (b.x - a.x), z: a.z + t * (b.z - a.z) }); }
|
|
465
|
+
}
|
|
466
|
+
points = next;
|
|
467
|
+
}
|
|
468
|
+
return points;
|
|
469
|
+
}
|
|
470
|
+
// A missed sample is not an empty view. Obstacle x edges divide the ground
|
|
471
|
+
// into at most 2B+1 bands, each with at most B+1 free z intervals. Check those
|
|
472
|
+
// finite regions, not a grid whose spacing could miss a narrow opening.
|
|
473
|
+
function vibeGap(polygon, boxes, near, far, seed) {
|
|
474
|
+
const { radius, half, front } = VIBE.roam;
|
|
475
|
+
polygon = vibeClip(vibeClip(polygon, 'x', -radius, radius), 'z', near, far);
|
|
476
|
+
const point = points => {
|
|
477
|
+
if (points.length < 3) return null;
|
|
478
|
+
const area = points.reduce((sum, a, i) => { const b = points[(i + 1) % points.length]; return sum + a.x * b.z - b.x * a.z; }, 0);
|
|
479
|
+
if (Math.abs(area) < 1e-10) return null;
|
|
480
|
+
const center = { x: 0, z: 0 };
|
|
481
|
+
for (const p of points) { center.x += p.x / points.length; center.z += p.z / points.length; }
|
|
482
|
+
const p = points[Math.floor(seed * points.length)], t = .25 + seed / 2;
|
|
483
|
+
return { x: center.x + (p.x - center.x) * t, z: center.z + (p.z - center.z) * t };
|
|
484
|
+
};
|
|
485
|
+
const ground = points => {
|
|
486
|
+
const p = point(points);
|
|
487
|
+
if (!p) return null;
|
|
488
|
+
if (Math.hypot(p.x, p.z) < radius || (Math.abs(p.x) <= half && p.z >= 0 && p.z <= front)) return p;
|
|
489
|
+
const apron = point(vibeClip(vibeClip(points, 'x', -half, half), 'z', 0, front));
|
|
490
|
+
if (apron) return apron;
|
|
491
|
+
// The plaza's obstacle excludes the circle's centre from every free
|
|
492
|
+
// region, so its nearest point is on an edge. Move from it into the
|
|
493
|
+
// polygon, staying inside the circle even for a small circular sliver.
|
|
494
|
+
let nearest = null, distance = Infinity;
|
|
495
|
+
for (let i = 0; i < points.length; i++) {
|
|
496
|
+
const a = points[i], b = points[(i + 1) % points.length], dx = b.x - a.x, dz = b.z - a.z;
|
|
497
|
+
const t = clamp(-(a.x * dx + a.z * dz) / (dx * dx + dz * dz || 1), 0, 1), q = { x: a.x + t * dx, z: a.z + t * dz }, d = Math.hypot(q.x, q.z);
|
|
498
|
+
if (d < distance) { nearest = q; distance = d; }
|
|
499
|
+
}
|
|
500
|
+
if (distance >= radius) return null;
|
|
501
|
+
const t = Math.min(.5, (radius - distance) / (2 * Math.hypot(p.x - nearest.x, p.z - nearest.z)));
|
|
502
|
+
return { x: nearest.x + t * (p.x - nearest.x), z: nearest.z + t * (p.z - nearest.z) };
|
|
503
|
+
};
|
|
504
|
+
if (!point(polygon)) return null;
|
|
505
|
+
const left = Math.min(...polygon.map(p => p.x)), right = Math.max(...polygon.map(p => p.x));
|
|
506
|
+
const edges = [...new Set([left, right, ...boxes.flatMap(b => [b.x - b.half, b.x + b.half]).filter(x => x > left && x < right)])].sort((a, b) => a - b);
|
|
507
|
+
const start = Math.floor(seed * (edges.length - 1));
|
|
508
|
+
for (let i = 0; i < edges.length - 1; i++) {
|
|
509
|
+
const k = (start + i) % (edges.length - 1), lo = edges[k], hi = edges[k + 1], x = (lo + hi) / 2;
|
|
510
|
+
const blocked = boxes.filter(b => Math.abs(x - b.x) < b.half).map(b => [b.z - b.half, b.z + b.half]).sort((a, b) => a[0] - b[0]);
|
|
511
|
+
let z = near;
|
|
512
|
+
for (const [low, high] of [...blocked, [far, far]]) {
|
|
513
|
+
if (low > z) {
|
|
514
|
+
const p = ground(vibeClip(vibeClip(polygon, 'x', lo, hi), 'z', z, Math.min(low, far)));
|
|
515
|
+
if (p) return p;
|
|
516
|
+
}
|
|
517
|
+
z = Math.max(z, high);
|
|
518
|
+
if (z >= far) break;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
export function vibeStop(id, n, providers, props = [], frame = null) {
|
|
524
|
+
const { radius, half, front, split, margin, head } = VIBE.roam;
|
|
525
|
+
const [near, far] = (n + hash(id)) % 2 ? [split, front] : [-radius, split];
|
|
526
|
+
const boxes = [...obstacles(providers).map(box => ({ ...box, half: box.half + margin })),
|
|
527
|
+
...props.map(p => ({ x: p.x, z: p.z, half: STAGING.station.half + margin }))];
|
|
528
|
+
const clear = (x, z) => (Math.hypot(x - STAGING.plaza.x, z - STAGING.plaza.z) <= radius || (Math.abs(x) <= half && z >= 0 && z <= front))
|
|
529
|
+
&& !boxes.some(box => Math.abs(x - box.x) < box.half && Math.abs(z - box.z) < box.half);
|
|
530
|
+
if (frame) {
|
|
531
|
+
const { right, up, forward } = viewBasis(), [r0, r1] = frame.right, u0 = frame.up[0], u1 = frame.up[1] - head * up[1];
|
|
532
|
+
const at = (r, u) => {
|
|
533
|
+
const s = (-frame.target[1] - u * up[1]) / forward[1];
|
|
534
|
+
return { x: frame.target[0] + r * right[0] + u * up[0] + s * forward[0], z: frame.target[2] + r * right[2] + u * up[2] + s * forward[2] };
|
|
535
|
+
};
|
|
536
|
+
if (r1 > r0 && u1 > u0) {
|
|
537
|
+
const polygon = [at(r0, u0), at(r1, u0), at(r1, u1), at(r0, u1)];
|
|
538
|
+
for (const [low, high] of [[near, far], near === split ? [-radius, split] : [split, front]]) {
|
|
539
|
+
for (let k = 0; k < 48; k++) {
|
|
540
|
+
const p = at(r0 + unit(id + ':vibe:x:' + n + ':' + k) * (r1 - r0), u0 + unit(id + ':vibe:z:' + n + ':' + k) * (u1 - u0));
|
|
541
|
+
const x = round(p.x), z = round(p.z);
|
|
542
|
+
if (z >= low && z <= high && clear(x, z) && inVibeFrame(frame, x, 0, z) && inVibeFrame(frame, x, head, z)) return { x, z };
|
|
543
|
+
}
|
|
544
|
+
const p = vibeGap(polygon, boxes, low, high, unit(id + ':vibe:gap:' + n));
|
|
545
|
+
if (p) return p;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// No usable visible ground (or no frame): keep the original seeded walk.
|
|
550
|
+
// Never turn a crowded/covered view into a stationary, repeatedly retried unit.
|
|
551
|
+
for (let k = 0; k < 48; k++) {
|
|
552
|
+
const x = round((unit(id + ':vibe:x:' + n + ':' + k) * 2 - 1) * radius), z = round(near + unit(id + ':vibe:z:' + n + ':' + k) * (far - near));
|
|
553
|
+
if (clear(x, z)) return { x, z };
|
|
554
|
+
}
|
|
555
|
+
return { x: STAGING.plaza.x, z: round(STAGING.plaza.z + STAGING.plaza.half + margin + STAGING.station.half) };
|
|
556
|
+
}
|
|
557
|
+
export function vibeFlight(key, seconds, frame, companionHeight = 1) {
|
|
558
|
+
const { height, lap, reach, top, scale } = VIBE.flight, { right, up, forward } = viewBasis();
|
|
559
|
+
const pick = (name, [low, high]) => low + unit(key + ':vibe:' + name) * (high - low);
|
|
560
|
+
const out = pick('reach', reach), turn = unit(key + ':vibe:turn') < .5 ? 1 : -1, lobes = 2 + Math.floor(unit(key + ':vibe:lobes') * 3);
|
|
561
|
+
const angle = turn * 2 * Math.PI * (seconds / pick('lap', lap) + unit(key + ':vibe:start'));
|
|
562
|
+
const wide = out * (1 + .12 * Math.sin(lobes * angle + pick('phase', [0, 2 * Math.PI]))) / 1.12;
|
|
563
|
+
const y = (height[0] + height[1]) / 2 + (height[1] - height[0]) / 2 * Math.sin(2 * angle + pick('rise', [0, 2 * Math.PI]));
|
|
564
|
+
const [r0, r1] = frame.right, u0 = frame.up[0], u1 = frame.up[1] - Math.max(top, companionHeight * scale * up[1]);
|
|
565
|
+
const r = (r0 + r1) / 2 + Math.cos(angle) * Math.max(0, r1 - r0) / 2 * wide;
|
|
566
|
+
const u = (u0 + u1) / 2 + Math.sin(angle) * Math.max(0, u1 - u0) / 2 * wide;
|
|
567
|
+
const s = (y - frame.target[1] - u * up[1]) / forward[1];
|
|
568
|
+
return { x: frame.target[0] + r * right[0] + u * up[0] + s * forward[0], y, z: frame.target[2] + r * right[2] + u * up[2] + s * forward[2] };
|
|
569
|
+
}
|
|
437
570
|
// Where a session's helpers stand, around it, as the renderer has always placed them.
|
|
438
571
|
export function helperOffset(i, count) {
|
|
439
572
|
const angle = (i / Math.min(count, 6)) * Math.PI * 2;
|
package/web/yard/renderer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as T from './vendor/engine.js';
|
|
2
2
|
import { WORLDS, PROVIDER_ORDER, characterIndex, GILDED, CAMERA, SUN, minZoom, overviewFor, clampPan, viewBasis, providerPositions, sessionPose, familiarPose, hash,
|
|
3
|
-
stage, nextStagingChange, doorFor, arrives, route, clipFor, helperOffset, stationProp, hallActivity, lookTarget, lookLimits, yardExtras, playableClip, STAGING, photoSize, marksFor, SHOWN_HELPERS } from './model.mjs';
|
|
3
|
+
stage, nextStagingChange, doorFor, arrives, route, clipFor, helperOffset, stationProp, hallActivity, lookTarget, lookLimits, yardExtras, playableClip, STAGING, photoSize, marksFor, SHOWN_HELPERS, VIBE, vibeStop, vibeFlight, vibeFrame, inVibeFrame } from './model.mjs';
|
|
4
4
|
|
|
5
5
|
const ASSETS = new URL('./assets/', import.meta.url);
|
|
6
6
|
const vector = new T.Vector3();
|
|
@@ -16,7 +16,7 @@ const pieceFiles = (skin, slot, piece) => (slot === 'companion' ? [0, 1, 2, 3].m
|
|
|
16
16
|
const vector2 = new T.Vector2();
|
|
17
17
|
// How fast characters turn to a new heading, in radians a second, and how
|
|
18
18
|
// long a clip change blends.
|
|
19
|
-
const TURN = 7, FADE = .3;
|
|
19
|
+
const TURN = 7, FADE = .3, TAG = 44;
|
|
20
20
|
// Models rigged before the staging contract name their clips differently.
|
|
21
21
|
const LEGACY = { walk: 'working', idle_0: 'resting', idle_1: 'resting', idle_2: 'resting', pause: 'resting', work_0: 'working', work_1: 'working', idle: 'resting', move: 'working' };
|
|
22
22
|
// How bright a hall's glow grows at low and high activity, over what the
|
|
@@ -139,15 +139,15 @@ function disposeWorld(root) {
|
|
|
139
139
|
}
|
|
140
140
|
export class YardRenderer {
|
|
141
141
|
// clock gives the time staging is judged at; tests pass a fixed one.
|
|
142
|
-
constructor(host,labels,{select,open,error=noop,piece=noop,clock=Date.now}) {
|
|
143
|
-
this.host=host;this.labels=labels;this.onSelect=select;this.onOpen=open;this.onError=error;this.onPiece=piece;this.clock=clock;
|
|
142
|
+
constructor(host,labels,{select,open,error=noop,piece=noop,clock=Date.now,cover=[]}) {
|
|
143
|
+
this.host=host;this.labels=labels;this.cover=cover;this.onSelect=select;this.onOpen=open;this.onError=error;this.onPiece=piece;this.clock=clock;
|
|
144
144
|
this.pieceSets={};this.shownPieces={skin:null,pieces:{}};this.pieceLoaders=new Map();this.preloads=new Map();
|
|
145
145
|
this.scene=new T.Scene();this.units=new Map();this.halls=new Map();this.slots=new Map();this.cache=new Map();
|
|
146
146
|
this.loadingManager=new T.LoadingManager();
|
|
147
147
|
this.providers=[];this.sessions=[];this.selected=null;this.world=null;this.skin=null;this.request=0;this.worldRequest=0;this.pending=null;
|
|
148
148
|
this.staged=null;this.seen=new Set();this.stageTimer=0;this.sparks=[];this.shows=[];this.marks=new T.Group();this.scene.add(this.marks);this.glyphs=null;this.markPlane=new T.PlaneGeometry(1,1);
|
|
149
149
|
this.props=new Map();this.propSpots=[];this.station=null;this.effects=null;
|
|
150
|
-
this.active=false;this.reduced=false;this.disposed=false;this.frame=0;this.lastTime=0;this.dirty=true;
|
|
150
|
+
this.active=false;this.reduced=false;this.vibe=false;this.vibeTime=0;this.vibeFrames={stop:null,flight:vibeFrame(1)};this.disposed=false;this.frame=0;this.lastTime=0;this.dirty=true;
|
|
151
151
|
this.crowd=null;this.rows=new Map();
|
|
152
152
|
this.list=Object.assign(document.createElement('div'),{className:'yard-crowd-list',id:'yard-crowd-list-'+(++crowdLists),hidden:true});
|
|
153
153
|
this.list.setAttribute('role','group');
|
|
@@ -195,7 +195,7 @@ export class YardRenderer {
|
|
|
195
195
|
this.canvas.addEventListener('pointerdown',this.onPointerDown);
|
|
196
196
|
this.canvas.addEventListener('click',this.onClick);
|
|
197
197
|
this.canvas.addEventListener('dblclick',this.onDouble);
|
|
198
|
-
this.resizeObserver=new ResizeObserver(()=>this.resize());this.resizeObserver.observe(
|
|
198
|
+
this.resizeObserver=new ResizeObserver(()=>this.resize());for(const el of [host,...cover])this.resizeObserver.observe(el);
|
|
199
199
|
this.resize();this.overview();
|
|
200
200
|
}
|
|
201
201
|
async asset(name,url) {
|
|
@@ -482,8 +482,22 @@ export class YardRenderer {
|
|
|
482
482
|
this.camera.zoom=T.MathUtils.clamp(this.camera.zoom,this.controls.minZoom,this.controls.maxZoom);
|
|
483
483
|
this.camera.updateProjectionMatrix();
|
|
484
484
|
this.dirty=true;
|
|
485
|
+
this.frameVibe(w,h);
|
|
485
486
|
if(this.atOverview)this.overview();else this.drawOnce();
|
|
486
487
|
}
|
|
488
|
+
frameVibe(w,h) {
|
|
489
|
+
const box=this.host.getBoundingClientRect();let top=0,bottom=0;
|
|
490
|
+
for(const el of this.cover){
|
|
491
|
+
const r=el.getBoundingClientRect();
|
|
492
|
+
if(!r.width||!r.height)continue;
|
|
493
|
+
if(r.top+r.bottom<box.top+box.bottom)top=Math.max(top,r.bottom-box.top);else bottom=Math.max(bottom,box.bottom-r.top);
|
|
494
|
+
}
|
|
495
|
+
top=T.MathUtils.clamp(top,0,h);bottom=T.MathUtils.clamp(bottom,0,h);
|
|
496
|
+
this.vibeFrames={stop:vibeFrame(w/h,(top+TAG)/h,bottom/h),flight:vibeFrame(w/h,top/h,bottom/h)};
|
|
497
|
+
if(!this.vibing())return;
|
|
498
|
+
const {stop}=this.vibeFrames,{head}=VIBE.roam;
|
|
499
|
+
for(const unit of this.units.values())if(unit.moving&&unit.target&&!(inVibeFrame(stop,unit.target.x,0,unit.target.z)&&inVibeFrame(stop,unit.target.x,head,unit.target.z)))this.roam(unit);
|
|
500
|
+
}
|
|
487
501
|
overview() {
|
|
488
502
|
const {target,zoom}=overviewFor((this.camera.right-this.camera.left)/(this.camera.top-this.camera.bottom));
|
|
489
503
|
this.atOverview=true;
|
|
@@ -922,6 +936,7 @@ export class YardRenderer {
|
|
|
922
936
|
goTo(unit,place) {
|
|
923
937
|
const target=unit.target;
|
|
924
938
|
unit.mode=place.mode;
|
|
939
|
+
if(this.vibing()){if(!unit.moving)this.roam(unit);return;}
|
|
925
940
|
if(target&&target.x===place.x&&target.z===place.z){unit.target=place;if(!unit.moving)this.play(unit);return;}
|
|
926
941
|
unit.target=place;
|
|
927
942
|
const at={x:unit.root.position.x,z:unit.root.position.z};
|
|
@@ -929,12 +944,33 @@ export class YardRenderer {
|
|
|
929
944
|
else {unit.path=route(at,place,this.providers,this.propSpots);unit.moving=true;}
|
|
930
945
|
this.play(unit);
|
|
931
946
|
}
|
|
947
|
+
roam(unit) {
|
|
948
|
+
unit.roamed=(unit.roamed??-1)+1;
|
|
949
|
+
const at={x:unit.root.position.x,z:unit.root.position.z},stop=vibeStop(unit.session.id,unit.roamed,this.providers,this.propSpots,this.vibeFrames.stop);
|
|
950
|
+
unit.target=stop;unit.path=route(at,stop,this.providers,this.propSpots);unit.moving=true;
|
|
951
|
+
this.play(unit);
|
|
952
|
+
}
|
|
953
|
+
vibing() { return this.vibe&&!this.reduced; }
|
|
954
|
+
setVibe(value) {
|
|
955
|
+
if(this.vibe===Boolean(value))return;
|
|
956
|
+
const was=this.vibing();
|
|
957
|
+
this.vibe=Boolean(value);
|
|
958
|
+
if(was!==this.vibing())this.restage();
|
|
959
|
+
}
|
|
960
|
+
restage() {
|
|
961
|
+
if(!this.staged)return;
|
|
962
|
+
for(const [id,unit]of this.units){
|
|
963
|
+
const place=this.staged.places.get(id);
|
|
964
|
+
if(place){unit.target=null;this.goTo(unit,place);}
|
|
965
|
+
}
|
|
966
|
+
this.dirty=true;this.drawOnce();
|
|
967
|
+
}
|
|
932
968
|
// Straight to its place, helpers with it.
|
|
933
969
|
arrive(unit) {
|
|
934
970
|
const place=unit.target;
|
|
935
971
|
unit.root.position.set(place.x,.17,place.z);unit.path=[];unit.heading=place.face;unit.moving=false;
|
|
936
972
|
if(unit.model)unit.model.rotation.y=unit.heading;
|
|
937
|
-
for(const helper of unit.helpers)helper.model.position.set(place.x+helper.offset.x,.27,place.z+helper.offset.z);
|
|
973
|
+
for(const helper of unit.helpers){helper.model.position.set(place.x+helper.offset.x,.27,place.z+helper.offset.z);helper.model.scale.setScalar(.45);}
|
|
938
974
|
}
|
|
939
975
|
// The clip a character should be playing now: the walk while it moves,
|
|
940
976
|
// otherwise its mode's seeded sequence (model.mjs clipFor).
|
|
@@ -966,7 +1002,7 @@ export class YardRenderer {
|
|
|
966
1002
|
// Moves a walking character along its path at its stride's speed, turning
|
|
967
1003
|
// smoothly, and turns a standing one to face its place.
|
|
968
1004
|
advance(unit,dt) {
|
|
969
|
-
const root=unit.root.position;
|
|
1005
|
+
const root=unit.root.position,vibing=this.vibing();
|
|
970
1006
|
let want=unit.target?.face??unit.heading;
|
|
971
1007
|
if(unit.moving&&unit.path.length) {
|
|
972
1008
|
let step=unit.speed*dt;
|
|
@@ -976,7 +1012,7 @@ export class YardRenderer {
|
|
|
976
1012
|
if(d<=step){root.x=next.x;root.z=next.z;unit.path.shift();step-=d;}
|
|
977
1013
|
else {root.x+=dx/d*step;root.z+=dz/d*step;step=0;}
|
|
978
1014
|
}
|
|
979
|
-
if(!unit.path.length){unit.moving=false;this.play(unit);}
|
|
1015
|
+
if(!unit.path.length){if(vibing)this.roam(unit);else {unit.moving=false;this.play(unit);}}
|
|
980
1016
|
this.dirty=true;
|
|
981
1017
|
}
|
|
982
1018
|
const turn=angleTo(unit.heading,want);
|
|
@@ -988,13 +1024,15 @@ export class YardRenderer {
|
|
|
988
1024
|
// Helpers trail their character, closing most of the gap in a second.
|
|
989
1025
|
const pull=1-Math.exp(-4*dt);
|
|
990
1026
|
for(const helper of unit.helpers){
|
|
991
|
-
const
|
|
992
|
-
const dx=
|
|
993
|
-
|
|
1027
|
+
const to=vibing?vibeFlight(unit.session.id+':'+helper.id,this.vibeTime,this.vibeFrames.flight,helper.height):{x:root.x+helper.offset.x,y:.27,z:root.z+helper.offset.z};
|
|
1028
|
+
const p=helper.model.position,dx=to.x-p.x,dy=to.y-p.y,dz=to.z-p.z,far=Math.hypot(dx,dz);
|
|
1029
|
+
const size=vibing?VIBE.flight.scale:.45,grow=size-helper.model.scale.x;
|
|
1030
|
+
p.x+=dx*pull;p.y+=dy*pull;p.z+=dz*pull;
|
|
1031
|
+
if(grow)helper.model.scale.setScalar(Math.abs(grow)<1e-3?size:helper.model.scale.x+grow*pull);
|
|
994
1032
|
if(far>.05)helper.model.rotation.y=Math.atan2(dx,dz);
|
|
995
1033
|
const moving=far>.08;
|
|
996
1034
|
if(moving!==helper.moving){helper.moving=moving;this.helperClip(helper,moving?'move':helper.pose);}
|
|
997
|
-
if(far>1e-3)this.dirty=true;
|
|
1035
|
+
if(far>1e-3||Math.abs(dy)>1e-3||grow)this.dirty=true;
|
|
998
1036
|
}
|
|
999
1037
|
this.fire(unit);
|
|
1000
1038
|
}
|
|
@@ -1134,13 +1172,20 @@ export class YardRenderer {
|
|
|
1134
1172
|
spark.points.geometry.attributes.position.needsUpdate=true;
|
|
1135
1173
|
}
|
|
1136
1174
|
}
|
|
1175
|
+
restHelper(unit,helper) {
|
|
1176
|
+
helper.model.position.set(unit.root.position.x+helper.offset.x,.27,unit.root.position.z+helper.offset.z);
|
|
1177
|
+
helper.model.scale.setScalar(.45);
|
|
1178
|
+
helper.moving=false;helper.mixer.stopAllAction();helper.action=null;
|
|
1179
|
+
this.helperClip(helper,helper.pose);
|
|
1180
|
+
helper.mixer.update(0);
|
|
1181
|
+
}
|
|
1137
1182
|
helperClip(helper,name) {
|
|
1138
1183
|
const clip=helper.clips.find(c=>c.name===name)||helper.clips.find(c=>c.name===LEGACY[name])||helper.clips[0];
|
|
1139
1184
|
if(!clip)return;
|
|
1140
1185
|
const action=helper.mixer.clipAction(clip);
|
|
1141
1186
|
if(action===helper.action)return;
|
|
1142
1187
|
action.reset().play();
|
|
1143
|
-
if(helper.action)
|
|
1188
|
+
if(helper.action)helper.action.crossFadeTo(action,FADE,false);
|
|
1144
1189
|
helper.action=action;
|
|
1145
1190
|
}
|
|
1146
1191
|
clearHelpers(unit) {
|
|
@@ -1154,9 +1199,15 @@ export class YardRenderer {
|
|
|
1154
1199
|
const kinds=WORLDS[this.skin].helpers,source=this.pieceSource('companion');
|
|
1155
1200
|
const signature=JSON.stringify([source?.revision??null,agents]);
|
|
1156
1201
|
if(unit.helperSignature===signature)return;
|
|
1157
|
-
|
|
1202
|
+
const shown=agents.slice(0,SHOWN_HELPERS);
|
|
1203
|
+
unit.helpers=unit.helpers.filter(helper=>{
|
|
1204
|
+
if(shown.some(agent=>agent.id===helper.id))return true;
|
|
1205
|
+
unit.crew.remove(helper.model);helper.mixer.stopAllAction();helper.mixer.uncacheRoot(helper.model);disposeSkeletons(helper.model);
|
|
1206
|
+
return false;
|
|
1207
|
+
});
|
|
1158
1208
|
unit.helperSignature=signature;
|
|
1159
|
-
const rev
|
|
1209
|
+
const rev=++unit.revision,generation=this.request;
|
|
1210
|
+
this.dirty=true;this.drawOnce();
|
|
1160
1211
|
const stale=()=>this.disposed||generation!==this.request||rev!==unit.revision||!unit.root.parent;
|
|
1161
1212
|
let pieces=null;
|
|
1162
1213
|
if(source&&agents.length){
|
|
@@ -1171,26 +1222,31 @@ export class YardRenderer {
|
|
|
1171
1222
|
}
|
|
1172
1223
|
// The full helper count and all helper details remain in the canonical inspector.
|
|
1173
1224
|
// Small squads preserve readable character silhouettes at the overview scale.
|
|
1174
|
-
await Promise.all(
|
|
1225
|
+
await Promise.all(shown.map(async(agent,i)=>{
|
|
1226
|
+
const offset=helperOffset(i,agents.length),existing=unit.helpers.find(helper=>helper.id===agent.id);
|
|
1227
|
+
if(existing){
|
|
1228
|
+
existing.offset=offset;existing.pose=agent.pose;
|
|
1229
|
+
if(this.reduced)this.restHelper(unit,existing);
|
|
1230
|
+
else if(!existing.moving&&!this.vibing())this.helperClip(existing,agent.pose);
|
|
1231
|
+
this.dirty=true;this.drawOnce();
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1175
1234
|
try {
|
|
1176
1235
|
const n=hash(agent.name||agent.id)%4;
|
|
1177
1236
|
const asset=pieces?pieces[n]:await this.asset((agent.shell?kinds.shell:kinds.agent)+n);
|
|
1178
1237
|
if(stale())return;
|
|
1179
1238
|
const model=T.cloneSkeleton(asset.scene);
|
|
1180
1239
|
model.traverse(n=>{if(n.isSkinnedMesh)n.frustumCulled=false;});
|
|
1181
|
-
const offset=helperOffset(i,agents.length);
|
|
1182
1240
|
model.scale.setScalar(.45);model.position.set(unit.root.position.x+offset.x,.27,unit.root.position.z+offset.z);
|
|
1183
1241
|
unit.crew.add(model);
|
|
1184
1242
|
const mixer=new T.AnimationMixer(model);
|
|
1185
1243
|
let yard=null;asset.scene.traverse(n=>{if(n.userData?.yard)yard=n.userData.yard;});
|
|
1186
|
-
const helper={id:agent.id,
|
|
1187
|
-
this.
|
|
1188
|
-
mixer.update(.3+(hash(agent.id)%40)/10);
|
|
1244
|
+
const helper={id:agent.id,height:yardExtras(yard,'helper').height,get top(){return this.height*this.model.scale.y;},model,mixer,offset,pose:agent.pose,clips:asset.animations,moving:false,action:null};
|
|
1245
|
+
if(this.reduced)this.restHelper(unit,helper);
|
|
1246
|
+
else {this.helperClip(helper,agent.pose);mixer.update(.3+(hash(agent.id)%40)/10);}
|
|
1189
1247
|
unit.helpers.push(helper);
|
|
1190
1248
|
this.dirty=true;this.drawOnce();
|
|
1191
1249
|
} catch {
|
|
1192
|
-
// The next state transition can rebuild this incomplete squad. Routine
|
|
1193
|
-
// unchanged updates do not call helpers(), so failure cannot loop.
|
|
1194
1250
|
if(!stale())unit.helperSignature=null;
|
|
1195
1251
|
}
|
|
1196
1252
|
}));
|
|
@@ -1343,9 +1399,12 @@ export class YardRenderer {
|
|
|
1343
1399
|
this.list.style.top=Math.max(6,ct+half+4+lh<=h-6?ct+half+4:ct-half-4-lh)+'px';
|
|
1344
1400
|
}
|
|
1345
1401
|
setReducedMotion(value) {
|
|
1402
|
+
const was=this.vibing();
|
|
1346
1403
|
this.reduced=value;
|
|
1404
|
+
if(was!==this.vibing())this.restage();
|
|
1347
1405
|
if(value)for(const unit of this.units.values()){this.unlook(unit);unit.lookYaw=0;unit.lookPitch=0;}
|
|
1348
1406
|
if(value)for(const unit of this.units.values())if(unit.moving&&unit.target){this.arrive(unit);this.play(unit);}
|
|
1407
|
+
if(value)for(const unit of this.units.values())for(const helper of unit.helpers)this.restHelper(unit,helper);
|
|
1349
1408
|
if(value){for(const item of this.halls.values())item.level=item.target;this.glow();this.stillEffects();this.clearFireworks();}
|
|
1350
1409
|
this.dirty=true;this.drawOnce();
|
|
1351
1410
|
}
|
|
@@ -1372,12 +1431,14 @@ export class YardRenderer {
|
|
|
1372
1431
|
// Advances walks, turns, helpers, clips, glances, sparks and the halls'
|
|
1373
1432
|
// glow and effects by dt seconds, without drawing.
|
|
1374
1433
|
step(dt) {
|
|
1434
|
+
const vibing=this.vibing();
|
|
1435
|
+
if(vibing)this.vibeTime+=dt;
|
|
1375
1436
|
for(const unit of this.units.values()) {
|
|
1376
1437
|
this.advance(unit,dt);
|
|
1377
1438
|
// Characters out of view animate five times a second, enough to stay
|
|
1378
1439
|
// in step for when the camera comes back to them.
|
|
1379
1440
|
let step=dt;
|
|
1380
|
-
if(unit.offscreen){unit.lag=(unit.lag||0)+dt;if(unit.lag<.2)continue;step=unit.lag;}
|
|
1441
|
+
if(unit.offscreen&&!vibing){unit.lag=(unit.lag||0)+dt;if(unit.lag<.2)continue;step=unit.lag;}
|
|
1381
1442
|
unit.lag=0;
|
|
1382
1443
|
this.unlook(unit);
|
|
1383
1444
|
for(const mixer of unit.mixers)mixer.update(step);
|
package/web/yard/view.js
CHANGED
|
@@ -169,6 +169,20 @@ export function initYard(controller) {
|
|
|
169
169
|
button.disabled = !renderer || disposed || motion.matches;
|
|
170
170
|
button.title = motion.matches ? 'Fireworks off: reduced motion is enabled' : 'Fireworks in this Yard only';
|
|
171
171
|
};
|
|
172
|
+
let vibe = false;
|
|
173
|
+
try { vibe = localStorage.getItem('agentGuild.vibe') === 'on'; } catch {}
|
|
174
|
+
const vibeButton = () => {
|
|
175
|
+
const button = $('yard-vibe');
|
|
176
|
+
button.setAttribute('aria-pressed', String(vibe));
|
|
177
|
+
button.disabled = motion.matches;
|
|
178
|
+
button.title = motion.matches ? 'Vibes Mode off: reduced motion is enabled' : vibe ? 'Vibes Mode is on in every Yard' : 'Vibes Mode: everyone roams, in every Yard';
|
|
179
|
+
};
|
|
180
|
+
const toggleVibe = () => {
|
|
181
|
+
vibe = !vibe;
|
|
182
|
+
try { localStorage.setItem('agentGuild.vibe', vibe ? 'on' : 'off'); } catch {}
|
|
183
|
+
renderer?.setVibe(vibe);
|
|
184
|
+
vibeButton();
|
|
185
|
+
};
|
|
172
186
|
const sceneVisible = () => view === 'yard' && !$('app').hidden && document.visibilityState !== 'hidden' && $('terminal-panel').hidden;
|
|
173
187
|
const skinEntry = (skin = root.dataset.skin) => (window.agentGuildSkins || []).find((item) => item.id === skin) || null;
|
|
174
188
|
const yardOff = root.dataset.yard === 'none';
|
|
@@ -281,6 +295,7 @@ export function initYard(controller) {
|
|
|
281
295
|
open: id => controller.openSession(id),
|
|
282
296
|
error: () => { if (attempt === sceneAttempt) failScene(); },
|
|
283
297
|
piece: pieceDrawn,
|
|
298
|
+
cover: [...document.querySelectorAll('.yard-heading > *, #yard-stage .yard-overlay')],
|
|
284
299
|
});
|
|
285
300
|
pendingRenderer = candidate;
|
|
286
301
|
// Appearance can change while the first world is loading.
|
|
@@ -299,6 +314,7 @@ export function initYard(controller) {
|
|
|
299
314
|
renderer = candidate;
|
|
300
315
|
lastSkin = skin; lastTheme = theme; lastRevision = revision;
|
|
301
316
|
renderer.setReducedMotion(motion.matches);
|
|
317
|
+
renderer.setVibe(vibe);
|
|
302
318
|
renderer.update(snapshot.providers, snapshot.sessions);
|
|
303
319
|
renderer.select(selected);
|
|
304
320
|
renderer.setActive(sceneVisible());
|
|
@@ -465,6 +481,8 @@ export function initYard(controller) {
|
|
|
465
481
|
$('yard-zoom-out').addEventListener('click',() => renderer?.zoom(1/1.2));
|
|
466
482
|
$('yard-fireworks').addEventListener('click',celebrate);
|
|
467
483
|
fireworksButton();
|
|
484
|
+
$('yard-vibe').addEventListener('click',toggleVibe);
|
|
485
|
+
vibeButton();
|
|
468
486
|
$('yard-photo').hidden = !controller.photo;
|
|
469
487
|
$('yard-photo').addEventListener('click',async () => {
|
|
470
488
|
let YardPhoto;
|
|
@@ -490,7 +508,7 @@ export function initYard(controller) {
|
|
|
490
508
|
setView(preferYard && skinAllowsYard() ? 'yard' : 'cards', { remember: false });
|
|
491
509
|
renderer?.setActive(sceneVisible());
|
|
492
510
|
};
|
|
493
|
-
const motionChange = () => { renderer?.setReducedMotion(motion.matches); fireworksButton(); };
|
|
511
|
+
const motionChange = () => { renderer?.setReducedMotion(motion.matches); fireworksButton(); vibeButton(); };
|
|
494
512
|
const pageHide = () => renderer?.setActive(false);
|
|
495
513
|
const observer = new MutationObserver((records) => {
|
|
496
514
|
if (records.some((record) => record.attributeName === 'data-skin' || record.attributeName === 'data-theme')) onAppearance();
|
|
@@ -507,6 +525,7 @@ export function initYard(controller) {
|
|
|
507
525
|
return { update, select, setView, closePhoto, celebrate, dispose() {
|
|
508
526
|
closePhoto();
|
|
509
527
|
$('yard-fireworks').removeEventListener('click',celebrate);
|
|
528
|
+
$('yard-vibe').removeEventListener('click',toggleVibe);
|
|
510
529
|
disposed=true; fireworksButton(); sceneAttempt++; worldAttempt++; clearTimeout(swapReveal); observer.disconnect(); pendingRenderer?.dispose(); renderer?.dispose();
|
|
511
530
|
document.removeEventListener('visibilitychange',visibility);
|
|
512
531
|
motion.removeEventListener('change',motionChange);
|
package/web/yard/yard.css
CHANGED
|
@@ -265,3 +265,4 @@
|
|
|
265
265
|
@container (max-width:440px) { .yard-play-text { display:none; } .yard-play-link { padding:0 7px; } }
|
|
266
266
|
@container (max-width:380px) { .yard-play:not(.yard-play-solo) .yard-play-link { display:none; } .yard-play-chip { --play-more-w:44px; } .yard-play-more { padding:0; } .yard-play-more::before { display:none; } .yard-play-more-orb { display:block; grid-area:1 / 1; } .yard-play-caret { grid-area:1 / 1; place-self:end; width:9px; height:6px; margin:0 7px 6px 0; stroke-width:2.2; } }
|
|
267
267
|
@container (max-width:340px) { :root:is([data-skin="orbital"],[data-skin="gnomeland"],[data-skin="goblinville"],[data-skin="grove"],[data-skin="guild"],[data-skin="professional"]) :is(.yard-eyebrow, #yard-subtitle) { display:none; } }
|
|
268
|
+
#yard-vibe[aria-pressed="true"] { border-color:var(--accent); color:var(--accent); }
|