@adatechnology/game-weapons-three 0.2.0 → 0.3.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
@@ -1,6 +1,294 @@
1
1
  // src/area.ts
2
2
  import { createArsenal } from "@adatechnology/game-weapons";
3
3
 
4
+ // src/attachments-key.ts
5
+ var attachmentsKey = (parts) => {
6
+ if (!parts) return "";
7
+ let key = String(parts.length);
8
+ for (const part of parts) key += `|${part.id}@${part.mount}${part.on ? "+" : ""}`;
9
+ return key;
10
+ };
11
+
12
+ // src/accessory-bench.ts
13
+ var ACCESSORY_MOUNTS = ["muzzle", "rail_top", "rail_left", "rail_right", "rail_bottom", "underbarrel", "magazine", "feed", "sling", "barrel"];
14
+ var ACCESSORY_KEYS = { light: "KeyL", laser: "KeyK", stock: "KeyV", underbarrel: "KeyU" };
15
+ var FIT_PREFIX = "fit_";
16
+ var NONE = "none";
17
+ var SUPPORTED_CONTROL_ID = "supported_stance";
18
+ var ACTION_IDS = { light: "toggle_light", laser: "toggle_laser", stock: "toggle_stock", underbarrel: "switch_underbarrel" };
19
+ var SECONDARY_BUTTON = 2;
20
+ var TEXT_FIELDS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT"]);
21
+ var accessoryControls = (text) => [
22
+ ...ACCESSORY_MOUNTS.map((mount) => ({ kind: "choice", id: `${FIT_PREFIX}${mount}`, name: text.mounts[mount] ?? mount, options: [{ id: NONE, name: text.none }], default: NONE })),
23
+ { kind: "boolean", id: SUPPORTED_CONTROL_ID, name: text.supported.name, hint: text.supported.hint, default: false },
24
+ ...Object.keys(ACTION_IDS).map((key) => ({ kind: "action", id: ACTION_IDS[key], name: text.actions[key].name, hint: text.actions[key].hint }))
25
+ ];
26
+ var accessoryLayers = (text) => [
27
+ { id: "attachments", name: text.layers.attachments },
28
+ { id: "beams", name: text.layers.beams }
29
+ ];
30
+ var accessoryMetrics = (text, tool) => [
31
+ { id: "weapon_mass", name: text.metrics.mass, unit: "kg", digits: 2, read: () => tool().mass ?? 0 },
32
+ { id: "aim_time", name: text.metrics.aim, unit: "s", digits: 2, read: () => tool().aimSeconds ?? 0 },
33
+ { id: "accessories_fitted", name: text.metrics.fitted, digits: 0, read: () => (tool().attachments ?? []).length },
34
+ { id: "suppressed", name: text.metrics.suppressed, read: () => tool().suppressed ? text.yes : text.no }
35
+ ];
36
+ var fittedOf = (parts) => (parts ?? []).map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } }));
37
+ var fillText = (template, vars) => template.replace(/\{(\w+)\}/g, (_, key) => vars[key] ?? "");
38
+ function createAccessoryBench(options) {
39
+ const { arsenal, host, text } = options;
40
+ const camera = host.camera;
41
+ const bench = host.bench;
42
+ const pending = /* @__PURE__ */ new Map();
43
+ let signature = "";
44
+ let aiming = false;
45
+ let aimProgress = 0;
46
+ let baseFov = camera.fov;
47
+ let appliedZoom = 1;
48
+ const nameOf = (id) => text.attachments[id] ?? arsenal.attachments.get(id)?.name ?? id;
49
+ const offers = (tool, mount) => {
50
+ const fitted = fittedOf(tool.attachments);
51
+ const here = fitted.find((part) => part.mount === mount);
52
+ const choices = [{ id: NONE, name: text.none }];
53
+ if (here) choices.push({ id: here.id, name: nameOf(here.id) });
54
+ for (const def of arsenal.attachments.values()) {
55
+ if (def.id === here?.id || !arsenal.canFit(tool.weapon, def.id, { fitted, mount }).ok) continue;
56
+ choices.push({ id: def.id, name: nameOf(def.id) });
57
+ }
58
+ return choices;
59
+ };
60
+ const refresh = () => {
61
+ const tool = host.tool();
62
+ const next = `${tool.weapon}|${attachmentsKey(tool.attachments)}`;
63
+ if (next === signature) return;
64
+ signature = next;
65
+ const def = arsenal.weapons.get(tool.weapon);
66
+ for (const mount of ACCESSORY_MOUNTS) {
67
+ const has = (def?.mounts ?? []).some((place) => place.kind === mount);
68
+ const here = (tool.attachments ?? []).find((part) => part.mount === mount);
69
+ const choices = offers(tool, mount);
70
+ const isFixed = here?.integral === true;
71
+ bench?.patch(`${FIT_PREFIX}${mount}`, { hidden: !has, disabled: isFixed, options: choices, ...pending.has(mount) ? {} : { value: here?.id ?? NONE } });
72
+ }
73
+ };
74
+ const refuse = (mount, reason) => {
75
+ host.log(fillText(text.fitRefused, { mount: text.mounts[mount] ?? mount, weapon: arsenal.name(host.tool().weapon), reason: reason ?? "" }), { mount, reason });
76
+ pending.delete(mount);
77
+ signature = "";
78
+ };
79
+ const settle = () => {
80
+ for (const [mount, wanted] of pending) {
81
+ const here = (host.tool().attachments ?? []).find((part) => part.mount === mount && !part.integral);
82
+ if (here?.id === wanted || !here && wanted === NONE) {
83
+ pending.delete(mount);
84
+ continue;
85
+ }
86
+ const request = here ? { type: "remove", attachment: here.id } : { type: "fit", attachment: wanted, mount };
87
+ const result = host.command?.(request);
88
+ if (!result || !result.ok && result.reason !== "busy") refuse(mount, result?.reason);
89
+ if (result?.reason === "busy") return;
90
+ }
91
+ };
92
+ const toggleKind = (kind) => {
93
+ const part = (host.tool().attachments ?? []).find((candidate) => arsenal.attachments.get(candidate.id)?.kind === kind);
94
+ if (!part) host.log(kind === "light" ? text.noLight : text.noLaser);
95
+ else host.command?.({ type: "toggle", attachment: part.id });
96
+ };
97
+ const toggleStock = () => {
98
+ const tool = host.tool();
99
+ const kind = arsenal.weapons.get(tool.weapon)?.stock?.kind;
100
+ if (kind === void 0 || kind === "fixed" || kind === "none") return false;
101
+ host.command?.({ type: "stock", state: tool.stock === "collapsed" ? "extended" : "collapsed" });
102
+ return true;
103
+ };
104
+ const switchUnderbarrel = () => {
105
+ const tool = host.tool();
106
+ const hasLauncher = (tool.attachments ?? []).some((part) => arsenal.attachments.get(part.id)?.launcher !== void 0);
107
+ if (!hasLauncher) host.log(text.noLauncher);
108
+ else host.command?.({ type: "underbarrel", on: tool.underbarrelActive !== true });
109
+ };
110
+ const act = (id) => {
111
+ if (id === ACTION_IDS.light) toggleKind("light");
112
+ else if (id === ACTION_IDS.laser) toggleKind("laser");
113
+ else if (id === ACTION_IDS.stock) {
114
+ if (!toggleStock()) host.log(text.noStock);
115
+ } else if (id === ACTION_IDS.underbarrel) switchUnderbarrel();
116
+ else return false;
117
+ return true;
118
+ };
119
+ const zoomOf = (tool) => {
120
+ const optic = (tool.attachments ?? []).map((part) => arsenal.attachments.get(part.id)?.optic).find((entry) => entry !== void 0);
121
+ return optic?.zoom[0] ?? 1;
122
+ };
123
+ const isFirearm = (tool) => arsenal.weapons.get(tool.weapon)?.action.kind === "firearm";
124
+ const setAim = (on) => {
125
+ if (on === aiming) return;
126
+ aiming = on;
127
+ host.command?.({ type: "stance", aiming: on });
128
+ };
129
+ const follow = (dt) => {
130
+ const tool = host.tool();
131
+ const seconds = Math.max(0.05, tool.aimSeconds ?? 0.2);
132
+ aimProgress = Math.max(0, Math.min(1, aimProgress + (aiming ? dt : -dt) / seconds));
133
+ const zoom = 1 + (zoomOf(tool) - 1) * aimProgress;
134
+ if (Math.abs(zoom - appliedZoom) < 1e-4) return;
135
+ if (appliedZoom === 1) baseFov = camera.fov;
136
+ appliedZoom = zoom;
137
+ camera.fov = zoom === 1 ? baseFov : 2 * Math.atan(Math.tan(baseFov * Math.PI / 360) / zoom) * 180 / Math.PI;
138
+ camera.updateProjectionMatrix();
139
+ };
140
+ const onKeyDown = (event) => {
141
+ if (event.repeat || event.ctrlKey || event.metaKey || event.altKey || TEXT_FIELDS.has(event.target?.tagName ?? "")) return;
142
+ const handled2 = event.code === ACCESSORY_KEYS.light ? act(ACTION_IDS.light) : event.code === ACCESSORY_KEYS.laser ? act(ACTION_IDS.laser) : event.code === ACCESSORY_KEYS.underbarrel ? act(ACTION_IDS.underbarrel) : event.code === ACCESSORY_KEYS.stock && toggleStock();
143
+ if (!handled2) return;
144
+ event.preventDefault();
145
+ event.stopImmediatePropagation();
146
+ };
147
+ const isOnRoom = (event) => Boolean(document.pointerLockElement) || event.target instanceof HTMLCanvasElement;
148
+ const onMouseDown = (event) => {
149
+ if (event.button !== SECONDARY_BUTTON || !isOnRoom(event) || !isFirearm(host.tool())) return;
150
+ event.preventDefault();
151
+ event.stopImmediatePropagation();
152
+ setAim(true);
153
+ };
154
+ const onMouseUp = (event) => {
155
+ if (event.button === SECONDARY_BUTTON) setAim(false);
156
+ };
157
+ const onBlur = () => setAim(false);
158
+ window.addEventListener("keydown", onKeyDown, true);
159
+ window.addEventListener("mousedown", onMouseDown, true);
160
+ window.addEventListener("mouseup", onMouseUp);
161
+ window.addEventListener("blur", onBlur);
162
+ const offFrame = host.onFrame((dt) => {
163
+ refresh();
164
+ settle();
165
+ follow(dt);
166
+ });
167
+ const offEvents = host.on((event) => {
168
+ if (event.type === "attachment") {
169
+ const template = event.action === "fit" ? text.fitting : event.action === "remove" ? text.removing : text.broke;
170
+ host.log(fillText(template, { attachment: nameOf(event.attachment), weapon: arsenal.name(event.weapon), seconds: String(event.duration ?? 0) }), { attachment: event.attachment, action: event.action });
171
+ }
172
+ if (event.type !== "tool") return;
173
+ pending.clear();
174
+ setAim(false);
175
+ bench?.patch(SUPPORTED_CONTROL_ID, { value: false });
176
+ signature = "";
177
+ });
178
+ return {
179
+ handleChange(change) {
180
+ if (change.kind === "action") return act(change.id);
181
+ if (change.kind === "control" && change.id === SUPPORTED_CONTROL_ID && typeof change.value === "boolean") {
182
+ host.command?.({ type: "stance", supported: change.value });
183
+ return true;
184
+ }
185
+ if (change.kind !== "control" || !change.id.startsWith(FIT_PREFIX) || typeof change.value !== "string") return false;
186
+ const mount = change.id.slice(FIT_PREFIX.length);
187
+ pending.set(mount, change.value);
188
+ settle();
189
+ return true;
190
+ },
191
+ dispose() {
192
+ offFrame();
193
+ offEvents();
194
+ window.removeEventListener("keydown", onKeyDown, true);
195
+ window.removeEventListener("mousedown", onMouseDown, true);
196
+ window.removeEventListener("mouseup", onMouseUp);
197
+ window.removeEventListener("blur", onBlur);
198
+ if (appliedZoom !== 1) {
199
+ camera.fov = baseFov;
200
+ camera.updateProjectionMatrix();
201
+ }
202
+ }
203
+ };
204
+ }
205
+
206
+ // src/accessory-text.ts
207
+ var ACCESSORY_TEXT = {
208
+ en: {
209
+ none: "None",
210
+ mounts: { muzzle: "Muzzle", rail_top: "Top rail", rail_left: "Left rail", rail_right: "Right rail", rail_bottom: "Bottom rail", underbarrel: "Under the barrel", magazine: "Magazine", feed: "Feed (backpack)", sling: "Sling", barrel: "Barrel" },
211
+ actions: {
212
+ light: { name: "Light on/off (L)", hint: "Turns the weapon light on or off" },
213
+ laser: { name: "Laser on/off (K)", hint: "Turns the laser sight on or off" },
214
+ stock: { name: "Fold or open the stock (V)", hint: "Folds or slides the stock in, or opens it" },
215
+ underbarrel: { name: "Launcher under the barrel (U)", hint: "Switches the trigger and the reload between the weapon and the launcher under its barrel" }
216
+ },
217
+ layers: { attachments: "Accessory models", beams: "Light cone and laser beam" },
218
+ supported: { name: "Supported (deployed / prone)", hint: "The weapon rests on something: a bipod then takes its recoil and spread effect" },
219
+ metrics: { mass: "Weapon mass", aim: "Time to the sights", fitted: "Accessories on the weapon", suppressed: "Suppressed" },
220
+ yes: "yes",
221
+ no: "no",
222
+ attachments: {},
223
+ fitRefused: "Could not change {mount} of {weapon}: {reason}",
224
+ fitting: "Fitting {attachment} on {weapon} ({seconds} s)",
225
+ removing: "Taking {attachment} off {weapon} ({seconds} s)",
226
+ broke: "{attachment} broke on {weapon}",
227
+ noLight: "No light on this weapon",
228
+ noLaser: "No laser on this weapon",
229
+ noStock: "This weapon has no stock that folds or slides",
230
+ noLauncher: "No launcher under the barrel of this weapon"
231
+ },
232
+ "pt-BR": {
233
+ none: "Nenhum",
234
+ mounts: { muzzle: "Boca do cano", rail_top: "Trilho superior", rail_left: "Trilho esquerdo", rail_right: "Trilho direito", rail_bottom: "Trilho inferior", underbarrel: "Sob o cano", magazine: "Carregador", feed: "Alimenta\xE7\xE3o (mochila)", sling: "Bandoleira", barrel: "Cano" },
235
+ actions: {
236
+ light: { name: "Ligar/desligar a lanterna (L)", hint: "Liga ou desliga a lanterna da arma" },
237
+ laser: { name: "Ligar/desligar o laser (K)", hint: "Liga ou desliga o laser da arma" },
238
+ stock: { name: "Dobrar ou abrir a coronha (V)", hint: "Dobra ou recolhe a coronha, ou a abre" },
239
+ underbarrel: { name: "Lan\xE7a-granada sob o cano (U)", hint: "Passa o gatilho e a recarga entre a arma e o lan\xE7a-granada sob o cano" }
240
+ },
241
+ layers: { attachments: "Modelos dos acess\xF3rios", beams: "Cone da lanterna e feixe do laser" },
242
+ supported: { name: "Apoiado (bip\xE9 aberto / deitado)", hint: "A arma est\xE1 apoiada: o bip\xE9 passa a valer no recuo e na dispers\xE3o" },
243
+ metrics: { mass: "Massa da arma", aim: "Tempo at\xE9 a mira", fitted: "Acess\xF3rios na arma", suppressed: "Silenciada" },
244
+ yes: "sim",
245
+ no: "n\xE3o",
246
+ attachments: {
247
+ suppressor: "Supressor",
248
+ oil_filter_suppressor: "Supressor de filtro de \xF3leo",
249
+ integral_suppressor: "Supressor integrado",
250
+ muzzle_brake: "Freio de boca",
251
+ muzzle_brake_heavy: "Freio de boca pesado",
252
+ compensator: "Compensador",
253
+ red_dot: "Mira red dot",
254
+ holographic_sight: "Mira hologr\xE1fica",
255
+ acog_4x: "ACOG 4\xD7",
256
+ scope_3_9x: "Luneta 3\u20139\xD7",
257
+ scope_6_24x: "Luneta 6\u201324\xD7",
258
+ night_vision_scope: "Mira noturna",
259
+ thermal_scope: "Mira t\xE9rmica",
260
+ integral_optic_1_5x: "Mira integrada 1,5\xD7",
261
+ integral_optic_3x: "Mira integrada 3\xD7",
262
+ pso_1: "Mira PSO-1",
263
+ fixed_scope_10x: "Luneta fixa 10\xD7",
264
+ vertical_grip: "Empunhadura vertical",
265
+ bipod: "Bip\xE9",
266
+ m203: "Lan\xE7a-granada M203",
267
+ gp_25: "Lan\xE7a-granada GP-25",
268
+ ag36: "Lan\xE7a-granada AG36",
269
+ weapon_light: "Lanterna",
270
+ laser_sight: "Laser",
271
+ extended_magazine: "Carregador estendido",
272
+ drum_magazine: "Carregador tambor",
273
+ dual_coupler: "Acoplador de carregadores",
274
+ sawn_off_barrel: "Cano serrado",
275
+ long_barrel: "Cano longo",
276
+ sling: "Bandoleira",
277
+ ammo_backpack: "Mochila de muni\xE7\xE3o",
278
+ tripod: "Trip\xE9"
279
+ },
280
+ fitRefused: "N\xE3o deu para mudar {mount} de {weapon}: {reason}",
281
+ fitting: "Encaixando {attachment} em {weapon} ({seconds} s)",
282
+ removing: "Tirando {attachment} de {weapon} ({seconds} s)",
283
+ broke: "{attachment} quebrou em {weapon}",
284
+ noLight: "Esta arma n\xE3o tem lanterna",
285
+ noLaser: "Esta arma n\xE3o tem laser",
286
+ noStock: "Esta arma n\xE3o tem coronha que dobre ou recolha",
287
+ noLauncher: "Esta arma n\xE3o tem lan\xE7a-granada sob o cano"
288
+ }
289
+ };
290
+ var accessoryTextOf = (locale) => ACCESSORY_TEXT[locale] ?? ACCESSORY_TEXT.en;
291
+
4
292
  // src/audio.ts
