eve-fit-engine 0.1.35 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/data/manifest.json +1 -1
  2. package/data/v0c61c617c9b9/attributes.json +1 -0
  3. package/dist/index.cjs +816 -548
  4. package/dist/index.d.cts +104 -4
  5. package/dist/index.d.ts +104 -4
  6. package/dist/index.js +815 -549
  7. package/dist/node.cjs +816 -548
  8. package/dist/node.d.cts +1 -1
  9. package/dist/node.d.ts +1 -1
  10. package/dist/node.js +815 -549
  11. package/package.json +3 -1
  12. package/data/v796a8249240a/attributes.json +0 -1
  13. /package/data/{v796a8249240a → v0c61c617c9b9}/categories.json +0 -0
  14. /package/data/{v796a8249240a → v0c61c617c9b9}/clone-grades.json +0 -0
  15. /package/data/{v796a8249240a → v0c61c617c9b9}/dbuff-collections.json +0 -0
  16. /package/data/{v796a8249240a → v0c61c617c9b9}/dynamic-attributes.json +0 -0
  17. /package/data/{v796a8249240a → v0c61c617c9b9}/effects.json +0 -0
  18. /package/data/{v796a8249240a → v0c61c617c9b9}/groups.json +0 -0
  19. /package/data/{v796a8249240a → v0c61c617c9b9}/market-groups.json +0 -0
  20. /package/data/{v796a8249240a → v0c61c617c9b9}/meta-groups.json +0 -0
  21. /package/data/{v796a8249240a → v0c61c617c9b9}/types/charges.json +0 -0
  22. /package/data/{v796a8249240a → v0c61c617c9b9}/types/drones.json +0 -0
  23. /package/data/{v796a8249240a → v0c61c617c9b9}/types/fighters.json +0 -0
  24. /package/data/{v796a8249240a → v0c61c617c9b9}/types/implants.json +0 -0
  25. /package/data/{v796a8249240a → v0c61c617c9b9}/types/modules.json +0 -0
  26. /package/data/{v796a8249240a → v0c61c617c9b9}/types/mutaplasmids.json +0 -0
  27. /package/data/{v796a8249240a → v0c61c617c9b9}/types/ships.json +0 -0
  28. /package/data/{v796a8249240a → v0c61c617c9b9}/types/skills.json +0 -0
  29. /package/data/{v796a8249240a → v0c61c617c9b9}/types/structure-modules.json +0 -0
  30. /package/data/{v796a8249240a → v0c61c617c9b9}/types/structures.json +0 -0
  31. /package/data/{v796a8249240a → v0c61c617c9b9}/types/subsystems.json +0 -0
  32. /package/data/{v796a8249240a → v0c61c617c9b9}/types/system-effects.json +0 -0
  33. /package/data/{v796a8249240a → v0c61c617c9b9}/units.json +0 -0
package/dist/index.js CHANGED
@@ -464,6 +464,32 @@ var LEGACY_EFFECT_IDS = [
464
464
  // Frigate MJD sig bloom.
465
465
  { id: 12126, name: null, handler: "modifierEngine::applyLegacyMjdSigBloom (Frigate MJD)" }
466
466
  ];
467
+ var BOOSTER_SIDE_EFFECT_IDS = /* @__PURE__ */ new Set([
468
+ 2735,
469
+ // boosterArmorHpPenalty
470
+ 2736,
471
+ // boosterArmorRepairAmountPenalty
472
+ 2737,
473
+ // boosterShieldCapacityPenalty
474
+ 2739,
475
+ // boosterTurretOptimalRangePenalty
476
+ 2741,
477
+ // boosterTurretFalloffPenalty
478
+ 2745,
479
+ // boosterCapacitorCapacityPenalty
480
+ 2746,
481
+ // boosterMaxVelocityPenalty
482
+ 2747,
483
+ // boosterTurretTrackingPenalty
484
+ 2748,
485
+ // boosterMissileVelocityPenalty
486
+ 2749,
487
+ // boosterMissileExplosionVelocityPenalty
488
+ 2791,
489
+ // boosterMissileExplosionCloudPenaltyFixed
490
+ 4970
491
+ // boosterShieldBoostAmountPenaltyShieldSkills
492
+ ]);
467
493
  var OUT_OF_SCOPE_EFFECT_IDS = [
468
494
  // ---- Hardpoint / slot dispatch markers consumed via fitChecks (not a modifier dispatch path). ----
469
495
  { id: 40, name: null, handler: "fitChecks::isMissileLauncher (hardpoint marker)" },
@@ -551,7 +577,204 @@ function verifyLegacyEffectIds(effects) {
551
577
  return complaints;
552
578
  }
553
579
 
580
+ // src/fitChecks.ts
581
+ function readAttr(t, id) {
582
+ return t.attributes.find((a) => a.id === id)?.v;
583
+ }
584
+ function typeFitsSlotType(t, slot) {
585
+ for (const e of t.effects) {
586
+ const mapped = SLOT_EFFECT_TO_SLOT_TYPE[e.id];
587
+ if (mapped === slot) return true;
588
+ }
589
+ return false;
590
+ }
591
+ function isTurretWeapon(t) {
592
+ return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "TURRET");
593
+ }
594
+ function isMissileLauncher(t) {
595
+ return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "MISSILE");
596
+ }
597
+ function isSmartBomb(t) {
598
+ return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "SMARTBOMB");
599
+ }
600
+ function shipGroupRestrictions(t) {
601
+ const out = [];
602
+ for (const id of CAN_FIT_SHIP_GROUP_ATTRS) {
603
+ const v = readAttr(t, id);
604
+ if (v != null && v > 0) out.push(Math.round(v));
605
+ }
606
+ return out;
607
+ }
608
+ function shipTypeRestrictions(t) {
609
+ const out = [];
610
+ for (const id of CAN_FIT_SHIP_TYPE_ATTRS) {
611
+ const v = readAttr(t, id);
612
+ if (v != null && v > 0) out.push(Math.round(v));
613
+ }
614
+ return out;
615
+ }
616
+ function maxGroupFittedFor(mod) {
617
+ const v = readAttr(mod, ATTR.MAX_GROUP_FITTED);
618
+ return v != null && v > 0 ? Math.round(v) : void 0;
619
+ }
620
+ function maxTypeFittedFor(mod) {
621
+ const v = readAttr(mod, ATTR.MAX_TYPE_FITTED);
622
+ return v != null && v > 0 ? Math.round(v) : void 0;
623
+ }
624
+ function freeFitGroupSlotsFor(mod, fittedModules, dataset) {
625
+ const groupCap = maxGroupFittedFor(mod);
626
+ const typeCap = maxTypeFittedFor(mod);
627
+ if (groupCap === void 0 && typeCap === void 0) return Number.POSITIVE_INFINITY;
628
+ let groupCount = 0;
629
+ let typeCount = 0;
630
+ for (const fm of fittedModules) {
631
+ if (typeCap !== void 0 && fm.typeID === mod.id) typeCount++;
632
+ if (groupCap !== void 0) {
633
+ const t = dataset.getType(fm.typeID);
634
+ if (t && t.groupID === mod.groupID) groupCount++;
635
+ }
636
+ }
637
+ let free = Number.POSITIVE_INFINITY;
638
+ if (groupCap !== void 0) free = Math.min(free, groupCap - groupCount);
639
+ if (typeCap !== void 0) free = Math.min(free, typeCap - typeCount);
640
+ return Math.max(0, free);
641
+ }
642
+ function canFitModuleOnShip(mod, ship, slot, fitContext) {
643
+ if (!typeFitsSlotType(mod, slot)) {
644
+ return { ok: false, reason: `Module does not declare a ${slot} slot effect` };
645
+ }
646
+ if (!ship) return { ok: true };
647
+ const allowedGroups = shipGroupRestrictions(mod);
648
+ const allowedTypes = shipTypeRestrictions(mod);
649
+ if (allowedGroups.length > 0 || allowedTypes.length > 0) {
650
+ const groupOk = allowedGroups.includes(ship.groupID);
651
+ const typeOk = allowedTypes.includes(ship.id);
652
+ if (!groupOk && !typeOk) {
653
+ return { ok: false, reason: "Ship hull/group not allowed by this module" };
654
+ }
655
+ }
656
+ if (slot === "RIG") {
657
+ const modSize = readAttr(mod, ATTR.RIG_SIZE);
658
+ const shipSize = readAttr(ship, ATTR.RIG_SIZE);
659
+ if (modSize != null && shipSize != null && Math.round(modSize) !== Math.round(shipSize)) {
660
+ return { ok: false, reason: "Rig size mismatch" };
661
+ }
662
+ }
663
+ if (slot === "SUBSYSTEM") {
664
+ const fitsTo = readAttr(mod, 1380);
665
+ if (fitsTo != null && fitsTo > 0 && Math.round(fitsTo) !== ship.id) {
666
+ return { ok: false, reason: "Subsystem locked to a different T3C hull" };
667
+ }
668
+ }
669
+ if (slot === "HI") {
670
+ if (isTurretWeapon(mod)) {
671
+ const turrets = readAttr(ship, ATTR.TURRET_HARDPOINTS) ?? 0;
672
+ if (turrets <= 0) {
673
+ return { ok: false, reason: "Ship has no turret hardpoints" };
674
+ }
675
+ } else if (isMissileLauncher(mod)) {
676
+ const launchers = readAttr(ship, ATTR.LAUNCHER_HARDPOINTS) ?? 0;
677
+ if (launchers <= 0) {
678
+ return { ok: false, reason: "Ship has no launcher hardpoints" };
679
+ }
680
+ }
681
+ }
682
+ if (fitContext) {
683
+ const free = freeFitGroupSlotsFor(mod, fitContext.fittedModules, fitContext.dataset);
684
+ if (free <= 0) {
685
+ const cap = maxTypeFittedFor(mod) ?? maxGroupFittedFor(mod);
686
+ return { ok: false, reason: `Only ${cap} of this module type can be fitted to a ship` };
687
+ }
688
+ }
689
+ return { ok: true };
690
+ }
691
+ function chargeGroupsForModule(mod) {
692
+ const out = [];
693
+ for (const attrID of CHARGE_GROUP_ATTRS) {
694
+ const v = readAttr(mod, attrID);
695
+ if (v != null && v > 0) out.push(Math.round(v));
696
+ }
697
+ return out;
698
+ }
699
+ function moduleAcceptsAnyCharge(mod) {
700
+ return chargeGroupsForModule(mod).length > 0;
701
+ }
702
+ function moduleAcceptsChargeType(mod, charge) {
703
+ const allowedGroups = chargeGroupsForModule(mod);
704
+ if (allowedGroups.length === 0) return false;
705
+ if (!allowedGroups.includes(charge.groupID)) return false;
706
+ const modSize = readAttr(mod, ATTR.CHARGE_SIZE);
707
+ const chargeSize = readAttr(charge, ATTR.CHARGE_SIZE);
708
+ if (modSize != null && chargeSize != null && Math.round(chargeSize) > Math.round(modSize)) {
709
+ return false;
710
+ }
711
+ return true;
712
+ }
713
+ var ACTIVE_EFFECT_CATEGORIES = /* @__PURE__ */ new Set([1, 2, 3]);
714
+ function isActivatableModule(mod, effects) {
715
+ for (const e of mod.effects) {
716
+ const eff = effects.get(e.id);
717
+ if (eff && eff.effectCategoryID !== void 0 && ACTIVE_EFFECT_CATEGORIES.has(eff.effectCategoryID)) {
718
+ return true;
719
+ }
720
+ }
721
+ return false;
722
+ }
723
+ var DEFAULT_OFFLINE_ACTIVATION_GROUPS = /* @__PURE__ */ new Set([
724
+ 330,
725
+ 4117
726
+ ]);
727
+ function defaultStateForModule(mod, effects) {
728
+ if (DEFAULT_OFFLINE_ACTIVATION_GROUPS.has(mod.groupID)) return "ONLINE";
729
+ return isActivatableModule(mod, effects) ? "ACTIVE" : "ONLINE";
730
+ }
731
+ function freeHardpointsFor(mod, ship, fittedHiModules, dataset) {
732
+ const turretCap = readAttr(ship, ATTR.TURRET_HARDPOINTS) ?? 0;
733
+ const launcherCap = readAttr(ship, ATTR.LAUNCHER_HARDPOINTS) ?? 0;
734
+ let turretUsed = 0;
735
+ let launcherUsed = 0;
736
+ for (const m of fittedHiModules) {
737
+ const t = dataset.getType(m.typeID);
738
+ if (!t) continue;
739
+ if (isTurretWeapon(t)) turretUsed++;
740
+ else if (isMissileLauncher(t)) launcherUsed++;
741
+ }
742
+ let hardpointFree;
743
+ if (isTurretWeapon(mod)) hardpointFree = Math.max(0, turretCap - turretUsed);
744
+ else if (isMissileLauncher(mod)) hardpointFree = Math.max(0, launcherCap - launcherUsed);
745
+ else hardpointFree = Number.POSITIVE_INFINITY;
746
+ const groupFree = freeFitGroupSlotsFor(mod, fittedHiModules, dataset);
747
+ return Math.min(hardpointFree, groupFree);
748
+ }
749
+ var TACTICAL_DESTROYER_GROUP = 1305;
750
+ var MODED_HULL_NAMES = /* @__PURE__ */ new Set(["Anhinga"]);
751
+ var SHIP_MODIFIERS_GROUP = 1306;
752
+ function defaultModeTypeIDFor(ship, dataset) {
753
+ if (ship.groupID !== TACTICAL_DESTROYER_GROUP && !MODED_HULL_NAMES.has(ship.name ?? "")) return void 0;
754
+ const prefix = (ship.name ?? "").toLowerCase();
755
+ if (!prefix) return void 0;
756
+ let best;
757
+ for (const t of dataset.typesByBucket.modules?.values() ?? []) {
758
+ if (t.groupID !== SHIP_MODIFIERS_GROUP) continue;
759
+ if (!(t.name ?? "").toLowerCase().startsWith(prefix)) continue;
760
+ if (best === void 0 || t.id < best.id) best = t;
761
+ }
762
+ return best?.id;
763
+ }
764
+
554
765
  // src/fitContext.ts
