eve-fit-engine 0.1.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 (37) hide show
  1. package/LICENSE +674 -0
  2. package/NOTICE +31 -0
  3. package/README.md +95 -0
  4. package/data/.build +1 -0
  5. package/data/SDE-LICENSE.md +17 -0
  6. package/data/manifest.json +1 -0
  7. package/data/v3e885b627373/attributes.json +1 -0
  8. package/data/v3e885b627373/categories.json +1 -0
  9. package/data/v3e885b627373/clone-grades.json +1 -0
  10. package/data/v3e885b627373/dbuff-collections.json +1 -0
  11. package/data/v3e885b627373/dynamic-attributes.json +1 -0
  12. package/data/v3e885b627373/effects.json +1 -0
  13. package/data/v3e885b627373/groups.json +1 -0
  14. package/data/v3e885b627373/market-groups.json +1 -0
  15. package/data/v3e885b627373/meta-groups.json +1 -0
  16. package/data/v3e885b627373/types/charges.json +1 -0
  17. package/data/v3e885b627373/types/drones.json +1 -0
  18. package/data/v3e885b627373/types/fighters.json +1 -0
  19. package/data/v3e885b627373/types/implants.json +1 -0
  20. package/data/v3e885b627373/types/modules.json +1 -0
  21. package/data/v3e885b627373/types/mutaplasmids.json +1 -0
  22. package/data/v3e885b627373/types/ships.json +1 -0
  23. package/data/v3e885b627373/types/skills.json +1 -0
  24. package/data/v3e885b627373/types/structure-modules.json +1 -0
  25. package/data/v3e885b627373/types/structures.json +1 -0
  26. package/data/v3e885b627373/types/subsystems.json +1 -0
  27. package/data/v3e885b627373/types/system-effects.json +1 -0
  28. package/data/v3e885b627373/units.json +1 -0
  29. package/dist/index.cjs +5911 -0
  30. package/dist/index.d.cts +2196 -0
  31. package/dist/index.d.ts +2196 -0
  32. package/dist/index.js +5839 -0
  33. package/dist/node.cjs +6046 -0
  34. package/dist/node.d.cts +34 -0
  35. package/dist/node.d.ts +34 -0
  36. package/dist/node.js +5948 -0
  37. package/package.json +73 -0
