diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
index 00000000..777085b7
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,20 @@
+#2.2.x
+ * Update DPS/HPS/EPS in real-time as modifiers change
+ * Use coriolis-data 2.2.2:
+ * Add distributor draw modifier to shield generators
+ * Remove modifiers for sensors
+ * Add initial loadout passenger cabins for Beluga
+ * Add initial loadout passenger cabins for Orca
+ * Update costs and initial loadouts for Keelback and Type-7
+ * Add resistances for hull reinforcement packages
+ * Added modifier actions to create modifications from raw data
+ * Show modification icon for modified modules
+ * Take modifications in to account when deciding whether to issue a warning on a standard module
+ * Fix hardpoint comparison DPS number when selecting an alternate module
+ * Ensure that retrofit tab only shows changed modules
+ * Fix import and export of ships with modifications, bump schema version to 4
+ * Enable boost display even if power distributor is disabled
+ * Calculate breakdown of ship offensive and defensive stats
+ * Add 'Offence summary' and 'Defence summary' components
+ * Add ability to import from companion API output through import feature
+ * Add ability to import from companion API output through URL
diff --git a/__tests__/fixtures/anaconda-test-detailed-export-v4.json b/__tests__/fixtures/anaconda-test-detailed-export-v4.json
new file mode 100644
index 00000000..4bda3ad3
--- /dev/null
+++ b/__tests__/fixtures/anaconda-test-detailed-export-v4.json
@@ -0,0 +1,313 @@
+{
+ "$schema": "http://cdn.coriolis.io/schemas/ship-loadout/4.json#",
+ "name": "Test My Ship",
+ "ship": "Anaconda",
+ "references": [
+ {
+ "name": "Coriolis.io",
+ "url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==?bn=Test%20My%20Ship",
+ "old-code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==",
+ "code": "4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==",
+ "shipId": "anaconda"
+ }
+ ],
+ "components": {
+ "standard": {
+ "bulkheads": "Reactive Surface Composite",
+ "cargoHatch": {
+ "enabled": false,
+ "priority": 5
+ },
+ "powerPlant": {
+ "class": 8,
+ "rating": "A",
+ "enabled": true,
+ "priority": 1,
+ "modifications": {
+ "pgen": 1000
+ }
+ },
+ "thrusters": {
+ "class": 6,
+ "rating": "A",
+ "enabled": true,
+ "priority": 1
+ },
+ "frameShiftDrive": {
+ "class": 6,
+ "rating": "A",
+ "enabled": true,
+ "priority": 3
+ },
+ "lifeSupport": {
+ "class": 5,
+ "rating": "A",
+ "enabled": true,
+ "priority": 1
+ },
+ "powerDistributor": {
+ "class": 8,
+ "rating": "A",
+ "enabled": true,
+ "priority": 1
+ },
+ "sensors": {
+ "class": 8,
+ "rating": "A",
+ "enabled": true,
+ "priority": 1
+ },
+ "fuelTank": {
+ "class": 5,
+ "rating": "C",
+ "enabled": true,
+ "priority": 1
+ }
+ },
+ "hardpoints": [
+ {
+ "class": 4,
+ "rating": "A",
+ "enabled": true,
+ "priority": 2,
+ "group": "Plasma Accelerator",
+ "mount": "Fixed"
+ },
+ {
+ "class": 3,
+ "rating": "D",
+ "enabled": true,
+ "priority": 2,
+ "group": "Beam Laser",
+ "mount": "Turret"
+ },
+ {
+ "class": 3,
+ "rating": "D",
+ "enabled": true,
+ "priority": 2,
+ "group": "Beam Laser",
+ "mount": "Turret"
+ },
+ {
+ "class": 3,
+ "rating": "D",
+ "enabled": true,
+ "priority": 2,
+ "group": "Beam Laser",
+ "mount": "Turret"
+ },
+ {
+ "class": 2,
+ "rating": "E",
+ "enabled": true,
+ "priority": 2,
+ "group": "Cannon",
+ "mount": "Turret"
+ },
+ {
+ "class": 2,
+ "rating": "E",
+ "enabled": true,
+ "priority": 2,
+ "group": "Cannon",
+ "mount": "Turret"
+ },
+ {
+ "class": 1,
+ "rating": "F",
+ "enabled": true,
+ "priority": 2,
+ "group": "Beam Laser",
+ "mount": "Turret"
+ },
+ {
+ "class": 1,
+ "rating": "F",
+ "enabled": true,
+ "priority": 2,
+ "group": "Beam Laser",
+ "mount": "Turret"
+ }
+ ],
+ "utility": [
+ {
+ "class": 0,
+ "rating": "A",
+ "enabled": true,
+ "priority": 1,
+ "group": "Shield Booster"
+ },
+ {
+ "class": 0,
+ "rating": "A",
+ "enabled": true,
+ "priority": 1,
+ "group": "Shield Booster"
+ },
+ null,
+ {
+ "class": 0,
+ "rating": "C",
+ "enabled": true,
+ "priority": 2,
+ "group": "Kill Warrant Scanner"
+ },
+ {
+ "class": 0,
+ "rating": "C",
+ "enabled": true,
+ "priority": 2,
+ "group": "Cargo Scanner"
+ },
+ {
+ "class": 0,
+ "rating": "F",
+ "enabled": false,
+ "priority": 1,
+ "group": "Electronic Countermeasure",
+ "name": "Electronic Countermeasure"
+ },
+ {
+ "class": 0,
+ "rating": "I",
+ "enabled": true,
+ "priority": 1,
+ "group": "Chaff Launcher",
+ "name": "Chaff Launcher"
+ },
+ {
+ "class": 0,
+ "rating": "I",
+ "enabled": true,
+ "priority": 2,
+ "group": "Point Defence",
+ "name": "Point Defence"
+ }
+ ],
+ "internal": [
+ {
+ "class": 7,
+ "rating": "A",
+ "enabled": true,
+ "priority": 1,
+ "group": "Shield Generator"
+ },
+ {
+ "class": 6,
+ "rating": "A",
+ "enabled": true,
+ "priority": 1,
+ "group": "Shield Cell Bank"
+ },
+ {
+ "class": 6,
+ "rating": "E",
+ "enabled": true,
+ "priority": 1,
+ "group": "Cargo Rack"
+ },
+ {
+ "class": 5,
+ "rating": "D",
+ "enabled": true,
+ "priority": 1,
+ "group": "Hull Reinforcement Package"
+ },
+ {
+ "class": 5,
+ "rating": "E",
+ "enabled": true,
+ "priority": 1,
+ "group": "Cargo Rack"
+ },
+ null,
+ null,
+ {
+ "class": 4,
+ "rating": "E",
+ "enabled": true,
+ "priority": 1,
+ "group": "Cargo Rack"
+ },
+ {
+ "class": 4,
+ "rating": "E",
+ "enabled": true,
+ "priority": 1,
+ "group": "Cargo Rack"
+ },
+ {
+ "class": 4,
+ "rating": "A",
+ "enabled": true,
+ "priority": 3,
+ "group": "Fuel Scoop"
+ },
+ {
+ "class": 2,
+ "rating": "A",
+ "enabled": true,
+ "priority": 3,
+ "group": "Frame Shift Drive Interdictor"
+ }
+ ]
+ },
+ "stats": {
+ "class": 3,
+ "fighterHangars": 1,
+ "hullCost": 141889930,
+ "speed": 180,
+ "topSpeed": 186.5,
+ "boost": 240,
+ "boostEnergy": 27,
+ "topBoost": 248.62,
+ "topSpeed": 186.46,
+ "totalCost": 882362058,
+ "totalDpe": 127.26,
+ "totalDps": 97.74,
+ "totalEps": 22.71,
+ "totalHps": 677.29,
+ "totalExplDpe": 0,
+ "totalExplDps": 0,
+ "totalExplSDps": 0,
+ "totalHps": 33.28,
+ "totalKinDpe": 103.97,
+ "totalKinDps": 28.92,
+ "totalKinSDps": 21.23,
+ "totalSDps": 85.77,
+ "totalThermDpe": 23.29,
+ "totalThermDps": 68.82,
+ "totalThermSDps": 64.53,
+ "agility": 2,
+ "baseShieldStrength": 350,
+ "baseArmour": 945,
+ "hullExplRes": 0.78,
+ "hullKinRes": 0.73,
+ "hullMass": 400,
+ "hullThermRes": 1.37,
+ "masslock": 23,
+ "pipSpeed": 0.14,
+ "moduleCostMultiplier": 1,
+ "fuelCapacity": 32,
+ "cargoCapacity": 128,
+ "ladenMass": 1339.2,
+ "armour": 2227.5,
+ "baseArmour": 525,
+ "unladenMass": 1179.2,
+ "powerAvailable": 39.6,
+ "powerRetracted": 23.33,
+ "powerDeployed": 34.76,
+ "unladenRange": 18.49,
+ "fullTankRange": 18.12,
+ "ladenRange": 16.39,
+ "unladenFastestRange": 73.21,
+ "ladenFastestRange": 66.15,
+ "maxJumpCount": 4,
+ "shield": 833,
+ "shieldCells": 1840,
+ "shieldExplRes": 0.5,
+ "shieldKinRes": 0.6,
+ "shieldThermRes": 1.2
+ }
+}
diff --git a/__tests__/fixtures/companion-api-import-1.json b/__tests__/fixtures/companion-api-import-1.json
new file mode 100644
index 00000000..9546ffae
--- /dev/null
+++ b/__tests__/fixtures/companion-api-import-1.json
@@ -0,0 +1,1288 @@
+{
+ "cargo": {
+ "capacity": 128,
+ "items": [
+ {
+ "commodity": "ancientrelic",
+ "marked": 0,
+ "masq": null,
+ "mission": null,
+ "origin": 2,
+ "owner": 822212,
+ "powerplayOrigin": null,
+ "qty": 1,
+ "value": 0,
+ "xyz": null
+ }
+ ],
+ "lock": 359606096,
+ "qty": 1,
+ "ts": {
+ "sec": 1478848090,
+ "usec": 568000
+ }
+ },
+ "cockpitBreached": false,
+ "free": false,
+ "fuel": {
+ "main": {
+ "capacity": 96,
+ "level": 96
+ },
+ "reserve": {
+ "capacity": 1.13,
+ "level": 1.13
+ },
+ "superchargedFSD": 0
+ },
+ "health": {
+ "hull": 1000000,
+ "integrity": 30447,
+ "paintwork": 30448,
+ "shield": 1000000,
+ "shieldup": true
+ },
+ "id": 21,
+ "modules": {
+ "Armour": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128049372,
+ "name": "Federation_Corvette_Armour_Grade3",
+ "on": true,
+ "priority": 1,
+ "unloaned": 0,
+ "value": 143796630
+ }
+ },
+ "Bobble01": [],
+ "Bobble02": [],
+ "Bobble03": [],
+ "Bobble04": [],
+ "Bobble05": [],
+ "Bobble06": [],
+ "Bobble07": [],
+ "Bobble08": [],
+ "Bobble09": [],
+ "Bobble10": [],
+ "Decal1": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128667742,
+ "name": "Decal_Combat_Deadly",
+ "on": true,
+ "priority": 1,
+ "unloaned": 0,
+ "value": 0
+ }
+ },
+ "Decal2": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128667757,
+ "name": "Decal_Explorer_Ranger",
+ "on": true,
+ "priority": 1,
+ "unloaned": 0,
+ "value": 0
+ }
+ },
+ "Decal3": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128667750,
+ "name": "Decal_Trade_Tycoon",
+ "on": true,
+ "priority": 1,
+ "unloaned": 0,
+ "value": 0
+ }
+ },
+ "EngineColour": [],
+ "FrameShiftDrive": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128064127,
+ "modifiers": {
+ "engineerID": 300100,
+ "id": 3904,
+ "modifiers": [
+ {
+ "name": "mod_mass",
+ "type": 1,
+ "value": 0.42523837089539
+ },
+ {
+ "name": "mod_health",
+ "type": 1,
+ "value": -0.2126482129097
+ },
+ {
+ "name": "mod_passive_power",
+ "type": 1,
+ "value": 0.23241454362869
+ },
+ {
+ "name": "mod_fsd_optimised_mass",
+ "type": 1,
+ "value": 0.46293264627457
+ },
+ {
+ "name": "mod_fsd_optimised_mass",
+ "type": 2,
+ "value": 0.040929391980171
+ },
+ {
+ "name": "trade_mass_health",
+ "type": 2,
+ "value": 0.088846378028393
+ }
+ ],
+ "moduleTags": [
+ 16
+ ],
+ "recipeID": 128673694,
+ "slotIndex": 47
+ },
+ "name": "Int_Hyperdrive_Size6_Class5",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 5,
+ "recipeName": "FSD_LongRange",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 13752602
+ }
+ },
+ "FuelTank": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128064350,
+ "name": "Int_FuelTank_Size5_Class3",
+ "on": true,
+ "priority": 1,
+ "unloaned": 83090,
+ "value": 83090
+ }
+ },
+ "HugeHardpoint1": {
+ "module": {
+ "ammo": {
+ "clip": 1,
+ "hopper": 1
+ },
+ "free": false,
+ "health": 1000000,
+ "id": 128681994,
+ "modifiers": {
+ "engineerID": 300180,
+ "id": 3380,
+ "modifiers": [
+ {
+ "name": "mod_weapon_range",
+ "type": 1,
+ "value": 0.19140657782555
+ },
+ {
+ "name": "mod_weapon_damage",
+ "type": 1,
+ "value": -0.043880753219128
+ },
+ {
+ "name": "mod_weapon_active_power",
+ "type": 1,
+ "value": 0.08408235758543
+ },
+ {
+ "name": "mod_mass",
+ "type": 1,
+ "value": 0.042046930640936
+ },
+ {
+ "name": "mod_passive_power",
+ "type": 2,
+ "value": -0.094719000160694
+ },
+ {
+ "name": "special_thermalshock",
+ "type": 3,
+ "value": 1
+ }
+ ],
+ "moduleTags": [
+ 1,
+ 4
+ ],
+ "recipeID": 128673335,
+ "slotIndex": 26
+ },
+ "name": "Hpt_BeamLaser_Gimbal_Huge",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 1,
+ "recipeName": "Weapon_LongRange",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 7871544
+ }
+ },
+ "HugeHardpoint2": {
+ "module": {
+ "ammo": {
+ "clip": 1,
+ "hopper": 1
+ },
+ "free": false,
+ "health": 1000000,
+ "id": 128681994,
+ "name": "Hpt_BeamLaser_Gimbal_Huge",
+ "on": true,
+ "priority": 0,
+ "unloaned": 0,
+ "value": 7871544
+ }
+ },
+ "LargeHardpoint1": {
+ "module": {
+ "ammo": {
+ "clip": 90,
+ "hopper": 2100
+ },
+ "free": false,
+ "health": 1000000,
+ "id": 128049461,
+ "modifiers": {
+ "engineerID": 300260,
+ "id": 4699,
+ "modifiers": [
+ {
+ "name": "mod_weapon_damage",
+ "type": 1,
+ "value": 0.24115231633186
+ },
+ {
+ "name": "mod_weapon_active_power",
+ "type": 1,
+ "value": 0.61637383699417
+ },
+ {
+ "name": "mod_weapon_burst_interval",
+ "type": 1,
+ "value": -0.11122596263885
+ },
+ {
+ "name": "mod_weapon_jitter_radius",
+ "type": 1,
+ "value": 0.56468063592911
+ },
+ {
+ "name": "mod_weapon_active_heat",
+ "type": 1,
+ "value": 0.12204115837812
+ },
+ {
+ "name": "mod_weapon_hardness_piercing",
+ "type": 2,
+ "value": 0.033723440021276
+ },
+ {
+ "name": "mod_weapon_hardness_piercing",
+ "type": 2,
+ "value": -0.036045636981726
+ },
+ {
+ "name": "special_incendiary_rounds",
+ "type": 3,
+ "value": 1
+ }
+ ],
+ "moduleTags": [
+ 1,
+ 8
+ ],
+ "recipeID": 128673502,
+ "slotIndex": 28
+ },
+ "name": "Hpt_MultiCannon_Gimbal_Large",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 3,
+ "recipeName": "Weapon_Overcharged",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 520593
+ }
+ },
+ "LifeSupport": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128064159,
+ "name": "Int_LifeSupport_Size5_Class2",
+ "on": true,
+ "priority": 0,
+ "unloaned": 0,
+ "value": 67528
+ }
+ },
+ "MainEngines": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128064094,
+ "modifiers": {
+ "engineerID": 300100,
+ "id": 3920,
+ "modifiers": [
+ {
+ "name": "mod_engine_mass_curve_multiplier",
+ "type": 1,
+ "value": 0.16290386021137
+ },
+ {
+ "name": "mod_engine_heat",
+ "type": 1,
+ "value": 0.51375859975815
+ },
+ {
+ "name": "mod_passive_power",
+ "type": 1,
+ "value": 0.068986810743809
+ },
+ {
+ "name": "mod_health",
+ "type": 1,
+ "value": -0.12497692555189
+ },
+ {
+ "name": "mod_engine_mass_curve",
+ "type": 1,
+ "value": -0.046525910496712
+ },
+ {
+ "name": "mod_engine_mass_curve_multiplier",
+ "type": 2,
+ "value": 0.0091453474014997
+ },
+ {
+ "name": "mod_mass",
+ "type": 2,
+ "value": 0.090709120035172
+ }
+ ],
+ "moduleTags": [
+ 17
+ ],
+ "recipeID": 128673657,
+ "slotIndex": 46
+ },
+ "name": "Int_Engine_Size7_Class2",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 3,
+ "recipeName": "Engine_Dirty",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 1614658
+ }
+ },
+ "MediumHardpoint1": {
+ "module": {
+ "ammo": {
+ "clip": 90,
+ "hopper": 2100
+ },
+ "free": false,
+ "health": 1000000,
+ "id": 128049463,
+ "modifiers": {
+ "engineerID": 300260,
+ "id": 4729,
+ "modifiers": [
+ {
+ "name": "mod_weapon_damage",
+ "type": 1,
+ "value": 0.24256283044815
+ },
+ {
+ "name": "mod_weapon_active_power",
+ "type": 1,
+ "value": 0.61838209629059
+ },
+ {
+ "name": "mod_weapon_burst_interval",
+ "type": 1,
+ "value": -0.10679690539837
+ },
+ {
+ "name": "mod_weapon_jitter_radius",
+ "type": 1,
+ "value": 0.56370949745178
+ },
+ {
+ "name": "mod_weapon_active_heat",
+ "type": 1,
+ "value": 0.10164558887482
+ },
+ {
+ "name": "trade_weapon_damage_weapon_active_power",
+ "type": 2,
+ "value": 0.052759505808353
+ },
+ {
+ "name": "mod_health",
+ "type": 2,
+ "value": -0.032706737518311
+ }
+ ],
+ "moduleTags": [
+ 1,
+ 8
+ ],
+ "recipeID": 128673502,
+ "slotIndex": 29
+ },
+ "name": "Hpt_MultiCannon_Turret_Medium",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 3,
+ "recipeName": "Weapon_Overcharged",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 1292800
+ }
+ },
+ "MediumHardpoint2": {
+ "module": {
+ "ammo": {
+ "clip": 90,
+ "hopper": 2100
+ },
+ "free": false,
+ "health": 1000000,
+ "id": 128049463,
+ "modifiers": {
+ "engineerID": 300260,
+ "id": 4731,
+ "modifiers": [
+ {
+ "name": "mod_weapon_damage",
+ "type": 1,
+ "value": 0.2441771030426
+ },
+ {
+ "name": "mod_weapon_active_power",
+ "type": 1,
+ "value": 0.69319069385529
+ },
+ {
+ "name": "mod_weapon_burst_interval",
+ "type": 1,
+ "value": -0.10855937004089
+ },
+ {
+ "name": "mod_weapon_jitter_radius",
+ "type": 1,
+ "value": 0.55067348480225
+ },
+ {
+ "name": "mod_weapon_active_heat",
+ "type": 1,
+ "value": 0.11971006542444
+ },
+ {
+ "name": "trade_mass_health",
+ "type": 2,
+ "value": 0.0050421766936779
+ },
+ {
+ "name": "mod_passive_power",
+ "type": 2,
+ "value": 0.03293726965785
+ }
+ ],
+ "moduleTags": [
+ 1,
+ 8
+ ],
+ "recipeID": 128673502,
+ "slotIndex": 30
+ },
+ "name": "Hpt_MultiCannon_Turret_Medium",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 3,
+ "recipeName": "Weapon_Overcharged",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 1292800
+ }
+ },
+ "PaintJob": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128732313,
+ "name": "PaintJob_Federation_Corvette_Militaire_Sand",
+ "on": true,
+ "priority": 1,
+ "unloaned": 0,
+ "value": 0
+ }
+ },
+ "PlanetaryApproachSuite": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128672317,
+ "name": "Int_PlanetApproachSuite",
+ "on": true,
+ "priority": 1,
+ "unloaned": 425,
+ "value": 425
+ }
+ },
+ "PowerDistributor": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128064217,
+ "modifiers": {
+ "engineerID": 300180,
+ "id": 3376,
+ "modifiers": [
+ {
+ "name": "mod_powerdistributor_weapon_charge",
+ "type": 1,
+ "value": 0.27520388364792
+ },
+ {
+ "name": "mod_powerdistributor_weapon_rate",
+ "type": 1,
+ "value": 0.10803784430027
+ },
+ {
+ "name": "mod_powerdistributor_system_charge",
+ "type": 1,
+ "value": -0.13366678357124
+ },
+ {
+ "name": "mod_powerdistributor_system_rate",
+ "type": 1,
+ "value": -0.029957808554173
+ },
+ {
+ "name": "mod_powerdistributor_engine_charge",
+ "type": 1,
+ "value": -0.085655435919762
+ },
+ {
+ "name": "mod_powerdistributor_engine_rate",
+ "type": 1,
+ "value": -0.12185442447662
+ },
+ {
+ "name": "mod_health",
+ "type": 2,
+ "value": 0.056631729006767
+ },
+ {
+ "name": "mod_powerdistributor_weapon_charge",
+ "type": 2,
+ "value": 0.0055946228094399
+ },
+ {
+ "name": "mod_powerdistributor_global_rate",
+ "type": 2,
+ "value": -0.011308163404465
+ }
+ ],
+ "moduleTags": [
+ 19
+ ],
+ "recipeID": 128673752,
+ "slotIndex": 49
+ },
+ "name": "Int_PowerDistributor_Size8_Class5",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 3,
+ "recipeName": "PowerDistributor_PriorityWeapons",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 23161983
+ }
+ },
+ "PowerPlant": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128064067,
+ "modifiers": {
+ "engineerID": 300100,
+ "id": 3942,
+ "modifiers": [
+ {
+ "name": "mod_powerplant_power",
+ "type": 1,
+ "value": 0.081056952476501
+ },
+ {
+ "name": "mod_health",
+ "type": 1,
+ "value": -0.0084498468786478
+ },
+ {
+ "name": "mod_powerplant_heat",
+ "type": 1,
+ "value": 0.010082358494401
+ },
+ {
+ "name": "mod_powerplant_power",
+ "type": 2,
+ "value": 0.043923784047365
+ }
+ ],
+ "moduleTags": [
+ 18
+ ],
+ "recipeID": 128673765,
+ "slotIndex": 45
+ },
+ "name": "Int_Powerplant_Size8_Class5",
+ "on": true,
+ "priority": 1,
+ "recipeLevel": 1,
+ "recipeName": "PowerPlant_Boosted",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 138198514
+ }
+ },
+ "Radar": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128064254,
+ "name": "Int_Sensors_Size8_Class2",
+ "on": true,
+ "priority": 0,
+ "unloaned": 0,
+ "value": 1482367
+ }
+ },
+ "Slot01_Size7": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128064344,
+ "name": "Int_CargoRack_Size7_Class1",
+ "on": true,
+ "priority": 1,
+ "unloaned": 0,
+ "value": 1001657
+ }
+ },
+ "Slot02_Size7": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128671329,
+ "modifiers": {
+ "engineerID": 300160,
+ "id": 1783,
+ "modifiers": [
+ {
+ "name": "mod_shield_mass_curve_multiplier",
+ "type": 1,
+ "value": 0.11387270689011
+ },
+ {
+ "name": "mod_shield_global_mult",
+ "type": 1,
+ "value": -0.084811583161354
+ },
+ {
+ "name": "mod_shield_broken_regen",
+ "type": 1,
+ "value": -0.14731486141682
+ },
+ {
+ "name": "mod_shield_normal_regen",
+ "type": 1,
+ "value": -0.036414299160242
+ },
+ {
+ "name": "mod_shield_energy_per_regen",
+ "type": 1,
+ "value": 0.022321036085486
+ },
+ {
+ "name": "trade_shield_curve_shield_curve_mult",
+ "type": 2,
+ "value": -0.11246068775654
+ },
+ {
+ "name": "mod_shield_broken_regen",
+ "type": 2,
+ "value": -0.063284143805504
+ }
+ ],
+ "moduleTags": [
+ 15
+ ],
+ "recipeID": 128673837,
+ "slotIndex": 53
+ },
+ "name": "Int_ShieldGenerator_Size7_Class5_Strong",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 3,
+ "recipeName": "ShieldGenerator_Reinforced",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 69240302
+ }
+ },
+ "Slot03_Size7": {
+ "module": {
+ "ammo": {
+ "clip": 1,
+ "hopper": 4
+ },
+ "free": false,
+ "health": 1000000,
+ "id": 128064332,
+ "modifiers": {
+ "engineerID": 300160,
+ "id": 1789,
+ "modifiers": [
+ {
+ "name": "mod_shieldcell_spin_up",
+ "type": 1,
+ "value": -0.086017057299614
+ },
+ {
+ "name": "mod_shieldcell_duration",
+ "type": 1,
+ "value": -0.047102440148592
+ },
+ {
+ "name": "mod_shieldcell_shield_units",
+ "type": 1,
+ "value": 0.021292699500918
+ },
+ {
+ "name": "mod_boot_time",
+ "type": 1,
+ "value": 0.071096949279308
+ },
+ {
+ "name": "mod_shieldcell_spin_up",
+ "type": 2,
+ "value": -0.018995799124241
+ }
+ ],
+ "moduleTags": [
+ 24
+ ],
+ "recipeID": 128673805,
+ "slotIndex": 54
+ },
+ "name": "Int_ShieldCellBank_Size7_Class5",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 1,
+ "recipeName": "ShieldCellBank_Rapid",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 8272137
+ }
+ },
+ "Slot04_Size6": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128064351,
+ "name": "Int_FuelTank_Size6_Class3",
+ "on": true,
+ "priority": 1,
+ "unloaned": 0,
+ "value": 290341
+ }
+ },
+ "Slot05_Size6": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128666681,
+ "name": "Int_FuelScoop_Size6_Class5",
+ "on": true,
+ "priority": 1,
+ "unloaned": 0,
+ "value": 24449069
+ }
+ },
+ "Slot06_Size5": {
+ "module": {
+ "ammo": {
+ "clip": 1,
+ "hopper": 3
+ },
+ "free": false,
+ "health": 1000000,
+ "id": 128064322,
+ "modifiers": {
+ "engineerID": 300160,
+ "id": 1788,
+ "modifiers": [
+ {
+ "name": "mod_shieldcell_spin_up",
+ "type": 1,
+ "value": -0.084282241761684
+ },
+ {
+ "name": "mod_shieldcell_duration",
+ "type": 1,
+ "value": -0.069141998887062
+ },
+ {
+ "name": "mod_shieldcell_shield_units",
+ "type": 1,
+ "value": 0.025862643495202
+ },
+ {
+ "name": "mod_boot_time",
+ "type": 1,
+ "value": 0.081815980374813
+ },
+ {
+ "name": "mod_shieldcell_duration",
+ "type": 2,
+ "value": 0.041702415794134
+ }
+ ],
+ "moduleTags": [
+ 24
+ ],
+ "recipeID": 128673805,
+ "slotIndex": 57
+ },
+ "name": "Int_ShieldCellBank_Size5_Class5",
+ "on": false,
+ "priority": 0,
+ "recipeLevel": 1,
+ "recipeName": "ShieldCellBank_Rapid",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 1055120
+ }
+ },
+ "Slot07_Size5": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128727930,
+ "name": "Int_FighterBay_Size5_Class1",
+ "on": true,
+ "priority": 0,
+ "unloaned": 0,
+ "value": 575643
+ }
+ },
+ "Slot08_Size4": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128672289,
+ "name": "Int_BuggyBay_Size2_Class2",
+ "on": true,
+ "priority": 1,
+ "unloaned": 0,
+ "value": 18360
+ }
+ },
+ "Slot09_Size4": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128666722,
+ "modifiers": {
+ "engineerID": 300100,
+ "id": 2026,
+ "modifiers": [
+ {
+ "name": "mod_fsdinterdictor_range",
+ "type": 1,
+ "value": 0.086278550326824
+ },
+ {
+ "name": "mod_mass",
+ "type": 1,
+ "value": 0.07684014737606
+ },
+ {
+ "name": "mod_passive_power",
+ "type": 1,
+ "value": 0.13557997345924
+ },
+ {
+ "name": "mod_fsdinterdictor_facing_limit",
+ "type": 1,
+ "value": -0.07848084717989
+ }
+ ],
+ "moduleTags": [
+ 23
+ ],
+ "recipeID": 128673680,
+ "slotIndex": 60
+ },
+ "name": "Int_FSDInterdictor_Size3_Class5",
+ "on": true,
+ "priority": 1,
+ "recipeLevel": 1,
+ "recipeName": "FSDinterdictor_LongRange",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 6477408
+ }
+ },
+ "Slot10_Size3": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128663561,
+ "name": "Int_StellarBodyDiscoveryScanner_Advanced",
+ "on": true,
+ "priority": 1,
+ "unloaned": 0,
+ "value": 1313250
+ }
+ },
+ "SmallHardpoint1": {
+ "module": {
+ "ammo": {
+ "clip": 1,
+ "hopper": 1
+ },
+ "free": false,
+ "health": 1000000,
+ "id": 128049388,
+ "name": "Hpt_PulseLaser_Turret_Small",
+ "on": true,
+ "priority": 0,
+ "unloaned": 0,
+ "value": 26000
+ }
+ },
+ "SmallHardpoint2": {
+ "module": {
+ "ammo": {
+ "clip": 1,
+ "hopper": 1
+ },
+ "free": false,
+ "health": 1000000,
+ "id": 128049388,
+ "name": "Hpt_PulseLaser_Turret_Small",
+ "on": true,
+ "priority": 0,
+ "unloaned": 0,
+ "value": 26000
+ }
+ },
+ "TinyHardpoint1": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128668536,
+ "modifiers": {
+ "engineerID": 300100,
+ "id": 3948,
+ "modifiers": [
+ {
+ "name": "mod_passive_power",
+ "type": 1,
+ "value": 0.065562553703785
+ },
+ {
+ "name": "mod_defencemodifier_global_shield_mult",
+ "type": 1,
+ "value": -0.046933718025684
+ },
+ {
+ "name": "mod_health",
+ "type": 1,
+ "value": -0.037372145801783
+ },
+ {
+ "name": "mod_passive_power",
+ "type": 2,
+ "value": -0.054788526147604
+ }
+ ],
+ "moduleTags": [
+ 22
+ ],
+ "recipeID": 128673790,
+ "slotIndex": 33
+ },
+ "name": "Hpt_ShieldBooster_Size0_Class5",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 1,
+ "recipeName": "ShieldBooster_Resistive",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 238850
+ }
+ },
+ "TinyHardpoint2": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128668536,
+ "modifiers": {
+ "engineerID": 300100,
+ "id": 3949,
+ "modifiers": [
+ {
+ "name": "mod_passive_power",
+ "type": 1,
+ "value": 0.050724714994431
+ },
+ {
+ "name": "mod_defencemodifier_global_shield_mult",
+ "type": 1,
+ "value": -0.042270515114069
+ },
+ {
+ "name": "mod_health",
+ "type": 1,
+ "value": -0.030213074758649
+ },
+ {
+ "name": "mod_defencemodifier_global_shield_mult",
+ "type": 2,
+ "value": -0.0012421812862158
+ }
+ ],
+ "moduleTags": [
+ 22
+ ],
+ "recipeID": 128673790,
+ "slotIndex": 34
+ },
+ "name": "Hpt_ShieldBooster_Size0_Class5",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 1,
+ "recipeName": "ShieldBooster_Resistive",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 238850
+ }
+ },
+ "TinyHardpoint3": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128668536,
+ "modifiers": {
+ "engineerID": 300100,
+ "id": 3950,
+ "modifiers": [
+ {
+ "name": "mod_passive_power",
+ "type": 1,
+ "value": 0.049866359680891
+ },
+ {
+ "name": "mod_defencemodifier_global_shield_mult",
+ "type": 1,
+ "value": -0.046149685978889
+ },
+ {
+ "name": "mod_health",
+ "type": 1,
+ "value": -0.024229854345322
+ },
+ {
+ "name": "mod_defencemodifier_shield_mult",
+ "type": 2,
+ "value": 0.036322306841612
+ }
+ ],
+ "moduleTags": [
+ 22
+ ],
+ "recipeID": 128673790,
+ "slotIndex": 35
+ },
+ "name": "Hpt_ShieldBooster_Size0_Class5",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 1,
+ "recipeName": "ShieldBooster_Resistive",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 238850
+ }
+ },
+ "TinyHardpoint4": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128668536,
+ "modifiers": {
+ "engineerID": 300100,
+ "id": 3951,
+ "modifiers": [
+ {
+ "name": "mod_passive_power",
+ "type": 1,
+ "value": 0.073374435305595
+ },
+ {
+ "name": "mod_defencemodifier_global_shield_mult",
+ "type": 1,
+ "value": -0.046808175742626
+ },
+ {
+ "name": "mod_health",
+ "type": 1,
+ "value": -0.037895452231169
+ },
+ {
+ "name": "mod_defencemodifier_shield_kinetic_mult",
+ "type": 2,
+ "value": -0.0045657334849238
+ }
+ ],
+ "moduleTags": [
+ 22
+ ],
+ "recipeID": 128673790,
+ "slotIndex": 36
+ },
+ "name": "Hpt_ShieldBooster_Size0_Class5",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 1,
+ "recipeName": "ShieldBooster_Resistive",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 238850
+ }
+ },
+ "TinyHardpoint5": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128668536,
+ "modifiers": {
+ "engineerID": 300100,
+ "id": 3954,
+ "modifiers": [
+ {
+ "name": "mod_passive_power",
+ "type": 1,
+ "value": 0.013932451605797
+ },
+ {
+ "name": "mod_defencemodifier_global_shield_mult",
+ "type": 1,
+ "value": -0.047755606472492
+ },
+ {
+ "name": "mod_health",
+ "type": 1,
+ "value": -0.024494780227542
+ },
+ {
+ "name": "trade_passive_power_booster_global_mult",
+ "type": 2,
+ "value": -0.066611737012863
+ }
+ ],
+ "moduleTags": [
+ 22
+ ],
+ "recipeID": 128673790,
+ "slotIndex": 37
+ },
+ "name": "Hpt_ShieldBooster_Size0_Class5",
+ "on": true,
+ "priority": 0,
+ "recipeLevel": 1,
+ "recipeName": "ShieldBooster_Resistive",
+ "recipeValue": 0,
+ "unloaned": 0,
+ "value": 238850
+ }
+ },
+ "TinyHardpoint6": {
+ "module": {
+ "ammo": {
+ "clip": 1,
+ "hopper": 2
+ },
+ "free": false,
+ "health": 1000000,
+ "id": 128049519,
+ "name": "Hpt_HeatSinkLauncher_Turret_Tiny",
+ "on": true,
+ "priority": 0,
+ "unloaned": 0,
+ "value": 2975
+ }
+ },
+ "TinyHardpoint7": {
+ "module": {
+ "ammo": {
+ "clip": 1,
+ "hopper": 2
+ },
+ "free": false,
+ "health": 1000000,
+ "id": 128049519,
+ "name": "Hpt_HeatSinkLauncher_Turret_Tiny",
+ "on": false,
+ "priority": 2,
+ "unloaned": 0,
+ "value": 3150
+ }
+ },
+ "TinyHardpoint8": {
+ "module": {
+ "free": false,
+ "health": 1000000,
+ "id": 128662532,
+ "name": "Hpt_CrimeScanner_Size0_Class3",
+ "on": true,
+ "priority": 0,
+ "unloaned": 0,
+ "value": 103615
+ }
+ },
+ "WeaponColour": []
+ },
+ "name": "Federation_Corvette",
+ "oxygenRemaining": 450000,
+ "passengers": [],
+ "refinery": null,
+ "value": {
+ "cargo": 0,
+ "hull": 155200708,
+ "modules": 455056355,
+ "total": 610257063,
+ "unloaned": 83515
+ }
+}
diff --git a/__tests__/fixtures/ed-shipyard-import-valid.json b/__tests__/fixtures/ed-shipyard-import-valid.json
index b056d143..68c62411 100644
--- a/__tests__/fixtures/ed-shipyard-import-valid.json
+++ b/__tests__/fixtures/ed-shipyard-import-valid.json
@@ -2,31 +2,31 @@
{
"shipId": "anaconda",
"buildName": "Imported Anaconda",
- "buildCode": "0pyttFolodDsyf5------1717--------05044j-03--2h--00.Iw18ZlA=.Aw18ZlA=",
+ "buildCode": "0pyttFolodDsyf5------1717--------05044j-03--2h--00.Iw18ZlA=.Aw18ZlA=.",
"buildText": "[Anaconda]\nS: 1F/F Pulse Laser\nS: 1F/F Pulse Laser\n\nBH: 1I Lightweight Alloy\nRB: 8E Power Plant\nTM: 7E Thrusters\nFH: 6E Frame Shift Drive\nEC: 5E Life Support\nPC: 8E Power Distributor\nSS: 8E Sensors\nFS: 5C Fuel Tank (Capacity: 32)\n\n7: 6E Cargo Rack (Capacity: 64)\n6: 5E Cargo Rack (Capacity: 32)\n6: 6E Shield Generator\n5: 4E Cargo Rack (Capacity: 16)\n4: 1E Basic Discovery Scanner\n2: 1E Cargo Rack (Capacity: 2)\n"
},
{
"shipId": "anaconda",
"buildName": "Imported Anaconda",
- "buildCode": "0pyttFolodDsyf5------1717--------05044j-03--2h--00.Iw18ZlA=.Aw18ZlA=",
+ "buildCode": "0pyttFolodDsyf5------1717--------05044j-03--2h--00.Iw18ZlA=.Aw18ZlA=.",
"buildText": "\n\n \t[Anaconda]\nS: 1F/F Pulse Laser\nS: 1F/F Pulse Laser\n\nBH: 1I Lightweight Alloy\nRB: 8E Power Plant\nTM: 7E Thrusters\nFH: 6E Frame Shift Drive\nEC: 5E Life Support\nPC: 8E Power Distributor\nSS: 8E Sensors\nFS: 5C Fuel Tank (Capacity: 32)\n\n7: 6E Cargo Rack (Capacity: 64)\n6: 5E Cargo Rack (Capacity: 32)\n6: 6E Shield Generator\n5: 4E Cargo Rack (Capacity: 16)\n4: 1E Basic Discovery Scanner\n2: 1E Cargo Rack (Capacity: 2)\n"
},
{
"shipId": "cobra_mk_iii",
"buildName": "Imported Cobra Mk III",
- "buildCode": "0patcFeldd5sdf41712222503040202490f242h.Iw1-kA==.Aw1-kA==",
+ "buildCode": "0patcFeldd5sdf41712222503040202490f242h.Iw1-kA==.Aw1-kA==.",
"buildText": "[Cobra Mk III]\nM: 1F/F Pulse Laser\nM: 1G/G Burst Laser\nS: 1E/T Fragment Cannon\nS: 1G/T Multi-cannon\nU: 0I Point Defence\nU: 0A Shield Booster\n\nBH: 1I Lightweight Alloy\nRB: 4A Power Plant\nTM: 4C Thrusters\nFH: 4E Frame Shift Drive\nEC: 3D Life Support\nPC: 2A Power Distributor\nSS: 3D Sensors\nFS: 4C Fuel Tank (Capacity: 16)\n\n4: 3E Cargo Rack (Capacity: 8)\n4: 3E Cargo Rack (Capacity: 8)\n4: 4E Shield Generator\n2: 2C Auto Field-Maintenance Unit\n2: 1E Standard Docking Computer\n2: 1E Basic Discovery Scanner\n---\nShield: 112.29 MJ\nPower : 10.45 MW retracted (67%)\n 12.16 MW deployed (78%)\n 15.60 MW available\nCargo : 16 T\nFuel : 16 T\nMass : 235.5 T empty\n 267.5 T full\nRange : 10.69 LY unladen\n 10.05 LY laden\nPrice : 2,929,040 CR\nRe-Buy: 146,452 CR @ 95% insurance\n"
},
{
"shipId": "type_9_heavy",
"buildName": "Imported Type-9 Heavy",
- "buildCode": "3pftsFklkdisif57e2k2f2h110001020306054j03022f01242i.Iw18eQ==.Aw18eQ==",
+ "buildCode": "3pftsFklkdisif57e2k2f2h110001020306054j03022f01242i.Iw18eQ==.Aw18eQ==.",
"buildText": "[Type-9 Heavy]\nM: 2D/G Fragment Cannon\nM: 2I/F Mine Launcher\nM: 2B/FD Missile Rack\nS: 1I/FS Torpedo Pylon\nS: 1F/F Burst Laser\nU: 0I Chaff Launcher\nU: 0F Electronic Countermeasure\nU: 0I Heat Sink Launcher\nU: 0I Point Defence\n\nBH: 1I Mirrored Surface Composite\nRB: 5A Power Plant\nTM: 7D Thrusters\nFH: 6A Frame Shift Drive\nEC: 5A Life Support\nPC: 4D Power Distributor\nSS: 4D Sensors\nFS: 5C Fuel Tank (Capacity: 32)\n\n8: 7E Cargo Rack (Capacity: 128)\n7: 6E Cargo Rack (Capacity: 64)\n6: 6E Shield Generator\n5: 4E Cargo Rack (Capacity: 16)\n4: 3E Cargo Rack (Capacity: 8)\n4: 1C Advanced Discovery Scanner\n3: 2E Cargo Rack (Capacity: 4)\n3: 1E Standard Docking Computer\n2: 1C Detailed Surface Scanner\n"
},
{
"shipId": "vulture",
"buildName": "Imported Vulture",
- "buildCode": "4patfFalddksif31e1e0e0j04044a0n532jf1.Iw19kA==.Aw19kA==",
+ "buildCode": "4patfFalddksif31e1e0e0j04044a0n532jf1.Iw19kA==.Aw19kA==.",
"buildText": "[Vulture]\nL: 3E/G Pulse Laser\nL: 3E/G Pulse Laser\nU: 0A Frame Shift Wake Scanner\nU: 0A Kill Warrant Scanner\nU: 0A Shield Booster\nU: 0A Shield Booster\n\nBH: 1I Reactive Surface Composite\nRB: 4A Power Plant\nTM: 5A Thrusters\nFH: 4A Frame Shift Drive\nEC: 3D Life Support\nPC: 5A Power Distributor\nSS: 4D Sensors\nFS: 3C Fuel Tank (Capacity: 8)\n\n5: 5A Shield Generator\n4: 4A Auto Field-Maintenance Unit\n2: 2A Shield Cell Bank\n1: 1A Fuel Scoop\n1: 1C Fuel Tank (Capacity: 2)"
}
-]
\ No newline at end of file
+]
diff --git a/__tests__/fixtures/expected-builds.json b/__tests__/fixtures/expected-builds.json
index d7f12000..2b985df4 100644
--- a/__tests__/fixtures/expected-builds.json
+++ b/__tests__/fixtures/expected-builds.json
@@ -1,50 +1,50 @@
{
"type_6_transporter": {
- "Cargo": "0p0tdFal8d8s8f4-----04040303430101.Iw1-kA==.Aw1-kA==",
- "Miner": "0p5tdFal8d8s8f42l2l---040403451q0101.Iw1-kA==.Aw1-kA==",
- "Hopper": "0p0tdFal8d0s8f41717---030302024300-.Iw1-kA==.Aw1-kA=="
+ "Cargo": "0p0tdFal8d8s8f4-----04040303430101.Iw1-kA==.Aw1-kA==.",
+ "Miner": "0p5tdFal8d8s8f42l2l---040403451q0101.Iw1-kA==.Aw1-kA==.",
+ "Hopper": "0p0tdFal8d0s8f41717---030302024300-.Iw1-kA==.Aw1-kA==."
},
"type_7_transport": {
- "Cargo": "0p0tiFfliddsdf5--------0505040403480101.Iw18aQ==.Aw18aQ==",
- "Miner": "0pdtiFflid8sdf5--2l2l----0505041v03450000.Iw18aQ==.Aw18aQ=="
+ "Cargo": "0p0tiFfliddsdf5--------0505040403480101.Iw18aQ==.Aw18aQ==.",
+ "Miner": "0pdtiFflid8sdf5--2l2l----0505041v03450000.Iw18aQ==.Aw18aQ==."
},
"federal_dropship": {
- "Cargo": "0pdtiFflnddsif4-1717------05040448020201.Iw18aQ==.Aw18aQ=="
+ "Cargo": "0pdtiFflnddsif4-1717------05040448020201.Iw18aQ==.Aw18aQ==."
},
"asp": {
- "Miner": "2pftfFflidfskf50s0s24242l2l---04054a1q02022o27.Iw18WQ==.Aw18WQ=="
+ "Miner": "2pftfFflidfskf50s0s24242l2l---04054a1q02022o27.Iw18WQ==.Aw18WQ==."
},
"imperial_clipper": {
- "Cargo": "0p5tiFflndisnf4--0s0s----0605450302020101.Iw18aQ==.Aw18aQ==",
- "Dream": "2pktkFflndpskf40v0v0s0s0404040n4k5n5d2b29292o-.Iw18aQ==.Aw18aQ==",
- "Current": "0patkFflndfskf4----------------.Iw18aQ==.Aw18aQ=="
+ "Cargo": "0p5tiFflndisnf4--0s0s----0605450302020101.Iw18aQ==.Aw18aQ==.",
+ "Dream": "2pktkFflndpskf40v0v0s0s0404040n4k5n5d2b29292o-.Iw18aQ==.Aw18aQ==.",
+ "Current": "0patkFflndfskf4----------------.Iw18aQ==.Aw18aQ==."
},
"type_9_heavy": {
- "Current": "0patsFklndnsif6---------0706054a0303020224.Iw18eQ==.Aw18eQ=="
+ "Current": "0patsFklndnsif6---------0706054a0303020224.Iw18eQ==.Aw18eQ==."
},
"python": {
- "Cargo": "0patnFflidsssf5---------050505040448020201.Iw18eQ==.Aw18eQ==",
- "Miner": "0pktkFflidpspf50v0v0v2m2m0404--050505Ce4a1v02022o.Iw18eQ==.Aw18eQ==",
- "Dream": "2pptkFfliduspf50v0v0v27270404040m5n5n4f2d2d032t0201.Iw18eQ==.Aw18eQ==",
- "Missile": "0pttoFjljdystf52f2g2d2ePh----04044j03---002h.Iw18eQ==.Aw18eQ=="
+ "Cargo": "0patnFflidsssf5---------050505040448020201.Iw18eQ==.Aw18eQ==.",
+ "Miner": "0pktkFflidpspf50v0v0v2m2m0404--050505Ce4a1v02022o.Iw18eQ==.Aw18eQ==.",
+ "Dream": "2pptkFfliduspf50v0v0v27270404040m5n5n4f2d2d032t0201.Iw18eQ==.Aw18eQ==.",
+ "Missile": "0pttoFjljdystf52f2g2d2ePh----04044j03---002h.Iw18eQ==.Aw18eQ==."
},
"anaconda": {
- "Dream": "4putpFklndzsuf52c0o0o0o1m1m0q0q0404040l0b0100004k5n5n112d2d040303326b.Iw18ZlA=.Aw18ZlA=",
- "Cargo": "0patnFklndnsxf5----------------0605050504040445030301.Iw18ZlA=.Aw18ZlA=",
- "Current": "0patnFklndksxf5----------------0605050504040403034524.Iw18ZlA=.Aw18ZlA=",
- "Explorer": "0patnFklndksxf5--------0202------f7050505040s372f2i4524.Iw18ZlA=.Aw18ZlA=",
- "Test": "4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.Iw18ZlA=.Aw18ZlA="
+ "Dream": "4putpFklndzsuf52c0o0o0o1m1m0q0q0404040l0b0100004k5n5n112d2d040303326b.Iw18ZlA=.Aw18ZlA=.",
+ "Cargo": "0patnFklndnsxf5----------------0605050504040445030301.Iw18ZlA=.Aw18ZlA=.",
+ "Current": "0patnFklndksxf5----------------0605050504040403034524.Iw18ZlA=.Aw18ZlA=.",
+ "Explorer": "0patnFklndksxf5--------0202------f7050505040s372f2i4524.Iw18ZlA=.Aw18ZlA=.",
+ "Test": "4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.Iw18ZlA=.Aw18ZlA=."
},
"diamondback_explorer": {
- "Explorer": "0p0tdFfldddsdf5---0202--320p432i2f.Iw1-kA==.Aw1-kA=="
+ "Explorer": "0p0tdFfldddsdf5---0202--320p432i2f.Iw1-kA==.Aw1-kA==."
},
"vulture": {
- "Bounty Hunter": "3patcFalddksff31e1e0404-0l4a5d27662j.Iw19kA==.Aw19kA=="
+ "Bounty Hunter": "3patcFalddksff31e1e0404-0l4a5d27662j.Iw19kA==.Aw19kA==."
},
"fer_de_lance": {
- "Attack": "2pfthFalidpsff31r0s0s0s0s000404-04-4a-5d27-.Iw18aQ==.Aw18aQ=="
+ "Attack": "2pfthFalidpsff31r0s0s0s0s000404-04-4a-5d27-.Iw18aQ==.Aw18aQ==."
},
"eagle": {
- "Figther": "4p0t5F5l3d5s5f20p0p24-40532j-.Iw1-EA==.Aw1-EA=="
+ "Figther": "4p0t5F5l3d5s5f20p0p24-40532j-.Iw1-EA==.Aw1-EA==."
}
-}
\ No newline at end of file
+}
diff --git a/__tests__/fixtures/valid-backup.json b/__tests__/fixtures/valid-backup.json
index 683511e1..a705dd3a 100644
--- a/__tests__/fixtures/valid-backup.json
+++ b/__tests__/fixtures/valid-backup.json
@@ -33,7 +33,8 @@
},
"anaconda": {
"Dream": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404040l0b0100034k5n05050404040303326b.AwRj4yo5dig=.MwBhEYy6duwEziA=",
- "Cargo": "03A7D6A5D4D8D5C----------------060505054d040403030301.AwRj4yuqg===.Aw18ZlA="
+ "Cargo": "03A7D6A5D4D8D5C----------------060505054d040403030301.AwRj4yuqg===.Aw18ZlA=",
+ "Modified": "0pyttFolodDsyf5------1717--------05044j-03----2h00.Iw18ZlA=.Aw18ZlA=.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA=="
},
"diamondback_explorer": {
"Explorer": "02A4D5A3D3D3D5C-------320p432i2f.AwRj4zTI.AwiMIypI"
@@ -63,4 +64,4 @@
1,
1
]
-}
\ No newline at end of file
+}
diff --git a/__tests__/test-import.js b/__tests__/test-import.js
index 27aa9a4c..fd5ef1b1 100644
--- a/__tests__/test-import.js
+++ b/__tests__/test-import.js
@@ -129,7 +129,7 @@ describe('Import Modal', function() {
});
});
- describe('Import Detailed Build', function() {
+ describe('Import Detailed V3 Build', function() {
beforeEach(reset);
@@ -142,7 +142,7 @@ describe('Import Modal', function() {
expect(modal.state.singleBuild).toBe(true);
clickProceed();
expect(MockRouter.go.mock.calls.length).toBe(1);
- expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/anaconda/4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA?bn=Test%20My%20Ship');
+ expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/anaconda/4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.?bn=Test%20My%20Ship');
});
it('catches an invalid build', function() {
@@ -154,6 +154,23 @@ describe('Import Modal', function() {
});
});
+ describe('Import Detailed V4 Build', function() {
+
+ beforeEach(reset);
+
+ it('imports a valid v4 build', function() {
+ const importData = require('./fixtures/anaconda-test-detailed-export-v4');
+ pasteText(JSON.stringify(importData));
+
+ expect(modal.state.importValid).toBeTruthy();
+ expect(modal.state.errorMsg).toEqual(null);
+ expect(modal.state.singleBuild).toBe(true);
+ clickProceed();
+ expect(MockRouter.go.mock.calls.length).toBe(1);
+ expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/anaconda/4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==?bn=Test%20My%20Ship');
+ });
+ });
+
describe('Import Detaild Builds Array', function() {
beforeEach(reset);
@@ -179,6 +196,23 @@ describe('Import Modal', function() {
});
});
+ describe('Import Companion API Build', function() {
+
+ beforeEach(reset);
+
+ it('imports a valid v4 build', function() {
+ const importData = require('./fixtures/companion-api-import-1');
+ pasteText(JSON.stringify(importData));
+
+ expect(modal.state.importValid).toBeTruthy();
+ expect(modal.state.errorMsg).toEqual(null);
+ expect(modal.state.singleBuild).toBe(true);
+ clickProceed();
+ expect(MockRouter.go.mock.calls.length).toBe(1);
+ expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/federal_corvette/2putsFklndzsxf50x0x7l28281919040404040402020l06p05sf63c5ifhv66g2f.AwRj4zNaKA==.CwRgDBldUExuBiQqA===.H4sIAAAAAAAAAx2Rzy4DURTGz7TuzHRu47ZjWreKlg5iQ9KFZ9CENyBWtWo8gIUFsamteAIJi0qEWIhdN11ZEN1IwyNYVKRpcXzH5su553f_XyfvKiLTYma-TkScyHVcokoYEdmbBNDsiDla-WUOT5LgyfAshHdvyGyjFFHUQCSrBU8TLT4gYq4DNL_LhNTFN3PwiqdZQyX2C-sekep-Mrs1RIbnDppsIogD1UAtN7JEM9eIzZg8hmhsEU32gFmrdgB_UARvjYEr4QMUMffoxGnV-M8X3hZ_lAO-gmWq2Eq2IVtDOzZ2Hbbuws6KxCKmKUUydgRb3woSiUXMs6Cs7Qt6FCQSi5hxkNKhj6qhfcPU_kU4wYrFMseSOmFXMKbuwZsViUWMlq1sbhvJ_lKyfqTqEJGJyoC5eIpU9x2TRnUswYXyF77BW4Z3qQuv05GDTpfvcDzvSbxJ5DtV_aHS1I4clyB2A5_b-pAL8x_enn626gEAAA==?bn=Imported%20Federal%20Corvette');
+ });
+ });
+
describe('Import E:D Shipyard Builds', function() {
it('imports a valid builds', function() {
diff --git a/__tests__/test-serializer.js b/__tests__/test-serializer.js
index c03aff75..59260558 100644
--- a/__tests__/test-serializer.js
+++ b/__tests__/test-serializer.js
@@ -4,16 +4,16 @@ import * as Serializer from '../src/app/shipyard/Serializer';
import jsen from 'jsen';
describe("Serializer", function() {
- const anacondaTestExport = require.requireActual('./fixtures/anaconda-test-detailed-export-v3');
+ const anacondaTestExport = require.requireActual('./fixtures/anaconda-test-detailed-export-v4');
const code = anacondaTestExport.references[0].code;
const anaconda = Ships.anaconda;
- const validate = jsen(require('../src/schemas/ship-loadout/3'));
+ const validate = jsen(require('../src/schemas/ship-loadout/4'));
describe("To Detailed Build", function() {
let testBuild = new Ship('anaconda', anaconda.properties, anaconda.slots).buildFrom(code);
let exportData = Serializer.toDetailedBuild('Test My Ship', testBuild);
- it("conforms to the v3 ship-loadout schema", function() {
+ it("conforms to the v4 ship-loadout schema", function() {
expect(validate(exportData)).toBe(true);
});
@@ -31,7 +31,7 @@ describe("Serializer", function() {
const builds = require('./fixtures/expected-builds');
const exportData = Serializer.toDetailedExport(builds);
- it("conforms to the v3 ship-loadout schema", function() {
+ it("conforms to the v4 ship-loadout schema", function() {
expect(exportData instanceof Array).toBe(true);
for (let detailedBuild of exportData) {
diff --git a/__tests__/test-ship.js b/__tests__/test-ship.js
index 00ef39cc..9d52f01c 100644
--- a/__tests__/test-ship.js
+++ b/__tests__/test-ship.js
@@ -24,7 +24,7 @@ describe("Ship", function() {
expect(ship.fuelCapacity).toBeGreaterThan(0, s + ' fuelCapacity');
expect(ship.unladenFastestRange).toBeGreaterThan(0, s + ' unladenFastestRange');
expect(ship.ladenFastestRange).toBeGreaterThan(0, s + ' ladenFastestRange');
- expect(ship.shieldStrength).toBeGreaterThan(0, s + ' shieldStrength');
+ expect(ship.shield).toBeGreaterThan(0, s + ' shield');
expect(ship.armour).toBeGreaterThan(0, s + ' armour');
expect(ship.topSpeed).toBeGreaterThan(0, s + ' topSpeed');
}
diff --git a/package.json b/package.json
index 674af442..113483eb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "coriolis_shipyard",
- "version": "2.2.1",
+ "version": "2.2.2",
"repository": {
"type": "git",
"url": "https://github.com/EDCD/coriolis"
@@ -84,12 +84,13 @@
"dependencies": {
"babel-polyfill": "*",
"classnames": "^2.2.0",
+ "browserify-zlib": "ipfs/browserify-zlib",
"coriolis-data": "EDCD/coriolis-data",
"d3": "3.5.16",
"fbemitter": "^2.0.0",
"lodash": "^4.15.0",
"lz-string": "^1.4.4",
- "react-number-editor": "^4.0.2",
+ "react-number-editor": "Athanasius/react-number-editor.git#miggy",
"react": "^15.0.1",
"react-dom": "^15.0.1",
"superagent": "^1.4.0"
diff --git a/src/app/Coriolis.jsx b/src/app/Coriolis.jsx
index ad17d1d5..2e180587 100644
--- a/src/app/Coriolis.jsx
+++ b/src/app/Coriolis.jsx
@@ -7,6 +7,8 @@ import Persist from './stores/Persist';
import Header from './components/Header';
import Tooltip from './components/Tooltip';
import ModalImport from './components/ModalImport';
+import * as CompanionApiUtils from './utils/CompanionApiUtils';
+import { outfitURL } from './utils/UrlGenerators'
import AboutPage from './pages/AboutPage';
import NotFoundPage from './pages/NotFoundPage';
@@ -15,6 +17,8 @@ import ComparisonPage from './pages/ComparisonPage';
import ShipyardPage from './pages/ShipyardPage';
import ErrorDetails from './pages/ErrorDetails';
+const zlib = require('zlib');
+
/**
* Coriolis App
*/
@@ -52,6 +56,7 @@ export default class Coriolis extends React.Component {
this._onLanguageChange = this._onLanguageChange.bind(this);
this._onSizeRatioChange = this._onSizeRatioChange.bind(this);
this._keyDown = this._keyDown.bind(this);
+ this._importBuild = this._importBuild.bind(this);
this.emitter = new EventEmitter();
this.state = {
@@ -63,13 +68,36 @@ export default class Coriolis extends React.Component {
};
Router('', (r) => this._setPage(ShipyardPage, r));
+ Router('/import?', (r) => this._importBuild(r));
+ Router('/import/:data', (r) => this._importBuild(r));
+ Router('/outfit/?', (r) => this._setPage(OutfittingPage, r));
+ Router('/outfit/:ship/?', (r) => this._setPage(OutfittingPage, r));
Router('/outfit/:ship/:code?', (r) => this._setPage(OutfittingPage, r));
Router('/compare/:name?', (r) => this._setPage(ComparisonPage, r));
+ Router('/comparison?', (r) => this._setPage(ComparisonPage, r));
Router('/comparison/:code', (r) => this._setPage(ComparisonPage, r));
Router('/about', (r) => this._setPage(AboutPage, r));
Router('*', (r) => this._setPage(null, r));
}
+ /**
+ * Import a build directly
+ * @param {Object} r The current route
+ */
+ _importBuild(r) {
+ try {
+ // Need to decode and gunzip the data, then build the ship
+ const data = zlib.gunzipSync(new Buffer(r.params.data, 'base64'));
+ const json = JSON.parse(data);
+ const ship = CompanionApiUtils.shipFromJson(json);
+ r.params.ship = ship.id;
+ r.params.code = ship.toString();
+ this._setPage(OutfittingPage, r);
+ } catch (err) {
+ this._onError('Failed to import ship', r.path, 0, 0, err);
+ }
+ }
+
/**
* Updates / Sets the page and route context
* @param {[type]} page The page to be shown
diff --git a/src/app/components/ComparisonTable.jsx b/src/app/components/ComparisonTable.jsx
index ca99d307..3f831a3a 100644
--- a/src/app/components/ComparisonTable.jsx
+++ b/src/app/components/ComparisonTable.jsx
@@ -2,6 +2,7 @@ import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import Link from './Link';
import cn from 'classnames';
+import { outfitURL } from '../utils/UrlGenerators';
import { SizeMap } from '../shipyard/Constants';
@@ -71,7 +72,7 @@ export default class ComparisonTable extends TranslatedComponent {
* @return {React.Component} Table row
*/
_buildRow(build, facets, formats, units) {
- let url = `/outfit/${build.id}/${build.toString()}?bn=${build.buildName}`;
+ let url = outfitURL(build.id, build.toString(), build.buildName)
let cells = [
{build.name} ,
{build.buildName}
diff --git a/src/app/components/CostSection.jsx b/src/app/components/CostSection.jsx
index 699f5d20..2df44855 100644
--- a/src/app/components/CostSection.jsx
+++ b/src/app/components/CostSection.jsx
@@ -379,7 +379,7 @@ export default class CostSection extends TranslatedComponent {
{translate('retrofit from')}
▾
-
+
{options}
@@ -419,7 +419,9 @@ export default class CostSection extends TranslatedComponent {
let retroSlotGroup = retrofitShip[g];
let slotGroup = ship[g];
for (i = 0, l = slotGroup.length; i < l; i++) {
- if (slotGroup[i].m != retroSlotGroup[i].m) {
+ const modId = slotGroup[i].m ? slotGroup[i].m.eddbID : null;
+ const retroModId = retroSlotGroup[i].m ? retroSlotGroup[i].m.eddbID : null;
+ if (modId != retroModId) {
item = { netCost: 0, retroItem: retroSlotGroup[i] };
if (slotGroup[i].m) {
item.buyName = slotGroup[i].m.name || slotGroup[i].m.grp;
@@ -505,19 +507,19 @@ export default class CostSection extends TranslatedComponent {
scoop = true;
break;
case 'scb':
- q = slotGroup[i].m.cells;
+ q = slotGroup[i].m.getCells();
break;
case 'am':
- q = slotGroup[i].m.ammo;
+ q = slotGroup[i].m.getAmmo();
break;
case 'pv':
- srvs += slotGroup[i].m.vehicles;
+ srvs += slotGroup[i].m.getBays();
break;
case 'fx': case 'hb': case 'cc': case 'pc':
limpets = ship.cargoCapacity;
break;
default:
- q = slotGroup[i].m.clip + slotGroup[i].m.ammo;
+ q = slotGroup[i].m.getClip() + slotGroup[i].m.getAmmo();
}
// Calculate ammo costs only if a cost is specified
if (slotGroup[i].m.ammocost > 0) {
@@ -530,6 +532,17 @@ export default class CostSection extends TranslatedComponent {
ammoCosts.push(item);
ammoTotal += item.total;
}
+ // Add fighters
+ if (slotGroup[i].m.grp === 'fh') {
+ item = {
+ m: slotGroup[i].m,
+ max: slotGroup[i].m.getRebuildsPerBay() * slotGroup[i].m.getBays(),
+ cost: slotGroup[i].m.fightercost,
+ total: slotGroup[i].m.getRebuildsPerBay() * slotGroup[i].m.getBays() * slotGroup[i].m.fightercost
+ };
+ ammoCosts.push(item);
+ ammoTotal += item.total;
+ }
}
}
}
@@ -550,12 +563,13 @@ export default class CostSection extends TranslatedComponent {
item = {
m: { name: 'SRVs', class: '', rating: '' },
max: srvs,
- cost: 6005,
- total: srvs * 6005
+ cost: 1030,
+ total: srvs * 1030
};
ammoCosts.push(item);
ammoTotal += item.total;
}
+
// Calculate refuel costs if no scoop present
if (!scoop) {
item = {
diff --git a/src/app/components/DefenceSummary.jsx b/src/app/components/DefenceSummary.jsx
new file mode 100644
index 00000000..d9ec54a9
--- /dev/null
+++ b/src/app/components/DefenceSummary.jsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import cn from 'classnames';
+import TranslatedComponent from './TranslatedComponent';
+import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons';
+
+/**
+ * Defence summary
+ */
+export default class DefenceSummary extends TranslatedComponent {
+ static PropTypes = {
+ ship: React.PropTypes.object.isRequired
+ };
+
+ /**
+ * Constructor
+ * @param {Object} props React Component properties
+ */
+ constructor(props) {
+ super(props);
+ }
+
+ /**
+ * Render defence summary
+ * @return {React.Component} contents
+ */
+ render() {
+ let ship = this.props.ship;
+ let { language, tooltip, termtip } = this.context;
+ let { formats, translate, units } = language;
+ let hide = tooltip.bind(null, null);
+
+ return (
+
+ {translate('defence summary')}
+
+
+ {ship.shield ?
+
+ {translate('shields')}: {formats.int(ship.shield)} {units.MJ}
+ : null }
+ {ship.shield ?
+
+ {translate('recovery')}
+ {formats.time(ship.calcShieldRecovery())}
+ {translate('recharge')}
+ {formats.time(ship.calcShieldRecharge())}
+ : null }
+ {ship.shield ?
+
+ {translate('damage from')}
+ {formats.pct1(ship.shieldExplRes || 1)}
+ {formats.pct1(ship.shieldKinRes || 1)}
+ {formats.pct1(ship.shieldThermRes || 1)}
+ : null }
+
+ { ship.shield && ship.shieldCells ?
+
+ {translate('shield cells')}: {formats.int(ship.shieldCells)} {units.MJ}
+ : null }
+
+
+ {translate('armour')}: {formats.int(ship.armour)}
+
+
+ {translate('damage from')}
+ {formats.pct1(ship.hullExplRes || 1)}
+ {formats.pct1(ship.hullKinRes || 1)}
+ {formats.pct1(ship.hullThermRes || 1)}
+
+
+
+
+ );
+ }
+}
diff --git a/src/app/components/HardpointSlot.jsx b/src/app/components/HardpointSlot.jsx
index 9ecc6dae..fa3e29e5 100644
--- a/src/app/components/HardpointSlot.jsx
+++ b/src/app/components/HardpointSlot.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import Slot from './Slot';
-import { DamageKinetic, DamageThermal, DamageExplosive, MountFixed, MountGimballed, MountTurret, ListModifications } from './SvgIcons';
+import Persist from '../stores/Persist';
+import { DamageKinetic, DamageThermal, DamageExplosive, MountFixed, MountGimballed, MountTurret, ListModifications, Modified } from './SvgIcons';
import { Modifications } from 'coriolis-data/dist';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
@@ -41,6 +42,7 @@ export default class HardpointSlot extends Slot {
let { drag, drop } = this.props;
let { termtip, tooltip } = this.context;
let validMods = Modifications.validity[m.grp] || [];
+ let showModuleResistances = Persist.showModuleResistances();
return
@@ -51,7 +53,9 @@ export default class HardpointSlot extends Slot {
{m.type && m.type.match('K') ? : ''}
{m.type && m.type.match('T') ? : ''}
{m.type && m.type.match('E') ? : ''}
- {classRating} {translate(m.name || m.grp)}
+ {classRating} {translate(m.name || m.grp)}{ m.mods && Object.keys(m.mods).length > 0 ?
: null }
+
+
{formats.round(m.getMass())}{u.T}
@@ -60,10 +64,15 @@ export default class HardpointSlot extends Slot {
{ m.getHps() ?
{translate('HPS')}: {formats.round1(m.getHps())} { m.getClip() ? ({formats.round1((m.getClip() * m.getHps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload())) }) : null }
: null }
{ m.getDps() && m.getEps() ?
{translate('DPE')}: {formats.f1(m.getDps() / m.getEps())}
: null }
{ m.getRoF() ?
{translate('ROF')}: {formats.f1(m.getRoF())}{u.ps}
: null }
- { m.getRange() && !m.getDps() ?
{translate('Range')} : {formats.round(m.getRange() / 1000)}{u.km}
: null }
+ { m.getRange() ?
{translate('range')} {formats.f1(m.getRange() / 1000)}{u.km}
: null }
{ m.getShieldBoost() ?
+{formats.pct1(m.getShieldBoost())}
: null }
{ m.getAmmo() ?
{translate('ammunition')}: {formats.int(m.getClip())}/{formats.int(m.getAmmo())}
: null }
+ { m.getJitter() ?
{translate('jitter')}: {formats.f2(m.getJitter())}°
: null }
+ { showModuleResistances && m.getExplosiveResistance() ?
{translate('explres')}: {formats.pct(m.getExplosiveResistance())}
: null }
+ { showModuleResistances && m.getKineticResistance() ?
{translate('kinres')}: {formats.pct(m.getKineticResistance())}
: null }
+ { showModuleResistances && m.getThermalResistance() ?
{translate('thermres')}: {formats.pct(m.getThermalResistance())}
: null }
{ m && validMods.length > 0 ?
: null }
+
;
} else {
diff --git a/src/app/components/Header.jsx b/src/app/components/Header.jsx
index db2294d5..8b679a6b 100644
--- a/src/app/components/Header.jsx
+++ b/src/app/components/Header.jsx
@@ -203,6 +203,13 @@ export default class Header extends TranslatedComponent {
Persist.showTooltips(!Persist.showTooltips());
}
+ /**
+ * Toggle module resistances setting
+ */
+ _toggleModuleResistances() {
+ Persist.showModuleResistances(!Persist.showModuleResistances());
+ }
+
/**
* Show delete all modal
* @param {SyntheticEvent} e Event
@@ -359,6 +366,7 @@ export default class Header extends TranslatedComponent {
_getSettingsMenu() {
let translate = this.context.language.translate;
let tips = Persist.showTooltips();
+ let moduleResistances = Persist.showModuleResistances();
return (
e.stopPropagation() }>
@@ -376,6 +384,10 @@ export default class Header extends TranslatedComponent {
{translate('tooltips')}
{(tips ? '✓' : '✗')}
+
+ {translate('module resistances')}
+ {(moduleResistances ? '✓' : '✗')}
+
{translate('insurance')}
@@ -438,6 +450,7 @@ export default class Header extends TranslatedComponent {
Persist.addListener('deletedAll', update);
Persist.addListener('builds', update);
Persist.addListener('tooltips', update);
+ Persist.addListener('moduleresistances', update);
}
/**
diff --git a/src/app/components/InternalSlot.jsx b/src/app/components/InternalSlot.jsx
index 318bb85b..f936b6be 100644
--- a/src/app/components/InternalSlot.jsx
+++ b/src/app/components/InternalSlot.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import Slot from './Slot';
-import { ListModifications } from './SvgIcons';
+import Persist from '../stores/Persist';
+import { ListModifications, Modified } from './SvgIcons';
import { Modifications } from 'coriolis-data/dist';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
@@ -23,11 +24,12 @@ export default class InternalSlot extends Slot {
let { drag, drop, ship } = this.props;
let { termtip, tooltip } = this.context;
let validMods = Modifications.validity[m.grp] || [];
+ let showModuleResistances = Persist.showModuleResistances();
let mass = m.getMass() || m.cargo || m.fuel || 0;
return
-
{classRating} {translate(m.name || m.grp)}
+
{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? : ''}
{formats.round(mass)}{u.T}
@@ -38,7 +40,7 @@ export default class InternalSlot extends Slot {
{ m.rate ?
{translate('rate')}: {m.rate}{u.kgs} {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}
: null }
{ m.getAmmo() ?
{translate('ammunition')}: {formats.gen(m.getAmmo())}
: null }
{ m.cells ?
{translate('cells')}: {m.cells}
: null }
- { m.recharge ?
{translate('recharge')}: {m.recharge} MJ {translate('total')}: {m.cells * m.recharge}{u.MJ}
: null }
+ { m.shieldreinforcement ?
{translate('shieldreinforcement')}: {formats.int(m.getShieldReinforcement())} MJ {translate('total')}: {formats.int(m.cells * m.getShieldReinforcement())}{u.MJ}
: null }
{ m.repair ?
{translate('repair')}: {m.repair}
: null }
{ m.getFacingLimit() ?
{translate('facinglimit')} {formats.f1(m.getFacingLimit())}°
: null }
{ m.getRange() ?
{translate('range')} {formats.f2(m.getRange())}{u.km}
: null }
@@ -49,8 +51,12 @@ export default class InternalSlot extends Slot {
{ m.rangeLS ?
{translate('range')}: {m.rangeLS}{u.Ls}
: null }
{ m.rangeLS === null ?
∞{u.Ls}
: null }
{ m.rangeRating ?
{translate('range')}: {m.rangeRating}
: null }
- { m.getHullReinforcement() ?
+{formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost'))} {translate('armour')}
: null }
+ { m.getHullReinforcement() ?
+{formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost') / 10000)} {translate('armour')}
: null }
{ m.passengers ?
{translate('passengers')}: {m.passengers}
: null }
+ { showModuleResistances && m.getExplosiveResistance() ?
{translate('explres')}: {formats.pct(m.getExplosiveResistance())}
: null }
+ { showModuleResistances && m.getKineticResistance() ?
{translate('kinres')}: {formats.pct(m.getKineticResistance())}
: null }
+ { showModuleResistances && m.getThermalResistance() ?
{translate('thermres')}: {formats.pct(m.getThermalResistance())}
: null }
+
{ m && validMods.length > 0 ?
: null }
diff --git a/src/app/components/ModalImport.jsx b/src/app/components/ModalImport.jsx
index 04dd2b87..b698a9d9 100644
--- a/src/app/components/ModalImport.jsx
+++ b/src/app/components/ModalImport.jsx
@@ -11,6 +11,7 @@ import * as ModuleUtils from '../shipyard/ModuleUtils';
import { fromDetailedBuild } from '../shipyard/Serializer';
import { Download } from './SvgIcons';
import { outfitURL } from '../utils/UrlGenerators';
+import * as CompanionApiUtils from '../utils/CompanionApiUtils';
const textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n');
const lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)');
@@ -112,6 +113,7 @@ export default class ModalImport extends TranslatedComponent {
this._importBackup = this._importBackup.bind(this);
this._importDetailedArray = this._importDetailedArray.bind(this);
this._importTextBuild = this._importTextBuild.bind(this);
+ this._importCompanionApiBuild = this._importCompanionApiBuild.bind(this);
this._validateImport = this._validateImport.bind(this);
}
@@ -183,6 +185,21 @@ export default class ModalImport extends TranslatedComponent {
this.setState({ builds });
}
+ /**
+ * Import a build direct from the companion API
+ * @param {string} build JSON from the companion API information
+ * @throws {string} if parse/import fails
+ */
+ _importCompanionApiBuild(build) {
+ const shipModel = CompanionApiUtils.shipModelFromJson(build);
+ const ship = CompanionApiUtils.shipFromJson(build);
+
+ let builds = {};
+ builds[shipModel] = {};
+ builds[shipModel]['Imported ' + Ships[shipModel].properties.name] = ship.toString();
+ this.setState({ builds, singleBuild: true });
+ }
+
/**
* Import a text build from ED Shipyard
* @param {string} buildStr Build string
@@ -315,7 +332,11 @@ export default class ModalImport extends TranslatedComponent {
throw 'Must be an object or array!';
}
- if (importData instanceof Array) { // Must be detailed export json
+ if (importData.modules != null && importData.modules.Armour != null) { // Only the companion API has this information
+ this._importCompanionApiBuild(importData); // Single sihp definition
+ } else if (importData.ship != null && importData.ship.modules != null && importData.ship.modules.Armour != null) { // Only the companion API has this information
+ this._importCompanionApiBuild(importData.ship); // Complete API dump
+ } else if (importData instanceof Array) { // Must be detailed export json
this._importDetailedArray(importData);
} else if (importData.ship && typeof importData.name !== undefined) { // Using JSON from a single ship build export
this._importDetailedArray([importData]); // Convert to array with singleobject
diff --git a/src/app/components/Modification.jsx b/src/app/components/Modification.jsx
index c73d0607..6a1464d4 100644
--- a/src/app/components/Modification.jsx
+++ b/src/app/components/Modification.jsx
@@ -7,7 +7,7 @@ import NumberEditor from 'react-number-editor';
/**
* Modification
*/
-export default class ModificationsMenu extends TranslatedComponent {
+export default class Modification extends TranslatedComponent {
static propTypes = {
ship: React.PropTypes.object.isRequired,
@@ -24,26 +24,30 @@ export default class ModificationsMenu extends TranslatedComponent {
constructor(props, context) {
super(props);
this.state = {};
- this.state.value = this.props.m.getModValue(this.props.name) * 100 || 0;
+ this.state.value = this.props.m.getModValue(this.props.name) / 100 || 0;
}
/**
* Update modification given a value.
- * @param {Number} value The value to set
+ * @param {Number} value The value to set. This comes in as a string and must be stored in state as a string,
+ * because it needs to allow illegal 'numbers' ('-', '1.', etc) when the user is typing
+ * in a value by hand
*/
_updateValue(value) {
- let scaledValue = Math.floor(Number(value) * 100) / 10000;
+ const name = this.props.name;
+
+ let scaledValue = Math.round(Number(value) * 100);
// Limit to +1000% / -100%
- if (scaledValue > 10) {
- scaledValue = 10;
+ if (scaledValue > 100000) {
+ scaledValue = 100000;
value = 1000;
}
- if (scaledValue < -1) {
- scaledValue = -1;
+ if (scaledValue < -10000) {
+ scaledValue = -10000;
value = -100;
}
+
let m = this.props.m;
- let name = this.props.name;
let ship = this.props.ship;
ship.setModification(m, name, scaledValue);
@@ -62,7 +66,7 @@ export default class ModificationsMenu extends TranslatedComponent {
return (
{translate(name)}{name === 'jitter' ? ' (°)' : ' (%)'}
-
+
);
}
diff --git a/src/app/components/ModificationsMenu.jsx b/src/app/components/ModificationsMenu.jsx
index bdf633d3..f38c8b1e 100644
--- a/src/app/components/ModificationsMenu.jsx
+++ b/src/app/components/ModificationsMenu.jsx
@@ -50,11 +50,13 @@ export default class ModificationsMenu extends TranslatedComponent {
* @return {React.Component} List
*/
render() {
+ let { tooltip, termtip } = this.context;
return (
e.stopPropagation() }
onContextMenu={stopCtxPropagation}
+ onMouseOver={termtip.bind(null, 'HELP_MODIFICATIONS_MENU')} onMouseOut={tooltip.bind(null, null)}
>
{this.state.list}
diff --git a/src/app/components/OffenceSummary.jsx b/src/app/components/OffenceSummary.jsx
new file mode 100644
index 00000000..9123ce5b
--- /dev/null
+++ b/src/app/components/OffenceSummary.jsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import cn from 'classnames';
+import TranslatedComponent from './TranslatedComponent';
+import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons';
+
+/**
+ * Offence summary
+ */
+export default class OffenceSummary extends TranslatedComponent {
+ static PropTypes = {
+ ship: React.PropTypes.object.isRequired
+ };
+
+ /**
+ * Constructor
+ * @param {Object} props React Component properties
+ */
+ constructor(props) {
+ super(props);
+ }
+
+ /**
+ * Render offence summary
+ * @return {React.Component} contents
+ */
+ render() {
+ let ship = this.props.ship;
+ let { language, tooltip, termtip } = this.context;
+ let { formats, translate } = language;
+
+ return (
+
+ {translate('offence summary')}
+
+
+
+ {translate('dps')}: {formats.f1(ship.totalDps)}
+
+
+ {translate('damage by')}
+ {formats.f1(ship.totalExplDps)}
+ {formats.f1(ship.totalKinDps)}
+ {formats.f1(ship.totalThermDps)}
+
+
+
+ {translate('sdps')}: {formats.f1(ship.totalSDps)}
+
+
+ {translate('damage by')}
+ {formats.f1(ship.totalExplSDps)}
+ {formats.f1(ship.totalKinSDps)}
+ {formats.f1(ship.totalThermSDps)}
+
+
+
+ {translate('dpe')}: {formats.f1(ship.totalDpe)}
+
+
+ {translate('damage by')}
+ {formats.f1(ship.totalExplDpe)}
+ {formats.f1(ship.totalKinDpe)}
+ {formats.f1(ship.totalThermDpe)}
+
+
+
+
+ );
+ }
+}
diff --git a/src/app/components/ShipSummaryTable.jsx b/src/app/components/ShipSummaryTable.jsx
index e5069369..9ab15a9d 100644
--- a/src/app/components/ShipSummaryTable.jsx
+++ b/src/app/components/ShipSummaryTable.jsx
@@ -34,20 +34,6 @@ export default class ShipSummaryTable extends TranslatedComponent {
sgRecharge = time(ship.calcShieldRecharge());
}
- //
{translate('shield resistance')}
- //
{translate('hull resistance')}
- //
{translate('explosive')}
- //
{translate('kinetic')}
- //
{translate('thermal')}
- //
{translate('explosive')}
- //
{translate('kinetic')}
- //
{translate('thermal')}
- //
{pct(ship.shieldExplRes)}
- //
{pct(ship.shieldKinRes)}
- //
{pct(ship.shieldThermRes)}
- //
{pct(ship.hullExplRes)}
- //
{pct(ship.hullKinRes)}
- //
{pct(ship.hullThermRes)}
return
@@ -58,7 +44,7 @@ export default class ShipSummaryTable extends TranslatedComponent {
{translate('EPS')}
{translate('HPS')}
{translate('armour')}
- {translate('shields')}
+ {translate('shields')}
{translate('mass')}
{translate('cargo')}
{translate('fuel')}
@@ -67,9 +53,6 @@ export default class ShipSummaryTable extends TranslatedComponent {
{translate('MLF')}
- {translate('strength')}
- {translate('recovery')}
- {translate('recharge')}
{translate('hull')}
{translate('unladen')}
{translate('laden')}
@@ -90,8 +73,6 @@ export default class ShipSummaryTable extends TranslatedComponent {
{f1(ship.totalHps)}
{int(ship.armour)}
{int(ship.shield)} {u.MJ}
- {sgRecover}
- {sgRecharge}
{ship.hullMass} {u.T}
{int(ship.unladenMass)} {u.T}
{int(ship.ladenMass)} {u.T}
diff --git a/src/app/components/StandardSlot.jsx b/src/app/components/StandardSlot.jsx
index 8140f7f0..f4a61415 100644
--- a/src/app/components/StandardSlot.jsx
+++ b/src/app/components/StandardSlot.jsx
@@ -1,11 +1,12 @@
import React from 'react';
import cn from 'classnames';
+import Persist from '../stores/Persist';
import TranslatedComponent from './TranslatedComponent';
import { jumpRange } from '../shipyard/Calculations';
import { diffDetails } from '../utils/SlotFunctions';
import AvailableModulesMenu from './AvailableModulesMenu';
import ModificationsMenu from './ModificationsMenu';
-import { ListModifications } from './SvgIcons';
+import { ListModifications, Modified } from './SvgIcons';
import { Modifications } from 'coriolis-data/dist';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
@@ -46,6 +47,7 @@ export default class StandardSlot extends TranslatedComponent {
let classRating = m.class + m.rating;
let menu;
let validMods = m == null ? [] : (Modifications.validity[m.grp] || []);
+ let showModuleResistances = Persist.showModuleResistances();
let mass = m.getMass() || m.cargo || m.fuel || 0;
if (!selected) {
@@ -79,11 +81,10 @@ export default class StandardSlot extends TranslatedComponent {
{slot.maxClass}
-
{classRating} {translate(m.grp == 'bh' ? m.grp : m.name || m.grp)}
+
{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? : null }
{formats.round(mass)}{units.T}
- { m.grp == 'bh' && m.name ?
{translate(m.name)}
: null }
{ m.getOptimalMass() ?
{translate('optimal mass')}: {formats.int(m.getOptimalMass())}{units.T}
: null }
{ m.getMaxMass() ?
{translate('max mass')}: {formats.int(m.getMaxMass())}{units.T}
: null }
{ m.getRange() ?
{translate('range')}: {formats.f2(m.getRange())}{units.km}
: null }
@@ -94,6 +95,9 @@ export default class StandardSlot extends TranslatedComponent {
{ m.getWeaponsCapacity() ?
{translate('WEP')}: {formats.f1(m.getWeaponsCapacity())}{units.MJ} / {formats.f1(m.getWeaponsRechargeRate())}{units.MW}
: null }
{ m.getSystemsCapacity() ?
{translate('SYS')}: {formats.f1(m.getSystemsCapacity())}{units.MJ} / {formats.f1(m.getSystemsRechargeRate())}{units.MW}
: null }
{ m.getEnginesCapacity() ?
{translate('ENG')}: {formats.f1(m.getEnginesCapacity())}{units.MJ} / {formats.f1(m.getEnginesRechargeRate())}{units.MW}
: null }
+ { showModuleResistances && m.getExplosiveResistance() ?
{translate('explres')}: {formats.pct(m.getExplosiveResistance())}
: null }
+ { showModuleResistances && m.getKineticResistance() ?
{translate('kinres')}: {formats.pct(m.getKineticResistance())}
: null }
+ { showModuleResistances && m.getThermalResistance() ?
{translate('thermres')}: {formats.pct(m.getThermalResistance())}
: null }
{ validMods.length > 0 ?
: null }
diff --git a/src/app/components/StandardSlotSection.jsx b/src/app/components/StandardSlotSection.jsx
index 48e53656..e319f524 100644
--- a/src/app/components/StandardSlotSection.jsx
+++ b/src/app/components/StandardSlotSection.jsx
@@ -2,8 +2,8 @@ import React from 'react';
import cn from 'classnames';
import SlotSection from './SlotSection';
import StandardSlot from './StandardSlot';
+import Module from '../shipyard/Module';
import { diffDetails } from '../utils/SlotFunctions';
-import * as ModuleUtils from '../shipyard/ModuleUtils';
import * as ShipRoles from '../shipyard/ShipRoles';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
@@ -114,7 +114,7 @@ export default class StandardSlotSection extends SlotSection {
selected={currentMenu == st[0]}
onChange={this.props.onChange}
ship={ship}
- warning={m => m.pgen < ship.powerRetracted}
+ warning={m => m instanceof Module ? m.getPowerGeneration() < ship.powerRetracted : m.pgen < ship.powerRetracted}
/>;
slots[2] =
m.maxmass < (ship.ladenMass - st[1].mass + m.mass)}
+ warning={m => m instanceof Module ? m.getMaxMass() < (ship.ladenMass - st[1].mass + m.mass) : m.maxmass < (ship.ladenMass - st[1].mass + m.mass)}
/>;
@@ -161,7 +161,7 @@ export default class StandardSlotSection extends SlotSection {
selected={currentMenu == st[4]}
onChange={this.props.onChange}
ship={ship}
- warning= {m => m.engcap < ship.boostEnergy}
+ warning={m => m instanceof Module ? m.getEnginesCapacity() < ship.boostEnergy : m.engcap < ship.boostEnergy}
/>;
slots[6] =
+ {this.svg()}
+
+ );
+ }
+
/**
* Generate the SVG
* @return {React.Component} SVG Contents
@@ -507,6 +519,40 @@ export class ListModifications extends SvgIcon {
}
}
+/**
+ * Modified (engineers)
+ */
+export class Modified extends SvgIcon {
+ /**
+ * Overriden view box
+ * @return {String} view box
+ */
+ viewBox() { return '0 0 200 200'; }
+
+ /**
+ * Render the Icon
+ * @return {React.Component} SVG Icon
+ */
+ render() {
+ return (
+
+ {this.svg()}
+
+ );
+ }
+
+ /**
+ * Generate the SVG
+ * @return {React.Component} SVG Contents
+ */
+ svg() {
+ return
+
+
+ ;
+ }
+}
+
/**
* Hammer
*/
diff --git a/src/app/i18n/Language.jsx b/src/app/i18n/Language.jsx
index da82304f..5a6a5ee4 100644
--- a/src/app/i18n/Language.jsx
+++ b/src/app/i18n/Language.jsx
@@ -5,6 +5,7 @@ import * as ES from './es';
import * as FR from './fr';
import * as IT from './it';
import * as RU from './ru';
+import * as PL from './pl';
import d3 from 'd3';
let fallbackTerms = EN.terms;
@@ -23,6 +24,7 @@ export function getLanguage(langCode) {
case 'fr': lang = FR; break;
case 'it': lang = IT; break;
case 'ru': lang = RU; break;
+ case 'pl': lang = PL; break;
default:
lang = EN;
}
@@ -82,5 +84,6 @@ export const Languages = {
it: 'Italiano',
es: 'Español',
fr: 'Français',
- ru: 'ру́сский'
+ ru: 'ру́сский',
+ pl: 'polski'
};
diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js
index 6276f0d1..b8b7f0ff 100644
--- a/src/app/i18n/en.js
+++ b/src/app/i18n/en.js
@@ -29,6 +29,8 @@ export const terms = {
PHRASE_UNLADEN: 'Ship mass excluding fuel and cargo',
PHRASE_UPDATE_RDY: 'Update Available! Click to refresh',
+ HELP_MODIFICATIONS_MENU: 'Click on a number to enter a new value, or drag along the bar for small changes',
+
// Other languages fallback to these values
// Only Translate to other languages if the name is different in-game
am: 'Auto Field-Maintenance Unit',
@@ -92,14 +94,18 @@ export const terms = {
// Unit for seconds
secs: 's',
- // Hardpoint abbreviations
+ // Weapon, offence and defence
dpe: 'Damage per MJ of energy',
dps: 'Damage per second',
+ sdps: 'Sustained damage per second',
dpssdps: 'Damage per second (sustained damage per second)',
eps: 'Energy per second',
epsseps: 'Energy per second (sustained energy per second)',
hps: 'Heat per second',
hpsshps: 'Heat per second (sustained heat per second)',
+ 'damage by': 'Damage by',
+ 'damage from': 'Damage from',
+ 'shield cells': 'Shield cells',
// Modifications
ammo: 'Ammunition maximum',
@@ -134,6 +140,7 @@ export const terms = {
rof: 'Rate of fire',
shield: 'Shield',
shieldboost: 'Shield boost',
+ shieldreinforcement: 'Shield reinforcement',
spinup: 'Spin up time',
syscap: 'Systems capacity',
sysrate: 'Systems recharge rate',
diff --git a/src/app/i18n/pl.js b/src/app/i18n/pl.js
new file mode 100644
index 00000000..8444532e
--- /dev/null
+++ b/src/app/i18n/pl.js
@@ -0,0 +1,77 @@
+export const formats = {
+ decimal: '.',
+ thousands: ',',
+ grouping: [3],
+ currency: ['$', ''],
+ dateTime: '%a %b %e %X %Y',
+ date: '%m/%d/%Y',
+ time: '%H:%M:%S',
+ periods: ['AM', 'PM'],
+ days: ['Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota'],
+ shortDays: ['Nie', 'Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob'],
+ months: ['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'],
+ shortMonths: ['Sty', 'Lut', 'Mar', 'Kwi', 'Maj', 'Cze', 'Lip', 'Sie', 'Wrz', 'Paź', 'Lis', 'Gru']
+};
+
+export const terms = {
+ PHRASE_ALT_ALL: 'Alt + kliknięcie by wypełnić wszystkie sloty',
+ PHRASE_BACKUP_DESC: 'Kopia zapasowa wszystkich danych Coriolis w celu zapisu lub przeniesienia na inne urządzenie/przeglądarkę',
+ PHRASE_CONFIRMATION: 'Czy jesteś pewien?',
+ PHRASE_EXPORT_DESC: 'Szczegółowy eksport schematu w formacie JSON w celu użycia na innych stronach i narzędziach',
+ PHRASE_FASTEST_RANGE: 'Maksymalna ilość skoków na najwyższym zasięgu',
+ PHRASE_IMPORT: 'Wklej tu JSON lub importuj',
+ PHRASE_LADEN: 'Masa statku + paliwo + ładunek',
+ PHRASE_NO_BUILDS: 'Nie dodano schematu do porównania!',
+ PHRASE_NO_RETROCH: 'Brak zmian retrofit',
+ PHRASE_SELECT_BUILDS: 'Wybierz schematy do porównania',
+ PHRASE_SG_RECHARGE: 'Czas od 50% do 100% naładowania',
+ PHRASE_SG_RECOVER: 'Odnowienie (do 50%) po upadku',
+ PHRASE_UNLADEN: 'Masa statku z wyłączeniem paliwa i ładunku',
+ PHRASE_UPDATE_RDY: 'Dostępna aktualizacja! Naciśnij by odświeżyć',
+
+ // Other languages fallback to these values
+ // Only Translate to other languages if the name is different in-game
+ am: 'Auto Field-Maintenance Unit',
+ bh: 'Bulkheads',
+ bl: 'Beam Laser',
+ bsg: 'Bi-Weave Shield Generator',
+ c: 'Cannon',
+ cc: 'Collector Limpet Controller',
+ cm: 'Countermeasure',
+ cr: 'Cargo Rack',
+ cs: 'Cargo Scanner',
+ dc: 'Docking Computer',
+ fc: 'Fragment Cannon',
+ fi: 'FSD Interdictor',
+ fs: 'Fuel Scoop',
+ fsd: 'Frame Shift Drive',
+ ft: 'Fuel Tank',
+ fx: 'Fuel Transfer Limpet Controller',
+ hb: 'Hatch Breaker Limpet Controller',
+ hr: 'Hull Reinforcement Package',
+ kw: 'Kill Warrant Scanner',
+ ls: 'Life Support',
+ mc: 'Multi-cannon',
+ ml: 'Mining Laser',
+ mr: 'Missile Rack',
+ nl: 'Mine Launcher',
+ pa: 'Plasma Accelerator',
+ pas: 'Planetary Approach Suite',
+ pc: 'Prospector Limpet Controller',
+ pd: 'power distributor',
+ pl: 'Pulse Laser',
+ pp: 'Power Plant',
+ psg: 'Prismatic Shield Generator',
+ pv: 'Planetary Vehicle Hangar',
+ rf: 'Refinery',
+ rg: 'Rail Gun',
+ s: 'Sensors',
+ sb: 'Shield Booster',
+ sc: 'Scanner',
+ scb: 'Shield Cell Bank',
+ sg: 'Shield Generator',
+ t: 'thrusters',
+ tp: 'Torpedo Pylon',
+ ul: 'Burst Laser',
+ ws: 'Frame Shift Wake Scanner'
+};
diff --git a/src/app/pages/ComparisonPage.jsx b/src/app/pages/ComparisonPage.jsx
index 5531525f..b258247e 100644
--- a/src/app/pages/ComparisonPage.jsx
+++ b/src/app/pages/ComparisonPage.jsx
@@ -345,7 +345,7 @@ export default class ComparisonPage extends Page {
let code = fromComparison(name, builds, selectedFacets, predicate, desc);
let loc = window.location;
- return `${loc.protocol}//${loc.host}/comparison/${code}`;
+ return loc.protocol + '//' + loc.host + '/comparison?code=' + encodeURIComponent(code);
}
/**
diff --git a/src/app/pages/ErrorDetails.jsx b/src/app/pages/ErrorDetails.jsx
index 61be46ad..da100d3b 100644
--- a/src/app/pages/ErrorDetails.jsx
+++ b/src/app/pages/ErrorDetails.jsx
@@ -26,7 +26,7 @@ export default class ErrorDetails extends React.Component {
if (ed) {
content =
diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx
index 839c6b00..6dd3ba3a 100644
--- a/src/app/pages/OutfittingPage.jsx
+++ b/src/app/pages/OutfittingPage.jsx
@@ -8,13 +8,14 @@ import Persist from '../stores/Persist';
import Ship from '../shipyard/Ship';
import { toDetailedBuild } from '../shipyard/Serializer';
import { outfitURL } from '../utils/UrlGenerators';
-
import { FloppyDisk, Bin, Switch, Download, Reload, Fuel } from '../components/SvgIcons';
import ShipSummaryTable from '../components/ShipSummaryTable';
import StandardSlotSection from '../components/StandardSlotSection';
import HardpointsSlotSection from '../components/HardpointsSlotSection';
import InternalSlotSection from '../components/InternalSlotSection';
import UtilitySlotSection from '../components/UtilitySlotSection';
+import OffenceSummary from '../components/OffenceSummary';
+import DefenceSummary from '../components/DefenceSummary';
import LineChart from '../components/LineChart';
import PowerManagement from '../components/PowerManagement';
import CostSection from '../components/CostSection';
@@ -288,12 +289,14 @@ export default class OutfittingPage extends Page {
sStr = ship.getStandardString() + '.' + ship.getModificationsString(),
iStr = ship.getInternalString() + '.' + ship.getModificationsString();
+ Router.replace(outfitURL(ship.id, code, buildName));
+
return (
{ship.name}
-
+
@@ -324,31 +327,11 @@ export default class OutfittingPage extends Page {
-
{translate('jump range')}
-
+
-
{translate('total range')}
-
+
@@ -397,3 +380,16 @@ export default class OutfittingPage extends Page {
);
}
}
+//
+//
{translate('jump range')}
+//
+//
diff --git a/src/app/pages/ShipyardPage.jsx b/src/app/pages/ShipyardPage.jsx
index fe6bcae6..f794eb10 100644
--- a/src/app/pages/ShipyardPage.jsx
+++ b/src/app/pages/ShipyardPage.jsx
@@ -55,6 +55,7 @@ function shipSummary(shipId, shipData) {
ship.optimizeMass({ th: ship.standard[1].maxClass + 'A', fsd: '2D', ft: '1C' }); // Optmize mass with Max Thrusters
summary.topSpeed = ship.topSpeed;
summary.topBoost = ship.topBoost;
+ summary.baseArmour = ship.armour;
return summary;
}
@@ -141,7 +142,7 @@ export default class ShipyardPage extends Page {
{s.agility}
{fInt(s.speed)}{u['m/s']}
{fInt(s.boost)}{u['m/s']}
-
{s.baseArmour}
+
{fInt(s.baseArmour)}
{fInt(s.baseShieldStrength)}{u.MJ}
{fInt(s.topSpeed)}{u['m/s']}
{fInt(s.topBoost)}{u['m/s']}
diff --git a/src/app/shipyard/Constants.js b/src/app/shipyard/Constants.js
index 566bcf06..e8714216 100755
--- a/src/app/shipyard/Constants.js
+++ b/src/app/shipyard/Constants.js
@@ -51,15 +51,19 @@ export const ModuleGroupToName = {
bl: 'Beam Laser',
ul: 'Burst Laser',
c: 'Cannon',
+ ch: 'Chaff Launcher',
cs: 'Cargo Scanner',
cm: 'Countermeasure',
+ ec: 'Electronic Countermeasure',
fc: 'Fragment Cannon',
+ hs: 'Heat Sink Launcher',
ws: 'Frame Shift Wake Scanner',
kw: 'Kill Warrant Scanner',
nl: 'Mine Launcher',
ml: 'Mining Laser',
mr: 'Missile Rack',
pa: 'Plasma Accelerator',
+ po: 'Point Defence',
mc: 'Multi-cannon',
pl: 'Pulse Laser',
rg: 'Rail Gun',
@@ -121,7 +125,7 @@ export const ShipFacets = [
},
{ // 3
title: 'shields',
- props: ['shieldStrength'],
+ props: ['shield'],
unit: 'MJ',
fmt: 'int',
i: 3
diff --git a/src/app/shipyard/Module.js b/src/app/shipyard/Module.js
index d367cc9a..d995ef16 100755
--- a/src/app/shipyard/Module.js
+++ b/src/app/shipyard/Module.js
@@ -11,7 +11,7 @@ export default class Module {
* @param {Object} params Module parameters. Either grp/id or template
*/
constructor(params) {
- let properties = Object.assign({ grp: null, id: null, template: null }, params);
+ let properties = Object.assign({ grp: null, id: null, template: null, }, params);
let template;
if (properties.template == undefined) {
@@ -23,48 +23,65 @@ export default class Module {
for (let p in template) { this[p] = template[p]; }
}
}
- this.mods = {};
}
/**
* Get a value for a given modification
* @param {Number} name The name of the modification
- * @return {Number} The value of the modification, as a decimal value where 1 is 100%
+ * @return {Number} The value of the modification, as an integer value scaled so that 1.23% == 123
*/
getModValue(name) {
- return this.mods && this.mods[name] ? this.mods[name] / 10000 : null;
+ return this.mods && this.mods[name] ? this.mods[name] : null;
}
/**
* Set a value for a given modification ID
* @param {Number} name The name of the modification
- * @param {Number} value The value of the modification, as a decimal value where 1 is 100%
+ * @param {Number} value The value of the modification, as an integer scaled so that -2.34% == -234
*/
setModValue(name, value) {
+ if (!this.mods) {
+ this.mods = {};
+ }
+
if (value == null || value == 0) {
delete this.mods[name];
} else {
- // Store value with 2dp
- this.mods[name] = Math.round(value * 10000);
+ // Round just to be sure
+ this.mods[name] = Math.round(value);
}
}
/**
* Helper to obtain a modified value using standard multipliers
- * @param {String} name the name of the modifier to obtain
- * @return {Number} the mass of this module
- */
- _getModifiedValue(name) {
- let result = 0;
- if (this[name]) {
- result = this[name];
- if (result) {
- let mult = this.getModValue(name);
- if (mult) { result = result * (1 + mult); }
+ * @param {String} name the name of the modifier to obtain
+ * @param {Boolean} additive Optional true if the value is additive rather than multiplicative
+ * @return {Number} the mass of this module
+ */
+ _getModifiedValue(name, additive) {
+ let result = this[name] || (additive ? 0 : null); // Additive NULL === 0
+ if (result != null) {
+ // Jitter is special, being the only non-percentage value (it is in fact degrees)
+ const modValue = name === 'jitter' ? this.getModValue(name) / 100 : this.getModValue(name) / 10000;
+ if (modValue) {
+ if (additive) {
+ result = result + modValue;
+ } else {
+ result = result * (1 + modValue);
+ }
}
}
return result;
}
+
+ /**
+ * Return true if this is a shield generator
+ * @return {Boolean} if this is a shield generator
+ */
+ isShieldGenerator() {
+ return (this.grp === 'sg' || this.grp === 'psg' || this.grp === 'bsg');
+ }
+
/**
* Get the power generation of this module, taking in to account modifications
* @return {Number} the power generation of this module
@@ -190,7 +207,7 @@ export default class Module {
* @return {Number} the kinetic resistance of this module
*/
getKineticResistance() {
- return this._getModifiedValue('kinres');
+ return this._getModifiedValue('kinres', true);
}
/**
@@ -198,7 +215,7 @@ export default class Module {
* @return {Number} the thermal resistance of this module
*/
getThermalResistance() {
- return this._getModifiedValue('thermres');
+ return this._getModifiedValue('thermres', true);
}
/**
@@ -206,7 +223,7 @@ export default class Module {
* @return {Number} the explosive resistance of this module
*/
getExplosiveResistance() {
- return this._getModifiedValue('explres');
+ return this._getModifiedValue('explres', true);
}
/**
@@ -291,7 +308,7 @@ export default class Module {
if (this['minmass']) {
result = this['minmass'];
if (result) {
- let mult = this.getModValue('optmass');
+ let mult = this.getModValue('optmass') / 10000;
if (mult) { result = result * (1 + mult); }
}
}
@@ -316,7 +333,7 @@ export default class Module {
if (this['maxmass']) {
result = this['maxmass'];
if (result) {
- let mult = this.getModValue('optmass');
+ let mult = this.getModValue('optmass') / 10000;
if (mult) { result = result * (1 + mult); }
}
}
@@ -333,7 +350,7 @@ export default class Module {
if (this['minmul']) {
result = this['minmul'];
if (result) {
- let mult = this.getModValue('optmul');
+ let mult = this.getModValue('optmul') / 10000;
if (mult) { result = result * (1 + mult); }
}
}
@@ -358,7 +375,7 @@ export default class Module {
if (this['maxmul']) {
result = this['maxmul'];
if (result) {
- let mult = this.getModValue('optmul');
+ let mult = this.getModValue('optmul') / 10000;
if (mult) { result = result * (1 + mult); }
}
}
@@ -484,4 +501,44 @@ export default class Module {
return this._getModifiedValue('hullboost');
}
+ /**
+ * Get the shield reinforcement for this module, taking in to account modifications
+ * @return {Number} the shield reinforcement for this module
+ */
+ getShieldReinforcement() {
+ return this._getModifiedValue('shieldreinforcement');
+ }
+
+ /**
+ * Get the bays for this module, taking in to account modifications
+ * @return {Number} the bays for this module
+ */
+ getBays() {
+ return this._getModifiedValue('bays');
+ }
+
+ /**
+ * Get the rebuilds per bay for this module, taking in to account modifications
+ * @return {Number} the rebuilds per bay for this module
+ */
+ getRebuildsPerBay() {
+ return this._getModifiedValue('rebuildsperbay');
+ }
+
+ /**
+ * Get the cells for this module, taking in to account modifications
+ * @return {Number} the cells for this module
+ */
+ getCells() {
+ return this._getModifiedValue('cells');
+ }
+
+
+ /**
+ * Get the jitter for this module, taking in to account modifications
+ * @return {Number} the jitter for this module
+ */
+ getJitter() {
+ return this._getModifiedValue('jitter', true);
+ }
}
diff --git a/src/app/shipyard/ModuleUtils.js b/src/app/shipyard/ModuleUtils.js
index dc0b95e6..4949fd85 100755
--- a/src/app/shipyard/ModuleUtils.js
+++ b/src/app/shipyard/ModuleUtils.js
@@ -212,6 +212,16 @@ export function findHardpoint(groupName, clss, rating, name, mount, missile) {
*/
export function findHardpointId(groupName, clss, rating, name, mount, missile) {
let h = this.findHardpoint(groupName, clss, rating, name, mount, missile);
+ if (h) {
+ return h.id;
+ }
+
+ // Countermeasures used to be lumped in a single group but have been broken, out. If we have been given a groupName of 'Countermeasure' then
+ // rely on the unique name to find it
+ if (groupName === 'cm' || groupName === 'Countermeasure') {
+ h = this.findHardpoint(null, clss, rating, name, mount, missile);
+ }
+
return h ? h.id : 0;
}
diff --git a/src/app/shipyard/Serializer.js b/src/app/shipyard/Serializer.js
index 1fb3e477..df2a1a7a 100644
--- a/src/app/shipyard/Serializer.js
+++ b/src/app/shipyard/Serializer.js
@@ -2,7 +2,9 @@ import { ModuleGroupToName, MountMap, BulkheadNames } from './Constants';
import { Ships } from 'coriolis-data/dist';
import Ship from './Ship';
import * as ModuleUtils from './ModuleUtils';
+import * as Utils from '../utils/UtilityFunctions';
import LZString from 'lz-string';
+import { outfitURL } from '../utils/UrlGenerators';
const STANDARD = ['powerPlant', 'thrusters', 'frameShiftDrive', 'lifeSupport', 'powerDistributor', 'sensors', 'fuelTank'];
@@ -83,7 +85,7 @@ export function toDetailedBuild(buildName, ship) {
ship: ship.name,
references: [{
name: 'Coriolis.io',
- url: `https://coriolis.edcd.io/outfit/${ship.id}/${code}?bn=${encodeURIComponent(buildName)}`,
+ url: 'https://coriolis.edcd.io' + outfitURL(ship.id, code, buildName),
code,
shipId: ship.id
}],
@@ -115,32 +117,12 @@ export function toDetailedBuild(buildName, ship) {
return data;
};
-/**
- * Instantiates a ship from a ship-loadout object, using the code
- * @param {Object} detailedBuild ship-loadout object
- * @return {Ship} Ship instance
- */
-export function fromDetailedBuild(detailedBuild) {
- let shipId = Object.keys(Ships).find((shipId) => Ships[shipId].properties.name.toLowerCase() == detailedBuild.ship.toLowerCase());
-
- if (!shipId) {
- throw 'No such ship: ' + detailedBuild.ship;
- }
-
- let comps = detailedBuild.components;
- let stn = comps.standard;
- let shipData = Ships[shipId];
- let ship = new Ship(shipId, shipData.properties, shipData.slots);
-
- return ship.buildFrom(detailedBuild.references[0].code);
-};
-
/**
* Instantiates a ship from a ship-loadout object
* @param {Object} detailedBuild ship-loadout object
* @return {Ship} Ship instance
*/
-export function oldfromDetailedBuild(detailedBuild) {
+export function fromDetailedBuild(detailedBuild) {
let shipId = Object.keys(Ships).find((shipId) => Ships[shipId].properties.name.toLowerCase() == detailedBuild.ship.toLowerCase());
if (!shipId) {
@@ -154,6 +136,7 @@ export function oldfromDetailedBuild(detailedBuild) {
let shipData = Ships[shipId];
let ship = new Ship(shipId, shipData.properties, shipData.slots);
let bulkheads = ModuleUtils.bulkheadIndex(stn.bulkheads);
+ let modifications = new Array(stn.bulkheads.modifications);
if (bulkheads < 0) {
throw 'Invalid bulkheads: ' + stn.bulkheads;
@@ -165,6 +148,7 @@ export function oldfromDetailedBuild(detailedBuild) {
}
priorities.push(stn[c].priority === undefined ? 0 : stn[c].priority - 1);
enabled.push(stn[c].enabled === undefined ? true : stn[c].enabled);
+ modifications.push(stn[c].modifications);
return stn[c].class + stn[c].rating;
});
@@ -185,8 +169,13 @@ export function oldfromDetailedBuild(detailedBuild) {
comps.utility.map(c => (!c || c.enabled === undefined) ? true : c.enabled * 1),
comps.internal.map(c => (!c || c.enabled === undefined) ? true : c.enabled * 1)
);
+ modifications = modifications.concat(
+ comps.hardpoints.map(c => (c && c.m ? c.m.modifications : null)),
+ comps.utility.map(c => (c && c.m ? c.m.modifications : null)),
+ comps.internal.map(c => (c && c.m ? c.m.modifications : null))
+ );
- ship.buildWith({ bulkheads, standard, hardpoints, internal }, priorities, enabled);
+ ship.buildWith({ bulkheads, standard, hardpoints, internal }, priorities, enabled, modifications);
return ship;
};
@@ -228,7 +217,7 @@ export function fromComparison(name, builds, facets, predicate, desc) {
f: facets,
p: predicate,
d: desc ? 1 : 0
- })).replace(/\//g, '-');
+ }));
};
/**
@@ -237,5 +226,5 @@ export function fromComparison(name, builds, facets, predicate, desc) {
* @return {Object} Comparison data object
*/
export function toComparison(code) {
- return JSON.parse(LZString.decompressFromBase64(code.replace(/-/g, '/')));
+ return JSON.parse(LZString.decompressFromBase64(Utils.fromUrlSafe(code)));
};
diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js
index 25e35110..46f77500 100755
--- a/src/app/shipyard/Ship.js
+++ b/src/app/shipyard/Ship.js
@@ -1,5 +1,6 @@
import * as Calc from './Calculations';
import * as ModuleUtils from './ModuleUtils';
+import * as Utils from '../utils/UtilityFunctions';
import Module from './Module';
import LZString from 'lz-string';
import isEqual from 'lodash/lang';
@@ -126,7 +127,6 @@ export default class Ship {
*/
canBoost() {
return this.canThrust() && // Thrusters operational
- this.getSlotStatus(this.standard[4]) == 3 && // Power distributor operational
this.boostEnergy <= this.standard[4].m.getEnginesCapacity(); // PD capacitor is sufficient for boost
}
@@ -194,10 +194,12 @@ export default class Ship {
*/
calcShieldRecovery() {
if (this.shield > 0) {
- let sgSlot = this.findInternalByGroup('sg');
- let brokenRegenRate = 1 + sgSlot.m.getModValue('brokenregen');
- // 50% of shield strength / recovery recharge rate + 15 second delay before recharge starts
- return ((this.shield / 2) / (sgSlot.m.recover * brokenRegenRate)) + 15;
+ const sgSlot = this.findInternalByGroup('sg');
+ if (sgSlot != null) {
+ let brokenRegenRate = 1 + sgSlot.m.getModValue('brokenregen') / 10000;
+ // 50% of shield strength / recovery recharge rate + 15 second delay before recharge starts
+ return ((this.shield / 2) / (sgSlot.m.recover * brokenRegenRate)) + 15;
+ }
}
return 0;
}
@@ -210,10 +212,12 @@ export default class Ship {
*/
calcShieldRecharge() {
if (this.shield > 0) {
- let sgSlot = this.findInternalByGroup('sg');
- let regenRate = 1 + sgSlot.m.getModValue('regen');
- // 50% -> 100% recharge time, Bi-Weave shields charge at 1.8 MJ/s
- return (this.shield / 2) / ((sgSlot.m.grp == 'bsg' ? 1.8 : 1) * regenRate);
+ const sgSlot = this.findInternalByGroup('sg');
+ if (sgSlot != null) {
+ let regenRate = 1 + sgSlot.m.getModValue('regen') / 10000;
+ // 50% -> 100% recharge time, Bi-Weave shields charge at 1.8 MJ/s
+ return (this.shield / 2) / ((sgSlot.m.grp == 'bsg' ? 1.8 : 1) * regenRate);
+ }
}
return 0;
}
@@ -233,9 +237,8 @@ export default class Ship {
sg = sgSlot.m;
}
- // TODO obtain shield boost
- // return Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sg, this.shieldMultiplier + (multiplierDelta || 0));
- return Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sg, 0 + (multiplierDelta || 0));
+ // TODO Not accurate if the ship has modified shield boosters
+ return Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sg, 1 + (multiplierDelta || 0));
}
/**
@@ -397,19 +400,24 @@ export default class Ship {
* Set a modification value
* @param {Object} m The module to change
* @param {Object} name The name of the modification to change
- * @param {Number} value The new value of the modification
+ * @param {Number} value The new value of the modification. The value of the modification is scaled to provide two decimal places of precision in an integer. For example 1.23% is stored as 123
*/
setModification(m, name, value) {
+ if (isNaN(value)) {
+ // Value passed is invalid; reset it to 0
+ value = 0;
+ }
+
// Handle special cases
- if (name == 'pgen') {
+ if (name === 'pgen') {
// Power generation
m.setModValue(name, value);
this.updatePowerGenerated();
- } else if (name == 'power') {
+ } else if (name === 'power') {
// Power usage
m.setModValue(name, value);
this.updatePowerUsed();
- } else if (name == 'mass') {
+ } else if (name === 'mass') {
// Mass
let oldMass = m.getMass();
m.setModValue(name, value);
@@ -418,25 +426,40 @@ export default class Ship {
this.ladenMass = this.ladenMass - oldMass + newMass;
this.updateTopSpeed();
this.updateJumpStats();
- } else if (name == 'maxfuel') {
+ } else if (name === 'maxfuel') {
m.setModValue(name, value);
this.updateJumpStats();
- } else if (name == 'optmass') {
+ } else if (name === 'optmass') {
m.setModValue(name, value);
- // Could be for either thrusters or FSD
+ // Could be for any of thrusters, FSD or shield
this.updateTopSpeed();
this.updateJumpStats();
- } else if (name == 'optmul') {
+ this.recalculateShield();
+ } else if (name === 'optmul') {
m.setModValue(name, value);
- // Could be for either thrusters or FSD
+ // Could be for any of thrusters, FSD or shield
this.updateTopSpeed();
this.updateJumpStats();
- } else if (name == 'shieldboost') {
+ this.recalculateShield();
+ } else if (name === 'shieldboost') {
+ m.setModValue(name, value);
+ this.recalculateShield();
+ } else if (name === 'hullboost' || name === 'hullreinforcement') {
m.setModValue(name, value);
- this.updateShield();
- } else if (name == 'hullboost') {
+ this.recalculateArmour();
+ } else if (name === 'shieldreinforcement') {
m.setModValue(name, value);
- this.updateArmour();
+ this.recalculateShieldCells();
+ } else if (name === 'burst' || name === 'clip' || name === 'damage' || name === 'distdraw' || name === 'jitter' || name === 'piercing' || name === 'range' || name === 'reload' || name === 'rof' || name === 'thermload') {
+ m.setModValue(name, value);
+ this.recalculateDps();
+ this.recalculateHps();
+ this.recalculateEps();
+ } else if (name === 'explres' || name === 'kinres' || name === 'thermres') {
+ m.setModValue(name, value);
+ // Could be for shields or armour
+ this.recalculateArmour();
+ this.recalculateShield();
} else {
// Generic
m.setModValue(name, value);
@@ -464,9 +487,21 @@ export default class Ship {
this.ladenMass = 0;
this.armour = this.baseArmour;
this.shield = this.baseShieldStrength;
+ this.shieldCells = 0;
this.totalCost = this.m.incCost ? this.m.discountedCost : 0;
this.unladenMass = this.hullMass;
+ this.totalDpe = 0;
+ this.totalExplDpe = 0;
+ this.totalKinDpe = 0;
+ this.totalThermDpe = 0;
this.totalDps = 0;
+ this.totalExplDps = 0;
+ this.totalKinDps = 0;
+ this.totalThermDps = 0;
+ this.totalSDps = 0;
+ this.totalExplSDps = 0;
+ this.totalKinSDps = 0;
+ this.totalThermSDps = 0;
this.totalEps = 0;
this.totalHps = 0;
this.shieldExplRes = 0;
@@ -481,7 +516,6 @@ export default class Ship {
this.bulkheads.m.mods = mods && mods[0] ? mods[0] : {};
this.cargoHatch.priority = priorities ? priorities[0] * 1 : 0;
this.cargoHatch.enabled = enabled ? enabled[0] * 1 : true;
- this.cargoHatch.mods = mods ? mods[0] : {};
for (i = 0; i < cl; i++) {
standard[i].cat = 0;
@@ -538,8 +572,12 @@ export default class Ship {
this.updatePowerGenerated()
.updatePowerUsed()
.updateJumpStats()
- .updateShield()
- .updateArmour()
+ .recalculateShield()
+ .recalculateShieldCells()
+ .recalculateArmour()
+ .recalculateDps()
+ .recalculateEps()
+ .recalculateHps()
.updateTopSpeed();
}
@@ -557,27 +595,27 @@ export default class Ship {
let standard = new Array(this.standard.length),
hardpoints = new Array(this.hardpoints.length),
internal = new Array(this.internal.length),
- mods = new Array(1 + this.standard.length + this.hardpoints.length + this.internal.length),
+ modifications = new Array(1 + this.standard.length + this.hardpoints.length + this.internal.length),
parts = serializedString.split('.'),
priorities = null,
enabled = null,
code = parts[0];
if (parts[1]) {
- enabled = LZString.decompressFromBase64(parts[1].replace(/-/g, '/')).split('');
+ enabled = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[1])).split('');
}
if (parts[2]) {
- priorities = LZString.decompressFromBase64(parts[2].replace(/-/g, '/')).split('');
+ priorities = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[2])).split('');
}
if (parts[3]) {
- const modstr = parts[3].replace(/-/g, '/');
+ const modstr = parts[3];
if (modstr.match(':')) {
- this.decodeModificationsString(modstr, mods);
+ this.decodeModificationsString(modstr, modifications);
} else {
try {
- this.decodeModificationsStruct(zlib.gunzipSync(new Buffer(modstr, 'base64')), mods);
+ this.decodeModificationsStruct(zlib.gunzipSync(new Buffer(Utils.fromUrlSafe(modstr), 'base64')), modifications);
} catch (err) {
// Could be out-of-date URL; ignore
}
@@ -595,7 +633,7 @@ export default class Ship {
},
priorities,
enabled,
- mods
+ modifications
);
};
@@ -681,21 +719,27 @@ export default class Ship {
if (slot.enabled != enabled) { // Enabled state is changing
slot.enabled = enabled;
if (slot.m) {
- if (ModuleUtils.isShieldGenerator(slot.m.grp) || slot.m.grp == 'sb') {
- this.updateShield();
+ if (ModuleUtils.isShieldGenerator(slot.m.grp) || slot.m.grp === 'sb') {
+ this.recalculateShield();
}
- if (slot.m.getDps()) {
- this.totalDps += slot.m.getDps() * (enabled ? 1 : -1);
+ if (slot.m.grp === 'scb') {
+ this.recalculateShieldCells();
}
- if (slot.m.getEps()) {
- this.totalEps += slot.m.getEps() * (enabled ? 1 : -1);
+
+ this.updatePowerUsed();
+ this.updatePowerEnabledString();
+
+ if (slot.m.getDps()) {
+ this.recalculateDps();
}
+
if (slot.m.getHps()) {
- this.totalHps += slot.m.getHps() * (enabled ? 1 : -1);
+ this.recalculateHps();
}
- this.updatePowerUsed();
- this.updatePowerEnabledString();
+ if (slot.m.getEps()) {
+ this.recalculateEps();
+ }
}
}
return this;
@@ -732,10 +776,15 @@ export default class Ship {
updateStats(slot, n, old, preventUpdate) {
let powerGeneratedChange = slot == this.standard[0];
let powerUsedChange = false;
+ let dpsChanged = n && n.getDps() || old && old.getDps();
+ let epsChanged = n && n.getEps() || old && old.getEps();
+ let hpsChanged = n && n.getHps() || old && old.getHps();
- let armourChange = (slot == this.bulkheads) || (n && n.grp == 'hr') || (old && old.grp == 'hr');
+ let armourChange = (slot === this.bulkheads) || (n && n.grp === 'hr') || (old && old.grp === 'hr');
- let shieldChange = (n && n.grp == 'bsg') || (old && old.grp == 'bsg') || (n && n.grp == 'psg') || (old && old.grp == 'psg') || (n && n.grp == 'sg') || (old && old.grp == 'sg') || (n && n.grp == 'sb') || (old && old.grp == 'sb');
+ let shieldChange = (n && n.grp === 'bsg') || (old && old.grp === 'bsg') || (n && n.grp === 'psg') || (old && old.grp === 'psg') || (n && n.grp === 'sg') || (old && old.grp === 'sg') || (n && n.grp === 'sb') || (old && old.grp === 'sb');
+
+ let shieldCellsChange = (n && n.grp === 'scb') || (old && old.grp === 'scb');
if (old) { // Old modul now being removed
switch (old.grp) {
@@ -755,16 +804,6 @@ export default class Ship {
powerUsedChange = true;
}
- if (old.getDps()) {
- this.totalDps -= old.getDps();
- }
- if (old.getEps()) {
- this.totalEps -= old.getEps();
- }
- if (old.getHps()) {
- this.totalHps -= old.getHps();
- }
-
this.unladenMass -= old.getMass() || 0;
}
@@ -786,22 +825,21 @@ export default class Ship {
powerUsedChange = true;
}
- if (n.getDps()) {
- this.totalDps += n.getDps();
- }
- if (n.getEps()) {
- this.totalEps += n.getEps();
- }
- if (n.getHps()) {
- this.totalHps += n.getHps();
- }
-
this.unladenMass += n.getMass() || 0;
}
this.ladenMass = this.unladenMass + this.cargoCapacity + this.fuelCapacity;
if (!preventUpdate) {
+ if (dpsChanged) {
+ this.recalculateDps();
+ }
+ if (epsChanged) {
+ this.recalculateEps();
+ }
+ if (hpsChanged) {
+ this.recalculateHps();
+ }
if (powerGeneratedChange) {
this.updatePowerGenerated();
}
@@ -809,10 +847,13 @@ export default class Ship {
this.updatePowerUsed();
}
if (armourChange) {
- this.updateArmour();
+ this.recalculateArmour();
}
if (shieldChange) {
- this.updateShield();
+ this.recalculateShield();
+ }
+ if (shieldCellsChange) {
+ this.recalculateShieldCells();
}
this.updateTopSpeed();
this.updateJumpStats();
@@ -820,6 +861,148 @@ export default class Ship {
return this;
}
+ /**
+ * Calculate diminishing returns value, where values below a given limit are returned
+ * as-is, and values between the lower and upper limit of the diminishing returns are
+ * given at half value.
+ * Commonly used for resistances.
+ * @param {Number} val The value
+ * @param {Number} drll The lower limit for diminishing returns
+ * @param {Number} drul The upper limit for diminishing returns
+ * @return {this} The ship instance (for chaining operations)
+ */
+ diminishingReturns(val, drll, drul) {
+ if (val > drll) {
+ val = drll + (val - drll) / 2;
+ }
+ if (val > drul) {
+ val = drul;
+ }
+ return val;
+ }
+
+ /**
+ * Calculate damage per second for weapons
+ * @return {this} The ship instance (for chaining operations)
+ */
+ recalculateDps() {
+ let totalDpe = 0;
+ let totalExplDpe = 0;
+ let totalKinDpe = 0;
+ let totalThermDpe = 0;
+ let totalDps = 0;
+ let totalExplDps = 0;
+ let totalKinDps = 0;
+ let totalThermDps = 0;
+ let totalSDps = 0;
+ let totalExplSDps = 0;
+ let totalKinSDps = 0;
+ let totalThermSDps = 0;
+
+ for (let slotNum in this.hardpoints) {
+ const slot = this.hardpoints[slotNum];
+ if (slot.m && slot.enabled && slot.m.getDps()) {
+ const dpe = slot.m.getDps() / slot.m.getEps();
+ const dps = slot.m.getDps();
+ const sdps = slot.m.getClip() ? (slot.m.getClip() * slot.m.getDps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : dps;
+
+ totalDpe += dpe;
+ totalDps += dps;
+ totalSDps += sdps;
+ if (slot.m.type === 'E') {
+ totalExplDpe += dpe;
+ totalExplDps += dps;
+ totalExplSDps += sdps;
+ }
+ if (slot.m.type === 'K') {
+ totalKinDpe += dpe;
+ totalKinDps += dps;
+ totalKinSDps += sdps;
+ }
+ if (slot.m.type === 'T') {
+ totalThermDpe += dpe;
+ totalThermDps += dps;
+ totalThermSDps += sdps;
+ }
+ if (slot.m.type === 'EK') {
+ totalExplDpe += dpe / 2;
+ totalKinDpe += dpe / 2;
+ totalExplDps += dps / 2;
+ totalKinDps += dps / 2;
+ totalExplSDps += sdps / 2;
+ totalKinSDps += sdps / 2;
+ }
+ if (slot.m.type === 'ET') {
+ totalExplDpe += dpe / 2;
+ totalThermDpe += dpe / 2;
+ totalExplDps += dps / 2;
+ totalThermDps += dps / 2;
+ totalExplSDps += sdps / 2;
+ totalThermSDps += sdps / 2;
+ }
+ if (slot.m.type === 'KT') {
+ totalKinDpe += dpe / 2;
+ totalThermDpe += dpe / 2;
+ totalKinDps += dps / 2;
+ totalThermDps += dps / 2;
+ totalKinSDps += sdps / 2;
+ totalThermSDps += sdps / 2;
+ }
+ }
+ }
+
+ this.totalDpe = totalDpe;
+ this.totalExplDpe = totalExplDpe;
+ this.totalKinDpe = totalKinDpe;
+ this.totalThermDpe = totalThermDpe;
+ this.totalDps = totalDps;
+ this.totalExplDps = totalExplDps;
+ this.totalKinDps = totalKinDps;
+ this.totalThermDps = totalThermDps;
+ this.totalSDps = totalSDps;
+ this.totalExplSDps = totalExplSDps;
+ this.totalKinSDps = totalKinSDps;
+ this.totalThermSDps = totalThermSDps;
+
+ return this;
+ }
+
+ /**
+ * Calculate heat per second for weapons
+ * @return {this} The ship instance (for chaining operations)
+ */
+ recalculateHps() {
+ let totalHps = 0;
+
+ for (let slotNum in this.hardpoints) {
+ const slot = this.hardpoints[slotNum];
+ if (slot.m && slot.enabled && slot.m.getHps()) {
+ totalHps += slot.m.getHps();
+ }
+ }
+ this.totalHps = totalHps;
+
+ return this;
+ }
+
+ /**
+ * Calculate energy per second for weapons
+ * @return {this} The ship instance (for chaining operations)
+ */
+ recalculateEps() {
+ let totalEps = 0;
+
+ for (let slotNum in this.hardpoints) {
+ const slot = this.hardpoints[slotNum];
+ if (slot.m && slot.enabled && slot.m.getEps()) {
+ totalEps += slot.m.getEps();
+ }
+ }
+ this.totalEps = totalEps;
+
+ return this;
+ }
+
/**
* Update power calculations when amount generated changes
* @return {this} The ship instance (for chaining operations)
@@ -898,44 +1081,88 @@ export default class Ship {
* Update shield
* @return {this} The ship instance (for chaining operations)
*/
- updateShield() {
- // Base shield from generator
- let baseShield = 0;
- let sgSlot = this.findInternalByGroup('sg');
+ recalculateShield() {
+ let shield = 0;
+ let shieldExplRes = null;
+ let shieldKinRes = null;
+ let shieldThermRes = null;
+
+ const sgSlot = this.findInternalByGroup('sg');
if (sgSlot && sgSlot.enabled) {
- baseShield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1);
+ // Shield from generator
+ const baseShield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1);
+ shield = baseShield;
+ shieldExplRes = 1 - sgSlot.m.getExplosiveResistance();
+ shieldKinRes = 1 - sgSlot.m.getKineticResistance();
+ shieldThermRes = 1 - sgSlot.m.getThermalResistance();
+
+ // Shield from boosters
+ for (let slot of this.hardpoints) {
+ if (slot.m && slot.m.grp == 'sb') {
+ shield += baseShield * slot.m.getShieldBoost();
+ shieldExplRes *= (1 - slot.m.getExplosiveResistance());
+ shieldKinRes *= (1 - slot.m.getKineticResistance());
+ shieldThermRes *= (1 - slot.m.getThermalResistance());
+ }
+ }
}
- let shield = baseShield;
+ this.shield = shield;
+ this.shieldExplRes = shieldExplRes ? 1 - this.diminishingReturns(1 - shieldExplRes, 0.5, 0.75) : null;
+ this.shieldKinRes = shieldKinRes ? 1 - this.diminishingReturns(1 - shieldKinRes, 0.5, 0.75) : null;
+ this.shieldThermRes = shieldThermRes ? 1 - this.diminishingReturns(1 - shieldThermRes, 0.5, 0.75) : null;
+
+ return this;
+ }
- // Shield from boosters
- for (let slot of this.hardpoints) {
- if (slot.m && slot.m.grp == 'sb') {
- shield += baseShield * slot.m.getShieldBoost();
+ /**
+ * Update shield cells
+ * @return {this} The ship instance (for chaining operations)
+ */
+ recalculateShieldCells() {
+ let shieldCells = 0;
+
+ for (let slot of this.internal) {
+ if (slot.m && slot.m.grp == 'scb') {
+ shieldCells += slot.m.getShieldReinforcement() * slot.m.getCells();
}
}
- this.shield = shield;
+
+ this.shieldCells = shieldCells;
+
return this;
}
/**
- * Update armour
+ * Update armour and hull resistances
* @return {this} The ship instance (for chaining operations)
*/
- updateArmour() {
+ recalculateArmour() {
// Armour from bulkheads
let bulkhead = this.bulkheads.m;
let armour = this.baseArmour + (this.baseArmour * bulkhead.getHullBoost());
+ let hullExplRes = 1 - bulkhead.getExplosiveResistance();
+ let hullKinRes = 1 - bulkhead.getKineticResistance();
+ let hullThermRes = 1 - bulkhead.getThermalResistance();
// Armour from HRPs
for (let slot of this.internal) {
if (slot.m && slot.m.grp == 'hr') {
armour += slot.m.getHullReinforcement();
// Hull boost for HRPs is applied against the ship's base armour
- armour += this.baseArmour * slot.m.getModValue('hullboost');
+ armour += this.baseArmour * slot.m.getModValue('hullboost') / 10000;
+
+ hullExplRes *= (1 - slot.m.getExplosiveResistance());
+ hullKinRes *= (1 - slot.m.getKineticResistance());
+ hullThermRes *= (1 - slot.m.getThermalResistance());
}
}
+
this.armour = armour;
+ this.hullExplRes = 1 - this.diminishingReturns(1 - hullExplRes, 0.5, 0.75);
+ this.hullKinRes = 1 - this.diminishingReturns(1 - hullKinRes, 0.5, 0.75);
+ this.hullThermRes = 1 - this.diminishingReturns(1 - hullThermRes, 0.5, 0.75);
+
return this;
}
@@ -972,7 +1199,7 @@ export default class Ship {
priorities.push(slot.priority);
}
- this.serialized.priorities = LZString.compressToBase64(priorities.join('')).replace(/\//g, '-');
+ this.serialized.priorities = LZString.compressToBase64(priorities.join(''));
return this;
}
@@ -993,21 +1220,21 @@ export default class Ship {
enabled.push(slot.enabled ? 1 : 0);
}
- this.serialized.enabled = LZString.compressToBase64(enabled.join('')).replace(/\//g, '-');
+ this.serialized.enabled = LZString.compressToBase64(enabled.join(''));
return this;
}
/**
- * Update the modifications string
+ * Update the modifications string in a human-readable format
* @return {this} The ship instance (for chaining operations)
*/
- oldupdateModificationsString() {
+ debugupdateModificationsString() {
let allMods = new Array();
let bulkheadMods = new Array();
if (this.bulkheads.m && this.bulkheads.m.mods) {
for (let modKey in this.bulkheads.m.mods) {
- bulkheadMods.push(Modifications.modifiers.indexOf(modKey) + ':' + Math.round(this.bulkheads.m.getModValue(modKey) * 10000));
+ bulkheadMods.push(Modifications.modifications.indexOf(modKey) + ':' + this.bulkheads.m.getModValue(modKey));
}
}
allMods.push(bulkheadMods.join(';'));
@@ -1016,7 +1243,7 @@ export default class Ship {
let slotMods = new Array();
if (slot.m && slot.m.mods) {
for (let modKey in slot.m.mods) {
- slotMods.push(Modifications.modifiers.indexOf(modKey) + ':' + Math.round(slot.m.getModValue(modKey) * 10000));
+ slotMods.push(Modifications.modifications.indexOf(modKey) + ':' + slot.m.getModValue(modKey));
}
}
allMods.push(slotMods.join(';'));
@@ -1025,7 +1252,7 @@ export default class Ship {
let slotMods = new Array();
if (slot.m && slot.m.mods) {
for (let modKey in slot.m.mods) {
- slotMods.push(Modifications.modifiers.indexOf(modKey) + ':' + Math.round(slot.m.getModValue(modKey) * 10000));
+ slotMods.push(Modifications.modifications.indexOf(modKey) + ':' + slot.m.getModValue(modKey));
}
}
allMods.push(slotMods.join(';'));
@@ -1034,12 +1261,12 @@ export default class Ship {
let slotMods = new Array();
if (slot.m && slot.m.mods) {
for (let modKey in slot.m.mods) {
- slotMods.push(Modifications.modifiers.indexOf(modKey) + ':' + Math.round(slot.m.getModValue(modKey) * 10000));
+ slotMods.push(Modifications.modifications.indexOf(modKey) + ':' + slot.m.getModValue(modKey));
}
}
allMods.push(slotMods.join(';'));
}
- this.serialized.modifications = LZString.compressToBase64(allMods.join(',').replace(/,+$/, '')).replace(/\//g, '-');
+ this.serialized.modifications = LZString.compressToBase64(allMods.join(',').replace(/,+$/, ''));
return this;
}
@@ -1057,7 +1284,7 @@ export default class Ship {
for (let j = 0; j < mods.length; j++) {
let modElements = mods[j].split(':');
if (modElements[0].match('[0-9]+')) {
- arr[i][Modifications.modifiers[modElements[0]]] = Number(modElements[1]);
+ arr[i][Modifications.modifications[modElements[0]]] = Number(modElements[1]);
} else {
arr[i][modElements[0]] = Number(modElements[1]);
}
@@ -1071,7 +1298,7 @@ export default class Ship {
* This is a binary structure. It starts with a byte that identifies a slot, with bulkheads being ID 0 and moving through
* standard modules, hardpoints, and finally internal modules. It then contains one or more modifications, with each
* modification being a one-byte modification ID and at two-byte modification value. Modification IDs are based on the array
- * in Modifications.modifiers. The list of modifications is terminated by a modification ID of -1. The structure then repeats
+ * in Modifications.modifications. The list of modifications is terminated by a modification ID of -1. The structure then repeats
* for the next module, and the next, and is terminated by a slot ID of -1.
* @return {this} The ship instance (for chaining operations)
*/
@@ -1082,7 +1309,10 @@ export default class Ship {
let bulkheadMods = new Array();
if (this.bulkheads.m && this.bulkheads.m.mods) {
for (let modKey in this.bulkheads.m.mods) {
- bulkheadMods.push({ id: Modifications.modifiers.indexOf(modKey), value: Math.round(this.bulkheads.m.getModValue(modKey) * 10000) });
+ // Filter out invalid modifications
+ if (Modifications.validity['bh'] && Modifications.validity['bh'].indexOf(modKey) != -1) {
+ bulkheadMods.push({ id: Modifications.modifications.indexOf(modKey), value: this.bulkheads.m.getModValue(modKey) });
+ }
}
}
slots.push(bulkheadMods);
@@ -1091,7 +1321,10 @@ export default class Ship {
let slotMods = new Array();
if (slot.m && slot.m.mods) {
for (let modKey in slot.m.mods) {
- slotMods.push({ id: Modifications.modifiers.indexOf(modKey), value: Math.round(slot.m.getModValue(modKey) * 10000) });
+ // Filter out invalid modifications
+ if (Modifications.validity[slot.m.grp] && Modifications.validity[slot.m.grp].indexOf(modKey) != -1) {
+ slotMods.push({ id: Modifications.modifications.indexOf(modKey), value: slot.m.getModValue(modKey) });
+ }
}
}
slots.push(slotMods);
@@ -1101,7 +1334,10 @@ export default class Ship {
let slotMods = new Array();
if (slot.m && slot.m.mods) {
for (let modKey in slot.m.mods) {
- slotMods.push({ id: Modifications.modifiers.indexOf(modKey), value: Math.round(slot.m.getModValue(modKey) * 10000) });
+ // Filter out invalid modifications
+ if (Modifications.validity[slot.m.grp] && Modifications.validity[slot.m.grp].indexOf(modKey) != -1) {
+ slotMods.push({ id: Modifications.modifications.indexOf(modKey), value: slot.m.getModValue(modKey) });
+ }
}
}
slots.push(slotMods);
@@ -1111,7 +1347,10 @@ export default class Ship {
let slotMods = new Array();
if (slot.m && slot.m.mods) {
for (let modKey in slot.m.mods) {
- slotMods.push({ id: Modifications.modifiers.indexOf(modKey), value: Math.round(slot.m.getModValue(modKey) * 10000) });
+ // Filter out invalid modifications
+ if (Modifications.validity[slot.m.grp] && Modifications.validity[slot.m.grp].indexOf(modKey) != -1) {
+ slotMods.push({ id: Modifications.modifications.indexOf(modKey), value: slot.m.getModValue(modKey) });
+ }
}
}
slots.push(slotMods);
@@ -1137,6 +1376,7 @@ export default class Ship {
for (let slotMod of slot) {
buffer.writeInt8(slotMod.id, curpos++);
buffer.writeInt32LE(slotMod.value, curpos);
+ // console.log('ENCODE Slot ' + i + ': ' + Modifications.modifications[slotMod.id] + ' = ' + slotMod.value);
curpos += 4;
}
buffer.writeInt8(-1, curpos++);
@@ -1147,7 +1387,7 @@ export default class Ship {
buffer.writeInt8(-1, curpos++);
}
- this.serialized.modifications = zlib.gzipSync(buffer).toString('base64').replace(/\//g, '-');
+ this.serialized.modifications = zlib.gzipSync(buffer).toString('base64');
} else {
this.serialized.modifications = null;
}
@@ -1169,7 +1409,8 @@ export default class Ship {
while (modificationId != -1) {
let modificationValue = buffer.readInt32LE(curpos);
curpos += 4;
- modifications[Modifications.modifiers[modificationId]] = modificationValue;
+ // console.log('DECODE Slot ' + slot + ': ' + Modifications.modifications[modificationId] + ' = ' + modificationValue);
+ modifications[Modifications.modifications[modificationId]] = modificationValue;
modificationId = buffer.readInt8(curpos++);
}
arr[slot] = modifications;
@@ -1219,7 +1460,6 @@ export default class Ship {
case 1: this.serialized.hardpoints = null; break;
case 2: this.serialized.internal = null;
}
- this.serialized.modifications = null;
}
return this;
}
diff --git a/src/app/stores/Persist.js b/src/app/stores/Persist.js
index 63473691..4ef740f6 100644
--- a/src/app/stores/Persist.js
+++ b/src/app/stores/Persist.js
@@ -11,6 +11,7 @@ const LS_KEY_MOD_DISCOUNT = 'moduleDiscount';
const LS_KEY_STATE = 'state';
const LS_KEY_SIZE_RATIO = 'sizeRatio';
const LS_KEY_TOOLTIPS = 'tooltips';
+const LS_KEY_MODULE_RESISTANCES = 'moduleResistances';
let LS;
@@ -81,6 +82,7 @@ export class Persist extends EventEmitter {
LS = null;
}
+ let moduleResistances = _get(LS_KEY_MODULE_RESISTANCES);
let tips = _get(LS_KEY_TOOLTIPS);
let insurance = _getString(LS_KEY_INSURANCE);
let shipDiscount = _get(LS_KEY_SHIP_DISCOUNT);
@@ -99,6 +101,7 @@ export class Persist extends EventEmitter {
this.state = _get(LS_KEY_STATE);
this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1;
this.tooltipsEnabled = tips === null ? true : tips;
+ this.moduleResistancesEnabled = moduleResistances === null ? true : moduleResistances;
if (LS) {
window.addEventListener('storage', this.onStorageChange);
@@ -143,6 +146,10 @@ export class Persist extends EventEmitter {
this.tooltipsEnabled = !!newValue && newValue.toLowerCase() == 'true';
this.emit('tooltips', this.tooltipsEnabled);
break;
+ case LS_KEY_MODULE_RESISTANCES:
+ this.moduleResistancesEnabled = !!newValue && newValue.toLowerCase() == 'true';
+ this.emit('moduleresistances', this.moduleResistancesEnabled);
+ break;
}
} catch (e) {
// On JSON.Parse Error - don't sync or do anything
@@ -183,6 +190,21 @@ export class Persist extends EventEmitter {
return this.tooltipsEnabled;
}
+ /**
+ * Show module resistances setting
+ * @param {boolean} show Optional - update setting
+ * @return {boolean} True if module resistances should be shown
+ */
+ showModuleResistances(show) {
+ if (show !== undefined) {
+ this.moduleResistancesEnabled = !!show;
+ _put(LS_KEY_MODULE_RESISTANCES, this.moduleResistancesEnabled);
+ this.emit('moduleresistances', this.moduleResistancesEnabled);
+ }
+
+ return this.moduleResistancesEnabled;
+ }
+
/**
* Persist a ship build in local storage.
*
diff --git a/src/app/utils/CompanionApiUtils.js b/src/app/utils/CompanionApiUtils.js
new file mode 100644
index 00000000..c6c3b71f
--- /dev/null
+++ b/src/app/utils/CompanionApiUtils.js
@@ -0,0 +1,367 @@
+import React from 'react';
+import { Modifications, Modules, Ships } from 'coriolis-data/dist';
+import Module from '../shipyard/Module';
+import Ship from '../shipyard/Ship';
+
+// mapping from fd's ship model names to coriolis'
+const SHIP_FD_NAME_TO_CORIOLIS_NAME = {
+ 'Adder': 'adder',
+ 'Anaconda': 'anaconda',
+ 'Asp': 'asp',
+ 'Asp_Scout': 'asp_scout',
+ 'BelugaLiner': 'beluga',
+ 'CobraMkIII': 'cobra_mk_iii',
+ 'CobraMkIV': 'cobra_mk_iv',
+ 'Cutter': 'imperial_cutter',
+ 'DiamondBack': 'diamondback_explorer',
+ 'DiamondBackXL': 'diamondback',
+ 'Eagle': 'eagle',
+ 'Empire_Courier': 'imperial_courier',
+ 'Empire_Eagle': 'imperial_eagle',
+ 'Empire_Trader': 'imperial_clipper',
+ 'Federation_Corvette': 'federal_corvette',
+ 'Federation_Dropship': 'federal_dropship',
+ 'Federation_Dropship_MkII': 'federal_assault_ship',
+ 'Federation_Gunship': 'federal_gunship',
+ 'FerDeLance': 'fer_de_lance',
+ 'Hauler': 'hauler',
+ 'Independant_Trader': 'keelback',
+ 'Orca': 'orca',
+ 'Python': 'python',
+ 'SideWinder': 'sidewinder',
+ 'Type6': 'type_6_transporter',
+ 'Type7': 'type_7_transport',
+ 'Type9': 'type_9_heavy',
+ 'Viper': 'viper',
+ 'Viper_MKIV': 'viper_mk_iv',
+ 'Vulture': 'vulture'
+};
+
+// Mapping from hardpoint class to name in companion API
+const HARDPOINT_NUM_TO_CLASS = {
+ 0: 'Tiny',
+ 1: 'Small',
+ 2: 'Medium',
+ 3: 'Large',
+ 4: 'Huge'
+};
+
+
+/**
+ * Obtain a module given its ED ID
+ * @param {Integer} edId the Elite ID of the module
+ * @return {Module} the module
+ */
+function _moduleFromEdId(edId) {
+ if (!edId) return null;
+
+ // Check standard modules
+ for (const grp in Modules.standard) {
+ if (Modules.standard.hasOwnProperty(grp)) {
+ for (const i in Modules.standard[grp]) {
+ if (Modules.standard[grp][i].edID === edId) {
+ // Found it
+ return new Module({ template: Modules.standard[grp][i] });
+ }
+ }
+ }
+ }
+
+ // Check hardpoint modules
+ for (const grp in Modules.hardpoints) {
+ if (Modules.hardpoints.hasOwnProperty(grp)) {
+ for (const i in Modules.hardpoints[grp]) {
+ if (Modules.hardpoints[grp][i].edID === edId) {
+ // Found it
+ return new Module({ template: Modules.hardpoints[grp][i] });
+ }
+ }
+ }
+ }
+
+ // Check internal modules
+ for (const grp in Modules.internal) {
+ if (Modules.internal.hasOwnProperty(grp)) {
+ for (const i in Modules.internal[grp]) {
+ if (Modules.internal[grp][i].edID === edId) {
+ // Found it
+ return new Module({ template: Modules.internal[grp][i] });
+ }
+ }
+ }
+ }
+
+ // Not found
+ return null;
+}
+
+/**
+ * Obtain the model of a ship given its ED name
+ * @param {string} edName the Elite name of the ship
+ * @return {string} the Coriolis model of the ship
+ */
+function _shipModelFromEDName(edName) {
+ return SHIP_FD_NAME_TO_CORIOLIS_NAME[edName];
+}
+
+/**
+ * Obtain a ship's model from the companion API JSON
+ * @param {object} json the companion API JSON
+ * @return {string} the Coriolis model of the ship
+ */
+export function shipModelFromJson(json) {
+ return _shipModelFromEDName(json.name);
+}
+
+/**
+ * Build a ship from the companion API JSON
+ * @param {object} json the companion API JSON
+ * @return {Ship} the built ship
+ */
+export function shipFromJson(json) {
+ // Start off building a basic ship
+ const shipModel = shipModelFromJson(json);
+ if (!shipModel) {
+ throw 'No such ship found: "' + json.name + '"';
+ }
+ const shipTemplate = Ships[shipModel];
+
+ let ship = new Ship(shipModel, shipTemplate.properties, shipTemplate.slots);
+ ship.buildWith(null);
+
+ // Set the cargo hatch. We don't have any information on it so guess it's priority 5 and disabled
+ ship.cargoHatch.enabled = false;
+ ship.cargoHatch.priority = 4;
+
+ // Add the bulkheads
+ const armourJson = json.modules.Armour.module;
+ if (armourJson.name.endsWith('_Armour_Grade1')) {
+ ship.useBulkhead(0, true);
+ } else if (armourJson.name.endsWith('_Armour_Grade2')) {
+ ship.useBulkhead(1, true);
+ } else if (armourJson.name.endsWith('_Armour_Grade3')) {
+ ship.useBulkhead(2, true);
+ } else if (armourJson.name.endsWith('_Armour_Mirrored')) {
+ ship.useBulkhead(3, true);
+ } else if (armourJson.name.endsWith('_Armour_Reactive')) {
+ ship.useBulkhead(4, true);
+ } else {
+ throw 'Unknown bulkheads "' + armourJson.name + '"';
+ }
+ ship.bulkheads.enabled = true;
+ if (armourJson.modifiers) _addModifications(ship.bulkheads.m, armourJson.modifiers);
+
+ // Add the standard modules
+ // Power plant
+ const powerplantJson = json.modules.PowerPlant.module;
+ const powerplant = _moduleFromEdId(powerplantJson.id);
+ if (powerplantJson.modifiers) _addModifications(powerplant, powerplantJson.modifiers);
+ ship.use(ship.standard[0], powerplant, true);
+ ship.standard[0].enabled = powerplantJson.on === true;
+ ship.standard[0].priority = powerplantJson.priority;
+
+ // Thrusters
+ const thrustersJson = json.modules.MainEngines.module;
+ const thrusters = _moduleFromEdId(thrustersJson.id);
+ if (thrustersJson.modifiers) _addModifications(thrusters, thrustersJson.modifiers);
+ ship.use(ship.standard[1], thrusters, true);
+ ship.standard[1].enabled = thrustersJson.on === true;
+ ship.standard[1].priority = thrustersJson.priority;
+
+ // FSD
+ const frameshiftdriveJson = json.modules.FrameShiftDrive.module;
+ const frameshiftdrive = _moduleFromEdId(frameshiftdriveJson.id);
+ if (frameshiftdriveJson.modifiers) _addModifications(frameshiftdrive, frameshiftdriveJson.modifiers);
+ ship.use(ship.standard[2], frameshiftdrive, true);
+ ship.standard[2].enabled = frameshiftdriveJson.on === true;
+ ship.standard[2].priority = frameshiftdriveJson.priority;
+
+ // Life support
+ const lifesupportJson = json.modules.LifeSupport.module;
+ const lifesupport = _moduleFromEdId(lifesupportJson.id);
+ if (lifesupportJson.modifiers)_addModifications(lifesupport, lifesupportJson.modifiers);
+ ship.use(ship.standard[3], lifesupport, true);
+ ship.standard[3].enabled = lifesupportJson.on === true;
+ ship.standard[3].priority = lifesupportJson.priority;
+
+ // Power distributor
+ const powerdistributorJson = json.modules.PowerDistributor.module;
+ const powerdistributor = _moduleFromEdId(powerdistributorJson.id);
+ if (powerdistributorJson.modifiers) _addModifications(powerdistributor, powerdistributorJson.modifiers);
+ ship.use(ship.standard[4], powerdistributor, true);
+ ship.standard[4].enabled = powerdistributorJson.on === true;
+ ship.standard[4].priority = powerdistributorJson.priority;
+
+ // Sensors
+ const sensorsJson = json.modules.Radar.module;
+ const sensors = _moduleFromEdId(sensorsJson.id);
+ if (sensorsJson.modifiers) _addModifications(sensors, sensorsJson.modifiers);
+ ship.use(ship.standard[5], sensors, true);
+ ship.standard[5].enabled = sensorsJson.on === true;
+ ship.standard[5].priority = sensorsJson.priority;
+
+ // Fuel tank
+ const fueltankJson = json.modules.FuelTank.module;
+ const fueltank = _moduleFromEdId(fueltankJson.id);
+ ship.use(ship.standard[6], fueltank, true);
+ ship.standard[6].enabled = true;
+ ship.standard[6].priority = 0;
+
+ // Add hardpoints
+ let hardpointClassNum = -1;
+ let hardpointSlotNum = -1;
+ let hardpointArrayNum = 0;
+ for (let i in shipTemplate.slots.hardpoints) {
+ if (shipTemplate.slots.hardpoints[i] === hardpointClassNum) {
+ // Another slot of the same class
+ hardpointSlotNum++;
+ } else {
+ // The first slot of a new class
+ hardpointClassNum = shipTemplate.slots.hardpoints[i];
+ hardpointSlotNum = 1;
+ }
+
+ // Now that we know what we're looking for, find it
+ const hardpointName = HARDPOINT_NUM_TO_CLASS[hardpointClassNum] + 'Hardpoint' + hardpointSlotNum;
+ const hardpointSlot = json.modules[hardpointName];
+ if (!hardpointSlot.module) {
+ // No module
+ } else {
+ const hardpointJson = hardpointSlot.module;
+ const hardpoint = _moduleFromEdId(hardpointJson.id);
+ if (hardpointJson.modifiers) _addModifications(hardpoint, hardpointJson.modifiers);
+ ship.use(ship.hardpoints[hardpointArrayNum], hardpoint, true);
+ ship.hardpoints[hardpointArrayNum].enabled = hardpointJson.on === true;
+ ship.hardpoints[hardpointArrayNum].priority = hardpointJson.priority;
+ }
+ hardpointArrayNum++;
+ }
+
+ // Add internal compartments
+ let internalSlotNum = 1;
+ for (let i in shipTemplate.slots.internal) {
+ const internalClassNum = shipTemplate.slots.internal[i];
+
+ let internalSlot = null;
+ while (internalSlot === null && internalSlotNum < 99) {
+ // Slot numbers are not contiguous so handle skips
+ const internalName = 'Slot' + (internalSlotNum <= 9 ? '0' : '') + internalSlotNum + '_Size' + internalClassNum;
+ if (json.modules[internalName]) {
+ internalSlot = json.modules[internalName];
+ }
+ internalSlotNum++;
+ }
+ if (!internalSlot.module) {
+ // No module
+ } else {
+ const internalJson = internalSlot.module;
+ const internal = _moduleFromEdId(internalJson.id);
+ if (internalJson.modifiers) _addModifications(internal, internalJson.modifiers);
+ ship.use(ship.internal[i], internal, true);
+ ship.internal[i].enabled = internalJson.on === true;
+ ship.internal[i].priority = internalJson.priority;
+ }
+ }
+
+ // Now update the ship's codes before returning it
+ return ship.updatePowerPrioritesString().updatePowerEnabledString().updateModificationsString();
+}
+
+/**
+ * Add the modifications for a module
+ * @param {Module} module the module
+ * @param {Object} modifiers the modifiers
+ */
+function _addModifications(module, modifiers) {
+ if (!modifiers || !modifiers.modifiers) return;
+
+ for (const i in modifiers.modifiers) {
+ // Look up the modifiers to find what we need to do
+ const modifierActions = Modifications.modifierActions[modifiers.modifiers[i].name];
+ const value = modifiers.modifiers[i].value;
+
+ // Carry out the required changes
+ for (const action in modifierActions) {
+ const actionValue = modifierActions[action] * value;
+ let mod = module.getModValue(action) / 10000;
+ if (!mod) {
+ mod = 0;
+ }
+ module.setModValue(action, ((1 + mod) * (1 + actionValue) - 1) * 10000);
+ }
+ }
+
+ // Need to fix up a few items
+
+ // Shield boosters are treated internally as straight modifiers, so rather than (for example)
+ // being a 4% boost they are a 104% multiplier. Unfortunately this means that our % modification
+ // is incorrect so we fix it
+ if (module.grp === 'sb' && module.getModValue('shieldboost')) {
+ const alteredBoost = (1 + module.shieldboost) * (module.getModValue('shieldboost') / 10000);
+ module.setModValue('shieldboost', alteredBoost * 10000 / module.shieldboost);
+ }
+
+ // Shield booster resistance is actually a damage modifier, so needs to be inverted.
+ if (module.grp === 'sb') {
+ if (module.getModValue('explres')) {
+ module.setModValue('explres', ((module.getModValue('explres') / 10000) * -1) * 10000);
+ }
+ if (module.getModValue('kinres')) {
+ module.setModValue('kinres', ((module.getModValue('kinres') / 10000) * -1) * 10000);
+ }
+ if (module.getModValue('thermres')) {
+ module.setModValue('thermres', ((module.getModValue('thermres') / 10000) * -1) * 10000);
+ }
+ }
+
+ // Shield generator resistance is actually a damage modifier, so needs to be inverted.
+ // In addition, the modification is based off the inherent resistance of the module
+ if (module.isShieldGenerator()) {
+ if (module.getModValue('explres')) {
+ module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000);
+ }
+ if (module.getModValue('kinres')) {
+ module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000);
+ }
+ if (module.getModValue('thermres')) {
+ module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000);
+ }
+ }
+
+ // Hull reinforcement package resistance is actually a damage modifier, so needs to be inverted.
+ if (module.grp === 'hr') {
+ if (module.getModValue('explres')) {
+ module.setModValue('explres', ((module.getModValue('explres') / 10000) * -1) * 10000);
+ }
+ if (module.getModValue('kinres')) {
+ module.setModValue('kinres', ((module.getModValue('kinres') / 10000) * -1) * 10000);
+ }
+ if (module.getModValue('thermres')) {
+ module.setModValue('thermres', ((module.getModValue('thermres') / 10000) * -1) * 10000);
+ }
+ }
+
+ // Bulkhead resistance is actually a damage modifier, so needs to be inverted.
+ // In addition, the modification is based off the inherent resistance of the module
+ if (module.grp == 'bh') {
+ if (module.getModValue('explres')) {
+ module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000);
+ }
+ if (module.getModValue('kinres')) {
+ module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000);
+ }
+ if (module.getModValue('thermres')) {
+ module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000);
+ }
+ }
+
+ // Jitter is an absolute number, so we need to divide it by 100
+ if (module.getModValue('jitter')) {
+ module.setModValue('jitter', module.getModValue('jitter') / 100);
+ }
+
+ // FD uses interval between bursts internally, so we need to translate this to a real rate of fire
+ if (module.getModValue('rof')) {
+ module.setModValue('rof', ((1 / (1 + module.getModValue('rof') / 10000)) - 1) * 10000);
+ }
+}
diff --git a/src/app/utils/SlotFunctions.js b/src/app/utils/SlotFunctions.js
index 74697c7f..a9481315 100644
--- a/src/app/utils/SlotFunctions.js
+++ b/src/app/utils/SlotFunctions.js
@@ -93,69 +93,6 @@ export function slotComparator(translate, propComparator, desc) {
};
}
-const PROP_BLACKLIST = {
- eddbID: 1,
- edID: 1,
- id: 1,
- index: 1,
- 'class': 1,
- rating: 1,
- maxfuel: 1,
- fuelmul: 1,
- fuelpower: 1,
- optmass: 1,
- maxmass: 1,
- minmass: 1,
- passive: 1,
- thermload: 1,
- ammocost: 1,
- activepower: 1,
- cooldown: 1,
- chargeup: 1,
- optmul: 1,
- minmul: 1,
- maxmul: 1,
- ssdam: 1,
- mjdps: 1,
- mjeps: 1,
- mass: 1,
- cost: 1,
- recover: 1,
- wepcap: 1,
- weprate: 1,
- engcap: 1,
- engrate: 1,
- syscap: 1,
- sysrate: 1,
- breachdps: 1,
- breachmin: 1,
- breachmax: 1,
- integrity: 1
-};
-
-const TERM_LOOKUP = {
- pgen: 'power',
- armouradd: 'armour',
- rof: 'ROF',
- dps: 'DPS'
-};
-
-const FORMAT_LOOKUP = {
- time: 'time'
-};
-
-const UNIT_LOOKUP = {
- fuel: 'T',
- cargo: 'T',
- rate: 'kgs',
- range: 'km',
- recharge: 'MJ',
- rangeLS: 'Ls',
- power: 'MJ',
- pgen: 'MJ',
- rof: 'ps'
-};
-
/**
* Determine the appropriate class based on diff value
* @param {Number} a Potential Module (cannot be null)
@@ -214,23 +151,15 @@ export function diffDetails(language, m, mm) {
let mmMass = mm ? mm.getMass() : 0;
if (mMass != mmMass) propDiffs.push(
{translate('mass')}: {diff(formats.round, mMass, mmMass)}{units.T}
);
+ let mPowerGeneration = m.pgen || 0;
+ let mmPowerGeneration = mm ? mm.getPowerGeneration() : 0;
+ if (mPowerGeneration != mmPowerGeneration) propDiffs.push(
{translate('pgen')}: {diff(formats.round, mPowerGeneration, mmPowerGeneration)}{units.MJ}
);
+
let mPowerUsage = m.power || 0;
let mmPowerUsage = mm ? mm.getPowerUsage() : 0;
if (mPowerUsage != mmPowerUsage) propDiffs.push(
{translate('power')}: {diff(formats.round, mPowerUsage, mmPowerUsage)}{units.MJ}
);
-// for (let p in m) {
-// if (!PROP_BLACKLIST[p] && !isNaN(m[p])) {
-// let mVal = m[p] === null ? Infinity : m[p];
-// let mmVal = mm[p] === null ? Infinity : mm[p];
-// let format = formats[FORMAT_LOOKUP[p]] || formats.round;
-// propDiffs.push(
-// {`${translate(TERM_LOOKUP[p] || p)}: `}
-// {diff(format, mVal, mmVal)}{units[UNIT_LOOKUP[p]]}
-//
);
-// }
-// }
-
- let mDps = m.damage * (m.rpshot || 1) * m.rof || 0;
+ let mDps = m.damage * (m.rpshot || 1) * (m.rof || 1) || 0;
let mmDps = mm ? mm.getDps() || 0 : 0;
if (mDps != mmDps) propDiffs.push(
{translate('dps')}: {diff(formats.round, mDps, mmDps)}
);
diff --git a/src/app/utils/UrlGenerators.js b/src/app/utils/UrlGenerators.js
index e599fd4d..827cc04e 100644
--- a/src/app/utils/UrlGenerators.js
+++ b/src/app/utils/UrlGenerators.js
@@ -1,4 +1,3 @@
-
/**
* Generates a URL for the outiffing page
* @param {String} shipId Ship Id
@@ -7,15 +6,18 @@
* @return {String} URL
*/
export function outfitURL(shipId, code, buildName) {
- let parts = ['/outfit/', shipId];
+ let path = '/outfit/' + shipId;
+
+ let sepChar = '?';
if (code) {
- parts.push('/', code);
+ path = path + sepChar + 'code=' + encodeURIComponent(code);
+ sepChar = '&';
}
if (buildName) {
- parts.push('?bn=', encodeURIComponent(buildName));
+ path = path + sepChar + 'bn=' + encodeURIComponent(buildName);
}
- return parts.join('');
-}
+ return path;
+}
diff --git a/src/app/utils/UtilityFunctions.js b/src/app/utils/UtilityFunctions.js
index fd51b434..1137af26 100644
--- a/src/app/utils/UtilityFunctions.js
+++ b/src/app/utils/UtilityFunctions.js
@@ -58,3 +58,16 @@ export function shallowEqual(objA, objB) {
return true;
}
+
+/**
+ * Turn a URL-safe base-64 encoded string in to a normal version.
+ * Coriolis used to use a different encoding system, and some old
+ * data might be bookmarked or on local storage, so we keep this
+ * around and use it when decoding data from the old-style URLs to
+ * be safe.
+ * @param {string} data the string
+ * @return {string} the converted string
+ */
+export function fromUrlSafe(data) {
+ return data ? data.replace(/-/g, '/').replace(/_/g, '+') : null;
+}
diff --git a/src/less/icons.less b/src/less/icons.less
index 63c8316f..9af0c0e6 100755
--- a/src/less/icons.less
+++ b/src/less/icons.less
@@ -1,4 +1,5 @@
+// Standard icons
.icon {
display: inline-block;
vertical-align: middle;
@@ -25,3 +26,39 @@
height: 2em;
}
}
+
+// Modifiction icons - hard-code stroke/fill
+.modicon {
+ display: inline-block;
+ vertical-align: middle;
+ width: 1.1em;
+ height: 1em;
+ stoke: @fg;
+ fill: transparent;
+
+ &.sm {
+ width: 0.8em;
+ height: 0.75em;
+ }
+
+ &.tn {
+ width: 0.6em;
+ height: 0.5em;
+ }
+
+ &.lg {
+ width: 1.6em;
+ height: 1.5em;
+ }
+
+ &.xl {
+ width: 2.1em;
+ height: 2em;
+ }
+}
+
+.summary {
+ stroke: @fg;
+ stroke-width: 10;
+ fill: @fg;
+}
diff --git a/src/schemas/ship-loadout/4.json b/src/schemas/ship-loadout/4.json
new file mode 100644
index 00000000..d3158f9f
--- /dev/null
+++ b/src/schemas/ship-loadout/4.json
@@ -0,0 +1,356 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "id": "http://cdn.coriolis.io/schemas/ship-loadout/4.json#",
+ "title": "Ship Loadout",
+ "type": "object",
+ "description": "The details for a specific ship build/loadout",
+ "required": ["name", "ship", "components"],
+ "properties": {
+ "name": {
+ "description": "The name of the build/loadout",
+ "type": "string",
+ "minLength": 1
+ },
+ "ship": {
+ "description": "The full display name of the ship",
+ "type": "string",
+ "minimum": 3
+ },
+ "manufacturer": {
+ "description": "The ship manufacturer",
+ "type": "string"
+ },
+ "references" : {
+ "description": "3rd Party references and/or links to this build/loadout",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["name","url"],
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "description": "The name of the 3rd party, .e.g 'Coriolis.io' or 'E:D Shipyard'",
+ "type": "string"
+ },
+ "url": {
+ "description": "The link/url to the 3rd party referencing this build/loadout",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "components": {
+ "description": "The components used by this build",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["standard", "internal", "hardpoints", "utility"],
+ "properties": {
+ "standard": {
+ "description": "The set of standard components across all ships",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["bulkheads", "powerPlant", "thrusters", "frameShiftDrive", "lifeSupport", "powerDistributor", "sensors", "fuelTank", "cargoHatch"],
+ "properties": {
+ "bulkheads": {
+ "enum": ["Lightweight Alloy", "Reinforced Alloy", "Military Grade Composite", "Mirrored Surface Composite", "Reactive Surface Composite"]
+ },
+ "cargoHatch": {
+ "required": ["enabled", "priority"],
+ "properties": {
+ "enabled": { "type": "boolean" },
+ "priority": { "type": "integer", "minimum": 1, "maximum": 5 }
+ }
+ },
+ "powerPlant": {
+ "required": ["class", "rating", "enabled", "priority"],
+ "properties": {
+ "class": { "type": "integer", "minimum": 2, "maximum": 8 },
+ "rating": { "$ref": "#/definitions/standardRatings" },
+ "enabled": { "type": "boolean" },
+ "priority": { "type": "integer", "minimum": 1, "maximum": 5 },
+ "modifications": { "type": "object" }
+ }
+ },
+ "thrusters": {
+ "required": ["class", "rating", "enabled", "priority"],
+ "properties": {
+ "class": { "type": "integer", "minimum": 2, "maximum": 8 },
+ "rating": { "$ref": "#/definitions/standardRatings" },
+ "enabled": { "type": "boolean" },
+ "priority": { "type": "integer", "minimum": 1, "maximum": 5 },
+ "name": {
+ "description": "The name identifing the thrusters (if applicable), e.g. 'Enhanced Performance'",
+ "type": "string"
+ },
+ "modifications": { "type": "object" }
+ }
+ },
+ "frameShiftDrive": {
+ "required": ["class", "rating", "enabled", "priority"],
+ "properties": {
+ "class": { "type": "integer", "minimum": 2, "maximum": 8 },
+ "rating": { "$ref": "#/definitions/standardRatings" },
+ "enabled": { "type": "boolean" },
+ "priority": { "type": "integer", "minimum": 1, "maximum": 5 },
+ "modifications": { "type": "object" }
+ }
+ },
+ "lifeSupport": {
+ "required": ["class", "rating", "enabled", "priority"],
+ "properties": {
+ "class": { "type": "integer", "minimum": 1, "maximum": 6 },
+ "rating": { "$ref": "#/definitions/standardRatings" },
+ "enabled": { "type": "boolean" },
+ "priority": { "type": "integer", "minimum": 1, "maximum": 5 },
+ "modifications": { "type": "object" }
+ }
+ },
+ "powerDistributor": {
+ "required": ["class", "rating", "enabled", "priority"],
+ "properties": {
+ "class": { "type": "integer", "minimum": 1, "maximum": 8 },
+ "rating": { "$ref": "#/definitions/standardRatings" },
+ "enabled": { "type": "boolean" },
+ "priority": { "type": "integer", "minimum": 1, "maximum": 5 },
+ "modifications": { "type": "object" }
+ }
+ },
+ "sensors": {
+ "required": ["class", "rating", "enabled", "priority"],
+ "properties": {
+ "class": { "type": "integer", "minimum": 1, "maximum": 8 },
+ "rating": { "$ref": "#/definitions/standardRatings" },
+ "enabled": { "type": "boolean" },
+ "priority": { "type": "integer", "minimum": 1, "maximum": 5 },
+ "modifications": { "type": "object" }
+ }
+ },
+ "fuelTank": {
+ "required": ["class", "rating", "enabled", "priority"],
+ "properties": {
+ "class": { "type": "integer", "minimum": 1, "maximum": 6 },
+ "rating": { "$ref": "#/definitions/standardRatings" },
+ "enabled": { "type": "boolean" },
+ "priority": { "type": "integer", "minimum": 1, "maximum": 5 },
+ "modifications": { "type": "object" }
+ }
+ }
+ }
+ },
+ "internal": {
+ "type": "array",
+ "items": {
+ "type": ["object", "null"],
+ "required": ["class", "rating", "enabled", "priority", "group"],
+ "properties" : {
+ "class": { "type": "integer", "minimum": 1, "maximum": 8 },
+ "rating": { "$ref": "#/definitions/standardRatings" },
+ "enabled": { "type": "boolean" },
+ "priority": { "type": "integer", "minimum": 1, "maximum": 5 },
+ "group": {
+ "description": "The group of the component, e.g. 'Shield Generator', or 'Cargo Rack'",
+ "type": "string"
+ },
+ "name": {
+ "description": "The name identifying the component (if applicable), e.g. 'Advance Discovery Scanner', or 'Detailed Surface Scanner'",
+ "type": "string"
+ },
+ "modifications": { "type": "object" }
+ }
+ },
+ "minItems": 3
+ },
+ "hardpoints": {
+ "type": "array",
+ "items": {
+ "type": ["object", "null"],
+ "required": ["class", "rating", "enabled", "priority", "group", "mount"],
+ "properties" : {
+ "class": { "type": "integer", "minimum": 1, "maximum": 4 },
+ "rating": { "$ref": "#/definitions/allRatings" },
+ "enabled": { "type": "boolean" },
+ "priority": { "type": "integer", "minimum": 1, "maximum": 5 },
+ "mount": { "type": "string", "enum": ["Fixed", "Gimballed", "Turret"] },
+ "group": {
+ "description": "The group of the component, e.g. 'Beam Laser', or 'Missile Rack'",
+ "type": "string"
+ },
+ "name": {
+ "description": "The name identifing the component (if applicable), e.g. 'Retributor', or 'Mining Lance'",
+ "type": "string"
+ },
+ "modifications": { "type": "object" }
+ }
+ },
+ "minItems": 1
+ },
+ "utility": {
+ "type": "array",
+ "items": {
+ "type": ["object", "null"],
+ "required": ["class", "rating", "enabled", "priority", "group"],
+ "properties" : {
+ "class": { "type": "integer", "minimum": 0, "maximum": 0 },
+ "rating": { "$ref": "#/definitions/allRatings" },
+ "enabled": { "type": "boolean" },
+ "priority": { "type": "integer", "minimum": 1, "maximum": 5 },
+ "group": {
+ "description": "The group of the component, e.g. 'Shield Booster', or 'Kill Warrant Scanner'",
+ "type": "string"
+ },
+ "name": {
+ "description": "The name identifing the component (if applicable), e.g. 'Point Defence', or 'Electronic Countermeasure'",
+ "type": "string"
+ },
+ "modifications": { "type": "object" }
+ }
+ },
+ "minItems": 1
+ }
+ }
+ },
+ "stats": {
+ "description": "Optional statistics from the build",
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "agility": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "armour": {
+ "description": "Sum of base armour + any hull reinforcements",
+ "type": "integer",
+ "minimum": 1
+ },
+ "armourAdded":{
+ "description": "Armour added through Hull reinforcement",
+ "type": "integer",
+ "minimum": 0
+ },
+ "baseShieldStrength": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "baseArmour": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "boost": {
+ "description": "Maximum boost speed of the ships (4 pips, straight-line)",
+ "type": "number",
+ "minimum": 0
+ },
+ "cargoCapacity": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "class": {
+ "description": "Ship Class/Size [Small, Medium, Large]",
+ "enum": [1,2,3]
+ },
+ "totalDps": {
+ "description": "Total damage dealt per second of all weapons",
+ "type": "number",
+ "minimum": 0
+ },
+ "totalEps": {
+ "description": "Total energy consumed per second of all weapons",
+ "type": "number",
+ "minimum": 0
+ },
+ "totalHps": {
+ "description": "Total heat generated per second of all weapons",
+ "type": "number",
+ "minimum": 0
+ },
+ "hullCost": {
+ "description": "Cost of the ship's hull",
+ "type": "integer",
+ "minimum": 1
+ },
+ "hullMass": {
+ "description": "Mass of the Ship hull only",
+ "type": "number",
+ "minimum": 1
+ },
+ "hullExplRes": {
+ "description": "Multiplier for explosive damage to hull",
+ "type": "number"
+ },
+ "hullKinRes": {
+ "description": "Multiplier for kinetic damage to hull",
+ "type": "number"
+ },
+ "hullThermRes": {
+ "description": "Multiplier for thermal damage to hull",
+ "type": "number"
+ },
+ "fuelCapacity": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "fullTankRange": {
+ "description": "Single Jump range with a full tank (unladenMass + fuel)",
+ "type": "number",
+ "minimum": 0
+ },
+ "ladenMass": {
+ "description": "Mass of the Ship + fuel + cargo (hull + all components + fuel tank + cargo capacity)",
+ "type": "number",
+ "minimum": 1
+ },
+ "ladenRange": {
+ "description": "Single Jump range with full cargo load, see ladenMass",
+ "type": "number",
+ "minimum": 0
+ },
+ "masslock": {
+ "description": "Mass Lock Factor of the Ship",
+ "type": "integer",
+ "minimum": 1
+ },
+ "shield": {
+ "description": "Shield strength in Mega Joules (Mj)",
+ "type": "number",
+ "minimum": 0
+ },
+ "shieldExplRes": {
+ "description": "Multiplier for explosive damage to shields",
+ "type": "number"
+ },
+ "shieldKinRes": {
+ "description": "Multiplier for kinetic damage to shields",
+ "type": "number"
+ },
+ "shieldThermRes": {
+ "description": "Multiplier for thermal damage to shields",
+ "type": "number"
+ },
+ "speed": {
+ "description": "Maximum speed of the ships (4 pips, straight-line)",
+ "type": "number",
+ "minimum": 1
+ },
+ "totalCost": {
+ "description": "Total cost of the loadout, including discounts",
+ "type": "number"
+ },
+ "unladenRange": {
+ "description": "Single Jump range when unladen, see unladenMass",
+ "type": "number",
+ "minimum": 0
+ },
+ "unladenMass": {
+ "description": "Mass of the Ship (hull + all components)",
+ "type": "number",
+ "minimum": 1
+ }
+ }
+ }
+ },
+ "definitions": {
+ "standardRatings": { "enum": ["A", "B", "C", "D", "E", "F", "G", "H"] },
+ "allRatings": { "enum": ["A", "B", "C", "D", "E", "F", "G", "H", "I" ] }
+ }
+}