766
+ var MISSILE_BOOST_SKILLS = [
767
+ 3319,
768
+ // Missile Launcher Operation
769
+ 3323
770
+ // Defender Missiles
771
+ ];
772
+ var PYFA_GROUP_FILTERED_EFFECTS = /* @__PURE__ */ new Map([
773
+ // 3495 shipCapPropulsionJamming — the Interceptor / Interdictor role bonus
774
+ // to Warp Scrambler + Stasis Web capacitor need. pyfa:
775
+ // `mod.item.group.name in ('Stasis Web', 'Warp Scrambler')`.
776
+ [3495, /* @__PURE__ */ new Set([52, 65])]
777
+ ]);
555
778
  var FitContext = class {
556
779
  constructor(init) {
557
780
  /** Projected hostile sources currently in scope. Populated by the
@@ -659,14 +882,19 @@ var FitContext = class {
659
882
  * - EffectStopper: handled outside this resolver (stops other effects
660
883
  * rather than applying a modifier).
661
884
  */
662
- targetsForModifier(modifier, source) {
885
+ targetsForModifier(modifier, source, effectID) {
663
886
  const root = this.resolveDomain(modifier.domain, source);
664
887
  switch (modifier.func) {
665
888
  case "ItemModifier":
889
+ if (modifier.domain === "charID" && modifier.modifiedAttributeID === ATTR.DRONE_CONTROL_RANGE) {
890
+ return this.ship ? [this.ship] : [];
891
+ }
666
892
  if (modifier.domain === "charID" && source.kind === "module" && modifier.modifiedAttributeID === 212) {
667
893
  const out = [];
668
894
  for (const m of this.modules) {
669
- if (m.charge) out.push(m.charge);
895
+ if (!m.charge) continue;
896
+ if (!MISSILE_BOOST_SKILLS.some((sid) => itemRequiresSkill(m.charge, sid))) continue;
897
+ out.push(m.charge);
670
898
  }
671
899
  return out;
672
900
  }
@@ -678,7 +906,10 @@ var FitContext = class {
678
906
  return this.itemsInLocation(root).filter((it) => it.groupID === modifier.groupID);
679
907
  }
680
908
  case "LocationRequiredSkillModifier": {
681
- if (!root || modifier.skillTypeID === void 0) return [];
909
+ if (!root) return [];
910
+ const groups = effectID === void 0 ? void 0 : PYFA_GROUP_FILTERED_EFFECTS.get(effectID);
911
+ if (groups) return this.itemsInLocation(root).filter((it) => groups.has(it.groupID));
912
+ if (modifier.skillTypeID === void 0) return [];
682
913
  const sid = modifier.skillTypeID;
683
914
  return this.itemsInLocation(root).filter((it) => itemRequiresSkill(it, sid));
684
915
  }
@@ -854,15 +1085,11 @@ var ModifiedAttribute = class {
854
1085
  for (const a of byOp.get("PostDiv") ?? []) {
855
1086
  postMulEntries.push({ value: 1 / a.value, stackingGroup: a.stackingGroup });
856
1087
  }
857
- if (postMulEntries.length > 0) {
858
- value *= combineMultiplicative(postMulEntries);
859
- }
860
- const postPercentEntries = [];
861
1088
  for (const a of byOp.get("PostPercent") ?? []) {
862
- postPercentEntries.push({ value: 1 + a.value, stackingGroup: a.stackingGroup });
1089
+ postMulEntries.push({ value: 1 + a.value, stackingGroup: a.stackingGroup });
863
1090
  }
864
- if (postPercentEntries.length > 0) {
865
- value *= combineMultiplicative(postPercentEntries);
1091
+ if (postMulEntries.length > 0) {
1092
+ value *= combineMultiplicative(postMulEntries);
866
1093
  }
867
1094
  const postAssign = byOp.get("PostAssign");
868
1095
  if (postAssign && postAssign.length > 0) {
@@ -895,6 +1122,13 @@ var VOLUME_ATTR_ID = 161;
895
1122
  var CAPACITY_ATTR_ID = 38;
896
1123
  var ItemState = class {
897
1124
  constructor(init) {
1125
+ /**
1126
+ * Attribute schema, needed to honour the SDE's attribute CAPS
1127
+ * (`maxAttributeID` / `minAttributeID`). Assigned by `computeFit` once for
1128
+ * every item in the fit; when absent, `getFinal` simply doesn't clamp — the
1129
+ * behaviour before caps existed.
1130
+ */
1131
+ this.attrSchema = null;
898
1132
  this.attrs = /* @__PURE__ */ new Map();
899
1133
  this.kind = init.kind;
900
1134
  this.id = init.id;
@@ -904,6 +1138,7 @@ var ItemState = class {
904
1138
  this.categoryID = init.type.categoryID;
905
1139
  this.state = init.state ?? "ONLINE";
906
1140
  this.charge = init.charge ?? null;
1141
+ this.activeSideEffects = new Set(init.activeSideEffects ?? []);
907
1142
  for (const a of init.type.attributes) {
908
1143
  const base = init.attributeOverrides?.[a.id] ?? a.v;
909
1144
  this.attrs.set(a.id, new ModifiedAttribute(a.id, base));
@@ -947,7 +1182,47 @@ var ItemState = class {
947
1182
  getFinal(id, fallback = 0) {
948
1183
  const ma = this.attrs.get(id);
949
1184
  if (!ma) return fallback;
950
- return ma.compute();
1185
+ return this.clampToCaps(id, ma.compute());
1186
+ }
1187
+ /**
1188
+ * Clamp a computed value to the ceiling / floor the SDE declares for that
1189
+ * attribute, mirroring pyfa's `__calculateValue` (which applies the same
1190
+ * bounds to forced values AND to normally-calculated ones).
1191
+ *
1192
+ * An attribute may name another attribute as its bound — resonances are
1193
+ * capped by `<layer>MaxDamageResonance` (1.0), `maxVelocity` by
1194
+ * `speedLimit`, the `*Bonus` attributes floored by `ConstantMinusNinetyNine`
1195
+ * — and the bound is read off THIS item, so a hull that raises its own
1196
+ * ceiling is honoured.
1197
+ *
1198
+ * Without it a Polarized weapon, which force-assigns every resonance to
1199
+ * `resistanceKiller` = 100, read as -9900% resistance instead of the 0% the
1200
+ * game shows: the cap is what turns "assign 100" into "resistances removed".
1201
+ * Measured on real published fits — five hulls, forty stats.
1202
+ */
1203
+ clampToCaps(id, value) {
1204
+ const schema = this.attrSchema?.get(id);
1205
+ if (!schema) return value;
1206
+ let out = value;
1207
+ if (schema.minAttributeID !== void 0) {
1208
+ const floor = this.capValue(schema.minAttributeID);
1209
+ if (floor !== void 0) out = Math.max(out, floor);
1210
+ }
1211
+ if (schema.maxAttributeID !== void 0) {
1212
+ const ceil = this.capValue(schema.maxAttributeID);
1213
+ if (ceil !== void 0) out = Math.min(out, ceil);
1214
+ }
1215
+ return out;
1216
+ }
1217
+ /** Value of a bounding attribute on this item, falling back to the SDE
1218
+ * default when the item doesn't carry it (pyfa's `getOriginal(key,
1219
+ * default)`). Bounds never declare bounds of their own, so this recurses
1220
+ * exactly one level. */
1221
+ capValue(capAttrID) {
1222
+ const ma = this.attrs.get(capAttrID);
1223
+ if (ma) return ma.compute();
1224
+ const def = this.attrSchema?.get(capAttrID)?.defaultValue;
1225
+ return def;
951
1226
  }
952
1227
  /** `defaultBase` is the seed base value when the target attribute isn't
953
1228
  * in the item's typeDogma — needed for SDE attrs whose `defaultValue`
@@ -1065,7 +1340,13 @@ function makeImplantState(fi, type) {
1065
1340
  return new ItemState({ kind: "implant", id: fi.id, type, state: "ONLINE" });
1066
1341
  }
1067
1342
  function makeBoosterState(fb, type) {
1068
- return new ItemState({ kind: "booster", id: fb.id, type, state: "ONLINE" });
1343
+ return new ItemState({
1344
+ kind: "booster",
1345
+ id: fb.id,
1346
+ type,
1347
+ state: "ONLINE",
1348
+ activeSideEffects: fb.activeSideEffects ?? []
1349
+ });
1069
1350
  }
1070
1351
  function makeSubsystemState(fs, type) {
1071
1352
  return new ItemState({ kind: "subsystem", id: fs.id, type, state: "ONLINE" });
@@ -1074,312 +1355,347 @@ function makeModeState(modeTypeID, type) {
1074
1355
  return new ItemState({ kind: "mode", id: `mode:${modeTypeID}`, type, state: "ACTIVE" });
1075
1356
  }
1076
1357
 
1077
- // src/stackingGroups.ts
1078
- var STACKING_PENALTY_GROUPS = /* @__PURE__ */ new Map([
1079
- [89, "default"],
1080
- [91, "default"],
1081
- [92, "default"],
1082
- [93, "default"],
1083
- [95, "default"],
1084
- [96, "default"],
1085
- [394, "default"],
1086
- [395, "default"],
1087
- [494, "default"],
1088
- [607, "postMul"],
1089
- [657, "default"],
1090
- [699, "default"],
1091
- [763, "default"],
1092
- [784, "default"],
1093
- [854, "cloakingScanResolutionMultiplier"],
1094
- [856, "default"],
1095
- [889, "default"],
1096
- [891, "default"],
1097
- [892, "default"],
1098
- [1024, "default"],
1099
- [1230, "default"],
1100
- [1281, "default"],
1101
- [1318, "default"],
1102
- [1445, "default"],
1103
- [1446, "default"],
1104
- [1448, "default"],
1105
- [1452, "default"],
1106
- [1472, "default"],
1107
- [1590, "default"],
1108
- [1617, "default"],
1109
- [1720, "default"],
1110
- [1764, "default"],
1111
- [1885, "postPerc"],
1112
- [1886, "postPerc"],
1113
- [2013, "default"],
1114
- [2014, "default"],
1115
- [2019, "default"],
1116
- [2020, "default"],
1117
- [2041, "default"],
1118
- [2052, "default"],
1119
- [2152, "default"],
1120
- [2232, "default"],
1121
- [2302, "preMul"],
1122
- [2644, "default"],
1123
- [2645, "default"],
1124
- [2646, "default"],
1125
- [2670, "default"],
1126
- [2693, "default"],
1127
- [2694, "default"],
1128
- [2695, "default"],
1129
- [2696, "default"],
1130
- [2697, "default"],
1131
- [2698, "default"],
1132
- [2716, "default"],
1133
- [2717, "default"],
1134
- [2792, "default"],
1135
- [2795, "default"],
1136
- [2796, "default"],
1137
- [2797, "default"],
1138
- [2798, "default"],
1139
- [2799, "default"],
1140
- [2801, "default"],
1141
- [2802, "default"],
1142
- [2803, "default"],
1143
- [2804, "default"],
1144
- [2851, "default"],
1145
- [2858, "default"],
1146
- [2865, "default"],
1147
- [2867, "default"],
1148
- [2868, "default"],
1149
- [3001, "postPerc"],
1150
- [3046, "default"],
1151
- [3174, "default"],
1152
- [3175, "default"],
1153
- [3182, "default"],
1154
- [3200, "default"],
1155
- [3201, "default"],
1156
- [3586, "default"],
1157
- [3655, "default"],
1158
- [3656, "default"],
1159
- [3657, "default"],
1160
- [3659, "default"],
1161
- [3674, "default"],
1162
- [3726, "default"],
1163
- [3727, "default"],
1164
- [3993, "postMul"],
1165
- [3995, "postMul"],
1166
- [3996, "default"],
1167
- [3997, "default"],
1168
- [3998, "default"],
1169
- [3999, "default"],
1170
- [4002, "postMul"],
1171
- [4003, "postMul"],
1172
- [4016, "default"],
1173
- [4017, "postMul"],
1174
- [4018, "postMul"],
1175
- [4019, "postMul"],
1176
- [4020, "postMul"],
1177
- [4021, "postMul"],
1178
- [4022, "postMul"],
1179
- [4023, "postMul"],
1180
- [4054, "default"],
1181
- [4055, "default"],
1182
- [4056, "default"],
1183
- [4057, "postMul"],
1184
- [4058, "postMul"],
1185
- [4059, "postMul"],
1186
- [4060, "postMul"],
1187
- [4061, "postMul"],
1188
- [4062, "postMul"],
1189
- [4063, "postMul"],
1190
- [4086, "postMul"],
1191
- [4088, "postMul"],
1192
- [4089, "postMul"],
1193
- [4135, "default"],
1194
- [4136, "default"],
1195
- [4137, "default"],
1196
- [4138, "default"],
1197
- [4162, "default"],
1198
- [4280, "default"],
1199
- [4358, "default"],
1200
- [4464, "default"],
1201
- [4489, "default"],
1202
- [4490, "default"],
1203
- [4491, "default"],
1204
- [4492, "default"],
1205
- [4527, "default"],
1206
- [4559, "default"],
1207
- [4575, "default"],
1208
- [4809, "default"],
1209
- [4810, "default"],
1210
- [4811, "default"],
1211
- [4812, "default"],
1212
- [4906, "postMul"],
1213
- [4928, "preMul"],
1214
- [4961, "postMul"],
1215
- [5081, "default"],
1216
- [5188, "default"],
1217
- [5189, "default"],
1218
- [5190, "default"],
1219
- [5213, "default"],
1220
- [5214, "default"],
1221
- [5230, "default"],
1222
- [5231, "default"],
1223
- [5397, "default"],
1224
- [5399, "default"],
1225
- [5440, "postMul"],
1226
- [5468, "default"],
1227
- [5560, "default"],
1228
- [5618, "postPerc"],
1229
- [5757, "default"],
1230
- [5867, "default"],
1231
- [5911, "default"],
1232
- [5912, "postMul"],
1233
- [5914, "postMul"],
1234
- [5915, "postMul"],
1235
- [5916, "postMul"],
1236
- [5917, "postMul"],
1237
- [5918, "postMul"],
1238
- [5919, "postMul"],
1239
- [5920, "postMul"],
1240
- [5921, "postMul"],
1241
- [5922, "postMul"],
1242
- [5923, "postMul"],
1243
- [5924, "postMul"],
1244
- [5925, "postMul"],
1245
- [5926, "postMul"],
1246
- [5927, "postMul"],
1247
- [5929, "postMul"],
1248
- [5951, "default"],
1249
- [5998, "default"],
1250
- [6010, "postDiv"],
1251
- [6011, "postDiv"],
1252
- [6012, "postDiv"],
1253
- [6014, "postDiv"],
1254
- [6015, "postDiv"],
1255
- [6016, "postDiv"],
1256
- [6017, "postDiv"],
1257
- [6039, "postDiv"],
1258
- [6040, "postDiv"],
1259
- [6041, "postDiv"],
1260
- [6063, "default"],
1261
- [6076, "postDiv"],
1262
- [6110, "default"],
1263
- [6111, "default"],
1264
- [6112, "default"],
1265
- [6113, "default"],
1266
- [6135, "default"],
1267
- [6152, "postDiv"],
1268
- [6154, "postDiv"],
1269
- [6164, "default"],
1270
- [6201, "default"],
1271
- [6208, "default"],
1272
- [6402, "default"],
1273
- [6403, "default"],
1274
- [6404, "default"],
1275
- [6405, "default"],
1276
- [6406, "default"],
1277
- [6409, "default"],
1278
- [6410, "default"],
1279
- [6411, "default"],
1280
- [6412, "default"],
1281
- [6422, "default"],
1282
- [6423, "default"],
1283
- [6424, "default"],
1284
- [6425, "default"],
1285
- [6426, "default"],
1286
- [6427, "default"],
1287
- [6428, "default"],
1288
- [6435, "default"],
1289
- [6439, "default"],
1290
- [6440, "default"],
1291
- [6441, "default"],
1292
- [6448, "default"],
1293
- [6449, "default"],
1294
- [6472, "default"],
1295
- [6473, "default"],
1296
- [6474, "default"],
1297
- [6476, "default"],
1298
- [6478, "default"],
1299
- [6479, "default"],
1300
- [6481, "default"],
1301
- [6484, "postMul"],
1302
- [6487, "default"],
1303
- [6555, "default"],
1304
- [6556, "default"],
1305
- [6557, "default"],
1306
- [6559, "default"],
1307
- [6566, "postMul"],
1308
- [6567, "default"],
1309
- [6581, "default"],
1310
- [6582, "postPercent"],
1311
- [6658, "preMul"],
1312
- [6670, "default"],
1313
- [6671, "default"],
1314
- [6682, "default"],
1315
- [6683, "default"],
1316
- [6684, "default"],
1317
- [6686, "default"],
1318
- [6690, "default"],
1319
- [6692, "default"],
1320
- [6693, "default"],
1321
- [6694, "default"],
1322
- [6727, "default"],
1323
- [6730, "postMul"],
1324
- [6731, "postMul"],
1325
- [6796, "postDiv"],
1326
- [6797, "postDiv"],
1327
- [6798, "postDiv"],
1328
- [6799, "postDiv"],
1329
- [6801, "postDiv"],
1330
- [6877, "default"],
1331
- [7029, "default"],
1332
- [7077, "default"],
1333
- [7078, "default"],
1334
- [7098, "default"],
1335
- [7111, "default"],
1336
- [7142, "default"],
1337
- [7202, "default"],
1338
- [7203, "default"],
1339
- [7223, "default"],
1340
- [7237, "default"],
1341
- [8033, "default"],
1342
- [8057, "default"],
1343
- [8076, "default"],
1344
- [8082, "default"],
1345
- [8108, "postMul"],
1346
- [8109, "postMul"],
1347
- [8111, "default"],
1348
- [8112, "default"],
1349
- [8113, "postMul"],
1350
- [8114, "postMul"],
1351
- [8119, "default"],
1352
- [11445, "default"],
1353
- [11691, "default"],
1354
- [11946, "default"],
1355
- [11947, "postMul"],
1356
- [11948, "postMul"],
1357
- [11953, "postMul"],
1358
- [12126, "default"],
1359
- [12597, "default"],
1360
- [12761, "default"],
1361
- [12794, "postDiv"],
1362
- [12795, "postDiv"],
1363
- [12796, "postDiv"],
1364
- [12798, "postDiv"],
1365
- [12799, "postDiv"],
1366
- [12838, "postMul"],
1367
- [12839, "postMul"]
1358
+ // src/stackingPenalised.ts
1359
+ var STACKING_PENALISED = /* @__PURE__ */ new Map([
1360
+ [89, ["51#default"]],
1361
+ [91, ["64#default"]],
1362
+ [92, ["64#default"]],
1363
+ [93, ["64#default"]],
1364
+ [95, ["51#default"]],
1365
+ [96, ["51#default"]],
1366
+ [394, ["37#default!booster+implant+skill"]],
1367
+ [395, ["70#default!booster+implant+skill"]],
1368
+ [607, ["37#postMul"]],
1369
+ [657, ["70#default"]],
1370
+ [699, ["564#default!booster+implant+skill"]],
1371
+ [763, ["*#default"]],
1372
+ [784, ["281#default!booster+implant+skill"]],
1373
+ [854, ["564#cloakingScanResolutionMultiplier"]],
1374
+ [856, ["600#default!booster+implant+skill"]],
1375
+ [889, ["51#default"]],
1376
+ [891, ["37#default!ship"]],
1377
+ [892, ["37#default!ship"]],
1378
+ [1024, ["37#default!ship"]],
1379
+ [1230, ["37#default!ship"]],
1380
+ [1281, ["84#default!implant"]],
1381
+ [1445, ["54#default!booster+skill"]],
1382
+ [1446, ["54#default!booster+skill"]],
1383
+ [1448, ["54#default!booster+skill"]],
1384
+ [1472, ["654#default!booster+implant+skill"]],
1385
+ [1590, ["653#default!booster+implant+skill"]],
1386
+ [1617, ["70#default"]],
1387
+ [1720, ["68#default"]],
1388
+ [1764, ["37#default!booster+implant+skill"]],
1389
+ [1885, ["51#default!ship"]],
1390
+ [1886, ["51#default!ship"]],
1391
+ [2013, ["37#default!booster+implant"]],
1392
+ [2014, ["54#default!skill"]],
1393
+ [2019, ["68#default!implant+skill"]],
1394
+ [2020, ["84#default!implant+skill"]],
1395
+ [2041, ["*#default"]],
1396
+ [2052, ["*#default"]],
1397
+ [2152, ["37#default"]],
1398
+ [2232, ["*#default"]],
1399
+ [2302, ["*#preMul"]],
1400
+ [2644, ["552#default"]],
1401
+ [2645, ["564#default"]],
1402
+ [2646, ["76#default"]],
1403
+ [2670, ["*#default", "564#default", "76#default"]],
1404
+ [2693, ["158#default!booster"]],
1405
+ [2694, ["158#default!booster"]],
1406
+ [2695, ["158#default!booster"]],
1407
+ [2696, ["54#default!booster"]],
1408
+ [2697, ["54#default!booster"]],
1409
+ [2698, ["54#default!booster"]],
1410
+ [2716, ["552#default"]],
1411
+ [2717, ["70#default"]],
1412
+ [2792, ["*#default"]],
1413
+ [2795, ["*#default"]],
1414
+ [2796, ["4#default"]],
1415
+ [2797, ["51#default"]],
1416
+ [2798, ["64#default!booster"]],
1417
+ [2799, ["51#default"]],
1418
+ [2801, ["51#default"]],
1419
+ [2802, ["64#default!booster"]],
1420
+ [2803, ["64#default!booster"]],
1421
+ [2804, ["51#default"]],
1422
+ [2851, ["*#default!booster"]],
1423
+ [2858, ["37#default"]],
1424
+ [2865, ["37#default"]],
1425
+ [2867, ["64#default"]],
1426
+ [2868, ["84#default"]],
1427
+ [3001, ["51#postPerc"]],
1428
+ [3046, ["37#default"]],
1429
+ [3174, ["54#default"]],
1430
+ [3175, ["20#default"]],
1431
+ [3182, ["*#default"]],
1432
+ [3200, ["84#default"]],
1433
+ [3201, ["68#default"]],
1434
+ [3586, ["566#default!skill"]],
1435
+ [3655, ["54#default"]],
1436
+ [3656, ["160#default"]],
1437
+ [3657, ["564#default"]],
1438
+ [3659, ["76#default"]],
1439
+ [3674, ["37#default"]],
1440
+ [3726, ["70#default"]],
1441
+ [3727, ["37#default"]],
1442
+ [3993, ["76#postMul"]],
1443
+ [3995, ["552#postMul"]],
1444
+ [3996, ["267#default"]],
1445
+ [3997, ["268#default"]],
1446
+ [3998, ["269#default"]],
1447
+ [3999, ["270#default"]],
1448
+ [4002, ["37#postMul"]],
1449
+ [4003, ["37#postMul"]],
1450
+ [4016, ["64#default"]],
1451
+ [4017, ["118#postMul"]],
1452
+ [4018, ["114#postMul"]],
1453
+ [4019, ["116#postMul"]],
1454
+ [4020, ["117#postMul"]],
1455
+ [4021, ["64#postMul"]],
1456
+ [4022, ["160#postMul"]],
1457
+ [4023, ["653#postMul"]],
1458
+ [4054, ["64#default"]],
1459
+ [4055, ["64#default"]],
1460
+ [4056, ["64#default"]],
1461
+ [4057, ["114#postMul"]],
1462
+ [4058, ["116#postMul"]],
1463
+ [4059, ["117#postMul"]],
1464
+ [4060, ["118#postMul"]],
1465
+ [4061, ["118#postMul"]],
1466
+ [4062, ["114#postMul"]],
1467
+ [4063, ["116#postMul"]],
1468
+ [4086, ["84#postMul"]],
1469
+ [4088, ["84#postMul"]],
1470
+ [4089, ["68#postMul"]],
1471
+ [4135, ["271#default"]],
1472
+ [4136, ["272#default"]],
1473
+ [4137, ["273#default"]],
1474
+ [4138, ["274#default"]],
1475
+ [4162, ["1371#default!booster+implant+skill"]],
1476
+ [4280, ["70#default"]],
1477
+ [4358, ["54#default"]],
1478
+ [4464, ["51#default"]],
1479
+ [4489, ["37#default"]],
1480
+ [4490, ["37#default"]],
1481
+ [4491, ["37#default"]],
1482
+ [4492, ["37#default"]],
1483
+ [4527, ["158#default"]],
1484
+ [4559, ["*#default"]],
1485
+ [4575, ["2044#default", "37#default", "54#default", "564#default", "64#default", "68#default"]],
1486
+ [4809, ["238#default"]],
1487
+ [4810, ["239#default"]],
1488
+ [4811, ["240#default"]],
1489
+ [4812, ["241#default"]],
1490
+ [4906, ["64#postMul"]],
1491
+ [4928, ["*#preMul"]],
1492
+ [4961, ["68#postMul"]],
1493
+ [5081, ["76#default"]],
1494
+ [5188, ["160#default!booster"]],
1495
+ [5189, ["160#default!booster"]],
1496
+ [5190, ["160#default!booster"]],
1497
+ [5213, ["37#default!ship"]],
1498
+ [5214, ["37#default!ship"]],
1499
+ [5230, ["*#default"]],
1500
+ [5231, ["*#default"]],
1501
+ [5397, ["1372#default"]],
1502
+ [5399, ["1371#default"]],
1503
+ [5440, ["117#postMul"]],
1504
+ [5468, ["70#default!ship"]],
1505
+ [5560, ["669#default!ship"]],
1506
+ [5618, ["51#default!ship"]],
1507
+ [5757, ["*#default", "566#default"]],
1508
+ [5867, ["281#default!ship"]],
1509
+ [5911, ["868#default"]],
1510
+ [5912, ["90#postMul"]],
1511
+ [5914, ["97#postMul"]],
1512
+ [5915, ["90#postMul"]],
1513
+ [5916, ["116#postMul"]],
1514
+ [5917, ["117#postMul"]],
1515
+ [5918, ["118#postMul"]],
1516
+ [5919, ["114#postMul"]],
1517
+ [5920, ["654#postMul"]],
1518
+ [5921, ["554#postMul"]],
1519
+ [5922, ["20#postMul"]],
1520
+ [5923, ["97#postMul"]],
1521
+ [5924, ["238#postMul"]],
1522
+ [5925, ["239#postMul"]],
1523
+ [5926, ["240#postMul"]],
1524
+ [5927, ["241#postMul"]],
1525
+ [5929, ["160#postMul"]],
1526
+ [5951, ["600#default"]],
1527
+ [5998, ["70#default"]],
1528
+ [6001, ["70#default"]],
1529
+ [6010, ["76#postDiv"]],
1530
+ [6011, ["54#postDiv"]],
1531
+ [6012, ["*#postDiv"]],
1532
+ [6014, ["552#postDiv"]],
1533
+ [6015, ["*#postDiv"]],
1534
+ [6016, ["70#postDiv"]],
1535
+ [6017, ["37#postDiv"]],
1536
+ [6039, ["160#postDiv"]],
1537
+ [6040, ["554#postDiv"]],
1538
+ [6041, ["*#postDiv"]],
1539
+ [6063, ["*#default"]],
1540
+ [6076, ["37#postDiv"]],
1541
+ [6110, ["37#default"]],
1542
+ [6111, ["281#default"]],
1543
+ [6112, ["654#default"]],
1544
+ [6113, ["653#default"]],
1545
+ [6135, ["*#default"]],
1546
+ [6152, ["54#postDiv"]],
1547
+ [6154, ["20#postDiv"]],
1548
+ [6164, ["37#default"]],
1549
+ [6201, ["37#default"]],
1550
+ [6208, ["552#default"]],
1551
+ [6402, ["653#default"]],
1552
+ [6403, ["37#default"]],
1553
+ [6404, ["2044#default", "54#default"]],
1554
+ [6405, ["6#default"]],
1555
+ [6406, ["158#default", "2044#default", "54#default"]],
1556
+ [6409, ["564#default"]],
1557
+ [6410, ["654#default"]],
1558
+ [6411, ["37#default"]],
1559
+ [6412, ["99#default"]],
1560
+ [6422, ["564#default", "76#default"]],
1561
+ [6423, ["*#default"]],
1562
+ [6424, ["*#default"]],
1563
+ [6425, ["552#default"]],
1564
+ [6426, ["37#default"]],
1565
+ [6427, ["*#default"]],
1566
+ [6428, ["*#default"]],
1567
+ [6435, ["37#default"]],
1568
+ [6439, ["271#default", "272#default", "273#default", "274#default", "37#default", "552#default"]],
1569
+ [6440, ["37#default"]],
1570
+ [6441, ["37#default", "552#default"]],
1571
+ [6448, ["*#default"]],
1572
+ [6449, ["*#default", "51#default"]],
1573
+ [6472, ["37#default"]],
1574
+ [6473, ["37#default"]],
1575
+ [6474, ["37#default"]],
1576
+ [6476, ["37#default"]],
1577
+ [6478, ["552#default"]],
1578
+ [6479, ["*#default"]],
1579
+ [6481, ["564#default", "76#default"]],
1580
+ [6484, ["*#postMul"]],
1581
+ [6487, ["2045#default"]],
1582
+ [6555, ["37#default"]],
1583
+ [6556, ["2130#default", "2178#default", "2226#default", "64#default"]],
1584
+ [6557, ["158#default", "160#default", "2125#default", "2126#default", "2149#default", "2175#default", "2176#default", "2180#default", "2234#default", "2235#default", "2236#default", "2237#default", "54#default"]],
1585
+ [6559, ["158#default", "160#default", "2125#default", "2126#default", "2149#default", "2175#default", "2176#default", "2180#default", "2234#default", "2235#default", "2236#default", "2237#default", "54#default"]],
1586
+ [6566, ["2177#postMul", "2182#postMul", "2233#postMul", "37#postMul"]],
1587
+ [6567, ["*#default", "564#default"]],
1588
+ [6581, ["*#default", "2044#default", "37#default", "4#default", "54#default", "564#default", "64#default"]],
1589
+ [6582, ["4#postMul", "68#default", "84#default", "charge:37#default", "module:51#postPercent"]],
1590
+ [6658, ["158#default", "51#postPerc", "54#default", "68#default", "84#default", "charge:37#default", "ship:*#default", "ship:*#preMul"]],
1591
+ [6670, ["2149#default", "2175#default", "2236#default", "54#default"]],
1592
+ [6671, ["37#default"]],
1593
+ [6682, ["37#default"]],
1594
+ [6683, ["552#default"]],
1595
+ [6684, ["564#default", "76#default"]],
1596
+ [6686, ["*#default"]],
1597
+ [6690, ["37#default"]],
1598
+ [6692, ["552#default"]],
1599
+ [6693, ["564#default", "76#default"]],
1600
+ [6694, ["*#default"]],
1601
+ [6727, ["2044#default"]],
1602
+ [6730, ["37#postMul", "552#default"]],
1603
+ [6731, ["37#postMul"]],
1604
+ [6796, ["64#postDiv"]],
1605
+ [6797, ["64#postDiv"]],
1606
+ [6798, ["64#postDiv"]],
1607
+ [6799, ["*#postDiv"]],
1608
+ [6801, ["20#postDiv"]],
1609
+ [6877, ["600#default"]],
1610
+ [7029, ["2751#default"]],
1611
+ [7077, ["64#default"]],
1612
+ [7078, ["51#default"]],
1613
+ [7098, ["564#default", "76#default"]],
1614
+ [7111, ["64#default"]],
1615
+ [7142, ["20#default", "37#default", "4#default"]],
1616
+ [7202, ["37#default"]],
1617
+ [7203, ["64#default"]],
1618
+ [7223, ["70#default"]],
1619
+ [7237, ["600#default"]],
1620
+ [8033, ["54#default"]],
1621
+ [8057, ["51#default", "64#default"]],
1622
+ [8076, ["110#default"]],
1623
+ [8082, ["564#default"]],
1624
+ [8108, ["552#postMul"]],
1625
+ [8109, ["552#postMul"]],
1626
+ [8111, ["113#default", "1371#default", "267#default", "271#default"]],
1627
+ [8112, ["109#default", "269#default", "273#default", "564#default", "600#default"]],
1628
+ [8113, ["111#default", "268#default", "272#default", "552#postMul", "68#postMul", "84#postMul"]],
1629
+ [8114, ["*#postMul", "110#default", "160#postMul", "270#default", "274#default", "654#postMul", "drone:64#postMul", "fighter:64#postMul", "module:64#default"]],
1630
+ [8119, ["37#default", "54#default", "64#default", "68#default"]],
1631
+ [11445, ["*#default"]],
1632
+ [11691, ["37#default"]],
1633
+ [11946, ["64#default"]],
1634
+ [11947, ["654#postMul"]],
1635
+ [11948, ["64#postMul"]],
1636
+ [11953, ["653#postMul"]],
1637
+ [12126, ["552#default"]],
1638
+ [12597, ["*#default", "564#default", "76#default"]],
1639
+ [12761, ["3154#default"]],
1640
+ [12794, ["37#postDiv"]],
1641
+ [12795, ["37#postDiv"]],
1642
+ [12796, ["281#postDiv"]],
1643
+ [12798, ["37#postDiv"]],
1644
+ [12799, ["51#postDiv"]],
1645
+ [12838, ["1371#postMul"]],
1646
+ [12839, ["1372#postMul"]]
1368
1647
  ]);
1648
+ var CONTEXT_KIND = {
1649
+ skill: ["skill", "character"],
1650
+ ship: ["ship"],
1651
+ implant: ["implant"],
1652
+ booster: ["booster"],
1653
+ module: ["module"],
1654
+ drone: ["drone"],
1655
+ fighter: ["fighter"],
1656
+ subsystem: ["subsystem"],
1657
+ mode: ["mode"]
1658
+ };
1659
+ function stackPenaltyGroup(effectID, attributeID, receiver, sourceKind) {
1660
+ const keys = STACKING_PENALISED.get(effectID);
1661
+ if (!keys) return null;
1662
+ const id = attributeID === void 0 ? null : String(attributeID);
1663
+ for (const key of keys) {
1664
+ const bang = key.indexOf("!");
1665
+ const exempt = bang < 0 ? [] : key.slice(bang + 1).split("+");
1666
+ const head = bang < 0 ? key : key.slice(0, bang);
1667
+ const hash = head.lastIndexOf("#");
1668
+ const group = head.slice(hash + 1);
1669
+ const target = head.slice(0, hash);
1670
+ const colon = target.indexOf(":");
1671
+ const recv = colon < 0 ? null : target.slice(0, colon);
1672
+ const attr = colon < 0 ? target : target.slice(colon + 1);
1673
+ if (recv !== null && recv !== receiver) continue;
1674
+ if (attr !== "*" && (id === null || attr !== id)) continue;
1675
+ if (exempt.some((c) => (CONTEXT_KIND[c] ?? []).includes(sourceKind))) return null;
1676
+ return group;
1677
+ }
1678
+ return null;
1679
+ }
1369
1680
 
1370
1681
  // src/modifierEngine.ts
1371
1682
  var NO_PENALTY_KINDS = /* @__PURE__ */ new Set([
1372
1683
  "skill",
1373
- "ship",
1374
- "mode",
1375
- "subsystem",
1376
- "character"
1684
+ "character",
1685
+ "implant",
1686
+ "booster"
1377
1687
  ]);
1378
1688
  function scaleForPipeline(rawValue, _unitID, op) {
1379
1689
  if (op === "PostPercent") return rawValue / 100;
1380
1690
  return rawValue;
1381
1691
  }
1382
- function applySourceItem(source, ctx, dataset) {
1692
+ function isPeerModifier(source, mi, ctx, effectID) {
1693
+ for (const t of ctx.targetsForModifier(mi, source, effectID)) {
1694
+ if (t.kind === "implant" || t.kind === "booster") return true;
1695
+ }
1696
+ return false;
1697
+ }
1698
+ function applySourceItem(source, ctx, dataset, pass = "all") {
1383
1699
  const isLocalModule = source.kind === "module";
1384
1700
  const selfMods = [];
1385
1701
  const outMods = [];
@@ -1387,10 +1703,15 @@ function applySourceItem(source, ctx, dataset) {
1387
1703
  if (LEGACY_HANDLED_EFFECT_IDS.has(eid)) continue;
1388
1704
  if (SEC_STATUS_SCALED_EFFECT_IDS.has(eid)) continue;
1389
1705
  if (isLocalModule && ctx.stoppedLocalEffectIDs.has(eid)) continue;
1706
+ if (source.kind === "booster" && BOOSTER_SIDE_EFFECT_IDS.has(eid) && !source.activeSideEffects.has(eid)) continue;
1390
1707
  const effect = dataset.effects.get(eid);
1391
1708
  if (!effect) continue;
1392
1709
  if (!source.appliesAtState(effect)) continue;
1393
1710
  for (const mi of effect.modifierInfo) {
1711
+ if (pass !== "all") {
1712
+ const peer = isPeerModifier(source, mi, ctx, effect.id);
1713
+ if (pass === "peer" ? !peer : peer) continue;
1714
+ }
1394
1715
  (mi.domain === "itemID" ? selfMods : outMods).push({ effect, mi });
1395
1716
  }
1396
1717
  }
@@ -1433,7 +1754,7 @@ function applyLegacyFighterProjection(fighter, ctx) {
1433
1754
  sourceID: fighter.id,
1434
1755
  operation: "PostPercent",
1435
1756
  value: pen / 100,
1436
- stackingGroup: `attr:${ATTR_MAX_VEL}`
1757
+ stackingGroup: `default:${ATTR_MAX_VEL}`
1437
1758
  });
1438
1759
  }
1439
1760
  }
@@ -1459,9 +1780,8 @@ function applyOneModifier(source, effect, mi, ctx, dataset) {
1459
1780
  if (mi.modifiedAttributeID === void 0) return;
1460
1781
  const computed = computeModifierValue(source, mi, ctx, dataset, op);
1461
1782
  if (computed === null) return;
1462
- const targets = ctx.targetsForModifier(mi, source);
1783
+ const targets = ctx.targetsForModifier(mi, source, effect.id);
1463
1784
  if (targets.length === 0) return;
1464
- const stackingGroup = computeStackingGroup(source, mi, dataset, effect);
1465
1785
  const isMul = op === "PreMul" || op === "PostMul" || op === "PreDiv" || op === "PostDiv";
1466
1786
  const value = isMul && computed.scaled ? 1 + computed.value : computed.value;
1467
1787
  const sdeDefault = isMul ? dataset.attributes.get(mi.modifiedAttributeID)?.defaultValue ?? 0 : 0;
@@ -1471,7 +1791,7 @@ function applyOneModifier(source, effect, mi, ctx, dataset) {
1471
1791
  sourceID: source.id,
1472
1792
  operation: op,
1473
1793
  value,
1474
- stackingGroup,
1794
+ stackingGroup: computeStackingGroup(source, mi, dataset, effect, target, ctx),
1475
1795
  resistanceAttributeID: effect.resistanceAttributeID
1476
1796
  };
1477
1797
  target.addAffliction(mi.modifiedAttributeID, affliction, sdeDefault);
@@ -1978,8 +2298,17 @@ var SHIP_BONUS_SCALING_SKILL = /* @__PURE__ */ new Map([
1978
2298
  // Notable: Exhumer/Barge shield+armor resist role bonuses (Hulk/Skiff/
1979
2299
  // Mackinaw shield resist 4 %→20 %), Bhaalgorn drone+laser (492), industrial
1980
2300
  // command (Orca/Rorqual), Marauder/pirate/expedition hull bonuses.
2301
+ // NB: attr 310 (`cpuNeedBonus`) is deliberately ABSENT. The auto-derivation
2302
+ // proposed it because the Weapon Upgrades SKILL scales its own attr 310 per
2303
+ // level (pyfa Effect3519 — a skill source, handled by the skill path), but
2304
+ // no SHIP-side effect does: pyfa's Effect5901 (`roleBonusBulkheadCPU`,
2305
+ // freighters + jump freighters) takes `ship.getModifiedItemAttr('cpuNeedBonus')`
2306
+ // verbatim, and every one of the 18 hulls carrying the attribute holds a
2307
+ // FULL-value role bonus (-99 % / -100 %), never a per-level one. Scaling it
2308
+ // by Electronics Upgrades V turned an Obelisk's -100 % bulkhead CPU into
2309
+ // -500 %, so three Reinforced Bulkheads reported -480 tf of CPU used
2310
+ // instead of 0. Caught on real published freighter fits.
1981
2311
  [66, 89611],
1982
- [310, 3432],
1983
2312
  [349, 19760],
1984
2313
  [492, 3339],
1985
2314
  [1296, 21610],
@@ -2147,6 +2476,9 @@ function computeModifierValue(source, mi, ctx, dataset, op) {
2147
2476
  if (source.kind === "ship") {
2148
2477
  return { value: baseValue, scaled: false };
2149
2478
  }
2479
+ if (source.kind === "implant" || source.kind === "booster") {
2480
+ return { value: baseValue, scaled: false };
2481
+ }
2150
2482
  if (itemRequiresSkill(source, skillID)) {
2151
2483
  return { value: baseValue, scaled: false };
2152
2484
  }
@@ -2154,19 +2486,42 @@ function computeModifierValue(source, mi, ctx, dataset, op) {
2154
2486
  }
2155
2487
  return { value: baseValue, scaled: false };
2156
2488
  }
2157
- function computeStackingGroup(source, mi, dataset, effect) {
2158
- if (NO_PENALTY_KINDS.has(source.kind)) return null;
2159
- if (mi.modifiedAttributeID !== void 0) {
2160
- const attr = dataset.attributes.get(mi.modifiedAttributeID);
2161
- if (attr?.stackable) return null;
2162
- }
2163
- const group = STACKING_PENALTY_GROUPS.get(effect.id);
2164
- if (group !== void 0 && group !== "default" && CUSTOM_STACK_GROUPS_HONOURED.has(group)) {
2165
- return `${group}:${mi.modifiedAttributeID}`;
2489
+ function penaltyReceiver(target, source, ctx) {
2490
+ if (target === ctx.ship) return "ship";
2491
+ if (target === source) return "self";
2492
+ switch (target.kind) {
2493
+ case "charge":
2494
+ return "charge";
2495
+ case "drone":
2496
+ return "drone";
2497
+ case "fighter":
2498
+ return "fighter";
2499
+ case "character":
2500
+ return "character";
2501
+ case "module":
2502
+ case "subsystem":
2503
+ return "module";
2504
+ default:
2505
+ return "self";
2166
2506
  }
2167
- return `attr:${mi.modifiedAttributeID}`;
2168
2507
  }
2169
- var CUSTOM_STACK_GROUPS_HONOURED = /* @__PURE__ */ new Set([
2508
+ function computeStackingGroup(source, mi, dataset, effect, target, ctx) {
2509
+ if (NO_PENALTY_KINDS.has(source.kind)) return null;
2510
+ const group = stackPenaltyGroup(
2511
+ effect.id,
2512
+ mi.modifiedAttributeID,
2513
+ penaltyReceiver(target, source, ctx),
2514
+ source.kind
2515
+ );
2516
+ if (group === null) return null;
2517
+ if (!HONOURED_PENALTY_GROUPS.has(group)) return `default:${mi.modifiedAttributeID}`;
2518
+ return `${group}:${mi.modifiedAttributeID}`;
2519
+ }
2520
+ var HONOURED_PENALTY_GROUPS = /* @__PURE__ */ new Set([
2521
+ "postMul",
2522
+ "postDiv",
2523
+ "postPerc",
2524
+ "postPercent",
2170
2525
  "cloakingScanResolutionMultiplier"
2171
2526
  ]);
2172
2527
  function mapSourceKind(kind) {
@@ -2230,7 +2585,7 @@ function applySkills(ctx, dataset) {
2230
2585
  const sourceAttrMeta = dataset.attributes.get(mi.modifyingAttributeID);
2231
2586
  const baseVal = scaleForPipeline(rawBase, sourceAttrMeta?.unitID, op);
2232
2587
  const value = isMul ? 1 + baseVal * level : baseVal * level;
2233
- const stackingGroup = NO_PENALTY_KINDS.has("character") ? null : `attr:${mi.modifiedAttributeID}`;
2588
+ const stackingGroup = NO_PENALTY_KINDS.has("character") ? null : `default:${mi.modifiedAttributeID}`;
2234
2589
  const targets = resolveSkillTargets(mi, ctx);
2235
2590
  if (targets.length === 0) continue;
2236
2591
  const affliction = {
@@ -2442,7 +2797,7 @@ function applyLegacyPropMods(ctx) {
2442
2797
  sourceID: mod.id,
2443
2798
  operation: "PostPercent",
2444
2799
  value: sigBonus / 100,
2445
- stackingGroup: `attr:${ATTR_SIGNATURE_RADIUS}`
2800
+ stackingGroup: `default:${ATTR_SIGNATURE_RADIUS}`
2446
2801
  });
2447
2802
  }
2448
2803
  }
@@ -2450,13 +2805,13 @@ function applyLegacyPropMods(ctx) {
2450
2805
  var ENTOSIS_LINK_EFFECT_ID = 6063;
2451
2806
  function applyLegacyEntosisLink(ctx) {
2452
2807
  for (const mod of ctx.modules) {
2453
- if (mod.state === "OFFLINE") continue;
2808
+ if (mod.state !== "ACTIVE" && mod.state !== "OVERLOAD") continue;
2454
2809
  if (!mod.effectIDs.has(ENTOSIS_LINK_EFFECT_ID)) continue;
2455
2810
  const sensorPairs = [
2456
- [1027, ATTR_SCAN_RADAR],
2811
+ [1027, ATTR_SCAN_GRAVIMETRIC],
2457
2812
  [1028, ATTR_SCAN_LADAR],
2458
2813
  [1029, ATTR_SCAN_MAGNETOMETRIC],
2459
- [1030, ATTR_SCAN_GRAVIMETRIC]
2814
+ [1030, ATTR_SCAN_RADAR]
2460
2815
  ];
2461
2816
  for (const [srcAttr, tgtAttr] of sensorPairs) {
2462
2817
  const bonusPct = mod.getBase(srcAttr) ?? 0;
@@ -2490,7 +2845,7 @@ function applyLegacyCapitalEhe(ctx) {
2490
2845
  sourceID: mod.id,
2491
2846
  operation: "PostMul",
2492
2847
  value: factor,
2493
- stackingGroup: `attr:${tgtAttr}`
2848
+ stackingGroup: `default:${tgtAttr}`
2494
2849
  });
2495
2850
  }
2496
2851
  }
@@ -2569,13 +2924,13 @@ function applyLegacyMjdSigBloom(ctx) {
2569
2924
  if (!isMjd) continue;
2570
2925
  const bonusPct = mod.getFinal(ATTR_SIGNATURE_RADIUS_BONUS_PERCENT, 0);
2571
2926
  if (bonusPct === 0) continue;
2927
+ const penalised = [...LEGACY_MJD_EFFECT_IDS].some((eid) => mod.effectIDs.has(eid) && stackPenaltyGroup(eid, ATTR_SIGNATURE_RADIUS, "ship", "module") !== null);
2572
2928
  ctx.ship.addAffliction(ATTR_SIGNATURE_RADIUS, {
2573
2929
  sourceKind: "module",
2574
2930
  sourceID: mod.id,
2575
2931
  operation: "PostPercent",
2576
2932
  value: bonusPct / 100,
2577
- stackingGroup: `attr:${ATTR_SIGNATURE_RADIUS}`
2578
- // stack-penalised with MWD bloom
2933
+ stackingGroup: penalised ? `default:${ATTR_SIGNATURE_RADIUS}` : null
2579
2934
  });
2580
2935
  }
2581
2936
  }
@@ -2978,7 +3333,7 @@ function applyLegacyBastion(ctx) {
2978
3333
  sourceID,
2979
3334
  operation: "PreMul",
2980
3335
  value: factor,
2981
- stackingGroup: HULL_RES_ATTRS.has(tgt) ? null : `attr:${tgt}`
3336
+ stackingGroup: HULL_RES_ATTRS.has(tgt) ? null : `default:${tgt}`
2982
3337
  });
2983
3338
  }
2984
3339
  const maxRangeBonus = mod.getFinal(ATTR_MAX_RANGE_BONUS, 0);
@@ -2993,14 +3348,14 @@ function applyLegacyBastion(ctx) {
2993
3348
  sourceID,
2994
3349
  operation: "PostPercent",
2995
3350
  value: maxRangeBonus / 100,
2996
- stackingGroup: `attr:${ATTR_MAX_RANGE}`
3351
+ stackingGroup: `default:${ATTR_MAX_RANGE}`
2997
3352
  });
2998
3353
  if (turretRofBonus !== 0) target.addAffliction(ATTR_SPEED, {
2999
3354
  sourceKind: "module",
3000
3355
  sourceID,
3001
3356
  operation: "PostPercent",
3002
3357
  value: turretRofBonus / 100,
3003
- stackingGroup: `attr:${ATTR_SPEED}`
3358
+ stackingGroup: `default:${ATTR_SPEED}`
3004
3359
  });
3005
3360
  }
3006
3361
  if (matchTurretFalloff(target) && falloffBonusV !== 0) {
@@ -3009,7 +3364,7 @@ function applyLegacyBastion(ctx) {
3009
3364
  sourceID,
3010
3365
  operation: "PostPercent",
3011
3366
  value: falloffBonusV / 100,
3012
- stackingGroup: `attr:${ATTR_FALLOFF}`
3367
+ stackingGroup: `default:${ATTR_FALLOFF}`
3013
3368
  });
3014
3369
  }
3015
3370
  }
@@ -3024,7 +3379,7 @@ function applyLegacyBastion(ctx) {
3024
3379
  sourceID,
3025
3380
  operation: "PostPercent",
3026
3381
  value: missileVelocityBonus / 100,
3027
- stackingGroup: `attr:${ATTR_MAX_VELOCITY}`
3382
+ stackingGroup: `default:${ATTR_MAX_VELOCITY}`
3028
3383
  });
3029
3384
  }
3030
3385
  if (matchMissileLauncherROF(target) && missileRofBonus !== 0) {
@@ -3033,7 +3388,7 @@ function applyLegacyBastion(ctx) {
3033
3388
  sourceID,
3034
3389
  operation: "PostPercent",
3035
3390
  value: missileRofBonus / 100,
3036
- stackingGroup: `attr:${ATTR_SPEED}`
3391
+ stackingGroup: `default:${ATTR_SPEED}`
3037
3392
  });
3038
3393
  }
3039
3394
  }
@@ -3049,7 +3404,7 @@ function applyLegacyBastion(ctx) {
3049
3404
  sourceID,
3050
3405
  operation: "PostPercent",
3051
3406
  value: armorRepBonus / 100,
3052
- stackingGroup: `attr:${ATTR_ARMOR_REP_AMOUNT}`
3407
+ stackingGroup: `default:${ATTR_ARMOR_REP_AMOUNT}`
3053
3408
  });
3054
3409
  }
3055
3410
  if (isShield && shieldBoostBonus !== 0) {
@@ -3058,7 +3413,7 @@ function applyLegacyBastion(ctx) {
3058
3413
  sourceID,
3059
3414
  operation: "PostPercent",
3060
3415
  value: shieldBoostBonus / 100,
3061
- stackingGroup: `attr:${ATTR_SHIELD_REP_AMOUNT}`
3416
+ stackingGroup: `default:${ATTR_SHIELD_REP_AMOUNT}`
3062
3417
  });
3063
3418
  }
3064
3419
  if ((isArmor || isShield) && repairDurationBonus !== 0) {
@@ -3102,7 +3457,7 @@ function applyLegacyBastion(ctx) {
3102
3457
  sourceID,
3103
3458
  operation: "PostPercent",
3104
3459
  value: v / 100,
3105
- stackingGroup: `attr:${tgt}`
3460
+ stackingGroup: `default:${tgt}`
3106
3461
  });
3107
3462
  }
3108
3463
  const resistPairs = [
@@ -3201,7 +3556,7 @@ function applyLegacyDoomsdaySelfEffects(ctx) {
3201
3556
  sourceID: mod.id,
3202
3557
  operation: "PostPercent",
3203
3558
  value: speedFactor / 100,
3204
- stackingGroup: `attr:${ATTR_MAX_VELOCITY}`
3559
+ stackingGroup: `default:${ATTR_MAX_VELOCITY}`
3205
3560
  });
3206
3561
  }
3207
3562
  const siegeStatus = mod.getFinal(ATTR_SIEGE_WARP_STATUS, 0);
@@ -3272,7 +3627,7 @@ function applyCommandBuff(buffID, value, sourceMod, ctx) {
3272
3627
  sourceID: sourceMod.id,
3273
3628
  operation,
3274
3629
  value: scaledValue,
3275
- stackingGroup: `attr:${attrID}`
3630
+ stackingGroup: `default:${attrID}`
3276
3631
  });
3277
3632
  for (const mi of dbuff.itemModifiers) {
3278
3633
  ctx.ship.addAffliction(mi.dogmaAttributeID, affliction(op, mi.dogmaAttributeID));
@@ -3618,7 +3973,7 @@ function applyLegacyFighterAbilities(ctx) {
3618
3973
  sourceID: fighter.id,
3619
3974
  operation: "PostPercent",
3620
3975
  value: speedPct / 100,
3621
- stackingGroup: `attr:${ATTR_MAX_VELOCITY}`
3976
+ stackingGroup: `default:${ATTR_MAX_VELOCITY}`
3622
3977
  });
3623
3978
  }
3624
3979
  }
@@ -3630,7 +3985,7 @@ function applyLegacyFighterAbilities(ctx) {
3630
3985
  sourceID: fighter.id,
3631
3986
  operation: "PostPercent",
3632
3987
  value: speedPct / 100,
3633
- stackingGroup: `attr:${ATTR_MAX_VELOCITY}`
3988
+ stackingGroup: `default:${ATTR_MAX_VELOCITY}`
3634
3989
  });
3635
3990
  }
3636
3991
  const sigPct = fighter.getFinal(ATTR_FIGHTER_MWD_SIG_BONUS, 0);
@@ -3640,7 +3995,7 @@ function applyLegacyFighterAbilities(ctx) {
3640
3995
  sourceID: fighter.id,
3641
3996
  operation: "PostPercent",
3642
3997
  value: sigPct / 100,
3643
- stackingGroup: `attr:${ATTR_SIGNATURE_RADIUS}`
3998
+ stackingGroup: `default:${ATTR_SIGNATURE_RADIUS}`
3644
3999
  });
