eve-fit-engine 0.1.34 → 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 (34) hide show
  1. package/data/.build +1 -1
  2. package/data/manifest.json +1 -1
  3. package/data/v0c61c617c9b9/attributes.json +1 -0
  4. package/dist/index.cjs +816 -548
  5. package/dist/index.d.cts +104 -4
  6. package/dist/index.d.ts +104 -4
  7. package/dist/index.js +815 -549
  8. package/dist/node.cjs +816 -548
  9. package/dist/node.d.cts +1 -1
  10. package/dist/node.d.ts +1 -1
  11. package/dist/node.js +815 -549
  12. package/package.json +3 -1
  13. package/data/v1e9ac4ea6d60/attributes.json +0 -1
  14. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/categories.json +0 -0
  15. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/clone-grades.json +0 -0
  16. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/dbuff-collections.json +0 -0
  17. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/dynamic-attributes.json +0 -0
  18. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/effects.json +0 -0
  19. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/groups.json +0 -0
  20. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/market-groups.json +0 -0
  21. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/meta-groups.json +0 -0
  22. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/types/charges.json +0 -0
  23. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/types/drones.json +0 -0
  24. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/types/fighters.json +0 -0
  25. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/types/implants.json +0 -0
  26. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/types/modules.json +0 -0
  27. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/types/mutaplasmids.json +0 -0
  28. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/types/ships.json +0 -0
  29. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/types/skills.json +0 -0
  30. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/types/structure-modules.json +0 -0
  31. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/types/structures.json +0 -0
  32. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/types/subsystems.json +0 -0
  33. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/types/system-effects.json +0 -0
  34. /package/data/{v1e9ac4ea6d60 → v0c61c617c9b9}/units.json +0 -0
package/dist/node.cjs CHANGED
@@ -494,6 +494,32 @@ var LEGACY_EFFECT_IDS = [
494
494
  // Frigate MJD sig bloom.
495
495
  { id: 12126, name: null, handler: "modifierEngine::applyLegacyMjdSigBloom (Frigate MJD)" }
496
496
  ];
497
+ var BOOSTER_SIDE_EFFECT_IDS = /* @__PURE__ */ new Set([
498
+ 2735,
499
+ // boosterArmorHpPenalty
500
+ 2736,
501
+ // boosterArmorRepairAmountPenalty
502
+ 2737,
503
+ // boosterShieldCapacityPenalty
504
+ 2739,
505
+ // boosterTurretOptimalRangePenalty
506
+ 2741,
507
+ // boosterTurretFalloffPenalty
508
+ 2745,
509
+ // boosterCapacitorCapacityPenalty
510
+ 2746,
511
+ // boosterMaxVelocityPenalty
512
+ 2747,
513
+ // boosterTurretTrackingPenalty
514
+ 2748,
515
+ // boosterMissileVelocityPenalty
516
+ 2749,
517
+ // boosterMissileExplosionVelocityPenalty
518
+ 2791,
519
+ // boosterMissileExplosionCloudPenaltyFixed
520
+ 4970
521
+ // boosterShieldBoostAmountPenaltyShieldSkills
522
+ ]);
497
523
  var OUT_OF_SCOPE_EFFECT_IDS = [
498
524
  // ---- Hardpoint / slot dispatch markers consumed via fitChecks (not a modifier dispatch path). ----
499
525
  { id: 40, name: null, handler: "fitChecks::isMissileLauncher (hardpoint marker)" },
@@ -581,7 +607,204 @@ function verifyLegacyEffectIds(effects) {
581
607
  return complaints;
582
608
  }
583
609
 
610
+ // src/fitChecks.ts
611
+ function readAttr(t, id) {
612
+ return t.attributes.find((a) => a.id === id)?.v;
613
+ }
614
+ function typeFitsSlotType(t, slot) {
615
+ for (const e of t.effects) {
616
+ const mapped = SLOT_EFFECT_TO_SLOT_TYPE[e.id];
617
+ if (mapped === slot) return true;
618
+ }
619
+ return false;
620
+ }
621
+ function isTurretWeapon(t) {
622
+ return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "TURRET");
623
+ }
624
+ function isMissileLauncher(t) {
625
+ return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "MISSILE");
626
+ }
627
+ function isSmartBomb(t) {
628
+ return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "SMARTBOMB");
629
+ }
630
+ function shipGroupRestrictions(t) {
631
+ const out = [];
632
+ for (const id of CAN_FIT_SHIP_GROUP_ATTRS) {
633
+ const v = readAttr(t, id);
634
+ if (v != null && v > 0) out.push(Math.round(v));
635
+ }
636
+ return out;
637
+ }
638
+ function shipTypeRestrictions(t) {
639
+ const out = [];
640
+ for (const id of CAN_FIT_SHIP_TYPE_ATTRS) {
641
+ const v = readAttr(t, id);
642
+ if (v != null && v > 0) out.push(Math.round(v));
643
+ }
644
+ return out;
645
+ }
646
+ function maxGroupFittedFor(mod) {
647
+ const v = readAttr(mod, ATTR.MAX_GROUP_FITTED);
648
+ return v != null && v > 0 ? Math.round(v) : void 0;
649
+ }
650
+ function maxTypeFittedFor(mod) {
651
+ const v = readAttr(mod, ATTR.MAX_TYPE_FITTED);
652
+ return v != null && v > 0 ? Math.round(v) : void 0;
653
+ }
654
+ function freeFitGroupSlotsFor(mod, fittedModules, dataset) {
655
+ const groupCap = maxGroupFittedFor(mod);
656
+ const typeCap = maxTypeFittedFor(mod);
657
+ if (groupCap === void 0 && typeCap === void 0) return Number.POSITIVE_INFINITY;
658
+ let groupCount = 0;
659
+ let typeCount = 0;
660
+ for (const fm of fittedModules) {
661
+ if (typeCap !== void 0 && fm.typeID === mod.id) typeCount++;
662
+ if (groupCap !== void 0) {
663
+ const t = dataset.getType(fm.typeID);
664
+ if (t && t.groupID === mod.groupID) groupCount++;
665
+ }
666
+ }
667
+ let free = Number.POSITIVE_INFINITY;
668
+ if (groupCap !== void 0) free = Math.min(free, groupCap - groupCount);
669
+ if (typeCap !== void 0) free = Math.min(free, typeCap - typeCount);
670
+ return Math.max(0, free);
671
+ }
672
+ function canFitModuleOnShip(mod, ship, slot, fitContext) {
673
+ if (!typeFitsSlotType(mod, slot)) {
674
+ return { ok: false, reason: `Module does not declare a ${slot} slot effect` };
675
+ }
676
+ if (!ship) return { ok: true };
677
+ const allowedGroups = shipGroupRestrictions(mod);
678
+ const allowedTypes = shipTypeRestrictions(mod);
679
+ if (allowedGroups.length > 0 || allowedTypes.length > 0) {
680
+ const groupOk = allowedGroups.includes(ship.groupID);
681
+ const typeOk = allowedTypes.includes(ship.id);
682
+ if (!groupOk && !typeOk) {
683
+ return { ok: false, reason: "Ship hull/group not allowed by this module" };
684
+ }
685
+ }
686
+ if (slot === "RIG") {
687
+ const modSize = readAttr(mod, ATTR.RIG_SIZE);
688
+ const shipSize = readAttr(ship, ATTR.RIG_SIZE);
689
+ if (modSize != null && shipSize != null && Math.round(modSize) !== Math.round(shipSize)) {
690
+ return { ok: false, reason: "Rig size mismatch" };
691
+ }
692
+ }
693
+ if (slot === "SUBSYSTEM") {
694
+ const fitsTo = readAttr(mod, 1380);
695
+ if (fitsTo != null && fitsTo > 0 && Math.round(fitsTo) !== ship.id) {
696
+ return { ok: false, reason: "Subsystem locked to a different T3C hull" };
697
+ }
698
+ }
699
+ if (slot === "HI") {
700
+ if (isTurretWeapon(mod)) {
701
+ const turrets = readAttr(ship, ATTR.TURRET_HARDPOINTS) ?? 0;
702
+ if (turrets <= 0) {
703
+ return { ok: false, reason: "Ship has no turret hardpoints" };
704
+ }
705
+ } else if (isMissileLauncher(mod)) {
706
+ const launchers = readAttr(ship, ATTR.LAUNCHER_HARDPOINTS) ?? 0;
707
+ if (launchers <= 0) {
708
+ return { ok: false, reason: "Ship has no launcher hardpoints" };
709
+ }
710
+ }
711
+ }
712
+ if (fitContext) {
713
+ const free = freeFitGroupSlotsFor(mod, fitContext.fittedModules, fitContext.dataset);
714
+ if (free <= 0) {
715
+ const cap = maxTypeFittedFor(mod) ?? maxGroupFittedFor(mod);
716
+ return { ok: false, reason: `Only ${cap} of this module type can be fitted to a ship` };
717
+ }
718
+ }
719
+ return { ok: true };
720
+ }
721
+ function chargeGroupsForModule(mod) {
722
+ const out = [];
723
+ for (const attrID of CHARGE_GROUP_ATTRS) {
724
+ const v = readAttr(mod, attrID);
725
+ if (v != null && v > 0) out.push(Math.round(v));
726
+ }
727
+ return out;
728
+ }
729
+ function moduleAcceptsAnyCharge(mod) {
730
+ return chargeGroupsForModule(mod).length > 0;
731
+ }
732
+ function moduleAcceptsChargeType(mod, charge) {
733
+ const allowedGroups = chargeGroupsForModule(mod);
734
+ if (allowedGroups.length === 0) return false;
735
+ if (!allowedGroups.includes(charge.groupID)) return false;
736
+ const modSize = readAttr(mod, ATTR.CHARGE_SIZE);
737
+ const chargeSize = readAttr(charge, ATTR.CHARGE_SIZE);
738
+ if (modSize != null && chargeSize != null && Math.round(chargeSize) > Math.round(modSize)) {
739
+ return false;
740
+ }
741
+ return true;
742
+ }
743
+ var ACTIVE_EFFECT_CATEGORIES = /* @__PURE__ */ new Set([1, 2, 3]);
744
+ function isActivatableModule(mod, effects) {
745
+ for (const e of mod.effects) {
746
+ const eff = effects.get(e.id);
747
+ if (eff && eff.effectCategoryID !== void 0 && ACTIVE_EFFECT_CATEGORIES.has(eff.effectCategoryID)) {
748
+ return true;
749
+ }
750
+ }
751
+ return false;
752
+ }
753
+ var DEFAULT_OFFLINE_ACTIVATION_GROUPS = /* @__PURE__ */ new Set([
754
+ 330,
755
+ 4117
756
+ ]);
757
+ function defaultStateForModule(mod, effects) {
758
+ if (DEFAULT_OFFLINE_ACTIVATION_GROUPS.has(mod.groupID)) return "ONLINE";
759
+ return isActivatableModule(mod, effects) ? "ACTIVE" : "ONLINE";
760
+ }
761
+ function freeHardpointsFor(mod, ship, fittedHiModules, dataset) {
762
+ const turretCap = readAttr(ship, ATTR.TURRET_HARDPOINTS) ?? 0;
763
+ const launcherCap = readAttr(ship, ATTR.LAUNCHER_HARDPOINTS) ?? 0;
764
+ let turretUsed = 0;
765
+ let launcherUsed = 0;
766
+ for (const m of fittedHiModules) {
767
+ const t = dataset.getType(m.typeID);
768
+ if (!t) continue;
769
+ if (isTurretWeapon(t)) turretUsed++;
770
+ else if (isMissileLauncher(t)) launcherUsed++;
771
+ }
772
+ let hardpointFree;
773
+ if (isTurretWeapon(mod)) hardpointFree = Math.max(0, turretCap - turretUsed);
774
+ else if (isMissileLauncher(mod)) hardpointFree = Math.max(0, launcherCap - launcherUsed);
775
+ else hardpointFree = Number.POSITIVE_INFINITY;
776
+ const groupFree = freeFitGroupSlotsFor(mod, fittedHiModules, dataset);
777
+ return Math.min(hardpointFree, groupFree);
778
+ }
779
+ var TACTICAL_DESTROYER_GROUP = 1305;
780
+ var MODED_HULL_NAMES = /* @__PURE__ */ new Set(["Anhinga"]);
781
+ var SHIP_MODIFIERS_GROUP = 1306;
782
+ function defaultModeTypeIDFor(ship, dataset) {
783
+ if (ship.groupID !== TACTICAL_DESTROYER_GROUP && !MODED_HULL_NAMES.has(ship.name ?? "")) return void 0;
784
+ const prefix = (ship.name ?? "").toLowerCase();
785
+ if (!prefix) return void 0;
786
+ let best;
787
+ for (const t of dataset.typesByBucket.modules?.values() ?? []) {
788
+ if (t.groupID !== SHIP_MODIFIERS_GROUP) continue;
789
+ if (!(t.name ?? "").toLowerCase().startsWith(prefix)) continue;
790
+ if (best === void 0 || t.id < best.id) best = t;
791
+ }
792
+ return best?.id;
793
+ }
794
+
584
795
  // src/fitContext.ts