5
293
  import { createRng } from "@adatechnology/game-weapons";
6
294
 
@@ -21,6 +309,9 @@ var MECHANICAL_SOUNDS = {
21
309
  jam: [metal(0, 700, 0.4), metal(0.08, 500, 0.35)],
22
310
  clear_start: [metal(0, 1e3, 0.3)],
23
311
  clear_end: [metal(0, 1300, 0.35)],
312
+ stock_fold: [metal(0, 800, 0.3), metal(0.18, 1200, 0.3), thud(0.3, 0.2, 600)],
313
+ light_click: [metal(0, 2400, 0.2), metal(0.03, 1800, 0.12)],
314
+ attach: [metal(0, 1400, 0.35), thud(0.04, 0.2, 700), metal(0.12, 1900, 0.25)],
24
315
  magazine_out: [metal(0, 1500, 0.35), sound(0, "reload", 0.6)],
25
316
  magazine_in: seat,
26
317
  charge: slide,
@@ -67,6 +358,14 @@ var mechanicalSoundOf = (event) => {
67
358
  return "jam";
68
359
  case "clear":
69
360
  return event.phase === "start" ? "clear_start" : "clear_end";
361
+ case "attachment":
362
+ return event.action === "break" ? "jam" : "attach";
363
+ case "toggle":
364
+ return "light_click";
365
+ case "stock":
366
+ return "stock_fold";
367
+ case "underbarrel":
368
+ return "mode";
70
369
  default:
71
370
  return void 0;
72
371
  }
@@ -315,7 +614,42 @@ function createWeaponAudio(volume = 0.6, options = {}) {
315
614
  import { createRng as createRng2, muzzleEnergy as muzzleEnergy2 } from "@adatechnology/game-weapons";
316
615
 
317
616
  // src/fx/layers.ts
318
- var createLayers = () => ({ casings: true, magazines: true, muzzleFlash: true });
617
+ var createLayers = () => ({ casings: true, magazines: true, muzzleFlash: true, attachments: true, beams: true });
618
+
619
+ // src/laser-raycast.ts
620
+ var VIEWMODEL_NODE_NAME = "weapon-viewmodel";
621
+ var IN_HAND_NODE_NAME = "weapon-in-hand";
622
+ var LASER_RAYCAST_HZ = 30;
623
+ var isHeld = (object) => {
624
+ for (let node = object; node; node = node.parent) if (node.name === VIEWMODEL_NODE_NAME || node.name === IN_HAND_NODE_NAME) return true;
625
+ return false;
626
+ };
627
+ var isVisibleInScene = (object) => {
628
+ for (let node = object; node; node = node.parent) if (!node.visible) return false;
629
+ return true;
630
+ };
631
+ var createLaserRaycast = (THREE, scene, camera, options = {}) => {
632
+ const now = options.now ?? (() => performance.now());
633
+ const interval = 1e3 / LASER_RAYCAST_HZ;
634
+ const raycaster = new THREE.Raycaster();
635
+ raycaster.camera = camera;
636
+ let lastAt = -Infinity;
637
+ let lastReach = null;
638
+ return (origin, direction, maxDistance) => {
639
+ const at = now();
640
+ if (at - lastAt < interval) return lastReach;
641
+ lastAt = at;
642
+ raycaster.set(origin, direction);
643
+ raycaster.far = maxDistance;
644
+ const hit = raycaster.intersectObjects(scene.children, true).find((entry) => {
645
+ const mesh = entry.object;
646
+ const material = mesh.material;
647
+ return mesh.isMesh && isVisibleInScene(mesh) && material?.transparent !== true && !isHeld(mesh);
648
+ });
649
+ lastReach = hit ? hit.distance : null;
650
+ return lastReach;
651
+ };
652
+ };
319
653
 
320
654
  // src/merge-by-material.ts
321
655
  var ATTRIBUTES = ["position", "normal", "uv"];
@@ -1510,29 +1844,417 @@ var models_default = {
1510
1844
  lt: "flameDIY"
1511
1845
  }
1512
1846
  },
1513
- browning_m2: {
1514
- builder: "m2",
1515
- caliber: ".50 BMG",
1516
- length: 1650,
1517
- barrel: 1140,
1847
+ browning_m2: {
1848
+ builder: "m2",
1849
+ caliber: ".50 BMG",
1850
+ length: 1650,
1851
+ barrel: 1140,
1852
+ options: {
1853
+ rpm: 550
1854
+ }
1855
+ },
1856
+ remington_700_300wm: {
1857
+ builder: "rifle",
1858
+ caliber: ".308",
1859
+ length: 1100,
1860
+ barrel: 610,
1861
+ options: {
1862
+ wood: 1,
1863
+ stock: "trad",
1864
+ mag: "internal",
1865
+ scope: 3,
1866
+ bolt: 1,
1867
+ round: 1
1868
+ },
1869
+ variantOf: "remington_700"
1870
+ },
1871
+ hk_mp5sd3: {
1872
+ builder: "rifle",
1873
+ caliber: "9\xD719",
1874
+ length: 805,
1875
+ barrel: 146,
1876
+ options: {
1877
+ mag: "curved",
1878
+ magLen: 0.72,
1879
+ stock: "retract",
1880
+ stockLen: 0.22,
1881
+ hR: 0.055,
1882
+ hg: "poly",
1883
+ hgFrac: 0.85,
1884
+ g3: 1,
1885
+ rpm: 700,
1886
+ sel: 1,
1887
+ jacket: 1,
1888
+ noFH: 1,
1889
+ br: 0.0115
1890
+ },
1891
+ variantOf: "hk_mp5a3"
1892
+ },
1893
+ fn_parafal: {
1894
+ builder: "rifle",
1895
+ caliber: "7,62\xD751",
1896
+ length: 1020,
1897
+ barrel: 440,
1898
+ options: {
1899
+ stock: "fold",
1900
+ mag: "box",
1901
+ curve: 0.05,
1902
+ magLen: 1.05,
1903
+ hg: "poly",
1904
+ hgFrac: 0.45,
1905
+ carry: 1,
1906
+ brake: 1,
1907
+ charge: 1,
1908
+ rpm: 650,
1909
+ stockLen: 0.25
1910
+ },
1911
+ variantOf: "fn_fal"
1912
+ },
1913
+ taurus_t4_civil: {
1914
+ builder: "rifle",
1915
+ caliber: "5,56\xD745",
1916
+ length: 878,
1917
+ barrel: 406,
1918
+ options: {
1919
+ stock: "retract",
1920
+ mag: "box",
1921
+ curve: 0.07,
1922
+ magLen: 1.3,
1923
+ hg: "rail",
1924
+ hgFrac: 0.7,
1925
+ rail: 1,
1926
+ scope: 1,
1927
+ ar: 1,
1928
+ rpm: 120
1929
+ },
1930
+ variantOf: "taurus_t4"
1931
+ },
1932
+ taurus_th380: {
1933
+ builder: "pistol",
1934
+ caliber: ".380 ACP",
1935
+ length: 165,
1936
+ barrel: 90,
1937
+ options: {
1938
+ st: "g2c",
1939
+ gl: 0.085,
1940
+ rel: 1.5
1941
+ },
1942
+ variantOf: "taurus_g2c"
1943
+ },
1944
+ mossberg_500_cruiser: {
1945
+ builder: "rifle",
1946
+ caliber: "12 ga",
1947
+ length: 720,
1948
+ barrel: 470,
1949
+ options: {
1950
+ stock: "none",
1951
+ mag: "none",
1952
+ tube: 1,
1953
+ pump: 1,
1954
+ hg: "none",
1955
+ br: 0.0105,
1956
+ hR: 0.055,
1957
+ noFH: 1
1958
+ },
1959
+ variantOf: "mossberg_500"
1960
+ },
1961
+ hk_mp5k: {
1962
+ builder: "rifle",
1963
+ caliber: "9\xD719",
1964
+ length: 325,
1965
+ barrel: 115,
1966
+ options: {
1967
+ mag: "curved",
1968
+ magLen: 0.72,
1969
+ stock: "none",
1970
+ stockLen: 0.22,
1971
+ hR: 0.055,
1972
+ hg: "poly",
1973
+ hgFrac: 0.5,
1974
+ g3: 1,
1975
+ rpm: 900,
1976
+ sel: 1
1977
+ },
1978
+ variantOf: "hk_mp5a3"
1979
+ },
1980
+ hk_ump9: {
1981
+ builder: "rifle",
1982
+ caliber: "9\xD719",
1983
+ length: 690,
1984
+ barrel: 200,
1985
+ options: {
1986
+ mag: "curved",
1987
+ magLen: 1,
1988
+ stock: "skeleton",
1989
+ stockLen: 0.22,
1990
+ hR: 0.07,
1991
+ hg: "poly",
1992
+ hgFrac: 0.85,
1993
+ rpm: 700,
1994
+ sel: 1,
1995
+ rail: 1
1996
+ },
1997
+ variantOf: "hk_ump_45"
1998
+ },
1999
+ mini_uzi: {
2000
+ builder: "rifle",
2001
+ caliber: "9\xD719",
2002
+ length: 600,
2003
+ barrel: 197,
2004
+ options: {
2005
+ mag: "box",
2006
+ magAt: 0.06,
2007
+ magLen: 0.9,
2008
+ stock: "fold",
2009
+ stockLen: 0.2,
2010
+ hR: 0.065,
2011
+ hg: "poly",
2012
+ hgFrac: 0.4,
2013
+ grip: "none",
2014
+ rpm: 950
2015
+ },
2016
+ variantOf: "uzi"
2017
+ },
2018
+ hk_mp7a1: {
2019
+ builder: "rifle",
2020
+ caliber: "4,6\xD730",
2021
+ length: 638,
2022
+ barrel: 180,
2023
+ options: {
2024
+ mag: "box",
2025
+ magAt: 0.1,
2026
+ magLen: 0.8,
2027
+ stock: "retract",
2028
+ stockLen: 0.2,
2029
+ hR: 0.07,
2030
+ hg: "rail",
2031
+ hgFrac: 0.45,
2032
+ grip: "none",
2033
+ rail: 1,
2034
+ rpm: 950,
2035
+ sel: 1
2036
+ }
2037
+ },
2038
+ hk_g36: {
2039
+ builder: "rifle",
2040
+ caliber: "5,56\xD745",
2041
+ length: 999,
2042
+ barrel: 480,
2043
+ options: {
2044
+ stock: "fold",
2045
+ stockLen: 0.24,
2046
+ mag: "box",
2047
+ curve: 0.05,
2048
+ magLen: 1,
2049
+ hg: "poly",
2050
+ hgFrac: 0.5,
2051
+ hR: 0.065,
2052
+ charge: 1,
2053
+ sel: 1,
2054
+ rpm: 750,
2055
+ carry: 1,
2056
+ scope: 2
2057
+ }
2058
+ },
2059
+ hk_g36k: {
2060
+ builder: "rifle",
2061
+ caliber: "5,56\xD745",
2062
+ length: 860,
2063
+ barrel: 318,
2064
+ options: {
2065
+ stock: "fold",
2066
+ stockLen: 0.24,
2067
+ mag: "box",
2068
+ curve: 0.05,
2069
+ magLen: 1,
2070
+ hg: "poly",
2071
+ hgFrac: 0.5,
2072
+ hR: 0.065,
2073
+ charge: 1,
2074
+ sel: 1,
2075
+ rpm: 750,
2076
+ carry: 1,
2077
+ scope: 2
2078
+ }
2079
+ },
2080
+ hk_g36c: {
2081
+ builder: "rifle",
2082
+ caliber: "5,56\xD745",
2083
+ length: 720,
2084
+ barrel: 228,
2085
+ options: {
2086
+ stock: "fold",
2087
+ stockLen: 0.24,
2088
+ mag: "box",
2089
+ curve: 0.05,
2090
+ magLen: 1,
2091
+ hg: "poly",
2092
+ hgFrac: 0.5,
2093
+ hR: 0.065,
2094
+ charge: 1,
2095
+ sel: 1,
2096
+ rpm: 750,
2097
+ rail: 1
2098
+ }
2099
+ },
2100
+ fn_m249: {
2101
+ builder: "rifle",
2102
+ caliber: "5,56\xD745",
2103
+ length: 1040,
2104
+ barrel: 465,
2105
+ options: {
2106
+ stock: "fixed",
2107
+ mag: "belt",
2108
+ hg: "poly",
2109
+ hgFrac: 0.35,
2110
+ bipod: 1,
2111
+ hR: 0.08,
2112
+ carry: 1,
2113
+ charge: 1,
2114
+ rpm: 850
2115
+ },
2116
+ variantOf: "fn_minimi"
2117
+ },
2118
+ fn_minimi_para: {
2119
+ builder: "rifle",
2120
+ caliber: "5,56\xD745",
2121
+ length: 893,
2122
+ barrel: 349,
2123
+ options: {
2124
+ stock: "retract",
2125
+ mag: "belt",
2126
+ hg: "poly",
2127
+ hgFrac: 0.35,
2128
+ bipod: 1,
2129
+ hR: 0.08,
2130
+ carry: 1,
2131
+ charge: 1,
2132
+ rpm: 850,
2133
+ stockLen: 0.22
2134
+ },
2135
+ variantOf: "fn_minimi"
2136
+ },
2137
+ fn_mag: {
2138
+ builder: "rifle",
2139
+ caliber: "7,62\xD751",
2140
+ length: 1263,
2141
+ barrel: 630,
2142
+ options: {
2143
+ stock: "fixed",
2144
+ mag: "belt",
2145
+ hg: "poly",
2146
+ hgFrac: 0.35,
2147
+ bipod: 1,
2148
+ carry: 1,
2149
+ hR: 0.085,
2150
+ br: 0.011,
2151
+ charge: 1,
2152
+ rpm: 750
2153
+ }
2154
+ },
2155
+ rpk: {
2156
+ builder: "rifle",
2157
+ caliber: "7,62\xD739",
2158
+ length: 1040,
2159
+ barrel: 590,
2160
+ options: {
2161
+ wood: 1,
2162
+ stock: "wood",
2163
+ mag: "curved",
2164
+ hg: "wood",
2165
+ hgFrac: 0.42,
2166
+ gas: 1,
2167
+ charge: 1,
2168
+ ak: 1,
2169
+ rod: 1,
2170
+ bayo: 0,
2171
+ rpm: 600,
2172
+ rel: 0.4,
2173
+ bipod: 1,
2174
+ magLen: 1.4
2175
+ },
2176
+ variantOf: "ak_47"
2177
+ },
2178
+ sig_mpx: {
2179
+ builder: "rifle",
2180
+ caliber: "9\xD719",
2181
+ length: 610,
2182
+ barrel: 203,
2183
+ options: {
2184
+ mag: "box",
2185
+ curve: 0.05,
2186
+ magLen: 1,
2187
+ stock: "retract",
2188
+ stockLen: 0.2,
2189
+ hR: 0.06,
2190
+ hg: "rail",
2191
+ hgFrac: 0.7,
2192
+ rail: 1,
2193
+ charge: 1,
2194
+ sel: 1,
2195
+ rpm: 850
2196
+ }
2197
+ },
2198
+ mg3: {
2199
+ builder: "rifle",
2200
+ caliber: "7,62\xD751",
2201
+ length: 1225,
2202
+ barrel: 565,
2203
+ options: {
2204
+ stock: "fixed",
2205
+ mag: "belt",
2206
+ hg: "poly",
2207
+ hgFrac: 0.3,
2208
+ bipod: 1,
2209
+ carry: 1,
2210
+ hR: 0.085,
2211
+ br: 0.011,
2212
+ charge: 1,
2213
+ rpm: 1100
2214
+ }
2215
+ },
2216
+ pp19_bizon: {
2217
+ builder: "rifle",
2218
+ caliber: "9\xD718",
2219
+ length: 660,
2220
+ barrel: 195,
1518
2221
  options: {
1519
- rpm: 550
2222
+ mag: "helical",
2223
+ magLen: 1,
2224
+ stock: "fold",
2225
+ stockLen: 0.2,
2226
+ hR: 0.06,
2227
+ hg: "poly",
2228
+ hgFrac: 0.5,
2229
+ ak: 1,
2230
+ charge: 1,
2231
+ rpm: 680
1520
2232
  }
1521
2233
  },
1522
- remington_700_300wm: {
2234
+ vz61: {
1523
2235
  builder: "rifle",
1524
- caliber: ".308",
1525
- length: 1100,
1526
- barrel: 610,
2236
+ caliber: ".32 ACP",
2237
+ length: 517,
2238
+ barrel: 115,
1527
2239
  options: {
1528
- wood: 1,
1529
- stock: "trad",
1530
- mag: "internal",
1531
- scope: 3,
1532
- bolt: 1,
1533
- round: 1
1534
- },
1535
- variantOf: "remington_700"
2240
+ mag: "curved",
2241
+ magLen: 0.6,
2242
+ stock: "fold",
2243
+ stockLen: 0.2,
2244
+ hR: 0.05,
2245
+ hg: "none",
2246
+ rpm: 850,
2247
+ sel: 1
2248
+ }
2249
+ },
2250
+ m134: {
2251
+ builder: "m134",
2252
+ caliber: "7,62\xD751",
2253
+ length: 801,
2254
+ barrel: 559,
2255
+ options: {
2256
+ rpm: 3e3
2257
+ }
1536
2258
  }
1537
2259
  };
1538
2260
 
@@ -1750,6 +2472,80 @@ function buildBreakAction(kit, options) {
1750
2472
  return { group, parts };
1751
2473
  }
1752
2474
 
2475
+ // src/models/attachments/mount-frames.ts
2476
+ var DEFAULT_BARREL_RADIUS = 85e-4;
2477
+ var DEFAULT_RECEIVER_WIDTH = 0.034;
2478
+ var RECEIVER_START_FRACTION = 0.25;
2479
+ var RECEIVER_END_FRACTION = 0.55;
2480
+ var RECEIVER_HEIGHT_FRACTION = 0.4;
2481
+ var DEFAULT_GRIP_OFFSET = 0.07;
2482
+ var HANDGUARD_FRACTION = 0.5;
2483
+ function deriveFrames(THREE, model) {
2484
+ const { root, inner, parts } = model;
2485
+ root.updateMatrixWorld(true);
2486
+ const box2 = new THREE.Box3().setFromObject(root);
2487
+ const span = box2.max.x - box2.min.x;
2488
+ const toInnerX = (x) => (x - inner.position.x) / inner.scale.x;
2489
+ const muzzle = parts.muzzle?.position;
2490
+ const length = muzzle ? muzzle.x - 0.01 : toInnerX(box2.max.x);
2491
+ const receiverStart = toInnerX(box2.min.x + span * RECEIVER_START_FRACTION);
2492
+ const receiverEnd = toInnerX(box2.min.x + span * RECEIVER_END_FRACTION);
2493
+ return {
2494
+ length,
2495
+ barrelY: muzzle?.y ?? (box2.max.y + box2.min.y) / 2,
2496
+ barrelRadius: DEFAULT_BARREL_RADIUS,
2497
+ receiverStart,
2498
+ receiverEnd,
2499
+ receiverHeight: (box2.max.y - box2.min.y) * RECEIVER_HEIGHT_FRACTION,
2500
+ receiverWidth: Math.min(DEFAULT_RECEIVER_WIDTH, box2.max.z - box2.min.z),
2501
+ handguardEnd: receiverEnd + (length - receiverEnd) * HANDGUARD_FRACTION,
2502
+ magazineX: parts.mag ? parts.mag.position.x : (receiverStart + receiverEnd) / 2,
2503
+ gripX: receiverStart + DEFAULT_GRIP_OFFSET,
2504
+ hasRail: false
2505
+ };
2506
+ }
2507
+ function mountFrame(model, mount) {
2508
+ const frames = model.parts.frames;
2509
+ if (!frames) return void 0;
2510
+ const { inner } = model;
2511
+ const toRoot = (x, y, z) => [inner.position.x + x * inner.scale.x, y, z];
2512
+ const handguardHeight = frames.receiverHeight * 0.8;
2513
+ const railTopY = frames.receiverHeight / 2 + (frames.hasRail ? 6e-3 : 0);
2514
+ const forendMiddle = (frames.receiverEnd + frames.handguardEnd) / 2;
2515
+ const base = { mount, barrelRadius: frames.barrelRadius, receiverWidth: frames.receiverWidth, receiverHeight: frames.receiverHeight, hasRail: frames.hasRail };
2516
+ switch (mount) {
2517
+ case "muzzle":
2518
+ return { ...base, position: toRoot(frames.length, frames.barrelY, 0), side: 0 };
2519
+ case "rail_top":
2520
+ return { ...base, position: toRoot((frames.receiverStart + frames.receiverEnd) / 2 + 0.03, railTopY, 0), side: 0 };
2521
+ case "rail_bottom":
2522
+ return { ...base, position: toRoot(forendMiddle, frames.barrelY - handguardHeight / 2, 0), side: 0 };
2523
+ case "rail_left":
2524
+ return { ...base, position: toRoot(forendMiddle, frames.barrelY, -(frames.receiverWidth * 0.54 + 4e-3)), side: -1 };
2525
+ case "rail_right":
2526
+ return { ...base, position: toRoot(forendMiddle, frames.barrelY, frames.receiverWidth * 0.54 + 4e-3), side: 1 };
2527
+ case "underbarrel": {
2528
+ const [x, y] = frames.underbarrel ?? [frames.receiverEnd + (frames.handguardEnd - frames.receiverEnd) * 0.25, frames.barrelY - handguardHeight / 2 - 0.02];
2529
+ return { ...base, position: toRoot(x, y, 0), side: 0 };
2530
+ }
2531
+ case "magazine":
2532
+ return { ...base, position: toRoot(frames.magazineX, -frames.receiverHeight / 2, 0), side: 0 };
2533
+ case "feed": {
2534
+ const belt = model.parts.belt?.position;
2535
+ return { ...base, position: belt ? toRoot(belt.x, belt.y, belt.z) : toRoot(frames.magazineX + 0.02, -frames.receiverHeight / 2 - 0.05, -0.045), side: -1 };
2536
+ }
2537
+ case "sling":
2538
+ return {
2539
+ ...base,
2540
+ position: toRoot(frames.receiverEnd + (frames.length - frames.receiverEnd) * 0.45, -frames.receiverHeight / 2 - 0.03, 0),
2541
+ secondary: toRoot(0.08, -0.09, 0),
2542
+ side: 0
2543
+ };
2544
+ default:
2545
+ return void 0;
2546
+ }
2547
+ }
2548
+
1753
2549
  // src/models/details.ts
1754
2550
  var PI2 = Math.PI;
1755
2551
  function rail(kit, parent, x, length, y, z, width, rotationX = 0) {
@@ -2092,6 +2888,90 @@ function buildLauncher(kit, options) {
2092
2888
  return { group, parts };
2093
2889
  }
2094
2890
 
2891
+ // src/models/m134.ts
2892
+ var PI4 = Math.PI;
2893
+ var BARRELS = 6;
2894
+ var CLUSTER_RADIUS = 0.03;
2895
+ var BARREL_RADIUS = 78e-4;
2896
+ var BARREL_END = 0.8;
2897
+ function buildBarrelCluster(kit, parts, group) {
2898
+ const { M, cylinderX } = kit;
2899
+ const barrels = kit.pivot(group, 0, 0, 0);
2900
+ parts.barrels = barrels;
2901
+ const length = BARREL_END - 0.3;
2902
+ let heated;
2903
+ for (let i = 0; i < BARRELS; i++) {
2904
+ const angle = i * 2 * PI4 / BARRELS;
2905
+ const y = Math.cos(angle) * CLUSTER_RADIUS;
2906
+ const z = Math.sin(angle) * CLUSTER_RADIUS;
2907
+ const barrel = cylinderX(barrels, BARREL_RADIUS, length, M.metal, 0.3 + length / 2, y, z, 14);
2908
+ if (heated) barrel.material = heated;
2909
+ else heated = heatify(parts.glow, barrel).material;
2910
+ cylinderX(barrels, BARREL_RADIUS * 0.55, 4e-3, M.hole, BARREL_END + 5e-4, y, z, 10);
2911
+ }
2912
+ cylinderX(barrels, 0.012, length, M.steel, 0.3 + length / 2, 0, 0, 12);
2913
+ for (const x of [0.42, 0.58, 0.74]) cylinderX(barrels, CLUSTER_RADIUS + BARREL_RADIUS * 1.6, 0.014, M.steel, x, 0, 0, 24);
2914
+ cylinderX(barrels, CLUSTER_RADIUS + BARREL_RADIUS * 1.8, 0.03, M.dark, BARREL_END - 0.015, 0, 0, 24);
2915
+ }
2916
+ function buildHousing(kit, parts, group) {
2917
+ const { M, box: box2, cylinderX, cylinderZ } = kit;
2918
+ box2(group, 0.3, 0.14, 0.15, M.metal, 0.18, 0, 0);
2919
+ cylinderX(group, 0.052, 0.12, M.dark, 0.06, 0, 0, 20);
2920
+ cylinderX(group, 0.062, 0.2, M.dark, 0.38, 0, 0, 22);
2921
+ for (let i = 0; i < 4; i++) cylinderX(group, 0.064, 8e-3, M.steel, 0.3 + i * 0.05, 0, 0, 22);
2922
+ box2(group, 0.04, 0.05, 0.2, M.dark, 0.1, 0.095, 0);
2923
+ box2(group, 0.2, 0.012, 0.045, M.dark, 0.22, 0.158, 0);
2924
+ box2(group, 0.012, 0.05, 0.045, M.dark, 0.13, 0.13, 0);
2925
+ box2(group, 0.012, 0.05, 0.045, M.dark, 0.31, 0.13, 0);
2926
+ for (const side of [-1, 1]) {
2927
+ box2(group, 0.03, 0.03, 0.1, M.poly, 0.06, -0.01, side * 0.14);
2928
+ cylinderZ(group, 0.016, 0.07, M.poly, 0.06, -0.01, side * 0.2, 14);
2929
+ }
2930
+ parts.trigger = box2(group, 0.012, 0.02, 0.016, M.dark, 0.1, 0.085, 0.14, 0.3);
2931
+ parts.safety = box2(group, 0.014, 6e-3, 0.01, M.dark, 0.2, 0.073, -0.076, 0.4);
2932
+ }
2933
+ function buildFeed(kit, parts, group) {
2934
+ const { M, box: box2, cylinderX } = kit;
2935
+ const belt = kit.pivot(group, 0.2, -0.13, -0.16);
2936
+ parts.belt = belt;
2937
+ box2(belt, 0.16, 0.12, 0.1, M.olive, 0, 0, 0);
2938
+ box2(belt, 0.162, 0.012, 0.102, M.dark, 0, 0.056, 0);
2939
+ for (let i = 0; i < 8; i++) cylinderX(belt, 46e-4, 0.04, M.brass, -0.07 + i * 0.02, 0.075, 0.04, 10);
2940
+ box2(group, 0.1, 0.03, 0.1, M.metal, 0.22, -0.06, -0.1, 0, 0, 0.5);
2941
+ const cover = kit.pivot(group, 0.1, 0.075, 0);
2942
+ parts.cover = cover;
2943
+ box2(cover, 0.24, 0.012, 0.15, M.metal, 0.12, 6e-3, 0);
2944
+ const bolt = kit.pivot(group);
2945
+ parts.bolt = bolt;
2946
+ parts.stroke = 0.03;
2947
+ box2(bolt, 0.03, 0.012, 0.012, M.steelL, 0.28, 0.02, 0.078);
2948
+ }
2949
+ function buildM134(kit) {
2950
+ const group = kit.group();
2951
+ const parts = { family: "belt", kick: 0.9, glow: [] };
2952
+ buildHousing(kit, parts, group);
2953
+ buildBarrelCluster(kit, parts, group);
2954
+ buildFeed(kit, parts, group);
2955
+ parts.frames = {
2956
+ length: BARREL_END,
2957
+ barrelY: 0,
2958
+ barrelRadius: BARREL_RADIUS,
2959
+ receiverStart: 0.06,
2960
+ receiverEnd: 0.48,
2961
+ receiverHeight: 0.14,
2962
+ receiverWidth: 0.15,
2963
+ handguardEnd: 0.6,
2964
+ magazineX: 0.2,
2965
+ gripX: 0.06,
2966
+ hasRail: false,
2967
+ // the tripod head, where the gun balances (the tripod of the counters joins here too)
2968
+ underbarrel: [0.4, -0.1]
2969
+ };
2970
+ parts.muzzle = kit.mark(group, BARREL_END + 0.01, 0, 0);
2971
+ parts.eject = kit.mark(group, 0.3, -0.07, 0.08);
2972
+ return { group, parts };
2973
+ }
2974
+
2095
2975
  // src/models/m2.ts
2096
2976
  function buildM2(kit) {
2097
2977
  const { M, box: box2, cylinderX, cylinderZ } = kit;
@@ -2157,7 +3037,7 @@ function buildP90(kit) {
2157
3037
  }
2158
3038
 
2159
3039
  // 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"];
3040
+ var SINGLE_PARTS = ["slide", "bolt", "boltHandle", "mag", "trigger", "hammer", "lever", "pump", "belt", "cover", "crane", "cylinder", "barrels", "safety", "muzzle", "eject", "loadPort", "topLoad", "rear", "round", "gauge", "stock", "ugl"];
2161
3041
  function partRegistry(parts) {
2162
3042
  const registry = /* @__PURE__ */ new Map();
2163
3043
  for (const key of SINGLE_PARTS) {
@@ -2170,7 +3050,7 @@ function partRegistry(parts) {
2170
3050
  }
2171
3051
 
2172
3052
  // src/models/pistol-slide.ts
2173
- var PI4 = Math.PI;
3053
+ var PI5 = Math.PI;
2174
3054
  function buildSlide(kit, slide2, parts, options, style, size, metal2) {
2175
3055
  const { M, box: box2, cylinderX } = kit;
2176
3056
  const { length: L, width: w } = size;
@@ -2198,8 +3078,8 @@ function buildSlide(kit, slide2, parts, options, style, size, metal2) {
2198
3078
  if (style.tri) {
2199
3079
  const tube = new kit.THREE.Mesh(new kit.THREE.CylinderGeometry(0.016, 0.016, L * 0.6, 3), metal2);
2200
3080
  tube.castShadow = tube.receiveShadow = true;
2201
- tube.rotation.set(0, 0, -PI4 / 2);
2202
- tube.rotateY(PI4 / 6);
3081
+ tube.rotation.set(0, 0, -PI5 / 2);
3082
+ tube.rotateY(PI5 / 6);
2203
3083
  tube.position.set(L * 0.7, slideHeight * 0.08, 0);
2204
3084
  slide2.add(tube);
2205
3085
  heatify(parts.glow, tube);
@@ -2251,7 +3131,7 @@ var STYLES = {
2251
3131
  };
2252
3132
 
2253
3133
  // src/models/pistol.ts
2254
- var PI5 = Math.PI;
3134
+ var PI6 = Math.PI;
2255
3135
  function buildFrame(kit, group, parts, options, style, size, materials) {
2256
3136
  const { M, box: box2, cylinderZ, sphere } = kit;
2257
3137
  const { length: L, height: h, width: w } = size;
@@ -2342,8 +3222,8 @@ function buildPistol(kit, options) {
2342
3222
  box2(group, 4e-3, 0.03, 9e-3, frameMetal, gx + 0.073, ty - 0.02, 0, 0.2);
2343
3223
  box2(group, 8e-3, 6e-3, 9e-3, frameMetal, gx + 0.074, ty - 0.037, 0, 0.4);
2344
3224
  } 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;
3225
+ const guard = kit.torus(group, new kit.THREE.TorusGeometry(0.019, 22e-4, 6, 16, PI6), frameMetal, gx + 0.05, ty - 0.014, 0);
3226
+ guard.rotation.z = PI6;
2347
3227
  guard.scale.set(1.2, 1, 1);
2348
3228
  }
2349
3229
  const isGlock = options.st === "glock" || !options.st;
@@ -2367,7 +3247,7 @@ function buildPistol(kit, options) {
2367
3247
  }
2368
3248
 
2369
3249
  // src/models/revolver.ts
2370
- var PI6 = Math.PI;
3250
+ var PI7 = Math.PI;
2371
3251
  var DEFAULT_CHAMBERS = 6;
2372
3252
  function buildRevolver(kit, options) {
2373
3253
  const { M, box: box2, cylinderX } = kit;
@@ -2385,12 +3265,12 @@ function buildRevolver(kit, options) {
2385
3265
  parts.cylinder = cylinder;
2386
3266
  cylinderX(cylinder, 0.0215, 0.042, metal2, 0, 0, 0, 24);
2387
3267
  for (let i = 0; i < chambers; i++) {
2388
- const angle = i * 2 * PI6 / chambers;
3268
+ const angle = i * 2 * PI7 / chambers;
2389
3269
  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
3270
  }
2391
3271
  parts.chambers = [];
2392
3272
  for (let i = 0; i < chambers; i++) {
2393
- const angle = i * 2 * PI6 / chambers + PI6 / chambers;
3273
+ const angle = i * 2 * PI7 / chambers + PI7 / chambers;
2394
3274
  parts.chambers.push(kit.mark(cylinder, -0.022, Math.cos(angle) * 0.013, Math.sin(angle) * 0.013));
2395
3275
  cylinderX(cylinder, 46e-4, 2e-3, M.brass, -0.0215, Math.cos(angle) * 0.013, Math.sin(angle) * 0.013, 12);
2396
3276
  }
@@ -2414,27 +3294,55 @@ function buildRevolver(kit, options) {
2414
3294
  return { group, parts };
2415
3295
  }
2416
3296
 
3297
+ // src/models/attachments/muzzle-brake.ts
3298
+ function buildBrake(kit, parent, kind, x, y, barrelRadius, heat = () => void 0) {
3299
+ const { M, box: box2, cylinderX } = kit;
3300
+ const scale = kind === 2 ? 3.2 : 2;
3301
+ const length = kind === 2 ? 0.12 : 0.06;
3302
+ heat(cylinderX(parent, barrelRadius * scale, length, M.dark, x - length / 2, y, 0, kind === 2 ? 8 : 16));
3303
+ for (let i = 0; i < 3; i++) box2(parent, length * 0.16, barrelRadius * scale * 1.3, barrelRadius * scale * 2.2, M.hole, x - length * 0.25 - i * length * 0.25, y, 0);
3304
+ }
3305
+
3306
+ // src/models/capture.ts
3307
+ function captured(parent, draw) {
3308
+ const before = parent.children.length;
3309
+ draw();
3310
+ return parent.children.slice(before);
3311
+ }
3312
+ function recordFitting(parts, id, parent, draw) {
3313
+ parts.fittings = { ...parts.fittings, [id]: captured(parent, draw) };
3314
+ }
3315
+
3316
+ // src/models/attachments/bipod.ts
3317
+ function buildBipod(kit, parent, x, y) {
3318
+ const { M, box: box2, cylinderX } = kit;
3319
+ cylinderX(parent, 8e-3, 0.02, M.dark, x, y, 0, 12);
3320
+ for (const side of [-1, 1]) {
3321
+ const leg = kit.pivot(parent, x, y, side * 0.012);
3322
+ leg.rotation.set(side * 0.15, 0, -0.35);
3323
+ box2(leg, 6e-3, 0.2, 6e-3, M.dark, 0, -0.1, 0);
3324
+ box2(leg, 0.01, 6e-3, 0.01, M.rubber, 0, -0.2, 0);
3325
+ }
3326
+ }
3327
+ var buildBipodModel = ({ kit }) => {
3328
+ const group = kit.group();
3329
+ buildBipod(kit, group, 0, 0);
3330
+ return group;
3331
+ };
3332
+
2417
3333
  // src/models/rifle-fittings.ts
3334
+ var SCOPE_IDS = { 1: "red_dot", 2: "acog_4x", 3: "scope_3_9x", 4: "scope_6_24x" };
2418
3335
  function buildFittings(context, gripX) {
2419
3336
  const { kit, group, parts, options: o, furniture: furn, length: L, receiverStart: x0, receiverEnd: x1, receiverHeight: hR, receiverWidth: wR, barrelRadius: r, barrelY: by } = context;
2420
3337
  const { M, box: box2, cylinderX, cylinderZ, sphere } = kit;
2421
3338
  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);
3339
+ if (o.scope) recordFitting(parts, SCOPE_IDS[o.scope] ?? "scope", group, () => scope(kit, group, o.scope, middle + 0.03, hR / 2 + (o.rail ? 6e-3 : 0), o.scope === 4 ? -0.02 : 0));
2423
3340
  if (o.carry) {
2424
3341
  box2(group, 0.15, 0.026, 0.014, M.metal, middle, hR / 2 + 0.03, 0);
2425
3342
  box2(group, 0.012, 0.03, 0.014, M.metal, middle + 0.07, hR / 2 + 0.014, 0);
2426
3343
  box2(group, 0.012, 0.03, 0.014, M.metal, middle - 0.07, hR / 2 + 0.014, 0);
2427
3344
  }
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
- }
3345
+ if (o.bipod) recordFitting(parts, "bipod", group, () => buildBipod(kit, group, x1 + (L - x1) * 0.45 + 0.04, by - 0.012));
2438
3346
  if (o.bolt) {
2439
3347
  const handle = kit.pivot(group, x1 - 0.045, by, 0);
2440
3348
  parts.boltHandle = handle;
@@ -2472,7 +3380,7 @@ function buildFittings(context, gripX) {
2472
3380
  }
2473
3381
 
2474
3382
  // src/models/rifle-forend.ts
2475
- var PI7 = Math.PI;
3383
+ var PI8 = Math.PI;
2476
3384
  function buildForend(context) {
2477
3385
  const { kit, group, options: o, furniture: furn, length: L, receiverEnd: x1, receiverHeight: hR, receiverWidth: wR, barrelRadius: r, barrelY: by } = context;
2478
3386
  const { M, box: box2 } = kit;
@@ -2484,9 +3392,9 @@ function buildForend(context) {
2484
3392
  if (o.hg === "rail") {
2485
3393
  box2(group, handguardLength, hh, wR * 1.04, M.dark, hx, by - 2e-3, 0);
2486
3394
  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);
3395
+ rail(kit, group, x1 + 0.01, handguardLength - 0.02, by - 2e-3, wR * 0.54, wR * 0.4, PI8 / 2);
3396
+ rail(kit, group, x1 + 0.01, handguardLength - 0.02, by - 2e-3, -wR * 0.54, wR * 0.4, -PI8 / 2);
3397
+ rail(kit, group, x1 + 0.01, handguardLength - 0.02, by - hh / 2, 0, wR * 0.6, PI8);
2490
3398
  } else {
2491
3399
  const material = o.hg === "wood" ? M.wood : furn;
2492
3400
  if (o.ak) {
@@ -2516,6 +3424,46 @@ function buildForend(context) {
2516
3424
  } else box2(group, 0.012, 0.024, 0.012, M.dark, sightX - 0.04, by + hR * 0.4 + 0.012, 0);
2517
3425
  }
2518
3426
 
3427
+ // src/models/attachments/belt-box.ts
3428
+ function buildBeltBox(kit, belt) {
3429
+ const { M, box: box2, cylinderX } = kit;
3430
+ box2(belt, 0.11, 0.1, 0.08, M.olive, 0, 0, 0);
3431
+ box2(belt, 0.112, 0.012, 0.082, M.dark, 0, 0.046, 0);
3432
+ for (let i = 0; i < 7; i++) {
3433
+ cylinderX(belt, 42e-4, 0.034, M.brass, -0.04 + i * 0.012, 0.07, 0.035, 10);
3434
+ box2(belt, 6e-3, 6e-3, 0.012, M.metal, -0.04 + i * 0.012, 0.066, 0.035);
3435
+ }
3436
+ }
3437
+
3438
+ // src/models/attachments/drum.ts
3439
+ function buildDrum(kit, magazine) {
3440
+ const { M, box: box2, cylinderZ } = kit;
3441
+ cylinderZ(magazine, 0.075, 0.07, M.dark, 0.02, -0.085, 0, 22);
3442
+ cylinderZ(magazine, 0.05, 0.074, M.metal, 0.02, -0.085, 0, 18);
3443
+ box2(magazine, 0.05, 0.04, 0.03, M.dark, 0, -0.02, 0);
3444
+ }
3445
+
3446
+ // src/models/helical-magazine.ts
3447
+ var PI9 = Math.PI;
3448
+ var LENGTH = 0.26;
3449
+ var RADIUS = 0.026;
3450
+ var TURNS = 11;
3451
+ function buildHelicalMagazine(context) {
3452
+ const { kit, group, parts, receiverEnd, barrelY, barrelRadius } = context;
3453
+ const { M, cylinderX, box: box2 } = kit;
3454
+ const x = receiverEnd + LENGTH / 2 + 0.02;
3455
+ const y = barrelY - barrelRadius - RADIUS - 0.012;
3456
+ const magazine = kit.pivot(group, x, y, 0);
3457
+ parts.mag = magazine;
3458
+ cylinderX(magazine, RADIUS, LENGTH, M.dark, 0, 0, 0, 20);
3459
+ cylinderX(magazine, RADIUS * 1.06, 0.01, M.metal, -LENGTH / 2, 0, 0, 20);
3460
+ cylinderX(magazine, RADIUS * 1.06, 0.01, M.metal, LENGTH / 2, 0, 0, 20);
3461
+ for (let i = 0; i < TURNS; i++) box2(magazine, 0.012, RADIUS * 2.15, 0.014, M.metal, -LENGTH / 2 + (i + 0.5) * LENGTH / TURNS, 0, 0, 0, 0, i * PI9 / 4);
3462
+ box2(magazine, 0.05, 0.03, 0.03, M.dark, -LENGTH / 2 - 0.02, RADIUS * 0.4, 0);
3463
+ parts.magDrop = 0.16;
3464
+ return x;
3465
+ }
3466
+
2519
3467
  // src/models/rifle-controls.ts
2520
3468
  function buildGrip(context) {
2521
3469
  const { kit, group, parts, options: o, furniture: furn, receiverStart: x0, receiverHeight: hR, receiverWidth: wR } = context;
@@ -2536,7 +3484,8 @@ function buildGrip(context) {
2536
3484
  }
2537
3485
  function buildMagazine(context, gx) {
2538
3486
  const { kit, group, parts, options: o, receiverStart: x0, receiverHeight: hR, receiverWidth: wR, receiverLength: RL } = context;
2539
- const { M, box: box2, cylinderX, cylinderZ } = kit;
3487
+ const { M, box: box2 } = kit;
3488
+ if (o.mag === "helical") return buildHelicalMagazine(context);
2540
3489
  const mx = o.magAt != null ? x0 + o.magAt : gx + 0.075;
2541
3490
  const scale = o.magLen || 1;
2542
3491
  const magazine = kit.pivot(group, mx, -hR / 2, 0);
@@ -2577,9 +3526,7 @@ function buildMagazine(context, gx) {
2577
3526
  box2(magazine, 0.056, 7e-3, 0.028, material, px, py - 2e-3, 0, angle);
2578
3527
  parts.magDrop = length + 0.03;
2579
3528
  } 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);
3529
+ buildDrum(kit, magazine);
2583
3530
  parts.magDrop = 0.2;
2584
3531
  } else if (o.mag === "internal") {
2585
3532
  box2(group, 0.08, 0.025, 0.03, context.isTraditional ? context.furniture : M.metal, mx, -hR / 2 - 0.012, 0);
@@ -2588,12 +3535,7 @@ function buildMagazine(context, gx) {
2588
3535
  if (o.mag === "belt") {
2589
3536
  const belt = kit.pivot(group, mx + 0.02, -hR / 2 - 0.05, -0.045);
2590
3537
  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
- }
3538
+ buildBeltBox(kit, belt);
2597
3539
  const cover = kit.pivot(group, x0 + 0.01, hR / 2, 0);
2598
3540
  parts.cover = cover;
2599
3541
  box2(cover, RL - 0.03, 0.014, wR * 1.05, M.metal, (RL - 0.03) / 2, 7e-3, 0);
@@ -2643,7 +3585,7 @@ function buildStock(context, magazineX) {
2643
3585
  }
2644
3586
 
2645
3587
  // src/models/rifle.ts
2646
- var PI8 = Math.PI;
3588
+ var PI10 = Math.PI;
2647
3589
  function buildReceiver(context) {
2648
3590
  const { kit, group, parts, options: o, length: L, receiverStart: x0, receiverEnd: x1, receiverHeight: hR, receiverWidth: wR, receiverLength: RL, barrelY: by } = context;
2649
3591
  const { M, box: box2, cylinderX, cylinderY, cylinderZ, sphere } = kit;
@@ -2696,16 +3638,16 @@ function buildBarrel(context) {
2696
3638
  }
2697
3639
  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
3640
  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);
3641
+ const kind = o.brake === 2 ? 2 : 1;
3642
+ recordFitting(parts, kind === 2 ? "muzzle_brake_heavy" : "muzzle_brake", group, () => buildBrake(kit, group, kind, L, by, r, (body) => void heatify(parts.glow, body)));
2703
3643
  } 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
- }
3644
+ recordFitting(parts, "muzzle_default", group, () => {
3645
+ heatify(parts.glow, cylinderX(group, r * 1.45, 0.042, M.dark, L - 0.021, by, 0, 16));
3646
+ for (let i = 0; i < 4; i++) {
3647
+ const angle = i * PI10 / 2 + PI10 / 4;
3648
+ 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);
3649
+ }
3650
+ });
2709
3651
  }
2710
3652
  cylinderX(group, r * 0.55, 4e-3, M.hole, L + 5e-4, by, 0, 12);
2711
3653
  }
@@ -2751,8 +3693,23 @@ function buildRifle(kit, options) {
2751
3693
  buildForend(context);
2752
3694
  const gripX = buildGrip(context);
2753
3695
  const magazineX = buildMagazine(context, gripX);
2754
- buildStock(context, magazineX);
3696
+ parts.stockMeshes = context.isTraditional ? [] : captured(group, () => buildStock(context, magazineX));
3697
+ if (context.isTraditional) buildStock(context, magazineX);
2755
3698
  buildFittings(context, gripX);
3699
+ const hasHandguard = !context.isTraditional && options.hg !== "none";
3700
+ parts.frames = {
3701
+ length,
3702
+ barrelY: context.barrelY,
3703
+ barrelRadius: context.barrelRadius,
3704
+ receiverStart,
3705
+ receiverEnd,
3706
+ receiverHeight,
3707
+ receiverWidth: context.receiverWidth,
3708
+ handguardEnd: receiverEnd + (length - receiverEnd) * (hasHandguard ? options.hgFrac || 0.5 : 0.4),
3709
+ magazineX,
3710
+ gripX,
3711
+ hasRail: Boolean(options.rail)
3712
+ };
2756
3713
  const { barrelY: by, receiverWidth: wR } = context;
2757
3714
  const ejectX = receiverEnd - (options.ejectBack || 0.07);
2758
3715
  const ejectY = options.ar ? receiverHeight * 0.2 : by;
@@ -2784,6 +3741,7 @@ var BUILDERS = {
2784
3741
  p90: (kit) => buildP90(kit),
2785
3742
  break: buildBreakAction,
2786
3743
  m2: (kit) => buildM2(kit),
3744
+ m134: (kit) => buildM134(kit),
2787
3745
  launcher: buildLauncher
2788
3746
  };
2789
3747
  var nameParts = (parts) => {
@@ -2814,9 +3772,26 @@ function buildWeaponModelById(THREE, id) {
2814
3772
  built.group.position.x = -new THREE.Box3().setFromObject(built.group).min.x;
2815
3773
  } else root = fitLength(kit, built.group, entry.length / 1e3);
2816
3774
  root.name = id;
2817
- return { id, root, inner: built.group, parts: built.parts };
3775
+ const model = { id, root, inner: built.group, parts: built.parts };
3776
+ if (!built.parts.frames) built.parts.frames = deriveFrames(THREE, model);
3777
+ return model;
2818
3778
  }
2819
3779
 
3780
+ // src/models/attachments/tripod.ts
3781
+ var LEG = { length: 0.62, width: 0.012 };
3782
+ function buildTripodLegs(kit, parent) {
3783
+ const { length, width } = LEG;
3784
+ kit.box(parent, 0.03, 0.06, 0.03, kit.M.metal, 0, 0, 0);
3785
+ kit.box(parent, width, length, width, kit.M.metal, -0.2, -0.25, 0, 0.75);
3786
+ kit.box(parent, width, length, width, kit.M.metal, 0.22, -0.25, 0.16, -0.75, 0, 0.3);
3787
+ kit.box(parent, width, length, width, kit.M.metal, 0.22, -0.25, -0.16, -0.75, 0, -0.3);
3788
+ }
3789
+ var buildTripod = ({ kit }) => {
3790
+ const group = kit.group();
3791
+ buildTripodLegs(kit, group);
3792
+ return group;
3793
+ };
3794
+
2820
3795
  // src/props.ts
2821
3796
  var box = (id, material, size, position, extra = {}) => ({ id, material, shape: { kind: "box", size }, position, ...extra });
2822
3797
  var joint = (a, b, kind) => ({ a, b, kind });
@@ -2889,9 +3864,10 @@ function buildWeaponModel(THREE, def) {
2889
3864
  return buildProp(THREE, weaponProp(def));
2890
3865
  }
2891
3866
  var MODEL_BODY = "body";
2892
- function buildModeledProp(THREE, def, name) {
3867
+ function buildModeledProp(THREE, def, name, decorate) {
2893
3868
  const model = buildWeaponModelById(THREE, def.id);
2894
3869
  if (!model) return void 0;
3870
+ decorate?.(model.root);
2895
3871
  const group = new THREE.Group();
2896
3872
  group.name = name;
2897
3873
  const box2 = new THREE.Box3().setFromObject(model.root);
@@ -2908,6 +3884,13 @@ function buildModeledProp(THREE, def, name) {
2908
3884
  group.userData.footprint = [size.x, size.y, size.z];
2909
3885
  return group;
2910
3886
  }
3887
+ var TRIPOD_HEAD = { x: 0.4, y: -0.1 };
3888
+ function buildTripodMountedProp(THREE, def, name) {
3889
+ return buildModeledProp(THREE, def, name, (root) => {
3890
+ const kit = modelKit(THREE);
3891
+ buildTripodLegs(kit, kit.pivot(root, TRIPOD_HEAD.x, TRIPOD_HEAD.y, 0));
3892
+ });
3893
+ }
2911
3894
  function modeledPropSpec(root, def, name) {
2912
3895
  const footprint = root.userData.footprint ?? [0.3, 0.1, 0.1];
2913
3896
  const size = [Math.max(0.02, footprint[0]), Math.max(0.02, footprint[1]), Math.max(0.02, footprint[2])];
@@ -2934,7 +3917,7 @@ function createBoxFx(options) {
2934
3917
  const shell = new THREE.MeshStandardMaterial({ color: 11022891, roughness: 0.6 });
2935
3918
  const steel = new THREE.MeshStandardMaterial({ color: 3093047, metalness: 0.5, roughness: 0.5 });
2936
3919
  const hand = new THREE.Group();
2937
- hand.name = "weapon-in-hand";
3920
+ hand.name = IN_HAND_NODE_NAME;
2938
3921
  scene.add(hand);
2939
3922
  let model = null;
2940
3923
  let inHand = null;
@@ -3154,6 +4137,42 @@ function composeClip(id, tweens, events = [], minDuration = 0) {
3154
4137
  }
3155
4138
  var renamed = (clip, id) => ({ ...clip, id });
3156
4139
 
4140
+ // src/animation/accessory-clips.ts
4141
+ var PI11 = Math.PI;
4142
+ var STOCK_SECONDS = 0.6;
4143
+ var TOGGLE_SECONDS = 0.14;
4144
+ var TOGGLE_TILT = 0.02;
4145
+ var LAUNCHER_SLIDE = 0.05;
4146
+ var LAUNCHER_RELOAD = { open: 0, loaded: 0.4, close: 0.8, end: 1.05, slide: 0.3, back: 0.25 };
4147
+ var LAUNCHER_KICK = 0.03;
4148
+ function stockClips(context) {
4149
+ const { stock, stockMove } = context.parts;
4150
+ if (!stock || !stockMove) return [];
4151
+ if (stockMove.kind === "fold") {
4152
+ return [
4153
+ composeClip("stock_fold", [turn("stock", stockMove.axis, 0, STOCK_SECONDS, 0, PI11)]),
4154
+ composeClip("stock_unfold", [turn("stock", stockMove.axis, 0, STOCK_SECONDS, PI11, 0)])
4155
+ ];
4156
+ }
4157
+ return [
4158
+ composeClip("stock_collapse", [move("stock", "x", 0, STOCK_SECONDS, 0, stockMove.travel)]),
4159
+ composeClip("stock_extend", [move("stock", "x", 0, STOCK_SECONDS, stockMove.travel, 0)])
4160
+ ];
4161
+ }
4162
+ var toggleClip = () => composeClip("toggle", [turn(ROOT, "z", 0, TOGGLE_SECONDS / 2, 0, TOGGLE_TILT), turn(ROOT, "z", TOGGLE_SECONDS / 2, TOGGLE_SECONDS / 2, TOGGLE_TILT, 0)]);
4163
+ function launcherClips(context) {
4164
+ if (!context.parts.ugl) return [];
4165
+ const { open, loaded, close, end, slide: slide2, back } = LAUNCHER_RELOAD;
4166
+ const reload = composeClip(
4167
+ "reload_ugl",
4168
+ [move("ugl", "x", open, slide2, 0, LAUNCHER_SLIDE), move("ugl", "x", close, back, LAUNCHER_SLIDE, 0)],
4169
+ [stage("breech_open", open), stage("round_in", loaded), stage("breech_close", close)],
4170
+ end
4171
+ );
4172
+ const shot = composeClip("shot_ugl", [move(ROOT, "x", 0, 0.04, 0, -LAUNCHER_KICK), move(ROOT, "x", 0.04, 0.2, -LAUNCHER_KICK, 0)], [cue(0, "muzzle_flash", { at: "ugl" })]);
4173
+ return [reload, shot];
4174
+ }
4175
+
3157
4176
  // src/animation/magazine-clips.ts
3158
4177
  var MAGAZINE_SWAP_SECONDS = 0.82;
3159
4178
  var MAGAZINE_IN_AT = 0.45;
@@ -3377,10 +4396,11 @@ var recoil = (context) => {
3377
4396
  turn(ROOT, "z", 0.03, 0.18, 0.045 * kick, 0)
3378
4397
  ];
3379
4398
  };
4399
+ var spinBarrels = (context) => context.parts.barrels && context.family === "belt" ? [turn("barrels", "x", 0, 0.03, 0, Math.PI / 3)] : [];
3380
4400
  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
4401
  function shotClip(id, context, locked) {
3382
4402
  const { parts, family } = context;
3383
- const tweens = [...pullTrigger(context), ...recoil(context), ...hammerFall(context)];
4403
+ const tweens = [...pullTrigger(context), ...recoil(context), ...hammerFall(context), ...spinBarrels(context)];
3384
4404
  const events = [cue(0, "muzzle_flash")];
3385
4405
  if (BLOWBACK.has(family)) {
3386
4406
  const part = blowbackPart(context);
@@ -3436,7 +4456,10 @@ function buildToolClips(context) {
3436
4456
  ...faultClips(context),
3437
4457
  ...clearClips(context),
3438
4458
  ...safetyClips(context),
3439
- drawClip()
4459
+ drawClip(),
4460
+ ...stockClips(context),
4461
+ toggleClip(),
4462
+ ...launcherClips(context)
3440
4463
  ];
3441
4464
  const set = {};
3442
4465
  for (const clip of clips) if (clip) set[clip.id] = clip;
@@ -3463,11 +4486,494 @@ var weaponEventOf = (event) => {
3463
4486
  return { type: "clear", fault: event.fault, phase: event.phase, at: 0 };
3464
4487
  case "safety":
3465
4488
  return { type: "safety", on: event.on, at: 0 };
4489
+ case "attachment":
4490
+ return { type: "attachment", action: event.action, id: event.attachment, mount: event.mount, at: 0 };
4491
+ case "toggle":
4492
+ return { type: "toggle", id: event.attachment, on: event.on, at: 0 };
4493
+ case "stock":
4494
+ return { type: "stock", state: event.state, kind: event.kind ?? "fold", at: 0 };
4495
+ case "underbarrel":
4496
+ return { type: "underbarrel", on: event.on, at: 0 };
3466
4497
  default:
3467
4498
  return void 0;
3468
4499
  }
3469
4500
  };
3470
4501
 
4502
+ // src/models/attachments/ammo-backpack.ts
4503
+ import { catenary, createCord } from "@adatechnology/game-interactables-three";
4504
+ var PACK_AT = [-0.55, -0.4, 0.1];
4505
+ var PACK_SIZE = [0.3, 0.42, 0.2];
4506
+ var BELT_SAG = 0.14;
4507
+ var BELT_POINTS = 16;
4508
+ var BELT_ROUND_EVERY = 2;
4509
+ var buildAmmoBackpack = ({ kit, frame, model }) => {
4510
+ const { THREE, M, box: box2, cylinderZ } = kit;
4511
+ const group = kit.group();
4512
+ const pack = [PACK_AT[0] - frame.position[0], PACK_AT[1] - frame.position[1], PACK_AT[2] - frame.position[2]];
4513
+ box2(group, ...PACK_SIZE, M.olive, ...pack);
4514
+ box2(group, PACK_SIZE[0] * 1.02, 0.03, PACK_SIZE[2] * 1.02, M.dark, pack[0], pack[1] + PACK_SIZE[1] / 2, pack[2]);
4515
+ box2(group, 0.04, PACK_SIZE[1] * 0.9, 0.012, M.tan, pack[0] + PACK_SIZE[0] / 2, pack[1], pack[2] - 0.04);
4516
+ const feed = kit.pivot(group, 0, 0, 0);
4517
+ buildBeltBox(kit, feed);
4518
+ const points = catenary([pack[0] + PACK_SIZE[0] / 2, pack[1] + PACK_SIZE[1] * 0.3, pack[2]], [0, 0, 0], BELT_SAG, BELT_POINTS);
4519
+ const strip = createCord(THREE, points, 35e-4, M.metal);
4520
+ group.add(strip);
4521
+ points.forEach((point, index) => {
4522
+ if (index % BELT_ROUND_EVERY === 0) cylinderZ(group, 45e-4, 0.02, M.brass, point[0], point[1], point[2], 8);
4523
+ });
4524
+ return {
4525
+ object: group,
4526
+ ...model.parts.belt ? { hides: [model.parts.belt] } : {},
4527
+ dispose() {
4528
+ strip.geometry.dispose();
4529
+ }
4530
+ };
4531
+ };
4532
+
4533
+ // src/models/attachments/foregrip.ts
4534
+ var RIBS = 4;
4535
+ var buildForegrip = ({ kit, attachment }) => {
4536
+ const { M, box: box2, cylinderY } = kit;
4537
+ const length = attachment.dims?.length ?? 0.1;
4538
+ const group = kit.group();
4539
+ box2(group, 0.045, 0.014, 0.034, M.dark, 0, -7e-3, 0);
4540
+ cylinderY(group, 0.0135, length, M.poly, 0, -0.014 - length / 2, 0, 14);
4541
+ for (let i = 0; i < RIBS; i++) cylinderY(group, 0.0145, 4e-3, M.rubber, 0, -0.03 - i * (length / (RIBS + 1)), 0, 14);
4542
+ cylinderY(group, 0.0155, 0.01, M.dark, 0, -0.014 - length, 0, 14);
4543
+ return group;
4544
+ };
4545
+
4546
+ // src/models/attachments/laser.ts
4547
+ var DEFAULT_RANGE = 120;
4548
+ var DEFAULT_COLOR = 16711680;
4549
+ var BEAM_WIDTH = 25e-4;
4550
+ var DOT_RADIUS = 0.012;
4551
+ var buildLaser = ({ kit, attachment, frame, raycast }) => {
4552
+ const { THREE, M, box: box2, cylinderX } = kit;
4553
+ const length = attachment.dims?.length ?? 0.08;
4554
+ const half = (attachment.dims?.diameter ?? 0.025) / 2;
4555
+ const range = attachment.laser?.range ?? DEFAULT_RANGE;
4556
+ const color = attachment.laser?.color ?? DEFAULT_COLOR;
4557
+ const group = kit.group();
4558
+ const offset = frame.side === 0 ? { y: -(half + 4e-3), z: 0 } : { y: 0, z: frame.side * (half + 4e-3) };
4559
+ box2(group, 0.03, 0.012, 0.028, M.dark, 0, frame.side === 0 ? -2e-3 : 0, 0);
4560
+ box2(group, length, half * 2, half * 2, M.dark, 0, offset.y, offset.z);
4561
+ const emitter = new THREE.MeshBasicMaterial({ color: 5574929 });
4562
+ cylinderX(group, half * 0.5, 4e-3, emitter, length / 2 + 1e-3, offset.y, offset.z, 10);
4563
+ const glow = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.7, blending: THREE.AdditiveBlending, depthWrite: false });
4564
+ const beam = new THREE.Mesh(new THREE.BoxGeometry(1, BEAM_WIDTH, BEAM_WIDTH), glow);
4565
+ const dot = new THREE.Mesh(new THREE.SphereGeometry(DOT_RADIUS, 10, 8), new THREE.MeshBasicMaterial({ color }));
4566
+ const front = length / 2 + 4e-3;
4567
+ beam.position.set(front, offset.y, offset.z);
4568
+ dot.position.set(front, offset.y, offset.z);
4569
+ beam.visible = false;
4570
+ dot.visible = false;
4571
+ group.add(beam, dot);
4572
+ const origin = new THREE.Vector3();
4573
+ const direction = new THREE.Vector3();
4574
+ const scale = new THREE.Vector3();
4575
+ let isOn = false;
4576
+ return {
4577
+ object: group,
4578
+ setOn(on) {
4579
+ isOn = on;
4580
+ emitter.color.setHex(on ? color : 5574929);
4581
+ beam.visible = on;
4582
+ if (!on) dot.visible = false;
4583
+ },
4584
+ update() {
4585
+ if (!isOn) return;
4586
+ group.updateWorldMatrix(true, false);
4587
+ group.getWorldPosition(origin);
4588
+ direction.set(1, 0, 0).transformDirection(group.matrixWorld);
4589
+ group.getWorldScale(scale);
4590
+ const hit = raycast?.(origin, direction, range) ?? null;
4591
+ const reach = (hit ?? range) / Math.max(scale.x, 1e-6);
4592
+ beam.scale.x = reach;
4593
+ beam.position.x = front + reach / 2;
4594
+ dot.visible = hit !== null;
4595
+ dot.position.x = front + reach;
4596
+ },
4597
+ dispose() {
4598
+ beam.geometry.dispose();
4599
+ dot.geometry.dispose();
4600
+ glow.dispose();
4601
+ emitter.dispose();
4602
+ dot.material.dispose();
4603
+ }
4604
+ };
4605
+ };
4606
+
4607
+ // src/models/attachments/light.ts
4608
+ var LIGHT_INTENSITY = 150;
4609
+ var LENS_GLOW = 3;
4610
+ var DEFAULT_CONE_DEG = 30;
4611
+ var DEFAULT_RANGE2 = 60;
4612
+ var buildLight = ({ kit, attachment, frame }) => {
4613
+ const { THREE, M, box: box2, cylinderX } = kit;
4614
+ const length = attachment.dims?.length ?? 0.1;
4615
+ const radius = (attachment.dims?.diameter ?? 0.03) / 2;
4616
+ const group = kit.group();
4617
+ const offset = frame.side === 0 ? { y: -(radius + 4e-3), z: 0 } : { y: 0, z: frame.side * (radius + 4e-3) };
4618
+ box2(group, 0.035, 0.012, 0.03, M.dark, 0, frame.side === 0 ? -2e-3 : 0, 0);
4619
+ cylinderX(group, radius, length, M.dark, 0, offset.y, offset.z, 14);
4620
+ cylinderX(group, radius * 1.08, 0.012, M.metal, length / 2 - 6e-3, offset.y, offset.z, 14);
4621
+ const lens = new THREE.MeshStandardMaterial({ color: 16773832, roughness: 0.1, metalness: 0.2, emissive: 16773832, emissiveIntensity: 0 });
4622
+ cylinderX(group, radius * 0.82, 4e-3, lens, length / 2 + 1e-3, offset.y, offset.z, 14);
4623
+ const cone = (attachment.light?.coneDeg ?? DEFAULT_CONE_DEG) * (Math.PI / 180);
4624
+ const spot = new THREE.SpotLight(16773584, LIGHT_INTENSITY, attachment.light?.range ?? DEFAULT_RANGE2, cone / 2, 0.35, 1.2);
4625
+ spot.position.set(length / 2 + 0.01, offset.y, offset.z);
4626
+ spot.intensity = 0;
4627
+ const target = new THREE.Object3D();
4628
+ target.position.set(length / 2 + 1, offset.y, offset.z);
4629
+ group.add(spot, target);
4630
+ spot.target = target;
4631
+ return {
4632
+ object: group,
4633
+ setOn(on) {
4634
+ spot.intensity = on ? LIGHT_INTENSITY : 0;
4635
+ lens.emissiveIntensity = on ? LENS_GLOW : 0;
4636
+ },
4637
+ dispose() {
4638
+ lens.dispose();
4639
+ spot.dispose();
4640
+ }
4641
+ };
4642
+ };
4643
+
4644
+ // src/models/attachments/magazine.ts
4645
+ var EXTENSION_FRACTION = 0.5;
4646
+ var MAGAZINE_LEAN = 0.08;
4647
+ var buildDrumMagazine = ({ kit, factoryMagazine }) => {
4648
+ const group = kit.group();
4649
+ buildDrum(kit, group);
4650
+ return { object: group, hangsFrom: "magazine", hides: factoryMagazine };
4651
+ };
4652
+ var buildExtendedMagazine = ({ kit, model }) => {
4653
+ const { M, box: box2 } = kit;
4654
+ const length = Math.max(0.1, (model.parts.magDrop ?? 0.16) - 0.03);
4655
+ const extension = length * EXTENSION_FRACTION;
4656
+ const group = kit.group();
4657
+ const lean = Math.sin(MAGAZINE_LEAN) * (length + extension / 2);
4658
+ box2(group, 0.05, extension + 4e-3, 0.024, M.dark, lean, -(Math.cos(MAGAZINE_LEAN) * (length + extension / 2)), 0, MAGAZINE_LEAN);
4659
+ box2(group, 0.056, 7e-3, 0.028, M.dark, lean + Math.sin(MAGAZINE_LEAN) * extension / 2, -(Math.cos(MAGAZINE_LEAN) * (length + extension)) - 3e-3, 0);
4660
+ return { object: group, hangsFrom: "magazine" };
4661
+ };
4662
+ var buildDualCoupler = ({ kit, factoryMagazine }) => {
4663
+ const { M, box: box2 } = kit;
4664
+ const group = kit.group();
4665
+ const spare = kit.group();
4666
+ spare.position.set(0, 0, 0.032);
4667
+ for (const piece of factoryMagazine) spare.add(piece.clone(true));
4668
+ group.add(spare);
4669
+ box2(group, 0.05, 0.012, 0.04, M.tape, 0, -0.03, 0.016);
4670
+ return { object: group, hangsFrom: "magazine" };
4671
+ };
4672
+
4673
+ // src/models/attachments/muzzle-device.ts
4674
+ var buildMuzzleBrake = (kind) => ({ kit, frame }) => {
4675
+ const group = kit.group();
4676
+ buildBrake(kit, group, kind, 0, 0, frame.barrelRadius);
4677
+ return group;
4678
+ };
4679
+ var buildCompensator = ({ kit, frame }) => {
4680
+ const { M, box: box2, cylinderX } = kit;
4681
+ const radius = frame.barrelRadius * 1.9;
4682
+ const length = 0.07;
4683
+ const group = kit.group();
4684
+ cylinderX(group, radius, length, M.dark, -length / 2, 0, 0, 16);
4685
+ for (let i = 0; i < 3; i++) box2(group, length * 0.14, radius * 0.9, radius * 0.7, M.hole, -length * 0.2 - i * length * 0.25, radius * 0.75, 0);
4686
+ return group;
4687
+ };
4688
+
4689
+ // src/models/attachments/optic.ts
4690
+ var OPTICS = {
4691
+ red_dot: { kind: 1 },
4692
+ holographic_sight: { kind: 1 },
4693
+ acog_4x: { kind: 2 },
4694
+ scope_3_9x: { kind: 3 },
4695
+ scope_6_24x: { kind: 4 },
4696
+ night_vision_scope: { kind: 3, lens: 3538794 },
4697
+ thermal_scope: { kind: 3, lens: 16747066 },
4698
+ pso_1: { kind: 2 },
4699
+ fixed_scope_10x: { kind: 4 }
4700
+ };
4701
+ var ADAPTER_LENGTH = 0.1;
4702
+ var ADAPTER_BACK = 0.05;
4703
+ var tinted = (context, group, color) => {
4704
+ const { THREE, M } = context.kit;
4705
+ const lens = new THREE.MeshStandardMaterial({ color, roughness: 0.05, metalness: 0.3, transparent: true, opacity: 0.9, emissive: color, emissiveIntensity: 0.6 });
4706
+ group.traverse((node) => {
4707
+ const mesh = node;
4708
+ if (mesh.isMesh && mesh.material === M.lens) mesh.material = lens;
4709
+ });
4710
+ return lens;
4711
+ };
4712
+ var buildOptic = (context) => {
4713
+ const look = OPTICS[context.attachment.model];
4714
+ if (!look) return null;
4715
+ const { kit, frame } = context;
4716
+ const group = kit.group();
4717
+ scope(kit, group, look.kind, 0, 0, 0);
4718
+ if (!frame.hasRail && frame.mount === "rail_top") rail(kit, group, -ADAPTER_BACK, ADAPTER_LENGTH, -3e-3, 0, frame.receiverWidth * 0.68);
4719
+ if (look.lens === void 0) return group;
4720
+ const lens = tinted(context, group, look.lens);
4721
+ return { object: group, dispose: () => lens.dispose() };
4722
+ };
4723
+ var OPTIC_MODEL_IDS = Object.keys(OPTICS);
4724
+
4725
+ // src/models/attachments/sling.ts
4726
+ import { catenary as catenary2, createCord as createCord2 } from "@adatechnology/game-interactables-three";
4727
+ var SAG = 0.2;
4728
+ var STRAP_RADIUS = 7e-3;
4729
+ var CORD_POINTS = 12;
4730
+ var buildSling = ({ kit, frame }) => {
4731
+ if (!frame.secondary) return null;
4732
+ const { THREE, M } = kit;
4733
+ const group = kit.group();
4734
+ const rear = [frame.secondary[0] - frame.position[0], frame.secondary[1] - frame.position[1], frame.secondary[2] - frame.position[2]];
4735
+ const swivels = [];
4736
+ for (const at of [[0, 0, 0], rear]) {
4737
+ const swivel2 = kit.torus(group, new THREE.TorusGeometry(6e-3, 15e-4, 6, 12), M.metal, at[0], at[1], at[2]);
4738
+ swivel2.rotation.y = Math.PI / 2;
4739
+ swivels.push(swivel2);
4740
+ }
4741
+ const strap = createCord2(THREE, catenary2(rear, [0, 0, 0], SAG, CORD_POINTS), STRAP_RADIUS, M.olive);
4742
+ group.add(strap);
4743
+ return {
4744
+ object: group,
4745
+ dispose() {
4746
+ strap.geometry.dispose();
4747
+ for (const swivel2 of swivels) swivel2.geometry.dispose();
4748
+ }
4749
+ };
4750
+ };
4751
+
4752
+ // src/models/attachments/suppressor.ts
4753
+ var BAFFLES = 5;
4754
+ var buildSuppressor = ({ kit, attachment, fitted }) => {
4755
+ const { M, cylinderX } = kit;
4756
+ const length = attachment.dims?.length ?? 0.2;
4757
+ const radius = (attachment.dims?.diameter ?? 0.045) / 2;
4758
+ const isImprovised = attachment.id === "oil_filter_suppressor";
4759
+ const group = kit.group();
4760
+ const middle = fitted.integral === true ? -length / 2 : length / 2;
4761
+ cylinderX(group, radius, length, isImprovised ? M.rust : M.dark, middle, 0, 0, 18);
4762
+ if (isImprovised) {
4763
+ for (const along of [0.2, 0.8]) cylinderX(group, radius * 1.04, 0.012, M.tape, middle - length / 2 + length * along, 0, 0, 18);
4764
+ cylinderX(group, radius * 0.7, 8e-3, M.rustD, middle + length / 2 - 4e-3, 0, 0, 14);
4765
+ } else {
4766
+ for (let i = 0; i < BAFFLES; i++) cylinderX(group, radius * 1.03, 4e-3, M.metal, middle - length / 2 + length * (i + 0.5) / BAFFLES, 0, 0, 18);
4767
+ cylinderX(group, radius * 0.9, 6e-3, M.steel, middle - length / 2 + 3e-3, 0, 0, 18);
4768
+ }
4769
+ cylinderX(group, radius * 0.22, 4e-3, M.hole, middle + length / 2 + 5e-4, 0, 0, 12);
4770
+ return { object: group, ...fitted.integral === true ? {} : { muzzleOffset: length } };
4771
+ };
4772
+
4773
+ // src/models/attachments/ugl.ts
4774
+ var LAUNCHERS = {
4775
+ m203: { tubeRadius: 0.0215, tubeFraction: 0.8, housing: [0.1, 0.05, 0.05], hasFrontGrip: false },
4776
+ gp_25: { tubeRadius: 0.023, tubeFraction: 0.78, housing: [0.08, 0.055, 0.05], hasFrontGrip: false },
4777
+ ag36: { tubeRadius: 0.02, tubeFraction: 0.8, housing: [0.13, 0.06, 0.05], hasFrontGrip: true }
4778
+ };
4779
+ var drawFixed = (kit, group, look, length) => {
4780
+ const { M, box: box2, cylinderY } = kit;
4781
+ const [housingLength, housingHeight, housingWidth] = look.housing;
4782
+ box2(group, housingLength, housingHeight, housingWidth, M.dark, housingLength / 2 - 0.02, -look.tubeRadius, 0);
4783
+ box2(group, 0.05, 4e-3, 0.012, M.metal, housingLength * 0.45, -look.tubeRadius - housingHeight / 2 - 4e-3, 0);
4784
+ box2(group, 4e-3, 0.02, 8e-3, M.dark, housingLength * 0.6, -look.tubeRadius - housingHeight / 2 - 0.016, 0);
4785
+ if (look.hasFrontGrip) {
4786
+ cylinderY(group, 0.012, 0.07, M.poly, length * 0.55, -look.tubeRadius - 0.04, 0, 12);
4787
+ box2(group, 0.04, 0.01, 0.03, M.dark, length * 0.55, -look.tubeRadius - 4e-3, 0);
4788
+ }
4789
+ };
4790
+ var buildUnderbarrelLauncher = ({ kit, attachment, slide: slide2 }) => {
4791
+ const look = LAUNCHERS[attachment.model];
4792
+ if (!look || !slide2) return null;
4793
+ const { M, cylinderX } = kit;
4794
+ const length = attachment.dims?.length ?? 0.35;
4795
+ const fixed = kit.group();
4796
+ drawFixed(kit, fixed, look, length);
4797
+ const tubeLength = length * look.tubeFraction;
4798
+ const start = -0.02;
4799
+ cylinderX(slide2, look.tubeRadius, tubeLength, M.metal, start + tubeLength / 2, -look.tubeRadius, 0, 18);
4800
+ cylinderX(slide2, look.tubeRadius * 1.12, 0.012, M.dark, start + tubeLength - 6e-3, -look.tubeRadius, 0, 18);
4801
+ cylinderX(slide2, look.tubeRadius * 0.7, 4e-3, M.hole, start + tubeLength + 5e-4, -look.tubeRadius, 0, 14);
4802
+ const muzzle = kit.mark(slide2, start + tubeLength + 0.01, -look.tubeRadius, 0);
4803
+ muzzle.name = "ugl_muzzle";
4804
+ return {
4805
+ object: fixed,
4806
+ muzzle,
4807
+ dispose() {
4808
+ slide2.remove(...slide2.children);
4809
+ }
4810
+ };
4811
+ };
4812
+
4813
+ // src/models/attachments/registry.ts
4814
+ var ATTACHMENT_MODELS = {
4815
+ ...Object.fromEntries(OPTIC_MODEL_IDS.map((id) => [id, buildOptic])),
4816
+ bipod: buildBipodModel,
4817
+ muzzle_brake: buildMuzzleBrake(1),
4818
+ muzzle_brake_heavy: buildMuzzleBrake(2),
4819
+ compensator: buildCompensator,
4820
+ suppressor: buildSuppressor,
4821
+ oil_filter_suppressor: buildSuppressor,
4822
+ integral_suppressor: buildSuppressor,
4823
+ vertical_grip: buildForegrip,
4824
+ weapon_light: buildLight,
4825
+ laser_sight: buildLaser,
4826
+ m203: buildUnderbarrelLauncher,
4827
+ gp_25: buildUnderbarrelLauncher,
4828
+ ag36: buildUnderbarrelLauncher,
4829
+ ammo_backpack: buildAmmoBackpack,
4830
+ tripod: buildTripod,
4831
+ sling: buildSling,
4832
+ drum_magazine: buildDrumMagazine,
4833
+ extended_magazine: buildExtendedMagazine,
4834
+ dual_coupler: buildDualCoupler
4835
+ };
4836
+
4837
+ // src/models/attachments/fit.ts
4838
+ var toModel = (made) => "object" in made ? made : { object: made };
4839
+ var MUZZLE_DEFAULT = "muzzle_default";
4840
+ function fitAttachmentModels(model, fitted, kit, options) {
4841
+ const registry = { ...ATTACHMENT_MODELS, ...options.overrides };
4842
+ const { parts, root, inner } = model;
4843
+ const factoryMagazine = parts.mag ? [...parts.mag.children] : [];
4844
+ const muzzleRest = parts.muzzle ? parts.muzzle.position.x : 0;
4845
+ const drawn = /* @__PURE__ */ new Map();
4846
+ const hiddenBy = /* @__PURE__ */ new Map();
4847
+ const switchedOn = /* @__PURE__ */ new Map();
4848
+ const hide = (id, objects) => {
4849
+ for (const object of objects) {
4850
+ const owners = hiddenBy.get(object) ?? /* @__PURE__ */ new Set();
4851
+ owners.add(id);
4852
+ hiddenBy.set(object, owners);
4853
+ object.visible = false;
4854
+ }
4855
+ };
4856
+ const unhide = (id, objects) => {
4857
+ for (const object of objects) {
4858
+ const owners = hiddenBy.get(object);
4859
+ owners?.delete(id);
4860
+ if (!owners || owners.size === 0) {
4861
+ hiddenBy.delete(object);
4862
+ object.visible = true;
4863
+ }
4864
+ }
4865
+ };
4866
+ const take = (id) => {
4867
+ const entry = drawn.get(id);
4868
+ if (!entry) return;
4869
+ drawn.delete(id);
4870
+ switchedOn.delete(id);
4871
+ entry.model.object.removeFromParent();
4872
+ unhide(id, entry.model.hides ?? []);
4873
+ if (parts.uglMuzzle && entry.model.muzzle === parts.uglMuzzle) delete parts.uglMuzzle;
4874
+ entry.model.dispose?.();
4875
+ };
4876
+ const place = (part, attachment) => {
4877
+ const builder = registry[attachment.model] ?? registry[attachment.id];
4878
+ const frame = mountFrame(model, part.mount);
4879
+ if (!builder || !frame) return;
4880
+ const made = builder({ kit, model, attachment, fitted: part, frame, raycast: options.raycast, factoryMagazine, slide: parts.ugl });
4881
+ if (!made) return;
4882
+ const entry = toModel(made);
4883
+ const parent = entry.hangsFrom === "magazine" && parts.mag ? parts.mag : root;
4884
+ if (parent === root) entry.object.position.set(...frame.position);
4885
+ parent.add(entry.object);
4886
+ hide(attachment.id, entry.hides ?? []);
4887
+ if (entry.muzzle) parts.uglMuzzle = entry.muzzle;
4888
+ drawn.set(attachment.id, { model: entry, mount: part.mount });
4889
+ };
4890
+ const shine = () => {
4891
+ const isDrawn = options.isDrawn?.() ?? true;
4892
+ const beams = options.areBeamsOn?.() ?? true;
4893
+ for (const [id, entry] of drawn) {
4894
+ entry.model.object.visible = isDrawn;
4895
+ entry.model.setOn?.(beams && switchedOn.get(id) === true);
4896
+ }
4897
+ };
4898
+ const sync = (list) => {
4899
+ const wanted = new Map(list.map((part) => [part.id, part]));
4900
+ for (const id of [...drawn.keys()]) if (!wanted.has(id) || drawn.get(id)?.mount !== wanted.get(id)?.mount) take(id);
4901
+ for (const [id, objects] of Object.entries(parts.fittings ?? {})) {
4902
+ if (id === MUZZLE_DEFAULT) continue;
4903
+ for (const object of objects) object.visible = wanted.has(id);
4904
+ }
4905
+ for (const object of parts.fittings?.[MUZZLE_DEFAULT] ?? []) object.visible = !list.some((part) => part.mount === "muzzle");
4906
+ for (const part of list) {
4907
+ const attachment = options.catalogue.get(part.id);
4908
+ if (!attachment || drawn.has(part.id) || parts.fittings?.[part.id]) continue;
4909
+ if (part.integral === true && attachment.kind === "optic") continue;
4910
+ place(part, attachment);
4911
+ }
4912
+ for (const id of drawn.keys()) switchedOn.set(id, wanted.get(id)?.on === true);
4913
+ shine();
4914
+ if (parts.muzzle) parts.muzzle.position.x = muzzleRest + [...drawn.values()].reduce((sum, entry) => sum + (entry.model.muzzleOffset ?? 0), 0) / inner.scale.x;
4915
+ };
4916
+ sync(fitted);
4917
+ return {
4918
+ sync,
4919
+ update() {
4920
+ shine();
4921
+ for (const entry of drawn.values()) entry.model.update?.();
4922
+ },
4923
+ get drawn() {
4924
+ return [...drawn.keys()];
4925
+ },
4926
+ dispose() {
4927
+ for (const id of [...drawn.keys()]) take(id);
4928
+ }
4929
+ };
4930
+ }
4931
+
4932
+ // src/models/attachments/rig-underbarrel.ts
4933
+ var LAUNCHER_INTERFACES = /* @__PURE__ */ new Set(["m203", "gp25", "ag36"]);
4934
+ function rigUnderbarrel(THREE, model, def) {
4935
+ const place = def.mounts?.find((mount) => mount.kind === "underbarrel" && mount.interfaces.some((name) => LAUNCHER_INTERFACES.has(name)));
4936
+ const frame = place ? mountFrame(model, "underbarrel") : void 0;
4937
+ if (!frame) return;
4938
+ const pivot = new THREE.Group();
4939
+ pivot.name = "ugl";
4940
+ pivot.position.set(...frame.position);
4941
+ model.root.add(pivot);
4942
+ model.parts.ugl = pivot;
4943
+ }
4944
+
4945
+ // src/models/rig-stock.ts
4946
+ var FOLD_AXIS_BY_SIDE = { right: "y", left: "y", under: "z", over: "z" };
4947
+ var hingeOf = (model, side) => {
4948
+ const frames = model.parts.frames;
4949
+ const x = frames?.receiverStart ?? 0;
4950
+ const halfWidth = (frames?.receiverWidth ?? 0.034) / 2;
4951
+ const halfHeight = (frames?.receiverHeight ?? 0.06) / 2;
4952
+ if (side === "right") return [x, 0, halfWidth];
4953
+ if (side === "left") return [x, 0, -halfWidth];
4954
+ if (side === "under") return [x, -halfHeight, 0];
4955
+ return [x, halfHeight, 0];
4956
+ };
4957
+ function rigStock(THREE, model, def) {
4958
+ const stock = def.stock;
4959
+ const meshes = model.parts.stockMeshes;
4960
+ if (!stock || stock.kind === "fixed" || stock.kind === "none" || !meshes || meshes.length === 0) return;
4961
+ const isFolding = stock.kind === "fold" || stock.kind === "fold_retract";
4962
+ const side = stock.foldAxis ?? "right";
4963
+ const hinge = isFolding ? hingeOf(model, side) : [0, 0, 0];
4964
+ const pivot = new THREE.Group();
4965
+ pivot.name = "stock";
4966
+ pivot.position.set(...hinge);
4967
+ model.inner.add(pivot);
4968
+ model.inner.updateMatrixWorld(true);
4969
+ for (const mesh of meshes) pivot.attach(mesh);
4970
+ const dimensions = def.dimensions;
4971
+ const travel = dimensions?.lengthCollapsed === void 0 ? 0 : (dimensions.length - dimensions.lengthCollapsed) / model.inner.scale.x;
4972
+ const move2 = isFolding ? { kind: "fold", axis: FOLD_AXIS_BY_SIDE[side] ?? "y", travel } : { kind: "retract", axis: "x", travel };
4973
+ model.parts.stock = pivot;
4974
+ model.parts.stockMove = move2;
4975
+ }
4976
+
3471
4977
  // src/models/condition.ts
3472
4978
  var substitutions = (kit, condition) => {
3473
4979
  const { M } = kit;
@@ -3574,7 +5080,7 @@ function buildRound(kit, dims, shape) {
3574
5080
  }
3575
5081
 
3576
5082
  // src/fx/effects.ts
3577
- var PI9 = Math.PI;
5083
+ var PI12 = Math.PI;
3578
5084
  var FLASH_BY_CASE = { shell: 1.6, big: 2.6, tiny: 0.5, pistol: 0.8, rifle: 1.2 };
3579
5085
  var FLASH_SECONDS = 0.055;
3580
5086
  function createEffects(kit, scene, particles, random) {
@@ -3585,15 +5091,15 @@ function createEffects(kit, scene, particles, random) {
3585
5091
  const buildFlash = (size) => {
3586
5092
  const group = new THREE.Group();
3587
5093
  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;
5094
+ cone.rotation.z = -PI12 / 2;
3589
5095
  cone.position.x = 0.042 * size;
3590
5096
  group.add(cone);
3591
5097
  for (let i = 0; i < 4; i++) {
3592
- const angle = i * PI9 / 2 + random() * 0.6;
5098
+ const angle = i * PI12 / 2 + random() * 0.6;
3593
5099
  const spike = new THREE.Mesh(new THREE.ConeGeometry(5e-3 * size, 0.04 * size, 6), M.flashA);
3594
5100
  spike.position.set(0.012 * size, Math.cos(angle) * 0.016 * size, Math.sin(angle) * 0.016 * size);
3595
5101
  spike.rotation.set(angle, 0, 0);
3596
- spike.rotateZ(-PI9 / 2);
5102
+ spike.rotateZ(-PI12 / 2);
3597
5103
  group.add(spike);
3598
5104
  }
3599
5105
  const core = new THREE.Mesh(new THREE.SphereGeometry(0.016 * size, 12, 8), M.flashB);
@@ -3617,7 +5123,7 @@ function createEffects(kit, scene, particles, random) {
3617
5123
  const group = buildFlash(size);
3618
5124
  group.position.copy(position);
3619
5125
  group.quaternion.copy(orientation);
3620
- group.rotateX(random() * PI9);
5126
+ group.rotateX(random() * PI12);
3621
5127
  scene.add(group);
3622
5128
  flash = { group, age: 0 };
3623
5129
  light.position.copy(position);
@@ -3649,7 +5155,7 @@ function createEffects(kit, scene, particles, random) {
3649
5155
  }
3650
5156
 
3651
5157
  // src/fx/heavy.ts
3652
- var PI10 = Math.PI;
5158
+ var PI13 = Math.PI;
3653
5159
  var flameMaterials = (kit) => {
3654
5160
  const { THREE } = kit;
3655
5161
  return [
@@ -3692,7 +5198,7 @@ function launchRocket(context) {
3692
5198
  rocket.position.copy(muzzle);
3693
5199
  scene.add(rocket);
3694
5200
  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;
5201
+ exhaust.rotation.z = PI13 / 2;
3696
5202
  exhaust.position.x = -0.08;
3697
5203
  rocket.add(exhaust);
3698
5204
  const light = new THREE.PointLight(16751168, 3, 2.5);
@@ -3888,7 +5394,8 @@ var toWorld = (session, vector, scaled = false) => {
3888
5394
  };
3889
5395
 
3890
5396
  // src/fx/viewmodel-events.ts
3891
- var PI11 = Math.PI;
5397
+ var PI14 = Math.PI;
5398
+ var UGL_FLASH = 1.6;
3892
5399
  var restFlat = (object) => {
3893
5400
  object.rotation.x = 0;
3894
5401
  object.rotation.z = 0;
@@ -3961,14 +5468,14 @@ function createClipEventHandler(world, session) {
3961
5468
  root.updateWorldMatrix(true, true);
3962
5469
  const copy = parts.mag.clone(true);
3963
5470
  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) });
5471
+ 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, PI14 / 2) });
3965
5472
  };
3966
5473
  const stuckCase = () => {
3967
5474
  if (!session.cartridge || !parts.eject) return;
3968
5475
  const casing = buildCasing(kit, session.cartridge.dims, session.cartridge.shape);
3969
5476
  const at = root.worldToLocal(offsetFrom(positionOf(parts.eject), 0, casing.length * 0.35, 0));
3970
5477
  casing.group.position.copy(at);
3971
- casing.group.rotation.set(0, 0, PI11 / 2 - 0.25);
5478
+ casing.group.rotation.set(0, 0, PI14 / 2 - 0.25);
3972
5479
  root.add(casing.group);
3973
5480
  session.stuckCase = casing.group;
3974
5481
  };
@@ -3990,7 +5497,9 @@ function createClipEventHandler(world, session) {
3990
5497
  const data = event.data ?? {};
3991
5498
  switch (event.type) {
3992
5499
  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]);
5500
+ if (data.at === "ugl") {
5501
+ if (parts.uglMuzzle && world.layers.muzzleFlash) effects.muzzleFlash(positionOf(parts.uglMuzzle), root.getWorldQuaternion(new THREE.Quaternion()), UGL_FLASH);
5502
+ } else 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
5503
  break;
3995
5504
  case "eject_casing":
3996
5505
  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 }));
@@ -4045,10 +5554,16 @@ function createClipEventHandler(world, session) {
4045
5554
  }
4046
5555
 
4047
5556
  // src/fx/viewmodel.ts
4048
- var PI12 = Math.PI;
5557
+ var PI15 = Math.PI;
4049
5558
  var HAND_OFFSET2 = [0.2, -0.2, -0.42];
4050
5559
  var HEAT_DISPLAY_MAX_FALLBACK = 120;
4051
5560
  var LAUNCH_VOLUME = 0.6;
5561
+ var fittedOf2 = (parts) => parts.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } }));
5562
+ var collapsedStockPose = (move2) => {
5563
+ if (move2.kind === "retract") return { position: [move2.travel, 0, 0] };
5564
+ return { rotation: [move2.axis === "x" ? PI15 : 0, move2.axis === "y" ? PI15 : 0, move2.axis === "z" ? PI15 : 0] };
5565
+ };
5566
+ var underbarrelStep = (step, tool) => tool.underbarrelActive && step.type === "play" && (step.clip === "shot" || step.clip === "reload") ? { ...step, variant: "ugl" } : step;
4052
5567
  var caliberOf = (arsenal, def) => def.action.kind === "firearm" ? arsenal.calibers.get(def.action.caliber) : void 0;
4053
5568
  function createViewmodel(options) {
4054
5569
  const { THREE, scene, camera, arsenal, groundAt, audio } = options;
@@ -4060,10 +5575,10 @@ function createViewmodel(options) {
4060
5575
  particles.onBounce = (kind) => kind === "casing" ? audio?.clink() : kind === "magazine" ? audio?.thud() : void 0;
4061
5576
  const effects = createEffects(kit, scene, particles, random);
4062
5577
  const hand = new THREE.Group();
4063
- hand.name = "weapon-viewmodel";
5578
+ hand.name = VIEWMODEL_NODE_NAME;
4064
5579
  scene.add(hand);
4065
5580
  const holder = new THREE.Group();
4066
- holder.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), PI12 / 2);
5581
+ holder.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), PI15 / 2);
4067
5582
  hand.add(holder);
4068
5583
  let tasks = [];
4069
5584
  let session = null;
@@ -4085,6 +5600,7 @@ function createViewmodel(options) {
4085
5600
  const heatMax = arsenal.rules.heat.displayMax || HEAT_DISPLAY_MAX_FALLBACK;
4086
5601
  const release = () => {
4087
5602
  if (!session) return;
5603
+ session.attachments?.dispose();
4088
5604
  session.model.root.removeFromParent();
4089
5605
  player = null;
4090
5606
  session = null;
@@ -4108,6 +5624,8 @@ function createViewmodel(options) {
4108
5624
  const model = def ? buildWeaponModelById(THREE, weapon) : void 0;
4109
5625
  if (!def || !model) return false;
4110
5626
  release();
5627
+ rigStock(THREE, model, def);
5628
+ rigUnderbarrel(THREE, model, def);
4111
5629
  const length = new THREE.Box3().setFromObject(model.root).getSize(new THREE.Vector3()).x;
4112
5630
  const scale = length > 0.8 ? 0.55 : length > 0.4 ? 0.75 : 1;
4113
5631
  holder.scale.setScalar(scale);
@@ -4125,21 +5643,37 @@ function createViewmodel(options) {
4125
5643
  kit,
4126
5644
  cartridge: caliber && dims ? { dims, shape: cartridgeShape(caliber, dims) } : null,
4127
5645
  stuckCase: null,
4128
- nextChamber: 0
5646
+ nextChamber: 0,
5647
+ attachments: null,
5648
+ attachmentsSignature: ""
4129
5649
  };
5650
+ const hostTool = options.tool?.();
5651
+ const onTool = hostTool?.weapon === weapon ? hostTool : void 0;
5652
+ const fitted = onTool?.attachments ? fittedOf2(onTool.attachments) : [...def.fitted ?? []];
5653
+ session.attachments = fitAttachmentModels(model, fitted, kit, { catalogue: arsenal.attachments, isDrawn: () => layers.attachments, areBeamsOn: () => layers.beams, ...options.attachmentModels ? { overrides: options.attachmentModels } : {}, ...options.raycast ? { raycast: options.raycast } : {} });
5654
+ session.attachmentsSignature = attachmentsKey(onTool?.attachments);
4130
5655
  const action = def.action;
4131
5656
  const reloadFamily = action.kind === "firearm" || action.kind === "flame" || action.kind === "launcher" ? action.family : void 0;
4132
5657
  const rpm = action.kind === "firearm" ? action.rpm : 0;
5658
+ const rig = createModelRig(model);
4133
5659
  player = createToolClipPlayer({
4134
5660
  clips: buildToolClips({ parts: model.parts, family: model.parts.family, caseType: session.caseType, reloadFamily }),
4135
- rig: createModelRig(model),
5661
+ rig,
4136
5662
  onEvent: createClipEventHandler(world, session),
4137
5663
  repeatInterval: () => rpm > 0 ? 60 / rpm : 0.1
4138
5664
  });
5665
+ if (model.parts.stockMove && onTool?.stock === "collapsed") rig.setPartPose("stock", collapsedStockPose(model.parts.stockMove));
4139
5666
  applyCondition(kit, model, condition);
4140
5667
  lastAmmo = options.tool?.().ammo ?? 0;
4141
5668
  return true;
4142
5669
  };
5670
+ const syncAttachments = (current, tool) => {
5671
+ if (!current.attachments || tool?.weapon !== current.id || !tool.attachments) return;
5672
+ const signature = attachmentsKey(tool.attachments);
5673
+ if (signature === current.attachmentsSignature) return;
5674
+ current.attachmentsSignature = signature;
5675
+ current.attachments.sync(fittedOf2(tool.attachments));
5676
+ };
4143
5677
  const place = () => {
4144
5678
  const [x, y, z] = HAND_OFFSET2;
4145
5679
  hand.position.set(x, y, z).applyQuaternion(camera.quaternion).add(camera.position);
@@ -4156,7 +5690,11 @@ function createViewmodel(options) {
4156
5690
  material.emissiveIntensity = 2.2 * level;
4157
5691
  }
4158
5692
  };
4159
- const handleShot = (current) => {
5693
+ const handleShot = (current, event, tool) => {
5694
+ if (tool.underbarrelActive && current.model.parts.uglMuzzle) {
5695
+ audio?.play("explosion", LAUNCH_VOLUME, distance());
5696
+ return;
5697
+ }
4160
5698
  const action = current.def.action;
4161
5699
  if (action.kind === "launcher") {
4162
5700
  launchRocket(heavy(current));
@@ -4165,7 +5703,7 @@ function createViewmodel(options) {
4165
5703
  if (action.kind !== "firearm") return;
4166
5704
  const caliber = caliberOf(arsenal, current.def);
4167
5705
  if (!caliber) return;
4168
- audio?.shot(muzzleEnergy3(caliber) * (caliber.pellets ?? 1), current.def.category, { caliber, distance: distance() });
5706
+ audio?.shot(muzzleEnergy3(caliber) * (caliber.pellets ?? 1), current.def.category, { caliber, distance: distance(), ...event.suppressed === true ? { suppressed: true } : {} });
4169
5707
  };
4170
5708
  return {
4171
5709
  equip,
@@ -4175,7 +5713,7 @@ function createViewmodel(options) {
4175
5713
  if (event.type === "tool") equip(event.weapon);
4176
5714
  if (!session || !player || event.weapon !== session.id) return;
4177
5715
  const current = session;
4178
- if (event.type === "shot") handleShot(current);
5716
+ if (event.type === "shot") handleShot(current, event, tool);
4179
5717
  else if (event.type === "reload" && event.phase === "start") current.nextChamber = 0;
4180
5718
  else if (event.type === "clear" && event.phase === "end") current.stuckCase = null;
4181
5719
  const mechanical = weaponEventOf(event);
@@ -4183,13 +5721,15 @@ function createViewmodel(options) {
4183
5721
  const step = toolAnimationStep(event);
4184
5722
  if (!step) return;
4185
5723
  const last = step.type === "play" && step.clip === "shot" && current.model.parts.family === "pistol" && tool.ammo === 0;
4186
- player.apply(last ? { ...step, variant: "last" } : step);
5724
+ player.apply(last ? { ...step, variant: "last" } : underbarrelStep(step, tool));
4187
5725
  },
4188
5726
  update(dt) {
4189
5727
  if (session) {
4190
5728
  place();
4191
5729
  player?.update(dt);
4192
5730
  const tool = options.tool?.();
5731
+ syncAttachments(session, tool);
5732
+ session.attachments?.update();
4193
5733
  glow(tool);
4194
5734
  if (tool && session.def.action.kind === "flame") {
4195
5735
  if (tool.ammo < lastAmmo) flameJet(heavy(session), dt, session.model.parts.launcherType === "flame" ? 1 : 0.6);
@@ -4254,7 +5794,9 @@ function createWeaponFx(options) {
4254
5794
  audio: options.audio ?? null,
4255
5795
  ...options.seed === void 0 ? {} : { seed: options.seed },
4256
5796
  ...options.tool ? { tool: options.tool } : {},
4257
- ...options.condition ? { condition: options.condition } : {}
5797
+ ...options.condition ? { condition: options.condition } : {},
5798
+ ...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
5799
+ ...options.raycast ? { raycast: options.raycast } : {}
4258
5800
  });
4259
5801
  return {
4260
5802
  handle(event, tool) {
@@ -4340,11 +5882,13 @@ var SOUND_DISTANCE_MAX = 200;
4340
5882
  var FUSE_LENGTH_MAX = 1;
4341
5883
  var DEFAULT_FUSE_LENGTH = 0.03;
4342
5884
  var COUNTER = { length: 4.8, depth: 1.6, height: 0.9 };
4343
- var COUNTER_X = [-6.5, -9, -11.5];
5885
+ var COUNTER_X = [-6.5, -9, -11.5, -14];
4344
5886
  var COUNTER_Z = [-1, 5.5];
5887
+ var MOUNTED_M134_ITEM_ID = "m134_tripod";
5888
+ var MOUNTED_M134 = { weaponId: "m134", at: [-3.5, 0, 2], rotationY: Math.PI / 2 };
4345
5889
  var END_MARGIN = 0.5;
4346
5890
  var DEFAULT_PITCH = 0.16;
4347
- var PITCH_BY_ID = { browning_m2: 0.5, m2_flamethrower: 0.45, bow: 0.3, crossbow: 0.3 };
5891
+ var PITCH_BY_ID = { browning_m2: 0.5, m134: 0.45, m2_flamethrower: 0.45, bow: 0.3, crossbow: 0.3 };
4348
5892
  var WORLD_EXPLOSIVES = [
4349
5893
  { id: "fuel_barrel", at: [10, 0, -8] },
4350
5894
  { id: "fuel_barrel", at: [11.2, 0, -8] },
@@ -4356,6 +5900,7 @@ var TARGETS = [
4356
5900
  { id: "target_plywood", at: [0, 0, -7] },
4357
5901
  { id: "target_concrete", at: [1.5, 0, -7] }
4358
5902
  ];
5903
+ var EMPTY_TOOL = { weapon: "", category: "", ammo: 0, capacity: 0, reserve: 0, reloading: false };
4359
5904
  var TEXT = {
4360
5905
  en: {
4361
5906
  group: "Weapons",
@@ -4365,6 +5910,7 @@ var TEXT = {
4365
5910
  reloadStart: "Reload started: {weapon} ({ammo}/{capacity})",
4366
5911
  reloadEnd: "Reload done: {weapon} ({ammo}/{capacity})",
4367
5912
  worldExplosives: { fuel_barrel: "Fuel barrel", propane_p13: "Propane cylinder (P13)", car_fuel_tank: "Car fuel tank" },
5913
+ mountedM134: "M134 Minigun on its tripod",
4368
5914
  condition: "Weapon condition",
4369
5915
  conditionHint: "Wear of the weapon: it jams more and the metal shows it",
4370
5916
  conditions: { new: "New", used: "Used", worn: "Worn", rusty: "Rusty" },
@@ -4386,6 +5932,7 @@ var TEXT = {
4386
5932
  reloadStart: "Recarga iniciada: {weapon} ({ammo}/{capacity})",
4387
5933
  reloadEnd: "Recarga completa: {weapon} ({ammo}/{capacity})",
4388
5934
  worldExplosives: { fuel_barrel: "Barril de combust\xEDvel", propane_p13: "Botij\xE3o de g\xE1s (P13)", car_fuel_tank: "Tanque de combust\xEDvel do carro" },
5935
+ mountedM134: "M134 Minigun no trip\xE9",
4389
5936
  condition: "Estado da arma",
4390
5937
  conditionHint: "Desgaste da arma: trava mais e o metal mostra",
4391
5938
  conditions: { new: "Nova", used: "Usada", worn: "Gasta", rusty: "Enferrujada" },
@@ -4451,6 +5998,14 @@ var weaponItem = (def, name) => {
4451
5998
  movable: true
4452
5999
  };
4453
6000
  };
6001
+ var mountedM134Item = (def, name) => ({
6002
+ id: MOUNTED_M134_ITEM_ID,
6003
+ name,
6004
+ build: (THREE) => buildTripodMountedProp(THREE, def, name),
6005
+ spec: (root) => modeledPropSpec(root, def, name),
6006
+ weapon: def.id,
6007
+ movable: false
6008
+ });
4454
6009
  var asItem = (id, name, prop, extra = {}) => ({
4455
6010
  id,
4456
6011
  name,
@@ -4485,6 +6040,7 @@ function createWeaponsTestModule(options = {}) {
4485
6040
  const locale = options.locale ?? "pt-BR";
4486
6041
  const arsenal = options.arsenal ?? createArsenal({ locale });
4487
6042
  const text = textOf(locale);
6043
+ const accessoryText = accessoryTextOf(locale);
4488
6044
  const audio = options.sound === false ? null : createWeaponAudio(void 0, options.seed === void 0 ? {} : { seed: options.seed });
4489
6045
  const weaponDefs = [...arsenal.weapons.values()].filter((def) => !BODY_ACTIONS.has(def.id));
4490
6046
  const weaponIds = weaponDefs.filter((def) => !(def.id in ARCHETYPE_MODELS)).map((def) => `${WEAPON_ITEM_PREFIX}${def.id}`);
@@ -4495,6 +6051,8 @@ function createWeaponsTestModule(options = {}) {
4495
6051
  return item ? [item] : [];
4496
6052
  });
4497
6053
  const worldExplosiveIds = new Set(worldExplosives.map((item) => item.id));
6054
+ const mountedM134Def = arsenal.weapons.get(MOUNTED_M134.weaponId);
6055
+ const mountedM134 = mountedM134Def && hasWeaponModel(mountedM134Def.id) ? [mountedM134Item(mountedM134Def, text.mountedM134)] : [];
4498
6056
  let fx = null;
4499
6057
  let host = null;
4500
6058
  let soundDistance = 0;
@@ -4511,10 +6069,14 @@ function createWeaponsTestModule(options = {}) {
4511
6069
  ...weaponDefs.map((def) => weaponItem(def, arsenal.name(def.id))),
4512
6070
  asItem(COUNTER_ITEM_ID, text.counter, counterProp(text.counter), { movable: false }),
4513
6071
  ...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
6072
+ ...worldExplosives,
6073
+ ...mountedM134
4515
6074
  ],
4516
6075
  area: () => ({
4517
- placements: weaponsLayout(weaponIds, pitchOf).filter((placement) => !placement.item.startsWith(WORLD_EXPLOSIVE_PREFIX) || worldExplosiveIds.has(placement.item)),
6076
+ placements: [
6077
+ ...weaponsLayout(weaponIds, pitchOf).filter((placement) => !placement.item.startsWith(WORLD_EXPLOSIVE_PREFIX) || worldExplosiveIds.has(placement.item)),
6078
+ ...mountedM134.map((item) => ({ item: item.id, position: MOUNTED_M134.at, rotationY: MOUNTED_M134.rotationY, asleep: true }))
6079
+ ],
4518
6080
  view: { eye: [-3.4, 1.9, 9.2], target: [-9, 0.9, 1.6] }
4519
6081
  }),
4520
6082
  controls: () => [
@@ -4535,13 +6097,15 @@ function createWeaponsTestModule(options = {}) {
4535
6097
  default: arsenal.rules.defaultAmmoQuality
4536
6098
  },
4537
6099
  { 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 }
6100
+ { 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 },
6101
+ ...accessoryControls(accessoryText)
4539
6102
  ],
4540
- layers: () => ["muzzle_flash", "casings", "magazines"].map((id) => ({ id, name: text.layers[id] })),
6103
+ layers: () => [...["muzzle_flash", "casings", "magazines"].map((id) => ({ id, name: text.layers[id] })), ...accessoryLayers(accessoryText)],
4541
6104
  metrics: () => [
4542
6105
  { id: "casings_live", name: text.metrics.casings_live, read: () => fx?.casings ?? 0 },
4543
6106
  { 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 }
6107
+ { id: "heat", name: text.metrics.heat, unit: "%", max: 100, digits: 0, read: heatPercent },
6108
+ ...accessoryMetrics(accessoryText, () => host?.tool() ?? EMPTY_TOOL)
4545
6109
  ],
4546
6110
  attach(lent) {
4547
6111
  host = lent;
@@ -4554,14 +6118,20 @@ function createWeaponsTestModule(options = {}) {
4554
6118
  audio,
4555
6119
  tool: lent.tool,
4556
6120
  soundDistance: () => soundDistance,
6121
+ raycast: createLaserRaycast(lent.THREE, lent.scene, lent.camera),
6122
+ ...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
4557
6123
  ...options.condition ? { condition: options.condition } : {},
4558
6124
  ...options.seed === void 0 ? {} : { seed: options.seed }
4559
6125
  });
4560
6126
  fx = created;
4561
6127
  const camera = lent.camera;
6128
+ const accessories = createAccessoryBench({ arsenal, host: lent, text: accessoryText });
4562
6129
  const offBench = lent.bench?.onChange((change) => {
6130
+ if (accessories.handleChange(change)) return;
4563
6131
  if (change.kind === "layer") {
4564
- if (change.id === "muzzle_flash") created.layers.muzzleFlash = change.on;
6132
+ if (change.id === "attachments") created.layers.attachments = change.on;
6133
+ else if (change.id === "beams") created.layers.beams = change.on;
6134
+ else if (change.id === "muzzle_flash") created.layers.muzzleFlash = change.on;
4565
6135
  else if (change.id === "casings") created.layers.casings = change.on;
4566
6136
  else if (change.id === "magazines") created.layers.magazines = change.on;
4567
6137
  } else if (change.kind === "control" && typeof change.value === "string") {
@@ -4585,6 +6155,7 @@ function createWeaponsTestModule(options = {}) {
4585
6155
  const offFrame = lent.onFrame((dt) => created.update(dt));
4586
6156
  created.handle({ type: "tool", weapon: lent.tool().weapon }, lent.tool());
4587
6157
  return () => {
6158
+ accessories.dispose();
4588
6159
  offBench?.();
4589
6160
  offEvents();
4590
6161
  offFrame();
@@ -4598,6 +6169,7 @@ function createWeaponsTestModule(options = {}) {
4598
6169
  }
4599
6170
  export {
4600
6171
  ARCHETYPE_MODELS,
6172
+ ATTACHMENT_MODELS,
4601
6173
  BODY_ACTIONS,
4602
6174
  CLASS_REFERENCE_ENERGY,
4603
6175
  COUNTER_ITEM_ID,
@@ -4606,6 +6178,7 @@ export {
4606
6178
  GAME_SOUND_CLASSES,
4607
6179
  MECHANICAL_SOUNDS,
4608
6180
  MODEL_ENTRIES,
6181
+ MOUNTED_M134_ITEM_ID,
4609
6182
  WEAPON_ITEM_PREFIX,
4610
6183
  WORLD_EXPLOSIVE_IDS,
4611
6184
  WORLD_EXPLOSIVE_PREFIX,
@@ -4622,10 +6195,15 @@ export {
4622
6195
  createWeaponAudio,
4623
6196
  createWeaponFx,
4624
6197
  createWeaponsTestModule,
6198
+ deriveFrames,
6199
+ fitAttachmentModels,
4625
6200
  hasWeaponModel,
4626
6201
  mechanicalSoundOf,
4627
6202
  modelKit,
6203
+ mountFrame,
4628
6204
  partRegistry,
6205
+ rigStock,
6206
+ rigUnderbarrel,
4629
6207
  weaponProp,
4630
6208
  weaponsLayout,
4631
6209
  worldExplosiveItem