3645
4000
  }
3646
4001
  }
@@ -3652,7 +4007,7 @@ function applyLegacyFighterAbilities(ctx) {
3652
4007
  sourceID: fighter.id,
3653
4008
  operation: "PostPercent",
3654
4009
  value: speedPct / 100,
3655
- stackingGroup: `attr:${ATTR_MAX_VELOCITY}`
4010
+ stackingGroup: `default:${ATTR_MAX_VELOCITY}`
3656
4011
  });
3657
4012
  }
3658
4013
  const sigPct = fighter.getFinal(ATTR_FIGHTER_EVASIVE_SIG_BONUS, 0);
@@ -3662,7 +4017,7 @@ function applyLegacyFighterAbilities(ctx) {
3662
4017
  sourceID: fighter.id,
3663
4018
  operation: "PostPercent",
3664
4019
  value: sigPct / 100,
3665
- stackingGroup: `attr:${ATTR_SIGNATURE_RADIUS}`
4020
+ stackingGroup: `default:${ATTR_SIGNATURE_RADIUS}`
3666
4021
  });
3667
4022
  }
3668
4023
  const resonances = [
@@ -3679,7 +4034,7 @@ function applyLegacyFighterAbilities(ctx) {
3679
4034
  sourceID: fighter.id,
3680
4035
  operation: "PostMul",
3681
4036
  value: mul,
3682
- stackingGroup: `attr:${tgtAttr}`
4037
+ stackingGroup: `default:${tgtAttr}`
3683
4038
  });