796
+ var MISSILE_BOOST_SKILLS = [
797
+ 3319,
798
+ // Missile Launcher Operation
799
+ 3323
800
+ // Defender Missiles
801
+ ];
802
+ var PYFA_GROUP_FILTERED_EFFECTS = /* @__PURE__ */ new Map([
803
+ // 3495 shipCapPropulsionJamming — the Interceptor / Interdictor role bonus
804
+ // to Warp Scrambler + Stasis Web capacitor need. pyfa:
805
+ // `mod.item.group.name in ('Stasis Web', 'Warp Scrambler')`.
806
+ [3495, /* @__PURE__ */ new Set([52, 65])]
807
+ ]);
585
808
  var FitContext = class {
586
809
  constructor(init) {
587
810
  /** Projected hostile sources currently in scope. Populated by the
@@ -689,14 +912,19 @@ var FitContext = class {
689
912
  * - EffectStopper: handled outside this resolver (stops other effects
690
913
  * rather than applying a modifier).
691
914
  */
692
- targetsForModifier(modifier, source) {
915
+ targetsForModifier(modifier, source, effectID) {
693
916
  const root = this.resolveDomain(modifier.domain, source);
694
917
  switch (modifier.func) {
695
918
  case "ItemModifier":
919
+ if (modifier.domain === "charID" && modifier.modifiedAttributeID === ATTR.DRONE_CONTROL_RANGE) {
920
+ return this.ship ? [this.ship] : [];
921
+ }
696
922
  if (modifier.domain === "charID" && source.kind === "module" && modifier.modifiedAttributeID === 212) {
697
923
  const out = [];
698
924
  for (const m of this.modules) {
699
- if (m.charge) out.push(m.charge);
925
+ if (!m.charge) continue;
926
+ if (!MISSILE_BOOST_SKILLS.some((sid) => itemRequiresSkill(m.charge, sid))) continue;
927
+ out.push(m.charge);
700
928
  }
701
929
  return out;
702
930
  }
@@ -708,7 +936,10 @@ var FitContext = class {
708
936
  return this.itemsInLocation(root).filter((it) => it.groupID === modifier.groupID);
709
937
  }
710
938
  case "LocationRequiredSkillModifier": {
711
- if (!root || modifier.skillTypeID === void 0) return [];
939
+ if (!root) return [];
940
+ const groups = effectID === void 0 ? void 0 : PYFA_GROUP_FILTERED_EFFECTS.get(effectID);
941
+ if (groups) return this.itemsInLocation(root).filter((it) => groups.has(it.groupID));
942
+ if (modifier.skillTypeID === void 0) return [];
712
943
  const sid = modifier.skillTypeID;
713
944
  return this.itemsInLocation(root).filter((it) => itemRequiresSkill(it, sid));
714
945
  }
@@ -884,15 +1115,11 @@ var ModifiedAttribute = class {
884
1115
  for (const a of byOp.get("PostDiv") ?? []) {
885
1116
  postMulEntries.push({ value: 1 / a.value, stackingGroup: a.stackingGroup });
886
1117
  }
887
- if (postMulEntries.length > 0) {
888
- value *= combineMultiplicative(postMulEntries);
889
- }
890
- const postPercentEntries = [];
891
1118
  for (const a of byOp.get("PostPercent") ?? []) {
892
- postPercentEntries.push({ value: 1 + a.value, stackingGroup: a.stackingGroup });
1119
+ postMulEntries.push({ value: 1 + a.value, stackingGroup: a.stackingGroup });
893
1120
  }
894
- if (postPercentEntries.length > 0) {
895
- value *= combineMultiplicative(postPercentEntries);
1121
+ if (postMulEntries.length > 0) {
1122
+ value *= combineMultiplicative(postMulEntries);
896
1123
  }
897
1124
  const postAssign = byOp.get("PostAssign");
898
1125
  if (postAssign && postAssign.length > 0) {
@@ -925,6 +1152,13 @@ var VOLUME_ATTR_ID = 161;
925
1152
  var CAPACITY_ATTR_ID = 38;
926
1153
  var ItemState = class {
927
1154
  constructor(init) {
1155
+ /**
1156
+ * Attribute schema, needed to honour the SDE's attribute CAPS
1157
+ * (`maxAttributeID` / `minAttributeID`). Assigned by `computeFit` once for
1158
+ * every item in the fit; when absent, `getFinal` simply doesn't clamp — the
1159
+ * behaviour before caps existed.
1160
+ */
1161
+ this.attrSchema = null;
928
1162
  this.attrs = /* @__PURE__ */ new Map();
929
1163
  this.kind = init.kind;
930
1164
  this.id = init.id;
@@ -934,6 +1168,7 @@ var ItemState = class {
934
1168
  this.categoryID = init.type.categoryID;
935
1169
  this.state = init.state ?? "ONLINE";
936
1170
  this.charge = init.charge ?? null;
1171
+ this.activeSideEffects = new Set(init.activeSideEffects ?? []);
937
1172
  for (const a of init.type.attributes) {
938
1173
  const base = init.attributeOverrides?.[a.id] ?? a.v;
939
1174
  this.attrs.set(a.id, new ModifiedAttribute(a.id, base));
@@ -977,7 +1212,47 @@ var ItemState = class {
977
1212
  getFinal(id, fallback = 0) {
978
1213
  const ma = this.attrs.get(id);
979
1214
  if (!ma) return fallback;
980
- return ma.compute();
1215
+ return this.clampToCaps(id, ma.compute());
1216
+ }
1217
+ /**
1218
+ * Clamp a computed value to the ceiling / floor the SDE declares for that
1219
+ * attribute, mirroring pyfa's `__calculateValue` (which applies the same
1220
+ * bounds to forced values AND to normally-calculated ones).
1221
+ *
1222
+ * An attribute may name another attribute as its bound — resonances are
1223
+ * capped by `<layer>MaxDamageResonance` (1.0), `maxVelocity` by
1224
+ * `speedLimit`, the `*Bonus` attributes floored by `ConstantMinusNinetyNine`
1225
+ * — and the bound is read off THIS item, so a hull that raises its own
1226
+ * ceiling is honoured.
1227
+ *
1228
+ * Without it a Polarized weapon, which force-assigns every resonance to
1229
+ * `resistanceKiller` = 100, read as -9900% resistance instead of the 0% the
1230
+ * game shows: the cap is what turns "assign 100" into "resistances removed".
1231
+ * Measured on real published fits — five hulls, forty stats.
1232
+ */
1233
+ clampToCaps(id, value) {
1234
+ const schema = this.attrSchema?.get(id);
1235
+ if (!schema) return value;
1236
+ let out = value;
1237
+ if (schema.minAttributeID !== void 0) {
1238
+ const floor = this.capValue(schema.minAttributeID);
1239
+ if (floor !== void 0) out = Math.max(out, floor);
1240
+ }
1241
+ if (schema.maxAttributeID !== void 0) {
1242
+ const ceil = this.capValue(schema.maxAttributeID);
1243
+ if (ceil !== void 0) out = Math.min(out, ceil);
1244
+ }
1245
+ return out;
1246
+ }
1247
+ /** Value of a bounding attribute on this item, falling back to the SDE
1248
+ * default when the item doesn't carry it (pyfa's `getOriginal(key,
1249
+ * default)`). Bounds never declare bounds of their own, so this recurses
1250
+ * exactly one level. */
1251
+ capValue(capAttrID) {
1252
+ const ma = this.attrs.get(capAttrID);
1253
+ if (ma) return ma.compute();
1254
+ const def = this.attrSchema?.get(capAttrID)?.defaultValue;
1255
+ return def;
981
1256
  }
982
1257
  /** `defaultBase` is the seed base value when the target attribute isn't
983
1258
  * in the item's typeDogma — needed for SDE attrs whose `defaultValue`
@@ -1095,7 +1370,13 @@ function makeImplantState(fi, type) {
1095
1370
  return new ItemState({ kind: "implant", id: fi.id, type, state: "ONLINE" });
1096
1371
  }
1097
1372
  function makeBoosterState(fb, type) {
1098
- return new ItemState({ kind: "booster", id: fb.id, type, state: "ONLINE" });
1373
+ return new ItemState({
1374
+ kind: "booster",
1375
+ id: fb.id,
1376
+ type,
1377
+ state: "ONLINE",
1378
+ activeSideEffects: fb.activeSideEffects ?? []
1379
+ });
1099
1380
  }
1100
1381
  function makeSubsystemState(fs2, type) {
1101
1382
  return new ItemState({ kind: "subsystem", id: fs2.id, type, state: "ONLINE" });
@@ -1104,312 +1385,347 @@ function makeModeState(modeTypeID, type) {
1104
1385
  return new ItemState({ kind: "mode", id: `mode:${modeTypeID}`, type, state: "ACTIVE" });
1105
1386
  }
1106
1387
 
1107
- // src/stackingGroups.ts
1108
- var STACKING_PENALTY_GROUPS = /* @__PURE__ */ new Map([
1109
- [89, "default"],
1110
- [91, "default"],
1111
- [92, "default"],
1112
- [93, "default"],
1113
- [95, "default"],
1114
- [96, "default"],
1115
- [394, "default"],
1116
- [395, "default"],
1117
- [494, "default"],
1118
- [607, "postMul"],
1119
- [657, "default"],
1120
- [699, "default"],
1121
- [763, "default"],
1122
- [784, "default"],
1123
- [854, "cloakingScanResolutionMultiplier"],
1124
- [856, "default"],
1125
- [889, "default"],
1126
- [891, "default"],
1127
- [892, "default"],
1128
- [1024, "default"],
1129
- [1230, "default"],
1130
- [1281, "default"],
1131
- [1318, "default"],
1132
- [1445, "default"],
1133
- [1446, "default"],
1134
- [1448, "default"],
1135
- [1452, "default"],
1136
- [1472, "default"],
1137
- [1590, "default"],
1138
- [1617, "default"],
1139
- [1720, "default"],
1140
- [1764, "default"],
1141
- [1885, "postPerc"],
1142
- [1886, "postPerc"],
1143
- [2013, "default"],
1144
- [2014, "default"],
1145
- [2019, "default"],
1146
- [2020, "default"],
1147
- [2041, "default"],
1148
- [2052, "default"],
1149
- [2152, "default"],
1150
- [2232, "default"],
1151
- [2302, "preMul"],
1152
- [2644, "default"],
1153
- [2645, "default"],
1154
- [2646, "default"],
1155
- [2670, "default"],
1156
- [2693, "default"],
1157
- [2694, "default"],
1158
- [2695, "default"],
1159
- [2696, "default"],
1160
- [2697, "default"],
1161
- [2698, "default"],
1162
- [2716, "default"],
1163
- [2717, "default"],
1164
- [2792, "default"],
1165
- [2795, "default"],
1166
- [2796, "default"],
1167
- [2797, "default"],
1168
- [2798, "default"],
1169
- [2799, "default"],
1170
- [2801, "default"],
1171
- [2802, "default"],
1172
- [2803, "default"],
1173
- [2804, "default"],
1174
- [2851, "default"],
1175
- [2858, "default"],
1176
- [2865, "default"],
1177
- [2867, "default"],
1178
- [2868, "default"],
1179
- [3001, "postPerc"],
1180
- [3046, "default"],
1181
- [3174, "default"],
1182
- [3175, "default"],
1183
- [3182, "default"],
1184
- [3200, "default"],
1185
- [3201, "default"],
1186
- [3586, "default"],
1187
- [3655, "default"],
1188
- [3656, "default"],
1189
- [3657, "default"],
1190
- [3659, "default"],
1191
- [3674, "default"],
1192
- [3726, "default"],
1193
- [3727, "default"],
1194
- [3993, "postMul"],
1195
- [3995, "postMul"],
1196
- [3996, "default"],
1197
- [3997, "default"],
1198
- [3998, "default"],
1199
- [3999, "default"],
1200
- [4002, "postMul"],
1201
- [4003, "postMul"],
1202
- [4016, "default"],
1203
- [4017, "postMul"],
1204
- [4018, "postMul"],
1205
- [4019, "postMul"],
1206
- [4020, "postMul"],
1207
- [4021, "postMul"],
1208
- [4022, "postMul"],
1209
- [4023, "postMul"],
1210
- [4054, "default"],
1211
- [4055, "default"],
1212
- [4056, "default"],
1213
- [4057, "postMul"],
1214
- [4058, "postMul"],
1215
- [4059, "postMul"],
1216
- [4060, "postMul"],
1217
- [4061, "postMul"],
1218
- [4062, "postMul"],
1219
- [4063, "postMul"],
1220
- [4086, "postMul"],
1221
- [4088, "postMul"],
1222
- [4089, "postMul"],
1223
- [4135, "default"],
1224
- [4136, "default"],
1225
- [4137, "default"],
1226
- [4138, "default"],
1227
- [4162, "default"],
1228
- [4280, "default"],
1229
- [4358, "default"],
1230
- [4464, "default"],
1231
- [4489, "default"],
1232
- [4490, "default"],
1233
- [4491, "default"],
1234
- [4492, "default"],
1235
- [4527, "default"],
1236
- [4559, "default"],
1237
- [4575, "default"],
1238
- [4809, "default"],
1239
- [4810, "default"],
1240
- [4811, "default"],
1241
- [4812, "default"],
1242
- [4906, "postMul"],
1243
- [4928, "preMul"],
1244
- [4961, "postMul"],
1245
- [5081, "default"],
1246
- [5188, "default"],
1247
- [5189, "default"],
1248
- [5190, "default"],
1249
- [5213, "default"],
1250
- [5214, "default"],
1251
- [5230, "default"],
1252
- [5231, "default"],
1253
- [5397, "default"],
1254
- [5399, "default"],
1255
- [5440, "postMul"],
1256
- [5468, "default"],
1257
- [5560, "default"],
1258
- [5618, "postPerc"],
1259
- [5757, "default"],
1260
- [5867, "default"],
1261
- [5911, "default"],
1262
- [5912, "postMul"],
1263
- [5914, "postMul"],
1264
- [5915, "postMul"],
1265
- [5916, "postMul"],
1266
- [5917, "postMul"],
1267
- [5918, "postMul"],
1268
- [5919, "postMul"],
1269
- [5920, "postMul"],
1270
- [5921, "postMul"],
1271
- [5922, "postMul"],
1272
- [5923, "postMul"],
1273
- [5924, "postMul"],
1274
- [5925, "postMul"],
1275
- [5926, "postMul"],
1276
- [5927, "postMul"],
1277
- [5929, "postMul"],
1278
- [5951, "default"],
1279
- [5998, "default"],
1280
- [6010, "postDiv"],
1281
- [6011, "postDiv"],
1282
- [6012, "postDiv"],
1283
- [6014, "postDiv"],
1284
- [6015, "postDiv"],
1285
- [6016, "postDiv"],
1286
- [6017, "postDiv"],
1287
- [6039, "postDiv"],
1288
- [6040, "postDiv"],
1289
- [6041, "postDiv"],
1290
- [6063, "default"],
1291
- [6076, "postDiv"],
1292
- [6110, "default"],
1293
- [6111, "default"],
1294
- [6112, "default"],
1295
- [6113, "default"],
1296
- [6135, "default"],
1297
- [6152, "postDiv"],
1298
- [6154, "postDiv"],
1299
- [6164, "default"],
1300
- [6201, "default"],
1301
- [6208, "default"],
1302
- [6402, "default"],
1303
- [6403, "default"],
1304
- [6404, "default"],
1305
- [6405, "default"],
1306
- [6406, "default"],
1307
- [6409, "default"],
1308
- [6410, "default"],
1309
- [6411, "default"],
1310
- [6412, "default"],
1311
- [6422, "default"],
1312
- [6423, "default"],
1313
- [6424, "default"],
1314
- [6425, "default"],
1315
- [6426, "default"],
1316
- [6427, "default"],
1317
- [6428, "default"],
1318
- [6435, "default"],
1319
- [6439, "default"],
1320
- [6440, "default"],
1321
- [6441, "default"],
1322
- [6448, "default"],
1323
- [6449, "default"],
1324
- [6472, "default"],
1325
- [6473, "default"],
1326
- [6474, "default"],
1327
- [6476, "default"],
1328
- [6478, "default"],
1329
- [6479, "default"],
1330
- [6481, "default"],
1331
- [6484, "postMul"],
1332
- [6487, "default"],
1333
- [6555, "default"],
1334
- [6556, "default"],
1335
- [6557, "default"],
1336
- [6559, "default"],
1337
- [6566, "postMul"],
1338
- [6567, "default"],
1339
- [6581, "default"],
1340
- [6582, "postPercent"],
1341
- [6658, "preMul"],
1342
- [6670, "default"],
1343
- [6671, "default"],
1344
- [6682, "default"],
1345
- [6683, "default"],
1346
- [6684, "default"],
1347
- [6686, "default"],
1348
- [6690, "default"],
1349
- [6692, "default"],
1350
- [6693, "default"],
1351
- [6694, "default"],
1352
- [6727, "default"],
1353
- [6730, "postMul"],
1354
- [6731, "postMul"],
1355
- [6796, "postDiv"],
1356
- [6797, "postDiv"],
1357
- [6798, "postDiv"],
1358
- [6799, "postDiv"],
1359
- [6801, "postDiv"],
1360
- [6877, "default"],
1361
- [7029, "default"],
1362
- [7077, "default"],
1363
- [7078, "default"],
1364
- [7098, "default"],
1365
- [7111, "default"],
1366
- [7142, "default"],
1367
- [7202, "default"],
1368
- [7203, "default"],
1369
- [7223, "default"],
1370
- [7237, "default"],
1371
- [8033, "default"],
1372
- [8057, "default"],
1373
- [8076, "default"],
1374
- [8082, "default"],
1375
- [8108, "postMul"],
1376
- [8109, "postMul"],
1377
- [8111, "default"],
1378
- [8112, "default"],
1379
- [8113, "postMul"],
1380
- [8114, "postMul"],
1381
- [8119, "default"],
1382
- [11445, "default"],
1383
- [11691, "default"],
1384
- [11946, "default"],
1385
- [11947, "postMul"],
1386
- [11948, "postMul"],
1387
- [11953, "postMul"],
1388
- [12126, "default"],
1389
- [12597, "default"],
1390
- [12761, "default"],
1391
- [12794, "postDiv"],
1392
- [12795, "postDiv"],
1393
- [12796, "postDiv"],
1394
- [12798, "postDiv"],
1395
- [12799, "postDiv"],
1396
- [12838, "postMul"],
1397
- [12839, "postMul"]
1388
+ // src/stackingPenalised.ts
1389
+ var STACKING_PENALISED = /* @__PURE__ */ new Map([
1390
+ [89, ["51#default"]],
1391
+ [91, ["64#default"]],
1392
+ [92, ["64#default"]],
1393
+ [93, ["64#default"]],
1394
+ [95, ["51#default"]],
1395
+ [96, ["51#default"]],
1396
+ [394, ["37#default!booster+implant+skill"]],
1397
+ [395, ["70#default!booster+implant+skill"]],
1398
+ [607, ["37#postMul"]],
1399
+ [657, ["70#default"]],
1400
+ [699, ["564#default!booster+implant+skill"]],
1401
+ [763, ["*#default"]],
1402
+ [784, ["281#default!booster+implant+skill"]],
1403
+ [854, ["564#cloakingScanResolutionMultiplier"]],
1404
+ [856, ["600#default!booster+implant+skill"]],
1405
+ [889, ["51#default"]],
1406
+ [891, ["37#default!ship"]],
1407
+ [892, ["37#default!ship"]],
1408
+ [1024, ["37#default!ship"]],
1409
+ [1230, ["37#default!ship"]],
1410
+ [1281, ["84#default!implant"]],
1411
+ [1445, ["54#default!booster+skill"]],
1412
+ [1446, ["54#default!booster+skill"]],
1413
+ [1448, ["54#default!booster+skill"]],
1414
+ [1472, ["654#default!booster+implant+skill"]],
1415
+ [1590, ["653#default!booster+implant+skill"]],
1416
+ [1617, ["70#default"]],
1417
+ [1720, ["68#default"]],
1418
+ [1764, ["37#default!booster+implant+skill"]],
1419
+ [1885, ["51#default!ship"]],
1420
+ [1886, ["51#default!ship"]],
1421
+ [2013, ["37#default!booster+implant"]],
1422
+ [2014, ["54#default!skill"]],
1423
+ [2019, ["68#default!implant+skill"]],
1424
+ [2020, ["84#default!implant+skill"]],
1425
+ [2041, ["*#default"]],
1426
+ [2052, ["*#default"]],
1427
+ [2152, ["37#default"]],
1428
+ [2232, ["*#default"]],
1429
+ [2302, ["*#preMul"]],
1430
+ [2644, ["552#default"]],
1431
+ [2645, ["564#default"]],
1432
+ [2646, ["76#default"]],
1433
+ [2670, ["*#default", "564#default", "76#default"]],
1434
+ [2693, ["158#default!booster"]],
1435
+ [2694, ["158#default!booster"]],
1436
+ [2695, ["158#default!booster"]],
1437
+ [2696, ["54#default!booster"]],
1438
+ [2697, ["54#default!booster"]],
1439
+ [2698, ["54#default!booster"]],
1440
+ [2716, ["552#default"]],
1441
+ [2717, ["70#default"]],
1442
+ [2792, ["*#default"]],
1443
+ [2795, ["*#default"]],
1444
+ [2796, ["4#default"]],
1445
+ [2797, ["51#default"]],
1446
+ [2798, ["64#default!booster"]],
1447
+ [2799, ["51#default"]],
1448
+ [2801, ["51#default"]],
1449
+ [2802, ["64#default!booster"]],
1450
+ [2803, ["64#default!booster"]],
1451
+ [2804, ["51#default"]],
1452
+ [2851, ["*#default!booster"]],
1453
+ [2858, ["37#default"]],
1454
+ [2865, ["37#default"]],
1455
+ [2867, ["64#default"]],
1456
+ [2868, ["84#default"]],
1457
+ [3001, ["51#postPerc"]],
1458
+ [3046, ["37#default"]],
1459
+ [3174, ["54#default"]],
1460
+ [3175, ["20#default"]],
1461
+ [3182, ["*#default"]],
1462
+ [3200, ["84#default"]],
1463
+ [3201, ["68#default"]],
1464
+ [3586, ["566#default!skill"]],
1465
+ [3655, ["54#default"]],
1466
+ [3656, ["160#default"]],
1467
+ [3657, ["564#default"]],
1468
+ [3659, ["76#default"]],
1469
+ [3674, ["37#default"]],
1470
+ [3726, ["70#default"]],
1471
+ [3727, ["37#default"]],
1472
+ [3993, ["76#postMul"]],
1473
+ [3995, ["552#postMul"]],
1474
+ [3996, ["267#default"]],
1475
+ [3997, ["268#default"]],
1476
+ [3998, ["269#default"]],
1477
+ [3999, ["270#default"]],
1478
+ [4002, ["37#postMul"]],
1479
+ [4003, ["37#postMul"]],
1480
+ [4016, ["64#default"]],
1481
+ [4017, ["118#postMul"]],
1482
+ [4018, ["114#postMul"]],
1483
+ [4019, ["116#postMul"]],
1484
+ [4020, ["117#postMul"]],
1485
+ [4021, ["64#postMul"]],
1486
+ [4022, ["160#postMul"]],
1487
+ [4023, ["653#postMul"]],
1488
+ [4054, ["64#default"]],
1489
+ [4055, ["64#default"]],
1490
+ [4056, ["64#default"]],
1491
+ [4057, ["114#postMul"]],
1492
+ [4058, ["116#postMul"]],
1493
+ [4059, ["117#postMul"]],
1494
+ [4060, ["118#postMul"]],
1495
+ [4061, ["118#postMul"]],
1496
+ [4062, ["114#postMul"]],
1497
+ [4063, ["116#postMul"]],
1498
+ [4086, ["84#postMul"]],
1499
+ [4088, ["84#postMul"]],
1500
+ [4089, ["68#postMul"]],
1501
+ [4135, ["271#default"]],
1502
+ [4136, ["272#default"]],
1503
+ [4137, ["273#default"]],
1504
+ [4138, ["274#default"]],
1505
+ [4162, ["1371#default!booster+implant+skill"]],
1506
+ [4280, ["70#default"]],
1507
+ [4358, ["54#default"]],
1508
+ [4464, ["51#default"]],
1509
+ [4489, ["37#default"]],
1510
+ [4490, ["37#default"]],
1511
+ [4491, ["37#default"]],
1512
+ [4492, ["37#default"]],
1513
+ [4527, ["158#default"]],
1514
+ [4559, ["*#default"]],
1515
+ [4575, ["2044#default", "37#default", "54#default", "564#default", "64#default", "68#default"]],
1516
+ [4809, ["238#default"]],
1517
+ [4810, ["239#default"]],
1518
+ [4811, ["240#default"]],
1519
+ [4812, ["241#default"]],
1520
+ [4906, ["64#postMul"]],
1521
+ [4928, ["*#preMul"]],
1522
+ [4961, ["68#postMul"]],
1523
+ [5081, ["76#default"]],
1524
+ [5188, ["160#default!booster"]],
1525
+ [5189, ["160#default!booster"]],
1526
+ [5190, ["160#default!booster"]],
1527
+ [5213, ["37#default!ship"]],
1528
+ [5214, ["37#default!ship"]],
1529
+ [5230, ["*#default"]],
1530
+ [5231, ["*#default"]],
1531
+ [5397, ["1372#default"]],
1532
+ [5399, ["1371#default"]],
1533
+ [5440, ["117#postMul"]],
1534
+ [5468, ["70#default!ship"]],
1535
+ [5560, ["669#default!ship"]],
1536
+ [5618, ["51#default!ship"]],
1537
+ [5757, ["*#default", "566#default"]],
1538
+ [5867, ["281#default!ship"]],
1539
+ [5911, ["868#default"]],
1540
+ [5912, ["90#postMul"]],
1541
+ [5914, ["97#postMul"]],
1542
+ [5915, ["90#postMul"]],
1543
+ [5916, ["116#postMul"]],
1544
+ [5917, ["117#postMul"]],
1545
+ [5918, ["118#postMul"]],
1546
+ [5919, ["114#postMul"]],
1547
+ [5920, ["654#postMul"]],
1548
+ [5921, ["554#postMul"]],
1549
+ [5922, ["20#postMul"]],
1550
+ [5923, ["97#postMul"]],
1551
+ [5924, ["238#postMul"]],
1552
+ [5925, ["239#postMul"]],
1553
+ [5926, ["240#postMul"]],
1554
+ [5927, ["241#postMul"]],
1555
+ [5929, ["160#postMul"]],
1556
+ [5951, ["600#default"]],
1557
+ [5998, ["70#default"]],
1558
+ [6001, ["70#default"]],
1559
+ [6010, ["76#postDiv"]],
1560
+ [6011, ["54#postDiv"]],
1561
+ [6012, ["*#postDiv"]],
1562
+ [6014, ["552#postDiv"]],
1563
+ [6015, ["*#postDiv"]],
1564
+ [6016, ["70#postDiv"]],
1565
+ [6017, ["37#postDiv"]],
1566
+ [6039, ["160#postDiv"]],
1567
+ [6040, ["554#postDiv"]],
1568
+ [6041, ["*#postDiv"]],
1569
+ [6063, ["*#default"]],
1570
+ [6076, ["37#postDiv"]],
1571
+ [6110, ["37#default"]],
1572
+ [6111, ["281#default"]],
1573
+ [6112, ["654#default"]],
1574
+ [6113, ["653#default"]],
1575
+ [6135, ["*#default"]],
1576
+ [6152, ["54#postDiv"]],
1577
+ [6154, ["20#postDiv"]],
1578
+ [6164, ["37#default"]],
1579
+ [6201, ["37#default"]],
1580
+ [6208, ["552#default"]],
1581
+ [6402, ["653#default"]],
1582
+ [6403, ["37#default"]],
1583
+ [6404, ["2044#default", "54#default"]],
1584
+ [6405, ["6#default"]],
1585
+ [6406, ["158#default", "2044#default", "54#default"]],
1586
+ [6409, ["564#default"]],
1587
+ [6410, ["654#default"]],
1588
+ [6411, ["37#default"]],
1589
+ [6412, ["99#default"]],
1590
+ [6422, ["564#default", "76#default"]],
1591
+ [6423, ["*#default"]],
1592
+ [6424, ["*#default"]],
1593
+ [6425, ["552#default"]],
1594
+ [6426, ["37#default"]],
1595
+ [6427, ["*#default"]],
1596
+ [6428, ["*#default"]],
1597
+ [6435, ["37#default"]],
1598
+ [6439, ["271#default", "272#default", "273#default", "274#default", "37#default", "552#default"]],
1599
+ [6440, ["37#default"]],
1600
+ [6441, ["37#default", "552#default"]],
1601
+ [6448, ["*#default"]],
1602
+ [6449, ["*#default", "51#default"]],
1603
+ [6472, ["37#default"]],
1604
+ [6473, ["37#default"]],
1605
+ [6474, ["37#default"]],
1606
+ [6476, ["37#default"]],
1607
+ [6478, ["552#default"]],
1608
+ [6479, ["*#default"]],
1609
+ [6481, ["564#default", "76#default"]],
1610
+ [6484, ["*#postMul"]],
1611
+ [6487, ["2045#default"]],
1612
+ [6555, ["37#default"]],
1613
+ [6556, ["2130#default", "2178#default", "2226#default", "64#default"]],
1614
+ [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"]],
1615
+ [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"]],
1616
+ [6566, ["2177#postMul", "2182#postMul", "2233#postMul", "37#postMul"]],
1617
+ [6567, ["*#default", "564#default"]],
1618
+ [6581, ["*#default", "2044#default", "37#default", "4#default", "54#default", "564#default", "64#default"]],
1619
+ [6582, ["4#postMul", "68#default", "84#default", "charge:37#default", "module:51#postPercent"]],
1620
+ [6658, ["158#default", "51#postPerc", "54#default", "68#default", "84#default", "charge:37#default", "ship:*#default", "ship:*#preMul"]],
1621
+ [6670, ["2149#default", "2175#default", "2236#default", "54#default"]],
1622
+ [6671, ["37#default"]],
1623
+ [6682, ["37#default"]],
1624
+ [6683, ["552#default"]],
1625
+ [6684, ["564#default", "76#default"]],
1626
+ [6686, ["*#default"]],
1627
+ [6690, ["37#default"]],
1628
+ [6692, ["552#default"]],
1629
+ [6693, ["564#default", "76#default"]],
1630
+ [6694, ["*#default"]],
1631
+ [6727, ["2044#default"]],
1632
+ [6730, ["37#postMul", "552#default"]],
1633
+ [6731, ["37#postMul"]],
1634
+ [6796, ["64#postDiv"]],
1635
+ [6797, ["64#postDiv"]],
1636
+ [6798, ["64#postDiv"]],
1637
+ [6799, ["*#postDiv"]],
1638
+ [6801, ["20#postDiv"]],
1639
+ [6877, ["600#default"]],
1640
+ [7029, ["2751#default"]],
1641
+ [7077, ["64#default"]],
1642
+ [7078, ["51#default"]],
1643
+ [7098, ["564#default", "76#default"]],
1644
+ [7111, ["64#default"]],
1645
+ [7142, ["20#default", "37#default", "4#default"]],
1646
+ [7202, ["37#default"]],
1647
+ [7203, ["64#default"]],
1648
+ [7223, ["70#default"]],
1649
+ [7237, ["600#default"]],
1650
+ [8033, ["54#default"]],
1651
+ [8057, ["51#default", "64#default"]],
1652
+ [8076, ["110#default"]],
1653
+ [8082, ["564#default"]],
1654
+ [8108, ["552#postMul"]],
1655
+ [8109, ["552#postMul"]],
1656
+ [8111, ["113#default", "1371#default", "267#default", "271#default"]],
1657
+ [8112, ["109#default", "269#default", "273#default", "564#default", "600#default"]],
1658
+ [8113, ["111#default", "268#default", "272#default", "552#postMul", "68#postMul", "84#postMul"]],
1659
+ [8114, ["*#postMul", "110#default", "160#postMul", "270#default", "274#default", "654#postMul", "drone:64#postMul", "fighter:64#postMul", "module:64#default"]],
1660
+ [8119, ["37#default", "54#default", "64#default", "68#default"]],
1661
+ [11445, ["*#default"]],
1662
+ [11691, ["37#default"]],
1663
+ [11946, ["64#default"]],
1664
+ [11947, ["654#postMul"]],
1665
+ [11948, ["64#postMul"]],
1666
+ [11953, ["653#postMul"]],
1667
+ [12126, ["552#default"]],
1668
+ [12597, ["*#default", "564#default", "76#default"]],
1669
+ [12761, ["3154#default"]],
1670
+ [12794, ["37#postDiv"]],
1671
+ [12795, ["37#postDiv"]],
1672
+ [12796, ["281#postDiv"]],
1673
+ [12798, ["37#postDiv"]],
1674
+ [12799, ["51#postDiv"]],
1675
+ [12838, ["1371#postMul"]],
1676
+ [12839, ["1372#postMul"]]
1398
1677
  ]);
1678
+ var CONTEXT_KIND = {
1679
+ skill: ["skill", "character"],
1680
+ ship: ["ship"],
1681
+ implant: ["implant"],
1682
+ booster: ["booster"],
1683
+ module: ["module"],
1684
+ drone: ["drone"],
1685
+ fighter: ["fighter"],
1686
+ subsystem: ["subsystem"],
1687
+ mode: ["mode"]
1688
+ };
1689
+ function stackPenaltyGroup(effectID, attributeID, receiver, sourceKind) {
1690
+ const keys = STACKING_PENALISED.get(effectID);
1691
+ if (!keys) return null;
1692
+ const id = attributeID === void 0 ? null : String(attributeID);
1693
+ for (const key of keys) {
1694
+ const bang = key.indexOf("!");
1695
+ const exempt = bang < 0 ? [] : key.slice(bang + 1).split("+");
1696
+ const head = bang < 0 ? key : key.slice(0, bang);
1697
+ const hash = head.lastIndexOf("#");
1698
+ const group = head.slice(hash + 1);
1699
+ const target = head.slice(0, hash);
1700
+ const colon = target.indexOf(":");
1701
+ const recv = colon < 0 ? null : target.slice(0, colon);
1702
+ const attr = colon < 0 ? target : target.slice(colon + 1);
1703
+ if (recv !== null && recv !== receiver) continue;
1704
+ if (attr !== "*" && (id === null || attr !== id)) continue;
1705
+ if (exempt.some((c) => (CONTEXT_KIND[c] ?? []).includes(sourceKind))) return null;
1706
+ return group;
1707
+ }
1708
+ return null;
1709
+ }
1399
1710
 
1400
1711
  // src/modifierEngine.ts
1401
1712
  var NO_PENALTY_KINDS = /* @__PURE__ */ new Set([
1402
1713
  "skill",
1403
- "ship",
1404
- "mode",
1405
- "subsystem",
1406
- "character"
1714
+ "character",
1715
+ "implant",
1716
+ "booster"
1407
1717
  ]);
1408
1718
  function scaleForPipeline(rawValue, _unitID, op) {
1409
1719
  if (op === "PostPercent") return rawValue / 100;
1410
1720
  return rawValue;
1411
1721
  }
1412
- function applySourceItem(source, ctx, dataset) {
1722
+ function isPeerModifier(source, mi, ctx, effectID) {
1723
+ for (const t of ctx.targetsForModifier(mi, source, effectID)) {
1724
+ if (t.kind === "implant" || t.kind === "booster") return true;
1725
+ }
1726
+ return false;
1727
+ }
1728
+ function applySourceItem(source, ctx, dataset, pass = "all") {
1413
1729
  const isLocalModule = source.kind === "module";
1414
1730
  const selfMods = [];
1415
1731
  const outMods = [];
@@ -1417,10 +1733,15 @@ function applySourceItem(source, ctx, dataset) {
1417
1733
  if (LEGACY_HANDLED_EFFECT_IDS.has(eid)) continue;
1418
1734
  if (SEC_STATUS_SCALED_EFFECT_IDS.has(eid)) continue;
1419
1735
  if (isLocalModule && ctx.stoppedLocalEffectIDs.has(eid)) continue;
1736
+ if (source.kind === "booster" && BOOSTER_SIDE_EFFECT_IDS.has(eid) && !source.activeSideEffects.has(eid)) continue;
1420
1737
  const effect = dataset.effects.get(eid);
1421
1738
  if (!effect) continue;
1422
1739
  if (!source.appliesAtState(effect)) continue;
1423
1740
  for (const mi of effect.modifierInfo) {
1741
+ if (pass !== "all") {
1742
+ const peer = isPeerModifier(source, mi, ctx, effect.id);
1743
+ if (pass === "peer" ? !peer : peer) continue;
1744
+ }
1424
1745
  (mi.domain === "itemID" ? selfMods : outMods).push({ effect, mi });
1425
1746
  }
1426
1747
  }
@@ -1463,7 +1784,7 @@ function applyLegacyFighterProjection(fighter, ctx) {
1463
1784
  sourceID: fighter.id,
1464
1785
  operation: "PostPercent",
1465
1786
  value: pen / 100,
1466
- stackingGroup: `attr:${ATTR_MAX_VEL}`
1787
+ stackingGroup: `default:${ATTR_MAX_VEL}`
1467
1788
  });
1468
1789
  }
1469
1790
  }
@@ -1489,9 +1810,8 @@ function applyOneModifier(source, effect, mi, ctx, dataset) {
1489
1810
  if (mi.modifiedAttributeID === void 0) return;
1490
1811
  const computed = computeModifierValue(source, mi, ctx, dataset, op);
1491
1812
  if (computed === null) return;
1492
- const targets = ctx.targetsForModifier(mi, source);
1813
+ const targets = ctx.targetsForModifier(mi, source, effect.id);
1493
1814
  if (targets.length === 0) return;
1494
- const stackingGroup = computeStackingGroup(source, mi, dataset, effect);
1495
1815
  const isMul = op === "PreMul" || op === "PostMul" || op === "PreDiv" || op === "PostDiv";
1496
1816
  const value = isMul && computed.scaled ? 1 + computed.value : computed.value;
1497
1817
  const sdeDefault = isMul ? dataset.attributes.get(mi.modifiedAttributeID)?.defaultValue ?? 0 : 0;
@@ -1501,7 +1821,7 @@ function applyOneModifier(source, effect, mi, ctx, dataset) {
1501
1821
  sourceID: source.id,
1502
1822
  operation: op,
1503
1823
  value,
1504
- stackingGroup,
1824
+ stackingGroup: computeStackingGroup(source, mi, dataset, effect, target, ctx),
1505
1825
  resistanceAttributeID: effect.resistanceAttributeID
1506
1826
  };
1507
1827
  target.addAffliction(mi.modifiedAttributeID, affliction, sdeDefault);
@@ -2008,8 +2328,17 @@ var SHIP_BONUS_SCALING_SKILL = /* @__PURE__ */ new Map([
2008
2328
  // Notable: Exhumer/Barge shield+armor resist role bonuses (Hulk/Skiff/
2009
2329
  // Mackinaw shield resist 4 %→20 %), Bhaalgorn drone+laser (492), industrial
2010
2330
  // command (Orca/Rorqual), Marauder/pirate/expedition hull bonuses.
2331
+ // NB: attr 310 (`cpuNeedBonus`) is deliberately ABSENT. The auto-derivation
2332
+ // proposed it because the Weapon Upgrades SKILL scales its own attr 310 per
2333
+ // level (pyfa Effect3519 — a skill source, handled by the skill path), but
2334
+ // no SHIP-side effect does: pyfa's Effect5901 (`roleBonusBulkheadCPU`,
2335
+ // freighters + jump freighters) takes `ship.getModifiedItemAttr('cpuNeedBonus')`
2336
+ // verbatim, and every one of the 18 hulls carrying the attribute holds a
2337
+ // FULL-value role bonus (-99 % / -100 %), never a per-level one. Scaling it
2338
+ // by Electronics Upgrades V turned an Obelisk's -100 % bulkhead CPU into
2339
+ // -500 %, so three Reinforced Bulkheads reported -480 tf of CPU used
2340
+ // instead of 0. Caught on real published freighter fits.
2011
2341
  [66, 89611],
2012
- [310, 3432],
2013
2342
  [349, 19760],
2014
2343
  [492, 3339],
2015
2344
  [1296, 21610],
@@ -2177,6 +2506,9 @@ function computeModifierValue(source, mi, ctx, dataset, op) {
2177
2506
  if (source.kind === "ship") {
2178
2507
  return { value: baseValue, scaled: false };
2179
2508
  }
2509
+ if (source.kind === "implant" || source.kind === "booster") {
2510
+ return { value: baseValue, scaled: false };
2511
+ }
2180
2512
  if (itemRequiresSkill(source, skillID)) {
2181
2513
  return { value: baseValue, scaled: false };
2182
2514
  }
@@ -2184,19 +2516,42 @@ function computeModifierValue(source, mi, ctx, dataset, op) {
2184
2516
  }
2185
2517
  return { value: baseValue, scaled: false };
2186
2518
  }
2187
- function computeStackingGroup(source, mi, dataset, effect) {
2188
- if (NO_PENALTY_KINDS.has(source.kind)) return null;
2189
- if (mi.modifiedAttributeID !== void 0) {
2190
- const attr = dataset.attributes.get(mi.modifiedAttributeID);
2191
- if (attr?.stackable) return null;
2192
- }
2193
- const group = STACKING_PENALTY_GROUPS.get(effect.id);
2194
- if (group !== void 0 && group !== "default" && CUSTOM_STACK_GROUPS_HONOURED.has(group)) {
2195
- return `${group}:${mi.modifiedAttributeID}`;
2519
+ function penaltyReceiver(target, source, ctx) {
2520
+ if (target === ctx.ship) return "ship";
2521
+ if (target === source) return "self";
2522
+ switch (target.kind) {
2523
+ case "charge":
2524
+ return "charge";
2525
+ case "drone":
2526
+ return "drone";
2527
+ case "fighter":
2528
+ return "fighter";
2529
+ case "character":
2530
+ return "character";
2531
+ case "module":
2532
+ case "subsystem":
2533
+ return "module";
2534
+ default:
2535
+ return "self";
2196
2536
  }
2197
- return `attr:${mi.modifiedAttributeID}`;
2198
2537
  }
2199
- var CUSTOM_STACK_GROUPS_HONOURED = /* @__PURE__ */ new Set([
2538
+ function computeStackingGroup(source, mi, dataset, effect, target, ctx) {
2539
+ if (NO_PENALTY_KINDS.has(source.kind)) return null;
2540
+ const group = stackPenaltyGroup(
2541
+ effect.id,
2542
+ mi.modifiedAttributeID,
2543
+ penaltyReceiver(target, source, ctx),
2544
+ source.kind
2545
+ );
2546
+ if (group === null) return null;
2547
+ if (!HONOURED_PENALTY_GROUPS.has(group)) return `default:${mi.modifiedAttributeID}`;
2548
+ return `${group}:${mi.modifiedAttributeID}`;
2549
+ }
2550
+ var HONOURED_PENALTY_GROUPS = /* @__PURE__ */ new Set([
2551
+ "postMul",
2552
+ "postDiv",
2553
+ "postPerc",
2554
+ "postPercent",
2200
2555
  "cloakingScanResolutionMultiplier"
2201
2556
  ]);
2202
2557
  function mapSourceKind(kind) {
@@ -2260,7 +2615,7 @@ function applySkills(ctx, dataset) {
2260
2615
  const sourceAttrMeta = dataset.attributes.get(mi.modifyingAttributeID);
2261
2616
  const baseVal = scaleForPipeline(rawBase, sourceAttrMeta?.unitID, op);
2262
2617
  const value = isMul ? 1 + baseVal * level : baseVal * level;
2263
- const stackingGroup = NO_PENALTY_KINDS.has("character") ? null : `attr:${mi.modifiedAttributeID}`;
2618
+ const stackingGroup = NO_PENALTY_KINDS.has("character") ? null : `default:${mi.modifiedAttributeID}`;
2264
2619
  const targets = resolveSkillTargets(mi, ctx);
2265
2620
  if (targets.length === 0) continue;
2266
2621
  const affliction = {
@@ -2472,7 +2827,7 @@ function applyLegacyPropMods(ctx) {
2472
2827
  sourceID: mod.id,
2473
2828
  operation: "PostPercent",
2474
2829
  value: sigBonus / 100,
2475
- stackingGroup: `attr:${ATTR_SIGNATURE_RADIUS}`
2830
+ stackingGroup: `default:${ATTR_SIGNATURE_RADIUS}`
2476
2831
  });
2477
2832
  }
2478
2833
  }
@@ -2480,13 +2835,13 @@ function applyLegacyPropMods(ctx) {
2480
2835
  var ENTOSIS_LINK_EFFECT_ID = 6063;
2481
2836
  function applyLegacyEntosisLink(ctx) {
2482
2837
  for (const mod of ctx.modules) {
2483
- if (mod.state === "OFFLINE") continue;
2838
+ if (mod.state !== "ACTIVE" && mod.state !== "OVERLOAD") continue;
2484
2839
  if (!mod.effectIDs.has(ENTOSIS_LINK_EFFECT_ID)) continue;
2485
2840
  const sensorPairs = [
2486
- [1027, ATTR_SCAN_RADAR],
2841
+ [1027, ATTR_SCAN_GRAVIMETRIC],
2487
2842
  [1028, ATTR_SCAN_LADAR],
2488
2843
  [1029, ATTR_SCAN_MAGNETOMETRIC],
2489
- [1030, ATTR_SCAN_GRAVIMETRIC]
2844
+ [1030, ATTR_SCAN_RADAR]
2490
2845
  ];
2491
2846
  for (const [srcAttr, tgtAttr] of sensorPairs) {
2492
2847
  const bonusPct = mod.getBase(srcAttr) ?? 0;
@@ -2520,7 +2875,7 @@ function applyLegacyCapitalEhe(ctx) {
2520
2875
  sourceID: mod.id,
2521
2876
  operation: "PostMul",
2522
2877
  value: factor,
2523
- stackingGroup: `attr:${tgtAttr}`
2878
+ stackingGroup: `default:${tgtAttr}`
2524
2879
  });
2525
2880
  }
2526
2881
  }
@@ -2599,13 +2954,13 @@ function applyLegacyMjdSigBloom(ctx) {
2599
2954
  if (!isMjd) continue;
2600
2955
  const bonusPct = mod.getFinal(ATTR_SIGNATURE_RADIUS_BONUS_PERCENT, 0);
2601
2956
  if (bonusPct === 0) continue;
2957
+ const penalised = [...LEGACY_MJD_EFFECT_IDS].some((eid) => mod.effectIDs.has(eid) && stackPenaltyGroup(eid, ATTR_SIGNATURE_RADIUS, "ship", "module") !== null);
2602
2958
  ctx.ship.addAffliction(ATTR_SIGNATURE_RADIUS, {
2603
2959
  sourceKind: "module",
2604
2960
  sourceID: mod.id,
2605
2961
  operation: "PostPercent",
2606
2962
  value: bonusPct / 100,
2607
- stackingGroup: `attr:${ATTR_SIGNATURE_RADIUS}`
2608
- // stack-penalised with MWD bloom
2963
+ stackingGroup: penalised ? `default:${ATTR_SIGNATURE_RADIUS}` : null
2609
2964
  });
2610
2965
  }
2611
2966
  }
@@ -3008,7 +3363,7 @@ function applyLegacyBastion(ctx) {
3008
3363
  sourceID,
3009
3364
  operation: "PreMul",
3010
3365
  value: factor,
3011
- stackingGroup: HULL_RES_ATTRS.has(tgt) ? null : `attr:${tgt}`
3366
+ stackingGroup: HULL_RES_ATTRS.has(tgt) ? null : `default:${tgt}`
3012
3367
  });
3013
3368
  }
3014
3369
  const maxRangeBonus = mod.getFinal(ATTR_MAX_RANGE_BONUS, 0);
@@ -3023,14 +3378,14 @@ function applyLegacyBastion(ctx) {
3023
3378
  sourceID,
3024
3379
  operation: "PostPercent",
3025
3380
  value: maxRangeBonus / 100,
3026
- stackingGroup: `attr:${ATTR_MAX_RANGE}`
3381
+ stackingGroup: `default:${ATTR_MAX_RANGE}`
3027
3382
  });
3028
3383
  if (turretRofBonus !== 0) target.addAffliction(ATTR_SPEED, {
3029
3384
  sourceKind: "module",
3030
3385
  sourceID,
3031
3386
  operation: "PostPercent",
3032
3387
  value: turretRofBonus / 100,
3033
- stackingGroup: `attr:${ATTR_SPEED}`
3388
+ stackingGroup: `default:${ATTR_SPEED}`
3034
3389
  });
3035
3390
  }
3036
3391
  if (matchTurretFalloff(target) && falloffBonusV !== 0) {
@@ -3039,7 +3394,7 @@ function applyLegacyBastion(ctx) {
3039
3394
  sourceID,
3040
3395
  operation: "PostPercent",
3041
3396
  value: falloffBonusV / 100,
3042
- stackingGroup: `attr:${ATTR_FALLOFF}`
3397
+ stackingGroup: `default:${ATTR_FALLOFF}`
3043
3398
  });
3044
3399
  }
3045
3400
  }
@@ -3054,7 +3409,7 @@ function applyLegacyBastion(ctx) {
3054
3409
  sourceID,
3055
3410
  operation: "PostPercent",
3056
3411
  value: missileVelocityBonus / 100,
3057
- stackingGroup: `attr:${ATTR_MAX_VELOCITY}`
3412
+ stackingGroup: `default:${ATTR_MAX_VELOCITY}`
3058
3413
  });
3059
3414
  }
3060
3415
  if (matchMissileLauncherROF(target) && missileRofBonus !== 0) {
@@ -3063,7 +3418,7 @@ function applyLegacyBastion(ctx) {
3063
3418
  sourceID,
3064
3419
  operation: "PostPercent",
3065
3420
  value: missileRofBonus / 100,
3066
- stackingGroup: `attr:${ATTR_SPEED}`
3421
+ stackingGroup: `default:${ATTR_SPEED}`
3067
3422
  });
3068
3423
  }
3069
3424
  }
@@ -3079,7 +3434,7 @@ function applyLegacyBastion(ctx) {
3079
3434
  sourceID,
3080
3435
  operation: "PostPercent",
3081
3436
  value: armorRepBonus / 100,
3082
- stackingGroup: `attr:${ATTR_ARMOR_REP_AMOUNT}`
3437
+ stackingGroup: `default:${ATTR_ARMOR_REP_AMOUNT}`
3083
3438
  });
3084
3439
  }
3085
3440
  if (isShield && shieldBoostBonus !== 0) {
@@ -3088,7 +3443,7 @@ function applyLegacyBastion(ctx) {
3088
3443
  sourceID,
3089
3444
  operation: "PostPercent",
3090
3445
  value: shieldBoostBonus / 100,
3091
- stackingGroup: `attr:${ATTR_SHIELD_REP_AMOUNT}`
3446
+ stackingGroup: `default:${ATTR_SHIELD_REP_AMOUNT}`
3092
3447
  });
3093
3448
  }
3094
3449
  if ((isArmor || isShield) && repairDurationBonus !== 0) {
@@ -3132,7 +3487,7 @@ function applyLegacyBastion(ctx) {
3132
3487
  sourceID,
3133
3488
  operation: "PostPercent",
3134
3489
  value: v / 100,
3135
- stackingGroup: `attr:${tgt}`
3490
+ stackingGroup: `default:${tgt}`
3136
3491
  });
3137
3492
  }
3138
3493
  const resistPairs = [
@@ -3231,7 +3586,7 @@ function applyLegacyDoomsdaySelfEffects(ctx) {
3231
3586
  sourceID: mod.id,
3232
3587
  operation: "PostPercent",
3233
3588
  value: speedFactor / 100,
3234
- stackingGroup: `attr:${ATTR_MAX_VELOCITY}`
3589
+ stackingGroup: `default:${ATTR_MAX_VELOCITY}`
3235
3590
  });
3236
3591
  }
3237
3592
  const siegeStatus = mod.getFinal(ATTR_SIEGE_WARP_STATUS, 0);
@@ -3302,7 +3657,7 @@ function applyCommandBuff(buffID, value, sourceMod, ctx) {
3302
3657
  sourceID: sourceMod.id,
3303
3658
  operation,
3304
3659
  value: scaledValue,
3305
- stackingGroup: `attr:${attrID}`
3660
+ stackingGroup: `default:${attrID}`
3306
3661
  });
3307
3662
  for (const mi of dbuff.itemModifiers) {
3308
3663
  ctx.ship.addAffliction(mi.dogmaAttributeID, affliction(op, mi.dogmaAttributeID));
@@ -3648,7 +4003,7 @@ function applyLegacyFighterAbilities(ctx) {
3648
4003
  sourceID: fighter.id,
3649
4004
  operation: "PostPercent",
3650
4005
  value: speedPct / 100,
3651
- stackingGroup: `attr:${ATTR_MAX_VELOCITY}`
4006
+ stackingGroup: `default:${ATTR_MAX_VELOCITY}`
3652
4007
  });
3653
4008
  }
3654
4009
  }
@@ -3660,7 +4015,7 @@ function applyLegacyFighterAbilities(ctx) {
3660
4015
  sourceID: fighter.id,
3661
4016
  operation: "PostPercent",
3662
4017
  value: speedPct / 100,
3663
- stackingGroup: `attr:${ATTR_MAX_VELOCITY}`
4018
+ stackingGroup: `default:${ATTR_MAX_VELOCITY}`
3664
4019
  });
3665
4020
  }
3666
4021
  const sigPct = fighter.getFinal(ATTR_FIGHTER_MWD_SIG_BONUS, 0);
@@ -3670,7 +4025,7 @@ function applyLegacyFighterAbilities(ctx) {
3670
4025
  sourceID: fighter.id,
3671
4026
  operation: "PostPercent",
3672
4027
  value: sigPct / 100,
3673
- stackingGroup: `attr:${ATTR_SIGNATURE_RADIUS}`
4028
+ stackingGroup: `default:${ATTR_SIGNATURE_RADIUS}`
3674
4029
  });
3675
4030
  }
3676
4031
  }
