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