3684
4039
  }
3685
4040
  }
@@ -3799,7 +4154,7 @@ function applyLegacyRAH(ctx, damageProfile) {
3799
4154
  sourceID: mod.id,
3800
4155
  operation: "PreMul",
3801
4156
  value: factor,
3802
- stackingGroup: `attr:${tgtAttr}`
4157
+ stackingGroup: `default:${tgtAttr}`
3803
4158
  });
3804
4159
  }
3805
4160
  }
@@ -3963,176 +4318,6 @@ function computeTotalEhp(shield, armor, hull, useProfile) {
3963
4318
  return Number.isFinite(total) ? total : Number.MAX_SAFE_INTEGER;
3964
4319
  }
3965
4320
 
3966
- // src/fitChecks.ts
3967
- function readAttr(t, id) {
3968
- return t.attributes.find((a) => a.id === id)?.v;
3969
- }
3970
- function typeFitsSlotType(t, slot) {
3971
- for (const e of t.effects) {
3972
- const mapped = SLOT_EFFECT_TO_SLOT_TYPE[e.id];
3973
- if (mapped === slot) return true;
3974
- }
3975
- return false;
3976
- }
3977
- function isTurretWeapon(t) {
3978
- return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "TURRET");
3979
- }
3980
- function isMissileLauncher(t) {
3981
- return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "MISSILE");
3982
- }
3983
- function isSmartBomb(t) {
3984
- return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "SMARTBOMB");
3985
- }
3986
- function shipGroupRestrictions(t) {
3987
- const out = [];
3988
- for (const id of CAN_FIT_SHIP_GROUP_ATTRS) {
3989
- const v = readAttr(t, id);
3990
- if (v != null && v > 0) out.push(Math.round(v));
3991
- }
3992
- return out;
3993
- }
3994
- function shipTypeRestrictions(t) {
3995
- const out = [];
3996
- for (const id of CAN_FIT_SHIP_TYPE_ATTRS) {
3997
- const v = readAttr(t, id);
3998
- if (v != null && v > 0) out.push(Math.round(v));
3999
- }
4000
- return out;
4001
- }
4002
- function maxGroupFittedFor(mod) {
4003
- const v = readAttr(mod, ATTR.MAX_GROUP_FITTED);
4004
- return v != null && v > 0 ? Math.round(v) : void 0;
4005
- }
4006
- function maxTypeFittedFor(mod) {
4007
- const v = readAttr(mod, ATTR.MAX_TYPE_FITTED);
4008
- return v != null && v > 0 ? Math.round(v) : void 0;
4009
- }
4010
- function freeFitGroupSlotsFor(mod, fittedModules, dataset) {
4011
- const groupCap = maxGroupFittedFor(mod);
4012
- const typeCap = maxTypeFittedFor(mod);
4013
- if (groupCap === void 0 && typeCap === void 0) return Number.POSITIVE_INFINITY;
4014
- let groupCount = 0;
4015
- let typeCount = 0;
4016
- for (const fm of fittedModules) {
4017
- if (typeCap !== void 0 && fm.typeID === mod.id) typeCount++;
4018
- if (groupCap !== void 0) {
4019
- const t = dataset.getType(fm.typeID);
4020
- if (t && t.groupID === mod.groupID) groupCount++;
4021
- }
4022
- }
4023
- let free = Number.POSITIVE_INFINITY;
4024
- if (groupCap !== void 0) free = Math.min(free, groupCap - groupCount);
4025
- if (typeCap !== void 0) free = Math.min(free, typeCap - typeCount);
4026
- return Math.max(0, free);
4027
- }
4028
- function canFitModuleOnShip(mod, ship, slot, fitContext) {
4029
- if (!typeFitsSlotType(mod, slot)) {
4030
- return { ok: false, reason: `Module does not declare a ${slot} slot effect` };
4031
- }
4032
- if (!ship) return { ok: true };
4033
- const allowedGroups = shipGroupRestrictions(mod);
4034
- const allowedTypes = shipTypeRestrictions(mod);
4035
- if (allowedGroups.length > 0 || allowedTypes.length > 0) {
4036
- const groupOk = allowedGroups.includes(ship.groupID);
4037
- const typeOk = allowedTypes.includes(ship.id);
4038
- if (!groupOk && !typeOk) {
4039
- return { ok: false, reason: "Ship hull/group not allowed by this module" };
4040
- }
4041
- }
4042
- if (slot === "RIG") {
4043
- const modSize = readAttr(mod, ATTR.RIG_SIZE);
4044
- const shipSize = readAttr(ship, ATTR.RIG_SIZE);
4045
- if (modSize != null && shipSize != null && Math.round(modSize) !== Math.round(shipSize)) {
4046
- return { ok: false, reason: "Rig size mismatch" };
4047
- }
4048
- }
4049
- if (slot === "SUBSYSTEM") {
4050
- const fitsTo = readAttr(mod, 1380);
4051
- if (fitsTo != null && fitsTo > 0 && Math.round(fitsTo) !== ship.id) {
4052
- return { ok: false, reason: "Subsystem locked to a different T3C hull" };
4053
- }
4054
- }
4055
- if (slot === "HI") {
4056
- if (isTurretWeapon(mod)) {
4057
- const turrets = readAttr(ship, ATTR.TURRET_HARDPOINTS) ?? 0;
4058
- if (turrets <= 0) {
4059
- return { ok: false, reason: "Ship has no turret hardpoints" };
4060
- }
4061
- } else if (isMissileLauncher(mod)) {
4062
- const launchers = readAttr(ship, ATTR.LAUNCHER_HARDPOINTS) ?? 0;
4063
- if (launchers <= 0) {
4064
- return { ok: false, reason: "Ship has no launcher hardpoints" };
4065
- }
4066
- }
4067
- }
4068
- if (fitContext) {
4069
- const free = freeFitGroupSlotsFor(mod, fitContext.fittedModules, fitContext.dataset);
4070
- if (free <= 0) {
4071
- const cap = maxTypeFittedFor(mod) ?? maxGroupFittedFor(mod);
4072
- return { ok: false, reason: `Only ${cap} of this module type can be fitted to a ship` };
4073
- }
4074
- }
4075
- return { ok: true };
4076
- }
4077
- function chargeGroupsForModule(mod) {
4078
- const out = [];
4079
- for (const attrID of CHARGE_GROUP_ATTRS) {
4080
- const v = readAttr(mod, attrID);
4081
- if (v != null && v > 0) out.push(Math.round(v));
4082
- }
4083
- return out;
4084
- }
4085
- function moduleAcceptsAnyCharge(mod) {
4086
- return chargeGroupsForModule(mod).length > 0;
4087
- }
4088
- function moduleAcceptsChargeType(mod, charge) {
4089
- const allowedGroups = chargeGroupsForModule(mod);
4090
- if (allowedGroups.length === 0) return false;
4091
- if (!allowedGroups.includes(charge.groupID)) return false;
4092
- const modSize = readAttr(mod, ATTR.CHARGE_SIZE);
4093
- const chargeSize = readAttr(charge, ATTR.CHARGE_SIZE);
4094
- if (modSize != null && chargeSize != null && Math.round(chargeSize) > Math.round(modSize)) {
4095
- return false;
4096
- }
4097
- return true;
4098
- }
4099
- var ACTIVE_EFFECT_CATEGORIES = /* @__PURE__ */ new Set([1, 2, 3]);
4100
- function isActivatableModule(mod, effects) {
4101
- for (const e of mod.effects) {
4102
- const eff = effects.get(e.id);
4103
- if (eff && eff.effectCategoryID !== void 0 && ACTIVE_EFFECT_CATEGORIES.has(eff.effectCategoryID)) {
4104
- return true;
4105
- }
4106
- }
4107
- return false;
4108
- }
4109
- var DEFAULT_OFFLINE_ACTIVATION_GROUPS = /* @__PURE__ */ new Set([
4110
- 330,
4111
- 4117
4112
- ]);
4113
- function defaultStateForModule(mod, effects) {
4114
- if (DEFAULT_OFFLINE_ACTIVATION_GROUPS.has(mod.groupID)) return "ONLINE";
4115
- return isActivatableModule(mod, effects) ? "ACTIVE" : "ONLINE";
4116
- }
4117
- function freeHardpointsFor(mod, ship, fittedHiModules, dataset) {
4118
- const turretCap = readAttr(ship, ATTR.TURRET_HARDPOINTS) ?? 0;
4119
- const launcherCap = readAttr(ship, ATTR.LAUNCHER_HARDPOINTS) ?? 0;
4120
- let turretUsed = 0;
4121
- let launcherUsed = 0;
4122
- for (const m of fittedHiModules) {
4123
- const t = dataset.getType(m.typeID);
4124
- if (!t) continue;
4125
- if (isTurretWeapon(t)) turretUsed++;
4126
- else if (isMissileLauncher(t)) launcherUsed++;
4127
- }
4128
- let hardpointFree;
4129
- if (isTurretWeapon(mod)) hardpointFree = Math.max(0, turretCap - turretUsed);
4130
- else if (isMissileLauncher(mod)) hardpointFree = Math.max(0, launcherCap - launcherUsed);
4131
- else hardpointFree = Number.POSITIVE_INFINITY;
4132
- const groupFree = freeFitGroupSlotsFor(mod, fittedHiModules, dataset);
4133
- return Math.min(hardpointFree, groupFree);
4134
- }
4135
-
4136
4321
  // src/derived/capacitor.ts