@@ -3682,7 +4037,7 @@ function applyLegacyFighterAbilities(ctx) {
3682
4037
  sourceID: fighter.id,
3683
4038
  operation: "PostPercent",
3684
4039
  value: speedPct / 100,
3685
- stackingGroup: `attr:${ATTR_MAX_VELOCITY}`
4040
+ stackingGroup: `default:${ATTR_MAX_VELOCITY}`
3686
4041
  });
3687
4042
  }
3688
4043
  const sigPct = fighter.getFinal(ATTR_FIGHTER_EVASIVE_SIG_BONUS, 0);
@@ -3692,7 +4047,7 @@ function applyLegacyFighterAbilities(ctx) {
3692
4047
  sourceID: fighter.id,
3693
4048
  operation: "PostPercent",
3694
4049
  value: sigPct / 100,
3695
- stackingGroup: `attr:${ATTR_SIGNATURE_RADIUS}`
4050
+ stackingGroup: `default:${ATTR_SIGNATURE_RADIUS}`
3696
4051
  });
3697
4052
  }
3698
4053
  const resonances = [
@@ -3709,7 +4064,7 @@ function applyLegacyFighterAbilities(ctx) {
3709
4064
  sourceID: fighter.id,
3710
4065
  operation: "PostMul",
3711
4066
  value: mul,
3712
- stackingGroup: `attr:${tgtAttr}`
4067
+ stackingGroup: `default:${tgtAttr}`
3713
4068
  });
