mirror of
https://github.com/EDCD/coriolis.git
synced 2025-12-09 14:45:35 +00:00
Compare commits
82 Commits
8583c6a811
...
ed-forge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25c30e46a6 | ||
|
|
870c033c64 | ||
|
|
46b0200b28 | ||
|
|
e8bcb18f12 | ||
|
|
68ca24ef96 | ||
|
|
7ca2754934 | ||
|
|
e82ab0aec0 | ||
|
|
d406018f0b | ||
|
|
2438aa1f48 | ||
|
|
c5bbeacc6a | ||
|
|
d9763f2db7 | ||
|
|
5692cc6fe3 | ||
|
|
22cdfcdecf | ||
|
|
3db3b09c22 | ||
|
|
23c264e321 | ||
|
|
d2c3787165 | ||
|
|
1e18d6e463 | ||
|
|
044fea2d33 | ||
|
|
bc865f534b | ||
|
|
796694a4e0 | ||
|
|
829815a40b | ||
|
|
7a2c849ece | ||
|
|
2eed1bc85b | ||
|
|
3469af10b6 | ||
|
|
629ba35bc5 | ||
|
|
e453ff73b7 | ||
|
|
c73ce1c234 | ||
|
|
00d3a93b91 | ||
|
|
d4e612cb61 | ||
|
|
c07cfc6e70 | ||
|
|
c4c6d32a5d | ||
|
|
a65bb06754 | ||
|
|
23548e7c5c | ||
|
|
74e6f54e19 | ||
|
|
a46f8f97f6 | ||
|
|
44dbdb1703 | ||
|
|
187c5dae4a | ||
|
|
07d324a3fa | ||
|
|
5c63afd96c | ||
|
|
53c40ac9c4 | ||
|
|
a180cbfdd4 | ||
|
|
fc1524a943 | ||
|
|
c3747e4e5e | ||
|
|
ab153981c9 | ||
|
|
a970e052c1 | ||
|
|
df7e264a02 | ||
|
|
cdcda004f3 | ||
|
|
20e448fc0a | ||
|
|
436e626c42 | ||
|
|
3dd4675a0b | ||
|
|
d3766d9e17 | ||
|
|
d987c08ac8 | ||
|
|
f865ef6c6c | ||
|
|
f2b7daac82 | ||
|
|
832bc488b6 | ||
|
|
f513166d6c | ||
|
|
61fd0eb991 | ||
|
|
1e51e7d4a6 | ||
|
|
4943d36bb8 | ||
|
|
9271d1fa09 | ||
|
|
8a09d94dfa | ||
|
|
c44925dd62 | ||
|
|
d006bbcb0f | ||
|
|
14453f6b80 | ||
|
|
8c267150a9 | ||
|
|
ed60a78be0 | ||
|
|
82142b0cb1 | ||
|
|
d8949fedb2 | ||
|
|
cf72bd11a8 | ||
|
|
16ef7ea389 | ||
|
|
ff455e349e | ||
|
|
ba9e7f1a32 | ||
|
|
904498b20c | ||
|
|
409be7374c | ||
|
|
00c525e6ab | ||
|
|
a2f52c03a1 | ||
|
|
037df6b166 | ||
|
|
90ab5b4b0a | ||
|
|
7bbfa8c43f | ||
|
|
2fbcd158cc | ||
|
|
33c201800e | ||
|
|
9797a8d781 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,4 +10,3 @@ env
|
||||
.project
|
||||
.vscode/
|
||||
docs/
|
||||
package-lock.json
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"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}
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
{
|
||||
"$schema": "http://cdn.coriolis.io/schemas/ship-loadout/3.json#",
|
||||
"name": "Test My Ship",
|
||||
"ship": "Anaconda",
|
||||
"references": [
|
||||
{
|
||||
"name": "Coriolis.io",
|
||||
"url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA?bn=Test%20My%20Ship",
|
||||
"old-code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA",
|
||||
"code": "4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA",
|
||||
"shipId": "anaconda"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"standard": {
|
||||
"bulkheads": "Reactive Surface Composite",
|
||||
"cargoHatch": {
|
||||
"enabled": false,
|
||||
"priority": 5
|
||||
},
|
||||
"powerPlant": {
|
||||
"class": 8,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"thrusters": {
|
||||
"class": 6,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"frameShiftDrive": {
|
||||
"class": 6,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 3
|
||||
},
|
||||
"lifeSupport": {
|
||||
"class": 5,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"powerDistributor": {
|
||||
"class": 8,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"sensors": {
|
||||
"class": 8,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"fuelTank": {
|
||||
"class": 5,
|
||||
"rating": "C",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
}
|
||||
},
|
||||
"hardpoints": [
|
||||
{
|
||||
"class": 4,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Plasma Accelerator",
|
||||
"mount": "Fixed"
|
||||
},
|
||||
{
|
||||
"class": 3,
|
||||
"rating": "D",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 3,
|
||||
"rating": "D",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 3,
|
||||
"rating": "D",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Cannon",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Cannon",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 1,
|
||||
"rating": "F",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 1,
|
||||
"rating": "F",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
}
|
||||
],
|
||||
"utility": [
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Shield Booster"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Shield Booster"
|
||||
},
|
||||
null,
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "C",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Kill Warrant Scanner"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "C",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Cargo Scanner"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "F",
|
||||
"enabled": false,
|
||||
"priority": 1,
|
||||
"group": "Countermeasure",
|
||||
"name": "Electronic Countermeasure"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "I",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Countermeasure",
|
||||
"name": "Chaff Launcher"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "I",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Countermeasure",
|
||||
"name": "Point Defence"
|
||||
}
|
||||
],
|
||||
"internal": [
|
||||
{
|
||||
"class": 7,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Shield Generator"
|
||||
},
|
||||
{
|
||||
"class": 6,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Shield Cell Bank"
|
||||
},
|
||||
{
|
||||
"class": 6,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
{
|
||||
"class": 5,
|
||||
"rating": "D",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Hull Reinforcement Package"
|
||||
},
|
||||
{
|
||||
"class": 5,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"class": 4,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
{
|
||||
"class": 4,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
{
|
||||
"class": 4,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 3,
|
||||
"group": "Fuel Scoop"
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 3,
|
||||
"group": "Frame Shift Drive Interdictor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {
|
||||
"class": 3,
|
||||
"hullCost": 141889930,
|
||||
"speed": 180,
|
||||
"topSpeed": 186.5,
|
||||
"boost": 240,
|
||||
"boostEnergy": 29,
|
||||
"topBoost": 248.66,
|
||||
"agility": 2,
|
||||
"baseShieldStrength": 350,
|
||||
"baseArmour": 945,
|
||||
"hullMass": 400,
|
||||
"masslock": 23,
|
||||
"pipSpeed": 0.14,
|
||||
"moduleCostMultiplier": 1,
|
||||
"fuelCapacity": 32,
|
||||
"cargoCapacity": 128,
|
||||
"ladenMass": 1339.2,
|
||||
"armour": 2228,
|
||||
"armourAdded": 390,
|
||||
"armourMultiplier": 1.95,
|
||||
"shieldMultiplier": 1.4,
|
||||
"totalCost": 882362060,
|
||||
"unladenMass": 1179.2,
|
||||
"totalDps": 29,
|
||||
"powerAvailable": 36,
|
||||
"powerRetracted": 23.33,
|
||||
"powerDeployed": 34.76,
|
||||
"unladenRange": 18.49,
|
||||
"fullTankRange": 18.12,
|
||||
"ladenRange": 16.39,
|
||||
"unladenFastestRange": 73.21,
|
||||
"ladenFastestRange": 66.15,
|
||||
"maxJumpCount": 4,
|
||||
"shieldStrength": 833
|
||||
}
|
||||
}
|
||||
@@ -1,325 +0,0 @@
|
||||
{
|
||||
"$schema": "http://cdn.coriolis.io/schemas/ship-loadout/4.json#",
|
||||
"name": "Test My Ship",
|
||||
"ship": "Anaconda",
|
||||
"references": [
|
||||
{
|
||||
"name": "Coriolis.io",
|
||||
"url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==?bn=Test%20My%20Ship",
|
||||
"old-code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==",
|
||||
"code": "4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==",
|
||||
"shipId": "anaconda"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"standard": {
|
||||
"bulkheads": "Reactive Surface Composite",
|
||||
"cargoHatch": {
|
||||
"enabled": false,
|
||||
"priority": 5
|
||||
},
|
||||
"powerPlant": {
|
||||
"class": 8,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"modifications": {
|
||||
"pgen": 1000
|
||||
}
|
||||
},
|
||||
"thrusters": {
|
||||
"class": 6,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"frameShiftDrive": {
|
||||
"class": 6,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 3
|
||||
},
|
||||
"lifeSupport": {
|
||||
"class": 5,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"powerDistributor": {
|
||||
"class": 8,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"sensors": {
|
||||
"class": 8,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"fuelTank": {
|
||||
"class": 5,
|
||||
"rating": "C",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
}
|
||||
},
|
||||
"hardpoints": [
|
||||
{
|
||||
"class": 4,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Plasma Accelerator",
|
||||
"mount": "Fixed"
|
||||
},
|
||||
{
|
||||
"class": 3,
|
||||
"rating": "D",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 3,
|
||||
"rating": "D",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 3,
|
||||
"rating": "D",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Cannon",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Cannon",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 1,
|
||||
"rating": "F",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 1,
|
||||
"rating": "F",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
}
|
||||
],
|
||||
"utility": [
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Shield Booster"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Shield Booster"
|
||||
},
|
||||
null,
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "C",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Kill Warrant Scanner"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "C",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Cargo Scanner"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "F",
|
||||
"enabled": false,
|
||||
"priority": 1,
|
||||
"group": "Electronic Countermeasure",
|
||||
"name": "Electronic Countermeasure"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "I",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Chaff Launcher",
|
||||
"name": "Chaff Launcher"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "I",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Point Defence",
|
||||
"name": "Point Defence"
|
||||
}
|
||||
],
|
||||
"internal": [
|
||||
{
|
||||
"class": 7,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Shield Generator"
|
||||
},
|
||||
{
|
||||
"class": 6,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Shield Cell Bank"
|
||||
},
|
||||
{
|
||||
"class": 6,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
{
|
||||
"class": 5,
|
||||
"rating": "D",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Hull Reinforcement Package"
|
||||
},
|
||||
{
|
||||
"class": 5,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"class": 4,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
{
|
||||
"class": 4,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
{
|
||||
"class": 4,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 3,
|
||||
"group": "Fuel Scoop"
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 3,
|
||||
"group": "Frame Shift Drive Interdictor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {
|
||||
"class": 3,
|
||||
"fighterHangars": 1,
|
||||
"hullCost": 141889930,
|
||||
"speed": 180,
|
||||
"topSpeed": 186.5,
|
||||
"boost": 240,
|
||||
"boostEnergy": 27,
|
||||
"topBoost": 249.34,
|
||||
"topPitch": 25.97,
|
||||
"topRoll": 62.34,
|
||||
"topYaw": 10.39,
|
||||
"topSpeed": 187.01,
|
||||
"totalCost": 882362058,
|
||||
"totalDpe": 142.68,
|
||||
"totalDps": 101.13,
|
||||
"totalEps": 18.71,
|
||||
"totalExplDpe": 0,
|
||||
"totalExplDps": 0,
|
||||
"totalExplSDps": 0,
|
||||
"totalAbsDpe": 3.57,
|
||||
"totalAbsDps": 18.78,
|
||||
"totalAbsSDps": 14.45,
|
||||
"totalHps": 28.28,
|
||||
"totalKinDpe": 117.48,
|
||||
"totalKinDps": 22.27,
|
||||
"totalKinSDps": 16.91,
|
||||
"totalSDps": 89.99,
|
||||
"totalThermDpe": 21.63,
|
||||
"totalThermDps": 60.08,
|
||||
"totalThermSDps": 58.64,
|
||||
"baseShieldStrength": 350,
|
||||
"baseArmour": 945,
|
||||
"hullExplRes": 0.22,
|
||||
"hullKinRes": 0.27,
|
||||
"hullMass": 400,
|
||||
"hullThermRes": -0.36,
|
||||
"masslock": 23,
|
||||
"pipSpeed": 0.14,
|
||||
"pitch": 25,
|
||||
"moduleCostMultiplier": 1,
|
||||
"modulearmour": 0,
|
||||
"moduleprotection": 0,
|
||||
"fuelCapacity": 32,
|
||||
"cargoCapacity": 128,
|
||||
"ladenMass": 1323.2,
|
||||
"armour": 2227.5,
|
||||
"baseArmour": 525,
|
||||
"unladenMass": 1163.2,
|
||||
"powerAvailable": 39.6,
|
||||
"powerRetracted": 23.33,
|
||||
"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,
|
||||
"shieldExplRes": 0.5,
|
||||
"shieldKinRes": 0.4,
|
||||
"shieldThermRes": -0.2,
|
||||
"crew": 3
|
||||
}
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
{
|
||||
"$schema": "http://cdn.coriolis.io/schemas/ship-loadout/4.json#",
|
||||
"name": "Multi-purpose Asp Explorer",
|
||||
"ship": "Asp Explorer",
|
||||
"references": [
|
||||
{
|
||||
"name": "Coriolis.io",
|
||||
"url": "https://coriolis.edcd.io/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",
|
||||
"code": "0pftiFflfddsnf5------020202033c044002v62f2i.AwRj4yvI.CwRgDBldHnJA.H4sIAAAAAAAAA2P858DAwPCXEUhwHPvx/78YG5AltB7I/8/0TwImJboDSPJ/+/f/v/KlX///i3AwMTBIfARK/Gf+JwVSxArStVAYqOjvz///JVo5GRhE2IBc4SKQSSz/DGEmCa398P8//2+gTf//AwDFxwtofAAAAA==",
|
||||
"shipId": "asp"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"standard": {
|
||||
"bulkheads": "Lightweight Alloy",
|
||||
"cargoHatch": {
|
||||
"enabled": false,
|
||||
"priority": 5
|
||||
},
|
||||
"powerPlant": {
|
||||
"class": 5,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"modifications": {
|
||||
"eff": -1850,
|
||||
"pgen": 6,
|
||||
"mass": 431
|
||||
},
|
||||
"blueprint": {
|
||||
"id": 64,
|
||||
"name": "Low emissions",
|
||||
"grade": 1
|
||||
}
|
||||
},
|
||||
"thrusters": {
|
||||
"class": 5,
|
||||
"rating": "D",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"modifications": {
|
||||
"optmul": 440,
|
||||
"integrity": -266,
|
||||
"thermload": -1326,
|
||||
"optmass": 520,
|
||||
"power": 241
|
||||
},
|
||||
"blueprint": {
|
||||
"id": 24,
|
||||
"name": "Clean",
|
||||
"grade": 1
|
||||
}
|
||||
},
|
||||
"frameShiftDrive": {
|
||||
"class": 5,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"modifications": {
|
||||
"mass": 5025,
|
||||
"integrity": -1539,
|
||||
"power": 2437,
|
||||
"optmass": 4870,
|
||||
"maxfuel": 370
|
||||
},
|
||||
"blueprint": {
|
||||
"id": 26,
|
||||
"name": "Increased range",
|
||||
"grade": 5
|
||||
}
|
||||
},
|
||||
"lifeSupport": {
|
||||
"class": 4,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"modifications": {
|
||||
"mass": -3923,
|
||||
"integrity": -1797
|
||||
},
|
||||
"blueprint": {
|
||||
"id": 49,
|
||||
"name": "Lightweight",
|
||||
"grade": 1
|
||||
}
|
||||
},
|
||||
"powerDistributor": {
|
||||
"class": 3,
|
||||
"rating": "D",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"sensors": {
|
||||
"class": 5,
|
||||
"rating": "D",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"fuelTank": {
|
||||
"class": 5,
|
||||
"rating": "C",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
}
|
||||
},
|
||||
"hardpoints": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"utility": [
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "I",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Heat Sink Launcher",
|
||||
"name": "Heat Sink Launcher"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "I",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Heat Sink Launcher",
|
||||
"name": "Heat Sink Launcher"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "I",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Heat Sink Launcher",
|
||||
"name": "Heat Sink Launcher"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "I",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Point Defence",
|
||||
"name": "Point Defence"
|
||||
}
|
||||
],
|
||||
"internal": [
|
||||
{
|
||||
"class": 6,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Fuel Scoop"
|
||||
},
|
||||
{
|
||||
"class": 5,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
{
|
||||
"class": 3,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Shield Generator"
|
||||
},
|
||||
{
|
||||
"class": 3,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "G",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Planetary Vehicle Hangar"
|
||||
},
|
||||
{
|
||||
"class": 1,
|
||||
"rating": "C",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Scanner",
|
||||
"name": "Advanced Discovery Scanner"
|
||||
},
|
||||
{
|
||||
"class": 1,
|
||||
"rating": "C",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Scanner",
|
||||
"name": "Detailed Surface Scanner"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {
|
||||
"class": 2,
|
||||
"hullCost": 6135660,
|
||||
"speed": 250,
|
||||
"boost": 340,
|
||||
"boostEnergy": 13,
|
||||
"agility": 6,
|
||||
"baseShieldStrength": 140,
|
||||
"baseArmour": 210,
|
||||
"hullMass": 280,
|
||||
"masslock": 11,
|
||||
"pipSpeed": 0.13,
|
||||
"moduleCostMultiplier": 1,
|
||||
"fuelCapacity": 32,
|
||||
"cargoCapacity": 40,
|
||||
"ladenMass": 435.26,
|
||||
"armour": 378,
|
||||
"shield": 113.43,
|
||||
"shieldCells": 0,
|
||||
"totalCost": 48402550,
|
||||
"unladenMass": 363.26,
|
||||
"totalDpe": 0,
|
||||
"totalExplDpe": 0,
|
||||
"totalKinDpe": 0,
|
||||
"totalThermDpe": 0,
|
||||
"totalDps": 0,
|
||||
"totalExplDps": 0,
|
||||
"totalKinDps": 0,
|
||||
"totalThermDps": 0,
|
||||
"totalSDps": 0,
|
||||
"totalExplSDps": 0,
|
||||
"totalKinSDps": 0,
|
||||
"totalThermSDps": 0,
|
||||
"totalEps": 1.2,
|
||||
"totalHps": 1,
|
||||
"shieldExplRes": 0.5,
|
||||
"shieldKinRes": 0.6,
|
||||
"shieldThermRes": 1.2,
|
||||
"hullExplRes": 1.4,
|
||||
"hullKinRes": 1.2,
|
||||
"hullThermRes": 1,
|
||||
"powerAvailable": 20.41,
|
||||
"powerRetracted": 11.91,
|
||||
"powerDeployed": 11.91,
|
||||
"unladenRange": 50.45,
|
||||
"fullTankRange": 47.03,
|
||||
"ladenRange": 42.71,
|
||||
"unladenFastestRange": 317.24,
|
||||
"ladenFastestRange": 287.02,
|
||||
"maxJumpCount": 7,
|
||||
"topSpeed": 274.01,
|
||||
"topBoost": 372.65
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,552 +0,0 @@
|
||||
{
|
||||
"cargo": {
|
||||
"capacity": 32
|
||||
},
|
||||
"free": false,
|
||||
"fuel": {
|
||||
"main": {
|
||||
"capacity": 128
|
||||
},
|
||||
"reserve": {
|
||||
"capacity": 0.81
|
||||
}
|
||||
},
|
||||
"id": 31,
|
||||
"modules": {
|
||||
"Armour": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128049346,
|
||||
"name": "BelugaLiner_Armour_Grade1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"Bobble01": [],
|
||||
"Bobble02": [],
|
||||
"Bobble03": [],
|
||||
"Bobble04": [],
|
||||
"Bobble05": [],
|
||||
"Bobble06": [],
|
||||
"Bobble07": [],
|
||||
"Bobble08": [],
|
||||
"Bobble09": [],
|
||||
"Bobble10": [],
|
||||
"Decal1": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128667757,
|
||||
"name": "Decal_Explorer_Ranger",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"Decal2": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128667742,
|
||||
"name": "Decal_Combat_Deadly",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"Decal3": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128667750,
|
||||
"name": "Decal_Trade_Tycoon",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"EngineColour": [],
|
||||
"FrameShiftDrive": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064132,
|
||||
"modifiers": {
|
||||
"engineerID": 300100,
|
||||
"id": 175,
|
||||
"modifiers": [
|
||||
{
|
||||
"name": "mod_mass",
|
||||
"type": 1,
|
||||
"value": 0.4457540512085
|
||||
},
|
||||
{
|
||||
"name": "mod_health",
|
||||
"type": 1,
|
||||
"value": -0.24584779143333
|
||||
},
|
||||
{
|
||||
"name": "mod_passive_power",
|
||||
"type": 1,
|
||||
"value": 0.24457727372646
|
||||
},
|
||||
{
|
||||
"name": "mod_fsd_optimised_mass",
|
||||
"type": 1,
|
||||
"value": 0.49257898330688
|
||||
},
|
||||
{
|
||||
"name": "mod_fsd_max_fuel_per_jump",
|
||||
"type": 2,
|
||||
"value": 0.028505677357316
|
||||
},
|
||||
{
|
||||
"name": "mod_fsd_heat_rate",
|
||||
"type": 2,
|
||||
"value": -0.079360365867615
|
||||
}
|
||||
],
|
||||
"moduleTags": [
|
||||
16
|
||||
],
|
||||
"recipeID": 128673694,
|
||||
"slotIndex": 53
|
||||
},
|
||||
"name": "Int_Hyperdrive_Size7_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"recipeLevel": 5,
|
||||
"recipeName": "FSD_LongRange",
|
||||
"recipeValue": 0,
|
||||
"unloaned": 0,
|
||||
"value": 46160201
|
||||
}
|
||||
},
|
||||
"FuelTank": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064352,
|
||||
"name": "Int_FuelTank_Size7_Class3",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 1602822,
|
||||
"value": 1602822
|
||||
}
|
||||
},
|
||||
"LifeSupport": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064174,
|
||||
"name": "Int_LifeSupport_Size8_Class2",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 1569565
|
||||
}
|
||||
},
|
||||
"MainEngines": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064094,
|
||||
"modifiers": {
|
||||
"engineerID": 300100,
|
||||
"id": 253,
|
||||
"modifiers": [
|
||||
{
|
||||
"name": "mod_engine_mass_curve_multiplier",
|
||||
"type": 1,
|
||||
"value": 0.098235413432121
|
||||
},
|
||||
{
|
||||
"name": "mod_engine_heat",
|
||||
"type": 1,
|
||||
"value": 0.18069696426392
|
||||
},
|
||||
{
|
||||
"name": "mod_passive_power",
|
||||
"type": 1,
|
||||
"value": 0.033788848668337
|
||||
},
|
||||
{
|
||||
"name": "mod_health",
|
||||
"type": 1,
|
||||
"value": -0.056404989212751
|
||||
},
|
||||
{
|
||||
"name": "mod_engine_mass_curve",
|
||||
"type": 1,
|
||||
"value": -0.027384582906961
|
||||
},
|
||||
{
|
||||
"name": "mod_engine_heat",
|
||||
"type": 2,
|
||||
"value": -0.072683908045292
|
||||
}
|
||||
],
|
||||
"moduleTags": [
|
||||
17
|
||||
],
|
||||
"recipeID": 128673655,
|
||||
"slotIndex": 52
|
||||
},
|
||||
"name": "Int_Engine_Size7_Class2",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"recipeLevel": 1,
|
||||
"recipeName": "Engine_Dirty",
|
||||
"recipeValue": 0,
|
||||
"unloaned": 0,
|
||||
"value": 1709638
|
||||
}
|
||||
},
|
||||
"MediumHardpoint1": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128049436,
|
||||
"name": "Hpt_BeamLaser_Turret_Medium",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 1889910
|
||||
}
|
||||
},
|
||||
"MediumHardpoint2": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128049436,
|
||||
"name": "Hpt_BeamLaser_Turret_Medium",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 1889910
|
||||
}
|
||||
},
|
||||
"MediumHardpoint3": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128049460,
|
||||
"name": "Hpt_MultiCannon_Gimbal_Medium",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 51300
|
||||
}
|
||||
},
|
||||
"MediumHardpoint4": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128049460,
|
||||
"name": "Hpt_MultiCannon_Gimbal_Medium",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 51300
|
||||
}
|
||||
},
|
||||
"MediumHardpoint5": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128049460,
|
||||
"name": "Hpt_MultiCannon_Gimbal_Medium",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 51300
|
||||
}
|
||||
},
|
||||
"PaintJob": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128732290,
|
||||
"name": "PaintJob_BelugaLiner_Tactical_White",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"PlanetaryApproachSuite": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128672317,
|
||||
"name": "Int_PlanetApproachSuite",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 450,
|
||||
"value": 450
|
||||
}
|
||||
},
|
||||
"PowerDistributor": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064207,
|
||||
"name": "Int_PowerDistributor_Size6_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 3128120
|
||||
}
|
||||
},
|
||||
"PowerPlant": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064057,
|
||||
"modifiers": {
|
||||
"engineerID": 300100,
|
||||
"id": 277,
|
||||
"modifiers": [
|
||||
{
|
||||
"name": "mod_powerplant_power",
|
||||
"type": 1,
|
||||
"value": 0.054692290723324
|
||||
},
|
||||
{
|
||||
"name": "mod_health",
|
||||
"type": 1,
|
||||
"value": -0.033690698444843
|
||||
},
|
||||
{
|
||||
"name": "mod_powerplant_heat",
|
||||
"type": 1,
|
||||
"value": 0.027470717206597
|
||||
},
|
||||
{
|
||||
"name": "mod_powerplant_heat",
|
||||
"type": 2,
|
||||
"value": -0.056317910552025
|
||||
}
|
||||
],
|
||||
"moduleTags": [
|
||||
18
|
||||
],
|
||||
"recipeID": 128673765,
|
||||
"slotIndex": 51
|
||||
},
|
||||
"name": "Int_Powerplant_Size6_Class5",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"recipeLevel": 1,
|
||||
"recipeName": "PowerPlant_Boosted",
|
||||
"recipeValue": 0,
|
||||
"unloaned": 0,
|
||||
"value": 14561578
|
||||
}
|
||||
},
|
||||
"Radar": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064239,
|
||||
"name": "Int_Sensors_Size5_Class2",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 71500
|
||||
}
|
||||
},
|
||||
"Slot01_Size6": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128666681,
|
||||
"name": "Int_FuelScoop_Size6_Class5",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 25887249
|
||||
}
|
||||
},
|
||||
"Slot02_Size6": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064287,
|
||||
"name": "Int_ShieldGenerator_Size6_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 14561578
|
||||
}
|
||||
},
|
||||
"Slot03_Size6": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128727927,
|
||||
"name": "Int_PassengerCabin_Size6_Class2",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 165808,
|
||||
"value": 165808
|
||||
}
|
||||
},
|
||||
"Slot04_Size6": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128727928,
|
||||
"name": "Int_PassengerCabin_Size6_Class3",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 497429
|
||||
}
|
||||
},
|
||||
"Slot05_Size5": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128727925,
|
||||
"name": "Int_PassengerCabin_Size5_Class4",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 1492286
|
||||
}
|
||||
},
|
||||
"Slot06_Size5": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064342,
|
||||
"name": "Int_CargoRack_Size5_Class1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 100409,
|
||||
"value": 100409
|
||||
}
|
||||
},
|
||||
"Slot07_Size4": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128727922,
|
||||
"name": "Int_PassengerCabin_Size4_Class1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 17059
|
||||
}
|
||||
},
|
||||
"Slot08_Size3": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128667632,
|
||||
"name": "Int_Repairer_Size3_Class5",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 2361960
|
||||
}
|
||||
},
|
||||
"Slot09_Size3": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128672289,
|
||||
"name": "Int_BuggyBay_Size2_Class2",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 19440
|
||||
}
|
||||
},
|
||||
"Slot10_Size3": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128666634,
|
||||
"name": "Int_DetailedSurfaceScanner_Tiny",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 225000
|
||||
}
|
||||
},
|
||||
"Slot11_Size3": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128663561,
|
||||
"name": "Int_StellarBodyDiscoveryScanner_Advanced",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 1390500
|
||||
}
|
||||
},
|
||||
"TinyHardpoint1": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128049513,
|
||||
"name": "Hpt_ChaffLauncher_Tiny",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 7650
|
||||
}
|
||||
},
|
||||
"TinyHardpoint2": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128668536,
|
||||
"name": "Hpt_ShieldBooster_Size0_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 252900
|
||||
}
|
||||
},
|
||||
"TinyHardpoint3": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128668536,
|
||||
"name": "Hpt_ShieldBooster_Size0_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 252900
|
||||
}
|
||||
},
|
||||
"TinyHardpoint4": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128668536,
|
||||
"name": "Hpt_ShieldBooster_Size0_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 281000
|
||||
}
|
||||
},
|
||||
"TinyHardpoint5": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128668536,
|
||||
"name": "Hpt_ShieldBooster_Size0_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 281000
|
||||
}
|
||||
},
|
||||
"TinyHardpoint6": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128668536,
|
||||
"name": "Hpt_ShieldBooster_Size0_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 281000
|
||||
}
|
||||
},
|
||||
"WeaponColour": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128732194,
|
||||
"name": "WeaponCustomisation_Purple",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "BelugaLiner",
|
||||
"value": {
|
||||
"hull": 71688743,
|
||||
"modules": 120812762,
|
||||
"unloaned": 1869489
|
||||
}
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
{
|
||||
"cargo": {
|
||||
"capacity": 264
|
||||
},
|
||||
"free": false,
|
||||
"fuel": {
|
||||
"main": {
|
||||
"capacity": 32
|
||||
},
|
||||
"reserve": {
|
||||
"capacity": 0.52
|
||||
}
|
||||
},
|
||||
"id": 4,
|
||||
"modules": {
|
||||
"Armour": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128049298,
|
||||
"name": "Type7_Armour_Grade1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"Bobble01": [],
|
||||
"Bobble02": [],
|
||||
"Bobble03": [],
|
||||
"Bobble04": [],
|
||||
"Bobble05": [],
|
||||
"Bobble06": [],
|
||||
"Bobble07": [],
|
||||
"Bobble08": [],
|
||||
"Bobble09": [],
|
||||
"Bobble10": [],
|
||||
"Decal1": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128667746,
|
||||
"name": "Decal_Trade_Dealer",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"Decal2": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128667738,
|
||||
"name": "Decal_Combat_Competent",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"Decal3": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128667753,
|
||||
"name": "Decal_Explorer_Scout",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"EngineColour": [],
|
||||
"FrameShiftDrive": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064122,
|
||||
"name": "Int_Hyperdrive_Size5_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 5103953
|
||||
}
|
||||
},
|
||||
"FuelTank": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064350,
|
||||
"name": "Int_FuelTank_Size5_Class3",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 97754,
|
||||
"value": 97754
|
||||
}
|
||||
},
|
||||
"LifeSupport": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064154,
|
||||
"name": "Int_LifeSupport_Size4_Class2",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 28373
|
||||
}
|
||||
},
|
||||
"MainEngines": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064087,
|
||||
"name": "Int_Engine_Size5_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 5103953
|
||||
}
|
||||
},
|
||||
"PaintJob": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128671422,
|
||||
"name": "PaintJob_Type7_Tactical_White",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"PlanetaryApproachSuite": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128672317,
|
||||
"name": "Int_PlanetApproachSuite",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 500,
|
||||
"value": 500
|
||||
}
|
||||
},
|
||||
"PowerDistributor": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064192,
|
||||
"name": "Int_PowerDistributor_Size3_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 158331
|
||||
}
|
||||
},
|
||||
"PowerPlant": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064047,
|
||||
"name": "Int_Powerplant_Size4_Class5",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 1610080
|
||||
}
|
||||
},
|
||||
"Radar": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064229,
|
||||
"name": "Int_Sensors_Size3_Class2",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 10133
|
||||
}
|
||||
},
|
||||
"Slot01_Size6": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064343,
|
||||
"name": "Int_CargoRack_Size6_Class1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 362591
|
||||
}
|
||||
},
|
||||
"Slot02_Size6": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064343,
|
||||
"name": "Int_CargoRack_Size6_Class1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 362591
|
||||
}
|
||||
},
|
||||
"Slot03_Size5": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064343,
|
||||
"name": "Int_CargoRack_Size6_Class1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 362591
|
||||
}
|
||||
},
|
||||
"Slot04_Size5": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064342,
|
||||
"name": "Int_CargoRack_Size5_Class1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 111566,
|
||||
"value": 111566
|
||||
}
|
||||
},
|
||||
"Slot05_Size4": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064342,
|
||||
"name": "Int_CargoRack_Size5_Class1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 111566,
|
||||
"value": 111566
|
||||
}
|
||||
},
|
||||
"Slot06_Size4": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064279,
|
||||
"name": "Int_ShieldGenerator_Size5_Class2",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 189035
|
||||
}
|
||||
},
|
||||
"Slot07_Size2": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128049549,
|
||||
"name": "Int_DockingComputer_Standard",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 4500
|
||||
}
|
||||
},
|
||||
"Slot08_Size2": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064340,
|
||||
"name": "Int_CargoRack_Size3_Class1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"unloaned": 0,
|
||||
"value": 10563
|
||||
}
|
||||
},
|
||||
"SmallHardpoint1": [],
|
||||
"SmallHardpoint2": [],
|
||||
"SmallHardpoint3": [],
|
||||
"SmallHardpoint4": [],
|
||||
"TinyHardpoint1": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128668536,
|
||||
"name": "Hpt_ShieldBooster_Size0_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 281000
|
||||
}
|
||||
},
|
||||
"TinyHardpoint2": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128668536,
|
||||
"name": "Hpt_ShieldBooster_Size0_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 281000
|
||||
}
|
||||
},
|
||||
"TinyHardpoint3": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128668536,
|
||||
"name": "Hpt_ShieldBooster_Size0_Class5",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 281000
|
||||
}
|
||||
},
|
||||
"TinyHardpoint4": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128049513,
|
||||
"name": "Hpt_ChaffLauncher_Tiny",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"unloaned": 0,
|
||||
"value": 8500
|
||||
}
|
||||
},
|
||||
"WeaponColour": []
|
||||
},
|
||||
"name": "Type7",
|
||||
"value": {
|
||||
"hull": 16780009,
|
||||
"modules": 14479580,
|
||||
"unloaned": 321386
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
{
|
||||
"free": false,
|
||||
"id": 2,
|
||||
"modules": {
|
||||
"Armour": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128049280,
|
||||
"name": "CobraMkIII_Armour_Grade1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"FrameShiftDrive": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064117,
|
||||
"name": "Int_Hyperdrive_Size4_Class5",
|
||||
"on": true,
|
||||
"priority": 4,
|
||||
"value": 1610080
|
||||
}
|
||||
},
|
||||
"FuelTank": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064349,
|
||||
"name": "Int_FuelTank_Size4_Class3",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"value": 24734
|
||||
}
|
||||
},
|
||||
"LifeSupport": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064149,
|
||||
"name": "Int_LifeSupport_Size3_Class2",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"value": 10133
|
||||
}
|
||||
},
|
||||
"MainEngines": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064079,
|
||||
"name": "Int_Engine_Size4_Class2",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"value": 59633
|
||||
}
|
||||
},
|
||||
"PaintJob": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128741033,
|
||||
"name": "PaintJob_CobraMKIII_Corrosive_05",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"PlanetaryApproachSuite": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128672317,
|
||||
"name": "Int_PlanetApproachSuite",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"value": 500
|
||||
}
|
||||
},
|
||||
"PowerDistributor": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064179,
|
||||
"name": "Int_PowerDistributor_Size1_Class2",
|
||||
"on": true,
|
||||
"priority": 2,
|
||||
"value": 1293
|
||||
}
|
||||
},
|
||||
"PowerPlant": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064037,
|
||||
"name": "Int_Powerplant_Size2_Class5",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"value": 160224
|
||||
}
|
||||
},
|
||||
"Radar": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128064229,
|
||||
"name": "Int_Sensors_Size3_Class2",
|
||||
"on": true,
|
||||
"priority": 0,
|
||||
"value": 10133
|
||||
}
|
||||
},
|
||||
"ShipID0": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128758976,
|
||||
"name": "Nameplate_ShipID_Black",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"ShipID1": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128758976,
|
||||
"name": "Nameplate_ShipID_Black",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"ShipKitBumper": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128740698,
|
||||
"name": "CobraMkIII_ShipkitRaider1_Bumper1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"ShipKitSpoiler": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128740701,
|
||||
"name": "CobraMkIII_ShipkitRaider1_Spoiler1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"ShipKitTail": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128740705,
|
||||
"name": "CobraMkIII_ShipkitRaider1_Tail2",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"ShipKitWings": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128740707,
|
||||
"name": "CobraMkIII_ShipkitRaider1_Wings1",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"ShipName0": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128758944,
|
||||
"name": "Nameplate_Explorer01_Black",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"ShipName1": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128758944,
|
||||
"name": "Nameplate_Explorer01_Black",
|
||||
"on": true,
|
||||
"priority": 1,
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"Slot01_Size4": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128666663,
|
||||
"name": "Int_FuelScoop_Size4_Class3",
|
||||
"on": true,
|
||||
"priority": 2,
|
||||
"value": 178898
|
||||
}
|
||||
},
|
||||
"Slot02_Size4": [],
|
||||
"Slot03_Size4": [],
|
||||
"Slot04_Size2": [],
|
||||
"Slot05_Size2": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128663561,
|
||||
"name": "Int_StellarBodyDiscoveryScanner_Advanced",
|
||||
"on": true,
|
||||
"priority": 2,
|
||||
"value": 1545000
|
||||
}
|
||||
},
|
||||
"Slot06_Size2": {
|
||||
"module": {
|
||||
"free": false,
|
||||
"id": 128666634,
|
||||
"name": "Int_DetailedSurfaceScanner_Tiny",
|
||||
"on": true,
|
||||
"priority": 2,
|
||||
"value": 250000
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "CobraMkIII",
|
||||
"value": {
|
||||
"hull": 205287,
|
||||
"modules": 3850628,
|
||||
"unloaned": 1751109
|
||||
}
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
{
|
||||
"$schema": "http://cdn.coriolis.io/schemas/ship-loadout/4.json#",
|
||||
"name": "Multi-purpose Imperial Courier",
|
||||
"ship": "Imperial Courier",
|
||||
"references": [
|
||||
{
|
||||
"name": "Coriolis.io",
|
||||
"url": "https://coriolis.edcd.io/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",
|
||||
"code": "0patzF5l0das8f31a1a270202000e402t0101-2f.AwRj4zKA.CwRgDBldLiQ=.H4sIAAAAAAAAA12OP0tCYRjFj9fuVbvF1du9ekkT8s/kIg4NElyIBBd321yaGvwUQTS3N7UFfYygIT9EoyQUJA36ns47XJCWA+/z+z3Pe3ImBbDNKaqNPSBoGrL4ngfomKpFGiJ+LgHteR1IPjxJT5pF11uSeXNsJVcRfgdC92syWUuK0iMdKZqrjJ/0aoA71lJ5oKf38knWcCiptCPdhJIerdS00vlK0qktlqoj983UmqqHjQ33VsW8eazFmaTyULP2hQ4lX8LBme6g/6v0TTdbxJ2KhdEIaCw15MF/NB0L+S2hwEwyFM8KgP+qEpWWA3Qu9Z3z9kPWHzakt7Dt+AeD7ghSTgEAAA==",
|
||||
"shipId": "imperial_courier"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"standard": {
|
||||
"bulkheads": "Lightweight Alloy",
|
||||
"cargoHatch": {
|
||||
"enabled": false,
|
||||
"priority": 5
|
||||
},
|
||||
"powerPlant": {
|
||||
"class": 4,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"modifications": {
|
||||
"pgen": 1052,
|
||||
"integrity": -482,
|
||||
"eff": 974
|
||||
},
|
||||
"blueprint": {
|
||||
"id": 63,
|
||||
"name": "Overcharged",
|
||||
"grade": 1
|
||||
}
|
||||
},
|
||||
"thrusters": {
|
||||
"class": 3,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"name": "Enhanced Performance",
|
||||
"modifications": {
|
||||
"optmul": 2476,
|
||||
"thermload": 7023,
|
||||
"power": 1763,
|
||||
"integrity": 165,
|
||||
"optmass": -667
|
||||
},
|
||||
"blueprint": {
|
||||
"id": 22,
|
||||
"name": "Dirty",
|
||||
"grade": 4
|
||||
}
|
||||
},
|
||||
"frameShiftDrive": {
|
||||
"class": 3,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"modifications": {
|
||||
"mass": 4082,
|
||||
"integrity": -2422,
|
||||
"power": 1782,
|
||||
"optmass": 4927
|
||||
},
|
||||
"blueprint": {
|
||||
"id": 26,
|
||||
"name": "Increased range",
|
||||
"grade": 5
|
||||
}
|
||||
},
|
||||
"lifeSupport": {
|
||||
"class": 1,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"powerDistributor": {
|
||||
"class": 3,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"sensors": {
|
||||
"class": 2,
|
||||
"rating": "D",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
},
|
||||
"fuelTank": {
|
||||
"class": 3,
|
||||
"rating": "C",
|
||||
"enabled": true,
|
||||
"priority": 1
|
||||
}
|
||||
},
|
||||
"hardpoints": [
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "F",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Pulse Laser",
|
||||
"mount": "Fixed",
|
||||
"modifications": {
|
||||
"rof": 5931,
|
||||
"damage": -184,
|
||||
"jitter": 50,
|
||||
"distdraw": -4689,
|
||||
"piercing": 3328
|
||||
},
|
||||
"blueprint": {
|
||||
"id": 89,
|
||||
"name": "Rapid fire",
|
||||
"grade": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "F",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Pulse Laser",
|
||||
"mount": "Fixed",
|
||||
"modifications": {
|
||||
"rof": 4715,
|
||||
"damage": -97,
|
||||
"jitter": 30,
|
||||
"distdraw": -4548,
|
||||
"piercing": 1057,
|
||||
"integrity": 319
|
||||
},
|
||||
"blueprint": {
|
||||
"id": 89,
|
||||
"name": "Rapid fire",
|
||||
"grade": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "F",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Multi-cannon",
|
||||
"mount": "Gimballed",
|
||||
"modifications": {
|
||||
"damage": 2437,
|
||||
"distdraw": 5487,
|
||||
"rof": 1120,
|
||||
"jitter": 58,
|
||||
"thermload": 1346,
|
||||
"power": 1009,
|
||||
"integrity": -202,
|
||||
"ammo": -2000
|
||||
},
|
||||
"blueprint": {
|
||||
"id": 88,
|
||||
"name": "Overcharged",
|
||||
"grade": 3,
|
||||
"special": {
|
||||
"id": 3,
|
||||
"name": "Corrosive shell"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"utility": [
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "I",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Heat Sink Launcher",
|
||||
"name": "Heat Sink Launcher",
|
||||
"modifications": {
|
||||
"ammo": 5000,
|
||||
"mass": 17684,
|
||||
"reload": 9707
|
||||
},
|
||||
"blueprint": {
|
||||
"id": 37,
|
||||
"name": "Ammo capacity",
|
||||
"grade": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "I",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Heat Sink Launcher",
|
||||
"name": "Heat Sink Launcher",
|
||||
"modifications": {
|
||||
"ammo": 5000,
|
||||
"mass": 18520,
|
||||
"reload": 8715
|
||||
},
|
||||
"blueprint": {
|
||||
"id": 37,
|
||||
"name": "Ammo capacity",
|
||||
"grade": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "I",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Chaff Launcher",
|
||||
"name": "Chaff Launcher"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Frame Shift Wake Scanner"
|
||||
}
|
||||
],
|
||||
"internal": [
|
||||
{
|
||||
"class": 3,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Shield Generator",
|
||||
"modifications": {
|
||||
"optmul": 1888,
|
||||
"explres": 455,
|
||||
"kinres": 546,
|
||||
"thermres": 1092,
|
||||
"brokenregen": -2614,
|
||||
"regen": -876,
|
||||
"distdraw": 463
|
||||
},
|
||||
"blueprint": {
|
||||
"id": 77,
|
||||
"name": "Reinforced",
|
||||
"grade": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"class": 3,
|
||||
"rating": "A",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"group": "Fuel Scoop"
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "E",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
null,
|
||||
{
|
||||
"class": 1,
|
||||
"rating": "C",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"group": "Scanner",
|
||||
"name": "Advanced Discovery Scanner"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {
|
||||
"class": 1,
|
||||
"hullCost": 2481550,
|
||||
"speed": 280,
|
||||
"boost": 380,
|
||||
"boostEnergy": 10,
|
||||
"agility": 6,
|
||||
"baseShieldStrength": 200,
|
||||
"baseArmour": 80,
|
||||
"hullMass": 35,
|
||||
"masslock": 7,
|
||||
"pipSpeed": 0.05,
|
||||
"moduleCostMultiplier": 1,
|
||||
"fuelCapacity": 8,
|
||||
"cargoCapacity": 8,
|
||||
"ladenMass": 104.25,
|
||||
"armour": 144,
|
||||
"shield": 404.19,
|
||||
"shieldCells": 0,
|
||||
"totalCost": 14059860,
|
||||
"unladenMass": 88.25,
|
||||
"totalDpe": 32.25,
|
||||
"totalExplDpe": 0,
|
||||
"totalKinDpe": 9.41,
|
||||
"totalThermDpe": 22.84,
|
||||
"totalDps": 53.8,
|
||||
"totalExplDps": 0,
|
||||
"totalKinDps": 17.44,
|
||||
"totalThermDps": 36.35,
|
||||
"totalSDps": 48.99,
|
||||
"totalExplSDps": 0,
|
||||
"totalKinSDps": 12.64,
|
||||
"totalThermSDps": 36.35,
|
||||
"totalEps": 9.84,
|
||||
"totalHps": 12.28,
|
||||
"shieldExplRes": 0.48,
|
||||
"shieldKinRes": 0.55,
|
||||
"shieldThermRes": 1.09,
|
||||
"hullExplRes": 1.4,
|
||||
"hullKinRes": 1.2,
|
||||
"hullThermRes": 1,
|
||||
"powerAvailable": 17.24,
|
||||
"powerRetracted": 11.3,
|
||||
"powerDeployed": 16.41,
|
||||
"unladenRange": 25.57,
|
||||
"fullTankRange": 23.92,
|
||||
"ladenRange": 22.09,
|
||||
"unladenFastestRange": 116.23,
|
||||
"ladenFastestRange": 107,
|
||||
"maxJumpCount": 5,
|
||||
"topSpeed": 386.56,
|
||||
"topBoost": 524.62
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
[
|
||||
{
|
||||
"buildText": "[Imaginary Ship]\nbla bla",
|
||||
"errorMsg": "No such ship found: \"Imaginary Ship\""
|
||||
},
|
||||
{
|
||||
"buildText": "[Viper]\nS: 1F/F Pulse Laser\nsome un-parseable nonsense\nS: 1F/F Pulse Laser\n",
|
||||
"errorMsg": "Error parsing: \"some un-parseable nonsense\""
|
||||
},
|
||||
{
|
||||
"buildText": "[Sidewinder]\nS: 2F/F Pulse Laser\nS: 1F/F Pulse Laser\n",
|
||||
"errorMsg": "2F Pulse Laser exceeds slot size: \"S: 2F/F Pulse Laser\""
|
||||
},
|
||||
{
|
||||
"buildText": "[Sidewinder]\nL: 2F/F Pulse Laser\nS: 1F/F Pulse Laser\n",
|
||||
"errorMsg": "No hardpoint slot available for: \"L: 2F/F Pulse Laser\""
|
||||
},
|
||||
{
|
||||
"buildText": "[Sidewinder]\nS: 1F/F Magic Thing\nS: 1F/F Pulse Laser\n",
|
||||
"errorMsg": "Unknown component: \"S: 1F/F Magic Thing\""
|
||||
}
|
||||
]
|
||||
@@ -1,32 +0,0 @@
|
||||
[
|
||||
{
|
||||
"shipId": "anaconda",
|
||||
"buildName": "Imported Anaconda",
|
||||
"buildCode": "0pyttFolodDsyf5------1717--------05044j-03--2h--00.Iw18ZlA=.Aw18ZlA=.",
|
||||
"buildText": "[Anaconda]\nS: 1F/F Pulse Laser\nS: 1F/F Pulse Laser\n\nBH: 1I Lightweight Alloy\nRB: 8E Power Plant\nTM: 7E Thrusters\nFH: 6E Frame Shift Drive\nEC: 5E Life Support\nPC: 8E Power Distributor\nSS: 8E Sensors\nFS: 5C Fuel Tank (Capacity: 32)\n\n7: 6E Cargo Rack (Capacity: 64)\n6: 5E Cargo Rack (Capacity: 32)\n6: 6E Shield Generator\n5: 4E Cargo Rack (Capacity: 16)\n4: 1E Basic Discovery Scanner\n2: 1E Cargo Rack (Capacity: 2)\n"
|
||||
},
|
||||
{
|
||||
"shipId": "anaconda",
|
||||
"buildName": "Imported Anaconda",
|
||||
"buildCode": "0pyttFolodDsyf5------1717--------05044j-03--2h--00.Iw18ZlA=.Aw18ZlA=.",
|
||||
"buildText": "\n\n \t[Anaconda]\nS: 1F/F Pulse Laser\nS: 1F/F Pulse Laser\n\nBH: 1I Lightweight Alloy\nRB: 8E Power Plant\nTM: 7E Thrusters\nFH: 6E Frame Shift Drive\nEC: 5E Life Support\nPC: 8E Power Distributor\nSS: 8E Sensors\nFS: 5C Fuel Tank (Capacity: 32)\n\n7: 6E Cargo Rack (Capacity: 64)\n6: 5E Cargo Rack (Capacity: 32)\n6: 6E Shield Generator\n5: 4E Cargo Rack (Capacity: 16)\n4: 1E Basic Discovery Scanner\n2: 1E Cargo Rack (Capacity: 2)\n"
|
||||
},
|
||||
{
|
||||
"shipId": "cobra_mk_iii",
|
||||
"buildName": "Imported Cobra Mk III",
|
||||
"buildCode": "0patcFeldd5sdf41712222503040202490f242h.Iw1-kA==.Aw1-kA==.",
|
||||
"buildText": "[Cobra Mk III]\nM: 1F/F Pulse Laser\nM: 1G/G Burst Laser\nS: 1E/T Fragment Cannon\nS: 1G/T Multi-cannon\nU: 0I Point Defence\nU: 0A Shield Booster\n\nBH: 1I Lightweight Alloy\nRB: 4A Power Plant\nTM: 4C Thrusters\nFH: 4E Frame Shift Drive\nEC: 3D Life Support\nPC: 2A Power Distributor\nSS: 3D Sensors\nFS: 4C Fuel Tank (Capacity: 16)\n\n4: 3E Cargo Rack (Capacity: 8)\n4: 3E Cargo Rack (Capacity: 8)\n4: 4E Shield Generator\n2: 2C Auto Field-Maintenance Unit\n2: 1E Standard Docking Computer\n2: 1E Basic Discovery Scanner\n---\nShield: 112.29 MJ\nPower : 10.45 MW retracted (67%)\n 12.16 MW deployed (78%)\n 15.60 MW available\nCargo : 16 T\nFuel : 16 T\nMass : 235.5 T empty\n 267.5 T full\nRange : 10.69 LY unladen\n 10.05 LY laden\nPrice : 2,929,040 CR\nRe-Buy: 146,452 CR @ 95% insurance\n"
|
||||
},
|
||||
{
|
||||
"shipId": "type_9_heavy",
|
||||
"buildName": "Imported Type-9 Heavy",
|
||||
"buildCode": "3pftsFklkdisif57e2k2f2h110001020306054j03022f01242i.Iw18eQ==.Aw18eQ==.",
|
||||
"buildText": "[Type-9 Heavy]\nM: 2D/G Fragment Cannon\nM: 2I/F Mine Launcher\nM: 2B/FD Missile Rack\nS: 1I/FS Torpedo Pylon\nS: 1F/F Burst Laser\nU: 0I Chaff Launcher\nU: 0F Electronic Countermeasure\nU: 0I Heat Sink Launcher\nU: 0I Point Defence\n\nBH: 1I Mirrored Surface Composite\nRB: 5A Power Plant\nTM: 7D Thrusters\nFH: 6A Frame Shift Drive\nEC: 5A Life Support\nPC: 4D Power Distributor\nSS: 4D Sensors\nFS: 5C Fuel Tank (Capacity: 32)\n\n8: 7E Cargo Rack (Capacity: 128)\n7: 6E Cargo Rack (Capacity: 64)\n6: 6E Shield Generator\n5: 4E Cargo Rack (Capacity: 16)\n4: 3E Cargo Rack (Capacity: 8)\n4: 1C Advanced Discovery Scanner\n3: 2E Cargo Rack (Capacity: 4)\n3: 1E Standard Docking Computer\n2: 1C Detailed Surface Scanner\n"
|
||||
},
|
||||
{
|
||||
"shipId": "vulture",
|
||||
"buildName": "Imported Vulture",
|
||||
"buildCode": "4patfFalddksif31e1e0e0j04044a0n532jf1.Iw19kA==.Aw19kA==.",
|
||||
"buildText": "[Vulture]\nL: 3E/G Pulse Laser\nL: 3E/G Pulse Laser\nU: 0A Frame Shift Wake Scanner\nU: 0A Kill Warrant Scanner\nU: 0A Shield Booster\nU: 0A Shield Booster\n\nBH: 1I Reactive Surface Composite\nRB: 4A Power Plant\nTM: 5A Thrusters\nFH: 4A Frame Shift Drive\nEC: 3D Life Support\nPC: 5A Power Distributor\nSS: 4D Sensors\nFS: 3C Fuel Tank (Capacity: 8)\n\n5: 5A Shield Generator\n4: 4A Auto Field-Maintenance Unit\n2: 2A Shield Cell Bank\n1: 1A Fuel Scoop\n1: 1C Fuel Tank (Capacity: 2)"
|
||||
}
|
||||
]
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"type_6_transporter": {
|
||||
"Cargo": "A0p0tdFal8d8s8f4-----04040303430101-.Iw18UA==.Aw18UA==.",
|
||||
"Miner": "A0p5tdFal8d8s8f42l2l---040403451q0101-.Iw18UA==.Aw18UA==.",
|
||||
"Hopper": "A0p0tdFal8d0s8f41717---030302024300--.Iw18UA==.Aw18UA==."
|
||||
},
|
||||
"type_7_transport": {
|
||||
"Cargo": "A0p0tiFfliddsdf5--------0505040403480101--.Iw18eQ==.Aw18eQ==.",
|
||||
"Miner": "A0pdtiFflid8sdf5--2l2l----0505041v03450000--.Iw18eQ==.Aw18eQ==."
|
||||
},
|
||||
"federal_dropship": {
|
||||
"Cargo": "A0pdtiFflnddsif4-1717------05040448--020201-.Iw18RQ==.Aw18RQ==."
|
||||
},
|
||||
"asp": {
|
||||
"Miner": "A2pftfFflidfskf50s0s24242l2l---04054a1q02022o27-.Iw18eQ==.Aw18eQ==."
|
||||
},
|
||||
"imperial_clipper": {
|
||||
"Cargo": "A0p5tiFflndisnf4--0s0s----0605450302020101-.Iw18WQ==.Aw18WQ==.",
|
||||
"Dream": "A2pktkFflndpskf40v0v0s0s0404040n4k5n5d2b29292o--.AwRj4yWU1Yg=.CwBhCYy6YRigzPIA.",
|
||||
"Current": "A0patkFflndfskf4-----------------.AwRj4yWU1Yg=.CwBhCYy6YRigzPIA."
|
||||
},
|
||||
"type_9_heavy": {
|
||||
"Current": "A0patsFklndnsif6---------0706054a0303020224--.AwRj4yo5iA==.EwBhEYy6d6g=."
|
||||
},
|
||||
"python": {
|
||||
"Cargo": "A0patnFflidsssf5---------050505040448020201-.Iw18eAMQ.Aw18RQ==.",
|
||||
"Miner": "A0pktkFflidpspf50v0v0v2m2m0404--050505Ce4a1v02022o-.Iw18eAMQ.IwBhBYy6dkCYRA==.",
|
||||
"Dream": "A2pptkFfliduspf50v0v0v27270404040m5n5n4f2d2d032t0201-.Iw1+gDByUA==.EwBhEYy6e0VEA===.",
|
||||
"Missile": "A0pttoFjljdystf52f2g2d2ePh----04044j03---00--.Iw18eAMQ.Aw18RQ==."
|
||||
},
|
||||
"anaconda": {
|
||||
"Dream": "A4putpFklndzsuf52c0o0o0o1m1m0q0q0404040l0b0100004k5n5n112d2d04-0303326b-.AwRj4yo5dzhA.MwBhCYy6duvARhEA.",
|
||||
"Cargo": "A0patnFklndnsxf5----------------06050505040404-45030301-.Iw18ZUAxA===.Aw18ZXEA.",
|
||||
"Current": "A0patnFklndksxf5----------------06050505040404-03034524-.Iw18ZUAxA===.Aw18ZXEA.",
|
||||
"Explorer": "A0patnFklndksxf5--------0202------f7050505040s37--2i4524-.AwRj4yVKJ9jCA===.AwhMIyumQRgkA===.",
|
||||
"Test": "A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b-.Iw18ZUAxA===.Aw18ZXEA."
|
||||
},
|
||||
"diamondback_explorer": {
|
||||
"Explorer": "A0p0tdFfldddsdf5---0202--320p432i----.AwRj4zTZaA==.AwiMIyqo."
|
||||
},
|
||||
"vulture": {
|
||||
"Bounty Hunter": "A3patcFalddksff31e1e0404-0l4a-5d27662j--.AwRj4z2Gg===.MwBhBYy6oJmAjLMQ."
|
||||
},
|
||||
"fer_de_lance": {
|
||||
"Attack": "A2pfthFalidpsff31r0s0s0s0s000404-04-4a-5d27--.Iw18aAMQ.CwBhrSu8EZxEA===."
|
||||
},
|
||||
"eagle": {
|
||||
"Figther": "A4p0t5F5l3d5s5f20p0p24-4053-2j---.Iw18gDJQ.Aw19kA==."
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"builds": {
|
||||
"type_6_transporter": {
|
||||
"Cargo": "02A4D4A2D2D2D4C-----04040303430101",
|
||||
"Miner": "03A4D4A2D2D2D4C2l2l---040403451q0101",
|
||||
"Hopper": "02A4D4A2D1A2D4C1717---030302024300-"
|
||||
},
|
||||
"type_7_transport": {
|
||||
"Cargo": "02A5D5A4D3D3D5C--------0505040403480101",
|
||||
"Miner": "04D5D5A4D2D3D5C--2l2l----0505041v03450000"
|
||||
},
|
||||
"federal_dropship": {
|
||||
"Cargo": "04D5D5A5D3D4D4C-1717------05040448020201"
|
||||
},
|
||||
"asp": {
|
||||
"Miner": "25A5A5A4D4A5A5C0s0s24242l2l---04054a1q02022o27"
|
||||
},
|
||||
"imperial_clipper": {
|
||||
"Cargo": "03A5D5A5D4D5D4C--0s0s----0605450302020101",
|
||||
"Dream": "26A6A5A5D6A5A4C0v0v0s0s0404040n4k5n5d2b29292o-.AwRj4yWU1I==.CwBhCYy6YRigzLIA",
|
||||
"Current": "04A6A5A5D4A5A4C----------------.AwRj4yWU1I==.CwBhCYy6YRigzLIA"
|
||||
},
|
||||
"type_9_heavy": {
|
||||
"Current": "04A7D6A5D5D4D6C---------0706054a0303020224.AwRj4yoo.EwBhEYy6dsg="
|
||||
},
|
||||
"python": {
|
||||
"Cargo": "04A6D5A4D6D6D5C---------050505040448020201.Iw18eQ==.Aw18eQ==",
|
||||
"Miner": "06A6A5A4D6A6A5C0v0v0v2m2m0404--050505Ce4a1v02022o.Iw18eQ==.IwBhBYy6dkCYg===",
|
||||
"Dream": "27A6A5A4D7A6A5C0v0v0v27270404040m5n5n4f2d2d032t0201.Iw1+gDBxA===.EwBhEYy6e0WEA==="
|
||||
},
|
||||
"anaconda": {
|
||||
"Dream": "48A7A6A5D8A8A5C2c0o0o0o1m1m0q0q0404040l0b0100004k5n5n112d2d040303326b.AwRj4yo5dig=.MwBhCYy6du3ARiA=",
|
||||
"Cargo": "04A6D6A5D5D8D5C----------------0605050504040445030301.Iw18ZlA=.Aw18ZlA=",
|
||||
"Current": "04A6D6A5D5A8D5C----------------0605050504040403034524.Iw18ZlA=.Aw18ZlA=",
|
||||
"Explorer": "04A6D6A5D5A8D5C--------0202------f7050505040s372f2i4524.AwRj4yVKJthA.AwhMIyungRhEA==="
|
||||
},
|
||||
"diamondback_explorer": {
|
||||
"Explorer": "02A4D5A3D3D3D5C---0202--320p432i2f.AwRj4zTI.AwiMIypI"
|
||||
},
|
||||
"vulture": {
|
||||
"Bounty Hunter": "34A4C4A3D5A4A3C1e1e0404-0l4a5d27662j.AwRj4y2I.MwBhBYy6wJmAjLIA"
|
||||
},
|
||||
"fer_de_lance": {
|
||||
"Attack": "25A5C4A4D6A4A3C1r0s0s0s0s000404-04-4a-5d27-.Iw18aQ==.CwBhrSu8EZyA"
|
||||
},
|
||||
"eagle": {
|
||||
"Figther": "42A3A3A1D2A2A2C0p0p24-40532j.AwRj49iA.AwgsIkEZigmIA==="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
[
|
||||
{
|
||||
"header": {
|
||||
"appName": "Inara",
|
||||
"appVersion": "1.0",
|
||||
"appURL": "https:\/\/inara.cz\/cmdr-fleet\/123\/123\/",
|
||||
"appCustomProperties": {
|
||||
"inaraCommanderID": 123,
|
||||
"inaraShipID": 123
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"Ship": "krait_mkii",
|
||||
"ShipID": 7,
|
||||
"ShipName": "pancake hammer",
|
||||
"ShipIdent": "PH-01",
|
||||
"HullValue": 44160710,
|
||||
"ModulesValue": 111274094,
|
||||
"Rebuy": 7771743,
|
||||
"Modules": [
|
||||
{
|
||||
"Slot": "largehardpoint1",
|
||||
"Item": "hpt_mininglaser_fixed_small",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "largehardpoint2",
|
||||
"Item": "hpt_cannon_gimbal_large",
|
||||
"On": true,
|
||||
"Engineering": {
|
||||
"BlueprintName": "weapon_overcharged",
|
||||
"Level": 2,
|
||||
"Quality": 1,
|
||||
"ExperimentalEffect": "special_auto_loader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "largehardpoint3",
|
||||
"Item": "hpt_cannon_gimbal_large",
|
||||
"On": true,
|
||||
"Engineering": {
|
||||
"BlueprintName": "weapon_overcharged",
|
||||
"Level": 2,
|
||||
"Quality": 1,
|
||||
"ExperimentalEffect": "special_auto_loader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "mediumhardpoint1",
|
||||
"Item": "hpt_basicmissilerack_fixed_medium",
|
||||
"On": true,
|
||||
"Engineering": {
|
||||
"BlueprintName": "weapon_highcapacity",
|
||||
"Level": 5,
|
||||
"Quality": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "mediumhardpoint2",
|
||||
"Item": "hpt_basicmissilerack_fixed_medium",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "tinyhardpoint1",
|
||||
"Item": "hpt_heatsinklauncher_turret_tiny",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "tinyhardpoint2",
|
||||
"Item": "hpt_cloudscanner_size0_class3",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "tinyhardpoint3",
|
||||
"Item": "hpt_shieldbooster_size0_class5",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "tinyhardpoint4",
|
||||
"Item": "hpt_shieldbooster_size0_class5",
|
||||
"On": true,
|
||||
"Priority": 1
|
||||
},
|
||||
{
|
||||
"Slot": "slot01_size6",
|
||||
"Item": "int_cargorack_size6_class1",
|
||||
"On": true,
|
||||
"Priority": 1
|
||||
},
|
||||
{
|
||||
"Slot": "slot02_size6",
|
||||
"Item": "int_cargorack_size6_class1",
|
||||
"On": true,
|
||||
"Priority": 1
|
||||
},
|
||||
{
|
||||
"Slot": "slot03_size5",
|
||||
"Item": "int_guardianfsdbooster_size5",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "slot04_size5",
|
||||
"Item": "int_fighterbay_size5_class1",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "slot05_size4",
|
||||
"Item": "int_shieldgenerator_size4_class5",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "slot06_size3",
|
||||
"Item": "int_dronecontrol_collection_size3_class4",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "slot07_size3",
|
||||
"Item": "int_dronecontrol_collection_size3_class4",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "slot08_size2",
|
||||
"Item": "int_refinery_size2_class2",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "slot09_size1",
|
||||
"Item": "int_dronecontrol_prospector_size1_class4",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "powerplant",
|
||||
"Item": "int_powerplant_size7_class5",
|
||||
"On": true,
|
||||
"Priority": 1
|
||||
},
|
||||
{
|
||||
"Slot": "mainengines",
|
||||
"Item": "int_engine_size6_class5",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "frameshiftdrive",
|
||||
"Item": "int_hyperdrive_size5_class5",
|
||||
"On": true,
|
||||
"Engineering": {
|
||||
"BlueprintName": "fsd_longrange",
|
||||
"Level": 2,
|
||||
"Quality": 0.861
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "lifesupport",
|
||||
"Item": "int_lifesupport_size4_class2",
|
||||
"On": true,
|
||||
"Priority": 3
|
||||
},
|
||||
{
|
||||
"Slot": "powerdistributor",
|
||||
"Item": "int_powerdistributor_size7_class5",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "radar",
|
||||
"Item": "int_sensors_size6_class2",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "fueltank",
|
||||
"Item": "int_fueltank_size5_class3",
|
||||
"On": true,
|
||||
"Priority": 1
|
||||
},
|
||||
{
|
||||
"Slot": "armour",
|
||||
"Item": "krait_mkii_armour_grade3",
|
||||
"On": true,
|
||||
"Priority": 1,
|
||||
"Engineering": {
|
||||
"BlueprintName": "armour_heavyduty",
|
||||
"Level": 5,
|
||||
"Quality": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"header": {
|
||||
"appName": "Inara",
|
||||
"appVersion": "1.0",
|
||||
"appURL": "https:\/\/inara.cz\/cmdr-fleet\/123\/123\/",
|
||||
"appCustomProperties": {
|
||||
"inaraCommanderID": 123,
|
||||
"inaraShipID": 123
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"Ship": "diamondbackxl",
|
||||
"ShipID": 11,
|
||||
"ShipName": "star Hopper",
|
||||
"ShipIdent": "PH-02",
|
||||
"HullValue": 1615649,
|
||||
"ModulesValue": 16981039,
|
||||
"Rebuy": 929837,
|
||||
"Modules": [
|
||||
{
|
||||
"Slot": "tinyhardpoint1",
|
||||
"Item": "hpt_heatsinklauncher_turret_tiny",
|
||||
"On": true,
|
||||
"Value": 3072
|
||||
},
|
||||
{
|
||||
"Slot": "slot01_size4",
|
||||
"Item": "int_fuelscoop_size4_class5",
|
||||
"On": true,
|
||||
"Priority": 3,
|
||||
"Value": 2862364
|
||||
},
|
||||
{
|
||||
"Slot": "slot02_size4",
|
||||
"Item": "int_guardianfsdbooster_size4",
|
||||
"On": true,
|
||||
"Value": 2847499
|
||||
},
|
||||
{
|
||||
"Slot": "slot03_size3",
|
||||
"Item": "int_shieldgenerator_size3_class2",
|
||||
"On": true,
|
||||
"Value": 18812,
|
||||
"Engineering": {
|
||||
"BlueprintName": "shieldgenerator_thermic",
|
||||
"Level": 3,
|
||||
"Quality": 1,
|
||||
"ExperimentalEffect": "special_shield_health"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "slot04_size3",
|
||||
"Item": "int_repairer_size3_class5",
|
||||
"On": true,
|
||||
"Value": 2302911
|
||||
},
|
||||
{
|
||||
"Slot": "slot05_size2",
|
||||
"Item": "int_buggybay_size2_class2",
|
||||
"On": true,
|
||||
"Priority": 3,
|
||||
"Value": 21600
|
||||
},
|
||||
{
|
||||
"Slot": "slot06_size2",
|
||||
"Item": "int_cargorack_size2_class1",
|
||||
"On": true,
|
||||
"Priority": 1,
|
||||
"Value": 2852
|
||||
},
|
||||
{
|
||||
"Slot": "slot07_size1",
|
||||
"Item": "int_supercruiseassist",
|
||||
"On": true,
|
||||
"Priority": 3,
|
||||
"Value": 9121
|
||||
},
|
||||
{
|
||||
"Slot": "slot08_size1",
|
||||
"Item": "int_detailedsurfacescanner_tiny",
|
||||
"On": true,
|
||||
"Value": 250000,
|
||||
"Engineering": {
|
||||
"BlueprintName": "sensor_expanded",
|
||||
"Level": 5,
|
||||
"Quality": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "powerplant",
|
||||
"Item": "int_powerplant_size4_class5",
|
||||
"On": true,
|
||||
"Priority": 1,
|
||||
"Value": 1441233,
|
||||
"Engineering": {
|
||||
"BlueprintName": "powerplant_boosted",
|
||||
"Level": 1,
|
||||
"Quality": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "mainengines",
|
||||
"Item": "int_engine_size4_class5",
|
||||
"On": true,
|
||||
"Value": 1610080,
|
||||
"Engineering": {
|
||||
"BlueprintName": "engine_dirty",
|
||||
"Level": 5,
|
||||
"Quality": 1,
|
||||
"ExperimentalEffect": "special_engine_lightweight"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "frameshiftdrive",
|
||||
"Item": "int_hyperdrive_size5_class5",
|
||||
"On": true,
|
||||
"Value": 5103953,
|
||||
"Engineering": {
|
||||
"BlueprintName": "fsd_longrange",
|
||||
"Level": 5,
|
||||
"Quality": 1,
|
||||
"ExperimentalEffect": "special_fsd_lightweight"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "lifesupport",
|
||||
"Item": "int_lifesupport_size3_class2",
|
||||
"On": true,
|
||||
"Value": 10133,
|
||||
"Engineering": {
|
||||
"BlueprintName": "misc_lightweight",
|
||||
"Level": 3,
|
||||
"Quality": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "powerdistributor",
|
||||
"Item": "int_powerdistributor_size4_class5",
|
||||
"On": true,
|
||||
"Value": 389022,
|
||||
"Engineering": {
|
||||
"BlueprintName": "powerdistributor_highfrequency",
|
||||
"Level": 4,
|
||||
"Quality": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "radar",
|
||||
"Item": "int_sensors_size3_class2",
|
||||
"On": true,
|
||||
"Value": 10133,
|
||||
"Engineering": {
|
||||
"BlueprintName": "sensor_lightweight",
|
||||
"Level": 5,
|
||||
"Quality": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "fueltank",
|
||||
"Item": "int_fueltank_size5_class3",
|
||||
"On": true,
|
||||
"Priority": 1,
|
||||
"Value": 97754
|
||||
},
|
||||
{
|
||||
"Slot": "armour",
|
||||
"Item": "diamondbackxl_armour_grade1",
|
||||
"On": true,
|
||||
"Priority": 1,
|
||||
"Engineering": {
|
||||
"BlueprintName": "armour_heavyduty",
|
||||
"Level": 5,
|
||||
"Quality": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"krait_mkii": {
|
||||
"Imported pancake hammer": "A2pptkFflidussf52l1o1o2g2g020g040405051Ofr45C9C91oP3.Iw18eQ==.AwRgzKIkA===."
|
||||
},
|
||||
"diamondback_explorer": {
|
||||
"Imported star Hopper": "A0pataFflddfsdf5---02---321P430iv6013w2i.Iw18SQ==.AwRm44GYpKg=."
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
[
|
||||
{
|
||||
"header": {
|
||||
"appName": "Inara",
|
||||
"appVersion": "1.0",
|
||||
"appURL": "https:\/\/inara.cz\/cmdr-fleet\/123\/123\/",
|
||||
"appCustomProperties": {
|
||||
"inaraCommanderID": 123,
|
||||
"inaraShipID": 123
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"Ship": "krait_mkii",
|
||||
"ShipID": 7,
|
||||
"ShipName": "pancake hammer",
|
||||
"ShipIdent": "PH-01",
|
||||
"HullValue": 44160710,
|
||||
"ModulesValue": 111274094,
|
||||
"Rebuy": 7771743,
|
||||
"Modules": [
|
||||
{
|
||||
"Slot": "largehardpoint1",
|
||||
"Item": "hpt_mininglaser_fixed_small",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "largehardpoint2",
|
||||
"Item": "hpt_cannon_gimbal_large",
|
||||
"On": true,
|
||||
"Engineering": {
|
||||
"BlueprintName": "weapon_overcharged",
|
||||
"Level": 2,
|
||||
"Quality": 1,
|
||||
"ExperimentalEffect": "special_auto_loader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "largehardpoint3",
|
||||
"Item": "hpt_cannon_gimbal_large",
|
||||
"On": true,
|
||||
"Engineering": {
|
||||
"BlueprintName": "weapon_overcharged",
|
||||
"Level": 2,
|
||||
"Quality": 1,
|
||||
"ExperimentalEffect": "special_auto_loader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "mediumhardpoint1",
|
||||
"Item": "hpt_basicmissilerack_fixed_medium",
|
||||
"On": true,
|
||||
"Engineering": {
|
||||
"BlueprintName": "weapon_highcapacity",
|
||||
"Level": 5,
|
||||
"Quality": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "mediumhardpoint2",
|
||||
"Item": "hpt_basicmissilerack_fixed_medium",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "tinyhardpoint1",
|
||||
"Item": "hpt_heatsinklauncher_turret_tiny",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "tinyhardpoint2",
|
||||
"Item": "hpt_cloudscanner_size0_class3",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "tinyhardpoint3",
|
||||
"Item": "hpt_shieldbooster_size0_class5",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "tinyhardpoint4",
|
||||
"Item": "hpt_shieldbooster_size0_class5",
|
||||
"On": true,
|
||||
"Priority": 1
|
||||
},
|
||||
{
|
||||
"Slot": "slot01_size6",
|
||||
"Item": "int_cargorack_size6_class1",
|
||||
"On": true,
|
||||
"Priority": 1
|
||||
},
|
||||
{
|
||||
"Slot": "slot02_size6",
|
||||
"Item": "int_cargorack_size6_class1",
|
||||
"On": true,
|
||||
"Priority": 1
|
||||
},
|
||||
{
|
||||
"Slot": "slot03_size5",
|
||||
"Item": "int_guardianfsdbooster_size5",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "slot04_size5",
|
||||
"Item": "int_fighterbay_size5_class1",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "slot05_size4",
|
||||
"Item": "int_shieldgenerator_size4_class5",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "slot06_size3",
|
||||
"Item": "int_dronecontrol_collection_size3_class4",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "slot07_size3",
|
||||
"Item": "int_dronecontrol_collection_size3_class4",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "slot08_size2",
|
||||
"Item": "int_refinery_size2_class2",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "slot09_size1",
|
||||
"Item": "int_dronecontrol_prospector_size1_class4",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "powerplant",
|
||||
"Item": "int_powerplant_size7_class5",
|
||||
"On": true,
|
||||
"Priority": 1
|
||||
},
|
||||
{
|
||||
"Slot": "mainengines",
|
||||
"Item": "int_engine_size6_class5",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "frameshiftdrive",
|
||||
"Item": "int_hyperdrive_size5_class5",
|
||||
"On": true,
|
||||
"Engineering": {
|
||||
"BlueprintName": "fsd_longrange",
|
||||
"Level": 2,
|
||||
"Quality": 0.861
|
||||
}
|
||||
},
|
||||
{
|
||||
"Slot": "lifesupport",
|
||||
"Item": "int_lifesupport_size4_class2",
|
||||
"On": true,
|
||||
"Priority": 3
|
||||
},
|
||||
{
|
||||
"Slot": "powerdistributor",
|
||||
"Item": "int_powerdistributor_size7_class5",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "radar",
|
||||
"Item": "int_sensors_size6_class2",
|
||||
"On": true
|
||||
},
|
||||
{
|
||||
"Slot": "fueltank",
|
||||
"Item": "int_fueltank_size5_class3",
|
||||
"On": true,
|
||||
"Priority": 1
|
||||
},
|
||||
{
|
||||
"Slot": "armour",
|
||||
"Item": "krait_mkii_armour_grade3",
|
||||
"On": true,
|
||||
"Priority": 1,
|
||||
"Engineering": {
|
||||
"BlueprintName": "armour_heavyduty",
|
||||
"Level": 5,
|
||||
"Quality": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,67 +0,0 @@
|
||||
{
|
||||
"builds": {
|
||||
"type_6_transporter": {
|
||||
"Cargo": "02A4D4A2D2D2D4C-----04040303430101",
|
||||
"Miner": "03A4D4A2D2D2D4C2l2l---040403451q0101",
|
||||
"Hopper": "02A4D4A2D1A2D4C1717---030302024300-"
|
||||
},
|
||||
"type_7_transport": {
|
||||
"Cargo": "02A5D5A4D3D3D5C--------0505040403480101",
|
||||
"Miner": "04D5D5A4D2D3D5C--2l2l----0505041v03450000"
|
||||
},
|
||||
"federal_dropship": {
|
||||
"Cargo": "04D5D5A5D3D4D4C-1717------05040448020201"
|
||||
},
|
||||
"asp": {
|
||||
"Miner": "25A5A5A4D4A5A5C0s0s24242l2l---04054a1q02022o27"
|
||||
},
|
||||
"cobra_mk_iii": {
|
||||
"Example": "24A4A4A3D3A3A4C0s0s2d2d0m0445032b2o2753.AwRj4yKA.CwBhEYyrKhmMQ==="
|
||||
},
|
||||
"imperial_clipper": {
|
||||
"Cargo": "03A5D5A5D4D5D4C--0s0s----0605450302020101",
|
||||
"Multi-purpose": "26A4A5A5D6A5A4C0v0v272704090j0h064f2c0302020101",
|
||||
"Current": "05A6D5A5D6A5A4C0v0v27270404050n4m05035d29292o01.AwRj4yrI.AwhMIyuBGNiA",
|
||||
"Dream": "26A6A5A5D6A5A4C0v0v0s0s04040c0n064f5d2b02022o0d.AwRj49UlmI==.AwiMIyuo"
|
||||
},
|
||||
"type_9_heavy": {
|
||||
"Cargo": "04A6D6A5D4D4D5C---------07064f040303010201.AwRj4yoo.EwBhEYy6dsg="
|
||||
},
|
||||
"python": {
|
||||
"Cargo": "04A6D5A4D6D6D5C---------050505044a03020201",
|
||||
"Miner": "04A6D5A4D6D6D5C---2m2m----050505044d1v02022o"
|
||||
},
|
||||
"anaconda": {
|
||||
"Dream": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404040l0b0100034k5n05050404040303326b.AwRj4yo5dig=.MwBhEYy6duwEziA=",
|
||||
"Cargo": "03A7D6A5D4D8D5C----------------060505054d040403030301.AwRj4yuqg===.Aw18ZlA=",
|
||||
"Modified": "0pyttFolodDsyf5------1717--------05044j-03----2h00.Iw18ZlA=.Aw18ZlA=.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA=="
|
||||
},
|
||||
"diamondback_explorer": {
|
||||
"Explorer": "02A4D5A3D3D3D5C-------320p432i2f.AwRj4zTI.AwiMIypI"
|
||||
}
|
||||
},
|
||||
"comparisons": {
|
||||
"Test": {
|
||||
"facets": [ 9, 6, 4, 1, 3, 2 ],
|
||||
"builds": [
|
||||
{
|
||||
"shipId": "anaconda",
|
||||
"buildName": "Dream"
|
||||
},
|
||||
{
|
||||
"shipId": "asp",
|
||||
"buildName": "Miner"
|
||||
},
|
||||
{
|
||||
"shipId": "diamondback_explorer",
|
||||
"buildName": "Explorer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"insurance": "Beta",
|
||||
"discounts": [
|
||||
1,
|
||||
1
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,90 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,368 +0,0 @@
|
||||
jest.unmock('../src/app/stores/Persist');
|
||||
jest.unmock('../src/app/components/TranslatedComponent');
|
||||
jest.unmock('../src/app/components/ModalImport');
|
||||
jest.unmock('prop-types');
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import TU from 'react-testutils-additions';
|
||||
import Utils from './testUtils';
|
||||
import { getLanguage } from '../src/app/i18n/Language';
|
||||
|
||||
describe('Import Modal', function() {
|
||||
|
||||
let MockRouter = require('../src/app/Router').default;
|
||||
const Persist = require('../src/app/stores/Persist').default;
|
||||
const ModalImport = require('../src/app/components/ModalImport').default;
|
||||
const mockContext = {
|
||||
language: getLanguage('en'),
|
||||
sizeRatio: 1,
|
||||
openMenu: jest.fn(),
|
||||
closeMenu: jest.fn(),
|
||||
showModal: jest.fn(),
|
||||
hideModal: jest.fn(),
|
||||
tooltip: jest.fn(),
|
||||
termtip: jest.fn(),
|
||||
onWindowResize: jest.fn()
|
||||
};
|
||||
|
||||
let modal, render, ContextProvider = Utils.createContextProvider(mockContext);
|
||||
|
||||
/**
|
||||
* Clear saved builds, and reset React DOM
|
||||
*/
|
||||
function reset() {
|
||||
MockRouter.go.mockClear();
|
||||
Persist.deleteAll();
|
||||
render = TU.renderIntoDocument(<ContextProvider><ModalImport /></ContextProvider>);
|
||||
modal = TU.findRenderedComponentWithType(render, ModalImport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate user import text entry / paste
|
||||
* @param {string} text Import text / raw data
|
||||
*/
|
||||
function pasteText(text) {
|
||||
let textarea = TU.findRenderedDOMComponentWithTag(render, 'textarea');
|
||||
TU.Simulate.change(textarea, { target: { value: text } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate click on Proceed button
|
||||
*/
|
||||
function clickProceed() {
|
||||
let proceedButton = TU.findRenderedDOMComponentWithId(render, 'proceed');
|
||||
TU.Simulate.click(proceedButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate click on Import button
|
||||
*/
|
||||
function clickImport() {
|
||||
let importButton = TU.findRenderedDOMComponentWithId(render, 'import');
|
||||
TU.Simulate.click(importButton);
|
||||
}
|
||||
|
||||
describe('Import Backup', function() {
|
||||
|
||||
beforeEach(reset);
|
||||
|
||||
it('imports a valid backup', function() {
|
||||
let importData = require('./fixtures/valid-backup');
|
||||
let importString = JSON.stringify(importData);
|
||||
|
||||
expect(modal.state.importValid).toEqual(false);
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
pasteText(importString);
|
||||
expect(modal.state.importValid).toBe(true);
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
expect(modal.state.builds).toEqual(importData.builds);
|
||||
expect(modal.state.comparisons).toEqual(importData.comparisons);
|
||||
expect(modal.state.shipDiscount).toEqual(importData.discounts[0]);
|
||||
expect(modal.state.moduleDiscount).toEqual(importData.discounts[1]);
|
||||
expect(modal.state.insurance).toBe(importData.insurance.toLowerCase());
|
||||
clickProceed();
|
||||
expect(modal.state.processed).toBe(true);
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
clickImport();
|
||||
expect(Persist.getBuilds()).toEqual(importData.builds);
|
||||
expect(Persist.getComparisons()).toEqual(importData.comparisons);
|
||||
expect(Persist.getInsurance()).toEqual(importData.insurance.toLowerCase());
|
||||
expect(Persist.getShipDiscount()).toEqual(importData.discounts[0]);
|
||||
expect(Persist.getModuleDiscount()).toEqual(importData.discounts[1]);
|
||||
});
|
||||
|
||||
it('imports an old valid backup', function() {
|
||||
const importData = require('./fixtures/old-valid-export');
|
||||
const importStr = JSON.stringify(importData);
|
||||
|
||||
pasteText(importStr);
|
||||
expect(modal.state.builds).toEqual(importData.builds);
|
||||
expect(modal.state.importValid).toBe(true);
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
clickProceed();
|
||||
expect(modal.state.processed).toBeTruthy();
|
||||
clickImport();
|
||||
expect(Persist.getBuilds()).toEqual(importData.builds);
|
||||
});
|
||||
|
||||
it('catches an invalid backup', function() {
|
||||
const importData = require('./fixtures/valid-backup');
|
||||
let invalidImportData = Object.assign({}, importData);
|
||||
// Remove Asp Miner build used in comparison
|
||||
delete(invalidImportData.builds.asp);
|
||||
|
||||
pasteText('"this is not valid"');
|
||||
expect(modal.state.importValid).toBeFalsy();
|
||||
expect(modal.state.errorMsg).toEqual('Must be an object or array!');
|
||||
|
||||
pasteText('{ "builds": "Should not be a string" }');
|
||||
expect(modal.state.importValid).toBeFalsy();
|
||||
expect(modal.state.errorMsg).toEqual('builds must be an object!');
|
||||
|
||||
pasteText(JSON.stringify(importData).replace(/anaconda/g, 'invalid_ship'));
|
||||
expect(modal.state.importValid).toBeFalsy();
|
||||
expect(Object.keys(modal.state.builds)).not.toContain('anaconda');
|
||||
|
||||
pasteText(JSON.stringify(importData).replace('Dream', ''));
|
||||
expect(modal.state.importValid).toBeFalsy();
|
||||
expect(Object.keys(modal.state.builds.imperial_clipper).length).toEqual(3);
|
||||
|
||||
pasteText(JSON.stringify(invalidImportData));
|
||||
expect(modal.state.importValid).toBeFalsy();
|
||||
expect(modal.state.errorMsg).toEqual('asp build "Miner" data is missing!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import Detailed V3 Build', function() {
|
||||
|
||||
beforeEach(reset);
|
||||
|
||||
it('imports a valid v3 build', function() {
|
||||
const importData = require('./fixtures/anaconda-test-detailed-export-v3');
|
||||
pasteText(JSON.stringify(importData));
|
||||
|
||||
expect(modal.state.importValid).toBeTruthy();
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
expect(modal.state.singleBuild).toBe(true);
|
||||
clickProceed();
|
||||
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/anaconda?code=A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b-.AwRj4zNLeI%3D%3D.CwBhCYzBGW9qCTSqq5JA.&bn=Test%20My%20Ship');
|
||||
});
|
||||
|
||||
it('catches an invalid build', function() {
|
||||
const importData = require('./fixtures/anaconda-test-detailed-export-v3');
|
||||
pasteText(JSON.stringify(importData).replace('references', 'refs'));
|
||||
|
||||
expect(modal.state.importValid).toBeFalsy();
|
||||
expect(modal.state.errorMsg).toEqual('Anaconda Build "Test My Ship": Invalid data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import Detailed V4 Build', function() {
|
||||
|
||||
beforeEach(reset);
|
||||
|
||||
it('imports a valid v4 build', function() {
|
||||
const importData = require('./fixtures/anaconda-test-detailed-export-v4');
|
||||
pasteText(JSON.stringify(importData));
|
||||
|
||||
expect(modal.state.importValid).toBeTruthy();
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
expect(modal.state.singleBuild).toBe(true);
|
||||
clickProceed();
|
||||
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/anaconda?code=A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b-.AwRj4zNLeI%3D%3D.CwBhCYzBGW9qCTSqq5JA.H4sIAAAAAAAAE2MUe8HMwPD%2FPwMAAGvB0AkAAAA%3D&bn=Test%20My%20Ship');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import Detailed Engineered V4 Build', function() {
|
||||
|
||||
beforeEach(reset);
|
||||
|
||||
it('imports a valid v4 build', function() {
|
||||
const importData = require('./fixtures/asp-test-detailed-export-v4');
|
||||
pasteText(JSON.stringify(importData));
|
||||
|
||||
expect(modal.state.importValid).toBeTruthy();
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
expect(modal.state.singleBuild).toBe(true);
|
||||
clickProceed();
|
||||
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/asp?code=A0pftiFflfddsnf5------020202033c044002v6-2i-.AwRj4yvYg%3D%3D%3D.CwRgDBldHn5A.H4sIAAAAAAAAE2P858DAwPCXEUhwHPvx%2F78YG5AltB7I%2F8%2F0TwImJboDSPJ%2F%2B%2Ff%2Fv%2FKlX%2F%2F%2Fi3AwMTBIfARK%2FGf%2BJwVSxArStVAYqOjvz%2F%2F%2FJVo5GRhE2IBc4SKQSSz%2FDGEmCa398P8%2F%2F2%2BgTf%2F%2FA7kMAExxqlSAAAAA&bn=Multi-purpose%20Asp%20Explorer');
|
||||
});
|
||||
|
||||
it('imports a valid v4 build with modifications', function() {
|
||||
const importData = require('./fixtures/courier-test-detailed-export-v4');
|
||||
pasteText(JSON.stringify(importData));
|
||||
|
||||
expect(modal.state.importValid).toBeTruthy();
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
expect(modal.state.singleBuild).toBe(true);
|
||||
clickProceed();
|
||||
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/imperial_courier?code=A0patzF5l0das8f31a1a270202000e402t0101----.AwRj4zOYg%3D%3D%3D.CwRgDBldLuZA.H4sIAAAAAAAAE12OPUvDYBSFT1OTfkRJjUkbbC3Yj8mlODgUISAtdOlety5ODv0Vgji7O7kJ%2FgzBQX%2BEY7Gg0NKhfY%2FnHQLFDBdynufe9%2BRMCmCb06g29oCgacjiRx6gY6oWKUT8UgLaszqQfHmSnpVFN1uSeXNsJVcj%2FA2EHlZkspIUpUc6UjTXGT85qwHuSEuVc%2F16r99kDQeSSjvSbSjpyUpNK10uJJ3aYqk6smwm1lQ9bOxw71TMm8VanEqq9JW1r3Qo%2BREOLnQHvbWmb7rZIu5VLIyGQGOukPv%2F0WQk5LeEAjPOUDwtAP6bShy2HKAz0HPO%2B5KsP25I79O2I7LvD%2Bz4Il1XAQAA&bn=Multi-purpose%20Imperial%20Courier');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import Detailed Builds Array', function() {
|
||||
|
||||
beforeEach(reset);
|
||||
|
||||
it('imports all builds', function() {
|
||||
const importData = require('./fixtures/valid-detailed-export');
|
||||
const expectedBuilds = require('./fixtures/expected-builds');
|
||||
|
||||
pasteText(JSON.stringify(importData));
|
||||
expect(modal.state.importValid).toBeTruthy();
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
clickProceed();
|
||||
expect(modal.state.processed).toBeTruthy();
|
||||
clickImport();
|
||||
|
||||
let builds = Persist.getBuilds();
|
||||
|
||||
for (let s in builds) {
|
||||
for (let b in builds[s]) {
|
||||
expect(builds[s][b]).toEqual(expectedBuilds[s][b]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import Companion API Build', function() {
|
||||
|
||||
beforeEach(reset);
|
||||
|
||||
it('imports a valid companion API build', function() {
|
||||
const importData = require('./fixtures/companion-api-import-1');
|
||||
pasteText(JSON.stringify(importData));
|
||||
|
||||
expect(modal.state.importValid).toBeTruthy();
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
expect(modal.state.singleBuild).toBe(true);
|
||||
clickProceed();
|
||||
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/federal_corvette?code=A2putsFklndzsxf50x0x7l28281919040404040402020l06p05sf63c5ifr--v66g--.AwRj4zNapI%3D%3D.CwRgDBldUExuBiIlWIA%3D.&bn=Imported%20Federal%20Corvette');
|
||||
});
|
||||
|
||||
it('imports a valid companion API build', function() {
|
||||
const importData = require('./fixtures/companion-api-import-2');
|
||||
pasteText(JSON.stringify(importData));
|
||||
|
||||
expect(modal.state.importValid).toBeTruthy();
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
expect(modal.state.singleBuild).toBe(true);
|
||||
clickProceed();
|
||||
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/beluga?code=A0pktsFplCdpsnf70t0t2727270004040404043c4fmimlmm04mc0iv62i--.AwRj4yusg%3D%3D%3D.CwRgDBldHi8IWIA%3D.&bn=Imported%20Beluga%20Liner');
|
||||
});
|
||||
|
||||
it('imports a valid companion API build', function() {
|
||||
const importData = require('./fixtures/companion-api-import-3');
|
||||
pasteText(JSON.stringify(importData));
|
||||
|
||||
expect(modal.state.importValid).toBeTruthy();
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
expect(modal.state.singleBuild).toBe(true);
|
||||
clickProceed();
|
||||
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/type_7_transport?code=A0patfFflidasdf5----0404040005050504044d2402--.AwRj4yoo.CwRgDBlVK7HjEA%3D%3D.&bn=Imported%20Type-7%20Transporter');
|
||||
});
|
||||
|
||||
it('imports a valid companion API build', function() {
|
||||
const importData = require('./fixtures/companion-api-import-4');
|
||||
pasteText(JSON.stringify(importData));
|
||||
|
||||
expect(modal.state.importValid).toBeTruthy();
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
expect(modal.state.singleBuild).toBe(true);
|
||||
clickProceed();
|
||||
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/cobra_mk_iii?code=A0p0tdFaldd3sdf4------34----2i--.AwRj4yqA.CwRgDMYExrezBig%3D.&bn=Imported%20Cobra%20Mk%20III');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import E:D Shipyard Builds', function() {
|
||||
|
||||
// it('imports a valid build', function() {
|
||||
// const imports = require('./fixtures/ed-shipyard-import-valid');
|
||||
//
|
||||
// for (let i = 0; i < imports.length; i++ ) {
|
||||
// reset();
|
||||
// let fixture = imports[i];
|
||||
// pasteText(fixture.buildText);
|
||||
// expect(modal.state.importValid).toBeTruthy();
|
||||
// expect(modal.state.errorMsg).toEqual(null);
|
||||
// clickProceed();
|
||||
// expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||
// expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/' + fixture.shipId + '?code=' + encodeURIComponent(fixture.buildCode) + '&bn=' + encodeURIComponent(fixture.buildName));
|
||||
// }
|
||||
// });
|
||||
|
||||
it('catches invalid builds', function() {
|
||||
const imports = require('./fixtures/ed-shipyard-import-invalid');
|
||||
|
||||
for (let i = 0; i < imports.length; i++ ) {
|
||||
reset();
|
||||
pasteText(imports[i].buildText);
|
||||
expect(modal.state.importValid).toBeFalsy();
|
||||
expect(modal.state.errorMsg).toEqual(imports[i].errorMsg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Imports from a Comparison', function() {
|
||||
|
||||
it('imports a valid comparison', function() {
|
||||
const importBuilds = require('./fixtures/valid-backup').builds;
|
||||
Persist.deleteAll();
|
||||
render = TU.renderIntoDocument(<ContextProvider><ModalImport builds={importBuilds} /></ContextProvider>);
|
||||
modal = TU.findRenderedComponentWithType(render, ModalImport);
|
||||
|
||||
expect(modal.state.processed).toBe(true);
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
clickImport();
|
||||
expect(Persist.getBuilds()).toEqual(importBuilds);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Imports SLEF data', () => {
|
||||
beforeEach(reset);
|
||||
|
||||
it('imports a single valid SLEF build', () => {
|
||||
const importData = require('./fixtures/slef-single-build.json');
|
||||
pasteText(JSON.stringify(importData));
|
||||
|
||||
expect(modal.state.importValid).toBeTruthy();
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
expect(modal.state.singleBuild).toBe(true);
|
||||
clickProceed();
|
||||
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/krait_mkii?code=A2pptkFflidussf52l1o1o2g2g020g040405051Ofr45C9C91oP3.Iw18eQ%3D%3D.AwRgzKIkA%3D%3D%3D.&bn=Imported%20pancake%20hammer');
|
||||
});
|
||||
|
||||
it('imports multiple SLEF builds', () => {
|
||||
const importData = require('./fixtures/slef-multiple-builds.json');
|
||||
const expectedBuilds = require('./fixtures/slef-multiple-expected-builds.json');
|
||||
pasteText(JSON.stringify(importData));
|
||||
|
||||
expect(modal.state.importValid).toBeTruthy();
|
||||
expect(modal.state.errorMsg).toEqual(null);
|
||||
expect(modal.state.singleBuild).toBe(false);
|
||||
clickProceed();
|
||||
expect(modal.state.processed).toBeTruthy();
|
||||
clickImport();
|
||||
|
||||
const builds = Persist.getBuilds();
|
||||
|
||||
for (const shipModel in builds) {
|
||||
for (const buildName in builds[shipModel]) {
|
||||
expect(builds[shipModel][buildName])
|
||||
.toEqual(expectedBuilds[shipModel][buildName]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
jest.unmock('../src/app/stores/Persist');
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import TU from 'react-testutils-additions';
|
||||
|
||||
let origAddEventListener = window.addEventListener;
|
||||
let storageListener;
|
||||
let ls = {};
|
||||
|
||||
// Implment mock localStorage
|
||||
let localStorage = {
|
||||
getItem: function(key) {
|
||||
return ls[key];
|
||||
},
|
||||
setItem: function(key, value) {
|
||||
ls[key] = value;
|
||||
},
|
||||
removeItem: function(key) {
|
||||
delete ls[key];
|
||||
},
|
||||
clear: function() {
|
||||
ls = {};
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener = function(eventName, listener) {
|
||||
|
||||
if(eventName == 'storage') {
|
||||
storageListener = listener; // Keep track of latest storage listener
|
||||
} else {
|
||||
origAddEventListener.apply(arguments);
|
||||
}
|
||||
}
|
||||
|
||||
describe('Persist', function() {
|
||||
|
||||
const Persist = require('../src/app/stores/Persist').Persist;
|
||||
|
||||
describe('Multi tab/window', function() {
|
||||
it("syncs builds", function() {
|
||||
window.localStorage = localStorage;
|
||||
ls = {};
|
||||
let p = new Persist();
|
||||
let newBuilds = {
|
||||
anaconda: { test: '1234' }
|
||||
};
|
||||
|
||||
storageListener({ key: 'builds', newValue: JSON.stringify(newBuilds) });
|
||||
expect(p.getBuild('anaconda', 'test')).toBe('1234');
|
||||
});
|
||||
});
|
||||
|
||||
describe('General and Settings', function() {
|
||||
it("has defaults", function() {
|
||||
window.localStorage = localStorage;
|
||||
ls = {};
|
||||
let p = new Persist();
|
||||
expect(p.getLangCode()).toBe('en');
|
||||
expect(p.showTooltips()).toBe(true);
|
||||
expect(p.getInsurance()).toBe('standard');
|
||||
expect(p.getShipDiscount()).toBe(0);
|
||||
expect(p.getModuleDiscount()).toBe(0);
|
||||
expect(p.getSizeRatio()).toBe(1);
|
||||
});
|
||||
|
||||
it("loads from localStorage correctly", function() {
|
||||
window.localStorage = localStorage;
|
||||
let savedData = require('./fixtures/valid-backup');
|
||||
ls = {};
|
||||
ls.builds = JSON.stringify(savedData.builds);
|
||||
ls.NG_TRANSLATE_LANG_KEY = 'de';
|
||||
ls.insurance = 'Standard';
|
||||
ls.shipDiscount = 0.25;
|
||||
ls.moduleDiscount = 0.15;
|
||||
let p = new Persist();
|
||||
|
||||
expect(p.getInsurance()).toBe('standard');
|
||||
expect(p.getShipDiscount()).toBe(0.25);
|
||||
expect(p.getModuleDiscount()).toBe(0.15);
|
||||
expect(p.getLangCode()).toEqual('de');
|
||||
expect(p.getBuilds('anaconda')).toEqual(savedData.builds.anaconda);
|
||||
expect(p.getBuilds('python')).toEqual(savedData.builds.python);
|
||||
expect(p.getBuildsNamesFor('imperial_clipper')).toEqual(['Cargo', 'Current', 'Dream', 'Multi-purpose']);
|
||||
expect(p.getBuild('type_7_transport', 'Cargo')).toEqual('02A5D5A4D3D3D5C--------0505040403480101');
|
||||
});
|
||||
|
||||
it("uses defaults from a corrupted localStorage", function() {
|
||||
window.localStorage = localStorage;
|
||||
ls = {};
|
||||
ls.builds = "not valid json";
|
||||
ls.comparisons = "1, 3, 4";
|
||||
ls.insurance = 'this insurance does not exist';
|
||||
ls.shipDiscount = 'this is not a number';
|
||||
ls.moduleDiscount = 10; // Way to big
|
||||
|
||||
let p = new Persist();
|
||||
expect(p.getLangCode()).toBe('en');
|
||||
expect(p.showTooltips()).toBe(true);
|
||||
expect(p.getInsurance()).toBe('standard');
|
||||
expect(p.getShipDiscount()).toBe(0);
|
||||
expect(p.getModuleDiscount()).toBe(0);
|
||||
expect(p.getBuilds()).toEqual({});
|
||||
expect(p.getComparisons()).toEqual({});
|
||||
});
|
||||
|
||||
it("works without localStorage", function() {
|
||||
window.localStorage = null;
|
||||
let p = new Persist();
|
||||
expect(p.getLangCode()).toBe('en');
|
||||
expect(p.showTooltips()).toBe(true);
|
||||
expect(p.getInsurance()).toBe('standard');
|
||||
expect(p.getShipDiscount()).toBe(0);
|
||||
expect(p.getModuleDiscount()).toBe(0);
|
||||
expect(p.getSizeRatio()).toBe(1);
|
||||
|
||||
p.saveBuild('anaconda', 'test', '12345');
|
||||
expect(p.getBuild('anaconda', 'test')).toBe('12345');
|
||||
|
||||
p.deleteBuild('anaconda', 'test');
|
||||
expect(p.hasBuilds()).toBe(false);
|
||||
});
|
||||
|
||||
it("generates the backup", function() {
|
||||
window.localStorage = localStorage;
|
||||
let savedData = require('./fixtures/valid-backup');
|
||||
ls = {};
|
||||
ls.builds = JSON.stringify(savedData.builds);
|
||||
ls.insurance = 'Beta';
|
||||
ls.shipDiscount = 0.25;
|
||||
ls.moduleDiscount = 0.15;
|
||||
|
||||
let p = new Persist();
|
||||
let backup = p.getAll();
|
||||
|
||||
expect(backup.insurance).toBe('beta');
|
||||
expect(backup.shipDiscount).toBe(0.25);
|
||||
expect(backup.moduleDiscount).toBe(0.15);
|
||||
expect(backup.builds).toEqual(savedData.builds);
|
||||
expect(backup.comparisons).toEqual({});
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -1,63 +0,0 @@
|
||||
import Ship from '../src/app/shipyard/Ship';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import * as Serializer from '../src/app/shipyard/Serializer';
|
||||
import jsen from 'jsen';
|
||||
|
||||
describe("Serializer", function() {
|
||||
const anacondaTestExport = require.requireActual('./fixtures/anaconda-test-detailed-export-v4');
|
||||
const code = anacondaTestExport.references[0].code;
|
||||
const anaconda = Ships.anaconda;
|
||||
const validate = jsen(require('../src/schemas/ship-loadout/4'));
|
||||
|
||||
describe("To Detailed Build", function() {
|
||||
let testBuild = new Ship('anaconda', anaconda.properties, anaconda.slots).buildFrom(code);
|
||||
let exportData = Serializer.toDetailedBuild('Test My Ship', testBuild);
|
||||
|
||||
it("conforms to the v4 ship-loadout schema", function() {
|
||||
expect(validate(exportData)).toBe(true);
|
||||
});
|
||||
|
||||
it("contains the correct components and stats", function() {
|
||||
expect(exportData.components).toEqual(anacondaTestExport.components);
|
||||
expect(exportData.stats).toEqual(anacondaTestExport.stats);
|
||||
expect(exportData.ship).toEqual(anacondaTestExport.ship);
|
||||
expect(exportData.name).toEqual(anacondaTestExport.name);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Export Detailed Builds", function() {
|
||||
const expectedExport = require('./fixtures/valid-detailed-export');
|
||||
const builds = require('./fixtures/expected-builds');
|
||||
const exportData = Serializer.toDetailedExport(builds);
|
||||
|
||||
it("conforms to the v4 ship-loadout schema", function() {
|
||||
expect(exportData instanceof Array).toBe(true);
|
||||
|
||||
for (let detailedBuild of exportData) {
|
||||
expect(validate(detailedBuild)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("From Detailed Build", function() {
|
||||
|
||||
it("builds the ship correctly", function() {
|
||||
let testBuildA = new Ship('anaconda', anaconda.properties, anaconda.slots);
|
||||
testBuildA.buildFrom(code);
|
||||
let testBuildB = Serializer.fromDetailedBuild(anacondaTestExport);
|
||||
|
||||
for(var p in testBuildB) {
|
||||
if (p == 'availCS') {
|
||||
continue;
|
||||
}
|
||||
expect(testBuildB[p]).toEqual(testBuildA[p], p + ' does not match');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -1,156 +0,0 @@
|
||||
import Ship from '../src/app/shipyard/Ship';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import * as ModuleUtils from '../src/app/shipyard/ModuleUtils';
|
||||
|
||||
describe("Ship", function() {
|
||||
|
||||
it("can build all ships", function() {
|
||||
for (let s in Ships) {
|
||||
let shipData = Ships[s];
|
||||
let ship = new Ship(s, shipData.properties, shipData.slots);
|
||||
|
||||
for (let p in shipData.properties) {
|
||||
expect(ship[p]).toEqual(shipData.properties[p], s + ' property [' + p + '] does not match when built');
|
||||
}
|
||||
|
||||
ship.buildWith(shipData.defaults);
|
||||
|
||||
expect(ship.totalCost).toEqual(shipData.retailCost, s + ' retail cost does not match default build cost');
|
||||
expect(ship.cargoCapacity).toBeDefined();
|
||||
expect(ship.priorityBands[0].retracted).toBeGreaterThan(0, s + ' priorityBands');
|
||||
expect(ship.powerAvailable).toBeGreaterThan(0, s + ' powerAvailable');
|
||||
expect(ship.unladenRange).toBeGreaterThan(0, s + ' unladenRange');
|
||||
expect(ship.ladenRange).toBeGreaterThan(0, s + ' ladenRange');
|
||||
expect(ship.fuelCapacity).toBeGreaterThan(0, s + ' fuelCapacity');
|
||||
expect(ship.unladenFastestRange).toBeGreaterThan(0, s + ' unladenFastestRange');
|
||||
expect(ship.ladenFastestRange).toBeGreaterThan(0, s + ' ladenFastestRange');
|
||||
expect(ship.shield).toBeGreaterThan(0, s + ' shield');
|
||||
expect(ship.armour).toBeGreaterThan(0, s + ' armour');
|
||||
expect(ship.topSpeed).toBeGreaterThan(0, s + ' topSpeed');
|
||||
}
|
||||
});
|
||||
|
||||
it("resets and rebuilds properly", function() {
|
||||
var id = 'cobra_mk_iii';
|
||||
var cobra = Ships[id];
|
||||
var shipA = new Ship(id, cobra.properties, cobra.slots);
|
||||
var shipB = new Ship(id, cobra.properties, cobra.slots);
|
||||
var testShip = new Ship(id, cobra.properties, cobra.slots);
|
||||
|
||||
var buildA = cobra.defaults;
|
||||
var buildB = {
|
||||
standard:['4A', '4A', '4A', '3D', '3A', '3A', '4C'],
|
||||
hardpoints: ['0s', '0s', '2d', '2d', 0, '04'],
|
||||
internal: ['45', '03', '2b', '2o', '27', '53']
|
||||
};
|
||||
|
||||
shipA.buildWith(buildA); // Build A
|
||||
shipB.buildWith(buildB);// Build B
|
||||
testShip.buildWith(buildA);
|
||||
|
||||
for(var p in testShip) {
|
||||
if (p == 'availCS') {
|
||||
continue;
|
||||
}
|
||||
expect(testShip[p]).toEqual(shipA[p], p + ' does not match');
|
||||
}
|
||||
|
||||
testShip.buildWith(buildB);
|
||||
|
||||
for(var p in testShip) {
|
||||
if (p == 'availCS') {
|
||||
continue;
|
||||
}
|
||||
expect(testShip[p]).toEqual(shipB[p], p + ' does not match');
|
||||
}
|
||||
|
||||
testShip.buildWith(buildA);
|
||||
|
||||
for(var p in testShip) {
|
||||
if (p == 'availCS') {
|
||||
continue;
|
||||
}
|
||||
expect(testShip[p]).toEqual(shipA[p], p + ' does not match');
|
||||
}
|
||||
});
|
||||
|
||||
it("discounts hull and components properly", function() {
|
||||
var id = 'cobra_mk_iii';
|
||||
var cobra = Ships[id];
|
||||
var testShip = new Ship(id, cobra.properties, cobra.slots);
|
||||
testShip.buildWith(cobra.defaults);
|
||||
|
||||
var originalHullCost = testShip.hullCost;
|
||||
var originalTotalCost = testShip.totalCost;
|
||||
var discount = 0.1;
|
||||
|
||||
expect(testShip.m.discountedCost).toEqual(originalHullCost, 'Hull cost does not match');
|
||||
|
||||
testShip.applyDiscounts(discount, discount);
|
||||
|
||||
// Floating point errors cause miniscule decimal places which are handled in the app by rounding/formatting
|
||||
|
||||
expect(Math.floor(testShip.m.discountedCost)).toEqual(Math.floor(originalHullCost * (1 - discount)), 'Discounted Hull cost does not match');
|
||||
expect(Math.floor(testShip.totalCost)).toEqual(Math.floor(originalTotalCost * (1 - discount)), 'Discounted Total cost does not match');
|
||||
|
||||
testShip.applyDiscounts(0, 0); // No discount, 100% of cost
|
||||
|
||||
expect(testShip.m.discountedCost).toEqual(originalHullCost, 'Hull cost does not match');
|
||||
expect(testShip.totalCost).toEqual(originalTotalCost, 'Total cost does not match');
|
||||
|
||||
testShip.applyDiscounts(discount, 0); // Only discount hull
|
||||
|
||||
expect(Math.floor(testShip.m.discountedCost)).toEqual(Math.round(originalHullCost * (1 - discount)), 'Discounted Hull cost does not match');
|
||||
expect(testShip.totalCost).toEqual(originalTotalCost - originalHullCost + testShip.m.discountedCost, 'Total cost does not match');
|
||||
|
||||
});
|
||||
|
||||
it("enforces a single shield generator", function() {
|
||||
var id = 'anaconda';
|
||||
var anacondaData = Ships[id];
|
||||
var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots);
|
||||
anaconda.buildWith(anacondaData.defaults);
|
||||
|
||||
expect(anaconda.internal[2].m.grp).toEqual('sg', 'Anaconda default shield generator slot');
|
||||
|
||||
anaconda.use(anaconda.internal[1], ModuleUtils.internal('4j')); // 6E Shield Generator
|
||||
|
||||
expect(anaconda.internal[2].m).toEqual(null, 'Anaconda default shield generator slot is empty');
|
||||
expect(anaconda.internal[1].m.id).toEqual('4j', 'Slot 1 should have SG 4j in it');
|
||||
expect(anaconda.internal[1].m.grp).toEqual('sg','Slot 1 should have SG 4j in it');
|
||||
|
||||
});
|
||||
|
||||
it("enforces a single shield fuel scoop", function() {
|
||||
var id = 'anaconda';
|
||||
var anacondaData = Ships[id];
|
||||
var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots);
|
||||
anaconda.buildWith(anacondaData.defaults);
|
||||
|
||||
anaconda.use(anaconda.internal[4], ModuleUtils.internal('32')); // 4A Fuel Scoop
|
||||
expect(anaconda.internal[4].m.grp).toEqual('fs', 'Anaconda fuel scoop slot');
|
||||
|
||||
anaconda.use(anaconda.internal[3], ModuleUtils.internal('32'));
|
||||
|
||||
expect(anaconda.internal[4].m).toEqual(null, 'Anaconda original fuel scoop slot is empty');
|
||||
expect(anaconda.internal[3].m.id).toEqual('32', 'Slot 1 should have FS 32 in it');
|
||||
expect(anaconda.internal[3].m.grp).toEqual('fs','Slot 1 should have FS 32 in it');
|
||||
});
|
||||
|
||||
it("enforces a single refinery", function() {
|
||||
var id = 'anaconda';
|
||||
var anacondaData = Ships[id];
|
||||
var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots);
|
||||
anaconda.buildWith(anacondaData.defaults);
|
||||
|
||||
anaconda.use(anaconda.internal[4], ModuleUtils.internal('23')); // 4E Refinery
|
||||
expect(anaconda.internal[4].m.grp).toEqual('rf', 'Anaconda refinery slot');
|
||||
|
||||
anaconda.use(anaconda.internal[3], ModuleUtils.internal('23'));
|
||||
|
||||
expect(anaconda.internal[4].m).toEqual(null, 'Anaconda original refinery slot is empty');
|
||||
expect(anaconda.internal[3].m.id).toEqual('23', 'Slot 1 should have RF 23 in it');
|
||||
expect(anaconda.internal[3].m.grp).toEqual('rf','Slot 1 should have RF 23 in it');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const TestUtils = {
|
||||
createContextProvider: function(context) {
|
||||
var _contextTypes = {};
|
||||
|
||||
Object.keys(context).forEach(function(key) {
|
||||
_contextTypes[key] = PropTypes.any;
|
||||
});
|
||||
|
||||
return React.createClass({
|
||||
displayName: 'ContextProvider',
|
||||
childContextTypes: _contextTypes,
|
||||
getChildContext() { return context; },
|
||||
|
||||
render() {
|
||||
return React.Children.only(this.props.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default TestUtils;
|
||||
30544
package-lock.json
generated
Normal file
30544
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -15,45 +15,12 @@
|
||||
"clean": "rimraf build",
|
||||
"start": "node devServer.js",
|
||||
"lint": "eslint --ext .js,.jsx src",
|
||||
"test": "jest",
|
||||
"prod-serve": "nginx -p $(pwd) -c nginx.conf",
|
||||
"prod-stop": "kill -QUIT $(cat nginx.pid)",
|
||||
"build": "npm run clean && cross-env NODE_ENV=production webpack -p --config webpack.config.prod.js",
|
||||
"rsync": "rsync -ae \"ssh -i $CORIOLIS_PEM\" --delete build/ $CORIOLIS_USER@$CORIOLIS_HOST:~/wwws",
|
||||
"deploy": "npm run lint && npm test && npm run build && npm run rsync"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
".*": "<rootDir>/node_modules/babel-jest"
|
||||
},
|
||||
"testRegex": "(/__tests__/test-.*|\\.(test|spec))\\.js$",
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"jsx"
|
||||
],
|
||||
"automock": true,
|
||||
"bail": false,
|
||||
"unmockedModulePathPatterns": [
|
||||
"<rootDir>/node_modules/lodash",
|
||||
"<rootDir>/node_modules/react",
|
||||
"<rootDir>/node_modules/react-dom",
|
||||
"<rootDir>/node_modules/react-transition-group",
|
||||
"<rootDir>/node_modules/react-testutils-additions",
|
||||
"<rootDir>/node_modules/fbjs",
|
||||
"<rootDir>/node_modules/fbemitter",
|
||||
"<rootDir>/node_modules/classnames",
|
||||
"<rootDir>/node_modules/d3",
|
||||
"<rootDir>/node_modules/lz-string",
|
||||
"<rootDir>/node_modules/jsen",
|
||||
"coriolis-data",
|
||||
"<rootDir>/src/app/shipyard",
|
||||
"<rootDir>/src/app/i18n",
|
||||
"<rootDir>/src/app/utils",
|
||||
"<rootDir>/src/schemas",
|
||||
"<rootDir>/__tests__"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||
@@ -77,7 +44,6 @@
|
||||
"appcache-webpack-plugin": "^1.4.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^23.6.0",
|
||||
"babel-loader": "^8.0.0",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"create-react-class": "^15.6.3",
|
||||
@@ -98,7 +64,6 @@
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"html-webpack-plugin": "^3.0.7",
|
||||
"jest-cli": "^23.6.0",
|
||||
"jsen": "^0.6.4",
|
||||
"json-loader": "^0.5.4",
|
||||
"less": "^3.8.1",
|
||||
@@ -123,11 +88,12 @@
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.0.0",
|
||||
"auto-bind": "^2.1.1",
|
||||
"browserify-zlib-next": "^1.0.1",
|
||||
"classnames": "^2.2.6",
|
||||
"coriolis-data": "../coriolis-data",
|
||||
"d3": "^5.7.0",
|
||||
"detect-browser": "^3.0.1",
|
||||
"ed-forge": "../ed-forge",
|
||||
"fbemitter": "^2.1.1",
|
||||
"lodash": "^4.17.11",
|
||||
"lz-string": "^1.4.4",
|
||||
@@ -138,7 +104,7 @@
|
||||
"react-extras": "^0.7.1",
|
||||
"react-fuzzy": "^0.5.2",
|
||||
"react-ga": "^2.5.3",
|
||||
"react-number-editor": "Athanasius/react-number-editor.git#miggy",
|
||||
"react-number-editor": "^4.0.3",
|
||||
"recharts": "^1.2.0",
|
||||
"register-service-worker": "^1.5.2",
|
||||
"superagent": "^3.8.3"
|
||||
|
||||
@@ -5,27 +5,20 @@ import { register } from 'register-service-worker';
|
||||
import { EventEmitter } from 'fbemitter';
|
||||
import { getLanguage } from './i18n/Language';
|
||||
import Persist from './stores/Persist';
|
||||
import { Ship } from 'ed-forge';
|
||||
|
||||
import Announcement from './components/Announcement';
|
||||
import Header from './components/Header';
|
||||
import Tooltip from './components/Tooltip';
|
||||
import ModalExport from './components/ModalExport';
|
||||
import ModalHelp from './components/ModalHelp';
|
||||
import ModalImport from './components/ModalImport';
|
||||
import ModalPermalink from './components/ModalPermalink';
|
||||
import * as CompanionApiUtils from './utils/CompanionApiUtils';
|
||||
import * as JournalUtils from './utils/JournalUtils';
|
||||
import AboutPage from './pages/AboutPage';
|
||||
import NotFoundPage from './pages/NotFoundPage';
|
||||
import OutfittingPage from './pages/OutfittingPage';
|
||||
import ComparisonPage from './pages/ComparisonPage';
|
||||
import ShipyardPage from './pages/ShipyardPage';
|
||||
import ErrorDetails from './pages/ErrorDetails';
|
||||
|
||||
|
||||
const zlib = require('pako');
|
||||
const request = require('superagent');
|
||||
|
||||
/**
|
||||
* Coriolis App
|
||||
*/
|
||||
@@ -62,7 +55,6 @@ export default class Coriolis extends React.Component {
|
||||
this._onLanguageChange = this._onLanguageChange.bind(this);
|
||||
this._onSizeRatioChange = this._onSizeRatioChange.bind(this);
|
||||
this._keyDown = this._keyDown.bind(this);
|
||||
this._importBuild = this._importBuild.bind(this);
|
||||
|
||||
this.emitter = new EventEmitter();
|
||||
this.state = {
|
||||
@@ -73,15 +65,14 @@ export default class Coriolis extends React.Component {
|
||||
route: {},
|
||||
sizeRatio: Persist.getSizeRatio()
|
||||
};
|
||||
// TODO: New mechanism for announcements
|
||||
// this._getAnnouncements();
|
||||
Router('', (r) => this._setPage(ShipyardPage, r));
|
||||
Router('/import?', (r) => this._importBuild(r));
|
||||
Router('/import/:data', (r) => this._importBuild(r));
|
||||
Router('/outfit/?', (r) => this._setPage(OutfittingPage, r));
|
||||
Router('/outfit/:ship/?', (r) => this._setPage(OutfittingPage, r));
|
||||
Router('/outfit/:ship/:code?', (r) => this._setPage(OutfittingPage, r));
|
||||
Router('/compare/:name?', (r) => this._setPage(ComparisonPage, r));
|
||||
Router('/comparison?', (r) => this._setPage(ComparisonPage, r));
|
||||
Router('/comparison/:code', (r) => this._setPage(ComparisonPage, r));
|
||||
Router('/about', (r) => this._setPage(AboutPage, r));
|
||||
Router('*', (r) => this._setPage(null, r));
|
||||
}
|
||||
@@ -93,39 +84,25 @@ export default class Coriolis extends React.Component {
|
||||
_importBuild(r) {
|
||||
try {
|
||||
// Need to decode and gunzip the data, then build the ship
|
||||
const data = zlib.inflate(new Buffer(r.params.data, 'base64'), { to: 'string' });
|
||||
const json = JSON.parse(data);
|
||||
console.info('Ship import data: ');
|
||||
console.info(json);
|
||||
let ship, importString;
|
||||
if (json) {
|
||||
if (json.length && json[0].data) { // SLEF
|
||||
if (json.length > 1) { // Multiple builds, open modal
|
||||
importString = data;
|
||||
} else { // Single build, import directly
|
||||
ship = JournalUtils.shipFromLoadoutJSON(json[0].data);
|
||||
}
|
||||
} else { // not SLEF
|
||||
if (json.modules) {
|
||||
ship = CompanionApiUtils.shipFromJson(json);
|
||||
} else if (json.Modules) {
|
||||
ship = JournalUtils.shipFromLoadoutJSON(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ship) {
|
||||
r.params.ship = ship.id;
|
||||
r.params.code = ship.toString();
|
||||
let ship = new Ship(r.params.data);
|
||||
r.params.ship = ship.getShipType();
|
||||
r.params.code = ship.compress();
|
||||
this._setPage(OutfittingPage, r);
|
||||
} else if (importString) {
|
||||
this._setPage(ShipyardPage, r);
|
||||
this._showModal(<ModalImport importString={data}/>);
|
||||
}
|
||||
} catch (err) {
|
||||
this._onError('Failed to import ship', r.path, 0, 0, err);
|
||||
}
|
||||
}
|
||||
|
||||
// async _getAnnouncements() {
|
||||
// try {
|
||||
// const announces = await request.get('https://orbis.zone/api/announcement')
|
||||
// .query({ showInCoriolis: true });
|
||||
// this.setState({ announcements: announces.body });
|
||||
// } catch (err) {
|
||||
// console.error(err)
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Updates / Sets the page and route context
|
||||
* @param {[type]} page The page to be shown
|
||||
@@ -400,11 +377,12 @@ export default class Coriolis extends React.Component {
|
||||
render() {
|
||||
let currentMenu = this.state.currentMenu;
|
||||
return <div style={{ minHeight: '100%' }} onClick={this._closeMenu}
|
||||
className={this.state.noTouch ? 'no-touch' : null}>
|
||||
className={this.state.noTouch ? 'no-touch' : null}
|
||||
>
|
||||
<Header announcements={this.state.announcements} appCacheUpdate={this.state.appCacheUpdate}
|
||||
currentMenu={currentMenu}/>
|
||||
<div className="announcement-container">{this.state.announcements.map(a => <Announcement
|
||||
text={a.text}/>)}</div>
|
||||
{/* <div className="announcement-container">{this.state.announcements.map(a => <Announcement
|
||||
text={a.message}/>)}</div> */}
|
||||
{this.state.error ? this.state.error : this.state.page ? React.createElement(this.state.page, { currentMenu }) :
|
||||
<NotFoundPage/>}
|
||||
{this.state.modal}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { autoBind } from 'react-extras';
|
||||
* Announcement component
|
||||
*/
|
||||
export default class Announcement extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
text: PropTypes.string
|
||||
};
|
||||
@@ -27,5 +26,4 @@ export default class Announcement extends React.Component {
|
||||
render() {
|
||||
return <div className="announcement" >{this.props.text}</div>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,120 +1,22 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
import cn from 'classnames';
|
||||
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
|
||||
import FuzzySearch from 'react-fuzzy';
|
||||
import { getModuleInfo } from 'ed-forge/lib/src/data/items';
|
||||
import { get, groupBy, mapValues, sortBy, zip, zipWith } from 'lodash';
|
||||
import autoBind from 'auto-bind';
|
||||
import MODULE_STATS from 'ed-forge/lib/src/module-stats';
|
||||
import { SHOW } from '../shipyard/StatsMapping';
|
||||
|
||||
const PRESS_THRESHOLD = 500; // mouse/touch down threshold
|
||||
|
||||
/*
|
||||
* Categorisation of module groups
|
||||
*/
|
||||
const GRPCAT = {
|
||||
'sg': 'shields',
|
||||
'bsg': 'shields',
|
||||
'psg': 'shields',
|
||||
'scb': 'shields',
|
||||
'cc': 'limpet controllers',
|
||||
'fx': 'limpet controllers',
|
||||
'hb': 'limpet controllers',
|
||||
'pc': 'limpet controllers',
|
||||
'rpl': 'limpet controllers',
|
||||
'pce': 'passenger cabins',
|
||||
'pci': 'passenger cabins',
|
||||
'pcm': 'passenger cabins',
|
||||
'pcq': 'passenger cabins',
|
||||
'fh': 'hangars',
|
||||
'pv': 'hangars',
|
||||
'fs': 'fuel',
|
||||
'ft': 'fuel',
|
||||
'hr': 'structural reinforcement',
|
||||
'mrp': 'structural reinforcement',
|
||||
'bl': 'lasers',
|
||||
'pl': 'lasers',
|
||||
'ul': 'lasers',
|
||||
'ml': 'lasers',
|
||||
'c': 'projectiles',
|
||||
'mc': 'projectiles',
|
||||
'axmc': 'experimental',
|
||||
'fc': 'projectiles',
|
||||
'rfl': 'experimental',
|
||||
'pa': 'projectiles',
|
||||
'rg': 'projectiles',
|
||||
'mr': 'ordnance',
|
||||
'axmr': 'experimental',
|
||||
'rcpl': 'experimental',
|
||||
'dtl': 'experimental',
|
||||
'tbsc': 'experimental',
|
||||
'tbem': 'experimental',
|
||||
'tbrfl': 'experimental',
|
||||
'mahr': 'experimental',
|
||||
'rsl': 'experimental',
|
||||
'tp': 'ordnance',
|
||||
'nl': 'ordnance',
|
||||
'sc': 'scanners',
|
||||
'ss': 'scanners',
|
||||
// Utilities
|
||||
'cs': 'scanners',
|
||||
'kw': 'scanners',
|
||||
'ws': 'scanners',
|
||||
'xs': 'scanners',
|
||||
'ch': 'defence',
|
||||
'po': 'defence',
|
||||
'ec': 'defence',
|
||||
'sfn': 'defence',
|
||||
// Guardian
|
||||
'gpp': 'guardian',
|
||||
'gpc': 'guardian',
|
||||
'gsrp': 'guardian',
|
||||
'ggc': 'guardian',
|
||||
'gfsb': 'guardian',
|
||||
'gmrp': 'guardian',
|
||||
'gsc': 'guardian',
|
||||
'ghrp': 'guardian',
|
||||
|
||||
// Mining
|
||||
'scl': 'mining',
|
||||
'pwa': 'mining',
|
||||
'sdm': 'mining',
|
||||
|
||||
// Assists
|
||||
'dc': 'flight assists',
|
||||
'sua': 'flight assists',
|
||||
};
|
||||
// Order here is the order in which items will be shown in the modules menu
|
||||
const CATEGORIES = {
|
||||
// Internals
|
||||
'am': ['am'],
|
||||
'cr': ['cr'],
|
||||
'fi': ['fi'],
|
||||
'fuel': ['ft', 'fs'],
|
||||
'hangars': ['fh', 'pv'],
|
||||
'limpet controllers': ['cc', 'fx', 'hb', 'pc', 'rpl'],
|
||||
'passenger cabins': ['pce', 'pci', 'pcm', 'pcq'],
|
||||
'rf': ['rf'],
|
||||
'shields': ['sg', 'bsg', 'psg', 'scb'],
|
||||
'structural reinforcement': ['hr', 'mrp'],
|
||||
'flight assists': ['dc', 'sua'],
|
||||
|
||||
// Hardpoints
|
||||
'lasers': ['pl', 'ul', 'bl'],
|
||||
'projectiles': ['mc', 'c', 'fc', 'pa', 'rg'],
|
||||
'ordnance': ['mr', 'tp', 'nl'],
|
||||
// Utilities
|
||||
'sb': ['sb'],
|
||||
'hs': ['hs'],
|
||||
'defence': ['ch', 'po', 'ec'],
|
||||
'scanners': ['sc', 'ss', 'cs', 'kw', 'ws'], // Overloaded with internal scanners
|
||||
// Experimental
|
||||
'experimental': ['axmc', 'axmr', 'rfl', 'tbrfl', 'tbsc', 'tbem', 'xs', 'sfn', 'rcpl', 'dtl', 'rsl', 'mahr',],
|
||||
|
||||
// Guardian
|
||||
'guardian': ['gpp', 'gpd', 'gpc', 'ggc', 'gsrp', 'gfsb', 'ghrp', 'gmrp', 'gsc'],
|
||||
|
||||
'mining': ['ml', 'scl', 'pwa', 'sdm', 'abl'],
|
||||
const MOUNT_MAP = {
|
||||
fixed: <MountFixed className={'lg'} />,
|
||||
gimbal: <MountGimballed className={'lg'} />,
|
||||
turret: <MountTurret className={'lg'} />,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -122,15 +24,10 @@ const CATEGORIES = {
|
||||
*/
|
||||
export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
modules: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
diffDetails: PropTypes.func,
|
||||
hideSearch: PropTypes.bool,
|
||||
m: PropTypes.object,
|
||||
ship: PropTypes.object.isRequired,
|
||||
warning: PropTypes.func,
|
||||
firstSlotId: PropTypes.string,
|
||||
lastSlotId: PropTypes.string,
|
||||
activeSlotId: PropTypes.string,
|
||||
slotDiv: PropTypes.object
|
||||
};
|
||||
|
||||
@@ -141,10 +38,8 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this._hideDiff = this._hideDiff.bind(this);
|
||||
this._showSearch = this._showSearch.bind(this);
|
||||
autoBind(this);
|
||||
this.state = this._initState(props, context);
|
||||
this.slotItems = [];// Array to hold <li> refs.
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,164 +49,103 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
* @return {Object} list: Array of React Components, currentGroup Component if any
|
||||
*/
|
||||
_initState(props, context) {
|
||||
let translate = context.language.translate;
|
||||
let { m, warning, onSelect, modules, ship } = props;
|
||||
let list, currentGroup;
|
||||
const { translate } = context.language;
|
||||
const { m } = props;
|
||||
const list = [], fuzzy = [];
|
||||
let currentGroup;
|
||||
|
||||
let buildGroup = this._buildGroup.bind(
|
||||
this,
|
||||
ship,
|
||||
translate,
|
||||
m,
|
||||
warning,
|
||||
(m, event) => {
|
||||
this._hideDiff(event);
|
||||
onSelect(m);
|
||||
}
|
||||
const modules = m.getApplicableItems().map(getModuleInfo);
|
||||
const groups = mapValues(
|
||||
groupBy(modules, (info) => info.meta.group),
|
||||
(infos) => groupBy(infos, (info) => info.meta.type),
|
||||
);
|
||||
let fuzzy = [];
|
||||
if (modules instanceof Array) {
|
||||
list = buildGroup(modules[0].grp, modules);
|
||||
} else {
|
||||
list = [];
|
||||
// At present time slots with grouped options (Hardpoints and Internal) can be empty
|
||||
if (m) {
|
||||
let emptyId = 'empty';
|
||||
if (this.firstSlotId == null) this.firstSlotId = emptyId;
|
||||
let keyDown = this._keyDown.bind(this, onSelect);
|
||||
list.push(<div className='empty-c upp' key={emptyId} data-id={emptyId} onClick={onSelect.bind(null, null)}
|
||||
onKeyDown={keyDown} tabIndex="0"
|
||||
ref={slotItem => this.slotItems[emptyId] = slotItem}>{translate('empty')}</div>);
|
||||
// Build categories sorted by translated category name
|
||||
const groupKeys = sortBy(Object.keys(groups), translate);
|
||||
for (const group of groupKeys) {
|
||||
const groupName = translate(group);
|
||||
if (groupKeys.length > 1) {
|
||||
list.push(<div key={`group-${group}`} className="select-category upp">{groupName}</div>);
|
||||
}
|
||||
|
||||
// Need to regroup the modules by our own categorisation
|
||||
let catmodules = {};
|
||||
// Pre-create to preserve ordering
|
||||
for (let cat in CATEGORIES) {
|
||||
catmodules[cat] = [];
|
||||
const categories = groups[group];
|
||||
const categoryKeys = sortBy(Object.keys(categories), translate);
|
||||
for (const category of categoryKeys) {
|
||||
const categoryName = translate(category);
|
||||
const infos = categories[category];
|
||||
if (categoryKeys.length > 1) {
|
||||
list.push(<div key={`category-${category}`} className="select-group cap">{categoryName}</div>);
|
||||
}
|
||||
for (let g in modules) {
|
||||
const moduleCategory = GRPCAT[g] || g;
|
||||
const existing = catmodules[moduleCategory] || [];
|
||||
catmodules[moduleCategory] = existing.concat(modules[g]);
|
||||
}
|
||||
|
||||
for (let category in catmodules) {
|
||||
let categoryHeader = false;
|
||||
// Order through CATEGORIES if present
|
||||
const categories = CATEGORIES[category] || [category];
|
||||
if (categories && categories.length) {
|
||||
for (let n in categories) {
|
||||
const grp = categories[n];
|
||||
// We now have the group and the category. We might not have any modules, though...
|
||||
if (modules[grp]) {
|
||||
// Decide if we need a category header as well as a group header
|
||||
if (categories.length === 1) {
|
||||
// Show category header instead of group header
|
||||
if (m && grp == m.grp) {
|
||||
list.push(<div ref={(elem) => this.groupElem = elem} key={category}
|
||||
className={'select-category upp'}>{translate(category)}</div>);
|
||||
} else {
|
||||
list.push(<div key={category} className={'select-category upp'}>{translate(category)}</div>);
|
||||
}
|
||||
} else {
|
||||
// Show category header as well as group header
|
||||
if (!categoryHeader) {
|
||||
list.push(<div key={category} className={'select-category upp'}>{translate(category)}</div>);
|
||||
categoryHeader = true;
|
||||
}
|
||||
if (m && grp == m.grp) {
|
||||
list.push(<div ref={(elem) => this.groupElem = elem} key={grp}
|
||||
className={'select-group cap'}>{translate(grp)}</div>);
|
||||
} else {
|
||||
list.push(<div key={grp} className={'select-group cap'}>{translate(grp)}</div>);
|
||||
list.push(
|
||||
this._buildGroup(
|
||||
m,
|
||||
category,
|
||||
infos,
|
||||
),
|
||||
);
|
||||
fuzzy.push(
|
||||
...infos.map((info) => {
|
||||
const { meta } = info;
|
||||
const mount = meta.mount ? ' ' + translate(meta.mount) : '';
|
||||
return {
|
||||
grp: groupName,
|
||||
cat: categoryName,
|
||||
m: info.proto.Item,
|
||||
name: `${meta.class}${meta.rating}${mount} ${categoryName}`,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
list.push(buildGroup(grp, modules[grp]));
|
||||
for (const i of modules[grp]) {
|
||||
let mount = '';
|
||||
if (i.mount === 'F') {
|
||||
mount = 'Fixed';
|
||||
} else if (i.mount === 'G') {
|
||||
mount = 'Gimballed';
|
||||
} else if (i.mount === 'T') {
|
||||
mount = 'Turreted';
|
||||
}
|
||||
const fuzz = { grp, m: i, name: `${i.class}${i.rating}${mount ? ' ' + mount : ''} ${translate(grp)}` };
|
||||
fuzzy.push(fuzz);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let trackingFocus = false;
|
||||
return { list, currentGroup, fuzzy, trackingFocus };
|
||||
return { list, currentGroup, fuzzy, trackingFocus: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate React Components for Module Group
|
||||
* @param {Ship} ship Ship the selection is for
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Object} mountedModule Mounted Module
|
||||
* @param {Function} warningFunc Warning function
|
||||
* @param {function} onSelect Select/Mount callback
|
||||
* @param {string} grp Group name
|
||||
* @param {String} category Category key
|
||||
* @param {Array} modules Available modules
|
||||
* @return {React.Component} Available Module Group contents
|
||||
*/
|
||||
_buildGroup(ship, translate, mountedModule, warningFunc, onSelect, grp, modules) {
|
||||
let prevClass = null, prevRating = null, prevName;
|
||||
let elems = [];
|
||||
_buildGroup(mountedModule, category, modules) {
|
||||
const { warning } = this.props;
|
||||
const ship = mountedModule.getShip();
|
||||
const classMapping = groupBy(modules, (info) => info.meta.class);
|
||||
|
||||
const sortedModules = modules.sort(this._moduleOrder);
|
||||
const itemsPerClass = Math.max(
|
||||
...Object.values(classMapping).map((l) => l.length),
|
||||
);
|
||||
const itemsPerRow = itemsPerClass <= 2 ? 6 : itemsPerClass;
|
||||
// Nested array of <li> elements; will be flattened before being rendered.
|
||||
// Each sub-array represents one row in the final view.
|
||||
const elems = [[]];
|
||||
|
||||
// Calculate the number of items per class. Used so we don't have long lists with only a few items in each row
|
||||
const tmp = sortedModules.map((v, i) => v['class']).reduce((count, cls) => {
|
||||
count[cls] = ++count[cls] || 1;
|
||||
return count;
|
||||
}, {});
|
||||
const itemsPerClass = Math.max.apply(null, Object.keys(tmp).map(key => tmp[key]));
|
||||
// Reverse sort for descending order of module class
|
||||
for (const clazz of Object.keys(classMapping).sort().reverse()) {
|
||||
for (let info of sortBy(
|
||||
classMapping[clazz],
|
||||
(info) => info.meta.mount || info.meta.rating,
|
||||
)) {
|
||||
const { meta } = info;
|
||||
const { Item } = info.proto;
|
||||
|
||||
let itemsOnThisRow = 0;
|
||||
for (let i = 0; i < sortedModules.length; i++) {
|
||||
let m = sortedModules[i];
|
||||
let mount = null;
|
||||
let disabled = false;
|
||||
prevName = m.name;
|
||||
if (ModuleUtils.isShieldGenerator(m.grp)) {
|
||||
// Shield generators care about maximum hull mass
|
||||
disabled = ship.hullMass > m.maxmass;
|
||||
// If the mounted module is experimental as well, we can replace it so
|
||||
// the maximum does not apply
|
||||
} else if (m.experimental && (!mountedModule || !mountedModule.experimental)) {
|
||||
disabled = 4 <= ship.hardpoints.filter(o => o.m && o.m.experimental).length;
|
||||
// Can only be true if shieldgenmaximalmass is defined, i.e. this
|
||||
// module must be a shield generator
|
||||
let disabled = info.props.shieldgenmaximalmass < ship.readProp('hullmass');
|
||||
if (meta.experimental && !mountedModule.readMeta('experimental')) {
|
||||
disabled =
|
||||
4 <=
|
||||
ship.getHardpoints().filter((m) => m.readMeta('experimental'))
|
||||
.length;
|
||||
}
|
||||
let active = mountedModule && mountedModule.id === m.id;
|
||||
let classes = cn(m.name ? 'lc' : 'c', {
|
||||
warning: !disabled && warningFunc && warningFunc(m),
|
||||
active,
|
||||
disabled
|
||||
});
|
||||
let eventHandlers;
|
||||
|
||||
if (disabled) {
|
||||
eventHandlers = {
|
||||
onKeyDown: this._keyDown.bind(this, null),
|
||||
onKeyUp: this._keyUp.bind(this, null)
|
||||
|
||||
// Default event handlers for objects that are disabled
|
||||
let eventHandlers = {};
|
||||
if (!disabled) {
|
||||
const showDiff = this._showDiff.bind(this, mountedModule, info);
|
||||
const select = (event) => {
|
||||
this._hideDiff(event);
|
||||
this.props.onSelect(Item);
|
||||
};
|
||||
} else {
|
||||
/**
|
||||
* Get the ids of the first and last <li> elements in the <ul> that are focusable (i.e. are not active or disabled)
|
||||
* Will be used to keep focus inside the <ul> on Tab and Shift-Tab while it is visible
|
||||
*/
|
||||
if (this.firstSlotId == null) this.firstSlotId = sortedModules[i].id;
|
||||
if (active) this.activeSlotId = sortedModules[i].id;
|
||||
this.lastSlotId = sortedModules[i].id;
|
||||
|
||||
let showDiff = this._showDiff.bind(this, mountedModule, m);
|
||||
let select = onSelect.bind(null, m);
|
||||
|
||||
eventHandlers = {
|
||||
onMouseEnter: this._over.bind(this, showDiff),
|
||||
@@ -319,67 +153,100 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
onTouchEnd: this._touchEnd.bind(this, select),
|
||||
onMouseLeave: this._hideDiff,
|
||||
onClick: select,
|
||||
onKeyDown: this._keyDown.bind(this, select),
|
||||
onKeyUp: this._keyUp.bind(this, select)
|
||||
};
|
||||
}
|
||||
|
||||
switch (m.mount) {
|
||||
case 'F':
|
||||
mount = <MountFixed className={'lg'}/>;
|
||||
break;
|
||||
case 'G':
|
||||
mount = <MountGimballed className={'lg'}/>;
|
||||
break;
|
||||
case 'T':
|
||||
mount = <MountTurret className={'lg'}/>;
|
||||
break;
|
||||
}
|
||||
if (m.name && m.name === prevName) {
|
||||
// elems.push(<br key={'b' + m.grp + i} />);
|
||||
itemsOnThisRow = 0;
|
||||
}
|
||||
if (itemsOnThisRow == 6 || i > 0 && sortedModules.length > 3 && itemsPerClass > 2 && m.class != prevClass && (m.rating != prevRating || m.mount)) {
|
||||
elems.push(<br key={'b' + m.grp + i}/>);
|
||||
itemsOnThisRow = 0;
|
||||
}
|
||||
let tbIdx = (classes.indexOf('disabled') < 0) ? 0 : undefined;
|
||||
elems.push(
|
||||
<li key={m.id} data-id={m.id} className={classes} {...eventHandlers} tabIndex={tbIdx}
|
||||
ref={slotItem => this.slotItems[m.id] = slotItem}>
|
||||
{mount}
|
||||
{(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}
|
||||
</li>
|
||||
const mountSymbol = MOUNT_MAP[meta.mount];
|
||||
const li = (
|
||||
<li key={Item} data-id={Item}
|
||||
ref={Item === mountedModule.getItem() ? (ref) => { this.activeSlotRef = ref; } : undefined}
|
||||
className={cn(meta.type === 'armour' ? 'lc' : 'c', {
|
||||
warning: !disabled && warning && warning(info),
|
||||
active: mountedModule.getItem() === Item,
|
||||
disabled,
|
||||
hardpoint: mountSymbol,
|
||||
})}
|
||||
{...eventHandlers}
|
||||
>{mountSymbol}{meta.type === 'armour' ? Item : `${meta.class}${meta.rating}`}</li>
|
||||
);
|
||||
|
||||
itemsOnThisRow++;
|
||||
prevClass = m.class;
|
||||
prevRating = m.rating;
|
||||
prevName = m.name;
|
||||
const tail = elems.pop();
|
||||
let newTail = [tail];
|
||||
if (tail.length < itemsPerRow) {
|
||||
// If the row has not grown too long, the new <li> element can be
|
||||
// added to the row itself
|
||||
tail.push(li);
|
||||
} else {
|
||||
// Otherwise, the last row gets a line break element added and this
|
||||
// item is put into a new row
|
||||
tail.push(<br key={elems.length}/>);
|
||||
newTail.push([li]);
|
||||
}
|
||||
return <ul key={'modules' + grp}>{elems}</ul>;
|
||||
elems.push(...newTail);
|
||||
}
|
||||
}
|
||||
|
||||
return <ul key={'ul' + category}>{[].concat(...elems)}</ul>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tooltip content for the difference between the
|
||||
* mounted module and the hovered modules
|
||||
* @param {Object} mm The module mounet currently
|
||||
* @param {Object} m The hovered module
|
||||
* @param {Object} mountedModule The module mounted currently
|
||||
* @param {Object} hoveringModule The hovered module
|
||||
* @param {DOMRect} rect DOMRect for target element
|
||||
*/
|
||||
_showDiff(mm, m, rect) {
|
||||
if (this.props.diffDetails) {
|
||||
_showDiff(mountedModule, hoveringModule, rect) {
|
||||
const { tooltip, language } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
this.touchTimeout = null;
|
||||
this.context.tooltip(this.props.diffDetails(m, mm), rect);
|
||||
const mountedIsEmpty = mountedModule.isEmpty();
|
||||
const props = (
|
||||
mountedIsEmpty ? ['mass'] : Object.keys(hoveringModule.props)
|
||||
).map((prop) => SHOW[prop] ? SHOW[prop].as : prop);
|
||||
const oldProps = mountedIsEmpty ?
|
||||
[{ value: 0 }] :
|
||||
props.map((prop) => mountedModule.getFormatted(prop, false));
|
||||
const newProps = mountedModule.try(() => {
|
||||
mountedModule.setItem(hoveringModule.proto.Item);
|
||||
return props.map((prop) => mountedModule.getFormatted(prop, false));
|
||||
});
|
||||
|
||||
const diffs = zipWith(oldProps, newProps, (oldVal, newVal) => {
|
||||
const { unit, value } = newVal;
|
||||
if (!oldVal.value) {
|
||||
return undefined;
|
||||
}
|
||||
return { value, diff: value - oldVal.value, unit };
|
||||
});
|
||||
const namedDiffs = zip(props, diffs).filter(([_, stat]) => stat !== undefined);
|
||||
namedDiffs.push(['cost', {
|
||||
value: hoveringModule.meta.cost,
|
||||
diff: hoveringModule.meta.cost - (mountedIsEmpty ? 0 : mountedModule.readMeta('cost')),
|
||||
unit: units.CR,
|
||||
}]);
|
||||
|
||||
const tt = <div className='cap' style={{ whiteSpace: 'nowrap' }}>
|
||||
{sortBy(namedDiffs, ([prop, _]) => prop).map(([prop, stats]) => {
|
||||
const { unit, value, diff } = stats;
|
||||
const beneficial = get(MODULE_STATS, [prop, 'higherbetter'], false) === diff > 0;
|
||||
return <div key={prop}>
|
||||
{translate(prop)}: <span className={diff === 0 ? 'disabled' : beneficial ? 'secondary' : 'warning'}>
|
||||
{formats.round(value)} {diff !== 0 && ` (${diff > 0 ? '+' : ''}${formats.round(diff)})`}{unit}
|
||||
</span>
|
||||
</div>;
|
||||
})}
|
||||
</div>;
|
||||
tooltip(tt, rect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tooltip content for the difference between the
|
||||
* mounted module and the hovered modules
|
||||
* @returns {React.Component} Search component if available
|
||||
*/
|
||||
_showSearch() {
|
||||
if (this.props.modules instanceof Array) {
|
||||
if (this.props.hideSearch) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
@@ -442,41 +309,6 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
this._hideDiff();
|
||||
}
|
||||
|
||||
/**
|
||||
* Key down - select module on Enter key, move to next/previous module on Tab/Shift-Tab, close on Esc
|
||||
* @param {Function} select Select module callback
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_keyDown(select, event) {
|
||||
let className = event.currentTarget.attributes['class'].value;
|
||||
if (event.key == 'Enter' && className.indexOf('disabled') < 0 && className.indexOf('active') < 0) {
|
||||
select();
|
||||
return;
|
||||
}
|
||||
let elemId = event.currentTarget.attributes['data-id'].value;
|
||||
if (className.indexOf('disabled') < 0 && event.key == 'Tab') {
|
||||
if (event.shiftKey && elemId == this.firstSlotId) {
|
||||
event.preventDefault();
|
||||
this.slotItems[this.lastSlotId].focus();
|
||||
return;
|
||||
}
|
||||
if (!event.shiftKey && elemId == this.lastSlotId) {
|
||||
event.preventDefault();
|
||||
this.slotItems[this.firstSlotId].focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Key Up
|
||||
* @param {Function} select Select module callback
|
||||
* @param {SytheticEvent} event Event
|
||||
*/
|
||||
_keyUp(select, event) {
|
||||
// nothing here yet
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide diff tooltip
|
||||
* @param {SyntheticEvent} event Event
|
||||
@@ -487,60 +319,12 @@ 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 highest (A) to lowest (E)
|
||||
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
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (this.groupElem) { // Scroll to currently selected group
|
||||
this.node.scrollTop = this.groupElem.offsetTop;
|
||||
}
|
||||
/**
|
||||
* Set focus on active or first slot element, if applicable.
|
||||
*/
|
||||
if (this.slotItems[this.activeSlotId]) {
|
||||
this.slotItems[this.activeSlotId].focus();
|
||||
} else if (this.slotItems[this.firstSlotId]) {
|
||||
this.slotItems[this.firstSlotId].focus();
|
||||
if (this.activeSlotRef) {
|
||||
this.activeSlotRef.focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import autoBind from 'auto-bind';
|
||||
|
||||
/**
|
||||
* Boost displays a boost button that toggles bosot
|
||||
@@ -8,8 +9,6 @@ import TranslatedComponent from './TranslatedComponent';
|
||||
*/
|
||||
export default class Boost extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
marker: PropTypes.string.isRequired,
|
||||
ship: PropTypes.object.isRequired,
|
||||
boost: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
@@ -19,12 +18,9 @@ export default class Boost extends TranslatedComponent {
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { ship, boost } = props;
|
||||
|
||||
this._keyDown = this._keyDown.bind(this);
|
||||
this._toggleBoost = this._toggleBoost.bind(this);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,13 +66,12 @@ export default class Boost extends TranslatedComponent {
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { formats, translate, units } = this.context.language;
|
||||
const { ship, boost } = this.props;
|
||||
|
||||
// TODO disable if ship cannot boost
|
||||
const { translate } = this.context.language;
|
||||
return (
|
||||
<span id='boost'>
|
||||
<button id='boost' className={boost ? 'selected' : null} onClick={this._toggleBoost}>{translate('boost')}</button>
|
||||
<button id='boost' className={this.props.boost ? 'selected' : null} onClick={this._toggleBoost}>
|
||||
{translate('boost')}
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import Slider from '../components/Slider';
|
||||
import autoBind from 'auto-bind';
|
||||
|
||||
/**
|
||||
* Cargo slider
|
||||
@@ -21,8 +22,7 @@ export default class Cargo extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
this._cargoChange = this._cargoChange.bind(this);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import Persist from '../stores/Persist';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import { Factory, Ship } from 'ed-forge';
|
||||
import { Insurance } from '../shipyard/Constants';
|
||||
import { slotName, slotComparator } from '../utils/SlotFunctions';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { ShoppingIcon } from '../components/SvgIcons';
|
||||
import { ShoppingIcon } from './SvgIcons';
|
||||
import autoBind from 'auto-bind';
|
||||
import { assign, differenceBy, sortBy, reverse } from 'lodash';
|
||||
import { FUEL_CAPACITY } from 'ed-forge/lib/src/ship-stats';
|
||||
|
||||
/**
|
||||
* Cost Section
|
||||
@@ -16,7 +17,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
ship: PropTypes.object.isRequired,
|
||||
code: PropTypes.string.isRequired,
|
||||
buildName: PropTypes.string
|
||||
buildName: PropTypes.string,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -25,71 +26,34 @@ export default class CostSection extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._costsTab = this._costsTab.bind(this);
|
||||
this._sortCost = this._sortCost.bind(this);
|
||||
this._sortAmmo = this._sortAmmo.bind(this);
|
||||
this._sortRetrofit = this._sortRetrofit.bind(this);
|
||||
this._buildRetrofitShip = this._buildRetrofitShip.bind(this);
|
||||
this._onBaseRetrofitChange = this._onBaseRetrofitChange.bind(this);
|
||||
this._defaultRetrofitName = this._defaultRetrofitName.bind(this);
|
||||
this._eddbShoppingList = this._eddbShoppingList.bind(this);
|
||||
|
||||
let data = Ships[props.ship.id]; // Retrieve the basic ship properties, slots and defaults
|
||||
let retrofitName = this._defaultRetrofitName(props.ship.id, props.buildName);
|
||||
let retrofitShip = this._buildRetrofitShip(props.ship.id, retrofitName);
|
||||
let shipDiscount = Persist.getShipDiscount();
|
||||
let moduleDiscount = Persist.getModuleDiscount();
|
||||
|
||||
this.props.ship.applyDiscounts(shipDiscount, moduleDiscount);
|
||||
retrofitShip.applyDiscounts(shipDiscount, moduleDiscount);
|
||||
autoBind(this);
|
||||
|
||||
const { ship, buildName } = props;
|
||||
this.shipType = ship.getShipType();
|
||||
this.state = {
|
||||
retrofitShip,
|
||||
retrofitName,
|
||||
shipDiscount,
|
||||
moduleDiscount,
|
||||
retrofitName: Persist.hasBuild(ship.getShipType(), buildName) ? buildName : null,
|
||||
shipDiscount: Persist.getShipDiscount(),
|
||||
moduleDiscount: Persist.getModuleDiscount(),
|
||||
insurance: Insurance[Persist.getInsurance()],
|
||||
tab: Persist.getCostTab(),
|
||||
buildOptions: Persist.getBuildsNamesFor(props.ship.id),
|
||||
ammoPredicate: 'cr',
|
||||
ammoDesc: true,
|
||||
costPredicate: 'cr',
|
||||
costDesc: true,
|
||||
retroPredicate: 'cr',
|
||||
retroDesc: true
|
||||
buildOptions: Persist.getBuildsNamesFor(ship.getShipType()),
|
||||
predicate: 'cr',
|
||||
desc: true,
|
||||
excluded: {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ship instance to base/reference retrofit changes from
|
||||
* @param {string} shipId Ship Id
|
||||
* @param {string} name Build name
|
||||
* @param {Ship} retrofitShip Existing retrofit ship
|
||||
* @return {Ship} Retrofit ship
|
||||
*/
|
||||
_buildRetrofitShip(shipId, name, retrofitShip) {
|
||||
let data = Ships[shipId]; // Retrieve the basic ship properties, slots and defaults
|
||||
|
||||
if (!retrofitShip) { // Don't create a new instance unless needed
|
||||
retrofitShip = new Ship(shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison
|
||||
}
|
||||
|
||||
if (Persist.hasBuild(shipId, name)) {
|
||||
retrofitShip.buildFrom(Persist.getBuild(shipId, name)); // Populate modules from existing build
|
||||
_buildRetrofitShip() {
|
||||
const { retrofitName } = this.state;
|
||||
if (Persist.hasBuild(this.shipType, retrofitName)) {
|
||||
return new Ship(Persist.getBuild(this.shipType, retrofitName));
|
||||
} else {
|
||||
retrofitShip.buildWith(data.defaults); // Populate with default components
|
||||
return Factory.newShip(this.shipType);
|
||||
}
|
||||
return retrofitShip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default retrofit build name if it exists
|
||||
* @param {string} shipId Ship Id
|
||||
* @param {string} name Build name
|
||||
* @return {string} Build name or null
|
||||
*/
|
||||
_defaultRetrofitName(shipId, name) {
|
||||
return Persist.hasBuild(shipId, name) ? name : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,9 +71,6 @@ export default class CostSection extends TranslatedComponent {
|
||||
_onDiscountChanged() {
|
||||
let shipDiscount = Persist.getShipDiscount();
|
||||
let moduleDiscount = Persist.getModuleDiscount();
|
||||
this.props.ship.applyDiscounts(shipDiscount, moduleDiscount);
|
||||
this.state.retrofitShip.applyDiscounts(shipDiscount, moduleDiscount);
|
||||
this._updateRetrofit(this.props.ship, this.state.retrofitShip);
|
||||
this.setState({ shipDiscount, moduleDiscount });
|
||||
}
|
||||
|
||||
@@ -126,156 +87,33 @@ export default class CostSection extends TranslatedComponent {
|
||||
* @param {SyntheticEvent} event Build name to base the retrofit ship on
|
||||
*/
|
||||
_onBaseRetrofitChange(event) {
|
||||
let retrofitName = event.target.value;
|
||||
let ship = this.props.ship;
|
||||
|
||||
if (retrofitName) {
|
||||
this.state.retrofitShip.buildFrom(Persist.getBuild(ship.id, retrofitName));
|
||||
} else {
|
||||
this.state.retrofitShip.buildWith(Ships[ship.id].defaults); // Retrofit ship becomes stock build
|
||||
}
|
||||
this._updateRetrofit(ship, this.state.retrofitShip);
|
||||
this.setState({ retrofitName });
|
||||
this.setState({ retrofitName: event.target.value });
|
||||
}
|
||||
|
||||
/**
|
||||
* On builds changed check to see if the retrofit ship needs
|
||||
* to be updated
|
||||
* Toggle item cost inclusion
|
||||
* @param {String} key Key of the row to toggle
|
||||
*/
|
||||
_onBuildsChanged() {
|
||||
let update = false;
|
||||
let ship = this.props.ship;
|
||||
let { retrofitName, retrofitShip } = this.state;
|
||||
|
||||
if(!Persist.hasBuild(ship.id, retrofitName)) {
|
||||
retrofitShip.buildWith(Ships[ship.id].defaults); // Retrofit ship becomes stock build
|
||||
this.setState({ retrofitName: null });
|
||||
update = true;
|
||||
} else if (Persist.getBuild(ship.id, retrofitName) != retrofitShip.toString()) {
|
||||
retrofitShip.buildFrom(Persist.getBuild(ship.id, retrofitName)); // Repopulate modules from saved build
|
||||
update = true;
|
||||
}
|
||||
|
||||
if (update) { // Update retrofit comparison
|
||||
this._updateRetrofit(ship, retrofitShip);
|
||||
}
|
||||
// Update list of retrofit base build options
|
||||
this.setState({ buildOptions: Persist.getBuildsNamesFor(ship.id) });
|
||||
_toggleExcluded(key) {
|
||||
let { excluded } = this.state;
|
||||
excluded = assign({}, excluded);
|
||||
const slotExcluded = excluded[key];
|
||||
excluded[key] = (slotExcluded === undefined ? true : !slotExcluded);
|
||||
this.setState({ excluded });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle item cost inclusion in overall total
|
||||
* @param {Object} item Cost item
|
||||
* Set list sort predicate
|
||||
* @param {string} newPredicate sort predicate
|
||||
*/
|
||||
_toggleCost(item) {
|
||||
this.props.ship.setCostIncluded(item, !item.incCost);
|
||||
this.forceUpdate();
|
||||
_sortBy(newPredicate) {
|
||||
let { predicate, desc } = this.state;
|
||||
|
||||
if (newPredicate == predicate) {
|
||||
desc = !desc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle item cost inclusion in retrofit total
|
||||
* @param {Object} item Cost item
|
||||
*/
|
||||
_toggleRetrofitCost(item) {
|
||||
let retrofitTotal = this.state.retrofitTotal;
|
||||
item.retroItem.incCost = !item.retroItem.incCost;
|
||||
retrofitTotal += item.netCost * (item.retroItem.incCost ? 1 : -1);
|
||||
this.setState({ retrofitTotal });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cost list sort predicate
|
||||
* @param {string} predicate sort predicate
|
||||
*/
|
||||
_sortCostBy(predicate) {
|
||||
let { costPredicate, costDesc } = this.state;
|
||||
|
||||
if (costPredicate == predicate) {
|
||||
costDesc = !costDesc;
|
||||
}
|
||||
|
||||
this.setState({ costPredicate: predicate, costDesc });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort cost list
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {string} predicate Sort predicate
|
||||
* @param {Boolean} desc Sort descending
|
||||
*/
|
||||
_sortCost(ship, predicate, desc) {
|
||||
let costList = ship.costList;
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
if (predicate == 'm') {
|
||||
costList.sort(slotComparator(translate, null, desc));
|
||||
} else {
|
||||
costList.sort(slotComparator(translate, (a, b) => (a.m.cost || 0) - (b.m.cost || 0), desc));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ammo list sort predicate
|
||||
* @param {string} predicate sort predicate
|
||||
*/
|
||||
_sortAmmoBy(predicate) {
|
||||
let { ammoPredicate, ammoDesc } = this.state;
|
||||
|
||||
if (ammoPredicate == predicate) {
|
||||
ammoDesc = !ammoDesc;
|
||||
}
|
||||
|
||||
this.setState({ ammoPredicate: predicate, ammoDesc });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort ammo cost list
|
||||
* @param {Array} ammoCosts Ammo cost list
|
||||
* @param {string} predicate Sort predicate
|
||||
* @param {Boolean} desc Sort descending
|
||||
*/
|
||||
_sortAmmo(ammoCosts, predicate, desc) {
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
if (predicate == 'm') {
|
||||
ammoCosts.sort(slotComparator(translate, null, desc));
|
||||
} else {
|
||||
ammoCosts.sort(slotComparator(translate, (a, b) => a[predicate] - b[predicate], desc));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set retrofit list sort predicate
|
||||
* @param {string} predicate sort predicate
|
||||
*/
|
||||
_sortRetrofitBy(predicate) {
|
||||
let { retroPredicate, retroDesc } = this.state;
|
||||
|
||||
if (retroPredicate == predicate) {
|
||||
retroDesc = !retroDesc;
|
||||
}
|
||||
|
||||
this.setState({ retroPredicate: predicate, retroDesc });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort retrofit cost list
|
||||
* @param {Array} retrofitCosts Retrofit cost list
|
||||
* @param {string} predicate Sort predicate
|
||||
* @param {Boolean} desc Sort descending
|
||||
*/
|
||||
_sortRetrofit(retrofitCosts, predicate, desc) {
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
if (predicate == 'cr') {
|
||||
retrofitCosts.sort((a, b) => a.netCost - b.netCost);
|
||||
} else {
|
||||
retrofitCosts.sort((a , b) => (a[predicate] ? translate(a[predicate]).toLowerCase() : '').localeCompare(b[predicate] ? translate(b[predicate]).toLowerCase() : ''));
|
||||
}
|
||||
|
||||
if (!desc) {
|
||||
retrofitCosts.reverse();
|
||||
}
|
||||
this.setState({ predicate: newPredicate, desc });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,18 +122,34 @@ export default class CostSection extends TranslatedComponent {
|
||||
*/
|
||||
_costsTab() {
|
||||
let { ship } = this.props;
|
||||
let { shipDiscount, moduleDiscount, insurance } = this.state;
|
||||
let {
|
||||
excluded, shipDiscount, moduleDiscount, insurance, desc, predicate
|
||||
} = this.state;
|
||||
let { translate, formats, units } = this.context.language;
|
||||
let rows = [];
|
||||
|
||||
for (let i = 0, l = ship.costList.length; i < l; i++) {
|
||||
let item = ship.costList[i];
|
||||
if (item.m && item.m.cost) {
|
||||
let toggle = this._toggleCost.bind(this, item);
|
||||
rows.push(<tr key={i} className={cn('highlight', { disabled: !item.incCost })}>
|
||||
<td className='ptr' style={{ width: '1em' }} onClick={toggle}>{item.m.class + item.m.rating}</td>
|
||||
<td className='le ptr shorten cap' onClick={toggle}>{slotName(translate, item)}</td>
|
||||
<td className='ri ptr' onClick={toggle}>{formats.int(item.discountedCost)}{units.CR}</td>
|
||||
let modules = sortBy(
|
||||
ship.getModules(),
|
||||
(predicate === 'm' ? (m) => m.getItem() : (m) => m.readMeta('cost'))
|
||||
);
|
||||
if (desc) {
|
||||
reverse(modules);
|
||||
}
|
||||
|
||||
let totalCost = 0;
|
||||
for (let module of modules) {
|
||||
const cost = module.readMeta('cost');
|
||||
const slot = module.getSlot();
|
||||
if (cost) {
|
||||
let toggle = this._toggleExcluded.bind(this, slot);
|
||||
const disabled = excluded[slot];
|
||||
if (!disabled) {
|
||||
totalCost += cost;
|
||||
}
|
||||
rows.push(<tr key={slot} className={cn('highlight', { disabled })}>
|
||||
<td className='ptr' style={{ width: '1em' }} onClick={toggle}>{module.getClassRating()}</td>
|
||||
<td className='le ptr shorten cap' onClick={toggle}>{translate(module.readMeta('type'))}</td>
|
||||
<td className='ri ptr' onClick={toggle}>{formats.int(cost * (1 - moduleDiscount))}{units.CR}</td>
|
||||
</tr>);
|
||||
}
|
||||
}
|
||||
@@ -304,23 +158,23 @@ export default class CostSection extends TranslatedComponent {
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th colSpan='2' className='sortable le' onClick={this._sortCostBy.bind(this,'m')}>
|
||||
<th colSpan='2' className='sortable le' onClick={() => this._sortBy('m')}>
|
||||
{translate('module')}
|
||||
{shipDiscount ? <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('ship')} ${formats.pct(-1 * shipDiscount)}]`}</u> : null}
|
||||
{moduleDiscount ? <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} ${formats.pct(-1 * moduleDiscount)}]`}</u> : null}
|
||||
</th>
|
||||
<th className='sortable le' onClick={this._sortCostBy.bind(this, 'cr')} >{translate('credits')}</th>
|
||||
<th className='sortable le' onClick={() => this._sortBy('cr')} >{translate('credits')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
<tr className='ri'>
|
||||
<td colSpan='2' className='lbl' >{translate('total')}</td>
|
||||
<td className='val'>{formats.int(ship.totalCost)}{units.CR}</td>
|
||||
<td className='val'>{formats.int(totalCost)}{units.CR}</td>
|
||||
</tr>
|
||||
<tr className='ri'>
|
||||
<td colSpan='2' className='lbl'>{translate('insurance')}</td>
|
||||
<td className='val'>{formats.int(ship.totalCost * insurance)}{units.CR}</td>
|
||||
<td className='val'>{formats.int(totalCost * insurance)}{units.CR}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -331,14 +185,63 @@ export default class CostSection extends TranslatedComponent {
|
||||
* Open up a window for EDDB with a shopping list of our retrofit components
|
||||
*/
|
||||
_eddbShoppingList() {
|
||||
const { retrofitCosts } = this.state;
|
||||
const {} = this.state;
|
||||
const { ship } = this.props;
|
||||
|
||||
// Provide unique list of non-PP module EDDB IDs to buy
|
||||
const modIds = retrofitCosts.filter(item => item.retroItem.incCost && item.buyId && !item.buyPp).map(item => item.buyId).filter((v, i, a) => a.indexOf(v) === i);
|
||||
// const modIds = retrofitCosts.filter(item => item.retroItem.incCost && item.buyId && !item.buyPp).map(item => item.buyId).filter((v, i, a) => a.indexOf(v) === i);
|
||||
|
||||
// Open up the relevant URL
|
||||
window.open('https://eddb.io/station?m=' + modIds.join(','));
|
||||
// TODO:
|
||||
// window.open('https://eddb.io/station?m=' + modIds.join(','));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
_retrofitInfo() {
|
||||
const { ship } = this.props;
|
||||
const { desc, moduleDiscount, predicate, retrofitName, excluded } = this.state;
|
||||
const retrofitShip = this._buildRetrofitShip();
|
||||
|
||||
const currentModules = ship.getModules();
|
||||
const oldModules = retrofitShip.getModules();
|
||||
const buyModules = differenceBy(currentModules, oldModules, (m) => m.getItem());
|
||||
const sellModules = differenceBy(oldModules, currentModules, (m) => m.getItem());
|
||||
|
||||
let modules = [];
|
||||
let totalCost = 0;
|
||||
const addModule = (m, costFactor) => {
|
||||
const key = `${m.getItem()}@${m.getSlot()}`;
|
||||
const cost = costFactor * m.readMeta('cost') * (1 - moduleDiscount);
|
||||
modules.push({
|
||||
key, cost,
|
||||
rating: m.getClassRating(),
|
||||
item: m.readMeta('type'),
|
||||
});
|
||||
if (!excluded[key]) {
|
||||
totalCost += cost;
|
||||
}
|
||||
};
|
||||
for (let m of buyModules) {
|
||||
addModule(m, 1);
|
||||
}
|
||||
for (let m of sellModules) {
|
||||
addModule(m, -1);
|
||||
}
|
||||
|
||||
let _sortF = undefined;
|
||||
switch (predicate) {
|
||||
case 'cr': _sortF = (o) => o.cost; break;
|
||||
case 'm':
|
||||
default: _sortF = (o) => o.item; break;
|
||||
};
|
||||
|
||||
modules = sortBy(modules, _sortF);
|
||||
if (desc) {
|
||||
reverse(modules);
|
||||
}
|
||||
return [totalCost, modules];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,59 +249,52 @@ export default class CostSection extends TranslatedComponent {
|
||||
* @return {React.Component} Tab contents
|
||||
*/
|
||||
_retrofitTab() {
|
||||
let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state;
|
||||
let { buildOptions, excluded, moduleDiscount, retrofitName } = this.state;
|
||||
const { termtip, tooltip } = this.context;
|
||||
let { translate, formats, units } = this.context.language;
|
||||
let int = formats.int;
|
||||
let rows = [], options = [<option key='stock' value=''>{translate('Stock')}</option>];
|
||||
|
||||
for (let opt of this.state.buildOptions) {
|
||||
options.push(<option key={opt} value={opt}>{opt}</option>);
|
||||
}
|
||||
|
||||
if (retrofitCosts.length) {
|
||||
for (let i = 0, l = retrofitCosts.length; i < l; i++) {
|
||||
let item = retrofitCosts[i];
|
||||
rows.push(<tr key={i} className={cn('highlight', { disabled: !item.retroItem.incCost })} onClick={this._toggleRetrofitCost.bind(this, item)}>
|
||||
<td className='ptr' style={{ width: '1em' }}>{item.sellClassRating}</td>
|
||||
<td className='le ptr shorten cap'>{translate(item.sellName)}</td>
|
||||
<td className='ptr' style={{ width: '1em' }}>{item.buyClassRating}</td>
|
||||
<td className='le ptr shorten cap'>{translate(item.buyName)}</td>
|
||||
<td colSpan='2' className={cn('ri ptr', item.retroItem.incCost ? item.netCost > 0 ? 'warning' : 'secondary-disabled' : 'disabled')}>{int(item.netCost)}{units.CR}</td>
|
||||
</tr>);
|
||||
}
|
||||
} else {
|
||||
rows = <tr><td colSpan='7' style={{ padding: '3em 0' }}>{translate('PHRASE_NO_RETROCH')}</td></tr>;
|
||||
}
|
||||
|
||||
const [retrofitTotal, retrofitInfo] = this._retrofitInfo();
|
||||
return <div>
|
||||
<div className='scroll-x'>
|
||||
<table style={{ width: '100%' }}>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'sellName')}>{translate('sell')}</th>
|
||||
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'buyName')}>{translate('buy')}</th>
|
||||
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'cr')}>
|
||||
<th colSpan='2' className='sortable le' onClick={() => this._sortBy('m')}>{translate('module')}</th>
|
||||
<th colSpan='2' className='sortable le' onClick={() => this._sortBy('cr')}>
|
||||
{translate('net cost')}
|
||||
{moduleDiscount ? <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.pct(moduleDiscount)}]`}</u> : null}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
{retrofitInfo.length ?
|
||||
retrofitInfo.map((info) => (
|
||||
<tr key={info.key} className={cn('highlight', { disabled: excluded[info.key] })}
|
||||
onClick={() => this._toggleExcluded(info.key)}>
|
||||
<td className='ptr' style={{ width: '1em' }}>{info.rating}</td>
|
||||
<td className='le ptr shorten cap'>{translate(info.item)}</td>
|
||||
<td colSpan="2" className={cn('ri ptr', excluded[info.key] ? 'disabled' : (info.cost < 0 ? 'secondary-disabled' : 'warning'))}>
|
||||
{int(info.cost)}{units.CR}
|
||||
</td>
|
||||
</tr>
|
||||
)) : (
|
||||
<tr><td colSpan='7' style={{ padding: '3em 0' }}>{translate('PHRASE_NO_RETROCH')}</td></tr>
|
||||
)}
|
||||
<tr className='ri'>
|
||||
<td className='lbl' ><button onClick={this._eddbShoppingList} onMouseOver={termtip.bind(null, 'PHRASE_REFIT_SHOPPING_LIST')} onMouseOut={tooltip.bind(null, null)}><ShoppingIcon className='lg' style={{ fill: 'black' }}/></button></td>
|
||||
<td colSpan='3' className='lbl' >{translate('cost')}</td>
|
||||
<td className='lbl' >{translate('cost')}</td>
|
||||
<td colSpan='2' className={cn('val', retrofitTotal > 0 ? 'warning' : 'secondary-disabled')} style={{ borderBottom:'none' }}>
|
||||
{int(retrofitTotal)}{units.CR}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className='ri'>
|
||||
<td colSpan='4' className='lbl cap' >{translate('retrofit from')}</td>
|
||||
<td colSpan='2' className='lbl cap' >{translate('retrofit from')}</td>
|
||||
<td className='val cen' style={{ borderRight: 'none', width: '1em' }}><u className='primary-disabled'>▾</u></td>
|
||||
<td className='val' style={{ borderLeft:'none', padding: 0 }}>
|
||||
<select style={{ width: '100%', padding: 0 }} value={retrofitName || translate('Stock')} onChange={this._onBaseRetrofitChange}>
|
||||
{options}
|
||||
<option key='stock' value=''>{translate('Stock')}</option>
|
||||
{buildOptions.map((opt) => <option key={opt} value={opt}>{opt}</option>)}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -408,63 +304,50 @@ export default class CostSection extends TranslatedComponent {
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update retrofit costs
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {Ship} retrofitShip Retrofit Ship instance
|
||||
*
|
||||
* @param {*} modules
|
||||
*/
|
||||
_updateRetrofit(ship, retrofitShip) {
|
||||
let retrofitCosts = [];
|
||||
let retrofitTotal = 0, i, l, item;
|
||||
_ammoInfo() {
|
||||
const { ship } = this.props;
|
||||
const { desc, predicate } = this.state;
|
||||
|
||||
if (ship.bulkheads.m.index != retrofitShip.bulkheads.m.index) {
|
||||
item = {
|
||||
buyClassRating: ship.bulkheads.m.class + ship.bulkheads.m.rating,
|
||||
buyId: ship.bulkheads.m.eddbID,
|
||||
buyPp: ship.bulkheads.m.pp,
|
||||
buyName: ship.bulkheads.m.name,
|
||||
sellClassRating: retrofitShip.bulkheads.m.class + retrofitShip.bulkheads.m.rating,
|
||||
sellName: retrofitShip.bulkheads.m.name,
|
||||
netCost: ship.bulkheads.discountedCost - retrofitShip.bulkheads.discountedCost,
|
||||
retroItem: retrofitShip.bulkheads
|
||||
};
|
||||
retrofitCosts.push(item);
|
||||
if (retrofitShip.bulkheads.incCost) {
|
||||
retrofitTotal += item.netCost;
|
||||
let info = [{
|
||||
key: 'fuel',
|
||||
item: 'Fuel',
|
||||
qty: ship.get(FUEL_CAPACITY),
|
||||
unitCost: 50,
|
||||
cost: 50 * ship.get(FUEL_CAPACITY),
|
||||
}];
|
||||
for (let m of ship.getModules()) {
|
||||
const rebuilds = m.get('bays') * m.get('rebuildsperbay');
|
||||
const ammo = (m.get('ammomaximum') + m.get('ammoclipsize')) || rebuilds;
|
||||
if (ammo) {
|
||||
const unitCost = m.readMeta('ammocost');
|
||||
info.push({
|
||||
key: `restock_${m.getSlot()}`,
|
||||
rating: m.getClassRating(),
|
||||
item: m.readMeta('type'),
|
||||
qty: ammo,
|
||||
unitCost, cost: unitCost * ammo,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (let g in { standard: 1, internal: 1, hardpoints: 1 }) {
|
||||
let retroSlotGroup = retrofitShip[g];
|
||||
let slotGroup = ship[g];
|
||||
for (i = 0, l = slotGroup.length; i < l; i++) {
|
||||
const modId = slotGroup[i].m ? slotGroup[i].m.eddbID : null;
|
||||
const retroModId = retroSlotGroup[i].m ? retroSlotGroup[i].m.eddbID : null;
|
||||
if (modId != retroModId) {
|
||||
item = { netCost: 0, retroItem: retroSlotGroup[i] };
|
||||
if (slotGroup[i].m) {
|
||||
item.buyId = slotGroup[i].m.eddbID,
|
||||
item.buyPp = slotGroup[i].m.pp,
|
||||
item.buyName = slotGroup[i].m.name || slotGroup[i].m.grp;
|
||||
item.buyClassRating = slotGroup[i].m.class + slotGroup[i].m.rating;
|
||||
item.netCost = slotGroup[i].discountedCost;
|
||||
}
|
||||
if (retroSlotGroup[i].m) {
|
||||
item.sellName = retroSlotGroup[i].m.name || retroSlotGroup[i].m.grp;
|
||||
item.sellClassRating = retroSlotGroup[i].m.class + retroSlotGroup[i].m.rating;
|
||||
item.netCost -= retroSlotGroup[i].discountedCost;
|
||||
}
|
||||
retrofitCosts.push(item);
|
||||
if (retroSlotGroup[i].incCost) {
|
||||
retrofitTotal += item.netCost;
|
||||
}
|
||||
}
|
||||
let _sortF = undefined;
|
||||
switch (predicate) {
|
||||
case 'cr': _sortF = (o) => o.cost; break;
|
||||
case 'qty': _sortF = (o) => o.qty; break;
|
||||
case 'cost': _sortF = (o) => o.unitCost; break;
|
||||
case 'm':
|
||||
default: _sortF = (o) => o.item;
|
||||
}
|
||||
info = sortBy(info, _sortF);
|
||||
if (desc) {
|
||||
reverse(info);
|
||||
}
|
||||
|
||||
this.setState({ retrofitCosts, retrofitTotal });
|
||||
this._sortRetrofit(retrofitCosts, this.state.retroPredicate, this.state.retroDesc);
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -472,20 +355,24 @@ export default class CostSection extends TranslatedComponent {
|
||||
* @return {React.Component} Tab contents
|
||||
*/
|
||||
_ammoTab() {
|
||||
let { ammoTotal, ammoCosts } = this.state;
|
||||
let { translate, formats, units } = this.context.language;
|
||||
let int = formats.int;
|
||||
let rows = [];
|
||||
const { excluded } = this.state;
|
||||
const { translate, formats, units } = this.context.language;
|
||||
const int = formats.int;
|
||||
const rows = [];
|
||||
|
||||
for (let i = 0, l = ammoCosts.length; i < l; i++) {
|
||||
let item = ammoCosts[i];
|
||||
rows.push(<tr key={i} className='highlight'>
|
||||
<td style={{ width: '1em' }}>{item.m.class + item.m.rating}</td>
|
||||
<td className='le shorten cap'>{slotName(translate, item)}</td>
|
||||
<td className='ri'>{int(item.max)}</td>
|
||||
<td className='ri'>{int(item.cost)}{units.CR}</td>
|
||||
<td className='ri'>{int(item.total)}{units.CR}</td>
|
||||
const ammoInfo = this._ammoInfo();
|
||||
let total = 0;
|
||||
for (let i of ammoInfo) {
|
||||
const disabled = excluded[i.key];
|
||||
rows.push(<tr key={i.key} onClick={() => this._toggleExcluded(i.key)}
|
||||
className={cn('highlight', { disabled })}>
|
||||
<td style={{ width: '1em' }}>{i.rating}</td>
|
||||
<td className='le shorten cap'>{translate(i.item)}</td>
|
||||
<td className='ri'>{int(i.qty)}</td>
|
||||
<td className='ri'>{int(i.unitCost)}{units.CR}</td>
|
||||
<td className='ri'>{int(i.cost)}{units.CR}</td>
|
||||
</tr>);
|
||||
total += disabled ? 0 : i.cost;
|
||||
}
|
||||
|
||||
return <div>
|
||||
@@ -493,17 +380,17 @@ export default class CostSection extends TranslatedComponent {
|
||||
<table style={{ width: '100%' }}>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th colSpan='2' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'm')} >{translate('module')}</th>
|
||||
<th colSpan='1' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'max')} >{translate('qty')}</th>
|
||||
<th colSpan='1' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'cost')} >{translate('unit cost')}</th>
|
||||
<th className='sortable le' onClick={this._sortAmmoBy.bind(this, 'total')}>{translate('subtotal')}</th>
|
||||
<th colSpan='2' className='sortable le' onClick={() => this._sortBy('m')}>{translate('module')}</th>
|
||||
<th colSpan='1' className='sortable le' onClick={() => this._sortBy('qty')}>{translate('qty')}</th>
|
||||
<th colSpan='1' className='sortable le' onClick={() => this._sortBy('cost')}>{translate('unit cost')}</th>
|
||||
<th className='sortable le' onClick={() => this._sortBy('cr')}>{translate('subtotal')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
<tr className='ri'>
|
||||
<td colSpan='4' className='lbl' >{translate('total')}</td>
|
||||
<td className='val'>{int(ammoTotal)}{units.CR}</td>
|
||||
<td className='val'>{int(total)}{units.CR}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -511,103 +398,6 @@ export default class CostSection extends TranslatedComponent {
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate all ammo costs
|
||||
* @param {Ship} ship Ship instance
|
||||
*/
|
||||
_updateAmmoCosts(ship) {
|
||||
let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, srvs = 0, scoop = false;
|
||||
|
||||
for (let g in { standard: 1, internal: 1, hardpoints: 1 }) {
|
||||
let slotGroup = ship[g];
|
||||
for (let i = 0, l = slotGroup.length; i < l; i++) {
|
||||
if (slotGroup[i].m) {
|
||||
// Special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip
|
||||
q = 0;
|
||||
switch (slotGroup[i].m.grp) {
|
||||
case 'fs': // Skip fuel calculation if scoop present
|
||||
scoop = true;
|
||||
break;
|
||||
case 'scb':
|
||||
q = slotGroup[i].m.getAmmo() + 1;
|
||||
break;
|
||||
case 'am':
|
||||
q = slotGroup[i].m.getAmmo();
|
||||
break;
|
||||
case 'pv':
|
||||
srvs += slotGroup[i].m.getBays();
|
||||
break;
|
||||
case 'fx': case 'hb': case 'cc': case 'pc':
|
||||
limpets = ship.cargoCapacity;
|
||||
break;
|
||||
default:
|
||||
q = slotGroup[i].m.getClip() + slotGroup[i].m.getAmmo();
|
||||
}
|
||||
// Calculate ammo costs only if a cost is specified
|
||||
if (slotGroup[i].m.ammocost > 0) {
|
||||
item = {
|
||||
m: slotGroup[i].m,
|
||||
max: q,
|
||||
cost: slotGroup[i].m.ammocost,
|
||||
total: q * slotGroup[i].m.ammocost
|
||||
};
|
||||
ammoCosts.push(item);
|
||||
ammoTotal += item.total;
|
||||
}
|
||||
// Add fighters
|
||||
if (slotGroup[i].m.grp === 'fh') {
|
||||
item = {
|
||||
m: slotGroup[i].m,
|
||||
max: slotGroup[i].m.getRebuildsPerBay() * slotGroup[i].m.getBays(),
|
||||
cost: slotGroup[i].m.fightercost,
|
||||
total: slotGroup[i].m.getRebuildsPerBay() * slotGroup[i].m.getBays() * slotGroup[i].m.fightercost
|
||||
};
|
||||
ammoCosts.push(item);
|
||||
ammoTotal += item.total;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Limpets if controllers exist and cargo space available
|
||||
if (limpets > 0) {
|
||||
item = {
|
||||
m: { name: 'limpets', class: '', rating: '' },
|
||||
max: ship.cargoCapacity,
|
||||
cost: 101,
|
||||
total: ship.cargoCapacity * 101
|
||||
};
|
||||
ammoCosts.push(item);
|
||||
ammoTotal += item.total;
|
||||
}
|
||||
|
||||
if (srvs > 0) {
|
||||
item = {
|
||||
m: { name: 'SRVs', class: '', rating: '' },
|
||||
max: srvs,
|
||||
cost: 1030,
|
||||
total: srvs * 1030
|
||||
};
|
||||
ammoCosts.push(item);
|
||||
ammoTotal += item.total;
|
||||
}
|
||||
|
||||
// Calculate refuel costs if no scoop present
|
||||
if (!scoop) {
|
||||
item = {
|
||||
m: { name: 'fuel', class: '', rating: '' },
|
||||
max: ship.fuelCapacity,
|
||||
cost: 50,
|
||||
total: ship.fuelCapacity * 50
|
||||
};
|
||||
ammoCosts.push(item);
|
||||
ammoTotal += item.total;
|
||||
}
|
||||
|
||||
this.setState({ ammoTotal, ammoCosts });
|
||||
this._sortAmmo(ammoCosts, this.state.ammoPredicate, this.state.ammoDesc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add listeners on mount and update costs
|
||||
*/
|
||||
@@ -615,64 +405,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
this.listeners = [
|
||||
Persist.addListener('discounts', this._onDiscountChanged.bind(this)),
|
||||
Persist.addListener('insurance', this._onInsuranceChanged.bind(this)),
|
||||
Persist.addListener('builds', this._onBuildsChanged.bind(this)),
|
||||
];
|
||||
this._updateAmmoCosts(this.props.ship);
|
||||
this._updateRetrofit(this.props.ship, this.state.retrofitShip);
|
||||
this._sortCost(this.props.ship);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next context
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
let retrofitShip = this.state.retrofitShip;
|
||||
|
||||
if (nextProps.ship != this.props.ship) { // Ship has changed
|
||||
let nextId = nextProps.ship.id;
|
||||
let retrofitName = this._defaultRetrofitName(nextId, nextProps.buildName);
|
||||
retrofitShip = this._buildRetrofitShip(nextId, retrofitName, nextId == this.props.ship.id ? retrofitShip : null);
|
||||
this.setState({
|
||||
retrofitShip,
|
||||
retrofitName,
|
||||
buildOptions: Persist.getBuildsNamesFor(nextId)
|
||||
});
|
||||
}
|
||||
|
||||
if (nextProps.ship != this.props.ship || nextProps.code != this.props.code) {
|
||||
nextProps.ship.applyDiscounts(Persist.getShipDiscount(), Persist.getModuleDiscount());
|
||||
this._updateAmmoCosts(nextProps.ship);
|
||||
this._updateRetrofit(nextProps.ship, retrofitShip);
|
||||
this._sortCost(nextProps.ship);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort lists before render
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextState Incoming/Next state
|
||||
*/
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
let state = this.state;
|
||||
|
||||
switch (nextState.tab) {
|
||||
case 'ammo':
|
||||
if (state.ammoPredicate != nextState.ammoPredicate || state.ammoDesc != nextState.ammoDesc) {
|
||||
this._sortAmmo(nextState.ammoCosts, nextState.ammoPredicate, nextState.ammoDesc);
|
||||
}
|
||||
break;
|
||||
case 'retrofit':
|
||||
if (state.retroPredicate != nextState.retroPredicate || state.retroDesc != nextState.retroDesc) {
|
||||
this._sortRetrofit(nextState.retrofitCosts, nextState.retroPredicate, nextState.retroDesc);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (state.costPredicate != nextState.costPredicate || state.costDesc != nextState.costDesc) {
|
||||
this._sortCost(nextProps.ship, nextState.costPredicate, nextState.costDesc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import * as Calc from '../shipyard/Calculations';
|
||||
import PieChart from './PieChart';
|
||||
import VerticalBarChart from './VerticalBarChart';
|
||||
import autoBind from 'auto-bind';
|
||||
import { ARMOUR_METRICS, MODULE_PROTECTION_METRICS, SHIELD_METRICS } from 'ed-forge/lib/src/ship-stats';
|
||||
|
||||
/**
|
||||
* Defence information
|
||||
@@ -15,12 +16,10 @@ import VerticalBarChart from './VerticalBarChart';
|
||||
*/
|
||||
export default class Defence extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
marker: PropTypes.string.isRequired,
|
||||
code: PropTypes.string.isRequired,
|
||||
ship: PropTypes.object.isRequired,
|
||||
opponent: PropTypes.object.isRequired,
|
||||
engagementrange: PropTypes.number.isRequired,
|
||||
sys: PropTypes.number.isRequired,
|
||||
opponentWep: PropTypes.number.isRequired
|
||||
engagementRange: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -29,22 +28,7 @@ export default class Defence extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(props.ship, props.opponent, props.sys, props.opponentWep, props.engagementrange);
|
||||
this.state = { shield, armour, shielddamage, armourdamage };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state if our properties change
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @return {boolean} Returns true if the component should be rerendered
|
||||
*/
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.marker != nextProps.marker || this.props.sys != nextProps.sys) {
|
||||
const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(nextProps.ship, nextProps.opponent, nextProps.sys, nextProps.opponentWep, nextProps.engagementrange);
|
||||
this.setState({ shield, armour, shielddamage, armourdamage });
|
||||
}
|
||||
return true;
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,187 +36,104 @@ export default class Defence extends TranslatedComponent {
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { opponent, sys, opponentWep } = this.props;
|
||||
const { ship } = this.props;
|
||||
const { language, tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { shield, armour, shielddamage, armourdamage } = this.state;
|
||||
|
||||
const pd = opponent.standard[4].m;
|
||||
const shields = ship.get(SHIELD_METRICS);
|
||||
|
||||
const shieldSourcesData = [];
|
||||
const effectiveShieldData = [];
|
||||
const shieldDamageTakenData = [];
|
||||
const shieldSourcesTt = [];
|
||||
const shieldDamageTakenAbsoluteTt = [];
|
||||
const shieldDamageTakenExplosiveTt = [];
|
||||
const shieldDamageTakenKineticTt = [];
|
||||
const shieldDamageTakenThermalTt = [];
|
||||
const effectiveShieldAbsoluteTt = [];
|
||||
const effectiveShieldExplosiveTt = [];
|
||||
const effectiveShieldKineticTt = [];
|
||||
const effectiveShieldThermalTt = [];
|
||||
let maxEffectiveShield = 0;
|
||||
if (shield.total) {
|
||||
shieldSourcesData.push({ value: Math.round(shield.generator), label: translate('generator') });
|
||||
shieldSourcesData.push({ value: Math.round(shield.boosters), label: translate('boosters') });
|
||||
shieldSourcesData.push({ value: Math.round(shield.cells), label: translate('cells') });
|
||||
shieldSourcesData.push({ value: Math.round(shield.addition), label: translate('shield addition') });
|
||||
// Data for pie chart (absolute MJ)
|
||||
const shieldSourcesData = [
|
||||
'byBoosters', 'byGenerator', 'byReinforcements', 'bySCBs',
|
||||
].map((key) => { return { label: key, value: Math.round(shields[key]) }; });
|
||||
|
||||
if (shield.generator > 0) {
|
||||
shieldSourcesTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
|
||||
effectiveShieldAbsoluteTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
|
||||
effectiveShieldExplosiveTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
|
||||
effectiveShieldKineticTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
|
||||
effectiveShieldThermalTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
|
||||
if (shield.boosters > 0) {
|
||||
shieldSourcesTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}</div>);
|
||||
effectiveShieldAbsoluteTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}</div>);
|
||||
effectiveShieldExplosiveTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}</div>);
|
||||
effectiveShieldKineticTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}</div>);
|
||||
effectiveShieldThermalTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}</div>);
|
||||
}
|
||||
// Data for tooltip
|
||||
const shieldSourcesTt = shieldSourcesData.map((o) => {
|
||||
let { label, value } = o;
|
||||
return <div key={label}>
|
||||
{translate(label)} {formats.int(value)}{units.MJ}
|
||||
</div>;
|
||||
});
|
||||
|
||||
if (shield.cells > 0) {
|
||||
shieldSourcesTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
|
||||
effectiveShieldAbsoluteTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
|
||||
effectiveShieldExplosiveTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
|
||||
effectiveShieldKineticTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
|
||||
effectiveShieldThermalTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
|
||||
}
|
||||
// Shield resistances
|
||||
const shieldDamageTakenData = [
|
||||
'absolute', 'explosive', 'kinetic', 'thermal',
|
||||
].map((label) => {
|
||||
const dmgMult = shields[label];
|
||||
const tooltip = ['byBoosters', 'byGenerator', 'bySys'].map(
|
||||
(label) => <div key={label}>
|
||||
{translate(label)} {formats.pct1(dmgMult[label])}
|
||||
</div>
|
||||
);
|
||||
return { label, value: Math.round(100 * dmgMult.withSys), tooltip };
|
||||
});
|
||||
|
||||
// Add effective shield from resistances
|
||||
const rawMj = shield.generator + shield.boosters + shield.cells;
|
||||
const explosiveMj = rawMj / (shield.explosive.base) - rawMj;
|
||||
if (explosiveMj != 0) effectiveShieldExplosiveTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(explosiveMj)}{units.MJ}</div>);
|
||||
const kineticMj = rawMj / (shield.kinetic.base) - rawMj;
|
||||
if (kineticMj != 0) effectiveShieldKineticTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(kineticMj)}{units.MJ}</div>);
|
||||
const thermalMj = rawMj / (shield.thermal.base) - rawMj;
|
||||
if (thermalMj != 0) effectiveShieldThermalTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(thermalMj)}{units.MJ}</div>);
|
||||
// Effective MJ
|
||||
const effectiveShieldData = [
|
||||
'absolute', 'explosive', 'kinetic', 'thermal'
|
||||
].map((label) => {
|
||||
const dmgMult = shields[label];
|
||||
const raw = shields.withSCBs;
|
||||
const tooltip = ['byBoosters', 'byGenerator', 'bySys'].map(
|
||||
(label) => <div key={label}>
|
||||
{translate(label)} {formats.int(raw * dmgMult[label])}{units.MJ}
|
||||
</div>
|
||||
);
|
||||
return { label, value: Math.round(dmgMult.withSys * raw), tooltip };
|
||||
});
|
||||
const maxEffectiveShield = Math.max(...effectiveShieldData.map((o) => o.value));
|
||||
|
||||
// Add effective shield from power distributor SYS pips
|
||||
if (shield.absolute.sys != 1) {
|
||||
effectiveShieldAbsoluteTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.absolute.total - rawMj)}{units.MJ}</div>);
|
||||
effectiveShieldExplosiveTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.explosive.total - rawMj / shield.explosive.base)}{units.MJ}</div>);
|
||||
effectiveShieldKineticTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.kinetic.total - rawMj / shield.kinetic.base)}{units.MJ}</div>);
|
||||
effectiveShieldThermalTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.thermal.total - rawMj / shield.thermal.base)}{units.MJ}</div>);
|
||||
}
|
||||
}
|
||||
const armour = ship.get(ARMOUR_METRICS);
|
||||
const moduleProtection = ship.get(MODULE_PROTECTION_METRICS);
|
||||
|
||||
shieldDamageTakenAbsoluteTt.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.absolute.generator)}</div>);
|
||||
shieldDamageTakenAbsoluteTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.absolute.boosters)}</div>);
|
||||
shieldDamageTakenAbsoluteTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.absolute.sys)}</div>);
|
||||
// Data for pie chart (absolute HP)
|
||||
const armourSourcesData = ['base', 'byAlloys', 'byHRPs',].map(
|
||||
(key) => { return { label: key, value: Math.round(armour[key]) }; }
|
||||
);
|
||||
|
||||
shieldDamageTakenExplosiveTt.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.explosive.generator)}</div>);
|
||||
shieldDamageTakenExplosiveTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.explosive.boosters)}</div>);
|
||||
shieldDamageTakenExplosiveTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.explosive.sys)}</div>);
|
||||
// Data for tooltip
|
||||
const armourSourcesTt = armourSourcesData.map((o) => {
|
||||
let { label, value } = o;
|
||||
return <div key={label}>{translate(label)} {formats.int(value)}</div>;
|
||||
});
|
||||
|
||||
shieldDamageTakenKineticTt.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.kinetic.generator)}</div>);
|
||||
shieldDamageTakenKineticTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.kinetic.boosters)}</div>);
|
||||
shieldDamageTakenKineticTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.kinetic.sys)}</div>);
|
||||
// Armour resistances
|
||||
const armourDamageTakenData = [
|
||||
'absolute', 'explosive', 'kinetic', 'thermal', 'caustic',
|
||||
].map((label) => {
|
||||
const dmgMult = armour[label];
|
||||
const tooltip = ['byAlloys', 'byHRPs'].map(
|
||||
(label) => <div key={label}>
|
||||
{translate(label)} {formats.pct1(dmgMult[label])}
|
||||
</div>
|
||||
);
|
||||
return { label, value: Math.round(100 * dmgMult.damageMultiplier), tooltip };
|
||||
});
|
||||
|
||||
shieldDamageTakenThermalTt.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.thermal.generator)}</div>);
|
||||
shieldDamageTakenThermalTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.thermal.boosters)}</div>);
|
||||
shieldDamageTakenThermalTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.thermal.sys)}</div>);
|
||||
|
||||
const effectiveAbsoluteShield = shield.total / shield.absolute.total;
|
||||
effectiveShieldData.push({ value: Math.round(effectiveAbsoluteShield), label: translate('absolute'), tooltip: effectiveShieldAbsoluteTt });
|
||||
const effectiveExplosiveShield = shield.total / shield.explosive.total;
|
||||
effectiveShieldData.push({ value: Math.round(effectiveExplosiveShield), label: translate('explosive'), tooltip: effectiveShieldExplosiveTt });
|
||||
const effectiveKineticShield = shield.total / shield.kinetic.total;
|
||||
effectiveShieldData.push({ value: Math.round(effectiveKineticShield), label: translate('kinetic'), tooltip: effectiveShieldKineticTt });
|
||||
const effectiveThermalShield = shield.total / shield.thermal.total;
|
||||
effectiveShieldData.push({ value: Math.round(effectiveThermalShield), label: translate('thermal'), tooltip: effectiveShieldThermalTt });
|
||||
|
||||
shieldDamageTakenData.push({ value: Math.round(shield.absolute.total * 100), label: translate('absolute'), tooltip: shieldDamageTakenAbsoluteTt });
|
||||
shieldDamageTakenData.push({ value: Math.round(shield.explosive.total * 100), label: translate('explosive'), tooltip: shieldDamageTakenExplosiveTt });
|
||||
shieldDamageTakenData.push({ value: Math.round(shield.kinetic.total * 100), label: translate('kinetic'), tooltip: shieldDamageTakenKineticTt });
|
||||
shieldDamageTakenData.push({ value: Math.round(shield.thermal.total * 100), label: translate('thermal'), tooltip: shieldDamageTakenThermalTt });
|
||||
|
||||
maxEffectiveShield = Math.max(shield.total / shield.absolute.max, shield.total / shield.explosive.max, shield.total / shield.kinetic.max, shield.total / shield.thermal.max);
|
||||
}
|
||||
|
||||
const armourSourcesData = [];
|
||||
armourSourcesData.push({ value: Math.round(armour.bulkheads), label: translate('bulkheads') });
|
||||
armourSourcesData.push({ value: Math.round(armour.reinforcement), label: translate('reinforcement') });
|
||||
|
||||
const armourSourcesTt = [];
|
||||
const effectiveArmourAbsoluteTt = [];
|
||||
const effectiveArmourExplosiveTt = [];
|
||||
const effectiveArmourKineticTt = [];
|
||||
const effectiveArmourThermalTt = [];
|
||||
const effectiveArmourCausticTt = [];
|
||||
if (armour.bulkheads > 0) {
|
||||
armourSourcesTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
|
||||
effectiveArmourAbsoluteTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
|
||||
effectiveArmourExplosiveTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
|
||||
effectiveArmourKineticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
|
||||
effectiveArmourThermalTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
|
||||
effectiveArmourCausticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
|
||||
if (armour.reinforcement > 0) {
|
||||
armourSourcesTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
|
||||
effectiveArmourAbsoluteTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
|
||||
effectiveArmourExplosiveTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
|
||||
effectiveArmourKineticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
|
||||
effectiveArmourThermalTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
|
||||
effectiveArmourCausticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
|
||||
}
|
||||
}
|
||||
|
||||
const rawArmour = armour.bulkheads + armour.reinforcement;
|
||||
|
||||
const armourDamageTakenTt = [];
|
||||
armourDamageTakenTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.absolute.bulkheads)}</div>);
|
||||
armourDamageTakenTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.absolute.reinforcement)}</div>);
|
||||
|
||||
const armourDamageTakenExplosiveTt = [];
|
||||
armourDamageTakenExplosiveTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.explosive.bulkheads)}</div>);
|
||||
armourDamageTakenExplosiveTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.explosive.reinforcement)}</div>);
|
||||
if (armour.explosive.total != 1) effectiveArmourExplosiveTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / armour.explosive.total - rawArmour)}</div>);
|
||||
|
||||
const armourDamageTakenKineticTt = [];
|
||||
armourDamageTakenKineticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.kinetic.bulkheads)}</div>);
|
||||
armourDamageTakenKineticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.kinetic.reinforcement)}</div>);
|
||||
if (armour.kinetic.total != 1) effectiveArmourKineticTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / armour.kinetic.total - rawArmour)}</div>);
|
||||
|
||||
const armourDamageTakenThermalTt = [];
|
||||
armourDamageTakenThermalTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.thermal.bulkheads)}</div>);
|
||||
armourDamageTakenThermalTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.thermal.reinforcement)}</div>);
|
||||
if (armour.thermal.total != 1) effectiveArmourThermalTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / armour.thermal.total - rawArmour)}</div>);
|
||||
|
||||
const armourDamageTakenCausticTt = [];
|
||||
armourDamageTakenCausticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.caustic.bulkheads)}</div>);
|
||||
armourDamageTakenCausticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.caustic.reinforcement)}</div>);
|
||||
if (armour.thermal.total != 1) effectiveArmourCausticTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / armour.caustic.total - rawArmour)}</div>);
|
||||
|
||||
const effectiveArmourData = [];
|
||||
const effectiveAbsoluteArmour = armour.total / armour.absolute.total;
|
||||
effectiveArmourData.push({ value: Math.round(effectiveAbsoluteArmour), label: translate('absolute'), tooltip: effectiveArmourAbsoluteTt });
|
||||
const effectiveExplosiveArmour = armour.total / armour.explosive.total;
|
||||
effectiveArmourData.push({ value: Math.round(effectiveExplosiveArmour), label: translate('explosive'), tooltip: effectiveArmourExplosiveTt });
|
||||
const effectiveKineticArmour = armour.total / armour.kinetic.total;
|
||||
effectiveArmourData.push({ value: Math.round(effectiveKineticArmour), label: translate('kinetic'), tooltip: effectiveArmourKineticTt });
|
||||
const effectiveThermalArmour = armour.total / armour.thermal.total;
|
||||
effectiveArmourData.push({ value: Math.round(effectiveThermalArmour), label: translate('thermal'), tooltip: effectiveArmourThermalTt });
|
||||
const effectiveCausticArmour = armour.total / armour.caustic.total;
|
||||
effectiveArmourData.push({ value: Math.round(effectiveCausticArmour), label: translate('caustic'), tooltip: effectiveArmourCausticTt });
|
||||
|
||||
const armourDamageTakenData = [];
|
||||
armourDamageTakenData.push({ value: Math.round(armour.absolute.total * 100), label: translate('absolute'), tooltip: armourDamageTakenTt });
|
||||
armourDamageTakenData.push({ value: Math.round(armour.explosive.total * 100), label: translate('explosive'), tooltip: armourDamageTakenExplosiveTt });
|
||||
armourDamageTakenData.push({ value: Math.round(armour.kinetic.total * 100), label: translate('kinetic'), tooltip: armourDamageTakenKineticTt });
|
||||
armourDamageTakenData.push({ value: Math.round(armour.thermal.total * 100), label: translate('thermal'), tooltip: armourDamageTakenThermalTt });
|
||||
armourDamageTakenData.push({ value: Math.round(armour.caustic.total * 100), label: translate('caustic'), tooltip: armourDamageTakenCausticTt });
|
||||
// Effective HP
|
||||
const effectiveArmourData = [
|
||||
'absolute', 'explosive', 'kinetic', 'thermal'
|
||||
].map((label) => {
|
||||
const dmgMult = armour[label];
|
||||
const raw = armour.armour;
|
||||
const tooltip = ['byBoosters', 'byGenerator', 'bySys'].map(
|
||||
(label) => <div key={label}>
|
||||
{translate(label)} {formats.int(raw * dmgMult[label])}
|
||||
</div>
|
||||
);
|
||||
return { label, value: Math.round(dmgMult.damageMultiplier * raw), tooltip };
|
||||
});
|
||||
|
||||
return (
|
||||
<span id='defence'>
|
||||
{shield.total ? <span>
|
||||
{shields.withSCBs ? <span>
|
||||
<div className='group quarter'>
|
||||
<h2>{translate('shield metrics')}</h2>
|
||||
<br/>
|
||||
<h2 onMouseOver={termtip.bind(null, <div>{shieldSourcesTt}</div>)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw shield strength')}<br/>{formats.int(shield.total)}{units.MJ}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_LOSE_SHIELDS'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_LOSE_SHIELDS')}<br/>{shielddamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(shield.total, shielddamage.totalsdps, shielddamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * opponentWep / 4))}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SG_RECOVER'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_RECOVER_SHIELDS')}<br/>{shield.recover === Math.Inf ? translate('never') : formats.time(shield.recover)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SG_RECHARGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_RECHARGE_SHIELDS')}<br/>{shield.recharge === Math.Inf ? translate('never') : formats.time(shield.recharge)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, <div>{shieldSourcesTt}</div>)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw shield strength')}<br/>{formats.int(shields.withSCBs)}{units.MJ}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_LOSE_SHIELDS'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_LOSE_SHIELDS')}<br/>TODO</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SG_RECOVER'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_RECOVER_SHIELDS')}<br/>{shields.recover ? formats.time(shields.recover) : translate('never')}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SG_RECHARGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_RECHARGE_SHIELDS')}<br/>{shields.recharge ? formats.time(shields.recharge) : translate('never')}</h2>
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SHIELD_SOURCES'))} onMouseOut={tooltip.bind(null, null)}>{translate('shield sources')}</h2>
|
||||
@@ -250,11 +151,11 @@ export default class Defence extends TranslatedComponent {
|
||||
|
||||
<div className='group quarter'>
|
||||
<h2>{translate('armour metrics')}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, <div>{armourSourcesTt}</div>)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw armour strength')}<br/>{formats.int(armour.total)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_LOSE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_LOSE_ARMOUR')}<br/>{armourdamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(armour.total, armourdamage.totalsdps, armourdamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * opponentWep / 4))}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('raw module armour')}<br/>{formats.int(armour.modulearmour)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_PROTECTION_EXTERNAL'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_MODULE_PROTECTION_EXTERNAL')}<br/>{formats.pct1(armour.moduleprotection / 2)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_PROTECTION_INTERNAL'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_MODULE_PROTECTION_INTERNAL')}<br/>{formats.pct1(armour.moduleprotection)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, <div>{armourSourcesTt}</div>)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw armour strength')}<br/>{formats.int(armour.armour)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_LOSE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_LOSE_ARMOUR')}<br/>TODO</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('raw module armour')}<br/>{formats.int(moduleProtection.moduleArmour)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_PROTECTION_EXTERNAL'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_MODULE_PROTECTION_EXTERNAL')}<br/>{formats.pct1((1 - moduleProtection.moduleProtection) / 2)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_PROTECTION_INTERNAL'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_MODULE_PROTECTION_INTERNAL')}<br/>{formats.pct1(1 - moduleProtection.moduleProtection)}</h2>
|
||||
<br/>
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
|
||||
134
src/app/components/EDEngineerButton.jsx
Normal file
134
src/app/components/EDEngineerButton.jsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import autoBind from 'auto-bind';
|
||||
import Persist from '../stores/Persist';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getBlueprintUuid, getExperimentalUuid } from 'ed-forge/lib/src/data/blueprints';
|
||||
import { Loader, MatIcon } from '../components/SvgIcons';
|
||||
import request from 'superagent';
|
||||
import { chain, entries } from 'lodash';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
|
||||
const STATE = {
|
||||
READY: 0,
|
||||
LOADING: 1,
|
||||
ERROR: 2,
|
||||
DONE: 3,
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default class EDEngineerButton extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
ship: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
autoBind(this);
|
||||
|
||||
const { ship } = props;
|
||||
const uuids = chain(ship.getModules())
|
||||
.filter((m) => m.getBlueprint())
|
||||
.map((m) => {
|
||||
const uuids = [getBlueprintUuid(m.getBlueprint(), m.getBlueprintGrade())];
|
||||
const exp = m.getExperimental();
|
||||
if (exp) {
|
||||
uuids.push(getExperimentalUuid(exp));
|
||||
}
|
||||
return uuids;
|
||||
})
|
||||
.flatMap()
|
||||
.groupBy()
|
||||
.mapValues((v) => v.length)
|
||||
.value();
|
||||
|
||||
this.state = {
|
||||
status: STATE.READY,
|
||||
uuids,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the shopping list
|
||||
*/
|
||||
_sendToEDEngineer() {
|
||||
const { uuids } = this.state;
|
||||
this.setState({ status: STATE.LOADING });
|
||||
request.get('http://localhost:44405/commanders')
|
||||
.then((data) => {
|
||||
const [cmdr] = JSON.parse(data.text);
|
||||
return Promise.all(
|
||||
entries(uuids).map(
|
||||
(entry) => {
|
||||
const [uuid, n] = entry;
|
||||
return new Promise((resolve, reject) => {
|
||||
request.patch(`http://localhost:44405/${cmdr}/shopping-list`)
|
||||
.field('uuid', uuid)
|
||||
.field('size', n)
|
||||
.end((err, res) => {
|
||||
console.log('request goes out!');
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
})
|
||||
.then(() => this.setState({ status: STATE.DONE }))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
this.setState({ status: STATE.ERROR });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for browser compatibility of sending to ED Engineer.
|
||||
* @returns {boolean} True if browser is compatible
|
||||
*/
|
||||
_browserIsCompatible() {
|
||||
// !== Firefox 1.0+
|
||||
// TODO: Double check if this really doesn't work in firefox
|
||||
return typeof InstallTrigger === 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
render() {
|
||||
const { termtip, tooltip } = this.context;
|
||||
const hide = tooltip.bind(null, null);
|
||||
const { status } = this.state;
|
||||
|
||||
let msg = 'PHRASE_FIREFOX_EDENGINEER';
|
||||
if (this._browserIsCompatible()) {
|
||||
switch (status) {
|
||||
case STATE.READY: msg = 'Send to EDEngineer'; break;
|
||||
case STATE.LOADING: msg = 'Sending...'; break;
|
||||
case STATE.ERROR: msg = 'Error sending to EDEngineer'; break;
|
||||
case STATE.DONE: msg = 'Success! Clicking sends again.'; break;
|
||||
}
|
||||
}
|
||||
|
||||
return (<button
|
||||
disabled={!this._browserIsCompatible()}
|
||||
onClick={status !== STATE.LOADING && this._sendToEDEngineer}
|
||||
onMouseOver={termtip.bind(null, msg)}
|
||||
onMouseOut={hide}
|
||||
>
|
||||
{status === STATE.LOADING ?
|
||||
<Loader className="lg" /> :
|
||||
<MatIcon className="lg" />
|
||||
}
|
||||
</button>);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import Slider from '../components/Slider';
|
||||
import { moduleReduce } from 'ed-forge/lib/src/helper';
|
||||
|
||||
/**
|
||||
* Engagement range slider
|
||||
@@ -21,35 +22,18 @@ export default class EngagementRange extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
const { ship } = props;
|
||||
|
||||
const maxRange = Math.round(this._calcMaxRange(ship));
|
||||
|
||||
this.state = {
|
||||
maxRange
|
||||
maxRange: moduleReduce(
|
||||
this.props.ship.getHardpoints(),
|
||||
'maximumrange',
|
||||
true,
|
||||
// Don't use plain `Math.max` because callback will be passed four args
|
||||
(a, v) => Math.max(a, v),
|
||||
1000,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the maximum range of a ship's weapons
|
||||
* @param {Object} ship The ship
|
||||
* @returns {int} The maximum range, in metres
|
||||
*/
|
||||
_calcMaxRange(ship) {
|
||||
let maxRange = 1000;
|
||||
for (let i = 0; i < ship.hardpoints.length; i++) {
|
||||
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
|
||||
const thisRange = ship.hardpoints[i].m.getRange();
|
||||
if (thisRange > maxRange) {
|
||||
maxRange = thisRange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return maxRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update range
|
||||
* @param {number} rangeLevel percentage level from 0 to 1
|
||||
@@ -61,7 +45,9 @@ export default class EngagementRange extends TranslatedComponent {
|
||||
const range = Math.round(rangeLevel * maxRange);
|
||||
|
||||
if (range !== this.props.engagementRange) {
|
||||
this.props.onChange(range);
|
||||
const { onChange, ship } = this.props;
|
||||
ship.setEngagementRange(range);
|
||||
onChange(range);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +56,8 @@ export default class EngagementRange extends TranslatedComponent {
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { language, onWindowResize, sizeRatio } = this.context;
|
||||
const { formats, translate } = language;
|
||||
const { engagementRange } = this.props;
|
||||
const { maxRange } = this.state;
|
||||
|
||||
|
||||
@@ -2,98 +2,58 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import LineChart from '../components/LineChart';
|
||||
import * as Calc from '../shipyard/Calculations';
|
||||
import { getBoostMultiplier, getSpeedMultipliers } from 'ed-forge/lib/src/stats/SpeedProfile';
|
||||
import { ShipProps } from 'ed-forge';
|
||||
const { LADEN_MASS } = ShipProps;
|
||||
|
||||
/**
|
||||
* Engine profile for a given ship
|
||||
*/
|
||||
export default class EngineProfile extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
code: PropTypes.string.isRequired,
|
||||
ship: PropTypes.object.isRequired,
|
||||
cargo: PropTypes.number.isRequired,
|
||||
fuel: PropTypes.number.isRequired,
|
||||
eng: PropTypes.number.isRequired,
|
||||
pips: PropTypes.number.isRequired,
|
||||
boost: PropTypes.bool.isRequired,
|
||||
marker: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
const ship = this.props.ship;
|
||||
|
||||
this.state = {
|
||||
calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, ship, this.props.eng, this.props.boost)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the 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.marker != this.props.marker) {
|
||||
this.setState({ calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, nextProps.ship, nextProps.eng, nextProps.boost) });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the top speed for this ship given thrusters, mass and pips to ENG
|
||||
* @param {Object} ship The ship
|
||||
* @param {Object} eng The number of pips to ENG
|
||||
* @param {Object} boost If boost is enabled
|
||||
* @param {Object} mass The mass at which to calculate the top speed
|
||||
* @return {number} The maximum speed
|
||||
*/
|
||||
calcMaxSpeed(ship, eng, boost, mass) {
|
||||
// Obtain the top speed
|
||||
return Calc.calcSpeed(mass, ship.speed, ship.standard[1].m, ship.pipSpeed, eng, ship.boost / ship.speed, boost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render engine profile
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { ship, cargo, eng, fuel, boost } = this.props;
|
||||
const { language } = this.context;
|
||||
const { translate } = language;
|
||||
const { code, ship, pips, boost } = this.props;
|
||||
|
||||
// Calculate bounds for our line chart
|
||||
const thrusters = ship.standard[1].m;
|
||||
const minMass = ship.calcLowestPossibleMass({ th: thrusters });
|
||||
const maxMass = thrusters.getMaxMass();
|
||||
const mass = ship.unladenMass + fuel + cargo;
|
||||
const minSpeed = Calc.calcSpeed(maxMass, ship.speed, thrusters, ship.pipSpeed, 0, ship.boost / ship.speed, false);
|
||||
const maxSpeed = Calc.calcSpeed(minMass, ship.speed, thrusters, ship.pipSpeed, 4, ship.boost / ship.speed, true);
|
||||
// Add a mark at our current mass
|
||||
const mark = Math.min(mass, maxMass);
|
||||
|
||||
const code = `${ship.toString()}:${cargo}:${fuel}:${eng}:${boost}`;
|
||||
|
||||
const minMass = ship.readProp('hullmass');
|
||||
const maxMass = ship.getThrusters().get('enginemaximalmass');
|
||||
const baseSpeed = ship.readProp('speed');
|
||||
const baseBoost = getBoostMultiplier(ship);
|
||||
const cb = (eng, boost, mass) => {
|
||||
const mult = getSpeedMultipliers(ship, mass)[(boost ? 4 : eng) / 0.5];
|
||||
return baseSpeed * (boost ? baseBoost : 1) * mult;
|
||||
};
|
||||
// This graph can have a precipitous fall-off so we use lots of points to make it look a little smoother
|
||||
return (
|
||||
<LineChart
|
||||
xMin={minMass}
|
||||
xMax={maxMass}
|
||||
yMin={minSpeed}
|
||||
yMax={maxSpeed}
|
||||
xMark={mark}
|
||||
yMin={cb(0, false, maxMass)}
|
||||
yMax={cb(4, true, minMass)}
|
||||
// Add a mark at our current mass
|
||||
xMark={Math.min(ship.get(LADEN_MASS), maxMass)}
|
||||
xLabel={translate('mass')}
|
||||
xUnit={translate('T')}
|
||||
yLabel={translate('maximum speed')}
|
||||
yUnit={translate('m/s')}
|
||||
func={this.state.calcMaxSpeedFunc}
|
||||
func={cb.bind(this, pips.Eng.base + pips.Eng.mc, boost)}
|
||||
points={1000}
|
||||
code={code}
|
||||
// Encode boost in code to re-render on state change
|
||||
code={`${pips.Eng.base + pips.Eng.mc}:${Number(boost)}:${code}`}
|
||||
aspect={0.7}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,94 +2,47 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import LineChart from '../components/LineChart';
|
||||
import * as Calc from '../shipyard/Calculations';
|
||||
import { calculateJumpRange } from 'ed-forge/lib/src/stats/JumpRangeProfile';
|
||||
import { ShipProps } from 'ed-forge';
|
||||
const { LADEN_MASS } = ShipProps;
|
||||
|
||||
/**
|
||||
* FSD profile for a given ship
|
||||
*/
|
||||
export default class FSDProfile extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
code: PropTypes.string.isRequired,
|
||||
ship: PropTypes.object.isRequired,
|
||||
cargo: PropTypes.number.isRequired,
|
||||
fuel: PropTypes.number.isRequired,
|
||||
marker: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
const ship = this.props.ship;
|
||||
|
||||
this.state = {
|
||||
calcMaxRangeFunc: this._calcMaxRange.bind(this, ship, this.props.fuel)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the 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.marker != this.props.marker) {
|
||||
this.setState({ calcMaxRangeFunc: this._calcMaxRange.bind(this, nextProps.ship, nextProps.fuel) });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the maximum range for this ship across its applicable mass
|
||||
* @param {Object} ship The ship
|
||||
* @param {Object} fuel The fuel on the ship
|
||||
* @param {Object} mass The mass at which to calculate the maximum range
|
||||
* @return {number} The maximum range
|
||||
*/
|
||||
_calcMaxRange(ship, fuel, mass) {
|
||||
// Obtain the maximum range
|
||||
return Calc.jumpRange(mass, ship.standard[2].m, Math.min(fuel, ship.standard[2].m.getMaxFuelPerJump()), ship);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render FSD profile
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { ship, cargo, fuel } = this.props;
|
||||
|
||||
|
||||
// Calculate bounds for our line chart - use thruster info for X
|
||||
const thrusters = ship.standard[1].m;
|
||||
const fsd = ship.standard[2].m;
|
||||
const minMass = ship.calcLowestPossibleMass({ th: thrusters });
|
||||
const maxMass = thrusters.getMaxMass();
|
||||
const mass = ship.unladenMass + fuel + cargo;
|
||||
const minRange = 0;
|
||||
const maxRange = Calc.jumpRange(minMass + fsd.getMaxFuelPerJump(), fsd, fsd.getMaxFuelPerJump(), ship);
|
||||
// Add a mark at our current mass
|
||||
const mark = Math.min(mass, maxMass);
|
||||
|
||||
const code = ship.name + ship.toString() + '.' + fuel;
|
||||
const { language } = this.context;
|
||||
const { translate } = language;
|
||||
const { code, ship } = this.props;
|
||||
|
||||
const minMass = ship.readProp('hullmass');
|
||||
const maxMass = ship.getThrusters().get('enginemaximalmass');
|
||||
const mass = ship.get(LADEN_MASS);
|
||||
const cb = (mass) => calculateJumpRange(ship.getFSD(), 0, mass, Infinity, true);
|
||||
return (
|
||||
<LineChart
|
||||
xMin={minMass}
|
||||
xMax={maxMass}
|
||||
yMin={minRange}
|
||||
yMax={maxRange}
|
||||
xMark={mark}
|
||||
yMin={0}
|
||||
yMax={cb(minMass)}
|
||||
// Add a mark at our current mass
|
||||
xMark={Math.min(mass, maxMass)}
|
||||
xLabel={translate('mass')}
|
||||
xUnit={translate('T')}
|
||||
yLabel={translate('maximum range')}
|
||||
yUnit={translate('LY')}
|
||||
func={this.state.calcMaxRangeFunc}
|
||||
func={cb}
|
||||
points={200}
|
||||
code={code}
|
||||
aspect={0.7}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import Slider from '../components/Slider';
|
||||
import autoBind from 'auto-bind';
|
||||
|
||||
/**
|
||||
* Fuel slider
|
||||
@@ -21,8 +22,7 @@ export default class Fuel extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
this._fuelChange = this._fuelChange.bind(this);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import Slot from './Slot';
|
||||
import Persist from '../stores/Persist';
|
||||
import {
|
||||
DamageAbsolute,
|
||||
DamageKinetic,
|
||||
DamageThermal,
|
||||
DamageExplosive,
|
||||
MountFixed,
|
||||
MountGimballed,
|
||||
MountTurret,
|
||||
ListModifications,
|
||||
Modified
|
||||
} from './SvgIcons';
|
||||
import { Modifications } from 'coriolis-data/dist';
|
||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
import { blueprintTooltip } from '../utils/BlueprintFunctions';
|
||||
|
||||
/**
|
||||
* Hardpoint / Utility Slot
|
||||
*/
|
||||
export default class HardpointSlot extends Slot {
|
||||
|
||||
/**
|
||||
* Get the CSS class name for the slot.
|
||||
* @return {string} CSS Class name
|
||||
*/
|
||||
_getClassNames() {
|
||||
return this.props.maxClass > 0 ? 'hardpoint' : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the label for the slot
|
||||
* @param {Function} translate Translate function
|
||||
* @return {string} Label
|
||||
*/
|
||||
_getMaxClassLabel(translate) {
|
||||
return translate(['U', 'S', 'M', 'L', 'H'][this.props.maxClass]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the slot contents
|
||||
* @param {Object} m Mounted Module
|
||||
* @param {Boolean} enabled Slot enabled
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Object} formats Localized Formats map
|
||||
* @param {Object} u Localized Units Map
|
||||
* @return {React.Component} Slot contents
|
||||
*/
|
||||
_getSlotDetails(m, enabled, translate, formats, u) {
|
||||
if (m) {
|
||||
let classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`;
|
||||
let { drag, drop } = this.props;
|
||||
let { termtip, tooltip } = this.context;
|
||||
let validMods = Modifications.modules[m.grp].modifications || [];
|
||||
let showModuleResistances = Persist.showModuleResistances();
|
||||
|
||||
// Modifications tooltip shows blueprint and grade, if available
|
||||
let modTT = translate('modified');
|
||||
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 >= 0) {
|
||||
modTT += ', ' + translate(m.blueprint.special.name);
|
||||
}
|
||||
modTT = (
|
||||
<div>
|
||||
<div>{modTT}</div>
|
||||
{blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const className = cn('details', enabled ? '' : 'disabled');
|
||||
return <div className={className} draggable='true' onDragStart={drag} onDragEnd={drop}>
|
||||
<div className={'cb'}>
|
||||
<div className={'l'}>
|
||||
{m.mount && m.mount == 'F' ? <span onMouseOver={termtip.bind(null, 'fixed')}
|
||||
onMouseOut={tooltip.bind(null, null)}><MountFixed/></span> : ''}
|
||||
{m.mount && m.mount == 'G' ? <span onMouseOver={termtip.bind(null, 'gimballed')}
|
||||
onMouseOut={tooltip.bind(null, null)}><MountGimballed/></span> : ''}
|
||||
{m.mount && m.mount == 'T' ? <span onMouseOver={termtip.bind(null, 'turreted')}
|
||||
onMouseOut={tooltip.bind(null, null)}><MountTurret/></span> : ''}
|
||||
{m.getDamageDist() && m.getDamageDist().K ? <span onMouseOver={termtip.bind(null, 'kinetic')}
|
||||
onMouseOut={tooltip.bind(null, null)}><DamageKinetic/></span> : ''}
|
||||
{m.getDamageDist() && m.getDamageDist().T ? <span onMouseOver={termtip.bind(null, 'thermal')}
|
||||
onMouseOut={tooltip.bind(null, null)}><DamageThermal/></span> : ''}
|
||||
{m.getDamageDist() && m.getDamageDist().E ? <span onMouseOver={termtip.bind(null, 'explosive')}
|
||||
onMouseOut={tooltip.bind(null, null)}><DamageExplosive/></span> : ''}
|
||||
{m.getDamageDist() && m.getDamageDist().A ? <span onMouseOver={termtip.bind(null, 'absolute')}
|
||||
onMouseOut={tooltip.bind(null, null)}><DamageAbsolute/></span> : ''}
|
||||
{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? <span className='r'
|
||||
onMouseOver={termtip.bind(null, modTT)}
|
||||
onMouseOut={tooltip.bind(null, null)}><Modified/></span> : null}
|
||||
</div>
|
||||
|
||||
<div className={'r'}>{formats.round(m.getMass())}{u.T}</div>
|
||||
</div>
|
||||
<div className={'cb'}>
|
||||
{m.getDps() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getClip() ? 'dpssdps' : 'dps')}
|
||||
onMouseOut={tooltip.bind(null, null)}>{translate('DPS')}: {formats.round1(m.getDps())} {m.getClip() ?
|
||||
<span>({formats.round1(m.getSDps())})</span> : null}</div> : null}
|
||||
{m.getDamage() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getDamage() ? 'shotdmg' : 'shotdmg')}
|
||||
onMouseOut={tooltip.bind(null, null)}>{translate('shotdmg')}: {formats.round1(m.getDamage())}</div> : null}
|
||||
{m.getEps() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getClip() ? 'epsseps' : 'eps')}
|
||||
onMouseOut={tooltip.bind(null, null)}>{translate('EPS')}: {formats.round1(m.getEps())}{u.MW} {m.getClip() ?
|
||||
<span>({formats.round1(m.getEps() * m.getSustainedFactor())}{u.MW})</span> : null}</div> : null}
|
||||
{m.getHps() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getClip() ? 'hpsshps' : 'hps')}
|
||||
onMouseOut={tooltip.bind(null, null)}>{translate('HPS')}: {formats.round1(m.getHps())} {m.getClip() ?
|
||||
<span>({formats.round1(m.getHps() * m.getSustainedFactor())})</span> : null}</div> : null}
|
||||
{m.getDps() && m.getEps() ? <div className={'l'} onMouseOver={termtip.bind(null, 'dpe')}
|
||||
onMouseOut={tooltip.bind(null, null)}>{translate('DPE')}: {formats.f1(m.getDps() / m.getEps())}</div> : null}
|
||||
{m.getRoF() ? <div className={'l'} onMouseOver={termtip.bind(null, 'rof')}
|
||||
onMouseOut={tooltip.bind(null, null)}>{translate('ROF')}: {formats.f1(m.getRoF())}{u.ps}</div> : null}
|
||||
{m.getRange() ? <div
|
||||
className={'l'}>{translate('range', m.grp)} {formats.f1(m.getRange() / 1000)}{u.km}</div> : null}
|
||||
{m.getScanTime() ? <div
|
||||
className={'l'}>{translate('scantime')} {formats.f1(m.getScanTime())}{u.s}</div> : null}
|
||||
{m.getFalloff() ? <div
|
||||
className={'l'}>{translate('falloff')} {formats.round(m.getFalloff() / 1000)}{u.km}</div> : null}
|
||||
{m.getShieldBoost() ? <div className={'l'}>+{formats.pct1(m.getShieldBoost())}</div> : null}
|
||||
{m.getAmmo() ? <div
|
||||
className={'l'}>{translate('ammunition')}: {formats.int(m.getClip())}/{formats.int(m.getAmmo())}</div> : null}
|
||||
{m.getReload() ? <div className={'l'}>{translate('wep_reload')}: {formats.round(m.getReload())}{u.s}</div> : null}
|
||||
{m.getShotSpeed() ? <div
|
||||
className={'l'}>{translate('shotspeed')}: {formats.int(m.getShotSpeed())}{u.mps}</div> : null}
|
||||
{m.getPiercing() ? <div className={'l'}>{translate('piercing')}: {formats.int(m.getPiercing())}</div> : null}
|
||||
{m.getJitter() ? <div className={'l'}>{translate('jitter')}: {formats.f2(m.getJitter())}°</div> : null}
|
||||
{m.getScanAngle() ? <div className={'l'}>{translate('scan angle')}: {formats.f2(m.getScanAngle())}°</div> : null}
|
||||
{m.getScanRange() ? <div className={'l'}>{translate('scan range')}: {formats.int(m.getScanRange())}{u.m}</div> : null}
|
||||
{m.getMaxAngle() ? <div className={'l'}>{translate('max angle')}: {formats.f2(m.getMaxAngle())}°</div> : null}
|
||||
{showModuleResistances && m.getExplosiveResistance() ? <div
|
||||
className='l'>{translate('explres')}: {formats.pct(m.getExplosiveResistance())}</div> : null}
|
||||
{showModuleResistances && m.getKineticResistance() ? <div
|
||||
className='l'>{translate('kinres')}: {formats.pct(m.getKineticResistance())}</div> : null}
|
||||
{showModuleResistances && m.getThermalResistance() ? <div
|
||||
className='l'>{translate('thermres')}: {formats.pct(m.getThermalResistance())}</div> : null}
|
||||
{m.getIntegrity() ? <div className='l'>{translate('integrity')}: {formats.int(m.getIntegrity())}</div> : null}
|
||||
{m && validMods.length > 0 ? <div className='r' tabIndex="0" ref={modButton => this.modButton = modButton}>
|
||||
<button tabIndex="-1" onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation}
|
||||
onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}>
|
||||
<ListModifications/></button>
|
||||
</div> : null}
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
return <div className={'empty'}>{translate('empty')}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,63 +1,58 @@
|
||||
import React from 'react';
|
||||
import SlotSection from './SlotSection';
|
||||
import HardpointSlot from './HardpointSlot';
|
||||
import Slot from './Slot';
|
||||
import { MountFixed, MountGimballed, MountTurret } from '../components/SvgIcons';
|
||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
import autoBind from 'auto-bind';
|
||||
|
||||
const SIZE_ORDER = ['huge', 'large', 'medium', 'small'];
|
||||
|
||||
/**
|
||||
* Hardpoint slot section
|
||||
*/
|
||||
export default class HardpointSlotSection extends SlotSection {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context, 'hardpoints', 'hardpoints');
|
||||
this._empty = this._empty.bind(this);
|
||||
this.selectedRefId = null;
|
||||
this.firstRefId = 'emptyall';
|
||||
this.lastRefId = 'nl-F';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle focus when component updates
|
||||
* @param {Object} prevProps React Component properties
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
this._handleSectionFocus(prevProps,this.firstRefId, this.lastRefId);
|
||||
constructor(props) {
|
||||
super(props, 'hardpoints');
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all slots
|
||||
*/
|
||||
_empty() {
|
||||
this.selectedRefId = 'emptyall';
|
||||
this.props.ship.emptyWeapons();
|
||||
this.props.onChange();
|
||||
this.props.ship.getHardpoints(undefined, true).forEach((slot) => slot.reset());
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill slots with specified module
|
||||
* @param {string} group Group name
|
||||
* @param {string} mount Mount Type - F, G, T
|
||||
* @param {string} type Type of item
|
||||
* @param {string} rating Mount Type - (fixed, gimbal, turret)
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fill(group, mount, event) {
|
||||
this.selectedRefId = group + '-' + mount;
|
||||
this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt'));
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
_fill(type, rating, event) {
|
||||
const fillAll = event.getModifierState('Alt');
|
||||
this.props.ship.getHardpoints(undefined, true).forEach((slot) => {
|
||||
if (slot.isEmpty() || fillAll) {
|
||||
const slotSize = slot.getSize();
|
||||
const fittingSizes = SIZE_ORDER.slice(SIZE_ORDER.findIndex((e) => e === slotSize));
|
||||
for (const size of fittingSizes) {
|
||||
try {
|
||||
slot.setItem(type, size, rating);
|
||||
} catch (err) {
|
||||
// Try next item if this doesn't fit/exist
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all on section header right click
|
||||
*/
|
||||
_contextMenu() {
|
||||
this._empty();
|
||||
// If still here, we were able to apply the module
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,33 +60,24 @@ export default class HardpointSlotSection extends SlotSection {
|
||||
* @return {Array} Array of Slots
|
||||
*/
|
||||
_getSlots() {
|
||||
let { ship, currentMenu } = this.props;
|
||||
let { ship, currentMenu, propsToShow, onPropToggle } = this.props;
|
||||
let { originSlot, targetSlot } = this.state;
|
||||
let slots = [];
|
||||
let hardpoints = ship.hardpoints;
|
||||
let availableModules = ship.getAvailableModules();
|
||||
|
||||
for (let i = 0, l = hardpoints.length; i < l; i++) {
|
||||
let h = hardpoints[i];
|
||||
if (h.maxClass) {
|
||||
slots.push(<HardpointSlot
|
||||
key={i}
|
||||
maxClass={h.maxClass}
|
||||
availableModules={() => availableModules.getHps(h.maxClass)}
|
||||
onOpen={this._openMenu.bind(this, h)}
|
||||
onSelect={this._selectModule.bind(this, h)}
|
||||
onChange={this.props.onChange}
|
||||
selected={currentMenu == h}
|
||||
for (let h of ship.getHardpoints(undefined, true)) {
|
||||
slots.push(<Slot
|
||||
key={h.object.Slot}
|
||||
currentMenu={currentMenu}
|
||||
drag={this._drag.bind(this, h)}
|
||||
dragOver={this._dragOverSlot.bind(this, h)}
|
||||
drop={this._drop}
|
||||
dropClass={this._dropClass(h, originSlot, targetSlot)}
|
||||
ship={ship}
|
||||
m={h.m}
|
||||
m={h}
|
||||
enabled={h.enabled ? true : false}
|
||||
propsToShow={propsToShow}
|
||||
onPropToggle={onPropToggle}
|
||||
/>);
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
@@ -101,68 +87,68 @@ export default class HardpointSlotSection extends SlotSection {
|
||||
* @param {Function} translate Translate function
|
||||
* @return {React.Component} Section menu
|
||||
*/
|
||||
_getSectionMenu(translate) {
|
||||
_getSectionMenu() {
|
||||
const { translate } = this.context.language;
|
||||
let _fill = this._fill;
|
||||
|
||||
return <div className='select hardpoint' onClick={(e) => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
|
||||
<ul>
|
||||
<li className='lc' tabIndex='0' onClick={this._empty} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['emptyall'] = smRef}>{translate('empty all')}</li>
|
||||
<li className='lc' tabIndex="0" onClick={this._empty}>{translate('empty all')}</li>
|
||||
<li className='optional-hide' style={{ textAlign: 'center', marginTop: '1em' }}>{translate('PHRASE_ALT_ALL')}</li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('pl')}</div>
|
||||
<div className='select-group cap'>{translate('pulselaser')}</div>
|
||||
<ul>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'pl', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pl-F'] = smRef}><MountFixed className='lg'/></li>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'pl', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pl-G'] = smRef}><MountGimballed className='lg'/></li>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'pl', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pl-T'] = smRef}><MountTurret className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'pulselaser', 'fixed')}><MountFixed className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'pulselaser', 'gimbal')}><MountGimballed className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'pulselaser', 'turret')}><MountTurret className='lg'/></li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('ul')}</div>
|
||||
<div className='select-group cap'>{translate('burstlaser')}</div>
|
||||
<ul>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'ul', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ul-F'] = smRef}><MountFixed className='lg'/></li>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'ul', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ul-G'] = smRef}><MountGimballed className='lg'/></li>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'ul', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ul-T'] = smRef}><MountTurret className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'burstlaser', 'fixed')}><MountFixed className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'burstlaser', 'gimbal')}><MountGimballed className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'burstlaser', 'turret')}><MountTurret className='lg'/></li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('bl')}</div>
|
||||
<div className='select-group cap'>{translate('beamlaser')}</div>
|
||||
<ul>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'bl', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['bl-F'] = smRef}><MountFixed className='lg'/></li>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'bl', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['bl-G'] = smRef}><MountGimballed className='lg'/></li>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'bl', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['bl-T'] = smRef}><MountTurret className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'beamlaser', 'fixed')}><MountFixed className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'beamlaser', 'gimbal')}><MountGimballed className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'beamlaser', 'turret')}><MountTurret className='lg'/></li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('mc')}</div>
|
||||
<div className='select-group cap'>{translate('multicannon')}</div>
|
||||
<ul>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'mc', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['mc-F'] = smRef}><MountFixed className='lg'/></li>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'mc', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['mc-G'] = smRef}><MountGimballed className='lg'/></li>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'mc', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['mc-T'] = smRef}><MountTurret className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'multicannon', 'fixed')}><MountFixed className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'multicannon', 'gimbal')}><MountGimballed className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'multicannon', 'turret')}><MountTurret className='lg'/></li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('c')}</div>
|
||||
<div className='select-group cap'>{translate('cannon')}</div>
|
||||
<ul>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'c', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['c-F'] = smRef}><MountFixed className='lg'/></li>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'c', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['c-G'] = smRef}><MountGimballed className='lg'/></li>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'c', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['c-T'] = smRef}><MountTurret className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'cannon', 'fixed')}><MountFixed className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'cannon', 'gimbal')}><MountGimballed className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'cannon', 'turret')}><MountTurret className='lg'/></li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('fc')}</div>
|
||||
<div className='select-group cap'>{translate('fragcannon')}</div>
|
||||
<ul>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'fc', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['fc-F'] = smRef}><MountFixed className='lg'/></li>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'fc', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['fc-G'] = smRef}><MountGimballed className='lg'/></li>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'fc', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['fc-T'] = smRef}><MountTurret className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'fragcannon', 'fixed')}><MountFixed className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'fragcannon', 'gimbal')}><MountGimballed className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'fragcannon', 'turret')}><MountTurret className='lg'/></li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('pa')}</div>
|
||||
<div className='select-group cap'>{translate('plasmaacc')}</div>
|
||||
<ul>
|
||||
<li className='lc' tabIndex='0' onClick={_fill.bind(this, 'pa', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pa-F'] = smRef}>{translate('pa')}</li>
|
||||
<li className='lc' tabIndex="0" onClick={_fill.bind(this, 'plasmaacc', 'fixed')}>{translate('pa')}</li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('rg')}</div>
|
||||
<div className='select-group cap'>{translate('railgun')}</div>
|
||||
<ul>
|
||||
<li className='lc' tabIndex='0' onClick={_fill.bind(this, 'rg', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['rg-F'] = smRef}>{translate('rg')}</li>
|
||||
<li className='lc' tabIndex="0" onClick={_fill.bind(this, 'railgun', 'fixed')}>{translate('rg')}</li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('nl')}</div>
|
||||
<div className='select-group cap'>{translate('minelauncher')}</div>
|
||||
<ul>
|
||||
<li className='lc' tabIndex='0' onClick={_fill.bind(this, 'nl', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['nl-F'] = smRef}>{translate('nl')}</li>
|
||||
<li className='lc' tabIndex="0" onClick={_fill.bind(this, 'minelauncher', 'fixed')}>{translate('nl')}</li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('rfl')}</div>
|
||||
<div className='select-group cap'>{translate('flaklauncher')}</div>
|
||||
<ul>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'rfl', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['rfl-F'] = smRef}><MountFixed className='lg'/></li>
|
||||
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'rfl', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['rfl-T'] = smRef}><MountTurret className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'flaklauncher', 'fixed')}><MountFixed className='lg'/></li>
|
||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'flaklauncher', 'turret')}><MountTurret className='lg'/></li>
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@ import Link from './Link';
|
||||
import ActiveLink from './ActiveLink';
|
||||
import cn from 'classnames';
|
||||
import { Cogs, CoriolisLogo, Hammer, Help, Rocket, StatsBars } from './SvgIcons';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import Persist from '../stores/Persist';
|
||||
import { toDetailedExport } from '../shipyard/Serializer';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import ModalDeleteAll from './ModalDeleteAll';
|
||||
import ModalExport from './ModalExport';
|
||||
import ModalHelp from './ModalHelp';
|
||||
@@ -17,6 +14,9 @@ import ModalImport from './ModalImport';
|
||||
import Slider from './Slider';
|
||||
import Announcement from './Announcement';
|
||||
import { outfitURL } from '../utils/UrlGenerators';
|
||||
import autoBind from 'auto-bind';
|
||||
import { Factory, Ship } from 'ed-forge';
|
||||
import { chain, entries } from 'lodash';
|
||||
|
||||
const SIZE_MIN = 0.65;
|
||||
const SIZE_RANGE = 0.55;
|
||||
@@ -55,7 +55,6 @@ function selectAll(e) {
|
||||
* Coriolis App Header section / menus
|
||||
*/
|
||||
export default class Header extends TranslatedComponent {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
@@ -63,24 +62,14 @@ export default class Header extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this.shipOrder = Object.keys(Ships).sort();
|
||||
autoBind(this);
|
||||
this.ships = Factory.getAllShipTypes().sort();
|
||||
|
||||
this._setLanguage = this._setLanguage.bind(this);
|
||||
this._setInsurance = this._setInsurance.bind(this);
|
||||
this._setShipDiscount = this._setShipDiscount.bind(this);
|
||||
this._changeShipDiscount = this._changeShipDiscount.bind(this);
|
||||
this._kpShipDiscount = this._kpShipDiscount.bind(this);
|
||||
this._setModuleDiscount = this._setModuleDiscount.bind(this);
|
||||
this._changeModuleDiscount = this._changeModuleDiscount.bind(this);
|
||||
this._kpModuleDiscount = this._kpModuleDiscount.bind(this);
|
||||
this._openShips = this._openMenu.bind(this, 's');
|
||||
this._openBuilds = this._openMenu.bind(this, 'b');
|
||||
this._openComp = this._openMenu.bind(this, 'comp');
|
||||
this._openAnnounce = this._openMenu.bind(this, 'announce');
|
||||
this._getAnnouncementsMenu = this._getAnnouncementsMenu.bind(this);
|
||||
this._openSettings = this._openMenu.bind(this, 'settings');
|
||||
this._showHelp = this._showHelp.bind(this);
|
||||
this.update = this.update.bind(this);
|
||||
this.languageOptions = [];
|
||||
this.insuranceOptions = [];
|
||||
this.state = {
|
||||
@@ -210,13 +199,6 @@ export default class Header extends TranslatedComponent {
|
||||
Persist.showTooltips(!Persist.showTooltips());
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle module resistances setting
|
||||
*/
|
||||
_toggleModuleResistances() {
|
||||
Persist.showModuleResistances(!Persist.showModuleResistances());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show delete all modal
|
||||
* @param {SyntheticEvent} e Event
|
||||
@@ -226,20 +208,6 @@ export default class Header extends TranslatedComponent {
|
||||
this.context.showModal(<ModalDeleteAll />);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show export modal with backup data
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_showBackup(e) {
|
||||
let translate = this.context.language.translate;
|
||||
e.preventDefault();
|
||||
this.context.showModal(<ModalExport
|
||||
title={translate('backup')}
|
||||
description={translate('PHRASE_BACKUP_DESC')}
|
||||
data={Persist.getAll()}
|
||||
/>);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show export modal with detailed export
|
||||
* @param {SyntheticEvent} e Event
|
||||
@@ -248,10 +216,22 @@ export default class Header extends TranslatedComponent {
|
||||
let translate = this.context.language.translate;
|
||||
e.preventDefault();
|
||||
|
||||
const builds = chain(Persist.getBuilds())
|
||||
.values()
|
||||
.map((builds) => Object.values(builds))
|
||||
.flatMap()
|
||||
.map((code) => new Ship(code))
|
||||
.value();
|
||||
|
||||
this.context.showModal(<ModalExport
|
||||
title={translate('detailed export')}
|
||||
description={translate('PHRASE_EXPORT_DESC')}
|
||||
data={toDetailedExport(Persist.getBuilds())}
|
||||
data={JSON.stringify(builds.map((build) => {
|
||||
return {
|
||||
header: { appName: 'Inara', 'appVersion': '1.0' },
|
||||
data: build.toJSON(),
|
||||
};
|
||||
}))}
|
||||
/>);
|
||||
}
|
||||
|
||||
@@ -309,15 +289,10 @@ export default class Header extends TranslatedComponent {
|
||||
* @return {React.Component} Menu
|
||||
*/
|
||||
_getShipsMenu() {
|
||||
let shipList = [];
|
||||
|
||||
for (let s of this.shipOrder) {
|
||||
shipList.push(<ActiveLink key={s} href={outfitURL(s)} className='block'>{Ships[s].properties.name}</ActiveLink>);
|
||||
}
|
||||
|
||||
const { translate } = this.context.language;
|
||||
return (
|
||||
<div className='menu-list dbl no-wrap' onClick={ (e) => e.stopPropagation() }>
|
||||
{shipList}
|
||||
{this.ships.map((s) => <ActiveLink key={s} href={outfitURL(s)} className='block'>{translate(s)}</ActiveLink>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -327,9 +302,10 @@ export default class Header extends TranslatedComponent {
|
||||
* @return {React.Component} Menu
|
||||
*/
|
||||
_getBuildsMenu() {
|
||||
const { translate } = this.context.language;
|
||||
let builds = Persist.getBuilds();
|
||||
let buildList = [];
|
||||
for (let shipId of this.shipOrder) {
|
||||
for (let shipId of this.ships) {
|
||||
if (builds[shipId]) {
|
||||
let shipBuilds = [];
|
||||
let buildNameOrder = Object.keys(builds[shipId]).sort();
|
||||
@@ -337,7 +313,7 @@ export default class Header extends TranslatedComponent {
|
||||
let href = outfitURL(shipId, builds[shipId][buildName], buildName);
|
||||
shipBuilds.push(<li key={shipId + '-' + buildName} ><ActiveLink href={href} className='block'>{buildName}</ActiveLink></li>);
|
||||
}
|
||||
buildList.push(<ul key={shipId}>{Ships[shipId].properties.name}{shipBuilds}</ul>);
|
||||
buildList.push(<ul key={shipId}>{translate(shipId)}{shipBuilds}</ul>);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,7 +386,6 @@ export default class Header extends TranslatedComponent {
|
||||
_getSettingsMenu() {
|
||||
let translate = this.context.language.translate;
|
||||
let tips = Persist.showTooltips();
|
||||
let moduleResistances = Persist.showModuleResistances();
|
||||
|
||||
return (
|
||||
<div className='menu-list no-wrap cap' onClick={ (e) => e.stopPropagation() }>
|
||||
@@ -428,10 +403,6 @@ export default class Header extends TranslatedComponent {
|
||||
<td>{translate('tooltips')}</td>
|
||||
<td className={cn('ri', { disabled: !tips, 'primary-disabled': tips })}>{(tips ? '✓' : '✗')}</td>
|
||||
</tr>
|
||||
<tr className='cap ptr' onClick={this._toggleModuleResistances} >
|
||||
<td>{translate('module resistances')}</td>
|
||||
<td className={cn('ri', { disabled: !moduleResistances, 'primary-disabled': moduleResistances })}>{(moduleResistances ? '✓' : '✗')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{translate('insurance')}</td>
|
||||
<td className='ri'>
|
||||
@@ -459,10 +430,9 @@ export default class Header extends TranslatedComponent {
|
||||
<hr />
|
||||
<ul style={{ width: '100%' }}>
|
||||
{translate('builds')} & {translate('comparisons')}
|
||||
<li><Link href="#" className='block' onClick={this._showBackup.bind(this)}>{translate('backup')}</Link></li>
|
||||
<li><Link href="#" className='block' onClick={this._showDetailedExport.bind(this)}>{translate('detailed export')}</Link></li>
|
||||
<li><Link href="#" className='block' onClick={this._showImport.bind(this)}>{translate('import')}</Link></li>
|
||||
<li><Link href="#" className='block' onClick={this._showDeleteAll.bind(this)}>{translate('delete all')}</Link></li>
|
||||
<li><Link href="#" className='block' onClick={this._showDetailedExport}>{translate('detailed export')}</Link></li>
|
||||
<li><Link href="#" className='block' onClick={this._showImport}>{translate('import')}</Link></li>
|
||||
<li><Link href="#" className='block' onClick={this._showDeleteAll}>{translate('delete all')}</Link></li>
|
||||
</ul>
|
||||
<hr />
|
||||
<table style={{ width: 300, backgroundColor: 'transparent' }}>
|
||||
@@ -494,7 +464,6 @@ export default class Header extends TranslatedComponent {
|
||||
Persist.addListener('deletedAll', update);
|
||||
Persist.addListener('builds', update);
|
||||
Persist.addListener('tooltips', update);
|
||||
Persist.addListener('moduleresistances', update);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -564,19 +533,13 @@ export default class Header extends TranslatedComponent {
|
||||
{openedMenu == 'b' ? this._getBuildsMenu() : null}
|
||||
</div>
|
||||
|
||||
<div className='l menu'>
|
||||
<div className={cn('menu-header', { selected: openedMenu == 'comp', disabled: !hasBuilds })} onClick={hasBuilds && this._openComp}>
|
||||
<StatsBars className={cn('warning', { 'warning-disabled': !hasBuilds })} /><span className='menu-item-label'>{translate('compare')}</span>
|
||||
</div>
|
||||
{openedMenu == 'comp' ? this._getComparisonsMenu() : null}
|
||||
</div>
|
||||
|
||||
<div className='l menu'>
|
||||
<div className={cn('menu-header', { selected: openedMenu == 'announce', disabled: this.props.announcements.length === 0})} onClick={this.props.announcements.length !== 0 && this._openAnnounce}>
|
||||
{/* TODO: Enable */}
|
||||
{/* <div className='l menu'>
|
||||
<div className={cn('menu-header', { selected: openedMenu == 'announce', disabled: this.props.announcements.length === 0 })} onClick={this.props.announcements.length !== 0 && this._openAnnounce}>
|
||||
<span className='menu-item-label'>{translate('announcements')}</span>
|
||||
</div>
|
||||
{openedMenu == 'announce' ? this._getAnnouncementsMenu() : null}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{window.location.origin.search('.edcd.io') >= 0 ?
|
||||
<div className='l menu'>
|
||||
@@ -603,5 +566,4 @@ export default class Header extends TranslatedComponent {
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import Slot from './Slot';
|
||||
import Persist from '../stores/Persist';
|
||||
import { ListModifications, Modified } from './SvgIcons';
|
||||
import { Modifications } from 'coriolis-data/dist';
|
||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
import { blueprintTooltip } from '../utils/BlueprintFunctions';
|
||||
|
||||
/**
|
||||
* Internal Slot
|
||||
*/
|
||||
export default class InternalSlot extends Slot {
|
||||
|
||||
/**
|
||||
* Generate the slot contents
|
||||
* @param {Object} m Mounted Module
|
||||
* @param {Boolean} enabled Slot enabled
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Object} formats Localized Formats map
|
||||
* @param {Object} u Localized Units Map
|
||||
* @return {React.Component} Slot contents
|
||||
*/
|
||||
_getSlotDetails(m, enabled, translate, formats, u) {
|
||||
if (m) {
|
||||
let classRating = m.class + m.rating;
|
||||
let { drag, drop, ship } = this.props;
|
||||
let { termtip, tooltip } = this.context;
|
||||
let validMods = (Modifications.modules[m.grp] ? Modifications.modules[m.grp].modifications : []);
|
||||
let showModuleResistances = Persist.showModuleResistances();
|
||||
|
||||
// Modifications tooltip shows blueprint and grade, if available
|
||||
let modTT = translate('modified');
|
||||
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 >= 0) {
|
||||
modTT += ', ' + translate(m.blueprint.special.name);
|
||||
}
|
||||
modTT = (
|
||||
<div>
|
||||
<div>{modTT}</div>
|
||||
{blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let mass = m.getMass() || m.cargo || m.fuel || 0;
|
||||
|
||||
const className = cn('details', enabled ? '' : 'disabled');
|
||||
return <div className={className} draggable='true' onDragStart={drag} onDragEnd={drop}>
|
||||
<div className={'cb'}>
|
||||
<div className={'l'}>{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? <span onMouseOver={termtip.bind(null, modTT)} onMouseOut={tooltip.bind(null, null)}><Modified /></span> : ''}</div>
|
||||
<div className={'r'}>{formats.round(mass)}{u.T}</div>
|
||||
</div>
|
||||
<div className={'cb'}>
|
||||
{ m.getOptMass() ? <div className={'l'}>{translate('optmass', 'sg')}: {formats.int(m.getOptMass())}{u.T}</div> : null }
|
||||
{ m.getMaxMass() ? <div className={'l'}>{translate('maxmass', 'sg')}: {formats.int(m.getMaxMass())}{u.T}</div> : null }
|
||||
{ m.bins ? <div className={'l'}>{m.bins} <u>{translate('bins')}</u></div> : null }
|
||||
{ m.bays ? <div className={'l'}>{translate('bays')}: {m.bays}</div> : null }
|
||||
{ m.rebuildsperbay ? <div className={'l'}>{translate('rebuildsperbay')}: {m.rebuildsperbay}</div> : null }
|
||||
{ m.rate ? <div className={'l'}>{translate('rate')}: {m.rate}{u.kgs} {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}</div> : null }
|
||||
{ m.getAmmo() && m.grp !== 'scb' ? <div className={'l'}>{translate('ammunition')}: {formats.gen(m.getAmmo())}</div> : null }
|
||||
{ m.getSpinup() ? <div className={'l'}>{translate('spinup')}: {formats.f1(m.getSpinup())}{u.s}</div> : null }
|
||||
{ m.getDuration() ? <div className={'l'}>{translate('duration')}: {formats.f1(m.getDuration())}{u.s}</div> : null }
|
||||
{ m.grp === 'scb' ? <div className={'l'}>{translate('cells')}: {formats.int(m.getAmmo() + 1)}</div> : null }
|
||||
{ m.grp === 'gsrp' ? <div className={'l'}>{translate('shield addition')}: {formats.f1(m.getShieldAddition())}{u.MJ}</div> : null }
|
||||
{ m.grp === 'gfsb' ? <div className={'l'}>{translate('jump addition')}: {formats.f1(m.getJumpBoost())}{u.LY}</div> : null }
|
||||
{ m.grp === 'gs' ? <div className={'l'}>{translate('shield addition')}: {formats.f1(m.getShieldAddition())}{u.MJ}</div> : null }
|
||||
{ m.getShieldReinforcement() ? <div className={'l'}>{translate('shieldreinforcement')}: {formats.f1(m.getDuration() * m.getShieldReinforcement())}{u.MJ}</div> : null }
|
||||
{ m.getShieldReinforcement() ? <div className={'l'}>{translate('total')}: {formats.int((m.getAmmo() + 1) * (m.getDuration() * m.getShieldReinforcement()))}{u.MJ}</div> : null }
|
||||
{ m.repair ? <div className={'l'}>{translate('repair')}: {m.repair}</div> : null }
|
||||
{ m.getFacingLimit() ? <div className={'l'}>{translate('facinglimit')} {formats.f1(m.getFacingLimit())}°</div> : null }
|
||||
{ m.getRange() ? <div className={'l'}>{translate('range')} {formats.f2(m.getRange())}{u.km}</div> : null }
|
||||
{ m.getRangeT() ? <div className={'l'}>{translate('ranget')} {formats.f1(m.getRangeT())}{u.s}</div> : null }
|
||||
{ m.getTime() ? <div className={'l'}>{translate('time')}: {formats.time(m.getTime())}</div> : null }
|
||||
{ m.getHackTime() ? <div className={'l'}>{translate('hacktime')}: {formats.time(m.getHackTime())}</div> : null }
|
||||
{ m.maximum ? <div className={'l'}>{translate('max')}: {(m.maximum)}</div> : null }
|
||||
{ m.rangeLS ? <div className={'l'}>{translate('range')}: {m.rangeLS}{u.Ls}</div> : null }
|
||||
{ m.rangeLS === null ? <div className={'l'}>∞{u.Ls}</div> : null }
|
||||
{ m.rangeRating ? <div className={'l'}>{translate('range')}: {m.rangeRating}</div> : null }
|
||||
{ m.passengers ? <div className={'l'}>{translate('passengers')}: {m.passengers}</div> : null }
|
||||
{ m.getRegenerationRate() ? <div className='l'>{translate('regen')}: {formats.round1(m.getRegenerationRate())}{u.ps}</div> : null }
|
||||
{ m.getBrokenRegenerationRate() ? <div className='l'>{translate('brokenregen')}: {formats.round1(m.getBrokenRegenerationRate())}{u.ps}</div> : null }
|
||||
{ showModuleResistances && m.getExplosiveResistance() ? <div className='l'>{translate('explres')}: {formats.pct(m.getExplosiveResistance())}</div> : null }
|
||||
{ showModuleResistances && m.getKineticResistance() ? <div className='l'>{translate('kinres')}: {formats.pct(m.getKineticResistance())}</div> : null }
|
||||
{ showModuleResistances && m.getThermalResistance() ? <div className='l'>{translate('thermres')}: {formats.pct(m.getThermalResistance())}</div> : null }
|
||||
{ showModuleResistances && m.getCausticResistance() ? <div className='l'>{translate('causres')}: {formats.pct(m.getCausticResistance())}</div> : null }
|
||||
{ m.getHullReinforcement() ? <div className='l'>{translate('armour')}: {formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost') / 10000)}</div> : null }
|
||||
{ m.getProtection() ? <div className='l'>{translate('protection')}: {formats.rPct(m.getProtection())}</div> : null }
|
||||
{ m.getIntegrity() ? <div className='l'>{translate('integrity')}: {formats.int(m.getIntegrity())}</div> : null }
|
||||
{ m && validMods.length > 0 ? <div className='r' tabIndex="0" ref={ modButton => this.modButton = modButton }><button tabIndex="-1" onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
return <div className={'empty'}>{translate('empty')}</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,46 @@
|
||||
import React from 'react';
|
||||
import SlotSection from './SlotSection';
|
||||
import InternalSlot from './InternalSlot';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import Slot from './Slot';
|
||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
import { canMount } from '../utils/SlotFunctions';
|
||||
import autoBind from 'auto-bind';
|
||||
import { TYPES } from 'ed-forge/lib/src/data/slots';
|
||||
|
||||
/**
|
||||
* Sets all empty slots of a ship to a item of the given size.
|
||||
* @param {Ship} ship Ship to set items for
|
||||
* @param {boolean} fillAll True to also fill occupied
|
||||
* @param {string} type Item type
|
||||
* @param {string} rating Item rating
|
||||
*/
|
||||
function setAllEmpty(ship, fillAll, type, rating = '') {
|
||||
ship.getModules(TYPES.ANY_INTERNAL, undefined, true).forEach((slot) => {
|
||||
if (slot.isEmpty() || fillAll) {
|
||||
try {
|
||||
// Maybe the item does not exist. Simply catch this error.
|
||||
slot.setItem(type, slot.getSize(), rating);
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal slot section
|
||||
*/
|
||||
export default class InternalSlotSection extends SlotSection {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context, 'internal', 'optional internal');
|
||||
this._empty = this._empty.bind(this);
|
||||
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);
|
||||
this._fillWithBusinessClassCabins = this._fillWithBusinessClassCabins.bind(this);
|
||||
this._fillWithEconomyClassCabins = this._fillWithEconomyClassCabins.bind(this);
|
||||
this.selectedRefId = null;
|
||||
this.firstRefId = 'emptyall';
|
||||
this.lastRefId = this.sectionRefArr['pcq'] ? 'pcq' : 'pcm';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle focus when component updates
|
||||
* @param {Object} prevProps React Component properties
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
this._handleSectionFocus(prevProps,this.firstRefId, this.lastRefId);
|
||||
constructor(props) {
|
||||
super(props, 'optional internal');
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all slots
|
||||
*/
|
||||
_empty() {
|
||||
this.selectedRefId = 'emptyall';
|
||||
this.props.ship.emptyInternal();
|
||||
this.props.onChange();
|
||||
this.props.ship.getModules(TYPES.ANY_INTERNAL).forEach((slot) => slot.reset());
|
||||
this._close();
|
||||
}
|
||||
|
||||
@@ -55,15 +49,8 @@ export default class InternalSlotSection extends SlotSection {
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithCargo(event) {
|
||||
this.selectedRefId = 'cargo';
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
ship.internal.forEach((slot) => {
|
||||
if ((clobber || !slot.m) && canMount(ship, slot, 'cr')) {
|
||||
ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E'));
|
||||
}
|
||||
});
|
||||
this.props.onChange();
|
||||
const fillAll = event.getModifierState('Alt');
|
||||
setAllEmpty(this.props.ship, fillAll, 'cargorack');
|
||||
this._close();
|
||||
}
|
||||
|
||||
@@ -72,15 +59,8 @@ export default class InternalSlotSection extends SlotSection {
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithFuelTanks(event) {
|
||||
this.selectedRefId = 'ft';
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
ship.internal.forEach((slot) => {
|
||||
if ((clobber || !slot.m) && canMount(ship, slot, 'ft')) {
|
||||
ship.use(slot, ModuleUtils.findInternal('ft', slot.maxClass, 'C'));
|
||||
}
|
||||
});
|
||||
this.props.onChange();
|
||||
const fillAll = event.getModifierState('Alt');
|
||||
setAllEmpty(this.props.ship, fillAll, 'fueltank', '3');
|
||||
this._close();
|
||||
}
|
||||
|
||||
@@ -89,15 +69,8 @@ export default class InternalSlotSection extends SlotSection {
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithLuxuryCabins(event) {
|
||||
this.selectedRefId = 'pcq';
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
ship.internal.forEach((slot) => {
|
||||
if ((clobber || !slot.m) && canMount(ship, slot, 'pcq')) {
|
||||
ship.use(slot, ModuleUtils.findInternal('pcq', Math.min(slot.maxClass, 6), 'B')); // Passenger cabins top out at 6
|
||||
}
|
||||
});
|
||||
this.props.onChange();
|
||||
const fillAll = event.getModifierState('Alt');
|
||||
setAllEmpty(this.props.ship, fillAll, 'passengercabins', '4');
|
||||
this._close();
|
||||
}
|
||||
|
||||
@@ -106,15 +79,8 @@ export default class InternalSlotSection extends SlotSection {
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithFirstClassCabins(event) {
|
||||
this.selectedRefId = 'pcm';
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
ship.internal.forEach((slot) => {
|
||||
if ((clobber || !slot.m) && canMount(ship, slot, 'pcm')) {
|
||||
ship.use(slot, ModuleUtils.findInternal('pcm', Math.min(slot.maxClass, 6), 'C')); // Passenger cabins top out at 6
|
||||
}
|
||||
});
|
||||
this.props.onChange();
|
||||
const fillAll = event.getModifierState('Alt');
|
||||
setAllEmpty(this.props.ship, fillAll, 'passengercabins', '3');
|
||||
this._close();
|
||||
}
|
||||
|
||||
@@ -123,15 +89,8 @@ export default class InternalSlotSection extends SlotSection {
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithBusinessClassCabins(event) {
|
||||
this.selectedRefId = 'pci';
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
ship.internal.forEach((slot) => {
|
||||
if ((clobber || !slot.m) && canMount(ship, slot, 'pci')) {
|
||||
ship.use(slot, ModuleUtils.findInternal('pci', Math.min(slot.maxClass, 6), 'D')); // Passenger cabins top out at 6
|
||||
}
|
||||
});
|
||||
this.props.onChange();
|
||||
const fillAll = event.getModifierState('Alt');
|
||||
setAllEmpty(this.props.ship, fillAll, 'passengercabins', '2');
|
||||
this._close();
|
||||
}
|
||||
|
||||
@@ -140,15 +99,8 @@ export default class InternalSlotSection extends SlotSection {
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithEconomyClassCabins(event) {
|
||||
this.selectedRefId = 'pce';
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
ship.internal.forEach((slot) => {
|
||||
if ((clobber || !slot.m) && canMount(ship, slot, 'pce')) {
|
||||
ship.use(slot, ModuleUtils.findInternal('pce', Math.min(slot.maxClass, 6), 'E')); // Passenger cabins top out at 6
|
||||
}
|
||||
});
|
||||
this.props.onChange();
|
||||
const fillAll = event.getModifierState('Alt');
|
||||
setAllEmpty(this.props.ship, fillAll, 'passengercabins', '1');
|
||||
this._close();
|
||||
}
|
||||
|
||||
@@ -157,18 +109,8 @@ export default class InternalSlotSection extends SlotSection {
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithCells(event) {
|
||||
this.selectedRefId = 'scb';
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
let chargeCap = 0; // Capacity of single activation
|
||||
ship.internal.forEach(function(slot) {
|
||||
if ((clobber && !(slot.m && ModuleUtils.isShieldGenerator(slot.m.grp)) || !slot.m) && canMount(ship, slot, '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;
|
||||
}
|
||||
});
|
||||
this.props.onChange();
|
||||
const fillAll = event.getModifierState('Alt');
|
||||
setAllEmpty(this.props.ship, fillAll, 'scb', '5');
|
||||
this._close();
|
||||
}
|
||||
|
||||
@@ -177,15 +119,8 @@ export default class InternalSlotSection extends SlotSection {
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithArmor(event) {
|
||||
this.selectedRefId = 'hr';
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
ship.internal.forEach((slot) => {
|
||||
if ((clobber || !slot.m) && canMount(ship, slot, 'hr')) {
|
||||
ship.use(slot, ModuleUtils.findInternal('hr', Math.min(slot.maxClass, 5), 'D')); // Hull reinforcements top out at 5D
|
||||
}
|
||||
});
|
||||
this.props.onChange();
|
||||
const fillAll = event.getModifierState('Alt');
|
||||
setAllEmpty(this.props.ship, fillAll, 'hrp', '2');
|
||||
this._close();
|
||||
}
|
||||
|
||||
@@ -194,56 +129,31 @@ export default class InternalSlotSection extends SlotSection {
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithModuleReinforcementPackages(event) {
|
||||
this.selectedRefId = 'mrp';
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
ship.internal.forEach((slot) => {
|
||||
if ((clobber || !slot.m) && canMount(ship, slot, 'mrp')) {
|
||||
ship.use(slot, ModuleUtils.findInternal('mrp', Math.min(slot.maxClass, 5), 'D')); // Module reinforcements top out at 5D
|
||||
}
|
||||
});
|
||||
this.props.onChange();
|
||||
const fillAll = event.getModifierState('Alt');
|
||||
setAllEmpty(this.props.ship, fillAll, 'mrp', '2');
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all on section header right click
|
||||
*/
|
||||
_contextMenu() {
|
||||
this._empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the slot React Components
|
||||
* @return {Array} Array of Slots
|
||||
*/
|
||||
_getSlots() {
|
||||
let slots = [];
|
||||
let { currentMenu, ship } = this.props;
|
||||
let { currentMenu, ship, propsToShow, onPropToggle } = this.props;
|
||||
let { originSlot, targetSlot } = this.state;
|
||||
let { internal, fuelCapacity } = ship;
|
||||
let availableModules = ship.getAvailableModules();
|
||||
|
||||
for (let i = 0, l = internal.length; i < l; i++) {
|
||||
let s = internal[i];
|
||||
|
||||
slots.push(<InternalSlot
|
||||
key={i}
|
||||
maxClass={s.maxClass}
|
||||
availableModules={() => availableModules.getInts(ship, s.maxClass, s.eligible)}
|
||||
onOpen={this._openMenu.bind(this,s)}
|
||||
onChange={this.props.onChange}
|
||||
onSelect={this._selectModule.bind(this, s)}
|
||||
selected={currentMenu == s}
|
||||
eligible={s.eligible}
|
||||
m={s.m}
|
||||
drag={this._drag.bind(this, s)}
|
||||
dragOver={this._dragOverSlot.bind(this, s)}
|
||||
for (const m of ship.getInternals(undefined, true)) {
|
||||
slots.push(<Slot
|
||||
key={m.object.Slot}
|
||||
currentMenu={currentMenu}
|
||||
m={m}
|
||||
drag={this._drag.bind(this, m)}
|
||||
dragOver={this._dragOverSlot.bind(this, m)}
|
||||
drop={this._drop}
|
||||
dropClass={this._dropClass(s, originSlot, targetSlot)}
|
||||
fuel={fuelCapacity}
|
||||
ship={ship}
|
||||
enabled={s.enabled ? true : false}
|
||||
dropClass={this._dropClass(m, originSlot, targetSlot)}
|
||||
propsToShow={propsToShow}
|
||||
onPropToggle={onPropToggle}
|
||||
/>);
|
||||
}
|
||||
|
||||
@@ -256,22 +166,23 @@ export default class InternalSlotSection extends SlotSection {
|
||||
* @param {Function} ship The ship
|
||||
* @return {React.Component} Section menu
|
||||
*/
|
||||
_getSectionMenu(translate, ship) {
|
||||
_getSectionMenu() {
|
||||
const { ship } = this.props;
|
||||
const { translate } = this.context.language;
|
||||
return <div className='select' onClick={e => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
|
||||
<ul>
|
||||
<li className='lc' tabIndex='0' onClick={this._empty} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['emptyall'] = smRef}>{translate('empty all')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithCargo} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['cargo'] = smRef}>{translate('cargo')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithCells} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['scb'] = smRef}>{translate('scb')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithArmor} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['hr'] = smRef}>{translate('hr')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithModuleReinforcementPackages} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['mrp'] = smRef}>{translate('mrp')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithFuelTanks} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ft'] = smRef}>{translate('ft')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithEconomyClassCabins} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pce'] = smRef}>{translate('pce')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithBusinessClassCabins} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pci'] = smRef}>{translate('pci')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithFirstClassCabins} onKeyDown={ship.luxuryCabins ? '' : this._keyDown} ref={smRef => this.sectionRefArr['pcm'] = smRef}>{translate('pcm')}</li>
|
||||
{ ship.luxuryCabins ? <li className='lc' tabIndex='0' onClick={this._fillWithLuxuryCabins} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pcq'] = smRef}>{translate('pcq')}</li> : ''}
|
||||
<li className='lc' tabIndex='0' onClick={this._empty}>{translate('empty all')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithCargo}>{translate('cargo')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithCells}>{translate('scb')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithArmor}>{translate('hr')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithModuleReinforcementPackages}>{translate('mrp')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithFuelTanks}>{translate('ft')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithEconomyClassCabins}>{translate('pce')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithBusinessClassCabins}>{translate('pci')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._fillWithFirstClassCabins} onKeyDown={ship.luxuryCabins ? '' : this._keyDown}>{translate('pcm')}</li>
|
||||
{ ship.readMeta('luxuryCabins') ? <li className='lc' tabIndex='0' onClick={this._fillWithLuxuryCabins}>{translate('pcq')}</li> : ''}
|
||||
<li className='optional-hide' style={{ textAlign: 'center', marginTop: '1em' }}>{translate('PHRASE_ALT_ALL')}</li>
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import LineChart from '../components/LineChart';
|
||||
import Slider from '../components/Slider';
|
||||
import * as Calc from '../shipyard/Calculations';
|
||||
|
||||
/**
|
||||
* Jump range for a given ship
|
||||
*/
|
||||
export default class JumpRange extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
ship: PropTypes.object.isRequired,
|
||||
code: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
const ship = this.props.ship;
|
||||
|
||||
this.state = {
|
||||
fuelLevel: 1,
|
||||
calcJumpRangeFunc: this._calcJumpRange.bind(this, ship)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the 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) {
|
||||
this.setState({ fuelLevel: 1,
|
||||
calcJumpRangeFunc: this._calcJumpRange.bind(this, nextProps.ship) });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the jump range this ship at a given cargo
|
||||
* @param {Object} ship The ship
|
||||
* @param {Object} cargo The cargo
|
||||
* @return {number} The jump range
|
||||
*/
|
||||
_calcJumpRange(ship, cargo) {
|
||||
// Obtain the FSD for this ship
|
||||
const fsd = ship.standard[2].m;
|
||||
|
||||
const fuel = this.state.fuelLevel * ship.fuelCapacity;
|
||||
|
||||
// Obtain the jump range
|
||||
return Calc.jumpRange(ship.unladenMass + fuel + cargo, fsd, fuel, ship);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update fuel level
|
||||
* @param {number} fuelLevel Fuel level 0 - 1
|
||||
*/
|
||||
_fuelChange(fuelLevel) {
|
||||
this.setState({
|
||||
fuelLevel,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render engine profile
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { ship } = this.props;
|
||||
const { fuelLevel } = this.state;
|
||||
|
||||
const code = ship.toString() + '.' + ship.getModificationsString() + '.' + fuelLevel;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<h1>{translate('jump range')}</h1>
|
||||
<LineChart
|
||||
xMax={ship.cargoCapacity}
|
||||
yMax={ship.unladenRange}
|
||||
xLabel={translate('cargo')}
|
||||
xUnit={translate('T')}
|
||||
yLabel={translate('jump range')}
|
||||
yUnit={translate('LY')}
|
||||
func={this.state.calcJumpRangeFunc}
|
||||
points={200}
|
||||
code={code}
|
||||
/>
|
||||
<h3>{translate('fuel carried')}: {formats.f2(fuelLevel * ship.fuelCapacity)}{units.T}</h3>
|
||||
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
|
||||
<tbody >
|
||||
<tr>
|
||||
<td>
|
||||
<Slider
|
||||
axis={true}
|
||||
onChange={this._fuelChange.bind(this)}
|
||||
axisUnit={translate('T')}
|
||||
percent={fuelLevel}
|
||||
max={ship.fuelCapacity}
|
||||
scale={sizeRatio}
|
||||
onResize={onWindowResize}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import ContainerDimensions from 'react-container-dimensions';
|
||||
import * as d3 from 'd3';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import autoBind from 'auto-bind';
|
||||
|
||||
const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 };
|
||||
|
||||
@@ -10,7 +11,6 @@ const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 };
|
||||
* Line Chart
|
||||
*/
|
||||
export default class LineChart extends TranslatedComponent {
|
||||
|
||||
static defaultProps = {
|
||||
code: '',
|
||||
xMin: 0,
|
||||
@@ -45,13 +45,7 @@ export default class LineChart extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
this._updateDimensions = this._updateDimensions.bind(this);
|
||||
this._updateSeries = this._updateSeries.bind(this);
|
||||
this._tooltip = this._tooltip.bind(this);
|
||||
this._showTip = this._showTip.bind(this);
|
||||
this._hideTip = this._hideTip.bind(this);
|
||||
this._moveTip = this._moveTip.bind(this);
|
||||
autoBind(this);
|
||||
|
||||
const series = props.series;
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { shallowEqual } from '../utils/UtilityFunctions';
|
||||
* Link wrapper component
|
||||
*/
|
||||
export default class Link extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
href: PropTypes.string.isRequired,
|
||||
@@ -56,5 +55,4 @@ export default class Link extends React.Component {
|
||||
render() {
|
||||
return <a {...this.props} onClick={this.handler}>{this.props.children}</a>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ function buildComparator(a, b) {
|
||||
* Compare builds modal
|
||||
*/
|
||||
export default class ModalCompare extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
builds: PropTypes.array
|
||||
@@ -105,8 +104,8 @@ export default class ModalCompare extends TranslatedComponent {
|
||||
|
||||
let selectedBuilds = usedBuilds.map((build, i) =>
|
||||
<tr key={i} onClick={this._removeBuild.bind(this, i)}>
|
||||
<td className='tl'>{build.name}</td><
|
||||
td className='tl'>{build.buildName}</td>
|
||||
<td className='tl'>{build.name}</td>
|
||||
<td className='tl'>{build.buildName}</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import TranslatedComponent from './TranslatedComponent';
|
||||
* Export Modal
|
||||
*/
|
||||
export default class ModalExport extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
generator: PropTypes.func,
|
||||
|
||||
@@ -7,19 +7,10 @@ import TranslatedComponent from './TranslatedComponent';
|
||||
* Help Modal
|
||||
*/
|
||||
export default class ModalHelp extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modal
|
||||
* @return {React.Component} Modal Content
|
||||
|
||||
@@ -2,92 +2,21 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import Router from '../Router';
|
||||
import Persist from '../stores/Persist';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import { ModuleNameToGroup, Insurance } from '../shipyard/Constants';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import { fromDetailedBuild } from '../shipyard/Serializer';
|
||||
import { Download } from './SvgIcons';
|
||||
import { outfitURL } from '../utils/UrlGenerators';
|
||||
import { shipFromJson, shipModelFromJson } from '../utils/CompanionApiUtils';
|
||||
import { shipFromLoadoutJSON } from '../utils/JournalUtils';
|
||||
import autoBind from 'auto-bind';
|
||||
import { isArray } from 'lodash';
|
||||
import { Ship } from 'ed-forge';
|
||||
|
||||
const zlib = require('pako');
|
||||
|
||||
const textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n');
|
||||
const lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)');
|
||||
const mountMap = { 'H': 4, 'L': 3, 'M': 2, 'S': 1, 'U': 0 };
|
||||
const standardMap = { 'RB': 0, 'TM': 1, 'FH': 2, 'EC': 3, 'PC': 4, 'SS': 5, 'FS': 6 };
|
||||
const bhMap = { 'lightweight alloy': 0, 'reinforced alloy': 1, 'military grade composite': 2, 'mirrored surface composite': 3, 'reactive surface composite': 4 };
|
||||
|
||||
/**
|
||||
* Check is slot is empty
|
||||
* @param {Object} slot Slot model
|
||||
* @return {Boolean} True if empty
|
||||
*/
|
||||
function isEmptySlot(slot) {
|
||||
return slot.maxClass == this && slot.m === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a build is valid
|
||||
* @param {string} shipId Ship ID
|
||||
* @param {string} code Serialzied ship build 'code'
|
||||
* @param {string} name Build name
|
||||
* @throws {string} If build is not valid
|
||||
*/
|
||||
function validateBuild(shipId, code, name) {
|
||||
let shipData = Ships[shipId];
|
||||
|
||||
if (!shipData) {
|
||||
throw '"' + shipId + '" is not a valid Ship Id!';
|
||||
}
|
||||
if (typeof name != 'string' || name.length == 0) {
|
||||
throw shipData.properties.name + ' build "' + name + '" must be a string at least 1 character long!';
|
||||
}
|
||||
if (typeof code != 'string' || code.length < 10) {
|
||||
throw shipData.properties.name + ' build "' + name + '" is not valid!';
|
||||
}
|
||||
try {
|
||||
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||
ship.buildFrom(code);
|
||||
} catch (e) {
|
||||
throw shipData.properties.name + ' build "' + name + '" is not valid!';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a ship-loadout JSON object to a Coriolis build
|
||||
* @param {Object} detailedBuild ship-loadout
|
||||
* @return {Object} Coriolis build
|
||||
*/
|
||||
function detailedJsonToBuild(detailedBuild) {
|
||||
let ship;
|
||||
if (!detailedBuild.name) {
|
||||
throw 'Build Name missing!';
|
||||
}
|
||||
|
||||
if (!detailedBuild.name.trim()) {
|
||||
throw 'Build Name must be a string at least 1 character long!';
|
||||
}
|
||||
|
||||
try {
|
||||
ship = fromDetailedBuild(detailedBuild);
|
||||
} catch (e) {
|
||||
throw detailedBuild.ship + ' Build "' + detailedBuild.name + '": Invalid data';
|
||||
}
|
||||
|
||||
return { shipId: ship.id, name: detailedBuild.name, code: ship.toString() };
|
||||
}
|
||||
const STATE = {
|
||||
READY: 0,
|
||||
PARSED: 1,
|
||||
ERROR: 2,
|
||||
};
|
||||
|
||||
/**
|
||||
* Import Modal
|
||||
*/
|
||||
export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
|
||||
static propTypes = {
|
||||
importString: PropTypes.string, // Optional: Default data for import modal
|
||||
builds: PropTypes.object, // Optional: Import object
|
||||
@@ -99,228 +28,12 @@ export default class ModalImport extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
autoBind(this);
|
||||
|
||||
this.state = {
|
||||
builds: props.builds,
|
||||
canEdit: !props.builds,
|
||||
loadoutEvent: null,
|
||||
comparisons: null,
|
||||
shipDiscount: null,
|
||||
moduleDiscount: null,
|
||||
errorMsg: null,
|
||||
importString: props.importString || null,
|
||||
importValid: false,
|
||||
insurance: null
|
||||
status: STATE.READY,
|
||||
builds: props.builds || [],
|
||||
};
|
||||
|
||||
this._process = this._process.bind(this);
|
||||
this._import = this._import.bind(this);
|
||||
this._importBackup = this._importBackup.bind(this);
|
||||
this._importLoadout = this._importLoadout.bind(this);
|
||||
this._importDetailedArray = this._importDetailedArray.bind(this);
|
||||
this._importTextBuild = this._importTextBuild.bind(this);
|
||||
this._importCompanionApiBuild = this._importCompanionApiBuild.bind(this);
|
||||
this._validateImport = this._validateImport.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a Loadout event from Elite: Dangerous journal files
|
||||
* @param {Object} data Loadout event
|
||||
* @throws {string} If import fails
|
||||
*/
|
||||
_importLoadout(data) {
|
||||
if (data && data.Ship && data.Modules) {
|
||||
const deflated = zlib.deflate(JSON.stringify(data), { to: 'string' });
|
||||
let compressed = btoa(deflated);
|
||||
this.setState({loadoutEvent: compressed});
|
||||
} else {
|
||||
throw 'Loadout event must contain Ship and Modules';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a Coriolis backup
|
||||
* @param {Object} importData Backup Data
|
||||
* @throws {string} If import fails
|
||||
*/
|
||||
_importBackup(importData) {
|
||||
if (importData.builds && typeof importData.builds == 'object') {
|
||||
for (let shipId in importData.builds) {
|
||||
for (let buildName in importData.builds[shipId]) {
|
||||
try {
|
||||
validateBuild(shipId, importData.builds[shipId][buildName], buildName);
|
||||
} catch (err) {
|
||||
delete importData.builds[shipId][buildName];
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState({ builds: importData.builds });
|
||||
} else {
|
||||
throw 'builds must be an object!';
|
||||
}
|
||||
if (importData.comparisons) {
|
||||
for (let compName in importData.comparisons) {
|
||||
let comparison = importData.comparisons[compName];
|
||||
for (let i = 0, l = comparison.builds.length; i < l; i++) {
|
||||
let build = comparison.builds[i];
|
||||
if (!importData.builds[build.shipId] || !importData.builds[build.shipId][build.buildName]) {
|
||||
throw build.shipId + ' build "' + build.buildName + '" data is missing!';
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState({ comparisons: importData.comparisons });
|
||||
}
|
||||
// Check for old/deprecated discounts
|
||||
if (importData.discounts instanceof Array && importData.discounts.length == 2) {
|
||||
this.setState({ shipDiscount: importData.discounts[0], moduleDiscount: importData.discounts[1] });
|
||||
}
|
||||
// Check for ship discount
|
||||
if (!isNaN(importData.shipDiscount)) {
|
||||
this.setState({ shipDiscount: importData.shipDiscount * 1 });
|
||||
}
|
||||
// Check for module discount
|
||||
if (!isNaN(importData.moduleDiscount)) {
|
||||
this.setState({ moduleDiscount: importData.moduleDiscount * 1 });
|
||||
}
|
||||
|
||||
if (typeof importData.insurance == 'string') {
|
||||
let insurance = importData.insurance.toLowerCase();
|
||||
|
||||
if (Insurance[insurance] !== undefined) {
|
||||
this.setState({ insurance });
|
||||
} else {
|
||||
throw 'Invalid insurance type: ' + insurance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import an array of ship-loadout objects / builds
|
||||
* @param {Array} importArr Array of ship-loadout JSON Schema builds
|
||||
*/
|
||||
_importDetailedArray(importArr) {
|
||||
let builds = {};
|
||||
for (let i = 0, l = importArr.length; i < l; i++) {
|
||||
let build = detailedJsonToBuild(importArr[i]);
|
||||
if (!builds[build.shipId]) {
|
||||
builds[build.shipId] = {};
|
||||
}
|
||||
builds[build.shipId][build.name] = build.code;
|
||||
}
|
||||
this.setState({ builds });
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a build direct from the companion API
|
||||
* @param {string} build JSON from the companion API information
|
||||
* @throws {string} if parse/import fails
|
||||
*/
|
||||
_importCompanionApiBuild(build) {
|
||||
const shipModel = shipModelFromJson(build);
|
||||
const ship = shipFromJson(build);
|
||||
|
||||
let builds = {};
|
||||
builds[shipModel] = {};
|
||||
builds[shipModel]['Imported ' + Ships[shipModel].properties.name] = ship.toString();
|
||||
this.setState({ builds, singleBuild: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a text build from ED Shipyard
|
||||
* @param {string} buildStr Build string
|
||||
* @throws {string} If parse / import fails
|
||||
*/
|
||||
_importTextBuild(buildStr) {
|
||||
let buildName = textBuildRegex.exec(buildStr)[1].trim();
|
||||
let shipName = buildName.toLowerCase();
|
||||
let shipId = null;
|
||||
|
||||
for (let sId in Ships) {
|
||||
if (Ships[sId].properties.name.toLowerCase() == shipName) {
|
||||
shipId = sId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shipId) {
|
||||
throw 'No such ship found: "' + buildName + '"';
|
||||
}
|
||||
|
||||
let lines = buildStr.split('\n');
|
||||
let ship = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots);
|
||||
ship.buildWith(null);
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
let line = lines[i].trim();
|
||||
|
||||
if (!line) { continue; }
|
||||
if (line.substring(0, 3) == '---') { break; }
|
||||
|
||||
let parts = lineRegex.exec(line);
|
||||
|
||||
if (!parts) { throw 'Error parsing: "' + line + '"'; }
|
||||
|
||||
let typeSize = parts[1];
|
||||
let cl = parts[2];
|
||||
let rating = parts[3];
|
||||
let mount = parts[4];
|
||||
let missile = parts[5];
|
||||
let name = parts[6].trim();
|
||||
let slot, group;
|
||||
|
||||
if (isNaN(typeSize)) { // Standard or Hardpoint
|
||||
if (typeSize.length == 1) { // Hardpoint
|
||||
let slotClass = mountMap[typeSize];
|
||||
|
||||
if (cl > slotClass) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; }
|
||||
|
||||
slot = ship.hardpoints.find(isEmptySlot, slotClass);
|
||||
|
||||
if (!slot) { throw 'No hardpoint slot available for: "' + line + '"'; }
|
||||
|
||||
group = ModuleNameToGroup[name.toLowerCase()];
|
||||
|
||||
let hp = ModuleUtils.findHardpoint(group, cl, rating, group ? null : name, mount, missile);
|
||||
|
||||
if (!hp) { throw 'Unknown component: "' + line + '"'; }
|
||||
|
||||
ship.use(slot, hp, true);
|
||||
} else if (typeSize == 'BH') {
|
||||
let bhId = bhMap[name.toLowerCase()];
|
||||
|
||||
if (bhId === undefined) { throw 'Unknown bulkhead: "' + line + '"'; }
|
||||
|
||||
ship.useBulkhead(bhId, true);
|
||||
} else if (standardMap[typeSize] != undefined) {
|
||||
let standardIndex = standardMap[typeSize];
|
||||
|
||||
if (ship.standard[standardIndex].maxClass < cl) { throw name + ' exceeds max class for the ' + ship.name; }
|
||||
|
||||
ship.use(ship.standard[standardIndex], ModuleUtils.standard(standardIndex, cl + rating), true);
|
||||
} else {
|
||||
throw 'Unknown component: "' + line + '"';
|
||||
}
|
||||
} else {
|
||||
if (cl > typeSize) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; }
|
||||
|
||||
slot = ship.internal.find(isEmptySlot, typeSize);
|
||||
|
||||
if (!slot) { throw 'No internal slot available for: "' + line + '"'; }
|
||||
|
||||
group = ModuleNameToGroup[name.toLowerCase()];
|
||||
|
||||
let intComp = ModuleUtils.findInternal(group, cl, rating, group ? null : name);
|
||||
|
||||
if (!intComp) { throw 'Unknown component: "' + line + '"'; }
|
||||
|
||||
ship.use(slot, intComp);
|
||||
}
|
||||
}
|
||||
|
||||
let builds = {};
|
||||
builds[shipId] = {};
|
||||
builds[shipId]['Imported ' + buildName] = ship.toString();
|
||||
this.setState({ builds, singleBuild: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -352,166 +65,50 @@ export default class ModalImport extends TranslatedComponent {
|
||||
* @param {SyntheticEvent} event Event
|
||||
* @throws {string} If validation fails
|
||||
*/
|
||||
_validateImport(event) {
|
||||
let importData = null;
|
||||
let importString = event.target.value.trim();
|
||||
this.setState({
|
||||
builds: null,
|
||||
comparisons: null,
|
||||
shipDiscount: null,
|
||||
moduleDiscount: null,
|
||||
errorMsg: null,
|
||||
importValid: false,
|
||||
insurance: null,
|
||||
singleBuild: false,
|
||||
importString,
|
||||
});
|
||||
|
||||
if (!importString) {
|
||||
return;
|
||||
}
|
||||
|
||||
_parse(event) {
|
||||
const importString = event.target.value.trim();
|
||||
try {
|
||||
if (textBuildRegex.test(importString)) { // E:D Shipyard build text
|
||||
this._importTextBuild(importString);
|
||||
} else { // JSON Build data
|
||||
importData = JSON.parse(importString);
|
||||
|
||||
if (!importData || typeof importData != 'object') {
|
||||
throw 'Must be an object or array!';
|
||||
let data = JSON.parse(importString);
|
||||
if (!isArray(data)) {
|
||||
data = [data];
|
||||
}
|
||||
|
||||
if (importData?.[0]?.header?.appName) { // has SLEF envelope?
|
||||
this._importSlefBuilds(importData);
|
||||
} else if (importData.modules != null && importData.modules.Armour != null) { // Only the companion API has this information
|
||||
this._importCompanionApiBuild(importData); // Single ship definition
|
||||
} else if (importData.ship != null && importData.ship.modules != null && importData.ship.modules.Armour != null) { // Only the companion API has this information
|
||||
this._importCompanionApiBuild(importData.ship); // Complete API dump
|
||||
} else if (importData instanceof Array) { // Must be detailed export json
|
||||
this._importDetailedArray(importData);
|
||||
} else if (importData.ship && typeof importData.name !== undefined) { // Using JSON from a single ship build export
|
||||
this._importDetailedArray([importData]); // Convert to array with singleobject
|
||||
this.setState({ singleBuild: true });
|
||||
} else if (importData.Modules != null && importData.Modules[0] != null) {
|
||||
this._importLoadout(importData);
|
||||
} else { // Using Backup JSON
|
||||
this._importBackup(importData);
|
||||
const ships = data.map((item) => {
|
||||
try {
|
||||
return new Ship(item.data ? item.data : item);
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
});
|
||||
this.setState({ ships, status: STATE.PARSED });
|
||||
} catch (err) {
|
||||
this.setState({ err, status: STATE.ERROR });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
this.setState({ errorMsg: (typeof e == 'string') ? e : 'Cannot Parse the data!' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ importValid: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Process imported data
|
||||
*/
|
||||
_process() {
|
||||
let builds = null, comparisons = null;
|
||||
|
||||
if (this.state.loadoutEvent) {
|
||||
return Router.go(`/import?data=${this.state.loadoutEvent}`);
|
||||
}
|
||||
|
||||
// If only importing a single build go straight to the outfitting page
|
||||
if (this.state.singleBuild) {
|
||||
builds = this.state.builds;
|
||||
let shipId = Object.keys(builds)[0];
|
||||
let name = Object.keys(builds[shipId])[0];
|
||||
Router.go(outfitURL(shipId, builds[shipId][name], name));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (this.state.builds) {
|
||||
builds = {}; // Create new builds object such that orginal name retained, but can be renamed
|
||||
for (let shipId in this.state.builds) {
|
||||
let shipbuilds = this.state.builds[shipId];
|
||||
builds[shipId] = {};
|
||||
for (let buildName in shipbuilds) {
|
||||
builds[shipId][buildName] = {
|
||||
code: shipbuilds[buildName],
|
||||
useName: buildName
|
||||
};
|
||||
for (const build of this.state.builds) {
|
||||
if (!build instanceof Error) {
|
||||
Persist.saveBuild(build.Ship, build.CoriolisBuildName || build.ShipName, build.compress());
|
||||
}
|
||||
}
|
||||
this.setState({ builds: [], status: STATE.READY });
|
||||
}
|
||||
|
||||
if (this.state.comparisons) {
|
||||
comparisons = {};
|
||||
for (let name in this.state.comparisons) {
|
||||
comparisons[name] = Object.assign({ useName: name }, this.state.comparisons[name]);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ processed: true, builds, comparisons });
|
||||
};
|
||||
|
||||
/**
|
||||
* Import parsed, processed data and save
|
||||
*/
|
||||
_import() {
|
||||
let state = this.state;
|
||||
if (state.builds) {
|
||||
let builds = state.builds;
|
||||
for (let shipId in builds) {
|
||||
for (let buildName in builds[shipId]) {
|
||||
let build = builds[shipId][buildName];
|
||||
let name = build.useName.trim();
|
||||
if (name) {
|
||||
Persist.saveBuild(shipId, name, build.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.comparisons) {
|
||||
let comparisons = state.comparisons;
|
||||
for (let comp in comparisons) {
|
||||
let comparison = comparisons[comp];
|
||||
let useName = comparison.useName.trim();
|
||||
if (useName) {
|
||||
Persist.saveComparison(useName, comparison.builds, comparison.facets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.shipDiscount !== undefined) {
|
||||
Persist.setShipDiscount(state.shipDiscount);
|
||||
}
|
||||
if (state.moduleDiscount !== undefined) {
|
||||
Persist.setModuleDiscount(state.moduleDiscount);
|
||||
}
|
||||
|
||||
if (state.insurance) {
|
||||
Persist.setInsurance(state.insurance);
|
||||
}
|
||||
|
||||
this.context.hideModal();
|
||||
};
|
||||
|
||||
/**
|
||||
* Capture build name changes
|
||||
* @param {Object} item Build/Comparison import object
|
||||
* @param {SyntheticEvent} e Event
|
||||
* @param {Object} index Build/Comparison import object
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_changeName(item, e) {
|
||||
item.useName = e.target.value;
|
||||
this.forceUpdate();
|
||||
_changeName(index, event) {
|
||||
const { builds } = this.state;
|
||||
builds[index].CoriolisBuildName = event.target.value.trim();
|
||||
this.setState({ builds });
|
||||
}
|
||||
|
||||
/**
|
||||
* If imported data is already provided process immediately on mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
if (this.props.builds) {
|
||||
this._process();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* If textarea is shown focus on mount
|
||||
*/
|
||||
@@ -526,77 +123,34 @@ export default class ModalImport extends TranslatedComponent {
|
||||
* @return {React.Component} Modal contents
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let state = this.state;
|
||||
let importStage;
|
||||
const { translate } = this.context.language;
|
||||
const { status, builds, err } = this.state;
|
||||
|
||||
if (!state.processed) {
|
||||
importStage = (
|
||||
const buildRows = builds.map((build, i) => {
|
||||
if (build instanceof Error) {
|
||||
return <tr key={i} className='cb'>
|
||||
<td colSpan={3} className='warning'>Error: {build.name}</td>
|
||||
</tr>;
|
||||
}
|
||||
|
||||
const exists = Persist.hasBuild(build.Ship, build.CoriolisBuildName);
|
||||
const saveName = build.CoriolisBuildName || build.ShipName;
|
||||
return <tr key={i} className='cb'>
|
||||
<td>{translate(build.Ship)}</td>
|
||||
<td><input type='text' onChange={this._changeName.bind(this, i)} value={saveName}/></td>
|
||||
<td style={{ textAlign: 'center' }} className={cn('cap', { warning: exists, disabled: saveName === '' })}>
|
||||
{translate(saveName === '' ? 'skip' : (exists ? 'overwrite' : 'create'))}
|
||||
</td>
|
||||
</tr>;
|
||||
});
|
||||
|
||||
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
||||
<h2 >{translate('import')}</h2>
|
||||
<div>
|
||||
<textarea spellCheck={false} className='cb json' ref={node => this.importField = node} onChange={this._validateImport} defaultValue={this.state.importString} placeholder={translate('PHRASE_IMPORT')} />
|
||||
<button id='proceed' className='l cap' onClick={this._process} disabled={!state.importValid} >{translate('proceed')}</button>
|
||||
<div className='l warning' style={{ marginLeft:'3em' }}>{state.errorMsg}</div>
|
||||
<textarea spellCheck={false} className='cb json' ref={node => this.importField = node} onChange={this._parse} defaultValue={this.state.importString} placeholder={translate('PHRASE_IMPORT')} />
|
||||
{status === STATE.ERROR && <div className='l warning' style={{ marginLeft:'3em' }}>{err.toString()}</div>}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
let comparisonTable, edit, buildRows = [];
|
||||
if (state.comparisons) {
|
||||
let comparisonRows = [];
|
||||
|
||||
for (let name in state.comparisons) {
|
||||
let comparison = state.comparisons[name];
|
||||
let hasComparison = Persist.hasComparison(comparison.useName);
|
||||
comparisonRows.push(
|
||||
<tr key={name} className='cb'>
|
||||
<td>
|
||||
<input type='text' onChange={this._changeName.bind(this, comparison)} value={comparison.useName}/>
|
||||
</td>
|
||||
<td style={{ textAlign:'center' }} className={ cn('cap', { warning: hasComparison, disabled: comparison.useName == '' }) }>
|
||||
{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
comparisonTable = (
|
||||
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%' }} >
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign:'left' }}>{translate('comparison')}</th>
|
||||
<th>{translate('action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{comparisonRows}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
if(this.state.canEdit) {
|
||||
edit = <button className='l cap' style={{ marginLeft: '2em' }} onClick={() => this.setState({ processed: false })}>{translate('edit data')}</button>;
|
||||
}
|
||||
|
||||
let builds = this.state.builds;
|
||||
for (let shipId in builds) {
|
||||
let shipBuilds = builds[shipId];
|
||||
for (let buildName in shipBuilds) {
|
||||
let b = shipBuilds[buildName];
|
||||
let hasBuild = Persist.hasBuild(shipId, b.useName);
|
||||
buildRows.push(
|
||||
<tr key={shipId + buildName} className='cb'>
|
||||
<td>{Ships[shipId].properties.name}</td>
|
||||
<td><input type='text' onChange={this._changeName.bind(this, b)} value={b.useName}/></td>
|
||||
<td style={{ textAlign: 'center' }} className={cn('cap', { warning: hasBuild, disabled: b.useName == '' })}>
|
||||
{translate(b.useName == '' ? 'skip' : (hasBuild ? 'overwrite' : 'create'))}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
importStage = (
|
||||
<div>
|
||||
{builds.length && <div>
|
||||
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -609,17 +163,14 @@ export default class ModalImport extends TranslatedComponent {
|
||||
{buildRows}
|
||||
</tbody>
|
||||
</table>
|
||||
{comparisonTable}
|
||||
<button id='import' className='cl l' onClick={this._import}><Download/> {translate('import')}</button>
|
||||
{edit}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
||||
<h2 >{translate('import')}</h2>
|
||||
{importStage}
|
||||
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
|
||||
</div>}
|
||||
<button id='proceed' className='l cap' onClick={this._process}
|
||||
disabled={status !== STATE.PARSED} >
|
||||
{translate('proceed')}
|
||||
</button>
|
||||
<button className={'r dismiss cap'} onClick={this.context.hideModal}>
|
||||
{translate('close')}
|
||||
</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import ShortenUrl from '../utils/ShortenUrl';
|
||||
* Permalink modal
|
||||
*/
|
||||
export default class ModalPermalink extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
url: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import request from 'superagent';
|
||||
import Persist from '../stores/Persist';
|
||||
|
||||
/**
|
||||
* Permalink modal
|
||||
*/
|
||||
export default class ModalShoppingList extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
ship: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
matsList: '',
|
||||
mats: {},
|
||||
failed: false,
|
||||
cmdrName: Persist.getCmdr().selected,
|
||||
cmdrs: Persist.getCmdr().cmdrs,
|
||||
matsPerGrade: Persist.getRolls(),
|
||||
blueprints: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* React component did mount
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.renderMats();
|
||||
if (this.checkBrowserIsCompatible()) {
|
||||
this.getCommanders();
|
||||
this.registerBPs();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all blueprints needed to make a build.
|
||||
*/
|
||||
registerBPs() {
|
||||
const ship = this.props.ship;
|
||||
let blueprints = [];
|
||||
for (const module of ship.costList) {
|
||||
if (module.type === 'SHIP') {
|
||||
continue;
|
||||
}
|
||||
if (module.m && module.m.blueprint) {
|
||||
if (!module.m.blueprint.grade || !module.m.blueprint.grades) {
|
||||
continue;
|
||||
}
|
||||
if (module.m.blueprint.special) {
|
||||
console.log(module.m.blueprint.special);
|
||||
blueprints.push({ uuid: module.m.blueprint.special.uuid, number: 1 });
|
||||
}
|
||||
for (const g in module.m.blueprint.grades) {
|
||||
if (!module.m.blueprint.grades.hasOwnProperty(g)) {
|
||||
continue;
|
||||
}
|
||||
if (g > module.m.blueprint.grade) {
|
||||
continue;
|
||||
}
|
||||
blueprints.push({ uuid: module.m.blueprint.grades[g].uuid, number: this.state.matsPerGrade[g] });
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState({ blueprints });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check browser isn't firefox.
|
||||
* @return {boolean} true if compatible, false if not.
|
||||
*/
|
||||
checkBrowserIsCompatible() {
|
||||
// Firefox 1.0+
|
||||
return typeof InstallTrigger === 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of commanders from EDEngineer.
|
||||
*/
|
||||
getCommanders() {
|
||||
request
|
||||
.get('http://localhost:44405/commanders')
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return this.setState({ failed: true });
|
||||
}
|
||||
const cmdrs = JSON.parse(res.text);
|
||||
if (!this.state.cmdrName) {
|
||||
this.setState({ cmdrName: cmdrs[0] });
|
||||
}
|
||||
this.setState({ cmdrs }, () => {
|
||||
Persist.setCmdr({ selected: this.state.cmdrName, cmdrs });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send all blueprints to ED Engineer
|
||||
* @param {Event} event React event
|
||||
*/
|
||||
sendToEDEng(event) {
|
||||
event.preventDefault();
|
||||
let translate = this.context.language.translate;
|
||||
const target = event.target;
|
||||
target.disabled = this.state.blueprints.length > 0;
|
||||
if (this.state.blueprints.length === 0) {
|
||||
target.innerText = translate('No modded components.');
|
||||
target.disabled = true;
|
||||
setTimeout(() => {
|
||||
target.innerText = translate('Send to EDEngineer');
|
||||
target.disabled = false;
|
||||
}, 3000);
|
||||
} else {
|
||||
target.innerText = translate('Sending...');
|
||||
}
|
||||
let countSent = 0;
|
||||
let countTotal = this.state.blueprints.length;
|
||||
|
||||
for (const i of this.state.blueprints) {
|
||||
request
|
||||
.patch(`http://localhost:44405/${this.state.cmdrName}/shopping-list`)
|
||||
.field('uuid', i.uuid)
|
||||
.field('size', i.number)
|
||||
.end(err => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
if (err.message !== 'Bad Request') {
|
||||
this.setState({ failed: true });
|
||||
}
|
||||
}
|
||||
countSent++;
|
||||
if (countSent === countTotal) {
|
||||
target.disabled = false;
|
||||
target.innerText = translate('Send to EDEngineer');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert mats object to string
|
||||
*/
|
||||
renderMats() {
|
||||
const ship = this.props.ship;
|
||||
let mats = {};
|
||||
for (const module of ship.costList) {
|
||||
if (module.type === 'SHIP') {
|
||||
continue;
|
||||
}
|
||||
if (module.m && module.m.blueprint) {
|
||||
if (!module.m.blueprint.grade || !module.m.blueprint.grades) {
|
||||
continue;
|
||||
}
|
||||
for (const g in module.m.blueprint.grades) {
|
||||
if (!module.m.blueprint.grades.hasOwnProperty(g)) {
|
||||
continue;
|
||||
}
|
||||
if (g > module.m.blueprint.grade) {
|
||||
continue;
|
||||
}
|
||||
for (const i in module.m.blueprint.grades[g].components) {
|
||||
if (!module.m.blueprint.grades[g].components.hasOwnProperty(i)) {
|
||||
continue;
|
||||
}
|
||||
if (mats[i]) {
|
||||
mats[i] += module.m.blueprint.grades[g].components[i] * this.state.matsPerGrade[g];
|
||||
} else {
|
||||
mats[i] = module.m.blueprint.grades[g].components[i] * this.state.matsPerGrade[g];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let matsString = '';
|
||||
for (const i in mats) {
|
||||
if (!mats.hasOwnProperty(i)) {
|
||||
continue;
|
||||
}
|
||||
if (mats[i] === 0) {
|
||||
delete mats[i];
|
||||
continue;
|
||||
}
|
||||
matsString += `${i}: ${mats[i]}\n`;
|
||||
}
|
||||
this.setState({ matsList: matsString, mats });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for changing roll amounts
|
||||
* @param {SyntheticEvent} e React Event
|
||||
*/
|
||||
changeHandler(e) {
|
||||
let grade = e.target.id;
|
||||
let newState = this.state.matsPerGrade;
|
||||
newState[grade] = parseInt(e.target.value);
|
||||
this.setState({ matsPerGrade: newState });
|
||||
Persist.setRolls(newState);
|
||||
this.renderMats();
|
||||
this.registerBPs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for changing cmdr name
|
||||
* @param {SyntheticEvent} e React Event
|
||||
*/
|
||||
cmdrChangeHandler(e) {
|
||||
let cmdrName = e.target.value;
|
||||
this.setState({ cmdrName }, () => {
|
||||
Persist.setCmdr({ selected: this.state.cmdrName, cmdrs: this.state.cmdrs });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modal
|
||||
* @return {React.Component} Modal Content
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
this.changeHandler = this.changeHandler.bind(this);
|
||||
const compatible = this.checkBrowserIsCompatible();
|
||||
this.cmdrChangeHandler = this.cmdrChangeHandler.bind(this);
|
||||
this.sendToEDEng = this.sendToEDEng.bind(this);
|
||||
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
||||
<h2>{translate('PHRASE_SHOPPING_MATS')}</h2>
|
||||
<label>{translate('Grade 1 rolls ')}</label>
|
||||
<input id={1} type={'number'} min={0} defaultValue={this.state.matsPerGrade[1]} onChange={this.changeHandler} />
|
||||
<br/>
|
||||
<label>{translate('Grade 2 rolls ')}</label>
|
||||
<input id={2} type={'number'} min={0} defaultValue={this.state.matsPerGrade[2]} onChange={this.changeHandler} />
|
||||
<br/>
|
||||
<label>{translate('Grade 3 rolls ')}</label>
|
||||
<input id={3} type={'number'} min={0} value={this.state.matsPerGrade[3]} onChange={this.changeHandler} />
|
||||
<br/>
|
||||
<label>{translate('Grade 4 rolls ')}</label>
|
||||
<input id={4} type={'number'} min={0} value={this.state.matsPerGrade[4]} onChange={this.changeHandler} />
|
||||
<br/>
|
||||
<label>{translate('Grade 5 rolls ')}</label>
|
||||
<input id={5} type={'number'} min={0} value={this.state.matsPerGrade[5]} onChange={this.changeHandler} />
|
||||
<div>
|
||||
<textarea className='cb json' readOnly value={this.state.matsList} />
|
||||
</div>
|
||||
<label hidden={!compatible} className={'l cap'}>{translate('CMDR Name')}</label>
|
||||
<br/>
|
||||
<select hidden={!compatible} className={'cmdr-select l cap'} onChange={this.cmdrChangeHandler} defaultValue={this.state.cmdrName}>
|
||||
{this.state.cmdrs.map(e => <option key={e}>{e}</option>)}
|
||||
</select>
|
||||
<br/>
|
||||
<p hidden={!this.state.failed} id={'failed'} className={'l'}>{translate('PHRASE_FAIL_EDENGINEER')}</p>
|
||||
<p hidden={compatible} id={'browserbad'} className={'l'}>{translate('PHRASE_FIREFOX_EDENGINEER')}</p>
|
||||
<button className={'l cb dismiss cap'} disabled={!!this.state.failed || !compatible} onClick={this.sendToEDEng}>{translate('Send to EDEngineer')}</button>
|
||||
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -3,131 +3,106 @@ import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import cn from 'classnames';
|
||||
import NumberEditor from 'react-number-editor';
|
||||
import { isChangeValueBeneficial } from '../utils/BlueprintFunctions';
|
||||
import { Modifications } from 'coriolis-data/dist';
|
||||
import { Module } from 'ed-forge';
|
||||
|
||||
/**
|
||||
* Modification
|
||||
*/
|
||||
export default class Modification extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
ship: PropTypes.object.isRequired,
|
||||
m: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.number.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onKeyDown: PropTypes.func.isRequired,
|
||||
modItems: PropTypes.array.isRequired,
|
||||
handleModChange: PropTypes.func.isRequired
|
||||
highlight: PropTypes.bool,
|
||||
m: PropTypes.instanceOf(Module).isRequired,
|
||||
property: PropTypes.string.isRequired,
|
||||
onSet: PropTypes.func.isRequired,
|
||||
showProp: PropTypes.object,
|
||||
onPropToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.state.value = props.value;
|
||||
const { m, property, showProp } = props;
|
||||
const { beneficial, unit, value } = m.getFormatted(property, true);
|
||||
this.state = { beneficial, unit, value, showProp };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update modification given a value.
|
||||
* @param {Number} value The value to set. This comes in as a string and must be stored in state as a string,
|
||||
* because it needs to allow illegal 'numbers' ('-', '1.', etc) when the user is typing
|
||||
* in a value by hand
|
||||
*/
|
||||
_updateValue(value) {
|
||||
this.setState({ value });
|
||||
let reCast = String(Number(value));
|
||||
if (reCast.endsWith(value) || reCast.startsWith(value)) {
|
||||
let { m, name, ship } = this.props;
|
||||
value = Math.max(Math.min(value, 50000), -50000);
|
||||
ship.setModification(m, name, value, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when a key is pressed down with focus on the number editor.
|
||||
* @param {SyntheticEvent} event Key down event
|
||||
*/
|
||||
_keyDown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
this._updateFinished();
|
||||
}
|
||||
this.props.onKeyDown(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when an update to slider value is finished i.e. when losing focus
|
||||
*
|
||||
* pnellesen (24/05/2018): added value check below - this should prevent experimental effects from being recalculated
|
||||
* with each onBlur event, even when no change has actually been made to the field.
|
||||
* Notify listeners that a new value has been entered and commited.
|
||||
*/
|
||||
_updateFinished() {
|
||||
if (this.props.value != this.state.value) {
|
||||
this.props.handleModChange(true);
|
||||
this.props.onChange();
|
||||
const { onSet, m, property } = this.props;
|
||||
const { inputValue } = this.state;
|
||||
const numValue = Number(inputValue);
|
||||
if (!isNaN(numValue) && this.state.value !== numValue) {
|
||||
onSet(property, numValue);
|
||||
const { beneficial, unit, value } = m.getFormatted(property, true);
|
||||
this.setState({ beneficial, unit, value });
|
||||
}
|
||||
}
|
||||
|
||||
_toggleProperty() {
|
||||
const { onPropToggle, property } = this.props;
|
||||
const showProp = !this.state.showProp;
|
||||
// TODO: defer until menu closed
|
||||
onPropToggle(property, showProp);
|
||||
this.setState({ showProp });
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modification
|
||||
* @return {React.Component} modification
|
||||
*/
|
||||
render() {
|
||||
let { translate, formats, units } = this.context.language;
|
||||
let { m, name } = this.props;
|
||||
let modValue = m.getChange(name);
|
||||
let isOverwrite = Modifications.modifications[name].method === 'overwrite';
|
||||
const { formats } = this.context.language;
|
||||
const { highlight, m, property } = this.props;
|
||||
const { beneficial, unit, value, inputValue, showProp } = this.state;
|
||||
|
||||
if (name === 'damagedist') {
|
||||
// We don't show damage distribution
|
||||
// Some features only apply to specific modules; these features will be
|
||||
// undefined on items that do not belong to the same class. Filter these
|
||||
// features here
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let inputClassNames = {
|
||||
'cb': true,
|
||||
'greyed-out': !this.props.highlight
|
||||
};
|
||||
|
||||
const { value: modifierValue, unit: modifierUnit } = m.getModifierFormatted(property);
|
||||
return (
|
||||
<div onBlur={this._updateFinished.bind(this)} key={name}
|
||||
className={cn('cb', 'modification-container')}
|
||||
ref={ modItem => this.props.modItems[name] = modItem }>
|
||||
<span className={'cb'}>{translate(name, m.grp)}</span>
|
||||
<span className={'header-adjuster'}></span>
|
||||
<table style={{ width: '100%' }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className={'input-container'}>
|
||||
<td>
|
||||
<span>
|
||||
{this.props.editable ?
|
||||
<NumberEditor className={cn(inputClassNames)} value={this.state.value}
|
||||
decimals={2} style={{ textAlign: 'right' }} step={0.01}
|
||||
stepModifier={1} onKeyDown={this._keyDown.bind(this)}
|
||||
onValueChange={this._updateValue.bind(this)} /> :
|
||||
<input type="text" value={formats.f2(this.state.value)}
|
||||
disabled className={cn('number-editor', 'greyed-out')}
|
||||
style={{ textAlign: 'right', cursor: 'inherit' }}/>
|
||||
<input type="checkbox" checked={showProp} onClick={() => this._toggleProperty()}/>
|
||||
</span>
|
||||
</td>
|
||||
<td className="input-container">
|
||||
<span>
|
||||
<NumberEditor value={inputValue || value} stepModifier={1}
|
||||
decimals={2} step={0.01} style={{ textAlign: 'right', width: '100%' }}
|
||||
className={cn('cb', { 'greyed-out': !highlight })}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key == 'Enter') {
|
||||
this._updateFinished();
|
||||
event.stopPropagation();
|
||||
}
|
||||
<span className={'unit-container'}>
|
||||
{units[m.getStoredUnitFor(name)]}
|
||||
</span>
|
||||
}}
|
||||
onValueChange={(inputValue) => {
|
||||
if (inputValue.length <= 15) {
|
||||
this.setState({ inputValue });
|
||||
}
|
||||
}} />
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ textAlign: 'center' }} className={
|
||||
modValue ?
|
||||
isChangeValueBeneficial(name, modValue) ? 'secondary' : 'warning' :
|
||||
''
|
||||
}>
|
||||
{formats.f2(modValue / 100) || 0}{isOverwrite ? '' : '%'}
|
||||
<td style={{ textAlign: 'left' }}>
|
||||
<span className="unit-container">{unit}</span>
|
||||
</td>
|
||||
<td style={{ textAlign: 'center' }}
|
||||
className={cn({
|
||||
secondary: beneficial,
|
||||
warning: beneficial === false,
|
||||
})}
|
||||
>{formats.f2(modifierValue)}{modifierUnit || ''}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as _ from 'lodash';
|
||||
import { chain, flatMap, keys } from 'lodash';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
import cn from 'classnames';
|
||||
import { Modifications } from 'coriolis-data/dist';
|
||||
import Modification from './Modification';
|
||||
import {
|
||||
getBlueprint,
|
||||
blueprintTooltip,
|
||||
setPercent,
|
||||
getPercent,
|
||||
setRandom,
|
||||
specialToolTip
|
||||
} from '../utils/BlueprintFunctions';
|
||||
|
||||
const MODIFICATIONS_COMPARATOR = (mod1, mod2) => {
|
||||
return mod1.props.name.localeCompare(mod2.props.name);
|
||||
};
|
||||
import { getBlueprintInfo, getExperimentalInfo } from 'ed-forge/lib/src/data/blueprints';
|
||||
import { getModuleInfo } from 'ed-forge/lib/src/data/items';
|
||||
import { SHOW } from '../shipyard/StatsMapping';
|
||||
|
||||
/**
|
||||
* Modifications menu
|
||||
*/
|
||||
export default class ModificationsMenu extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
ship: PropTypes.object.isRequired,
|
||||
className: PropTypes.string,
|
||||
m: PropTypes.object.isRequired,
|
||||
marker: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
modButton:PropTypes.object
|
||||
propsToShow: PropTypes.object.isRequired,
|
||||
onPropToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -41,122 +34,58 @@ export default class ModificationsMenu extends TranslatedComponent {
|
||||
|
||||
this._toggleBlueprintsMenu = this._toggleBlueprintsMenu.bind(this);
|
||||
this._toggleSpecialsMenu = this._toggleSpecialsMenu.bind(this);
|
||||
this._rollFifty = this._rollFifty.bind(this);
|
||||
this._rollRandom = this._rollRandom.bind(this);
|
||||
this._rollBest = this._rollBest.bind(this);
|
||||
this._rollWorst = this._rollWorst.bind(this);
|
||||
this._reset = this._reset.bind(this);
|
||||
this._keyDown = this._keyDown.bind(this);
|
||||
this.modItems = [];// Array to hold various element refs (<li>, <div>, <ul>, etc.)
|
||||
this.firstModId = null;
|
||||
this.firstBPLabel = null;// First item in mod menu
|
||||
this.lastModId = null;
|
||||
this.selectedModId = null;
|
||||
this.selectedSpecialId = null;
|
||||
this.lastNeId = null;// Last number editor id. Used to set focus to last number editor when shift-tab pressed on first element in mod menu.
|
||||
this.modValDidChange = false; // used to determine if component update was caused by change in modification value.
|
||||
this._handleModChange = this._handleModChange.bind(this);
|
||||
this.selectedModRef = null;
|
||||
this.selectedSpecialRef = null;
|
||||
|
||||
const { m } = props;
|
||||
this.state = {
|
||||
blueprintMenuOpened: !(props.m.blueprint && props.m.blueprint.name),
|
||||
blueprintProgress: m.getBlueprintProgress(),
|
||||
blueprintMenuOpened: !m.getBlueprint(),
|
||||
specialMenuOpened: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the blueprints
|
||||
* @param {Object} props React component properties
|
||||
* @param {Object} context React component context
|
||||
* @return {Object} list: Array of React Components
|
||||
*/
|
||||
_renderBlueprints(props, context) {
|
||||
const { m } = props;
|
||||
const { language, tooltip, termtip } = context;
|
||||
const translate = language.translate;
|
||||
const blueprints = [];
|
||||
for (const blueprintName in Modifications.modules[m.grp].blueprints) {
|
||||
const blueprint = getBlueprint(blueprintName, m);
|
||||
let blueprintGrades = [];
|
||||
for (let grade in Modifications.modules[m.grp].blueprints[blueprintName].grades) {
|
||||
_renderBlueprints() {
|
||||
const { m } = this.props;
|
||||
const { language, tooltip, termtip } = this.context;
|
||||
const { translate } = language;
|
||||
|
||||
const blueprints = m.getApplicableBlueprints().map(blueprint => {
|
||||
const info = getBlueprintInfo(blueprint);
|
||||
let blueprintGrades = keys(info.features).map(grade => {
|
||||
// Grade is a string in the JSON so make it a number
|
||||
grade = Number(grade);
|
||||
const classes = cn('c', {
|
||||
active: m.blueprint && blueprint.id === m.blueprint.id && grade === m.blueprint.grade
|
||||
const active = m.getBlueprint() === blueprint && m.getBlueprintGrade() === grade;
|
||||
const key = blueprint + ':' + grade;
|
||||
return <li key={key} data-id={key} className={cn('c', { active })}
|
||||
style={{ width: '2em' }}
|
||||
onMouseOver={termtip.bind(null, blueprintTooltip(language, m, blueprint, grade))}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
onClick={() => {
|
||||
m.setBlueprint(blueprint, grade, 1);
|
||||
this.setState({
|
||||
blueprintMenuOpened: false,
|
||||
specialMenuOpened: true,
|
||||
});
|
||||
}}
|
||||
ref={active ? (ref) => { this.selectedModRef = ref; } : undefined}
|
||||
>{grade}</li>;
|
||||
});
|
||||
const close = this._blueprintSelected.bind(this, blueprintName, grade);
|
||||
const key = blueprintName + ':' + grade;
|
||||
const tooltipContent = blueprintTooltip(translate, blueprint.grades[grade], Modifications.modules[m.grp].blueprints[blueprintName].grades[grade].engineers, m.grp);
|
||||
if (classes.indexOf('active') >= 0) this.selectedModId = key;
|
||||
blueprintGrades.unshift(<li key={key} tabIndex="0" data-id={key} className={classes} style={{ width: '2em' }} onMouseOver={termtip.bind(null, tooltipContent)} onMouseOut={tooltip.bind(null, null)} onClick={close} onKeyDown={this._keyDown} ref={modItem => this.modItems[key] = modItem}>{grade}</li>);
|
||||
}
|
||||
if (blueprintGrades) {
|
||||
const thisLen = blueprintGrades.length;
|
||||
if (this.firstModId == null) this.firstModId = blueprintGrades[0].key;
|
||||
this.lastModId = blueprintGrades[thisLen - 1].key;
|
||||
blueprints.push(<div key={blueprint.name} className={'select-group cap'}>{translate(blueprint.name)}</div>);
|
||||
blueprints.push(<ul key={blueprintName}>{blueprintGrades}</ul>);
|
||||
}
|
||||
}
|
||||
return blueprints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key down - select module on Enter key, move to next/previous module on Tab/Shift-Tab, close on Esc
|
||||
* @param {SyntheticEvent} event Event
|
||||
*
|
||||
*/
|
||||
_keyDown(event) {
|
||||
let className = null;
|
||||
let elemId = null;
|
||||
if (event.currentTarget.attributes['class']) className = event.currentTarget.attributes['class'].value;
|
||||
if (event.currentTarget.attributes['data-id']) elemId = event.currentTarget.attributes['data-id'].value;
|
||||
return [
|
||||
<div key={'div' + blueprint} className={'select-group cap'}>
|
||||
{translate(blueprint)}
|
||||
</div>,
|
||||
<ul key={'ul' + blueprint}>{blueprintGrades}</ul>
|
||||
];
|
||||
});
|
||||
|
||||
if (event.key == 'Enter' && className.indexOf('disabled') < 0 && className.indexOf('active') < 0) {
|
||||
event.stopPropagation();
|
||||
if (elemId != null) {
|
||||
this.modItems[elemId].click();
|
||||
} else {
|
||||
event.currentTarget.click();
|
||||
return flatMap(blueprints);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key == 'Tab') {
|
||||
// Shift-Tab
|
||||
if(event.shiftKey) {
|
||||
if (elemId == this.firstModId && elemId != null) {
|
||||
// Initial modification menu
|
||||
event.preventDefault();
|
||||
this.modItems[this.lastModId].focus();
|
||||
return;
|
||||
} else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.previousElementSibling == null && this.lastNeId != null && this.modItems[this.lastNeId] != null) {
|
||||
// shift-tab on first element in modifications menu. set focus to last number editor field if open
|
||||
event.preventDefault();
|
||||
this.modItems[this.lastNeId].lastChild.focus();
|
||||
return;
|
||||
} else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.previousElementSibling == null) {
|
||||
// shift-tab on button-inline-menu with no number editor
|
||||
event.preventDefault();
|
||||
event.currentTarget.parentElement.lastElementChild.focus();
|
||||
}
|
||||
} else {
|
||||
if (elemId == this.lastModId && elemId != null) {
|
||||
// Initial modification menu
|
||||
event.preventDefault();
|
||||
this.modItems[this.firstModId].focus();
|
||||
return;
|
||||
} else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.nextSibling == null && event.currentTarget.nodeName != 'TD') {
|
||||
// Experimental menu
|
||||
event.preventDefault();
|
||||
event.currentTarget.parentElement.firstElementChild.focus();
|
||||
return;
|
||||
} else if (event.currentTarget.className == 'cb' && event.currentTarget.parentElement.nextSibling == null) {
|
||||
event.preventDefault();
|
||||
this.modItems[this.firstBPLabel].focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Render the specials
|
||||
@@ -164,204 +93,118 @@ export default class ModificationsMenu extends TranslatedComponent {
|
||||
* @param {Object} context React component context
|
||||
* @return {Object} list: Array of React Components
|
||||
*/
|
||||
_renderSpecials(props, context) {
|
||||
const { m } = props;
|
||||
const { language, tooltip, termtip } = context;
|
||||
_renderSpecials() {
|
||||
const { m } = this.props;
|
||||
const { language, tooltip, termtip } = this.context;
|
||||
const translate = language.translate;
|
||||
const specials = [];
|
||||
const specialsId = m.missile && Modifications.modules[m.grp]['specials_' + m.missile] ? 'specials_' + m.missile : 'specials';
|
||||
if (Modifications.modules[m.grp][specialsId] && Modifications.modules[m.grp][specialsId].length > 0) {
|
||||
const close = this._specialSelected.bind(this, null);
|
||||
specials.push(<div tabIndex="0" style={{ cursor: 'pointer', fontWeight: 'bold' }} className={ 'button-inline-menu warning' } key={ 'none' } data-id={ 'none' } onClick={ close } onKeyDown={this._keyDown} ref={modItem => this.modItems['none'] = modItem}>{translate('PHRASE_NO_SPECIAL')}</div>);
|
||||
for (const specialName of Modifications.modules[m.grp][specialsId]) {
|
||||
if (Modifications.specials[specialName].name.search('Legacy') >= 0) {
|
||||
continue;
|
||||
|
||||
const applied = m.getExperimental();
|
||||
const experimentals = [];
|
||||
for (const experimental of m.getApplicableExperimentals()) {
|
||||
const active = experimental === applied;
|
||||
let specialTt = specialToolTip(language, m, experimental);
|
||||
experimentals.push(
|
||||
<div key={experimental} data-id={experimental}
|
||||
style={{ cursor: 'pointer' }}
|
||||
className={cn('button-inline-menu', { active })}
|
||||
onClick={this._specialSelected(experimental)}
|
||||
ref={active ? (ref) => { this.selectedSpecialRef = ref; } : undefined}
|
||||
onMouseOver={termtip.bind(null, specialTt)}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>{translate(experimental)}</div>
|
||||
);
|
||||
}
|
||||
const classes = cn('button-inline-menu', {
|
||||
active: m.blueprint && m.blueprint.special && m.blueprint.special.edname == specialName
|
||||
});
|
||||
if (classes.indexOf('active') >= 0) this.selectedSpecialId = specialName;
|
||||
const close = this._specialSelected.bind(this, specialName);
|
||||
if (m.blueprint && m.blueprint.name) {
|
||||
let tmp = {};
|
||||
if (m.blueprint.special) {
|
||||
tmp = m.blueprint.special;
|
||||
} else {
|
||||
tmp = undefined;
|
||||
|
||||
if (experimentals.length) {
|
||||
experimentals.unshift(
|
||||
<div style={{ cursor: 'pointer', fontWeight: 'bold' }}
|
||||
className="button-inline-menu warning" key="none" data-id="none"
|
||||
// Setting the special effect to undefined clears it
|
||||
onClick={this._specialSelected(undefined)}
|
||||
ref={!applied ? (ref) => { this.selectedSpecialRef = ref; } : undefined}
|
||||
>{translate('PHRASE_NO_SPECIAL')}</div>
|
||||
);
|
||||
}
|
||||
m.blueprint.special = Modifications.specials[specialName];
|
||||
let specialTt = specialToolTip(translate, m.blueprint.grades[m.blueprint.grade], m.grp, m, specialName);
|
||||
m.blueprint.special = tmp;
|
||||
specials.push(<div tabIndex="0" style={{ cursor: 'pointer' }} className={classes} key={ specialName } data-id={ specialName } onMouseOver={termtip.bind(null, specialTt)} onMouseOut={tooltip.bind(null, null)} onClick={ close } onKeyDown={this._keyDown} ref={modItem => this.modItems[specialName] = modItem}>{translate(Modifications.specials[specialName].name)}</div>);
|
||||
} else {
|
||||
specials.push(<div tabIndex="0" style={{ cursor: 'pointer' }} className={classes} key={ specialName } data-id={ specialName }onClick={ close } onKeyDown={this._keyDown} ref={modItem => this.modItems[specialName] = modItem}>{translate(Modifications.specials[specialName].name)}</div>);
|
||||
|
||||
return experimentals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a modification component
|
||||
*/
|
||||
_mkModification(property, highlight) {
|
||||
const { translate } = this.context.language;
|
||||
const { m, propsToShow, onPropToggle } = this.props;
|
||||
|
||||
let onSet = m.set.bind(m);
|
||||
// Show resistance instead of effectiveness
|
||||
const mapped = SHOW[property];
|
||||
if (mapped) {
|
||||
property = mapped.as;
|
||||
onSet = mapped.setter.bind(undefined, m);
|
||||
}
|
||||
}
|
||||
return specials;
|
||||
|
||||
return [
|
||||
<tr key={`th-${property}`}>
|
||||
<th colSpan="4">
|
||||
<span className="cb">{translate(property)}</span>
|
||||
</th>
|
||||
</tr>,
|
||||
<Modification key={property} m={m} property={property}
|
||||
onSet={onSet} highlight={highlight} showProp={propsToShow[property]}
|
||||
onPropToggle={onPropToggle} />
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modifications
|
||||
* @param {Object} props React Component properties
|
||||
* @return {Object} list: Array of React Components
|
||||
* @return {Array} Array of React Components
|
||||
*/
|
||||
_renderModifications(props) {
|
||||
const { m, onChange, ship } = props;
|
||||
const modifiableModifications = [];
|
||||
const modifications = [];
|
||||
for (const modName of Modifications.modules[m.grp].modifications) {
|
||||
if (!Modifications.modifications[modName].hidden) {
|
||||
const key = modName + (m.getModValue(modName) / 100 || 0);
|
||||
const editable = modName !== 'fallofffromrange';
|
||||
const highlight = m.blueprint.grades[m.blueprint.grade].features[modName];
|
||||
this.lastNeId = modName;
|
||||
(editable && highlight ? modifiableModifications : modifications).push(
|
||||
<Modification key={ key } ship={ ship } m={ m } highlight={highlight}
|
||||
value={m.getPretty(modName) || 0} modItems={this.modItems}
|
||||
onChange={onChange} onKeyDown={this._keyDown} name={modName}
|
||||
editable={editable} handleModChange = {this._handleModChange} />
|
||||
);
|
||||
}
|
||||
}
|
||||
_renderModifications() {
|
||||
const { m } = this.props;
|
||||
|
||||
modifiableModifications.sort(MODIFICATIONS_COMPARATOR);
|
||||
modifications.sort(MODIFICATIONS_COMPARATOR);
|
||||
return modifiableModifications.concat(modifications);
|
||||
const blueprintFeatures = getBlueprintInfo(m.getBlueprint()).features[
|
||||
m.getBlueprintGrade()
|
||||
];
|
||||
const blueprintModifications = chain(keys(blueprintFeatures))
|
||||
.map((feature) => this._mkModification(feature, true))
|
||||
.filter(([_, mod]) => Boolean(mod))
|
||||
.flatMap()
|
||||
.value();
|
||||
const moduleModifications = chain(keys(getModuleInfo(m.getItem()).props))
|
||||
.filter((prop) => !blueprintFeatures[prop])
|
||||
.map((prop) => this._mkModification(prop, false))
|
||||
.flatMap()
|
||||
.value();
|
||||
|
||||
return blueprintModifications.concat(moduleModifications);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the blueprints menu
|
||||
*/
|
||||
_toggleBlueprintsMenu() {
|
||||
const blueprintMenuOpened = !this.state.blueprintMenuOpened;
|
||||
this.setState({ blueprintMenuOpened });
|
||||
}
|
||||
|
||||
/**
|
||||
* Activated when a blueprint is selected
|
||||
* @param {int} fdname The Frontier name of the blueprint
|
||||
* @param {int} grade The grade of the selected blueprint
|
||||
*/
|
||||
_blueprintSelected(fdname, grade) {
|
||||
this.context.tooltip(null);
|
||||
const { m, ship } = this.props;
|
||||
const blueprint = getBlueprint(fdname, m);
|
||||
blueprint.grade = grade;
|
||||
ship.setModuleBlueprint(m, blueprint);
|
||||
setPercent(ship, m, 100);
|
||||
|
||||
this.setState({ blueprintMenuOpened: false, specialMenuOpened: true });
|
||||
this.props.onChange();
|
||||
this.setState({ blueprintMenuOpened: !this.state.blueprintMenuOpened });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the specials menu
|
||||
*/
|
||||
_toggleSpecialsMenu() {
|
||||
const specialMenuOpened = !this.state.specialMenuOpened;
|
||||
this.setState({ specialMenuOpened });
|
||||
this.setState({ specialMenuOpened: !this.state.specialMenuOpened });
|
||||
}
|
||||
|
||||
/**
|
||||
* Activated when a special is selected
|
||||
* @param {int} special The name of the selected special
|
||||
* Creates a callback for when a special effect is being selected
|
||||
* @param {string} special The name of the selected special
|
||||
* @returns {function} Callback
|
||||
*/
|
||||
_specialSelected(special) {
|
||||
this.context.tooltip(null);
|
||||
const { m, ship } = this.props;
|
||||
|
||||
if (special === null) {
|
||||
ship.clearModuleSpecial(m);
|
||||
} else {
|
||||
ship.setModuleSpecial(m, Modifications.specials[special]);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const { m } = this.props;
|
||||
m.setExperimental(special);
|
||||
this.setState({ specialMenuOpened: false });
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a '50%' roll within the information we have
|
||||
*/
|
||||
_rollFifty() {
|
||||
const { m, ship } = this.props;
|
||||
setPercent(ship, m, 50);
|
||||
|
||||
// this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
|
||||
this._handleModChange(true);
|
||||
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a random roll within the information we have
|
||||
*/
|
||||
_rollRandom() {
|
||||
const { m, ship } = this.props;
|
||||
setRandom(ship, m);
|
||||
|
||||
// this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
|
||||
this._handleModChange(true);
|
||||
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a 'best' roll within the information we have
|
||||
*/
|
||||
_rollBest() {
|
||||
const { m, ship } = this.props;
|
||||
setPercent(ship, m, 100);
|
||||
|
||||
// this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
|
||||
this._handleModChange(true);
|
||||
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a 'worst' roll within the information we have
|
||||
*/
|
||||
_rollWorst() {
|
||||
const { m, ship } = this.props;
|
||||
setPercent(ship, m, 0);
|
||||
// this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
|
||||
this._handleModChange(true);
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset modification information
|
||||
*/
|
||||
_reset() {
|
||||
const { m, ship } = this.props;
|
||||
ship.clearModifications(m);
|
||||
ship.clearModuleBlueprint(m);
|
||||
this.selectedModId = null;
|
||||
this.selectedSpecialId = null;
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* set mod did change boolean
|
||||
* @param {boolean} b Boolean to determine if a change has been made to a module
|
||||
*/
|
||||
_handleModChange(b) {
|
||||
this.modValDidChange = b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set focus on first element in modifications menu
|
||||
* after it first mounts
|
||||
*/
|
||||
componentDidMount() {
|
||||
let firstEleCn = this.modItems['modMainDiv'].children.length > 0 ? this.modItems['modMainDiv'].children[0].className : null;
|
||||
if (firstEleCn.indexOf('select-group cap') >= 0) {
|
||||
this.modItems['modMainDiv'].children[1].firstElementChild.focus();
|
||||
} else {
|
||||
this.modItems['modMainDiv'].firstElementChild.focus();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -370,33 +213,13 @@ export default class ModificationsMenu extends TranslatedComponent {
|
||||
* in a modification
|
||||
*/
|
||||
componentDidUpdate() {
|
||||
if (!this.modValDidChange) {
|
||||
if (this.modItems['modMainDiv'].children.length > 0) {
|
||||
if (this.modItems[this.selectedModId]) {
|
||||
this.modItems[this.selectedModId].focus();
|
||||
if (this.selectedModRef) {
|
||||
this.selectedModRef.focus();
|
||||
return;
|
||||
} else if (this.modItems[this.selectedSpecialId]) {
|
||||
this.modItems[this.selectedSpecialId].focus();
|
||||
} else if (this.selectedSpecialRef) {
|
||||
this.selectedSpecialRef.focus();
|
||||
return;
|
||||
}
|
||||
let firstEleCn = this.modItems['modMainDiv'].children[0].className;
|
||||
if (firstEleCn.indexOf('button-inline-menu') >= 0) {
|
||||
this.modItems['modMainDiv'].firstElementChild.focus();
|
||||
} else if (firstEleCn.indexOf('select-group cap') >= 0) {
|
||||
this.modItems['modMainDiv'].children[1].firstElementChild.focus();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._handleModChange(false);// Need to reset if component update due to value change
|
||||
}
|
||||
}
|
||||
/**
|
||||
* set focus to the modification menu icon after mod menu is unmounted.
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
if (this.props.modButton) {
|
||||
this.props.modButton.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,90 +230,155 @@ export default class ModificationsMenu extends TranslatedComponent {
|
||||
const { language, tooltip, termtip } = this.context;
|
||||
const translate = language.translate;
|
||||
const { m } = this.props;
|
||||
const { blueprintMenuOpened, specialMenuOpened } = this.state;
|
||||
const {
|
||||
blueprintProgress, blueprintMenuOpened, specialMenuOpened,
|
||||
} = this.state;
|
||||
|
||||
const _toggleBlueprintsMenu = this._toggleBlueprintsMenu;
|
||||
const _toggleSpecialsMenu = this._toggleSpecialsMenu;
|
||||
const _rollFull = this._rollBest;
|
||||
const _rollWorst = this._rollWorst;
|
||||
const _rollFifty = this._rollFifty;
|
||||
const _rollRandom = this._rollRandom;
|
||||
const _reset = this._reset;
|
||||
const appliedBlueprint = m.getBlueprint();
|
||||
const appliedGrade = m.getBlueprintGrade();
|
||||
const appliedExperimental = m.getExperimental();
|
||||
|
||||
let blueprintLabel;
|
||||
let haveBlueprint = false;
|
||||
let blueprintTt;
|
||||
let blueprintCv;
|
||||
// TODO: Fix this to actually find the correct blueprint.
|
||||
if (!m.blueprint || !m.blueprint.name || !m.blueprint.fdname || !Modifications.modules[m.grp].blueprints || !Modifications.modules[m.grp].blueprints[m.blueprint.fdname]) {
|
||||
this.props.ship.clearModuleBlueprint(m);
|
||||
this.props.ship.clearModuleSpecial(m);
|
||||
}
|
||||
if (m.blueprint && m.blueprint.name && Modifications.modules[m.grp].blueprints[m.blueprint.fdname].grades[m.blueprint.grade]) {
|
||||
blueprintLabel = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
|
||||
haveBlueprint = true;
|
||||
blueprintTt = blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], Modifications.modules[m.grp].blueprints[m.blueprint.fdname].grades[m.blueprint.grade].engineers, m.grp);
|
||||
blueprintCv = getPercent(m);
|
||||
}
|
||||
let renderComponents = [];
|
||||
switch (true) {
|
||||
case !appliedBlueprint || blueprintMenuOpened:
|
||||
renderComponents = this._renderBlueprints();
|
||||
break;
|
||||
case specialMenuOpened:
|
||||
renderComponents = this._renderSpecials();
|
||||
break;
|
||||
default:
|
||||
// Since the first case didn't apply, there is a blueprint applied so
|
||||
// we render the modifications
|
||||
let blueprintTt = blueprintTooltip(language, m, appliedBlueprint, appliedGrade);
|
||||
|
||||
let specialLabel;
|
||||
let specialTt;
|
||||
if (m.blueprint && m.blueprint.special) {
|
||||
specialLabel = translate(m.blueprint.special.name);
|
||||
specialTt = specialToolTip(translate, m.blueprint.grades[m.blueprint.grade], m.grp, m, m.blueprint.special.edname);
|
||||
} else {
|
||||
specialLabel = translate('PHRASE_SELECT_SPECIAL');
|
||||
}
|
||||
|
||||
const specials = this._renderSpecials(this.props, this.context);
|
||||
/**
|
||||
* pnellesen - 05/28/2018 - added additional checks for specials.length below to ensure menus
|
||||
* display correctly in cases where there are no specials (ex: AFMUs.)
|
||||
*/
|
||||
const showBlueprintsMenu = blueprintMenuOpened;
|
||||
const showSpecial = haveBlueprint && specials.length && !blueprintMenuOpened;
|
||||
const showSpecialsMenu = specialMenuOpened && specials.length;
|
||||
const showRolls = haveBlueprint && !blueprintMenuOpened && (!specialMenuOpened || !specials.length);
|
||||
const showReset = !blueprintMenuOpened && (!specialMenuOpened || !specials.length) && haveBlueprint;
|
||||
const showMods = !blueprintMenuOpened && (!specialMenuOpened || !specials.length) && haveBlueprint;
|
||||
if (haveBlueprint) {
|
||||
this.firstBPLabel = blueprintLabel;
|
||||
} else {
|
||||
this.firstBPLabel = 'selectBP';
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cn('select', this.props.className)}
|
||||
onClick={(e) => e.stopPropagation() }
|
||||
onContextMenu={stopCtxPropagation}
|
||||
ref={modItem => this.modItems['modMainDiv'] = modItem}
|
||||
renderComponents.push(
|
||||
<div style={{ cursor: 'pointer' }} key="blueprintsMenu"
|
||||
className="section-menu button-inline-menu"
|
||||
onMouseOver={termtip.bind(null, blueprintTt)}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
onClick={this._toggleBlueprintsMenu}
|
||||
>
|
||||
{ showBlueprintsMenu | showSpecialsMenu ? '' : haveBlueprint ?
|
||||
<div tabIndex="0" className={ cn('section-menu button-inline-menu', { selected: blueprintMenuOpened })} style={{ cursor: 'pointer' }} onMouseOver={termtip.bind(null, blueprintTt)} onMouseOut={tooltip.bind(null, null)} onClick={_toggleBlueprintsMenu} onKeyDown={ this._keyDown } ref={modItems => this.modItems[this.firstBPLabel] = modItems}>{blueprintLabel}</div> :
|
||||
<div tabIndex="0" className={ cn('section-menu button-inline-menu', { selected: blueprintMenuOpened })} style={{ cursor: 'pointer' }} onClick={_toggleBlueprintsMenu} onKeyDown={ this._keyDown } ref={modItems => this.modItems[this.firstBPLabel] = modItems}>{translate('PHRASE_SELECT_BLUEPRINT')}</div> }
|
||||
{ showBlueprintsMenu ? this._renderBlueprints(this.props, this.context) : null }
|
||||
{ showSpecial & !showSpecialsMenu ? <div tabIndex="0" className={ cn('section-menu button-inline-menu', { selected: specialMenuOpened })} style={{ cursor: 'pointer' }} onMouseOver={specialTt ? termtip.bind(null, specialTt) : null} onMouseOut={specialTt ? tooltip.bind(null, null) : null} onClick={_toggleSpecialsMenu} onKeyDown={ this._keyDown }>{specialLabel}</div> : null }
|
||||
{ showSpecialsMenu ? specials : null }
|
||||
{ showReset ? <div tabIndex="0" className={'section-menu button-inline-menu warning'} style={{ cursor: 'pointer' }} onClick={_reset} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RESET')} onMouseOut={tooltip.bind(null, null)}> { translate('reset') } </div> : null }
|
||||
{ showRolls ?
|
||||
{translate(appliedBlueprint)} {translate('grade')} {appliedGrade}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (m.getApplicableExperimentals().length) {
|
||||
let specialLabel = translate('PHRASE_SELECT_SPECIAL');
|
||||
let specialTt;
|
||||
if (appliedExperimental) {
|
||||
specialLabel = appliedExperimental;
|
||||
specialTt = specialToolTip(language, m, appliedExperimental);
|
||||
}
|
||||
renderComponents.push(
|
||||
<div className="section-menu button-inline-menu"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onMouseOver={specialTt ? termtip.bind(null, specialTt) : null}
|
||||
onMouseOut={specialTt ? tooltip.bind(null, null) : null}
|
||||
onClick={this._toggleSpecialsMenu}
|
||||
>{specialLabel}</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderComponents.push(
|
||||
<div
|
||||
className="section-menu button-inline-menu warning"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
m.resetEngineering();
|
||||
this.selectedModRef = null;
|
||||
this.selectedSpecialRef = null;
|
||||
tooltip(null);
|
||||
this.setState({
|
||||
blueprintMenuOpened: true,
|
||||
blueprintProgress: undefined,
|
||||
});
|
||||
}}
|
||||
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RESET')}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>{translate('reset')}</div>,
|
||||
<table style={{ width: '100%', backgroundColor: 'transparent' }}>
|
||||
<tbody>
|
||||
{ showRolls ?
|
||||
<tr>
|
||||
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: false }) }> { translate('mroll') }: </td>
|
||||
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: blueprintCv === 0 }) } style={{ cursor: 'pointer' }} onClick={_rollWorst} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_WORST')} onMouseOut={tooltip.bind(null, null)}> { translate('0%') } </td>
|
||||
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: blueprintCv === 50 })} style={{ cursor: 'pointer' }} onClick={_rollFifty} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_FIFTY')} onMouseOut={tooltip.bind(null, null)}> { translate('50%') } </td>
|
||||
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: blueprintCv === 100 })} style={{ cursor: 'pointer' }} onClick={_rollFull} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_BEST')} onMouseOut={tooltip.bind(null, null)}> { translate('100%') } </td>
|
||||
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: blueprintCv === null || blueprintCv % 50 != 0 })} style={{ cursor: 'pointer' }} onClick={_rollRandom} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RANDOM')} onMouseOut={tooltip.bind(null, null)}> { translate('random') } </td>
|
||||
</tr> : null }
|
||||
<td
|
||||
className={cn(
|
||||
'section-menu button-inline-menu',
|
||||
{ active: false },
|
||||
)}
|
||||
>{translate('mroll')}:</td>
|
||||
<td
|
||||
className={cn(
|
||||
'section-menu button-inline-menu',
|
||||
{ active: blueprintProgress === 0 },
|
||||
)} style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
m.setBlueprintProgress(0);
|
||||
this.setState({ blueprintProgress: 0 });
|
||||
}}
|
||||
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_WORST')}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>{translate('0%')}</td>
|
||||
<td
|
||||
className={cn(
|
||||
'section-menu button-inline-menu',
|
||||
{ active: blueprintProgress === 0.5 },
|
||||
)} style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
m.setBlueprintProgress(0.5);
|
||||
this.setState({ blueprintProgress: 0.5 });
|
||||
}}
|
||||
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_FIFTY')}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>{translate('50%')}</td>
|
||||
<td
|
||||
className={cn(
|
||||
'section-menu button-inline-menu',
|
||||
{ active: blueprintProgress === 1 },
|
||||
)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
m.setBlueprintProgress(1);
|
||||
this.setState({ blueprintProgress: 1 });
|
||||
}}
|
||||
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_BEST')}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>{translate('100%')}</td>
|
||||
<td
|
||||
className={cn(
|
||||
'section-menu button-inline-menu',
|
||||
{ active: blueprintProgress % 0.5 !== 0 },
|
||||
)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
const blueprintProgress = Math.random();
|
||||
m.setBlueprintProgress(blueprintProgress);
|
||||
this.setState({ blueprintProgress });
|
||||
}}
|
||||
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RANDOM')}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>{translate('random')}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table> : null }
|
||||
{ showMods ? <hr /> : null }
|
||||
{ showMods ?
|
||||
<span onMouseOver={termtip.bind(null, 'HELP_MODIFICATIONS_MENU')} onMouseOut={tooltip.bind(null, null)} >
|
||||
{ this._renderModifications(this.props) }
|
||||
</span> : null }
|
||||
</table>,
|
||||
<hr />,
|
||||
<span
|
||||
onMouseOver={termtip.bind(null, 'HELP_MODIFICATIONS_MENU')}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>
|
||||
<table style={{ width: '100%' }}>
|
||||
<tbody>
|
||||
{this._renderModifications()}
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('select', this.props.className)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onContextMenu={stopCtxPropagation}
|
||||
>
|
||||
{renderComponents}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,36 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { ShipProps } from 'ed-forge';
|
||||
const { SPEED, BOOST_SPEED, ROLL, BOOST_ROLL, YAW, BOOST_YAW, PITCH, BOOST_PITCH } = ShipProps;
|
||||
|
||||
/**
|
||||
* Movement
|
||||
*/
|
||||
export default class Movement extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
marker: PropTypes.string.isRequired,
|
||||
code: PropTypes.string.isRequired,
|
||||
ship: PropTypes.object.isRequired,
|
||||
boost: PropTypes.bool.isRequired,
|
||||
eng: PropTypes.number.isRequired,
|
||||
fuel: PropTypes.number.isRequired,
|
||||
cargo: PropTypes.number.isRequired
|
||||
pips: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render movement
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { ship, boost, eng, cargo, fuel } = this.props;
|
||||
const { ship, boost } = this.props;
|
||||
const { language } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { formats } = language;
|
||||
|
||||
return (
|
||||
<span id='movement'>
|
||||
@@ -57,14 +49,10 @@ export default class Movement extends TranslatedComponent {
|
||||
<path d="M359.5 422.4l-1.2 19.3-1.6.4-10.7-16 .2-.2 13-3.4.3.4zm-9 5l5.2 7.8.6-9.3-5.7 1.2zm-10.5 24l-13.2 8.6-2.6-9.7 15.8 1z"/>
|
||||
<path d="M342 450l.4 1.5-16.2 10.7-.4-.2-3.5-13 .3-.3L342 450zm-14.3 7.6l7.7-5-9.2-.6 1.5 5.6z"/>
|
||||
|
||||
{/* Speed */}
|
||||
<text x="470" y="30" strokeWidth='0'>{ship.canThrust(cargo, fuel) ? formats.int(ship.calcSpeed(eng, fuel, cargo, boost)) + 'm/s' : '-'}</text>
|
||||
{/* Pitch */}
|
||||
<text x="355" y="410" strokeWidth='0'>{ship.canThrust(cargo, fuel) ? formats.int(ship.calcPitch(eng, fuel, cargo, boost)) + '°/s' : '-'}</text>
|
||||
{/* Roll */}
|
||||
<text x="450" y="110" strokeWidth='0'>{ship.canThrust(cargo, fuel) ? formats.int(ship.calcRoll(eng, fuel, cargo, boost)) + '°/s' : '-'}</text>
|
||||
{/* Yaw */}
|
||||
<text x="160" y="430" strokeWidth='0'>{ship.canThrust(cargo, fuel) ? formats.int(ship.calcYaw(eng, fuel, cargo, boost)) + '°/s' : '-'}</text>
|
||||
<text x="470" y="30" strokeWidth='0'>{formats.int(ship.get(boost ? BOOST_SPEED : SPEED)) + 'm/s'}</text>
|
||||
<text x="355" y="410" strokeWidth='0'>{formats.int(ship.get(boost ? BOOST_PITCH : PITCH)) + '°/s'}</text>
|
||||
<text x="450" y="110" strokeWidth='0'>{formats.int(ship.get(boost ? BOOST_ROLL : ROLL)) + '°/s'}</text>
|
||||
<text x="160" y="430" strokeWidth='0'>{formats.int(ship.get(boost ? BOOST_YAW : YAW)) + '°/s'}</text>
|
||||
</svg>
|
||||
</span>);
|
||||
}
|
||||
|
||||
@@ -1,103 +1,37 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import * as Calc from '../shipyard/Calculations';
|
||||
import PieChart from './PieChart';
|
||||
import { nameComparator } from '../utils/SlotFunctions';
|
||||
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
|
||||
import { Ship } from 'ed-forge';
|
||||
import autoBind from 'auto-bind';
|
||||
import { DAMAGE_METRICS } from 'ed-forge/lib/src/ship-stats';
|
||||
import { clone, mapValues, mergeWith, reverse, sortBy, sum, toPairs, values } from 'lodash';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Turns an object into a tooltip.
|
||||
* @param {function} translate Translate function
|
||||
* @param {object} o Map to make the tooltip from
|
||||
* @returns {React.Component} Tooltip
|
||||
*/
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a tooltip that shows damage by type.
|
||||
* @param {function} translate Translation function
|
||||
* @param {Object} formats Object that holds format functions
|
||||
* @param {Calc.SDps} sdpsObject Object that holds sdps split up by type
|
||||
* @returns {Array} Tooltip
|
||||
*/
|
||||
function getSDpsTooltip(translate, formats, sdpsObject) {
|
||||
return Object.keys(sdpsObject).filter(key => sdpsObject[key])
|
||||
.map(key => {
|
||||
return (
|
||||
<div key={key}>
|
||||
{translate(key) + ' ' + formats.f1(sdpsObject[key])}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
function objToTooltip(translate, o) {
|
||||
return toPairs(o)
|
||||
.filter(([k, v]) => Boolean(v))
|
||||
.map(([k, v]) => <div key={k}>{`${translate(k)}: ${v}`}</div>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a data object used by {@link PieChart} that shows damage by type.
|
||||
* @param {function} translate Translation function
|
||||
* @param {Calc.SDps} sdpsObject Object that holds sdps split up by type
|
||||
* @param {Calc.SDps} o Object that holds sdps split up by type
|
||||
* @returns {Object} Data object
|
||||
*/
|
||||
function getSDpsData(translate, sdpsObject) {
|
||||
return Object.keys(sdpsObject).map(key => {
|
||||
return {
|
||||
value: Math.round(sdpsObject[key]),
|
||||
label: translate(key)
|
||||
};
|
||||
function objToPie(translate, o) {
|
||||
return toPairs(o).map(([k, value]) => {
|
||||
return { label: translate(k), value };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all damage of `add` onto `addOn`.
|
||||
* @param {Calc.SDps} addOn Object that holds sdps split up by type (will be mutated)
|
||||
* @param {Calc.SDps} add Object that holds sdps split up by type
|
||||
*/
|
||||
function addSDps(addOn, add) {
|
||||
Object.keys(addOn).map(k => addOn[k] += (add[k] ? add[k] : 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the overall sdps of an sdps object.
|
||||
* @param {Calc.SDps} sdpsObject Object that holds sdps spluit up by type
|
||||
*/
|
||||
function sumSDps(sdpsObject) {
|
||||
if (sdpsObject.total) {
|
||||
return sdpsObject.total;
|
||||
}
|
||||
|
||||
return Object.keys(sdpsObject).reduce(
|
||||
(acc, k) => acc + (sdpsObject[k] ? sdpsObject[k] : 0),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offence information
|
||||
* Offence information consists of four panels:
|
||||
@@ -108,12 +42,10 @@ function sumSDps(sdpsObject) {
|
||||
*/
|
||||
export default class Offence extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
marker: PropTypes.string.isRequired,
|
||||
ship: PropTypes.object.isRequired,
|
||||
opponent: PropTypes.object.isRequired,
|
||||
engagementrange: PropTypes.number.isRequired,
|
||||
wep: PropTypes.number.isRequired,
|
||||
opponentSys: PropTypes.number.isRequired
|
||||
code: PropTypes.string.isRequired,
|
||||
ship: PropTypes.instanceOf(Ship).isRequired,
|
||||
opponent: PropTypes.instanceOf(Ship).isRequired,
|
||||
engagementRange: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -122,151 +54,234 @@ export default class Offence extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
autoBind(this);
|
||||
|
||||
this._sort = this._sort.bind(this);
|
||||
|
||||
const damage = Calc.offenceMetrics(props.ship, props.opponent, props.wep, props.opponentSys, props.engagementrange);
|
||||
this.state = {
|
||||
predicate: 'n',
|
||||
predicate: 'classRating',
|
||||
desc: true,
|
||||
damage
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state if our properties change
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @return {boolean} Returns true if the component should be rerendered
|
||||
*/
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.marker != nextProps.marker || this.props.eng != nextProps.eng) {
|
||||
const damage = Calc.offenceMetrics(nextProps.ship, nextProps.opponent, nextProps.wep, nextProps.opponentSys, nextProps.engagementrange);
|
||||
this.setState({ damage });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(predicate, desc);
|
||||
let desc = predicate == this.state.predicate ? !this.state.desc : true;
|
||||
this.setState({ predicate, desc });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the weapon list
|
||||
* @param {string} predicate Sort predicate
|
||||
* @param {Boolean} desc Sort order descending
|
||||
*/
|
||||
_sort(predicate, desc) {
|
||||
let comp = weaponComparator.bind(null, this.context.language.translate);
|
||||
|
||||
switch (predicate) {
|
||||
case 'n': comp = comp(null, desc); break;
|
||||
case 'esdpss': comp = comp((a, b) => a.sdps.shields.total - b.sdps.shields.total, desc); break;
|
||||
case 'es': comp = comp((a, b) => a.effectiveness.shields.total - b.effectiveness.shields.total, desc); break;
|
||||
case 'esdpsh': comp = comp((a, b) => a.sdps.armour.total - b.sdps.armour.total, desc); break;
|
||||
case 'eh': comp = comp((a, b) => a.effectiveness.armour.total - b.effectiveness.armour.total, desc); break;
|
||||
}
|
||||
|
||||
this.state.damage.sort(comp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render offence
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { ship, opponent, wep, engagementrange } = this.props;
|
||||
const { ship } = this.props;
|
||||
const { language, tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { damage } = this.state;
|
||||
const sortOrder = this._sortOrder;
|
||||
|
||||
const pd = ship.standard[4].m;
|
||||
const {
|
||||
drained, sustained, rangeMultiplier, hardnessMultiplier, timeToDrain
|
||||
} = ship.getMetrics(DAMAGE_METRICS);
|
||||
const portions = {
|
||||
Absolute: sustained.types.abs,
|
||||
Explosive: sustained.types.expl,
|
||||
Kinetic: sustained.types.kin,
|
||||
Thermic: sustained.types.therm,
|
||||
};
|
||||
|
||||
const opponentShields = Calc.shieldMetrics(opponent, 4);
|
||||
const opponentArmour = Calc.armourMetrics(opponent);
|
||||
const oppShield = ship.getOpponent().getShield();
|
||||
const shieldMults = {
|
||||
Absolute: 1,
|
||||
Explosive: oppShield.explosive.damageMultiplier,
|
||||
Kinetic: oppShield.kinetic.damageMultiplier,
|
||||
Thermic: oppShield.thermal.damageMultiplier,
|
||||
};
|
||||
|
||||
const timeToDrain = Calc.timeToDrainWep(ship, wep);
|
||||
const oppArmour = ship.getOpponent().getArmour();
|
||||
const armourMults = {
|
||||
Absolute: 1,
|
||||
Explosive: oppArmour.explosive.damageMultiplier,
|
||||
Kinetic: oppArmour.kinetic.damageMultiplier,
|
||||
Thermic: oppArmour.thermal.damageMultiplier,
|
||||
};
|
||||
|
||||
const weapons = sortBy(ship.getHardpoints(), (m) => m.get('distributordraw'));
|
||||
let rows = weapons.map((weapon) => {
|
||||
const sdps = weapon.get('sustaineddamagepersecond');
|
||||
const byRange = weapon.getRangeEffectiveness();
|
||||
const weaponPortions = {
|
||||
Absolute: weapon.get('absolutedamageportion'),
|
||||
Explosive: weapon.get('explosivedamageportion'),
|
||||
Kinetic: weapon.get('kineticdamageportion'),
|
||||
Thermic: weapon.get('thermicdamageportion'),
|
||||
};
|
||||
const baseSdpsTooltip = objToTooltip(
|
||||
translate,
|
||||
mapValues(weaponPortions, (p) => formats.f1(sdps * p)),
|
||||
);
|
||||
|
||||
let totalSEps = 0;
|
||||
let totalSDpsObject = { 'absolute': 0, 'explosive': 0, 'kinetic': 0, 'thermal': 0 };
|
||||
let shieldsSDpsObject = { 'absolute': 0, 'explosive': 0, 'kinetic': 0, 'thermal': 0 };
|
||||
let armourSDpsObject = { 'absolute': 0, 'explosive': 0, 'kinetic': 0, 'thermal': 0 };
|
||||
const bySys = oppShield.absolute.bySys;
|
||||
const shieldResEfts = mergeWith(
|
||||
clone(weaponPortions),
|
||||
shieldMults,
|
||||
(objV, srcV) => objV * srcV
|
||||
);
|
||||
const byShieldRes = sum(values(shieldResEfts));
|
||||
const shieldsSdpsTooltip = objToTooltip(
|
||||
translate,
|
||||
mapValues(
|
||||
shieldResEfts,
|
||||
(mult) => formats.f1(byRange * mult * bySys * sdps),
|
||||
),
|
||||
);
|
||||
const shieldsEftTooltip = objToTooltip(
|
||||
translate,
|
||||
{
|
||||
range: formats.pct1(byRange),
|
||||
resistance: formats.pct1(byShieldRes),
|
||||
'power distributor': formats.pct1(bySys),
|
||||
},
|
||||
);
|
||||
const shieldEft = byRange * byShieldRes * bySys;
|
||||
|
||||
const rows = [];
|
||||
for (let i = 0; i < damage.length; i++) {
|
||||
const weapon = damage[i];
|
||||
const byHardness = weapon.getArmourEffectiveness();
|
||||
const armourResEfts = mergeWith(
|
||||
clone(weaponPortions),
|
||||
armourMults,
|
||||
(objV, srcV) => objV * srcV,
|
||||
);
|
||||
const byArmourRes = sum(values(armourResEfts));
|
||||
const armourSdpsTooltip = objToTooltip(
|
||||
translate,
|
||||
mapValues(
|
||||
armourResEfts,
|
||||
(mult) => formats.f1(byRange * mult * byHardness * sdps)
|
||||
),
|
||||
);
|
||||
const armourEftTooltip = objToTooltip(
|
||||
translate,
|
||||
{
|
||||
range: formats.pct1(byRange),
|
||||
resistance: formats.pct1(byArmourRes),
|
||||
hardness: formats.pct1(byHardness),
|
||||
},
|
||||
);
|
||||
const armourEft = byRange * byArmourRes * byHardness;
|
||||
|
||||
totalSEps += weapon.seps;
|
||||
addSDps(totalSDpsObject, weapon.sdps.base);
|
||||
addSDps(shieldsSDpsObject, weapon.sdps.shields);
|
||||
addSDps(armourSDpsObject, weapon.sdps.armour);
|
||||
const bp = weapon.getBlueprint();
|
||||
const grade = weapon.getBlueprintGrade();
|
||||
const exp = weapon.getExperimental();
|
||||
let bpTitle = `${translate(bp)} ${translate('grade')} ${grade}`;
|
||||
if (exp) {
|
||||
bpTitle += `, ${translate(exp)}`;
|
||||
}
|
||||
return {
|
||||
slot: weapon.getSlot(),
|
||||
mount: weapon.mount,
|
||||
classRating: weapon.getClassRating(),
|
||||
type: weapon.readMeta('type'),
|
||||
bpTitle: bp ? ` (${bpTitle})` : null,
|
||||
sdps,
|
||||
baseSdpsTooltip,
|
||||
shieldSdps: sdps * shieldEft,
|
||||
shieldEft,
|
||||
shieldsSdpsTooltip,
|
||||
shieldsEftTooltip,
|
||||
armourSdps: sdps * armourEft,
|
||||
armourEft,
|
||||
armourSdpsTooltip,
|
||||
armourEftTooltip,
|
||||
};
|
||||
});
|
||||
|
||||
const baseSDpsTooltipDetails = getSDpsTooltip(translate, formats, weapon.sdps.base);
|
||||
|
||||
const effectivenessShieldsTooltipDetails = [];
|
||||
effectivenessShieldsTooltipDetails.push(<div key='range'>{translate('range') + ' ' + formats.pct1(weapon.effectiveness.shields.range)}</div>);
|
||||
effectivenessShieldsTooltipDetails.push(<div key='resistance'>{translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.shields.resistance)}</div>);
|
||||
effectivenessShieldsTooltipDetails.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(weapon.effectiveness.shields.sys)}</div>);
|
||||
|
||||
const effectiveShieldsSDpsTooltipDetails = getSDpsTooltip(translate, formats, weapon.sdps.armour);
|
||||
|
||||
const effectivenessArmourTooltipDetails = [];
|
||||
effectivenessArmourTooltipDetails.push(<div key='range'>{translate('range') + ' ' + formats.pct1(weapon.effectiveness.armour.range)}</div>);
|
||||
effectivenessArmourTooltipDetails.push(<div key='resistance'>{translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.armour.resistance)}</div>);
|
||||
effectivenessArmourTooltipDetails.push(<div key='hardness'>{translate('hardness') + ' ' + formats.pct1(weapon.effectiveness.armour.hardness)}</div>);
|
||||
|
||||
const effectiveArmourSDpsTooltipDetails = getSDpsTooltip(translate, formats, weapon.sdps.armour);
|
||||
|
||||
rows.push(
|
||||
<tr key={weapon.id}>
|
||||
<td className='ri'>
|
||||
{weapon.mount == 'F' ? <span onMouseOver={termtip.bind(null, 'fixed')} onMouseOut={tooltip.bind(null, null)}><MountFixed className='icon'/></span> : null}
|
||||
{weapon.mount == 'G' ? <span onMouseOver={termtip.bind(null, 'gimballed')} onMouseOut={tooltip.bind(null, null)}><MountGimballed /></span> : null}
|
||||
{weapon.mount == 'T' ? <span onMouseOver={termtip.bind(null, 'turreted')} onMouseOut={tooltip.bind(null, null)}><MountTurret /></span> : null}
|
||||
{weapon.classRating} {translate(weapon.name)}
|
||||
{weapon.engineering ? ' (' + weapon.engineering + ')' : null }
|
||||
</td>
|
||||
<td className='ri'><span onMouseOver={termtip.bind(null, baseSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.f1(weapon.sdps.base.total)}</span></td>
|
||||
<td className='ri'><span onMouseOver={termtip.bind(null, effectiveShieldsSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.f1(weapon.sdps.shields.total)}</span></td>
|
||||
<td className='ri'><span onMouseOver={termtip.bind(null, effectivenessShieldsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(weapon.effectiveness.shields.total)}</span></td>
|
||||
|
||||
<td className='ri'><span>{formats.f1(weapon.effectiveness.shields.dpe)}</span></td>
|
||||
|
||||
<td className='ri'><span onMouseOver={termtip.bind(null, effectiveArmourSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.f1(weapon.sdps.armour.total)}</span></td>
|
||||
<td className='ri'><span onMouseOver={termtip.bind(null, effectivenessArmourTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(weapon.effectiveness.armour.total)}</span></td>
|
||||
|
||||
<td className='ri'><span>{formats.f1(weapon.effectiveness.armour.dpe)}</span></td>
|
||||
</tr>);
|
||||
const { predicate, desc } = this.state;
|
||||
rows = sortBy(rows, (row) => row[predicate]);
|
||||
if (desc) {
|
||||
reverse(rows);
|
||||
}
|
||||
|
||||
const totalSDps = sumSDps(totalSDpsObject);
|
||||
const totalSDpsTooltipDetails = getSDpsTooltip(translate, formats, totalSDpsObject);
|
||||
const totalSDpsData = getSDpsData(translate, totalSDpsObject);
|
||||
const sdpsTooltip = objToTooltip(
|
||||
translate,
|
||||
mapValues(portions, (p) => formats.f1(sustained.dps * p)),
|
||||
);
|
||||
const sdpsPie = objToPie(
|
||||
translate,
|
||||
mapValues(portions, (p) => Math.round(sustained.dps * p)),
|
||||
);
|
||||
|
||||
const totalShieldsSDps = sumSDps(shieldsSDpsObject);
|
||||
const totalShieldsSDpsTooltipDetails = getSDpsTooltip(translate, formats, shieldsSDpsObject);
|
||||
const shieldsSDpsData = getSDpsData(translate, shieldsSDpsObject);
|
||||
const shieldSdpsSrcs = mergeWith(
|
||||
clone(portions),
|
||||
shieldMults,
|
||||
(objV, srcV) => sustained.dps * oppShield.absolute.bySys *
|
||||
rangeMultiplier * objV * srcV,
|
||||
);
|
||||
const shieldsSdps = sum(values(shieldSdpsSrcs));
|
||||
const shieldsSdpsTooltip = objToTooltip(
|
||||
translate,
|
||||
mapValues(shieldSdpsSrcs, (v) => formats.f1(v)),
|
||||
);
|
||||
const shieldsSdpsPie = objToPie(
|
||||
translate,
|
||||
mapValues(shieldSdpsSrcs, (v) => Math.round(v)),
|
||||
);
|
||||
|
||||
const totalArmourSDps = sumSDps(armourSDpsObject);
|
||||
const totalArmourSDpsTooltipDetails = getSDpsTooltip(translate, formats, armourSDpsObject);
|
||||
const armourSDpsData = getSDpsData(translate, armourSDpsObject);
|
||||
const armourSdpsSrcs = mergeWith(
|
||||
clone(portions),
|
||||
armourMults,
|
||||
(objV, srcV) => sustained.dps * hardnessMultiplier * rangeMultiplier *
|
||||
objV * srcV,
|
||||
);
|
||||
const armourSdps = sum(values(armourSdpsSrcs));
|
||||
const totalArmourSDpsTooltipDetails = objToTooltip(
|
||||
translate,
|
||||
mapValues(armourSdpsSrcs, (v) => formats.f1(v)),
|
||||
);
|
||||
const armourSDpsData = objToPie(
|
||||
translate,
|
||||
mapValues(armourSdpsSrcs, (v) => Math.round(v)),
|
||||
);
|
||||
|
||||
const timeToDepleteShields = Calc.timeToDeplete(opponentShields.total, totalShieldsSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
|
||||
const timeToDepleteArmour = Calc.timeToDeplete(opponentArmour.total, totalArmourSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
|
||||
const drainedPortions = {
|
||||
Absolute: drained.types.abs,
|
||||
Explosive: drained.types.expl,
|
||||
Kinetic: drained.types.kin,
|
||||
Thermic: drained.types.therm,
|
||||
};
|
||||
|
||||
// How much damage do we deal, before the capacitor is empty?
|
||||
const armourLeft = oppArmour.armour - (timeToDrain * armourSdps);
|
||||
// If we can't kill the enemy on one capacitor, factor in drained damage
|
||||
let timeToDepleteArmour;
|
||||
if (armourLeft > 0) {
|
||||
const effectiveDrainedDps = sum(values(mergeWith(
|
||||
clone(drainedPortions),
|
||||
armourMults,
|
||||
(objV, srcV) => objV * srcV,
|
||||
))) * drained.dps * rangeMultiplier *
|
||||
hardnessMultiplier;
|
||||
timeToDepleteArmour = effectiveDrainedDps === 0 ? Infinity :
|
||||
timeToDrain + (armourLeft / effectiveDrainedDps);
|
||||
} else {
|
||||
timeToDepleteArmour = oppArmour.armour / armourSdps;
|
||||
}
|
||||
|
||||
// How much damage do we deal, before the capacitor is empty?
|
||||
const shieldsLeft = oppShield.withSCBs - (timeToDrain * shieldsSdps);
|
||||
// If we can't kill the enemy on one capacitor, factor in drained damage
|
||||
let timeToDepleteShields;
|
||||
if (shieldsLeft > 0) {
|
||||
const effectiveDrainedDps = sum(values(mergeWith(
|
||||
clone(drainedPortions),
|
||||
shieldMults,
|
||||
(objV, srcV) => objV * srcV,
|
||||
))) * drained.dps * rangeMultiplier;
|
||||
timeToDepleteShields = effectiveDrainedDps === 0 ? Infinity :
|
||||
timeToDrain + (shieldsLeft / effectiveDrainedDps);
|
||||
} else {
|
||||
timeToDepleteShields = oppShield.withSCBs / shieldsSdps;
|
||||
}
|
||||
|
||||
return (
|
||||
<span id='offence'>
|
||||
@@ -274,34 +289,75 @@ export default class Offence extends TranslatedComponent {
|
||||
<table>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th rowSpan='2' className='sortable' onClick={sortOrder.bind(this, 'n')}>{translate('weapon')}</th>
|
||||
<th rowSpan='2' className='sortable' onClick={sortOrder.bind(this, 'classRating')}>{translate('weapon')}</th>
|
||||
<th colSpan='1'>{translate('overall')}</th>
|
||||
<th colSpan='3'>{translate('opponent\'s shields')}</th>
|
||||
<th colSpan='3'>{translate('opponent\'s armour')}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_SHIELDS')} onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'esdpss')}>{'sdps'}</th>
|
||||
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_SHIELDS')} onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'esdpss')}>{'sdps'}</th>
|
||||
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_SHIELDS')} onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'es')}>{'eft'}</th>
|
||||
|
||||
<th className='sortable'>{'dpe'}</th>
|
||||
|
||||
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_ARMOUR')} onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'esdpsh')}>{'sdps'}</th>
|
||||
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_ARMOUR')} onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'eh')}>{'eft'}</th>
|
||||
|
||||
<th className='sortable'>{'dpe'}</th>
|
||||
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_SHIELDS')}
|
||||
onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'sdps')}>sdps</th>
|
||||
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_SHIELDS')}
|
||||
onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'shieldSdps')}>sdps</th>
|
||||
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_SHIELDS')}
|
||||
onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'shieldEft')}>eft</th>
|
||||
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_ARMOUR')}
|
||||
onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'armourSdps')}>sdps</th>
|
||||
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_ARMOUR')}
|
||||
onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'armourEft')}>eft</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
{rows.map((row) => (
|
||||
<tr key={row.slot}>
|
||||
<td className='ri'>
|
||||
{row.mount == 'F' ? <span onMouseOver={termtip.bind(null, 'fixed')} onMouseOut={tooltip.bind(null, null)}><MountFixed className='icon'/></span> : null}
|
||||
{row.mount == 'G' ? <span onMouseOver={termtip.bind(null, 'gimballed')} onMouseOut={tooltip.bind(null, null)}><MountGimballed /></span> : null}
|
||||
{row.mount == 'T' ? <span onMouseOver={termtip.bind(null, 'turreted')} onMouseOut={tooltip.bind(null, null)}><MountTurret /></span> : null}
|
||||
{row.classRating} {translate(row.type)}
|
||||
{row.bpTitle}
|
||||
</td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, row.baseSdpsTooltip)}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>{formats.f1(row.sdps)}</span></td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, row.shieldsSdpsTooltip)}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>{formats.f1(row.shieldSdps)}</span></td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, row.shieldsEftTooltip)}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>{formats.pct1(row.shieldEft)}</span></td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, row.armourSdpsTooltip)}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>{formats.f1(row.armourSdps)}</span></td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, row.armourEftTooltip)}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>{formats.pct1(row.armourEft)}</span></td>
|
||||
</tr>
|
||||
))}
|
||||
{rows.length > 0 &&
|
||||
<tr>
|
||||
<td></td>
|
||||
<td className='ri'><span onMouseOver={termtip.bind(null, totalSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>={formats.f1(totalSDps)}</span></td>
|
||||
<td className='ri'><span onMouseOver={termtip.bind(null, totalShieldsSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>={formats.f1(totalShieldsSDps)}</span></td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, sdpsTooltip)} onMouseOut={tooltip.bind(null, null)}>
|
||||
={formats.f1(sustained.dps)}
|
||||
</span>
|
||||
</td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, shieldsSdpsTooltip)} onMouseOut={tooltip.bind(null, null)}>
|
||||
={formats.f1(shieldsSdps)}
|
||||
</span>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td className='ri'><span onMouseOver={termtip.bind(null, totalArmourSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>={formats.f1(totalArmourSDps)}</span></td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, totalArmourSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>
|
||||
={formats.f1(armourSdps)}
|
||||
</span>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
@@ -311,22 +367,51 @@ export default class Offence extends TranslatedComponent {
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<h2>{translate('offence metrics')}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_DRAIN_WEP'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_DRAIN_WEP')}<br/>{timeToDrain === Infinity ? translate('never') : formats.time(timeToDrain)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_EFFECTIVE_SDPS_SHIELDS'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_EFFECTIVE_SDPS_SHIELDS')}<br/>{formats.f1(totalShieldsSDps)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_SHIELDS'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}<br/>{timeToDepleteShields === Infinity ? translate('never') : formats.time(timeToDepleteShields)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_EFFECTIVE_SDPS_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_EFFECTIVE_SDPS_ARMOUR')}<br/>{formats.f1(totalArmourSDps)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}<br/>{timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_DRAIN_WEP'))}
|
||||
onMouseOut={tooltip.bind(null, null)}>
|
||||
{translate('PHRASE_TIME_TO_DRAIN_WEP')}<br/>
|
||||
{timeToDrain === Infinity ? translate('never') : formats.time(timeToDrain)}
|
||||
</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_EFFECTIVE_SDPS_SHIELDS'))}
|
||||
onMouseOut={tooltip.bind(null, null)}>
|
||||
{translate('PHRASE_EFFECTIVE_SDPS_SHIELDS')}<br/>
|
||||
{formats.f1(shieldsSdps)}
|
||||
</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_SHIELDS'))}
|
||||
onMouseOut={tooltip.bind(null, null)}>
|
||||
{translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}<br/>
|
||||
{timeToDepleteShields === Infinity ? translate('never') : formats.time(timeToDepleteShields)}
|
||||
</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_EFFECTIVE_SDPS_ARMOUR'))}
|
||||
onMouseOut={tooltip.bind(null, null)}>
|
||||
{translate('PHRASE_EFFECTIVE_SDPS_ARMOUR')}<br/>
|
||||
{formats.f1(armourSdps)}
|
||||
</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_ARMOUR'))}
|
||||
onMouseOut={tooltip.bind(null, null)}>
|
||||
{translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}<br/>
|
||||
{timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)}
|
||||
</h2>
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_OVERALL_DAMAGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('overall damage')}</h2>
|
||||
<PieChart data={totalSDpsData} />
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_OVERALL_DAMAGE'))}
|
||||
onMouseOut={tooltip.bind(null, null)}>
|
||||
{translate('overall damage')}
|
||||
</h2>
|
||||
<PieChart data={sdpsPie} />
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SHIELD_DAMAGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('shield damage sources')}</h2>
|
||||
<PieChart data={shieldsSDpsData} />
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SHIELD_DAMAGE'))}
|
||||
onMouseOut={tooltip.bind(null, null)}>
|
||||
{translate('shield damage sources')}
|
||||
</h2>
|
||||
<PieChart data={shieldsSdpsPie} />
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_ARMOUR_DAMAGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('armour damage sources')}</h2>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_ARMOUR_DAMAGE'))}
|
||||
onMouseOut={tooltip.bind(null, null)}>
|
||||
{translate('armour damage sources')}
|
||||
</h2>
|
||||
<PieChart data={armourSDpsData} />
|
||||
</div>
|
||||
</span>);
|
||||
|
||||
@@ -11,29 +11,24 @@ import Movement from './Movement';
|
||||
import Offence from './Offence';
|
||||
import Defence from './Defence';
|
||||
import WeaponDamageChart from './WeaponDamageChart';
|
||||
import Pips from '../components/Pips';
|
||||
import Boost from '../components/Boost';
|
||||
import Fuel from '../components/Fuel';
|
||||
import Cargo from '../components/Cargo';
|
||||
import ShipPicker from '../components/ShipPicker';
|
||||
import EngagementRange from '../components/EngagementRange';
|
||||
import autoBind from 'auto-bind';
|
||||
import { ShipProps } from 'ed-forge';
|
||||
const { CARGO_CAPACITY, FUEL_CAPACITY } = ShipProps;
|
||||
|
||||
/**
|
||||
* Outfitting subpages
|
||||
*/
|
||||
export default class OutfittingSubpages extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
ship: PropTypes.object.isRequired,
|
||||
code: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
buildName: PropTypes.string,
|
||||
sys: PropTypes.number.isRequired,
|
||||
eng: PropTypes.number.isRequired,
|
||||
wep: PropTypes.number.isRequired,
|
||||
cargo: PropTypes.number.isRequired,
|
||||
fuel: PropTypes.number.isRequired,
|
||||
boost: PropTypes.bool.isRequired,
|
||||
engagementRange: PropTypes.number.isRequired,
|
||||
opponent: PropTypes.object.isRequired,
|
||||
opponentBuild: PropTypes.string,
|
||||
opponentSys: PropTypes.number.isRequired,
|
||||
opponentEng: PropTypes.number.isRequired,
|
||||
opponentWep: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -42,13 +37,17 @@ export default class OutfittingSubpages extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._powerTab = this._powerTab.bind(this);
|
||||
this._profilesTab = this._profilesTab.bind(this);
|
||||
this._offenceTab = this._offenceTab.bind(this);
|
||||
this._defenceTab = this._defenceTab.bind(this);
|
||||
autoBind(this);
|
||||
|
||||
this.props.ship.setOpponent(this.props.ship);
|
||||
this.state = {
|
||||
boost: false,
|
||||
cargo: props.ship.get(CARGO_CAPACITY),
|
||||
fuel: props.ship.get(FUEL_CAPACITY),
|
||||
pips: props.ship.getDistributorSettingsObject(),
|
||||
tab: Persist.getOutfittingTab() || 'power',
|
||||
engagementRange: 1000,
|
||||
opponent: this.props.ship,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,128 +56,113 @@ export default class OutfittingSubpages extends TranslatedComponent {
|
||||
* @param {string} tab Tab name
|
||||
*/
|
||||
_showTab(tab) {
|
||||
Persist.setOutfittingTab(tab);
|
||||
this.setState({ tab });
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the power tab
|
||||
* @return {React.Component} Tab contents
|
||||
*/
|
||||
_powerTab() {
|
||||
let { ship, buildName, code, onChange } = this.props;
|
||||
Persist.setOutfittingTab('power');
|
||||
|
||||
const powerMarker = `${ship.toString()}`;
|
||||
const costMarker = `${ship.toString().split('.')[0]}`;
|
||||
|
||||
return <div>
|
||||
<PowerManagement ship={ship} code={powerMarker} onChange={onChange} />
|
||||
<CostSection ship={ship} buildName={buildName} code={costMarker} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the profiles tab
|
||||
* @return {React.Component} Tab contents
|
||||
*/
|
||||
_profilesTab() {
|
||||
const { ship, opponent, cargo, fuel, eng, boost, engagementRange, opponentSys } = this.props;
|
||||
const { translate } = this.context.language;
|
||||
let realBoost = boost && ship.canBoost(cargo, fuel);
|
||||
Persist.setOutfittingTab('profiles');
|
||||
|
||||
const engineProfileMarker = `${ship.toString()}:${cargo}:${fuel}:${eng}:${realBoost}`;
|
||||
const fsdProfileMarker = `${ship.toString()}:${cargo}:${fuel}`;
|
||||
const movementMarker = `${ship.topSpeed}:${ship.pitch}:${ship.roll}:${ship.yaw}:${ship.canBoost(cargo, fuel)}`;
|
||||
const damageMarker = `${ship.toString()}:${opponent.toString()}:${engagementRange}:${opponentSys}`;
|
||||
|
||||
return <div>
|
||||
<div className='group third'>
|
||||
<h1>{translate('engine profile')}</h1>
|
||||
<EngineProfile ship={ship} marker={engineProfileMarker} fuel={fuel} cargo={cargo} eng={eng} boost={realBoost} />
|
||||
</div>
|
||||
|
||||
<div className='group third'>
|
||||
<h1>{translate('fsd profile')}</h1>
|
||||
<FSDProfile ship={ship} marker={fsdProfileMarker} fuel={fuel} cargo={cargo} />
|
||||
</div>
|
||||
|
||||
<div className='group third'>
|
||||
<h1>{translate('movement profile')}</h1>
|
||||
<Movement marker={movementMarker} ship={ship} boost={boost} eng={eng} cargo={cargo} fuel={fuel} />
|
||||
</div>
|
||||
|
||||
<div className='group half'>
|
||||
<h1>{translate('damage to opponent\'s shields')}</h1>
|
||||
<WeaponDamageChart marker={damageMarker} ship={ship} opponent={opponent} opponentSys={opponentSys} hull={false} engagementRange={engagementRange} />
|
||||
</div>
|
||||
|
||||
<div className='group half'>
|
||||
<h1>{translate('damage to opponent\'s hull')}</h1>
|
||||
<WeaponDamageChart marker={damageMarker} ship={ship} opponent={opponent} opponentSys={opponentSys} hull={true} engagementRange={engagementRange} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the offence tab
|
||||
* @return {React.Component} Tab contents
|
||||
*/
|
||||
_offenceTab() {
|
||||
const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild, opponentSys } = this.props;
|
||||
Persist.setOutfittingTab('offence');
|
||||
|
||||
const marker = `${ship.toString()}${opponent.toString()}${opponentBuild}${engagementRange}${opponentSys}`;
|
||||
|
||||
return <div>
|
||||
<Offence marker={marker} ship={ship} opponent={opponent} wep={wep} opponentSys={opponentSys} engagementrange={engagementRange}/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the defence tab
|
||||
* @return {React.Component} Tab contents
|
||||
*/
|
||||
_defenceTab() {
|
||||
const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild, opponentWep } = this.props;
|
||||
Persist.setOutfittingTab('defence');
|
||||
|
||||
const marker = `${ship.toString()}${opponent.toString()}{opponentBuild}${engagementRange}${opponentWep}`;
|
||||
|
||||
return <div>
|
||||
<Defence marker={marker} ship={ship} opponent={opponent} sys={sys} opponentWep={opponentWep} engagementrange={engagementRange}/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the section
|
||||
* @return {React.Component} Contents
|
||||
*/
|
||||
render() {
|
||||
const tab = this.state.tab;
|
||||
const translate = this.context.language.translate;
|
||||
let tabSection;
|
||||
|
||||
switch (tab) {
|
||||
case 'power': tabSection = this._powerTab(); break;
|
||||
case 'profiles': tabSection = this._profilesTab(); break;
|
||||
case 'offence': tabSection = this._offenceTab(); break;
|
||||
case 'defence': tabSection = this._defenceTab(); break;
|
||||
}
|
||||
const { buildName, code, ship } = this.props;
|
||||
const { boost, cargo, fuel, pips, tab, engagementRange, opponent } = this.state;
|
||||
const { translate } = this.context.language;
|
||||
|
||||
const cargoCapacity = ship.get(CARGO_CAPACITY);
|
||||
const showCargoSlider = cargoCapacity > 0;
|
||||
return (
|
||||
<div>
|
||||
{/* Control of ship and opponent */}
|
||||
<div className="group quarter">
|
||||
<h2 style={{ verticalAlign: 'middle', textAlign: 'center' }}>
|
||||
{translate('ship control')}
|
||||
</h2>
|
||||
<Boost boost={boost} onChange={(boost) => this.setState({ boost })} />
|
||||
</div>
|
||||
<div className="group quarter">
|
||||
<h2 style={{ verticalAlign: 'middle', textAlign: 'center' }}>
|
||||
{translate('opponent')}
|
||||
</h2>
|
||||
<ShipPicker ship={ship} onChange={(opponent) => this.setState({ opponent })} />
|
||||
</div>
|
||||
<div className={cn('group', { quarter: showCargoSlider, half: !showCargoSlider })}>
|
||||
<Fuel fuelCapacity={ship.get(FUEL_CAPACITY)} fuel={fuel}
|
||||
onChange={(fuel) => this.setState({ fuel })} />
|
||||
</div>
|
||||
{showCargoSlider ?
|
||||
<div className="group quarter">
|
||||
<Cargo cargoCapacity={cargoCapacity} cargo={cargo}
|
||||
onChange={(cargo) => this.setState({ cargo })} />
|
||||
</div> : null}
|
||||
<div className="group half">
|
||||
<Pips ship={ship} pips={pips} onChange={(pips) => this.setState({ pips })} />
|
||||
</div>
|
||||
<div className="group half">
|
||||
<EngagementRange ship={ship} engagementRange={engagementRange}
|
||||
onChange={(engagementRange) => this.setState({ engagementRange })} />
|
||||
</div>
|
||||
<div className='group full' style={{ minHeight: '1000px' }}>
|
||||
<table className='tabs'>
|
||||
{/* Select tab section */}
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width:'25%' }} className={cn({ active: tab == 'power' })} onClick={this._showTab.bind(this, 'power')} >{translate('power and costs')}</th>
|
||||
<th style={{ width:'25%' }} className={cn({ active: tab == 'profiles' })} onClick={this._showTab.bind(this, 'profiles')} >{translate('profiles')}</th>
|
||||
<th style={{ width:'25%' }} className={cn({ active: tab == 'offence' })} onClick={this._showTab.bind(this, 'offence')} >{translate('offence')}</th>
|
||||
<th style={{ width:'25%' }} className={cn({ active: tab == 'defence' })} onClick={this._showTab.bind(this, 'defence')} >{translate('tab_defence')}</th>
|
||||
<th style={{ width:'25%' }} className={cn({ active: tab == 'power' })}
|
||||
onClick={this._showTab.bind(this, 'power')}>
|
||||
{translate('power and costs')}
|
||||
</th>
|
||||
<th style={{ width:'25%' }} className={cn({ active: tab == 'profiles' })}
|
||||
onClick={this._showTab.bind(this, 'profiles')}>
|
||||
{translate('profiles')}</th>
|
||||
<th style={{ width:'25%' }} className={cn({ active: tab == 'offence' })}
|
||||
onClick={this._showTab.bind(this, 'offence')}>
|
||||
{translate('offence')}
|
||||
</th>
|
||||
<th style={{ width:'25%' }} className={cn({ active: tab == 'defence' })}
|
||||
onClick={this._showTab.bind(this, 'defence')}>
|
||||
{translate('tab_defence')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
{tabSection}
|
||||
{/* Show selected tab */}
|
||||
{tab == 'power' ?
|
||||
<div>
|
||||
<PowerManagement ship={ship} code={code} />
|
||||
<CostSection ship={ship} buildName={buildName} code={code} />
|
||||
</div> : null}
|
||||
{tab == 'profiles' ?
|
||||
<div>
|
||||
<div className='group third'>
|
||||
<h1>{translate('engine profile')}</h1>
|
||||
<EngineProfile code={code} ship={ship} fuel={fuel} cargo={cargo} pips={pips} boost={boost} />
|
||||
</div>
|
||||
<div className='group third'>
|
||||
<h1>{translate('fsd profile')}</h1>
|
||||
<FSDProfile code={code} ship={ship} fuel={fuel} cargo={cargo} />
|
||||
</div>
|
||||
<div className='group third'>
|
||||
<h1>{translate('movement profile')}</h1>
|
||||
<Movement code={code} ship={ship} boost={boost} pips={pips} />
|
||||
</div>
|
||||
<div className='group half'>
|
||||
<h1>{translate('damage to opponent\'s shields')}</h1>
|
||||
<WeaponDamageChart code={code} ship={ship} opponentDefence={opponent.getShield()} engagementRange={engagementRange} />
|
||||
</div>
|
||||
<div className='group half'>
|
||||
<h1>{translate('damage to opponent\'s hull')}</h1>
|
||||
<WeaponDamageChart code={code} ship={ship} opponentDefence={opponent.getArmour()} engagementRange={engagementRange} />
|
||||
</div>
|
||||
</div> : null}
|
||||
{tab == 'offence' ?
|
||||
<div>
|
||||
<Offence code={code} ship={ship} opponent={opponent} engagementRange={engagementRange} />
|
||||
</div> : null}
|
||||
{tab == 'defence' ?
|
||||
<div>
|
||||
<Defence code={code} ship={ship} opponent={opponent} engagementRange={engagementRange} />
|
||||
</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ const LABEL_COLOUR = '#000000';
|
||||
* A pie chart
|
||||
*/
|
||||
export default class PieChart extends Component {
|
||||
|
||||
static propTypes = {
|
||||
data : PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Pip } from './SvgIcons';
|
||||
import { autoBind } from 'react-extras';
|
||||
import { Ship } from 'ed-forge';
|
||||
|
||||
/**
|
||||
* Pips displays SYS/ENG/WEP pips and allows users to change them with key presses by clicking on the relevant area.
|
||||
@@ -10,13 +11,9 @@ import { autoBind } from 'react-extras';
|
||||
*/
|
||||
export default class Pips extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
sys: PropTypes.number.isRequired,
|
||||
eng: PropTypes.number.isRequired,
|
||||
wep: PropTypes.number.isRequired,
|
||||
mcSys: PropTypes.number.isRequired,
|
||||
mcEng: PropTypes.number.isRequired,
|
||||
mcWep: PropTypes.number.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
ship: PropTypes.instanceOf(Ship).isRequired,
|
||||
pips: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -27,6 +24,12 @@ export default class Pips extends TranslatedComponent {
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
autoBind(this);
|
||||
|
||||
const { ship } = props;
|
||||
this._incSys = this._change(ship.incSys);
|
||||
this._incEng = this._change(ship.incEng);
|
||||
this._incWep = this._change(ship.incWep);
|
||||
this._reset = this._change(ship.pipsReset);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,153 +74,42 @@ export default class Pips extends TranslatedComponent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the capacitor
|
||||
*/
|
||||
_reset(isMc) {
|
||||
let { sys, eng, wep, mcSys, mcEng, mcWep } = this.props;
|
||||
if (isMc) {
|
||||
if (mcSys || mcEng || mcWep) {
|
||||
sys -= mcSys;
|
||||
eng -= mcEng;
|
||||
wep -= mcWep;
|
||||
this.props.onChange(sys, eng, wep, 0, 0, 0);
|
||||
}
|
||||
} else if (sys != 2 || eng != 2 || wep != 2) {
|
||||
sys = eng = wep = 2;
|
||||
this.props.onChange(sys + mcSys, eng + mcEng, wep + mcWep, mcSys, mcEng, mcWep);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the SYS capacitor
|
||||
*/
|
||||
_incSys() {
|
||||
this._inc('sys', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the ENG capacitor
|
||||
*/
|
||||
_incEng() {
|
||||
this._inc('eng', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the WEP capacitor
|
||||
*/
|
||||
_incWep() {
|
||||
this._inc('wep', false);
|
||||
}
|
||||
|
||||
_wrapMcClick(key) {
|
||||
return (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (key == 'rst') {
|
||||
this._reset(true);
|
||||
} else {
|
||||
this._inc(key, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases a given capacitor
|
||||
* @param {String} key Pip name to increase (one of 'sys', 'eng', 'wep')
|
||||
* Creates a function that handles pip assignment and call `onChance`.
|
||||
* @param {String} cb Callback that handles the actual pip assignment
|
||||
* @param {Boolean} isMc True when increase is by multi crew
|
||||
* @returns {Function} Function that handles pip assigment
|
||||
*/
|
||||
_inc(key, isMc) {
|
||||
if (!['sys', 'eng', 'wep'].includes(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { sys, eng, wep, mcSys, mcEng, mcWep } = this.props;
|
||||
let mc = key == 'sys' ? mcSys : (key == 'eng' ? mcEng : mcWep);
|
||||
let pips = this.props[key] - mc;
|
||||
let other1 = key == 'sys' ? eng - mcEng : sys - mcSys;
|
||||
let other2 = key == 'wep' ? eng - mcEng : wep - mcWep;
|
||||
|
||||
const required = Math.min(1, 4 - mc - pips);
|
||||
if (isMc) {
|
||||
// We can only set full pips in multi-crew also we can only set two pips
|
||||
if (required > 0.5 && mcSys + mcEng + mcWep < 2) {
|
||||
if (key == 'sys') {
|
||||
mcSys += 1;
|
||||
} else if (key == 'eng') {
|
||||
mcEng += 1;
|
||||
} else {
|
||||
mcWep += 1;
|
||||
}
|
||||
}
|
||||
} else if (required > 0) {
|
||||
if (required == 0.5) {
|
||||
// Take from whichever is larger
|
||||
if (other1 > other2) {
|
||||
other1 -= 0.5;
|
||||
} else {
|
||||
other2 -= 0.5;
|
||||
}
|
||||
pips += 0.5;
|
||||
} else {
|
||||
// Required is 1 - take from both if possible
|
||||
if (other1 == 0) {
|
||||
other2 -= 1;
|
||||
} else if (other2 == 0) {
|
||||
other1 -= 1;
|
||||
} else {
|
||||
other1 -= 0.5;
|
||||
other2 -= 0.5;
|
||||
}
|
||||
pips += 1;
|
||||
}
|
||||
}
|
||||
|
||||
sys = mcSys + (key == 'sys' ? pips : other1);
|
||||
eng = mcEng + (key == 'eng' ? pips : (key == 'sys' ? other1 : other2));
|
||||
wep = mcWep + (key == 'wep' ? pips : other2);
|
||||
this.props.onChange(sys, eng, wep, mcSys, mcEng, mcWep);
|
||||
_change(cb, isMc) {
|
||||
return () => {
|
||||
cb(isMc);
|
||||
this.props.onChange(this.props.ship.getDistributorSettingsObject());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the rendering for pips
|
||||
* @param {Number} sys the SYS pips
|
||||
* @param {Number} eng the ENG pips
|
||||
* @param {Number} wep the WEP pips
|
||||
* @param {Number} mcSys SYS pips from multi-crew
|
||||
* @param {Number} mcEng ENG pips from multi-crew
|
||||
* @param {Number} mcWep WEP pips from multi-crew
|
||||
* @returns {Object} Object containing the rendering for the pips
|
||||
*/
|
||||
_renderPips(sys, eng, wep, mcSys, mcEng, mcWep) {
|
||||
_renderPips() {
|
||||
const pipsSvg = {
|
||||
SYS: [],
|
||||
ENG: [],
|
||||
WEP: [],
|
||||
Sys: [],
|
||||
Eng: [],
|
||||
Wep: [],
|
||||
};
|
||||
|
||||
// Multi-crew pipsSettings actually are included in the overall pip count therefore
|
||||
// we can consider [0, sys - mcSys] as normal pipsSettings whilst [sys - mcSys, sys]
|
||||
// are the multi-crew pipsSettings in what follows.
|
||||
|
||||
let pipsSettings = {
|
||||
SYS: [sys, mcSys],
|
||||
ENG: [eng, mcEng],
|
||||
WEP: [wep, mcWep],
|
||||
};
|
||||
|
||||
for (let pipName in pipsSettings) {
|
||||
let [pips, mcPips] = pipsSettings[pipName];
|
||||
for (let i = 0; i < Math.floor(pips - mcPips); i++) {
|
||||
pipsSvg[pipName].push(<Pip key={i} className='full' />);
|
||||
for (let k in this.props.pips) {
|
||||
let { base, mc } = this.props.pips[k];
|
||||
for (let i = 0; i < Math.floor(base); i++) {
|
||||
pipsSvg[k].push(<Pip key={i} className='full' />);
|
||||
}
|
||||
if (pips > Math.floor(pips)) {
|
||||
pipsSvg[pipName].push(<Pip className='half' key={'half'} />);
|
||||
if (base > Math.floor(base)) {
|
||||
pipsSvg[k].push(<Pip className='half' key={'half'} />);
|
||||
}
|
||||
for (let i = pips - mcPips; i < Math.floor(pips); i++) {
|
||||
pipsSvg[pipName].push(<Pip key={i} className='mc' />);
|
||||
for (let i = 0; i < mc; i++) {
|
||||
pipsSvg[k].push(<Pip key={base + i} className='mc' />);
|
||||
}
|
||||
for (let i = Math.floor(pips + 0.5); i < 4; i++) {
|
||||
pipsSvg[pipName].push(<Pip className='empty' key={i} />);
|
||||
for (let i = Math.ceil(base + mc); i < 4; i++) {
|
||||
pipsSvg[k].push(<Pip className='empty' key={i} />);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,11 +121,10 @@ export default class Pips extends TranslatedComponent {
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = this.context.language;
|
||||
const { sys, eng, wep, mcSys, mcEng, mcWep } = this.props;
|
||||
const { ship } = this.props;
|
||||
const { translate } = this.context.language;
|
||||
|
||||
const pipsSvg = this._renderPips(sys, eng, wep, mcSys, mcEng, mcWep);
|
||||
const pipsSvg = this._renderPips();
|
||||
return (
|
||||
<span id='pips'>
|
||||
<table>
|
||||
@@ -241,38 +132,40 @@ export default class Pips extends TranslatedComponent {
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td className='clickable' onClick={() => this._inc('eng')}
|
||||
onContextMenu={this._wrapMcClick('eng')}>{pipsSvg['ENG']}</td>
|
||||
<td className='clickable' onClick={this._incEng}>{pipsSvg.Eng}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td className='clickable' onClick={this._incSys}
|
||||
onContextMenu={this._wrapMcClick('sys')}>{pipsSvg['SYS']}</td>
|
||||
<td className='clickable' onClick={this._incEng}
|
||||
onContextMenu={this._wrapMcClick('eng')}>{translate('ENG')}</td>
|
||||
<td className='clickable' onClick={this._incWep}
|
||||
onContextMenu={this._wrapMcClick('wep')}>{pipsSvg['WEP']}</td>
|
||||
<td className='clickable' onClick={this._incSys}>{pipsSvg.Sys}</td>
|
||||
<td className='clickable' onClick={this._incEng}>
|
||||
{translate('ENG')}
|
||||
</td>
|
||||
<td className='clickable' onClick={this._incWep}>{pipsSvg.Wep}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td className='clickable' onClick={this._incSys}
|
||||
onContextMenu={this._wrapMcClick('sys')}>{translate('SYS')}</td>
|
||||
<td className='clickable' onClick={this._reset.bind(this, false)}>
|
||||
<td className='clickable' onClick={this._incSys}>
|
||||
{translate('SYS')}
|
||||
</td>
|
||||
<td className='clickable' onClick={this._reset}>
|
||||
{translate('RST')}
|
||||
</td>
|
||||
<td className='clickable' onClick={this._incWep}
|
||||
onContextMenu={this._wrapMcClick('wep')}>{translate('WEP')}</td>
|
||||
<td className='clickable' onClick={this._incWep}>
|
||||
{translate('WEP')}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td className='clickable secondary' onClick={this._wrapMcClick('rst')}
|
||||
onMouseEnter={termtip.bind(null, 'PHRASE_MULTI_CREW_CAPACITOR_POINTS')}
|
||||
onMouseLeave={tooltip.bind(null, null)}>
|
||||
{translate('RST')}
|
||||
<td className='clickable' onClick={this._change(ship.incSys, true)}>
|
||||
<Pip className='mc' />
|
||||
</td>
|
||||
<td className='clickable' onClick={this._change(ship.incEng, true)}>
|
||||
<Pip className='mc' />
|
||||
</td>
|
||||
<td className='clickable' onClick={this._change(ship.incWep, true)}>
|
||||
<Pip className='mc' />
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -4,27 +4,25 @@ import * as d3 from 'd3';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||
import { Ship } from 'ed-forge';
|
||||
import { POWER_METRICS } from 'ed-forge/lib/src/ship-stats';
|
||||
import autoBind from 'auto-bind';
|
||||
|
||||
/**
|
||||
* Round to avoid floating point precision errors
|
||||
* Get the band-class.
|
||||
* @param {Boolean} selected Band selected
|
||||
* @param {number} sum Band power sum
|
||||
* @param {number} avail Total available power
|
||||
* @param {Number} relDraw Relative amount of power drawn by this band and
|
||||
* all prior
|
||||
* @return {string} CSS Class name
|
||||
*/
|
||||
function getClass(selected, sum, avail) {
|
||||
return selected ? 'secondary' : ((Math.round(sum * 100) / 100) >= avail) ? 'warning' : 'primary';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the # label for a Priority band
|
||||
* @param {number} val Priority Band Watt value
|
||||
* @param {number} index Priority Band index
|
||||
* @param {Function} wattScale Watt Scale function
|
||||
* @return {number} label / text
|
||||
*/
|
||||
function bandText(val, index, wattScale) {
|
||||
return (val > 0 && wattScale(val) > 13) ? index + 1 : null;
|
||||
function getClass(selected, relDraw) {
|
||||
if (selected) {
|
||||
return 'secondary';
|
||||
} else if (relDraw >= 1) {
|
||||
return 'warning';
|
||||
} else {
|
||||
return 'primary';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,10 +31,9 @@ function bandText(val, index, wattScale) {
|
||||
*/
|
||||
export default class PowerBands extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
bands: PropTypes.array.isRequired,
|
||||
available: PropTypes.number.isRequired,
|
||||
ship: PropTypes.instanceOf(Ship).isRequired,
|
||||
code: PropTypes.string.isRequired,
|
||||
width: PropTypes.number.isRequired,
|
||||
code: PropTypes.string,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -46,20 +43,16 @@ export default class PowerBands extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
autoBind(this);
|
||||
this.wattScale = d3.scaleLinear();
|
||||
this.pctScale = d3.scaleLinear().domain([0, 1]);
|
||||
this.wattAxis = d3.axisTop(this.wattScale).tickSizeOuter(0).tickFormat(context.language.formats.r2);
|
||||
this.pctAxis = d3.axisBottom(this.pctScale).tickSizeOuter(0).tickFormat(context.language.formats.rPct);
|
||||
|
||||
this._updateDimensions = this._updateDimensions.bind(this);
|
||||
this._updateScales = this._updateScales.bind(this);
|
||||
this._selectNone = this._selectNone.bind(this);
|
||||
this._hidetip = () => this.context.tooltip();
|
||||
|
||||
let maxBand = props.bands[props.bands.length - 1];
|
||||
|
||||
this.profile = props.ship.getMetrics(POWER_METRICS);
|
||||
this.state = {
|
||||
maxPwr: Math.max(props.available, maxBand.retractedSum, maxBand.deployedSum),
|
||||
ret: {},
|
||||
dep: {}
|
||||
};
|
||||
@@ -83,8 +76,6 @@ export default class PowerBands extends TranslatedComponent {
|
||||
let mRight = Math.round(140 * scale);
|
||||
let innerWidth = props.width - mLeft - mRight;
|
||||
|
||||
this._updateScales(innerWidth, this.state.maxPwr, props.available);
|
||||
|
||||
this.setState({
|
||||
barHeight,
|
||||
innerHeight,
|
||||
@@ -140,41 +131,67 @@ export default class PowerBands extends TranslatedComponent {
|
||||
this.setState({ dep: Object.assign({}, dep) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update scale
|
||||
* @param {number} innerWidth SVG innerwidth
|
||||
* @param {number} maxPwr Maximum power level MJ (deployed or available)
|
||||
* @param {number} available Available power MJ
|
||||
*/
|
||||
_updateScales(innerWidth, maxPwr, available) {
|
||||
this.wattScale.range([0, innerWidth]).domain([0, maxPwr]).clamp(true);
|
||||
this.pctScale.range([0, innerWidth]).domain([0, maxPwr / available]).clamp(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next context
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
let { innerWidth, maxPwr } = this.state;
|
||||
let { language, sizeRatio } = this.context;
|
||||
let maxBand = nextProps.bands[nextProps.bands.length - 1];
|
||||
let nextMaxPwr = Math.max(nextProps.available, maxBand.retractedSum, maxBand.deployedSum);
|
||||
|
||||
if (language !== nextContext.language) {
|
||||
this.wattAxis.tickFormat(nextContext.language.formats.r2);
|
||||
this.pctAxis.tickFormat(nextContext.language.formats.rPct);
|
||||
}
|
||||
|
||||
if (maxPwr != nextMaxPwr) { // Update Axes if max power has changed
|
||||
this._updateScales(innerWidth, nextMaxPwr, nextProps.available);
|
||||
this.setState({ maxPwr: nextMaxPwr });
|
||||
} else if (nextProps.width != this.props.width || sizeRatio != nextContext.sizeRatio) {
|
||||
if (nextProps.width != this.props.width || sizeRatio != nextContext.sizeRatio) {
|
||||
this._updateDimensions(nextProps, nextContext.sizeRatio);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble bands for relative consumption array.
|
||||
* @param {Number[]} consumed Array of relative-consumption numbers
|
||||
* @param {object} selected Object mapping selected bands to 1
|
||||
* @param {Number} yOffset Offset in y-direction of the bar
|
||||
* @param {Function} onClick onClick callback
|
||||
* @returns {React.Component} Bands
|
||||
*/
|
||||
_consumedToBands(consumed, selected, yOffset, onClick) {
|
||||
const { state, wattScale } = this;
|
||||
const bands = [];
|
||||
let consumesPrev = 0;
|
||||
for (let i = 0; i < consumed.length; i++) {
|
||||
consumesPrev = consumed[i - 1] || consumesPrev;
|
||||
const consumes = consumed[i];
|
||||
|
||||
if (!consumes) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bands.push(<rect
|
||||
key={'b' + i}
|
||||
width={Math.ceil(Math.max(wattScale(consumes - consumesPrev), 0))}
|
||||
height={state.barHeight}
|
||||
x={wattScale(consumesPrev)}
|
||||
y={yOffset + 1}
|
||||
onClick={onClick.bind(this, i)}
|
||||
className={getClass(selected[i], consumes)}
|
||||
/>);
|
||||
|
||||
bands.push(<text
|
||||
key={'t' + i}
|
||||
dy='0.5em'
|
||||
textAnchor='middle'
|
||||
height={state.barHeight}
|
||||
x={wattScale(consumesPrev) + (wattScale(consumes - consumesPrev) / 2)}
|
||||
y={yOffset + (state.barHeight / 2)}
|
||||
onClick={onClick.bind(this, i)}
|
||||
className='primary-bg'>{i + 1}</text>
|
||||
);
|
||||
}
|
||||
return bands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the power bands
|
||||
* @return {React.Component} Power bands
|
||||
@@ -184,78 +201,27 @@ export default class PowerBands extends TranslatedComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
let { wattScale, pctScale, context, props, state } = this;
|
||||
let { pctScale, context, props, state } = this;
|
||||
let { translate, formats } = context.language;
|
||||
let { f2, pct1 } = formats; // wattFmt, pctFmt
|
||||
let { available, bands } = props;
|
||||
let { innerWidth, ret, dep } = state;
|
||||
let pwrWarningClass = cn('threshold', { exceeded: bands[0].retractedSum > available * 0.4 });
|
||||
let deployed = [];
|
||||
let retracted = [];
|
||||
let { ship } = props;
|
||||
let { innerWidth, ret, dep, barHeight } = state;
|
||||
|
||||
let {
|
||||
consumed, generated, relativeConsumed, relativeConsumedRetracted
|
||||
} = ship.getMetrics(POWER_METRICS);
|
||||
let maxPwr = Math.max(consumed, generated);
|
||||
let retSum = relativeConsumedRetracted[relativeConsumedRetracted.length - 1];
|
||||
let depSum = relativeConsumed[relativeConsumed.length - 1];
|
||||
|
||||
this.wattScale.range([0, innerWidth]).domain([0, 1]).clamp(true);
|
||||
this.pctScale.range([0, innerWidth]).domain([0, maxPwr / generated]).clamp(true);
|
||||
|
||||
let pwrWarningClass = cn('threshold', { exceeded: retSum > generated * 0.4 });
|
||||
let retracted = this._consumedToBands(relativeConsumedRetracted, ret, 0, this._selectRet);
|
||||
let deployed = this._consumedToBands(relativeConsumed, dep, barHeight, this._selectDep);
|
||||
let retSelected = Object.keys(ret).length > 0;
|
||||
let depSelected = Object.keys(dep).length > 0;
|
||||
let retSum = 0;
|
||||
let depSum = 0;
|
||||
|
||||
for (let i = 0; i < bands.length; i++) {
|
||||
let b = bands[i];
|
||||
retSum += (!retSelected || ret[i]) ? b.retracted : 0;
|
||||
depSum += (!depSelected || dep[i]) ? b.deployed + b.retracted : 0;
|
||||
|
||||
if (b.retracted > 0) {
|
||||
let retLbl = bandText(b.retracted, i, wattScale);
|
||||
|
||||
retracted.push(<rect
|
||||
key={'rB' + i}
|
||||
width={Math.ceil(Math.max(wattScale(b.retracted), 0))}
|
||||
height={state.barHeight}
|
||||
x={Math.floor(Math.max(wattScale(b.retractedSum) - wattScale(b.retracted), 0))}
|
||||
y={1}
|
||||
onClick={this._selectRet.bind(this, i)}
|
||||
className={getClass(ret[i], b.retractedSum, available)}
|
||||
/>);
|
||||
|
||||
if (retLbl) {
|
||||
retracted.push(<text
|
||||
key={'rT' + i}
|
||||
dy='0.5em'
|
||||
textAnchor='middle'
|
||||
height={state.barHeight}
|
||||
x={wattScale(b.retractedSum) - (wattScale(b.retracted) / 2)}
|
||||
y={state.retY}
|
||||
onClick={this._selectRet.bind(this, i)}
|
||||
className='primary-bg'>{retLbl}</text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (b.retracted > 0 || b.deployed > 0) {
|
||||
let depLbl = bandText(b.deployed + b.retracted, i, wattScale);
|
||||
|
||||
deployed.push(<rect
|
||||
key={'dB' + i}
|
||||
width={Math.ceil(Math.max(wattScale(b.deployed + b.retracted), 0))}
|
||||
height={state.barHeight}
|
||||
x={Math.floor(Math.max(wattScale(b.deployedSum) - wattScale(b.retracted) - wattScale(b.deployed), 0))}
|
||||
y={state.barHeight + 1}
|
||||
onClick={this._selectDep.bind(this, i)}
|
||||
className={getClass(dep[i], b.deployedSum, available)}
|
||||
/>);
|
||||
|
||||
if (depLbl) {
|
||||
deployed.push(<text
|
||||
key={'dT' + i}
|
||||
dy='0.5em'
|
||||
textAnchor='middle'
|
||||
height={state.barHeight}
|
||||
x={wattScale(b.deployedSum) - ((wattScale(b.retracted) + wattScale(b.deployed)) / 2)}
|
||||
y={state.depY}
|
||||
onClick={this._selectDep.bind(this, i)}
|
||||
className='primary-bg'>{depLbl}</text>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<svg style={{ marginTop: '1em', width: '100%', height: state.height }} onContextMenu={wrapCtxMenu(this._selectNone)}>
|
||||
@@ -271,8 +237,8 @@ export default class PowerBands extends TranslatedComponent {
|
||||
<line x1={pctScale(0.4)} x2={pctScale(0.4)} y1='0' y2={state.innerHeight} className={pwrWarningClass} />
|
||||
<text dy='0.5em' x='-3' y={state.retY} className='primary upp' textAnchor='end' onMouseOver={this.context.termtip.bind(null, 'retracted')} onMouseLeave={this._hidetip}>{translate('ret')}</text>
|
||||
<text dy='0.5em' x='-3' y={state.depY} className='primary upp' textAnchor='end' onMouseOver={this.context.termtip.bind(null, 'deployed', { orientation: 's', cap: 1 })} onMouseLeave={this._hidetip}>{translate('dep')}</text>
|
||||
<text dy='0.5em' x={innerWidth + 5} y={state.retY} className={getClass(retSelected, retSum, available)}>{f2(Math.max(0, retSum))} ({pct1(Math.max(0, retSum / available))})</text>
|
||||
<text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, available)}>{f2(Math.max(0, depSum))} ({pct1(Math.max(0, depSum / available))})</text>
|
||||
<text dy='0.5em' x={innerWidth + 5} y={state.retY} className={getClass(retSelected, retSum, generated)}>{f2(Math.max(0, retSum * generated))} ({pct1(Math.max(0, retSum))})</text>
|
||||
<text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, generated)}>{f2(Math.max(0, depSum * generated))} ({pct1(Math.max(0, depSum))})</text>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -3,24 +3,49 @@ import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import PowerBands from './PowerBands';
|
||||
import { slotName, slotComparator } from '../utils/SlotFunctions';
|
||||
import { Power, NoPower } from './SvgIcons';
|
||||
import autoBind from 'auto-bind';
|
||||
import { Ship, Module } from 'ed-forge';
|
||||
|
||||
const POWER = [
|
||||
null,
|
||||
null,
|
||||
<NoPower className='icon warning' />,
|
||||
<Power className='secondary-disabled' />
|
||||
];
|
||||
/**
|
||||
* Makes a comparison based on the order `false < undefined < true` (fut) and
|
||||
* maps it to `[-1, 0, 1]`.
|
||||
* @param {boolean} a Bool or undefined
|
||||
* @param {boolean} b Bool or undefined
|
||||
* @returns {number} Comparison
|
||||
*/
|
||||
function futComp(a, b) {
|
||||
switch (a) {
|
||||
case false: return (b === false ? 0 : 1);
|
||||
// The next else-expression maps false to -1 and true to 1
|
||||
case undefined: return (b === undefined ? 0 : 2 * Number(b) - 1);
|
||||
case true: return (b === true ? 0 : -1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enabled-icon.
|
||||
* @param {boolean} enabled Is the module enabled?
|
||||
* @returns {React.Component} Enabled icon.
|
||||
*/
|
||||
function getPowerIcon(enabled) {
|
||||
if (enabled === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (enabled) {
|
||||
return <Power className='secondary-disabled' />;
|
||||
} else {
|
||||
return <NoPower className='icon warning' />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Power Management Section
|
||||
*/
|
||||
export default class PowerManagement extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
ship: PropTypes.object.isRequired,
|
||||
ship: PropTypes.instanceOf(Ship).isRequired,
|
||||
code: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -29,19 +54,17 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._renderPowerRows = this._renderPowerRows.bind(this);
|
||||
this._updateWidth = this._updateWidth.bind(this);
|
||||
this._sort = this._sort.bind(this);
|
||||
autoBind(this);
|
||||
|
||||
this.state = {
|
||||
predicate: 'pwr',
|
||||
desc: false,
|
||||
desc: true,
|
||||
width: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sort order and sort
|
||||
* Set the sort order
|
||||
* @param {string} predicate Sort predicate
|
||||
*/
|
||||
_sortOrder(predicate) {
|
||||
@@ -53,50 +76,51 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
desc = true;
|
||||
}
|
||||
|
||||
this._sort(this.props.ship, predicate, desc);
|
||||
this.setState({ predicate, desc });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the power list
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {string} predicate Sort predicate
|
||||
* @param {Boolean} desc Sort order descending
|
||||
* @param {Module[]} modules Modules to sort
|
||||
* @returns {Module[]} Sorted modules
|
||||
*/
|
||||
_sort(ship, predicate, desc) {
|
||||
let powerList = ship.powerList;
|
||||
let comp = slotComparator.bind(null, this.context.language.translate);
|
||||
|
||||
_sortAndFilter(modules) {
|
||||
modules = modules.filter((m) => m.get('powerdraw') >= 0);
|
||||
let { translate } = this.context.language;
|
||||
const { predicate, desc } = this.state;
|
||||
let comp;
|
||||
switch (predicate) {
|
||||
case 'n': comp = comp(null, desc); break;
|
||||
case 't': comp = comp((a, b) => a.type.localeCompare(b.type), desc); break;
|
||||
case 'pri': comp = comp((a, b) => a.priority - b.priority, desc); break;
|
||||
case 'pwr': comp = comp((a, b) => a.m.getPowerUsage() - b.m.getPowerUsage(), desc); break;
|
||||
case 'r': comp = comp((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b), desc); break;
|
||||
case 'd': comp = comp((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true), desc); break;
|
||||
case 'n': comp = (a, b) => translate(a.readMeta('type')).localeCompare(
|
||||
translate(b.readMeta('type'))
|
||||
); break;
|
||||
// case 't': comp = comp((a, b) => a.type.localeCompare(b.type), desc); break;
|
||||
case 'pri': comp = (a, b) => a.getPowerPriority() - b.getPowerPriority(); break;
|
||||
case 'pwr': comp = (a, b) => a.get('powerdraw') - b.get('powerdraw'); break;
|
||||
case 'r': comp = (a, b) => futComp(a.isPowered().retracted, b.isPowered().retracted); break;
|
||||
case 'd': comp = (a, b) => futComp(a.isPowered().deployed, b.isPowered().deployed); break;
|
||||
}
|
||||
|
||||
powerList.sort(comp);
|
||||
modules.sort(comp);
|
||||
if (desc) {
|
||||
modules.reverse();
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update slot priority
|
||||
* @param {Object} slot Slot model
|
||||
* @param {number} inc increment / decrement
|
||||
* Creates a callback that changes the power priority for the given module
|
||||
* based on the given delta.
|
||||
* @param {Module} m Module to set the priority for
|
||||
* @param {Number} delta Delta to set
|
||||
* @returns {Function} Callback
|
||||
*/
|
||||
_priority(slot, inc) {
|
||||
if (this.props.ship.setSlotPriority(slot, slot.priority + inc)) {
|
||||
this.props.onChange();
|
||||
_prioCb(m, delta) {
|
||||
return () => {
|
||||
const prio = m.getPowerPriority();
|
||||
const newPrio = Math.max(0, prio + delta);
|
||||
if (0 <= newPrio) {
|
||||
m.setPowerPriority(newPrio);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle slot active/inactive
|
||||
* @param {Object} slot Slot model
|
||||
*/
|
||||
_toggleEnabled(slot) {
|
||||
this.props.ship.setSlotEnabled(slot, !slot.enabled);
|
||||
this.props.onChange();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,37 +134,36 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
_renderPowerRows(ship, translate, pwr, pct) {
|
||||
let powerRows = [];
|
||||
|
||||
for (let i = 0, l = ship.powerList.length; i < l; i++) {
|
||||
let slot = ship.powerList[i];
|
||||
|
||||
if (slot.m && slot.m.getPowerUsage() > 0) {
|
||||
let m = slot.m;
|
||||
let toggleEnabled = this._toggleEnabled.bind(this, slot);
|
||||
let modules = this._sortAndFilter(ship.getModules());
|
||||
for (let m of modules) {
|
||||
let retractedElem = null, deployedElem = null;
|
||||
|
||||
if (slot.enabled) {
|
||||
retractedElem = <td className='ptr upp' onClick={toggleEnabled}>{POWER[ship.getSlotStatus(slot, false)]}</td>;
|
||||
deployedElem = <td className='ptr upp' onClick={toggleEnabled}>{POWER[ship.getSlotStatus(slot, true)]}</td>;
|
||||
const flipEnabled = () => m.setEnabled();
|
||||
if (m.isEnabled()) {
|
||||
let powered = m.isPowered();
|
||||
retractedElem = <td className='ptr upp' onClick={flipEnabled}>{getPowerIcon(powered.retracted)}</td>;
|
||||
deployedElem = <td className='ptr upp' onClick={flipEnabled}>{getPowerIcon(powered.deployed)}</td>;
|
||||
} else {
|
||||
retractedElem = <td className='ptr disabled upp' colSpan='2' onClick={toggleEnabled}>{translate('disabled')}</td>;
|
||||
retractedElem = <td className='ptr disabled upp' colSpan='2' onClick={flipEnabled}>{translate('disabled')}</td>;
|
||||
}
|
||||
|
||||
powerRows.push(<tr key={i} className={cn('highlight', { disabled: !slot.enabled })}>
|
||||
<td className='ptr' style={{ width: '1em' }} onClick={toggleEnabled}>{m.class + m.rating}</td>
|
||||
<td className='ptr le shorten cap' onClick={toggleEnabled}>{slotName(translate, slot)}</td>
|
||||
<td className='ptr' onClick={toggleEnabled}><u>{translate(slot.type)}</u></td>
|
||||
const slot = m.getSlot();
|
||||
powerRows.push(<tr key={slot} className={cn('highlight', { disabled: !m.isEnabled() })}>
|
||||
<td className='ptr' style={{ width: '1em' }} onClick={flipEnabled}>{String(m.getClass()) + m.getRating()}</td>
|
||||
<td className='ptr le shorten cap' onClick={flipEnabled}>{translate(m.readMeta('type'))}</td>
|
||||
{/* <td className='ptr' onClick={flipEnabled}><u>{translate(slot.type)}</u></td> */}
|
||||
<td>
|
||||
<span className='flip ptr btn' onClick={this._priority.bind(this, slot, -1)}>►</span>
|
||||
{' ' + (slot.priority + 1) + ' '}
|
||||
<span className='ptr btn' onClick={this._priority.bind(this, slot, 1)}>►</span>
|
||||
<span className='flip ptr btn' onClick={this._prioCb(m, -1)}>►</span>
|
||||
{' ' + (m.getPowerPriority() + 1) + ' '}
|
||||
<span className='ptr btn' onClick={this._prioCb(m, 1)}>►</span>
|
||||
</td>
|
||||
<td className='ri ptr' style={{ width: '3.25em' }} onClick={flipEnabled}>{pwr(m.get('powerdraw'))}</td>
|
||||
<td className='ri ptr' style={{ width: '3em' }} onClick={flipEnabled}>
|
||||
<u>{pct(m.get('powerdraw') / ship.getPowerPlant().get('powercapacity'))}</u>
|
||||
</td>
|
||||
<td className='ri ptr' style={{ width: '3.25em' }} onClick={toggleEnabled}>{pwr(m.getPowerUsage())}</td>
|
||||
<td className='ri ptr' style={{ width: '3em' }} onClick={toggleEnabled}><u>{pct(m.getPowerUsage() / ship.powerAvailable)}</u></td>
|
||||
{retractedElem}
|
||||
{deployedElem}
|
||||
</tr>);
|
||||
}
|
||||
}
|
||||
return powerRows;
|
||||
}
|
||||
|
||||
@@ -155,7 +178,6 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
* Add listeners when about to mount and sort power list
|
||||
*/
|
||||
componentWillMount() {
|
||||
this._sort(this.props.ship, this.state.predicate, this.state.desc);
|
||||
this.resizeListener = this.context.onWindowResize(this._updateWidth);
|
||||
}
|
||||
|
||||
@@ -166,17 +188,6 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
this._updateWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort power list if the ship instance has changed
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextState Incoming/Next state
|
||||
*/
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
if (this.props.ship != nextProps.ship) {
|
||||
this._sort(nextProps.ship, nextState.predicate, nextState.desc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove listeners on unmount
|
||||
*/
|
||||
@@ -191,39 +202,38 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
render() {
|
||||
let { ship, code } = this.props;
|
||||
let { translate, formats } = this.context.language;
|
||||
let pwr = formats.f2;
|
||||
let pp = ship.standard[0].m;
|
||||
let sortOrder = this._sortOrder;
|
||||
let pp = ship.getPowerPlant();
|
||||
|
||||
return (
|
||||
<div ref={node => this.node = node} className='group half' id='componentPriority'>
|
||||
<table style={{ width: '100%' }}>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th colSpan='2' className='sortable le' onClick={sortOrder.bind(this, 'n')} >{translate('module')}</th>
|
||||
<th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 't')} >{translate('type')}</th>
|
||||
<th style={{ width: '4em' }} className='sortable' onClick={sortOrder.bind(this, 'pri')} >{translate('pri')}</th>
|
||||
<th colSpan='2' className='sortable' onClick={sortOrder.bind(this, 'pwr')} >{translate('PWR')}</th>
|
||||
<th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 'r')} >{translate('ret')}</th>
|
||||
<th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 'd')} >{translate('dep')}</th>
|
||||
<th colSpan='2' className='sortable le' onClick={() => this._sortOrder('n')} >{translate('module')}</th>
|
||||
{/* <th style={{ width: '3em' }} className='sortable' onClick={() => this._sortOrder('t')} >{translate('type')}</th> */}
|
||||
<th style={{ width: '4em' }} className='sortable' onClick={() => this._sortOrder('pri')} >{translate('pri')}</th>
|
||||
<th colSpan='2' className='sortable' onClick={() => this._sortOrder('pwr')} >{translate('PWR')}</th>
|
||||
<th style={{ width: '3em' }} className='sortable' onClick={() => this._sortOrder('r')} >{translate('ret')}</th>
|
||||
<th style={{ width: '3em' }} className='sortable' onClick={() => this._sortOrder('d')} >{translate('dep')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{pp.class + pp.rating}</td>
|
||||
<td>{String(pp.getClass()) + pp.getRating()}</td>
|
||||
<td className='le shorten cap' >{translate('pp')}</td>
|
||||
<td><u >{translate('SYS')}</u></td>
|
||||
<td>1</td>
|
||||
<td className='ri'>{pwr(pp.getPowerGeneration())}</td>
|
||||
<td className='ri'>{formats.f2(pp.get('powercapacity'))}</td>
|
||||
<td className='ri'><u>100%</u></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr><td style={{ lineHeight:0 }} colSpan='8'><hr style={{ margin: '0 0 3px', background: '#ff8c0d', border: 0, height: 1 }} /></td></tr>
|
||||
{this._renderPowerRows(ship, translate, pwr, formats.pct1)}
|
||||
<tr><td style={{ lineHeight:0 }} colSpan='8'>
|
||||
<hr style={{ margin: '0 0 3px', background: '#ff8c0d', border: 0, height: 1 }} />
|
||||
</td></tr>
|
||||
{this._renderPowerRows(ship, translate, formats.f2, formats.pct1)}
|
||||
</tbody>
|
||||
</table>
|
||||
<PowerBands width={this.state.width} code={code} available={pp.getPowerGeneration()} bands={ship.priorityBands} />
|
||||
<PowerBands width={this.state.width} ship={ship} code={code} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import { Rocket } from './SvgIcons';
|
||||
import Persist from '../stores/Persist';
|
||||
import cn from 'classnames';
|
||||
import { Factory, Ship } from 'ed-forge';
|
||||
import autoBind from 'auto-bind';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
/**
|
||||
* Ship picker
|
||||
@@ -13,40 +15,49 @@ import cn from 'classnames';
|
||||
export default class ShipPicker extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
ship: PropTypes.string.isRequired,
|
||||
build: PropTypes.string
|
||||
ship: PropTypes.instanceOf(Ship).isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
ship: 'eagle'
|
||||
}
|
||||
|
||||
/**
|
||||
* constructor
|
||||
* @param {object} props Properties react
|
||||
* @param {object} context react context
|
||||
*/
|
||||
constructor(props, context) { // eslint-disable-line
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
this.shipOrder = Object.keys(Ships).sort();
|
||||
this._toggleMenu = this._toggleMenu.bind(this);
|
||||
this._closeMenu = this._closeMenu.bind(this);
|
||||
|
||||
this.state = { menuOpen: false };
|
||||
autoBind(this);
|
||||
this.state = {
|
||||
menuOpen: false,
|
||||
opponent: {
|
||||
self: true,
|
||||
type: props.ship.getShipType(),
|
||||
stock: false,
|
||||
id: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ship
|
||||
* @param {object} ship the ship
|
||||
* @param {string} build the build, if present
|
||||
* @param {boolean} self True to compare with ship itself
|
||||
* @param {object} type The ship type
|
||||
* @param {boolean} stock True to compare with a stock version of given type
|
||||
* @param {string} id The build's stored ID
|
||||
*/
|
||||
_shipChange(ship, build) {
|
||||
this._closeMenu();
|
||||
|
||||
// Ensure that the ship has changed
|
||||
if (ship !== this.props.ship || build !== this.props.build) {
|
||||
this.props.onChange(ship, build);
|
||||
_shipChange(self, type, stock = false, id = null) {
|
||||
const opponent = { self, type, stock, id };
|
||||
if (isEqual(opponent, this.state.opponent)) {
|
||||
this.setState({ menuOpen: false });
|
||||
} else {
|
||||
const { onChange } = this.props;
|
||||
if (self) {
|
||||
onChange(this.props.ship);
|
||||
} else if (stock) {
|
||||
onChange(Factory.newShip(type));
|
||||
} else {
|
||||
onChange(new Ship(Persist.getBuild(type, id)));
|
||||
}
|
||||
this.setState({ menuOpen: false, opponent });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,26 +66,41 @@ export default class ShipPicker extends TranslatedComponent {
|
||||
* @returns {object} the picker menu
|
||||
*/
|
||||
_renderPickerMenu() {
|
||||
const { ship, build } = this.props;
|
||||
const _shipChange = this._shipChange;
|
||||
const builds = Persist.getBuilds();
|
||||
const buildList = [];
|
||||
for (let shipId of this.shipOrder) {
|
||||
const shipBuilds = [];
|
||||
// Add stock build
|
||||
const stockSelected = (ship == shipId && !build);
|
||||
shipBuilds.push(<li key={shipId} className={ cn({ 'selected': stockSelected })} onClick={_shipChange.bind(this, shipId, null)}>Stock</li>);
|
||||
if (builds[shipId]) {
|
||||
let buildNameOrder = Object.keys(builds[shipId]).sort();
|
||||
for (let buildName of buildNameOrder) {
|
||||
const buildSelected = ship === shipId && build === buildName;
|
||||
shipBuilds.push(<li key={shipId + '-' + buildName} className={ cn({ 'selected': buildSelected })} onClick={_shipChange.bind(this, shipId, buildName)}>{buildName}</li>);
|
||||
}
|
||||
}
|
||||
buildList.push(<ul key={shipId} className='block'>{Ships[shipId].properties.name}{shipBuilds}</ul>);
|
||||
const { menuOpen } = this.state;
|
||||
if (!menuOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buildList;
|
||||
const { translate } = this.context.language;
|
||||
const { self, type, stock, id } = this.state.opponent;
|
||||
return <div className='menu-list' onClick={(e) => e.stopPropagation()}>
|
||||
<div className='quad'>
|
||||
{Factory.getAllShipTypes().sort().map((shipType) =>
|
||||
<ul key={shipType} className='block'>
|
||||
{translate(shipType)}
|
||||
{/* Add stock build */}
|
||||
<li key={shipType}
|
||||
onClick={this._shipChange.bind(this, false, shipType, true)}
|
||||
className={cn({ selected: stock && type === shipType })}>
|
||||
{translate('stock')}
|
||||
</li>
|
||||
{Persist.getBuildsNamesFor(shipType).sort().map((storedId) =>
|
||||
<li key={`${shipType}-${storedId}`}
|
||||
onClick={this._shipChange.bind(this, false, shipType, false, storedId)}
|
||||
className={ cn({ selected: type === shipType && id === storedId })}>
|
||||
{storedId}
|
||||
</li>)}
|
||||
{/* Add ship itself */}
|
||||
{(this.props.ship.getShipType() === shipType ?
|
||||
<li key='self'
|
||||
onClick={this._shipChange.bind(this, true, shipType)}
|
||||
className={cn({ selected: self })}>
|
||||
{translate('THIS_SHIP')}
|
||||
</li> :
|
||||
null)}
|
||||
</ul>)}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,41 +111,36 @@ export default class ShipPicker extends TranslatedComponent {
|
||||
this.setState({ menuOpen: !menuOpen });
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the menu
|
||||
*/
|
||||
_closeMenu() {
|
||||
const { menuOpen } = this.state;
|
||||
if (menuOpen) {
|
||||
this._toggleMenu();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render picker
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { ship, build } = this.props;
|
||||
const { translate } = this.context.language;
|
||||
const { ship } = this.props;
|
||||
const { menuOpen } = this.state;
|
||||
const { self, type, stock, id } = this.state.opponent;
|
||||
|
||||
let label;
|
||||
if (self) {
|
||||
label = translate('THIS_SHIP');
|
||||
} else if (stock) {
|
||||
label = translate('stock');
|
||||
} else {
|
||||
label = id;
|
||||
}
|
||||
|
||||
const shipString = ship + ': ' + (build ? build : translate('stock'));
|
||||
return (
|
||||
<div className='shippicker' onClick={ (e) => e.stopPropagation() }>
|
||||
<div className='menu'>
|
||||
<div className={cn('menu-header', { selected: menuOpen })} onClick={this._toggleMenu}>
|
||||
<span><Rocket className='warning' /></span>
|
||||
<span className='menu-item-label'>{shipString}</span>
|
||||
<span className='menu-item-label'>
|
||||
{`${translate(type)}: ${label}`}
|
||||
</span>
|
||||
</div>
|
||||
{ menuOpen ?
|
||||
<div className='menu-list' onClick={ (e) => e.stopPropagation() }>
|
||||
<div className='quad'>
|
||||
{this._renderPickerMenu()}
|
||||
</div>
|
||||
</div> : null }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import autoBind from 'auto-bind';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import cn from 'classnames';
|
||||
import { Warning } from './SvgIcons';
|
||||
import * as Calc from '../shipyard/Calculations';
|
||||
|
||||
import { ShipProps } from 'ed-forge';
|
||||
import { BOOST_INTERVAL, MINIMUM_MASS } from 'ed-forge/lib/src/ship-stats';
|
||||
const {
|
||||
SPEED, BOOST_SPEED, DAMAGE_METRICS, JUMP_METRICS, SHIELD_METRICS,
|
||||
ARMOUR_METRICS, CARGO_CAPACITY, FUEL_CAPACITY, UNLADEN_MASS, LADEN_MASS,
|
||||
MODULE_PROTECTION_METRICS, PASSENGER_CAPACITY
|
||||
} = ShipProps;
|
||||
|
||||
/**
|
||||
* Ship Summary Table / Stats
|
||||
*/
|
||||
export default class ShipSummaryTable extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
ship: PropTypes.object.isRequired,
|
||||
cargo: PropTypes.number.isRequired,
|
||||
fuel: PropTypes.number.isRequired,
|
||||
marker: PropTypes.string.isRequired,
|
||||
pips: PropTypes.object.isRequired
|
||||
code: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -24,7 +28,7 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.didContextChange = this.didContextChange.bind(this);
|
||||
autoBind(this);
|
||||
this.state = {
|
||||
shieldColour: 'blue'
|
||||
};
|
||||
@@ -35,44 +39,53 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
||||
* @return {React.Component} Summary table
|
||||
*/
|
||||
render() {
|
||||
const { ship, cargo, fuel, pips } = this.props;
|
||||
const { ship } = this.props;
|
||||
let { language, tooltip, termtip } = this.context;
|
||||
let translate = language.translate;
|
||||
let u = language.units;
|
||||
let formats = language.formats;
|
||||
let { time, int, round, f1, f2 } = formats;
|
||||
let { time, int, f1, f2 } = formats;
|
||||
let hide = tooltip.bind(null, null);
|
||||
const shieldGenerator = ship.findInternalByGroup('sg') || ship.findInternalByGroup('psg');
|
||||
const sgClassNames = cn({ warning: shieldGenerator && !ship.shield, muted: !shieldGenerator });
|
||||
const sgTooltip = shieldGenerator ? 'TT_SUMMARY_SHIELDS' : 'TT_SUMMARY_SHIELDS_NONFUNCTIONAL';
|
||||
const timeToDrain = Calc.timeToDrainWep(ship, 4);
|
||||
const canThrust = ship.canThrust(cargo, ship.fuelCapacity);
|
||||
|
||||
const speed = ship.get(SPEED);
|
||||
const shipBoost = ship.get(BOOST_SPEED);
|
||||
const boostInterval = ship.get(BOOST_INTERVAL);
|
||||
const canThrust = 0 < speed;
|
||||
const canBoost = canThrust && !isNaN(shipBoost);
|
||||
const speedTooltip = canThrust ? 'TT_SUMMARY_SPEED' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
|
||||
const canBoost = ship.canBoost(cargo, ship.fuelCapacity);
|
||||
const boostTooltip = canBoost ? 'TT_SUMMARY_BOOST' : canThrust ? 'TT_SUMMARY_BOOST_NONFUNCTIONAL' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
|
||||
const canJump = ship.getSlotStatus(ship.standard[2]) == 3;
|
||||
const sgMetrics = Calc.shieldMetrics(ship, pips.sys);
|
||||
const shipBoost = canBoost ? Calc.calcBoost(ship) : 'No Boost';
|
||||
const restingHeat = Math.sqrt(((ship.standard[0].m.pgen * ship.standard[0].m.eff) / ship.heatCapacity) / 0.2);
|
||||
const armourMetrics = Calc.armourMetrics(ship);
|
||||
|
||||
const sgMetrics = ship.get(SHIELD_METRICS);
|
||||
const armourMetrics = ship.get(ARMOUR_METRICS);
|
||||
const damageMetrics = ship.get(DAMAGE_METRICS);
|
||||
const moduleProtectionMetrics = ship.get(MODULE_PROTECTION_METRICS);
|
||||
const timeToDrain = damageMetrics.timeToDrain;
|
||||
|
||||
const shieldGenerator = ship.getShieldGenerator();
|
||||
const sgClassNames = cn({
|
||||
warning: shieldGenerator && !shieldGenerator.isEnabled(),
|
||||
muted: !shieldGenerator,
|
||||
});
|
||||
const sgTooltip = shieldGenerator ? 'TT_SUMMARY_SHIELDS' : 'TT_SUMMARY_SHIELDS_NONFUNCTIONAL';
|
||||
const sgType = shieldGenerator ? shieldGenerator.readMeta('type') : undefined;
|
||||
let shieldColour = 'blue';
|
||||
if (shieldGenerator && shieldGenerator.m.grp === 'psg') {
|
||||
shieldColour = 'green';
|
||||
} else if (shieldGenerator && shieldGenerator.m.grp === 'bsg') {
|
||||
shieldColour = 'purple';
|
||||
switch (sgType) {
|
||||
case 'biweaveshieldgen': shieldColour = 'purple'; break;
|
||||
case 'prismaticshieldgen': shieldColour = 'green'; break;
|
||||
}
|
||||
this.state = {
|
||||
shieldColour
|
||||
};
|
||||
this.state = { shieldColour };
|
||||
|
||||
const jumpRangeMetrics = ship.getMetrics(JUMP_METRICS);
|
||||
|
||||
return <div id='summary'>
|
||||
<div style={{display: "table", width: "100%"}}>
|
||||
<div style={{display: "table-row"}}>
|
||||
<div style={{ display: 'table', width: '100%' }}>
|
||||
<div style={{ display: 'table-row' }}>
|
||||
<table className={'summaryTable'}>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !canThrust }) }>{translate('speed')}</th>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': speed == 0 }) }>{translate('speed')}</th>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !canBoost }) }>{translate('boost')}</th>
|
||||
<th colSpan={5} className={ cn({ 'bg-warning-disabled': !canJump }) }>{translate('jump range')}</th>
|
||||
<th colSpan={5} className={ cn({ 'bg-warning-disabled': jumpRangeMetrics.jumpRangeCurrent == 0 }) }>{translate('jump range')}</th>
|
||||
<th rowSpan={2}>{translate('shield')}</th>
|
||||
<th rowSpan={2}>{translate('integrity')}</th>
|
||||
<th rowSpan={2}>{translate('DPS')}</th>
|
||||
@@ -87,14 +100,15 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
||||
<th rowSpan={2}>{translate('crew')}</th>
|
||||
<th onMouseEnter={termtip.bind(null, 'mass lock factor', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('MLF')}</th>
|
||||
<th onMouseEnter={termtip.bind(null, 'TT_SUMMARY_BOOST_INTERVAL', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('boost interval')}</th>
|
||||
<th rowSpan={2}>{translate('resting heat (Beta)')}</th>
|
||||
{/* TODO: Resting heat */}
|
||||
{/* <th rowSpan={2}>{translate('resting heat (Beta)')}</th> */}
|
||||
</tr>
|
||||
<tr>
|
||||
<th className={ cn({ 'lft': true, 'bg-warning-disabled': !canJump }) }>{translate('max')}</th>
|
||||
<th className={ cn({ 'bg-warning-disabled': !canJump }) }>{translate('unladen')}</th>
|
||||
<th className={ cn({ 'bg-warning-disabled': !canJump }) }>{translate('laden')}</th>
|
||||
<th className={ cn({ 'bg-warning-disabled': !canJump }) }>{translate('total unladen')}</th>
|
||||
<th className={ cn({ 'bg-warning-disabled': !canJump }) }>{translate('total laden')}</th>
|
||||
<th className="lft">{translate('max')}</th>
|
||||
<th>{translate('unladen')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
<th>{translate('total unladen')}</th>
|
||||
<th>{translate('total laden')}</th>
|
||||
<th className='lft'>{translate('hull')}</th>
|
||||
<th>{translate('unladen')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
@@ -102,30 +116,68 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td onMouseEnter={termtip.bind(null, speedTooltip, { cap: 0 })} onMouseLeave={hide}>{ canThrust ? <span>{int(ship.calcSpeed(4, ship.fuelCapacity, 0, false))}{u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td onMouseEnter={termtip.bind(null, boostTooltip, { cap: 0 })} onMouseLeave={hide}>{ canBoost ? <span>{int(ship.calcSpeed(4, ship.fuelCapacity, 0, true))}{u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_MAX_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{ canJump ? <span>{ f2(Calc.jumpRange(ship.unladenMass + ship.standard[2].m.getMaxFuelPerJump(), ship.standard[2].m, ship.standard[2].m.getMaxFuelPerJump(), ship))}{u.LY}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{ canJump ? <span>{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity, ship))}{u.LY}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{ canJump ? <span>{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity, ship))}{u.LY}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_TOTAL_JUMP', { cap: 0 })} onMouseLeave={hide}>{ canJump ? <span>{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity, ship))}{u.LY}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_TOTAL_JUMP', { cap: 0 })} onMouseLeave={hide}>{ canJump ? <span>{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity, ship))}{u.LY}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td className={sgClassNames} onMouseEnter={termtip.bind(null, sgTooltip, { cap: 0 })} onMouseLeave={hide}>{int(ship.shield)}{u.MJ}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_INTEGRITY', { cap: 0 })} onMouseLeave={hide}>{int(ship.armour)}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_DPS', { cap: 0 })} onMouseLeave={hide}>{f1(ship.totalDps)}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_EPS', { cap: 0 })} onMouseLeave={hide}>{f1(ship.totalEps)}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_TTD', { cap: 0 })} onMouseLeave={hide}>{timeToDrain === Infinity ? '∞' : time(timeToDrain)}</td>
|
||||
<td onMouseEnter={termtip.bind(null, speedTooltip, { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{canThrust ?
|
||||
<span>{int(speed)}{u['m/s']}</span> :
|
||||
<span className='warning'>0<Warning/></span>
|
||||
}</td>
|
||||
<td onMouseEnter={termtip.bind(null, boostTooltip, { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{canBoost ?
|
||||
<span>{int(shipBoost)}{u['m/s']}</span> :
|
||||
<span className='warning'>0<Warning/></span>
|
||||
}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_MAX_SINGLE_JUMP', { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{<span>{f2(jumpRangeMetrics.jumpRangeMax)}{u.LY}</span>}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_SINGLE_JUMP', { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{<span>{f2(jumpRangeMetrics.jumpRangeUnladen)}{u.LY}</span>}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_SINGLE_JUMP', { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{<span>{f2(jumpRangeMetrics.jumpRangeLaden)}{u.LY}</span>}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_TOTAL_JUMP', { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{<span>{f2(jumpRangeMetrics.totalRangeUnladen)}{u.LY}</span>}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_TOTAL_JUMP', { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{<span>{f2(jumpRangeMetrics.totalRangeLaden)}{u.LY}</span>}</td>
|
||||
<td className={sgClassNames}
|
||||
onMouseEnter={termtip.bind(null, sgTooltip, { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{int(sgMetrics.shieldStrength)}{u.MJ}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_INTEGRITY', { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{int(armourMetrics.armour)}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_DPS', { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{f1(damageMetrics.dps)}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_EPS', { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{f1(damageMetrics.eps)}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_TTD', { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{timeToDrain === Infinity ? '∞' : time(timeToDrain)}</td>
|
||||
{/* <td>{f1(ship.totalHps)}</td> */}
|
||||
<td>{round(ship.cargoCapacity)}{u.T}</td>
|
||||
<td>{ship.passengerCapacity}</td>
|
||||
<td>{round(ship.fuelCapacity)}{u.T}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_HULL_MASS', { cap: 0 })} onMouseLeave={hide}>{ship.hullMass}{u.T}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_MASS', { cap: 0 })} onMouseLeave={hide}>{int(ship.unladenMass)}{u.T}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_MASS', { cap: 0 })} onMouseLeave={hide}>{int(ship.ladenMass)}{u.T}</td>
|
||||
<td>{int(ship.hardness)}</td>
|
||||
<td>{ship.crew}</td>
|
||||
<td>{ship.masslock}</td>
|
||||
<td>{shipBoost !== 'No Boost' ? formats.time(shipBoost) : 'No Boost'}</td>
|
||||
<td>{formats.pct(restingHeat)}</td>
|
||||
<td>{ship.get(CARGO_CAPACITY)}{u.T}</td>
|
||||
<td>{ship.get(PASSENGER_CAPACITY)}</td>
|
||||
<td>{ship.get(FUEL_CAPACITY)}{u.T}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_HULL_MASS', { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{ship.readProp('hullmass')}{u.T}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_MASS', { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{int(ship.get(MINIMUM_MASS))}{u.T}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_MASS', { cap: 0 })}
|
||||
onMouseLeave={hide}
|
||||
>{int(ship.get(LADEN_MASS))}{u.T}</td>
|
||||
<td>{int(ship.readProp('hardness'))}</td>
|
||||
<td>{ship.readMeta('crew')}</td>
|
||||
<td>{ship.readProp('masslock')}</td>
|
||||
<td>{time(boostInterval)}</td>
|
||||
{/* TODO: resting heat */}
|
||||
{/* <td>{NaN}</td> */}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -154,19 +206,19 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{translate(shieldGenerator && shieldGenerator.m.grp || 'No Shield')}</td>
|
||||
<td>{formats.pct1(ship.shieldExplRes)}</td>
|
||||
<td>{formats.pct1(ship.shieldKinRes)}</td>
|
||||
<td>{formats.pct1(ship.shieldThermRes)}</td>
|
||||
<td>{translate(sgType || 'No Shield')}</td>
|
||||
<td>{formats.pct1(1 - sgMetrics.explosive.damageMultiplier)}</td>
|
||||
<td>{formats.pct1(1 - sgMetrics.kinetic.damageMultiplier)}</td>
|
||||
<td>{formats.pct1(1 - sgMetrics.thermal.damageMultiplier)}</td>
|
||||
<td></td>
|
||||
|
||||
<td>{int(ship && sgMetrics.summary > 0 ? sgMetrics.summary : 0)}{u.MJ}</td>
|
||||
<td>{int(ship && sgMetrics.summary > 0 ? sgMetrics.summary / sgMetrics.explosive.base : 0)}{u.MJ}</td>
|
||||
<td>{int(ship && sgMetrics.summary ? sgMetrics.summary / sgMetrics.kinetic.base : 0)}{u.MJ}</td>
|
||||
<td>{int(ship && sgMetrics.summary ? sgMetrics.summary / sgMetrics.thermal.base : 0)}{u.MJ}</td>
|
||||
<td>{int(sgMetrics.shieldStrength || 0)}{u.MJ}</td>
|
||||
<td>{int(sgMetrics.shieldStrength / sgMetrics.explosive.damageMultiplier || 0)}{u.MJ}</td>
|
||||
<td>{int(sgMetrics.shieldStrength / sgMetrics.kinetic.damageMultiplier || 0)}{u.MJ}</td>
|
||||
<td>{int(sgMetrics.shieldStrength / sgMetrics.thermal.damageMultiplier || 0)}{u.MJ}</td>
|
||||
<td></td>
|
||||
<td>{sgMetrics && sgMetrics.recover === Math.Inf ? translate('Never') : formats.time(sgMetrics.recover)}</td>
|
||||
<td>{sgMetrics && sgMetrics.recharge === Math.Inf ? translate('Never') : formats.time(sgMetrics.recharge)}</td>
|
||||
<td>{isNaN(sgMetrics.recover) ? translate('Never') : formats.time(sgMetrics.recover)}</td>
|
||||
<td>{isNaN(sgMetrics.recharge) ? translate('Never') : formats.time(sgMetrics.recharge)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<thead>
|
||||
@@ -193,19 +245,18 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{translate(ship && ship.bulkheads && ship.bulkheads.m && ship.bulkheads.m.name || 'No Armour')}</td>
|
||||
<td>{formats.pct1(ship.hullExplRes)}</td>
|
||||
<td>{formats.pct1(ship.hullKinRes)}</td>
|
||||
<td>{formats.pct1(ship.hullThermRes)}</td>
|
||||
<td>{formats.pct1(ship.hullCausRes)}</td>
|
||||
<td>{int(armourMetrics.total)}</td>
|
||||
<td>{int(armourMetrics.total / armourMetrics.explosive.total)}</td>
|
||||
<td>{int(armourMetrics.total/ armourMetrics.kinetic.total)}</td>
|
||||
<td>{int(armourMetrics.total / armourMetrics.thermal.total)}</td>
|
||||
<td>{int(armourMetrics.total/ armourMetrics.caustic.total)}</td>
|
||||
<td>{int(armourMetrics.modulearmour)}</td>
|
||||
<td>{int(armourMetrics.moduleprotection * 100) + '%'}</td>
|
||||
|
||||
<td>{translate(ship.getAlloys().readMeta('type') || 'No Armour')}</td>
|
||||
<td>{formats.pct1(1 - armourMetrics.explosive.damageMultiplier)}</td>
|
||||
<td>{formats.pct1(1 - armourMetrics.kinetic.damageMultiplier)}</td>
|
||||
<td>{formats.pct1(1 - armourMetrics.thermal.damageMultiplier)}</td>
|
||||
<td>{formats.pct1(1 - armourMetrics.caustic.damageMultiplier)}</td>
|
||||
<td>{int(armourMetrics.armour)}</td>
|
||||
<td>{int(armourMetrics.armour / armourMetrics.explosive.damageMultiplier)}</td>
|
||||
<td>{int(armourMetrics.armour / armourMetrics.kinetic.damageMultiplier)}</td>
|
||||
<td>{int(armourMetrics.armour / armourMetrics.thermal.damageMultiplier)}</td>
|
||||
<td>{int(armourMetrics.armour / armourMetrics.caustic.damageMultiplier)}</td>
|
||||
<td>{int(moduleProtectionMetrics.moduleArmour)}</td>
|
||||
<td>{formats.pct1(1 - moduleProtectionMetrics.moduleProtection)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import autoBind from 'auto-bind';
|
||||
|
||||
const MARGIN_LR = 8; // Left/ Right margin
|
||||
|
||||
@@ -7,7 +8,6 @@ const MARGIN_LR = 8; // Left/ Right margin
|
||||
* Horizontal Slider
|
||||
*/
|
||||
export default class Slider extends React.Component {
|
||||
|
||||
static defaultProps = {
|
||||
axis: false,
|
||||
min: 0,
|
||||
@@ -32,16 +32,7 @@ export default class Slider extends React.Component {
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._down = this._down.bind(this);
|
||||
this._move = this._move.bind(this);
|
||||
this._up = this._up.bind(this);
|
||||
this._keyup = this._keyup.bind(this);
|
||||
this._keydown = this._keydown.bind(this);
|
||||
this._touchstart = this._touchstart.bind(this);
|
||||
this._touchend = this._touchend.bind(this);
|
||||
|
||||
this._updatePercent = this._updatePercent.bind(this);
|
||||
this._updateDimensions = this._updateDimensions.bind(this);
|
||||
autoBind(this);
|
||||
|
||||
this.state = { width: 0 };
|
||||
}
|
||||
@@ -55,7 +46,6 @@ export default class Slider extends React.Component {
|
||||
this.left = rect.left;
|
||||
this.width = rect.width;
|
||||
this._move(event);
|
||||
this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,70 +65,11 @@ export default class Slider extends React.Component {
|
||||
* @param {Event} event DOM Event
|
||||
*/
|
||||
_up(event) {
|
||||
this.sliderInputBox.sliderVal.focus();
|
||||
clearTimeout(this.touchStartTimer);
|
||||
event.preventDefault();
|
||||
this.left = null;
|
||||
this.width = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Key up handler for keyboard.
|
||||
* display the number field then set focus to it
|
||||
* when "Enter" key is pressed
|
||||
* @param {Event} event Keyboard event
|
||||
*/
|
||||
_keyup(event) {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
this.sliderInputBox._setDisplay('block');
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Key down handler
|
||||
* increment slider position by +/- 1 when right/left arrow key is pressed or held
|
||||
* @param {Event} event Keyboard even
|
||||
*/
|
||||
_keydown(event) {
|
||||
let newVal = this.props.percent * this.props.max;
|
||||
switch (event.key) {
|
||||
case 'ArrowRight':
|
||||
newVal += 1;
|
||||
if (newVal <= this.props.max) this.props.onChange(newVal / this.props.max);
|
||||
return;
|
||||
case 'ArrowLeft':
|
||||
newVal -= 1;
|
||||
if (newVal >= 0) this.props.onChange(newVal / this.props.max);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch start handler
|
||||
* @param {Event} event DOM Event
|
||||
*
|
||||
*/
|
||||
_touchstart(event) {
|
||||
this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch end handler
|
||||
* @param {Event} event DOM Event
|
||||
*
|
||||
*/
|
||||
_touchend(event) {
|
||||
this.sliderInputBox.sliderVal.focus();
|
||||
clearTimeout(this.touchStartTimer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is still dragging
|
||||
* @param {SyntheticEvent} event Event
|
||||
@@ -213,7 +144,7 @@ export default class Slider extends React.Component {
|
||||
let width = outerWidth - (margin * 2);
|
||||
let pctPos = width * this.props.percent;
|
||||
return <div><svg
|
||||
onMouseUp={this._up} onMouseEnter={this._enter.bind(this)} onMouseMove={this._move} onKeyUp={this._keyup} onKeyDown={this._keydown} style={style} ref={node => this.node = node} tabIndex="0">
|
||||
onMouseUp={this._up} onMouseEnter={this._enter.bind(this)} onMouseMove={this._move} style={style} ref={node => this.node = node} tabIndex="0">
|
||||
<rect className='primary' style={{ opacity: 0.3 }} x={margin} y='0.25em' rx='0.3em' ry='0.3em' width={width} height='0.7em' />
|
||||
<rect className='primary-disabled' x={margin} y='0.45em' rx='0.15em' ry='0.15em' width={pctPos} height='0.3em' />
|
||||
<circle className='primary' r={margin} cy='0.6em' cx={pctPos + margin} />
|
||||
@@ -224,163 +155,6 @@ export default class Slider extends React.Component {
|
||||
<text className='primary-disabled' y='3em' x='100%' style={{ textAnchor: 'end' }}>{max + axisUnit}</text>
|
||||
</g>}
|
||||
</svg>
|
||||
<TextInputBox ref={(tb) => this.sliderInputBox = tb}
|
||||
onChange={this.props.onChange}
|
||||
percent={this.props.percent}
|
||||
axisUnit={this.props.axisUnit}
|
||||
scale={this.props.scale}
|
||||
max={this.props.max}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* New component to add keyboard support for sliders - works on all devices (desktop, iOS, Android)
|
||||
**/
|
||||
class TextInputBox extends React.Component {
|
||||
static propTypes = {
|
||||
axisUnit: PropTypes.string,// units (T, M, etc.)
|
||||
max: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,// function which determins percent value
|
||||
percent: PropTypes.number.isRequired,// value of slider
|
||||
scale: PropTypes.number
|
||||
};
|
||||
/**
|
||||
* Determine if the user is still dragging
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._handleFocus = this._handleFocus.bind(this);
|
||||
this._handleBlur = this._handleBlur.bind(this);
|
||||
this._handleChange = this._handleChange.bind(this);
|
||||
this._keyup = this._keyup.bind(this);
|
||||
this.state = this._getInitialState();
|
||||
}
|
||||
/**
|
||||
* Update input value if slider changes will change props/state
|
||||
* @param {Object} nextProps React Component properites
|
||||
* @param {Object} nextState React Component state values
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextState) {
|
||||
let nextValue = nextProps.percent * nextProps.max;
|
||||
// See https://stackoverflow.com/questions/32414308/updating-state-on-props-change-in-react-form
|
||||
if (nextValue !== this.state.inputValue && nextValue <= nextProps.max) {
|
||||
this.setState({ inputValue: nextValue });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update slider textbox visibility/values if changes are made to slider
|
||||
* @param {Object} prevProps React Component properites
|
||||
* @param {Object} prevState React Component state values
|
||||
*/
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.divStyle.display == 'none' && this.state.divStyle.display == 'block') {
|
||||
this.enterTimer = setTimeout(() => this.sliderVal.focus(), 10);
|
||||
}
|
||||
if (prevProps.max !== this.props.max && this.state.inputValue > this.props.max) {
|
||||
// they chose a different module
|
||||
this.setState({ inputValue: this.props.max });
|
||||
}
|
||||
if (this.state.inputValue != prevState.inputValue && prevProps.max == this.props.max) {
|
||||
this.props.onChange(this.state.inputValue / this.props.max);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Set initial state for the textbox.
|
||||
* We may want to rethink this to
|
||||
* try and make it a stateless component
|
||||
* @returns {object} React state object with initial values set
|
||||
*/
|
||||
_getInitialState() {
|
||||
return {
|
||||
divStyle: { display:'none' },
|
||||
inputStyle: { width:'4em' },
|
||||
labelStyle: { marginLeft: '.1em' },
|
||||
maxLength:5,
|
||||
size:5,
|
||||
min:0,
|
||||
tabIndex:-1,
|
||||
type:'number',
|
||||
readOnly: true,
|
||||
inputValue: this.props.percent * this.props.max
|
||||
};
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {string} val block or none
|
||||
*/
|
||||
_setDisplay(val) {
|
||||
this.setState({
|
||||
divStyle: { display:val }
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update the input value
|
||||
* when textbox gets focus
|
||||
*/
|
||||
_handleFocus() {
|
||||
this.setState({
|
||||
inputValue:this._getValue()
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update inputValue when textbox loses focus
|
||||
*/
|
||||
_handleBlur() {
|
||||
this._setDisplay('none');
|
||||
if (this.state.inputValue !== '') {
|
||||
this.props.onChange(this.state.inputValue / this.props.max);
|
||||
} else {
|
||||
this.setState({
|
||||
inputValue: this.props.percent * this.props.max
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the value in the text box
|
||||
* @returns {number} inputValue Value of the input box
|
||||
*/
|
||||
_getValue() {
|
||||
return this.state.inputValue;
|
||||
}
|
||||
/**
|
||||
* Update and set limits on input box
|
||||
* values depending on what user
|
||||
* has selected
|
||||
*
|
||||
* @param {SyntheticEvent} event ReactJs onChange event
|
||||
*/
|
||||
_handleChange(event) {
|
||||
if (event.target.value < 0) {
|
||||
this.setState({ inputValue: 0 });
|
||||
} else if (event.target.value <= this.props.max) {
|
||||
this.setState({ inputValue: event.target.value });
|
||||
} else {
|
||||
this.setState({ inputValue: this.props.max });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Key up handler for input field.
|
||||
* If user hits Enter key, blur/close the input field
|
||||
* @param {Event} event Keyboard event
|
||||
*/
|
||||
_keyup(event) {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
this.sliderVal.blur();
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the value in the text box
|
||||
* @return {React.Component} Text Input component for Slider
|
||||
*/
|
||||
render() {
|
||||
let { axisUnit, onChange, percent, scale } = this.props;
|
||||
return <div style={this.state.divStyle}><input style={this.state.inputStyle} value={this._getValue()} min={this.state.min} max={this.props.max} onChange={this._handleChange} onKeyUp={this._keyup} tabIndex={this.state.tabIndex} maxLength={this.state.maxLength} size={this.state.size} onBlur={() => {this._handleBlur();}} onFocus={() => {this._handleFocus();}} type={this.state.type} ref={(ip) => this.sliderVal = ip}/><text className="primary upp" style={this.state.labelStyle}>{this.props.axisUnit}</text></div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,30 +2,37 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import cn from 'classnames';
|
||||
import { ListModifications, Modified } from './SvgIcons';
|
||||
import AvailableModulesMenu from './AvailableModulesMenu';
|
||||
import ModificationsMenu from './ModificationsMenu';
|
||||
import { diffDetails } from '../utils/SlotFunctions';
|
||||
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||
import { stopCtxPropagation, wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||
import { blueprintTooltip } from '../utils/BlueprintFunctions';
|
||||
import { Module } from 'ed-forge';
|
||||
import { TYPES } from 'ed-forge/lib/src/data/slots';
|
||||
import autoBind from 'auto-bind';
|
||||
import { toPairs } from 'lodash';
|
||||
|
||||
const HARDPOINT_SLOT_LABELS = {
|
||||
1: 'S',
|
||||
2: 'M',
|
||||
3: 'L',
|
||||
4: 'H',
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstract Slot
|
||||
*/
|
||||
export default class Slot extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
availableModules: PropTypes.func.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onOpen: PropTypes.func.isRequired,
|
||||
maxClass: PropTypes.number.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
m: PropTypes.object,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
ship: PropTypes.object.isRequired,
|
||||
eligible: PropTypes.object,
|
||||
currentMenu: PropTypes.any,
|
||||
hideSearch: PropTypes.bool,
|
||||
m: PropTypes.instanceOf(Module),
|
||||
warning: PropTypes.func,
|
||||
drag: PropTypes.func,
|
||||
drop: PropTypes.func,
|
||||
dropClass: PropTypes.string
|
||||
dropClass: PropTypes.string,
|
||||
propsToShow: PropTypes.object.isRequired,
|
||||
onPropToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -34,25 +41,122 @@ export default class Slot extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._modificationsSelected = false;
|
||||
|
||||
this._contextMenu = wrapCtxMenu(this._contextMenu.bind(this));
|
||||
this._getMaxClassLabel = this._getMaxClassLabel.bind(this);
|
||||
this._keyDown = this._keyDown.bind(this);
|
||||
this.slotDiv = null;
|
||||
autoBind(this);
|
||||
this.state = { menuIndex: 0 };
|
||||
}
|
||||
|
||||
// Must be implemented by subclasses:
|
||||
// _getSlotDetails()
|
||||
/**
|
||||
* Opens a menu while setting state.
|
||||
* @param {Object} newMenuIndex New menu index
|
||||
* @param {Event} event Event object
|
||||
*/
|
||||
_openMenu(newMenuIndex, event) {
|
||||
const slotName = this.props.m.getSlot();
|
||||
if (
|
||||
this.props.currentMenu === slotName &&
|
||||
newMenuIndex === this.state.menuIndex
|
||||
) {
|
||||
this.context.closeMenu();
|
||||
} {
|
||||
this.setState({ menuIndex: newMenuIndex });
|
||||
this.context.openMenu(slotName);
|
||||
}
|
||||
// If we don't stop event propagation, the underlying divs also might
|
||||
// get clicked which would open up other menus
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CSS class name for the slot. Can/should be overriden
|
||||
* as necessary.
|
||||
* @return {string} CSS Class name
|
||||
* Generate the slot contents
|
||||
* @return {React.Component} Slot contents
|
||||
*/
|
||||
_getClassNames() {
|
||||
_getSlotDetails() {
|
||||
const { m, propsToShow } = this.props;
|
||||
let { termtip, tooltip, language } = this.context;
|
||||
const { translate, units, formats } = language;
|
||||
|
||||
if (m.isEmpty()) {
|
||||
return <div className="empty">
|
||||
{translate(
|
||||
m.isOnSlot(TYPES.MILITARY) ? 'emptyrestricted' : 'empty'
|
||||
)}
|
||||
</div>;
|
||||
} else {
|
||||
let classRating = m.getClassRating();
|
||||
let { drag, drop } = this.props;
|
||||
|
||||
// Modifications tooltip shows blueprint and grade, if available
|
||||
let modTT = translate('modified');
|
||||
const blueprint = m.getBlueprint();
|
||||
const experimental = m.getExperimental();
|
||||
const grade = m.getBlueprintGrade();
|
||||
if (blueprint) {
|
||||
modTT = `${translate(blueprint)} ${translate('grade')}: ${grade}`;
|
||||
if (experimental) {
|
||||
modTT += `, ${translate(experimental)}`;
|
||||
}
|
||||
modTT = (
|
||||
<div>
|
||||
<div>{modTT}</div>
|
||||
{blueprintTooltip(language, m)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let mass = m.get('mass') || m.get('cargo') || m.get('fuel') || 0;
|
||||
return (
|
||||
<div
|
||||
className={cn('details', { disabled: !m.isEnabled() })}
|
||||
draggable="true"
|
||||
onDragStart={drag}
|
||||
onDragEnd={drop}
|
||||
>
|
||||
<div className={'cb'}>
|
||||
<div className={'l'}>
|
||||
{classRating} {translate(m.readMeta('type'))}
|
||||
{blueprint && (
|
||||
<span
|
||||
onMouseOver={termtip.bind(null, modTT)}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>
|
||||
<Modified />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{propsToShow.mass ?
|
||||
<div className={'r'}>
|
||||
{formats.round(mass)}
|
||||
{units.T}
|
||||
</div> : null}
|
||||
</div>
|
||||
<div className={'cb'}>
|
||||
{toPairs(propsToShow).sort().map(([prop, show]) => {
|
||||
const { unit, value } = m.getFormatted(prop, true);
|
||||
// Don't show mass again; it's already shown on the top right
|
||||
// corner of a slot
|
||||
if (!show || isNaN(value) || prop == 'mass') {
|
||||
return null;
|
||||
} else {
|
||||
return (<div className='l'>
|
||||
{translate(prop)}: {formats.round(value)}{unit}
|
||||
</div>);
|
||||
}
|
||||
})}
|
||||
{(m.getApplicableBlueprints() || []).length > 0 ? (
|
||||
<div className="r">
|
||||
<button onClick={this._openMenu.bind(this, 1)}
|
||||
onContextMenu={stopCtxPropagation}
|
||||
onMouseOver={termtip.bind(null, translate('modifications'))}
|
||||
onMouseOut={tooltip.bind(null, null)}
|
||||
>
|
||||
<ListModifications />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,7 +165,19 @@ export default class Slot extends TranslatedComponent {
|
||||
* @return {string} label
|
||||
*/
|
||||
_getMaxClassLabel() {
|
||||
return this.props.maxClass;
|
||||
const { m } = this.props;
|
||||
let size = m.getSizeNum();
|
||||
switch (true) {
|
||||
case m.getSlot() === 'armour':
|
||||
return '';
|
||||
case size === 0:
|
||||
// This can also happen for armour but that case was handled above
|
||||
return 'U';
|
||||
case m.isOnSlot(TYPES.HARDPOINT):
|
||||
return HARDPOINT_SLOT_LABELS[size];
|
||||
default:
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,25 +187,15 @@ export default class Slot extends TranslatedComponent {
|
||||
_contextMenu(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.props.onSelect(null,null);
|
||||
const { m } = this.props;
|
||||
m.reset();
|
||||
if (this.props.currentMenu === m.getSlot()) {
|
||||
this.context.closeMenu();
|
||||
} else {
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/** Key Down handler
|
||||
* @param {SyntheticEvent} event Event
|
||||
* ToDo: see if this can be moved up
|
||||
* we do more or less the same thing
|
||||
* in every section when Enter key is pressed
|
||||
* on a focusable item
|
||||
*
|
||||
*/
|
||||
_keyDown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
if(event.target.className == 'r') {
|
||||
this._toggleModifications();
|
||||
}
|
||||
this.props.onOpen(event);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Render the slot
|
||||
* @return {React.Component} The slot
|
||||
@@ -97,64 +203,41 @@ export default class Slot extends TranslatedComponent {
|
||||
render() {
|
||||
let language = this.context.language;
|
||||
let translate = language.translate;
|
||||
let { ship, m, enabled, dropClass, dragOver, onOpen, onChange, selected, eligible, onSelect, warning, availableModules } = this.props;
|
||||
let slotDetails, modificationsMarker, menu;
|
||||
|
||||
if (!selected) {
|
||||
// If not selected then sure that modifications flag is unset
|
||||
this._modificationsSelected = false;
|
||||
}
|
||||
|
||||
if (m) {
|
||||
slotDetails = this._getSlotDetails(m, enabled, translate, language.formats, language.units); // Must be implemented by sub classes
|
||||
modificationsMarker = JSON.stringify(m);
|
||||
} else {
|
||||
slotDetails = <div className={'empty'}>{translate(eligible ? 'emptyrestricted' : 'empty')}</div>;
|
||||
modificationsMarker = '';
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
if (this._modificationsSelected) {
|
||||
menu = <ModificationsMenu
|
||||
className={this._getClassNames()}
|
||||
onChange={onChange}
|
||||
ship={ship}
|
||||
m={m}
|
||||
marker={modificationsMarker}
|
||||
modButton = {this.modButton}
|
||||
/>;
|
||||
} else {
|
||||
menu = <AvailableModulesMenu
|
||||
className={this._getClassNames()}
|
||||
modules={availableModules()}
|
||||
m={m}
|
||||
ship={ship}
|
||||
onSelect={onSelect}
|
||||
warning={warning}
|
||||
diffDetails={diffDetails.bind(ship, this.context.language)}
|
||||
slotDiv = {this.slotDiv}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
let {
|
||||
currentMenu, m, dropClass, dragOver, warning, hideSearch, propsToShow,
|
||||
onPropToggle,
|
||||
} = this.props;
|
||||
const { menuIndex } = this.state;
|
||||
|
||||
// TODO: implement touch dragging
|
||||
|
||||
const selected = currentMenu === m.getSlot();
|
||||
return (
|
||||
<div className={cn('slot', dropClass, { selected })} onClick={onOpen} onKeyDown={this._keyDown} onContextMenu={this._contextMenu} onDragOver={dragOver} tabIndex="0" ref={slotDiv => this.slotDiv = slotDiv}>
|
||||
<div className='details-container'>
|
||||
<div className='sz'>{this._getMaxClassLabel(translate)}</div>
|
||||
{slotDetails}
|
||||
<div
|
||||
className={cn('slot', dropClass, { selected })}
|
||||
onContextMenu={this._contextMenu}
|
||||
onDragOver={dragOver} tabIndex="0"
|
||||
onClick={this._openMenu.bind(this, 0)}
|
||||
>
|
||||
<div className={cn(
|
||||
'details-container',
|
||||
{ warning: warning && warning(m) },
|
||||
)}>
|
||||
<div className="sz">{this._getMaxClassLabel(translate)}</div>
|
||||
{this._getSlotDetails()}
|
||||
</div>
|
||||
{menu}
|
||||
{selected && menuIndex === 0 &&
|
||||
<AvailableModulesMenu
|
||||
m={m} hideSearch={hideSearch}
|
||||
onSelect={(item) => {
|
||||
m.setItem(item);
|
||||
this.context.closeMenu();
|
||||
}}
|
||||
warning={warning}
|
||||
/>}
|
||||
{selected && menuIndex === 1 &&
|
||||
<ModificationsMenu m={m} propsToShow={propsToShow}
|
||||
onPropToggle={onPropToggle} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Toggle the modifications flag when selecting the modifications icon
|
||||
*/
|
||||
_toggleModifications() {
|
||||
this._modificationsSelected = !this._modificationsSelected;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,50 +2,35 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||
import { canMount } from '../utils/SlotFunctions';
|
||||
import { Equalizer } from '../components/SvgIcons';
|
||||
import cn from 'classnames';
|
||||
import { Ship } from 'ed-forge';
|
||||
import autoBind from 'auto-bind';
|
||||
const browser = require('detect-browser');
|
||||
|
||||
/**
|
||||
* Abstract Slot Section
|
||||
*/
|
||||
export default class SlotSection extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
ship: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onCargoChange: PropTypes.func.isRequired,
|
||||
onFuelChange: PropTypes.func.isRequired,
|
||||
ship: PropTypes.instanceOf(Ship),
|
||||
code: PropTypes.string.isRequired,
|
||||
togglePwr: PropTypes.func,
|
||||
sectionMenuRefs: PropTypes.object
|
||||
propsToShow: PropTypes.object.isRequired,
|
||||
onPropToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
* @param {string} sectionId Section DOM Id
|
||||
* @param {string} sectionName Section name
|
||||
*/
|
||||
constructor(props, context, sectionId, sectionName) {
|
||||
constructor(props, sectionName) {
|
||||
super(props);
|
||||
this.sectionId = sectionId;
|
||||
this.sectionName = sectionName;
|
||||
this.ssHeadRef = null;
|
||||
autoBind(this);
|
||||
|
||||
this.sectionName = sectionName;
|
||||
|
||||
this.sectionRefArr = this.props.sectionMenuRefs[this.sectionId] = [];
|
||||
this.sectionRefArr['selectedRef'] = null;
|
||||
this._getSlots = this._getSlots.bind(this);
|
||||
this._selectModule = this._selectModule.bind(this);
|
||||
this._getSectionMenu = this._getSectionMenu.bind(this);
|
||||
this._contextMenu = this._contextMenu.bind(this);
|
||||
this._drop = this._drop.bind(this);
|
||||
this._dragOverNone = this._dragOverNone.bind(this);
|
||||
this._close = this._close.bind(this);
|
||||
this._keyDown = this._keyDown.bind(this);
|
||||
this._handleSectionFocus = this._handleSectionFocus.bind(this);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
@@ -55,82 +40,6 @@ export default class SlotSection extends TranslatedComponent {
|
||||
// _contextMenu()
|
||||
// componentDidUpdate(prevProps)
|
||||
|
||||
/**
|
||||
* TODO: May either need to send the function to be triggered when Enter key is pressed, or else
|
||||
* may need a separate keyDown handler for each subclass (StandardSlotSection, HardpointSlotSection, etc.)
|
||||
* ex: _keyDown(_keyDownfn, event)
|
||||
*
|
||||
* @param {SyntheticEvent} event KeyDown event
|
||||
*/
|
||||
_keyDown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
event.stopPropagation();
|
||||
if (event.currentTarget.nodeName === 'H1') {
|
||||
this._openMenu(this.sectionName, event);
|
||||
} else {
|
||||
event.currentTarget.click();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key == 'Tab') {
|
||||
if (event.shiftKey) {
|
||||
if ((event.currentTarget === this.sectionRefArr[this.firstRefId]) && this.sectionRefArr[this.lastRefId]) {
|
||||
event.preventDefault();
|
||||
this.sectionRefArr[this.lastRefId].focus();
|
||||
}
|
||||
} else {
|
||||
if ((event.currentTarget === this.sectionRefArr[this.lastRefId]) && this.sectionRefArr[this.firstRefId]) {
|
||||
event.preventDefault();
|
||||
this.sectionRefArr[this.firstRefId].focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set focus on appropriate Slot Section Menu element
|
||||
* @param {Object} focusPrevProps prevProps for componentDidUpdate() from ...SlotSection.jsx
|
||||
* @param {String} firstRef id of the first ref in ...SlotSection.jsx
|
||||
* @param {String} lastRef id of the last ref in ...SlotSection.jsx
|
||||
*
|
||||
*/
|
||||
_handleSectionFocus(focusPrevProps, firstRef, lastRef) {
|
||||
if (this.selectedRefId !== null && this.sectionRefArr[this.selectedRefId]) {
|
||||
// set focus on the previously selected option for the currently open section menu
|
||||
this.sectionRefArr[this.selectedRefId].focus();
|
||||
} else if (this.sectionRefArr[firstRef] && this.sectionRefArr[firstRef] != null) {
|
||||
// set focus on the first option in the currently open section menu if none have been selected previously
|
||||
this.sectionRefArr[firstRef].focus();
|
||||
} else if (this.props.currentMenu == null && focusPrevProps.currentMenu == this.sectionName && this.sectionRefArr['ssHeadRef']) {
|
||||
// set focus on the section menu header when section menu is closed
|
||||
this.sectionRefArr['ssHeadRef'].focus();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Open a menu
|
||||
* @param {string} menu Menu name
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_openMenu(menu, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (this.props.currentMenu === menu) {
|
||||
menu = null;
|
||||
}
|
||||
this.context.openMenu(menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount/Use the specified module in the slot
|
||||
* @param {Object} slot Slot
|
||||
* @param {Object} m Selected module
|
||||
*/
|
||||
_selectModule(slot, m) {
|
||||
this.props.ship.use(slot, m, false);
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Slot Drag Handler
|
||||
* @param {object} originSlot Origin slot model
|
||||
@@ -207,7 +116,6 @@ export default class SlotSection extends TranslatedComponent {
|
||||
// Copy power info
|
||||
targetSlot.enabled = originSlot.enabled;
|
||||
targetSlot.priority = originSlot.priority;
|
||||
this.props.onChange();
|
||||
}
|
||||
} else {
|
||||
// Store power info
|
||||
@@ -236,7 +144,6 @@ export default class SlotSection extends TranslatedComponent {
|
||||
targetSlot.enabled = targetEnabled;
|
||||
targetSlot.priority = targetPriority;
|
||||
}
|
||||
this.props.onChange();
|
||||
this.props.ship
|
||||
.updatePowerGenerated()
|
||||
.updatePowerUsed()
|
||||
@@ -282,6 +189,17 @@ export default class SlotSection extends TranslatedComponent {
|
||||
return 'ineligible'; // Cannot be dropped / invalid drop slot
|
||||
}
|
||||
|
||||
_open(newMenu, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const { currentMenu } = this.props;
|
||||
if (currentMenu === newMenu) {
|
||||
this.context.closeMenu();
|
||||
} else {
|
||||
this.context.openMenu(newMenu);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close current menu
|
||||
*/
|
||||
@@ -298,14 +216,13 @@ export default class SlotSection extends TranslatedComponent {
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let sectionMenuOpened = this.props.currentMenu === this.sectionName;
|
||||
let open = this._openMenu.bind(this, this.sectionName);
|
||||
let ctx = wrapCtxMenu(this._contextMenu);
|
||||
|
||||
return (
|
||||
<div id={this.sectionId} className={'group'} onDragLeave={this._dragOverNone}>
|
||||
<div className={cn('section-menu', { selected: sectionMenuOpened })} onClick={open} onContextMenu={ctx}>
|
||||
<h1 tabIndex="0" onKeyDown={this._keyDown} ref={ssHead => this.sectionRefArr['ssHeadRef'] = ssHead}>{translate(this.sectionName)} <Equalizer/></h1>
|
||||
{sectionMenuOpened ? this._getSectionMenu(translate, this.props.ship) : null }
|
||||
<div className="group" onDragLeave={this._dragOverNone}>
|
||||
<div className={cn('section-menu', { selected: sectionMenuOpened })}
|
||||
onContextMenu={wrapCtxMenu(this._contextMenu)} onClick={this._open.bind(this, this.sectionName)}>
|
||||
<h1 tabIndex="0">{translate(this.sectionName)}<Equalizer/></h1>
|
||||
{sectionMenuOpened && this._getSectionMenu()}
|
||||
</div>
|
||||
{this._getSlots()}
|
||||
</div>
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import Persist from '../stores/Persist';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { diffDetails } from '../utils/SlotFunctions';
|
||||
import AvailableModulesMenu from './AvailableModulesMenu';
|
||||
import ModificationsMenu from './ModificationsMenu';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import { ListModifications, Modified } from './SvgIcons';
|
||||
import { Modifications } from 'coriolis-data/dist';
|
||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
import { blueprintTooltip } from '../utils/BlueprintFunctions';
|
||||
|
||||
/**
|
||||
* Standard Slot
|
||||
*/
|
||||
export default class StandardSlot extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
slot: PropTypes.object,
|
||||
modules: PropTypes.array.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onOpen: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
ship: PropTypes.object.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
warning: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct the slot
|
||||
* @param {object} props Object properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._modificationsSelected = false;
|
||||
this._keyDown = this._keyDown.bind(this);
|
||||
this.modButton = null;
|
||||
this.slotDiv = null;
|
||||
}
|
||||
/**
|
||||
* Handle Enter key
|
||||
* @param {SyntheticEvent} event KeyDown event
|
||||
*/
|
||||
_keyDown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
if(event.target.className == 'r') {
|
||||
this._toggleModifications();
|
||||
}
|
||||
this.props.onOpen(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the slot
|
||||
* @return {React.Component} Slot component
|
||||
*/
|
||||
render() {
|
||||
let { termtip, tooltip } = this.context;
|
||||
let { translate, formats, units } = this.context.language;
|
||||
let { modules, slot, selected, warning, onSelect, onChange, ship } = this.props;
|
||||
let m = slot.m;
|
||||
let classRating = m.class + m.rating;
|
||||
let menu;
|
||||
let validMods = m == null || !Modifications.modules[m.grp] ? [] : (Modifications.modules[m.grp].modifications || []);
|
||||
if (m && m.name && m.name === 'Guardian Hybrid Power Plant') {
|
||||
validMods = [];
|
||||
}
|
||||
if (m && m.name && m.name === 'Guardian Power Distributor') {
|
||||
validMods = [];
|
||||
}
|
||||
let showModuleResistances = Persist.showModuleResistances();
|
||||
let mass = m.getMass() || m.cargo || m.fuel || 0;
|
||||
|
||||
// Modifications tooltip shows blueprint and grade, if available
|
||||
let modTT = translate('modified');
|
||||
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 >= 0) {
|
||||
modTT += ', ' + translate(m.blueprint.special.name);
|
||||
}
|
||||
modTT = (
|
||||
<div>
|
||||
<div>{modTT}</div>
|
||||
{blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
// If not selected then sure that modifications flag is unset
|
||||
this._modificationsSelected = false;
|
||||
}
|
||||
|
||||
const modificationsMarker = JSON.stringify(m);
|
||||
|
||||
if (selected) {
|
||||
if (this._modificationsSelected) {
|
||||
menu = <ModificationsMenu
|
||||
className='standard'
|
||||
onChange={onChange}
|
||||
ship={ship}
|
||||
m={m}
|
||||
marker={modificationsMarker}
|
||||
modButton = {this.modButton}
|
||||
/>;
|
||||
} else {
|
||||
menu = <AvailableModulesMenu
|
||||
className='standard'
|
||||
modules={modules}
|
||||
m={m}
|
||||
ship={ship}
|
||||
onSelect={onSelect}
|
||||
warning={warning}
|
||||
diffDetails={diffDetails.bind(ship, this.context.language)}
|
||||
slotDiv = {this.slotDiv}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('slot', { selected: this.props.selected })} onClick={this.props.onOpen} onKeyDown={this._keyDown} onContextMenu={stopCtxPropagation} tabIndex="0" ref={ slotDiv => this.slotDiv = slotDiv }>
|
||||
<div className={cn('details-container', { warning: warning && warning(slot.m), disabled: m.grp !== 'bh' && !slot.enabled })}>
|
||||
<div className={'sz'}>{m.grp == 'bh' ? m.name.charAt(0) : slot.maxClass}</div>
|
||||
<div>
|
||||
<div className={'l'}>{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? <span className='r' onMouseOver={termtip.bind(null, modTT)} onMouseOut={tooltip.bind(null, null)}><Modified /></span> : null }</div>
|
||||
<div className={'r'}>{formats.round(mass)}{units.T}</div>
|
||||
<div/>
|
||||
<div className={'cb'}>
|
||||
{ m.getMinMass() ? <div className='l'>{translate('minimum mass')}: {formats.int(m.getMinMass())}{units.T}</div> : null }
|
||||
{ m.getOptMass() ? <div className='l'>{translate('optimal mass')}: {formats.int(m.getOptMass())}{units.T}</div> : null }
|
||||
{ m.getMaxMass() ? <div className='l'>{translate('max mass')}: {formats.int(m.getMaxMass())}{units.T}</div> : null }
|
||||
{ m.getOptMul() ? <div className='l'>{translate('optimal multiplier')}: {formats.rPct(m.getOptMul())}</div> : null }
|
||||
{ m.getRange() ? <div className='l'>{translate('range', m.grp)}: {formats.f2(m.getRange())}{units.km}</div> : null }
|
||||
{ m.time ? <div className='l'>{translate('time')}: {formats.time(m.time)}</div> : null }
|
||||
{ m.getThermalEfficiency() ? <div className='l'>{translate('efficiency')}: {formats.f2(m.getThermalEfficiency())}</div> : null }
|
||||
{ m.getPowerGeneration() > 0 ? <div className='l'>{translate('pgen')}: {formats.f1(m.getPowerGeneration())}{units.MW}</div> : null }
|
||||
{ m.getMaxFuelPerJump() ? <div className='l'>{translate('max')} {translate('fuel')}: {formats.f1(m.getMaxFuelPerJump())}{units.T}</div> : null }
|
||||
{ m.getWeaponsCapacity() ? <div className='l'>{translate('WEP')}: {formats.f1(m.getWeaponsCapacity())}{units.MJ} / {formats.f1(m.getWeaponsRechargeRate())}{units.MW}</div> : null }
|
||||
{ m.getSystemsCapacity() ? <div className='l'>{translate('SYS')}: {formats.f1(m.getSystemsCapacity())}{units.MJ} / {formats.f1(m.getSystemsRechargeRate())}{units.MW}</div> : null }
|
||||
{ m.getEnginesCapacity() ? <div className='l'>{translate('ENG')}: {formats.f1(m.getEnginesCapacity())}{units.MJ} / {formats.f1(m.getEnginesRechargeRate())}{units.MW}</div> : null }
|
||||
{ showModuleResistances && m.getExplosiveResistance() ? <div className='l'>{translate('explres')}: {formats.pct(m.getExplosiveResistance())}</div> : null }
|
||||
{ showModuleResistances && m.getKineticResistance() ? <div className='l'>{translate('kinres')}: {formats.pct(m.getKineticResistance())}</div> : null }
|
||||
{ showModuleResistances && m.getThermalResistance() ? <div className='l'>{translate('thermres')}: {formats.pct(m.getThermalResistance())}</div> : null }
|
||||
{ m.getIntegrity() ? <div className='l'>{translate('integrity')}: {formats.int(m.getIntegrity())}</div> : null }
|
||||
{ validMods.length > 0 ? <div className='r' tabIndex="0" ref={ modButton => this.modButton = modButton }><button tabIndex="-1" onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{menu}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the modifications flag when selecting the modifications icon
|
||||
*/
|
||||
_toggleModifications() {
|
||||
this._modificationsSelected = !this._modificationsSelected;
|
||||
}
|
||||
}
|
||||
@@ -1,132 +1,67 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import SlotSection from './SlotSection';
|
||||
import StandardSlot from './StandardSlot';
|
||||
import Module from '../shipyard/Module';
|
||||
import * as ShipRoles from '../shipyard/ShipRoles';
|
||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
import Slot from './Slot';
|
||||
import autoBind from 'auto-bind';
|
||||
import { stopCtxPropagation, moduleGet } from '../utils/UtilityFunctions';
|
||||
import { ShipProps, Module } from 'ed-forge';
|
||||
import { getModuleInfo } from 'ed-forge/lib/src/data/items';
|
||||
const { CONSUMED_RETR, LADEN_MASS } = ShipProps;
|
||||
|
||||
/**
|
||||
* Standard Slot section
|
||||
*/
|
||||
export default class StandardSlotSection extends SlotSection {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context, 'standard', 'core internal');
|
||||
this._optimizeStandard = this._optimizeStandard.bind(this);
|
||||
this._selectBulkhead = this._selectBulkhead.bind(this);
|
||||
this.selectedRefId = null;
|
||||
this.firstRefId = 'maxjump';
|
||||
this.lastRefId = 'racer';
|
||||
}
|
||||
/**
|
||||
* Handle focus if the component updates
|
||||
* @param {Object} prevProps React Component properties
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
this._handleSectionFocus(prevProps,this.firstRefId, this.lastRefId);
|
||||
constructor(props) {
|
||||
super(props, 'core internal');
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the lightest/optimal available standard modules
|
||||
* Resets all modules of the ship
|
||||
*/
|
||||
_optimizeStandard() {
|
||||
this.selectedRefId = 'maxjump';
|
||||
this.props.ship.useLightestStandard();
|
||||
this.props.onChange();
|
||||
this.props.onCargoChange(this.props.ship.cargoCapacity);
|
||||
this.props.onFuelChange(this.props.ship.fuelCapacity);
|
||||
_emptyAll() {
|
||||
this.props.ship.getModules().forEach((slot) => slot.reset());
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill all standard slots with the specificed rating (using max class)
|
||||
* @param {Boolean} shielded True if shield generator should be included
|
||||
* @param {integer} bulkheadIndex Bulkhead to use see Constants.BulkheadNames
|
||||
* Sets all modules to a specific rating
|
||||
* @param {string} rating Module rating to set
|
||||
* @param {string} fsdPPException Custom rating for FSD
|
||||
*/
|
||||
_multiPurpose(shielded, bulkheadIndex) {
|
||||
this.selectedRefId = 'multipurpose';
|
||||
if (bulkheadIndex === 2) this.selectedRefId = 'combat';
|
||||
ShipRoles.multiPurpose(this.props.ship, shielded, bulkheadIndex);
|
||||
this.props.onChange();
|
||||
this.props.onCargoChange(this.props.ship.cargoCapacity);
|
||||
this.props.onFuelChange(this.props.ship.fuelCapacity);
|
||||
_nRated(rating, fsdPPException) {
|
||||
const { ship } = this.props;
|
||||
const pp = ship.getPowerPlant();
|
||||
pp.setItem('powerplant', pp.getSize(), fsdPPException || rating);
|
||||
const eng = ship.getThrusters();
|
||||
eng.setItem('thrusters', eng.getSize(), rating);
|
||||
const fsd = ship.getFSD();
|
||||
fsd.setItem('fsd', fsd.getSize(), fsdPPException || rating);
|
||||
const ls = ship.getLifeSupport();
|
||||
ls.setItem('lifesupport', ls.getSize(), rating);
|
||||
const pd = ship.getPowerDistributor();
|
||||
pd.setItem('powerdistributor', pd.getSize(), rating);
|
||||
const sen = ship.getSensors();
|
||||
sen.setItem('sensors', sen.getSize(), rating);
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trader Build
|
||||
* @param {Boolean} shielded True if shield generator should be included
|
||||
* Creates a new slot for a given module.
|
||||
* @param {Module} m Module to create the slot for
|
||||
* @param {function} warning Warning callback
|
||||
* @return {React.Component} Slot component
|
||||
*/
|
||||
_optimizeCargo(shielded) {
|
||||
this.selectedRefId = 'trader';
|
||||
ShipRoles.trader(this.props.ship, shielded);
|
||||
this.props.onChange();
|
||||
this.props.onCargoChange(this.props.ship.cargoCapacity);
|
||||
this.props.onFuelChange(this.props.ship.fuelCapacity);
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Miner Build
|
||||
* @param {Boolean} shielded True if shield generator should be included
|
||||
*/
|
||||
_optimizeMiner(shielded) {
|
||||
this.selectedRefId = 'miner';
|
||||
ShipRoles.miner(this.props.ship, shielded);
|
||||
this.props.onChange();
|
||||
this.props.onCargoChange(this.props.ship.cargoCapacity);
|
||||
this.props.onFuelChange(this.props.ship.fuelCapacity);
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Explorer role
|
||||
* @param {Boolean} planetary True if Planetary Vehicle Hangar (PVH) should be included
|
||||
*/
|
||||
_optimizeExplorer(planetary) {
|
||||
this.selectedRefId = 'explorer';
|
||||
if (planetary) this.selectedRefId = 'planetary';
|
||||
ShipRoles.explorer(this.props.ship, planetary);
|
||||
this.props.onChange();
|
||||
this.props.onCargoChange(this.props.ship.cargoCapacity);
|
||||
this.props.onFuelChange(this.props.ship.fuelCapacity);
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Racer role
|
||||
*/
|
||||
_optimizeRacer() {
|
||||
this.selectedRefId = 'racer';
|
||||
ShipRoles.racer(this.props.ship);
|
||||
this.props.onChange();
|
||||
this.props.onCargoChange(this.props.ship.cargoCapacity);
|
||||
this.props.onFuelChange(this.props.ship.fuelCapacity);
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the specified bulkhead
|
||||
* @param {Object} bulkhead Bulkhead module details
|
||||
*/
|
||||
_selectBulkhead(bulkhead) {
|
||||
this.props.ship.useBulkhead(bulkhead.index);
|
||||
this.context.tooltip();
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* On right click optimize the standard modules
|
||||
*/
|
||||
_contextMenu() {
|
||||
this._optimizeStandard();
|
||||
_mkSlot(m, warning) {
|
||||
const { currentMenu, propsToShow, onPropToggle } = this.props;
|
||||
return <Slot key={m.getSlot()} m={m} warning={warning} hideSearch={true}
|
||||
currentMenu={currentMenu} propsToShow={propsToShow} onPropToggle={onPropToggle}
|
||||
/>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,108 +69,30 @@ export default class StandardSlotSection extends SlotSection {
|
||||
* @return {Array} Array of Slots
|
||||
*/
|
||||
_getSlots() {
|
||||
let { ship, currentMenu, cargo, fuel } = this.props;
|
||||
let slots = new Array(8);
|
||||
let open = this._openMenu;
|
||||
let select = this._selectModule;
|
||||
let st = ship.standard;
|
||||
let avail = ship.getAvailableModules().standard;
|
||||
let bh = ship.bulkheads;
|
||||
|
||||
slots[0] = <StandardSlot
|
||||
key='bh'
|
||||
slot={bh}
|
||||
modules={ship.getAvailableModules().bulkheads}
|
||||
onOpen={open.bind(this, bh)}
|
||||
onSelect={this._selectBulkhead}
|
||||
selected={currentMenu == bh}
|
||||
onChange={this.props.onChange}
|
||||
ship={ship}
|
||||
/>;
|
||||
|
||||
slots[1] = <StandardSlot
|
||||
key='pp'
|
||||
slot={st[0]}
|
||||
modules={avail[0]}
|
||||
onOpen={open.bind(this, st[0])}
|
||||
onSelect={select.bind(this, st[0])}
|
||||
selected={currentMenu == st[0]}
|
||||
onChange={this.props.onChange}
|
||||
ship={ship}
|
||||
warning={m => m instanceof Module ? m.getPowerGeneration() < ship.powerRetracted : m.pgen < ship.powerRetracted}
|
||||
/>;
|
||||
|
||||
slots[2] = <StandardSlot
|
||||
key='th'
|
||||
slot={st[1]}
|
||||
modules={avail[1]}
|
||||
onOpen={open.bind(this, st[1])}
|
||||
onSelect={select.bind(this, st[1])}
|
||||
selected={currentMenu == st[1]}
|
||||
onChange={this.props.onChange}
|
||||
ship={ship}
|
||||
warning={m => m instanceof Module ? m.getMaxMass() < (ship.unladenMass + cargo + fuel - st[1].m.mass + m.mass) : m.maxmass < (ship.unladenMass + cargo + fuel - st[1].m.mass + m.mass)}
|
||||
/>;
|
||||
|
||||
|
||||
slots[3] = <StandardSlot
|
||||
key='fsd'
|
||||
slot={st[2]}
|
||||
modules={avail[2]}
|
||||
onOpen={open.bind(this, st[2])}
|
||||
onSelect={select.bind(this, st[2])}
|
||||
onChange={this.props.onChange}
|
||||
ship={ship}
|
||||
selected={currentMenu == st[2]}
|
||||
/>;
|
||||
|
||||
slots[4] = <StandardSlot
|
||||
key='ls'
|
||||
slot={st[3]}
|
||||
modules={avail[3]}
|
||||
onOpen={open.bind(this, st[3])}
|
||||
onSelect={select.bind(this, st[3])}
|
||||
onChange={this.props.onChange}
|
||||
ship={ship}
|
||||
selected={currentMenu == st[3]}
|
||||
/>;
|
||||
|
||||
slots[5] = <StandardSlot
|
||||
key='pd'
|
||||
slot={st[4]}
|
||||
modules={avail[4]}
|
||||
onOpen={open.bind(this, st[4])}
|
||||
onSelect={select.bind(this, st[4])}
|
||||
selected={currentMenu == st[4]}
|
||||
onChange={this.props.onChange}
|
||||
ship={ship}
|
||||
warning={m => m instanceof Module ? m.getEnginesCapacity() <= ship.boostEnergy : m.engcap <= ship.boostEnergy}
|
||||
/>;
|
||||
|
||||
slots[6] = <StandardSlot
|
||||
key='ss'
|
||||
slot={st[5]}
|
||||
modules={avail[5]}
|
||||
onOpen={open.bind(this, st[5])}
|
||||
onSelect={select.bind(this, st[5])}
|
||||
selected={currentMenu == st[5]}
|
||||
onChange={this.props.onChange}
|
||||
ship={ship}
|
||||
/>;
|
||||
|
||||
slots[7] = <StandardSlot
|
||||
key='ft'
|
||||
slot={st[6]}
|
||||
modules={avail[6]}
|
||||
onOpen={open.bind(this, st[6])}
|
||||
onSelect={select.bind(this, st[6])}
|
||||
selected={currentMenu == st[6]}
|
||||
onChange={this.props.onChange}
|
||||
ship={ship}
|
||||
warning= {m => m.fuel < st[2].m.maxfuel} // Show warning when fuel tank is smaller than FSD Max Fuel
|
||||
/>;
|
||||
|
||||
return slots;
|
||||
const { ship } = this.props;
|
||||
const fsd = ship.getFSD();
|
||||
return [
|
||||
this._mkSlot(ship.getAlloys()),
|
||||
this._mkSlot(
|
||||
ship.getPowerPlant(),
|
||||
(m) => moduleGet(m, 'powercapacity') < ship.get(CONSUMED_RETR),
|
||||
),
|
||||
this._mkSlot(
|
||||
ship.getThrusters(),
|
||||
(m) => moduleGet(m, 'enginemaximalmass') < ship.get(LADEN_MASS),
|
||||
),
|
||||
this._mkSlot(fsd),
|
||||
this._mkSlot(
|
||||
ship.getPowerDistributor(),
|
||||
(m) => moduleGet(m, 'enginescapacity') <= ship.readProp('boostenergy'),
|
||||
),
|
||||
this._mkSlot(ship.getLifeSupport()),
|
||||
this._mkSlot(ship.getSensors()),
|
||||
this._mkSlot(
|
||||
ship.getCoreFuelTank(),
|
||||
(m) => moduleGet(m, 'fuel') < fsd.get('maxfuel')
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,23 +100,18 @@ export default class StandardSlotSection extends SlotSection {
|
||||
* @param {Function} translate Translate function
|
||||
* @return {React.Component} Section menu
|
||||
*/
|
||||
_getSectionMenu(translate) {
|
||||
let planetaryDisabled = this.props.ship.internal.length < 4;
|
||||
_getSectionMenu() {
|
||||
const { translate } = this.context.language;
|
||||
return <div className='select' onClick={(e) => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
|
||||
<ul>
|
||||
<li className='lc' tabIndex="0" onClick={this._optimizeStandard} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['maxjump'] = smRef}>{translate('Maximize Jump Range')}</li>
|
||||
<li className='lc' tabIndex="0" onClick={this._emptyAll}>{translate('empty all slots')}</li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('roles')}</div>
|
||||
<div className='select-group cap'>{translate('core')}</div>
|
||||
<ul>
|
||||
<li className='lc' tabIndex="0" onClick={this._multiPurpose.bind(this, false, 0)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['multipurpose'] = smRef}>{translate('Multi-purpose')}</li>
|
||||
<li className='lc' tabIndex="0" onClick={this._multiPurpose.bind(this, true, 2)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['combat'] = smRef}>{translate('Combat')}</li>
|
||||
<li className='lc' tabIndex="0" onClick={this._optimizeCargo.bind(this, true)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['trader'] = smRef}>{translate('Trader')}</li>
|
||||
<li className='lc' tabIndex="0" onClick={this._optimizeExplorer.bind(this, false)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['explorer'] = smRef}>{translate('Explorer')}</li>
|
||||
<li className={cn('lc', { disabled: planetaryDisabled })} tabIndex={planetaryDisabled ? '' : '0'} onClick={!planetaryDisabled && this._optimizeExplorer.bind(this, true)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['planetary'] = smRef}>{translate('Planetary Explorer')}</li>
|
||||
<li className='lc' tabIndex="0" onClick={this._optimizeMiner.bind(this, true)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['miner'] = smRef}>{translate('Miner')}</li>
|
||||
<li className='lc' tabIndex="0" onClick={this._optimizeRacer.bind(this)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['racer'] = smRef}>{translate('Racer')}</li>
|
||||
<li className='lc' tabIndex="0" onClick={this._nRated.bind(this, '5', undefined)}>{translate('A-rated')}</li>
|
||||
<li className='lc' tabIndex="0" onClick={this._nRated.bind(this, '2', undefined)}>{translate('D-rated')}</li>
|
||||
<li className='lc' tabIndex="0" onClick={this._nRated.bind(this, '2', '5')}>{translate('D-rated + A-rated FSD/PP')}</li>
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -247,8 +247,7 @@ export class OrbisIcon extends SvgIcon {
|
||||
<path d="m155.34 679.12 173.25-190.21-15.626-13.721-170.9 190.4zm31.01 31.714 202.41-169.1-16.418-14.417-198.76 170.43z"/>
|
||||
<path d="m702.66 178.87-173.25 190.21 15.625 13.721 170.9-190.4zm-31.01-31.714-202.41 169.1 16.418 14.417 198.76-170.43z" />
|
||||
<rect transform="matrix(-.7071 -.7071 .7071 -.7071 429.34 1036.2)" x="387.09" y="420.77" width="84.379" height="16.859" />
|
||||
</g>
|
||||
);
|
||||
</g>);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +262,7 @@ export class MatIcon extends SvgIcon {
|
||||
*/
|
||||
svg() {
|
||||
return<g xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#FF7100" d="M 24.86,4.18
|
||||
<path d="M 24.86,4.18
|
||||
C 24.86,4.18 17.17,7.82 17.17,7.82
|
||||
17.17,7.82 15.35,14.55 15.35,14.55
|
||||
15.35,14.55 24.70,9.75 24.70,9.75
|
||||
|
||||
@@ -6,7 +6,6 @@ import TranslatedComponent from './TranslatedComponent';
|
||||
* Document Root Tooltip
|
||||
*/
|
||||
export default class Tooltip extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
rect: PropTypes.object.isRequired,
|
||||
options: PropTypes.object
|
||||
@@ -127,5 +126,4 @@ export default class Tooltip extends TranslatedComponent {
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { shallowEqual } from '../utils/UtilityFunctions';
|
||||
* Abstract Translated Component
|
||||
*/
|
||||
export default class TranslatedComponent extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
language: PropTypes.object.isRequired,
|
||||
sizeRatio: PropTypes.number.isRequired,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import SlotSection from './SlotSection';
|
||||
import HardpointSlot from './HardpointSlot';
|
||||
import Slot from './Slot';
|
||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
import autoBind from 'auto-bind';
|
||||
|
||||
/**
|
||||
* Utility Slot Section
|
||||
@@ -10,54 +11,34 @@ export default class UtilitySlotSection extends SlotSection {
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context, 'utility', 'utility mounts');
|
||||
this._empty = this._empty.bind(this);
|
||||
this.selectedRefId = null;
|
||||
this.firstRefId = 'emptyall';
|
||||
this.lastRefId = 'po';
|
||||
}
|
||||
/**
|
||||
* Handle focus if the component updates
|
||||
* @param {Object} prevProps React Component properties
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
this._handleSectionFocus(prevProps,this.firstRefId, this.lastRefId);
|
||||
constructor(props) {
|
||||
super(props, 'utility mounts');
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all utility slots and close the menu
|
||||
*/
|
||||
_empty() {
|
||||
this.selectedRefId = this.firstRefId;
|
||||
this.props.ship.emptyUtility();
|
||||
this.props.onChange();
|
||||
this.props.ship.getUtilities().forEach((slot) => slot.reset());
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount module in utility slot, replace all if Alt is held
|
||||
* @param {string} group Module Group name
|
||||
* @param {string} type Module type
|
||||
* @param {string} rating Module Rating
|
||||
* @param {string} name Module name
|
||||
* @param {Synthetic} event Event
|
||||
*/
|
||||
_use(group, rating, name, event) {
|
||||
this.selectedRefId = group;
|
||||
if (rating !== null) this.selectedRefId += '-' + rating;
|
||||
|
||||
this.props.ship.useUtility(group, rating, name, event.getModifierState('Alt'));
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
_use(type, rating, event) {
|
||||
const fillAll = event.getModifierState('Alt');
|
||||
for (const slot of this.props.ship.getUtilities(undefined, true)) {
|
||||
if (slot.isEmpty() || fillAll) {
|
||||
slot.setItem(type, '', rating);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all utility slots on right-click
|
||||
*/
|
||||
_contextMenu() {
|
||||
this._empty();
|
||||
}
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,32 +47,23 @@ export default class UtilitySlotSection extends SlotSection {
|
||||
*/
|
||||
_getSlots() {
|
||||
let slots = [];
|
||||
let { ship, currentMenu } = this.props;
|
||||
let hardpoints = ship.hardpoints;
|
||||
let { ship, currentMenu, propsToShow, onPropToggle } = this.props;
|
||||
let { originSlot, targetSlot } = this.state;
|
||||
let availableModules = ship.getAvailableModules();
|
||||
|
||||
for (let i = 0, l = hardpoints.length; i < l; i++) {
|
||||
let h = hardpoints[i];
|
||||
if (h.maxClass === 0) {
|
||||
slots.push(<HardpointSlot
|
||||
key={i}
|
||||
maxClass={h.maxClass}
|
||||
availableModules={() => availableModules.getHps(h.maxClass)}
|
||||
onOpen={this._openMenu.bind(this,h)}
|
||||
onSelect={this._selectModule.bind(this, h)}
|
||||
onChange={this.props.onChange}
|
||||
selected={currentMenu == h}
|
||||
for (let h of ship.getUtilities(undefined, true)) {
|
||||
slots.push(<Slot
|
||||
key={h.object.Slot}
|
||||
currentMenu={currentMenu}
|
||||
drag={this._drag.bind(this, h)}
|
||||
dragOver={this._dragOverSlot.bind(this, h)}
|
||||
drop={this._drop}
|
||||
dropClass={this._dropClass(h, originSlot, targetSlot)}
|
||||
ship={ship}
|
||||
m={h.m}
|
||||
m={h}
|
||||
enabled={h.enabled ? true : false}
|
||||
propsToShow={propsToShow}
|
||||
onPropToggle={onPropToggle}
|
||||
/>);
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
@@ -101,33 +73,34 @@ export default class UtilitySlotSection extends SlotSection {
|
||||
* @param {Function} translate Translate function
|
||||
* @return {React.Component} Section menu
|
||||
*/
|
||||
_getSectionMenu(translate) {
|
||||
_getSectionMenu() {
|
||||
const { translate } = this.context.language;
|
||||
let _use = this._use;
|
||||
|
||||
return <div className='select' onClick={(e) => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
|
||||
<ul>
|
||||
<li className='lc' tabIndex='0' onClick={this._empty} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['emptyall'] = smRef}>{translate('empty all')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={this._empty}>{translate('empty all')}</li>
|
||||
<li className='optional-hide' style={{ textAlign: 'center', marginTop: '1em' }}>{translate('PHRASE_ALT_ALL')}</li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('sb')}</div>
|
||||
<ul>
|
||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'A', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-A'] = smRef}>A</li>
|
||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'B', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-B'] = smRef}>B</li>
|
||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'C', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-C'] = smRef}>C</li>
|
||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'D', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-D'] = smRef}>D</li>
|
||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'E', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-E'] = smRef}>E</li>
|
||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'shieldbooster', '5')}>A</li>
|
||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'shieldbooster', '4')}>B</li>
|
||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'shieldbooster', '3')}>C</li>
|
||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'shieldbooster', '2')}>D</li>
|
||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'shieldbooster', '1')}>E</li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('hs')}</div>
|
||||
<ul>
|
||||
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'hs', null, 'Heat Sink Launcher')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['hs'] = smRef}>{translate('Heat Sink Launcher')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'heatsinklauncher', '')}>{translate('Heat Sink Launcher')}</li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('ch')}</div>
|
||||
<ul>
|
||||
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'ch', null, 'Chaff Launcher')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ch'] = smRef}>{translate('Chaff Launcher')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'chafflauncher', '')}>{translate('Chaff Launcher')}</li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('po')}</div>
|
||||
<ul>
|
||||
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'po', null, 'Point Defence')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['po'] = smRef}>{translate('Point Defence')}</li>
|
||||
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'pointdefence', '')}>{translate('Point Defence')}</li>
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -2,194 +2,99 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import LineChart from '../components/LineChart';
|
||||
import * as Calc from '../shipyard/Calculations';
|
||||
import { moduleReduce } from 'ed-forge/lib/src/helper';
|
||||
import { chain, keys, mapValues, values } from 'lodash';
|
||||
|
||||
const DAMAGE_DEALT_COLORS = ['#FFFFFF', '#FF0000', '#00FF00', '#7777FF', '#FFFF00', '#FF00FF', '#00FFFF', '#777777'];
|
||||
const PORTION_MAPPINGS = {
|
||||
'absolute': 'absolutedamageportion',
|
||||
'explosive': 'explosivedamageportion',
|
||||
'kinetic': 'kineticdamageportion',
|
||||
'thermal': 'thermicdamageportion',
|
||||
};
|
||||
const MULTS = keys(PORTION_MAPPINGS);
|
||||
|
||||
// TODO: help with this in ed-forge
|
||||
/**
|
||||
* .
|
||||
* @param {Object} opponentDefence .
|
||||
* @returns {Object} .
|
||||
*/
|
||||
function defenceToMults(opponentDefence) {
|
||||
return chain(opponentDefence)
|
||||
.pick(MULTS)
|
||||
.mapKeys((v, k) => PORTION_MAPPINGS[k])
|
||||
.mapValues((resistanceProfile) => resistanceProfile.damageMultiplier)
|
||||
.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Weapon damage chart
|
||||
*/
|
||||
export default class WeaponDamageChart extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
code: PropTypes.string.isRequired,
|
||||
ship: PropTypes.object.isRequired,
|
||||
opponent: PropTypes.object.isRequired,
|
||||
hull: PropTypes.bool.isRequired,
|
||||
opponentDefence: PropTypes.object.isRequired,
|
||||
engagementRange: PropTypes.number.isRequired,
|
||||
opponentSys: PropTypes.number.isRequired,
|
||||
marker: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the initial weapons state
|
||||
*/
|
||||
componentWillMount() {
|
||||
const weaponNames = this._weaponNames(this.props.ship, this.context);
|
||||
const opponentShields = Calc.shieldMetrics(this.props.opponent, this.props.opponentSys);
|
||||
const opponentArmour = Calc.armourMetrics(this.props.opponent);
|
||||
const maxRange = this._calcMaxRange(this.props.ship);
|
||||
const maxDps = this._calcMaxSDps(this.props.ship, this.props.opponent, opponentShields, opponentArmour);
|
||||
|
||||
this.setState({ maxRange, maxDps, weaponNames, opponentShields, opponentArmour, calcSDpsFunc: this._calcSDps.bind(this, this.props.ship, weaponNames, this.props.opponent, opponentShields, opponentArmour, this.props.hull) });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.marker != this.props.marker) {
|
||||
const weaponNames = this._weaponNames(nextProps.ship, nextContext);
|
||||
const opponentShields = Calc.shieldMetrics(nextProps.opponent, nextProps.opponentSys);
|
||||
const opponentArmour = Calc.armourMetrics(nextProps.opponent);
|
||||
const maxRange = this._calcMaxRange(nextProps.ship);
|
||||
const maxDps = this._calcMaxSDps(nextProps.ship, nextProps.opponent, opponentShields, opponentArmour);
|
||||
this.setState({ weaponNames,
|
||||
opponentShields,
|
||||
opponentArmour,
|
||||
maxRange,
|
||||
maxDps,
|
||||
calcSDpsFunc: this._calcSDps.bind(this, nextProps.ship, weaponNames, nextProps.opponent, opponentShields, opponentArmour, nextProps.hull)
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the maximum range of a ship's weapons
|
||||
* @param {Object} ship The ship
|
||||
* @returns {int} The maximum range, in metres
|
||||
*/
|
||||
_calcMaxRange(ship) {
|
||||
let maxRange = 1000; // Minimum
|
||||
for (let i = 0; i < ship.hardpoints.length; i++) {
|
||||
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
|
||||
const thisRange = ship.hardpoints[i].m.getRange();
|
||||
if (thisRange > maxRange) {
|
||||
maxRange = thisRange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return maxRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the maximum sustained single-weapon DPS for this ship
|
||||
* @param {Object} ship The ship
|
||||
* @param {Object} opponent The opponent ship
|
||||
* @param {Object} opponentShields The opponent's shields
|
||||
* @param {Object} opponentArmour The opponent's armour
|
||||
* @return {number} The maximum sustained single-weapon DPS
|
||||
*/
|
||||
_calcMaxSDps(ship, opponent, opponentShields, opponentArmour) {
|
||||
// Additional information to allow effectiveness calculations
|
||||
let maxSDps = 0;
|
||||
for (let i = 0; i < ship.hardpoints.length; i++) {
|
||||
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
|
||||
const m = ship.hardpoints[i].m;
|
||||
|
||||
const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, 0);
|
||||
const thisSDps = sustainedDps.damage.armour.total > sustainedDps.damage.shields.total ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total;
|
||||
if (thisSDps > maxSDps) {
|
||||
maxSDps = thisSDps;
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxSDps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the weapon names for this ship
|
||||
* @param {Object} ship The ship
|
||||
* @param {Object} context The context
|
||||
* @return {array} The weapon names
|
||||
*/
|
||||
_weaponNames(ship, context) {
|
||||
const translate = context.language.translate;
|
||||
let names = [];
|
||||
let num = 1;
|
||||
for (let i = 0; i < ship.hardpoints.length; i++) {
|
||||
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
|
||||
const m = ship.hardpoints[i].m;
|
||||
let name = '' + num++ + ': ' + m.class + m.rating + (m.missile ? '/' + m.missile : '') + ' ' + translate(m.name || m.grp);
|
||||
let engineering;
|
||||
if (m.blueprint && m.blueprint.name) {
|
||||
engineering = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
|
||||
if (m.blueprint.special && m.blueprint.special.id) {
|
||||
engineering += ', ' + translate(m.blueprint.special.name);
|
||||
}
|
||||
}
|
||||
if (engineering) {
|
||||
name = name + ' (' + engineering + ')';
|
||||
}
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the per-weapon sustained DPS for this ship against another ship at a given range
|
||||
* @param {Object} ship The ship
|
||||
* @param {Object} weaponNames The names of the weapons for which to calculate DPS
|
||||
* @param {Object} opponent The target
|
||||
* @param {Object} opponentShields The opponent's shields
|
||||
* @param {Object} opponentArmour The opponent's armour
|
||||
* @param {bool} hull true if to calculate against hull, false if to calculate against shields
|
||||
* @param {Object} engagementRange The engagement range
|
||||
* @return {array} The array of weapon DPS
|
||||
*/
|
||||
_calcSDps(ship, weaponNames, opponent, opponentShields, opponentArmour, hull, engagementRange) {
|
||||
let results = {};
|
||||
let weaponNum = 0;
|
||||
for (let i = 0; i < ship.hardpoints.length; i++) {
|
||||
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
|
||||
const m = ship.hardpoints[i].m;
|
||||
const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementRange);
|
||||
results[weaponNames[weaponNum++]] = hull ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render damage dealt
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { maxRange } = this.state;
|
||||
const { ship, opponent, engagementRange } = this.props;
|
||||
const { language } = this.context;
|
||||
const { translate } = language;
|
||||
const { code, ship, opponentDefence, engagementRange } = this.props;
|
||||
|
||||
const sortOrder = this._sortOrder;
|
||||
const onCollapseExpand = this._onCollapseExpand;
|
||||
|
||||
const code = `${ship.toString()}:${opponent.toString()}`;
|
||||
const hardpoints = ship.getHardpoints();
|
||||
const hardpointsMap = chain(hardpoints)
|
||||
.map((m) => [m.getSlot(), m])
|
||||
.fromPairs()
|
||||
.value();
|
||||
const mults = defenceToMults(opponentDefence);
|
||||
const cb = (range) => {
|
||||
return mapValues(
|
||||
hardpointsMap,
|
||||
(m) => {
|
||||
const sdps = m.get('sustaineddamagepersecond', true);
|
||||
const resistanceMul = chain(mults)
|
||||
.toPairs()
|
||||
.map((pair) => {
|
||||
const [k, mul] = pair;
|
||||
return m.get(k, true) * mul;
|
||||
})
|
||||
.sum()
|
||||
.value();
|
||||
|
||||
const falloff = m.get('damagefalloffrange', true);
|
||||
const rangeMul = Math.min(1, Math.max(0,
|
||||
1 - (range - falloff) / (m.get('maximumrange', true) - falloff)
|
||||
));
|
||||
return sdps * resistanceMul * rangeMul;
|
||||
}
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<LineChart
|
||||
xMax={maxRange}
|
||||
yMax={this.state.maxDps}
|
||||
xMin={0}
|
||||
xMax={moduleReduce(
|
||||
hardpoints, 'maximumrange', true, (a, v) => Math.max(a, v), 1000,
|
||||
)}
|
||||
yMin={0}
|
||||
// Factor in highest damage multiplier to get a safe upper bound
|
||||
yMax={Math.max(1, ...values(mults)) * moduleReduce(
|
||||
hardpoints, 'sustaineddamagepersecond', true, (a, v) => Math.max(a, v), 0,
|
||||
)}
|
||||
xLabel={translate('range')}
|
||||
xUnit={translate('m')}
|
||||
yLabel={translate('sdps')}
|
||||
series={this.state.weaponNames}
|
||||
xMark={this.props.engagementRange}
|
||||
yLabel={translate('sustaineddamagepersecond')}
|
||||
series={hardpoints.map((m) => m.getSlot())}
|
||||
xMark={engagementRange}
|
||||
colors={DAMAGE_DEALT_COLORS}
|
||||
func={this.state.calcSDpsFunc}
|
||||
func={cb}
|
||||
points={200}
|
||||
code={code}
|
||||
/>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"TT_SUMMARY_LADEN_TOTAL_JUMP": "Farthest possible range with full cargo, a full fuel tank, and jumping as far as possible each time",
|
||||
"HELP_MODIFICATIONS_MENU": "Click on a number to enter a new value, or drag along the bar for small changes",
|
||||
"PHRASE_FAIL_EDENGINEER": "Failed to send to EDEngineer (Launch EDEngineer and make sure the API is started then refresh the page.)",
|
||||
"PHRASE_FIREFOX_EDENGINEER": "Sending to EDEngineer is not compatible with Firefox's security settings. Please try again with Chrome.",
|
||||
"PHRASE_FIREFOX_EDENGINEER": "Send to EDEngineer is incompatible with Firefox.",
|
||||
"am": "Auto Field-Maintenance Unit",
|
||||
"bh": "Bulkheads",
|
||||
"bl": "Beam Laser",
|
||||
|
||||
@@ -1,627 +0,0 @@
|
||||
import React from 'react';
|
||||
import Page from './Page';
|
||||
import Router from '../Router';
|
||||
import cn from 'classnames';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import { fromComparison, toComparison } from '../shipyard/Serializer';
|
||||
import Persist from '../stores/Persist';
|
||||
import { SizeMap, ShipFacets } from '../shipyard/Constants';
|
||||
import ComparisonTable from '../components/ComparisonTable';
|
||||
import BarChart from '../components/BarChart';
|
||||
import ModalCompare from '../components/ModalCompare';
|
||||
import ModalExport from '../components/ModalExport';
|
||||
import ModalPermalink from '../components/ModalPermalink';
|
||||
import ModalImport from '../components/ModalImport';
|
||||
import {
|
||||
FloppyDisk,
|
||||
Bin,
|
||||
Download,
|
||||
Embed,
|
||||
Rocket,
|
||||
LinkIcon
|
||||
} from '../components/SvgIcons';
|
||||
import ShortenUrl from '../utils/ShortenUrl';
|
||||
import { comparisonBBCode } from '../utils/BBCode';
|
||||
const browser = require('detect-browser');
|
||||
|
||||
/**
|
||||
* Creates a comparator based on the specified predicate
|
||||
* @param {string} predicate Predicate / propterty name
|
||||
* @return {Function} Comparator
|
||||
*/
|
||||
function sortBy(predicate) {
|
||||
return (a, b) => {
|
||||
if (a[predicate] === b[predicate]) {
|
||||
if (a.name == b.name) {
|
||||
a.buildName.toLowerCase() > b.buildName.toLowerCase() ? 1 : -1;
|
||||
}
|
||||
return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1;
|
||||
}
|
||||
if (typeof a[predicate] == 'string') {
|
||||
return a[predicate].toLowerCase() > b[predicate].toLowerCase() ? 1 : -1;
|
||||
}
|
||||
return a[predicate] > b[predicate] ? 1 : -1;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparison Page
|
||||
*/
|
||||
export default class ComparisonPage extends Page {
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this._sortShips = this._sortShips.bind(this);
|
||||
this._buildsSelected = this._buildsSelected.bind(this);
|
||||
this._updateDiscounts = this._updateDiscounts.bind(this);
|
||||
this.state = this._initState(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* [Re]Create initial state from context
|
||||
* @param {context} context React component context
|
||||
* @return {Object} New state object
|
||||
*/
|
||||
_initState(context) {
|
||||
let defaultFacets = [13, 12, 11, 9, 6, 4, 1, 3, 2]; // Reverse order of Armour, Shields, Speed, Jump Range, Cargo Capacity, Cost, DPS, EPS, HPS
|
||||
let params = context.route.params;
|
||||
let code = params.code;
|
||||
let name = params.name ? decodeURIComponent(params.name) : null;
|
||||
let newName = '';
|
||||
let compareMode = !code;
|
||||
let builds = [];
|
||||
let saved = false;
|
||||
let predicate = 'name';
|
||||
let desc = false;
|
||||
let importObj = {};
|
||||
|
||||
if (compareMode) {
|
||||
if (name == 'all') {
|
||||
let allBuilds = Persist.getBuilds();
|
||||
newName = name;
|
||||
for (let shipId in allBuilds) {
|
||||
for (let buildName in allBuilds[shipId]) {
|
||||
if (buildName && allBuilds[shipId][buildName]) {
|
||||
builds.push(
|
||||
this._createBuild(
|
||||
shipId,
|
||||
buildName,
|
||||
allBuilds[shipId][buildName]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let comparisonData = Persist.getComparison(name);
|
||||
if (comparisonData) {
|
||||
defaultFacets = comparisonData.facets;
|
||||
comparisonData.builds.forEach(b =>
|
||||
builds.push(this._createBuild(b.shipId, b.buildName))
|
||||
);
|
||||
saved = true;
|
||||
newName = name;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
let comparisonData = toComparison(code);
|
||||
defaultFacets = comparisonData.f;
|
||||
newName = name = comparisonData.n;
|
||||
predicate = comparisonData.p;
|
||||
desc = comparisonData.d;
|
||||
comparisonData.b.forEach(build => {
|
||||
builds.push(this._createBuild(build.s, build.n, build.c));
|
||||
if (!importObj[build.s]) {
|
||||
importObj[build.s] = {};
|
||||
}
|
||||
importObj[build.s][build.n] = build.c;
|
||||
});
|
||||
} catch (e) {
|
||||
throw { type: 'bad-comparison', message: e.message, details: e };
|
||||
}
|
||||
}
|
||||
|
||||
let facets = [];
|
||||
let selectedLength = defaultFacets.length;
|
||||
let selectedFacets = new Array(selectedLength);
|
||||
|
||||
for (let i = 0; i < ShipFacets.length; i++) {
|
||||
let facet = Object.assign({}, ShipFacets[i]);
|
||||
let defaultIndex = defaultFacets.indexOf(facet.i);
|
||||
if (defaultIndex == -1) {
|
||||
facets.push(facet);
|
||||
} else {
|
||||
facet.active = true;
|
||||
selectedFacets[selectedLength - defaultIndex - 1] = facet;
|
||||
}
|
||||
}
|
||||
|
||||
facets = selectedFacets.concat(facets);
|
||||
builds.sort(sortBy(predicate));
|
||||
|
||||
return {
|
||||
title: 'Coriolis EDCD Edition - Compare',
|
||||
predicate,
|
||||
desc,
|
||||
facets,
|
||||
builds,
|
||||
compareMode,
|
||||
code,
|
||||
name,
|
||||
newName,
|
||||
saved,
|
||||
importObj
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Create a Ship instance / build
|
||||
* @param {string} id Ship Id
|
||||
* @param {name} name Build name
|
||||
* @param {string} code Optional - Serialized ship code
|
||||
* @return {Object} Ship instance with build name
|
||||
*/
|
||||
_createBuild(id, name, code) {
|
||||
code = code ? code : Persist.getBuild(id, name); // Retrieve build code if not passed
|
||||
|
||||
if (!code) {
|
||||
// No build found
|
||||
return;
|
||||
}
|
||||
|
||||
let data = Ships[id]; // Get ship properties
|
||||
let b = new Ship(id, data.properties, data.slots); // Create a new Ship instance
|
||||
b.buildFrom(code); // Populate components from code
|
||||
b.buildName = name;
|
||||
b.applyDiscounts(Persist.getShipDiscount(), Persist.getModuleDiscount());
|
||||
return b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state with the specified sort predicates
|
||||
* @param {String} predicate Sort predicate - property name
|
||||
*/
|
||||
_sortShips(predicate) {
|
||||
let { builds, desc } = this.state;
|
||||
if (this.state.predicate == predicate) {
|
||||
desc = !desc;
|
||||
}
|
||||
|
||||
builds.sort(sortBy(predicate));
|
||||
|
||||
if (desc) {
|
||||
builds.reverse();
|
||||
}
|
||||
|
||||
this.setState({ predicate, desc });
|
||||
}
|
||||
|
||||
/**
|
||||
* Show selected builds modal
|
||||
*/
|
||||
_selectBuilds() {
|
||||
this.context.showModal(
|
||||
<ModalCompare
|
||||
onSelect={this._buildsSelected}
|
||||
builds={this.state.builds}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selected builds with new list
|
||||
* @param {Array} newBuilds List of new builds
|
||||
*/
|
||||
_buildsSelected(newBuilds) {
|
||||
this.context.hideModal();
|
||||
let builds = [];
|
||||
|
||||
for (let b of newBuilds) {
|
||||
builds.push(this._createBuild(b.id, b.buildName));
|
||||
}
|
||||
|
||||
this.setState({ builds, saved: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle facet display
|
||||
* @param {string} facet Facet / Ship Property
|
||||
*/
|
||||
_toggleFacet(facet) {
|
||||
facet.active = !facet.active;
|
||||
this.setState({ facets: [].concat(this.state.facets), saved: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle facet drag
|
||||
* @param {Event} e Drag Event
|
||||
*/
|
||||
_facetDrag(e) {
|
||||
this.nodeAfter = false;
|
||||
this.dragged = e.currentTarget;
|
||||
let placeholder = (this.placeholder = document.createElement('li'));
|
||||
placeholder.style.width = Math.round(this.dragged.offsetWidth) + 'px';
|
||||
placeholder.className = 'facet-placeholder';
|
||||
if (!browser || (browser.name !== 'edge' && browser.name !== 'ie')) {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/html', e.currentTarget);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle facet drop
|
||||
* @param {Event} e Drop Event
|
||||
*/
|
||||
_facetDrop(e) {
|
||||
this.dragged.parentNode.removeChild(this.placeholder);
|
||||
let facets = this.state.facets;
|
||||
let frm = Number(this.dragged.dataset.i);
|
||||
let to = Number(this.over.dataset.i);
|
||||
|
||||
if (frm < to) {
|
||||
to--;
|
||||
}
|
||||
if (this.nodeAfter) {
|
||||
to++;
|
||||
}
|
||||
|
||||
facets.splice(to, 0, facets.splice(frm, 1)[0]);
|
||||
this.dragged.style.display = null;
|
||||
this.setState({ facets: [].concat(facets), saved: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle facet drag over
|
||||
* @param {Event} e Drag over Event
|
||||
*/
|
||||
_facetDragOver(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.target.className == 'facet-placeholder') {
|
||||
return;
|
||||
} else if (e.target != e.currentTarget) {
|
||||
this.over = e.target;
|
||||
this.dragged.style.display = 'none';
|
||||
let relX = e.clientX - this.over.getBoundingClientRect().left;
|
||||
let width = this.over.offsetWidth / 2;
|
||||
let parent = e.target.parentNode;
|
||||
|
||||
if (parent == e.currentTarget) {
|
||||
if (relX > width && this.dragged != e.target) {
|
||||
this.nodeAfter = true;
|
||||
parent.insertBefore(this.placeholder, e.target.nextElementSibling);
|
||||
} else {
|
||||
this.nodeAfter = false;
|
||||
parent.insertBefore(this.placeholder, e.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle name change and update state
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_onNameChange(e) {
|
||||
this.setState({ newName: e.target.value, saved: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the current comparison
|
||||
*/
|
||||
_delete() {
|
||||
Persist.deleteComparison(this.state.name);
|
||||
Router.go('/compare');
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the comparison builds
|
||||
*/
|
||||
_import() {
|
||||
let builds = {};
|
||||
|
||||
for (let ship of this.state.builds) {
|
||||
if (!builds[ship.id]) {
|
||||
builds[ship.id] = {};
|
||||
}
|
||||
builds[ship.id][ship.buildName] = ship.toString();
|
||||
}
|
||||
|
||||
this.context.showModal(<ModalImport builds={builds} />);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current comparison
|
||||
*/
|
||||
_save() {
|
||||
let { newName, builds, facets } = this.state;
|
||||
let selectedFacets = [];
|
||||
|
||||
facets.forEach(f => {
|
||||
if (f.active) {
|
||||
selectedFacets.unshift(f.i);
|
||||
}
|
||||
});
|
||||
|
||||
Persist.saveComparison(newName, builds, selectedFacets);
|
||||
Router.replace(`/compare/${encodeURIComponent(this.state.newName)}`);
|
||||
this.setState({ name: newName, saved: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize and generate a long URL for the current comparison
|
||||
* @return {string} URL for serialized comparison
|
||||
*/
|
||||
_buildUrl() {
|
||||
let { facets, builds, name, predicate, desc } = this.state;
|
||||
let selectedFacets = [];
|
||||
|
||||
for (let f of facets) {
|
||||
if (f.active) {
|
||||
selectedFacets.unshift(f.i);
|
||||
}
|
||||
}
|
||||
|
||||
let code = fromComparison(name, builds, selectedFacets, predicate, desc);
|
||||
let loc = window.location;
|
||||
return (
|
||||
loc.protocol +
|
||||
'//' +
|
||||
loc.host +
|
||||
'/comparison?code=' +
|
||||
encodeURIComponent(code)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the long permalink URL
|
||||
*/
|
||||
_genPermalink() {
|
||||
this.context.showModal(<ModalPermalink url={this._buildUrl()} />);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate E:D Forum BBCode and show in the export modal
|
||||
*/
|
||||
_genBBcode() {
|
||||
let { translate, formats } = this.context.language;
|
||||
let { facets, builds } = this.state;
|
||||
|
||||
let generator = callback => {
|
||||
let url = this._buildUrl();
|
||||
ShortenUrl(
|
||||
url,
|
||||
shortenedUrl =>
|
||||
callback(
|
||||
comparisonBBCode(translate, formats, facets, builds, shortenedUrl)
|
||||
),
|
||||
error =>
|
||||
callback(comparisonBBCode(translate, formats, facets, builds, url))
|
||||
);
|
||||
};
|
||||
|
||||
this.context.showModal(
|
||||
<ModalExport
|
||||
title={translate('forum') + ' BBCode'}
|
||||
generator={generator}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dimenions from rendered DOM
|
||||
*/
|
||||
_updateDimensions() {
|
||||
this.setState({
|
||||
chartWidth: this.chartRef.offsetWidth
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all ship costs on disount change
|
||||
*/
|
||||
_updateDiscounts() {
|
||||
let shipDiscount = Persist.getShipDiscount();
|
||||
let moduleDiscount = Persist.getModuleDiscount();
|
||||
let builds = [];
|
||||
|
||||
for (let b of this.state.builds) {
|
||||
builds.push(b.applyDiscounts(shipDiscount, moduleDiscount));
|
||||
}
|
||||
|
||||
this.setState({ builds });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
if (this.context.route !== nextContext.route) {
|
||||
// Only reinit state if the route has changed
|
||||
this.setState(this._initState(nextContext));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add listeners when about to mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
this.resizeListener = this.context.onWindowResize(this._updateDimensions);
|
||||
this.persistListener = Persist.addListener(
|
||||
'discounts',
|
||||
this._updateDiscounts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger DOM updates on mount
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updateDimensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove listeners on unmount
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this.resizeListener.remove();
|
||||
this.persistListener.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Page
|
||||
* @return {React.Component} The page contents
|
||||
*/
|
||||
renderPage() {
|
||||
let translate = this.context.language.translate;
|
||||
let compareHeader;
|
||||
let {
|
||||
newName,
|
||||
name,
|
||||
saved,
|
||||
builds,
|
||||
facets,
|
||||
predicate,
|
||||
desc,
|
||||
chartWidth
|
||||
} = this.state;
|
||||
|
||||
if (this.state.compareMode) {
|
||||
compareHeader = (
|
||||
<tr>
|
||||
<td className="head">{translate('comparison')}</td>
|
||||
<td>
|
||||
<input
|
||||
value={newName}
|
||||
onChange={this._onNameChange}
|
||||
placeholder={translate('Enter Name')}
|
||||
maxLength="50"
|
||||
/>
|
||||
<button
|
||||
onClick={this._save}
|
||||
disabled={!newName || newName == 'all' || saved}
|
||||
>
|
||||
<FloppyDisk className="lg" />
|
||||
<span className="button-lbl">{translate('save')}</span>
|
||||
</button>
|
||||
<button onClick={this._delete} disabled={name == 'all' || !saved}>
|
||||
<Bin className="lg warning" />
|
||||
</button>
|
||||
<button onClick={this._selectBuilds}>
|
||||
<Rocket className="lg" />
|
||||
<span className="button-lbl">{translate('builds')}</span>
|
||||
</button>
|
||||
<button
|
||||
className="r"
|
||||
onClick={this._genPermalink}
|
||||
disabled={builds.length == 0}
|
||||
>
|
||||
<LinkIcon className="lg" />
|
||||
<span className="button-lbl">{translate('permalink')}</span>
|
||||
</button>
|
||||
<button
|
||||
className="r"
|
||||
onClick={this._genBBcode}
|
||||
disabled={builds.length == 0}
|
||||
>
|
||||
<Embed className="lg" />
|
||||
<span className="button-lbl">{translate('forum')}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
} else {
|
||||
compareHeader = (
|
||||
<tr>
|
||||
<td className="head">{translate('comparison')}</td>
|
||||
<td>
|
||||
<h3>{name}</h3>
|
||||
<button className="r" onClick={this._import}>
|
||||
<Download className="lg" />
|
||||
{translate('import')}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'page'}
|
||||
style={{ fontSize: this.context.sizeRatio + 'em' }}
|
||||
>
|
||||
<table id="comparison">
|
||||
<tbody>
|
||||
{compareHeader}
|
||||
<tr key="facets">
|
||||
<td className="head">{translate('compare')}</td>
|
||||
<td>
|
||||
<ul id="facet-container" onDragOver={this._facetDragOver}>
|
||||
{facets.map((f, i) => (
|
||||
<li
|
||||
key={f.title}
|
||||
data-i={i}
|
||||
draggable="true"
|
||||
onDragStart={this._facetDrag}
|
||||
onDragEnd={this._facetDrop}
|
||||
className={cn('facet', { active: f.active })}
|
||||
onClick={this._toggleFacet.bind(this, f)}
|
||||
>
|
||||
{'↔ ' + translate(f.title)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ComparisonTable
|
||||
builds={builds}
|
||||
facets={facets}
|
||||
onSort={this._sortShips}
|
||||
predicate={predicate}
|
||||
desc={desc}
|
||||
/>
|
||||
|
||||
{!builds.length ? (
|
||||
<div className="chart" ref={node => (this.chartRef = node)}>
|
||||
{translate('PHRASE_NO_BUILDS')}
|
||||
</div>
|
||||
) : (
|
||||
facets.filter(f => f.active).map((f, i) => (
|
||||
<div
|
||||
key={f.title}
|
||||
className="chart"
|
||||
ref={i == 0 ? node => (this.chartRef = node) : null}
|
||||
>
|
||||
<h3
|
||||
className="ptr"
|
||||
onClick={this._sortShips.bind(this, f.props[0])}
|
||||
>
|
||||
{translate(f.title)}
|
||||
</h3>
|
||||
<BarChart
|
||||
width={chartWidth}
|
||||
data={builds}
|
||||
properties={f.props}
|
||||
labels={f.lbls}
|
||||
unit={translate(f.unit)}
|
||||
format={f.fmt}
|
||||
title={translate(f.title)}
|
||||
predicate={predicate}
|
||||
desc={desc}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import PropTypes from 'prop-types';
|
||||
* Unexpected Error page / block
|
||||
*/
|
||||
export default class ErrorDetails extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
route: PropTypes.object.isRequired,
|
||||
language: PropTypes.object.isRequired
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,6 @@ import { shallowEqual } from '../utils/UtilityFunctions';
|
||||
* Abstract/Base Page
|
||||
*/
|
||||
export default class Page extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
closeMenu: PropTypes.func.isRequired,
|
||||
hideModal: PropTypes.func.isRequired,
|
||||
@@ -92,5 +91,4 @@ export default class Page extends React.Component {
|
||||
}
|
||||
return this.renderPage();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,102 +1,81 @@
|
||||
import React from 'react';
|
||||
import Page from './Page';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import cn from 'classnames';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import { Factory } from 'ed-forge';
|
||||
import { JUMP_METRICS } from 'ed-forge/lib/src/ship-stats';
|
||||
import { SizeMap } from '../shipyard/Constants';
|
||||
import Link from '../components/Link';
|
||||
|
||||
/**
|
||||
* Counts the hardpoints by class/size
|
||||
* @param {Object} slot Hardpoint Slot model
|
||||
*/
|
||||
function countHp(slot) {
|
||||
this.hp[slot.maxClass]++;
|
||||
this.hpCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the internal slots and aggregated properties
|
||||
* @param {Object} slot Internal Slots
|
||||
*/
|
||||
function countInt(slot) {
|
||||
let crEligible = !slot.eligible || slot.eligible.cr;
|
||||
this.int[slot.maxClass - 1]++; // Subtract 1 since there is no Class 0 Internal compartment
|
||||
this.intCount++;
|
||||
this.maxCargo += crEligible ?
|
||||
ModuleUtils.findInternal('cr', slot.maxClass, 'E').cargo :
|
||||
0;
|
||||
|
||||
// if no eligiblity, then assume pce
|
||||
let passSlotType = null;
|
||||
let passSlotRating = null;
|
||||
if (!slot.eligible || slot.eligible.pce) {
|
||||
passSlotType = 'pce';
|
||||
passSlotRating = 'E';
|
||||
} else if (slot.eligible.pci) {
|
||||
passSlotType = 'pci';
|
||||
passSlotRating = 'D';
|
||||
} else if (slot.eligible.pcm) {
|
||||
passSlotType = 'pcm';
|
||||
passSlotRating = 'C';
|
||||
} else if (slot.eligible.pcq) {
|
||||
passSlotType = 'pcq';
|
||||
passSlotRating = 'B';
|
||||
}
|
||||
let passengerBay = passSlotType ?
|
||||
ModuleUtils.findMaxInternal(passSlotType, slot.maxClass, passSlotRating) :
|
||||
null;
|
||||
this.maxPassengers += passengerBay ? passengerBay.passengers : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Ship summary and aggregated properties
|
||||
* @param {String} shipId Ship Id
|
||||
* @param {Object} shipData Ship Default Data
|
||||
* @return {Object} Ship summary and aggregated properties
|
||||
*/
|
||||
function shipSummary(shipId, shipData) {
|
||||
function shipSummary(shipId) {
|
||||
// Build Ship
|
||||
let ship = Factory.newShip(shipId);
|
||||
|
||||
let coreSizes = ship.readMeta('coreSizes');
|
||||
let { jumpRangeLaden, totalRange } = ship.getMetrics(JUMP_METRICS);
|
||||
let summary = {
|
||||
agility: ship.readProp('pitch') + ship.readProp('yaw') + ship.readProp('roll'),
|
||||
baseArmour: ship.readProp('basearmour'),
|
||||
baseShieldStrength: ship.readProp('baseshieldstrength'),
|
||||
boost: ship.readProp('boost'),
|
||||
class: ship.readMeta('class'),
|
||||
crew: ship.readMeta('crew'),
|
||||
id: shipId,
|
||||
hardness: ship.readProp('hardness'),
|
||||
hpCount: 0,
|
||||
hullMass: ship.readProp('hullmass'),
|
||||
intCount: 0,
|
||||
beta: shipData.beta,
|
||||
maxCargo: 0,
|
||||
maxPassengers: 0,
|
||||
masslock: ship.readProp('masslock'),
|
||||
hp: [0, 0, 0, 0, 0], // Utility, Small, Medium, Large, Huge
|
||||
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
|
||||
jumpRangeLaden,
|
||||
pitch: ship.readProp('pitch'),
|
||||
retailCost: ship.readMeta('retailCost'),
|
||||
roll: ship.readProp('roll'),
|
||||
speed: ship.readProp('speed'),
|
||||
standard: [
|
||||
'powerplant',
|
||||
'mainengines',
|
||||
'frameshiftdrive',
|
||||
'lifesupport',
|
||||
'powerdistributor',
|
||||
'radar',
|
||||
'fueltank'
|
||||
].map(k => coreSizes[k]),
|
||||
totalRange,
|
||||
yaw: ship.readProp('yaw'),
|
||||
};
|
||||
Object.assign(summary, shipData.properties);
|
||||
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||
|
||||
// Build Ship
|
||||
ship.buildWith(shipData.defaults); // Populate with stock/default components
|
||||
ship.hardpoints.forEach(countHp.bind(summary)); // Count Hardpoints by class
|
||||
ship.internal.forEach(countInt.bind(summary)); // Count Internal Compartments by class
|
||||
summary.retailCost = ship.totalCost; // Record Stock/Default/retail cost
|
||||
ship.optimizeMass({ pd: '1D' }); // Optimize Mass with 1D PD for maximum possible jump range
|
||||
summary.maxJumpRange = ship.unladenRange; // Record Jump Range
|
||||
// Count Hardpoints by class
|
||||
ship.getHardpoints(undefined, true).forEach(hardpoint => {
|
||||
summary.hp[hardpoint.getSizeNum()]++;
|
||||
summary.hpCount++;
|
||||
});
|
||||
// Count Internal Compartments by class
|
||||
let maxCargo = 0, maxPassengers = 0;
|
||||
ship.getInternals(undefined, true).forEach(internal => {
|
||||
const size = String(internal.getSizeNum());
|
||||
summary.int[size]++;
|
||||
summary.intCount++;
|
||||
|
||||
// Best thrusters
|
||||
let th;
|
||||
if (ship.standard[1].maxClass === 3) {
|
||||
th = 'tz';
|
||||
} else if (ship.standard[1].maxClass === 2) {
|
||||
th = 'u0';
|
||||
} else {
|
||||
th = ship.standard[1].maxClass + 'A';
|
||||
}
|
||||
// Try cargo racks
|
||||
try {
|
||||
internal.setItem('cargorack', size);
|
||||
maxCargo += internal.get('cargo');
|
||||
} catch {}
|
||||
// Try economy cabins
|
||||
try {
|
||||
internal.setItem('passengercabins', size < '6' ? size : '6', '1');
|
||||
maxPassengers += internal.get('cabincapacity');
|
||||
} catch {}
|
||||
});
|
||||
|
||||
ship.optimizeMass({ th, fsd: '2D', ft: '1C' }); // Optmize mass with Max Thrusters
|
||||
summary.topSpeed = ship.topSpeed;
|
||||
summary.topBoost = ship.topBoost;
|
||||
summary.baseArmour = ship.armour;
|
||||
summary.maxCargo = maxCargo;
|
||||
summary.maxPassengers = maxPassengers;
|
||||
|
||||
return summary;
|
||||
}
|
||||
@@ -117,14 +96,14 @@ export default class ShipyardPage extends Page {
|
||||
|
||||
if (!ShipyardPage.cachedShipSummaries) {
|
||||
ShipyardPage.cachedShipSummaries = [];
|
||||
for (let s in Ships) {
|
||||
ShipyardPage.cachedShipSummaries.push(shipSummary(s, Ships[s]));
|
||||
for (let s of Factory.getAllShipTypes()) {
|
||||
ShipyardPage.cachedShipSummaries.push(shipSummary(s));
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
title: 'Coriolis EDCD Edition - Shipyard',
|
||||
shipPredicate: 'name',
|
||||
shipPredicate: 'id',
|
||||
shipDesc: true,
|
||||
shipSummaries: ShipyardPage.cachedShipSummaries,
|
||||
compare: {},
|
||||
@@ -157,7 +136,7 @@ export default class ShipyardPage extends Page {
|
||||
* @private
|
||||
*/
|
||||
_toggleGroupCompared() {
|
||||
this.setState({groupCompared: !this.state.groupCompared})
|
||||
this.setState({ groupCompared: !this.state.groupCompared });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,16 +188,18 @@ export default class ShipyardPage extends Page {
|
||||
<td className="ri cap">{translate(SizeMap[s.class])}</td>
|
||||
<td className="ri">{fInt(s.crew)}</td>
|
||||
<td className="ri">{s.masslock}</td>
|
||||
<td className="ri">{fInt(s.agility)}</td>
|
||||
<td className="ri">{fInt(s.hardness)}</td>
|
||||
<td className="ri">{fInt(s.hullMass)}</td>
|
||||
<td className="ri">{fInt(s.agility)}</td>
|
||||
<td className="ri">{fInt(s.speed)}</td>
|
||||
<td className="ri">{fInt(s.boost)}</td>
|
||||
<td className="ri">{fInt(s.pitch)}</td>
|
||||
<td className="ri">{fInt(s.yaw)}</td>
|
||||
<td className="ri">{fInt(s.roll)}</td>
|
||||
<td className="ri">{fRound(s.jumpRangeLaden)}</td>
|
||||
<td className="ri">{fRound(s.totalRange)}</td>
|
||||
<td className="ri">{fInt(s.hardness)}</td>
|
||||
<td className="ri">{fInt(s.baseArmour)}</td>
|
||||
<td className="ri">{fInt(s.baseShieldStrength)}</td>
|
||||
<td className="ri">{fInt(s.topSpeed)}</td>
|
||||
<td className="ri">{fInt(s.topBoost)}</td>
|
||||
<td className="ri">{fRound(s.maxJumpRange)}</td>
|
||||
<td className="ri">{fInt(s.maxCargo)}</td>
|
||||
<td className="ri">{fInt(s.maxPassengers)}</td>
|
||||
<td className="cn">{s.standard[0]}</td>
|
||||
@@ -254,7 +235,6 @@ export default class ShipyardPage extends Page {
|
||||
let { translate, formats, units } = language;
|
||||
let hide = this.context.tooltip.bind(null, null);
|
||||
let fInt = formats.int;
|
||||
let fRound = formats.round;
|
||||
let { shipSummaries, shipPredicate, shipPredicateIndex, compare, groupCompared } = this.state;
|
||||
let sortShips = (predicate, index) =>
|
||||
this._sortShips.bind(this, predicate, index);
|
||||
@@ -298,7 +278,7 @@ export default class ShipyardPage extends Page {
|
||||
}
|
||||
|
||||
if (valA == valB) {
|
||||
if (a.name > b.name) {
|
||||
if (a.id > b.id) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
@@ -334,7 +314,7 @@ export default class ShipyardPage extends Page {
|
||||
onClick={() => this._toggleCompare(s.id)}
|
||||
>
|
||||
<td className="le">
|
||||
<Link href={'/outfit/' + s.id}>{s.name} {s.beta === true ? '(Beta)' : null}</Link>
|
||||
<Link href={'/outfit/' + s.id}>{s.id} {s.beta === true ? '(Beta)' : null}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -342,16 +322,16 @@ export default class ShipyardPage extends Page {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page" style={{fontSize: sizeRatio + 'em'}}>
|
||||
<div className="page" style={{ fontSize: sizeRatio + 'em' }}>
|
||||
<div className="content-wrapper">
|
||||
<div className="shipyard-table-wrapper">
|
||||
<table style={{width: '12em', position: 'absolute', zIndex: 1}} className="shipyard-table">
|
||||
<table style={{ width: '12em', position: 'absolute', zIndex: 1 }} className="shipyard-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="le rgt"> </th>
|
||||
</tr>
|
||||
<tr className="main">
|
||||
<th className="sortable le rgt" onClick={sortShips('name')}>
|
||||
<th className="sortable le rgt" onClick={sortShips('id')}>
|
||||
{translate('ship')}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -367,103 +347,87 @@ export default class ShipyardPage extends Page {
|
||||
<table style={{ marginLeft: 'calc(12em - 1px)', zIndex: 0 }} className="shipyard-table">
|
||||
<thead>
|
||||
<tr className="main">
|
||||
{/* First all headers that spread out over three rows */}
|
||||
{/* cost placeholder */}
|
||||
<th> </th>
|
||||
<th
|
||||
rowSpan={3}
|
||||
className="sortable"
|
||||
onClick={sortShips('class')}
|
||||
>
|
||||
<th rowSpan={3} className="sortable" onClick={sortShips('class')}>
|
||||
{translate('size')}
|
||||
</th>
|
||||
<th
|
||||
rowSpan={3}
|
||||
className="sortable"
|
||||
onClick={sortShips('crew')}
|
||||
>
|
||||
<th rowSpan={3} className="sortable" onClick={sortShips('crew')}>
|
||||
{translate('crew')}
|
||||
</th>
|
||||
<th
|
||||
rowSpan={3}
|
||||
className="sortable"
|
||||
<th rowSpan={3} className="sortable"
|
||||
onMouseEnter={termtip.bind(null, 'mass lock factor')}
|
||||
onMouseLeave={hide}
|
||||
onClick={sortShips('masslock')}
|
||||
>
|
||||
onMouseLeave={hide} onClick={sortShips('masslock')}>
|
||||
{translate('MLF')}
|
||||
</th>
|
||||
<th
|
||||
rowSpan={3}
|
||||
className="sortable"
|
||||
onClick={sortShips('agility')}
|
||||
>
|
||||
{translate('agility')}
|
||||
</th>
|
||||
<th
|
||||
rowSpan={3}
|
||||
className="sortable"
|
||||
onMouseEnter={termtip.bind(null, 'hardness')}
|
||||
onMouseLeave={hide}
|
||||
onClick={sortShips('hardness')}
|
||||
>
|
||||
{translate('hrd')}
|
||||
</th>
|
||||
{/* hull mass placeholder */}
|
||||
<th> </th>
|
||||
<th colSpan={6}>{translate('agility')}</th>
|
||||
<th colSpan={2}>{translate('travel')}</th>
|
||||
<th colSpan={3}>{translate('defence')}</th>
|
||||
{/* cargo placeholder */}
|
||||
<th> </th>
|
||||
{/* pax placeholder */}
|
||||
<th> </th>
|
||||
<th colSpan={4}>{translate('base')}</th>
|
||||
<th colSpan={5}>{translate('max')}</th>
|
||||
<th className="lft" colSpan={7} />
|
||||
<th className="lft" colSpan={5} />
|
||||
<th className="lft" colSpan={8} />
|
||||
</tr>
|
||||
<tr>
|
||||
<th
|
||||
className="sortable lft"
|
||||
onClick={sortShips('retailCost')}
|
||||
>
|
||||
{/* Now all headers in a second-row */}
|
||||
<th className="sortable lft" onClick={sortShips('retailCost')}>
|
||||
{translate('cost')}
|
||||
</th>
|
||||
<th className="sortable lft" onClick={sortShips('hullMass')}>
|
||||
{translate('hull')}
|
||||
</th>
|
||||
<th className="sortable lft" onClick={sortShips('speed')}>
|
||||
<th className="sortable lft" onClick={sortShips('agility')}>
|
||||
{translate('rating')}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('speed')}>
|
||||
{translate('speed')}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('boost')}>
|
||||
{translate('boost')}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('pitch')}>
|
||||
{translate('pitch')}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('yaw')}>
|
||||
{translate('yaw')}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('roll')}>
|
||||
{translate('roll')}
|
||||
</th>
|
||||
<th className="sortable lft" onClick={sortShips('jumpRangeLaden')}>
|
||||
{translate('jump')}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('totalRange')}>
|
||||
{translate('range')}
|
||||
</th>
|
||||
<th className="sortable lft" onMouseEnter={termtip.bind(null, 'hardness')}
|
||||
onMouseLeave={hide} onClick={sortShips('hardness')}>
|
||||
{translate('hrd')}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('baseArmour')}>
|
||||
{translate('armour')}
|
||||
</th>
|
||||
<th
|
||||
className="sortable"
|
||||
onClick={sortShips('baseShieldStrength')}
|
||||
>
|
||||
<th className="sortable" onClick={sortShips('baseShieldStrength')}>
|
||||
{translate('shields')}
|
||||
</th>
|
||||
|
||||
<th className="sortable lft" onClick={sortShips('topSpeed')}>
|
||||
{translate('speed')}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('topBoost')}>
|
||||
{translate('boost')}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('maxJumpRange')}>
|
||||
{translate('jump')}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('maxCargo')}>
|
||||
<th className="sortable lft" onClick={sortShips('maxCargo')}>
|
||||
{translate('cargo')}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('maxPassengers')} onMouseEnter={termtip.bind(null, 'passenger capacity')}
|
||||
onMouseLeave={hide}>
|
||||
<th className="sortable lft" onClick={sortShips('maxPassengers')}
|
||||
onMouseEnter={termtip.bind(null, 'passenger capacity')} onMouseLeave={hide}>
|
||||
{translate('pax')}
|
||||
</th>
|
||||
<th className="lft" colSpan={7}>
|
||||
{translate('core module classes')}
|
||||
</th>
|
||||
<th
|
||||
colSpan={5}
|
||||
className="sortable lft"
|
||||
onClick={sortShips('hpCount')}
|
||||
>
|
||||
<th colSpan={5} className="sortable lft" onClick={sortShips('hpCount')}>
|
||||
{translate('hardpoints')}
|
||||
</th>
|
||||
<th
|
||||
@@ -475,6 +439,7 @@ export default class ShipyardPage extends Page {
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{/* Third row headers, i.e., units */}
|
||||
<th
|
||||
className="sortable lft"
|
||||
onClick={sortShips('retailCost')}
|
||||
@@ -484,12 +449,31 @@ export default class ShipyardPage extends Page {
|
||||
<th className="sortable lft" onClick={sortShips('hullMass')}>
|
||||
{units.T}
|
||||
</th>
|
||||
<th className="sortable lft" onClick={sortShips('speed')}>
|
||||
{/* agility rating placeholder */}
|
||||
<th className="lft"> </th>
|
||||
<th className="sortable" onClick={sortShips('speed')}>
|
||||
{units['m/s']}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('boost')}>
|
||||
{units['m/s']}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('pitch')}>
|
||||
{units['°/s']}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('yaw')}>
|
||||
{units['°/s']}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('roll')}>
|
||||
{units['°/s']}
|
||||
</th>
|
||||
<th className="sortable lft" onClick={sortShips('jumpRangeLaden')}>
|
||||
{units.LY}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('totalRange')}>
|
||||
{units.LY}
|
||||
</th>
|
||||
<th className="lft"> </th>
|
||||
{/* armour placeholder */}
|
||||
<th> </th>
|
||||
<th
|
||||
className="sortable"
|
||||
@@ -497,73 +481,37 @@ export default class ShipyardPage extends Page {
|
||||
>
|
||||
{units.MJ}
|
||||
</th>
|
||||
<th className="sortable lft" onClick={sortShips('topSpeed')}>
|
||||
{units['m/s']}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('topBoost')}>
|
||||
{units['m/s']}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('maxJumpRange')}>
|
||||
{units.LY}
|
||||
</th>
|
||||
<th className="sortable" onClick={sortShips('maxCargo')}>
|
||||
<th className="sortable lft" onClick={sortShips('maxCargo')}>
|
||||
{units.T}
|
||||
</th>
|
||||
<th> </th>
|
||||
<th
|
||||
className="sortable lft"
|
||||
onMouseEnter={termtip.bind(null, 'power plant')}
|
||||
onMouseLeave={hide}
|
||||
onClick={sortShips('standard', 0)}
|
||||
>
|
||||
{/* pax placeholder */}
|
||||
<th className="lft"> </th>
|
||||
<th className="sortable lft" onMouseEnter={termtip.bind(null, 'power plant')}
|
||||
onMouseLeave={hide} onClick={sortShips('standard', 0)}>
|
||||
{'pp'}
|
||||
</th>
|
||||
<th
|
||||
className="sortable"
|
||||
onMouseEnter={termtip.bind(null, 'thrusters')}
|
||||
onMouseLeave={hide}
|
||||
onClick={sortShips('standard', 1)}
|
||||
>
|
||||
<th className="sortable" onMouseEnter={termtip.bind(null, 'thrusters')}
|
||||
onMouseLeave={hide} onClick={sortShips('standard', 1)}>
|
||||
{'th'}
|
||||
</th>
|
||||
<th
|
||||
className="sortable"
|
||||
onMouseEnter={termtip.bind(null, 'frame shift drive')}
|
||||
onMouseLeave={hide}
|
||||
onClick={sortShips('standard', 2)}
|
||||
>
|
||||
<th className="sortable" onMouseEnter={termtip.bind(null, 'frame shift drive')}
|
||||
onMouseLeave={hide} onClick={sortShips('standard', 2)}>
|
||||
{'fsd'}
|
||||
</th>
|
||||
<th
|
||||
className="sortable"
|
||||
onMouseEnter={termtip.bind(null, 'life support')}
|
||||
onMouseLeave={hide}
|
||||
onClick={sortShips('standard', 3)}
|
||||
>
|
||||
<th className="sortable" onMouseEnter={termtip.bind(null, 'life support')}
|
||||
onMouseLeave={hide} onClick={sortShips('standard', 3)}>
|
||||
{'ls'}
|
||||
</th>
|
||||
<th
|
||||
className="sortable"
|
||||
onMouseEnter={termtip.bind(null, 'power distributor')}
|
||||
onMouseLeave={hide}
|
||||
onClick={sortShips('standard', 4)}
|
||||
>
|
||||
<th className="sortable" onMouseEnter={termtip.bind(null, 'power distriubtor')}
|
||||
onMouseLeave={hide} onClick={sortShips('standard', 4)}>
|
||||
{'pd'}
|
||||
</th>
|
||||
<th
|
||||
className="sortable"
|
||||
onMouseEnter={termtip.bind(null, 'sensors')}
|
||||
onMouseLeave={hide}
|
||||
onClick={sortShips('standard', 5)}
|
||||
>
|
||||
<th className="sortable" onMouseEnter={termtip.bind(null, 'sensors')}
|
||||
onMouseLeave={hide} onClick={sortShips('standard', 5)}>
|
||||
{'s'}
|
||||
</th>
|
||||
<th
|
||||
className="sortable"
|
||||
onMouseEnter={termtip.bind(null, 'fuel tank')}
|
||||
onMouseLeave={hide}
|
||||
onClick={sortShips('standard', 6)}
|
||||
>
|
||||
<th className="sortable" onMouseEnter={termtip.bind(null, 'fuel tank')}
|
||||
onMouseLeave={hide} onClick={sortShips('standard', 6)}>
|
||||
{'ft'}
|
||||
</th>
|
||||
<th className="sortable lft" onClick={sortShips('hp', 1)}>
|
||||
@@ -615,7 +563,7 @@ export default class ShipyardPage extends Page {
|
||||
</div>
|
||||
</div>
|
||||
<div className="table-tools" >
|
||||
<label><input type="checkbox" checked={this.state.groupCompared} onClick={() => this._toggleGroupCompared()}/>{translate('Group highlighted ships')}</label>
|
||||
<label><input type="checkbox" checked={this.state.groupCompared} onClick={() => this._toggleGroupCompared()}/>Group highlighted ships</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Modification - a modification and its value
|
||||
*/
|
||||
export default class Modification {
|
||||
/**
|
||||
* @param {String} id Unique modification ID
|
||||
* @param {Number} value Value of the modification
|
||||
*/
|
||||
constructor(id, value) {
|
||||
this.id = id;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,197 +0,0 @@
|
||||
import Module from './Module';
|
||||
import { BulkheadNames } from './Constants';
|
||||
|
||||
/**
|
||||
* Filter eligble modules based on parameters
|
||||
* @param {Array} arr Available modules array
|
||||
* @param {number} maxClass Max class
|
||||
* @param {number} minClass Minimum class
|
||||
* @param {number} mass Mass
|
||||
* @return {Array} Fitlered module subset
|
||||
*/
|
||||
function filter(arr, maxClass, minClass, mass) {
|
||||
return arr.filter(m => m.class <= maxClass && m.class >= minClass && (m.maxmass === undefined || mass <= m.maxmass));
|
||||
}
|
||||
|
||||
/**
|
||||
* The available module set for a specific ship
|
||||
*/
|
||||
export default class ModuleSet {
|
||||
/**
|
||||
* Instantiate the module set
|
||||
* @param {Object} modules All Modules
|
||||
* @param {Object} shipData Ship Specifications Data (see coriolis-data/Ships)
|
||||
*/
|
||||
constructor(modules, shipData) {
|
||||
let maxInternal = isNaN(shipData.slots.internal[0]) ? shipData.slots.internal[0].class : shipData.slots.internal[0];
|
||||
let mass = shipData.properties.hullMass + 6.5;
|
||||
let maxStandardArr = shipData.slots.standard;
|
||||
let maxHardPoint = shipData.slots.hardpoints[0];
|
||||
let stnd = modules.standard;
|
||||
this.mass = mass;
|
||||
this.standard = {};
|
||||
this.internal = {};
|
||||
this.hardpoints = {};
|
||||
this.hpClass = {};
|
||||
this.intClass = {};
|
||||
|
||||
this.bulkheads = shipData.bulkheads.map((b, i) => {
|
||||
return Object.assign(new Module(), { grp: 'bh', id: i, name: BulkheadNames[i], index: i, class: '', rating: '' }, b);
|
||||
});
|
||||
|
||||
this.standard[0] = filter(stnd.pp, maxStandardArr[0], 0, mass); // Power Plant
|
||||
this.standard[2] = filter(stnd.fsd, maxStandardArr[2], 0, mass); // FSD
|
||||
this.standard[4] = filter(stnd.pd, maxStandardArr[4], 0, mass); // Power Distributor
|
||||
this.standard[6] = filter(stnd.ft, maxStandardArr[6], 0, mass); // Fuel Tank
|
||||
// Thrusters, filter modules by class only (to show full list of ratings for that class)
|
||||
let minThrusterClass = stnd.t.reduce((clazz, th) => (th.maxmass >= mass && th.class < clazz) ? th.class : clazz, maxStandardArr[1]);
|
||||
this.standard[1] = filter(stnd.t, maxStandardArr[1], minThrusterClass, 0); // Thrusters
|
||||
// Slots where module class must be equal to slot class
|
||||
this.standard[3] = filter(stnd.ls, maxStandardArr[3], maxStandardArr[3], 0); // Life Supprt
|
||||
this.standard[5] = filter(stnd.s, maxStandardArr[5], maxStandardArr[5], mass); // Sensors
|
||||
|
||||
for (let h in modules.hardpoints) {
|
||||
this.hardpoints[h] = filter(modules.hardpoints[h], maxHardPoint, 0, mass);
|
||||
}
|
||||
|
||||
for (let g in modules.internal) {
|
||||
this.internal[g] = filter(modules.internal[g], maxInternal, 0, mass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the specified bulkhead
|
||||
* @param {integer} index Bulkhead index
|
||||
* @return {Object} Bulkhead module details
|
||||
*/
|
||||
getBulkhead(index) {
|
||||
return this.bulkheads[index] ? new Module({ template: this.bulkheads[index] }) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the modules that areeligible for an internal slot
|
||||
* @param {Object} ship The ship
|
||||
* @param {integer} c The max class module that can be mounted in the slot
|
||||
* @param {Object} eligible) The map of eligible internal groups
|
||||
* @return {object} A map of all eligible modules by group
|
||||
*/
|
||||
getInts(ship, c, eligible) {
|
||||
let o = {};
|
||||
for (let key in this.internal) {
|
||||
if (eligible && !eligible[key]) {
|
||||
continue;
|
||||
}
|
||||
if (key == 'pcq' && !(ship.luxuryCabins && ship.luxuryCabins === true)) {
|
||||
continue;
|
||||
}
|
||||
if (key == 'fh' && !(ship.fighterHangars && ship.fighterHangars === true)) {
|
||||
continue;
|
||||
}
|
||||
let data = filter(this.internal[key], c, 0, this.mass);
|
||||
if (data.length) { // If group is not empty
|
||||
o[key] = data;
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determining the modules that are eligible for an hardpoint slot
|
||||
* @param {integer} c The max class module that can be mounted in the slot
|
||||
* @param {Object} eligible) The map of eligible hardpoint groups
|
||||
* @return {object} A map of all eligible modules by group
|
||||
*/
|
||||
getHps(c, eligible) {
|
||||
let o = {};
|
||||
for (let key in this.hardpoints) {
|
||||
if (eligible && !eligible[key]) {
|
||||
continue;
|
||||
}
|
||||
let data = filter(this.hardpoints[key], c, c ? 1 : 0, this.mass);
|
||||
if (data.length) { // If group is not empty
|
||||
o[key] = data;
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the lightest Power Distributor that provides sufficient
|
||||
* energy to boost.
|
||||
* @param {number} boostEnergy The energy that is required to boost
|
||||
* @return {Object} Power Distributor
|
||||
*/
|
||||
lightestPowerDist(boostEnergy) {
|
||||
let pd = this.standard[4][0];
|
||||
for (let p of this.standard[4]) {
|
||||
if (p.mass < pd.mass && p.engcap > boostEnergy) {
|
||||
pd = p;
|
||||
}
|
||||
}
|
||||
return new Module({ template: pd });
|
||||
};
|
||||
|
||||
/** Find the power distributor that matches the requirements
|
||||
* @param {Object} requirements The requirements to be met (currently only support 'weprate')
|
||||
* @return {Object} Power distributor
|
||||
*/
|
||||
matchingPowerDist(requirements) {
|
||||
let pd = this.standard[4][0];
|
||||
for (let p of this.standard[4]) {
|
||||
if (p.weprate >= requirements.weprate || p.weprate >= pd.weprate) {
|
||||
pd = p;
|
||||
}
|
||||
}
|
||||
return new Module({ template: pd });
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the lightest Thruster that can handle the specified tonnage
|
||||
* @param {number} ladenMass Ship laden mass (mass + cargo + fuel)
|
||||
* @return {Object} Thruster
|
||||
*/
|
||||
lightestThruster(ladenMass) {
|
||||
let th = this.standard[1][0];
|
||||
|
||||
for (let t of this.standard[1]) {
|
||||
if (t.mass < th.mass && t.maxmass >= ladenMass) {
|
||||
th = t;
|
||||
}
|
||||
}
|
||||
return new Module({ template: th });
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the lightest usable Shield Generator
|
||||
* @param {number} hullMass Ship hull mass
|
||||
* @return {Object} Thruster
|
||||
*/
|
||||
lightestShieldGenerator(hullMass) {
|
||||
let sg = this.internal.sg[0];
|
||||
|
||||
for (let s of this.internal.sg) {
|
||||
if (s.mass < sg.mass && s.maxmass > hullMass) {
|
||||
sg = s;
|
||||
}
|
||||
}
|
||||
return new Module({ template: sg });
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the lightest Power Plant that provides sufficient power
|
||||
* @param {number} powerNeeded Power requirements in MJ
|
||||
* @param {string} rating The optional rating of the power plant
|
||||
* @return {Object} Power Plant
|
||||
*/
|
||||
lightestPowerPlant(powerNeeded, rating) {
|
||||
let pp = this.standard[0][0];
|
||||
|
||||
for (let p of this.standard[0]) {
|
||||
// Provides enough power, is lighter or the same mass as current power plant but better output/efficiency
|
||||
if (p.pgen >= powerNeeded && (p.mass < pp.mass || (p.mass == pp.mass && p.pgen > pp.pgen)) && (!rating || rating == p.rating)) {
|
||||
pp = p;
|
||||
}
|
||||
}
|
||||
return new Module({ template: pp });
|
||||
}
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
import { ModuleNameToGroup, BulkheadNames, StandardArray } from './Constants';
|
||||
import ModuleSet from './ModuleSet';
|
||||
import Module from './Module';
|
||||
import { Ships, Modules } from 'coriolis-data/dist';
|
||||
|
||||
/*
|
||||
* All functions below must return a fresh Module rather than a definition or existing module, as
|
||||
* the resultant object can be altered with modifications.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Created a cargo hatch model
|
||||
* @return {Object} Cargo hatch model
|
||||
*/
|
||||
export function cargoHatch() {
|
||||
let hatch = new Module();
|
||||
Object.assign(hatch, { name: 'Cargo Hatch', class: 1, rating: 'H', power: 0.6 });
|
||||
return hatch;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the module with the specific group and ID
|
||||
* @param {String} grp Module group (pp - power plant, pl - pulse laser etc)
|
||||
* @param {String} id The module ID
|
||||
* @return {Object} The module or null
|
||||
*/
|
||||
export function findModule(grp, id) {
|
||||
// See if it's a standard module
|
||||
if (Modules.standard[grp]) {
|
||||
let standardmod = Modules.standard[grp].find(e => e.id == id);
|
||||
if (standardmod != null) {
|
||||
return new Module({ template: standardmod });
|
||||
}
|
||||
}
|
||||
|
||||
// See if it's an internal module
|
||||
if (Modules.internal[grp]) {
|
||||
let internalmod = Modules.internal[grp].find(e => e.id == id);
|
||||
if (internalmod != null) {
|
||||
return new Module({ template: internalmod });
|
||||
}
|
||||
}
|
||||
|
||||
// See if it's a hardpoint module
|
||||
if (Modules.hardpoints[grp]) {
|
||||
let hardpointmod = Modules.hardpoints[grp].find(e => e.id == id);
|
||||
if (hardpointmod != null) {
|
||||
return new Module({ template: hardpointmod });
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the standard module type with the specified ID
|
||||
* @param {String|Number} type Standard Module Type (0/pp - Power Plant, 1/t - Thrusters, etc)
|
||||
* @param {String} id The module ID or '[Class][Rating]'
|
||||
* @return {Object} The standard module or null
|
||||
*/
|
||||
export function standard(type, id) {
|
||||
if (!isNaN(type)) {
|
||||
type = StandardArray[type];
|
||||
}
|
||||
let s = Modules.standard[type].find(e => e.id === id);
|
||||
if (!s) {
|
||||
s = Modules.standard[type].find(e => (e.class == id.charAt(0) && e.rating == id.charAt(1)));
|
||||
}
|
||||
if (s) {
|
||||
s = new Module({ template: s });
|
||||
}
|
||||
return s || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the hardpoint with the specified ID
|
||||
* @param {String} id Hardpoint ID
|
||||
* @return {Object} Hardpoint module or null
|
||||
*/
|
||||
export function hardpoints(id) {
|
||||
for (let n in Modules.hardpoints) {
|
||||
let group = Modules.hardpoints[n];
|
||||
for (let i = 0; i < group.length; i++) {
|
||||
if (group[i].id == id) {
|
||||
return new Module({ template: group[i] });
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the internal module with the specified ID
|
||||
* @param {String} id Internal module ID
|
||||
* @return {Object} Internal module or null
|
||||
*/
|
||||
export function internal(id) {
|
||||
for (let n in Modules.internal) {
|
||||
let group = Modules.internal[n];
|
||||
for (let i = 0; i < group.length; i++) {
|
||||
if (group[i].id == id) {
|
||||
return new Module({ template: group[i] });
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a standard module based on Class, Rating, Group and/or name.
|
||||
* At least one of Group name or unique module name must be provided
|
||||
*
|
||||
* @param {String} groupName [Optional] Full name or abbreviated name for module group
|
||||
* @param {integer} clss module Class
|
||||
* @param {String} rating module Rating
|
||||
* @param {String} name [Optional] Long/unique name for module -e.g. 'Advanced Discover Scanner'
|
||||
* @return {Object} The module if found, null if not found
|
||||
*/
|
||||
export function findStandard(groupName, clss, rating, name) {
|
||||
let groups = {};
|
||||
|
||||
if (groupName) {
|
||||
if (Modules.standard[groupName]) {
|
||||
groups[groupName] = Modules.standard[groupName];
|
||||
} else {
|
||||
let grpCode = ModuleNameToGroup[groupName.toLowerCase()];
|
||||
if (grpCode && Modules.standard[grpCode]) {
|
||||
groups[grpCode] = Modules.standard[grpCode];
|
||||
}
|
||||
}
|
||||
} else if (name) {
|
||||
groups = Modules.standard;
|
||||
}
|
||||
|
||||
for (let g in groups) {
|
||||
let group = groups[g];
|
||||
for (let i = 0, l = group.length; i < l; i++) {
|
||||
if (group[i].class == clss && group[i].rating == rating && ((!name && !group[i].name) || group[i].name == name)) {
|
||||
return group[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a standard Module ID based on Class, Rating, Group and/or name.
|
||||
* At least one of Group name or unique module name must be provided
|
||||
*
|
||||
* @param {String} groupName [Optional] Full name or abbreviated name for module group
|
||||
* @param {integer} clss module Class
|
||||
* @param {String} rating Module Rating
|
||||
* @param {String} name [Optional] Long/unique name for module -e.g. 'Advanced Discover Scanner'
|
||||
* @return {String} The id of the module if found, null if not found
|
||||
*/
|
||||
export function findStandardId(groupName, clss, rating, name) {
|
||||
let i = this.findStandard(groupName, clss, rating, name);
|
||||
return i ? i.id : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an internal module based on Class, Rating, Group and/or name.
|
||||
* At least one ofGroup name or unique module name must be provided
|
||||
*
|
||||
* @param {String} groupName [Optional] Full name or abbreviated name for module group
|
||||
* @param {integer} clss module Class
|
||||
* @param {String} rating module Rating
|
||||
* @param {String} name [Optional] Long/unique name for module -e.g. 'Advanced Discover Scanner'
|
||||
* @return {Object} The module if found, null if not found
|
||||
*/
|
||||
export function findInternal(groupName, clss, rating, name) {
|
||||
let groups = {};
|
||||
|
||||
if (groupName) {
|
||||
if (Modules.internal[groupName]) {
|
||||
groups[groupName] = Modules.internal[groupName];
|
||||
} else {
|
||||
let grpCode = ModuleNameToGroup[groupName.toLowerCase()];
|
||||
if (grpCode && Modules.internal[grpCode]) {
|
||||
groups[grpCode] = Modules.internal[grpCode];
|
||||
}
|
||||
}
|
||||
} else if (name) {
|
||||
groups = Modules.internal;
|
||||
}
|
||||
|
||||
for (let g in groups) {
|
||||
let group = groups[g];
|
||||
for (let i = 0, l = group.length; i < l; i++) {
|
||||
if (group[i].class == clss && group[i].rating == rating && ((!name && !group[i].name) || group[i].name == name)) {
|
||||
return group[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an internal module based on Class, Rating, Group and/or name.
|
||||
* At least one of Group name or unique module name must be provided.
|
||||
* will start searching at specified class and proceed lower until a
|
||||
* module is found or 0 is hit
|
||||
* Uses findInternal internally
|
||||
*
|
||||
* @param {String} groupName [Optional] Full name or abbreviated name for module group
|
||||
* @param {integer} clss module Class
|
||||
* @param {String} rating module Rating
|
||||
* @param {String} name [Optional] Long/unique name for module -e.g. 'Advanced Discover Scanner'
|
||||
* @return {Object} The module if found, null if not found
|
||||
*/
|
||||
export function findMaxInternal(groupName, clss, rating, name) {
|
||||
let foundModule = null;
|
||||
let currentClss = clss;
|
||||
while (currentClss > 0 && foundModule == null) {
|
||||
foundModule = findInternal(groupName, currentClss, rating, name);
|
||||
currentClss = currentClss - 1;
|
||||
}
|
||||
return foundModule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an internal Module ID based on Class, Rating, Group and/or name.
|
||||
* At least one ofGroup name or unique module name must be provided
|
||||
*
|
||||
* @param {String} groupName [Optional] Full name or abbreviated name for module group
|
||||
* @param {integer} clss module Class
|
||||
* @param {String} rating Module Rating
|
||||
* @param {String} name [Optional] Long/unique name for module -e.g. 'Advanced Discover Scanner'
|
||||
* @return {String} The id of the module if found, null if not found
|
||||
*/
|
||||
export function findInternalId(groupName, clss, rating, name) {
|
||||
let i = this.findInternal(groupName, clss, rating, name);
|
||||
return i ? i.id : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a hardpoint Module based on Class, Rating, Group and/or name.
|
||||
* At least one ofGroup name or unique module name must be provided
|
||||
*
|
||||
* @param {String} groupName [Optional] Full name or abbreviated name for module group
|
||||
* @param {integer} clss Module Class
|
||||
* @param {String} rating [Optional] module Rating
|
||||
* @param {String} name [Optional] Long/unique name for module -e.g. 'Heat Sink Launcher'
|
||||
* @param {String} mount Mount type - [F]ixed, [G]imballed, [T]urret
|
||||
* @param {String} missile [Optional] Missile type - [D]umbfire, [S]eeker
|
||||
* @return {String} The id of the module if found, null if not found
|
||||
*/
|
||||
export function findHardpoint(groupName, clss, rating, name, mount, missile) {
|
||||
let groups = {};
|
||||
|
||||
if (groupName) {
|
||||
if (Modules.hardpoints[groupName]) {
|
||||
groups[groupName] = Modules.hardpoints[groupName];
|
||||
} else {
|
||||
let grpCode = ModuleNameToGroup[groupName.toLowerCase()];
|
||||
if (grpCode && Modules.hardpoints[grpCode]) {
|
||||
groups[grpCode] = Modules.hardpoints[grpCode];
|
||||
}
|
||||
}
|
||||
} else if (name) {
|
||||
groups = Modules.hardpoints;
|
||||
}
|
||||
|
||||
for (let g in groups) {
|
||||
let group = groups[g];
|
||||
for (let h of group) {
|
||||
if (h.class == clss && (!rating || h.rating == rating) && h.mount == mount && h.name == name && h.missile == missile) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a hardpoint module ID based on Class, Rating, Group and/or name.
|
||||
* At least one of Group name or unique module name must be provided
|
||||
*
|
||||
* @param {String} groupName [Optional] Full name or abbreviated name for module group
|
||||
* @param {integer} clss module Class
|
||||
* @param {String} rating module Rating
|
||||
* @param {String} name [Optional] Long/unique name for module -e.g. 'Heat Sink Launcher'
|
||||
* @param {String} mount Mount type - [F]ixed, [G]imballed, [T]urret
|
||||
* @param {String} missile [Optional] Missile type - [D]umbfire, [S]eeker
|
||||
* @return {String} The id of the module if found, null if not found
|
||||
*/
|
||||
export function findHardpointId(groupName, clss, rating, name, mount, missile) {
|
||||
let h = this.findHardpoint(groupName, clss, rating, name, mount, missile);
|
||||
if (h) {
|
||||
return h.id;
|
||||
}
|
||||
|
||||
// Countermeasures used to be lumped in a single group but have been broken, out. If we have been given a groupName of 'Countermeasure' then
|
||||
// rely on the unique name to find it
|
||||
if (groupName === 'cm' || groupName === 'Countermeasure') {
|
||||
h = this.findHardpoint(null, clss, rating, name, mount, missile);
|
||||
}
|
||||
|
||||
return h ? h.id : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bulkhead index for the given bulkhead name
|
||||
* @param {String} bulkheadName Bulkhead name in english
|
||||
* @return {number} Bulkhead index
|
||||
*/
|
||||
export function bulkheadIndex(bulkheadName) {
|
||||
return BulkheadNames.indexOf(bulkheadName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if a module group is a shield generator
|
||||
* @param {String} g Module Group name
|
||||
* @return {Boolean} True if the group is a shield generator
|
||||
*/
|
||||
export function isShieldGenerator(g) {
|
||||
return g == 'sg' || g == 'psg' || g == 'bsg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ModuleSet that contains all available modules
|
||||
* that the specified ship is eligible to use.
|
||||
*
|
||||
* 6.5 T is the lightest possible mass of standard components that any ship can use
|
||||
*
|
||||
* @param {String} shipId Unique ship Id/Key
|
||||
* @return {ModuleSet} The set of modules the ship can install
|
||||
*/
|
||||
export function forShip(shipId) {
|
||||
return new ModuleSet(Modules, Ships[shipId]);
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
import { ModuleGroupToName, MountMap, BulkheadNames } from './Constants';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import Ship from './Ship';
|
||||
import * as Utils from '../utils/UtilityFunctions';
|
||||
import LZString from 'lz-string';
|
||||
import { outfitURL } from '../utils/UrlGenerators';
|
||||
|
||||
/**
|
||||
* Generates ship-loadout JSON Schema standard object
|
||||
* @param {Object} standard model
|
||||
* @return {Object} JSON Schema
|
||||
*/
|
||||
function standardToSchema(standard) {
|
||||
if (standard.m) {
|
||||
let o = {
|
||||
class: standard.m.class,
|
||||
rating: standard.m.rating,
|
||||
enabled: Boolean(standard.enabled),
|
||||
priority: standard.priority + 1
|
||||
};
|
||||
|
||||
if (standard.m.name) {
|
||||
o.name = standard.m.name;
|
||||
}
|
||||
|
||||
if (standard.m.mods && Object.keys(standard.m.mods).length > 0) {
|
||||
o.modifications = standard.m.mods;
|
||||
}
|
||||
|
||||
if (standard.m.blueprint && Object.keys(standard.m.blueprint).length > 0) {
|
||||
o.blueprint = standard.m.blueprint;
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates ship-loadout JSON Schema slot object
|
||||
* @param {Object} slot Slot model
|
||||
* @return {Object} JSON Schema Slot
|
||||
*/
|
||||
function slotToSchema(slot) {
|
||||
if (slot.m) {
|
||||
let o = {
|
||||
class: slot.m.class,
|
||||
rating: slot.m.rating,
|
||||
enabled: Boolean(slot.enabled),
|
||||
priority: slot.priority + 1,
|
||||
group: ModuleGroupToName[slot.m.grp]
|
||||
};
|
||||
|
||||
if (slot.m.name) {
|
||||
o.name = slot.m.name;
|
||||
}
|
||||
if (slot.m.mount) {
|
||||
o.mount = MountMap[slot.m.mount];
|
||||
}
|
||||
if (slot.m.missile) {
|
||||
o.missile = slot.m.missile;
|
||||
}
|
||||
if (slot.m.mods && Object.keys(slot.m.mods).length > 0) {
|
||||
o.modifications = slot.m.mods;
|
||||
}
|
||||
if (slot.m.blueprint && Object.keys(slot.m.blueprint).length > 0) {
|
||||
o.blueprint = slot.m.blueprint;
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an object conforming to the ship-loadout JSON schema from a Ship model
|
||||
* @param {string} buildName The build name
|
||||
* @param {Ship} ship Ship instance
|
||||
* @return {Object} ship-loadout object
|
||||
*/
|
||||
export function toDetailedBuild(buildName, ship) {
|
||||
let standard = ship.standard,
|
||||
hardpoints = ship.hardpoints,
|
||||
internal = ship.internal,
|
||||
code = ship.toString();
|
||||
|
||||
let data = {
|
||||
$schema: 'https://coriolis.io/schemas/ship-loadout/4.json#',
|
||||
name: buildName,
|
||||
ship: ship.name,
|
||||
references: [{
|
||||
name: 'Coriolis.io',
|
||||
url: 'https://coriolis.io' + outfitURL(ship.id, code, buildName),
|
||||
code,
|
||||
shipId: ship.id
|
||||
}],
|
||||
components: {
|
||||
standard: {
|
||||
bulkheads: BulkheadNames[ship.bulkheads.m.index],
|
||||
cargoHatch: { enabled: Boolean(ship.cargoHatch.enabled), priority: ship.cargoHatch.priority + 1 },
|
||||
powerPlant: standardToSchema(standard[0]),
|
||||
thrusters: standardToSchema(standard[1]),
|
||||
frameShiftDrive: standardToSchema(standard[2]),
|
||||
lifeSupport: standardToSchema(standard[3]),
|
||||
powerDistributor: standardToSchema(standard[4]),
|
||||
sensors: standardToSchema(standard[5]),
|
||||
fuelTank: standardToSchema(standard[6])
|
||||
},
|
||||
hardpoints: hardpoints.filter(slot => slot.maxClass > 0).map(slotToSchema),
|
||||
utility: hardpoints.filter(slot => slot.maxClass === 0).map(slotToSchema),
|
||||
internal: internal.map(slotToSchema)
|
||||
},
|
||||
stats: {}
|
||||
};
|
||||
|
||||
for (let stat in ship) {
|
||||
if (!isNaN(ship[stat])) {
|
||||
data.stats[stat] = Math.round(ship[stat] * 100) / 100;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiates a ship from a ship-loadout object
|
||||
* @param {Object} detailedBuild ship-loadout object
|
||||
* @return {Ship} Ship instance
|
||||
*/
|
||||
export function fromDetailedBuild(detailedBuild) {
|
||||
let shipId = Object.keys(Ships).find((shipId) => Ships[shipId].properties.name.toLowerCase() == detailedBuild.ship.toLowerCase());
|
||||
if (!shipId) {
|
||||
throw 'No such ship: ' + detailedBuild.ship;
|
||||
}
|
||||
|
||||
let shipData = Ships[shipId];
|
||||
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||
|
||||
if (!detailedBuild.references[0] || !detailedBuild.references[0].code) {
|
||||
throw 'Missing reference code';
|
||||
}
|
||||
|
||||
ship.buildFrom(detailedBuild.references[0].code);
|
||||
|
||||
return ship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of ship-loadout JSON Schema object for export
|
||||
* @param {Array} builds Array of ship builds
|
||||
* @return {Array} Array of of ship-loadout objects
|
||||
*/
|
||||
export function toDetailedExport(builds) {
|
||||
let data = [];
|
||||
|
||||
for (let shipId in builds) {
|
||||
for (let buildName in builds[shipId]) {
|
||||
let code = builds[shipId][buildName];
|
||||
let shipData = Ships[shipId];
|
||||
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||
ship.buildFrom(code);
|
||||
data.push(toDetailedBuild(buildName, ship, code));
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializes a comparion and all of the ships to zipped
|
||||
* Base 64 encoded JSON.
|
||||
* @param {string} name Comparison name
|
||||
* @param {array} builds Array of ship builds
|
||||
* @param {array} facets Selected facets
|
||||
* @param {string} predicate sort predicate
|
||||
* @param {boolean} desc sort order
|
||||
* @return {string} Zipped Base 64 encoded JSON
|
||||
*/
|
||||
export function fromComparison(name, builds, facets, predicate, desc) {
|
||||
return LZString.compressToBase64(JSON.stringify({
|
||||
n: name,
|
||||
b: builds.map((b) => { return { s: b.id, n: b.buildName, c: b.toString() }; }),
|
||||
f: facets,
|
||||
p: predicate,
|
||||
d: desc ? 1 : 0
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the comarison data string back to an object.
|
||||
* @param {string} code Zipped Base 64 encoded JSON comparison data
|
||||
* @return {Object} Comparison data object
|
||||
*/
|
||||
export function toComparison(code) {
|
||||
return JSON.parse(LZString.decompressFromBase64(Utils.fromUrlSafe(code)));
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,415 +0,0 @@
|
||||
import * as ModuleUtils from './ModuleUtils';
|
||||
import { canMount } from '../utils/SlotFunctions';
|
||||
|
||||
/**
|
||||
* Standard / typical role for multi-purpose or combat (if shielded with better bulkheads)
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {Boolean} shielded True if shield generator should be included
|
||||
* @param {integer} bulkheadIndex Bulkhead to use see Constants.BulkheadNames
|
||||
*/
|
||||
export function multiPurpose(ship, shielded, bulkheadIndex) {
|
||||
ship.useStandard('A')
|
||||
.use(ship.standard[3], ModuleUtils.standard(3, ship.standard[3].maxClass + 'D')) // D Life Support
|
||||
.use(ship.standard[5], ModuleUtils.standard(5, ship.standard[5].maxClass + 'D')) // D Sensors
|
||||
.useBulkhead(bulkheadIndex);
|
||||
|
||||
if (shielded) {
|
||||
ship.internal.some(function(slot) {
|
||||
if (canMount(ship, slot, 'sg')) { // Assuming largest slot can hold an eligible shield
|
||||
ship.use(slot, ModuleUtils.findInternal('sg', slot.maxClass, 'A'));
|
||||
ship.setSlotEnabled(slot, true);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trader Role
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {Boolean} shielded True if shield generator should be included
|
||||
* @param {Object} standardOpts [Optional] Standard module optional overrides
|
||||
*/
|
||||
export function trader(ship, shielded, standardOpts) {
|
||||
let usedSlots = [];
|
||||
let bstCount = 2;
|
||||
let sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass);
|
||||
ship.useStandard('A')
|
||||
.use(ship.standard[3], ModuleUtils.standard(3, ship.standard[3].maxClass + 'D')) // D Life Support
|
||||
.use(ship.standard[1], ModuleUtils.standard(1, ship.standard[1].maxClass + 'D')) // D Life Support
|
||||
.use(ship.standard[4], ModuleUtils.standard(4, ship.standard[4].maxClass + 'D')) // D Life Support
|
||||
.use(ship.standard[5], ModuleUtils.standard(5, ship.standard[5].maxClass + 'D')); // D Sensors
|
||||
|
||||
const shieldOrder = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const shieldInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
|
||||
.filter(a => (!a.eligible) || a.eligible.sg)
|
||||
.filter(a => a.maxClass >= sg.class)
|
||||
.sort((a, b) => shieldOrder.indexOf(a.maxClass) - shieldOrder.indexOf(b.maxClass));
|
||||
shieldInternals.some(function(slot) {
|
||||
if (canMount(ship, slot, 'sg')) { // Assuming largest slot can hold an eligible shield
|
||||
const shield = ModuleUtils.findInternal('sg', slot.maxClass, 'A');
|
||||
if (shield && shield.maxmass > ship.hullMass) {
|
||||
ship.use(slot, shield);
|
||||
ship.setSlotEnabled(slot, true);
|
||||
usedSlots.push(slot);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fill the empty internals with cargo racks
|
||||
for (let i = ship.internal.length; i--;) {
|
||||
let slot = ship.internal[i];
|
||||
if (usedSlots.indexOf(slot) == -1 && canMount(ship, slot, 'cr')) {
|
||||
ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E'));
|
||||
}
|
||||
}
|
||||
|
||||
// Empty the hardpoints
|
||||
for (let s of ship.hardpoints) {
|
||||
ship.use(s, null);
|
||||
}
|
||||
for (let s of ship.hardpoints) {
|
||||
if (s.maxClass == 0 && bstCount) { // Mount up to 2 boosters
|
||||
ship.use(s, ModuleUtils.hardpoints('04'));
|
||||
bstCount--;
|
||||
} else {
|
||||
ship.use(s, null);
|
||||
}
|
||||
}
|
||||
// ship.useLightestStandard(standardOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Explorer Role
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {Boolean} planetary True if Planetary Vehicle Hangar (PVH) should be included
|
||||
*/
|
||||
export function explorer(ship, planetary) {
|
||||
let standardOpts = { ppRating: 'A' },
|
||||
heatSinkCount = 2, // Fit 2 heat sinks if possible
|
||||
usedSlots = [],
|
||||
sgSlot,
|
||||
fuelScoopSlot,
|
||||
sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass);
|
||||
|
||||
if (!planetary) { // Non-planetary explorers don't really need to boost
|
||||
standardOpts.pd = '1D';
|
||||
}
|
||||
|
||||
// Cargo hatch can be disabled
|
||||
ship.setSlotEnabled(ship.cargoHatch, false);
|
||||
|
||||
// Advanced Discovery Scanner - class 1 or higher
|
||||
const adsOrder = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const adsInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
|
||||
.filter(a => (!a.eligible) || a.eligible.sc)
|
||||
.sort((a, b) => adsOrder.indexOf(a.maxClass) - adsOrder.indexOf(b.maxClass));
|
||||
for (let i = 0; i < adsInternals.length; i++) {
|
||||
if (canMount(ship, adsInternals[i], 'sc')) {
|
||||
ship.use(adsInternals[i], ModuleUtils.internal('2f'));
|
||||
usedSlots.push(adsInternals[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (planetary) {
|
||||
// Planetary Vehicle Hangar - class 2 or higher
|
||||
const pvhOrder = [2, 3, 4, 5, 6, 7, 8, 1];
|
||||
const pvhInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
|
||||
.filter(a => (!a.eligible) || a.eligible.pv)
|
||||
.sort((a, b) => pvhOrder.indexOf(a.maxClass) - pvhOrder.indexOf(b.maxClass));
|
||||
for (let i = 0; i < pvhInternals.length; i++) {
|
||||
if (canMount(ship, pvhInternals[i], 'pv')) {
|
||||
// Planetary Vehical Hangar only has even classes
|
||||
const pvhClass = pvhInternals[i].maxClass % 2 === 1 ? pvhInternals[i].maxClass - 1 : pvhInternals[i].maxClass;
|
||||
ship.use(pvhInternals[i], ModuleUtils.findInternal('pv', pvhClass, 'G')); // G is lower mass
|
||||
ship.setSlotEnabled(pvhInternals[i], false); // Disable power for Planetary Vehical Hangar
|
||||
usedSlots.push(pvhInternals[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shield generator
|
||||
const shieldOrder = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const shieldInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
|
||||
.filter(a => (!a.eligible) || a.eligible.sg)
|
||||
.filter(a => a.maxClass >= sg.class)
|
||||
.sort((a, b) => shieldOrder.indexOf(a.maxClass) - shieldOrder.indexOf(b.maxClass));
|
||||
for (let i = 0; i < shieldInternals.length; i++) {
|
||||
if (canMount(ship, shieldInternals[i], 'sg')) {
|
||||
ship.use(shieldInternals[i], sg);
|
||||
usedSlots.push(shieldInternals[i]);
|
||||
sgSlot = shieldInternals[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Detailed Surface Scanner
|
||||
const dssOrder = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const dssInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
|
||||
.filter(a => (!a.eligible) || a.eligible.sc)
|
||||
.sort((a, b) => dssOrder.indexOf(a.maxClass) - dssOrder.indexOf(b.maxClass));
|
||||
for (let i = 0; i < dssInternals.length; i++) {
|
||||
if (canMount(ship, dssInternals[i], 'sc')) {
|
||||
ship.use(dssInternals[i], ModuleUtils.internal('2i'));
|
||||
usedSlots.push(dssInternals[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fuel scoop - best possible
|
||||
const fuelScoopOrder = [8, 7, 6, 5, 4, 3, 2, 1];
|
||||
const fuelScoopInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
|
||||
.filter(a => (!a.eligible) || a.eligible.fs)
|
||||
.sort((a, b) => fuelScoopOrder.indexOf(a.maxClass) - fuelScoopOrder.indexOf(b.maxClass));
|
||||
for (let i = 0; i < fuelScoopInternals.length; i++) {
|
||||
if (canMount(ship, fuelScoopInternals[i], 'fs')) {
|
||||
ship.use(fuelScoopInternals[i], ModuleUtils.findInternal('fs', fuelScoopInternals[i].maxClass, 'A'));
|
||||
usedSlots.push(fuelScoopInternals[i]);
|
||||
fuelScoopSlot = fuelScoopInternals[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// AFMUs - fill as they are 0-weight
|
||||
const afmuOrder = [8, 7, 6, 5, 4, 3, 2, 1];
|
||||
const afmuInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
|
||||
.filter(a => (!a.eligible) || a.eligible.pc)
|
||||
.sort((a, b) => afmuOrder.indexOf(a.maxClass) - afmuOrder.indexOf(b.maxClass));
|
||||
for (let i = 0; i < afmuInternals.length; i++) {
|
||||
if (canMount(ship, afmuInternals[i], 'am')) {
|
||||
ship.use(afmuInternals[i], ModuleUtils.findInternal('am', afmuInternals[i].maxClass, 'A'));
|
||||
usedSlots.push(afmuInternals[i]);
|
||||
ship.setSlotEnabled(afmuInternals[i], false); // Disable power for AFM Unit
|
||||
}
|
||||
}
|
||||
|
||||
for (let s of ship.hardpoints) {
|
||||
if (s.maxClass == 0 && heatSinkCount) { // Mount up to 2 heatsinks
|
||||
ship.use(s, ModuleUtils.hardpoints('02'));
|
||||
ship.setSlotEnabled(s, heatSinkCount == 2); // Only enable a single Heatsink
|
||||
heatSinkCount--;
|
||||
} else {
|
||||
ship.use(s, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (sgSlot && fuelScoopSlot) {
|
||||
// The SG and Fuel scoop to not need to be powered at the same time
|
||||
if (sgSlot.m.getPowerUsage() > fuelScoopSlot.m.getPowerUsage()) { // The Shield generator uses the most power
|
||||
ship.setSlotEnabled(fuelScoopSlot, false);
|
||||
} else { // The Fuel scoop uses the most power
|
||||
ship.setSlotEnabled(sgSlot, false);
|
||||
}
|
||||
}
|
||||
|
||||
ship.useLightestStandard(standardOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Miner Role
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {Boolean} shielded True if shield generator should be included
|
||||
*/
|
||||
export function miner(ship, shielded) {
|
||||
shielded = true;
|
||||
let standardOpts = { ppRating: 'A' },
|
||||
miningLaserCount = 2,
|
||||
usedSlots = [],
|
||||
sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass);
|
||||
|
||||
// Cargo hatch should be enabled
|
||||
ship.setSlotEnabled(ship.cargoHatch, true);
|
||||
|
||||
// Largest possible refinery
|
||||
const refineryOrder = [4, 5, 6, 7, 8, 3, 2, 1];
|
||||
const refineryInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
|
||||
.filter(a => (!a.eligible) || a.eligible.rf)
|
||||
.sort((a, b) => refineryOrder.indexOf(a.maxClass) - refineryOrder.indexOf(b.maxClass));
|
||||
for (let i = 0; i < refineryInternals.length; i++) {
|
||||
if (canMount(ship, refineryInternals[i], 'rf')) {
|
||||
ship.use(refineryInternals[i], ModuleUtils.findInternal('rf', Math.min(refineryInternals[i].maxClass, 4), 'A'));
|
||||
usedSlots.push(refineryInternals[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Prospector limpet controller - 3A if possible
|
||||
const prospectorOrder = [3, 4, 5, 6, 7, 8, 2, 1];
|
||||
const prospectorInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
|
||||
.filter(a => (!a.eligible) || a.eligible.pc)
|
||||
.sort((a, b) => prospectorOrder.indexOf(a.maxClass) - prospectorOrder.indexOf(b.maxClass));
|
||||
for (let i = 0; i < prospectorInternals.length; i++) {
|
||||
if (canMount(ship, prospectorInternals[i], 'pc')) {
|
||||
// Prospector only has odd classes
|
||||
const prospectorClass = prospectorInternals[i].maxClass % 2 === 0 ? prospectorInternals[i].maxClass - 1 : prospectorInternals[i].maxClass;
|
||||
ship.use(prospectorInternals[i], ModuleUtils.findInternal('pc', prospectorClass, 'A'));
|
||||
usedSlots.push(prospectorInternals[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Shield generator if required
|
||||
if (shielded) {
|
||||
const shieldOrder = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const shieldInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
|
||||
.filter(a => (!a.eligible) || a.eligible.sg)
|
||||
.filter(a => a.maxClass >= sg.class)
|
||||
.sort((a, b) => shieldOrder.indexOf(a.maxClass) - shieldOrder.indexOf(b.maxClass));
|
||||
for (let i = 0; i < shieldInternals.length; i++) {
|
||||
if (canMount(ship, shieldInternals[i], 'sg')) {
|
||||
ship.use(shieldInternals[i], sg);
|
||||
usedSlots.push(shieldInternals[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dual mining lasers of highest possible class; remove anything else
|
||||
const miningLaserOrder = [2, 3, 4, 1, 0];
|
||||
const miningLaserHardpoints = ship.hardpoints.concat().sort(function(a, b) {
|
||||
return miningLaserOrder.indexOf(a.maxClass) - miningLaserOrder.indexOf(b.maxClass);
|
||||
});
|
||||
for (let s of miningLaserHardpoints) {
|
||||
if (s.maxClass >= 1 && miningLaserCount) {
|
||||
ship.use(s, ModuleUtils.hardpoints(s.maxClass >= 2 ? '2m' : '2l'));
|
||||
miningLaserCount--;
|
||||
} else {
|
||||
ship.use(s, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Number of collector limpets required to be active is a function of the size of the ship and the power of the lasers
|
||||
const miningLaserDps = ship.hardpoints.filter(h => h.m != null)
|
||||
.reduce(function(a, b) {
|
||||
return a + b.m.getDps();
|
||||
}, 0);
|
||||
// Find out how many internal slots we have, and their potential cargo size
|
||||
const potentialCargo = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
|
||||
.filter(a => (!a.eligible) || a.eligible.cr)
|
||||
.map(b => Math.pow(2, b.maxClass));
|
||||
// One collector for each 1.25 DPS, multiply by 1.25 for medium ships and 1.5 for large ships as they have further to travel
|
||||
// 0 if we only have 1 cargo slot, otherwise minium of 1 and maximum of 6 (excluding size modifier)
|
||||
const sizeModifier = ship.class == 2 ? 1.2 : ship.class == 3 ? 1.5 : 1;
|
||||
let collectorLimpetsRequired = potentialCargo.length == 1 ? 0 : Math.ceil(sizeModifier * Math.min(6, Math.floor(miningLaserDps / 1.25)));
|
||||
|
||||
if (collectorLimpetsRequired > 0) {
|
||||
const collectorOrder = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const collectorInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
|
||||
.filter(a => (!a.eligible) || a.eligible.cc)
|
||||
.sort((a, b) => collectorOrder.indexOf(a.maxClass) - collectorOrder.indexOf(b.maxClass));
|
||||
// Always keep at least 2 slots free for cargo racks (1 for shielded)
|
||||
for (let i = 0; i < collectorInternals.length - (shielded ? 1 : 2) && collectorLimpetsRequired > 0; i++) {
|
||||
if (canMount(ship, collectorInternals[i], 'cc')) {
|
||||
// Collector only has odd classes
|
||||
const collectorClass = collectorInternals[i].maxClass % 2 === 0 ? collectorInternals[i].maxClass - 1 : collectorInternals[i].maxClass;
|
||||
ship.use(collectorInternals[i], ModuleUtils.findInternal('cc', collectorClass, 'D'));
|
||||
usedSlots.push(collectorInternals[i]);
|
||||
collectorLimpetsRequired -= collectorInternals[i].m.maximum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power distributor to power the mining lasers indefinitely
|
||||
const wepRateRequired = ship.hardpoints.filter(h => h.m != null)
|
||||
.reduce(function(a, b) {
|
||||
return a + b.m.getEps();
|
||||
}, 0);
|
||||
standardOpts.pd = ship.getAvailableModules().matchingPowerDist({ weprate: wepRateRequired }).id;
|
||||
|
||||
// Fill the empty internals with cargo racks
|
||||
for (let i = ship.internal.length; i--;) {
|
||||
let slot = ship.internal[i];
|
||||
if (usedSlots.indexOf(slot) == -1 && canMount(ship, slot, 'cr')) {
|
||||
ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E'));
|
||||
}
|
||||
}
|
||||
|
||||
ship.useLightestStandard(standardOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Racer Role
|
||||
* @param {Ship} ship Ship instance
|
||||
*/
|
||||
export function racer(ship) {
|
||||
let standardOpts = {},
|
||||
usedSlots = [],
|
||||
sgSlot,
|
||||
sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass);
|
||||
|
||||
// Cargo hatch can be disabled
|
||||
ship.setSlotEnabled(ship.cargoHatch, false);
|
||||
|
||||
// Shield generator
|
||||
const shieldOrder = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const shieldInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
|
||||
.filter(a => (!a.eligible) || a.eligible.sg)
|
||||
.filter(a => a.maxClass >= sg.class)
|
||||
.sort((a, b) => shieldOrder.indexOf(a.maxClass) - shieldOrder.indexOf(b.maxClass));
|
||||
for (let i = 0; i < shieldInternals.length; i++) {
|
||||
if (canMount(ship, shieldInternals[i], 'sg')) {
|
||||
ship.use(shieldInternals[i], sg);
|
||||
usedSlots.push(shieldInternals[i]);
|
||||
sgSlot = shieldInternals[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Empty the hardpoints
|
||||
for (let s of ship.hardpoints) {
|
||||
ship.use(s, null);
|
||||
}
|
||||
|
||||
// Empty the internals
|
||||
for (let i = ship.internal.length; i--;) {
|
||||
let slot = ship.internal[i];
|
||||
if (usedSlots.indexOf(slot) == -1) {
|
||||
ship.use(slot, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Best thrusters
|
||||
if (ship.standard[1].maxClass === 3) {
|
||||
standardOpts.th = 'tz';
|
||||
} else if (ship.standard[1].maxClass === 2) {
|
||||
standardOpts.th = 'u0';
|
||||
} else {
|
||||
standardOpts.th = ship.standard[1].maxClass + 'A';
|
||||
}
|
||||
|
||||
// Best power distributor for more boosting
|
||||
standardOpts.pd = ship.standard[4].maxClass + 'A';
|
||||
|
||||
// Smallest possible FSD drive
|
||||
standardOpts.fsd = '2D';
|
||||
// Minimal fuel tank
|
||||
standardOpts.ft = '1C';
|
||||
|
||||
// Disable nearly everything
|
||||
standardOpts.fsdDisabled = true;
|
||||
standardOpts.sDisabled = true;
|
||||
standardOpts.pdDisabled = true;
|
||||
standardOpts.lsDisabled = true;
|
||||
|
||||
ship.useLightestStandard(standardOpts);
|
||||
|
||||
// Apply engineering to each module
|
||||
// ship.standard[1].m.blueprint = getBlueprint('Engine_Dirty', ship.standard[0]);
|
||||
// ship.standard[1].m.blueprint.grade = 5;
|
||||
// setBest(ship, ship.standard[1].m);
|
||||
|
||||
// ship.standard[3].m.blueprint = getBlueprint('LifeSupport_LightWeight', ship.standard[3]);
|
||||
// ship.standard[3].m.blueprint.grade = 4;
|
||||
// setBest(ship, ship.standard[3].m);
|
||||
|
||||
// ship.standard[4].m.blueprint = getBlueprint('PowerDistributor_PriorityEngines', ship.standard[4]);
|
||||
// ship.standard[4].m.blueprint.grade = 3;
|
||||
// setBest(ship, ship.standard[4].m);
|
||||
|
||||
// ship.standard[5].m.blueprint = getBlueprint('Sensor_Sensor_LightWeight', ship.standard[5]);
|
||||
// ship.standard[5].m.blueprint.grade = 5;
|
||||
// setBest(ship, ship.standard[5].m);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
export const SI_PREFIXES = {
|
||||
'Y': 1e+24, // Yotta
|
||||
'Z': 1e+21, // Zetta
|
||||
'E': 1e+18, // Peta
|
||||
'P': 1e+15, // Peta
|
||||
'T': 1e+12, // Tera
|
||||
'G': 1e+9, // Giga
|
||||
'M': 1e+6, // Mega
|
||||
'k': 1e+3, // Kilo
|
||||
'h': 1e+2, // Hekto
|
||||
'da': 1e+1, // Deka
|
||||
'': 1,
|
||||
'd': 1e-1, // Dezi
|
||||
'c': 1e-2, // Zenti
|
||||
'm': 1e-3, // Milli
|
||||
'μ': 1e-6, // mikro not supported due to charset
|
||||
'n': 10e-9, // Nano
|
||||
'p': 1e-12, // Nano
|
||||
'f': 1e-15, // Femto
|
||||
'a': 1e-18, // Atto
|
||||
'z': 1e-21, // Zepto
|
||||
'y': 1e-24 // Yokto
|
||||
};
|
||||
|
||||
export const STATS_FORMATTING = {
|
||||
'ammo': { 'format': 'int', },
|
||||
'boot': { 'format': 'int', 'unit': 'secs' },
|
||||
'brokenregen': { 'format': 'round1', 'unit': 'ps' },
|
||||
'burst': { 'format': 'int', 'change': 'additive' },
|
||||
'burstrof': { 'format': 'round1', 'unit': 'ps', 'change': 'additive' },
|
||||
'causres': { 'format': 'pct' },
|
||||
'clip': { 'format': 'int' },
|
||||
'damage': { 'format': 'round' },
|
||||
'dps': { 'format': 'round', 'units': 'ps', 'synthetic': 'getDps' },
|
||||
'dpe': { 'format': 'round', 'units': 'ps', 'synthetic': 'getDpe' },
|
||||
'distdraw': { 'format': 'round', 'unit': 'MW' },
|
||||
'duration': { 'format': 'round1', 'unit': 's' },
|
||||
'eff': { 'format': 'round2' },
|
||||
'engcap': { 'format': 'round1', 'unit': 'MJ' },
|
||||
'engrate': { 'format': 'round1', 'unit': 'MW' },
|
||||
'eps': { 'format': 'round', 'units': 'ps', 'synthetic': 'getEps' },
|
||||
'explres': { 'format': 'pct' },
|
||||
'facinglimit': { 'format': 'round1', 'unit': 'ang' },
|
||||
'falloff': { 'format': 'round', 'unit': 'km', 'storedUnit': 'm' },
|
||||
'fallofffromrange': { 'format': 'round', 'unit': 'km', 'storedUnit': 'm', 'synthetic': 'getFalloff' },
|
||||
'hps': { 'format': 'round', 'units': 'ps', 'synthetic': 'getHps' },
|
||||
'hullboost': { 'format': 'pct1', 'change': 'additive' },
|
||||
'hullreinforcement': { 'format': 'int' },
|
||||
'integrity': { 'format': 'round1' },
|
||||
'jitter': { 'format': 'round', 'unit': 'ang' },
|
||||
'kinres': { 'format': 'pct' },
|
||||
'mass': { 'format': 'round1', 'unit': 'T' },
|
||||
'maxfuel': { 'format': 'round1', 'unit': 'T' },
|
||||
'optmass': { 'format': 'int', 'unit': 'T' },
|
||||
'optmul': { 'format': 'pct', 'change': 'additive' },
|
||||
'pgen': { 'format': 'round1', 'unit': 'MW' },
|
||||
'piercing': { 'format': 'int' },
|
||||
'power': { 'format': 'round', 'unit': 'MW' },
|
||||
'protection': { 'format': 'pct' },
|
||||
'range': { 'format': 'f2', 'unit': 'km', 'storedUnit': 'm' },
|
||||
'ranget': { 'format': 'f1', 'unit': 's' },
|
||||
'regen': { 'format': 'round1', 'unit': 'ps' },
|
||||
'reload': { 'format': 'int', 'unit': 's' },
|
||||
'rof': { 'format': 'round1', 'unit': 'ps', 'synthetic': 'getRoF', 'higherbetter': true },
|
||||
'angle': { 'format': 'round1', 'unit': 'ang' },
|
||||
'scanrate': { 'format': 'int' },
|
||||
'scantime': { 'format': 'round1', 'unit': 's' },
|
||||
'sdps': { 'format': 'round1', 'units': 'ps', 'synthetic': 'getSDps' },
|
||||
'shield': { 'format': 'int', 'unit': 'MJ' },
|
||||
'shieldaddition': { 'format': 'round1', 'unit': 'MJ' },
|
||||
'shieldboost': { 'format': 'pct1', 'change': 'additive' },
|
||||
'shieldreinforcement': { 'format': 'round1', 'unit': 'MJ' },
|
||||
'shotspeed': { 'format': 'int', 'unit': 'm/s' },
|
||||
'spinup': { 'format': 'round1', 'unit': 's' },
|
||||
'syscap': { 'format': 'round1', 'unit': 'MJ' },
|
||||
'sysrate': { 'format': 'round1', 'unit': 'MW' },
|
||||
'thermload': { 'format': 'round1' },
|
||||
'thermres': { 'format': 'pct' },
|
||||
'wepcap': { 'format': 'round1', 'unit': 'MJ' },
|
||||
'weprate': { 'format': 'round1', 'unit': 'MW' },
|
||||
'jumpboost': { 'format': 'round1', 'unit': 'LY' },
|
||||
'proberadius': { 'format': 'pct1', 'unit': 'pct' },
|
||||
};
|
||||
33
src/app/shipyard/StatsMapping.js
Normal file
33
src/app/shipyard/StatsMapping.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Module } from 'ed-forge';
|
||||
|
||||
/**
|
||||
* Sets a resistance value of a module as
|
||||
* @param {Module} module Module to set the property
|
||||
* @param {string} prop Property name; must end with 'resistance'
|
||||
* @param {number} val Resistance value to set
|
||||
*/
|
||||
function setterResToEff(module, prop, val) {
|
||||
module.set(
|
||||
prop.replace('resistance', 'effectiveness'),
|
||||
1 - val / 100,
|
||||
);
|
||||
}
|
||||
|
||||
export const SHOW = {
|
||||
causticeffectiveness: {
|
||||
as: 'causticresistance',
|
||||
setter: setterResToEff,
|
||||
},
|
||||
explosiveeffectiveness: {
|
||||
as: 'explosiveresistance',
|
||||
setter: setterResToEff,
|
||||
},
|
||||
kineticeffectiveness: {
|
||||
as: 'kineticresistance',
|
||||
setter: setterResToEff,
|
||||
},
|
||||
thermiceffectiveness: {
|
||||
as: 'thermicresistance',
|
||||
setter: setterResToEff,
|
||||
},
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { EventEmitter } from 'fbemitter';
|
||||
import { Insurance } from '../shipyard/Constants';
|
||||
|
||||
const LS_KEY_BUILDS = 'builds';
|
||||
const LS_KEY_COMPARISONS = 'comparisons';
|
||||
const LS_KEY_LANG = 'NG_TRANSLATE_LANG_KEY';
|
||||
const LS_KEY_COST_TAB = 'costTab';
|
||||
const LS_KEY_CMDR_NAME = 'cmdrName';
|
||||
@@ -13,7 +12,6 @@ const LS_KEY_MOD_DISCOUNT = 'moduleDiscount';
|
||||
const LS_KEY_STATE = 'state';
|
||||
const LS_KEY_SIZE_RATIO = 'sizeRatio';
|
||||
const LS_KEY_TOOLTIPS = 'tooltips';
|
||||
const LS_KEY_MODULE_RESISTANCES = 'moduleResistances';
|
||||
const LS_KEY_ROLLS = 'matsPerGrade';
|
||||
|
||||
let LS;
|
||||
@@ -84,7 +82,6 @@ export class Persist extends EventEmitter {
|
||||
LS = null;
|
||||
}
|
||||
|
||||
let moduleResistances = _get(LS_KEY_MODULE_RESISTANCES);
|
||||
let matsPerGrade = _get(LS_KEY_ROLLS);
|
||||
let cmdrName = _get(LS_KEY_CMDR_NAME);
|
||||
let tips = _get(LS_KEY_TOOLTIPS);
|
||||
@@ -92,7 +89,6 @@ export class Persist extends EventEmitter {
|
||||
let shipDiscount = _get(LS_KEY_SHIP_DISCOUNT);
|
||||
let moduleDiscount = _get(LS_KEY_MOD_DISCOUNT);
|
||||
let buildJson = _get(LS_KEY_BUILDS);
|
||||
let comparisonJson = _get(LS_KEY_COMPARISONS);
|
||||
|
||||
this.onStorageChange = this.onStorageChange.bind(this);
|
||||
this.langCode = _getString(LS_KEY_LANG) || 'en';
|
||||
@@ -100,7 +96,6 @@ export class Persist extends EventEmitter {
|
||||
this.shipDiscount = !isNaN(shipDiscount) && shipDiscount < 1 ? shipDiscount * 1 : 0;
|
||||
this.moduleDiscount = !isNaN(moduleDiscount) && moduleDiscount < 1 ? moduleDiscount * 1 : 0;
|
||||
this.builds = buildJson && typeof buildJson == 'object' ? buildJson : {};
|
||||
this.comparisons = comparisonJson && typeof comparisonJson == 'object' ? comparisonJson : {};
|
||||
this.costTab = _getString(LS_KEY_COST_TAB);
|
||||
this.outfittingTab = _getString(LS_KEY_OUTFITTING_TAB);
|
||||
this.state = _get(LS_KEY_STATE);
|
||||
@@ -114,7 +109,6 @@ export class Persist extends EventEmitter {
|
||||
};
|
||||
this.cmdrName = cmdrName || { selected: '', cmdrs: [] };
|
||||
this.tooltipsEnabled = tips === null ? true : tips;
|
||||
this.moduleResistancesEnabled = moduleResistances === null ? true : moduleResistances;
|
||||
|
||||
if (LS) {
|
||||
window.addEventListener('storage', this.onStorageChange);
|
||||
@@ -135,10 +129,6 @@ export class Persist extends EventEmitter {
|
||||
this.builds = newValue ? JSON.parse(newValue) : {};
|
||||
this.emit('builds');
|
||||
break;
|
||||
case LS_KEY_COMPARISONS:
|
||||
this.comparisons = newValue ? JSON.parse(newValue) : {};
|
||||
this.emit('comparisons');
|
||||
break;
|
||||
case LS_KEY_LANG:
|
||||
this.langCode = newValue;
|
||||
this.emit('language', newValue);
|
||||
@@ -159,10 +149,6 @@ export class Persist extends EventEmitter {
|
||||
this.tooltipsEnabled = !!newValue && newValue.toLowerCase() == 'true';
|
||||
this.emit('tooltips', this.tooltipsEnabled);
|
||||
break;
|
||||
case LS_KEY_MODULE_RESISTANCES:
|
||||
this.moduleResistancesEnabled = !!newValue && newValue.toLowerCase() == 'true';
|
||||
this.emit('moduleresistances', this.moduleResistancesEnabled);
|
||||
break;
|
||||
case LS_KEY_ROLLS:
|
||||
this.matsPerGrade = JSON.parse(newValue);
|
||||
this.emit('matsPerGrade', this.matsPerGrade);
|
||||
@@ -207,21 +193,6 @@ export class Persist extends EventEmitter {
|
||||
return this.tooltipsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show module resistances setting
|
||||
* @param {boolean} show Optional - update setting
|
||||
* @return {boolean} True if module resistances should be shown
|
||||
*/
|
||||
showModuleResistances(show) {
|
||||
if (show !== undefined) {
|
||||
this.moduleResistancesEnabled = !!show;
|
||||
_put(LS_KEY_MODULE_RESISTANCES, this.moduleResistancesEnabled);
|
||||
this.emit('moduleresistances', this.moduleResistancesEnabled);
|
||||
}
|
||||
|
||||
return this.moduleResistancesEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a ship build in local storage.
|
||||
*
|
||||
@@ -310,97 +281,16 @@ export class Persist extends EventEmitter {
|
||||
delete this.builds[shipId];
|
||||
}
|
||||
_put(LS_KEY_BUILDS, this.builds);
|
||||
// Check if the build was used in existing comparisons
|
||||
let comps = this.comparisons;
|
||||
for (let c in comps) {
|
||||
for (let i = 0; i < comps[c].builds.length; i++) { // For all builds in the current comparison
|
||||
if (comps[c].builds[i].shipId == shipId && comps[c].builds[i].buildName == name) {
|
||||
comps[c].builds.splice(i, 1);
|
||||
break; // A build is unique per comparison
|
||||
}
|
||||
}
|
||||
}
|
||||
_put(LS_KEY_COMPARISONS, this.comparisons);
|
||||
this.emit('builds');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a comparison in localstorage.
|
||||
*
|
||||
* @param {String} name The name of the comparison
|
||||
* @param {array} builds Array of builds
|
||||
* @param {array} facets Array of facet indices
|
||||
*/
|
||||
saveComparison(name, builds, facets) {
|
||||
if (!this.comparisons[name]) {
|
||||
this.comparisons[name] = {};
|
||||
}
|
||||
this.comparisons[name] = {
|
||||
facets,
|
||||
builds: builds.map(b => { return { shipId: b.id || b.shipId, buildName: b.buildName }; })
|
||||
};
|
||||
_put(LS_KEY_COMPARISONS, this.comparisons);
|
||||
this.emit('comparisons');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a comparison
|
||||
* @param {String} name Comparison name
|
||||
* @return {Object} Object containing array of facets and ship id + build names
|
||||
*/
|
||||
getComparison(name) {
|
||||
if (this.comparisons[name]) {
|
||||
return this.comparisons[name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all saved comparisons
|
||||
* @return {Object} All comparisons
|
||||
*/
|
||||
getComparisons() {
|
||||
return this.comparisons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comparison has been saved
|
||||
* @param {String} name Comparison name
|
||||
* @return {Boolean} True if a comparison has been saved
|
||||
*/
|
||||
hasComparison(name) {
|
||||
return !!this.comparisons[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any comparisons have been saved
|
||||
* @return {Boolean} True if any comparisons have been saved
|
||||
*/
|
||||
hasComparisons() {
|
||||
return Object.keys(this.comparisons).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the comparison from localstorage.
|
||||
* @param {String} name Comparison name
|
||||
*/
|
||||
deleteComparison(name) {
|
||||
if (this.comparisons[name]) {
|
||||
delete this.comparisons[name];
|
||||
_put(LS_KEY_COMPARISONS, this.comparisons);
|
||||
this.emit('comparisons');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all builds and comparisons from localStorage
|
||||
* Delete all builds from localStorage
|
||||
*/
|
||||
deleteAll() {
|
||||
this.builds = {};
|
||||
this.comparisons = {};
|
||||
_put(LS_KEY_BUILDS, {});
|
||||
_put(LS_KEY_COMPARISONS, {});
|
||||
this.emit('deletedAll');
|
||||
}
|
||||
|
||||
@@ -411,7 +301,6 @@ export class Persist extends EventEmitter {
|
||||
getAll() {
|
||||
let data = {};
|
||||
data[LS_KEY_BUILDS] = this.getBuilds();
|
||||
data[LS_KEY_COMPARISONS] = this.getComparisons();
|
||||
data[LS_KEY_INSURANCE] = this.getInsurance();
|
||||
data[LS_KEY_SHIP_DISCOUNT] = this.shipDiscount;
|
||||
data[LS_KEY_MOD_DISCOUNT] = this.moduleDiscount;
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* Generate a BBCode (Forum) compatible table from comparisons
|
||||
* @param {Function} translate Translate language function
|
||||
* @param {object} formats Number Formats
|
||||
* @param {array} facets Ship Facets
|
||||
* @param {object} builds Ship builds
|
||||
* @param {string} link Link to the comparison
|
||||
* @return {string} the BBCode
|
||||
*/
|
||||
export function comparisonBBCode(translate, formats, facets, builds, link) {
|
||||
let colCount = 2, b, i, j, k, f, fl, p, pl, l = [];
|
||||
|
||||
for (i = 0; i < facets.length; i++) {
|
||||
if (facets[i].active) {
|
||||
f = facets[i];
|
||||
p = f.props;
|
||||
|
||||
if (p.length == 1) {
|
||||
l.push('[th][B][COLOR=#FF8C0D]', translate(f.title).toUpperCase(), '[/COLOR][/B][/th]');
|
||||
colCount++;
|
||||
} else {
|
||||
for (j = 0; j < p.length; j++) {
|
||||
l.push('[th][B][COLOR=#FF8C0D]', translate(f.title).toUpperCase(), '\n', translate(f.lbls[j]).toUpperCase(), '[/COLOR][/B][/th]');
|
||||
colCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
l.push('[/tr]\n');
|
||||
|
||||
for (i = 0; i < builds.length; i++) {
|
||||
b = builds[i];
|
||||
|
||||
l.push('[tr][td]', b.name, '[/td][td]', b.buildName, '[/td]');
|
||||
|
||||
for (j = 0, fl = facets.length; j < fl; j++) {
|
||||
if (facets[j].active) {
|
||||
f = facets[j];
|
||||
p = f.props;
|
||||
for (k = 0, pl = p.length; k < pl; k++) {
|
||||
l.push('[td="align: right"]', formats[f.fmt](b[p[k]]), ' [size=-2]', translate(f.unit), '[/size][/td]');
|
||||
}
|
||||
}
|
||||
}
|
||||
l.push('[/tr]\n');
|
||||
}
|
||||
l.push('[tr][td="align: center, colspan:', colCount, '"][size=-3]\n[url=', link, ']Interactive Comparison at Coriolis.io[/url][/td][/tr]\n[/size][/table]');
|
||||
l.unshift('[table="width:', colCount * 90, ',align: center"]\n[tr][th][B][COLOR=#FF8C0D]Ship[/COLOR][/B][/th][th][B][COLOR="#FF8C0D"]Build[/COLOR][/B][/th]');
|
||||
return l.join('');
|
||||
}
|
||||
@@ -1,61 +1,60 @@
|
||||
import React from 'react';
|
||||
import { Modifications } from 'coriolis-data/dist';
|
||||
import { STATS_FORMATTING } from '../shipyard/StatsFormatting';
|
||||
import { Module } from 'ed-forge';
|
||||
import { getBlueprintInfo, getExperimentalInfo } from 'ed-forge/lib/src/data/blueprints';
|
||||
import { fromPairs, keys, uniq } from 'lodash';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Module} module Module to get modifiers for
|
||||
* @param {string[]} props Properties to get modifiers for
|
||||
* @returns {React.Component[]} Table-cell modifiers
|
||||
*/
|
||||
function _getModifiers(formats, module, props) {
|
||||
return fromPairs(props.map((prop) => {
|
||||
const { value, unit, beneficial } = module.getModifierFormatted(prop);
|
||||
return [
|
||||
prop,
|
||||
<td className={beneficial === undefined ? '' : beneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>
|
||||
{formats.round(value || 0)}{unit}
|
||||
</td>
|
||||
];
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a tooltip with details of a blueprint's specials
|
||||
* @param {Object} translate The translate object
|
||||
* @param {Object} blueprint The blueprint at the required grade
|
||||
* @param {string} grp The group of the module
|
||||
* @param {Object} m The module to compare with
|
||||
* @param {Object} language The translate object
|
||||
* @param {Module} m The module to compare with
|
||||
* @param {string} specialName The name of the special
|
||||
* @returns {Object} The react components
|
||||
*/
|
||||
export function specialToolTip(translate, blueprint, grp, m, specialName) {
|
||||
const effects = [];
|
||||
if (!blueprint || !blueprint.features) {
|
||||
return undefined;
|
||||
}
|
||||
if (m) {
|
||||
// We also add in any benefits from specials that aren't covered above
|
||||
if (m.blueprint) {
|
||||
for (const feature in Modifications.modifierActions[specialName]) {
|
||||
// if (!blueprint.features[feature] && !m.mods.feature) {
|
||||
const featureDef = Modifications.modifications[feature];
|
||||
if (featureDef && !featureDef.hidden) {
|
||||
let symbol = '';
|
||||
if (feature === 'jitter') {
|
||||
symbol = '°';
|
||||
} else if (featureDef.type === 'percentage') {
|
||||
symbol = '%';
|
||||
}
|
||||
let current = m.getModValue(feature) - m.getModValue(feature, true);
|
||||
if (featureDef.type === 'percentage') {
|
||||
current = Math.round(current / 10) / 10;
|
||||
} else if (featureDef.type === 'numeric') {
|
||||
current /= 100;
|
||||
}
|
||||
const currentIsBeneficial = isValueBeneficial(feature, current);
|
||||
|
||||
effects.push(
|
||||
<tr key={feature + '_specialTT'}>
|
||||
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
|
||||
<td> </td>
|
||||
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'}
|
||||
style={{ textAlign: 'right' }}>{current}{symbol}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function specialToolTip(language, m, specialName) {
|
||||
const { formats, translate } = language;
|
||||
const features = keys(getExperimentalInfo(specialName).features);
|
||||
const currents = _getModifiers(formats, m, features);
|
||||
const thens = m.try(() => {
|
||||
m.setExperimental(specialName);
|
||||
return _getModifiers(formats, m, features);
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<table width='100%'>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{translate('feature')}</td>
|
||||
<td>{translate('current')}</td>
|
||||
<td>{translate('then')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{effects}
|
||||
{features.map((prop) => {
|
||||
return <tr key={prop + '_specialTT'}>
|
||||
<td style={{ textAlign: 'left' }}>{translate(prop)}</td>
|
||||
{currents[prop]}
|
||||
{thens[prop]}
|
||||
</tr>;
|
||||
}
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -63,154 +62,33 @@ export function specialToolTip(translate, blueprint, grp, m, specialName) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a tooltip with details of a blueprint's effects
|
||||
* @param {Object} translate The translate object
|
||||
* @param {Object} blueprint The blueprint at the required grade
|
||||
* @param {Array} engineers The engineers supplying this blueprint
|
||||
* @param {string} grp The group of the module
|
||||
* @param {Object} m The module to compare with
|
||||
* Generate a tooltip with details and preview of a blueprint's effects
|
||||
* @param {Object} language The language object
|
||||
* @param {Module} m The module to compare with
|
||||
* @param {string} previewBP Blueprint to preview
|
||||
* @param {number} previewGrade Grade to preview
|
||||
* @returns {Object} The react components
|
||||
*/
|
||||
export function blueprintTooltip(translate, blueprint, engineers, grp, m) {
|
||||
const effects = [];
|
||||
if (!blueprint || !blueprint.features) {
|
||||
return undefined;
|
||||
}
|
||||
for (const feature in blueprint.features) {
|
||||
const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]);
|
||||
const featureDef = Modifications.modifications[feature];
|
||||
if (!featureDef.hidden) {
|
||||
let symbol = '';
|
||||
if (feature === 'jitter') {
|
||||
symbol = '°';
|
||||
} else if (featureDef.type === 'percentage') {
|
||||
symbol = '%';
|
||||
}
|
||||
let lowerBound = blueprint.features[feature][0];
|
||||
let upperBound = blueprint.features[feature][1];
|
||||
if (featureDef.type === 'percentage') {
|
||||
lowerBound = Math.round(lowerBound * 1000) / 10;
|
||||
upperBound = Math.round(upperBound * 1000) / 10;
|
||||
}
|
||||
const lowerIsBeneficial = isValueBeneficial(feature, lowerBound);
|
||||
const upperIsBeneficial = isValueBeneficial(feature, upperBound);
|
||||
if (m) {
|
||||
// We have a module - add in the current value
|
||||
let current = m.getModValue(feature);
|
||||
if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
|
||||
current = Math.round(current / 10) / 10;
|
||||
} else if (featureDef.type === 'numeric') {
|
||||
current /= 100;
|
||||
}
|
||||
const currentIsBeneficial = isValueBeneficial(feature, current);
|
||||
effects.push(
|
||||
<tr key={feature}>
|
||||
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
|
||||
<td className={lowerBound === 0 ? '' : lowerIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{lowerBound}{symbol}</td>
|
||||
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td>
|
||||
<td className={upperBound === 0 ? '' : upperIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{upperBound}{symbol}</td>
|
||||
</tr>
|
||||
);
|
||||
} else {
|
||||
// We do not have a module, no value
|
||||
effects.push(
|
||||
<tr key={feature}>
|
||||
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
|
||||
<td className={lowerBound === 0 ? '' : lowerIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{lowerBound}{symbol}</td>
|
||||
<td className={upperBound === 0 ? '' : upperIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{upperBound}{symbol}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m) {
|
||||
// Because we have a module add in any benefits that aren't part of the primary blueprint
|
||||
for (const feature in m.mods) {
|
||||
if (!blueprint.features[feature]) {
|
||||
const featureDef = Modifications.modifications[feature];
|
||||
if (featureDef && !featureDef.hidden) {
|
||||
let symbol = '';
|
||||
if (feature === 'jitter') {
|
||||
symbol = '°';
|
||||
} else if (featureDef.type === 'percentage') {
|
||||
symbol = '%';
|
||||
}
|
||||
let current = m.getModValue(feature);
|
||||
if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
|
||||
current = Math.round(current / 10) / 10;
|
||||
} else if (featureDef.type === 'numeric') {
|
||||
current /= 100;
|
||||
}
|
||||
const currentIsBeneficial = isValueBeneficial(feature, current);
|
||||
effects.push(
|
||||
<tr key={feature}>
|
||||
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
|
||||
<td> </td>
|
||||
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
export function blueprintTooltip(language, m, previewBP, previewGrade) {
|
||||
const { translate, formats } = language;
|
||||
const blueprint = previewBP || m.getBlueprint();
|
||||
const grade = previewGrade || m.getBlueprintGrade();
|
||||
if (!blueprint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We also add in any benefits from specials that aren't covered above
|
||||
if (m.blueprint && m.blueprint.special) {
|
||||
for (const feature in Modifications.modifierActions[m.blueprint.special.edname]) {
|
||||
if (!blueprint.features[feature] && !m.mods.feature) {
|
||||
const featureDef = Modifications.modifications[feature];
|
||||
if (featureDef && !featureDef.hidden) {
|
||||
let symbol = '';
|
||||
if (feature === 'jitter') {
|
||||
symbol = '°';
|
||||
} else if (featureDef.type === 'percentage') {
|
||||
symbol = '%';
|
||||
}
|
||||
let current = m.getModValue(feature);
|
||||
if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
|
||||
current = Math.round(current / 10) / 10;
|
||||
} else if (featureDef.type === 'numeric') {
|
||||
current /= 100;
|
||||
}
|
||||
const currentIsBeneficial = isValueBeneficial(feature, current);
|
||||
effects.push(
|
||||
<tr key={feature}>
|
||||
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
|
||||
<td> </td>
|
||||
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let components;
|
||||
if (!m) {
|
||||
components = [];
|
||||
for (const component in blueprint.components) {
|
||||
components.push(
|
||||
<tr key={component}>
|
||||
<td style={{ textAlign: 'left' }}>{translate(component)}</td>
|
||||
<td style={{ textAlign: 'right' }}>{blueprint.components[component]}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let engineersList;
|
||||
if (engineers) {
|
||||
engineersList = [];
|
||||
for (const engineer of engineers) {
|
||||
engineersList.push(
|
||||
<tr key={engineer}>
|
||||
<td style={{ textAlign: 'left' }}>{engineer}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
const features = uniq(m.getModifiedProperties().concat(
|
||||
keys(getBlueprintInfo(blueprint).features[grade])
|
||||
));
|
||||
const mins = m.try(() => {
|
||||
m.setBlueprint(blueprint, grade, 0);
|
||||
return _getModifiers(formats, m, features);
|
||||
});
|
||||
const currents = _getModifiers(formats, m, features);
|
||||
const maxs = m.try(() => {
|
||||
m.setBlueprint(blueprint, grade, 1);
|
||||
return _getModifiers(formats, m, features);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -219,217 +97,21 @@ export function blueprintTooltip(translate, blueprint, engineers, grp, m) {
|
||||
<tr>
|
||||
<td>{translate('feature')}</td>
|
||||
<td>{translate('worst')}</td>
|
||||
{m ? <td>{translate('current')}</td> : null }
|
||||
<td>{translate('current')}</td>
|
||||
<td>{translate('best')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{effects}
|
||||
{features.map((prop) => {
|
||||
return (<tr key={prop}>
|
||||
<td style={{ textAlign: 'left' }}>{translate(prop)}</td>
|
||||
{mins[prop]}
|
||||
{currents[prop]}
|
||||
{maxs[prop]}
|
||||
</tr>);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{ components ? <table width='100%'>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{translate('component')}</td>
|
||||
<td>{translate('amount')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{components}
|
||||
</tbody>
|
||||
</table> : null }
|
||||
{ engineersList ? <table width='100%'>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{translate('engineers')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{engineersList}
|
||||
</tbody>
|
||||
</table> : null }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this blueprint feature beneficial?
|
||||
* @param {string} feature The name of the feature
|
||||
* @param {array} values The value of the feature
|
||||
* @returns {boolean} True if this feature is beneficial
|
||||
*/
|
||||
export function isBeneficial(feature, values) {
|
||||
const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0));
|
||||
if (Modifications.modifications[feature].higherbetter) {
|
||||
return !fact;
|
||||
} else {
|
||||
return fact;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this feature value beneficial?
|
||||
* @param {string} feature The name of the feature
|
||||
* @param {number} value The value of the feature
|
||||
* @returns {boolean} True if this value is beneficial
|
||||
*/
|
||||
export function isValueBeneficial(feature, value) {
|
||||
if (Modifications.modifications[feature].higherbetter) {
|
||||
return value > 0;
|
||||
} else {
|
||||
return value < 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the change as shown beneficial?
|
||||
* @param {string} feature The name of the feature
|
||||
* @param {number} value The value of the feature as percentage change
|
||||
* @returns True if the value is beneficial
|
||||
*/
|
||||
export function isChangeValueBeneficial(feature, value) {
|
||||
let changeHigherBetter = STATS_FORMATTING[feature].higherbetter;
|
||||
if (changeHigherBetter === undefined) {
|
||||
return isValueBeneficial(feature, value);
|
||||
}
|
||||
|
||||
if (changeHigherBetter) {
|
||||
return value > 0;
|
||||
} else {
|
||||
return value < 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a blueprint with a given name and an optional module
|
||||
* @param {string} name The name of the blueprint
|
||||
* @param {Object} module The module for which to obtain this blueprint
|
||||
* @returns {Object} The matching blueprint
|
||||
*/
|
||||
export function getBlueprint(name, module) {
|
||||
// Start with a copy of the blueprint
|
||||
const findMod = val => Object.keys(Modifications.blueprints).find(elem => elem.toString().toLowerCase().search(val.toString().toLowerCase().replace(/(OutfittingFieldType_|persecond)/igm, '')) >= 0);
|
||||
const found = Modifications.blueprints[findMod(name)];
|
||||
if (!found || !found.fdname) {
|
||||
return {};
|
||||
}
|
||||
const blueprint = JSON.parse(JSON.stringify(found));
|
||||
return blueprint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide 'percent' primary modifications
|
||||
* @param {Object} ship The ship for which to perform the modifications
|
||||
* @param {Object} m The module for which to perform the modifications
|
||||
* @param {Number} percent The percent to set values to of full.
|
||||
*/
|
||||
export function setPercent(ship, m, percent) {
|
||||
ship.clearModifications(m);
|
||||
// Pick given value as multiplier
|
||||
const mult = percent / 100;
|
||||
setQualityCB(m.blueprint, mult, (featureName, value) => ship.setModification(m, featureName, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the blueprint quality and fires a callback for each property affected.
|
||||
* @param {Object} blueprint The ship for which to perform the modifications
|
||||
* @param {Number} quality The quality to apply - float number 0 to 1.
|
||||
* @param {Function} cb The Callback to run for each property. Function (featureName, value)
|
||||
*/
|
||||
export function setQualityCB(blueprint, quality, cb) {
|
||||
// Pick given value as multiplier
|
||||
const features = blueprint.grades[blueprint.grade].features;
|
||||
for (const featureName in features) {
|
||||
let value;
|
||||
if (Modifications.modifications[featureName].higherbetter) {
|
||||
// Higher is better, but is this making it better or worse?
|
||||
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
|
||||
value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality);
|
||||
} else {
|
||||
value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality);
|
||||
}
|
||||
} else {
|
||||
// Higher is worse, but is this making it better or worse?
|
||||
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
|
||||
value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality);
|
||||
} else {
|
||||
value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality);
|
||||
}
|
||||
}
|
||||
|
||||
if (Modifications.modifications[featureName].type == 'percentage') {
|
||||
value = value * 10000;
|
||||
} else if (Modifications.modifications[featureName].type == 'numeric') {
|
||||
value = value * 100;
|
||||
}
|
||||
|
||||
cb(featureName, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide 'random' primary modifications
|
||||
* @param {Object} ship The ship for which to perform the modifications
|
||||
* @param {Object} m The module for which to perform the modifications
|
||||
*/
|
||||
export function setRandom(ship, m) {
|
||||
// Pick a single value for our randomness
|
||||
setPercent(ship, m, Math.random() * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide 'percent' primary query
|
||||
* @param {Object} m The module for which to perform the query
|
||||
* @returns {Number} percent The percentage indicator of current applied values.
|
||||
*/
|
||||
export function getPercent(m) {
|
||||
let result = null;
|
||||
const features = m.blueprint.grades[m.blueprint.grade].features;
|
||||
for (const featureName in features) {
|
||||
if (features[featureName][0] === features[featureName][1]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = _getValue(m, featureName);
|
||||
let mult;
|
||||
if (Modifications.modifications[featureName].higherbetter) {
|
||||
// Higher is better, but is this making it better or worse?
|
||||
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
|
||||
mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100);
|
||||
} else {
|
||||
mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100);
|
||||
}
|
||||
} else {
|
||||
// Higher is worse, but is this making it better or worse?
|
||||
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
|
||||
mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100);
|
||||
} else {
|
||||
mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
if (result && result != mult) {
|
||||
return null;
|
||||
} else if (result != mult) {
|
||||
result = mult;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query a feature value
|
||||
* @param {Object} m The module for which to perform the query
|
||||
* @param {string} featureName The feature being queried
|
||||
* @returns {number} The value of the modification as a %
|
||||
*/
|
||||
function _getValue(m, featureName) {
|
||||
if (Modifications.modifications[featureName].type == 'percentage') {
|
||||
return m.getModValue(featureName, true) / 10000;
|
||||
} else if (Modifications.modifications[featureName].type == 'numeric') {
|
||||
return m.getModValue(featureName, true) / 100;
|
||||
} else {
|
||||
return m.getModValue(featureName, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,470 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Modifications, Modules, Ships } from 'coriolis-data/dist';
|
||||
import Module from '../shipyard/Module';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import { getBlueprint } from '../utils/BlueprintFunctions';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
|
||||
// mapping from fd's ship model names to coriolis'
|
||||
export const SHIP_FD_NAME_TO_CORIOLIS_NAME = {
|
||||
'Adder': 'adder',
|
||||
'Anaconda': 'anaconda',
|
||||
'Asp': 'asp',
|
||||
'Asp_Scout': 'asp_scout',
|
||||
'BelugaLiner': 'beluga',
|
||||
'CobraMkIII': 'cobra_mk_iii',
|
||||
'CobraMkIV': 'cobra_mk_iv',
|
||||
'Cutter': 'imperial_cutter',
|
||||
'DiamondBackXL': 'diamondback_explorer',
|
||||
'DiamondBack': 'diamondback',
|
||||
'Dolphin': 'dolphin',
|
||||
'Eagle': 'eagle',
|
||||
'Empire_Courier': 'imperial_courier',
|
||||
'Empire_Eagle': 'imperial_eagle',
|
||||
'Empire_Trader': 'imperial_clipper',
|
||||
'Federation_Corvette': 'federal_corvette',
|
||||
'Federation_Dropship': 'federal_dropship',
|
||||
'Federation_Dropship_MkII': 'federal_assault_ship',
|
||||
'Federation_Gunship': 'federal_gunship',
|
||||
'FerDeLance': 'fer_de_lance',
|
||||
'Hauler': 'hauler',
|
||||
'Independant_Trader': 'keelback',
|
||||
'Krait_MkII': 'krait_mkii',
|
||||
'Mamba': 'mamba',
|
||||
'Krait_Light': 'krait_phantom',
|
||||
'Orca': 'orca',
|
||||
'Python': 'python',
|
||||
'SideWinder': 'sidewinder',
|
||||
'Type6': 'type_6_transporter',
|
||||
'Type7': 'type_7_transport',
|
||||
'Type9': 'type_9_heavy',
|
||||
'Type9_Military': 'type_10_defender',
|
||||
'TypeX': 'alliance_chieftain',
|
||||
'TypeX_2': 'alliance_crusader',
|
||||
'TypeX_3': 'alliance_challenger',
|
||||
'Viper': 'viper',
|
||||
'Viper_MkIV': 'viper_mk_iv',
|
||||
'Vulture': 'vulture'
|
||||
};
|
||||
|
||||
// Mapping from hardpoint class to name in companion API
|
||||
export const HARDPOINT_NUM_TO_CLASS = {
|
||||
0: 'Tiny',
|
||||
1: 'Small',
|
||||
2: 'Medium',
|
||||
3: 'Large',
|
||||
4: 'Huge'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Obtain a module given its ED ID
|
||||
* @param {Integer} edId the Elite ID of the module
|
||||
* @return {Module} the module
|
||||
*/
|
||||
function _moduleFromEdId(edId) {
|
||||
if (!edId) return null;
|
||||
|
||||
// Check standard modules
|
||||
for (const grp in Modules.standard) {
|
||||
if (Modules.standard.hasOwnProperty(grp)) {
|
||||
for (const i in Modules.standard[grp]) {
|
||||
if (Modules.standard[grp][i].edID === edId) {
|
||||
// Found it
|
||||
return new Module({ template: Modules.standard[grp][i] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check hardpoint modules
|
||||
for (const grp in Modules.hardpoints) {
|
||||
if (Modules.hardpoints.hasOwnProperty(grp)) {
|
||||
for (const i in Modules.hardpoints[grp]) {
|
||||
if (Modules.hardpoints[grp][i].edID === edId) {
|
||||
// Found it
|
||||
return new Module({ template: Modules.hardpoints[grp][i] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check internal modules
|
||||
for (const grp in Modules.internal) {
|
||||
if (Modules.internal.hasOwnProperty(grp)) {
|
||||
for (const i in Modules.internal[grp]) {
|
||||
if (Modules.internal[grp][i].edID === edId) {
|
||||
// Found it
|
||||
return new Module({ template: Modules.internal[grp][i] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the model of a ship given its ED name
|
||||
* @param {string} edName the Elite name of the ship
|
||||
* @return {string} the Coriolis model of the ship
|
||||
*/
|
||||
function _shipModelFromEDName(edName) {
|
||||
return SHIP_FD_NAME_TO_CORIOLIS_NAME[Object.keys(SHIP_FD_NAME_TO_CORIOLIS_NAME).find(elem => elem.toLowerCase() === edName.toLowerCase())];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a ship's model from the companion API JSON
|
||||
* @param {object} json the companion API JSON
|
||||
* @return {string} the Coriolis model of the ship
|
||||
*/
|
||||
export function shipModelFromJson(json) {
|
||||
return _shipModelFromEDName(json.name || json.Ship);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a ship from the companion API JSON
|
||||
* @param {object} json the companion API JSON
|
||||
* @return {Ship} the built ship
|
||||
*/
|
||||
export function shipFromJson(json) {
|
||||
// Start off building a basic ship
|
||||
const shipModel = shipModelFromJson(json);
|
||||
if (!shipModel) {
|
||||
throw 'No such ship found: "' + json.name + '"';
|
||||
}
|
||||
const shipTemplate = Ships[shipModel];
|
||||
|
||||
let ship = new Ship(shipModel, shipTemplate.properties, shipTemplate.slots);
|
||||
ship.buildWith(null);
|
||||
|
||||
// Set the cargo hatch
|
||||
if (json.modules.CargoHatch) {
|
||||
ship.cargoHatch.enabled = json.modules.CargoHatch.module.on == true;
|
||||
ship.cargoHatch.priority = json.modules.CargoHatch.module.priority;
|
||||
} else {
|
||||
// We don't have any information on it so guess it's priority 5 and disabled
|
||||
ship.cargoHatch.enabled = false;
|
||||
ship.cargoHatch.priority = 4;
|
||||
}
|
||||
|
||||
let rootModule;
|
||||
|
||||
// Add the bulkheads
|
||||
const armourJson = json.modules.Armour.module;
|
||||
if (armourJson.name.toLowerCase().endsWith('_armour_grade1')) {
|
||||
ship.useBulkhead(0, true);
|
||||
} else if (armourJson.name.toLowerCase().endsWith('_armour_grade2')) {
|
||||
ship.useBulkhead(1, true);
|
||||
} else if (armourJson.name.toLowerCase().endsWith('_armour_grade3')) {
|
||||
ship.useBulkhead(2, true);
|
||||
} else if (armourJson.name.toLowerCase().endsWith('_armour_mirrored')) {
|
||||
ship.useBulkhead(3, true);
|
||||
} else if (armourJson.name.toLowerCase().endsWith('_armour_reactive')) {
|
||||
ship.useBulkhead(4, true);
|
||||
} else {
|
||||
throw 'Unknown bulkheads "' + armourJson.name + '"';
|
||||
}
|
||||
ship.bulkheads.enabled = true;
|
||||
rootModule = json.modules.Armour;
|
||||
if (rootModule.WorkInProgress_modifications) _addModifications(ship.bulkheads.m, rootModule.WorkInProgress_modifications, rootModule.engineer.recipeName, rootModule.engineer.recipeLevel);
|
||||
|
||||
// Add the standard modules
|
||||
// Power plant
|
||||
const powerplantJson = json.modules.PowerPlant.module;
|
||||
const powerplant = _moduleFromEdId(powerplantJson.id);
|
||||
rootModule = json.modules.PowerPlant;
|
||||
if (rootModule.WorkInProgress_modifications) _addModifications(powerplant, rootModule.WorkInProgress_modifications, rootModule.engineer.recipeName, rootModule.engineer.recipeLevel);
|
||||
ship.use(ship.standard[0], powerplant, true);
|
||||
ship.standard[0].enabled = powerplantJson.on === true;
|
||||
ship.standard[0].priority = powerplantJson.priority;
|
||||
|
||||
// Thrusters
|
||||
const thrustersJson = json.modules.MainEngines.module;
|
||||
const thrusters = _moduleFromEdId(thrustersJson.id);
|
||||
rootModule = json.modules.MainEngines;
|
||||
if (rootModule.WorkInProgress_modifications) _addModifications(thrusters, rootModule.WorkInProgress_modifications, rootModule.engineer.recipeName, rootModule.engineer.recipeLevel);
|
||||
ship.use(ship.standard[1], thrusters, true);
|
||||
ship.standard[1].enabled = thrustersJson.on === true;
|
||||
ship.standard[1].priority = thrustersJson.priority;
|
||||
|
||||
// FSD
|
||||
const frameshiftdriveJson = json.modules.FrameShiftDrive.module;
|
||||
const frameshiftdrive = _moduleFromEdId(frameshiftdriveJson.id);
|
||||
rootModule = json.modules.FrameShiftDrive;
|
||||
if (rootModule.WorkInProgress_modifications) _addModifications(frameshiftdrive, rootModule.WorkInProgress_modifications, rootModule.engineer.recipeName, rootModule.engineer.recipeLevel);
|
||||
ship.use(ship.standard[2], frameshiftdrive, true);
|
||||
ship.standard[2].enabled = frameshiftdriveJson.on === true;
|
||||
ship.standard[2].priority = frameshiftdriveJson.priority;
|
||||
|
||||
// Life support
|
||||
const lifesupportJson = json.modules.LifeSupport.module;
|
||||
const lifesupport = _moduleFromEdId(lifesupportJson.id);
|
||||
rootModule = json.modules.LifeSupport;
|
||||
if (rootModule.WorkInProgress_modifications) _addModifications(lifesupport, rootModule.WorkInProgress_modifications, rootModule.engineer.recipeName, rootModule.engineer.recipeLevel);
|
||||
ship.use(ship.standard[3], lifesupport, true);
|
||||
ship.standard[3].enabled = lifesupportJson.on === true;
|
||||
ship.standard[3].priority = lifesupportJson.priority;
|
||||
|
||||
// Power distributor
|
||||
const powerdistributorJson = json.modules.PowerDistributor.module;
|
||||
const powerdistributor = _moduleFromEdId(powerdistributorJson.id);
|
||||
rootModule = json.modules.PowerDistributor;
|
||||
if (rootModule.WorkInProgress_modifications) _addModifications(powerdistributor, rootModule.WorkInProgress_modifications, rootModule.engineer.recipeName, rootModule.engineer.recipeLevel);
|
||||
ship.use(ship.standard[4], powerdistributor, true);
|
||||
ship.standard[4].enabled = powerdistributorJson.on === true;
|
||||
ship.standard[4].priority = powerdistributorJson.priority;
|
||||
|
||||
// Sensors
|
||||
const sensorsJson = json.modules.Radar.module;
|
||||
const sensors = _moduleFromEdId(sensorsJson.id);
|
||||
rootModule = json.modules.Radar;
|
||||
if (rootModule.WorkInProgress_modifications) _addModifications(sensors, rootModule.WorkInProgress_modifications, rootModule.engineer.recipeName, rootModule.engineer.recipeLevel);
|
||||
ship.use(ship.standard[5], sensors, true);
|
||||
ship.standard[5].enabled = sensorsJson.on === true;
|
||||
ship.standard[5].priority = sensorsJson.priority;
|
||||
|
||||
// Fuel tank
|
||||
const fueltankJson = json.modules.FuelTank.module;
|
||||
const fueltank = _moduleFromEdId(fueltankJson.id);
|
||||
ship.use(ship.standard[6], fueltank, true);
|
||||
ship.standard[6].enabled = true;
|
||||
ship.standard[6].priority = 0;
|
||||
|
||||
// Add hardpoints
|
||||
let hardpointClassNum = -1;
|
||||
let hardpointSlotNum = -1;
|
||||
let hardpointArrayNum = 0;
|
||||
for (let i in shipTemplate.slots.hardpoints) {
|
||||
if (shipTemplate.slots.hardpoints[i] === hardpointClassNum) {
|
||||
// Another slot of the same class
|
||||
hardpointSlotNum++;
|
||||
} else {
|
||||
// The first slot of a new class
|
||||
hardpointClassNum = shipTemplate.slots.hardpoints[i];
|
||||
hardpointSlotNum = 1;
|
||||
}
|
||||
|
||||
// Now that we know what we're looking for, find it
|
||||
const hardpointName = HARDPOINT_NUM_TO_CLASS[hardpointClassNum] + 'Hardpoint' + hardpointSlotNum;
|
||||
const hardpointSlot = json.modules[hardpointName];
|
||||
if (!hardpointSlot) {
|
||||
// This can happen with old imports that don't contain new hardpoints
|
||||
} else if (!hardpointSlot.module) {
|
||||
// No module
|
||||
} else {
|
||||
const hardpointJson = hardpointSlot.module;
|
||||
const hardpoint = _moduleFromEdId(hardpointJson.id);
|
||||
rootModule = hardpointSlot;
|
||||
if (rootModule.WorkInProgress_modifications) _addModifications(hardpoint, rootModule.WorkInProgress_modifications, rootModule.engineer.recipeName, rootModule.engineer.recipeLevel, rootModule.specialModifications);
|
||||
ship.use(ship.hardpoints[hardpointArrayNum], hardpoint, true);
|
||||
ship.hardpoints[hardpointArrayNum].enabled = hardpointJson.on === true;
|
||||
ship.hardpoints[hardpointArrayNum].priority = hardpointJson.priority;
|
||||
}
|
||||
hardpointArrayNum++;
|
||||
}
|
||||
|
||||
// Add internal compartments
|
||||
let internalSlotNum = 1;
|
||||
let militarySlotNum = 1;
|
||||
for (let i in shipTemplate.slots.internal) {
|
||||
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;
|
||||
if (isMilitary) {
|
||||
const internalName = 'Military0' + militarySlotNum;
|
||||
internalSlot = json.modules[internalName];
|
||||
militarySlotNum++;
|
||||
} else {
|
||||
// Slot numbers are not contiguous so handle skips.
|
||||
while (internalSlot === null && internalSlotNum < 99) {
|
||||
// Slot sizes have no relationship to the actual size, either, so check all possibilities
|
||||
for (let slotsize = 0; slotsize < 9; slotsize++) {
|
||||
const internalName = 'Slot' + (internalSlotNum <= 9 ? '0' : '') + internalSlotNum + '_Size' + slotsize;
|
||||
if (json.modules[internalName]) {
|
||||
internalSlot = json.modules[internalName];
|
||||
break;
|
||||
}
|
||||
}
|
||||
internalSlotNum++;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
const internal = _moduleFromEdId(internalJson.id);
|
||||
rootModule = internalSlot;
|
||||
if (rootModule.WorkInProgress_modifications) _addModifications(internal, rootModule.WorkInProgress_modifications, rootModule.engineer.recipeName, rootModule.engineer.recipeLevel);
|
||||
ship.use(ship.internal[i], internal, true);
|
||||
ship.internal[i].enabled = internalJson.on === true;
|
||||
ship.internal[i].priority = internalJson.priority;
|
||||
}
|
||||
}
|
||||
|
||||
// Now update the ship's codes before returning it
|
||||
return ship.updatePowerPrioritesString().updatePowerEnabledString().updateModificationsString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the modifications for a module
|
||||
* @param {Module} module the module
|
||||
* @param {Object} modifiers the modifiers
|
||||
* @param {Object} blueprint the blueprint of the modification
|
||||
* @param {Object} grade the grade of the modification
|
||||
* @param {Object} specialModifications special modification
|
||||
*/
|
||||
function _addModifications(module, modifiers, blueprint, grade, specialModifications) {
|
||||
if (!modifiers) return;
|
||||
let special;
|
||||
if (specialModifications) {
|
||||
special = Modifications.specials[Object.keys(specialModifications)[0]];
|
||||
}
|
||||
for (const i in modifiers) {
|
||||
// Some special modifications
|
||||
if (modifiers[i].name === 'mod_weapon_clip_size_override') {
|
||||
// This is a numeric addition to the clip size, but we need to work it out in terms of being a percentage so
|
||||
// that it works the same as other modifications
|
||||
const origClip = module.clip || 1;
|
||||
module.setModValue('clip', ((modifiers[i].value - origClip) / origClip) * 10000);
|
||||
} else if (modifiers[i].name === 'mod_weapon_burst_size') {
|
||||
// This is an absolute number that acts as an override
|
||||
module.setModValue('burst', modifiers[i].value * 100);
|
||||
} else if (modifiers[i].name === 'mod_weapon_burst_rof') {
|
||||
// This is an absolute number that acts as an override
|
||||
module.setModValue('burstrof', modifiers[i].value * 100);
|
||||
} else if (modifiers[i].name === 'mod_weapon_falloffrange_from_range') {
|
||||
// Obtain the falloff value directly from the range
|
||||
module.setModValue('fallofffromrange', 1);
|
||||
} else if (modifiers[i].name && modifiers[i].name.startsWith('special_')) {
|
||||
// We don't add special effects directly, but keep a note of them so they can be added when fetching values
|
||||
special = Modifications.specials[modifiers[i].name];
|
||||
} else {
|
||||
// Look up the modifiers to find what we need to do
|
||||
const modifierActions = Modifications.modifierActions[i];
|
||||
let value;
|
||||
if (i === 'OutfittingFieldType_DefenceModifierShieldMultiplier') {
|
||||
value = modifiers[i].value - 1;
|
||||
} else if (i === 'OutfittingFieldType_DefenceModifierHealthMultiplier' && blueprint.startsWith('Armour_')) {
|
||||
value = (modifiers[i].value - module.hullboost) / module.hullboost;
|
||||
} else if (i === 'OutfittingFieldType_DefenceModifierHealthMultiplier') {
|
||||
value = modifiers[i].value / module.hullboost;
|
||||
} else if (i === 'OutfittingFieldType_RateOfFire') {
|
||||
value = (1 / Math.abs(modifiers[i].value));
|
||||
} else {
|
||||
value = modifiers[i].value - 1;
|
||||
}
|
||||
// Carry out the required changes
|
||||
for (const action in modifierActions) {
|
||||
if (isNaN(modifierActions[action])) {
|
||||
module.setModValue(action, modifierActions[action]);
|
||||
} else {
|
||||
const actionValue = modifierActions[action] * value;
|
||||
let mod = module.getModValue(action) / 10000;
|
||||
if (!mod) {
|
||||
mod = 0;
|
||||
}
|
||||
module.setModValue(action, ((1 + mod) * (1 + actionValue) - 1) * 10000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the blueprint definition, grade and special
|
||||
if (blueprint) {
|
||||
module.blueprint = getBlueprint(blueprint, module);
|
||||
if (grade) {
|
||||
module.blueprint.grade = Number(grade);
|
||||
}
|
||||
if (special) {
|
||||
module.blueprint.special = special;
|
||||
}
|
||||
}
|
||||
|
||||
// Need to fix up a few items
|
||||
|
||||
// Shield boosters are treated internally as straight modifiers, so rather than (for example)
|
||||
// being a 4% boost they are a 104% multiplier. Unfortunately this means that our % modification
|
||||
// is incorrect so we fix it
|
||||
if (module.grp === 'sb' && module.getModValue('shieldboost')) {
|
||||
const alteredBoost = (1 + module.shieldboost) * (module.getModValue('shieldboost') / 10000);
|
||||
module.setModValue('shieldboost', alteredBoost * 10000 / module.shieldboost);
|
||||
}
|
||||
|
||||
// Shield booster resistance is actually a damage modifier, so needs to be inverted.
|
||||
if (module.grp === 'sb') {
|
||||
if (module.getModValue('explres')) {
|
||||
module.setModValue('explres', ((module.getModValue('explres') / 10000) * -1) * 10000);
|
||||
}
|
||||
if (module.getModValue('kinres')) {
|
||||
module.setModValue('kinres', ((module.getModValue('kinres') / 10000) * -1) * 10000);
|
||||
}
|
||||
if (module.getModValue('thermres')) {
|
||||
module.setModValue('thermres', ((module.getModValue('thermres') / 10000) * -1) * 10000);
|
||||
}
|
||||
}
|
||||
|
||||
// Shield generator resistance is actually a damage modifier, so needs to be inverted.
|
||||
// In addition, the modification is based off the inherent resistance of the module
|
||||
if (ModuleUtils.isShieldGenerator(module.grp)) {
|
||||
if (module.getModValue('explres')) {
|
||||
module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000);
|
||||
}
|
||||
if (module.getModValue('kinres')) {
|
||||
module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000);
|
||||
}
|
||||
if (module.getModValue('thermres')) {
|
||||
module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000);
|
||||
}
|
||||
}
|
||||
|
||||
// Hull reinforcement package resistance is actually a damage modifier, so needs to be inverted.
|
||||
// In addition, the modification is based off the inherent resistance of the module
|
||||
if (module.grp === 'hr') {
|
||||
if (module.getModValue('explres')) {
|
||||
module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000);
|
||||
}
|
||||
if (module.getModValue('kinres')) {
|
||||
module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000);
|
||||
}
|
||||
if (module.getModValue('thermres')) {
|
||||
module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000);
|
||||
}
|
||||
}
|
||||
|
||||
// Bulkhead resistance is actually a damage modifier, so needs to be inverted.
|
||||
// In addition, the modification is based off the inherent resistance of the module
|
||||
if (module.grp == 'bh') {
|
||||
if (module.getModValue('explres')) {
|
||||
module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000);
|
||||
}
|
||||
if (module.getModValue('kinres')) {
|
||||
module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000);
|
||||
}
|
||||
if (module.getModValue('thermres')) {
|
||||
module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000);
|
||||
}
|
||||
}
|
||||
|
||||
// Bulkhead boost is based off the inherent boost of the module
|
||||
if (module.grp == 'bh') {
|
||||
const alteredBoost = (1 + module.hullboost) * (1 + module.getModValue('hullboost') / 10000) - 1;
|
||||
module.setModValue('hullboost', (alteredBoost / module.hullboost - 1) * 1000);
|
||||
}
|
||||
|
||||
// Jitter is an absolute number, so we need to divide it by 100
|
||||
if (module.getModValue('jitter')) {
|
||||
module.setModValue('jitter', module.getModValue('jitter') / 100);
|
||||
}
|
||||
|
||||
// Clip size is rounded up so that the result is a whole number
|
||||
if (module.getModValue('clip')) {
|
||||
const individual = 1 / (module.clip || 1);
|
||||
module.setModValue('clip', Math.ceil((module.getModValue('clip') / 10000) / individual) * individual * 10000);
|
||||
}
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
import Ship from '../shipyard/Ship';
|
||||
import { HARDPOINT_NUM_TO_CLASS, shipModelFromJson } from './CompanionApiUtils';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import Module from '../shipyard/Module';
|
||||
import { Modules } from 'coriolis-data/dist';
|
||||
import { Modifications } from 'coriolis-data/dist';
|
||||
import { getBlueprint, setQualityCB } from './BlueprintFunctions';
|
||||
|
||||
/**
|
||||
* Obtain a module given its FD Name
|
||||
* @param {string} fdname the FD Name of the module
|
||||
* @return {Module} the module
|
||||
*/
|
||||
function _moduleFromFdName(fdname) {
|
||||
if (!fdname) return null;
|
||||
fdname = fdname.toLowerCase();
|
||||
// Check standard modules
|
||||
for (const grp in Modules.standard) {
|
||||
if (Modules.standard.hasOwnProperty(grp)) {
|
||||
for (const i in Modules.standard[grp]) {
|
||||
if (Modules.standard[grp][i].symbol && Modules.standard[grp][i].symbol.toLowerCase() === fdname) {
|
||||
// Found it
|
||||
return new Module({ template: Modules.standard[grp][i] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check hardpoint modules
|
||||
for (const grp in Modules.hardpoints) {
|
||||
if (Modules.hardpoints.hasOwnProperty(grp)) {
|
||||
for (const i in Modules.hardpoints[grp]) {
|
||||
if (Modules.hardpoints[grp][i].symbol && Modules.hardpoints[grp][i].symbol.toLowerCase() === fdname) {
|
||||
// Found it
|
||||
return new Module({ template: Modules.hardpoints[grp][i] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check internal modules
|
||||
for (const grp in Modules.internal) {
|
||||
if (Modules.internal.hasOwnProperty(grp)) {
|
||||
for (const i in Modules.internal[grp]) {
|
||||
if (Modules.internal[grp][i].symbol && Modules.internal[grp][i].symbol.toLowerCase() === fdname) {
|
||||
// Found it
|
||||
return new Module({ template: Modules.internal[grp][i] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a ship from the journal Loadout event JSON
|
||||
* @param {object} json the Loadout event JSON
|
||||
* @return {Ship} the built ship
|
||||
*/
|
||||
export function shipFromLoadoutJSON(json) {
|
||||
// Start off building a basic ship
|
||||
const shipModel = shipModelFromJson(json);
|
||||
if (!shipModel) {
|
||||
throw 'No such ship found: "' + json.Ship + '"';
|
||||
}
|
||||
const shipTemplate = Ships[shipModel];
|
||||
|
||||
let ship = new Ship(shipModel, shipTemplate.properties, shipTemplate.slots);
|
||||
ship.buildWith(null);
|
||||
// Initial Ship building, don't do engineering yet.
|
||||
let modsToAdd = [];
|
||||
|
||||
for (const module of json.Modules) {
|
||||
switch (module.Slot.toLowerCase()) {
|
||||
// Cargo Hatch.
|
||||
case 'cargohatch':
|
||||
ship.cargoHatch.enabled = module.On;
|
||||
ship.cargoHatch.priority = module.Priority;
|
||||
break;
|
||||
// Add the bulkheads
|
||||
case 'armour':
|
||||
if (module.Item.toLowerCase().endsWith('_armour_grade1')) {
|
||||
ship.useBulkhead(0, true);
|
||||
} else if (module.Item.toLowerCase().endsWith('_armour_grade2')) {
|
||||
ship.useBulkhead(1, true);
|
||||
} else if (module.Item.toLowerCase().endsWith('_armour_grade3')) {
|
||||
ship.useBulkhead(2, true);
|
||||
} else if (module.Item.toLowerCase().endsWith('_armour_mirrored')) {
|
||||
ship.useBulkhead(3, true);
|
||||
} else if (module.Item.toLowerCase().endsWith('_armour_reactive')) {
|
||||
ship.useBulkhead(4, true);
|
||||
} else {
|
||||
throw 'Unknown bulkheads "' + module.Item + '"';
|
||||
}
|
||||
ship.bulkheads.enabled = true;
|
||||
if (module.Engineering) _addModifications(ship.bulkheads.m, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect);
|
||||
break;
|
||||
case 'powerplant':
|
||||
const powerplant = _moduleFromFdName(module.Item);
|
||||
ship.use(ship.standard[0], powerplant, true);
|
||||
ship.standard[0].enabled = module.On;
|
||||
ship.standard[0].priority = module.Priority;
|
||||
if (module.Engineering) _addModifications(powerplant, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect);
|
||||
break;
|
||||
case 'mainengines':
|
||||
const thrusters = _moduleFromFdName(module.Item);
|
||||
ship.use(ship.standard[1], thrusters, true);
|
||||
ship.standard[1].enabled = module.On;
|
||||
ship.standard[1].priority = module.Priority;
|
||||
if (module.Engineering) _addModifications(thrusters, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect);
|
||||
break;
|
||||
case 'frameshiftdrive':
|
||||
const frameshiftdrive = _moduleFromFdName(module.Item);
|
||||
ship.use(ship.standard[2], frameshiftdrive, true);
|
||||
ship.standard[2].enabled = module.On;
|
||||
ship.standard[2].priority = module.Priority;
|
||||
if (module.Engineering) _addModifications(frameshiftdrive, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect);
|
||||
break;
|
||||
case 'lifesupport':
|
||||
const lifesupport = _moduleFromFdName(module.Item);
|
||||
ship.use(ship.standard[3], lifesupport, true);
|
||||
ship.standard[3].enabled = module.On === true;
|
||||
ship.standard[3].priority = module.Priority;
|
||||
if (module.Engineering) _addModifications(lifesupport, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect);
|
||||
break;
|
||||
case 'powerdistributor':
|
||||
const powerdistributor = _moduleFromFdName(module.Item);
|
||||
ship.use(ship.standard[4], powerdistributor, true);
|
||||
ship.standard[4].enabled = module.On;
|
||||
ship.standard[4].priority = module.Priority;
|
||||
if (module.Engineering) _addModifications(powerdistributor, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect);
|
||||
break;
|
||||
case 'radar':
|
||||
const sensors = _moduleFromFdName(module.Item);
|
||||
ship.use(ship.standard[5], sensors, true);
|
||||
ship.standard[5].enabled = module.On;
|
||||
ship.standard[5].priority = module.Priority;
|
||||
if (module.Engineering) _addModifications(sensors, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect);
|
||||
break;
|
||||
case 'fueltank':
|
||||
const fueltank = _moduleFromFdName(module.Item);
|
||||
ship.use(ship.standard[6], fueltank, true);
|
||||
ship.standard[6].enabled = true;
|
||||
ship.standard[6].priority = 0;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
if (module.Slot.toLowerCase().search(/hardpoint/) !== -1) {
|
||||
// Add hardpoints
|
||||
let hardpoint;
|
||||
let hardpointClassNum = -1;
|
||||
let hardpointSlotNum = -1;
|
||||
let hardpointArrayNum = 0;
|
||||
for (let i in shipTemplate.slots.hardpoints) {
|
||||
if (shipTemplate.slots.hardpoints[i] === hardpointClassNum) {
|
||||
// Another slot of the same class
|
||||
hardpointSlotNum++;
|
||||
} else {
|
||||
// The first slot of a new class
|
||||
hardpointClassNum = shipTemplate.slots.hardpoints[i];
|
||||
hardpointSlotNum = 1;
|
||||
}
|
||||
|
||||
// Now that we know what we're looking for, find it
|
||||
const hardpointName = HARDPOINT_NUM_TO_CLASS[hardpointClassNum] + 'Hardpoint' + hardpointSlotNum;
|
||||
const hardpointSlot = json.Modules.find(elem => elem.Slot.toLowerCase() === hardpointName.toLowerCase());
|
||||
if (!hardpointSlot) {
|
||||
// This can happen with old imports that don't contain new hardpoints
|
||||
} else {
|
||||
hardpoint = _moduleFromFdName(hardpointSlot.Item);
|
||||
ship.use(ship.hardpoints[hardpointArrayNum], hardpoint, true);
|
||||
ship.hardpoints[hardpointArrayNum].enabled = hardpointSlot.On;
|
||||
ship.hardpoints[hardpointArrayNum].priority = hardpointSlot.Priority;
|
||||
modsToAdd.push({ coriolisMod: hardpoint, json: hardpointSlot });
|
||||
}
|
||||
hardpointArrayNum++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let internalSlotNum = 0;
|
||||
let militarySlotNum = 1;
|
||||
for (let i in shipTemplate.slots.internal) {
|
||||
if (!shipTemplate.slots.internal.hasOwnProperty(i)) {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
if (isMilitary) {
|
||||
const internalName = 'Military0' + militarySlotNum;
|
||||
internalSlot = json.Modules.find(elem => elem.Slot.toLowerCase() === internalName.toLowerCase());
|
||||
militarySlotNum++;
|
||||
} else {
|
||||
// Slot numbers are not contiguous so handle skips.
|
||||
for (; internalSlot === null && internalSlotNum < 99; internalSlotNum++) {
|
||||
// Slot sizes have no relationship to the actual size, either, so check all possibilities
|
||||
for (let slotsize = 0; slotsize < 9; slotsize++) {
|
||||
const internalName = 'Slot' + (internalSlotNum <= 9 ? '0' : '') + internalSlotNum + '_Size' + slotsize;
|
||||
if (json.Modules.find(elem => elem.Slot.toLowerCase() === internalName.toLowerCase())) {
|
||||
internalSlot = json.Modules.find(elem => elem.Slot.toLowerCase() === internalName.toLowerCase());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!internalSlot) {
|
||||
// This can happen with old imports that don't contain new slots
|
||||
} else {
|
||||
const internalJson = internalSlot;
|
||||
const internal = _moduleFromFdName(internalJson.Item);
|
||||
ship.use(ship.internal[i], internal, true);
|
||||
ship.internal[i].enabled = internalJson.On === true;
|
||||
ship.internal[i].priority = internalJson.Priority;
|
||||
modsToAdd.push({ coriolisMod: internal, json: internalSlot });
|
||||
}
|
||||
}
|
||||
|
||||
for (const i of modsToAdd) {
|
||||
if (i.json.Engineering) {
|
||||
_addModifications(i.coriolisMod, i.json.Engineering.Modifiers, i.json.Engineering.Quality, i.json.Engineering.BlueprintName, i.json.Engineering.Level, i.json.Engineering.ExperimentalEffect);
|
||||
}
|
||||
}
|
||||
// We don't have any information on it so guess it's priority 5 and disabled
|
||||
if (!ship.cargoHatch) {
|
||||
ship.cargoHatch.enabled = false;
|
||||
ship.cargoHatch.priority = 4;
|
||||
}
|
||||
|
||||
// Now update the ship's codes before returning it
|
||||
return ship.updatePowerPrioritesString().updatePowerEnabledString().updateModificationsString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the modifications for a module
|
||||
* @param {Module} module the module
|
||||
* @param {Object} modifiers the modifiers
|
||||
* @param {float} quality quality of the modifiers 0 to 1
|
||||
* @param {Object} blueprint the blueprint of the modification
|
||||
* @param {Object} grade the grade of the modification
|
||||
* @param {Object} specialModifications special modification
|
||||
*/
|
||||
function _addModifications(module, modifiers, quality, blueprint, grade, specialModifications) {
|
||||
if (!modifiers && !quality) return;
|
||||
let special;
|
||||
if (specialModifications) {
|
||||
if (specialModifications == 'special_plasma_slug') {
|
||||
if (module.symbol.match(/PlasmaAccelerator/i)) {
|
||||
specialModifications = 'special_plasma_slug_pa';
|
||||
} else {
|
||||
specialModifications = 'special_plasma_slug_cooled';
|
||||
}
|
||||
}
|
||||
special = Modifications.specials[specialModifications];
|
||||
}
|
||||
// Add the blueprint definition, grade and special
|
||||
if (blueprint) {
|
||||
module.blueprint = getBlueprint(blueprint, module);
|
||||
if (grade) {
|
||||
module.blueprint.grade = Number(grade);
|
||||
}
|
||||
if (special) {
|
||||
module.blueprint.special = special;
|
||||
}
|
||||
}
|
||||
if (modifiers) {
|
||||
for (const i in modifiers) {
|
||||
// Some special modifications
|
||||
// Look up the modifiers to find what we need to do
|
||||
const findMod = val => Object.keys(Modifications.modifierActions).find(elem => elem.toString().toLowerCase().replace(/(outfittingfieldtype_|persecond)/igm, '') === val.toString().toLowerCase().replace(/(outfittingfieldtype_|persecond)/igm, ''));
|
||||
const modifierActions = Modifications.modifierActions[findMod(modifiers[i].Label)];
|
||||
// TODO: Figure out how to scale this value.
|
||||
if (!!modifiers[i].LessIsGood) {
|
||||
|
||||
}
|
||||
let value = (modifiers[i].Value / modifiers[i].OriginalValue * 100 - 100) * 100;
|
||||
if (value === Infinity) {
|
||||
value = modifiers[i].Value * 100;
|
||||
}
|
||||
if (modifiers[i].Label.search('DamageFalloffRange') >= 0) {
|
||||
value = (modifiers[i].Value / module.range - 1) * 100;
|
||||
}
|
||||
if (modifiers[i].Label.search('Resistance') >= 0) {
|
||||
value = (modifiers[i].Value * 100) - (modifiers[i].OriginalValue * 100);
|
||||
}
|
||||
if (modifiers[i].Label.search('ShieldMultiplier') >= 0 || modifiers[i].Label.search('DefenceModifierHealthMultiplier') >= 0) {
|
||||
value = ((100 + modifiers[i].Value) / (100 + modifiers[i].OriginalValue) * 100 - 100) * 100;
|
||||
}
|
||||
|
||||
// Carry out the required changes
|
||||
for (const action in modifierActions) {
|
||||
if (isNaN(modifierActions[action])) {
|
||||
module.setModValue(action, modifierActions[action]);
|
||||
} else {
|
||||
module.setModValue(action, value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (quality) {
|
||||
setQualityCB(module.blueprint, quality, (featureName, value) => module.setModValue(featureName, value, false));
|
||||
}
|
||||
}
|
||||
@@ -20,60 +20,6 @@ export default function shorternUrl(url, success, error) {
|
||||
orbisShorten(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
|
||||
*/
|
||||
function shortenUrlGoogle(url, success, error) {
|
||||
if (window.navigator.onLine) {
|
||||
try {
|
||||
request.post(SHORTEN_API_GOOGLE + window.CORIOLIS_GAPI_KEY)
|
||||
.send({ longUrl: url })
|
||||
.end(function(err, response) {
|
||||
if (err) {
|
||||
error(response.statusText == 'OK' ? 'Bad Request' : response.statusText);
|
||||
} else {
|
||||
success(response.body.id);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
error(e.message ? e.message : e);
|
||||
}
|
||||
} else {
|
||||
error('Not Online');
|
||||
}
|
||||
}
|
||||
|
||||
const SHORTEN_API_EDDP = 'https://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');
|
||||
}
|
||||
}
|
||||
|
||||
const SHORTEN_API_ORBIS = 'https://s.orbis.zone/api.php';
|
||||
/**
|
||||
* Shorten a URL using Orbis's URL shortener API
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
import React from 'react';
|
||||
import Persist from '../stores/Persist';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import Module from '../shipyard/Module';
|
||||
|
||||
/**
|
||||
* Determine if a slot on a ship can mount a module of a particular class and group
|
||||
* @param {Object} ship Ship object
|
||||
* @param {Object} slot Slot object
|
||||
* @param {String} group Module group/type abbrivation/code
|
||||
* @param {Integer} clazz [Optional] Module Class/Size
|
||||
* @return {Boolean} True if the slot can mount the module
|
||||
*/
|
||||
export function canMount(ship, slot, group, clazz) {
|
||||
if (slot &&
|
||||
(!slot.eligible || slot.eligible[group]) &&
|
||||
(group != 'pcq' || (ship.luxuryCabins && ship.luxuryCabins === true)) &&
|
||||
(group != 'fh' || (ship.fighterHangars && ship.fighterHangars === true)) &&
|
||||
(clazz === undefined || slot.maxClass >= clazz)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translate name for the module mounted in the specified
|
||||
* slot.
|
||||
* @param {function} translate Translation function
|
||||
* @param {object} slot Slot object
|
||||
* @return {string} The translated name
|
||||
*/
|
||||
export function slotName(translate, slot) {
|
||||
return slot.m ? translate(slot.m.name || slot.m.grp) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Slot name comparator
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Object} a Slot object
|
||||
* @param {Object} b Slot object
|
||||
* @return {Number} 1, 0, -1
|
||||
*/
|
||||
export function nameComparator(translate, a, b) {
|
||||
return translate(a.name || a.grp).localeCompare(translate(b.name || b.grp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an internationalization friendly slot comparator that will
|
||||
* sort by specified property (if provided) then by name/group, class, rating
|
||||
* @param {function} translate Tranlation function
|
||||
* @param {function} propComparator Optional property comparator
|
||||
* @param {boolean} desc Use descending order
|
||||
* @return {function} Comparator function for slot names
|
||||
*/
|
||||
export function slotComparator(translate, propComparator, desc) {
|
||||
return (a, b) => {
|
||||
// retain descending order when sorting sorting by name/group/class/rating
|
||||
let am = a.m; // Slot A's mounted module
|
||||
let bm = b.m; // Slot B's mounted module
|
||||
|
||||
if (!desc) { // Flip A and B if ascending order
|
||||
let t = a;
|
||||
a = b;
|
||||
b = t;
|
||||
}
|
||||
|
||||
// Check for empty slots first
|
||||
if (a.m && !b.m) {
|
||||
return 1;
|
||||
} else if (!a.m && b.m) {
|
||||
return -1;
|
||||
} else if (!a.m && !b.m) {
|
||||
return 0;
|
||||
}
|
||||
// If a property comparator is provided use it first
|
||||
let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a.m, b.m);
|
||||
|
||||
if (diff) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
// Property matches so sort by name / group, then class, rating
|
||||
if (am.name === bm.name && am.grp === bm.grp) {
|
||||
if(am.class == bm.class) {
|
||||
return am.rating > bm.rating ? 1 : -1;
|
||||
}
|
||||
return am.class - bm.class;
|
||||
}
|
||||
|
||||
return nameComparator(translate, am, bm);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the appropriate class based on diff value
|
||||
* @param {Number} a Potential Module (cannot be null)
|
||||
* @param {Number} b Currently mounted module (optional - null)
|
||||
* @param {Boolean} negative A positive diff has a negative implication
|
||||
* @return {String} CSS Class name
|
||||
*/
|
||||
function diffClass(a, b, negative) {
|
||||
if (b === undefined || a === b) {
|
||||
return 'muted';
|
||||
} else if (a > b) {
|
||||
return negative ? 'warning' : 'secondary';
|
||||
}
|
||||
return negative ? 'secondary' : 'warning';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the displayable diff of a module's proprty
|
||||
* @param {Function} format Formatter
|
||||
* @param {Number} mVal Potential Module property value
|
||||
* @param {Number} mmVal Currently mounted module property value
|
||||
* @return {string | React.Component} Component to be rendered
|
||||
*/
|
||||
function diff(format, mVal, mmVal) {
|
||||
if (mVal == Infinity) {
|
||||
return '∞';
|
||||
} else {
|
||||
let diff = mVal - mmVal;
|
||||
if (!diff || mVal === undefined || diff == mVal || Math.abs(diff) == Infinity) {
|
||||
return format(mVal);
|
||||
}
|
||||
return `${format(mVal)} (${diff > 0 ? '+' : ''}${format(diff)})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a a summary and diff of the potential module
|
||||
* versus the currently mounted module if there is one. Must be bound
|
||||
* to a ship instance
|
||||
*
|
||||
* @this {Ship}
|
||||
* @param {Object} language Current language
|
||||
* @param {Object} m Potential Module (cannot be null)
|
||||
* @param {Object} mm Currently mounted module (optional - null if empty)
|
||||
* @return {React.Component} Component to be rendered
|
||||
*/
|
||||
export function diffDetails(language, m, mm) {
|
||||
let { formats, translate, units } = language;
|
||||
let propDiffs = [];
|
||||
m = new Module(m);
|
||||
|
||||
// Module-specific items
|
||||
|
||||
if (m.grp === 'pp') {
|
||||
let mPowerGeneration = m.getPowerGeneration() || 0;
|
||||
let mmPowerGeneration = mm ? mm.getPowerGeneration() : 0;
|
||||
if (mPowerGeneration != mmPowerGeneration) propDiffs.push(<div key='pgen'>{translate('pgen')}: <span className={diffClass(mPowerGeneration, mmPowerGeneration)}>{diff(formats.round, mPowerGeneration, mmPowerGeneration)}{units.MW}</span></div>);
|
||||
} else {
|
||||
let mPowerUsage = m.getPowerUsage() || 0;
|
||||
let mmPowerUsage = mm ? mm.getPowerUsage() || 0 : 0;
|
||||
if (mPowerUsage != mmPowerUsage) propDiffs.push(<div key='power'>{translate('power')}: <span className={diffClass(mPowerUsage, mmPowerUsage, true)}>{diff(formats.round, mPowerUsage, mmPowerUsage)}{units.MW}</span></div>);
|
||||
}
|
||||
|
||||
let mDps = m.getDps() || 0;
|
||||
let mmDps = mm ? mm.getDps() || 0 : 0;
|
||||
if (mDps && mDps != mmDps) propDiffs.push(<div key='dps'>{translate('dps')}: <span className={diffClass(mmDps, mDps, true)}>{diff(formats.round, mDps, mmDps)}</span></div>);
|
||||
|
||||
let mAffectsShield = ModuleUtils.isShieldGenerator(m.grp) || m.grp == 'sb';
|
||||
let mmAffectsShield = mm ? ModuleUtils.isShieldGenerator(m.grp) || mm.grp == 'sb' : false;
|
||||
if (mAffectsShield || mmAffectsShield) {
|
||||
let shield = this.calcShieldStrengthWith(); // Get shield strength regardless of slot active / inactive
|
||||
let newShield = 0;
|
||||
|
||||
if (mAffectsShield) {
|
||||
if (m.grp == 'sb') { // Both m and mm must be utility modules if this is true
|
||||
newShield = this.calcShieldStrengthWith(null, m.getShieldBoost() - (mm ? mm.getShieldBoost() || 0 : 0));
|
||||
} else {
|
||||
newShield = this.calcShieldStrengthWith(m);
|
||||
}
|
||||
} else {
|
||||
// Old module must be a shield booster
|
||||
newShield = this.calcShieldStrengthWith(null, -mm.getShieldBoost());
|
||||
}
|
||||
|
||||
let sgDiffClass = Math.round((newShield - shield) * 100) / 100 == 0 ? 'muted' : (newShield > shield ? 'secondary' : 'warning');
|
||||
|
||||
propDiffs.push(<div key='shields'>{translate('shields')}: <span className={sgDiffClass}>{diff(formats.int, newShield, shield)}{units.MJ}</span></div>);
|
||||
}
|
||||
|
||||
if (m.grp === 'mrp') {
|
||||
let mProtection = m.getProtection();
|
||||
let mmProtection = mm ? mm.getProtection() || 0 : 0;
|
||||
if (mProtection != mmProtection) {
|
||||
propDiffs.push(<div key='protection'>{translate('protection')}: <span className={diffClass(mmProtection, mProtection, true)}>{diff(formats.pct, mProtection, mmProtection)}</span></div>);
|
||||
}
|
||||
}
|
||||
|
||||
if (m.grp === 'hr') {
|
||||
let mHullReinforcement = m.getHullReinforcement();
|
||||
let mmHullReinforcement = mm ? mm.getHullReinforcement() || 0 : 0;
|
||||
if (mHullReinforcement && mHullReinforcement != mmHullReinforcement) propDiffs.push(<div key='hullreinforcement'>{translate('hullreinforcement')}: <span className={diffClass(mmHullReinforcement, mHullReinforcement, true)}>{diff(formats.round, mHullReinforcement, mmHullReinforcement)}</span></div>);
|
||||
}
|
||||
|
||||
if (m.grp == 'pd') {
|
||||
propDiffs.push(<div key='wep'>
|
||||
{`${translate('WEP')}: `}
|
||||
<span className={diffClass(m.wepcap, mm.getWeaponsCapacity())}>{m.wepcap}{units.MJ}</span>
|
||||
{' / '}
|
||||
<span className={diffClass(m.weprate, mm.getWeaponsRechargeRate())}>{m.weprate}{units.MW}</span>
|
||||
</div>);
|
||||
propDiffs.push(<div key='sys'>
|
||||
{`${translate('SYS')}: `}
|
||||
<span className={diffClass(m.syscap, mm.getSystemsCapacity())}>{m.syscap}{units.MJ}</span>
|
||||
{' / '}
|
||||
<span className={diffClass(m.sysrate, mm.getSystemsRechargeRate())}>{m.sysrate}{units.MW}</span>
|
||||
</div>);
|
||||
propDiffs.push(<div key='eng'>
|
||||
{`${translate('ENG')}: `}
|
||||
<span className={diffClass(m.engcap, mm.getEnginesCapacity())}>{m.engcap}{units.MJ}</span>
|
||||
{' / '}
|
||||
<span className={diffClass(m.engrate, mm.getEnginesRechargeRate())}>{m.engrate}{units.MW}</span>
|
||||
</div>);
|
||||
}
|
||||
|
||||
// Common items
|
||||
|
||||
let mCost = m.cost || 0;
|
||||
let mmCost = mm ? mm.cost : 0;
|
||||
if (mCost != mmCost) propDiffs.push(<div key='cost'>{translate('cost')}: <span className={diffClass(mCost, mmCost, true) }>{formats.int(mCost ? Math.round(mCost * (1 - Persist.getModuleDiscount())) : 0)}{units.CR}</span></div>);
|
||||
|
||||
let mMass = m.getMass() || 0;
|
||||
let mmMass = mm ? mm.getMass() : 0;
|
||||
if (mMass != mmMass) propDiffs.push(<div key='mass'>{translate('mass')}: <span className={diffClass(mMass, mmMass, true)}>{diff(formats.round, mMass, mmMass)}{units.T}</span></div>);
|
||||
|
||||
let massDiff = mMass - mmMass;
|
||||
let mCap = m.fuel || m.cargo || 0;
|
||||
let mmCap = mm ? mm.fuel || mm.cargo || 0 : 0;
|
||||
let capDiff = mCap - mmCap;
|
||||
if (m.grp == 'fsd' || massDiff || capDiff) {
|
||||
let fsd = m.grp == 'fsd' ? m : null;
|
||||
let maxRange = this.calcUnladenRange(massDiff, m.fuel, fsd);
|
||||
let ladenRange = this.calcLadenRange(massDiff + capDiff, m.fuel, fsd);
|
||||
|
||||
if (maxRange != this.unladenRange) {
|
||||
propDiffs.push(<div key='maxRange'>{translate('max')} {translate('jump range')}: <span className={maxRange > this.unladenRange ? 'secondary' : 'warning'}>{formats.round(maxRange)}{units.LY}</span></div>);
|
||||
}
|
||||
if (ladenRange != this.ladenRange) {
|
||||
propDiffs.push(<div key='unladenRange'>{translate('laden')} {translate('jump range')}: <span className={ladenRange > this.ladenRange ? 'secondary' : 'warning'}>{formats.round(ladenRange)}{units.LY}</span></div>);
|
||||
}
|
||||
}
|
||||
|
||||
let mIntegrity = m.getIntegrity() || 0;
|
||||
let mmIntegrity = mm ? mm.getIntegrity() || 0 : 0;
|
||||
if (mIntegrity != mmIntegrity) {
|
||||
propDiffs.push(<div key='integrity'>{translate('integrity')}: <span className={diffClass(mmIntegrity, mIntegrity, true)}>{diff(formats.round, mIntegrity, mmIntegrity)}</span></div>);
|
||||
}
|
||||
|
||||
return propDiffs.length > 0 ? <div className='cap' style={{ whiteSpace: 'nowrap' }}>{propDiffs}</div> : null;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Module } from 'ed-forge';
|
||||
|
||||
/**
|
||||
* Wraps the callback/context menu handler such that the default
|
||||
@@ -83,3 +84,18 @@ export function isEmpty(obj) {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches a property from either a Module or a moduleInfo object
|
||||
* @param {Object} m Either a Module or a moduleInfo object
|
||||
* @param {string} property Property name
|
||||
* @returns {number} Property value
|
||||
*/
|
||||
export function moduleGet(m, property) {
|
||||
if (m instanceof Module) {
|
||||
return m.get(property);
|
||||
} else {
|
||||
// Assume its a moduleInfo object
|
||||
return m.props[property];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,11 +170,8 @@ select {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
&.hardpoint {
|
||||
.c {
|
||||
.hardpoint {
|
||||
width: 4.5em;
|
||||
padding: 0.1em 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user