@adatechnology/game-animals-three 0.3.7 → 0.4.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.
@@ -0,0 +1,1258 @@
1
+ // src/test-area/module.ts
2
+ import { discardObject3D } from "@adatechnology/game-shapes-three";
3
+ import { lineUpLength, visualStateOf as visualStateOf2 } from "@adatechnology/game-animals";
4
+ import { createStaticBatcher } from "@adatechnology/game-render-three";
5
+
6
+ // src/audio/recipes.ts
7
+ import { DISTANCE_CURVES } from "@adatechnology/game-audio";
8
+ var call = (o) => ({
9
+ id: o.id,
10
+ bus: "world",
11
+ order: "calls",
12
+ duration: o.seconds,
13
+ tail: 0.02,
14
+ // the page makes its gain and filter before it looks at the level
15
+ keepSilent: true,
16
+ layers: [
17
+ {
18
+ repeat: { count: o.count, spacing: o.spacing },
19
+ source: { kind: "osc", wave: o.wave, frequency: { from: o.frequency, to: o.frequency * o.drop, curve: "exp", over: "duration" } },
20
+ filters: { mode: "serial", bank: [{ type: "bandpass", frequency: { set: o.frequency * 1.2 }, q: 3 }] },
21
+ envelope: [
22
+ [0, 1e-4, "set", "fixed"],
23
+ [0.015, 1, "exp"],
24
+ ["1d", 1e-4, "exp", "fixed"]
25
+ ]
26
+ }
27
+ ],
28
+ falloff: DISTANCE_CURVES.animals,
29
+ sources: [`jogo:L66625-66639 bichoSom('${o.team}')`]
30
+ });
31
+ var DOG_BARK = call({ id: "dog_bark", team: "latido", count: 2, spacing: 0.22, seconds: 0.12, wave: "square", frequency: 520, drop: 0.6 });
32
+ var LAPWING_CALL = call({ id: "lapwing_call", team: "queroquero", count: 3, spacing: 0.14, seconds: 0.09, wave: "square", frequency: 2400, drop: 0.6 });
33
+ var HEN_CLUCK = call({ id: "hen_cluck", team: "cacarejo", count: 4, spacing: 0.14, seconds: 0.09, wave: "square", frequency: 900, drop: 0.6 });
34
+ var COW_MOO = call({ id: "cow_moo", team: "mugido", count: 1, spacing: 0.14, seconds: 1.2, wave: "sawtooth", frequency: 140, drop: 0.8 });
35
+ var ANIMAL_SOUNDS = [DOG_BARK, LAPWING_CALL, HEN_CLUCK, COW_MOO];
36
+ var ANIMAL_SOUND_BANK = { id: "animals_three", sounds: ANIMAL_SOUNDS };
37
+
38
+ // src/audio/calls.json
39
+ var calls_default = {
40
+ sources: [
41
+ "jogo:L66677 galinha: cacarejo, 6 s between calls, noise 30 m",
42
+ "jogo:L66680 quero-quero: queroquero when the player is near (alarme), 8 s, noise 40 m",
43
+ "jogo:L66681 vaca: mugido, 30 s, noise 80 m",
44
+ "jogo:L66683 cachorro: latido when a zombie is within 12 m, 3 s, noise 60 m"
45
+ ],
46
+ species: {
47
+ chicken: { sound: "hen_cluck", cooldown: 6, noiseMetres: 30 },
48
+ southern_lapwing: { sound: "lapwing_call", cooldown: 8, noiseMetres: 40 },
49
+ cow: { sound: "cow_moo", cooldown: 30, noiseMetres: 80 },
50
+ dog: { sound: "dog_bark", cooldown: 3, noiseMetres: 60 }
51
+ }
52
+ };
53
+
54
+ // src/audio/sound.ts
55
+ var DATA = calls_default;
56
+ var ANIMAL_CALLS = DATA.species;
57
+ var ANIMAL_CALL_SOURCES = DATA.sources;
58
+ function createAnimalSound(sink, options = {}) {
59
+ sink.register?.(ANIMAL_SOUND_BANK);
60
+ const calls = { ...ANIMAL_CALLS, ...options.calls };
61
+ const waiting = /* @__PURE__ */ new Map();
62
+ const callOf = (speciesId) => calls[speciesId] ?? void 0;
63
+ return {
64
+ callOf,
65
+ call(speciesId, position, o = {}) {
66
+ const found = callOf(speciesId);
67
+ if (!found) return false;
68
+ const key = o.key ?? speciesId;
69
+ if ((waiting.get(key) ?? 0) > 0) return false;
70
+ waiting.set(key, found.cooldown);
71
+ sink.play({ sound: found.sound, position, ...o.volume === void 0 ? {} : { volume: o.volume } });
72
+ return true;
73
+ },
74
+ update(dt) {
75
+ for (const [key, seconds] of waiting) {
76
+ if (seconds - dt <= 0) waiting.delete(key);
77
+ else waiting.set(key, seconds - dt);
78
+ }
79
+ },
80
+ dispose: () => waiting.clear()
81
+ };
82
+ }
83
+
84
+ // src/animals-three.ts
85
+ import { GAIT_PHASES } from "@adatechnology/game-animals";
86
+ import { markKitOrigin } from "@adatechnology/game-contracts";
87
+ import { collectReferencedNodes, createNodeSkin } from "@adatechnology/game-props3d";
88
+ import { shapeGeometries as shapeGeometries2 } from "@adatechnology/game-shapes-three";
89
+
90
+ // src/animate-bird.ts
91
+ var { PI } = Math;
92
+ var animateBird = (rig, params) => {
93
+ const { time: t, delta: dt, clip } = params;
94
+ const species = rig.species;
95
+ const spec = species.body;
96
+ const isVulture = species.id === "vulture";
97
+ const isChicken = species.id === "chicken";
98
+ const isLapwing = species.id === "southern_lapwing";
99
+ const flying = clip === "run" && !isChicken;
100
+ const hz = clip === "run" && !flying ? species.runHz : clip === "walk" ? species.walkHz : 0;
101
+ rig.phase += dt * (flying ? isVulture ? 1.1 : 4.5 : hz) * 2 * PI;
102
+ const targetHeight = flying ? rig.size * 1.2 : 0;
103
+ rig.trunk.position.y += (targetHeight - rig.trunk.position.y) * Math.min(1, dt * 3);
104
+ rig.legs.forEach((leg, index) => {
105
+ if (flying) {
106
+ leg.hip.rotation.x = 1.2;
107
+ leg.knee.rotation.x = -1.6;
108
+ } else if (clip === "idle") {
109
+ leg.hip.rotation.x = 0;
110
+ leg.knee.rotation.x = 0;
111
+ } else {
112
+ const phase = rig.phase + index * PI;
113
+ leg.hip.rotation.x = -Math.sin(phase) * 0.45;
114
+ leg.knee.rotation.x = -Math.max(0, Math.cos(phase)) * 0.7;
115
+ }
116
+ leg.foot.rotation.x = -(leg.hip.rotation.x + leg.knee.rotation.x);
117
+ });
118
+ rig.wings.forEach((wing) => {
119
+ const { side, shoulder, elbow } = wing;
120
+ if (flying) {
121
+ const flap = isVulture ? 0.25 + Math.sin(rig.phase) * 0.18 : Math.sin(rig.phase) * 0.9;
122
+ shoulder.rotation.y += (0 - shoulder.rotation.y) * Math.min(1, dt * 6);
123
+ shoulder.rotation.z = side * flap;
124
+ elbow.rotation.z = side * (isVulture ? 0.08 : Math.sin(rig.phase - 0.6) * 0.4);
125
+ elbow.rotation.y = 0;
126
+ shoulder.scale.x += (1 - shoulder.scale.x) * Math.min(1, dt * 6);
127
+ } else {
128
+ const alert = isLapwing && clip === "idle" ? Math.max(0, Math.sin(t * 1.3)) * 0.9 : 0;
129
+ const flutter = isChicken && clip === "run" ? Math.abs(Math.sin(rig.phase * 1.5)) * 0.8 : 0;
130
+ shoulder.rotation.y += (side * PI / 2 * 0.97 - alert * side * 0.9 - shoulder.rotation.y) * Math.min(1, dt * 6);
131
+ shoulder.rotation.z = side * (-0.06 + flutter + alert * 0.6);
132
+ elbow.rotation.z = side * 0.04;
133
+ elbow.rotation.y = 0;
134
+ shoulder.scale.x += ((alert > 0.1 ? 1 : 0.58) - shoulder.scale.x) * Math.min(1, dt * 6);
135
+ }
136
+ });
137
+ const peck = isChicken && clip === "idle" ? Math.max(0, Math.sin(t * 1.6)) ** 8 * 1.2 : 0;
138
+ if (rig.neck) {
139
+ rig.neck.rotation.x = rig.neckAngle + (clip === "walk" && !isVulture ? Math.max(0, Math.sin(rig.phase * 2)) * 0.35 : 0) + peck + (flying ? 0.6 : 0);
140
+ rig.neck.rotation.y = clip === "idle" ? Math.sin(t * 0.9) * 0.5 : 0;
141
+ }
142
+ if (rig.jaw) rig.jaw.rotation.x = isLapwing && clip === "idle" ? Math.max(0, Math.sin(t * 1.3)) * Math.abs(Math.sin(t * 14)) * 0.5 : 0;
143
+ if (rig.tailRoot) rig.tailRoot.rotation.x = (spec.tailA || 0) + (flying ? 0.6 : 0);
144
+ };
145
+
146
+ // src/animate-quadruped.ts
147
+ var { PI: PI2 } = Math;
148
+ var animateQuadruped = (rig, params, gaits) => {
149
+ const { time: t, delta: dt, clip } = params;
150
+ const species = rig.species;
151
+ const spec = species.body;
152
+ if (rig.dead) {
153
+ rig.root.rotation.z += (PI2 / 2 * 0.97 - rig.root.rotation.z) * Math.min(1, dt * 3);
154
+ rig.root.position.y += (spec.R * (spec.sx || 1) * 0.85 - rig.root.position.y) * Math.min(1, dt * 3);
155
+ rig.legs.forEach((leg) => {
156
+ leg.hip.rotation.x = leg.angle1 + (leg.isFront ? -0.5 : 0.4);
157
+ leg.knee.rotation.x = 0.05;
158
+ leg.foot.rotation.x = 0;
159
+ });
160
+ if (rig.neck) rig.neck.rotation.x = rig.neckAngle + 0.35;
161
+ if (rig.jaw) rig.jaw.rotation.x = 0.25;
162
+ return;
163
+ }
164
+ const hz = (clip === "run" ? species.runHz : clip === "walk" ? species.walkHz : 0) * (rig.infected ? 1.2 : 1);
165
+ rig.phase += dt * hz * 2 * PI2;
166
+ const gait = clip === "run" ? species.gallop ? gaits.gallop : gaits.trot : gaits.walk;
167
+ const amplitude = clip === "run" ? 0.62 : 0.34;
168
+ const flex = clip === "run" ? 1.15 : 0.75;
169
+ rig.legs.forEach((leg, index) => {
170
+ if (clip === "idle") {
171
+ leg.hip.rotation.x = leg.angle1;
172
+ leg.knee.rotation.x = leg.angle2;
173
+ } else {
174
+ const phase = rig.phase + gait[index];
175
+ const limit = rig.infected && index === 1 ? 0.35 : 1;
176
+ leg.hip.rotation.x = leg.angle1 - Math.sin(phase) * amplitude * limit;
177
+ leg.knee.rotation.x = leg.angle2 + Math.max(0, Math.cos(phase)) * flex * limit;
178
+ }
179
+ leg.foot.rotation.x = -(leg.hip.rotation.x + leg.knee.rotation.x) * 0.85;
180
+ });
181
+ const bob = clip === "idle" ? 0 : Math.abs(Math.sin(rig.phase)) * (clip === "run" ? 0.05 : 0.018) * rig.size;
182
+ rig.trunk.position.y = bob * 0.6;
183
+ const bodyGroup = rig.bodyGroup;
184
+ if (bodyGroup) {
185
+ bodyGroup.rotation.x = clip === "run" && species.gallop ? Math.sin(rig.phase + PI2 * 0.4) * 0.07 : 0;
186
+ bodyGroup.scale.y = 1 + (clip === "idle" ? Math.sin(t * 2.2) * 0.012 : 0);
187
+ }
188
+ const low = rig.infected ? 0.35 : 0;
189
+ if (rig.neck) {
190
+ rig.neck.rotation.x = rig.neckAngle + low + (clip === "idle" ? Math.sin(t * 0.7) * 0.08 : Math.sin(rig.phase * 2) * 0.04);
191
+ rig.neck.rotation.y = clip === "idle" ? Math.sin(t * 0.45) * 0.35 : 0;
192
+ }
193
+ if (rig.head) rig.head.rotation.z = rig.infected ? Math.sin(t * 11) * Math.sin(t * 1.7) * 0.12 : 0;
194
+ if (rig.jaw) rig.jaw.rotation.x = rig.infected ? 0.25 + Math.max(0, Math.sin(t * 6)) * 0.25 : clip === "run" ? 0.12 : 0;
195
+ const wagging = species.id === "dog" && !rig.infected;
196
+ rig.tail.forEach((segment, index) => {
197
+ segment.rotation.z = Math.sin(t * (wagging ? 9 : 2.2) - index * 0.6) * (wagging ? 0.35 : 0.12);
198
+ });
199
+ };
200
+
201
+ // src/animate-snake.ts
202
+ var { PI: PI3 } = Math;
203
+ var bodyPath = (THREE, rig, params, spacing) => {
204
+ const { time: t, clip } = params;
205
+ const points = [];
206
+ for (let index = 0; index < rig.segmentCount; index++) {
207
+ let x;
208
+ let z;
209
+ let y = 0;
210
+ if (clip === "walk") {
211
+ z = 0.6 - index * spacing;
212
+ x = Math.sin(rig.phase * 2.2 - index * 0.42) * 0.08 * Math.min(1, index / 6 + 0.3);
213
+ } else {
214
+ const strike = clip === "run" ? Math.max(0, Math.sin(t * 2.2)) ** 6 : 0;
215
+ if (index < 9) {
216
+ const k = index / 9;
217
+ z = 0.08 + (1 - k) * 0.12 + strike * (1 - k) * 0.35;
218
+ y = (1 - k) * 0.13 * (1 - strike * 0.6);
219
+ x = Math.sin(k * PI3 * 1.6) * 0.05 * (1 - strike);
220
+ } else {
221
+ const angle = (index - 9) * 0.36;
222
+ const radius = 0.16 - (index - 9) * 45e-4;
223
+ x = Math.cos(angle) * radius;
224
+ z = Math.sin(angle) * radius - 0.05;
225
+ y = (index - 9) * 12e-4;
226
+ }
227
+ }
228
+ points.push(new THREE.Vector3(x, y, z));
229
+ }
230
+ return points;
231
+ };
232
+ var animateSnake = (THREE, rig, params) => {
233
+ const { time: t, delta: dt, clip } = params;
234
+ const spacing = rig.segmentLength * (params.split ? 1.5 : 1);
235
+ if (clip === "walk") rig.phase += dt * 2.4;
236
+ const points = bodyPath(THREE, rig, params, spacing);
237
+ const at = (index) => points[index];
238
+ for (let index = 0; index < rig.segmentCount; index++) {
239
+ const group = rig.segments[index].group;
240
+ group.position.copy(at(index));
241
+ const direction = index === 0 ? at(0).clone().sub(at(1)) : at(index - 1).clone().sub(at(index));
242
+ group.rotation.set(0, Math.atan2(direction.x, direction.z), 0);
243
+ group.rotateX(-Math.atan2(direction.y, Math.hypot(direction.x, direction.z)));
244
+ }
245
+ const head = rig.head;
246
+ const headDirection = at(0).clone().sub(at(1)).normalize();
247
+ head.position.copy(at(0)).addScaledVector(headDirection, 0.02);
248
+ head.rotation.set(0, Math.atan2(headDirection.x, headDirection.z), 0);
249
+ head.rotateX(-Math.atan2(headDirection.y, Math.hypot(headDirection.x, headDirection.z)) * 0.5);
250
+ const jaw = rig.jaw;
251
+ jaw.rotation.x = clip === "run" ? Math.max(0, Math.sin(t * 2.2)) ** 6 * 0.9 : 0;
252
+ const tongue = rig.tongue;
253
+ tongue.visible = Math.sin(t * 5) > 0.3;
254
+ tongue.scale.z = 1 + Math.sin(t * 20) * 0.3;
255
+ };
256
+
257
+ // src/bird.ts
258
+ import { birdSize } from "@adatechnology/game-animals";
259
+
260
+ // src/context.ts
261
+ var createStream = (seed) => {
262
+ let state = seed;
263
+ return () => {
264
+ state |= 0;
265
+ state = state + 1831565813 | 0;
266
+ let t = Math.imul(state ^ state >>> 15, 1 | state);
267
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
268
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
269
+ };
270
+ };
271
+ var addMesh = (THREE, parent, geometry, material, x, y, z, rx, ry, rz) => {
272
+ const mesh = new THREE.Mesh(geometry, material);
273
+ mesh.position.set(x || 0, y || 0, z || 0);
274
+ if (rx || ry || rz) mesh.rotation.set(rx || 0, ry || 0, rz || 0);
275
+ mesh.castShadow = true;
276
+ mesh.receiveShadow = true;
277
+ parent.add(mesh);
278
+ return mesh;
279
+ };
280
+ var createRigTools = (THREE, rig) => {
281
+ const tools = {
282
+ material(color, extras) {
283
+ const material = new THREE.MeshStandardMaterial(Object.assign({ color, roughness: 0.8, metalness: 0, flatShading: false }, extras || {}));
284
+ material.userData.base = material.color.clone();
285
+ rig.materials.push(material);
286
+ return material;
287
+ },
288
+ ball(parent, radius, material, x, y, z, sx, sy, sz) {
289
+ const mesh = addMesh(THREE, parent, new THREE.SphereGeometry(radius, 16, 12), material, x, y, z);
290
+ if (sx) mesh.scale.set(sx, sy, sz);
291
+ return mesh;
292
+ },
293
+ limb: (parent, radius1, radius2, length, material) => addMesh(THREE, parent, new THREE.CylinderGeometry(radius2, radius1, length, 14, 1).translate(0, -length / 2, 0), material),
294
+ joint(parent, radius, material, x, y, z) {
295
+ const joint = tools.ball(parent, radius, material, x, y, z);
296
+ joint.userData.joint = 1;
297
+ rig.joints.push(joint);
298
+ return joint;
299
+ },
300
+ pivot(parent, x, y, z, offset) {
301
+ const group = new THREE.Group();
302
+ group.position.set(x, y, z);
303
+ parent.add(group);
304
+ rig.pivots.push({ group, rest: group.position.clone(), offset: offset || new THREE.Vector3() });
305
+ return group;
306
+ },
307
+ eyes(head, radius, x, y, z) {
308
+ const eyeMaterial = tools.material(1118481, { roughness: 0.25 });
309
+ rig.eyeMaterial = eyeMaterial;
310
+ for (const side of [-1, 1]) rig.eyes.push(tools.ball(head, radius, eyeMaterial, side * x, y, z));
311
+ }
312
+ };
313
+ return tools;
314
+ };
315
+ var createRig = (THREE, species, options) => {
316
+ const rig = {
317
+ species,
318
+ root: new THREE.Group(),
319
+ trunk: new THREE.Group(),
320
+ materials: [],
321
+ joints: [],
322
+ pivots: [],
323
+ legs: [],
324
+ tail: [],
325
+ ears: [],
326
+ infectionParts: [],
327
+ eyes: [],
328
+ wings: [],
329
+ segments: [],
330
+ phase: options.phase,
331
+ size: 1,
332
+ segmentLength: 0,
333
+ segmentCount: 0,
334
+ home: new THREE.Vector3(),
335
+ infected: false,
336
+ dead: false,
337
+ neckAngle: 0,
338
+ headAngle: 0,
339
+ triangles: 0
340
+ };
341
+ rig.root.add(rig.trunk);
342
+ return rig;
343
+ };
344
+
345
+ // src/bird.ts
346
+ var buildWings = (THREE, rig, spec, parts) => {
347
+ const tools = createRigTools(THREE, rig);
348
+ const { bodyGroup, main, dark } = parts;
349
+ for (const side of [-1, 1]) {
350
+ const shoulder = tools.pivot(bodyGroup, side * spec.r * spec.s[0] * 1.02, spec.r * 0.22, spec.r * 0.3, new THREE.Vector3(side * 0.12, 0.03, 0).multiplyScalar(rig.size));
351
+ tools.joint(shoulder, spec.r * 0.14, main, 0, 0, 0);
352
+ tools.ball(shoulder, 0.5, spec.wingM ? tools.material(spec.wingM) : dark, side * spec.wi * 0.5, 0, -spec.wc * 0.3, spec.wi * 1.05, spec.r * 0.16, spec.wc);
353
+ const elbow = tools.pivot(shoulder, side * spec.wi, 0, 0, new THREE.Vector3(side * 0.06, 0, 0).multiplyScalar(rig.size));
354
+ tools.joint(elbow, spec.r * 0.1, main, 0, 0, 0);
355
+ tools.ball(elbow, 0.5, dark, side * spec.wo * 0.45, 0, -spec.wc * 0.32, spec.wo * 1.05, spec.r * 0.12, spec.wc * 0.75);
356
+ if (spec.fingers) {
357
+ for (let finger = 0; finger < 4; finger++) {
358
+ addMesh(THREE, elbow, new THREE.BoxGeometry(spec.wo * 0.35, spec.r * 0.03, spec.wc * 0.12), dark, side * (spec.wo + spec.wo * 0.12), 0, -spec.wc * 0.05 - finger * spec.wc * 0.18, 0, side * (0.05 + finger * 0.12), 0);
359
+ }
360
+ }
361
+ if (spec.spur) addMesh(THREE, elbow, new THREE.ConeGeometry(spec.r * 0.06, spec.r * 0.25, 5), tools.material(12595242), 0, 0, spec.wc * 0.2, Math.PI / 2, 0, 0);
362
+ rig.wings.push({ shoulder, elbow, side });
363
+ }
364
+ };
365
+ var buildLegs = (THREE, rig, spec, parts) => {
366
+ const tools = createRigTools(THREE, rig);
367
+ const { bodyGroup, main, leg } = parts;
368
+ for (const side of [-1, 1]) {
369
+ const hip = tools.pivot(bodyGroup, side * spec.r * 0.38, -spec.r * 0.55, spec.r * 0.05, new THREE.Vector3(side * 0.06, -0.05, 0).multiplyScalar(rig.size));
370
+ tools.joint(hip, spec.r * 0.18, main, 0, 0, 0);
371
+ tools.limb(hip, spec.r * 0.16, spec.r * 0.09, spec.shank, spec.featherLeg ? main : leg);
372
+ const ankle = tools.pivot(hip, 0, -spec.shank, 0, new THREE.Vector3(0, -0.04, 0).multiplyScalar(rig.size));
373
+ tools.joint(ankle, spec.r * 0.06, leg, 0, 0, 0);
374
+ tools.limb(ankle, spec.r * 0.055, spec.r * 0.05, spec.tars, leg);
375
+ const foot = tools.pivot(ankle, 0, -spec.tars, 0);
376
+ for (const angle of [-0.45, 0, 0.45]) addMesh(THREE, foot, new THREE.BoxGeometry(spec.r * 0.05, spec.r * 0.04, spec.r * 0.55).translate(0, 0, spec.r * 0.25), leg, 0, -spec.r * 0.02, 0, 0, angle, 0);
377
+ addMesh(THREE, foot, new THREE.BoxGeometry(spec.r * 0.05, spec.r * 0.04, spec.r * 0.25).translate(0, 0, -spec.r * 0.12), leg, 0, -spec.r * 0.02, 0);
378
+ rig.legs.push({ hip, knee: ankle, foot, isFront: false, angle1: 0.35, angle2: -0.5, side });
379
+ }
380
+ };
381
+ var buildBird = (THREE, rig, options) => {
382
+ const spec = rig.species.body;
383
+ const tools = createRigTools(THREE, rig);
384
+ const colors = spec.c;
385
+ const main = tools.material(colors.main);
386
+ const dark = tools.material(colors.dark);
387
+ const leg = tools.material(colors.leg);
388
+ const beak = tools.material(colors.beak, { roughness: 0.5 });
389
+ const belly = tools.material(colors.belly || colors.main);
390
+ rig.main = main;
391
+ const hipY = spec.tars + spec.shank * 0.9;
392
+ rig.size = birdSize(spec);
393
+ const bodyGroup = tools.pivot(rig.trunk, 0, hipY + spec.r * 0.55, 0);
394
+ rig.bodyGroup = bodyGroup;
395
+ bodyGroup.rotation.x = spec.pitch || 0;
396
+ tools.ball(bodyGroup, spec.r, main, 0, 0, 0, spec.s[0], spec.s[1], spec.s[2]);
397
+ tools.ball(bodyGroup, spec.r * 0.9, belly, 0, -spec.r * 0.22, spec.r * 0.15, spec.s[0] * 0.9, spec.s[1] * 0.8, spec.s[2] * 0.85);
398
+ const tail = tools.pivot(bodyGroup, 0, spec.r * 0.15, -spec.r * spec.s[2] * 0.85, new THREE.Vector3(0, 0, -0.08).multiplyScalar(rig.size));
399
+ tail.rotation.x = spec.tailA || -0.3;
400
+ rig.tailRoot = tail;
401
+ addMesh(THREE, tail, new THREE.BoxGeometry(spec.tailW, spec.r * 0.08, spec.tailL).translate(0, 0, -spec.tailL / 2), spec.tailM ? tools.material(spec.tailM) : dark);
402
+ buildWings(THREE, rig, spec, { bodyGroup, main, dark });
403
+ const neck = tools.pivot(bodyGroup, 0, spec.r * 0.45, spec.r * spec.s[2] * 0.75, new THREE.Vector3(0, 0.05, 0.08).multiplyScalar(rig.size));
404
+ neck.rotation.x = spec.neckA;
405
+ rig.neck = neck;
406
+ rig.neckAngle = spec.neckA;
407
+ tools.joint(neck, spec.nr * 1.1, spec.neckM ? tools.material(spec.neckM) : main, 0, 0, 0);
408
+ addMesh(THREE, neck, new THREE.CylinderGeometry(spec.nr * 0.85, spec.nr, spec.nl, 12).translate(0, spec.nl / 2, 0), spec.neckM ? tools.material(spec.neckM) : main);
409
+ const head = tools.pivot(neck, 0, spec.nl, 0, new THREE.Vector3(0, 0.05, 0.05).multiplyScalar(rig.size));
410
+ head.rotation.x = -spec.neckA + (spec.headA || 0);
411
+ rig.head = head;
412
+ rig.headAngle = head.rotation.x;
413
+ const headMaterial = spec.headM ? tools.material(spec.headM) : main;
414
+ tools.ball(head, spec.hr, headMaterial, 0, 0, 0, spec.hs ? spec.hs[0] : 1, spec.hs ? spec.hs[1] : 1, spec.hs ? spec.hs[2] : 1.1);
415
+ addMesh(THREE, head, new THREE.ConeGeometry(spec.br, spec.bl, 8).rotateX(Math.PI / 2).translate(0, 0, spec.bl / 2), beak, 0, -spec.hr * 0.1, spec.hr * 0.85, spec.hook || 0, 0, 0);
416
+ const jaw = tools.pivot(head, 0, -spec.hr * 0.25, spec.hr * 0.8);
417
+ rig.jaw = jaw;
418
+ addMesh(THREE, jaw, new THREE.ConeGeometry(spec.br * 0.7, spec.bl * 0.85, 8).rotateX(Math.PI / 2).translate(0, 0, spec.bl * 0.42), beak).scale.set(1, 0.5, 1);
419
+ tools.eyes(head, spec.hr * 0.17, spec.hr * 0.68, spec.hr * 0.2, spec.hr * 0.35);
420
+ if (spec.eyeC && rig.eyeMaterial) {
421
+ rig.eyeMaterial.color.set(spec.eyeC);
422
+ rig.eyeMaterial.userData.base = rig.eyeMaterial.color.clone();
423
+ }
424
+ options.extra?.({ THREE, rig, tools }, { bodyGroup, head, neck, main, dark });
425
+ buildLegs(THREE, rig, spec, { bodyGroup, main, leg });
426
+ return rig;
427
+ };
428
+
429
+ // src/extras.ts
430
+ var cow = ({ THREE, tools }, k) => {
431
+ tools.ball(k.bodyGroup, 0.2, k.main, 0, k.Rb * 1.1, k.L * 0.33, 0.95, 1.1, 1.1);
432
+ tools.ball(k.neck, 0.13, k.main, 0, 0.12, 0.08, 0.55, 1.4, 1.05);
433
+ for (const side of [-1, 1]) addMesh(THREE, k.head, new THREE.ConeGeometry(0.025, 0.13, 8), tools.material(13616808), side * 0.12, k.hd.r * 1.05, k.hd.r * 0.05, 0, 0, side * -0.9);
434
+ };
435
+ var horse = ({ THREE, rig, tools }, k) => {
436
+ for (let index = 0; index < 9; index++) addMesh(THREE, k.neck, new THREE.BoxGeometry(0.035, 0.16, 0.08), k.dark, 0, 0.06 + index * 0.065, -0.12 + index * 4e-3, -0.25, 0, 0);
437
+ addMesh(THREE, k.head, new THREE.BoxGeometry(0.03, 0.12, 0.06), k.dark, 0, k.hd.r * 1.05, k.hd.r * 0.3, 0.6, 0, 0);
438
+ const last = rig.tail[rig.tail.length - 1];
439
+ tools.ball(last, 0.08, k.dark, 0, -0.42, 0, 0.7, 4.5, 0.7);
440
+ };
441
+ var wildBoar = ({ THREE, tools }, k) => {
442
+ for (let index = 0; index < 12; index++) addMesh(THREE, k.bodyGroup, new THREE.BoxGeometry(0.03, 0.1, 0.06), k.dark, 0, k.Rb * 1.12 - index * 4e-3, k.L * 0.38 - index * 0.06, -0.2, 0, 0);
443
+ for (const side of [-1, 1])
444
+ addMesh(THREE, k.head, new THREE.ConeGeometry(0.012, 0.09, 6), tools.material(15261900), side * 0.07, k.hd.r * -0.25 + 0.03, k.hd.r * 0.25 + k.hd.r * k.hd.s[2] * 0.55 + 0.13, -0.6, 0, side * 0.5);
445
+ };
446
+ var chicken = ({ tools }, k) => {
447
+ const red = tools.material(12593182);
448
+ for (let index = 0; index < 4; index++) tools.ball(k.head, 0.014, red, 0, 0.045, -0.012 + index * 0.014, 0.5, 1, 0.8);
449
+ tools.ball(k.head, 0.012, red, 0, -0.04, 0.04, 0.5, 1.3, 0.7);
450
+ };
451
+ var southernLapwing = ({ THREE, tools }, k) => {
452
+ const black = tools.material(1447446);
453
+ tools.ball(k.bodyGroup, 0.05, black, 0, 0.03, 0.08, 1.1, 0.9, 0.6);
454
+ addMesh(THREE, k.head, new THREE.BoxGeometry(6e-3, 6e-3, 0.06).translate(0, 0, -0.03), black, 0, 0.025, -0.01, 0.5, 0, 0);
455
+ tools.ball(k.head, 0.026, black, 0, -5e-3, 0.02, 1.05, 0.7, 0.9);
456
+ };
457
+ var QUADRUPED_EXTRAS = { cow, horse, wild_boar: wildBoar };
458
+ var BIRD_EXTRAS = { chicken, southern_lapwing: southernLapwing };
459
+
460
+ // src/quadruped.ts
461
+ import { quadrupedLayout } from "@adatechnology/game-animals";
462
+
463
+ // src/textures.ts
464
+ var canvasTexture = (THREE, document, width, height, draw) => {
465
+ const canvas = document.createElement("canvas");
466
+ canvas.width = width;
467
+ canvas.height = height;
468
+ draw(canvas.getContext("2d"), width, height);
469
+ const texture = new THREE.CanvasTexture(canvas);
470
+ texture.colorSpace = THREE.SRGBColorSpace;
471
+ return texture;
472
+ };
473
+ var spotsTexture = (THREE, document, base, spot) => canvasTexture(THREE, document, 256, 256, (painter, width, height) => {
474
+ painter.fillStyle = base;
475
+ painter.fillRect(0, 0, width, height);
476
+ const next = createStream(11);
477
+ for (let index = 0; index < 70; index++) {
478
+ const x = next() * width;
479
+ const y = next() * height;
480
+ const radius = 5 + next() * 7;
481
+ painter.strokeStyle = spot;
482
+ painter.lineWidth = 3;
483
+ painter.beginPath();
484
+ painter.arc(x, y, radius, 0, 7);
485
+ painter.stroke();
486
+ painter.fillStyle = "rgba(120,70,20,.45)";
487
+ painter.beginPath();
488
+ painter.arc(x, y, radius * 0.6, 0, 7);
489
+ painter.fill();
490
+ }
491
+ for (let index = 0; index < 60; index++) {
492
+ painter.fillStyle = spot;
493
+ painter.beginPath();
494
+ painter.arc(next() * width, next() * height, 1.5 + next() * 2.5, 0, 7);
495
+ painter.fill();
496
+ }
497
+ });
498
+ var stripesTexture = (THREE, document, base, stripe) => canvasTexture(THREE, document, 128, 128, (painter, width) => {
499
+ painter.fillStyle = base;
500
+ painter.fillRect(0, 0, width, 128);
501
+ painter.fillStyle = stripe;
502
+ for (let index = 0; index < 9; index++) painter.fillRect(0, index * 14 + index % 2 * 3, width, 4);
503
+ });
504
+ var createCoatTexture = (THREE, document, marker) => marker.kind === "stripes" ? stripesTexture(THREE, document, marker.base, marker.stripe) : spotsTexture(THREE, document, marker.base, marker.spot);
505
+
506
+ // src/profile.ts
507
+ import { shapeGeometries } from "@adatechnology/game-shapes-three";
508
+ var profileGeometry = (THREE, length, profile, segments) => {
509
+ const points = profile.map((radius, index) => new THREE.Vector2(index / (profile.length - 1), radius));
510
+ const curve = new THREE.SplineCurve(points);
511
+ const outline = [];
512
+ outline.push(new THREE.Vector2(1e-3, -length / 2 - 0));
513
+ for (let step = 0; step <= 30; step++) {
514
+ const point = curve.getPoint(step / 30);
515
+ outline.push(new THREE.Vector2(Math.max(1e-3, point.y), -length / 2 + point.x * length));
516
+ }
517
+ outline.push(new THREE.Vector2(1e-3, length / 2));
518
+ return new (shapeGeometries(THREE)).LatheGeometry(outline, segments || 22).rotateX(Math.PI / 2);
519
+ };
520
+
521
+ // src/quadruped.ts
522
+ var RIB_COUNT = 4;
523
+ var LEG_DEFINITIONS = [
524
+ [1, 1, "F"],
525
+ [-1, 1, "F"],
526
+ [1, -1, "H"],
527
+ [-1, -1, "H"]
528
+ ];
529
+ var buildBodyAndLegs = (THREE, rig, spec, main, belly, dark) => {
530
+ const tools = createRigTools(THREE, rig);
531
+ const { L, R: radius } = spec;
532
+ const leg = spec.leg;
533
+ const layout = quadrupedLayout(spec);
534
+ const { frontHipHeight, hindHipHeight, footHeight, bodyHeight, span } = layout;
535
+ const frontAngle1 = leg.fa1 ?? 0.06;
536
+ const frontAngle2 = leg.fa2 ?? -0.08;
537
+ const hindAngle1 = leg.ha1 ?? -0.32;
538
+ const hindAngle2 = leg.ha2 ?? 0.58;
539
+ rig.size = layout.size;
540
+ const bodyGroup = tools.pivot(rig.trunk, 0, bodyHeight, 0);
541
+ rig.bodyGroup = bodyGroup;
542
+ const body = addMesh(THREE, bodyGroup, profileGeometry(THREE, L, spec.prof.map((value) => value * radius)), main);
543
+ body.scale.set(spec.sx || 1, spec.sy || 1, 1);
544
+ body.rotation.x = Math.atan2(hindHipHeight - frontHipHeight, span) * 0.6;
545
+ rig.body = body;
546
+ const belt = addMesh(THREE, bodyGroup, profileGeometry(THREE, L * 0.8, spec.prof.map((value) => value * 0.8 * radius)), belly);
547
+ belt.scale.set((spec.sx || 1) * 0.92, (spec.sy || 1) * 0.85, 1);
548
+ belt.position.y = -radius * 0.2;
549
+ LEG_DEFINITIONS.forEach(([sx, sz, kind]) => {
550
+ const front = kind === "F";
551
+ const upper = front ? leg.fu : leg.hu;
552
+ const lower = front ? leg.fl : leg.hl;
553
+ const height = front ? frontHipHeight : hindHipHeight;
554
+ const hip = tools.pivot(rig.trunk, sx * radius * (spec.sx || 1) * 0.55, height, sz * span / 2, new THREE.Vector3(sx * 0.18, -0.06, sz * 0.12).multiplyScalar(rig.size));
555
+ tools.ball(hip, leg.r * (front ? 1.9 : 2.3), main, 0, -upper * 0.18, front ? 0 : -leg.r * 0.3, 0.75, 1.25, 1);
556
+ tools.joint(hip, leg.r * 1.15, main, 0, 0, 0);
557
+ tools.limb(hip, leg.r * 1.25, leg.r * 0.95, upper, main);
558
+ const knee = tools.pivot(hip, 0, -upper, 0, new THREE.Vector3(0, -0.07 * rig.size, 0));
559
+ tools.joint(knee, leg.r * 0.95, main, 0, 0, 0);
560
+ tools.limb(knee, leg.r * 0.92, leg.r * 0.68, lower, main);
561
+ const foot = tools.pivot(knee, 0, -lower, 0, new THREE.Vector3(0, -0.05 * rig.size, 0));
562
+ if (leg.hoof) {
563
+ tools.joint(foot, leg.r * 0.72, dark, 0, 0, 0);
564
+ addMesh(THREE, foot, new THREE.CylinderGeometry(leg.r * 0.8, leg.r * 1.05, footHeight, 12).translate(0, -footHeight / 2, 0), dark);
565
+ } else {
566
+ tools.joint(foot, leg.r * 0.66, main, 0, 0, 0);
567
+ tools.ball(foot, leg.r * 0.95, leg.dark ? dark : main, 0, -footHeight * 0.55, leg.r * 0.5, 1, 0.62, 1.45);
568
+ }
569
+ rig.legs.push({ hip, knee, foot, isFront: front, angle1: front ? frontAngle1 : hindAngle1, angle2: front ? frontAngle2 : hindAngle2, side: sx });
570
+ });
571
+ return { bodyGroup, bodyHeight };
572
+ };
573
+ var buildHead = (THREE, rig, spec, parts) => {
574
+ const tools = createRigTools(THREE, rig);
575
+ const { bodyGroup, main, belly, nose } = parts;
576
+ const { L, R: radius } = spec;
577
+ const neckSpec = spec.neck;
578
+ const headSpec = spec.head;
579
+ const neck = tools.pivot(bodyGroup, 0, radius * (spec.sy || 1) * (neckSpec.y ?? 0.35), L / 2 * (neckSpec.z ?? 0.78), new THREE.Vector3(0, 0.05, 0.12).multiplyScalar(rig.size));
580
+ neck.rotation.x = neckSpec.ang;
581
+ rig.neck = neck;
582
+ rig.neckAngle = neckSpec.ang;
583
+ tools.joint(neck, neckSpec.r * 1.05, main, 0, 0, 0);
584
+ addMesh(THREE, neck, new THREE.CylinderGeometry(neckSpec.r2 || neckSpec.r * 0.85, neckSpec.r, neckSpec.len, 14).translate(0, neckSpec.len / 2, 0), main);
585
+ const head = tools.pivot(neck, 0, neckSpec.len, 0, new THREE.Vector3(0, 0.06, 0.08).multiplyScalar(rig.size));
586
+ head.rotation.x = -neckSpec.ang + (headSpec.down || 0);
587
+ rig.head = head;
588
+ rig.headAngle = head.rotation.x;
589
+ tools.joint(head, neckSpec.r2 || neckSpec.r * 0.85, main, 0, 0, 0);
590
+ tools.ball(head, headSpec.r, main, 0, headSpec.r * 0.25, headSpec.r * 0.25, headSpec.s[0], headSpec.s[1], headSpec.s[2]);
591
+ const snoutZ = headSpec.r * 0.25 + headSpec.r * headSpec.s[2] * 0.55;
592
+ const snout = addMesh(THREE, head, new THREE.CylinderGeometry(headSpec.sr2, headSpec.sr1, headSpec.sl, 14).rotateX(Math.PI / 2), headSpec.muzzle ? tools.material(headSpec.muzzle) : main, 0, headSpec.r * (headSpec.sy ?? 0), snoutZ + headSpec.sl / 2);
593
+ snout.scale.set(1, headSpec.sq || 1, 1);
594
+ tools.ball(head, headSpec.sr2 * 1.02, nose, 0, headSpec.r * (headSpec.sy ?? 0) + headSpec.sr2 * 0.1, snoutZ + headSpec.sl, 1, 0.85 * (headSpec.sq || 1), 0.45);
595
+ const jaw = tools.pivot(head, 0, headSpec.r * (headSpec.sy ?? 0) - headSpec.sr1 * 0.55 * (headSpec.sq || 1), snoutZ - headSpec.sr1 * 0.2, new THREE.Vector3(0, -0.04, 0.04).multiplyScalar(rig.size));
596
+ rig.jaw = jaw;
597
+ addMesh(THREE, jaw, new THREE.CylinderGeometry(headSpec.sr2 * 0.7, headSpec.sr1 * 0.75, headSpec.sl * 0.92, 12).rotateX(Math.PI / 2).translate(0, 0, headSpec.sl * 0.46), belly).scale.set(1, 0.45, 1);
598
+ const blood = tools.ball(jaw, headSpec.sr2 * 0.9, rig.woundMaterial || (rig.woundMaterial = new THREE.MeshStandardMaterial({ color: 6950924, roughness: 0.3 })), 0, 0, headSpec.sl * 0.9, 1.1, 0.6, 0.6);
599
+ tools.eyes(head, headSpec.r * 0.13, headSpec.r * headSpec.s[0] * 0.52, headSpec.r * 0.5, headSpec.r * 0.25 + headSpec.r * headSpec.s[2] * 0.55);
600
+ const ears = spec.ears;
601
+ for (const side of [-1, 1]) {
602
+ const earGroup = tools.pivot(head, side * headSpec.r * headSpec.s[0] * 0.55, headSpec.r * (0.25 + headSpec.s[1] * 0.75), headSpec.r * 0.1, new THREE.Vector3(side * 0.04, 0.04, 0).multiplyScalar(rig.size));
603
+ rig.ears.push(earGroup);
604
+ if (ears.t === "cone") addMesh(THREE, earGroup, new THREE.ConeGeometry(ears.s * 0.45, ears.s, 8).translate(0, ears.s / 2, 0), main, 0, 0, 0, ears.fwd || 0, 0, side * -(ears.tilt ?? 0.3));
605
+ else if (ears.t === "round") tools.ball(earGroup, ears.s, main, 0, ears.s * 0.4, 0, 1, 1, 0.35);
606
+ else if (ears.t === "flop") {
607
+ const ear = tools.ball(earGroup, ears.s, main, side * ears.s * 0.9, -ears.s * 0.1, 0, 1.5, 0.45, 0.7);
608
+ ear.rotation.z = side * -0.25;
609
+ }
610
+ }
611
+ return { neck, head, blood, neckSpec, headSpec };
612
+ };
613
+ var buildTail = (THREE, rig, spec, parts) => {
614
+ const tools = createRigTools(THREE, rig);
615
+ const tailSpec = spec.tail;
616
+ if (!tailSpec || !tailSpec.n) return;
617
+ const { bodyGroup, main, dark } = parts;
618
+ const { L, R: radius } = spec;
619
+ let parent = tools.pivot(bodyGroup, 0, radius * (spec.sy || 1) * (tailSpec.y ?? 0.3), -L / 2 * 0.95, new THREE.Vector3(0, 0.02, -0.12).multiplyScalar(rig.size));
620
+ parent.rotation.x = tailSpec.ang;
621
+ rig.tailRoot = parent;
622
+ const segmentLength = tailSpec.len / tailSpec.n;
623
+ for (let index = 0; index < tailSpec.n; index++) {
624
+ const radius1 = tailSpec.r * (1 - index / tailSpec.n * 0.6);
625
+ const radius2 = tailSpec.r * (1 - (index + 1) / tailSpec.n * 0.6);
626
+ tools.limb(parent, radius1, radius2, segmentLength, tailSpec.dark && index === tailSpec.n - 1 ? dark : main);
627
+ tools.joint(parent, radius1, main, 0, 0, 0);
628
+ rig.tail.push(parent);
629
+ if (index < tailSpec.n - 1) {
630
+ const next = tools.pivot(parent, 0, -segmentLength, 0);
631
+ next.rotation.x = tailSpec.curl || 0;
632
+ parent = next;
633
+ } else if (tailSpec.tuft) tools.ball(parent, tailSpec.r * 2.2, dark, 0, -segmentLength - tailSpec.r, 0, 1, 1.8, 1);
634
+ }
635
+ if (tailSpec.bushy) tools.ball(rig.tail[0], tailSpec.r * 2.2, dark, 0, -tailSpec.len * 0.55, 0, 1, tailSpec.len * 2.2 / tailSpec.r / 6, 1);
636
+ };
637
+ var buildInfectionParts = (THREE, rig, spec, parts) => {
638
+ const tools = createRigTools(THREE, rig);
639
+ const { bodyGroup, neck, blood, neckSpec } = parts;
640
+ const { L, R: radius } = spec;
641
+ const sideX = radius * (spec.sx || 1);
642
+ const sideY = radius * (spec.sy || 1);
643
+ const list = [blood];
644
+ const wound = (parent, size, x, y, z, sx, sy, sz) => tools.ball(parent, size, rig.woundMaterial || (rig.woundMaterial = new THREE.MeshStandardMaterial({ color: 5902862, roughness: 0.45 })), x, y, z, sx || 1, sy || 1, sz || 1);
645
+ list.push(wound(bodyGroup, radius * 0.28, sideX * 0.92, sideY * 0.15, L * 0.05, 0.35, 1, 1.3));
646
+ list.push(wound(bodyGroup, radius * 0.2, -sideX * 0.9, sideY * 0.3, -L * 0.2, 0.35, 1, 1.1));
647
+ list.push(wound(neck, neckSpec.r * 0.5, neckSpec.r * 0.85, neckSpec.len * 0.5, 0, 0.4, 1, 1));
648
+ const bone = tools.material(14274488, { roughness: 0.6 });
649
+ for (let index = 0; index < RIB_COUNT; index++) {
650
+ const rib = addMesh(THREE, bodyGroup, new THREE.TorusGeometry(radius * 0.95, radius * 0.035, 6, 16, Math.PI * 0.42), bone, sideX * 0.12, sideY * 0.05, L * 0.12 - index * L * 0.07, 0, Math.PI / 2, -0.3);
651
+ rib.scale.set(1, 1.02 * (spec.sy || 1), spec.sx || 1);
652
+ list.push(rib);
653
+ }
654
+ rig.infectionParts.push(...list);
655
+ list.forEach((part) => part.visible = false);
656
+ };
657
+ var buildQuadruped = (THREE, rig, options) => {
658
+ const spec = rig.species.body;
659
+ const tools = createRigTools(THREE, rig);
660
+ const colors = spec.c;
661
+ const texture = spec.texture ? createCoatTexture(THREE, options.document, spec.texture) : void 0;
662
+ const main = tools.material(texture ? 16777215 : colors.main, texture ? { map: texture } : void 0);
663
+ const belly = tools.material(colors.belly);
664
+ const dark = tools.material(colors.dark);
665
+ const nose = tools.material(colors.nose || colors.dark, { roughness: 0.4 });
666
+ rig.main = main;
667
+ rig.dark = dark;
668
+ const { bodyGroup, bodyHeight } = buildBodyAndLegs(THREE, rig, spec, main, belly, dark);
669
+ const { neck, head, blood, neckSpec, headSpec } = buildHead(THREE, rig, spec, { bodyGroup, main, belly, nose });
670
+ buildTail(THREE, rig, spec, { bodyGroup, main, dark });
671
+ options.extra?.({ THREE, rig, tools }, { bodyGroup, head, neck, main, belly, dark, L: spec.L, Rb: spec.R, bodyY: bodyHeight, hd: headSpec, nk: neckSpec });
672
+ if (rig.species.outcome === "turns") buildInfectionParts(THREE, rig, spec, { bodyGroup, neck, blood, neckSpec });
673
+ else blood.visible = false;
674
+ return rig;
675
+ };
676
+
677
+ // src/snake.ts
678
+ var SEGMENT_COUNT = 34;
679
+ var SEGMENT_LENGTH = 0.042;
680
+ var buildSnake = (THREE, rig) => {
681
+ const tools = createRigTools(THREE, rig);
682
+ rig.size = 0.5;
683
+ rig.segmentLength = SEGMENT_LENGTH;
684
+ rig.segmentCount = SEGMENT_COUNT;
685
+ const primary = tools.material(8022602);
686
+ const secondary = tools.material(3812898);
687
+ const belly = tools.material(12101768);
688
+ rig.main = primary;
689
+ for (let index = 0; index < SEGMENT_COUNT; index++) {
690
+ const t = index / (SEGMENT_COUNT - 1);
691
+ const radius = 0.012 + 0.03 * Math.sin(Math.min(1, t * 1.15) * Math.PI) * (t < 0.1 ? 0.75 + t * 2.5 : 1) + (t < 0.05 ? 0.012 : 0);
692
+ const material = index % 4 === 1 || index % 4 === 2 ? secondary : primary;
693
+ const group = new THREE.Group();
694
+ rig.trunk.add(group);
695
+ const sphere = tools.ball(group, radius, material, 0, radius, 0, 1, 0.78, 1.5);
696
+ sphere.userData.joint = index % 3 === 0 ? 1 : 0;
697
+ if (index % 3 === 0) rig.joints.push(sphere);
698
+ rig.segments.push({ group, radius });
699
+ }
700
+ const head = new THREE.Group();
701
+ rig.trunk.add(head);
702
+ rig.head = head;
703
+ tools.ball(head, 0.032, primary, 0, 0.03, 0.025, 1.25, 0.6, 1.45);
704
+ tools.ball(head, 0.024, secondary, 0, 0.04, 0, 1.3, 0.4, 0.9);
705
+ tools.ball(head, 0.02, primary, 0, 0.028, 0.06, 0.85, 0.55, 1.1);
706
+ const jaw = new THREE.Group();
707
+ jaw.position.set(0, 0.018, 0);
708
+ head.add(jaw);
709
+ rig.jaw = jaw;
710
+ tools.ball(jaw, 0.026, belly, 0, 0, 0.035, 1.1, 0.35, 1.5);
711
+ tools.eyes(head, 6e-3, 0.03, 0.04, 0.045);
712
+ rig.tongue = addMesh(THREE, head, new THREE.BoxGeometry(4e-3, 2e-3, 0.05), tools.material(11542560), 0, 0.024, 0.1);
713
+ return rig;
714
+ };
715
+
716
+ // src/visual-state.ts
717
+ var INFECTED_TINT = 8227430;
718
+ var POOL_COLOR = 3803142;
719
+ var applyVisualState = (THREE, rig, state) => {
720
+ rig.infected = state === "infected";
721
+ rig.dead = state === "dead";
722
+ const tint = new THREE.Color(INFECTED_TINT);
723
+ rig.materials.forEach((material) => {
724
+ if (material === rig.eyeMaterial || material === rig.woundMaterial) return;
725
+ material.color.copy(material.userData.base);
726
+ if (rig.infected) material.color.lerp(tint, 0.45).multiplyScalar(0.72);
727
+ });
728
+ const eye = rig.eyeMaterial;
729
+ if (eye) {
730
+ if (rig.infected) {
731
+ eye.color.set(14473392);
732
+ eye.emissive = new THREE.Color(9078368);
733
+ eye.emissiveIntensity = 0.8;
734
+ } else {
735
+ eye.color.copy(eye.userData.base);
736
+ eye.emissive = new THREE.Color(0);
737
+ }
738
+ }
739
+ rig.infectionParts.forEach((part) => part.visible = rig.infected);
740
+ const firstEar = rig.ears[0];
741
+ if (firstEar) firstEar.visible = !rig.infected;
742
+ if (!rig.dead) {
743
+ rig.root.rotation.z = 0;
744
+ rig.root.position.y = 0;
745
+ }
746
+ if (rig.pool) {
747
+ rig.pool.visible = rig.dead;
748
+ return;
749
+ }
750
+ if (!rig.dead) return;
751
+ const pool = new THREE.Mesh(new THREE.CircleGeometry(1, 22).rotateX(-Math.PI / 2), new THREE.MeshStandardMaterial({ color: POOL_COLOR, roughness: 0.25 }));
752
+ pool.position.set(rig.home.x, 5e-3, rig.home.z + 0.1);
753
+ pool.receiveShadow = true;
754
+ pool.scale.setScalar(rig.size * 0.45);
755
+ rig.root.parent?.add(pool);
756
+ rig.pool = pool;
757
+ };
758
+
759
+ // src/views.ts
760
+ var setJointView = (rig, on, jointMaterial) => {
761
+ rig.materials.forEach((material) => {
762
+ if (material === rig.eyeMaterial) return;
763
+ material.transparent = on;
764
+ material.opacity = on ? 0.28 : 1;
765
+ material.depthWrite = !on;
766
+ material.needsUpdate = true;
767
+ });
768
+ rig.joints.forEach((joint) => {
769
+ var _a;
770
+ if (on) (_a = joint.userData).restMaterial ?? (_a.restMaterial = joint.material);
771
+ joint.material = on ? jointMaterial : joint.userData.restMaterial;
772
+ joint.renderOrder = on ? 2 : 0;
773
+ });
774
+ };
775
+ var setSplit = (rig, on) => {
776
+ rig.pivots.forEach((pivot) => {
777
+ pivot.group.userData.target = on ? pivot.rest.clone().add(pivot.offset) : pivot.rest.clone();
778
+ });
779
+ };
780
+ var updateSplit = (rig, delta) => {
781
+ rig.pivots.forEach((pivot) => {
782
+ const target = pivot.group.userData.target;
783
+ if (target) pivot.group.position.lerp(target, Math.min(1, delta * 5));
784
+ });
785
+ };
786
+
787
+ // src/animals-three.ts
788
+ var countTriangles = (root) => {
789
+ let triangles = 0;
790
+ root.traverse((object) => {
791
+ const mesh = object;
792
+ if (!mesh.isMesh) return;
793
+ const geometry = mesh.geometry;
794
+ triangles += geometry.index ? geometry.index.count / 3 : geometry.attributes.position.count / 3;
795
+ });
796
+ return Math.round(triangles);
797
+ };
798
+ var createAnimalsThree = (host, options = {}) => {
799
+ const THREE = shapeGeometries2(host);
800
+ const random = options.random ?? createStream(options.seed ?? 7);
801
+ const documentOf = () => {
802
+ const found = options.document ?? globalThis.document;
803
+ if (!found) throw new Error("game-animals-three: a species with a coat texture needs `options.document` (no global document found)");
804
+ return found;
805
+ };
806
+ const jointMaterial = new THREE.MeshStandardMaterial({ color: 14722376, emissive: 14722376, emissiveIntensity: 0.8, roughness: 0.4 });
807
+ const gaits = GAIT_PHASES;
808
+ return {
809
+ jointMaterial,
810
+ build(species, buildOptions = {}) {
811
+ const rig = createRig(THREE, species, { phase: buildOptions.phase ?? random() * 6 });
812
+ if (species.body.kind === "snake") buildSnake(THREE, rig);
813
+ else if (species.body.kind === "bird") buildBird(THREE, rig, { extra: BIRD_EXTRAS[species.id] });
814
+ else buildQuadruped(THREE, rig, { document: species.body.texture ? documentOf() : void 0, extra: QUADRUPED_EXTRAS[species.id] });
815
+ rig.triangles = countTriangles(rig.root);
816
+ markKitOrigin(rig.root, { owner: "game-animals-three", kind: "animal", id: species.id });
817
+ rig.root.traverse((object) => {
818
+ if (object.isMesh) object.userData.speciesId = species.id;
819
+ });
820
+ return rig;
821
+ },
822
+ animate(rig, params) {
823
+ const kind = rig.species.body.kind;
824
+ if (kind === "snake") animateSnake(THREE, rig, params);
825
+ else if (kind === "bird") animateBird(rig, params);
826
+ else animateQuadruped(rig, params, gaits);
827
+ },
828
+ setVisualState: (rig, state) => applyVisualState(THREE, rig, state),
829
+ setJointView: (rig, on) => setJointView(rig, on, jointMaterial),
830
+ setSplit,
831
+ updateSplit,
832
+ createSkin: (rig) => createNodeSkin(THREE, rig.root, { nodes: collectReferencedNodes(rig, ["species"]) })
833
+ };
834
+ };
835
+
836
+ // src/test-area/scenarios.ts
837
+ import { visualStateOf } from "@adatechnology/game-animals";
838
+ var SKIN_COUNT = 40;
839
+ var createScenarios = (deps) => {
840
+ const scenario = (id, run) => ({ id, name: deps.text[id] ?? id, run });
841
+ const rate = () => 0.5;
842
+ return [
843
+ scenario("visual_states", ({ expect }) => {
844
+ const animals = deps.ensure();
845
+ for (const species of deps.species()) {
846
+ const rig = animals.build(species);
847
+ animals.setVisualState(rig, "infected");
848
+ expect(rig.infected && !rig.dead, `${species.id}: infected`);
849
+ animals.setVisualState(rig, "dead");
850
+ expect(rig.dead && !rig.infected, `${species.id}: dead`);
851
+ animals.setVisualState(rig, "alive");
852
+ expect(!rig.dead && !rig.infected, `${species.id}: alive again`);
853
+ }
854
+ }),
855
+ scenario("gait_moves_legs", ({ expect }) => {
856
+ const animals = deps.ensure();
857
+ const species = deps.species().find((entry) => entry.body.kind === "quadruped");
858
+ if (!species) return expect(false, "no quadruped among the species");
859
+ const rig = animals.build(species);
860
+ animals.animate(rig, { time: 0, delta: 0, clip: "walk" });
861
+ const before = rig.legs.map((leg) => leg.hip.rotation.x);
862
+ animals.animate(rig, { time: 0.4, delta: 0.4, clip: "walk" });
863
+ expect(rig.legs.some((leg, index) => leg.hip.rotation.x !== before[index]), `${species.id}: a walk step swings a hip`);
864
+ }),
865
+ scenario("outbreak_rule", ({ expect }) => {
866
+ const turns = deps.species().find((entry) => entry.outcome === "turns");
867
+ const immune = deps.species().find((entry) => entry.outcome === "immune");
868
+ const on = deps.rebuildOutbreak(true);
869
+ if (!on || !turns) return expect(false, "no outbreak rule or no species that turns");
870
+ expect(on.bite(turns.id, rate, 0) !== void 0, `a bite on ${turns.id} starts a fate with the rule on`);
871
+ if (immune) expect(on.bite(immune.id, rate, 0) === void 0, `${immune.id} shrugs the bite off`);
872
+ const off = deps.rebuildOutbreak(false);
873
+ expect(off?.bite(turns.id, rate, 0) === void 0, "with the rule off a bite does nothing");
874
+ deps.rebuildOutbreak(true);
875
+ }),
876
+ scenario("carcass_lure", ({ expect }) => {
877
+ const outbreak = deps.rebuildOutbreak(true);
878
+ const dies = deps.species().find((entry) => entry.outbreak?.carcass);
879
+ if (!outbreak || !dies) return expect(false, "no outbreak rule or no species that leaves a carcass");
880
+ const carcass = outbreak.carcass(dies.id, [0, 0, 0], 0);
881
+ expect(carcass !== void 0, `${dies.id} leaves a carcass`);
882
+ if (!carcass) return;
883
+ expect(outbreak.lures(carcass, carcass.lureRadiusMetres - 1, 1), "a zombie inside the radius is drawn");
884
+ expect(!outbreak.lures(carcass, carcass.lureRadiusMetres + 1, 1), "a zombie outside it is not");
885
+ }),
886
+ scenario("bite_to_turn_timer", ({ expect }) => {
887
+ const outbreak = deps.rebuildOutbreak(true);
888
+ const turns = deps.species().find((entry) => entry.outcome === "turns");
889
+ if (!outbreak || !turns) return expect(false, "no outbreak rule or no species that turns");
890
+ const fate = outbreak.bite(turns.id, rate, 0);
891
+ if (!fate) return expect(false, "the bite took");
892
+ expect(outbreak.stateOf(fate, 0) === "incubating", "it incubates at first");
893
+ expect(outbreak.stateOf(fate, fate.dueAt + 1) === "turned", "it has turned once the time is up (the bench clock runs it accelerated)");
894
+ expect(visualStateOf(turns.outcome, true) === "infected", "and shows the infected look");
895
+ }),
896
+ scenario("skins_40", ({ expect }) => {
897
+ const result = deps.skins();
898
+ expect(result.count === SKIN_COUNT, `${SKIN_COUNT} animals are in the field`, { count: result.count });
899
+ expect(result.meshes.drawCalls <= SKIN_COUNT + 16, "the skins are about one draw call each", result.meshes);
900
+ expect(result.nodes > 0, "each skin keeps the nodes the animation moves", { nodes: result.nodes });
901
+ deps.clearSkins();
902
+ })
903
+ ];
904
+ };
905
+
906
+ // src/test-area/text.ts
907
+ var TEXT = {
908
+ en: {
909
+ group: "Animals",
910
+ name: "Animals area",
911
+ controls: { visual_state: "Visual state", gait: "Gait", speed: "Animation speed", outbreak: "Outbreak rule", time_scale: "Bite clock" },
912
+ hints: {
913
+ visual_state: "The look of every animal: alive, infected (grey coat, glowing eyes, wounds) or dead (lying down, a pool of blood)",
914
+ gait: "idle, walk or run; the cycle of each species runs at its own rate",
915
+ outbreak: "Off: a bite does nothing. On: a species that turns incubates and turns, one that dies dies, the rest shrug it off",
916
+ time_scale: "Game seconds per real second for the bite timers (60: ten minutes pass in ten seconds)"
917
+ },
918
+ options: { visual_state: { alive: "Alive", infected: "Infected", dead: "Dead" }, gait: { idle: "Idle", walk: "Walk", run: "Run" } },
919
+ actions: { bite_all: "Bite every animal", recover: "Heal every animal", call: "Make the animals with a call call (hen, lapwing, cow, dog)", skin_40: "Add 40 animals as node skins", clear_skins: "Remove the skins" },
920
+ layers: { joints: "Joints", split: "Split view", sound_range: "Sound range", carcass_lure: "Carcass lure radius" },
921
+ metrics: { animals: "Animals", draw_calls: "Draw calls", triangles: "Triangles", nodes_per_skin: "Nodes per skin", incubating: "Incubating", turned: "Turned", dead: "Dead" },
922
+ scenarios: {
923
+ visual_states: "Alive, infected and dead for every species",
924
+ gait_moves_legs: "The gait moves the legs",
925
+ outbreak_rule: "Outbreak rule on and off",
926
+ carcass_lure: "A carcass lures zombies within its radius",
927
+ bite_to_turn_timer: "Bite, incubation, turn (accelerated)",
928
+ skins_40: "40 animals as node skins"
929
+ }
930
+ },
931
+ "pt-BR": {
932
+ group: "Animais",
933
+ name: "\xC1rea de animais",
934
+ controls: { visual_state: "Estado visual", gait: "Andar", speed: "Velocidade da anima\xE7\xE3o", outbreak: "Regra de surto", time_scale: "Rel\xF3gio da mordida" },
935
+ hints: {
936
+ visual_state: "O aspecto de todos os bichos: vivo, infectado (pelo cinza, olhos brilhando, feridas) ou morto (deitado, po\xE7a de sangue)",
937
+ gait: "idle, walk ou run; o ciclo de cada esp\xE9cie anda no seu ritmo",
938
+ outbreak: "Desligada: a mordida n\xE3o faz nada. Ligada: a esp\xE9cie que vira incuba e vira, a que morre morre, as outras n\xE3o sentem",
939
+ time_scale: "Segundos de jogo por segundo real nos prazos da mordida (60: dez minutos passam em dez segundos)"
940
+ },
941
+ options: { visual_state: { alive: "Vivo", infected: "Infectado", dead: "Morto" }, gait: { idle: "Parado", walk: "Andando", run: "Correndo" } },
942
+ actions: { bite_all: "Morder todos os bichos", recover: "Curar todos os bichos", call: "Fazer os bichos com chamado chamarem (galinha, quero-quero, vaca, cachorro)", skin_40: "P\xF4r 40 bichos como pele de n\xF3s", clear_skins: "Tirar as peles" },
943
+ layers: { joints: "Juntas", split: "Vista aberta", sound_range: "Alcance do som", carcass_lure: "Raio de atra\xE7\xE3o da carca\xE7a" },
944
+ metrics: { animals: "Bichos", draw_calls: "Chamadas de desenho", triangles: "Tri\xE2ngulos", nodes_per_skin: "N\xF3s por pele", incubating: "Incubando", turned: "Viraram", dead: "Morreram" },
945
+ scenarios: {
946
+ visual_states: "Vivo, infectado e morto em cada esp\xE9cie",
947
+ gait_moves_legs: "O andar mexe as pernas",
948
+ outbreak_rule: "Regra de surto ligada e desligada",
949
+ carcass_lure: "A carca\xE7a atrai zumbis dentro do raio",
950
+ bite_to_turn_timer: "Mordida, incuba\xE7\xE3o, vira (acelerado)",
951
+ skins_40: "40 bichos como pele de n\xF3s"
952
+ }
953
+ }
954
+ };
955
+
956
+ // src/test-area/module.ts
957
+ var ANIMALS_AREA_ID = "animals_area";
958
+ var ANIMAL_ITEM_PREFIX = "animal_";
959
+ var SKIN_COUNT2 = 40;
960
+ var SKIN_FIELD = { x0: -18, z0: -10, columns: 8, gap: 4.5 };
961
+ var DEFAULT_TIME_SCALE = 60;
962
+ var BATCH_BUDGET_MS = 1;
963
+ function createAnimalsTestModule(options = {}) {
964
+ const locale = options.locale ?? "pt-BR";
965
+ const text = TEXT[locale] ?? TEXT["pt-BR"];
966
+ const species = options.species ?? [];
967
+ const settings = { visual: "alive", gait: "walk", speed: 1, outbreak: true, timeScale: DEFAULT_TIME_SCALE };
968
+ let three = null;
969
+ let animals = null;
970
+ let host = null;
971
+ let outbreak = null;
972
+ let sound = null;
973
+ let time = 0;
974
+ let clock = 0;
975
+ let skinGroup = null;
976
+ let skins = [];
977
+ let rings = [];
978
+ const placed = /* @__PURE__ */ new Map();
979
+ const random = () => 0.5;
980
+ const drawSeed = options.seed ?? 7;
981
+ const bitten = () => ({ turned: [...placed.values()].filter((p) => p.counted === "turned").length, dead: [...placed.values()].filter((p) => p.counted === "dead").length });
982
+ const ensure = (THREE) => {
983
+ three = THREE;
984
+ animals ?? (animals = createAnimalsThree(THREE, { seed: drawSeed, ...options.document ? { document: options.document } : {} }));
985
+ return animals;
986
+ };
987
+ const rebuildOutbreak = () => {
988
+ outbreak = options.createOutbreak ? options.createOutbreak({ species, enabled: settings.outbreak, timeScale: settings.timeScale }) : null;
989
+ };
990
+ rebuildOutbreak();
991
+ const batch = (entry) => {
992
+ entry.batcher?.dispose();
993
+ if (three) entry.batcher = createStaticBatcher(three, entry.rig.root, { followMeshes: true, budgetMs: BATCH_BUDGET_MS });
994
+ };
995
+ const look = (entry, state) => {
996
+ animals?.setVisualState(entry.rig, state);
997
+ batch(entry);
998
+ };
999
+ const item = (animal) => ({
1000
+ id: `${ANIMAL_ITEM_PREFIX}${animal.id}`,
1001
+ name: animal.name[locale] ?? animal.id,
1002
+ movable: false,
1003
+ build: (THREE) => {
1004
+ const rig = ensure(THREE).build(animal);
1005
+ const entry = { id: rig.root.uuid, rig };
1006
+ placed.set(rig.root.uuid, entry);
1007
+ if (settings.visual !== "alive") animals?.setVisualState(rig, settings.visual);
1008
+ batch(entry);
1009
+ return rig.root;
1010
+ }
1011
+ });
1012
+ const statusOf = (entry) => {
1013
+ if (!entry.fate || !outbreak) return;
1014
+ const state = outbreak.stateOf(entry.fate, clock);
1015
+ if (state === "incubating") return;
1016
+ entry.counted = state === "turned" ? "turned" : "dead";
1017
+ look(entry, visualStateOf2(entry.rig.species.outcome, true, state === "dead"));
1018
+ };
1019
+ const clearRings = () => {
1020
+ discardObject3D(rings);
1021
+ rings = [];
1022
+ };
1023
+ const refreshLayers = () => {
1024
+ if (!host?.bench || !animals || !three) return;
1025
+ clearRings();
1026
+ for (const entry of placed.values()) {
1027
+ animals.setJointView(entry.rig, host.bench.layer("joints"));
1028
+ animals.setSplit(entry.rig, host.bench.layer("split"));
1029
+ }
1030
+ const ring = (radius, color, parent) => {
1031
+ const mesh = new three.Mesh(new three.RingGeometry(radius - 0.15, radius, 96).rotateX(-Math.PI / 2), new three.MeshBasicMaterial({ color, transparent: true, opacity: 0.55, depthWrite: false }));
1032
+ mesh.position.y = 0.03;
1033
+ parent.add(mesh);
1034
+ rings.push(mesh);
1035
+ };
1036
+ for (const entry of placed.values()) {
1037
+ if (!entry.rig.root.parent) continue;
1038
+ const radius = entry.rig.species.soundRangeMetres;
1039
+ if (host.bench.layer("sound_range") && radius) ring(radius, 4500223, entry.rig.root);
1040
+ const lure = entry.rig.species.outbreak?.carcass?.lureRadiusMetres;
1041
+ if (host.bench.layer("carcass_lure") && lure) ring(lure, 16733491, entry.rig.root);
1042
+ }
1043
+ };
1044
+ const addSkins = () => {
1045
+ if (!animals || !three || !host) return;
1046
+ skinGroup ?? (skinGroup = new three.Group());
1047
+ if (!skinGroup.parent) host.scene.add(skinGroup);
1048
+ for (let index = skins.length; index < SKIN_COUNT2; index++) {
1049
+ const animal = species[index % Math.max(1, species.length)];
1050
+ const rig = animals.build(animal);
1051
+ const skin = animals.createSkin(rig);
1052
+ rig.root.position.set(SKIN_FIELD.x0 + index % SKIN_FIELD.columns * SKIN_FIELD.gap, 0, SKIN_FIELD.z0 - Math.floor(index / SKIN_FIELD.columns) * SKIN_FIELD.gap);
1053
+ rig.root.traverse((object) => {
1054
+ if (object.isMesh) object.visible = false;
1055
+ });
1056
+ skinGroup.add(rig.root, skin.mesh);
1057
+ skins.push({ rig, skin });
1058
+ }
1059
+ };
1060
+ const clearSkins = () => {
1061
+ if (skinGroup) discardObject3D(skinGroup);
1062
+ skinGroup = null;
1063
+ skins = [];
1064
+ };
1065
+ const applyVisual = () => {
1066
+ for (const entry of placed.values()) {
1067
+ if (entry.fate) continue;
1068
+ look(entry, settings.visual);
1069
+ }
1070
+ };
1071
+ const onChange = (change) => {
1072
+ if (change.kind === "layer") return refreshLayers();
1073
+ if (change.kind === "action") {
1074
+ if (change.id === "bite_all") {
1075
+ for (const entry of placed.values()) {
1076
+ entry.fate = outbreak?.bite(entry.rig.species.id, random, clock);
1077
+ if (entry.fate) look(entry, visualStateOf2(entry.rig.species.outcome, true));
1078
+ }
1079
+ } else if (change.id === "recover") {
1080
+ for (const entry of placed.values()) {
1081
+ delete entry.fate;
1082
+ delete entry.counted;
1083
+ look(entry, settings.visual);
1084
+ }
1085
+ } else if (change.id === "call" && sound) {
1086
+ for (const [id, entry] of placed) {
1087
+ const at = entry.rig.root.getWorldPosition(new three.Vector3());
1088
+ sound.call(entry.rig.species.id, [at.x, at.y + 0.5, at.z], { key: id });
1089
+ }
1090
+ } else if (change.id === "skin_40") addSkins();
1091
+ else if (change.id === "clear_skins") clearSkins();
1092
+ return;
1093
+ }
1094
+ switch (change.id) {
1095
+ case "visual_state":
1096
+ settings.visual = String(change.value);
1097
+ applyVisual();
1098
+ break;
1099
+ case "gait":
1100
+ settings.gait = String(change.value);
1101
+ break;
1102
+ case "speed":
1103
+ settings.speed = Number(change.value);
1104
+ break;
1105
+ case "outbreak":
1106
+ settings.outbreak = Boolean(change.value);
1107
+ if (outbreak) outbreak.enabled = settings.outbreak;
1108
+ break;
1109
+ case "time_scale":
1110
+ settings.timeScale = Number(change.value);
1111
+ rebuildOutbreak();
1112
+ break;
1113
+ default:
1114
+ break;
1115
+ }
1116
+ };
1117
+ const frame = (dt) => {
1118
+ time += dt * settings.speed;
1119
+ clock += dt;
1120
+ sound?.update(dt);
1121
+ for (const [id, entry] of placed) {
1122
+ if (!entry.rig.root.parent) {
1123
+ entry.batcher?.dispose();
1124
+ placed.delete(id);
1125
+ continue;
1126
+ }
1127
+ statusOf(entry);
1128
+ animals?.animate(entry.rig, { time, delta: dt * settings.speed, clip: settings.gait, split: host?.bench?.layer("split") ?? false });
1129
+ animals?.updateSplit(entry.rig, dt);
1130
+ entry.batcher?.update();
1131
+ }
1132
+ for (const { rig, skin } of skins) {
1133
+ animals?.animate(rig, { time, delta: dt * settings.speed, clip: settings.gait });
1134
+ rig.root.updateMatrixWorld(true);
1135
+ skin.update();
1136
+ }
1137
+ };
1138
+ const countMeshes = (only) => {
1139
+ let drawCalls = 0;
1140
+ let triangles = 0;
1141
+ const count = (root) => root.traverse((object) => {
1142
+ const mesh = object;
1143
+ if (!mesh.isMesh || !mesh.visible) return;
1144
+ drawCalls += 1;
1145
+ const geometry = mesh.geometry;
1146
+ triangles += geometry.index ? geometry.index.count / 3 : geometry.attributes.position.count / 3;
1147
+ });
1148
+ if (only !== "skins") for (const entry of placed.values()) count(entry.rig.root);
1149
+ for (const { skin } of skins) count(skin.mesh);
1150
+ return { drawCalls, triangles: Math.round(triangles) };
1151
+ };
1152
+ const deps = {
1153
+ text: text.scenarios,
1154
+ species: () => species,
1155
+ ensure: () => ensure(three ?? host?.THREE),
1156
+ outbreak: () => outbreak,
1157
+ rebuildOutbreak: (enabled) => {
1158
+ settings.outbreak = enabled;
1159
+ rebuildOutbreak();
1160
+ return outbreak;
1161
+ },
1162
+ skins: () => {
1163
+ ensure(three ?? host?.THREE);
1164
+ addSkins();
1165
+ return { count: skins.length, meshes: countMeshes("skins"), nodes: skins[0]?.skin.nodes.length ?? 0, rich: skins[0]?.rig.triangles ?? 0 };
1166
+ },
1167
+ clearSkins
1168
+ };
1169
+ return {
1170
+ id: ANIMALS_AREA_ID,
1171
+ name: text.name,
1172
+ ground: { surface: "lawn" },
1173
+ group: text.group,
1174
+ get placed() {
1175
+ return placed;
1176
+ },
1177
+ items: () => species.map(item),
1178
+ area: () => {
1179
+ const gap = 0.85;
1180
+ let cursor = 0;
1181
+ const spots = species.map((animal) => {
1182
+ const length = lineUpLength(animal.body);
1183
+ const x = cursor + length / 2;
1184
+ cursor += length + gap;
1185
+ return { animal, x };
1186
+ });
1187
+ const offset = -(cursor - gap) / 2;
1188
+ return {
1189
+ placements: spots.map(({ animal, x }) => ({ item: `${ANIMAL_ITEM_PREFIX}${animal.id}`, position: [x + offset, 0, 0], rotationY: animal.body.kind === "snake" ? 2.2 : 1.1 })),
1190
+ view: { eye: [0, 4, Math.max(10, (cursor - gap) * 0.55)], target: [0, 0.6, 0] }
1191
+ };
1192
+ },
1193
+ controls: () => [
1194
+ { kind: "choice", id: "visual_state", name: text.controls.visual_state, hint: text.hints.visual_state, options: ["alive", "infected", "dead"].map((id) => ({ id, name: text.options.visual_state[id] })), default: "alive" },
1195
+ { kind: "choice", id: "gait", name: text.controls.gait, hint: text.hints.gait, options: ["idle", "walk", "run"].map((id) => ({ id, name: text.options.gait[id] })), default: "walk" },
1196
+ { kind: "number", id: "speed", name: text.controls.speed, min: 0, max: 4, step: 0.25, default: 1 },
1197
+ { kind: "boolean", id: "outbreak", name: text.controls.outbreak, hint: text.hints.outbreak, default: true },
1198
+ { kind: "number", id: "time_scale", name: text.controls.time_scale, hint: text.hints.time_scale, min: 1, max: 600, step: 1, default: DEFAULT_TIME_SCALE },
1199
+ { kind: "action", id: "bite_all", name: text.actions.bite_all },
1200
+ { kind: "action", id: "recover", name: text.actions.recover },
1201
+ { kind: "action", id: "call", name: text.actions.call },
1202
+ { kind: "action", id: "skin_40", name: text.actions.skin_40, slow: true },
1203
+ { kind: "action", id: "clear_skins", name: text.actions.clear_skins }
1204
+ ],
1205
+ layers: () => ["joints", "split", "sound_range", "carcass_lure"].map((id) => ({ id, name: text.layers[id], default: false })),
1206
+ metrics: () => [
1207
+ { id: "animals", name: text.metrics.animals, read: () => placed.size + skins.length },
1208
+ { id: "draw_calls", name: text.metrics.draw_calls, read: () => countMeshes().drawCalls },
1209
+ { id: "triangles", name: text.metrics.triangles, read: () => countMeshes().triangles },
1210
+ { id: "nodes_per_skin", name: text.metrics.nodes_per_skin, read: () => skins[0]?.skin.nodes.length ?? 0 },
1211
+ { id: "incubating", name: text.metrics.incubating, read: () => [...placed.values()].filter((p) => p.fate && !p.counted).length },
1212
+ { id: "turned", name: text.metrics.turned, read: () => bitten().turned },
1213
+ { id: "dead", name: text.metrics.dead, read: () => bitten().dead }
1214
+ ],
1215
+ scenarios: () => createScenarios(deps),
1216
+ attach(lent) {
1217
+ host = lent;
1218
+ ensure(lent.THREE);
1219
+ sound = lent.audio && options.sound !== false ? createAnimalSound(lent.audio, options.sound ?? {}) : null;
1220
+ const stopChange = lent.bench?.onChange(onChange);
1221
+ const stopFrame = lent.onFrame(frame);
1222
+ return () => {
1223
+ stopChange?.();
1224
+ stopFrame();
1225
+ clearRings();
1226
+ clearSkins();
1227
+ sound?.dispose();
1228
+ sound = null;
1229
+ host = null;
1230
+ };
1231
+ }
1232
+ };
1233
+ }
1234
+
1235
+ export {
1236
+ DOG_BARK,
1237
+ LAPWING_CALL,
1238
+ HEN_CLUCK,
1239
+ COW_MOO,
1240
+ ANIMAL_SOUNDS,
1241
+ ANIMAL_SOUND_BANK,
1242
+ ANIMAL_CALLS,
1243
+ ANIMAL_CALL_SOURCES,
1244
+ createAnimalSound,
1245
+ QUADRUPED_EXTRAS,
1246
+ BIRD_EXTRAS,
1247
+ profileGeometry,
1248
+ createCoatTexture,
1249
+ applyVisualState,
1250
+ setJointView,
1251
+ setSplit,
1252
+ updateSplit,
1253
+ createAnimalsThree,
1254
+ ANIMALS_AREA_ID,
1255
+ ANIMAL_ITEM_PREFIX,
1256
+ createAnimalsTestModule
1257
+ };
1258
+ //# sourceMappingURL=chunk-SC4QTSML.js.map