@adatechnology/game-test-room 0.19.0 → 0.19.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/README.md CHANGED
@@ -211,9 +211,9 @@ Every frame the room puts `root` at the player's feet turned to where the player
211
211
 
212
212
  **Pushing furniture.** Against a piece that stands on the floor, with bare hands, a tap on E grabs as ever and holding E fills a ring around the crosshair (longer the heavier). Then W pushes, S pulls, slower with the mass; letting go ends it. Before each step the volume of the piece is swept (`world.sweepBox`) against everything and the arena: at an obstacle the piece and the player stop and the player strains, and a piece too heavy for the player (`RoomOptions.push`, `FurniturePushRules` of game-player) never moves. A piece lower than a table is pushed from above, bent over it. Scripted: `room.push`. Try it: `?scene=push` in the example.
213
213
 
214
- **Opening by hand.** A tap of E on a leaf (a part an `open_*` clip of the item moves: furniture doors, drawers and lids, vehicle doors, hood and trunk) opens it, and E again closes it; looking into what an open leaf opened, E closes that leaf. The player steps up to the handle, the hand goes to the free edge of the leaf (a drawer by its front) and follows it, pulling (the arm bends, the body leans back) or pushing; a door that swings into the player makes the player step back. The leaf swings along the item's own clip at a speed set by its mass, its sweep tested against the world (`world.sweepBox`) and the arena: it stops at what is in its way with a knock. A locked leaf (`room.lockLeaf`) only rattles. Latch, hinge creak, the leaf meeting the frame and the rattle play by material. Holding E still pushes the piece. Scripted: `room.useLeaf`, `room.leafOpening`, `room.leaves`, `room.door`; the numbers are `RoomOptions.doors` (`DoorUseRules` of game-player). A module that moves its own leaves (the doors of a house) asks for the same hand and body with `host.reachFor`. Try it: `?scene=doors` in the example.
214
+ **Opening by hand.** A tap of E on a leaf (a part an `open_*` clip of the item moves: furniture doors, drawers and lids, vehicle doors, hood and trunk) opens it, and E again closes it; looking into what an open leaf opened, E closes that leaf. The player steps up to the handle, the body's hand goes to the free edge of the leaf (a drawer by its front) through the same `reach` as a held tool, in both views, and follows it, pulling (the arm bends, the body leans back) or pushing; a door that swings into the player makes the player step back. The leaf swings along the item's own clip at a speed set by its mass, its sweep tested against the world (`world.sweepBox`) and the arena: it stops at what is in its way with a knock. A locked leaf (`room.lockLeaf`) only rattles. Latch, hinge creak, the leaf meeting the frame and the rattle play by material. Holding E still pushes the piece. Scripted: `room.useLeaf`, `room.leafOpening`, `room.leaves`, `room.door`; the numbers are `RoomOptions.doors` (`DoorUseRules` of game-player). A module that moves its own leaves (the doors of a house) asks for the same hand and body with `host.reachFor`. Try it: `?scene=doors` in the example.
215
215
 
