@adatechnology/game-weapons-three 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,90 +2,296 @@
2
2
  import { createArsenal } from "@adatechnology/game-weapons";
3
3
 
4
4
  // src/audio.ts
5
- var NOISE_SECONDS = 0.6;
6
- function createWeaponAudio(volume = 0.6) {
7
- const Ctor = globalThis.AudioContext ?? globalThis.webkitAudioContext;
5
+ import { createRng } from "@adatechnology/game-weapons";
6
+
7
+ // src/audio/mechanical-sounds.ts
8
+ var metal = (at, frequency, gain) => ({ layer: "metal", at, frequency, gain });
9
+ var sound = (at, soundClass, volume) => ({ layer: "class", at, soundClass, volume });
10
+ var thud = (at, gain, frequency) => ({ layer: "thud", at, gain, frequency });
11
+ var slide = [metal(0, 900, 0.35), metal(0.14, 1300, 0.35)];
12
+ var seat = [metal(0, 1100, 0.45), sound(0, "reload", 0.8)];
13
+ var MECHANICAL_SOUNDS = {
14
+ dry_fire: [sound(0, "empty", 1), metal(0, 2600, 0.25)],
15
+ misfire: [sound(0, "empty", 0.8), metal(0, 2200, 0.2)],
16
+ feed: [metal(0, 1900, 0.2)],
17
+ cycle: [metal(0.22, 900, 0.35), metal(0.36, 1300, 0.35)],
18
+ mode: [metal(0, 2e3, 0.15)],
19
+ safety_on: [metal(0, 2200, 0.22)],
20
+ safety_off: [metal(0, 1800, 0.22)],
21
+ jam: [metal(0, 700, 0.4), metal(0.08, 500, 0.35)],
22
+ clear_start: [metal(0, 1e3, 0.3)],
23
+ clear_end: [metal(0, 1300, 0.35)],
24
+ magazine_out: [metal(0, 1500, 0.35), sound(0, "reload", 0.6)],
25
+ magazine_in: seat,
26
+ charge: slide,
27
+ bolt_open: [metal(0, 900, 0.35)],
28
+ round_in: [metal(0, 2e3, 0.2), sound(0, "reload", 0.6)],
29
+ bolt_close: [metal(0, 1300, 0.35)],
30
+ pump_cycle: slide,
31
+ lever_cycle: [metal(0, 1e3, 0.35), metal(0.12, 1500, 0.35)],
32
+ cylinder_open: [metal(0, 1500, 0.3)],
33
+ cylinder_eject: [metal(0, 2400, 0.25), metal(0.05, 2800, 0.2)],
34
+ cylinder_load: [metal(0, 2e3, 0.2)],
35
+ cylinder_close: seat,
36
+ barrels_open: [metal(0, 1e3, 0.4)],
37
+ casings_eject: [metal(0, 2600, 0.25)],
38
+ rounds_in: [metal(0, 2e3, 0.25), sound(0, "reload", 0.6)],
39
+ barrels_close: seat,
40
+ cover_open: [metal(0, 800, 0.4)],
41
+ belt_swap: [metal(0, 1400, 0.3), metal(0.08, 1800, 0.25)],
42
+ cover_close: [metal(0, 1e3, 0.45)],
43
+ tank_swap: [thud(0, 0.4, 500), metal(0.05, 600, 0.3)],
44
+ tube_drop: [thud(0, 0.35, 380)],
45
+ tube_seat: seat,
46
+ rocket_seat: [metal(0, 900, 0.45), sound(0, "reload", 0.8)],
47
+ breech_open: [metal(0, 1e3, 0.35)],
48
+ breech_close: seat
49
+ };
50
+ var mechanicalSoundOf = (event) => {
51
+ switch (event.type) {
52
+ case "reload_stage":
53
+ return event.stage;
54
+ case "empty":
55
+ return "dry_fire";
56
+ case "misfire":
57
+ return "misfire";
58
+ case "feed":
59
+ return "feed";
60
+ case "cycle":
61
+ return event.phase === "start" ? "cycle" : void 0;
62
+ case "mode":
63
+ return "mode";
64
+ case "safety":
65
+ return event.on ? "safety_on" : "safety_off";
66
+ case "fault":
67
+ return "jam";
68
+ case "clear":
69
+ return event.phase === "start" ? "clear_start" : "clear_end";
70
+ default:
71
+ return void 0;
72
+ }
73
+ };
74
+
75
+ // src/audio/shot-sound.ts
76
+ import { muzzleEnergy } from "@adatechnology/game-weapons";
77
+ var GAME_SOUND_CLASSES = {
78
+ pistol: [1800, 0.9, 0.18],
79
+ shotgun: [900, 1.2, 0.35],
80
+ rifle: [1400, 1.1, 0.28],
81
+ long_rifle: [1100, 1.3, 0.45],
82
+ impact: [2500, 0.25, 0.05],
83
+ flesh: [600, 0.35, 0.08],
84
+ step: [400, 0.08, 0.05],
85
+ glass: [5e3, 0.5, 0.3],
86
+ wood: [700, 0.5, 0.2],
87
+ reload: [3e3, 0.15, 0.06],
88
+ empty: [4e3, 0.12, 0.03],
89
+ explosion: [200, 1.6, 1.4]
90
+ };
91
+ var FALLBACK_SOUND_CLASS = [1e3, 0.5, 0.1];
92
+ var GAME_DISTANCE_CURVE = { gainFalloff: 4e-3, cutoffFalloff: 0.01, minGain: 0.01 };
93
+ var DEFAULT_ENERGY_CURVE = {
94
+ floorJ: 50,
95
+ spanDecades: 2.5,
96
+ gain: [0.7, 0.6],
97
+ duration: [0.75, 0.5],
98
+ cutoff: [1.15, -0.3],
99
+ crackGain: [0.3, 0.4],
100
+ bodyGain: [0.35, 0.4],
101
+ bodyDuration: [0.14, 0.2],
102
+ bodyFrequency: [140, -70]
103
+ };
104
+ var CLASS_REFERENCE_ENERGY = { pistol: 500, shotgun: 2700, rifle: 2100, long_rifle: 3500 };
105
+ var HANDGUN_CATEGORIES = /* @__PURE__ */ new Set(["pistol", "revolver", "smg"]);
106
+ var SHELL_KINDS = /* @__PURE__ */ new Set(["buck", "slug", "birdshot"]);
107
+ var DEFAULT_BANDS = { handgunBelowJ: 1e3, rifleBelowJ: 3e3 };
108
+ var DEFAULT_SUPPRESSOR = { gain: 0.3, cutoff: 0.6, crack: 0, body: 0.5 };
109
+ var DEFAULT_INDOORS = { duration: 1.5, cutoff: 0.85 };
110
+ var CRACK_CUTOFF = 3e3;
111
+ var CRACK_SECONDS = 0.03;
112
+ var BODY_TO_FREQUENCY = 40;
113
+ var linear = ([base, span], k) => base + span * k;
114
+ var energyPosition = (energyJ, curve) => Math.min(1, Math.max(0, Math.log10(Math.max(energyJ, curve.floorJ) / curve.floorJ) / curve.spanDecades));
115
+ var energyOfShot = (input) => {
116
+ if (input.energyJ !== void 0) return input.energyJ;
117
+ if (!input.caliber) return 0;
118
+ return muzzleEnergy(input.caliber) * (input.caliber.pellets ?? 1);
119
+ };
120
+ var defaultClassOf = (input, energyJ, bands) => {
121
+ if (input.category === "shotgun" || (input.caliber?.pellets ?? 1) > 1 || SHELL_KINDS.has(input.caliber?.kind ?? "")) return "shotgun";
122
+ if (input.category && HANDGUN_CATEGORIES.has(input.category)) return "pistol";
123
+ if (energyJ < bands.handgunBelowJ) return "pistol";
124
+ return energyJ < bands.rifleBelowJ ? "rifle" : "long_rifle";
125
+ };
126
+ var computeShotSound = (input, options = {}) => {
127
+ const curve = { ...DEFAULT_ENERGY_CURVE, ...options.energy };
128
+ const distanceCurve = { ...GAME_DISTANCE_CURVE, ...options.distance };
129
+ const energyJ = energyOfShot(input);
130
+ const soundClass = options.classOf ? options.classOf(input, energyJ) : defaultClassOf(input, energyJ, options.bands ?? DEFAULT_BANDS);
131
+ const table = { ...GAME_SOUND_CLASSES, ...options.classes };
132
+ const [classCutoff, classGain, classSeconds] = table[soundClass] ?? FALLBACK_SOUND_CLASS;
133
+ const referenceEnergy = options.referenceEnergy?.[soundClass] ?? CLASS_REFERENCE_ENERGY[soundClass] ?? energyJ;
134
+ const k = energyPosition(energyJ, curve);
135
+ const referenceK = energyPosition(referenceEnergy, curve);
136
+ const distance = Math.max(0, input.distance ?? 0);
137
+ const attenuation = (input.volume ?? 1) / (1 + distance * distance * distanceCurve.gainFalloff);
138
+ const muffling = 1 / (1 + distance * distanceCurve.cutoffFalloff);
139
+ const suppressor = input.suppressed ? { ...DEFAULT_SUPPRESSOR, ...options.suppressor } : { gain: 1, cutoff: 1, crack: 1, body: 1 };
140
+ const indoors = input.indoors ? { ...DEFAULT_INDOORS, ...options.indoors } : { duration: 1, cutoff: 1 };
141
+ const gain = classGain * attenuation * (linear(curve.gain, k) / linear(curve.gain, referenceK)) * suppressor.gain;
142
+ const duration = classSeconds * (linear(curve.duration, k) / linear(curve.duration, referenceK)) * indoors.duration;
143
+ const cutoff = classCutoff * muffling * (linear(curve.cutoff, k) / linear(curve.cutoff, referenceK)) * suppressor.cutoff * indoors.cutoff;
144
+ return {
145
+ soundClass,
146
+ energyJ,
147
+ audible: attenuation >= distanceCurve.minGain,
148
+ cutoff,
149
+ gain,
150
+ duration,
151
+ crack: { gain: linear(curve.crackGain, k) * attenuation * suppressor.crack, cutoff: CRACK_CUTOFF, duration: CRACK_SECONDS },
152
+ body: {
153
+ gain: linear(curve.bodyGain, k) * attenuation * suppressor.body,
154
+ frequency: linear(curve.bodyFrequency, k),
155
+ to: BODY_TO_FREQUENCY,
156
+ duration: linear(curve.bodyDuration, k) * indoors.duration
157
+ }
158
+ };
159
+ };
160
+
161
+ // src/audio.ts
162
+ var NOISE_SECONDS = 1.5;
163
+ var globalContext = () => {
164
+ const scope2 = globalThis;
165
+ const Ctor = scope2.AudioContext ?? scope2.webkitAudioContext;
166
+ return Ctor ? new Ctor() : null;
167
+ };
168
+ function createWeaponAudio(volume = 0.6, options = {}) {
169
+ const makeContext = options.createContext ?? globalContext;
170
+ const rng = createRng(options.seed ?? 1);
171
+ const classes = { ...GAME_SOUND_CLASSES, ...options.classes };
172
+ const distanceCurve = { ...GAME_DISTANCE_CURVE, ...options.distance };
173
+ const mechanicalTable = { ...MECHANICAL_SOUNDS, ...options.mechanical };
8
174
  let ctx = null;
9
175
  let master = null;
10
176
  let noise = null;
11
177
  let level = volume;
12
178
  const ready = () => {
13
- if (!Ctor) return null;
14
179
  if (!ctx) {
15
- ctx = new Ctor();
180
+ const created = makeContext();
181
+ if (!created) return null;
182
+ ctx = created;
16
183
  master = ctx.createGain();
17
184
  master.gain.value = level;
18
185
  master.connect(ctx.destination);
19
186
  noise = ctx.createBuffer(1, Math.floor(ctx.sampleRate * NOISE_SECONDS), ctx.sampleRate);
20
187
  const data = noise.getChannelData(0);
21
- for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
188
+ for (let i = 0; i < data.length; i++) data[i] = rng() * 2 - 1;
22
189
  }
23
190
  if (ctx.state === "suspended") void ctx.resume();
24
191
  return ctx;
25
192
  };
26
- const burst = (options) => {
193
+ const burst = (config) => {
27
194
  const audio = ready();
28
195
  if (!audio || !noise || !master) return;
29
- const start = audio.currentTime + (options.at ?? 0);
196
+ const start = audio.currentTime + (config.at ?? 0);
30
197
  const source = audio.createBufferSource();
31
198
  source.buffer = noise;
32
199
  const filter = audio.createBiquadFilter();
33
- filter.type = options.filter;
34
- filter.frequency.value = options.frequency;
35
- filter.Q.value = options.q ?? 0.8;
200
+ filter.type = config.filter;
201
+ filter.frequency.value = config.frequency;
202
+ filter.Q.value = config.q ?? 0.8;
36
203
  const gain = audio.createGain();
37
204
  gain.gain.setValueAtTime(1e-4, start);
38
- gain.gain.exponentialRampToValueAtTime(options.gain, start + 4e-3);
39
- gain.gain.exponentialRampToValueAtTime(1e-4, start + options.seconds);
205
+ gain.gain.exponentialRampToValueAtTime(Math.max(config.gain, 2e-4), start + 4e-3);
206
+ gain.gain.exponentialRampToValueAtTime(1e-4, start + config.seconds);
40
207
  source.connect(filter).connect(gain).connect(master);
41
- source.start(start);
42
- source.stop(start + options.seconds + 0.05);
208
+ source.start(start, rng() * Math.max(0, NOISE_SECONDS - config.seconds));
209
+ source.stop(start + config.seconds + 0.05);
43
210
  };
44
- const tone = (options) => {
211
+ const classBurst = (at, cutoff, gain, seconds) => {
212
+ const audio = ready();
213
+ if (!audio || !noise || !master) return;
214
+ const start = audio.currentTime + at;
215
+ const source = audio.createBufferSource();
216
+ source.buffer = noise;
217
+ const filter = audio.createBiquadFilter();
218
+ filter.type = "lowpass";
219
+ filter.frequency.value = cutoff;
220
+ const envelope = audio.createGain();
221
+ envelope.gain.setValueAtTime(gain, start);
222
+ envelope.gain.exponentialRampToValueAtTime(1e-3, start + seconds);
223
+ source.connect(filter).connect(envelope).connect(master);
224
+ source.start(start, rng() * Math.max(0, NOISE_SECONDS - seconds));
225
+ source.stop(start + seconds + 0.05);
226
+ };
227
+ const tone = (config) => {
45
228
  const audio = ready();
46
229
  if (!audio || !master) return;
47
- const start = audio.currentTime + (options.at ?? 0);
230
+ const start = audio.currentTime + (config.at ?? 0);
48
231
  const osc = audio.createOscillator();
49
- osc.type = options.type ?? "sine";
50
- osc.frequency.setValueAtTime(options.frequency, start);
51
- if (options.to) osc.frequency.exponentialRampToValueAtTime(options.to, start + options.seconds);
232
+ osc.type = config.type ?? "sine";
233
+ osc.frequency.setValueAtTime(config.frequency, start);
234
+ if (config.to) osc.frequency.exponentialRampToValueAtTime(config.to, start + config.seconds);
52
235
  const gain = audio.createGain();
53
236
  gain.gain.setValueAtTime(1e-4, start);
54
- gain.gain.exponentialRampToValueAtTime(options.gain, start + 3e-3);
55
- gain.gain.exponentialRampToValueAtTime(1e-4, start + options.seconds);
237
+ gain.gain.exponentialRampToValueAtTime(Math.max(config.gain, 2e-4), start + 3e-3);
238
+ gain.gain.exponentialRampToValueAtTime(1e-4, start + config.seconds);
56
239
  osc.connect(gain).connect(master);
57
240
  osc.start(start);
58
- osc.stop(start + options.seconds + 0.05);
241
+ osc.stop(start + config.seconds + 0.05);
59
242
  };
60
- const metal = (at, frequency, gain) => {
243
+ const metal2 = (at, frequency, gain) => {
61
244
  burst({ at, seconds: 0.05, gain, filter: "highpass", frequency, q: 2 });
62
245
  tone({ at, seconds: 0.04, gain: gain * 0.6, frequency: frequency * 0.6, type: "square" });
63
246
  };
247
+ const play = (soundClass, classVolume = 1, distance = 0, at = 0) => {
248
+ const attenuation = classVolume / (1 + distance * distance * distanceCurve.gainFalloff);
249
+ if (attenuation < distanceCurve.minGain) return;
250
+ const [cutoff, gain, seconds] = classes[soundClass] ?? FALLBACK_SOUND_CLASS;
251
+ classBurst(at, cutoff / (1 + distance * distanceCurve.cutoffFalloff), gain * attenuation, seconds);
252
+ };
253
+ const step = (entry) => {
254
+ if (entry.layer === "metal") metal2(entry.at, entry.frequency, entry.gain);
255
+ else if (entry.layer === "class") play(entry.soundClass, entry.volume, 0, entry.at);
256
+ else burst({ at: entry.at, seconds: 0.09, gain: entry.gain, filter: "lowpass", frequency: entry.frequency });
257
+ };
258
+ const mechanical = (id) => {
259
+ for (const entry of mechanicalTable[id] ?? []) step(entry);
260
+ };
261
+ const shotSound = (input) => computeShotSound(input, options);
64
262
  return {
65
- shot(energyJ, category) {
66
- const k = Math.min(1, Math.max(0, Math.log10(Math.max(energyJ, 50) / 50) / 2.5));
67
- const shotgun = category === "shotgun";
68
- burst({ seconds: 0.12 + k * 0.35 + (shotgun ? 0.1 : 0), gain: 0.5 + k * 0.5, filter: "lowpass", frequency: 4200 - k * 2600 });
69
- burst({ seconds: 0.03, gain: 0.6, filter: "highpass", frequency: 3e3 });
70
- tone({ seconds: 0.14 + k * 0.2, gain: 0.35 + k * 0.4, frequency: 140 - k * 70, to: 40 });
263
+ shot(energyJ, category, extras = {}) {
264
+ const sound2 = shotSound({ ...extras, energyJ, category });
265
+ if (!sound2.audible) return;
266
+ classBurst(0, sound2.cutoff, sound2.gain, sound2.duration);
267
+ if (sound2.crack.gain > 0) burst({ seconds: sound2.crack.duration, gain: sound2.crack.gain, filter: "highpass", frequency: sound2.crack.cutoff });
268
+ tone({ seconds: sound2.body.duration, gain: sound2.body.gain, frequency: sound2.body.frequency, to: sound2.body.to });
71
269
  },
270
+ shotSound,
72
271
  click() {
73
- metal(0, 2600, 0.25);
272
+ mechanical("dry_fire");
273
+ },
274
+ mechanical,
275
+ event(event) {
276
+ const id = mechanicalSoundOf(event);
277
+ if (id) mechanical(id);
278
+ },
279
+ play(soundClass, classVolume, distance) {
280
+ play(soundClass, classVolume, distance);
74
281
  },
75
282
  reloadStart(category) {
76
- metal(0, 1500, 0.35);
77
- if (category !== "shotgun" && category !== "revolver") metal(0.12, 900, 0.2);
283
+ metal2(0, 1500, 0.35);
284
+ if (category !== "shotgun" && category !== "revolver") metal2(0.12, 900, 0.2);
78
285
  },
79
286
  reloadEnd(category) {
80
- metal(0, 1100, 0.45);
81
- if (category !== "revolver") metal(0.18, 1900, 0.4);
287
+ metal2(0, 1100, 0.45);
288
+ if (category !== "revolver") metal2(0.18, 1900, 0.4);
82
289
  },
83
290
  cycle() {
84
- metal(0.22, 900, 0.35);
85
- metal(0.36, 1300, 0.35);
291
+ mechanical("cycle");
86
292
  },
87
293
  clink() {
88
- const f = 3200 + Math.random() * 2e3;
294
+ const f = 3200 + rng() * 2e3;
89
295
  tone({ seconds: 0.09, gain: 0.12, frequency: f });
90
296
  tone({ seconds: 0.06, gain: 0.06, frequency: f * 2.7 });
91
297
  },
@@ -105,6 +311,2512 @@ function createWeaponAudio(volume = 0.6) {
105
311
  };
106
312
  }
107
313
 
314
+ // src/box-fx.ts
315
+ import { createRng as createRng2, muzzleEnergy as muzzleEnergy2 } from "@adatechnology/game-weapons";
316
+
317
+ // src/fx/layers.ts
318
+ var createLayers = () => ({ casings: true, magazines: true, muzzleFlash: true });
319
+
320
+ // src/merge-by-material.ts
321
+ var ATTRIBUTES = ["position", "normal", "uv"];
322
+ var isVisible = (object) => {
323
+ for (let node = object; node; node = node.parent) if (!node.visible) return false;
324
+ return true;
325
+ };
326
+ var bake = (THREE, mesh, matrix) => {
327
+ const source = mesh.geometry.index ? mesh.geometry.toNonIndexed() : mesh.geometry.clone();
328
+ const baked = new THREE.BufferGeometry();
329
+ const count = source.getAttribute("position").count;
330
+ for (const name of ATTRIBUTES) {
331
+ const attribute = source.getAttribute(name);
332
+ baked.setAttribute(name, attribute ?? new THREE.BufferAttribute(new Float32Array(count * (name === "uv" ? 2 : 3)), name === "uv" ? 2 : 3));
333
+ }
334
+ source.dispose();
335
+ return baked.applyMatrix4(matrix);
336
+ };
337
+ var join = (THREE, geometries) => {
338
+ const merged = new THREE.BufferGeometry();
339
+ for (const name of ATTRIBUTES) {
340
+ const size = (geometries[0]?.getAttribute(name)).itemSize;
341
+ const total = geometries.reduce((sum, geometry) => sum + geometry.getAttribute(name).count, 0);
342
+ const array = new Float32Array(total * size);
343
+ let offset = 0;
344
+ for (const geometry of geometries) {
345
+ array.set(geometry.getAttribute(name).array, offset);
346
+ offset += geometry.getAttribute(name).count * size;
347
+ }
348
+ merged.setAttribute(name, new THREE.BufferAttribute(array, size));
349
+ }
350
+ return merged;
351
+ };
352
+ function mergeByMaterial(THREE, root, space) {
353
+ const toSpace = space.matrixWorld.clone().invert();
354
+ const byMaterial = /* @__PURE__ */ new Map();
355
+ root.traverse((object) => {
356
+ const mesh = object;
357
+ if (!mesh.isMesh || Array.isArray(mesh.material) || !isVisible(mesh)) return;
358
+ const list = byMaterial.get(mesh.material) ?? [];
359
+ list.push(bake(THREE, mesh, new THREE.Matrix4().multiplyMatrices(toSpace, mesh.matrixWorld)));
360
+ byMaterial.set(mesh.material, list);
361
+ });
362
+ return [...byMaterial].map(([material, geometries]) => {
363
+ const merged = new THREE.Mesh(join(THREE, geometries), material);
364
+ for (const geometry of geometries) geometry.dispose();
365
+ merged.castShadow = false;
366
+ merged.receiveShadow = true;
367
+ return merged;
368
+ });
369
+ }
370
+
371
+ // src/models/index.ts
372
+ import { WEAPONS } from "@adatechnology/game-weapons";
373
+
374
+ // src/data/models.json
375
+ var models_default = {
376
+ glock_17: {
377
+ builder: "pistol",
378
+ caliber: "9\xD719",
379
+ length: 202,
380
+ barrel: 114,
381
+ options: {
382
+ st: "glock",
383
+ rel: 0.7
384
+ }
385
+ },
386
+ glock_18: {
387
+ builder: "pistol",
388
+ caliber: "9\xD719",
389
+ length: 202,
390
+ barrel: 114,
391
+ options: {
392
+ st: "glock",
393
+ sel: 1,
394
+ rpm: 1200,
395
+ rel: 0.9
396
+ }
397
+ },
398
+ glock_19: {
399
+ builder: "pistol",
400
+ caliber: "9\xD719",
401
+ length: 185,
402
+ barrel: 102,
403
+ options: {
404
+ st: "glock",
405
+ gl: 0.095,
406
+ rel: 0.7
407
+ }
408
+ },
409
+ taurus_g2c: {
410
+ builder: "pistol",
411
+ caliber: "9\xD719",
412
+ length: 159,
413
+ barrel: 81,
414
+ options: {
415
+ st: "g2c",
416
+ gl: 0.085,
417
+ rel: 1.5
418
+ }
419
+ },
420
+ taurus_ts9: {
421
+ builder: "pistol",
422
+ caliber: "9\xD719",
423
+ length: 194,
424
+ barrel: 104,
425
+ options: {
426
+ st: "taurus",
427
+ rel: 1.2
428
+ }
429
+ },
430
+ beretta_92fs: {
431
+ builder: "pistol",
432
+ caliber: "9\xD719",
433
+ length: 217,
434
+ barrel: 125,
435
+ options: {
436
+ st: "beretta",
437
+ h: 0.032,
438
+ rel: 0.9
439
+ }
440
+ },
441
+ sig_sauer_p226: {
442
+ builder: "pistol",
443
+ caliber: "9\xD719",
444
+ length: 196,
445
+ barrel: 112,
446
+ options: {
447
+ st: "sig",
448
+ h: 0.034,
449
+ rel: 0.8
450
+ }
451
+ },
452
+ cz_75: {
453
+ builder: "pistol",
454
+ caliber: "9\xD719",
455
+ length: 206,
456
+ barrel: 120,
457
+ options: {
458
+ st: "cz",
459
+ h: 0.03,
460
+ rel: 0.9
461
+ }
462
+ },
463
+ taurus_pt100: {
464
+ builder: "pistol",
465
+ caliber: ".40 S&W",
466
+ length: 216,
467
+ barrel: 127,
468
+ options: {
469
+ st: "pt100",
470
+ stain: 1,
471
+ rel: 1.2
472
+ }
473
+ },
474
+ colt_m1911: {
475
+ builder: "pistol",
476
+ caliber: ".45 ACP",
477
+ length: 216,
478
+ barrel: 127,
479
+ options: {
480
+ st: "m1911",
481
+ h: 0.029,
482
+ gl: 0.1,
483
+ rel: 1.1
484
+ }
485
+ },
486
+ makarov_pm: {
487
+ builder: "pistol",
488
+ caliber: "9\xD718",
489
+ length: 161,
490
+ barrel: 94,
491
+ options: {
492
+ st: "makarov",
493
+ h: 0.028,
494
+ gl: 0.088,
495
+ rel: 0.8
496
+ }
497
+ },
498
+ fn_five_seven: {
499
+ builder: "pistol",
500
+ caliber: "5,7\xD728",
501
+ length: 208,
502
+ barrel: 122,
503
+ options: {
504
+ st: "fiveseven",
505
+ gl: 0.12,
506
+ rel: 0.9
507
+ }
508
+ },
509
+ desert_eagle_mark_xix: {
510
+ builder: "pistol",
511
+ caliber: ".50 AE",
512
+ length: 270,
513
+ barrel: 152,
514
+ options: {
515
+ st: "deagle",
516
+ h: 0.04,
517
+ gl: 0.125,
518
+ deagle: 1,
519
+ steel: 1,
520
+ rel: 1.8
521
+ }
522
+ },
523
+ taurus_82: {
524
+ builder: "revolver",
525
+ caliber: ".38 SPL",
526
+ length: 240,
527
+ barrel: 102,
528
+ options: {
529
+ wood: 1
530
+ }
531
+ },
532
+ sw_686: {
533
+ builder: "revolver",
534
+ caliber: ".357 Magnum",
535
+ length: 244,
536
+ barrel: 102,
537
+ options: {
538
+ steel: 1,
539
+ chambers: 7
540
+ }
541
+ },
542
+ sw_model_29: {
543
+ builder: "revolver",
544
+ caliber: ".44 Magnum",
545
+ length: 302,
546
+ barrel: 165,
547
+ options: {
548
+ wood: 1
549
+ }
550
+ },
551
+ hk_mp5a3: {
552
+ builder: "rifle",
553
+ caliber: "9\xD719",
554
+ length: 700,
555
+ barrel: 225,
556
+ options: {
557
+ mag: "curved",
558
+ magLen: 0.72,
559
+ stock: "retract",
560
+ stockLen: 0.22,
561
+ hR: 0.055,
562
+ hg: "poly",
563
+ hgFrac: 0.85,
564
+ g3: 1,
565
+ rpm: 800,
566
+ sel: 1
567
+ }
568
+ },
569
+ taurus_smt9: {
570
+ builder: "rifle",
571
+ caliber: "9\xD719",
572
+ length: 660,
573
+ barrel: 200,
574
+ options: {
575
+ mag: "box",
576
+ stock: "retract",
577
+ stockLen: 0.2,
578
+ hg: "rail",
579
+ hgFrac: 0.8,
580
+ rail: 1,
581
+ rpm: 750,
582
+ sel: 1
583
+ }
584
+ },
585
+ hk_ump_45: {
586
+ builder: "rifle",
587
+ caliber: ".45 ACP",
588
+ length: 690,
589
+ barrel: 200,
590
+ options: {
591
+ mag: "box",
592
+ magLen: 1.1,
593
+ stock: "skeleton",
594
+ stockLen: 0.22,
595
+ hR: 0.07,
596
+ hg: "poly",
597
+ hgFrac: 0.85,
598
+ rpm: 600,
599
+ sel: 1,
600
+ rail: 1
601
+ }
602
+ },
603
+ uzi: {
604
+ builder: "rifle",
605
+ caliber: "9\xD719",
606
+ length: 650,
607
+ barrel: 260,
608
+ options: {
609
+ mag: "box",
610
+ magAt: 0.06,
611
+ magLen: 1.3,
612
+ stock: "fold",
613
+ stockLen: 0.2,
614
+ hR: 0.065,
615
+ hg: "poly",
616
+ hgFrac: 0.4,
617
+ grip: "none",
618
+ rpm: 600
619
+ }
620
+ },
621
+ fn_p90: {
622
+ builder: "p90",
623
+ caliber: "5,7\xD728",
624
+ length: 500,
625
+ barrel: 264,
626
+ options: {
627
+ rpm: 900
628
+ }
629
+ },
630
+ kriss_vector: {
631
+ builder: "rifle",
632
+ caliber: ".45 ACP",
633
+ length: 622,
634
+ barrel: 140,
635
+ options: {
636
+ mag: "box",
637
+ magLen: 1.1,
638
+ stock: "retract",
639
+ stockLen: 0.2,
640
+ hR: 0.085,
641
+ hg: "rail",
642
+ hgFrac: 0.9,
643
+ rail: 1,
644
+ scope: 1,
645
+ rpm: 1100
646
+ }
647
+ },
648
+ thompson_m1a1: {
649
+ builder: "rifle",
650
+ caliber: ".45 ACP",
651
+ length: 813,
652
+ barrel: 267,
653
+ options: {
654
+ wood: 1,
655
+ mag: "box",
656
+ magLen: 1.2,
657
+ stock: "fixed",
658
+ stockLen: 0.3,
659
+ hg: "none",
660
+ hR: 0.055,
661
+ jacket: 1,
662
+ rpm: 700,
663
+ charge: 1
664
+ }
665
+ },
666
+ cz_scorpion_evo_3: {
667
+ builder: "rifle",
668
+ caliber: "9\xD719",
669
+ length: 670,
670
+ barrel: 196,
671
+ options: {
672
+ mag: "box",
673
+ curve: 0.05,
674
+ magLen: 1.2,
675
+ stock: "fold",
676
+ stockLen: 0.22,
677
+ hg: "rail",
678
+ hgFrac: 0.85,
679
+ rail: 1,
680
+ scope: 1,
681
+ rpm: 1150
682
+ }
683
+ },
684
+ mac_10: {
685
+ builder: "rifle",
686
+ caliber: ".45 ACP",
687
+ length: 548,
688
+ barrel: 146,
689
+ options: {
690
+ mag: "box",
691
+ magAt: 0.04,
692
+ magLen: 1.2,
693
+ stock: "fold",
694
+ stockLen: 0.22,
695
+ hR: 0.075,
696
+ hg: "none",
697
+ grip: "none",
698
+ brake: 2,
699
+ rpm: 1090,
700
+ rel: 1.4
701
+ }
702
+ },
703
+ remington_870: {
704
+ builder: "rifle",
705
+ caliber: "12 ga",
706
+ length: 990,
707
+ barrel: 470,
708
+ options: {
709
+ wood: 1,
710
+ stock: "wood",
711
+ mag: "none",
712
+ tube: 1,
713
+ pump: 1,
714
+ hg: "none",
715
+ br: 0.0105,
716
+ hR: 0.055,
717
+ noFH: 1
718
+ }
719
+ },
720
+ mossberg_500: {
721
+ builder: "rifle",
722
+ caliber: "12 ga",
723
+ length: 990,
724
+ barrel: 470,
725
+ options: {
726
+ stock: "fixed",
727
+ mag: "none",
728
+ tube: 1,
729
+ pump: 1,
730
+ hg: "none",
731
+ br: 0.0105,
732
+ hR: 0.055,
733
+ noFH: 1
734
+ }
735
+ },
736
+ cbc_pump_military_3_0: {
737
+ builder: "rifle",
738
+ caliber: "12 ga",
739
+ length: 1e3,
740
+ barrel: 480,
741
+ options: {
742
+ stock: "fixed",
743
+ mag: "none",
744
+ tube: 1,
745
+ tubeLen: 0.95,
746
+ pump: 1,
747
+ hg: "none",
748
+ br: 0.0105,
749
+ hR: 0.055,
750
+ rail: 1,
751
+ noFH: 1
752
+ }
753
+ },
754
+ benelli_m4: {
755
+ builder: "rifle",
756
+ caliber: "12 ga",
757
+ length: 1010,
758
+ barrel: 470,
759
+ options: {
760
+ stock: "retract",
761
+ mag: "none",
762
+ tube: 1,
763
+ hg: "poly",
764
+ hgFrac: 0.55,
765
+ br: 0.0105,
766
+ hR: 0.06,
767
+ rail: 1,
768
+ scope: 1,
769
+ noFH: 1,
770
+ charge: 1
771
+ }
772
+ },
773
+ franchi_spas_12: {
774
+ builder: "rifle",
775
+ caliber: "12 ga",
776
+ length: 1041,
777
+ barrel: 546,
778
+ options: {
779
+ stock: "fold",
780
+ mag: "none",
781
+ tube: 1,
782
+ pump: 1,
783
+ hg: "none",
784
+ br: 0.0105,
785
+ hR: 0.06,
786
+ heat: 1,
787
+ noFH: 1
788
+ }
789
+ },
790
+ kel_tec_ksg: {
791
+ builder: "bullpup",
792
+ caliber: "12 ga",
793
+ length: 660,
794
+ barrel: 470,
795
+ options: {
796
+ shot: 1
797
+ }
798
+ },
799
+ saiga_12: {
800
+ builder: "rifle",
801
+ caliber: "12 ga",
802
+ length: 1e3,
803
+ barrel: 430,
804
+ options: {
805
+ mag: "box",
806
+ magLen: 1.15,
807
+ stock: "fixed",
808
+ hg: "poly",
809
+ br: 0.0105,
810
+ hR: 0.07,
811
+ gas: 1,
812
+ charge: 1,
813
+ ak: 1,
814
+ rel: 1.3
815
+ }
816
+ },
817
+ aa_12: {
818
+ builder: "rifle",
819
+ caliber: "12 ga",
820
+ length: 966,
821
+ barrel: 457,
822
+ options: {
823
+ mag: "drum",
824
+ stock: "fixed",
825
+ hg: "poly",
826
+ hgFrac: 0.6,
827
+ br: 0.0105,
828
+ hR: 0.085,
829
+ carry: 1,
830
+ rpm: 300
831
+ }
832
+ },
833
+ boito_double_barrel: {
834
+ builder: "break",
835
+ caliber: "12 ga",
836
+ length: 1130,
837
+ barrel: 710,
838
+ options: {}
839
+ },
840
+ ak_47: {
841
+ builder: "rifle",
842
+ caliber: "7,62\xD739",
843
+ length: 880,
844
+ barrel: 415,
845
+ options: {
846
+ wood: 1,
847
+ stock: "wood",
848
+ mag: "curved",
849
+ hg: "wood",
850
+ hgFrac: 0.42,
851
+ gas: 1,
852
+ charge: 1,
853
+ ak: 1,
854
+ rod: 1,
855
+ bayo: 1,
856
+ rpm: 600,
857
+ rel: 0.4
858
+ }
859
+ },
860
+ akm: {
861
+ builder: "rifle",
862
+ caliber: "7,62\xD739",
863
+ length: 880,
864
+ barrel: 415,
865
+ options: {
866
+ wood: 1,
867
+ stock: "wood",
868
+ mag: "curved",
869
+ hg: "wood",
870
+ hgFrac: 0.42,
871
+ gas: 1,
872
+ charge: 1,
873
+ ak: 1,
874
+ rod: 1,
875
+ rpm: 600,
876
+ magMat: "bakelite",
877
+ rel: 0.45
878
+ }
879
+ },
880
+ ak_74: {
881
+ builder: "rifle",
882
+ caliber: "5,45\xD739",
883
+ length: 943,
884
+ barrel: 415,
885
+ options: {
886
+ stock: "fixed",
887
+ mag: "curved",
888
+ hg: "poly",
889
+ hgFrac: 0.42,
890
+ gas: 1,
891
+ charge: 1,
892
+ brake: 1,
893
+ ak: 1,
894
+ rod: 1,
895
+ rpm: 650,
896
+ magMat: "bakelite"
897
+ }
898
+ },
899
+ ak_12: {
900
+ builder: "rifle",
901
+ caliber: "5,45\xD739",
902
+ length: 945,
903
+ barrel: 415,
904
+ options: {
905
+ stock: "retract",
906
+ mag: "curved",
907
+ hg: "rail",
908
+ hgFrac: 0.5,
909
+ rail: 1,
910
+ brake: 1,
911
+ scope: 1,
912
+ charge: 1,
913
+ ak: 1,
914
+ rpm: 700,
915
+ magMat: "poly"
916
+ }
917
+ },
918
+ imbel_ia2: {
919
+ builder: "rifle",
920
+ caliber: "5,56\xD745",
921
+ length: 850,
922
+ barrel: 330,
923
+ options: {
924
+ stock: "fold",
925
+ mag: "box",
926
+ curve: 0.07,
927
+ magLen: 1.25,
928
+ hg: "poly",
929
+ hgFrac: 0.6,
930
+ rail: 1,
931
+ charge: 1,
932
+ rpm: 700
933
+ }
934
+ },
935
+ taurus_t4: {
936
+ builder: "rifle",
937
+ caliber: "5,56\xD745",
938
+ length: 840,
939
+ barrel: 368,
940
+ options: {
941
+ stock: "retract",
942
+ mag: "box",
943
+ curve: 0.07,
944
+ magLen: 1.3,
945
+ hg: "rail",
946
+ hgFrac: 0.7,
947
+ rail: 1,
948
+ scope: 1,
949
+ ar: 1,
950
+ rpm: 750
951
+ }
952
+ },
953
+ colt_m4a1: {
954
+ builder: "rifle",
955
+ caliber: "5,56\xD745",
956
+ length: 840,
957
+ barrel: 368,
958
+ options: {
959
+ stock: "retract",
960
+ mag: "box",
961
+ curve: 0.07,
962
+ magLen: 1.3,
963
+ hg: "rail",
964
+ hgFrac: 0.6,
965
+ rail: 1,
966
+ scope: 2,
967
+ ar: 1,
968
+ rpm: 800
969
+ }
970
+ },
971
+ colt_m16a2: {
972
+ builder: "rifle",
973
+ caliber: "5,56\xD745",
974
+ length: 1e3,
975
+ barrel: 508,
976
+ options: {
977
+ stock: "fixed",
978
+ mag: "box",
979
+ curve: 0.07,
980
+ magLen: 1.3,
981
+ hg: "poly",
982
+ hgFrac: 0.55,
983
+ carry: 1,
984
+ ar: 1,
985
+ rpm: 800,
986
+ burst: 3
987
+ }
988
+ },
989
+ hk416: {
990
+ builder: "rifle",
991
+ caliber: "5,56\xD745",
992
+ length: 900,
993
+ barrel: 368,
994
+ options: {
995
+ stock: "retract",
996
+ mag: "box",
997
+ curve: 0.07,
998
+ magLen: 1.3,
999
+ hg: "rail",
1000
+ hgFrac: 0.75,
1001
+ rail: 1,
1002
+ scope: 1,
1003
+ ar: 1,
1004
+ rpm: 850
1005
+ }
1006
+ },
1007
+ steyr_aug_a1: {
1008
+ builder: "bullpup",
1009
+ caliber: "5,56\xD745",
1010
+ length: 790,
1011
+ barrel: 508,
1012
+ options: {
1013
+ rpm: 680
1014
+ }
1015
+ },
1016
+ fn_fal: {
1017
+ builder: "rifle",
1018
+ caliber: "7,62\xD751",
1019
+ length: 1090,
1020
+ barrel: 533,
1021
+ options: {
1022
+ stock: "fixed",
1023
+ mag: "box",
1024
+ curve: 0.05,
1025
+ magLen: 1.05,
1026
+ hg: "poly",
1027
+ hgFrac: 0.45,
1028
+ carry: 1,
1029
+ brake: 1,
1030
+ charge: 1,
1031
+ rpm: 650
1032
+ }
1033
+ },
1034
+ hk_g3a3: {
1035
+ builder: "rifle",
1036
+ caliber: "7,62\xD751",
1037
+ length: 1025,
1038
+ barrel: 450,
1039
+ options: {
1040
+ stock: "fixed",
1041
+ mag: "box",
1042
+ curve: 0.07,
1043
+ magLen: 1.05,
1044
+ hg: "poly",
1045
+ hgFrac: 0.55,
1046
+ hR: 0.058,
1047
+ g3: 1,
1048
+ rpm: 550
1049
+ }
1050
+ },
1051
+ sks: {
1052
+ builder: "rifle",
1053
+ caliber: "7,62\xD739",
1054
+ length: 1020,
1055
+ barrel: 520,
1056
+ options: {
1057
+ wood: 1,
1058
+ stock: "trad",
1059
+ mag: "internal",
1060
+ fore: 0.55,
1061
+ gas: 1,
1062
+ charge: 1,
1063
+ bayo: 1
1064
+ }
1065
+ },
1066
+ m14_ebr: {
1067
+ builder: "rifle",
1068
+ caliber: "7,62\xD751",
1069
+ length: 1e3,
1070
+ barrel: 457,
1071
+ options: {
1072
+ stock: "retract",
1073
+ mag: "box",
1074
+ curve: 0.035,
1075
+ magLen: 1.05,
1076
+ hg: "rail",
1077
+ hgFrac: 0.7,
1078
+ rail: 1,
1079
+ scope: 3,
1080
+ olive: 1,
1081
+ charge: 1
1082
+ }
1083
+ },
1084
+ svd_dragunov: {
1085
+ builder: "rifle",
1086
+ caliber: "7,62\xD754R",
1087
+ length: 1225,
1088
+ barrel: 620,
1089
+ options: {
1090
+ wood: 1,
1091
+ stock: "skeleton",
1092
+ stockLen: 0.32,
1093
+ mag: "box",
1094
+ curve: 0.09,
1095
+ magLen: 0.9,
1096
+ hg: "wood",
1097
+ hgFrac: 0.4,
1098
+ scope: 4,
1099
+ brake: 1,
1100
+ ak: 1,
1101
+ charge: 1
1102
+ }
1103
+ },
1104
+ fn_scar_h: {
1105
+ builder: "rifle",
1106
+ caliber: "7,62\xD751",
1107
+ length: 965,
1108
+ barrel: 400,
1109
+ options: {
1110
+ tan: 1,
1111
+ stock: "fold",
1112
+ mag: "box",
1113
+ magLen: 1.05,
1114
+ hg: "rail",
1115
+ hgFrac: 0.7,
1116
+ hR: 0.07,
1117
+ rail: 1,
1118
+ scope: 2,
1119
+ charge: 1,
1120
+ rpm: 600
1121
+ }
1122
+ },
1123
+ knights_sr_25_mk11: {
1124
+ builder: "rifle",
1125
+ caliber: "7,62\xD751",
1126
+ length: 1156,
1127
+ barrel: 508,
1128
+ options: {
1129
+ stock: "fixed",
1130
+ mag: "box",
1131
+ magLen: 1,
1132
+ hg: "rail",
1133
+ hgFrac: 0.75,
1134
+ rail: 1,
1135
+ scope: 3,
1136
+ bipod: 1,
1137
+ ar: 1
1138
+ }
1139
+ },
1140
+ hk_psg_1: {
1141
+ builder: "rifle",
1142
+ caliber: "7,62\xD751",
1143
+ length: 1208,
1144
+ barrel: 650,
1145
+ options: {
1146
+ stock: "fixed",
1147
+ stockLen: 0.3,
1148
+ mag: "box",
1149
+ curve: 0.06,
1150
+ magLen: 0.75,
1151
+ hg: "poly",
1152
+ hgFrac: 0.55,
1153
+ hR: 0.058,
1154
+ g3: 1,
1155
+ scope: 3,
1156
+ rel: 0.7
1157
+ }
1158
+ },
1159
+ steyr_ssg_69: {
1160
+ builder: "rifle",
1161
+ caliber: "7,62\xD751",
1162
+ length: 1140,
1163
+ barrel: 650,
1164
+ options: {
1165
+ olive: 1,
1166
+ stock: "trad",
1167
+ mag: "internal",
1168
+ scope: 3,
1169
+ bolt: 1,
1170
+ round: 1,
1171
+ rel: 0.6
1172
+ }
1173
+ },
1174
+ barrett_mrad: {
1175
+ builder: "rifle",
1176
+ caliber: ".338 Lapua",
1177
+ length: 1260,
1178
+ barrel: 686,
1179
+ options: {
1180
+ tan: 1,
1181
+ stock: "fold",
1182
+ stockLen: 0.33,
1183
+ mag: "box",
1184
+ magLen: 0.75,
1185
+ hg: "rail",
1186
+ hgFrac: 0.55,
1187
+ rail: 1,
1188
+ scope: 3,
1189
+ bolt: 1,
1190
+ bipod: 1,
1191
+ brake: 1,
1192
+ round: 1
1193
+ }
1194
+ },
1195
+ cheytac_m200: {
1196
+ builder: "rifle",
1197
+ caliber: ".408 CheyTac",
1198
+ length: 1397,
1199
+ barrel: 737,
1200
+ options: {
1201
+ stock: "retract",
1202
+ stockLen: 0.3,
1203
+ mag: "box",
1204
+ magLen: 0.85,
1205
+ hg: "poly",
1206
+ hgFrac: 0.55,
1207
+ hR: 0.07,
1208
+ scope: 4,
1209
+ bolt: 1,
1210
+ bipod: 1,
1211
+ brake: 1,
1212
+ round: 1
1213
+ }
1214
+ },
1215
+ remington_700: {
1216
+ builder: "rifle",
1217
+ caliber: ".308",
1218
+ length: 1100,
1219
+ barrel: 610,
1220
+ options: {
1221
+ wood: 1,
1222
+ stock: "trad",
1223
+ mag: "internal",
1224
+ scope: 3,
1225
+ bolt: 1,
1226
+ round: 1
1227
+ }
1228
+ },
1229
+ m24_sws: {
1230
+ builder: "rifle",
1231
+ caliber: "7,62\xD751",
1232
+ length: 1092,
1233
+ barrel: 610,
1234
+ options: {
1235
+ olive: 1,
1236
+ stock: "trad",
1237
+ mag: "internal",
1238
+ scope: 3,
1239
+ bolt: 1,
1240
+ bipod: 1,
1241
+ round: 1
1242
+ }
1243
+ },
1244
+ mosin_nagant_91_30: {
1245
+ builder: "rifle",
1246
+ caliber: "7,62\xD754R",
1247
+ length: 1232,
1248
+ barrel: 730,
1249
+ options: {
1250
+ wood: 1,
1251
+ stock: "trad",
1252
+ mag: "internal",
1253
+ fore: 0.85,
1254
+ bolt: 1,
1255
+ round: 1,
1256
+ bands: 1
1257
+ }
1258
+ },
1259
+ accuracy_awm: {
1260
+ builder: "rifle",
1261
+ caliber: ".338 Lapua",
1262
+ length: 1230,
1263
+ barrel: 686,
1264
+ options: {
1265
+ olive: 1,
1266
+ stock: "skeleton",
1267
+ stockLen: 0.34,
1268
+ mag: "box",
1269
+ magLen: 0.6,
1270
+ hg: "poly",
1271
+ hgFrac: 0.3,
1272
+ scope: 3,
1273
+ bolt: 1,
1274
+ bipod: 1,
1275
+ brake: 1,
1276
+ round: 1
1277
+ }
1278
+ },
1279
+ sako_trg_42: {
1280
+ builder: "rifle",
1281
+ caliber: ".338 Lapua",
1282
+ length: 1200,
1283
+ barrel: 690,
1284
+ options: {
1285
+ stock: "fixed",
1286
+ stockLen: 0.33,
1287
+ mag: "box",
1288
+ magLen: 0.6,
1289
+ hg: "poly",
1290
+ hgFrac: 0.3,
1291
+ scope: 3,
1292
+ bolt: 1,
1293
+ bipod: 1,
1294
+ brake: 1,
1295
+ round: 1
1296
+ }
1297
+ },
1298
+ barrett_m82a1: {
1299
+ builder: "rifle",
1300
+ caliber: ".50 BMG",
1301
+ length: 1448,
1302
+ barrel: 737,
1303
+ options: {
1304
+ stock: "fixed",
1305
+ stockLen: 0.3,
1306
+ mag: "box",
1307
+ magLen: 0.8,
1308
+ hg: "poly",
1309
+ hgFrac: 0.55,
1310
+ hR: 0.1,
1311
+ wR: 0.05,
1312
+ br: 0.014,
1313
+ scope: 3,
1314
+ bipod: 1,
1315
+ brake: 2,
1316
+ carry: 1,
1317
+ charge: 1
1318
+ }
1319
+ },
1320
+ ruger_10_22: {
1321
+ builder: "rifle",
1322
+ caliber: ".22 LR",
1323
+ length: 940,
1324
+ barrel: 470,
1325
+ options: {
1326
+ wood: 1,
1327
+ stock: "trad",
1328
+ mag: "internal",
1329
+ hR: 0.05,
1330
+ br: 7e-3,
1331
+ charge: 1
1332
+ }
1333
+ },
1334
+ cbc_7022: {
1335
+ builder: "rifle",
1336
+ caliber: ".22 LR",
1337
+ length: 1e3,
1338
+ barrel: 530,
1339
+ options: {
1340
+ stock: "trad",
1341
+ mag: "internal",
1342
+ hR: 0.05,
1343
+ br: 7e-3,
1344
+ charge: 1
1345
+ }
1346
+ },
1347
+ cbc_122: {
1348
+ builder: "rifle",
1349
+ caliber: ".22 LR",
1350
+ length: 1030,
1351
+ barrel: 590,
1352
+ options: {
1353
+ wood: 1,
1354
+ stock: "trad",
1355
+ mag: "internal",
1356
+ hR: 0.045,
1357
+ br: 7e-3,
1358
+ bolt: 1,
1359
+ round: 1
1360
+ }
1361
+ },
1362
+ winchester_1894: {
1363
+ builder: "rifle",
1364
+ caliber: ".30-30",
1365
+ length: 965,
1366
+ barrel: 508,
1367
+ options: {
1368
+ wood: 1,
1369
+ stock: "straight",
1370
+ mag: "none",
1371
+ tube: 1,
1372
+ lever: 1,
1373
+ hR: 0.05,
1374
+ fore: 0,
1375
+ bands: 1
1376
+ }
1377
+ },
1378
+ rossi_puma: {
1379
+ builder: "rifle",
1380
+ caliber: ".38 SPL / .357",
1381
+ length: 950,
1382
+ barrel: 508,
1383
+ options: {
1384
+ wood: 1,
1385
+ stock: "straight",
1386
+ mag: "none",
1387
+ tube: 1,
1388
+ lever: 1,
1389
+ hR: 0.048,
1390
+ fore: 0
1391
+ }
1392
+ },
1393
+ hunting_rifle_30_06: {
1394
+ builder: "rifle",
1395
+ caliber: ".30-06",
1396
+ length: 1100,
1397
+ barrel: 560,
1398
+ options: {
1399
+ wood: 1,
1400
+ stock: "trad",
1401
+ mag: "internal",
1402
+ scope: 3,
1403
+ bolt: 1,
1404
+ round: 1
1405
+ }
1406
+ },
1407
+ fn_minimi: {
1408
+ builder: "rifle",
1409
+ caliber: "5,56\xD745",
1410
+ length: 1040,
1411
+ barrel: 465,
1412
+ options: {
1413
+ stock: "fixed",
1414
+ mag: "belt",
1415
+ hg: "poly",
1416
+ hgFrac: 0.35,
1417
+ bipod: 1,
1418
+ hR: 0.08,
1419
+ carry: 1,
1420
+ charge: 1,
1421
+ rpm: 850
1422
+ }
1423
+ },
1424
+ pkm: {
1425
+ builder: "rifle",
1426
+ caliber: "7,62\xD754R",
1427
+ length: 1173,
1428
+ barrel: 658,
1429
+ options: {
1430
+ wood: 1,
1431
+ stock: "skeleton",
1432
+ mag: "belt",
1433
+ hg: "none",
1434
+ bipod: 1,
1435
+ hR: 0.08,
1436
+ br: 0.01,
1437
+ charge: 1,
1438
+ rpm: 650
1439
+ }
1440
+ },
1441
+ m60: {
1442
+ builder: "rifle",
1443
+ caliber: "7,62\xD751",
1444
+ length: 1105,
1445
+ barrel: 560,
1446
+ options: {
1447
+ stock: "fixed",
1448
+ mag: "belt",
1449
+ hg: "poly",
1450
+ hgFrac: 0.3,
1451
+ bipod: 1,
1452
+ hR: 0.085,
1453
+ br: 0.011,
1454
+ charge: 1,
1455
+ rpm: 550,
1456
+ rel: 1.3
1457
+ }
1458
+ },
1459
+ rpg_7: {
1460
+ builder: "launcher",
1461
+ caliber: "PG-7V 40/85 mm",
1462
+ length: 950,
1463
+ barrel: 950,
1464
+ options: {
1465
+ lt: "rpg"
1466
+ }
1467
+ },
1468
+ m72_law: {
1469
+ builder: "launcher",
1470
+ caliber: "66 mm HEAT",
1471
+ length: 881,
1472
+ barrel: 881,
1473
+ options: {
1474
+ lt: "m72"
1475
+ }
1476
+ },
1477
+ at4: {
1478
+ builder: "launcher",
1479
+ caliber: "84 mm HEAT",
1480
+ length: 1020,
1481
+ barrel: 1020,
1482
+ options: {
1483
+ lt: "at4"
1484
+ }
1485
+ },
1486
+ carl_gustaf_m4: {
1487
+ builder: "launcher",
1488
+ caliber: "84 mm (HE / HEAT)",
1489
+ length: 950,
1490
+ barrel: 950,
1491
+ options: {
1492
+ lt: "carl"
1493
+ }
1494
+ },
1495
+ m2_flamethrower: {
1496
+ builder: "launcher",
1497
+ caliber: "Napalm / gasolina gel",
1498
+ length: 1e3,
1499
+ barrel: 620,
1500
+ options: {
1501
+ lt: "flame"
1502
+ }
1503
+ },
1504
+ improvised_flamethrower: {
1505
+ builder: "launcher",
1506
+ caliber: "Gasolina + extintor pressurizado",
1507
+ length: 800,
1508
+ barrel: 660,
1509
+ options: {
1510
+ lt: "flameDIY"
1511
+ }
1512
+ },
1513
+ browning_m2: {
1514
+ builder: "m2",
1515
+ caliber: ".50 BMG",
1516
+ length: 1650,
1517
+ barrel: 1140,
1518
+ options: {
1519
+ rpm: 550
1520
+ }
1521
+ },
1522
+ remington_700_300wm: {
1523
+ builder: "rifle",
1524
+ caliber: ".308",
1525
+ length: 1100,
1526
+ barrel: 610,
1527
+ options: {
1528
+ wood: 1,
1529
+ stock: "trad",
1530
+ mag: "internal",
1531
+ scope: 3,
1532
+ bolt: 1,
1533
+ round: 1
1534
+ },
1535
+ variantOf: "remington_700"
1536
+ }
1537
+ };
1538
+
1539
+ // src/models/rounded-box.ts
1540
+ var uvAlong = (THREE, faceDirection, normal, uvAxis, projectionAxis, radius, sideLength) => {
1541
+ const arcLength = 2 * Math.PI * radius / 4;
1542
+ const centerLength = Math.max(sideLength - 2 * radius, 0);
1543
+ const halfArc = Math.PI / 4;
1544
+ const projected = new THREE.Vector3().copy(normal);
1545
+ projected[projectionAxis] = 0;
1546
+ projected.normalize();
1547
+ const arcUvRatio = 0.5 * arcLength / (arcLength + centerLength);
1548
+ const arcAngleRatio = 1 - projected.angleTo(faceDirection) / halfArc;
1549
+ if (Math.sign(projected[uvAxis]) === 1) return arcAngleRatio * arcUvRatio;
1550
+ const centerUv = centerLength / (arcLength + centerLength);
1551
+ return centerUv + arcUvRatio + arcUvRatio * (1 - arcAngleRatio);
1552
+ };
1553
+ var FACES = [
1554
+ { direction: [1, 0, 0], u: ["z", "y", false], v: ["y", "z", true] },
1555
+ { direction: [-1, 0, 0], u: ["z", "y", true], v: ["y", "z", true] },
1556
+ { direction: [0, 1, 0], u: ["x", "z", true], v: ["z", "x", false] },
1557
+ { direction: [0, -1, 0], u: ["x", "z", true], v: ["z", "x", true] },
1558
+ { direction: [0, 0, 1], u: ["x", "y", true], v: ["y", "x", true] },
1559
+ { direction: [0, 0, -1], u: ["x", "y", false], v: ["y", "x", true] }
1560
+ ];
1561
+ function createRoundedBoxGeometry(THREE, width, height, depth, segments, radius) {
1562
+ const count = segments * 2 + 1;
1563
+ const rounding = Math.min(width / 2, height / 2, depth / 2, radius);
1564
+ const geometry = new THREE.BoxGeometry(1, 1, 1, count, count, count);
1565
+ const flat = geometry.toNonIndexed();
1566
+ geometry.setIndex(null);
1567
+ geometry.setAttribute("position", flat.attributes.position);
1568
+ geometry.setAttribute("normal", flat.attributes.normal);
1569
+ geometry.setAttribute("uv", flat.attributes.uv);
1570
+ const position = new THREE.Vector3();
1571
+ const normal = new THREE.Vector3();
1572
+ const box2 = new THREE.Vector3(width, height, depth).divideScalar(2).subScalar(rounding);
1573
+ const positions = geometry.attributes.position.array;
1574
+ const normals = geometry.attributes.normal.array;
1575
+ const uvs = geometry.attributes.uv.array;
1576
+ const faceTriangles = positions.length / 6;
1577
+ const halfSegment = 0.5 / count;
1578
+ const sides = { x: width, y: height, z: depth };
1579
+ for (let i = 0, j = 0; i < positions.length; i += 3, j += 2) {
1580
+ position.fromArray(positions, i);
1581
+ normal.copy(position);
1582
+ normal.x -= Math.sign(normal.x) * halfSegment;
1583
+ normal.y -= Math.sign(normal.y) * halfSegment;
1584
+ normal.z -= Math.sign(normal.z) * halfSegment;
1585
+ normal.normalize();
1586
+ positions[i] = box2.x * Math.sign(position.x) + normal.x * rounding;
1587
+ positions[i + 1] = box2.y * Math.sign(position.y) + normal.y * rounding;
1588
+ positions[i + 2] = box2.z * Math.sign(position.z) + normal.z * rounding;
1589
+ normals[i] = normal.x;
1590
+ normals[i + 1] = normal.y;
1591
+ normals[i + 2] = normal.z;
1592
+ const face = FACES[Math.floor(i / faceTriangles)];
1593
+ if (!face) continue;
1594
+ const direction = new THREE.Vector3(...face.direction);
1595
+ for (const [offset, [uvAxis, projectionAxis, mirrored]] of [[0, face.u], [1, face.v]]) {
1596
+ const value = uvAlong(THREE, direction, normal, uvAxis, projectionAxis, rounding, sides[uvAxis]);
1597
+ uvs[j + offset] = mirrored ? 1 - value : value;
1598
+ }
1599
+ }
1600
+ return geometry;
1601
+ }
1602
+
1603
+ // src/models/kit.ts
1604
+ var PI = Math.PI;
1605
+ var createMaterials = (THREE) => {
1606
+ const standard = (color, roughness = 0.7, metalness = 0.2, extra = {}) => new THREE.MeshStandardMaterial({ color, roughness, metalness, ...extra });
1607
+ const basic = (color, extra = {}) => new THREE.MeshBasicMaterial({ color, ...extra });
1608
+ const additive = { transparent: true, opacity: 0.95, blending: THREE.AdditiveBlending, depthWrite: false };
1609
+ return {
1610
+ metal: standard(3422010, 0.42, 0.75),
1611
+ steel: standard(7106418, 0.32, 0.85),
1612
+ steelL: standard(9080208, 0.28, 0.9),
1613
+ dark: standard(1711133, 0.55, 0.5),
1614
+ poly: standard(2369058, 0.8, 0.05),
1615
+ olive: standard(4936241, 0.8, 0.05),
1616
+ tan: standard(10259043, 0.8, 0.05),
1617
+ wood: standard(4860951, 0.55, 0),
1618
+ woodL: standard(6175263, 0.55, 0),
1619
+ brass: standard(12819013, 0.28, 0.9),
1620
+ copper: standard(11561531, 0.3, 0.85),
1621
+ lead: standard(7171955, 0.5, 0.5),
1622
+ shell: standard(10692380, 0.5, 0.05),
1623
+ glass: standard(8364968, 0.05, 0.1, { transparent: true, opacity: 0.42 }),
1624
+ lens: standard(2311770, 0.05, 0.3, { transparent: true, opacity: 0.9, emissive: 399392 }),
1625
+ rubber: standard(1381653, 0.85, 0),
1626
+ white: standard(13617332, 0.8, 0),
1627
+ hole: standard(526343, 1, 0),
1628
+ bake: standard(5911072, 0.55, 0),
1629
+ rust: standard(7027230, 0.95, 0.3),
1630
+ rustD: standard(4860440, 0.95, 0.25),
1631
+ worn: standard(5593178, 0.6, 0.6),
1632
+ fire: basic(16742946),
1633
+ fire2: basic(16764762),
1634
+ flashA: basic(16757575, additive),
1635
+ flashB: basic(16773568, additive),
1636
+ smokeP: standard(9079430, 1, 0, { transparent: true, opacity: 0.35, depthWrite: false }),
1637
+ /** Launcher paints. */
1638
+ yellow: standard(11570978, 0.6, 0),
1639
+ red: standard(9313300, 0.45, 0.1),
1640
+ redPaint: standard(10691354, 0.35, 0.2),
1641
+ tape: standard(9079944, 0.9, 0),
1642
+ pipe: standard(10115634, 0.35, 0.8),
1643
+ green: standard(3818536, 0.75, 0.05),
1644
+ /** Magazine finishes named by the options. */
1645
+ bakelite: standard(6958620, 0.6, 0),
1646
+ polyMag: standard(2763302, 0.85, 0)
1647
+ };
1648
+ };
1649
+ var kits = /* @__PURE__ */ new WeakMap();
1650
+ function modelKit(THREE) {
1651
+ const cached = kits.get(THREE);
1652
+ if (cached) return cached;
1653
+ const geometries = /* @__PURE__ */ new Map();
1654
+ const shadowed = (mesh) => {
1655
+ mesh.castShadow = true;
1656
+ mesh.receiveShadow = true;
1657
+ return mesh;
1658
+ };
1659
+ const place = (parent, mesh, x, y, z) => {
1660
+ mesh.position.set(x, y, z);
1661
+ parent.add(mesh);
1662
+ return mesh;
1663
+ };
1664
+ const kit = {
1665
+ THREE,
1666
+ M: createMaterials(THREE),
1667
+ roundness: null,
1668
+ group: () => new THREE.Group(),
1669
+ box(parent, w, h, d, material, x, y, z, rz = 0, ry = 0, rx = 0) {
1670
+ const radius = Math.min(w, h, d) * (kit.roundness ?? 0.2);
1671
+ const key = `${w.toFixed(4)},${h.toFixed(4)},${d.toFixed(4)},${radius.toFixed(5)}`;
1672
+ let geometry = geometries.get(key);
1673
+ if (!geometry) {
1674
+ geometry = radius > 25e-5 ? createRoundedBoxGeometry(THREE, w, h, d, 2, radius) : new THREE.BoxGeometry(w, h, d);
1675
+ geometries.set(key, geometry);
1676
+ }
1677
+ const mesh = place(parent, shadowed(new THREE.Mesh(geometry, material)), x, y, z);
1678
+ mesh.rotation.set(rx, ry, rz);
1679
+ return mesh;
1680
+ },
1681
+ cylinderX(parent, radius, length, material, x, y, z, segments = 16, radiusEnd) {
1682
+ const mesh = shadowed(new THREE.Mesh(new THREE.CylinderGeometry(radiusEnd ?? radius, radius, length, segments), material));
1683
+ mesh.rotation.z = -PI / 2;
1684
+ return place(parent, mesh, x, y, z);
1685
+ },
1686
+ cylinderY(parent, radius, height, material, x, y, z, segments = 16, radiusEnd) {
1687
+ return place(parent, shadowed(new THREE.Mesh(new THREE.CylinderGeometry(radiusEnd ?? radius, radius, height, segments), material)), x, y, z);
1688
+ },
1689
+ cylinderZ(parent, radius, length, material, x, y, z, segments = 16) {
1690
+ const mesh = shadowed(new THREE.Mesh(new THREE.CylinderGeometry(radius, radius, length, segments), material));
1691
+ mesh.rotation.x = PI / 2;
1692
+ return place(parent, mesh, x, y, z);
1693
+ },
1694
+ sphere(parent, radius, material, x, y, z, detail = 1) {
1695
+ return place(parent, shadowed(new THREE.Mesh(new THREE.IcosahedronGeometry(radius, detail), material)), x, y, z);
1696
+ },
1697
+ torus: (parent, geometry, material, x, y, z) => place(parent, shadowed(new THREE.Mesh(geometry, material)), x, y, z),
1698
+ mark(parent, x, y, z) {
1699
+ const object = new THREE.Object3D();
1700
+ object.position.set(x, y, z);
1701
+ parent.add(object);
1702
+ return object;
1703
+ },
1704
+ pivot(parent, x = 0, y = 0, z = 0) {
1705
+ const group = new THREE.Group();
1706
+ group.position.set(x, y, z);
1707
+ parent.add(group);
1708
+ return group;
1709
+ }
1710
+ };
1711
+ kits.set(THREE, kit);
1712
+ return kit;
1713
+ }
1714
+ function heatify(glow, mesh) {
1715
+ mesh.material = mesh.material.clone();
1716
+ glow.push(mesh);
1717
+ return mesh;
1718
+ }
1719
+
1720
+ // src/models/break-action.ts
1721
+ function buildBreakAction(kit, options) {
1722
+ const { M, box: box2, cylinderX, sphere } = kit;
1723
+ const group = kit.group();
1724
+ const L = (options.L ?? 0) / 1e3;
1725
+ const barrel = (options.B ?? 0) / 1e3;
1726
+ const parts = { family: "break", kick: 1.6, glow: [] };
1727
+ box2(group, 0.09, 0.05, 0.042, M.steel, L - barrel - 0.04, -5e-3, 0);
1728
+ for (let i = 0; i < 4; i++) box2(group, 0.05, 2e-3, 0.043, M.dark, L - barrel - 0.04, 0.01 - i * 8e-3, 0);
1729
+ const barrels = kit.pivot(group, L - barrel, -0.01, 0);
1730
+ parts.barrels = barrels;
1731
+ heatify(parts.glow, cylinderX(barrels, 0.0105, barrel, M.steel, barrel / 2, 0.01, 0.011, 18));
1732
+ heatify(parts.glow, cylinderX(barrels, 0.0105, barrel, M.steel, barrel / 2, 0.01, -0.011, 18));
1733
+ box2(barrels, barrel, 4e-3, 8e-3, M.metal, barrel / 2, 0.022, 0);
1734
+ sphere(barrels, 2e-3, M.white, barrel - 0.01, 0.026, 0, 1);
1735
+ cylinderX(barrels, 75e-4, 4e-3, M.hole, barrel + 1e-3, 0.01, 0.011, 12);
1736
+ cylinderX(barrels, 75e-4, 4e-3, M.hole, barrel + 1e-3, 0.01, -0.011, 12);
1737
+ box2(barrels, 0.22, 0.03, 0.038, M.wood, 0.12, -0.01, 0);
1738
+ parts.breech = [kit.mark(barrels, 4e-3, 0.01, 0.011), kit.mark(barrels, 4e-3, 0.01, -0.011)];
1739
+ box2(group, 0.12, 0.042, 0.032, M.wood, L - barrel - 0.14, -0.022, 0, 0.14);
1740
+ box2(group, 0.28, 0.07, 0.036, M.wood, 0.15, -0.05, 0, 0.08);
1741
+ box2(group, 0.012, 0.13, 0.04, M.rubber, 6e-3, -0.06, 0);
1742
+ box2(group, 4e-3, 0.014, 4e-3, M.dark, L - barrel - 0.05, -0.04, 0);
1743
+ parts.trigger = box2(group, 4e-3, 0.014, 4e-3, M.dark, L - barrel - 0.035, -0.04, 0);
1744
+ box2(group, 0.04, 4e-3, 0.01, M.steel, L - barrel - 0.045, -0.05, 0);
1745
+ box2(group, 0.012, 0.016, 6e-3, M.dark, L - barrel - 0.08, 0.022, 0.012, 0.5);
1746
+ box2(group, 0.012, 0.016, 6e-3, M.dark, L - barrel - 0.08, 0.022, -0.012, 0.5);
1747
+ parts.safety = box2(group, 0.02, 6e-3, 8e-3, M.steel, L - barrel - 0.07, 0.025, 0);
1748
+ parts.muzzle = kit.mark(barrels, barrel + 0.01, 0.01, 0);
1749
+ parts.eject = parts.breech[0];
1750
+ return { group, parts };
1751
+ }
1752
+
1753
+ // src/models/details.ts
1754
+ var PI2 = Math.PI;
1755
+ function rail(kit, parent, x, length, y, z, width, rotationX = 0) {
1756
+ const { M, box: box2 } = kit;
1757
+ const pivot = kit.pivot(parent, 0, y, z);
1758
+ pivot.rotation.x = rotationX;
1759
+ box2(pivot, length, 5e-3, width, M.dark, x + length / 2, 0, 0);
1760
+ const slots = Math.floor(length / 95e-4);
1761
+ for (let i = 0; i < slots; i++) box2(pivot, 52e-4, 35e-4, width * 1.12, M.metal, x + 6e-3 + i * 95e-4, 35e-4, 0);
1762
+ return pivot;
1763
+ }
1764
+ function vents(kit, parent, x, length, y, z, count, height) {
1765
+ for (let i = 0; i < count; i++) kit.box(parent, length / count * 0.45, height, 3e-3, kit.M.hole, x + (i + 0.5) * length / count, y, z);
1766
+ }
1767
+ function scope(kit, parent, kind, cx, cy, cz = 0) {
1768
+ const { M, box: box2, cylinderX, cylinderY, cylinderZ, THREE } = kit;
1769
+ if (kind === 1) {
1770
+ box2(parent, 0.044, 0.01, 0.028, M.dark, cx, cy + 5e-3, cz);
1771
+ cylinderX(parent, 0.0155, 0.034, M.dark, cx + 4e-3, cy + 0.026, cz, 18);
1772
+ cylinderX(parent, 0.0125, 2e-3, M.lens, cx + 0.022, cy + 0.026, cz, 18);
1773
+ cylinderY(parent, 4e-3, 6e-3, M.dark, cx + 4e-3, cy + 0.044, cz, 10);
1774
+ return;
1775
+ }
1776
+ if (kind === 2) {
1777
+ box2(parent, 0.07, 0.012, 0.026, M.dark, cx, cy + 6e-3, cz);
1778
+ cylinderX(parent, 0.016, 0.1, M.dark, cx - 0.01, cy + 0.032, cz, 18);
1779
+ cylinderX(parent, 0.016, 0.035, M.dark, cx + 0.055, cy + 0.032, cz, 18, 0.02);
1780
+ cylinderX(parent, 0.017, 2e-3, M.lens, cx + 0.073, cy + 0.032, cz, 18);
1781
+ cylinderY(parent, 7e-3, 0.012, M.dark, cx, cy + 0.052, cz, 12);
1782
+ box2(parent, 0.03, 0.01, 0.012, M.dark, cx - 0.01, cy + 0.054, cz);
1783
+ return;
1784
+ }
1785
+ const sleeve = kind === 4 ? 0.2 : 0.32;
1786
+ cylinderX(parent, 0.0135, sleeve * 0.55, M.dark, cx, cy + 0.045, cz, 18);
1787
+ cylinderX(parent, 0.0135, sleeve * 0.25, M.dark, cx + sleeve * 0.36, cy + 0.045, cz, 18, 0.026);
1788
+ cylinderX(parent, 0.026, 0.05, M.dark, cx + sleeve * 0.5, cy + 0.045, cz, 18);
1789
+ cylinderX(parent, 0.024, 2e-3, M.lens, cx + sleeve * 0.5 + 0.026, cy + 0.045, cz, 18);
1790
+ cylinderX(parent, 0.019, 0.06, M.dark, cx - sleeve * 0.38, cy + 0.045, cz, 18, 0.0135);
1791
+ cylinderX(parent, 0.019, 0.035, M.rubber, cx - sleeve * 0.38 - 0.045, cy + 0.045, cz, 18);
1792
+ cylinderX(parent, 0.017, 2e-3, M.lens, cx - sleeve * 0.38 - 0.063, cy + 0.045, cz, 18);
1793
+ cylinderY(parent, 0.01, 0.022, M.dark, cx, cy + 0.066, cz, 14);
1794
+ cylinderY(parent, 0.011, 4e-3, M.metal, cx, cy + 0.078, cz, 14);
1795
+ cylinderZ(parent, 0.01, 0.022, M.dark, cx, cy + 0.045, cz + 0.022, 14);
1796
+ for (const offset of [-0.07, 0.07]) {
1797
+ const ring = kit.torus(parent, new THREE.TorusGeometry(0.0155, 3e-3, 8, 20), M.dark, cx + offset, cy + 0.045, cz);
1798
+ ring.rotation.y = PI2 / 2;
1799
+ box2(parent, 0.016, 0.03, 0.022, M.dark, cx + offset, cy + 0.018, cz);
1800
+ }
1801
+ }
1802
+ var buttpad = (kit, parent, x, y, height, width) => void kit.box(parent, 0.012, height, width, kit.M.rubber, x, y, 0);
1803
+ var swivel = (kit, parent, x, y) => void kit.torus(parent, new kit.THREE.TorusGeometry(6e-3, 15e-4, 6, 12), kit.M.metal, x, y, 0);
1804
+
1805
+ // src/models/bullpup.ts
1806
+ function buildBullpup(kit, options) {
1807
+ const { M, box: box2, cylinderX } = kit;
1808
+ const group = kit.group();
1809
+ const L = (options.L ?? 0) / 1e3;
1810
+ const barrel = (options.B ?? 0) / 1e3;
1811
+ const bodyLength = Math.max(0.32, L - barrel * 0.72);
1812
+ const body = options.shot ? M.poly : M.olive;
1813
+ const parts = { family: options.shot ? "pump" : "rifle", stroke: 0.06, glow: [] };
1814
+ box2(group, bodyLength, 0.075, 0.042, body, bodyLength / 2, 0, 0);
1815
+ box2(group, bodyLength * 0.9, 0.02, 0.044, M.dark, bodyLength * 0.5, -0.02, 0);
1816
+ box2(group, 0.032, 0.13, 0.044, M.rubber, 0.016, -0.025, 0);
1817
+ const gx = bodyLength * 0.8;
1818
+ const grip = kit.pivot(group, gx, -0.085, 0);
1819
+ grip.rotation.z = -0.25;
1820
+ box2(grip, 0.03, 0.1, 0.03, M.poly, 0, 0, 0);
1821
+ box2(group, 0.1, 6e-3, 0.03, body, gx + 0.01, -0.06, 0);
1822
+ parts.trigger = box2(group, 4e-3, 0.016, 4e-3, M.dark, gx + 0.02, -0.05, 0, 0.15);
1823
+ heatify(parts.glow, cylinderX(group, options.shot ? 0.0105 : 85e-4, barrel, M.metal, L - barrel / 2, 0.012, 0, 16));
1824
+ cylinderX(group, 6e-3, 4e-3, M.hole, L + 1e-3, 0.012, 0, 12);
1825
+ box2(group, 0.05, 0.024, 4e-3, M.hole, bodyLength * 0.25, 5e-3, 0.0212);
1826
+ const bolt = kit.pivot(group);
1827
+ parts.bolt = bolt;
1828
+ box2(bolt, 0.044, 0.02, 6e-3, M.steelL, bodyLength * 0.25, 5e-3, 0.02);
1829
+ if (options.shot) {
1830
+ cylinderX(group, 0.012, bodyLength * 0.75, M.metal, bodyLength * 0.55, -0.025, 0.014, 14);
1831
+ cylinderX(group, 0.012, bodyLength * 0.75, M.metal, bodyLength * 0.55, -0.025, -0.014, 14);
1832
+ const pump = kit.pivot(group);
1833
+ parts.pump = pump;
1834
+ box2(pump, 0.15, 0.045, 0.052, M.poly, bodyLength + 0.06, -0.015, 0);
1835
+ for (let i = 0; i < 6; i++) box2(pump, 6e-3, 0.047, 0.054, M.dark, bodyLength + 5e-3 + i * 0.022, -0.015, 0);
1836
+ rail(kit, group, bodyLength * 0.15, bodyLength * 0.8, 0.04, 0, 0.02);
1837
+ parts.loadPort = kit.mark(group, bodyLength * 0.2, -0.04, 0);
1838
+ } else {
1839
+ const magazine = kit.pivot(group, gx - 0.11, -0.03, 0);
1840
+ magazine.rotation.z = 0.14;
1841
+ parts.mag = magazine;
1842
+ box2(magazine, 0.05, 0.14, 0.026, M.glass, 0, -0.07, 0);
1843
+ for (let i = 0; i < 6; i++) cylinderX(magazine, 3e-3, 0.04, M.brass, 0, -0.01 - i * 0.02, 0, 6);
1844
+ parts.magDrop = 0.18;
1845
+ scope(kit, group, 2, bodyLength * 0.5, 0.04, 0);
1846
+ box2(group, 0.2, 0.03, 0.02, body, bodyLength * 0.62, 0.048, 0);
1847
+ box2(group, 0.026, 0.075, 0.026, body, bodyLength + 0.05, -0.05, 0, 0.1);
1848
+ }
1849
+ parts.muzzle = kit.mark(group, L + 0.01, 0.012, 0);
1850
+ parts.eject = kit.mark(group, bodyLength * 0.25, 5e-3, 0.03);
1851
+ return { group, parts };
1852
+ }
1853
+
1854
+ // src/models/launcher-flame.ts
1855
+ var hose = (kit, group, points, radius, material) => {
1856
+ const { THREE } = kit;
1857
+ const curve = new THREE.CatmullRomCurve3(points.map((point) => new THREE.Vector3(...point)));
1858
+ const tube = new THREE.Mesh(new THREE.TubeGeometry(curve, 40, radius, 8, false), material);
1859
+ tube.castShadow = tube.receiveShadow = true;
1860
+ group.add(tube);
1861
+ };
1862
+ var pilot = (kit, group, x, y) => {
1863
+ const { M, cylinderX } = kit;
1864
+ cylinderX(group, 6e-3, 0.03, M.fire2, x + 0.015, y, 0, 6, 0).userData.flame = 1;
1865
+ cylinderX(group, 4e-3, 0.022, M.fire, x + 0.012, y, 0, 6, 0).userData.flame = 1;
1866
+ };
1867
+ function buildFlamethrower(kit, group, parts) {
1868
+ const { M, box: box2, cylinderX, cylinderY, sphere } = kit;
1869
+ parts.kick = 0.15;
1870
+ heatify(parts.glow, cylinderX(group, 0.016, 0.62, M.steel, 0.42, 0, 0, 14));
1871
+ cylinderX(group, 0.022, 0.1, M.dark, 0.78, 0, 0, 14);
1872
+ heatify(parts.glow, cylinderX(group, 0.013, 0.1, M.steel, 0.88, 0, 0, 12, 9e-3));
1873
+ cylinderX(group, 0.018, 0.02, M.metal, 0.73, 0, 0, 14);
1874
+ cylinderX(group, 0.012, 0.06, M.metal, 0.84, -0.025, 0, 10);
1875
+ pilot(kit, group, 0.92, -0.012);
1876
+ const foreGrip = kit.pivot(group, 0.6, -0.02, 0);
1877
+ foreGrip.rotation.z = -0.1;
1878
+ box2(foreGrip, 0.03, 0.1, 0.03, M.wood, 0, -0.05, 0);
1879
+ const lever = kit.pivot(group, 0.62, -0.02, 0);
1880
+ parts.trigger = box2(lever, 8e-3, 0.05, 6e-3, M.metal, 0.02, -0.03, 0, 0.3);
1881
+ const rearGrip = kit.pivot(group, 0.17, -0.02, 0);
1882
+ rearGrip.rotation.z = -0.28;
1883
+ box2(rearGrip, 0.03, 0.1, 0.03, M.wood, 0, -0.05, 0);
1884
+ box2(group, 0.05, 4e-3, 0.012, M.metal, 0.2, -0.035, 0);
1885
+ cylinderX(group, 0.02, 0.04, M.metal, 0.1, 0, 0, 12);
1886
+ hose(kit, group, [[0.08, 0, 0], [0, -0.04, 0], [-0.08, -0.2, 0.05], [-0.18, -0.32, 0.08], [-0.25, -0.33, 0.08]], 0.011, M.rubber);
1887
+ for (const z of [-0.1, 0.1]) {
1888
+ cylinderY(group, 0.085, 0.5, M.olive, -0.3, -0.05, z, 20);
1889
+ sphere(group, 0.085, M.olive, -0.3, 0.2, z, 2).scale.y = 0.45;
1890
+ sphere(group, 0.085, M.olive, -0.3, -0.3, z, 2).scale.y = 0.45;
1891
+ cylinderY(group, 0.03, 0.03, M.metal, -0.3, 0.24, z, 12);
1892
+ }
1893
+ sphere(group, 0.075, M.olive, -0.3, 0.1, 0, 2).position.x = -0.39;
1894
+ box2(group, 0.03, 0.6, 0.24, M.dark, -0.4, -0.05, 0);
1895
+ cylinderY(group, 0.03, 0.012, M.metal, -0.39, 0.18, 0, 14);
1896
+ const gauge = kit.pivot(group, -0.39, 0.18, 3e-3);
1897
+ parts.gauge = gauge;
1898
+ box2(gauge, 2e-3, 0.022, 2e-3, M.red, 0, 8e-3, 0.012);
1899
+ parts.muzzle = kit.pivot(group, 0.94, 0, 0);
1900
+ parts.rear = kit.pivot(group, 0.1, 0, 0);
1901
+ }
1902
+ function buildImprovisedFlamethrower(kit, group, parts) {
1903
+ const { M, box: box2, cylinderX, cylinderY, sphere } = kit;
1904
+ parts.kick = 0.12;
1905
+ heatify(parts.glow, cylinderX(group, 0.011, 0.66, M.pipe, 0.38, 0, 0, 12));
1906
+ heatify(parts.glow, cylinderX(group, 0.015, 0.06, M.steel, 0.74, 0, 0, 12, 0.01));
1907
+ cylinderX(group, 6e-3, 0.05, M.metal, 0.735, -0.02, 0, 8);
1908
+ pilot(kit, group, 0.77, -0.02);
1909
+ cylinderY(group, 0.012, 0.04, M.redPaint, 0.72, -0.055, 0, 10);
1910
+ box2(group, 0.02, 0.03, 0.012, M.dark, 0.72, -0.085, 0);
1911
+ cylinderX(group, 0.06, 0.42, M.redPaint, 0.3, -0.09, 0, 22);
1912
+ sphere(group, 0.06, M.redPaint, 0.51, -0.09, 0, 2).scale.x = 0.5;
1913
+ sphere(group, 0.06, M.redPaint, 0.09, -0.09, 0, 2).scale.x = 0.5;
1914
+ cylinderX(group, 0.02, 0.04, M.dark, 0.04, -0.09, 0, 12);
1915
+ box2(group, 0.02, 0.06, 5e-3, M.white, 0.3, -0.09, 0.06);
1916
+ for (const x of [0.15, 0.3, 0.45]) cylinderX(group, 0.063, 0.03, M.tape, x, -0.09, 0, 22);
1917
+ cylinderX(group, 0.016, 0.03, M.tape, 0.3, 0, 0, 12);
1918
+ hose(kit, group, [[0.04, -0.09, 0], [-0.02, -0.06, 0.02], [0, 0, 0.02], [0.05, 0, 0]], 7e-3, M.rubber);
1919
+ const rearGrip = kit.pivot(group, 0.12, -0.01, 0);
1920
+ rearGrip.rotation.z = -0.3;
1921
+ box2(rearGrip, 0.03, 0.1, 0.03, M.wood, 0, -0.05, 0);
1922
+ const lever = kit.pivot(group, 0.17, -0.02, 0);
1923
+ parts.trigger = box2(lever, 8e-3, 0.04, 6e-3, M.metal, 0, -0.02, 0, 0.2);
1924
+ parts.muzzle = kit.pivot(group, 0.78, 0, 0);
1925
+ parts.rear = kit.pivot(group, 0.05, 0, 0);
1926
+ }
1927
+
1928
+ // src/models/launcher-rounds.ts
1929
+ var PI3 = Math.PI;
1930
+ function buildRocketGrenade(kit) {
1931
+ const { M, cylinderX } = kit;
1932
+ const round2 = kit.group();
1933
+ cylinderX(round2, 0.018, 0.15, M.olive, -0.05, 0, 0, 14);
1934
+ cylinderX(round2, 0.02, 0.07, M.olive, 0.035, 0, 0, 18, 0.0425);
1935
+ cylinderX(round2, 0.0425, 0.12, M.olive, 0.13, 0, 0, 20);
1936
+ cylinderX(round2, 0.0428, 0.012, M.dark, 0.09, 0, 0, 20);
1937
+ cylinderX(round2, 0.0425, 0.13, M.olive, 0.255, 0, 0, 20, 0.011);
1938
+ cylinderX(round2, 0.011, 0.03, M.metal, 0.335, 0, 0, 10, 4e-3);
1939
+ return round2;
1940
+ }
1941
+ function buildRecoillessRound(kit) {
1942
+ const { M, cylinderX } = kit;
1943
+ const round2 = kit.group();
1944
+ cylinderX(round2, 0.041, 0.32, M.green, -0.16, 0, 0, 20);
1945
+ cylinderX(round2, 0.0415, 0.02, M.yellow, -0.05, 0, 0, 20);
1946
+ cylinderX(round2, 0.041, 0.12, M.green, 0.06, 0, 0, 20, 0.012);
1947
+ cylinderX(round2, 0.043, 0.03, M.brass, -0.33, 0, 0, 20);
1948
+ return round2;
1949
+ }
1950
+ function buildFlyingRocket(kit, launcherType) {
1951
+ const { M, box: box2, cylinderX } = kit;
1952
+ const rocket = kit.group();
1953
+ const diameter = launcherType === "at4" || launcherType === "carl" ? 0.041 : 0.032;
1954
+ cylinderX(rocket, diameter * 0.5, 0.2, M.dark, -0.1, 0, 0, 10);
1955
+ cylinderX(rocket, diameter, 0.3, M.olive, 0.15, 0, 0, 18);
1956
+ cylinderX(rocket, diameter, 0.12, M.olive, 0.36, 0, 0, 18, diameter * 0.25);
1957
+ for (let i = 0; i < 4; i++) {
1958
+ const fin = box2(rocket, 0.06, 2e-3, diameter * 1.4, M.metal, -0.17, 0, 0);
1959
+ fin.rotation.x = i * PI3 / 4;
1960
+ }
1961
+ return rocket;
1962
+ }
1963
+
1964
+ // src/models/launcher-rocket.ts
1965
+ function buildRpg(kit, group, parts) {
1966
+ const { M, box: box2, cylinderX } = kit;
1967
+ parts.kick = 1.6;
1968
+ cylinderX(group, 0.036, 0.13, M.dark, 0.065, 0, 0, 20, 0.021);
1969
+ cylinderX(group, 0.038, 0.012, M.metal, 0, 0, 0, 20);
1970
+ heatify(parts.glow, cylinderX(group, 0.021, 0.83, M.steel, 0.545, 0, 0, 18));
1971
+ cylinderX(group, 0.03, 0.3, M.wood, 0.47, 0, 0, 16);
1972
+ cylinderX(group, 0.031, 0.012, M.metal, 0.32, 0, 0, 16);
1973
+ cylinderX(group, 0.031, 0.012, M.metal, 0.62, 0, 0, 16);
1974
+ cylinderX(group, 0.024, 0.02, M.metal, 0.94, 0, 0, 16);
1975
+ box2(group, 0.09, 0.03, 0.032, M.metal, 0.46, -0.028, 0);
1976
+ const frontGrip = kit.pivot(group, 0.4, -0.04, 0);
1977
+ frontGrip.rotation.z = -0.22;
1978
+ box2(frontGrip, 0.03, 0.1, 0.03, M.wood, 0, -0.05, 0);
1979
+ const rearGrip = kit.pivot(group, 0.5, -0.04, 0);
1980
+ rearGrip.rotation.z = -0.18;
1981
+ box2(rearGrip, 0.03, 0.1, 0.03, M.wood, 0, -0.05, 0);
1982
+ box2(group, 0.06, 4e-3, 0.012, M.metal, 0.53, -0.05, 0);
1983
+ box2(group, 4e-3, 0.025, 0.012, M.metal, 0.56, -0.04, 0);
1984
+ parts.trigger = box2(group, 4e-3, 0.018, 4e-3, M.dark, 0.535, -0.045, 0, 0.15);
1985
+ const hammer = kit.pivot(group, 0.44, -0.012, 0);
1986
+ parts.hammer = hammer;
1987
+ box2(hammer, 0.012, 0.02, 0.01, M.dark, 0, 0, 0);
1988
+ box2(group, 6e-3, 0.03, 6e-3, M.dark, 0.78, 0.032, 0);
1989
+ box2(group, 0.02, 0.022, 0.014, M.dark, 0.46, 0.032, 0);
1990
+ box2(group, 0.12, 0.042, 0.032, M.dark, 0.5, 0.04, -0.04);
1991
+ cylinderX(group, 0.013, 0.05, M.rubber, 0.42, 0.048, -0.04, 14);
1992
+ cylinderX(group, 0.016, 4e-3, M.lens, 0.565, 0.048, -0.04, 14);
1993
+ box2(group, 0.04, 0.012, 0.012, M.metal, 0.5, 0.016, -0.03);
1994
+ const round2 = buildRocketGrenade(kit);
1995
+ round2.position.x = 0.95;
1996
+ group.add(round2);
1997
+ parts.round = round2;
1998
+ parts.roundRest = 0.95;
1999
+ parts.makeRound = () => buildRocketGrenade(kit);
2000
+ parts.muzzle = kit.pivot(group, 0.95, 0, 0);
2001
+ parts.rear = kit.pivot(group, 0, 0, 0);
2002
+ }
2003
+ function buildM72(kit, group, parts) {
2004
+ const { M, box: box2, cylinderX, cylinderY } = kit;
2005
+ parts.kick = 1.1;
2006
+ parts.disposable = true;
2007
+ heatify(parts.glow, cylinderX(group, 0.0365, 0.6, M.olive, 0.58, 0, 0, 20));
2008
+ cylinderX(group, 0.032, 0.32, M.olive, 0.16, 0, 0, 20);
2009
+ cylinderX(group, 0.039, 0.03, M.rubber, 0.866, 0, 0, 20);
2010
+ cylinderX(group, 0.035, 0.025, M.rubber, 0.012, 0, 0, 20);
2011
+ cylinderX(group, 0.037, 0.015, M.yellow, 0.8, 0, 0, 20);
2012
+ cylinderX(group, 0.037, 0.012, M.dark, 0.3, 0, 0, 20);
2013
+ box2(group, 4e-3, 0.05, 0.05, M.dark, 0.79, 0.055, 0);
2014
+ box2(group, 4e-3, 6e-3, 0.04, M.dark, 0.79, 0.078, 0);
2015
+ box2(group, 4e-3, 0.035, 0.024, M.dark, 0.38, 0.05, 0);
2016
+ cylinderY(group, 4e-3, 6e-3, M.lens, 0.38, 0.064, 0, 8);
2017
+ parts.trigger = box2(group, 0.05, 0.016, 0.024, M.rubber, 0.5, 0.042, 0);
2018
+ box2(group, 0.1, 0.01, 0.03, M.dark, 0.47, 0.036, 0);
2019
+ box2(group, 0.02, 0.03, 0.01, M.dark, 0.66, -0.035, 0.03);
2020
+ box2(group, 8e-3, 0.03, 8e-3, M.metal, 0.25, -0.04, 0.03);
2021
+ parts.muzzle = kit.pivot(group, 0.881, 0, 0);
2022
+ parts.rear = kit.pivot(group, 0, 0, 0);
2023
+ }
2024
+ function buildAt4(kit, group, parts) {
2025
+ const { M, box: box2, cylinderX } = kit;
2026
+ parts.kick = 1.3;
2027
+ parts.disposable = true;
2028
+ heatify(parts.glow, cylinderX(group, 0.05, 0.86, M.olive, 0.51, 0, 0, 24));
2029
+ cylinderX(group, 0.056, 0.08, M.dark, 0.98, 0, 0, 24);
2030
+ cylinderX(group, 0.058, 0.07, M.dark, 0.04, 0, 0, 24, 0.05);
2031
+ cylinderX(group, 0.051, 0.015, M.yellow, 0.86, 0, 0, 24);
2032
+ box2(group, 0.16, 0.026, 0.05, M.dark, 0.42, 0.058, 0);
2033
+ box2(group, 0.03, 0.012, 0.016, M.red, 0.47, 0.075, 0);
2034
+ parts.trigger = box2(group, 0.022, 0.012, 0.018, M.rubber, 0.5, 0.074, 0);
2035
+ box2(group, 0.012, 0.07, 0.02, M.dark, 0.35, 0.09, -0.035);
2036
+ box2(group, 0.012, 0.05, 0.02, M.dark, 0.71, 0.08, -0.035);
2037
+ const grip = kit.pivot(group, 0.68, -0.05, 0);
2038
+ grip.rotation.z = -0.15;
2039
+ box2(grip, 0.035, 0.11, 0.032, M.dark, 0, -0.05, 0);
2040
+ box2(group, 0.14, 0.035, 0.06, M.rubber, 0.32, -0.06, 0);
2041
+ box2(group, 0.3, 0.012, 0.055, M.dark, 0.5, -0.05, 0);
2042
+ parts.muzzle = kit.pivot(group, 1.02, 0, 0);
2043
+ parts.rear = kit.pivot(group, 0, 0, 0);
2044
+ }
2045
+ function buildCarlGustaf(kit, group, parts) {
2046
+ const { M, box: box2, cylinderX } = kit;
2047
+ parts.kick = 1.4;
2048
+ heatify(parts.glow, cylinderX(group, 0.045, 0.72, M.dark, 0.59, 0, 0, 24));
2049
+ cylinderX(group, 0.05, 0.04, M.rubber, 0.93, 0, 0, 24);
2050
+ const breech = kit.pivot(group, 0.21, 0, 0.05);
2051
+ parts.breech = [breech];
2052
+ cylinderX(breech, 0.06, 0.16, M.dark, -0.1, 0, -0.05, 24, 0.046);
2053
+ cylinderX(breech, 0.062, 0.015, M.metal, -0.18, 0, -0.05, 24);
2054
+ box2(group, 0.025, 0.03, 0.02, M.metal, 0.22, 0, 0.052);
2055
+ const foreGrip = kit.pivot(group, 0.62, -0.05, 0);
2056
+ foreGrip.rotation.z = -0.2;
2057
+ box2(foreGrip, 0.035, 0.11, 0.034, M.dark, 0, -0.05, 0);
2058
+ parts.trigger = box2(group, 4e-3, 0.018, 4e-3, M.dark, 0.66, -0.06, 0, 0.15);
2059
+ box2(group, 0.06, 4e-3, 0.012, M.metal, 0.66, -0.07, 0);
2060
+ const rearGrip = kit.pivot(group, 0.4, -0.05, 0);
2061
+ rearGrip.rotation.z = -0.25;
2062
+ box2(rearGrip, 0.035, 0.1, 0.034, M.dark, 0, -0.05, 0);
2063
+ box2(group, 0.13, 0.04, 0.07, M.rubber, 0.28, -0.065, 0);
2064
+ box2(group, 0.16, 0.05, 0.045, M.dark, 0.56, 0.07, -0.035);
2065
+ cylinderX(group, 0.016, 0.05, M.rubber, 0.46, 0.075, -0.035, 14);
2066
+ cylinderX(group, 0.02, 4e-3, M.lens, 0.645, 0.075, -0.035, 14);
2067
+ box2(group, 0.06, 0.02, 0.02, M.metal, 0.56, 0.04, -0.03);
2068
+ box2(group, 0.22, 0.01, 0.02, M.dark, 0.6, 0.06, 0.03);
2069
+ box2(group, 0.01, 0.03, 0.02, M.dark, 0.5, 0.05, 0.03);
2070
+ box2(group, 0.01, 0.03, 0.02, M.dark, 0.7, 0.05, 0.03);
2071
+ parts.makeRound = () => buildRecoillessRound(kit);
2072
+ parts.muzzle = kit.pivot(group, 0.95, 0, 0);
2073
+ parts.rear = kit.pivot(group, 0.05, 0, 0);
2074
+ }
2075
+ var ROCKET_BUILDERS = {
2076
+ rpg: buildRpg,
2077
+ m72: buildM72,
2078
+ at4: buildAt4,
2079
+ carl: buildCarlGustaf
2080
+ };
2081
+
2082
+ // src/models/launcher.ts
2083
+ function buildLauncher(kit, options) {
2084
+ const group = kit.group();
2085
+ const type = options.lt ?? "";
2086
+ const isFlame = type === "flame" || type === "flameDIY";
2087
+ const parts = { family: isFlame ? "flame" : "rocket", launcherType: type, glow: [] };
2088
+ if (type === "flame") buildFlamethrower(kit, group, parts);
2089
+ else if (type === "flameDIY") buildImprovisedFlamethrower(kit, group, parts);
2090
+ else ROCKET_BUILDERS[type]?.(kit, group, parts);
2091
+ parts.eject = parts.muzzle;
2092
+ return { group, parts };
2093
+ }
2094
+
2095
+ // src/models/m2.ts
2096
+ function buildM2(kit) {
2097
+ const { M, box: box2, cylinderX, cylinderZ } = kit;
2098
+ const group = kit.group();
2099
+ const parts = { family: "belt", kick: 2.2, stroke: 0.08, glow: [] };
2100
+ box2(group, 0.52, 0.13, 0.12, M.metal, 0.36, 0, 0);
2101
+ for (let i = 0; i < 5; i++) cylinderZ(group, 6e-3, 0.124, M.dark, 0.15 + i * 0.1, -0.03, 0, 10);
2102
+ heatify(parts.glow, cylinderX(group, 0.016, 1.14, M.metal, 1.14, 0.01, 0, 16));
2103
+ cylinderX(group, 0.034, 0.24, M.dark, 0.74, 0.01, 0, 16);
2104
+ for (let i = 0; i < 6; i++) cylinderX(group, 0.036, 0.012, M.metal, 0.66 + i * 0.035, 0.01, 0, 16);
2105
+ cylinderX(group, 0.022, 0.05, M.dark, 1.69, 0.01, 0, 16);
2106
+ box2(group, 0.04, 0.09, 0.03, M.wood, 0.05, -0.02, 0.05);
2107
+ box2(group, 0.04, 0.09, 0.03, M.wood, 0.05, -0.02, -0.05);
2108
+ box2(group, 0.06, 0.02, 0.12, M.metal, 0.09, -0.02, 0);
2109
+ const belt = kit.pivot(group, 0.38, -0.05, -0.12);
2110
+ parts.belt = belt;
2111
+ box2(belt, 0.16, 0.12, 0.09, M.olive, 0, 0, 0);
2112
+ for (let i = 0; i < 8; i++) cylinderX(belt, 6e-3, 0.05, M.brass, -0.08 + i * 0.02, 0.08, 0.04, 8);
2113
+ const cover = kit.pivot(group, 0.12, 0.065, 0);
2114
+ parts.cover = cover;
2115
+ box2(cover, 0.4, 0.012, 0.12, M.metal, 0.2, 6e-3, 0);
2116
+ box2(group, 0.04, 0.05, 0.04, M.dark, 0.66, 0.1, 0);
2117
+ const bolt = kit.pivot(group);
2118
+ parts.bolt = bolt;
2119
+ box2(bolt, 0.03, 0.03, 0.02, M.metal, 0.5, 0, 0.07);
2120
+ const tripod = kit.group();
2121
+ tripod.position.set(0.42, -0.1, 0);
2122
+ group.add(tripod);
2123
+ box2(tripod, 0.03, 0.06, 0.03, M.metal, 0, 0, 0);
2124
+ box2(tripod, 0.012, 0.62, 0.012, M.metal, -0.2, -0.25, 0, 0.75);
2125
+ box2(tripod, 0.012, 0.62, 0.012, M.metal, 0.22, -0.25, 0.16, -0.75, 0, 0.3);
2126
+ box2(tripod, 0.012, 0.62, 0.012, M.metal, 0.22, -0.25, -0.16, -0.75, 0, -0.3);
2127
+ parts.muzzle = kit.mark(group, 1.72, 0.01, 0);
2128
+ parts.eject = kit.mark(group, 0.3, -0.07, 0);
2129
+ return { group, parts };
2130
+ }
2131
+
2132
+ // src/models/p90.ts
2133
+ function buildP90(kit) {
2134
+ const { M, box: box2, cylinderX } = kit;
2135
+ const group = kit.group();
2136
+ const parts = { family: "p90", stroke: 0.04, kick: 0.6, glow: [] };
2137
+ box2(group, 0.4, 0.07, 0.055, M.poly, 0.2, 0, 0);
2138
+ box2(group, 0.12, 0.075, 0.05, M.poly, 0.2, -0.07, 0);
2139
+ box2(group, 0.08, 0.04, 0.03, M.hole, 0.21, -0.06, 0);
2140
+ box2(group, 0.06, 0.13, 0.056, M.poly, 0.03, -0.03, 0);
2141
+ box2(group, 0.05, 0.03, 0.056, M.poly, 0.33, -0.04, 0);
2142
+ heatify(parts.glow, cylinderX(group, 8e-3, 0.09, M.metal, 0.43, 0, 0, 16));
2143
+ const magazine = kit.pivot(group, 0.22, 0.047, 0);
2144
+ parts.mag = magazine;
2145
+ parts.magDir = 1;
2146
+ parts.magDrop = 0.08;
2147
+ box2(magazine, 0.3, 0.022, 0.05, M.glass, 0, 0, 0);
2148
+ for (let i = 0; i < 12; i++) cylinderX(magazine, 28e-4, 0.02, M.brass, -0.12 + i * 0.022, 0, 0, 6);
2149
+ scope(kit, group, 1, 0.24, 0.06, 0);
2150
+ const bolt = kit.pivot(group);
2151
+ parts.bolt = bolt;
2152
+ box2(bolt, 0.02, 0.012, 0.012, M.dark, 0.3, 0.02, 0.03);
2153
+ parts.trigger = box2(group, 4e-3, 0.016, 4e-3, M.dark, 0.24, -0.045, 0);
2154
+ parts.muzzle = kit.mark(group, 0.48, 0, 0);
2155
+ parts.eject = kit.mark(group, 0.2, -0.07, 0);
2156
+ return { group, parts };
2157
+ }
2158
+
2159
+ // src/models/part-registry.ts
2160
+ var SINGLE_PARTS = ["slide", "bolt", "boltHandle", "mag", "trigger", "hammer", "lever", "pump", "belt", "cover", "crane", "cylinder", "barrels", "safety", "muzzle", "eject", "loadPort", "topLoad", "rear", "round", "gauge"];
2161
+ function partRegistry(parts) {
2162
+ const registry = /* @__PURE__ */ new Map();
2163
+ for (const key of SINGLE_PARTS) {
2164
+ const part = parts[key];
2165
+ if (part) registry.set(key, part);
2166
+ }
2167
+ parts.breech?.forEach((part, index) => registry.set(`breech_${index}`, part));
2168
+ parts.chambers?.forEach((part, index) => registry.set(`chamber_${index}`, part));
2169
+ return registry;
2170
+ }
2171
+
2172
+ // src/models/pistol-slide.ts
2173
+ var PI4 = Math.PI;
2174
+ function buildSlide(kit, slide2, parts, options, style, size, metal2) {
2175
+ const { M, box: box2, cylinderX } = kit;
2176
+ const { length: L, width: w } = size;
2177
+ const slideHeight = style.inset ? size.height * 0.92 : size.height;
2178
+ const slideWidth = style.inset ? w * 0.86 : w;
2179
+ kit.roundness = style.rf;
2180
+ if (style.tri) box2(slide2, L * 0.42, slideHeight, slideWidth, metal2, L * 0.21, 0, 0);
2181
+ else if (style.open) {
2182
+ box2(slide2, L * 0.45, slideHeight, slideWidth, metal2, L * 0.225, 0, 0);
2183
+ box2(slide2, L * 0.55, slideHeight * 0.62, slideWidth, metal2, L * 0.72, -slideHeight * 0.19, 0);
2184
+ } else box2(slide2, L, slideHeight, slideWidth, metal2, L / 2, 0, 0);
2185
+ if (style.slope) box2(slide2, L * 0.25, slideHeight * 0.5, slideWidth * 0.98, metal2, L * 0.86, slideHeight * 0.35, 0, -0.22);
2186
+ kit.roundness = null;
2187
+ for (let i = 0; i < (style.flush ? 9 : 7); i++) box2(slide2, 22e-4, slideHeight * 0.72, slideWidth + 14e-4, M.dark, 7e-3 + i * 52e-4, -2e-3, 0, 0);
2188
+ if (style.frontSer) for (let i = 0; i < 5; i++) box2(slide2, 22e-4, slideHeight * 0.6, slideWidth + 14e-4, M.dark, L * 0.76 + i * 52e-4, -3e-3, 0);
2189
+ if (style.port) {
2190
+ box2(slide2, L * style.port, 4e-3, slideWidth * 0.55, M.hole, L * 0.6, slideHeight / 2 - 1e-3, slideWidth * 0.12);
2191
+ box2(slide2, L * style.port * 0.85, 3e-3, slideWidth * 0.4, M.steelL, L * 0.6, slideHeight / 2 - 4e-4, slideWidth * 0.1);
2192
+ box2(slide2, 0.012, 4e-3, 2e-3, M.dark, L * 0.55, slideHeight * 0.15, slideWidth / 2 + 8e-4);
2193
+ }
2194
+ if (style.open) {
2195
+ heatify(parts.glow, cylinderX(slide2, 62e-4, L * 0.56, M.steelL, L * 0.7, slideHeight * 0.12, 0, 18));
2196
+ box2(slide2, 0.03, 4e-3, slideWidth * 0.6, M.hole, L * 0.48, slideHeight / 2 - 1e-3, 0);
2197
+ }
2198
+ if (style.tri) {
2199
+ const tube = new kit.THREE.Mesh(new kit.THREE.CylinderGeometry(0.016, 0.016, L * 0.6, 3), metal2);
2200
+ tube.castShadow = tube.receiveShadow = true;
2201
+ tube.rotation.set(0, 0, -PI4 / 2);
2202
+ tube.rotateY(PI4 / 6);
2203
+ tube.position.set(L * 0.7, slideHeight * 0.08, 0);
2204
+ slide2.add(tube);
2205
+ heatify(parts.glow, tube);
2206
+ box2(slide2, L * 0.6, 6e-3, 0.012, metal2, L * 0.7, slideHeight * 0.38, 0);
2207
+ for (let i = 0; i < 6; i++) box2(slide2, 8e-3, 3e-3, 0.013, M.hole, L * 0.45 + i * 0.04, slideHeight * 0.41, 0);
2208
+ }
2209
+ if (style.bushing) {
2210
+ heatify(parts.glow, cylinderX(slide2, 78e-4, 6e-3, M.steelL, L + 2e-3, 1e-3, 0, 18));
2211
+ cylinderX(slide2, 45e-4, 4e-3, M.steel, L + 2e-3, -0.012, 0, 12);
2212
+ }
2213
+ const muzzleY = style.tri ? slideHeight * 0.08 : 2e-3;
2214
+ heatify(parts.glow, cylinderX(slide2, style.tri ? 72e-4 : 62e-4, 4e-3, M.dark, L + 15e-4, muzzleY, 0, 18));
2215
+ cylinderX(slide2, 34e-4, 5e-3, M.hole, L + 25e-4, muzzleY, 0, 12);
2216
+ const top = slideHeight / 2;
2217
+ if (style.sight === "u" || style.sight === "3dot") {
2218
+ box2(slide2, 4e-3, 7e-3, 4e-3, M.dark, L - 8e-3, top + 35e-4, 0);
2219
+ box2(slide2, 22e-4, 22e-4, 46e-4, M.white, L - 8e-3, top + 58e-4, 0);
2220
+ box2(slide2, 8e-3, 7e-3, 0.016, M.dark, 9e-3, top + 35e-4, 0);
2221
+ if (style.sight === "u") box2(slide2, 4e-3, 2e-3, 0.0122, M.white, 9e-3, top + 6e-3, 0);
2222
+ else box2(slide2, 22e-4, 22e-4, 0.0166, M.white, 9e-3, top + 58e-4, 0);
2223
+ } else {
2224
+ box2(slide2, 6e-3, 4e-3, 3e-3, M.dark, L - 6e-3, top + 2e-3, 0);
2225
+ box2(slide2, 6e-3, 4e-3, 0.012, M.dark, 8e-3, top + 2e-3, 0);
2226
+ }
2227
+ if (style.decock === "slide") {
2228
+ box2(slide2, 0.012, 8e-3, 4e-3, M.dark, 0.02, slideHeight * 0.15, slideWidth / 2 + 2e-3, 0.3);
2229
+ box2(slide2, 0.012, 8e-3, 4e-3, M.dark, 0.02, slideHeight * 0.15, -slideWidth / 2 - 2e-3, 0.3);
2230
+ }
2231
+ if (style.safety === "slide") parts.safety = box2(slide2, 0.012, 8e-3, 4e-3, M.dark, 0.018, slideHeight * 0.1, -slideWidth / 2 - 2e-3, 0.2);
2232
+ if (options.sel) {
2233
+ const selector = box2(slide2, 0.01, 8e-3, 3e-3, M.dark, 0.014, -slideHeight * 0.2, slideWidth / 2 + 14e-4);
2234
+ parts.safety ?? (parts.safety = selector);
2235
+ }
2236
+ }
2237
+
2238
+ // src/models/pistol-style.ts
2239
+ var STYLES = {
2240
+ glock: { rf: 0.07, ga: -0.36, grip: "finger", guard: "square", rail: 1, sight: "u", port: 0.2, slideM: "metal", frame: "poly" },
2241
+ taurus: { rf: 0.16, ga: -0.3, grip: "panel", guard: "square", rail: 1, sight: "3dot", port: 0.18, frontSer: 1, frame: "poly" },
2242
+ g2c: { rf: 0.2, ga: -0.3, grip: "panel", guard: "round", rail: 0, sight: "3dot", port: 0.17, frame: "poly", safety: "thumb" },
2243
+ beretta: { rf: 0.28, ga: -0.27, grip: "wood", guard: "round", hammer: "spur", sight: "3dot", open: 1, decock: "slide", port: 0, frame: "metal" },
2244
+ pt100: { rf: 0.25, ga: -0.27, grip: "rubber", guard: "round", hammer: "spur", sight: "3dot", port: 0.17, decock: "frame", frame: "metal" },
2245
+ sig: { rf: 0.14, ga: -0.3, grip: "rubber", guard: "hook", hammer: "spur", sight: "3dot", port: 0.19, decock: "frame", rail: 1, tall: 1.12, frame: "metal" },
2246
+ cz: { rf: 0.32, ga: -0.3, grip: "rubber", guard: "round", hammer: "spur", sight: "3dot", port: 0.15, inset: 1, safety: "thumb", frame: "metal" },
2247
+ m1911: { rf: 0.24, ga: -0.2, grip: "wood", guard: "round", hammer: "ring", sight: "small", port: 0.12, bushing: 1, beaver: 1, safety: "thumb", slideStop: 1, frame: "metal" },
2248
+ makarov: { rf: 0.48, ga: -0.24, grip: "star", guard: "round", hammer: "ring", sight: "small", port: 0.14, flush: 1, safety: "slide", lanyard: 1, frame: "metal" },
2249
+ fiveseven: { rf: 0.2, ga: -0.4, grip: "chunky", guard: "square", rail: 1, sight: "3dot", port: 0.18, slope: 1, frame: "poly" },
2250
+ deagle: { rf: 0.08, ga: -0.28, grip: "rubber", guard: "round", hammer: "big", sight: "small", port: 0.16, tri: 1, safety: "slide", frame: "metal" }
2251
+ };
2252
+
2253
+ // src/models/pistol.ts
2254
+ var PI5 = Math.PI;
2255
+ function buildFrame(kit, group, parts, options, style, size, materials) {
2256
+ const { M, box: box2, cylinderZ, sphere } = kit;
2257
+ const { length: L, height: h, width: w } = size;
2258
+ const fm = materials.frame;
2259
+ if (style.inset) {
2260
+ box2(group, L * 0.9, h * 0.35, w * 0.12, fm, L * 0.47, -h * 0.32, w * 0.47);
2261
+ box2(group, L * 0.9, h * 0.35, w * 0.12, fm, L * 0.47, -h * 0.32, -w * 0.47);
2262
+ }
2263
+ const frameLength = style.tri ? L * 0.66 : L * 0.8;
2264
+ kit.roundness = style.rf;
2265
+ box2(group, frameLength, 0.016, w * 0.92, fm, frameLength / 2 + L * 0.06, -h / 2 - 8e-3, 0);
2266
+ kit.roundness = null;
2267
+ if (style.rail) for (let i = 0; i < (style.slope ? 5 : 3); i++) box2(group, 4e-3, 3e-3, w * 0.96, M.dark, L * 0.62 + i * 9e-3, -h / 2 - 0.017, 0);
2268
+ const isGlock = options.st === "glock" || !options.st;
2269
+ if (isGlock) {
2270
+ box2(group, 8e-3, 4e-3, 3e-3, M.dark, L * 0.5, -h / 2 - 3e-3, -w / 2 - 12e-4);
2271
+ box2(group, 8e-3, 4e-3, 3e-3, M.dark, L * 0.5, -h / 2 - 3e-3, w / 2 + 12e-4);
2272
+ }
2273
+ if (style.slideStop) {
2274
+ box2(group, 0.03, 6e-3, 3e-3, M.dark, L * 0.42, -h / 2 - 2e-3, -w / 2 - 14e-4);
2275
+ sphere(group, 28e-4, M.dark, L * 0.55, -h / 2 - 3e-3, w / 2 + 1e-3, 1);
2276
+ } else box2(group, 0.012, 4e-3, 3e-3, M.dark, L * 0.45, -h / 2 - 3e-3, -w / 2 - 12e-4);
2277
+ if (style.decock === "frame") box2(group, 0.01, 0.012, 3e-3, M.dark, L * 0.38, -h / 2 - 0.012, -w / 2 - 14e-4, 0.6);
2278
+ if (style.safety === "thumb") parts.safety = box2(group, 0.014, 6e-3, 4e-3, M.dark, 0.02, -h / 2 - 2e-3, -w / 2 - 16e-4);
2279
+ const gripLength = options.gl ?? 0.105;
2280
+ const gx = 0.034 + (L - 0.2) * 0.15;
2281
+ const gy = -h / 2 - gripLength / 2 + 6e-3;
2282
+ const grip = kit.pivot(group, gx, gy, 0);
2283
+ grip.rotation.z = style.ga;
2284
+ const gripWidth = style.grip === "chunky" ? w * 1.18 : w * 1.06;
2285
+ const gripDepth = style.grip === "chunky" ? 0.036 : 0.032;
2286
+ kit.roundness = 0.3;
2287
+ if (style.grip === "wood" || style.grip === "star") {
2288
+ box2(grip, gripDepth * 0.86, gripLength, w * 0.9, fm, 0, 0, 0);
2289
+ box2(grip, gripDepth * 0.8, gripLength * 0.86, 4e-3, materials.grip, 1e-3, -2e-3, w * 0.47);
2290
+ box2(grip, gripDepth * 0.8, gripLength * 0.86, 4e-3, materials.grip, 1e-3, -2e-3, -w * 0.47);
2291
+ if (style.grip === "wood") {
2292
+ for (let i = 0; i < 6; i++) for (let j = 0; j < 2; j++) box2(grip, 3e-3, 3e-3, 12e-4, M.woodL, -6e-3 + j * 0.012, -gripLength * 0.3 + i * 0.012, w * 0.495, 0, 0, 0);
2293
+ cylinderZ(grip, 35e-4, 15e-4, M.brass, 0, gripLength * 0.25, w * 0.5, 10);
2294
+ cylinderZ(grip, 35e-4, 15e-4, M.brass, 0, -gripLength * 0.3, w * 0.5, 10);
2295
+ } else cylinderZ(grip, 8e-3, 15e-4, M.dark, 0, gripLength * 0.1, w * 0.5, 5);
2296
+ } else box2(grip, gripDepth, gripLength, gripWidth, materials.grip, 0, 0, 0);
2297
+ kit.roundness = null;
2298
+ if (style.grip === "finger") {
2299
+ for (let i = 0; i < 3; i++) box2(grip, 6e-3, 0.012, gripWidth * 0.98, materials.grip, gripDepth / 2 + 2e-3, gripLength * 0.18 - i * 0.022, 0);
2300
+ for (let i = 0; i < 5; i++) for (let j = 0; j < 3; j++) box2(grip, 3e-3, 3e-3, 12e-4, M.dark, -8e-3 + j * 8e-3, -gripLength * 0.25 + i * 0.012, gripWidth / 2 + 4e-4);
2301
+ }
2302
+ if (style.grip === "panel" || style.grip === "rubber" || style.grip === "chunky") for (let i = 0; i < 6; i++) box2(grip, gripDepth * 0.75, 25e-4, gripWidth + 1e-3, M.dark, 2e-3, -gripLength / 2 + 0.018 + i * 0.013, 0);
2303
+ if (style.grip === "chunky") box2(grip, 8e-3, gripLength * 0.8, gripWidth * 0.9, materials.grip, gripDepth / 2 + 2e-3, -4e-3, 0);
2304
+ if (style.beaver) {
2305
+ box2(group, 0.026, 8e-3, w * 0.9, fm, -4e-3, -h / 2 - 4e-3, 0, 0.25);
2306
+ box2(grip, 6e-3, gripLength * 0.55, w * 0.9, fm, -gripDepth * 0.5 - 2e-3, gripLength * 0.18, 0);
2307
+ } else box2(group, 0.02, 9e-3, w * 0.9, fm, 4e-3, -h / 2 - 6e-3, 0);
2308
+ if (style.lanyard) {
2309
+ const ring = kit.torus(grip, new kit.THREE.TorusGeometry(5e-3, 13e-4, 6, 12), M.dark, -0.012, -gripLength / 2 + 4e-3, 0);
2310
+ ring.position.set(-0.012, -gripLength / 2 + 4e-3, 0);
2311
+ }
2312
+ const magazine = kit.pivot(grip);
2313
+ parts.mag = magazine;
2314
+ parts.magDrop = gripLength * 1.15;
2315
+ box2(magazine, 0.022, gripLength * 0.94, w * 0.72, M.dark, 3e-3, -4e-3, 0);
2316
+ if (style.grip === "chunky") box2(magazine, 0.04, 0.01, gripWidth * 1.02, M.poly, 3e-3, -gripLength / 2 - 4e-3, 0);
2317
+ else if (style.flush || style.grip === "wood") box2(magazine, 0.028, 4e-3, w * 0.9, M.steel, 3e-3, -gripLength / 2 - 1e-3, 0);
2318
+ else box2(magazine, 0.031, 6e-3, gripWidth * 1.02, M.dark, 3e-3, -gripLength / 2 - 2e-3, 0);
2319
+ return { gx, ty: -h / 2 };
2320
+ }
2321
+ function buildPistol(kit, options) {
2322
+ const { M, box: box2 } = kit;
2323
+ const style = { ...STYLES[options.st ?? "glock"] };
2324
+ const group = kit.group();
2325
+ const parts = { family: "pistol", kick: options.deagle ? 1.8 : 0.6, glow: [] };
2326
+ const size = { length: (options.L ?? 0) / 1e3 * 0.97, height: (options.h ?? 0.031) * (style.tall ?? 1), width: options.deagle ? 0.032 : style.flush ? 0.024 : 0.025 };
2327
+ const { length: L, height: h, width: w } = size;
2328
+ const slideMetal = options.steel ? M.steel : options.stain ? M.steelL : M.metal;
2329
+ const frameMetal = style.frame === "poly" ? M.poly : slideMetal;
2330
+ const gripMaterial = style.grip === "wood" ? M.wood : style.grip === "star" ? M.bake : style.grip === "rubber" ? M.rubber : M.poly;
2331
+ const slide2 = kit.pivot(group);
2332
+ parts.slide = slide2;
2333
+ parts.stroke = L * 0.15;
2334
+ buildSlide(kit, slide2, parts, options, style, size, slideMetal);
2335
+ const { gx, ty } = buildFrame(kit, group, parts, options, style, size, { frame: frameMetal, grip: gripMaterial });
2336
+ if (style.guard === "square") {
2337
+ box2(group, 0.046, 4e-3, 9e-3, frameMetal, gx + 0.05, ty - 0.034, 0);
2338
+ box2(group, 4e-3, 0.03, 9e-3, frameMetal, gx + 0.074, ty - 0.02, 0);
2339
+ box2(group, 6e-3, 4e-3, 9e-3, frameMetal, gx + 0.075, ty - 0.034, 0);
2340
+ } else if (style.guard === "hook") {
2341
+ box2(group, 0.046, 4e-3, 9e-3, frameMetal, gx + 0.05, ty - 0.034, 0);
2342
+ box2(group, 4e-3, 0.03, 9e-3, frameMetal, gx + 0.073, ty - 0.02, 0, 0.2);
2343
+ box2(group, 8e-3, 6e-3, 9e-3, frameMetal, gx + 0.074, ty - 0.037, 0, 0.4);
2344
+ } else {
2345
+ const guard = kit.torus(group, new kit.THREE.TorusGeometry(0.019, 22e-4, 6, 16, PI5), frameMetal, gx + 0.05, ty - 0.014, 0);
2346
+ guard.rotation.z = PI5;
2347
+ guard.scale.set(1.2, 1, 1);
2348
+ }
2349
+ const isGlock = options.st === "glock" || !options.st;
2350
+ parts.trigger = box2(group, 4e-3, 0.014, 4e-3, isGlock ? M.dark : M.steel, gx + 0.042, ty - 0.022, 0, 0.2);
2351
+ if (isGlock) box2(parts.trigger, 15e-4, 8e-3, 45e-4, M.metal, 0, -1e-3, 0);
2352
+ if (style.hammer) {
2353
+ const hammer = kit.pivot(group, 3e-3, h / 2 - 8e-3, 0);
2354
+ parts.hammer = hammer;
2355
+ if (style.hammer === "ring") {
2356
+ box2(hammer, 6e-3, 0.012, 6e-3, M.dark, -2e-3, 4e-3, 0, 0.3);
2357
+ kit.torus(hammer, new kit.THREE.TorusGeometry(45e-4, 16e-4, 6, 12), M.dark, -4e-3, 0.012, 0);
2358
+ } else if (style.hammer === "big") box2(hammer, 0.014, 0.02, 9e-3, M.dark, -4e-3, 7e-3, 0, 0.35);
2359
+ else {
2360
+ box2(hammer, 8e-3, 0.016, 7e-3, M.dark, -2e-3, 6e-3, 0, 0.3);
2361
+ box2(hammer, 8e-3, 4e-3, 8e-3, M.dark, -6e-3, 0.013, 0, 0.3);
2362
+ }
2363
+ }
2364
+ parts.muzzle = kit.mark(group, L + 6e-3, style.tri ? h * 0.08 : 2e-3, 0);
2365
+ parts.eject = kit.mark(group, L * 0.6, h / 2, w * 0.25);
2366
+ return { group, parts };
2367
+ }
2368
+
2369
+ // src/models/revolver.ts
2370
+ var PI6 = Math.PI;
2371
+ var DEFAULT_CHAMBERS = 6;
2372
+ function buildRevolver(kit, options) {
2373
+ const { M, box: box2, cylinderX } = kit;
2374
+ const group = kit.group();
2375
+ const parts = { family: "revolver", kick: options.cal === ".44 Magnum" ? 1.5 : 0.9, glow: [] };
2376
+ const barrel = (options.B ?? 0) / 1e3;
2377
+ const chambers = options.chambers ?? DEFAULT_CHAMBERS;
2378
+ const metal2 = options.steel ? M.steel : M.metal;
2379
+ box2(group, 0.08, 0.044, 0.024, metal2, 0.07, 0, 0);
2380
+ box2(group, 0.036, 0.012, 0.022, metal2, 0.096, 0.022, 0);
2381
+ box2(group, 0.024, 0.012, 4e-3, M.dark, 0.055, 6e-3, -0.013);
2382
+ const crane = kit.pivot(group, 0.077, -0.024, 0);
2383
+ parts.crane = crane;
2384
+ const cylinder = kit.pivot(crane, 0, 0.022, 0);
2385
+ parts.cylinder = cylinder;
2386
+ cylinderX(cylinder, 0.0215, 0.042, metal2, 0, 0, 0, 24);
2387
+ for (let i = 0; i < chambers; i++) {
2388
+ const angle = i * 2 * PI6 / chambers;
2389
+ box2(cylinder, 0.026, 5e-3, 5e-3, M.dark, 0, Math.cos(angle) * 0.0205, Math.sin(angle) * 0.0205, 0, 0, angle);
2390
+ }
2391
+ parts.chambers = [];
2392
+ for (let i = 0; i < chambers; i++) {
2393
+ const angle = i * 2 * PI6 / chambers + PI6 / chambers;
2394
+ parts.chambers.push(kit.mark(cylinder, -0.022, Math.cos(angle) * 0.013, Math.sin(angle) * 0.013));
2395
+ cylinderX(cylinder, 46e-4, 2e-3, M.brass, -0.0215, Math.cos(angle) * 0.013, Math.sin(angle) * 0.013, 12);
2396
+ }
2397
+ heatify(parts.glow, cylinderX(group, 88e-4, barrel, metal2, 0.112 + barrel / 2, 9e-3, 0, 18));
2398
+ box2(group, barrel * 0.92, 0.015, 0.013, metal2, 0.112 + barrel * 0.46, -5e-3, 0);
2399
+ cylinderX(group, 4e-3, 5e-3, M.hole, 0.112 + barrel + 15e-4, 9e-3, 0, 12);
2400
+ box2(group, 0.012, 9e-3, 4e-3, M.dark, 0.112 + barrel - 8e-3, 0.021, 0, -0.3);
2401
+ box2(group, 0.012, 6e-3, 0.012, M.dark, 0.105, 0.031, 0);
2402
+ const hammer = kit.pivot(group, 0.036, 0.018, 0);
2403
+ box2(hammer, 0.01, 0.02, 8e-3, M.dark, -4e-3, 8e-3, 0, 0.45);
2404
+ parts.hammer = hammer;
2405
+ const grip = kit.pivot(group, 0.024, -0.062, 0);
2406
+ grip.rotation.z = -0.38;
2407
+ box2(grip, 0.034, 0.1, 0.031, options.wood ? M.wood : M.rubber, 0, 0, 0);
2408
+ if (!options.wood) for (let i = 0; i < 4; i++) box2(grip, 6e-3, 0.07, 0.033, M.dark, -0.012 + i * 8e-3, 0, 0);
2409
+ box2(group, 0.038, 4e-3, 8e-3, metal2, 0.062, -0.036, 0);
2410
+ box2(group, 4e-3, 0.022, 8e-3, metal2, 0.08, -0.026, 0, 0.2);
2411
+ parts.trigger = box2(group, 4e-3, 0.015, 4e-3, M.dark, 0.06, -0.027, 0, 0.2);
2412
+ parts.muzzle = kit.mark(group, 0.112 + barrel + 6e-3, 9e-3, 0);
2413
+ parts.eject = kit.mark(group, 0.077, 0, 0);
2414
+ return { group, parts };
2415
+ }
2416
+
2417
+ // src/models/rifle-fittings.ts
2418
+ function buildFittings(context, gripX) {
2419
+ const { kit, group, parts, options: o, furniture: furn, length: L, receiverStart: x0, receiverEnd: x1, receiverHeight: hR, receiverWidth: wR, barrelRadius: r, barrelY: by } = context;
2420
+ const { M, box: box2, cylinderX, cylinderZ, sphere } = kit;
2421
+ const middle = (x0 + x1) / 2;
2422
+ if (o.scope) scope(kit, group, o.scope, middle + 0.03, hR / 2 + (o.rail ? 6e-3 : 0), o.scope === 4 ? -0.02 : 0);
2423
+ if (o.carry) {
2424
+ box2(group, 0.15, 0.026, 0.014, M.metal, middle, hR / 2 + 0.03, 0);
2425
+ box2(group, 0.012, 0.03, 0.014, M.metal, middle + 0.07, hR / 2 + 0.014, 0);
2426
+ box2(group, 0.012, 0.03, 0.014, M.metal, middle - 0.07, hR / 2 + 0.014, 0);
2427
+ }
2428
+ if (o.bipod) {
2429
+ const bx = x1 + (L - x1) * 0.45;
2430
+ cylinderX(group, 8e-3, 0.02, M.dark, bx + 0.04, by - 0.012, 0, 12);
2431
+ for (const side of [-1, 1]) {
2432
+ const leg = kit.pivot(group, bx + 0.04, by - 0.012, side * 0.012);
2433
+ leg.rotation.set(side * 0.15, 0, -0.35);
2434
+ box2(leg, 6e-3, 0.2, 6e-3, M.dark, 0, -0.1, 0);
2435
+ box2(leg, 0.01, 6e-3, 0.01, M.rubber, 0, -0.2, 0);
2436
+ }
2437
+ }
2438
+ if (o.bolt) {
2439
+ const handle = kit.pivot(group, x1 - 0.045, by, 0);
2440
+ parts.boltHandle = handle;
2441
+ parts.boltHandleRest = handle.position.x;
2442
+ cylinderZ(handle, 42e-4, 0.05, M.steelL, 0, 0, wR / 2 + 0.022, 12);
2443
+ sphere(handle, 95e-4, M.dark, 0, -4e-3, wR / 2 + 0.048, 1);
2444
+ cylinderX(group, hR * 0.3, 0.03, M.steel, x0 + 5e-3, by, 0, 16);
2445
+ }
2446
+ if (o.lever) {
2447
+ const lever = kit.pivot(group, gripX - 0.02, -hR / 2 - 0.01, 0);
2448
+ parts.lever = lever;
2449
+ box2(lever, 0.11, 6e-3, 0.012, M.metal, 0.055, -0.052, 0);
2450
+ box2(lever, 6e-3, 0.05, 0.012, M.metal, 3e-3, -0.028, 0);
2451
+ box2(lever, 6e-3, 0.05, 0.012, M.metal, 0.108, -0.028, 0);
2452
+ box2(lever, 0.04, 6e-3, 0.012, M.metal, 0.08, -2e-3, 0);
2453
+ box2(group, 0.03, 0.012, 3e-3, M.dark, x1 - 0.04, by, wR / 2 + 1e-3);
2454
+ }
2455
+ if (o.tube) {
2456
+ const tubeLength = (L - x1) * (o.tubeLen || 0.8);
2457
+ cylinderX(group, 0.0105, tubeLength, M.metal, x1 + tubeLength / 2, by - 0.023, 0, 16);
2458
+ cylinderX(group, 0.0125, 0.02, M.dark, x1 + tubeLength + 6e-3, by - 0.023, 0, 16);
2459
+ }
2460
+ if (o.pump) {
2461
+ const pump = kit.pivot(group);
2462
+ parts.pump = pump;
2463
+ box2(pump, 0.17, 0.042, 0.046, furn, x1 + 0.14, by - 0.022, 0);
2464
+ for (let i = 0; i < 8; i++) box2(pump, 6e-3, 0.044, 0.048, M.dark, x1 + 0.07 + i * 0.02, by - 0.022, 0);
2465
+ }
2466
+ if (o.heat) {
2467
+ cylinderX(group, r * 1.75, (L - x1) * 0.7, M.dark, x1 + (L - x1) * 0.4, by, 0, 12);
2468
+ for (let i = 0; i < 10; i++) box2(group, 8e-3, r * 1.5, 4e-3, M.hole, x1 + (L - x1) * (0.1 + i * 0.06), by + r * 0.8, r * 1.5);
2469
+ }
2470
+ if (o.bayo) box2(group, 0.025, 8e-3, 0.01, M.metal, L - 0.07, by - 0.016, 0);
2471
+ if (o.rod) cylinderX(group, 3e-3, (L - x1) * 0.85, M.steel, x1 + (L - x1) * 0.45, by - 0.014, 0, 8);
2472
+ }
2473
+
2474
+ // src/models/rifle-forend.ts
2475
+ var PI7 = Math.PI;
2476
+ function buildForend(context) {
2477
+ const { kit, group, options: o, furniture: furn, length: L, receiverEnd: x1, receiverHeight: hR, receiverWidth: wR, barrelRadius: r, barrelY: by } = context;
2478
+ const { M, box: box2 } = kit;
2479
+ const hasLongScope = (o.scope ?? 0) >= 3;
2480
+ if (!context.isTraditional && o.hg !== "none") {
2481
+ const handguardLength = (L - x1) * (o.hgFrac || 0.5);
2482
+ const hx = x1 + handguardLength / 2;
2483
+ const hh = hR * 0.8;
2484
+ if (o.hg === "rail") {
2485
+ box2(group, handguardLength, hh, wR * 1.04, M.dark, hx, by - 2e-3, 0);
2486
+ rail(kit, group, x1, handguardLength, by + hh / 2, 0, wR * 0.66);
2487
+ rail(kit, group, x1 + 0.01, handguardLength - 0.02, by - 2e-3, wR * 0.54, wR * 0.4, PI7 / 2);
2488
+ rail(kit, group, x1 + 0.01, handguardLength - 0.02, by - 2e-3, -wR * 0.54, wR * 0.4, -PI7 / 2);
2489
+ rail(kit, group, x1 + 0.01, handguardLength - 0.02, by - hh / 2, 0, wR * 0.6, PI7);
2490
+ } else {
2491
+ const material = o.hg === "wood" ? M.wood : furn;
2492
+ if (o.ak) {
2493
+ box2(group, handguardLength, hh * 0.55, wR * 1.08, material, hx, by - hh * 0.22, 0);
2494
+ box2(group, handguardLength * 0.75, hh * 0.35, wR * 0.7, material, hx - handguardLength * 0.1, by + r * 2.2, 0);
2495
+ } else {
2496
+ box2(group, handguardLength, hh, wR * 1.08, material, hx, by - 2e-3, 0);
2497
+ vents(kit, group, x1 + 0.01, handguardLength - 0.02, by + hh * 0.15, wR * 0.54 + 1e-3, Math.round(handguardLength / 0.025), hh * 0.3);
2498
+ vents(kit, group, x1 + 0.01, handguardLength - 0.02, by + hh * 0.15, -wR * 0.54 - 1e-3, Math.round(handguardLength / 0.025), hh * 0.3);
2499
+ }
2500
+ }
2501
+ if (!hasLongScope && !o.rail && o.ak) {
2502
+ box2(group, 0.03, 0.016, wR * 0.6, M.metal, x1 + 0.02, by + r * 2.5 + 0.01, 0);
2503
+ box2(group, 0.012, 4e-3, wR * 0.5, M.dark, x1 + 0.03, by + r * 2.5 + 0.019, 0);
2504
+ }
2505
+ }
2506
+ if (hasLongScope) return;
2507
+ const sightX = L - (o.ak ? 0.06 : 0.08);
2508
+ if (o.ak) {
2509
+ box2(group, 0.022, 0.022, wR * 0.6, M.metal, sightX, by + 8e-3, 0);
2510
+ box2(group, 6e-3, 0.024, 4e-3, M.metal, sightX, by + 0.026, 8e-3);
2511
+ box2(group, 6e-3, 0.024, 4e-3, M.metal, sightX, by + 0.026, -8e-3);
2512
+ box2(group, 3e-3, 0.016, 3e-3, M.dark, sightX, by + 0.022, 0);
2513
+ } else if (!o.rail || o.carry) {
2514
+ box2(group, 0.018, 0.014, wR * 0.55, M.dark, sightX, by + 8e-3, 0);
2515
+ box2(group, 0.012, 0.034, 8e-3, M.dark, sightX, by + 0.03, 0, -0.25);
2516
+ } else box2(group, 0.012, 0.024, 0.012, M.dark, sightX - 0.04, by + hR * 0.4 + 0.012, 0);
2517
+ }
2518
+
2519
+ // src/models/rifle-controls.ts
2520
+ function buildGrip(context) {
2521
+ const { kit, group, parts, options: o, furniture: furn, receiverStart: x0, receiverHeight: hR, receiverWidth: wR } = context;
2522
+ const { M, box: box2 } = kit;
2523
+ const gx = x0 + (o.gripAt || 0.07);
2524
+ if (!context.isTraditional && o.grip !== "none") {
2525
+ const grip = kit.pivot(group, gx - 0.012, -hR / 2 - 0.046, 0);
2526
+ grip.rotation.z = -0.3;
2527
+ box2(grip, 0.032, 0.105, 0.028, furn, 0, 0, 0);
2528
+ for (let i = 0; i < 3; i++) box2(grip, 6e-3, 0.012, 0.026, furn, 0.017, 0.022 - i * 0.02, 0);
2529
+ box2(grip, 0.034, 8e-3, 0.03, furn, 0, -0.052, 0);
2530
+ }
2531
+ box2(group, 0.06, 4e-3, 0.01, M.metal, gx + 0.03, -hR / 2 - 0.024, 0);
2532
+ box2(group, 4e-3, 0.022, 0.01, M.metal, gx + 0.06, -hR / 2 - 0.013, 0);
2533
+ parts.trigger = box2(group, 4e-3, 0.016, 4e-3, M.dark, gx + 0.024, -hR / 2 - 0.012, 0, 0.15);
2534
+ if (o.sel) parts.safety = box2(group, 0.016, 5e-3, 3e-3, M.dark, gx + 5e-3, -hR * 0.15, -wR / 2 - 2e-3, 0.4);
2535
+ return gx;
2536
+ }
2537
+ function buildMagazine(context, gx) {
2538
+ const { kit, group, parts, options: o, receiverStart: x0, receiverHeight: hR, receiverWidth: wR, receiverLength: RL } = context;
2539
+ const { M, box: box2, cylinderX, cylinderZ } = kit;
2540
+ const mx = o.magAt != null ? x0 + o.magAt : gx + 0.075;
2541
+ const scale = o.magLen || 1;
2542
+ const magazine = kit.pivot(group, mx, -hR / 2, 0);
2543
+ parts.mag = magazine;
2544
+ const finish = o.magMat === "bakelite" ? M.bakelite : o.magMat === "poly" ? M.polyMag : void 0;
2545
+ if (o.mag === "curved") {
2546
+ let px = 0;
2547
+ let py = 0;
2548
+ const material = finish ?? M.metal;
2549
+ for (const angle of [0.06, 0.22, 0.38]) {
2550
+ const length = 0.07 * scale;
2551
+ const cx = px + Math.sin(angle) * length / 2;
2552
+ const cy = py - Math.cos(angle) * length / 2;
2553
+ box2(magazine, 0.052, length + 4e-3, 0.024, material, cx, cy, 0, angle);
2554
+ box2(magazine, 0.04, length * 0.8, 0.026, M.dark, cx, cy, 0, angle);
2555
+ px += Math.sin(angle) * length;
2556
+ py -= Math.cos(angle) * length;
2557
+ }
2558
+ box2(magazine, 0.058, 8e-3, 0.028, material, px, py, 0, 0.38);
2559
+ parts.magDrop = 0.07 * scale * 3 + 0.04;
2560
+ } else if (o.mag === "box") {
2561
+ const length = 0.13 * scale;
2562
+ const curve = o.curve || 0;
2563
+ const segment = length / 3;
2564
+ const material = finish ?? M.dark;
2565
+ let px = 0;
2566
+ let py = 0;
2567
+ let angle = 0.08;
2568
+ for (let i = 0; i < 3; i++) {
2569
+ angle = 0.08 + curve * i;
2570
+ const cx = px + Math.sin(angle) * segment / 2;
2571
+ const cy = py - Math.cos(angle) * segment / 2;
2572
+ box2(magazine, 0.05, segment + 4e-3, 0.024, material, cx, cy, 0, angle);
2573
+ box2(magazine, 0.04, 3e-3, 0.026, M.metal, cx, cy, 0, angle);
2574
+ px += Math.sin(angle) * segment;
2575
+ py -= Math.cos(angle) * segment;
2576
+ }
2577
+ box2(magazine, 0.056, 7e-3, 0.028, material, px, py - 2e-3, 0, angle);
2578
+ parts.magDrop = length + 0.03;
2579
+ } else if (o.mag === "drum") {
2580
+ cylinderZ(magazine, 0.075, 0.07, M.dark, 0.02, -0.085, 0, 22);
2581
+ cylinderZ(magazine, 0.05, 0.074, M.metal, 0.02, -0.085, 0, 18);
2582
+ box2(magazine, 0.05, 0.04, 0.03, M.dark, 0, -0.02, 0);
2583
+ parts.magDrop = 0.2;
2584
+ } else if (o.mag === "internal") {
2585
+ box2(group, 0.08, 0.025, 0.03, context.isTraditional ? context.furniture : M.metal, mx, -hR / 2 - 0.012, 0);
2586
+ parts.mag = null;
2587
+ } else parts.mag = null;
2588
+ if (o.mag === "belt") {
2589
+ const belt = kit.pivot(group, mx + 0.02, -hR / 2 - 0.05, -0.045);
2590
+ parts.belt = belt;
2591
+ box2(belt, 0.11, 0.1, 0.08, M.olive, 0, 0, 0);
2592
+ box2(belt, 0.112, 0.012, 0.082, M.dark, 0, 0.046, 0);
2593
+ for (let i = 0; i < 7; i++) {
2594
+ cylinderX(belt, 42e-4, 0.034, M.brass, -0.04 + i * 0.012, 0.07, 0.035, 10);
2595
+ box2(belt, 6e-3, 6e-3, 0.012, M.metal, -0.04 + i * 0.012, 0.066, 0.035);
2596
+ }
2597
+ const cover = kit.pivot(group, x0 + 0.01, hR / 2, 0);
2598
+ parts.cover = cover;
2599
+ box2(cover, RL - 0.03, 0.014, wR * 1.05, M.metal, (RL - 0.03) / 2, 7e-3, 0);
2600
+ box2(cover, 0.02, 0.012, wR * 0.6, M.dark, RL - 0.04, 0.016, 0);
2601
+ }
2602
+ return mx;
2603
+ }
2604
+
2605
+ // src/models/rifle-stock.ts
2606
+ function buildStock(context, magazineX) {
2607
+ const { kit, group, options: o, furniture: furn, length: L, stockLength: S, receiverStart: x0, receiverEnd: x1, receiverHeight: hR } = context;
2608
+ const { M, box: box2, cylinderX } = kit;
2609
+ if (context.isTraditional) {
2610
+ buttpad(kit, group, 6e-3, -0.04, 0.13, 0.038);
2611
+ box2(group, 0.03, 0.13, 0.038, furn, 0.024, -0.04, 0);
2612
+ box2(group, S, 0.055, 0.034, furn, S / 2, -0.012, 0, 0.06);
2613
+ box2(group, 0.09, 0.05, 0.032, furn, x0 - 0.02, -0.035, 0, 0.25);
2614
+ const forendLength = x1 - x0 + (L - x1) * (o.fore ?? 0.62);
2615
+ if (forendLength > 0) box2(group, forendLength, 0.045, 0.04, furn, x0 + forendLength / 2, -hR / 2 + 4e-3, 0);
2616
+ if (o.stock === "straight") box2(group, 0.15, 0.04, 0.035, furn, x1 + 0.08, -hR / 2, 0);
2617
+ box2(group, 0.04, 4e-3, 0.03, M.metal, magazineX, -hR / 2 - 0.022, 0);
2618
+ swivel(kit, group, 0.2, -0.08);
2619
+ swivel(kit, group, x1 + (L - x1) * 0.45, -hR / 2 - 0.03);
2620
+ if (o.bands) for (const fraction of [0.35, 0.75]) cylinderX(group, 0.024, 8e-3, M.metal, x1 + (L - x1) * fraction * (o.fore || 0.62), -4e-3, 0, 16);
2621
+ } else if (o.stock === "fixed" || o.stock === "wood") {
2622
+ box2(group, S, 0.05, 0.03, furn, S / 2, -8e-3, 0, 0.05);
2623
+ box2(group, 0.03, 0.13, 0.036, furn, 0.022, -0.035, 0);
2624
+ buttpad(kit, group, 4e-3, -0.035, 0.13, 0.036);
2625
+ if (o.stock === "wood") box2(group, S * 0.6, 0.03, 0.028, furn, S * 0.55, -0.045, 0, 0.2);
2626
+ swivel(kit, group, 0.08, -0.09);
2627
+ } else if (o.stock === "skeleton") {
2628
+ box2(group, S, 0.014, 0.026, furn, S / 2, 0.012, 0, 0.03);
2629
+ box2(group, S, 0.014, 0.026, furn, S / 2, -0.05, 0, -0.08);
2630
+ box2(group, 0.03, 0.12, 0.034, furn, 0.02, -0.025, 0);
2631
+ buttpad(kit, group, 4e-3, -0.025, 0.12, 0.034);
2632
+ } else if (o.stock === "retract") {
2633
+ cylinderX(group, 0.013, S, M.metal, S / 2, 0, 0, 16);
2634
+ for (let i = 0; i < 5; i++) box2(group, 3e-3, 3e-3, 8e-3, M.dark, S * 0.2 + i * 0.015, -0.013, 0);
2635
+ box2(group, 0.034, 0.11, 0.036, furn, 0.02, -0.025, 0);
2636
+ box2(group, S * 0.55, 0.032, 0.03, furn, S * 0.35, 0, 0);
2637
+ buttpad(kit, group, 3e-3, -0.025, 0.11, 0.036);
2638
+ } else if (o.stock === "fold") {
2639
+ box2(group, S, 6e-3, 6e-3, M.metal, S / 2, 0.01, 0.012);
2640
+ box2(group, S, 6e-3, 6e-3, M.metal, S / 2, -0.04, 0.012, -0.12);
2641
+ box2(group, 0.012, 0.1, 0.03, M.metal, 6e-3, -0.02, 0.012);
2642
+ }
2643
+ }
2644
+
2645
+ // src/models/rifle.ts
2646
+ var PI8 = Math.PI;
2647
+ function buildReceiver(context) {
2648
+ const { kit, group, parts, options: o, length: L, receiverStart: x0, receiverEnd: x1, receiverHeight: hR, receiverWidth: wR, receiverLength: RL, barrelY: by } = context;
2649
+ const { M, box: box2, cylinderX, cylinderY, cylinderZ, sphere } = kit;
2650
+ const receiverMaterial = o.tan ? M.tan : M.metal;
2651
+ const middle = (x0 + x1) / 2;
2652
+ if (o.round) {
2653
+ cylinderX(group, hR * 0.36, RL, M.steel, middle, by, 0, 20);
2654
+ cylinderX(group, hR * 0.32, 0.05, M.steel, x0 - 0.01, by, 0, 18);
2655
+ } else if (o.ar) {
2656
+ box2(group, RL, hR * 0.5, wR, receiverMaterial, middle, hR * 0.25, 0);
2657
+ box2(group, RL * 0.92, hR * 0.52, wR * 0.96, receiverMaterial, middle - RL * 0.02, -hR * 0.24, 0);
2658
+ box2(group, 0.05, 0.03, wR * 1.15, receiverMaterial, x0 + RL * 0.62, -hR * 0.5 - 0.01, 0);
2659
+ cylinderZ(group, 6e-3, 0.012, M.metal, x1 - 0.03, hR * 0.28, wR / 2 + 6e-3, 12);
2660
+ box2(group, 0.012, 0.01, 0.012, M.metal, x1 - 0.05, hR * 0.12, wR / 2 + 6e-3);
2661
+ box2(group, 0.03, 8e-3, 0.012, M.dark, x0 + 0.02, hR / 2 + 4e-3, 0);
2662
+ box2(group, 0.012, 6e-3, 4e-3, M.dark, x0 + RL * 0.55, -hR * 0.15, -wR / 2 - 2e-3);
2663
+ } else box2(group, RL, hR, wR, receiverMaterial, middle, 0, 0);
2664
+ if (o.ak) {
2665
+ box2(group, RL * 0.86, hR * 0.28, wR * 0.95, M.metal, middle - 8e-3, hR / 2 + 4e-3, 0);
2666
+ for (let i = 0; i < 3; i++) box2(group, 4e-3, hR * 0.3, wR * 0.97, M.dark, x0 + RL * 0.25 + i * 0.02, hR / 2 + 4e-3, 0);
2667
+ box2(group, RL * 0.5, 0.012, 3e-3, M.metal, x0 + RL * 0.55, -hR * 0.02, wR / 2 + 15e-4, -0.05);
2668
+ box2(group, 0.012, 0.014, 3e-3, M.dark, x0 + RL * 0.32, -hR * 0.05, wR / 2 + 2e-3);
2669
+ }
2670
+ if (o.g3) {
2671
+ cylinderX(group, 9e-3, (L - x1) * 0.6, M.metal, x1 + (L - x1) * 0.28, hR * 0.48, 0, 14);
2672
+ box2(group, 0.012, 0.024, 8e-3, M.dark, x1 + (L - x1) * 0.5, hR * 0.48, -0.012);
2673
+ cylinderY(group, 0.012, 0.016, M.dark, x0 + 0.03, hR / 2 + 8e-3, 0, 12);
2674
+ }
2675
+ const ejectX = x1 - (o.ejectBack || 0.07);
2676
+ const ejectY = o.ar ? hR * 0.2 : by;
2677
+ box2(group, 0.05, hR * 0.36, 4e-3, M.hole, ejectX, ejectY, wR / 2 - 4e-4);
2678
+ const bolt = kit.pivot(group);
2679
+ parts.bolt = bolt;
2680
+ parts.stroke = Math.min(0.1, RL * 0.32);
2681
+ box2(bolt, 0.044, hR * 0.26, 6e-3, M.steelL, ejectX, ejectY, wR / 2 - 12e-4);
2682
+ if (o.charge) {
2683
+ box2(bolt, 0.012, 0.01, 0.03, M.metal, ejectX + 0.04, ejectY + 4e-3, wR / 2 + 0.012);
2684
+ sphere(bolt, 65e-4, M.metal, ejectX + 0.04, ejectY + 4e-3, wR / 2 + 0.028, 1);
2685
+ }
2686
+ if (o.ar) box2(group, 0.012, 0.016, 6e-3, receiverMaterial, ejectX - 0.03, ejectY + 4e-3, wR / 2 + 4e-3);
2687
+ if (o.rail) rail(kit, group, x0 - 5e-3, RL + 0.07, hR / 2 + (o.ar ? 0 : 3e-3), 0, wR * 0.68);
2688
+ }
2689
+ function buildBarrel(context) {
2690
+ const { kit, group, parts, options: o, length: L, barrelLength: Bl, receiverEnd: x1, barrelRadius: r, barrelY: by } = context;
2691
+ const { M, box: box2, cylinderX } = kit;
2692
+ heatify(parts.glow, cylinderX(group, r, Bl, M.metal, L - Bl / 2, by, 0, 16));
2693
+ if (o.jacket) {
2694
+ cylinderX(group, r * 2, (L - x1) * 0.6, M.dark, x1 + (L - x1) * 0.3, by, 0, 16);
2695
+ for (let i = 0; i < 6; i++) cylinderX(group, r * 2.15, 4e-3, M.metal, x1 + (L - x1) * (0.05 + i * 0.09), by, 0, 16);
2696
+ }
2697
+ if (o.gas) cylinderX(group, r * 0.75, (L - x1) * 0.55, M.metal, x1 + (L - x1) * 0.3, by + r * 2.2, 0, 14);
2698
+ if (o.brake) {
2699
+ const scale = o.brake === 2 ? 3.2 : 2;
2700
+ const brakeLength = o.brake === 2 ? 0.12 : 0.06;
2701
+ heatify(parts.glow, cylinderX(group, r * scale, brakeLength, M.dark, L - brakeLength / 2, by, 0, o.brake === 2 ? 8 : 16));
2702
+ for (let i = 0; i < 3; i++) box2(group, brakeLength * 0.16, r * scale * 1.3, r * scale * 2.2, M.hole, L - brakeLength * 0.25 - i * brakeLength * 0.25, by, 0);
2703
+ } else if (!o.noFH) {
2704
+ heatify(parts.glow, cylinderX(group, r * 1.45, 0.042, M.dark, L - 0.021, by, 0, 16));
2705
+ for (let i = 0; i < 4; i++) {
2706
+ const angle = i * PI8 / 2 + PI8 / 4;
2707
+ box2(group, 0.026, 25e-4, 4e-3, M.hole, L - 0.02, by + Math.cos(angle) * r * 1.42, Math.sin(angle) * r * 1.42, 0, 0, angle);
2708
+ }
2709
+ }
2710
+ cylinderX(group, r * 0.55, 4e-3, M.hole, L + 5e-4, by, 0, 12);
2711
+ }
2712
+ var familyOf = (o) => {
2713
+ if (o.mag === "belt") return "belt";
2714
+ if (o.pump) return "pump";
2715
+ if (o.lever) return "lever";
2716
+ if (o.tube) return "semi12";
2717
+ if (o.bolt) return o.mag === "box" ? "boltmag" : "bolt";
2718
+ return o.mag === "internal" ? "semiint" : "rifle";
2719
+ };
2720
+ function buildRifle(kit, options) {
2721
+ const { M } = kit;
2722
+ const group = kit.group();
2723
+ const parts = { family: "rifle", glow: [] };
2724
+ const length = (options.L ?? 0) / 1e3;
2725
+ const barrelLength = (options.B ?? 0) / 1e3;
2726
+ const isTraditional = options.stock === "trad" || options.stock === "straight";
2727
+ const stockLength = options.stock === "none" ? 0 : options.stockLen || Math.min(0.36, Math.max(0.2, 0.3 * length));
2728
+ const receiverStart = stockLength;
2729
+ const receiverEnd = Math.max(receiverStart + 0.12, length - barrelLength * 0.92);
2730
+ const receiverHeight = options.hR || 0.06;
2731
+ const context = {
2732
+ kit,
2733
+ group,
2734
+ parts,
2735
+ options,
2736
+ furniture: options.wood ? M.wood : options.olive ? M.olive : options.tan ? M.tan : M.poly,
2737
+ isTraditional,
2738
+ length,
2739
+ barrelLength,
2740
+ stockLength,
2741
+ receiverStart,
2742
+ receiverEnd,
2743
+ receiverHeight,
2744
+ receiverWidth: options.wR || 0.034,
2745
+ barrelRadius: options.br || 85e-4,
2746
+ barrelY: receiverHeight * 0.15,
2747
+ receiverLength: receiverEnd - receiverStart
2748
+ };
2749
+ buildReceiver(context);
2750
+ buildBarrel(context);
2751
+ buildForend(context);
2752
+ const gripX = buildGrip(context);
2753
+ const magazineX = buildMagazine(context, gripX);
2754
+ buildStock(context, magazineX);
2755
+ buildFittings(context, gripX);
2756
+ const { barrelY: by, receiverWidth: wR } = context;
2757
+ const ejectX = receiverEnd - (options.ejectBack || 0.07);
2758
+ const ejectY = options.ar ? receiverHeight * 0.2 : by;
2759
+ parts.muzzle = kit.mark(group, length + 0.01, by, 0);
2760
+ parts.eject = kit.mark(group, ejectX, ejectY, wR / 2 + 6e-3);
2761
+ parts.loadPort = kit.mark(group, receiverEnd - 0.05, -receiverHeight / 2 - 4e-3, 0);
2762
+ parts.topLoad = kit.mark(group, ejectX, receiverHeight / 2 + 4e-3, 0);
2763
+ parts.family = familyOf(options);
2764
+ return { group, parts };
2765
+ }
2766
+
2767
+ // src/models/index.ts
2768
+ var MODEL_ENTRIES = models_default;
2769
+ var ARCHETYPE_MODELS = (() => {
2770
+ const actionOf = new Map(WEAPONS.map((def) => [def.id, def.action.kind]));
2771
+ const chosen = {};
2772
+ for (const def of [...WEAPONS].sort((a, b) => a.id < b.id ? -1 : 1)) {
2773
+ const { archetype } = def;
2774
+ if (!archetype || !(def.id in MODEL_ENTRIES) || archetype in MODEL_ENTRIES || archetype in chosen) continue;
2775
+ if (actionOf.get(archetype) === def.action.kind) chosen[archetype] = def.id;
2776
+ }
2777
+ return chosen;
2778
+ })();
2779
+ var BUILDERS = {
2780
+ pistol: buildPistol,
2781
+ revolver: buildRevolver,
2782
+ rifle: buildRifle,
2783
+ bullpup: buildBullpup,
2784
+ p90: (kit) => buildP90(kit),
2785
+ break: buildBreakAction,
2786
+ m2: (kit) => buildM2(kit),
2787
+ launcher: buildLauncher
2788
+ };
2789
+ var nameParts = (parts) => {
2790
+ for (const [name, part] of partRegistry(parts)) part.name = name;
2791
+ };
2792
+ var fitLength = (kit, inner, lengthMetres) => {
2793
+ const box2 = new kit.THREE.Box3().setFromObject(inner);
2794
+ const factor = lengthMetres / (box2.max.x - box2.min.x);
2795
+ inner.scale.x = factor;
2796
+ inner.position.x = -box2.min.x * factor;
2797
+ const root = kit.group();
2798
+ root.add(inner);
2799
+ return root;
2800
+ };
2801
+ var hasWeaponModel = (id) => id in MODEL_ENTRIES || id in ARCHETYPE_MODELS;
2802
+ function buildWeaponModelById(THREE, id) {
2803
+ const entry = MODEL_ENTRIES[ARCHETYPE_MODELS[id] ?? id];
2804
+ if (!entry) return void 0;
2805
+ const kit = modelKit(THREE);
2806
+ const options = { L: entry.length, B: entry.barrel, cal: entry.caliber, ...entry.options };
2807
+ if (id in ARCHETYPE_MODELS) delete options.chambers;
2808
+ const built = BUILDERS[entry.builder](kit, options);
2809
+ nameParts(built.parts);
2810
+ let root;
2811
+ if (entry.builder === "launcher") {
2812
+ root = kit.group();
2813
+ root.add(built.group);
2814
+ built.group.position.x = -new THREE.Box3().setFromObject(built.group).min.x;
2815
+ } else root = fitLength(kit, built.group, entry.length / 1e3);
2816
+ root.name = id;
2817
+ return { id, root, inner: built.group, parts: built.parts };
2818
+ }
2819
+
108
2820
  // src/props.ts
109
2821
  var box = (id, material, size, position, extra = {}) => ({ id, material, shape: { kind: "box", size }, position, ...extra });
110
2822
  var joint = (a, b, kind) => ({ a, b, kind });
@@ -176,19 +2888,47 @@ function buildProp(THREE, prop) {
176
2888
  function buildWeaponModel(THREE, def) {
177
2889
  return buildProp(THREE, weaponProp(def));
178
2890
  }
2891
+ var MODEL_BODY = "body";
2892
+ function buildModeledProp(THREE, def, name) {
2893
+ const model = buildWeaponModelById(THREE, def.id);
2894
+ if (!model) return void 0;
2895
+ const group = new THREE.Group();
2896
+ group.name = name;
2897
+ const box2 = new THREE.Box3().setFromObject(model.root);
2898
+ const center = box2.getCenter(new THREE.Vector3());
2899
+ model.root.position.set(-center.x, -box2.min.y, -center.z);
2900
+ group.add(model.root);
2901
+ group.updateMatrixWorld(true);
2902
+ for (const mesh of mergeByMaterial(THREE, model.root, group)) {
2903
+ mesh.userData.destructionPart = MODEL_BODY;
2904
+ group.add(mesh);
2905
+ }
2906
+ group.remove(model.root);
2907
+ const size = box2.getSize(new THREE.Vector3());
2908
+ group.userData.footprint = [size.x, size.y, size.z];
2909
+ return group;
2910
+ }
2911
+ function modeledPropSpec(root, def, name) {
2912
+ const footprint = root.userData.footprint ?? [0.3, 0.1, 0.1];
2913
+ const size = [Math.max(0.02, footprint[0]), Math.max(0.02, footprint[1]), Math.max(0.02, footprint[2])];
2914
+ return { name, parts: [{ id: MODEL_BODY, material: "steel", shape: { kind: "box", size }, position: [0, size[1] / 2, 0], mass: def.mass }], joints: [], tags: ["weapon"] };
2915
+ }
179
2916
 
180
- // src/fx.ts
2917
+ // src/box-fx.ts
181
2918
  var EJECTS_ON_SHOT = /* @__PURE__ */ new Set(["pistol", "smg", "carbine", "rifle", "sniper", "shotgun"]);
182
2919
  var DROPS_MAGAZINE = /* @__PURE__ */ new Set(["pistol", "smg", "carbine", "rifle", "sniper"]);
183
2920
  var CYCLES = /* @__PURE__ */ new Set(["shotgun_pump_12", "rifle_bolt_308"]);
2921
+ var FALLBACK_ENERGY = 300;
184
2922
  var MAX_CASINGS = 120;
185
2923
  var MAX_MAGAZINES = 12;
186
2924
  var LIFETIME_S = 25;
187
2925
  var HAND_OFFSET = [0.2, -0.2, -0.42];
188
2926
  var GRAVITY = 9.81;
189
- function createWeaponFx(options) {
2927
+ function createBoxFx(options) {
190
2928
  const { THREE, scene, camera, arsenal, groundAt } = options;
191
2929
  const audio = options.audio ?? null;
2930
+ const random = createRng2(options.seed ?? 1);
2931
+ const layers = options.layers ?? createLayers();
192
2932
  const bodies = [];
193
2933
  const brass = new THREE.MeshStandardMaterial({ color: 13214282, metalness: 0.8, roughness: 0.35 });
194
2934
  const shell = new THREE.MeshStandardMaterial({ color: 11022891, roughness: 0.6 });
@@ -220,13 +2960,17 @@ function createWeaponFx(options) {
220
2960
  };
221
2961
  const handPoint = (x, y, z, out) => out.set(HAND_OFFSET[0] + x, HAND_OFFSET[1] + y, HAND_OFFSET[2] + z).applyQuaternion(camera.quaternion).add(camera.position);
222
2962
  const throwBody = (kind, mesh, from, velocity, radius) => {
2963
+ if (!(kind === "casing" ? layers.casings : layers.magazines)) {
2964
+ mesh.geometry.dispose();
2965
+ return;
2966
+ }
223
2967
  const limit = kind === "casing" ? MAX_CASINGS : MAX_MAGAZINES;
224
2968
  const same = bodies.filter((b) => b.kind === kind);
225
2969
  if (same.length >= limit) removeBody(same[0]);
226
2970
  mesh.position.copy(from);
227
2971
  mesh.castShadow = true;
228
2972
  scene.add(mesh);
229
- bodies.push({ mesh, velocity, spin: new THREE.Vector3(Math.random() * 20 - 10, Math.random() * 30 - 15, Math.random() * 20 - 10), radius, age: 0, resting: false, bounces: 0, kind });
2973
+ bodies.push({ mesh, velocity, spin: new THREE.Vector3(random() * 20 - 10, random() * 30 - 15, random() * 20 - 10), radius, age: 0, resting: false, bounces: 0, kind });
230
2974
  };
231
2975
  const ejectCasing = (caliberMm, isShell, downward) => {
232
2976
  const radius = isShell ? 0.0105 : Math.max(3e-3, caliberMm / 1e3 * 0.6);
@@ -236,7 +2980,7 @@ function createWeaponFx(options) {
236
2980
  const from = handPoint(0.03, 0.03, 0.05, new THREE.Vector3());
237
2981
  const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
238
2982
  const up = new THREE.Vector3(0, 1, 0);
239
- const velocity = downward ? up.clone().multiplyScalar(-0.5).addScaledVector(right, (Math.random() - 0.5) * 0.4) : right.multiplyScalar(1.6 + Math.random() * 0.8).addScaledVector(up, 1.4 + Math.random() * 0.8);
2983
+ const velocity = downward ? up.clone().multiplyScalar(-0.5).addScaledVector(right, (random() - 0.5) * 0.4) : right.multiplyScalar(1.6 + random() * 0.8).addScaledVector(up, 1.4 + random() * 0.8);
240
2984
  throwBody("casing", mesh, from, velocity, radius);
241
2985
  };
242
2986
  const dropMagazine = (category) => {
@@ -248,7 +2992,7 @@ function createWeaponFx(options) {
248
2992
  throwBody("magazine", mesh, from, new THREE.Vector3(0, -0.4, 0), size[1] / 2);
249
2993
  audio?.reloadStart(category);
250
2994
  };
251
- const caliberOf = (weapon) => {
2995
+ const caliberOf2 = (weapon) => {
252
2996
  if (weapon.action.kind !== "firearm") return 9;
253
2997
  return arsenal.calibers.get(weapon.action.caliber)?.diameter ?? 9;
254
2998
  };
@@ -260,17 +3004,17 @@ function createWeaponFx(options) {
260
3004
  const onShot = (weapon) => {
261
3005
  kick = 1;
262
3006
  const caliber = weapon.action.kind === "firearm" ? arsenal.calibers.get(weapon.action.caliber) : void 0;
263
- const energy = caliber ? 0.5 * (caliber.bulletGrams / 1e3) * caliber.muzzleSpeed ** 2 * (caliber.pellets ?? 1) : 300;
264
- audio?.shot(energy, weapon.category);
3007
+ const energy = caliber ? muzzleEnergy2(caliber) * (caliber.pellets ?? 1) : FALLBACK_ENERGY;
3008
+ audio?.shot(energy, weapon.category, { ...caliber ? { caliber } : {}, distance: options.soundDistance?.() ?? 0 });
265
3009
  if (CYCLES.has(weapon.id)) audio?.cycle();
266
- if (EJECTS_ON_SHOT.has(weapon.category) && weapon.id !== "shotgun_double_12") ejectCasing(caliberOf(weapon), weapon.category === "shotgun", false);
3010
+ if (EJECTS_ON_SHOT.has(weapon.category) && weapon.id !== "shotgun_double_12") ejectCasing(caliberOf2(weapon), weapon.category === "shotgun", false);
267
3011
  };
268
3012
  const onReloadStart = (weapon, tool) => {
269
3013
  if (DROPS_MAGAZINE.has(weapon.category)) dropMagazine(weapon.category);
270
3014
  else audio?.reloadStart(weapon.category);
271
3015
  if (weapon.category === "revolver" || weapon.id === "shotgun_double_12") {
272
3016
  const spent = Math.max(1, tool.capacity - tool.ammo);
273
- for (let i = 0; i < spent; i++) ejectCasing(caliberOf(weapon), weapon.category === "shotgun", true);
3017
+ for (let i = 0; i < spent; i++) ejectCasing(caliberOf2(weapon), weapon.category === "shotgun", true);
274
3018
  }
275
3019
  };
276
3020
  const stepBody = (body, dt) => {
@@ -300,7 +3044,13 @@ function createWeaponFx(options) {
300
3044
  body.mesh.rotation.z = 0;
301
3045
  };
302
3046
  return {
3047
+ unequip() {
3048
+ if (model) hand.remove(model);
3049
+ model = null;
3050
+ inHand = null;
3051
+ },
303
3052
  handle(event, tool) {
3053
+ if (event.type === "explosion") return;
304
3054
  const weapon = arsenal.weapons.get(event.weapon);
305
3055
  if (event.type === "tool") equip(event.weapon);
306
3056
  if (!weapon) return;
@@ -342,14 +3092,1265 @@ function createWeaponFx(options) {
342
3092
  };
343
3093
  }
344
3094
 
3095
+ // src/fx/viewmodel.ts
3096
+ import { createToolClipPlayer, toolAnimationStep } from "@adatechnology/game-contracts";
3097
+ import { createRng as createRng3, muzzleEnergy as muzzleEnergy3 } from "@adatechnology/game-weapons";
3098
+
3099
+ // src/animation/clip-builder.ts
3100
+ import { STAGE_EVENT } from "@adatechnology/game-contracts";
3101
+ var ROOT = "root";
3102
+ var AXIS_INDEX = { x: 0, y: 1, z: 2 };
3103
+ var EASE_STEPS = 10;
3104
+ var ease = (progress) => progress < 0.5 ? 2 * progress * progress : 1 - (-2 * progress + 2) ** 2 / 2;
3105
+ var move = (part, axis, start, duration, from, to) => ({ part, channel: "position", axis, start, duration, from, to });
3106
+ var turn = (part, axis, start, duration, from, to) => ({ part, channel: "rotation", axis, start, duration, from, to });
3107
+ var cue = (t, type, data) => ({ t, type, ...data ? { data } : {} });
3108
+ var stage = (id, t) => ({ t, type: STAGE_EVENT, data: { stage: id } });
3109
+ var valueAt = (tweens, t, includeInstants) => {
3110
+ let value = 0;
3111
+ for (const tween of tweens) {
3112
+ if (tween.start > t || tween.duration === 0 && (tween.start === t ? !includeInstants : false)) continue;
3113
+ if (tween.duration === 0 || t >= tween.start + tween.duration) value = tween.to;
3114
+ else value = tween.from + (tween.to - tween.from) * ease((t - tween.start) / tween.duration);
3115
+ }
3116
+ return value;
3117
+ };
3118
+ var round = (value) => Math.round(value * 1e6) / 1e6;
3119
+ function composeClip(id, tweens, events = [], minDuration = 0) {
3120
+ const ordered = [...tweens].sort((a, b) => a.start - b.start);
3121
+ const duration = Math.max(minDuration, 1e-3, ...ordered.map((tween) => tween.start + tween.duration), ...events.map((event) => event.t));
3122
+ const times = /* @__PURE__ */ new Set([0, duration]);
3123
+ for (const tween of ordered) {
3124
+ times.add(round(tween.start));
3125
+ if (tween.duration > 0) for (let step = 1; step <= EASE_STEPS; step++) times.add(round(tween.start + tween.duration * step / EASE_STEPS));
3126
+ }
3127
+ const instants = new Set(ordered.filter((tween) => tween.duration === 0).map((tween) => round(tween.start)));
3128
+ const channels = /* @__PURE__ */ new Map();
3129
+ for (const tween of ordered) {
3130
+ const key = `${tween.part}|${tween.channel}|${tween.axis}`;
3131
+ channels.set(key, [...channels.get(key) ?? [], tween]);
3132
+ }
3133
+ const sampleKey = (t, includeInstants) => {
3134
+ const poses = /* @__PURE__ */ new Map();
3135
+ for (const [key, list] of channels) {
3136
+ const [part, channel, axis] = key.split("|");
3137
+ const pose = poses.get(part) ?? { position: [0, 0, 0], rotation: [0, 0, 0] };
3138
+ pose[channel][AXIS_INDEX[axis]] = round(valueAt(list, t, includeInstants));
3139
+ poses.set(part, pose);
3140
+ }
3141
+ const root = poses.get(ROOT);
3142
+ poses.delete(ROOT);
3143
+ const parts = {};
3144
+ for (const [part, pose] of poses) parts[part] = pose;
3145
+ return { t, ...root ? { position: root.position, rotation: root.rotation } : {}, ...poses.size ? { parts } : {} };
3146
+ };
3147
+ const keys = [];
3148
+ for (const t of [...times].sort((a, b) => a - b)) {
3149
+ if (instants.has(t)) keys.push(sampleKey(t, false));
3150
+ keys.push(sampleKey(t, true));
3151
+ }
3152
+ const sorted = [...events].sort((a, b) => a.t - b.t);
3153
+ return { id, duration, keys, ...sorted.length ? { events: sorted } : {} };
3154
+ }
3155
+ var renamed = (clip, id) => ({ ...clip, id });
3156
+
3157
+ // src/animation/magazine-clips.ts
3158
+ var MAGAZINE_SWAP_SECONDS = 0.82;
3159
+ var MAGAZINE_IN_AT = 0.45;
3160
+ function magazineSwap(parts, start) {
3161
+ const direction = parts.magDir || -1;
3162
+ const drop = parts.magDrop || 0.15;
3163
+ return {
3164
+ tweens: [
3165
+ move("mag", "y", start, 0.16, 0, direction * drop * 0.6),
3166
+ move("mag", "y", start + 0.16, 0, 0, direction * drop * 2.2),
3167
+ move("mag", "y", start + MAGAZINE_IN_AT, 0.3, direction * drop * 2.2, 0)
3168
+ ],
3169
+ events: [cue(start + 0.16, "drop_magazine", { up: direction > 0 })],
3170
+ end: start + MAGAZINE_SWAP_SECONDS
3171
+ };
3172
+ }
3173
+
3174
+ // src/animation/context.ts
3175
+ var KICK_BY_CASE = { shell: 1.6, big: 2.4, tiny: 0.3, pistol: 0.7, rifle: 1.1 };
3176
+ var kickOf = (context) => context.parts.kick ?? KICK_BY_CASE[context.caseType];
3177
+ var BLOWBACK = /* @__PURE__ */ new Set(["pistol", "rifle", "semiint", "semi12", "p90", "belt"]);
3178
+ var PUMP_TRAVEL = -0.075;
3179
+ var BOLT_TRAVEL = -0.07;
3180
+ var BOLT_TURN = -1.15;
3181
+ var LEVER_TURN = -0.85;
3182
+ var strokeOf = (parts) => -(parts.stroke || 0.06);
3183
+ var blowbackPart = (context) => context.family === "pistol" ? "slide" : "bolt";
3184
+
3185
+ // src/animation/cycle-clips.ts
3186
+ function boltStroke(start, hold = 0) {
3187
+ return [
3188
+ turn("boltHandle", "x", start, 0.09, 0, BOLT_TURN),
3189
+ move("boltHandle", "x", start + 0.1, 0.12, 0, BOLT_TRAVEL),
3190
+ move("bolt", "x", start + 0.1, 0.12, 0, BOLT_TRAVEL),
3191
+ move("boltHandle", "x", start + 0.26 + hold, 0.12, BOLT_TRAVEL, 0),
3192
+ move("bolt", "x", start + 0.26 + hold, 0.12, BOLT_TRAVEL, 0),
3193
+ turn("boltHandle", "x", start + 0.4 + hold, 0.09, BOLT_TURN, 0)
3194
+ ];
3195
+ }
3196
+ function cycleClip(context) {
3197
+ const { parts, family } = context;
3198
+ if (family === "pump" && parts.pump) return composeClip("cycle", [move("pump", "x", 0.22, 0.11, 0, PUMP_TRAVEL), move("pump", "x", 0.36, 0.11, PUMP_TRAVEL, 0)], [cue(0.3, "eject_casing", { down: false })], 0.5);
3199
+ if ((family === "bolt" || family === "boltmag") && parts.bolt && parts.boltHandle) return composeClip("cycle", boltStroke(0.3), [cue(0.5, "eject_casing", { down: false })], 0.82);
3200
+ if (family === "lever" && parts.lever) return composeClip("cycle", [turn("lever", "z", 0.22, 0.13, 0, LEVER_TURN), turn("lever", "z", 0.38, 0.13, LEVER_TURN, 0)], [cue(0.3, "eject_casing", { down: false })], 0.55);
3201
+ return void 0;
3202
+ }
3203
+ var STUCK_AT = { stovepipe: 0.35, double_feed: 0.6, misfire: 0, failure_to_extract: 0 };
3204
+ var CLEAR_HOLD = 0.04;
3205
+ function clearBlowback(context, fault) {
3206
+ const { parts, family } = context;
3207
+ const part = blowbackPart(context);
3208
+ const travel = strokeOf(parts);
3209
+ const stuck = travel * (STUCK_AT[fault] ?? 0);
3210
+ const swap = fault === "double_feed" && parts.mag ? magazineSwap(parts, 0) : void 0;
3211
+ const start = swap?.end ?? 0;
3212
+ const tweens = [...swap?.tweens ?? []];
3213
+ const events = [...swap?.events ?? []];
3214
+ if (family === "belt" && parts.cover) {
3215
+ tweens.push(turn("cover", "z", start, 0.18, 0, 0.9), turn("cover", "z", start + 0.3, 0.15, 0.9, 0));
3216
+ return composeClip(`clear_${fault}`, [...tweens, ...slideOut(part, stuck, travel, start + 0.5)], [...events, cue(start + 0.5 + 0.1, "fly_jam", { jam: fault })]);
3217
+ }
3218
+ return composeClip(`clear_${fault}`, [...tweens, ...slideOut(part, stuck, travel, start)], [...events, cue(start + 0.1, "fly_jam", { jam: fault })]);
3219
+ }
3220
+ var slideOut = (part, from, travel, start) => [move(part, "x", start, 0.12, from, travel), move(part, "x", start + 0.2, 0.07, travel, 0)];
3221
+ function clearManual(context, fault) {
3222
+ const { parts, family } = context;
3223
+ const fly = (t) => [cue(t, "fly_jam", { jam: fault })];
3224
+ if (family === "pump" && parts.pump) return composeClip(`clear_${fault}`, [move("pump", "x", 0, 0.12, 0, PUMP_TRAVEL), move("pump", "x", 0.2, 0.12, PUMP_TRAVEL, 0)], fly(0.12), 0.4);
3225
+ if ((family === "bolt" || family === "boltmag") && parts.bolt && parts.boltHandle) return composeClip(`clear_${fault}`, boltStroke(0, CLEAR_HOLD), fly(0.22), 0.6);
3226
+ if (family === "lever" && parts.lever) return composeClip(`clear_${fault}`, [turn("lever", "z", 0, 0.13, 0, LEVER_TURN), turn("lever", "z", 0.16, 0.13, LEVER_TURN, 0)], fly(0.1), 0.35);
3227
+ return void 0;
3228
+ }
3229
+ function clearClips(context) {
3230
+ return ["misfire", "stovepipe", "double_feed", "failure_to_extract"].map((fault) => {
3231
+ const manual = clearManual(context, fault);
3232
+ if (manual) return manual;
3233
+ const part = blowbackPart(context);
3234
+ if (context.parts[part] && ["pistol", "rifle", "semiint", "semi12", "p90", "belt"].includes(context.family)) return clearBlowback(context, fault);
3235
+ return composeClip(`clear_${fault}`, [], [cue(0, "fly_jam", { jam: fault })], 0.1);
3236
+ });
3237
+ }
3238
+
3239
+ // src/animation/reload-clips.ts
3240
+ import { RELOAD_STAGES } from "@adatechnology/game-weapons";
3241
+ var KINDS = ["tactical", "empty", "one_by_one"];
3242
+ var charge = (context, start) => {
3243
+ const travel = strokeOf(context.parts);
3244
+ return [move("bolt", "x", start, 0.12, 0, travel), move("bolt", "x", start + 0.18, 0.06, travel, 0)];
3245
+ };
3246
+ var cycleWithoutEject = (start) => ({ tweens: boltStroke(start), events: [], end: start + 0.55 });
3247
+ function magazineReload(context, kind) {
3248
+ const { parts, family } = context;
3249
+ const swap = magazineSwap(parts, 0);
3250
+ const draft = { tweens: [...swap.tweens], events: [...swap.events], end: swap.end };
3251
+ if (family === "pistol") {
3252
+ if (kind === "empty" && parts.slide) {
3253
+ draft.tweens.push(move("slide", "x", 0, 0, 0, strokeOf(parts)), move("slide", "x", swap.end, 0.05, strokeOf(parts), 0));
3254
+ draft.end += 0.12;
3255
+ }
3256
+ } else if (family === "boltmag") {
3257
+ const stroke = cycleWithoutEject(swap.end);
3258
+ draft.tweens.push(...stroke.tweens);
3259
+ draft.end = stroke.end;
3260
+ } else if (kind === "empty" && parts.bolt) {
3261
+ draft.tweens.push(...charge(context, swap.end));
3262
+ draft.end += 0.35;
3263
+ }
3264
+ return draft;
3265
+ }
3266
+ function revolverReload(kind) {
3267
+ const events = [cue(0.32, "eject_casing", { from: "chambers", down: false })];
3268
+ if (kind !== "one_by_one") events.push(cue(0.7, "move_in", { from: "chambers" }));
3269
+ return { tweens: [turn("crane", "x", 0, 0.22, 0, -1.25), turn("crane", "x", 1.05, 0.2, -1.25, 0)], events, end: 1.3 };
3270
+ }
3271
+ var breakReload = () => ({
3272
+ tweens: [turn("barrels", "z", 0, 0.2, 0, -0.5), turn("barrels", "z", 1, 0.15, -0.5, 0)],
3273
+ events: [cue(0.25, "eject_casing", { from: "breech", down: false }), cue(0.65, "move_in", { from: "breech" })],
3274
+ end: 1.2
3275
+ });
3276
+ var beltReload = (context) => ({
3277
+ tweens: [turn("cover", "z", 0, 0.2, 0, 0.9), move("belt", "z", 0.2, 0.25, 0, -0.3), move("belt", "z", 0.55, 0.3, -0.3, 0), turn("cover", "z", 0.9, 0.18, 0.9, 0), ...charge(context, 1.15)],
3278
+ events: [],
3279
+ end: 1.55
3280
+ });
3281
+ var UNIT = 1;
3282
+ function tubeReload(context, kind) {
3283
+ const { parts, family } = context;
3284
+ const draft = { tweens: [], events: [], end: UNIT };
3285
+ if (kind !== "empty") return draft;
3286
+ if (family === "pump" && parts.pump) draft.tweens.push(move("pump", "x", 0.88, 0.05, 0, PUMP_TRAVEL), move("pump", "x", 0.94, 0.05, PUMP_TRAVEL, 0));
3287
+ else if (family === "lever" && parts.lever) draft.tweens.push(turn("lever", "z", 0.87, 0.06, 0, LEVER_TURN), turn("lever", "z", 0.94, 0.06, LEVER_TURN, 0));
3288
+ else if (parts.bolt) draft.tweens.push(move("bolt", "x", 0.88, 0.07, 0, strokeOf(parts)), move("bolt", "x", 0.95, 0.04, strokeOf(parts), 0));
3289
+ return draft;
3290
+ }
3291
+ function boltReload(context) {
3292
+ if (!context.parts.boltHandle || !context.parts.bolt) return { tweens: [], events: [], end: UNIT };
3293
+ return {
3294
+ tweens: [
3295
+ turn("boltHandle", "x", 0, 0.05, 0, BOLT_TURN),
3296
+ move("boltHandle", "x", 0.05, 0.07, 0, BOLT_TRAVEL),
3297
+ move("bolt", "x", 0.05, 0.07, 0, BOLT_TRAVEL),
3298
+ move("boltHandle", "x", 0.86, 0.07, BOLT_TRAVEL, 0),
3299
+ move("bolt", "x", 0.86, 0.07, BOLT_TRAVEL, 0),
3300
+ turn("boltHandle", "x", 0.93, 0.05, BOLT_TURN, 0)
3301
+ ],
3302
+ events: [],
3303
+ end: UNIT
3304
+ };
3305
+ }
3306
+ function internalMagazineReload(context) {
3307
+ const travel = strokeOf(context.parts);
3308
+ return { tweens: context.parts.bolt ? [move("bolt", "x", 0, 0.04, 0, travel), move("bolt", "x", 0.89, 0.04, travel, 0)] : [], events: [], end: UNIT };
3309
+ }
3310
+ function heavyReload(context) {
3311
+ const { parts, family } = context;
3312
+ if (family === "flame") return { tweens: parts.gauge ? [turn("gauge", "z", 0, 1, -2.4, 0)] : [], events: [], end: 1.1 };
3313
+ if (parts.disposable) {
3314
+ return {
3315
+ tweens: [move(ROOT, "y", 0.6, 0, 0, -0.6), move(ROOT, "y", 0.6, 0.5, -0.6, 0)],
3316
+ events: [cue(0, "drop_tube"), cue(0.6, "seat_tube")],
3317
+ end: 1.15
3318
+ };
3319
+ }
3320
+ if (parts.round) {
3321
+ return {
3322
+ tweens: [move("round", "x", 0, 0.55, 0.45, 0), move("round", "y", 0, 0.55, 0.12, 0)],
3323
+ events: [cue(0, "show_round")],
3324
+ end: 0.6
3325
+ };
3326
+ }
3327
+ return {
3328
+ tweens: [turn("breech_0", "y", 0, 0.25, 0, -1.6), turn("breech_0", "y", 0.76, 0.22, -1.6, 0)],
3329
+ events: [cue(0.3, "breech_round")],
3330
+ end: 1.05
3331
+ };
3332
+ }
3333
+ var isTube = (context) => ["pump", "semi12", "lever"].includes(context.family);
3334
+ function draftFor(context, kind) {
3335
+ const { parts, family } = context;
3336
+ if (family === "rocket" || family === "flame") return heavyReload(context);
3337
+ if (parts.mag) return magazineReload(context, kind);
3338
+ if (family === "revolver" && parts.crane) return revolverReload(kind);
3339
+ if (family === "break" && parts.barrels) return breakReload();
3340
+ if (family === "belt" && parts.cover && parts.belt) return beltReload(context);
3341
+ if (isTube(context)) return tubeReload(context, kind);
3342
+ if (family === "bolt") return boltReload(context);
3343
+ if (family === "semiint") return internalMagazineReload(context);
3344
+ return void 0;
3345
+ }
3346
+ var markers = (context, kind, duration) => context.reloadFamily ? RELOAD_STAGES[context.reloadFamily][kind].map((entry) => stage(entry.stage, entry.at * duration)) : [];
3347
+ function reloadClips(context) {
3348
+ const clips = [];
3349
+ let first;
3350
+ for (const kind of KINDS) {
3351
+ const draft = draftFor(context, kind);
3352
+ if (!draft) return [];
3353
+ const clip = composeClip(kind === "tactical" ? "reload" : `reload_${kind}`, draft.tweens, [...draft.events, ...markers(context, kind, draft.end)], draft.end);
3354
+ if (!first) first = clip;
3355
+ else if (JSON.stringify({ ...clip, id: first.id }) === JSON.stringify(first)) continue;
3356
+ clips.push(clip);
3357
+ }
3358
+ return clips.length > 0 ? clips : first ? [renamed(first, "reload")] : [];
3359
+ }
3360
+ function feedClip(context) {
3361
+ const { family } = context;
3362
+ if (family === "revolver") return composeClip("feed", [], [cue(0, "move_in", { from: "chambers", next: true })], 0.24);
3363
+ if (family === "bolt" || family === "semiint") return composeClip("feed", [], [cue(0, "move_in", { from: "topLoad" })], 0.24);
3364
+ if (isTube(context)) return composeClip("feed", [], [cue(0, "move_in", { from: "loadPort" })], 0.24);
3365
+ return void 0;
3366
+ }
3367
+
3368
+ // src/animation/shot-clips.ts
3369
+ var FAULT_SLIDE_BACK = { stovepipe: 0.35, double_feed: 0.6 };
3370
+ var pullTrigger = (context) => context.parts.trigger ? [turn("trigger", "z", 0, 0.04, 0, 0.35), turn("trigger", "z", 0.06, 0.08, 0.35, 0)] : [];
3371
+ var recoil = (context) => {
3372
+ const kick = kickOf(context);
3373
+ return [
3374
+ move(ROOT, "x", 0, 0.03, 0, -0.014 * kick),
3375
+ turn(ROOT, "z", 0, 0.03, 0, 0.045 * kick),
3376
+ move(ROOT, "x", 0.03, 0.18, -0.014 * kick, 0),
3377
+ turn(ROOT, "z", 0.03, 0.18, 0.045 * kick, 0)
3378
+ ];
3379
+ };
3380
+ var hammerFall = (context) => context.parts.hammer && context.family !== "revolver" ? [turn("hammer", "z", 0, 0.02, 0, -0.5), turn("hammer", "z", 0.05, 0.06, -0.5, 0)] : [];
3381
+ function shotClip(id, context, locked) {
3382
+ const { parts, family } = context;
3383
+ const tweens = [...pullTrigger(context), ...recoil(context), ...hammerFall(context)];
3384
+ const events = [cue(0, "muzzle_flash")];
3385
+ if (BLOWBACK.has(family)) {
3386
+ const part = blowbackPart(context);
3387
+ const travel = strokeOf(parts);
3388
+ if (parts[part]) {
3389
+ tweens.push(move(part, "x", 0, 0.025, 0, travel));
3390
+ if (!locked) tweens.push(move(part, "x", 0.025, 0.045, travel, 0));
3391
+ }
3392
+ events.push(cue(0.012, "eject_casing", { down: family === "p90" || family === "belt" }));
3393
+ } else if (family === "revolver" && parts.cylinder && parts.hammer) {
3394
+ tweens.push(turn("cylinder", "x", 0, 0.05, 0, Math.PI / 3), turn("hammer", "z", 0, 0.05, 0, 0.5), turn("hammer", "z", 0.05, 0.03, 0.5, -0.5), turn("hammer", "z", 0.12, 0.08, -0.5, 0));
3395
+ }
3396
+ return composeClip(id, tweens, events);
3397
+ }
3398
+ function shotClips(context) {
3399
+ const clips = [shotClip("shot", context, false)];
3400
+ if (context.family === "pistol") clips.push(shotClip("shot_last", context, true));
3401
+ return clips;
3402
+ }
3403
+ var dudTweens = (context) => [...pullTrigger(context), ...context.parts.hammer ? [turn("hammer", "z", 0, 0.03, 0, -0.5)] : []];
3404
+ function misfireClip(context) {
3405
+ const tweens = dudTweens(context);
3406
+ if (context.family === "revolver" && context.parts.cylinder) tweens.push(turn("cylinder", "x", 0, 0.07, 0, Math.PI / 3));
3407
+ return composeClip("misfire", tweens);
3408
+ }
3409
+ var emptyClip = (context) => composeClip("empty", pullTrigger(context));
3410
+ function faultClips(context) {
3411
+ const { parts, family } = context;
3412
+ const misfire = composeClip("fault_misfire", dudTweens(context));
3413
+ const part = blowbackPart(context);
3414
+ if (!BLOWBACK.has(family) || !parts[part]) return [misfire];
3415
+ const travel = strokeOf(parts);
3416
+ const stopped = Object.entries(FAULT_SLIDE_BACK).map(
3417
+ ([fault, back]) => composeClip(`fault_${fault}`, [move(part, "x", 0, 0.025, 0, travel), move(part, "x", 0.025, 0.04, travel, travel * back)], fault === "stovepipe" ? [cue(0.03, "stuck_case")] : [], 0.25)
3418
+ );
3419
+ return [misfire, ...stopped];
3420
+ }
3421
+ function safetyClips(context) {
3422
+ if (!context.parts.safety) return [];
3423
+ return [composeClip("safety_on", [turn("safety", "z", 0, 0.12, 0, -0.6)]), composeClip("safety_off", [turn("safety", "z", 0, 0.12, -0.6, 0)]), composeClip("mode", [turn("safety", "z", 0, 0.07, 0, 0.4), turn("safety", "z", 0.07, 0.07, 0.4, 0)])];
3424
+ }
3425
+ var drawClip = () => composeClip("draw", [move(ROOT, "y", 0, 0.35, -0.1, 0), turn(ROOT, "z", 0, 0.35, -0.6, 0)]);
3426
+
3427
+ // src/animation/clip-set.ts
3428
+ function buildToolClips(context) {
3429
+ const clips = [
3430
+ ...shotClips(context),
3431
+ misfireClip(context),
3432
+ emptyClip(context),
3433
+ cycleClip(context),
3434
+ ...reloadClips(context),
3435
+ feedClip(context),
3436
+ ...faultClips(context),
3437
+ ...clearClips(context),
3438
+ ...safetyClips(context),
3439
+ drawClip()
3440
+ ];
3441
+ const set = {};
3442
+ for (const clip of clips) if (clip) set[clip.id] = clip;
3443
+ return set;
3444
+ }
3445
+
3446
+ // src/audio/host-events.ts
3447
+ var weaponEventOf = (event) => {
3448
+ switch (event.type) {
3449
+ case "misfire":
3450
+ return { type: "misfire", at: 0 };
3451
+ case "stage":
3452
+ return { type: "reload_stage", stage: event.stage, at: 0 };
3453
+ case "empty":
3454
+ case "feed":
3455
+ return { type: event.type, at: 0 };
3456
+ case "cycle":
3457
+ return { type: "cycle", phase: event.phase, at: 0 };
3458
+ case "mode":
3459
+ return { type: "mode", mode: event.mode, at: 0 };
3460
+ case "fault":
3461
+ return { type: "fault", fault: event.fault, at: 0 };
3462
+ case "clear":
3463
+ return { type: "clear", fault: event.fault, phase: event.phase, at: 0 };
3464
+ case "safety":
3465
+ return { type: "safety", on: event.on, at: 0 };
3466
+ default:
3467
+ return void 0;
3468
+ }
3469
+ };
3470
+
3471
+ // src/models/condition.ts
3472
+ var substitutions = (kit, condition) => {
3473
+ const { M } = kit;
3474
+ if (condition === "rusty")
3475
+ return /* @__PURE__ */ new Map([
3476
+ [M.metal, M.rust],
3477
+ [M.dark, M.rustD],
3478
+ [M.steel, M.rust],
3479
+ [M.steelL, M.rust]
3480
+ ]);
3481
+ if (condition === "worn")
3482
+ return /* @__PURE__ */ new Map([
3483
+ [M.metal, M.worn],
3484
+ [M.dark, M.worn]
3485
+ ]);
3486
+ return null;
3487
+ };
3488
+ function applyCondition(kit, model, condition) {
3489
+ const replacement = substitutions(kit, condition);
3490
+ const glowing = new Set(model.parts.glow);
3491
+ const paint = condition === "rusty" ? kit.M.rust.color : condition === "worn" ? kit.M.worn.color : void 0;
3492
+ model.root.traverse((object) => {
3493
+ var _a, _b;
3494
+ const mesh = object;
3495
+ if (!mesh.isMesh) return;
3496
+ (_a = mesh.userData).baseMaterial ?? (_a.baseMaterial = mesh.material);
3497
+ const base = mesh.userData.baseMaterial;
3498
+ if (glowing.has(mesh)) {
3499
+ const material = mesh.material;
3500
+ (_b = mesh.userData).baseColor ?? (_b.baseColor = material.color.clone());
3501
+ material.color.copy(paint ?? mesh.userData.baseColor);
3502
+ return;
3503
+ }
3504
+ mesh.material = replacement?.get(base) ?? base;
3505
+ });
3506
+ }
3507
+
3508
+ // src/models/cartridge.ts
3509
+ var BOTTLENECK_RATIO = 0.8;
3510
+ function cartridgeShape(caliber, dims) {
3511
+ if (caliber.caseType === "shell" || caliber.rim === "shell") return "shell";
3512
+ if (caliber.rim === "rimfire") return "rim";
3513
+ const isRimmed = caliber.rim === "rimmed";
3514
+ const isBottleneck = dims.projectileDiameter / dims.caseDiameter < BOTTLENECK_RATIO;
3515
+ if (isBottleneck) return isRimmed ? "rr" : "r";
3516
+ return isRimmed ? "rimp" : "p";
3517
+ }
3518
+ function buildCartridge(kit, dims, shape) {
3519
+ const { M, cylinderY } = kit;
3520
+ const group = kit.group();
3521
+ const radius = dims.caseDiameter / 2;
3522
+ const bulletRadius = dims.projectileDiameter / 2;
3523
+ const caseLength = dims.caseLength;
3524
+ if (shape === "shell") {
3525
+ cylinderY(group, radius, caseLength * 0.8, M.shell, 0, caseLength * 0.6, 0, 14);
3526
+ cylinderY(group, radius * 1.03, caseLength * 0.22, M.brass, 0, caseLength * 0.11, 0, 14);
3527
+ cylinderY(group, radius * 1.08, 1.2, M.brass, 0, 0.6, 0, 14);
3528
+ return group;
3529
+ }
3530
+ if (shape === "rim" || shape === "rimp" || shape === "rr") cylinderY(group, radius * 1.15, 1.2, M.brass, 0, 0.6, 0, 14);
3531
+ const extra = dims.overallLength - caseLength;
3532
+ if (shape === "r" || shape === "rr") {
3533
+ const bodyHeight = caseLength * 0.78;
3534
+ const shoulder = caseLength * 0.08;
3535
+ const neck = caseLength - bodyHeight - shoulder;
3536
+ const neckRadius = bulletRadius + 0.35;
3537
+ cylinderY(group, radius, bodyHeight, M.brass, 0, bodyHeight / 2, 0, 14);
3538
+ cylinderY(group, neckRadius, shoulder, M.brass, 0, bodyHeight + shoulder / 2, 0, 14, radius);
3539
+ cylinderY(group, neckRadius, neck, M.brass, 0, bodyHeight + shoulder + neck / 2, 0, 14);
3540
+ cylinderY(group, bulletRadius, extra * 0.3, M.copper, 0, caseLength + extra * 0.15, 0, 14);
3541
+ cylinderY(group, bulletRadius, extra * 0.7, M.copper, 0, caseLength + extra * 0.3 + extra * 0.35, 0, 14, bulletRadius * 0.12);
3542
+ return group;
3543
+ }
3544
+ const bullet = shape === "rim" || shape === "rimp" ? M.lead : M.copper;
3545
+ cylinderY(group, radius, caseLength, M.brass, 0, caseLength / 2, 0, 14);
3546
+ cylinderY(group, bulletRadius, extra * 0.45, bullet, 0, caseLength + extra * 0.225, 0, 14);
3547
+ cylinderY(group, bulletRadius, extra * 0.55, bullet, 0, caseLength + extra * 0.45 + extra * 0.275, 0, 14, bulletRadius * 0.45);
3548
+ return group;
3549
+ }
3550
+ function buildCasing(kit, dims, shape) {
3551
+ const { M, cylinderX } = kit;
3552
+ const group = kit.group();
3553
+ const radius = dims.caseDiameter / 2e3;
3554
+ const length = dims.caseLength / 1e3;
3555
+ if (shape === "shell") {
3556
+ const scale = length / 0.07;
3557
+ cylinderX(group, 0.0105 * scale, 0.05 * scale, M.shell, 8e-3 * scale, 0, 0, 14);
3558
+ cylinderX(group, 0.011 * scale, 0.016 * scale, M.brass, -0.025 * scale, 0, 0, 14);
3559
+ return { group, radius: 0.0105 * scale, length };
3560
+ }
3561
+ if (shape === "r" || shape === "rr") {
3562
+ cylinderX(group, radius, length * 0.75, M.brass, -length * 0.12, 0, 0, 12);
3563
+ cylinderX(group, radius * 0.62, length * 0.25, M.brass, length * 0.37, 0, 0, 12, radius * 0.55);
3564
+ } else cylinderX(group, radius, length, M.brass, 0, 0, 0, 12);
3565
+ return { group, radius, length };
3566
+ }
3567
+ function buildRound(kit, dims, shape) {
3568
+ const group = kit.group();
3569
+ const cartridge = buildCartridge(kit, dims, shape);
3570
+ cartridge.scale.setScalar(1e-3);
3571
+ cartridge.rotation.z = -Math.PI / 2;
3572
+ group.add(cartridge);
3573
+ return { group, radius: dims.caseDiameter / 2e3 };
3574
+ }
3575
+
3576
+ // src/fx/effects.ts
3577
+ var PI9 = Math.PI;
3578
+ var FLASH_BY_CASE = { shell: 1.6, big: 2.6, tiny: 0.5, pistol: 0.8, rifle: 1.2 };
3579
+ var FLASH_SECONDS = 0.055;
3580
+ function createEffects(kit, scene, particles, random) {
3581
+ const { THREE, M } = kit;
3582
+ const light = new THREE.PointLight(16756832, 0, 3, 2);
3583
+ scene.add(light);
3584
+ let flash = null;
3585
+ const buildFlash = (size) => {
3586
+ const group = new THREE.Group();
3587
+ const cone = new THREE.Mesh(new THREE.ConeGeometry(0.013 * size, 0.085 * size, 10, 1, true), M.flashA);
3588
+ cone.rotation.z = -PI9 / 2;
3589
+ cone.position.x = 0.042 * size;
3590
+ group.add(cone);
3591
+ for (let i = 0; i < 4; i++) {
3592
+ const angle = i * PI9 / 2 + random() * 0.6;
3593
+ const spike = new THREE.Mesh(new THREE.ConeGeometry(5e-3 * size, 0.04 * size, 6), M.flashA);
3594
+ spike.position.set(0.012 * size, Math.cos(angle) * 0.016 * size, Math.sin(angle) * 0.016 * size);
3595
+ spike.rotation.set(angle, 0, 0);
3596
+ spike.rotateZ(-PI9 / 2);
3597
+ group.add(spike);
3598
+ }
3599
+ const core = new THREE.Mesh(new THREE.SphereGeometry(0.016 * size, 12, 8), M.flashB);
3600
+ core.position.x = 8e-3 * size;
3601
+ group.add(core);
3602
+ return group;
3603
+ };
3604
+ const endFlash = () => {
3605
+ if (!flash) return;
3606
+ flash.group.removeFromParent();
3607
+ flash.group.traverse((child) => {
3608
+ const mesh = child;
3609
+ if (mesh.isMesh) mesh.geometry.dispose();
3610
+ });
3611
+ flash = null;
3612
+ light.intensity = 0;
3613
+ };
3614
+ const effects = {
3615
+ muzzleFlash(position, orientation, size) {
3616
+ endFlash();
3617
+ const group = buildFlash(size);
3618
+ group.position.copy(position);
3619
+ group.quaternion.copy(orientation);
3620
+ group.rotateX(random() * PI9);
3621
+ scene.add(group);
3622
+ flash = { group, age: 0 };
3623
+ light.position.copy(position);
3624
+ light.intensity = 4 * size;
3625
+ light.distance = 2 * size;
3626
+ for (let i = 0; i < 6; i++) {
3627
+ const velocity = new THREE.Vector3(0.15 + random() * 0.35, 0.03 + random() * 0.12, (random() - 0.5) * 0.14).applyQuaternion(orientation);
3628
+ effects.smoke(position, velocity, 0.013 * size, 1.3 + random() * 0.6, 0.45);
3629
+ }
3630
+ },
3631
+ smoke(position, velocity, size, life, opacity) {
3632
+ const puff = new THREE.Mesh(new THREE.SphereGeometry(size, 10, 8), M.smokeP.clone());
3633
+ puff.position.copy(position);
3634
+ particles.spawn(puff, velocity, { gravity: -0.08, life, fadeFrom: opacity, grow: 2.1, drag: 2.2, radius: -1, ownsGeometry: true });
3635
+ },
3636
+ update(dt) {
3637
+ if (!flash) return;
3638
+ flash.age += dt;
3639
+ flash.group.scale.setScalar(0.6 + Math.min(1, flash.age / FLASH_SECONDS) * 0.9);
3640
+ if (flash.age >= FLASH_SECONDS) endFlash();
3641
+ },
3642
+ dispose() {
3643
+ endFlash();
3644
+ light.removeFromParent();
3645
+ light.dispose();
3646
+ }
3647
+ };
3648
+ return effects;
3649
+ }
3650
+
3651
+ // src/fx/heavy.ts
3652
+ var PI10 = Math.PI;
3653
+ var flameMaterials = (kit) => {
3654
+ const { THREE } = kit;
3655
+ return [
3656
+ new THREE.MeshBasicMaterial({ color: 16742946, transparent: true, opacity: 0.9, depthWrite: false }),
3657
+ new THREE.MeshBasicMaterial({ color: 16760896, transparent: true, opacity: 0.9, depthWrite: false }),
3658
+ new THREE.MeshBasicMaterial({ color: 13121050, transparent: true, opacity: 0.8, depthWrite: false }),
3659
+ new THREE.MeshBasicMaterial({ color: 16773296, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false })
3660
+ ];
3661
+ };
3662
+ var spread = (context, x, y, z) => new context.kit.THREE.Vector3((context.random() - 0.5) * x, (context.random() - 0.5) * y, (context.random() - 0.5) * z);
3663
+ function createBits(context) {
3664
+ const { kit, particles, random } = context;
3665
+ const { THREE } = kit;
3666
+ const palette = flameMaterials(kit);
3667
+ return {
3668
+ flame(position, velocity, size, life) {
3669
+ const bit = new THREE.Mesh(new THREE.IcosahedronGeometry(size, 1), palette[Math.floor(random() * 4)].clone());
3670
+ bit.position.copy(position);
3671
+ bit.rotation.set(random() * 3, random() * 3, 0);
3672
+ particles.spawn(bit, velocity, { gravity: -1.4, life, fadeFrom: 0.9, grow: 2.2, drag: 1.4, radius: -1, spin: new THREE.Vector3(3, 4, 2), ownsGeometry: true });
3673
+ },
3674
+ smoke(position, velocity, size, life, opacity) {
3675
+ context.effects.smoke(position, velocity, size, life, opacity);
3676
+ },
3677
+ dispose: () => palette.forEach((material) => material.dispose())
3678
+ };
3679
+ }
3680
+ function launchRocket(context) {
3681
+ const { kit, scene, parts, random } = context;
3682
+ const { THREE } = kit;
3683
+ const bits = createBits(context);
3684
+ const forward = context.forward();
3685
+ const muzzle = context.positionOf(parts.muzzle);
3686
+ const rear = context.positionOf(parts.rear);
3687
+ for (let i = 0; i < 26; i++) bits.smoke(rear.clone(), forward.clone().multiplyScalar(-(1.5 + random() * 3)).add(spread(context, 1.4, 0.8, 1.4)), 0.03 + random() * 0.03, 1.8 + random() * 1.4, 0.5);
3688
+ for (let i = 0; i < 14; i++) bits.flame(rear.clone(), forward.clone().multiplyScalar(-(2 + random() * 2)).add(spread(context, 1, 0.6, 1)), 0.02, 0.25);
3689
+ const rocket = parts.makeRound ? parts.makeRound() : buildFlyingRocket(kit, parts.launcherType);
3690
+ if (parts.round) parts.round.visible = false;
3691
+ rocket.quaternion.copy(context.orientation());
3692
+ rocket.position.copy(muzzle);
3693
+ scene.add(rocket);
3694
+ const exhaust = new THREE.Mesh(new THREE.ConeGeometry(0.018, 0.12, 8, 1, true), new THREE.MeshBasicMaterial({ color: 16760896, transparent: true, opacity: 0.9, depthWrite: false }));
3695
+ exhaust.rotation.z = PI10 / 2;
3696
+ exhaust.position.x = -0.08;
3697
+ rocket.add(exhaust);
3698
+ const light = new THREE.PointLight(16751168, 3, 2.5);
3699
+ rocket.add(light);
3700
+ context.every(
3701
+ 1.6,
3702
+ (progress, dt) => {
3703
+ const flight = progress * 1.6;
3704
+ const distance = 1.2 * flight + (flight > 0.25 ? 9 * (flight - 0.25) ** 2 : 0);
3705
+ rocket.position.copy(muzzle).addScaledVector(forward, distance);
3706
+ rocket.rotation.x += 0.25 * dt * 60;
3707
+ exhaust.scale.set(1, 0.6 + random() * 0.8, 1);
3708
+ if (random() < 0.9) bits.smoke(rocket.position.clone().addScaledVector(forward, -0.12), spread(context, 0.1, 0, 0.1).setY(0.05), 0.02 + random() * 0.015, 2.2, 0.5);
3709
+ },
3710
+ () => {
3711
+ rocket.removeFromParent();
3712
+ exhaust.geometry.dispose();
3713
+ exhaust.material.dispose();
3714
+ light.dispose();
3715
+ bits.dispose();
3716
+ }
3717
+ );
3718
+ }
3719
+ function flameJet(context, dt, scale) {
3720
+ const { kit, particles, random, parts } = context;
3721
+ const { THREE } = kit;
3722
+ const bits = createBits(context);
3723
+ const forward = context.forward();
3724
+ const muzzle = context.positionOf(parts.muzzle);
3725
+ const count = Math.floor(dt * 420 * scale);
3726
+ for (let i = 0; i < count; i++) {
3727
+ const speed = (2.2 + random() * 1.1) * scale;
3728
+ bits.flame(muzzle.clone().addScaledVector(forward, random() * speed * Math.min(dt, 0.3)), forward.clone().multiplyScalar(speed).add(new THREE.Vector3((random() - 0.5) * 0.35, (random() - 0.3) * 0.3, (random() - 0.5) * 0.35)), 0.02 + random() * 0.022, 0.4 + random() * 0.35);
3729
+ }
3730
+ if (random() < 0.5) bits.smoke(muzzle.clone().addScaledVector(forward, 1.1 * scale + random() * 0.4), new THREE.Vector3(forward.x * 0.6, 0.35, forward.z * 0.6), 0.03, 1.6, 0.4);
3731
+ if (random() < 0.18) {
3732
+ const droplet = new THREE.Mesh(new THREE.SphereGeometry(6e-3, 6, 4), new THREE.MeshBasicMaterial({ color: 16742946 }));
3733
+ droplet.position.copy(muzzle).addScaledVector(forward, 0.3 + random() * 0.5);
3734
+ particles.spawn(droplet, forward.clone().multiplyScalar(1.2 * scale).add(new THREE.Vector3(0, 0.2, 0)), { life: 3, radius: 3e-3, ownsGeometry: true, onGround: (position) => groundFire(context, bits, position) });
3735
+ }
3736
+ bits.dispose();
3737
+ }
3738
+ function groundFire(context, bits, position) {
3739
+ const { kit, scene, random } = context;
3740
+ const { THREE } = kit;
3741
+ const fire = new THREE.Group();
3742
+ fire.position.set(position.x, context.groundAt(position.x, position.z) + 5e-3, position.z);
3743
+ scene.add(fire);
3744
+ const materials = flameMaterials(kit);
3745
+ const flames = [0, 1, 2].map((index) => {
3746
+ const flame = new THREE.Mesh(new THREE.ConeGeometry(0.02, 0.07, 6), materials[index % 2].clone());
3747
+ flame.position.set((random() - 0.5) * 0.04, 0.035, (random() - 0.5) * 0.04);
3748
+ fire.add(flame);
3749
+ return flame;
3750
+ });
3751
+ let clock = 0;
3752
+ context.every(
3753
+ 4,
3754
+ (progress, dt) => {
3755
+ clock += dt;
3756
+ flames.forEach((flame, index) => flame.scale.set(1 - progress * 0.7, (0.6 + Math.sin(clock * 20 + index * 2) * 0.35) * (1 - progress), 1 - progress * 0.7));
3757
+ if (random() < 0.08) bits.smoke(fire.position.clone().add(new THREE.Vector3(0, 0.08, 0)), new THREE.Vector3(0, 0.15, 0), 0.015, 1.2, 0.25);
3758
+ },
3759
+ () => {
3760
+ fire.removeFromParent();
3761
+ flames.forEach((flame) => {
3762
+ flame.geometry.dispose();
3763
+ flame.material.dispose();
3764
+ });
3765
+ materials.forEach((material) => material.dispose());
3766
+ }
3767
+ );
3768
+ }
3769
+
3770
+ // src/fx/particles.ts
3771
+ var MAX_PARTICLES = 900;
3772
+ var FLOOR_FRICTION = 0.45;
3773
+ var disposeOwned = (object) => {
3774
+ object.traverse((child) => {
3775
+ const mesh = child;
3776
+ if (mesh.isMesh) mesh.geometry.dispose();
3777
+ });
3778
+ };
3779
+ function createParticles(THREE, scene, groundAt) {
3780
+ let list = [];
3781
+ const remove = (particle) => {
3782
+ particle.object.removeFromParent();
3783
+ if (particle.ownsGeometry) disposeOwned(particle.object);
3784
+ if (particle.fadeFrom !== void 0) particle.object.material.dispose();
3785
+ list = list.filter((other) => other !== particle);
3786
+ };
3787
+ const api = {
3788
+ spawn(object, velocity, options = {}) {
3789
+ scene.add(object);
3790
+ list.push({
3791
+ object,
3792
+ velocity,
3793
+ spin: options.spin ?? new THREE.Vector3(),
3794
+ age: 0,
3795
+ resting: false,
3796
+ life: options.life ?? 2,
3797
+ gravity: options.gravity ?? 9.8,
3798
+ bounce: options.bounce ?? 0.3,
3799
+ drag: options.drag ?? 0,
3800
+ radius: options.radius ?? 3e-3,
3801
+ grow: options.grow ?? 0,
3802
+ kind: options.kind ?? "other",
3803
+ fadeFrom: options.fadeFrom,
3804
+ onRest: options.onRest,
3805
+ onGround: options.onGround,
3806
+ ownsGeometry: options.ownsGeometry ?? false,
3807
+ bounces: 0
3808
+ });
3809
+ const oldest = list[0];
3810
+ if (list.length > MAX_PARTICLES && oldest) remove(oldest);
3811
+ },
3812
+ update(dt) {
3813
+ for (const particle of [...list]) {
3814
+ particle.age += dt;
3815
+ if (particle.age > particle.life) {
3816
+ remove(particle);
3817
+ continue;
3818
+ }
3819
+ const { object } = particle;
3820
+ if (particle.fadeFrom !== void 0) object.material.opacity = Math.max(0, particle.fadeFrom * (1 - particle.age / particle.life));
3821
+ if (particle.resting) continue;
3822
+ particle.velocity.y -= particle.gravity * dt;
3823
+ if (particle.drag) particle.velocity.multiplyScalar(Math.max(0, 1 - particle.drag * dt));
3824
+ object.position.addScaledVector(particle.velocity, dt);
3825
+ object.rotation.x += particle.spin.x * dt;
3826
+ object.rotation.y += particle.spin.y * dt;
3827
+ object.rotation.z += particle.spin.z * dt;
3828
+ if (particle.grow) object.scale.multiplyScalar(1 + particle.grow * dt);
3829
+ if (particle.radius < 0) continue;
3830
+ const floor = groundAt(object.position.x, object.position.z) + particle.radius;
3831
+ if (object.position.y >= floor || particle.velocity.y >= 0) continue;
3832
+ if (particle.onGround) {
3833
+ particle.onGround(object.position.clone());
3834
+ remove(particle);
3835
+ continue;
3836
+ }
3837
+ object.position.y = floor;
3838
+ particle.velocity.y *= -particle.bounce;
3839
+ particle.velocity.x *= FLOOR_FRICTION;
3840
+ particle.velocity.z *= FLOOR_FRICTION;
3841
+ particle.spin.multiplyScalar(FLOOR_FRICTION);
3842
+ if (particle.bounces++ < 4 && Math.abs(particle.velocity.y) >= 0.18) api.onBounce?.(particle.kind);
3843
+ if (Math.abs(particle.velocity.y) < 0.18) {
3844
+ particle.resting = true;
3845
+ particle.onRest?.(object);
3846
+ }
3847
+ }
3848
+ },
3849
+ count: (kind) => list.filter((particle) => particle.kind === kind).length,
3850
+ dispose() {
3851
+ for (const particle of [...list]) remove(particle);
3852
+ }
3853
+ };
3854
+ return api;
3855
+ }
3856
+
3857
+ // src/fx/rig.ts
3858
+ function createModelRig(model) {
3859
+ const registry = partRegistry(model.parts);
3860
+ const rest = /* @__PURE__ */ new Map();
3861
+ for (const part of [...registry.values(), model.root]) rest.set(part, { position: part.position.clone(), rotation: part.rotation.clone() });
3862
+ const apply = (object, pose) => {
3863
+ const base = rest.get(object);
3864
+ if (!base) return;
3865
+ const [dx, dy, dz] = pose.position ?? [0, 0, 0];
3866
+ const [rx, ry, rz] = pose.rotation ?? [0, 0, 0];
3867
+ object.position.set(base.position.x + dx, base.position.y + dy, base.position.z + dz);
3868
+ object.rotation.set(base.rotation.x + rx, base.rotation.y + ry, base.rotation.z + rz);
3869
+ };
3870
+ return {
3871
+ setPartPose(partId, pose) {
3872
+ const part = registry.get(partId);
3873
+ if (part) apply(part, pose);
3874
+ },
3875
+ setRootPose: (pose) => apply(model.root, pose)
3876
+ };
3877
+ }
3878
+
3879
+ // src/fx/session.ts
3880
+ var worldPositionOf = (session, part, out) => {
3881
+ session.model.root.updateWorldMatrix(true, true);
3882
+ return part.getWorldPosition(out);
3883
+ };
3884
+ var toWorld = (session, vector, scaled = false) => {
3885
+ const quaternion = session.model.root.getWorldQuaternion(new session.kit.THREE.Quaternion());
3886
+ const world = vector.clone().applyQuaternion(quaternion);
3887
+ return scaled ? world.multiplyScalar(session.holderScale) : world;
3888
+ };
3889
+
3890
+ // src/fx/viewmodel-events.ts
3891
+ var PI11 = Math.PI;
3892
+ var restFlat = (object) => {
3893
+ object.rotation.x = 0;
3894
+ object.rotation.z = 0;
3895
+ };
3896
+ function createClipEventHandler(world, session) {
3897
+ const { kit, particles, effects, random } = world;
3898
+ const { THREE } = kit;
3899
+ const { parts, root } = session.model;
3900
+ const vector = (x, y, z) => new THREE.Vector3(x, y, z);
3901
+ const positionOf = (part) => worldPositionOf(session, part, new THREE.Vector3());
3902
+ const reach = Math.max(0.55, Math.min(1.2, session.lengthMetres));
3903
+ const throwCase = (mark, velocity, spin, tilt = 0) => {
3904
+ if (!session.cartridge || !world.layers.casings) return;
3905
+ const casing = buildCasing(kit, session.cartridge.dims, session.cartridge.shape);
3906
+ casing.group.position.copy(positionOf(mark));
3907
+ casing.group.rotation.set(0, random() * 0.6 - 0.3, tilt);
3908
+ particles.spawn(casing.group, velocity, { kind: "casing", life: 25, radius: casing.radius, bounce: 0.35, spin, onRest: restFlat, ownsGeometry: true });
3909
+ };
3910
+ const ejectFromPort = (down) => {
3911
+ if (!parts.eject) return;
3912
+ const local = down ? vector((random() - 0.5) * 0.3, -0.4, (random() - 0.5) * 0.3) : vector(-0.2 - random() * 0.4, (0.9 + random() * 0.5) * reach, (1 + random() * 0.6) * reach);
3913
+ throwCase(parts.eject, toWorld(session, local), vector(random() * 30, random() * 30, random() * 30));
3914
+ };
3915
+ const ejectAll = (marks, spent) => {
3916
+ marks?.forEach((mark, index) => {
3917
+ const { velocity, spin, tilt } = spent(index);
3918
+ throwCase(mark, toWorld(session, velocity), spin, tilt);
3919
+ });
3920
+ };
3921
+ const moveIn = (from, to, tilt) => {
3922
+ if (!session.cartridge) return;
3923
+ const { group } = buildRound(kit, session.cartridge.dims, session.cartridge.shape);
3924
+ const start = root.worldToLocal(from.clone());
3925
+ const end = root.worldToLocal(to.clone());
3926
+ group.rotation.z = tilt;
3927
+ group.position.copy(start);
3928
+ root.add(group);
3929
+ world.schedule(0, 0.24, (eased) => group.position.lerpVectors(start, end, eased), () => {
3930
+ group.removeFromParent();
3931
+ group.traverse((child) => {
3932
+ const mesh = child;
3933
+ if (mesh.isMesh) mesh.geometry.dispose();
3934
+ });
3935
+ });
3936
+ };
3937
+ const offsetFrom = (point, x, y, z) => point.clone().add(toWorld(session, vector(x, y, z), true));
3938
+ const feedRound = (from, next) => {
3939
+ if (from === "loadPort" && parts.loadPort) {
3940
+ const port = positionOf(parts.loadPort);
3941
+ moveIn(offsetFrom(port, -0.03, -0.08, 0.07), port, 0.25);
3942
+ } else if (from === "topLoad" && parts.topLoad) {
3943
+ const point = positionOf(parts.topLoad);
3944
+ moveIn(offsetFrom(point, 0, 0.07, 0), point, 0);
3945
+ } else if (from === "chambers" && parts.chambers) {
3946
+ const marks = next ? [parts.chambers[session.nextChamber++ % parts.chambers.length]] : parts.chambers;
3947
+ for (const mark of marks) {
3948
+ if (!mark) continue;
3949
+ const point = positionOf(mark);
3950
+ moveIn(offsetFrom(point, -0.07, 0, 0), offsetFrom(point, 6e-3, 0, 0), 0);
3951
+ }
3952
+ } else if (from === "breech" && parts.breech) {
3953
+ for (const mark of parts.breech) {
3954
+ const point = positionOf(mark);
3955
+ moveIn(offsetFrom(point, -0.09, 0.05, 0), offsetFrom(point, 0.02, -0.01, 0), -0.5);
3956
+ }
3957
+ }
3958
+ };
3959
+ const dropMagazine = (up) => {
3960
+ if (!parts.mag || !world.layers.magazines) return;
3961
+ root.updateWorldMatrix(true, true);
3962
+ const copy = parts.mag.clone(true);
3963
+ parts.mag.matrixWorld.decompose(copy.position, copy.quaternion, copy.scale);
3964
+ particles.spawn(copy, toWorld(session, vector(up ? -0.25 : 0.05, up ? 0.9 : -0.3, 0.18)), { kind: "magazine", life: 25, radius: 0.014, bounce: 0.15, spin: vector(1.5, 0.5, 2.5), onRest: (object) => object.rotation.set(0, object.rotation.y, PI11 / 2) });
3965
+ };
3966
+ const stuckCase = () => {
3967
+ if (!session.cartridge || !parts.eject) return;
3968
+ const casing = buildCasing(kit, session.cartridge.dims, session.cartridge.shape);
3969
+ const at = root.worldToLocal(offsetFrom(positionOf(parts.eject), 0, casing.length * 0.35, 0));
3970
+ casing.group.position.copy(at);
3971
+ casing.group.rotation.set(0, 0, PI11 / 2 - 0.25);
3972
+ root.add(casing.group);
3973
+ session.stuckCase = casing.group;
3974
+ };
3975
+ const flyJam = (jam) => {
3976
+ const stuck = session.stuckCase;
3977
+ session.stuckCase = null;
3978
+ if (stuck) {
3979
+ root.updateWorldMatrix(true, true);
3980
+ stuck.updateWorldMatrix(true, false);
3981
+ stuck.matrixWorld.decompose(stuck.position, stuck.quaternion, stuck.scale);
3982
+ particles.spawn(stuck, toWorld(session, vector(-0.2, 1.2, 1.3)), { kind: "casing", life: 25, radius: 6e-3, bounce: 0.3, spin: vector(9, 14, 7), onRest: restFlat, ownsGeometry: true });
3983
+ } else if (jam === "misfire" && session.cartridge && parts.eject) {
3984
+ const round2 = buildRound(kit, session.cartridge.dims, session.cartridge.shape);
3985
+ round2.group.position.copy(positionOf(parts.eject));
3986
+ particles.spawn(round2.group, toWorld(session, vector(-0.2, 1.1, 1.2)), { kind: "round", life: 25, radius: round2.radius, bounce: 0.3, spin: vector(8, 12, 6), onRest: restFlat, ownsGeometry: true });
3987
+ } else ejectFromPort(false);
3988
+ };
3989
+ return (event) => {
3990
+ const data = event.data ?? {};
3991
+ switch (event.type) {
3992
+ case "muzzle_flash":
3993
+ if (parts.muzzle && world.layers.muzzleFlash) effects.muzzleFlash(positionOf(parts.muzzle), root.getWorldQuaternion(new THREE.Quaternion()), session.def.action.kind === "launcher" ? 2.2 : FLASH_BY_CASE[session.caseType]);
3994
+ break;
3995
+ case "eject_casing":
3996
+ if (data.from === "chambers") ejectAll(parts.chambers, () => ({ velocity: vector(-0.5 - random() * 0.3, -0.1 + random() * 0.3, (random() - 0.5) * 0.25), spin: vector(10, 20, 5), tilt: 0 }));
3997
+ else if (data.from === "breech") ejectAll(parts.breech, () => ({ velocity: vector(-1, 0.9, (random() - 0.5) * 0.2), spin: vector(3, 8, 12), tilt: -0.5 }));
3998
+ else ejectFromPort(data.down === true);
3999
+ break;
4000
+ case "drop_magazine":
4001
+ dropMagazine(data.up === true);
4002
+ break;
4003
+ case "move_in":
4004
+ feedRound(String(data.from), data.next === true);
4005
+ break;
4006
+ case "stuck_case":
4007
+ stuckCase();
4008
+ break;
4009
+ case "fly_jam":
4010
+ flyJam(String(data.jam));
4011
+ break;
4012
+ case "show_round":
4013
+ if (parts.round) parts.round.visible = true;
4014
+ break;
4015
+ case "drop_tube":
4016
+ dropTube();
4017
+ break;
4018
+ case "seat_tube":
4019
+ root.visible = true;
4020
+ break;
4021
+ case "breech_round":
4022
+ breechRound();
4023
+ break;
4024
+ default:
4025
+ break;
4026
+ }
4027
+ };
4028
+ function dropTube() {
4029
+ root.updateWorldMatrix(true, true);
4030
+ const old = root.clone(true);
4031
+ root.matrixWorld.decompose(old.position, old.quaternion, old.scale);
4032
+ root.visible = false;
4033
+ particles.spawn(old, toWorld(session, vector(0.2, 0.4, 0.6)), { life: 8, radius: 0.05, bounce: 0.2, spin: vector(0, 0.8, 2.5) });
4034
+ }
4035
+ function breechRound() {
4036
+ const make = parts.makeRound;
4037
+ if (!make) return;
4038
+ const shell = make();
4039
+ session.model.inner.add(shell);
4040
+ shell.position.set(-0.25, 0, 0);
4041
+ world.schedule(0.02, 0.4, (eased) => shell.position.x = -0.25 + 0.6 * eased, () => {
4042
+ shell.removeFromParent();
4043
+ });
4044
+ }
4045
+ }
4046
+
4047
+ // src/fx/viewmodel.ts
4048
+ var PI12 = Math.PI;
4049
+ var HAND_OFFSET2 = [0.2, -0.2, -0.42];
4050
+ var HEAT_DISPLAY_MAX_FALLBACK = 120;
4051
+ var LAUNCH_VOLUME = 0.6;
4052
+ var caliberOf = (arsenal, def) => def.action.kind === "firearm" ? arsenal.calibers.get(def.action.caliber) : void 0;
4053
+ function createViewmodel(options) {
4054
+ const { THREE, scene, camera, arsenal, groundAt, audio } = options;
4055
+ const kit = modelKit(THREE);
4056
+ const random = createRng3(options.seed ?? 1);
4057
+ const layers = options.layers ?? createLayers();
4058
+ const distance = () => options.soundDistance?.() ?? 0;
4059
+ const particles = createParticles(THREE, scene, groundAt);
4060
+ particles.onBounce = (kind) => kind === "casing" ? audio?.clink() : kind === "magazine" ? audio?.thud() : void 0;
4061
+ const effects = createEffects(kit, scene, particles, random);
4062
+ const hand = new THREE.Group();
4063
+ hand.name = "weapon-viewmodel";
4064
+ scene.add(hand);
4065
+ const holder = new THREE.Group();
4066
+ holder.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), PI12 / 2);
4067
+ hand.add(holder);
4068
+ let tasks = [];
4069
+ let session = null;
4070
+ let player = null;
4071
+ let condition = options.condition ?? "new";
4072
+ let lastAmmo = 0;
4073
+ let smokeDebt = 0;
4074
+ const world = {
4075
+ kit,
4076
+ scene,
4077
+ particles,
4078
+ effects,
4079
+ random,
4080
+ audio,
4081
+ layers,
4082
+ groundAt,
4083
+ schedule: (delay, seconds, step, done) => void tasks.push({ delay, seconds, elapsed: 0, step: (progress) => step(ease(progress)), done })
4084
+ };
4085
+ const heatMax = arsenal.rules.heat.displayMax || HEAT_DISPLAY_MAX_FALLBACK;
4086
+ const release = () => {
4087
+ if (!session) return;
4088
+ session.model.root.removeFromParent();
4089
+ player = null;
4090
+ session = null;
4091
+ tasks = [];
4092
+ };
4093
+ const heavy = (current) => ({
4094
+ kit,
4095
+ scene,
4096
+ particles,
4097
+ effects,
4098
+ random,
4099
+ parts: current.model.parts,
4100
+ forward: () => toWorld(current, new THREE.Vector3(1, 0, 0)).normalize(),
4101
+ positionOf: (part) => worldPositionOf(current, part, new THREE.Vector3()),
4102
+ orientation: () => current.model.root.getWorldQuaternion(new THREE.Quaternion()),
4103
+ every: (seconds, step, done) => void tasks.push({ delay: 0, seconds, elapsed: 0, step, done }),
4104
+ groundAt
4105
+ });
4106
+ const equip = (weapon) => {
4107
+ const def = arsenal.weapons.get(weapon);
4108
+ const model = def ? buildWeaponModelById(THREE, weapon) : void 0;
4109
+ if (!def || !model) return false;
4110
+ release();
4111
+ const length = new THREE.Box3().setFromObject(model.root).getSize(new THREE.Vector3()).x;
4112
+ const scale = length > 0.8 ? 0.55 : length > 0.4 ? 0.75 : 1;
4113
+ holder.scale.setScalar(scale);
4114
+ holder.position.set(0, 0, Math.min(0.25, 0.25 * length * scale));
4115
+ holder.add(model.root);
4116
+ const caliber = caliberOf(arsenal, def);
4117
+ const dims = caliber?.cartridge;
4118
+ session = {
4119
+ id: weapon,
4120
+ def,
4121
+ model,
4122
+ holderScale: scale,
4123
+ lengthMetres: length,
4124
+ caseType: caliber?.caseType ?? "rifle",
4125
+ kit,
4126
+ cartridge: caliber && dims ? { dims, shape: cartridgeShape(caliber, dims) } : null,
4127
+ stuckCase: null,
4128
+ nextChamber: 0
4129
+ };
4130
+ const action = def.action;
4131
+ const reloadFamily = action.kind === "firearm" || action.kind === "flame" || action.kind === "launcher" ? action.family : void 0;
4132
+ const rpm = action.kind === "firearm" ? action.rpm : 0;
4133
+ player = createToolClipPlayer({
4134
+ clips: buildToolClips({ parts: model.parts, family: model.parts.family, caseType: session.caseType, reloadFamily }),
4135
+ rig: createModelRig(model),
4136
+ onEvent: createClipEventHandler(world, session),
4137
+ repeatInterval: () => rpm > 0 ? 60 / rpm : 0.1
4138
+ });
4139
+ applyCondition(kit, model, condition);
4140
+ lastAmmo = options.tool?.().ammo ?? 0;
4141
+ return true;
4142
+ };
4143
+ const place = () => {
4144
+ const [x, y, z] = HAND_OFFSET2;
4145
+ hand.position.set(x, y, z).applyQuaternion(camera.quaternion).add(camera.position);
4146
+ hand.quaternion.copy(camera.quaternion);
4147
+ hand.updateMatrixWorld(true);
4148
+ };
4149
+ const glow = (tool) => {
4150
+ if (!session) return;
4151
+ const heat = (tool?.heat ?? 0) * heatMax;
4152
+ const level = Math.max(0, Math.min(1, (heat - 50) / 50));
4153
+ for (const mesh of session.model.parts.glow) {
4154
+ const material = mesh.material;
4155
+ material.emissive.setRGB(level, 0.2 * level * level, 0.02 * level * level);
4156
+ material.emissiveIntensity = 2.2 * level;
4157
+ }
4158
+ };
4159
+ const handleShot = (current) => {
4160
+ const action = current.def.action;
4161
+ if (action.kind === "launcher") {
4162
+ launchRocket(heavy(current));
4163
+ audio?.play("explosion", LAUNCH_VOLUME, distance());
4164
+ }
4165
+ if (action.kind !== "firearm") return;
4166
+ const caliber = caliberOf(arsenal, current.def);
4167
+ if (!caliber) return;
4168
+ audio?.shot(muzzleEnergy3(caliber) * (caliber.pellets ?? 1), current.def.category, { caliber, distance: distance() });
4169
+ };
4170
+ return {
4171
+ equip,
4172
+ unequip: release,
4173
+ handle(event, tool) {
4174
+ if (event.type === "explosion") return;
4175
+ if (event.type === "tool") equip(event.weapon);
4176
+ if (!session || !player || event.weapon !== session.id) return;
4177
+ const current = session;
4178
+ if (event.type === "shot") handleShot(current);
4179
+ else if (event.type === "reload" && event.phase === "start") current.nextChamber = 0;
4180
+ else if (event.type === "clear" && event.phase === "end") current.stuckCase = null;
4181
+ const mechanical = weaponEventOf(event);
4182
+ if (mechanical) audio?.event(mechanical);
4183
+ const step = toolAnimationStep(event);
4184
+ if (!step) return;
4185
+ const last = step.type === "play" && step.clip === "shot" && current.model.parts.family === "pistol" && tool.ammo === 0;
4186
+ player.apply(last ? { ...step, variant: "last" } : step);
4187
+ },
4188
+ update(dt) {
4189
+ if (session) {
4190
+ place();
4191
+ player?.update(dt);
4192
+ const tool = options.tool?.();
4193
+ glow(tool);
4194
+ if (tool && session.def.action.kind === "flame") {
4195
+ if (tool.ammo < lastAmmo) flameJet(heavy(session), dt, session.model.parts.launcherType === "flame" ? 1 : 0.6);
4196
+ lastAmmo = tool.ammo;
4197
+ }
4198
+ if (tool && tool.heat !== void 0 && tool.heat * heatMax > 10 && session.model.parts.muzzle) {
4199
+ smokeDebt += dt * tool.heat * heatMax * 0.1;
4200
+ while (smokeDebt > 1) {
4201
+ smokeDebt -= 1;
4202
+ const k = 0.5 + tool.heat * heatMax / 60;
4203
+ effects.smoke(worldPositionOf(session, session.model.parts.muzzle, new THREE.Vector3()), new THREE.Vector3((random() - 0.5) * 0.03, 0.07 + random() * 0.08, (random() - 0.5) * 0.03), 7e-3 * k, 1.6 + random(), Math.min(0.42, 0.14 + k * 0.1));
4204
+ }
4205
+ }
4206
+ }
4207
+ for (const task of [...tasks]) {
4208
+ task.elapsed += dt;
4209
+ if (task.elapsed < task.delay) continue;
4210
+ const progress = task.seconds > 0 ? Math.min(1, (task.elapsed - task.delay) / task.seconds) : 1;
4211
+ task.step(progress, dt);
4212
+ if (progress >= 1) {
4213
+ task.done?.();
4214
+ tasks = tasks.filter((other) => other !== task);
4215
+ }
4216
+ }
4217
+ effects.update(dt);
4218
+ particles.update(dt);
4219
+ },
4220
+ setCondition(next) {
4221
+ condition = next;
4222
+ if (session) applyCondition(kit, session.model, condition);
4223
+ },
4224
+ get inHand() {
4225
+ return session?.id ?? null;
4226
+ },
4227
+ get casings() {
4228
+ return particles.count("casing");
4229
+ },
4230
+ get magazines() {
4231
+ return particles.count("magazine");
4232
+ },
4233
+ dispose() {
4234
+ release();
4235
+ particles.dispose();
4236
+ effects.dispose();
4237
+ hand.removeFromParent();
4238
+ }
4239
+ };
4240
+ }
4241
+
4242
+ // src/fx.ts
4243
+ function createWeaponFx(options) {
4244
+ const layers = options.layers ?? createLayers();
4245
+ const box2 = createBoxFx({ ...options, layers });
4246
+ const model = createViewmodel({
4247
+ layers,
4248
+ ...options.soundDistance ? { soundDistance: options.soundDistance } : {},
4249
+ THREE: options.THREE,
4250
+ scene: options.scene,
4251
+ camera: options.camera,
4252
+ arsenal: options.arsenal,
4253
+ groundAt: options.groundAt,
4254
+ audio: options.audio ?? null,
4255
+ ...options.seed === void 0 ? {} : { seed: options.seed },
4256
+ ...options.tool ? { tool: options.tool } : {},
4257
+ ...options.condition ? { condition: options.condition } : {}
4258
+ });
4259
+ return {
4260
+ handle(event, tool) {
4261
+ if (event.type === "explosion") return;
4262
+ if (event.type === "tool") {
4263
+ if (hasWeaponModel(event.weapon)) box2.unequip();
4264
+ else model.unequip();
4265
+ }
4266
+ if (model.inHand !== null || event.type === "tool" && hasWeaponModel(event.weapon)) model.handle(event, tool);
4267
+ else box2.handle(event, tool);
4268
+ },
4269
+ update(dt) {
4270
+ box2.update(dt);
4271
+ model.update(dt);
4272
+ },
4273
+ setCondition: (condition) => model.setCondition(condition),
4274
+ layers,
4275
+ get inHand() {
4276
+ return model.inHand ?? box2.inHand;
4277
+ },
4278
+ get casings() {
4279
+ return box2.casings + model.casings;
4280
+ },
4281
+ get magazines() {
4282
+ return box2.magazines + model.magazines;
4283
+ },
4284
+ dispose() {
4285
+ box2.dispose();
4286
+ model.dispose();
4287
+ }
4288
+ };
4289
+ }
4290
+
4291
+ // src/world-explosives.ts
4292
+ var WORLD_EXPLOSIVE_PREFIX = "world_";
4293
+ var SHAPES2 = {
4294
+ fuel_barrel: { shape: { kind: "cylinder", radius: 0.3, height: 0.9 }, position: [0, 0.45, 0], color: 3104666 },
4295
+ propane_p13: { shape: { kind: "cylinder", radius: 0.15, height: 0.48 }, position: [0, 0.24, 0], color: 13620184 },
4296
+ car_fuel_tank: { shape: { kind: "box", size: [0.9, 0.25, 0.5] }, position: [0, 0.4, 0], color: 4935509 }
4297
+ };
4298
+ var WORLD_EXPLOSIVE_IDS = Object.keys(SHAPES2);
4299
+ var SHELL = { thickness: 1e-3, fill: 0.03 };
4300
+ var explosivePart = (def, shape) => ({
4301
+ id: "tank",
4302
+ material: "steel",
4303
+ shape: shape.shape,
4304
+ position: shape.position,
4305
+ ...SHELL,
4306
+ explodes: { energy: def.energy, radius: def.injuryRadius, ...def.chain ? { chain: { ...def.chain } } : {} }
4307
+ });
4308
+ var meshOf = (THREE, shape) => {
4309
+ const geometry = shape.shape.kind === "cylinder" ? new THREE.CylinderGeometry(shape.shape.radius, shape.shape.radius, shape.shape.height, 16) : shape.shape.kind === "box" ? new THREE.BoxGeometry(...shape.shape.size) : new THREE.SphereGeometry(0.2);
4310
+ const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: shape.color, roughness: 0.55, metalness: 0.5 }));
4311
+ mesh.name = "tank";
4312
+ mesh.userData.destructionPart = "tank";
4313
+ mesh.position.set(...shape.position);
4314
+ mesh.castShadow = true;
4315
+ mesh.receiveShadow = true;
4316
+ return mesh;
4317
+ };
4318
+ var worldExplosiveItem = (def, name) => {
4319
+ const shape = SHAPES2[def.id];
4320
+ if (!shape) return void 0;
4321
+ const spec = { name, parts: [explosivePart(def, shape)], joints: [], tags: ["explosive"] };
4322
+ return {
4323
+ id: `${WORLD_EXPLOSIVE_PREFIX}${def.id}`,
4324
+ name,
4325
+ build: (THREE) => {
4326
+ const group = new THREE.Group();
4327
+ group.add(meshOf(THREE, shape));
4328
+ return group;
4329
+ },
4330
+ spec
4331
+ };
4332
+ };
4333
+
345
4334
  // src/area.ts
346
4335
  var WEAPON_ITEM_PREFIX = "weapon_";
347
4336
  var COUNTER_ITEM_ID = "counter_weapons";
4337
+ var EXPLOSION_VOLUME_BASE = 0.4;
4338
+ var EXPLOSION_VOLUME_PER_METRE = 0.04;
4339
+ var SOUND_DISTANCE_MAX = 200;
4340
+ var FUSE_LENGTH_MAX = 1;
4341
+ var DEFAULT_FUSE_LENGTH = 0.03;
348
4342
  var COUNTER = { length: 4.8, depth: 1.6, height: 0.9 };
349
4343
  var COUNTER_X = [-6.5, -9, -11.5];
350
- var COUNTER_Z = -1;
351
- var END_MARGIN = 0.6;
352
- var SLOT_GAP = 0.3;
4344
+ var COUNTER_Z = [-1, 5.5];
4345
+ var END_MARGIN = 0.5;
4346
+ var DEFAULT_PITCH = 0.16;
4347
+ var PITCH_BY_ID = { browning_m2: 0.5, m2_flamethrower: 0.45, bow: 0.3, crossbow: 0.3 };
4348
+ var WORLD_EXPLOSIVES = [
4349
+ { id: "fuel_barrel", at: [10, 0, -8] },
4350
+ { id: "fuel_barrel", at: [11.2, 0, -8] },
4351
+ { id: "propane_p13", at: [10.6, 0, -9.3] },
4352
+ { id: "car_fuel_tank", at: [8.6, 0, -9.3] }
4353
+ ];
353
4354
  var TARGETS = [
354
4355
  { id: "target_steel_plate", at: [-1.5, 0, -7] },
355
4356
  { id: "target_plywood", at: [0, 0, -7] },
@@ -362,7 +4363,20 @@ var TEXT = {
362
4363
  counter: "Weapon counter",
363
4364
  targets: { target_steel_plate: "Steel plate target", target_plywood: "Plywood target", target_concrete: "Concrete block target" },
364
4365
  reloadStart: "Reload started: {weapon} ({ammo}/{capacity})",
365
- reloadEnd: "Reload done: {weapon} ({ammo}/{capacity})"
4366
+ reloadEnd: "Reload done: {weapon} ({ammo}/{capacity})",
4367
+ worldExplosives: { fuel_barrel: "Fuel barrel", propane_p13: "Propane cylinder (P13)", car_fuel_tank: "Car fuel tank" },
4368
+ condition: "Weapon condition",
4369
+ conditionHint: "Wear of the weapon: it jams more and the metal shows it",
4370
+ conditions: { new: "New", used: "Used", worn: "Worn", rusty: "Rusty" },
4371
+ ammoQuality: "Ammunition",
4372
+ ammoQualityHint: "Reloaded rounds misfire more often",
4373
+ ammoQualities: { factory: "Factory", reloaded: "Reloaded" },
4374
+ soundDistance: "Sound distance",
4375
+ soundDistanceHint: "How far the listener is from the shot: farther is quieter and duller",
4376
+ fuseLength: "Dynamite fuse length",
4377
+ fuseLengthHint: "Metres of safety fuse (131 s per metre): a long one is set down lit, for a delayed trap",
4378
+ layers: { muzzle_flash: "Muzzle flash", casings: "Ejected casings", magazines: "Dropped magazines" },
4379
+ metrics: { casings_live: "Casings in the scene", magazines_live: "Magazines in the scene", heat: "Barrel heat" }
366
4380
  },
367
4381
  "pt-BR": {
368
4382
  group: "Armas",
@@ -370,7 +4384,20 @@ var TEXT = {
370
4384
  counter: "Balc\xE3o de armas",
371
4385
  targets: { target_steel_plate: "Alvo: chapa de a\xE7o", target_plywood: "Alvo: compensado", target_concrete: "Alvo: bloco de concreto" },
372
4386
  reloadStart: "Recarga iniciada: {weapon} ({ammo}/{capacity})",
373
- reloadEnd: "Recarga completa: {weapon} ({ammo}/{capacity})"
4387
+ reloadEnd: "Recarga completa: {weapon} ({ammo}/{capacity})",
4388
+ worldExplosives: { fuel_barrel: "Barril de combust\xEDvel", propane_p13: "Botij\xE3o de g\xE1s (P13)", car_fuel_tank: "Tanque de combust\xEDvel do carro" },
4389
+ condition: "Estado da arma",
4390
+ conditionHint: "Desgaste da arma: trava mais e o metal mostra",
4391
+ conditions: { new: "Nova", used: "Usada", worn: "Gasta", rusty: "Enferrujada" },
4392
+ ammoQuality: "Muni\xE7\xE3o",
4393
+ ammoQualityHint: "Muni\xE7\xE3o recarregada falha mais",
4394
+ ammoQualities: { factory: "De f\xE1brica", reloaded: "Recarregada" },
4395
+ soundDistance: "Dist\xE2ncia do som",
4396
+ soundDistanceHint: "Qu\xE3o longe o ouvinte est\xE1 do tiro: mais longe soa mais baixo e abafado",
4397
+ fuseLength: "Pavio da dinamite",
4398
+ fuseLengthHint: "Metros de pavio de seguran\xE7a (131 s por metro): um longo \xE9 posto no ch\xE3o aceso, para armadilha com atraso",
4399
+ layers: { muzzle_flash: "Clar\xE3o do cano", casings: "C\xE1psulas ejetadas", magazines: "Pentes que caem" },
4400
+ metrics: { casings_live: "C\xE1psulas na cena", magazines_live: "Pentes na cena", heat: "Calor do cano" }
374
4401
  }
375
4402
  };
376
4403
  var textOf = (locale) => TEXT[locale] ?? TEXT.en;
@@ -412,6 +4439,18 @@ var targetProp = (id, name) => {
412
4439
  };
413
4440
  return { spec, colors: { post: 3816770, face: color } };
414
4441
  };
4442
+ var weaponItem = (def, name) => {
4443
+ const id = `${WEAPON_ITEM_PREFIX}${def.id}`;
4444
+ if (!hasWeaponModel(def.id)) return asItem(id, name, weaponProp(def, name), { weapon: def.id, movable: true });
4445
+ return {
4446
+ id,
4447
+ name,
4448
+ build: (THREE) => buildModeledProp(THREE, def, name),
4449
+ spec: (root) => modeledPropSpec(root, def, name),
4450
+ weapon: def.id,
4451
+ movable: true
4452
+ };
4453
+ };
415
4454
  var asItem = (id, name, prop, extra = {}) => ({
416
4455
  id,
417
4456
  name,
@@ -419,26 +4458,47 @@ var asItem = (id, name, prop, extra = {}) => ({
419
4458
  spec: prop.spec,
420
4459
  ...extra
421
4460
  });
422
- function weaponsLayout(weaponItemIds) {
423
- const perCounter = Math.floor((COUNTER.length - 2 * END_MARGIN) / SLOT_GAP) + 1;
424
- const counters = COUNTER_X.slice(0, Math.max(1, Math.ceil(weaponItemIds.length / perCounter)));
425
- const placements = counters.map((x) => ({ item: COUNTER_ITEM_ID, position: [x, 0, COUNTER_Z], surface: { halfSize: [COUNTER.depth / 2, COUNTER.length / 2], height: COUNTER.height } }));
426
- weaponItemIds.forEach((item, index) => {
427
- const x = counters[Math.min(counters.length - 1, Math.floor(index / perCounter))];
428
- const z = COUNTER_Z - COUNTER.length / 2 + END_MARGIN + index % perCounter * SLOT_GAP;
429
- placements.push({ item, position: [x, COUNTER.height + 0.01, z], asleep: true });
430
- });
431
- for (const target of TARGETS) placements.push({ item: target.id, position: target.at });
432
- return placements;
4461
+ function weaponsLayout(weaponItemIds, pitchOf = () => DEFAULT_PITCH) {
4462
+ const usable = COUNTER.length - 2 * END_MARGIN;
4463
+ const slots = COUNTER_Z.flatMap((z) => COUNTER_X.map((x) => ({ x, z })));
4464
+ const counterOf = [];
4465
+ const placements = [];
4466
+ for (const item of weaponItemIds) {
4467
+ const pitch = pitchOf(item);
4468
+ let counter = counterOf[counterOf.length - 1];
4469
+ if (!counter || counter.used + pitch > usable + 1e-9) {
4470
+ const slot = slots[counterOf.length];
4471
+ if (!slot) throw new Error(`weapons area: ${weaponItemIds.length} weapons do not fit on ${slots.length} counters of ${usable.toFixed(2)} m`);
4472
+ if (pitch > usable + 1e-9) throw new Error(`weapons area: "${item}" takes ${pitch} m and a counter holds ${usable.toFixed(2)} m`);
4473
+ counter = { ...slot, used: 0 };
4474
+ counterOf.push(counter);
4475
+ }
4476
+ placements.push({ item, position: [counter.x, COUNTER.height + 0.01, counter.z - COUNTER.length / 2 + END_MARGIN + counter.used + pitch / 2], asleep: true });
4477
+ counter.used += pitch;
4478
+ }
4479
+ const counters = counterOf.map(({ x, z }) => ({ item: COUNTER_ITEM_ID, position: [x, 0, z], surface: { halfSize: [COUNTER.depth / 2, COUNTER.length / 2], height: COUNTER.height } }));
4480
+ const targets = TARGETS.map((target) => ({ item: target.id, position: target.at }));
4481
+ const explosives = WORLD_EXPLOSIVES.map((explosive) => ({ item: `${WORLD_EXPLOSIVE_PREFIX}${explosive.id}`, position: explosive.at }));
4482
+ return [...counters, ...placements, ...targets, ...explosives];
433
4483
  }
434
4484
  function createWeaponsTestModule(options = {}) {
435
4485
  const locale = options.locale ?? "pt-BR";
436
4486
  const arsenal = options.arsenal ?? createArsenal({ locale });
437
4487
  const text = textOf(locale);
438
- const audio = options.sound === false ? null : createWeaponAudio();
4488
+ const audio = options.sound === false ? null : createWeaponAudio(void 0, options.seed === void 0 ? {} : { seed: options.seed });
439
4489
  const weaponDefs = [...arsenal.weapons.values()].filter((def) => !BODY_ACTIONS.has(def.id));
440
- const weaponIds = weaponDefs.map((def) => `${WEAPON_ITEM_PREFIX}${def.id}`);
4490
+ const weaponIds = weaponDefs.filter((def) => !(def.id in ARCHETYPE_MODELS)).map((def) => `${WEAPON_ITEM_PREFIX}${def.id}`);
4491
+ const pitchOf = (itemId) => PITCH_BY_ID[itemId.slice(WEAPON_ITEM_PREFIX.length)] ?? DEFAULT_PITCH;
4492
+ const worldExplosives = WORLD_EXPLOSIVE_IDS.flatMap((id) => {
4493
+ const def = arsenal.explosives.get(id);
4494
+ const item = def ? worldExplosiveItem(def, text.worldExplosives[id] ?? id) : void 0;
4495
+ return item ? [item] : [];
4496
+ });
4497
+ const worldExplosiveIds = new Set(worldExplosives.map((item) => item.id));
441
4498
  let fx = null;
4499
+ let host = null;
4500
+ let soundDistance = 0;
4501
+ const heatPercent = () => Math.min(100, (host?.tool().heat ?? 0) * 100);
442
4502
  return {
443
4503
  id: "weapons_area",
444
4504
  name: text.name,
@@ -448,48 +4508,126 @@ function createWeaponsTestModule(options = {}) {
448
4508
  return fx;
449
4509
  },
450
4510
  items: () => [
451
- ...weaponDefs.map((def) => asItem(`${WEAPON_ITEM_PREFIX}${def.id}`, arsenal.name(def.id), weaponProp(def, arsenal.name(def.id)), { weapon: def.id, movable: true })),
4511
+ ...weaponDefs.map((def) => weaponItem(def, arsenal.name(def.id))),
452
4512
  asItem(COUNTER_ITEM_ID, text.counter, counterProp(text.counter), { movable: false }),
453
- ...TARGETS.map((target) => asItem(target.id, text.targets[target.id], targetProp(target.id, text.targets[target.id]), { movable: false }))
4513
+ ...TARGETS.map((target) => asItem(target.id, text.targets[target.id] ?? target.id, targetProp(target.id, text.targets[target.id] ?? target.id), { movable: false })),
4514
+ ...worldExplosives
4515
+ ],
4516
+ area: () => ({
4517
+ placements: weaponsLayout(weaponIds, pitchOf).filter((placement) => !placement.item.startsWith(WORLD_EXPLOSIVE_PREFIX) || worldExplosiveIds.has(placement.item)),
4518
+ view: { eye: [-3.4, 1.9, 9.2], target: [-9, 0.9, 1.6] }
4519
+ }),
4520
+ controls: () => [
4521
+ {
4522
+ kind: "choice",
4523
+ id: "condition",
4524
+ name: text.condition,
4525
+ hint: text.conditionHint,
4526
+ options: arsenal.rules.conditions.map((entry) => ({ id: entry.id, name: text.conditions[entry.id] ?? entry.id })),
4527
+ default: options.condition ?? arsenal.rules.defaultCondition
4528
+ },
4529
+ {
4530
+ kind: "choice",
4531
+ id: "ammo_quality",
4532
+ name: text.ammoQuality,
4533
+ hint: text.ammoQualityHint,
4534
+ options: arsenal.rules.ammoQualities.map((entry) => ({ id: entry.id, name: text.ammoQualities[entry.id] ?? entry.id })),
4535
+ default: arsenal.rules.defaultAmmoQuality
4536
+ },
4537
+ { kind: "number", id: "sound_distance", name: text.soundDistance, hint: text.soundDistanceHint, unit: "m", min: 0, max: SOUND_DISTANCE_MAX, step: 5, default: 0 },
4538
+ { kind: "number", id: "fuse_length", name: text.fuseLength, hint: text.fuseLengthHint, unit: "m", min: 0.01, max: FUSE_LENGTH_MAX, step: 0.01, default: arsenal.explosives.get("dynamite")?.fuseLength ?? DEFAULT_FUSE_LENGTH }
454
4539
  ],
455
- area: () => ({ placements: weaponsLayout(weaponIds), view: { eye: [-3.4, 1.7, 2.6], target: [-9, 0.9, -1.2] } }),
456
- attach(host) {
4540
+ layers: () => ["muzzle_flash", "casings", "magazines"].map((id) => ({ id, name: text.layers[id] })),
4541
+ metrics: () => [
4542
+ { id: "casings_live", name: text.metrics.casings_live, read: () => fx?.casings ?? 0 },
4543
+ { id: "magazines_live", name: text.metrics.magazines_live, read: () => fx?.magazines ?? 0 },
4544
+ { id: "heat", name: text.metrics.heat, unit: "%", max: 100, digits: 0, read: heatPercent }
4545
+ ],
4546
+ attach(lent) {
4547
+ host = lent;
457
4548
  const created = createWeaponFx({
458
- THREE: host.THREE,
459
- scene: host.scene,
460
- camera: host.camera,
4549
+ THREE: lent.THREE,
4550
+ scene: lent.scene,
4551
+ camera: lent.camera,
461
4552
  arsenal,
462
- groundAt: host.groundAt,
463
- audio
4553
+ groundAt: lent.groundAt,
4554
+ audio,
4555
+ tool: lent.tool,
4556
+ soundDistance: () => soundDistance,
4557
+ ...options.condition ? { condition: options.condition } : {},
4558
+ ...options.seed === void 0 ? {} : { seed: options.seed }
464
4559
  });
465
4560
  fx = created;
466
- const offEvents = host.on((event) => {
467
- const tool = host.tool();
4561
+ const camera = lent.camera;
4562
+ const offBench = lent.bench?.onChange((change) => {
4563
+ if (change.kind === "layer") {
4564
+ if (change.id === "muzzle_flash") created.layers.muzzleFlash = change.on;
4565
+ else if (change.id === "casings") created.layers.casings = change.on;
4566
+ else if (change.id === "magazines") created.layers.magazines = change.on;
4567
+ } else if (change.kind === "control" && typeof change.value === "string") {
4568
+ if (change.id === "condition") {
4569
+ created.setCondition(change.value);
4570
+ lent.configureTool?.({ condition: change.value });
4571
+ } else if (change.id === "ammo_quality") lent.configureTool?.({ ammo_quality: change.value });
4572
+ } else if (change.kind === "control" && change.id === "sound_distance" && typeof change.value === "number") soundDistance = change.value;
4573
+ else if (change.kind === "control" && change.id === "fuse_length" && typeof change.value === "number") lent.configureTool?.({ fuse_length: String(change.value) });
4574
+ });
4575
+ const offEvents = lent.on((event) => {
4576
+ if (event.type === "explosion") {
4577
+ const away = Math.hypot(event.point[0] - camera.position.x, event.point[1] - camera.position.y, event.point[2] - camera.position.z);
4578
+ audio?.play("explosion", Math.min(1, EXPLOSION_VOLUME_BASE + event.radius * EXPLOSION_VOLUME_PER_METRE), away);
4579
+ return;
4580
+ }
4581
+ const tool = lent.tool();
468
4582
  created.handle(event, tool);
469
- if (event.type === "reload") host.log(fill(event.phase === "start" ? text.reloadStart : text.reloadEnd, { weapon: arsenal.name(event.weapon), ammo: tool.ammo, capacity: tool.capacity }), { weapon: event.weapon, phase: event.phase });
4583
+ if (event.type === "reload") lent.log(fill(event.phase === "start" ? text.reloadStart : text.reloadEnd, { weapon: arsenal.name(event.weapon), ammo: tool.ammo, capacity: tool.capacity }), { weapon: event.weapon, phase: event.phase });
470
4584
  });
471
- const offFrame = host.onFrame((dt) => created.update(dt));
472
- created.handle({ type: "tool", weapon: host.tool().weapon }, host.tool());
4585
+ const offFrame = lent.onFrame((dt) => created.update(dt));
4586
+ created.handle({ type: "tool", weapon: lent.tool().weapon }, lent.tool());
473
4587
  return () => {
4588
+ offBench?.();
474
4589
  offEvents();
475
4590
  offFrame();
476
4591
  created.dispose();
477
4592
  audio?.dispose();
478
4593
  fx = null;
4594
+ host = null;
479
4595
  };
480
4596
  }
481
4597
  };
482
4598
  }
483
4599
  export {
4600
+ ARCHETYPE_MODELS,
484
4601
  BODY_ACTIONS,
4602
+ CLASS_REFERENCE_ENERGY,
485
4603
  COUNTER_ITEM_ID,
4604
+ DEFAULT_ENERGY_CURVE,
4605
+ GAME_DISTANCE_CURVE,
4606
+ GAME_SOUND_CLASSES,
4607
+ MECHANICAL_SOUNDS,
4608
+ MODEL_ENTRIES,
486
4609
  WEAPON_ITEM_PREFIX,
4610
+ WORLD_EXPLOSIVE_IDS,
4611
+ WORLD_EXPLOSIVE_PREFIX,
4612
+ applyCondition,
4613
+ buildCartridge,
4614
+ buildCasing,
487
4615
  buildProp,
4616
+ buildRound,
4617
+ buildToolClips,
488
4618
  buildWeaponModel,
4619
+ buildWeaponModelById,
4620
+ cartridgeShape,
4621
+ computeShotSound,
489
4622
  createWeaponAudio,
490
4623
  createWeaponFx,
491
4624
  createWeaponsTestModule,
4625
+ hasWeaponModel,
4626
+ mechanicalSoundOf,
4627
+ modelKit,
4628
+ partRegistry,
492
4629
  weaponProp,
493
- weaponsLayout
4630
+ weaponsLayout,
4631
+ worldExplosiveItem
494
4632
  };
495
4633
  //# sourceMappingURL=index.js.map