4137
4322
  function computeCapacitor(ctx, dataset) {
4138
4323
  const ship = ctx.ship;
@@ -5169,22 +5354,46 @@ function computeFit(fit, dataset, opts) {
5169
5354
  const t = dataset.getType(ff.typeID);
5170
5355
  return t ? [makeFighterState(ff, t)] : [];
5171
5356
  });
5172
- const implants = fit.implants.flatMap((fi) => {
5173
- const t = dataset.getType(fi.typeID);
5174
- return t ? [makeImplantState(fi, t)] : [];
5175
- });
5176
- const boosters = fit.boosters.flatMap((fb) => {
5177
- const t = dataset.getType(fb.typeID);
5178
- return t ? [makeBoosterState(fb, t)] : [];
5179
- });
5357
+ const implants = dropSlotCollisions(
5358
+ fit.implants.flatMap((fi) => {
5359
+ const t = dataset.getType(fi.typeID);
5360
+ return t ? [{ slot: t.attributes.find((a) => a.id === ATTR_IMPLANTNESS)?.v ?? fi.slot, item: makeImplantState(fi, t) }] : [];
5361
+ })
5362
+ );
5363
+ const boosters = dropSlotCollisions(
5364
+ fit.boosters.flatMap((fb) => {
5365
+ const t = dataset.getType(fb.typeID);
5366
+ return t ? [{ slot: t.attributes.find((a) => a.id === ATTR_BOOSTERNESS)?.v ?? fb.slot, item: makeBoosterState(fb, t) }] : [];
5367
+ })
5368
+ );
5180
5369
  const subsystems = fit.subsystems.flatMap((fs) => {
5181
5370
  const t = dataset.getType(fs.typeID);
5182
5371
  return t ? [makeSubsystemState(fs, t)] : [];
5183
5372
  });
