eve-fit-engine 0.1.0 → 0.1.2

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
@@ -647,7 +647,7 @@ var FitContext = class {
647
647
  const root = this.resolveDomain(modifier.domain, source);
648
648
  switch (modifier.func) {
649
649
  case "ItemModifier":
650
- if (modifier.domain === "charID" && source.kind === "module") {
650
+ if (modifier.domain === "charID" && source.kind === "module" && modifier.modifiedAttributeID === 212) {
651
651
  const out = [];
652
652
  for (const m of this.modules) {
653
653
  if (m.charge) out.push(m.charge);
@@ -1059,6 +1059,299 @@ function makeModeState(modeTypeID, type) {
1059
1059
  return new ItemState({ kind: "mode", id: `mode:${modeTypeID}`, type, state: "ACTIVE" });
1060
1060
  }
1061
1061
 
1062
+ // src/stackingGroups.ts
1063
+ var STACKING_PENALTY_GROUPS = /* @__PURE__ */ new Map([
1064
+ [89, "default"],
1065
+ [91, "default"],
1066
+ [92, "default"],
1067
+ [93, "default"],
1068
+ [95, "default"],
1069
+ [96, "default"],
1070
+ [394, "default"],
1071
+ [395, "default"],
1072
+ [494, "default"],
1073
+ [607, "postMul"],
1074
+ [657, "default"],
1075
+ [699, "default"],
1076
+ [763, "default"],
1077
+ [784, "default"],
1078
+ [854, "cloakingScanResolutionMultiplier"],
1079
+ [856, "default"],
1080
+ [889, "default"],
1081
+ [891, "default"],
1082
+ [892, "default"],
1083
+ [1024, "default"],
1084
+ [1230, "default"],
1085
+ [1281, "default"],
1086
+ [1318, "default"],
1087
+ [1445, "default"],
1088
+ [1446, "default"],
1089
+ [1448, "default"],
1090
+ [1452, "default"],
1091
+ [1472, "default"],
1092
+ [1590, "default"],
1093
+ [1617, "default"],
1094
+ [1720, "default"],
1095
+ [1764, "default"],
1096
+ [1885, "postPerc"],
1097
+ [1886, "postPerc"],
1098
+ [2013, "default"],
1099
+ [2014, "default"],
1100
+ [2019, "default"],
1101
+ [2020, "default"],
1102
+ [2041, "default"],
1103
+ [2052, "default"],
1104
+ [2152, "default"],
1105
+ [2232, "default"],
1106
+ [2302, "preMul"],
1107
+ [2644, "default"],
1108
+ [2645, "default"],
1109
+ [2646, "default"],
1110
+ [2670, "default"],
1111
+ [2693, "default"],
1112
+ [2694, "default"],
1113
+ [2695, "default"],
1114
+ [2696, "default"],
1115
+ [2697, "default"],
1116
+ [2698, "default"],
1117
+ [2716, "default"],
1118
+ [2717, "default"],
1119
+ [2792, "default"],
1120
+ [2795, "default"],
1121
+ [2796, "default"],
1122
+ [2797, "default"],
1123
+ [2798, "default"],
1124
+ [2799, "default"],
1125
+ [2801, "default"],
1126
+ [2802, "default"],
1127
+ [2803, "default"],
1128
+ [2804, "default"],
1129
+ [2851, "default"],
1130
+ [2858, "default"],
1131
+ [2865, "default"],
1132
+ [2867, "default"],
1133
+ [2868, "default"],
1134
+ [3001, "postPerc"],
1135
+ [3046, "default"],
1136
+ [3174, "default"],
1137
+ [3175, "default"],
1138
+ [3182, "default"],
1139
+ [3200, "default"],
1140
+ [3201, "default"],
1141
+ [3586, "default"],
1142
+ [3655, "default"],
1143
+ [3656, "default"],
1144
+ [3657, "default"],
1145
+ [3659, "default"],
1146
+ [3674, "default"],
1147
+ [3726, "default"],
1148
+ [3727, "default"],
1149
+ [3993, "postMul"],
1150
+ [3995, "postMul"],
1151
+ [3996, "default"],
1152
+ [3997, "default"],
1153
+ [3998, "default"],
1154
+ [3999, "default"],
1155
+ [4002, "postMul"],
1156
+ [4003, "postMul"],
1157
+ [4016, "default"],
1158
+ [4017, "postMul"],
1159
+ [4018, "postMul"],
1160
+ [4019, "postMul"],
1161
+ [4020, "postMul"],
1162
+ [4021, "postMul"],
1163
+ [4022, "postMul"],
1164
+ [4023, "postMul"],
1165
+ [4054, "default"],
1166
+ [4055, "default"],
1167
+ [4056, "default"],
1168
+ [4057, "postMul"],
1169
+ [4058, "postMul"],
1170
+ [4059, "postMul"],
1171
+ [4060, "postMul"],
1172
+ [4061, "postMul"],
1173
+ [4062, "postMul"],
1174
+ [4063, "postMul"],
1175
+ [4086, "postMul"],
1176
+ [4088, "postMul"],
1177
+ [4089, "postMul"],
1178
+ [4135, "default"],
1179
+ [4136, "default"],
1180
+ [4137, "default"],
1181
+ [4138, "default"],
1182
+ [4162, "default"],
1183
+ [4280, "default"],
1184
+ [4358, "default"],
1185
+ [4464, "default"],
1186
+ [4489, "default"],
1187
+ [4490, "default"],
1188
+ [4491, "default"],
1189
+ [4492, "default"],
1190
+ [4527, "default"],
1191
+ [4559, "default"],
1192
+ [4575, "default"],
1193
+ [4809, "default"],
1194
+ [4810, "default"],
1195
+ [4811, "default"],
1196
+ [4812, "default"],
1197
+ [4906, "postMul"],
1198
+ [4928, "preMul"],
1199
+ [4961, "postMul"],
1200
+ [5081, "default"],
1201
+ [5188, "default"],
1202
+ [5189, "default"],
1203
+ [5190, "default"],
1204
+ [5213, "default"],
1205
+ [5214, "default"],
1206
+ [5230, "default"],
1207
+ [5231, "default"],
1208
+ [5397, "default"],
1209
+ [5399, "default"],
1210
+ [5440, "postMul"],
1211
+ [5468, "default"],
1212
+ [5560, "default"],
1213
+ [5618, "postPerc"],
1214
+ [5757, "default"],
1215
+ [5867, "default"],
1216
+ [5911, "default"],
1217
+ [5912, "postMul"],
1218
+ [5914, "postMul"],
1219
+ [5915, "postMul"],
1220
+ [5916, "postMul"],
1221
+ [5917, "postMul"],
1222
+ [5918, "postMul"],
1223
+ [5919, "postMul"],
1224
+ [5920, "postMul"],
1225
+ [5921, "postMul"],
1226
+ [5922, "postMul"],
1227
+ [5923, "postMul"],
1228
+ [5924, "postMul"],
1229
+ [5925, "postMul"],
1230
+ [5926, "postMul"],
1231
+ [5927, "postMul"],
1232
+ [5929, "postMul"],
1233
+ [5951, "default"],
1234
+ [5998, "default"],
1235
+ [6010, "postDiv"],
1236
+ [6011, "postDiv"],
1237
+ [6012, "postDiv"],
1238
+ [6014, "postDiv"],
1239
+ [6015, "postDiv"],
1240
+ [6016, "postDiv"],
1241
+ [6017, "postDiv"],
1242
+ [6039, "postDiv"],
1243
+ [6040, "postDiv"],
1244
+ [6041, "postDiv"],
1245
+ [6063, "default"],
1246
+ [6076, "postDiv"],
1247
+ [6110, "default"],
1248
+ [6111, "default"],
1249
+ [6112, "default"],
1250
+ [6113, "default"],
1251
+ [6135, "default"],
1252
+ [6152, "postDiv"],
1253
+ [6154, "postDiv"],
1254
+ [6164, "default"],
1255
+ [6201, "default"],
1256
+ [6208, "default"],
1257
+ [6402, "default"],
1258
+ [6403, "default"],
1259
+ [6404, "default"],
1260
+ [6405, "default"],
1261
+ [6406, "default"],
1262
+ [6409, "default"],
1263
+ [6410, "default"],
1264
+ [6411, "default"],
1265
+ [6412, "default"],
1266
+ [6422, "default"],
1267
+ [6423, "default"],
1268
+ [6424, "default"],
1269
+ [6425, "default"],
1270
+ [6426, "default"],
1271
+ [6427, "default"],
1272
+ [6428, "default"],
1273
+ [6435, "default"],
1274
+ [6439, "default"],
1275
+ [6440, "default"],
1276
+ [6441, "default"],
1277
+ [6448, "default"],
1278
+ [6449, "default"],
1279
+ [6472, "default"],
1280
+ [6473, "default"],
1281
+ [6474, "default"],
1282
+ [6476, "default"],
1283
+ [6478, "default"],
1284
+ [6479, "default"],
1285
+ [6481, "default"],
1286
+ [6484, "postMul"],
1287
+ [6487, "default"],
1288
+ [6555, "default"],
1289
+ [6556, "default"],
1290
+ [6557, "default"],
1291
+ [6559, "default"],
1292
+ [6566, "postMul"],
1293
+ [6567, "default"],
1294
+ [6581, "default"],
1295
+ [6582, "postPercent"],
1296
+ [6658, "preMul"],
1297
+ [6670, "default"],
1298
+ [6671, "default"],
1299
+ [6682, "default"],
1300
+ [6683, "default"],
1301
+ [6684, "default"],
1302
+ [6686, "default"],
1303
+ [6690, "default"],
1304
+ [6692, "default"],
1305
+ [6693, "default"],
1306
+ [6694, "default"],
1307
+ [6727, "default"],
1308
+ [6730, "postMul"],
1309
+ [6731, "postMul"],
1310
+ [6796, "postDiv"],
1311
+ [6797, "postDiv"],
1312
+ [6798, "postDiv"],
1313
+ [6799, "postDiv"],
1314
+ [6801, "postDiv"],
1315
+ [6877, "default"],
1316
+ [7029, "default"],
1317
+ [7077, "default"],
1318
+ [7078, "default"],
1319
+ [7098, "default"],
1320
+ [7111, "default"],
1321
+ [7142, "default"],
1322
+ [7202, "default"],
1323
+ [7203, "default"],
1324
+ [7223, "default"],
1325
+ [7237, "default"],
1326
+ [8033, "default"],
1327
+ [8057, "default"],
1328
+ [8076, "default"],
1329
+ [8082, "default"],
1330
+ [8108, "postMul"],
1331
+ [8109, "postMul"],
1332
+ [8111, "default"],
1333
+ [8112, "default"],
1334
+ [8113, "postMul"],
1335
+ [8114, "postMul"],
1336
+ [8119, "default"],
1337
+ [11445, "default"],
1338
+ [11691, "default"],
1339
+ [11946, "default"],
1340
+ [11947, "postMul"],
1341
+ [11948, "postMul"],
1342
+ [11953, "postMul"],
1343
+ [12126, "default"],
1344
+ [12597, "default"],
1345
+ [12761, "default"],
1346
+ [12794, "postDiv"],
1347
+ [12795, "postDiv"],
1348
+ [12796, "postDiv"],
1349
+ [12798, "postDiv"],
1350
+ [12799, "postDiv"],
1351
+ [12838, "postMul"],
1352
+ [12839, "postMul"]
1353
+ ]);
1354
+
1062
1355
  // src/modifierEngine.ts
1063
1356
  var NO_PENALTY_KINDS = /* @__PURE__ */ new Set([
1064
1357
  "skill",
@@ -1073,16 +1366,21 @@ function scaleForPipeline(rawValue, _unitID, op) {
1073
1366
  }
1074
1367
  function applySourceItem(source, ctx, dataset) {
1075
1368
  const isLocalModule = source.kind === "module";
1369
+ const selfMods = [];
1370
+ const outMods = [];
1076
1371
  for (const eid of source.effectIDs) {
1077
1372
  if (LEGACY_HANDLED_EFFECT_IDS.has(eid)) continue;
1373
+ if (SEC_STATUS_SCALED_EFFECT_IDS.has(eid)) continue;
1078
1374
  if (isLocalModule && ctx.stoppedLocalEffectIDs.has(eid)) continue;
1079
1375
  const effect = dataset.effects.get(eid);
1080
1376
  if (!effect) continue;
1081
1377
  if (!source.appliesAtState(effect)) continue;
1082
1378
  for (const mi of effect.modifierInfo) {
1083
- applyOneModifier(source, effect, mi, ctx, dataset);
1379
+ (mi.domain === "itemID" ? selfMods : outMods).push({ effect, mi });
1084
1380
  }
1085
1381
  }
1382
+ for (const { effect, mi } of selfMods) applyOneModifier(source, effect, mi, ctx, dataset);
1383
+ for (const { effect, mi } of outMods) applyOneModifier(source, effect, mi, ctx, dataset);
1086
1384
  }
1087
1385
  function collectEffectStoppers(projectedSources, dataset) {
1088
1386
  const out = /* @__PURE__ */ new Set();
@@ -1148,7 +1446,7 @@ function applyOneModifier(source, effect, mi, ctx, dataset) {
1148
1446
  if (computed === null) return;
1149
1447
  const targets = ctx.targetsForModifier(mi, source);
1150
1448
  if (targets.length === 0) return;
1151
- const stackingGroup = computeStackingGroup(source, mi, dataset);
1449
+ const stackingGroup = computeStackingGroup(source, mi, dataset, effect);
1152
1450
  const isMul = op === "PreMul" || op === "PostMul" || op === "PreDiv" || op === "PostDiv";
1153
1451
  const value = isMul && computed.scaled ? 1 + computed.value : computed.value;
1154
1452
  const sdeDefault = isMul ? dataset.attributes.get(mi.modifiedAttributeID)?.defaultValue ?? 0 : 0;
@@ -1656,7 +1954,52 @@ var SHIP_BONUS_SCALING_SKILL = /* @__PURE__ */ new Map([
1656
1954
  [6112, 24313],
1657
1955
  [6113, 24312],
1658
1956
  [6114, 24311],
1659
- [6116, 24314]
1957
+ [6116, 24314],
1958
+ // ----- Auto-derived from the SDE: attrs that are BOTH skill-level-scaled
1959
+ // (a skill effect does `attr ×= skillLevel` via attr 280) AND read by a
1960
+ // SHIP-side effect as the bonus value. These are per-racial/role-skill hull
1961
+ // bonuses whose ship-side reader must scale by the skill level. Previously
1962
+ // missing → the bonus was taken at base (×1) instead of ×5 at All-V.
1963
+ // Notable: Exhumer/Barge shield+armor resist role bonuses (Hulk/Skiff/
1964
+ // Mackinaw shield resist 4 %→20 %), Bhaalgorn drone+laser (492), industrial
1965
+ // command (Orca/Rorqual), Marauder/pirate/expedition hull bonuses.
1966
+ [66, 89611],
1967
+ [310, 3432],
1968
+ [349, 19760],
1969
+ [492, 3339],
1970
+ [1296, 21610],
1971
+ [1669, 3184],
1972
+ [1670, 3184],
1973
+ [1842, 32918],
1974
+ [3167, 33856],
1975
+ [3181, 17940],
1976
+ [3182, 17940],
1977
+ [3183, 17940],
1978
+ [3184, 17940],
1979
+ [3185, 17940],
1980
+ [3187, 17940],
1981
+ [3188, 17940],
1982
+ [3190, 33856],
1983
+ [3191, 33856],
1984
+ [3192, 33856],
1985
+ [3193, 22551],
1986
+ [3194, 22551],
1987
+ [3197, 22551],
1988
+ [3198, 22551],
1989
+ [3199, 22551],
1990
+ [3203, 29637],
1991
+ [3204, 29637],
1992
+ [3205, 29637],
1993
+ [3210, 3341],
1994
+ [3221, 29637],
1995
+ [3222, 29637],
1996
+ [3223, 28374],
1997
+ [3224, 28374],
1998
+ [3237, 32918],
1999
+ [3240, 32918],
2000
+ [3326, 28374],
2001
+ [6088, 33092],
2002
+ [6089, 33094]
1660
2003
  ]);
1661
2004
  var SUBSYSTEM_BONUS_SCALING_SKILL = /* @__PURE__ */ new Map([
1662
2005
  // Amarr
@@ -1711,8 +2054,42 @@ var SUBSYSTEM_BONUS_SCALING_SKILL = /* @__PURE__ */ new Map([
1711
2054
  // MinmatarDefensive
1712
2055
  [1449, 30551],
1713
2056
  // MinmatarOffensive
1714
- [1450, 30554]
2057
+ [1450, 30554],
1715
2058
  // MinmatarPropulsion
2059
+ // ----- Authoritative completion: every subsystem-bonus attr that the SDE
2060
+ // ----- scales by a racial subsystem skill, derived verbatim from the
2061
+ // ----- `subsystemSkillLevel*` skill effects (modAttr = bonus attr,
2062
+ // ----- modifying = 280/skillLevel, PreMul). The Amarr/Caldari secondaries
2063
+ // ----- above were hand-added; Gallente + Minmatar secondaries (and the
2064
+ // ----- 2680-2687 defensive/core block) were MISSING, so subsystem-sourced
2065
+ // ----- bonuses on those races (e.g. Loki Propulsion agility 1523, Offensive
2066
+ // ----- RoF 1522 / 1534) fell to the flat path and applied ×1 instead of ×5.
2067
+ // ----- Duplicates of the entries above are harmless (same value).
2068
+ [1517, 30540],
2069
+ [1519, 30546],
2070
+ [1520, 30553],
2071
+ [1521, 30550],
2072
+ // Gallente Def/Core/Prop/Off secondaries
2073
+ [1522, 30551],
2074
+ [1523, 30554],
2075
+ [1525, 30547],
2076
+ [1526, 30545],
2077
+ // Minmatar Off/Prop/Core/Def secondaries
2078
+ [1531, 30537],
2079
+ [1532, 30550],
2080
+ [1533, 30549],
2081
+ [1534, 30551],
2082
+ // Off cross-race tertiaries (Amarr/Gallente/Caldari/Minmatar)
2083
+ [2680, 30532],
2084
+ [2681, 30539],
2085
+ [2682, 30544],
2086
+ [2683, 30548],
2087
+ // Def/Core extra (Amarr/Caldari)
2088
+ [2684, 30540],
2089
+ [2685, 30546],
2090
+ [2686, 30545],
2091
+ [2687, 30547]
2092
+ // Def/Core extra (Gallente/Minmatar)
1716
2093
  ]);
1717
2094
  function computeModifierValue(source, mi, ctx, dataset, op) {
1718
2095
  if (mi.modifyingAttributeID === void 0) return null;
@@ -1752,7 +2129,7 @@ function computeModifierValue(source, mi, ctx, dataset, op) {
1752
2129
  if (skillID === void 0) return null;
1753
2130
  const level = ctx.skillLevel(skillID);
1754
2131
  if (level === 0) return null;
1755
- if (source.kind === "ship" && SHIP_ROLE_BONUS_ATTRS.has(mi.modifyingAttributeID)) {
2132
+ if (source.kind === "ship") {
1756
2133
  return { value: baseValue, scaled: false };
1757
2134
  }
1758
2135
  if (itemRequiresSkill(source, skillID)) {
@@ -1762,46 +2139,21 @@ function computeModifierValue(source, mi, ctx, dataset, op) {
1762
2139
  }
1763
2140
  return { value: baseValue, scaled: false };
1764
2141
  }
1765
- var SHIP_ROLE_BONUS_ATTRS = /* @__PURE__ */ new Set([
1766
- 793,
1767
- // shipBonusRole7
1768
- 1688,
1769
- // shipBonusRole8
1770
- 1803,
1771
- // MWDSignatureRadiusBonus — Assault Frigate / Interceptor MWD sig role bonus (flat)
1772
- 2059,
1773
- // eliteBonusCommandDestroyer1 — Command Destroyer T2 specialisation (flat per-level applied via skill, but the SHIP-side reader is flat)
1774
- 2060,
1775
- // eliteBonusCommandDestroyer2
1776
- 2064,
1777
- // roleBonusCD — Command Destroyer command burst PG / activation cost reduction.
1778
- // Without this entry, effect 6214 (Draugur's `roleBonusCDLinksPGReduction`)
1779
- // double-applies the -95 % bonus at × Skirmish Command Burst V → -475 %
1780
- // PostPercent on Skirmish Command Burst II `power` (110) → -412.5 MW per
1781
- // burst → total ship power used reads −738 MW instead of +97 MW.
1782
- 2298,
1783
- // shipBonusRole1
1784
- 2299,
1785
- // shipBonusRole2
1786
- 2300,
1787
- // shipBonusRole3
1788
- 2301,
1789
- // shipBonusRole4
1790
- 2302,
1791
- // shipBonusRole5
1792
- 2303,
1793
- // shipBonusRole6
1794
- 5952
1795
- // shipBonusGasCloudDurationRoleBonusOreMiningDestroyer
1796
- ]);
1797
- function computeStackingGroup(source, mi, dataset) {
2142
+ function computeStackingGroup(source, mi, dataset, effect) {
1798
2143
  if (NO_PENALTY_KINDS.has(source.kind)) return null;
1799
2144
  if (mi.modifiedAttributeID !== void 0) {
1800
2145
  const attr = dataset.attributes.get(mi.modifiedAttributeID);
1801
2146
  if (attr?.stackable) return null;
1802
2147
  }
2148
+ const group = STACKING_PENALTY_GROUPS.get(effect.id);
2149
+ if (group !== void 0 && group !== "default" && CUSTOM_STACK_GROUPS_HONOURED.has(group)) {
2150
+ return `${group}:${mi.modifiedAttributeID}`;
2151
+ }
1803
2152
  return `attr:${mi.modifiedAttributeID}`;
1804
2153
  }
2154
+ var CUSTOM_STACK_GROUPS_HONOURED = /* @__PURE__ */ new Set([
2155
+ "cloakingScanResolutionMultiplier"
2156
+ ]);
1805
2157
  function mapSourceKind(kind) {
1806
2158
  switch (kind) {
1807
2159
  case "ship":
@@ -3519,6 +3871,13 @@ var LEGACY_HANDLED_HARDCODED_EFFECTS = /* @__PURE__ */ new Set([
3519
3871
  6658
3520
3872
  // Bastion Module — applyLegacyBastion
3521
3873
  ]);
3874
+ var SEC_STATUS_SCALED_EFFECT_IDS = /* @__PURE__ */ new Set([
3875
+ 6871,
3876
+ 12165,
3877
+ 12181,
3878
+ 12185,
3879
+ 12202
3880
+ ]);
3522
3881
  var LEGACY_HANDLED_EFFECT_IDS = /* @__PURE__ */ new Set([
3523
3882
  ...LEGACY_HANDLED_PASSIVE_ADD_EFFECTS,
3524
3883
  ...LEGACY_HANDLED_HARDCODED_EFFECTS
@@ -3589,6 +3948,176 @@ function computeTotalEhp(shield, armor, hull, useProfile) {
3589
3948
  return Number.isFinite(total) ? total : Number.MAX_SAFE_INTEGER;
3590
3949
  }
3591
3950
 
3951
+ // src/fitChecks.ts
3952
+ function readAttr(t, id) {
3953
+ return t.attributes.find((a) => a.id === id)?.v;
3954
+ }
3955
+ function typeFitsSlotType(t, slot) {
3956
+ for (const e of t.effects) {
3957
+ const mapped = SLOT_EFFECT_TO_SLOT_TYPE[e.id];
3958
+ if (mapped === slot) return true;
3959
+ }
3960
+ return false;
3961
+ }
3962
+ function isTurretWeapon(t) {
3963
+ return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "TURRET");
3964
+ }
3965
+ function isMissileLauncher(t) {
3966
+ return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "MISSILE");
3967
+ }
3968
+ function isSmartBomb(t) {
3969
+ return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "SMARTBOMB");
3970
+ }
3971
+ function shipGroupRestrictions(t) {
3972
+ const out = [];
3973
+ for (const id of CAN_FIT_SHIP_GROUP_ATTRS) {
3974
+ const v = readAttr(t, id);
3975
+ if (v != null && v > 0) out.push(Math.round(v));
3976
+ }
3977
+ return out;
3978
+ }
3979
+ function shipTypeRestrictions(t) {
3980
+ const out = [];
3981
+ for (const id of CAN_FIT_SHIP_TYPE_ATTRS) {
3982
+ const v = readAttr(t, id);
3983
+ if (v != null && v > 0) out.push(Math.round(v));
3984
+ }
3985
+ return out;
3986
+ }
3987
+ function maxGroupFittedFor(mod) {
3988
+ const v = readAttr(mod, ATTR.MAX_GROUP_FITTED);
3989
+ return v != null && v > 0 ? Math.round(v) : void 0;
3990
+ }
3991
+ function maxTypeFittedFor(mod) {
3992
+ const v = readAttr(mod, ATTR.MAX_TYPE_FITTED);
3993
+ return v != null && v > 0 ? Math.round(v) : void 0;
3994
+ }
3995
+ function freeFitGroupSlotsFor(mod, fittedModules, dataset) {
3996
+ const groupCap = maxGroupFittedFor(mod);
3997
+ const typeCap = maxTypeFittedFor(mod);
3998
+ if (groupCap === void 0 && typeCap === void 0) return Number.POSITIVE_INFINITY;
3999
+ let groupCount = 0;
4000
+ let typeCount = 0;
4001
+ for (const fm of fittedModules) {
4002
+ if (typeCap !== void 0 && fm.typeID === mod.id) typeCount++;
4003
+ if (groupCap !== void 0) {
4004
+ const t = dataset.getType(fm.typeID);
4005
+ if (t && t.groupID === mod.groupID) groupCount++;
4006
+ }
4007
+ }
4008
+ let free = Number.POSITIVE_INFINITY;
4009
+ if (groupCap !== void 0) free = Math.min(free, groupCap - groupCount);
4010
+ if (typeCap !== void 0) free = Math.min(free, typeCap - typeCount);
4011
+ return Math.max(0, free);
4012
+ }
4013
+ function canFitModuleOnShip(mod, ship, slot, fitContext) {
4014
+ if (!typeFitsSlotType(mod, slot)) {
4015
+ return { ok: false, reason: `Module does not declare a ${slot} slot effect` };
4016
+ }
4017
+ if (!ship) return { ok: true };
4018
+ const allowedGroups = shipGroupRestrictions(mod);
4019
+ const allowedTypes = shipTypeRestrictions(mod);
4020
+ if (allowedGroups.length > 0 || allowedTypes.length > 0) {
4021
+ const groupOk = allowedGroups.includes(ship.groupID);
4022
+ const typeOk = allowedTypes.includes(ship.id);
4023
+ if (!groupOk && !typeOk) {
4024
+ return { ok: false, reason: "Ship hull/group not allowed by this module" };
4025
+ }
4026
+ }
4027
+ if (slot === "RIG") {
4028
+ const modSize = readAttr(mod, ATTR.RIG_SIZE);
4029
+ const shipSize = readAttr(ship, ATTR.RIG_SIZE);
4030
+ if (modSize != null && shipSize != null && Math.round(modSize) !== Math.round(shipSize)) {
4031
+ return { ok: false, reason: "Rig size mismatch" };
4032
+ }
4033
+ }
4034
+ if (slot === "SUBSYSTEM") {
4035
+ const fitsTo = readAttr(mod, 1380);
4036
+ if (fitsTo != null && fitsTo > 0 && Math.round(fitsTo) !== ship.id) {
4037
+ return { ok: false, reason: "Subsystem locked to a different T3C hull" };
4038
+ }
4039
+ }
4040
+ if (slot === "HI") {
4041
+ if (isTurretWeapon(mod)) {
4042
+ const turrets = readAttr(ship, ATTR.TURRET_HARDPOINTS) ?? 0;
4043
+ if (turrets <= 0) {
4044
+ return { ok: false, reason: "Ship has no turret hardpoints" };
4045
+ }
4046
+ } else if (isMissileLauncher(mod)) {
4047
+ const launchers = readAttr(ship, ATTR.LAUNCHER_HARDPOINTS) ?? 0;
4048
+ if (launchers <= 0) {
4049
+ return { ok: false, reason: "Ship has no launcher hardpoints" };
4050
+ }
4051
+ }
4052
+ }
4053
+ if (fitContext) {
4054
+ const free = freeFitGroupSlotsFor(mod, fitContext.fittedModules, fitContext.dataset);
4055
+ if (free <= 0) {
4056
+ const cap = maxTypeFittedFor(mod) ?? maxGroupFittedFor(mod);
4057
+ return { ok: false, reason: `Only ${cap} of this module type can be fitted to a ship` };
4058
+ }
4059
+ }
4060
+ return { ok: true };
4061
+ }
4062
+ function chargeGroupsForModule(mod) {
4063
+ const out = [];
4064
+ for (const attrID of CHARGE_GROUP_ATTRS) {
4065
+ const v = readAttr(mod, attrID);
4066
+ if (v != null && v > 0) out.push(Math.round(v));
4067
+ }
4068
+ return out;
4069
+ }
4070
+ function moduleAcceptsAnyCharge(mod) {
4071
+ return chargeGroupsForModule(mod).length > 0;
4072
+ }
4073
+ function moduleAcceptsChargeType(mod, charge) {
4074
+ const allowedGroups = chargeGroupsForModule(mod);
4075
+ if (allowedGroups.length === 0) return false;
4076
+ if (!allowedGroups.includes(charge.groupID)) return false;
4077
+ const modSize = readAttr(mod, ATTR.CHARGE_SIZE);
4078
+ const chargeSize = readAttr(charge, ATTR.CHARGE_SIZE);
4079
+ if (modSize != null && chargeSize != null && Math.round(chargeSize) > Math.round(modSize)) {
4080
+ return false;
4081
+ }
4082
+ return true;
4083
+ }
4084
+ var ACTIVE_EFFECT_CATEGORIES = /* @__PURE__ */ new Set([1, 2, 3]);
4085
+ function isActivatableModule(mod, effects) {
4086
+ for (const e of mod.effects) {
4087
+ const eff = effects.get(e.id);
4088
+ if (eff && eff.effectCategoryID !== void 0 && ACTIVE_EFFECT_CATEGORIES.has(eff.effectCategoryID)) {
4089
+ return true;
4090
+ }
4091
+ }
4092
+ return false;
4093
+ }
4094
+ var DEFAULT_OFFLINE_ACTIVATION_GROUPS = /* @__PURE__ */ new Set([
4095
+ 330,
4096
+ 4117
4097
+ ]);
4098
+ function defaultStateForModule(mod, effects) {
4099
+ if (DEFAULT_OFFLINE_ACTIVATION_GROUPS.has(mod.groupID)) return "ONLINE";
4100
+ return isActivatableModule(mod, effects) ? "ACTIVE" : "ONLINE";
4101
+ }
4102
+ function freeHardpointsFor(mod, ship, fittedHiModules, dataset) {
4103
+ const turretCap = readAttr(ship, ATTR.TURRET_HARDPOINTS) ?? 0;
4104
+ const launcherCap = readAttr(ship, ATTR.LAUNCHER_HARDPOINTS) ?? 0;
4105
+ let turretUsed = 0;
4106
+ let launcherUsed = 0;
4107
+ for (const m of fittedHiModules) {
4108
+ const t = dataset.getType(m.typeID);
4109
+ if (!t) continue;
4110
+ if (isTurretWeapon(t)) turretUsed++;
4111
+ else if (isMissileLauncher(t)) launcherUsed++;
4112
+ }
4113
+ let hardpointFree;
4114
+ if (isTurretWeapon(mod)) hardpointFree = Math.max(0, turretCap - turretUsed);
4115
+ else if (isMissileLauncher(mod)) hardpointFree = Math.max(0, launcherCap - launcherUsed);
4116
+ else hardpointFree = Number.POSITIVE_INFINITY;
4117
+ const groupFree = freeFitGroupSlotsFor(mod, fittedHiModules, dataset);
4118
+ return Math.min(hardpointFree, groupFree);
4119
+ }
4120
+
3592
4121
  // src/derived/capacitor.ts
3593
4122
  function computeCapacitor(ctx, dataset) {
3594
4123
  const ship = ctx.ship;
@@ -3668,6 +4197,7 @@ var CAP_BOOSTER_EFFECT_ID = 48;
3668
4197
  var ATTR_CAPACITOR_BONUS = 67;
3669
4198
  var ATTR_CAPACITOR_NEED2 = 6;
3670
4199
  var ATTR_DURATION_MS2 = 73;
4200
+ var ATTR_REACTIVATION_MS = 669;
3671
4201
  var ATTR_RELOAD_TIME = 1795;
3672
4202
  var ATTR_CAPACITY = 38;
3673
4203
  var ATTR_VOLUME = 161;
@@ -3702,8 +4232,9 @@ function drainEntryFromEffect(effect, mod) {
3702
4232
  if (effect.dischargeAttributeID === void 0) return null;
3703
4233
  const discharge = mod.getFinal(effect.dischargeAttributeID, 0);
3704
4234
  if (discharge <= 0) return null;
3705
- const cycleMs = effect.durationAttributeID !== void 0 ? mod.getFinal(effect.durationAttributeID, 0) : 1e3;
3706
- if (cycleMs <= 0) return null;
4235
+ const baseCycleMs = effect.durationAttributeID !== void 0 ? mod.getFinal(effect.durationAttributeID, 0) : 1e3;
4236
+ if (baseCycleMs <= 0) return null;
4237
+ const cycleMs = Math.round(baseCycleMs + mod.getFinal(ATTR_REACTIVATION_MS, 0));
3707
4238
  return {
3708
4239
  cycleMs,
3709
4240
  capNeed: discharge,
@@ -3711,7 +4242,12 @@ function drainEntryFromEffect(effect, mod) {
3711
4242
  // ammo doesn't reload for cap purposes on regular modules
3712
4243
  reloadMs: 0,
3713
4244
  isInjector: false,
3714
- disableStagger: false
4245
+ // Pyfa: `disableStagger = mod.hardpoint == TURRET`. Turrets fire as a
4246
+ // synchronized volley (their cap drains aggregate, capNeed × N at the
4247
+ // shared cycle); everything else is staggered evenly across its cycle.
4248
+ // Without this, N turrets were staggered (one drain at cycle/N) instead
4249
+ // of N together, shifting the cap timeline (time-to-empty off ~5-10 %).
4250
+ disableStagger: isTurretWeapon(mod.type)
3715
4251
  };
3716
4252
  }
3717
4253
  function boosterDrainEntry(mod) {
@@ -3719,19 +4255,19 @@ function boosterDrainEntry(mod) {
3719
4255
  if (!charge) return null;
3720
4256
  const capNeed = mod.getFinal(ATTR_CAPACITOR_NEED2, 0);
3721
4257
  const inject = charge.getFinal(ATTR_CAPACITOR_BONUS, 0);
3722
- const cycleMs = mod.getFinal(ATTR_DURATION_MS2, 0);
4258
+ const cycleMs = Math.round(mod.getFinal(ATTR_DURATION_MS2, 0));
3723
4259
  if (cycleMs <= 0) return null;
3724
- let reloadMs = mod.getFinal(ATTR_RELOAD_TIME, 0);
4260
+ let reloadMs = Math.round(mod.getFinal(ATTR_RELOAD_TIME, 0));
3725
4261
  if (reloadMs <= 0) reloadMs = 1e4;
3726
4262
  const capacity = mod.getFinal(ATTR_CAPACITY, 0);
3727
4263
  const chargeVol = charge.getFinal(ATTR_VOLUME, 0);
3728
- const charges = capacity > 0 && chargeVol > 0 ? Math.floor(capacity / chargeVol) : 1;
4264
+ const charges = capacity > 0 && chargeVol > 0 ? Math.floor(capacity / chargeVol) : 0;
3729
4265
  return {
3730
4266
  cycleMs,
3731
4267
  // Pyfa convention: positive capNeed = drain, negative = injection.
3732
4268
  // For boosters the *net* per-cycle change is (capNeed_module - inject_charge).
3733
4269
  capNeed: capNeed - inject,
3734
- clipSize: Math.max(1, charges),
4270
+ clipSize: charges,
3735
4271
  reloadMs,
3736
4272
  isInjector: true,
3737
4273
  disableStagger: false
@@ -4663,8 +5199,8 @@ function computeFit(fit, dataset, opts) {
4663
5199
  }
4664
5200
  ctx.stoppedLocalEffectIDs = collectEffectStoppers(earlyProjected, dataset);
4665
5201
  }
4666
- for (const m of modules) applySourceItem(m, ctx, dataset);
4667
5202
  for (const m of modules) if (m.charge) applySourceItem(m.charge, ctx, dataset);
5203
+ for (const m of modules) applySourceItem(m, ctx, dataset);
4668
5204
  for (const d of drones) applySourceItem(d, ctx, dataset);
4669
5205
  for (const f of fighters) applySourceItem(f, ctx, dataset);
4670
5206
  for (const i of implants) applySourceItem(i, ctx, dataset);
@@ -4827,6 +5363,7 @@ function buildProjectionReport(source, ctx) {
4827
5363
  summary: labels[cls.kind] ?? cls.kind
4828
5364
  };
4829
5365
  }
5366
+ var CHAR_BASE_MAX_LOCKED_TARGETS = 2;
4830
5367
  function deriveStats(ctx, dataset, damageProfile, fit, projectionReports) {
4831
5368
  const ship = ctx.ship;
4832
5369
  const slotUsed = { HI: 0, MED: 0, LO: 0, RIG: 0, SUBSYSTEM: 0, SERVICE: 0 };
@@ -4947,7 +5484,18 @@ function deriveStats(ctx, dataset, damageProfile, fit, projectionReports) {
4947
5484
  ship.getFinal(ATTR.MAX_TARGET_RANGE, 0),
4948
5485
  ship.getFinal(ATTR.MAX_TARGET_RANGE_CAP, 3e5)
4949
5486
  ),
4950
- maxLockedTargets: ship.getFinal(ATTR.MAX_LOCKED_TARGETS, 0),
5487
+ // In-game lockable-target count is the LOWER of the ship's
5488
+ // maxLockedTargets and the pilot's skill-derived cap. Pyfa:
5489
+ // ceil(min(maxTargetsLockedFromSkills, ship maxLockedTargets)),
5490
+ // where maxTargetsLockedFromSkills seeds at a base of 2 (every
5491
+ // capsuleer locks 2 without skills) + Target Management / Advanced
5492
+ // Target Management (+1/level each) → 2 + 5 + 5 = 12 at All-V. The
5493
+ // character item carries only the skill ModAdds (10), so add the
5494
+ // base 2. Without the char cap, high-slot capitals reported 14-20.
5495
+ maxLockedTargets: Math.ceil(Math.min(
5496
+ CHAR_BASE_MAX_LOCKED_TARGETS + ctx.character.getFinal(ATTR.MAX_LOCKED_TARGETS, 0),
5497
+ ship.getFinal(ATTR.MAX_LOCKED_TARGETS, 0)
5498
+ )),
4951
5499
  signatureRadius: ship.getFinal(ATTR.SIGNATURE_RADIUS, 0),
4952
5500
  scanResolution: ship.getFinal(ATTR.SCAN_RESOLUTION, 0),
4953
5501
  sensorStrength: pickSensorStrength(ship).value,
@@ -5666,174 +6214,4 @@ var TARGET_PROFILE_PRESETS = [
5666
6214
  { name: "Keepstar", signatureRadius: 3e4, maxVelocity: 0, emResist: 0.7, thermalResist: 0.7, kineticResist: 0.7, explosiveResist: 0.7, isPreset: true }
5667
6215
  ];
5668
6216
 
5669
- // src/fitChecks.ts
5670
- function readAttr(t, id) {
5671
- return t.attributes.find((a) => a.id === id)?.v;
5672
- }
5673
- function typeFitsSlotType(t, slot) {
5674
- for (const e of t.effects) {
5675
- const mapped = SLOT_EFFECT_TO_SLOT_TYPE[e.id];
5676
- if (mapped === slot) return true;
5677
- }
5678
- return false;
5679
- }
5680
- function isTurretWeapon(t) {
5681
- return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "TURRET");
5682
- }
5683
- function isMissileLauncher(t) {
5684
- return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "MISSILE");
5685
- }
5686
- function isSmartBomb(t) {
5687
- return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "SMARTBOMB");
5688
- }
5689
- function shipGroupRestrictions(t) {
5690
- const out = [];
5691
- for (const id of CAN_FIT_SHIP_GROUP_ATTRS) {
5692
- const v = readAttr(t, id);
5693
- if (v != null && v > 0) out.push(Math.round(v));
5694
- }
5695
- return out;
5696
- }
5697
- function shipTypeRestrictions(t) {
5698
- const out = [];
5699
- for (const id of CAN_FIT_SHIP_TYPE_ATTRS) {
5700
- const v = readAttr(t, id);
5701
- if (v != null && v > 0) out.push(Math.round(v));
5702
- }
5703
- return out;
5704
- }
5705
- function maxGroupFittedFor(mod) {
5706
- const v = readAttr(mod, ATTR.MAX_GROUP_FITTED);
5707
- return v != null && v > 0 ? Math.round(v) : void 0;
5708
- }
5709
- function maxTypeFittedFor(mod) {
5710
- const v = readAttr(mod, ATTR.MAX_TYPE_FITTED);
5711
- return v != null && v > 0 ? Math.round(v) : void 0;
5712
- }
5713
- function freeFitGroupSlotsFor(mod, fittedModules, dataset) {
5714
- const groupCap = maxGroupFittedFor(mod);
5715
- const typeCap = maxTypeFittedFor(mod);
5716
- if (groupCap === void 0 && typeCap === void 0) return Number.POSITIVE_INFINITY;
5717
- let groupCount = 0;
5718
- let typeCount = 0;
5719
- for (const fm of fittedModules) {
5720
- if (typeCap !== void 0 && fm.typeID === mod.id) typeCount++;
5721
- if (groupCap !== void 0) {
5722
- const t = dataset.getType(fm.typeID);
5723
- if (t && t.groupID === mod.groupID) groupCount++;
5724
- }
5725
- }
5726
- let free = Number.POSITIVE_INFINITY;
5727
- if (groupCap !== void 0) free = Math.min(free, groupCap - groupCount);
5728
- if (typeCap !== void 0) free = Math.min(free, typeCap - typeCount);
5729
- return Math.max(0, free);
5730
- }
5731
- function canFitModuleOnShip(mod, ship, slot, fitContext) {
5732
- if (!typeFitsSlotType(mod, slot)) {
5733
- return { ok: false, reason: `Module does not declare a ${slot} slot effect` };
5734
- }
5735
- if (!ship) return { ok: true };
5736
- const allowedGroups = shipGroupRestrictions(mod);
5737
- const allowedTypes = shipTypeRestrictions(mod);
5738
- if (allowedGroups.length > 0 || allowedTypes.length > 0) {
5739
- const groupOk = allowedGroups.includes(ship.groupID);
5740
- const typeOk = allowedTypes.includes(ship.id);
5741
- if (!groupOk && !typeOk) {
5742
- return { ok: false, reason: "Ship hull/group not allowed by this module" };
5743
- }
5744
- }
5745
- if (slot === "RIG") {
5746
- const modSize = readAttr(mod, ATTR.RIG_SIZE);
5747
- const shipSize = readAttr(ship, ATTR.RIG_SIZE);
5748
- if (modSize != null && shipSize != null && Math.round(modSize) !== Math.round(shipSize)) {
5749
- return { ok: false, reason: "Rig size mismatch" };
5750
- }
5751
- }
5752
- if (slot === "SUBSYSTEM") {
5753
- const fitsTo = readAttr(mod, 1380);
5754
- if (fitsTo != null && fitsTo > 0 && Math.round(fitsTo) !== ship.id) {
5755
- return { ok: false, reason: "Subsystem locked to a different T3C hull" };
5756
- }
5757
- }
5758
- if (slot === "HI") {
5759
- if (isTurretWeapon(mod)) {
5760
- const turrets = readAttr(ship, ATTR.TURRET_HARDPOINTS) ?? 0;
5761
- if (turrets <= 0) {
5762
- return { ok: false, reason: "Ship has no turret hardpoints" };
5763
- }
5764
- } else if (isMissileLauncher(mod)) {
5765
- const launchers = readAttr(ship, ATTR.LAUNCHER_HARDPOINTS) ?? 0;
5766
- if (launchers <= 0) {
5767
- return { ok: false, reason: "Ship has no launcher hardpoints" };
5768
- }
5769
- }
5770
- }
5771
- if (fitContext) {
5772
- const free = freeFitGroupSlotsFor(mod, fitContext.fittedModules, fitContext.dataset);
5773
- if (free <= 0) {
5774
- const cap = maxTypeFittedFor(mod) ?? maxGroupFittedFor(mod);
5775
- return { ok: false, reason: `Only ${cap} of this module type can be fitted to a ship` };
5776
- }
5777
- }
5778
- return { ok: true };
5779
- }
5780
- function chargeGroupsForModule(mod) {
5781
- const out = [];
5782
- for (const attrID of CHARGE_GROUP_ATTRS) {
5783
- const v = readAttr(mod, attrID);
5784
- if (v != null && v > 0) out.push(Math.round(v));
5785
- }
5786
- return out;
5787
- }
5788
- function moduleAcceptsAnyCharge(mod) {
5789
- return chargeGroupsForModule(mod).length > 0;
5790
- }
5791
- function moduleAcceptsChargeType(mod, charge) {
5792
- const allowedGroups = chargeGroupsForModule(mod);
5793
- if (allowedGroups.length === 0) return false;
5794
- if (!allowedGroups.includes(charge.groupID)) return false;
5795
- const modSize = readAttr(mod, ATTR.CHARGE_SIZE);
5796
- const chargeSize = readAttr(charge, ATTR.CHARGE_SIZE);
5797
- if (modSize != null && chargeSize != null && Math.round(chargeSize) > Math.round(modSize)) {
5798
- return false;
5799
- }
5800
- return true;
5801
- }
5802
- var ACTIVE_EFFECT_CATEGORIES = /* @__PURE__ */ new Set([1, 2, 3]);
5803
- function isActivatableModule(mod, effects) {
5804
- for (const e of mod.effects) {
5805
- const eff = effects.get(e.id);
5806
- if (eff && eff.effectCategoryID !== void 0 && ACTIVE_EFFECT_CATEGORIES.has(eff.effectCategoryID)) {
5807
- return true;
5808
- }
5809
- }
5810
- return false;
5811
- }
5812
- var DEFAULT_OFFLINE_ACTIVATION_GROUPS = /* @__PURE__ */ new Set([
5813
- 330,
5814
- 4117
5815
- ]);
5816
- function defaultStateForModule(mod, effects) {
5817
- if (DEFAULT_OFFLINE_ACTIVATION_GROUPS.has(mod.groupID)) return "ONLINE";
5818
- return isActivatableModule(mod, effects) ? "ACTIVE" : "ONLINE";
5819
- }
5820
- function freeHardpointsFor(mod, ship, fittedHiModules, dataset) {
5821
- const turretCap = readAttr(ship, ATTR.TURRET_HARDPOINTS) ?? 0;
5822
- const launcherCap = readAttr(ship, ATTR.LAUNCHER_HARDPOINTS) ?? 0;
5823
- let turretUsed = 0;
5824
- let launcherUsed = 0;
5825
- for (const m of fittedHiModules) {
5826
- const t = dataset.getType(m.typeID);
5827
- if (!t) continue;
5828
- if (isTurretWeapon(t)) turretUsed++;
5829
- else if (isMissileLauncher(t)) launcherUsed++;
5830
- }
5831
- let hardpointFree;
5832
- if (isTurretWeapon(mod)) hardpointFree = Math.max(0, turretCap - turretUsed);
5833
- else if (isMissileLauncher(mod)) hardpointFree = Math.max(0, launcherCap - launcherUsed);
5834
- else hardpointFree = Number.POSITIVE_INFINITY;
5835
- const groupFree = freeFitGroupSlotsFor(mod, fittedHiModules, dataset);
5836
- return Math.min(hardpointFree, groupFree);
5837
- }
5838
-
5839
6217
  export { ACTIVATION_EFFECT_ID, ATTR, CATEGORY, DAMAGE_PROFILE_PRESETS, FitContext, ItemState, LEGACY_EFFECT_IDS, LEGACY_HANDLED_EFFECT_IDS, ModifiedAttribute, OPERATION_BY_SDE_CODE, OUT_OF_SCOPE_EFFECT_IDS, REPAIR_EFFECT_AMOUNT_ATTR, REQUIRED_SKILL_PAIRS, SLOT_EFFECT_ID, SLOT_EFFECT_TO_SLOT_TYPE, STACKING_PENALTY_K, TARGET_PROFILE_PRESETS, WEAPON_EFFECT_KIND, applyOneModifier, applySkills, applySourceItem, buildNameIndex, canFitModuleOnShip, chargeGroupsForModule, checkSkills, classifyEwar, classifyWeapon, combineJamChances, combineMultiplicative, combinePenalized, combineUnstacked, computeCapacitor, computeFit, computeLayerEhp, computeOffense, computeStructureMeta, computeT3CVariantCode, computeTank, computeTotalEhp, defaultStateForModule, disintegratorCyclesToFullSpool, disintegratorSpoolBonus, ecmJamChance, effectiveDps, ehpUnderProfile, formatDna, formatEft, formatMultibuy, formatTypeIds, freeFitGroupSlotsFor, freeHardpointsFor, isActivatableModule, isMissileLauncher, isSmartBomb, isTurretWeapon, marketGroupPlacement, maxGroupFittedFor, maxTypeFittedFor, moduleAcceptsAnyCharge, moduleAcceptsCharge, moduleAcceptsChargeType, parseEft, peakRecharge, readCycleInfo, readDamageComponents, readRangeInfo, rechargeRateAt, shipGroupRestrictions, shipTypeRestrictions, typeFitsSlotType, verifyLegacyEffectIds };