diff --git a/ChangeLog.md b/ChangeLog.md index 1b7ac9b7..4e137a21 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,3 +4,7 @@ * Add distributor draw modifier to shield generators * Remove modifiers for sensors * 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 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..cda62b18 --- /dev/null +++ b/__tests__/fixtures/anaconda-test-detailed-export-v4.json @@ -0,0 +1,300 @@ +{ + "$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, + "totalDps": 97.74, + "totalEps": 22.71, + "totalHps": 677.29, + "agility": 2, + "baseShieldStrength": 350, + "baseArmour": 945, + "hullExplRes": 0, + "hullKinRes": 0, + "hullMass": 400, + "hullThermRes": 0, + "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, + "shieldExplRes": 0, + "shieldKinRes": 0, + "shieldThermRes": 0 + } +} 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..4c62b00c 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); 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/src/app/components/CostSection.jsx b/src/app/components/CostSection.jsx index 699f5d20..4c59dc25 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; diff --git a/src/app/shipyard/Constants.js b/src/app/shipyard/Constants.js index 566bcf06..39bfd57d 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', diff --git a/src/app/shipyard/Module.js b/src/app/shipyard/Module.js index d367cc9a..03044e07 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,7 +23,6 @@ export default class Module { for (let p in template) { this[p] = template[p]; } } } - this.mods = {}; } /** @@ -41,6 +40,10 @@ export default class Module { * @param {Number} value The value of the modification, as a decimal value where 1 is 100% */ setModValue(name, value) { + if (!this.mods) { + this.mods = {}; + } + if (value == null || value == 0) { delete this.mods[name]; } else { 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..00719f00 100644 --- a/src/app/shipyard/Serializer.js +++ b/src/app/shipyard/Serializer.js @@ -115,32 +115,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 +134,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 +146,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 +167,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; }; diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index d1451e7a..bb2f0558 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -486,7 +486,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; @@ -562,7 +561,7 @@ 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, @@ -579,10 +578,10 @@ export default class Ship { if (parts[3]) { const modstr = parts[3].replace(/-/g, '/'); 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(modstr, 'base64')), modifications); } catch (err) { // Could be out-of-date URL; ignore } @@ -600,7 +599,7 @@ export default class Ship { }, priorities, enabled, - mods + modifications ); }; @@ -1051,10 +1050,10 @@ export default class Ship { } /** - * 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(); @@ -1272,7 +1271,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/schemas/ship-loadout/4.json b/src/schemas/ship-loadout/4.json new file mode 100644 index 00000000..cf6876b9 --- /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": "Resistance of the hull to explosive attacks", + "type": "number" + }, + "hullKinRes": { + "description": "Resistance of the hull to kinetic attacks", + "type": "number" + }, + "hullThermRes": { + "description": "Resistance of the hull to thermal attacks", + "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 strengh in Mega Joules (Mj)", + "type": "number", + "minimum": 0 + }, + "shieldExplRes": { + "description": "Resistance of the shield to explosive attacks", + "type": "number" + }, + "shieldKinRes": { + "description": "Resistance of the shield to kinetic attacks", + "type": "number" + }, + "shieldThermRes": { + "description": "Resistance of the shield to thermal attacks", + "type": "number" + }, + "speed": { + "description": "Maximum speed of the ships (4 pips, straight-line)", + "type": "number", + "minimum": 1 + }, + "totalCost": { + "type": "integer", + "minimum": 1 + }, + "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" ] } + } +}