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