@adatechnology/game-test-room 0.22.0 → 0.23.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/dist/index.d.ts CHANGED
@@ -406,6 +406,8 @@ interface RoomAvatarState {
406
406
  running: boolean;
407
407
  crouch: boolean;
408
408
  down: boolean;
409
+ /** Which way the body falls while `down`: true face down, false on its back; left out, the avatar picks. A blast falls it the way it pushed. */
410
+ fallForward?: boolean;
409
411
  /** A firearm is the tool in hand. */
410
412
  armed: boolean;
411
413
  /** Camera pitch, radians, positive looking up (the room's own convention). */
@@ -23543,7 +23543,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
23543
23543
  this.startFire(ev);
23544
23544
  this.activate(ev.object, ev.part);
23545
23545
  this.sound("ignite", ev.point, 0.7);
23546
- this.sound("burn_loop", ev.point, 0.5);
23546
+ this.sound("burn_loop", ev.point, 0.5, this.burning(ev.object, ev.part));
23547
23547
  break;
23548
23548
  case "primed":
23549
23549
  if (ev.trigger !== "explosion") {
@@ -23551,7 +23551,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
23551
23551
  this.primedBurning.add(`${ev.object}|${ev.part}`);
23552
23552
  this.activate(ev.object, ev.part);
23553
23553
  this.sound("primed", ev.point, 0.7, { seconds: ev.delay });
23554
- this.sound("burn_loop", ev.point, 0.5);
23554
+ this.sound("burn_loop", ev.point, 0.5, { fuel: "gas" });
23555
23555
  }
23556
23556
  break;
23557
23557
  case "spall":
@@ -23577,15 +23577,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
23577
23577
  this.stopFire(`${ev.object}|${ev.part}`);
23578
23578
  this.vfx.burst("steam", f.point, [0, 1, 0], 14, 1.2);
23579
23579
  }
23580
- if (f || primed) this.sound("burn_end", f?.point ?? this.rootPoint(ev.object), 0.5);
23580
+ if (f || primed) this.sound("burn_end", f?.point ?? this.rootPoint(ev.object), 0.5, primed ? { fuel: "gas" } : this.burning(ev.object, ev.part));
23581
23581
  this.activate(ev.object, ev.part);
23582
23582
  this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
23583
23583
  break;
23584
23584
  }
23585
23585
  case "burnout": {
23586
23586
  this.stopFire(`${ev.object}|${ev.part}`);
23587
- this.primedBurning.delete(`${ev.object}|${ev.part}`);
23588
- this.sound("burn_end", ev.point, 0.5);
23587
+ const primed = this.primedBurning.delete(`${ev.object}|${ev.part}`);
23588
+ this.sound("burn_end", ev.point, 0.5, primed ? { fuel: "gas" } : this.burning(ev.object, ev.part));
23589
23589
  this.vfx.burst("ash", ev.point, [0, 1, 0], 16, 1);
23590
23590
  this.vfx.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
23591
23591
  const pv = this.partVis(ev.object, ev.part);
@@ -23653,7 +23653,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
23653
23653
  break;
23654
23654
  }
23655
23655
  case "explode":
23656
- this.primedBurning.delete(`${ev.object}|${ev.part}`);
23656
+ if (this.primedBurning.delete(`${ev.object}|${ev.part}`)) this.sound("burn_end", ev.point, 0.5, { fuel: "gas" });
23657
23657
  this.onExplode(ev);
23658
23658
  break;
23659
23659
  case "destroyed":
@@ -23668,6 +23668,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
23668
23668
  if (detail) this.o.onSound?.(key2, point, volume, detail);
23669
23669
  else this.o.onSound?.(key2, point, volume);
23670
23670
  }
23671
+ /** What a burning part is made of, for the fire's sound. */
23672
+ burning(objectId, partId) {
23673
+ const material = this.partVis(objectId, partId)?.material || this.world.part(objectId, partId)?.material;
23674
+ return material ? { material } : void 0;
23675
+ }
23671
23676
  partVis(objectId, partId) {
23672
23677
  return this.bounds.get(objectId)?.parts.get(partId) ?? null;
23673
23678
  }
@@ -24536,6 +24541,47 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24536
24541
  volume: 0,
