@adatechnology/game-characters-three 0.1.4 → 0.2.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.
@@ -24,7 +24,10 @@ var GameCharactersThree = (() => {
24
24
  BAKE_NEUTRAL_POSE: () => BAKE_NEUTRAL_POSE,
25
25
  BODY_CHUNK: () => BODY_CHUNK,
26
26
  BODY_MATERIAL_DEF: () => BODY_MATERIAL_DEF,
27
+ BODY_SOUNDS: () => BODY_SOUNDS,
27
28
  CHARACTERS_AREA_ID: () => CHARACTERS_AREA_ID,
29
+ CHARACTER_SOUNDS: () => CHARACTER_SOUNDS,
30
+ CHARACTER_SOUND_BANK: () => CHARACTER_SOUND_BANK,
28
31
  EXTRAS: () => EXTRAS,
29
32
  FACE_FEATURES: () => FACE_FEATURES,
30
33
  HAND_ITEMS: () => HAND_ITEMS,
@@ -37,16 +40,22 @@ var GameCharactersThree = (() => {
37
40
  LOD_SEGMENTS: () => LOD_SEGMENTS,
38
41
  PENDULUM: () => PENDULUM,
39
42
  PERSON_ITEM_PREFIX: () => PERSON_ITEM_PREFIX,
43
+ PLAYER_SOUNDS: () => PLAYER_SOUNDS,
44
+ UNDEAD_VOICES: () => UNDEAD_VOICES,
45
+ UNDEAD_VOICE_GROUP: () => UNDEAD_VOICE_GROUP,
46
+ UNDEAD_VOICE_SOURCES: () => UNDEAD_VOICE_SOURCES,
40
47
  ZOMBIE_ITEM_PREFIX: () => ZOMBIE_ITEM_PREFIX,
41
48
  bakeBody: () => bakeBody,
42
49
  bakeVariants: () => bakeVariants,
43
50
  blobGeometry: () => blobGeometry,
44
51
  blobRadii: () => blobRadii,
52
+ bloodLookOfAge: () => bloodLookOfAge,
45
53
  buildInstancedBody: () => buildInstancedBody,
46
54
  createBestBuilder: () => createBestBuilder,
47
55
  createBodyBatch: () => createBodyBatch,
48
56
  createBodyChunk: () => createBodyChunk,
49
57
  createBodyMaterial: () => createBodyMaterial,
58
+ createCharacterSound: () => createCharacterSound,
50
59
  createCharactersTestModule: () => createCharactersTestModule,
51
60
  createEye: () => createEye,
52
61
  createFluidsBridge: () => createFluidsBridge,
@@ -349,6 +358,13 @@ var GameCharactersThree = (() => {
349
358
  }
350
359
  return removed;
351
360
  },
361
+ remove(index) {
362
+ const entry = entries[index];
363
+ if (!entry) return void 0;
364
+ entries[index] = entries[entries.length - 1];
365
+ entries.pop();
366
+ return entry.data;
367
+ },
352
368
  clear() {
353
369
  entries.length = 0;
354
370
  }
@@ -2864,7 +2880,7 @@ var GameCharactersThree = (() => {
2864
2880
  const TORSO_P = [[0.18, 0.86], [0.205, 0.95], [0.195, 1.03], [0.19, 1.12], [0.225, 1.27], [0.26, 1.4], [0.265, 1.47], [0.21, 1.55], [0.085, 1.6]];
2865
2881
  const TORSO_F = [[0.17, 0.86], [0.215, 0.95], [0.2, 1.02], [0.165, 1.1], [0.2, 1.25], [0.245, 1.38], [0.25, 1.46], [0.2, 1.54], [0.085, 1.6]];
2866
2882
  const torsoGeo = (sx, sz, fem) => {
2867
- const g = new THREE.LatheGeometry(new THREE.SplineCurve((fem ? TORSO_F : TORSO_P).map((p) => new THREE.Vector2(p[0], p[1]))).getPoints(Math.max(8, Math.round(30 * LK))), Math.max(6, Math.round(24 * LK)));
2883
+ const g = new THREE.LatheGeometry(new THREE.SplineCurve((fem ? TORSO_F : TORSO_P).map((p2) => new THREE.Vector2(p2[0], p2[1]))).getPoints(Math.max(8, Math.round(30 * LK))), Math.max(6, Math.round(24 * LK)));
2868
2884
  g.scale(1.12 * (sx || 1), 1, 0.64 * (sz || 1));
2869
2885
  return g;
2870
2886
  };
@@ -2952,34 +2968,34 @@ var GameCharactersThree = (() => {
2952
2968
  const skinFinal = P.char ? 1840658 : P.wet ? col(skinC, 8032160, 0.55) : skinC;
2953
2969
  const skinM = M(skinFinal);
2954
2970
  const topM = M(P.char ? col(topC, 1380880, 0.75) : P.wet ? col(topC, 1712688, 0.45) : col(topC, GRAY2, d * 0.3)), pantsM = M(col(pantC, GRAY2, d * 0.3)), shoeM = M(T.shoeColor), red = M(RED2);
2955
- const add = (p, geo, m, x, y, z2, rx, ry, rz) => {
2971
+ const add = (p2, geo, m, x, y, z2, rx, ry, rz) => {
2956
2972
  const o = new THREE.Mesh(geo, m);
2957
2973
  o.position.set(x || 0, y || 0, z2 || 0);
2958
2974
  if (rx || ry || rz) o.rotation.set(rx || 0, ry || 0, rz || 0);
2959
- p.add(o);
2975
+ p2.add(o);
2960
2976
  return o;
2961
2977
  };
2962
- const ball = (p, r, m, x, y, z2) => add(p, SPH(r, SEG, 12), m, x, y, z2);
2978
+ const ball = (p2, r, m, x, y, z2) => add(p2, SPH(r, SEG, 12), m, x, y, z2);
2963
2979
  topM_ref = topM;
2964
2980
  const SKM = {}, skM = (c) => SKM[c] || (SKM[c] = new THREE.MeshStandardMaterial({ color: c, roughness: 0.3, metalness: 0, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 }));
2965
- const flat = (p, x, y, zz, sx, sy, c) => {
2981
+ const flat = (p2, x, y, zz, sx, sy, c) => {
2966
2982
  const m = new THREE.Mesh(BLOB, skM(c));
2967
2983
  m.rotation.set(PI / 2, 0, BR() * 6.28);
2968
2984
  m.scale.set(sx, 1, sy);
2969
2985
  m.position.set(x, y, zz);
2970
- p.add(m);
2986
+ p2.add(m);
2971
2987
  return m;
2972
2988
  };
2973
- const splat = (p, x, y, zz, r, c, n) => {
2974
- const a = flat(p, x, y, zz + 4e-3, r, r * (0.7 + BR() * 0.5), c);
2989
+ const splat = (p2, x, y, zz, r, c, n) => {
2990
+ const a = flat(p2, x, y, zz + 4e-3, r, r * (0.7 + BR() * 0.5), c);
2975
2991
  for (let k = 0; k < (n || 0); k++) {
2976
2992
  const an = BR() * 6.28, dd = r * (1.2 + BR() * 0.9), q = r * (0.12 + BR() * 0.18);
2977
- flat(p, x + Math.cos(an) * dd, y + Math.sin(an) * dd, zz + 4e-3, q, q * 0.7, c);
2993
+ flat(p2, x + Math.cos(an) * dd, y + Math.sin(an) * dd, zz + 4e-3, q, q * 0.7, c);
2978
2994
  }
2979
- if (n) run(p, x + r * 0.15, y - r * 0.5, y - r * (2.2 + BR() * 1.2), zz + 3e-3, zz, c, r * 0.35);
2995
+ if (n) run(p2, x + r * 0.15, y - r * 0.5, y - r * (2.2 + BR() * 1.2), zz + 3e-3, zz, c, r * 0.35);
2980
2996
  return a;
2981
2997
  };
2982
- const run = (p, x, y0, y1, z0, z1, c, w) => {
2998
+ const run = (p2, x, y0, y1, z0, z1, c, w) => {
2983
2999
  const L = y0 - y1, sh = new THREE.Shape();
2984
3000
  sh.moveTo(-w / 2, 0);
2985
3001
  sh.lineTo(w / 2, 0);
@@ -2989,30 +3005,30 @@ var GameCharactersThree = (() => {
2989
3005
  const g = new THREE.ShapeGeometry(sh), m = new THREE.Mesh(g, skM(c));
2990
3006
  m.position.set(x, y0, z0);
2991
3007
  m.rotation.x = -Math.atan2(z0 - z1, L);
2992
- p.add(m);
3008
+ p2.add(m);
2993
3009
  const bl = new THREE.Mesh(SPH(w * 0.42, 10, 8), skM(c));
2994
3010
  bl.scale.set(1, 1.35, 0.5);
2995
3011
  bl.position.set(x + w * 0.03, y1, z1);
2996
- p.add(bl);
3012
+ p2.add(bl);
2997
3013
  return bl;
2998
3014
  };
2999
- const stump = (p, r, y, up) => {
3000
- const st = ball(p, r * 1.02, red, 0, y, 0);
3015
+ const stump = (p2, r, y, up) => {
3016
+ const st = ball(p2, r * 1.02, red, 0, y, 0);
3001
3017
  st.scale.set(1, 0.35, 1);
3002
- add(p, CYL(r * 0.32, r * 0.36, 0.07, 10), M(15261900), 0, y + (up ? 0.04 : -0.04), 0);
3018
+ add(p2, CYL(r * 0.32, r * 0.36, 0.07, 10), M(15261900), 0, y + (up ? 0.04 : -0.04), 0);
3003
3019
  };
3004
- const ring = (p, r, h, m, y, sx, sz) => {
3005
- const o = add(p, CYL(r, r, h, 24), m, 0, y, 0);
3020
+ const ring = (p2, r, h, m, y, sx, sz) => {
3021
+ const o = add(p2, CYL(r, r, h, 24), m, 0, y, 0);
3006
3022
  o.scale.set(sx || 1.12, 1, sz || 0.7);
3007
3023
  return o;
3008
3024
  };
3009
3025
  const root = new THREE.Group(), fig = new THREE.Group();
3010
- const drip = (p, x, y, zz, kind, every) => {
3026
+ const drip = (p2, x, y, zz, kind, every) => {
3011
3027
  if (d >= 3 && kind !== "bile") return;
3012
3028
  const o = new THREE.Object3D();
3013
3029
  o.position.set(x, y, zz);
3014
3030
  o.userData.drip = { kind: kind || (d < 1 ? "blood" : "old"), every: every || (d < 1 ? 1.2 : 2.4) };
3015
- p.add(o);
3031
+ p2.add(o);
3016
3032
  };
3017
3033
  fig.scale.set(P.s * P.bulk, P.s, P.s * Math.min(1.15, P.bulk));
3018
3034
  fig.position.y = P.drop * P.s;
@@ -4182,7 +4198,7 @@ var GameCharactersThree = (() => {
4182
4198
  }
4183
4199
  function torsoGeo(fem) {
4184
4200
  const P = fem ? [[0.17, 0.86], [0.215, 0.95], [0.2, 1.02], [0.165, 1.1], [0.2, 1.25], [0.245, 1.38], [0.25, 1.46], [0.2, 1.54], [0.085, 1.6]] : [[0.18, 0.86], [0.205, 0.95], [0.195, 1.03], [0.19, 1.12], [0.225, 1.27], [0.26, 1.4], [0.265, 1.47], [0.21, 1.55], [0.085, 1.6]];
4185
- const pts = new THREE.SplineCurve(P.map((p) => new THREE.Vector2(p[0], p[1]))).getPoints(30);
4201
+ const pts = new THREE.SplineCurve(P.map((p2) => new THREE.Vector2(p2[0], p2[1]))).getPoints(30);
4186
4202
  const g = new THREE.LatheGeometry(pts, 24);
4187
4203
  g.scale(1.12, 1, 0.64);
4188
4204
  return g;
@@ -4537,15 +4553,15 @@ var GameCharactersThree = (() => {
4537
4553
  }
4538
4554
  const MOVES_IDLE = MOVES.idle;
4539
4555
  function pose(c, z, W, t) {
4540
- const p = c.ph + t, freq = z ? 3.4 : 5.6, ph = t * freq + c.ph;
4541
- const breathe = reduce ? 0 : Math.sin(p * 1.6) * 0.012;
4556
+ const p2 = c.ph + t, freq = z ? 3.4 : 5.6, ph = t * freq + c.ph;
4557
+ const breathe = reduce ? 0 : Math.sin(p2 * 1.6) * 0.012;
4542
4558
  c.upper.scale.y = 1 + breathe;
4543
4559
  const hunch = c.hunch + (z ? 0.38 : 0) + W * 0.05;
4544
4560
  c.upper.rotation.x = hunch;
4545
4561
  c.upper.rotation.y = W * Math.sin(ph) * 0.1;
4546
4562
  c.upper.rotation.z = z ? Math.sin(ph * 0.5) * 0.06 * W + 0.05 : 0;
4547
4563
  c.fig.position.y = -W * Math.abs(Math.sin(ph)) * 0.035;
4548
- c.headG.rotation.y = reduce ? 0 : Math.sin(p * 0.5) * 0.25 * (z ? 0.3 : 1) * (W ? 0.4 : 1) * (t ? 1 : 0);
4564
+ c.headG.rotation.y = reduce ? 0 : Math.sin(p2 * 0.5) * 0.25 * (z ? 0.3 : 1) * (W ? 0.4 : 1) * (t ? 1 : 0);
4549
4565
  c.headG.rotation.x = -hunch * 0.6 + (z ? 0.15 : 0);
4550
4566
  c.headG.rotation.z = z ? 0.22 : 0;
4551
4567
  c.legs.forEach((l, k) => {
@@ -4558,7 +4574,7 @@ var GameCharactersThree = (() => {
4558
4574
  const raise = z ? 1 : 0;
4559
4575
  const base = a.hold ? -0.2 : 0;
4560
4576
  const swing = -a.s * Math.sin(ph) * (z ? 0.12 : 0.42) * W * (a.grip ? 0 : 1);
4561
- a.g.rotation.x = (a.grip && !z ? -0.75 : base - raise * (z ? 1.12 : 1.3)) + swing + (z && !reduce && t ? Math.sin(p * 3 + a.s * 2) * 0.08 : 0);
4577
+ a.g.rotation.x = (a.grip && !z ? -0.75 : base - raise * (z ? 1.12 : 1.3)) + swing + (z && !reduce && t ? Math.sin(p2 * 3 + a.s * 2) * 0.08 : 0);
4562
4578
  a.g.rotation.z = a.s * (0.07 + (z ? 0.1 : 0)) + (a.grip && !z ? -a.s * 0.35 : 0);
4563
4579
  a.el.rotation.x = z ? -0.18 : a.grip ? -1.1 : -(0.12 + (a.hold ? 0.25 : 0) + W * (0.25 + Math.max(0, a.s * Math.sin(ph)) * 0.3));
4564
4580
  if (a.fingers) {
@@ -4583,16 +4599,16 @@ var GameCharactersThree = (() => {
4583
4599
  c.mouth.visible = true;
4584
4600
  c.bags.material.opacity = 0.9;
4585
4601
  }
4586
- function buildPerson(p, zed) {
4602
+ function buildPerson(p2, zed) {
4587
4603
  const root = new THREE.Group(), chars = [];
4588
- if (p.b) {
4589
- const c = build(p.b, 0);
4604
+ if (p2.b) {
4605
+ const c = build(p2.b, 0);
4590
4606
  chars.push(c);
4591
4607
  root.add(c.root);
4592
- if (zed && !p.kid) zombify(c, p.zcat);
4593
- c.z = zed && !p.kid;
4608
+ if (zed && !p2.kid) zombify(c, p2.zcat);
4609
+ c.z = zed && !p2.kid;
4594
4610
  }
4595
- (p.kids || []).forEach((k) => {
4611
+ (p2.kids || []).forEach((k) => {
4596
4612
  const c = build(k, 0);
4597
4613
  c.root.position.x = k.x;
4598
4614
  c.root.rotation.y = -k.x * 0.4;
@@ -4850,11 +4866,11 @@ var GameCharactersThree = (() => {
4850
4866
  };
4851
4867
  return {
4852
4868
  pieces,
4853
- launch(source, scene, direction, dress, tag) {
4869
+ launch(source, scene2, direction, dress, tag) {
4854
4870
  source.updateWorldMatrix(true, true);
4855
4871
  const clone = source.clone(true);
4856
4872
  source.matrixWorld.decompose(clone.position, clone.quaternion, clone.scale);
4857
- scene.add(clone);
4873
+ scene2.add(clone);
4858
4874
  dress?.(clone);
4859
4875
  return add(
4860
4876
  clone,
@@ -5003,7 +5019,7 @@ var GameCharactersThree = (() => {
5003
5019
  // src/wounds/wounds.ts
5004
5020
  var worldPosition = (three, object) => object.getWorldPosition(new three.Vector3());
5005
5021
  var createWounds = (three, options) => {
5006
- const { random, scene, bridge } = options;
5022
+ const { random, scene: scene2, bridge, sound } = options;
5007
5023
  const materials = options.materials ?? createWoundMaterials(three);
5008
5024
  const flying = createFlyingPieces(three, { random, ...options.groundAt ? { groundAt: options.groundAt } : {}, ...options.maxPieces ? { max: options.maxPieces } : {} });
5009
5025
  const states = /* @__PURE__ */ new Map();
@@ -5012,9 +5028,9 @@ var GameCharactersThree = (() => {
5012
5028
  if (!found) throw new Error("game-characters-three: this body is not tracked by the wounds (call `track` first)");
5013
5029
  return found;
5014
5030
  };
5015
- const burstBlood = (change, object) => {
5031
+ const burstBlood = (state, change, object, direction) => {
5016
5032
  const point = worldPosition(three, object);
5017
- bridge?.burst(change, [point.x, point.y, point.z]);
5033
+ bridge?.burst(change, [point.x, point.y, point.z], state.body.blood, direction ? [direction.x, direction.y, direction.z] : void 0);
5018
5034
  };
5019
5035
  const sideOf2 = (object, body) => new three.Vector3(object.position.x >= 0 ? 1 : -1, 0, 0).applyQuaternion(body.root.quaternion);
5020
5036
  const hideBelow = (state, id) => {
@@ -5029,7 +5045,7 @@ var GameCharactersThree = (() => {
5029
5045
  }
5030
5046
  const { node, radius, flip } = target;
5031
5047
  const direction = sideOf2(node, state.body);
5032
- const fell = flying.launch(node, scene, direction, (clone) => {
5048
+ const fell = flying.launch(node, scene2, direction, (clone) => {
5033
5049
  makeCut(three, materials, random, clone, radius, true);
5034
5050
  }, id);
5035
5051
  void fell;
@@ -5043,7 +5059,7 @@ var GameCharactersThree = (() => {
5043
5059
  for (let o = pendulum.node; o; o = o.parent) if (!o.visible) shown = false;
5044
5060
  if (!shown) state.pendulums.splice(i, 1);
5045
5061
  }
5046
- burstBlood("severed", node);
5062
+ burstBlood(state, "severed", node, direction);
5047
5063
  if (id === "head") state.dead = true;
5048
5064
  };
5049
5065
  const destroy = (state, id, direction) => {
@@ -5052,14 +5068,14 @@ var GameCharactersThree = (() => {
5052
5068
  const { node, radius, flip } = target;
5053
5069
  const box = new three.Box3().setFromObject(node);
5054
5070
  explodeChunks(three, materials, random, box, direction, (chunk, velocity, spin) => {
5055
- scene.add(chunk);
5071
+ scene2.add(chunk);
5056
5072
  flying.throwObject(chunk, velocity, spin, id);
5057
5073
  });
5058
5074
  node.visible = false;
5059
5075
  makeStump(three, materials, random, node.parent, node.position, radius, flip);
5060
5076
  state.gone.add(id);
5061
5077
  hideBelow(state, id);
5062
- burstBlood("destroyed", node);
5078
+ burstBlood(state, "destroyed", node, direction);
5063
5079
  if (id === "head") state.dead = true;
5064
5080
  };
5065
5081
  const breakBone = (state, id) => {
@@ -5079,7 +5095,7 @@ var GameCharactersThree = (() => {
5079
5095
  const split2 = (state, direction) => {
5080
5096
  const upper = state.body.rig.upper;
5081
5097
  if (!upper.visible) return;
5082
- flying.launch(upper, scene, direction, (clone) => {
5098
+ flying.launch(upper, scene2, direction, (clone) => {
5083
5099
  makeCut(three, materials, random, clone, 0.16, false);
5084
5100
  }, "torso");
5085
5101
  upper.visible = false;
@@ -5087,7 +5103,7 @@ var GameCharactersThree = (() => {
5087
5103
  for (const id of ["torso", "neck", "head", "jaw", "upperArmL", "upperArmR", "forearmL", "forearmR", "handL", "handR"]) state.gone.add(id);
5088
5104
  state.pendulums.length = 0;
5089
5105
  state.dead = true;
5090
- burstBlood("split", upper);
5106
+ burstBlood(state, "split", upper, direction);
5091
5107
  };
5092
5108
  const direction3 = (v, fallback) => v ? new three.Vector3(v[0], v[1], v[2]).normalize() : fallback;
5093
5109
  return {
@@ -5121,6 +5137,10 @@ var GameCharactersThree = (() => {
5121
5137
  }
5122
5138
  if (event.type !== "body") return;
5123
5139
  const id = event.part;
5140
+ if (sound) {
5141
+ const heard = worldPosition(three, (id ? body.nodes[id]?.node : void 0) ?? body.root);
5142
+ sound.body(event, [heard.x, heard.y, heard.z]);
5143
+ }
5124
5144
  const fallback = new three.Vector3(0, 0.2, 1);
5125
5145
  const direction = direction3(event.exit, fallback);
5126
5146
  switch (event.change) {
@@ -5147,8 +5167,10 @@ var GameCharactersThree = (() => {
5147
5167
  break;
5148
5168
  case "wound": {
5149
5169
  const target = id ? body.nodes[id] : void 0;
5170
+ const at = worldPosition(three, target?.node ?? body.root);
5171
+ bridge?.wound([at.x, at.y, at.z], body.blood, event.exit ? [direction.x, direction.y, direction.z] : void 0);
5150
5172
  if (target && event.exit) gouge(three, materials, random, target.node, id.replace(/[LR]$/, ""), worldPosition(three, target.node), direction.clone().negate(), direction, 60, (chunk, velocity, spin) => {
5151
- scene.add(chunk);
5173
+ scene2.add(chunk);
5152
5174
  flying.throwObject(chunk, velocity, spin, id);
5153
5175
  });
5154
5176
  break;
@@ -5194,8 +5216,28 @@ var GameCharactersThree = (() => {
5194
5216
  };
5195
5217
 
5196
5218
  // src/wounds/fluids-bridge.ts
5219
+ var colorOf = (hex2) => Number.parseInt(hex2.replace(/^#|^0x/i, ""), 16);
5220
+ var bloodLookOfAge = (age, options = {}) => ({
5221
+ ...age.blood < (options.thinBelow ?? 0.3) ? { liquid: options.thinLiquid ?? "old_blood" } : {},
5222
+ tint: { fresh: colorOf(age.freshBloodColor), dry: colorOf(age.dryBloodColor) },
5223
+ amount: age.blood
5224
+ });
5197
5225
  var BURST_LITERS = { severed: 0.35, destroyed: 0.6, split: 0.9 };
5198
- var createFluidsBridge = ({ sink, liquid = "blood", spurtSpeed = 1.4 }) => {
5226
+ var BURST_DROPS = { severed: 4, destroyed: 7, split: 10 };
5227
+ var createFluidsBridge = ({ sink, film, spray, burstDrops = BURST_DROPS, liquid = "blood", spurtSpeed = 1.4 }) => {
5228
+ const throwDrops = (point, wanted, look, direction, speed, radius) => {
5229
+ const amount = look?.amount ?? 1;
5230
+ const count = amount <= 0 ? 0 : Math.max(1, Math.round(wanted * Math.min(1, amount)));
5231
+ return film && count > 0 ? film.splat({
5232
+ fluid: look?.liquid ?? liquid,
5233
+ point,
5234
+ count,
5235
+ ...direction ? { direction } : {},
5236
+ ...speed !== void 0 ? { speed } : {},
5237
+ ...radius !== void 0 ? { radius } : {},
5238
+ ...look?.tint ? { tint: { fresh: look.tint.fresh, dry: look.tint.dry } } : {}
5239
+ }) : 0;
5240
+ };
5199
5241
  const bleed = (point, litersPerSecond, dt, direction = [0, -1, 0], pressurized = false) => {
5200
5242
  const liters = litersPerSecond * dt;
5201
5243
  if (liters <= 0) return;
@@ -5207,10 +5249,462 @@ var GameCharactersThree = (() => {
5207
5249
  handle(event) {
5208
5250
  if (event.type === "leak" && event.fluid === "blood") bleed(event.point, event.rate, 1, event.direction, event.pressurized);
5209
5251
  },
5210
- burst: (change, point) => sink.splash(liquid, point, BURST_LITERS[change])
5252
+ burst(change, point, look, direction) {
5253
+ sink.splash(look?.liquid ?? liquid, point, BURST_LITERS[change]);
5254
+ throwDrops(point, burstDrops[change], look, direction);
5255
+ },
5256
+ wound: (point, look, direction) => throwDrops(point, spray?.count ?? 3, look, direction, spray?.speed, spray?.radius)
5257
+ };
5258
+ };
5259
+
5260
+ // ../game-audio/dist/index.js
5261
+ var DISTANCE_CURVES = {
5262
+ /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
5263
+ game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
5264
+ /** The game's `grito`: `1 / (1 + d² × 0.006)`, silent below 0.02 (jogo L62129–62130). */
5265
+ game_scream: { model: "inverse_square", k: 6e-3, minGain: 0.02 },
5266
+ /** The game's `voz`: `1 / (1 + d² × 0.01)`, silent below 0.02 (jogo L62146–62147). */
5267
+ game_voice: { model: "inverse_square", k: 0.01, minGain: 0.02 },
5268
+ /** The game's `boom`: `1.4 / (1 + d² × 0.002)`, never silent (jogo L68280). */
5269
+ game_boom: { model: "inverse_square", k: 2e-3, scale: 1.4 },
5270
+ /** The art scene's `outAt`: `vol / (1 + d² × 0.008)` (cena L1320). */
5271
+ art_scene: { model: "inverse_square", k: 8e-3 },
5272
+ /** The game's animal calls: `max(0, 1 − d / 70) × 0.25`, silent at or below 0.005 (jogo L66627–66628). */
5273
+ animals: { model: "linear", range: 70, scale: 0.25, minGain: 5e-3 }
5274
+ };
5275
+ var mix_profiles_default = [
5276
+ {
5277
+ id: "game",
5278
+ master: 0.5,
5279
+ noise: { seconds: 1.5, create: "lazy" },
5280
+ sources: ["jogo:L62077-62081 master 0.5 to the output", "jogo:L62082-62088 noise 1.5 s made by the first sound that needs it"]
5281
+ },
5282
+ {
5283
+ id: "art_scene",
5284
+ master: 0.85,
5285
+ compressor: { threshold: -12, ratio: 5 },
5286
+ reverb: { seconds: 2.2, decay: 3.4, wet: 0.4 },
5287
+ noise: { seconds: 2, create: "eager" },
5288
+ sources: [
5289
+ "cena:L1308 compressor -12 dB, ratio 5",
5290
+ "cena:L1309 master 0.85",
5291
+ "cena:L1310 convolver, impulse 2.2 s of (1 - i/len)^3.4",
5292
+ "cena:L1311 wet 0.4",
5293
+ "cena:L1312 noise 2 s made with the chain"
5294
+ ]
5295
+ }
5296
+ ];
5297
+ var MIX_PROFILES = Object.fromEntries(
5298
+ mix_profiles_default.map((profile) => [profile.id, profile])
5299
+ );
5300
+ var game_sfx_default = {
5301
+ sources: ["jogo:L62089-62107"],
5302
+ note: "The game's sfx table P: [lowpass cutoff Hz, peak gain, seconds]. Played as a noise burst with the game's distance curve (falloff game_sfx) and a 0.05 s tail. Team names in `team`.",
5303
+ classes: [
5304
+ { id: "pistol", team: "pistola", bus: "weapons", cutoff: 1800, gain: 0.9, seconds: 0.18 },
5305
+ { id: "shotgun", team: "escopeta", bus: "weapons", cutoff: 900, gain: 1.2, seconds: 0.35 },
5306
+ { id: "rifle", team: "fuzil", bus: "weapons", cutoff: 1400, gain: 1.1, seconds: 0.28 },
5307
+ { id: "long_rifle", team: "rifle", bus: "weapons", cutoff: 1100, gain: 1.3, seconds: 0.45 },
5308
+ { id: "impact", team: "impacto", bus: "world", cutoff: 2500, gain: 0.25, seconds: 0.05 },
5309
+ { id: "flesh", team: "carne", bus: "world", cutoff: 600, gain: 0.35, seconds: 0.08 },
5310
+ { id: "step", team: "passo", bus: "world", cutoff: 400, gain: 0.08, seconds: 0.05 },
5311
+ { id: "glass", team: "vidro", bus: "world", cutoff: 5e3, gain: 0.5, seconds: 0.3 },
5312
+ { id: "wood", team: "madeira", bus: "world", cutoff: 700, gain: 0.5, seconds: 0.2 },
5313
+ { id: "reload", team: "recarga", bus: "weapons", cutoff: 3e3, gain: 0.15, seconds: 0.06 },
5314
+ { id: "empty", team: "vazio", bus: "weapons", cutoff: 4e3, gain: 0.12, seconds: 0.03 },
5315
+ { id: "explosion", team: "explosao", bus: "world", cutoff: 200, gain: 1.6, seconds: 1.4 }
5316
+ ],
5317
+ fallback: { id: "unknown", team: "(any other name)", bus: "world", cutoff: 1e3, gain: 0.5, seconds: 0.1 }
5318
+ };
5319
+ var GAME_SFX = game_sfx_default;
5320
+ var GAME_SFX_CLASSES = GAME_SFX.classes;
5321
+ var GAME_SFX_FALLBACK = GAME_SFX.fallback;
5322
+ function noiseClassRecipe(row, id = `sfx_${row.id}`) {
5323
+ return {
5324
+ id,
5325
+ bus: row.bus,
5326
+ order: "burst",
5327
+ duration: row.seconds,
5328
+ tail: 0.05,
5329
+ layers: [
5330
+ {
5331
+ source: { kind: "noise", offset: "random" },
5332
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: row.cutoff } }] },
5333
+ envelope: [
5334
+ [0, row.gain, "set"],
5335
+ ["1d", 1e-3, "exp", "fixed"]
5336
+ ]
5337
+ }
5338
+ ],
5339
+ falloff: DISTANCE_CURVES.game_sfx,
5340
+ sources: [...GAME_SFX.sources.map((source) => `${source} ${row.team}`)]
5211
5341
  };
5342
+ }
5343
+ var GAME_SFX_BANK = { id: "game_sfx", sounds: [...GAME_SFX_CLASSES, GAME_SFX_FALLBACK].map((row) => noiseClassRecipe(row)) };
5344
+ var ATTACK = 2e-3;
5345
+ function lengthOf(d, attack) {
5346
+ if (typeof d === "number") return attack + d;
5347
+ if (Array.isArray(d)) return [attack + d[0], attack + d[1]];
5348
+ return { ...d, add: (d.add ?? 0) + attack };
5349
+ }
5350
+ var envelope = (attack) => [
5351
+ [0, 1e-4, "set", "fixed"],
5352
+ [attack, 1, "exp"],
5353
+ ["1d", 1e-4, "exp", "fixed"]
5354
+ ];
5355
+ function sceneNoise(o) {
5356
+ const a = o.a ?? ATTACK;
5357
+ const over = typeof o.d === "number" ? o.d : 0;
5358
+ return {
5359
+ filters: {
5360
+ mode: "serial",
5361
+ bank: [{ type: o.type ?? "lowpass", frequency: o.f2 === void 0 ? { set: o.f } : { set: o.f, to: o.f2, curve: "exp", over }, q: o.q ?? 0.7 }]
5362
+ },
5363
+ duration: lengthOf(o.d, a),
5364
+ gain: o.vol,
5365
+ ...o.delay === void 0 ? {} : { at: o.delay },
5366
+ ...o.wet === void 0 ? {} : { wet: o.wet },
5367
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
5368
+ envelope: envelope(a)
5369
+ };
5370
+ }
5371
+ function sceneTone(o) {
5372
+ return {
5373
+ source: { kind: "osc", wave: o.wave ?? "sine", frequency: { from: o.f, to: o.f2 ?? "from", curve: "exp", over: "duration" } },
5374
+ duration: lengthOf(o.d, ATTACK),
5375
+ gain: o.vol,
5376
+ ...o.delay === void 0 ? {} : { at: o.delay },
5377
+ ...o.wet === void 0 ? {} : { wet: o.wet },
5378
+ envelope: envelope(ATTACK)
5379
+ };
5380
+ }
5381
+ function sceneVoice(o) {
5382
+ return {
5383
+ duration: lengthOf(o.d, 0),
5384
+ gain: o.vol,
5385
+ wet: 0.45,
5386
+ source: {
5387
+ kind: "formant_voice",
5388
+ wave: o.wave ?? "sawtooth",
5389
+ frequency: { from: o.f0, to: o.f1, curve: "linear", over: "duration" },
5390
+ vibrato: { rate: [4, 10], depth: 0.08 },
5391
+ formants: [
5392
+ { frequency: o.fA, q: 4 },
5393
+ { frequency: o.fB, q: 6 }
5394
+ ],
5395
+ breath: { frequency: o.fB, q: 1.1, gain: o.noise },
5396
+ mix: 3.2
5397
+ },
5398
+ envelope: [
5399
+ [0, 1e-4, "set", "fixed"],
5400
+ [0.1, 1, "exp"],
5401
+ ["0.6d", 1, "set"],
5402
+ ["1d", 1e-4, "exp", "fixed"]
5403
+ ]
5404
+ };
5405
+ }
5406
+ function sceneRecipe(id, layers, line, options = {}) {
5407
+ return {
5408
+ id,
5409
+ bus: options.bus ?? "world",
5410
+ pan: true,
5411
+ layers,
5412
+ falloff: DISTANCE_CURVES.art_scene,
5413
+ ...options.vars ? { vars: options.vars } : {},
5414
+ ...options.group ? { group: options.group } : {},
5415
+ sources: [`cena:${line}`]
5416
+ };
5417
+ }
5418
+ var BENCH_BANK = {
5419
+ id: "audio_bench",
5420
+ sounds: [
5421
+ {
5422
+ id: "bench_ping",
5423
+ bus: "ui",
5424
+ pan: true,
5425
+ duration: 0.25,
5426
+ layers: [
5427
+ {
5428
+ gain: 0.5,
5429
+ wet: 0.3,
5430
+ source: { kind: "osc", wave: "sine", frequency: { from: 880 } },
5431
+ envelope: [
5432
+ [0, 1e-4, "set", "fixed"],
5433
+ [2e-3, 1, "exp"],
5434
+ ["1d", 1e-4, "exp", "fixed"]
5435
+ ]
5436
+ }
5437
+ ],
5438
+ falloff: DISTANCE_CURVES.art_scene,
5439
+ sources: ["sdk: bench probe, a 880 Hz ping through the art scene output stage"]
5440
+ },
5441
+ {
5442
+ id: "bench_hiss",
5443
+ bus: "ui",
5444
+ pan: true,
5445
+ duration: [0.3, 0.9],
5446
+ layers: [
5447
+ {
5448
+ gain: 0.4,
5449
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
5450
+ filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 3e3 }, q: 0.7 }] },
5451
+ envelope: [
5452
+ [0, 1e-4, "set", "fixed"],
5453
+ [2e-3, 1, "exp"],
5454
+ ["1d", 1e-4, "exp", "fixed"]
5455
+ ]
5456
+ }
5457
+ ],
5458
+ falloff: DISTANCE_CURVES.art_scene,
5459
+ sources: ["sdk: bench probe, a highpass hiss of random length"]
5460
+ }
5461
+ ]
5212
5462
  };
5213
5463
 
5464
+ // src/audio/recipes.ts
5465
+ var UNDEAD_VOICE_GROUP = "undead_voice";
5466
+ var voiced = { bus: "voices", group: UNDEAD_VOICE_GROUP };
5467
+ var p = (param) => ({ param });
5468
+ var SCREAM_HOARSE = {
5469
+ id: "scream_hoarse",
5470
+ bus: "voices",
5471
+ order: "scream",
5472
+ duration: [1, 1.4],
5473
+ layers: [
5474
+ {
5475
+ source: { kind: "noise", offset: "random" },
5476
+ filters: {
5477
+ mode: "parallel",
5478
+ bank: [
5479
+ { type: "bandpass", q: 6, frequency: { from: 700 * 1.15, to: 700 * 0.8, curve: "linear", over: "duration" }, level: 1 },
5480
+ { type: "bandpass", q: 8, frequency: { from: 1150 * 1.15, to: 1150 * 0.8, curve: "linear", over: "duration" }, level: 0.7 },
5481
+ { type: "bandpass", q: 10, frequency: { from: 2600 * 1.15, to: 2600 * 0.8, curve: "linear", over: "duration" }, level: 0.35 }
5482
+ ]
5483
+ },
5484
+ modulators: [{ target: "gain", wave: "square", rate: [31, 40], depth: 0.35 }],
5485
+ envelope: [
5486
+ [0, 0, "set"],
5487
+ [0.06, 0.9, "linear"],
5488
+ ["0.6d", 0.9, "set"],
5489
+ ["1d", 1e-3, "exp", "fixed"]
5490
+ ]
5491
+ }
5492
+ ],
5493
+ falloff: DISTANCE_CURVES.game_scream,
5494
+ sources: ["jogo:L62128-62142 grito"]
5495
+ };
5496
+ var groanLayer = (base) => ({
5497
+ source: {
5498
+ kind: "osc",
5499
+ wave: "sawtooth",
5500
+ frequency: typeof base === "number" ? { from: base, to: base * 0.7, curve: "linear", over: "duration" } : { from: base, to: { ...base, scale: 0.7 }, curve: "linear", over: "duration" }
5501
+ },
5502
+ filters: { mode: "serial", bank: [{ type: "bandpass", q: 3, frequency: { set: typeof base === "number" ? base * 3 : { ...base, scale: 3 } } }] },
5503
+ envelope: [
5504
+ [0, 0, "set"],
5505
+ [0.15, 0.25, "linear"],
5506
+ ["1d", 1e-3, "exp", "fixed"]
5507
+ ]
5508
+ });
5509
+ var GROAN = {
5510
+ id: "groan",
5511
+ ...voiced,
5512
+ order: "groan",
5513
+ vars: { base: [95, 135], dur: [0.9, 1.5] },
5514
+ duration: p("dur"),
5515
+ layers: [groanLayer(p("base"))],
5516
+ falloff: DISTANCE_CURVES.game_voice,
5517
+ sources: ["jogo:L62143-62155 voz gemido"]
5518
+ };
5519
+ var RUNNER_GROAN = {
5520
+ id: "runner_groan",
5521
+ ...voiced,
5522
+ order: "groan",
5523
+ vars: { dur: [0.9, 1.5] },
5524
+ duration: p("dur"),
5525
+ layers: [groanLayer(300)],
5526
+ falloff: DISTANCE_CURVES.game_voice,
5527
+ sources: ["jogo:L62143-62155 voz corredor"]
5528
+ };
5529
+ var scene = (id, layers, line, options = {}) => sceneRecipe(id, layers, line, options);
5530
+ var groan = (id, vars, voice, line) => scene(id, [sceneVoice(voice)], line, { vars, ...voiced });
5531
+ var GROAN_WALKER = groan("groan_walker", { f0: [72, 107], d: [0.9, 2], fA: [480, 680], fB: [1050, 1350] }, { f0: p("f0"), f1: 0, d: p("d"), fA: p("fA"), fB: p("fB"), noise: 0.35, vol: 0.65 }, "L1345 groan (walker)");
5532
+ var GROAN_BRUTE = groan("groan_brute", { f0: [48, 62], d: [0.9, 2], fA: [480, 680], fB: [1050, 1350] }, { f0: p("f0"), f1: 0, d: p("d"), fA: p("fA"), fB: p("fB"), noise: 0.35, vol: 0.65 }, "L1345 groan (brute)");
5533
+ var GROAN_RUNNER = groan("groan_runner", { f0: [160, 250], d: [0.45, 0.85] }, { f0: p("f0"), f1: 0, d: p("d"), fA: 950, fB: 2300, noise: 0.6, vol: 0.5, wave: "square" }, "L1345 groan (runner)");
5534
+ var SCREAM = scene(
5535
+ "scream",
5536
+ [
5537
+ sceneVoice({ f0: 520, f1: 880, d: 2.6, fA: 1800, fB: 3200, noise: 0.9, vol: 1.6 }),
5538
+ sceneVoice({ f0: 640, f1: 1020, d: 2.4, fA: 2400, fB: 3800, noise: 0.6, vol: 1.1, wave: "square" })
5539
+ ],
5540
+ "L1347 scream",
5541
+ { bus: "voices" }
5542
+ );
5543
+ var INHALE = scene("inhale", [sceneNoise({ type: "bandpass", f: 1400, q: 1.5, a: 0.6, d: 0.9, vol: 0.5 })], "L1348 inhale", { bus: "voices" });
5544
+ var LEAP = scene("leap", [sceneVoice({ f0: 200, f1: 330, d: 0.5, fA: 1100, fB: 2500, noise: 0.7, vol: 0.6, wave: "square" }), sceneNoise({ type: "bandpass", f: 600, f2: 1600, d: 0.5, vol: 0.4 })], "L1349 leap", { bus: "voices" });
5545
+ var GRAB = scene("grab", [sceneNoise({ type: "bandpass", f: 500, q: 1, d: 0.25, vol: 0.7 }), sceneTone({ wave: "sawtooth", f: 90, f2: 60, d: 0.4, vol: 0.4 })], "L1350 grab", { bus: "voices" });
5546
+ var BITE = scene(
5547
+ "bite",
5548
+ [
5549
+ sceneNoise({ f: 1400, d: 0.2, vol: 1.1 }),
5550
+ ...[0, 1, 2, 3].map((k) => sceneNoise({ type: "highpass", f: 2500, d: 0.03, vol: 0.5, delay: 0.03 + k * 0.04 })),
5551
+ sceneTone({ wave: "sawtooth", f: 320, f2: 170, d: 0.5, vol: 0.35, delay: 0.05 })
5552
+ ],
5553
+ "L1351 bite",
5554
+ { bus: "voices" }
5555
+ );
5556
+ var HURT = scene("hurt", [sceneTone({ wave: "sawtooth", f: 150, f2: 95, d: 0.22, vol: 0.3 }), sceneNoise({ f: 600, d: 0.1, vol: 0.4 })], "L1352 hurt", { bus: "voices" });
5557
+ var FLESH = scene("flesh", [sceneNoise({ f: 650, d: 0.08, vol: 0.6 }), sceneTone({ f: 90, f2: 45, d: 0.07, vol: 0.5 })], "L1353 flesh");
5558
+ var FLESH_HEAD = scene("flesh_head", [sceneNoise({ f: 1100, d: 0.12, vol: 0.9 }), sceneTone({ f: 130, f2: 45, d: 0.07, vol: 0.5 })], "L1353 flesh (head)");
5559
+ var SEVER = scene(
5560
+ "sever",
5561
+ [sceneNoise({ type: "bandpass", f: 500, q: 0.8, d: 0.3, vol: 1 }), sceneNoise({ type: "highpass", f: 2400, d: 0.06, vol: 0.7, delay: 0.02 }), sceneTone({ f: 110, f2: 40, d: 0.15, vol: 0.6 })],
5562
+ "L1355 sever"
5563
+ );
5564
+ var GORE = scene(
5565
+ "gore",
5566
+ [
5567
+ sceneNoise({ f: 900, f2: 200, d: 0.4, vol: 1.1, wet: 0.3 }),
5568
+ ...[0, 1, 2].map((k) => sceneNoise({ type: "bandpass", f: [400, 800], q: 2, d: 0.08, vol: 0.5, delay: 0.08 + k * 0.07 }))
5569
+ ],
5570
+ "L1356 gore"
5571
+ );
5572
+ var CHARACTER_SOUNDS = [
5573
+ SCREAM_HOARSE,
5574
+ GROAN,
5575
+ RUNNER_GROAN,
5576
+ GROAN_WALKER,
5577
+ GROAN_BRUTE,
5578
+ GROAN_RUNNER,
5579
+ SCREAM,
5580
+ INHALE,
5581
+ LEAP,
5582
+ GRAB,
5583
+ BITE,
5584
+ HURT,
5585
+ FLESH,
5586
+ FLESH_HEAD,
5587
+ SEVER,
5588
+ GORE
5589
+ ];
5590
+ var CHARACTER_SOUND_BANK = { id: "characters_three", sounds: CHARACTER_SOUNDS };
5591
+
5592
+ // src/audio/voices.json
5593
+ var voices_default = {
5594
+ sources: [
5595
+ "cena:L1345 groan(z): the Brutamonte has its own, a chase speed above 3.4 is a runner's, every other body groans like a walker",
5596
+ "cena:L1238, L1248, L1249 the scream and the inhale belong to the Gritador, the leap to the Saltador",
5597
+ "cena:L1226 the groan when a body wakes (FEIGN to CHASE) and the game L67275/L67328 voz('corredor') when one notices the player",
5598
+ "jogo:L66986 the groan of the game: 'corredor' for the runner category, 'gemido' for the rest; jogo:L66989 screamAt plays 'grito'",
5599
+ "types.json: the voice line of each Bestiario type"
5600
+ ],
5601
+ types: {
5602
+ walker: { origin: "team", groan: "groan_walker", gameGroan: "groan" },
5603
+ runner: { origin: "team", groan: "groan_runner", gameGroan: "runner_groan" },
5604
+ crawler: { origin: "rule", groan: "groan_walker", gameGroan: "groan" },
5605
+ brute: { origin: "team", groan: "groan_brute", gameGroan: "groan" },
5606
+ sniffer: {
5607
+ origin: "extra",
5608
+ groan: "groan_runner",
5609
+ gameGroan: "groan",
5610
+ idle: [{ sound: "inhale" }, { sound: "groan_runner", delay: 0.6 }]
5611
+ },
5612
+ leaper: { origin: "team", groan: "groan_runner", gameGroan: "groan", special: [{ sound: "leap" }] },
5613
+ bloater: { origin: "rule", groan: "groan_walker", gameGroan: "groan" },
5614
+ stalker: { origin: "rule", groan: "groan_runner", gameGroan: "groan", idle: [] },
5615
+ blind: { origin: "rule", groan: "groan_walker", gameGroan: "groan" },
5616
+ screamer: {
5617
+ origin: "team",
5618
+ groan: "groan_walker",
5619
+ gameGroan: "groan",
5620
+ special: [{ sound: "inhale" }, { sound: "scream", delay: 1.5 }],
5621
+ gameSpecial: [{ sound: "scream_hoarse" }]
5622
+ },
5623
+ charred: { origin: "rule", groan: "groan_walker", gameGroan: "groan" },
5624
+ drowned: { origin: "rule", groan: "groan_walker", gameGroan: "groan" },
5625
+ devourer: {
5626
+ origin: "extra",
5627
+ groan: "groan_walker",
5628
+ gameGroan: "groan",
5629
+ idle: [{ sound: "groan_walker" }, { sound: "bite", delay: 0.4 }]
5630
+ }
5631
+ }
5632
+ };
5633
+
5634
+ // src/audio/sound.ts
5635
+ var DATA = voices_default;
5636
+ var UNDEAD_VOICES = DATA.types;
5637
+ var UNDEAD_VOICE_SOURCES = DATA.sources;
5638
+ var BODY_SOUNDS = {
5639
+ art_scene: {
5640
+ wound: { sound: "flesh", head: "flesh_head" },
5641
+ severed: { sound: "sever" },
5642
+ destroyed: { sound: "gore" },
5643
+ split: { sound: "gore" }
5644
+ },
5645
+ game: {
5646
+ wound: { sound: "sfx_flesh" }
5647
+ }
5648
+ };
5649
+ var PLAYER_SOUNDS = { grab: "grab", bite: "bite", hurt: "hurt" };
5650
+ var HEAD_PARTS = /* @__PURE__ */ new Set(["head", "jaw"]);
5651
+ function createCharacterSound(sink, options = {}) {
5652
+ sink.register?.(CHARACTER_SOUND_BANK);
5653
+ sink.register?.(GAME_SFX_BANK);
5654
+ const style = options.style ?? "art_scene";
5655
+ const voices = { ...UNDEAD_VOICES, ...options.voices };
5656
+ const rules = { ...BODY_SOUNDS.art_scene, ...style === "game" ? BODY_SOUNDS.game : {}, ...options.body };
5657
+ const queue = [];
5658
+ const voiceOf = (typeId) => {
5659
+ const own = voices[typeId];
5660
+ if (own === null) return void 0;
5661
+ return own ?? (options.fallbackType ? voices[options.fallbackType] ?? void 0 : void 0);
5662
+ };
5663
+ const stepsOf = (typeId, moment) => {
5664
+ const voice = voiceOf(typeId);
5665
+ if (!voice) return [];
5666
+ const groan2 = style === "game" ? voice.gameGroan : voice.groan;
5667
+ if (moment === "special") return (style === "game" ? voice.gameSpecial ?? voice.special : voice.special) ?? [];
5668
+ if (moment === "alert") return voice.alert ?? [{ sound: style === "game" ? "runner_groan" : groan2 }];
5669
+ return voice.idle ?? [{ sound: groan2 }];
5670
+ };
5671
+ const start = (sound, at, volume) => void sink.play({ sound, position: at, volume });
5672
+ return {
5673
+ stepsOf,
5674
+ voice(typeId, moment, position, volume = 1) {
5675
+ const steps = stepsOf(typeId, moment);
5676
+ for (const step of steps) {
5677
+ if (step.delay && step.delay > 0) queue.push({ sound: step.sound, at: position, volume, wait: step.delay });
5678
+ else start(step.sound, position, volume);
5679
+ }
5680
+ return steps.length;
5681
+ },
5682
+ body(event, position, volume = 1) {
5683
+ if (event.type !== "body") return false;
5684
+ const rule = rules[event.change];
5685
+ if (!rule) return false;
5686
+ const head = event.part !== void 0 && HEAD_PARTS.has(event.part);
5687
+ start(head && rule.head ? rule.head : rule.sound, position, volume);
5688
+ return true;
5689
+ },
5690
+ player: (kind, volume = 1) => void sink.play({ sound: PLAYER_SOUNDS[kind], volume }),
5691
+ update(dt) {
5692
+ for (let i = queue.length - 1; i >= 0; i--) {
5693
+ const item = queue[i];
5694
+ item.wait -= dt;
5695
+ if (item.wait <= 0) {
5696
+ queue.splice(i, 1);
5697
+ start(item.sound, item.at, item.volume);
5698
+ }
5699
+ }
5700
+ },
5701
+ pending: () => queue.length,
5702
+ dispose() {
5703
+ queue.length = 0;
5704
+ }
5705
+ };
5706
+ }
5707
+
5214
5708
  // src/test-area/crowd.ts
5215
5709
  var FIELD = { x0: 12, x1: 30, z0: -16, z1: 16 };
5216
5710
  var STRIDE = 1;
@@ -5475,7 +5969,7 @@ var GameCharactersThree = (() => {
5475
5969
  hit_preset: "What the Fire hit button does to the body you look at",
5476
5970
  protection: "Worn by the bodies created from now on"
5477
5971
  },
5478
- actions: { heal: "Heal the aimed body", kill: "Kill the aimed body", infect: "Bite the aimed person (accelerated)", spawn_100: "Spawn 100 in the crowd", spawn_1000: "Spawn 1000 in the crowd", fire_preset: "Fire the hit" },
5972
+ actions: { heal: "Heal the aimed body", kill: "Kill the aimed body", infect: "Bite the aimed person (accelerated)", spawn_100: "Spawn 100 in the crowd", spawn_1000: "Spawn 1000 in the crowd", fire_preset: "Fire the hit", voice: "Make the aimed zombie speak (groan, by the moment)", special_call: "Make the aimed zombie use its call (scream, leap)" },
5479
5973
  options: {
5480
5974
  penetration: { physical: "Physical", "team-game": "Team game" },
5481
5975
  death_rule: { "vitality-or-head": "Vitality or head", "head-only": "Head only" },
@@ -5520,7 +6014,7 @@ var GameCharactersThree = (() => {
5520
6014
  hit_preset: "O que o bot\xE3o Disparar faz no corpo para o qual voc\xEA olha",
5521
6015
  protection: "Vestida pelos corpos criados daqui em diante"
5522
6016
  },
5523
- actions: { heal: "Curar o corpo mirado", kill: "Matar o corpo mirado", infect: "Morder a pessoa mirada (acelerado)", spawn_100: "P\xF4r 100 na multid\xE3o", spawn_1000: "P\xF4r 1000 na multid\xE3o", fire_preset: "Disparar o golpe" },
6017
+ actions: { heal: "Curar o corpo mirado", kill: "Matar o corpo mirado", infect: "Morder a pessoa mirada (acelerado)", spawn_100: "P\xF4r 100 na multid\xE3o", spawn_1000: "P\xF4r 1000 na multid\xE3o", fire_preset: "Disparar o golpe", voice: "Fazer o zumbi mirado falar (gemido, pelo momento)", special_call: "Fazer o zumbi mirado usar o chamado (grito, salto)" },
5524
6018
  options: {
5525
6019
  penetration: { physical: "F\xEDsica", "team-game": "Do jogo" },
5526
6020
  death_rule: { "vitality-or-head": "Vitalidade ou cabe\xE7a", "head-only": "S\xF3 a cabe\xE7a" },
@@ -5575,6 +6069,7 @@ var GameCharactersThree = (() => {
5575
6069
  let best = null;
5576
6070
  let people = null;
5577
6071
  let wounds = null;
6072
+ let sound = null;
5578
6073
  let crowd = null;
5579
6074
  let selected = null;
5580
6075
  let time = 0;
@@ -5667,6 +6162,9 @@ var GameCharactersThree = (() => {
5667
6162
  if (Math.abs(scale - 1) > 0.01) object.scale.setScalar(scale);
5668
6163
  const entry = { id, kind: made.kind, cardId: made.cardId, root: object, body: woundBodyOf(object), eyes: three ? findEyes(three, object) : null, built: made.built, character, walk: 0, tilt: 0 };
5669
6164
  entry.character = wrap(entry, character);
6165
+ const age = made.kind === "zombie" ? undead?.traitsOf(character)?.age : void 0;
6166
+ const blood = age !== void 0 ? options.bloodOf?.(age) : void 0;
6167
+ if (entry.body && blood) entry.body.blood = blood;
5670
6168
  entries.set(id, entry);
5671
6169
  selected ?? (selected = entry);
5672
6170
  trackEntry(entry);
@@ -5749,8 +6247,8 @@ var GameCharactersThree = (() => {
5749
6247
  const selectedOrFirst = () => selected ?? entries.values().next().value ?? null;
5750
6248
  const fireOn = (entry) => {
5751
6249
  const preset2 = hitPresetOf(settings.hit);
5752
- const p = entry.root.position;
5753
- entry.character.hit(hitFor(preset2, [p.x, p.y, p.z], 1.3, kit.calibers));
6250
+ const p2 = entry.root.position;
6251
+ entry.character.hit(hitFor(preset2, [p2.x, p2.y, p2.z], 1.3, kit.calibers));
5754
6252
  };
5755
6253
  const ensureCrowd = () => {
5756
6254
  if (crowd) return crowd;
@@ -5785,7 +6283,14 @@ var GameCharactersThree = (() => {
5785
6283
  if (infection.reanimated) target.character = wrap(target, undead.turn(target.character));
5786
6284
  }
5787
6285
  } else if (change.id === "fire_preset" && target) fireOn(target);
5788
- else if (change.id === "spawn_100" || change.id === "spawn_1000") {
6286
+ else if ((change.id === "voice" || change.id === "special_call") && sound) {
6287
+ const speaker = target?.kind === "zombie" ? target : [...entries.values()].find((entry) => entry.kind === "zombie");
6288
+ if (speaker) {
6289
+ const at = speaker.root.getWorldPosition(new host.THREE.Vector3());
6290
+ const hurried = settings.moment === "alert" || settings.moment === "chase" || settings.moment === "sprint";
6291
+ sound.voice(speaker.cardId, change.id === "special_call" ? "special" : hurried ? "alert" : "idle", [at.x, at.y + 1.5, at.z]);
6292
+ }
6293
+ } else if (change.id === "spawn_100" || change.id === "spawn_1000") {
5789
6294
  ensureBuilders(host?.THREE ?? three);
5790
6295
  ensureCrowd().spawn(change.id === "spawn_100" ? 100 : 1e3);
5791
6296
  }
@@ -5890,6 +6395,8 @@ var GameCharactersThree = (() => {
5890
6395
  withHint(choice("hit_preset", HIT_PRESETS.map((preset2) => preset2.id), "9x19"), text.hints.hit_preset),
5891
6396
  withHint(choice("protection", ["none", "vest_police", "vest_military", "helmet_combat", "turnout_gear", "vest_helmet"], "none"), text.hints.protection),
5892
6397
  { kind: "action", id: "fire_preset", name: text.actions.fire_preset },
6398
+ { kind: "action", id: "voice", name: text.actions.voice },
6399
+ { kind: "action", id: "special_call", name: text.actions.special_call },
5893
6400
  { kind: "action", id: "heal", name: text.actions.heal },
5894
6401
  { kind: "action", id: "kill", name: text.actions.kill },
5895
6402
  { kind: "action", id: "infect", name: text.actions.infect },
@@ -5917,9 +6424,10 @@ var GameCharactersThree = (() => {
5917
6424
  attach(lent) {
5918
6425
  host = lent;
5919
6426
  ensureBuilders(lent.THREE);
5920
- const scene = lent.scene;
5921
- const bridge = options.fluids ? createFluidsBridge({ sink: options.fluids }) : void 0;
5922
- wounds = createWounds(lent.THREE, { random, scene, groundAt: (x, z) => lent.groundAt(x, z), ...bridge ? { bridge } : {} });
6427
+ const scene2 = lent.scene;
6428
+ const bridge = options.fluids ? createFluidsBridge({ sink: options.fluids, ...options.film ? { film: options.film } : {} }) : void 0;
6429
+ sound = lent.audio && options.sound !== false ? createCharacterSound(lent.audio, options.sound ?? {}) : null;
6430
+ wounds = createWounds(lent.THREE, { random, scene: scene2, groundAt: (x, z) => lent.groundAt(x, z), ...bridge ? { bridge } : {}, ...sound ? { sound } : {} });
5923
6431
  for (const entry of entries.values()) trackEntry(entry);
5924
6432
  const stopChange = lent.bench?.onChange(onChange);
5925
6433
  const stopFrame = lent.onFrame((dt) => {
@@ -5932,6 +6440,7 @@ var GameCharactersThree = (() => {
5932
6440
  animate(entry, dt);
5933
6441
  }
5934
6442
  wounds?.update(dt);
6443
+ sound?.update(dt);
5935
6444
  crowd?.update(dt, lent.camera);
5936
6445
  });
5937
6446
  return () => {
@@ -5940,6 +6449,8 @@ var GameCharactersThree = (() => {
5940
6449
  clearOverlays();
5941
6450
  wounds?.dispose();
5942
6451
  wounds = null;
6452
+ sound?.dispose();
6453
+ sound = null;
5943
6454
  if (crowd) {
5944
6455
  crowd.group.parent?.remove(crowd.group);
5945
6456
  crowd = null;