216
- **First-person arms and strikes.** An avatar may carry `firstPerson` (arms drawn in the camera's space; the room shows them only in the first person, updates them every frame after the modules' frame hooks and hands them where the tool in the hands asks the hands to be) and `strike(clipId)`. The hand targets come from the module that places the tool (`host.setHandTargets(source)`, `HandTargets` of `@adatechnology/game-contracts`: world-space poses of the right and left hand plus the grip closure and the size of the hands), else from the carried item, else null; the stance is `hold` when there are targets, else `handStances[toolId]` (default `{ fists: 'fists' }`) or `relaxed`. A melee strike (`fists`, `shove`, Shift+V kick) no longer lands at the click: the room calls `avatar.strike(strikeClips[toolId])` (default `{ fists: 'punch_right', shove: 'shove_two_hands', kick: 'front_kick' }`), which plays the clip on the arms and on the body and answers the seconds to its impact, writes a `strike` line to the log, and calls the existing `meleeUse` at that second with the aim read again. The left click with bare hands (`hand`, the default tool) punches too (E or the right button grab). Punches alternate hands, right first (`punchHand`: `alternate`, `left`, `right`; `punchTools`, default `['hand', 'fists']`): the room calls `strike(clip, { hand })` and logs the hand, as the kick does with `kickLeg` and `{ leg }`. A **strike combo** is an ability the page gives (`strikeCombo`, such as `PUNCH_COMBO` of `game-characters-people`: jab, cross, hook, uppercut): while it is on (N, its button, `setStrikeCombo(true)`, `strikeComboOn`) each blow of its tools plays its next step (`{ clip, hand }`) and a pause longer than its `reset` starts it over; off, the tools strike as usual. The strike line carries `combo` and `step`. A tool without a clip, or an avatar that answers null, keeps the instant blow; a strike pending blocks the same tool. `toRoomAvatar(createPlayerAvatar(THREE))` already gives all of it (`firstPerson: false` for no arms).
216
+ **Hands and strikes.** The body is the only set of arms, in both views: the room hands its hands where the tool in them asks them to be (`reach`, every frame after the modules' frame hooks) and an avatar may answer `strike(clipId)`. The hand targets come from the module that places the tool (`host.setHandTargets(source)`, `HandTargets` of `@adatechnology/game-contracts`: world-space poses of the right and left hand plus the grip closure and the size of the hands), else from the carried item, else null; the stance is `hold` when there are targets, else `handStances[toolId]` (default `{ fists: 'fists' }`) or `relaxed`. A melee strike (`fists`, `shove`, Shift+V kick) no longer lands at the click: the room calls `avatar.strike(strikeClips[toolId])` (default `{ fists: 'punch_right', shove: 'shove_two_hands', kick: 'front_kick' }`), which plays the clip on the body (in both views) and answers the seconds to its impact, writes a `strike` line to the log, and calls the existing `meleeUse` at that second with the aim read again. The left click with bare hands (`hand`, the default tool) punches too (E or the right button grab). Punches alternate hands, right first (`punchHand`: `alternate`, `left`, `right`; `punchTools`, default `['hand', 'fists']`): the room calls `strike(clip, { hand })` and logs the hand, as the kick does with `kickLeg` and `{ leg }`. A **strike combo** is an ability the page gives (`strikeCombo`, such as `PUNCH_COMBO` of `game-characters-people`: jab, cross, hook, uppercut): while it is on (N, its button, `setStrikeCombo(true)`, `strikeComboOn`) each blow of its tools plays its next step (`{ clip, hand }`) and a pause longer than its `reset` starts it over; off, the tools strike as usual. The strike line carries `combo` and `step`. A tool without a clip, or an avatar that answers null, keeps the instant blow; a strike pending blocks the same tool. `toRoomAvatar(createPlayerAvatar(THREE))` already gives all of it .
217
217
 
218
218
  `room.setCameraMode('first' | 'third')` (option `cameraMode`, property `cameraMode`, event `room.on('camera', fn)`; every change is a `camera` action in the log). V toggles it when a body is set. In `first` the body is hidden; in `third` it is shown, the camera stands over the shoulder, and a carried item rides `avatar.hand('R')` instead of the camera frame. Option `thirdPerson` changes the view; the defaults are the team game's shoulder camera (`zombiada-modo-livre.html`, `cameraUpdate`, line 76546): `back` 3.8 m, `side` 0.9 m to the right, `up` 1.95 m (`crouchUp` 1.4 m), `lift` 0.1 m, three probe rays (`probes` 0 and +-0.2 m) that pull the camera in `margin` 0.3 m short of a wall (never closer than `minDistance` 0.2 m) and let it out at `recover` 4 per second.
219
219
 
package/dist/index.d.ts CHANGED
@@ -430,7 +430,7 @@ interface RoomAvatar {
430
430
  /** Hides the head and neck from `camera` while the view is `first` (shadows, mirrors and other cameras keep seeing them). */
431
431
  setView?(mode: CameraMode, camera: THREE_NS.Object3D | null): void;
432
432
  /**
433
- * Plays the strike `clipId` (on the body and on the arms) and answers the seconds until its impact, or null when this avatar has no such clip. The room then waits that long
433
+ * Plays the strike `clipId` (on the body, in both views) and answers the seconds until its impact, or null when this avatar has no such clip. The room then waits that long
434
434
  * before the blow lands (`strikeClips`); an avatar without `strike`, or one that answers null, gets the blow at the click.
435
435
  */
436
436
  strike?(clipId: string, options?: {
@@ -3150,6 +3150,7 @@ var GameTestRoom = (() => {
3150
3150
  var CHIP_ENERGY = 200;
3151
3151
  var CHIP_SPLASH = 0.5;
3152
3152
  var CRATER_SPENT = 0.6;
3153
+ var EDGE_REACH = 1.5;
3153
3154
  var BAR_BOW = 0.08;
3154
3155
  var BULLET_JOINT_SPAN = 0.3;
3155
3156
  var CUT_SLICE = 1;
@@ -4523,6 +4524,9 @@ var GameTestRoom = (() => {
4523
4524
  const lo = cell.center[a] - cell.size[a] / 2 + w / 2, hi = cell.center[a] + cell.size[a] / 2 - w / 2;
4524
4525
  size[a] = w;
4525
4526
  center[a] = clamp2(lp[a], lo, hi);
4527
+ const [from, to] = tornToEdge({ p, cell, a, b: a === u ? v : u, at: lp[a], crater: [center[a] - w / 2, center[a] + w / 2], reach: w * EDGE_REACH, gone: (k) => worn[k] === -1 });
4528
+ size[a] = to - from;
4529
+ center[a] = (from + to) / 2;
4526
4530
  }
4527
4531
  const taken2 = (cut2[i] ?? 0) + size[u] * size[v];
4528
4532
  if (taken2 >= cell.size[u] * cell.size[v] * CRATER_SPENT) {
@@ -4560,6 +4564,19 @@ var GameTestRoom = (() => {
4560
4564
  collapseCheck(o, src);
4561
4565
  }
4562
4566
  };
4567
+ const tornToEdge = (o) => {
4568
+ const { p, cell, a, b, at: at3, crater: crater2, reach, gone } = o;
4569
+ const half = p.dims[a] / 2;
4570
+ const lo = cell.center[a] - cell.size[a] / 2, hi = cell.center[a] + cell.size[a] / 2;
4571
+ const free = (side, sign) => {
4572
+ if (sign > 0 ? side >= half - 1e-6 : side <= -half + 1e-6) return sign > 0 || a !== planeAxes(p.thin)[1];
4573
+ const probe = side + sign * 1e-3;
4574
+ return gridOf(p).some((c, k) => gone(k) && Math.abs(probe - c.center[a]) <= c.size[a] / 2 && Math.abs(cell.center[b] - c.center[b]) <= c.size[b] / 2);
4575
+ };
4576
+ if (at3 - lo <= reach && free(lo, -1)) return [lo, Math.max(crater2[1], lo)];
4577
+ if (hi - at3 <= reach && free(hi, 1)) return [Math.min(crater2[0], hi), hi];
4578
+ return crater2;
4579
+ };
4563
4580
  const craterAcross = (energy) => clamp2(0.03 + 12e-4 * Math.sqrt(Math.max(0, energy)), 0.03, 0.14);
4564
4581
  const craterFragments = (o, p, cell, dir, speed, mass, tag) => {
4565
4582
  const { c, q } = partXf(o, p);
@@ -7524,7 +7541,7 @@ var GameTestRoom = (() => {
7524
7541
  const registry = createSoundRegistry(options.banks);
7525
7542
  const voices = createVoiceManager({ ...options.groups ? { groups: options.groups } : {}, ...options.maxVoices === void 0 ? {} : { maxVoices: options.maxVoices } });
7526
7543
  const engineRandom = createRng2(seed);
7527
- const shared = createNoiseStore(engineRandom);
7544
+ const shared2 = createNoiseStore(engineRandom);
7528
7545
  const busSettings = /* @__PURE__ */ new Map();
7529
7546
  const busNodes = /* @__PURE__ */ new Map();
7530
7547
  const meters = /* @__PURE__ */ new Map();
@@ -7563,7 +7580,7 @@ var GameTestRoom = (() => {
7563
7580
  if (!created) return null;
7564
7581
  context = created;
7565
7582
  chain = buildChain(created, mix, engineRandom);
7566
- if (mix.noise.create === "eager") noiseBuffer(shared, created, mix.noise.seconds);
7583
+ if (mix.noise.create === "eager") noiseBuffer(shared2, created, mix.noise.seconds);
7567
7584
  for (const [bus, setting] of Object.entries(mix.buses ?? {})) {
7568
7585
  busSettings.set(bus, { ...setting, ...busSettings.get(bus) });
7569
7586
  }
@@ -7682,8 +7699,8 @@ var GameTestRoom = (() => {
7682
7699
  const loopHost = {
7683
7700
  ready,
7684
7701
  output: outputFor,
7685
- prepareNoise: () => context && prepareNoise(shared, context, mix.noise.seconds),
7686
- noise: () => noiseBuffer(shared, context, mix.noise.seconds),
7702
+ prepareNoise: () => context && prepareNoise(shared2, context, mix.noise.seconds),
7703
+ noise: () => noiseBuffer(shared2, context, mix.noise.seconds),
7687
7704
  register: (loop) => void loops.add(loop),
7688
7705
  unregister: (loop) => void loops.delete(loop)
7689
7706
  };
@@ -7691,7 +7708,7 @@ var GameTestRoom = (() => {
7691
7708
  registry,
7692
7709
  register: (bank) => registry.register(bank),
7693
7710
  plan: (id, playOptions = {}) => planWith(id, playOptions, createRng2(playOptions.seed ?? seed)),
7694
- play: (id, playOptions = {}) => playWith(id, playOptions, randomFor(playOptions), shared),
7711
+ play: (id, playOptions = {}) => playWith(id, playOptions, randomFor(playOptions), shared2),
7695
7712
  playPlan(plan, playOptions = {}) {
7696
7713
  const audio = ready();
7697
7714
  if (!audio) return null;
@@ -7699,13 +7716,13 @@ var GameTestRoom = (() => {
7699
7716
  const recipe = { id: plan.id, bus: plan.bus, layers: [], sources: [], priority: plan.priority, ...plan.group ? { group: plan.group } : {} };
7700
7717
  const voiceId = plan.silent ? void 0 : admit(recipe, plan.level, plan.distance);
7701
7718
  if (voiceId === null) return null;
7702
- if (planUsesNoise(plan)) prepareNoise(shared, audio, mix.noise.seconds);
7703
- return start(plan, shared, voiceId, playOptions.delay);
7719
+ if (planUsesNoise(plan)) prepareNoise(shared2, audio, mix.noise.seconds);
7720
+ return start(plan, shared2, voiceId, playOptions.delay);
7704
7721
  },
7705
7722
  loop: (id, loopOptions = {}) => createLoopHandle(registry.loop(id), loopHost, loopOptions.volume),
7706
7723
  channel(channelOptions) {
7707
7724
  const random = channelOptions.random ?? (channelOptions.seed === void 0 ? engineRandom : createRng2(channelOptions.seed));
7708
- const store = channelOptions.ownNoise ? createNoiseStore(random) : shared;
7725
+ const store = channelOptions.ownNoise ? createNoiseStore(random) : shared2;
7709
7726
  return {
7710
7727
  random,
7711
7728
  play: (id, playOptions = {}) => playWith(id, playOptions, random, store),
@@ -7727,7 +7744,7 @@ var GameTestRoom = (() => {
7727
7744
  if (!context || !chain) return;
7728
7745
  const old = chain;
7729
7746
  chain = buildChain(context, mix, engineRandom);
7730
- if (mix.noise.seconds !== previous.noise.seconds) shared.reset();
7747
+ if (mix.noise.seconds !== previous.noise.seconds) shared2.reset();
7731
7748
  for (const node of busNodes.values()) {
7732
7749
  node.disconnect();
7733
7750
  node.connect(chain.master);
@@ -7746,7 +7763,7 @@ var GameTestRoom = (() => {
7746
7763
  warmUp() {
7747
7764
  const audio = ready();
7748
7765
  if (!audio) return false;
7749
- noiseBuffer(shared, audio, mix.noise.seconds);
7766
+ noiseBuffer(shared2, audio, mix.noise.seconds);
7750
7767
  return true;
7751
7768
  },
7752
7769
  level(bus) {
@@ -7781,7 +7798,7 @@ var GameTestRoom = (() => {
7781
7798
  chain = null;
7782
7799
  busNodes.clear();
7783
7800
  meters.clear();
7784
- shared.reset();
7801
+ shared2.reset();
7785
7802
  }
7786
7803
  };
7787
7804
  }
@@ -12588,12 +12605,12 @@ var GameTestRoom = (() => {
12588
12605
  }
12589
12606
  function drawing(entry, action) {
12590
12607
  const context = engine.context;
12591
- const shared = context.random;
12608
+ const shared2 = context.random;
12592
12609
  if (entry.random) context.random = entry.random;
12593
12610
  try {
12594
12611
  action();
12595
12612
  } finally {
12596
- context.random = shared;
12613
+ context.random = shared2;
12597
12614
  }
12598
12615
  }
12599
12616
  return {
@@ -40280,6 +40297,81 @@ void main(){
40280
40297
  return root;
40281
40298
  }
40282
40299
 
40300
+ // ../game-shapes-three/dist/index.js
40301
+ var isTexture = (v) => !!v && typeof v === "object" && v.isTexture === true;
40302
+ function texturesOf(material, out) {
40303
+ for (const value of Object.values(material)) if (isTexture(value)) out.add(value);
40304
+ const uniforms = material.uniforms;
40305
+ if (uniforms) {
40306
+ for (const u of Object.values(uniforms)) if (isTexture(u?.value)) out.add(u.value);
40307
+ }
40308
+ }
40309
+ function collect(roots) {
40310
+ const geometries = /* @__PURE__ */ new Set();
40311
+ const materials = /* @__PURE__ */ new Set();
40312
+ const textures = /* @__PURE__ */ new Set();
40313
+ const skeletons = /* @__PURE__ */ new Set();
40314
+ for (const root of roots) {
40315
+ root.traverse((object) => {
40316
+ const o = object;
40317
+ if (o.geometry && typeof o.geometry.dispose === "function") geometries.add(o.geometry);
40318
+ if (o.skeleton && typeof o.skeleton.dispose === "function") skeletons.add(o.skeleton);
40319
+ const list = Array.isArray(o.material) ? o.material : o.material ? [o.material] : [];
40320
+ for (const m of list) {
40321
+ if (!m || typeof m.dispose !== "function") continue;
40322
+ materials.add(m);
40323
+ texturesOf(m, textures);
40324
+ }
40325
+ });
40326
+ }
40327
+ return { geometries, materials, textures, skeletons };
40328
+ }
40329
+ var marked = /* @__PURE__ */ new WeakSet();
40330
+ var shared = (r) => r.userData?.shared === true || marked.has(r);
40331
+ function releaseObject3D(roots, options = {}) {
40332
+ const list = Array.isArray(roots) ? roots : [roots];
40333
+ const report = { geometries: 0, materials: 0, textures: 0 };
40334
+ if (list.length === 0) return report;
40335
+ const found = collect(list);
40336
+ const keepRoots = options.keep === void 0 ? [] : Array.isArray(options.keep) ? options.keep : [options.keep];
40337
+ const releasing = new Set(list);
40338
+ for (const keep of keepRoots) {
40339
+ const walk = (o) => {
40340
+ if (releasing.has(o)) return;
40341
+ const owner = o;
40342
+ if (owner.geometry) found.geometries.delete(owner.geometry);
40343
+ if (owner.skeleton) found.skeletons.delete(owner.skeleton);
40344
+ const mats = Array.isArray(owner.material) ? owner.material : owner.material ? [owner.material] : [];
40345
+ for (const m of mats) {
40346
+ if (!m) continue;
40347
+ found.materials.delete(m);
40348
+ const t = /* @__PURE__ */ new Set();
40349
+ texturesOf(m, t);
40350
+ for (const x of t) found.textures.delete(x);
40351
+ }
40352
+ for (const c of o.children) walk(c);
40353
+ };
40354
+ walk(keep);
40355
+ }
40356
+ for (const g of found.geometries) {
40357
+ if (shared(g)) continue;
40358
+ g.dispose();
40359
+ report.geometries++;
40360
+ }
40361
+ for (const m of found.materials) {
40362
+ if (shared(m)) continue;
40363
+ m.dispose();
40364
+ report.materials++;
40365
+ }
40366
+ for (const t of found.textures) {
40367
+ if (shared(t)) continue;
40368
+ t.dispose();
40369
+ report.textures++;
40370
+ }
40371
+ for (const s of found.skeletons) s.dispose();
40372
+ return report;
40373
+ }
40374
+
40283
40375
  // src/segment-box.ts
40284
40376
  var segmentTouchesBox = (from, to, min2, max2) => {
40285
40377
  let near = 0;
@@ -40320,9 +40412,13 @@ void main(){
40320
40412
  const projectiles = [];
40321
40413
  const meshes = [];
40322
40414
  const up = new THREE.Vector3(0, 1, 0);
40415
+ const drop = (objects) => {
40416
+ for (const object of objects) object.removeFromParent();
40417
+ releaseObject3D(objects, { keep: scene2 });
40418
+ };
40323
40419
  const clear = () => {
40324
40420
  projectiles.length = 0;
40325
- for (const mesh2 of meshes) mesh2.removeFromParent();
40421
+ drop(meshes);
40326
40422
  meshes.length = 0;
40327
40423
  };
40328
40424
  const addMesh = (geometry, color, metalness, at3) => {
@@ -40346,13 +40442,12 @@ void main(){
40346
40442
  };
40347
40443
  const remove = (projectile) => {
40348
40444
  for (const extra of projectile.attached ?? []) {
40349
- extra.removeFromParent();
40350
40445
  const extraAt = meshes.indexOf(extra);
40351
40446
  if (extraAt >= 0) meshes.splice(extraAt, 1);
40352
40447
  }
40353
- projectile.mesh.removeFromParent();
40354
40448
  const meshAt = meshes.indexOf(projectile.mesh);
40355
40449
  if (meshAt >= 0) meshes.splice(meshAt, 1);
40450
+ drop([...projectile.attached ?? [], projectile.mesh]);
40356
40451
  const at3 = projectiles.indexOf(projectile);
40357
40452
  if (at3 >= 0) projectiles.splice(at3, 1);
40358
40453
  };
@@ -47207,6 +47302,23 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
47207
47302
  }
47208
47303
  return m;
47209
47304
  };
47305
+ let releaseBatch = null;
47306
+ const dropTree = (root) => {
47307
+ if (!root) return;
47308
+ if (releaseBatch) releaseBatch.push(root);
47309
+ else releaseObject3D(root, { keep: scene2 });
47310
+ };
47311
+ const releasingTogether = (fn) => {
47312
+ if (releaseBatch) return fn();
47313
+ releaseBatch = [];
47314
+ try {
47315
+ fn();
47316
+ } finally {
47317
+ const roots = releaseBatch;
47318
+ releaseBatch = null;
47319
+ releaseObject3D(roots, { keep: scene2 });
47320
+ }
47321
+ };
47210
47322
  const removeEntry = (id) => {
47211
47323
  const e = entries.get(id);
47212
47324
  if (!e) return;
@@ -47223,7 +47335,10 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
47223
47335
  e.follower?.dispose?.();
47224
47336
  if (world.has(id)) world.remove(id);
47225
47337
  e.so.root.removeFromParent();
47338
+ const inner = wrapped.get(e.so.root);
47226
47339
  letGo(e.so.root);
47340
+ dropTree(e.so.root);
47341
+ dropTree(inner);
47227
47342
  entries.delete(id);
47228
47343
  doors.forget(id);
47229
47344
  rendererPieces.delete(id);
@@ -47239,12 +47354,14 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
47239
47354
  }
47240
47355
  if (world.has(id)) world.remove(id);
47241
47356
  p?.root.removeFromParent();
47357
+ dropTree(p?.root);
47242
47358
  rendererPieces.delete(id);
47243
47359
  splitOf.delete(id);
47244
47360
  }
47245
47361
  loose.length = 0;
47246
47362
  };
47247
- const clearEverything = () => {
47363
+ const clearEverything = () => releasingTogether(clearEverythingNow);
47364
+ const clearEverythingNow = () => {
47248
47365
  if (heldEntry || heldPiece) releaseHeld();
47249
47366
  useItemPlayer = null;
47250
47367
  playing.length = 0;
@@ -47293,8 +47410,10 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
47293
47410
  seating.release();
47294
47411
  pushByKey = false;
47295
47412
  push.end();
47296
- clearEverything();
47297
- for (const id of [...entries.keys()]) removeEntry(id);
47413
+ releasingTogether(() => {
47414
+ clearEverythingNow();
47415
+ for (const id of [...entries.keys()]) removeEntry(id);
47416
+ });
47298
47417
  renderer.reset();
47299
47418
  for (const listener of [...resetListeners]) listener();
47300
47419
  tools.clear();
@@ -47447,11 +47566,11 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
47447
47566
  };
47448
47567
  const reachAvatar = () => {
47449
47568
  const doorHand = doors.handTargets();
47450
- const targets = push.active ? null : heldPiece ? twistHand : handSource?.() ?? null;
47569
+ const targets = push.active ? null : doorHand ?? (heldPiece ? twistHand : handSource?.() ?? null);
47451
47570
  lastHand = {
47452
47571
  stance: push.active || doorHand ? "hold" : handStances[toolId] ?? (targets ? "hold" : "relaxed"),
47453
47572
  triggerFinger: targets?.fingers?.index ?? null,
47454
- targets: push.handTargets() ?? doorHand ?? targets
47573
+ targets: push.handTargets() ?? targets
47455
47574
  };
47456
47575
  if (!avatar?.reach) return;
47457
47576
  avatar.reach(targets, { camera: camera2, keepOutside: { contact: (origin, direction, maxDistance) => rayWorld(origin, direction, maxDistance)?.distance ?? null } });