@oddessentials/agent-guild 1.31.0 → 1.33.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/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/session-hooks.mjs +2 -1
- package/src/manager/shell-env.mjs +4 -2
- package/src/mcp/server.mjs +1 -1
- package/web/index.html +1 -1
- package/web/yard/art.json +1 -1
- package/web/yard/aura.js +165 -0
- package/web/yard/model.mjs +140 -0
- package/web/yard/renderer.js +119 -31
- package/web/yard/view.js +20 -1
- package/web/yard/yard.css +1 -0
package/web/yard/model.mjs
CHANGED
|
@@ -121,6 +121,13 @@ export function marksFor(s, mode) {
|
|
|
121
121
|
const crew = waiting.length > shown.length ? 'waiting' : mode === 'rest' && !agents.length && !s.shells?.length ? 'asleep' : null;
|
|
122
122
|
return { crew, helpers: shown.map(a => a.id) };
|
|
123
123
|
}
|
|
124
|
+
export const onFire = s => s.status !== 'exited' && (s.agents?.length || 0) + (s.shells?.length || 0) > SHOWN_HELPERS;
|
|
125
|
+
export function fireTint(r, g, b) {
|
|
126
|
+
const max = Math.max(r, g, b), spread = max - Math.min(r, g, b);
|
|
127
|
+
const hue = !spread ? 0 : max === r ? ((g - b) / spread + 6) % 6 : max === g ? (b - r) / spread + 2 : (r - g) / spread + 4;
|
|
128
|
+
const s = Math.min((max ? spread / max : 0) * 1.25, 1), v = Math.max(max, .9);
|
|
129
|
+
return [5, 3, 1].map(n => { const k = (n + hue) % 6; return v - v * s * Math.max(0, Math.min(k, 4 - k, 1)); });
|
|
130
|
+
}
|
|
124
131
|
export function hash(value) {
|
|
125
132
|
let n = 0;
|
|
126
133
|
for (const char of String(value)) n = ((n << 5) - n + char.charCodeAt(0)) | 0;
|
|
@@ -434,6 +441,139 @@ export function clipFor(id, mode, n) {
|
|
|
434
441
|
if (mode === 'work') return n % 3 === 2 && h % 2 ? set[1] : set[0];
|
|
435
442
|
return n % 3 === 2 ? set[1 + (h % (set.length - 1))] : set[0];
|
|
436
443
|
}
|
|
444
|
+
export const VIBE = {
|
|
445
|
+
// Must stay inside the ground every world keeps clear (Clearance.live_distance in agent-guild-builders).
|
|
446
|
+
roam: { radius: 13.2, half: 12.5, front: 30.5, split: 3, margin: .9, edge: 1.2, head: 2.4 },
|
|
447
|
+
flight: { height: [8.5, 12], lap: [26, 44], reach: [.45, 1], scale: 1.3, top: 2 },
|
|
448
|
+
};
|
|
449
|
+
export function vibeFrame(aspect, top = 0, bottom = 0) {
|
|
450
|
+
const { target, zoom } = overviewFor(aspect), height = CAMERA.height / zoom, width = height * aspect, { edge } = VIBE.roam;
|
|
451
|
+
return { target, right: [edge - width / 2, width / 2 - edge], up: [(bottom - .5) * height + edge, (.5 - top) * height - edge] };
|
|
452
|
+
}
|
|
453
|
+
export function inVibeFrame(frame, x, y, z) {
|
|
454
|
+
const { right, up } = viewBasis(), d = [x - frame.target[0], y - frame.target[1], z - frame.target[2]];
|
|
455
|
+
const r = dot(d, right), u = dot(d, up);
|
|
456
|
+
return r >= frame.right[0] && r <= frame.right[1] && u >= frame.up[0] && u <= frame.up[1];
|
|
457
|
+
}
|
|
458
|
+
function unit(value) {
|
|
459
|
+
let h = hash(value);
|
|
460
|
+
h = Math.imul(h ^ (h >>> 16), 0x45d9f3b); h = Math.imul(h ^ (h >>> 16), 0x45d9f3b);
|
|
461
|
+
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
462
|
+
}
|
|
463
|
+
// Clip the visible ground polygon to an axis-aligned interval. Destination
|
|
464
|
+
// geometry only: this neither builds nor checks a route.
|
|
465
|
+
function vibeClip(points, axis, low, high) {
|
|
466
|
+
for (const [edge, sign] of [[low, 1], [high, -1]]) {
|
|
467
|
+
const next = [];
|
|
468
|
+
for (let i = 0; i < points.length; i++) {
|
|
469
|
+
const a = points[i], b = points[(i + 1) % points.length], da = sign * (a[axis] - edge), db = sign * (b[axis] - edge);
|
|
470
|
+
if (da >= 0) next.push(a);
|
|
471
|
+
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) }); }
|
|
472
|
+
}
|
|
473
|
+
points = next;
|
|
474
|
+
}
|
|
475
|
+
return points;
|
|
476
|
+
}
|
|
477
|
+
// A missed sample is not an empty view. Obstacle x edges divide the ground
|
|
478
|
+
// into at most 2B+1 bands, each with at most B+1 free z intervals. Check those
|
|
479
|
+
// finite regions, not a grid whose spacing could miss a narrow opening.
|
|
480
|
+
function vibeGap(polygon, boxes, near, far, seed) {
|
|
481
|
+
const { radius, half, front } = VIBE.roam;
|
|
482
|
+
polygon = vibeClip(vibeClip(polygon, 'x', -radius, radius), 'z', near, far);
|
|
483
|
+
const point = points => {
|
|
484
|
+
if (points.length < 3) return null;
|
|
485
|
+
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);
|
|
486
|
+
if (Math.abs(area) < 1e-10) return null;
|
|
487
|
+
const center = { x: 0, z: 0 };
|
|
488
|
+
for (const p of points) { center.x += p.x / points.length; center.z += p.z / points.length; }
|
|
489
|
+
const p = points[Math.floor(seed * points.length)], t = .25 + seed / 2;
|
|
490
|
+
return { x: center.x + (p.x - center.x) * t, z: center.z + (p.z - center.z) * t };
|
|
491
|
+
};
|
|
492
|
+
const ground = points => {
|
|
493
|
+
const p = point(points);
|
|
494
|
+
if (!p) return null;
|
|
495
|
+
if (Math.hypot(p.x, p.z) < radius || (Math.abs(p.x) <= half && p.z >= 0 && p.z <= front)) return p;
|
|
496
|
+
const apron = point(vibeClip(vibeClip(points, 'x', -half, half), 'z', 0, front));
|
|
497
|
+
if (apron) return apron;
|
|
498
|
+
// The plaza's obstacle excludes the circle's centre from every free
|
|
499
|
+
// region, so its nearest point is on an edge. Move from it into the
|
|
500
|
+
// polygon, staying inside the circle even for a small circular sliver.
|
|
501
|
+
let nearest = null, distance = Infinity;
|
|
502
|
+
for (let i = 0; i < points.length; i++) {
|
|
503
|
+
const a = points[i], b = points[(i + 1) % points.length], dx = b.x - a.x, dz = b.z - a.z;
|
|
504
|
+
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);
|
|
505
|
+
if (d < distance) { nearest = q; distance = d; }
|
|
506
|
+
}
|
|
507
|
+
if (distance >= radius) return null;
|
|
508
|
+
const t = Math.min(.5, (radius - distance) / (2 * Math.hypot(p.x - nearest.x, p.z - nearest.z)));
|
|
509
|
+
return { x: nearest.x + t * (p.x - nearest.x), z: nearest.z + t * (p.z - nearest.z) };
|
|
510
|
+
};
|
|
511
|
+
if (!point(polygon)) return null;
|
|
512
|
+
const left = Math.min(...polygon.map(p => p.x)), right = Math.max(...polygon.map(p => p.x));
|
|
513
|
+
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);
|
|
514
|
+
const start = Math.floor(seed * (edges.length - 1));
|
|
515
|
+
for (let i = 0; i < edges.length - 1; i++) {
|
|
516
|
+
const k = (start + i) % (edges.length - 1), lo = edges[k], hi = edges[k + 1], x = (lo + hi) / 2;
|
|
517
|
+
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]);
|
|
518
|
+
let z = near;
|
|
519
|
+
for (const [low, high] of [...blocked, [far, far]]) {
|
|
520
|
+
if (low > z) {
|
|
521
|
+
const p = ground(vibeClip(vibeClip(polygon, 'x', lo, hi), 'z', z, Math.min(low, far)));
|
|
522
|
+
if (p) return p;
|
|
523
|
+
}
|
|
524
|
+
z = Math.max(z, high);
|
|
525
|
+
if (z >= far) break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
export function vibeStop(id, n, providers, props = [], frame = null) {
|
|
531
|
+
const { radius, half, front, split, margin, head } = VIBE.roam;
|
|
532
|
+
const [near, far] = (n + hash(id)) % 2 ? [split, front] : [-radius, split];
|
|
533
|
+
const boxes = [...obstacles(providers).map(box => ({ ...box, half: box.half + margin })),
|
|
534
|
+
...props.map(p => ({ x: p.x, z: p.z, half: STAGING.station.half + margin }))];
|
|
535
|
+
const clear = (x, z) => (Math.hypot(x - STAGING.plaza.x, z - STAGING.plaza.z) <= radius || (Math.abs(x) <= half && z >= 0 && z <= front))
|
|
536
|
+
&& !boxes.some(box => Math.abs(x - box.x) < box.half && Math.abs(z - box.z) < box.half);
|
|
537
|
+
if (frame) {
|
|
538
|
+
const { right, up, forward } = viewBasis(), [r0, r1] = frame.right, u0 = frame.up[0], u1 = frame.up[1] - head * up[1];
|
|
539
|
+
const at = (r, u) => {
|
|
540
|
+
const s = (-frame.target[1] - u * up[1]) / forward[1];
|
|
541
|
+
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] };
|
|
542
|
+
};
|
|
543
|
+
if (r1 > r0 && u1 > u0) {
|
|
544
|
+
const polygon = [at(r0, u0), at(r1, u0), at(r1, u1), at(r0, u1)];
|
|
545
|
+
for (const [low, high] of [[near, far], near === split ? [-radius, split] : [split, front]]) {
|
|
546
|
+
for (let k = 0; k < 48; k++) {
|
|
547
|
+
const p = at(r0 + unit(id + ':vibe:x:' + n + ':' + k) * (r1 - r0), u0 + unit(id + ':vibe:z:' + n + ':' + k) * (u1 - u0));
|
|
548
|
+
const x = round(p.x), z = round(p.z);
|
|
549
|
+
if (z >= low && z <= high && clear(x, z) && inVibeFrame(frame, x, 0, z) && inVibeFrame(frame, x, head, z)) return { x, z };
|
|
550
|
+
}
|
|
551
|
+
const p = vibeGap(polygon, boxes, low, high, unit(id + ':vibe:gap:' + n));
|
|
552
|
+
if (p) return p;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// No usable visible ground (or no frame): keep the original seeded walk.
|
|
557
|
+
// Never turn a crowded/covered view into a stationary, repeatedly retried unit.
|
|
558
|
+
for (let k = 0; k < 48; k++) {
|
|
559
|
+
const x = round((unit(id + ':vibe:x:' + n + ':' + k) * 2 - 1) * radius), z = round(near + unit(id + ':vibe:z:' + n + ':' + k) * (far - near));
|
|
560
|
+
if (clear(x, z)) return { x, z };
|
|
561
|
+
}
|
|
562
|
+
return { x: STAGING.plaza.x, z: round(STAGING.plaza.z + STAGING.plaza.half + margin + STAGING.station.half) };
|
|
563
|
+
}
|
|
564
|
+
export function vibeFlight(key, seconds, frame, companionHeight = 1) {
|
|
565
|
+
const { height, lap, reach, top, scale } = VIBE.flight, { right, up, forward } = viewBasis();
|
|
566
|
+
const pick = (name, [low, high]) => low + unit(key + ':vibe:' + name) * (high - low);
|
|
567
|
+
const out = pick('reach', reach), turn = unit(key + ':vibe:turn') < .5 ? 1 : -1, lobes = 2 + Math.floor(unit(key + ':vibe:lobes') * 3);
|
|
568
|
+
const angle = turn * 2 * Math.PI * (seconds / pick('lap', lap) + unit(key + ':vibe:start'));
|
|
569
|
+
const wide = out * (1 + .12 * Math.sin(lobes * angle + pick('phase', [0, 2 * Math.PI]))) / 1.12;
|
|
570
|
+
const y = (height[0] + height[1]) / 2 + (height[1] - height[0]) / 2 * Math.sin(2 * angle + pick('rise', [0, 2 * Math.PI]));
|
|
571
|
+
const [r0, r1] = frame.right, u0 = frame.up[0], u1 = frame.up[1] - Math.max(top, companionHeight * scale * up[1]);
|
|
572
|
+
const r = (r0 + r1) / 2 + Math.cos(angle) * Math.max(0, r1 - r0) / 2 * wide;
|
|
573
|
+
const u = (u0 + u1) / 2 + Math.sin(angle) * Math.max(0, u1 - u0) / 2 * wide;
|
|
574
|
+
const s = (y - frame.target[1] - u * up[1]) / forward[1];
|
|
575
|
+
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] };
|
|
576
|
+
}
|
|
437
577
|
// Where a session's helpers stand, around it, as the renderer has always placed them.
|
|
438
578
|
export function helperOffset(i, count) {
|
|
439
579
|
const angle = (i / Math.min(count, 6)) * Math.PI * 2;
|
package/web/yard/renderer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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, onFire, VIBE, vibeStop, vibeFlight, vibeFrame, inVibeFrame } from './model.mjs';
|
|
4
|
+
import { Aura, auraShapes, EMBER_SIZE } from './aura.js';
|
|
4
5
|
|
|
5
6
|
const ASSETS = new URL('./assets/', import.meta.url);
|
|
6
7
|
const vector = new T.Vector3();
|
|
@@ -16,7 +17,7 @@ const pieceFiles = (skin, slot, piece) => (slot === 'companion' ? [0, 1, 2, 3].m
|
|
|
16
17
|
const vector2 = new T.Vector2();
|
|
17
18
|
// How fast characters turn to a new heading, in radians a second, and how
|
|
18
19
|
// long a clip change blends.
|
|
19
|
-
const TURN = 7, FADE = .3;
|
|
20
|
+
const TURN = 7, FADE = .3, TAG = 44;
|
|
20
21
|
// Models rigged before the staging contract name their clips differently.
|
|
21
22
|
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
23
|
// How bright a hall's glow grows at low and high activity, over what the
|
|
@@ -102,6 +103,21 @@ function checkPiece({scene,animations}) {
|
|
|
102
103
|
});
|
|
103
104
|
} finally {disposeSkeletons(probe);}
|
|
104
105
|
}
|
|
106
|
+
function girth(scene) {
|
|
107
|
+
const probe=T.cloneSkeleton(scene);
|
|
108
|
+
try {
|
|
109
|
+
probe.updateMatrixWorld(true);
|
|
110
|
+
const inverse=new T.Matrix4().copy(probe.matrixWorld).invert(),local=new T.Matrix4();
|
|
111
|
+
let left=Infinity,right=-Infinity,back=Infinity,front=-Infinity;
|
|
112
|
+
probe.traverse(node=>{
|
|
113
|
+
if(!node.isSkinnedMesh)return;
|
|
114
|
+
node.skeleton.update();node.computeBoundingBox();
|
|
115
|
+
const {min,max}=node.boundingBox.applyMatrix4(local.multiplyMatrices(inverse,node.matrixWorld));
|
|
116
|
+
left=Math.min(left,min.x);right=Math.max(right,max.x);back=Math.min(back,min.z);front=Math.max(front,max.z);
|
|
117
|
+
});
|
|
118
|
+
return Math.max(right-left,front-back,0);
|
|
119
|
+
} finally {disposeSkeletons(probe);}
|
|
120
|
+
}
|
|
105
121
|
function disposeTree(root) {
|
|
106
122
|
disposeSkeletons(root);
|
|
107
123
|
const geometries=new Set(),materials=new Set();
|
|
@@ -139,15 +155,16 @@ function disposeWorld(root) {
|
|
|
139
155
|
}
|
|
140
156
|
export class YardRenderer {
|
|
141
157
|
// 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;
|
|
158
|
+
constructor(host,labels,{select,open,error=noop,piece=noop,clock=Date.now,cover=[]}) {
|
|
159
|
+
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
160
|
this.pieceSets={};this.shownPieces={skin:null,pieces:{}};this.pieceLoaders=new Map();this.preloads=new Map();
|
|
145
161
|
this.scene=new T.Scene();this.units=new Map();this.halls=new Map();this.slots=new Map();this.cache=new Map();
|
|
146
162
|
this.loadingManager=new T.LoadingManager();
|
|
147
163
|
this.providers=[];this.sessions=[];this.selected=null;this.world=null;this.skin=null;this.request=0;this.worldRequest=0;this.pending=null;
|
|
148
164
|
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);
|
|
165
|
+
this.pointScale={value:1};this.auraShapes=auraShapes();
|
|
149
166
|
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;
|
|
167
|
+
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
168
|
this.crowd=null;this.rows=new Map();
|
|
152
169
|
this.list=Object.assign(document.createElement('div'),{className:'yard-crowd-list',id:'yard-crowd-list-'+(++crowdLists),hidden:true});
|
|
153
170
|
this.list.setAttribute('role','group');
|
|
@@ -195,7 +212,7 @@ export class YardRenderer {
|
|
|
195
212
|
this.canvas.addEventListener('pointerdown',this.onPointerDown);
|
|
196
213
|
this.canvas.addEventListener('click',this.onClick);
|
|
197
214
|
this.canvas.addEventListener('dblclick',this.onDouble);
|
|
198
|
-
this.resizeObserver=new ResizeObserver(()=>this.resize());this.resizeObserver.observe(
|
|
215
|
+
this.resizeObserver=new ResizeObserver(()=>this.resize());for(const el of [host,...cover])this.resizeObserver.observe(el);
|
|
199
216
|
this.resize();this.overview();
|
|
200
217
|
}
|
|
201
218
|
async asset(name,url) {
|
|
@@ -278,6 +295,7 @@ export class YardRenderer {
|
|
|
278
295
|
}
|
|
279
296
|
dropModel(unit) {
|
|
280
297
|
for(const mixer of unit.mixers){mixer.stopAllAction();mixer.uncacheRoot(mixer.getRoot());}
|
|
298
|
+
unit.aura?.bind(null);
|
|
281
299
|
if(unit.model){unit.root.remove(unit.model);disposeSkeletons(unit.model);}
|
|
282
300
|
Object.assign(unit,{mixers:[],model:null,loaded:false,loading:false,animations:null,action:null,clip:null,lookBones:null,looking:false,load:(unit.load||0)+1});
|
|
283
301
|
}
|
|
@@ -474,7 +492,7 @@ export class YardRenderer {
|
|
|
474
492
|
const w=this.host.clientWidth,h=this.host.clientHeight;
|
|
475
493
|
if(!w||!h)return;
|
|
476
494
|
this.renderer.setSize(w,h,false);
|
|
477
|
-
|
|
495
|
+
this.pointScale.value=this.renderer.getDrawingBufferSize(vector2).y/2;
|
|
478
496
|
const half=CAMERA.height/2;
|
|
479
497
|
this.camera.left=-half*w/h;this.camera.right=half*w/h;
|
|
480
498
|
this.camera.top=half;this.camera.bottom=-half;
|
|
@@ -482,8 +500,22 @@ export class YardRenderer {
|
|
|
482
500
|
this.camera.zoom=T.MathUtils.clamp(this.camera.zoom,this.controls.minZoom,this.controls.maxZoom);
|
|
483
501
|
this.camera.updateProjectionMatrix();
|
|
484
502
|
this.dirty=true;
|
|
503
|
+
this.frameVibe(w,h);
|
|
485
504
|
if(this.atOverview)this.overview();else this.drawOnce();
|
|
486
505
|
}
|
|
506
|
+
frameVibe(w,h) {
|
|
507
|
+
const box=this.host.getBoundingClientRect();let top=0,bottom=0;
|
|
508
|
+
for(const el of this.cover){
|
|
509
|
+
const r=el.getBoundingClientRect();
|
|
510
|
+
if(!r.width||!r.height)continue;
|
|
511
|
+
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);
|
|
512
|
+
}
|
|
513
|
+
top=T.MathUtils.clamp(top,0,h);bottom=T.MathUtils.clamp(bottom,0,h);
|
|
514
|
+
this.vibeFrames={stop:vibeFrame(w/h,(top+TAG)/h,bottom/h),flight:vibeFrame(w/h,top/h,bottom/h)};
|
|
515
|
+
if(!this.vibing())return;
|
|
516
|
+
const {stop}=this.vibeFrames,{head}=VIBE.roam;
|
|
517
|
+
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);
|
|
518
|
+
}
|
|
487
519
|
overview() {
|
|
488
520
|
const {target,zoom}=overviewFor((this.camera.right-this.camera.left)/(this.camera.top-this.camera.bottom));
|
|
489
521
|
this.atOverview=true;
|
|
@@ -671,6 +703,9 @@ export class YardRenderer {
|
|
|
671
703
|
unit.label.style.setProperty('--unit-color',session.provider.color||'#d0ba82');
|
|
672
704
|
unit.label.setAttribute('aria-label',session.name+', '+unit.label.children[1].textContent);
|
|
673
705
|
unit.ring.material.opacity=place.mode==='work'?.65:place.mode==='done'?.15:.35;
|
|
706
|
+
const burning=onFire(session);
|
|
707
|
+
if(burning&&!unit.aura){unit.aura=new Aura(session.id,this.auraShapes,this.pointScale);unit.root.add(unit.aura.group);unit.aura.bind(unit.model,unit.girth,unit.height);}
|
|
708
|
+
unit.aura?.set(burning,session.provider.color||'#d0ba82',this.reduced);
|
|
674
709
|
}
|
|
675
710
|
for(const id of live)this.seen.add(id);
|
|
676
711
|
this.select(this.selected);this.dirty=true;this.drawOnce();
|
|
@@ -695,6 +730,7 @@ export class YardRenderer {
|
|
|
695
730
|
yard=yardExtras(yard,'character');
|
|
696
731
|
unit.speed=yard.stride/yard.cycle;
|
|
697
732
|
unit.events=yard.events;unit.height=yard.height;unit.surface=yard.surface;unit.look=lookLimits(yard.look);
|
|
733
|
+
unit.girth=asset.girth??=girth(asset.scene);unit.aura?.bind(model,unit.girth,unit.height);
|
|
698
734
|
const neck=model.getObjectByName('Neck'),head=model.getObjectByName('Head');
|
|
699
735
|
unit.lookBones=neck&&head&&model.getObjectByName('LeftUpperLeg')?{neck,head}:null;
|
|
700
736
|
this.scaleProp(unit.session?.id||session.id);
|
|
@@ -757,7 +793,7 @@ export class YardRenderer {
|
|
|
757
793
|
geometry.setAttribute('size',new T.BufferAttribute(new Float32Array(PARTICLES),1));
|
|
758
794
|
geometry.setAttribute('alpha',new T.BufferAttribute(new Float32Array(PARTICLES),1));
|
|
759
795
|
const material=new T.ShaderMaterial({
|
|
760
|
-
uniforms:{color:{value:new T.Color(preset.color)},scale:
|
|
796
|
+
uniforms:{color:{value:new T.Color(preset.color)},scale:this.pointScale},
|
|
761
797
|
vertexShader:`attribute float size;attribute float alpha;varying float vAlpha;uniform float scale;
|
|
762
798
|
void main(){vAlpha=alpha;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);gl_PointSize=size*scale*projectionMatrix[1][1];}`,
|
|
763
799
|
fragmentShader:`uniform vec3 color;varying float vAlpha;
|
|
@@ -922,6 +958,7 @@ export class YardRenderer {
|
|
|
922
958
|
goTo(unit,place) {
|
|
923
959
|
const target=unit.target;
|
|
924
960
|
unit.mode=place.mode;
|
|
961
|
+
if(this.vibing()){if(!unit.moving)this.roam(unit);return;}
|
|
925
962
|
if(target&&target.x===place.x&&target.z===place.z){unit.target=place;if(!unit.moving)this.play(unit);return;}
|
|
926
963
|
unit.target=place;
|
|
927
964
|
const at={x:unit.root.position.x,z:unit.root.position.z};
|
|
@@ -929,12 +966,33 @@ export class YardRenderer {
|
|
|
929
966
|
else {unit.path=route(at,place,this.providers,this.propSpots);unit.moving=true;}
|
|
930
967
|
this.play(unit);
|
|
931
968
|
}
|
|
969
|
+
roam(unit) {
|
|
970
|
+
unit.roamed=(unit.roamed??-1)+1;
|
|
971
|
+
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);
|
|
972
|
+
unit.target=stop;unit.path=route(at,stop,this.providers,this.propSpots);unit.moving=true;
|
|
973
|
+
this.play(unit);
|
|
974
|
+
}
|
|
975
|
+
vibing() { return this.vibe&&!this.reduced; }
|
|
976
|
+
setVibe(value) {
|
|
977
|
+
if(this.vibe===Boolean(value))return;
|
|
978
|
+
const was=this.vibing();
|
|
979
|
+
this.vibe=Boolean(value);
|
|
980
|
+
if(was!==this.vibing())this.restage();
|
|
981
|
+
}
|
|
982
|
+
restage() {
|
|
983
|
+
if(!this.staged)return;
|
|
984
|
+
for(const [id,unit]of this.units){
|
|
985
|
+
const place=this.staged.places.get(id);
|
|
986
|
+
if(place){unit.target=null;this.goTo(unit,place);}
|
|
987
|
+
}
|
|
988
|
+
this.dirty=true;this.drawOnce();
|
|
989
|
+
}
|
|
932
990
|
// Straight to its place, helpers with it.
|
|
933
991
|
arrive(unit) {
|
|
934
992
|
const place=unit.target;
|
|
935
993
|
unit.root.position.set(place.x,.17,place.z);unit.path=[];unit.heading=place.face;unit.moving=false;
|
|
936
994
|
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);
|
|
995
|
+
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
996
|
}
|
|
939
997
|
// The clip a character should be playing now: the walk while it moves,
|
|
940
998
|
// otherwise its mode's seeded sequence (model.mjs clipFor).
|
|
@@ -966,7 +1024,7 @@ export class YardRenderer {
|
|
|
966
1024
|
// Moves a walking character along its path at its stride's speed, turning
|
|
967
1025
|
// smoothly, and turns a standing one to face its place.
|
|
968
1026
|
advance(unit,dt) {
|
|
969
|
-
const root=unit.root.position;
|
|
1027
|
+
const root=unit.root.position,vibing=this.vibing();
|
|
970
1028
|
let want=unit.target?.face??unit.heading;
|
|
971
1029
|
if(unit.moving&&unit.path.length) {
|
|
972
1030
|
let step=unit.speed*dt;
|
|
@@ -976,7 +1034,7 @@ export class YardRenderer {
|
|
|
976
1034
|
if(d<=step){root.x=next.x;root.z=next.z;unit.path.shift();step-=d;}
|
|
977
1035
|
else {root.x+=dx/d*step;root.z+=dz/d*step;step=0;}
|
|
978
1036
|
}
|
|
979
|
-
if(!unit.path.length){unit.moving=false;this.play(unit);}
|
|
1037
|
+
if(!unit.path.length){if(vibing)this.roam(unit);else {unit.moving=false;this.play(unit);}}
|
|
980
1038
|
this.dirty=true;
|
|
981
1039
|
}
|
|
982
1040
|
const turn=angleTo(unit.heading,want);
|
|
@@ -988,13 +1046,15 @@ export class YardRenderer {
|
|
|
988
1046
|
// Helpers trail their character, closing most of the gap in a second.
|
|
989
1047
|
const pull=1-Math.exp(-4*dt);
|
|
990
1048
|
for(const helper of unit.helpers){
|
|
991
|
-
const
|
|
992
|
-
const dx=
|
|
993
|
-
|
|
1049
|
+
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};
|
|
1050
|
+
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);
|
|
1051
|
+
const size=vibing?VIBE.flight.scale:.45,grow=size-helper.model.scale.x;
|
|
1052
|
+
p.x+=dx*pull;p.y+=dy*pull;p.z+=dz*pull;
|
|
1053
|
+
if(grow)helper.model.scale.setScalar(Math.abs(grow)<1e-3?size:helper.model.scale.x+grow*pull);
|
|
994
1054
|
if(far>.05)helper.model.rotation.y=Math.atan2(dx,dz);
|
|
995
1055
|
const moving=far>.08;
|
|
996
1056
|
if(moving!==helper.moving){helper.moving=moving;this.helperClip(helper,moving?'move':helper.pose);}
|
|
997
|
-
if(far>1e-3)this.dirty=true;
|
|
1057
|
+
if(far>1e-3||Math.abs(dy)>1e-3||grow)this.dirty=true;
|
|
998
1058
|
}
|
|
999
1059
|
this.fire(unit);
|
|
1000
1060
|
}
|
|
@@ -1134,13 +1194,20 @@ export class YardRenderer {
|
|
|
1134
1194
|
spark.points.geometry.attributes.position.needsUpdate=true;
|
|
1135
1195
|
}
|
|
1136
1196
|
}
|
|
1197
|
+
restHelper(unit,helper) {
|
|
1198
|
+
helper.model.position.set(unit.root.position.x+helper.offset.x,.27,unit.root.position.z+helper.offset.z);
|
|
1199
|
+
helper.model.scale.setScalar(.45);
|
|
1200
|
+
helper.moving=false;helper.mixer.stopAllAction();helper.action=null;
|
|
1201
|
+
this.helperClip(helper,helper.pose);
|
|
1202
|
+
helper.mixer.update(0);
|
|
1203
|
+
}
|
|
1137
1204
|
helperClip(helper,name) {
|
|
1138
1205
|
const clip=helper.clips.find(c=>c.name===name)||helper.clips.find(c=>c.name===LEGACY[name])||helper.clips[0];
|
|
1139
1206
|
if(!clip)return;
|
|
1140
1207
|
const action=helper.mixer.clipAction(clip);
|
|
1141
1208
|
if(action===helper.action)return;
|
|
1142
1209
|
action.reset().play();
|
|
1143
|
-
if(helper.action)
|
|
1210
|
+
if(helper.action)helper.action.crossFadeTo(action,FADE,false);
|
|
1144
1211
|
helper.action=action;
|
|
1145
1212
|
}
|
|
1146
1213
|
clearHelpers(unit) {
|
|
@@ -1154,9 +1221,15 @@ export class YardRenderer {
|
|
|
1154
1221
|
const kinds=WORLDS[this.skin].helpers,source=this.pieceSource('companion');
|
|
1155
1222
|
const signature=JSON.stringify([source?.revision??null,agents]);
|
|
1156
1223
|
if(unit.helperSignature===signature)return;
|
|
1157
|
-
|
|
1224
|
+
const shown=agents.slice(0,SHOWN_HELPERS);
|
|
1225
|
+
unit.helpers=unit.helpers.filter(helper=>{
|
|
1226
|
+
if(shown.some(agent=>agent.id===helper.id))return true;
|
|
1227
|
+
unit.crew.remove(helper.model);helper.mixer.stopAllAction();helper.mixer.uncacheRoot(helper.model);disposeSkeletons(helper.model);
|
|
1228
|
+
return false;
|
|
1229
|
+
});
|
|
1158
1230
|
unit.helperSignature=signature;
|
|
1159
|
-
const rev
|
|
1231
|
+
const rev=++unit.revision,generation=this.request;
|
|
1232
|
+
this.dirty=true;this.drawOnce();
|
|
1160
1233
|
const stale=()=>this.disposed||generation!==this.request||rev!==unit.revision||!unit.root.parent;
|
|
1161
1234
|
let pieces=null;
|
|
1162
1235
|
if(source&&agents.length){
|
|
@@ -1171,26 +1244,31 @@ export class YardRenderer {
|
|
|
1171
1244
|
}
|
|
1172
1245
|
// The full helper count and all helper details remain in the canonical inspector.
|
|
1173
1246
|
// Small squads preserve readable character silhouettes at the overview scale.
|
|
1174
|
-
await Promise.all(
|
|
1247
|
+
await Promise.all(shown.map(async(agent,i)=>{
|
|
1248
|
+
const offset=helperOffset(i,agents.length),existing=unit.helpers.find(helper=>helper.id===agent.id);
|
|
1249
|
+
if(existing){
|
|
1250
|
+
existing.offset=offset;existing.pose=agent.pose;
|
|
1251
|
+
if(this.reduced)this.restHelper(unit,existing);
|
|
1252
|
+
else if(!existing.moving&&!this.vibing())this.helperClip(existing,agent.pose);
|
|
1253
|
+
this.dirty=true;this.drawOnce();
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1175
1256
|
try {
|
|
1176
1257
|
const n=hash(agent.name||agent.id)%4;
|
|
1177
1258
|
const asset=pieces?pieces[n]:await this.asset((agent.shell?kinds.shell:kinds.agent)+n);
|
|
1178
1259
|
if(stale())return;
|
|
1179
1260
|
const model=T.cloneSkeleton(asset.scene);
|
|
1180
1261
|
model.traverse(n=>{if(n.isSkinnedMesh)n.frustumCulled=false;});
|
|
1181
|
-
const offset=helperOffset(i,agents.length);
|
|
1182
1262
|
model.scale.setScalar(.45);model.position.set(unit.root.position.x+offset.x,.27,unit.root.position.z+offset.z);
|
|
1183
1263
|
unit.crew.add(model);
|
|
1184
1264
|
const mixer=new T.AnimationMixer(model);
|
|
1185
1265
|
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);
|
|
1266
|
+
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};
|
|
1267
|
+
if(this.reduced)this.restHelper(unit,helper);
|
|
1268
|
+
else {this.helperClip(helper,agent.pose);mixer.update(.3+(hash(agent.id)%40)/10);}
|
|
1189
1269
|
unit.helpers.push(helper);
|
|
1190
1270
|
this.dirty=true;this.drawOnce();
|
|
1191
1271
|
} catch {
|
|
1192
|
-
// The next state transition can rebuild this incomplete squad. Routine
|
|
1193
|
-
// unchanged updates do not call helpers(), so failure cannot loop.
|
|
1194
1272
|
if(!stale())unit.helperSignature=null;
|
|
1195
1273
|
}
|
|
1196
1274
|
}));
|
|
@@ -1215,9 +1293,10 @@ export class YardRenderer {
|
|
|
1215
1293
|
const w=this.host.clientWidth,h=this.host.clientHeight,buffer=this.renderer.getDrawingBufferSize(new T.Vector2());
|
|
1216
1294
|
const size=photoSize(w,h,buffer.x,buffer.y);
|
|
1217
1295
|
const viewport=this.renderer.getViewport({copy:v=>v.clone()}),ratio=this.renderer.getPixelRatio();
|
|
1218
|
-
const particleScale=this.
|
|
1296
|
+
const particleScale=this.pointScale,scale=particleScale.value;
|
|
1219
1297
|
const fireworks=this.sparks.filter(s=>s.firework).map(s=>({material:s.points.material,size:s.points.material.size}));
|
|
1220
|
-
const
|
|
1298
|
+
const embers=[...this.units.values()].some(unit=>unit.aura)?EMBER_SIZE:0;
|
|
1299
|
+
const radius=Math.max(Math.max(embers,...(this.effects?this.effects.preset.size:[]))*size.height*this.camera.projectionMatrix.elements[5]/4,
|
|
1221
1300
|
...fireworks.map(s=>s.size*size.scale/2));
|
|
1222
1301
|
const pad=Math.ceil(radius)+1,tw=buffer.x+2*pad,th=buffer.y+2*pad;
|
|
1223
1302
|
const canvas=Object.assign(document.createElement('canvas'),{width:size.width,height:size.height});
|
|
@@ -1231,7 +1310,7 @@ export class YardRenderer {
|
|
|
1231
1310
|
// Keep particle centers within a padded frustum. Extending the viewport
|
|
1232
1311
|
// beyond the canvas crops the margins without enlarging the live buffer.
|
|
1233
1312
|
this.renderer.setViewport(-pad/ratio,-pad/ratio,tw/ratio,th/ratio);
|
|
1234
|
-
|
|
1313
|
+
particleScale.value=th/2;
|
|
1235
1314
|
for(const {x,y}of size.tiles) {
|
|
1236
1315
|
this.camera.setViewOffset(size.width,size.height,x-pad,y-pad,tw,th);
|
|
1237
1316
|
this.renderer.render(this.scene,this.camera);
|
|
@@ -1240,7 +1319,7 @@ export class YardRenderer {
|
|
|
1240
1319
|
} finally {
|
|
1241
1320
|
for(const spark of fireworks)spark.material.size=spark.size;
|
|
1242
1321
|
this.renderer.setViewport(viewport);
|
|
1243
|
-
|
|
1322
|
+
particleScale.value=scale;
|
|
1244
1323
|
this.camera.clearViewOffset();
|
|
1245
1324
|
this.select(this.selected);
|
|
1246
1325
|
this.renderer.render(this.scene,this.camera);
|
|
@@ -1262,7 +1341,7 @@ export class YardRenderer {
|
|
|
1262
1341
|
this.pointer.set((event.clientX-rect.left)/rect.width*2-1,-(event.clientY-rect.top)/rect.height*2+1);
|
|
1263
1342
|
this.ray.setFromCamera(this.pointer,this.camera);
|
|
1264
1343
|
// A skinned mesh keeps the bounds of the pose it first had; picking needs the pose on screen.
|
|
1265
|
-
for(const unit of this.units.values())for(const root of [unit.root,unit.crew])root?.traverse(n=>{if(!n.isSkinnedMesh)return;n.computeBoundingSphere();if(n.boundingBox)n.computeBoundingBox();});
|
|
1344
|
+
for(const unit of this.units.values())for(const root of [unit.root,unit.crew])root?.traverse(n=>{if(!n.isSkinnedMesh||n.userData.aura)return;n.computeBoundingSphere();if(n.boundingBox)n.computeBoundingBox();});
|
|
1266
1345
|
const hits=this.ray.intersectObjects([...this.halls.values(),...this.units.values()].flatMap(i=>i.crew?[i.root,i.crew]:[i.root]),true);
|
|
1267
1346
|
for(const hit of hits) { for(let node=hit.object;node;node=node.parent) if(node.userData.entity)return node.userData.entity; }
|
|
1268
1347
|
return null;
|
|
@@ -1343,9 +1422,13 @@ export class YardRenderer {
|
|
|
1343
1422
|
this.list.style.top=Math.max(6,ct+half+4+lh<=h-6?ct+half+4:ct-half-4-lh)+'px';
|
|
1344
1423
|
}
|
|
1345
1424
|
setReducedMotion(value) {
|
|
1425
|
+
const was=this.vibing();
|
|
1346
1426
|
this.reduced=value;
|
|
1427
|
+
if(was!==this.vibing())this.restage();
|
|
1347
1428
|
if(value)for(const unit of this.units.values()){this.unlook(unit);unit.lookYaw=0;unit.lookPitch=0;}
|
|
1348
1429
|
if(value)for(const unit of this.units.values())if(unit.moving&&unit.target){this.arrive(unit);this.play(unit);}
|
|
1430
|
+
if(value)for(const unit of this.units.values())for(const helper of unit.helpers)this.restHelper(unit,helper);
|
|
1431
|
+
for(const unit of this.units.values())unit.aura?.set(unit.aura.active,unit.aura.color,value);
|
|
1349
1432
|
if(value){for(const item of this.halls.values())item.level=item.target;this.glow();this.stillEffects();this.clearFireworks();}
|
|
1350
1433
|
this.dirty=true;this.drawOnce();
|
|
1351
1434
|
}
|
|
@@ -1372,12 +1455,14 @@ export class YardRenderer {
|
|
|
1372
1455
|
// Advances walks, turns, helpers, clips, glances, sparks and the halls'
|
|
1373
1456
|
// glow and effects by dt seconds, without drawing.
|
|
1374
1457
|
step(dt) {
|
|
1458
|
+
const vibing=this.vibing();
|
|
1459
|
+
if(vibing)this.vibeTime+=dt;
|
|
1375
1460
|
for(const unit of this.units.values()) {
|
|
1376
1461
|
this.advance(unit,dt);
|
|
1377
1462
|
// Characters out of view animate five times a second, enough to stay
|
|
1378
1463
|
// in step for when the camera comes back to them.
|
|
1379
1464
|
let step=dt;
|
|
1380
|
-
if(unit.offscreen){unit.lag=(unit.lag||0)+dt;if(unit.lag<.2)continue;step=unit.lag;}
|
|
1465
|
+
if(unit.offscreen&&!vibing){unit.lag=(unit.lag||0)+dt;if(unit.lag<.2)continue;step=unit.lag;}
|
|
1381
1466
|
unit.lag=0;
|
|
1382
1467
|
this.unlook(unit);
|
|
1383
1468
|
for(const mixer of unit.mixers)mixer.update(step);
|
|
@@ -1388,6 +1473,7 @@ export class YardRenderer {
|
|
|
1388
1473
|
this.updateSparks(dt);
|
|
1389
1474
|
this.updateHalls(dt);
|
|
1390
1475
|
this.stepMarks(dt);
|
|
1476
|
+
for(const unit of this.units.values())if(unit.aura?.advance(dt))this.dirty=true;
|
|
1391
1477
|
}
|
|
1392
1478
|
drawOnce() { if(this.active)this.schedule(); }
|
|
1393
1479
|
mark(unit,place) {
|
|
@@ -1440,6 +1526,7 @@ export class YardRenderer {
|
|
|
1440
1526
|
for(const mixer of unit.mixers){mixer.stopAllAction();mixer.uncacheRoot(mixer.getRoot());}
|
|
1441
1527
|
for(const helper of unit.helpers){helper.mixer.stopAllAction();helper.mixer.uncacheRoot(helper.model);}
|
|
1442
1528
|
for(const mark of unit.marks?.values()||[])this.dropMark(mark);
|
|
1529
|
+
unit.aura?.dispose();
|
|
1443
1530
|
disposeSkeletons(unit.root);disposeSkeletons(unit.crew);
|
|
1444
1531
|
this.scene.remove(unit.root);this.scene.remove(unit.crew);unit.label.remove();unit.ring.geometry.dispose();unit.ring.material.dispose();
|
|
1445
1532
|
}
|
|
@@ -1456,6 +1543,7 @@ export class YardRenderer {
|
|
|
1456
1543
|
disposeWorld(this.world);
|
|
1457
1544
|
for(const entry of this.cache.values())entry.then(a=>disposeTree(a.scene)).catch(noop);this.cache.clear();
|
|
1458
1545
|
for(const texture of Object.values(this.glyphs||{}))texture.dispose();this.markPlane.dispose();
|
|
1546
|
+
for(const shape of Object.values(this.auraShapes))shape.dispose();
|
|
1459
1547
|
this.sun.shadow.map?.dispose();
|
|
1460
1548
|
this.canvas.removeEventListener('webglcontextlost',this.contextLost);
|
|
1461
1549
|
this.canvas.removeEventListener('pointerdown',this.onPointerDown);
|
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); }
|