3714
4069
  }
3715
4070
  }
@@ -3829,7 +4184,7 @@ function applyLegacyRAH(ctx, damageProfile) {
3829
4184
  sourceID: mod.id,
3830
4185
  operation: "PreMul",
3831
4186
  value: factor,
3832
- stackingGroup: `attr:${tgtAttr}`
4187
+ stackingGroup: `default:${tgtAttr}`
3833
4188
  });
3834
4189
  }
3835
4190
  }
@@ -3993,176 +4348,6 @@ function computeTotalEhp(shield, armor, hull, useProfile) {
3993
4348
  return Number.isFinite(total) ? total : Number.MAX_SAFE_INTEGER;
3994
4349
  }
3995
4350
 
3996
- // src/fitChecks.ts
3997
- function readAttr(t, id) {
3998
- return t.attributes.find((a) => a.id === id)?.v;
3999
- }
4000
- function typeFitsSlotType(t, slot) {
4001
- for (const e of t.effects) {
4002
- const mapped = SLOT_EFFECT_TO_SLOT_TYPE[e.id];
4003
- if (mapped === slot) return true;
4004
- }
4005
- return false;
4006
- }
4007
- function isTurretWeapon(t) {
4008
- return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "TURRET");
4009
- }
4010
- function isMissileLauncher(t) {
4011
- return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "MISSILE");
4012
- }
4013
- function isSmartBomb(t) {
4014
- return t.effects.some((e) => WEAPON_EFFECT_KIND[e.id] === "SMARTBOMB");
4015
- }
4016
- function shipGroupRestrictions(t) {
4017
- const out = [];
4018
- for (const id of CAN_FIT_SHIP_GROUP_ATTRS) {
4019
- const v = readAttr(t, id);
4020
- if (v != null && v > 0) out.push(Math.round(v));
4021
- }
4022
- return out;
4023
- }
4024
- function shipTypeRestrictions(t) {
4025
- const out = [];
4026
- for (const id of CAN_FIT_SHIP_TYPE_ATTRS) {
4027
- const v = readAttr(t, id);
4028
- if (v != null && v > 0) out.push(Math.round(v));
4029
- }
4030
- return out;
4031
- }
4032
- function maxGroupFittedFor(mod) {
4033
- const v = readAttr(mod, ATTR.MAX_GROUP_FITTED);
4034
- return v != null && v > 0 ? Math.round(v) : void 0;
4035
- }
4036
- function maxTypeFittedFor(mod) {
4037
- const v = readAttr(mod, ATTR.MAX_TYPE_FITTED);
4038
- return v != null && v > 0 ? Math.round(v) : void 0;
4039
- }
4040
- function freeFitGroupSlotsFor(mod, fittedModules, dataset) {
4041
- const groupCap = maxGroupFittedFor(mod);
4042
- const typeCap = maxTypeFittedFor(mod);
4043
- if (groupCap === void 0 && typeCap === void 0) return Number.POSITIVE_INFINITY;
4044
- let groupCount = 0;
4045
- let typeCount = 0;
4046
- for (const fm of fittedModules) {
4047
- if (typeCap !== void 0 && fm.typeID === mod.id) typeCount++;
4048
- if (groupCap !== void 0) {
4049
- const t = dataset.getType(fm.typeID);
4050
- if (t && t.groupID === mod.groupID) groupCount++;
4051
- }
4052
- }
4053
- let free = Number.POSITIVE_INFINITY;
4054
- if (groupCap !== void 0) free = Math.min(free, groupCap - groupCount);
4055
- if (typeCap !== void 0) free = Math.min(free, typeCap - typeCount);
4056
- return Math.max(0, free);
4057
- }
4058
- function canFitModuleOnShip(mod, ship, slot, fitContext) {
4059
- if (!typeFitsSlotType(mod, slot)) {
4060
- return { ok: false, reason: `Module does not declare a ${slot} slot effect` };
4061
- }
4062
- if (!ship) return { ok: true };
4063
- const allowedGroups = shipGroupRestrictions(mod);
4064
- const allowedTypes = shipTypeRestrictions(mod);
4065
- if (allowedGroups.length > 0 || allowedTypes.length > 0) {
4066
- const groupOk = allowedGroups.includes(ship.groupID);
4067
- const typeOk = allowedTypes.includes(ship.id);
4068
- if (!groupOk && !typeOk) {
4069
- return { ok: false, reason: "Ship hull/group not allowed by this module" };
4070
- }
4071
- }
4072
- if (slot === "RIG") {
4073
- const modSize = readAttr(mod, ATTR.RIG_SIZE);
4074
- const shipSize = readAttr(ship, ATTR.RIG_SIZE);
4075
- if (modSize != null && shipSize != null && Math.round(modSize) !== Math.round(shipSize)) {
4076
- return { ok: false, reason: "Rig size mismatch" };
4077
- }
4078
- }
4079
- if (slot === "SUBSYSTEM") {
4080
- const fitsTo = readAttr(mod, 1380);
4081
- if (fitsTo != null && fitsTo > 0 && Math.round(fitsTo) !== ship.id) {
4082
- return { ok: false, reason: "Subsystem locked to a different T3C hull" };
4083
- }
4084
- }
4085
- if (slot === "HI") {
4086
- if (isTurretWeapon(mod)) {
4087
- const turrets = readAttr(ship, ATTR.TURRET_HARDPOINTS) ?? 0;
4088
- if (turrets <= 0) {
4089
- return { ok: false, reason: "Ship has no turret hardpoints" };
4090
- }
4091
- } else if (isMissileLauncher(mod)) {
4092
- const launchers = readAttr(ship, ATTR.LAUNCHER_HARDPOINTS) ?? 0;
4093
- if (launchers <= 0) {
4094
- return { ok: false, reason: "Ship has no launcher hardpoints" };
4095
- }
4096
- }
4097
- }
4098
- if (fitContext) {
4099
- const free = freeFitGroupSlotsFor(mod, fitContext.fittedModules, fitContext.dataset);
4100
- if (free <= 0) {
4101
- const cap = maxTypeFittedFor(mod) ?? maxGroupFittedFor(mod);
4102
- return { ok: false, reason: `Only ${cap} of this module type can be fitted to a ship` };
4103
- }
4104
- }
4105
- return { ok: true };
4106
- }
4107
- function chargeGroupsForModule(mod) {
4108
- const out = [];
4109
- for (const attrID of CHARGE_GROUP_ATTRS) {
4110
- const v = readAttr(mod, attrID);
4111
- if (v != null && v > 0) out.push(Math.round(v));
4112
- }
4113
- return out;
4114
- }
4115
- function moduleAcceptsAnyCharge(mod) {
4116
- return chargeGroupsForModule(mod).length > 0;
4117
- }
4118
- function moduleAcceptsChargeType(mod, charge) {
4119
- const allowedGroups = chargeGroupsForModule(mod);
4120
- if (allowedGroups.length === 0) return false;
4121
- if (!allowedGroups.includes(charge.groupID)) return false;
4122
- const modSize = readAttr(mod, ATTR.CHARGE_SIZE);
4123
- const chargeSize = readAttr(charge, ATTR.CHARGE_SIZE);
4124
- if (modSize != null && chargeSize != null && Math.round(chargeSize) > Math.round(modSize)) {
4125
- return false;
4126
- }
4127
- return true;
4128
- }
4129
- var ACTIVE_EFFECT_CATEGORIES = /* @__PURE__ */ new Set([1, 2, 3]);
4130
- function isActivatableModule(mod, effects) {
4131
- for (const e of mod.effects) {
4132
- const eff = effects.get(e.id);
4133
- if (eff && eff.effectCategoryID !== void 0 && ACTIVE_EFFECT_CATEGORIES.has(eff.effectCategoryID)) {
4134
- return true;
4135
- }
4136
- }
4137
- return false;
4138
- }
4139
- var DEFAULT_OFFLINE_ACTIVATION_GROUPS = /* @__PURE__ */ new Set([
4140
- 330,
4141
- 4117
4142
- ]);
4143
- function defaultStateForModule(mod, effects) {
4144
- if (DEFAULT_OFFLINE_ACTIVATION_GROUPS.has(mod.groupID)) return "ONLINE";
4145
- return isActivatableModule(mod, effects) ? "ACTIVE" : "ONLINE";
4146
- }
4147
- function freeHardpointsFor(mod, ship, fittedHiModules, dataset) {
4148
- const turretCap = readAttr(ship, ATTR.TURRET_HARDPOINTS) ?? 0;
4149
- const launcherCap = readAttr(ship, ATTR.LAUNCHER_HARDPOINTS) ?? 0;
4150
- let turretUsed = 0;
4151
- let launcherUsed = 0;
4152
- for (const m of fittedHiModules) {
4153
- const t = dataset.getType(m.typeID);
4154
- if (!t) continue;
4155
- if (isTurretWeapon(t)) turretUsed++;
4156
- else if (isMissileLauncher(t)) launcherUsed++;
4157
- }
4158
- let hardpointFree;
4159
- if (isTurretWeapon(mod)) hardpointFree = Math.max(0, turretCap - turretUsed);
4160
- else if (isMissileLauncher(mod)) hardpointFree = Math.max(0, launcherCap - launcherUsed);
4161
- else hardpointFree = Number.POSITIVE_INFINITY;
4162
- const groupFree = freeFitGroupSlotsFor(mod, fittedHiModules, dataset);
4163
- return Math.min(hardpointFree, groupFree);
4164
- }
4165
-
4166
4351
  // src/derived/capacitor.ts
