@elite-dangerous-almanac/core 0.1.6 → 0.1.8
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/PROVENANCE/i18n/SOURCES.md +92 -35
- package/PROVENANCE/ships/SOURCES.md +54 -14
- package/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/chunk-3BM5BHI3.js +1 -0
- package/dist/chunk-3BM5BHI3.js.map +1 -0
- package/dist/{chunk-EWAS7KMF.js → chunk-4S6WB7QR.js} +1 -1
- package/dist/chunk-4TFAZX73.js +1 -0
- package/dist/{chunk-PYV7VI6I.js.map → chunk-4TFAZX73.js.map} +1 -1
- package/dist/chunk-4ZGUSHTP.js +1 -0
- package/dist/{chunk-5V4OXZLE.js.map → chunk-4ZGUSHTP.js.map} +1 -1
- package/dist/{chunk-BZDTEXXM.js → chunk-6BO5JJNI.js} +1 -1
- package/dist/{chunk-3MPLVD2R.js → chunk-6G6TYW2D.js} +1 -1
- package/dist/chunk-AFS6HN3D.js +1 -0
- package/dist/chunk-AFS6HN3D.js.map +1 -0
- package/dist/chunk-CJWFY6OX.js +1 -0
- package/dist/{chunk-JKWV3CZN.js.map → chunk-CJWFY6OX.js.map} +1 -1
- package/dist/{chunk-24INI4U7.js → chunk-CX7B67OI.js} +1 -1
- package/dist/chunk-CX7B67OI.js.map +1 -0
- package/dist/chunk-EKQCVCMK.js +1 -0
- package/dist/{chunk-WHAU3C4E.js.map → chunk-EKQCVCMK.js.map} +1 -1
- package/dist/chunk-ELT3FEA4.js +1 -0
- package/dist/{chunk-6FKBZPI6.js.map → chunk-ELT3FEA4.js.map} +1 -1
- package/dist/{chunk-3EO7PTAP.js → chunk-EVJ32UF3.js} +1 -1
- package/dist/{chunk-3EO7PTAP.js.map → chunk-EVJ32UF3.js.map} +1 -1
- package/dist/chunk-GRSNX36B.js +1 -0
- package/dist/{chunk-JXLCRQH5.js.map → chunk-GRSNX36B.js.map} +1 -1
- package/dist/chunk-GVNTKCVF.js +1 -0
- package/dist/chunk-GVNTKCVF.js.map +1 -0
- package/dist/{chunk-FEER4ERG.js → chunk-IOJXLDJN.js} +1 -1
- package/dist/{chunk-EVEFYI7K.js → chunk-J42ZXLBL.js} +1 -1
- package/dist/{chunk-EVEFYI7K.js.map → chunk-J42ZXLBL.js.map} +1 -1
- package/dist/chunk-KJFUNIIY.js +1 -0
- package/dist/{chunk-NCKOVFBJ.js.map → chunk-KJFUNIIY.js.map} +1 -1
- package/dist/{chunk-RC6F6RAU.js → chunk-LZYHBFJC.js} +1 -1
- package/dist/{chunk-RC6F6RAU.js.map → chunk-LZYHBFJC.js.map} +1 -1
- package/dist/chunk-M6G3UDTX.js +1 -0
- package/dist/chunk-M6G3UDTX.js.map +1 -0
- package/dist/{chunk-DLLBARN5.js → chunk-MKGJPVPS.js} +1 -1
- package/dist/{chunk-UXFMVZAY.js → chunk-OVUHQEH5.js} +1 -1
- package/dist/{chunk-UXFMVZAY.js.map → chunk-OVUHQEH5.js.map} +1 -1
- package/dist/{chunk-UPUC5X2C.js → chunk-PA5EITW5.js} +1 -1
- package/dist/{chunk-UPUC5X2C.js.map → chunk-PA5EITW5.js.map} +1 -1
- package/dist/chunk-QY3L5BKN.js +1 -0
- package/dist/{chunk-PEHOKSSD.js.map → chunk-QY3L5BKN.js.map} +1 -1
- package/dist/chunk-RVIYOHYF.js +1 -0
- package/dist/chunk-RVIYOHYF.js.map +1 -0
- package/dist/chunk-SD6GNFDX.js +1 -0
- package/dist/chunk-SD6GNFDX.js.map +1 -0
- package/dist/chunk-TW7LUBHD.js +1 -0
- package/dist/{chunk-QGAZWX7G.js.map → chunk-TW7LUBHD.js.map} +1 -1
- package/dist/chunk-V4C6FIE2.js +1 -0
- package/dist/{chunk-TLA6LHLN.js.map → chunk-V4C6FIE2.js.map} +1 -1
- package/dist/chunk-Z6XMXMVI.js +1 -0
- package/dist/{chunk-NEFUW32I.js.map → chunk-Z6XMXMVI.js.map} +1 -1
- package/dist/{chunk-T4CV4CDF.js → chunk-ZRG35JSH.js} +1 -1
- package/dist/i18n/blueprints.d.ts +2 -1
- package/dist/i18n/blueprints.js +1 -1
- package/dist/i18n/diagnostics.d.ts +1 -0
- package/dist/i18n/diagnostics.js +1 -1
- package/dist/i18n/engineering-groups.js +1 -1
- package/dist/i18n/experimental-effect-descriptions.js +1 -1
- package/dist/i18n/experimental-effects.js +1 -1
- package/dist/i18n/index.d.ts +9 -6
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/materials.js +1 -1
- package/dist/i18n/micro-resources.js +1 -1
- package/dist/i18n/module-families.d.ts +32 -0
- package/dist/i18n/module-families.js +1 -0
- package/dist/i18n/module-families.js.map +1 -0
- package/dist/i18n/modules.js +1 -1
- package/dist/i18n/pre-engineered.js +1 -1
- package/dist/i18n/ships.js +1 -1
- package/dist/i18n/slots.js +1 -1
- package/dist/materials/index.js +1 -1
- package/dist/materials/materials-all.js +1 -1
- package/dist/materials/materials.js +1 -1
- package/dist/materials/micro-resources-all.js +1 -1
- package/dist/materials/micro-resources.js +1 -1
- package/dist/ships/blueprint-costs.d.ts +1 -0
- package/dist/ships/blueprints.d.ts +1 -0
- package/dist/ships/engineering.d.ts +1 -0
- package/dist/ships/experimental-effect-costs.d.ts +1 -0
- package/dist/ships/experimental-effects.d.ts +1 -0
- package/dist/ships/index.d.ts +2 -1
- package/dist/ships/index.js +1 -1
- package/dist/ships/loadout-validation.d.ts +1 -0
- package/dist/ships/mobility.d.ts +12 -0
- package/dist/ships/mobility.js +1 -1
- package/dist/ships/module-capabilities.d.ts +1 -0
- package/dist/ships/module-families.d.ts +49 -0
- package/dist/ships/module-families.js +1 -0
- package/dist/ships/module-families.js.map +1 -0
- package/dist/ships/module-limits.d.ts +1 -0
- package/dist/ships/modules-all.d.ts +1 -0
- package/dist/ships/modules-all.js +1 -1
- package/dist/ships/modules-core.d.ts +1 -0
- package/dist/ships/modules-core.js +1 -1
- package/dist/ships/modules-hardpoint.d.ts +2 -1
- package/dist/ships/modules-hardpoint.js +1 -1
- package/dist/ships/modules-internal.d.ts +2 -1
- package/dist/ships/modules-internal.js +1 -1
- package/dist/ships/modules-utility.d.ts +2 -1
- package/dist/ships/modules-utility.js +1 -1
- package/dist/ships/modules.d.ts +41 -14
- package/dist/ships/modules.js +1 -1
- package/dist/ships/pre-engineered-stats.d.ts +1 -0
- package/dist/ships/pre-engineered-stats.js +1 -1
- package/dist/ships/ship-loadout.d.ts +143 -7
- package/dist/ships/ship-loadout.js +1 -1
- package/dist/ships/slef.d.ts +10 -8
- package/dist/ships/slef.js +1 -1
- package/dist/ships/weapons.d.ts +1 -0
- package/package.json +9 -1
- package/dist/chunk-24INI4U7.js.map +0 -1
- package/dist/chunk-5V4OXZLE.js +0 -1
- package/dist/chunk-6FKBZPI6.js +0 -1
- package/dist/chunk-DAQ3PJ4R.js +0 -1
- package/dist/chunk-DAQ3PJ4R.js.map +0 -1
- package/dist/chunk-JKWV3CZN.js +0 -1
- package/dist/chunk-JXLCRQH5.js +0 -1
- package/dist/chunk-NCKOVFBJ.js +0 -1
- package/dist/chunk-NEFUW32I.js +0 -1
- package/dist/chunk-NWBWTJM3.js +0 -1
- package/dist/chunk-NWBWTJM3.js.map +0 -1
- package/dist/chunk-PEHOKSSD.js +0 -1
- package/dist/chunk-PYV7VI6I.js +0 -1
- package/dist/chunk-QGAZWX7G.js +0 -1
- package/dist/chunk-SU352DIW.js +0 -1
- package/dist/chunk-SU352DIW.js.map +0 -1
- package/dist/chunk-TLA6LHLN.js +0 -1
- package/dist/chunk-WHAU3C4E.js +0 -1
- package/dist/chunk-ZKU7CMAH.js +0 -1
- package/dist/chunk-ZKU7CMAH.js.map +0 -1
- /package/dist/{chunk-EWAS7KMF.js.map → chunk-4S6WB7QR.js.map} +0 -0
- /package/dist/{chunk-BZDTEXXM.js.map → chunk-6BO5JJNI.js.map} +0 -0
- /package/dist/{chunk-3MPLVD2R.js.map → chunk-6G6TYW2D.js.map} +0 -0
- /package/dist/{chunk-FEER4ERG.js.map → chunk-IOJXLDJN.js.map} +0 -0
- /package/dist/{chunk-DLLBARN5.js.map → chunk-MKGJPVPS.js.map} +0 -0
- /package/dist/{chunk-T4CV4CDF.js.map → chunk-ZRG35JSH.js.map} +0 -0
|
@@ -6,13 +6,20 @@ They are deliberately sparse: absence means that no accepted source carried a va
|
|
|
6
6
|
the lookup API returns `null` for that locale rather than treating English as a
|
|
7
7
|
translation.
|
|
8
8
|
|
|
9
|
-
**
|
|
10
|
-
publish more —
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
**Six locales are stored: `en`, `de`, `es`, `fr`, `pt` and `ru`.** Several accepted
|
|
10
|
+
sources publish more — Italian, Hungarian, Georgian and Simplified Chinese among them —
|
|
11
|
+
and those values are deliberately not carried: the project supports those six languages
|
|
12
|
+
and no others. That is a decision about this catalogue, not a gap in any source, so
|
|
13
|
+
re-deriving a catalogue from its source must drop the other columns again rather than
|
|
14
|
+
restore them. Every locale is stored under a bare language tag; a regional or script tag
|
|
15
|
+
resolves to its language.
|
|
16
|
+
|
|
17
|
+
**`pt` is Brazilian Portuguese.** Every accepted source publishes exactly one Portuguese
|
|
18
|
+
column and it is the Brazilian one: Odyssey Materials Helper's CSVs and EDSY's
|
|
19
|
+
`lang-pt.json` carry Brazilian spellings, EDDI's only populated Portuguese resource file
|
|
20
|
+
is `Modules.pt-BR.resx` (its `pt-PT` sibling is an empty stub), and Frontier's own
|
|
21
|
+
in-game localisation ships pt-BR and no European Portuguese. Storing it under the bare
|
|
22
|
+
tag follows the rule above, so `pt-PT` resolves to Brazilian Portuguese.
|
|
16
23
|
|
|
17
24
|
## `module-names.jsonc`
|
|
18
25
|
|
|
@@ -31,25 +38,43 @@ language tag; a regional or script tag resolves to its language.
|
|
|
31
38
|
module name also exactly equals the canonical name. Values shared by modules with one
|
|
32
39
|
canonical name are retained only when they agree, then stored once under that name.
|
|
33
40
|
- **Coverage:** English covers all 1,199 symbols. The explicit translations cover 1,120
|
|
34
|
-
in German, 1,090 in Spanish, 1,088 in French and 1,153
|
|
41
|
+
in German, 1,090 in Spanish, 1,088 in French, 1,155 in Brazilian Portuguese and 1,153
|
|
42
|
+
in Russian. Portuguese comes from the same two joins, reading EDDI's
|
|
43
|
+
`Modules.pt-BR.resx` and EDSY's `lang-pt.json`.
|
|
35
44
|
- **Manual corrections:** none.
|
|
36
45
|
|
|
37
46
|
## `blueprint-names.jsonc`
|
|
38
47
|
|
|
39
|
-
- **Acquired:** 2026-08-14 UTC.
|
|
48
|
+
- **Acquired:** EDSY 2026-08-14 UTC; the in-game modification names 2026-08-23 UTC.
|
|
40
49
|
- **EDSY revision:** commit `e446fbe6e4597dea7ab0bd3105b9a36642388040`;
|
|
41
50
|
database version `424009901`, last-modified marker `20260810`.
|
|
51
|
+
- **In-game localisation revision:** none published; the game ships no immutable
|
|
52
|
+
identifier for its localisation tables. The acquired table is the evidence.
|
|
42
53
|
- **Derivation:** each of the 107 current keys and its English name comes from
|
|
43
|
-
`data/ships/blueprints.jsonc`.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
`data/ships/blueprints.jsonc`. Names remain keyed per blueprint because a single
|
|
55
|
+
English phrase can have different grammatical translations for different module
|
|
56
|
+
families. The Frontier `fdname` joins case-insensitively to the in-game modification
|
|
57
|
+
name table, which supplies every locale for the 90 recipes it names. The remaining 17
|
|
58
|
+
are ids this repository keys separately because their numbers differ, where the game
|
|
59
|
+
offers only the generic recipe's menu entry and so publishes only its name: each takes
|
|
60
|
+
the locales of the generic id it shadows — `AFM_Shielded`, `Refineries_Shielded` and
|
|
61
|
+
the four limpet-controller Shielded recipes from `Misc_Shielded`, the four
|
|
62
|
+
`LightWeight` and four `Reinforced` limpet recipes from `Misc_LightWeight` and
|
|
63
|
+
`Misc_Reinforced`, `MC_Overcharged` from `Weapon_Overcharged`, and `Scanner_LongRange`
|
|
64
|
+
and `Scanner_WideAngle` from `Sensor_LongRange` and `Sensor_WideAngle`. The last three
|
|
65
|
+
of those pairs are the same shadowing `blueprint-journal-names.jsonc` already records,
|
|
66
|
+
where the game writes the generic id for a recipe this repository keys specifically.
|
|
67
|
+
Only the English differs between a shadowing id and its generic: the Almanac's own
|
|
68
|
+
canonical name is kept, as it is for every other blueprint. Where EDSY and the in-game
|
|
69
|
+
table disagree the in-game value is kept, as everywhere else in this repository: it
|
|
70
|
+
differs for ten values across four locales, all of them wording, none of them meaning.
|
|
71
|
+
- **Coverage:** complete. All 107 blueprints carry all six locales.
|
|
72
|
+
- **Manual corrections:** two values in the in-game table carry a stray leading or
|
|
73
|
+
trailing ASCII space, marked in the acquired table and removed here so a successful
|
|
74
|
+
lookup is already trimmed like every other catalogue name: French `FSD_FastBoot` (which
|
|
75
|
+
EDSY carries with the same trailing space) and German `Railgun_LongShot`. The
|
|
76
|
+
Portuguese `GuardianModule_Sturdy` and `GuardianWeapon_Sturdy` values carry a literal
|
|
77
|
+
`<br>` line break for the in-game layout, which is stored as a single space.
|
|
53
78
|
|
|
54
79
|
## `experimental-effect-names.jsonc`
|
|
55
80
|
|
|
@@ -61,7 +86,7 @@ language tag; a regional or script tag resolves to its language.
|
|
|
61
86
|
EDSY's `expeffect.fdname`; that record's id selects an explicit `expeffect-<id>` entry
|
|
62
87
|
from each language file.
|
|
63
88
|
- **Coverage:** English covers all 86 effects; EDSY covers 84 in German and 85 each in
|
|
64
|
-
Spanish, French and Russian.
|
|
89
|
+
Spanish, French, Brazilian Portuguese and Russian.
|
|
65
90
|
- **Manual corrections:** none.
|
|
66
91
|
|
|
67
92
|
## `material-names.jsonc`
|
|
@@ -79,7 +104,7 @@ language tag; a regional or script tag resolves to its language.
|
|
|
79
104
|
parenthetical Guardian and Thargoid category labels where the owning catalogue does;
|
|
80
105
|
localized columns remain verbatim source values.
|
|
81
106
|
- **Coverage:** English, Spanish and Russian cover all 146 materials. The explicit
|
|
82
|
-
translations cover 128 in German and 140 in French.
|
|
107
|
+
translations cover 128 in German, and 140 each in French and Brazilian Portuguese.
|
|
83
108
|
- **Manual corrections:** none.
|
|
84
109
|
|
|
85
110
|
## `micro-resource-names.jsonc`
|
|
@@ -99,7 +124,10 @@ language tag; a regional or script tag resolves to its language.
|
|
|
99
124
|
German, Spanish, French and Russian. In both cases the owning catalogue remains
|
|
100
125
|
authoritative for canonical English names.
|
|
101
126
|
- **Coverage:** English, Spanish and Russian cover all 226 micro resources. The explicit
|
|
102
|
-
translations cover 218 in German and
|
|
127
|
+
translations cover 218 in German, 225 in French and 188 in Brazilian Portuguese. The
|
|
128
|
+
30 in-game-backed records take Portuguese from Odyssey Materials Helper where it has a
|
|
129
|
+
row, because the in-game capture supplied no Portuguese; `PowerVirus` and
|
|
130
|
+
`SmallCapacityPowerRegulator`, which the source does not carry at all, have none.
|
|
103
131
|
- **Manual corrections:** none.
|
|
104
132
|
|
|
105
133
|
## `ship-names.jsonc`
|
|
@@ -115,8 +143,11 @@ language tag; a regional or script tag resolves to its language.
|
|
|
115
143
|
from `data/ships/ships.jsonc`. The symbol is joined to Odyssey Materials
|
|
116
144
|
Helper's `ships.name.*` key after removing punctuation and folding case. EDSY's
|
|
117
145
|
`eddb.js` `ship.fdname` supplies the second join, with explicit `ship-<id>` language
|
|
118
|
-
entries filling locales absent from the CSV. The
|
|
119
|
-
|
|
146
|
+
entries filling locales absent from the CSV. The CSV keys a row by the ship's display
|
|
147
|
+
name rather than its symbol, which differ for the four hulls whose name carries a mark
|
|
148
|
+
the symbol omits (`Viper` is `ships.name.viper_mk_iii`), so the name is tried first and
|
|
149
|
+
the symbol second. The resulting source-backed coverage is 48 ships in Spanish, 44 in
|
|
150
|
+
French, 38 in Brazilian Portuguese and 48 in Russian; no accepted source carries a
|
|
120
151
|
German ship-name table.
|
|
121
152
|
- **Standing conclusion:** every source-backed non-English value is byte-for-byte equal
|
|
122
153
|
to the canonical English ship name. The explicit values remain in the catalogue so a
|
|
@@ -147,10 +178,33 @@ language tag; a regional or script tag resolves to its language.
|
|
|
147
178
|
`data/ships/SOURCES.md`. Where a canonical English name matches an EDSY `mtype-*`
|
|
148
179
|
value in `lang-en.json` byte-for-byte and uniquely, explicit values with the same key
|
|
149
180
|
are copied from `lang-ru.json`.
|
|
150
|
-
- **Coverage:** canonical English covers every engineering option group;
|
|
151
|
-
44 of 48. The Guardian power-plant, power-distributor
|
|
152
|
-
have no distinct EDSY label, and EDSY carries no
|
|
153
|
-
`frameShiftDrivesSCO`.
|
|
181
|
+
- **Coverage:** canonical English covers every engineering option group; Brazilian
|
|
182
|
+
Portuguese and Russian each cover 44 of 48. The Guardian power-plant, power-distributor
|
|
183
|
+
and hull-reinforcement groups have no distinct EDSY label, and EDSY carries no value in
|
|
184
|
+
either language for `frameShiftDrivesSCO`.
|
|
185
|
+
- **Manual corrections:** none.
|
|
186
|
+
|
|
187
|
+
## `module-family-names.jsonc`
|
|
188
|
+
|
|
189
|
+
- **Acquired:** 2026-08-23 UTC.
|
|
190
|
+
- **In-game localisation revision:** none published; the game ships no immutable
|
|
191
|
+
identifier for its localisation tables. The acquired table is the evidence.
|
|
192
|
+
- **Derivation:** each of the 77 family ids and its canonical English name is projected
|
|
193
|
+
from `data/ships/module-families.jsonc`, whose own derivation is recorded in
|
|
194
|
+
`data/ships/SOURCES.md`. 58 of them are named by an in-game outfitting category, and
|
|
195
|
+
those categories' explicit German, Spanish, French, Brazilian Portuguese and Russian
|
|
196
|
+
labels are copied verbatim. The join is on family identity rather than on English
|
|
197
|
+
string equality, because the English column of the in-game table *is* the canonical
|
|
198
|
+
English name the ships catalogue took from it.
|
|
199
|
+
- **Coverage:** English covers all 77 families; the five other locales cover the same 58.
|
|
200
|
+
The 19 without a source-backed label are the families the in-game outfitting screen
|
|
201
|
+
does not name separately: `axMissileRacks`, `axMultiCannons`, `cargoHatches`,
|
|
202
|
+
`causticSinkLaunchers`, `guardianGaussCannons`, `guardianHybridPowerDistributors`,
|
|
203
|
+
`guardianHybridPowerPlants`, `guardianNaniteTorpedoPylons`, `guardianPlasmaChargers`,
|
|
204
|
+
`guardianShardCannons`, `guardianShieldReinforcementPackages`,
|
|
205
|
+
`miningMultiLimpetControllers`, `remoteReleaseFlakLaunchers`,
|
|
206
|
+
`remoteReleaseFlechetteLaunchers`, `shockCannons`, `shutdownFieldNeutralisers`,
|
|
207
|
+
`subSurfaceDisplacementMissiles`, `subSurfaceExtractionMissiles` and `xenoScanners`.
|
|
154
208
|
- **Manual corrections:** none.
|
|
155
209
|
|
|
156
210
|
## `experimental-effect-descriptions.jsonc`
|
|
@@ -180,16 +234,19 @@ language tag; a regional or script tag resolves to its language.
|
|
|
180
234
|
are supplied by the repository owner from the in-game shop listing; no registry
|
|
181
235
|
publishes them.
|
|
182
236
|
- **Coverage:** English covers every variant. Ordinary names inherit the explicit
|
|
183
|
-
localized values available for their base module
|
|
184
|
-
|
|
237
|
+
localized values available for their base module, which now includes Brazilian
|
|
238
|
+
Portuguese on 51 of the 76 variants; the Merc-shop names carry the five locales the
|
|
239
|
+
repository owner supplied and no Portuguese; fixed reward names have no accepted
|
|
240
|
+
translation source.
|
|
185
241
|
- **Manual corrections:** none.
|
|
186
242
|
|
|
187
243
|
## Known gaps
|
|
188
244
|
|
|
189
245
|
Localized coverage follows the accepted sources and is not complete for every catalogue or
|
|
190
|
-
stored locale. A language absent from all
|
|
191
|
-
above rather than a gap. The accepted sources carry only canonical English for
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
246
|
+
stored locale. A language absent from all eleven catalogues is the locale decision
|
|
247
|
+
recorded above rather than a gap. The accepted sources carry only canonical English for
|
|
248
|
+
ship manufacturers, experimental-effect descriptions, 19 of the 77 outfitting-family
|
|
249
|
+
labels, some engineering-group labels, slot and restriction labels, fixed reward names,
|
|
250
|
+
and structured loadout, calculation, SLEF and edit messages. Missing source-backed
|
|
251
|
+
translations remain tracked by
|
|
195
252
|
[#320](https://github.com/DarkSession/Elite-Dangerous-Almanac/issues/320).
|
|
@@ -385,20 +385,60 @@ FDevIDs, stats from coriolis-data and EDSY, joined on `symbol`.
|
|
|
385
385
|
The group source, derivation, split
|
|
386
386
|
Guardian families and coverage are documented under Engineering options below; this
|
|
387
387
|
field is a projection of that map, not a separate classification source.
|
|
388
|
-
- **`
|
|
389
|
-
|
|
390
|
-
internal modules in
|
|
391
|
-
12.
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
`
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
388
|
+
- **`familyId` groups every outfitting list.** Every record in all four catalogues
|
|
389
|
+
carries the id of the family its outfitting list shows it under: 521 core modules in
|
|
390
|
+
8 families, 484 internal modules in 32, 159 hardpoints in 25, and 35 utility fittings
|
|
391
|
+
in 12. No family crosses an outfitting category. The ids and their canonical English
|
|
392
|
+
names are `module-families.jsonc`, below; localized labels are
|
|
393
|
+
`data/i18n/module-family-names.jsonc`. Variants remain with the base family rather
|
|
394
|
+
than fragmenting a list: Bi-Weave and Prismatic generators are `shieldGenerators`;
|
|
395
|
+
corrosion-resistant and Mk II racks are `cargoRacks`; Powerplay and pre-engineered
|
|
396
|
+
weapons stay with their base weapon; Mk I/Mk II cabins and vessel hangars share
|
|
397
|
+
`passengerCabins` and `vesselHangars`. Guardian and AX weapons retain their distinct
|
|
398
|
+
family where their function differs. A core record's family follows from its `slot`,
|
|
399
|
+
which is on every one of them, so the two never disagree.
|
|
400
|
+
- **`supercruiseOvercharge` — the SCO drive line.** The 36
|
|
401
|
+
`Int_Hyperdrive_Overcharge_*` records carry `true`; the 36 plain drives of the same
|
|
402
|
+
sizes and ratings, and every other record in all four catalogues, omit it.
|
|
403
|
+
- **Source:** FDevIDs `outfitting.csv`, which sells the two drive lines under distinct
|
|
404
|
+
symbol families and names the Overcharge one `Frame Shift Drive (SCO)`. No registry
|
|
405
|
+
publishes a field for the capability: coriolis-data and EDSY both leave it implicit
|
|
406
|
+
in the symbol and the display name.
|
|
407
|
+
- **Derivation:** the symbol family is the classification, exactly as it is for `slot`
|
|
408
|
+
above, and it is applied once here so that consumers read a datum instead of
|
|
409
|
+
matching `Int_Hyperdrive_Overcharge` themselves — which a pre-engineered or fitted
|
|
410
|
+
article's symbol does not always let them do. Stored as a sparse flag rather than a
|
|
411
|
+
`true`/`false` pair on all 72 drives, following `alwaysPowered` and
|
|
412
|
+
`guardianZoneResistance`.
|
|
413
|
+
- **Nothing else is derived from it.** The two lines share every jump constant this
|
|
414
|
+
catalogue carries (`optMass`, `maxFuel`, `fuelMul`, `fuelPower`, and the
|
|
415
|
+
size-determined `fsdHeatRate` above), so no calculation reads the flag; the
|
|
416
|
+
overcharged supercruise behaviour the drives are named for has no published figures
|
|
417
|
+
and none are stored.
|
|
418
|
+
|
|
419
|
+
### `module-families.jsonc` — the family ids and their English names
|
|
420
|
+
|
|
421
|
+
- **Source:** Frontier's in-game outfitting category labels, acquired 2026-08-23 UTC
|
|
422
|
+
and supplied by the repository owner. The game publishes no immutable identifier for
|
|
423
|
+
its localisation tables, so the acquired table is the evidence; it is the same table
|
|
424
|
+
that supplies `data/i18n/module-family-names.jsonc`.
|
|
425
|
+
- **Derivation:** 58 of the 77 families are named by an in-game outfitting category and
|
|
426
|
+
take that category's English label verbatim — which is why `Cargo Scanners` is
|
|
427
|
+
`Manifest Scanners`, `Torpedo Pylons` is `Torpedoes` and `Mine Launchers` is `Mines`.
|
|
428
|
+
The remaining 19 keep the Almanac's own descriptive name, because the outfitting
|
|
429
|
+
screen does not name them separately. Each id is the camelCase form of its English
|
|
430
|
+
name with punctuation removed, which `modules.test.ts` asserts, so an id and its
|
|
431
|
+
label cannot drift apart.
|
|
432
|
+
- **Merged families.** Where the game lists several Almanac families under one category,
|
|
433
|
+
they are one family here: the operations, rescue, universal and xeno multi-limpet
|
|
434
|
+
controllers are `multiLimpetControllers`; mining volley repeaters and seismic charge
|
|
435
|
+
launchers are `miningTools`; the Mk II Plasma Shock Accelerator is a
|
|
436
|
+
`plasmaAccelerators`; the Supercruise Assist is a `flightAssists`. Mining multi-limpet
|
|
437
|
+
controllers are deliberately **not** merged, and keep their own family.
|
|
438
|
+
- **The in-game English is not always the better name**, and is kept anyway so the
|
|
439
|
+
library's label is the one a player sees on the outfitting screen: `engines` for
|
|
440
|
+
thrusters, `fsd` for Frame Shift Drives, `afms` for Auto Field-Maintenance Units,
|
|
441
|
+
`ecms` for Electronic Countermeasures, and the singular `pulseWaveAnalyser`.
|
|
402
442
|
- **Stats source:** coriolis-data `modules/**` for the mechanical, defence, power and
|
|
403
443
|
weapon stats; EDSY `eddb.js` for mass, integrity, power draw, boot time and the
|
|
404
444
|
engineering base stats coriolis does not carry; and in-game verification for the
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -137,6 +137,9 @@ a verbatim copy produced at build time — edit this file, never the copy.
|
|
|
137
137
|
[update notes](https://forums.frontier.co.uk/threads/648012/), and the
|
|
138
138
|
[Rhea Disaster Community Goal](https://forums.frontier.co.uk/threads/deliver-critical-aid-for-the-rhea-disaster.626528/)
|
|
139
139
|
with its [announcement](https://x.com/EliteDangerous/status/1812792503776489745).
|
|
140
|
+
Frontier's own **in-game localisation** supplies the outfitting category labels behind
|
|
141
|
+
the module families and their German, Spanish, French, Brazilian Portuguese and Russian
|
|
142
|
+
display text, and the engineering modification names in the six stored locales.
|
|
140
143
|
Values read directly from the running game are Frontier's too — see the notice below.
|
|
141
144
|
- **A [community description](https://www.reddit.com/r/EliteDangerous/comments/1uk2zhp/plasma_laser_theorycrafting_following_new/)
|
|
142
145
|
by u/Techno3020** — linked only as corroboration that the Operations Plasma conversion's
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{weaponsCapacitorMetrics}from"./chunk-FMPRCYKY.js";import{shieldRecovery,cellBankSummary}from"./chunk-VZZUNSDF.js";import{shieldMetrics}from"./chunk-2FHHMYDC.js";import{getShipBySymbol,getShipByName,getShipSlots}from"./chunk-WNGQ3IAM.js";import{parseSlef,toSlef,stringifySlef,getLoadoutModifier}from"./chunk-OVUHQEH5.js";import{sourcePurchaseFromLoadout,getSourceModuleValue}from"./chunk-IYU4WLFM.js";import{powerBudget}from"./chunk-RLUS76LZ.js";import{isNonOutfittingSlot,isBuiltInHullModule,cloneLoadoutModule,identifyPreEngineeredVariant,cloneModuleStats,availableBlueprintsFor,availableExperimentalsFor,ownKeyIn,FITTED_ITEM,blueprintAvailableFor,blueprintRoutesFor,experimentalAvailableFor,primitiveEngineeringInputsFor,missingBaseLabels,journalModifiersFor,getPreEngineeredStats,getPreEngineeredJournalModifiers,getPreEngineeredModifiers,ordinaryEngineeringProof,unresolvedModifiers,orderBySlotLayout,matchingKeyIn,isCargoHatchSlot,isFinalGuardianWeaponEngineering,scaleDamageComponents}from"./chunk-6BO5JJNI.js";import{weaponMetrics,sumWeaponMetrics,combinedRateOfFire}from"./chunk-LJI7VXJD.js";import{builtInModuleBySymbol,getBulkheadsForShip}from"./chunk-PA5EITW5.js";import{getPreEngineeredVariants}from"./chunk-2VPVZELZ.js";import{singleJumpRange,frameShiftDriveMassFactor,fuelPerJump,totalRange}from"./chunk-GQ5GX5TB.js";import{calculateUnladenMass,calculateCargoCapacity,calculateFuelCapacity}from"./chunk-AFVYCQWL.js";import{fixedSlotReason,validateLoadout}from"./chunk-B4ZSTOM4.js";import{mobilityMetrics}from"./chunk-CX7B67OI.js";import{calculateModuleLimits}from"./chunk-KPETDYQX.js";import{ALL_MODULES}from"./chunk-QY3L5BKN.js";import{CORE_MODULES}from"./chunk-EVJ32UF3.js";import{distributorMetrics}from"./chunk-P2QOL63K.js";import{getExperimentalEffectCost}from"./chunk-SNADUQSJ.js";import{getExperimentalEffect}from"./chunk-JPBMRJFC.js";import{heatMetrics}from"./chunk-W6EPHHTS.js";import{ammunitionCapacity}from"./chunk-AA3K5XUE.js";import{armourMetrics}from"./chunk-FGXWVFN6.js";import{getBlueprintCost}from"./chunk-TEOJCBSG.js";import{baseStats,computeModifiers,labelsForDamageType,scaleForLabel,sumMaterials,fieldForLabel,labelsForField,capabilityValueForLabel,damageTypeForLabel}from"./chunk-DYPOL3CI.js";import{resolveBlueprintForModule}from"./chunk-2TW4ZBI3.js";import{getExperimentalsForModule}from"./chunk-QTZPA2FR.js";import{getBlueprintGrade}from"./chunk-Q3QCLNVG.js";import{getDefaultLoadout}from"./chunk-GRIK3EDH.js";import{loadoutSlotName}from"./chunk-KAA4VOOB.js";import{parseSlotName,enumerateSlots,SLOT_RESTRICTION_LABELS}from"./chunk-RAO35XDF.js";import{normalizeKey}from"./chunk-BKWLI44T.js";import{truncate,describeValue,requireString,requireStringIfPresent}from"./chunk-PKWVG2JY.js";import{deepFreeze}from"./chunk-DURKO7JU.js";var NO_ISSUES=Object.freeze([]);function completeResult(value){return Object.freeze({value:value,complete:true,issues:NO_ISSUES})}function incompleteResult(issues){return Object.freeze({value:null,complete:false,issues:Object.freeze(issues.map(issue=>Object.freeze({...issue,...issue.params?{params:Object.freeze({...issue.params})}:{}})))})}var PREFIX={powerPlant:["int_powerplant","int_guardianpowerplant"],thrusters:["int_engine","int_mkiiagileboost_engine"],frameShiftDrive:["int_hyperdrive"],powerDistributor:["int_powerdistributor","int_guardianpowerdistributor"],shieldGenerator:["int_shieldgenerator"],shieldCellBank:["int_shieldcellbank"],shieldBooster:["hpt_shieldbooster"],shieldReinforcement:["int_guardianshieldreinforcement"],hullReinforcement:["int_hullreinforcement","int_guardianhullreinforcement","int_metaalloyhullreinforcement"],moduleReinforcement:["int_modulereinforcement","int_guardianmodulereinforcement"]};var startsWithAny=(symbol,prefixes)=>prefixes.some(prefix=>symbol.toLowerCase().startsWith(prefix));function isPowerDistributor(module,stats){return stats?.slot?stats.slot==="powerDistributor":startsWithAny(module.Item,PREFIX.powerDistributor)}function isPowerPlant(module,stats){const parsed=parseSlotName(module.Slot);if(parsed?.kind==="core"&&parsed.core==="powerPlant")return true;return stats?.slot?stats.slot==="powerPlant":startsWithAny(module.Item,PREFIX.powerPlant)}function isThruster(module,stats){const parsed=parseSlotName(module.Slot);if(parsed?.kind==="core"&&parsed.core==="thrusters")return true;return stats?.slot?stats.slot==="thrusters":startsWithAny(module.Item,PREFIX.thrusters)}function isFrameShiftDrive(module,stats){const parsed=parseSlotName(module.Slot);if(parsed?.kind==="core"&&parsed.core==="frameShiftDrive")return true;return stats?.slot?stats.slot==="frameShiftDrive":startsWithAny(module.Item,PREFIX.frameShiftDrive)}function isEnabled(module){return module.On!==false}function priorityOf(module){return(module.Priority??0)+1}function effectiveStat(module,field,stats){const stated=statedModifier(module,field);if(stated!==void 0)return stated;const base=stats?.[field];return typeof base==="number"?base:void 0}function statedModifier(module,field){for(const label of labelsForField(field)){if(capabilityValueForLabel(label)!==null)continue;const modified=getLoadoutModifier(module,label);if(modified!==null)return modified/scaleForLabel(label)}return void 0}function effectiveCapability(module,field,stats){for(const label of labelsForField(field)){if(capabilityValueForLabel(label)===null)continue;if(module.Engineering?.Modifiers?.some(modifier=>modifier.Label.toLowerCase()===label.toLowerCase())){return true}}const base=stats[field];return typeof base==="boolean"?base:void 0}function effectiveDamageDistribution(module,stats){const fromEffect=module.Engineering?.ExperimentalEffect?getExperimentalEffect(module.Engineering.ExperimentalEffect)?.damageDistribution:void 0;const distribution={...fromEffect??stats.damageDistribution};let resolved=fromEffect!==void 0||stats.damageDistribution!==void 0;const seen=/* @__PURE__ */new Set;for(const modifier of module.Engineering?.Modifiers??[]){const type=damageTypeForLabel(modifier.Label);if(type===null||seen.has(type))continue;seen.add(type);if(modifier.Value===void 0)continue;distribution[type]=modifier.Value/scaleForLabel(modifier.Label);resolved=true}return resolved?distribution:void 0}function convertsDamage(module){if(module.Engineering?.ExperimentalEffect&&getExperimentalEffect(module.Engineering.ExperimentalEffect)?.damageDistribution){return true}return(module.Engineering?.Modifiers??[]).some(modifier=>damageTypeForLabel(modifier.Label)!==null)}function modifierRatio(module,stats,field){const base=stats?.[field];if(typeof base!=="number"||base===0)return 1;const effective=effectiveStat(module,field,stats);return effective===void 0?1:effective/base}function relatedStat(module,stats,field,ratio){const stated=statedModifier(module,field);if(stated!==void 0)return stated;const base=stats?.[field];return typeof base==="number"?base*ratio:void 0}function effectiveModule(module,stats){if(!stats||!module.Engineering)return stats;const merged={...stats};const fields=new Set(Object.keys(stats));for(const modifier of module.Engineering.Modifiers??[]){const field=fieldForLabel(modifier.Label,stats);if(field)fields.add(field)}for(const key of fields){const capability=effectiveCapability(module,key,stats);if(capability!==void 0){merged[key]=capability;continue}const value=effectiveStat(module,key,stats);if(value!==void 0)merged[key]=value}if(stats.engineeringGroup==="thrusters"){const massRatio=modifierRatio(module,stats,"optMass");const minMass=relatedStat(module,stats,"minMass",massRatio);const maxMass=relatedStat(module,stats,"maxMass",massRatio);if(minMass!==void 0)merged.minMass=minMass;if(maxMass!==void 0)merged.maxMass=maxMass}if(stats.engineeringGroup==="thrusters"||stats.engineeringGroup==="shieldGenerators"){const performanceRatio=modifierRatio(module,stats,"optMultiplier");const minMultiplier=relatedStat(module,stats,"minMultiplier",performanceRatio);const maxMultiplier=relatedStat(module,stats,"maxMultiplier",performanceRatio);if(minMultiplier!==void 0)merged.minMultiplier=minMultiplier;if(maxMultiplier!==void 0)merged.maxMultiplier=maxMultiplier;if(stats.engineeringGroup==="thrusters"){for(const field of["minSpeedMultiplier","optSpeedMultiplier","maxSpeedMultiplier","minRotationMultiplier","optRotationMultiplier","maxRotationMultiplier"]){const value=stats[field];if(value!==void 0)merged[field]=value*performanceRatio}}}const damageDistribution=effectiveDamageDistribution(module,stats);if(damageDistribution)merged.damageDistribution=damageDistribution;if(stats.category==="hardpoint")normalizeEffectiveWeapon(module,merged);if(convertsDamage(module)){delete merged.damageComponents}else if(stats.damageComponents){merged.damageComponents=scaleDamageComponents(stats.damageComponents,stats.damage,typeof merged.damage==="number"?merged.damage:void 0)}return merged}function powerConsumerFor(module,stats){const draw=effectiveStat(module,"powerDraw",stats);if(draw===void 0||draw===0)return null;const mounted=stats?.category==="hardpoint"||stats?.category==="utility";return{draw:draw,priority:priorityOf(module),enabled:isEnabled(module),deployedOnly:mounted&&stats?.alwaysPowered!==true,label:module.Slot,symbol:module.Item}}function powerAvailable(modules,statsFor){for(const module of modules){const stats=statsFor(module);if(!isPowerPlant(module,stats))continue;if(!isEnabled(module))return 0;return effectiveStat(module,"powerCapacity",stats)??0}return 0}function metricIssue(field,reason,module){const defaultSlot=field==="powerCapacity"?"PowerPlant":field==="thrusters"?"MainEngines":void 0;const slot=module?.Slot??defaultSlot;const symbol=module?.Item;const params={field:field,reason:reason,...slot===void 0?{}:{slot:slot},...symbol===void 0?{}:{symbol:symbol}};let message;if(reason==="missing"){message=field==="powerCapacity"?"PowerPlant: no power plant is fitted":field==="thrusters"?"MainEngines: no thrusters are fitted":"no shield generator is fitted"}else if(reason==="unresolved"){const unavailable=field==="thrusters"?"thruster stats":field==="shieldGenerator"?"shield-generator stats":field==="powerCapacity"?"power capacity":field;message=`${truncate(slot??field)}: ${unavailable} unavailable for ${truncate(symbol??field)}`}else if(reason==="disabled"){message=`${truncate(slot??field)}: ${truncate(symbol??field)} is switched off`}else if(reason==="invalid"){message=`${truncate(slot??field)}: ${truncate(symbol??field)} has invalid ${field}`}else{message=`${truncate(slot??field)}: ${truncate(symbol??field)} is not powered with hardpoints retracted`}return{field:field,reason:reason,...slot===void 0?{}:{slot:slot},...symbol===void 0?{}:{symbol:symbol},message:message,params:params}}function powerPlantIssueFor(modules,statsFor){for(const module of modules){const stats=statsFor(module);if(!isPowerPlant(module,stats))continue;if(!isEnabled(module))return metricIssue("powerCapacity","disabled",module);const capacity=effectiveStat(module,"powerCapacity",stats);if(capacity===void 0)return metricIssue("powerCapacity","unresolved",module);return!Number.isFinite(capacity)||capacity<=0?metricIssue("powerCapacity","invalid",module):null}return metricIssue("powerCapacity","missing")}function powerDrawIssueFor(modules,statsFor){for(const module of modules){const consumer=powerConsumerFor(module,statsFor(module));if(consumer&&(!Number.isFinite(consumer.draw)||consumer.draw<0)){return metricIssue("powerDraw","invalid",module)}}return null}function poweredMetricBudgetFor(field,module,stats,modules,getBudget,statsFor){if(!isEnabled(module))return incompleteResult([metricIssue(field,"disabled",module)]);const plantIssue=powerPlantIssueFor(modules,statsFor);if(plantIssue)return incompleteResult([plantIssue]);const drawIssue=powerDrawIssueFor(modules,statsFor);if(drawIssue)return incompleteResult([drawIssue]);const budget=getBudget();return budget.available>0&&poweredStates(module,stats,budget).retracted?completeResult(budget):incompleteResult([metricIssue(field,"shed",module)])}function poweredShieldGeneratorResultFor(modules,getBudget,statsFor){for(const module of modules){if(!startsWithAny(module.Item,PREFIX.shieldGenerator))continue;const stats=statsFor(module);const powered=poweredMetricBudgetFor("shieldGenerator",module,stats,modules,getBudget,statsFor);if(!powered.complete)return powered;return stats===null?incompleteResult([metricIssue("shieldGenerator","unresolved",module)]):completeResult({module:module,budget:powered.value})}return incompleteResult([metricIssue("shieldGenerator","missing")])}function mapResistanceFields(read){return{kineticResistance:read("kineticResistance")??0,thermalResistance:read("thermalResistance")??0,explosiveResistance:read("explosiveResistance")??0,causticResistance:read("causticResistance")??0}}function resistancesOf(module,stats){return mapResistanceFields(field=>effectiveStat(module,field,stats))}function shieldInputFor(ship,modules,budget,systemsPips,statsFor){let generator=null;const boosters=[];let reinforcement=0;for(const module of modules){if(!isEnabled(module))continue;const stats=statsFor(module);if(!(budget.available>0&&poweredStates(module,stats,budget).retracted))continue;if(!generator&&startsWithAny(module.Item,PREFIX.shieldGenerator)){const massRatio=modifierRatio(module,stats,"optMass");const strengthRatio=modifierRatio(module,stats,"optMultiplier");const optMass=effectiveStat(module,"optMass",stats);const minMass=relatedStat(module,stats,"minMass",massRatio);const maxMass=relatedStat(module,stats,"maxMass",Math.max(1,massRatio));const optMultiplier=effectiveStat(module,"optMultiplier",stats);const minMultiplier=relatedStat(module,stats,"minMultiplier",strengthRatio);const maxMultiplier=relatedStat(module,stats,"maxMultiplier",strengthRatio);generator={...optMass===void 0?{}:{optMass:optMass},...minMass===void 0?{}:{minMass:minMass},...maxMass===void 0?{}:{maxMass:maxMass},...optMultiplier===void 0?{}:{optMultiplier:optMultiplier},...minMultiplier===void 0?{}:{minMultiplier:minMultiplier},...maxMultiplier===void 0?{}:{maxMultiplier:maxMultiplier},...resistancesOf(module,stats)}}else if(startsWithAny(module.Item,PREFIX.shieldBooster)){boosters.push({shieldBoost:effectiveStat(module,"shieldBoost",stats)??0,...resistancesOf(module,stats)})}else if(startsWithAny(module.Item,PREFIX.shieldReinforcement)){reinforcement+=effectiveStat(module,"shieldAddition",stats)??0}}return{hullMass:ship.hullMass,baseShieldStrength:ship.baseShieldStrength,generator:generator,boosters:boosters,reinforcement:reinforcement,systemsPips:systemsPips}}function shieldInputResultFor(ship,modules,getBudget,systemsPips,statsFor){const fitted=poweredShieldGeneratorResultFor(modules,getBudget,statsFor);if(!fitted.complete)return fitted;const input=shieldInputFor(ship,modules,fitted.value.budget,systemsPips,statsFor);return completeResult(input)}function thrusterCurveFor(module,stats){const effective=effectiveModule(module,stats);const minMass=effective?.minMass;const optMass=effective?.optMass;const maxMass=effective?.maxMass;const minMultiplier=effective?.minMultiplier;const optMultiplier=effective?.optMultiplier;const maxMultiplier=effective?.maxMultiplier;if(minMass===void 0||optMass===void 0||maxMass===void 0||minMultiplier===void 0||optMultiplier===void 0||maxMultiplier===void 0){return null}return{minMass:minMass,optMass:optMass,maxMass:maxMass,minMultiplier:minMultiplier,optMultiplier:optMultiplier,maxMultiplier:maxMultiplier,...effective?.optSpeedMultiplier===void 0?{}:{speedCurve:{minMass:minMass,optMass:optMass,maxMass:maxMass,minMultiplier:effective.minSpeedMultiplier??minMultiplier,optMultiplier:effective.optSpeedMultiplier,maxMultiplier:effective.maxSpeedMultiplier??maxMultiplier}},...effective?.optRotationMultiplier===void 0?{}:{rotationCurve:{minMass:minMass,optMass:optMass,maxMass:maxMass,minMultiplier:effective.minRotationMultiplier??minMultiplier,optMultiplier:effective.optRotationMultiplier,maxMultiplier:effective.maxRotationMultiplier??maxMultiplier}}}}function fittedThrusterParamsFor(modules,statsFor){for(const module of modules){const stats=statsFor(module);if(isThruster(module,stats))return thrusterCurveFor(module,stats)}return null}function mobilityInputResultFor(ship,modules,getBudget,mass,enginesPips,statsFor){let thrusters=null;for(const module of modules){const stats=statsFor(module);if(!isThruster(module,stats))continue;const powered=poweredMetricBudgetFor("thrusters",module,stats,modules,getBudget,statsFor);if(!powered.complete)return powered;thrusters=thrusterCurveFor(module,stats);if(!thrusters)return incompleteResult([metricIssue("thrusters","unresolved",module)]);break}if(!thrusters)return incompleteResult([metricIssue("thrusters","missing")]);return completeResult({minimumSpeed:ship.minimumSpeed,maximumSpeed:ship.maximumSpeed,boost:ship.boost,minPitch:ship.minPitch,pitch:ship.pitch,minRoll:ship.minRoll,roll:ship.roll,minYaw:ship.minYaw,yaw:ship.yaw,mass:typeof mass==="function"?mass():mass,thrusters:thrusters,enginesPips:enginesPips})}function shieldRecoveryInputResultFor(ship,modules,getBudget,systemsPips,statsFor){const generator=poweredShieldGeneratorResultFor(modules,getBudget,statsFor);if(!generator.complete)return generator;const{module:generatorModule,budget:budget}=generator.value;let distributor=null;for(const module of modules){const stats=statsFor(module);if(isPowerDistributor(module,stats)&&isEnabled(module)&&budget.available>0&&poweredStates(module,stats,budget).retracted)distributor=module}const generatorStats=statsFor(generatorModule);const distributorStats=distributor?statsFor(distributor):null;const strength=shieldMetrics(shieldInputFor(ship,modules,budget,0,statsFor)).strength;return completeResult({strength:strength,regenRate:effectiveStat(generatorModule,"shieldRegenRate",generatorStats)??0,brokenRegenRate:effectiveStat(generatorModule,"shieldBrokenRegenRate",generatorStats)??0,distributorDraw:effectiveStat(generatorModule,"distributorDraw",generatorStats)??.6,systemsCapacity:distributor?effectiveStat(distributor,"systemsCapacity",distributorStats)??0:0,systemsRecharge:distributor?effectiveStat(distributor,"systemsRecharge",distributorStats)??0:0,systemsPips:systemsPips})}function weaponsCapacitorInputFor(modules,weaponsPips,budget,statsFor){let weaponsCapacity=0;let weaponsRecharge=0;let sustainedEnergyPerSecond=0;for(const module of modules){if(!isEnabled(module))continue;const stats=statsFor(module);if(!poweredStates(module,stats,budget).deployed)continue;if(isPowerDistributor(module,stats)){weaponsCapacity=effectiveStat(module,"weaponsCapacity",stats)??0;weaponsRecharge=effectiveStat(module,"weaponsRecharge",stats)??0}const weapon=weaponStatsFor(module,stats);if(weapon){sustainedEnergyPerSecond+=weaponMetrics(weapon).sustainedEnergyPerSecond}}return{weaponsCapacity:weaponsCapacity,weaponsRecharge:weaponsRecharge,sustainedEnergyPerSecond:sustainedEnergyPerSecond,weaponsPips:weaponsPips}}function distributorInputFor(modules,pips,budget,statsFor){for(const module of modules){if(!isEnabled(module))continue;const stats=statsFor(module);if(!isPowerDistributor(module,stats))continue;if(!poweredStates(module,stats,budget).retracted)return null;const systemsCapacity=effectiveStat(module,"systemsCapacity",stats);const systemsRecharge=effectiveStat(module,"systemsRecharge",stats);const enginesCapacity=effectiveStat(module,"enginesCapacity",stats);const enginesRecharge=effectiveStat(module,"enginesRecharge",stats);const weaponsCapacity=effectiveStat(module,"weaponsCapacity",stats);const weaponsRecharge=effectiveStat(module,"weaponsRecharge",stats);if(systemsCapacity===void 0||systemsRecharge===void 0||enginesCapacity===void 0||enginesRecharge===void 0||weaponsCapacity===void 0||weaponsRecharge===void 0){return null}return{systemsCapacity:systemsCapacity,systemsRecharge:systemsRecharge,enginesCapacity:enginesCapacity,enginesRecharge:enginesRecharge,weaponsCapacity:weaponsCapacity,weaponsRecharge:weaponsRecharge,...pips}}return null}function heatInputFor(ship,modules,budget,statsFor){let heatEfficiency;let thrusterHeatRate=0;let deployedThrusterHeatRate=0;let fsdHeatRate=0;let weaponsCapacity=0;const weapons=[];for(const module of modules){const stats=statsFor(module);if(isPowerPlant(module,stats)){if(!isEnabled(module))return null;heatEfficiency=effectiveStat(module,"heatEfficiency",stats);continue}if(!isEnabled(module))continue;const running=poweredStates(module,stats,budget);if(isThruster(module,stats)){const heat=effectiveStat(module,"engineHeatRate",stats)??0;if(running.retracted)thrusterHeatRate+=heat;if(running.deployed)deployedThrusterHeatRate+=heat}else if(isFrameShiftDrive(module,stats)){if(running.retracted)fsdHeatRate+=effectiveStat(module,"fsdHeatRate",stats)??0}else if(isPowerDistributor(module,stats)){if(running.deployed){weaponsCapacity=effectiveStat(module,"weaponsCapacity",stats)??0}}const weapon=weaponStatsFor(module,stats);if(!running.deployed)continue;if(!weapon)continue;weapons.push({heatPerSecond:weaponMetrics(weapon).sustainedHeatPerSecond,distributorDraw:weapon.distributorDraw??0})}if(heatEfficiency===void 0)return null;return{heatCapacity:ship.heatCapacity,heatDissipation:ship.heatDissipation,heatEfficiency:heatEfficiency,retractedPowerDraw:poweredDraw(budget,"retracted"),deployedPowerDraw:poweredDraw(budget,"deployed"),thrusterHeatRate:thrusterHeatRate,deployedThrusterHeatRate:deployedThrusterHeatRate,fsdHeatRate:fsdHeatRate,weaponsCapacity:weaponsCapacity,weapons:weapons}}function poweredDraw(budget,state){const powered=state==="retracted"?"poweredRetracted":"poweredDeployed";return budget.bands.reduce((total,band)=>band[powered]?total+band[state]:total,0)}function poweredStates(module,stats,budget){const draw=effectiveStat(module,"powerDraw",stats);if(draw===void 0||draw===0)return{retracted:true,deployed:true};const band=budget.bands[Math.min(budget.bands.length,Math.max(1,priorityOf(module)))-1];if(!band)return{retracted:true,deployed:true};return{retracted:band.poweredRetracted,deployed:band.poweredDeployed}}function cellBankInputsFor(modules,budget,statsFor){const banks=[];for(const module of modules){if(!startsWithAny(module.Item,PREFIX.shieldCellBank))continue;const stats=statsFor(module);const effective=effectiveModule(module,stats);const ammunition=effective?ammunitionCapacity(effective):null;banks.push({slot:module.Slot,symbol:module.Item,reinforcementRate:effective?.shieldBankReinforcement??0,cells:ammunition?.total??0,spinUp:effective?.shieldBankSpinUp??0,duration:effective?.shieldBankDuration??0,heat:effective?.shieldBankHeat??0,powered:budget.available>0&&isEnabled(module)&&poweredStates(module,stats,budget).deployed})}return banks}function stockBulkhead(ship){const variants=getBulkheadsForShip(ship.name,CORE_MODULES);return variants.find(v=>v.symbol.toLowerCase().endsWith("_default"))??variants.find(v=>v.mass===0)??variants[0]??null}function armourInputFor(ship,modules,statsFor){const reinforcements=[];const moduleReinforcements=[];let bulkhead=null;for(const module of modules){if(!isEnabled(module))continue;const stats=statsFor(module);if(module.Slot.toLowerCase()==="armour"){bulkhead={hullBoost:effectiveStat(module,"hullBoost",stats)??0,...resistancesOf(module,stats)}}else if(startsWithAny(module.Item,PREFIX.hullReinforcement)){const boost=getLoadoutModifier(module,"DefenceModifierHealthMultiplier");reinforcements.push({hullReinforcement:effectiveStat(module,"hullReinforcement",stats)??0,hullBoost:boost===null?0:boost/scaleForLabel("DefenceModifierHealthMultiplier"),...resistancesOf(module,stats)})}else if(startsWithAny(module.Item,PREFIX.moduleReinforcement)){moduleReinforcements.push({moduleProtection:effectiveStat(module,"moduleProtection",stats)??0,integrity:effectiveStat(module,"integrity",stats)??0})}}if(!bulkhead){const stock=stockBulkhead(ship);bulkhead=stock?{hullBoost:stock.hullBoost??0,...mapResistanceFields(field=>stock[field])}:null}return{baseArmour:ship.baseArmour,bulkhead:bulkhead,reinforcements:reinforcements,moduleReinforcements:moduleReinforcements}}var WEAPON_FIELDS=["damage","roundsPerShot","rateOfFire","burstInterval","burstRounds","burstRateOfFire","chargeTime","clipSize","ammoMaximum","reloadTime","distributorDraw","thermalLoad","powerDraw","maximumRange","falloffRange","armourPiercing"];function weaponStatsFor(module,stats){if(!stats||stats.category!=="hardpoint")return null;const weapon={};for(const field of WEAPON_FIELDS){const value=effectiveStat(module,field,stats);if(value!==void 0)weapon[field]=value}normalizeEffectiveWeapon(module,weapon);const damageDistribution=effectiveDamageDistribution(module,stats);if(damageDistribution)weapon.damageDistribution=damageDistribution;if(stats.damageComponents&&!convertsDamage(module)){weapon.damageComponents=scaleDamageComponents(stats.damageComponents,stats.damage,typeof weapon.damage==="number"?weapon.damage:void 0)}if(stats.projectileRange)weapon.projectileRange={...stats.projectileRange};return weapon}function burstAdjustedRateOfFire(module,weapon){if(getLoadoutModifier(module,"RateOfFire")!==null)return void 0;const touched=["BurstSize","BurstRateOfFire","BurstInterval"].some(label=>getLoadoutModifier(module,label)!==null);if(!touched)return void 0;return combinedRateOfFire(weapon)}function normalizeEffectiveWeapon(module,weapon){const rate=burstAdjustedRateOfFire(module,weapon);if(rate!==void 0)weapon.rateOfFire=rate;const statedDamagePerSecond=getLoadoutModifier(module,"DamagePerSecond");const firingFactor=Number(weapon.roundsPerShot??1)*Number(weapon.rateOfFire??1);if(statedDamagePerSecond!==null&&firingFactor>0){weapon.damage=statedDamagePerSecond/firingFactor}const{maximumRange:maximumRange,falloffRange:falloffRange}=weapon;if(maximumRange!==void 0&&falloffRange!==void 0&&falloffRange>maximumRange){weapon.falloffRange=maximumRange}}var MILITARY_PREFIXES=["int_hullreinforcement","int_metaalloyhullreinforcement","int_modulereinforcement","int_shieldcellbank","int_guardianhullreinforcement","int_guardianmodulereinforcement","int_guardianshieldreinforcement"];var MINING_PREFIXES=["hpt_mininglaser","hpt_mining_abrblstr","hpt_mining_seismchrgwarhd","hpt_mining_subsurfdispmisle","hpt_human_extraction","hpt_miningtoolv2"];var CARGO_PREFIXES=["int_cargorack","int_largecargorack","int_corrosionproofcargorack","int_fueltank"];var RESTRICTED_SLOT_PREFIXES={mining:MINING_PREFIXES,military:MILITARY_PREFIXES,cargo:CARGO_PREFIXES,limpetController:["int_dronecontrol","int_multidronecontrol"],vesselHangar:["int_fighterbay"],passenger:["int_passengercabin","int_mkii_passengercabin"],planetaryApproachSuite:["int_planetapproachsuite"]};var problem=(constraint,message,params)=>({constraint:constraint,message:message,...params===void 0?{}:{params:params}});function restrictionProblem(slot,symbol){const restriction=slot.restriction;if(!restriction)return null;if(RESTRICTED_SLOT_PREFIXES[restriction].some(prefix=>symbol.startsWith(prefix))){return null}return problem("restrictedMount",`slot only takes ${SLOT_RESTRICTION_LABELS[restriction]}`,{restriction:restriction})}function moduleSlotProblem(slot,module){const required=module.restrictedToSlot;if(!required||slot.restriction===required)return null;return problem("restrictedMount",`module only fits a mount that takes ${SLOT_RESTRICTION_LABELS[required]}`,{restriction:required})}function moduleFitProblem(shipSymbol,slot,module){if(slot.kind==="cargoHatch"){return problem("immutableSlot","the cargoHatch slot cannot be changed")}if(slot.kind==="armour"){const hull=getShipBySymbol(shipSymbol);if(module.slot!=="armour"||module.ship===void 0){return problem("armourRequired","not a ship armour module")}if(!hull||module.ship.toLowerCase()!==hull.name.toLowerCase()){const armourHull=getShipByName(module.ship);return problem("wrongHullArmour",`armour belongs to ${truncate(module.ship)}, not ${truncate(hull?.name??shipSymbol)}`,{armourShipName:module.ship,...armourHull===null?{}:{armourShipSymbol:armourHull.symbol},shipSymbol:shipSymbol,...hull===null?{}:{shipName:hull.name}})}return null}const restricted=module.restrictedToShips;if(restricted&&!restricted.some(symbol2=>symbol2.toLowerCase()===shipSymbol.toLowerCase())){const allowedShipNames=restricted.map(symbol2=>{const hull=getShipBySymbol(symbol2);return hull?.name??symbol2});const labels=restricted.map((symbol2,index)=>{const name=allowedShipNames[index];return name===symbol2?symbol2:`${name} (${symbol2})`});return problem("restrictedHull",`module is restricted to ${truncate(labels.join(", "))}`,{allowedShipNames:allowedShipNames,allowedShipSymbols:[...restricted],shipSymbol:shipSymbol})}const wrongMount=moduleSlotProblem(slot,module);if(wrongMount)return wrongMount;const symbol=module.symbol.toLowerCase();const moduleSlot=module.slot;switch(slot.kind){case"core":if(moduleSlot!==slot.core){return problem("wrongCoreType",`not a ${slot.core} module`,{requiredCore:slot.core,moduleSlot:moduleSlot??"none"})}break;case"hardpoint":{if(module.category!=="hardpoint"){return problem("hardpointRequired","not a hardpoint weapon")}const restricted2=restrictionProblem(slot,symbol);if(restricted2)return restricted2;break}case"utility":if(module.category!=="utility"){return problem("utilityRequired","not a utility module")}return null;case"optional":{const isFuelTank=moduleSlot==="fuelTank";if(module.category!=="internal"&&!isFuelTank){return problem("optionalInternalRequired","not an optional-internal module")}if(moduleSlot&&!isFuelTank){return problem("coreModuleInOptionalSlot","a core module only fits its core slot")}const restricted2=restrictionProblem(slot,symbol);if(restricted2)return restricted2;break}}return module.class>slot.size?problem("oversized",`module size ${truncate(module.class)} exceeds slot size ${slot.size}`,{moduleClass:module.class,slotSize:slot.size}):null}function moduleFitError(shipSymbol,slot,module){return moduleFitProblem(shipSymbol,slot,module)?.message??null}var REBUY_FRACTION=.05;function exportLoadoutEvent(input,options){const fromSource=options.credits==="source";const source=fromSource?input.sourcePurchase:null;const totals=source!==null&&!input.sourceTotalsVoided&&sourceTotalsHold(input.modules,source)?source:null;const hullValue=fromSource?source?.hullValue??null:input.retailHullValue;const modulesValue=fromSource?totals?.modulesValue??null:computedModulesValue(input.modules,input.statsFor);const rebuy=fromSource?totals?.rebuy??null:hullValue===null||modulesValue===null?null:Math.trunc((hullValue+modulesValue)*REBUY_FRACTION);return{Ship:input.shipSymbol.toLowerCase(),...input.shipName===void 0?{}:{ShipName:input.shipName},...input.shipIdent===void 0?{}:{ShipIdent:input.shipIdent},...hullValue===null?{}:{HullValue:hullValue},...modulesValue===null?{}:{ModulesValue:modulesValue},UnladenMass:input.unladenMass,CargoCapacity:input.cargoCapacity,...input.maxJumpRange===null?{}:{MaxJumpRange:input.maxJumpRange},FuelCapacity:{Main:input.fuelCapacity.main,Reserve:input.fuelCapacity.reserve},...rebuy===null?{}:{Rebuy:rebuy},Modules:exportModules(input,options,fromSource)}}function exportModules(input,options,fromSource){const ordered=options.moduleOrder==="slots"?slotOrderedModules(input.modules,input.layout):[...input.modules.values()];return ordered.map(module=>{const on=module.On??(options.explicitPower?true:void 0);const priority=module.Priority??(options.explicitPower?0:void 0);const value=fromSource?sourceModuleValue(module,input.sourcePurchase):moduleValue(module,input.statsFor);return{Slot:module.Slot,Item:module.Item.toLowerCase(),...on===void 0?{}:{On:on},...priority===void 0?{}:{Priority:priority},...module.Health===void 0?{}:{Health:module.Health},...typeof value==="number"?{Value:value}:{},...module.Engineering===void 0?{}:{Engineering:cloneLoadoutModule(module).Engineering}}})}function slotOrderedModules(modules,layout){return orderBySlotLayout([...modules.values()],layout,module=>module.Slot)}function computedModulesValue(modules,statsFor){let sum=0;for(const module of modules.values()){const value=moduleValue(module,statsFor);if(value==="unknown")return null;if(value!=="free")sum+=value}return sum}function moduleValue(module,statsFor){const stats=statsFor(module);if(stats!==null)return stats.cost??"unknown";return isNonOutfittingSlot(module.Slot)||isBuiltInHullModule(module)?"free":"unknown"}var SOURCE_ITEM="ShipLoadout: sourcePurchase moduleValues[].item";function sourceModuleValue(module,source){const entry=source===null?null:getSourceModuleValue(source,module.Slot);if(entry===null)return"unknown";return normalizeKey(entry.item,SOURCE_ITEM)===normalizeKey(module.Item,FITTED_ITEM)?entry.value:"unknown"}function sourceTotalsHold(modules,source){for(const entry of source.moduleValues){const key=matchingKeyIn(modules,entry.slot);const fitted=key===null?void 0:modules.get(key);if(!fitted)return false;if(isCargoHatchSlot(entry.slot)&&entry.value===0)continue;if(normalizeKey(fitted.Item,FITTED_ITEM)!==normalizeKey(entry.item,SOURCE_ITEM)){return false}}return true}function captureLoadoutEvent(event){const modules=event.Modules;const shipName=event.ShipName;const shipIdent=event.ShipIdent;const hullValue=event.HullValue;const modulesValue=event.ModulesValue;const rebuy=event.Rebuy;const unladenMass=event.UnladenMass;const cargoCapacity=event.CargoCapacity;const fuelCapacity=event.FuelCapacity;return{Ship:event.Ship,Modules:Array.isArray(modules)?captureEach(modules,captureModule):modules,...shipName===void 0?{}:{ShipName:shipName},...shipIdent===void 0?{}:{ShipIdent:shipIdent},...hullValue===void 0?{}:{HullValue:hullValue},...modulesValue===void 0?{}:{ModulesValue:modulesValue},...rebuy===void 0?{}:{Rebuy:rebuy},...unladenMass===void 0?{}:{UnladenMass:unladenMass},...cargoCapacity===void 0?{}:{CargoCapacity:cargoCapacity},...fuelCapacity===void 0?{}:{FuelCapacity:{...fuelCapacity}}}}function captureEach(values,capture){const captured=[];const{length:length}=values;for(let index=0;index<length;index++)captured.push(capture(values[index]));return captured}function captureModule(module){if(module===null||typeof module!=="object")return module;const on=module.On;const priority=module.Priority;const health=module.Health;const value=module.Value;const engineering=module.Engineering;return{Slot:module.Slot,Item:module.Item,...on===void 0?{}:{On:on},...priority===void 0?{}:{Priority:priority},...health===void 0?{}:{Health:health},...value===void 0?{}:{Value:value},...engineering===void 0?{}:{Engineering:captureEngineering(engineering)}}}function captureEngineering(engineering){if(engineering===null||typeof engineering!=="object")return engineering;const experimental=engineering.ExperimentalEffect;const experimentalLocalised=engineering.ExperimentalEffect_Localised;const modifiers=engineering.Modifiers;return{BlueprintName:engineering.BlueprintName,Level:engineering.Level,Quality:engineering.Quality,...experimental===void 0?{}:{ExperimentalEffect:experimental},...experimentalLocalised===void 0?{}:{ExperimentalEffect_Localised:experimentalLocalised},...modifiers===void 0?{}:{Modifiers:Array.isArray(modifiers)?captureEach(modifiers,modifier=>modifier!==null&&typeof modifier==="object"?{...modifier}:modifier):modifiers}}}function hullLayout(shipSymbol){const slots=getShipSlots(shipSymbol);return slots?enumerateSlots(slots):[]}function fixedMountRejects(shipSymbol,layout,slot,stats){const mount=layout.find(candidate=>candidate.key.toLowerCase()===slot.toLowerCase());return mount!==void 0&&moduleFitProblem(shipSymbol,mount,stats)!==null}function normalizeLoadoutEvent(rawEvent){const event=captureLoadoutEvent(rawEvent);requireString(event.Ship,"ShipLoadout.fromLoadout: event.Ship");if(!Array.isArray(event.Modules)){throw new TypeError(`ShipLoadout.fromLoadout: event.Modules must be an array, received ${describeValue(event.Modules)}`)}const modules=/* @__PURE__ */new Map;const slots=/* @__PURE__ */new Set;for(const module of event.Modules){if(module===null||typeof module!=="object"){throw new TypeError(`ShipLoadout.fromLoadout: event.Modules[] must hold module objects, received ${describeValue(module)}`)}requireString(module.Slot,"ShipLoadout.fromLoadout: module.Slot");requireString(module.Item,"ShipLoadout.fromLoadout: module.Item");if(module.Engineering!==void 0){if(module.Engineering===null||typeof module.Engineering!=="object"){throw new TypeError(`ShipLoadout.fromLoadout: module.Engineering must be an object, received ${describeValue(module.Engineering)}`)}requireStringIfPresent(module.Engineering.BlueprintName,"ShipLoadout.fromLoadout: module.Engineering.BlueprintName");requireStringIfPresent(module.Engineering.ExperimentalEffect,"ShipLoadout.fromLoadout: module.Engineering.ExperimentalEffect");if(module.Engineering.Modifiers!==void 0&&!Array.isArray(module.Engineering.Modifiers)){throw new TypeError(`ShipLoadout.fromLoadout: module.Engineering.Modifiers must be an array, received ${describeValue(module.Engineering.Modifiers)}`)}for(const[index,modifier]of(module.Engineering.Modifiers??[]).entries()){if(modifier===null||typeof modifier!=="object"){throw new TypeError(`ShipLoadout.fromLoadout: module.Engineering.Modifiers[${index}] must be an object, received ${describeValue(modifier)}`)}requireString(modifier.Label,`ShipLoadout.fromLoadout: module.Engineering.Modifiers[${index}].Label`)}}const normalizedSlot=module.Slot.toLowerCase();if(slots.has(normalizedSlot)){throw new TypeError(`ShipLoadout.fromLoadout: duplicate slot "${truncate(module.Slot)}"`)}slots.add(normalizedSlot);modules.set(module.Slot,cloneLoadoutModule(module))}const top={};if(event.ShipName!==void 0)top.ShipName=event.ShipName;if(event.ShipIdent!==void 0)top.ShipIdent=event.ShipIdent;if(event.HullValue!==void 0)top.HullValue=event.HullValue;if(event.ModulesValue!==void 0)top.ModulesValue=event.ModulesValue;if(event.Rebuy!==void 0)top.Rebuy=event.Rebuy;if(event.UnladenMass!==void 0)top.UnladenMass=event.UnladenMass;if(event.CargoCapacity!==void 0)top.CargoCapacity=event.CargoCapacity;if(event.FuelCapacity!==void 0)top.FuelCapacity={...event.FuelCapacity};let invalidatesAggregates=false;const outcomes=[];const defaults=getDefaultLoadout(event.Ship)?.modules??[];let layout;for(const[slot,module]of modules){if(isNonOutfittingSlot(slot)||isBuiltInHullModule(module))continue;const stats=builtInModuleBySymbol(module.Item,"ShipLoadout.fromLoadout: module.Item");const slotKind=parseSlotName(slot)?.kind;const fallback=slotKind==="core"||slotKind==="armour"||slotKind==="cargoHatch"?defaults.find(candidate=>candidate.slot.toLowerCase()===slot.toLowerCase()):void 0;const rejected=fallback!==void 0&&(stats===null||slotKind==="cargoHatch"||fixedMountRejects(event.Ship,layout??=hullLayout(event.Ship),slot,stats));if(stats!==null&&!rejected)continue;if(fallback){modules.set(slot,{Slot:slot,Item:fallback.symbol,...module.On===void 0?{}:{On:module.On},...module.Priority===void 0?{}:{Priority:module.Priority},...module.Health===void 0?{}:{Health:module.Health}});outcomes.push({action:"defaulted",slot:slot,sourceSymbol:module.Item,replacementSymbol:fallback.symbol})}else{modules.delete(slot);outcomes.push({action:"emptied",slot:slot,sourceSymbol:module.Item})}invalidatesAggregates=true}for(const fallback of defaults){const slotKind=parseSlotName(fallback.slot)?.kind;if(slotKind!=="core"&&slotKind!=="armour"&&slotKind!=="cargoHatch"||matchingKeyIn(modules,fallback.slot)!==null){continue}const slot=ownKeyIn(modules,fallback.slot);modules.set(slot,{Slot:slot,Item:fallback.symbol});outcomes.push({action:"defaulted",slot:slot,sourceSymbol:null,replacementSymbol:fallback.symbol});if(slotKind==="core")invalidatesAggregates=true}if(invalidatesAggregates){delete top.ModulesValue;delete top.Rebuy;delete top.UnladenMass;delete top.CargoCapacity;delete top.FuelCapacity}const moduleStats=/* @__PURE__ */new Map;const primitiveModifiers=/* @__PURE__ */new Map;for(const module of modules.values()){const engineering=module.Engineering;const variant=identifyPreEngineeredVariant(module);const catalogueVariantStats=variant?getPreEngineeredStats(variant):null;let variantStats=catalogueVariantStats;const retainsBakedExperimental=variant?.experimental!==void 0&&typeof engineering?.ExperimentalEffect==="string"&&variant.experimental.trim().toLowerCase()===engineering.ExperimentalEffect.trim().toLowerCase();if(variant&&catalogueVariantStats&&!catalogueVariantStats.engineeringLocked&&!retainsBakedExperimental){const{experimental:originalExperimental,...withoutExperimental}=variant;const currentExperimental=engineering?.ExperimentalEffect;const currentVariant=typeof currentExperimental==="string"?{...withoutExperimental,experimental:currentExperimental}:withoutExperimental;variantStats=getPreEngineeredStats(withoutExperimental);primitiveModifiers.set(module.Slot,[...engineering?.Modifiers??[],...getPreEngineeredModifiers(currentVariant)])}if(variantStats)moduleStats.set(module.Slot,cloneModuleStats(variantStats));const guardianFinal=typeof engineering?.BlueprintName==="string"&&isFinalGuardianWeaponEngineering(module.Item,engineering.BlueprintName);if(variantStats?.engineeringLocked||typeof engineering?.BlueprintName!=="string"||!guardianFinal){continue}const normalizedBlueprint=normalizeKey(engineering.BlueprintName,"ShipLoadout.fromLoadout: module.Engineering.BlueprintName");const normalizedExperimental=normalizeKey(engineering.ExperimentalEffect,"ShipLoadout.fromLoadout: module.Engineering.ExperimentalEffect");const variants=getPreEngineeredVariants(module.Item);const exact=variants.find(candidate=>candidate.engineeringLocked===true&&candidate.blueprint.toLowerCase()===normalizedBlueprint&&candidate.grade===engineering.Level&&candidate.experimental?.toLowerCase()===normalizedExperimental);const guardianBase=variants.find(candidate=>candidate.engineeringLocked===true&&candidate.blueprint.toLowerCase()===normalizedBlueprint&&candidate.grade===engineering.Level&&candidate.experimental===void 0);const stats=exact?getPreEngineeredStats(exact):guardianBase&&engineering.ExperimentalEffect!==void 0?getPreEngineeredStats({...guardianBase,experimental:engineering.ExperimentalEffect}):builtInModuleBySymbol(module.Item,"ShipLoadout.fromLoadout: module.Item");if(stats){moduleStats.set(module.Slot,cloneModuleStats({...stats,engineeringLocked:true}))}}return{shipSymbol:event.Ship,modules:modules,moduleStats:moduleStats,primitiveModifiers:primitiveModifiers,top:top,sourcePurchase:sourcePurchaseFromLoadout(event),outcomes:deepFreeze(outcomes)}}var SLOT_KEY="ShipLoadout: slotKey";var requireLoadOptions=(scope,options)=>{for(const field of["fuel","cargo"]){const value=options[field];if(value!==void 0&&(!Number.isFinite(value)||value<0)){throw new RangeError(`${scope}: ${field} must be a finite non-negative number`)}}};var requireEnginesPips=(scope,enginesPips)=>{if(!Number.isFinite(enginesPips)||enginesPips<0||enginesPips>4){throw new RangeError(`${scope}: enginesPips must be a finite number from 0 to 4`)}return enginesPips};var requireSystemsPips=(scope,systemsPips)=>{if(!Number.isFinite(systemsPips)||systemsPips<0||systemsPips>4){throw new RangeError(`${scope}: systemsPips must be a finite number from 0 to 4`)}return systemsPips};var LoadoutEditError=class extends TypeError{code;constraint;params;constructor(message,code,params,constraint){super(message);this.code=code;this.params=deepFreeze(structuredClone(params));if(constraint!==void 0)this.constraint=constraint}};var experimentalEffectUnsupported=(code,params)=>deepFreeze({kind:"unsupported",code:code,params:params});var engineeringNormalizationUnsupported=(code,params)=>deepFreeze({kind:"unsupported",code:code,params:params});var AGGREGATE_STATS=["mass","cargoCapacity","fuelCapacity"];var ShipLoadout=class _ShipLoadout{#ship;#shipSymbol;#modules;#moduleStats;#primitiveModifiers;#top;#sourcePurchase;#importOutcomes;#slotCache=/* @__PURE__ */new Map;#layoutCache;constructor(ship,shipSymbol,modules,top,sourcePurchase=null,moduleStats=/* @__PURE__ */new Map,primitiveModifiers=/* @__PURE__ */new Map,importOutcomes=deepFreeze([])){this.#ship=ship;this.#shipSymbol=shipSymbol;this.#modules=modules;this.#top=top;this.#sourcePurchase=sourcePurchase;this.#moduleStats=moduleStats;this.#primitiveModifiers=primitiveModifiers;this.#importOutcomes=importOutcomes}static fromSlef(input,index=0){const entries=parseSlef(input);const entry=entries[index];if(!entry){throw new TypeError(`ShipLoadout.fromSlef: no entry at index ${truncate(index)} (have ${entries.length})`)}const imported=normalizeLoadoutEvent(entry.data);const ship=getShipBySymbol(imported.shipSymbol);if(!ship){throw new TypeError(`ShipLoadout.fromSlef: unknown hull "${truncate(imported.shipSymbol)}"`)}return _ShipLoadout.#fromImported(imported,ship)}static fromLoadout(event){if(event===null||typeof event!=="object"){throw new TypeError(`ShipLoadout.fromLoadout: event must be a Loadout event, received ${describeValue(event)}`)}const imported=normalizeLoadoutEvent(event);const ship=getShipBySymbol(imported.shipSymbol);if(!ship){throw new TypeError(`ShipLoadout.fromLoadout: unknown hull "${truncate(imported.shipSymbol)}"`)}return _ShipLoadout.#fromImported(imported,ship)}static#fromImported(imported,ship){return new _ShipLoadout(ship,imported.shipSymbol,imported.modules,imported.top,imported.sourcePurchase,imported.moduleStats,imported.primitiveModifiers,imported.outcomes)}static empty(shipSymbol){const requested=requireString(shipSymbol,"ShipLoadout.empty: shipSymbol");const ship=getShipBySymbol(requested);if(!ship){throw new TypeError(`ShipLoadout.empty: no slot layout for hull "${truncate(shipSymbol)}"`)}const fitted=(getDefaultLoadout(ship.symbol)?.modules??[]).filter(module=>{const parsed=parseSlotName(module.slot);return parsed!==null&&fixedSlotReason(parsed)!==null});return new _ShipLoadout(ship,ship.symbol,new Map(fitted.map(module=>[module.slot,{Slot:module.slot,Item:module.symbol}])),{})}static default(shipSymbol){const requested=requireString(shipSymbol,"ShipLoadout.default: shipSymbol");const ship=getShipBySymbol(requested);if(!ship){throw new TypeError(`ShipLoadout.default: no default loadout for hull "${truncate(shipSymbol)}"`)}const loadout=getDefaultLoadout(ship.symbol);return new _ShipLoadout(ship,loadout.symbol,new Map(loadout.modules.map(module=>[module.slot,{Slot:module.slot,Item:module.symbol}])),{})}get shipSymbol(){return this.#shipSymbol}get shipName(){return this.#top.ShipName??null}get shipIdent(){return this.#top.ShipIdent??null}get unladenMass(){return this.#top.UnladenMass??this.#computedUnladenMass()}#computedUnladenMass(){return calculateUnladenMass(this.#ship.hullMass,this.#calculationModules())}get fuelCapacity(){const cap=this.#top.FuelCapacity;if(cap?.Main!==void 0&&cap.Reserve!==void 0){return Object.freeze({main:cap.Main,reserve:cap.Reserve})}const computed=this.#computedFuelCapacity();return Object.freeze({main:cap?.Main??computed.main,reserve:cap?.Reserve??computed.reserve})}get cargoCapacity(){return this.#top.CargoCapacity??this.#computedCargoCapacity()}#computedCargoCapacity(){return calculateCargoCapacity(this.#calculationModules())}#computedFuelCapacity(){return calculateFuelCapacity(this.#ship.reserveFuelCapacity,this.#calculationModules())}get hullValue(){return this.#top.HullValue??null}get modulesValue(){return this.#top.ModulesValue??null}get rebuy(){return this.#top.Rebuy??null}get sourcePurchase(){return this.#sourcePurchase}get importOutcomes(){return this.#importOutcomes}get validation(){const slots=this.#layout();const byKey=new Map(slots.map(slot=>[slot.key.toLowerCase(),slot]));const modules=[...this.#modules.values()].map(module=>{const stats=this.#statsFor(module);const slot=byKey.get(module.Slot.toLowerCase());const builtIn=stats===null&&isNonOutfittingSlot(module.Slot)||isBuiltInHullModule(module);const fitProblem=stats&&slot&&!builtIn?moduleFitProblem(this.#shipSymbol,slot,stats):null;return{slot:module.Slot,symbol:module.Item,requiresKnownSlot:!builtIn,fitError:fitProblem?.message??null,...fitProblem===null?{}:{fitConstraint:fitProblem.constraint},...fitProblem?.params===void 0?{}:{fitParams:fitProblem.params},...stats?.exclusionGroup===void 0?{}:{exclusionGroup:stats.exclusionGroup},...stats?.limitGroup===void 0?{}:{limitGroup:stats.limitGroup},...stats?.limitIncrease===void 0?{}:{limitIncrease:stats.limitIncrease}}});return validateLoadout({shipSymbol:this.#shipSymbol,slots:slots,modules:modules})}slots(kind){const cached=this.#slotCache.get(kind);if(cached!==void 0)return cached;const slots=kind===void 0?this.#layout():this.#layout().filter(slot=>slot.kind===kind);const currentLimits=this.#moduleLimits();const value=deepFreeze(slots.map(slot=>{const stats=this.#moduleStatsAt(slot.key);const fixedReason=fixedSlotReason(slot);const immovableReason=fixedReason!==null?fixedReason:stats?.limitIncrease!==void 0&&this.#moduleLimitRegression(slot.key,null,currentLimits,stats)!==null?"moduleLimit":null;return{...slot,name:loadoutSlotName(slot),module:this.fittedModuleAt(slot.key),removable:immovableReason===null,...immovableReason===null?{}:{immovableReason:immovableReason}}}));this.#slotCache.set(kind,value);return value}fittedModuleAt(slotKey){const module=this.#fittedModuleFor(slotKey);if(!module)return null;const raw=cloneLoadoutModule(module);const stats=this.#statsFor(module);const effective=effectiveModule(this.#moduleForEffectiveStats(module),stats);return deepFreeze({slot:module.Slot,symbol:raw.Item,on:raw.On,priority:raw.Priority,health:raw.Health,value:raw.Value,engineering:raw.Engineering,raw:raw,stats:stats===null?null:cloneModuleStats(stats),effectiveStats:effective===null?null:cloneModuleStats(effective),ammunition:ammunitionCapacity(effective),preEngineeredVariant:identifyPreEngineeredVariant(raw)})}fittedModules(){return deepFreeze([...this.#modules.keys()].flatMap(key=>{const fitted=this.fittedModuleAt(key);return fitted===null?[]:[fitted]}))}availableBlueprints(slotKey){const module=this.#fittedModuleFor(slotKey);return deepFreeze(module?availableBlueprintsFor(module.Item,this.#statsFor(module)):[])}availableExperimentalEffects(slotKey){const module=this.#fittedModuleFor(slotKey);return deepFreeze(module?availableExperimentalsFor(module.Item,this.#statsFor(module)):[])}modulesForSlot(slotKey){const slot=this.#requireSlot(slotKey);const occupied=new Set([...this.#modules.values()].flatMap(fitted=>{if(fitted.Slot.toLowerCase()===slot.key.toLowerCase())return[];const group=this.#statsFor(fitted)?.exclusionGroup;return group===void 0?[]:[group]}));const currentLimits=this.#moduleLimits();const currentStats=this.#moduleStatsAt(slot.key);return ALL_MODULES.filter(module=>moduleFitError(this.#shipSymbol,slot,module)===null&&(module.exclusionGroup===void 0||!occupied.has(module.exclusionGroup))&&this.#moduleLimitRegression(slot.key,module,currentLimits,currentStats)===null)}repairFixedMount(slotKey){const requested=requireString(slotKey,SLOT_KEY);const wanted=requested.toLowerCase();const slot=this.#requireSlot(requested);const fittedKey=this.#fittedKey(requested);const resultSlot=fittedKey??requested;const canonicalKey=slot.key;if(fixedSlotReason(slot)===null){return deepFreeze({status:"refused",slot:resultSlot,reason:"notFixedMount"})}const current=this.#fittedModuleFor(requested);const currentStats=current?this.#statsFor(current):null;const valid=current!==void 0&¤tStats!==null&&(slot.kind==="cargoHatch"?isBuiltInHullModule(current):moduleFitProblem(this.#shipSymbol,slot,currentStats)===null);if(valid){return deepFreeze({status:"unchanged",slot:current.Slot,symbol:current.Item})}const defaultModule=getDefaultLoadout(this.#shipSymbol)?.modules.find(module=>module.slot.toLowerCase()===wanted);if(!defaultModule){return deepFreeze({status:"defaultUnavailable",slot:resultSlot})}const ownKey=ownKeyIn(this.#modules,canonicalKey);const replacement={Slot:ownKey,Item:defaultModule.symbol,...current?.On===void 0?{}:{On:current.On},...current?.Priority===void 0?{}:{Priority:current.Priority},...current?.Health===void 0?{}:{Health:current.Health}};const replacementStats=builtInModuleBySymbol(defaultModule.symbol,"ShipLoadout.repairFixedMount")??(isBuiltInHullModule(replacement)?builtInModuleBySymbol("ModularCargoBayDoor","ShipLoadout.repairFixedMount"):null);if(!replacementStats){return deepFreeze({status:"defaultUnavailable",slot:resultSlot,symbol:defaultModule.symbol})}this.#replaceModule(ownKey,replacement,replacementStats);return deepFreeze({status:"repaired",slot:ownKey,symbol:defaultModule.symbol})}setModule(slotKey,module){const slot=this.#requireSlot(slotKey);if(module===null||module===void 0){throw new TypeError(`ShipLoadout.setModule: no module supplied for "${truncate(slotKey)}" (did the module lookup return undefined?)`)}if(typeof module.symbol!=="string"){throw new TypeError(`ShipLoadout.setModule: module for "${truncate(slotKey)}" must be an outfitting module, received ${describeValue(module)}`)}module=cloneModuleStats(module);const catalogued=builtInModuleBySymbol(module.symbol,FITTED_ITEM);if(catalogued===null){throw new TypeError(`ShipLoadout.setModule: no module is catalogued as "${truncate(module.symbol)}"`)}for(const field of AGGREGATE_STATS){const stated=module[field];if(stated===void 0){if(catalogued[field]===void 0)continue;throw new TypeError(`ShipLoadout.setModule: the supplied record for "${truncate(module.symbol)}" has no ${field}`)}if(typeof stated!=="number"||!Number.isFinite(stated)){throw new TypeError(`ShipLoadout.setModule: the supplied record for "${truncate(module.symbol)}" states a ${field} of ${describeValue(stated)}`)}}const problem2=moduleFitProblem(this.#shipSymbol,slot,module);if(problem2){if(problem2.constraint==="immutableSlot"){throw new LoadoutEditError(`ShipLoadout.setModule: ${truncate(module.symbol)} → ${truncate(slotKey)}: ${problem2.message}`,"immutableSlot",{slot:slot.key})}throw new LoadoutEditError(`ShipLoadout.setModule: ${truncate(module.symbol)} → ${truncate(slotKey)}: ${problem2.message}`,"incompatibleModule",{...problem2.params,slot:slot.key,symbol:module.symbol,constraint:problem2.constraint},problem2.constraint)}if(module.exclusionGroup!==void 0){const conflict=[...this.#modules.values()].find(fitted=>fitted.Slot.toLowerCase()!==slot.key.toLowerCase()&&this.#statsFor(fitted)?.exclusionGroup===module.exclusionGroup);if(conflict){throw new LoadoutEditError(`ShipLoadout.setModule: ${truncate(module.symbol)} → ${truncate(slotKey)}: ${module.exclusionGroup} is limited to one per ship (already fitted in ${truncate(conflict.Slot)})`,"duplicateExclusiveModule",{exclusionGroup:module.exclusionGroup,slot:slot.key,symbol:module.symbol,previousSlot:conflict.Slot,previousSymbol:conflict.Item})}}const limitProblem=this.#moduleLimitRegression(slot.key,module);if(limitProblem!==null){throw new LoadoutEditError(`ShipLoadout.setModule: ${truncate(module.symbol)} → ${truncate(slotKey)}: ${limitProblem.group} would have ${limitProblem.count} modules but the ship allows ${limitProblem.limit}`,"moduleLimitExceeded",{group:limitProblem.group,count:limitProblem.count,limit:limitProblem.limit,slot:slot.key,symbol:module.symbol})}const key=this.#fittedKey(slotKey)??slot.key;this.#replaceModule(key,{Slot:key,Item:module.symbol},module);return this}removeModule(slotKey){const wanted=requireString(slotKey,SLOT_KEY).toLowerCase();if(wanted==="cargohatch"){throw new LoadoutEditError("ShipLoadout.removeModule: the cargoHatch slot cannot be changed","immutableSlot",{slot:"CargoHatch"})}const parsed=parseSlotName(slotKey);const slot=this.#layout().find(candidate=>candidate.key.toLowerCase()===wanted);if(parsed!==null&&fixedSlotReason(parsed)==="requiredSlot"){const fittedKey=this.#fittedKey(slotKey);const key2=slot?.key??fittedKey??slotKey;throw new LoadoutEditError(`ShipLoadout.removeModule: the ${truncate(key2)} slot is required and cannot be emptied`,"requiredSlot",{slot:key2})}const key=this.#fittedKey(slotKey);if(key!==null){const limitProblem=this.#moduleLimitRegression(key,null);if(limitProblem!==null){const fitted=this.#modules.get(key);throw new LoadoutEditError(`ShipLoadout.removeModule: ${truncate(slotKey)}: ${limitProblem.group} would have ${limitProblem.count} modules but the ship allows ${limitProblem.limit}`,"moduleLimitExceeded",{group:limitProblem.group,count:limitProblem.count,limit:limitProblem.limit,slot:key,...fitted===void 0?{}:{symbol:fitted.Item}})}this.#replaceModule(key,null)}return this}applyBlueprint(slotKey,fdname,options){requireString(fdname,"ShipLoadout.applyBlueprint: fdname");if(options===null||typeof options!=="object"){throw new TypeError(`ShipLoadout.applyBlueprint: options must be an object with a grade, received ${describeValue(options)}`)}const wantedGrade=options.grade;const wantedQuality=options.quality;const wantedExperimental=options.experimental??void 0;requireStringIfPresent(wantedExperimental,"ShipLoadout.applyBlueprint: options.experimental");const module=this.#fittedModuleFor(slotKey);if(!module){throw new RangeError(`ShipLoadout.applyBlueprint: slot "${truncate(slotKey)}" is empty`)}const fittedStats=this.#statsFor(module);if(!fittedStats){throw new TypeError(`ShipLoadout.applyBlueprint: no stats for module "${truncate(module.Item)}"`)}const recipe=resolveBlueprintForModule(module.Item,fdname);const named=recipe===fdname?`"${truncate(fdname)}"`:`"${truncate(fdname)}" (${truncate(recipe)} on this module)`;const written=fdname.trim().toLowerCase();const resolved=recipe.trim().toLowerCase();if(getPreEngineeredVariants(module.Item).some(variant=>variant.acquisition==="eventReward"&&(variant.blueprint.toLowerCase()===written||variant.blueprint.toLowerCase()===resolved))){throw new TypeError(`ShipLoadout.applyBlueprint: ${named} is a fixed pre-engineered identity, not a craftable blueprint; use setPreEngineeredVariant to fit its fixed article`)}if(!Number.isInteger(wantedGrade)||wantedGrade<1||wantedGrade>5){throw new RangeError(`ShipLoadout.applyBlueprint: no blueprint ${named} grade ${truncate(wantedGrade)}`)}const grade=getBlueprintGrade(recipe,wantedGrade);if(!grade){throw new RangeError(`ShipLoadout.applyBlueprint: no blueprint ${named} grade ${truncate(wantedGrade)}`)}let experimental;if(wantedExperimental!==void 0){experimental=getExperimentalEffect(wantedExperimental);if(!experimental){throw new RangeError(`ShipLoadout.applyBlueprint: unknown experimental effect "${truncate(wantedExperimental)}"`)}}const quality=wantedQuality??1;if(!Number.isFinite(quality)||quality<0||quality>1){throw new RangeError(`ShipLoadout.applyBlueprint: quality must be a finite number in [0, 1]`)}if(fittedStats.engineeringLocked){throw new TypeError(`ShipLoadout.applyBlueprint: module "${truncate(module.Item)}" is a final pre-engineered article and accepts no further engineering`)}if(!blueprintAvailableFor(module.Item,fdname)){const offered=[...blueprintRoutesFor(module.Item)].map(([blueprint,route])=>`${blueprint} (${route})`);throw new TypeError(offered.length>0?`ShipLoadout.applyBlueprint: module "${truncate(module.Item)}" is not offered blueprint ${named}; available candidates are ${offered.join(", ")}`:`ShipLoadout.applyBlueprint: no registry lists an engineering menu for module "${truncate(module.Item)}"`)}if(wantedExperimental!==void 0&&!experimentalAvailableFor(module.Item,wantedExperimental)){const offered=getExperimentalsForModule(module.Item);throw new TypeError(`ShipLoadout.applyBlueprint: module "${truncate(module.Item)}" is not offered experimental effect "${truncate(wantedExperimental)}"; it takes ${offered.length>0?offered.join(", "):"no experimental effect"}`)}const stats=this.#engineeringBaseStats(module)??fittedStats;const base=baseStats(stats);const canonical=primitiveEngineeringInputsFor(stats,grade,experimental);const missing=missingBaseLabels(stats,base,canonical.grade.features,canonical.experimental?.modifiers);if(missing.length>0){throw new TypeError(`ShipLoadout.applyBlueprint: cannot compute ${named} for module "${truncate(module.Item)}"; missing base stats for ${missing.join(", ")}`)}const damageDistribution=experimental?.damageDistribution??grade.damageDistribution;const primitiveModifiers=computeModifiers(base,canonical.grade,quality,canonical.experimental);const modifiers=journalModifiersFor(stats,primitiveModifiers);if(damageDistribution){for(const type of["kinetic","thermal","explosive","absolute"]){const value=damageDistribution[type];if(value===void 0)continue;const label=labelsForDamageType(type)[0];if(label===void 0)continue;const damageModifier={Label:label,Value:value*scaleForLabel(label),OriginalValue:(stats.damageDistribution?.[type]??0)*scaleForLabel(label)};modifiers.push(damageModifier);primitiveModifiers.push(damageModifier)}}const engineering={BlueprintName:fdname,Level:wantedGrade,Quality:quality,...wantedExperimental!==void 0?{ExperimentalEffect:wantedExperimental}:{},Modifiers:modifiers};this.#replaceModule(module.Slot,{...module,Engineering:engineering},stats,primitiveModifiers);return this}setExperimentalEffect(slotKey,experimental){if(experimental!==null){requireString(experimental,"ShipLoadout.setExperimentalEffect: experimental")}const wanted=experimental;const module=this.#fittedModuleFor(slotKey);if(!module)return experimentalEffectUnsupported("emptySlot",{slot:slotKey});const stats=this.#statsFor(module);if(!stats){return experimentalEffectUnsupported("unsupportedEngineering",{slot:module.Slot,symbol:module.Item})}const engineering=module.Engineering;if(!engineering){return experimentalEffectUnsupported("notEngineered",{slot:module.Slot,symbol:module.Item})}if(stats.engineeringLocked){return experimentalEffectUnsupported("finalArticle",{slot:module.Slot,symbol:module.Item})}const previous=engineering.ExperimentalEffect??null;if(previous===null&&wanted===null||previous!==null&&wanted!==null&&previous.trim().toLowerCase()===wanted.trim().toLowerCase()){return deepFreeze({kind:"unchanged",experimental:previous})}const variant=identifyPreEngineeredVariant(module);let effect;if(wanted!==null){effect=getExperimentalEffect(wanted);if(!effect){return experimentalEffectUnsupported("unknownExperimentalEffect",{slot:module.Slot,symbol:module.Item,experimental:wanted})}const restoresBakedEffect=variant?.experimental!==void 0&&variant.experimental.trim().toLowerCase()===wanted.trim().toLowerCase();if(!experimentalAvailableFor(module.Item,wanted)&&!restoresBakedEffect){return experimentalEffectUnsupported("unsupportedExperimentalEffect",{slot:module.Slot,symbol:module.Item,experimental:wanted})}}if(variant&&variant.acquisition!=="mercenary"){const stock=this.#engineeringBaseStats(module);if(!stock){return experimentalEffectUnsupported("unsupportedEngineering",{slot:module.Slot,symbol:module.Item})}const missing=missingBaseLabels(stock,baseStats(stock),[],effect?.modifiers);if(missing.length>0){return experimentalEffectUnsupported("unresolvedModifiers",{slot:module.Slot,symbol:module.Item,labels:missing})}const{experimental:originalEffect,...withoutEffect}=variant;const adjusted=wanted===null?withoutEffect:{...withoutEffect,experimental:wanted};const resolved=getPreEngineeredStats(withoutEffect);if(!resolved){return experimentalEffectUnsupported("unsupportedEngineering",{slot:module.Slot,symbol:module.Item})}const replacement={BlueprintName:engineering.BlueprintName,Level:engineering.Level,Quality:engineering.Quality,...wanted===null?{}:{ExperimentalEffect:wanted},Modifiers:getPreEngineeredJournalModifiers(adjusted)};this.#replaceModule(module.Slot,{...module,Engineering:replacement},cloneModuleStats(resolved),getPreEngineeredModifiers(adjusted))}else{const blueprint=engineering.BlueprintName;const grade=engineering.Level;const quality=engineering.Quality;if(typeof blueprint!=="string"||!Number.isInteger(grade)||grade<1||grade>5){return experimentalEffectUnsupported("unsupportedEngineering",{slot:module.Slot,symbol:module.Item})}const currentEffect=previous===null?void 0:getExperimentalEffect(previous)??null;if(ordinaryEngineeringProof(module.Item,engineering,currentEffect)==="unproven"){return experimentalEffectUnsupported("unidentifiedPreEngineeredVariant",{slot:module.Slot,symbol:module.Item,blueprint:blueprint})}const recipe=resolveBlueprintForModule(module.Item,blueprint);const blueprintGrade=getBlueprintGrade(recipe,grade);if(!blueprintGrade||!blueprintAvailableFor(module.Item,blueprint)||!Number.isFinite(quality)||quality<0||quality>1){return experimentalEffectUnsupported("unsupportedEngineering",{slot:module.Slot,symbol:module.Item,blueprint:blueprint,grade:grade})}const base=this.#engineeringBaseStats(module)??stats;const canonical=primitiveEngineeringInputsFor(base,blueprintGrade,effect);const missing=missingBaseLabels(base,baseStats(base),canonical.grade.features,canonical.experimental?.modifiers);if(missing.length>0){return experimentalEffectUnsupported("unresolvedModifiers",{slot:module.Slot,symbol:module.Item,labels:missing})}this.applyBlueprint(module.Slot,blueprint,{grade:grade,quality:quality,...wanted===null?{}:{experimental:wanted}})}return deepFreeze({kind:"updated",previousExperimental:previous,experimental:wanted})}completeEngineeringGrade(slotKey){const module=this.#fittedModuleFor(slotKey);if(!module)return engineeringNormalizationUnsupported("emptySlot",{slot:slotKey});const stats=this.#statsFor(module);if(!stats){return engineeringNormalizationUnsupported("unsupportedEngineering",{slot:module.Slot,symbol:module.Item})}const engineering=module.Engineering;if(!engineering){return engineeringNormalizationUnsupported("notEngineered",{slot:module.Slot,symbol:module.Item})}if(stats.engineeringLocked){return engineeringNormalizationUnsupported("finalArticle",{slot:module.Slot,symbol:module.Item})}if(engineering.Quality===1&&engineering.Modifiers!==void 0){return deepFreeze({kind:"unchanged"})}const previousQuality=engineering.Quality;if(!Number.isFinite(previousQuality)||previousQuality<0||previousQuality>1){return engineeringNormalizationUnsupported("invalidQuality",{slot:module.Slot,symbol:module.Item})}const experimental=engineering.ExperimentalEffect??null;const effect=experimental===null?void 0:getExperimentalEffect(experimental)??void 0;if(experimental!==null&&!effect){return engineeringNormalizationUnsupported("unknownExperimentalEffect",{slot:module.Slot,symbol:module.Item,experimental:experimental})}const blueprint=engineering.BlueprintName;const grade=engineering.Level;if(typeof blueprint!=="string"||!Number.isInteger(grade)||grade<1||grade>5){return engineeringNormalizationUnsupported("unsupportedEngineering",{slot:module.Slot,symbol:module.Item})}const variant=identifyPreEngineeredVariant(module);const restoresBakedEffect=variant?.experimental!==void 0&&variant.experimental.trim().toLowerCase()===experimental?.trim().toLowerCase();if(experimental!==null&&!experimentalAvailableFor(module.Item,experimental)&&!restoresBakedEffect){return engineeringNormalizationUnsupported("unsupportedExperimentalEffect",{slot:module.Slot,symbol:module.Item,experimental:experimental})}if(variant&&variant.acquisition!=="mercenary"){const stock=this.#engineeringBaseStats(module);if(!stock){return engineeringNormalizationUnsupported("unsupportedEngineering",{slot:module.Slot,symbol:module.Item})}const missing=missingBaseLabels(stock,baseStats(stock),[],effect?.modifiers);if(missing.length>0){return engineeringNormalizationUnsupported("unresolvedModifiers",{slot:module.Slot,symbol:module.Item,labels:missing})}const{experimental:originalEffect,...withoutEffect}=variant;const adjusted=experimental===null?withoutEffect:{...withoutEffect,experimental:experimental};const resolved=getPreEngineeredStats(withoutEffect);if(!resolved){return engineeringNormalizationUnsupported("unsupportedEngineering",{slot:module.Slot,symbol:module.Item})}const replacement={BlueprintName:engineering.BlueprintName,Level:engineering.Level,Quality:1,...experimental===null?{}:{ExperimentalEffect:experimental},Modifiers:getPreEngineeredJournalModifiers(adjusted)};this.#replaceModule(module.Slot,{...module,Engineering:replacement},cloneModuleStats(resolved),getPreEngineeredModifiers(adjusted))}else{const proof=ordinaryEngineeringProof(module.Item,engineering,effect);if(proof==="unproven"||proof==="proven"&&!blueprintAvailableFor(module.Item,blueprint)){return engineeringNormalizationUnsupported("unidentifiedPreEngineeredVariant",{slot:module.Slot,symbol:module.Item,blueprint:blueprint})}const recipe=resolveBlueprintForModule(module.Item,blueprint);const blueprintGrade=getBlueprintGrade(recipe,grade);if(!blueprintGrade||!blueprintAvailableFor(module.Item,blueprint)){return engineeringNormalizationUnsupported("unsupportedEngineering",{slot:module.Slot,symbol:module.Item,blueprint:blueprint,grade:grade})}const base=this.#engineeringBaseStats(module)??stats;const canonical=primitiveEngineeringInputsFor(base,blueprintGrade,effect);const missing=missingBaseLabels(base,baseStats(base),canonical.grade.features,canonical.experimental?.modifiers);if(missing.length>0){return engineeringNormalizationUnsupported("unresolvedModifiers",{slot:module.Slot,symbol:module.Item,labels:missing})}this.applyBlueprint(module.Slot,blueprint,{grade:grade,quality:1,...experimental===null?{}:{experimental:experimental}})}return deepFreeze({kind:"normalized",previousQuality:previousQuality,quality:1})}setPreEngineeredVariant(slotKey,variant){if(variant===null||typeof variant!=="object"){throw new TypeError(`ShipLoadout.setPreEngineeredVariant: variant must be a pre-engineered variant, received ${describeValue(variant)}`)}requireString(variant.symbol,"ShipLoadout.setPreEngineeredVariant: variant.symbol");requireString(variant.blueprint,"ShipLoadout.setPreEngineeredVariant: variant.blueprint");requireString(variant.acquisition,"ShipLoadout.setPreEngineeredVariant: variant.acquisition");requireStringIfPresent(variant.experimental,"ShipLoadout.setPreEngineeredVariant: variant.experimental");const known=getPreEngineeredVariants(variant.symbol).find(candidate=>candidate.blueprint.toLowerCase()===variant.blueprint.toLowerCase()&&candidate.grade===variant.grade&&(candidate.experimental?.toLowerCase()??"")===(variant.experimental?.toLowerCase()??"")&&candidate.acquisition===variant.acquisition);if(!known){throw new RangeError(`ShipLoadout.setPreEngineeredVariant: no catalogued variant "${truncate(variant.blueprint)}" for module "${truncate(variant.symbol)}"`)}const stats=getPreEngineeredStats(known);const unresolved=unresolvedModifiers(known);if(unresolved.length>0){throw new TypeError(`ShipLoadout.setPreEngineeredVariant: cannot resolve "${truncate(variant.blueprint)}" for module "${truncate(variant.symbol)}"; missing base stats for ${unresolved.join(", ")}`)}this.setModule(slotKey,stats);const module=this.#fittedModuleFor(slotKey);const engineering={BlueprintName:known.blueprint,Level:known.grade,Quality:1,...known.experimental===void 0?{}:{ExperimentalEffect:known.experimental},...known.modifiers?.length?{Modifiers:getPreEngineeredJournalModifiers(known)}:{}};this.#replaceModule(module.Slot,{...module,Engineering:engineering},cloneModuleStats(stats),getPreEngineeredModifiers(known));return this}clearEngineering(slotKey){const module=this.#fittedModuleFor(slotKey);if(module&&this.#statsFor(module)?.engineeringLocked){throw new TypeError(`ShipLoadout.clearEngineering: module "${truncate(module.Item)}" is a final pre-engineered article and its engineering cannot be removed`)}if(module?.Engineering){const baseStats2=this.#engineeringBaseStats(module);const bare={Slot:module.Slot,Item:module.Item};if(module.On!==void 0)bare.On=module.On;if(module.Priority!==void 0){bare.Priority=module.Priority}if(module.Health!==void 0)bare.Health=module.Health;if(module.Value!==void 0)bare.Value=module.Value;this.#replaceModule(module.Slot,bare,baseStats2??void 0)}return this}setModuleEnabled(slotKey,on){this.#patchModule(slotKey,{On:on});return this}setModulePriority(slotKey,priority){if(!Number.isInteger(priority)||priority<0||priority>4){throw new RangeError(`ShipLoadout: power priority must be an integer 0-4, got ${truncate(priority)}`)}this.#patchModule(slotKey,{Priority:priority});return this}#patchModule(slotKey,patch){const module=this.#fittedModuleFor(slotKey);if(!module){throw new RangeError(`ShipLoadout: slot "${truncate(slotKey)}" is empty`)}this.#modules.set(module.Slot,cloneLoadoutModule({...module,...patch}));this.#slotCache.clear()}toLoadoutEvent(options={}){const unladenMass=this.#computedUnladenMass();const cargoCapacity=this.#computedCargoCapacity();const fuel=this.#computedFuelCapacity();const maxJumpRange=this.#exportableJumpRange(unladenMass);return exportLoadoutEvent({shipSymbol:this.#shipSymbol,...this.#top.ShipName===void 0?{}:{shipName:this.#top.ShipName},...this.#top.ShipIdent===void 0?{}:{shipIdent:this.#top.ShipIdent},modules:this.#modules,layout:this.#layout(),sourcePurchase:this.#sourcePurchase,retailHullValue:this.#ship.hullCost,unladenMass:unladenMass,cargoCapacity:cargoCapacity,fuelCapacity:fuel,maxJumpRange:maxJumpRange,sourceTotalsVoided:this.#importOutcomes.some(outcome=>outcome.sourceSymbol===null&&parseSlotName(outcome.slot)?.kind==="core"),statsFor:module=>this.#statsFor(module)},options)}toSlef(options){return toSlef(this.toLoadoutEvent(options),options.header)}toSlefString(options){return stringifySlef(this.toSlef(options),{indent:options.indent??0})}#exportableJumpRange(unladenMass){let drive;try{drive=this.#resolveDrive()}catch{return null}if(drive===null)return null;const tank=this.#computedFuelCapacity();return singleJumpRange(unladenMass,Math.min(tank.main,drive.maxFuel),drive)}get frameShiftDrive(){const drive=this.#resolveDrive();if(drive===null)throw new TypeError("ShipLoadout: build has no frame shift drive");return drive}get thrusters(){return fittedThrusterParamsFor([...this.#modules.values()],module=>this.#statsFor(module))}#maxJumpFuel(fsd){return Math.min(this.fuelCapacity.main,fsd.maxFuel)}frameShiftDriveMassFactor(options={}){requireLoadOptions("ShipLoadout.frameShiftDriveMassFactor",options);const fuel=options.fuel??this.fuelCapacity.main;return frameShiftDriveMassFactor(this.#requireMass(options.cargo??0),fuel,this.frameShiftDrive)}maxJumpRange(){const fsd=this.frameShiftDrive;return singleJumpRange(this.#requireMass(0),this.#maxJumpFuel(fsd),fsd)}jumpRange(options={}){requireLoadOptions("ShipLoadout.jumpRange",options);const fsd=this.frameShiftDrive;const fuel=options.fuel??this.fuelCapacity.main;return singleJumpRange(this.#requireMass(options.cargo??0),fuel,fsd)}ladenJumpRange(){return this.jumpRange({cargo:this.cargoCapacity})}fuelPerJump(distance,options={}){requireLoadOptions("ShipLoadout.fuelPerJump",options);const fsd=this.frameShiftDrive;const fuel=options.fuel??this.fuelCapacity.main;return fuelPerJump(distance,this.#requireMass(options.cargo??0),fuel,fsd)}totalRange(options={}){requireLoadOptions("ShipLoadout.totalRange",options);return totalRange(this.#requireMass(options.cargo??0),options.fuel??this.fuelCapacity.main,this.frameShiftDrive)}standardLoadResult(load){if(load!=="maximum"&&load!=="unladen"&&load!=="laden"){throw new RangeError("ShipLoadout.standardLoadResult: load must be 'maximum', 'unladen', or 'laden'")}const fuel=this.fuelCapacity;const cargo=load==="laden"?this.cargoCapacity:0;const mass=load==="maximum"?this.unladenMass:0;const issues=[];let drive=null;let maximumFuel=null;if(load==="maximum"){const fitted=this.#frameShiftDrive()?.module;let driveError=null;try{drive=this.#resolveDrive();maximumFuel=drive?.maxFuel??null}catch(error){if(!(error instanceof Error))throw error;driveError=error}if(maximumFuel===null){issues.push({field:"frameShiftDrive",reason:fitted?"unresolved":"missing",slot:fitted?.Slot??"FrameShiftDrive",...fitted?{symbol:fitted.Item}:{},message:driveError?.message??"FrameShiftDrive: no frame shift drive is fitted",params:fitted?{field:"frameShiftDrive",reason:"unresolved",slot:fitted.Slot,symbol:fitted.Item}:{field:"frameShiftDrive",reason:"missing",slot:"FrameShiftDrive"}})}}if(issues.length>0){return incompleteResult(issues)}const carriedFuel=load==="maximum"?Math.min(fuel.main,maximumFuel):fuel.main;const value=Object.freeze({fuel:carriedFuel,cargo:cargo,mass:this.unladenMass+carriedFuel+cargo});if(load==="maximum"){try{this.jumpRange(value)}catch(error){if(!(error instanceof RangeError))throw error;const fitted=this.#frameShiftDrive()?.module;const field=drive!==null&&(!Number.isFinite(drive.maxFuel)||drive.maxFuel<0)?"frameShiftDrive":!Number.isFinite(value.fuel)||value.fuel<0?"fuelCapacity":!Number.isFinite(mass)||mass<0?"mass":"frameShiftDrive";const driveParams=field==="frameShiftDrive"&&fitted?{slot:fitted.Slot,symbol:fitted.Item}:{};return incompleteResult([{field:field,reason:"invalid",...driveParams,message:error.message,params:{field:field,reason:"invalid",...driveParams}}])}}return completeResult(value)}jumpRangeSummary(){const maximum=this.#require(this.standardLoadResult("maximum"),"maximum load");const unladen=this.#require(this.standardLoadResult("unladen"),"unladen load");const laden=this.#require(this.standardLoadResult("laden"),"laden load");return{max:this.jumpRange(maximum),unladen:this.jumpRange(unladen),laden:this.jumpRange(laden),totalMax:this.totalRange(maximum),totalUnladen:this.totalRange(unladen),totalLaden:this.totalRange(laden)}}powerBudget(){const modules=[...this.#modules.values()];const consumers=[];for(const module of modules){const consumer=powerConsumerFor(module,this.#statsFor(module));if(consumer)consumers.push(consumer)}return powerBudget(powerAvailable(modules,module=>this.#statsFor(module)),consumers)}heatMetrics(){const input=heatInputFor(this.#ship,this.#modulesForEffectiveStats(),this.powerBudget(),module=>this.#statsFor(module));return input?heatMetrics(input):null}mobilityMetrics(options={}){requireEnginesPips("ShipLoadout.mobilityMetrics",options.enginesPips??4);requireLoadOptions("ShipLoadout.mobilityMetrics",options);return this.mobilityMetricsResult(options).value}mobilityMetricsResult(options={}){const enginesPips=requireEnginesPips("ShipLoadout.mobilityMetricsResult",options.enginesPips??4);requireLoadOptions("ShipLoadout.mobilityMetricsResult",options);const input=mobilityInputResultFor(this.#ship,[...this.#modules.values()],()=>this.powerBudget(),()=>{const main=options.fuel??this.#top.FuelCapacity?.Main??this.fuelCapacity.main;return this.#requireMass(options.cargo??0)+main},enginesPips,module=>this.#statsFor(module));if(!input.complete)return input;return completeResult(mobilityMetrics(input.value))}shieldMetrics(options={}){requireSystemsPips("ShipLoadout.shieldMetrics",options.systemsPips??0);return this.shieldMetricsResult(options).value}shieldMetricsResult(options={}){const systemsPips=requireSystemsPips("ShipLoadout.shieldMetricsResult",options.systemsPips??0);const input=shieldInputResultFor(this.#ship,[...this.#modules.values()],()=>this.powerBudget(),systemsPips,module=>this.#statsFor(module));if(!input.complete)return input;return completeResult(shieldMetrics(input.value))}shieldRecovery(options={}){requireSystemsPips("ShipLoadout.shieldRecovery",options.systemsPips??4);return this.shieldRecoveryResult(options).value}shieldRecoveryResult(options={}){const systemsPips=requireSystemsPips("ShipLoadout.shieldRecoveryResult",options.systemsPips??4);const input=shieldRecoveryInputResultFor(this.#ship,[...this.#modules.values()],()=>this.powerBudget(),systemsPips,module=>this.#statsFor(module));if(!input.complete)return input;return completeResult(shieldRecovery(input.value))}cellBanks(){const modules=[...this.#modules.values()];const banks=orderBySlotLayout(cellBankInputsFor(modules,this.powerBudget(),module=>this.#statsFor(module)),this.#layout(),bank=>bank.slot);return cellBankSummary(banks)}buildCost(){const hull=this.#ship.hullCost;let modules=0;let mercCoins=0;const unpriced=[];const materials=[];for(const module of this.#modules.values()){const stats=this.#statsFor(module);if(stats?.cost!==void 0){modules+=stats.cost}else if(stats!==null||!isNonOutfittingSlot(module.Slot)&&!isBuiltInHullModule(module)){unpriced.push({slot:module.Slot,symbol:module.Item})}const variant=identifyPreEngineeredVariant(module);mercCoins+=variant?.mercCoinCost??0;const engineering=module.Engineering;if(!engineering)continue;const grade=engineering.Level;const bought=variant?.grade??0;if(Number.isInteger(grade)&&grade>bought&&grade<=5){const climb=getBlueprintCost(engineering.BlueprintName,grade,bought);if(climb){materials.push(climb.materials);mercCoins+=climb.mercCoins}}const experimental=engineering.ExperimentalEffect;if(experimental!==void 0&&experimental.toLowerCase()!==variant?.experimental?.toLowerCase()){const cost=getExperimentalEffectCost(experimental);if(cost)materials.push(cost)}}return deepFreeze({credits:{total:hull+modules,hull:hull,modules:modules,rebuy:Math.trunc((hull+modules)*.05),unpriced:unpriced},mercCoins:mercCoins,materials:sumMaterials(...materials)})}buildMass(options={}){requireLoadOptions("ShipLoadout.buildMass",options);const hull=this.#ship.hullMass;const modules=calculateUnladenMass(0,this.#calculationModules());const unladen=this.unladenMass;const fuel=options.fuel??this.fuelCapacity.main;const cargo=options.cargo??0;return Object.freeze({hull:hull,modules:modules,unladen:unladen,fuel:fuel,cargo:cargo,total:unladen+fuel+cargo})}armourMetrics(){return armourMetrics(armourInputFor(this.#ship,[...this.#modules.values()],module=>this.#statsFor(module)))}weaponMetrics(){const weapons=[];for(const module of this.#modules.values()){const record=this.#statsFor(module);const stats=weaponStatsFor(this.#moduleForEffectiveStats(module),record);if(!stats)continue;weapons.push({slot:module.Slot,symbol:module.Item,name:record?.name??module.Item,enabled:module.On!==false,metrics:weaponMetrics(stats),ammunition:ammunitionCapacity(stats),...stats.maximumRange===void 0?{}:{maximumRange:stats.maximumRange},...stats.falloffRange===void 0?{}:{falloffRange:stats.falloffRange},...stats.projectileRange===void 0?{}:{projectileRange:{...stats.projectileRange}},...stats.armourPiercing===void 0?{}:{armourPiercing:stats.armourPiercing}})}const orderedWeapons=orderBySlotLayout(weapons,this.#layout(),weapon=>weapon.slot);return{weapons:orderedWeapons,total:sumWeaponMetrics(orderedWeapons.filter(weapon=>weapon.enabled).map(weapon=>weapon.metrics))}}weaponsCapacitorMetrics(options={}){const weaponsPips=options.weaponsPips??4;if(!Number.isFinite(weaponsPips)||weaponsPips<0||weaponsPips>4){throw new RangeError("ShipLoadout.weaponsCapacitorMetrics: weaponsPips must be a finite number from 0 to 4")}return weaponsCapacitorMetrics(weaponsCapacitorInputFor(this.#modulesForEffectiveStats(),weaponsPips,this.powerBudget(),module=>this.#statsFor(module)))}distributorMetrics(options={}){const pips={systemsPips:options.systemsPips??4,enginesPips:options.enginesPips??4,weaponsPips:options.weaponsPips??4};for(const field of["systemsPips","enginesPips","weaponsPips"]){const value=pips[field];if(!Number.isFinite(value)||value<0||value>4){throw new RangeError(`ShipLoadout.distributorMetrics: ${field} must be a finite number from 0 to 4`)}}const input=distributorInputFor(this.#modulesForEffectiveStats(),pips,this.powerBudget(),module=>this.#statsFor(module));return input?distributorMetrics(input):null}#layout(){if(this.#layoutCache===void 0){this.#layoutCache=Object.freeze(enumerateSlots(this.#ship))}return this.#layoutCache}#requireSlot(slotKey){const wanted=requireString(slotKey,SLOT_KEY).toLowerCase();const slot=this.#layout().find(s=>s.key.toLowerCase()===wanted);if(!slot){throw new RangeError(`ShipLoadout: hull "${truncate(this.#shipSymbol)}" has no slot "${truncate(slotKey)}"`)}return slot}#fittedKey(slotKey){return matchingKeyIn(this.#modules,requireString(slotKey,SLOT_KEY))}#fittedModuleFor(slotKey){const key=this.#fittedKey(slotKey);return key===null?void 0:this.#modules.get(key)}#require(result,what){if(result.value===null){throw new TypeError(`ShipLoadout: cannot determine ${what} (${result.issues.map(i=>i.message).join("; ")})`)}return result.value}#requireMass(cargo){return this.unladenMass+cargo}#calculationModules(){return[...this.#modules.values()].map(module=>({mass:this.#moduleMass(module),cargoCapacity:this.#moduleCapacity(module,"CargoCapacity","cargoCapacity"),fuelCapacity:this.#moduleCapacity(module,"FuelCapacity","fuelCapacity")}))}#moduleLimits(){const entries=[...this.#modules.values()].flatMap(module=>{const stats=this.#statsFor(module);return stats===null?[]:[stats]});return calculateModuleLimits(entries)}#moduleStatsAt(slotKey){const fitted=this.#fittedModuleFor(slotKey);return fitted===void 0?null:this.#statsFor(fitted)}#engineeringBaseStats(module){const fitted=this.#statsFor(module);if(!module.Engineering||identifyPreEngineeredVariant(module)===null)return fitted;const stock=builtInModuleBySymbol(module.Item,FITTED_ITEM);if(!stock)return fitted;return fitted?.engineeringLocked?cloneModuleStats({...stock,engineeringLocked:true}):stock}#moduleForEffectiveStats(module){const modifiers=this.#primitiveModifiers.get(module.Slot);if(modifiers===void 0||module.Engineering===void 0)return module;return{...module,Engineering:{...module.Engineering,Modifiers:modifiers}}}#modulesForEffectiveStats(){return[...this.#modules.values()].map(module=>this.#moduleForEffectiveStats(module))}#moduleLimitRegression(slotKey,replacement,current=this.#moduleLimits(),previous=this.#moduleStatsAt(slotKey)){if(previous?.limitGroup===void 0&&previous?.limitIncrease===void 0&&replacement?.limitGroup===void 0&&replacement?.limitIncrease===void 0){return null}for(const usage of current){const count=usage.count-Number(previous?.limitGroup===usage.group)+Number(replacement?.limitGroup===usage.group);const increase=usage.increase-(previous?.limitIncrease?.group===usage.group?previous.limitIncrease.amount:0)+(replacement?.limitIncrease?.group===usage.group?replacement.limitIncrease.amount:0);const limit=usage.baseLimit+increase;const excess=Math.max(0,count-limit);if(excess>usage.excess){return{...usage,increase:increase,limit:limit,count:count,excess:excess}}}return null}#replaceModule(slotKey,replacement,replacementStats,primitiveModifiers){const previous=this.#modules.get(slotKey)??null;this.#adjustImportedFigures(previous,replacement,replacementStats);if(replacement===null){this.#modules.delete(slotKey);this.#moduleStats.delete(slotKey);this.#primitiveModifiers.delete(slotKey)}else{this.#modules.set(slotKey,cloneLoadoutModule(replacement));if(replacementStats!==void 0)this.#moduleStats.set(slotKey,replacementStats);if(primitiveModifiers===void 0)this.#primitiveModifiers.delete(slotKey);else this.#primitiveModifiers.set(slotKey,primitiveModifiers)}this.#slotCache.clear()}#adjustImportedFigures(previous,next,nextStats){if(this.#top.UnladenMass!==void 0){this.#top.UnladenMass+=this.#moduleMass(next,nextStats)-this.#moduleMass(previous)}if(this.#top.CargoCapacity!==void 0){this.#top.CargoCapacity+=this.#moduleCapacity(next,"CargoCapacity","cargoCapacity",nextStats)-this.#moduleCapacity(previous,"CargoCapacity","cargoCapacity")}if(this.#top.FuelCapacity?.Main!==void 0){const reserve=this.#top.FuelCapacity.Reserve;this.#top.FuelCapacity={Main:this.#top.FuelCapacity.Main+this.#moduleCapacity(next,"FuelCapacity","fuelCapacity",nextStats)-this.#moduleCapacity(previous,"FuelCapacity","fuelCapacity"),...reserve===void 0?{}:{Reserve:reserve}}}if(normalizeKey(previous?.Item,FITTED_ITEM)===normalizeKey(next?.Item,FITTED_ITEM)){return}if(previous!==null&&isCargoHatchSlot(previous.Slot)||next!==null&&isCargoHatchSlot(next.Slot)){return}delete this.#top.ModulesValue;delete this.#top.Rebuy}#moduleMass(module,statsOverride){if(module===null)return 0;if(isCargoHatchSlot(module.Slot))return 0;const modified=getLoadoutModifier(module,"Mass");if(modified!==null)return modified;return(statsOverride??this.#statsFor(module))?.mass??0}#moduleCapacity(module,modifierLabel,field,statsOverride){if(module===null)return 0;if(isCargoHatchSlot(module.Slot))return 0;const modified=getLoadoutModifier(module,modifierLabel);if(modified!==null)return modified;return(statsOverride??this.#statsFor(module))?.[field]??0}#frameShiftDrive(){for(const m of this.#modules.values()){const stats=this.#statsFor(m);if(stats?.slot==="frameShiftDrive")return{module:m,stats:stats}}return void 0}#resolveDrive(){const fsd=this.#frameShiftDrive();if(!fsd)return null;const{module:fsdModule,stats:base}=fsd;if(base.fuelMul===void 0||base.fuelPower===void 0){throw new TypeError(`ShipLoadout: the fitted record for frame shift drive "${truncate(fsdModule.Item)}" has no jump constants`)}const optMass=getLoadoutModifier(fsdModule,"FSDOptimalMass")??base.optMass;const maxFuel=getLoadoutModifier(fsdModule,"MaxFuelPerJump")??base.maxFuel;if(optMass===void 0||maxFuel===void 0){const missing=[...optMass===void 0?["optMass"]:[],...maxFuel===void 0?["maxFuel"]:[]];throw new TypeError(`ShipLoadout: the fitted record for frame shift drive "${truncate(fsdModule.Item)}" has no ${missing.join(" or ")}`)}return{optMass:optMass,maxFuel:maxFuel,fuelMul:base.fuelMul,fuelPower:base.fuelPower,jumpBoost:this.#resolveJumpBoost()}}#resolveJumpBoost(){for(const m of this.#modules.values()){const stats=this.#statsFor(m);if(stats?.engineeringGroup!=="fsdBoosters"&&(stats?.jumpBoost===void 0||stats.jumpBoost===0)){continue}if(m.On===false)continue;if(stats?.jumpBoost===void 0){throw new TypeError(`ShipLoadout: the fitted record for FSD booster "${truncate(m.Item)}" has no jumpBoost`)}return stats.jumpBoost}return 0}#statsFor(module){if(module===null)return null;const stats=this.#moduleStats.get(module.Slot)??builtInModuleBySymbol(module.Item,FITTED_ITEM);if(stats)return stats;return isBuiltInHullModule(module)?builtInModuleBySymbol("ModularCargoBayDoor","ShipLoadout: built-in module"):null}};export{LoadoutEditError,ShipLoadout};//# sourceMappingURL=chunk-3BM5BHI3.js.map
|