diff --git a/.gitignore b/.gitignore index 9d011721..1bbc61e9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ build *.log nginx.pid .idea -/bin \ No newline at end of file +/bin +env diff --git a/ChangeLog.md b/ChangeLog.md index 4d35d1c7..48bad6ab 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,45 @@ +#2.2.6 + * Add pitch/roll/yaw information + * Use combination of pitch, roll and yaw to provide a more useful agility metric + * Add movement summary to outfitting page + * Add standard internal class sizes to shipyard page + * Fix issue when importing Viper Mk IV + * Ensure ordering of all types of modules (standard, internal, utilities) is consistent + * Add rebuilds per bay information for fighter hangars + * Add ability to show military compartments + * Show module reinforcement package results in defence summary + * Use separate speed/rotation/acceleration multipliers for thrusters if available + * Obey restricted slot rules when adding all for internal slots + * Version URLs to handle changes to ship specifications over time + * Do not include disabled shield boosters in calculations + * Add 'Damage dealt' section + * Add 'Damage received' section + * Add 'Piercing' information to hardpoints + * Add 'Hardness' information to ship summary + * Add module copy functionality - drag module whilst holding 'alt' to copy + * Add base resistances to defence summary tooltip + * Update shield recovery/regeneration calculations + * Pin menu to top of page + * Switch to custom shortlink method to avoid google length limitations + * Ensure that information is not lost on narrow screens + * Do not lose ship selector selection on narrow screens + * Reinstate jump range graph + * Use coriolis-data 2.2.6: + * Update weapons with changed values for 2.2.03 + * Add individual pitch/roll/yaw statistics for each ship + * Remove old and meaningless agility stat + * Use sane order for multi-module JSON - coriolis can re-order as it sees fit when displaying modules + * Fix cost of fighter hangars + * Update Powerplay weapons with current statistics + * Add separate min/opt/max multipliers for enhanced thrusters for speed, acceleration and rotation + * Add module reinforcement packages + * Add military compartments + * Fix missing damage value for 2B dumbfires + * Update shield recharge rates + * Reduce hull mass of Viper to 50T + * Fix incorrect optimal mass value for 8A thrusters + * Add power draw for detailed surface scanner + #2.2.5 * Calculate rate of fire for multi-burst weapons * Add note to disable ghostery in error situation diff --git a/__tests__/fixtures/agility-data.json b/__tests__/fixtures/agility-data.json new file mode 100644 index 00000000..e78f4ea5 --- /dev/null +++ b/__tests__/fixtures/agility-data.json @@ -0,0 +1,30 @@ +{ + "adder": { + "t3": {"speed": 205, "boost": 298, "pitch": 35.37, "roll": 93.09, "yaw": 13.03}, + "t2": {"speed": 209, "boost": 304, "pitch": 36.06, "roll": 94.90, "yaw": 13.29}, + "t1": {"speed": 213, "boost": 310, "pitch": 36.80, "roll": 96.84, "yaw": 13.56}, + "t0": {"speed": 218, "boost": 317, "pitch": 37.70, "roll": 99.20, "yaw": 13.89}, + "t9": {"speed": 220, "boost": 321, "pitch": 38.08, "roll": 100.21, "yaw": 14.03}, + "t8": {"speed": 225, "boost": 327, "pitch": 38.86, "roll": 102.26, "yaw": 14.32}, + "t7": {"speed": 230, "boost": 334, "pitch": 39.69, "roll": 104.44, "yaw": 14.62}, + "t6": {"speed": 234, "boost": 340, "pitch": 40.41, "roll": 106.34, "yaw": 14.89}, + "t5": {"speed": 242, "boost": 351, "pitch": 41.71, "roll": 109.78, "yaw": 15.37} + }, + "eagle": { + "t2": {"speed": 223, "boost": 325, "pitch": 46.45, "roll": 111.48, "yaw": 16.72}, + "t1": {"speed": 229, "boost": 334, "pitch": 47.69, "roll": 114.46, "yaw": 17.17}, + "t0": {"speed": 235, "boost": 343, "pitch": 49.00, "roll": 117.60, "yaw": 17.64}, + "t9": {"speed": 239, "boost": 349, "pitch": 49.80, "roll": 119.53, "yaw": 17.93}, + "t8": {"speed": 243, "boost": 355, "pitch": 50.70, "roll": 121.69, "yaw": 18.25}, + "t7": {"speed": 248, "boost": 361, "pitch": 51.62, "roll": 123.89, "yaw": 18.58}, + "t6": {"speed": 252, "boost": 367, "pitch": 52.46, "roll": 125.91, "yaw": 18.89}, + "t5": {"speed": 259, "boost": 378, "pitch": 53.99, "roll": 129.56, "yaw": 19.43} + }, + "hauler": { + "t4": {"speed": 203, "boost": 305, "pitch": 36.61, "roll": 101.71, "yaw": 14.24}, + "t3": {"speed": 209, "boost": 314, "pitch": 37.63, "roll": 104.54, "yaw": 14.64}, + "t2": {"speed": 216, "boost": 324, "pitch": 38.89, "roll": 108.03, "yaw": 15.12}, + "t1": {"speed": 222, "boost": 333, "pitch": 39.97, "roll": 111.02, "yaw": 15.54}, + "t0": {"speed": 232, "boost": 348, "pitch": 41.76, "roll": 116.00, "yaw": 16.24} + } +} diff --git a/__tests__/fixtures/anaconda-test-detailed-export-v4.json b/__tests__/fixtures/anaconda-test-detailed-export-v4.json index c925f052..fbe1dcfa 100644 --- a/__tests__/fixtures/anaconda-test-detailed-export-v4.json +++ b/__tests__/fixtures/anaconda-test-detailed-export-v4.json @@ -223,6 +223,7 @@ }, null, null, + null, { "class": 4, "rating": "E", @@ -261,25 +262,27 @@ "topSpeed": 186.5, "boost": 240, "boostEnergy": 27, - "topBoost": 248.62, - "topSpeed": 186.46, + "topBoost": 249.34, + "topPitch": 25.97, + "topRoll": 62.34, + "topYaw": 10.39, + "topSpeed": 187.01, "totalCost": 882362058, - "totalDpe": 127.26, - "totalDps": 97.74, + "totalDpe": 142.68, + "totalDps": 101.13, "totalEps": 22.71, "totalHps": 677.29, "totalExplDpe": 0, "totalExplDps": 0, "totalExplSDps": 0, "totalHps": 33.62, - "totalKinDpe": 103.97, - "totalKinDps": 28.92, - "totalKinSDps": 21.23, - "totalSDps": 85.77, - "totalThermDpe": 23.29, - "totalThermDps": 68.82, - "totalThermSDps": 64.53, - "agility": 2, + "totalKinDpe": 116.29, + "totalKinDps": 16.01, + "totalKinSDps": 12.09, + "totalSDps": 89.99, + "totalThermDpe": 20.44, + "totalThermDps": 53.82, + "totalThermSDps": 53.82, "baseShieldStrength": 350, "baseArmour": 945, "hullExplRes": 0.78, @@ -288,21 +291,27 @@ "hullThermRes": 1.37, "masslock": 23, "pipSpeed": 0.14, + "pitch": 25, "moduleCostMultiplier": 1, + "modulearmour": 0, + "moduleprotection": 0, "fuelCapacity": 32, "cargoCapacity": 128, - "ladenMass": 1339.2, + "ladenMass": 1323.2, "armour": 2227.5, "baseArmour": 525, - "unladenMass": 1179.2, + "unladenMass": 1163.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, + "powerDeployed": 34.13, + "roll": 60, + "unladenRange": 18.74, + "yaw": 10, + "fullTankRange": 18.36, + "hardness": 65, + "ladenRange": 16.59, + "unladenFastestRange": 74.2, + "ladenFastestRange": 66.96, "maxJumpCount": 4, "shield": 833, "shieldCells": 1840, diff --git a/__tests__/fixtures/expected-builds.json b/__tests__/fixtures/expected-builds.json index 054de777..6cfc0f68 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": "A0p0tdFal8d8s8f4-----04040303430101.Iw1/kA==.Aw1/kA==.", + "Miner": "A0p5tdFal8d8s8f42l2l---040403451q0101.Iw1/kA==.Aw1/kA==.", + "Hopper": "A0p0tdFal8d0s8f41717---030302024300-.Iw1/kA==.Aw1/kA==." }, "type_7_transport": { - "Cargo": "0p0tiFfliddsdf5--------0505040403480101.Iw18aQ==.Aw18aQ==.", - "Miner": "0pdtiFflid8sdf5--2l2l----0505041v03450000.Iw18aQ==.Aw18aQ==." + "Cargo": "A0p0tiFfliddsdf5--------0505040403480101.Iw18aQ==.Aw18aQ==.", + "Miner": "A0pdtiFflid8sdf5--2l2l----0505041v03450000.Iw18aQ==.Aw18aQ==." }, "federal_dropship": { - "Cargo": "0pdtiFflnddsif4-1717------05040448020201.Iw18aQ==.Aw18aQ==." + "Cargo": "A0pdtiFflnddsif4-1717------05040448--020201.Iw18eQ==.Aw18eQ==." }, "asp": { - "Miner": "2pftfFflidfskf50s0s24242l2l---04054a1q02022o27.Iw18WQ==.Aw18WQ==." + "Miner": "A2pftfFflidfskf50s0s24242l2l---04054a1q02022o27.Iw18WQ==.Aw18WQ==." }, "imperial_clipper": { - "Cargo": "0p5tiFflndisnf4--0s0s----0605450302020101.Iw18aQ==.Aw18aQ==.", - "Dream": "2pktkFflndpskf40v0v0s0s0404040n4k5n5d2b29292o-.Iw18aQ==.Aw18aQ==.", - "Current": "0patkFflndfskf4----------------.Iw18aQ==.Aw18aQ==." + "Cargo": "A0p5tiFflndisnf4--0s0s----0605450302020101.Iw18aQ==.Aw18aQ==.", + "Dream": "A2pktkFflndpskf40v0v0s0s0404040n4k5n5d2b29292o-.AwRj4yWU1I==.CwBhCYy6YRigzLIA.", + "Current": "A0patkFflndfskf4----------------.AwRj4yWU1I==.CwBhCYy6YRigzLIA." }, "type_9_heavy": { - "Current": "0patsFklndnsif6---------0706054a0303020224.Iw18eQ==.Aw18eQ==." + "Current": "A0patsFklndnsif6---------0706054a0303020224.AwRj4yoo.EwBhEYy6dsg=." }, "python": { - "Cargo": "0patnFflidsssf5---------050505040448020201.Iw18eQ==.Aw18eQ==.", - "Miner": "0pktkFflidpspf50v0v0v2m2m0404--050505Ce4a1v02022o.Iw18eQ==.Aw18eQ==.", - "Dream": "2pptkFfliduspf50v0v0v27270404040m5n5n4f2d2d032t0201.Iw18eQ==.Aw18eQ==.", - "Missile": "0pttoFjljdystf52f2g2d2ePh----04044j03---002h.Iw18eQ==.Aw18eQ==." + "Cargo": "A0patnFflidsssf5---------050505040448020201.Iw18eQ==.Aw18eQ==.", + "Miner": "A0pktkFflidpspf50v0v0v2m2m0404--050505Ce4a1v02022o.Iw18eQ==.IwBhBYy6dkCYg===.", + "Dream": "A2pptkFfliduspf50v0v0v27270404040m5n5n4f2d2d032t0201.Iw1+gDBxA===.EwBhEYy6e0WEA===.", + "Missile": "A0pttoFjljdystf52f2g2d2ePh----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": "A4putpFklndzsuf52c0o0o0o1m1m0q0q0404040l0b0100004k5n5n112d2d04-0303326b.AwRj4yo5dyg=.MwBhCYy6duvARiA=.", + "Cargo": "A0patnFklndnsxf5----------------06050505040404-45030301.Iw18ZVA=.Aw18ZVA=.", + "Current": "A0patnFklndksxf5----------------06050505040404-03034524.Iw18ZVA=.Aw18ZVA=.", + "Explorer": "A0patnFklndksxf5--------0202------f7050505040s37-2f2i4524.AwRj4yVKJ9hA.AwhMIyumQRhEA===.", + "Test": "A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b.Iw18ZVA=.Aw18ZVA=." }, "diamondback_explorer": { - "Explorer": "0p0tdFfldddsdf5---0202--320p432i2f.Iw1/kA==.Aw1/kA==." + "Explorer": "A0p0tdFfldddsdf5---0202--320p432i2f.AwRj4zTI.AwiMIypI." }, "vulture": { - "Bounty Hunter": "3patcFalddksff31e1e0404-0l4a5d27662j.Iw19kA==.Aw19kA==." + "Bounty Hunter": "A3patcFalddksff31e1e0404-0l4a-5d27662j.AwRj4z2I.MwBhBYy6oJmAjLIA." }, "fer_de_lance": { - "Attack": "2pfthFalidpsff31r0s0s0s0s000404-04-4a-5d27-.Iw18aQ==.Aw18aQ==." + "Attack": "A2pfthFalidpsff31r0s0s0s0s000404-04-4a-5d27-.Iw18aQ==.CwBhrSu8EZyA." }, "eagle": { - "Figther": "4p0t5F5l3d5s5f20p0p24-40532j-.Iw1/EA==.Aw1/EA==." + "Figther": "A4p0t5F5l3d5s5f20p0p24-4053-2j-.Iw18kA==.Aw18kA==." } } diff --git a/__tests__/test-agility.js b/__tests__/test-agility.js new file mode 100644 index 00000000..e3bd87cb --- /dev/null +++ b/__tests__/test-agility.js @@ -0,0 +1,90 @@ +import Ship from '../src/app/shipyard/Ship'; +import { Ships } from 'coriolis-data/dist'; +import * as ModuleUtils from '../src/app/shipyard/ModuleUtils'; + +describe("Agility", function() { + + it("correctly calculates speed", function() { + let agilityData = require('./fixtures/agility-data'); + + for (let shipId in agilityData) { + for (let thrusterId in agilityData[shipId]) { + const thrusterData = agilityData[shipId][thrusterId]; + let shipData = Ships[shipId]; + let ship = new Ship(shipId, shipData.properties, shipData.slots); + ship.buildWith(shipData.defaults); + ship.use(ship.standard[1], ModuleUtils.findModule('t', thrusterId)); + + expect(Math.round(ship.topSpeed)).toBe(thrusterData.speed); + } + } + }); + + it("correctly calculates boost", function() { + let agilityData = require('./fixtures/agility-data'); + + for (let shipId in agilityData) { + for (let thrusterId in agilityData[shipId]) { + const thrusterData = agilityData[shipId][thrusterId]; + let shipData = Ships[shipId]; + let ship = new Ship(shipId, shipData.properties, shipData.slots); + ship.buildWith(shipData.defaults); + // Turn off internals to ensure we have enough power to boost + for (let internal in ship.internal) { + ship.internal[internal].enabled = 0; + } + ship.use(ship.standard[1], ModuleUtils.findModule('t', thrusterId)); + + expect(Math.round(ship.topBoost)).toBe(thrusterData.boost); + } + } + }); + + it("correctly calculates pitch", function() { + let agilityData = require('./fixtures/agility-data'); + + for (let shipId in agilityData) { + for (let thrusterId in agilityData[shipId]) { + const thrusterData = agilityData[shipId][thrusterId]; + let shipData = Ships[shipId]; + let ship = new Ship(shipId, shipData.properties, shipData.slots); + ship.buildWith(shipData.defaults); + ship.use(ship.standard[1], ModuleUtils.findModule('t', thrusterId)); + + expect(Math.round(ship.pitches[4] * 100) / 100).toBeCloseTo(thrusterData.pitch, 1); + } + } + }); + + it("correctly calculates roll", function() { + let agilityData = require('./fixtures/agility-data'); + + for (let shipId in agilityData) { + for (let thrusterId in agilityData[shipId]) { + const thrusterData = agilityData[shipId][thrusterId]; + let shipData = Ships[shipId]; + let ship = new Ship(shipId, shipData.properties, shipData.slots); + ship.buildWith(shipData.defaults); + ship.use(ship.standard[1], ModuleUtils.findModule('t', thrusterId)); + + expect(Math.round(ship.rolls[4] * 100) / 100).toBeCloseTo(thrusterData.roll, 1); + } + } + }); + + it("correctly calculates yaw", function() { + let agilityData = require('./fixtures/agility-data'); + + for (let shipId in agilityData) { + for (let thrusterId in agilityData[shipId]) { + const thrusterData = agilityData[shipId][thrusterId]; + let shipData = Ships[shipId]; + let ship = new Ship(shipId, shipData.properties, shipData.slots); + ship.buildWith(shipData.defaults); + ship.use(ship.standard[1], ModuleUtils.findModule('t', thrusterId)); + + expect(Math.round(ship.yaws[4] * 100) / 100).toBeCloseTo(thrusterData.yaw, 1); + } + } + }); +}); diff --git a/__tests__/test-import.js b/__tests__/test-import.js index dbfd0bfd..a6a44dc6 100644 --- a/__tests__/test-import.js +++ b/__tests__/test-import.js @@ -142,12 +142,12 @@ 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?code=4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA%3D%3D.CwBhCYzBGW9qCTSqs5xA.&bn=Test%20My%20Ship'); + expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/anaconda?code=A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b.AwRj4zNLaA%3D%3D.CwBhCYzBGW9qCTSqq5xA.&bn=Test%20My%20Ship'); }); it('catches an invalid build', function() { const importData = require('./fixtures/anaconda-test-detailed-export-v3'); - pasteText(JSON.stringify(importData).replace('components', 'comps')); + pasteText(JSON.stringify(importData).replace('references', 'refs')); expect(modal.state.importValid).toBeFalsy(); expect(modal.state.errorMsg).toEqual('Anaconda Build "Test My Ship": Invalid data'); @@ -167,7 +167,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?code=4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA%3D%3D.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2P8xwAEf0GE2AtmBob%2F%2FwFvM%2BjKEgAAAA%3D%3D&bn=Test%20My%20Ship'); + expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/anaconda?code=A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b.AwRj4zNLaA%3D%3D.CwBhCYzBGW9qCTSqq5xA.H4sIAAAAAAAAA2MUe8HMwPD%2FPwMcAABTINwTEgAAAA%3D%3D&bn=Test%20My%20Ship'); }); }); @@ -184,7 +184,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/asp?code=0pftiFflfddsnf5------020202033c044002v62f2i.AwRj4yvI.CwRgDBldHnJA.H4sIAAAAAAAAA2P858DAwPCXEUhwHPvx%2F78YG5AltB7I%2F8%2F0TwImJboDSPJ%2F%2B%2Ff%2Fv%2FKlX%2F%2F%2Fi3AwMTBIfARK%2FGf%2BJwVSxArStVAYqOjvz%2F%2F%2FJVo5GRhE2IBc4SKQSSz%2FDGEmCa398P8%2F%2F2%2BgTf%2F%2FAwDFxwtofAAAAA%3D%3D&bn=Multi-purpose%20Asp%20Explorer'); + expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/asp?code=A0pftiFflfddsnf5------020202033c044002v62f2i.AwRj4yvI.CwRgDBldHnJA.H4sIAAAAAAAAA2P858DAwPCXEUhwHPvx%2F78YG5AltB7I%2F8%2F0TwImJboDSPJ%2F%2B%2Ff%2Fv%2FKlX%2F%2F%2Fi3AwMTBIfARK%2FGf%2BJwVSxArStVAYqOjvz%2F%2F%2FJVo5GRhE2IBc4SKQSSz%2FDGEmCa398P8%2F%2F2%2BgTf%2F%2FAwDFxwtofAAAAA%3D%3D&bn=Multi-purpose%20Asp%20Explorer'); }); it('imports a valid v4 build with modifications', function() { @@ -196,7 +196,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/imperial_courier?code=0patzF5l0das8f31a1a270202000e402t0101-2f.AwRj4zKA.CwRgDBldLiQ%3D.H4sIAAAAAAAAA12OP0tCYRjFj9fuVbvF1du9ekkT8s%2FkIg4NElyIBBd321yaGvwUQTS3N7UFfYygIT9EoyQUJA36ns47XJCWA%2B%2Fz%2Bz3Pe3ImBbDNKaqNPSBoGrL4ngfomKpFGiJ%2BLgHteR1IPjxJT5pF11uSeXNsJVcRfgdC92syWUuK0iMdKZqrjJ%2F0aoA71lJ5oKf38knWcCiptCPdhJIerdS00vlK0qktlqoj983UmqqHjQ33VsW8eazFmaTyULP2hQ4lX8LBme6g%2F6v0TTdbxJ2KhdEIaCw15MF%2FNB0L%2BS2hwEwyFM8KgP%2BqEpWWA3Qu9Z3z9kPWHzakt7Dt%2BAeD7ghSTgEAAA%3D%3D&bn=Multi-purpose%20Imperial%20Courier'); + expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/imperial_courier?code=A0patzF5l0das8f31a1a270202000e402t0101-2f.AwRj4zKA.CwRgDBldLiQ%3D.H4sIAAAAAAAAA12OP0tCYRjFj9fuVbvF1du9ekkT8s%2FkIg4NElyIBBd321yaGvwUQTS3N7UFfYygIT9EoyQUJA36ns47XJCWA%2B%2Fz%2Bz3Pe3ImBbDNKaqNPSBoGrL4ngfomKpFGiJ%2BLgHteR1IPjxJT5pF11uSeXNsJVcRfgdC92syWUuK0iMdKZqrjJ%2F0aoA71lJ5oKf38knWcCiptCPdhJIerdS00vlK0qktlqoj983UmqqHjQ33VsW8eazFmaTyULP2hQ4lX8LBme6g%2F6v0TTdbxJ2KhdEIaCw15MF%2FNB0L%2BS2hwEwyFM8KgP%2BqEpWWA3Qu9Z3z9kPWHzakt7Dt%2BAeD7ghSTgEAAA%3D%3D&bn=Multi-purpose%20Imperial%20Courier'); }); }); @@ -238,7 +238,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/federal_corvette?code=2putsFklndzsxf50x0x7l28281919040404040402020l06p05sf63c5ifrv66g2f.AwRj4zNaKA%3D%3D.CwRgDBldUExuBiQqA%3D%3D%3D.H4sIAAAAAAAAA02Svy9DURTHT1vvtfoat30eXlvV0ufXQmLAIDHSRDcJAzHV1PgDDAaJpVbxF0gYKhFiEFuXTgbCIsKfYJCItHWP75E83vLNue%2F7Od977zs3pBeJ6DsE6TcNIlVn5lgFSw7rfrEikL6mSVS0HSL3MgxoqM3sTGtm%2BxA2R3RGSLSTfWzD32kxu043kVNFDxt6wU8ajVpEY7coh5uARrYR0n3aYY4%2FY6lmkc4xveafqZOHpHejRMb9J7NZQqN9Ascto4fjet0P7iQgRhV7mo5LlLtAUnIe34rVDaKBF9AThUJhla3%2FHqMRB76XBV7v8vEvOOoGx%2BJEgKz9BgvZEHJOyHNUakYujUuSW8KxWOkl%2F%2BzuMsR6QpkS8URUTYKTAagNta4EEvFE1INAqQD0IdCdQCKeiOoBk9%2BPYU87QL7i2tajkITKk0odSFxvAJrClawX%2BCkRT0RZYNjV5b%2BRbyLaOpMkafJa%2BBgufjFnjxBnvgFxKvgBnNYlP7jwiXcRnYQ%2F%2FoRlqCnTHAz41xha9F78CNahGXk8eZ3z%2FcyWjJcg7goeU%2BJdZsw%2FFW2pAaMCAAA%3D&bn=Imported%20Federal%20Corvette'); + expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/federal_corvette?code=A2putsFklndzsxf50x0x7l28281919040404040402020l06p05sf63c5ifr--v66g2f.AwRj4zNaqA%3D%3D.CwRgDBldUExuBiIlUA%3D%3D.H4sIAAAAAAAAA02SO0sDQRSFbxJ389jgJOsaN%2FGVmPXVKKRQC8FSA9oJWihWWgV%2FgIWFYBNb8RcIWiiIYiF2NqksIqaRoD%2FBQghB41zPFVa3OdzZ890zM3snpBeI6DsEyZgGkbpg5tg2lhzWGbEikN6aSVS0HSL3Ogxo6IvZmdbM9hFsjuickGgn%2B8SGv%2FvJ7DpxIqeCHjb0vJ80GrWIxu5RFmqARnYQEj%2FrMCdesFQzSOeYXvPP1BmGZPeiREa9xWyW0WifwnFX0MMJve4Hd5IQo4I9TcclGrxCUmoO34qVDaK%2BJuiJfD6%2FytZ%2Fj%2FGQAL7fD%2Fyiy8fbcNQdjsXJAFn9DRbyQchZIS9RqZJcGpckt4xjsdKL%2FtndJYjVQJkW8URUVYJTAegLat0IJOKJqCeB0gHoQ6BHgUQ8EdUNZvgghj3tAPmKa1vPQhIqTyp1KHE9AWgKV7Ka8NMinoiywLCrV%2F5Gvolo61ySpMn7xMdwsc3cf4w48w2Is40fwFld9oPzLbyL6CT88QaWoZpMcyDg32Jo0br4EaxDJXk8BT3o%2B7ktGS9B3GU8puS7zJh%2FAHGMT2qjAgAA&bn=Imported%20Federal%20Corvette'); }); it('imports a valid v4 build', function() { @@ -250,7 +250,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/beluga?code=0pktsFplCdpsnf70t0t2727270004040404043c4fmimlmm04mc0iv62i2f.AwRj4yukg%3D%3D%3D.CwRgDBldHi8IUA%3D%3D.H4sIAAAAAAAAA2P8Z8%2FAwPCXEUiIKTMxMPCv%2F%2Ff%2FP8cFIPGf6Z8YTEr0GjMDg%2FJWICERBOTzn%2Fn7%2F7%2FIO5Ai5n9SIEWsQEIoSxAolfbt%2F3%2BJPk4GBhE7YQYGYVmgcuVnf4Aq%2FwMAIrEcGGsAAAA%3D&bn=Imported%20Beluga%20Liner'); + expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/beluga?code=A0pktsFplCdpsnf70t0t2727270004040404043c4fmimlmm04mc0iv62i2f.AwRj4yukg%3D%3D%3D.CwRgDBldHi8IUA%3D%3D.H4sIAAAAAAAAA2P8Z8%2FAwPCXEUiIKTMxMPCv%2F%2Ff%2FP8cFIPGf6Z8YTEr0GjMDg%2FJWICERBOTzn%2Fn7%2F7%2FIO5Ai5n9SIEWsQEIoSxAolfbt%2F3%2BJPk4GBhE7YQYGYVmgcuVnf4Aq%2FwMAIrEcGGsAAAA%3D&bn=Imported%20Beluga%20Liner'); }); }); diff --git a/package.json b/package.json index 3aeb579c..9e99f9a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coriolis_shipyard", - "version": "2.2.5", + "version": "2.2.6", "repository": { "type": "git", "url": "https://github.com/EDCD/coriolis" @@ -31,6 +31,7 @@ "jsx" ], "automock": true, + "bail": false, "unmockedModulePathPatterns": [ "/node_modules/lodash", "/node_modules/react", diff --git a/src/app/components/AvailableModulesMenu.jsx b/src/app/components/AvailableModulesMenu.jsx index f72a475d..4d3697bd 100644 --- a/src/app/components/AvailableModulesMenu.jsx +++ b/src/app/components/AvailableModulesMenu.jsx @@ -93,8 +93,10 @@ export default class AvailableModulesMenu extends TranslatedComponent { let prevClass = null, prevRating = null; let elems = []; - for (let i = 0; i < modules.length; i++) { - let m = modules[i]; + const sortedModules = modules.sort(this._moduleOrder); + + for (let i = 0; i < sortedModules.length; i++) { + let m = sortedModules[i]; let mount = null; let disabled = m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass; let active = mountedModule && mountedModule.id === m.id; @@ -126,7 +128,7 @@ export default class AvailableModulesMenu extends TranslatedComponent { case 'T': mount = ; break; } - if (i > 0 && modules.length > 3 && m.class != prevClass && (m.rating != prevRating || m.mount) && m.grp != 'pa') { + if (i > 0 && sortedModules.length > 3 && m.class != prevClass && (m.rating != prevRating || m.mount) && m.grp != 'pa') { elems.push(
); } @@ -201,6 +203,46 @@ export default class AvailableModulesMenu extends TranslatedComponent { this.context.tooltip(); } + /** + * Order two modules suitably for display in module selection + * @param {Object} a the first module + * @param {Object} b the second module + * @return {int} -1 if the first module should go first, 1 if the second module should go first + */ + _moduleOrder(a, b) { + // Named modules go last + if (!a.name && b.name) { + return -1; + } + if (a.name && !b.name) { + return 1; + } + // Class ordered from highest (8) to lowest (1) + if (a.class < b.class) { + return 1; + } + if (a.class > b.class) { + return -1; + } + // Mount type, if applicable + if (a.mount && b.mount && a.mount !== b.mount) { + if (a.mount === 'F' || (a.mount === 'G' && b.mount === 'T')) { + return -1; + } else { + return 1; + } + } + // Rating ordered from lowest (E) to highest (A) + if (a.rating < b.rating) { + return 1; + } + if (a.rating > b.rating) { + return -1; + } + // Do not attempt to order by name at this point, as that mucks up the order of armour + return 0; + } + /** * Scroll to mounted (if it exists) module group on mount */ diff --git a/src/app/components/DamageDealt.jsx b/src/app/components/DamageDealt.jsx new file mode 100644 index 00000000..01ba84b2 --- /dev/null +++ b/src/app/components/DamageDealt.jsx @@ -0,0 +1,244 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import ShipSelector from './ShipSelector'; +import { nameComparator } from '../utils/SlotFunctions'; +import { CollapseSection, ExpandSection, MountFixed, MountGimballed, MountTurret } from './SvgIcons'; + +/** + * Generates an internationalization friendly weapon comparator that will + * sort by specified property (if provided) then by name/group, class, rating + * @param {function} translate Translation function + * @param {function} propComparator Optional property comparator + * @param {boolean} desc Use descending order + * @return {function} Comparator function for names + */ +export function weaponComparator(translate, propComparator, desc) { + return (a, b) => { + if (!desc) { // Flip A and B if ascending order + let t = a; + a = b; + b = t; + } + + // If a property comparator is provided use it first + let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a, b); + + if (diff) { + return diff; + } + + // Property matches so sort by name / group, then class, rating + if (a.name === b.name && a.grp === b.grp) { + if(a.class == b.class) { + return a.rating > b.rating ? 1 : -1; + } + return a.class - b.class; + } + + return nameComparator(translate, a, b); + }; +} + +/** + * Damage against a selected ship + */ +export default class DamageDealt extends TranslatedComponent { + static PropTypes = { + ship: React.PropTypes.object.isRequired, + code: React.PropTypes.string.isRequired + }; + + static DEFAULT_AGAINST = Ships['anaconda']; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + + this._sort = this._sort.bind(this); + this._onShipChange = this._onShipChange.bind(this); + this._onCollapseExpand = this._onCollapseExpand.bind(this); + + this.state = { + predicate: 'n', + desc: true, + against: DamageDealt.DEFAULT_AGAINST, + expanded: false + }; + } + + /** + * Set the initial weapons state + */ + componentWillMount() { + const weapons = this._calcWeapons(this.props.ship, this.state.against); + this.setState({ weapons }); + } + + /** + * Set the updated weapons state if our ship changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + * @return {boolean} Returns true if the component should be rerendered + */ + componentWillReceiveProps(nextProps, nextContext) { + if (nextProps.code != this.props.code) { + const weapons = this._calcWeapons(this.props.ship, this.state.against); + this.setState({ weapons }); + } + return true; + } + + /** + * Calculate the damage dealt by a ship + * @param {Object} ship The ship which will deal the damage + * @param {Object} against The ship against which damage will be dealt + * @return {boolean} Returns the per-weapon damage + */ + _calcWeapons(ship, against) { + let weapons = []; + + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].m) { + const m = ship.hardpoints[i].m; + const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`; + const effectiveness = m.getPiercing() >= against.properties.hardness ? 1 : m.getPiercing() / against.properties.hardness; + const effectiveDps = m.getDps() * effectiveness; + const effectiveSDps = m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) * effectiveness : effectiveDps; + + weapons.push({ id: i, + mount: m.mount, + name: m.name || m.grp, + classRating, + effectiveDps, + effectiveSDps, + effectiveness }); + } + } + + return weapons; + } + + /** + * Triggered when the collapse or expand icons are clicked + */ + _onCollapseExpand() { + this.setState({ expanded: !this.state.expanded }); + } + + /** + * Triggered when the ship we compare against changes + * @param {string} s the new ship ID + */ + _onShipChange(s) { + const against = Ships[s]; + const weapons = this._calcWeapons(this.props.ship, against); + this.setState({ against, weapons }); + } + + /** + * Set the sort order and sort + * @param {string} predicate Sort predicate + */ + _sortOrder(predicate) { + let desc = this.state.desc; + + if (predicate == this.state.predicate) { + desc = !desc; + } else { + desc = true; + } + + this._sort(this.props.ship, predicate, desc); + this.setState({ predicate, desc }); + } + + /** + * Sorts the weapon list + * @param {Ship} ship Ship instance + * @param {string} predicate Sort predicate + * @param {Boolean} desc Sort order descending + */ + _sort(ship, predicate, desc) { + let comp = weaponComparator.bind(null, this.context.language.translate); + + switch (predicate) { + case 'n': comp = comp(null, desc); break; + case 'edps': comp = comp((a, b) => a.effectiveDps - b.effectiveDps, desc); break; + case 'esdps': comp = comp((a, b) => a.effectiveSDps - b.effectiveSDps, desc); break; + case 'e': comp = comp((a, b) => a.effectiveness - b.effectiveness, desc); break; + } + + this.state.weapons.sort(comp); + } + + /** + * Render individual rows for hardpoints + * @param {Function} translate Translate function + * @param {Object} formats Localised formats map + * @return {array} The individual rows + * + */ + _renderRows(translate, formats) { + const { termtip, tooltip } = this.context; + + let rows = []; + + if (this.state.weapons) { + for (let i = 0; i < this.state.weapons.length; i++) { + const weapon = this.state.weapons[i]; + + rows.push( + + {weapon.mount == 'F' ? : null} + {weapon.mount == 'G' ? : null} + {weapon.mount == 'T' ? : null} + {weapon.classRating} {translate(weapon.name)} + + {formats.round1(weapon.effectiveDps)} + {formats.round1(weapon.effectiveSDps)} + {formats.pct(weapon.effectiveness)} + ); + } + } + + return rows; + } + + /** + * Render damage dealt + * @return {React.Component} contents + */ + render() { + const { language, tooltip, termtip } = this.context; + const { formats, translate } = language; + const { expanded } = this.state; + + const sortOrder = this._sortOrder; + const onCollapseExpand = this._onCollapseExpand; + + return ( + +

{translate('damage dealt against')} {expanded ? : }

+ {expanded ? + + + + + + + + + + + + {this._renderRows(translate, formats)} + +
{translate('weapon')}{translate('effective dps')}{translate('effective sdps')}{translate('effectiveness')}
: null } +
+ ); + } +} diff --git a/src/app/components/DamageReceived.jsx b/src/app/components/DamageReceived.jsx new file mode 100644 index 00000000..f3d5e637 --- /dev/null +++ b/src/app/components/DamageReceived.jsx @@ -0,0 +1,274 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Modules } from 'coriolis-data/dist'; +import { nameComparator } from '../utils/SlotFunctions'; +import { CollapseSection, ExpandSection, MountFixed, MountGimballed, MountTurret } from './SvgIcons'; +import Module from '../shipyard/Module'; + +/** + * Generates an internationalization friendly weapon comparator that will + * sort by specified property (if provided) then by name/group, class, rating + * @param {function} translate Translation function + * @param {function} propComparator Optional property comparator + * @param {boolean} desc Use descending order + * @return {function} Comparator function for names + */ +export function weaponComparator(translate, propComparator, desc) { + return (a, b) => { + if (!desc) { // Flip A and B if ascending order + let t = a; + a = b; + b = t; + } + + // If a property comparator is provided use it first + let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a, b); + + if (diff) { + return diff; + } + + // Property matches so sort by name / group, then class, rating + if (a.name === b.name && a.grp === b.grp) { + if(a.class == b.class) { + return a.rating > b.rating ? 1 : -1; + } + return a.class - b.class; + } + + return nameComparator(translate, a, b); + }; +} + +/** + * Damage received by a selected ship + */ +export default class DamageReceived extends TranslatedComponent { + static PropTypes = { + ship: React.PropTypes.object.isRequired, + code: React.PropTypes.string.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + + this._sort = this._sort.bind(this); + this._onCollapseExpand = this._onCollapseExpand.bind(this); + + this.state = { + predicate: 'n', + desc: true, + expanded: false + }; + } + + /** + * Set the initial weapons state + */ + componentWillMount() { + this.setState({ weapons: this._calcWeapons(this.props.ship) }); + } + + /** + * Set the updated weapons state + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + * @return {boolean} Returns true if the component should be rerendered + */ + componentWillReceiveProps(nextProps, nextContext) { + this.setState({ weapons: this._calcWeapons(nextProps.ship) }); + return true; + } + + /** + * Calculate the damage received by a ship + * @param {Object} ship The ship which will receive the damage + * @return {boolean} Returns the per-weapon damage + */ + _calcWeapons(ship) { + let weapons = []; + + for (let grp in Modules.hardpoints) { + if (Modules.hardpoints[grp][0].damage && Modules.hardpoints[grp][0].type) { + for (let mId in Modules.hardpoints[grp]) { + const m = new Module(Modules.hardpoints[grp][mId]); + const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`; + + // Base DPS + const baseDps = m.getDps(); + const baseSDps = m.getClip() ? (m.getClip() * baseDps / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : baseDps; + + // Effective DPS taking in to account shield resistance + let effectivenessShields = 0; + if (m.getDamageType().indexOf('E') != -1) { + effectivenessShields += ship.shieldExplRes; + } + if (m.getDamageType().indexOf('K') != -1) { + effectivenessShields += ship.shieldKinRes; + } + if (m.getDamageType().indexOf('T') != -1) { + effectivenessShields += ship.shieldThermRes; + } + effectivenessShields /= m.getDamageType().length; + // Plasma accelerators deal absolute damage + if (m.grp == 'pa') effectivenessShields = 1; + const effectiveDpsShields = baseDps * effectivenessShields; + const effectiveSDpsShields = baseSDps * effectivenessShields; + + // Effective DPS taking in to account hull hardness and resistance + let effectivenessHull = 0; + if (m.getDamageType().indexOf('E') != -1) { + effectivenessHull += ship.hullExplRes; + } + if (m.getDamageType().indexOf('K') != -1) { + effectivenessHull += ship.hullKinRes; + } + if (m.getDamageType().indexOf('T') != -1) { + effectivenessHull += ship.hullThermRes; + } + effectivenessHull /= m.getDamageType().length; + // Plasma accelerators deal absolute damage (but could be reduced by hardness) + if (m.grp == 'pa') effectivenessHull = 1; + effectivenessHull *= Math.min(m.getPiercing() / ship.hardness, 1); + const effectiveDpsHull = baseDps * effectivenessHull; + const effectiveSDpsHull = baseSDps * effectivenessHull; + + weapons.push({ id: m.id, + classRating, + name: m.name || m.grp, + mount: m.mount, + effectiveDpsShields, + effectiveSDpsShields, + effectivenessShields, + effectiveDpsHull, + effectiveSDpsHull, + effectivenessHull }); + } + } + } + + return weapons; + } + + /** + * Triggered when the collapse or expand icons are clicked + */ + _onCollapseExpand() { + this.setState({ expanded: !this.state.expanded }); + } + + /** + * Set the sort order and sort + * @param {string} predicate Sort predicate + */ + _sortOrder(predicate) { + let desc = this.state.desc; + + if (predicate == this.state.predicate) { + desc = !desc; + } else { + desc = true; + } + + this._sort(this.props.ship, predicate, desc); + this.setState({ predicate, desc }); + } + + /** + * Sorts the weapon list + * @param {Ship} ship Ship instance + * @param {string} predicate Sort predicate + * @param {Boolean} desc Sort order descending + */ + _sort(ship, predicate, desc) { + let comp = weaponComparator.bind(null, this.context.language.translate); + + switch (predicate) { + case 'n': comp = comp(null, desc); break; + case 'edpss': comp = comp((a, b) => a.effectiveDpsShields - b.effectiveDpsShields, desc); break; + case 'esdpss': comp = comp((a, b) => a.effectiveSDpsShields - b.effectiveSDpsShields, desc); break; + case 'es': comp = comp((a, b) => a.effectivenessShields - b.effectivenessShields, desc); break; + case 'edpsh': comp = comp((a, b) => a.effectiveDpsHull - b.effectiveDpsHull, desc); break; + case 'esdpsh': comp = comp((a, b) => a.effectiveSDpsHull - b.effectiveSDpsHull, desc); break; + case 'eh': comp = comp((a, b) => a.effectivenessHull - b.effectivenessHull, desc); break; + } + + this.state.weapons.sort(comp); + } + + /** + * Render individual rows for weapons + * @param {Function} translate Translate function + * @param {Object} formats Localised formats map + * @return {array} The individual rows + * + */ + _renderRows(translate, formats) { + const { termtip, tooltip } = this.context; + + let rows = []; + + for (let i = 0; i < this.state.weapons.length; i++) { + const weapon = this.state.weapons[i]; + rows.push( + + {weapon.mount == 'F' ? : null} + {weapon.mount == 'G' ? : null} + {weapon.mount == 'T' ? : null} + {weapon.classRating} {translate(weapon.name)} + + {formats.round1(weapon.effectiveDpsShields)} + {formats.round1(weapon.effectiveSDpsShields)} + {formats.pct(weapon.effectivenessShields)} + {formats.round1(weapon.effectiveDpsHull)} + {formats.round1(weapon.effectiveSDpsHull)} + {formats.pct(weapon.effectivenessHull)} + ); + } + return rows; + } + + /** + * Render damage received + * @return {React.Component} contents + */ + render() { + const { language, tooltip, termtip } = this.context; + const { formats, translate } = language; + const { expanded } = this.state; + + const sortOrder = this._sortOrder; + const onCollapseExpand = this._onCollapseExpand; + + return ( + +

{translate('damage received by')} {expanded ? : }

+ {expanded ? + + + + + + + + + + + + + + + + + + {this._renderRows(translate, formats)} + +
{translate('weapon')}{translate('against shields')}{translate('against hull')}
{translate('DPS')}{translate('SDPS')}{translate('effectiveness')}{translate('DPS')}{translate('SDPS')}{translate('effectiveness')}
: null } +
+ ); + } +} diff --git a/src/app/components/DefenceSummary.jsx b/src/app/components/DefenceSummary.jsx index d9ec54a9..a20b1bb5 100644 --- a/src/app/components/DefenceSummary.jsx +++ b/src/app/components/DefenceSummary.jsx @@ -29,6 +29,8 @@ export default class DefenceSummary extends TranslatedComponent { let { formats, translate, units } = language; let hide = tooltip.bind(null, null); + const shieldGenerator = ship.findShieldGenerator(); + return (

{translate('defence summary')}

@@ -48,9 +50,18 @@ export default class DefenceSummary extends TranslatedComponent { {ship.shield ? {translate('damage from')} - {formats.pct1(ship.shieldExplRes || 1)} - {formats.pct1(ship.shieldKinRes || 1)} - {formats.pct1(ship.shieldThermRes || 1)} + +   + {formats.pct1(ship.shieldExplRes || 1)} + + +   + {formats.pct1(ship.shieldKinRes || 1)} + + +   + {formats.pct1(ship.shieldThermRes || 1)} + : null } { ship.shield && ship.shieldCells ? @@ -63,10 +74,29 @@ export default class DefenceSummary extends TranslatedComponent { {translate('damage from')} - {formats.pct1(ship.hullExplRes || 1)} - {formats.pct1(ship.hullKinRes || 1)} - {formats.pct1(ship.hullThermRes || 1)} + +   + {formats.pct1(ship.hullExplRes || 1)} + +   + {formats.pct1(ship.hullKinRes || 1)} + + +   + {formats.pct1(ship.hullThermRes || 1)} + + + {ship.modulearmour > 0 ? + +

{translate('module armour')}: {formats.int(ship.modulearmour)}

+ : null } + + {ship.moduleprotection > 0 ? + + {translate('internal protection')} {formats.pct1(ship.moduleprotection)} + {translate('external protection')} {formats.pct1(ship.moduleprotection / 2)} + : null }
diff --git a/src/app/components/HardpointSlot.jsx b/src/app/components/HardpointSlot.jsx index 6b4b28eb..cc1d80ac 100644 --- a/src/app/components/HardpointSlot.jsx +++ b/src/app/components/HardpointSlot.jsx @@ -46,7 +46,7 @@ export default class HardpointSlot extends Slot { // Modifications tooltip shows blueprint and grade, if available let modTT = translate('modified'); - if (m && m.blueprint) { + if (m && m.blueprint && m.blueprint.name) { modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; if (m.blueprint.special && m.blueprint.special.id) { modTT += ', ' + translate(m.blueprint.special.name); @@ -76,6 +76,7 @@ export default class HardpointSlot extends Slot { { 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.getPiercing() ?
{translate('piercing')}: {formats.int(m.getPiercing())}
: 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 } diff --git a/src/app/components/HardpointsSlotSection.jsx b/src/app/components/HardpointsSlotSection.jsx index 8eb212da..51bb79d4 100644 --- a/src/app/components/HardpointsSlotSection.jsx +++ b/src/app/components/HardpointsSlotSection.jsx @@ -133,6 +133,10 @@ export default class HardpointsSlotSection extends SlotSection {
  • +
    {translate('pa')}
    +
      +
    • {translate('pa')}
    • +
    {translate('nl')}
    • {translate('nl')}
    • diff --git a/src/app/components/InternalSlot.jsx b/src/app/components/InternalSlot.jsx index 2f8fb206..1bb7bf4b 100644 --- a/src/app/components/InternalSlot.jsx +++ b/src/app/components/InternalSlot.jsx @@ -28,7 +28,7 @@ export default class InternalSlot extends Slot { // Modifications tooltip shows blueprint and grade, if available let modTT = translate('modified'); - if (m && m.blueprint) { + if (m && m.blueprint && m.blueprint.name) { modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; } @@ -43,6 +43,7 @@ export default class InternalSlot extends Slot { { m.getMaxMass() ?
      {translate('max mass')}: {formats.int(m.getMaxMass())}{u.T}
      : null } { m.bins ?
      {m.bins} {translate('bins')}
      : null } { m.bays ?
      {translate('bays')}: {m.bays}
      : null } + { m.rebuildsperbay ?
      {translate('rebuildsperbay')}: {m.rebuildsperbay}
      : null } { 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 } @@ -58,10 +59,13 @@ export default class InternalSlot extends Slot { { m.rangeLS === null ?
      ∞{u.Ls}
      : null } { m.rangeRating ?
      {translate('range')}: {m.rangeRating}
      : null } { m.getHullReinforcement() ?
      +{formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost') / 10000)} {translate('armour')}
      : null } + { m.getProtection() ?
      {formats.rPct(m.getProtection())} {translate('protection')}
      : 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.getRegenerationRate() ?
      {translate('regen')}: {formats.round1(m.getRegenerationRate())}{u.ps}
      : null } + { m.getBrokenRegenerationRate() ?
      {translate('brokenregen')}: {formats.round1(m.getBrokenRegenerationRate())}{u.ps}
      : null } { m && validMods.length > 0 ?
      : null } diff --git a/src/app/components/InternalSlotSection.jsx b/src/app/components/InternalSlotSection.jsx index ae6b8bec..af60b841 100644 --- a/src/app/components/InternalSlotSection.jsx +++ b/src/app/components/InternalSlotSection.jsx @@ -22,6 +22,7 @@ export default class InternalSlotSection extends SlotSection { this._fillWithCargo = this._fillWithCargo.bind(this); this._fillWithCells = this._fillWithCells.bind(this); this._fillWithArmor = this._fillWithArmor.bind(this); + this._fillWithModuleReinforcementPackages = this._fillWithModuleReinforcementPackages.bind(this); this._fillWithFuelTanks = this._fillWithFuelTanks.bind(this); this._fillWithLuxuryCabins = this._fillWithLuxuryCabins.bind(this); this._fillWithFirstClassCabins = this._fillWithFirstClassCabins.bind(this); @@ -46,7 +47,7 @@ export default class InternalSlotSection extends SlotSection { let clobber = event.getModifierState('Alt'); let ship = this.props.ship; ship.internal.forEach((slot) => { - if (clobber || !slot.m) { + if ((clobber || !slot.m) && (!slot.eligible || slot.eligible.cr)) { ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E')); } }); @@ -62,7 +63,7 @@ export default class InternalSlotSection extends SlotSection { let clobber = event.getModifierState('Alt'); let ship = this.props.ship; ship.internal.forEach((slot) => { - if (clobber || !slot.m) { + if ((clobber || !slot.m) && (!slot.eligible || slot.eligible.ft)) { ship.use(slot, ModuleUtils.findInternal('ft', slot.maxClass, 'C')); } }); @@ -78,7 +79,7 @@ export default class InternalSlotSection extends SlotSection { let clobber = event.getModifierState('Alt'); let ship = this.props.ship; ship.internal.forEach((slot) => { - if (clobber || !slot.m) { + if ((clobber || !slot.m) && (!slot.eligible || slot.eligible.pcq)) { ship.use(slot, ModuleUtils.findInternal('pcq', Math.min(slot.maxClass, 6), 'B')); // Passenger cabins top out at 6 } }); @@ -94,7 +95,7 @@ export default class InternalSlotSection extends SlotSection { let clobber = event.getModifierState('Alt'); let ship = this.props.ship; ship.internal.forEach((slot) => { - if (clobber || !slot.m) { + if ((clobber || !slot.m) && (!slot.eligible || slot.eligible.pcm)) { ship.use(slot, ModuleUtils.findInternal('pcm', Math.min(slot.maxClass, 6), 'C')); // Passenger cabins top out at 6 } }); @@ -110,7 +111,7 @@ export default class InternalSlotSection extends SlotSection { let clobber = event.getModifierState('Alt'); let ship = this.props.ship; ship.internal.forEach((slot) => { - if (clobber || !slot.m) { + if ((clobber || !slot.m) && (!slot.eligible || slot.eligible.pci)) { ship.use(slot, ModuleUtils.findInternal('pci', Math.min(slot.maxClass, 6), 'D')); // Passenger cabins top out at 6 } }); @@ -126,7 +127,7 @@ export default class InternalSlotSection extends SlotSection { let clobber = event.getModifierState('Alt'); let ship = this.props.ship; ship.internal.forEach((slot) => { - if (clobber || !slot.m) { + if ((clobber || !slot.m) && (!slot.eligible || slot.eligible.pce)) { ship.use(slot, ModuleUtils.findInternal('pce', Math.min(slot.maxClass, 6), 'E')); // Passenger cabins top out at 6 } }); @@ -143,7 +144,7 @@ export default class InternalSlotSection extends SlotSection { let ship = this.props.ship; let chargeCap = 0; // Capacity of single activation ship.internal.forEach(function(slot) { - if ((!slot.m || (clobber && !ModuleUtils.isShieldGenerator(slot.m.grp))) && (!slot.eligible || slot.eligible.scb)) { // Check eligibility due to passenger ships special case + if ((clobber || (!slot.m && !ModuleUtils.isShieldGenerator(slot.m.grp))) && (!slot.eligible || slot.eligible.scb)) { ship.use(slot, ModuleUtils.findInternal('scb', slot.maxClass, 'A')); ship.setSlotEnabled(slot, chargeCap <= ship.shieldStrength); // Don't waste cell capacity on overcharge chargeCap += slot.m.recharge; @@ -161,7 +162,7 @@ export default class InternalSlotSection extends SlotSection { let clobber = event.getModifierState('Alt'); let ship = this.props.ship; ship.internal.forEach((slot) => { - if (clobber || !slot.m) { + if ((clobber || !slot.m) && (!slot.eligible || slot.eligible.hr)) { ship.use(slot, ModuleUtils.findInternal('hr', Math.min(slot.maxClass, 5), 'D')); // Hull reinforcements top out at 5D } }); @@ -169,6 +170,22 @@ export default class InternalSlotSection extends SlotSection { this._close(); } + /** + * Fill all slots with Module Reinforcement Packages + * @param {SyntheticEvent} event Event + */ + _fillWithModuleReinforcementPackages(event) { + let clobber = event.getModifierState('Alt'); + let ship = this.props.ship; + ship.internal.forEach((slot) => { + if ((clobber || !slot.m) && (!slot.eligible || slot.eligible.mrp)) { + ship.use(slot, ModuleUtils.findInternal('mrp', Math.min(slot.maxClass, 5), 'D')); // Module reinforcements top out at 5D + } + }); + this.props.onChange(); + this._close(); + } + /** * Empty all on section header right click */ @@ -226,6 +243,7 @@ export default class InternalSlotSection extends SlotSection {
    • {translate('cargo')}
    • {translate('scb')}
    • {translate('hr')}
    • +
    • {translate('mrp')}
    • {translate('ft')}
    • {translate('pce')}
    • {translate('pci')}
    • diff --git a/src/app/components/MovementSummary.jsx b/src/app/components/MovementSummary.jsx new file mode 100644 index 00000000..f5b31699 --- /dev/null +++ b/src/app/components/MovementSummary.jsx @@ -0,0 +1,92 @@ +import React from 'react'; +import cn from 'classnames'; +import TranslatedComponent from './TranslatedComponent'; +import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons'; + +/** + * Movement summary + */ +export default class MovementSummary extends TranslatedComponent { + static PropTypes = { + ship: React.PropTypes.object.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + } + + /** + * Render movement 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); + let boostMultiplier = ship.topBoost / ship.topSpeed; + + return ( + +

      {translate('movement summary')}

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
       {translate('engine pips')}
       012344B
      {translate('speed')} ({units['m/s']}){formats.int(ship.speeds[0])}{formats.int(ship.speeds[1])}{formats.int(ship.speeds[2])}{formats.int(ship.speeds[3])}{formats.int(ship.speeds[4])}{formats.int(ship.speeds[4] * boostMultiplier)}
      {translate('pitch')} ({units['°/s']}){formats.int(ship.pitches[0])}{formats.int(ship.pitches[1])}{formats.int(ship.pitches[2])}{formats.int(ship.pitches[3])}{formats.int(ship.pitches[4])}{formats.int(ship.pitches[4] * boostMultiplier)}
      {translate('roll')} ({units['°/s']}){formats.int(ship.rolls[0])}{formats.int(ship.rolls[1])}{formats.int(ship.rolls[2])}{formats.int(ship.rolls[3])}{formats.int(ship.rolls[4])}{formats.int(ship.rolls[4] * boostMultiplier)}
      {translate('yaw')} ({units['°/s']}){formats.int(ship.yaws[0])}{formats.int(ship.yaws[1])}{formats.int(ship.yaws[2])}{formats.int(ship.yaws[3])}{formats.int(ship.yaws[4])}{formats.int(ship.yaws[4] * boostMultiplier)}
      +
      + ); + } +} diff --git a/src/app/components/ShipSelector.jsx b/src/app/components/ShipSelector.jsx new file mode 100644 index 00000000..75a6ee92 --- /dev/null +++ b/src/app/components/ShipSelector.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import cn from 'classnames'; +import { Ships } from 'coriolis-data/dist'; +import TranslatedComponent from './TranslatedComponent'; +import { Rocket } from './SvgIcons'; + +/** + * Selector for ships + */ +export default class ShipSelector extends TranslatedComponent { + static PropTypes = { + initial: React.PropTypes.object.isRequired, + onChange: React.PropTypes.func.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + + this.state = { ship : this.props.initial }; + } + + /** + * Generate the ships menu + * @return {React.Component} Menu + */ + _getShipsMenu() { + const _selectShip = this._selectShip; + const _openMenu = this._openMenu; + + let shipList = []; + + for (let s in Ships) { + shipList.push(
      {Ships[s].properties.name}
      ); + } + + return shipList; + } + + /** + * Handle opening the menu + * @param {string} menu The ID of the opened menu + * @param {SyntheticEvent} event Event + */ + _openMenu(menu, event) { + event.stopPropagation(); + if (this.props.currentMenu == menu) { + menu = null; + } + + this.context.openMenu(menu); + } + + /** + * Handle selection of a ship + * @param {string} s The selected ship ID + */ + _selectShip(s) { + this.setState({ ship: Ships[s] }); + + this.context.openMenu(null); + this.props.onChange(s); + } + + /** + * Render ship selector + * @return {React.Component} contents + */ + render() { + const currentMenu = this.props.currentMenu; + const ship = this.state.ship; + + return ( +
      +
      +
      + {ship.properties.name} + {currentMenu == 'wds' ? +
      e.stopPropagation() }> + {this._getShipsMenu()} +
      : null } +
      +
      +
      + ); + } +} diff --git a/src/app/components/ShipSummaryTable.jsx b/src/app/components/ShipSummaryTable.jsx index 9ab15a9d..54c66e87 100644 --- a/src/app/components/ShipSummaryTable.jsx +++ b/src/app/components/ShipSummaryTable.jsx @@ -43,6 +43,7 @@ export default class ShipSummaryTable extends TranslatedComponent { {translate('DPS')} {translate('EPS')} {translate('HPS')} + {translate('hardness')} {translate('armour')} {translate('shields')} {translate('mass')} @@ -71,6 +72,7 @@ export default class ShipSummaryTable extends TranslatedComponent { {f1(ship.totalDps)} {f1(ship.totalEps)} {f1(ship.totalHps)} + {int(ship.hardness)} {int(ship.armour)} {int(ship.shield)} {u.MJ} {ship.hullMass} {u.T} diff --git a/src/app/components/SlotSection.jsx b/src/app/components/SlotSection.jsx index 4a339a33..d0789448 100644 --- a/src/app/components/SlotSection.jsx +++ b/src/app/components/SlotSection.jsx @@ -77,7 +77,7 @@ export default class SlotSection extends TranslatedComponent { _drag(originSlot, e) { e.dataTransfer.setData('text/html', e.currentTarget); e.dataTransfer.effectAllowed = 'all'; - this.setState({ originSlot }); + this.setState({ originSlot, copy: e.getModifierState('Alt') }); this._close(); } @@ -91,7 +91,9 @@ export default class SlotSection extends TranslatedComponent { e.stopPropagation(); let os = this.state.originSlot; if (os) { - e.dataTransfer.dropEffect = os != targetSlot && canMount(this.props.ship, targetSlot, os.m.grp, os.m.class) ? 'copyMove' : 'none'; + // Show correct icon + const effect = this.state.copy ? 'copy' : 'move'; + e.dataTransfer.dropEffect = os != targetSlot && canMount(this.props.ship, targetSlot, os.m.grp, os.m.class) ? effect : 'none'; this.setState({ targetSlot }); } else { e.dataTransfer.dropEffect = 'none'; @@ -114,20 +116,30 @@ export default class SlotSection extends TranslatedComponent { * the origin slot will be empty. */ _drop() { - let { originSlot, targetSlot } = this.state; + let { originSlot, targetSlot, copy } = this.state; let m = originSlot.m; - if (targetSlot && m && canMount(this.props.ship, targetSlot, m.grp, m.class)) { - // Swap modules if possible - if (targetSlot.m && canMount(this.props.ship, originSlot, targetSlot.m.grp, targetSlot.m.class)) { - this.props.ship.use(originSlot, targetSlot.m, true); - } else { // Otherwise empty the origin slot - this.props.ship.use(originSlot, null, true); // Empty but prevent summary update + if (copy) { + // We want to copy the module in to the target slot + if (targetSlot && canMount(this.props.ship, targetSlot, m.grp, m.class)) { + const mCopy = m.clone(); + this.props.ship.use(targetSlot, mCopy); + this.props.onChange(); + } + } else { + // We want to move the module in to the target slot, and swap back any module that was originally in the target slot + if (targetSlot && m && canMount(this.props.ship, targetSlot, m.grp, m.class)) { + // Swap modules if possible + if (targetSlot.m && canMount(this.props.ship, originSlot, targetSlot.m.grp, targetSlot.m.class)) { + this.props.ship.use(originSlot, targetSlot.m, true); + } else { // Otherwise empty the origin slot + this.props.ship.use(originSlot, null, true); // Empty but prevent summary update + } + this.props.ship.use(targetSlot, m); // update target slot + this.props.onChange(); } - this.props.ship.use(targetSlot, m); // update target slot - this.props.onChange(); } - this.setState({ originSlot: null, targetSlot: null }); + this.setState({ originSlot: null, targetSlot: null, copy: null }); } /** diff --git a/src/app/components/StandardSlot.jsx b/src/app/components/StandardSlot.jsx index 9bd56da9..ebbd5485 100644 --- a/src/app/components/StandardSlot.jsx +++ b/src/app/components/StandardSlot.jsx @@ -52,7 +52,7 @@ export default class StandardSlot extends TranslatedComponent { // Modifications tooltip shows blueprint and grade, if available let modTT = translate('modified'); - if (m && m.blueprint) { + if (m && m.blueprint && m.blueprint.name) { modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; } diff --git a/src/app/components/SvgIcons.jsx b/src/app/components/SvgIcons.jsx index b762fbe0..6093b050 100644 --- a/src/app/components/SvgIcons.jsx +++ b/src/app/components/SvgIcons.jsx @@ -475,6 +475,52 @@ export class MountTurret extends SvgIcon { } } +/** + * Collapse section + */ +export class CollapseSection extends SvgIcon { + /** + * Overriden view box + * @return {String} view box + */ + viewBox() { return '0 0 200 200'; } + /** + * Generate the SVG + * @return {React.Component} SVG Contents + */ + svg() { + return + + + + + ; + } +} + +/** + * Expand section + */ +export class ExpandSection extends SvgIcon { + /** + * Overriden view box + * @return {String} view box + */ + viewBox() { return '0 0 200 200'; } + /** + * Generate the SVG + * @return {React.Component} SVG Contents + */ + svg() { + return + + + + + ; + } +} + /** * Rocket ship */ diff --git a/src/app/i18n/Language.jsx b/src/app/i18n/Language.jsx index 5a6a5ee4..d16b2e1f 100644 --- a/src/app/i18n/Language.jsx +++ b/src/app/i18n/Language.jsx @@ -65,6 +65,7 @@ export function getLanguage(langCode) { LY: {translate('LY')}, // Light Years MJ: {translate('MJ')}, // Mega Joules 'm/s': {translate('m/s')}, // Meters per second + '°/s': {translate('°/s')}, // Degrees per second MW: {translate('MW')}, // Mega Watts (same as Mega Joules per second) ps: {translate('/s')}, // per second pm: {translate('/min')}, // per minute diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index b8b7f0ff..0d0496de 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -59,6 +59,7 @@ export const terms = { mc: 'Multi-cannon', ml: 'Mining Laser', mr: 'Missile Rack', + mrp: 'Module Reinforcement Package', nl: 'Mine Launcher', pa: 'Plasma Accelerator', pas: 'Planetary Approach Suite', @@ -86,15 +87,21 @@ export const terms = { ws: 'Frame Shift Wake Scanner', // Items on the outfitting page - // Notification of restricted slot for Orca/Beluga + // Notification of restricted slot emptyrestricted: 'empty (restricted)', + 'damage dealt against': 'Damage dealt against', + 'damage received by': 'Damage received by', + 'against shields': 'Against shields', + 'against hull': 'Against hull', // 'ammo' was overloaded for outfitting page and modul info, so changed to ammunition for outfitting page ammunition: 'Ammo', // Unit for seconds secs: 's', - // Weapon, offence and defence + rebuildsperbay: 'Rebuilds per bay', + + // Weapon, offence, defence and movement dpe: 'Damage per MJ of energy', dps: 'Damage per second', sdps: 'Sustained damage per second', @@ -106,12 +113,23 @@ export const terms = { 'damage by': 'Damage by', 'damage from': 'Damage from', 'shield cells': 'Shield cells', + 'recovery': 'Recovery', + 'recharge': 'Recharge', + 'engine pips': 'Engine Pips', + '4b': '4 pips and boost', + 'speed': 'Speed', + 'pitch': 'Pitch', + 'roll': 'Roll', + 'yaw': 'Yaw', + 'internal protection': 'Internal protection', + 'external protection': 'External protection', // Modifications ammo: 'Ammunition maximum', boot: 'Boot time', brokenregen: 'Broken regeneration rate', burst: 'Burst', + burstrof: 'Burst rate of fire', clip: 'Ammunition clip', damage: 'Damage', distdraw: 'Distributor draw', @@ -133,10 +151,11 @@ export const terms = { pgen: 'Power generation', piercing: 'Piercing', power: 'Power draw', + protection: 'Protection', range: 'Range', ranget: 'Range', // Range in time (for FSD interdictor) regen: 'Regeneration rate', - reload: 'Reload time', + reload: 'Reload', rof: 'Rate of fire', shield: 'Shield', shieldboost: 'Shield boost', diff --git a/src/app/pages/AboutPage.jsx b/src/app/pages/AboutPage.jsx index 77579786..7cfaa1fd 100644 --- a/src/app/pages/AboutPage.jsx +++ b/src/app/pages/AboutPage.jsx @@ -39,15 +39,6 @@ export default class AboutPage extends Page {

      Coriolis is an open source project. Checkout the list of upcoming features and to-do list on github. Any and all contributions and feedback are welcome. If you encounter any bugs please report them and provide as much detail as possible.

      -
      - - - - -
      - -

      Help keep the lights on! Donations will be used to cover costs of running and maintaining Coriolis. Thanks for helping!

      -

      Chat

      You can chat to us on our EDCD Discord server.

      ; diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index c7831e9d..1edadf1c 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -16,6 +16,9 @@ import InternalSlotSection from '../components/InternalSlotSection'; import UtilitySlotSection from '../components/UtilitySlotSection'; import OffenceSummary from '../components/OffenceSummary'; import DefenceSummary from '../components/DefenceSummary'; +import MovementSummary from '../components/MovementSummary'; +import DamageDealt from '../components/DamageDealt'; +import DamageReceived from '../components/DamageReceived'; import LineChart from '../components/LineChart'; import PowerManagement from '../components/PowerManagement'; import CostSection from '../components/CostSection'; @@ -335,7 +338,7 @@ export default class OutfittingPage extends Page { -
      +
      @@ -344,22 +347,21 @@ export default class OutfittingPage extends Page {
      -

      {translate('speed')}

      + +
      + +
      +

      {translate('jump range')}

      -
      - -
      @@ -385,7 +387,15 @@ export default class OutfittingPage extends Page {
      +
      + +
      + +
      + +
      + ); } } @@ -402,3 +412,43 @@ export default class OutfittingPage extends Page { // func={state.jumpRangeChartFunc} // /> // +//
      +//

      {translate('speed')}

      +// +//
      +//
      +// +// +// +// +// +// +// +// +//
      +// +// +// +// +// {formats.f2(fuelLevel * fuelCapacity)}{units.T} {formats.pct1(fuelLevel)} +//
      +//
      diff --git a/src/app/pages/ShipyardPage.jsx b/src/app/pages/ShipyardPage.jsx index f794eb10..24a08a08 100644 --- a/src/app/pages/ShipyardPage.jsx +++ b/src/app/pages/ShipyardPage.jsx @@ -40,7 +40,9 @@ function shipSummary(shipId, shipData) { intCount: 0, maxCargo: 0, hp: [0, 0, 0, 0, 0], // Utility, Small, Medium, Large, Huge - int: [0, 0, 0, 0, 0, 0, 0, 0] // Sizes 1 - 8 + int: [0, 0, 0, 0, 0, 0, 0, 0], // Sizes 1 - 8 + standard: shipData.slots.standard, + agility: shipData.properties.pitch + shipData.properties.yaw + shipData.properties.roll }; Object.assign(summary, shipData.properties); let ship = new Ship(shipId, shipData.properties, shipData.slots); @@ -139,7 +141,8 @@ export default class ShipyardPage extends Page { > {s.manufacturer} {translate(SizeMap[s.class])} - {s.agility} + {fInt(s.agility)} + {fInt(s.hardness)} {fInt(s.speed)}{u['m/s']} {fInt(s.boost)}{u['m/s']} {fInt(s.baseArmour)} @@ -148,6 +151,12 @@ export default class ShipyardPage extends Page { {fInt(s.topBoost)}{u['m/s']} {fRound(s.maxJumpRange)}{u.LY} {fInt(s.maxCargo)}{u.T} + {s.standard[0]} + {s.standard[1]} + {s.standard[2]} + {s.standard[3]} + {s.standard[4]} + {s.standard[5]} {s.hp[1]} {s.hp[2]} {s.hp[3]} @@ -260,9 +269,11 @@ export default class ShipyardPage extends Page { {translate('manufacturer')} {translate('size')} - {translate('mnv')} + {translate('agility')} + {translate('hardness')} {translate('base')} {translate('max')} + {translate('core module classes')} {translate('hardpoints')} {translate('internal compartments')} {translate('hull')} @@ -280,6 +291,13 @@ export default class ShipyardPage extends Page { {translate('jump')} {translate('cargo')} + {'pp'} + {'th'} + {'fsd'} + {'ls'} + {'pd'} + {'s'} + {translate('S')} {translate('M')} {translate('L')} diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index 96c7b0fe..f9ab11e4 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -67,32 +67,109 @@ export function shieldStrength(mass, baseShield, sg, multiplier) { /** * Calculate the a ships speed based on mass, and thrusters. * - * @param {number} mass Current mass of the ship - * @param {number} baseSpeed Base speed m/s for ship - * @param {number} baseBoost Base boost speed m/s for ship - * @param {object} thrusters The Thrusters used - * @param {number} pipSpeed Speed pip multiplier - * @return {object} Approximate speed by pips + * @param {number} mass the mass of the ship + * @param {number} baseSpeed base speed m/s for ship + * @param {object} thrusters The ship's thrusters + * @param {number} engpip the multiplier per pip to engines + * @return {array} Speed by pips */ -export function speed(mass, baseSpeed, baseBoost, thrusters, pipSpeed) { +export function speed(mass, baseSpeed, thrusters, engpip) { + // thrusters might be a module or a template; handle either here + const minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + const optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + const maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + const minMul = thrusters instanceof Module ? thrusters.getMinMul('speed') : (thrusters.minmulspeed ? thrusters.minmulspeed : thrusters.minmul); + const optMul = thrusters instanceof Module ? thrusters.getOptMul('speed') : (thrusters.optmulspeed ? thrusters.minmulspeed : thrusters.minmul); + const maxMul = thrusters instanceof Module ? thrusters.getMaxMul('speed') : (thrusters.maxmulspeed ? thrusters.minmulspeed : thrusters.minmul); + + let results = normValues(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseSpeed, engpip); + + return results; +} + +/** + * Calculate pitch of a ship based on mass and thrusters + * @param {number} mass the mass of the ship + * @param {number} basePitch base pitch of the ship + * @param {object} thrusters the ship's thrusters + * @param {number} engpip the multiplier per pip to engines + * @return {array} Pitch by pips + */ +export function pitch(mass, basePitch, thrusters, engpip) { // thrusters might be a module or a template; handle either here let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; - let minMul = thrusters instanceof Module ? thrusters.getMinMul() : thrusters.minmul; - let optMul = thrusters instanceof Module ? thrusters.getOptMul() : thrusters.optmul; - let maxMul = thrusters instanceof Module ? thrusters.getMaxMul() : thrusters.maxmul; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); - let xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass)); - let exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass))); - let ynorm = Math.pow(xnorm, exponent); - let mul = minMul + ynorm * (maxMul - minMul); - let speed = baseSpeed * mul; - - return { - '0 Pips': speed * (1 - (pipSpeed * 4)), - '2 Pips': speed * (1 - (pipSpeed * 2)), - '4 Pips': speed, - 'boost': baseBoost * mul - }; + return normValues(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, basePitch, engpip); +} + +/** + * Calculate yaw of a ship based on mass and thrusters + * @param {number} mass the mass of the ship + * @param {number} baseYaw base yaw of the ship + * @param {object} thrusters the ship's thrusters + * @param {number} engpip the multiplier per pip to engines + * @return {array} Yaw by pips + */ +export function yaw(mass, baseYaw, thrusters, engpip) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + return normValues(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseYaw, engpip); +} + +/** + * Calculate roll of a ship based on mass and thrusters + * @param {number} mass the mass of the ship + * @param {number} baseRoll base roll of the ship + * @param {object} thrusters the ship's thrusters + * @param {number} engpip the multiplier per pip to engines + * @return {array} Roll by pips + */ +export function roll(mass, baseRoll, thrusters, engpip) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + return normValues(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseRoll, engpip); +} + +/** + * Normalise according to FD's calculations and return suitable values + * @param {number} minMass the minimum mass of the thrusters + * @param {number} optMass the optimum mass of the thrusters + * @param {number} maxMass the maximum mass of the thrusters + * @param {number} minMul the minimum multiplier of the thrusters + * @param {number} optMul the optimum multiplier of the thrusters + * @param {number} maxMul the maximum multiplier of the thrusters + * @param {number} mass the mass of the ship + * @param {base} base the base value from which to calculate + * @param {number} engpip the multiplier per pip to engines + * @return {array} values by pips + */ +function normValues(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, base, engpip) { + const xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass)); + const exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass))); + const ynorm = Math.pow(xnorm, exponent); + const mul = minMul + ynorm * (maxMul - minMul); + const res = base * mul; + + return [res * (1 - (engpip * 4)), + res * (1 - (engpip * 3)), + res * (1 - (engpip * 2)), + res * (1 - (engpip * 1)), + res]; } diff --git a/src/app/shipyard/Constants.js b/src/app/shipyard/Constants.js index e8714216..35901ee3 100755 --- a/src/app/shipyard/Constants.js +++ b/src/app/shipyard/Constants.js @@ -105,8 +105,9 @@ export const BulkheadNames = [ export const ShipFacets = [ { // 0 title: 'agility', - props: ['agility'], - fmt: 'int', + props: ['topPitch', 'topRoll', 'topYaw'], + lbls: ['pitch', 'roll', 'yaw'], + fmt: 'f1', i: 0 }, { // 1 @@ -185,11 +186,18 @@ export const ShipFacets = [ }, { // 11 title: 'DPS', - props: ['totalDps'], - lbls: ['DPS'], + props: ['totalDps', 'totalExplDps', 'totalKinDps', 'totalThermDps'], + lbls: ['total', 'explosive', 'kinetic', 'thermal'], fmt: 'round', i: 11 }, + { // 14 + title: 'Sustained DPS', + props: ['totalSDps', 'totalExplSDps', 'totalKinSDps', 'totalThermSDps'], + lbls: ['total', 'explosive', 'kinetic', 'thermal'], + fmt: 'round', + i: 14 + }, { // 12 title: 'EPS', props: ['totalEps'], diff --git a/src/app/shipyard/Module.js b/src/app/shipyard/Module.js index bf38e8ec..37879de6 100755 --- a/src/app/shipyard/Module.js +++ b/src/app/shipyard/Module.js @@ -12,20 +12,28 @@ 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 template; - if (properties.template == undefined) { - return ModuleUtils.findModule(properties.grp, properties.id); + let properties = Object.assign({ grp: null, id: null, template: null }, params); + + if (properties.class != undefined) { + // We already have a fully-formed module; copy the data over + for (let p in properties) { this[p] = properties[p]; } + } else if (properties.template != undefined) { + // We have a template from coriolis-data; copy the data over + for (let p in properties.template) { this[p] = properties.template[p]; } } else { - template = properties.template; - if (template) { - // Copy all properties from coriolis-data template - for (let p in template) { this[p] = template[p]; } - } + // We don't have a template; find it given the group and ID + return ModuleUtils.findModule(properties.grp, properties.id); } } + /** + * Clone an existing module + * @return {Object} A clone of the existing module + */ + clone() { + return new Module(JSON.parse(JSON.stringify(this))); + } + /** * Get a value for a given modification * @param {Number} name The name of the modification @@ -302,6 +310,14 @@ export default class Module { return this._getModifiedValue('hullreinforcement'); } + /** + * Get the protection for this module, taking in to account modifications + * @return {Number} the protection of this module + */ + getProtection() { + return this._getModifiedValue('protection'); + } + /** * Get the delay for this module, taking in to account modifications * @return {Number} the delay of this module @@ -370,42 +386,60 @@ export default class Module { /** * Get the minimum multiplier for this module, taking in to account modifications + * @param {string} type the type for which we are obtaining the multiplier. Can be 'speed', 'rotation', 'acceleration', or null * @return {Number} the minimum multiplier of this module */ - getMinMul() { + getMinMul(type = null) { // Modifier is optmul let result = 0; - if (this['minmul']) { + if (this['minmul' + type]) { + result = this['minmul' + type]; + } else if (this['minmul']) { result = this['minmul']; - if (result) { - let mult = this.getModValue('optmul') / 10000; - if (mult) { result = result * (1 + mult); } - } + } + if (result) { + let mult = this.getModValue('optmul') / 10000; + if (mult) { result = result * (1 + mult); } } return result; } /** * Get the optimum multiplier for this module, taking in to account modifications + * @param {string} type the type for which we are obtaining the multiplier. Can be 'speed', 'rotation', 'acceleration', or null * @return {Number} the optimum multiplier of this module */ - getOptMul() { - return this._getModifiedValue('optmul'); + getOptMul(type = null) { + // Modifier is optmul + let result = 0; + if (this['optmul' + type]) { + result = this['optmul' + type]; + } else if (this['optmul']) { + result = this['optmul']; + } + if (result) { + let mult = this.getModValue('optmul') / 10000; + if (mult) { result = result * (1 + mult); } + } + return result; } /** * Get the maximum multiplier for this module, taking in to account modifications + * @param {string} type the type for which we are obtaining the multiplier. Can be 'speed', 'rotation', 'acceleration', or null * @return {Number} the maximum multiplier of this module */ - getMaxMul() { + getMaxMul(type = null) { // Modifier is optmul let result = 0; - if (this['maxmul']) { + if (this['maxmul' + type]) { + result = this['maxmul' + type]; + } else if (this['maxmul']) { result = this['maxmul']; - if (result) { - let mult = this.getModValue('optmul') / 10000; - if (mult) { result = result * (1 + mult); } - } + } + if (result) { + let mult = this.getModValue('optmul') / 10000; + if (mult) { result = result * (1 + mult); } } return result; } @@ -561,6 +595,14 @@ export default class Module { return this._getModifiedValue('shieldreinforcement'); } + /** + * Get the piercing for this module, taking in to account modifications + * @return {Number} the piercing for this module + */ + getPiercing() { + return this._getModifiedValue('piercing'); + } + /** * Get the bays for this module, taking in to account modifications * @return {Number} the bays for this module diff --git a/src/app/shipyard/Serializer.js b/src/app/shipyard/Serializer.js index cd9951a6..0e60c305 100644 --- a/src/app/shipyard/Serializer.js +++ b/src/app/shipyard/Serializer.js @@ -134,67 +134,21 @@ export function toDetailedBuild(buildName, ship) { */ 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 priorities = [stn.cargoHatch && stn.cargoHatch.priority !== undefined ? stn.cargoHatch.priority - 1 : 0]; - let enabled = [stn.cargoHatch && stn.cargoHatch.enabled !== undefined ? stn.cargoHatch.enabled : true]; 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); - let blueprints = new Array(stn.bulkheads.blueprint); - if (bulkheads < 0) { - throw 'Invalid bulkheads: ' + stn.bulkheads; + if (!detailedBuild.references[0] || !detailedBuild.references[0].code) { + throw 'Missing reference code'; } - let standard = STANDARD.map((c) => { - if (!stn[c].class || !stn[c].rating) { - throw 'Invalid value for ' + c; - } - 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); - blueprints.push(stn[c].blueprint); - return ModuleUtils.findStandardId(STANDARD_GROUPS[c], stn[c].class, stn[c].rating, stn[c].name); - }); - - let internal = comps.internal.map(c => c ? ModuleUtils.findInternalId(c.group, c.class, c.rating, c.name) : 0); - - let hardpoints = comps.hardpoints.map(c => c ? ModuleUtils.findHardpointId(c.group, c.class, c.rating, c.name, MountMap[c.mount], c.missile) : 0) - .concat(comps.utility.map(c => c ? ModuleUtils.findHardpointId(c.group, c.class, c.rating, c.name, MountMap[c.mount]) : 0)); - - // The ordering of these arrays must match the order in which they are read in Ship.buildWith - priorities = priorities.concat( - comps.hardpoints.map(c => (!c || c.priority === undefined) ? 0 : c.priority - 1), - comps.utility.map(c => (!c || c.priority === undefined) ? 0 : c.priority - 1), - comps.internal.map(c => (!c || c.priority === undefined) ? 0 : c.priority - 1) - ); - enabled = enabled.concat( - comps.hardpoints.map(c => (!c || c.enabled === undefined) ? true : c.enabled * 1), - 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.modifications : null)), - comps.utility.map(c => (c ? c.modifications : null)), - comps.internal.map(c => (c ? c.modifications : null)) - ); - blueprints = blueprints.concat( - comps.hardpoints.map(c => (c ? c.blueprint : null)), - comps.utility.map(c => (c ? c.blueprint : null)), - comps.internal.map(c => (c ? c.blueprint : null)) - ); - - ship.buildWith({ bulkheads, standard, hardpoints, internal }, priorities, enabled, modifications, blueprints); + ship.buildFrom(detailedBuild.references[0].code); return ship; -}; +} /** * Generates an array of ship-loadout JSON Schema object for export diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index be771cbc..0c016384 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -5,7 +5,7 @@ import Module from './Module'; import LZString from 'lz-string'; import * as _ from 'lodash'; import isEqual from 'lodash/lang'; -import { Modifications } from 'coriolis-data/dist'; +import { Ships, Modifications } from 'coriolis-data/dist'; const zlib = require('zlib'); const UNIQUE_MODULES = ['psg', 'sg', 'bsg', 'rf', 'fs', 'fh']; @@ -188,10 +188,10 @@ export default class Ship { * Calculate the hypothetical top speeds at cargo and fuel tonnage * @param {Number} fuel Fuel available in tons * @param {Number} cargo Cargo in tons - * @return {Object} Speed at pip settings and boost + * @return {array} Speed at pip settings */ calcSpeedsWith(fuel, cargo) { - return Calc.speed(this.unladenMass + fuel + cargo, this.speed, this.boost, this.standard[1].m, this.pipSpeed); + return Calc.speed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed); } /** @@ -201,13 +201,11 @@ export default class Ship { * @return {Number} Recovery time in seconds */ calcShieldRecovery() { - if (this.shield > 0) { - 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; - } + const shieldGenerator = this.findShieldGenerator(); + if (shieldGenerator) { + const brokenRegenRate = shieldGenerator.getBrokenRegenerationRate(); + // 50% of shield strength / broken recharge rate + 15 second delay before recharge starts + return ((this.shield / 2) / brokenRegenRate) + 15; } return 0; } @@ -219,13 +217,12 @@ export default class Ship { * @return {Number} 50 - 100% Recharge time in seconds */ calcShieldRecharge() { - if (this.shield > 0) { - 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); - } + const shieldGenerator = this.findShieldGenerator(); + if (shieldGenerator) { + const regenRate = shieldGenerator.getRegenerationRate(); + + // 50% of shield strength / recharge rate + return (this.shield / 2) / regenRate; } return 0; } @@ -295,12 +292,22 @@ export default class Ship { } } + /** + * Find the shield generator for this ship + * @return {object} The shield generator module for this ship + */ + findShieldGenerator() { + const slot = this.internal.find(slot => slot.m && ModuleUtils.isShieldGenerator(slot.m.grp)); + return slot ? slot.m : undefined; + } + /** * Serializes the ship to a string * @return {String} Serialized ship 'code' */ toString() { return [ + 'A', this.getStandardString(), this.getHardpointsString(), this.getInternalString(), @@ -432,7 +439,7 @@ export default class Ship { let newMass = m.getMass(); this.unladenMass = this.unladenMass - oldMass + newMass; this.ladenMass = this.ladenMass - oldMass + newMass; - this.updateTopSpeed(); + this.updateMovement(); this.updateJumpStats(); } else if (name === 'maxfuel') { m.setModValue(name, value); @@ -440,19 +447,19 @@ export default class Ship { } else if (name === 'optmass') { m.setModValue(name, value); // Could be for any of thrusters, FSD or shield - this.updateTopSpeed(); + this.updateMovement(); this.updateJumpStats(); this.recalculateShield(); } else if (name === 'optmul') { m.setModValue(name, value); // Could be for any of thrusters, FSD or shield - this.updateTopSpeed(); + this.updateMovement(); this.updateJumpStats(); this.recalculateShield(); } else if (name === 'shieldboost') { m.setModValue(name, value); this.recalculateShield(); - } else if (name === 'hullboost' || name === 'hullreinforcement') { + } else if (name === 'hullboost' || name === 'hullreinforcement' || name === 'modulereinforcement') { m.setModValue(name, value); this.recalculateArmour(); } else if (name === 'shieldreinforcement') { @@ -597,7 +604,7 @@ export default class Ship { .recalculateDps() .recalculateEps() .recalculateHps() - .updateTopSpeed(); + .updateMovement(); } return this.updatePowerPrioritesString().updatePowerEnabledString().updateModificationsString(); @@ -621,6 +628,19 @@ export default class Ship { enabled = null, code = parts[0]; + // Code has a version ID embedded as the first character (if it is alphabetic) + let version; + if (code && code.match(/^[0-4]/)) { + // Starting with bulkhead number is version 1 + version = 1; + } else { + // Version 2 (current version) + version = 2; + if (code) { + code = code.substring(1); + } + } + if (parts[1]) { enabled = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[1])).split(''); } @@ -644,6 +664,11 @@ export default class Ship { decodeToArray(code, internal, decodeToArray(code, hardpoints, decodeToArray(code, standard, 1))); + if (version != 2) { + // Alter as required due to changes in the (build) code from one version to the next + this.upgradeInternals(internal, 1 + this.standard.length + this.hardpoints.length, priorities, enabled, modifications, blueprints, version); + } + return this.buildWith( { bulkheads: code.charAt(0) * 1, @@ -801,7 +826,7 @@ export default class Ship { 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') || (n && n.grp === 'mrp') || (old && old.grp === 'mrp'); 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'); @@ -876,7 +901,7 @@ export default class Ship { if (shieldCellsChange) { this.recalculateShieldCells(); } - this.updateTopSpeed(); + this.updateMovement(); this.updateJumpStats(); } return this; @@ -1088,13 +1113,23 @@ export default class Ship { } /** - * Update top speed and boost + * Update movement values * @return {this} The ship instance (for chaining operations) */ - updateTopSpeed() { - let speeds = Calc.speed(this.unladenMass + this.fuelCapacity, this.speed, this.boost, this.standard[1].m, this.pipSpeed); - this.topSpeed = speeds['4 Pips']; - this.topBoost = this.canBoost() ? speeds.boost : 0; + updateMovement() { + this.speeds = Calc.speed(this.unladenMass + this.fuelCapacity, this.speed, this.standard[1].m, this.pipSpeed); + this.topSpeed = this.speeds[4]; + this.topBoost = this.canBoost() ? this.speeds[4] * this.boost / this.speed : 0; + + this.pitches = Calc.pitch(this.unladenMass + this.fuelCapacity, this.pitch, this.standard[1].m, this.pipSpeed); + this.topPitch = this.pitches[4]; + + this.rolls = Calc.roll(this.unladenMass + this.fuelCapacity, this.roll, this.standard[1].m, this.pipSpeed); + this.topRoll = this.rolls[4]; + + this.yaws = Calc.yaw(this.unladenMass + this.fuelCapacity, this.yaw, this.standard[1].m, this.pipSpeed); + this.topYaw = this.yaws[4]; + return this; } @@ -1104,6 +1139,7 @@ export default class Ship { */ recalculateShield() { let shield = 0; + let shieldBoost = 1; let shieldExplRes = null; let shieldKinRes = null; let shieldThermRes = null; @@ -1111,16 +1147,15 @@ export default class Ship { const sgSlot = this.findInternalByGroup('sg'); if (sgSlot && sgSlot.enabled) { // Shield from generator - const baseShield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1); - shield = baseShield; + shield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1); 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(); + if (slot.enabled && slot.m && slot.m.grp == 'sb') { + shieldBoost += slot.m.getShieldBoost(); shieldExplRes *= (1 - slot.m.getExplosiveResistance()); shieldKinRes *= (1 - slot.m.getKineticResistance()); shieldThermRes *= (1 - slot.m.getThermalResistance()); @@ -1128,6 +1163,12 @@ export default class Ship { } } + // We apply diminishing returns to the boosted value + // (no we don't; FD pulled back on this idea. But leave this here in case they reinstate it) + // shieldBoost = Math.min(shieldBoost, (1 - Math.pow(Math.E, -0.7 * shieldBoost)) * 2.5); + + shield = shield * shieldBoost; + 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; @@ -1162,11 +1203,13 @@ export default class Ship { // Armour from bulkheads let bulkhead = this.bulkheads.m; let armour = this.baseArmour + (this.baseArmour * bulkhead.getHullBoost()); + let modulearmour = 0; + let moduleprotection = 1; let hullExplRes = 1 - bulkhead.getExplosiveResistance(); let hullKinRes = 1 - bulkhead.getKineticResistance(); let hullThermRes = 1 - bulkhead.getThermalResistance(); - // Armour from HRPs + // Armour from HRPs and module armour from MRPs for (let slot of this.internal) { if (slot.m && slot.m.grp == 'hr') { armour += slot.m.getHullReinforcement(); @@ -1177,9 +1220,16 @@ export default class Ship { hullKinRes *= (1 - slot.m.getKineticResistance()); hullThermRes *= (1 - slot.m.getThermalResistance()); } + if (slot.m && slot.m.grp == 'mrp') { + modulearmour += slot.m.getIntegrity(); + moduleprotection = moduleprotection * (1 - slot.m.getProtection()); + } } + moduleprotection = 1 - moduleprotection; this.armour = armour; + this.modulearmour = modulearmour; + this.moduleprotection = moduleprotection; 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); @@ -1369,7 +1419,7 @@ export default class Ship { for (let slot of slots) { if (slot.length > 0) { buffer.writeInt8(i, curpos++); - if (blueprints[i]) { + if (blueprints[i] && blueprints[i].id) { buffer.writeInt8(MODIFICATION_ID_BLUEPRINT, curpos++); buffer.writeInt32LE(blueprints[i].id, curpos); curpos += 4; @@ -1434,9 +1484,13 @@ export default class Ship { curpos += 4; // There are a number of 'special' modification IDs, check for them here if (modificationId === MODIFICATION_ID_BLUEPRINT) { - blueprint = Object.assign(blueprint, _.find(Modifications.blueprints, function(o) { return o.id === modificationValue; })); + if (modificationValue !== 0) { + blueprint = Object.assign(blueprint, _.find(Modifications.blueprints, function(o) { return o.id === modificationValue; })); + } } else if (modificationId === MODIFICATION_ID_GRADE) { - blueprint.grade = modificationValue; + if (modificationValue !== 0) { + blueprint.grade = modificationValue; + } } else if (modificationId === MODIFICATION_ID_SPECIAL) { blueprint.special = _.find(Modifications.specials, function(o) { return o.id === modificationValue; }); } else { @@ -1447,7 +1501,9 @@ export default class Ship { modificationId = buffer.readInt8(curpos++); } modArr[slot] = modifications; - blueprintArr[slot] = blueprint; + if (blueprint.id) { + blueprintArr[slot] = blueprint; + } slot = buffer.readInt8(curpos++); } } @@ -1615,4 +1671,37 @@ export default class Ship { } return this; } + + /** + * Upgrade information about internals with version changes + * @param {array} internals the internals from the ship code + * @param {int} offset the offset of the internals information in the priorities etc. arrays + * @param {array} priorities the existing priorities arrray + * @param {array} enableds the existing enableds arrray + * @param {array} modifications the existing modifications arrray + * @param {array} blueprints the existing blueprints arrray + * @param {int} version the version of the information + */ + upgradeInternals(internals, offset, priorities, enableds, modifications, blueprints, version) { + if (version == 1) { + // Version 2 reflects the addition of military slots. this means that we need to juggle the internals and their + // associated information around to make holes in the appropriate places + for (let slotId = 0; slotId < this.internal.length; slotId++) { + if (this.internal[slotId].eligible && this.internal[slotId].eligible.mrp) { + // Found a restricted military slot - push all of the existing items down one to compensate for the fact that they didn't exist before now + internals.push.apply(internals, [0].concat(internals.splice(slotId).slice(0, -1))); + + const offsetSlotId = offset + slotId; + + // Same for priorities etc. + if (priorities) { priorities.push.apply(priorities, [0].concat(priorities.splice(offsetSlotId))); } + if (enableds) { enableds.push.apply(enableds, [1].concat(enableds.splice(offsetSlotId))); } + if (modifications) { modifications.push.apply(modifications, [null].concat(modifications.splice(offsetSlotId).slice(0, -1))); } + if (blueprints) { blueprints.push.apply(blueprints, [null].concat(blueprints.splice(offsetSlotId).slice(0, -1))); } + } + } + // Ensure that all items are the correct length + internals.splice(Ships[this.id].slots.internal.length); + } + } } diff --git a/src/app/utils/CompanionApiUtils.js b/src/app/utils/CompanionApiUtils.js index 6d90bc7f..11030548 100644 --- a/src/app/utils/CompanionApiUtils.js +++ b/src/app/utils/CompanionApiUtils.js @@ -33,7 +33,7 @@ const SHIP_FD_NAME_TO_CORIOLIS_NAME = { 'Type7': 'type_7_transport', 'Type9': 'type_9_heavy', 'Viper': 'viper', - 'Viper_MKIV': 'viper_mk_iv', + 'Viper_MkIV': 'viper_mk_iv', 'Vulture': 'vulture' }; @@ -224,7 +224,9 @@ export function shipFromJson(json) { // 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) { + if (!hardpointSlot) { + // This can happen with old imports that don't contain new hardpoints + } else if (!hardpointSlot.module) { // No module } else { const hardpointJson = hardpointSlot.module; @@ -239,19 +241,31 @@ export function shipFromJson(json) { // Add internal compartments let internalSlotNum = 1; + let militarySlotNum = 1; for (let i in shipTemplate.slots.internal) { const internalClassNum = isNaN(shipTemplate.slots.internal[i]) ? shipTemplate.slots.internal[i].class : shipTemplate.slots.internal[i]; + const isMilitary = isNaN(shipTemplate.slots.internal[i]) ? shipTemplate.slots.internal[i].name == 'Military' : false; + // The internal slot might be a standard or a military slot. Military slots have a different naming system 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]; + if (isMilitary) { + const internalName = 'Military0' + militarySlotNum; + internalSlot = json.modules[internalName]; + militarySlotNum++; + } else { + 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++; } - internalSlotNum++; } - if (!internalSlot.module) { + + if (!internalSlot) { + // This can happen with old imports that don't contain new slots + } else if (!internalSlot.module) { // No module } else { const internalJson = internalSlot.module; diff --git a/src/app/utils/ShortenUrl.js b/src/app/utils/ShortenUrl.js index e1db8eb3..f11efaa8 100644 --- a/src/app/utils/ShortenUrl.js +++ b/src/app/utils/ShortenUrl.js @@ -1,17 +1,21 @@ import request from 'superagent'; -const SHORTEN_API = 'https://www.googleapis.com/urlshortener/v1/url?key='; +export default function shorternUrl(url, success, error) { + shortenUrlEddp(url, success, error); +} + +const SHORTEN_API_GOOGLE = 'https://www.googleapis.com/urlshortener/v1/url?key='; /** * Shorten a URL using Google's URL shortener API * @param {string} url The URL to shorten * @param {function} success Success callback * @param {function} error Failure/Error callback */ -export default function shortenUrl(url, success, error) { +function shortenUrlGoogle(url, success, error) { if (window.navigator.onLine) { try { - request.post(SHORTEN_API + window.CORIOLIS_GAPI_KEY) + request.post(SHORTEN_API_GOOGLE + window.CORIOLIS_GAPI_KEY) .send({ longUrl: url }) .end(function(err, response) { if (err) { @@ -27,3 +31,30 @@ export default function shortenUrl(url, success, error) { error('Not Online'); } } + +const SHORTEN_API_EDDP = 'http://eddp.co/u'; +/** + * Shorten a URL using EDDP's URL shortener API + * @param {string} url The URL to shorten + * @param {function} success Success callback + * @param {function} error Failure/Error callback + */ +function shortenUrlEddp(url, success, error) { + if (window.navigator.onLine) { + try { + request.post(SHORTEN_API_EDDP) + .send(url) + .end(function(err, response) { + if (err) { + error('Bad Request'); + } else { + success(response.header['location']); + } + }); + } catch (e) { + error(e.message ? e.message : e); + } + } else { + error('Not Online'); + } +} diff --git a/src/less/app.less b/src/less/app.less index 7f4564ec..ccff47d7 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -16,6 +16,7 @@ @import 'tooltip'; @import 'buttons'; @import 'error'; +@import 'shipselector'; @import 'sortable'; @import 'loader'; @@ -41,8 +42,10 @@ div, a, li { #coriolis { width: 100%; height: 100%; + padding-top: 48px; overflow-y: scroll; -webkit-overflow-scrolling: touch; + box-sizing: border-box; } .page { diff --git a/src/less/header.less b/src/less/header.less index 510e61a3..07a24a5c 100755 --- a/src/less/header.less +++ b/src/less/header.less @@ -20,8 +20,12 @@ header { line-height: 3em; font-family: @fTitle; vertical-align: middle; - position: relative; + position: absolute; + top: 0px; + left: 0px; + width: 100%; z-index: 2; + box-sizing: border-box; .user-select-none(); .menu { @@ -186,6 +190,7 @@ header { } .no-wrap { + overflow-x: auto; white-space: nowrap; } @@ -200,4 +205,4 @@ header { margin:0px; text-transform: uppercase; } -} \ No newline at end of file +} diff --git a/src/less/icons.less b/src/less/icons.less index 9af0c0e6..999352a9 100755 --- a/src/less/icons.less +++ b/src/less/icons.less @@ -34,8 +34,10 @@ width: 1.1em; height: 1em; stoke: @fg; + stroke-width: 20; fill: transparent; + &.sm { width: 0.8em; height: 0.75em; diff --git a/src/less/shipselector.less b/src/less/shipselector.less new file mode 100755 index 00000000..5764bdc5 --- /dev/null +++ b/src/less/shipselector.less @@ -0,0 +1,199 @@ +.shipselector { + background-color: @bgBlack; + margin: 0; + padding: 0 0 0 1em; + height: 3em; + line-height: 3em; + font-family: @fTitle; + vertical-align: middle; + position: relative; + + .user-select-none(); + + .menu { + position: relative; + z-index: 1; + cursor: default; + + &.r { + .menu-list { + right: 0; + } + } + + .smallTablet({ + position: static; + position: initial; + }); + } + + .menu-header { + padding : 0 1em; + cursor: pointer; + color: @warning; + text-transform: uppercase; + } + + .menu-list { + font-family: @fStandard; + position: absolute; + padding: 0.5em 1em; + box-sizing: border-box; + min-width: 100%; + overflow-x: hidden; + background-color: @bgBlack; + font-size: 0.9em; + overflow-y: auto; + z-index: 0; + -webkit-overflow-scrolling: touch; + max-height: 500px; + + &::-webkit-scrollbar { + width: 0.5em; + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: @warning-disabled; + } + + input { + border: none; + background-color: transparent; + text-align: right; + font-size: 1em; + font-family: @fStandard; + } + + .smallTablet({ + max-height: 400px; + left: 0; + right: 0; + border-bottom: 1px solid @bg; + }); + + + .tablet({ + li, a { + padding: 0.3em 0; + } + }); + } + + .dbl { + -webkit-column-count: 2; /* Chrome, Safari, Opera */ + -moz-column-count: 2; /* Firefox */ + column-count: 2; + ul { + min-width: 10em; + } + + .smallTablet({ + -webkit-column-count: 3; /* Chrome, Safari, Opera */ + -moz-column-count: 3; /* Firefox */ + column-count: 3; + + ul { + min-width: 20em; + } + }); + + .largePhone({ + -webkit-column-count: 2; /* Chrome, Safari, Opera */ + -moz-column-count: 2; /* Firefox */ + column-count: 2; + }); + + .smallPhone({ + -webkit-column-count: 1; /* Chrome, Safari, Opera */ + -moz-column-count: 1; /* Firefox */ + column-count: 1; + }); + } + + .quad { + -webkit-column-count: 4; /* Chrome, Safari, Opera */ + -moz-column-count: 4; /* Firefox */ + column-count: 4; + ul { + min-width: 10em; + } + + .smallTablet({ + -webkit-column-count: 3; /* Chrome, Safari, Opera */ + -moz-column-count: 3; /* Firefox */ + column-count: 3; + + ul { + min-width: 20em; + } + }); + + .largePhone({ + -webkit-column-count: 2; /* Chrome, Safari, Opera */ + -moz-column-count: 2; /* Firefox */ + column-count: 2; + }); + + .smallPhone({ + -webkit-column-count: 1; /* Chrome, Safari, Opera */ + -moz-column-count: 1; /* Firefox */ + column-count: 1; + }); + } + + ul { + display: inline-block; + white-space: nowrap; + margin: 0 0 0.5em; + padding: 0; + line-height: 1.3em; + } + + li { + white-space: normal; + list-style: none; + margin-left: 1em; + line-height: 1.1em; + } + + a { + vertical-align: middle; + color: @warning; + text-decoration: none; + + &:visited { + color: @warning; + } + .no-touch &:hover { + color: teal; + } + &.active { + color: @primary; + } + } + + hr { + border: none; + border-top: 1px solid @disabled; + } + + .no-wrap { + white-space: nowrap; + } + + .block { + display: block; + line-height: 1.5em; + } + + .title { + font-size: 1.3em; + display: inline-block; + margin:0px; + text-transform: uppercase; + } +}