4167
4352
  function computeCapacitor(ctx, dataset) {
4168
4353
  const ship = ctx.ship;
@@ -5199,22 +5384,46 @@ function computeFit(fit, dataset, opts) {
5199
5384
  const t = dataset.getType(ff.typeID);
5200
5385
  return t ? [makeFighterState(ff, t)] : [];
5201
5386
  });
5202
- const implants = fit.implants.flatMap((fi) => {
5203
- const t = dataset.getType(fi.typeID);
5204
- return t ? [makeImplantState(fi, t)] : [];
5205
- });
5206
- const boosters = fit.boosters.flatMap((fb) => {
5207
- const t = dataset.getType(fb.typeID);
5208
- return t ? [makeBoosterState(fb, t)] : [];
5209
- });
5387
+ const implants = dropSlotCollisions(
5388
+ fit.implants.flatMap((fi) => {
5389
+ const t = dataset.getType(fi.typeID);
5390
+ return t ? [{ slot: t.attributes.find((a) => a.id === ATTR_IMPLANTNESS)?.v ?? fi.slot, item: makeImplantState(fi, t) }] : [];
5391
+ })
5392
+ );
5393
+ const boosters = dropSlotCollisions(
5394
+ fit.boosters.flatMap((fb) => {
5395
+ const t = dataset.getType(fb.typeID);
5396
+ return t ? [{ slot: t.attributes.find((a) => a.id === ATTR_BOOSTERNESS)?.v ?? fb.slot, item: makeBoosterState(fb, t) }] : [];
5397
+ })
5398
+ );
5210
5399
  const subsystems = fit.subsystems.flatMap((fs2) => {
5211
5400
  const t = dataset.getType(fs2.typeID);
5212
5401
  return t ? [makeSubsystemState(fs2, t)] : [];
5213
5402
  });
