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.
- package/LICENSE +674 -0
- package/NOTICE +31 -0
- package/README.md +95 -0
- package/data/.build +1 -0
- package/data/SDE-LICENSE.md +17 -0
- package/data/manifest.json +1 -0
- package/data/v3e885b627373/attributes.json +1 -0
- package/data/v3e885b627373/categories.json +1 -0
- package/data/v3e885b627373/clone-grades.json +1 -0
- package/data/v3e885b627373/dbuff-collections.json +1 -0
- package/data/v3e885b627373/dynamic-attributes.json +1 -0
- package/data/v3e885b627373/effects.json +1 -0
- package/data/v3e885b627373/groups.json +1 -0
- package/data/v3e885b627373/market-groups.json +1 -0
- package/data/v3e885b627373/meta-groups.json +1 -0
- package/data/v3e885b627373/types/charges.json +1 -0
- package/data/v3e885b627373/types/drones.json +1 -0
- package/data/v3e885b627373/types/fighters.json +1 -0
- package/data/v3e885b627373/types/implants.json +1 -0
- package/data/v3e885b627373/types/modules.json +1 -0
- package/data/v3e885b627373/types/mutaplasmids.json +1 -0
- package/data/v3e885b627373/types/ships.json +1 -0
- package/data/v3e885b627373/types/skills.json +1 -0
- package/data/v3e885b627373/types/structure-modules.json +1 -0
- package/data/v3e885b627373/types/structures.json +1 -0
- package/data/v3e885b627373/types/subsystems.json +1 -0
- package/data/v3e885b627373/types/system-effects.json +1 -0
- package/data/v3e885b627373/units.json +1 -0
- package/dist/index.cjs +5911 -0
- package/dist/index.d.cts +2196 -0
- package/dist/index.d.ts +2196 -0
- package/dist/index.js +5839 -0
- package/dist/node.cjs +6046 -0
- package/dist/node.d.cts +34 -0
- package/dist/node.d.ts +34 -0
- package/dist/node.js +5948 -0
- package/package.json +73 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|