mirror of
https://github.com/EDCD/coriolis.git
synced 2025-12-08 14:33:22 +00:00
Compare commits
239 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6a8e48fda | ||
|
|
f48af272c3 | ||
|
|
2b1402d099 | ||
|
|
1ad0afa0d7 | ||
|
|
93d6c1b871 | ||
|
|
243f1123ba | ||
|
|
2fa2625a8f | ||
|
|
42c43d3f2d | ||
|
|
1237833c7a | ||
|
|
171a84dd33 | ||
|
|
79dac7b38f | ||
|
|
0e86ae79c1 | ||
|
|
7d7ea18447 | ||
|
|
2a6da36aeb | ||
|
|
db1a976e20 | ||
|
|
3b058bda7f | ||
|
|
da274f1b75 | ||
|
|
d624663278 | ||
|
|
d8ac185b4d | ||
|
|
b66baef998 | ||
|
|
8a115f8323 | ||
|
|
442da6f05b | ||
|
|
f536c037b1 | ||
|
|
33a678a200 | ||
|
|
b0dc4d7864 | ||
|
|
8b0e822ea9 | ||
|
|
006811f5a9 | ||
|
|
873935c1d4 | ||
|
|
94d876e934 | ||
|
|
b055963fe0 | ||
|
|
8ad9472d56 | ||
|
|
d15e49f315 | ||
|
|
c7ea1eb95a | ||
|
|
82ce86a374 | ||
|
|
a1a17bc836 | ||
|
|
75a4e54453 | ||
|
|
32fb66139a | ||
|
|
3f18987007 | ||
|
|
eb042b2778 | ||
|
|
85f108556a | ||
|
|
73a75c69a3 | ||
|
|
2f5d123f02 | ||
|
|
e278babee8 | ||
|
|
86df6f20f6 | ||
|
|
117028875f | ||
|
|
cadd699bdf | ||
|
|
6afd80002c | ||
|
|
2736e1df79 | ||
|
|
0ff95ed1f1 | ||
|
|
fcb8980a38 | ||
|
|
49a076fd9e | ||
|
|
eb83969015 | ||
|
|
369d882354 | ||
|
|
23c4da55de | ||
|
|
7259a666eb | ||
|
|
91cab5a4f1 | ||
|
|
d60a8f2625 | ||
|
|
69489aa267 | ||
|
|
750d23b10a | ||
|
|
cd753af48e | ||
|
|
365810a610 | ||
|
|
73d609610a | ||
|
|
a3c03266bf | ||
|
|
7b3ad555a1 | ||
|
|
1605e80884 | ||
|
|
0729fc29fa | ||
|
|
964cdd2b9a | ||
|
|
49c3e395db | ||
|
|
bdd2299335 | ||
|
|
f782adb21d | ||
|
|
950c0c61f9 | ||
|
|
f6ebaf7445 | ||
|
|
3b35d5030e | ||
|
|
067b69f449 | ||
|
|
6f67267fec | ||
|
|
ec0cd37896 | ||
|
|
84719c997f | ||
|
|
2731ec3b90 | ||
|
|
340121c6bd | ||
|
|
ec4e70326a | ||
|
|
3a271e4b7b | ||
|
|
02db800c7b | ||
|
|
3a55c8cc0a | ||
|
|
e00b07c2f6 | ||
|
|
fbfd0deb6c | ||
|
|
dc11cc182f | ||
|
|
597344353a | ||
|
|
9aef1ff8a6 | ||
|
|
70f218f833 | ||
|
|
5937843be8 | ||
|
|
d4b384eded | ||
|
|
2b5a42d4e2 | ||
|
|
afd8ad7678 | ||
|
|
cdc5c29458 | ||
|
|
5a351d4c0d | ||
|
|
fac279d9dd | ||
|
|
f4e5254832 | ||
|
|
2fb3ee8cd8 | ||
|
|
cc2f3fd1fe | ||
|
|
87146b2cf3 | ||
|
|
bec3ae3f89 | ||
|
|
af2e0cbed3 | ||
|
|
4bf30c0cd5 | ||
|
|
a9fdf73d86 | ||
|
|
fe691d12c7 | ||
|
|
a5df542aa2 | ||
|
|
069959dabb | ||
|
|
a0e8f19683 | ||
|
|
342ca7af05 | ||
|
|
d00c0c3904 | ||
|
|
8e0f1ca977 | ||
|
|
33360fd6cf | ||
|
|
78ad34b082 | ||
|
|
ba98fe49a9 | ||
|
|
11f5c04efa | ||
|
|
ee9f65052a | ||
|
|
a25dde8d2d | ||
|
|
8d813688f7 | ||
|
|
99fc55ba6c | ||
|
|
402a4c1939 | ||
|
|
d1e16470b8 | ||
|
|
76027b8537 | ||
|
|
fd5ff3b6a8 | ||
|
|
d2d8f084d2 | ||
|
|
ea3d57399c | ||
|
|
77d3053ff8 | ||
|
|
aea3e43e1c | ||
|
|
a949bd6738 | ||
|
|
4981ffb908 | ||
|
|
4859138053 | ||
|
|
1f3c66d9ba | ||
|
|
f2af463d00 | ||
|
|
3858712613 | ||
|
|
d7941e0a8a | ||
|
|
178d38f28d | ||
|
|
e42a0d1210 | ||
|
|
ddb89c47e7 | ||
|
|
8b9aae342b | ||
|
|
ab1d73a6ea | ||
|
|
d73a3cc2b4 | ||
|
|
230351b959 | ||
|
|
4ee5c03cd1 | ||
|
|
d8e9733170 | ||
|
|
aba2abe507 | ||
|
|
45e6b71ec9 | ||
|
|
15a14dc280 | ||
|
|
cbac650b9e | ||
|
|
f011f1f4d5 | ||
|
|
d467ad5f7c | ||
|
|
b56ac177d9 | ||
|
|
5b13d64a1d | ||
|
|
2620935745 | ||
|
|
45852db507 | ||
|
|
7fbcbb75ed | ||
|
|
abf65ee436 | ||
|
|
24849cee08 | ||
|
|
9e5efe50dc | ||
|
|
39e9a38068 | ||
|
|
73d5915b3a | ||
|
|
b06d2d1f72 | ||
|
|
6e71f5e6db | ||
|
|
588cfc3990 | ||
|
|
191e31ff18 | ||
|
|
c655b65779 | ||
|
|
3e168a3e5f | ||
|
|
ee52a4b716 | ||
|
|
a14974fef4 | ||
|
|
2c0959d68b | ||
|
|
0221d05b04 | ||
|
|
975432b45a | ||
|
|
e78551f5d9 | ||
|
|
b1daf6f799 | ||
|
|
bc31be5884 | ||
|
|
df03102c35 | ||
|
|
55e4c51d77 | ||
|
|
e7d1a0df63 | ||
|
|
ca0c9675b3 | ||
|
|
491899fe49 | ||
|
|
cc22a89ad6 | ||
|
|
d389114189 | ||
|
|
9d0f7a166b | ||
|
|
c344d56d48 | ||
|
|
eb5d0e868c | ||
|
|
c28f22a78b | ||
|
|
e2b22dd4ca | ||
|
|
f5ecccec48 | ||
|
|
7c8eee4707 | ||
|
|
99e251fd4b | ||
|
|
c1ddecfd3e | ||
|
|
0ae59c5e48 | ||
|
|
eb76040b08 | ||
|
|
7698cb75e9 | ||
|
|
bd7139c703 | ||
|
|
c9b9404ba1 | ||
|
|
84ff013a9e | ||
|
|
d2bcf6cd71 | ||
|
|
e52c6730f4 | ||
|
|
e448b3cb0a | ||
|
|
705f3158aa | ||
|
|
f4789717fe | ||
|
|
93e69f215b | ||
|
|
d04e662de4 | ||
|
|
a9f9285278 | ||
|
|
c5e27d43aa | ||
|
|
db376829f5 | ||
|
|
79a31fd992 | ||
|
|
094f3aa3e2 | ||
|
|
41d51743d3 | ||
|
|
cf741d18ed | ||
|
|
c02f674530 | ||
|
|
89f2273b5d | ||
|
|
351b084ce9 | ||
|
|
dd48d2d007 | ||
|
|
63e850b5aa | ||
|
|
46775879f7 | ||
|
|
c9ba4e3fcb | ||
|
|
8b855b62a1 | ||
|
|
d6a687300c | ||
|
|
ddc968129d | ||
|
|
e364560ca7 | ||
|
|
baf6f8130a | ||
|
|
f8f5cd2581 | ||
|
|
8019453d0d | ||
|
|
6ad17595dc | ||
|
|
50e1f7d4df | ||
|
|
fdb931fe00 | ||
|
|
86efb59237 | ||
|
|
f05aa66e53 | ||
|
|
f4e95cd150 | ||
|
|
5798c0e8a6 | ||
|
|
22f98fa8a7 | ||
|
|
897668c0d4 | ||
|
|
8fcebf59f6 | ||
|
|
486c055631 | ||
|
|
92246302b9 | ||
|
|
0d646c6193 | ||
|
|
217ea3c5ac | ||
|
|
5d87a6cd56 | ||
|
|
c61c17d465 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ nginx.pid
|
||||
.idea
|
||||
/bin
|
||||
env
|
||||
*.swp
|
||||
|
||||
138
ChangeLog.md
138
ChangeLog.md
@@ -1,3 +1,141 @@
|
||||
#2.3.0
|
||||
* Make scan time visible on scanners where available
|
||||
* Update power distributor able-to-boost calculation to take fractional MJ values in to account
|
||||
* Revert to floating header due to issues on iOS
|
||||
* Fix issue where new module added to a slot did not reset its enabled status
|
||||
* Show integrity value for relevant modules
|
||||
* Reset old modification values when a new roll is applied
|
||||
* Fix issue with miner role where refinery would not be present in ships with class 5 slots but no class 4
|
||||
* Ensure that boost value is set correctly when modifications to power distributor enable/disable boost
|
||||
* Ensure that hull reinforcement modifications take the inherent resistance in to account when calculating modification percentages
|
||||
* Add tooltip for blueprints providing details of the features they alter, the components required for the blueprint and the engineer(s) who cam craft them
|
||||
* Use opponent's saved pips if available
|
||||
* Ignore rounds per shot for EPS and HPS calculations; it's already factored in to the numbers
|
||||
* Ensure that clip size modification imports result in whole numbers
|
||||
* Rework of separate offence/defence/movement sections to a unified interface
|
||||
* Use cargo hatch information on import if available
|
||||
* Additional information of power distributor pips, boost, cargo and fuel loads added to build
|
||||
* Additional information of opponent and engagement range added to build
|
||||
* Reworking of offence, defence and movement information in to separate tabs as part of the outfitting screen:
|
||||
* Power and costs section provides the existing 'Power' and 'Costs' sections
|
||||
* Profiles section provides a number of graphs that show how various components of the build (top speed, sustained DPS against opponent's shields and armour etc) are affected by mass, range, etc.
|
||||
* Offence section provides details of your build's damage distribution and per-weapon effectiveness. It also gives summary information for how long it will take for your build to wear down your opponent's shields and armour
|
||||
* Defence section provides details of your build's defences against your selected opponent. It provides details of the effectiveness of your resistances of both shields and armour, and effective strength of each as a result. It also provides key metrics around shield longevity and recovery times, as well as module protection
|
||||
* Fix power band marker to show safe power limit at 40% rather than 50%
|
||||
* Restyle blueprint list to improve consistency with similar menus
|
||||
* Use coriolis-data 2.3.0:
|
||||
* Add Dolphin
|
||||
* Add turreted mining lasers
|
||||
* Add long range / wide angle / fast scan scanner blueprints
|
||||
* Fix EDDB IDs for class 5 and 7 fighter hangars for correct shopping list
|
||||
* Fix cost for rocket-propelled FSD disruptor
|
||||
* Add module names for blueprints
|
||||
* Fix erroneous value for grade 5 kinetic shield booster
|
||||
* Add missing integrity values for some modules
|
||||
* Update module reinforcement package integrity
|
||||
* Update specs of Beluga as per 2.3
|
||||
* Update specs of Asp Scout as per 2.3
|
||||
* Update specs of Diamondback Explorer as per 2.3
|
||||
* Add ED ID for Rocket Propelled FSD Disruptor
|
||||
* Fix ED name for target lock breaker special
|
||||
* Update scan range and angle information for sensors
|
||||
* Tidy up shield cell bank information to allow for accurate calculations with modifications
|
||||
* Update mine launcher stats
|
||||
* Add appropriate engineers to per-module blueprint information
|
||||
|
||||
#2.2.19
|
||||
* Power management panel now displays modules in descending order of power usage by default
|
||||
* Shot speed can no longer be modified directly. Its value is derived from the range modifier for Long Range and Focused modifications
|
||||
* Ensure that jump range chart updates when fuel slider is changed
|
||||
* Add 'Engine profile' and 'FSD profile' charts. These show how your maximum speed/jump range will alter as you alter the mass of your build
|
||||
* Use coriolis-data 2.2.19:
|
||||
* Remove shot speed modification - it is directly tied to range
|
||||
* Fix incorrect minimal mass for 3C bi-weave shield generator
|
||||
|
||||
#2.2.18
|
||||
* Change methodology for calculating explorer role; can result in lighter builds
|
||||
* Tidy up layout for module selection and lay everything out in a consistent best-to-worst for both class and grade
|
||||
* Make integrity for module reinforcement packages visible
|
||||
* Clean up breakpoints for modules in available modules list; stops 7- or 8- module long lines
|
||||
* Add damager/range graphs to damage dealt
|
||||
* Reorder panels
|
||||
* Use coriolis-data 2.2.18:
|
||||
* Correct lower efficiency value to be better, not worse
|
||||
|
||||
#2.2.17
|
||||
* Use in-game terminology for shield generator optmul and optmass items
|
||||
* Add crew to shipyard and outfitting page information
|
||||
* Use coriolis-data 2.2.17:
|
||||
* Add mass as potential SCB modification
|
||||
* Fix mining laser statistics
|
||||
* Remove non-existent grade 4 and 5 wake scanner modifications
|
||||
* Add number of crew for each ship
|
||||
|
||||
#2.2.16
|
||||
* Fix 'Extreme' blueprint roll where some incorrect ranges were chosen
|
||||
* Use coriolis-data 2.2.16:
|
||||
* Fix incorrect thermal load modifiers for dirty drives
|
||||
* Provide explicit information about if values are higher numeric value == better or not
|
||||
|
||||
#2.2.15
|
||||
* Ensure that standard slots are repainted when any component changes
|
||||
* Reload page if Safari throws a security error
|
||||
* Handle import of ships with incorrectly-sized slots
|
||||
* Add 'Extreme' blueprint roll: best beneficial and worst detrimental outcome (in place of 'Average' roll)
|
||||
* Display information about Microsoft browser issues when an import fails
|
||||
* Add 'purchase this build' icon link to EDDB
|
||||
* Add 'miner' and 'shielded miner' ship roles
|
||||
* Use coriolis-data 2.2.15:
|
||||
* Fix location of initial cargo rack for Vulture
|
||||
* Fix broken regeneration rate for 6B shield generators
|
||||
* Tidy up breach damage values
|
||||
|
||||
#2.2.14
|
||||
* Ensure that jitter is shown correctly when the result of a special effect
|
||||
* Use restyled blueprint information
|
||||
* Use the ship name (if available) rather than the ship model for the window title
|
||||
* Use coriolis-data 2.2.14:
|
||||
* Alter blueprint structure to combine components and features
|
||||
* Make hidden value of modifications its own attribute
|
||||
* Fix incorrect ED ID for class 6 passenger cabins
|
||||
|
||||
#2.2.13
|
||||
* Add 'time to drain' summary value. This is the time to drain the WEP capacitor if firing all enabled weapons
|
||||
* Do not include utility slot DPS/EPS/HPS in summary information
|
||||
* Ensure that auto loader special shows in the tooltip
|
||||
* Ensure that ship mass is recalculated when appropriate
|
||||
* Use coriolis-data 2.2.13:
|
||||
* Add plasma slug special effect for plasma accelerator
|
||||
* Tweak hull costs of ships
|
||||
|
||||
#2.2.12
|
||||
* Tidy up old references to coriolis.io
|
||||
* Add ability to add and remove special effects to weapon modifications
|
||||
* Add weapon engineering information to Damage Dealt section
|
||||
* Change shortcut for link from ctrl-l to ctrl-o to avoid clash with location bar
|
||||
* Only show one of power generation or draw in tooltips, according to module
|
||||
* Use coriolis-data 2.2.12:
|
||||
* Add special effects for each blueprint
|
||||
* Add IDs for most Powerplay modules
|
||||
|
||||
#2.2.11
|
||||
* Add help system and initial help file
|
||||
* Make absolute damage visible
|
||||
* Add 'average' roll for blueprints
|
||||
* Update spacing for movement summary to make it more readable
|
||||
* Provide damage dealt statistics for both shields and hull
|
||||
* Damage dealt panel only shows enabled weapons
|
||||
* Add engagement range to damage received panel
|
||||
* Handle burst rate of fire as an absolute number rather than a perentage modification
|
||||
* Ensure that clip values are always rounded up
|
||||
* Ensure that focused weapon mod uses range modifier to increase falloff as well
|
||||
* Use coriolis-data 2.2.11:
|
||||
* Remove non-existent chaff launcher capacity blueprint grades
|
||||
* Fix incorrect values for charge enhanced power distributor
|
||||
* Remove incorrect AFMU blueprints
|
||||
* Correct fragment cannon Double Shot blueprint information
|
||||
* Correct Focused weapon blueprint information
|
||||
|
||||
#2.2.10
|
||||
* Fix detailed export of module reinforcement packages
|
||||
* Use damagedist for exact breakdown of weapons that have more than one type of damage
|
||||
|
||||
@@ -16,7 +16,7 @@ Chat to us on [Discord](https://discord.gg/0uwCh6R62aPRjk9w)!
|
||||
|
||||
## Development
|
||||
|
||||
See the [Developer's Guide](https://github.com/cmmcleod/coriolis/wiki/Developer's-Guide) in the wiki.
|
||||
See the [Developer's Guide](https://github.com/EDCD/coriolis/wiki/Developing-for-Coriolis) in the wiki.
|
||||
|
||||
|
||||
### Ship and Module Database
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"priority": 1,
|
||||
"modifications": {
|
||||
"pgen": 1000
|
||||
}
|
||||
}
|
||||
},
|
||||
"thrusters": {
|
||||
"class": 6,
|
||||
@@ -269,17 +269,19 @@
|
||||
"topSpeed": 187.01,
|
||||
"totalCost": 882362058,
|
||||
"totalDpe": 142.68,
|
||||
"totalDps": 103.8,
|
||||
"totalEps": 22.71,
|
||||
"totalHps": 677.29,
|
||||
"totalDps": 101.13,
|
||||
"totalEps": 18.71,
|
||||
"totalExplDpe": 0,
|
||||
"totalExplDps": 0,
|
||||
"totalExplSDps": 0,
|
||||
"totalHps": 33.62,
|
||||
"totalAbsDpe": 3.57,
|
||||
"totalAbsDps": 18.78,
|
||||
"totalAbsSDps": 14.45,
|
||||
"totalHps": 28.28,
|
||||
"totalKinDpe": 117.48,
|
||||
"totalKinDps": 24.94,
|
||||
"totalKinSDps": 18.76,
|
||||
"totalSDps": 91.84,
|
||||
"totalKinDps": 22.27,
|
||||
"totalKinSDps": 16.91,
|
||||
"totalSDps": 89.99,
|
||||
"totalThermDpe": 21.63,
|
||||
"totalThermDps": 60.08,
|
||||
"totalThermSDps": 58.64,
|
||||
@@ -317,6 +319,7 @@
|
||||
"shieldCells": 1840,
|
||||
"shieldExplRes": 0.5,
|
||||
"shieldKinRes": 0.4,
|
||||
"shieldThermRes": -0.2
|
||||
"shieldThermRes": -0.2,
|
||||
"crew": 3
|
||||
}
|
||||
}
|
||||
|
||||
314
__tests__/fixtures/companion-api-import-3.json
Normal file
314
__tests__/fixtures/companion-api-import-3.json
Normal file
@@ -0,0 +1,314 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"Test": "A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b.Iw18ZVA=.Aw18ZVA=."
|
||||
},
|
||||
"diamondback_explorer": {
|
||||
"Explorer": "A0p0tdFfldddsdf5---0202--320p432i2f.AwRj4zTI.AwiMIypI."
|
||||
"Explorer": "A0p0tdFfldddsdf5---0202--320p432i2f-.AwRj4zTYg===.AwiMIyoo."
|
||||
},
|
||||
"vulture": {
|
||||
"Bounty Hunter": "A3patcFalddksff31e1e0404-0l4a-5d27662j.AwRj4z2I.MwBhBYy6oJmAjLIA."
|
||||
|
||||
@@ -229,7 +229,7 @@ describe('Import Modal', function() {
|
||||
|
||||
beforeEach(reset);
|
||||
|
||||
it('imports a valid v4 build', function() {
|
||||
it('imports a valid companion API build', function() {
|
||||
const importData = require('./fixtures/companion-api-import-1');
|
||||
pasteText(JSON.stringify(importData));
|
||||
|
||||
@@ -238,10 +238,10 @@ describe('Import Modal', function() {
|
||||
expect(modal.state.singleBuild).toBe(true);
|
||||
clickProceed();
|
||||
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/federal_corvette?code=A2putsFklndzsxf50x0x7l28281919040404040402020l06p05sf63c5ifr--v66g2f.AwRj4zNaqA%3D%3D.CwRgDBldUExuBiIlUA%3D%3D.H4sIAAAAAAAAA02Svy9DURTHT1vvtfoat32eekVV9fm1kBgwSIw0YWYgBqmpMZkMBomFVfwFEoZKhBjE1qWTgegiDX%2BCQdKI1j2%2BR%2FJ4yzfnvu%2FnfO%2B979yQXiCi7xAkbRpEqsLMsRKWHNZpsSKQnppJVLAdIvc6DGiwxexMaWb7GDZHdJ%2BQaCf71Ia%2F88XsOp1EThk9bOh5P2kkahGN3qPM1wANbyOk87zNHH%2FBUs0gnWN61T9TOwfJ7EWJjMcms1lEo30Gx11BD8f1mh%2FcTkCMMvY0HZcoe4Wk5By%2BFcrrRL0N0OOlrd0Ntv57jGoc%2BH4%2F8EqHj3%2FCUXc4FicC5NFvsJBVIWeFvESlpuXSuCS5RRyLlV70z%2B4uQaw6ypSIJ6KOJDgZgFpQ60YgEU9EPQmUCkAfAj0IJOKJqC4wuYMY9rQD5CuubT0LSag8qdShxHUHoElcyWrAT4l4IsoCw65e%2BRv5BqKtC0mSJu8LH8OFT%2Bb%2BE8SZb0CcEn4AZ3TRDx5q4l1EJ%2BCP1bEM1WSaAwH%2FFkOLPoofwTo0LY8nr7O%2B37cp4yWIu4zHlHiXGfMPmat5gqMCAAA%3D&bn=Imported%20Federal%20Corvette');
|
||||
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/federal_corvette?code=A2putsFklndzsxf50x0x7l28281919040404040402020l06p05sf63c5ifr--v66g2f.AwRj4zNaqA%3D%3D.CwRgDBldUExuBiIlUA%3D%3D.H4sIAAAAAAAAA02SPy9DURjG3%2F65vW1v47TXVbeqqF7EQtIIBomRJswsYmISH8BgkFhqFZ9AwlALMYitkXQyEF2k4SMYJNK0dV7PK7nc5ck55%2Fm9z%2FnznpBeJqLvECQbM4hUjZnjO5hyWGfFikAGGjGiku0QuddhQCNdZmdWM9snsDmih4REOdlnNvz9DrPrJIicPdSwoZf8pAnTIpq8x7DYADS%2Bi5DERY85%2BYqpmkc6x%2FWGf6beKCR3YBIZFZCxCgrtczjuOmo4qTf94F4KYuxhz5jjEhXmUJNexFrpIUo02ALN1j9u1JMgD%2FMga1GfbMNRd9iHUwGy%2BpspZF3IBSGvMFJluS%2FuR24FJ2KlV%2Fxju6sQq4lhRsQTUVUJTgegLtS6EUjEE1HPAmUC0KdAjwKJeCKqD8zoURx72gHyDW9nvQhJGHkyUscS1x%2BAZnAlqwU%2FI%2BKJKEvextXrf93eQrR1KUlS5HWwGC61mfOn0oN3IM4OHoBzuuIHj33hS5jT8KeamIYa0sjhgH%2BLfplP4kcwD5Xl3xR1wfeHtqWzBHHX8I9SH9Je%2FgGvXxeungIAAA%3D%3D&bn=Imported%20Federal%20Corvette');
|
||||
});
|
||||
|
||||
it('imports a valid v4 build', function() {
|
||||
it('imports a valid companion API build', function() {
|
||||
const importData = require('./fixtures/companion-api-import-2');
|
||||
pasteText(JSON.stringify(importData));
|
||||
|
||||
@@ -252,6 +252,18 @@ describe('Import Modal', function() {
|
||||
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/beluga?code=A0pktsFplCdpsnf70t0t2727270004040404043c4fmimlmm04mc0iv62i2f.AwRj4yukg%3D%3D%3D.CwRgDBldHi8IUA%3D%3D.H4sIAAAAAAAAA2P8Z8%2FAwPCXEUiIKTMxMPCv%2F%2Ff%2FP8cFIPGf6Z8YTEr0GjMDg%2FJWICERBOTzn%2Fn7%2F7%2FIO5Ai5n9SIEWsQEIoSxAolfbt%2F3%2BJPk4GBhE7YQYGYVmgcuVnf4Aq%2FwMAIrEcGGsAAAA%3D&bn=Imported%20Beluga%20Liner');
|
||||
});
|
||||
|
||||
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.AwRj4yrI.CwRgDBlVK7EiA%3D%3D%3D.&bn=Imported%20Type-7%20Transporter');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import E:D Shipyard Builds', function() {
|
||||
|
||||
11
d3-funcs.js
vendored
Normal file
11
d3-funcs.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export {
|
||||
axisBottom,
|
||||
axisLeft,
|
||||
axisTop,
|
||||
formatLocale,
|
||||
line,
|
||||
scaleBand,
|
||||
scaleLinear,
|
||||
scaleOrdinal,
|
||||
select
|
||||
} from 'd3';
|
||||
4
d3.min.js
vendored
Normal file
4
d3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
27
package.json
27
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "coriolis_shipyard",
|
||||
"version": "2.2.10",
|
||||
"version": "2.3.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/EDCD/coriolis"
|
||||
@@ -11,6 +11,7 @@
|
||||
"engine": "node >= 4.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prepublish": "rollup -c && uglifyjs d3.js -c -m -o d3.min.js",
|
||||
"extract-translations": "grep -hroE \"(translate\\('[^']+'\\))|(tip.bind\\(null, '[^']+')\" src/* | grep -oE \"'[^']+'\" | grep -oE \"[^']+\" | sort -u -f",
|
||||
"clean": "rimraf build",
|
||||
"start": "node devServer.js",
|
||||
@@ -18,7 +19,7 @@
|
||||
"test": "jest",
|
||||
"prod-serve": "nginx -p $(pwd) -c nginx.conf",
|
||||
"prod-stop": "kill -QUIT $(cat nginx.pid)",
|
||||
"build": "npm run clean && NODE_ENV=production webpack -d -p --config webpack.config.prod.js",
|
||||
"build": "npm run clean && 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"
|
||||
},
|
||||
@@ -53,7 +54,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"appcache-webpack-plugin": "^1.2.1",
|
||||
"appcache-webpack-plugin": "^1.3.0",
|
||||
"babel-core": "*",
|
||||
"babel-eslint": "*",
|
||||
"babel-jest": "*",
|
||||
@@ -62,38 +63,44 @@
|
||||
"babel-preset-react": "*",
|
||||
"babel-preset-stage-0": "*",
|
||||
"css-loader": "^0.23.0",
|
||||
"d3-selection": "1",
|
||||
"eslint": "2.2.0",
|
||||
"eslint-plugin-react": "^4.0.0",
|
||||
"expose-loader": "^0.7.1",
|
||||
"express": "^4.13.3",
|
||||
"extract-text-webpack-plugin": "^0.9.1",
|
||||
"extract-text-webpack-plugin": "2.0.0",
|
||||
"file-loader": "^0.8.4",
|
||||
"html-webpack-plugin": "^1.7.0",
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"jest-cli": "^16.0.1",
|
||||
"jsen": "^0.6.0",
|
||||
"json-loader": "^0.5.3",
|
||||
"less": "^2.5.3",
|
||||
"less-loader": "^2.2.1",
|
||||
"react-addons-perf": "^15.4.2",
|
||||
"react-addons-test-utils": "^15.0.1",
|
||||
"react-measure": "^1.4.6",
|
||||
"react-testutils-additions": "^15.1.0",
|
||||
"rimraf": "^2.4.3",
|
||||
"rollup": "0.36",
|
||||
"rollup-plugin-node-resolve": "2",
|
||||
"style-loader": "^0.13.0",
|
||||
"url-loader": "^0.5.6",
|
||||
"webpack": "^1.9.6",
|
||||
"webpack-dev-server": "^1.14.0"
|
||||
"webpack": "^2.2.1",
|
||||
"webpack-dev-server": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-polyfill": "*",
|
||||
"classnames": "^2.2.0",
|
||||
"browserify-zlib": "ipfs/browserify-zlib",
|
||||
"classnames": "^2.2.0",
|
||||
"coriolis-data": "EDCD/coriolis-data",
|
||||
"d3": "3.5.16",
|
||||
"d3": "4.6.0",
|
||||
"fbemitter": "^2.0.0",
|
||||
"lodash": "^4.15.0",
|
||||
"lz-string": "^1.4.4",
|
||||
"react-number-editor": "Athanasius/react-number-editor.git#miggy",
|
||||
"react": "^15.0.1",
|
||||
"react-dom": "^15.0.1",
|
||||
"react-number-editor": "Athanasius/react-number-editor.git#miggy",
|
||||
"recharts": "^0.21.2",
|
||||
"superagent": "^1.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
9
rollup.config.js
Normal file
9
rollup.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import nodeResolve from "rollup-plugin-node-resolve";
|
||||
|
||||
export default {
|
||||
entry: "d3-funcs.js",
|
||||
format: "umd",
|
||||
moduleName: "d3",
|
||||
plugins: [nodeResolve({jsnext: true})],
|
||||
dest: "d3.js"
|
||||
};
|
||||
@@ -6,9 +6,11 @@ import Persist from './stores/Persist';
|
||||
|
||||
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 { outfitURL } from './utils/UrlGenerators';
|
||||
|
||||
import AboutPage from './pages/AboutPage';
|
||||
import NotFoundPage from './pages/NotFoundPage';
|
||||
@@ -159,14 +161,26 @@ export default class Coriolis extends React.Component {
|
||||
this._hideModal();
|
||||
this._closeMenu();
|
||||
break;
|
||||
case 72: // 'h'
|
||||
if (e.ctrlKey || e.metaKey) { // CTRL/CMD + h
|
||||
e.preventDefault();
|
||||
this._showModal(<ModalHelp />);
|
||||
}
|
||||
break;
|
||||
case 73: // 'i'
|
||||
if (e.ctrlKey || e.metaKey) { // CTRL/CMD + i
|
||||
e.preventDefault();
|
||||
this._showModal(<ModalImport />);
|
||||
}
|
||||
break;
|
||||
case 101010: // 's'
|
||||
if (e.ctrlKey || e.metaKey) { // CTRL/CMD + i
|
||||
case 79: // 'o'
|
||||
if (e.ctrlKey || e.metaKey) { // CTRL/CMD + o
|
||||
e.preventDefault();
|
||||
this._showModal(<ModalPermalink url={window.location.href}/>);
|
||||
}
|
||||
break;
|
||||
case 83: // 's'
|
||||
if (e.ctrlKey || e.metaKey) { // CTRL/CMD + s
|
||||
e.preventDefault();
|
||||
this.emitter.emit('command', 'save');
|
||||
}
|
||||
@@ -227,14 +241,19 @@ export default class Coriolis extends React.Component {
|
||||
/**
|
||||
* Show the term tip
|
||||
* @param {string} term Term or Phrase
|
||||
* @param {Object} opts Options - dontCap, orientation (n,e,s,w)
|
||||
* @param {Object} opts Options - dontCap, orientation (n,e,s,w) (can also be the event if no options supplied)
|
||||
* @param {SyntheticEvent} event Event
|
||||
* @param {SyntheticEvent} e2 Alternative location for synthetic event from charts (where 'Event' is actually a chart index)
|
||||
*/
|
||||
_termtip(term, opts, event) {
|
||||
if (opts && opts.nativeEvent) { // Opts is a SyntheticEvent
|
||||
_termtip(term, opts, event, e2) {
|
||||
if (opts && opts.nativeEvent) { // Opts is the SyntheticEvent
|
||||
event = opts;
|
||||
opts = { cap: true };
|
||||
}
|
||||
if (e2 instanceof Object && e2.nativeEvent) { // E2 is the SyntheticEvent
|
||||
event = e2;
|
||||
}
|
||||
|
||||
this._tooltip(
|
||||
<div className={'cen' + (opts.cap ? ' cap' : '')}>{this.state.language.translate(term)}</div>,
|
||||
event.currentTarget.getBoundingClientRect(),
|
||||
|
||||
@@ -76,7 +76,16 @@ Router.go = function(path, state) {
|
||||
if (isStandAlone()) {
|
||||
Persist.setState(ctx);
|
||||
}
|
||||
history.pushState(ctx.state, ctx.title, ctx.canonicalPath);
|
||||
try {
|
||||
history.pushState(ctx.state, ctx.title, ctx.canonicalPath);
|
||||
} catch (ex) {
|
||||
sessionStorage.setItem('__safari_history_fix', JSON.stringify({
|
||||
state: ctx.state,
|
||||
title: ctx.title,
|
||||
path: ctx.canonicalPath
|
||||
}));
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
@@ -99,7 +108,16 @@ Router.replace = function(path, state, dispatch) {
|
||||
if (isStandAlone()) {
|
||||
Persist.setState(ctx);
|
||||
}
|
||||
history.replaceState(ctx.state, ctx.title, ctx.canonicalPath);
|
||||
try {
|
||||
history.replaceState(ctx.state, ctx.title, ctx.canonicalPath);
|
||||
} catch (ex) {
|
||||
sessionStorage.setItem('__safari_history_fix', JSON.stringify({
|
||||
state: ctx.state,
|
||||
title: ctx.title,
|
||||
path: ctx.canonicalPath
|
||||
}));
|
||||
location.reload();
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
@@ -7,6 +8,75 @@ import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
|
||||
|
||||
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',
|
||||
'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',
|
||||
'fc': 'projectiles',
|
||||
'pa': 'projectiles',
|
||||
'rg': 'projectiles',
|
||||
'mr': 'ordnance',
|
||||
'tp': 'ordnance',
|
||||
'nl': 'ordnance',
|
||||
'sc': 'scanners',
|
||||
'ss': 'scanners',
|
||||
// Utilities
|
||||
'cs': 'scanners',
|
||||
'kw': 'scanners',
|
||||
'ws': 'scanners',
|
||||
'ch': 'defence',
|
||||
'po': 'defence',
|
||||
'ec': 'defence',
|
||||
};
|
||||
// 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'],
|
||||
'passenger cabins': ['pce', 'pci', 'pcm', 'pcq'],
|
||||
'rf': ['rf'],
|
||||
'shields': ['sg', 'bsg', 'psg', 'scb'],
|
||||
'structural reinforcement': ['hr', 'mrp'],
|
||||
'dc': ['dc'],
|
||||
// Hardpoints
|
||||
'lasers': ['pl', 'ul', 'bl', 'ml'],
|
||||
'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
|
||||
};
|
||||
|
||||
/**
|
||||
* Available modules menu
|
||||
*/
|
||||
@@ -63,15 +133,55 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
} else {
|
||||
list = [];
|
||||
// At present time slots with grouped options (Hardpoints and Internal) can be empty
|
||||
list.push(<div className='empty-c upp' key='empty' onClick={onSelect.bind(null, null)} >{translate('empty')}</div>);
|
||||
for (let g in modules) {
|
||||
if (m && g == m.grp) {
|
||||
list.push(<div ref={(elem) => this.groupElem = elem} key={g} className={'select-group cap'}>{translate(g)}</div>);
|
||||
} else {
|
||||
list.push(<div key={g} className={'select-group cap'}>{translate(g)}</div>);
|
||||
}
|
||||
if (m) {
|
||||
list.push(<div className='empty-c upp' key='empty' onClick={onSelect.bind(null, null)} >{translate('empty')}</div>);
|
||||
}
|
||||
|
||||
list.push(buildGroup(g, modules[g]));
|
||||
// Need to regroup the modules by our own categorisation
|
||||
let catmodules = {};
|
||||
// Pre-create to preserve ordering
|
||||
for (let cat in CATEGORIES) {
|
||||
catmodules[cat] = [];
|
||||
}
|
||||
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(buildGroup(grp, modules[grp]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +205,23 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
|
||||
const sortedModules = modules.sort(this._moduleOrder);
|
||||
|
||||
// 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]));
|
||||
|
||||
let itemsOnThisRow = 0;
|
||||
|
||||
for (let i = 0; i < sortedModules.length; i++) {
|
||||
let m = sortedModules[i];
|
||||
let mount = null;
|
||||
let disabled = m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass;
|
||||
let disabled = false;
|
||||
if (ModuleUtils.isShieldGenerator(m.grp)) {
|
||||
// Shield generators care about maximum hull mass
|
||||
disabled = mass > m.maxmass;
|
||||
} else if (m.maxmass) {
|
||||
// Thrusters care about total mass
|
||||
disabled = mass + m.mass > m.maxmass;
|
||||
}
|
||||
let active = mountedModule && mountedModule.id === m.id;
|
||||
let classes = cn(m.name ? 'lc' : 'c', {
|
||||
warning: !disabled && warningFunc && warningFunc(m),
|
||||
@@ -128,8 +251,9 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
case 'T': mount = <MountTurret className={'lg'}/>; break;
|
||||
}
|
||||
|
||||
if (i > 0 && sortedModules.length > 3 && m.class != prevClass && (m.rating != prevRating || m.mount) && m.grp != 'pa') {
|
||||
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;
|
||||
}
|
||||
|
||||
elems.push(
|
||||
@@ -138,6 +262,7 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
{(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}
|
||||
</li>
|
||||
);
|
||||
itemsOnThisRow++;
|
||||
prevClass = m.class;
|
||||
prevRating = m.rating;
|
||||
}
|
||||
@@ -232,12 +357,12 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// Rating ordered from lowest (E) to highest (A)
|
||||
// Rating ordered from highest (A) to lowest (E)
|
||||
if (a.rating < b.rating) {
|
||||
return 1;
|
||||
return -1;
|
||||
}
|
||||
if (a.rating > b.rating) {
|
||||
return -1;
|
||||
return 1;
|
||||
}
|
||||
// Do not attempt to order by name at this point, as that mucks up the order of armour
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import d3 from 'd3';
|
||||
import * as d3 from 'd3';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
|
||||
const MARGIN = { top: 15, right: 20, bottom: 40, left: 150 };
|
||||
@@ -44,7 +44,7 @@ export default class BarChart extends TranslatedComponent {
|
||||
unit: ''
|
||||
};
|
||||
|
||||
static PropTypes = {
|
||||
static propTypes = {
|
||||
colors: React.PropTypes.array,
|
||||
data: React.PropTypes.array.isRequired,
|
||||
desc: React.PropTypes.bool,
|
||||
@@ -68,13 +68,13 @@ export default class BarChart extends TranslatedComponent {
|
||||
this._updateDimensions = this._updateDimensions.bind(this);
|
||||
this._hideTip = this._hideTip.bind(this);
|
||||
|
||||
let scale = d3.scale.linear();
|
||||
let y0 = d3.scale.ordinal();
|
||||
let y1 = d3.scale.ordinal();
|
||||
let scale = d3.scaleLinear();
|
||||
let y0 = d3.scaleBand();
|
||||
let y1 = d3.scaleBand();
|
||||
|
||||
this.xAxis = d3.svg.axis().scale(scale).ticks(5).outerTickSize(0).orient('bottom').tickFormat(context.language.formats.s2);
|
||||
this.yAxis = d3.svg.axis().scale(y0).outerTickSize(0).orient('left');
|
||||
this.state = { scale, y0, y1, color: d3.scale.ordinal().range(props.colors) };
|
||||
this.xAxis = d3.axisBottom(scale).ticks(5).tickSizeOuter(0).tickFormat(context.language.formats.s2);
|
||||
this.yAxis = d3.axisLeft(y0).tickSizeOuter(0);
|
||||
this.state = { scale, y0, y1, color: d3.scaleOrdinal().range(props.colors) };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,8 +131,8 @@ export default class BarChart extends TranslatedComponent {
|
||||
let max = data.reduce((max, build) => (properties.reduce(((m, p) => (m > build[p] ? m : build[p])), max)), 0);
|
||||
|
||||
this.state.scale.range([0, innerWidth]).domain([0, max]);
|
||||
this.state.y0.domain(data.map(bName)).rangeRoundBands([0, innerHeight], 0.3);
|
||||
this.state.y1.domain(properties).rangeRoundBands([0, this.state.y0.rangeBand()]);
|
||||
this.state.y0.domain(data.map(bName)).range([0, innerHeight], 0.3).padding(0.4);
|
||||
this.state.y1.domain(properties).range([0, this.state.y0.bandwidth()]).padding(0.1);
|
||||
|
||||
this.setState({
|
||||
barHeight,
|
||||
@@ -192,7 +192,7 @@ export default class BarChart extends TranslatedComponent {
|
||||
x={0}
|
||||
y={y1(p)}
|
||||
width={scale(build[p])}
|
||||
height={y1.rangeBand()}
|
||||
height={y1.bandwidth()}
|
||||
fill={color(p)}
|
||||
onMouseOver={this._showTip.bind(this, build, p, propIndex)}
|
||||
onMouseOut={this._hideTip}
|
||||
|
||||
89
src/app/components/Boost.jsx
Normal file
89
src/app/components/Boost.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import { nameComparator } from '../utils/SlotFunctions';
|
||||
import { Pip } from './SvgIcons';
|
||||
import LineChart from '../components/LineChart';
|
||||
import Slider from '../components/Slider';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import Module from '../shipyard/Module';
|
||||
|
||||
/**
|
||||
* Boost displays a boost button that toggles bosot
|
||||
* Requires an onChange() function of the form onChange(boost) which is triggered whenever the boost changes.
|
||||
*/
|
||||
export default class Boost extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
marker: React.PropTypes.string.isRequired,
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
boost: React.PropTypes.bool.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
const { ship, boost } = props;
|
||||
|
||||
this._keyDown = this._keyDown.bind(this);
|
||||
this._toggleBoost = this._toggleBoost.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add listeners after mounting
|
||||
*/
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this._keyDown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove listeners before unmounting
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this._keyDown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Key Down
|
||||
* @param {Event} e Keyboard Event
|
||||
*/
|
||||
_keyDown(e) {
|
||||
if (e.ctrlKey || e.metaKey) { // CTRL/CMD
|
||||
switch (e.keyCode) {
|
||||
case 66: // b == boost
|
||||
if (this.props.ship.canBoost()) {
|
||||
e.preventDefault();
|
||||
this._toggleBoost();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the boost feature
|
||||
*/
|
||||
_toggleBoost() {
|
||||
this.props.onChange(!this.props.boost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render boost
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { formats, translate, units } = this.context.language;
|
||||
const { ship, boost } = this.props;
|
||||
|
||||
// TODO disable if ship cannot boost
|
||||
return (
|
||||
<span id='boost'>
|
||||
<button id='boost' className={boost ? 'selected' : null} onClick={this._toggleBoost}>{translate('boost')}</button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
75
src/app/components/Cargo.jsx
Normal file
75
src/app/components/Cargo.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import Slider from '../components/Slider';
|
||||
|
||||
/**
|
||||
* Cargo slider
|
||||
* Requires an onChange() function of the form onChange(cargo), providing the cargo in tonnes, which is triggered on cargo level change
|
||||
*/
|
||||
export default class Cargo extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
cargo: React.PropTypes.number.isRequired,
|
||||
cargoCapacity: React.PropTypes.number.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
this._cargoChange = this._cargoChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cargo level
|
||||
* @param {number} cargoLevel percentage level from 0 to 1
|
||||
*/
|
||||
_cargoChange(cargoLevel) {
|
||||
const { cargo, cargoCapacity } = this.props;
|
||||
if (cargoCapacity > 0) {
|
||||
// We round the cargo to whole number of tonnes
|
||||
const newCargo = Math.round(cargoLevel * cargoCapacity);
|
||||
if (newCargo != cargo) {
|
||||
this.props.onChange(newCargo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render cargo slider
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { cargo, cargoCapacity } = this.props;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<h3>{translate('cargo carried')}: {formats.int(cargo)}{units.T}</h3>
|
||||
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
|
||||
<tbody >
|
||||
<tr>
|
||||
<td>
|
||||
<Slider
|
||||
axis={true}
|
||||
onChange={this._cargoChange}
|
||||
axisUnit={translate('T')}
|
||||
percent={cargo / cargoCapacity}
|
||||
max={cargoCapacity}
|
||||
scale={sizeRatio}
|
||||
onResize={onWindowResize}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import TranslatedComponent from './TranslatedComponent';
|
||||
import Link from './Link';
|
||||
import cn from 'classnames';
|
||||
import { outfitURL } from '../utils/UrlGenerators';
|
||||
import { SizeMap } from '../shipyard/Constants';
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,7 @@ import TranslatedComponent from './TranslatedComponent';
|
||||
*/
|
||||
export default class CostSection extends TranslatedComponent {
|
||||
|
||||
static PropTypes = {
|
||||
static propTypes = {
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
code: React.PropTypes.string.isRequired,
|
||||
buildName: React.PropTypes.string
|
||||
@@ -507,7 +507,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
scoop = true;
|
||||
break;
|
||||
case 'scb':
|
||||
q = slotGroup[i].m.getCells();
|
||||
q = slotGroup[i].m.getAmmo() + 1;
|
||||
break;
|
||||
case 'am':
|
||||
q = slotGroup[i].m.getAmmo();
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import ShipSelector from './ShipSelector';
|
||||
import { nameComparator } from '../utils/SlotFunctions';
|
||||
import { CollapseSection, ExpandSection, MountFixed, MountGimballed, MountTurret } from './SvgIcons';
|
||||
import Slider from '../components/Slider';
|
||||
|
||||
/**
|
||||
* Generates an internationalization friendly weapon comparator that will
|
||||
* sort by specified property (if provided) then by name/group, class, rating
|
||||
* @param {function} translate Translation function
|
||||
* @param {function} propComparator Optional property comparator
|
||||
* @param {boolean} desc Use descending order
|
||||
* @return {function} Comparator function for names
|
||||
*/
|
||||
export function weaponComparator(translate, propComparator, desc) {
|
||||
return (a, b) => {
|
||||
if (!desc) { // Flip A and B if ascending order
|
||||
let t = a;
|
||||
a = b;
|
||||
b = t;
|
||||
}
|
||||
|
||||
// If a property comparator is provided use it first
|
||||
let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a, b);
|
||||
|
||||
if (diff) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
// Property matches so sort by name / group, then class, rating
|
||||
if (a.name === b.name && a.grp === b.grp) {
|
||||
if(a.class == b.class) {
|
||||
return a.rating > b.rating ? 1 : -1;
|
||||
}
|
||||
return a.class - b.class;
|
||||
}
|
||||
|
||||
return nameComparator(translate, a, b);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Damage against a selected ship
|
||||
*/
|
||||
export default class DamageDealt extends TranslatedComponent {
|
||||
static PropTypes = {
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
code: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
static DEFAULT_AGAINST = Ships['anaconda'];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._sort = this._sort.bind(this);
|
||||
this._onShipChange = this._onShipChange.bind(this);
|
||||
this._onCollapseExpand = this._onCollapseExpand.bind(this);
|
||||
|
||||
this.state = {
|
||||
predicate: 'n',
|
||||
desc: true,
|
||||
against: DamageDealt.DEFAULT_AGAINST,
|
||||
expanded: false,
|
||||
range: 0.1667,
|
||||
maxRange: 6000
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the initial weapons state
|
||||
*/
|
||||
componentWillMount() {
|
||||
const data = this._calcWeapons(this.props.ship, this.state.against, this.state.range * this.state.maxRange);
|
||||
this.setState({ weapons: data.weapons, totals: data.totals });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the updated weapons state if our ship changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
* @return {boolean} Returns true if the component should be rerendered
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
if (nextProps.code != this.props.code) {
|
||||
const data = this._calcWeapons(this.props.ship, this.state.against, this.state.range * this.state.maxRange);
|
||||
this.setState({ weapons: data.weapons, totals: data.totals });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the damage dealt by a ship
|
||||
* @param {Object} ship The ship which will deal the damage
|
||||
* @param {Object} against The ship against which damage will be dealt
|
||||
* @param {Object} range The engagement range
|
||||
* @return {boolean} Returns the per-weapon damage
|
||||
*/
|
||||
_calcWeapons(ship, against, range) {
|
||||
// Tidy up the range so that it's to 4 decimal places
|
||||
range = Math.round(10000 * range) / 10000;
|
||||
|
||||
// Track totals
|
||||
let totals = {};
|
||||
totals.effectiveness = 0;
|
||||
totals.effectiveDps = 0;
|
||||
totals.effectiveSDps = 0;
|
||||
let totalDps = 0;
|
||||
|
||||
let weapons = [];
|
||||
for (let i = 0; i < ship.hardpoints.length; i++) {
|
||||
if (ship.hardpoints[i].m) {
|
||||
const m = ship.hardpoints[i].m;
|
||||
if (m.getDamage() && m.grp !== 'po') {
|
||||
let dropoff = 1;
|
||||
if (m.getFalloff()) {
|
||||
// Calculate the dropoff % due to range
|
||||
if (range > m.getRange()) {
|
||||
// Weapon is out of range
|
||||
dropoff = 0;
|
||||
} else {
|
||||
const falloff = m.getFalloff();
|
||||
if (range > falloff) {
|
||||
const dropoffRange = m.getRange() - falloff;
|
||||
// Assuming straight-line falloff
|
||||
dropoff = 1 - (range - falloff) / dropoffRange;
|
||||
}
|
||||
}
|
||||
}
|
||||
const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`;
|
||||
const effectiveness = (m.getPiercing() >= against.properties.hardness ? 1 : m.getPiercing() / against.properties.hardness) * dropoff;
|
||||
const effectiveDps = m.getDps() * effectiveness * dropoff;
|
||||
const effectiveSDps = (m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) * effectiveness : effectiveDps) * dropoff;
|
||||
totals.effectiveDps += effectiveDps;
|
||||
totals.effectiveSDps += effectiveSDps;
|
||||
totalDps += m.getDps();
|
||||
|
||||
weapons.push({ id: i,
|
||||
mount: m.mount,
|
||||
name: m.name || m.grp,
|
||||
classRating,
|
||||
effectiveDps,
|
||||
effectiveSDps,
|
||||
effectiveness });
|
||||
}
|
||||
}
|
||||
}
|
||||
totals.effectiveness = totals.effectiveDps / totalDps;
|
||||
|
||||
return { weapons, totals };
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when the collapse or expand icons are clicked
|
||||
*/
|
||||
_onCollapseExpand() {
|
||||
this.setState({ expanded: !this.state.expanded });
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when the ship we compare against changes
|
||||
* @param {string} s the new ship ID
|
||||
*/
|
||||
_onShipChange(s) {
|
||||
const against = Ships[s];
|
||||
const data = this._calcWeapons(this.props.ship, against);
|
||||
this.setState({ against, weapons: data.weapons, totals: data.totals });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sort order and sort
|
||||
* @param {string} predicate Sort predicate
|
||||
*/
|
||||
_sortOrder(predicate) {
|
||||
let desc = this.state.desc;
|
||||
|
||||
if (predicate == this.state.predicate) {
|
||||
desc = !desc;
|
||||
} else {
|
||||
desc = true;
|
||||
}
|
||||
|
||||
this._sort(this.props.ship, predicate, desc);
|
||||
this.setState({ predicate, desc });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the weapon list
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {string} predicate Sort predicate
|
||||
* @param {Boolean} desc Sort order descending
|
||||
*/
|
||||
_sort(ship, predicate, desc) {
|
||||
let comp = weaponComparator.bind(null, this.context.language.translate);
|
||||
|
||||
switch (predicate) {
|
||||
case 'n': comp = comp(null, desc); break;
|
||||
case 'edps': comp = comp((a, b) => a.effectiveDps - b.effectiveDps, desc); break;
|
||||
case 'esdps': comp = comp((a, b) => a.effectiveSDps - b.effectiveSDps, desc); break;
|
||||
case 'e': comp = comp((a, b) => a.effectiveness - b.effectiveness, desc); break;
|
||||
}
|
||||
|
||||
this.state.weapons.sort(comp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render individual rows for hardpoints
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Object} formats Localised formats map
|
||||
* @return {array} The individual rows
|
||||
*
|
||||
*/
|
||||
_renderRows(translate, formats) {
|
||||
const { termtip, tooltip } = this.context;
|
||||
|
||||
let rows = [];
|
||||
|
||||
if (this.state.weapons) {
|
||||
for (let i = 0; i < this.state.weapons.length; i++) {
|
||||
const weapon = this.state.weapons[i];
|
||||
|
||||
rows.push(<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)}
|
||||
</td>
|
||||
<td className='ri'>{formats.round1(weapon.effectiveDps)}</td>
|
||||
<td className='ri'>{formats.round1(weapon.effectiveSDps)}</td>
|
||||
<td className='ri'>{formats.pct(weapon.effectiveness)}</td>
|
||||
</tr>);
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current range
|
||||
* @param {number} range Range 0-1
|
||||
*/
|
||||
_rangeChange(range) {
|
||||
const data = this._calcWeapons(this.props.ship, this.state.against, this.state.range * this.state.maxRange);
|
||||
this.setState({ range, weapons: data.weapons, totals: data.totals });
|
||||
}
|
||||
|
||||
/**
|
||||
* Render damage dealt
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { expanded, maxRange, range, totals } = this.state;
|
||||
|
||||
const sortOrder = this._sortOrder;
|
||||
const onCollapseExpand = this._onCollapseExpand;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<h1>{translate('damage dealt against')} {expanded ? <span onClick={onCollapseExpand}><CollapseSection className='summary'/></span> : <span onClick={onCollapseExpand}><ExpandSection className='summary'/></span>}</h1>
|
||||
{expanded ? <span>
|
||||
<ShipSelector initial={this.state.against} currentMenu={this.props.currentMenu} onChange={this._onShipChange} />
|
||||
<table className='summary' style={{ width: '100%' }}>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<td className='sortable' onClick={sortOrder.bind(this, 'n')}>{translate('weapon')}</td>
|
||||
<td className='sortable' onClick={sortOrder.bind(this, 'edps')}>{translate('effective dps')}</td>
|
||||
<td className='sortable' onClick={sortOrder.bind(this, 'esdps')}>{translate('effective sdps')}</td>
|
||||
<td className='sortable' onClick={sortOrder.bind(this, 'e')}>{translate('effectiveness')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this._renderRows(translate, formats)}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className='main'>
|
||||
<td className='ri'><i>{translate('total')}</i></td>
|
||||
<td className='ri'><i>{formats.round1(totals.effectiveDps)}</i></td>
|
||||
<td className='ri'><i>{formats.round1(totals.effectiveSDps)}</i></td>
|
||||
<td className='ri'><i>{formats.pct(totals.effectiveness)}</i></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<table style={{ width: '80%', lineHeight: '1em', backgroundColor: 'transparent', margin: 'auto' }}>
|
||||
<tbody >
|
||||
<tr>
|
||||
<td style={{ verticalAlign: 'top', padding: 0, width: '2.5em' }} onMouseEnter={termtip.bind(null, 'PHRASE_ENGAGEMENT_RANGE')} onMouseLeave={tooltip.bind(null, null)}>{translate('engagement range')}</td>
|
||||
<td>
|
||||
<Slider
|
||||
axis={true}
|
||||
onChange={this._rangeChange.bind(this)}
|
||||
axisUnit={translate('m')}
|
||||
percent={range}
|
||||
max={maxRange}
|
||||
scale={sizeRatio}
|
||||
onResize={onWindowResize}
|
||||
/>
|
||||
</td>
|
||||
<td className='primary' style={{ width: '10em', verticalAlign: 'top', fontSize: '0.9em', textAlign: 'left' }}>
|
||||
{formats.f2(range * maxRange / 1000)}{units.km}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table></span> : null }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Modules } from 'coriolis-data/dist';
|
||||
import { nameComparator } from '../utils/SlotFunctions';
|
||||
import { CollapseSection, ExpandSection, MountFixed, MountGimballed, MountTurret } from './SvgIcons';
|
||||
import Module from '../shipyard/Module';
|
||||
|
||||
/**
|
||||
* Generates an internationalization friendly weapon comparator that will
|
||||
* sort by specified property (if provided) then by name/group, class, rating
|
||||
* @param {function} translate Translation function
|
||||
* @param {function} propComparator Optional property comparator
|
||||
* @param {boolean} desc Use descending order
|
||||
* @return {function} Comparator function for names
|
||||
*/
|
||||
export function weaponComparator(translate, propComparator, desc) {
|
||||
return (a, b) => {
|
||||
if (!desc) { // Flip A and B if ascending order
|
||||
let t = a;
|
||||
a = b;
|
||||
b = t;
|
||||
}
|
||||
|
||||
// If a property comparator is provided use it first
|
||||
let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a, b);
|
||||
|
||||
if (diff) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
// Property matches so sort by name / group, then class, rating
|
||||
if (a.name === b.name && a.grp === b.grp) {
|
||||
if(a.class == b.class) {
|
||||
return a.rating > b.rating ? 1 : -1;
|
||||
}
|
||||
return a.class - b.class;
|
||||
}
|
||||
|
||||
return nameComparator(translate, a, b);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Damage received by a selected ship
|
||||
*/
|
||||
export default class DamageReceived extends TranslatedComponent {
|
||||
static PropTypes = {
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
code: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._sort = this._sort.bind(this);
|
||||
this._onCollapseExpand = this._onCollapseExpand.bind(this);
|
||||
|
||||
this.state = {
|
||||
predicate: 'n',
|
||||
desc: true,
|
||||
expanded: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the initial weapons state
|
||||
*/
|
||||
componentWillMount() {
|
||||
this.setState({ weapons: this._calcWeapons(this.props.ship) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the updated weapons state
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
* @return {boolean} Returns true if the component should be rerendered
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
this.setState({ weapons: this._calcWeapons(nextProps.ship) });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the damage received by a ship
|
||||
* @param {Object} ship The ship which will receive the damage
|
||||
* @return {boolean} Returns the per-weapon damage
|
||||
*/
|
||||
_calcWeapons(ship) {
|
||||
let weapons = [];
|
||||
|
||||
for (let grp in Modules.hardpoints) {
|
||||
if (Modules.hardpoints[grp][0].damage && Modules.hardpoints[grp][0].damagedist) {
|
||||
for (let mId in Modules.hardpoints[grp]) {
|
||||
const m = new Module(Modules.hardpoints[grp][mId]);
|
||||
const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`;
|
||||
|
||||
// Base DPS
|
||||
const baseDps = m.getDps();
|
||||
const baseSDps = m.getClip() ? (m.getClip() * baseDps / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : baseDps;
|
||||
|
||||
// Effective DPS taking in to account shield resistance
|
||||
let effectivenessShields = 0;
|
||||
if (m.getDamageDist().E) {
|
||||
effectivenessShields += m.getDamageDist().E * (1 - ship.shieldExplRes);
|
||||
}
|
||||
if (m.getDamageDist().K) {
|
||||
effectivenessShields += m.getDamageDist().K * (1 - ship.shieldKinRes);
|
||||
}
|
||||
if (m.getDamageDist().T) {
|
||||
effectivenessShields += m.getDamageDist().T * (1 - ship.shieldThermRes);
|
||||
}
|
||||
if (m.getDamageDist().A) {
|
||||
effectivenessShields += m.getDamageDist().A;
|
||||
}
|
||||
const effectiveDpsShields = baseDps * effectivenessShields;
|
||||
const effectiveSDpsShields = baseSDps * effectivenessShields;
|
||||
|
||||
// Effective DPS taking in to account hull hardness and resistance
|
||||
let effectivenessHull = 0;
|
||||
if (m.getDamageDist().E) {
|
||||
effectivenessHull += m.getDamageDist().E * (1 - ship.hullExplRes);
|
||||
}
|
||||
if (m.getDamageDist().K) {
|
||||
effectivenessHull += m.getDamageDist().K * (1 - ship.hullKinRes);
|
||||
}
|
||||
if (m.getDamageDist().T) {
|
||||
effectivenessHull += m.getDamageDist().T * (1 - ship.hullThermRes);
|
||||
}
|
||||
if (m.getDamageDist().A) {
|
||||
effectivenessHull += m.getDamageDist().A;
|
||||
}
|
||||
effectivenessHull *= Math.min(m.getPiercing() / ship.hardness, 1);
|
||||
const effectiveDpsHull = baseDps * effectivenessHull;
|
||||
const effectiveSDpsHull = baseSDps * effectivenessHull;
|
||||
|
||||
weapons.push({ id: m.id,
|
||||
classRating,
|
||||
name: m.name || m.grp,
|
||||
mount: m.mount,
|
||||
effectiveDpsShields,
|
||||
effectiveSDpsShields,
|
||||
effectivenessShields,
|
||||
effectiveDpsHull,
|
||||
effectiveSDpsHull,
|
||||
effectivenessHull });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return weapons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when the collapse or expand icons are clicked
|
||||
*/
|
||||
_onCollapseExpand() {
|
||||
this.setState({ expanded: !this.state.expanded });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sort order and sort
|
||||
* @param {string} predicate Sort predicate
|
||||
*/
|
||||
_sortOrder(predicate) {
|
||||
let desc = this.state.desc;
|
||||
|
||||
if (predicate == this.state.predicate) {
|
||||
desc = !desc;
|
||||
} else {
|
||||
desc = true;
|
||||
}
|
||||
|
||||
this._sort(this.props.ship, predicate, desc);
|
||||
this.setState({ predicate, desc });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the weapon list
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {string} predicate Sort predicate
|
||||
* @param {Boolean} desc Sort order descending
|
||||
*/
|
||||
_sort(ship, predicate, desc) {
|
||||
let comp = weaponComparator.bind(null, this.context.language.translate);
|
||||
|
||||
switch (predicate) {
|
||||
case 'n': comp = comp(null, desc); break;
|
||||
case 'edpss': comp = comp((a, b) => a.effectiveDpsShields - b.effectiveDpsShields, desc); break;
|
||||
case 'esdpss': comp = comp((a, b) => a.effectiveSDpsShields - b.effectiveSDpsShields, desc); break;
|
||||
case 'es': comp = comp((a, b) => a.effectivenessShields - b.effectivenessShields, desc); break;
|
||||
case 'edpsh': comp = comp((a, b) => a.effectiveDpsHull - b.effectiveDpsHull, desc); break;
|
||||
case 'esdpsh': comp = comp((a, b) => a.effectiveSDpsHull - b.effectiveSDpsHull, desc); break;
|
||||
case 'eh': comp = comp((a, b) => a.effectivenessHull - b.effectivenessHull, desc); break;
|
||||
}
|
||||
|
||||
this.state.weapons.sort(comp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render individual rows for weapons
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Object} formats Localised formats map
|
||||
* @return {array} The individual rows
|
||||
*
|
||||
*/
|
||||
_renderRows(translate, formats) {
|
||||
const { termtip, tooltip } = this.context;
|
||||
|
||||
let rows = [];
|
||||
|
||||
for (let i = 0; i < this.state.weapons.length; i++) {
|
||||
const weapon = this.state.weapons[i];
|
||||
rows.push(<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)}
|
||||
</td>
|
||||
<td>{formats.round1(weapon.effectiveDpsShields)}</td>
|
||||
<td>{formats.round1(weapon.effectiveSDpsShields)}</td>
|
||||
<td>{formats.pct(weapon.effectivenessShields)}</td>
|
||||
<td>{formats.round1(weapon.effectiveDpsHull)}</td>
|
||||
<td>{formats.round1(weapon.effectiveSDpsHull)}</td>
|
||||
<td>{formats.pct(weapon.effectivenessHull)}</td>
|
||||
</tr>);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render damage received
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { language, tooltip, termtip } = this.context;
|
||||
const { formats, translate } = language;
|
||||
const { expanded } = this.state;
|
||||
|
||||
const sortOrder = this._sortOrder;
|
||||
const onCollapseExpand = this._onCollapseExpand;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<h1>{translate('damage received by')} {expanded ? <span onClick={onCollapseExpand}><CollapseSection className='summary'/></span> : <span onClick={onCollapseExpand}><ExpandSection className='summary'/></span>}</h1>
|
||||
{expanded ? <span>
|
||||
<table className='summary' style={{ width: '100%' }}>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th rowSpan={2} className='sortable' onClick={sortOrder.bind(this, 'n')} >{translate('weapon')}</th>
|
||||
<th colSpan={3} >{translate('against shields')}</th>
|
||||
<th colSpan={3} >{translate('against hull')}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className='sortable lft' onClick={sortOrder.bind(this, 'edpss')} onMouseOver={termtip.bind(null, 'dps')} onMouseOut={tooltip.bind(null, null)}>{translate('DPS')}</th>
|
||||
<th className='sortable' onClick={sortOrder.bind(this, 'esdpss')} onMouseOver={termtip.bind(null, 'sdps')} onMouseOut={tooltip.bind(null, null)}>{translate('SDPS')}</th>
|
||||
<th className='sortable' onClick={sortOrder.bind(this, 'es')} >{translate('effectiveness')}</th>
|
||||
<th className='sortable lft' onClick={sortOrder.bind(this, 'edpsh')} onMouseOver={termtip.bind(null, 'dps')} onMouseOut={tooltip.bind(null, null)}>{translate('DPS')}</th>
|
||||
<th className='sortable' onClick={sortOrder.bind(this, 'esdpsh')} onMouseOver={termtip.bind(null, 'sdps')} onMouseOut={tooltip.bind(null, null)}>{translate('SDPS')}</th>
|
||||
<th className='sortable' onClick={sortOrder.bind(this, 'eh')} >{translate('effectiveness')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this._renderRows(translate, formats)}
|
||||
</tbody>
|
||||
</table></span> : null }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
261
src/app/components/Defence.jsx
Normal file
261
src/app/components/Defence.jsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import * as Calc from '../shipyard/Calculations';
|
||||
import PieChart from './PieChart';
|
||||
import VerticalBarChart from './VerticalBarChart';
|
||||
|
||||
/**
|
||||
* Defence information
|
||||
* Shield information consists of four panels:
|
||||
* - textual information (time to lose shields etc.)
|
||||
* - breakdown of shield sources (pie chart)
|
||||
* - comparison of shield resistances (bar chart)
|
||||
* - effective shield (bar chart)
|
||||
*/
|
||||
export default class Defence extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
marker: React.PropTypes.string.isRequired,
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
opponent: React.PropTypes.object.isRequired,
|
||||
engagementrange: React.PropTypes.number.isRequired,
|
||||
sys: React.PropTypes.number.isRequired,
|
||||
opponentWep: React.PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render defence
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { ship, sys, opponentWep } = this.props;
|
||||
const { language, tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { shield, armour, shielddamage, armourdamage } = this.state;
|
||||
|
||||
const pd = ship.standard[4].m;
|
||||
|
||||
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') });
|
||||
|
||||
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>);
|
||||
}
|
||||
|
||||
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>);
|
||||
}
|
||||
|
||||
// Add effective shield from resistances
|
||||
const rawMj = shield.generator + shield.boosters + shield.cells;
|
||||
const explosiveMj = rawMj / (shield.explosive.generator * shield.explosive.boosters) - rawMj;
|
||||
if (explosiveMj != 0) effectiveShieldExplosiveTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(explosiveMj)}{units.MJ}</div>);
|
||||
const kineticMj = rawMj / (shield.kinetic.generator * shield.kinetic.boosters) - rawMj;
|
||||
if (kineticMj != 0) effectiveShieldKineticTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(kineticMj)}{units.MJ}</div>);
|
||||
const thermalMj = rawMj / (shield.thermal.generator * shield.thermal.boosters) - rawMj;
|
||||
if (thermalMj != 0) effectiveShieldThermalTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(thermalMj)}{units.MJ}</div>);
|
||||
|
||||
// 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.sys - rawMj)}{units.MJ}</div>);
|
||||
effectiveShieldExplosiveTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.explosive.sys - rawMj)}{units.MJ}</div>);
|
||||
effectiveShieldKineticTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.kinetic.sys - rawMj)}{units.MJ}</div>);
|
||||
effectiveShieldThermalTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.thermal.sys - rawMj)}{units.MJ}</div>);
|
||||
}
|
||||
}
|
||||
|
||||
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>);
|
||||
|
||||
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>);
|
||||
|
||||
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>);
|
||||
|
||||
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 = [];
|
||||
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>);
|
||||
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>);
|
||||
}
|
||||
}
|
||||
|
||||
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.bulkheads * armour.explosive.reinforcement != 1) effectiveArmourExplosiveTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / (armour.explosive.bulkheads * armour.explosive.reinforcement) - 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.bulkheads * armour.kinetic.reinforcement != 1) effectiveArmourKineticTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / (armour.kinetic.bulkheads * armour.kinetic.reinforcement) - 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.bulkheads * armour.thermal.reinforcement != 1) effectiveArmourThermalTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / (armour.thermal.bulkheads * armour.thermal.reinforcement) - 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 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 });
|
||||
|
||||
return (
|
||||
<span id='defence'>
|
||||
{shield.total ? <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>
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SHIELD_SOURCES'))} onMouseOut={tooltip.bind(null, null)}>{translate('shield sources')}</h2>
|
||||
<PieChart data={shieldSourcesData} />
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_DAMAGE_TAKEN'))} onMouseOut={tooltip.bind(null, null)}>{translate('damage taken')}(%)</h2>
|
||||
<VerticalBarChart data={shieldDamageTakenData} yMax={140} />
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_EFFECTIVE_SHIELD'))} onMouseOut={tooltip.bind(null, null)}>{translate('effective shield')}(MJ)</h2>
|
||||
<VerticalBarChart data={effectiveShieldData} yMax={maxEffectiveShield}/>
|
||||
</div>
|
||||
</span> : null }
|
||||
|
||||
<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>
|
||||
<br/>
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_ARMOUR_SOURCES'))} onMouseOut={tooltip.bind(null, null)}>{translate('armour sources')}</h2>
|
||||
<PieChart data={armourSourcesData} />
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_DAMAGE_TAKEN'))} onMouseOut={tooltip.bind(null, null)}>{translate('damage taken')}(%)</h2>
|
||||
<VerticalBarChart data={armourDamageTakenData} yMax={100} />
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_EFFECTIVE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('effective armour')}</h2>
|
||||
<VerticalBarChart data={effectiveArmourData} />
|
||||
</div>
|
||||
</span>);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons';
|
||||
|
||||
/**
|
||||
* Defence summary
|
||||
*/
|
||||
export default class DefenceSummary extends TranslatedComponent {
|
||||
static PropTypes = {
|
||||
ship: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render defence summary
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
let ship = this.props.ship;
|
||||
let { language, tooltip, termtip } = this.context;
|
||||
let { formats, translate, units } = language;
|
||||
let hide = tooltip.bind(null, null);
|
||||
|
||||
const shieldGenerator = ship.findShieldGenerator();
|
||||
|
||||
// Damage values are 1 - resistance values
|
||||
return (
|
||||
<span>
|
||||
<h1>{translate('defence summary')}</h1>
|
||||
<table className='summary' style={{ marginLeft: 'auto', marginRight: 'auto', lineHeight: '1em', backgroundColor: 'transparent' }}>
|
||||
<tbody>
|
||||
{ship.shield ?
|
||||
<tr>
|
||||
<td colSpan='4' className='summary'><h2>{translate('shields')}: {formats.int(ship.shield)} {units.MJ}</h2></td>
|
||||
</tr> : null }
|
||||
{ship.shield ?
|
||||
<tr>
|
||||
<td className='ri' onMouseEnter={termtip.bind(null, 'PHRASE_SG_RECOVER', { cap: 0 })} onMouseLeave={hide}>{translate('recovery')}</td>
|
||||
<td className='le'>{formats.time(ship.calcShieldRecovery())}</td>
|
||||
<td className='ri' onMouseEnter={termtip.bind(null, 'PHRASE_SG_RECHARGE', { cap: 0 })} onMouseLeave={hide}>{translate('recharge')}</td>
|
||||
<td className='le'>{formats.time(ship.calcShieldRecharge())}</td>
|
||||
</tr> : null }
|
||||
{ship.shield ?
|
||||
<tr>
|
||||
<td className='le'>{translate('damage from')}</td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, 'explosive')} onMouseOut={tooltip.bind(null, null)}><DamageExplosive /></span>
|
||||
<span onMouseOver={termtip.bind(null, translate('base') + ' ' + formats.pct1(1 - shieldGenerator.explres))} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(1 - ship.shieldExplRes)}</span>
|
||||
</td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, 'kinetic')} onMouseOut={tooltip.bind(null, null)}><DamageKinetic /></span>
|
||||
<span onMouseOver={termtip.bind(null, translate('base') + ' ' + formats.pct1(1 - shieldGenerator.kinres))} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(1 - ship.shieldKinRes)}</span>
|
||||
</td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, 'thermal')} onMouseOut={tooltip.bind(null, null)}><DamageThermal /></span>
|
||||
<span onMouseOver={termtip.bind(null, translate('base') + ' ' + formats.pct1(1 - shieldGenerator.thermres))} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(1 - ship.shieldThermRes)}</span>
|
||||
</td>
|
||||
</tr> : null }
|
||||
|
||||
{ ship.shield && ship.shieldCells ?
|
||||
<tr>
|
||||
<td colSpan='4'><h2>{translate('shield cells')}: {formats.int(ship.shieldCells)} {units.MJ}</h2></td>
|
||||
</tr> : null }
|
||||
|
||||
<tr>
|
||||
<td colSpan='4'><h2>{translate('armour')}: {formats.int(ship.armour)}</h2></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='le'>{translate('damage from')}</td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, 'explosive')} onMouseOut={tooltip.bind(null, null)}><DamageExplosive /></span>
|
||||
<span onMouseOver={termtip.bind(null, translate('base') + ' ' + formats.pct1(1 - ship.bulkheads.m.explres))} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(1 - ship.hullExplRes)}</span></td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, 'kinetic')} onMouseOut={tooltip.bind(null, null)}><DamageKinetic /></span>
|
||||
<span onMouseOver={termtip.bind(null, translate('base') + ' ' + formats.pct1(1 - ship.bulkheads.m.kinres))} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(1 - ship.hullKinRes)}</span>
|
||||
</td>
|
||||
<td className='ri'>
|
||||
<span onMouseOver={termtip.bind(null, 'thermal')} onMouseOut={tooltip.bind(null, null)}><DamageThermal /></span>
|
||||
<span onMouseOver={termtip.bind(null, translate('base') + ' ' + formats.pct1(1 - ship.bulkheads.m.thermres))} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(1 - ship.hullThermRes)}</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{ship.modulearmour > 0 ?
|
||||
<tr>
|
||||
<td colSpan='4'><h2>{translate('module armour')}: {formats.int(ship.modulearmour)}</h2></td>
|
||||
</tr> : null }
|
||||
|
||||
{ship.moduleprotection > 0 ?
|
||||
<tr>
|
||||
<td colSpan='2' className='cn'>{translate('internal protection')} {formats.pct1(ship.moduleprotection)}</td>
|
||||
<td colSpan='2' className='cn'>{translate('external protection')} {formats.pct1(ship.moduleprotection / 2)}</td>
|
||||
</tr> : null }
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
101
src/app/components/EngagementRange.jsx
Normal file
101
src/app/components/EngagementRange.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import Slider from '../components/Slider';
|
||||
|
||||
/**
|
||||
* Engagement range slider
|
||||
* Requires an onChange() function of the form onChange(range), providing the range in metres, which is triggered on range change
|
||||
*/
|
||||
export default class EngagementRange extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
engagementRange: React.PropTypes.number.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
const { ship } = props;
|
||||
|
||||
const maxRange = this._calcMaxRange(ship);
|
||||
|
||||
this.state = {
|
||||
maxRange
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
_rangeChange(rangeLevel) {
|
||||
const { maxRange } = this.state;
|
||||
|
||||
// We round the range to an integer value
|
||||
const range = Math.round(rangeLevel * maxRange);
|
||||
|
||||
if (range !== this.props.engagementRange) {
|
||||
this.props.onChange(range);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render range slider
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { engagementRange } = this.props;
|
||||
const { maxRange } = this.state;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<h3>{translate('engagement range')}: {formats.int(engagementRange)}{translate('m')}</h3>
|
||||
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
|
||||
<tbody >
|
||||
<tr>
|
||||
<td>
|
||||
<Slider
|
||||
axis={true}
|
||||
onChange={this._rangeChange.bind(this)}
|
||||
axisUnit={translate('m')}
|
||||
percent={engagementRange / maxRange}
|
||||
max={maxRange}
|
||||
scale={sizeRatio}
|
||||
onResize={onWindowResize}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
105
src/app/components/EngineProfile.jsx
Normal file
105
src/app/components/EngineProfile.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import { nameComparator } from '../utils/SlotFunctions';
|
||||
import LineChart from '../components/LineChart';
|
||||
import Slider from '../components/Slider';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import Module from '../shipyard/Module';
|
||||
import * as Calc from '../shipyard/Calculations';
|
||||
|
||||
/**
|
||||
* Engine profile for a given ship
|
||||
*/
|
||||
export default class EngineProfile extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
cargo: React.PropTypes.number.isRequired,
|
||||
fuel: React.PropTypes.number.isRequired,
|
||||
eng: React.PropTypes.number.isRequired,
|
||||
boost: React.PropTypes.bool.isRequired,
|
||||
marker: React.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;
|
||||
|
||||
// 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}`;
|
||||
|
||||
// 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}
|
||||
xLabel={translate('mass')}
|
||||
xUnit={translate('T')}
|
||||
yLabel={translate('maximum speed')}
|
||||
yUnit={translate('m/s')}
|
||||
func={this.state.calcMaxSpeedFunc}
|
||||
points={1000}
|
||||
code={code}
|
||||
aspect={0.7}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
103
src/app/components/FSDProfile.jsx
Normal file
103
src/app/components/FSDProfile.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import { nameComparator } from '../utils/SlotFunctions';
|
||||
import LineChart from '../components/LineChart';
|
||||
import Slider from '../components/Slider';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import Module from '../shipyard/Module';
|
||||
import * as Calc from '../shipyard/Calculations';
|
||||
|
||||
/**
|
||||
* FSD profile for a given ship
|
||||
*/
|
||||
export default class FSDProfile extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
cargo: React.PropTypes.number.isRequired,
|
||||
fuel: React.PropTypes.number.isRequired,
|
||||
marker: React.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()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
// Add a mark at our current mass
|
||||
const mark = Math.min(mass, maxMass);
|
||||
|
||||
const code = ship.name + ship.toString() + '.' + fuel;
|
||||
|
||||
return (
|
||||
<LineChart
|
||||
xMin={minMass}
|
||||
xMax={maxMass}
|
||||
yMin={minRange}
|
||||
yMax={maxRange}
|
||||
xMark={mark}
|
||||
xLabel={translate('mass')}
|
||||
xUnit={translate('T')}
|
||||
yLabel={translate('maximum range')}
|
||||
yUnit={translate('LY')}
|
||||
func={this.state.calcMaxRangeFunc}
|
||||
points={200}
|
||||
code={code}
|
||||
aspect={0.7}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
74
src/app/components/Fuel.jsx
Normal file
74
src/app/components/Fuel.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import Slider from '../components/Slider';
|
||||
|
||||
/**
|
||||
* Fuel slider
|
||||
* Requires an onChange() function of the form onChange(fuel), providing the fuel in tonnes, which is triggered on fuel level change
|
||||
*/
|
||||
export default class Fuel extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
fuel: React.PropTypes.number.isRequired,
|
||||
fuelCapacity: React.PropTypes.number.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
this._fuelChange = this._fuelChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update fuel level
|
||||
* @param {number} fuelLevel percentage level from 0 to 1
|
||||
*/
|
||||
_fuelChange(fuelLevel) {
|
||||
const { fuel, fuelCapacity } = this.props;
|
||||
|
||||
const newFuel = fuelLevel * fuelCapacity;
|
||||
// Only send an update if the fuel has changed significantly
|
||||
if (Math.round(fuel * 10) != Math.round(newFuel * 10)) {
|
||||
this.props.onChange(Math.round(newFuel * 10) / 10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render fuel slider
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
const { fuel, fuelCapacity } = this.props;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<h3>{translate('fuel carried')}: {formats.f1(fuel)}{units.T}</h3>
|
||||
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
|
||||
<tbody >
|
||||
<tr>
|
||||
<td>
|
||||
<Slider
|
||||
axis={true}
|
||||
onChange={this._fuelChange}
|
||||
axisUnit={translate('T')}
|
||||
percent={fuel / fuelCapacity}
|
||||
max={fuelCapacity}
|
||||
scale={sizeRatio}
|
||||
onResize={onWindowResize}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import Slot from './Slot';
|
||||
import Persist from '../stores/Persist';
|
||||
import { DamageKinetic, DamageThermal, DamageExplosive, MountFixed, MountGimballed, MountTurret, ListModifications, Modified } from './SvgIcons';
|
||||
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';
|
||||
|
||||
|
||||
/**
|
||||
@@ -48,9 +49,15 @@ export default class HardpointSlot extends Slot {
|
||||
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) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className='details' draggable='true' onDragStart={drag} onDragEnd={drop}>
|
||||
@@ -62,6 +69,7 @@ export default class HardpointSlot extends Slot {
|
||||
{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>
|
||||
|
||||
@@ -73,18 +81,20 @@ export default class HardpointSlot extends Slot {
|
||||
{ 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.getClip() * m.getHps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload())) })</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')} {formats.f1(m.getRange() / 1000)}{u.km}</div> : null }
|
||||
{ m.getFalloff() ? <div className={'l'}>{translate('falloff')} {formats.f1(m.getFalloff() / 1000)}{u.km}</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('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 }
|
||||
{ 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' ><button onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
/**
|
||||
* Hardpoint slot section
|
||||
*/
|
||||
export default class HardpointsSlotSection extends SlotSection {
|
||||
export default class HardpointSlotSection extends SlotSection {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -5,12 +5,13 @@ import { Insurance } from '../shipyard/Constants';
|
||||
import Link from './Link';
|
||||
import ActiveLink from './ActiveLink';
|
||||
import cn from 'classnames';
|
||||
import { Cogs, CoriolisLogo, Hammer, Rocket, StatsBars } from './SvgIcons';
|
||||
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 ModalDeleteAll from './ModalDeleteAll';
|
||||
import ModalExport from './ModalExport';
|
||||
import ModalHelp from './ModalHelp';
|
||||
import ModalImport from './ModalImport';
|
||||
import Slider from './Slider';
|
||||
import { outfitURL } from '../utils/UrlGenerators';
|
||||
@@ -53,11 +54,11 @@ function selectAll(e) {
|
||||
*/
|
||||
export default class Header extends TranslatedComponent {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this.shipOrder = Object.keys(Ships).sort();
|
||||
@@ -74,6 +75,7 @@ export default class Header extends TranslatedComponent {
|
||||
this._openBuilds = this._openMenu.bind(this, 'b');
|
||||
this._openComp = this._openMenu.bind(this, 'comp');
|
||||
this._openSettings = this._openMenu.bind(this, 'settings');
|
||||
this._showHelp = this._showHelp.bind(this);
|
||||
this.languageOptions = [];
|
||||
this.insuranceOptions = [];
|
||||
this.state = {
|
||||
@@ -248,6 +250,17 @@ export default class Header extends TranslatedComponent {
|
||||
/>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show help modal
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_showHelp(e) {
|
||||
let translate = this.context.language.translate;
|
||||
e.preventDefault();
|
||||
|
||||
this.context.showModal(<ModalHelp title={translate('help')} />);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show import modal
|
||||
* @param {SyntheticEvent} e Event
|
||||
@@ -343,7 +356,7 @@ export default class Header extends TranslatedComponent {
|
||||
let comps = Object.keys(Persist.getComparisons()).sort();
|
||||
|
||||
for (let name of comps) {
|
||||
comparisons.push(<ActiveLink key={name} href={'/compare/' + name} className='block name'>{name}</ActiveLink>);
|
||||
comparisons.push(<ActiveLink key={name} href={'/compare/' + encodeURIComponent(name)} className='block name'>{name}</ActiveLink>);
|
||||
}
|
||||
} else {
|
||||
comparisons = <span className='cap'>{translate('none created')}</span>;
|
||||
@@ -521,6 +534,12 @@ export default class Header extends TranslatedComponent {
|
||||
</div>
|
||||
{openedMenu == 'settings' ? this._getSettingsMenu() : null}
|
||||
</div>
|
||||
|
||||
<div className='r menu'>
|
||||
<div className={cn('menu-header')} onClick={this._showHelp}>
|
||||
<Help className='xl warning'/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -30,6 +31,12 @@ export default class InternalSlot extends Slot {
|
||||
let modTT = translate('modified');
|
||||
if (m && m.blueprint && m.blueprint.name) {
|
||||
modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
|
||||
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;
|
||||
@@ -39,36 +46,38 @@ export default class InternalSlot extends Slot {
|
||||
<div className={'r'}>{formats.round(mass)}{u.T}</div>
|
||||
</div>
|
||||
<div className={'cb'}>
|
||||
{ m.getOptMass() ? <div className={'l'}>{translate('optimal mass')}: {formats.int(m.getOptMass())}{u.T}</div> : null }
|
||||
{ m.getMaxMass() ? <div className={'l'}>{translate('max mass')}: {formats.int(m.getMaxMass())}{u.T}</div> : null }
|
||||
{ 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() ? <div className={'l'}>{translate('ammunition')}: {formats.gen(m.getAmmo())}</div> : null }
|
||||
{ m.cells ? <div className={'l'}>{translate('cells')}: {m.cells}</div> : null }
|
||||
{ m.shieldreinforcement ? <div className={'l'}>{translate('shieldreinforcement')}: {formats.int(m.getShieldReinforcement())} <u>MJ</u> {translate('total')}: {formats.int(m.cells * m.getShieldReinforcement())}{u.MJ}</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.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.spinup ? <div className={'l'}>{translate('spinup')}: {formats.f1(m.spinup)}{u.s}</div> : null }
|
||||
{ m.time ? <div className={'l'}>{translate('time')}: {formats.time(m.time)}</div> : null }
|
||||
{ m.getTime() ? <div className={'l'}>{translate('time')}: {formats.time(m.getTime())}</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.getHullReinforcement() ? <div className={'l'}>+{formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost') / 10000)} <u className='cap'>{translate('armour')}</u></div> : null }
|
||||
{ m.getProtection() ? <div className={'l'}>{formats.rPct(m.getProtection())} <u className='cap'>{translate('protection')}</u></div> : null }
|
||||
{ m.maximum ? <div className={'l'}>{translate('max')}: {(m.maximum)}</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 }
|
||||
{ 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 }
|
||||
|
||||
{ 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' ><button onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
|
||||
123
src/app/components/JumpRange.jsx
Normal file
123
src/app/components/JumpRange.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import { nameComparator } from '../utils/SlotFunctions';
|
||||
import LineChart from '../components/LineChart';
|
||||
import Slider from '../components/Slider';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import Module from '../shipyard/Module';
|
||||
import * as Calc from '../shipyard/Calculations';
|
||||
|
||||
/**
|
||||
* Jump range for a given ship
|
||||
*/
|
||||
export default class JumpRange extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
code: React.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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import d3 from 'd3';
|
||||
import Measure from 'react-measure';
|
||||
import * as d3 from 'd3';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
|
||||
const RENDER_POINTS = 20; // Only render 20 points on the graph
|
||||
const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 };
|
||||
|
||||
/**
|
||||
@@ -11,24 +11,30 @@ const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 };
|
||||
export default class LineChart extends TranslatedComponent {
|
||||
|
||||
static defaultProps = {
|
||||
code: '',
|
||||
xMin: 0,
|
||||
yMin: 0,
|
||||
colors: ['#ff8c0d']
|
||||
points: 20,
|
||||
colors: ['#ff8c0d'],
|
||||
aspect: 0.5
|
||||
};
|
||||
|
||||
static PropTypes = {
|
||||
width: React.PropTypes.number.isRequired,
|
||||
static propTypes = {
|
||||
func: React.PropTypes.func.isRequired,
|
||||
xLabel: React.PropTypes.string.isRequired,
|
||||
xMin: React.PropTypes.number,
|
||||
xMax: React.PropTypes.number.isRequired,
|
||||
xUnit: React.PropTypes.string.isRequired,
|
||||
xMark: React.PropTypes.number,
|
||||
yLabel: React.PropTypes.string.isRequired,
|
||||
yMin: React.PropTypes.number,
|
||||
yMax: React.PropTypes.number.isRequired,
|
||||
yUnit: React.PropTypes.string.isRequired,
|
||||
yUnit: React.PropTypes.string,
|
||||
series: React.PropTypes.array,
|
||||
colors: React.PropTypes.array,
|
||||
points: React.PropTypes.number,
|
||||
aspect: React.PropTypes.number,
|
||||
code: React.PropTypes.string,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -40,38 +46,30 @@ export default class LineChart extends TranslatedComponent {
|
||||
super(props);
|
||||
|
||||
this._updateDimensions = this._updateDimensions.bind(this);
|
||||
this._updateSeriesData = this._updateSeriesData.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);
|
||||
|
||||
let markerElems = [];
|
||||
let detailElems = [<text key='lbl' className='text-tip x' y='1.25em'/>];
|
||||
let xScale = d3.scale.linear();
|
||||
let xAxisScale = d3.scale.linear();
|
||||
let yScale = d3.scale.linear();
|
||||
let series = props.series;
|
||||
let seriesLines = [];
|
||||
const series = props.series;
|
||||
|
||||
this.xAxis = d3.svg.axis().scale(xAxisScale).outerTickSize(0).orient('bottom');
|
||||
this.yAxis = d3.svg.axis().scale(yScale).ticks(6).outerTickSize(0).orient('left');
|
||||
let xScale = d3.scaleLinear();
|
||||
let yScale = d3.scaleLinear();
|
||||
let xAxisScale = d3.scaleLinear();
|
||||
|
||||
for(let i = 0, l = series ? series.length : 1; i < l; i++) {
|
||||
let yAccessor = series ? function(d) { return yScale(d[1][this]); }.bind(series[i]) : (d) => yScale(d[1]);
|
||||
seriesLines.push(d3.svg.line().x((d) => xScale(d[0])).y(yAccessor));
|
||||
detailElems.push(<text key={i} className='text-tip y' y={1.25 * (i + 2) + 'em'}/>);
|
||||
markerElems.push(<circle key={i} className='marker' r='4' />);
|
||||
}
|
||||
this.xAxis = d3.axisBottom(xAxisScale).tickSizeOuter(0);
|
||||
this.yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0);
|
||||
|
||||
this.state = {
|
||||
xScale,
|
||||
xAxisScale,
|
||||
yScale,
|
||||
seriesLines,
|
||||
detailElems,
|
||||
markerElems,
|
||||
tipHeight: 2 + (1.2 * (series ? series.length : 0.8))
|
||||
tipHeight: 2 + (1.2 * (series ? series.length : 0.8)),
|
||||
dimensions: {
|
||||
width: 100,
|
||||
height: 100
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,13 +79,14 @@ export default class LineChart extends TranslatedComponent {
|
||||
*/
|
||||
_tooltip(xPos) {
|
||||
let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props;
|
||||
let { xScale, yScale, innerWidth } = this.state;
|
||||
let { xScale, yScale } = this.state;
|
||||
let { width } = this.state.dimensions;
|
||||
let { formats, translate } = this.context.language;
|
||||
let x0 = xScale.invert(xPos),
|
||||
y0 = func(x0),
|
||||
tips = this.tipContainer,
|
||||
yTotal = 0,
|
||||
flip = (xPos / innerWidth > 0.60),
|
||||
flip = (xPos / width > 0.50),
|
||||
tipWidth = 0,
|
||||
tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height;
|
||||
|
||||
@@ -98,7 +97,7 @@ export default class LineChart extends TranslatedComponent {
|
||||
let yVal = series ? y0[series[i]] : y0;
|
||||
yTotal += yVal;
|
||||
return (series ? translate(series[i]) : '') + ' ' + formats.f2(yVal);
|
||||
}).append('tspan').attr('class', 'metric').text(' ' + yUnit);
|
||||
}).append('tspan').attr('class', 'metric').text(yUnit ? ' ' + yUnit : '');
|
||||
|
||||
tips.selectAll('text').each(function() {
|
||||
if (this.getBBox().width > tipWidth) {
|
||||
@@ -118,19 +117,21 @@ export default class LineChart extends TranslatedComponent {
|
||||
|
||||
/**
|
||||
* Update dimensions based on properties and scale
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} props React Component properties
|
||||
* @param {number} scale size ratio / scale
|
||||
* @returns {Object} calculated dimensions
|
||||
*/
|
||||
_updateDimensions(props, scale) {
|
||||
let { width, xMax, xMin, yMin, yMax } = props;
|
||||
let innerWidth = width - MARGIN.left - MARGIN.right;
|
||||
let outerHeight = Math.round(width * 0.5 * scale);
|
||||
let innerHeight = outerHeight - MARGIN.top - MARGIN.bottom;
|
||||
const { xMax, xMin, yMin, yMax } = props;
|
||||
const { width, height } = this.state.dimensions;
|
||||
const innerWidth = width - MARGIN.left - MARGIN.right;
|
||||
const outerHeight = Math.round(width * props.aspect);
|
||||
const innerHeight = outerHeight - MARGIN.top - MARGIN.bottom;
|
||||
|
||||
this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true);
|
||||
this.state.xAxisScale.range([0, innerWidth]).domain([xMin, xMax]).clamp(true);
|
||||
this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax]);
|
||||
this.setState({ innerWidth, outerHeight, innerHeight });
|
||||
this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax + (yMax - yMin) * 0.1]); // 10% higher than maximum value for tooltip visibility
|
||||
return { innerWidth, outerHeight, innerHeight };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,35 +165,47 @@ export default class LineChart extends TranslatedComponent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update series data generated from props
|
||||
* @param {Object} props React Component properties
|
||||
* Update series generated from props
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} state React Component state
|
||||
*/
|
||||
_updateSeriesData(props) {
|
||||
let { func, xMin, xMax, series } = props;
|
||||
let delta = (xMax - xMin) / RENDER_POINTS;
|
||||
let seriesData = new Array(RENDER_POINTS);
|
||||
_updateSeries(props, state) {
|
||||
let { func, xMin, xMax, series, points } = props;
|
||||
let delta = (xMax - xMin) / points;
|
||||
let seriesData = new Array(points);
|
||||
|
||||
if (delta) {
|
||||
seriesData = new Array(RENDER_POINTS);
|
||||
for (let i = 0, x = xMin; i < RENDER_POINTS; i++) {
|
||||
seriesData = new Array(points);
|
||||
for (let i = 0, x = xMin; i < points; i++) {
|
||||
seriesData[i] = [x, func(x)];
|
||||
x += delta;
|
||||
}
|
||||
seriesData[RENDER_POINTS - 1] = [xMax, func(xMax)];
|
||||
seriesData[points - 1] = [xMax, func(xMax)];
|
||||
} else {
|
||||
let yVal = func(xMin);
|
||||
seriesData = [[0, yVal], [1, yVal]];
|
||||
}
|
||||
|
||||
this.setState({ seriesData });
|
||||
const markerElems = [];
|
||||
const detailElems = [<text key='lbl' className='text-tip x' y='1.25em'/>];
|
||||
const seriesLines = [];
|
||||
for (let i = 0, l = series ? series.length : 1; i < l; i++) {
|
||||
const yAccessor = series ? function(d) { return state.yScale(d[1][this]); }.bind(series[i]) : (d) => state.yScale(d[1]);
|
||||
seriesLines.push(d3.line().x((d, i) => this.state.xScale(d[0])).y(yAccessor));
|
||||
detailElems.push(<text key={i} className='text-tip y' strokeWidth={0} fill={props.colors[i]} y={1.25 * (i + 2) + 'em'}/>);
|
||||
markerElems.push(<circle key={i} className='marker' r='4' />);
|
||||
}
|
||||
|
||||
const tipHeight = 2 + (1.2 * (seriesLines ? seriesLines.length : 0.8));
|
||||
|
||||
this.setState({ markerElems, detailElems, seriesLines, seriesData, tipHeight });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dimensions and series data based on props and context.
|
||||
*/
|
||||
componentWillMount() {
|
||||
this._updateDimensions(this.props, this.context.sizeRatio);
|
||||
this._updateSeriesData(this.props);
|
||||
this._updateSeries(this.props, this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,17 +214,10 @@ export default class LineChart extends TranslatedComponent {
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
let { func, xMin, xMax, yMin, yMax, width } = nextProps;
|
||||
let props = this.props;
|
||||
const props = this.props;
|
||||
|
||||
let domainChanged = xMax != props.xMax || xMin != props.xMin || yMax != props.yMax || yMin != props.yMin || func != props.func;
|
||||
|
||||
if (width != props.width || domainChanged || this.context.sizeRatio != nextContext.sizeRatio) {
|
||||
this._updateDimensions(nextProps, nextContext.sizeRatio);
|
||||
}
|
||||
|
||||
if (domainChanged) {
|
||||
this._updateSeriesData(nextProps);
|
||||
if (props.code != nextProps.code) {
|
||||
this._updateSeries(nextProps, this.state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,49 +226,57 @@ export default class LineChart extends TranslatedComponent {
|
||||
* @return {React.Component} Chart SVG
|
||||
*/
|
||||
render() {
|
||||
if (!this.props.width) {
|
||||
return null;
|
||||
}
|
||||
const { innerWidth, outerHeight, innerHeight } = this._updateDimensions(this.props, this.context.sizeRatio);
|
||||
const { width, height } = this.state.dimensions;
|
||||
const { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props;
|
||||
const { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state;
|
||||
const line = this.line;
|
||||
const lines = seriesLines.map((line, i) => <path key={i} className='line' fill='none' stroke={colors[i]} strokeWidth='1' d={line(seriesData)} />).reverse();
|
||||
|
||||
let { xLabel, yLabel, xUnit, yUnit, colors } = this.props;
|
||||
let { innerWidth, outerHeight, innerHeight, tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state;
|
||||
let line = this.line;
|
||||
let lines = seriesLines.map((line, i) => <path key={i} className='line' stroke={colors[i]} strokeWidth='2' d={line(seriesData)} />);
|
||||
const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0;
|
||||
const xmark = xMark ? <path key={'mark'} className='line' fill='none' strokeDasharray='5,5' stroke={'#ff8c0d'} strokeWidth='1' d={'M ' + markX + ' ' + innerHeight + ' L ' + markX + ' 0'} /> : '';
|
||||
|
||||
return <svg style={{ width: '100%', height: outerHeight }}>
|
||||
<g transform={`translate(${MARGIN.left},${MARGIN.top})`}>
|
||||
<g>{lines}</g>
|
||||
<g className='x axis' ref={(elem) => d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}>
|
||||
<text className='cap' y='30' dy='.1em' x={innerWidth / 2} style={{ textAnchor: 'middle' }}>
|
||||
<tspan>{xLabel}</tspan>
|
||||
<tspan className='metric'> ({xUnit})</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g className='y axis' ref={(elem) => d3.select(elem).call(this.yAxis)}>
|
||||
<text className='cap' transform='rotate(-90)' y='-50' dy='.1em' x={innerHeight / -2} style={{ textAnchor: 'middle' }}>
|
||||
<tspan>{yLabel}</tspan>
|
||||
<tspan className='metric'> ({yUnit})</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g ref={(g) => this.tipContainer = d3.select(g)} style={{ display: 'none' }}>
|
||||
<rect className='tooltip' height={tipHeight + 'em'}></rect>
|
||||
{detailElems}
|
||||
</g>
|
||||
<g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}>
|
||||
{markerElems}
|
||||
</g>
|
||||
<rect
|
||||
fillOpacity='0'
|
||||
height={innerHeight}
|
||||
width={innerWidth + 1}
|
||||
onMouseEnter={this._showTip}
|
||||
onTouchStart={this._showTip}
|
||||
onMouseLeave={this._hideTip}
|
||||
onTouchEnd={this._hideTip}
|
||||
onMouseMove={this._moveTip}
|
||||
onTouchMove={this._moveTip}
|
||||
/>
|
||||
</g>
|
||||
</svg>;
|
||||
return (
|
||||
<Measure width='100%' whitelist={['width', 'top']} onMeasure={ (dimensions) => { this.setState({ dimensions }); }}>
|
||||
<div width={width} height={height}>
|
||||
<svg style={{ width: '100%', height: outerHeight }}>
|
||||
<g transform={`translate(${MARGIN.left},${MARGIN.top})`}>
|
||||
<g>{xmark}</g>
|
||||
<g>{lines}</g>
|
||||
<g className='x axis' ref={(elem) => d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}>
|
||||
<text className='cap' y='30' dy='.1em' x={innerWidth / 2} style={{ textAnchor: 'middle' }}>
|
||||
<tspan>{xLabel}</tspan>
|
||||
<tspan className='metric'> ({xUnit})</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g className='y axis' ref={(elem) => d3.select(elem).call(this.yAxis)}>
|
||||
<text className='cap' transform='rotate(-90)' y='-50' dy='.1em' x={innerHeight / -2} style={{ textAnchor: 'middle' }}>
|
||||
<tspan>{yLabel}</tspan>
|
||||
{ yUnit && <tspan className='metric'> ({yUnit})</tspan> }
|
||||
</text>
|
||||
</g>
|
||||
<g ref={(g) => this.tipContainer = d3.select(g)} style={{ display: 'none' }}>
|
||||
<rect className='tooltip' height={tipHeight + 'em'}></rect>
|
||||
{detailElems}
|
||||
</g>
|
||||
<g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}>
|
||||
{markerElems}
|
||||
</g>
|
||||
<rect
|
||||
fillOpacity='0'
|
||||
height={innerHeight}
|
||||
width={innerWidth + 1}
|
||||
onMouseEnter={this._showTip}
|
||||
onTouchStart={this._showTip}
|
||||
onMouseLeave={this._hideTip}
|
||||
onTouchEnd={this._hideTip}
|
||||
onMouseMove={this._moveTip}
|
||||
onTouchMove={this._moveTip}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
48
src/app/components/ModalHelp.jsx
Normal file
48
src/app/components/ModalHelp.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
/* eslint react/no-danger: 0 */
|
||||
import React from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
|
||||
/**
|
||||
* Help Modal
|
||||
*/
|
||||
export default class ModalHelp extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: React.PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus on textarea and select all
|
||||
*/
|
||||
componentDidMount() {
|
||||
const e = findDOMNode(this.refs.exportField);
|
||||
if (e) {
|
||||
e.focus();
|
||||
e.select();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modal
|
||||
* @return {React.Component} Modal Content
|
||||
*/
|
||||
render() {
|
||||
const translate = this.context.language.translate;
|
||||
const text = translate('HELP_TEXT');
|
||||
|
||||
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
||||
<h2>{translate(this.props.title || 'Help')}</h2>
|
||||
<div dangerouslySetInnerHTML={{ __html: text }} />
|
||||
<button className='r dismiss cap' onClick={this.context.hideModal}>{translate('close')}</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import cn from 'classnames';
|
||||
import NumberEditor from 'react-number-editor';
|
||||
@@ -38,7 +37,7 @@ export default class Modification extends TranslatedComponent {
|
||||
const name = this.props.name;
|
||||
|
||||
let scaledValue = Math.round(Number(value) * 100);
|
||||
// Limit to +1000% / -100%
|
||||
// Limit to +1000% / -99.99%
|
||||
if (scaledValue > 100000) {
|
||||
scaledValue = 100000;
|
||||
value = 1000;
|
||||
@@ -50,9 +49,15 @@ export default class Modification extends TranslatedComponent {
|
||||
|
||||
let m = this.props.m;
|
||||
let ship = this.props.ship;
|
||||
ship.setModification(m, name, scaledValue);
|
||||
ship.setModification(m, name, scaledValue, true);
|
||||
|
||||
this.setState({ value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when an update to slider value is finished i.e. when losing focus
|
||||
*/
|
||||
_updateFinished() {
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
@@ -72,7 +77,7 @@ export default class Modification extends TranslatedComponent {
|
||||
let symbol;
|
||||
if (name === 'jitter') {
|
||||
symbol = '°';
|
||||
} else if (name !== 'burst') {
|
||||
} else if (name !== 'burst' && name != 'burstrof') {
|
||||
symbol = '%';
|
||||
}
|
||||
if (symbol) {
|
||||
@@ -80,8 +85,8 @@ export default class Modification extends TranslatedComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'cb'} key={name}>
|
||||
<div className={'cb'}>{translate(name)}{symbol}</div>
|
||||
<div onBlur={this._updateFinished.bind(this)} className={'cb'} key={name}>
|
||||
<div className={'cb'}>{translate(name, m.grp)}{symbol}</div>
|
||||
<NumberEditor className={'cb'} style={{ width: '90%', textAlign: 'center' }} step={0.01} stepModifier={1} decimals={2} value={this.state.value} onValueChange={this._updateValue.bind(this)} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import * as _ from 'lodash';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { isEmpty, stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
import cn from 'classnames';
|
||||
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
|
||||
import { Modifications } from 'coriolis-data/dist';
|
||||
import Modification from './Modification';
|
||||
import { getBlueprint, blueprintTooltip } from '../utils/BlueprintFunctions';
|
||||
|
||||
/**
|
||||
* Modifications menu
|
||||
@@ -16,6 +15,7 @@ export default class ModificationsMenu extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
m: React.PropTypes.object.isRequired,
|
||||
marker: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -26,54 +26,88 @@ export default class ModificationsMenu extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this.state = this._initState(props, context);
|
||||
|
||||
this._toggleBlueprintsMenu = this._toggleBlueprintsMenu.bind(this);
|
||||
this._toggleSpecialsMenu = this._toggleSpecialsMenu.bind(this);
|
||||
this._rollWorst = this._rollWorst.bind(this);
|
||||
this._rollRandom = this._rollRandom.bind(this);
|
||||
this._rollBest = this._rollBest.bind(this);
|
||||
this._rollExtreme = this._rollExtreme.bind(this);
|
||||
this._reset = this._reset.bind(this);
|
||||
|
||||
this.state = {
|
||||
blueprintMenuOpened: false,
|
||||
specialMenuOpened: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise state
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
* Render the blueprints
|
||||
* @param {Object} props React component properties
|
||||
* @param {Object} context React component context
|
||||
* @return {Object} list: Array of React Components
|
||||
*/
|
||||
_initState(props, context) {
|
||||
let { m, onChange, ship } = props;
|
||||
_renderBlueprints(props, context) {
|
||||
const { m } = props;
|
||||
const { language, tooltip, termtip } = context;
|
||||
const translate = language.translate;
|
||||
|
||||
let blueprints = [];
|
||||
const blueprints = [];
|
||||
for (const blueprintName in Modifications.modules[m.grp].blueprints) {
|
||||
for (const grade of Modifications.modules[m.grp].blueprints[blueprintName]) {
|
||||
const close = this._blueprintSelected.bind(this, Modifications.blueprints[blueprintName].id, grade);
|
||||
const blueprint = getBlueprint(blueprintName, m);
|
||||
let blueprintGrades = [];
|
||||
for (let grade in Modifications.modules[m.grp].blueprints[blueprintName].grades) {
|
||||
// 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 close = this._blueprintSelected.bind(this, blueprintName, grade);
|
||||
const key = blueprintName + ':' + grade;
|
||||
blueprints.push(<div style={{ cursor: 'pointer' }} key={ key } onClick={ close }>{Modifications.blueprints[blueprintName].name} grade {grade}</div>);
|
||||
const tooltipContent = blueprintTooltip(translate, blueprint.grades[grade], Modifications.modules[m.grp].blueprints[blueprintName].grades[grade].engineers, m.grp);
|
||||
blueprintGrades.unshift(<li key={key} className={classes} style={{ width: '2em' }} onMouseOver={termtip.bind(null, tooltipContent)} onMouseOut={tooltip.bind(null, null)} onClick={close}>{grade}</li>);
|
||||
}
|
||||
if (blueprintGrades) {
|
||||
blueprints.push(<div key={blueprint.name} className={'select-group cap'}>{translate(blueprint.name)}</div>);
|
||||
blueprints.push(<ul key={blueprintName}>{blueprintGrades}</ul>);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the modifications
|
||||
const modifications = this._setModifications(props);
|
||||
|
||||
const blueprintMenuOpened = false;
|
||||
|
||||
// Set up the specials for this module
|
||||
// const specials = _selectSpecials(m);
|
||||
|
||||
return { blueprintMenuOpened, blueprints, modifications };
|
||||
return blueprints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the modifications
|
||||
* Render the specials
|
||||
* @param {Object} props React component properties
|
||||
* @param {Object} context React component context
|
||||
* @return {Object} list: Array of React Components
|
||||
*/
|
||||
_renderSpecials(props, context) {
|
||||
const { m } = props;
|
||||
const { language, tooltip, termtip } = context;
|
||||
const translate = language.translate;
|
||||
|
||||
const specials = [];
|
||||
if (Modifications.modules[m.grp].specials && Modifications.modules[m.grp].specials.length > 0) {
|
||||
const close = this._specialSelected.bind(this, null);
|
||||
specials.push(<div style={{ cursor: 'pointer' }} key={ 'none' } onClick={ close }>{translate('PHRASE_NO_SPECIAL')}</div>);
|
||||
for (const specialName of Modifications.modules[m.grp].specials) {
|
||||
const close = this._specialSelected.bind(this, specialName);
|
||||
specials.push(<div style={{ cursor: 'pointer' }} key={ specialName } onClick={ close }>{translate(Modifications.specials[specialName].name)}</div>);
|
||||
}
|
||||
}
|
||||
return specials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modifications
|
||||
* @param {Object} props React Component properties
|
||||
* @return {Object} list: Array of React Components
|
||||
*/
|
||||
_setModifications(props) {
|
||||
_renderModifications(props) {
|
||||
const { m, onChange, ship } = props;
|
||||
let modifications = [];
|
||||
const modifications = [];
|
||||
for (const modName of Modifications.modules[m.grp].modifications) {
|
||||
if (Modifications.modifications[modName].type === 'percentage' || Modifications.modifications[modName].type === 'numeric') {
|
||||
if (!Modifications.modifications[modName].hidden) {
|
||||
const key = modName + (m.getModValue(modName) / 100 || 0);
|
||||
modifications.push(<Modification key={ key } ship={ ship } m={ m } name={ modName } value={ m.getModValue(modName) / 100 || 0 } onChange={ onChange }/>);
|
||||
}
|
||||
@@ -91,45 +125,80 @@ export default class ModificationsMenu extends TranslatedComponent {
|
||||
|
||||
/**
|
||||
* Activated when a blueprint is selected
|
||||
* @param {int} blueprintId The ID of the selected blueprint
|
||||
* @param {int} grade The grade of the selected blueprint
|
||||
* @param {int} fdname The Frontier name of the blueprint
|
||||
* @param {int} grade The grade of the selected blueprint
|
||||
*/
|
||||
_blueprintSelected(blueprintId, grade) {
|
||||
_blueprintSelected(fdname, grade) {
|
||||
this.context.tooltip(null);
|
||||
const { m } = this.props;
|
||||
const blueprint = Object.assign({}, _.find(Modifications.blueprints, function(o) { return o.id === blueprintId; }));
|
||||
const blueprint = getBlueprint(fdname, m);
|
||||
blueprint.grade = grade;
|
||||
m.blueprint = blueprint;
|
||||
|
||||
const blueprintMenuOpened = false;
|
||||
this.setState({ blueprintMenuOpened });
|
||||
this.setState({ blueprintMenuOpened: false });
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the specials menu
|
||||
*/
|
||||
_toggleSpecialsMenu() {
|
||||
const specialMenuOpened = !this.state.specialMenuOpened;
|
||||
this.setState({ specialMenuOpened });
|
||||
}
|
||||
|
||||
/**
|
||||
* Activated when a special is selected
|
||||
* @param {int} special The name of the selected special
|
||||
*/
|
||||
_specialSelected(special) {
|
||||
this.context.tooltip(null);
|
||||
const { m, ship } = this.props;
|
||||
|
||||
if (m.blueprint) {
|
||||
if (special === null) {
|
||||
m.blueprint.special = null;
|
||||
} else {
|
||||
m.blueprint.special = Modifications.specials[special];
|
||||
}
|
||||
ship.recalculateDps();
|
||||
ship.recalculateHps();
|
||||
ship.recalculateEps();
|
||||
}
|
||||
|
||||
this.setState({ specialMenuOpened: false });
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the result of a roll
|
||||
* @param {object} ship The ship to which the roll applies
|
||||
* @param {object} m The module to which the roll applies
|
||||
* @param {string} featureName The modification feature to which the roll applies
|
||||
* @param {number} value The value of the roll
|
||||
*/
|
||||
_setRollResult(ship, m, featureName, value) {
|
||||
if (Modifications.modifications[featureName].type == 'percentage') {
|
||||
ship.setModification(m, featureName, value * 10000);
|
||||
} else if (Modifications.modifications[featureName].type == 'numeric') {
|
||||
ship.setModification(m, featureName, value * 100);
|
||||
} else {
|
||||
ship.setModification(m, featureName, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a 'worst' roll within the information we have
|
||||
*/
|
||||
_rollWorst() {
|
||||
const { m, ship } = this.props;
|
||||
const features = m.blueprint.features[m.blueprint.grade];
|
||||
ship.clearModifications(m);
|
||||
const features = m.blueprint.grades[m.blueprint.grade].features;
|
||||
for (const featureName in features) {
|
||||
if (Modifications.modifications[featureName].method == 'overwrite') {
|
||||
ship.setModification(m, featureName, features[featureName][1]);
|
||||
} else {
|
||||
let value = features[featureName][0];
|
||||
if (m.grp == 'sb' && featureName == 'shieldboost') {
|
||||
// Shield boosters are a special case. Their boost is dependent on their base so we need to calculate the value here
|
||||
value = ((1 + m.shieldboost) * (1 + value) - 1) / m.shieldboost - 1;
|
||||
}
|
||||
|
||||
if (Modifications.modifications[featureName].type == 'percentage') {
|
||||
ship.setModification(m, featureName, value * 10000);
|
||||
} else if (Modifications.modifications[featureName].type == 'numeric') {
|
||||
ship.setModification(m, featureName, value * 100);
|
||||
}
|
||||
}
|
||||
let value = features[featureName][0];
|
||||
this._setRollResult(ship, m, featureName, value);
|
||||
}
|
||||
|
||||
this.setState({ modifications: this._setModifications(this.props) });
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
@@ -138,26 +207,13 @@ export default class ModificationsMenu extends TranslatedComponent {
|
||||
*/
|
||||
_rollRandom() {
|
||||
const { m, ship } = this.props;
|
||||
const features = m.blueprint.features[m.blueprint.grade];
|
||||
ship.clearModifications(m);
|
||||
const features = m.blueprint.grades[m.blueprint.grade].features;
|
||||
for (const featureName in features) {
|
||||
if (Modifications.modifications[featureName].method == 'overwrite') {
|
||||
ship.setModification(m, featureName, features[featureName][1]);
|
||||
} else {
|
||||
let value = features[featureName][0] + (Math.random() * (features[featureName][1] - features[featureName][0]));
|
||||
if (m.grp == 'sb' && featureName == 'shieldboost') {
|
||||
// Shield boosters are a special case. Their boost is dependent on their base so we need to calculate the value here
|
||||
value = ((1 + m.shieldboost) * (1 + value) - 1) / m.shieldboost - 1;
|
||||
}
|
||||
|
||||
if (Modifications.modifications[featureName].type == 'percentage') {
|
||||
ship.setModification(m, featureName, value * 10000);
|
||||
} else if (Modifications.modifications[featureName].type == 'numeric') {
|
||||
ship.setModification(m, featureName, value * 100);
|
||||
}
|
||||
}
|
||||
let value = features[featureName][0] + (Math.random() * (features[featureName][1] - features[featureName][0]));
|
||||
this._setRollResult(ship, m, featureName, value);
|
||||
}
|
||||
|
||||
this.setState({ modifications: this._setModifications(this.props) });
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
@@ -166,26 +222,43 @@ export default class ModificationsMenu extends TranslatedComponent {
|
||||
*/
|
||||
_rollBest() {
|
||||
const { m, ship } = this.props;
|
||||
const features = m.blueprint.features[m.blueprint.grade];
|
||||
const features = m.blueprint.grades[m.blueprint.grade].features;
|
||||
for (const featureName in features) {
|
||||
if (Modifications.modifications[featureName].method == 'overwrite') {
|
||||
ship.setModification(m, featureName, features[featureName][1]);
|
||||
} else {
|
||||
let value = features[featureName][1];
|
||||
if (m.grp == 'sb' && featureName == 'shieldboost') {
|
||||
// Shield boosters are a special case. Their boost is dependent on their base so we need to calculate the value here
|
||||
value = ((1 + m.shieldboost) * (1 + value) - 1) / m.shieldboost - 1;
|
||||
}
|
||||
|
||||
if (Modifications.modifications[featureName].type == 'percentage') {
|
||||
ship.setModification(m, featureName, value * 10000);
|
||||
} else if (Modifications.modifications[featureName].type == 'numeric') {
|
||||
ship.setModification(m, featureName, value * 100);
|
||||
}
|
||||
}
|
||||
let value = features[featureName][1];
|
||||
this._setRollResult(ship, m, featureName, value);
|
||||
}
|
||||
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide an 'extreme' roll within the information we have
|
||||
*/
|
||||
_rollExtreme() {
|
||||
const { m, ship } = this.props;
|
||||
ship.clearModifications(m);
|
||||
const features = m.blueprint.grades[m.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][0];
|
||||
} else {
|
||||
value = features[featureName][1];
|
||||
}
|
||||
} 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][1];
|
||||
} else {
|
||||
value = features[featureName][0];
|
||||
}
|
||||
}
|
||||
|
||||
this._setRollResult(ship, m, featureName, value);
|
||||
}
|
||||
|
||||
this.setState({ modifications: this._setModifications(this.props) });
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
@@ -197,7 +270,6 @@ export default class ModificationsMenu extends TranslatedComponent {
|
||||
ship.clearModifications(m);
|
||||
ship.clearBlueprint(m);
|
||||
|
||||
this.setState({ modifications: this._setModifications(this.props) });
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
@@ -206,51 +278,77 @@ export default class ModificationsMenu extends TranslatedComponent {
|
||||
* @return {React.Component} List
|
||||
*/
|
||||
render() {
|
||||
const language = this.context.language;
|
||||
const { language, tooltip, termtip } = this.context;
|
||||
const translate = language.translate;
|
||||
const { tooltip, termtip } = this.context;
|
||||
const { m } = this.props;
|
||||
const { blueprintMenuOpened } = this.state;
|
||||
const { blueprintMenuOpened, specialMenuOpened } = this.state;
|
||||
|
||||
const _toggleBlueprintsMenu = this._toggleBlueprintsMenu;
|
||||
const _toggleSpecialsMenu = this._toggleSpecialsMenu;
|
||||
const _rollBest = this._rollBest;
|
||||
const _rollExtreme = this._rollExtreme;
|
||||
const _rollWorst = this._rollWorst;
|
||||
const _rollRandom = this._rollRandom;
|
||||
const _reset = this._reset;
|
||||
|
||||
let blueprintLabel;
|
||||
let haveBlueprint = false;
|
||||
let blueprintTt;
|
||||
if (m.blueprint && !isEmpty(m.blueprint)) {
|
||||
blueprintLabel = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
|
||||
haveBlueprint = true;
|
||||
} else {
|
||||
blueprintLabel = translate('PHRASE_SELECT_BLUEPRINT');
|
||||
blueprintTt = blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], Modifications.modules[m.grp].blueprints[m.blueprint.fdname].grades[m.blueprint.grade].engineers, m.grp);
|
||||
}
|
||||
|
||||
let specialLabel;
|
||||
if (m.blueprint && m.blueprint.special) {
|
||||
specialLabel = m.blueprint.special.name;
|
||||
} else {
|
||||
specialLabel = translate('PHRASE_SELECT_SPECIAL');
|
||||
}
|
||||
|
||||
const specials = this._renderSpecials(this.props, this.context);
|
||||
|
||||
const showBlueprintsMenu = blueprintMenuOpened;
|
||||
const showSpecial = haveBlueprint && specials.length && !blueprintMenuOpened;
|
||||
const showSpecialsMenu = specialMenuOpened;
|
||||
const showRolls = haveBlueprint && !blueprintMenuOpened && !specialMenuOpened;
|
||||
const showReset = !blueprintMenuOpened && !specialMenuOpened;
|
||||
const showMods = !blueprintMenuOpened && !specialMenuOpened;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('select', this.props.className)}
|
||||
onClick={(e) => e.stopPropagation() }
|
||||
onContextMenu={stopCtxPropagation}
|
||||
>
|
||||
<div className={ cn('section-menu', { selected: true })} style={{ cursor: 'pointer' }} onClick={_toggleBlueprintsMenu}>{blueprintLabel}</div>
|
||||
{ blueprintMenuOpened ? this.state.blueprints : '' }
|
||||
{ haveBlueprint ?
|
||||
<table style={{ width: '100%', backgroundColor: 'transparent' }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> { translate('roll') }: </td>
|
||||
<td style={{ cursor: 'pointer' }} onClick={_rollWorst} onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_WORST')} onMouseOut={tooltip.bind(null, null)}> { translate('worst') } </td>
|
||||
<td style={{ cursor: 'pointer' }} onClick={_rollRandom} onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RANDOM')} onMouseOut={tooltip.bind(null, null)}> { translate('random') } </td>
|
||||
<td style={{ cursor: 'pointer' }} onClick={_rollBest}onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_BEST')} onMouseOut={tooltip.bind(null, null)}> { translate('best') } </td>
|
||||
<td style={{ cursor: 'pointer' }} onClick={_reset}onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RESET')} onMouseOut={tooltip.bind(null, null)}> { translate('reset') } </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table> : '' }
|
||||
{ blueprintMenuOpened ? '' :
|
||||
{ showBlueprintsMenu ? '' : haveBlueprint ?
|
||||
<div className={ cn('section-menu', { selected: blueprintMenuOpened })} style={{ cursor: 'pointer' }} onMouseOver={termtip.bind(null, blueprintTt)} onMouseOut={tooltip.bind(null, null)} onClick={_toggleBlueprintsMenu}>{blueprintLabel}</div> :
|
||||
<div className={ cn('section-menu', { selected: blueprintMenuOpened })} style={{ cursor: 'pointer' }} onClick={_toggleBlueprintsMenu}>{translate('PHRASE_SELECT_BLUEPRINT')}</div> }
|
||||
{ showBlueprintsMenu ? this._renderBlueprints(this.props, this.context) : null }
|
||||
{ showSpecial ? <div className={ cn('section-menu', { selected: specialMenuOpened })} style={{ cursor: 'pointer' }} onClick={_toggleSpecialsMenu}>{specialLabel}</div> : null }
|
||||
{ showSpecialsMenu ? specials : null }
|
||||
{ showRolls || showReset ?
|
||||
<table style={{ width: '100%', backgroundColor: 'transparent' }}>
|
||||
<tbody>
|
||||
{ showRolls ?
|
||||
<tr>
|
||||
<td> { translate('roll') }: </td>
|
||||
<td style={{ cursor: 'pointer' }} onClick={_rollWorst} onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_WORST')} onMouseOut={tooltip.bind(null, null)}> { translate('worst') } </td>
|
||||
<td style={{ cursor: 'pointer' }} onClick={_rollBest}onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_BEST')} onMouseOut={tooltip.bind(null, null)}> { translate('best') } </td>
|
||||
<td style={{ cursor: 'pointer' }} onClick={_rollExtreme}onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_EXTREME')} onMouseOut={tooltip.bind(null, null)}> { translate('extreme') } </td>
|
||||
<td style={{ cursor: 'pointer' }} onClick={_rollRandom} onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RANDOM')} onMouseOut={tooltip.bind(null, null)}> { translate('random') } </td>
|
||||
</tr> : null }
|
||||
{ showReset ?
|
||||
<tr>
|
||||
<td colSpan={'5'} style={{ cursor: 'pointer' }} onClick={_reset}onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RESET')} onMouseOut={tooltip.bind(null, null)}> { translate('reset') } </td>
|
||||
</tr> : null }
|
||||
</tbody>
|
||||
</table> : null }
|
||||
{ showMods ?
|
||||
<span onMouseOver={termtip.bind(null, 'HELP_MODIFICATIONS_MENU')} onMouseOut={tooltip.bind(null, null)} >
|
||||
{ this.state.modifications }
|
||||
</span> }
|
||||
{ this._renderModifications(this.props) }
|
||||
</span> : null }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
70
src/app/components/Movement.jsx
Normal file
70
src/app/components/Movement.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
|
||||
/**
|
||||
* Movement
|
||||
*/
|
||||
export default class Movement extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
marker: React.PropTypes.string.isRequired,
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
boost: React.PropTypes.bool.isRequired,
|
||||
eng: React.PropTypes.number.isRequired,
|
||||
fuel: React.PropTypes.number.isRequired,
|
||||
cargo: React.PropTypes.number.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 { language } = this.context;
|
||||
const { formats, translate, units } = language;
|
||||
|
||||
return (
|
||||
<span id='movement'>
|
||||
<svg viewBox='0 0 600 600' fillRule="evenodd" clipRule="evenodd">
|
||||
// Axes
|
||||
<path d="M150 250v300" strokeWidth='1'/>
|
||||
<path d="M150 250l236 236" strokeWidth='1'/>
|
||||
<path d="M150 250l350 -200" strokeWidth='1'/>
|
||||
// End Arrow
|
||||
<path d="M508 43.3L487 67l-10-17.3 31-6.4z"/>
|
||||
// Axes arcs and arrows
|
||||
<path d="M71.7 251.7C64.2 259.2 60 269.4 60 280c0 22 18 40 40 40s40-18 40-40c0-10.6-4.2-20.8-11.7-28.3 7.5 7.5 11.7 17.7 11.7 28.3 0 22-18 40-40 40s-40-18-40-40c0-10.6 4.2-20.8 11.7-28.3z" strokeWidth='4' transform="matrix(.6 0 0 .3 87.5 376.3)"/>
|
||||
<path d="M142.8 453l-13.2 8.7-2.6-9.7 15.8 1z"/>
|
||||
<path d="M144.7 451.6l.5 1.6-16.2 10.6h-.4l-3.5-13 .7-.4 19.3 1.2zm-14.2 7.7l7.7-5-9.2-.7 1.5 5.7zm25.7-6.3l15.8-1-2.6 9.7-13.2-8.8z"/>
|
||||
<path d="M174 450.8l-3.6 13h-.4l-16.2-10.6.5-1.6 19.3-1.2.3.4zm-13.2 3.4l7.7 5 1.5-5.6-9.2.6z"/>
|
||||
|
||||
<path d="M407.7 119c2 .7 4.3 1 6.4 1 14 0 25-11.2 25-25s-11-25-25-25c-11 0-21 7.6-24 18.5 3-11 13-18.5 24-18.5 14 0 25 11.2 25 25s-11 25-25 25c-2 0-4-.3-6-1z" strokeWidth='2'/>
|
||||
<path d="M388 99.7L387 84l9.8 2.5-8.7 13.2z"/>
|
||||
<path d="M398.8 85.5l.2.5-10.7 16-1.6-.3-1.2-19.3.4-.3 12.5 3.8zm-9.5 9.7l5-7.7-5.6-1.6.6 9zm10 20.8l15.7-1-2.6 9.7-13.2-8.8z"/>
|
||||
<path d="M417 113.8l-3.6 13h-.4l-16.2-10.6.5-1.6 19.3-1.2.3.4zm-13.2 3.4l7.7 5 1.5-5.6-9.2.6z"/>
|
||||
|
||||
<path d="M355 430c0-13.8-11.2-25-25-25s-25 11.2-25 25 11.2 25 25 25c-13.8 0-25-11.2-25-25s11.2-25 25-25 25 11.2 25 25z" strokeWidth='2'/>
|
||||
<path d="M357 439.7l-8.8-13 9.7-2.7-1 15.7z"/>
|
||||
<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'>{formats.int(ship.calcSpeed(eng, fuel, cargo, boost))}m/s</text>
|
||||
// Pitch
|
||||
<text x="355" y="410" strokeWidth='0'>{formats.int(ship.calcPitch(eng, fuel, cargo, boost))}°/s</text>
|
||||
// Roll
|
||||
<text x="450" y="110" strokeWidth='0'>{formats.int(ship.calcRoll(eng, fuel, cargo, boost))}°/s</text>
|
||||
// Yaw
|
||||
<text x="160" y="430" strokeWidth='0'>{formats.int(ship.calcYaw(eng, fuel, cargo, boost))}°/s</text>
|
||||
</svg>
|
||||
</span>);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons';
|
||||
|
||||
/**
|
||||
* Movement summary
|
||||
*/
|
||||
export default class MovementSummary extends TranslatedComponent {
|
||||
static PropTypes = {
|
||||
ship: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render movement summary
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
let ship = this.props.ship;
|
||||
let { language, tooltip, termtip } = this.context;
|
||||
let { formats, translate, units } = language;
|
||||
let hide = tooltip.bind(null, null);
|
||||
let boostMultiplier = ship.topBoost / ship.topSpeed;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<h1>{translate('movement summary')}</h1>
|
||||
<table style={{ marginLeft: 'auto', marginRight: 'auto', lineHeight: '1em', backgroundColor: 'transparent' }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td > </td>
|
||||
<td colSpan='6'>{translate('engine pips')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>0</td>
|
||||
<td>1</td>
|
||||
<td>2</td>
|
||||
<td>3</td>
|
||||
<td>4</td>
|
||||
<td onMouseOver={termtip.bind(null, '4b')} onMouseOut={tooltip.bind(null, null)}>4B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='ri'>{translate('speed')} ({units['m/s']})</td>
|
||||
<td className='ri'>{formats.int(ship.speeds[0])}</td>
|
||||
<td className='ri'>{formats.int(ship.speeds[1])}</td>
|
||||
<td className='ri'>{formats.int(ship.speeds[2])}</td>
|
||||
<td className='ri'>{formats.int(ship.speeds[3])}</td>
|
||||
<td className='ri'>{formats.int(ship.speeds[4])}</td>
|
||||
<td className='ri'>{formats.int(ship.speeds[4] * boostMultiplier)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='ri'>{translate('pitch')} ({units['°/s']})</td>
|
||||
<td className='ri'>{formats.int(ship.pitches[0])}</td>
|
||||
<td className='ri'>{formats.int(ship.pitches[1])}</td>
|
||||
<td className='ri'>{formats.int(ship.pitches[2])}</td>
|
||||
<td className='ri'>{formats.int(ship.pitches[3])}</td>
|
||||
<td className='ri'>{formats.int(ship.pitches[4])}</td>
|
||||
<td className='ri'>{formats.int(ship.pitches[4] * boostMultiplier)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='ri'>{translate('roll')} ({units['°/s']})</td>
|
||||
<td className='ri'>{formats.int(ship.rolls[0])}</td>
|
||||
<td className='ri'>{formats.int(ship.rolls[1])}</td>
|
||||
<td className='ri'>{formats.int(ship.rolls[2])}</td>
|
||||
<td className='ri'>{formats.int(ship.rolls[3])}</td>
|
||||
<td className='ri'>{formats.int(ship.rolls[4])}</td>
|
||||
<td className='ri'>{formats.int(ship.rolls[4] * boostMultiplier)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='ri'>{translate('yaw')} ({units['°/s']})</td>
|
||||
<td className='ri'>{formats.int(ship.yaws[0])}</td>
|
||||
<td className='ri'>{formats.int(ship.yaws[1])}</td>
|
||||
<td className='ri'>{formats.int(ship.yaws[2])}</td>
|
||||
<td className='ri'>{formats.int(ship.yaws[3])}</td>
|
||||
<td className='ri'>{formats.int(ship.yaws[4])}</td>
|
||||
<td className='ri'>{formats.int(ship.yaws[4] * boostMultiplier)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
266
src/app/components/Offence.jsx
Normal file
266
src/app/components/Offence.jsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import React from 'react';
|
||||
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 VerticalBarChart from './VerticalBarChart';
|
||||
|
||||
/**
|
||||
* Generates an internationalization friendly weapon comparator that will
|
||||
* sort by specified property (if provided) then by name/group, class, rating
|
||||
* @param {function} translate Translation function
|
||||
* @param {function} propComparator Optional property comparator
|
||||
* @param {boolean} desc Use descending order
|
||||
* @return {function} Comparator function for names
|
||||
*/
|
||||
export function weaponComparator(translate, propComparator, desc) {
|
||||
return (a, b) => {
|
||||
if (!desc) { // Flip A and B if ascending order
|
||||
let t = a;
|
||||
a = b;
|
||||
b = t;
|
||||
}
|
||||
|
||||
// If a property comparator is provided use it first
|
||||
let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a, b);
|
||||
|
||||
if (diff) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
// Property matches so sort by name / group, then class, rating
|
||||
if (a.name === b.name && a.grp === b.grp) {
|
||||
if(a.class == b.class) {
|
||||
return a.rating > b.rating ? 1 : -1;
|
||||
}
|
||||
return a.class - b.class;
|
||||
}
|
||||
|
||||
return nameComparator(translate, a, b);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Offence information
|
||||
* Offence information consists of four panels:
|
||||
* - textual information (time to drain cap, time to take down shields etc.)
|
||||
* - breakdown of damage sources (pie chart)
|
||||
* - comparison of shield resistances (table chart)
|
||||
* - effective sustained DPS of weapons (bar chart)
|
||||
*/
|
||||
export default class Offence extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
marker: React.PropTypes.string.isRequired,
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
opponent: React.PropTypes.object.isRequired,
|
||||
engagementrange: React.PropTypes.number.isRequired,
|
||||
wep: React.PropTypes.number.isRequired,
|
||||
opponentSys: React.PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._sort = this._sort.bind(this);
|
||||
|
||||
const damage = Calc.offenceMetrics(props.ship, props.opponent, props.wep, props.opponentSys, props.engagementrange);
|
||||
this.state = {
|
||||
predicate: 'n',
|
||||
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);
|
||||
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 { 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 opponentShields = Calc.shieldMetrics(opponent, 4);
|
||||
const opponentArmour = Calc.armourMetrics(opponent);
|
||||
|
||||
const timeToDrain = Calc.timeToDrainWep(ship, wep);
|
||||
|
||||
let absoluteShieldsSDps = 0;
|
||||
let explosiveShieldsSDps = 0;
|
||||
let kineticShieldsSDps = 0;
|
||||
let thermalShieldsSDps = 0;
|
||||
let absoluteArmourSDps = 0;
|
||||
let explosiveArmourSDps = 0;
|
||||
let kineticArmourSDps = 0;
|
||||
let thermalArmourSDps = 0;
|
||||
|
||||
let totalSEps = 0;
|
||||
|
||||
const rows = [];
|
||||
for (let i = 0; i < damage.length; i++) {
|
||||
const weapon = damage[i];
|
||||
|
||||
totalSEps += weapon.seps;
|
||||
absoluteShieldsSDps += weapon.sdps.shields.absolute;
|
||||
explosiveShieldsSDps += weapon.sdps.shields.explosive;
|
||||
kineticShieldsSDps += weapon.sdps.shields.kinetic;
|
||||
thermalShieldsSDps += weapon.sdps.shields.thermal;
|
||||
absoluteArmourSDps += weapon.sdps.armour.absolute;
|
||||
explosiveArmourSDps += weapon.sdps.armour.explosive;
|
||||
kineticArmourSDps += weapon.sdps.armour.kinetic;
|
||||
thermalArmourSDps += weapon.sdps.armour.thermal;
|
||||
|
||||
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 = [];
|
||||
if (weapon.sdps.shields.absolute) effectiveShieldsSDpsTooltipDetails.push(<div key='absolute'>{translate('absolute') + ' ' + formats.f1(weapon.sdps.shields.absolute)}</div>);
|
||||
if (weapon.sdps.shields.explosive) effectiveShieldsSDpsTooltipDetails.push(<div key='explosive'>{translate('explosive') + ' ' + formats.f1(weapon.sdps.shields.explosive)}</div>);
|
||||
if (weapon.sdps.shields.kinetic) effectiveShieldsSDpsTooltipDetails.push(<div key='kinetic'>{translate('kinetic') + ' ' + formats.f1(weapon.sdps.shields.kinetic)}</div>);
|
||||
if (weapon.sdps.shields.thermal) effectiveShieldsSDpsTooltipDetails.push(<div key='thermal'>{translate('thermal') + ' ' + formats.f1(weapon.sdps.shields.thermal)}</div>);
|
||||
|
||||
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 = [];
|
||||
if (weapon.sdps.armour.absolute) effectiveArmourSDpsTooltipDetails.push(<div key='absolute'>{translate('absolute') + ' ' + formats.f1(weapon.sdps.armour.absolute)}</div>);
|
||||
if (weapon.sdps.armour.explosive) effectiveArmourSDpsTooltipDetails.push(<div key='explosive'>{translate('explosive') + ' ' + formats.f1(weapon.sdps.armour.explosive)}</div>);
|
||||
if (weapon.sdps.armour.kinetic) effectiveArmourSDpsTooltipDetails.push(<div key='kinetic'>{translate('kinetic') + ' ' + formats.f1(weapon.sdps.armour.kinetic)}</div>);
|
||||
if (weapon.sdps.armour.thermal) effectiveArmourSDpsTooltipDetails.push(<div key='thermal'>{translate('thermal') + ' ' + formats.f1(weapon.sdps.armour.thermal)}</div>);
|
||||
|
||||
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, 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 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>
|
||||
</tr>);
|
||||
}
|
||||
|
||||
const totalShieldsSDps = absoluteShieldsSDps + explosiveShieldsSDps + kineticShieldsSDps + thermalShieldsSDps;
|
||||
const totalArmourSDps = absoluteArmourSDps + explosiveArmourSDps + kineticArmourSDps + thermalArmourSDps;
|
||||
|
||||
const shieldsSDpsData = [];
|
||||
shieldsSDpsData.push({ value: Math.round(absoluteShieldsSDps), label: translate('absolute') });
|
||||
shieldsSDpsData.push({ value: Math.round(explosiveShieldsSDps), label: translate('explosive') });
|
||||
shieldsSDpsData.push({ value: Math.round(kineticShieldsSDps), label: translate('kinetic') });
|
||||
shieldsSDpsData.push({ value: Math.round(thermalShieldsSDps), label: translate('thermal') });
|
||||
|
||||
const armourSDpsData = [];
|
||||
armourSDpsData.push({ value: Math.round(absoluteArmourSDps), label: translate('absolute') });
|
||||
armourSDpsData.push({ value: Math.round(explosiveArmourSDps), label: translate('explosive') });
|
||||
armourSDpsData.push({ value: Math.round(kineticArmourSDps), label: translate('kinetic') });
|
||||
armourSDpsData.push({ value: Math.round(thermalArmourSDps), label: translate('thermal') });
|
||||
|
||||
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));
|
||||
|
||||
return (
|
||||
<span id='offence'>
|
||||
<div className='group full'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th rowSpan='2' className='sortable' onClick={sortOrder.bind(this, 'n')}>{translate('weapon')}</th>
|
||||
<th colSpan='2'>{translate('opponent\'s shields')}</th>
|
||||
<th colSpan='2'>{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='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_SHIELDS')} onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'es')}>{'eft'}</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
</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} />
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_ARMOUR_DAMAGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('armour damage sources')}</h2>
|
||||
<PieChart data={armourSDpsData} />
|
||||
</div>
|
||||
</span>);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons';
|
||||
|
||||
/**
|
||||
* Offence summary
|
||||
*/
|
||||
export default class OffenceSummary extends TranslatedComponent {
|
||||
static PropTypes = {
|
||||
ship: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render offence summary
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
let ship = this.props.ship;
|
||||
let { language, tooltip, termtip } = this.context;
|
||||
let { formats, translate } = language;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<h1>{translate('offence summary')}</h1>
|
||||
<table className='summary' style={{ marginLeft: 'auto', marginRight: 'auto', lineHeight: '1em', backgroundColor: 'transparent' }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan='4' className='summary'><h2>{translate('dps')}: {formats.f1(ship.totalDps)}</h2></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='le'>{translate('damage by')}</td>
|
||||
<td className='ri' onMouseOver={termtip.bind(null, 'explosive')} onMouseOut={tooltip.bind(null, null)}><DamageExplosive /> {formats.f1(ship.totalExplDps)}</td>
|
||||
<td className='ri' onMouseOver={termtip.bind(null, 'kinetic')} onMouseOut={tooltip.bind(null, null)}><DamageKinetic /> {formats.f1(ship.totalKinDps)}</td>
|
||||
<td className='ri' onMouseOver={termtip.bind(null, 'thermal')} onMouseOut={tooltip.bind(null, null)}><DamageThermal /> {formats.f1(ship.totalThermDps)}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colSpan='4' className='summary'><h2>{translate('sdps')}: {formats.f1(ship.totalSDps)}</h2></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='le'>{translate('damage by')}</td>
|
||||
<td className='ri' onMouseOver={termtip.bind(null, 'explosive')} onMouseOut={tooltip.bind(null, null)}><DamageExplosive /> {formats.f1(ship.totalExplSDps)}</td>
|
||||
<td className='ri' onMouseOver={termtip.bind(null, 'kinetic')} onMouseOut={tooltip.bind(null, null)}><DamageKinetic /> {formats.f1(ship.totalKinSDps)}</td>
|
||||
<td className='ri' onMouseOver={termtip.bind(null, 'thermal')} onMouseOut={tooltip.bind(null, null)}><DamageThermal /> {formats.f1(ship.totalThermSDps)}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colSpan='4' className='summary'><h2>{translate('dpe')}: {formats.f1(ship.totalDpe)}</h2></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='le'>{translate('damage by')}</td>
|
||||
<td className='ri' onMouseOver={termtip.bind(null, 'explosive')} onMouseOut={tooltip.bind(null, null)}><DamageExplosive /> {formats.f1(ship.totalExplDpe)}</td>
|
||||
<td className='ri' onMouseOver={termtip.bind(null, 'kinetic')} onMouseOut={tooltip.bind(null, null)}><DamageKinetic /> {formats.f1(ship.totalKinDpe)}</td>
|
||||
<td className='ri' onMouseOver={termtip.bind(null, 'thermal')} onMouseOut={tooltip.bind(null, null)}><DamageThermal /> {formats.f1(ship.totalThermDpe)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
186
src/app/components/OutfittingSubpages.jsx
Normal file
186
src/app/components/OutfittingSubpages.jsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import Persist from '../stores/Persist';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import PowerManagement from './PowerManagement';
|
||||
import CostSection from './CostSection';
|
||||
import EngineProfile from './EngineProfile';
|
||||
import FSDProfile from './FSDProfile';
|
||||
import Movement from './Movement';
|
||||
import Offence from './Offence';
|
||||
import Defence from './Defence';
|
||||
import WeaponDamageChart from './WeaponDamageChart';
|
||||
|
||||
/**
|
||||
* Outfitting subpages
|
||||
*/
|
||||
export default class OutfittingSubpages extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
code: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
buildName: React.PropTypes.string,
|
||||
sys: React.PropTypes.number.isRequired,
|
||||
eng: React.PropTypes.number.isRequired,
|
||||
wep: React.PropTypes.number.isRequired,
|
||||
cargo: React.PropTypes.number.isRequired,
|
||||
fuel: React.PropTypes.number.isRequired,
|
||||
boost: React.PropTypes.bool.isRequired,
|
||||
engagementRange: React.PropTypes.number.isRequired,
|
||||
opponent: React.PropTypes.object.isRequired,
|
||||
opponentBuild: React.PropTypes.string,
|
||||
opponentSys: React.PropTypes.number.isRequired,
|
||||
opponentEng: React.PropTypes.number.isRequired,
|
||||
opponentWep: React.PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
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);
|
||||
|
||||
this.state = {
|
||||
tab: Persist.getOutfittingTab() || 'power',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show selected tab
|
||||
* @param {string} tab Tab name
|
||||
*/
|
||||
_showTab(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;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='group full' style={{ minHeight: '1000px' }}>
|
||||
<table className='tabs'>
|
||||
<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('defence')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
{tabSection}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
96
src/app/components/PieChart.jsx
Normal file
96
src/app/components/PieChart.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { Component } from 'react';
|
||||
import Measure from 'react-measure';
|
||||
import * as d3 from 'd3';
|
||||
|
||||
const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D'];
|
||||
const LABEL_COLOUR = '#000000';
|
||||
|
||||
/**
|
||||
* A pie chart
|
||||
*/
|
||||
export default class PieChart extends Component {
|
||||
|
||||
static propTypes = {
|
||||
data : React.PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
this.pie = d3.pie().value((d) => d.value);
|
||||
this.colors = CORIOLIS_COLOURS;
|
||||
this.arc = d3.arc();
|
||||
this.arc.innerRadius(0);
|
||||
|
||||
this.state = {
|
||||
dimensions: {
|
||||
width: 100,
|
||||
height: 100
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a slice of the pie chart
|
||||
* @param {Object} d the data for this slice
|
||||
* @param {number} i the index of this slice
|
||||
* @returns {Object} the SVG for the slice
|
||||
*/
|
||||
sliceGenerator(d, i) {
|
||||
if (!d || d.value == 0) {
|
||||
// Ignore 0 values
|
||||
return null;
|
||||
}
|
||||
|
||||
const { width, height } = this.state.dimensions;
|
||||
const { data } = this.props;
|
||||
|
||||
// Push the labels further out from the centre of the slice
|
||||
let [labelX, labelY] = this.arc.centroid(d);
|
||||
const labelTranslate = `translate(${labelX * 1.5}, ${labelY * 1.5})`;
|
||||
|
||||
// Put the keys in a line with equal spacing
|
||||
const nonZeroItems = data.filter(d => d.value != 0).length;
|
||||
const thisItemIndex = data.slice(0, i + 1).filter(d => d.value != 0).length - 1;
|
||||
const keyX = -width / 2 + (width / nonZeroItems) * (thisItemIndex + 0.5);
|
||||
const keyTranslate = `translate(${keyX}, ${width * 0.45})`;
|
||||
|
||||
return (
|
||||
<g key={`group-${i}`}>
|
||||
<path key={`arc-${i}`} d={this.arc(d)} style={{ fill: this.colors[i] }} />
|
||||
<text key={`label-${i}`} transform={labelTranslate} style={{ strokeWidth: '0px', fill: LABEL_COLOUR }} textAnchor='middle'>{d.value}</text>
|
||||
<text key={`key-${i}`} transform={keyTranslate} style={{ strokeWidth:'0px', fill: this.colors[i] }} textAnchor='middle'>{d.data.label}</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
* @returns {object} Markup
|
||||
*/
|
||||
render() {
|
||||
const { width, height } = this.state.dimensions;
|
||||
const pie = this.pie(this.props.data),
|
||||
translate = `translate(${width / 2}, ${width * 0.4})`;
|
||||
|
||||
this.arc.outerRadius(width * 0.4);
|
||||
|
||||
return (
|
||||
<Measure width='100%' whitelist={['width', 'top']} onMeasure={ (dimensions) => { this.setState({ dimensions }); }}>
|
||||
<div width={width} height={width}>
|
||||
<svg style={{ stroke: 'None' }} width={width} height={width * 0.9}>
|
||||
<g transform={translate}>
|
||||
{pie.map((d, i) => this.sliceGenerator(d, i))}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
298
src/app/components/Pips.jsx
Normal file
298
src/app/components/Pips.jsx
Normal file
@@ -0,0 +1,298 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import { nameComparator } from '../utils/SlotFunctions';
|
||||
import { Pip } from './SvgIcons';
|
||||
import LineChart from '../components/LineChart';
|
||||
import Slider from '../components/Slider';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import Module from '../shipyard/Module';
|
||||
|
||||
/**
|
||||
* Pips displays SYS/ENG/WEP pips and allows users to change them with key presses by clicking on the relevant area.
|
||||
* Requires an onChange() function of the form onChange(sys, eng, wep) which is triggered whenever the pips change.
|
||||
*/
|
||||
export default class Pips extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
sys: React.PropTypes.number.isRequired,
|
||||
eng: React.PropTypes.number.isRequired,
|
||||
wep: React.PropTypes.number.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
const { sys, eng, wep } = props;
|
||||
|
||||
this._keyDown = this._keyDown.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add listeners after mounting
|
||||
*/
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this._keyDown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove listeners before unmounting
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this._keyDown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Key Down
|
||||
* @param {Event} e Keyboard Event
|
||||
*/
|
||||
_keyDown(e) {
|
||||
if (e.ctrlKey || e.metaKey) { // CTRL/CMD
|
||||
switch (e.keyCode) {
|
||||
case 37: // Left arrow == increase SYS
|
||||
e.preventDefault();
|
||||
this._incSys();
|
||||
break;
|
||||
case 38: // Up arrow == increase ENG
|
||||
e.preventDefault();
|
||||
this._incEng();
|
||||
break;
|
||||
case 39: // Right arrow == increase WEP
|
||||
e.preventDefault();
|
||||
this._incWep();
|
||||
break;
|
||||
case 40: // Down arrow == reset
|
||||
e.preventDefault();
|
||||
this._reset();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a click
|
||||
* @param {string} which Which item was clicked
|
||||
*/
|
||||
onClick(which) {
|
||||
if (which == 'SYS') {
|
||||
this._incSys();
|
||||
} else if (which == 'ENG') {
|
||||
this._incEng();
|
||||
} else if (which == 'WEP') {
|
||||
this._incWep();
|
||||
} else if (which == 'RST') {
|
||||
this._reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the capacitor
|
||||
*/
|
||||
_reset() {
|
||||
let { sys, eng, wep } = this.props;
|
||||
if (sys != 2 || eng != 2 || wep != 2) {
|
||||
sys = eng = wep = 2;
|
||||
this.props.onChange(sys, eng, wep);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the SYS capacitor
|
||||
*/
|
||||
_incSys() {
|
||||
let { sys, eng, wep } = this.props;
|
||||
|
||||
const required = Math.min(1, 4 - sys);
|
||||
if (required > 0) {
|
||||
if (required == 0.5) {
|
||||
// Take from whichever is larger
|
||||
if (eng > wep) {
|
||||
eng -= 0.5;
|
||||
sys += 0.5;
|
||||
} else {
|
||||
wep -= 0.5;
|
||||
sys += 0.5;
|
||||
}
|
||||
} else {
|
||||
// Required is 1 - take from both if possible
|
||||
if (eng == 0) {
|
||||
wep -= 1;
|
||||
sys += 1;
|
||||
} else if (wep == 0) {
|
||||
eng -= 1;
|
||||
sys += 1;
|
||||
} else {
|
||||
eng -= 0.5;
|
||||
wep -= 0.5;
|
||||
sys += 1;
|
||||
}
|
||||
}
|
||||
this.props.onChange(sys, eng, wep);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the ENG capacitor
|
||||
*/
|
||||
_incEng() {
|
||||
let { sys, eng, wep } = this.props;
|
||||
|
||||
const required = Math.min(1, 4 - eng);
|
||||
if (required > 0) {
|
||||
if (required == 0.5) {
|
||||
// Take from whichever is larger
|
||||
if (sys > wep) {
|
||||
sys -= 0.5;
|
||||
eng += 0.5;
|
||||
} else {
|
||||
wep -= 0.5;
|
||||
eng += 0.5;
|
||||
}
|
||||
} else {
|
||||
// Required is 1 - take from both if possible
|
||||
if (sys == 0) {
|
||||
wep -= 1;
|
||||
eng += 1;
|
||||
} else if (wep == 0) {
|
||||
sys -= 1;
|
||||
eng += 1;
|
||||
} else {
|
||||
sys -= 0.5;
|
||||
wep -= 0.5;
|
||||
eng += 1;
|
||||
}
|
||||
}
|
||||
this.props.onChange(sys, eng, wep);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the WEP capacitor
|
||||
*/
|
||||
_incWep() {
|
||||
let { sys, eng, wep } = this.props;
|
||||
|
||||
const required = Math.min(1, 4 - wep);
|
||||
if (required > 0) {
|
||||
if (required == 0.5) {
|
||||
// Take from whichever is larger
|
||||
if (sys > eng) {
|
||||
sys -= 0.5;
|
||||
wep += 0.5;
|
||||
} else {
|
||||
eng -= 0.5;
|
||||
wep += 0.5;
|
||||
}
|
||||
} else {
|
||||
// Required is 1 - take from both if possible
|
||||
if (sys == 0) {
|
||||
eng -= 1;
|
||||
wep += 1;
|
||||
} else if (eng == 0) {
|
||||
sys -= 1;
|
||||
wep += 1;
|
||||
} else {
|
||||
sys -= 0.5;
|
||||
eng -= 0.5;
|
||||
wep += 1;
|
||||
}
|
||||
}
|
||||
this.props.onChange(sys, eng, wep);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the rendering for pips
|
||||
* @param {int} sys the SYS pips
|
||||
* @param {int} eng the ENG pips
|
||||
* @param {int} wep the WEP pips
|
||||
* @returns {Object} Object containing the rendering for the pips
|
||||
*/
|
||||
_renderPips(sys, eng, wep) {
|
||||
const pipsSvg = {};
|
||||
|
||||
// SYS
|
||||
pipsSvg['SYS'] = [];
|
||||
for (let i = 0; i < Math.floor(sys); i++) {
|
||||
pipsSvg['SYS'].push(<Pip className='full' key={i} />);
|
||||
}
|
||||
if (sys > Math.floor(sys)) {
|
||||
pipsSvg['SYS'].push(<Pip className='half' key={'half'} />);
|
||||
}
|
||||
for (let i = Math.floor(sys + 0.5); i < 4; i++) {
|
||||
pipsSvg['SYS'].push(<Pip className='empty' key={i} />);
|
||||
}
|
||||
|
||||
// ENG
|
||||
pipsSvg['ENG'] = [];
|
||||
for (let i = 0; i < Math.floor(eng); i++) {
|
||||
pipsSvg['ENG'].push(<Pip className='full' key={i} />);
|
||||
}
|
||||
if (eng > Math.floor(eng)) {
|
||||
pipsSvg['ENG'].push(<Pip className='half' key={'half'} />);
|
||||
}
|
||||
for (let i = Math.floor(eng + 0.5); i < 4; i++) {
|
||||
pipsSvg['ENG'].push(<Pip className='empty' key={i} />);
|
||||
}
|
||||
|
||||
// WEP
|
||||
pipsSvg['WEP'] = [];
|
||||
for (let i = 0; i < Math.floor(wep); i++) {
|
||||
pipsSvg['WEP'].push(<Pip className='full' key={i} />);
|
||||
}
|
||||
if (wep > Math.floor(wep)) {
|
||||
pipsSvg['WEP'].push(<Pip className='half' key={'half'} />);
|
||||
}
|
||||
for (let i = Math.floor(wep + 0.5); i < 4; i++) {
|
||||
pipsSvg['WEP'].push(<Pip className='empty' key={i} />);
|
||||
}
|
||||
|
||||
return pipsSvg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render pips
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const { formats, translate, units } = this.context.language;
|
||||
const { sys, eng, wep } = this.props;
|
||||
|
||||
const onSysClicked = this.onClick.bind(this, 'SYS');
|
||||
const onEngClicked = this.onClick.bind(this, 'ENG');
|
||||
const onWepClicked = this.onClick.bind(this, 'WEP');
|
||||
const onRstClicked = this.onClick.bind(this, 'RST');
|
||||
|
||||
const pipsSvg = this._renderPips(sys, eng, wep);
|
||||
return (
|
||||
<span id='pips'>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
<td className='clickable' onClick={onEngClicked}>{pipsSvg['ENG']}</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td className='clickable' onClick={onSysClicked}>{pipsSvg['SYS']}</td>
|
||||
<td className='clickable' onClick={onEngClicked}>{translate('ENG')}</td>
|
||||
<td className='clickable' onClick={onWepClicked}>{pipsSvg['WEP']}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td className='clickable' onClick={onSysClicked}>{translate('SYS')}</td>
|
||||
<td className='clickable' onClick={onRstClicked}>{translate('RST')}</td>
|
||||
<td className='clickable' onClick={onWepClicked}>{translate('WEP')}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import d3 from 'd3';
|
||||
import * as d3 from 'd3';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||
@@ -46,10 +46,10 @@ export default class PowerBands extends TranslatedComponent {
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this.wattScale = d3.scale.linear();
|
||||
this.pctScale = d3.scale.linear().domain([0, 1]);
|
||||
this.wattAxis = d3.svg.axis().scale(this.wattScale).outerTickSize(0).orient('top').tickFormat(context.language.formats.r2);
|
||||
this.pctAxis = d3.svg.axis().scale(this.pctScale).outerTickSize(0).orient('bottom').tickFormat(context.language.formats.rPct);
|
||||
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);
|
||||
@@ -186,10 +186,10 @@ export default class PowerBands extends TranslatedComponent {
|
||||
|
||||
let { wattScale, pctScale, context, props, state } = this;
|
||||
let { translate, formats } = context.language;
|
||||
let { f2, pct1, rPct, r2 } = formats; // wattFmt, pctFmt, pctAxis, wattAxis
|
||||
let { available, bands, width } = props;
|
||||
let { innerWidth, maxPwr, ret, dep } = state;
|
||||
let pwrWarningClass = cn('threshold', { exceeded: bands[0].retractedSum * 2 >= available });
|
||||
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 retSelected = Object.keys(ret).length > 0;
|
||||
@@ -268,7 +268,7 @@ export default class PowerBands extends TranslatedComponent {
|
||||
axis.call(this.pctAxis);
|
||||
axis.select('g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass);
|
||||
}} className='pct axis' transform={`translate(0,${state.innerHeight})`}></g>
|
||||
<line x1={pctScale(0.5)} x2={pctScale(0.5)} y1='0' y2={state.innerHeight} className={pwrWarningClass} />
|
||||
<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>
|
||||
|
||||
@@ -17,7 +17,7 @@ const POWER = [
|
||||
* Power Management Section
|
||||
*/
|
||||
export default class PowerManagement extends TranslatedComponent {
|
||||
static PropTypes = {
|
||||
static propTypes = {
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
code: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
@@ -34,8 +34,8 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
this._sort = this._sort.bind(this);
|
||||
|
||||
this.state = {
|
||||
predicate: 'n',
|
||||
desc: true,
|
||||
predicate: 'pwr',
|
||||
desc: false,
|
||||
width: 0
|
||||
};
|
||||
}
|
||||
|
||||
126
src/app/components/ShipPicker.jsx
Normal file
126
src/app/components/ShipPicker.jsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import { Rocket } from './SvgIcons';
|
||||
import Persist from '../stores/Persist';
|
||||
import cn from 'classnames';
|
||||
|
||||
/**
|
||||
* Ship picker
|
||||
* Requires an onChange() function of the form onChange(ship), providing the ship, which is triggered on ship change
|
||||
*/
|
||||
export default class ShipPicker extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
ship: React.PropTypes.string.isRequired,
|
||||
build: React.PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
ship: 'eagle'
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ship
|
||||
* @param {object} ship the ship
|
||||
* @param {string} build the build, if present
|
||||
*/
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the menu for the picker
|
||||
* @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>);
|
||||
}
|
||||
|
||||
return buildList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the menu state
|
||||
*/
|
||||
_toggleMenu() {
|
||||
const { menuOpen } = this.state;
|
||||
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 { menuOpen } = this.state;
|
||||
|
||||
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>
|
||||
</div>
|
||||
{ menuOpen ?
|
||||
<div className='menu-list' onClick={ (e) => e.stopPropagation() }>
|
||||
<div className='quad'>
|
||||
{this._renderPickerMenu()}
|
||||
</div>
|
||||
</div> : null }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Rocket } from './SvgIcons';
|
||||
|
||||
/**
|
||||
* Selector for ships
|
||||
*/
|
||||
export default class ShipSelector extends TranslatedComponent {
|
||||
static PropTypes = {
|
||||
initial: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { ship : this.props.initial };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the ships menu
|
||||
* @return {React.Component} Menu
|
||||
*/
|
||||
_getShipsMenu() {
|
||||
const _selectShip = this._selectShip;
|
||||
const _openMenu = this._openMenu;
|
||||
|
||||
let shipList = [];
|
||||
|
||||
for (let s in Ships) {
|
||||
shipList.push(<div key={s} onClick={_selectShip.bind(this, s)} className='block' >{Ships[s].properties.name}</div>);
|
||||
}
|
||||
|
||||
return shipList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle opening the menu
|
||||
* @param {string} menu The ID of the opened menu
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_openMenu(menu, event) {
|
||||
event.stopPropagation();
|
||||
if (this.props.currentMenu == menu) {
|
||||
menu = null;
|
||||
}
|
||||
|
||||
this.context.openMenu(menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle selection of a ship
|
||||
* @param {string} s The selected ship ID
|
||||
*/
|
||||
_selectShip(s) {
|
||||
this.setState({ ship: Ships[s] });
|
||||
|
||||
this.context.openMenu(null);
|
||||
this.props.onChange(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render ship selector
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
const currentMenu = this.props.currentMenu;
|
||||
const ship = this.state.ship;
|
||||
|
||||
return (
|
||||
<div className='shipselector'>
|
||||
<div className='menu'>
|
||||
<div className={cn('menu-header', { selected: currentMenu == 'wds' })} onClick={this._openMenu.bind(this, 'wds')}>
|
||||
<Rocket className='warning' /><span className='menu-item-label'>{ship.properties.name}</span>
|
||||
{currentMenu == 'wds' ?
|
||||
<div className='menu-list quad no-wrap' onClick={ (e) => e.stopPropagation() }>
|
||||
{this._getShipsMenu()}
|
||||
</div> : null }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import cn from 'classnames';
|
||||
import { SizeMap } from '../shipyard/Constants';
|
||||
import { Warning } from './SvgIcons';
|
||||
import * as Calc from '../shipyard/Calculations';
|
||||
|
||||
/**
|
||||
* Ship Summary Table / Stats
|
||||
@@ -10,7 +10,10 @@ import { Warning } from './SvgIcons';
|
||||
export default class ShipSummaryTable extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
ship: React.PropTypes.object.isRequired
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
cargo: React.PropTypes.number.isRequired,
|
||||
fuel: React.PropTypes.number.isRequired,
|
||||
marker: React.PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -18,74 +21,76 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
||||
* @return {React.Component} Summary table
|
||||
*/
|
||||
render() {
|
||||
let ship = this.props.ship;
|
||||
const { ship, cargo, fuel } = 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, pct } = formats;
|
||||
let sgClassNames = cn({ warning: ship.findInternalByGroup('sg') && !ship.shield, muted: !ship.findInternalByGroup('sg') });
|
||||
let sgRecover = '-';
|
||||
let sgRecharge = '-';
|
||||
let { time, int, round, f1, f2 } = formats;
|
||||
let hide = tooltip.bind(null, null);
|
||||
|
||||
if (ship.shield) {
|
||||
sgRecover = time(ship.calcShieldRecovery());
|
||||
sgRecharge = time(ship.calcShieldRecharge());
|
||||
}
|
||||
const shieldGenerator = ship.findInternalByGroup('sg');
|
||||
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, fuel);
|
||||
const speedTooltip = canThrust ? 'TT_SUMMARY_SPEED' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
|
||||
const canBoost = ship.canBoost(cargo, fuel);
|
||||
const boostTooltip = canBoost ? 'TT_SUMMARY_BOOST' : canThrust ? 'TT_SUMMARY_BOOST_NONFUNCTIONAL' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
|
||||
|
||||
return <div id='summary'>
|
||||
<table id='summaryTable'>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canThrust() }) }>{translate('speed')}</th>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canBoost() }) }>{translate('boost')}</th>
|
||||
<th onMouseEnter={termtip.bind(null, 'damage per second')} onMouseLeave={hide} rowSpan={2}>{translate('DPS')}</th>
|
||||
<th onMouseEnter={termtip.bind(null, 'energy per second')} onMouseLeave={hide} rowSpan={2}>{translate('EPS')}</th>
|
||||
<th onMouseEnter={termtip.bind(null, 'heat per second')} onMouseLeave={hide} rowSpan={2}>{translate('HPS')}</th>
|
||||
<th rowSpan={2}>{translate('hardness')}</th>
|
||||
<th rowSpan={2}>{translate('armour')}</th>
|
||||
<th rowSpan={2}>{translate('shields')}</th>
|
||||
<th colSpan={3}>{translate('mass')}</th>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !canThrust }) }>{translate('speed')}</th>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !canBoost }) }>{translate('boost')}</th>
|
||||
<th colSpan={5}>{translate('jump range')}</th>
|
||||
<th rowSpan={2}>{translate('shield')}</th>
|
||||
<th rowSpan={2}>{translate('integrity')}</th>
|
||||
<th rowSpan={2}>{translate('DPS')}</th>
|
||||
<th rowSpan={2}>{translate('EPS')}</th>
|
||||
<th rowSpan={2}>{translate('TTD')}</th>
|
||||
{/* <th onMouseEnter={termtip.bind(null, 'heat per second')} onMouseLeave={hide} rowSpan={2}>{translate('HPS')}</th> */}
|
||||
<th rowSpan={2}>{translate('cargo')}</th>
|
||||
<th rowSpan={2}>{translate('fuel')}</th>
|
||||
<th colSpan={3}>{translate('jump range')}</th>
|
||||
<th onMouseEnter={termtip.bind(null, 'PHRASE_FASTEST_RANGE')} onMouseLeave={hide} colSpan={3}>{translate('fastest range')}</th>
|
||||
<th onMouseEnter={termtip.bind(null, 'mass lock factor')} onMouseLeave={hide} rowSpan={2}>{translate('MLF')}</th>
|
||||
<th colSpan={3}>{translate('mass')}</th>
|
||||
<th onMouseEnter={termtip.bind(null, 'hull hardness', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('hrd')}</th>
|
||||
<th rowSpan={2}>{translate('crew')}</th>
|
||||
<th onMouseEnter={termtip.bind(null, 'mass lock factor', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('MLF')}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className='lft'>{translate('hull')}</th>
|
||||
<th onMouseEnter={termtip.bind(null, 'PHRASE_UNLADEN', { cap: 0 })} onMouseLeave={hide}>{translate('unladen')}</th>
|
||||
<th onMouseEnter={termtip.bind(null, 'PHRASE_LADEN', { cap: 0 })} onMouseLeave={hide}>{translate('laden')}</th>
|
||||
<th className='lft'>{translate('max')}</th>
|
||||
<th>{translate('full tank')}</th>
|
||||
<th>{translate('unladen')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
<th className='lft'>{translate('jumps')}</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{ ship.canThrust() ? <span>{int(ship.topSpeed)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{ ship.canBoost() ? <span>{int(ship.topBoost)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{f1(ship.totalDps)}</td>
|
||||
<td>{f1(ship.totalEps)}</td>
|
||||
<td>{f1(ship.totalHps)}</td>
|
||||
<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><span onMouseEnter={termtip.bind(null, 'TT_SUMMARY_MAX_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.jumpRange(ship.unladenMass + ship.standard[2].m.getMaxFuelPerJump(), ship.standard[2].m, ship.standard[2].m.getMaxFuelPerJump()))}{u.LY}</span></td>
|
||||
<td><span onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}</span></td>
|
||||
<td><span onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}</span></td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_TOTAL_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}</td>
|
||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_TOTAL_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}</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>{f1(ship.totalHps)}</td> */}
|
||||
<td>{round(ship.cargoCapacity)}{u.T}</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>{int(ship.armour)}</td>
|
||||
<td className={sgClassNames}>{int(ship.shield)} {u.MJ}</td>
|
||||
<td>{ship.hullMass} {u.T}</td>
|
||||
<td>{int(ship.unladenMass)} {u.T}</td>
|
||||
<td>{int(ship.ladenMass)} {u.T}</td>
|
||||
<td>{round(ship.cargoCapacity)} {u.T}</td>
|
||||
<td>{round(ship.fuelCapacity)} {u.T}</td>
|
||||
<td>{f2(ship.unladenRange)} {u.LY}</td>
|
||||
<td>{f2(ship.fullTankRange)} {u.LY}</td>
|
||||
<td>{f2(ship.ladenRange)} {u.LY}</td>
|
||||
<td>{int(ship.maxJumpCount)}</td>
|
||||
<td>{f2(ship.unladenFastestRange)} {u.LY}</td>
|
||||
<td>{f2(ship.ladenFastestRange)} {u.LY}</td>
|
||||
<td>{ship.crew}</td>
|
||||
<td>{ship.masslock}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -3,8 +3,6 @@ import TranslatedComponent from './TranslatedComponent';
|
||||
import cn from 'classnames';
|
||||
import AvailableModulesMenu from './AvailableModulesMenu';
|
||||
import ModificationsMenu from './ModificationsMenu';
|
||||
import { Modifications } from 'coriolis-data/dist';
|
||||
import { ListModifications } from './SvgIcons';
|
||||
import { diffDetails } from '../utils/SlotFunctions';
|
||||
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
@@ -81,7 +79,7 @@ export default class Slot extends TranslatedComponent {
|
||||
let language = this.context.language;
|
||||
let translate = language.translate;
|
||||
let { ship, m, dropClass, dragOver, onOpen, onChange, selected, eligible, onSelect, warning, availableModules } = this.props;
|
||||
let slotDetails, menu;
|
||||
let slotDetails, modificationsMarker, menu;
|
||||
|
||||
if (!selected) {
|
||||
// If not selected then sure that modifications flag is unset
|
||||
@@ -90,8 +88,10 @@ export default class Slot extends TranslatedComponent {
|
||||
|
||||
if (m) {
|
||||
slotDetails = this._getSlotDetails(m, 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) {
|
||||
@@ -101,6 +101,7 @@ export default class Slot extends TranslatedComponent {
|
||||
onChange={onChange}
|
||||
ship={ship}
|
||||
m={m}
|
||||
marker={modificationsMarker}
|
||||
/>;
|
||||
} else {
|
||||
menu = <AvailableModulesMenu
|
||||
|
||||
@@ -64,7 +64,7 @@ export default class SlotSection extends TranslatedComponent {
|
||||
* @param {Object} m Selected module
|
||||
*/
|
||||
_selectModule(slot, m) {
|
||||
this.props.ship.use(slot, m);
|
||||
this.props.ship.use(slot, m, false);
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
@@ -123,7 +123,7 @@ export default class SlotSection extends TranslatedComponent {
|
||||
// We want to copy the module in to the target slot
|
||||
if (targetSlot && canMount(this.props.ship, targetSlot, m.grp, m.class)) {
|
||||
const mCopy = m.clone();
|
||||
this.props.ship.use(targetSlot, mCopy);
|
||||
this.props.ship.use(targetSlot, mCopy, false);
|
||||
this.props.onChange();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -2,13 +2,14 @@ import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import Persist from '../stores/Persist';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { jumpRange } from '../shipyard/Calculations';
|
||||
import { diffDetails } from '../utils/SlotFunctions';
|
||||
import AvailableModulesMenu from './AvailableModulesMenu';
|
||||
import ModificationsMenu from './ModificationsMenu';
|
||||
import * 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
|
||||
@@ -54,6 +55,12 @@ export default class StandardSlot extends TranslatedComponent {
|
||||
let modTT = translate('modified');
|
||||
if (m && m.blueprint && m.blueprint.name) {
|
||||
modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
|
||||
modTT = (
|
||||
<div>
|
||||
<div>{modTT}</div>
|
||||
{blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
@@ -61,6 +68,8 @@ export default class StandardSlot extends TranslatedComponent {
|
||||
this._modificationsSelected = false;
|
||||
}
|
||||
|
||||
const modificationsMarker = JSON.stringify(m);
|
||||
|
||||
if (selected) {
|
||||
if (this._modificationsSelected) {
|
||||
menu = <ModificationsMenu
|
||||
@@ -68,12 +77,13 @@ export default class StandardSlot extends TranslatedComponent {
|
||||
onChange={onChange}
|
||||
ship={ship}
|
||||
m={m}
|
||||
marker={modificationsMarker}
|
||||
/>;
|
||||
} else {
|
||||
menu = <AvailableModulesMenu
|
||||
className='standard'
|
||||
modules={modules}
|
||||
shipMass={ship.ladenMass}
|
||||
shipMass={ModuleUtils.isShieldGenerator(m.grp) ? ship.hullMass : ship.unladenMass}
|
||||
m={m}
|
||||
onSelect={onSelect}
|
||||
warning={warning}
|
||||
@@ -91,9 +101,11 @@ export default class StandardSlot extends TranslatedComponent {
|
||||
<div className={'r'}>{formats.round(mass)}{units.T}</div>
|
||||
<div/>
|
||||
<div className={'cb'}>
|
||||
{ m.getOptimalMass() ? <div className='l'>{translate('optimal mass')}: {formats.int(m.getOptimalMass())}{units.T}</div> : null }
|
||||
{ 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.getRange() ? <div className='l'>{translate('range')}: {formats.f2(m.getRange())}{units.km}</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 }
|
||||
@@ -104,7 +116,7 @@ export default class StandardSlot extends TranslatedComponent {
|
||||
{ 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' ><button onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@ import cn from 'classnames';
|
||||
import SlotSection from './SlotSection';
|
||||
import StandardSlot from './StandardSlot';
|
||||
import Module from '../shipyard/Module';
|
||||
import { diffDetails } from '../utils/SlotFunctions';
|
||||
import * as ShipRoles from '../shipyard/ShipRoles';
|
||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||
|
||||
@@ -53,6 +52,16 @@ export default class StandardSlotSection extends SlotSection {
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Miner Build
|
||||
* @param {Boolean} shielded True if shield generator should be included
|
||||
*/
|
||||
_optimizeMiner(shielded) {
|
||||
ShipRoles.miner(this.props.ship, shielded);
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Explorer role
|
||||
* @param {Boolean} planetary True if Planetary Vehicle Hangar (PVH) should be included
|
||||
@@ -86,7 +95,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
* @return {Array} Array of Slots
|
||||
*/
|
||||
_getSlots() {
|
||||
let { ship, currentMenu } = this.props;
|
||||
let { ship, currentMenu, cargo, fuel } = this.props;
|
||||
let slots = new Array(8);
|
||||
let open = this._openMenu;
|
||||
let select = this._selectModule;
|
||||
@@ -126,7 +135,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
selected={currentMenu == st[1]}
|
||||
onChange={this.props.onChange}
|
||||
ship={ship}
|
||||
warning={m => m instanceof Module ? m.getMaxMass() < (ship.ladenMass - st[1].mass + m.mass) : m.maxmass < (ship.ladenMass - st[1].mass + m.mass)}
|
||||
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)}
|
||||
/>;
|
||||
|
||||
|
||||
@@ -161,7 +170,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
selected={currentMenu == st[4]}
|
||||
onChange={this.props.onChange}
|
||||
ship={ship}
|
||||
warning={m => m instanceof Module ? m.getEnginesCapacity() < ship.boostEnergy : m.engcap < ship.boostEnergy}
|
||||
warning={m => m instanceof Module ? m.getEnginesCapacity() <= ship.boostEnergy : m.engcap <= ship.boostEnergy}
|
||||
/>;
|
||||
|
||||
slots[6] = <StandardSlot
|
||||
@@ -209,6 +218,8 @@ export default class StandardSlotSection extends SlotSection {
|
||||
<li className='lc' onClick={this._optimizeCargo.bind(this, true)}>{translate('Shielded Trader')}</li>
|
||||
<li className='lc' onClick={this._optimizeExplorer.bind(this, false)}>{translate('Explorer')}</li>
|
||||
<li className={cn('lc', { disabled: planetaryDisabled })} onClick={!planetaryDisabled && this._optimizeExplorer.bind(this, true)}>{translate('Planetary Explorer')}</li>
|
||||
<li className='lc' onClick={this._optimizeMiner.bind(this, false)}>{translate('Miner')}</li>
|
||||
<li className='lc' onClick={this._optimizeMiner.bind(this, true)}>{translate('Shielded Miner')}</li>
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -227,6 +227,26 @@ export class LinkIcon extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shopping icon (dollar sign)
|
||||
*/
|
||||
export class ShoppingIcon extends SvgIcon {
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {String} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M94 188v-17c-9-1-16-3-21-6-6-3-11-7-15-14-4-6-6-14-6-23l17-3c2 9 4 16 7 21 5 6 11 9 18 10v-56c-7-1-14-4-22-8-6-3-10-8-13-13-3-6-4-12-4-19 0-13 4-23 13-31 6-5 15-8 26-9v-8h11v8c10 1 18 4 24 9 8 6 12 15 14 26l-18 3c-1-7-4-12-7-16s-8-6-13-7v50l17 6c6 2 10 5 13 8 4 4 7 8 8 13 2 4 3 10 3 15 0 12-4 22-11 31-8 8-18 12-30 13v17H94zm0-153c-7 1-12 3-16 8-4 4-6 9-6 15s2 11 5 16c4 4 9 7 17 9V35zm11 121a28 28 0 0 0 24-28c-1-6-2-11-6-15-3-4-9-7-18-10v53z'/>
|
||||
</g>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* No Power - Lightning bolt + no entry
|
||||
*/
|
||||
@@ -319,6 +339,24 @@ export class Warning extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute damage
|
||||
*/
|
||||
export class DamageAbsolute extends SvgIcon {
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {String} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <ellipse cx='100' cy='100' rx='90' ry='90' fillOpacity='0' />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thermal damage
|
||||
*/
|
||||
@@ -534,6 +572,25 @@ export class Rocket extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Help
|
||||
*/
|
||||
export class Help extends SvgIcon {
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {String} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d="M90.381 140.644a225.03 225.03 0 0 1-.106-5.706c0-7.47 1.058-13.92 3.172-19.346 1.55-4.087 4.052-8.21 7.505-12.367 2.536-3.031 7.1-7.453 13.688-13.267 6.59-5.815 10.872-10.448 12.844-13.9 1.974-3.453 2.96-7.224 2.96-11.312 0-7.4-2.89-13.9-8.667-19.503-5.779-5.602-12.862-8.403-21.248-8.403-8.104 0-14.87 2.536-20.296 7.61-5.427 5.074-8.986 13.003-10.677 23.784L50 65.91c1.762-14.448 6.995-25.511 15.698-33.194 8.704-7.68 20.208-11.522 34.514-11.522 15.152 0 27.237 4.124 36.258 12.369C145.49 41.807 150 51.779 150 63.477c0 6.766-1.585 13.003-4.756 18.712-3.17 5.708-9.372 12.65-18.605 20.824-6.201 5.496-10.253 9.55-12.156 12.156-1.904 2.609-3.312 5.603-4.23 8.986-.915 3.383-1.443 8.88-1.584 16.49H90.38zm-1.163 38.162v-21.67h21.67v21.67h-21.67z"/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ListModifications (engineers)
|
||||
*/
|
||||
@@ -651,6 +708,24 @@ export class Switch extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pip
|
||||
*/
|
||||
export class Pip extends SvgIcon {
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {String} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <rect x='10' y='10' width='180' height='180'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-game Coriolis Station logo
|
||||
*/
|
||||
|
||||
@@ -99,11 +99,11 @@ export default class UtilitySlotSection extends SlotSection {
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('sb')}</div>
|
||||
<ul>
|
||||
<li className='c' onClick={_use.bind(this, 'sb', 'E', null)}>E</li>
|
||||
<li className='c' onClick={_use.bind(this, 'sb', 'D', null)}>D</li>
|
||||
<li className='c' onClick={_use.bind(this, 'sb', 'C', null)}>C</li>
|
||||
<li className='c' onClick={_use.bind(this, 'sb', 'B', null)}>B</li>
|
||||
<li className='c' onClick={_use.bind(this, 'sb', 'A', null)}>A</li>
|
||||
<li className='c' onClick={_use.bind(this, 'sb', 'B', null)}>B</li>
|
||||
<li className='c' onClick={_use.bind(this, 'sb', 'C', null)}>C</li>
|
||||
<li className='c' onClick={_use.bind(this, 'sb', 'D', null)}>D</li>
|
||||
<li className='c' onClick={_use.bind(this, 'sb', 'E', null)}>E</li>
|
||||
</ul>
|
||||
<div className='select-group cap'>{translate('hs')}</div>
|
||||
<ul>
|
||||
|
||||
107
src/app/components/VerticalBarChart.jsx
Normal file
107
src/app/components/VerticalBarChart.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import React, { PropTypes } from 'react';
|
||||
import Measure from 'react-measure';
|
||||
import { BarChart, Bar, XAxis, YAxis } from 'recharts';
|
||||
|
||||
const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D'];
|
||||
const LABEL_COLOUR = '#000000';
|
||||
const AXIS_COLOUR = '#C06400';
|
||||
|
||||
const ASPECT = 1;
|
||||
|
||||
const merge = function(one, two) {
|
||||
return Object.assign({}, one, two);
|
||||
};
|
||||
|
||||
/**
|
||||
* A vertical bar chart
|
||||
*/
|
||||
export default class VerticalBarChart extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
data : PropTypes.array.isRequired,
|
||||
yMax : PropTypes.number
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
this._termtip = this._termtip.bind(this);
|
||||
|
||||
this.state = {
|
||||
dimensions: {
|
||||
width: 300,
|
||||
height: 300
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the bar chart
|
||||
* @returns {Object} the markup
|
||||
*/
|
||||
render() {
|
||||
const { width, height } = this.state.dimensions;
|
||||
const { tooltip, termtip } = this.context;
|
||||
|
||||
// Calculate maximum for Y
|
||||
let dataMax = Math.max(...this.props.data.map(d => d.value));
|
||||
if (dataMax == -Infinity) dataMax = 0;
|
||||
let yMax = this.props.yMax ? Math.round(this.props.yMax) : 0;
|
||||
const localMax = Math.max(dataMax, yMax);
|
||||
|
||||
return (
|
||||
<Measure whitelist={['width', 'top']} onMeasure={ (dimensions) => this.setState({ dimensions }) }>
|
||||
<div width='100%'>
|
||||
<BarChart width={width} height={width * ASPECT} data={this.props.data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
|
||||
<XAxis interval={0} fontSize='0.8em' stroke={AXIS_COLOUR} dataKey='label' />
|
||||
<YAxis interval={'preserveStart'} tickCount={11} fontSize='0.8em' stroke={AXIS_COLOUR} type='number' domain={[0, localMax]}/>
|
||||
<Bar dataKey='value' label={<ValueLabel />} fill={CORIOLIS_COLOURS[0]} isAnimationActive={false} onMouseOver={this._termtip} onMouseOut={tooltip.bind(null, null)}/>
|
||||
</BarChart>
|
||||
</div>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a term tip
|
||||
* @param {Object} d the data
|
||||
* @param {number} i the index
|
||||
* @param {Object} e the event
|
||||
* @returns {Object} termtip markup
|
||||
*/
|
||||
_termtip(d, i, e) {
|
||||
if (this.props.data[i].tooltip) {
|
||||
return this.context.termtip(this.props.data[i].tooltip, e);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A label that displays the value within the bar of the chart
|
||||
*/
|
||||
const ValueLabel = React.createClass({
|
||||
propTypes: {
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.object,
|
||||
value: PropTypes.number
|
||||
},
|
||||
|
||||
render() {
|
||||
const { x, y, payload, value } = this.props;
|
||||
|
||||
const em = value < 1000 ? '1em' : value < 1000 ? '0.8em' : '0.7em';
|
||||
|
||||
return (
|
||||
<text x={x} y={y} fill="#000000" textAnchor="middle" dy={20} style={{ fontSize: em }}>{value}</text>
|
||||
);
|
||||
}
|
||||
});
|
||||
203
src/app/components/WeaponDamageChart.jsx
Normal file
203
src/app/components/WeaponDamageChart.jsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import { nameComparator } from '../utils/SlotFunctions';
|
||||
import { CollapseSection, ExpandSection, MountFixed, MountGimballed, MountTurret } from './SvgIcons';
|
||||
import LineChart from '../components/LineChart';
|
||||
import Slider from '../components/Slider';
|
||||
import * as Calc from '../shipyard/Calculations';
|
||||
import Module from '../shipyard/Module';
|
||||
|
||||
const DAMAGE_DEALT_COLORS = ['#FFFFFF', '#FF0000', '#00FF00', '#7777FF', '#FFFF00', '#FF00FF', '#00FFFF', '#777777'];
|
||||
|
||||
/**
|
||||
* Weapon damage chart
|
||||
*/
|
||||
export default class WeaponDamageChart extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
opponent: React.PropTypes.object.isRequired,
|
||||
hull: React.PropTypes.bool.isRequired,
|
||||
engagementRange: React.PropTypes.number.isRequired,
|
||||
opponentSys: React.PropTypes.number.isRequired,
|
||||
marker: React.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 sortOrder = this._sortOrder;
|
||||
const onCollapseExpand = this._onCollapseExpand;
|
||||
|
||||
const code = `${ship.toString()}:${opponent.toString()}`;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<LineChart
|
||||
xMax={maxRange}
|
||||
yMax={this.state.maxDps}
|
||||
xLabel={translate('range')}
|
||||
xUnit={translate('m')}
|
||||
yLabel={translate('sdps')}
|
||||
series={this.state.weaponNames}
|
||||
xMark={this.props.engagementRange}
|
||||
colors={DAMAGE_DEALT_COLORS}
|
||||
func={this.state.calcSDpsFunc}
|
||||
points={200}
|
||||
code={code}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import * as FR from './fr';
|
||||
import * as IT from './it';
|
||||
import * as RU from './ru';
|
||||
import * as PL from './pl';
|
||||
import d3 from 'd3';
|
||||
import * as d3 from 'd3';
|
||||
|
||||
let fallbackTerms = EN.terms;
|
||||
|
||||
@@ -30,48 +30,49 @@ export function getLanguage(langCode) {
|
||||
}
|
||||
|
||||
let currentTerms = lang.terms;
|
||||
let d3Locale = d3.locale(lang.formats);
|
||||
let gen = d3Locale.numberFormat('n');
|
||||
let d3Locale = d3.formatLocale(lang.formats);
|
||||
let gen = d3Locale.format('');
|
||||
const round = function(x, n) { const ten_n = Math.pow(10,n); return Math.round(x * ten_n) / ten_n; };
|
||||
|
||||
if(lang === EN) {
|
||||
translate = (t) => { return currentTerms[t] || t; };
|
||||
translate = (t, x) => { return currentTerms[t + '_' + x] || currentTerms[t] || t; };
|
||||
} else {
|
||||
translate = (t) => { return currentTerms[t] || fallbackTerms[t] || t; };
|
||||
translate = (t, x) => { return currentTerms[t + '_' + x] || currentTerms[t] || fallbackTerms[t + '_' + x] || fallbackTerms[t] || t; };
|
||||
}
|
||||
|
||||
return {
|
||||
formats: {
|
||||
gen, // General number format (.e.g 1,001,001.1234)
|
||||
int: d3Locale.numberFormat(',.0f'), // Fixed to 0 decimal places (.e.g 1,001)
|
||||
f1: d3Locale.numberFormat(',.1f'), // Fixed to 1 decimal place (.e.g 1,001.1)
|
||||
f2: d3Locale.numberFormat(',.2f'), // Fixed to 2 decimal places (.e.g 1,001.10)
|
||||
s2: d3Locale.numberFormat('.2s'), // SI Format to 2 decimal places (.e.g 1.1k)
|
||||
pct: d3Locale.numberFormat('.2%'), // % to 2 decimal places (.e.g 5.40%)
|
||||
pct1: d3Locale.numberFormat('.1%'), // % to 1 decimal places (.e.g 5.4%)
|
||||
r1: d3Locale.numberFormat('.1r'), // Rounded to 1 significant number (.e.g 512 => 500, 4.122 => 4)
|
||||
r2: d3Locale.numberFormat('.2r'), // Rounded to 2 significant numbers (.e.g 512 => 510, 4.122 => 4.1)
|
||||
rPct: d3.format('%'), // % to 0 decimal places (.e.g 5%)
|
||||
round1: (d) => gen(d3.round(d, 1)), // Rounded to 0-1 decimal places (.e.g 5.1, 4)
|
||||
round: (d) => gen(d3.round(d, 2)), // Rounded to 0-2 decimal places (.e.g 5.12, 4.1)
|
||||
gen, // General number format (.e.g 1,001,001.1234)
|
||||
int: d3Locale.format(',.0f'), // Fixed to 0 decimal places (.e.g 1,001)
|
||||
f1: d3Locale.format(',.1f'), // Fixed to 1 decimal place (.e.g 1,001.1)
|
||||
f2: d3Locale.format(',.2f'), // Fixed to 2 decimal places (.e.g 1,001.10)
|
||||
s2: d3Locale.format('.2s'), // SI Format to 2 decimal places (.e.g 1.1k)
|
||||
pct: d3Locale.format('.2%'), // % to 2 decimal places (.e.g 5.40%)
|
||||
pct1: d3Locale.format('.1%'), // % to 1 decimal places (.e.g 5.4%)
|
||||
r1: d3Locale.format('.1r'), // Rounded to 1 significant number (.e.g 512 => 500, 4.122 => 4)
|
||||
r2: d3Locale.format('.2r'), // Rounded to 2 significant numbers (.e.g 512 => 510, 4.122 => 4.1)
|
||||
rPct: d3Locale.format('.0%'), // % to 0 decimal places (.e.g 5%)
|
||||
round1: (d) => gen(round(d, 1)), // Round to 0-1 decimal places (e.g. 5.1, 4)
|
||||
round: (d) => gen(round(d, 2)), // Rounded to 0-2 decimal places (.e.g 5.12, 4.1)
|
||||
time: (d) => (d < 0 ? '-' : '') + Math.floor(Math.abs(d) / 60) + ':' + ('00' + Math.floor(Math.abs(d) % 60)).substr(-2, 2)
|
||||
},
|
||||
translate,
|
||||
units: {
|
||||
CR: <u> {translate('CR')}</u>, // Credits
|
||||
kg: <u> {translate('kg')}</u>, // Kilograms
|
||||
kgs: <u> {translate('kg/s')}</u>, // Kilograms per second
|
||||
km: <u> {translate('km')}</u>, // Kilometers
|
||||
Ls: <u> {translate('Ls')}</u>, // Light Seconds
|
||||
LY: <u> {translate('LY')}</u>, // Light Years
|
||||
MJ: <u> {translate('MJ')}</u>, // Mega Joules
|
||||
'm/s': <u> {translate('m/s')}</u>, // Meters per second
|
||||
'°/s': <u> {translate('°/s')}</u>, // Degrees per second
|
||||
MW: <u> {translate('MW')}</u>, // Mega Watts (same as Mega Joules per second)
|
||||
CR: <u>{translate('CR')}</u>, // Credits
|
||||
kg: <u>{translate('kg')}</u>, // Kilograms
|
||||
kgs: <u>{translate('kg/s')}</u>, // Kilograms per second
|
||||
km: <u>{translate('km')}</u>, // Kilometers
|
||||
Ls: <u>{translate('Ls')}</u>, // Light Seconds
|
||||
LY: <u>{translate('LY')}</u>, // Light Years
|
||||
MJ: <u>{translate('MJ')}</u>, // Mega Joules
|
||||
'm/s': <u>{translate('m/s')}</u>, // Meters per second
|
||||
'°/s': <u>{translate('°/s')}</u>, // Degrees per second
|
||||
MW: <u>{translate('MW')}</u>, // Mega Watts (same as Mega Joules per second)
|
||||
mps: <u>{translate('m/s')}</u>, // Metres per second
|
||||
ps: <u>{translate('/s')}</u>, // per second
|
||||
pm: <u>{translate('/min')}</u>, // per minute
|
||||
s: <u>{translate('secs')}</u>, // Seconds
|
||||
T: <u> {translate('T')}</u>, // Metric Tons
|
||||
T: <u>{translate('T')}</u>, // Metric Tons
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ export const terms = {
|
||||
PHRASE_NO_BUILDS: 'No builds added to comparison!',
|
||||
PHRASE_NO_RETROCH: 'No Retrofitting changes',
|
||||
PHRASE_SELECT_BUILDS: 'Select builds to compare',
|
||||
PHRASE_SG_RECHARGE: 'Time from 50% to 100% charge',
|
||||
PHRASE_SG_RECOVER: 'Recovery (to 50%) after collapse',
|
||||
PHRASE_SG_RECHARGE: 'Time from 50% to 100% charge, assuming full SYS capacitor to start with',
|
||||
PHRASE_SG_RECOVER: 'Time from 0% to 50% charge, assuming full SYS capacitor to start with',
|
||||
PHRASE_UNLADEN: 'Ship mass excluding fuel and cargo',
|
||||
PHRASE_UPDATE_RDY: 'Update Available! Click to refresh',
|
||||
PHRASE_ENGAGEMENT_RANGE: 'The distance between your ship and its target',
|
||||
@@ -33,7 +33,64 @@ export const terms = {
|
||||
PHRASE_BLUEPRINT_WORST: 'Worst primary values for this blueprint',
|
||||
PHRASE_BLUEPRINT_RANDOM: 'Random selection between worst and best primary values for this blueprint',
|
||||
PHRASE_BLUEPRINT_BEST: 'Best primary values for this blueprint',
|
||||
PHRASE_BLUEPRINT_EXTREME: 'Best beneficial and worst detrimental primary values for this blueprint',
|
||||
PHRASE_BLUEPRINT_RESET: 'Remove all modifications and blueprint',
|
||||
PHRASE_SELECT_SPECIAL: 'Click to select an experimental effect',
|
||||
PHRASE_NO_SPECIAL: 'No experimental effect',
|
||||
PHRASE_SHOPPING_LIST: 'Stations that sell this build',
|
||||
PHRASE_TOTAL_EFFECTIVE_SHIELD: 'Total amount of damage that can be taken from each damage type, if using all shield cells',
|
||||
PHRASE_TIME_TO_LOSE_SHIELDS: 'Shields will hold for',
|
||||
PHRASE_TIME_TO_RECOVER_SHIELDS: 'Shields will recover in',
|
||||
PHRASE_TIME_TO_RECHARGE_SHIELDS: 'Shields will recharge in',
|
||||
PHRASE_SHIELD_SOURCES: 'Breakdown of the supply of shield energy',
|
||||
PHRASE_EFFECTIVE_SHIELD: 'Effective shield strength against different damage types',
|
||||
PHRASE_ARMOUR_SOURCES: 'Breakdown of the supply of armour',
|
||||
PHRASE_EFFECTIVE_ARMOUR: 'Effective armour strength against different damage types',
|
||||
PHRASE_DAMAGE_TAKEN: '% of raw damage taken for different damage types',
|
||||
PHRASE_TIME_TO_LOSE_ARMOUR: 'Armour will hold for',
|
||||
PHRASE_MODULE_PROTECTION_EXTERNAL: 'Protection for hardpoints',
|
||||
PHRASE_MODULE_PROTECTION_INTERNAL: 'Protection for all other modules',
|
||||
PHRASE_SHIELD_DAMAGE: 'Breakdown of sources for sustained DPS against shields',
|
||||
PHRASE_ARMOUR_DAMAGE: 'Breakdown of sources for sustained DPS against armour',
|
||||
|
||||
PHRASE_TIME_TO_REMOVE_SHIELDS: 'Will remove shields in',
|
||||
TT_TIME_TO_REMOVE_SHIELDS: 'With sustained fire by all weapons',
|
||||
PHRASE_TIME_TO_REMOVE_ARMOUR: 'Will remove armour in',
|
||||
TT_TIME_TO_REMOVE_ARMOUR: 'With sustained fire by all weapons',
|
||||
PHRASE_TIME_TO_DRAIN_WEP: 'Will drain WEP in',
|
||||
TT_TIME_TO_DRAIN_WEP: 'Time to drain WEP capacitor with all weapons firing',
|
||||
TT_TIME_TO_LOSE_SHIELDS: 'Against sustained fire from all opponent\'s weapons',
|
||||
TT_TIME_TO_LOSE_ARMOUR: 'Against sustained fire from all opponent\'s weapons',
|
||||
TT_MODULE_ARMOUR: 'Armour protecting against module damage',
|
||||
TT_MODULE_PROTECTION_EXTERNAL: 'Percentage of damage diverted from hardpoints to module reinforcement packages',
|
||||
TT_MODULE_PROTECTION_INTERNAL: 'Percentage of damage diverted from non-hardpoint modules to module reinforcement packages',
|
||||
|
||||
TT_EFFECTIVE_SDPS_SHIELDS: 'Actual sustained DPS whilst WEP capacitor is not empty',
|
||||
TT_EFFECTIVENESS_SHIELDS: 'Effectivness compared to hitting a 0-resistance target with 0 pips to SYS at 0m',
|
||||
TT_EFFECTIVE_SDPS_ARMOUR: 'Actual sustained DPS whilst WEP capacitor is not empty',
|
||||
TT_EFFECTIVENESS_ARMOUR: 'Effectivness compared to hitting a 0-resistance target at 0m',
|
||||
|
||||
PHRASE_EFFECTIVE_SDPS_SHIELDS: 'SDPS against shields',
|
||||
PHRASE_EFFECTIVE_SDPS_ARMOUR: 'SDPS against armour',
|
||||
|
||||
TT_SUMMARY_SPEED: 'With full fuel tank and 4 pips to ENG',
|
||||
TT_SUMMARY_SPEED_NONFUNCTIONAL: 'Thrusters powered off or over maximum mass',
|
||||
TT_SUMMARY_BOOST: 'With full fuel tank and 4 pips to ENG',
|
||||
TT_SUMMARY_BOOST_NONFUNCTIONAL: 'Power distributor not able to supply enough power to boost',
|
||||
TT_SUMMARY_SHIELDS: 'Raw shield strength, including boosters',
|
||||
TT_SUMMARY_SHIELDS_NONFUNCTIONAL: 'No shield generator or shield generator powered off',
|
||||
TT_SUMMARY_INTEGRITY: 'Ship integrity, including bulkheads and hull reinforcement packages',
|
||||
TT_SUMMARY_HULL_MASS: 'Mass of the hull prior to any modules being installed',
|
||||
TT_SUMMARY_UNLADEN_MASS: 'Mass of the hull and modules prior to any fuel or cargo',
|
||||
TT_SUMMARY_LADEN_MASS: 'Mass of the hull and modules with full fuel and cargo',
|
||||
TT_SUMMARY_DPS: 'Damage per second with all weapons firing',
|
||||
TT_SUMMARY_EPS: 'WEP capacitor consumed per second with all weapons firing',
|
||||
TT_SUMMARY_TTD: 'Time to drain WEP capacitor with all weapons firing and 4 pips to WEP',
|
||||
TT_SUMMARY_MAX_SINGLE_JUMP: 'Farthest possible jump range with no cargo and only enough fuel for the jump itself',
|
||||
TT_SUMMARY_UNLADEN_SINGLE_JUMP: 'Farthest possible jump range with no cargo and a full fuel tank',
|
||||
TT_SUMMARY_LADEN_SINGLE_JUMP: 'Farthest possible jump range with full cargo and a full fuel tank',
|
||||
TT_SUMMARY_UNLADEN_TOTAL_JUMP: 'Farthest possible range with no cargo, a full fuel tank, and jumping as far as possible each time',
|
||||
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',
|
||||
|
||||
@@ -84,9 +141,10 @@ export const terms = {
|
||||
rg: 'Rail Gun',
|
||||
s: 'Sensors',
|
||||
sb: 'Shield Booster',
|
||||
sc: 'Scanner',
|
||||
sc: 'Stellar Scanners',
|
||||
scb: 'Shield Cell Bank',
|
||||
sg: 'Shield Generator',
|
||||
ss: 'Surface Scanners',
|
||||
t: 'thrusters',
|
||||
tp: 'Torpedo Pylon',
|
||||
ul: 'Burst Laser',
|
||||
@@ -95,10 +153,12 @@ export const terms = {
|
||||
// Items on the outfitting page
|
||||
// Notification of restricted slot
|
||||
emptyrestricted: 'empty (restricted)',
|
||||
'damage dealt against': 'Damage dealt against',
|
||||
'damage received by': 'Damage received by',
|
||||
'damage dealt to': 'Damage dealt to',
|
||||
'damage received from': 'Damage received from',
|
||||
'against shields': 'Against shields',
|
||||
'against hull': 'Against hull',
|
||||
'total effective shield': 'Total effective shield',
|
||||
|
||||
// 'ammo' was overloaded for outfitting page and modul info, so changed to ammunition for outfitting page
|
||||
ammunition: 'Ammo',
|
||||
|
||||
@@ -109,8 +169,10 @@ export const terms = {
|
||||
|
||||
// Blueprint rolls
|
||||
worst: 'Worst',
|
||||
average: 'Average',
|
||||
random: 'Random',
|
||||
best: 'Best',
|
||||
extreme: 'Extreme',
|
||||
reset: 'Reset',
|
||||
|
||||
// Weapon, offence, defence and movement
|
||||
@@ -171,6 +233,8 @@ export const terms = {
|
||||
regen: 'Regeneration rate',
|
||||
reload: 'Reload',
|
||||
rof: 'Rate of fire',
|
||||
angle: 'Scan angle',
|
||||
scantime: 'Scan time',
|
||||
shield: 'Shield',
|
||||
shieldboost: 'Shield boost',
|
||||
shieldreinforcement: 'Shield reinforcement',
|
||||
@@ -182,4 +246,223 @@ export const terms = {
|
||||
thermres: 'Thermal resistance',
|
||||
wepcap: 'Weapons capacity',
|
||||
weprate: 'Weapons recharge rate',
|
||||
|
||||
// Shield generators use a different terminology
|
||||
minmass_sg: 'Minimum hull mass',
|
||||
optmass_sg: 'Optimal hull mass',
|
||||
maxmass_sg: 'Maximum hull mass',
|
||||
minmul_sg: 'Minimum strength',
|
||||
optmul_sg: 'Optimal strength',
|
||||
maxmul_sg: 'Minimum strength',
|
||||
minmass_psg: 'Minimum hull mass',
|
||||
optmass_psg: 'Optimal hull mass',
|
||||
maxmass_psg: 'Maximum hull mass',
|
||||
minmul_psg: 'Minimum strength',
|
||||
optmul_psg: 'Optimal strength',
|
||||
maxmul_psg: 'Minimum strength',
|
||||
minmass_bsg: 'Minimum hull mass',
|
||||
optmass_bsg: 'Optimal hull mass',
|
||||
maxmass_bsg: 'Maximum hull mass',
|
||||
minmul_bsg: 'Minimum strength',
|
||||
optmul_bsg: 'Optimal strength',
|
||||
maxmul_bsg: 'Minimum strength',
|
||||
|
||||
range_s: 'Typical emission range',
|
||||
|
||||
// Damage types
|
||||
absolute: 'Absolute',
|
||||
explosive: 'Explosive',
|
||||
kinetic: 'Kinetic',
|
||||
thermal: 'Thermal',
|
||||
|
||||
// Shield sources
|
||||
generator: 'Generator',
|
||||
boosters: 'Boosters',
|
||||
cells: 'Cells',
|
||||
|
||||
// Armour sources
|
||||
bulkheads: 'Bulkheads',
|
||||
reinforcement: 'Reinforcement',
|
||||
|
||||
// Help text
|
||||
HELP_TEXT: `
|
||||
<h1>Introduction</h1>
|
||||
Coriolis is a ship builder for Elite: Dangerous. This help file provides you with the information you need to use Coriolis.
|
||||
|
||||
<h1>Importing Your Ship Into Coriolis</h1>
|
||||
Often, you will want to start with your existing ship in Coriolis and see how particular changes might affect it, for example upgrading your FSD. There are a number of tools that can be used to import your ship without you having to create it manually. This has the added benefit of copying over any engineering modifications that have taken place as well. </p>
|
||||
|
||||
<h2>Importing Your Ship From EDDI</h2>
|
||||
To import your ship from EDDI first ensure that your connection to the Frontier servers' companion API is working. To do this check the 'Companion App' tab where you should see "Your connection to the companion app is operational". If not then follow the instructions in the companion app tab in EDDI to connect to the Frontier servers.</p>
|
||||
|
||||
Once you have a working companion API connection go to the 'Shipyard' tab. At the right-hand side of each ship is an 'Export to Coriolis' button that will open your default web browser in Coriolis with the ship's build. </p>
|
||||
|
||||
Note that Internet Explorer and Edge might not import correctly, due to their internal restrictions on URL length. If you find that this is the case then please change your default browser to Chrome. </p>
|
||||
|
||||
Also, the imported information does not provide any data on the power priority or enabled status of your cargo hatch. Coriolis sets this item to have a power priority of "5" and to be disabled by default. You can change this after import in the Power Management section. </p>
|
||||
|
||||
<h2>Importing Your Ship From EDMC</h2>
|
||||
To import your ship from EDMC once your connection to the Frontier servers' companion API is working go to 'Settings ->Configuration' and set the 'Preferred Shipyard' to 'Coriolis'. Once this is set up clicking on your ship in the main window will open your default web browser in Coriolis with the ship's build.</p>
|
||||
|
||||
Note that Internet Explorer and Edge might not import correctly, due to their internal restrictions on URL length. If you find that this is the case then please change your default browser to Chrome. </p>
|
||||
|
||||
<h1>Understanding And Using The Outfitting Panels</h1>
|
||||
The outfitting page is where you will spend most of your time, and contains the information for your ship. Information on each of the panels is provided below. </p>
|
||||
|
||||
<h2>Key Values</h2>
|
||||
Along the top of the screen are some of the key values for your build. This is a handy reference for the values, but more information is provided for the values in the further panels. </p>
|
||||
|
||||
Here, along with most places in Coriolis, acronyms will have tooltips explaining what they mean. Hover over the acronym to obtain more detail, or look in the glossary at the end of this help.</p>
|
||||
|
||||
All values are the highest possible, assuming that you an optimal setup for that particular value (maximum pips in ENG for speed, minimum fuel for jump range, etc.). This means that these values will not be affected by changes to pip settings. Details of the specific setup for each value are listed in the associated tootip.</p>
|
||||
|
||||
<h2>Modules</h2>
|
||||
The next set of panels laid out horizontally across the screen contain the modules you have put in your build. From left to right these are the core modules, the internal modules, the hardpoints and the utility mounts. These represent the available slots in your ship and cannot be altered. Each slot has a class, or size, and in general any module up to a given size can fit in a given slot (exceptions being bulkheads, life support and sensors in core modules and restricted internal slots, which can only take a subset of module depending on their restrictions). </p>
|
||||
|
||||
To add a module to a slot left-click on the slot and select the required module. Only the modules capable of fitting in the selected slot will be shown. </p>
|
||||
|
||||
To remove a module from a slot right-click on the module. </p>
|
||||
|
||||
To move a module from one slot to another drag it. If you instead want to copy the module drag it whilst holding down the 'Alt' key. </p>
|
||||
|
||||
Clicking on the headings for each set of modules gives you the ability to either select an overall role for your ship (when clicking the core internal header) or a specific module with which you want to fill all applicable slots (when clicking the other headers). </p>
|
||||
|
||||
<h2>Ship Controls</h2>
|
||||
The ship controls allow you to set your pips, boost, and amount of fuel and cargo that your build carries. The changes made here will effect the information supplied in the subsequent panels, giving you a clearer view of what effect different changing these items will have. </p>
|
||||
|
||||
Ship control settings are saved as part of a build. </p>
|
||||
|
||||
<h2>Opponent</h2>
|
||||
The opponet selection allows you to choose your opponent. The opponent can be either a stock build of a ship or one of your own saved builds. You can also set the engagement range between you and your opponent. Your selection here will effect the information supplied in the subsequent panels, specifically the Offence and Defence panels. </p>
|
||||
|
||||
Opponent settings are saved as part of a build. </p>
|
||||
|
||||
<h2>Power and Costs Sub-panels</h2>
|
||||
<h3>Power</h3>
|
||||
The power management panel provides information about power usage and priorities. It allows you to enable and disable individual modules, as well as set power priorities for each module. Disabled modules will not be included in the build's statistics, with the exception of Shield Cell Banks as they are usually disabled when not in use and only enabled when required. </p>
|
||||
|
||||
<h3>Costs</h3>
|
||||
The costs panel provides information about the costs for each of your modules, and the total cost and insurance for your build. By default Coriolis uses the standard costs, however discounts for your ship, modules and insurance can be altered in the 'Settings' at the top-right of the page.</p>
|
||||
|
||||
The retrofit costs provides information about the costs of changing the base build for your ship, or your saved build, to the current build.</p>
|
||||
|
||||
The reload costs provides information about the costs of reloading your current build.</p>
|
||||
|
||||
<h2>Profiles</h2>
|
||||
Profiles provide graphs that show the general performance of modules in your build
|
||||
|
||||
<h3>Engine Profile</h3>
|
||||
The engine profile panel provides information about the capabilities of your current thrusters. The graph shows you how the maximum speed alters with the overall mass of your build. The vertical dashed line on the graph shows your current mass. Your engine profile can be altered by obtaining different thrusters or engineering your existing thrusters, and you can increase your maximum speed by adding pips to the ENG capacitor as well as reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build. You can also temporarily increase your speed by hitting the boost button. </p>
|
||||
|
||||
<h3>FSD Profile</h3>
|
||||
The FSD profile panel provides information about the capabilities of your current frame shift drive. The graph shows you how the maximum jump range alters with the overall mass of your build. The vertical dashed line on the graph shows your current maximum single jump range. Your FSD profile can be altered by obtaining a different FSD or engineering your existing FSD, and you can increase your maximum jump range by reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build, </p>
|
||||
|
||||
<h3>Movement Profile</h3>
|
||||
The movement profile panel provides information about the capabilities of your current thrusters with your current overall mass and ENG pips settings. The diagram shows your ability to move and rotate in the different axes:
|
||||
|
||||
<dl>
|
||||
<dt>Speed</dt><dd>The fastest the ship can move, in metres per second</dd>
|
||||
<dt>Pitch</dt><dd>The fastest the ship can raise or lower its nose, in degrees per second</dd>
|
||||
<dt>Roll</dt><dd>The fastest the ship can roll its body, in degrees per second</dd>
|
||||
<dt>Yaw</dt><dd>The fastest the ship can turn its nose left or right, in degrees per second</dd>
|
||||
</dl>
|
||||
|
||||
Your movement profile can be altered by obtaining different thrusters or engineering your existing thrusters, and you can increase your movement values by adding pips to the ENG capacitor as well as reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build. You can also temporarily increase your movement profile by hitting the boost button. </p>
|
||||
|
||||
<h3>Damage Profile</h3>
|
||||
The damage profile provides two graphs showing how the the build's damage to the opponent's shields and hull change with engagement range. The vertical dashed line on the graph shows your current engagement range. This combines information about the build's weapons with the opponent's shields and hull to provide an accurate picture of sustained damage that can be inflicted on the opponent. </p>
|
||||
|
||||
<h2>Offence</h2>
|
||||
<h3>Summary</h3>
|
||||
The offence summary provides per-weapon information about sustained damage per second inflicted to shields and hull, along with a measure of effectiveness of that weapon. The effectiveness value has a tooltip that provides a breakdown of the effectiveness, and can include reductions or increases due to range, resistance, and either power distributor (for shields) or hardness (for hull). The final effectiveness value is calculated by multiplying these percentages together. </p>
|
||||
|
||||
<h3>Offence Metrics</h3>
|
||||
The offence metrics panel provides information about your offence. </p>
|
||||
|
||||
Time to drain is a measure of how quickly your WEP capacitor will drain when firing all weapons. It is affected by the number of pips you have in your WEP capacitor, with more pips resulting in a higher WEP recharge rate and hence a longer time to drain. </p>
|
||||
|
||||
The next value is the time it will take you to remove your opponent's shields. This assumes that you have 100% time on target and that your engagement range stays constant. Note that if your time to remove shields is longer than your time to drain this assumes that you continue firing throughout, inflicting lower damage due to the reduced energy in your WEP capacitor. </p>
|
||||
|
||||
The next value is the time it will take you to remove your opponent's armour. This follows the same logic as the time to remove shields. </p>
|
||||
|
||||
<h3>Shield Damage Sources</h3>
|
||||
The shield damage sources provides information about the sources of damage to your opponent by damage type. For each applicable type of damage (absolute explosive, kinetic, thermal) a sustained damage per second value is provided. </p>
|
||||
|
||||
<h3>Hull Damage Sources</h3>
|
||||
The hull damage sources provides information about the sources of damage to your opponent by damage type. For each applicable type of damage (absolute explosive, kinetic, thermal) a sustained damage per second value is provided. </p>
|
||||
|
||||
<h2>Defence</h2>
|
||||
<h3>Shield Metrics</h3>
|
||||
The shield metrics provides information about your shield defence. </p>
|
||||
|
||||
Raw shield strength is the sum of the shield from your generator, boosters and shield cell banks. A tooltip provides a breakdown of these values. </p>
|
||||
|
||||
The time the shields will hold for is the time it will take your opponent' to remove your shields. This assumes that they have 100% time on target and that the engagement range stays constant. It also assumes that you fire all of your shield cell banks prior to your shields being lost. </p>
|
||||
|
||||
The time the shields will recover in is the time it will take your shields to go from collapsed (0%) to recovered (50%). This is affected by the number of pips you have in your SYS capacitor. </p>
|
||||
|
||||
The time the shields will recharge in is the time it will take your shields to go from recovered (50%) to full (100%). This is affected by the number of pips you have in your SYS capacitor. </p>
|
||||
|
||||
</h3>Shield Sources</h3>
|
||||
This chart provides information about the sources of your shields. For each applicable source of shields (generator, boosters, shield cell banks) a value is provided. </p>
|
||||
|
||||
</h3>Damage Taken</h3>
|
||||
This graph shows how the initial damage from the weapons of each type are reduced before their damage is applied to the shields. For each type of damage (absolute, explosive, kinetic, thermal) a percentage of the initial damage is provided. A tooltip provides a breakdown of these values. </p>
|
||||
|
||||
</h3>Effective Shield</h3>
|
||||
This graph shows the effective shield for each damage type, found by dividing the raw shield value by the damage taken for that type. </p>
|
||||
|
||||
<h3>Amour Metrics</h3>
|
||||
The armour metrics provides information about your armour defence. </p>
|
||||
|
||||
Raw armour strength is the sum of the armour from your bulkheads and hull reinforcement packages. A tooltip provides a breakdown of these values. </p>
|
||||
|
||||
The time the armour will hold for is the time it will take your opponent' to take your armour to 0. This assumes that they have 100% time on target, the engagement range stays constant, and that all damage is dealt to the armour rather than modules. </p>
|
||||
|
||||
Raw module armour is the sum of the protection from your module reinforcement packages. </p>
|
||||
|
||||
Protection for hardpoints is the amount of protection that your module reinforcement packages provide to hardpoints. This percentage of damage to the hardpoints will be diverted to the module reinforcement packages. </p>
|
||||
|
||||
Protection for all other modules is the amount of protection that your module reinforcement packages provide to everything other than hardpoints. This percentage of damage to the modules will be diverted to the module reinforcement packages. </p>
|
||||
|
||||
</h3>Armour Sources</h3>
|
||||
This chart provides information about the sources of your armour. For each applicable source of shields (bulkheads, hull reinforcement packages) a value is provided. </p>
|
||||
|
||||
</h3>Damage Taken</h3>
|
||||
This graph shows how the initial damage from the weapons of each type are reduced before their damage is applied to the armour. For each type of damage (absolute, explosive, kinetic, thermal) a percentage of the initial damage is provided. A tooltip provides a breakdown of these values. </p>
|
||||
|
||||
</h3>Effective Armour</h3>
|
||||
This graph shows the effective armour for each damage type, found by dividing the raw armour value by the damage taken for that type. </p>
|
||||
|
||||
<h1>Keyboard Shortcuts</h1>
|
||||
<dl>
|
||||
<dt>Ctrl-b</dt><dd>toggle boost</dd>
|
||||
<dt>Ctrl-e</dt><dd>open export dialogue (outfitting page only)</dd>
|
||||
<dt>Ctrl-h</dt><dd>open help dialogue</dd>
|
||||
<dt>Ctrl-i</dt><dd>open import dialogue</dd>
|
||||
<dt>Ctrl-o</dt><dd>open shortlink dialogue</dd>
|
||||
<dt>Ctrl-left-arrow</dt><dd>increase SYS capacitor</dd>
|
||||
<dt>Ctrl-up-arrow</dt><dd>increase ENG capacitor</dd>
|
||||
<dt>Ctrl-right-arrow</dt><dd>increase WEP capacitor</dd>
|
||||
<dt>Ctrl-down-arrow</dt><dd>reset power distributor</dd>
|
||||
<dt>Esc</dt><dd>close any open dialogue</dd>
|
||||
</dl>
|
||||
<h1>Glossary</h1>
|
||||
<dl>
|
||||
<dt>Absolute damage</dt><dd>A type of damage, without any protection. Absolute damage is always dealt at 100% regardless of if the damage is to shields, hull or modules, and irrespective of resistances</dd>
|
||||
<dt>DPS</dt><dd>Damage per second; the amount of damage that a weapon or a ship can deal per second to a target under optimum conditions</dd>
|
||||
<dt>EPS</dt><dd>Energy per second; the amount of energy that a weapon or a ship drains from the weapons capacitor per second when firing</dd>
|
||||
<dt>HPS</dt><dd>Heat per second; the amount of heat that a weapon or a ship generates per second when firing</dd>
|
||||
<dt>Effectivness</dt><dd>A comparison of the maximum DPS of a given weapon to the actual DPS of the given weapon in a specific situation. DPS can be reduced by range to the target, the target's hull and shield resistances, and the target's hardness</dd>
|
||||
<dt>Explosive damage</dt><dd>A type of damage, protected against by explosive resistance</dd>
|
||||
<dt>Hardness</dt><dd>The inherent resistance to damage of a ship's hull. Hardness is defined on a per-ship basis and there is currently nothing that can be done to change it. Hardness of a ship's hull is compared to the piercing of weapons: if piercing is higher than hardness the weapon does 100% damage, otherwise it does a fraction of its damage calculated as piercing/hardness</dd>
|
||||
<dt>Falloff</dt><dd>The distance at which a weapons starts to do less damage than its stated DPS</dd>
|
||||
<dt>Kinetic damage</dt><dd>A type of damage, protected against by kinetic resistance</dd>
|
||||
<dt>SDPS</dt><dd>Sustained damage per second; the amount of damage that a weapon or a ship can deal per second to a target, taking in to account ammunition reload</dd>
|
||||
<dt>SEPS</dt><dd>Sustained energy per second; the amount of energy that a weapon or a ship drains from the weapons capacitor per second when firing, taking in to account ammunition reload</dd>
|
||||
<dt>SHPS</dt><dd>Sustained heat per second; the amount of heat that a weapon or a ship generates per second when firing, taking in to account ammunition reload</dd>
|
||||
<dt>Thermal damage</dt><dd>A type of damage, protected against by thermal resistance</dd>
|
||||
</dl>
|
||||
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -39,10 +39,16 @@ export default class ErrorDetails extends React.Component {
|
||||
</div>;
|
||||
}
|
||||
|
||||
const importerror = ed && ed.scriptUrl && ed.scriptUrl.indexOf('/import') != -1;
|
||||
|
||||
return <div className='error'>
|
||||
<h1>Jameson, we have a problem..</h1>
|
||||
<h1><small>{error.message}</small></h1>
|
||||
<br/>
|
||||
{importerror ? <div>If you are attempting to import a ship from EDDI or EDMC and are seeing a 'Z_BUF_ERROR' it means that the URL has not been provided correctly. This is a common problem when using Microsoft Internet Explorer or Microsoft Edge, and you should use another browser instead.</div> : null }
|
||||
<br/>
|
||||
<div>Please note that this site uses Google Analytics to track performance and usage. If you are blocking cookies, for example using Ghostery, please disable blocking for this site and try again.</div>
|
||||
<br/>
|
||||
{content}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
import React from 'react';
|
||||
// import Perf from 'react-addons-perf';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import cn from 'classnames';
|
||||
import Page from './Page';
|
||||
import Router from '../Router';
|
||||
import Persist from '../stores/Persist';
|
||||
import * as Utils from '../utils/UtilityFunctions';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import { toDetailedBuild } from '../shipyard/Serializer';
|
||||
import { outfitURL } from '../utils/UrlGenerators';
|
||||
import { FloppyDisk, Bin, Switch, Download, Reload, Fuel, LinkIcon } from '../components/SvgIcons';
|
||||
import { FloppyDisk, Bin, Switch, Download, Reload, LinkIcon, ShoppingIcon } from '../components/SvgIcons';
|
||||
import LZString from 'lz-string';
|
||||
import ShipSummaryTable from '../components/ShipSummaryTable';
|
||||
import StandardSlotSection from '../components/StandardSlotSection';
|
||||
import HardpointsSlotSection from '../components/HardpointsSlotSection';
|
||||
import HardpointSlotSection from '../components/HardpointSlotSection';
|
||||
import InternalSlotSection from '../components/InternalSlotSection';
|
||||
import UtilitySlotSection from '../components/UtilitySlotSection';
|
||||
import OffenceSummary from '../components/OffenceSummary';
|
||||
import DefenceSummary from '../components/DefenceSummary';
|
||||
import MovementSummary from '../components/MovementSummary';
|
||||
import DamageDealt from '../components/DamageDealt';
|
||||
import DamageReceived from '../components/DamageReceived';
|
||||
import LineChart from '../components/LineChart';
|
||||
import PowerManagement from '../components/PowerManagement';
|
||||
import CostSection from '../components/CostSection';
|
||||
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 OutfittingSubpages from '../components/OutfittingSubpages';
|
||||
import ModalExport from '../components/ModalExport';
|
||||
import ModalPermalink from '../components/ModalPermalink';
|
||||
import Slider from '../components/Slider';
|
||||
|
||||
const SPEED_SERIES = ['boost', '4 Pips', '2 Pips', '0 Pips'];
|
||||
const SPEED_COLORS = ['#0088d2', '#ff8c0d', '#D26D00', '#c06400'];
|
||||
|
||||
/**
|
||||
* Document Title Generator
|
||||
@@ -36,7 +34,7 @@ const SPEED_COLORS = ['#0088d2', '#ff8c0d', '#D26D00', '#c06400'];
|
||||
* @return {String} Document title
|
||||
*/
|
||||
function getTitle(shipName, buildName) {
|
||||
return `${shipName}${buildName ? ` - ${buildName}` : ''}`;
|
||||
return buildName ? buildName : shipName;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,15 +49,25 @@ export default class OutfittingPage extends Page {
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = this._initState(context);
|
||||
// window.Perf = Perf;
|
||||
this.state = this._initState(props, context);
|
||||
this._keyDown = this._keyDown.bind(this);
|
||||
this._exportBuild = this._exportBuild.bind(this);
|
||||
this._pipsUpdated = this._pipsUpdated.bind(this);
|
||||
this._boostUpdated = this._boostUpdated.bind(this);
|
||||
this._cargoUpdated = this._cargoUpdated.bind(this);
|
||||
this._fuelUpdated = this._fuelUpdated.bind(this);
|
||||
this._opponentUpdated = this._opponentUpdated.bind(this);
|
||||
this._engagementRangeUpdated = this._engagementRangeUpdated.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* [Re]Create initial state from context
|
||||
* @param {Object} props React component properties
|
||||
* @param {context} context React component context
|
||||
* @return {Object} New state object
|
||||
*/
|
||||
_initState(context) {
|
||||
_initState(props, context) {
|
||||
let params = context.route.params;
|
||||
let shipId = params.ship;
|
||||
let code = params.code;
|
||||
@@ -79,9 +87,10 @@ export default class OutfittingPage extends Page {
|
||||
ship.buildWith(data.defaults); // Populate with default components
|
||||
}
|
||||
|
||||
let fuelCapacity = ship.fuelCapacity;
|
||||
this._getTitle = getTitle.bind(this, data.properties.name);
|
||||
|
||||
// Obtain ship control from code
|
||||
const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = this._obtainControlFromCode(ship, code);
|
||||
return {
|
||||
error: null,
|
||||
title: this._getTitle(buildName),
|
||||
@@ -92,11 +101,18 @@ export default class OutfittingPage extends Page {
|
||||
ship,
|
||||
code,
|
||||
savedCode,
|
||||
fuelCapacity,
|
||||
fuelLevel: 1,
|
||||
jumpRangeChartFunc: ship.calcJumpRangeWith.bind(ship, fuelCapacity),
|
||||
fastestRangeChartFunc: ship.calcFastestRangeWith.bind(ship, fuelCapacity),
|
||||
speedChartFunc: ship.calcSpeedsWith.bind(ship, fuelCapacity)
|
||||
sys,
|
||||
eng,
|
||||
wep,
|
||||
boost,
|
||||
fuel,
|
||||
cargo,
|
||||
opponent,
|
||||
opponentBuild,
|
||||
opponentSys,
|
||||
opponentEng,
|
||||
opponentWep,
|
||||
engagementRange
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,35 +134,213 @@ export default class OutfittingPage extends Page {
|
||||
this.setState(stateChanges);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the control part of the route
|
||||
*/
|
||||
_updateRouteOnControlChange() {
|
||||
const { ship, shipId, buildName } = this.state;
|
||||
const code = this._fullCode(ship);
|
||||
this._updateRoute(shipId, buildName, code);
|
||||
this.setState({ code });
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a full code for this ship, including any additions due to the outfitting page
|
||||
* @param {Object} ship the ship
|
||||
* @param {number} fuel the fuel carried by the ship (if different from that in state)
|
||||
* @param {number} cargo the cargo carried by the ship (if different from that in state)
|
||||
* @returns {string} the code for this ship
|
||||
*/
|
||||
_fullCode(ship, fuel, cargo) {
|
||||
return `${ship.toString()}.${LZString.compressToBase64(this._controlCode(fuel, cargo))}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the control information from the build code
|
||||
* @param {Object} ship The ship
|
||||
* @param {string} code The build code
|
||||
* @returns {Object} The control information
|
||||
*/
|
||||
_obtainControlFromCode(ship, code) {
|
||||
// Defaults
|
||||
let sys = 2;
|
||||
let eng = 2;
|
||||
let wep = 2;
|
||||
let boost = false;
|
||||
let fuel = ship.fuelCapacity;
|
||||
let cargo = ship.cargoCapacity;
|
||||
let opponent = new Ship('eagle', Ships['eagle'].properties, Ships['eagle'].slots).buildWith(Ships['eagle'].defaults);
|
||||
let opponentSys = 2;
|
||||
let opponentEng = 2;
|
||||
let opponentWep = 2;
|
||||
let opponentBuild;
|
||||
let engagementRange = 1000;
|
||||
|
||||
// Obtain updates from code, if available
|
||||
if (code) {
|
||||
const parts = code.split('.');
|
||||
if (parts.length >= 5) {
|
||||
// We have control information in the code
|
||||
const control = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[4])).split('/');
|
||||
sys = parseFloat(control[0]);
|
||||
eng = parseFloat(control[1]);
|
||||
wep = parseFloat(control[2]);
|
||||
boost = control[3] == 1 ? true : false;
|
||||
fuel = parseFloat(control[4]);
|
||||
cargo = parseInt(control[5]);
|
||||
if (control[6]) {
|
||||
const shipId = control[6];
|
||||
opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots);
|
||||
if (control[7] && Persist.getBuild(shipId, control[7])) {
|
||||
// Ship is a particular build
|
||||
const opponentCode = Persist.getBuild(shipId, control[7]);
|
||||
opponent.buildFrom(opponentCode);
|
||||
opponentBuild = control[7];
|
||||
if (opponentBuild) {
|
||||
// Obtain opponent's sys/eng/wep pips from their code
|
||||
const opponentParts = opponentCode.split('.');
|
||||
if (opponentParts.length >= 5) {
|
||||
const opponentControl = LZString.decompressFromBase64(Utils.fromUrlSafe(opponentParts[4])).split('/');
|
||||
opponentSys = parseFloat(opponentControl[0]);
|
||||
opponentEng = parseFloat(opponentControl[1]);
|
||||
opponentWep = parseFloat(opponentControl[2]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ship is a stock build
|
||||
opponent.buildWith(Ships[shipId].defaults);
|
||||
}
|
||||
}
|
||||
engagementRange = parseInt(control[8]);
|
||||
}
|
||||
}
|
||||
|
||||
return { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange };
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when pips have been updated
|
||||
* @param {number} sys SYS pips
|
||||
* @param {number} eng ENG pips
|
||||
* @param {number} wep WEP pips
|
||||
*/
|
||||
_pipsUpdated(sys, eng, wep) {
|
||||
this.setState({ sys, eng, wep }, () => this._updateRouteOnControlChange());
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when boost has been updated
|
||||
* @param {boolean} boost true if boosting
|
||||
*/
|
||||
_boostUpdated(boost) {
|
||||
this.setState({ boost }, () => this._updateRouteOnControlChange());
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when fuel has been updated
|
||||
* @param {number} fuel the amount of fuel, in T
|
||||
*/
|
||||
_fuelUpdated(fuel) {
|
||||
this.setState({ fuel }, () => this._updateRouteOnControlChange());
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when cargo has been updated
|
||||
* @param {number} cargo the amount of cargo, in T
|
||||
*/
|
||||
_cargoUpdated(cargo) {
|
||||
this.setState({ cargo }, () => this._updateRouteOnControlChange());
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when engagement range has been updated
|
||||
* @param {number} engagementRange the engagement range, in m
|
||||
*/
|
||||
_engagementRangeUpdated(engagementRange) {
|
||||
this.setState({ engagementRange }, () => this._updateRouteOnControlChange());
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when target ship has been updated
|
||||
* @param {string} opponent the opponent's ship model
|
||||
* @param {string} opponentBuild the name of the opponent's build
|
||||
*/
|
||||
_opponentUpdated(opponent, opponentBuild) {
|
||||
const opponentShip = new Ship(opponent, Ships[opponent].properties, Ships[opponent].slots);
|
||||
let opponentSys = this.state.opponentSys;
|
||||
let opponentEng = this.state.opponentEng;
|
||||
let opponentWep = this.state.opponentWep;
|
||||
if (opponentBuild && Persist.getBuild(opponent, opponentBuild)) {
|
||||
// Ship is a particular build
|
||||
opponentShip.buildFrom(Persist.getBuild(opponent, opponentBuild));
|
||||
// Set pips for opponent
|
||||
const opponentParts = Persist.getBuild(opponent, opponentBuild).split('.');
|
||||
if (opponentParts.length >= 5) {
|
||||
const opponentControl = LZString.decompressFromBase64(Utils.fromUrlSafe(opponentParts[4])).split('/');
|
||||
opponentSys = parseFloat(opponentControl[0]);
|
||||
opponentEng = parseFloat(opponentControl[1]);
|
||||
opponentWep = parseFloat(opponentControl[2]);
|
||||
}
|
||||
} else {
|
||||
// Ship is a stock build
|
||||
opponentShip.buildWith(Ships[opponent].defaults);
|
||||
opponentSys = 2;
|
||||
opponentEng = 2;
|
||||
opponentWep = 2;
|
||||
}
|
||||
|
||||
this.setState({ opponent: opponentShip, opponentBuild, opponentSys, opponentEng, opponentWep }, () => this._updateRouteOnControlChange());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the control code for this outfitting page
|
||||
* @param {number} fuel the fuel carried by the ship (if different from that in state)
|
||||
* @param {number} cargo the cargo carried by the ship (if different from that in state)
|
||||
* @returns {string} The control code
|
||||
*/
|
||||
_controlCode(fuel, cargo) {
|
||||
const { sys, eng, wep, boost, opponent, opponentBuild, engagementRange } = this.state;
|
||||
const code = `${sys}/${eng}/${wep}/${boost ? 1 : 0}/${fuel || this.state.fuel}/${cargo || this.state.cargo}/${opponent.id}/${opponentBuild ? opponentBuild : ''}/${engagementRange}`;
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current build
|
||||
*/
|
||||
_saveBuild() {
|
||||
let code = this.state.ship.toString();
|
||||
let { buildName, newBuildName, shipId } = this.state;
|
||||
const { code, buildName, newBuildName, shipId } = this.state;
|
||||
|
||||
if (buildName === newBuildName) {
|
||||
Persist.saveBuild(shipId, buildName, code);
|
||||
this._updateRoute(shipId, buildName, code);
|
||||
Persist.saveBuild(shipId, newBuildName, code);
|
||||
this._updateRoute(shipId, newBuildName, code);
|
||||
|
||||
let opponent, opponentBuild, opponentSys, opponentEng, opponentWep;
|
||||
if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) {
|
||||
// This is a save of our current opponent build; update it
|
||||
opponentBuild = newBuildName;
|
||||
opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots).buildFrom(code);
|
||||
opponentSys = this.state.sys;
|
||||
opponentEng = this.state.eng;
|
||||
opponentWep = this.state.wep;
|
||||
} else {
|
||||
Persist.saveBuild(shipId, newBuildName, code);
|
||||
this._updateRoute(shipId, newBuildName, code);
|
||||
opponentBuild = this.state.opponentBuild;
|
||||
opponent = this.state.opponent;
|
||||
opponentSys = this.state.opponentSys;
|
||||
opponentEng = this.state.opponentEng;
|
||||
opponentWep = this.state.opponentWep;
|
||||
}
|
||||
|
||||
this.setState({ buildName: newBuildName, code, savedCode: code, title: this._getTitle(newBuildName) });
|
||||
this.setState({ buildName: newBuildName, code, savedCode: code, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, title: this._getTitle(newBuildName) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the current build
|
||||
*/
|
||||
_renameBuild() {
|
||||
let { buildName, newBuildName, shipId, ship } = this.state;
|
||||
const { code, buildName, newBuildName, shipId, ship } = this.state;
|
||||
if (buildName != newBuildName && newBuildName.length) {
|
||||
let code = ship.toString();
|
||||
Persist.deleteBuild(shipId, buildName);
|
||||
Persist.saveBuild(shipId, newBuildName, code);
|
||||
this._updateRoute(shipId, newBuildName, code);
|
||||
this.setState({ buildName: newBuildName, code, savedCode: code });
|
||||
this.setState({ buildName: newBuildName, code, savedCode: code, opponentBuild: newBuildName });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,24 +348,50 @@ export default class OutfittingPage extends Page {
|
||||
* Reload build from last save
|
||||
*/
|
||||
_reloadBuild() {
|
||||
this.state.ship.buildFrom(this.state.savedCode);
|
||||
this._shipUpdated();
|
||||
this.setState({ code: this.state.savedCode }, () => this._codeUpdated());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset build to Stock/Factory defaults
|
||||
*/
|
||||
_resetBuild() {
|
||||
this.state.ship.buildWith(Ships[this.state.shipId].defaults);
|
||||
this._shipUpdated();
|
||||
const { ship, shipId, buildName } = this.state;
|
||||
// Rebuild ship
|
||||
ship.buildWith(Ships[shipId].defaults);
|
||||
// Reset controls
|
||||
const code = ship.toString();
|
||||
const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code);
|
||||
// Update state, and refresh the ship
|
||||
this.setState({
|
||||
sys,
|
||||
eng,
|
||||
wep,
|
||||
boost,
|
||||
fuel,
|
||||
cargo,
|
||||
opponent,
|
||||
opponentBuild,
|
||||
engagementRange
|
||||
}, () => this._updateRoute(shipId, buildName, code));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the build
|
||||
*/
|
||||
_deleteBuild() {
|
||||
Persist.deleteBuild(this.state.shipId, this.state.buildName);
|
||||
const { shipId, buildName } = this.state;
|
||||
Persist.deleteBuild(shipId, buildName);
|
||||
|
||||
let opponentBuild;
|
||||
if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) {
|
||||
// Our current opponent has been deleted; revert to stock
|
||||
opponentBuild = null;
|
||||
} else {
|
||||
opponentBuild = this.state.opponentBuild;
|
||||
}
|
||||
Router.go(outfitURL(this.state.shipId));
|
||||
|
||||
this.setState({ opponentBuild });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,25 +401,50 @@ export default class OutfittingPage extends Page {
|
||||
let translate = this.context.language.translate;
|
||||
let { buildName, ship } = this.state;
|
||||
this.context.showModal(<ModalExport
|
||||
title={buildName + ' ' + translate('export')}
|
||||
title={(buildName || ship.name) + ' ' + translate('export')}
|
||||
description={translate('PHRASE_EXPORT_DESC')}
|
||||
data={toDetailedBuild(buildName, ship, ship.toString())}
|
||||
/>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger render on ship model change
|
||||
* Called when the code for the ship has been updated, to synchronise the rest of the data
|
||||
*/
|
||||
_codeUpdated() {
|
||||
const { code, ship, shipId, buildName } = this.state;
|
||||
|
||||
// Rebuild ship from the code
|
||||
this.state.ship.buildFrom(code);
|
||||
|
||||
// Obtain controls from the code
|
||||
const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code);
|
||||
// Update state, and refresh the route when complete
|
||||
this.setState({
|
||||
sys,
|
||||
eng,
|
||||
wep,
|
||||
boost,
|
||||
fuel,
|
||||
cargo,
|
||||
opponent,
|
||||
opponentBuild,
|
||||
engagementRange
|
||||
}, () => this._updateRoute(shipId, buildName, code));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the ship has been updated, to set the code and then update accordingly
|
||||
*/
|
||||
_shipUpdated() {
|
||||
let { shipId, buildName, ship, fuelCapacity } = this.state;
|
||||
let code = ship.toString();
|
||||
|
||||
if (fuelCapacity != ship.fuelCapacity) {
|
||||
this._fuelChange(this.state.fuelLevel);
|
||||
let { ship, shipId, buildName, cargo, fuel } = this.state;
|
||||
if (cargo > ship.cargoCapacity) {
|
||||
cargo = ship.cargoCapacity;
|
||||
}
|
||||
|
||||
this._updateRoute(shipId, buildName, code);
|
||||
this.setState({ code });
|
||||
if (fuel > ship.fuelCapacity) {
|
||||
fuel = ship.fuelCapacity;
|
||||
}
|
||||
const code = this._fullCode(ship, fuel, cargo);
|
||||
this.setState({ code, cargo, fuel }, () => this._updateRoute(shipId, buildName, code));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,36 +457,6 @@ export default class OutfittingPage extends Page {
|
||||
Router.replace(outfitURL(shipId, code, buildName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current fuel level
|
||||
* @param {number} fuelLevel Fuel leval 0 - 1
|
||||
*/
|
||||
_fuelChange(fuelLevel) {
|
||||
let ship = this.state.ship;
|
||||
let fuelCapacity = ship.fuelCapacity;
|
||||
let fuel = fuelCapacity * fuelLevel;
|
||||
this.setState({
|
||||
fuelLevel,
|
||||
fuelCapacity,
|
||||
jumpRangeChartFunc: ship.calcJumpRangeWith.bind(ship, fuel),
|
||||
fastestRangeChartFunc: ship.calcFastestRangeWith.bind(ship, fuel),
|
||||
speedChartFunc: ship.calcSpeedsWith.bind(ship, fuel)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dimenions from rendered DOM
|
||||
*/
|
||||
_updateDimensions() {
|
||||
let elem = findDOMNode(this.refs.chartThird);
|
||||
|
||||
if (elem) {
|
||||
this.setState({
|
||||
chartWidth: findDOMNode(this.refs.chartThird).offsetWidth
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
@@ -249,7 +464,7 @@ export default class OutfittingPage extends Page {
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
|
||||
this.setState(this._initState(nextContext));
|
||||
this.setState(this._initState(nextProps, nextContext));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,21 +472,14 @@ export default class OutfittingPage extends Page {
|
||||
* Add listeners when about to mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
this.resizeListener = this.context.onWindowResize(this._updateDimensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger DOM updates on mount
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updateDimensions();
|
||||
document.addEventListener('keydown', this._keyDown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove listeners on unmount
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this.resizeListener.remove();
|
||||
document.removeEventListener('keydown', this._keyDown);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -281,6 +489,36 @@ export default class OutfittingPage extends Page {
|
||||
this.context.showModal(<ModalPermalink url={window.location.href}/>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open up a window for EDDB with a shopping list of our components
|
||||
*/
|
||||
_eddbShoppingList() {
|
||||
const ship = this.state.ship;
|
||||
|
||||
const shipId = Ships[ship.id].eddbID;
|
||||
// Provide unique list of non-PP module EDDB IDs
|
||||
const modIds = ship.internal.concat(ship.bulkheads, ship.standard, ship.hardpoints).filter(slot => slot !== null && slot.m !== null && !slot.m.pp).map(slot => slot.m.eddbID).filter((v, i, a) => a.indexOf(v) === i);
|
||||
|
||||
// Open up the relevant URL
|
||||
window.open('https://eddb.io/station?s=' + shipId + '&m=' + modIds.join(','));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Key Down
|
||||
* @param {Event} e Keyboard Event
|
||||
*/
|
||||
_keyDown(e) {
|
||||
// .keyCode will eventually be replaced with .key
|
||||
switch (e.keyCode) {
|
||||
case 69: // 'e'
|
||||
if (e.ctrlKey || e.metaKey) { // CTRL/CMD + e
|
||||
e.preventDefault();
|
||||
this._exportBuild();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Page
|
||||
* @return {React.Component} The page contents
|
||||
@@ -289,16 +527,29 @@ export default class OutfittingPage extends Page {
|
||||
let state = this.state,
|
||||
{ language, termtip, tooltip, sizeRatio, onWindowResize } = this.context,
|
||||
{ translate, units, formats } = language,
|
||||
{ ship, code, savedCode, buildName, newBuildName, chartWidth, fuelCapacity, fuelLevel } = state,
|
||||
{ ship, code, savedCode, buildName, newBuildName, sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = state,
|
||||
hide = tooltip.bind(null, null),
|
||||
menu = this.props.currentMenu,
|
||||
shipUpdated = this._shipUpdated,
|
||||
canSave = (newBuildName || buildName) && code !== savedCode,
|
||||
canRename = buildName && newBuildName && buildName != newBuildName,
|
||||
canReload = savedCode && canSave,
|
||||
hStr = ship.getHardpointsString() + '.' + ship.getModificationsString(),
|
||||
sStr = ship.getStandardString() + '.' + ship.getModificationsString(),
|
||||
iStr = ship.getInternalString() + '.' + ship.getModificationsString();
|
||||
canReload = savedCode && canSave;
|
||||
|
||||
// Code can be blank for a default loadout. Prefix it with the ship name to ensure that changes in default ships is picked up
|
||||
code = ship.name + (code || '');
|
||||
|
||||
// Markers are used to propagate state changes without requiring a deep comparison of the ship, as that takes a long time
|
||||
const _sStr = ship.getStandardString();
|
||||
const _iStr = ship.getInternalString();
|
||||
const _hStr = ship.getHardpointsString();
|
||||
const _pStr = `${ship.getPowerEnabledString()}${ship.getPowerPrioritiesString()}`;
|
||||
const _mStr = ship.getModificationsString();
|
||||
|
||||
const standardSlotMarker = `${ship.name}${_sStr}${_pStr}${_mStr}${ship.ladenMass}${cargo}${fuel}`;
|
||||
const internalSlotMarker = `${ship.name}${_iStr}${_pStr}${_mStr}`;
|
||||
const hardpointsSlotMarker = `${ship.name}${_hStr}${_pStr}${_mStr}`;
|
||||
const boostMarker = `${ship.canBoost(cargo, fuel)}`;
|
||||
const shipSummaryMarker = `${ship.name}${_sStr}${_iStr}${_hStr}${_pStr}${_mStr}${ship.ladenMass}${ship.cargo}${ship.fuel}`;
|
||||
|
||||
return (
|
||||
<div id='outfit' className={'page'} style={{ fontSize: (sizeRatio * 0.9) + 'em' }}>
|
||||
@@ -324,131 +575,72 @@ export default class OutfittingPage extends Page {
|
||||
<button onClick={buildName && this._exportBuild} disabled={!buildName} onMouseOver={termtip.bind(null, 'export')} onMouseOut={hide}>
|
||||
<Download className='lg'/>
|
||||
</button>
|
||||
<button onClick={this._eddbShoppingList} onMouseOver={termtip.bind(null, 'PHRASE_SHOPPING_LIST')} onMouseOut={hide}>
|
||||
<ShoppingIcon className='lg' />
|
||||
</button>
|
||||
<button onClick={this._genShortlink} onMouseOver={termtip.bind(null, 'shortlink')} onMouseOut={hide}>
|
||||
<LinkIcon className='lg' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ShipSummaryTable ship={ship} code={code} />
|
||||
<StandardSlotSection ship={ship} code={sStr} onChange={shipUpdated} currentMenu={menu} />
|
||||
<InternalSlotSection ship={ship} code={iStr} onChange={shipUpdated} currentMenu={menu} />
|
||||
<HardpointsSlotSection ship={ship} code={hStr} onChange={shipUpdated} currentMenu={menu} />
|
||||
<UtilitySlotSection ship={ship} code={hStr} onChange={shipUpdated} currentMenu={menu} />
|
||||
<PowerManagement ship={ship} code={code} onChange={shipUpdated} />
|
||||
<CostSection ship={ship} buildName={buildName} code={sStr + hStr + iStr} />
|
||||
{/* Main tables */}
|
||||
<ShipSummaryTable ship={ship} fuel={fuel} cargo={cargo} marker={shipSummaryMarker} />
|
||||
<StandardSlotSection ship={ship} fuel={fuel} cargo={cargo} code={standardSlotMarker} onChange={shipUpdated} currentMenu={menu} />
|
||||
<InternalSlotSection ship={ship} code={internalSlotMarker} onChange={shipUpdated} currentMenu={menu} />
|
||||
<HardpointSlotSection ship={ship} code={hardpointsSlotMarker} onChange={shipUpdated} currentMenu={menu} />
|
||||
<UtilitySlotSection ship={ship} code={hardpointsSlotMarker} onChange={shipUpdated} currentMenu={menu} />
|
||||
|
||||
<div className='group third'>
|
||||
<OffenceSummary ship={ship} code={code}/>
|
||||
{/* Control of ship and opponent */}
|
||||
<div className='group quarter'>
|
||||
<div className='group half'>
|
||||
<h2 style={{ verticalAlign: 'middle', textAlign: 'left' }}>{translate('ship control')}</h2>
|
||||
</div>
|
||||
<div className='group half'>
|
||||
<Boost marker={boostMarker} ship={ship} boost={boost} onChange={this._boostUpdated} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<Pips sys={sys} eng={eng} wep={wep} onChange={this._pipsUpdated} />
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
<Fuel fuelCapacity={ship.fuelCapacity} fuel={fuel} onChange={this._fuelUpdated}/>
|
||||
</div>
|
||||
<div className='group quarter'>
|
||||
{ ship.cargoCapacity > 0 ? <Cargo cargoCapacity={ship.cargoCapacity} cargo={cargo} onChange={this._cargoUpdated}/> : null }
|
||||
</div>
|
||||
<div className='group half'>
|
||||
<div className='group quarter'>
|
||||
<h2 style={{ verticalAlign: 'middle', textAlign: 'left' }}>{translate('opponent')}</h2>
|
||||
</div>
|
||||
<div className='group threequarters'>
|
||||
<ShipPicker ship={opponent.id} build={opponentBuild} onChange={this._opponentUpdated}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='group half'>
|
||||
<EngagementRange ship={ship} engagementRange={engagementRange} onChange={this._engagementRangeUpdated}/>
|
||||
</div>
|
||||
|
||||
<div className='group third'>
|
||||
<DefenceSummary ship={ship} code={code}/>
|
||||
</div>
|
||||
|
||||
<div className='group third'>
|
||||
<MovementSummary ship={ship} code={code}/>
|
||||
</div>
|
||||
|
||||
<div ref='chartThird' className='group third'>
|
||||
<h1>{translate('jump range')}</h1>
|
||||
<LineChart
|
||||
width={chartWidth}
|
||||
xMax={ship.cargoCapacity}
|
||||
yMax={ship.unladenRange}
|
||||
xUnit={translate('T')}
|
||||
yUnit={translate('LY')}
|
||||
yLabel={translate('jump range')}
|
||||
xLabel={translate('cargo')}
|
||||
func={state.jumpRangeChartFunc}
|
||||
/>
|
||||
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
|
||||
<tbody >
|
||||
<tr>
|
||||
<td style={{ verticalAlign: 'top', padding: 0, width: '2.5em' }} onMouseEnter={termtip.bind(null, 'fuel level')} onMouseLeave={hide}>
|
||||
<Fuel className='xl primary-disabled' />
|
||||
</td>
|
||||
<td>
|
||||
<Slider
|
||||
axis={true}
|
||||
onChange={this._fuelChange}
|
||||
axisUnit={translate('T')}
|
||||
percent={fuelLevel}
|
||||
max={fuelCapacity}
|
||||
scale={sizeRatio}
|
||||
onResize={onWindowResize}
|
||||
/>
|
||||
</td>
|
||||
<td className='primary' style={{ width: '10em', verticalAlign: 'top', fontSize: '0.9em', textAlign: 'left' }}>
|
||||
{formats.f2(fuelLevel * fuelCapacity)}{units.T} {formats.pct1(fuelLevel)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DamageDealt ship={ship} code={code} currentMenu={menu}/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DamageReceived ship={ship} code={code} currentMenu={menu}/>
|
||||
</div>
|
||||
{/* Tabbed subpages */}
|
||||
<OutfittingSubpages
|
||||
ship={ship}
|
||||
code={code}
|
||||
buildName={buildName}
|
||||
onChange={shipUpdated}
|
||||
sys={sys}
|
||||
eng={eng}
|
||||
wep={wep}
|
||||
boost={boost}
|
||||
cargo={cargo}
|
||||
fuel={fuel}
|
||||
engagementRange={engagementRange}
|
||||
opponent={opponent}
|
||||
opponentBuild={opponentBuild}
|
||||
opponentSys={opponentSys}
|
||||
opponentEng={opponentEng}
|
||||
opponentWep={opponentWep}
|
||||
/>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
// <div ref='chartThird' className='group third'>
|
||||
// <h1>{translate('jump range')}</h1>
|
||||
// <LineChart
|
||||
// width={chartWidth}
|
||||
// xMax={ship.cargoCapacity}
|
||||
// yMax={ship.unladenRange}
|
||||
// xUnit={translate('T')}
|
||||
// yUnit={translate('LY')}
|
||||
// yLabel={translate('jump range')}
|
||||
// xLabel={translate('cargo')}
|
||||
// func={state.jumpRangeChartFunc}
|
||||
// />
|
||||
// </div>
|
||||
// <div className='group third'>
|
||||
// <h1>{translate('speed')}</h1>
|
||||
// <LineChart
|
||||
// width={chartWidth}
|
||||
// xMax={ship.cargoCapacity}
|
||||
// yMax={ship.topBoost + 10}
|
||||
// xUnit={translate('T')}
|
||||
// yUnit={translate('m/s')}
|
||||
// yLabel={translate('speed')}
|
||||
// series={SPEED_SERIES}
|
||||
// colors={SPEED_COLORS}
|
||||
// xLabel={translate('cargo')}
|
||||
// func={state.speedChartFunc}
|
||||
// />
|
||||
// </div>
|
||||
// <div className='group half'>
|
||||
// <table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
|
||||
// <tbody >
|
||||
// <tr>
|
||||
// <td style={{ verticalAlign: 'top', padding: 0, width: '2.5em' }} onMouseEnter={termtip.bind(null, 'fuel level')} onMouseLeave={hide}>
|
||||
// <Fuel className='xl primary-disabled' />
|
||||
// </td>
|
||||
// <td>
|
||||
// <Slider
|
||||
// axis={true}
|
||||
// onChange={this._fuelChange}
|
||||
// axisUnit={translate('T')}
|
||||
// percent={fuelLevel}
|
||||
// max={fuelCapacity}
|
||||
// scale={sizeRatio}
|
||||
// onResize={onWindowResize}
|
||||
// />
|
||||
// </td>
|
||||
// <td className='primary' style={{ width: '10em', verticalAlign: 'top', fontSize: '0.9em', textAlign: 'left' }}>
|
||||
// {formats.f2(fuelLevel * fuelCapacity)}{units.T} {formats.pct1(fuelLevel)}
|
||||
// </td>
|
||||
// </tr>
|
||||
// </tbody>
|
||||
// </table>
|
||||
// </div>
|
||||
|
||||
@@ -39,6 +39,14 @@ export default class Page extends React.Component {
|
||||
this[prop] = this[prop].bind(this);
|
||||
}
|
||||
});
|
||||
|
||||
let fix = sessionStorage.getItem('__safari_history_fix');
|
||||
sessionStorage.removeItem('__safari_history_fix');
|
||||
if (fix) {
|
||||
fix = JSON.parse(fix);
|
||||
history.replaceState(history.state, document.title, location.href);
|
||||
history.pushState(fix.state, fix.title, fix.path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,4 +90,4 @@ export default class Page extends React.Component {
|
||||
return this.renderPage();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +143,7 @@ export default class ShipyardPage extends Page {
|
||||
<td className='cap'>{translate(SizeMap[s.class])}</td>
|
||||
<td className='ri'>{fInt(s.agility)}</td>
|
||||
<td className='ri'>{fInt(s.hardness)}</td>
|
||||
<td className='ri'>{fInt(s.crew)}</td>
|
||||
<td className='ri'>{fInt(s.speed)}{u['m/s']}</td>
|
||||
<td className='ri'>{fInt(s.boost)}{u['m/s']}</td>
|
||||
<td className='ri'>{fInt(s.baseArmour)}</td>
|
||||
@@ -251,7 +252,6 @@ export default class ShipyardPage extends Page {
|
||||
|
||||
return (
|
||||
<div className='page' style={{ fontSize: sizeRatio + 'em' }}>
|
||||
<p style={{ textAlign: 'center' }}>This is <strong>Coriolis EDCD Edition</strong> - a temporary clone of <a href='https://coriolis.io/' target='_blank'>https://coriolis.io/</a> with added support for E:D 2.2. For more info see Settings / <Link href="/about" className='block'>About</Link></p>
|
||||
<div style={{ whiteSpace: 'nowrap', margin: '0 auto', fontSize: '0.8em', position: 'relative', display: 'inline-block', maxWidth: '100%' }}>
|
||||
<table style={{ width: '12em', position: 'absolute', zIndex: 1 }}>
|
||||
<thead>
|
||||
@@ -271,6 +271,7 @@ export default class ShipyardPage extends Page {
|
||||
<th rowSpan={2} className='sortable' onClick={sortShips('class')}>{translate('size')}</th>
|
||||
<th rowSpan={2} className='sortable' onClick={sortShips('agility')}>{translate('agility')}</th>
|
||||
<th rowSpan={2} className='sortable' onClick={sortShips('hardness')}>{translate('hardness')}</th>
|
||||
<th rowSpan={2} className='sortable' onClick={sortShips('crew')}>{translate('crew')}</th>
|
||||
<th colSpan={4}>{translate('base')}</th>
|
||||
<th colSpan={4}>{translate('max')}</th>
|
||||
<th colSpan={6}>{translate('core module classes')}</th>
|
||||
|
||||
@@ -9,33 +9,33 @@ import Module from './Module';
|
||||
* @return {number} Distance in Light Years
|
||||
*/
|
||||
export function jumpRange(mass, fsd, fuel) {
|
||||
let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel;
|
||||
let fsdOptimalMass = fsd instanceof Module ? fsd.getOptimalMass() : fsd.optmass;
|
||||
const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel;
|
||||
const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass;
|
||||
return Math.pow(Math.min(fuel === undefined ? fsdMaxFuelPerJump : fuel, fsdMaxFuelPerJump) / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the fastest (total) range based on mass and a specific FSD, and all fuel available
|
||||
* Calculate the total jump range based on mass and a specific FSD, and all fuel available
|
||||
*
|
||||
* @param {number} mass Mass of a ship: laden, unlanden, partially laden, etc
|
||||
* @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass
|
||||
* @param {number} fuel The total fuel available
|
||||
* @return {number} Distance in Light Years
|
||||
*/
|
||||
export function fastestRange(mass, fsd, fuel) {
|
||||
let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel;
|
||||
let fsdOptimalMass = fsd instanceof Module ? fsd.getOptimalMass() : fsd.optmass;
|
||||
let fuelRemaining = fuel % fsdMaxFuelPerJump; // Fuel left after making N max jumps
|
||||
let jumps = Math.floor(fuel / fsdMaxFuelPerJump);
|
||||
mass += fuelRemaining;
|
||||
// Going backwards, start with the last jump using the remaining fuel
|
||||
let fastestRange = fuelRemaining > 0 ? Math.pow(fuelRemaining / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass : 0;
|
||||
// For each max fuel jump, calculate the max jump range based on fuel mass left in the tank
|
||||
for (let j = 0; j < jumps; j++) {
|
||||
mass += fsd.maxfuel;
|
||||
fastestRange += Math.pow(fsdMaxFuelPerJump / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass;
|
||||
export function totalJumpRange(mass, fsd, fuel) {
|
||||
const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel;
|
||||
const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass;
|
||||
|
||||
let fuelRemaining = fuel;
|
||||
let totalRange = 0;
|
||||
while (fuelRemaining > 0) {
|
||||
const fuelForThisJump = Math.min(fuelRemaining, fsdMaxFuelPerJump);
|
||||
totalRange += this.jumpRange(mass, fsd, fuelForThisJump);
|
||||
// Mass is reduced
|
||||
mass -= fuelForThisJump;
|
||||
fuelRemaining -= fuelForThisJump;
|
||||
}
|
||||
return fastestRange;
|
||||
return totalRange;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -173,3 +173,683 @@ function normValues(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, bas
|
||||
res * (1 - (engpip * 1)),
|
||||
res];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a single value
|
||||
* @param {number} minMass the minimum mass of the thrusters
|
||||
* @param {number} optMass the optimum mass of the thrusters
|
||||
* @param {number} maxMass the maximum mass of the thrusters
|
||||
* @param {number} minMul the minimum multiplier of the thrusters
|
||||
* @param {number} optMul the optimum multiplier of the thrusters
|
||||
* @param {number} maxMul the maximum multiplier of the thrusters
|
||||
* @param {number} mass the mass of the ship
|
||||
* @param {base} base the base value from which to calculate
|
||||
* @param {number} engpip the multiplier per pip to engines
|
||||
* @param {number} eng the pips to engines
|
||||
* @returns {number} the resultant value
|
||||
*/
|
||||
function calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, base, engpip, eng) {
|
||||
const xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass));
|
||||
const exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass)));
|
||||
const ynorm = Math.pow(xnorm, exponent);
|
||||
const mul = minMul + ynorm * (maxMul - minMul);
|
||||
const res = base * mul;
|
||||
|
||||
return res * (1 - (engpip * (4 - eng)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate speed for a given setup
|
||||
* @param {number} mass the mass of the ship
|
||||
* @param {number} baseSpeed the base speed of the ship
|
||||
* @param {ojbect} thrusters the thrusters of the ship
|
||||
* @param {number} engpip the multiplier per pip to engines
|
||||
* @param {number} eng the pips to engines
|
||||
* @param {number} boostFactor the boost factor for ths ship
|
||||
* @param {boolean} boost true if the boost is activated
|
||||
* @returns {number} the resultant speed
|
||||
*/
|
||||
export function calcSpeed(mass, baseSpeed, thrusters, engpip, eng, boostFactor, boost) {
|
||||
// thrusters might be a module or a template; handle either here
|
||||
const minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass;
|
||||
const optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass;
|
||||
const maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass;
|
||||
const minMul = thrusters instanceof Module ? thrusters.getMinMul('speed') : (thrusters.minmulspeed ? thrusters.minmulspeed : thrusters.minmul);
|
||||
const optMul = thrusters instanceof Module ? thrusters.getOptMul('speed') : (thrusters.optmulspeed ? thrusters.minmulspeed : thrusters.minmul);
|
||||
const maxMul = thrusters instanceof Module ? thrusters.getMaxMul('speed') : (thrusters.maxmulspeed ? thrusters.minmulspeed : thrusters.minmul);
|
||||
|
||||
let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseSpeed, engpip, eng);
|
||||
if (boost == true) {
|
||||
result *= boostFactor;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate pitch for a given setup
|
||||
* @param {number} mass the mass of the ship
|
||||
* @param {number} basePitch the base pitch of the ship
|
||||
* @param {ojbect} thrusters the thrusters of the ship
|
||||
* @param {number} engpip the multiplier per pip to engines
|
||||
* @param {number} eng the pips to engines
|
||||
* @param {number} boostFactor the boost factor for ths ship
|
||||
* @param {boolean} boost true if the boost is activated
|
||||
* @returns {number} the resultant pitch
|
||||
*/
|
||||
export function calcPitch(mass, basePitch, thrusters, engpip, eng, boostFactor, boost) {
|
||||
// thrusters might be a module or a template; handle either here
|
||||
let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass;
|
||||
let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass;
|
||||
let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass;
|
||||
let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul);
|
||||
let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul);
|
||||
let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul);
|
||||
|
||||
let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, basePitch, engpip, eng);
|
||||
if (boost == true) {
|
||||
result *= boostFactor;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate roll for a given setup
|
||||
* @param {number} mass the mass of the ship
|
||||
* @param {number} baseRoll the base roll of the ship
|
||||
* @param {ojbect} thrusters the thrusters of the ship
|
||||
* @param {number} engpip the multiplier per pip to engines
|
||||
* @param {number} eng the pips to engines
|
||||
* @param {number} boostFactor the boost factor for ths ship
|
||||
* @param {boolean} boost true if the boost is activated
|
||||
* @returns {number} the resultant roll
|
||||
*/
|
||||
export function calcRoll(mass, baseRoll, thrusters, engpip, eng, boostFactor, boost) {
|
||||
// thrusters might be a module or a template; handle either here
|
||||
let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass;
|
||||
let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass;
|
||||
let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass;
|
||||
let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul);
|
||||
let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul);
|
||||
let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul);
|
||||
|
||||
let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseRoll, engpip, eng);
|
||||
if (boost == true) {
|
||||
result *= boostFactor;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate yaw for a given setup
|
||||
* @param {number} mass the mass of the ship
|
||||
* @param {number} baseYaw the base yaw of the ship
|
||||
* @param {ojbect} thrusters the thrusters of the ship
|
||||
* @param {number} engpip the multiplier per pip to engines
|
||||
* @param {number} eng the pips to engines
|
||||
* @param {number} boostFactor the boost factor for ths ship
|
||||
* @param {boolean} boost true if the boost is activated
|
||||
* @returns {number} the resultant yaw
|
||||
*/
|
||||
export function calcYaw(mass, baseYaw, thrusters, engpip, eng, boostFactor, boost) {
|
||||
// thrusters might be a module or a template; handle either here
|
||||
let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass;
|
||||
let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass;
|
||||
let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass;
|
||||
let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul);
|
||||
let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul);
|
||||
let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul);
|
||||
|
||||
let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseYaw, engpip, eng);
|
||||
if (boost == true) {
|
||||
result *= boostFactor;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate shield metrics
|
||||
* @param {Object} ship The ship
|
||||
* @param {int} sys The pips to SYS
|
||||
* @returns {Object} Shield metrics
|
||||
*/
|
||||
export function shieldMetrics(ship, sys) {
|
||||
const sysResistance = this.sysResistance(sys);
|
||||
const maxSysResistance = this.sysResistance(4);
|
||||
|
||||
let shield = {};
|
||||
|
||||
const shieldGeneratorSlot = ship.findInternalByGroup('sg');
|
||||
if (shieldGeneratorSlot && shieldGeneratorSlot.enabled && shieldGeneratorSlot.m) {
|
||||
const shieldGenerator = shieldGeneratorSlot.m;
|
||||
|
||||
// Boosters
|
||||
let boost = 1;
|
||||
let boosterExplDmg = 1;
|
||||
let boosterKinDmg = 1;
|
||||
let boosterThermDmg = 1;
|
||||
for (let slot of ship.hardpoints) {
|
||||
if (slot.enabled && slot.m && slot.m.grp == 'sb') {
|
||||
boost += slot.m.getShieldBoost();
|
||||
boosterExplDmg = boosterExplDmg * (1 - slot.m.getExplosiveResistance());
|
||||
boosterKinDmg = boosterKinDmg * (1 - slot.m.getKineticResistance());
|
||||
boosterThermDmg = boosterThermDmg * (1 - slot.m.getThermalResistance());
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate diminishing returns for boosters
|
||||
// Diminishing returns not currently in-game
|
||||
// boost = Math.min(boost, (1 - Math.pow(Math.E, -0.7 * boost)) * 2.5);
|
||||
|
||||
// Remove base shield generator strength
|
||||
boost -= 1;
|
||||
// Apply diminishing returns
|
||||
boosterExplDmg = boosterExplDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterExplDmg) / 2;
|
||||
boosterKinDmg = boosterKinDmg > 0.7 ? boosterKinDmg : 0.7 - (0.7 - boosterKinDmg) / 2;
|
||||
boosterThermDmg = boosterThermDmg > 0.7 ? boosterThermDmg : 0.7 - (0.7 - boosterThermDmg) / 2;
|
||||
|
||||
const generatorStrength = this.shieldStrength(ship.hullMass, ship.baseShieldStrength, shieldGenerator, 1);
|
||||
const boostersStrength = generatorStrength * boost;
|
||||
|
||||
// Recover time is the time taken to go from 0 to 50%. It includes a 16-second wait before shields start to recover
|
||||
const shieldToRecover = (generatorStrength + boostersStrength) / 2;
|
||||
const powerDistributor = ship.standard[4].m;
|
||||
const sysRechargeRate = this.sysRechargeRate(powerDistributor, sys);
|
||||
|
||||
// Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes
|
||||
// 0.6 is a magic number from FD: each 0.6 MW of energy from the power distributor recharges 1 MJ/s of regeneration
|
||||
let capacitorDrain = (shieldGenerator.getBrokenRegenerationRate() * 0.6) - sysRechargeRate;
|
||||
let capacitorLifetime = powerDistributor.getSystemsCapacity() / capacitorDrain;
|
||||
|
||||
let recover = 16;
|
||||
if (capacitorDrain <= 0 || shieldToRecover < capacitorLifetime * shieldGenerator.getBrokenRegenerationRate()) {
|
||||
// We can recover the entire shield from the capacitor store
|
||||
recover += shieldToRecover / shieldGenerator.getBrokenRegenerationRate();
|
||||
} else {
|
||||
// We can recover some of the shield from the capacitor store
|
||||
recover += capacitorLifetime;
|
||||
const remainingShieldToRecover = shieldToRecover - capacitorLifetime * shieldGenerator.getBrokenRegenerationRate();
|
||||
if (sys === 0) {
|
||||
// No system pips so will never recover shields
|
||||
recover = Math.Inf;
|
||||
} else {
|
||||
// Recover remaining shields at the rate of the power distributor's recharge
|
||||
recover += remainingShieldToRecover / (sysRechargeRate / 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
// Recharge time is the time taken to go from 50% to 100%
|
||||
const shieldToRecharge = (generatorStrength + boostersStrength) / 2;
|
||||
|
||||
// Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes
|
||||
// 0.6 is a magic number from FD: each 0.6 MW of energy from the power distributor recharges 1 MJ/s of regeneration
|
||||
capacitorDrain = (shieldGenerator.getRegenerationRate() * 0.6) - sysRechargeRate;
|
||||
capacitorLifetime = powerDistributor.getSystemsCapacity() / capacitorDrain;
|
||||
|
||||
let recharge = 0;
|
||||
if (capacitorDrain <= 0 || shieldToRecharge < capacitorLifetime * shieldGenerator.getRegenerationRate()) {
|
||||
// We can recharge the entire shield from the capacitor store
|
||||
recharge += shieldToRecharge / shieldGenerator.getRegenerationRate();
|
||||
} else {
|
||||
// We can recharge some of the shield from the capacitor store
|
||||
recharge += capacitorLifetime;
|
||||
const remainingShieldToRecharge = shieldToRecharge - capacitorLifetime * shieldGenerator.getRegenerationRate();
|
||||
if (sys === 0) {
|
||||
// No system pips so will never recharge shields
|
||||
recharge = Math.Inf;
|
||||
} else {
|
||||
// Recharge remaining shields at the rate of the power distributor's recharge
|
||||
recharge += remainingShieldToRecharge / (sysRechargeRate / 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
shield = {
|
||||
generator: generatorStrength,
|
||||
boosters: boostersStrength,
|
||||
cells: ship.shieldCells,
|
||||
total: generatorStrength + boostersStrength + ship.shieldCells,
|
||||
recover,
|
||||
recharge,
|
||||
};
|
||||
|
||||
// Shield resistances have three components: the shield generator, the shield boosters and the SYS pips.
|
||||
// We re-cast these as damage percentages
|
||||
shield.absolute = {
|
||||
generator: 1,
|
||||
boosters: 1,
|
||||
sys: 1 - sysResistance,
|
||||
total: 1 - sysResistance,
|
||||
max: 1 - maxSysResistance
|
||||
};
|
||||
|
||||
shield.explosive = {
|
||||
generator: 1 - shieldGenerator.getExplosiveResistance(),
|
||||
boosters: boosterExplDmg,
|
||||
sys: (1 - sysResistance),
|
||||
total: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - sysResistance),
|
||||
max: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - maxSysResistance)
|
||||
};
|
||||
|
||||
shield.kinetic = {
|
||||
generator: 1 - shieldGenerator.getKineticResistance(),
|
||||
boosters: boosterKinDmg,
|
||||
sys: (1 - sysResistance),
|
||||
total: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - sysResistance),
|
||||
max: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - maxSysResistance)
|
||||
};
|
||||
|
||||
shield.thermal = {
|
||||
generator: 1 - shieldGenerator.getThermalResistance(),
|
||||
boosters: boosterThermDmg,
|
||||
sys: (1 - sysResistance),
|
||||
total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance),
|
||||
max: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - maxSysResistance)
|
||||
};
|
||||
}
|
||||
|
||||
return shield;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate armour metrics
|
||||
* @param {Object} ship The ship
|
||||
* @returns {Object} Armour metrics
|
||||
*/
|
||||
export function armourMetrics(ship) {
|
||||
// Armour from bulkheads
|
||||
const armourBulkheads = ship.baseArmour + (ship.baseArmour * ship.bulkheads.m.getHullBoost());
|
||||
let armourReinforcement = 0;
|
||||
|
||||
let moduleArmour = 0;
|
||||
let moduleProtection = 1;
|
||||
|
||||
let hullExplDmg = 1;
|
||||
let hullKinDmg = 1;
|
||||
let hullThermDmg = 1;
|
||||
|
||||
// Armour from HRPs and module armour from MRPs
|
||||
for (let slot of ship.internal) {
|
||||
if (slot.m && slot.m.grp == 'hr') {
|
||||
armourReinforcement += slot.m.getHullReinforcement();
|
||||
// Hull boost for HRPs is applied against the ship's base armour
|
||||
armourReinforcement += ship.baseArmour * slot.m.getModValue('hullboost') / 10000;
|
||||
|
||||
hullExplDmg = hullExplDmg * (1 - slot.m.getExplosiveResistance());
|
||||
hullKinDmg = hullKinDmg * (1 - slot.m.getKineticResistance());
|
||||
hullThermDmg = hullThermDmg * (1 - slot.m.getThermalResistance());
|
||||
}
|
||||
if (slot.m && slot.m.grp == 'mrp') {
|
||||
moduleArmour += slot.m.getIntegrity();
|
||||
moduleProtection = moduleProtection * (1 - slot.m.getProtection());
|
||||
}
|
||||
}
|
||||
moduleProtection = 1 - moduleProtection;
|
||||
|
||||
// Apply diminishing returns
|
||||
hullExplDmg = hullExplDmg > 0.7 ? hullExplDmg : 0.7 - (0.7 - hullExplDmg) / 2;
|
||||
hullKinDmg = hullKinDmg > 0.7 ? hullKinDmg : 0.7 - (0.7 - hullKinDmg) / 2;
|
||||
hullThermDmg = hullThermDmg > 0.7 ? hullThermDmg : 0.7 - (0.7 - hullThermDmg) / 2;
|
||||
|
||||
const armour = {
|
||||
bulkheads: armourBulkheads,
|
||||
reinforcement: armourReinforcement,
|
||||
modulearmour: moduleArmour,
|
||||
moduleprotection: moduleProtection,
|
||||
total: armourBulkheads + armourReinforcement
|
||||
};
|
||||
|
||||
// Armour resistances have two components: bulkheads and HRPs
|
||||
// We re-cast these as damage percentages
|
||||
armour.absolute = {
|
||||
bulkheads: 1,
|
||||
reinforcement: 1,
|
||||
total: 1
|
||||
};
|
||||
|
||||
armour.explosive = {
|
||||
bulkheads: 1 - ship.bulkheads.m.getExplosiveResistance(),
|
||||
reinforcement: hullExplDmg,
|
||||
total: (1 - ship.bulkheads.m.getExplosiveResistance()) * hullExplDmg
|
||||
};
|
||||
|
||||
armour.kinetic = {
|
||||
bulkheads: 1 - ship.bulkheads.m.getKineticResistance(),
|
||||
reinforcement: hullKinDmg,
|
||||
total: (1 - ship.bulkheads.m.getKineticResistance()) * hullKinDmg
|
||||
};
|
||||
|
||||
armour.thermal = {
|
||||
bulkheads: 1 - ship.bulkheads.m.getThermalResistance(),
|
||||
reinforcement: hullThermDmg,
|
||||
total: (1 - ship.bulkheads.m.getThermalResistance()) * hullThermDmg
|
||||
};
|
||||
|
||||
return armour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate defence metrics for a ship
|
||||
* @param {Object} ship The ship
|
||||
* @param {Object} opponent The opponent ship
|
||||
* @param {int} sys The pips to SYS
|
||||
* @param {int} opponentWep The pips to pponent's WEP
|
||||
* @param {int} engagementrange The range between the ship and opponent
|
||||
* @returns {Object} Defence metrics
|
||||
*/
|
||||
export function defenceMetrics(ship, opponent, sys, opponentWep, engagementrange) {
|
||||
// Obtain the shield metrics
|
||||
const shield = this.shieldMetrics(ship, sys);
|
||||
|
||||
// Obtain the armour metrics
|
||||
const armour = this.armourMetrics(ship);
|
||||
|
||||
// Obtain the opponent's sustained DPS on us
|
||||
const sustainedDps = this.sustainedDps(opponent, ship, sys, engagementrange);
|
||||
|
||||
const shielddamage = shield.generator ? {
|
||||
absolutesdps: sustainedDps.shieldsdps.absolute,
|
||||
explosivesdps: sustainedDps.shieldsdps.explosive,
|
||||
kineticsdps: sustainedDps.shieldsdps.kinetic,
|
||||
thermalsdps: sustainedDps.shieldsdps.thermal,
|
||||
totalsdps: sustainedDps.shieldsdps.absolute + sustainedDps.shieldsdps.explosive + sustainedDps.shieldsdps.kinetic + sustainedDps.shieldsdps.thermal,
|
||||
totalseps: sustainedDps.eps
|
||||
} : {};
|
||||
|
||||
const armourdamage = {
|
||||
absolutesdps: sustainedDps.armoursdps.absolute,
|
||||
explosivesdps: sustainedDps.armoursdps.explosive,
|
||||
kineticsdps: sustainedDps.armoursdps.kinetic,
|
||||
thermalsdps: sustainedDps.armoursdps.thermal,
|
||||
totalsdps: sustainedDps.armoursdps.absolute + sustainedDps.armoursdps.explosive + sustainedDps.armoursdps.kinetic + sustainedDps.armoursdps.thermal,
|
||||
totalseps: sustainedDps.eps
|
||||
};
|
||||
|
||||
return { shield, armour, shielddamage, armourdamage };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate offence metrics for a ship
|
||||
* @param {Object} ship The ship
|
||||
* @param {Object} opponent The opponent ship
|
||||
* @param {int} wep The pips to WEP
|
||||
* @param {int} opponentSys The pips to opponent's SYS
|
||||
* @param {int} engagementrange The range between the ship and opponent
|
||||
* @returns {array} Offence metrics
|
||||
*/
|
||||
export function offenceMetrics(ship, opponent, wep, opponentSys, engagementrange) {
|
||||
// Per-weapon and total damage
|
||||
const damage = [];
|
||||
|
||||
// Obtain the opponent's shield and armour metrics
|
||||
const opponentShields = this.shieldMetrics(opponent, opponentSys);
|
||||
const opponentArmour = this.armourMetrics(opponent);
|
||||
|
||||
// Per-weapon and total damage to shields
|
||||
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 classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`;
|
||||
let engineering;
|
||||
if (m.blueprint && m.blueprint.name) {
|
||||
engineering = m.blueprint.name + ' ' + 'grade' + ' ' + m.blueprint.grade;
|
||||
if (m.blueprint.special && m.blueprint.special.id >= 0) {
|
||||
engineering += ', ' + m.blueprint.special.name;
|
||||
}
|
||||
}
|
||||
|
||||
const weaponSustainedDps = this._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange);
|
||||
damage.push({
|
||||
id: i,
|
||||
mount: m.mount,
|
||||
name: m.name || m.grp,
|
||||
classRating,
|
||||
engineering,
|
||||
sdps: weaponSustainedDps.damage,
|
||||
seps: weaponSustainedDps.eps,
|
||||
effectiveness: weaponSustainedDps.effectiveness
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return damage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the resistance provided by SYS pips
|
||||
* @param {integer} sys the value of the SYS pips
|
||||
* @returns {integer} the resistance for the given pips
|
||||
*/
|
||||
export function sysResistance(sys) {
|
||||
return Math.pow(sys, 0.85) * 0.6 / Math.pow(4, 0.85);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the recharge rate of the SYS capacitor of a power distributor given pips
|
||||
* @param {Object} pd The power distributor
|
||||
* @param {number} sys The number of pips to SYS
|
||||
* @returns {number} The recharge rate in MJ/s
|
||||
*/
|
||||
export function sysRechargeRate(pd, sys) {
|
||||
return pd.getSystemsRechargeRate() * Math.pow(sys, 1.1) / Math.pow(4, 1.1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the sustained DPS for a ship against an opponent at a given range
|
||||
* @param {Object} ship The ship
|
||||
* @param {Object} opponent The opponent ship
|
||||
* @param {number} sys Pips to opponent's SYS
|
||||
* @param {int} engagementrange The range between the ship and opponent
|
||||
* @returns {Object} Sustained DPS for shield and armour
|
||||
*/
|
||||
export function sustainedDps(ship, opponent, sys, engagementrange) {
|
||||
// Obtain the opponent's shield and armour metrics
|
||||
const opponentShields = this.shieldMetrics(opponent, sys);
|
||||
const opponentArmour = this.armourMetrics(opponent);
|
||||
|
||||
return this._sustainedDps(ship, opponent, opponentShields, opponentArmour, engagementrange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the sustained DPS for a ship against an opponent at a given range
|
||||
* @param {Object} ship The ship
|
||||
* @param {Object} opponent The opponent ship
|
||||
* @param {Object} opponentShields The opponent's shield resistances
|
||||
* @param {Object} opponentArmour The opponent's armour resistances
|
||||
* @param {int} engagementrange The range between the ship and opponent
|
||||
* @returns {Object} Sustained DPS for shield and armour
|
||||
*/
|
||||
export function _sustainedDps(ship, opponent, opponentShields, opponentArmour, engagementrange) {
|
||||
const shieldsdps = {
|
||||
absolute: 0,
|
||||
explosive: 0,
|
||||
kinetic: 0,
|
||||
thermal: 0
|
||||
};
|
||||
|
||||
const armoursdps = {
|
||||
absolute: 0,
|
||||
explosive: 0,
|
||||
kinetic: 0,
|
||||
thermal: 0
|
||||
};
|
||||
|
||||
let eps = 0;
|
||||
|
||||
for (let i = 0; i < ship.hardpoints.length; i++) {
|
||||
if (ship.hardpoints[i].m && ship.hardpoints[i].enabled && ship.hardpoints[i].maxClass > 0) {
|
||||
const m = ship.hardpoints[i].m;
|
||||
const sustainedDps = this._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange);
|
||||
shieldsdps.absolute += sustainedDps.damage.shields.absolute;
|
||||
shieldsdps.explosive += sustainedDps.damage.shields.explosive;
|
||||
shieldsdps.kinetic += sustainedDps.damage.shields.kinetic;
|
||||
shieldsdps.thermal += sustainedDps.damage.shields.thermal;
|
||||
armoursdps.absolute += sustainedDps.damage.armour.absolute;
|
||||
armoursdps.explosive += sustainedDps.damage.armour.explosive;
|
||||
armoursdps.kinetic += sustainedDps.damage.armour.kinetic;
|
||||
armoursdps.thermal += sustainedDps.damage.armour.thermal;
|
||||
eps += sustainedDps.eps;
|
||||
}
|
||||
}
|
||||
|
||||
return { shieldsdps, armoursdps, eps };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the sustained DPS for a weapon at a given range
|
||||
* @param {Object} m The weapon
|
||||
* @param {Object} opponent The opponent ship
|
||||
* @param {Object} opponentShields The opponent's shield resistances
|
||||
* @param {Object} opponentArmour The opponent's armour resistances
|
||||
* @param {int} engagementrange The range between the ship and opponent
|
||||
* @returns {Object} Sustained DPS for shield and armour
|
||||
*/
|
||||
export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange) {
|
||||
const opponentHasShields = opponentShields.generator ? true : false;
|
||||
const weapon = {
|
||||
eps: 0,
|
||||
damage: {
|
||||
shields: {
|
||||
absolute: 0,
|
||||
explosive: 0,
|
||||
kinetic: 0,
|
||||
thermal: 0,
|
||||
total: 0
|
||||
},
|
||||
armour: {
|
||||
absolute: 0,
|
||||
explosive: 0,
|
||||
kinetic: 0,
|
||||
thermal: 0,
|
||||
total: 0
|
||||
},
|
||||
},
|
||||
effectiveness: {
|
||||
shields: {
|
||||
range: 1,
|
||||
sys: opponentHasShields ? opponentShields.absolute.sys : 1,
|
||||
resistance: 1
|
||||
},
|
||||
armour: {
|
||||
range: 1,
|
||||
hardness: 1,
|
||||
resistance: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// EPS
|
||||
weapon.eps = m.getClip() ? (m.getClip() * m.getEps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getEps();
|
||||
|
||||
// Initial sustained DPS
|
||||
let sDps = m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getDps();
|
||||
|
||||
// Take fall-off in to account
|
||||
const falloff = m.getFalloff();
|
||||
if (falloff && engagementrange > falloff) {
|
||||
const dropoffRange = m.getRange() - falloff;
|
||||
const dropoff = 1 - Math.min((engagementrange - falloff) / dropoffRange, 1);
|
||||
weapon.effectiveness.shields.range = weapon.effectiveness.armour.range = dropoff;
|
||||
sDps *= dropoff;
|
||||
}
|
||||
|
||||
// Piercing/hardness modifier (for armour only)
|
||||
const armourMultiple = m.getPiercing() >= opponent.hardness ? 1 : m.getPiercing() / opponent.hardness;
|
||||
weapon.effectiveness.armour.hardness = armourMultiple;
|
||||
|
||||
// Break out the damage according to type
|
||||
let shieldsResistance = 0;
|
||||
let armourResistance = 0;
|
||||
if (m.getDamageDist().A) {
|
||||
weapon.damage.shields.absolute += sDps * m.getDamageDist().A * (opponentHasShields ? opponentShields.absolute.total : 1);
|
||||
weapon.damage.armour.absolute += sDps * m.getDamageDist().A * armourMultiple * opponentArmour.absolute.total;
|
||||
shieldsResistance += m.getDamageDist().A * (opponentHasShields ? opponentShields.absolute.generator * opponentShields.absolute.boosters : 1);
|
||||
armourResistance += m.getDamageDist().A * opponentArmour.absolute.bulkheads * opponentArmour.absolute.reinforcement;
|
||||
}
|
||||
if (m.getDamageDist().E) {
|
||||
weapon.damage.shields.explosive += sDps * m.getDamageDist().E * (opponentHasShields ? opponentShields.explosive.total : 1);
|
||||
weapon.damage.armour.explosive += sDps * m.getDamageDist().E * armourMultiple * opponentArmour.explosive.total;
|
||||
shieldsResistance += m.getDamageDist().E * (opponentHasShields ? opponentShields.explosive.generator * opponentShields.explosive.boosters : 1);
|
||||
armourResistance += m.getDamageDist().E * opponentArmour.explosive.bulkheads * opponentArmour.explosive.reinforcement;
|
||||
}
|
||||
if (m.getDamageDist().K) {
|
||||
weapon.damage.shields.kinetic += sDps * m.getDamageDist().K * (opponentHasShields ? opponentShields.kinetic.total : 1);
|
||||
weapon.damage.armour.kinetic += sDps * m.getDamageDist().K * armourMultiple * opponentArmour.kinetic.total;
|
||||
shieldsResistance += m.getDamageDist().K * (opponentHasShields ? opponentShields.kinetic.generator * opponentShields.kinetic.boosters : 1);
|
||||
armourResistance += m.getDamageDist().K * opponentArmour.kinetic.bulkheads * opponentArmour.kinetic.reinforcement;
|
||||
}
|
||||
if (m.getDamageDist().T) {
|
||||
weapon.damage.shields.thermal += sDps * m.getDamageDist().T * (opponentHasShields ? opponentShields.thermal.total : 1);
|
||||
weapon.damage.armour.thermal += sDps * m.getDamageDist().T * armourMultiple * opponentArmour.thermal.total;
|
||||
shieldsResistance += m.getDamageDist().T * (opponentHasShields ? opponentShields.thermal.generator * opponentShields.thermal.boosters : 1);
|
||||
armourResistance += m.getDamageDist().T * opponentArmour.thermal.bulkheads * opponentArmour.thermal.reinforcement;
|
||||
}
|
||||
weapon.damage.shields.total = weapon.damage.shields.absolute + weapon.damage.shields.explosive + weapon.damage.shields.kinetic + weapon.damage.shields.thermal;
|
||||
weapon.damage.armour.total = weapon.damage.armour.absolute + weapon.damage.armour.explosive + weapon.damage.armour.kinetic + weapon.damage.armour.thermal;
|
||||
|
||||
weapon.effectiveness.shields.resistance *= shieldsResistance;
|
||||
weapon.effectiveness.armour.resistance *= armourResistance;
|
||||
|
||||
weapon.effectiveness.shields.total = weapon.effectiveness.shields.range * weapon.effectiveness.shields.sys * weapon.effectiveness.shields.resistance;
|
||||
weapon.effectiveness.armour.total = weapon.effectiveness.armour.range * weapon.effectiveness.armour.resistance * weapon.effectiveness.armour.hardness;
|
||||
return weapon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate time to drain WEP capacitor
|
||||
* @param {object} ship The ship
|
||||
* @param {number} wep Pips to WEP
|
||||
* @returns {number} The time to drain the WEP capacitor, in seconds
|
||||
*/
|
||||
export function timeToDrainWep(ship, wep) {
|
||||
let totalSEps = 0;
|
||||
|
||||
for (let slotNum in ship.hardpoints) {
|
||||
const slot = ship.hardpoints[slotNum];
|
||||
if (slot.maxClass > 0 && slot.m && slot.enabled && slot.type === 'WEP' && slot.m.getDps()) {
|
||||
totalSEps += slot.m.getClip() ? (slot.m.getClip() * slot.m.getEps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : slot.m.getEps();
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the drain time
|
||||
const drainPerSecond = totalSEps - ship.standard[4].m.getWeaponsRechargeRate() * wep / 4;
|
||||
if (drainPerSecond <= 0) {
|
||||
// Can fire forever
|
||||
return Infinity;
|
||||
} else {
|
||||
const initialCharge = ship.standard[4].m.getWeaponsCapacity();
|
||||
return initialCharge / drainPerSecond;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the time to deplete an amount of shields or armour
|
||||
* @param {number} amount The amount to be depleted
|
||||
* @param {number} dps The depletion per second
|
||||
* @param {number} eps The energy drained per second
|
||||
* @param {number} capacity The initial energy capacity
|
||||
* @param {number} recharge The energy recharged per second
|
||||
* @returns {number} The number of seconds to deplete to 0
|
||||
*/
|
||||
export function timeToDeplete(amount, dps, eps, capacity, recharge) {
|
||||
const drainPerSecond = eps - recharge;
|
||||
if (drainPerSecond <= 0) {
|
||||
// Simple result
|
||||
return amount / dps;
|
||||
} else {
|
||||
// We are draining the capacitor, but can we deplete before we run out
|
||||
const timeToDrain = capacity / drainPerSecond;
|
||||
const depletedBeforeDrained = dps * timeToDrain;
|
||||
if (depletedBeforeDrained >= amount) {
|
||||
return amount / dps;
|
||||
} else {
|
||||
const restToDeplete = amount - depletedBeforeDrained;
|
||||
// We delete the rest at the reduced rate
|
||||
const reducedDps = dps * (recharge / eps);
|
||||
return timeToDrain + (restToDeplete / reducedDps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export const ModuleGroupToName = {
|
||||
pcm: 'First Class Passenger Cabin',
|
||||
pcq: 'Luxury Passenger Cabin',
|
||||
cc: 'Collector Limpet Controller',
|
||||
ss: 'Surface Scanner',
|
||||
|
||||
// Hard Points
|
||||
bl: 'Beam Laser',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as ModuleUtils from './ModuleUtils';
|
||||
import * as _ from 'lodash';
|
||||
import { Modifications } from 'coriolis-data/dist';
|
||||
|
||||
/**
|
||||
@@ -36,88 +35,133 @@ export default class Module {
|
||||
|
||||
/**
|
||||
* Get a value for a given modification
|
||||
* @param {Number} name The name of the modification
|
||||
* @return {object} The value of the modification. If it is a numeric value then it is returned as an integer value scaled so that 1.23% == 123
|
||||
* @param {Number} name The name of the modification
|
||||
* @param {Number} raw True if the value returned should be raw i.e. without the influence of special effects
|
||||
* @return {object} The value of the modification. If it is a numeric value then it is returned as an integer value scaled so that 1.23% == 123
|
||||
*/
|
||||
getModValue(name) {
|
||||
return this.mods && this.mods[name] ? this.mods[name] : null;
|
||||
getModValue(name, raw) {
|
||||
let result = this.mods && this.mods[name] ? this.mods[name] : null;
|
||||
if ((!raw) && this.blueprint && this.blueprint.special) {
|
||||
// This module has a special effect, see if we need to alter our returned value
|
||||
const modifierActions = Modifications.modifierActions[this.blueprint.special.edname];
|
||||
if (modifierActions && modifierActions[name]) {
|
||||
// this special effect modifies our returned value
|
||||
const modification = Modifications.modifications[name];
|
||||
if (modification.method === 'additive') {
|
||||
result = result + modifierActions[name] * 100;
|
||||
} else if (modification.method === 'overwrite') {
|
||||
result = modifierActions[name];
|
||||
} else {
|
||||
// rate of fire is special, as it's really burst interval. Handle that here
|
||||
let mod = null;
|
||||
if (name === 'rof') {
|
||||
mod = 1 / (1 + modifierActions[name]) - 1;
|
||||
} else {
|
||||
mod = modifierActions[name];
|
||||
}
|
||||
const multiplier = modification.type === 'percentage' ? 10000 : 100;
|
||||
result = (((1 + result / multiplier) * (1 + mod)) - 1) * multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitise the resultant value to 4dp equivalent
|
||||
return isNaN(result) ? result : Math.round(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value for a given modification ID
|
||||
* @param {Number} name The name of the modification
|
||||
* @param {Number} name The name of the modification
|
||||
* @param {object} value The value of the modification. If it is a numeric value then it should be an integer scaled so that -2.34% == -234
|
||||
* @param {bool} valueiswithspecial true if the value includes the special effect (when coming from a UI component)
|
||||
*/
|
||||
setModValue(name, value) {
|
||||
setModValue(name, value, valueiswithspecial) {
|
||||
if (!this.mods) {
|
||||
this.mods = {};
|
||||
}
|
||||
if (valueiswithspecial && this.blueprint && this.blueprint.special) {
|
||||
// This module has a special effect, see if we need to alter the stored value
|
||||
const modifierActions = Modifications.modifierActions[this.blueprint.special.edname];
|
||||
if (modifierActions && modifierActions[name]) {
|
||||
// This special effect modifies the value being set, so we need to revert it prior to storing the value
|
||||
const modification = Modifications.modifications[name];
|
||||
if (modification.method === 'additive') {
|
||||
value = value - modifierActions[name];
|
||||
} else if (modification.method === 'overwrite') {
|
||||
value = null;
|
||||
} else {
|
||||
// rate of fire is special, as it's really burst interval. Handle that here
|
||||
let mod = null;
|
||||
if (name === 'rof') {
|
||||
mod = 1 / (1 + modifierActions[name]) - 1;
|
||||
} else {
|
||||
mod = modifierActions[name];
|
||||
}
|
||||
value = ((value / 10000 + 1) / (1 + mod) - 1) * 10000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null || value == 0) {
|
||||
delete this.mods[name];
|
||||
} else {
|
||||
if (isNaN(value)) {
|
||||
this.mods[name] = value;
|
||||
} else {
|
||||
// Round just to be sure
|
||||
this.mods[name] = Math.round(value);
|
||||
}
|
||||
this.mods[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to obtain a modified value using standard multipliers
|
||||
* @param {String} name the name of the modifier to obtain
|
||||
* @param {Boolean} additive Optional true if the value is additive rather than multiplicative
|
||||
* @return {Number} the mass of this module
|
||||
*/
|
||||
_getModifiedValue(name, additive) {
|
||||
let result = this[name] || (additive ? 0 : null); // Additive NULL === 0
|
||||
if (result != null) {
|
||||
const modification = Modifications.modifications[name];
|
||||
if (!modification) {
|
||||
return result;
|
||||
}
|
||||
// We store percentages as decimals, so to get them back we need to divide by 10000. Otherwise
|
||||
// we divide by 100. Both ways we end up with a value with two decimal places
|
||||
let modValue;
|
||||
if (modification.type === 'percentage') {
|
||||
modValue = this.getModValue(name) / 10000;
|
||||
} else if (modification.type === 'numeric') {
|
||||
modValue = this.getModValue(name) / 100;
|
||||
_getModifiedValue(name) {
|
||||
const modification = Modifications.modifications[name];
|
||||
let result = this[name];
|
||||
|
||||
if (!result) {
|
||||
if (modification && modification.method === 'additive') {
|
||||
// Additive modifications start at 0 rather than NULL
|
||||
result = 0;
|
||||
} else {
|
||||
modValue = this.getModValue(name);
|
||||
result = null;
|
||||
}
|
||||
if (modValue) {
|
||||
if (additive) {
|
||||
result = result + modValue;
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
if (modification) {
|
||||
// We store percentages as decimals, so to get them back we need to divide by 10000. Otherwise
|
||||
// we divide by 100. Both ways we end up with a value with two decimal places
|
||||
let modValue;
|
||||
if (modification.type === 'percentage') {
|
||||
modValue = this.getModValue(name) / 10000;
|
||||
} else if (modification.type === 'numeric') {
|
||||
modValue = this.getModValue(name) / 100;
|
||||
} else {
|
||||
result = result * (1 + modValue);
|
||||
modValue = this.getModValue(name);
|
||||
}
|
||||
if (modValue) {
|
||||
if (modification.method === 'additive') {
|
||||
result = result + modValue;
|
||||
} else if (modification.method === 'overwrite') {
|
||||
result = modValue;
|
||||
} else {
|
||||
result = result * (1 + modValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (name === 'burst') {
|
||||
// Burst is special, as if it can not exist but have a modification
|
||||
const modValue = this.getModValue(name) / 100;
|
||||
return modValue;
|
||||
result = this.getModValue(name) / 100;
|
||||
} else if (name === 'burstrof') {
|
||||
// Burst rate of fire is special, as if it can not exist but have a modification
|
||||
const modValue = this.getModValue(name) / 100;
|
||||
return modValue;
|
||||
result = this.getModValue(name) / 100;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this is a shield generator
|
||||
* @return {Boolean} if this is a shield generator
|
||||
*/
|
||||
isShieldGenerator() {
|
||||
return (this.grp === 'sg' || this.grp === 'psg' || this.grp === 'bsg');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the power generation of this module, taking in to account modifications
|
||||
* @return {Number} the power generation of this module
|
||||
@@ -158,30 +202,6 @@ export default class Module {
|
||||
return this._getModifiedValue('eff');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum mass of this module, taking in to account modifications
|
||||
* @return {Number} the maximum mass of this module
|
||||
*/
|
||||
getMaxMass() {
|
||||
return this._getModifiedValue('maxmass');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the optimal mass of this module, taking in to account modifications
|
||||
* @return {Number} the optimal mass of this module
|
||||
*/
|
||||
getOptimalMass() {
|
||||
return this._getModifiedValue('optmass');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the optimal multiplier of this module, taking in to account modifications
|
||||
* @return {Number} the optimal multiplier of this module
|
||||
*/
|
||||
getOptimalMultiplier() {
|
||||
return this._getModifiedValue('optmult');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum fuel per jump for this module, taking in to account modifications
|
||||
* @return {Number} the maximum fuel per jump of this module
|
||||
@@ -243,7 +263,7 @@ export default class Module {
|
||||
* @return {Number} the kinetic resistance of this module
|
||||
*/
|
||||
getKineticResistance() {
|
||||
return this._getModifiedValue('kinres', true);
|
||||
return this._getModifiedValue('kinres');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -251,7 +271,7 @@ export default class Module {
|
||||
* @return {Number} the thermal resistance of this module
|
||||
*/
|
||||
getThermalResistance() {
|
||||
return this._getModifiedValue('thermres', true);
|
||||
return this._getModifiedValue('thermres');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,7 +279,7 @@ export default class Module {
|
||||
* @return {Number} the explosive resistance of this module
|
||||
*/
|
||||
getExplosiveResistance() {
|
||||
return this._getModifiedValue('explres', true);
|
||||
return this._getModifiedValue('explres');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,11 +312,19 @@ export default class Module {
|
||||
*/
|
||||
getFalloff() {
|
||||
if (this.getModValue('fallofffromrange')) {
|
||||
// Falloff from range means what it says, so use range instead of falloff
|
||||
return this.getRange();
|
||||
} else {
|
||||
const falloff = this._getModifiedValue('falloff');
|
||||
const range = this.getRange();
|
||||
return (falloff > range ? range : falloff);
|
||||
// Need to find out if we have a focused modification, in which case our falloff is scaled to range
|
||||
if (this.blueprint && this.blueprint.name === 'Focused') {
|
||||
const rangeMod = this.getModValue('range') / 10000;
|
||||
return this.falloff * (1 + rangeMod);
|
||||
} else {
|
||||
// Standard falloff calculation
|
||||
const range = this.getRange();
|
||||
const falloff = this._getModifiedValue('falloff');
|
||||
return (falloff > range ? range : falloff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +336,14 @@ export default class Module {
|
||||
return this._getModifiedValue('ranget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scan time for this module, taking in to account modifications
|
||||
* @return {Number} the scan time of this module
|
||||
*/
|
||||
getScanTime() {
|
||||
return this._getModifiedValue('scantime');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the capture arc for this module, taking in to account modifications
|
||||
* @return {Number} the capture arc of this module
|
||||
@@ -510,10 +546,10 @@ export default class Module {
|
||||
getEps() {
|
||||
// EPS is a synthetic value
|
||||
let distdraw = this.getDistDraw();
|
||||
let rpshot = this.roundspershot || 1;
|
||||
// We don't use rpshot here as dist draw is per combined shot
|
||||
let rof = this.getRoF() || 1;
|
||||
|
||||
return distdraw * rpshot * rof;
|
||||
return distdraw * rof;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -523,10 +559,10 @@ export default class Module {
|
||||
getHps() {
|
||||
// HPS is a synthetic value
|
||||
let heat = this.getThermalLoad();
|
||||
let rpshot = this.roundspershot || 1;
|
||||
// We don't use rpshot here as dist draw is per combined shot
|
||||
let rof = this.getRoF() || 1;
|
||||
|
||||
return heat * rpshot * rof;
|
||||
return heat * rof;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -534,7 +570,10 @@ export default class Module {
|
||||
* @return {Number} the clip size of this module
|
||||
*/
|
||||
getClip() {
|
||||
return this._getModifiedValue('clip');
|
||||
// Clip size is always rounded up
|
||||
let result = this._getModifiedValue('clip');
|
||||
if (result) { result = Math.ceil(result); }
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -633,14 +672,6 @@ export default class Module {
|
||||
return this._getModifiedValue('rebuildsperbay');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cells for this module, taking in to account modifications
|
||||
* @return {Number} the cells for this module
|
||||
*/
|
||||
getCells() {
|
||||
return this._getModifiedValue('cells');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the jitter for this module, taking in to account modifications
|
||||
* @return {Number} the jitter for this module
|
||||
@@ -659,9 +690,35 @@ export default class Module {
|
||||
|
||||
/**
|
||||
* Get the shot speed for this module, taking in to account modifications
|
||||
* @return {string} the damage distribution for this module
|
||||
* @return {string} the shot speed for this module
|
||||
*/
|
||||
getShotSpeed() {
|
||||
if (this.blueprint && (this.blueprint.name === 'Focused' || this.blueprintname === 'Long Range')) {
|
||||
// If the modification is focused or long range then the shot speed
|
||||
// uses the range modifier
|
||||
const rangemod = this.getModValue('range') / 10000;
|
||||
let result = this['shotspeed'];
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
return result * (1 + rangemod);
|
||||
}
|
||||
return this._getModifiedValue('shotspeed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the spinup for this module, taking in to account modifications
|
||||
* @return {string} the spinup for this module
|
||||
*/
|
||||
getSpinup() {
|
||||
return this._getModifiedValue('spinup');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time for this module, taking in to account modifications
|
||||
* @return {string} the time for this module
|
||||
*/
|
||||
getTime() {
|
||||
return this._getModifiedValue('time');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ export default class ModuleSet {
|
||||
let pd = this.standard[4][0];
|
||||
|
||||
for (let p of this.standard[4]) {
|
||||
if (p.mass < pd.mass && p.engcap >= boostEnergy) {
|
||||
if (p.mass < pd.mass && p.engcap > boostEnergy) {
|
||||
pd = p;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { ModuleGroupToName, MountMap, BulkheadNames } from './Constants';
|
||||
import { Ships } from 'coriolis-data/dist';
|
||||
import Ship from './Ship';
|
||||
import * as ModuleUtils from './ModuleUtils';
|
||||
import * as Utils from '../utils/UtilityFunctions';
|
||||
import LZString from 'lz-string';
|
||||
import { outfitURL } from '../utils/UrlGenerators';
|
||||
|
||||
const STANDARD = ['powerPlant', 'thrusters', 'frameShiftDrive', 'lifeSupport', 'powerDistributor', 'sensors', 'fuelTank'];
|
||||
|
||||
const STANDARD_GROUPS = { 'powerPlant': 'pp', 'thrusters': 't', 'frameShiftDrive': 'fsd', 'lifeSupport': 'ls', 'powerDistributor': 'pd', 'sensors': 's', 'fuelTank': 'ft' };
|
||||
|
||||
/**
|
||||
* Generates ship-loadout JSON Schema standard object
|
||||
* @param {Object} standard model
|
||||
@@ -90,7 +85,7 @@ export function toDetailedBuild(buildName, ship) {
|
||||
code = ship.toString();
|
||||
|
||||
let data = {
|
||||
$schema: 'http://cdn.coriolis.io/schemas/ship-loadout/4.json#',
|
||||
$schema: 'https://coriolis.edcd.io/schemas/ship-loadout/4.json#',
|
||||
name: buildName,
|
||||
ship: ship.name,
|
||||
references: [{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as Calc from './Calculations';
|
||||
import * as ModuleUtils from './ModuleUtils';
|
||||
import * as Utils from '../utils/UtilityFunctions';
|
||||
import { getBlueprint } from '../utils/BlueprintFunctions';
|
||||
import Module from './Module';
|
||||
import LZString from 'lz-string';
|
||||
import * as _ from 'lodash';
|
||||
@@ -122,31 +123,24 @@ export default class Ship {
|
||||
|
||||
/**
|
||||
* Can the ship thrust/move
|
||||
* @param {Number} cargo Amount of cargo in the ship
|
||||
* @param {Number} fuel Amount of fuel in the ship
|
||||
* @return {[type]} True if thrusters operational
|
||||
*/
|
||||
canThrust() {
|
||||
canThrust(cargo, fuel) {
|
||||
return this.getSlotStatus(this.standard[1]) == 3 && // Thrusters are powered
|
||||
this.ladenMass < this.standard[1].m.getMaxMass(); // Max mass not exceeded
|
||||
this.unladenMass + cargo + fuel < this.standard[1].m.getMaxMass(); // Max mass not exceeded
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the ship boost
|
||||
* @param {Number} cargo Amount of cargo in the ship
|
||||
* @param {Number} fuel Amount of fuel in the ship
|
||||
* @return {[type]} True if boost capable
|
||||
*/
|
||||
canBoost() {
|
||||
return this.canThrust() && // Thrusters operational
|
||||
this.boostEnergy <= this.standard[4].m.getEnginesCapacity(); // PD capacitor is sufficient for boost
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate hypothetical jump range using the installed FSD and the
|
||||
* specified mass which can be more or less than ships actual mass
|
||||
* @param {Number} fuel Fuel available in tons
|
||||
* @param {Number} cargo Cargo in tons
|
||||
* @return {Number} Jump range in Light Years
|
||||
*/
|
||||
calcJumpRangeWith(fuel, cargo) {
|
||||
return Calc.jumpRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel);
|
||||
canBoost(cargo, fuel) {
|
||||
return this.canThrust(cargo, fuel) && // Thrusters operational
|
||||
this.standard[4].m.getEnginesCapacity() > this.boostEnergy; // PD capacitor is sufficient for boost
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,17 +167,6 @@ export default class Ship {
|
||||
return Calc.jumpRange(this.unladenMass + (massDelta || 0) + Math.min(fsdMaxFuelPerJump, fuel || this.fuelCapacity), fsd || this.standard[2].m, fuel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cumulative (total) jump range when making longest jumps using the installed FSD and the
|
||||
* specified mass which can be more or less than ships actual mass
|
||||
* @param {Number} fuel Fuel available in tons
|
||||
* @param {Number} cargo Cargo in tons
|
||||
* @return {Number} Total/Cumulative Jump range in Light Years
|
||||
*/
|
||||
calcFastestRangeWith(fuel, cargo) {
|
||||
return Calc.fastestRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the hypothetical top speeds at cargo and fuel tonnage
|
||||
* @param {Number} fuel Fuel available in tons
|
||||
@@ -195,36 +178,51 @@ export default class Ship {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the recovery time after losing or turning on shields
|
||||
* Thanks to CMDRs Al Gray, GIF, and Nomad Enigma for providing Shield recharge data and formulas
|
||||
*
|
||||
* @return {Number} Recovery time in seconds
|
||||
* Calculate the speed for a given configuration
|
||||
* @param {Number} eng Number of pips in ENG
|
||||
* @param {Number} fuel Amount of fuel carried
|
||||
* @param {Number} cargo Amount of cargo carried
|
||||
* @param {boolean} boost true if boost is applied
|
||||
* @return {Number} Speed
|
||||
*/
|
||||
calcShieldRecovery() {
|
||||
const shieldGenerator = this.findShieldGenerator();
|
||||
if (shieldGenerator) {
|
||||
const brokenRegenRate = shieldGenerator.getBrokenRegenerationRate();
|
||||
// 50% of shield strength / broken recharge rate + 15 second delay before recharge starts
|
||||
return ((this.shield / 2) / brokenRegenRate) + 15;
|
||||
}
|
||||
return 0;
|
||||
calcSpeed(eng, fuel, cargo, boost) {
|
||||
return Calc.calcSpeed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed, eng, this.boost / this.speed, boost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the recharge time for a shield going from 50% to 100%
|
||||
* Thanks to CMDRs Al Gray, GIF, and Nomad Enigma for providing Shield recharge data and formulas
|
||||
*
|
||||
* @return {Number} 50 - 100% Recharge time in seconds
|
||||
* Calculate the pitch for a given configuration
|
||||
* @param {Number} eng Number of pips in ENG
|
||||
* @param {Number} fuel Amount of fuel carried
|
||||
* @param {Number} cargo Amount of cargo carried
|
||||
* @param {boolean} boost true if boost is applied
|
||||
* @return {Number} Pitch
|
||||
*/
|
||||
calcShieldRecharge() {
|
||||
const shieldGenerator = this.findShieldGenerator();
|
||||
if (shieldGenerator) {
|
||||
const regenRate = shieldGenerator.getRegenerationRate();
|
||||
calcPitch(eng, fuel, cargo, boost) {
|
||||
return Calc.calcPitch(this.unladenMass + fuel + cargo, this.pitch, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost);
|
||||
}
|
||||
|
||||
// 50% of shield strength / recharge rate
|
||||
return (this.shield / 2) / regenRate;
|
||||
}
|
||||
return 0;
|
||||
/**
|
||||
* Calculate the roll for a given configuration
|
||||
* @param {Number} eng Number of pips in ENG
|
||||
* @param {Number} fuel Amount of fuel carried
|
||||
* @param {Number} cargo Amount of cargo carried
|
||||
* @param {boolean} boost true if boost is applied
|
||||
* @return {Number} Roll
|
||||
*/
|
||||
calcRoll(eng, fuel, cargo, boost) {
|
||||
return Calc.calcRoll(this.unladenMass + fuel + cargo, this.roll, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the yaw for a given configuration
|
||||
* @param {Number} eng Number of pips in ENG
|
||||
* @param {Number} fuel Amount of fuel carried
|
||||
* @param {Number} cargo Amount of cargo carried
|
||||
* @param {boolean} boost true if boost is applied
|
||||
* @return {Number} Yaw
|
||||
*/
|
||||
calcYaw(eng, fuel, cargo, boost) {
|
||||
return Calc.calcYaw(this.unladenMass + fuel + cargo, this.yaw, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -419,6 +417,7 @@ export default class Ship {
|
||||
m.mods = {};
|
||||
this.updatePowerGenerated()
|
||||
.updatePowerUsed()
|
||||
.recalculateMass()
|
||||
.updateJumpStats()
|
||||
.recalculateShield()
|
||||
.recalculateShieldCells()
|
||||
@@ -438,12 +437,13 @@ export default class Ship {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a modification value
|
||||
* @param {Object} m The module to change
|
||||
* @param {Object} name The name of the modification to change
|
||||
* Set a modification value and update ship stats
|
||||
* @param {Object} m The module to change
|
||||
* @param {Object} name The name of the modification to change
|
||||
* @param {Number} value The new value of the modification. The value of the modification is scaled to provide two decimal places of precision in an integer. For example 1.23% is stored as 123
|
||||
* @param {bool} sentfromui True if this update was sent from the UI
|
||||
*/
|
||||
setModification(m, name, value) {
|
||||
setModification(m, name, value, sentfromui) {
|
||||
if (isNaN(value)) {
|
||||
// Value passed is invalid; reset it to 0
|
||||
value = 0;
|
||||
@@ -452,58 +452,59 @@ export default class Ship {
|
||||
// Handle special cases
|
||||
if (name === 'pgen') {
|
||||
// Power generation
|
||||
m.setModValue(name, value);
|
||||
m.setModValue(name, value, sentfromui);
|
||||
this.updatePowerGenerated();
|
||||
} else if (name === 'power') {
|
||||
// Power usage
|
||||
m.setModValue(name, value);
|
||||
m.setModValue(name, value, sentfromui);
|
||||
this.updatePowerUsed();
|
||||
} else if (name === 'mass') {
|
||||
// Mass
|
||||
let oldMass = m.getMass();
|
||||
m.setModValue(name, value);
|
||||
let newMass = m.getMass();
|
||||
this.unladenMass = this.unladenMass - oldMass + newMass;
|
||||
this.ladenMass = this.ladenMass - oldMass + newMass;
|
||||
m.setModValue(name, value, sentfromui);
|
||||
this.recalculateMass();
|
||||
this.updateMovement();
|
||||
this.updateJumpStats();
|
||||
} else if (name === 'maxfuel') {
|
||||
m.setModValue(name, value);
|
||||
m.setModValue(name, value, sentfromui);
|
||||
this.updateJumpStats();
|
||||
} else if (name === 'optmass') {
|
||||
m.setModValue(name, value);
|
||||
m.setModValue(name, value, sentfromui);
|
||||
// Could be for any of thrusters, FSD or shield
|
||||
this.updateMovement();
|
||||
this.updateJumpStats();
|
||||
this.recalculateShield();
|
||||
} else if (name === 'optmul') {
|
||||
m.setModValue(name, value);
|
||||
m.setModValue(name, value, sentfromui);
|
||||
// Could be for any of thrusters, FSD or shield
|
||||
this.updateMovement();
|
||||
this.updateJumpStats();
|
||||
this.recalculateShield();
|
||||
} else if (name === 'shieldboost') {
|
||||
m.setModValue(name, value);
|
||||
m.setModValue(name, value, sentfromui);
|
||||
this.recalculateShield();
|
||||
} else if (name === 'hullboost' || name === 'hullreinforcement' || name === 'modulereinforcement') {
|
||||
m.setModValue(name, value);
|
||||
m.setModValue(name, value, sentfromui);
|
||||
this.recalculateArmour();
|
||||
} else if (name === 'shieldreinforcement') {
|
||||
m.setModValue(name, value);
|
||||
m.setModValue(name, value, sentfromui);
|
||||
this.recalculateShieldCells();
|
||||
} else if (name === 'burst' || name == 'burstrof' || name === 'clip' || name === 'damage' || name === 'distdraw' || name === 'jitter' || name === 'piercing' || name === 'range' || name === 'reload' || name === 'rof' || name === 'thermload') {
|
||||
m.setModValue(name, value);
|
||||
m.setModValue(name, value, sentfromui);
|
||||
this.recalculateDps();
|
||||
this.recalculateHps();
|
||||
this.recalculateEps();
|
||||
} else if (name === 'explres' || name === 'kinres' || name === 'thermres') {
|
||||
m.setModValue(name, value);
|
||||
m.setModValue(name, value, sentfromui);
|
||||
// Could be for shields or armour
|
||||
this.recalculateArmour();
|
||||
this.recalculateShield();
|
||||
} else if (name === 'engcap') {
|
||||
m.setModValue(name, value, sentfromui);
|
||||
// Might have resulted in a change in boostability
|
||||
this.updateMovement();
|
||||
} else {
|
||||
// Generic
|
||||
m.setModValue(name, value);
|
||||
m.setModValue(name, value, sentfromui);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,14 +534,17 @@ export default class Ship {
|
||||
this.totalCost = this.m.incCost ? this.m.discountedCost : 0;
|
||||
this.unladenMass = this.hullMass;
|
||||
this.totalDpe = 0;
|
||||
this.totalAbsDpe = 0;
|
||||
this.totalExplDpe = 0;
|
||||
this.totalKinDpe = 0;
|
||||
this.totalThermDpe = 0;
|
||||
this.totalDps = 0;
|
||||
this.totalAbsDps = 0;
|
||||
this.totalExplDps = 0;
|
||||
this.totalKinDps = 0;
|
||||
this.totalThermDps = 0;
|
||||
this.totalSDps = 0;
|
||||
this.totalAbsSDps = 0;
|
||||
this.totalExplSDps = 0;
|
||||
this.totalKinSDps = 0;
|
||||
this.totalThermSDps = 0;
|
||||
@@ -556,13 +560,18 @@ export default class Ship {
|
||||
this.bulkheads.m = null;
|
||||
this.useBulkhead(comps && comps.bulkheads ? comps.bulkheads : 0, true);
|
||||
this.bulkheads.m.mods = mods && mods[0] ? mods[0] : {};
|
||||
this.bulkheads.m.blueprint = blueprints && blueprints[0] ? blueprints[0] : {};
|
||||
if (blueprints && blueprints[0]) {
|
||||
this.bulkheads.m.blueprint = getBlueprint(blueprints[0].fdname, this.bulkheads.m);
|
||||
this.bulkheads.m.blueprint.grade = blueprints[0].grade;
|
||||
this.bulkheads.m.blueprint.special = blueprints[0].special;
|
||||
} else {
|
||||
this.bulkheads.m.blueprint = {};
|
||||
}
|
||||
this.cargoHatch.priority = priorities ? priorities[0] * 1 : 0;
|
||||
this.cargoHatch.enabled = enabled ? enabled[0] * 1 : true;
|
||||
|
||||
for (i = 0; i < cl; i++) {
|
||||
standard[i].cat = 0;
|
||||
standard[i].enabled = enabled ? enabled[i + 1] * 1 : true;
|
||||
standard[i].priority = priorities && priorities[i + 1] ? priorities[i + 1] * 1 : 0;
|
||||
standard[i].type = 'SYS';
|
||||
standard[i].m = null; // Resetting 'old' modul if there was one
|
||||
@@ -571,10 +580,17 @@ export default class Ship {
|
||||
let module = ModuleUtils.standard(i, comps.standard[i]);
|
||||
if (module != null) {
|
||||
module.mods = mods && mods[i + 1] ? mods[i + 1] : {};
|
||||
module.blueprint = blueprints && blueprints[i + 1] ? blueprints[i + 1] : {};
|
||||
if (blueprints && blueprints[i + 1]) {
|
||||
module.blueprint = getBlueprint(blueprints[i + 1].fdname, module);
|
||||
module.blueprint.grade = blueprints[i + 1].grade;
|
||||
module.blueprint.special = blueprints[i + 1].special;
|
||||
} else {
|
||||
module.blueprint = {};
|
||||
}
|
||||
}
|
||||
this.use(standard[i], module, true);
|
||||
}
|
||||
standard[i].enabled = enabled ? enabled[i + 1] * 1 : true;
|
||||
}
|
||||
|
||||
standard[1].type = 'ENG'; // Thrusters
|
||||
@@ -583,7 +599,6 @@ export default class Ship {
|
||||
|
||||
for (i = 0, l = hps.length; i < l; i++) {
|
||||
hps[i].cat = 1;
|
||||
hps[i].enabled = enabled ? enabled[cl + i] * 1 : true;
|
||||
hps[i].priority = priorities && priorities[cl + i] ? priorities[cl + i] * 1 : 0;
|
||||
hps[i].type = hps[i].maxClass ? 'WEP' : 'SYS';
|
||||
hps[i].m = null; // Resetting 'old' modul if there was one
|
||||
@@ -593,17 +608,23 @@ export default class Ship {
|
||||
let module = ModuleUtils.hardpoints(comps.hardpoints[i]);
|
||||
if (module != null) {
|
||||
module.mods = mods && mods[cl + i] ? mods[cl + i] : {};
|
||||
module.blueprint = blueprints && blueprints[cl + i] ? blueprints[cl + i] : {};
|
||||
if (blueprints && blueprints[cl + i]) {
|
||||
module.blueprint = getBlueprint(blueprints[cl + i].fdname, module);
|
||||
module.blueprint.grade = blueprints[cl + i].grade;
|
||||
module.blueprint.special = blueprints[cl + i].special;
|
||||
} else {
|
||||
module.blueprint = {};
|
||||
}
|
||||
}
|
||||
this.use(hps[i], module, true);
|
||||
}
|
||||
hps[i].enabled = enabled ? enabled[cl + i] * 1 : true;
|
||||
}
|
||||
|
||||
cl += hps.length; // Increase accounts for hardpoints
|
||||
|
||||
for (i = 0, l = internal.length; i < l; i++) {
|
||||
internal[i].cat = 2;
|
||||
internal[i].enabled = enabled ? enabled[cl + i] * 1 : true;
|
||||
internal[i].priority = priorities && priorities[cl + i] ? priorities[cl + i] * 1 : 0;
|
||||
internal[i].type = 'SYS';
|
||||
internal[i].m = null; // Resetting 'old' modul if there was one
|
||||
@@ -613,16 +634,24 @@ export default class Ship {
|
||||
let module = ModuleUtils.internal(comps.internal[i]);
|
||||
if (module != null) {
|
||||
module.mods = mods && mods[cl + i] ? mods[cl + i] : {};
|
||||
module.blueprint = blueprints && blueprints[cl + i] ? blueprints[cl + i] : {};
|
||||
if (blueprints && blueprints[cl + i]) {
|
||||
module.blueprint = getBlueprint(blueprints[cl + i].fdname, module);
|
||||
module.blueprint.grade = blueprints[cl + i].grade;
|
||||
module.blueprint.special = blueprints[cl + i].special;
|
||||
} else {
|
||||
module.blueprint = {};
|
||||
}
|
||||
}
|
||||
this.use(internal[i], module, true);
|
||||
}
|
||||
internal[i].enabled = enabled ? enabled[cl + i] * 1 : true;
|
||||
}
|
||||
|
||||
// Update aggragated stats
|
||||
if (comps) {
|
||||
this.updatePowerGenerated()
|
||||
.updatePowerUsed()
|
||||
.recalculateMass()
|
||||
.updateJumpStats()
|
||||
.recalculateShield()
|
||||
.recalculateShieldCells()
|
||||
@@ -825,7 +854,6 @@ export default class Ship {
|
||||
*/
|
||||
setSlotPriority(slot, newPriority) {
|
||||
if (newPriority >= 0 && newPriority < this.priorityBands.length) {
|
||||
let oldPriority = slot.priority;
|
||||
slot.priority = newPriority;
|
||||
this.updatePowerPrioritesString();
|
||||
|
||||
@@ -847,6 +875,7 @@ export default class Ship {
|
||||
*/
|
||||
updateStats(slot, n, old, preventUpdate) {
|
||||
let powerGeneratedChange = slot == this.standard[0];
|
||||
let powerDistributorChange = slot == this.standard[4];
|
||||
let powerUsedChange = false;
|
||||
let dpsChanged = n && n.getDps() || old && old.getDps();
|
||||
let epsChanged = n && n.getEps() || old && old.getEps();
|
||||
@@ -859,15 +888,6 @@ export default class Ship {
|
||||
let shieldCellsChange = (n && n.grp === 'scb') || (old && old.grp === 'scb');
|
||||
|
||||
if (old) { // Old modul now being removed
|
||||
switch (old.grp) {
|
||||
case 'ft':
|
||||
this.fuelCapacity -= old.fuel;
|
||||
break;
|
||||
case 'cr':
|
||||
this.cargoCapacity -= old.cargo;
|
||||
break;
|
||||
}
|
||||
|
||||
if (slot.incCost && old.cost) {
|
||||
this.totalCost -= old.cost * this.moduleCostMultiplier;
|
||||
}
|
||||
@@ -875,20 +895,9 @@ export default class Ship {
|
||||
if (old.getPowerUsage() > 0 && slot.enabled) {
|
||||
powerUsedChange = true;
|
||||
}
|
||||
|
||||
this.unladenMass -= old.getMass() || 0;
|
||||
}
|
||||
|
||||
if (n) {
|
||||
switch (n.grp) {
|
||||
case 'ft':
|
||||
this.fuelCapacity += n.fuel;
|
||||
break;
|
||||
case 'cr':
|
||||
this.cargoCapacity += n.cargo;
|
||||
break;
|
||||
}
|
||||
|
||||
if (slot.incCost && n.cost) {
|
||||
this.totalCost += n.cost * this.moduleCostMultiplier;
|
||||
}
|
||||
@@ -896,13 +905,11 @@ export default class Ship {
|
||||
if (n.power && slot.enabled) {
|
||||
powerUsedChange = true;
|
||||
}
|
||||
|
||||
this.unladenMass += n.getMass() || 0;
|
||||
}
|
||||
|
||||
this.ladenMass = this.unladenMass + this.cargoCapacity + this.fuelCapacity;
|
||||
|
||||
if (!preventUpdate) {
|
||||
// Must recalculate mass first, as movement, jump etc. relies on it
|
||||
this.recalculateMass();
|
||||
if (dpsChanged) {
|
||||
this.recalculateDps();
|
||||
}
|
||||
@@ -953,26 +960,29 @@ export default class Ship {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate damage per second for weapons
|
||||
* Calculate damage per second and related items for weapons
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
recalculateDps() {
|
||||
let totalDpe = 0;
|
||||
let totalAbsDpe = 0;
|
||||
let totalExplDpe = 0;
|
||||
let totalKinDpe = 0;
|
||||
let totalThermDpe = 0;
|
||||
let totalDps = 0;
|
||||
let totalAbsDps = 0;
|
||||
let totalExplDps = 0;
|
||||
let totalKinDps = 0;
|
||||
let totalThermDps = 0;
|
||||
let totalSDps = 0;
|
||||
let totalAbsSDps = 0;
|
||||
let totalExplSDps = 0;
|
||||
let totalKinSDps = 0;
|
||||
let totalThermSDps = 0;
|
||||
|
||||
for (let slotNum in this.hardpoints) {
|
||||
const slot = this.hardpoints[slotNum];
|
||||
if (slot.m && slot.enabled && slot.m.getDps()) {
|
||||
if (slot.m && slot.enabled && slot.type === 'WEP' && slot.m.getDps()) {
|
||||
const dpe = slot.m.getEps() === 0 ? 0 : slot.m.getDps() / slot.m.getEps();
|
||||
const dps = slot.m.getDps();
|
||||
const sdps = slot.m.getClip() ? (slot.m.getClip() * slot.m.getDps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : dps;
|
||||
@@ -981,6 +991,11 @@ export default class Ship {
|
||||
totalDps += dps;
|
||||
totalSDps += sdps;
|
||||
if (slot.m.getDamageDist()) {
|
||||
if (slot.m.getDamageDist().A) {
|
||||
totalAbsDpe += dpe * slot.m.getDamageDist().A;
|
||||
totalAbsDps += dps * slot.m.getDamageDist().A;
|
||||
totalAbsSDps += sdps * slot.m.getDamageDist().A;
|
||||
}
|
||||
if (slot.m.getDamageDist().E) {
|
||||
totalExplDpe += dpe * slot.m.getDamageDist().E;
|
||||
totalExplDps += dps * slot.m.getDamageDist().E;
|
||||
@@ -1001,14 +1016,17 @@ export default class Ship {
|
||||
}
|
||||
|
||||
this.totalDpe = totalDpe;
|
||||
this.totalAbsDpe = totalAbsDpe;
|
||||
this.totalExplDpe = totalExplDpe;
|
||||
this.totalKinDpe = totalKinDpe;
|
||||
this.totalThermDpe = totalThermDpe;
|
||||
this.totalDps = totalDps;
|
||||
this.totalAbsDps = totalAbsDps;
|
||||
this.totalExplDps = totalExplDps;
|
||||
this.totalKinDps = totalKinDps;
|
||||
this.totalThermDps = totalThermDps;
|
||||
this.totalSDps = totalSDps;
|
||||
this.totalAbsSDps = totalAbsSDps;
|
||||
this.totalExplSDps = totalExplSDps;
|
||||
this.totalKinSDps = totalKinSDps;
|
||||
this.totalThermSDps = totalThermSDps;
|
||||
@@ -1025,7 +1043,7 @@ export default class Ship {
|
||||
|
||||
for (let slotNum in this.hardpoints) {
|
||||
const slot = this.hardpoints[slotNum];
|
||||
if (slot.m && slot.enabled && slot.m.getHps()) {
|
||||
if (slot.m && slot.enabled && slot.type === 'WEP' && slot.m.getHps()) {
|
||||
totalHps += slot.m.getHps();
|
||||
}
|
||||
}
|
||||
@@ -1043,7 +1061,7 @@ export default class Ship {
|
||||
|
||||
for (let slotNum in this.hardpoints) {
|
||||
const slot = this.hardpoints[slotNum];
|
||||
if (slot.m && slot.enabled && slot.m.getEps()) {
|
||||
if (slot.m && slot.enabled && slot.m.getEps() && slot.type === 'WEP') {
|
||||
totalEps += slot.m.getEps();
|
||||
}
|
||||
}
|
||||
@@ -1115,6 +1133,55 @@ export default class Ship {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eecalculate mass
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
recalculateMass() {
|
||||
let unladenMass = this.hullMass;
|
||||
let cargoCapacity = 0;
|
||||
let fuelCapacity = 0;
|
||||
|
||||
unladenMass += this.bulkheads.m.getMass();
|
||||
|
||||
for (let slotNum in this.standard) {
|
||||
const slot = this.standard[slotNum];
|
||||
if (slot.m) {
|
||||
unladenMass += slot.m.getMass();
|
||||
if (slot.m.grp === 'ft') {
|
||||
fuelCapacity += slot.m.fuel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let slotNum in this.internal) {
|
||||
const slot = this.internal[slotNum];
|
||||
if (slot.m) {
|
||||
unladenMass += slot.m.getMass();
|
||||
if (slot.m.grp === 'ft') {
|
||||
fuelCapacity += slot.m.fuel;
|
||||
} else if (slot.m.grp === 'cr') {
|
||||
cargoCapacity += slot.m.cargo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let slotNum in this.hardpoints) {
|
||||
const slot = this.hardpoints[slotNum];
|
||||
if (slot.m) {
|
||||
unladenMass += slot.m.getMass();
|
||||
}
|
||||
}
|
||||
|
||||
// Update global stats
|
||||
this.unladenMass = unladenMass;
|
||||
this.cargoCapacity = cargoCapacity;
|
||||
this.fuelCapacity = fuelCapacity;
|
||||
this.ladenMass = unladenMass + fuelCapacity + cargoCapacity;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update movement values
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
@@ -1122,7 +1189,7 @@ export default class Ship {
|
||||
updateMovement() {
|
||||
this.speeds = Calc.speed(this.unladenMass + this.fuelCapacity, this.speed, this.standard[1].m, this.pipSpeed);
|
||||
this.topSpeed = this.speeds[4];
|
||||
this.topBoost = this.canBoost() ? this.speeds[4] * this.boost / this.speed : 0;
|
||||
this.topBoost = this.canBoost(0, 0) ? this.speeds[4] * this.boost / this.speed : 0;
|
||||
|
||||
this.pitches = Calc.pitch(this.unladenMass + this.fuelCapacity, this.pitch, this.standard[1].m, this.pipSpeed);
|
||||
this.topPitch = this.pitches[4];
|
||||
@@ -1141,54 +1208,13 @@ export default class Ship {
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
recalculateShield() {
|
||||
let shield = 0;
|
||||
let shieldBoost = 1;
|
||||
let shieldExplRes = null;
|
||||
let shieldKinRes = null;
|
||||
let shieldThermRes = null;
|
||||
let shieldExplDRStart = null;
|
||||
let shieldExplDREnd = null;
|
||||
let shieldKinDRStart = null;
|
||||
let shieldKinDREnd = null;
|
||||
let shieldThermDRStart = null;
|
||||
let shieldThermDREnd = null;
|
||||
|
||||
const sgSlot = this.findInternalByGroup('sg');
|
||||
if (sgSlot && sgSlot.enabled) {
|
||||
// Shield from generator
|
||||
shield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1);
|
||||
shieldExplRes = 1 - sgSlot.m.getExplosiveResistance();
|
||||
shieldExplDRStart = shieldExplRes * 0.7;
|
||||
shieldExplDREnd = shieldExplRes * 0; // Currently don't know where this is
|
||||
shieldKinRes = 1 - sgSlot.m.getKineticResistance();
|
||||
shieldKinDRStart = shieldKinRes * 0.7;
|
||||
shieldKinDREnd = shieldKinRes * 0; // Currently don't know where this is
|
||||
shieldThermRes = 1 - sgSlot.m.getThermalResistance();
|
||||
shieldThermDRStart = shieldThermRes * 0.7;
|
||||
shieldThermDREnd = shieldThermRes * 0; // Currently don't know where this is
|
||||
|
||||
// Shield from boosters
|
||||
for (let slot of this.hardpoints) {
|
||||
if (slot.enabled && slot.m && slot.m.grp == 'sb') {
|
||||
shieldBoost += slot.m.getShieldBoost();
|
||||
shieldExplRes *= (1 - slot.m.getExplosiveResistance());
|
||||
shieldKinRes *= (1 - slot.m.getKineticResistance());
|
||||
shieldThermRes *= (1 - slot.m.getThermalResistance());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We apply diminishing returns to the boosted value
|
||||
// (no we don't; FD pulled back on this idea. But leave this here in case they reinstate it)
|
||||
// shieldBoost = Math.min(shieldBoost, (1 - Math.pow(Math.E, -0.7 * shieldBoost)) * 2.5);
|
||||
|
||||
shield = shield * shieldBoost;
|
||||
|
||||
this.shield = shield;
|
||||
this.shieldExplRes = shieldExplRes ? 1 - this.diminishingReturns(shieldExplRes, shieldExplDREnd, shieldExplDRStart) : null;
|
||||
this.shieldKinRes = shieldKinRes ? 1 - this.diminishingReturns(shieldKinRes, shieldKinDREnd, shieldKinDRStart) : null;
|
||||
this.shieldThermRes = shieldThermRes ? 1 - this.diminishingReturns(shieldThermRes, shieldThermDREnd, shieldThermDRStart) : null;
|
||||
// Obtain shield metrics with 0 pips to sys (parts affected by SYS aren't used here)
|
||||
const metrics = Calc.shieldMetrics(this, 0);
|
||||
|
||||
this.shield = metrics.generator ? metrics.generator + metrics.boosters : 0;
|
||||
this.shieldExplRes = this.shield > 0 ? 1 - metrics.explosive.total : null;
|
||||
this.shieldKinRes = this.shield > 0 ? 1 - metrics.kinetic.total : null;
|
||||
this.shieldThermRes = this.shield > 0 ? 1 - metrics.thermal.total : null;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1201,7 +1227,9 @@ export default class Ship {
|
||||
|
||||
for (let slot of this.internal) {
|
||||
if (slot.m && slot.m.grp == 'scb') {
|
||||
shieldCells += slot.m.getShieldReinforcement() * slot.m.getCells();
|
||||
// There is currently a bug with Elite where you can have a clip > 1 thanks to engineering but it doesn't do anything,
|
||||
// so we need to hard-code clip to 1
|
||||
shieldCells += slot.m.getShieldReinforcement() * slot.m.getDuration() * (slot.m.getAmmo() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1222,13 +1250,13 @@ export default class Ship {
|
||||
let moduleprotection = 1;
|
||||
let hullExplRes = 1 - bulkhead.getExplosiveResistance();
|
||||
const hullExplResDRStart = hullExplRes * 0.7;
|
||||
const hullExplResDREnd = hullExplRes * 0; // Currently don't know where this is
|
||||
const hullExplResDREnd = hullExplRes * 0;
|
||||
let hullKinRes = 1 - bulkhead.getKineticResistance();
|
||||
const hullKinResDRStart = hullKinRes * 0.7;
|
||||
const hullKinResDREnd = hullKinRes * 0; // Currently don't know where this is
|
||||
const hullKinResDREnd = hullKinRes * 0;
|
||||
let hullThermRes = 1 - bulkhead.getThermalResistance();
|
||||
const hullThermResDRStart = hullThermRes * 0.7;
|
||||
const hullThermResDREnd = hullThermRes * 0; // Currently don't know where this is
|
||||
const hullThermResDREnd = hullThermRes * 0;
|
||||
|
||||
// Armour from HRPs and module armour from MRPs
|
||||
for (let slot of this.internal) {
|
||||
@@ -1268,9 +1296,9 @@ export default class Ship {
|
||||
this.unladenRange = this.calcUnladenRange(); // Includes fuel weight for jump
|
||||
this.fullTankRange = Calc.jumpRange(unladenMass + fuelCapacity, fsd); // Full Tank
|
||||
this.ladenRange = this.calcLadenRange(); // Includes full tank and caro
|
||||
this.unladenFastestRange = Calc.fastestRange(unladenMass, fsd, fuelCapacity);
|
||||
this.ladenFastestRange = Calc.fastestRange(unladenMass + this.cargoCapacity, fsd, fuelCapacity);
|
||||
this.maxJumpCount = Math.ceil(fuelCapacity / fsd.maxfuel);
|
||||
this.unladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity, fsd, fuelCapacity);
|
||||
this.ladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity + this.cargoCapacity, fsd, fuelCapacity);
|
||||
this.maxJumpCount = Math.ceil(fuelCapacity / fsd.getMaxFuelPerJump());
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1357,12 +1385,11 @@ export default class Ship {
|
||||
|
||||
let bulkheadMods = new Array();
|
||||
let bulkheadBlueprint = null;
|
||||
let bulkheadBlueprintGrade = null;
|
||||
if (this.bulkheads.m && this.bulkheads.m.mods) {
|
||||
for (let modKey in this.bulkheads.m.mods) {
|
||||
// Filter out invalid modifications
|
||||
if (Modifications.modules['bh'] && Modifications.modules['bh'].modifications.indexOf(modKey) != -1) {
|
||||
bulkheadMods.push({ id: Modifications.modifications[modKey].id, value: this.bulkheads.m.getModValue(modKey) });
|
||||
bulkheadMods.push({ id: Modifications.modifications[modKey].id, value: this.bulkheads.m.getModValue(modKey, true) });
|
||||
}
|
||||
}
|
||||
bulkheadBlueprint = this.bulkheads.m.blueprint;
|
||||
@@ -1377,7 +1404,7 @@ export default class Ship {
|
||||
for (let modKey in slot.m.mods) {
|
||||
// Filter out invalid modifications
|
||||
if (Modifications.modules[slot.m.grp] && Modifications.modules[slot.m.grp].modifications.indexOf(modKey) != -1) {
|
||||
slotMods.push({ id: Modifications.modifications[modKey].id, value: slot.m.getModValue(modKey) });
|
||||
slotMods.push({ id: Modifications.modifications[modKey].id, value: slot.m.getModValue(modKey, true) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1392,7 +1419,7 @@ export default class Ship {
|
||||
for (let modKey in slot.m.mods) {
|
||||
// Filter out invalid modifications
|
||||
if (Modifications.modules[slot.m.grp] && Modifications.modules[slot.m.grp].modifications.indexOf(modKey) != -1) {
|
||||
slotMods.push({ id: Modifications.modifications[modKey].id, value: slot.m.getModValue(modKey) });
|
||||
slotMods.push({ id: Modifications.modifications[modKey].id, value: slot.m.getModValue(modKey, true) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1407,7 +1434,7 @@ export default class Ship {
|
||||
for (let modKey in slot.m.mods) {
|
||||
// Filter out invalid modifications
|
||||
if (Modifications.modules[slot.m.grp] && Modifications.modules[slot.m.grp].modifications.indexOf(modKey) != -1) {
|
||||
slotMods.push({ id: Modifications.modifications[modKey].id, value: slot.m.getModValue(modKey) });
|
||||
slotMods.push({ id: Modifications.modifications[modKey].id, value: slot.m.getModValue(modKey, true) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1563,6 +1590,7 @@ export default class Ship {
|
||||
}
|
||||
let oldModule = slot.m;
|
||||
slot.m = m;
|
||||
slot.enabled = true;
|
||||
slot.discountedCost = (m && m.cost) ? m.cost * this.moduleCostMultiplier : 0;
|
||||
this.updateStats(slot, m, oldModule, preventUpdate);
|
||||
|
||||
@@ -1604,6 +1632,25 @@ export default class Ship {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the lowest possible mass for this ship.
|
||||
* @param {Object} m Module override set (standard type => Module)
|
||||
* @return {number} The lowest possible mass for this ship
|
||||
*/
|
||||
calcLowestPossibleMass(m) {
|
||||
m = m || {};
|
||||
|
||||
let mass = this.hullMass;
|
||||
mass += m.pp ? m.pp.getMass() : ModuleUtils.standard(0, '2D').getMass();
|
||||
mass += m.th ? m.th.getMass() : ModuleUtils.standard(1, '2D').getMass();
|
||||
mass += m.fsd ? m.fsd.getMass() : ModuleUtils.standard(2, '2D').getMass();
|
||||
mass += m.ls ? m.ls.getMass() : ModuleUtils.standard(3, this.standard[3].maxClass + 'D').getMass() * 0.3; // Lightweight grade 4 mod reduces mass by up to 70%
|
||||
mass += m.pd ? m.pd.getMass() : ModuleUtils.standard(4, '1D').getMass();
|
||||
mass += m.s ? m.s.getMass() : ModuleUtils.standard(5, this.standard[5].maxClass + 'D').getMass() * 0.2; // Lightweight grade 5 mod reduces mass by up to 80%
|
||||
// Ignore fuel tank as it could be empty
|
||||
return mass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the lightest standard ModuleUtils unless otherwise specified
|
||||
* @param {Object} m Module override set (standard type => module ID)
|
||||
|
||||
@@ -31,20 +31,38 @@ export function multiPurpose(ship, shielded, bulkheadIndex) {
|
||||
* @param {Object} standardOpts [Optional] Standard module optional overrides
|
||||
*/
|
||||
export function trader(ship, shielded, standardOpts) {
|
||||
let sg = shielded ? ship.getAvailableModules().lightestShieldGenerator(ship.hullMass) : null;
|
||||
|
||||
for (let i = ship.internal.length; i--;) {
|
||||
let slot = ship.internal[i];
|
||||
if (sg && canMount(ship, slot, 'sg', sg.class)) {
|
||||
ship.use(slot, sg);
|
||||
sg = null;
|
||||
} else {
|
||||
if (canMount(ship, slot, 'cr')) {
|
||||
ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E'));
|
||||
let usedSlots = [],
|
||||
sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
ship.useLightestStandard(standardOpts);
|
||||
}
|
||||
|
||||
@@ -55,53 +73,102 @@ export function trader(ship, shielded, standardOpts) {
|
||||
*/
|
||||
export function explorer(ship, planetary) {
|
||||
let standardOpts = { ppRating: 'A' },
|
||||
intLength = ship.internal.length,
|
||||
heatSinkCount = 2, // Fit 2 heat sinks if possible
|
||||
afmUnitCount = 2, // Fit 2 AFM Units if possible
|
||||
shieldNext = planetary,
|
||||
usedSlots = [],
|
||||
sgSlot,
|
||||
fuelScoopSlot,
|
||||
pvhSlot,
|
||||
sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass);
|
||||
|
||||
if (!planetary) { // Non-planetary explorers don't really need to boost
|
||||
standardOpts.pd = '1D';
|
||||
}
|
||||
|
||||
ship.setSlotEnabled(ship.cargoHatch, false)
|
||||
.use(ship.internal[--intLength], ModuleUtils.internal('2f')); // Advanced Discovery Scanner
|
||||
// Cargo hatch can be disabled
|
||||
ship.setSlotEnabled(ship.cargoHatch, false);
|
||||
|
||||
if (!planetary || intLength > 3) { // Don't mount a DDS on planetary explorer ships too small for both a PVH and DDS
|
||||
ship.use(ship.internal[--intLength], ModuleUtils.internal('2i')); // Detailed Surface Scanner
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < intLength; i++) {
|
||||
let slot = ship.internal[i];
|
||||
let nextSlot = (i + 1) < intLength ? ship.internal[i + 1] : null;
|
||||
// Fit best possible Fuel Scoop
|
||||
if (!fuelScoopSlot && canMount(ship, slot, 'fs')) {
|
||||
fuelScoopSlot = slot;
|
||||
ship.use(slot, ModuleUtils.findInternal('fs', slot.maxClass, 'A'));
|
||||
ship.setSlotEnabled(slot, true);
|
||||
// Mount a Shield generator if possible AND an AFM Unit has been mounted already (Guarantees at least 1 AFM Unit)
|
||||
} else if (!sgSlot && shieldNext && canMount(ship, slot, 'sg', sg.class) && !canMount(ship, nextSlot, 'sg', sg.class)) {
|
||||
sgSlot = slot;
|
||||
shieldNext = false;
|
||||
ship.use(slot, sg);
|
||||
ship.setSlotEnabled(slot, true);
|
||||
// if planetary explorer and the next slot cannot mount a PVH or the next modul to mount is a SG
|
||||
} else if (planetary && !pvhSlot && canMount(ship, slot, 'pv') && (shieldNext || !canMount(ship, nextSlot, 'pv', 2))) {
|
||||
pvhSlot = slot;
|
||||
ship.use(slot, ModuleUtils.findInternal('pv', Math.min(Math.floor(pvhSlot.maxClass / 2) * 2, 6), 'G'));
|
||||
ship.setSlotEnabled(slot, false); // Disabled power for PVH
|
||||
shieldNext = !sgSlot;
|
||||
} else if (afmUnitCount > 0 && canMount(ship, slot, 'am')) {
|
||||
afmUnitCount--;
|
||||
ship.use(slot, ModuleUtils.findInternal('am', slot.maxClass, 'A'));
|
||||
ship.setSlotEnabled(slot, false); // Disabled power for AFM Unit
|
||||
shieldNext = !sgSlot;
|
||||
} else {
|
||||
ship.use(slot, null);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +182,7 @@ export function explorer(ship, planetary) {
|
||||
}
|
||||
}
|
||||
|
||||
if (sgSlot) {
|
||||
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);
|
||||
@@ -126,3 +193,104 @@ export function explorer(ship, planetary) {
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collector limpet controller if there are enough internals left
|
||||
let collectorLimpetsRequired = Math.max(ship.internal.filter(a => (!a.eligible) || a.eligible.cr).length - 6, 0);
|
||||
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));
|
||||
for (let i = 0; i < collectorInternals.length && 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, 'A'));
|
||||
usedSlots.push(collectorInternals[i]);
|
||||
collectorLimpetsRequired -= collectorInternals[i].m.maximum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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_OUTFITTING_TAB = 'outfittingTab';
|
||||
const LS_KEY_INSURANCE = 'insurance';
|
||||
const LS_KEY_SHIP_DISCOUNT = 'shipDiscount';
|
||||
const LS_KEY_MOD_DISCOUNT = 'moduleDiscount';
|
||||
@@ -98,6 +99,7 @@ export class Persist extends EventEmitter {
|
||||
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);
|
||||
this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1;
|
||||
this.tooltipsEnabled = tips === null ? true : tips;
|
||||
@@ -472,6 +474,22 @@ export class Persist extends EventEmitter {
|
||||
return this.costTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist selected outfitting tab
|
||||
* @param {string} tabName Cost tab name
|
||||
*/
|
||||
setOutfittingTab(tabName) {
|
||||
this.outfittingTab = tabName;
|
||||
_put(LS_KEY_OUTFITTING_TAB, tabName);
|
||||
}
|
||||
/**
|
||||
* Get the current outfitting tab
|
||||
* @return {string} the current outfitting tab
|
||||
*/
|
||||
getOutfittingTab() {
|
||||
return this.outfittingTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the last router state from local storage
|
||||
* @return {Object} state State object containing state name and params
|
||||
|
||||
229
src/app/utils/BlueprintFunctions.js
Normal file
229
src/app/utils/BlueprintFunctions.js
Normal file
@@ -0,0 +1,229 @@
|
||||
import React from 'react';
|
||||
import { Modifications } from 'coriolis-data/dist';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {Object} The react components
|
||||
*/
|
||||
export function blueprintTooltip(translate, blueprint, engineers, grp, m) {
|
||||
const effects = [];
|
||||
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];
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table width='100%'>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{translate('feature')}</td>
|
||||
<td>{translate('worst')}</td>
|
||||
{m ? <td>{translate('current')}</td> : null }
|
||||
<td>{translate('best')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{effects}
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 blueprint = JSON.parse(JSON.stringify(Modifications.blueprints[name]));
|
||||
if (module) {
|
||||
if (module.grp === 'bh' || module.grp === 'hr' || module.grp === 'sg' || module.grp === 'psg' || module.grp === 'bsg') {
|
||||
// Bulkheads, hull reinforcements and shield generators need to have their resistances altered by the base values
|
||||
for (const grade in blueprint.grades) {
|
||||
for (const feature in blueprint.grades[grade].features) {
|
||||
if (feature === 'explres') {
|
||||
blueprint.grades[grade].features[feature][0] *= (1 - module.explres);
|
||||
blueprint.grades[grade].features[feature][1] *= (1 - module.explres);
|
||||
}
|
||||
if (feature === 'kinres') {
|
||||
blueprint.grades[grade].features[feature][0] *= (1 - module.kinres);
|
||||
blueprint.grades[grade].features[feature][1] *= (1 - module.kinres);
|
||||
}
|
||||
if (feature === 'thermres') {
|
||||
blueprint.grades[grade].features[feature][0] *= (1 - module.thermres);
|
||||
blueprint.grades[grade].features[feature][1] *= (1 - module.thermres);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (module.grp === 'sb') {
|
||||
// Shield boosters are treated internally as straight modifiers, so rather than (for example)
|
||||
// being a 4% boost they are a 104% multiplier. We need to fix the values here so that they look
|
||||
// accurate as per the information in Elite
|
||||
for (const grade in blueprint.grades) {
|
||||
for (const feature in blueprint.grades[grade].features) {
|
||||
if (feature === 'shieldboost') {
|
||||
blueprint.grades[grade].features[feature][0] = ((1 + blueprint.grades[grade].features[feature][0]) * (1 + module.shieldboost) - 1) / module.shieldboost - 1;
|
||||
blueprint.grades[grade].features[feature][1] = ((1 + blueprint.grades[grade].features[feature][1]) * (1 + module.shieldboost) - 1) / module.shieldboost - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return blueprint;
|
||||
}
|
||||
@@ -2,6 +2,8 @@ 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'
|
||||
const SHIP_FD_NAME_TO_CORIOLIS_NAME = {
|
||||
@@ -129,9 +131,15 @@ export function shipFromJson(json) {
|
||||
let ship = new Ship(shipModel, shipTemplate.properties, shipTemplate.slots);
|
||||
ship.buildWith(null);
|
||||
|
||||
// Set the cargo hatch. We don't have any information on it so guess it's priority 5 and disabled
|
||||
ship.cargoHatch.enabled = false;
|
||||
ship.cargoHatch.priority = 4;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Add the bulkheads
|
||||
const armourJson = json.modules.Armour.module;
|
||||
@@ -243,7 +251,6 @@ export function shipFromJson(json) {
|
||||
let internalSlotNum = 1;
|
||||
let militarySlotNum = 1;
|
||||
for (let i in shipTemplate.slots.internal) {
|
||||
const internalClassNum = isNaN(shipTemplate.slots.internal[i]) ? shipTemplate.slots.internal[i].class : shipTemplate.slots.internal[i];
|
||||
const isMilitary = isNaN(shipTemplate.slots.internal[i]) ? shipTemplate.slots.internal[i].name == 'Military' : false;
|
||||
|
||||
// The internal slot might be a standard or a military slot. Military slots have a different naming system
|
||||
@@ -253,11 +260,15 @@ export function shipFromJson(json) {
|
||||
internalSlot = json.modules[internalName];
|
||||
militarySlotNum++;
|
||||
} else {
|
||||
// Slot numbers are not contiguous so handle skips.
|
||||
while (internalSlot === null && internalSlotNum < 99) {
|
||||
// Slot numbers are not contiguous so handle skips
|
||||
const internalName = 'Slot' + (internalSlotNum <= 9 ? '0' : '') + internalSlotNum + '_Size' + internalClassNum;
|
||||
if (json.modules[internalName]) {
|
||||
internalSlot = json.modules[internalName];
|
||||
// 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++;
|
||||
}
|
||||
@@ -303,11 +314,14 @@ function _addModifications(module, modifiers, blueprint, grade) {
|
||||
// This is an absolute number that acts as an override
|
||||
module.setModValue('burst', modifiers.modifiers[i].value * 100);
|
||||
} else if (modifiers.modifiers[i].name === 'mod_weapon_burst_rof') {
|
||||
// For some reason this is a non-normalised percentage (i.e. 12.23% is 12.23 value rather than 0.1223 as everywhere else), so fix that here
|
||||
// This is an absolute number that acts as an override
|
||||
module.setModValue('burstrof', modifiers.modifiers[i].value * 100);
|
||||
} else if (modifiers.modifiers[i].name === 'mod_weapon_falloffrange_from_range') {
|
||||
// Obtain the falloff value directly from the range
|
||||
module.setModValue('fallofffromrange', 1);
|
||||
} else if (modifiers.modifiers[i].name && modifiers.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.modifiers[i].name];
|
||||
} else {
|
||||
// Look up the modifiers to find what we need to do
|
||||
const modifierActions = Modifications.modifierActions[modifiers.modifiers[i].name];
|
||||
@@ -327,16 +341,11 @@ function _addModifications(module, modifiers, blueprint, grade) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note the special if present
|
||||
if (modifiers.modifiers[i].name && modifiers.modifiers[i].name.startsWith('special_')) {
|
||||
special = Modifications.specials[modifiers.modifiers[i].name];
|
||||
}
|
||||
}
|
||||
|
||||
// Add the blueprint ID, grade and special
|
||||
// Add the blueprint definition, grade and special
|
||||
if (blueprint) {
|
||||
module.blueprint = Object.assign({}, Modifications.blueprints[blueprint]);
|
||||
module.blueprint = getBlueprint(blueprint, module);
|
||||
if (grade) {
|
||||
module.blueprint.grade = Number(grade);
|
||||
}
|
||||
@@ -370,7 +379,7 @@ function _addModifications(module, modifiers, blueprint, grade) {
|
||||
|
||||
// Shield generator resistance is actually a damage modifier, so needs to be inverted.
|
||||
// In addition, the modification is based off the inherent resistance of the module
|
||||
if (module.isShieldGenerator()) {
|
||||
if (ModuleUtils.isShieldGenerator(module.grp)) {
|
||||
if (module.getModValue('explres')) {
|
||||
module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000);
|
||||
}
|
||||
@@ -383,15 +392,16 @@ function _addModifications(module, modifiers, blueprint, grade) {
|
||||
}
|
||||
|
||||
// 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', ((module.getModValue('explres') / 10000) * -1) * 10000);
|
||||
module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000);
|
||||
}
|
||||
if (module.getModValue('kinres')) {
|
||||
module.setModValue('kinres', ((module.getModValue('kinres') / 10000) * -1) * 10000);
|
||||
module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000);
|
||||
}
|
||||
if (module.getModValue('thermres')) {
|
||||
module.setModValue('thermres', ((module.getModValue('thermres') / 10000) * -1) * 10000);
|
||||
module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,4 +434,10 @@ function _addModifications(module, modifiers, blueprint, grade) {
|
||||
if (module.getModValue('rof')) {
|
||||
module.setModValue('rof', ((1 / (1 + module.getModValue('rof') / 10000)) - 1) * 10000);
|
||||
}
|
||||
|
||||
// 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,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { isShieldGenerator } from '../shipyard/ModuleUtils';
|
||||
import Module from '../shipyard/Module';
|
||||
import { Infinite } from '../components/SvgIcons';
|
||||
import Persist from '../stores/Persist';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
|
||||
/**
|
||||
* Determine if a slot on a ship can mount a module of a particular class and group
|
||||
@@ -143,28 +143,24 @@ export function diffDetails(language, m, mm) {
|
||||
let { formats, translate, units } = language;
|
||||
let propDiffs = [];
|
||||
|
||||
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) }>{mCost ? Math.round(mCost * (1 - Persist.getModuleDiscount())) : 0}{units.CR}</span></div>);
|
||||
// Module-specific items
|
||||
|
||||
let mMass = m.mass || 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>);
|
||||
if (m.grp === 'pp') {
|
||||
let mPowerGeneration = m.pgen || 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.MJ}</span></div>);
|
||||
} else {
|
||||
let mPowerUsage = m.power || 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.MJ}</span></div>);
|
||||
}
|
||||
|
||||
let mPowerGeneration = m.pgen || 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.MJ}</span></div>);
|
||||
|
||||
let mPowerUsage = m.power || 0;
|
||||
let mmPowerUsage = mm ? mm.getPowerUsage() : 0;
|
||||
if (mPowerUsage != mmPowerUsage) propDiffs.push(<div key='power'>{translate('power')}: <span className={diffClass(mPowerUsage, mmPowerUsage, true)}>{diff(formats.round, mPowerUsage, mmPowerUsage)}{units.MJ}</span></div>);
|
||||
|
||||
let mDps = m.damage * (m.rpshot || 1) * (m.rof || 1) || 0;
|
||||
let mDps = m.damage * (m.rpshot || 1) * (m.rof || 1);
|
||||
let mmDps = mm ? mm.getDps() || 0 : 0;
|
||||
if (mDps != mmDps) propDiffs.push(<div key='dps'>{translate('dps')}: <span className={diffClass(mmDps, mDps, true)}>{diff(formats.round, mDps, mmDps)}</span></div>);
|
||||
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 = isShieldGenerator(m.grp) || m.grp == 'sb';
|
||||
let mmAffectsShield = isShieldGenerator(mm ? mm.grp : null) || mm && mm.grp == 'sb';
|
||||
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;
|
||||
@@ -185,6 +181,20 @@ export function diffDetails(language, m, mm) {
|
||||
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.protection;
|
||||
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.hullreinforcement;
|
||||
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')}: `}
|
||||
@@ -206,6 +216,16 @@ export function diffDetails(language, m, mm) {
|
||||
</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) }>{mCost ? Math.round(mCost * (1 - Persist.getModuleDiscount())) : 0}{units.CR}</span></div>);
|
||||
|
||||
let mMass = m.mass || 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;
|
||||
@@ -223,5 +243,11 @@ export function diffDetails(language, m, mm) {
|
||||
}
|
||||
}
|
||||
|
||||
return propDiffs ? <div className='cap' style={{ whiteSpace: 'nowrap' }}>{propDiffs}</div> : null;
|
||||
let mIntegrity = m.integrity || 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,5 +1,5 @@
|
||||
{
|
||||
"name": "Coriolis.io",
|
||||
"name": "Coriolis EDCD Edition",
|
||||
"short_name": "Coriolis",
|
||||
"icons": [
|
||||
{
|
||||
@@ -27,7 +27,7 @@
|
||||
"density": "4.0"
|
||||
}
|
||||
],
|
||||
"start_url": "http:\/\/coriolis.io",
|
||||
"start_url": "https:\/\/edcd.coriolis.io",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html {%= o.htmlWebpackPlugin.options.appCache ? 'manifest=/' + o.htmlWebpackPlugin.options.appCache : '' %} >
|
||||
<html <%= htmlWebpackPlugin.options.appCache ? 'manifest=/' + htmlWebpackPlugin.options.appCache : '' %> >
|
||||
<head>
|
||||
<title>Coriolis EDCD Edition</title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="{%= o.htmlWebpackPlugin.files.css[0] %}">
|
||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[0] %>">
|
||||
<!-- Standard headers -->
|
||||
<meta name="description" content="A ship builder, outfitting and comparison tool for Elite Dangerous">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
@@ -22,24 +22,24 @@
|
||||
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
|
||||
<meta name="msapplication-config" content="/browserconfig.xml">
|
||||
<meta name="theme-color" content="#000000">
|
||||
|
||||
<script>
|
||||
window.CORIOLIS_GAPI_KEY = '<%- htmlWebpackPlugin.options.gapiKey %>';
|
||||
window.CORIOLIS_VERSION = '<%- htmlWebpackPlugin.options.version %>';
|
||||
window.CORIOLIS_DATE = '<%- new Date().toISOString().slice(0, 10) %>';
|
||||
</script>
|
||||
<% if (htmlWebpackPlugin.options.uaTracking) { %>
|
||||
<script>
|
||||
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
|
||||
ga('create', '<%- htmlWebpackPlugin.options.uaTracking %>', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
<script async src='https://www.google-analytics.com/analytics.js'></script>
|
||||
<% } %>
|
||||
</head>
|
||||
<body style="background-color:#000;">
|
||||
<section id="coriolis"></section>
|
||||
<script>
|
||||
window.CORIOLIS_GAPI_KEY = '{%= o.htmlWebpackPlugin.options.gapiKey %}';
|
||||
window.CORIOLIS_VERSION = '{%= o.htmlWebpackPlugin.options.version %}';
|
||||
window.CORIOLIS_DATE = '{%= new Date().toISOString().slice(0, 10) %}';
|
||||
</script>
|
||||
<script src="{%= o.htmlWebpackPlugin.files.chunks.lib.entry %}" charset="utf-8" crossorigin="anonymous"></script>
|
||||
<script src="{%= o.htmlWebpackPlugin.files.chunks.app.entry %}" charset="utf-8" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
{% if (o.htmlWebpackPlugin.options.uaTracking) { %}
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
ga('create', '{%= o.htmlWebpackPlugin.options.uaTracking %}', 'auto');
|
||||
{% } %}
|
||||
</script>
|
||||
<script src="<%= htmlWebpackPlugin.files.chunks.lib.entry %>" charset="utf-8" crossorigin="anonymous"></script>
|
||||
<script src="<%= htmlWebpackPlugin.files.chunks.app.entry %>" charset="utf-8" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,9 +16,14 @@
|
||||
@import 'tooltip';
|
||||
@import 'buttons';
|
||||
@import 'error';
|
||||
@import 'shipselector';
|
||||
@import 'sortable';
|
||||
@import 'loader';
|
||||
@import 'pips';
|
||||
@import 'boost';
|
||||
@import 'movement';
|
||||
@import 'shippicker';
|
||||
@import 'defence';
|
||||
@import 'offence';
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
@@ -42,7 +47,6 @@ div, a, li {
|
||||
#coriolis {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 48px;
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-sizing: border-box;
|
||||
|
||||
14
src/less/boost.less
Executable file
14
src/less/boost.less
Executable file
@@ -0,0 +1,14 @@
|
||||
#boost {
|
||||
button {
|
||||
font-size: 1.2em;
|
||||
background: @primary-bg;
|
||||
color: @primary;
|
||||
border: 1px solid @primary;
|
||||
&.selected {
|
||||
// Shown when button is selected
|
||||
background: @primary;
|
||||
color: @primary-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ svg {
|
||||
}
|
||||
|
||||
text {
|
||||
font-size: 0.8em;
|
||||
font-size: 1.2em;
|
||||
font-family: @fStandard;
|
||||
fill: @primary-disabled;
|
||||
}
|
||||
|
||||
@@ -43,6 +44,9 @@ svg {
|
||||
|
||||
.label, .text-tip {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.x {
|
||||
fill: @fg;
|
||||
}
|
||||
|
||||
@@ -55,7 +59,7 @@ svg {
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.75em;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.text-tip {
|
||||
|
||||
14
src/less/defence.less
Executable file
14
src/less/defence.less
Executable file
@@ -0,0 +1,14 @@
|
||||
#defence {
|
||||
table {
|
||||
background-color: @bgBlack;
|
||||
color: @primary;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.icon {
|
||||
stroke: @primary;
|
||||
stroke-width: 20;
|
||||
fill: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,7 @@ header {
|
||||
line-height: 3em;
|
||||
font-family: @fTitle;
|
||||
vertical-align: middle;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
.user-select-none();
|
||||
|
||||
14
src/less/movement.less
Normal file
14
src/less/movement.less
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
#movement {
|
||||
svg {
|
||||
width: 75%;
|
||||
height: 75%;
|
||||
stroke: @primary-disabled;
|
||||
fill: @primary-disabled;
|
||||
|
||||
text {
|
||||
stroke: @primary;
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/less/offence.less
Executable file
14
src/less/offence.less
Executable file
@@ -0,0 +1,14 @@
|
||||
#offence {
|
||||
table {
|
||||
background-color: @bgBlack;
|
||||
color: @fg;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.icon {
|
||||
stroke: @fg;
|
||||
stroke-width: 20;
|
||||
fill: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,9 +190,47 @@
|
||||
});
|
||||
}
|
||||
|
||||
&.quarter {
|
||||
width: 25%;
|
||||
|
||||
.tablet({
|
||||
td {
|
||||
line-height: 2em;
|
||||
}
|
||||
});
|
||||
|
||||
.smallTablet({
|
||||
width: 50% !important;
|
||||
});
|
||||
}
|
||||
|
||||
&.third {
|
||||
width: 33%;
|
||||
|
||||
.smallTablet({
|
||||
width: 50% !important;
|
||||
});
|
||||
}
|
||||
|
||||
&.twothirds {
|
||||
width: 67%;
|
||||
|
||||
.smallTablet({
|
||||
width: 100% !important;
|
||||
});
|
||||
}
|
||||
|
||||
&.threequarters {
|
||||
width: 75%;
|
||||
|
||||
.smallTablet({
|
||||
width: 100% !important;
|
||||
});
|
||||
}
|
||||
|
||||
&.full {
|
||||
width: 100%;
|
||||
|
||||
.smallTablet({
|
||||
width: 100% !important;
|
||||
});
|
||||
|
||||
33
src/less/pips.less
Executable file
33
src/less/pips.less
Executable file
@@ -0,0 +1,33 @@
|
||||
// The pips table - keep the background black
|
||||
#pips {
|
||||
|
||||
table {
|
||||
background-color: @bgBlack;
|
||||
color: @primary;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// A clickable entity in the pips table
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// A full pip
|
||||
.full {
|
||||
stroke: @primary;
|
||||
fill: @primary;
|
||||
}
|
||||
|
||||
// A half pip
|
||||
.half {
|
||||
stroke: @primary-disabled;
|
||||
fill: @primary-disabled;
|
||||
}
|
||||
|
||||
// An empty pip
|
||||
.empty {
|
||||
stroke: @primary-bg;
|
||||
fill: @primary-bg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ select {
|
||||
padding: 0.5em 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
max-height: 400px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
z-index: 0;
|
||||
@@ -55,10 +55,23 @@ select {
|
||||
background-color: @primary-disabled;
|
||||
}
|
||||
|
||||
.select-category {
|
||||
white-space: nowrap;
|
||||
line-height: 2em;
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
margin: 0.5em 0;
|
||||
padding-left: 5px;
|
||||
border-top: 3px solid @primary-disabled;
|
||||
border-bottom: 3px solid @primary-disabled;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.select-group {
|
||||
white-space: nowrap;
|
||||
line-height: 1.5em;
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
margin: 0.5em 0;
|
||||
padding-left: 5px;
|
||||
border-top: 1px solid @primary-disabled;
|
||||
@@ -145,15 +158,6 @@ select {
|
||||
width: 4.5em;
|
||||
padding: 0.1em 0.2em;
|
||||
}
|
||||
ul {
|
||||
width: 17em;
|
||||
}
|
||||
}
|
||||
|
||||
&.standard {
|
||||
ul {
|
||||
width: 16.25em;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
.shipselector {
|
||||
.shippicker {
|
||||
background-color: @bgBlack;
|
||||
margin: 0;
|
||||
padding: 0 0 0 1em;
|
||||
height: 3em;
|
||||
line-height: 3em;
|
||||
font-family: @fTitle;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
.user-select-none();
|
||||
|
||||
.menu {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
cursor: default;
|
||||
|
||||
&.r {
|
||||
@@ -28,10 +26,29 @@
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
padding : 0 1em;
|
||||
cursor: pointer;
|
||||
color: @warning;
|
||||
text-transform: uppercase;
|
||||
|
||||
&.disabled {
|
||||
color: @warning-disabled;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: @bgBlack;
|
||||
}
|
||||
|
||||
.menu-item-label {
|
||||
margin-left: 1em;
|
||||
|
||||
.smallTablet({
|
||||
display: none;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
@@ -44,7 +61,7 @@
|
||||
background-color: @bgBlack;
|
||||
font-size: 0.9em;
|
||||
overflow-y: auto;
|
||||
z-index: 0;
|
||||
z-index: 1;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
max-height: 500px;
|
||||
|
||||
@@ -75,7 +92,6 @@
|
||||
border-bottom: 1px solid @bg;
|
||||
});
|
||||
|
||||
|
||||
.tablet({
|
||||
li, a {
|
||||
padding: 0.3em 0;
|
||||
@@ -83,37 +99,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
.dbl {
|
||||
-webkit-column-count: 2; /* Chrome, Safari, Opera */
|
||||
-moz-column-count: 2; /* Firefox */
|
||||
column-count: 2;
|
||||
ul {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.smallTablet({
|
||||
-webkit-column-count: 3; /* Chrome, Safari, Opera */
|
||||
-moz-column-count: 3; /* Firefox */
|
||||
column-count: 3;
|
||||
|
||||
ul {
|
||||
min-width: 20em;
|
||||
}
|
||||
});
|
||||
|
||||
.largePhone({
|
||||
-webkit-column-count: 2; /* Chrome, Safari, Opera */
|
||||
-moz-column-count: 2; /* Firefox */
|
||||
column-count: 2;
|
||||
});
|
||||
|
||||
.smallPhone({
|
||||
-webkit-column-count: 1; /* Chrome, Safari, Opera */
|
||||
-moz-column-count: 1; /* Firefox */
|
||||
column-count: 1;
|
||||
});
|
||||
}
|
||||
|
||||
.quad {
|
||||
-webkit-column-count: 4; /* Chrome, Safari, Opera */
|
||||
-moz-column-count: 4; /* Firefox */
|
||||
@@ -151,6 +136,7 @@
|
||||
margin: 0 0 0.5em;
|
||||
padding: 0;
|
||||
line-height: 1.3em;
|
||||
color: @fg;
|
||||
}
|
||||
|
||||
li {
|
||||
@@ -158,20 +144,10 @@
|
||||
list-style: none;
|
||||
margin-left: 1em;
|
||||
line-height: 1.1em;
|
||||
}
|
||||
|
||||
a {
|
||||
vertical-align: middle;
|
||||
color: @warning;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:visited {
|
||||
color: @warning;
|
||||
}
|
||||
.no-touch &:hover {
|
||||
color: teal;
|
||||
}
|
||||
&.active {
|
||||
&.selected {
|
||||
color: @primary;
|
||||
}
|
||||
}
|
||||
@@ -182,6 +158,7 @@
|
||||
}
|
||||
|
||||
.no-wrap {
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Coriolis: Migrate to HTTPS</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
|
||||
function fromJsonToObject(str) {
|
||||
try {
|
||||
var o = JSON.parse(str);
|
||||
if (o instanceof Object && !(o instanceof Array)) {
|
||||
return o;
|
||||
}
|
||||
} catch (e) { }
|
||||
return {};
|
||||
}
|
||||
|
||||
function listener(event) {
|
||||
var origin = event.origin || event.originalEvent.origin;
|
||||
var source = event.source;
|
||||
var data = event.message;
|
||||
|
||||
if (window.location.href.indexOf('coriolis') != -1 && origin !== "http://coriolis.io") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var builds = fromJsonToObject(localStorage.getItem('builds'));
|
||||
var comparisons = fromJsonToObject(localStorage.getItem('comparisons'));
|
||||
|
||||
// merge builds
|
||||
if (typeof data.builds == 'object') {
|
||||
|
||||
for (var bName in data.builds) {
|
||||
// Build existing in http and does not existing in HTTPS
|
||||
if (data.builds.hasOwnProperty(bName) && !builds[bName]) {
|
||||
build[bName] = data.builds[bName];
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem('builds', JSON.stringify(builds));
|
||||
}
|
||||
// merge comparisons
|
||||
if (typeof data.comparisons == 'object') {
|
||||
|
||||
for (var comp in data.comparisons) {
|
||||
// Comparison existing in http and does not existing in HTTPS
|
||||
if (data.comparisons.hasOwnProperty(comp) && !data.comparisons[comp]) {
|
||||
comparisons[comp] = data.comparisons[comp];
|
||||
}
|
||||
}
|
||||
localStorage.setItem('comparisons', JSON.stringify(comparisons));
|
||||
}
|
||||
|
||||
source.postMessage({
|
||||
success: true,
|
||||
buildCount: Object.keys(builds).length,
|
||||
comparisonCount: Object.keys(comparisons).length
|
||||
}, origin);
|
||||
|
||||
} catch (e) {
|
||||
source.postMessage({ success: false, error: e }, origin);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (window.addEventListener){
|
||||
window.addEventListener("message", listener, false);
|
||||
} else {
|
||||
window.attachEvent("onmessage", listener);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"id": "http://cdn.coriolis.io/schemas/ship-loadout/1.json#",
|
||||
"id": "https://coriolis.edcd.io/schemas/ship-loadout/1.json#",
|
||||
"title": "Ship Loadout",
|
||||
"type": "object",
|
||||
"description": "The details for a specific ship build/loadout. DEPRECATED in favor of Version 3",
|
||||
@@ -278,4 +278,4 @@
|
||||
"standardRatings": { "enum": ["A", "B", "C", "D", "E"] },
|
||||
"allRatings": { "enum": ["A", "B", "C", "D", "E", "F", "I" ] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"id": "http://cdn.coriolis.io/schemas/ship-loadout/2.json#",
|
||||
"id": "https://coriolis.edcd.io/schemas/ship-loadout/2.json#",
|
||||
"title": "Ship Loadout",
|
||||
"type": "object",
|
||||
"description": "The details for a specific ship build/loadout. DEPRECATED in favor of Version 3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"id": "http://cdn.coriolis.io/schemas/ship-loadout/3.json#",
|
||||
"id": "https://coriolis.edcd.io/schemas/ship-loadout/3.json#",
|
||||
"title": "Ship Loadout",
|
||||
"type": "object",
|
||||
"description": "The details for a specific ship build/loadout",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"id": "http://cdn.coriolis.io/schemas/ship-loadout/4.json#",
|
||||
"id": "https://coriolis.edcd.io/schemas/ship-loadout/4.json#",
|
||||
"title": "Ship Loadout",
|
||||
"type": "object",
|
||||
"description": "The details for a specific ship build/loadout",
|
||||
@@ -230,16 +230,16 @@
|
||||
},
|
||||
"armour": {
|
||||
"description": "Sum of base armour + any hull reinforcements",
|
||||
"type": "integer",
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
},
|
||||
"armourAdded":{
|
||||
"description": "Armour added through Hull reinforcement",
|
||||
"type": "integer",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"baseShieldStrength": {
|
||||
"type": "integer",
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
},
|
||||
"baseArmour": {
|
||||
|
||||
@@ -27,7 +27,7 @@ module.exports = {
|
||||
},
|
||||
resolve: {
|
||||
// When requiring, you don't need to add these extensions
|
||||
extensions: ['', '.js', '.jsx', '.json', '.less']
|
||||
extensions: ['.js', '.jsx', '.json', '.less']
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'build'),
|
||||
@@ -36,30 +36,34 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
new CopyDirPlugin(path.join(__dirname, 'src/.htaccess'), ''),
|
||||
new webpack.optimize.CommonsChunkPlugin('lib', 'lib.js'),
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'lib',
|
||||
filename: 'lib.js'
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
inject: false,
|
||||
template: path.join(__dirname, "src/index.html"),
|
||||
template: path.join(__dirname, "src/index.ejs"),
|
||||
version: pkgJson.version,
|
||||
gapiKey: process.env.CORIOLIS_GAPI_KEY || '',
|
||||
}),
|
||||
new ExtractTextPlugin('app.css', {
|
||||
new ExtractTextPlugin({
|
||||
filename: 'app.css',
|
||||
disable: false,
|
||||
allChunks: true
|
||||
}),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NoErrorsPlugin()
|
||||
new webpack.NoEmitOnErrorsPlugin()
|
||||
],
|
||||
module: {
|
||||
loaders: [
|
||||
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader','css-loader') },
|
||||
{ test: /\.less$/, loader: ExtractTextPlugin.extract('style-loader','css-loader!less-loader') },
|
||||
{ test: /\.(js|jsx)$/, loaders: [ 'babel' ], include: path.join(__dirname, 'src') },
|
||||
{ test: /\.json$/, loader: 'json-loader' },
|
||||
{ test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/font-woff' },
|
||||
{ test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/font-woff' },
|
||||
{ test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/octet-stream' },
|
||||
{ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file' },
|
||||
{ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=image/svg+xml' }
|
||||
rules: [
|
||||
{ test: /\.css$/, loader: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader'}) },
|
||||
{ test: /\.less$/, loader: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader!less-loader'}) },
|
||||
{ test: /\.(js|jsx)$/, loaders: [ 'babel-loader' ], include: path.join(__dirname, 'src') },
|
||||
{ test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/font-woff' },
|
||||
{ test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/font-woff' },
|
||||
{ test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/octet-stream' },
|
||||
{ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader' },
|
||||
{ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=image/svg+xml' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,12 +6,6 @@ var HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
var ExtractTextPlugin = require("extract-text-webpack-plugin");
|
||||
var AppCachePlugin = require('appcache-webpack-plugin');
|
||||
|
||||
var node_modules_dir = path.resolve(__dirname, 'node_modules');
|
||||
var d3Path = path.resolve(node_modules_dir, 'd3/d3.min.js');
|
||||
var reactPath = path.resolve(node_modules_dir, 'react/dist/react.min.js');
|
||||
var reactDomPath = path.resolve(node_modules_dir, 'react-dom/dist/react-dom.min.js');
|
||||
var lzStringPath = path.resolve(node_modules_dir, 'lz-string/libs/lz-string.min.js');
|
||||
|
||||
function CopyDirPlugin(source, destination) {
|
||||
this.source = source;
|
||||
this.destination = destination;
|
||||
@@ -26,16 +20,10 @@ CopyDirPlugin.prototype.apply = function(compiler) {
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: ['babel-polyfill', path.resolve(__dirname, 'src/app/index')],
|
||||
lib: ['babel-polyfill', 'd3', 'react', 'react-dom', 'classnames', 'fbemitter', 'lz-string']
|
||||
lib: ['d3', 'react', 'react-dom', 'classnames', 'fbemitter', 'lz-string']
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['', '.js', '.jsx', '.json', '.less'],
|
||||
alias: {
|
||||
'd3': d3Path,
|
||||
'react': reactPath,
|
||||
'react-dom': reactDomPath,
|
||||
'lz-string': lzStringPath
|
||||
},
|
||||
extensions: ['.js', '.jsx', '.json', '.less']
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'build'),
|
||||
@@ -45,12 +33,12 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
mangle: {
|
||||
except: []
|
||||
},
|
||||
'screw-ie8': true
|
||||
}),
|
||||
new webpack.optimize.CommonsChunkPlugin('lib', 'lib.[chunkhash:6].js'),
|
||||
//new webpack.optimize.CommonsChunkPlugin({
|
||||
// name: 'lib',
|
||||
// filename: 'lib.[chunkhash:6].js'
|
||||
//}),
|
||||
new HtmlWebpackPlugin({
|
||||
inject: false,
|
||||
appCache: 'coriolis.appcache',
|
||||
@@ -64,17 +52,18 @@ module.exports = {
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true
|
||||
},
|
||||
template: path.join(__dirname, "src/index.html"),
|
||||
template: path.join(__dirname, "src/index.ejs"),
|
||||
uaTracking: process.env.CORIOLIS_UA_TRACKING || '',
|
||||
gapiKey: process.env.CORIOLIS_GAPI_KEY || '',
|
||||
version: pkgJson.version
|
||||
}),
|
||||
new ExtractTextPlugin('[contenthash:6].css', {
|
||||
new ExtractTextPlugin({
|
||||
filename: '[contenthash:6].css',
|
||||
disable: false,
|
||||
allChunks: true
|
||||
}),
|
||||
new CopyDirPlugin(path.join(__dirname, 'src/schemas'), 'schemas'),
|
||||
new CopyDirPlugin(path.join(__dirname, 'src/images/logo/*'), ''),
|
||||
new CopyDirPlugin(path.join(__dirname, 'src/migrate.html'), ''),
|
||||
new CopyDirPlugin(path.join(__dirname, 'src/.htaccess'), ''),
|
||||
new AppCachePlugin({
|
||||
network: ['*'],
|
||||
@@ -84,22 +73,15 @@ module.exports = {
|
||||
})
|
||||
],
|
||||
module: {
|
||||
noParse: [d3Path, reactPath, lzStringPath],
|
||||
loaders: [
|
||||
// Expose non-parsed globally scoped libs
|
||||
{ test: reactPath, loader: "expose?React" },
|
||||
{ test: d3Path, loader: "expose?d3" },
|
||||
{ test: lzStringPath, loader: "expose?LZString" },
|
||||
|
||||
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader','css-loader') },
|
||||
{ test: /\.less$/, loader: ExtractTextPlugin.extract('style-loader','css-loader!less-loader') },
|
||||
{ test: /\.(js|jsx)$/, loaders: [ 'babel' ], include: path.join(__dirname, 'src') },
|
||||
{ test: /\.json$/, loader: 'json-loader' },
|
||||
{ test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/font-woff' },
|
||||
{ test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/font-woff' },
|
||||
{ test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/octet-stream' },
|
||||
{ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file' },
|
||||
{ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=image/svg+xml' }
|
||||
rules: [
|
||||
{ test: /\.css$/, loader: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader'}) },
|
||||
{ test: /\.less$/, loader: ExtractTextPlugin.extract({ fallback: 'style-loader',use: 'css-loader!less-loader'}) },
|
||||
{ test: /\.(js|jsx)$/, loaders: [ 'babel-loader' ], include: path.join(__dirname, 'src') },
|
||||
{ test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/font-woff' },
|
||||
{ test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/font-woff' },
|
||||
{ test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/octet-stream' },
|
||||
{ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader' },
|
||||
{ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=image/svg+xml' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user