5184
5373
  let mode;
5185
- if (fit.modeTypeID !== void 0) {
5186
- const t = dataset.getType(fit.modeTypeID);
5187
- if (t) mode = makeModeState(fit.modeTypeID, t);
5374
+ const modeTypeID = fit.modeTypeID ?? defaultModeTypeIDFor(shipType, dataset);
5375
+ if (modeTypeID !== void 0) {
5376
+ const t = dataset.getType(modeTypeID);
5377
+ if (t) mode = makeModeState(modeTypeID, t);
5378
+ }
5379
+ ship.attr(
5380
+ ATTR.DRONE_CONTROL_RANGE,
5381
+ dataset.attributes.get(ATTR.DRONE_CONTROL_RANGE)?.defaultValue ?? 2e4
5382
+ );
5383
+ for (const it of [
5384
+ ship,
5385
+ character,
5386
+ mode,
5387
+ ...modules,
5388
+ ...drones,
5389
+ ...fighters,
5390
+ ...implants,
5391
+ ...boosters,
5392
+ ...subsystems
5393
+ ]) {
5394
+ if (!it) continue;
5395
+ it.attrSchema = dataset.attributes;
5396
+ if (it.charge) it.charge.attrSchema = dataset.attributes;
5188
5397
  }
5189
5398
  const skillLevels = /* @__PURE__ */ new Map();
5190
5399
  for (const [k, v] of Object.entries(opts.skillProfile.skills)) {
@@ -5230,8 +5439,9 @@ function computeFit(fit, dataset, opts) {
5230
5439
  for (const m of modules) applySourceItem(m, ctx, dataset);
5231
5440
  for (const d of drones) applySourceItem(d, ctx, dataset);
5232
5441
  for (const f of fighters) applySourceItem(f, ctx, dataset);
5233
- for (const i of implants) applySourceItem(i, ctx, dataset);
5234
- for (const b of boosters) applySourceItem(b, ctx, dataset);
5442
+ const charSide = [...implants, ...boosters];
5443
+ for (const it of charSide) applySourceItem(it, ctx, dataset, "peer");
5444
+ for (const it of charSide) applySourceItem(it, ctx, dataset, "outgoing");
5235
5445
  if (mode) applySourceItem(mode, ctx, dataset);
5236
5446
  applyLegacyOverload(ctx);
5237
5447
  applyLegacyPropMods(ctx);
@@ -5438,6 +5648,7 @@ function deriveStats(ctx, dataset, damageProfile, fit, projectionReports) {
5438
5648
  const cap = computeCapacitor(ctx, dataset);
5439
5649
  const tank = computeTank(ctx);
5440
5650
  const offense = computeOffense(ctx, dataset, fit);
5651
+ const sensor = pickSensorStrength(ship);
5441
5652
  return {
5442
5653
  fitting: {
5443
5654
  cpuUsed,
@@ -5526,8 +5737,8 @@ function deriveStats(ctx, dataset, damageProfile, fit, projectionReports) {
5526
5737
  )),
5527
5738
  signatureRadius: ship.getFinal(ATTR.SIGNATURE_RADIUS, 0),
5528
5739
  scanResolution: ship.getFinal(ATTR.SCAN_RESOLUTION, 0),
5529
- sensorStrength: pickSensorStrength(ship).value,
5530
- sensorType: pickSensorStrength(ship).type
5740
+ sensorStrength: sensor.value,
5741
+ sensorType: sensor.type
5531
5742
  },
5532
5743
  drones: {
5533
5744
  bayUsed: droneBayUsed,
@@ -5538,13 +5749,12 @@ function deriveStats(ctx, dataset, damageProfile, fit, projectionReports) {
5538
5749
  // ItemState per FitDrone stack, so filtering by state counted five
5539
5750
  // active Ogre IIs as 1 — and this is the number bandwidth gates.
5540
5751
  active: fit.drones.reduce((n, d) => n + d.countActive, 0),
5541
- // Drone control range is the ship base (default 20 km, attr 458)
5542
- // PLUS the character's accumulated skill bonus on the same attr.
5543
- // Drone Avionics V + Advanced Drone Avionics V each ModAdd to
5544
- // char.attr_458 via effect 504 (`domain: charID`). Pyfa reads
5545
- // both and sums; ship.attr_458 alone misses 40 km of skills at
5546
- // All-V → produces 20 km instead of the in-game 60 km.
5547
- controlRange: ship.getFinal(ATTR.DRONE_CONTROL_RANGE, 2e4) + ctx.character.getFinal(ATTR.DRONE_CONTROL_RANGE, 0)
5752
+ // ONE accumulator, on the ship (attr 458, SDE default 20 km) — the
5753
+ // charID-domain modifiers that carry the skills and the hull bonus
5754
+ // are re-routed there, see FitContext.targetsForModifier. Summing a
5755
+ // ship half and a character half instead let a percentage bonus
5756
+ // apply to only one of them.
5757
+ controlRange: ship.getFinal(ATTR.DRONE_CONTROL_RANGE, 2e4)
5548
5758
  },
5549
5759
  projected: projectionReports,
5550
5760
  structure: computeStructureMeta(ctx),
@@ -5571,18 +5781,36 @@ function computeAlignTime(ship) {
5571
5781
  if (!mass || !agility) return 0;
5572
5782
  return Math.log(4) * mass * agility / 1e6;
5573
5783
  }
5784
+ var ATTR_IMPLANTNESS = 331;
5785
+ var ATTR_BOOSTERNESS = 1087;
5786
+ function dropSlotCollisions(entries) {
5787
+ const taken = /* @__PURE__ */ new Set();
5788
+ const out = [];
5789
+ for (const e of entries) {
5790
+ if (taken.has(e.slot)) continue;
5791
+ taken.add(e.slot);
5792
+ out.push(e.item);
5793
+ }
5794
+ return out;
5795
+ }
5574
5796
  function pickSensorStrength(ship) {
5575
5797
  const candidates = [
5576
- { attr: ATTR.SCAN_RADAR_STRENGTH, type: "radar" },
5577
- { attr: ATTR.SCAN_LADAR_STRENGTH, type: "ladar" },
5578
5798
  { attr: ATTR.SCAN_MAGNETOMETRIC_STRENGTH, type: "magnetometric" },
5799
+ { attr: ATTR.SCAN_LADAR_STRENGTH, type: "ladar" },
5800
+ { attr: ATTR.SCAN_RADAR_STRENGTH, type: "radar" },
5579
5801
  { attr: ATTR.SCAN_GRAVIMETRIC_STRENGTH, type: "gravimetric" }
5580
5802
  ];
5803
+ let best = -1;
5804
+ let type = "unknown";
5581
5805
  for (const c of candidates) {
5582
5806
  const v = ship.getFinal(c.attr, 0);
5583
- if (v > 0) return { value: v, type: c.type };
5807
+ if (v > best) {
5808
+ best = v;
5809
+ type = c.type;
5810
+ } else if (v === best) type = "multispectral";
5584
5811
  }
5585
- return { value: 0, type: "unknown" };
5812
+ if (best <= 0) return { value: Math.max(best, 0), type: best === 0 ? type : "unknown" };
5813
+ return { value: best, type };
5586
5814
  }
5587
5815
 
5588
5816
  // src/eft/parser.ts
@@ -5685,6 +5913,20 @@ function parseEft(text, dataset) {
5685
5913
  return { fit, warnings };
5686
5914
  }
5687
5915
  function classifySection(entries, idx, _dataset) {
5916
+ let sawImplant = false;
5917
+ let allImplant = true;
5918
+ for (const e of entries) {
5919
+ const { name, quantity } = parseEntryName(e.text);
5920
+ const typeID = idx.map.get(name.toLowerCase());
5921
+ if (typeID === void 0) {
5922
+ allImplant = false;
5923
+ continue;
5924
+ }
5925
+ const t = idx.types.get(typeID);
5926
+ if (quantity === null && isFittableImplant(t)) sawImplant = true;
5927
+ else allImplant = false;
5928
+ }
5929
+ if (sawImplant && allImplant) return "implants";
5688
5930
  for (const e of entries) {
5689
5931
  const { name, quantity } = parseEntryName(e.text);
5690
5932
  const typeID = idx.map.get(name.toLowerCase());
@@ -5701,6 +5943,13 @@ function classifySection(entries, idx, _dataset) {
5701
5943
  }
5702
5944
  return "cargo";
5703
5945
  }
5946
+ var ATTR_IMPLANTNESS2 = 331;
5947
+ var ATTR_BOOSTERNESS2 = 1087;
5948
+ var attrValue = (t, id) => t.attributes?.find((a) => a.id === id)?.v;
5949
+ function isFittableImplant(t) {
5950
+ if (t.categoryID !== CATEGORY.IMPLANT) return false;
5951
+ return attrValue(t, ATTR_IMPLANTNESS2) !== void 0 || attrValue(t, ATTR_BOOSTERNESS2) !== void 0;
5952
+ }
5704
5953
  function parseEntryName(text) {
5705
5954
  const m = QUANTITY_RE.exec(text);
5706
5955
  if (m) {
@@ -5737,6 +5986,23 @@ function consumeEntry(entry, sectionKind, fit, slotCursor, idx, dataset, warning
5737
5986
  fit.drones.push(drone);
5738
5987
  return;
5739
5988
  }
5989
+ if (sectionKind === "implants" && quantity === null && isFittableImplant(type)) {
5990
+ const implantSlot = attrValue(type, ATTR_IMPLANTNESS2);
5991
+ if (implantSlot !== void 0) {
5992
+ fit.implants.push({ id: tempId(), typeID, slot: implantSlot });
5993
+ } else {
5994
+ fit.boosters.push({
5995
+ id: tempId(),
5996
+ typeID,
5997
+ slot: attrValue(type, ATTR_BOOSTERNESS2) ?? 1,
5998
+ // Side effects are opt-in and an EFT cannot express them, so an
5999
+ // imported booster arrives with its drawbacks off — the same
6000
+ // default pyfa gives it.
6001
+ activeSideEffects: []
6002
+ });
6003
+ }
6004
+ return;
6005
+ }
5740
6006
  if (sectionKind === "cargo" || quantity !== null) {
5741
6007
  const cargo = { id: tempId(), typeID, count: quantity ?? 1 };
5742
6008
  fit.cargo.push(cargo);
@@ -6102,7 +6368,7 @@ var SUBSYSTEM_RANK_BY_TYPE_ID = /* @__PURE__ */ new Map([
6102
6368
  [45621, 3]
6103
6369
  // Loki Propulsion - Wake Limiter
6104
6370
  ]);
6105
- function attrValue(t, id) {
6371
+ function attrValue2(t, id) {
6106
6372
  for (const a of t.attributes) if (a.id === id) return a.v;
6107
6373
  return void 0;
6108
6374
  }
@@ -6116,7 +6382,7 @@ function computeT3CVariantCode(shipTypeID, subsystems, dataset) {
6116
6382
  for (const fs of subsystems) {
6117
6383
  const t = subsBucket.get(fs.typeID);
6118
6384
  if (!t) return null;
6119
- if (attrValue(t, PARENT_SHIP_ATTR) !== shipTypeID) return null;
6385
+ if (attrValue2(t, PARENT_SHIP_ATTR) !== shipTypeID) return null;
6120
6386
  fittedByGroup.set(t.groupID, fs.typeID);
6121
6387
  }
6122
6388
  let code = "";
@@ -6245,4 +6511,4 @@ var TARGET_PROFILE_PRESETS = [
6245
6511
  { name: "Keepstar", signatureRadius: 3e4, maxVelocity: 0, emResist: 0.7, thermalResist: 0.7, kineticResist: 0.7, explosiveResist: 0.7, isPreset: true }
6246
6512
  ];
6247
6513
 
6248
- 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 };
6514
+ export { ACTIVATION_EFFECT_ID, ATTR, BOOSTER_SIDE_EFFECT_IDS, 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, defaultModeTypeIDFor, 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 };