24537
24542
  sources: ["sdk: the roar under the crackles of a fire"]
24538
24543
  };
24544
+ var FIRE_FUEL_OF = {
24545
+ wood_soft: "wood",
24546
+ wood_hard: "wood",
24547
+ plywood: "wood",
24548
+ mdf: "wood",
24549
+ green_wood: "wood",
24550
+ foliage: "wood",
24551
+ cactus: "wood",
24552
+ paper: "fabric",
24553
+ fabric: "fabric",
24554
+ leather: "fabric",
24555
+ foam: "plastic",
24556
+ plastic: "plastic",
24557
+ rubber: "plastic",
24558
+ wax: "plastic"
24559
+ };
24560
+ var FIRE_LOOP_OF = {
24561
+ wood: "fire_loop",
24562
+ fabric: "fire_loop",
24563
+ plastic: "fire_loop_plastic",
24564
+ gas: "fire_loop_gas",
24565
+ liquid: "fire_loop_liquid"
24566
+ };
24567
+ var FIRE_CRACKLE_OF = { wood: 1, fabric: 0.5, plastic: 0.35, gas: 0, liquid: 0 };
24568
+ var roar = (id, bank, source) => ({
24569
+ id,
24570
+ bus: "world",
24571
+ order: "chain_loop",
24572
+ source: { kind: "noise", loop: true },
24573
+ filters: { mode: "serial", bank },
24574
+ volume: 0,
24575
+ sources: [source]
24576
+ });
24577
+ var FIRE_FUEL_LOOPS = [
24578
+ // a gas jet: a steady, broad roar, brighter than wood and without crackles
24579
+ roar("fire_loop_gas", [{ type: "highpass", frequency: { set: 220 }, q: 0.7 }, { type: "lowpass", frequency: { set: 2400 }, q: 0.6 }], "sdk: a gas jet burning (a vessel venting into the flame)"),
24580
+ // a fuel pool: a soft, low, rolling roar
24581
+ roar("fire_loop_liquid", [{ type: "highpass", frequency: { set: 90 }, q: 0.7 }, { type: "lowpass", frequency: { set: 650 }, q: 0.9 }], "sdk: a pool of fuel burning"),
24582
+ // plastic and rubber: a thin sizzle over a low burn
24583
+ roar("fire_loop_plastic", [{ type: "bandpass", frequency: { set: 1800 }, q: 0.6 }], "sdk: plastic and rubber melting in the fire")
24584
+ ];
24539
24585
  var BLAST_SOUND_OF = {
24540
24586
  grenade: "explosion_grenade",
24541
24587
  big: "explosion_big",
@@ -24680,7 +24726,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24680
24726
  impact2("plastic_crack", [sceneNoise({ type: "bandpass", f: 1800, q: 2, d: 0.06, vol: 0.75 }), sceneTone({ f: [450, 550], f2: 250, d: 0.05, vol: 0.22 })]),
24681
24727
  impact2("mdf_crumble", [sceneNoise({ type: "lowpass", f: 700, d: 0.25, vol: 0.75, wet: 0.2 }), ...sceneMany(4, () => sceneNoise({ type: "lowpass", f: 500, d: 0.05, vol: 0.32, delay: [0.05, 0.3] }))])
24682
24728
  ];
