@oddessentials/agent-guild 1.29.0 → 1.30.1
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/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 +6 -2
- package/node_modules/@jsquash/jpeg/CHANGELOG.md +54 -0
- package/node_modules/@jsquash/jpeg/LICENSE +201 -0
- package/node_modules/@jsquash/jpeg/README.md +105 -0
- package/node_modules/@jsquash/jpeg/codec/LICENSE.codec.md +132 -0
- package/node_modules/@jsquash/jpeg/codec/dec/mozjpeg_dec.d.ts +7 -0
- package/node_modules/@jsquash/jpeg/codec/dec/mozjpeg_dec.js +15 -0
- package/node_modules/@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm +0 -0
- package/node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.d.ts +37 -0
- package/node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.js +15 -0
- package/node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm +0 -0
- package/node_modules/@jsquash/jpeg/codec/pre.js +24 -0
- package/node_modules/@jsquash/jpeg/decode.d.ts +16 -0
- package/node_modules/@jsquash/jpeg/decode.js +36 -0
- package/node_modules/@jsquash/jpeg/emscripten-types.d.ts +137 -0
- package/node_modules/@jsquash/jpeg/encode.d.ts +21 -0
- package/node_modules/@jsquash/jpeg/encode.js +35 -0
- package/node_modules/@jsquash/jpeg/index.d.ts +3 -0
- package/node_modules/@jsquash/jpeg/index.js +2 -0
- package/node_modules/@jsquash/jpeg/meta.d.ts +24 -0
- package/node_modules/@jsquash/jpeg/meta.js +25 -0
- package/node_modules/@jsquash/jpeg/package.json +32 -0
- package/node_modules/@jsquash/jpeg/utils.d.ts +17 -0
- package/node_modules/@jsquash/jpeg/utils.js +31 -0
- package/node_modules/@jsquash/png/CHANGELOG.md +69 -0
- package/node_modules/@jsquash/png/LICENSE +201 -0
- package/node_modules/@jsquash/png/README.md +114 -0
- package/node_modules/@jsquash/png/codec/LICENSE.codec.md +29 -0
- package/node_modules/@jsquash/png/codec/README.md +5 -0
- package/node_modules/@jsquash/png/codec/pkg/README.md +5 -0
- package/node_modules/@jsquash/png/codec/pkg/squoosh_png.d.ts +71 -0
- package/node_modules/@jsquash/png/codec/pkg/squoosh_png.js +314 -0
- package/node_modules/@jsquash/png/codec/pkg/squoosh_png_bg.wasm +0 -0
- package/node_modules/@jsquash/png/codec/pkg/squoosh_png_bg.wasm.d.ts +13 -0
- package/node_modules/@jsquash/png/codec/pre.js +24 -0
- package/node_modules/@jsquash/png/decode.d.ts +29 -0
- package/node_modules/@jsquash/png/decode.js +35 -0
- package/node_modules/@jsquash/png/encode.d.ts +31 -0
- package/node_modules/@jsquash/png/encode.js +40 -0
- package/node_modules/@jsquash/png/index.d.ts +3 -0
- package/node_modules/@jsquash/png/index.js +2 -0
- package/node_modules/@jsquash/png/meta.d.ts +16 -0
- package/node_modules/@jsquash/png/meta.js +15 -0
- package/node_modules/@jsquash/png/package.json +34 -0
- package/node_modules/@jsquash/webp/CHANGELOG.md +48 -0
- package/node_modules/@jsquash/webp/LICENSE +201 -0
- package/node_modules/@jsquash/webp/README.md +100 -0
- package/node_modules/@jsquash/webp/codec/LICENSE.codec.md +29 -0
- package/node_modules/@jsquash/webp/codec/Makefile.animated.backup +88 -0
- package/node_modules/@jsquash/webp/codec/README.md +5 -0
- package/node_modules/@jsquash/webp/codec/dec/README.md +18 -0
- package/node_modules/@jsquash/webp/codec/dec/webp_dec.d.ts +7 -0
- package/node_modules/@jsquash/webp/codec/dec/webp_dec.js +16 -0
- package/node_modules/@jsquash/webp/codec/dec/webp_dec.wasm +0 -0
- package/node_modules/@jsquash/webp/codec/enc/README.md +22 -0
- package/node_modules/@jsquash/webp/codec/enc/webp_enc.d.ts +42 -0
- package/node_modules/@jsquash/webp/codec/enc/webp_enc.js +16 -0
- package/node_modules/@jsquash/webp/codec/enc/webp_enc.wasm +0 -0
- package/node_modules/@jsquash/webp/codec/enc/webp_enc_simd.d.ts +1 -0
- package/node_modules/@jsquash/webp/codec/enc/webp_enc_simd.js +16 -0
- package/node_modules/@jsquash/webp/codec/enc/webp_enc_simd.wasm +0 -0
- package/node_modules/@jsquash/webp/codec/pre.js +24 -0
- package/node_modules/@jsquash/webp/decode.d.ts +15 -0
- package/node_modules/@jsquash/webp/decode.js +34 -0
- package/node_modules/@jsquash/webp/emscripten-types.d.ts +137 -0
- package/node_modules/@jsquash/webp/encode.d.ts +23 -0
- package/node_modules/@jsquash/webp/encode.js +43 -0
- package/node_modules/@jsquash/webp/index.d.ts +3 -0
- package/node_modules/@jsquash/webp/index.js +2 -0
- package/node_modules/@jsquash/webp/meta.d.ts +19 -0
- package/node_modules/@jsquash/webp/meta.js +33 -0
- package/node_modules/@jsquash/webp/package.json +35 -0
- package/node_modules/@jsquash/webp/utils.d.ts +17 -0
- package/node_modules/@jsquash/webp/utils.js +31 -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/node_modules/wasm-feature-detect/LICENSE +202 -0
- package/node_modules/wasm-feature-detect/README.md +109 -0
- package/node_modules/wasm-feature-detect/README.md.ejs +89 -0
- package/node_modules/wasm-feature-detect/dist/cjs/index.cjs +1 -0
- package/node_modules/wasm-feature-detect/dist/esm/index.js +1 -0
- package/node_modules/wasm-feature-detect/dist/index.d.cts +24 -0
- package/node_modules/wasm-feature-detect/dist/index.d.ts +24 -0
- package/node_modules/wasm-feature-detect/dist/umd/index.js +1 -0
- package/node_modules/wasm-feature-detect/package.json +53 -0
- package/package.json +4 -1
- package/src/manager/config.mjs +1 -0
- package/src/manager/folder-browser.mjs +31 -2
- package/src/manager/main.mjs +2 -0
- package/src/manager/server.mjs +19 -5
- package/src/manager/steam-workshop.mjs +64 -0
- package/src/manager/yard-piece-check.mjs +424 -0
- package/src/manager/yard-pieces.mjs +188 -0
- package/web/app.js +55 -17
- package/web/index.html +14 -1
- package/web/skins/guild/skin.css +2 -13
- package/web/styles.css +14 -0
- package/web/yard/art.json +1 -1
- package/web/yard/model.mjs +44 -2
- package/web/yard/renderer.js +136 -27
- package/web/yard/view.js +123 -11
package/web/yard/renderer.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
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, 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 } from './model.mjs';
|
|
4
4
|
|
|
5
5
|
const ASSETS = new URL('./assets/', import.meta.url);
|
|
6
6
|
const vector = new T.Vector3();
|
|
7
7
|
const lookModel = new T.Matrix4(), lookParent = new T.Matrix4(), lookFrame = new T.Quaternion(), lookTurn = new T.Quaternion(), lookTilt = new T.Quaternion();
|
|
8
8
|
const axisX = new T.Vector3(1, 0, 0), axisY = new T.Vector3(0, 1, 0);
|
|
9
9
|
const noop = () => {};
|
|
10
|
+
const cancelled = new WeakSet();
|
|
11
|
+
addEventListener('unhandledrejection', event => { if (cancelled.has(event.reason)) event.preventDefault(); });
|
|
12
|
+
const cancel = loader => { const reason = new DOMException('This piece is no longer shown.', 'AbortError'); cancelled.add(reason); loader.abortController.abort(reason); };
|
|
10
13
|
const worldFile = (skin, custom, file) => `../worlds/${skin}/${file.split('/').pop()}?v=${custom.revision}`;
|
|
14
|
+
const pieceFiles = (skin, slot, piece) => (slot === 'companion' ? [0, 1, 2, 3].map(n => `companion_${n}.glb`) : ['character.glb'])
|
|
15
|
+
.map(file => new URL(`../pieces/${skin}/${slot}/${file}?v=${piece.revision}`, ASSETS).href);
|
|
11
16
|
const vector2 = new T.Vector2();
|
|
12
17
|
// How fast characters turn to a new heading, in radians a second, and how
|
|
13
18
|
// long a clip change blends.
|
|
@@ -82,6 +87,21 @@ function disposeSkeletons(root) {
|
|
|
82
87
|
root?.traverse(node=>{if(node.skeleton)skeletons.add(node.skeleton);});
|
|
83
88
|
for(const skeleton of skeletons)skeleton.dispose();
|
|
84
89
|
}
|
|
90
|
+
function checkPiece({scene,animations}) {
|
|
91
|
+
if(!animations.every(playableClip))throw new Error('An animation in this piece has values the Yard cannot play.');
|
|
92
|
+
const probe=T.cloneSkeleton(scene);
|
|
93
|
+
try {
|
|
94
|
+
probe.updateMatrixWorld(true);
|
|
95
|
+
probe.traverse(node=>{
|
|
96
|
+
if(!node.isMesh)return;
|
|
97
|
+
const vertices=node.geometry.attributes.position?.count,triangles=node.geometry.index?.array;
|
|
98
|
+
if(!vertices||(triangles&&!triangles.every(at=>at<vertices)))throw new Error('A mesh in this piece refers to vertices it does not have.');
|
|
99
|
+
if(!node.isSkinnedMesh)return;
|
|
100
|
+
node.skeleton.update();node.computeBoundingBox();node.computeBoundingSphere();
|
|
101
|
+
if(!Number.isFinite(node.boundingSphere?.radius))throw new Error('A rigged mesh in this piece cannot be drawn.');
|
|
102
|
+
});
|
|
103
|
+
} finally {disposeSkeletons(probe);}
|
|
104
|
+
}
|
|
85
105
|
function disposeTree(root) {
|
|
86
106
|
disposeSkeletons(root);
|
|
87
107
|
const geometries=new Set(),materials=new Set();
|
|
@@ -119,8 +139,9 @@ function disposeWorld(root) {
|
|
|
119
139
|
}
|
|
120
140
|
export class YardRenderer {
|
|
121
141
|
// clock gives the time staging is judged at; tests pass a fixed one.
|
|
122
|
-
constructor(host,labels,{select,open,error=noop,clock=Date.now}) {
|
|
123
|
-
this.host=host;this.labels=labels;this.onSelect=select;this.onOpen=open;this.onError=error;this.clock=clock;
|
|
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;
|
|
144
|
+
this.pieceSets={};this.shownPieces={skin:null,pieces:{}};this.pieceLoaders=new Map();this.preloads=new Map();
|
|
124
145
|
this.scene=new T.Scene();this.units=new Map();this.halls=new Map();this.slots=new Map();this.cache=new Map();
|
|
125
146
|
this.loadingManager=new T.LoadingManager();
|
|
126
147
|
this.providers=[];this.sessions=[];this.selected=null;this.world=null;this.skin=null;this.request=0;this.worldRequest=0;this.pending=null;
|
|
@@ -177,20 +198,29 @@ export class YardRenderer {
|
|
|
177
198
|
this.resizeObserver=new ResizeObserver(()=>this.resize());this.resizeObserver.observe(host);
|
|
178
199
|
this.resize();this.overview();
|
|
179
200
|
}
|
|
180
|
-
async asset(name) {
|
|
201
|
+
async asset(name,url) {
|
|
181
202
|
if(!this.cache.has(name)) {
|
|
182
|
-
const
|
|
203
|
+
const failed=[],manager=url?new T.LoadingManager():this.loadingManager;
|
|
204
|
+
if(url){manager.onError=item=>failed.push(item);this.pieceLoaders.set(name,manager);}
|
|
205
|
+
const pending=new T.GLTFLoader(manager).loadAsync(url||new URL(name+'.glb',ASSETS).href).then(asset=>{
|
|
206
|
+
try {
|
|
207
|
+
if(failed.length)throw new Error('A texture in this piece could not be drawn.');
|
|
208
|
+
if(url)checkPiece(asset);
|
|
209
|
+
return asset;
|
|
210
|
+
} catch(err){disposeTree(asset.scene);throw err;}
|
|
211
|
+
}).catch(err=>{
|
|
183
212
|
if(this.cache.get(name)===pending)this.cache.delete(name);
|
|
184
213
|
throw err;
|
|
185
214
|
});
|
|
186
215
|
this.cache.set(name,pending);
|
|
216
|
+
if(url)pending.catch(noop).finally(()=>{if(this.pieceLoaders.get(name)===manager)this.pieceLoaders.delete(name);});
|
|
187
217
|
}
|
|
188
218
|
return this.cache.get(name);
|
|
189
219
|
}
|
|
190
|
-
goldAsset(name) {
|
|
220
|
+
goldAsset(name,url) {
|
|
191
221
|
const key='gold:'+name;
|
|
192
222
|
if(!this.cache.has(key)) {
|
|
193
|
-
const pending=this.asset(name).then(asset=>{const scene=T.cloneSkeleton(asset.scene);gild(scene);return {...asset,scene};}).catch(err=>{
|
|
223
|
+
const pending=this.asset(name,url).then(asset=>{const scene=T.cloneSkeleton(asset.scene);gild(scene);return {...asset,scene};}).catch(err=>{
|
|
194
224
|
if(this.cache.get(key)===pending)this.cache.delete(key);
|
|
195
225
|
throw err;
|
|
196
226
|
});
|
|
@@ -198,6 +228,59 @@ export class YardRenderer {
|
|
|
198
228
|
}
|
|
199
229
|
return this.cache.get(key);
|
|
200
230
|
}
|
|
231
|
+
activePieces() { return this.pieceSets[this.skin]||{}; }
|
|
232
|
+
pieceSource(slot) {
|
|
233
|
+
const piece=this.activePieces()[slot];
|
|
234
|
+
return piece?{slot,revision:piece.revision,urls:pieceFiles(this.skin,slot,piece)}:null;
|
|
235
|
+
}
|
|
236
|
+
// skin's pieces by slot, each { revision }; a slot without one shows the original.
|
|
237
|
+
setPieces(skin,pieces) {
|
|
238
|
+
this.pieceSets[skin]=pieces;
|
|
239
|
+
if(skin===this.skin)this.applyPieces();
|
|
240
|
+
}
|
|
241
|
+
applyPieces() {
|
|
242
|
+
if(this.disposed||!this.world)return;
|
|
243
|
+
const before=this.shownPieces,after={skin:this.skin,pieces:{...this.activePieces()}};
|
|
244
|
+
const changed=[...new Set([...Object.keys(before.pieces),...Object.keys(after.pieces)])]
|
|
245
|
+
.filter(slot=>before.skin!==after.skin||before.pieces[slot]?.revision!==after.pieces[slot]?.revision);
|
|
246
|
+
if(!changed.length&&before.skin===after.skin)return;
|
|
247
|
+
this.shownPieces=after;
|
|
248
|
+
if(before.skin===after.skin)for(const unit of this.units.values()){
|
|
249
|
+
if(changed.includes('companion'))this.clearHelpers(unit);
|
|
250
|
+
if(changed.includes(PROVIDER_ORDER[characterIndex(unit.session.provider.id)])){this.dropModel(unit);this.loadUnit(unit,unit.session);}
|
|
251
|
+
if(changed.includes('companion')&&unit.loaded)this.helpers(unit,unit.session);
|
|
252
|
+
}
|
|
253
|
+
for(const slot of changed){
|
|
254
|
+
if(before.pieces[slot]){this.preloads.delete(slot);for(const url of pieceFiles(before.skin,slot,before.pieces[slot]))this.release(url);}
|
|
255
|
+
if(after.pieces[slot])this.preload(slot,after.pieces[slot]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
preload(slot,piece) {
|
|
259
|
+
const skin=this.skin,attempt={};
|
|
260
|
+
this.preloads.set(slot,attempt);
|
|
261
|
+
Promise.all(pieceFiles(skin,slot,piece).map(url=>this.asset(url,url))).then(()=>true,()=>false).then(ok=>{
|
|
262
|
+
if(this.preloads.get(slot)!==attempt)return;
|
|
263
|
+
this.preloads.delete(slot);
|
|
264
|
+
if(!this.disposed&&this.skin===skin&&this.activePieces()[slot]?.revision===piece.revision)this.onPiece({skin,slot,revision:piece.revision,ok});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
pieceFailed(source) {
|
|
268
|
+
if(this.activePieces()[source.slot]?.revision===source.revision)this.onPiece({skin:this.skin,slot:source.slot,revision:source.revision,ok:false});
|
|
269
|
+
}
|
|
270
|
+
release(name) {
|
|
271
|
+
const loader=this.pieceLoaders.get(name);if(loader){cancel(loader);this.pieceLoaders.delete(name);}
|
|
272
|
+
for(const key of [name,'gold:'+name]){
|
|
273
|
+
const pending=this.cache.get(key);
|
|
274
|
+
if(!pending)continue;
|
|
275
|
+
this.cache.delete(key);
|
|
276
|
+
pending.then(asset=>disposeTree(asset.scene)).catch(noop);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
dropModel(unit) {
|
|
280
|
+
for(const mixer of unit.mixers){mixer.stopAllAction();mixer.uncacheRoot(mixer.getRoot());}
|
|
281
|
+
if(unit.model){unit.root.remove(unit.model);disposeSkeletons(unit.model);}
|
|
282
|
+
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
|
+
}
|
|
201
284
|
// Shows skin's world in theme, or only theme's plates when that world is
|
|
202
285
|
// already up. What is on screen stays until the new world or theme is
|
|
203
286
|
// ready; progress receives the bytes downloaded so far.
|
|
@@ -235,6 +318,7 @@ export class YardRenderer {
|
|
|
235
318
|
this.halls.clear();
|
|
236
319
|
if(this.world){this.scene.remove(this.world);disposeWorld(this.world);}
|
|
237
320
|
this.world=loaded.scene;this.skin=skin;this.custom=custom;this.scene.add(this.world);
|
|
321
|
+
this.applyPieces();
|
|
238
322
|
this.makeEffects(WORLDS[skin].effect);
|
|
239
323
|
this.light(skin,theme);
|
|
240
324
|
this.host.dataset.world=skin;
|
|
@@ -593,22 +677,24 @@ export class YardRenderer {
|
|
|
593
677
|
}
|
|
594
678
|
async loadUnit(unit,session) {
|
|
595
679
|
if(unit.loaded||unit.loading)return;
|
|
596
|
-
const generation=this.request;
|
|
597
|
-
const
|
|
598
|
-
const
|
|
680
|
+
const generation=this.request,token=unit.load=(unit.load||0)+1;
|
|
681
|
+
const index=characterIndex(session.provider.id),source=this.pieceSource(PROVIDER_ORDER[index]);
|
|
682
|
+
const name=source?source.urls[0]:WORLDS[this.skin].characters+index;
|
|
683
|
+
const stale=()=>this.disposed||generation!==this.request||token!==unit.load||!unit.root.parent;
|
|
599
684
|
unit.loading=true;
|
|
600
685
|
try {
|
|
601
|
-
const asset=await (GILDED.includes(session.provider.id)?this.goldAsset(
|
|
602
|
-
if(
|
|
686
|
+
const asset=await (GILDED.includes(session.provider.id)?this.goldAsset(name,source?.urls[0]):this.asset(name,source?.urls[0]));
|
|
687
|
+
if(stale())return;
|
|
603
688
|
const model=T.cloneSkeleton(asset.scene);
|
|
604
689
|
model.rotation.y=unit.heading;
|
|
605
|
-
model.traverse(n=>{if(n.isMesh){n.castShadow=true;n.receiveShadow=true;}});
|
|
690
|
+
model.traverse(n=>{if(n.isMesh){n.castShadow=true;n.receiveShadow=true;}if(n.isSkinnedMesh)n.frustumCulled=false;});
|
|
606
691
|
unit.root.add(model);unit.model=model;unit.loaded=true;
|
|
607
692
|
unit.label.title='';
|
|
608
693
|
// The walk's stride and cycle, and where blows land, as rig.py wrote them.
|
|
609
694
|
let yard=null;asset.scene.traverse(n=>{if(n.userData?.yard)yard=n.userData.yard;});
|
|
610
|
-
|
|
611
|
-
unit.
|
|
695
|
+
yard=yardExtras(yard,'character');
|
|
696
|
+
unit.speed=yard.stride/yard.cycle;
|
|
697
|
+
unit.events=yard.events;unit.height=yard.height;unit.surface=yard.surface;unit.look=lookLimits(yard.look);
|
|
612
698
|
const neck=model.getObjectByName('Neck'),head=model.getObjectByName('Head');
|
|
613
699
|
unit.lookBones=neck&&head&&model.getObjectByName('LeftUpperLeg')?{neck,head}:null;
|
|
614
700
|
this.scaleProp(unit.session?.id||session.id);
|
|
@@ -618,8 +704,10 @@ export class YardRenderer {
|
|
|
618
704
|
unit.clip=null;this.play(unit);
|
|
619
705
|
this.helpers(unit,unit.session||session);this.dirty=true;this.drawOnce();
|
|
620
706
|
} catch(err){
|
|
621
|
-
if(
|
|
622
|
-
|
|
707
|
+
if(stale())return;
|
|
708
|
+
if(source){this.pieceFailed(source);return;}
|
|
709
|
+
console.warn('Yard character unavailable:',err.message);unit.label.title='Character art unavailable. Session controls remain available.';
|
|
710
|
+
} finally {if(token===unit.load)unit.loading=false;}
|
|
623
711
|
}
|
|
624
712
|
// A hall's own copies of the materials that glow with its crew, so one
|
|
625
713
|
// hall lighting up does not light the others that share them.
|
|
@@ -1055,28 +1143,47 @@ export class YardRenderer {
|
|
|
1055
1143
|
if(helper.action){if(this.reduced)helper.action.stop();else helper.action.crossFadeTo(action,FADE,false);}
|
|
1056
1144
|
helper.action=action;
|
|
1057
1145
|
}
|
|
1146
|
+
clearHelpers(unit) {
|
|
1147
|
+
unit.revision++;unit.helperSignature=null;
|
|
1148
|
+
for(const helper of unit.helpers){unit.crew.remove(helper.model);helper.mixer?.stopAllAction();helper.mixer?.uncacheRoot(helper.model);disposeSkeletons(helper.model);}
|
|
1149
|
+
unit.helpers=[];
|
|
1150
|
+
this.dirty=true;this.drawOnce();
|
|
1151
|
+
}
|
|
1058
1152
|
async helpers(unit,session) {
|
|
1059
1153
|
const agents=helpersFor(session);
|
|
1060
|
-
const
|
|
1154
|
+
const kinds=WORLDS[this.skin].helpers,source=this.pieceSource('companion');
|
|
1155
|
+
const signature=JSON.stringify([source?.revision??null,agents]);
|
|
1061
1156
|
if(unit.helperSignature===signature)return;
|
|
1157
|
+
this.clearHelpers(unit);
|
|
1062
1158
|
unit.helperSignature=signature;
|
|
1063
|
-
const rev
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1159
|
+
const rev=unit.revision,generation=this.request;
|
|
1160
|
+
const stale=()=>this.disposed||generation!==this.request||rev!==unit.revision||!unit.root.parent;
|
|
1161
|
+
let pieces=null;
|
|
1162
|
+
if(source&&agents.length){
|
|
1163
|
+
try {pieces=await Promise.all(source.urls.map(url=>this.asset(url,url)));}
|
|
1164
|
+
catch {
|
|
1165
|
+
if(stale())return;
|
|
1166
|
+
unit.helperSignature=null;
|
|
1167
|
+
this.pieceFailed(source);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
if(stale())return;
|
|
1171
|
+
}
|
|
1067
1172
|
// The full helper count and all helper details remain in the canonical inspector.
|
|
1068
1173
|
// Small squads preserve readable character silhouettes at the overview scale.
|
|
1069
1174
|
await Promise.all(agents.slice(0,SHOWN_HELPERS).map(async(agent,i)=>{
|
|
1070
1175
|
try {
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1176
|
+
const n=hash(agent.name||agent.id)%4;
|
|
1177
|
+
const asset=pieces?pieces[n]:await this.asset((agent.shell?kinds.shell:kinds.agent)+n);
|
|
1178
|
+
if(stale())return;
|
|
1073
1179
|
const model=T.cloneSkeleton(asset.scene);
|
|
1180
|
+
model.traverse(n=>{if(n.isSkinnedMesh)n.frustumCulled=false;});
|
|
1074
1181
|
const offset=helperOffset(i,agents.length);
|
|
1075
1182
|
model.scale.setScalar(.45);model.position.set(unit.root.position.x+offset.x,.27,unit.root.position.z+offset.z);
|
|
1076
1183
|
unit.crew.add(model);
|
|
1077
1184
|
const mixer=new T.AnimationMixer(model);
|
|
1078
1185
|
let yard=null;asset.scene.traverse(n=>{if(n.userData?.yard)yard=n.userData.yard;});
|
|
1079
|
-
const helper={id:agent.id,top:(yard
|
|
1186
|
+
const helper={id:agent.id,top:yardExtras(yard,'helper').height*model.scale.y,model,mixer,offset,pose:agent.pose,clips:asset.animations,moving:false,action:null};
|
|
1080
1187
|
this.helperClip(helper,agent.pose);
|
|
1081
1188
|
mixer.update(.3+(hash(agent.id)%40)/10);
|
|
1082
1189
|
unit.helpers.push(helper);
|
|
@@ -1084,7 +1191,7 @@ export class YardRenderer {
|
|
|
1084
1191
|
} catch {
|
|
1085
1192
|
// The next state transition can rebuild this incomplete squad. Routine
|
|
1086
1193
|
// unchanged updates do not call helpers(), so failure cannot loop.
|
|
1087
|
-
if(!
|
|
1194
|
+
if(!stale())unit.helperSignature=null;
|
|
1088
1195
|
}
|
|
1089
1196
|
}));
|
|
1090
1197
|
}
|
|
@@ -1154,6 +1261,8 @@ export class YardRenderer {
|
|
|
1154
1261
|
const rect=this.canvas.getBoundingClientRect();
|
|
1155
1262
|
this.pointer.set((event.clientX-rect.left)/rect.width*2-1,-(event.clientY-rect.top)/rect.height*2+1);
|
|
1156
1263
|
this.ray.setFromCamera(this.pointer,this.camera);
|
|
1264
|
+
// 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();});
|
|
1157
1266
|
const hits=this.ray.intersectObjects([...this.halls.values(),...this.units.values()].flatMap(i=>i.crew?[i.root,i.crew]:[i.root]),true);
|
|
1158
1267
|
for(const hit of hits) { for(let node=hit.object;node;node=node.parent) if(node.userData.entity)return node.userData.entity; }
|
|
1159
1268
|
return null;
|
|
@@ -1341,7 +1450,7 @@ export class YardRenderer {
|
|
|
1341
1450
|
this.clearFireworks();this.fireworkTexture?.dispose();
|
|
1342
1451
|
for(const spark of this.sparks){spark.points.geometry.dispose();spark.points.material.dispose();}this.sparks=[];
|
|
1343
1452
|
this.releaseEffects();this.clearProps();
|
|
1344
|
-
this.loadingManager.abort();this.pending?.close();this.pending=null;
|
|
1453
|
+
this.loadingManager.abort();for(const loader of this.pieceLoaders.values())cancel(loader);this.pieceLoaders.clear();this.pending?.close();this.pending=null;
|
|
1345
1454
|
this.resizeObserver.disconnect();this.controls.dispose();this.clearUnits();
|
|
1346
1455
|
for(const item of this.halls.values()){item.label.remove();item.chip.remove();}this.halls.clear();this.list.remove();
|
|
1347
1456
|
disposeWorld(this.world);
|
package/web/yard/view.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WORLDS, sessionPose } from './model.mjs';
|
|
1
|
+
import { WORLDS, PROVIDER_ORDER, sessionPose } from './model.mjs';
|
|
2
2
|
|
|
3
3
|
const $ = id => document.getElementById(id);
|
|
4
4
|
const make = (tag, cls, text) => Object.assign(document.createElement(tag), { className: cls, ...(text === undefined ? {} : { textContent: text }) });
|
|
@@ -17,6 +17,8 @@ async function boundedLoad(start, onProgress) {
|
|
|
17
17
|
} finally { settled = true; clearTimeout(timer); }
|
|
18
18
|
}
|
|
19
19
|
const megabytes = bytes => (bytes / 1048576).toFixed(1) + ' MB';
|
|
20
|
+
const CREWS = { anthropic: 'Claude Code', openai: 'Codex CLI', google: 'Antigravity CLI', xai: 'Grok Build', shell: 'Shell', docker: 'Docker Agent' };
|
|
21
|
+
const pieceKind = slot => slot === 'companion' ? 'companion' : 'character';
|
|
20
22
|
|
|
21
23
|
/** A layout with injected canonical controls. No API, sockets, or business rules. */
|
|
22
24
|
export function initYard(controller) {
|
|
@@ -36,9 +38,18 @@ export function initYard(controller) {
|
|
|
36
38
|
return world && !world.problem && unloadable.get(skin) !== world.revision ? world : null;
|
|
37
39
|
};
|
|
38
40
|
const titleFor = (skin) => customFor(skin)?.title ?? WORLDS[skin].title;
|
|
41
|
+
const worldShown = new Map();
|
|
39
42
|
const loadWorld = (target, skin, theme, progress) => {
|
|
43
|
+
if (target.skin !== skin) {
|
|
44
|
+
worldShown.delete(skin);
|
|
45
|
+
for (const [key, shown] of pieceShown) if (key.startsWith(skin + '/') && shown.ok) pieceShown.delete(key);
|
|
46
|
+
target.setPieces(skin, piecesFor(skin));
|
|
47
|
+
worldSpot();
|
|
48
|
+
}
|
|
40
49
|
const custom = customFor(skin);
|
|
41
|
-
return target.setWorld(skin, theme, { progress, custom }).
|
|
50
|
+
return target.setWorld(skin, theme, { progress, custom }).then(() => {
|
|
51
|
+
if (custom) { worldShown.set(skin, custom.revision); worldSpot(); }
|
|
52
|
+
}, (err) => {
|
|
42
53
|
if (!custom || disposed) throw err;
|
|
43
54
|
unloadable.set(skin, custom.revision);
|
|
44
55
|
if (skin === lastSkin) lastRevision = null;
|
|
@@ -46,6 +57,99 @@ export function initYard(controller) {
|
|
|
46
57
|
return target.setWorld(skin, theme, { progress });
|
|
47
58
|
});
|
|
48
59
|
};
|
|
60
|
+
const pieceShown = new Map();
|
|
61
|
+
const pieceKey = (skin, slot) => skin + '/' + slot;
|
|
62
|
+
const piecesOf = (skin) => controller.yardPieces?.(skin) ?? {};
|
|
63
|
+
const piecesFor = (skin) => {
|
|
64
|
+
const pieces = {};
|
|
65
|
+
for (const [slot, piece] of Object.entries(piecesOf(skin))) {
|
|
66
|
+
const shown = pieceShown.get(pieceKey(skin, slot));
|
|
67
|
+
if (piece.revision && !(shown?.revision === piece.revision && !shown.ok)) pieces[slot] = { revision: piece.revision };
|
|
68
|
+
}
|
|
69
|
+
return pieces;
|
|
70
|
+
};
|
|
71
|
+
const syncPieces = (target) => {
|
|
72
|
+
const skin = root.dataset.skin;
|
|
73
|
+
if (target && WORLDS[skin]) target.setPieces(skin, piecesFor(skin));
|
|
74
|
+
};
|
|
75
|
+
const pieceDrawn = ({ skin, slot, revision, ok }) => {
|
|
76
|
+
if (disposed || piecesOf(skin)[slot]?.revision !== revision) return;
|
|
77
|
+
const key = pieceKey(skin, slot), was = pieceShown.get(key);
|
|
78
|
+
if (was?.revision === revision && (!was.ok || was.ok === ok)) return;
|
|
79
|
+
pieceShown.set(key, { revision, ok });
|
|
80
|
+
if (!ok) for (const target of [renderer, pendingRenderer]) target?.setPieces(skin, piecesFor(skin));
|
|
81
|
+
worldSpot();
|
|
82
|
+
};
|
|
83
|
+
function worldStatus(skin) {
|
|
84
|
+
const world = controller.yardWorld(skin);
|
|
85
|
+
if (!world) return { state: 'original', text: 'Original' };
|
|
86
|
+
if (world.problem) return { state: 'failed', text: world.problem + ' Showing the original.' };
|
|
87
|
+
if (!customFor(skin)) return { state: 'failed', text: world.title + ' could not load. Showing the original.' };
|
|
88
|
+
return worldShown.get(skin) === world.revision ? { state: 'custom', text: 'Customized: ' + world.title } : { state: 'loading', text: 'Loading ' + world.title + '…' };
|
|
89
|
+
}
|
|
90
|
+
function pieceStatus(skin, slot) {
|
|
91
|
+
const piece = piecesOf(skin)[slot], kind = pieceKind(slot);
|
|
92
|
+
if (!piece) return { state: 'original', text: 'Original' };
|
|
93
|
+
if (piece.problem) return { state: 'failed', text: piece.problem + ' Showing the original ' + kind + '.' };
|
|
94
|
+
const shown = pieceShown.get(pieceKey(skin, slot));
|
|
95
|
+
if (shown?.revision !== piece.revision) return { state: 'loading', text: 'Loading ' + piece.title + '…' };
|
|
96
|
+
return shown.ok ? { state: 'custom', text: 'Customized: ' + piece.title } : { state: 'failed', text: piece.title + ' could not load. Showing the original ' + kind + '.' };
|
|
97
|
+
}
|
|
98
|
+
const slotName = (slot) => slot === 'world' ? 'World' : slot === 'companion' ? 'Companions' : snapshot.providers.find(p => p.id === slot)?.tool || CREWS[slot];
|
|
99
|
+
const customizeRows = new Map();
|
|
100
|
+
function customizeRow(slot) {
|
|
101
|
+
if (customizeRows.has(slot)) return customizeRows.get(slot);
|
|
102
|
+
const row = make('li', 'flag-row yard-customize-row'), copy = make('div', 'yard-customize-copy'), actions = make('div', 'yard-customize-actions');
|
|
103
|
+
const name = make('span', 'flag-name'), status = make('span', 'yard-customize-status'), note = make('span', 'yard-customize-note');
|
|
104
|
+
const replace = make('button', 'btn', 'Replace'), restore = make('button', 'btn', 'Restore original');
|
|
105
|
+
replace.type = restore.type = 'button';
|
|
106
|
+
note.hidden = slot === 'world';
|
|
107
|
+
const skin = () => root.dataset.skin;
|
|
108
|
+
replace.addEventListener('click', () => slot === 'world' ? controller.replaceYard(skin()) : controller.replacePiece(skin(), slot));
|
|
109
|
+
restore.addEventListener('click', () => slot === 'world' ? controller.restoreYard(skin()) : controller.restorePiece(skin(), slot));
|
|
110
|
+
copy.append(name, status, note);
|
|
111
|
+
actions.append(replace, restore);
|
|
112
|
+
row.append(copy, actions);
|
|
113
|
+
row.dataset.slot = slot;
|
|
114
|
+
const parts = { row, name, status, note, replace, restore };
|
|
115
|
+
customizeRows.set(slot, parts);
|
|
116
|
+
return parts;
|
|
117
|
+
}
|
|
118
|
+
function paintCustomize() {
|
|
119
|
+
const dialog = $('yard-customize'), skin = root.dataset.skin;
|
|
120
|
+
if (!dialog.open) return;
|
|
121
|
+
if (!WORLDS[skin]) { dialog.close(); return; }
|
|
122
|
+
$('yard-customize-sub').textContent = WORLDS[skin].title + ' only. Other Yards keep their own.';
|
|
123
|
+
const busy = Boolean(controller.yardBusy?.()) || !snapshot.connected;
|
|
124
|
+
const heading = (text) => {
|
|
125
|
+
if (!customizeRows.has(text)) { const item = make('li', 'yard-customize-heading', text); item.setAttribute('role', 'presentation'); customizeRows.set(text, item); }
|
|
126
|
+
return customizeRows.get(text);
|
|
127
|
+
};
|
|
128
|
+
const items = [];
|
|
129
|
+
for (const [title, slots] of [['Yard', ['world']], ['Characters', PROVIDER_ORDER], ['Companions', ['companion']]]) {
|
|
130
|
+
items.push(heading(title));
|
|
131
|
+
for (const slot of slots) {
|
|
132
|
+
const parts = customizeRow(slot), status = controller.yardChecking?.() === pieceKey(skin, slot) ? { state: 'loading', text: 'Checking replacement…' } : slot === 'world' ? worldStatus(skin) : pieceStatus(skin, slot);
|
|
133
|
+
const installed = slot === 'world' ? Boolean(controller.yardWorld(skin)) : Boolean(piecesOf(skin)[slot]);
|
|
134
|
+
parts.name.textContent = slotName(slot);
|
|
135
|
+
parts.status.textContent = status.text;
|
|
136
|
+
if (slot !== 'world') parts.note.textContent = slot === 'companion' ? 'Shows beside sessions with helpers in this Yard.'
|
|
137
|
+
: 'Shows on your ' + slotName(slot) + ' sessions in this Yard.';
|
|
138
|
+
parts.row.dataset.state = status.state;
|
|
139
|
+
parts.replace.disabled = busy;
|
|
140
|
+
parts.restore.disabled = busy || !installed;
|
|
141
|
+
parts.replace.setAttribute('aria-label', 'Replace ' + slotName(slot));
|
|
142
|
+
parts.restore.setAttribute('aria-label', 'Restore the original ' + slotName(slot));
|
|
143
|
+
items.push(parts.row);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const list = $('yard-customize-list');
|
|
147
|
+
if (items.some((item, i) => list.children[i] !== item) || list.children.length !== items.length) {
|
|
148
|
+
const focused = list.contains(document.activeElement) ? document.activeElement : null;
|
|
149
|
+
list.replaceChildren(...items);
|
|
150
|
+
focused?.focus();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
49
153
|
function worldSpot() {
|
|
50
154
|
const skin = root.dataset.skin;
|
|
51
155
|
if (!WORLDS[skin]) return;
|
|
@@ -53,11 +157,11 @@ export function initYard(controller) {
|
|
|
53
157
|
$('yard-title').textContent = titleFor(skin);
|
|
54
158
|
$('yard-title').classList.toggle('yard-title-custom', Boolean(customFor(skin)));
|
|
55
159
|
const reason = world?.problem || (world && !customFor(skin) ? world.title + ' could not load.' : '');
|
|
56
|
-
|
|
57
|
-
$('yard-
|
|
58
|
-
$('yard-world-
|
|
59
|
-
$('yard-world-note').
|
|
60
|
-
|
|
160
|
+
const failed = !reason && Object.keys(piecesOf(skin)).map(slot => pieceStatus(skin, slot)).find(status => status.state === 'failed');
|
|
161
|
+
$('yard-customize-open').hidden = !controller.canReplaceYard();
|
|
162
|
+
$('yard-world-note').hidden = !reason && !failed;
|
|
163
|
+
$('yard-world-note').textContent = reason ? reason + ' This Yard is showing its original.' : failed ? failed.text : '';
|
|
164
|
+
paintCustomize();
|
|
61
165
|
}
|
|
62
166
|
const celebrate = () => { if (!disposed && sceneVisible()) renderer?.fireworks(); };
|
|
63
167
|
const fireworksButton = () => {
|
|
@@ -96,7 +200,7 @@ export function initYard(controller) {
|
|
|
96
200
|
if (allowed) yardChoice.parentElement.removeAttribute('title');
|
|
97
201
|
else yardChoice.parentElement.title = `${skinEntry()?.name || 'This skin'} has no yard yet.`;
|
|
98
202
|
$('yard').hidden = view !== 'yard';
|
|
99
|
-
if (view !== 'yard') closePhoto();
|
|
203
|
+
if (view !== 'yard') { closePhoto(); $('yard-customize').close(); }
|
|
100
204
|
$('providers').hidden = view === 'yard';
|
|
101
205
|
$('providers').inert = view === 'yard';
|
|
102
206
|
for (const section of document.querySelectorAll('#app > section.sessions, #app > section.news')) {
|
|
@@ -176,6 +280,7 @@ export function initYard(controller) {
|
|
|
176
280
|
select: next => select(next),
|
|
177
281
|
open: id => controller.openSession(id),
|
|
178
282
|
error: () => { if (attempt === sceneAttempt) failScene(); },
|
|
283
|
+
piece: pieceDrawn,
|
|
179
284
|
});
|
|
180
285
|
pendingRenderer = candidate;
|
|
181
286
|
// Appearance can change while the first world is loading.
|
|
@@ -184,10 +289,12 @@ export function initYard(controller) {
|
|
|
184
289
|
skin = root.dataset.skin; theme = root.dataset.theme;
|
|
185
290
|
if (!WORLDS[skin]) { candidate.dispose(); return; }
|
|
186
291
|
revision = customFor(skin)?.revision ?? null;
|
|
292
|
+
syncPieces(candidate);
|
|
187
293
|
showLoading('Loading ' + titleFor(skin) + '…', 'scene');
|
|
188
294
|
await boundedLoad(progress => loadWorld(candidate, skin, theme, progress), loadingProgress);
|
|
189
295
|
} while (!disposed && !candidate.disposed && attempt === sceneAttempt && (skin !== root.dataset.skin || theme !== root.dataset.theme || revision !== (customFor(skin)?.revision ?? null)));
|
|
190
296
|
if (disposed || candidate.disposed || attempt !== sceneAttempt) { candidate.dispose(); return; }
|
|
297
|
+
syncPieces(candidate);
|
|
191
298
|
pendingRenderer = null;
|
|
192
299
|
renderer = candidate;
|
|
193
300
|
lastSkin = skin; lastTheme = theme; lastRevision = revision;
|
|
@@ -318,9 +425,10 @@ export function initYard(controller) {
|
|
|
318
425
|
lastSkin = root.dataset.skin; lastTheme = root.dataset.theme; lastRevision = revision;
|
|
319
426
|
swapWorld();
|
|
320
427
|
}
|
|
428
|
+
syncPieces(renderer);
|
|
321
429
|
renderer.update(snapshot.providers,snapshot.sessions);
|
|
322
430
|
renderer.setActive(sceneVisible());
|
|
323
|
-
} else ensureScene();
|
|
431
|
+
} else { syncPieces(pendingRenderer); ensureScene(); }
|
|
324
432
|
}
|
|
325
433
|
$('view-cards').addEventListener('change',() => setView('cards'));
|
|
326
434
|
$('view-yard').addEventListener('change',() => { if (skinAllowsYard()) setView('yard'); });
|
|
@@ -342,8 +450,12 @@ export function initYard(controller) {
|
|
|
342
450
|
lastSkin = ''; lastTheme = '';
|
|
343
451
|
update();
|
|
344
452
|
});
|
|
345
|
-
$('yard-
|
|
346
|
-
|
|
453
|
+
$('yard-customize-open').addEventListener('click',() => {
|
|
454
|
+
if (!$('yard-customize').open) $('yard-customize').showModal();
|
|
455
|
+
paintCustomize();
|
|
456
|
+
});
|
|
457
|
+
$('yard-customize-close').addEventListener('click',() => $('yard-customize').close());
|
|
458
|
+
$('yard-customize').addEventListener('click',event => { if (event.target === $('yard-customize')) $('yard-customize').close(); });
|
|
347
459
|
$('yard-filter').addEventListener('input',roster);
|
|
348
460
|
$('yard-news').addEventListener('click',controller.openNews);
|
|
349
461
|
$('yard-deselect').addEventListener('click',() => { select(null); $('yard-filter').focus(); });
|