@@ -0,0 +1,2196 @@
1
+ /**
2
+ * Core types for the fitting tool. Shared between:
3
+ * - the bundle (loaded into memory client-side from /public/fitting-data/)
4
+ * - the calc engine (modifier application, derived stats)
5
+ * - the UI components (slot rendering, picker, stats panels)
6
+ * - the API endpoints (fit save/load on Prisma)
7
+ *
8
+ * Naming convention: this file contains ONLY interfaces & enum-likes. No
9
+ * runtime constants — those live in `constants.ts` so types can be imported
10
+ * without pulling in code.
11
+ */
12
+ interface SdeAttribute {
13
+ id: number;
14
+ name: string;
15
+ displayName?: string;
16
+ unitID?: number;
17
+ iconID?: number;
18
+ defaultValue: number;
19
+ highIsGood: boolean;
20
+ stackable: boolean;
21
+ attributeCategoryID?: number;
22
+ dataType?: number;
23
+ }
24
+ interface SdeUnit {
25
+ id: number;
26
+ name: string;
27
+ displayName?: string;
28
+ }
29
+ interface SdeModifierInfo {
30
+ /** Domain strings as they appear in the SDE — values observed:
31
+ * `itemID` (= self), `charID` (= the character/skills), `shipID`,
32
+ * `target` and `targetID` (both → projected target), `otherID`
33
+ * (charge ↔ module), `structureID`. The legacy aliases `'self'` and
34
+ * `'char'` are kept for places that historically wrote them by hand.
35
+ * All variants are normalised by `FitContext.resolveDomain`. */
36
+ domain: 'self' | 'itemID' | 'char' | 'charID' | 'shipID' | 'target' | 'targetID' | 'otherID' | 'structureID';
37
+ func: 'ItemModifier' | 'LocationModifier' | 'LocationGroupModifier' | 'LocationRequiredSkillModifier' | 'OwnerRequiredSkillModifier' | 'EffectStopper';
38
+ modifiedAttributeID?: number;
39
+ modifyingAttributeID?: number;
40
+ operation?: number;
41
+ groupID?: number;
42
+ skillTypeID?: number;
43
+ /** Only set on `func: 'EffectStopper'` modifiers. The numeric SDE
44
+ * effect ID this modifier suppresses on the target — e.g. warp
45
+ * scrambler / disruptor entries carry `effectID: 6441` (MWD) and
46
+ * `effectID: 6442` (MJD). Read by `collectEffectStoppers()` in the
47
+ * projection pre-pass. */
48
+ effectID?: number;
49
+ }
50
+ interface SdeEffect {
51
+ id: number;
52
+ name: string;
53
+ displayName?: string;
54
+ effectCategoryID?: number;
55
+ isOffensive: boolean;
56
+ isAssistance: boolean;
57
+ isWarpSafe: boolean;
58
+ durationAttributeID?: number;
59
+ dischargeAttributeID?: number;
60
+ rangeAttributeID?: number;
61
+ falloffAttributeID?: number;
62
+ trackingSpeedAttributeID?: number;
63
+ fittingUsageChanceAttributeID?: number;
64
+ resistanceAttributeID?: number;
65
+ distribution?: number;
66
+ propulsionChance: boolean;
67
+ electronicChance: boolean;
68
+ rangeChance: boolean;
69
+ disallowAutoRepeat: boolean;
70
+ guid?: string;
71
+ modifierInfo: SdeModifierInfo[];
72
+ }
73
+ interface SdeMetaGroup {
74
+ id: number;
75
+ name?: string;
76
+ color?: {
77
+ r: number;
78
+ g: number;
79
+ b: number;
80
+ };
81
+ iconID?: number;
82
+ }
83
+ interface SdeCategory {
84
+ id: number;
85
+ name?: string;
86
+ }
87
+ interface SdeGroup {
88
+ id: number;
89
+ categoryID: number;
90
+ name?: string;
91
+ }
92
+ /** EVE in-game Market window taxonomy node. Forms a tree via
93
+ * `parentGroupID`; root nodes have it undefined. The picker uses
94
+ * this hierarchy to render Module > Capacitor > Cap Battery the
95
+ * same way the in-game Market window does, instead of the flat
96
+ * SDE category > group fallback. */
97
+ interface SdeMarketGroup {
98
+ id: number;
99
+ name?: string;
100
+ parentGroupID?: number;
101
+ iconID?: number;
102
+ /** True iff items can sit directly under this node (vs. it being
103
+ * a pure folder grouping deeper market groups). Useful for
104
+ * picking the right level to show as the "leaf" subgroup. */
105
+ hasTypes?: boolean;
106
+ }
107
+ interface SdeCloneGrade {
108
+ id: number;
109
+ name: string;
110
+ skills: Array<{
111
+ typeID: number;
112
+ level: number;
113
+ }>;
114
+ }
115
+ interface SdeDbuffCollection {
116
+ id: number;
117
+ aggregateMode: 'Minimum' | 'Maximum' | 'Sum';
118
+ operationName: string;
119
+ displayName?: string;
120
+ showOutputValueInUI?: 'ShowNormal' | 'ShowInverted' | 'Hide';
121
+ itemModifiers: Array<{
122
+ dogmaAttributeID: number;
123
+ }>;
124
+ locationModifiers: Array<{
125
+ dogmaAttributeID: number;
126
+ }>;
127
+ locationGroupModifiers: Array<{
128
+ dogmaAttributeID: number;
129
+ groupID: number;
130
+ }>;
131
+ locationRequiredSkillModifiers: Array<{
132
+ dogmaAttributeID: number;
133
+ skillID: number;
134
+ }>;
135
+ }
136
+ interface SdeDynamicAttribute {
137
+ id: number;
138
+ attributeIDs: Array<{
139
+ id: number;
140
+ min: number;
141
+ max: number;
142
+ }>;
143
+ inputOutputMapping: Array<{
144
+ applicableTypes: number[];
145
+ resultingType: number;
146
+ }>;
147
+ }
148
+ interface SdeType {
149
+ id: number;
150
+ name?: string;
151
+ groupID: number;
152
+ categoryID: number;
153
+ marketGroupID?: number;
154
+ iconID?: number;
155
+ metaGroupID?: number;
156
+ metaLevel?: number;
157
+ variationParentTypeID?: number;
158
+ mass?: number;
159
+ volume?: number;
160
+ capacity?: number;
161
+ portionSize?: number;
162
+ basePrice?: number;
163
+ attributes: Array<{
164
+ id: number;
165
+ v: number;
166
+ }>;
167
+ effects: Array<{
168
+ id: number;
169
+ def: 0 | 1;
170
+ }>;
171
+ }
172
+ interface BundleManifest {
173
+ version: string;
174
+ builtAt: string;
175
+ totalBytes: number;
176
+ files: Record<string, {
177
+ bytes: number;
178
+ entries: number;
179
+ }>;
180
+ }
181
+ interface FittingDataset {
182
+ version: string;
183
+ attributes: Map<number, SdeAttribute>;
184
+ units: Map<number, SdeUnit>;
185
+ effects: Map<number, SdeEffect>;
186
+ metaGroups: Map<number, SdeMetaGroup>;
187
+ categories: Map<number, SdeCategory>;
188
+ groups: Map<number, SdeGroup>;
189
+ /** EVE Market-window hierarchy. Pickers walk the parent-chain
190
+ * from a type's `marketGroupID` up to the root to render the
191
+ * same tree the user sees in-game (Modules → Capacitor →
192
+ * Capacitor Battery). Empty when the loaded bundle predates
193
+ * market-group support. */
194
+ marketGroups: Map<number, SdeMarketGroup>;
195
+ cloneGrades: Map<number, SdeCloneGrade>;
196
+ dbuffCollections: Map<number, SdeDbuffCollection>;
197
+ dynamicAttributes: Map<number, SdeDynamicAttribute>;
198
+ typesByBucket: Partial<Record<TypeBucket, Map<number, SdeType>>>;
199
+ /** Resolves a type from any already-loaded bucket. */
200
+ getType(id: number): SdeType | undefined;
201
+ /** Lazily loads a bucket if not already in memory. */
202
+ loadBucket(bucket: TypeBucket): Promise<Map<number, SdeType>>;
203
+ }
204
+ type TypeBucket = 'ships' | 'modules' | 'charges' | 'drones' | 'fighters' | 'implants' | 'subsystems' | 'skills' | 'systemEffects' | 'structures' | 'structureModules' | 'mutaplasmids';
205
+ type SlotType = 'HI' | 'MED' | 'LO' | 'RIG' | 'SUBSYSTEM' | 'SERVICE';
206
+ type ModuleState = 'OFFLINE' | 'ONLINE' | 'ACTIVE' | 'OVERLOAD';
207
+ type FitVisibility = 'PRIVATE' | 'PUBLIC' | 'LINK';
208
+ interface MutatorData {
209
+ /** Mutaplasmid (dynamicItemAttribute) type id used to mutate this module. */
210
+ dynamicTypeID: number;
211
+ /** User-picked attribute values within the mutaplasmid's min/max range. */
212
+ attributes: Record<number, number>;
213
+ /** Source module type the abyssal was created from. The fit's
214
+ * `module.typeID` swaps to the mutaplasmid's `resultingType` (e.g.
215
+ * "Abyssal Warp Disruptor") on apply; this preserves the original
216
+ * type so the editor can: (a) reuse the source's base attribute
217
+ * values to compute slider ranges when re-editing, (b) restore
218
+ * the original module typeID when the user clears the mutator. */
219
+ sourceTypeID?: number;
220
+ }
221
+ interface FitModule {
222
+ /** Stable id within the fit (uuid client-side, db id server-side). */
223
+ id: string;
224
+ slotType: SlotType;
225
+ /** 0-indexed position within the slot type (max 8). */
226
+ position: number;
227
+ typeID: number;
228
+ state: ModuleState;
229
+ chargeTypeID?: number;
230
+ mutator?: MutatorData;
231
+ }
232
+ interface FitDrone {
233
+ id: string;
234
+ typeID: number;
235
+ countTotal: number;
236
+ countActive: number;
237
+ }
238
+ interface FitFighter {
239
+ id: string;
240
+ typeID: number;
241
+ count: number;
242
+ abilityState: Record<number, boolean>;
243
+ }
244
+ interface FitCargo {
245
+ id: string;
246
+ typeID: number;
247
+ count: number;
248
+ }
249
+ interface FitImplant {
250
+ id: string;
251
+ typeID: number;
252
+ slot: number;
253
+ }
254
+ interface FitBooster {
255
+ id: string;
256
+ typeID: number;
257
+ slot: number;
258
+ activeSideEffects: number[];
259
+ }
260
+ interface FitSubsystem {
261
+ id: string;
262
+ slot: number;
263
+ typeID: number;
264
+ }
265
+ interface DamageProfile {
266
+ id?: string;
267
+ name: string;
268
+ em: number;
269
+ thermal: number;
270
+ kinetic: number;
271
+ explosive: number;
272
+ isPreset?: boolean;
273
+ }
274
+ interface TargetProfile {
275
+ id?: string;
276
+ name: string;
277
+ signatureRadius: number;
278
+ maxVelocity: number;
279
+ emResist: number;
280
+ thermalResist: number;
281
+ kineticResist: number;
282
+ explosiveResist: number;
283
+ isPreset?: boolean;
284
+ }
285
+ interface SkillProfile {
286
+ id?: string;
287
+ name: string;
288
+ isDefault: boolean;
289
+ source: 'manual' | 'esi' | 'preset';
290
+ sourceCharacterID?: string;
291
+ /** typeID → 0..5. Sparse map; missing skills assumed level 0. */
292
+ skills: Record<number, 0 | 1 | 2 | 3 | 4 | 5>;
293
+ syncedAt?: string;
294
+ }
295
+ interface Fit {
296
+ id?: string;
297
+ discordUserID?: string;
298
+ shipTypeID: number;
299
+ name: string;
300
+ description?: string;
301
+ visibility: FitVisibility;
302
+ shareSlug?: string;
303
+ damageProfileID?: string;
304
+ targetProfileID?: string;
305
+ skillProfileID?: string;
306
+ authorCharacterID?: string;
307
+ authorName?: string;
308
+ authorCorpID?: string;
309
+ authorCorpName?: string;
310
+ tags: string[];
311
+ modules: FitModule[];
312
+ drones: FitDrone[];
313
+ fighters: FitFighter[];
314
+ cargo: FitCargo[];
315
+ implants: FitImplant[];
316
+ boosters: FitBooster[];
317
+ subsystems: FitSubsystem[];
318
+ /** T3D / T3C exclusive mode type id, if any. */
319
+ modeTypeID?: number;
320
+ createdAt?: string;
321
+ updatedAt?: string;
322
+ }
323
+ /**
324
+ * Every modifier applied by the engine carries one of these operations.
325
+ * Maps 1:1 to EVE's dogma operation enum. Order is significant — pipeline
326
+ * applies them in this exact sequence per attribute.
327
+ */
328
+ type ModifierOperation = 'PreAssign' | 'PreMul' | 'PreDiv' | 'ModAdd' | 'ModSub' | 'PostMul' | 'PostDiv' | 'PostPercent' | 'PostAssign';
329
+ interface ModifierAffliction {
330
+ sourceKind: 'module' | 'skill' | 'ship' | 'implant' | 'booster' | 'mode' | 'projected' | 'fleet' | 'drone' | 'fighter' | 'subsystem';
331
+ sourceID: string;
332
+ operation: ModifierOperation;
333
+ value: number;
334
+ /** Stacking penalty group key. null = unstacked. */
335
+ stackingGroup: string | null;
336
+ /** Optional resistance attribute applied to this modifier (projected effects). */
337
+ resistanceAttributeID?: number;
338
+ }
339
+ interface ComputedAttribute {
340
+ id: number;
341
+ base: number;
342
+ final: number;
343
+ afflictions: ModifierAffliction[];
344
+ }
345
+ /** What computeFit returns — the full derived view of a fit. */
346
+ interface ComputedFit {
347
+ fit: Fit;
348
+ /** Computed attributes for the ship itself (capacity, max velocity, etc). */
349
+ ship: Map<number, ComputedAttribute>;
350
+ /** Per-module computed state (modified attributes after skills + ship bonuses + module bonuses). */
351
+ modules: Map<string, ModuleComputed>;
352
+ drones: Map<string, DroneComputed>;
353
+ fighters: Map<string, FighterComputed>;
354
+ derived: DerivedStats;
355
+ }
356
+ interface ModuleComputed {
357
+ fitModuleID: string;
358
+ typeID: number;
359
+ slotType: SlotType;
360
+ state: ModuleState;
361
+ attributes: Map<number, ComputedAttribute>;
362
+ /** Effective CPU/PG cost (modifiable by Engineering skills). */
363
+ effectiveCpu: number;
364
+ effectivePower: number;
365
+ }
366
+ interface DroneComputed {
367
+ fitDroneID: string;
368
+ typeID: number;
369
+ attributes: Map<number, ComputedAttribute>;
370
+ /** DPS contribution from this drone group (per-drone × count). */
371
+ dps: number;
372
+ }
373
+ interface FighterComputed {
374
+ fitFighterID: string;
375
+ typeID: number;
376
+ attributes: Map<number, ComputedAttribute>;
377
+ abilities: Array<{
378
+ effectID: number;
379
+ enabled: boolean;
380
+ dps: number;
381
+ }>;
382
+ }
383
+ interface DerivedStats {
384
+ fitting: {
385
+ cpuUsed: number;
386
+ cpuMax: number;
387
+ powerUsed: number;
388
+ powerMax: number;
389
+ calibrationUsed: number;
390
+ calibrationMax: number;
391
+ droneBandwidthUsed: number;
392
+ droneBandwidthMax: number;
393
+ droneBayUsed: number;
394
+ droneBayMax: number;
395
+ slots: Record<SlotType, {
396
+ used: number;
397
+ max: number;
398
+ }>;
399
+ /** Per-weapon-class hardpoint accounting. Turrets and launchers
400
+ * consume separate physical mount points on the hull (attrs
401
+ * `turretHardpoints` 102 and `launcherHardpoints` 101). HI
402
+ * modules that are NEITHER turret nor launcher (smartbombs,
403
+ * EWAR bursts, command bursts, cloaks, MJD…) consume a HI
404
+ * slot but no hardpoint. */
405
+ hardpoints: {
406
+ turret: {
407
+ used: number;
408
+ max: number;
409
+ };
410
+ launcher: {
411
+ used: number;
412
+ max: number;
413
+ };
414
+ };
415
+ };
416
+ defense: {
417
+ shield: {
418
+ hp: number;
419
+ ehpUniform: number;
420
+ ehpAgainstProfile: number;
421
+ resistances: {
422
+ em: number;
423
+ thermal: number;
424
+ kinetic: number;
425
+ explosive: number;
426
+ };
427
+ };
428
+ armor: {
429
+ hp: number;
430
+ ehpUniform: number;
431
+ ehpAgainstProfile: number;
432
+ resistances: {
433
+ em: number;
434
+ thermal: number;
435
+ kinetic: number;
436
+ explosive: number;
437
+ };
438
+ };
439
+ hull: {
440
+ hp: number;
441
+ ehpUniform: number;
442
+ ehpAgainstProfile: number;
443
+ resistances: {
444
+ em: number;
445
+ thermal: number;
446
+ kinetic: number;
447
+ explosive: number;
448
+ };
449
+ };
450
+ ehpTotalAgainstProfile: number;
451
+ };
452
+ offense: {
453
+ weaponDps: number;
454
+ /** Reload-amortised weapon DPS. */
455
+ weaponSustainedDps: number;
456
+ droneDps: number;
457
+ fighterDps: number;
458
+ totalDps: number;
459
+ /** Reload-amortised total DPS (weapons + drones). */
460
+ totalSustainedDps: number;
461
+ alphaStrike: number;
462
+ weaponOptimal: number;
463
+ weaponFalloff: number;
464
+ weaponTracking?: number;
465
+ explosionVelocity?: number;
466
+ explosionRadius?: number;
467
+ breakdown: WeaponContribution[];
468
+ };
469
+ capacitor: {
470
+ capacity: number;
471
+ rechargeMs: number;
472
+ peakRechargeRate: number;
473
+ usagePerSecond: number;
474
+ stable: boolean;
475
+ stablePercent: number;
476
+ secondsToEmpty?: number;
477
+ };
478
+ tank: {
479
+ shieldRepairAmount: number;
480
+ shieldRepairDuration: number;
481
+ shieldRepairPerSecond: number;
482
+ /** Reload-amortised shield rep/sec (paste-fueled AAR / cap-fueled
483
+ * ASB modulate this lower than peak). */
484
+ shieldRepairPerSecondSustained: number;
485
+ armorRepairAmount: number;
486
+ armorRepairDuration: number;
487
+ armorRepairPerSecond: number;
488
+ armorRepairPerSecondSustained: number;
489
+ hullRepairAmount: number;
490
+ hullRepairDuration: number;
491
+ hullRepairPerSecond: number;
492
+ hullRepairPerSecondSustained: number;
493
+ passiveShieldRegenPeak: number;
494
+ };
495
+ navigation: {
496
+ maxVelocity: number;
497
+ mass: number;
498
+ agility: number;
499
+ alignTimeSeconds: number;
500
+ warpSpeed: number;
501
+ };
502
+ targeting: {
503
+ maxTargetingRange: number;
504
+ maxLockedTargets: number;
505
+ signatureRadius: number;
506
+ scanResolution: number;
507
+ sensorStrength: number;
508
+ sensorType: 'radar' | 'ladar' | 'magnetometric' | 'gravimetric' | 'unknown';
509
+ };
510
+ drones: {
511
+ bayUsed: number;
512
+ bayMax: number;
513
+ bandwidthUsed: number;
514
+ bandwidthMax: number;
515
+ active: number;
516
+ controlRange: number;
517
+ };
518
+ /** Active projected effects from the ProjectedSource list. Empty when
519
+ * no projection is configured. */
520
+ projected: ProjectedEffectReport[];
521
+ /** Upwell-structure metadata. Populated only when the host typeID
522
+ * resolves to a category=65 type; null for ship fits. Carries the
523
+ * service-slot summary + total fuel-block consumption across all
524
+ * online service modules. */
525
+ structure: StructureMeta | null;
526
+ /** Per-module final-attribute snapshot keyed by `Fit.modules[i].id`.
527
+ * The engine writes the final (post-skill, post-hull-bonus,
528
+ * post-modifier-pipeline) value of every attribute on each
529
+ * module + its loaded charge after `applySourceItem` runs.
530
+ * Consumed by the hover popover to show user-meaningful values
531
+ * ("3.6 km optimal" with skill bonuses) instead of raw SDE base
532
+ * values. Each entry is `{ module: Map<attrID, finalValue>,
533
+ * charge: Map<attrID, finalValue> | null }`. */
534
+ moduleSnapshots: Record<string, ModuleAttrSnapshot>;
535
+ }
536
+ interface ModuleAttrSnapshot {
537
+ module: Record<number, number>;
538
+ charge: Record<number, number> | null;
539
+ }
540
+ interface StructureServiceModule {
541
+ /** Index of the module in `Fit.modules`. */
542
+ moduleIndex: number;
543
+ typeID: number;
544
+ name: string;
545
+ /** ONLINE / ACTIVE / OFFLINE — only ONLINE+ contributes fuel. */
546
+ state: 'OFFLINE' | 'ONLINE' | 'ACTIVE' | 'OVERLOAD';
547
+ /** Per-hour fuel block cost (attr 2109 serviceModuleFuelAmount).
548
+ * Zero when the module is OFFLINE. */
549
+ fuelBlocksPerHour: number;
550
+ }
551
+ interface StructureMeta {
552
+ /** Service-slot capacity from the host hull (attr 2056). */
553
+ serviceSlotsMax: number;
554
+ /** Number of modules currently fitted in a SERVICE slot (regardless
555
+ * of state). */
556
+ serviceSlotsUsed: number;
557
+ /** Sum of `serviceModuleFuelAmount` across modules whose state is
558
+ * ONLINE or above. Per-hour rate. */
559
+ fuelBlocksPerHour: number;
560
+ /** Per-service-module breakdown for the UI. */
561
+ services: StructureServiceModule[];
562
+ }
563
+ /** Result of computeFit when the engine cannot compute a portion (missing
564
+ * data, malformed fit, etc.). Captures partial results + warnings. */
565
+ interface FitWarning {
566
+ code: string;
567
+ message: string;
568
+ sourceID?: string;
569
+ }
570
+ /**
571
+ * Projected source — a hostile module/drone applying its effects to the fit
572
+ * being computed. Modelled as a typeID + state + optional charge so the
573
+ * same modifier engine can dispatch (with `domain: 'targetID'` resolving to
574
+ * the fit's own ship as the target). Used for previewing PvP scenarios:
575
+ * "what does my ship look like under ECM / web / damp?".
576
+ */
577
+ interface ProjectedSource {
578
+ id: string;
579
+ typeID: number;
580
+ state: ModuleState;
581
+ chargeTypeID?: number;
582
+ mutator?: MutatorData;
583
+ /** Distance in meters between attacker and target. Optional — if
584
+ * unset the engine treats the projection as in-optimal (full effect,
585
+ * factor = 1). For falloff-projected EWAR (web/damp/paint/track/
586
+ * guidance) the engine applies `0.5 ** ((max(0, distance - optimal)
587
+ * / falloff) ** 2)` and clamps to 0 past `optimal + 3 × falloff`. */
588
+ projectionRange?: number;
589
+ }
590
+ /** Summary of an active projected effect — surfaced so the UI can render
591
+ * "you're being jammed at X% per cycle" / "your tracking is reduced". */
592
+ interface ProjectedEffectReport {
593
+ /** Source projected module type. */
594
+ typeID: number;
595
+ /** What kind of EWAR this is. */
596
+ kind: 'ECM' | 'SENSOR_DAMP' | 'TRACKING_DISRUPT' | 'WEB' | 'WARP_SCRAM' | 'WARP_DISRUPT' | 'NEUT' | 'NOS' | 'OTHER' | 'REMOTE_REP_SHIELD' | 'REMOTE_REP_ARMOR' | 'REMOTE_REP_HULL' | 'REMOTE_CAP';
597
+ /** Per-cycle jam probability (0..1) — only ECM. */
598
+ jamChance?: number;
599
+ /** Per-second healing received on the relevant layer (REMOTE_REP_*) or
600
+ * per-second cap drain (positive = drain, negative = injection) for
601
+ * REMOTE_CAP / NEUT / NOS. */
602
+ perSecond?: number;
603
+ /** Free-form summary the UI can render verbatim. */
604
+ summary: string;
605
+ }
606
+ /**
607
+ * Per-weapon DPS / range breakdown row. Aggregated into DerivedStats.offense
608
+ * by `derived/offense.ts::computeOffense`. The UI uses this to render a
609
+ * "weapon by weapon" listing under the offense tab + to drive the
610
+ * "max engagement range" / "tracking limit" badges.
611
+ */
612
+ type WeaponKind = 'TURRET' | 'MISSILE' | 'SMARTBOMB' | 'DOOMSDAY' | 'DRONE';
613
+ interface WeaponContribution {
614
+ sourceID: string;
615
+ typeID: number;
616
+ name?: string;
617
+ kind: WeaponKind;
618
+ /** Single-volley damage (sum across all weapons of this row's count). */
619
+ alpha: number;
620
+ /** Peak DPS — alpha / cycle. Burst rate during the active firing window. */
621
+ dps: number;
622
+ /** DPS amortised across reload windows. For weapons with frequent reloads
623
+ * (lasers, cap boosters, capital weapons) this is meaningfully lower
624
+ * than `dps`. For HAM/cruise launchers the difference is < 1 %. Equals
625
+ * `dps` when no charge / no reload model. */
626
+ sustainedDps?: number;
627
+ /** Reload time in seconds (parsed from module's `reloadTime` attribute
628
+ * or assigned a 1 s default by Pyfa-parity legacy effects 10/34/67/
629
+ * 101/6995). */
630
+ reloadSeconds?: number;
631
+ /** Charges per loadout — `floor(launcher_capacity / charge_volume)`. */
632
+ chargesPerLoad?: number;
633
+ /** Vorton Projector ONLY — best-case chain DPS assuming `arcTargets`
634
+ * are within range. Computed as a geometric series of base DPS:
635
+ * Σ(base × (1 - reduction)^k) for k=0..N-1. */
636
+ chainDpsMax?: number;
637
+ /** Vorton Projector ONLY — number of chain targets (e.g. 10). */
638
+ chainTargetCount?: number;
639
+ cycleSeconds: number;
640
+ damages: {
641
+ em: number;
642
+ thermal: number;
643
+ kinetic: number;
644
+ explosive: number;
645
+ total: number;
646
+ };
647
+ range: {
648
+ optimal: number;
649
+ falloff: number;
650
+ tracking: number;
651
+ burstRange: number;
652
+ explosionRadius: number;
653
+ explosionVelocity: number;
654
+ drf: number;
655
+ };
656
+ chargeTypeID?: number;
657
+ count: number;
658
+ /** Triglavian disintegrator (effect 6995) spool data. Carries the
659
+ * fully-modified max bonus (attr 2734 post-ship-hull boosts like
660
+ * Babaroga's +20%/level Large Precursor Weapon) and the per-cycle
661
+ * bonus (attr 2733). The UI reads these to render the spool slider's
662
+ * Min/Max DPS columns and the time-to-full-spool readout without
663
+ * re-running the engine. Absent on every other weapon kind. */
664
+ disintegrator?: {
665
+ maxBonus: number;
666
+ bonusPerCycle: number;
667
+ /** DPS at spool=0 (cold start). Computed by the engine from the
668
+ * current `dps` and the spool factor used in this very compute
669
+ * pass — `baseDps = dps / (1 + currentSpoolPct × maxBonus)`.
670
+ * Storing it here makes the slider Min/Max readouts INVARIANT to
671
+ * the slider position: both Min (= baseDps) and Max (= baseDps ×
672
+ * (1 + maxBonus)) come straight from this field, instead of the
673
+ * UI reverse-engineering them on every render — which produced a
674
+ * visible drift while the debounced engine recompute was
675
+ * in-flight. */
676
+ baseDps: number;
677
+ };
678
+ }
679
+
680
+ /**
681
+ * Top-level fit calculation orchestrator.
682
+ *
683
+ * `computeFit(fit, dataset, options)` builds the runtime ItemState graph,
684
+ * applies the canonical EVE modifier pipeline in order, and returns a
685
+ * ComputedFit suitable for UI rendering.
686
+ *
687
+ * Phase 1 scope (this file as of the foundation cut):
688
+ * - Build ItemState graph from the persistent Fit + dataset
689
+ * - Apply skills via the modifier engine (LocationRequiredSkillModifier
690
+ * family) — full coverage for ~93% of skill-based bonuses
691
+ * - Apply ship intrinsic effects (LocationModifier on the ship type's
692
+ * own effects)
693
+ * - Apply module effects filtered by current state (offline excluded)
694
+ * - Apply implants / boosters / mode / subsystems via the same dispatcher
695
+ * - Snapshot a minimal DerivedStats (ship hp/cap/nav/targeting + slot
696
+ * usage). Advanced derived stats (DPS / cap stability / EHP against
697
+ * damage profile / projected EWAR) are STUBBED — they live in
698
+ * dedicated modules under `derived/` and `effects/` that the next
699
+ * phases will fill in.
700
+ *
701
+ * Out of scope (Phase 2+):
702
+ * - Capacitor stability simulation (cap drain vs recharge curve)
703
+ * - Weapon DPS calc (turret tracking + missile DRF)
704
+ * - Drone DPS aggregation
705
+ * - Damage-profile-aware EHP (requires a target damage distribution)
706
+ * - Projected EWAR / fleet command bursts
707
+ * - Mutaplasmid attribute application (handled at ItemState construction
708
+ * via attributeOverrides, but the UI sliders aren't wired yet)
709
+ *
710
+ * The returned `derived` block is intentionally partial in this phase;
711
+ * downstream code MUST treat it as best-effort and not assume DPS/cap
712
+ * fields are populated until Phase 2/3.
713
+ */
714
+
715
+ interface ComputeFitOptions {
716
+ /** Skill levels keyed by skill type id. Missing skills default to 0. */
717
+ skillProfile: SkillProfile;
718
+ /** Damage profile for EHP-vs-profile computation. Omitted → omni 25%. */
719
+ damageProfile?: DamageProfile | null;
720
+ /** Target profile for stats-vs-target (effective DPS at range,
721
+ * application drop-off vs sig/speed). Stored on the engine output
722
+ * for downstream consumers; the offense aggregator reads it
723
+ * directly from there. */
724
+ targetProfile?: TargetProfile | null;
725
+ /** Hostile sources projecting onto this fit. Their effects apply with
726
+ * `domain: 'targetID'` resolved to the fit's own ship. */
727
+ projected?: ProjectedSource[];
728
+ /** Skill levels assumed for the projected attacker. Defaults to All V
729
+ * (matches how Pyfa renders projected effects "fed by All V skills"). */
730
+ projectedSkillLevels?: Map<number, number>;
731
+ /** Mutadaptive Remote Armor Repairer spool fraction (0..1). 1.0 =
732
+ * fully spooled (max bonus); 0 = unspooled (no bonus). Defaults to 1
733
+ * if omitted, matching Pyfa's "always-spooled" sustained engagement
734
+ * assumption. */
735
+ spoolPercent?: number;
736
+ /** Triglavian Entropic Disintegrator spool fraction (0..1). Same
737
+ * semantics as `spoolPercent` but applied to disintegrator weapons'
738
+ * damageMultiplier (effect 6995, attrs 2733/2734). Defaults to 1
739
+ * (full spool) for parity with Pyfa's default DPS column. */
740
+ disintegratorSpoolPercent?: number;
741
+ /** System effect beacon typeID (Incursion/Triglavian/Drifter/Wormhole).
742
+ * When set, the engine reads the beacon's attrs from the dataset and
743
+ * applies the corresponding system-wide debuff/buff. */
744
+ systemEffectTypeID?: number | null;
745
+ }
746
+ declare function computeFit(fit: Fit, dataset: FittingDataset, opts: ComputeFitOptions): ComputedFit;
747
+
748
+ /**
749
+ * EFT (EVE Fitting Tool) text format parser.
750
+ *
751
+ * EFT is the de-facto community-standard text format for sharing ship fits.
752
+ * Pyfa, EFT, AFTER, in-game (after the export-to-clipboard feature), and
753
+ * almost every fit tool can read/write it.
754
+ *
755
+ * Canonical structure:
756
+ *
757
+ * [ShipName, FitName]
758
+ * <empty line>
759
+ * <Low slot module>
760
+ * <Low slot module>
761
+ * <empty line>
762
+ * <Mid slot module>
763
+ * <Mid slot module>
764
+ * <empty line>
765
+ * <Hi slot module>, <charge name> ← optional ", <charge>" pairing
766
+ * <empty line>
767
+ * <Rig module>
768
+ * <empty line> ← optional Subsystem section (T3C only)
769
+ * <Subsystem module>
770
+ * <empty line> ← Drone bay
771
+ * <Drone name> x<count>
772
+ * <empty line> ← Implants/Boosters/Cargo (mixed)
773
+ * <Item name> x<count>
774
+ *
775
+ * Real-world EFT exports omit empty sections entirely (no trailing blank
776
+ * lines), have inconsistent whitespace, occasionally include `[Empty Slot]`
777
+ * placeholders, and sometimes interleave drones/cargo without a separator.
778
+ * This parser is forgiving: it tolerates missing sections, extra blank
779
+ * lines, mixed casing, and resolves names case-insensitively against the
780
+ * dataset.
781
+ *
782
+ * Slot ordering convention (used by every modern tool):
783
+ * 1. Low slots
784
+ * 2. Mid slots
785
+ * 3. High slots
786
+ * 4. Rigs
787
+ * 5. Subsystems (T3C only)
788
+ * 6. Drones
789
+ * 7. Cargo / Implants / Boosters (mixed, identified by category)
790
+ *
791
+ * In-game export reverses this (high → low). We accept both and disambiguate
792
+ * by counting against the ship's slot attribute values when possible.
793
+ */
794
+
795
+ interface EftParseResult {
796
+ fit: Fit;
797
+ /** Lines that couldn't be matched to anything in the SDE — surfaced to
798
+ * the UI as warnings (typo? renamed item? unsupported entry?). */
799
+ warnings: Array<{
800
+ line: number;
801
+ text: string;
802
+ reason: string;
803
+ }>;
804
+ }
805
+ interface NameIndex {
806
+ /** Lower-cased name → typeID (latest-published wins on collision). */
807
+ map: Map<string, number>;
808
+ types: Map<number, SdeType>;
809
+ }
810
+ /**
811
+ * Build a fast name-lookup index from the dataset. Caller is expected to
812
+ * have loaded all type buckets that might appear in EFT input — typically
813
+ * ships + modules + charges + drones + implants + subsystems + skills (for
814
+ * implant detection — implants live in the implant bucket, but boosters
815
+ * may also be there depending on SDE version).
816
+ */
817
+ declare function buildNameIndex(dataset: FittingDataset): NameIndex;
818
+ /**
819
+ * Parse an EFT-format string into a Fit object. The returned `fit.id` is
820
+ * left undefined — the caller is responsible for assigning a uuid before
821
+ * persisting to the database.
822
+ *
823
+ * The parser is intentionally tolerant: missing sections, extra blank
824
+ * lines, charge with leading space, etc. Anything truly unparseable is
825
+ * collected in `warnings` instead of throwing.
826
+ */
827
+ declare function parseEft(text: string, dataset: FittingDataset): EftParseResult;
828
+
829
+ /**
830
+ * EFT (EVE Fitting Tool) text format writer.
831
+ *
832
+ * Output layout matches the in-game export format and what every modern
833
+ * fit tool produces:
834
+ *
835
+ * [ShipName, FitName]
836
+ * <empty line>
837
+ * <Low slots>
838
+ * <empty line>
839
+ * <Mid slots>
840
+ * <empty line>
841
+ * <High slots, optional ", charge">
842
+ * <empty line>
843
+ * <Rigs>
844
+ * <empty line>
845
+ * <Subsystems> (T3C only — section omitted otherwise)
846
+ * <empty line>
847
+ * <Drones x N>
848
+ * <empty line>
849
+ * <Implants / Boosters / Cargo x N>
850
+ *
851
+ * Empty sections are omitted entirely (no double blank lines). Implants and
852
+ * boosters are rendered alongside cargo with quantity suffix per the
853
+ * in-game export convention.
854
+ *
855
+ * Module ordering within a section follows the saved `position` field, then
856
+ * id as tiebreaker. Stable ordering matters because EFT consumers expect the
857
+ * same fit to round-trip identically.
858
+ */
859
+
860
+ declare function formatEft(fit: Fit, dataset: FittingDataset): string;
861
+
862
+ /**
863
+ * Multi-format fit exporters: DNA, Multibuy, plain typeID list. EFT
864
+ * already lives in [eft/format.ts](eft/format.ts).
865
+ *
866
+ * - DNA: `shipID:moduleID;count::` — used by EVE's in-game chat links
867
+ * and several third-party tools. Format ends with `::`.
868
+ * - Multibuy: simple "name x count" list, suitable for pasting into
869
+ * EVE's market multi-buy window or a contract. Includes ship + every
870
+ * fitted item + cargo, deduplicated and aggregated.
871
+ * - Type-id list: a flat newline-separated typeID list for tooling.
872
+ */
873
+
874
+ /** EVE in-game DNA link format. */
875
+ declare function formatDna(fit: Fit): string;
876
+ /** "Name x count" list for multibuy / contracts. */
877
+ declare function formatMultibuy(fit: Fit, dataset: FittingDataset): string;
878
+ /** typeID-per-line list. */
879
+ declare function formatTypeIds(fit: Fit): string;
880
+
881
+ /**
882
+ * Well-known EVE Online dogma constants used by the fitting engine.
883
+ *
884
+ * These are stable IDs that have been carried since the SDE existed (2010+).
885
+ * Fenris Creations occasionally adds new attributes/effects but never reuses or renames
886
+ * existing ones. This file is the single source of truth — no other module
887
+ * should hardcode these numeric IDs.
888
+ *
889
+ * Sources:
890
+ * - Pyfa's `eos/const.py` for the canonical list
891
+ * - EVE Reference / EVE Online Static Data Export documentation
892
+ * - Cross-referenced against our /server/data/SDE/dogma{Attributes,Effects}.jsonl
893
+ */
894
+
895
+ declare const CATEGORY: {
896
+ readonly SHIP: 6;
897
+ readonly MODULE: 7;
898
+ readonly CHARGE: 8;
899
+ readonly SKILL: 16;
900
+ readonly DRONE: 18;
901
+ readonly IMPLANT: 20;
902
+ readonly SUBSYSTEM: 32;
903
+ readonly FIGHTER: 87;
904
+ readonly STRUCTURE_MODULE: 66;
905
+ };
906
+ declare const OPERATION_BY_SDE_CODE: Record<number, ModifierOperation>;
907
+ declare const SLOT_EFFECT_ID: {
908
+ readonly LO_POWER: 11;
909
+ readonly HI_POWER: 12;
910
+ readonly MED_POWER: 13;
911
+ readonly RIG_SLOT: 2663;
912
+ readonly SUBSYSTEM: 3772;
913
+ readonly SERVICE_SLOT: 6306;
914
+ };
915
+ declare const SLOT_EFFECT_TO_SLOT_TYPE: Record<number, 'HI' | 'MED' | 'LO' | 'RIG' | 'SUBSYSTEM' | 'SERVICE'>;
916
+ declare const ACTIVATION_EFFECT_ID: {
917
+ readonly ONLINE_FOR_STRUCTURES: 16;
918
+ readonly ONLINE: 16;
919
+ readonly LASER_TURRET: 10;
920
+ readonly PROJECTILE_TURRET: 34;
921
+ readonly HYBRID_TURRET: 35;
922
+ readonly MISSILE_LAUNCH: 87;
923
+ readonly MISSILE_LAUNCH_DUMB: 4947;
924
+ readonly DRONE_DAMAGE_AMP: 5379;
925
+ readonly SHIELD_BOOSTING: 4;
926
+ readonly SHIELD_BOOSTING_FUELED: 4936;
927
+ readonly ARMOR_REPAIR: 27;
928
+ readonly ARMOR_REPAIR_FUELED: 5275;
929
+ readonly HULL_REPAIR: 26;
930
+ readonly AB_THRUST: 6;
931
+ readonly MWD_THRUST: 8;
932
+ readonly SHIELD_TRANSFER: 18;
933
+ readonly ARMOR_TRANSFER: 592;
934
+ readonly REMOTE_CAP_TRANSFER: 31;
935
+ readonly NOSFERATU: 1;
936
+ readonly WEB_TARGETED: 14;
937
+ readonly WARP_SCRAMBLER: 19;
938
+ readonly SENSOR_DAMP: 1130;
939
+ readonly TRACKING_DISRUPTOR: 1799;
940
+ readonly ECM: 1786;
941
+ readonly ENERGY_NEUTRALIZE: 28;
942
+ };
943
+ type WeaponEffectKind = 'TURRET' | 'MISSILE' | 'SMARTBOMB' | 'DOOMSDAY';
944
+ declare const WEAPON_EFFECT_KIND: Record<number, WeaponEffectKind>;
945
+ declare const REPAIR_EFFECT_AMOUNT_ATTR: Record<number, {
946
+ amountAttr: number;
947
+ layer: 'SHIELD' | 'ARMOR' | 'HULL';
948
+ }>;
949
+ declare const ATTR: {
950
+ readonly MASS: 4;
951
+ readonly HP: 9;
952
+ readonly AGILITY: 70;
953
+ readonly VOLUME: 161;
954
+ readonly CAPACITY: 38;
955
+ readonly POWER_OUTPUT: 11;
956
+ readonly POWER_USED: 30;
957
+ readonly CPU_OUTPUT: 48;
958
+ readonly CPU_USED: 50;
959
+ readonly UPGRADE_CAPACITY: 1132;
960
+ readonly UPGRADE_COST: 1153;
961
+ readonly DRONE_BANDWIDTH: 1271;
962
+ readonly DRONE_CAPACITY: 283;
963
+ readonly HI_SLOTS: 14;
964
+ readonly MED_SLOTS: 13;
965
+ readonly LOW_SLOTS: 12;
966
+ readonly RIG_SLOTS: 1137;
967
+ readonly SUBSYSTEM_SLOTS: 1367;
968
+ readonly SERVICE_SLOTS: 2056;
969
+ readonly LAUNCHER_HARDPOINTS: 101;
970
+ readonly TURRET_HARDPOINTS: 102;
971
+ readonly RIG_SIZE: 1547;
972
+ /** Per-ship cap on how many modules of THIS ITEM'S group can be fitted.
973
+ * e.g. Medium Breacher Pod Launcher carries `maxGroupFitted = 1`, so
974
+ * only one of its group (`Breacher Pod Launcher`) is allowed per hull,
975
+ * even if the ship has multiple launcher hardpoints. */
976
+ readonly MAX_GROUP_FITTED: 1544;
977
+ /** Per-ship cap on how many modules of THIS EXACT typeID can be fitted.
978
+ * Sibling of MAX_GROUP_FITTED — used by a small set of unique-named
979
+ * modules (Bastion Module, certain doomsday weapons). */
980
+ readonly MAX_TYPE_FITTED: 2487;
981
+ readonly CAPACITOR_CAPACITY: 482;
982
+ readonly CAPACITOR_RECHARGE_RATE: 55;
983
+ readonly SHIELD_CAPACITY: 263;
984
+ readonly SHIELD_RECHARGE_RATE: 479;
985
+ readonly SHIELD_EM_RES: 271;
986
+ readonly SHIELD_THERMAL_RES: 274;
987
+ readonly SHIELD_KINETIC_RES: 273;
988
+ readonly SHIELD_EXPLOSIVE_RES: 272;
989
+ readonly ARMOR_HP: 265;
990
+ readonly ARMOR_EM_RES: 267;
991
+ readonly ARMOR_THERMAL_RES: 270;
992
+ readonly ARMOR_KINETIC_RES: 269;
993
+ readonly ARMOR_EXPLOSIVE_RES: 268;
994
+ readonly STRUCTURE_EM_RES: 113;
995
+ readonly STRUCTURE_THERMAL_RES: 110;
996
+ readonly STRUCTURE_KINETIC_RES: 109;
997
+ readonly STRUCTURE_EXPLOSIVE_RES: 111;
998
+ readonly MAX_TARGET_RANGE: 76;
999
+ /** Theoretical maximum-targeting-range cap (`maximumRangeCap`). Default
1000
+ * 300 km; raised by Sensor Array / Sensor Booster overload etc. via
1001
+ * PreAssign on attr 797. The SDE encodes this as `maxAttributeID=797`
1002
+ * on attr 76 — clamping is applied at the engine read site. */
1003
+ readonly MAX_TARGET_RANGE_CAP: 797;
1004
+ readonly MAX_LOCKED_TARGETS: 192;
1005
+ readonly SIGNATURE_RADIUS: 552;
1006
+ readonly SCAN_RESOLUTION: 564;
1007
+ readonly SCAN_RADAR_STRENGTH: 208;
1008
+ readonly SCAN_LADAR_STRENGTH: 209;
1009
+ readonly SCAN_MAGNETOMETRIC_STRENGTH: 210;
1010
+ readonly SCAN_GRAVIMETRIC_STRENGTH: 211;
1011
+ readonly DRONE_CONTROL_RANGE: 458;
1012
+ readonly MAX_VELOCITY: 37;
1013
+ readonly WARP_SPEED_MULTIPLIER: 600;
1014
+ readonly DAMAGE_MULTIPLIER: 64;
1015
+ readonly EM_DAMAGE: 114;
1016
+ readonly THERMAL_DAMAGE: 118;
1017
+ readonly KINETIC_DAMAGE: 117;
1018
+ readonly EXPLOSIVE_DAMAGE: 116;
1019
+ readonly OPTIMAL_RANGE: 54;
1020
+ readonly FALLOFF_RANGE: 158;
1021
+ readonly TRACKING_SPEED: 160;
1022
+ readonly RATE_OF_FIRE: 51;
1023
+ readonly DAMAGE_DURATION: 73;
1024
+ readonly MISSILE_DAMAGE_MULTIPLIER: 212;
1025
+ readonly EXPLOSION_VELOCITY: 653;
1026
+ readonly EXPLOSION_RADIUS: 654;
1027
+ readonly DRF: 858;
1028
+ readonly DOT_DURATION: 5735;
1029
+ readonly DOT_MAX_DAMAGE_PER_TICK: 5736;
1030
+ readonly DOT_MAX_HP_PERCENTAGE_PER_TICK: 5737;
1031
+ readonly CHARGE_GROUP_1: 604;
1032
+ readonly CHARGE_GROUP_2: 605;
1033
+ readonly CHARGE_GROUP_3: 606;
1034
+ readonly CHARGE_GROUP_4: 609;
1035
+ readonly CHARGE_GROUP_5: 610;
1036
+ readonly CHARGE_SIZE: 128;
1037
+ readonly REQUIRED_SKILL_1: 182;
1038
+ readonly REQUIRED_SKILL_2: 183;
1039
+ readonly REQUIRED_SKILL_3: 184;
1040
+ readonly REQUIRED_SKILL_4: 1285;
1041
+ readonly REQUIRED_SKILL_5: 1289;
1042
+ readonly REQUIRED_SKILL_6: 1290;
1043
+ readonly REQUIRED_SKILL_1_LEVEL: 277;
1044
+ readonly REQUIRED_SKILL_2_LEVEL: 278;
1045
+ readonly REQUIRED_SKILL_3_LEVEL: 279;
1046
+ readonly REQUIRED_SKILL_4_LEVEL: 1286;
1047
+ readonly REQUIRED_SKILL_5_LEVEL: 1287;
1048
+ readonly REQUIRED_SKILL_6_LEVEL: 1288;
1049
+ readonly MAX_ACTIVE_DRONES: 352;
1050
+ readonly MAX_VELOCITY_LIMIT: 192;
1051
+ readonly MAX_RANGE_LIMIT: 192;
1052
+ readonly CAN_FIT_SHIP_GROUP_1: 1298;
1053
+ readonly CAN_FIT_SHIP_GROUP_2: 1299;
1054
+ readonly CAN_FIT_SHIP_GROUP_3: 1300;
1055
+ readonly CAN_FIT_SHIP_GROUP_4: 1301;
1056
+ readonly CAN_FIT_SHIP_GROUP_5: 1872;
1057
+ readonly CAN_FIT_SHIP_GROUP_6: 1879;
1058
+ readonly CAN_FIT_SHIP_GROUP_7: 1880;
1059
+ readonly CAN_FIT_SHIP_GROUP_8: 1881;
1060
+ readonly CAN_FIT_SHIP_GROUP_9: 2065;
1061
+ readonly CAN_FIT_SHIP_TYPE_1: 1302;
1062
+ readonly CAN_FIT_SHIP_TYPE_2: 1303;
1063
+ readonly CAN_FIT_SHIP_TYPE_3: 1304;
1064
+ readonly CAN_FIT_SHIP_TYPE_4: 1305;
1065
+ };
1066
+ /** Pairs of (skill_attr_id, level_attr_id). Used by skill-requirement derivation. */
1067
+ declare const REQUIRED_SKILL_PAIRS: ReadonlyArray<readonly [number, number]>;
1068
+ declare const STACKING_PENALTY_K = 7.1289;
1069
+ interface LegacyEffectEntry {
1070
+ /** SDE effect ID. */
1071
+ id: number;
1072
+ /** Expected SDE `effectName`, or null if the bundle doesn't export one
1073
+ * for this ID (the engine still relies on the numeric ID). */
1074
+ name: string | null;
1075
+ /** Free-form descriptor: which engine handler / table claims this ID. */
1076
+ handler: string;
1077
+ }
1078
+ declare const LEGACY_EFFECT_IDS: ReadonlyArray<LegacyEffectEntry>;
1079
+ declare const OUT_OF_SCOPE_EFFECT_IDS: ReadonlyArray<LegacyEffectEntry>;
1080
+ /** Verify every effect ID in LEGACY_EFFECT_IDS is still present in the
1081
+ * loaded dataset. Returns an array of complaints (empty when clean) so
1082
+ * callers can surface the result via console.warn / throw / log telemetry.
1083
+ *
1084
+ * Two failure modes detected:
1085
+ * - "missing": SDE no longer carries this effect ID — the handler will
1086
+ * silently no-op (Fenris Creations either renamed or deleted). Investigate.
1087
+ * - "name-mismatch": SDE still carries the ID but with a different name
1088
+ * than registered. Probably a Fenris Creations rename — the engine still works,
1089
+ * but the registry comment / handler description is stale.
1090
+ *
1091
+ * Run at engine boot in dev mode (gate via `import.meta.dev`). Production
1092
+ * callers can opt in by passing the dataset to `verifyLegacyEffectIds()`. */
1093
+ declare function verifyLegacyEffectIds(effects: ReadonlyMap<number, {
1094
+ effectName?: string;
1095
+ }>): Array<{
1096
+ id: number;
1097
+ kind: 'missing' | 'name-mismatch';
1098
+ expected: string | null;
1099
+ actual: string | null;
1100
+ }>;
1101
+
1102
+ /**
1103
+ * Per-attribute modification pipeline.
1104
+ *
1105
+ * One instance per (item × attributeID) holds:
1106
+ * - the base value (from the SDE typeDogma row, possibly preassigned)
1107
+ * - a flat list of "afflictions" — every modifier that's been applied
1108
+ * during the calc pass (skills, ship bonuses, modules, fleet, etc.)
1109
+ *
1110
+ * `compute()` runs the EVE-canonical pipeline:
1111
+ *
1112
+ * base ─► PreAssign override ─► PreMul/PreDiv (stack-penalized)
1113
+ * ─► ModAdd/ModSub (additive)
1114
+ * ─► PostMul/PostDiv/PostPercent (stack-penalized)
1115
+ * ─► PostAssign override ─► (optional cap) ─► final
1116
+ *
1117
+ * `PostPercent` is treated identically to `PostMul(1 + value)` — see
1118
+ * https://wiki.eveuniversity.org/EVE_dogma_engine
1119
+ *
1120
+ * The afflictions list is preserved on the result so the UI can drill into
1121
+ * which sources contributed to a final value (Pyfa-style breakdown).
1122
+ */
1123
+
1124
+ declare class ModifiedAttribute {
1125
+ readonly attributeID: number;
1126
+ readonly base: number;
1127
+ readonly afflictions: ModifierAffliction[];
1128
+ private _cache;
1129
+ constructor(attributeID: number, base: number);
1130
+ addAffliction(a: ModifierAffliction): void;
1131
+ /**
1132
+ * Reset all applied modifiers, keeping the base value. Used when the
1133
+ * engine re-runs (e.g. user toggles a module state).
1134
+ */
1135
+ reset(): void;
1136
+ /**
1137
+ * Compute the final value. Result is cached until `addAffliction()` or
1138
+ * `reset()` is called.
1139
+ *
1140
+ * @param maxAttribute optional cap — if provided, final value is clamped
1141
+ * to MIN(computed, maxAttribute). Used for attributes
1142
+ * with a maxAttributeID reference.
1143
+ */
1144
+ compute(maxAttribute?: number): number;
1145
+ /**
1146
+ * Compute and bundle into a ComputedAttribute (for UI consumption /
1147
+ * breakdown rendering). The afflictions array is shared by reference;
1148
+ * callers should not mutate it.
1149
+ */
1150
+ snapshot(maxAttribute?: number): {
1151
+ id: number;
1152
+ base: number;
1153
+ final: number;
1154
+ afflictions: ModifierAffliction[];
1155
+ };
1156
+ }
1157
+
1158
+ /**
1159
+ * Runtime state of a single fittable item — ship, module, drone, fighter,
1160
+ * implant, booster, charge, subsystem, mode, character.
1161
+ *
1162
+ * Each item carries its own ModifiedAttribute map keyed by attributeID.
1163
+ * Base values come from the SDE typeDogma row; modifiers are added during
1164
+ * the calc pass via the modifier engine, then `compute()` is called once
1165
+ * the pass is complete.
1166
+ *
1167
+ * Module state matters: only modules that pass `appliesAtState()` contribute
1168
+ * effects to the fit (offline modules don't add CPU usage either). Charges
1169
+ * project their attributes into the parent module's `otherID` domain.
1170
+ */
1171
+
1172
+ type ItemKind = 'ship' | 'module' | 'drone' | 'fighter' | 'implant' | 'booster' | 'charge' | 'subsystem' | 'mode' | 'character';
1173
+ interface ItemStateInit {
1174
+ kind: ItemKind;
1175
+ /** Stable identifier within the fit (uuid for fit_modules etc.; sentinel
1176
+ * strings like "ship", "char" for the singletons). */
1177
+ id: string;
1178
+ type: SdeType;
1179
+ /** Module/drone-style runtime state. Defaults to ONLINE for items where
1180
+ * state semantics don't apply (ship, char). */
1181
+ state?: ModuleState;
1182
+ /** Charge loaded into a module (only meaningful for kind === 'module'). */
1183
+ charge?: ItemState;
1184
+ /** Per-instance attribute overrides (mutaplasmid). */
1185
+ attributeOverrides?: Record<number, number>;
1186
+ }
1187
+ declare class ItemState {
1188
+ readonly kind: ItemKind;
1189
+ readonly id: string;
1190
+ readonly type: SdeType;
1191
+ readonly typeID: number;
1192
+ readonly groupID: number;
1193
+ readonly categoryID: number;
1194
+ state: ModuleState;
1195
+ charge: ItemState | null;
1196
+ /** Effect IDs the item carries, populated lazily on first access. */
1197
+ readonly effectIDs: ReadonlySet<number>;
1198
+ readonly attrs: Map<number, ModifiedAttribute>;
1199
+ constructor(init: ItemStateInit);
1200
+ /**
1201
+ * Get-or-create the ModifiedAttribute for this id. Returning a fresh
1202
+ * default-valued instance lets modifiers target attributes that aren't
1203
+ * in the type's typeDogma row yet — e.g. a skill that adds CPU output
1204
+ * to a ship that has no base CPU output (rare but valid in EVE).
1205
+ */
1206
+ attr(id: number, defaultBase?: number): ModifiedAttribute;
1207
+ /** Read-only base value; `undefined` if the type doesn't carry the attr. */
1208
+ getBase(id: number): number | undefined;
1209
+ /** Whether this item's typeDogma actually carries the attribute. Use to
1210
+ * distinguish "missing attr" from "attr present but final value is 0",
1211
+ * since `getFinal` returns 0 for both. */
1212
+ hasAttr(id: number): boolean;
1213
+ /** Compute the current modified value (cached until next addAffliction). */
1214
+ getFinal(id: number, fallback?: number): number;
1215
+ /** `defaultBase` is the seed base value when the target attribute isn't
1216
+ * in the item's typeDogma — needed for SDE attrs whose `defaultValue`
1217
+ * is non-zero (e.g. `missileDamageMultiplier` defaults to 1; without
1218
+ * this, a BCS PreMul affliction would compute 1.1 × 0 = 0 → zero
1219
+ * missile DPS on charges that don't carry attr_212 explicitly). */
1220
+ addAffliction(attributeID: number, a: ModifierAffliction, defaultBase?: number): void;
1221
+ /** Reset every attribute on this item — used for incremental recompute. */
1222
+ resetAttributes(): void;
1223
+ /** Snapshot all attributes — for UI rendering or debug breakdown. */
1224
+ snapshotAll(): Map<number, ReturnType<ModifiedAttribute['snapshot']>>;
1225
+ /** Iterator over (attributeID, ModifiedAttribute) — for the engine. */
1226
+ attributesEntries(): IterableIterator<readonly [number, ModifiedAttribute]>;
1227
+ /** Whether this item's effects are active at the current state. */
1228
+ appliesAtState(effect: SdeEffect): boolean;
1229
+ /**
1230
+ * Determine the slot type of a module by inspecting its effects.
1231
+ * Returns null for non-module items (no slot-classifying effect).
1232
+ */
1233
+ slotType(): SlotType | null;
1234
+ /**
1235
+ * Required skills for using this item, derived from typeDogma's reserved
1236
+ * attribute IDs (REQUIRED_SKILL_N → skillTypeID, REQUIRED_SKILL_N_LEVEL
1237
+ * → minimum level). Used by the skill validator + by skill-source
1238
+ * modifier filtering.
1239
+ */
1240
+ requiredSkills(): Array<{
1241
+ skillID: number;
1242
+ level: number;
1243
+ }>;
1244
+ }
1245
+
1246
+ /**
1247
+ * FitContext aggregates every ItemState participating in a single fit
1248
+ * computation: the ship, the module list, drones, fighters, implants,
1249
+ * boosters, subsystems, the mode (T3D/T3C), the character (skill source),
1250
+ * and an optional projected target for ranged DPS / EWAR projection.
1251
+ *
1252
+ * It also implements the `domain` resolution required by EVE's modifierInfo
1253
+ * model:
1254
+ * - 'self' → the source item itself
1255
+ * - 'shipID' → the ship hull
1256
+ * - 'char' → the character (where skills live)
1257
+ * - 'otherID' → the paired item (charge ↔ module)
1258
+ * - 'targetID' → the projected target (for hostile EWAR)
1259
+ * - 'structureID' → citadel structures (rarely used for fits)
1260
+ *
1261
+ * `LocationGroupModifier` and `LocationRequiredSkillModifier` further filter
1262
+ * within the resolved location — see `targetsForModifier()`.
1263
+ */
1264
+
1265
+ interface FitContextInit {
1266
+ ship: ItemState;
1267
+ character: ItemState;
1268
+ /** Skill levels keyed by skill type id. Missing entries default to 0. */
1269
+ skillLevels: ReadonlyMap<number, number>;
1270
+ modules: ItemState[];
1271
+ drones: ItemState[];
1272
+ fighters: ItemState[];
1273
+ implants: ItemState[];
1274
+ boosters: ItemState[];
1275
+ subsystems: ItemState[];
1276
+ mode?: ItemState;
1277
+ /** Optional projected target. NULL when no target is selected. */
1278
+ target?: ItemState | null;
1279
+ /** Optional citadel/structure context for structure modifierInfo. */
1280
+ structure?: ItemState | null;
1281
+ skillProfile: SkillProfile;
1282
+ /** Dataset reference — needed for transitive skill prerequisite walks
1283
+ * (`itemRequiresSkillTransitive`). The skills bucket on the dataset is
1284
+ * the only place skill type definitions live. */
1285
+ dataset: FittingDataset;
1286
+ /** Triglavian disintegrator spool fraction (0..1). Stored on the
1287
+ * context so derived/offense.ts can compute the spool=0 baseline DPS
1288
+ * for the UI (Min/Max labels) without re-running the engine. Without
1289
+ * this, the slider drag produces a brief race where the spool % has
1290
+ * updated but the engine's output hasn't, and the derived Min/Max
1291
+ * drift visibly until the debounced recompute settles. */
1292
+ disintegratorSpoolPercent: number;
1293
+ }
1294
+ declare class FitContext {
1295
+ readonly ship: ItemState;
1296
+ readonly character: ItemState;
1297
+ readonly skillLevels: ReadonlyMap<number, number>;
1298
+ readonly modules: ItemState[];
1299
+ readonly drones: ItemState[];
1300
+ readonly fighters: ItemState[];
1301
+ readonly implants: ItemState[];
1302
+ readonly boosters: ItemState[];
1303
+ readonly subsystems: ItemState[];
1304
+ readonly mode: ItemState | null;
1305
+ /** Mutable so projection passes can temporarily redirect `targetID`
1306
+ * domain resolution (e.g. when applying hostile EWAR onto your own
1307
+ * fit, target is swapped to ctx.ship). The engine restores it after. */
1308
+ target: ItemState | null;
1309
+ readonly structure: ItemState | null;
1310
+ readonly skillProfile: SkillProfile;
1311
+ readonly dataset: FittingDataset;
1312
+ readonly disintegratorSpoolPercent: number;
1313
+ /** Projected hostile sources currently in scope. Populated by the
1314
+ * engine when ProjectedSource[] is passed in ComputeFitOptions. */
1315
+ projectedSources: ItemState[];
1316
+ /** Effect IDs the dispatcher must skip when applying LOCAL modules
1317
+ * (modules fitted to ctx.ship). Populated by `collectEffectStoppers()`
1318
+ * during the projection pre-pass: each `func: EffectStopper`
1319
+ * modifierInfo on a projected source contributes its `effectID` here.
1320
+ *
1321
+ * Pyfa-parity: warp scrambler / disruptor effects (5928, 5934, 6745,
1322
+ * …) suppress effects 6441 (MWD) and 6442 (MJD) on the target — the
1323
+ * scrambled ship can't activate its prop module. The engine reads
1324
+ * this set inside `applySourceItem` and skips matching effects on
1325
+ * ship-mounted modules. Empty by default = no projected scram. */
1326
+ stoppedLocalEffectIDs: Set<number>;
1327
+ constructor(init: FitContextInit);
1328
+ /** All items that can carry effects + receive modifications. */
1329
+ allItems(): IterableIterator<ItemState>;
1330
+ /** Skill level lookup with default 0 for untrained skills. */
1331
+ skillLevel(skillTypeID: number): number;
1332
+ /**
1333
+ * Resolve the `domain` string of a modifier into the corresponding root
1334
+ * ItemState. The `source` is the item carrying the effect (e.g. the
1335
+ * module whose effect we're applying); `self` resolves to it directly.
1336
+ */
1337
+ resolveDomain(domain: SdeModifierInfo['domain'], source: ItemState): ItemState | null;
1338
+ /** Find which module currently has the given charge loaded. */
1339
+ findChargeParent(charge: ItemState): ItemState | null;
1340
+ /**
1341
+ * Resolve the target list for a modifier — combination of `func` +
1342
+ * `domain` + (optional) `groupID` / `skillTypeID` filter.
1343
+ *
1344
+ * - ItemModifier: applies to the single domain item.
1345
+ * - LocationModifier: applies to the location root (typically the ship)
1346
+ * AND to every item physically located in that location (modules,
1347
+ * drones, etc.) — interpretation depends on context, but most
1348
+ * modifierInfo entries we encounter use it for the root only.
1349
+ * Conservative: target the location root only. The few effects that
1350
+ * actually want "every item in the location" are usually duplicated
1351
+ * as LocationGroupModifier with no group filter — handled there.
1352
+ * - LocationGroupModifier: every item in the location whose groupID
1353
+ * matches modifier.groupID.
1354
+ * - LocationRequiredSkillModifier: every item in the location that
1355
+ * requires the skill modifier.skillTypeID.
1356
+ * - OwnerRequiredSkillModifier: every item owned by the character that
1357
+ * requires the skill — i.e. modules + drones + fighters across the
1358
+ * fit. Used by skill bonuses that should apply regardless of where
1359
+ * the item is mounted.
1360
+ * - EffectStopper: handled outside this resolver (stops other effects
1361
+ * rather than applying a modifier).
1362
+ */
1363
+ targetsForModifier(modifier: SdeModifierInfo, source: ItemState): ItemState[];
1364
+ /**
1365
+ * Items that count as "located in" a given root. The exact set depends
1366
+ * on the root kind:
1367
+ * - Ship: the ship itself + every module + active drone/fighter +
1368
+ * subsystems + mode + charges
1369
+ * - Character: every char-attached item (the character itself,
1370
+ * implants, boosters)
1371
+ * - Anything else: just the root (no spreading)
1372
+ *
1373
+ * This mirrors Pyfa's location semantics.
1374
+ */
1375
+ private itemsInLocation;
1376
+ }
1377
+ /** Charge-loadability check: charge.groupID must match one of the module's
1378
+ * charge group attributes. Used by the editor to validate charge swaps. */
1379
+ declare function moduleAcceptsCharge(module: ItemState, charge: ItemState): boolean;
1380
+
1381
+ /**
1382
+ * Generic modifierInfo dispatcher.
1383
+ *
1384
+ * For every dogma effect that has a populated `modifierInfo` array (~93% of
1385
+ * the SDE), this engine reads each entry, computes the final modifier value,
1386
+ * resolves the target list via the FitContext domain rules, and pushes an
1387
+ * Affliction onto each target's ModifiedAttribute pipeline.
1388
+ *
1389
+ * The remaining ~7% of effects (capacitor sim, weapon DPS, missile DRF,
1390
+ * EWAR projection probability, etc.) bypass this engine and run through
1391
+ * dedicated handlers in `effects/`. Those handlers are dispatched from the
1392
+ * top-level `engine.ts` orchestrator, NOT from here.
1393
+ *
1394
+ * Skill scaling: modifiers tagged `*RequiredSkillModifier` multiply the
1395
+ * source value by the character's skill level (0..5). The bonus value
1396
+ * stored on the source item is the per-level value (e.g. 5%/level →
1397
+ * the modifyingAttributeID points to an attribute whose value is 0.05).
1398
+ *
1399
+ * Stacking penalty key: defaults to `${attributeID}` so multiple modules
1400
+ * affecting the same attribute share a penalty stack. Skill / ship / mode /
1401
+ * subsystem sources bypass the penalty (`stackingGroup === null`) — these
1402
+ * are intrinsic bonuses that EVE does not penalize. The `stackable` flag
1403
+ * on the dogma attribute also bypasses (some attributes are explicitly
1404
+ * additive without penalty).
1405
+ */
1406
+
1407
+ /**
1408
+ * Apply every modifier of every effect on a single source item. Effects are
1409
+ * filtered by the item's current state (only effects active at the current
1410
+ * state contribute). EffectStopper modifiers are NOT applied here — see
1411
+ * `collectEffectStoppers()` for that.
1412
+ */
1413
+ declare function applySourceItem(source: ItemState, ctx: FitContext, dataset: FittingDataset): void;
1414
+ /**
1415
+ * Apply a single modifier descriptor. Pulled out for testability and so the
1416
+ * top-level engine can call it directly when applying skill effects through
1417
+ * the character item (which exposes effects through its skill book typeIDs).
1418
+ */
1419
+ declare function applyOneModifier(source: ItemState, effect: SdeEffect, mi: SdeModifierInfo, ctx: FitContext, dataset: FittingDataset): void;
1420
+ /**
1421
+ * Apply skill bonuses by walking the character's skill set and, for each
1422
+ * skill type, dispatching its passive effects through the modifier engine.
1423
+ *
1424
+ * EVE encodes "skill X gives bonus Y per level" as a passive effect on the
1425
+ * SKILL TYPE itself, with a LocationRequiredSkillModifier modifier whose
1426
+ * `skillTypeID` matches the skill in question. The character item carries a
1427
+ * synthetic effect set that delegates to the loaded skill types.
1428
+ *
1429
+ * This function does NOT mutate the character's effect list — it walks the
1430
+ * skill profile externally and applies skill effects directly on behalf of
1431
+ * the character source. Preserves correct attribution (sourceKind = 'skill').
1432
+ */
1433
+ declare function applySkills(ctx: FitContext, dataset: FittingDataset): void;
1434
+ /** Compute current spool damage bonus (fractional, e.g. 1.5 means +150 %).
1435
+ * Caller-side helper for UI breakdowns; engine-side application uses the
1436
+ * same math via `applyLegacyDisintegratorSpool`. */
1437
+ declare function disintegratorSpoolBonus(maxBonus: number, spoolPercent: number): number;
1438
+ /** Cycles needed to reach full spool — ceil(max / perCycle). Returns 0 when
1439
+ * the weapon has no spool-up (e.g. attrs missing). */
1440
+ declare function disintegratorCyclesToFullSpool(maxBonus: number, bonusPerCycle: number): number;
1441
+ declare const LEGACY_HANDLED_EFFECT_IDS: ReadonlySet<number>;
1442
+
1443
+ /**
1444
+ * Stacking penalty math.
1445
+ *
1446
+ * Per EVE rules: when multiple multiplicative modifiers stack on the same
1447
+ * attribute (and the attribute is NOT marked stackable in the SDE), the most
1448
+ * impactful modifier applies at full strength while subsequent ones are
1449
+ * exponentially attenuated:
1450
+ *
1451
+ * effective_multiplier_i = 1 + (raw_i − 1) × exp(−i² / k)
1452
+ *
1453
+ * where i is the 0-indexed position after sorting by absolute deviation from
1454
+ * 1 (descending) and k = 7.1289.
1455
+ *
1456
+ * Bonuses (raw > 1) and penalties (raw < 1) are penalized as TWO INDEPENDENT
1457
+ * sequences — i.e. the strongest bonus is at full strength even if a stronger
1458
+ * penalty exists in the same group, and vice versa. This mirrors Pyfa's
1459
+ * `eos/calc.py::calculateMultiplier`.
1460
+ *
1461
+ * Skill bonuses, ship hull bonuses, and modules with the `stackable` flag
1462
+ * bypass this penalty entirely — they multiply at face value. The caller
1463
+ * decides whether to penalize via the `stackingGroup` field on the Affliction
1464
+ * (null = no penalty).
1465
+ */
1466
+ interface StackableValue {
1467
+ /** Raw multiplier (1.05 = +5% bonus, 0.9 = -10% penalty). */
1468
+ value: number;
1469
+ }
1470
+ /**
1471
+ * Reduce a list of multipliers from the same stacking group into a single
1472
+ * combined factor. Returns 1 for an empty list.
1473
+ */
1474
+ declare function combinePenalized(values: readonly StackableValue[]): number;
1475
+ /**
1476
+ * Combine UNSTACKED multiplicative modifiers — straight product, no penalty.
1477
+ */
1478
+ declare function combineUnstacked(values: readonly StackableValue[]): number;
1479
+ /**
1480
+ * Group a flat list of {value, stackingGroup} entries by their stacking
1481
+ * group key, then combine each group via the appropriate path.
1482
+ *
1483
+ * stackingGroup === null means "no penalty for this entry, multiply directly".
1484
+ * Other keys denote a penalty group (typically the attribute id, or
1485
+ * source-type:attribute).
1486
+ */
1487
+ declare function combineMultiplicative(entries: readonly {
1488
+ value: number;
1489
+ stackingGroup: string | null;
1490
+ }[]): number;
1491
+
1492
+ /**
1493
+ * Effective HitPoints (EHP) calculation.
1494
+ *
1495
+ * EVE damage application per layer:
1496
+ * damage_taken = damage_dealt × (1 - resistance)
1497
+ *
1498
+ * For incoming damage with a known type distribution (DamageProfile):
1499
+ * absorbed_per_layer = sum over types: weight × HP / (1 - resistance_type)
1500
+ *
1501
+ * Equivalent and easier to compute:
1502
+ * effective_resistance = sum(weight × resistance_type)
1503
+ * ehp_layer = HP / (1 - effective_resistance)
1504
+ *
1505
+ * `ehpUniform` uses a uniform 25% per type (used as a default badge in the UI).
1506
+ * `ehpAgainstProfile` uses the user-supplied DamageProfile weights.
1507
+ *
1508
+ * Important: the SDE stores resistances as RESONANCE values (0..1) where
1509
+ * 0 = full resist and 1 = no resist. The fitting engine consumes those raw,
1510
+ * but the UI prefers the inverted "resist %" form. Conversion happens here:
1511
+ * resist_percent = 1 - resonance
1512
+ */
1513
+
1514
+ type DefenseLayerKind = 'SHIELD' | 'ARMOR' | 'HULL';
1515
+ interface LayerEhp {
1516
+ hp: number;
1517
+ /** EHP under uniform 25%/type damage. Pyfa's "Uniform" reference. */
1518
+ ehpUniform: number;
1519
+ /** EHP under the supplied profile (or uniform when no profile is given). */
1520
+ ehpAgainstProfile: number;
1521
+ /** Resist percentages (0..1) for the UI. 1 - resonance. */
1522
+ resistances: {
1523
+ em: number;
1524
+ thermal: number;
1525
+ kinetic: number;
1526
+ explosive: number;
1527
+ };
1528
+ }
1529
+ /**
1530
+ * Compute EHP for a single defense layer. Caller passes the ship state and
1531
+ * a DamageProfile; both omni-EHP and profile-EHP are returned together so
1532
+ * the UI can render a comparison.
1533
+ */
1534
+ declare function computeLayerEhp(ship: ItemState, layer: DefenseLayerKind, profile?: DamageProfile | null): LayerEhp;
1535
+ /**
1536
+ * Pure math: HP under a damage profile. Resonances passed verbatim from the
1537
+ * SDE (0 = full resist, 1 = no resist).
1538
+ *
1539
+ * Returns Infinity if the layer is mathematically immune (resonance=0
1540
+ * across all types with non-zero weight). The caller should clamp to a
1541
+ * sensible display value.
1542
+ */
1543
+ declare function ehpUnderProfile(hp: number, resonanceEm: number, resonanceThermal: number, resonanceKinetic: number, resonanceExplosive: number, profile: DamageProfile): number;
1544
+ /**
1545
+ * Combined EHP across all three layers. The "total" EHP a ship can absorb
1546
+ * before going pop is the sum of shield + armor + hull EHP under the same
1547
+ * damage profile (they're consumed sequentially in EVE).
1548
+ */
1549
+ declare function computeTotalEhp(shield: LayerEhp, armor: LayerEhp, hull: LayerEhp, useProfile: boolean): number;
1550
+
1551
+ /**
1552
+ * Capacitor simulation.
1553
+ *
1554
+ * EVE's capacitor follows a non-linear S-shaped recharge curve. Per the
1555
+ * canonical formula (Pyfa / EVE-Wiki):
1556
+ *
1557
+ * dC/dt = (10 × C_max / τ) × (√x − x) where x = C / C_max
1558
+ *
1559
+ * τ is the `capacitorRechargeRate` attribute IN MILLISECONDS. The factor
1560
+ * stems from Newton-fit empirical data: peak recharge rate occurs at
1561
+ * x = 0.25 (i.e. cap at 25% capacity) and equals 2.5 × C_max / τ_seconds.
1562
+ *
1563
+ * Stability check: a fit is "cap stable" if at some level x ∈ (0, 1) the
1564
+ * recharge rate equals the steady-state usage rate. Solving for that level:
1565
+ *
1566
+ * usage = (10 × C_max / τ) × (√x − x)
1567
+ * ⇒ √x − x = u where u = usage × τ / (10 × C_max)
1568
+ *
1569
+ * The function (√x − x) is concave on [0, 1] with maximum 0.25 at x=0.25,
1570
+ * so a solution exists ⇔ u ≤ 0.25, i.e. usage ≤ 2.5 × C_max / τ. The
1571
+ * smaller of the two roots is the *unstable* equilibrium (cap drops to it
1572
+ * if it dips below 25%); the larger root is the stable equilibrium and is
1573
+ * what the UI calls "cap stable at X%".
1574
+ *
1575
+ * If the fit is NOT stable, we report seconds-to-empty: integration of
1576
+ * dC/dt from 100% down to 0% under the assumption that drain rate is
1577
+ * constant at `usage` (a slight under-estimate; the true integral is
1578
+ * shorter because the recharge contribution decreases as cap drops).
1579
+ *
1580
+ * Module drain: each module's effect with a `dischargeAttributeID` reads
1581
+ * its drain per cycle from that attribute, and its cycle duration from
1582
+ * `durationAttributeID`. Drain per second = discharge / (duration / 1000).
1583
+ * Only modules in ACTIVE / OVERLOAD state contribute (offline + online +
1584
+ * passive don't drain).
1585
+ *
1586
+ * --- Cap booster handling: discrete-event simulation (Pyfa parity) ---
1587
+ *
1588
+ * The naïve closed-form approach amortises booster injection across the
1589
+ * (charges × cycle + reload) window and treats it as a constant negative
1590
+ * drain. That's wrong by 5-15 % for fits with a heavy cap booster + high
1591
+ * net injection: in reality, booster activations that would push cap above
1592
+ * 100 % are POSTPONED (`awaitingInjectors` queue in Pyfa's `capSim.py`).
1593
+ * The deferred injection is applied later when cap dips low enough to
1594
+ * absorb it without overshoot.
1595
+ *
1596
+ * The closed-form ignores this overshoot loss and reports a higher cap
1597
+ * stable % than reality. Apoc Navy + Heavy F-RX shows 93 % closed-form vs
1598
+ * 79.9 % Pyfa — 13 pp gap.
1599
+ *
1600
+ * Fix: when any active module has a cap booster effect (effectID 48), run
1601
+ * a discrete-event simulator that mirrors Pyfa's `CapSimulator`:
1602
+ * - Min-heap of (t_now, duration, capNeed, shot, clipSize, reloadTime,
1603
+ * isInjector) tuples (negative capNeed for boosters = injection).
1604
+ * - At each event, regenerate cap analytically since the previous event,
1605
+ * postpone overshoot injectors, drain/inject, advance.
1606
+ * - Stop when (cap_now ≥ cap_at_period_start AND awaiting injectors
1607
+ * match) — the system is stable. Or when cap < 0 (unstable).
1608
+ *
1609
+ * UI metric matches Pyfa: `capState = (cap_low + cap_low_pre) / (2·C_max)`
1610
+ * — average of the post-drain low watermark and the pre-drain low
1611
+ * watermark. Cycle-average operating cap level, which is what writers
1612
+ * actually care about (the closed-form equilibrium is where rate goes to
1613
+ * zero, NOT the average level under a periodic drain schedule).
1614
+ *
1615
+ * Trap encountered & avoided. The OLD per-load amortisation
1616
+ * rate = N × (capNeed - inject) / (N × cycle + reload)
1617
+ * is mathematically correct as the *long-run mean injection rate* but the
1618
+ * cap-stable solver assumes that mean is delivered every instant — which
1619
+ * over-credits the booster because postponed injections lose effective
1620
+ * value (they get clipped at 100 % cap).
1621
+ */
1622
+
1623
+ interface CapacitorReport {
1624
+ /** Max capacitor capacity (GJ). */
1625
+ capacity: number;
1626
+ /** Recharge time τ in milliseconds (raw SDE value). */
1627
+ rechargeMs: number;
1628
+ /** Peak recharge rate, GJ/s, achieved at 25% capacity. */
1629
+ peakRechargeRate: number;
1630
+ /** Total active drain across all online/active modules, GJ/s. Reported
1631
+ * as the *gross* drain (before accounting for cap booster injection)
1632
+ * when the fit has injectors. */
1633
+ usagePerSecond: number;
1634
+ /** True iff a stable equilibrium exists. */
1635
+ stable: boolean;
1636
+ /** Equilibrium cap level (0..1) when stable. Always ≥ 0.25 in the
1637
+ * closed-form path; in the simulator path this is Pyfa's
1638
+ * (cap_low + cap_low_pre) / (2·capacity). */
1639
+ stablePercent: number;
1640
+ /** Time until cap reaches 0 from full, in seconds. Only meaningful when
1641
+ * not stable; undefined when stable. */
1642
+ secondsToEmpty?: number;
1643
+ }
1644
+ declare function computeCapacitor(ctx: FitContext, dataset: FittingDataset): CapacitorReport;
1645
+ /**
1646
+ * Peak passive recharge rate at 25% capacity. Used for both the capacitor
1647
+ * and (with shield_capacity / shield_recharge_rate as inputs) the passive
1648
+ * shield regen.
1649
+ *
1650
+ * rate(x) = (10 × C / τ) × (√x − x)
1651
+ * peak occurs at x = 0.25 → rate_max = 2.5 × C / τ_seconds
1652
+ */
1653
+ declare function peakRecharge(capacity: number, rechargeMs: number): number;
1654
+ /**
1655
+ * Recharge rate at an arbitrary cap level (0..1). Useful for time-domain
1656
+ * simulations / charts.
1657
+ */
1658
+ declare function rechargeRateAt(capacity: number, rechargeMs: number, fillFraction: number): number;
1659
+
1660
+ /**
1661
+ * Tank rate aggregation.
1662
+ *
1663
+ * For each defense layer (shield / armor / hull) we report:
1664
+ * - Single-cycle repair amount (sum of all repair modules' per-cycle output)
1665
+ * - Single-cycle duration (the longest cycle among the modules contributing
1666
+ * — for now we use the average of cycles which is a fair shorthand when
1667
+ * they're roughly synchronized; refining to a proper time-weighted
1668
+ * simulation is Phase 5+ territory)
1669
+ * - Sustained repair-per-second (sum of amount/cycle across all reppers)
1670
+ *
1671
+ * Plus shield-specific:
1672
+ * - Passive peak shield regen (2.5 × shield_capacity / shield_recharge_seconds)
1673
+ *
1674
+ * Repair handlers are recognised by effect id via REPAIR_EFFECT_AMOUNT_ATTR
1675
+ * — that map is the (small) hand-coded table this engine maintains.
1676
+ * Modules in OFFLINE / ONLINE state don't contribute (you have to actually
1677
+ * be cycling the rep to gain HP back), only ACTIVE / OVERLOAD.
1678
+ *
1679
+ * Ancillary shield/armor reps are treated as plain reps in this iteration:
1680
+ * their FUELED variant doubles output when loaded with a charge, but the
1681
+ * doubling is conditional on cap drain semantics (no cap = doubled rate).
1682
+ * Modelling that requires the cap simulator output and proper charge
1683
+ * tracking — Phase 5 task. For now, ancillary reps report their unfueled
1684
+ * baseline.
1685
+ */
1686
+
1687
+ interface TankRates {
1688
+ shieldRepairAmount: number;
1689
+ shieldRepairDuration: number;
1690
+ shieldRepairPerSecond: number;
1691
+ /** Sustained shield rep/sec amortised across reload windows for fueled
1692
+ * reppers (paste-loaded AAR or cap-charge-loaded ASB). Equals
1693
+ * `shieldRepairPerSecond` when no fuel-bound repper is contributing. */
1694
+ shieldRepairPerSecondSustained: number;
1695
+ armorRepairAmount: number;
1696
+ armorRepairDuration: number;
1697
+ armorRepairPerSecond: number;
1698
+ armorRepairPerSecondSustained: number;
1699
+ hullRepairAmount: number;
1700
+ hullRepairDuration: number;
1701
+ hullRepairPerSecond: number;
1702
+ hullRepairPerSecondSustained: number;
1703
+ /** Peak passive shield regen (GJ/s ≡ HP/s for shields). */
1704
+ passiveShieldRegenPeak: number;
1705
+ }
1706
+ declare function computeTank(ctx: FitContext): TankRates;
1707
+
1708
+ /**
1709
+ * Offense aggregation — turret + missile + smart-bomb + drone DPS.
1710
+ *
1711
+ * For each fitted module + active drone we:
1712
+ * 1. Classify it as a weapon (effects/weapon.ts)
1713
+ * 2. Read damage components from the right source (charge for turrets +
1714
+ * missiles, item itself for smart bombs + drones)
1715
+ * 3. Compute raw DPS = (sum_damage × damageMultiplier) / cycle_seconds
1716
+ * 4. Compute reload-aware sustained DPS by amortising the per-load
1717
+ * damage volley over (cycles_in_load + reload_seconds). When no
1718
+ * charge or reload time, sustained equals peak.
1719
+ *
1720
+ * Reload model:
1721
+ * - Modules with effect 10/34/67/101/6995 (Pyfa-parity legacy reload
1722
+ * effects) get a 1000 ms default reload when no `reloadTime` attribute.
1723
+ * - Charges-per-load = floor(launcher.capacity / charge.volume) for
1724
+ * weapons; for cap boosters Pyfa uses the same formula.
1725
+ * - Time-per-load = chargesPerLoad × cycleSeconds + reloadSeconds.
1726
+ * - Sustained DPS = (chargesPerLoad × alpha) / timePerLoad.
1727
+ *
1728
+ * What's intentionally still simplified:
1729
+ * - No tracking application: turret tracking against target velocity /
1730
+ * sig would multiply DPS by hit-quality < 1. We expose tracking +
1731
+ * range attributes so the UI can show them, but DPS reported is the
1732
+ * optimal-range no-tracking-loss baseline.
1733
+ * - No DRF application for missiles: same reasoning.
1734
+ * - Drone control range / EWAR projection / fighter ability cooldowns:
1735
+ * all stubbed.
1736
+ * - Resists vs damage profile: not applied here; the offense view shows
1737
+ * RAW damage components, the defense view shows EHP-vs-profile.
1738
+ */
1739
+
1740
+ interface OffenseReport {
1741
+ weaponDps: number;
1742
+ /** Reload-amortised weapon DPS — for short-magazine weapons (lasers,
1743
+ * cap boosters) this is meaningfully lower than `weaponDps`. */
1744
+ weaponSustainedDps: number;
1745
+ droneDps: number;
1746
+ fighterDps: number;
1747
+ totalDps: number;
1748
+ totalSustainedDps: number;
1749
+ /** Single-volley alpha across all weapons (no synchronization assumed —
1750
+ * this is "if every gun fired right now, total damage applied"). */
1751
+ alphaStrike: number;
1752
+ /** Effective optimal range — minimum of any weapon's optimal that
1753
+ * contributes meaningful DPS. UI calls it "max engagement range". */
1754
+ weaponOptimal: number;
1755
+ weaponFalloff: number;
1756
+ weaponTracking?: number;
1757
+ explosionVelocity?: number;
1758
+ explosionRadius?: number;
1759
+ breakdown: WeaponContribution[];
1760
+ }
1761
+ declare function computeOffense(ctx: FitContext, dataset: FittingDataset, fit: {
1762
+ drones: Array<{
1763
+ id: string;
1764
+ typeID: number;
1765
+ countTotal: number;
1766
+ countActive: number;
1767
+ }>;
1768
+ fighters?: Array<{
1769
+ id: string;
1770
+ typeID: number;
1771
+ count: number;
1772
+ abilityState?: Record<number, boolean>;
1773
+ }>;
1774
+ }): OffenseReport;
1775
+
1776
+ /**
1777
+ * Upwell-structure metadata: service slots + fuel-block consumption.
1778
+ *
1779
+ * Computed only when the host typeID resolves to a category=65 type
1780
+ * (Astrahus / Raitaru / Fortizar / etc.). Ship fits skip this entirely
1781
+ * and the field is reported as null on `ComputedFit.derived.structure`.
1782
+ *
1783
+ * Pyfa parity:
1784
+ * - `serviceSlotsMax` from the structure hull's attr 2056 (`serviceSlots`)
1785
+ * - `serviceModuleFuelAmount` (attr 2109) is the per-hour fuel block
1786
+ * cost of an ONLINE service module. We sum it across modules whose
1787
+ * state is ONLINE / ACTIVE / OVERLOAD (OFFLINE modules are anchored
1788
+ * but not contributing to fuel burn).
1789
+ * - The `serviceModuleFuelOnlineAmount` (attr 2110) attribute is the
1790
+ * one-time onlining cost (not surfaced in the headline panel).
1791
+ */
1792
+
1793
+ declare function computeStructureMeta(ctx: FitContext): StructureMeta | null;
1794
+
1795
+ /**
1796
+ * Damage application against a target — what fraction of a weapon's
1797
+ * theoretical DPS actually hits given the target's signature radius,
1798
+ * speed and the engagement range. Used by the "Stats vs Target" panel
1799
+ * and the DPS-over-range graph.
1800
+ *
1801
+ * Two regimes:
1802
+ * - TURRETS: chance-to-hit = max(0, 0.5^(range_factor² + tracking_factor²))
1803
+ * where range_factor = max(0, range - optimal) / falloff
1804
+ * tracking_factor = (angular_velocity / tracking) × (sig_resolution / sig_radius)
1805
+ * For a stationary target we set angular_velocity = 0 so only the
1806
+ * range factor matters.
1807
+ *
1808
+ * - MISSILES: applied_damage = base × min(1, sig_radius / explosion_radius)
1809
+ * × min(1, (sig × explosion_velocity) / (target_velocity × explosion_radius))^drf
1810
+ * The first term is the sig-radius drop-off, the second is the
1811
+ * velocity drop-off raised to the missile's damage reduction factor.
1812
+ *
1813
+ * For drones we approximate as turrets at 0 angular velocity.
1814
+ */
1815
+
1816
+ /** Apply hit-chance / sig-and-velocity falloff to a weapon's DPS at the
1817
+ * given engagement range, against the supplied target profile. Returns
1818
+ * the effective DPS after application losses. */
1819
+ declare function effectiveDps(weapon: WeaponContribution, target: TargetProfile | null, rangeMeters: number): number;
1820
+
1821
+ /**
1822
+ * Weapon effect inspection — given an ItemState (a fitted module or a drone),
1823
+ * classify the kind of weapon and extract the canonical attributes used by
1824
+ * the offense aggregator: damage components, cycle time, range, falloff,
1825
+ * tracking, alpha multiplier, missile sig/velocity.
1826
+ *
1827
+ * What this file deliberately does NOT do:
1828
+ * - Apply tracking against a target (offense.ts handles that — this file
1829
+ * only surfaces the raw attributes)
1830
+ * - Compute applied DPS — same reason
1831
+ * - Iterate the fit — caller picks one item and asks "is this a weapon?"
1832
+ *
1833
+ * The damage source for turrets/missiles is the LOADED CHARGE, not the
1834
+ * launcher. For smart bombs it's the launcher itself (no charge slot).
1835
+ * Drones carry damage attributes baked into the drone type.
1836
+ */
1837
+
1838
+ interface WeaponClassification {
1839
+ kind: WeaponEffectKind;
1840
+ /** The dogma effect that classified this item as a weapon. Carries the
1841
+ * duration / range / falloff / tracking attribute references. */
1842
+ effectID: number;
1843
+ /** ItemState whose attributes provide the damage components. For turrets
1844
+ * + missiles this is the loaded charge; for smart bombs it's the
1845
+ * module itself; for drones it's the drone. May be null when the
1846
+ * weapon has no ammo loaded → caller treats as zero damage. */
1847
+ damageSource: ItemState | null;
1848
+ }
1849
+ /**
1850
+ * Detect the primary weapon effect on an item. Returns null if the item
1851
+ * isn't a weapon. If multiple weapon effects are present (rare — usually
1852
+ * only on items with both turret + smart-bomb effects historically), the
1853
+ * first match wins by `WEAPON_EFFECT_KIND` lookup order.
1854
+ */
1855
+ declare function classifyWeapon(item: ItemState): WeaponClassification | null;
1856
+ interface WeaponDamageComponents {
1857
+ em: number;
1858
+ thermal: number;
1859
+ kinetic: number;
1860
+ explosive: number;
1861
+ total: number;
1862
+ }
1863
+ /**
1864
+ * Read the four damage components from a damage source. Empty (no ammo)
1865
+ * yields all-zero. Turret damage multiplier is NOT applied here — that's
1866
+ * the offense aggregator's concern (the multiplier sits on the LAUNCHER,
1867
+ * not on the charge).
1868
+ */
1869
+ declare function readDamageComponents(source: ItemState | null): WeaponDamageComponents;
1870
+ interface WeaponCycleInfo {
1871
+ cycleSeconds: number;
1872
+ /** Damage multiplier applied per cycle. 1.0 for missiles / smart bombs;
1873
+ * the launcher's `damageMultiplier` (attr 64) for turrets. Skill +
1874
+ * module bonuses already baked in via the modifier engine. */
1875
+ damageMultiplier: number;
1876
+ }
1877
+ declare function readCycleInfo(item: ItemState, kind: WeaponEffectKind): WeaponCycleInfo;
1878
+ interface WeaponRangeInfo {
1879
+ /** Optimal range in meters. 0 for missiles + smart bombs (range is
1880
+ * tied to flight time × velocity for missiles). */
1881
+ optimal: number;
1882
+ /** Falloff in meters — turret-specific. */
1883
+ falloff: number;
1884
+ /** Tracking speed (rad/s) — turret-specific. */
1885
+ tracking: number;
1886
+ /** Smart-bomb burst range (also used as effective hard cap). */
1887
+ burstRange: number;
1888
+ /** Missile-specific: explosion radius (m). */
1889
+ explosionRadius: number;
1890
+ /** Missile-specific: explosion velocity (m/s). */
1891
+ explosionVelocity: number;
1892
+ /** Missile-specific: damage reduction factor (DRF). */
1893
+ drf: number;
1894
+ }
1895
+ declare function readRangeInfo(item: ItemState, effect: SdeEffect, kind: WeaponEffectKind): WeaponRangeInfo;
1896
+
1897
+ /**
1898
+ * EWAR (Electronic Warfare) classifier.
1899
+ *
1900
+ * Identifies hostile modules that project an electronic effect onto a target
1901
+ * and surfaces the per-effect data needed to render an accurate "under
1902
+ * pressure" view:
1903
+ * - ECM: stochastic (jam chance per cycle)
1904
+ * - Sensor Dampener: deterministic (lock range / scan res reduction)
1905
+ * - Tracking Disruptor: deterministic (turret tracking / range reduction)
1906
+ * - Stasis Web: deterministic (max velocity reduction)
1907
+ * - Warp Scrambler/Disr: deterministic (warp ability)
1908
+ * - Energy Neutralizer: deterministic (cap drain on target)
1909
+ * - Energy Vampire: deterministic (cap transfer to source)
1910
+ *
1911
+ * The deterministic effects are already handled by the generic modifier
1912
+ * engine (their modifierInfo applies via `domain: targetID`). This file
1913
+ * exists to:
1914
+ * 1. Classify a module as EWAR for UI rendering
1915
+ * 2. Compute the stochastic ECM jam chance (which has no modifierInfo
1916
+ * equivalent — it's read against the target's sensor strengths)
1917
+ *
1918
+ * ECM jam chance formula (canonical):
1919
+ * per_type_chance = ecm_strength_per_type / target_sensor_strength_per_type
1920
+ * per_cycle_chance = max across the four types
1921
+ *
1922
+ * Each ECM module fires once per cycle, with a per-cycle probability equal
1923
+ * to `per_cycle_chance` clamped to [0, 1]. Multiple ECMs combine via:
1924
+ * combined = 1 - product(1 - p_i)
1925
+ */
1926
+
1927
+ type EwarKind = 'ECM' | 'SENSOR_DAMP' | 'TRACKING_DISRUPT' | 'WEB' | 'WARP_SCRAM' | 'WARP_DISRUPT' | 'NEUT' | 'NOS' | 'OTHER';
1928
+ /**
1929
+ * Identify an EWAR module. Returns null if the module isn't EWAR.
1930
+ */
1931
+ declare function classifyEwar(item: ItemState): {
1932
+ kind: EwarKind;
1933
+ effectID: number;
1934
+ } | null;
1935
+ /**
1936
+ * Compute a single ECM module's per-cycle jam chance against a given target.
1937
+ * Picks the maximum of the four sensor-type chances (RADAR/LADAR/MAG/GRAV).
1938
+ *
1939
+ * Returns 0 if the source lacks ECM strength attributes (defensive fallback).
1940
+ */
1941
+ declare function ecmJamChance(source: ItemState, target: ItemState): number;
1942
+ /**
1943
+ * Combine independent per-cycle jam chances across multiple ECM modules
1944
+ * into a single combined "any jam this cycle" probability via:
1945
+ * 1 - product(1 - p_i)
1946
+ */
1947
+ declare function combineJamChances(chances: readonly number[]): number;
1948
+
1949
+ /**
1950
+ * EVE Market-window taxonomy walker.
1951
+ *
1952
+ * The picker UIs render the same tree EVE shows in-game (Modules >
1953
+ * Capacitor Modules > Capacitor Battery, Modules > Hybrid Weapons >
1954
+ * Small Hybrid Turret, Drones > Combat Drones > Light Combat) by
1955
+ * walking each type's `marketGroupID` parent chain. Live engine
1956
+ * code never touches market groups — they're a pure presentation
1957
+ * concern routed through this helper.
1958
+ */
1959
+
1960
+ interface MarketGroupPlacement {
1961
+ /** Stable category key for collapse/expand state persistence. */
1962
+ categoryKey: string;
1963
+ /** Visible category label (top-level visible bucket in the picker). */
1964
+ categoryName: string;
1965
+ /** Stable subgroup key — also feeds the deterministic hash used
1966
+ * for open/close persistence within the picker. */
1967
+ subGroupKey: string;
1968
+ /** Visible subgroup label (the leaf bucket directly containing
1969
+ * the items). */
1970
+ subGroupName: string;
1971
+ }
1972
+ /**
1973
+ * Map a type to a (category, subgroup) pair using the Market-window
1974
+ * tree.
1975
+ *
1976
+ * - Walks `t.marketGroupID` up to the root via `parentGroupID`.
1977
+ * - Subgroup = the leaf (closest to the item) — that's what
1978
+ * in-game shows directly under each item.
1979
+ * - Category = parent-of-leaf when the chain has ≥ 2 levels;
1980
+ * otherwise the leaf itself (top-level Market entries like
1981
+ * "Apparel" sit there).
1982
+ * - Falls back to "Other / <SDE group name>" when the type carries
1983
+ * no `marketGroupID` (skill books, system effects, …).
1984
+ */
1985
+ declare function marketGroupPlacement(t: SdeType, dataset: FittingDataset): MarketGroupPlacement;
1986
+
1987
+ /**
1988
+ * T3 Cruiser visual-variant resolver for the EstamelGG/EVE_Model_Gallery.
1989
+ *
1990
+ * The repo ships per-subsystem-combination models named like
1991
+ * `<typeID>_<ShipName><dddd>_lite.glb`. Each digit is the 1..3 rank of
1992
+ * the subsystem fitted in that slot. The gallery's digit order is:
1993
+ *
1994
+ * digit 1 = Core slot (groupID 958)
1995
+ * digit 2 = Defensive slot (groupID 954)
1996
+ * digit 3 = Offensive slot (groupID 956)
1997
+ * digit 4 = Propulsion slot (groupID 957)
1998
+ *
1999
+ * Within each slot the gallery's rank-1/2/3 ordering doesn't follow
2000
+ * typeID, marketGroupID or any other obvious sort key — it appears to be
2001
+ * the editor's hand-picked order from the gallery repo. We therefore
2002
+ * encode the mapping explicitly per subsystem typeID; unknown
2003
+ * subsystems return null so the loader falls back to the base hull.
2004
+ */
2005
+
2006
+ /** Returns the 4-digit variant code for a fully-fitted T3C, or null when
2007
+ * the ship isn't a T3C, the dataset isn't ready, any subsystem slot is
2008
+ * empty, or any fitted subsystem isn't in the explicit rank table (the
2009
+ * gallery only ships verified combinations; an unknown rank would yield
2010
+ * a wrong filename). */
2011
+ declare function computeT3CVariantCode(shipTypeID: number, subsystems: ReadonlyArray<FitSubsystem>, dataset: FittingDataset | null | undefined): string | null;
2012
+
2013
+ /**
2014
+ * Skill prerequisite analysis. Walks every fitted item (modules, drones,
2015
+ * fighters, charges, implants, boosters, subsystems) and aggregates the
2016
+ * unmet skill requirements against the active SkillProfile.
2017
+ */
2018
+
2019
+ interface SkillRequirement {
2020
+ skillID: number;
2021
+ skillName: string;
2022
+ requiredLevel: number;
2023
+ currentLevel: number;
2024
+ }
2025
+ interface SkillCheckResult {
2026
+ /** Items that can't be used because at least one required skill is below
2027
+ * the level dictated by the SDE. */
2028
+ unmet: Array<{
2029
+ sourceTypeID: number;
2030
+ sourceName: string;
2031
+ sourceKind: 'module' | 'charge' | 'drone' | 'fighter' | 'implant' | 'booster' | 'subsystem';
2032
+ requirements: SkillRequirement[];
2033
+ }>;
2034
+ /** All distinct skills required by anything in the fit, with the
2035
+ * highest level demanded across the whole fit. */
2036
+ aggregated: Map<number, {
2037
+ skillName: string;
2038
+ requiredLevel: number;
2039
+ currentLevel: number;
2040
+ }>;
2041
+ }
2042
+ declare function checkSkills(fit: Fit, dataset: FittingDataset, profile: SkillProfile): SkillCheckResult;
2043
+
2044
+ /**
2045
+ * Built-in DamageProfile and TargetProfile presets. These match the
2046
+ * canonical values Pyfa ships, sourced from the EVE Online static data
2047
+ * (NPC race damage distribution + fleet-doctrine targets).
2048
+ *
2049
+ * Presets are read-only; the editor can save custom profiles to the
2050
+ * `damage_profiles` / `target_profiles` Prisma tables, but the presets
2051
+ * here always work even before any DB row exists.
2052
+ */
2053
+
2054
+ declare const DAMAGE_PROFILE_PRESETS: ReadonlyArray<DamageProfile>;
2055
+ declare const TARGET_PROFILE_PRESETS: ReadonlyArray<TargetProfile>;
2056
+
2057
+ /**
2058
+ * Fit-restriction predicates: given a candidate module + a target ship,
2059
+ * return whether the module is allowed by EVE's hard fitting rules.
2060
+ *
2061
+ * What this enforces (module won't physically fit if any check fails):
2062
+ * - Slot type compatibility (HI / MED / LO / RIG / SUBSYSTEM / SERVICE)
2063
+ * - canFitShipGroup1-9: module → ship group whitelist
2064
+ * - canFitShipType1-4: module → ship type whitelist
2065
+ * - rigSize: rig must match the ship's rig size class
2066
+ * - Turret hardpoint count: turret weapons need ship.turretHardpoints > 0
2067
+ * - Launcher hardpoint count: missile launchers need ship.launcherHardpoints > 0
2068
+ * - Subsystem fitsToShipType: subsystem (categoryID 32) limited to its T3C parent
2069
+ *
2070
+ * What this does NOT enforce (these are soft warnings, not picker filters):
2071
+ * - CPU / Power Grid availability (a fit can be over-budget temporarily)
2072
+ * - Calibration cost (rigs)
2073
+ * - Skill prerequisites
2074
+ * - maxGroupFitted (only one of group X allowed) — surfaced as warning later
2075
+ *
2076
+ * Soft warnings belong on the fitted module, not on the picker.
2077
+ */
2078
+
2079
+ /** Does this type carry an effect that maps to the given slot type? */
2080
+ declare function typeFitsSlotType(t: SdeType, slot: SlotType): boolean;
2081
+ /** True if the module is a turret weapon (laser / projectile / hybrid). */
2082
+ declare function isTurretWeapon(t: SdeType): boolean;
2083
+ /** True if the module is a missile launcher (any missile launching effect). */
2084
+ declare function isMissileLauncher(t: SdeType): boolean;
2085
+ /** True if the module is a smart bomb / AoE high-slot (no hardpoint needed). */
2086
+ declare function isSmartBomb(t: SdeType): boolean;
2087
+ /**
2088
+ * Collect all canFitShipGroup1-9 values declared on a module. Empty list
2089
+ * means no group restriction.
2090
+ */
2091
+ declare function shipGroupRestrictions(t: SdeType): number[];
2092
+ /**
2093
+ * Collect all canFitShipType1-4 values. Empty list = no type restriction.
2094
+ */
2095
+ declare function shipTypeRestrictions(t: SdeType): number[];
2096
+ /** Read `maxGroupFitted` (attr 1544) — undefined if no per-group cap. */
2097
+ declare function maxGroupFittedFor(mod: SdeType): number | undefined;
2098
+ /** Read `maxTypeFitted` (attr 2487) — undefined if no per-typeID cap. */
2099
+ declare function maxTypeFittedFor(mod: SdeType): number | undefined;
2100
+ /**
2101
+ * How many more copies of `mod` the ship can still accept under the per-
2102
+ * group / per-type fitting caps (`maxGroupFitted` / `maxTypeFitted`),
2103
+ * given the current fit. Returns `Infinity` when the module declares no
2104
+ * cap. The caller does the slot-availability arithmetic separately
2105
+ * (`freeHardpointsFor`); this only enforces the EVE-wide "max N of this
2106
+ * group/type per hull" rule.
2107
+ *
2108
+ * Example: Medium Breacher Pod Launcher has `maxGroupFitted = 1`. Even on
2109
+ * a Cenotaph with 3 launcher hardpoints, only ONE breacher launcher may
2110
+ * be fitted — the remaining hardpoints stay open for non-breacher
2111
+ * launchers (in practice the Cenotaph's `canFitShipType1` restriction on
2112
+ * the launcher means non-breacher launchers can't replace it; the cap is
2113
+ * the effective "no second breacher" rule).
2114
+ */
2115
+ declare function freeFitGroupSlotsFor(mod: SdeType, fittedModules: Array<{
2116
+ typeID: number;
2117
+ }>, dataset: {
2118
+ getType(id: number): SdeType | undefined;
2119
+ }): number;
2120
+ /**
2121
+ * Master predicate: can this module physically fit on this ship?
2122
+ *
2123
+ * @param mod The candidate module / rig / subsystem.
2124
+ * @param ship The hull. Must be a category-6 SdeType.
2125
+ * @param slot Slot the user is trying to fill (HIGH/MED/LO/RIG/SUBSYSTEM/SERVICE).
2126
+ * @param fitContext Optional current fit + dataset reference. When
2127
+ * supplied, the predicate ALSO enforces
2128
+ * `maxGroupFitted` / `maxTypeFitted` against the
2129
+ * existing modules (one Breacher Pod Launcher per
2130
+ * ship, one Bastion Module per ship, …). Omit for the
2131
+ * "is this combo even possible?" stateless check.
2132
+ */
2133
+ declare function canFitModuleOnShip(mod: SdeType, ship: SdeType | null | undefined, slot: SlotType, fitContext?: {
2134
+ fittedModules: Array<{
2135
+ typeID: number;
2136
+ }>;
2137
+ dataset: {
2138
+ getType(id: number): SdeType | undefined;
2139
+ };
2140
+ }): {
2141
+ ok: boolean;
2142
+ reason?: string;
2143
+ };
2144
+ /**
2145
+ * Returns the set of charge groupIDs accepted by this module — i.e. any
2146
+ * non-zero CHARGE_GROUP_1..5 attribute. Empty set means the module
2147
+ * doesn't take a charge (e.g. damage control, prop mod, smart bomb).
2148
+ */
2149
+ declare function chargeGroupsForModule(mod: SdeType): number[];
2150
+ /** True if the module declares any chargeGroup attribute — i.e. it takes
2151
+ * ammo / a script / cap booster charges / etc. */
2152
+ declare function moduleAcceptsAnyCharge(mod: SdeType): boolean;
2153
+ /**
2154
+ * Charge-fits-module predicate. Both must pass:
2155
+ * 1. charge.groupID ∈ module's CHARGE_GROUP_1..5
2156
+ * 2. charge size ≤ module's chargeSize (when both declare CHARGE_SIZE).
2157
+ * Modules without an explicit chargeSize accept any size.
2158
+ */
2159
+ declare function moduleAcceptsChargeType(mod: SdeType, charge: SdeType): boolean;
2160
+ /**
2161
+ * True iff the module declares at least one activation-class effect
2162
+ * (cat 1=active, 2=target-attack, 3=area). These modules accept the
2163
+ * full ONLINE → ACTIVE → OVERLOAD cycle. Pure-passive modules (rigs,
2164
+ * damage controls, gyrostabilizers, signal amplifiers, … with only
2165
+ * cat 0/4/6 effects) don't — they should never expose an "activate"
2166
+ * affordance in the UI because clicking changes nothing in the engine.
2167
+ */
2168
+ declare function isActivatableModule(mod: SdeType, effects: Map<number, {
2169
+ effectCategoryID?: number;
2170
+ }>): boolean;
2171
+ /**
2172
+ * Decide the default state a module should be in when newly fitted /
2173
+ * imported. Modules with at least one activation-class effect want to
2174
+ * be ACTIVE — that's where weapons, propulsion, hardeners, repairers,
2175
+ * EWAR all live. Pure-passive items stay ONLINE.
2176
+ *
2177
+ * Special case: Cloaking Devices (group 330) default to ONLINE even
2178
+ * though they're activatable, because activating one prevents the rest
2179
+ * of the fit from doing anything. The user can manually flip a cloak
2180
+ * to ACTIVE if they want to model a stealth approach.
2181
+ */
2182
+ declare function defaultStateForModule(mod: SdeType, effects: Map<number, {
2183
+ effectCategoryID?: number;
2184
+ }>): 'ONLINE' | 'ACTIVE';
2185
+ /**
2186
+ * Hardpoint usage helper for the multi-fit drag-on-ship feature. Returns
2187
+ * how many turret/launcher hardpoints are still free given the current
2188
+ * fit, so we can decide how many copies of a weapon to drop in.
2189
+ */
2190
+ declare function freeHardpointsFor(mod: SdeType, ship: SdeType, fittedHiModules: Array<{
2191
+ typeID: number;
2192
+ }>, dataset: {
2193
+ getType(id: number): SdeType | undefined;
2194
+ }): number;
2195
+
2196
+ export { ACTIVATION_EFFECT_ID, ATTR, type BundleManifest, CATEGORY, type CapacitorReport, type ComputeFitOptions, type ComputedAttribute, type ComputedFit, DAMAGE_PROFILE_PRESETS, type DamageProfile, type DefenseLayerKind, type DerivedStats, type DroneComputed, type EftParseResult, type EwarKind, type FighterComputed, type Fit, type FitBooster, type FitCargo, FitContext, type FitDrone, type FitFighter, type FitImplant, type FitModule, type FitSubsystem, type FitVisibility, type FitWarning, type FittingDataset, type ItemKind, ItemState, LEGACY_EFFECT_IDS, LEGACY_HANDLED_EFFECT_IDS, type LayerEhp, type LegacyEffectEntry, type MarketGroupPlacement, ModifiedAttribute, type ModifierAffliction, type ModifierOperation, type ModuleAttrSnapshot, type ModuleComputed, type ModuleState, type MutatorData, OPERATION_BY_SDE_CODE, OUT_OF_SCOPE_EFFECT_IDS, type OffenseReport, type ProjectedEffectReport, type ProjectedSource, REPAIR_EFFECT_AMOUNT_ATTR, REQUIRED_SKILL_PAIRS, SLOT_EFFECT_ID, SLOT_EFFECT_TO_SLOT_TYPE, STACKING_PENALTY_K, type SdeAttribute, type SdeCategory, type SdeCloneGrade, type SdeDbuffCollection, type SdeDynamicAttribute, type SdeEffect, type SdeGroup, type SdeMarketGroup, type SdeMetaGroup, type SdeModifierInfo, type SdeType, type SdeUnit, type SkillCheckResult, type SkillProfile, type SkillRequirement, type SlotType, type StructureMeta, type StructureServiceModule, TARGET_PROFILE_PRESETS, type TankRates, type TargetProfile, type TypeBucket, WEAPON_EFFECT_KIND, type WeaponContribution, type WeaponEffectKind, type WeaponKind, applyOneModifier, applySkills, applySourceItem, buildNameIndex, canFitModuleOnShip, chargeGroupsForModule, checkSkills, classifyEwar, classifyWeapon, combineJamChances, combineMultiplicative, combinePenalized, combineUnstacked, computeCapacitor, computeFit, computeLayerEhp, computeOffense, computeStructureMeta, computeT3CVariantCode, computeTank, computeTotalEhp, defaultStateForModule, disintegratorCyclesToFullSpool, disintegratorSpoolBonus, ecmJamChance, effectiveDps, ehpUnderProfile, formatDna, formatEft, formatMultibuy, formatTypeIds, freeFitGroupSlotsFor, freeHardpointsFor, isActivatableModule, isMissileLauncher, isSmartBomb, isTurretWeapon, marketGroupPlacement, maxGroupFittedFor, maxTypeFittedFor, moduleAcceptsAnyCharge, moduleAcceptsCharge, moduleAcceptsChargeType, parseEft, peakRecharge, readCycleInfo, readDamageComponents, readRangeInfo, rechargeRateAt, shipGroupRestrictions, shipTypeRestrictions, typeFitsSlotType, verifyLegacyEffectIds };