5214
5403
  let mode;
5215
- if (fit.modeTypeID !== void 0) {
5216
- const t = dataset.getType(fit.modeTypeID);
5217
- if (t) mode = makeModeState(fit.modeTypeID, t);
5404
+ const modeTypeID = fit.modeTypeID ?? defaultModeTypeIDFor(shipType, dataset);
5405
+ if (modeTypeID !== void 0) {
5406
+ const t = dataset.getType(modeTypeID);
5407
+ if (t) mode = makeModeState(modeTypeID, t);
5408
+ }
5409
+ ship.attr(
5410
+ ATTR.DRONE_CONTROL_RANGE,
5411
+ dataset.attributes.get(ATTR.DRONE_CONTROL_RANGE)?.defaultValue ?? 2e4
5412
+ );
5413
+ for (const it of [
5414
+ ship,
5415
+ character,
5416
+ mode,
5417
+ ...modules,
5418
+ ...drones,
5419
+ ...fighters,
5420
+ ...implants,
5421
+ ...boosters,
5422
+ ...subsystems
5423
+ ]) {
5424
+ if (!it) continue;
5425
+ it.attrSchema = dataset.attributes;
5426
+ if (it.charge) it.charge.attrSchema = dataset.attributes;
5218
5427
  }
5219
5428
  const skillLevels = /* @__PURE__ */ new Map();
5220
5429
  for (const [k, v] of Object.entries(opts.skillProfile.skills)) {
@@ -5260,8 +5469,9 @@ function computeFit(fit, dataset, opts) {
5260
5469
  for (const m of modules) applySourceItem(m, ctx, dataset);
5261
5470
  for (const d of drones) applySourceItem(d, ctx, dataset);
5262
5471
  for (const f of fighters) applySourceItem(f, ctx, dataset);
5263
- for (const i of implants) applySourceItem(i, ctx, dataset);
5264
- for (const b of boosters) applySourceItem(b, ctx, dataset);
5472
+ const charSide = [...implants, ...boosters];
5473
+ for (const it of charSide) applySourceItem(it, ctx, dataset, "peer");
5474
+ for (const it of charSide) applySourceItem(it, ctx, dataset, "outgoing");
5265
5475
  if (mode) applySourceItem(mode, ctx, dataset);
5266
5476
  applyLegacyOverload(ctx);
5267
5477
  applyLegacyPropMods(ctx);
@@ -5468,6 +5678,7 @@ function deriveStats(ctx, dataset, damageProfile, fit, projectionReports) {
5468
5678
  const cap = computeCapacitor(ctx, dataset);
5469
5679
  const tank = computeTank(ctx);
5470
5680
  const offense = computeOffense(ctx, dataset, fit);
5681
+ const sensor = pickSensorStrength(ship);
5471
5682
  return {
5472
5683
  fitting: {
5473
5684
  cpuUsed,
@@ -5556,8 +5767,8 @@ function deriveStats(ctx, dataset, damageProfile, fit, projectionReports) {
5556
5767
  )),
5557
5768
  signatureRadius: ship.getFinal(ATTR.SIGNATURE_RADIUS, 0),
5558
5769
  scanResolution: ship.getFinal(ATTR.SCAN_RESOLUTION, 0),
5559
- sensorStrength: pickSensorStrength(ship).value,
5560
- sensorType: pickSensorStrength(ship).type
5770
+ sensorStrength: sensor.value,
5771
+ sensorType: sensor.type
5561
5772
  },
5562
5773
  drones: {
5563
5774
  bayUsed: droneBayUsed,
@@ -5568,13 +5779,12 @@ function deriveStats(ctx, dataset, damageProfile, fit, projectionReports) {
5568
5779
  // ItemState per FitDrone stack, so filtering by state counted five
5569
5780
  // active Ogre IIs as 1 — and this is the number bandwidth gates.
5570
5781
  active: fit.drones.reduce((n, d) => n + d.countActive, 0),
5571
- // Drone control range is the ship base (default 20 km, attr 458)
5572
- // PLUS the character's accumulated skill bonus on the same attr.
5573
- // Drone Avionics V + Advanced Drone Avionics V each ModAdd to
5574
- // char.attr_458 via effect 504 (`domain: charID`). Pyfa reads
5575
- // both and sums; ship.attr_458 alone misses 40 km of skills at
5576
- // All-V → produces 20 km instead of the in-game 60 km.
5577
- controlRange: ship.getFinal(ATTR.DRONE_CONTROL_RANGE, 2e4) + ctx.character.getFinal(ATTR.DRONE_CONTROL_RANGE, 0)
5782
+ // ONE accumulator, on the ship (attr 458, SDE default 20 km) — the
5783
+ // charID-domain modifiers that carry the skills and the hull bonus
5784
+ // are re-routed there, see FitContext.targetsForModifier. Summing a
5785
+ // ship half and a character half instead let a percentage bonus
5786
+ // apply to only one of them.
5787
+ controlRange: ship.getFinal(ATTR.DRONE_CONTROL_RANGE, 2e4)
5578
5788
  },
5579
5789
  projected: projectionReports,
5580
5790
  structure: computeStructureMeta(ctx),
@@ -5601,18 +5811,36 @@ function computeAlignTime(ship) {
5601
5811
  if (!mass || !agility) return 0;
5602
5812
  return Math.log(4) * mass * agility / 1e6;
5603
5813
  }
5814
+ var ATTR_IMPLANTNESS = 331;
5815
+ var ATTR_BOOSTERNESS = 1087;
5816
+ function dropSlotCollisions(entries) {
5817
+ const taken = /* @__PURE__ */ new Set();
5818
+ const out = [];
5819
+ for (const e of entries) {
5820
+ if (taken.has(e.slot)) continue;
5821
+ taken.add(e.slot);
5822
+ out.push(e.item);
5823
+ }
5824
+ return out;
5825
+ }
5604
5826
  function pickSensorStrength(ship) {
5605
5827
  const candidates = [
5606
- { attr: ATTR.SCAN_RADAR_STRENGTH, type: "radar" },
5607
- { attr: ATTR.SCAN_LADAR_STRENGTH, type: "ladar" },
5608
5828
  { attr: ATTR.SCAN_MAGNETOMETRIC_STRENGTH, type: "magnetometric" },
5829
+ { attr: ATTR.SCAN_LADAR_STRENGTH, type: "ladar" },
5830
+ { attr: ATTR.SCAN_RADAR_STRENGTH, type: "radar" },
5609
5831
  { attr: ATTR.SCAN_GRAVIMETRIC_STRENGTH, type: "gravimetric" }
5610
5832
  ];
5833
+ let best = -1;
5834
+ let type = "unknown";
5611
5835
  for (const c of candidates) {
5612
5836
  const v = ship.getFinal(c.attr, 0);
5613
- if (v > 0) return { value: v, type: c.type };
5837
+ if (v > best) {
5838
+ best = v;
5839
+ type = c.type;
5840
+ } else if (v === best) type = "multispectral";
5614
5841
  }
5615
- return { value: 0, type: "unknown" };
5842
+ if (best <= 0) return { value: Math.max(best, 0), type: best === 0 ? type : "unknown" };
5843
+ return { value: best, type };
5616
5844
  }
5617
5845
 
5618
5846
  // src/eft/parser.ts
@@ -5715,6 +5943,20 @@ function parseEft(text, dataset) {
5715
5943
  return { fit, warnings };
5716
5944
  }
5717
5945
  function classifySection(entries, idx, _dataset) {
5946
+ let sawImplant = false;
5947
+ let allImplant = true;
5948
+ for (const e of entries) {
5949
+ const { name, quantity } = parseEntryName(e.text);
5950
+ const typeID = idx.map.get(name.toLowerCase());
5951
+ if (typeID === void 0) {
5952
+ allImplant = false;
5953
+ continue;
5954
+ }
5955
+ const t = idx.types.get(typeID);
5956
+ if (quantity === null && isFittableImplant(t)) sawImplant = true;
5957
+ else allImplant = false;
5958
+ }
5959
+ if (sawImplant && allImplant) return "implants";
5718
5960
  for (const e of entries) {
5719
5961
  const { name, quantity } = parseEntryName(e.text);
5720
5962
  const typeID = idx.map.get(name.toLowerCase());
@@ -5731,6 +5973,13 @@ function classifySection(entries, idx, _dataset) {
5731
5973
  }
5732
5974
  return "cargo";
5733
5975
  }
5976
+ var ATTR_IMPLANTNESS2 = 331;
5977
+ var ATTR_BOOSTERNESS2 = 1087;
5978
+ var attrValue = (t, id) => t.attributes?.find((a) => a.id === id)?.v;
5979
+ function isFittableImplant(t) {
5980
+ if (t.categoryID !== CATEGORY.IMPLANT) return false;
5981
+ return attrValue(t, ATTR_IMPLANTNESS2) !== void 0 || attrValue(t, ATTR_BOOSTERNESS2) !== void 0;
5982
+ }
5734
5983
  function parseEntryName(text) {
5735
5984
  const m = QUANTITY_RE.exec(text);
5736
5985
  if (m) {
@@ -5767,6 +6016,23 @@ function consumeEntry(entry, sectionKind, fit, slotCursor, idx, dataset, warning
5767
6016
  fit.drones.push(drone);
5768
6017
  return;
5769
6018
  }
6019
+ if (sectionKind === "implants" && quantity === null && isFittableImplant(type)) {
6020
+ const implantSlot = attrValue(type, ATTR_IMPLANTNESS2);
6021
+ if (implantSlot !== void 0) {
6022
+ fit.implants.push({ id: tempId(), typeID, slot: implantSlot });
6023
+ } else {
6024
+ fit.boosters.push({
6025
+ id: tempId(),
6026
+ typeID,
6027
+ slot: attrValue(type, ATTR_BOOSTERNESS2) ?? 1,
6028
+ // Side effects are opt-in and an EFT cannot express them, so an
6029
+ // imported booster arrives with its drawbacks off — the same
6030
+ // default pyfa gives it.
6031
+ activeSideEffects: []
6032
+ });
6033
+ }
6034
+ return;
6035
+ }
5770
6036
  if (sectionKind === "cargo" || quantity !== null) {
5771
6037
  const cargo = { id: tempId(), typeID, count: quantity ?? 1 };
5772
6038
  fit.cargo.push(cargo);
@@ -6132,7 +6398,7 @@ var SUBSYSTEM_RANK_BY_TYPE_ID = /* @__PURE__ */ new Map([
6132
6398
  [45621, 3]
6133
6399
  // Loki Propulsion - Wake Limiter
6134
6400
  ]);
6135
- function attrValue(t, id) {
6401
+ function attrValue2(t, id) {
6136
6402
  for (const a of t.attributes) if (a.id === id) return a.v;
6137
6403
  return void 0;
6138
6404
  }
@@ -6146,7 +6412,7 @@ function computeT3CVariantCode(shipTypeID, subsystems, dataset) {
6146
6412
  for (const fs2 of subsystems) {
6147
6413
  const t = subsBucket.get(fs2.typeID);
6148
6414
  if (!t) return null;
6149
- if (attrValue(t, PARENT_SHIP_ATTR) !== shipTypeID) return null;
6415
+ if (attrValue2(t, PARENT_SHIP_ATTR) !== shipTypeID) return null;
6150
6416
  fittedByGroup.set(t.groupID, fs2.typeID);
6151
6417
  }
6152
6418
  let code = "";
@@ -6380,6 +6646,7 @@ async function computeFromEft(eft, options = {}) {
6380
6646
 
6381
6647
  exports.ACTIVATION_EFFECT_ID = ACTIVATION_EFFECT_ID;
6382
6648
  exports.ATTR = ATTR;
6649
+ exports.BOOSTER_SIDE_EFFECT_IDS = BOOSTER_SIDE_EFFECT_IDS;
6383
6650
  exports.BUNDLED_DATA_DIR = BUNDLED_DATA_DIR;
6384
6651
  exports.CATEGORY = CATEGORY;
6385
6652
  exports.DAMAGE_PROFILE_PRESETS = DAMAGE_PROFILE_PRESETS;
@@ -6420,6 +6687,7 @@ exports.computeStructureMeta = computeStructureMeta;
6420
6687
  exports.computeT3CVariantCode = computeT3CVariantCode;
6421
6688
  exports.computeTank = computeTank;
6422
6689
  exports.computeTotalEhp = computeTotalEhp;
6690
+ exports.defaultModeTypeIDFor = defaultModeTypeIDFor;
6423
6691
  exports.defaultStateForModule = defaultStateForModule;
6424
6692
  exports.disintegratorCyclesToFullSpool = disintegratorCyclesToFullSpool;
6425
6693
  exports.disintegratorSpoolBonus = disintegratorSpoolBonus;