24683
- var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, ...IMPACT_SOUNDS, EXPLOSION_BOOM, ...BLAST_SOUNDS, ...FIRE_SOUNDS], loops: [BURN_LOOP, FIRE_LOOP] };
24729
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, ...IMPACT_SOUNDS, EXPLOSION_BOOM, ...BLAST_SOUNDS, ...FIRE_SOUNDS], loops: [BURN_LOOP, FIRE_LOOP, ...FIRE_FUEL_LOOPS] };
24684
24730
  var IMPACT_SOUND_KEYS = {
24685
24731
  impact_metal_ballistic: { sound: "metal_bullet" },
24686
24732
  impact_metal_blunt: { sound: "metal_blunt" },
@@ -24829,31 +24875,52 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24829
24875
  const rules = { ...DESTRUCTION_SOUND_KEYS.art_scene, ...options.style === "game" ? DESTRUCTION_SOUND_KEYS.game : {}, ...options.keys };
24830
24876
  const blastByKind = options.style !== "game" && !(options.keys && "explode" in options.keys);
24831
24877
  const channel = engine.channel({ seed: options.seed ?? 1 });
24832
- let loop;
24833
- let burning = 0;
24878
+ const fuelOf = { ...FIRE_FUEL_OF, ...options.fuels };
24879
+ const loopOf = { ...FIRE_LOOP_OF, ...options.fireLoop ? { wood: fireLoop, fabric: fireLoop } : {}, ...options.fireLoops };
24880
+ const defaultFuel = options.defaultFuel ?? "wood";
24881
+ const loops = /* @__PURE__ */ new Map();
24834
24882
  const fires = [];
24835
24883
  let nextCrackle = 0;
24884
+ const fuelFor = (detail) => detail?.fuel ?? (detail?.material !== void 0 ? fuelOf[detail.material] : void 0) ?? defaultFuel;
24836
24885
  const setFire = () => {
24837
24886
  if (options.burnLoop === false) return;
24838
- if (burning <= 0) {
24839
- loop?.stop();
24840
- loop = void 0;
24841
- return;
24887
+ const count = /* @__PURE__ */ new Map();
24888
+ for (const f of fires) {
24889
+ const id = loopOf[f.fuel];
24890
+ count.set(id, (count.get(id) ?? 0) + 1);
24891
+ }
24892
+ for (const [id, handle] of loops) {
24893
+ if (count.has(id)) continue;
24894
+ handle.stop();
24895
+ loops.delete(id);
24896
+ }
24897
+ for (const [id, n] of count) {
24898
+ let handle = loops.get(id);
24899
+ if (!handle) {
24900
+ handle = engine.loop(id);
24901
+ loops.set(id, handle);
24902
+ }
24903
+ handle.set({ volume: Math.min(fire2.max, fire2.perFire * n) });
24842
24904
  }
24843
- loop ?? (loop = engine.loop(fireLoop));
24844
- loop.set({ volume: Math.min(fire2.max, fire2.perFire * burning) });
24845
24905
  };
24846
- const forget = (point) => {
24906
+ const forget = (point, fuel) => {
24847
24907
  let best = -1;
24848
24908
  let bestD = Infinity;
24849
24909
  fires.forEach((f, i) => {
24850
- const d = (f[0] - point[0]) ** 2 + (f[1] - point[1]) ** 2 + (f[2] - point[2]) ** 2;
24910
+ if (fuel !== void 0 && f.fuel !== fuel) return;
24911
+ const d = (f.point[0] - point[0]) ** 2 + (f.point[1] - point[1]) ** 2 + (f.point[2] - point[2]) ** 2;
24851
24912
  if (d < bestD) {
24852
24913
  bestD = d;
24853
24914
  best = i;
24854
24915
  }
24855
24916
  });
24856
- if (best >= 0) fires.splice(best, 1);
24917
+ if (best < 0 && fuel !== void 0) forget(point, void 0);
24918
+ else if (best >= 0) fires.splice(best, 1);
24919
+ };
24920
+ const reset = () => {
24921
+ for (const handle of loops.values()) handle.stop();
24922
+ loops.clear();
24923
+ fires.length = 0;
24857
24924
  };
24858
24925
  const blastSound = (point, volume, detail) => {
24859
24926
  const named = detail?.effect === void 0 ? void 0 : kinds[detail.effect];
@@ -24865,14 +24932,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24865
24932
  return {
24866
24933
  onSound(key2, point, volume, detail) {
24867
24934
  if (key2 === "burn_loop") {
24868
- burning += 1;
24869
- fires.push(point);
24935
+ fires.push({ point, fuel: fuelFor(detail) });
24870
24936
  setFire();
24871
24937
  return;
24872
24938
  }
24873
24939
  if (key2 === "burn_end") {
24874
- if (burning > 0) burning -= 1;
24875
- forget(point);
24940
+ forget(point, detail?.fuel ?? (detail?.material !== void 0 ? fuelOf[detail.material] : void 0));
24876
24941
  setFire();
24877
24942
  return;
24878
24943
  }
@@ -24902,12 +24967,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24902
24967
  },
24903
24968
  update(dt) {
24904
24969
  if (!crackles || fires.length === 0 || options.burnLoop === false) return;
24970
+ const weight = fires.reduce((sum2, f) => sum2 + FIRE_CRACKLE_OF[f.fuel], 0);
24971
+ if (weight <= 0) return;
24905
24972
  nextCrackle -= dt;
24906
24973
  for (let i = 0; i < 4 && nextCrackle <= 0; i++) {
24907
- const rate = Math.min(crackles.max, crackles.base + crackles.perSqrtFire * Math.sqrt(fires.length));
24974
+ const rate = Math.min(crackles.max, crackles.base * Math.min(1, weight) + crackles.perSqrtFire * Math.sqrt(weight));
24908
24975
  nextCrackle += (0.4 + channel.random() * 1.2) / rate;
24909
- const at3 = fires[Math.floor(channel.random() * fires.length)];
24910
- engine.play(channel.random() < 0.18 ? "fire_pop" : "fire_crackle", { position: at3, volume: crackles.volume });
24976
+ let pick3 = channel.random() * weight;
24977
+ const at3 = fires.find((f) => (pick3 -= FIRE_CRACKLE_OF[f.fuel]) <= 0) ?? fires[fires.length - 1];
24978
+ engine.play(channel.random() < 0.18 ? "fire_pop" : "fire_crackle", { position: at3.point, volume: crackles.volume });
24911
24979
  }
24912
24980
  if (nextCrackle < 0) nextCrackle = 0;
24913
24981
  },
@@ -24916,13 +24984,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24916
24984
  for (const k of detail?.keys ?? []) if (k in rules) return rules[k] ?? void 0;
24917
24985
  return rules[key2] ?? void 0;
24918
24986
  },
24919
- burning: () => burning,
24920
- dispose() {
24921
- loop?.stop();
24922
- loop = void 0;
24923
- burning = 0;
24924
- fires.length = 0;
24925
- }
24987
+ burning: () => fires.length,
24988
+ burningBy: () => {
24989
+ const by = {};
24990
+ for (const f of fires) by[f.fuel] = (by[f.fuel] ?? 0) + 1;
24991
+ return by;
24992
+ },
24993
+ reset,
24994
+ dispose: reset
24926
24995
  };
24927
24996
  }
24928
24997
  var blast2 = (gain) => ({ bus: "world", gain, falloff: BLAST_FALLOFF, cutoffK: 6e-3, priority: 3 });
@@ -24941,7 +25010,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24941
25010
  fire_extinguish: fire(0.7),
24942
25011
  gas_leak: fire(0.5),
24943
25012
  // the roar: recorded fires chained with a crossfade (the level comes from `createDestructionSound`, by how much burns)
24944
- fire_loop: { bus: "world", gain: 4, loop: { crossfade: 1 } }
25013
+ fire_loop: { bus: "world", gain: 4, loop: { crossfade: 1 } },
25014
+ // each fuel has its own roar: a gas jet (burners and torches), a pool of fuel, plastic and rubber sizzling under the flames
25015
+ fire_loop_gas: { bus: "world", gain: 6, loop: { crossfade: 1 } },
25016
+ fire_loop_liquid: { bus: "world", gain: 3.5, loop: { crossfade: 1 } },
25017
+ fire_loop_plastic: { bus: "world", gain: 1, loop: { crossfade: 0.6 } }
24945
25018
  };
24946
25019
 
24947
25020
  // src/body-sounds.ts
@@ -47790,6 +47863,7 @@ void main(){
47790
47863
  var LYING_EYE = 0.3;
47791
47864
  var LYING_PITCH = 1.05;
47792
47865
  var LYING_ROLL = 0.35;
47866
+ var LYING_PITCH_FACE_DOWN = -1.05;
47793
47867
  function createPlayer(THREE, o) {
47794
47868
  const cam = o.camera;
47795
47869
  let yaw = 0;
@@ -47828,11 +47902,12 @@ void main(){
47828
47902
  floorLevel += Math.max(-reach, Math.min(reach, gap));
47829
47903
  };
47830
47904
  let knocked = null;
47905
+ let knockedForward = false;
47831
47906
  let rollSide = 1;
47832
47907
  const apply = () => {
47833
47908
  const lying = knocked?.lying ?? 0;
47834
47909
  const fallen = lying * lying * (3 - 2 * lying);
47835
- euler.set(pitch + (LYING_PITCH - pitch) * fallen, yaw, LYING_ROLL * rollSide * fallen, "YXZ");
47910
+ euler.set(pitch + ((knockedForward ? LYING_PITCH_FACE_DOWN : LYING_PITCH) - pitch) * fallen, yaw, LYING_ROLL * rollSide * fallen, "YXZ");
47836
47911
  cam.quaternion.setFromEuler(euler);
47837
47912
  const standing = eye - CROUCH_DROP * crouch - bend;
47838
47913
  cam.position.set(px.x, floorLevel + y + standing + (LYING_EYE - standing) * fallen, px.z);
@@ -48110,6 +48185,7 @@ void main(){
48110
48185
  if (next.down) {
48111
48186
  const right = Math.cos(yaw) * next.velocity[0] - Math.sin(yaw) * next.velocity[2];
48112
48187
  rollSide = right >= 0 ? -1 : 1;
48188
+ knockedForward = -Math.sin(yaw) * next.velocity[0] - Math.cos(yaw) * next.velocity[2] > 0;
48113
48189
  crouchLatched = false;
48114
48190
  }
48115
48191
  if (knocked.airborne) vy2 = Math.max(vy2, knocked.velocity[1]);
@@ -48117,6 +48193,9 @@ void main(){
48117
48193
  get knocked() {
48118
48194
  return knocked;
48119
48195
  },
48196
+ get knockedForward() {
48197
+ return knockedForward;
48198
+ },
48120
48199
  update(dt) {
48121
48200
  if (knocked) {
48122
48201
  stepKnocked(dt);
@@ -53946,6 +54025,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
53946
54025
  explosives.clear();
53947
54026
  fireZones.length = 0;
53948
54027
  burningStains.length = 0;
54028
+ destructionSound?.reset();
53949
54029
  terrain.setLayout(defaultGround);
53950
54030
  for (const id of [...entries2.keys()]) if (!entries2.get(id)?.arena) removeEntry(id);
53951
54031
  clearLoose();
@@ -54113,6 +54193,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54113
54193
  avatarState.running = !player.crouching && (player.keys.has("ShiftLeft") || player.keys.has("ShiftRight"));
54114
54194
  avatarState.crouch = crouchForced || player.crouching || push.isBent;
54115
54195
  avatarState.down = downForced || Boolean(player.knocked?.down || (player.knocked?.lying ?? 0) > 0.5);
54196
+ avatarState.fallForward = downForced ? void 0 : player.knockedForward;
54116
54197
  avatarState.armed = armedForced ?? tool.def.action.kind === "firearm";
54117
54198
  avatarState.pitch = player.pitch;
54118
54199
  avatarState.holding = heldPiece !== null;
@@ -54312,6 +54393,15 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54312
54393
  };
54313
54394
  const fireZones = [];
54314
54395
  const burningStains = [];
54396
+ const poolFire = (point, burning) => playSound(burning ? "burn_loop" : "burn_end", point, 0.5, { fuel: "liquid" });
54397
+ const dropZone = (i) => {
54398
+ const [zone] = fireZones.splice(i, 1);
54399
+ if (zone) poolFire(zone.point, false);
54400
+ };
54401
+ const dropStain = (i) => {
54402
+ const [stain] = burningStains.splice(i, 1);
54403
+ if (stain) poolFire(stain.center, false);
54404
+ };
54315
54405
  const stainRadius = (s) => {
54316
54406
  let sum2 = 0;
54317
54407
  for (const r of s.front) sum2 += r;
@@ -54328,6 +54418,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54328
54418
  if (stainBurning(s)) continue;
54329
54419
  const seconds = Math.max(3, Math.min(20, 4 + s.liters * 30 + stainRadius(s) * 6));
54330
54420
  burningStains.push({ center: [s.center[0], s.center[1], s.center[2]], remaining: seconds, acc: 0 });
54421
+ poolFire([s.center[0], s.center[1], s.center[2]], true);
54331
54422
  lit = true;
54332
54423
  log.add({
54333
54424
  channel: "event",
@@ -54358,7 +54449,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54358
54449
  const a = rnd2() * Math.PI * 2;
54359
54450
  const r = Math.sqrt(rnd2()) * z.radius;
54360
54451
  renderer.emit("fire", [z.point[0] + Math.cos(a) * r, z.point[1] + 0.05, z.point[2] + Math.sin(a) * r], [0, 1, 0], 2, 1.5);
54361
- if (z.remaining <= 0) fireZones.splice(i, 1);
54452
+ if (z.remaining <= 0) dropZone(i);
54362
54453
  }
54363
54454
  if (burningStains.length) {
54364
54455
  const stains = fluids.stains();
@@ -54379,7 +54470,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54379
54470
  if (b.remaining <= 0 || !st) {
54380
54471
  fluids.clearStains(b.center, r + 0.1);
54381
54472
  log.add({ channel: "event", type: "stain_burned", text: t("ev.stain_burned"), data: { point: b.center } });
54382
- burningStains.splice(i, 1);
54473
+ dropStain(i);
54383
54474
  }
54384
54475
  }
54385
54476
  }
@@ -54716,8 +54807,8 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54716
54807
  hit.add(r.object);
54717
54808
  world.extinguish(r.object, void 0, u.coolC * Math.max(u.duration, 0.05) * 6);
54718
54809
  fluids.clearStains(r.point, 0.5);
54719
- for (let i = burningStains.length - 1; i >= 0; i--) if (Math.hypot(burningStains[i].center[0] - r.point[0], burningStains[i].center[2] - r.point[2]) < 1.2) burningStains.splice(i, 1);
54720
- for (let i = fireZones.length - 1; i >= 0; i--) if (Math.hypot(fireZones[i].point[0] - r.point[0], fireZones[i].point[2] - r.point[2]) < 1.5) fireZones.splice(i, 1);
54810
+ for (let i = burningStains.length - 1; i >= 0; i--) if (Math.hypot(burningStains[i].center[0] - r.point[0], burningStains[i].center[2] - r.point[2]) < 1.2) dropStain(i);
54811
+ for (let i = fireZones.length - 1; i >= 0; i--) if (Math.hypot(fireZones[i].point[0] - r.point[0], fireZones[i].point[2] - r.point[2]) < 1.5) dropZone(i);
54721
54812
  }
54722
54813
  });
54723
54814
  log.coalesce(
@@ -54728,7 +54819,9 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54728
54819
  };
54729
54820
  const startFire = (weapon, at3, fire3, impact3) => {
54730
54821
  fluids.splash("fuel", [at3[0], Math.max(at3[1], groundAt(at3[0], at3[2])), at3[2]], 0.5);
54731
- fireZones.push({ point: [at3[0], Math.max(0.02, groundAt(at3[0], at3[2])), at3[2]], radius: fire3.radius, temperature: fire3.temperature, remaining: fire3.seconds, acc: 0 });
54822
+ const zone = { point: [at3[0], Math.max(0.02, groundAt(at3[0], at3[2])), at3[2]], radius: fire3.radius, temperature: fire3.temperature, remaining: fire3.seconds, acc: 0 };
54823
+ fireZones.push(zone);
54824
+ poolFire(zone.point, true);
54732
54825
  renderer.emit("fire", at3, [0, 1, 0], 24, 3);
54733
54826
  action("molotov_fire", t("act.molotov_fire", { weapon: toolName(weapon), seconds: num(fire3.seconds, 0), radius: num(fire3.radius, 1) }), { data: { weapon, impact: impact3 } });
54734
54827
  };
@@ -55866,6 +55959,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
55866
55959
  loading,
55867
55960
  addView: (view) => moduleViews.add(view),
55868
55961
  setViewCamera: (view) => moduleViews.setMain(view ?? null),
55962
+ holdPlayer: (held) => player.setWalkHeld(held),
55869
55963
  ...options.blastShove ? { blastShove: options.blastShove } : {},
55870
55964
  blastReach: (point, target, ignore) => world.blastReach(point, target, ignore ? { ignore } : {}),
55871
55965
  tool: () => ({