mirror of
https://github.com/EDCD/coriolis.git
synced 2025-12-11 08:43:02 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9abb4f6d05 | ||
|
|
38038fcc32 | ||
|
|
9fa740f54e | ||
|
|
d37d69c3a7 | ||
|
|
78d8779641 | ||
|
|
352023f0fb | ||
|
|
3ea194d43e | ||
|
|
c6269192f0 | ||
|
|
841e6c3348 | ||
|
|
0febc581d1 | ||
|
|
dc6e398526 | ||
|
|
cb24c1fc69 | ||
|
|
90ad9de831 | ||
|
|
9360b1d574 | ||
|
|
08d2573d1f | ||
|
|
427b9af7de | ||
|
|
318d06d9f9 | ||
|
|
1f3c3726f2 | ||
|
|
71beda3a6c | ||
|
|
1d6644b531 |
77
.dockerignore
Normal file
77
.dockerignore
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
### Node template
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# nuxt.js build output
|
||||||
|
.nuxt
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless
|
||||||
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ env
|
|||||||
.project
|
.project
|
||||||
.vscode/
|
.vscode/
|
||||||
docs/
|
docs/
|
||||||
|
package-lock.json
|
||||||
|
|||||||
16
.travis.yml
Normal file
16
.travis.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
language: node_js
|
||||||
|
notifications:
|
||||||
|
email: false
|
||||||
|
sudo: false
|
||||||
|
node_js:
|
||||||
|
- "4.8.1"
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- node_modules
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- git clone https://github.com/EDCD/coriolis-data.git ../coriolis-data
|
||||||
|
|
||||||
|
script:
|
||||||
|
- npm run lint
|
||||||
|
- npm test
|
||||||
34
Dockerfile
Normal file
34
Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
### STAGE 1: Build ###
|
||||||
|
FROM node:9.11.1-alpine as builder
|
||||||
|
ARG branch=develop
|
||||||
|
ENV BRANCH=$branch
|
||||||
|
WORKDIR /src/app
|
||||||
|
RUN mkdir -p /src/app/coriolis
|
||||||
|
RUN mkdir -p /src/app/coriolis-data
|
||||||
|
|
||||||
|
RUN apk add --update git
|
||||||
|
|
||||||
|
COPY . /src/app/coriolis
|
||||||
|
|
||||||
|
RUN npm i -g npm
|
||||||
|
|
||||||
|
# Set up coriolis-data
|
||||||
|
WORKDIR /src/app/coriolis-data
|
||||||
|
RUN git clone https://github.com/EDCD/coriolis-data.git .
|
||||||
|
RUN git checkout ${BRANCH}
|
||||||
|
RUN npm install --no-package-lock
|
||||||
|
RUN npm start
|
||||||
|
|
||||||
|
# Set up coriolis
|
||||||
|
WORKDIR /src/app/coriolis
|
||||||
|
RUN npm install --no-package-lock
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
|
### STAGE 2: Production Environment ###
|
||||||
|
FROM fholzer/nginx-brotli as web
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
COPY --from=builder /src/app/coriolis/build /usr/share/nginx/html
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-c", "/etc/nginx/nginx.conf", "-g", "daemon off;"]
|
||||||
24
LICENSE.md
24
LICENSE.md
@@ -1,24 +0,0 @@
|
|||||||
All Data and [associated JSON](https://github.com/EDCD/coriolis-data) files are intellectual property and copyright of Frontier Developments plc ('Frontier', 'Frontier Developments') and are subject to their
|
|
||||||
[terms and conditions](https://www.frontierstore.net/terms-and-conditions/).
|
|
||||||
|
|
||||||
The code (Javascript, CSS, HTML, and SVG files only) specificially for Coriolis.io is released under the MIT License.
|
|
||||||
|
|
||||||
Copyright (c) 2015 Coriolis.io, Colin McLeod
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software (Javascript, CSS, HTML, and SVG files only), and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
62
README.md
62
README.md
@@ -1,4 +1,4 @@
|
|||||||
[](https://discord.gg/0uwCh6R62aPRjk9w)
|
 [](https://travis-ci.org/EDCD/coriolis) [](https://discord.gg/0uwCh6R62aPRjk9w)
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
@@ -8,41 +8,51 @@ Coriolis was created using assets and imagery from Elite: Dangerous, with the pe
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
- [Submit issues](https://github.com/EDCD/coriolis/issues)
|
Please [submit issues](https://github.com/EDCD/coriolis/issues), or better yet [pull requests](https://github.com/EDCD/coriolis/pulls) for any corrections or additions to the database or the code.
|
||||||
- [Submit pull requests](https://github.com/EDCD/coriolis/pulls) targetting `develop` branch
|
|
||||||
- Chat to us on [Discord](https://discord.gg/0uwCh6R62aPRjk9w)!
|
### Translations
|
||||||
|
|
||||||
|
Please use the OneSky translation site to suggest new translations: http://edcd-coriolis.oneskyapp.com
|
||||||
|
These will be merged regularly by the project manager.
|
||||||
|
|
||||||
|
### Feature Requests, Suggestions & Bugs
|
||||||
|
|
||||||
|
Chat to us on [Discord](https://discord.gg/0uwCh6R62aPRjk9w)!
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
To get a local instance of coriolis running, perform the following steps in a shell:
|
See the [Developer's Guide](https://github.com/EDCD/coriolis/wiki/Developing-for-Coriolis) in the wiki.
|
||||||
```sh
|
|
||||||
> git clone https://github.com/EDCD/coriolis.git
|
|
||||||
> git clone https://github.com/EDCD/coriolis-data.git
|
|
||||||
> cd ./coriolis-data
|
|
||||||
> npm install
|
|
||||||
> cd ../coriolis
|
|
||||||
> npm install
|
|
||||||
> npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
You will then have a development server running on `localhost:3300`.
|
Also see [the documentation site.](https://coriolis.willb.info/)
|
||||||
|
|
||||||
### Ship and Module Database
|
### Ship and Module Database
|
||||||
|
|
||||||
See the [Data wiki](https://github.com/cmmcleod/coriolis-data/wiki) for details on structure, etc.
|
See the [Data wiki](https://github.com/cmmcleod/coriolis-data/wiki) for details on structure, etc.
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
Follow the steps for [Development](#development) as above, but instead
|
## License
|
||||||
of `npm start` you'll want to:
|
|
||||||
|
|
||||||
```sh
|
All Data and [associated JSON](https://github.com/EDCD/coriolis-data) files are intellectual property and copyright of Frontier Developments plc ('Frontier', 'Frontier Developments') and are subject to their
|
||||||
> npm run build
|
[terms and conditions](https://www.frontierstore.net/terms-and-conditions/).
|
||||||
```
|
|
||||||
|
|
||||||
this will result in a `build/` directory being created containing all the necessary files.
|
The code (Javascript, CSS, HTML, and SVG files only) specificially for Coriolis.io is released under the MIT License.
|
||||||
|
|
||||||
After this you need to serve the files in some manner.
|
Copyright (c) 2015 Coriolis.io, Colin McLeod
|
||||||
Either configure your webserver to make the actual `build/` directory
|
|
||||||
visible on the web, or alternatively copy it to somewhere to serve it
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
from.
|
of this software (Javascript, CSS, HTML, and SVG files only), and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|||||||
30
__tests__/fixtures/agility-data.json
Normal file
30
__tests__/fixtures/agility-data.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"adder": {
|
||||||
|
"t3": {"speed": 205, "boost": 298, "pitch": 35.37, "roll": 93.09, "yaw": 13.03},
|
||||||
|
"t2": {"speed": 209, "boost": 304, "pitch": 36.06, "roll": 94.90, "yaw": 13.29},
|
||||||
|
"t1": {"speed": 213, "boost": 310, "pitch": 36.80, "roll": 96.84, "yaw": 13.56},
|
||||||
|
"t0": {"speed": 218, "boost": 317, "pitch": 37.70, "roll": 99.20, "yaw": 13.89},
|
||||||
|
"t9": {"speed": 220, "boost": 321, "pitch": 38.08, "roll": 100.21, "yaw": 14.03},
|
||||||
|
"t8": {"speed": 225, "boost": 327, "pitch": 38.86, "roll": 102.26, "yaw": 14.32},
|
||||||
|
"t7": {"speed": 230, "boost": 334, "pitch": 39.69, "roll": 104.44, "yaw": 14.62},
|
||||||
|
"t6": {"speed": 234, "boost": 340, "pitch": 40.41, "roll": 106.34, "yaw": 14.89},
|
||||||
|
"t5": {"speed": 242, "boost": 351, "pitch": 41.71, "roll": 109.78, "yaw": 15.37}
|
||||||
|
},
|
||||||
|
"eagle": {
|
||||||
|
"t2": {"speed": 223, "boost": 325, "pitch": 46.45, "roll": 111.48, "yaw": 16.72},
|
||||||
|
"t1": {"speed": 229, "boost": 334, "pitch": 47.69, "roll": 114.46, "yaw": 17.17},
|
||||||
|
"t0": {"speed": 235, "boost": 343, "pitch": 49.00, "roll": 117.60, "yaw": 17.64},
|
||||||
|
"t9": {"speed": 239, "boost": 349, "pitch": 49.80, "roll": 119.53, "yaw": 17.93},
|
||||||
|
"t8": {"speed": 243, "boost": 355, "pitch": 50.70, "roll": 121.69, "yaw": 18.25},
|
||||||
|
"t7": {"speed": 248, "boost": 361, "pitch": 51.62, "roll": 123.89, "yaw": 18.58},
|
||||||
|
"t6": {"speed": 252, "boost": 367, "pitch": 52.46, "roll": 125.91, "yaw": 18.89},
|
||||||
|
"t5": {"speed": 259, "boost": 378, "pitch": 53.99, "roll": 129.56, "yaw": 19.43}
|
||||||
|
},
|
||||||
|
"hauler": {
|
||||||
|
"t4": {"speed": 203, "boost": 305, "pitch": 36.61, "roll": 101.71, "yaw": 14.24},
|
||||||
|
"t3": {"speed": 209, "boost": 314, "pitch": 37.63, "roll": 104.54, "yaw": 14.64},
|
||||||
|
"t2": {"speed": 216, "boost": 324, "pitch": 38.89, "roll": 108.03, "yaw": 15.12},
|
||||||
|
"t1": {"speed": 222, "boost": 333, "pitch": 39.97, "roll": 111.02, "yaw": 15.54},
|
||||||
|
"t0": {"speed": 232, "boost": 348, "pitch": 41.76, "roll": 116.00, "yaw": 16.24}
|
||||||
|
}
|
||||||
|
}
|
||||||
289
__tests__/fixtures/anaconda-test-detailed-export-v3.json
Normal file
289
__tests__/fixtures/anaconda-test-detailed-export-v3.json
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://cdn.coriolis.io/schemas/ship-loadout/3.json#",
|
||||||
|
"name": "Test My Ship",
|
||||||
|
"ship": "Anaconda",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Coriolis.io",
|
||||||
|
"url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA?bn=Test%20My%20Ship",
|
||||||
|
"old-code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA",
|
||||||
|
"code": "4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA",
|
||||||
|
"shipId": "anaconda"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"components": {
|
||||||
|
"standard": {
|
||||||
|
"bulkheads": "Reactive Surface Composite",
|
||||||
|
"cargoHatch": {
|
||||||
|
"enabled": false,
|
||||||
|
"priority": 5
|
||||||
|
},
|
||||||
|
"powerPlant": {
|
||||||
|
"class": 8,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"thrusters": {
|
||||||
|
"class": 6,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"frameShiftDrive": {
|
||||||
|
"class": 6,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 3
|
||||||
|
},
|
||||||
|
"lifeSupport": {
|
||||||
|
"class": 5,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"powerDistributor": {
|
||||||
|
"class": 8,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"sensors": {
|
||||||
|
"class": 8,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"fuelTank": {
|
||||||
|
"class": 5,
|
||||||
|
"rating": "C",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hardpoints": [
|
||||||
|
{
|
||||||
|
"class": 4,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Plasma Accelerator",
|
||||||
|
"mount": "Fixed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 3,
|
||||||
|
"rating": "D",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Beam Laser",
|
||||||
|
"mount": "Turret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 3,
|
||||||
|
"rating": "D",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Beam Laser",
|
||||||
|
"mount": "Turret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 3,
|
||||||
|
"rating": "D",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Beam Laser",
|
||||||
|
"mount": "Turret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 2,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Cannon",
|
||||||
|
"mount": "Turret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 2,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Cannon",
|
||||||
|
"mount": "Turret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 1,
|
||||||
|
"rating": "F",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Beam Laser",
|
||||||
|
"mount": "Turret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 1,
|
||||||
|
"rating": "F",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Beam Laser",
|
||||||
|
"mount": "Turret"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"utility": [
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Shield Booster"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Shield Booster"
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "C",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Kill Warrant Scanner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "C",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Cargo Scanner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "F",
|
||||||
|
"enabled": false,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Countermeasure",
|
||||||
|
"name": "Electronic Countermeasure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "I",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Countermeasure",
|
||||||
|
"name": "Chaff Launcher"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "I",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Countermeasure",
|
||||||
|
"name": "Point Defence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"internal": [
|
||||||
|
{
|
||||||
|
"class": 7,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Shield Generator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 6,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Shield Cell Bank"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 6,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Cargo Rack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 5,
|
||||||
|
"rating": "D",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Hull Reinforcement Package"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 5,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Cargo Rack"
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"class": 4,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Cargo Rack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 4,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Cargo Rack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 4,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 3,
|
||||||
|
"group": "Fuel Scoop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 2,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 3,
|
||||||
|
"group": "Frame Shift Drive Interdictor"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"class": 3,
|
||||||
|
"hullCost": 141889930,
|
||||||
|
"speed": 180,
|
||||||
|
"topSpeed": 186.5,
|
||||||
|
"boost": 240,
|
||||||
|
"boostEnergy": 29,
|
||||||
|
"topBoost": 248.66,
|
||||||
|
"agility": 2,
|
||||||
|
"baseShieldStrength": 350,
|
||||||
|
"baseArmour": 945,
|
||||||
|
"hullMass": 400,
|
||||||
|
"masslock": 23,
|
||||||
|
"pipSpeed": 0.14,
|
||||||
|
"moduleCostMultiplier": 1,
|
||||||
|
"fuelCapacity": 32,
|
||||||
|
"cargoCapacity": 128,
|
||||||
|
"ladenMass": 1339.2,
|
||||||
|
"armour": 2228,
|
||||||
|
"armourAdded": 390,
|
||||||
|
"armourMultiplier": 1.95,
|
||||||
|
"shieldMultiplier": 1.4,
|
||||||
|
"totalCost": 882362060,
|
||||||
|
"unladenMass": 1179.2,
|
||||||
|
"totalDps": 29,
|
||||||
|
"powerAvailable": 36,
|
||||||
|
"powerRetracted": 23.33,
|
||||||
|
"powerDeployed": 34.76,
|
||||||
|
"unladenRange": 18.49,
|
||||||
|
"fullTankRange": 18.12,
|
||||||
|
"ladenRange": 16.39,
|
||||||
|
"unladenFastestRange": 73.21,
|
||||||
|
"ladenFastestRange": 66.15,
|
||||||
|
"maxJumpCount": 4,
|
||||||
|
"shieldStrength": 833
|
||||||
|
}
|
||||||
|
}
|
||||||
325
__tests__/fixtures/anaconda-test-detailed-export-v4.json
Normal file
325
__tests__/fixtures/anaconda-test-detailed-export-v4.json
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://cdn.coriolis.io/schemas/ship-loadout/4.json#",
|
||||||
|
"name": "Test My Ship",
|
||||||
|
"ship": "Anaconda",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Coriolis.io",
|
||||||
|
"url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==?bn=Test%20My%20Ship",
|
||||||
|
"old-code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==",
|
||||||
|
"code": "4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==",
|
||||||
|
"shipId": "anaconda"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"components": {
|
||||||
|
"standard": {
|
||||||
|
"bulkheads": "Reactive Surface Composite",
|
||||||
|
"cargoHatch": {
|
||||||
|
"enabled": false,
|
||||||
|
"priority": 5
|
||||||
|
},
|
||||||
|
"powerPlant": {
|
||||||
|
"class": 8,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"modifications": {
|
||||||
|
"pgen": 1000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"thrusters": {
|
||||||
|
"class": 6,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"frameShiftDrive": {
|
||||||
|
"class": 6,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 3
|
||||||
|
},
|
||||||
|
"lifeSupport": {
|
||||||
|
"class": 5,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"powerDistributor": {
|
||||||
|
"class": 8,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"sensors": {
|
||||||
|
"class": 8,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"fuelTank": {
|
||||||
|
"class": 5,
|
||||||
|
"rating": "C",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hardpoints": [
|
||||||
|
{
|
||||||
|
"class": 4,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Plasma Accelerator",
|
||||||
|
"mount": "Fixed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 3,
|
||||||
|
"rating": "D",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Beam Laser",
|
||||||
|
"mount": "Turret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 3,
|
||||||
|
"rating": "D",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Beam Laser",
|
||||||
|
"mount": "Turret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 3,
|
||||||
|
"rating": "D",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Beam Laser",
|
||||||
|
"mount": "Turret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 2,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Cannon",
|
||||||
|
"mount": "Turret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 2,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Cannon",
|
||||||
|
"mount": "Turret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 1,
|
||||||
|
"rating": "F",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Beam Laser",
|
||||||
|
"mount": "Turret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 1,
|
||||||
|
"rating": "F",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Beam Laser",
|
||||||
|
"mount": "Turret"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"utility": [
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Shield Booster"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Shield Booster"
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "C",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Kill Warrant Scanner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "C",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Cargo Scanner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "F",
|
||||||
|
"enabled": false,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Electronic Countermeasure",
|
||||||
|
"name": "Electronic Countermeasure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "I",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Chaff Launcher",
|
||||||
|
"name": "Chaff Launcher"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "I",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Point Defence",
|
||||||
|
"name": "Point Defence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"internal": [
|
||||||
|
{
|
||||||
|
"class": 7,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Shield Generator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 6,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Shield Cell Bank"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 6,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Cargo Rack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 5,
|
||||||
|
"rating": "D",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Hull Reinforcement Package"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 5,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Cargo Rack"
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"class": 4,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Cargo Rack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 4,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Cargo Rack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 4,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 3,
|
||||||
|
"group": "Fuel Scoop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 2,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 3,
|
||||||
|
"group": "Frame Shift Drive Interdictor"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"class": 3,
|
||||||
|
"fighterHangars": 1,
|
||||||
|
"hullCost": 141889930,
|
||||||
|
"speed": 180,
|
||||||
|
"topSpeed": 186.5,
|
||||||
|
"boost": 240,
|
||||||
|
"boostEnergy": 27,
|
||||||
|
"topBoost": 249.34,
|
||||||
|
"topPitch": 25.97,
|
||||||
|
"topRoll": 62.34,
|
||||||
|
"topYaw": 10.39,
|
||||||
|
"topSpeed": 187.01,
|
||||||
|
"totalCost": 882362058,
|
||||||
|
"totalDpe": 142.68,
|
||||||
|
"totalDps": 101.13,
|
||||||
|
"totalEps": 18.71,
|
||||||
|
"totalExplDpe": 0,
|
||||||
|
"totalExplDps": 0,
|
||||||
|
"totalExplSDps": 0,
|
||||||
|
"totalAbsDpe": 3.57,
|
||||||
|
"totalAbsDps": 18.78,
|
||||||
|
"totalAbsSDps": 14.45,
|
||||||
|
"totalHps": 28.28,
|
||||||
|
"totalKinDpe": 117.48,
|
||||||
|
"totalKinDps": 22.27,
|
||||||
|
"totalKinSDps": 16.91,
|
||||||
|
"totalSDps": 89.99,
|
||||||
|
"totalThermDpe": 21.63,
|
||||||
|
"totalThermDps": 60.08,
|
||||||
|
"totalThermSDps": 58.64,
|
||||||
|
"baseShieldStrength": 350,
|
||||||
|
"baseArmour": 945,
|
||||||
|
"hullExplRes": 0.22,
|
||||||
|
"hullKinRes": 0.27,
|
||||||
|
"hullMass": 400,
|
||||||
|
"hullThermRes": -0.36,
|
||||||
|
"masslock": 23,
|
||||||
|
"pipSpeed": 0.14,
|
||||||
|
"pitch": 25,
|
||||||
|
"moduleCostMultiplier": 1,
|
||||||
|
"modulearmour": 0,
|
||||||
|
"moduleprotection": 0,
|
||||||
|
"fuelCapacity": 32,
|
||||||
|
"cargoCapacity": 128,
|
||||||
|
"ladenMass": 1323.2,
|
||||||
|
"armour": 2227.5,
|
||||||
|
"baseArmour": 525,
|
||||||
|
"unladenMass": 1163.2,
|
||||||
|
"powerAvailable": 39.6,
|
||||||
|
"powerRetracted": 23.33,
|
||||||
|
"powerDeployed": 34.13,
|
||||||
|
"roll": 60,
|
||||||
|
"unladenRange": 18.74,
|
||||||
|
"yaw": 10,
|
||||||
|
"fullTankRange": 18.36,
|
||||||
|
"hardness": 65,
|
||||||
|
"ladenRange": 16.59,
|
||||||
|
"unladenFastestRange": 74.2,
|
||||||
|
"ladenFastestRange": 66.96,
|
||||||
|
"maxJumpCount": 4,
|
||||||
|
"shield": 833,
|
||||||
|
"shieldCells": 1840,
|
||||||
|
"shieldExplRes": 0.5,
|
||||||
|
"shieldKinRes": 0.4,
|
||||||
|
"shieldThermRes": -0.2,
|
||||||
|
"crew": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
255
__tests__/fixtures/asp-test-detailed-export-v4.json
Normal file
255
__tests__/fixtures/asp-test-detailed-export-v4.json
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://cdn.coriolis.io/schemas/ship-loadout/4.json#",
|
||||||
|
"name": "Multi-purpose Asp Explorer",
|
||||||
|
"ship": "Asp Explorer",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Coriolis.io",
|
||||||
|
"url": "https://coriolis.edcd.io/outfit/asp?code=0pftiFflfddsnf5------020202033c044002v62f2i.AwRj4yvI.CwRgDBldHnJA.H4sIAAAAAAAAA2P858DAwPCXEUhwHPvx%2F78YG5AltB7I%2F8%2F0TwImJboDSPJ%2F%2B%2Ff%2Fv%2FKlX%2F%2F%2Fi3AwMTBIfARK%2FGf%2BJwVSxArStVAYqOjvz%2F%2F%2FJVo5GRhE2IBc4SKQSSz%2FDGEmCa398P8%2F%2F2%2BgTf%2F%2FAwDFxwtofAAAAA%3D%3D&bn=Multi-purpose%20Asp%20Explorer",
|
||||||
|
"code": "0pftiFflfddsnf5------020202033c044002v62f2i.AwRj4yvI.CwRgDBldHnJA.H4sIAAAAAAAAA2P858DAwPCXEUhwHPvx/78YG5AltB7I/8/0TwImJboDSPJ/+/f/v/KlX///i3AwMTBIfARK/Gf+JwVSxArStVAYqOjvz///JVo5GRhE2IBc4SKQSSz/DGEmCa398P8//2+gTf//AwDFxwtofAAAAA==",
|
||||||
|
"shipId": "asp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"components": {
|
||||||
|
"standard": {
|
||||||
|
"bulkheads": "Lightweight Alloy",
|
||||||
|
"cargoHatch": {
|
||||||
|
"enabled": false,
|
||||||
|
"priority": 5
|
||||||
|
},
|
||||||
|
"powerPlant": {
|
||||||
|
"class": 5,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"modifications": {
|
||||||
|
"eff": -1850,
|
||||||
|
"pgen": 6,
|
||||||
|
"mass": 431
|
||||||
|
},
|
||||||
|
"blueprint": {
|
||||||
|
"id": 64,
|
||||||
|
"name": "Low emissions",
|
||||||
|
"grade": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"thrusters": {
|
||||||
|
"class": 5,
|
||||||
|
"rating": "D",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"modifications": {
|
||||||
|
"optmul": 440,
|
||||||
|
"integrity": -266,
|
||||||
|
"thermload": -1326,
|
||||||
|
"optmass": 520,
|
||||||
|
"power": 241
|
||||||
|
},
|
||||||
|
"blueprint": {
|
||||||
|
"id": 24,
|
||||||
|
"name": "Clean",
|
||||||
|
"grade": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"frameShiftDrive": {
|
||||||
|
"class": 5,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"modifications": {
|
||||||
|
"mass": 5025,
|
||||||
|
"integrity": -1539,
|
||||||
|
"power": 2437,
|
||||||
|
"optmass": 4870,
|
||||||
|
"maxfuel": 370
|
||||||
|
},
|
||||||
|
"blueprint": {
|
||||||
|
"id": 26,
|
||||||
|
"name": "Increased range",
|
||||||
|
"grade": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lifeSupport": {
|
||||||
|
"class": 4,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"modifications": {
|
||||||
|
"mass": -3923,
|
||||||
|
"integrity": -1797
|
||||||
|
},
|
||||||
|
"blueprint": {
|
||||||
|
"id": 49,
|
||||||
|
"name": "Lightweight",
|
||||||
|
"grade": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"powerDistributor": {
|
||||||
|
"class": 3,
|
||||||
|
"rating": "D",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"sensors": {
|
||||||
|
"class": 5,
|
||||||
|
"rating": "D",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"fuelTank": {
|
||||||
|
"class": 5,
|
||||||
|
"rating": "C",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hardpoints": [
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
],
|
||||||
|
"utility": [
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "I",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Heat Sink Launcher",
|
||||||
|
"name": "Heat Sink Launcher"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "I",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Heat Sink Launcher",
|
||||||
|
"name": "Heat Sink Launcher"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "I",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Heat Sink Launcher",
|
||||||
|
"name": "Heat Sink Launcher"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "I",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Point Defence",
|
||||||
|
"name": "Point Defence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"internal": [
|
||||||
|
{
|
||||||
|
"class": 6,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Fuel Scoop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 5,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Cargo Rack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 3,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Shield Generator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 3,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Cargo Rack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 2,
|
||||||
|
"rating": "G",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Planetary Vehicle Hangar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 1,
|
||||||
|
"rating": "C",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Scanner",
|
||||||
|
"name": "Advanced Discovery Scanner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 1,
|
||||||
|
"rating": "C",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Scanner",
|
||||||
|
"name": "Detailed Surface Scanner"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"class": 2,
|
||||||
|
"hullCost": 6135660,
|
||||||
|
"speed": 250,
|
||||||
|
"boost": 340,
|
||||||
|
"boostEnergy": 13,
|
||||||
|
"agility": 6,
|
||||||
|
"baseShieldStrength": 140,
|
||||||
|
"baseArmour": 210,
|
||||||
|
"hullMass": 280,
|
||||||
|
"masslock": 11,
|
||||||
|
"pipSpeed": 0.13,
|
||||||
|
"moduleCostMultiplier": 1,
|
||||||
|
"fuelCapacity": 32,
|
||||||
|
"cargoCapacity": 40,
|
||||||
|
"ladenMass": 435.26,
|
||||||
|
"armour": 378,
|
||||||
|
"shield": 113.43,
|
||||||
|
"shieldCells": 0,
|
||||||
|
"totalCost": 48402550,
|
||||||
|
"unladenMass": 363.26,
|
||||||
|
"totalDpe": 0,
|
||||||
|
"totalExplDpe": 0,
|
||||||
|
"totalKinDpe": 0,
|
||||||
|
"totalThermDpe": 0,
|
||||||
|
"totalDps": 0,
|
||||||
|
"totalExplDps": 0,
|
||||||
|
"totalKinDps": 0,
|
||||||
|
"totalThermDps": 0,
|
||||||
|
"totalSDps": 0,
|
||||||
|
"totalExplSDps": 0,
|
||||||
|
"totalKinSDps": 0,
|
||||||
|
"totalThermSDps": 0,
|
||||||
|
"totalEps": 1.2,
|
||||||
|
"totalHps": 1,
|
||||||
|
"shieldExplRes": 0.5,
|
||||||
|
"shieldKinRes": 0.6,
|
||||||
|
"shieldThermRes": 1.2,
|
||||||
|
"hullExplRes": 1.4,
|
||||||
|
"hullKinRes": 1.2,
|
||||||
|
"hullThermRes": 1,
|
||||||
|
"powerAvailable": 20.41,
|
||||||
|
"powerRetracted": 11.91,
|
||||||
|
"powerDeployed": 11.91,
|
||||||
|
"unladenRange": 50.45,
|
||||||
|
"fullTankRange": 47.03,
|
||||||
|
"ladenRange": 42.71,
|
||||||
|
"unladenFastestRange": 317.24,
|
||||||
|
"ladenFastestRange": 287.02,
|
||||||
|
"maxJumpCount": 7,
|
||||||
|
"topSpeed": 274.01,
|
||||||
|
"topBoost": 372.65
|
||||||
|
}
|
||||||
|
}
|
||||||
1288
__tests__/fixtures/companion-api-import-1.json
Normal file
1288
__tests__/fixtures/companion-api-import-1.json
Normal file
File diff suppressed because it is too large
Load Diff
552
__tests__/fixtures/companion-api-import-2.json
Normal file
552
__tests__/fixtures/companion-api-import-2.json
Normal file
@@ -0,0 +1,552 @@
|
|||||||
|
{
|
||||||
|
"cargo": {
|
||||||
|
"capacity": 32
|
||||||
|
},
|
||||||
|
"free": false,
|
||||||
|
"fuel": {
|
||||||
|
"main": {
|
||||||
|
"capacity": 128
|
||||||
|
},
|
||||||
|
"reserve": {
|
||||||
|
"capacity": 0.81
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": 31,
|
||||||
|
"modules": {
|
||||||
|
"Armour": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128049346,
|
||||||
|
"name": "BelugaLiner_Armour_Grade1",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Bobble01": [],
|
||||||
|
"Bobble02": [],
|
||||||
|
"Bobble03": [],
|
||||||
|
"Bobble04": [],
|
||||||
|
"Bobble05": [],
|
||||||
|
"Bobble06": [],
|
||||||
|
"Bobble07": [],
|
||||||
|
"Bobble08": [],
|
||||||
|
"Bobble09": [],
|
||||||
|
"Bobble10": [],
|
||||||
|
"Decal1": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128667757,
|
||||||
|
"name": "Decal_Explorer_Ranger",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Decal2": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128667742,
|
||||||
|
"name": "Decal_Combat_Deadly",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Decal3": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128667750,
|
||||||
|
"name": "Decal_Trade_Tycoon",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EngineColour": [],
|
||||||
|
"FrameShiftDrive": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064132,
|
||||||
|
"modifiers": {
|
||||||
|
"engineerID": 300100,
|
||||||
|
"id": 175,
|
||||||
|
"modifiers": [
|
||||||
|
{
|
||||||
|
"name": "mod_mass",
|
||||||
|
"type": 1,
|
||||||
|
"value": 0.4457540512085
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mod_health",
|
||||||
|
"type": 1,
|
||||||
|
"value": -0.24584779143333
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mod_passive_power",
|
||||||
|
"type": 1,
|
||||||
|
"value": 0.24457727372646
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mod_fsd_optimised_mass",
|
||||||
|
"type": 1,
|
||||||
|
"value": 0.49257898330688
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mod_fsd_max_fuel_per_jump",
|
||||||
|
"type": 2,
|
||||||
|
"value": 0.028505677357316
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mod_fsd_heat_rate",
|
||||||
|
"type": 2,
|
||||||
|
"value": -0.079360365867615
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"moduleTags": [
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"recipeID": 128673694,
|
||||||
|
"slotIndex": 53
|
||||||
|
},
|
||||||
|
"name": "Int_Hyperdrive_Size7_Class5",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"recipeLevel": 5,
|
||||||
|
"recipeName": "FSD_LongRange",
|
||||||
|
"recipeValue": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 46160201
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FuelTank": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064352,
|
||||||
|
"name": "Int_FuelTank_Size7_Class3",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 1602822,
|
||||||
|
"value": 1602822
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LifeSupport": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064174,
|
||||||
|
"name": "Int_LifeSupport_Size8_Class2",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 1569565
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MainEngines": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064094,
|
||||||
|
"modifiers": {
|
||||||
|
"engineerID": 300100,
|
||||||
|
"id": 253,
|
||||||
|
"modifiers": [
|
||||||
|
{
|
||||||
|
"name": "mod_engine_mass_curve_multiplier",
|
||||||
|
"type": 1,
|
||||||
|
"value": 0.098235413432121
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mod_engine_heat",
|
||||||
|
"type": 1,
|
||||||
|
"value": 0.18069696426392
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mod_passive_power",
|
||||||
|
"type": 1,
|
||||||
|
"value": 0.033788848668337
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mod_health",
|
||||||
|
"type": 1,
|
||||||
|
"value": -0.056404989212751
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mod_engine_mass_curve",
|
||||||
|
"type": 1,
|
||||||
|
"value": -0.027384582906961
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mod_engine_heat",
|
||||||
|
"type": 2,
|
||||||
|
"value": -0.072683908045292
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"moduleTags": [
|
||||||
|
17
|
||||||
|
],
|
||||||
|
"recipeID": 128673655,
|
||||||
|
"slotIndex": 52
|
||||||
|
},
|
||||||
|
"name": "Int_Engine_Size7_Class2",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"recipeLevel": 1,
|
||||||
|
"recipeName": "Engine_Dirty",
|
||||||
|
"recipeValue": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 1709638
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MediumHardpoint1": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128049436,
|
||||||
|
"name": "Hpt_BeamLaser_Turret_Medium",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 1889910
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MediumHardpoint2": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128049436,
|
||||||
|
"name": "Hpt_BeamLaser_Turret_Medium",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 1889910
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MediumHardpoint3": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128049460,
|
||||||
|
"name": "Hpt_MultiCannon_Gimbal_Medium",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 51300
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MediumHardpoint4": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128049460,
|
||||||
|
"name": "Hpt_MultiCannon_Gimbal_Medium",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 51300
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MediumHardpoint5": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128049460,
|
||||||
|
"name": "Hpt_MultiCannon_Gimbal_Medium",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 51300
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PaintJob": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128732290,
|
||||||
|
"name": "PaintJob_BelugaLiner_Tactical_White",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PlanetaryApproachSuite": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128672317,
|
||||||
|
"name": "Int_PlanetApproachSuite",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 450,
|
||||||
|
"value": 450
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PowerDistributor": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064207,
|
||||||
|
"name": "Int_PowerDistributor_Size6_Class5",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 3128120
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PowerPlant": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064057,
|
||||||
|
"modifiers": {
|
||||||
|
"engineerID": 300100,
|
||||||
|
"id": 277,
|
||||||
|
"modifiers": [
|
||||||
|
{
|
||||||
|
"name": "mod_powerplant_power",
|
||||||
|
"type": 1,
|
||||||
|
"value": 0.054692290723324
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mod_health",
|
||||||
|
"type": 1,
|
||||||
|
"value": -0.033690698444843
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mod_powerplant_heat",
|
||||||
|
"type": 1,
|
||||||
|
"value": 0.027470717206597
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mod_powerplant_heat",
|
||||||
|
"type": 2,
|
||||||
|
"value": -0.056317910552025
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"moduleTags": [
|
||||||
|
18
|
||||||
|
],
|
||||||
|
"recipeID": 128673765,
|
||||||
|
"slotIndex": 51
|
||||||
|
},
|
||||||
|
"name": "Int_Powerplant_Size6_Class5",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"recipeLevel": 1,
|
||||||
|
"recipeName": "PowerPlant_Boosted",
|
||||||
|
"recipeValue": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 14561578
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Radar": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064239,
|
||||||
|
"name": "Int_Sensors_Size5_Class2",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 71500
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot01_Size6": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128666681,
|
||||||
|
"name": "Int_FuelScoop_Size6_Class5",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 25887249
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot02_Size6": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064287,
|
||||||
|
"name": "Int_ShieldGenerator_Size6_Class5",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 14561578
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot03_Size6": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128727927,
|
||||||
|
"name": "Int_PassengerCabin_Size6_Class2",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 165808,
|
||||||
|
"value": 165808
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot04_Size6": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128727928,
|
||||||
|
"name": "Int_PassengerCabin_Size6_Class3",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 497429
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot05_Size5": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128727925,
|
||||||
|
"name": "Int_PassengerCabin_Size5_Class4",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 1492286
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot06_Size5": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064342,
|
||||||
|
"name": "Int_CargoRack_Size5_Class1",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 100409,
|
||||||
|
"value": 100409
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot07_Size4": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128727922,
|
||||||
|
"name": "Int_PassengerCabin_Size4_Class1",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 17059
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot08_Size3": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128667632,
|
||||||
|
"name": "Int_Repairer_Size3_Class5",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 2361960
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot09_Size3": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128672289,
|
||||||
|
"name": "Int_BuggyBay_Size2_Class2",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 19440
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot10_Size3": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128666634,
|
||||||
|
"name": "Int_DetailedSurfaceScanner_Tiny",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 225000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot11_Size3": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128663561,
|
||||||
|
"name": "Int_StellarBodyDiscoveryScanner_Advanced",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 1390500
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TinyHardpoint1": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128049513,
|
||||||
|
"name": "Hpt_ChaffLauncher_Tiny",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 7650
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TinyHardpoint2": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128668536,
|
||||||
|
"name": "Hpt_ShieldBooster_Size0_Class5",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 252900
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TinyHardpoint3": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128668536,
|
||||||
|
"name": "Hpt_ShieldBooster_Size0_Class5",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 252900
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TinyHardpoint4": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128668536,
|
||||||
|
"name": "Hpt_ShieldBooster_Size0_Class5",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 281000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TinyHardpoint5": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128668536,
|
||||||
|
"name": "Hpt_ShieldBooster_Size0_Class5",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 281000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TinyHardpoint6": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128668536,
|
||||||
|
"name": "Hpt_ShieldBooster_Size0_Class5",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 281000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"WeaponColour": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128732194,
|
||||||
|
"name": "WeaponCustomisation_Purple",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"unloaned": 0,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "BelugaLiner",
|
||||||
|
"value": {
|
||||||
|
"hull": 71688743,
|
||||||
|
"modules": 120812762,
|
||||||
|
"unloaned": 1869489
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
225
__tests__/fixtures/companion-api-import-4.json
Normal file
225
__tests__/fixtures/companion-api-import-4.json
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
{
|
||||||
|
"free": false,
|
||||||
|
"id": 2,
|
||||||
|
"modules": {
|
||||||
|
"Armour": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128049280,
|
||||||
|
"name": "CobraMkIII_Armour_Grade1",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FrameShiftDrive": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064117,
|
||||||
|
"name": "Int_Hyperdrive_Size4_Class5",
|
||||||
|
"on": true,
|
||||||
|
"priority": 4,
|
||||||
|
"value": 1610080
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FuelTank": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064349,
|
||||||
|
"name": "Int_FuelTank_Size4_Class3",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"value": 24734
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LifeSupport": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064149,
|
||||||
|
"name": "Int_LifeSupport_Size3_Class2",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"value": 10133
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MainEngines": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064079,
|
||||||
|
"name": "Int_Engine_Size4_Class2",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"value": 59633
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PaintJob": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128741033,
|
||||||
|
"name": "PaintJob_CobraMKIII_Corrosive_05",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PlanetaryApproachSuite": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128672317,
|
||||||
|
"name": "Int_PlanetApproachSuite",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"value": 500
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PowerDistributor": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064179,
|
||||||
|
"name": "Int_PowerDistributor_Size1_Class2",
|
||||||
|
"on": true,
|
||||||
|
"priority": 2,
|
||||||
|
"value": 1293
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PowerPlant": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064037,
|
||||||
|
"name": "Int_Powerplant_Size2_Class5",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"value": 160224
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Radar": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128064229,
|
||||||
|
"name": "Int_Sensors_Size3_Class2",
|
||||||
|
"on": true,
|
||||||
|
"priority": 0,
|
||||||
|
"value": 10133
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ShipID0": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128758976,
|
||||||
|
"name": "Nameplate_ShipID_Black",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ShipID1": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128758976,
|
||||||
|
"name": "Nameplate_ShipID_Black",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ShipKitBumper": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128740698,
|
||||||
|
"name": "CobraMkIII_ShipkitRaider1_Bumper1",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ShipKitSpoiler": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128740701,
|
||||||
|
"name": "CobraMkIII_ShipkitRaider1_Spoiler1",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ShipKitTail": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128740705,
|
||||||
|
"name": "CobraMkIII_ShipkitRaider1_Tail2",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ShipKitWings": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128740707,
|
||||||
|
"name": "CobraMkIII_ShipkitRaider1_Wings1",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ShipName0": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128758944,
|
||||||
|
"name": "Nameplate_Explorer01_Black",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ShipName1": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128758944,
|
||||||
|
"name": "Nameplate_Explorer01_Black",
|
||||||
|
"on": true,
|
||||||
|
"priority": 1,
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot01_Size4": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128666663,
|
||||||
|
"name": "Int_FuelScoop_Size4_Class3",
|
||||||
|
"on": true,
|
||||||
|
"priority": 2,
|
||||||
|
"value": 178898
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot02_Size4": [],
|
||||||
|
"Slot03_Size4": [],
|
||||||
|
"Slot04_Size2": [],
|
||||||
|
"Slot05_Size2": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128663561,
|
||||||
|
"name": "Int_StellarBodyDiscoveryScanner_Advanced",
|
||||||
|
"on": true,
|
||||||
|
"priority": 2,
|
||||||
|
"value": 1545000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Slot06_Size2": {
|
||||||
|
"module": {
|
||||||
|
"free": false,
|
||||||
|
"id": 128666634,
|
||||||
|
"name": "Int_DetailedSurfaceScanner_Tiny",
|
||||||
|
"on": true,
|
||||||
|
"priority": 2,
|
||||||
|
"value": 250000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "CobraMkIII",
|
||||||
|
"value": {
|
||||||
|
"hull": 205287,
|
||||||
|
"modules": 3850628,
|
||||||
|
"unloaned": 1751109
|
||||||
|
}
|
||||||
|
}
|
||||||
327
__tests__/fixtures/courier-test-detailed-export-v4.json
Normal file
327
__tests__/fixtures/courier-test-detailed-export-v4.json
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://cdn.coriolis.io/schemas/ship-loadout/4.json#",
|
||||||
|
"name": "Multi-purpose Imperial Courier",
|
||||||
|
"ship": "Imperial Courier",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"name": "Coriolis.io",
|
||||||
|
"url": "https://coriolis.edcd.io/outfit/imperial_courier?code=0patzF5l0das8f31a1a270202000e402t0101-2f.AwRj4zKA.CwRgDBldLiQ%3D.H4sIAAAAAAAAA12OP0tCYRjFj9fuVbvF1du9ekkT8s%2FkIg4NElyIBBd321yaGvwUQTS3N7UFfYygIT9EoyQUJA36ns47XJCWA%2B%2Fz%2Bz3Pe3ImBbDNKaqNPSBoGrL4ngfomKpFGiJ%2BLgHteR1IPjxJT5pF11uSeXNsJVcRfgdC92syWUuK0iMdKZqrjJ%2F0aoA71lJ5oKf38knWcCiptCPdhJIerdS00vlK0qktlqoj983UmqqHjQ33VsW8eazFmaTyULP2hQ4lX8LBme6g%2F6v0TTdbxJ2KhdEIaCw15MF%2FNB0L%2BS2hwEwyFM8KgP%2BqEpWWA3Qu9Z3z9kPWHzakt7Dt%2BAeD7ghSTgEAAA%3D%3D&bn=Multi-purpose%20Imperial%20Courier",
|
||||||
|
"code": "0patzF5l0das8f31a1a270202000e402t0101-2f.AwRj4zKA.CwRgDBldLiQ=.H4sIAAAAAAAAA12OP0tCYRjFj9fuVbvF1du9ekkT8s/kIg4NElyIBBd321yaGvwUQTS3N7UFfYygIT9EoyQUJA36ns47XJCWA+/z+z3Pe3ImBbDNKaqNPSBoGrL4ngfomKpFGiJ+LgHteR1IPjxJT5pF11uSeXNsJVcRfgdC92syWUuK0iMdKZqrjJ/0aoA71lJ5oKf38knWcCiptCPdhJIerdS00vlK0qktlqoj983UmqqHjQ33VsW8eazFmaTyULP2hQ4lX8LBme6g/6v0TTdbxJ2KhdEIaCw15MF/NB0L+S2hwEwyFM8KgP+qEpWWA3Qu9Z3z9kPWHzakt7Dt+AeD7ghSTgEAAA==",
|
||||||
|
"shipId": "imperial_courier"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"components": {
|
||||||
|
"standard": {
|
||||||
|
"bulkheads": "Lightweight Alloy",
|
||||||
|
"cargoHatch": {
|
||||||
|
"enabled": false,
|
||||||
|
"priority": 5
|
||||||
|
},
|
||||||
|
"powerPlant": {
|
||||||
|
"class": 4,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"modifications": {
|
||||||
|
"pgen": 1052,
|
||||||
|
"integrity": -482,
|
||||||
|
"eff": 974
|
||||||
|
},
|
||||||
|
"blueprint": {
|
||||||
|
"id": 63,
|
||||||
|
"name": "Overcharged",
|
||||||
|
"grade": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"thrusters": {
|
||||||
|
"class": 3,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"name": "Enhanced Performance",
|
||||||
|
"modifications": {
|
||||||
|
"optmul": 2476,
|
||||||
|
"thermload": 7023,
|
||||||
|
"power": 1763,
|
||||||
|
"integrity": 165,
|
||||||
|
"optmass": -667
|
||||||
|
},
|
||||||
|
"blueprint": {
|
||||||
|
"id": 22,
|
||||||
|
"name": "Dirty",
|
||||||
|
"grade": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"frameShiftDrive": {
|
||||||
|
"class": 3,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"modifications": {
|
||||||
|
"mass": 4082,
|
||||||
|
"integrity": -2422,
|
||||||
|
"power": 1782,
|
||||||
|
"optmass": 4927
|
||||||
|
},
|
||||||
|
"blueprint": {
|
||||||
|
"id": 26,
|
||||||
|
"name": "Increased range",
|
||||||
|
"grade": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lifeSupport": {
|
||||||
|
"class": 1,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"powerDistributor": {
|
||||||
|
"class": 3,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"sensors": {
|
||||||
|
"class": 2,
|
||||||
|
"rating": "D",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"fuelTank": {
|
||||||
|
"class": 3,
|
||||||
|
"rating": "C",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hardpoints": [
|
||||||
|
{
|
||||||
|
"class": 2,
|
||||||
|
"rating": "F",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Pulse Laser",
|
||||||
|
"mount": "Fixed",
|
||||||
|
"modifications": {
|
||||||
|
"rof": 5931,
|
||||||
|
"damage": -184,
|
||||||
|
"jitter": 50,
|
||||||
|
"distdraw": -4689,
|
||||||
|
"piercing": 3328
|
||||||
|
},
|
||||||
|
"blueprint": {
|
||||||
|
"id": 89,
|
||||||
|
"name": "Rapid fire",
|
||||||
|
"grade": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 2,
|
||||||
|
"rating": "F",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Pulse Laser",
|
||||||
|
"mount": "Fixed",
|
||||||
|
"modifications": {
|
||||||
|
"rof": 4715,
|
||||||
|
"damage": -97,
|
||||||
|
"jitter": 30,
|
||||||
|
"distdraw": -4548,
|
||||||
|
"piercing": 1057,
|
||||||
|
"integrity": 319
|
||||||
|
},
|
||||||
|
"blueprint": {
|
||||||
|
"id": 89,
|
||||||
|
"name": "Rapid fire",
|
||||||
|
"grade": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 2,
|
||||||
|
"rating": "F",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Multi-cannon",
|
||||||
|
"mount": "Gimballed",
|
||||||
|
"modifications": {
|
||||||
|
"damage": 2437,
|
||||||
|
"distdraw": 5487,
|
||||||
|
"rof": 1120,
|
||||||
|
"jitter": 58,
|
||||||
|
"thermload": 1346,
|
||||||
|
"power": 1009,
|
||||||
|
"integrity": -202,
|
||||||
|
"ammo": -2000
|
||||||
|
},
|
||||||
|
"blueprint": {
|
||||||
|
"id": 88,
|
||||||
|
"name": "Overcharged",
|
||||||
|
"grade": 3,
|
||||||
|
"special": {
|
||||||
|
"id": 3,
|
||||||
|
"name": "Corrosive shell"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"utility": [
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "I",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Heat Sink Launcher",
|
||||||
|
"name": "Heat Sink Launcher",
|
||||||
|
"modifications": {
|
||||||
|
"ammo": 5000,
|
||||||
|
"mass": 17684,
|
||||||
|
"reload": 9707
|
||||||
|
},
|
||||||
|
"blueprint": {
|
||||||
|
"id": 37,
|
||||||
|
"name": "Ammo capacity",
|
||||||
|
"grade": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "I",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Heat Sink Launcher",
|
||||||
|
"name": "Heat Sink Launcher",
|
||||||
|
"modifications": {
|
||||||
|
"ammo": 5000,
|
||||||
|
"mass": 18520,
|
||||||
|
"reload": 8715
|
||||||
|
},
|
||||||
|
"blueprint": {
|
||||||
|
"id": 37,
|
||||||
|
"name": "Ammo capacity",
|
||||||
|
"grade": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "I",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Chaff Launcher",
|
||||||
|
"name": "Chaff Launcher"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 0,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Frame Shift Wake Scanner"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"internal": [
|
||||||
|
{
|
||||||
|
"class": 3,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Shield Generator",
|
||||||
|
"modifications": {
|
||||||
|
"optmul": 1888,
|
||||||
|
"explres": 455,
|
||||||
|
"kinres": 546,
|
||||||
|
"thermres": 1092,
|
||||||
|
"brokenregen": -2614,
|
||||||
|
"regen": -876,
|
||||||
|
"distdraw": 463
|
||||||
|
},
|
||||||
|
"blueprint": {
|
||||||
|
"id": 77,
|
||||||
|
"name": "Reinforced",
|
||||||
|
"grade": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 3,
|
||||||
|
"rating": "A",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 1,
|
||||||
|
"group": "Fuel Scoop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 2,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Cargo Rack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class": 2,
|
||||||
|
"rating": "E",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Cargo Rack"
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"class": 1,
|
||||||
|
"rating": "C",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 2,
|
||||||
|
"group": "Scanner",
|
||||||
|
"name": "Advanced Discovery Scanner"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"class": 1,
|
||||||
|
"hullCost": 2481550,
|
||||||
|
"speed": 280,
|
||||||
|
"boost": 380,
|
||||||
|
"boostEnergy": 10,
|
||||||
|
"agility": 6,
|
||||||
|
"baseShieldStrength": 200,
|
||||||
|
"baseArmour": 80,
|
||||||
|
"hullMass": 35,
|
||||||
|
"masslock": 7,
|
||||||
|
"pipSpeed": 0.05,
|
||||||
|
"moduleCostMultiplier": 1,
|
||||||
|
"fuelCapacity": 8,
|
||||||
|
"cargoCapacity": 8,
|
||||||
|
"ladenMass": 104.25,
|
||||||
|
"armour": 144,
|
||||||
|
"shield": 404.19,
|
||||||
|
"shieldCells": 0,
|
||||||
|
"totalCost": 14059860,
|
||||||
|
"unladenMass": 88.25,
|
||||||
|
"totalDpe": 32.25,
|
||||||
|
"totalExplDpe": 0,
|
||||||
|
"totalKinDpe": 9.41,
|
||||||
|
"totalThermDpe": 22.84,
|
||||||
|
"totalDps": 53.8,
|
||||||
|
"totalExplDps": 0,
|
||||||
|
"totalKinDps": 17.44,
|
||||||
|
"totalThermDps": 36.35,
|
||||||
|
"totalSDps": 48.99,
|
||||||
|
"totalExplSDps": 0,
|
||||||
|
"totalKinSDps": 12.64,
|
||||||
|
"totalThermSDps": 36.35,
|
||||||
|
"totalEps": 9.84,
|
||||||
|
"totalHps": 12.28,
|
||||||
|
"shieldExplRes": 0.48,
|
||||||
|
"shieldKinRes": 0.55,
|
||||||
|
"shieldThermRes": 1.09,
|
||||||
|
"hullExplRes": 1.4,
|
||||||
|
"hullKinRes": 1.2,
|
||||||
|
"hullThermRes": 1,
|
||||||
|
"powerAvailable": 17.24,
|
||||||
|
"powerRetracted": 11.3,
|
||||||
|
"powerDeployed": 16.41,
|
||||||
|
"unladenRange": 25.57,
|
||||||
|
"fullTankRange": 23.92,
|
||||||
|
"ladenRange": 22.09,
|
||||||
|
"unladenFastestRange": 116.23,
|
||||||
|
"ladenFastestRange": 107,
|
||||||
|
"maxJumpCount": 5,
|
||||||
|
"topSpeed": 386.56,
|
||||||
|
"topBoost": 524.62
|
||||||
|
}
|
||||||
|
}
|
||||||
22
__tests__/fixtures/ed-shipyard-import-invalid.json
Normal file
22
__tests__/fixtures/ed-shipyard-import-invalid.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"buildText": "[Imaginary Ship]\nbla bla",
|
||||||
|
"errorMsg": "No such ship found: \"Imaginary Ship\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buildText": "[Viper]\nS: 1F/F Pulse Laser\nsome un-parseable nonsense\nS: 1F/F Pulse Laser\n",
|
||||||
|
"errorMsg": "Error parsing: \"some un-parseable nonsense\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buildText": "[Sidewinder]\nS: 2F/F Pulse Laser\nS: 1F/F Pulse Laser\n",
|
||||||
|
"errorMsg": "2F Pulse Laser exceeds slot size: \"S: 2F/F Pulse Laser\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buildText": "[Sidewinder]\nL: 2F/F Pulse Laser\nS: 1F/F Pulse Laser\n",
|
||||||
|
"errorMsg": "No hardpoint slot available for: \"L: 2F/F Pulse Laser\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buildText": "[Sidewinder]\nS: 1F/F Magic Thing\nS: 1F/F Pulse Laser\n",
|
||||||
|
"errorMsg": "Unknown component: \"S: 1F/F Magic Thing\""
|
||||||
|
}
|
||||||
|
]
|
||||||
32
__tests__/fixtures/ed-shipyard-import-valid.json
Normal file
32
__tests__/fixtures/ed-shipyard-import-valid.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"shipId": "anaconda",
|
||||||
|
"buildName": "Imported Anaconda",
|
||||||
|
"buildCode": "0pyttFolodDsyf5------1717--------05044j-03--2h--00.Iw18ZlA=.Aw18ZlA=.",
|
||||||
|
"buildText": "[Anaconda]\nS: 1F/F Pulse Laser\nS: 1F/F Pulse Laser\n\nBH: 1I Lightweight Alloy\nRB: 8E Power Plant\nTM: 7E Thrusters\nFH: 6E Frame Shift Drive\nEC: 5E Life Support\nPC: 8E Power Distributor\nSS: 8E Sensors\nFS: 5C Fuel Tank (Capacity: 32)\n\n7: 6E Cargo Rack (Capacity: 64)\n6: 5E Cargo Rack (Capacity: 32)\n6: 6E Shield Generator\n5: 4E Cargo Rack (Capacity: 16)\n4: 1E Basic Discovery Scanner\n2: 1E Cargo Rack (Capacity: 2)\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "anaconda",
|
||||||
|
"buildName": "Imported Anaconda",
|
||||||
|
"buildCode": "0pyttFolodDsyf5------1717--------05044j-03--2h--00.Iw18ZlA=.Aw18ZlA=.",
|
||||||
|
"buildText": "\n\n \t[Anaconda]\nS: 1F/F Pulse Laser\nS: 1F/F Pulse Laser\n\nBH: 1I Lightweight Alloy\nRB: 8E Power Plant\nTM: 7E Thrusters\nFH: 6E Frame Shift Drive\nEC: 5E Life Support\nPC: 8E Power Distributor\nSS: 8E Sensors\nFS: 5C Fuel Tank (Capacity: 32)\n\n7: 6E Cargo Rack (Capacity: 64)\n6: 5E Cargo Rack (Capacity: 32)\n6: 6E Shield Generator\n5: 4E Cargo Rack (Capacity: 16)\n4: 1E Basic Discovery Scanner\n2: 1E Cargo Rack (Capacity: 2)\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "cobra_mk_iii",
|
||||||
|
"buildName": "Imported Cobra Mk III",
|
||||||
|
"buildCode": "0patcFeldd5sdf41712222503040202490f242h.Iw1-kA==.Aw1-kA==.",
|
||||||
|
"buildText": "[Cobra Mk III]\nM: 1F/F Pulse Laser\nM: 1G/G Burst Laser\nS: 1E/T Fragment Cannon\nS: 1G/T Multi-cannon\nU: 0I Point Defence\nU: 0A Shield Booster\n\nBH: 1I Lightweight Alloy\nRB: 4A Power Plant\nTM: 4C Thrusters\nFH: 4E Frame Shift Drive\nEC: 3D Life Support\nPC: 2A Power Distributor\nSS: 3D Sensors\nFS: 4C Fuel Tank (Capacity: 16)\n\n4: 3E Cargo Rack (Capacity: 8)\n4: 3E Cargo Rack (Capacity: 8)\n4: 4E Shield Generator\n2: 2C Auto Field-Maintenance Unit\n2: 1E Standard Docking Computer\n2: 1E Basic Discovery Scanner\n---\nShield: 112.29 MJ\nPower : 10.45 MW retracted (67%)\n 12.16 MW deployed (78%)\n 15.60 MW available\nCargo : 16 T\nFuel : 16 T\nMass : 235.5 T empty\n 267.5 T full\nRange : 10.69 LY unladen\n 10.05 LY laden\nPrice : 2,929,040 CR\nRe-Buy: 146,452 CR @ 95% insurance\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "type_9_heavy",
|
||||||
|
"buildName": "Imported Type-9 Heavy",
|
||||||
|
"buildCode": "3pftsFklkdisif57e2k2f2h110001020306054j03022f01242i.Iw18eQ==.Aw18eQ==.",
|
||||||
|
"buildText": "[Type-9 Heavy]\nM: 2D/G Fragment Cannon\nM: 2I/F Mine Launcher\nM: 2B/FD Missile Rack\nS: 1I/FS Torpedo Pylon\nS: 1F/F Burst Laser\nU: 0I Chaff Launcher\nU: 0F Electronic Countermeasure\nU: 0I Heat Sink Launcher\nU: 0I Point Defence\n\nBH: 1I Mirrored Surface Composite\nRB: 5A Power Plant\nTM: 7D Thrusters\nFH: 6A Frame Shift Drive\nEC: 5A Life Support\nPC: 4D Power Distributor\nSS: 4D Sensors\nFS: 5C Fuel Tank (Capacity: 32)\n\n8: 7E Cargo Rack (Capacity: 128)\n7: 6E Cargo Rack (Capacity: 64)\n6: 6E Shield Generator\n5: 4E Cargo Rack (Capacity: 16)\n4: 3E Cargo Rack (Capacity: 8)\n4: 1C Advanced Discovery Scanner\n3: 2E Cargo Rack (Capacity: 4)\n3: 1E Standard Docking Computer\n2: 1C Detailed Surface Scanner\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "vulture",
|
||||||
|
"buildName": "Imported Vulture",
|
||||||
|
"buildCode": "4patfFalddksif31e1e0e0j04044a0n532jf1.Iw19kA==.Aw19kA==.",
|
||||||
|
"buildText": "[Vulture]\nL: 3E/G Pulse Laser\nL: 3E/G Pulse Laser\nU: 0A Frame Shift Wake Scanner\nU: 0A Kill Warrant Scanner\nU: 0A Shield Booster\nU: 0A Shield Booster\n\nBH: 1I Reactive Surface Composite\nRB: 4A Power Plant\nTM: 5A Thrusters\nFH: 4A Frame Shift Drive\nEC: 3D Life Support\nPC: 5A Power Distributor\nSS: 4D Sensors\nFS: 3C Fuel Tank (Capacity: 8)\n\n5: 5A Shield Generator\n4: 4A Auto Field-Maintenance Unit\n2: 2A Shield Cell Bank\n1: 1A Fuel Scoop\n1: 1C Fuel Tank (Capacity: 2)"
|
||||||
|
}
|
||||||
|
]
|
||||||
50
__tests__/fixtures/expected-builds.json
Normal file
50
__tests__/fixtures/expected-builds.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"type_6_transporter": {
|
||||||
|
"Cargo": "A0p0tdFal8d8s8f4-----04040303430101.Iw1/kA==.Aw1/kA==.",
|
||||||
|
"Miner": "A0p5tdFal8d8s8f42l2l---040403451q0101.Iw1/kA==.Aw1/kA==.",
|
||||||
|
"Hopper": "A0p0tdFal8d0s8f41717---030302024300-.Iw1/kA==.Aw1/kA==."
|
||||||
|
},
|
||||||
|
"type_7_transport": {
|
||||||
|
"Cargo": "A0p0tiFfliddsdf5--------0505040403480101.Iw18aQ==.Aw18aQ==.",
|
||||||
|
"Miner": "A0pdtiFflid8sdf5--2l2l----0505041v03450000.Iw18aQ==.Aw18aQ==."
|
||||||
|
},
|
||||||
|
"federal_dropship": {
|
||||||
|
"Cargo": "A0pdtiFflnddsif4-1717------05040448--020201.Iw18eQ==.Aw18eQ==."
|
||||||
|
},
|
||||||
|
"asp": {
|
||||||
|
"Miner": "A2pftfFflidfskf50s0s24242l2l---04054a1q02022o27.Iw18WQ==.Aw18WQ==."
|
||||||
|
},
|
||||||
|
"imperial_clipper": {
|
||||||
|
"Cargo": "A0p5tiFflndisnf4--0s0s----0605450302020101.Iw18aQ==.Aw18aQ==.",
|
||||||
|
"Dream": "A2pktkFflndpskf40v0v0s0s0404040n4k5n5d2b29292o-.AwRj4yWU1I==.CwBhCYy6YRigzLIA.",
|
||||||
|
"Current": "A0patkFflndfskf4----------------.AwRj4yWU1I==.CwBhCYy6YRigzLIA."
|
||||||
|
},
|
||||||
|
"type_9_heavy": {
|
||||||
|
"Current": "A0patsFklndnsif6---------0706054a0303020224.AwRj4yoo.EwBhEYy6dsg=."
|
||||||
|
},
|
||||||
|
"python": {
|
||||||
|
"Cargo": "A0patnFflidsssf5---------050505040448020201.Iw18eQ==.Aw18eQ==.",
|
||||||
|
"Miner": "A0pktkFflidpspf50v0v0v2m2m0404--050505Ce4a1v02022o.Iw18eQ==.IwBhBYy6dkCYg===.",
|
||||||
|
"Dream": "A2pptkFfliduspf50v0v0v27270404040m5n5n4f2d2d032t0201.Iw1+gDBxA===.EwBhEYy6e0WEA===.",
|
||||||
|
"Missile": "A0pttoFjljdystf52f2g2d2ePh----04044j03---002h.Iw18eQ==.Aw18eQ==."
|
||||||
|
},
|
||||||
|
"anaconda": {
|
||||||
|
"Dream": "A4putpFklndzsuf52c0o0o0o1m1m0q0q0404040l0b0100004k5n5n112d2d04-0303326b.AwRj4yo5dyg=.MwBhCYy6duvARiA=.",
|
||||||
|
"Cargo": "A0patnFklndnsxf5----------------06050505040404-45030301.Iw18ZVA=.Aw18ZVA=.",
|
||||||
|
"Current": "A0patnFklndksxf5----------------06050505040404-03034524.Iw18ZVA=.Aw18ZVA=.",
|
||||||
|
"Explorer": "A0patnFklndksxf5--------0202------f7050505040s37-2f2i4524.AwRj4yVKJ9hA.AwhMIyumQRhEA===.",
|
||||||
|
"Test": "A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b.Iw18ZVA=.Aw18ZVA=."
|
||||||
|
},
|
||||||
|
"diamondback_explorer": {
|
||||||
|
"Explorer": "A0p0tdFfldddsdf5---0202--320p432i2f-.AwRj4zTYg===.AwiMIyoo."
|
||||||
|
},
|
||||||
|
"vulture": {
|
||||||
|
"Bounty Hunter": "A3patcFalddksff31e1e0404-0l4a-5d27662j.AwRj4z2I.MwBhBYy6oJmAjLIA."
|
||||||
|
},
|
||||||
|
"fer_de_lance": {
|
||||||
|
"Attack": "A2pfthFalidpsff31r0s0s0s0s000404-04-4a-5d27-.Iw18aQ==.CwBhrSu8EZyA."
|
||||||
|
},
|
||||||
|
"eagle": {
|
||||||
|
"Figther": "A4p0t5F5l3d5s5f20p0p24-4053-2j-.Iw18kA==.Aw18kA==."
|
||||||
|
}
|
||||||
|
}
|
||||||
50
__tests__/fixtures/old-valid-export.json
Normal file
50
__tests__/fixtures/old-valid-export.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"builds": {
|
||||||
|
"type_6_transporter": {
|
||||||
|
"Cargo": "02A4D4A2D2D2D4C-----04040303430101",
|
||||||
|
"Miner": "03A4D4A2D2D2D4C2l2l---040403451q0101",
|
||||||
|
"Hopper": "02A4D4A2D1A2D4C1717---030302024300-"
|
||||||
|
},
|
||||||
|
"type_7_transport": {
|
||||||
|
"Cargo": "02A5D5A4D3D3D5C--------0505040403480101",
|
||||||
|
"Miner": "04D5D5A4D2D3D5C--2l2l----0505041v03450000"
|
||||||
|
},
|
||||||
|
"federal_dropship": {
|
||||||
|
"Cargo": "04D5D5A5D3D4D4C-1717------05040448020201"
|
||||||
|
},
|
||||||
|
"asp": {
|
||||||
|
"Miner": "25A5A5A4D4A5A5C0s0s24242l2l---04054a1q02022o27"
|
||||||
|
},
|
||||||
|
"imperial_clipper": {
|
||||||
|
"Cargo": "03A5D5A5D4D5D4C--0s0s----0605450302020101",
|
||||||
|
"Dream": "26A6A5A5D6A5A4C0v0v0s0s0404040n4k5n5d2b29292o-.AwRj4yWU1I==.CwBhCYy6YRigzLIA",
|
||||||
|
"Current": "04A6A5A5D4A5A4C----------------.AwRj4yWU1I==.CwBhCYy6YRigzLIA"
|
||||||
|
},
|
||||||
|
"type_9_heavy": {
|
||||||
|
"Current": "04A7D6A5D5D4D6C---------0706054a0303020224.AwRj4yoo.EwBhEYy6dsg="
|
||||||
|
},
|
||||||
|
"python": {
|
||||||
|
"Cargo": "04A6D5A4D6D6D5C---------050505040448020201.Iw18eQ==.Aw18eQ==",
|
||||||
|
"Miner": "06A6A5A4D6A6A5C0v0v0v2m2m0404--050505Ce4a1v02022o.Iw18eQ==.IwBhBYy6dkCYg===",
|
||||||
|
"Dream": "27A6A5A4D7A6A5C0v0v0v27270404040m5n5n4f2d2d032t0201.Iw1+gDBxA===.EwBhEYy6e0WEA==="
|
||||||
|
},
|
||||||
|
"anaconda": {
|
||||||
|
"Dream": "48A7A6A5D8A8A5C2c0o0o0o1m1m0q0q0404040l0b0100004k5n5n112d2d040303326b.AwRj4yo5dig=.MwBhCYy6du3ARiA=",
|
||||||
|
"Cargo": "04A6D6A5D5D8D5C----------------0605050504040445030301.Iw18ZlA=.Aw18ZlA=",
|
||||||
|
"Current": "04A6D6A5D5A8D5C----------------0605050504040403034524.Iw18ZlA=.Aw18ZlA=",
|
||||||
|
"Explorer": "04A6D6A5D5A8D5C--------0202------f7050505040s372f2i4524.AwRj4yVKJthA.AwhMIyungRhEA==="
|
||||||
|
},
|
||||||
|
"diamondback_explorer": {
|
||||||
|
"Explorer": "02A4D5A3D3D3D5C---0202--320p432i2f.AwRj4zTI.AwiMIypI"
|
||||||
|
},
|
||||||
|
"vulture": {
|
||||||
|
"Bounty Hunter": "34A4C4A3D5A4A3C1e1e0404-0l4a5d27662j.AwRj4y2I.MwBhBYy6wJmAjLIA"
|
||||||
|
},
|
||||||
|
"fer_de_lance": {
|
||||||
|
"Attack": "25A5C4A4D6A4A3C1r0s0s0s0s000404-04-4a-5d27-.Iw18aQ==.CwBhrSu8EZyA"
|
||||||
|
},
|
||||||
|
"eagle": {
|
||||||
|
"Figther": "42A3A3A1D2A2A2C0p0p24-40532j.AwRj49iA.AwgsIkEZigmIA==="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
__tests__/fixtures/valid-backup.json
Normal file
67
__tests__/fixtures/valid-backup.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"builds": {
|
||||||
|
"type_6_transporter": {
|
||||||
|
"Cargo": "02A4D4A2D2D2D4C-----04040303430101",
|
||||||
|
"Miner": "03A4D4A2D2D2D4C2l2l---040403451q0101",
|
||||||
|
"Hopper": "02A4D4A2D1A2D4C1717---030302024300-"
|
||||||
|
},
|
||||||
|
"type_7_transport": {
|
||||||
|
"Cargo": "02A5D5A4D3D3D5C--------0505040403480101",
|
||||||
|
"Miner": "04D5D5A4D2D3D5C--2l2l----0505041v03450000"
|
||||||
|
},
|
||||||
|
"federal_dropship": {
|
||||||
|
"Cargo": "04D5D5A5D3D4D4C-1717------05040448020201"
|
||||||
|
},
|
||||||
|
"asp": {
|
||||||
|
"Miner": "25A5A5A4D4A5A5C0s0s24242l2l---04054a1q02022o27"
|
||||||
|
},
|
||||||
|
"cobra_mk_iii": {
|
||||||
|
"Example": "24A4A4A3D3A3A4C0s0s2d2d0m0445032b2o2753.AwRj4yKA.CwBhEYyrKhmMQ==="
|
||||||
|
},
|
||||||
|
"imperial_clipper": {
|
||||||
|
"Cargo": "03A5D5A5D4D5D4C--0s0s----0605450302020101",
|
||||||
|
"Multi-purpose": "26A4A5A5D6A5A4C0v0v272704090j0h064f2c0302020101",
|
||||||
|
"Current": "05A6D5A5D6A5A4C0v0v27270404050n4m05035d29292o01.AwRj4yrI.AwhMIyuBGNiA",
|
||||||
|
"Dream": "26A6A5A5D6A5A4C0v0v0s0s04040c0n064f5d2b02022o0d.AwRj49UlmI==.AwiMIyuo"
|
||||||
|
},
|
||||||
|
"type_9_heavy": {
|
||||||
|
"Cargo": "04A6D6A5D4D4D5C---------07064f040303010201.AwRj4yoo.EwBhEYy6dsg="
|
||||||
|
},
|
||||||
|
"python": {
|
||||||
|
"Cargo": "04A6D5A4D6D6D5C---------050505044a03020201",
|
||||||
|
"Miner": "04A6D5A4D6D6D5C---2m2m----050505044d1v02022o"
|
||||||
|
},
|
||||||
|
"anaconda": {
|
||||||
|
"Dream": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404040l0b0100034k5n05050404040303326b.AwRj4yo5dig=.MwBhEYy6duwEziA=",
|
||||||
|
"Cargo": "03A7D6A5D4D8D5C----------------060505054d040403030301.AwRj4yuqg===.Aw18ZlA=",
|
||||||
|
"Modified": "0pyttFolodDsyf5------1717--------05044j-03----2h00.Iw18ZlA=.Aw18ZlA=.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA=="
|
||||||
|
},
|
||||||
|
"diamondback_explorer": {
|
||||||
|
"Explorer": "02A4D5A3D3D3D5C-------320p432i2f.AwRj4zTI.AwiMIypI"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"comparisons": {
|
||||||
|
"Test": {
|
||||||
|
"facets": [ 9, 6, 4, 1, 3, 2 ],
|
||||||
|
"builds": [
|
||||||
|
{
|
||||||
|
"shipId": "anaconda",
|
||||||
|
"buildName": "Dream"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "asp",
|
||||||
|
"buildName": "Miner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "diamondback_explorer",
|
||||||
|
"buildName": "Explorer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"insurance": "Beta",
|
||||||
|
"discounts": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
}
|
||||||
3621
__tests__/fixtures/valid-detailed-export.json
Normal file
3621
__tests__/fixtures/valid-detailed-export.json
Normal file
File diff suppressed because it is too large
Load Diff
90
__tests__/test-agility.js
Normal file
90
__tests__/test-agility.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import Ship from '../src/app/shipyard/Ship';
|
||||||
|
import { Ships } from 'coriolis-data/dist';
|
||||||
|
import * as ModuleUtils from '../src/app/shipyard/ModuleUtils';
|
||||||
|
|
||||||
|
describe("Agility", function() {
|
||||||
|
|
||||||
|
it("correctly calculates speed", function() {
|
||||||
|
let agilityData = require('./fixtures/agility-data');
|
||||||
|
|
||||||
|
for (let shipId in agilityData) {
|
||||||
|
for (let thrusterId in agilityData[shipId]) {
|
||||||
|
const thrusterData = agilityData[shipId][thrusterId];
|
||||||
|
let shipData = Ships[shipId];
|
||||||
|
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||||
|
ship.buildWith(shipData.defaults);
|
||||||
|
ship.use(ship.standard[1], ModuleUtils.findModule('t', thrusterId));
|
||||||
|
|
||||||
|
expect(Math.round(ship.topSpeed)).toBe(thrusterData.speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correctly calculates boost", function() {
|
||||||
|
let agilityData = require('./fixtures/agility-data');
|
||||||
|
|
||||||
|
for (let shipId in agilityData) {
|
||||||
|
for (let thrusterId in agilityData[shipId]) {
|
||||||
|
const thrusterData = agilityData[shipId][thrusterId];
|
||||||
|
let shipData = Ships[shipId];
|
||||||
|
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||||
|
ship.buildWith(shipData.defaults);
|
||||||
|
// Turn off internals to ensure we have enough power to boost
|
||||||
|
for (let internal in ship.internal) {
|
||||||
|
ship.internal[internal].enabled = 0;
|
||||||
|
}
|
||||||
|
ship.use(ship.standard[1], ModuleUtils.findModule('t', thrusterId));
|
||||||
|
|
||||||
|
expect(Math.round(ship.topBoost)).toBe(thrusterData.boost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correctly calculates pitch", function() {
|
||||||
|
let agilityData = require('./fixtures/agility-data');
|
||||||
|
|
||||||
|
for (let shipId in agilityData) {
|
||||||
|
for (let thrusterId in agilityData[shipId]) {
|
||||||
|
const thrusterData = agilityData[shipId][thrusterId];
|
||||||
|
let shipData = Ships[shipId];
|
||||||
|
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||||
|
ship.buildWith(shipData.defaults);
|
||||||
|
ship.use(ship.standard[1], ModuleUtils.findModule('t', thrusterId));
|
||||||
|
|
||||||
|
expect(Math.round(ship.pitches[4] * 100) / 100).toBeCloseTo(thrusterData.pitch, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correctly calculates roll", function() {
|
||||||
|
let agilityData = require('./fixtures/agility-data');
|
||||||
|
|
||||||
|
for (let shipId in agilityData) {
|
||||||
|
for (let thrusterId in agilityData[shipId]) {
|
||||||
|
const thrusterData = agilityData[shipId][thrusterId];
|
||||||
|
let shipData = Ships[shipId];
|
||||||
|
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||||
|
ship.buildWith(shipData.defaults);
|
||||||
|
ship.use(ship.standard[1], ModuleUtils.findModule('t', thrusterId));
|
||||||
|
|
||||||
|
expect(Math.round(ship.rolls[4] * 100) / 100).toBeCloseTo(thrusterData.roll, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correctly calculates yaw", function() {
|
||||||
|
let agilityData = require('./fixtures/agility-data');
|
||||||
|
|
||||||
|
for (let shipId in agilityData) {
|
||||||
|
for (let thrusterId in agilityData[shipId]) {
|
||||||
|
const thrusterData = agilityData[shipId][thrusterId];
|
||||||
|
let shipData = Ships[shipId];
|
||||||
|
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||||
|
ship.buildWith(shipData.defaults);
|
||||||
|
ship.use(ship.standard[1], ModuleUtils.findModule('t', thrusterId));
|
||||||
|
|
||||||
|
expect(Math.round(ship.yaws[4] * 100) / 100).toBeCloseTo(thrusterData.yaw, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
327
__tests__/test-import.js
Normal file
327
__tests__/test-import.js
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
jest.unmock('../src/app/stores/Persist');
|
||||||
|
jest.unmock('../src/app/components/TranslatedComponent');
|
||||||
|
jest.unmock('../src/app/components/ModalImport');
|
||||||
|
jest.unmock('prop-types');
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import TU from 'react-testutils-additions';
|
||||||
|
import Utils from './testUtils';
|
||||||
|
import { getLanguage } from '../src/app/i18n/Language';
|
||||||
|
|
||||||
|
describe('Import Modal', function() {
|
||||||
|
|
||||||
|
let MockRouter = require('../src/app/Router').default;
|
||||||
|
const Persist = require('../src/app/stores/Persist').default;
|
||||||
|
const ModalImport = require('../src/app/components/ModalImport').default;
|
||||||
|
const mockContext = {
|
||||||
|
language: getLanguage('en'),
|
||||||
|
sizeRatio: 1,
|
||||||
|
openMenu: jest.genMockFunction(),
|
||||||
|
closeMenu: jest.genMockFunction(),
|
||||||
|
showModal: jest.genMockFunction(),
|
||||||
|
hideModal: jest.genMockFunction(),
|
||||||
|
tooltip: jest.genMockFunction(),
|
||||||
|
termtip: jest.genMockFunction(),
|
||||||
|
onWindowResize: jest.genMockFunction()
|
||||||
|
};
|
||||||
|
|
||||||
|
let modal, render, ContextProvider = Utils.createContextProvider(mockContext);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear saved builds, and reset React DOM
|
||||||
|
*/
|
||||||
|
function reset() {
|
||||||
|
MockRouter.go.mockClear();
|
||||||
|
Persist.deleteAll();
|
||||||
|
render = TU.renderIntoDocument(<ContextProvider><ModalImport /></ContextProvider>);
|
||||||
|
modal = TU.findRenderedComponentWithType(render, ModalImport);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate user import text entry / paste
|
||||||
|
* @param {string} text Import text / raw data
|
||||||
|
*/
|
||||||
|
function pasteText(text) {
|
||||||
|
let textarea = TU.findRenderedDOMComponentWithTag(render, 'textarea');
|
||||||
|
TU.Simulate.change(textarea, { target: { value: text } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate click on Proceed button
|
||||||
|
*/
|
||||||
|
function clickProceed() {
|
||||||
|
let proceedButton = TU.findRenderedDOMComponentWithId(render, 'proceed');
|
||||||
|
TU.Simulate.click(proceedButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate click on Import button
|
||||||
|
*/
|
||||||
|
function clickImport() {
|
||||||
|
let importButton = TU.findRenderedDOMComponentWithId(render, 'import');
|
||||||
|
TU.Simulate.click(importButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Import Backup', function() {
|
||||||
|
|
||||||
|
beforeEach(reset);
|
||||||
|
|
||||||
|
it('imports a valid backup', function() {
|
||||||
|
let importData = require('./fixtures/valid-backup');
|
||||||
|
let importString = JSON.stringify(importData);
|
||||||
|
|
||||||
|
expect(modal.state.importValid).toEqual(false);
|
||||||
|
expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
pasteText(importString);
|
||||||
|
expect(modal.state.importValid).toBe(true);
|
||||||
|
expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
expect(modal.state.builds).toEqual(importData.builds);
|
||||||
|
expect(modal.state.comparisons).toEqual(importData.comparisons);
|
||||||
|
expect(modal.state.shipDiscount).toEqual(importData.discounts[0]);
|
||||||
|
expect(modal.state.moduleDiscount).toEqual(importData.discounts[1]);
|
||||||
|
expect(modal.state.insurance).toBe(importData.insurance.toLowerCase());
|
||||||
|
clickProceed();
|
||||||
|
expect(modal.state.processed).toBe(true);
|
||||||
|
expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
clickImport();
|
||||||
|
expect(Persist.getBuilds()).toEqual(importData.builds);
|
||||||
|
expect(Persist.getComparisons()).toEqual(importData.comparisons);
|
||||||
|
expect(Persist.getInsurance()).toEqual(importData.insurance.toLowerCase());
|
||||||
|
expect(Persist.getShipDiscount()).toEqual(importData.discounts[0]);
|
||||||
|
expect(Persist.getModuleDiscount()).toEqual(importData.discounts[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('imports an old valid backup', function() {
|
||||||
|
const importData = require('./fixtures/old-valid-export');
|
||||||
|
const importStr = JSON.stringify(importData);
|
||||||
|
|
||||||
|
pasteText(importStr);
|
||||||
|
expect(modal.state.builds).toEqual(importData.builds);
|
||||||
|
expect(modal.state.importValid).toBe(true);
|
||||||
|
expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
clickProceed();
|
||||||
|
expect(modal.state.processed).toBeTruthy();
|
||||||
|
clickImport();
|
||||||
|
expect(Persist.getBuilds()).toEqual(importData.builds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('catches an invalid backup', function() {
|
||||||
|
const importData = require('./fixtures/valid-backup');
|
||||||
|
let invalidImportData = Object.assign({}, importData);
|
||||||
|
//invalidImportData.builds.asp = null; // Remove Asp Miner build used in comparison
|
||||||
|
delete(invalidImportData.builds.asp);
|
||||||
|
|
||||||
|
pasteText('"this is not valid"');
|
||||||
|
expect(modal.state.importValid).toBeFalsy();
|
||||||
|
expect(modal.state.errorMsg).toEqual('Must be an object or array!');
|
||||||
|
pasteText('{ "builds": "Should not be a string" }');
|
||||||
|
expect(modal.state.importValid).toBeFalsy();
|
||||||
|
expect(modal.state.errorMsg).toEqual('builds must be an object!');
|
||||||
|
pasteText(JSON.stringify(importData).replace('anaconda', 'invalid_ship'));
|
||||||
|
expect(modal.state.importValid).toBeFalsy();
|
||||||
|
expect(modal.state.errorMsg).toEqual('"invalid_ship" is not a valid Ship Id!');
|
||||||
|
pasteText(JSON.stringify(importData).replace('Dream', ''));
|
||||||
|
expect(modal.state.importValid).toBeFalsy();
|
||||||
|
expect(modal.state.errorMsg).toEqual('Imperial Clipper build "" must be a string at least 1 character long!');
|
||||||
|
pasteText(JSON.stringify(invalidImportData));
|
||||||
|
expect(modal.state.importValid).toBeFalsy();
|
||||||
|
expect(modal.state.errorMsg).toEqual('asp build "Miner" data is missing!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Import Detailed V3 Build', function() {
|
||||||
|
|
||||||
|
beforeEach(reset);
|
||||||
|
|
||||||
|
it('imports a valid v3 build', function() {
|
||||||
|
const importData = require('./fixtures/anaconda-test-detailed-export-v3');
|
||||||
|
pasteText(JSON.stringify(importData));
|
||||||
|
|
||||||
|
expect(modal.state.importValid).toBeTruthy();
|
||||||
|
expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
expect(modal.state.singleBuild).toBe(true);
|
||||||
|
clickProceed();
|
||||||
|
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||||
|
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/anaconda?code=A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b.AwRj4zNLaA%3D%3D.CwBhCYzBGW9qCTSqq5xA.&bn=Test%20My%20Ship');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('catches an invalid build', function() {
|
||||||
|
const importData = require('./fixtures/anaconda-test-detailed-export-v3');
|
||||||
|
pasteText(JSON.stringify(importData).replace('references', 'refs'));
|
||||||
|
|
||||||
|
expect(modal.state.importValid).toBeFalsy();
|
||||||
|
expect(modal.state.errorMsg).toEqual('Anaconda Build "Test My Ship": Invalid data');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Import Detailed V4 Build', function() {
|
||||||
|
|
||||||
|
beforeEach(reset);
|
||||||
|
|
||||||
|
it('imports a valid v4 build', function() {
|
||||||
|
const importData = require('./fixtures/anaconda-test-detailed-export-v4');
|
||||||
|
pasteText(JSON.stringify(importData));
|
||||||
|
|
||||||
|
expect(modal.state.importValid).toBeTruthy();
|
||||||
|
expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
expect(modal.state.singleBuild).toBe(true);
|
||||||
|
clickProceed();
|
||||||
|
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||||
|
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/anaconda?code=A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b.AwRj4zNLaA%3D%3D.CwBhCYzBGW9qCTSqq5xA.H4sIAAAAAAAAA2MUe8HMwPD%2FPwMAAGvB0AkAAAA%3D&bn=Test%20My%20Ship');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Import Detailed Engineered V4 Build', function() {
|
||||||
|
|
||||||
|
beforeEach(reset);
|
||||||
|
|
||||||
|
it('imports a valid v4 build', function() {
|
||||||
|
const importData = require('./fixtures/asp-test-detailed-export-v4');
|
||||||
|
pasteText(JSON.stringify(importData));
|
||||||
|
|
||||||
|
expect(modal.state.importValid).toBeTruthy();
|
||||||
|
expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
expect(modal.state.singleBuild).toBe(true);
|
||||||
|
clickProceed();
|
||||||
|
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||||
|
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/asp?code=A0pftiFflfddsnf5------020202033c044002v62f2i.AwRj4yvI.CwRgDBldHnJA.H4sIAAAAAAAAA2P858DAwPCXEUhwHPvx%2F78YG5AltB7I%2F8%2F0TwImJboDSPJ%2F%2B%2Ff%2Fv%2FKlX%2F%2F%2Fi3AwMTBIfARK%2FGf%2BJwVSxArStVAYqOjvz%2F%2F%2FJVo5GRhE2IBc4SKQSSz%2FDGEmCa398P8%2F%2F2%2BgTf%2F%2FA7kMAExxqlSAAAAA&bn=Multi-purpose%20Asp%20Explorer');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('imports a valid v4 build with modifications', function() {
|
||||||
|
const importData = require('./fixtures/courier-test-detailed-export-v4');
|
||||||
|
pasteText(JSON.stringify(importData));
|
||||||
|
|
||||||
|
expect(modal.state.importValid).toBeTruthy();
|
||||||
|
expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
expect(modal.state.singleBuild).toBe(true);
|
||||||
|
clickProceed();
|
||||||
|
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||||
|
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/imperial_courier?code=A0patzF5l0das8f31a1a270202000e402t0101-2f.AwRj4zKA.CwRgDBldLiQ%3D.H4sIAAAAAAAAA12OPUvDYBSFT1OTfkRJjUkbbC3Yj8mlODgUISAtdOlety5ODv0Vgji7O7kJ%2FgzBQX%2BEY7Gg0NKhfY%2FnHQLFDBdynufe9%2BRMCmCb06g29oCgacjiRx6gY6oWKUT8UgLaszqQfHmSnpVFN1uSeXNsJVcj%2FA2EHlZkspIUpUc6UjTXGT85qwHuSEuVc%2F16r99kDQeSSjvSbSjpyUpNK10uJJ3aYqk6smwm1lQ9bOxw71TMm8VanEqq9JW1r3Qo%2BREOLnQHvbWmb7rZIu5VLIyGQGOukPv%2F0WQk5LeEAjPOUDwtAP6bShy2HKAz0HPO%2B5KsP25I79O2I7LvD%2Bz4Il1XAQAA&bn=Multi-purpose%20Imperial%20Courier');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Import Detaild Builds Array', function() {
|
||||||
|
|
||||||
|
beforeEach(reset);
|
||||||
|
|
||||||
|
it('imports all builds', function() {
|
||||||
|
const importData = require('./fixtures/valid-detailed-export');
|
||||||
|
const expectedBuilds = require('./fixtures/expected-builds');
|
||||||
|
|
||||||
|
pasteText(JSON.stringify(importData));
|
||||||
|
expect(modal.state.importValid).toBeTruthy();
|
||||||
|
expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
clickProceed();
|
||||||
|
expect(modal.state.processed).toBeTruthy();
|
||||||
|
clickImport();
|
||||||
|
|
||||||
|
let builds = Persist.getBuilds();
|
||||||
|
|
||||||
|
for (let s in builds) {
|
||||||
|
for (let b in builds[s]) {
|
||||||
|
expect(builds[s][b]).toEqual(expectedBuilds[s][b]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Import Companion API Build', function() {
|
||||||
|
|
||||||
|
beforeEach(reset);
|
||||||
|
|
||||||
|
it('imports a valid companion API build', function() {
|
||||||
|
const importData = require('./fixtures/companion-api-import-1');
|
||||||
|
pasteText(JSON.stringify(importData));
|
||||||
|
|
||||||
|
expect(modal.state.importValid).toBeTruthy();
|
||||||
|
expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
expect(modal.state.singleBuild).toBe(true);
|
||||||
|
clickProceed();
|
||||||
|
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||||
|
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/federal_corvette?code=A2putsFklndzsxf50x0x7l28281919040404040402020l06p05sf63c5ifr--v66g2f.AwRj4zNaqA%3D%3D.CwRgDBldUExuBiIlUA%3D%3D.H4sIAAAAAAAAA12STy8DURTFb1szU53Ga8dg2qqqDmJDIoKFxJImumYjVrVqfAALC4lNbcUnkLCoDbEQu0bSlQVhI8JHsJBIQ73rXMkwMYuT9%2Bb87nl%2F7ovoRSL6ikD6TYNINZg5XsWUo7pfrBikr2USlRyXyDuLAhr6ZHanNLOzD5tjOiskysk5dOBvfTB7bjeRW0MNG3ohSBq1bKKxKwyLLUAjmwjpPu4wJx4xVbNI57heDfbUKUAy2xaRUQZpllHoHMHxKqjhhF4LgjtJiFHDmqbrEeVnUJOax7%2FSdRfRwBNotv9wo5kAuZMD2egKyDYcdYl1OBki6z%2BZQjaFnBPyFCM1LefF%2BcgrY0es9FKwbW8ZYj9gmBbxRVRdglMh6BNqnwsk4ouoO4HSIehNoBuBRHwR1QOmsBvHmk6IfMbd2fdCEka%2BjNSexPWGoEkcyX6CnxbxRZQtd%2BPpym%2B31xFtn0iSFPkf%2BBkttZlzB9KDFyBuFRfAGV0Ogoff8SSsCfjjD5hGWtLIwZB%2FgX5Zt%2BLHMI9My7sp6nzgZzekswTxVvCOkq%2FSXqb%2F3zfLxh6HrwIAAA%3D%3D&bn=Imported%20Federal%20Corvette');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('imports a valid companion API build', function() {
|
||||||
|
const importData = require('./fixtures/companion-api-import-2');
|
||||||
|
pasteText(JSON.stringify(importData));
|
||||||
|
|
||||||
|
expect(modal.state.importValid).toBeTruthy();
|
||||||
|
expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
expect(modal.state.singleBuild).toBe(true);
|
||||||
|
clickProceed();
|
||||||
|
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||||
|
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/beluga?code=A0pktsFplCdpsnf70t0t2727270004040404043c4fmimlmm04mc0iv62i2f.AwRj4yukg%3D%3D%3D.CwRgDBldHi8IUA%3D%3D.H4sIAAAAAAAAA2P8Z8%2FAwPCXEUiIKTMxMPCv%2F%2Ff%2FP8cFIPGf6Z8YTEr0GjMDg%2FJWICERBOTzn%2Fn7%2F7%2FIO5Ai5n9SIEWsQEIoSxAolfbt%2F3%2BJPk4GBhE7YQYGYVmgcuVnf4Aq%2FwOVAAAyiFctbgAAAA%3D%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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('imports a valid companion API build', function() {
|
||||||
|
const importData = require('./fixtures/companion-api-import-4');
|
||||||
|
pasteText(JSON.stringify(importData));
|
||||||
|
|
||||||
|
expect(modal.state.importValid).toBeTruthy();
|
||||||
|
expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
expect(modal.state.singleBuild).toBe(true);
|
||||||
|
clickProceed();
|
||||||
|
expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||||
|
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/cobra_mk_iii?code=A0p0tdFaldd3sdf4------34---2f2i.AwRj4yKA.CwRgDMYExrezBUg%3D.&bn=Imported%20Cobra%20Mk%20III');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Import E:D Shipyard Builds', function() {
|
||||||
|
|
||||||
|
// it('imports a valid build', function() {
|
||||||
|
// const imports = require('./fixtures/ed-shipyard-import-valid');
|
||||||
|
//
|
||||||
|
// for (let i = 0; i < imports.length; i++ ) {
|
||||||
|
// reset();
|
||||||
|
// let fixture = imports[i];
|
||||||
|
// pasteText(fixture.buildText);
|
||||||
|
// expect(modal.state.importValid).toBeTruthy();
|
||||||
|
// expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
// clickProceed();
|
||||||
|
// expect(MockRouter.go.mock.calls.length).toBe(1);
|
||||||
|
// expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/' + fixture.shipId + '?code=' + encodeURIComponent(fixture.buildCode) + '&bn=' + encodeURIComponent(fixture.buildName));
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
it('catches invalid builds', function() {
|
||||||
|
const imports = require('./fixtures/ed-shipyard-import-invalid');
|
||||||
|
|
||||||
|
for (let i = 0; i < imports.length; i++ ) {
|
||||||
|
reset();
|
||||||
|
pasteText(imports[i].buildText);
|
||||||
|
expect(modal.state.importValid).toBeFalsy();
|
||||||
|
expect(modal.state.errorMsg).toEqual(imports[i].errorMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Imports from a Comparison', function() {
|
||||||
|
|
||||||
|
it('imports a valid comparison', function() {
|
||||||
|
const importBuilds = require('./fixtures/valid-backup').builds;
|
||||||
|
Persist.deleteAll();
|
||||||
|
render = TU.renderIntoDocument(<ContextProvider><ModalImport builds={importBuilds} /></ContextProvider>);
|
||||||
|
modal = TU.findRenderedComponentWithType(render, ModalImport);
|
||||||
|
|
||||||
|
expect(modal.state.processed).toBe(true);
|
||||||
|
expect(modal.state.errorMsg).toEqual(null);
|
||||||
|
clickImport();
|
||||||
|
expect(Persist.getBuilds()).toEqual(importBuilds);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
143
__tests__/test-persist.js
Normal file
143
__tests__/test-persist.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
jest.unmock('../src/app/stores/Persist');
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import TU from 'react-testutils-additions';
|
||||||
|
|
||||||
|
let origAddEventListener = window.addEventListener;
|
||||||
|
let storageListener;
|
||||||
|
let ls = {};
|
||||||
|
|
||||||
|
// Implment mock localStorage
|
||||||
|
let localStorage = {
|
||||||
|
getItem: function(key) {
|
||||||
|
return ls[key];
|
||||||
|
},
|
||||||
|
setItem: function(key, value) {
|
||||||
|
ls[key] = value;
|
||||||
|
},
|
||||||
|
removeItem: function(key) {
|
||||||
|
delete ls[key];
|
||||||
|
},
|
||||||
|
clear: function() {
|
||||||
|
ls = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener = function(eventName, listener) {
|
||||||
|
|
||||||
|
if(eventName == 'storage') {
|
||||||
|
storageListener = listener; // Keep track of latest storage listener
|
||||||
|
} else {
|
||||||
|
origAddEventListener.apply(arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Persist', function() {
|
||||||
|
|
||||||
|
const Persist = require('../src/app/stores/Persist').Persist;
|
||||||
|
|
||||||
|
describe('Multi tab/window', function() {
|
||||||
|
it("syncs builds", function() {
|
||||||
|
window.localStorage = localStorage;
|
||||||
|
ls = {};
|
||||||
|
let p = new Persist();
|
||||||
|
let newBuilds = {
|
||||||
|
anaconda: { test: '1234' }
|
||||||
|
};
|
||||||
|
|
||||||
|
storageListener({ key: 'builds', newValue: JSON.stringify(newBuilds) });
|
||||||
|
expect(p.getBuild('anaconda', 'test')).toBe('1234');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('General and Settings', function() {
|
||||||
|
it("has defaults", function() {
|
||||||
|
window.localStorage = localStorage;
|
||||||
|
ls = {};
|
||||||
|
let p = new Persist();
|
||||||
|
expect(p.getLangCode()).toBe('en');
|
||||||
|
expect(p.showTooltips()).toBe(true);
|
||||||
|
expect(p.getInsurance()).toBe('standard');
|
||||||
|
expect(p.getShipDiscount()).toBe(0);
|
||||||
|
expect(p.getModuleDiscount()).toBe(0);
|
||||||
|
expect(p.getSizeRatio()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads from localStorage correctly", function() {
|
||||||
|
window.localStorage = localStorage;
|
||||||
|
let savedData = require('./fixtures/valid-backup');
|
||||||
|
ls = {};
|
||||||
|
ls.builds = JSON.stringify(savedData.builds);
|
||||||
|
ls.NG_TRANSLATE_LANG_KEY = 'de';
|
||||||
|
ls.insurance = 'Standard';
|
||||||
|
ls.shipDiscount = 0.25;
|
||||||
|
ls.moduleDiscount = 0.15;
|
||||||
|
let p = new Persist();
|
||||||
|
|
||||||
|
expect(p.getInsurance()).toBe('standard');
|
||||||
|
expect(p.getShipDiscount()).toBe(0.25);
|
||||||
|
expect(p.getModuleDiscount()).toBe(0.15);
|
||||||
|
expect(p.getLangCode()).toEqual('de');
|
||||||
|
expect(p.getBuilds('anaconda')).toEqual(savedData.builds.anaconda);
|
||||||
|
expect(p.getBuilds('python')).toEqual(savedData.builds.python);
|
||||||
|
expect(p.getBuildsNamesFor('imperial_clipper')).toEqual(['Cargo', 'Current', 'Dream', 'Multi-purpose']);
|
||||||
|
expect(p.getBuild('type_7_transport', 'Cargo')).toEqual('02A5D5A4D3D3D5C--------0505040403480101');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses defaults from a corrupted localStorage", function() {
|
||||||
|
window.localStorage = localStorage;
|
||||||
|
ls = {};
|
||||||
|
ls.builds = "not valid json";
|
||||||
|
ls.comparisons = "1, 3, 4";
|
||||||
|
ls.insurance = 'this insurance does not exist';
|
||||||
|
ls.shipDiscount = 'this is not a number';
|
||||||
|
ls.moduleDiscount = 10; // Way to big
|
||||||
|
|
||||||
|
let p = new Persist();
|
||||||
|
expect(p.getLangCode()).toBe('en');
|
||||||
|
expect(p.showTooltips()).toBe(true);
|
||||||
|
expect(p.getInsurance()).toBe('standard');
|
||||||
|
expect(p.getShipDiscount()).toBe(0);
|
||||||
|
expect(p.getModuleDiscount()).toBe(0);
|
||||||
|
expect(p.getBuilds()).toEqual({});
|
||||||
|
expect(p.getComparisons()).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works without localStorage", function() {
|
||||||
|
window.localStorage = null;
|
||||||
|
let p = new Persist();
|
||||||
|
expect(p.getLangCode()).toBe('en');
|
||||||
|
expect(p.showTooltips()).toBe(true);
|
||||||
|
expect(p.getInsurance()).toBe('standard');
|
||||||
|
expect(p.getShipDiscount()).toBe(0);
|
||||||
|
expect(p.getModuleDiscount()).toBe(0);
|
||||||
|
expect(p.getSizeRatio()).toBe(1);
|
||||||
|
|
||||||
|
p.saveBuild('anaconda', 'test', '12345');
|
||||||
|
expect(p.getBuild('anaconda', 'test')).toBe('12345');
|
||||||
|
|
||||||
|
p.deleteBuild('anaconda', 'test');
|
||||||
|
expect(p.hasBuilds()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates the backup", function() {
|
||||||
|
window.localStorage = localStorage;
|
||||||
|
let savedData = require('./fixtures/valid-backup');
|
||||||
|
ls = {};
|
||||||
|
ls.builds = JSON.stringify(savedData.builds);
|
||||||
|
ls.insurance = 'Beta';
|
||||||
|
ls.shipDiscount = 0.25;
|
||||||
|
ls.moduleDiscount = 0.15;
|
||||||
|
|
||||||
|
let p = new Persist();
|
||||||
|
let backup = p.getAll();
|
||||||
|
|
||||||
|
expect(backup.insurance).toBe('beta');
|
||||||
|
expect(backup.shipDiscount).toBe(0.25);
|
||||||
|
expect(backup.moduleDiscount).toBe(0.15);
|
||||||
|
expect(backup.builds).toEqual(savedData.builds);
|
||||||
|
expect(backup.comparisons).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
63
__tests__/test-serializer.js
Normal file
63
__tests__/test-serializer.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import Ship from '../src/app/shipyard/Ship';
|
||||||
|
import { Ships } from 'coriolis-data/dist';
|
||||||
|
import * as Serializer from '../src/app/shipyard/Serializer';
|
||||||
|
import jsen from 'jsen';
|
||||||
|
|
||||||
|
describe("Serializer", function() {
|
||||||
|
const anacondaTestExport = require.requireActual('./fixtures/anaconda-test-detailed-export-v4');
|
||||||
|
const code = anacondaTestExport.references[0].code;
|
||||||
|
const anaconda = Ships.anaconda;
|
||||||
|
const validate = jsen(require('../src/schemas/ship-loadout/4'));
|
||||||
|
|
||||||
|
describe("To Detailed Build", function() {
|
||||||
|
let testBuild = new Ship('anaconda', anaconda.properties, anaconda.slots).buildFrom(code);
|
||||||
|
let exportData = Serializer.toDetailedBuild('Test My Ship', testBuild);
|
||||||
|
|
||||||
|
it("conforms to the v4 ship-loadout schema", function() {
|
||||||
|
expect(validate(exportData)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("contains the correct components and stats", function() {
|
||||||
|
expect(exportData.components).toEqual(anacondaTestExport.components);
|
||||||
|
expect(exportData.stats).toEqual(anacondaTestExport.stats);
|
||||||
|
expect(exportData.ship).toEqual(anacondaTestExport.ship);
|
||||||
|
expect(exportData.name).toEqual(anacondaTestExport.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Export Detailed Builds", function() {
|
||||||
|
const expectedExport = require('./fixtures/valid-detailed-export');
|
||||||
|
const builds = require('./fixtures/expected-builds');
|
||||||
|
const exportData = Serializer.toDetailedExport(builds);
|
||||||
|
|
||||||
|
it("conforms to the v4 ship-loadout schema", function() {
|
||||||
|
expect(exportData instanceof Array).toBe(true);
|
||||||
|
|
||||||
|
for (let detailedBuild of exportData) {
|
||||||
|
expect(validate(detailedBuild)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("From Detailed Build", function() {
|
||||||
|
|
||||||
|
it("builds the ship correctly", function() {
|
||||||
|
let testBuildA = new Ship('anaconda', anaconda.properties, anaconda.slots);
|
||||||
|
testBuildA.buildFrom(code);
|
||||||
|
let testBuildB = Serializer.fromDetailedBuild(anacondaTestExport);
|
||||||
|
|
||||||
|
for(var p in testBuildB) {
|
||||||
|
if (p == 'availCS') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
expect(testBuildB[p]).toEqual(testBuildA[p], p + ' does not match');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
156
__tests__/test-ship.js
Normal file
156
__tests__/test-ship.js
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import Ship from '../src/app/shipyard/Ship';
|
||||||
|
import { Ships } from 'coriolis-data/dist';
|
||||||
|
import * as ModuleUtils from '../src/app/shipyard/ModuleUtils';
|
||||||
|
|
||||||
|
describe("Ship", function() {
|
||||||
|
|
||||||
|
it("can build all ships", function() {
|
||||||
|
for (let s in Ships) {
|
||||||
|
let shipData = Ships[s];
|
||||||
|
let ship = new Ship(s, shipData.properties, shipData.slots);
|
||||||
|
|
||||||
|
for (let p in shipData.properties) {
|
||||||
|
expect(ship[p]).toEqual(shipData.properties[p], s + ' property [' + p + '] does not match when built');
|
||||||
|
}
|
||||||
|
|
||||||
|
ship.buildWith(shipData.defaults);
|
||||||
|
|
||||||
|
expect(ship.totalCost).toEqual(shipData.retailCost, s + ' retail cost does not match default build cost');
|
||||||
|
expect(ship.cargoCapacity).toBeDefined();
|
||||||
|
expect(ship.priorityBands[0].retracted).toBeGreaterThan(0, s + ' priorityBands');
|
||||||
|
expect(ship.powerAvailable).toBeGreaterThan(0, s + ' powerAvailable');
|
||||||
|
expect(ship.unladenRange).toBeGreaterThan(0, s + ' unladenRange');
|
||||||
|
expect(ship.ladenRange).toBeGreaterThan(0, s + ' ladenRange');
|
||||||
|
expect(ship.fuelCapacity).toBeGreaterThan(0, s + ' fuelCapacity');
|
||||||
|
expect(ship.unladenFastestRange).toBeGreaterThan(0, s + ' unladenFastestRange');
|
||||||
|
expect(ship.ladenFastestRange).toBeGreaterThan(0, s + ' ladenFastestRange');
|
||||||
|
expect(ship.shield).toBeGreaterThan(0, s + ' shield');
|
||||||
|
expect(ship.armour).toBeGreaterThan(0, s + ' armour');
|
||||||
|
expect(ship.topSpeed).toBeGreaterThan(0, s + ' topSpeed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets and rebuilds properly", function() {
|
||||||
|
var id = 'cobra_mk_iii';
|
||||||
|
var cobra = Ships[id];
|
||||||
|
var shipA = new Ship(id, cobra.properties, cobra.slots);
|
||||||
|
var shipB = new Ship(id, cobra.properties, cobra.slots);
|
||||||
|
var testShip = new Ship(id, cobra.properties, cobra.slots);
|
||||||
|
|
||||||
|
var buildA = cobra.defaults;
|
||||||
|
var buildB = {
|
||||||
|
standard:['4A', '4A', '4A', '3D', '3A', '3A', '4C'],
|
||||||
|
hardpoints: ['0s', '0s', '2d', '2d', 0, '04'],
|
||||||
|
internal: ['45', '03', '2b', '2o', '27', '53']
|
||||||
|
};
|
||||||
|
|
||||||
|
shipA.buildWith(buildA); // Build A
|
||||||
|
shipB.buildWith(buildB);// Build B
|
||||||
|
testShip.buildWith(buildA);
|
||||||
|
|
||||||
|
for(var p in testShip) {
|
||||||
|
if (p == 'availCS') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
expect(testShip[p]).toEqual(shipA[p], p + ' does not match');
|
||||||
|
}
|
||||||
|
|
||||||
|
testShip.buildWith(buildB);
|
||||||
|
|
||||||
|
for(var p in testShip) {
|
||||||
|
if (p == 'availCS') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
expect(testShip[p]).toEqual(shipB[p], p + ' does not match');
|
||||||
|
}
|
||||||
|
|
||||||
|
testShip.buildWith(buildA);
|
||||||
|
|
||||||
|
for(var p in testShip) {
|
||||||
|
if (p == 'availCS') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
expect(testShip[p]).toEqual(shipA[p], p + ' does not match');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("discounts hull and components properly", function() {
|
||||||
|
var id = 'cobra_mk_iii';
|
||||||
|
var cobra = Ships[id];
|
||||||
|
var testShip = new Ship(id, cobra.properties, cobra.slots);
|
||||||
|
testShip.buildWith(cobra.defaults);
|
||||||
|
|
||||||
|
var originalHullCost = testShip.hullCost;
|
||||||
|
var originalTotalCost = testShip.totalCost;
|
||||||
|
var discount = 0.1;
|
||||||
|
|
||||||
|
expect(testShip.m.discountedCost).toEqual(originalHullCost, 'Hull cost does not match');
|
||||||
|
|
||||||
|
testShip.applyDiscounts(discount, discount);
|
||||||
|
|
||||||
|
// Floating point errors cause miniscule decimal places which are handled in the app by rounding/formatting
|
||||||
|
|
||||||
|
expect(Math.floor(testShip.m.discountedCost)).toEqual(Math.floor(originalHullCost * (1 - discount)), 'Discounted Hull cost does not match');
|
||||||
|
expect(Math.floor(testShip.totalCost)).toEqual(Math.floor(originalTotalCost * (1 - discount)), 'Discounted Total cost does not match');
|
||||||
|
|
||||||
|
testShip.applyDiscounts(0, 0); // No discount, 100% of cost
|
||||||
|
|
||||||
|
expect(testShip.m.discountedCost).toEqual(originalHullCost, 'Hull cost does not match');
|
||||||
|
expect(testShip.totalCost).toEqual(originalTotalCost, 'Total cost does not match');
|
||||||
|
|
||||||
|
testShip.applyDiscounts(discount, 0); // Only discount hull
|
||||||
|
|
||||||
|
expect(Math.floor(testShip.m.discountedCost)).toEqual(Math.round(originalHullCost * (1 - discount)), 'Discounted Hull cost does not match');
|
||||||
|
expect(testShip.totalCost).toEqual(originalTotalCost - originalHullCost + testShip.m.discountedCost, 'Total cost does not match');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enforces a single shield generator", function() {
|
||||||
|
var id = 'anaconda';
|
||||||
|
var anacondaData = Ships[id];
|
||||||
|
var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots);
|
||||||
|
anaconda.buildWith(anacondaData.defaults);
|
||||||
|
|
||||||
|
expect(anaconda.internal[2].m.grp).toEqual('sg', 'Anaconda default shield generator slot');
|
||||||
|
|
||||||
|
anaconda.use(anaconda.internal[1], ModuleUtils.internal('4j')); // 6E Shield Generator
|
||||||
|
|
||||||
|
expect(anaconda.internal[2].m).toEqual(null, 'Anaconda default shield generator slot is empty');
|
||||||
|
expect(anaconda.internal[1].m.id).toEqual('4j', 'Slot 1 should have SG 4j in it');
|
||||||
|
expect(anaconda.internal[1].m.grp).toEqual('sg','Slot 1 should have SG 4j in it');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enforces a single shield fuel scoop", function() {
|
||||||
|
var id = 'anaconda';
|
||||||
|
var anacondaData = Ships[id];
|
||||||
|
var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots);
|
||||||
|
anaconda.buildWith(anacondaData.defaults);
|
||||||
|
|
||||||
|
anaconda.use(anaconda.internal[4], ModuleUtils.internal('32')); // 4A Fuel Scoop
|
||||||
|
expect(anaconda.internal[4].m.grp).toEqual('fs', 'Anaconda fuel scoop slot');
|
||||||
|
|
||||||
|
anaconda.use(anaconda.internal[3], ModuleUtils.internal('32'));
|
||||||
|
|
||||||
|
expect(anaconda.internal[4].m).toEqual(null, 'Anaconda original fuel scoop slot is empty');
|
||||||
|
expect(anaconda.internal[3].m.id).toEqual('32', 'Slot 1 should have FS 32 in it');
|
||||||
|
expect(anaconda.internal[3].m.grp).toEqual('fs','Slot 1 should have FS 32 in it');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enforces a single refinery", function() {
|
||||||
|
var id = 'anaconda';
|
||||||
|
var anacondaData = Ships[id];
|
||||||
|
var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots);
|
||||||
|
anaconda.buildWith(anacondaData.defaults);
|
||||||
|
|
||||||
|
anaconda.use(anaconda.internal[4], ModuleUtils.internal('23')); // 4E Refinery
|
||||||
|
expect(anaconda.internal[4].m.grp).toEqual('rf', 'Anaconda refinery slot');
|
||||||
|
|
||||||
|
anaconda.use(anaconda.internal[3], ModuleUtils.internal('23'));
|
||||||
|
|
||||||
|
expect(anaconda.internal[4].m).toEqual(null, 'Anaconda original refinery slot is empty');
|
||||||
|
expect(anaconda.internal[3].m.id).toEqual('23', 'Slot 1 should have RF 23 in it');
|
||||||
|
expect(anaconda.internal[3].m.grp).toEqual('rf','Slot 1 should have RF 23 in it');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
25
__tests__/testUtils.js
Normal file
25
__tests__/testUtils.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const TestUtils = {
|
||||||
|
createContextProvider: function(context) {
|
||||||
|
var _contextTypes = {};
|
||||||
|
|
||||||
|
Object.keys(context).forEach(function(key) {
|
||||||
|
_contextTypes[key] = PropTypes.any;
|
||||||
|
});
|
||||||
|
|
||||||
|
return React.createClass({
|
||||||
|
displayName: 'ContextProvider',
|
||||||
|
childContextTypes: _contextTypes,
|
||||||
|
getChildContext() { return context; },
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return React.Children.only(this.props.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default TestUtils;
|
||||||
51
docker-compose.yml
Normal file
51
docker-compose.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
version: '2.2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
coriolis_prod:
|
||||||
|
image: edcd/coriolis:master
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
labels:
|
||||||
|
- "traefik.docker.network=web"
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.basic.frontend.rule=Host:coriolis.io,coriolis.edcd.io"
|
||||||
|
- "traefik.basic.port=80"
|
||||||
|
- "traefik.basic.protocol=http"
|
||||||
|
|
||||||
|
coriolis_dev:
|
||||||
|
image: edcd/coriolis:develop
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
labels:
|
||||||
|
- "traefik.docker.network=web"
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.basic.frontend.rule=Host:beta.coriolis.io,beta.coriolis.edcd.io"
|
||||||
|
- "traefik.basic.port=80"
|
||||||
|
- "traefik.basic.protocol=http"
|
||||||
|
|
||||||
|
coriolis_dw2:
|
||||||
|
image: edcd/coriolis:dw2
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
labels:
|
||||||
|
- "traefik.docker.network=web"
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.basic.frontend.rule=Host:dw2.coriolis.io"
|
||||||
|
- "traefik.basic.port=80"
|
||||||
|
- "traefik.basic.protocol=http"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
96
nginx.conf
Normal file
96
nginx.conf
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
worker_processes 1;
|
||||||
|
user nobody nobody;
|
||||||
|
error_log /tmp/error.log;
|
||||||
|
pid /tmp/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
sendfile on;
|
||||||
|
client_body_temp_path /tmp/client_body;
|
||||||
|
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||||
|
proxy_temp_path /tmp/proxy_temp;
|
||||||
|
scgi_temp_path /tmp/scgi_temp;
|
||||||
|
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||||
|
access_log /tmp/access.log;
|
||||||
|
error_log /tmp/error.log;
|
||||||
|
|
||||||
|
# https://nginx.org/en/docs/http/ngx_http_gzip_module.html
|
||||||
|
# Enable gzip compression.
|
||||||
|
# Default: off
|
||||||
|
gzip off;
|
||||||
|
|
||||||
|
# Compression level (1-9).
|
||||||
|
# 5 is a perfect compromise between size and CPU usage, offering about
|
||||||
|
# 75% reduction for most ASCII files (almost identical to level 9).
|
||||||
|
# Default: 1
|
||||||
|
gzip_comp_level 5;
|
||||||
|
|
||||||
|
# Don't compress anything that's already small and unlikely to shrink much
|
||||||
|
# if at all (the default is 20 bytes, which is bad as that usually leads to
|
||||||
|
# larger files after gzipping).
|
||||||
|
# Default: 20
|
||||||
|
gzip_min_length 256;
|
||||||
|
|
||||||
|
# Compress data even for clients that are connecting to us via proxies,
|
||||||
|
# identified by the "Via" header (required for CloudFront).
|
||||||
|
# Default: off
|
||||||
|
gzip_proxied any;
|
||||||
|
|
||||||
|
# Tell proxies to cache both the gzipped and regular version of a resource
|
||||||
|
# whenever the client's Accept-Encoding capabilities header varies;
|
||||||
|
# Avoids the issue where a non-gzip capable client (which is extremely rare
|
||||||
|
# today) would display gibberish if their proxy gave them the gzipped version.
|
||||||
|
# Default: off
|
||||||
|
gzip_vary on;
|
||||||
|
|
||||||
|
# Compress all output labeled with one of the following MIME-types.
|
||||||
|
# text/html is always compressed by gzip module.
|
||||||
|
# Default: text/html
|
||||||
|
gzip_types *;
|
||||||
|
brotli on;
|
||||||
|
# brotli_static on;
|
||||||
|
brotli_types *;
|
||||||
|
# This should be turned on if you are going to have pre-compressed copies (.gz) of
|
||||||
|
# static files available. If not it should be left off as it will cause extra I/O
|
||||||
|
# for the check. It is best if you enable this in a location{} block for
|
||||||
|
# a specific directory, or on an individual server{} level.
|
||||||
|
# gzip_static on;
|
||||||
|
keepalive_timeout 3000;
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
index index.html;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
autoindex on;
|
||||||
|
|
||||||
|
location ~* \.(?:manifest|appcache|html?|xml|json|css|js|map|jpg|jpeg|gif|png|ico|svg|eot|ttf|woff|woff2)$ {
|
||||||
|
add_header Access-Control-Allow-Origin *;
|
||||||
|
add_header Access-Control-Allow-Credentials true;
|
||||||
|
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
|
||||||
|
add_header Access-Control-Allow-Headers 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
location /service-worker.js {
|
||||||
|
expires -1;
|
||||||
|
add_header Access-Control-Allow-Origin *;
|
||||||
|
add_header Access-Control-Allow-Credentials true;
|
||||||
|
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
|
||||||
|
add_header Access-Control-Allow-Headers 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html =404;
|
||||||
|
}
|
||||||
|
location /iframe.html {
|
||||||
|
try_files $uri $uri/ /iframe.html =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30544
package-lock.json
generated
30544
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -15,12 +15,45 @@
|
|||||||
"clean": "rimraf build",
|
"clean": "rimraf build",
|
||||||
"start": "node devServer.js",
|
"start": "node devServer.js",
|
||||||
"lint": "eslint --ext .js,.jsx src",
|
"lint": "eslint --ext .js,.jsx src",
|
||||||
|
"test": "jest",
|
||||||
"prod-serve": "nginx -p $(pwd) -c nginx.conf",
|
"prod-serve": "nginx -p $(pwd) -c nginx.conf",
|
||||||
"prod-stop": "kill -QUIT $(cat nginx.pid)",
|
"prod-stop": "kill -QUIT $(cat nginx.pid)",
|
||||||
"build": "npm run clean && cross-env NODE_ENV=production webpack -p --config webpack.config.prod.js",
|
"build": "npm run clean && cross-env NODE_ENV=production webpack -p --config webpack.config.prod.js",
|
||||||
"rsync": "rsync -ae \"ssh -i $CORIOLIS_PEM\" --delete build/ $CORIOLIS_USER@$CORIOLIS_HOST:~/wwws",
|
"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"
|
"deploy": "npm run lint && npm test && npm run build && npm run rsync"
|
||||||
},
|
},
|
||||||
|
"jest": {
|
||||||
|
"transform": {
|
||||||
|
".*": "<rootDir>/node_modules/babel-jest"
|
||||||
|
},
|
||||||
|
"testRegex": "(/__tests__/test-.*|\\.(test|spec))\\.js$",
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"jsx"
|
||||||
|
],
|
||||||
|
"automock": true,
|
||||||
|
"bail": false,
|
||||||
|
"unmockedModulePathPatterns": [
|
||||||
|
"<rootDir>/node_modules/lodash",
|
||||||
|
"<rootDir>/node_modules/react",
|
||||||
|
"<rootDir>/node_modules/react-dom",
|
||||||
|
"<rootDir>/node_modules/react-transition-group",
|
||||||
|
"<rootDir>/node_modules/react-testutils-additions",
|
||||||
|
"<rootDir>/node_modules/fbjs",
|
||||||
|
"<rootDir>/node_modules/fbemitter",
|
||||||
|
"<rootDir>/node_modules/classnames",
|
||||||
|
"<rootDir>/node_modules/d3",
|
||||||
|
"<rootDir>/node_modules/lz-string",
|
||||||
|
"<rootDir>/node_modules/jsen",
|
||||||
|
"coriolis-data",
|
||||||
|
"<rootDir>/src/app/shipyard",
|
||||||
|
"<rootDir>/src/app/i18n",
|
||||||
|
"<rootDir>/src/app/utils",
|
||||||
|
"<rootDir>/src/schemas",
|
||||||
|
"<rootDir>/__tests__"
|
||||||
|
]
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.0.0",
|
"@babel/core": "^7.0.0",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||||
@@ -44,6 +77,7 @@
|
|||||||
"appcache-webpack-plugin": "^1.4.0",
|
"appcache-webpack-plugin": "^1.4.0",
|
||||||
"babel-core": "^7.0.0-bridge.0",
|
"babel-core": "^7.0.0-bridge.0",
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.0.1",
|
||||||
|
"babel-jest": "^23.6.0",
|
||||||
"babel-loader": "^8.0.0",
|
"babel-loader": "^8.0.0",
|
||||||
"copy-webpack-plugin": "^4.5.2",
|
"copy-webpack-plugin": "^4.5.2",
|
||||||
"create-react-class": "^15.6.3",
|
"create-react-class": "^15.6.3",
|
||||||
@@ -64,6 +98,7 @@
|
|||||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||||
"file-loader": "^2.0.0",
|
"file-loader": "^2.0.0",
|
||||||
"html-webpack-plugin": "^3.0.7",
|
"html-webpack-plugin": "^3.0.7",
|
||||||
|
"jest-cli": "^23.6.0",
|
||||||
"jsen": "^0.6.4",
|
"jsen": "^0.6.4",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"less": "^3.8.1",
|
"less": "^3.8.1",
|
||||||
@@ -88,12 +123,11 @@
|
|||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/polyfill": "^7.0.0",
|
"@babel/polyfill": "^7.0.0",
|
||||||
"auto-bind": "^2.1.1",
|
|
||||||
"browserify-zlib-next": "^1.0.1",
|
"browserify-zlib-next": "^1.0.1",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
"coriolis-data": "../coriolis-data",
|
||||||
"d3": "^5.7.0",
|
"d3": "^5.7.0",
|
||||||
"detect-browser": "^3.0.1",
|
"detect-browser": "^3.0.1",
|
||||||
"ed-forge": "../ed-forge",
|
|
||||||
"fbemitter": "^2.1.1",
|
"fbemitter": "^2.1.1",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.11",
|
||||||
"lz-string": "^1.4.4",
|
"lz-string": "^1.4.4",
|
||||||
@@ -104,7 +138,7 @@
|
|||||||
"react-extras": "^0.7.1",
|
"react-extras": "^0.7.1",
|
||||||
"react-fuzzy": "^0.5.2",
|
"react-fuzzy": "^0.5.2",
|
||||||
"react-ga": "^2.5.3",
|
"react-ga": "^2.5.3",
|
||||||
"react-number-editor": "^4.0.3",
|
"react-number-editor": "Athanasius/react-number-editor.git#miggy",
|
||||||
"recharts": "^1.2.0",
|
"recharts": "^1.2.0",
|
||||||
"register-service-worker": "^1.5.2",
|
"register-service-worker": "^1.5.2",
|
||||||
"superagent": "^3.8.3"
|
"superagent": "^3.8.3"
|
||||||
|
|||||||
@@ -5,20 +5,26 @@ import { register } from 'register-service-worker';
|
|||||||
import { EventEmitter } from 'fbemitter';
|
import { EventEmitter } from 'fbemitter';
|
||||||
import { getLanguage } from './i18n/Language';
|
import { getLanguage } from './i18n/Language';
|
||||||
import Persist from './stores/Persist';
|
import Persist from './stores/Persist';
|
||||||
import { Ship } from 'ed-forge';
|
|
||||||
|
|
||||||
import Announcement from './components/Announcement';
|
import Announcement from './components/Announcement';
|
||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
import Tooltip from './components/Tooltip';
|
import Tooltip from './components/Tooltip';
|
||||||
|
import ModalExport from './components/ModalExport';
|
||||||
import ModalHelp from './components/ModalHelp';
|
import ModalHelp from './components/ModalHelp';
|
||||||
import ModalImport from './components/ModalImport';
|
import ModalImport from './components/ModalImport';
|
||||||
import ModalPermalink from './components/ModalPermalink';
|
import ModalPermalink from './components/ModalPermalink';
|
||||||
|
import * as CompanionApiUtils from './utils/CompanionApiUtils';
|
||||||
|
import * as JournalUtils from './utils/JournalUtils';
|
||||||
import AboutPage from './pages/AboutPage';
|
import AboutPage from './pages/AboutPage';
|
||||||
import NotFoundPage from './pages/NotFoundPage';
|
import NotFoundPage from './pages/NotFoundPage';
|
||||||
import OutfittingPage from './pages/OutfittingPage';
|
import OutfittingPage from './pages/OutfittingPage';
|
||||||
|
import ComparisonPage from './pages/ComparisonPage';
|
||||||
import ShipyardPage from './pages/ShipyardPage';
|
import ShipyardPage from './pages/ShipyardPage';
|
||||||
import ErrorDetails from './pages/ErrorDetails';
|
import ErrorDetails from './pages/ErrorDetails';
|
||||||
|
|
||||||
|
const zlib = require('pako');
|
||||||
|
const request = require('superagent');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coriolis App
|
* Coriolis App
|
||||||
*/
|
*/
|
||||||
@@ -55,6 +61,7 @@ export default class Coriolis extends React.Component {
|
|||||||
this._onLanguageChange = this._onLanguageChange.bind(this);
|
this._onLanguageChange = this._onLanguageChange.bind(this);
|
||||||
this._onSizeRatioChange = this._onSizeRatioChange.bind(this);
|
this._onSizeRatioChange = this._onSizeRatioChange.bind(this);
|
||||||
this._keyDown = this._keyDown.bind(this);
|
this._keyDown = this._keyDown.bind(this);
|
||||||
|
this._importBuild = this._importBuild.bind(this);
|
||||||
|
|
||||||
this.emitter = new EventEmitter();
|
this.emitter = new EventEmitter();
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -65,14 +72,16 @@ export default class Coriolis extends React.Component {
|
|||||||
route: {},
|
route: {},
|
||||||
sizeRatio: Persist.getSizeRatio()
|
sizeRatio: Persist.getSizeRatio()
|
||||||
};
|
};
|
||||||
// TODO: New mechanism for announcements
|
this._getAnnouncements();
|
||||||
// this._getAnnouncements();
|
|
||||||
Router('', (r) => this._setPage(ShipyardPage, r));
|
Router('', (r) => this._setPage(ShipyardPage, r));
|
||||||
Router('/import?', (r) => this._importBuild(r));
|
Router('/import?', (r) => this._importBuild(r));
|
||||||
Router('/import/:data', (r) => this._importBuild(r));
|
Router('/import/:data', (r) => this._importBuild(r));
|
||||||
Router('/outfit/?', (r) => this._setPage(OutfittingPage, r));
|
Router('/outfit/?', (r) => this._setPage(OutfittingPage, r));
|
||||||
Router('/outfit/:ship/?', (r) => this._setPage(OutfittingPage, r));
|
Router('/outfit/:ship/?', (r) => this._setPage(OutfittingPage, r));
|
||||||
Router('/outfit/:ship/:code?', (r) => this._setPage(OutfittingPage, r));
|
Router('/outfit/:ship/:code?', (r) => this._setPage(OutfittingPage, r));
|
||||||
|
Router('/compare/:name?', (r) => this._setPage(ComparisonPage, r));
|
||||||
|
Router('/comparison?', (r) => this._setPage(ComparisonPage, r));
|
||||||
|
Router('/comparison/:code', (r) => this._setPage(ComparisonPage, r));
|
||||||
Router('/about', (r) => this._setPage(AboutPage, r));
|
Router('/about', (r) => this._setPage(AboutPage, r));
|
||||||
Router('*', (r) => this._setPage(null, r));
|
Router('*', (r) => this._setPage(null, r));
|
||||||
}
|
}
|
||||||
@@ -84,24 +93,33 @@ export default class Coriolis extends React.Component {
|
|||||||
_importBuild(r) {
|
_importBuild(r) {
|
||||||
try {
|
try {
|
||||||
// Need to decode and gunzip the data, then build the ship
|
// Need to decode and gunzip the data, then build the ship
|
||||||
let ship = new Ship(r.params.data);
|
const data = zlib.inflate(new Buffer(r.params.data, 'base64'), { to: 'string' });
|
||||||
r.params.ship = ship.getShipType();
|
const json = JSON.parse(data);
|
||||||
r.params.code = ship.compress();
|
console.info('Ship import data: ');
|
||||||
this._setPage(OutfittingPage, r);
|
console.info(json);
|
||||||
|
let ship;
|
||||||
|
if (json && json.modules) {
|
||||||
|
ship = CompanionApiUtils.shipFromJson(json);
|
||||||
|
} else if (json && json.Modules) {
|
||||||
|
ship = JournalUtils.shipFromLoadoutJSON(json);
|
||||||
|
}
|
||||||
|
r.params.ship = ship.id;
|
||||||
|
r.params.code = ship.toString();
|
||||||
|
this._setPage(OutfittingPage, r)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._onError('Failed to import ship', r.path, 0, 0, err);
|
this._onError('Failed to import ship', r.path, 0, 0, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// async _getAnnouncements() {
|
async _getAnnouncements() {
|
||||||
// try {
|
try {
|
||||||
// const announces = await request.get('https://orbis.zone/api/announcement')
|
const announces = await request.get('https://orbis.zone/api/announcement')
|
||||||
// .query({ showInCoriolis: true });
|
.query({ showInCoriolis: true });
|
||||||
// this.setState({ announcements: announces.body });
|
this.setState({ announcements: announces.body });
|
||||||
// } catch (err) {
|
} catch (err) {
|
||||||
// console.error(err)
|
console.error(err)
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates / Sets the page and route context
|
* Updates / Sets the page and route context
|
||||||
@@ -376,19 +394,18 @@ export default class Coriolis extends React.Component {
|
|||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
let currentMenu = this.state.currentMenu;
|
let currentMenu = this.state.currentMenu;
|
||||||
|
|
||||||
return <div style={{ minHeight: '100%' }} onClick={this._closeMenu}
|
return <div style={{ minHeight: '100%' }} onClick={this._closeMenu}
|
||||||
className={this.state.noTouch ? 'no-touch' : null}
|
className={this.state.noTouch ? 'no-touch' : null}>
|
||||||
>
|
|
||||||
<Header announcements={this.state.announcements} appCacheUpdate={this.state.appCacheUpdate}
|
<Header announcements={this.state.announcements} appCacheUpdate={this.state.appCacheUpdate}
|
||||||
currentMenu={currentMenu}/>
|
currentMenu={currentMenu}/>
|
||||||
{/* <div className="announcement-container">{this.state.announcements.map(a => <Announcement
|
<div className="announcement-container">{this.state.announcements.map(a => <Announcement
|
||||||
text={a.message}/>)}</div> */}
|
text={a.message}/>)}</div>
|
||||||
{this.state.error ? this.state.error : this.state.page ? React.createElement(this.state.page, { currentMenu }) :
|
{this.state.error ? this.state.error : this.state.page ? React.createElement(this.state.page, { currentMenu }) :
|
||||||
<NotFoundPage/>}
|
<NotFoundPage/>}
|
||||||
{this.state.modal}
|
{this.state.modal}
|
||||||
{this.state.tooltip}
|
{this.state.tooltip}
|
||||||
<footer>
|
<footer>
|
||||||
|
|
||||||
<div className="right cap">
|
<div className="right cap">
|
||||||
<a href="https://github.com/EDCD/coriolis" target="_blank" rel="noopener noreferrer"
|
<a href="https://github.com/EDCD/coriolis" target="_blank" rel="noopener noreferrer"
|
||||||
title="Coriolis Github Project">{window.CORIOLIS_VERSION} - {window.CORIOLIS_DATE}</a>
|
title="Coriolis Github Project">{window.CORIOLIS_VERSION} - {window.CORIOLIS_DATE}</a>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import Persist from './stores/Persist';
|
import Persist from './stores/Persist';
|
||||||
|
import ReactGA from 'react-ga';
|
||||||
|
|
||||||
|
ReactGA.initialize('UA-55840909-18');
|
||||||
let standalone = undefined;
|
let standalone = undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,7 +74,6 @@ Router.go = function(path, state) {
|
|||||||
gaTrack(path);
|
gaTrack(path);
|
||||||
let ctx = new Context(path, state);
|
let ctx = new Context(path, state);
|
||||||
Router.dispatch(ctx);
|
Router.dispatch(ctx);
|
||||||
|
|
||||||
if (!ctx.unhandled) {
|
if (!ctx.unhandled) {
|
||||||
if (isStandAlone()) {
|
if (isStandAlone()) {
|
||||||
Persist.setState(ctx);
|
Persist.setState(ctx);
|
||||||
@@ -258,8 +259,16 @@ Route.prototype.match = function(path, params) {
|
|||||||
* @param {string} path Path to track
|
* @param {string} path Path to track
|
||||||
*/
|
*/
|
||||||
function gaTrack(path) {
|
function gaTrack(path) {
|
||||||
const _paq = window._paq || [];
|
const match = path.match(/\/outfit\/(.*)(\?code=.*)/);
|
||||||
_paq.push(['trackPageView']);
|
if (match) {
|
||||||
|
if (match[1]) {
|
||||||
|
ReactGA.ga('set', 'contentGroup1', match[1]);
|
||||||
|
}
|
||||||
|
if (match[2]) {
|
||||||
|
ReactGA.ga('set', 'contentGroup2', match[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReactGA.pageview(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { autoBind } from 'react-extras';
|
|||||||
* Announcement component
|
* Announcement component
|
||||||
*/
|
*/
|
||||||
export default class Announcement extends React.Component {
|
export default class Announcement extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
text: PropTypes.string
|
text: PropTypes.string
|
||||||
};
|
};
|
||||||
@@ -26,4 +27,5 @@ export default class Announcement extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return <div className="announcement" >{this.props.text}</div>;
|
return <div className="announcement" >{this.props.text}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,115 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
|
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
|
||||||
import FuzzySearch from 'react-fuzzy';
|
import FuzzySearch from 'react-fuzzy';
|
||||||
import { getModuleInfo } from 'ed-forge/lib/src/data/items';
|
|
||||||
import { get, groupBy, mapValues, sortBy, zip, zipWith } from 'lodash';
|
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
import MODULE_STATS from 'ed-forge/lib/src/module-stats';
|
|
||||||
import { SHOW } from '../shipyard/StatsMapping';
|
|
||||||
|
|
||||||
const PRESS_THRESHOLD = 500; // mouse/touch down threshold
|
const PRESS_THRESHOLD = 500; // mouse/touch down threshold
|
||||||
|
|
||||||
const MOUNT_MAP = {
|
/*
|
||||||
fixed: <MountFixed className={'lg'} />,
|
* Categorisation of module groups
|
||||||
gimbal: <MountGimballed className={'lg'} />,
|
*/
|
||||||
turret: <MountTurret className={'lg'} />,
|
const GRPCAT = {
|
||||||
|
'sg': 'shields',
|
||||||
|
'bsg': 'shields',
|
||||||
|
'psg': 'shields',
|
||||||
|
'scb': 'shields',
|
||||||
|
'cc': 'limpet controllers',
|
||||||
|
'fx': 'limpet controllers',
|
||||||
|
'hb': 'limpet controllers',
|
||||||
|
'pc': 'limpet controllers',
|
||||||
|
'rpl': 'limpet controllers',
|
||||||
|
'pce': 'passenger cabins',
|
||||||
|
'pci': 'passenger cabins',
|
||||||
|
'pcm': 'passenger cabins',
|
||||||
|
'pcq': 'passenger cabins',
|
||||||
|
'fh': 'hangars',
|
||||||
|
'pv': 'hangars',
|
||||||
|
'fs': 'fuel',
|
||||||
|
'ft': 'fuel',
|
||||||
|
'hr': 'structural reinforcement',
|
||||||
|
'mrp': 'structural reinforcement',
|
||||||
|
'bl': 'lasers',
|
||||||
|
'pl': 'lasers',
|
||||||
|
'ul': 'lasers',
|
||||||
|
'ml': 'lasers',
|
||||||
|
'c': 'projectiles',
|
||||||
|
'mc': 'projectiles',
|
||||||
|
'axmc': 'experimental',
|
||||||
|
'fc': 'projectiles',
|
||||||
|
'rfl': 'experimental',
|
||||||
|
'pa': 'projectiles',
|
||||||
|
'rg': 'projectiles',
|
||||||
|
'mr': 'ordnance',
|
||||||
|
'axmr': 'experimental',
|
||||||
|
'rcpl': 'experimental',
|
||||||
|
'dtl': 'experimental',
|
||||||
|
'tbsc': 'experimental',
|
||||||
|
'tbem': 'experimental',
|
||||||
|
'tbrfl': 'experimental',
|
||||||
|
'mahr': 'experimental',
|
||||||
|
'rsl': 'experimental',
|
||||||
|
'tp': 'ordnance',
|
||||||
|
'nl': 'ordnance',
|
||||||
|
'sc': 'scanners',
|
||||||
|
'ss': 'scanners',
|
||||||
|
// Utilities
|
||||||
|
'cs': 'scanners',
|
||||||
|
'kw': 'scanners',
|
||||||
|
'ws': 'scanners',
|
||||||
|
'xs': 'scanners',
|
||||||
|
'ch': 'defence',
|
||||||
|
'po': 'defence',
|
||||||
|
'ec': 'defence',
|
||||||
|
'sfn': 'defence',
|
||||||
|
// Guardian
|
||||||
|
'gpp': 'guardian',
|
||||||
|
'gpc': 'guardian',
|
||||||
|
'gsrp': 'guardian',
|
||||||
|
'ggc': 'guardian',
|
||||||
|
'gfsb': 'guardian',
|
||||||
|
'gmrp': 'guardian',
|
||||||
|
'gsc': 'guardian',
|
||||||
|
'ghrp': 'guardian',
|
||||||
|
|
||||||
|
// Mining
|
||||||
|
'scl': 'mining',
|
||||||
|
'pwa': 'mining',
|
||||||
|
'sdm': 'mining'
|
||||||
|
};
|
||||||
|
// Order here is the order in which items will be shown in the modules menu
|
||||||
|
const CATEGORIES = {
|
||||||
|
// Internals
|
||||||
|
'am': ['am'],
|
||||||
|
'cr': ['cr'],
|
||||||
|
'fi': ['fi'],
|
||||||
|
'fuel': ['ft', 'fs'],
|
||||||
|
'hangars': ['fh', 'pv'],
|
||||||
|
'limpet controllers': ['cc', 'fx', 'hb', 'pc', 'rpl'],
|
||||||
|
'passenger cabins': ['pce', 'pci', 'pcm', 'pcq'],
|
||||||
|
'rf': ['rf'],
|
||||||
|
'shields': ['sg', 'bsg', 'psg', 'scb'],
|
||||||
|
'structural reinforcement': ['hr', 'mrp'],
|
||||||
|
'dc': ['dc'],
|
||||||
|
// Hardpoints
|
||||||
|
'lasers': ['pl', 'ul', 'bl'],
|
||||||
|
'projectiles': ['mc', 'c', 'fc', 'pa', 'rg'],
|
||||||
|
'ordnance': ['mr', 'tp', 'nl'],
|
||||||
|
// Utilities
|
||||||
|
'sb': ['sb'],
|
||||||
|
'hs': ['hs'],
|
||||||
|
'defence': ['ch', 'po', 'ec'],
|
||||||
|
'scanners': ['sc', 'ss', 'cs', 'kw', 'ws'], // Overloaded with internal scanners
|
||||||
|
// Experimental
|
||||||
|
'experimental': ['axmc', 'axmr', 'rfl', 'tbrfl', 'tbsc', 'tbem', 'xs', 'sfn', 'rcpl', 'dtl', 'rsl', 'mahr',],
|
||||||
|
|
||||||
|
// Guardian
|
||||||
|
'guardian': ['gpp', 'gpd', 'gpc', 'ggc', 'gsrp', 'gfsb', 'ghrp', 'gmrp', 'gsc'],
|
||||||
|
|
||||||
|
'mining': ['ml', 'scl', 'pwa', 'sdm', 'abl'],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,13 +117,22 @@ const MOUNT_MAP = {
|
|||||||
*/
|
*/
|
||||||
export default class AvailableModulesMenu extends TranslatedComponent {
|
export default class AvailableModulesMenu extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
modules: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
hideSearch: PropTypes.bool,
|
diffDetails: PropTypes.func,
|
||||||
m: PropTypes.object,
|
m: PropTypes.object,
|
||||||
|
shipMass: PropTypes.number,
|
||||||
warning: PropTypes.func,
|
warning: PropTypes.func,
|
||||||
|
firstSlotId: PropTypes.string,
|
||||||
|
lastSlotId: PropTypes.string,
|
||||||
|
activeSlotId: PropTypes.string,
|
||||||
slotDiv: PropTypes.object
|
slotDiv: PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
shipMass: 0
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
* @param {Object} props React Component properties
|
* @param {Object} props React Component properties
|
||||||
@@ -38,8 +140,10 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
this._hideDiff = this._hideDiff.bind(this);
|
||||||
|
this._showSearch = this._showSearch.bind(this);
|
||||||
this.state = this._initState(props, context);
|
this.state = this._initState(props, context);
|
||||||
|
this.slotItems = [];// Array to hold <li> refs.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,103 +153,165 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
|||||||
* @return {Object} list: Array of React Components, currentGroup Component if any
|
* @return {Object} list: Array of React Components, currentGroup Component if any
|
||||||
*/
|
*/
|
||||||
_initState(props, context) {
|
_initState(props, context) {
|
||||||
const { translate } = context.language;
|
let translate = context.language.translate;
|
||||||
const { m } = props;
|
let { m, warning, shipMass, onSelect, modules, firstSlotId, lastSlotId } = props;
|
||||||
const list = [], fuzzy = [];
|
let list, currentGroup;
|
||||||
let currentGroup;
|
|
||||||
|
|
||||||
const modules = m.getApplicableItems().map(getModuleInfo);
|
let buildGroup = this._buildGroup.bind(
|
||||||
const groups = mapValues(
|
this,
|
||||||
groupBy(modules, (info) => info.meta.group),
|
translate,
|
||||||
(infos) => groupBy(infos, (info) => info.meta.type),
|
|
||||||
);
|
|
||||||
// Build categories sorted by translated category name
|
|
||||||
const groupKeys = sortBy(Object.keys(groups), translate);
|
|
||||||
for (const group of groupKeys) {
|
|
||||||
const groupName = translate(group);
|
|
||||||
if (groupKeys.length > 1) {
|
|
||||||
list.push(<div key={`group-${group}`} className="select-category upp">{groupName}</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
const categories = groups[group];
|
|
||||||
const categoryKeys = sortBy(Object.keys(categories), translate);
|
|
||||||
for (const category of categoryKeys) {
|
|
||||||
const categoryName = translate(category);
|
|
||||||
const infos = categories[category];
|
|
||||||
if (categoryKeys.length > 1) {
|
|
||||||
list.push(<div key={`category-${category}`} className="select-group cap">{categoryName}</div>);
|
|
||||||
}
|
|
||||||
list.push(
|
|
||||||
this._buildGroup(
|
|
||||||
m,
|
m,
|
||||||
category,
|
warning,
|
||||||
infos,
|
shipMass - (m && m.mass ? m.mass : 0),
|
||||||
),
|
(m, event) => {
|
||||||
);
|
this._hideDiff(event);
|
||||||
fuzzy.push(
|
onSelect(m);
|
||||||
...infos.map((info) => {
|
}
|
||||||
const { meta } = info;
|
|
||||||
const mount = meta.mount ? ' ' + translate(meta.mount) : '';
|
|
||||||
return {
|
|
||||||
grp: groupName,
|
|
||||||
cat: categoryName,
|
|
||||||
m: info.proto.Item,
|
|
||||||
name: `${meta.class}${meta.rating}${mount} ${categoryName}`,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
let fuzzy = [];
|
||||||
|
if (modules instanceof Array) {
|
||||||
|
list = buildGroup(modules[0].grp, modules);
|
||||||
|
} else {
|
||||||
|
list = [];
|
||||||
|
// At present time slots with grouped options (Hardpoints and Internal) can be empty
|
||||||
|
if (m) {
|
||||||
|
let emptyId = 'empty';
|
||||||
|
if (this.firstSlotId == null) this.firstSlotId = emptyId;
|
||||||
|
let keyDown = this._keyDown.bind(this, onSelect);
|
||||||
|
list.push(<div className='empty-c upp' key={emptyId} data-id={emptyId} onClick={onSelect.bind(null, null)}
|
||||||
|
onKeyDown={keyDown} tabIndex="0"
|
||||||
|
ref={slotItem => this.slotItems[emptyId] = slotItem}>{translate('empty')}</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { list, currentGroup, fuzzy, trackingFocus: false };
|
list.push(buildGroup(grp, modules[grp]));
|
||||||
|
for (const i of modules[grp]) {
|
||||||
|
let mount = '';
|
||||||
|
if (i.mount === 'F') {
|
||||||
|
mount = 'Fixed';
|
||||||
|
} else if (i.mount === 'G') {
|
||||||
|
mount = 'Gimballed';
|
||||||
|
} else if (i.mount === 'T') {
|
||||||
|
mount = 'Turreted';
|
||||||
|
}
|
||||||
|
const fuzz = { grp, m: i, name: `${i.class}${i.rating}${mount ? ' ' + mount : ''} ${translate(grp)}` };
|
||||||
|
fuzzy.push(fuzz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let trackingFocus = false;
|
||||||
|
return { list, currentGroup, fuzzy, trackingFocus };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate React Components for Module Group
|
* Generate React Components for Module Group
|
||||||
|
* @param {Function} translate Translate function
|
||||||
* @param {Object} mountedModule Mounted Module
|
* @param {Object} mountedModule Mounted Module
|
||||||
* @param {String} category Category key
|
* @param {Function} warningFunc Warning function
|
||||||
|
* @param {number} mass Mass
|
||||||
|
* @param {function} onSelect Select/Mount callback
|
||||||
|
* @param {string} grp Group name
|
||||||
* @param {Array} modules Available modules
|
* @param {Array} modules Available modules
|
||||||
|
* @param {string} firstSlotId id of first slot item
|
||||||
|
* @param {string} lastSlotId id of last slot item
|
||||||
* @return {React.Component} Available Module Group contents
|
* @return {React.Component} Available Module Group contents
|
||||||
*/
|
*/
|
||||||
_buildGroup(mountedModule, category, modules) {
|
_buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules, firstSlotId, lastSlotId) {
|
||||||
const { warning } = this.props;
|
let prevClass = null, prevRating = null, prevName;
|
||||||
const ship = mountedModule.getShip();
|
let elems = [];
|
||||||
const classMapping = groupBy(modules, (info) => info.meta.class);
|
|
||||||
|
|
||||||
const itemsPerClass = Math.max(
|
const sortedModules = modules.sort(this._moduleOrder);
|
||||||
...Object.values(classMapping).map((l) => l.length),
|
|
||||||
);
|
|
||||||
const itemsPerRow = itemsPerClass <= 2 ? 6 : itemsPerClass;
|
|
||||||
// Nested array of <li> elements; will be flattened before being rendered.
|
|
||||||
// Each sub-array represents one row in the final view.
|
|
||||||
const elems = [[]];
|
|
||||||
|
|
||||||
// Reverse sort for descending order of module class
|
// Calculate the number of items per class. Used so we don't have long lists with only a few items in each row
|
||||||
for (const clazz of Object.keys(classMapping).sort().reverse()) {
|
const tmp = sortedModules.map((v, i) => v['class']).reduce((count, cls) => {
|
||||||
for (let info of sortBy(
|
count[cls] = ++count[cls] || 1;
|
||||||
classMapping[clazz],
|
return count;
|
||||||
(info) => info.meta.mount || info.meta.rating,
|
}, {});
|
||||||
)) {
|
const itemsPerClass = Math.max.apply(null, Object.keys(tmp).map(key => tmp[key]));
|
||||||
const { meta } = info;
|
|
||||||
const { Item } = info.proto;
|
|
||||||
|
|
||||||
// Can only be true if shieldgenmaximalmass is defined, i.e. this
|
let itemsOnThisRow = 0;
|
||||||
// module must be a shield generator
|
for (let i = 0; i < sortedModules.length; i++) {
|
||||||
let disabled = info.props.shieldgenmaximalmass < ship.readProp('hullmass');
|
let m = sortedModules[i];
|
||||||
if (meta.experimental && !mountedModule.readMeta('experimental')) {
|
let mount = null;
|
||||||
disabled =
|
let disabled = false;
|
||||||
4 <=
|
prevName = m.name;
|
||||||
ship.getHardpoints().filter((m) => m.readMeta('experimental'))
|
if (ModuleUtils.isShieldGenerator(m.grp)) {
|
||||||
.length;
|
// 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),
|
||||||
|
active,
|
||||||
|
disabled
|
||||||
|
});
|
||||||
|
let eventHandlers;
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
eventHandlers = {
|
||||||
|
onKeyDown: this._keyDown.bind(this, null),
|
||||||
|
onKeyUp: this._keyUp.bind(this, null)
|
||||||
|
|
||||||
// Default event handlers for objects that are disabled
|
|
||||||
let eventHandlers = {};
|
|
||||||
if (!disabled) {
|
|
||||||
const showDiff = this._showDiff.bind(this, mountedModule, info);
|
|
||||||
const select = (event) => {
|
|
||||||
this._hideDiff(event);
|
|
||||||
this.props.onSelect(Item);
|
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* Get the ids of the first and last <li> elements in the <ul> that are focusable (i.e. are not active or disabled)
|
||||||
|
* Will be used to keep focus inside the <ul> on Tab and Shift-Tab while it is visible
|
||||||
|
*/
|
||||||
|
if (this.firstSlotId == null) this.firstSlotId = sortedModules[i].id;
|
||||||
|
if (active) this.activeSlotId = sortedModules[i].id;
|
||||||
|
this.lastSlotId = sortedModules[i].id;
|
||||||
|
|
||||||
|
let showDiff = this._showDiff.bind(this, mountedModule, m);
|
||||||
|
let select = onSelect.bind(null, m);
|
||||||
|
|
||||||
eventHandlers = {
|
eventHandlers = {
|
||||||
onMouseEnter: this._over.bind(this, showDiff),
|
onMouseEnter: this._over.bind(this, showDiff),
|
||||||
@@ -153,100 +319,67 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
|||||||
onTouchEnd: this._touchEnd.bind(this, select),
|
onTouchEnd: this._touchEnd.bind(this, select),
|
||||||
onMouseLeave: this._hideDiff,
|
onMouseLeave: this._hideDiff,
|
||||||
onClick: select,
|
onClick: select,
|
||||||
|
onKeyDown: this._keyDown.bind(this, select),
|
||||||
|
onKeyUp: this._keyUp.bind(this, select)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mountSymbol = MOUNT_MAP[meta.mount];
|
switch (m.mount) {
|
||||||
const li = (
|
case 'F':
|
||||||
<li key={Item} data-id={Item}
|
mount = <MountFixed className={'lg'}/>;
|
||||||
ref={Item === mountedModule.getItem() ? (ref) => { this.activeSlotRef = ref; } : undefined}
|
break;
|
||||||
className={cn(meta.type === 'armour' ? 'lc' : 'c', {
|
case 'G':
|
||||||
warning: !disabled && warning && warning(info),
|
mount = <MountGimballed className={'lg'}/>;
|
||||||
active: mountedModule.getItem() === Item,
|
break;
|
||||||
disabled,
|
case 'T':
|
||||||
hardpoint: mountSymbol,
|
mount = <MountTurret className={'lg'}/>;
|
||||||
})}
|
break;
|
||||||
{...eventHandlers}
|
}
|
||||||
>{mountSymbol}{meta.type === 'armour' ? Item : `${meta.class}${meta.rating}`}</li>
|
if (m.name && m.name === prevName) {
|
||||||
|
// elems.push(<br key={'b' + m.grp + i} />);
|
||||||
|
itemsOnThisRow = 0;
|
||||||
|
}
|
||||||
|
if (itemsOnThisRow == 6 || i > 0 && sortedModules.length > 3 && itemsPerClass > 2 && m.class != prevClass && (m.rating != prevRating || m.mount)) {
|
||||||
|
elems.push(<br key={'b' + m.grp + i}/>);
|
||||||
|
itemsOnThisRow = 0;
|
||||||
|
}
|
||||||
|
let tbIdx = (classes.indexOf('disabled') < 0) ? 0 : undefined;
|
||||||
|
elems.push(
|
||||||
|
<li key={m.id} data-id={m.id} className={classes} {...eventHandlers} tabIndex={tbIdx}
|
||||||
|
ref={slotItem => this.slotItems[m.id] = slotItem}>
|
||||||
|
{mount}
|
||||||
|
{(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}
|
||||||
|
</li>
|
||||||
);
|
);
|
||||||
|
|
||||||
const tail = elems.pop();
|
itemsOnThisRow++;
|
||||||
let newTail = [tail];
|
prevClass = m.class;
|
||||||
if (tail.length < itemsPerRow) {
|
prevRating = m.rating;
|
||||||
// If the row has not grown too long, the new <li> element can be
|
prevName = m.name;
|
||||||
// added to the row itself
|
|
||||||
tail.push(li);
|
|
||||||
} else {
|
|
||||||
// Otherwise, the last row gets a line break element added and this
|
|
||||||
// item is put into a new row
|
|
||||||
tail.push(<br key={elems.length}/>);
|
|
||||||
newTail.push([li]);
|
|
||||||
}
|
}
|
||||||
elems.push(...newTail);
|
return <ul key={'modules' + grp}>{elems}</ul>;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ul key={'ul' + category}>{[].concat(...elems)}</ul>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate tooltip content for the difference between the
|
* Generate tooltip content for the difference between the
|
||||||
* mounted module and the hovered modules
|
* mounted module and the hovered modules
|
||||||
* @param {Object} mountedModule The module mounted currently
|
* @param {Object} mm The module mounet currently
|
||||||
* @param {Object} hoveringModule The hovered module
|
* @param {Object} m The hovered module
|
||||||
* @param {DOMRect} rect DOMRect for target element
|
* @param {DOMRect} rect DOMRect for target element
|
||||||
*/
|
*/
|
||||||
_showDiff(mountedModule, hoveringModule, rect) {
|
_showDiff(mm, m, rect) {
|
||||||
const { tooltip, language } = this.context;
|
if (this.props.diffDetails) {
|
||||||
const { formats, translate, units } = language;
|
|
||||||
this.touchTimeout = null;
|
this.touchTimeout = null;
|
||||||
const mountedIsEmpty = mountedModule.isEmpty();
|
this.context.tooltip(this.props.diffDetails(m, mm), rect);
|
||||||
const props = (
|
|
||||||
mountedIsEmpty ? ['mass'] : Object.keys(hoveringModule.props)
|
|
||||||
).map((prop) => SHOW[prop] ? SHOW[prop].as : prop);
|
|
||||||
const oldProps = mountedIsEmpty ?
|
|
||||||
[{ value: 0 }] :
|
|
||||||
props.map((prop) => mountedModule.getFormatted(prop, false));
|
|
||||||
const newProps = mountedModule.try(() => {
|
|
||||||
mountedModule.setItem(hoveringModule.proto.Item);
|
|
||||||
return props.map((prop) => mountedModule.getFormatted(prop, false));
|
|
||||||
});
|
|
||||||
|
|
||||||
const diffs = zipWith(oldProps, newProps, (oldVal, newVal) => {
|
|
||||||
const { unit, value } = newVal;
|
|
||||||
if (!oldVal.value) {
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
return { value, diff: value - oldVal.value, unit };
|
|
||||||
});
|
|
||||||
const namedDiffs = zip(props, diffs).filter(([_, stat]) => stat !== undefined);
|
|
||||||
namedDiffs.push(['cost', {
|
|
||||||
value: hoveringModule.meta.cost,
|
|
||||||
diff: hoveringModule.meta.cost - (mountedIsEmpty ? 0 : mountedModule.readMeta('cost')),
|
|
||||||
unit: units.CR,
|
|
||||||
}]);
|
|
||||||
|
|
||||||
const tt = <div className='cap' style={{ whiteSpace: 'nowrap' }}>
|
|
||||||
{sortBy(namedDiffs, ([prop, _]) => prop).map(([prop, stats]) => {
|
|
||||||
const { unit, value, diff } = stats;
|
|
||||||
const beneficial = get(MODULE_STATS, [prop, 'higherbetter'], false) === diff > 0;
|
|
||||||
return <div key={prop}>
|
|
||||||
{translate(prop)}: <span className={diff === 0 ? 'disabled' : beneficial ? 'secondary' : 'warning'}>
|
|
||||||
{formats.round(value)} {diff !== 0 && ` (${diff > 0 ? '+' : ''}${formats.round(diff)})`}{unit}
|
|
||||||
</span>
|
|
||||||
</div>;
|
|
||||||
})}
|
|
||||||
</div>;
|
|
||||||
tooltip(tt, rect);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate tooltip content for the difference between the
|
* Generate tooltip content for the difference between the
|
||||||
* mounted module and the hovered modules
|
* mounted module and the hovered modules
|
||||||
* @returns {React.Component} Search component if available
|
|
||||||
*/
|
*/
|
||||||
_showSearch() {
|
_showSearch() {
|
||||||
if (this.props.hideSearch) {
|
if (this.props.modules instanceof Array) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -309,6 +442,41 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
|||||||
this._hideDiff();
|
this._hideDiff();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key down - select module on Enter key, move to next/previous module on Tab/Shift-Tab, close on Esc
|
||||||
|
* @param {Function} select Select module callback
|
||||||
|
* @param {SyntheticEvent} event Event
|
||||||
|
*/
|
||||||
|
_keyDown(select, event) {
|
||||||
|
let className = event.currentTarget.attributes['class'].value;
|
||||||
|
if (event.key == 'Enter' && className.indexOf('disabled') < 0 && className.indexOf('active') < 0) {
|
||||||
|
select();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let elemId = event.currentTarget.attributes['data-id'].value;
|
||||||
|
if (className.indexOf('disabled') < 0 && event.key == 'Tab') {
|
||||||
|
if (event.shiftKey && elemId == this.firstSlotId) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.slotItems[this.lastSlotId].focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!event.shiftKey && elemId == this.lastSlotId) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.slotItems[this.firstSlotId].focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key Up
|
||||||
|
* @param {Function} select Select module callback
|
||||||
|
* @param {SytheticEvent} event Event
|
||||||
|
*/
|
||||||
|
_keyUp(select, event) {
|
||||||
|
// nothing here yet
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide diff tooltip
|
* Hide diff tooltip
|
||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
@@ -319,12 +487,60 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
|||||||
this.context.tooltip();
|
this.context.tooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order two modules suitably for display in module selection
|
||||||
|
* @param {Object} a the first module
|
||||||
|
* @param {Object} b the second module
|
||||||
|
* @return {int} -1 if the first module should go first, 1 if the second module should go first
|
||||||
|
*/
|
||||||
|
_moduleOrder(a, b) {
|
||||||
|
// Named modules go last
|
||||||
|
if (!a.name && b.name) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.name && !b.name) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// Class ordered from highest (8) to lowest (1)
|
||||||
|
if (a.class < b.class) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a.class > b.class) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Mount type, if applicable
|
||||||
|
if (a.mount && b.mount && a.mount !== b.mount) {
|
||||||
|
if (a.mount === 'F' || (a.mount === 'G' && b.mount === 'T')) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Rating ordered from highest (A) to lowest (E)
|
||||||
|
if (a.rating < b.rating) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.rating > b.rating) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// Do not attempt to order by name at this point, as that mucks up the order of armour
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll to mounted (if it exists) module group on mount
|
* Scroll to mounted (if it exists) module group on mount
|
||||||
*/
|
*/
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.activeSlotRef) {
|
if (this.groupElem) { // Scroll to currently selected group
|
||||||
this.activeSlotRef.focus();
|
this.node.scrollTop = this.groupElem.offsetTop;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set focus on active or first slot element, if applicable.
|
||||||
|
*/
|
||||||
|
if (this.slotItems[this.activeSlotId]) {
|
||||||
|
this.slotItems[this.activeSlotId].focus();
|
||||||
|
} else if (this.slotItems[this.firstSlotId]) {
|
||||||
|
this.slotItems[this.firstSlotId].focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boost displays a boost button that toggles bosot
|
* Boost displays a boost button that toggles bosot
|
||||||
@@ -9,6 +8,8 @@ import autoBind from 'auto-bind';
|
|||||||
*/
|
*/
|
||||||
export default class Boost extends TranslatedComponent {
|
export default class Boost extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
marker: PropTypes.string.isRequired,
|
||||||
|
ship: PropTypes.object.isRequired,
|
||||||
boost: PropTypes.bool.isRequired,
|
boost: PropTypes.bool.isRequired,
|
||||||
onChange: PropTypes.func.isRequired
|
onChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
@@ -18,9 +19,12 @@ export default class Boost extends TranslatedComponent {
|
|||||||
* @param {Object} props React Component properties
|
* @param {Object} props React Component properties
|
||||||
* @param {Object} context React Component context
|
* @param {Object} context React Component context
|
||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
const { ship, boost } = props;
|
||||||
|
|
||||||
|
this._keyDown = this._keyDown.bind(this);
|
||||||
|
this._toggleBoost = this._toggleBoost.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,12 +70,13 @@ export default class Boost extends TranslatedComponent {
|
|||||||
* @return {React.Component} contents
|
* @return {React.Component} contents
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { translate } = this.context.language;
|
const { formats, translate, units } = this.context.language;
|
||||||
|
const { ship, boost } = this.props;
|
||||||
|
|
||||||
|
// TODO disable if ship cannot boost
|
||||||
return (
|
return (
|
||||||
<span id='boost'>
|
<span id='boost'>
|
||||||
<button id='boost' className={this.props.boost ? 'selected' : null} onClick={this._toggleBoost}>
|
<button id='boost' className={boost ? 'selected' : null} onClick={this._toggleBoost}>{translate('boost')}</button>
|
||||||
{translate('boost')}
|
|
||||||
</button>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import Slider from '../components/Slider';
|
import Slider from '../components/Slider';
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cargo slider
|
* Cargo slider
|
||||||
@@ -22,7 +21,8 @@ export default class Cargo extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
|
||||||
|
this._cargoChange = this._cargoChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
|
import { Ships } from 'coriolis-data/dist';
|
||||||
import Persist from '../stores/Persist';
|
import Persist from '../stores/Persist';
|
||||||
import { Factory, Ship } from 'ed-forge';
|
import Ship from '../shipyard/Ship';
|
||||||
import { Insurance } from '../shipyard/Constants';
|
import { Insurance } from '../shipyard/Constants';
|
||||||
|
import { slotName, slotComparator } from '../utils/SlotFunctions';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import { ShoppingIcon } from './SvgIcons';
|
import { ShoppingIcon } from '../components/SvgIcons';
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
import { assign, differenceBy, sortBy, reverse } from 'lodash';
|
|
||||||
import { FUEL_CAPACITY } from 'ed-forge/lib/src/ship-stats';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cost Section
|
* Cost Section
|
||||||
@@ -17,7 +16,7 @@ export default class CostSection extends TranslatedComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
ship: PropTypes.object.isRequired,
|
ship: PropTypes.object.isRequired,
|
||||||
code: PropTypes.string.isRequired,
|
code: PropTypes.string.isRequired,
|
||||||
buildName: PropTypes.string,
|
buildName: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,34 +25,71 @@ export default class CostSection extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
this._costsTab = this._costsTab.bind(this);
|
||||||
|
this._sortCost = this._sortCost.bind(this);
|
||||||
|
this._sortAmmo = this._sortAmmo.bind(this);
|
||||||
|
this._sortRetrofit = this._sortRetrofit.bind(this);
|
||||||
|
this._buildRetrofitShip = this._buildRetrofitShip.bind(this);
|
||||||
|
this._onBaseRetrofitChange = this._onBaseRetrofitChange.bind(this);
|
||||||
|
this._defaultRetrofitName = this._defaultRetrofitName.bind(this);
|
||||||
|
this._eddbShoppingList = this._eddbShoppingList.bind(this);
|
||||||
|
|
||||||
|
let data = Ships[props.ship.id]; // Retrieve the basic ship properties, slots and defaults
|
||||||
|
let retrofitName = this._defaultRetrofitName(props.ship.id, props.buildName);
|
||||||
|
let retrofitShip = this._buildRetrofitShip(props.ship.id, retrofitName);
|
||||||
|
let shipDiscount = Persist.getShipDiscount();
|
||||||
|
let moduleDiscount = Persist.getModuleDiscount();
|
||||||
|
|
||||||
|
this.props.ship.applyDiscounts(shipDiscount, moduleDiscount);
|
||||||
|
retrofitShip.applyDiscounts(shipDiscount, moduleDiscount);
|
||||||
|
|
||||||
const { ship, buildName } = props;
|
|
||||||
this.shipType = ship.getShipType();
|
|
||||||
this.state = {
|
this.state = {
|
||||||
retrofitName: Persist.hasBuild(ship.getShipType(), buildName) ? buildName : null,
|
retrofitShip,
|
||||||
shipDiscount: Persist.getShipDiscount(),
|
retrofitName,
|
||||||
moduleDiscount: Persist.getModuleDiscount(),
|
shipDiscount,
|
||||||
|
moduleDiscount,
|
||||||
insurance: Insurance[Persist.getInsurance()],
|
insurance: Insurance[Persist.getInsurance()],
|
||||||
tab: Persist.getCostTab(),
|
tab: Persist.getCostTab(),
|
||||||
buildOptions: Persist.getBuildsNamesFor(ship.getShipType()),
|
buildOptions: Persist.getBuildsNamesFor(props.ship.id),
|
||||||
predicate: 'cr',
|
ammoPredicate: 'cr',
|
||||||
desc: true,
|
ammoDesc: true,
|
||||||
excluded: {},
|
costPredicate: 'cr',
|
||||||
|
costDesc: true,
|
||||||
|
retroPredicate: 'cr',
|
||||||
|
retroDesc: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a ship instance to base/reference retrofit changes from
|
* Create a ship instance to base/reference retrofit changes from
|
||||||
|
* @param {string} shipId Ship Id
|
||||||
|
* @param {string} name Build name
|
||||||
|
* @param {Ship} retrofitShip Existing retrofit ship
|
||||||
* @return {Ship} Retrofit ship
|
* @return {Ship} Retrofit ship
|
||||||
*/
|
*/
|
||||||
_buildRetrofitShip() {
|
_buildRetrofitShip(shipId, name, retrofitShip) {
|
||||||
const { retrofitName } = this.state;
|
let data = Ships[shipId]; // Retrieve the basic ship properties, slots and defaults
|
||||||
if (Persist.hasBuild(this.shipType, retrofitName)) {
|
|
||||||
return new Ship(Persist.getBuild(this.shipType, retrofitName));
|
if (!retrofitShip) { // Don't create a new instance unless needed
|
||||||
} else {
|
retrofitShip = new Ship(shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison
|
||||||
return Factory.newShip(this.shipType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Persist.hasBuild(shipId, name)) {
|
||||||
|
retrofitShip.buildFrom(Persist.getBuild(shipId, name)); // Populate modules from existing build
|
||||||
|
} else {
|
||||||
|
retrofitShip.buildWith(data.defaults); // Populate with default components
|
||||||
|
}
|
||||||
|
return retrofitShip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default retrofit build name if it exists
|
||||||
|
* @param {string} shipId Ship Id
|
||||||
|
* @param {string} name Build name
|
||||||
|
* @return {string} Build name or null
|
||||||
|
*/
|
||||||
|
_defaultRetrofitName(shipId, name) {
|
||||||
|
return Persist.hasBuild(shipId, name) ? name : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,6 +107,9 @@ export default class CostSection extends TranslatedComponent {
|
|||||||
_onDiscountChanged() {
|
_onDiscountChanged() {
|
||||||
let shipDiscount = Persist.getShipDiscount();
|
let shipDiscount = Persist.getShipDiscount();
|
||||||
let moduleDiscount = Persist.getModuleDiscount();
|
let moduleDiscount = Persist.getModuleDiscount();
|
||||||
|
this.props.ship.applyDiscounts(shipDiscount, moduleDiscount);
|
||||||
|
this.state.retrofitShip.applyDiscounts(shipDiscount, moduleDiscount);
|
||||||
|
this._updateRetrofit(this.props.ship, this.state.retrofitShip);
|
||||||
this.setState({ shipDiscount, moduleDiscount });
|
this.setState({ shipDiscount, moduleDiscount });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,33 +126,156 @@ export default class CostSection extends TranslatedComponent {
|
|||||||
* @param {SyntheticEvent} event Build name to base the retrofit ship on
|
* @param {SyntheticEvent} event Build name to base the retrofit ship on
|
||||||
*/
|
*/
|
||||||
_onBaseRetrofitChange(event) {
|
_onBaseRetrofitChange(event) {
|
||||||
this.setState({ retrofitName: event.target.value });
|
let retrofitName = event.target.value;
|
||||||
|
let ship = this.props.ship;
|
||||||
|
|
||||||
|
if (retrofitName) {
|
||||||
|
this.state.retrofitShip.buildFrom(Persist.getBuild(ship.id, retrofitName));
|
||||||
|
} else {
|
||||||
|
this.state.retrofitShip.buildWith(Ships[ship.id].defaults); // Retrofit ship becomes stock build
|
||||||
|
}
|
||||||
|
this._updateRetrofit(ship, this.state.retrofitShip);
|
||||||
|
this.setState({ retrofitName });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle item cost inclusion
|
* On builds changed check to see if the retrofit ship needs
|
||||||
* @param {String} key Key of the row to toggle
|
* to be updated
|
||||||
*/
|
*/
|
||||||
_toggleExcluded(key) {
|
_onBuildsChanged() {
|
||||||
let { excluded } = this.state;
|
let update = false;
|
||||||
excluded = assign({}, excluded);
|
let ship = this.props.ship;
|
||||||
const slotExcluded = excluded[key];
|
let { retrofitName, retrofitShip } = this.state;
|
||||||
excluded[key] = (slotExcluded === undefined ? true : !slotExcluded);
|
|
||||||
this.setState({ excluded });
|
if(!Persist.hasBuild(ship.id, retrofitName)) {
|
||||||
|
retrofitShip.buildWith(Ships[ship.id].defaults); // Retrofit ship becomes stock build
|
||||||
|
this.setState({ retrofitName: null });
|
||||||
|
update = true;
|
||||||
|
} else if (Persist.getBuild(ship.id, retrofitName) != retrofitShip.toString()) {
|
||||||
|
retrofitShip.buildFrom(Persist.getBuild(ship.id, retrofitName)); // Repopulate modules from saved build
|
||||||
|
update = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update) { // Update retrofit comparison
|
||||||
|
this._updateRetrofit(ship, retrofitShip);
|
||||||
|
}
|
||||||
|
// Update list of retrofit base build options
|
||||||
|
this.setState({ buildOptions: Persist.getBuildsNamesFor(ship.id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set list sort predicate
|
* Toggle item cost inclusion in overall total
|
||||||
* @param {string} newPredicate sort predicate
|
* @param {Object} item Cost item
|
||||||
*/
|
*/
|
||||||
_sortBy(newPredicate) {
|
_toggleCost(item) {
|
||||||
let { predicate, desc } = this.state;
|
this.props.ship.setCostIncluded(item, !item.incCost);
|
||||||
|
this.forceUpdate();
|
||||||
if (newPredicate == predicate) {
|
|
||||||
desc = !desc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ predicate: newPredicate, desc });
|
/**
|
||||||
|
* Toggle item cost inclusion in retrofit total
|
||||||
|
* @param {Object} item Cost item
|
||||||
|
*/
|
||||||
|
_toggleRetrofitCost(item) {
|
||||||
|
let retrofitTotal = this.state.retrofitTotal;
|
||||||
|
item.retroItem.incCost = !item.retroItem.incCost;
|
||||||
|
retrofitTotal += item.netCost * (item.retroItem.incCost ? 1 : -1);
|
||||||
|
this.setState({ retrofitTotal });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set cost list sort predicate
|
||||||
|
* @param {string} predicate sort predicate
|
||||||
|
*/
|
||||||
|
_sortCostBy(predicate) {
|
||||||
|
let { costPredicate, costDesc } = this.state;
|
||||||
|
|
||||||
|
if (costPredicate == predicate) {
|
||||||
|
costDesc = !costDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ costPredicate: predicate, costDesc });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort cost list
|
||||||
|
* @param {Ship} ship Ship instance
|
||||||
|
* @param {string} predicate Sort predicate
|
||||||
|
* @param {Boolean} desc Sort descending
|
||||||
|
*/
|
||||||
|
_sortCost(ship, predicate, desc) {
|
||||||
|
let costList = ship.costList;
|
||||||
|
let translate = this.context.language.translate;
|
||||||
|
|
||||||
|
if (predicate == 'm') {
|
||||||
|
costList.sort(slotComparator(translate, null, desc));
|
||||||
|
} else {
|
||||||
|
costList.sort(slotComparator(translate, (a, b) => (a.m.cost || 0) - (b.m.cost || 0), desc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set ammo list sort predicate
|
||||||
|
* @param {string} predicate sort predicate
|
||||||
|
*/
|
||||||
|
_sortAmmoBy(predicate) {
|
||||||
|
let { ammoPredicate, ammoDesc } = this.state;
|
||||||
|
|
||||||
|
if (ammoPredicate == predicate) {
|
||||||
|
ammoDesc = !ammoDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ ammoPredicate: predicate, ammoDesc });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort ammo cost list
|
||||||
|
* @param {Array} ammoCosts Ammo cost list
|
||||||
|
* @param {string} predicate Sort predicate
|
||||||
|
* @param {Boolean} desc Sort descending
|
||||||
|
*/
|
||||||
|
_sortAmmo(ammoCosts, predicate, desc) {
|
||||||
|
let translate = this.context.language.translate;
|
||||||
|
|
||||||
|
if (predicate == 'm') {
|
||||||
|
ammoCosts.sort(slotComparator(translate, null, desc));
|
||||||
|
} else {
|
||||||
|
ammoCosts.sort(slotComparator(translate, (a, b) => a[predicate] - b[predicate], desc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set retrofit list sort predicate
|
||||||
|
* @param {string} predicate sort predicate
|
||||||
|
*/
|
||||||
|
_sortRetrofitBy(predicate) {
|
||||||
|
let { retroPredicate, retroDesc } = this.state;
|
||||||
|
|
||||||
|
if (retroPredicate == predicate) {
|
||||||
|
retroDesc = !retroDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ retroPredicate: predicate, retroDesc });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort retrofit cost list
|
||||||
|
* @param {Array} retrofitCosts Retrofit cost list
|
||||||
|
* @param {string} predicate Sort predicate
|
||||||
|
* @param {Boolean} desc Sort descending
|
||||||
|
*/
|
||||||
|
_sortRetrofit(retrofitCosts, predicate, desc) {
|
||||||
|
let translate = this.context.language.translate;
|
||||||
|
|
||||||
|
if (predicate == 'cr') {
|
||||||
|
retrofitCosts.sort((a, b) => a.netCost - b.netCost);
|
||||||
|
} else {
|
||||||
|
retrofitCosts.sort((a , b) => (a[predicate] ? translate(a[predicate]).toLowerCase() : '').localeCompare(b[predicate] ? translate(b[predicate]).toLowerCase() : ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!desc) {
|
||||||
|
retrofitCosts.reverse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,34 +284,18 @@ export default class CostSection extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
_costsTab() {
|
_costsTab() {
|
||||||
let { ship } = this.props;
|
let { ship } = this.props;
|
||||||
let {
|
let { shipDiscount, moduleDiscount, insurance } = this.state;
|
||||||
excluded, shipDiscount, moduleDiscount, insurance, desc, predicate
|
|
||||||
} = this.state;
|
|
||||||
let { translate, formats, units } = this.context.language;
|
let { translate, formats, units } = this.context.language;
|
||||||
let rows = [];
|
let rows = [];
|
||||||
|
|
||||||
let modules = sortBy(
|
for (let i = 0, l = ship.costList.length; i < l; i++) {
|
||||||
ship.getModules(),
|
let item = ship.costList[i];
|
||||||
(predicate === 'm' ? (m) => m.getItem() : (m) => m.readMeta('cost'))
|
if (item.m && item.m.cost) {
|
||||||
);
|
let toggle = this._toggleCost.bind(this, item);
|
||||||
if (desc) {
|
rows.push(<tr key={i} className={cn('highlight', { disabled: !item.incCost })}>
|
||||||
reverse(modules);
|
<td className='ptr' style={{ width: '1em' }} onClick={toggle}>{item.m.class + item.m.rating}</td>
|
||||||
}
|
<td className='le ptr shorten cap' onClick={toggle}>{slotName(translate, item)}</td>
|
||||||
|
<td className='ri ptr' onClick={toggle}>{formats.int(item.discountedCost)}{units.CR}</td>
|
||||||
let totalCost = 0;
|
|
||||||
for (let module of modules) {
|
|
||||||
const cost = module.readMeta('cost');
|
|
||||||
const slot = module.getSlot();
|
|
||||||
if (cost) {
|
|
||||||
let toggle = this._toggleExcluded.bind(this, slot);
|
|
||||||
const disabled = excluded[slot];
|
|
||||||
if (!disabled) {
|
|
||||||
totalCost += cost;
|
|
||||||
}
|
|
||||||
rows.push(<tr key={slot} className={cn('highlight', { disabled })}>
|
|
||||||
<td className='ptr' style={{ width: '1em' }} onClick={toggle}>{module.getClassRating()}</td>
|
|
||||||
<td className='le ptr shorten cap' onClick={toggle}>{translate(module.readMeta('type'))}</td>
|
|
||||||
<td className='ri ptr' onClick={toggle}>{formats.int(cost * (1 - moduleDiscount))}{units.CR}</td>
|
|
||||||
</tr>);
|
</tr>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,23 +304,23 @@ export default class CostSection extends TranslatedComponent {
|
|||||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className='main'>
|
<tr className='main'>
|
||||||
<th colSpan='2' className='sortable le' onClick={() => this._sortBy('m')}>
|
<th colSpan='2' className='sortable le' onClick={this._sortCostBy.bind(this,'m')}>
|
||||||
{translate('module')}
|
{translate('module')}
|
||||||
{shipDiscount ? <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('ship')} ${formats.pct(-1 * shipDiscount)}]`}</u> : null}
|
{shipDiscount ? <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('ship')} -${formats.pct(shipDiscount)}]`}</u> : null}
|
||||||
{moduleDiscount ? <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} ${formats.pct(-1 * moduleDiscount)}]`}</u> : null}
|
{moduleDiscount ? <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.pct(moduleDiscount)}]`}</u> : null}
|
||||||
</th>
|
</th>
|
||||||
<th className='sortable le' onClick={() => this._sortBy('cr')} >{translate('credits')}</th>
|
<th className='sortable le' onClick={this._sortCostBy.bind(this, 'cr')} >{translate('credits')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows}
|
{rows}
|
||||||
<tr className='ri'>
|
<tr className='ri'>
|
||||||
<td colSpan='2' className='lbl' >{translate('total')}</td>
|
<td colSpan='2' className='lbl' >{translate('total')}</td>
|
||||||
<td className='val'>{formats.int(totalCost)}{units.CR}</td>
|
<td className='val'>{formats.int(ship.totalCost)}{units.CR}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className='ri'>
|
<tr className='ri'>
|
||||||
<td colSpan='2' className='lbl'>{translate('insurance')}</td>
|
<td colSpan='2' className='lbl'>{translate('insurance')}</td>
|
||||||
<td className='val'>{formats.int(totalCost * insurance)}{units.CR}</td>
|
<td className='val'>{formats.int(ship.totalCost * insurance)}{units.CR}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -185,63 +331,14 @@ export default class CostSection extends TranslatedComponent {
|
|||||||
* Open up a window for EDDB with a shopping list of our retrofit components
|
* Open up a window for EDDB with a shopping list of our retrofit components
|
||||||
*/
|
*/
|
||||||
_eddbShoppingList() {
|
_eddbShoppingList() {
|
||||||
const {} = this.state;
|
const { retrofitCosts } = this.state;
|
||||||
const { ship } = this.props;
|
const { ship } = this.props;
|
||||||
|
|
||||||
// Provide unique list of non-PP module EDDB IDs to buy
|
// Provide unique list of non-PP module EDDB IDs to buy
|
||||||
// const modIds = retrofitCosts.filter(item => item.retroItem.incCost && item.buyId && !item.buyPp).map(item => item.buyId).filter((v, i, a) => a.indexOf(v) === i);
|
const modIds = retrofitCosts.filter(item => item.retroItem.incCost && item.buyId && !item.buyPp).map(item => item.buyId).filter((v, i, a) => a.indexOf(v) === i);
|
||||||
|
|
||||||
// Open up the relevant URL
|
// Open up the relevant URL
|
||||||
// TODO:
|
window.open('https://eddb.io/station?m=' + modIds.join(','));
|
||||||
// window.open('https://eddb.io/station?m=' + modIds.join(','));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
_retrofitInfo() {
|
|
||||||
const { ship } = this.props;
|
|
||||||
const { desc, moduleDiscount, predicate, retrofitName, excluded } = this.state;
|
|
||||||
const retrofitShip = this._buildRetrofitShip();
|
|
||||||
|
|
||||||
const currentModules = ship.getModules();
|
|
||||||
const oldModules = retrofitShip.getModules();
|
|
||||||
const buyModules = differenceBy(currentModules, oldModules, (m) => m.getItem());
|
|
||||||
const sellModules = differenceBy(oldModules, currentModules, (m) => m.getItem());
|
|
||||||
|
|
||||||
let modules = [];
|
|
||||||
let totalCost = 0;
|
|
||||||
const addModule = (m, costFactor) => {
|
|
||||||
const key = `${m.getItem()}@${m.getSlot()}`;
|
|
||||||
const cost = costFactor * m.readMeta('cost') * (1 - moduleDiscount);
|
|
||||||
modules.push({
|
|
||||||
key, cost,
|
|
||||||
rating: m.getClassRating(),
|
|
||||||
item: m.readMeta('type'),
|
|
||||||
});
|
|
||||||
if (!excluded[key]) {
|
|
||||||
totalCost += cost;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for (let m of buyModules) {
|
|
||||||
addModule(m, 1);
|
|
||||||
}
|
|
||||||
for (let m of sellModules) {
|
|
||||||
addModule(m, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let _sortF = undefined;
|
|
||||||
switch (predicate) {
|
|
||||||
case 'cr': _sortF = (o) => o.cost; break;
|
|
||||||
case 'm':
|
|
||||||
default: _sortF = (o) => o.item; break;
|
|
||||||
};
|
|
||||||
|
|
||||||
modules = sortBy(modules, _sortF);
|
|
||||||
if (desc) {
|
|
||||||
reverse(modules);
|
|
||||||
}
|
|
||||||
return [totalCost, modules];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -249,52 +346,59 @@ export default class CostSection extends TranslatedComponent {
|
|||||||
* @return {React.Component} Tab contents
|
* @return {React.Component} Tab contents
|
||||||
*/
|
*/
|
||||||
_retrofitTab() {
|
_retrofitTab() {
|
||||||
let { buildOptions, excluded, moduleDiscount, retrofitName } = this.state;
|
let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state;
|
||||||
const { termtip, tooltip } = this.context;
|
const { termtip, tooltip } = this.context;
|
||||||
let { translate, formats, units } = this.context.language;
|
let { translate, formats, units } = this.context.language;
|
||||||
let int = formats.int;
|
let int = formats.int;
|
||||||
|
let rows = [], options = [<option key='stock' value=''>{translate('Stock')}</option>];
|
||||||
|
|
||||||
|
for (let opt of this.state.buildOptions) {
|
||||||
|
options.push(<option key={opt} value={opt}>{opt}</option>);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retrofitCosts.length) {
|
||||||
|
for (let i = 0, l = retrofitCosts.length; i < l; i++) {
|
||||||
|
let item = retrofitCosts[i];
|
||||||
|
rows.push(<tr key={i} className={cn('highlight', { disabled: !item.retroItem.incCost })} onClick={this._toggleRetrofitCost.bind(this, item)}>
|
||||||
|
<td className='ptr' style={{ width: '1em' }}>{item.sellClassRating}</td>
|
||||||
|
<td className='le ptr shorten cap'>{translate(item.sellName)}</td>
|
||||||
|
<td className='ptr' style={{ width: '1em' }}>{item.buyClassRating}</td>
|
||||||
|
<td className='le ptr shorten cap'>{translate(item.buyName)}</td>
|
||||||
|
<td colSpan='2' className={cn('ri ptr', item.retroItem.incCost ? item.netCost > 0 ? 'warning' : 'secondary-disabled' : 'disabled')}>{int(item.netCost)}{units.CR}</td>
|
||||||
|
</tr>);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rows = <tr><td colSpan='7' style={{ padding: '3em 0' }}>{translate('PHRASE_NO_RETROCH')}</td></tr>;
|
||||||
|
}
|
||||||
|
|
||||||
const [retrofitTotal, retrofitInfo] = this._retrofitInfo();
|
|
||||||
return <div>
|
return <div>
|
||||||
<div className='scroll-x'>
|
<div className='scroll-x'>
|
||||||
<table style={{ width: '100%' }}>
|
<table style={{ width: '100%' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className='main'>
|
<tr className='main'>
|
||||||
<th colSpan='2' className='sortable le' onClick={() => this._sortBy('m')}>{translate('module')}</th>
|
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'sellName')}>{translate('sell')}</th>
|
||||||
<th colSpan='2' className='sortable le' onClick={() => this._sortBy('cr')}>
|
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'buyName')}>{translate('buy')}</th>
|
||||||
|
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'cr')}>
|
||||||
{translate('net cost')}
|
{translate('net cost')}
|
||||||
{moduleDiscount ? <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.pct(moduleDiscount)}]`}</u> : null}
|
{moduleDiscount ? <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.pct(moduleDiscount)}]`}</u> : null}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{retrofitInfo.length ?
|
{rows}
|
||||||
retrofitInfo.map((info) => (
|
|
||||||
<tr key={info.key} className={cn('highlight', { disabled: excluded[info.key] })}
|
|
||||||
onClick={() => this._toggleExcluded(info.key)}>
|
|
||||||
<td className='ptr' style={{ width: '1em' }}>{info.rating}</td>
|
|
||||||
<td className='le ptr shorten cap'>{translate(info.item)}</td>
|
|
||||||
<td colSpan="2" className={cn('ri ptr', excluded[info.key] ? 'disabled' : (info.cost < 0 ? 'secondary-disabled' : 'warning'))}>
|
|
||||||
{int(info.cost)}{units.CR}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)) : (
|
|
||||||
<tr><td colSpan='7' style={{ padding: '3em 0' }}>{translate('PHRASE_NO_RETROCH')}</td></tr>
|
|
||||||
)}
|
|
||||||
<tr className='ri'>
|
<tr className='ri'>
|
||||||
<td className='lbl' ><button onClick={this._eddbShoppingList} onMouseOver={termtip.bind(null, 'PHRASE_REFIT_SHOPPING_LIST')} onMouseOut={tooltip.bind(null, null)}><ShoppingIcon className='lg' style={{ fill: 'black' }}/></button></td>
|
<td className='lbl' ><button onClick={this._eddbShoppingList} onMouseOver={termtip.bind(null, 'PHRASE_REFIT_SHOPPING_LIST')} onMouseOut={tooltip.bind(null, null)}><ShoppingIcon className='lg' style={{ fill: 'black' }}/></button></td>
|
||||||
<td className='lbl' >{translate('cost')}</td>
|
<td colSpan='3' className='lbl' >{translate('cost')}</td>
|
||||||
<td colSpan='2' className={cn('val', retrofitTotal > 0 ? 'warning' : 'secondary-disabled')} style={{ borderBottom:'none' }}>
|
<td colSpan='2' className={cn('val', retrofitTotal > 0 ? 'warning' : 'secondary-disabled')} style={{ borderBottom:'none' }}>
|
||||||
{int(retrofitTotal)}{units.CR}
|
{int(retrofitTotal)}{units.CR}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className='ri'>
|
<tr className='ri'>
|
||||||
<td colSpan='2' className='lbl cap' >{translate('retrofit from')}</td>
|
<td colSpan='4' className='lbl cap' >{translate('retrofit from')}</td>
|
||||||
<td className='val cen' style={{ borderRight: 'none', width: '1em' }}><u className='primary-disabled'>▾</u></td>
|
<td className='val cen' style={{ borderRight: 'none', width: '1em' }}><u className='primary-disabled'>▾</u></td>
|
||||||
<td className='val' style={{ borderLeft:'none', padding: 0 }}>
|
<td className='val' style={{ borderLeft:'none', padding: 0 }}>
|
||||||
<select style={{ width: '100%', padding: 0 }} value={retrofitName || translate('Stock')} onChange={this._onBaseRetrofitChange}>
|
<select style={{ width: '100%', padding: 0 }} value={retrofitName || translate('Stock')} onChange={this._onBaseRetrofitChange}>
|
||||||
<option key='stock' value=''>{translate('Stock')}</option>
|
{options}
|
||||||
{buildOptions.map((opt) => <option key={opt} value={opt}>{opt}</option>)}
|
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -304,50 +408,63 @@ export default class CostSection extends TranslatedComponent {
|
|||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Update retrofit costs
|
||||||
* @param {*} modules
|
* @param {Ship} ship Ship instance
|
||||||
|
* @param {Ship} retrofitShip Retrofit Ship instance
|
||||||
*/
|
*/
|
||||||
_ammoInfo() {
|
_updateRetrofit(ship, retrofitShip) {
|
||||||
const { ship } = this.props;
|
let retrofitCosts = [];
|
||||||
const { desc, predicate } = this.state;
|
let retrofitTotal = 0, i, l, item;
|
||||||
|
|
||||||
let info = [{
|
if (ship.bulkheads.m.index != retrofitShip.bulkheads.m.index) {
|
||||||
key: 'fuel',
|
item = {
|
||||||
item: 'Fuel',
|
buyClassRating: ship.bulkheads.m.class + ship.bulkheads.m.rating,
|
||||||
qty: ship.get(FUEL_CAPACITY),
|
buyId: ship.bulkheads.m.eddbID,
|
||||||
unitCost: 50,
|
buyPp: ship.bulkheads.m.pp,
|
||||||
cost: 50 * ship.get(FUEL_CAPACITY),
|
buyName: ship.bulkheads.m.name,
|
||||||
}];
|
sellClassRating: retrofitShip.bulkheads.m.class + retrofitShip.bulkheads.m.rating,
|
||||||
for (let m of ship.getModules()) {
|
sellName: retrofitShip.bulkheads.m.name,
|
||||||
const rebuilds = m.get('bays') * m.get('rebuildsperbay');
|
netCost: ship.bulkheads.discountedCost - retrofitShip.bulkheads.discountedCost,
|
||||||
const ammo = (m.get('ammomaximum') + m.get('ammoclipsize')) || rebuilds;
|
retroItem: retrofitShip.bulkheads
|
||||||
if (ammo) {
|
};
|
||||||
const unitCost = m.readMeta('ammocost');
|
retrofitCosts.push(item);
|
||||||
info.push({
|
if (retrofitShip.bulkheads.incCost) {
|
||||||
key: `restock_${m.getSlot()}`,
|
retrofitTotal += item.netCost;
|
||||||
rating: m.getClassRating(),
|
|
||||||
item: m.readMeta('type'),
|
|
||||||
qty: ammo,
|
|
||||||
unitCost, cost: unitCost * ammo,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _sortF = undefined;
|
for (let g in { standard: 1, internal: 1, hardpoints: 1 }) {
|
||||||
switch (predicate) {
|
let retroSlotGroup = retrofitShip[g];
|
||||||
case 'cr': _sortF = (o) => o.cost; break;
|
let slotGroup = ship[g];
|
||||||
case 'qty': _sortF = (o) => o.qty; break;
|
for (i = 0, l = slotGroup.length; i < l; i++) {
|
||||||
case 'cost': _sortF = (o) => o.unitCost; break;
|
const modId = slotGroup[i].m ? slotGroup[i].m.eddbID : null;
|
||||||
case 'm':
|
const retroModId = retroSlotGroup[i].m ? retroSlotGroup[i].m.eddbID : null;
|
||||||
default: _sortF = (o) => o.item;
|
if (modId != retroModId) {
|
||||||
|
item = { netCost: 0, retroItem: retroSlotGroup[i] };
|
||||||
|
if (slotGroup[i].m) {
|
||||||
|
item.buyId = slotGroup[i].m.eddbID,
|
||||||
|
item.buyPp = slotGroup[i].m.pp,
|
||||||
|
item.buyName = slotGroup[i].m.name || slotGroup[i].m.grp;
|
||||||
|
item.buyClassRating = slotGroup[i].m.class + slotGroup[i].m.rating;
|
||||||
|
item.netCost = slotGroup[i].discountedCost;
|
||||||
|
}
|
||||||
|
if (retroSlotGroup[i].m) {
|
||||||
|
item.sellName = retroSlotGroup[i].m.name || retroSlotGroup[i].m.grp;
|
||||||
|
item.sellClassRating = retroSlotGroup[i].m.class + retroSlotGroup[i].m.rating;
|
||||||
|
item.netCost -= retroSlotGroup[i].discountedCost;
|
||||||
|
}
|
||||||
|
retrofitCosts.push(item);
|
||||||
|
if (retroSlotGroup[i].incCost) {
|
||||||
|
retrofitTotal += item.netCost;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
info = sortBy(info, _sortF);
|
|
||||||
if (desc) {
|
|
||||||
reverse(info);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return info;
|
this.setState({ retrofitCosts, retrofitTotal });
|
||||||
|
this._sortRetrofit(retrofitCosts, this.state.retroPredicate, this.state.retroDesc);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -355,24 +472,20 @@ export default class CostSection extends TranslatedComponent {
|
|||||||
* @return {React.Component} Tab contents
|
* @return {React.Component} Tab contents
|
||||||
*/
|
*/
|
||||||
_ammoTab() {
|
_ammoTab() {
|
||||||
const { excluded } = this.state;
|
let { ammoTotal, ammoCosts } = this.state;
|
||||||
const { translate, formats, units } = this.context.language;
|
let { translate, formats, units } = this.context.language;
|
||||||
const int = formats.int;
|
let int = formats.int;
|
||||||
const rows = [];
|
let rows = [];
|
||||||
|
|
||||||
const ammoInfo = this._ammoInfo();
|
for (let i = 0, l = ammoCosts.length; i < l; i++) {
|
||||||
let total = 0;
|
let item = ammoCosts[i];
|
||||||
for (let i of ammoInfo) {
|
rows.push(<tr key={i} className='highlight'>
|
||||||
const disabled = excluded[i.key];
|
<td style={{ width: '1em' }}>{item.m.class + item.m.rating}</td>
|
||||||
rows.push(<tr key={i.key} onClick={() => this._toggleExcluded(i.key)}
|
<td className='le shorten cap'>{slotName(translate, item)}</td>
|
||||||
className={cn('highlight', { disabled })}>
|
<td className='ri'>{int(item.max)}</td>
|
||||||
<td style={{ width: '1em' }}>{i.rating}</td>
|
<td className='ri'>{int(item.cost)}{units.CR}</td>
|
||||||
<td className='le shorten cap'>{translate(i.item)}</td>
|
<td className='ri'>{int(item.total)}{units.CR}</td>
|
||||||
<td className='ri'>{int(i.qty)}</td>
|
|
||||||
<td className='ri'>{int(i.unitCost)}{units.CR}</td>
|
|
||||||
<td className='ri'>{int(i.cost)}{units.CR}</td>
|
|
||||||
</tr>);
|
</tr>);
|
||||||
total += disabled ? 0 : i.cost;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
@@ -380,17 +493,17 @@ export default class CostSection extends TranslatedComponent {
|
|||||||
<table style={{ width: '100%' }}>
|
<table style={{ width: '100%' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className='main'>
|
<tr className='main'>
|
||||||
<th colSpan='2' className='sortable le' onClick={() => this._sortBy('m')}>{translate('module')}</th>
|
<th colSpan='2' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'm')} >{translate('module')}</th>
|
||||||
<th colSpan='1' className='sortable le' onClick={() => this._sortBy('qty')}>{translate('qty')}</th>
|
<th colSpan='1' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'max')} >{translate('qty')}</th>
|
||||||
<th colSpan='1' className='sortable le' onClick={() => this._sortBy('cost')}>{translate('unit cost')}</th>
|
<th colSpan='1' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'cost')} >{translate('unit cost')}</th>
|
||||||
<th className='sortable le' onClick={() => this._sortBy('cr')}>{translate('subtotal')}</th>
|
<th className='sortable le' onClick={this._sortAmmoBy.bind(this, 'total')}>{translate('subtotal')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows}
|
{rows}
|
||||||
<tr className='ri'>
|
<tr className='ri'>
|
||||||
<td colSpan='4' className='lbl' >{translate('total')}</td>
|
<td colSpan='4' className='lbl' >{translate('total')}</td>
|
||||||
<td className='val'>{int(total)}{units.CR}</td>
|
<td className='val'>{int(ammoTotal)}{units.CR}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -398,6 +511,103 @@ export default class CostSection extends TranslatedComponent {
|
|||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recalculate all ammo costs
|
||||||
|
* @param {Ship} ship Ship instance
|
||||||
|
*/
|
||||||
|
_updateAmmoCosts(ship) {
|
||||||
|
let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, srvs = 0, scoop = false;
|
||||||
|
|
||||||
|
for (let g in { standard: 1, internal: 1, hardpoints: 1 }) {
|
||||||
|
let slotGroup = ship[g];
|
||||||
|
for (let i = 0, l = slotGroup.length; i < l; i++) {
|
||||||
|
if (slotGroup[i].m) {
|
||||||
|
// Special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip
|
||||||
|
q = 0;
|
||||||
|
switch (slotGroup[i].m.grp) {
|
||||||
|
case 'fs': // Skip fuel calculation if scoop present
|
||||||
|
scoop = true;
|
||||||
|
break;
|
||||||
|
case 'scb':
|
||||||
|
q = slotGroup[i].m.getAmmo() + 1;
|
||||||
|
break;
|
||||||
|
case 'am':
|
||||||
|
q = slotGroup[i].m.getAmmo();
|
||||||
|
break;
|
||||||
|
case 'pv':
|
||||||
|
srvs += slotGroup[i].m.getBays();
|
||||||
|
break;
|
||||||
|
case 'fx': case 'hb': case 'cc': case 'pc':
|
||||||
|
limpets = ship.cargoCapacity;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
q = slotGroup[i].m.getClip() + slotGroup[i].m.getAmmo();
|
||||||
|
}
|
||||||
|
// Calculate ammo costs only if a cost is specified
|
||||||
|
if (slotGroup[i].m.ammocost > 0) {
|
||||||
|
item = {
|
||||||
|
m: slotGroup[i].m,
|
||||||
|
max: q,
|
||||||
|
cost: slotGroup[i].m.ammocost,
|
||||||
|
total: q * slotGroup[i].m.ammocost
|
||||||
|
};
|
||||||
|
ammoCosts.push(item);
|
||||||
|
ammoTotal += item.total;
|
||||||
|
}
|
||||||
|
// Add fighters
|
||||||
|
if (slotGroup[i].m.grp === 'fh') {
|
||||||
|
item = {
|
||||||
|
m: slotGroup[i].m,
|
||||||
|
max: slotGroup[i].m.getRebuildsPerBay() * slotGroup[i].m.getBays(),
|
||||||
|
cost: slotGroup[i].m.fightercost,
|
||||||
|
total: slotGroup[i].m.getRebuildsPerBay() * slotGroup[i].m.getBays() * slotGroup[i].m.fightercost
|
||||||
|
};
|
||||||
|
ammoCosts.push(item);
|
||||||
|
ammoTotal += item.total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpets if controllers exist and cargo space available
|
||||||
|
if (limpets > 0) {
|
||||||
|
item = {
|
||||||
|
m: { name: 'limpets', class: '', rating: '' },
|
||||||
|
max: ship.cargoCapacity,
|
||||||
|
cost: 101,
|
||||||
|
total: ship.cargoCapacity * 101
|
||||||
|
};
|
||||||
|
ammoCosts.push(item);
|
||||||
|
ammoTotal += item.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (srvs > 0) {
|
||||||
|
item = {
|
||||||
|
m: { name: 'SRVs', class: '', rating: '' },
|
||||||
|
max: srvs,
|
||||||
|
cost: 1030,
|
||||||
|
total: srvs * 1030
|
||||||
|
};
|
||||||
|
ammoCosts.push(item);
|
||||||
|
ammoTotal += item.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate refuel costs if no scoop present
|
||||||
|
if (!scoop) {
|
||||||
|
item = {
|
||||||
|
m: { name: 'fuel', class: '', rating: '' },
|
||||||
|
max: ship.fuelCapacity,
|
||||||
|
cost: 50,
|
||||||
|
total: ship.fuelCapacity * 50
|
||||||
|
};
|
||||||
|
ammoCosts.push(item);
|
||||||
|
ammoTotal += item.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ ammoTotal, ammoCosts });
|
||||||
|
this._sortAmmo(ammoCosts, this.state.ammoPredicate, this.state.ammoDesc);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add listeners on mount and update costs
|
* Add listeners on mount and update costs
|
||||||
*/
|
*/
|
||||||
@@ -405,7 +615,64 @@ export default class CostSection extends TranslatedComponent {
|
|||||||
this.listeners = [
|
this.listeners = [
|
||||||
Persist.addListener('discounts', this._onDiscountChanged.bind(this)),
|
Persist.addListener('discounts', this._onDiscountChanged.bind(this)),
|
||||||
Persist.addListener('insurance', this._onInsuranceChanged.bind(this)),
|
Persist.addListener('insurance', this._onInsuranceChanged.bind(this)),
|
||||||
|
Persist.addListener('builds', this._onBuildsChanged.bind(this)),
|
||||||
];
|
];
|
||||||
|
this._updateAmmoCosts(this.props.ship);
|
||||||
|
this._updateRetrofit(this.props.ship, this.state.retrofitShip);
|
||||||
|
this._sortCost(this.props.ship);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update state based on property and context changes
|
||||||
|
* @param {Object} nextProps Incoming/Next properties
|
||||||
|
* @param {Object} nextContext Incoming/Next context
|
||||||
|
*/
|
||||||
|
componentWillReceiveProps(nextProps, nextContext) {
|
||||||
|
let retrofitShip = this.state.retrofitShip;
|
||||||
|
|
||||||
|
if (nextProps.ship != this.props.ship) { // Ship has changed
|
||||||
|
let nextId = nextProps.ship.id;
|
||||||
|
let retrofitName = this._defaultRetrofitName(nextId, nextProps.buildName);
|
||||||
|
retrofitShip = this._buildRetrofitShip(nextId, retrofitName, nextId == this.props.ship.id ? retrofitShip : null);
|
||||||
|
this.setState({
|
||||||
|
retrofitShip,
|
||||||
|
retrofitName,
|
||||||
|
buildOptions: Persist.getBuildsNamesFor(nextId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextProps.ship != this.props.ship || nextProps.code != this.props.code) {
|
||||||
|
nextProps.ship.applyDiscounts(Persist.getShipDiscount(), Persist.getModuleDiscount());
|
||||||
|
this._updateAmmoCosts(nextProps.ship);
|
||||||
|
this._updateRetrofit(nextProps.ship, retrofitShip);
|
||||||
|
this._sortCost(nextProps.ship);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort lists before render
|
||||||
|
* @param {Object} nextProps Incoming/Next properties
|
||||||
|
* @param {Object} nextState Incoming/Next state
|
||||||
|
*/
|
||||||
|
componentWillUpdate(nextProps, nextState) {
|
||||||
|
let state = this.state;
|
||||||
|
|
||||||
|
switch (nextState.tab) {
|
||||||
|
case 'ammo':
|
||||||
|
if (state.ammoPredicate != nextState.ammoPredicate || state.ammoDesc != nextState.ammoDesc) {
|
||||||
|
this._sortAmmo(nextState.ammoCosts, nextState.ammoPredicate, nextState.ammoDesc);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'retrofit':
|
||||||
|
if (state.retroPredicate != nextState.retroPredicate || state.retroDesc != nextState.retroDesc) {
|
||||||
|
this._sortRetrofit(nextState.retrofitCosts, nextState.retroPredicate, nextState.retroDesc);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (state.costPredicate != nextState.costPredicate || state.costDesc != nextState.costDesc) {
|
||||||
|
this._sortCost(nextProps.ship, nextState.costPredicate, nextState.costDesc);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
|
import * as Calc from '../shipyard/Calculations';
|
||||||
import PieChart from './PieChart';
|
import PieChart from './PieChart';
|
||||||
import VerticalBarChart from './VerticalBarChart';
|
import VerticalBarChart from './VerticalBarChart';
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
import { ARMOUR_METRICS, MODULE_PROTECTION_METRICS, SHIELD_METRICS } from 'ed-forge/lib/src/ship-stats';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defence information
|
* Defence information
|
||||||
@@ -16,10 +15,12 @@ import { ARMOUR_METRICS, MODULE_PROTECTION_METRICS, SHIELD_METRICS } from 'ed-fo
|
|||||||
*/
|
*/
|
||||||
export default class Defence extends TranslatedComponent {
|
export default class Defence extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
code: PropTypes.string.isRequired,
|
marker: PropTypes.string.isRequired,
|
||||||
ship: PropTypes.object.isRequired,
|
ship: PropTypes.object.isRequired,
|
||||||
opponent: PropTypes.object.isRequired,
|
opponent: PropTypes.object.isRequired,
|
||||||
engagementRange: PropTypes.number.isRequired,
|
engagementrange: PropTypes.number.isRequired,
|
||||||
|
sys: PropTypes.number.isRequired,
|
||||||
|
opponentWep: PropTypes.number.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +29,22 @@ export default class Defence extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,104 +52,187 @@ export default class Defence extends TranslatedComponent {
|
|||||||
* @return {React.Component} contents
|
* @return {React.Component} contents
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { ship } = this.props;
|
const { ship, sys, opponentWep } = this.props;
|
||||||
const { language, tooltip, termtip } = this.context;
|
const { language, tooltip, termtip } = this.context;
|
||||||
const { formats, translate, units } = language;
|
const { formats, translate, units } = language;
|
||||||
|
const { shield, armour, shielddamage, armourdamage } = this.state;
|
||||||
|
|
||||||
const shields = ship.get(SHIELD_METRICS);
|
const pd = ship.standard[4].m;
|
||||||
|
|
||||||
// Data for pie chart (absolute MJ)
|
const shieldSourcesData = [];
|
||||||
const shieldSourcesData = [
|
const effectiveShieldData = [];
|
||||||
'byBoosters', 'byGenerator', 'byReinforcements', 'bySCBs',
|
const shieldDamageTakenData = [];
|
||||||
].map((key) => { return { label: key, value: Math.round(shields[key]) }; });
|
const shieldSourcesTt = [];
|
||||||
|
const shieldDamageTakenAbsoluteTt = [];
|
||||||
|
const shieldDamageTakenExplosiveTt = [];
|
||||||
|
const shieldDamageTakenKineticTt = [];
|
||||||
|
const shieldDamageTakenThermalTt = [];
|
||||||
|
const effectiveShieldAbsoluteTt = [];
|
||||||
|
const effectiveShieldExplosiveTt = [];
|
||||||
|
const effectiveShieldKineticTt = [];
|
||||||
|
const effectiveShieldThermalTt = [];
|
||||||
|
let maxEffectiveShield = 0;
|
||||||
|
if (shield.total) {
|
||||||
|
shieldSourcesData.push({ value: Math.round(shield.generator), label: translate('generator') });
|
||||||
|
shieldSourcesData.push({ value: Math.round(shield.boosters), label: translate('boosters') });
|
||||||
|
shieldSourcesData.push({ value: Math.round(shield.cells), label: translate('cells') });
|
||||||
|
shieldSourcesData.push({ value: Math.round(shield.addition), label: translate('shield addition') });
|
||||||
|
|
||||||
// Data for tooltip
|
if (shield.generator > 0) {
|
||||||
const shieldSourcesTt = shieldSourcesData.map((o) => {
|
shieldSourcesTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
|
||||||
let { label, value } = o;
|
effectiveShieldAbsoluteTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
|
||||||
return <div key={label}>
|
effectiveShieldExplosiveTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
|
||||||
{translate(label)} {formats.int(value)}{units.MJ}
|
effectiveShieldKineticTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
|
||||||
</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>);
|
||||||
|
}
|
||||||
|
|
||||||
// Shield resistances
|
if (shield.cells > 0) {
|
||||||
const shieldDamageTakenData = [
|
shieldSourcesTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
|
||||||
'absolute', 'explosive', 'kinetic', 'thermal',
|
effectiveShieldAbsoluteTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
|
||||||
].map((label) => {
|
effectiveShieldExplosiveTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
|
||||||
const dmgMult = shields[label];
|
effectiveShieldKineticTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
|
||||||
const tooltip = ['byBoosters', 'byGenerator', 'bySys'].map(
|
effectiveShieldThermalTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
|
||||||
(label) => <div key={label}>
|
}
|
||||||
{translate(label)} {formats.pct1(dmgMult[label])}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return { label, value: Math.round(100 * dmgMult.withSys), tooltip };
|
|
||||||
});
|
|
||||||
|
|
||||||
// Effective MJ
|
// Add effective shield from resistances
|
||||||
const effectiveShieldData = [
|
const rawMj = shield.generator + shield.boosters + shield.cells;
|
||||||
'absolute', 'explosive', 'kinetic', 'thermal'
|
const explosiveMj = rawMj / (shield.explosive.base) - rawMj;
|
||||||
].map((label) => {
|
if (explosiveMj != 0) effectiveShieldExplosiveTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(explosiveMj)}{units.MJ}</div>);
|
||||||
const dmgMult = shields[label];
|
const kineticMj = rawMj / (shield.kinetic.base) - rawMj;
|
||||||
const raw = shields.withSCBs;
|
if (kineticMj != 0) effectiveShieldKineticTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(kineticMj)}{units.MJ}</div>);
|
||||||
const tooltip = ['byBoosters', 'byGenerator', 'bySys'].map(
|
const thermalMj = rawMj / (shield.thermal.base) - rawMj;
|
||||||
(label) => <div key={label}>
|
if (thermalMj != 0) effectiveShieldThermalTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(thermalMj)}{units.MJ}</div>);
|
||||||
{translate(label)} {formats.int(raw * dmgMult[label])}{units.MJ}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return { label, value: Math.round(dmgMult.withSys * raw), tooltip };
|
|
||||||
});
|
|
||||||
const maxEffectiveShield = Math.max(...effectiveShieldData.map((o) => o.value));
|
|
||||||
|
|
||||||
const armour = ship.get(ARMOUR_METRICS);
|
// Add effective shield from power distributor SYS pips
|
||||||
const moduleProtection = ship.get(MODULE_PROTECTION_METRICS);
|
if (shield.absolute.sys != 1) {
|
||||||
|
effectiveShieldAbsoluteTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.absolute.total - rawMj)}{units.MJ}</div>);
|
||||||
|
effectiveShieldExplosiveTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.explosive.total - rawMj / shield.explosive.base)}{units.MJ}</div>);
|
||||||
|
effectiveShieldKineticTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.kinetic.total - rawMj / shield.kinetic.base)}{units.MJ}</div>);
|
||||||
|
effectiveShieldThermalTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.thermal.total - rawMj / shield.thermal.base)}{units.MJ}</div>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Data for pie chart (absolute HP)
|
shieldDamageTakenAbsoluteTt.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.absolute.generator)}</div>);
|
||||||
const armourSourcesData = ['base', 'byAlloys', 'byHRPs',].map(
|
shieldDamageTakenAbsoluteTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.absolute.boosters)}</div>);
|
||||||
(key) => { return { label: key, value: Math.round(armour[key]) }; }
|
shieldDamageTakenAbsoluteTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.absolute.sys)}</div>);
|
||||||
);
|
|
||||||
|
|
||||||
// Data for tooltip
|
shieldDamageTakenExplosiveTt.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.explosive.generator)}</div>);
|
||||||
const armourSourcesTt = armourSourcesData.map((o) => {
|
shieldDamageTakenExplosiveTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.explosive.boosters)}</div>);
|
||||||
let { label, value } = o;
|
shieldDamageTakenExplosiveTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.explosive.sys)}</div>);
|
||||||
return <div key={label}>{translate(label)} {formats.int(value)}</div>;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Armour resistances
|
shieldDamageTakenKineticTt.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.kinetic.generator)}</div>);
|
||||||
const armourDamageTakenData = [
|
shieldDamageTakenKineticTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.kinetic.boosters)}</div>);
|
||||||
'absolute', 'explosive', 'kinetic', 'thermal', 'caustic',
|
shieldDamageTakenKineticTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.kinetic.sys)}</div>);
|
||||||
].map((label) => {
|
|
||||||
const dmgMult = armour[label];
|
|
||||||
const tooltip = ['byAlloys', 'byHRPs'].map(
|
|
||||||
(label) => <div key={label}>
|
|
||||||
{translate(label)} {formats.pct1(dmgMult[label])}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return { label, value: Math.round(100 * dmgMult.damageMultiplier), tooltip };
|
|
||||||
});
|
|
||||||
|
|
||||||
// Effective HP
|
shieldDamageTakenThermalTt.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.thermal.generator)}</div>);
|
||||||
const effectiveArmourData = [
|
shieldDamageTakenThermalTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.thermal.boosters)}</div>);
|
||||||
'absolute', 'explosive', 'kinetic', 'thermal'
|
shieldDamageTakenThermalTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.thermal.sys)}</div>);
|
||||||
].map((label) => {
|
|
||||||
const dmgMult = armour[label];
|
const effectiveAbsoluteShield = shield.total / shield.absolute.total;
|
||||||
const raw = armour.armour;
|
effectiveShieldData.push({ value: Math.round(effectiveAbsoluteShield), label: translate('absolute'), tooltip: effectiveShieldAbsoluteTt });
|
||||||
const tooltip = ['byBoosters', 'byGenerator', 'bySys'].map(
|
const effectiveExplosiveShield = shield.total / shield.explosive.total;
|
||||||
(label) => <div key={label}>
|
effectiveShieldData.push({ value: Math.round(effectiveExplosiveShield), label: translate('explosive'), tooltip: effectiveShieldExplosiveTt });
|
||||||
{translate(label)} {formats.int(raw * dmgMult[label])}
|
const effectiveKineticShield = shield.total / shield.kinetic.total;
|
||||||
</div>
|
effectiveShieldData.push({ value: Math.round(effectiveKineticShield), label: translate('kinetic'), tooltip: effectiveShieldKineticTt });
|
||||||
);
|
const effectiveThermalShield = shield.total / shield.thermal.total;
|
||||||
return { label, value: Math.round(dmgMult.damageMultiplier * raw), tooltip };
|
effectiveShieldData.push({ value: Math.round(effectiveThermalShield), label: translate('thermal'), tooltip: effectiveShieldThermalTt });
|
||||||
});
|
|
||||||
|
shieldDamageTakenData.push({ value: Math.round(shield.absolute.total * 100), label: translate('absolute'), tooltip: shieldDamageTakenAbsoluteTt });
|
||||||
|
shieldDamageTakenData.push({ value: Math.round(shield.explosive.total * 100), label: translate('explosive'), tooltip: shieldDamageTakenExplosiveTt });
|
||||||
|
shieldDamageTakenData.push({ value: Math.round(shield.kinetic.total * 100), label: translate('kinetic'), tooltip: shieldDamageTakenKineticTt });
|
||||||
|
shieldDamageTakenData.push({ value: Math.round(shield.thermal.total * 100), label: translate('thermal'), tooltip: shieldDamageTakenThermalTt });
|
||||||
|
|
||||||
|
maxEffectiveShield = Math.max(shield.total / shield.absolute.max, shield.total / shield.explosive.max, shield.total / shield.kinetic.max, shield.total / shield.thermal.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
const armourSourcesData = [];
|
||||||
|
armourSourcesData.push({ value: Math.round(armour.bulkheads), label: translate('bulkheads') });
|
||||||
|
armourSourcesData.push({ value: Math.round(armour.reinforcement), label: translate('reinforcement') });
|
||||||
|
|
||||||
|
const armourSourcesTt = [];
|
||||||
|
const effectiveArmourAbsoluteTt = [];
|
||||||
|
const effectiveArmourExplosiveTt = [];
|
||||||
|
const effectiveArmourKineticTt = [];
|
||||||
|
const effectiveArmourThermalTt = [];
|
||||||
|
const effectiveArmourCausticTt = [];
|
||||||
|
if (armour.bulkheads > 0) {
|
||||||
|
armourSourcesTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
|
||||||
|
effectiveArmourAbsoluteTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
|
||||||
|
effectiveArmourExplosiveTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
|
||||||
|
effectiveArmourKineticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
|
||||||
|
effectiveArmourThermalTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
|
||||||
|
effectiveArmourCausticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
|
||||||
|
if (armour.reinforcement > 0) {
|
||||||
|
armourSourcesTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
|
||||||
|
effectiveArmourAbsoluteTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
|
||||||
|
effectiveArmourExplosiveTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
|
||||||
|
effectiveArmourKineticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
|
||||||
|
effectiveArmourThermalTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
|
||||||
|
effectiveArmourCausticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawArmour = armour.bulkheads + armour.reinforcement;
|
||||||
|
|
||||||
|
const armourDamageTakenTt = [];
|
||||||
|
armourDamageTakenTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.absolute.bulkheads)}</div>);
|
||||||
|
armourDamageTakenTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.absolute.reinforcement)}</div>);
|
||||||
|
|
||||||
|
const armourDamageTakenExplosiveTt = [];
|
||||||
|
armourDamageTakenExplosiveTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.explosive.bulkheads)}</div>);
|
||||||
|
armourDamageTakenExplosiveTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.explosive.reinforcement)}</div>);
|
||||||
|
if (armour.explosive.total != 1) effectiveArmourExplosiveTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / armour.explosive.total - rawArmour)}</div>);
|
||||||
|
|
||||||
|
const armourDamageTakenKineticTt = [];
|
||||||
|
armourDamageTakenKineticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.kinetic.bulkheads)}</div>);
|
||||||
|
armourDamageTakenKineticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.kinetic.reinforcement)}</div>);
|
||||||
|
if (armour.kinetic.total != 1) effectiveArmourKineticTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / armour.kinetic.total - rawArmour)}</div>);
|
||||||
|
|
||||||
|
const armourDamageTakenThermalTt = [];
|
||||||
|
armourDamageTakenThermalTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.thermal.bulkheads)}</div>);
|
||||||
|
armourDamageTakenThermalTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.thermal.reinforcement)}</div>);
|
||||||
|
if (armour.thermal.total != 1) effectiveArmourThermalTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / armour.thermal.total - rawArmour)}</div>);
|
||||||
|
|
||||||
|
const armourDamageTakenCausticTt = [];
|
||||||
|
armourDamageTakenCausticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.caustic.bulkheads)}</div>);
|
||||||
|
armourDamageTakenCausticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.caustic.reinforcement)}</div>);
|
||||||
|
if (armour.thermal.total != 1) effectiveArmourCausticTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / armour.caustic.total - rawArmour)}</div>);
|
||||||
|
|
||||||
|
const effectiveArmourData = [];
|
||||||
|
const effectiveAbsoluteArmour = armour.total / armour.absolute.total;
|
||||||
|
effectiveArmourData.push({ value: Math.round(effectiveAbsoluteArmour), label: translate('absolute'), tooltip: effectiveArmourAbsoluteTt });
|
||||||
|
const effectiveExplosiveArmour = armour.total / armour.explosive.total;
|
||||||
|
effectiveArmourData.push({ value: Math.round(effectiveExplosiveArmour), label: translate('explosive'), tooltip: effectiveArmourExplosiveTt });
|
||||||
|
const effectiveKineticArmour = armour.total / armour.kinetic.total;
|
||||||
|
effectiveArmourData.push({ value: Math.round(effectiveKineticArmour), label: translate('kinetic'), tooltip: effectiveArmourKineticTt });
|
||||||
|
const effectiveThermalArmour = armour.total / armour.thermal.total;
|
||||||
|
effectiveArmourData.push({ value: Math.round(effectiveThermalArmour), label: translate('thermal'), tooltip: effectiveArmourThermalTt });
|
||||||
|
const effectiveCausticArmour = armour.total / armour.caustic.total;
|
||||||
|
effectiveArmourData.push({ value: Math.round(effectiveCausticArmour), label: translate('caustic'), tooltip: effectiveArmourCausticTt });
|
||||||
|
|
||||||
|
const armourDamageTakenData = [];
|
||||||
|
armourDamageTakenData.push({ value: Math.round(armour.absolute.total * 100), label: translate('absolute'), tooltip: armourDamageTakenTt });
|
||||||
|
armourDamageTakenData.push({ value: Math.round(armour.explosive.total * 100), label: translate('explosive'), tooltip: armourDamageTakenExplosiveTt });
|
||||||
|
armourDamageTakenData.push({ value: Math.round(armour.kinetic.total * 100), label: translate('kinetic'), tooltip: armourDamageTakenKineticTt });
|
||||||
|
armourDamageTakenData.push({ value: Math.round(armour.thermal.total * 100), label: translate('thermal'), tooltip: armourDamageTakenThermalTt });
|
||||||
|
armourDamageTakenData.push({ value: Math.round(armour.caustic.total * 100), label: translate('caustic'), tooltip: armourDamageTakenCausticTt });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span id='defence'>
|
<span id='defence'>
|
||||||
{shields.withSCBs ? <span>
|
{shield.total ? <span>
|
||||||
<div className='group quarter'>
|
<div className='group quarter'>
|
||||||
<h2>{translate('shield metrics')}</h2>
|
<h2>{translate('shield metrics')}</h2>
|
||||||
<br/>
|
<br/>
|
||||||
<h2 onMouseOver={termtip.bind(null, <div>{shieldSourcesTt}</div>)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw shield strength')}<br/>{formats.int(shields.withSCBs)}{units.MJ}</h2>
|
<h2 onMouseOver={termtip.bind(null, <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/>TODO</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/>{shields.recover ? formats.time(shields.recover) : translate('never')}</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/>{shields.recharge ? formats.time(shields.recharge) : translate('never')}</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>
|
||||||
<div className='group quarter'>
|
<div className='group quarter'>
|
||||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SHIELD_SOURCES'))} onMouseOut={tooltip.bind(null, null)}>{translate('shield sources')}</h2>
|
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SHIELD_SOURCES'))} onMouseOut={tooltip.bind(null, null)}>{translate('shield sources')}</h2>
|
||||||
@@ -151,11 +250,11 @@ export default class Defence extends TranslatedComponent {
|
|||||||
|
|
||||||
<div className='group quarter'>
|
<div className='group quarter'>
|
||||||
<h2>{translate('armour metrics')}</h2>
|
<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.armour)}</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/>TODO</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(moduleProtection.moduleArmour)}</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((1 - moduleProtection.moduleProtection) / 2)}</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(1 - moduleProtection.moduleProtection)}</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/>
|
<br/>
|
||||||
</div>
|
</div>
|
||||||
<div className='group quarter'>
|
<div className='group quarter'>
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
import Persist from '../stores/Persist';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { getBlueprintUuid, getExperimentalUuid } from 'ed-forge/lib/src/data/blueprints';
|
|
||||||
import { Loader, MatIcon } from '../components/SvgIcons';
|
|
||||||
import request from 'superagent';
|
|
||||||
import { chain, entries } from 'lodash';
|
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
|
||||||
|
|
||||||
const STATE = {
|
|
||||||
READY: 0,
|
|
||||||
LOADING: 1,
|
|
||||||
ERROR: 2,
|
|
||||||
DONE: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export default class EDEngineerButton extends TranslatedComponent {
|
|
||||||
static propTypes = {
|
|
||||||
ship: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
* @param {Object} props React Component properties
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
autoBind(this);
|
|
||||||
|
|
||||||
const { ship } = props;
|
|
||||||
const uuids = chain(ship.getModules())
|
|
||||||
.filter((m) => m.getBlueprint())
|
|
||||||
.map((m) => {
|
|
||||||
const uuids = [getBlueprintUuid(m.getBlueprint(), m.getBlueprintGrade())];
|
|
||||||
const exp = m.getExperimental();
|
|
||||||
if (exp) {
|
|
||||||
uuids.push(getExperimentalUuid(exp));
|
|
||||||
}
|
|
||||||
return uuids;
|
|
||||||
})
|
|
||||||
.flatMap()
|
|
||||||
.groupBy()
|
|
||||||
.mapValues((v) => v.length)
|
|
||||||
.value();
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
status: STATE.READY,
|
|
||||||
uuids,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates the shopping list
|
|
||||||
*/
|
|
||||||
_sendToEDEngineer() {
|
|
||||||
const { uuids } = this.state;
|
|
||||||
this.setState({ status: STATE.LOADING });
|
|
||||||
request.get('http://localhost:44405/commanders')
|
|
||||||
.then((data) => {
|
|
||||||
const [cmdr] = JSON.parse(data.text);
|
|
||||||
return Promise.all(
|
|
||||||
entries(uuids).map(
|
|
||||||
(entry) => {
|
|
||||||
const [uuid, n] = entry;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
request.patch(`http://localhost:44405/${cmdr}/shopping-list`)
|
|
||||||
.field('uuid', uuid)
|
|
||||||
.field('size', n)
|
|
||||||
.end((err, res) => {
|
|
||||||
console.log('request goes out!');
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve(res);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.then(() => this.setState({ status: STATE.DONE }))
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
this.setState({ status: STATE.ERROR });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks for browser compatibility of sending to ED Engineer.
|
|
||||||
* @returns {boolean} True if browser is compatible
|
|
||||||
*/
|
|
||||||
_browserIsCompatible() {
|
|
||||||
// !== Firefox 1.0+
|
|
||||||
// TODO: Double check if this really doesn't work in firefox
|
|
||||||
return typeof InstallTrigger === 'undefined';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
const { termtip, tooltip } = this.context;
|
|
||||||
const hide = tooltip.bind(null, null);
|
|
||||||
const { status } = this.state;
|
|
||||||
|
|
||||||
let msg = 'PHRASE_FIREFOX_EDENGINEER';
|
|
||||||
if (this._browserIsCompatible()) {
|
|
||||||
switch (status) {
|
|
||||||
case STATE.READY: msg = 'Send to EDEngineer'; break;
|
|
||||||
case STATE.LOADING: msg = 'Sending...'; break;
|
|
||||||
case STATE.ERROR: msg = 'Error sending to EDEngineer'; break;
|
|
||||||
case STATE.DONE: msg = 'Success! Clicking sends again.'; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<button
|
|
||||||
disabled={!this._browserIsCompatible()}
|
|
||||||
onClick={status !== STATE.LOADING && this._sendToEDEngineer}
|
|
||||||
onMouseOver={termtip.bind(null, msg)}
|
|
||||||
onMouseOut={hide}
|
|
||||||
>
|
|
||||||
{status === STATE.LOADING ?
|
|
||||||
<Loader className="lg" /> :
|
|
||||||
<MatIcon className="lg" />
|
|
||||||
}
|
|
||||||
</button>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import Slider from '../components/Slider';
|
import Slider from '../components/Slider';
|
||||||
import { moduleReduce } from 'ed-forge/lib/src/helper';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Engagement range slider
|
* Engagement range slider
|
||||||
@@ -22,18 +21,35 @@ export default class EngagementRange extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
const { ship } = props;
|
||||||
|
|
||||||
|
const maxRange = Math.round(this._calcMaxRange(ship));
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
maxRange: moduleReduce(
|
maxRange
|
||||||
this.props.ship.getHardpoints(),
|
|
||||||
'maximumrange',
|
|
||||||
true,
|
|
||||||
// Don't use plain `Math.max` because callback will be passed four args
|
|
||||||
(a, v) => Math.max(a, v),
|
|
||||||
1000,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the maximum range of a ship's weapons
|
||||||
|
* @param {Object} ship The ship
|
||||||
|
* @returns {int} The maximum range, in metres
|
||||||
|
*/
|
||||||
|
_calcMaxRange(ship) {
|
||||||
|
let maxRange = 1000;
|
||||||
|
for (let i = 0; i < ship.hardpoints.length; i++) {
|
||||||
|
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
|
||||||
|
const thisRange = ship.hardpoints[i].m.getRange();
|
||||||
|
if (thisRange > maxRange) {
|
||||||
|
maxRange = thisRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxRange;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update range
|
* Update range
|
||||||
* @param {number} rangeLevel percentage level from 0 to 1
|
* @param {number} rangeLevel percentage level from 0 to 1
|
||||||
@@ -45,9 +61,7 @@ export default class EngagementRange extends TranslatedComponent {
|
|||||||
const range = Math.round(rangeLevel * maxRange);
|
const range = Math.round(rangeLevel * maxRange);
|
||||||
|
|
||||||
if (range !== this.props.engagementRange) {
|
if (range !== this.props.engagementRange) {
|
||||||
const { onChange, ship } = this.props;
|
this.props.onChange(range);
|
||||||
ship.setEngagementRange(range);
|
|
||||||
onChange(range);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,8 +70,8 @@ export default class EngagementRange extends TranslatedComponent {
|
|||||||
* @return {React.Component} contents
|
* @return {React.Component} contents
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { language, onWindowResize, sizeRatio } = this.context;
|
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||||
const { formats, translate } = language;
|
const { formats, translate, units } = language;
|
||||||
const { engagementRange } = this.props;
|
const { engagementRange } = this.props;
|
||||||
const { maxRange } = this.state;
|
const { maxRange } = this.state;
|
||||||
|
|
||||||
|
|||||||
@@ -2,58 +2,98 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import LineChart from '../components/LineChart';
|
import LineChart from '../components/LineChart';
|
||||||
import { getBoostMultiplier, getSpeedMultipliers } from 'ed-forge/lib/src/stats/SpeedProfile';
|
import * as Calc from '../shipyard/Calculations';
|
||||||
import { ShipProps } from 'ed-forge';
|
|
||||||
const { LADEN_MASS } = ShipProps;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Engine profile for a given ship
|
* Engine profile for a given ship
|
||||||
*/
|
*/
|
||||||
export default class EngineProfile extends TranslatedComponent {
|
export default class EngineProfile extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
code: PropTypes.string.isRequired,
|
|
||||||
ship: PropTypes.object.isRequired,
|
ship: PropTypes.object.isRequired,
|
||||||
cargo: PropTypes.number.isRequired,
|
cargo: PropTypes.number.isRequired,
|
||||||
fuel: PropTypes.number.isRequired,
|
fuel: PropTypes.number.isRequired,
|
||||||
pips: PropTypes.number.isRequired,
|
eng: PropTypes.number.isRequired,
|
||||||
boost: PropTypes.bool.isRequired,
|
boost: PropTypes.bool.isRequired,
|
||||||
|
marker: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param {Object} props React Component properties
|
||||||
|
* @param {Object} context React Component context
|
||||||
|
*/
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const ship = this.props.ship;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, ship, this.props.eng, this.props.boost)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the state if our ship changes
|
||||||
|
* @param {Object} nextProps Incoming/Next properties
|
||||||
|
* @param {Object} nextContext Incoming/Next conext
|
||||||
|
* @return {boolean} Returns true if the component should be rerendered
|
||||||
|
*/
|
||||||
|
componentWillReceiveProps(nextProps, nextContext) {
|
||||||
|
if (nextProps.marker != this.props.marker) {
|
||||||
|
this.setState({ calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, nextProps.ship, nextProps.eng, nextProps.boost) });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the top speed for this ship given thrusters, mass and pips to ENG
|
||||||
|
* @param {Object} ship The ship
|
||||||
|
* @param {Object} eng The number of pips to ENG
|
||||||
|
* @param {Object} boost If boost is enabled
|
||||||
|
* @param {Object} mass The mass at which to calculate the top speed
|
||||||
|
* @return {number} The maximum speed
|
||||||
|
*/
|
||||||
|
calcMaxSpeed(ship, eng, boost, mass) {
|
||||||
|
// Obtain the top speed
|
||||||
|
return Calc.calcSpeed(mass, ship.speed, ship.standard[1].m, ship.pipSpeed, eng, ship.boost / ship.speed, boost);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render engine profile
|
* Render engine profile
|
||||||
* @return {React.Component} contents
|
* @return {React.Component} contents
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { language } = this.context;
|
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||||
const { translate } = language;
|
const { formats, translate, units } = language;
|
||||||
const { code, ship, pips, boost } = this.props;
|
const { ship, cargo, eng, fuel, boost } = this.props;
|
||||||
|
|
||||||
// Calculate bounds for our line chart
|
// Calculate bounds for our line chart
|
||||||
const minMass = ship.readProp('hullmass');
|
const thrusters = ship.standard[1].m;
|
||||||
const maxMass = ship.getThrusters().get('enginemaximalmass');
|
const minMass = ship.calcLowestPossibleMass({ th: thrusters });
|
||||||
const baseSpeed = ship.readProp('speed');
|
const maxMass = thrusters.getMaxMass();
|
||||||
const baseBoost = getBoostMultiplier(ship);
|
const mass = ship.unladenMass + fuel + cargo;
|
||||||
const cb = (eng, boost, mass) => {
|
const minSpeed = Calc.calcSpeed(maxMass, ship.speed, thrusters, ship.pipSpeed, 0, ship.boost / ship.speed, false);
|
||||||
const mult = getSpeedMultipliers(ship, mass)[(boost ? 4 : eng) / 0.5];
|
const maxSpeed = Calc.calcSpeed(minMass, ship.speed, thrusters, ship.pipSpeed, 4, ship.boost / ship.speed, true);
|
||||||
return baseSpeed * (boost ? baseBoost : 1) * mult;
|
// 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
|
// This graph can have a precipitous fall-off so we use lots of points to make it look a little smoother
|
||||||
return (
|
return (
|
||||||
<LineChart
|
<LineChart
|
||||||
xMin={minMass}
|
xMin={minMass}
|
||||||
xMax={maxMass}
|
xMax={maxMass}
|
||||||
yMin={cb(0, false, maxMass)}
|
yMin={minSpeed}
|
||||||
yMax={cb(4, true, minMass)}
|
yMax={maxSpeed}
|
||||||
// Add a mark at our current mass
|
xMark={mark}
|
||||||
xMark={Math.min(ship.get(LADEN_MASS), maxMass)}
|
|
||||||
xLabel={translate('mass')}
|
xLabel={translate('mass')}
|
||||||
xUnit={translate('T')}
|
xUnit={translate('T')}
|
||||||
yLabel={translate('maximum speed')}
|
yLabel={translate('maximum speed')}
|
||||||
yUnit={translate('m/s')}
|
yUnit={translate('m/s')}
|
||||||
func={cb.bind(this, pips.Eng.base + pips.Eng.mc, boost)}
|
func={this.state.calcMaxSpeedFunc}
|
||||||
points={1000}
|
points={1000}
|
||||||
// Encode boost in code to re-render on state change
|
code={code}
|
||||||
code={`${pips.Eng.base + pips.Eng.mc}:${Number(boost)}:${code}`}
|
|
||||||
aspect={0.7}
|
aspect={0.7}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,47 +2,94 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import LineChart from '../components/LineChart';
|
import LineChart from '../components/LineChart';
|
||||||
import { calculateJumpRange } from 'ed-forge/lib/src/stats/JumpRangeProfile';
|
import * as Calc from '../shipyard/Calculations';
|
||||||
import { ShipProps } from 'ed-forge';
|
|
||||||
const { LADEN_MASS } = ShipProps;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FSD profile for a given ship
|
* FSD profile for a given ship
|
||||||
*/
|
*/
|
||||||
export default class FSDProfile extends TranslatedComponent {
|
export default class FSDProfile extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
code: PropTypes.string.isRequired,
|
|
||||||
ship: PropTypes.object.isRequired,
|
ship: PropTypes.object.isRequired,
|
||||||
cargo: PropTypes.number.isRequired,
|
cargo: PropTypes.number.isRequired,
|
||||||
fuel: PropTypes.number.isRequired,
|
fuel: PropTypes.number.isRequired,
|
||||||
|
marker: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param {Object} props React Component properties
|
||||||
|
* @param {Object} context React Component context
|
||||||
|
*/
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const ship = this.props.ship;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
calcMaxRangeFunc: this._calcMaxRange.bind(this, ship, this.props.fuel)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the state if our ship changes
|
||||||
|
* @param {Object} nextProps Incoming/Next properties
|
||||||
|
* @param {Object} nextContext Incoming/Next conext
|
||||||
|
* @return {boolean} Returns true if the component should be rerendered
|
||||||
|
*/
|
||||||
|
componentWillReceiveProps(nextProps, nextContext) {
|
||||||
|
if (nextProps.marker != this.props.marker) {
|
||||||
|
this.setState({ calcMaxRangeFunc: this._calcMaxRange.bind(this, nextProps.ship, nextProps.fuel) });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the maximum range for this ship across its applicable mass
|
||||||
|
* @param {Object} ship The ship
|
||||||
|
* @param {Object} fuel The fuel on the ship
|
||||||
|
* @param {Object} mass The mass at which to calculate the maximum range
|
||||||
|
* @return {number} The maximum range
|
||||||
|
*/
|
||||||
|
_calcMaxRange(ship, fuel, mass) {
|
||||||
|
// Obtain the maximum range
|
||||||
|
return Calc.jumpRange(mass, ship.standard[2].m, Math.min(fuel, ship.standard[2].m.getMaxFuelPerJump()), ship);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render FSD profile
|
* Render FSD profile
|
||||||
* @return {React.Component} contents
|
* @return {React.Component} contents
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { language } = this.context;
|
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||||
const { translate } = language;
|
const { formats, translate, units } = language;
|
||||||
const { code, ship } = this.props;
|
const { ship, cargo, fuel } = this.props;
|
||||||
|
|
||||||
|
|
||||||
|
// Calculate bounds for our line chart - use thruster info for X
|
||||||
|
const thrusters = ship.standard[1].m;
|
||||||
|
const fsd = ship.standard[2].m;
|
||||||
|
const minMass = ship.calcLowestPossibleMass({ th: thrusters });
|
||||||
|
const maxMass = thrusters.getMaxMass();
|
||||||
|
const mass = ship.unladenMass + fuel + cargo;
|
||||||
|
const minRange = 0;
|
||||||
|
const maxRange = Calc.jumpRange(minMass + fsd.getMaxFuelPerJump(), fsd, fsd.getMaxFuelPerJump(), ship);
|
||||||
|
// Add a mark at our current mass
|
||||||
|
const mark = Math.min(mass, maxMass);
|
||||||
|
|
||||||
|
const code = ship.name + ship.toString() + '.' + fuel;
|
||||||
|
|
||||||
const minMass = ship.readProp('hullmass');
|
|
||||||
const maxMass = ship.getThrusters().get('enginemaximalmass');
|
|
||||||
const mass = ship.get(LADEN_MASS);
|
|
||||||
const cb = (mass) => calculateJumpRange(ship.getFSD(), 0, mass, Infinity, true);
|
|
||||||
return (
|
return (
|
||||||
<LineChart
|
<LineChart
|
||||||
xMin={minMass}
|
xMin={minMass}
|
||||||
xMax={maxMass}
|
xMax={maxMass}
|
||||||
yMin={0}
|
yMin={minRange}
|
||||||
yMax={cb(minMass)}
|
yMax={maxRange}
|
||||||
// Add a mark at our current mass
|
xMark={mark}
|
||||||
xMark={Math.min(mass, maxMass)}
|
|
||||||
xLabel={translate('mass')}
|
xLabel={translate('mass')}
|
||||||
xUnit={translate('T')}
|
xUnit={translate('T')}
|
||||||
yLabel={translate('maximum range')}
|
yLabel={translate('maximum range')}
|
||||||
yUnit={translate('LY')}
|
yUnit={translate('LY')}
|
||||||
func={cb}
|
func={this.state.calcMaxRangeFunc}
|
||||||
points={200}
|
points={200}
|
||||||
code={code}
|
code={code}
|
||||||
aspect={0.7}
|
aspect={0.7}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import Slider from '../components/Slider';
|
import Slider from '../components/Slider';
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fuel slider
|
* Fuel slider
|
||||||
@@ -22,7 +21,8 @@ export default class Fuel extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
|
||||||
|
this._fuelChange = this._fuelChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
151
src/app/components/HardpointSlot.jsx
Normal file
151
src/app/components/HardpointSlot.jsx
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import Slot from './Slot';
|
||||||
|
import Persist from '../stores/Persist';
|
||||||
|
import {
|
||||||
|
DamageAbsolute,
|
||||||
|
DamageKinetic,
|
||||||
|
DamageThermal,
|
||||||
|
DamageExplosive,
|
||||||
|
MountFixed,
|
||||||
|
MountGimballed,
|
||||||
|
MountTurret,
|
||||||
|
ListModifications,
|
||||||
|
Modified
|
||||||
|
} from './SvgIcons';
|
||||||
|
import { Modifications } from 'coriolis-data/dist';
|
||||||
|
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||||
|
import { blueprintTooltip } from '../utils/BlueprintFunctions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hardpoint / Utility Slot
|
||||||
|
*/
|
||||||
|
export default class HardpointSlot extends Slot {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the CSS class name for the slot.
|
||||||
|
* @return {string} CSS Class name
|
||||||
|
*/
|
||||||
|
_getClassNames() {
|
||||||
|
return this.props.maxClass > 0 ? 'hardpoint' : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the label for the slot
|
||||||
|
* @param {Function} translate Translate function
|
||||||
|
* @return {string} Label
|
||||||
|
*/
|
||||||
|
_getMaxClassLabel(translate) {
|
||||||
|
return translate(['U', 'S', 'M', 'L', 'H'][this.props.maxClass]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the slot contents
|
||||||
|
* @param {Object} m Mounted Module
|
||||||
|
* @param {Boolean} enabled Slot enabled
|
||||||
|
* @param {Function} translate Translate function
|
||||||
|
* @param {Object} formats Localized Formats map
|
||||||
|
* @param {Object} u Localized Units Map
|
||||||
|
* @return {React.Component} Slot contents
|
||||||
|
*/
|
||||||
|
_getSlotDetails(m, enabled, translate, formats, u) {
|
||||||
|
if (m) {
|
||||||
|
let classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`;
|
||||||
|
let { drag, drop } = this.props;
|
||||||
|
let { termtip, tooltip } = this.context;
|
||||||
|
let validMods = Modifications.modules[m.grp].modifications || [];
|
||||||
|
let showModuleResistances = Persist.showModuleResistances();
|
||||||
|
|
||||||
|
// Modifications tooltip shows blueprint and grade, if available
|
||||||
|
let modTT = translate('modified');
|
||||||
|
if (m && m.blueprint && m.blueprint.name) {
|
||||||
|
modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
|
||||||
|
if (m.blueprint.special && m.blueprint.special.id >= 0) {
|
||||||
|
modTT += ', ' + translate(m.blueprint.special.name);
|
||||||
|
}
|
||||||
|
modTT = (
|
||||||
|
<div>
|
||||||
|
<div>{modTT}</div>
|
||||||
|
{blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = cn('details', enabled ? '' : 'disabled');
|
||||||
|
return <div className={className} draggable='true' onDragStart={drag} onDragEnd={drop}>
|
||||||
|
<div className={'cb'}>
|
||||||
|
<div className={'l'}>
|
||||||
|
{m.mount && m.mount == 'F' ? <span onMouseOver={termtip.bind(null, 'fixed')}
|
||||||
|
onMouseOut={tooltip.bind(null, null)}><MountFixed/></span> : ''}
|
||||||
|
{m.mount && m.mount == 'G' ? <span onMouseOver={termtip.bind(null, 'gimballed')}
|
||||||
|
onMouseOut={tooltip.bind(null, null)}><MountGimballed/></span> : ''}
|
||||||
|
{m.mount && m.mount == 'T' ? <span onMouseOver={termtip.bind(null, 'turreted')}
|
||||||
|
onMouseOut={tooltip.bind(null, null)}><MountTurret/></span> : ''}
|
||||||
|
{m.getDamageDist() && m.getDamageDist().K ? <span onMouseOver={termtip.bind(null, 'kinetic')}
|
||||||
|
onMouseOut={tooltip.bind(null, null)}><DamageKinetic/></span> : ''}
|
||||||
|
{m.getDamageDist() && m.getDamageDist().T ? <span onMouseOver={termtip.bind(null, 'thermal')}
|
||||||
|
onMouseOut={tooltip.bind(null, null)}><DamageThermal/></span> : ''}
|
||||||
|
{m.getDamageDist() && m.getDamageDist().E ? <span onMouseOver={termtip.bind(null, 'explosive')}
|
||||||
|
onMouseOut={tooltip.bind(null, null)}><DamageExplosive/></span> : ''}
|
||||||
|
{m.getDamageDist() && m.getDamageDist().A ? <span onMouseOver={termtip.bind(null, 'absolute')}
|
||||||
|
onMouseOut={tooltip.bind(null, null)}><DamageAbsolute/></span> : ''}
|
||||||
|
{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? <span className='r'
|
||||||
|
onMouseOver={termtip.bind(null, modTT)}
|
||||||
|
onMouseOut={tooltip.bind(null, null)}><Modified/></span> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'r'}>{formats.round(m.getMass())}{u.T}</div>
|
||||||
|
</div>
|
||||||
|
<div className={'cb'}>
|
||||||
|
{m.getDps() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getClip() ? 'dpssdps' : 'dps')}
|
||||||
|
onMouseOut={tooltip.bind(null, null)}>{translate('DPS')}: {formats.round1(m.getDps())} {m.getClip() ?
|
||||||
|
<span>({formats.round1(m.getSDps())})</span> : null}</div> : null}
|
||||||
|
{m.getDamage() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getDamage() ? 'shotdmg' : 'shotdmg')}
|
||||||
|
onMouseOut={tooltip.bind(null, null)}>{translate('shotdmg')}: {formats.round1(m.getDamage())}</div> : null}
|
||||||
|
{m.getEps() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getClip() ? 'epsseps' : 'eps')}
|
||||||
|
onMouseOut={tooltip.bind(null, null)}>{translate('EPS')}: {formats.round1(m.getEps())}{u.MW} {m.getClip() ?
|
||||||
|
<span>({formats.round1((m.getClip() * m.getEps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()))}{u.MW})</span> : null}</div> : null}
|
||||||
|
{m.getHps() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getClip() ? 'hpsshps' : 'hps')}
|
||||||
|
onMouseOut={tooltip.bind(null, null)}>{translate('HPS')}: {formats.round1(m.getHps())} {m.getClip() ?
|
||||||
|
<span>({formats.round1((m.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', 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}
|
||||||
|
{m.getScanAngle() ? <div className={'l'}>{translate('scan angle')}: {formats.f2(m.getScanAngle())}°</div> : null}
|
||||||
|
{m.getScanRange() ? <div className={'l'}>{translate('scan range')}: {formats.int(m.getScanRange())}{u.m}</div> : null}
|
||||||
|
{m.getMaxAngle() ? <div className={'l'}>{translate('max angle')}: {formats.f2(m.getMaxAngle())}°</div> : null}
|
||||||
|
{showModuleResistances && m.getExplosiveResistance() ? <div
|
||||||
|
className='l'>{translate('explres')}: {formats.pct(m.getExplosiveResistance())}</div> : null}
|
||||||
|
{showModuleResistances && m.getKineticResistance() ? <div
|
||||||
|
className='l'>{translate('kinres')}: {formats.pct(m.getKineticResistance())}</div> : null}
|
||||||
|
{showModuleResistances && m.getThermalResistance() ? <div
|
||||||
|
className='l'>{translate('thermres')}: {formats.pct(m.getThermalResistance())}</div> : null}
|
||||||
|
{m.getIntegrity() ? <div className='l'>{translate('integrity')}: {formats.int(m.getIntegrity())}</div> : null}
|
||||||
|
{m && validMods.length > 0 ? <div className='r' tabIndex="0" ref={modButton => this.modButton = modButton}>
|
||||||
|
<button tabIndex="-1" onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation}
|
||||||
|
onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}>
|
||||||
|
<ListModifications/></button>
|
||||||
|
</div> : null}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
return <div className={'empty'}>{translate('empty')}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,83 +1,97 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SlotSection from './SlotSection';
|
import SlotSection from './SlotSection';
|
||||||
import Slot from './Slot';
|
import HardpointSlot from './HardpointSlot';
|
||||||
import { MountFixed, MountGimballed, MountTurret } from '../components/SvgIcons';
|
import { MountFixed, MountGimballed, MountTurret } from '../components/SvgIcons';
|
||||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
|
|
||||||
const SIZE_ORDER = ['huge', 'large', 'medium', 'small'];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hardpoint slot section
|
* Hardpoint slot section
|
||||||
*/
|
*/
|
||||||
export default class HardpointSlotSection extends SlotSection {
|
export default class HardpointSlotSection extends SlotSection {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
* @param {Object} props React Component properties
|
* @param {Object} props React Component properties
|
||||||
|
* @param {Object} context React Component context
|
||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props, context) {
|
||||||
super(props, 'hardpoints');
|
super(props, context, 'hardpoints', 'hardpoints');
|
||||||
autoBind(this);
|
this._empty = this._empty.bind(this);
|
||||||
|
this.selectedRefId = null;
|
||||||
|
this.firstRefId = 'emptyall';
|
||||||
|
this.lastRefId = 'nl-F';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle focus when component updates
|
||||||
|
* @param {Object} prevProps React Component properties
|
||||||
|
*/
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
this._handleSectionFocus(prevProps,this.firstRefId, this.lastRefId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Empty all slots
|
* Empty all slots
|
||||||
*/
|
*/
|
||||||
_empty() {
|
_empty() {
|
||||||
this.props.ship.getHardpoints(undefined, true).forEach((slot) => slot.reset());
|
this.selectedRefId = 'emptyall';
|
||||||
|
this.props.ship.emptyWeapons();
|
||||||
|
this.props.onChange();
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fill slots with specified module
|
* Fill slots with specified module
|
||||||
* @param {string} type Type of item
|
* @param {string} group Group name
|
||||||
* @param {string} rating Mount Type - (fixed, gimbal, turret)
|
* @param {string} mount Mount Type - F, G, T
|
||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
*/
|
*/
|
||||||
_fill(type, rating, event) {
|
_fill(group, mount, event) {
|
||||||
const fillAll = event.getModifierState('Alt');
|
this.selectedRefId = group + '-' + mount;
|
||||||
this.props.ship.getHardpoints(undefined, true).forEach((slot) => {
|
this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt'));
|
||||||
if (slot.isEmpty() || fillAll) {
|
this.props.onChange();
|
||||||
const slotSize = slot.getSize();
|
|
||||||
const fittingSizes = SIZE_ORDER.slice(SIZE_ORDER.findIndex((e) => e === slotSize));
|
|
||||||
for (const size of fittingSizes) {
|
|
||||||
try {
|
|
||||||
slot.setItem(type, size, rating);
|
|
||||||
} catch (err) {
|
|
||||||
// Try next item if this doesn't fit/exist
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// If still here, we were able to apply the module
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty all on section header right click
|
||||||
|
*/
|
||||||
|
_contextMenu() {
|
||||||
|
this._empty();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate the slot React Components
|
* Generate the slot React Components
|
||||||
* @return {Array} Array of Slots
|
* @return {Array} Array of Slots
|
||||||
*/
|
*/
|
||||||
_getSlots() {
|
_getSlots() {
|
||||||
let { ship, currentMenu, propsToShow, onPropToggle } = this.props;
|
let { ship, currentMenu } = this.props;
|
||||||
let { originSlot, targetSlot } = this.state;
|
let { originSlot, targetSlot } = this.state;
|
||||||
let slots = [];
|
let slots = [];
|
||||||
|
let hardpoints = ship.hardpoints;
|
||||||
|
let availableModules = ship.getAvailableModules();
|
||||||
|
|
||||||
for (let h of ship.getHardpoints(undefined, true)) {
|
for (let i = 0, l = hardpoints.length; i < l; i++) {
|
||||||
slots.push(<Slot
|
let h = hardpoints[i];
|
||||||
key={h.object.Slot}
|
if (h.maxClass) {
|
||||||
currentMenu={currentMenu}
|
slots.push(<HardpointSlot
|
||||||
|
key={i}
|
||||||
|
maxClass={h.maxClass}
|
||||||
|
availableModules={() => availableModules.getHps(h.maxClass)}
|
||||||
|
onOpen={this._openMenu.bind(this, h)}
|
||||||
|
onSelect={this._selectModule.bind(this, h)}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
selected={currentMenu == h}
|
||||||
drag={this._drag.bind(this, h)}
|
drag={this._drag.bind(this, h)}
|
||||||
dragOver={this._dragOverSlot.bind(this, h)}
|
dragOver={this._dragOverSlot.bind(this, h)}
|
||||||
drop={this._drop}
|
drop={this._drop}
|
||||||
dropClass={this._dropClass(h, originSlot, targetSlot)}
|
dropClass={this._dropClass(h, originSlot, targetSlot)}
|
||||||
m={h}
|
ship={ship}
|
||||||
|
m={h.m}
|
||||||
enabled={h.enabled ? true : false}
|
enabled={h.enabled ? true : false}
|
||||||
propsToShow={propsToShow}
|
|
||||||
onPropToggle={onPropToggle}
|
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return slots;
|
return slots;
|
||||||
}
|
}
|
||||||
@@ -87,68 +101,68 @@ export default class HardpointSlotSection extends SlotSection {
|
|||||||
* @param {Function} translate Translate function
|
* @param {Function} translate Translate function
|
||||||
* @return {React.Component} Section menu
|
* @return {React.Component} Section menu
|
||||||
*/
|
*/
|
||||||
_getSectionMenu() {
|
_getSectionMenu(translate) {
|
||||||
const { translate } = this.context.language;
|
|
||||||
let _fill = this._fill;
|
let _fill = this._fill;
|
||||||
|
|
||||||
return <div className='select hardpoint' onClick={(e) => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
|
return <div className='select hardpoint' onClick={(e) => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='lc' tabIndex="0" onClick={this._empty}>{translate('empty all')}</li>
|
<li className='lc' tabIndex='0' onClick={this._empty} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['emptyall'] = smRef}>{translate('empty all')}</li>
|
||||||
<li className='optional-hide' style={{ textAlign: 'center', marginTop: '1em' }}>{translate('PHRASE_ALT_ALL')}</li>
|
<li className='optional-hide' style={{ textAlign: 'center', marginTop: '1em' }}>{translate('PHRASE_ALT_ALL')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('pulselaser')}</div>
|
<div className='select-group cap'>{translate('pl')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'pulselaser', 'fixed')}><MountFixed className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'pl', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pl-F'] = smRef}><MountFixed className='lg'/></li>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'pulselaser', 'gimbal')}><MountGimballed className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'pl', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pl-G'] = smRef}><MountGimballed className='lg'/></li>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'pulselaser', 'turret')}><MountTurret className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'pl', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pl-T'] = smRef}><MountTurret className='lg'/></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('burstlaser')}</div>
|
<div className='select-group cap'>{translate('ul')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'burstlaser', 'fixed')}><MountFixed className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'ul', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ul-F'] = smRef}><MountFixed className='lg'/></li>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'burstlaser', 'gimbal')}><MountGimballed className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'ul', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ul-G'] = smRef}><MountGimballed className='lg'/></li>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'burstlaser', 'turret')}><MountTurret className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'ul', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ul-T'] = smRef}><MountTurret className='lg'/></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('beamlaser')}</div>
|
<div className='select-group cap'>{translate('bl')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'beamlaser', 'fixed')}><MountFixed className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'bl', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['bl-F'] = smRef}><MountFixed className='lg'/></li>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'beamlaser', 'gimbal')}><MountGimballed className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'bl', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['bl-G'] = smRef}><MountGimballed className='lg'/></li>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'beamlaser', 'turret')}><MountTurret className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'bl', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['bl-T'] = smRef}><MountTurret className='lg'/></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('multicannon')}</div>
|
<div className='select-group cap'>{translate('mc')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'multicannon', 'fixed')}><MountFixed className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'mc', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['mc-F'] = smRef}><MountFixed className='lg'/></li>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'multicannon', 'gimbal')}><MountGimballed className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'mc', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['mc-G'] = smRef}><MountGimballed className='lg'/></li>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'multicannon', 'turret')}><MountTurret className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'mc', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['mc-T'] = smRef}><MountTurret className='lg'/></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('cannon')}</div>
|
<div className='select-group cap'>{translate('c')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'cannon', 'fixed')}><MountFixed className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'c', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['c-F'] = smRef}><MountFixed className='lg'/></li>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'cannon', 'gimbal')}><MountGimballed className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'c', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['c-G'] = smRef}><MountGimballed className='lg'/></li>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'cannon', 'turret')}><MountTurret className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'c', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['c-T'] = smRef}><MountTurret className='lg'/></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('fragcannon')}</div>
|
<div className='select-group cap'>{translate('fc')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'fragcannon', 'fixed')}><MountFixed className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'fc', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['fc-F'] = smRef}><MountFixed className='lg'/></li>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'fragcannon', 'gimbal')}><MountGimballed className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'fc', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['fc-G'] = smRef}><MountGimballed className='lg'/></li>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'fragcannon', 'turret')}><MountTurret className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'fc', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['fc-T'] = smRef}><MountTurret className='lg'/></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('plasmaacc')}</div>
|
<div className='select-group cap'>{translate('pa')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='lc' tabIndex="0" onClick={_fill.bind(this, 'plasmaacc', 'fixed')}>{translate('pa')}</li>
|
<li className='lc' tabIndex='0' onClick={_fill.bind(this, 'pa', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pa-F'] = smRef}>{translate('pa')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('railgun')}</div>
|
<div className='select-group cap'>{translate('nl')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='lc' tabIndex="0" onClick={_fill.bind(this, 'railgun', 'fixed')}>{translate('rg')}</li>
|
<li className='lc' tabIndex='0' onClick={_fill.bind(this, 'nl', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['nl-F'] = smRef}>{translate('nl')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('minelauncher')}</div>
|
<div className='select-group cap'>{translate('ggc')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='lc' tabIndex="0" onClick={_fill.bind(this, 'minelauncher', 'fixed')}>{translate('nl')}</li>
|
<li className='lc' tabIndex='0' onClick={_fill.bind(this, 'ggc', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ggc-F'] = smRef}>{translate('ggc')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('flaklauncher')}</div>
|
<div className='select-group cap'>{translate('rfl')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'flaklauncher', 'fixed')}><MountFixed className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'rfl', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['rfl-F'] = smRef}><MountFixed className='lg'/></li>
|
||||||
<li className="c hardpoint" tabIndex="0" onClick={_fill.bind(this, 'flaklauncher', 'turret')}><MountTurret className='lg'/></li>
|
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'rfl', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['rfl-T'] = smRef}><MountTurret className='lg'/></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import Link from './Link';
|
|||||||
import ActiveLink from './ActiveLink';
|
import ActiveLink from './ActiveLink';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { Cogs, CoriolisLogo, Hammer, Help, 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 Persist from '../stores/Persist';
|
||||||
|
import { toDetailedExport } from '../shipyard/Serializer';
|
||||||
|
import Ship from '../shipyard/Ship';
|
||||||
|
import ModalBatchOrbis from './ModalBatchOrbis';
|
||||||
import ModalDeleteAll from './ModalDeleteAll';
|
import ModalDeleteAll from './ModalDeleteAll';
|
||||||
import ModalExport from './ModalExport';
|
import ModalExport from './ModalExport';
|
||||||
import ModalHelp from './ModalHelp';
|
import ModalHelp from './ModalHelp';
|
||||||
@@ -14,9 +18,6 @@ import ModalImport from './ModalImport';
|
|||||||
import Slider from './Slider';
|
import Slider from './Slider';
|
||||||
import Announcement from './Announcement';
|
import Announcement from './Announcement';
|
||||||
import { outfitURL } from '../utils/UrlGenerators';
|
import { outfitURL } from '../utils/UrlGenerators';
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
import { Factory, Ship } from 'ed-forge';
|
|
||||||
import { chain, entries } from 'lodash';
|
|
||||||
|
|
||||||
const SIZE_MIN = 0.65;
|
const SIZE_MIN = 0.65;
|
||||||
const SIZE_RANGE = 0.55;
|
const SIZE_RANGE = 0.55;
|
||||||
@@ -55,6 +56,7 @@ function selectAll(e) {
|
|||||||
* Coriolis App Header section / menus
|
* Coriolis App Header section / menus
|
||||||
*/
|
*/
|
||||||
export default class Header extends TranslatedComponent {
|
export default class Header extends TranslatedComponent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
* @param {Object} props React Component properties
|
* @param {Object} props React Component properties
|
||||||
@@ -62,14 +64,24 @@ export default class Header extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
this.shipOrder = Object.keys(Ships).sort();
|
||||||
this.ships = Factory.getAllShipTypes().sort();
|
|
||||||
|
|
||||||
|
this._setLanguage = this._setLanguage.bind(this);
|
||||||
|
this._setInsurance = this._setInsurance.bind(this);
|
||||||
|
this._setShipDiscount = this._setShipDiscount.bind(this);
|
||||||
|
this._changeShipDiscount = this._changeShipDiscount.bind(this);
|
||||||
|
this._kpShipDiscount = this._kpShipDiscount.bind(this);
|
||||||
|
this._setModuleDiscount = this._setModuleDiscount.bind(this);
|
||||||
|
this._changeModuleDiscount = this._changeModuleDiscount.bind(this);
|
||||||
|
this._kpModuleDiscount = this._kpModuleDiscount.bind(this);
|
||||||
this._openShips = this._openMenu.bind(this, 's');
|
this._openShips = this._openMenu.bind(this, 's');
|
||||||
this._openBuilds = this._openMenu.bind(this, 'b');
|
this._openBuilds = this._openMenu.bind(this, 'b');
|
||||||
this._openComp = this._openMenu.bind(this, 'comp');
|
this._openComp = this._openMenu.bind(this, 'comp');
|
||||||
this._openAnnounce = this._openMenu.bind(this, 'announce');
|
this._openAnnounce = this._openMenu.bind(this, 'announce');
|
||||||
|
this._getAnnouncementsMenu = this._getAnnouncementsMenu.bind(this);
|
||||||
this._openSettings = this._openMenu.bind(this, 'settings');
|
this._openSettings = this._openMenu.bind(this, 'settings');
|
||||||
|
this._showHelp = this._showHelp.bind(this);
|
||||||
|
this.update = this.update.bind(this);
|
||||||
this.languageOptions = [];
|
this.languageOptions = [];
|
||||||
this.insuranceOptions = [];
|
this.insuranceOptions = [];
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -199,6 +211,13 @@ export default class Header extends TranslatedComponent {
|
|||||||
Persist.showTooltips(!Persist.showTooltips());
|
Persist.showTooltips(!Persist.showTooltips());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle module resistances setting
|
||||||
|
*/
|
||||||
|
_toggleModuleResistances() {
|
||||||
|
Persist.showModuleResistances(!Persist.showModuleResistances());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show delete all modal
|
* Show delete all modal
|
||||||
* @param {SyntheticEvent} e Event
|
* @param {SyntheticEvent} e Event
|
||||||
@@ -208,6 +227,57 @@ export default class Header extends TranslatedComponent {
|
|||||||
this.context.showModal(<ModalDeleteAll />);
|
this.context.showModal(<ModalDeleteAll />);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show export modal with backup data
|
||||||
|
* @param {SyntheticEvent} e Event
|
||||||
|
*/
|
||||||
|
_showBackup(e) {
|
||||||
|
let translate = this.context.language.translate;
|
||||||
|
e.preventDefault();
|
||||||
|
this.context.showModal(<ModalExport
|
||||||
|
title={translate('backup')}
|
||||||
|
description={translate('PHRASE_BACKUP_DESC')}
|
||||||
|
data={Persist.getAll()}
|
||||||
|
/>);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads all ship-builds to orbis
|
||||||
|
* @param {e} e Event
|
||||||
|
*/
|
||||||
|
_uploadAllBuildsToOrbis(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const data = Persist.getBuilds();
|
||||||
|
let postObject = [];
|
||||||
|
for (const ship in data) {
|
||||||
|
for (const code in data[ship]) {
|
||||||
|
const shipModel = ship;
|
||||||
|
if (!shipModel) {
|
||||||
|
throw 'No such ship found: "' + ship + '"';
|
||||||
|
}
|
||||||
|
const shipTemplate = Ships[shipModel];
|
||||||
|
const shipPostObject = {};
|
||||||
|
let shipInstance = new Ship(shipModel, shipTemplate.properties, shipTemplate.slots);
|
||||||
|
shipInstance.buildWith(null);
|
||||||
|
shipInstance.buildFrom(data[ship][code]);
|
||||||
|
shipPostObject.coriolisId = shipInstance.id;
|
||||||
|
shipPostObject.coriolisShip = shipInstance;
|
||||||
|
|
||||||
|
shipPostObject.coriolisShip.url = window.location.origin + outfitURL(shipModel, data[ship][code], code);
|
||||||
|
shipPostObject.title = code || shipInstance.id;
|
||||||
|
shipPostObject.description = code || shipInstance.id;
|
||||||
|
shipPostObject.ShipName = shipInstance.id;
|
||||||
|
shipPostObject.Ship = shipInstance.id;
|
||||||
|
postObject.push(shipPostObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(postObject);
|
||||||
|
|
||||||
|
this.context.showModal(<ModalBatchOrbis
|
||||||
|
ships={postObject}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show export modal with detailed export
|
* Show export modal with detailed export
|
||||||
* @param {SyntheticEvent} e Event
|
* @param {SyntheticEvent} e Event
|
||||||
@@ -216,22 +286,10 @@ export default class Header extends TranslatedComponent {
|
|||||||
let translate = this.context.language.translate;
|
let translate = this.context.language.translate;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const builds = chain(Persist.getBuilds())
|
|
||||||
.values()
|
|
||||||
.map((builds) => Object.values(builds))
|
|
||||||
.flatMap()
|
|
||||||
.map((code) => new Ship(code))
|
|
||||||
.value();
|
|
||||||
|
|
||||||
this.context.showModal(<ModalExport
|
this.context.showModal(<ModalExport
|
||||||
title={translate('detailed export')}
|
title={translate('detailed export')}
|
||||||
description={translate('PHRASE_EXPORT_DESC')}
|
description={translate('PHRASE_EXPORT_DESC')}
|
||||||
data={JSON.stringify(builds.map((build) => {
|
data={toDetailedExport(Persist.getBuilds())}
|
||||||
return {
|
|
||||||
header: { appName: 'Inara', 'appVersion': '1.0' },
|
|
||||||
data: build.toJSON(),
|
|
||||||
};
|
|
||||||
}))}
|
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,10 +347,15 @@ export default class Header extends TranslatedComponent {
|
|||||||
* @return {React.Component} Menu
|
* @return {React.Component} Menu
|
||||||
*/
|
*/
|
||||||
_getShipsMenu() {
|
_getShipsMenu() {
|
||||||
const { translate } = this.context.language;
|
let shipList = [];
|
||||||
|
|
||||||
|
for (let s in Ships) {
|
||||||
|
shipList.push(<ActiveLink key={s} href={outfitURL(s)} className='block'>{Ships[s].properties.name}</ActiveLink>);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='menu-list dbl no-wrap' onClick={ (e) => e.stopPropagation() }>
|
<div className='menu-list dbl no-wrap' onClick={ (e) => e.stopPropagation() }>
|
||||||
{this.ships.map((s) => <ActiveLink key={s} href={outfitURL(s)} className='block'>{translate(s)}</ActiveLink>)}
|
{shipList}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -302,10 +365,9 @@ export default class Header extends TranslatedComponent {
|
|||||||
* @return {React.Component} Menu
|
* @return {React.Component} Menu
|
||||||
*/
|
*/
|
||||||
_getBuildsMenu() {
|
_getBuildsMenu() {
|
||||||
const { translate } = this.context.language;
|
|
||||||
let builds = Persist.getBuilds();
|
let builds = Persist.getBuilds();
|
||||||
let buildList = [];
|
let buildList = [];
|
||||||
for (let shipId of this.ships) {
|
for (let shipId of this.shipOrder) {
|
||||||
if (builds[shipId]) {
|
if (builds[shipId]) {
|
||||||
let shipBuilds = [];
|
let shipBuilds = [];
|
||||||
let buildNameOrder = Object.keys(builds[shipId]).sort();
|
let buildNameOrder = Object.keys(builds[shipId]).sort();
|
||||||
@@ -313,7 +375,7 @@ export default class Header extends TranslatedComponent {
|
|||||||
let href = outfitURL(shipId, builds[shipId][buildName], buildName);
|
let href = outfitURL(shipId, builds[shipId][buildName], buildName);
|
||||||
shipBuilds.push(<li key={shipId + '-' + buildName} ><ActiveLink href={href} className='block'>{buildName}</ActiveLink></li>);
|
shipBuilds.push(<li key={shipId + '-' + buildName} ><ActiveLink href={href} className='block'>{buildName}</ActiveLink></li>);
|
||||||
}
|
}
|
||||||
buildList.push(<ul key={shipId}>{translate(shipId)}{shipBuilds}</ul>);
|
buildList.push(<ul key={shipId}>{Ships[shipId].properties.name}{shipBuilds}</ul>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,10 +426,7 @@ export default class Header extends TranslatedComponent {
|
|||||||
if (this.props.announcements) {
|
if (this.props.announcements) {
|
||||||
announcements = [];
|
announcements = [];
|
||||||
for (let announce of this.props.announcements) {
|
for (let announce of this.props.announcements) {
|
||||||
if (announce.expiry < Date.now()) {
|
announcements.push(<Announcement text={announce.message} />);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
announcements.push(<Announcement text={announce.text} />);
|
|
||||||
announcements.push(<hr/>);
|
announcements.push(<hr/>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -386,6 +445,7 @@ export default class Header extends TranslatedComponent {
|
|||||||
_getSettingsMenu() {
|
_getSettingsMenu() {
|
||||||
let translate = this.context.language.translate;
|
let translate = this.context.language.translate;
|
||||||
let tips = Persist.showTooltips();
|
let tips = Persist.showTooltips();
|
||||||
|
let moduleResistances = Persist.showModuleResistances();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='menu-list no-wrap cap' onClick={ (e) => e.stopPropagation() }>
|
<div className='menu-list no-wrap cap' onClick={ (e) => e.stopPropagation() }>
|
||||||
@@ -403,6 +463,10 @@ export default class Header extends TranslatedComponent {
|
|||||||
<td>{translate('tooltips')}</td>
|
<td>{translate('tooltips')}</td>
|
||||||
<td className={cn('ri', { disabled: !tips, 'primary-disabled': tips })}>{(tips ? '✓' : '✗')}</td>
|
<td className={cn('ri', { disabled: !tips, 'primary-disabled': tips })}>{(tips ? '✓' : '✗')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr className='cap ptr' onClick={this._toggleModuleResistances} >
|
||||||
|
<td>{translate('module resistances')}</td>
|
||||||
|
<td className={cn('ri', { disabled: !moduleResistances, 'primary-disabled': moduleResistances })}>{(moduleResistances ? '✓' : '✗')}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{translate('insurance')}</td>
|
<td>{translate('insurance')}</td>
|
||||||
<td className='ri'>
|
<td className='ri'>
|
||||||
@@ -430,9 +494,11 @@ export default class Header extends TranslatedComponent {
|
|||||||
<hr />
|
<hr />
|
||||||
<ul style={{ width: '100%' }}>
|
<ul style={{ width: '100%' }}>
|
||||||
{translate('builds')} & {translate('comparisons')}
|
{translate('builds')} & {translate('comparisons')}
|
||||||
<li><Link href="#" className='block' onClick={this._showDetailedExport}>{translate('detailed export')}</Link></li>
|
<li><Link href="#" className='block' onClick={this._showBackup.bind(this)}>{translate('backup')}</Link></li>
|
||||||
<li><Link href="#" className='block' onClick={this._showImport}>{translate('import')}</Link></li>
|
<li><Link href="#" className='block' onClick={this._showDetailedExport.bind(this)}>{translate('detailed export')}</Link></li>
|
||||||
<li><Link href="#" className='block' onClick={this._showDeleteAll}>{translate('delete all')}</Link></li>
|
<li><Link href="#" className='block' onClick={this._uploadAllBuildsToOrbis.bind(this)}>{translate('upload all builds to orbis')}</Link></li>
|
||||||
|
<li><Link href="#" className='block' onClick={this._showImport.bind(this)}>{translate('import')}</Link></li>
|
||||||
|
<li><Link href="#" className='block' onClick={this._showDeleteAll.bind(this)}>{translate('delete all')}</Link></li>
|
||||||
</ul>
|
</ul>
|
||||||
<hr />
|
<hr />
|
||||||
<table style={{ width: 300, backgroundColor: 'transparent' }}>
|
<table style={{ width: 300, backgroundColor: 'transparent' }}>
|
||||||
@@ -464,6 +530,7 @@ export default class Header extends TranslatedComponent {
|
|||||||
Persist.addListener('deletedAll', update);
|
Persist.addListener('deletedAll', update);
|
||||||
Persist.addListener('builds', update);
|
Persist.addListener('builds', update);
|
||||||
Persist.addListener('tooltips', update);
|
Persist.addListener('tooltips', update);
|
||||||
|
Persist.addListener('moduleresistances', update);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -533,13 +600,19 @@ export default class Header extends TranslatedComponent {
|
|||||||
{openedMenu == 'b' ? this._getBuildsMenu() : null}
|
{openedMenu == 'b' ? this._getBuildsMenu() : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TODO: Enable */}
|
<div className='l menu'>
|
||||||
{/* <div className='l menu'>
|
<div className={cn('menu-header', { selected: openedMenu == 'comp', disabled: !hasBuilds })} onClick={hasBuilds && this._openComp}>
|
||||||
|
<StatsBars className={cn('warning', { 'warning-disabled': !hasBuilds })} /><span className='menu-item-label'>{translate('compare')}</span>
|
||||||
|
</div>
|
||||||
|
{openedMenu == 'comp' ? this._getComparisonsMenu() : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='l menu'>
|
||||||
<div className={cn('menu-header', { selected: openedMenu == 'announce', disabled: this.props.announcements.length === 0})} onClick={this.props.announcements.length !== 0 && this._openAnnounce}>
|
<div className={cn('menu-header', { selected: openedMenu == 'announce', disabled: this.props.announcements.length === 0})} onClick={this.props.announcements.length !== 0 && this._openAnnounce}>
|
||||||
<span className='menu-item-label'>{translate('announcements')}</span>
|
<span className='menu-item-label'>{translate('announcements')}</span>
|
||||||
</div>
|
</div>
|
||||||
{openedMenu == 'announce' ? this._getAnnouncementsMenu() : null}
|
{openedMenu == 'announce' ? this._getAnnouncementsMenu() : null}
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
{window.location.origin.search('.edcd.io') >= 0 ?
|
{window.location.origin.search('.edcd.io') >= 0 ?
|
||||||
<div className='l menu'>
|
<div className='l menu'>
|
||||||
@@ -566,4 +639,5 @@ export default class Header extends TranslatedComponent {
|
|||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
99
src/app/components/InternalSlot.jsx
Normal file
99
src/app/components/InternalSlot.jsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import Slot from './Slot';
|
||||||
|
import Persist from '../stores/Persist';
|
||||||
|
import { ListModifications, Modified } from './SvgIcons';
|
||||||
|
import { Modifications } from 'coriolis-data/dist';
|
||||||
|
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||||
|
import { blueprintTooltip } from '../utils/BlueprintFunctions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal Slot
|
||||||
|
*/
|
||||||
|
export default class InternalSlot extends Slot {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the slot contents
|
||||||
|
* @param {Object} m Mounted Module
|
||||||
|
* @param {Boolean} enabled Slot enabled
|
||||||
|
* @param {Function} translate Translate function
|
||||||
|
* @param {Object} formats Localized Formats map
|
||||||
|
* @param {Object} u Localized Units Map
|
||||||
|
* @return {React.Component} Slot contents
|
||||||
|
*/
|
||||||
|
_getSlotDetails(m, enabled, translate, formats, u) {
|
||||||
|
if (m) {
|
||||||
|
let classRating = m.class + m.rating;
|
||||||
|
let { drag, drop, ship } = this.props;
|
||||||
|
let { termtip, tooltip } = this.context;
|
||||||
|
let validMods = (Modifications.modules[m.grp] ? Modifications.modules[m.grp].modifications : []);
|
||||||
|
let showModuleResistances = Persist.showModuleResistances();
|
||||||
|
|
||||||
|
// Modifications tooltip shows blueprint and grade, if available
|
||||||
|
let modTT = translate('modified');
|
||||||
|
if (m && m.blueprint && m.blueprint.name) {
|
||||||
|
modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
|
||||||
|
if (m.blueprint.special && m.blueprint.special.id >= 0) {
|
||||||
|
modTT += ', ' + translate(m.blueprint.special.name);
|
||||||
|
}
|
||||||
|
modTT = (
|
||||||
|
<div>
|
||||||
|
<div>{modTT}</div>
|
||||||
|
{blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mass = m.getMass() || m.cargo || m.fuel || 0;
|
||||||
|
|
||||||
|
const className = cn('details', enabled ? '' : 'disabled');
|
||||||
|
return <div className={className} draggable='true' onDragStart={drag} onDragEnd={drop}>
|
||||||
|
<div className={'cb'}>
|
||||||
|
<div className={'l'}>{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? <span onMouseOver={termtip.bind(null, modTT)} onMouseOut={tooltip.bind(null, null)}><Modified /></span> : ''}</div>
|
||||||
|
<div className={'r'}>{formats.round(mass)}{u.T}</div>
|
||||||
|
</div>
|
||||||
|
<div className={'cb'}>
|
||||||
|
{ m.getOptMass() ? <div className={'l'}>{translate('optmass', 'sg')}: {formats.int(m.getOptMass())}{u.T}</div> : null }
|
||||||
|
{ m.getMaxMass() ? <div className={'l'}>{translate('maxmass', 'sg')}: {formats.int(m.getMaxMass())}{u.T}</div> : null }
|
||||||
|
{ m.bins ? <div className={'l'}>{m.bins} <u>{translate('bins')}</u></div> : null }
|
||||||
|
{ m.bays ? <div className={'l'}>{translate('bays')}: {m.bays}</div> : null }
|
||||||
|
{ m.rebuildsperbay ? <div className={'l'}>{translate('rebuildsperbay')}: {m.rebuildsperbay}</div> : null }
|
||||||
|
{ m.rate ? <div className={'l'}>{translate('rate')}: {m.rate}{u.kgs} {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}</div> : null }
|
||||||
|
{ m.getAmmo() && m.grp !== 'scb' ? <div className={'l'}>{translate('ammunition')}: {formats.gen(m.getAmmo())}</div> : null }
|
||||||
|
{ m.getSpinup() ? <div className={'l'}>{translate('spinup')}: {formats.f1(m.getSpinup())}{u.s}</div> : null }
|
||||||
|
{ m.getDuration() ? <div className={'l'}>{translate('duration')}: {formats.f1(m.getDuration())}{u.s}</div> : null }
|
||||||
|
{ m.grp === 'scb' ? <div className={'l'}>{translate('cells')}: {formats.int(m.getAmmo() + 1)}</div> : null }
|
||||||
|
{ m.grp === 'gsrp' ? <div className={'l'}>{translate('shield addition')}: {formats.f1(m.getShieldAddition())}{u.MJ}</div> : null }
|
||||||
|
{ m.grp === 'gfsb' ? <div className={'l'}>{translate('jump addition')}: {formats.f1(m.getJumpBoost())}{u.LY}</div> : null }
|
||||||
|
{ m.grp === 'gs' ? <div className={'l'}>{translate('shield addition')}: {formats.f1(m.getShieldAddition())}{u.MJ}</div> : null }
|
||||||
|
{ m.getShieldReinforcement() ? <div className={'l'}>{translate('shieldreinforcement')}: {formats.f1(m.getDuration() * m.getShieldReinforcement())}{u.MJ}</div> : null }
|
||||||
|
{ m.getShieldReinforcement() ? <div className={'l'}>{translate('total')}: {formats.int((m.getAmmo() + 1) * (m.getDuration() * m.getShieldReinforcement()))}{u.MJ}</div> : null }
|
||||||
|
{ m.repair ? <div className={'l'}>{translate('repair')}: {m.repair}</div> : null }
|
||||||
|
{ m.getFacingLimit() ? <div className={'l'}>{translate('facinglimit')} {formats.f1(m.getFacingLimit())}°</div> : null }
|
||||||
|
{ m.getRange() ? <div className={'l'}>{translate('range')} {formats.f2(m.getRange())}{u.km}</div> : null }
|
||||||
|
{ m.getRangeT() ? <div className={'l'}>{translate('ranget')} {formats.f1(m.getRangeT())}{u.s}</div> : null }
|
||||||
|
{ m.getTime() ? <div className={'l'}>{translate('time')}: {formats.time(m.getTime())}</div> : null }
|
||||||
|
{ m.getHackTime() ? <div className={'l'}>{translate('hacktime')}: {formats.time(m.getHackTime())}</div> : null }
|
||||||
|
{ m.maximum ? <div className={'l'}>{translate('max')}: {(m.maximum)}</div> : null }
|
||||||
|
{ m.rangeLS ? <div className={'l'}>{translate('range')}: {m.rangeLS}{u.Ls}</div> : null }
|
||||||
|
{ m.rangeLS === null ? <div className={'l'}>∞{u.Ls}</div> : null }
|
||||||
|
{ m.rangeRating ? <div className={'l'}>{translate('range')}: {m.rangeRating}</div> : null }
|
||||||
|
{ m.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 }
|
||||||
|
{ showModuleResistances && m.getCausticResistance() ? <div className='l'>{translate('causres')}: {formats.pct(m.getCausticResistance())}</div> : null }
|
||||||
|
{ m.getHullReinforcement() ? <div className='l'>{translate('armour')}: {formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost') / 10000)}</div> : null }
|
||||||
|
{ m.getProtection() ? <div className='l'>{translate('protection')}: {formats.rPct(m.getProtection())}</div> : null }
|
||||||
|
{ m.getIntegrity() ? <div className='l'>{translate('integrity')}: {formats.int(m.getIntegrity())}</div> : null }
|
||||||
|
{ m && validMods.length > 0 ? <div className='r' tabIndex="0" ref={ modButton => this.modButton = modButton }><button tabIndex="-1" onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
return <div className={'empty'}>{translate('empty')}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +1,52 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SlotSection from './SlotSection';
|
import SlotSection from './SlotSection';
|
||||||
import Slot from './Slot';
|
import InternalSlot from './InternalSlot';
|
||||||
|
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||||
import autoBind from 'auto-bind';
|
import { canMount } from '../utils/SlotFunctions';
|
||||||
import { TYPES } from 'ed-forge/lib/src/data/slots';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets all empty slots of a ship to a item of the given size.
|
|
||||||
* @param {Ship} ship Ship to set items for
|
|
||||||
* @param {boolean} fillAll True to also fill occupied
|
|
||||||
* @param {string} type Item type
|
|
||||||
* @param {string} rating Item rating
|
|
||||||
*/
|
|
||||||
function setAllEmpty(ship, fillAll, type, rating = '') {
|
|
||||||
ship.getModules(TYPES.ANY_INTERNAL, undefined, true).forEach((slot) => {
|
|
||||||
if (slot.isEmpty() || fillAll) {
|
|
||||||
try {
|
|
||||||
// Maybe the item does not exist. Simply catch this error.
|
|
||||||
slot.setItem(type, slot.getSize(), rating);
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal slot section
|
* Internal slot section
|
||||||
*/
|
*/
|
||||||
export default class InternalSlotSection extends SlotSection {
|
export default class InternalSlotSection extends SlotSection {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
* @param {Object} props React Component properties
|
* @param {Object} props React Component properties
|
||||||
|
* @param {Object} context React Component context
|
||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props, context) {
|
||||||
super(props, 'optional internal');
|
super(props, context, 'internal', 'optional internal');
|
||||||
autoBind(this);
|
this._empty = this._empty.bind(this);
|
||||||
|
this._fillWithCargo = this._fillWithCargo.bind(this);
|
||||||
|
this._fillWithCells = this._fillWithCells.bind(this);
|
||||||
|
this._fillWithArmor = this._fillWithArmor.bind(this);
|
||||||
|
this._fillWithModuleReinforcementPackages = this._fillWithModuleReinforcementPackages.bind(this);
|
||||||
|
this._fillWithFuelTanks = this._fillWithFuelTanks.bind(this);
|
||||||
|
this._fillWithLuxuryCabins = this._fillWithLuxuryCabins.bind(this);
|
||||||
|
this._fillWithFirstClassCabins = this._fillWithFirstClassCabins.bind(this);
|
||||||
|
this._fillWithBusinessClassCabins = this._fillWithBusinessClassCabins.bind(this);
|
||||||
|
this._fillWithEconomyClassCabins = this._fillWithEconomyClassCabins.bind(this);
|
||||||
|
this.selectedRefId = null;
|
||||||
|
this.firstRefId = 'emptyall';
|
||||||
|
this.lastRefId = this.sectionRefArr['pcq'] ? 'pcq' : 'pcm';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle focus when component updates
|
||||||
|
* @param {Object} prevProps React Component properties
|
||||||
|
*/
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
this._handleSectionFocus(prevProps,this.firstRefId, this.lastRefId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Empty all slots
|
* Empty all slots
|
||||||
*/
|
*/
|
||||||
_empty() {
|
_empty() {
|
||||||
this.props.ship.getModules(TYPES.ANY_INTERNAL).forEach((slot) => slot.reset());
|
this.selectedRefId = 'emptyall';
|
||||||
|
this.props.ship.emptyInternal();
|
||||||
|
this.props.onChange();
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +55,15 @@ export default class InternalSlotSection extends SlotSection {
|
|||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
*/
|
*/
|
||||||
_fillWithCargo(event) {
|
_fillWithCargo(event) {
|
||||||
const fillAll = event.getModifierState('Alt');
|
this.selectedRefId = 'cargo';
|
||||||
setAllEmpty(this.props.ship, fillAll, 'cargorack');
|
let clobber = event.getModifierState('Alt');
|
||||||
|
let ship = this.props.ship;
|
||||||
|
ship.internal.forEach((slot) => {
|
||||||
|
if ((clobber || !slot.m) && canMount(ship, slot, 'cr')) {
|
||||||
|
ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.props.onChange();
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +72,15 @@ export default class InternalSlotSection extends SlotSection {
|
|||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
*/
|
*/
|
||||||
_fillWithFuelTanks(event) {
|
_fillWithFuelTanks(event) {
|
||||||
const fillAll = event.getModifierState('Alt');
|
this.selectedRefId = 'ft';
|
||||||
setAllEmpty(this.props.ship, fillAll, 'fueltank', '3');
|
let clobber = event.getModifierState('Alt');
|
||||||
|
let ship = this.props.ship;
|
||||||
|
ship.internal.forEach((slot) => {
|
||||||
|
if ((clobber || !slot.m) && canMount(ship, slot, 'ft')) {
|
||||||
|
ship.use(slot, ModuleUtils.findInternal('ft', slot.maxClass, 'C'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.props.onChange();
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,8 +89,15 @@ export default class InternalSlotSection extends SlotSection {
|
|||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
*/
|
*/
|
||||||
_fillWithLuxuryCabins(event) {
|
_fillWithLuxuryCabins(event) {
|
||||||
const fillAll = event.getModifierState('Alt');
|
this.selectedRefId = 'pcq';
|
||||||
setAllEmpty(this.props.ship, fillAll, 'passengercabins', '4');
|
let clobber = event.getModifierState('Alt');
|
||||||
|
let ship = this.props.ship;
|
||||||
|
ship.internal.forEach((slot) => {
|
||||||
|
if ((clobber || !slot.m) && canMount(ship, slot, 'pcq')) {
|
||||||
|
ship.use(slot, ModuleUtils.findInternal('pcq', Math.min(slot.maxClass, 6), 'B')); // Passenger cabins top out at 6
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.props.onChange();
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +106,15 @@ export default class InternalSlotSection extends SlotSection {
|
|||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
*/
|
*/
|
||||||
_fillWithFirstClassCabins(event) {
|
_fillWithFirstClassCabins(event) {
|
||||||
const fillAll = event.getModifierState('Alt');
|
this.selectedRefId = 'pcm';
|
||||||
setAllEmpty(this.props.ship, fillAll, 'passengercabins', '3');
|
let clobber = event.getModifierState('Alt');
|
||||||
|
let ship = this.props.ship;
|
||||||
|
ship.internal.forEach((slot) => {
|
||||||
|
if ((clobber || !slot.m) && canMount(ship, slot, 'pcm')) {
|
||||||
|
ship.use(slot, ModuleUtils.findInternal('pcm', Math.min(slot.maxClass, 6), 'C')); // Passenger cabins top out at 6
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.props.onChange();
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,8 +123,15 @@ export default class InternalSlotSection extends SlotSection {
|
|||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
*/
|
*/
|
||||||
_fillWithBusinessClassCabins(event) {
|
_fillWithBusinessClassCabins(event) {
|
||||||
const fillAll = event.getModifierState('Alt');
|
this.selectedRefId = 'pci';
|
||||||
setAllEmpty(this.props.ship, fillAll, 'passengercabins', '2');
|
let clobber = event.getModifierState('Alt');
|
||||||
|
let ship = this.props.ship;
|
||||||
|
ship.internal.forEach((slot) => {
|
||||||
|
if ((clobber || !slot.m) && canMount(ship, slot, 'pci')) {
|
||||||
|
ship.use(slot, ModuleUtils.findInternal('pci', Math.min(slot.maxClass, 6), 'D')); // Passenger cabins top out at 6
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.props.onChange();
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,8 +140,15 @@ export default class InternalSlotSection extends SlotSection {
|
|||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
*/
|
*/
|
||||||
_fillWithEconomyClassCabins(event) {
|
_fillWithEconomyClassCabins(event) {
|
||||||
const fillAll = event.getModifierState('Alt');
|
this.selectedRefId = 'pce';
|
||||||
setAllEmpty(this.props.ship, fillAll, 'passengercabins', '1');
|
let clobber = event.getModifierState('Alt');
|
||||||
|
let ship = this.props.ship;
|
||||||
|
ship.internal.forEach((slot) => {
|
||||||
|
if ((clobber || !slot.m) && canMount(ship, slot, 'pce')) {
|
||||||
|
ship.use(slot, ModuleUtils.findInternal('pce', Math.min(slot.maxClass, 6), 'E')); // Passenger cabins top out at 6
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.props.onChange();
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +157,18 @@ export default class InternalSlotSection extends SlotSection {
|
|||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
*/
|
*/
|
||||||
_fillWithCells(event) {
|
_fillWithCells(event) {
|
||||||
const fillAll = event.getModifierState('Alt');
|
this.selectedRefId = 'scb';
|
||||||
setAllEmpty(this.props.ship, fillAll, 'scb', '5');
|
let clobber = event.getModifierState('Alt');
|
||||||
|
let ship = this.props.ship;
|
||||||
|
let chargeCap = 0; // Capacity of single activation
|
||||||
|
ship.internal.forEach(function(slot) {
|
||||||
|
if ((clobber && !(slot.m && ModuleUtils.isShieldGenerator(slot.m.grp)) || !slot.m) && canMount(ship, slot, 'scb')) {
|
||||||
|
ship.use(slot, ModuleUtils.findInternal('scb', slot.maxClass, 'A'));
|
||||||
|
ship.setSlotEnabled(slot, chargeCap <= ship.shieldStrength); // Don't waste cell capacity on overcharge
|
||||||
|
chargeCap += slot.m.recharge;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.props.onChange();
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,8 +177,15 @@ export default class InternalSlotSection extends SlotSection {
|
|||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
*/
|
*/
|
||||||
_fillWithArmor(event) {
|
_fillWithArmor(event) {
|
||||||
const fillAll = event.getModifierState('Alt');
|
this.selectedRefId = 'hr';
|
||||||
setAllEmpty(this.props.ship, fillAll, 'hrp', '2');
|
let clobber = event.getModifierState('Alt');
|
||||||
|
let ship = this.props.ship;
|
||||||
|
ship.internal.forEach((slot) => {
|
||||||
|
if ((clobber || !slot.m) && canMount(ship, slot, 'hr')) {
|
||||||
|
ship.use(slot, ModuleUtils.findInternal('hr', Math.min(slot.maxClass, 5), 'D')); // Hull reinforcements top out at 5D
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.props.onChange();
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,31 +194,56 @@ export default class InternalSlotSection extends SlotSection {
|
|||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
*/
|
*/
|
||||||
_fillWithModuleReinforcementPackages(event) {
|
_fillWithModuleReinforcementPackages(event) {
|
||||||
const fillAll = event.getModifierState('Alt');
|
this.selectedRefId = 'mrp';
|
||||||
setAllEmpty(this.props.ship, fillAll, 'mrp', '2');
|
let clobber = event.getModifierState('Alt');
|
||||||
|
let ship = this.props.ship;
|
||||||
|
ship.internal.forEach((slot) => {
|
||||||
|
if ((clobber || !slot.m) && canMount(ship, slot, 'mrp')) {
|
||||||
|
ship.use(slot, ModuleUtils.findInternal('mrp', Math.min(slot.maxClass, 5), 'D')); // Module reinforcements top out at 5D
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.props.onChange();
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty all on section header right click
|
||||||
|
*/
|
||||||
|
_contextMenu() {
|
||||||
|
this._empty();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate the slot React Components
|
* Generate the slot React Components
|
||||||
* @return {Array} Array of Slots
|
* @return {Array} Array of Slots
|
||||||
*/
|
*/
|
||||||
_getSlots() {
|
_getSlots() {
|
||||||
let slots = [];
|
let slots = [];
|
||||||
let { currentMenu, ship, propsToShow, onPropToggle } = this.props;
|
let { currentMenu, ship } = this.props;
|
||||||
let { originSlot, targetSlot } = this.state;
|
let { originSlot, targetSlot } = this.state;
|
||||||
|
let { internal, fuelCapacity } = ship;
|
||||||
|
let availableModules = ship.getAvailableModules();
|
||||||
|
|
||||||
for (const m of ship.getInternals(undefined, true)) {
|
for (let i = 0, l = internal.length; i < l; i++) {
|
||||||
slots.push(<Slot
|
let s = internal[i];
|
||||||
key={m.object.Slot}
|
|
||||||
currentMenu={currentMenu}
|
slots.push(<InternalSlot
|
||||||
m={m}
|
key={i}
|
||||||
drag={this._drag.bind(this, m)}
|
maxClass={s.maxClass}
|
||||||
dragOver={this._dragOverSlot.bind(this, m)}
|
availableModules={() => availableModules.getInts(ship, s.maxClass, s.eligible)}
|
||||||
|
onOpen={this._openMenu.bind(this,s)}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
onSelect={this._selectModule.bind(this, s)}
|
||||||
|
selected={currentMenu == s}
|
||||||
|
eligible={s.eligible}
|
||||||
|
m={s.m}
|
||||||
|
drag={this._drag.bind(this, s)}
|
||||||
|
dragOver={this._dragOverSlot.bind(this, s)}
|
||||||
drop={this._drop}
|
drop={this._drop}
|
||||||
dropClass={this._dropClass(m, originSlot, targetSlot)}
|
dropClass={this._dropClass(s, originSlot, targetSlot)}
|
||||||
propsToShow={propsToShow}
|
fuel={fuelCapacity}
|
||||||
onPropToggle={onPropToggle}
|
ship={ship}
|
||||||
|
enabled={s.enabled ? true : false}
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,23 +256,22 @@ export default class InternalSlotSection extends SlotSection {
|
|||||||
* @param {Function} ship The ship
|
* @param {Function} ship The ship
|
||||||
* @return {React.Component} Section menu
|
* @return {React.Component} Section menu
|
||||||
*/
|
*/
|
||||||
_getSectionMenu() {
|
_getSectionMenu(translate, ship) {
|
||||||
const { ship } = this.props;
|
|
||||||
const { translate } = this.context.language;
|
|
||||||
return <div className='select' onClick={e => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
|
return <div className='select' onClick={e => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='lc' tabIndex='0' onClick={this._empty}>{translate('empty all')}</li>
|
<li className='lc' tabIndex='0' onClick={this._empty} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['emptyall'] = smRef}>{translate('empty all')}</li>
|
||||||
<li className='lc' tabIndex='0' onClick={this._fillWithCargo}>{translate('cargo')}</li>
|
<li className='lc' tabIndex='0' onClick={this._fillWithCargo} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['cargo'] = smRef}>{translate('cargo')}</li>
|
||||||
<li className='lc' tabIndex='0' onClick={this._fillWithCells}>{translate('scb')}</li>
|
<li className='lc' tabIndex='0' onClick={this._fillWithCells} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['scb'] = smRef}>{translate('scb')}</li>
|
||||||
<li className='lc' tabIndex='0' onClick={this._fillWithArmor}>{translate('hr')}</li>
|
<li className='lc' tabIndex='0' onClick={this._fillWithArmor} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['hr'] = smRef}>{translate('hr')}</li>
|
||||||
<li className='lc' tabIndex='0' onClick={this._fillWithModuleReinforcementPackages}>{translate('mrp')}</li>
|
<li className='lc' tabIndex='0' onClick={this._fillWithModuleReinforcementPackages} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['mrp'] = smRef}>{translate('mrp')}</li>
|
||||||
<li className='lc' tabIndex='0' onClick={this._fillWithFuelTanks}>{translate('ft')}</li>
|
<li className='lc' tabIndex='0' onClick={this._fillWithFuelTanks} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ft'] = smRef}>{translate('ft')}</li>
|
||||||
<li className='lc' tabIndex='0' onClick={this._fillWithEconomyClassCabins}>{translate('pce')}</li>
|
<li className='lc' tabIndex='0' onClick={this._fillWithEconomyClassCabins} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pce'] = smRef}>{translate('pce')}</li>
|
||||||
<li className='lc' tabIndex='0' onClick={this._fillWithBusinessClassCabins}>{translate('pci')}</li>
|
<li className='lc' tabIndex='0' onClick={this._fillWithBusinessClassCabins} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pci'] = smRef}>{translate('pci')}</li>
|
||||||
<li className='lc' tabIndex='0' onClick={this._fillWithFirstClassCabins} onKeyDown={ship.luxuryCabins ? '' : this._keyDown}>{translate('pcm')}</li>
|
<li className='lc' tabIndex='0' onClick={this._fillWithFirstClassCabins} onKeyDown={ship.luxuryCabins ? '' : this._keyDown} ref={smRef => this.sectionRefArr['pcm'] = smRef}>{translate('pcm')}</li>
|
||||||
{ ship.readMeta('luxuryCabins') ? <li className='lc' tabIndex='0' onClick={this._fillWithLuxuryCabins}>{translate('pcq')}</li> : ''}
|
{ ship.luxuryCabins ? <li className='lc' tabIndex='0' onClick={this._fillWithLuxuryCabins} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pcq'] = smRef}>{translate('pcq')}</li> : ''}
|
||||||
<li className='optional-hide' style={{ textAlign: 'center', marginTop: '1em' }}>{translate('PHRASE_ALT_ALL')}</li>
|
<li className='optional-hide' style={{ textAlign: 'center', marginTop: '1em' }}>{translate('PHRASE_ALT_ALL')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
120
src/app/components/JumpRange.jsx
Normal file
120
src/app/components/JumpRange.jsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
|
import LineChart from '../components/LineChart';
|
||||||
|
import Slider from '../components/Slider';
|
||||||
|
import * as Calc from '../shipyard/Calculations';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jump range for a given ship
|
||||||
|
*/
|
||||||
|
export default class JumpRange extends TranslatedComponent {
|
||||||
|
static propTypes = {
|
||||||
|
ship: PropTypes.object.isRequired,
|
||||||
|
code: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param {Object} props React Component properties
|
||||||
|
* @param {Object} context React Component context
|
||||||
|
*/
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const ship = this.props.ship;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
fuelLevel: 1,
|
||||||
|
calcJumpRangeFunc: this._calcJumpRange.bind(this, ship)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the state if our ship changes
|
||||||
|
* @param {Object} nextProps Incoming/Next properties
|
||||||
|
* @param {Object} nextContext Incoming/Next conext
|
||||||
|
* @return {boolean} Returns true if the component should be rerendered
|
||||||
|
*/
|
||||||
|
componentWillReceiveProps(nextProps, nextContext) {
|
||||||
|
if (nextProps.code != this.props.code) {
|
||||||
|
this.setState({ fuelLevel: 1,
|
||||||
|
calcJumpRangeFunc: this._calcJumpRange.bind(this, nextProps.ship) });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the jump range this ship at a given cargo
|
||||||
|
* @param {Object} ship The ship
|
||||||
|
* @param {Object} cargo The cargo
|
||||||
|
* @return {number} The jump range
|
||||||
|
*/
|
||||||
|
_calcJumpRange(ship, cargo) {
|
||||||
|
// Obtain the FSD for this ship
|
||||||
|
const fsd = ship.standard[2].m;
|
||||||
|
|
||||||
|
const fuel = this.state.fuelLevel * ship.fuelCapacity;
|
||||||
|
|
||||||
|
// Obtain the jump range
|
||||||
|
return Calc.jumpRange(ship.unladenMass + fuel + cargo, fsd, fuel, ship);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update fuel level
|
||||||
|
* @param {number} fuelLevel Fuel level 0 - 1
|
||||||
|
*/
|
||||||
|
_fuelChange(fuelLevel) {
|
||||||
|
this.setState({
|
||||||
|
fuelLevel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render engine profile
|
||||||
|
* @return {React.Component} contents
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||||
|
const { formats, translate, units } = language;
|
||||||
|
const { ship } = this.props;
|
||||||
|
const { fuelLevel } = this.state;
|
||||||
|
|
||||||
|
const code = ship.toString() + '.' + ship.getModificationsString() + '.' + fuelLevel;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<h1>{translate('jump range')}</h1>
|
||||||
|
<LineChart
|
||||||
|
xMax={ship.cargoCapacity}
|
||||||
|
yMax={ship.unladenRange}
|
||||||
|
xLabel={translate('cargo')}
|
||||||
|
xUnit={translate('T')}
|
||||||
|
yLabel={translate('jump range')}
|
||||||
|
yUnit={translate('LY')}
|
||||||
|
func={this.state.calcJumpRangeFunc}
|
||||||
|
points={200}
|
||||||
|
code={code}
|
||||||
|
/>
|
||||||
|
<h3>{translate('fuel carried')}: {formats.f2(fuelLevel * ship.fuelCapacity)}{units.T}</h3>
|
||||||
|
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
|
||||||
|
<tbody >
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Slider
|
||||||
|
axis={true}
|
||||||
|
onChange={this._fuelChange.bind(this)}
|
||||||
|
axisUnit={translate('T')}
|
||||||
|
percent={fuelLevel}
|
||||||
|
max={ship.fuelCapacity}
|
||||||
|
scale={sizeRatio}
|
||||||
|
onResize={onWindowResize}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||||||
import ContainerDimensions from 'react-container-dimensions';
|
import ContainerDimensions from 'react-container-dimensions';
|
||||||
import * as d3 from 'd3';
|
import * as d3 from 'd3';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
|
|
||||||
const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 };
|
const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 };
|
||||||
|
|
||||||
@@ -11,6 +10,7 @@ const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 };
|
|||||||
* Line Chart
|
* Line Chart
|
||||||
*/
|
*/
|
||||||
export default class LineChart extends TranslatedComponent {
|
export default class LineChart extends TranslatedComponent {
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
code: '',
|
code: '',
|
||||||
xMin: 0,
|
xMin: 0,
|
||||||
@@ -45,7 +45,13 @@ export default class LineChart extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
|
||||||
|
this._updateDimensions = this._updateDimensions.bind(this);
|
||||||
|
this._updateSeries = this._updateSeries.bind(this);
|
||||||
|
this._tooltip = this._tooltip.bind(this);
|
||||||
|
this._showTip = this._showTip.bind(this);
|
||||||
|
this._hideTip = this._hideTip.bind(this);
|
||||||
|
this._moveTip = this._moveTip.bind(this);
|
||||||
|
|
||||||
const series = props.series;
|
const series = props.series;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { shallowEqual } from '../utils/UtilityFunctions';
|
|||||||
* Link wrapper component
|
* Link wrapper component
|
||||||
*/
|
*/
|
||||||
export default class Link extends React.Component {
|
export default class Link extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
href: PropTypes.string.isRequired,
|
href: PropTypes.string.isRequired,
|
||||||
@@ -55,4 +56,5 @@ export default class Link extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return <a {...this.props} onClick={this.handler}>{this.props.children}</a>;
|
return <a {...this.props} onClick={this.handler}>{this.props.children}</a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
93
src/app/components/ModalBatchOrbis.jsx
Normal file
93
src/app/components/ModalBatchOrbis.jsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import request from 'superagent';
|
||||||
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
|
import { orbisUpload } from '../utils/ShortenUrl';
|
||||||
|
import Persist from '../stores/Persist';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permalink modal
|
||||||
|
*/
|
||||||
|
export default class ModalBatchOrbis extends TranslatedComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
ships: PropTypes.any.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param {Object} props React Component properties
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
orbisCreds: Persist.getOrbisCreds(),
|
||||||
|
resp: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send ship to Orbis.zone
|
||||||
|
* @param {SyntheticEvent} e React Event
|
||||||
|
* @return {Promise} Promise sending post request to orbis
|
||||||
|
*/
|
||||||
|
sendToOrbis(e) {
|
||||||
|
let agent;
|
||||||
|
try {
|
||||||
|
agent = request.agent(); // apparently this crashes somehow
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
if (!agent) {
|
||||||
|
agent = request;
|
||||||
|
}
|
||||||
|
const API_ORBIS = 'https://orbis.zone/api/builds/add/batch';
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
agent
|
||||||
|
.post(API_ORBIS)
|
||||||
|
.withCredentials()
|
||||||
|
.redirects(0)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(this.props.ships)
|
||||||
|
.end((err, response) => {
|
||||||
|
console.log(response);
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.setState({ resp: response.text });
|
||||||
|
reject('Bad Request');
|
||||||
|
} else {
|
||||||
|
this.setState({ resp: 'All builds uploaded. Check https://orbis.zone' });
|
||||||
|
resolve('All builds uploaded. Check https://orbis.zone');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
reject(e.message ? e.message : e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the modal
|
||||||
|
* @return {React.Component} Modal Content
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
let translate = this.context.language.translate;
|
||||||
|
this.sendToOrbis = this.sendToOrbis.bind(this);
|
||||||
|
|
||||||
|
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
||||||
|
<h2>{translate('permalink')}</h2>
|
||||||
|
<br/>
|
||||||
|
<a className='button' href="https://orbis.zone/api/auth">Log in / signup to Orbis</a>
|
||||||
|
<br/><br/>
|
||||||
|
<h3 >{translate('success')}</h3>
|
||||||
|
<input value={this.state.resp} readOnly size={25} onFocus={ (e) => e.target.select() }/>
|
||||||
|
<br/><br/>
|
||||||
|
<p>Orbis.zone is currently in a trial period, and may be wiped at any time as development progresses. Some elements are also still placeholders.</p>
|
||||||
|
<button className={'l cb dismiss cap'} disabled={!!this.state.failed} onClick={this.sendToOrbis}>{translate('PHASE_UPLOAD_ORBIS')}</button>
|
||||||
|
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ function buildComparator(a, b) {
|
|||||||
* Compare builds modal
|
* Compare builds modal
|
||||||
*/
|
*/
|
||||||
export default class ModalCompare extends TranslatedComponent {
|
export default class ModalCompare extends TranslatedComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onSelect: PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
builds: PropTypes.array
|
builds: PropTypes.array
|
||||||
@@ -104,8 +105,8 @@ export default class ModalCompare extends TranslatedComponent {
|
|||||||
|
|
||||||
let selectedBuilds = usedBuilds.map((build, i) =>
|
let selectedBuilds = usedBuilds.map((build, i) =>
|
||||||
<tr key={i} onClick={this._removeBuild.bind(this, i)}>
|
<tr key={i} onClick={this._removeBuild.bind(this, i)}>
|
||||||
<td className='tl'>{build.name}</td>
|
<td className='tl'>{build.name}</td><
|
||||||
<td className='tl'>{build.buildName}</td>
|
td className='tl'>{build.buildName}</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import TranslatedComponent from './TranslatedComponent';
|
|||||||
* Export Modal
|
* Export Modal
|
||||||
*/
|
*/
|
||||||
export default class ModalExport extends TranslatedComponent {
|
export default class ModalExport extends TranslatedComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
generator: PropTypes.func,
|
generator: PropTypes.func,
|
||||||
|
|||||||
@@ -7,10 +7,19 @@ import TranslatedComponent from './TranslatedComponent';
|
|||||||
* Help Modal
|
* Help Modal
|
||||||
*/
|
*/
|
||||||
export default class ModalHelp extends TranslatedComponent {
|
export default class ModalHelp extends TranslatedComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
title: PropTypes.string
|
title: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param {Object} props React Component properties
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the modal
|
* Render the modal
|
||||||
* @return {React.Component} Modal Content
|
* @return {React.Component} Modal Content
|
||||||
|
|||||||
@@ -2,23 +2,90 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
|
import Router from '../Router';
|
||||||
import Persist from '../stores/Persist';
|
import Persist from '../stores/Persist';
|
||||||
import autoBind from 'auto-bind';
|
import { Ships } from 'coriolis-data/dist';
|
||||||
import { isArray } from 'lodash';
|
import Ship from '../shipyard/Ship';
|
||||||
import { Ship } from 'ed-forge';
|
import { ModuleNameToGroup, Insurance } from '../shipyard/Constants';
|
||||||
|
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||||
|
import { fromDetailedBuild } from '../shipyard/Serializer';
|
||||||
|
import { Download } from './SvgIcons';
|
||||||
|
import { outfitURL } from '../utils/UrlGenerators';
|
||||||
|
import * as CompanionApiUtils from '../utils/CompanionApiUtils';
|
||||||
|
|
||||||
const STATE = {
|
const textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n');
|
||||||
READY: 0,
|
const lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)');
|
||||||
PARSED: 1,
|
const mountMap = { 'H': 4, 'L': 3, 'M': 2, 'S': 1, 'U': 0 };
|
||||||
ERROR: 2,
|
const standardMap = { 'RB': 0, 'TM': 1, 'FH': 2, 'EC': 3, 'PC': 4, 'SS': 5, 'FS': 6 };
|
||||||
};
|
const bhMap = { 'lightweight alloy': 0, 'reinforced alloy': 1, 'military grade composite': 2, 'mirrored surface composite': 3, 'reactive surface composite': 4 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check is slot is empty
|
||||||
|
* @param {Object} slot Slot model
|
||||||
|
* @return {Boolean} True if empty
|
||||||
|
*/
|
||||||
|
function isEmptySlot(slot) {
|
||||||
|
return slot.maxClass == this && slot.m === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a build is valid
|
||||||
|
* @param {string} shipId Ship ID
|
||||||
|
* @param {string} code Serialzied ship build 'code'
|
||||||
|
* @param {string} name Build name
|
||||||
|
* @throws {string} If build is not valid
|
||||||
|
*/
|
||||||
|
function validateBuild(shipId, code, name) {
|
||||||
|
let shipData = Ships[shipId];
|
||||||
|
|
||||||
|
if (!shipData) {
|
||||||
|
throw '"' + shipId + '" is not a valid Ship Id!';
|
||||||
|
}
|
||||||
|
if (typeof name != 'string' || name.length == 0) {
|
||||||
|
throw shipData.properties.name + ' build "' + name + '" must be a string at least 1 character long!';
|
||||||
|
}
|
||||||
|
if (typeof code != 'string' || code.length < 10) {
|
||||||
|
throw shipData.properties.name + ' build "' + name + '" is not valid!';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||||
|
ship.buildFrom(code);
|
||||||
|
} catch (e) {
|
||||||
|
throw shipData.properties.name + ' build "' + name + '" is not valid!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a ship-loadout JSON object to a Coriolis build
|
||||||
|
* @param {Object} detailedBuild ship-loadout
|
||||||
|
* @return {Object} Coriolis build
|
||||||
|
*/
|
||||||
|
function detailedJsonToBuild(detailedBuild) {
|
||||||
|
let ship;
|
||||||
|
if (!detailedBuild.name) {
|
||||||
|
throw 'Build Name missing!';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!detailedBuild.name.trim()) {
|
||||||
|
throw 'Build Name must be a string at least 1 character long!';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ship = fromDetailedBuild(detailedBuild);
|
||||||
|
} catch (e) {
|
||||||
|
throw detailedBuild.ship + ' Build "' + detailedBuild.name + '": Invalid data';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { shipId: ship.id, name: detailedBuild.name, code: ship.toString() };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import Modal
|
* Import Modal
|
||||||
*/
|
*/
|
||||||
export default class ModalImport extends TranslatedComponent {
|
export default class ModalImport extends TranslatedComponent {
|
||||||
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
importString: PropTypes.string, // Optional: Default data for import modal
|
|
||||||
builds: PropTypes.object, // Optional: Import object
|
builds: PropTypes.object, // Optional: Import object
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,36 +95,211 @@ export default class ModalImport extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
status: STATE.READY,
|
builds: props.builds,
|
||||||
builds: props.builds || [],
|
canEdit: !props.builds,
|
||||||
|
comparisons: null,
|
||||||
|
shipDiscount: null,
|
||||||
|
moduleDiscount: null,
|
||||||
|
errorMsg: null,
|
||||||
|
importString: null,
|
||||||
|
importValid: false,
|
||||||
|
insurance: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._process = this._process.bind(this);
|
||||||
|
this._import = this._import.bind(this);
|
||||||
|
this._importBackup = this._importBackup.bind(this);
|
||||||
|
this._importDetailedArray = this._importDetailedArray.bind(this);
|
||||||
|
this._importTextBuild = this._importTextBuild.bind(this);
|
||||||
|
this._importCompanionApiBuild = this._importCompanionApiBuild.bind(this);
|
||||||
|
this._validateImport = this._validateImport.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import SLEF formatted builds. Sets state to a map of the builds on success
|
* Import a Coriolis backup
|
||||||
* and flags if there was only a single build.
|
* @param {Object} importData Backup Data
|
||||||
*
|
* @throws {string} If import fails
|
||||||
* @param {string} importData - Array of the list of builds.
|
*/
|
||||||
|
_importBackup(importData) {
|
||||||
|
if (importData.builds && typeof importData.builds == 'object') {
|
||||||
|
for (let shipId in importData.builds) {
|
||||||
|
for (let buildName in importData.builds[shipId]) {
|
||||||
|
try {
|
||||||
|
validateBuild(shipId, importData.builds[shipId][buildName], buildName);
|
||||||
|
} catch (err) {
|
||||||
|
delete importData.builds[shipId][buildName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({ builds: importData.builds });
|
||||||
|
} else {
|
||||||
|
throw 'builds must be an object!';
|
||||||
|
}
|
||||||
|
if (importData.comparisons) {
|
||||||
|
for (let compName in importData.comparisons) {
|
||||||
|
let comparison = importData.comparisons[compName];
|
||||||
|
for (let i = 0, l = comparison.builds.length; i < l; i++) {
|
||||||
|
let build = comparison.builds[i];
|
||||||
|
if (!importData.builds[build.shipId] || !importData.builds[build.shipId][build.buildName]) {
|
||||||
|
throw build.shipId + ' build "' + build.buildName + '" data is missing!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({ comparisons: importData.comparisons });
|
||||||
|
}
|
||||||
|
// Check for old/deprecated discounts
|
||||||
|
if (importData.discounts instanceof Array && importData.discounts.length == 2) {
|
||||||
|
this.setState({ shipDiscount: importData.discounts[0], moduleDiscount: importData.discounts[1] });
|
||||||
|
}
|
||||||
|
// Check for ship discount
|
||||||
|
if (!isNaN(importData.shipDiscount)) {
|
||||||
|
this.setState({ shipDiscount: importData.shipDiscount * 1 });
|
||||||
|
}
|
||||||
|
// Check for module discount
|
||||||
|
if (!isNaN(importData.moduleDiscount)) {
|
||||||
|
this.setState({ shipDiscount: importData.moduleDiscount * 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof importData.insurance == 'string') {
|
||||||
|
let insurance = importData.insurance.toLowerCase();
|
||||||
|
|
||||||
|
if (Insurance[insurance] !== undefined) {
|
||||||
|
this.setState({ insurance });
|
||||||
|
} else {
|
||||||
|
throw 'Invalid insurance type: ' + insurance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import an array of ship-loadout objects / builds
|
||||||
|
* @param {Array} importArr Array of ship-loadout JSON Schema builds
|
||||||
|
*/
|
||||||
|
_importDetailedArray(importArr) {
|
||||||
|
let builds = {};
|
||||||
|
for (let i = 0, l = importArr.length; i < l; i++) {
|
||||||
|
let build = detailedJsonToBuild(importArr[i]);
|
||||||
|
if (!builds[build.shipId]) {
|
||||||
|
builds[build.shipId] = {};
|
||||||
|
}
|
||||||
|
builds[build.shipId][build.name] = build.code;
|
||||||
|
}
|
||||||
|
this.setState({ builds });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a build direct from the companion API
|
||||||
|
* @param {string} build JSON from the companion API information
|
||||||
|
* @throws {string} if parse/import fails
|
||||||
|
*/
|
||||||
|
_importCompanionApiBuild(build) {
|
||||||
|
const shipModel = CompanionApiUtils.shipModelFromJson(build);
|
||||||
|
const ship = CompanionApiUtils.shipFromJson(build);
|
||||||
|
|
||||||
|
let builds = {};
|
||||||
|
builds[shipModel] = {};
|
||||||
|
builds[shipModel]['Imported ' + Ships[shipModel].properties.name] = ship.toString();
|
||||||
|
this.setState({ builds, singleBuild: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a text build from ED Shipyard
|
||||||
|
* @param {string} buildStr Build string
|
||||||
* @throws {string} If parse / import fails
|
* @throws {string} If parse / import fails
|
||||||
*/
|
*/
|
||||||
_importSlefBuilds(importData) {
|
_importTextBuild(buildStr) {
|
||||||
const builds = importData.reduce((memo, { data }) => {
|
let buildName = textBuildRegex.exec(buildStr)[1].trim();
|
||||||
const shipModel = shipModelFromJson(data);
|
let shipName = buildName.toLowerCase();
|
||||||
const ship = shipFromLoadoutJSON(data);
|
let shipId = null;
|
||||||
const shipTemplate = Ships[shipModel];
|
|
||||||
const shipName = data.ShipName || shipTemplate.properties.name;
|
|
||||||
|
|
||||||
const key = `Imported ${shipName}`;
|
for (let sId in Ships) {
|
||||||
memo[shipModel] = {};
|
if (Ships[sId].properties.name.toLowerCase() == shipName) {
|
||||||
memo[shipModel][key] = ship.toString();
|
shipId = sId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return memo;
|
if (!shipId) {
|
||||||
}, {});
|
throw 'No such ship found: "' + buildName + '"';
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ builds, singleBuild: Object.keys(builds).length === 1 });
|
let lines = buildStr.split('\n');
|
||||||
|
let ship = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots);
|
||||||
|
ship.buildWith(null);
|
||||||
|
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
let line = lines[i].trim();
|
||||||
|
|
||||||
|
if (!line) { continue; }
|
||||||
|
if (line.substring(0, 3) == '---') { break; }
|
||||||
|
|
||||||
|
let parts = lineRegex.exec(line);
|
||||||
|
|
||||||
|
if (!parts) { throw 'Error parsing: "' + line + '"'; }
|
||||||
|
|
||||||
|
let typeSize = parts[1];
|
||||||
|
let cl = parts[2];
|
||||||
|
let rating = parts[3];
|
||||||
|
let mount = parts[4];
|
||||||
|
let missile = parts[5];
|
||||||
|
let name = parts[6].trim();
|
||||||
|
let slot, group;
|
||||||
|
|
||||||
|
if (isNaN(typeSize)) { // Standard or Hardpoint
|
||||||
|
if (typeSize.length == 1) { // Hardpoint
|
||||||
|
let slotClass = mountMap[typeSize];
|
||||||
|
|
||||||
|
if (cl > slotClass) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; }
|
||||||
|
|
||||||
|
slot = ship.hardpoints.find(isEmptySlot, slotClass);
|
||||||
|
|
||||||
|
if (!slot) { throw 'No hardpoint slot available for: "' + line + '"'; }
|
||||||
|
|
||||||
|
group = ModuleNameToGroup[name.toLowerCase()];
|
||||||
|
|
||||||
|
let hp = ModuleUtils.findHardpoint(group, cl, rating, group ? null : name, mount, missile);
|
||||||
|
|
||||||
|
if (!hp) { throw 'Unknown component: "' + line + '"'; }
|
||||||
|
|
||||||
|
ship.use(slot, hp, true);
|
||||||
|
} else if (typeSize == 'BH') {
|
||||||
|
let bhId = bhMap[name.toLowerCase()];
|
||||||
|
|
||||||
|
if (bhId === undefined) { throw 'Unknown bulkhead: "' + line + '"'; }
|
||||||
|
|
||||||
|
ship.useBulkhead(bhId, true);
|
||||||
|
} else if (standardMap[typeSize] != undefined) {
|
||||||
|
let standardIndex = standardMap[typeSize];
|
||||||
|
|
||||||
|
if (ship.standard[standardIndex].maxClass < cl) { throw name + ' exceeds max class for the ' + ship.name; }
|
||||||
|
|
||||||
|
ship.use(ship.standard[standardIndex], ModuleUtils.standard(standardIndex, cl + rating), true);
|
||||||
|
} else {
|
||||||
|
throw 'Unknown component: "' + line + '"';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (cl > typeSize) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; }
|
||||||
|
|
||||||
|
slot = ship.internal.find(isEmptySlot, typeSize);
|
||||||
|
|
||||||
|
if (!slot) { throw 'No internal slot available for: "' + line + '"'; }
|
||||||
|
|
||||||
|
group = ModuleNameToGroup[name.toLowerCase()];
|
||||||
|
|
||||||
|
let intComp = ModuleUtils.findInternal(group, cl, rating, group ? null : name);
|
||||||
|
|
||||||
|
if (!intComp) { throw 'Unknown component: "' + line + '"'; }
|
||||||
|
|
||||||
|
ship.use(slot, intComp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let builds = {};
|
||||||
|
builds[shipId] = {};
|
||||||
|
builds[shipId]['Imported ' + buildName] = ship.toString();
|
||||||
|
this.setState({ builds, singleBuild: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,50 +307,158 @@ export default class ModalImport extends TranslatedComponent {
|
|||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
* @throws {string} If validation fails
|
* @throws {string} If validation fails
|
||||||
*/
|
*/
|
||||||
_parse(event) {
|
_validateImport(event) {
|
||||||
const importString = event.target.value.trim();
|
let importData = null;
|
||||||
try {
|
let importString = event.target.value.trim();
|
||||||
let data = JSON.parse(importString);
|
this.setState({
|
||||||
if (!isArray(data)) {
|
builds: null,
|
||||||
data = [data];
|
comparisons: null,
|
||||||
|
shipDiscount: null,
|
||||||
|
moduleDiscount: null,
|
||||||
|
errorMsg: null,
|
||||||
|
importValid: false,
|
||||||
|
insurance: null,
|
||||||
|
singleBuild: false,
|
||||||
|
importString,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!importString) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ships = data.map((item) => {
|
|
||||||
try {
|
try {
|
||||||
return new Ship(item.data ? item.data : item);
|
if (textBuildRegex.test(importString)) { // E:D Shipyard build text
|
||||||
} catch (err) {
|
this._importTextBuild(importString);
|
||||||
return err;
|
} else { // JSON Build data
|
||||||
|
importData = JSON.parse(importString);
|
||||||
|
|
||||||
|
if (!importData || typeof importData != 'object') {
|
||||||
|
throw 'Must be an object or array!';
|
||||||
}
|
}
|
||||||
});
|
|
||||||
this.setState({ ships, status: STATE.PARSED });
|
if (importData.modules != null && importData.modules.Armour != null) { // Only the companion API has this information
|
||||||
} catch (err) {
|
this._importCompanionApiBuild(importData); // Single sihp definition
|
||||||
this.setState({ err, status: STATE.ERROR });
|
} else if (importData.ship != null && importData.ship.modules != null && importData.ship.modules.Armour != null) { // Only the companion API has this information
|
||||||
|
this._importCompanionApiBuild(importData.ship); // Complete API dump
|
||||||
|
} else if (importData instanceof Array) { // Must be detailed export json
|
||||||
|
this._importDetailedArray(importData);
|
||||||
|
} else if (importData.ship && typeof importData.name !== undefined) { // Using JSON from a single ship build export
|
||||||
|
this._importDetailedArray([importData]); // Convert to array with singleobject
|
||||||
|
this.setState({ singleBuild: true });
|
||||||
|
} else { // Using Backup JSON
|
||||||
|
this._importBackup(importData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// console.log(e.stack);
|
||||||
|
this.setState({ errorMsg: (typeof e == 'string') ? e : 'Cannot Parse the data!' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ importValid: true });
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process imported data
|
* Process imported data
|
||||||
*/
|
*/
|
||||||
_process() {
|
_process() {
|
||||||
for (const build of this.state.builds) {
|
let builds = null, comparisons = null;
|
||||||
if (!build instanceof Error) {
|
|
||||||
Persist.saveBuild(build.Ship, build.CoriolisBuildName || build.ShipName, build.compress());
|
// If only importing a single build go straight to the outfitting page
|
||||||
|
if (this.state.singleBuild) {
|
||||||
|
builds = this.state.builds;
|
||||||
|
let shipId = Object.keys(builds)[0];
|
||||||
|
let name = Object.keys(builds[shipId])[0];
|
||||||
|
Router.go(outfitURL(shipId, builds[shipId][name], name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (this.state.builds) {
|
||||||
|
builds = {}; // Create new builds object such that orginal name retained, but can be renamed
|
||||||
|
for (let shipId in this.state.builds) {
|
||||||
|
let shipbuilds = this.state.builds[shipId];
|
||||||
|
builds[shipId] = {};
|
||||||
|
for (let buildName in shipbuilds) {
|
||||||
|
builds[shipId][buildName] = {
|
||||||
|
code: shipbuilds[buildName],
|
||||||
|
useName: buildName
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setState({ builds: [], status: STATE.READY });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.comparisons) {
|
||||||
|
comparisons = {};
|
||||||
|
for (let name in this.state.comparisons) {
|
||||||
|
comparisons[name] = Object.assign({ useName: name }, this.state.comparisons[name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ processed: true, builds, comparisons });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import parsed, processed data and save
|
||||||
|
*/
|
||||||
|
_import() {
|
||||||
|
let state = this.state;
|
||||||
|
if (state.builds) {
|
||||||
|
let builds = state.builds;
|
||||||
|
for (let shipId in builds) {
|
||||||
|
for (let buildName in builds[shipId]) {
|
||||||
|
let build = builds[shipId][buildName];
|
||||||
|
let name = build.useName.trim();
|
||||||
|
if (name) {
|
||||||
|
Persist.saveBuild(shipId, name, build.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.comparisons) {
|
||||||
|
let comparisons = state.comparisons;
|
||||||
|
for (let comp in comparisons) {
|
||||||
|
let comparison = comparisons[comp];
|
||||||
|
let useName = comparison.useName.trim();
|
||||||
|
if (useName) {
|
||||||
|
Persist.saveComparison(useName, comparison.builds, comparison.facets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.shipDiscount !== undefined) {
|
||||||
|
Persist.setShipDiscount(state.shipDiscount);
|
||||||
|
}
|
||||||
|
if (state.moduleDiscount !== undefined) {
|
||||||
|
Persist.setModuleDiscount(state.moduleDiscount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.insurance) {
|
||||||
|
Persist.setInsurance(state.insurance);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.hideModal();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capture build name changes
|
* Capture build name changes
|
||||||
* @param {Object} index Build/Comparison import object
|
* @param {Object} item Build/Comparison import object
|
||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} e Event
|
||||||
*/
|
*/
|
||||||
_changeName(index, event) {
|
_changeName(item, e) {
|
||||||
const { builds } = this.state;
|
item.useName = e.target.value;
|
||||||
builds[index].CoriolisBuildName = event.target.value.trim();
|
this.forceUpdate();
|
||||||
this.setState({ builds });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If imported data is already provided process immediately on mount
|
||||||
|
*/
|
||||||
|
componentWillMount() {
|
||||||
|
if (this.props.builds) {
|
||||||
|
this._process();
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* If textarea is shown focus on mount
|
* If textarea is shown focus on mount
|
||||||
*/
|
*/
|
||||||
@@ -123,34 +473,77 @@ export default class ModalImport extends TranslatedComponent {
|
|||||||
* @return {React.Component} Modal contents
|
* @return {React.Component} Modal contents
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { translate } = this.context.language;
|
let translate = this.context.language.translate;
|
||||||
const { status, builds, err } = this.state;
|
let state = this.state;
|
||||||
|
let importStage;
|
||||||
|
|
||||||
const buildRows = builds.map((build, i) => {
|
if (!state.processed) {
|
||||||
if (build instanceof Error) {
|
importStage = (
|
||||||
return <tr key={i} className='cb'>
|
<div>
|
||||||
<td colSpan={3} className='warning'>Error: {build.name}</td>
|
<textarea className='cb json' ref={node => this.importField = node} onChange={this._validateImport} defaultValue={this.state.importString} placeholder={translate('PHRASE_IMPORT')} />
|
||||||
</tr>;
|
<button id='proceed' className='l cap' onClick={this._process} disabled={!state.importValid} >{translate('proceed')}</button>
|
||||||
|
<div className='l warning' style={{ marginLeft:'3em' }}>{state.errorMsg}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let comparisonTable, edit, buildRows = [];
|
||||||
|
if (state.comparisons) {
|
||||||
|
let comparisonRows = [];
|
||||||
|
|
||||||
|
for (let name in state.comparisons) {
|
||||||
|
let comparison = state.comparisons[name];
|
||||||
|
let hasComparison = Persist.hasComparison(comparison.useName);
|
||||||
|
comparisonRows.push(
|
||||||
|
<tr key={name} className='cb'>
|
||||||
|
<td>
|
||||||
|
<input type='text' onChange={this._changeName.bind(this, comparison)} value={comparison.useName}/>
|
||||||
|
</td>
|
||||||
|
<td style={{ textAlign:'center' }} className={ cn('cap', { warning: hasComparison, disabled: comparison.useName == '' }) }>
|
||||||
|
{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exists = Persist.hasBuild(build.Ship, build.CoriolisBuildName);
|
comparisonTable = (
|
||||||
const saveName = build.CoriolisBuildName || build.ShipName;
|
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%' }} >
|
||||||
return <tr key={i} className='cb'>
|
<thead>
|
||||||
<td>{translate(build.Ship)}</td>
|
<tr>
|
||||||
<td><input type='text' onChange={this._changeName.bind(this, i)} value={saveName}/></td>
|
<th style={{ textAlign:'left' }}>{translate('comparison')}</th>
|
||||||
<td style={{ textAlign: 'center' }} className={cn('cap', { warning: exists, disabled: saveName === '' })}>
|
<th>{translate('action')}</th>
|
||||||
{translate(saveName === '' ? 'skip' : (exists ? 'overwrite' : 'create'))}
|
</tr>
|
||||||
</td>
|
</thead>
|
||||||
</tr>;
|
<tbody>
|
||||||
});
|
{comparisonRows}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
if(this.state.canEdit) {
|
||||||
<h2 >{translate('import')}</h2>
|
edit = <button className='l cap' style={{ marginLeft: '2em' }} onClick={() => this.setState({ processed: false })}>{translate('edit data')}</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let builds = this.state.builds;
|
||||||
|
for (let shipId in builds) {
|
||||||
|
let shipBuilds = builds[shipId];
|
||||||
|
for (let buildName in shipBuilds) {
|
||||||
|
let b = shipBuilds[buildName];
|
||||||
|
let hasBuild = Persist.hasBuild(shipId, b.useName);
|
||||||
|
buildRows.push(
|
||||||
|
<tr key={shipId + buildName} className='cb'>
|
||||||
|
<td>{Ships[shipId].properties.name}</td>
|
||||||
|
<td><input type='text' onChange={this._changeName.bind(this, b)} value={b.useName}/></td>
|
||||||
|
<td style={{ textAlign: 'center' }} className={cn('cap', { warning: hasBuild, disabled: b.useName == '' })}>
|
||||||
|
{translate(b.useName == '' ? 'skip' : (hasBuild ? 'overwrite' : 'create'))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
importStage = (
|
||||||
<div>
|
<div>
|
||||||
<textarea spellCheck={false} className='cb json' ref={node => this.importField = node} onChange={this._parse} defaultValue={this.state.importString} placeholder={translate('PHRASE_IMPORT')} />
|
|
||||||
{status === STATE.ERROR && <div className='l warning' style={{ marginLeft:'3em' }}>{err.toString()}</div>}
|
|
||||||
</div>
|
|
||||||
{builds.length && <div>
|
|
||||||
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%' }}>
|
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -163,14 +556,17 @@ export default class ModalImport extends TranslatedComponent {
|
|||||||
{buildRows}
|
{buildRows}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>}
|
{comparisonTable}
|
||||||
<button id='proceed' className='l cap' onClick={this._process}
|
<button id='import' className='cl l' onClick={this._import}><Download/> {translate('import')}</button>
|
||||||
disabled={status !== STATE.PARSED} >
|
{edit}
|
||||||
{translate('proceed')}
|
</div>
|
||||||
</button>
|
);
|
||||||
<button className={'r dismiss cap'} onClick={this.context.hideModal}>
|
}
|
||||||
{translate('close')}
|
|
||||||
</button>
|
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
||||||
|
<h2 >{translate('import')}</h2>
|
||||||
|
{importStage}
|
||||||
|
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
141
src/app/components/ModalOrbis.jsx
Normal file
141
src/app/components/ModalOrbis.jsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
|
import { orbisUpload } from '../utils/ShortenUrl';
|
||||||
|
import Persist from '../stores/Persist';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permalink modal
|
||||||
|
*/
|
||||||
|
export default class ModalOrbis extends TranslatedComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
ship: PropTypes.any.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param {Object} props React Component properties
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
orbisCreds: Persist.getOrbisCreds(),
|
||||||
|
orbisUrl: '...',
|
||||||
|
ship: this.props.ship,
|
||||||
|
authenticatedStatus: 'Checking...'
|
||||||
|
};
|
||||||
|
this.orbisCategory = this.orbisCategory.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send ship to Orbis.zone
|
||||||
|
* @param {SyntheticEvent} e React Event
|
||||||
|
*/
|
||||||
|
sendToOrbis(e) {
|
||||||
|
const target = e.target;
|
||||||
|
target.disabled = true;
|
||||||
|
this.setState({ orbisUrl: 'Sending...' }, () => {
|
||||||
|
orbisUpload(this.props.ship, this.state.orbisCreds)
|
||||||
|
.then(orbisUrl => {
|
||||||
|
target.disabled = false;
|
||||||
|
this.setState({ orbisUrl });
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
target.disabled = false;
|
||||||
|
this.setState({ orbisUrl: 'Error - ' + err });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Orbis.zone auth status
|
||||||
|
* @returns {Object} auth status
|
||||||
|
*/
|
||||||
|
getOrbisAuthStatus() {
|
||||||
|
return fetch('https://orbis.zone/api/checkauth', {
|
||||||
|
credentials: 'include',
|
||||||
|
mode: 'cors'
|
||||||
|
})
|
||||||
|
.then(data => data.json())
|
||||||
|
.then(res => {
|
||||||
|
this.setState({ authenticatedStatus: res.status || res.error });
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.setState({ authenticatedStatus: err.message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for changing cmdr name
|
||||||
|
* @param {SyntheticEvent} e React Event
|
||||||
|
*/
|
||||||
|
orbisPasswordHandler(e) {
|
||||||
|
let password = e.target.value;
|
||||||
|
this.setState({ orbisCreds: { email: this.state.orbisCreds.email, password } }, () => {
|
||||||
|
Persist.setOrbisCreds(this.state.orbisCreds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for changing cmdr name
|
||||||
|
* @param {SyntheticEvent} e React Event
|
||||||
|
*/
|
||||||
|
orbisUsername(e) {
|
||||||
|
let orbisUsername = e.target.value;
|
||||||
|
this.setState({ orbisCreds: { email: orbisUsername, password: this.state.orbisCreds.password } }, () => {
|
||||||
|
Persist.setOrbisCreds(this.state.orbisCreds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for changing category
|
||||||
|
* @param {SyntheticEvent} e React Event
|
||||||
|
*/
|
||||||
|
orbisCategory(e) {
|
||||||
|
let ship = this.state.ship;
|
||||||
|
let cat = e.target.value;
|
||||||
|
ship.category = cat;
|
||||||
|
this.setState({ship});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the modal
|
||||||
|
* @return {React.Component} Modal Content
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
let translate = this.context.language.translate;
|
||||||
|
this.orbisPasswordHandler = this.orbisPasswordHandler.bind(this);
|
||||||
|
this.orbisUsername = this.orbisUsername.bind(this);
|
||||||
|
this.sendToOrbis = this.sendToOrbis.bind(this);
|
||||||
|
this.getOrbisAuthStatus();
|
||||||
|
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
||||||
|
<h2>{translate('upload to orbis')}</h2>
|
||||||
|
<br/>
|
||||||
|
<label>Orbis auth status: </label>
|
||||||
|
<input value={this.state.authenticatedStatus} readOnly size={25} onFocus={ (e) => e.target.select() }/>
|
||||||
|
<br/><br/>
|
||||||
|
<a className='button' href="https://orbis.zone/api/auth">Log in / signup to Orbis</a>
|
||||||
|
<br/><br/>
|
||||||
|
<h3>Category</h3>
|
||||||
|
<select onChange={this.orbisCategory}>
|
||||||
|
<option value="">No Category</option>
|
||||||
|
<option>Combat</option>
|
||||||
|
<option>Mining</option>
|
||||||
|
<option>Trading</option>
|
||||||
|
<option>Exploration</option>
|
||||||
|
<option>Passenger Liner</option>
|
||||||
|
<option>PvP</option>
|
||||||
|
</select>
|
||||||
|
<br/><br/>
|
||||||
|
<h3 >{translate('Orbis link')}</h3>
|
||||||
|
<input value={this.state.orbisUrl} readOnly size={25} onFocus={ (e) => e.target.select() }/>
|
||||||
|
<br/><br/>
|
||||||
|
<p>Orbis.zone is currently in a trial period, and may be wiped at any time as development progresses. Some elements are also still placeholders.</p>
|
||||||
|
<button className={'l cb dismiss cap'} disabled={!!this.state.failed} onClick={this.sendToOrbis}>{translate('PHASE_UPLOAD_ORBIS')}</button>
|
||||||
|
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import ShortenUrl from '../utils/ShortenUrl';
|
|||||||
* Permalink modal
|
* Permalink modal
|
||||||
*/
|
*/
|
||||||
export default class ModalPermalink extends TranslatedComponent {
|
export default class ModalPermalink extends TranslatedComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
url: PropTypes.string.isRequired
|
url: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
262
src/app/components/ModalShoppingList.jsx
Normal file
262
src/app/components/ModalShoppingList.jsx
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
|
import request from 'superagent';
|
||||||
|
import Persist from '../stores/Persist';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permalink modal
|
||||||
|
*/
|
||||||
|
export default class ModalShoppingList extends TranslatedComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
ship: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param {Object} props React Component properties
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
matsList: '',
|
||||||
|
mats: {},
|
||||||
|
failed: false,
|
||||||
|
cmdrName: Persist.getCmdr().selected,
|
||||||
|
cmdrs: Persist.getCmdr().cmdrs,
|
||||||
|
matsPerGrade: Persist.getRolls(),
|
||||||
|
blueprints: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React component did mount
|
||||||
|
*/
|
||||||
|
componentDidMount() {
|
||||||
|
this.renderMats();
|
||||||
|
if (this.checkBrowserIsCompatible()) {
|
||||||
|
this.getCommanders();
|
||||||
|
this.registerBPs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all blueprints needed to make a build.
|
||||||
|
*/
|
||||||
|
registerBPs() {
|
||||||
|
const ship = this.props.ship;
|
||||||
|
let blueprints = [];
|
||||||
|
for (const module of ship.costList) {
|
||||||
|
if (module.type === 'SHIP') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (module.m && module.m.blueprint) {
|
||||||
|
if (!module.m.blueprint.grade || !module.m.blueprint.grades) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (module.m.blueprint.special) {
|
||||||
|
console.log(module.m.blueprint.special);
|
||||||
|
blueprints.push({ uuid: module.m.blueprint.special.uuid, number: 1 });
|
||||||
|
}
|
||||||
|
for (const g in module.m.blueprint.grades) {
|
||||||
|
if (!module.m.blueprint.grades.hasOwnProperty(g)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (g > module.m.blueprint.grade) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
blueprints.push({ uuid: module.m.blueprint.grades[g].uuid, number: this.state.matsPerGrade[g] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({ blueprints });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check browser isn't firefox.
|
||||||
|
* @return {boolean} true if compatible, false if not.
|
||||||
|
*/
|
||||||
|
checkBrowserIsCompatible() {
|
||||||
|
// Firefox 1.0+
|
||||||
|
return typeof InstallTrigger === 'undefined';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of commanders from EDEngineer.
|
||||||
|
*/
|
||||||
|
getCommanders() {
|
||||||
|
request
|
||||||
|
.get('http://localhost:44405/commanders')
|
||||||
|
.end((err, res) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(err);
|
||||||
|
return this.setState({ failed: true });
|
||||||
|
}
|
||||||
|
const cmdrs = JSON.parse(res.text);
|
||||||
|
if (!this.state.cmdrName) {
|
||||||
|
this.setState({ cmdrName: cmdrs[0] });
|
||||||
|
}
|
||||||
|
this.setState({ cmdrs }, () => {
|
||||||
|
Persist.setCmdr({ selected: this.state.cmdrName, cmdrs });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send all blueprints to ED Engineer
|
||||||
|
* @param {Event} event React event
|
||||||
|
*/
|
||||||
|
sendToEDEng(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const target = event.target;
|
||||||
|
target.disabled = this.state.blueprints.length > 0;
|
||||||
|
if (this.state.blueprints.length === 0) {
|
||||||
|
target.innerText = 'No modded components.';
|
||||||
|
target.disabled = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
target.innerText = 'Send to EDEngineer';
|
||||||
|
target.disabled = false;
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
target.innerText = 'Sending...';
|
||||||
|
}
|
||||||
|
let countSent = 0;
|
||||||
|
let countTotal = this.state.blueprints.length;
|
||||||
|
|
||||||
|
for (const i of this.state.blueprints) {
|
||||||
|
request
|
||||||
|
.patch(`http://localhost:44405/${this.state.cmdrName}/shopping-list`)
|
||||||
|
.field('uuid', i.uuid)
|
||||||
|
.field('size', i.number)
|
||||||
|
.end(err => {
|
||||||
|
if (err) {
|
||||||
|
console.log(err);
|
||||||
|
if (err.message !== 'Bad Request') {
|
||||||
|
this.setState({ failed: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
countSent++;
|
||||||
|
if (countSent === countTotal) {
|
||||||
|
target.disabled = false;
|
||||||
|
target.innerText = 'Send to EDEngineer';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert mats object to string
|
||||||
|
*/
|
||||||
|
renderMats() {
|
||||||
|
const ship = this.props.ship;
|
||||||
|
let mats = {};
|
||||||
|
for (const module of ship.costList) {
|
||||||
|
if (module.type === 'SHIP') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (module.m && module.m.blueprint) {
|
||||||
|
if (!module.m.blueprint.grade || !module.m.blueprint.grades) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const g in module.m.blueprint.grades) {
|
||||||
|
if (!module.m.blueprint.grades.hasOwnProperty(g)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (g > module.m.blueprint.grade) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const i in module.m.blueprint.grades[g].components) {
|
||||||
|
if (!module.m.blueprint.grades[g].components.hasOwnProperty(i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (mats[i]) {
|
||||||
|
mats[i] += module.m.blueprint.grades[g].components[i] * this.state.matsPerGrade[g];
|
||||||
|
} else {
|
||||||
|
mats[i] = module.m.blueprint.grades[g].components[i] * this.state.matsPerGrade[g];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let matsString = '';
|
||||||
|
for (const i in mats) {
|
||||||
|
if (!mats.hasOwnProperty(i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (mats[i] === 0) {
|
||||||
|
delete mats[i];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
matsString += `${i}: ${mats[i]}\n`;
|
||||||
|
}
|
||||||
|
this.setState({ matsList: matsString, mats });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for changing roll amounts
|
||||||
|
* @param {SyntheticEvent} e React Event
|
||||||
|
*/
|
||||||
|
changeHandler(e) {
|
||||||
|
let grade = e.target.id;
|
||||||
|
let newState = this.state.matsPerGrade;
|
||||||
|
newState[grade] = parseInt(e.target.value);
|
||||||
|
this.setState({ matsPerGrade: newState });
|
||||||
|
Persist.setRolls(newState);
|
||||||
|
this.renderMats();
|
||||||
|
this.registerBPs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for changing cmdr name
|
||||||
|
* @param {SyntheticEvent} e React Event
|
||||||
|
*/
|
||||||
|
cmdrChangeHandler(e) {
|
||||||
|
let cmdrName = e.target.value;
|
||||||
|
this.setState({ cmdrName }, () => {
|
||||||
|
Persist.setCmdr({ selected: this.state.cmdrName, cmdrs: this.state.cmdrs });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the modal
|
||||||
|
* @return {React.Component} Modal Content
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
let translate = this.context.language.translate;
|
||||||
|
this.changeHandler = this.changeHandler.bind(this);
|
||||||
|
const compatible = this.checkBrowserIsCompatible();
|
||||||
|
this.cmdrChangeHandler = this.cmdrChangeHandler.bind(this);
|
||||||
|
this.sendToEDEng = this.sendToEDEng.bind(this);
|
||||||
|
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
||||||
|
<h2>{translate('PHRASE_SHOPPING_MATS')}</h2>
|
||||||
|
<label>Grade 1 rolls </label>
|
||||||
|
<input id={1} type={'number'} min={0} defaultValue={this.state.matsPerGrade[1]} onChange={this.changeHandler} />
|
||||||
|
<br/>
|
||||||
|
<label>Grade 2 rolls </label>
|
||||||
|
<input id={2} type={'number'} min={0} defaultValue={this.state.matsPerGrade[2]} onChange={this.changeHandler} />
|
||||||
|
<br/>
|
||||||
|
<label>Grade 3 rolls </label>
|
||||||
|
<input id={3} type={'number'} min={0} value={this.state.matsPerGrade[3]} onChange={this.changeHandler} />
|
||||||
|
<br/>
|
||||||
|
<label>Grade 4 rolls </label>
|
||||||
|
<input id={4} type={'number'} min={0} value={this.state.matsPerGrade[4]} onChange={this.changeHandler} />
|
||||||
|
<br/>
|
||||||
|
<label>Grade 5 rolls </label>
|
||||||
|
<input id={5} type={'number'} min={0} value={this.state.matsPerGrade[5]} onChange={this.changeHandler} />
|
||||||
|
<div>
|
||||||
|
<textarea className='cb json' readOnly value={this.state.matsList} />
|
||||||
|
</div>
|
||||||
|
<label hidden={!compatible} className={'l cap'}>CMDR Name </label>
|
||||||
|
<br/>
|
||||||
|
<select hidden={!compatible} className={'cmdr-select l cap'} onChange={this.cmdrChangeHandler} defaultValue={this.state.cmdrName}>
|
||||||
|
{this.state.cmdrs.map(e => <option key={e}>{e}</option>)}
|
||||||
|
</select>
|
||||||
|
<br/>
|
||||||
|
<p hidden={!this.state.failed} id={'failed'} className={'l'}>Failed to send to EDEngineer (Launch EDEngineer and make sure the API is started then refresh the page.)</p>
|
||||||
|
<p hidden={compatible} id={'browserbad'} className={'l'}>Sending to EDEngineer is not compatible with Firefox's security settings. Please try again with Chrome.</p>
|
||||||
|
<button className={'l cb dismiss cap'} disabled={!!this.state.failed || !compatible} onClick={this.sendToEDEng}>{translate('Send To EDEngineer')}</button>
|
||||||
|
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,52 +3,72 @@ import PropTypes from 'prop-types';
|
|||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import NumberEditor from 'react-number-editor';
|
import NumberEditor from 'react-number-editor';
|
||||||
import { Module } from 'ed-forge';
|
import { isValueBeneficial } from '../utils/BlueprintFunctions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modification
|
* Modification
|
||||||
*/
|
*/
|
||||||
export default class Modification extends TranslatedComponent {
|
export default class Modification extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
highlight: PropTypes.bool,
|
ship: PropTypes.object.isRequired,
|
||||||
m: PropTypes.instanceOf(Module).isRequired,
|
m: PropTypes.object.isRequired,
|
||||||
property: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
onSet: PropTypes.func.isRequired,
|
value: PropTypes.number.isRequired,
|
||||||
showProp: PropTypes.object,
|
onChange: PropTypes.func.isRequired,
|
||||||
onPropToggle: PropTypes.func.isRequired,
|
onKeyDown: PropTypes.func.isRequired,
|
||||||
|
modItems: PropTypes.array.isRequired,
|
||||||
|
handleModChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
* @param {Object} props React Component properties
|
* @param {Object} props React Component properties
|
||||||
|
* @param {Object} context React Component context
|
||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props);
|
||||||
const { m, property, showProp } = props;
|
this.state = {};
|
||||||
const { beneficial, unit, value } = m.getFormatted(property, true);
|
this.state.value = props.value;
|
||||||
this.state = { beneficial, unit, value, showProp };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify listeners that a new value has been entered and commited.
|
* Update modification given a value.
|
||||||
|
* @param {Number} value The value to set. This comes in as a string and must be stored in state as a string,
|
||||||
|
* because it needs to allow illegal 'numbers' ('-', '1.', etc) when the user is typing
|
||||||
|
* in a value by hand
|
||||||
*/
|
*/
|
||||||
_updateFinished() {
|
_updateValue(value) {
|
||||||
const { onSet, m, property } = this.props;
|
this.setState({ value });
|
||||||
const { inputValue } = this.state;
|
let reCast = String(Number(value));
|
||||||
const numValue = Number(inputValue);
|
if (reCast.endsWith(value) || reCast.startsWith(value)) {
|
||||||
if (!isNaN(numValue) && this.state.value !== numValue) {
|
let { m, name, ship } = this.props;
|
||||||
onSet(property, numValue);
|
value = Math.max(Math.min(value, 50000), -50000);
|
||||||
const { beneficial, unit, value } = m.getFormatted(property, true);
|
ship.setModification(m, name, value, true, true);
|
||||||
this.setState({ beneficial, unit, value });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_toggleProperty() {
|
/**
|
||||||
const { onPropToggle, property } = this.props;
|
* Triggered when a key is pressed down with focus on the number editor.
|
||||||
const showProp = !this.state.showProp;
|
* @param {SyntheticEvent} event Key down event
|
||||||
// TODO: defer until menu closed
|
*/
|
||||||
onPropToggle(property, showProp);
|
_keyDown(event) {
|
||||||
this.setState({ showProp });
|
if (event.key == 'Enter') {
|
||||||
|
this._updateFinished();
|
||||||
|
}
|
||||||
|
this.props.onKeyDown(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered when an update to slider value is finished i.e. when losing focus
|
||||||
|
*
|
||||||
|
* pnellesen (24/05/2018): added value check below - this should prevent experimental effects from being recalculated
|
||||||
|
* with each onBlur event, even when no change has actually been made to the field.
|
||||||
|
*/
|
||||||
|
_updateFinished() {
|
||||||
|
if (this.props.value != this.state.value) {
|
||||||
|
this.props.handleModChange(true);
|
||||||
|
this.props.onChange();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,53 +76,56 @@ export default class Modification extends TranslatedComponent {
|
|||||||
* @return {React.Component} modification
|
* @return {React.Component} modification
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { formats } = this.context.language;
|
let { translate, formats, units } = this.context.language;
|
||||||
const { highlight, m, property } = this.props;
|
let { m, name } = this.props;
|
||||||
const { beneficial, unit, value, inputValue, showProp } = this.state;
|
let modValue = m.getChange(name);
|
||||||
|
|
||||||
// Some features only apply to specific modules; these features will be
|
if (name === 'damagedist') {
|
||||||
// undefined on items that do not belong to the same class. Filter these
|
// We don't show damage distribution
|
||||||
// features here
|
|
||||||
if (value === undefined) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { value: modifierValue, unit: modifierUnit } = m.getModifierFormatted(property);
|
let inputClassNames = {
|
||||||
|
'cb': true,
|
||||||
|
'greyed-out': !this.props.highlight
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div onBlur={this._updateFinished.bind(this)} key={name}
|
||||||
|
className={cn('cb', 'modification-container')}
|
||||||
|
ref={ modItem => this.props.modItems[name] = modItem }>
|
||||||
|
<span className={'cb'}>{translate(name, m.grp)}</span>
|
||||||
|
<span className={'header-adjuster'}></span>
|
||||||
|
<table style={{ width: '100%' }}>
|
||||||
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td className={'input-container'}>
|
||||||
<span>
|
<span>
|
||||||
<input type="checkbox" checked={showProp} onClick={() => this._toggleProperty()}/>
|
{this.props.editable ?
|
||||||
|
<NumberEditor className={cn(inputClassNames)} value={this.state.value}
|
||||||
|
decimals={2} style={{ textAlign: 'right' }} step={0.01}
|
||||||
|
stepModifier={1} onKeyDown={this._keyDown.bind(this)}
|
||||||
|
onValueChange={this._updateValue.bind(this)} /> :
|
||||||
|
<input type="text" value={formats.f2(this.state.value)}
|
||||||
|
disabled className={cn('number-editor', 'greyed-out')}
|
||||||
|
style={{ textAlign: 'right', cursor: 'inherit' }}/>
|
||||||
|
}
|
||||||
|
<span className={'unit-container'}>
|
||||||
|
{units[m.getStoredUnitFor(name)]}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="input-container">
|
<td style={{ textAlign: 'center' }} className={
|
||||||
<span>
|
modValue ?
|
||||||
<NumberEditor value={inputValue || value} stepModifier={1}
|
isValueBeneficial(name, modValue) ? 'secondary' : 'warning' :
|
||||||
decimals={2} step={0.01} style={{ textAlign: 'right', width: '100%' }}
|
''
|
||||||
className={cn('cb', { 'greyed-out': !highlight })}
|
}>
|
||||||
onKeyDown={(event) => {
|
{formats.f2(modValue / 100) || 0}%
|
||||||
if (event.key == 'Enter') {
|
|
||||||
this._updateFinished();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onValueChange={(inputValue) => {
|
|
||||||
if (inputValue.length <= 15) {
|
|
||||||
this.setState({ inputValue });
|
|
||||||
}
|
|
||||||
}} />
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td style={{ textAlign: 'left' }}>
|
|
||||||
<span className="unit-container">{unit}</span>
|
|
||||||
</td>
|
|
||||||
<td style={{ textAlign: 'center' }}
|
|
||||||
className={cn({
|
|
||||||
secondary: beneficial,
|
|
||||||
warning: beneficial === false,
|
|
||||||
})}
|
|
||||||
>{formats.f2(modifierValue)}{modifierUnit || ''}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { chain, flatMap, keys } from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
|
import { Modifications } from 'coriolis-data/dist';
|
||||||
import Modification from './Modification';
|
import Modification from './Modification';
|
||||||
import {
|
import {
|
||||||
|
getBlueprint,
|
||||||
blueprintTooltip,
|
blueprintTooltip,
|
||||||
|
setPercent,
|
||||||
|
getPercent,
|
||||||
|
setRandom,
|
||||||
specialToolTip
|
specialToolTip
|
||||||
} from '../utils/BlueprintFunctions';
|
} from '../utils/BlueprintFunctions';
|
||||||
import { getBlueprintInfo, getExperimentalInfo } from 'ed-forge/lib/src/data/blueprints';
|
|
||||||
import { getModuleInfo } from 'ed-forge/lib/src/data/items';
|
const MODIFICATIONS_COMPARATOR = (mod1, mod2) => {
|
||||||
import { SHOW } from '../shipyard/StatsMapping';
|
return mod1.props.name.localeCompare(mod2.props.name);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modifications menu
|
* Modifications menu
|
||||||
*/
|
*/
|
||||||
export default class ModificationsMenu extends TranslatedComponent {
|
export default class ModificationsMenu extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
className: PropTypes.string,
|
ship: PropTypes.object.isRequired,
|
||||||
m: PropTypes.object.isRequired,
|
m: PropTypes.object.isRequired,
|
||||||
propsToShow: PropTypes.object.isRequired,
|
marker: PropTypes.string.isRequired,
|
||||||
onPropToggle: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
modButton:PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,58 +41,122 @@ export default class ModificationsMenu extends TranslatedComponent {
|
|||||||
|
|
||||||
this._toggleBlueprintsMenu = this._toggleBlueprintsMenu.bind(this);
|
this._toggleBlueprintsMenu = this._toggleBlueprintsMenu.bind(this);
|
||||||
this._toggleSpecialsMenu = this._toggleSpecialsMenu.bind(this);
|
this._toggleSpecialsMenu = this._toggleSpecialsMenu.bind(this);
|
||||||
this.selectedModRef = null;
|
this._rollFifty = this._rollFifty.bind(this);
|
||||||
this.selectedSpecialRef = null;
|
this._rollRandom = this._rollRandom.bind(this);
|
||||||
|
this._rollBest = this._rollBest.bind(this);
|
||||||
|
this._rollWorst = this._rollWorst.bind(this);
|
||||||
|
this._reset = this._reset.bind(this);
|
||||||
|
this._keyDown = this._keyDown.bind(this);
|
||||||
|
this.modItems = [];// Array to hold various element refs (<li>, <div>, <ul>, etc.)
|
||||||
|
this.firstModId = null;
|
||||||
|
this.firstBPLabel = null;// First item in mod menu
|
||||||
|
this.lastModId = null;
|
||||||
|
this.selectedModId = null;
|
||||||
|
this.selectedSpecialId = null;
|
||||||
|
this.lastNeId = null;// Last number editor id. Used to set focus to last number editor when shift-tab pressed on first element in mod menu.
|
||||||
|
this.modValDidChange = false; // used to determine if component update was caused by change in modification value.
|
||||||
|
this._handleModChange = this._handleModChange.bind(this);
|
||||||
|
|
||||||
const { m } = props;
|
|
||||||
this.state = {
|
this.state = {
|
||||||
blueprintProgress: m.getBlueprintProgress(),
|
blueprintMenuOpened: !(props.m.blueprint && props.m.blueprint.name),
|
||||||
blueprintMenuOpened: !m.getBlueprint(),
|
|
||||||
specialMenuOpened: false
|
specialMenuOpened: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the blueprints
|
* Render the blueprints
|
||||||
|
* @param {Object} props React component properties
|
||||||
|
* @param {Object} context React component context
|
||||||
* @return {Object} list: Array of React Components
|
* @return {Object} list: Array of React Components
|
||||||
*/
|
*/
|
||||||
_renderBlueprints() {
|
_renderBlueprints(props, context) {
|
||||||
const { m } = this.props;
|
const { m } = props;
|
||||||
const { language, tooltip, termtip } = this.context;
|
const { language, tooltip, termtip } = context;
|
||||||
const { translate } = language;
|
const translate = language.translate;
|
||||||
|
const blueprints = [];
|
||||||
const blueprints = m.getApplicableBlueprints().map(blueprint => {
|
for (const blueprintName in Modifications.modules[m.grp].blueprints) {
|
||||||
const info = getBlueprintInfo(blueprint);
|
const blueprint = getBlueprint(blueprintName, m);
|
||||||
let blueprintGrades = keys(info.features).map(grade => {
|
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 is a string in the JSON so make it a number
|
||||||
grade = Number(grade);
|
grade = Number(grade);
|
||||||
const active = m.getBlueprint() === blueprint && m.getBlueprintGrade() === grade;
|
const classes = cn('c', {
|
||||||
const key = blueprint + ':' + grade;
|
active: m.blueprint && blueprint.id === m.blueprint.id && grade === m.blueprint.grade
|
||||||
return <li key={key} data-id={key} className={cn('c', { active })}
|
|
||||||
style={{ width: '2em' }}
|
|
||||||
onMouseOver={termtip.bind(null, blueprintTooltip(language, m, blueprint, grade))}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
onClick={() => {
|
|
||||||
m.setBlueprint(blueprint, grade, 1);
|
|
||||||
this.setState({
|
|
||||||
blueprintMenuOpened: false,
|
|
||||||
specialMenuOpened: true,
|
|
||||||
});
|
});
|
||||||
}}
|
const close = this._blueprintSelected.bind(this, blueprintName, grade);
|
||||||
ref={active ? (ref) => { this.selectedModRef = ref; } : undefined}
|
const key = blueprintName + ':' + grade;
|
||||||
>{grade}</li>;
|
const tooltipContent = blueprintTooltip(translate, blueprint.grades[grade], Modifications.modules[m.grp].blueprints[blueprintName].grades[grade].engineers, m.grp);
|
||||||
});
|
if (classes.indexOf('active') >= 0) this.selectedModId = key;
|
||||||
|
blueprintGrades.unshift(<li key={key} tabIndex="0" data-id={key} className={classes} style={{ width: '2em' }} onMouseOver={termtip.bind(null, tooltipContent)} onMouseOut={tooltip.bind(null, null)} onClick={close} onKeyDown={this._keyDown} ref={modItem => this.modItems[key] = modItem}>{grade}</li>);
|
||||||
return [
|
|
||||||
<div key={'div' + blueprint} className={'select-group cap'}>
|
|
||||||
{translate(blueprint)}
|
|
||||||
</div>,
|
|
||||||
<ul key={'ul' + blueprint}>{blueprintGrades}</ul>
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
return flatMap(blueprints);
|
|
||||||
}
|
}
|
||||||
|
if (blueprintGrades) {
|
||||||
|
const thisLen = blueprintGrades.length;
|
||||||
|
if (this.firstModId == null) this.firstModId = blueprintGrades[0].key;
|
||||||
|
this.lastModId = blueprintGrades[thisLen - 1].key;
|
||||||
|
blueprints.push(<div key={blueprint.name} className={'select-group cap'}>{translate(blueprint.name)}</div>);
|
||||||
|
blueprints.push(<ul key={blueprintName}>{blueprintGrades}</ul>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return blueprints;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key down - select module on Enter key, move to next/previous module on Tab/Shift-Tab, close on Esc
|
||||||
|
* @param {SyntheticEvent} event Event
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
_keyDown(event) {
|
||||||
|
let className = null;
|
||||||
|
let elemId = null;
|
||||||
|
if (event.currentTarget.attributes['class']) className = event.currentTarget.attributes['class'].value;
|
||||||
|
if (event.currentTarget.attributes['data-id']) elemId = event.currentTarget.attributes['data-id'].value;
|
||||||
|
|
||||||
|
if (event.key == 'Enter' && className.indexOf('disabled') < 0 && className.indexOf('active') < 0) {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (elemId != null) {
|
||||||
|
this.modItems[elemId].click();
|
||||||
|
} else {
|
||||||
|
event.currentTarget.click();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key == 'Tab') {
|
||||||
|
// Shift-Tab
|
||||||
|
if(event.shiftKey) {
|
||||||
|
if (elemId == this.firstModId && elemId != null) {
|
||||||
|
// Initial modification menu
|
||||||
|
event.preventDefault();
|
||||||
|
this.modItems[this.lastModId].focus();
|
||||||
|
return;
|
||||||
|
} else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.previousElementSibling == null && this.lastNeId != null && this.modItems[this.lastNeId] != null) {
|
||||||
|
// shift-tab on first element in modifications menu. set focus to last number editor field if open
|
||||||
|
event.preventDefault();
|
||||||
|
this.modItems[this.lastNeId].lastChild.focus();
|
||||||
|
return;
|
||||||
|
} else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.previousElementSibling == null) {
|
||||||
|
// shift-tab on button-inline-menu with no number editor
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.parentElement.lastElementChild.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (elemId == this.lastModId && elemId != null) {
|
||||||
|
// Initial modification menu
|
||||||
|
event.preventDefault();
|
||||||
|
this.modItems[this.firstModId].focus();
|
||||||
|
return;
|
||||||
|
} else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.nextSibling == null && event.currentTarget.nodeName != 'TD') {
|
||||||
|
// Experimental menu
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.parentElement.firstElementChild.focus();
|
||||||
|
return;
|
||||||
|
} else if (event.currentTarget.className == 'cb' && event.currentTarget.parentElement.nextSibling == null) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.modItems[this.firstBPLabel].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the specials
|
* Render the specials
|
||||||
@@ -93,118 +164,204 @@ export default class ModificationsMenu extends TranslatedComponent {
|
|||||||
* @param {Object} context React component context
|
* @param {Object} context React component context
|
||||||
* @return {Object} list: Array of React Components
|
* @return {Object} list: Array of React Components
|
||||||
*/
|
*/
|
||||||
_renderSpecials() {
|
_renderSpecials(props, context) {
|
||||||
const { m } = this.props;
|
const { m } = props;
|
||||||
const { language, tooltip, termtip } = this.context;
|
const { language, tooltip, termtip } = context;
|
||||||
const translate = language.translate;
|
const translate = language.translate;
|
||||||
|
const specials = [];
|
||||||
const applied = m.getExperimental();
|
const specialsId = m.missile && Modifications.modules[m.grp]['specials_' + m.missile] ? 'specials_' + m.missile : 'specials';
|
||||||
const experimentals = [];
|
if (Modifications.modules[m.grp][specialsId] && Modifications.modules[m.grp][specialsId].length > 0) {
|
||||||
for (const experimental of m.getApplicableExperimentals()) {
|
const close = this._specialSelected.bind(this, null);
|
||||||
const active = experimental === applied;
|
specials.push(<div tabIndex="0" style={{ cursor: 'pointer', fontWeight: 'bold' }} className={ 'button-inline-menu warning' } key={ 'none' } data-id={ 'none' } onClick={ close } onKeyDown={this._keyDown} ref={modItem => this.modItems['none'] = modItem}>{translate('PHRASE_NO_SPECIAL')}</div>);
|
||||||
let specialTt = specialToolTip(language, m, experimental);
|
for (const specialName of Modifications.modules[m.grp][specialsId]) {
|
||||||
experimentals.push(
|
if (Modifications.specials[specialName].name.search('Legacy') >= 0) {
|
||||||
<div key={experimental} data-id={experimental}
|
continue;
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
className={cn('button-inline-menu', { active })}
|
|
||||||
onClick={this._specialSelected(experimental)}
|
|
||||||
ref={active ? (ref) => { this.selectedSpecialRef = ref; } : undefined}
|
|
||||||
onMouseOver={termtip.bind(null, specialTt)}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>{translate(experimental)}</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
const classes = cn('button-inline-menu', {
|
||||||
if (experimentals.length) {
|
active: m.blueprint && m.blueprint.special && m.blueprint.special.edname == specialName
|
||||||
experimentals.unshift(
|
});
|
||||||
<div style={{ cursor: 'pointer', fontWeight: 'bold' }}
|
if (classes.indexOf('active') >= 0) this.selectedSpecialId = specialName;
|
||||||
className="button-inline-menu warning" key="none" data-id="none"
|
const close = this._specialSelected.bind(this, specialName);
|
||||||
// Setting the special effect to undefined clears it
|
if (m.blueprint && m.blueprint.name) {
|
||||||
onClick={this._specialSelected(undefined)}
|
let tmp = {};
|
||||||
ref={!applied ? (ref) => { this.selectedSpecialRef = ref; } : undefined}
|
if (m.blueprint.special) {
|
||||||
>{translate('PHRASE_NO_SPECIAL')}</div>
|
tmp = m.blueprint.special;
|
||||||
);
|
} else {
|
||||||
|
tmp = undefined;
|
||||||
}
|
}
|
||||||
|
m.blueprint.special = Modifications.specials[specialName];
|
||||||
return experimentals;
|
let specialTt = specialToolTip(translate, m.blueprint.grades[m.blueprint.grade], m.grp, m, specialName);
|
||||||
|
m.blueprint.special = tmp;
|
||||||
|
specials.push(<div tabIndex="0" style={{ cursor: 'pointer' }} className={classes} key={ specialName } data-id={ specialName } onMouseOver={termtip.bind(null, specialTt)} onMouseOut={tooltip.bind(null, null)} onClick={ close } onKeyDown={this._keyDown} ref={modItem => this.modItems[specialName] = modItem}>{translate(Modifications.specials[specialName].name)}</div>);
|
||||||
|
} else {
|
||||||
|
specials.push(<div tabIndex="0" style={{ cursor: 'pointer' }} className={classes} key={ specialName } data-id={ specialName }onClick={ close } onKeyDown={this._keyDown} ref={modItem => this.modItems[specialName] = modItem}>{translate(Modifications.specials[specialName].name)}</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a modification component
|
|
||||||
*/
|
|
||||||
_mkModification(property, highlight) {
|
|
||||||
const { translate } = this.context.language;
|
|
||||||
const { m, propsToShow, onPropToggle } = this.props;
|
|
||||||
|
|
||||||
let onSet = m.set.bind(m);
|
|
||||||
// Show resistance instead of effectiveness
|
|
||||||
const mapped = SHOW[property];
|
|
||||||
if (mapped) {
|
|
||||||
property = mapped.as;
|
|
||||||
onSet = mapped.setter.bind(undefined, m);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return [
|
return specials;
|
||||||
<tr key={`th-${property}`}>
|
|
||||||
<th colSpan="4">
|
|
||||||
<span className="cb">{translate(property)}</span>
|
|
||||||
</th>
|
|
||||||
</tr>,
|
|
||||||
<Modification key={property} m={m} property={property}
|
|
||||||
onSet={onSet} highlight={highlight} showProp={propsToShow[property]}
|
|
||||||
onPropToggle={onPropToggle} />
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the modifications
|
* Render the modifications
|
||||||
* @return {Array} Array of React Components
|
* @param {Object} props React Component properties
|
||||||
|
* @return {Object} list: Array of React Components
|
||||||
*/
|
*/
|
||||||
_renderModifications() {
|
_renderModifications(props) {
|
||||||
const { m } = this.props;
|
const { m, onChange, ship } = props;
|
||||||
|
const modifiableModifications = [];
|
||||||
|
const modifications = [];
|
||||||
|
for (const modName of Modifications.modules[m.grp].modifications) {
|
||||||
|
if (!Modifications.modifications[modName].hidden) {
|
||||||
|
const key = modName + (m.getModValue(modName) / 100 || 0);
|
||||||
|
const editable = modName !== 'fallofffromrange';
|
||||||
|
const highlight = m.blueprint.grades[m.blueprint.grade].features[modName];
|
||||||
|
this.lastNeId = modName;
|
||||||
|
(editable && highlight ? modifiableModifications : modifications).push(
|
||||||
|
<Modification key={ key } ship={ ship } m={ m } highlight={highlight}
|
||||||
|
value={m.getPretty(modName) || 0} modItems={this.modItems}
|
||||||
|
onChange={onChange} onKeyDown={this._keyDown} name={modName}
|
||||||
|
editable={editable} handleModChange = {this._handleModChange} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const blueprintFeatures = getBlueprintInfo(m.getBlueprint()).features[
|
modifiableModifications.sort(MODIFICATIONS_COMPARATOR);
|
||||||
m.getBlueprintGrade()
|
modifications.sort(MODIFICATIONS_COMPARATOR);
|
||||||
];
|
return modifiableModifications.concat(modifications);
|
||||||
const blueprintModifications = chain(keys(blueprintFeatures))
|
|
||||||
.map((feature) => this._mkModification(feature, true))
|
|
||||||
.filter(([_, mod]) => Boolean(mod))
|
|
||||||
.flatMap()
|
|
||||||
.value();
|
|
||||||
const moduleModifications = chain(keys(getModuleInfo(m.getItem()).props))
|
|
||||||
.filter((prop) => !blueprintFeatures[prop])
|
|
||||||
.map((prop) => this._mkModification(prop, false))
|
|
||||||
.flatMap()
|
|
||||||
.value();
|
|
||||||
|
|
||||||
return blueprintModifications.concat(moduleModifications);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle the blueprints menu
|
* Toggle the blueprints menu
|
||||||
*/
|
*/
|
||||||
_toggleBlueprintsMenu() {
|
_toggleBlueprintsMenu() {
|
||||||
this.setState({ blueprintMenuOpened: !this.state.blueprintMenuOpened });
|
const blueprintMenuOpened = !this.state.blueprintMenuOpened;
|
||||||
|
this.setState({ blueprintMenuOpened });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activated when a blueprint is selected
|
||||||
|
* @param {int} fdname The Frontier name of the blueprint
|
||||||
|
* @param {int} grade The grade of the selected blueprint
|
||||||
|
*/
|
||||||
|
_blueprintSelected(fdname, grade) {
|
||||||
|
this.context.tooltip(null);
|
||||||
|
const { m, ship } = this.props;
|
||||||
|
const blueprint = getBlueprint(fdname, m);
|
||||||
|
blueprint.grade = grade;
|
||||||
|
ship.setModuleBlueprint(m, blueprint);
|
||||||
|
setPercent(ship, m, 100);
|
||||||
|
|
||||||
|
this.setState({ blueprintMenuOpened: false, specialMenuOpened: true });
|
||||||
|
this.props.onChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle the specials menu
|
* Toggle the specials menu
|
||||||
*/
|
*/
|
||||||
_toggleSpecialsMenu() {
|
_toggleSpecialsMenu() {
|
||||||
this.setState({ specialMenuOpened: !this.state.specialMenuOpened });
|
const specialMenuOpened = !this.state.specialMenuOpened;
|
||||||
|
this.setState({ specialMenuOpened });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a callback for when a special effect is being selected
|
* Activated when a special is selected
|
||||||
* @param {string} special The name of the selected special
|
* @param {int} special The name of the selected special
|
||||||
* @returns {function} Callback
|
|
||||||
*/
|
*/
|
||||||
_specialSelected(special) {
|
_specialSelected(special) {
|
||||||
return () => {
|
this.context.tooltip(null);
|
||||||
const { m } = this.props;
|
const { m, ship } = this.props;
|
||||||
m.setExperimental(special);
|
|
||||||
|
if (special === null) {
|
||||||
|
ship.clearModuleSpecial(m);
|
||||||
|
} else {
|
||||||
|
ship.setModuleSpecial(m, Modifications.specials[special]);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ specialMenuOpened: false });
|
this.setState({ specialMenuOpened: false });
|
||||||
};
|
this.props.onChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide a '50%' roll within the information we have
|
||||||
|
*/
|
||||||
|
_rollFifty() {
|
||||||
|
const { m, ship } = this.props;
|
||||||
|
setPercent(ship, m, 50);
|
||||||
|
|
||||||
|
// this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
|
||||||
|
this._handleModChange(true);
|
||||||
|
|
||||||
|
this.props.onChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide a random roll within the information we have
|
||||||
|
*/
|
||||||
|
_rollRandom() {
|
||||||
|
const { m, ship } = this.props;
|
||||||
|
setRandom(ship, m);
|
||||||
|
|
||||||
|
// this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
|
||||||
|
this._handleModChange(true);
|
||||||
|
|
||||||
|
this.props.onChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide a 'best' roll within the information we have
|
||||||
|
*/
|
||||||
|
_rollBest() {
|
||||||
|
const { m, ship } = this.props;
|
||||||
|
setPercent(ship, m, 100);
|
||||||
|
|
||||||
|
// this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
|
||||||
|
this._handleModChange(true);
|
||||||
|
|
||||||
|
this.props.onChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide a 'worst' roll within the information we have
|
||||||
|
*/
|
||||||
|
_rollWorst() {
|
||||||
|
const { m, ship } = this.props;
|
||||||
|
setPercent(ship, m, 0);
|
||||||
|
// this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
|
||||||
|
this._handleModChange(true);
|
||||||
|
this.props.onChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset modification information
|
||||||
|
*/
|
||||||
|
_reset() {
|
||||||
|
const { m, ship } = this.props;
|
||||||
|
ship.clearModifications(m);
|
||||||
|
ship.clearModuleBlueprint(m);
|
||||||
|
this.selectedModId = null;
|
||||||
|
this.selectedSpecialId = null;
|
||||||
|
this.props.onChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set mod did change boolean
|
||||||
|
* @param {boolean} b Boolean to determine if a change has been made to a module
|
||||||
|
*/
|
||||||
|
_handleModChange(b) {
|
||||||
|
this.modValDidChange = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set focus on first element in modifications menu
|
||||||
|
* after it first mounts
|
||||||
|
*/
|
||||||
|
componentDidMount() {
|
||||||
|
let firstEleCn = this.modItems['modMainDiv'].children.length > 0 ? this.modItems['modMainDiv'].children[0].className : null;
|
||||||
|
if (firstEleCn.indexOf('select-group cap') >= 0) {
|
||||||
|
this.modItems['modMainDiv'].children[1].firstElementChild.focus();
|
||||||
|
} else {
|
||||||
|
this.modItems['modMainDiv'].firstElementChild.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,13 +370,33 @@ export default class ModificationsMenu extends TranslatedComponent {
|
|||||||
* in a modification
|
* in a modification
|
||||||
*/
|
*/
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
if (this.selectedModRef) {
|
if (!this.modValDidChange) {
|
||||||
this.selectedModRef.focus();
|
if (this.modItems['modMainDiv'].children.length > 0) {
|
||||||
|
if (this.modItems[this.selectedModId]) {
|
||||||
|
this.modItems[this.selectedModId].focus();
|
||||||
return;
|
return;
|
||||||
} else if (this.selectedSpecialRef) {
|
} else if (this.modItems[this.selectedSpecialId]) {
|
||||||
this.selectedSpecialRef.focus();
|
this.modItems[this.selectedSpecialId].focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let firstEleCn = this.modItems['modMainDiv'].children[0].className;
|
||||||
|
if (firstEleCn.indexOf('button-inline-menu') >= 0) {
|
||||||
|
this.modItems['modMainDiv'].firstElementChild.focus();
|
||||||
|
} else if (firstEleCn.indexOf('select-group cap') >= 0) {
|
||||||
|
this.modItems['modMainDiv'].children[1].firstElementChild.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._handleModChange(false);// Need to reset if component update due to value change
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* set focus to the modification menu icon after mod menu is unmounted.
|
||||||
|
*/
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.props.modButton) {
|
||||||
|
this.props.modButton.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,155 +407,90 @@ export default class ModificationsMenu extends TranslatedComponent {
|
|||||||
const { language, tooltip, termtip } = this.context;
|
const { language, tooltip, termtip } = this.context;
|
||||||
const translate = language.translate;
|
const translate = language.translate;
|
||||||
const { m } = this.props;
|
const { m } = this.props;
|
||||||
const {
|
const { blueprintMenuOpened, specialMenuOpened } = this.state;
|
||||||
blueprintProgress, blueprintMenuOpened, specialMenuOpened,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const appliedBlueprint = m.getBlueprint();
|
const _toggleBlueprintsMenu = this._toggleBlueprintsMenu;
|
||||||
const appliedGrade = m.getBlueprintGrade();
|
const _toggleSpecialsMenu = this._toggleSpecialsMenu;
|
||||||
const appliedExperimental = m.getExperimental();
|
const _rollFull = this._rollBest;
|
||||||
|
const _rollWorst = this._rollWorst;
|
||||||
|
const _rollFifty = this._rollFifty;
|
||||||
|
const _rollRandom = this._rollRandom;
|
||||||
|
const _reset = this._reset;
|
||||||
|
|
||||||
let renderComponents = [];
|
let blueprintLabel;
|
||||||
switch (true) {
|
let haveBlueprint = false;
|
||||||
case !appliedBlueprint || blueprintMenuOpened:
|
let blueprintTt;
|
||||||
renderComponents = this._renderBlueprints();
|
let blueprintCv;
|
||||||
break;
|
// TODO: Fix this to actually find the correct blueprint.
|
||||||
case specialMenuOpened:
|
if (!m.blueprint || !m.blueprint.name || !m.blueprint.fdname || !Modifications.modules[m.grp].blueprints || !Modifications.modules[m.grp].blueprints[m.blueprint.fdname]) {
|
||||||
renderComponents = this._renderSpecials();
|
this.props.ship.clearModuleBlueprint(m);
|
||||||
break;
|
this.props.ship.clearModuleSpecial(m);
|
||||||
default:
|
}
|
||||||
// Since the first case didn't apply, there is a blueprint applied so
|
if (m.blueprint && m.blueprint.name && Modifications.modules[m.grp].blueprints[m.blueprint.fdname].grades[m.blueprint.grade]) {
|
||||||
// we render the modifications
|
blueprintLabel = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
|
||||||
let blueprintTt = blueprintTooltip(language, m, appliedBlueprint, appliedGrade);
|
haveBlueprint = true;
|
||||||
|
blueprintTt = blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], Modifications.modules[m.grp].blueprints[m.blueprint.fdname].grades[m.blueprint.grade].engineers, m.grp);
|
||||||
|
blueprintCv = getPercent(m);
|
||||||
|
}
|
||||||
|
|
||||||
renderComponents.push(
|
let specialLabel;
|
||||||
<div style={{ cursor: 'pointer' }} key="blueprintsMenu"
|
|
||||||
className="section-menu button-inline-menu"
|
|
||||||
onMouseOver={termtip.bind(null, blueprintTt)}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
onClick={this._toggleBlueprintsMenu}
|
|
||||||
>
|
|
||||||
{translate(appliedBlueprint)} {translate('grade')} {appliedGrade}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (m.getApplicableExperimentals().length) {
|
|
||||||
let specialLabel = translate('PHRASE_SELECT_SPECIAL');
|
|
||||||
let specialTt;
|
let specialTt;
|
||||||
if (appliedExperimental) {
|
if (m.blueprint && m.blueprint.special) {
|
||||||
specialLabel = appliedExperimental;
|
specialLabel = m.blueprint.special.name;
|
||||||
specialTt = specialToolTip(language, m, appliedExperimental);
|
specialTt = specialToolTip(translate, m.blueprint.grades[m.blueprint.grade], m.grp, m, m.blueprint.special.edname);
|
||||||
}
|
} else {
|
||||||
renderComponents.push(
|
specialLabel = translate('PHRASE_SELECT_SPECIAL');
|
||||||
<div className="section-menu button-inline-menu"
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
onMouseOver={specialTt ? termtip.bind(null, specialTt) : null}
|
|
||||||
onMouseOut={specialTt ? tooltip.bind(null, null) : null}
|
|
||||||
onClick={this._toggleSpecialsMenu}
|
|
||||||
>{specialLabel}</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderComponents.push(
|
const specials = this._renderSpecials(this.props, this.context);
|
||||||
<div
|
/**
|
||||||
className="section-menu button-inline-menu warning"
|
* pnellesen - 05/28/2018 - added additional checks for specials.length below to ensure menus
|
||||||
style={{ cursor: 'pointer' }}
|
* display correctly in cases where there are no specials (ex: AFMUs.)
|
||||||
onClick={() => {
|
*/
|
||||||
m.resetEngineering();
|
const showBlueprintsMenu = blueprintMenuOpened;
|
||||||
this.selectedModRef = null;
|
const showSpecial = haveBlueprint && specials.length && !blueprintMenuOpened;
|
||||||
this.selectedSpecialRef = null;
|
const showSpecialsMenu = specialMenuOpened && specials.length;
|
||||||
tooltip(null);
|
const showRolls = haveBlueprint && !blueprintMenuOpened && (!specialMenuOpened || !specials.length);
|
||||||
this.setState({
|
const showReset = !blueprintMenuOpened && (!specialMenuOpened || !specials.length) && haveBlueprint;
|
||||||
blueprintMenuOpened: true,
|
const showMods = !blueprintMenuOpened && (!specialMenuOpened || !specials.length) && haveBlueprint;
|
||||||
blueprintProgress: undefined,
|
if (haveBlueprint) {
|
||||||
});
|
this.firstBPLabel = blueprintLabel;
|
||||||
}}
|
} else {
|
||||||
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RESET')}
|
this.firstBPLabel = 'selectBP';
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>{translate('reset')}</div>,
|
|
||||||
<table style={{ width: '100%', backgroundColor: 'transparent' }}>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
className={cn(
|
|
||||||
'section-menu button-inline-menu',
|
|
||||||
{ active: false },
|
|
||||||
)}
|
|
||||||
>{translate('mroll')}:</td>
|
|
||||||
<td
|
|
||||||
className={cn(
|
|
||||||
'section-menu button-inline-menu',
|
|
||||||
{ active: blueprintProgress === 0 },
|
|
||||||
)} style={{ cursor: 'pointer' }}
|
|
||||||
onClick={() => {
|
|
||||||
m.setBlueprintProgress(0);
|
|
||||||
this.setState({ blueprintProgress: 0 });
|
|
||||||
}}
|
|
||||||
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_WORST')}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>{translate('0%')}</td>
|
|
||||||
<td
|
|
||||||
className={cn(
|
|
||||||
'section-menu button-inline-menu',
|
|
||||||
{ active: blueprintProgress === 0.5 },
|
|
||||||
)} style={{ cursor: 'pointer' }}
|
|
||||||
onClick={() => {
|
|
||||||
m.setBlueprintProgress(0.5);
|
|
||||||
this.setState({ blueprintProgress: 0.5 });
|
|
||||||
}}
|
|
||||||
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_FIFTY')}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>{translate('50%')}</td>
|
|
||||||
<td
|
|
||||||
className={cn(
|
|
||||||
'section-menu button-inline-menu',
|
|
||||||
{ active: blueprintProgress === 1 },
|
|
||||||
)}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
onClick={() => {
|
|
||||||
m.setBlueprintProgress(1);
|
|
||||||
this.setState({ blueprintProgress: 1 });
|
|
||||||
}}
|
|
||||||
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_BEST')}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>{translate('100%')}</td>
|
|
||||||
<td
|
|
||||||
className={cn(
|
|
||||||
'section-menu button-inline-menu',
|
|
||||||
{ active: blueprintProgress % 0.5 !== 0 },
|
|
||||||
)}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
onClick={() => {
|
|
||||||
const blueprintProgress = Math.random();
|
|
||||||
m.setBlueprintProgress(blueprintProgress);
|
|
||||||
this.setState({ blueprintProgress });
|
|
||||||
}}
|
|
||||||
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RANDOM')}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>{translate('random')}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>,
|
|
||||||
<hr />,
|
|
||||||
<span
|
|
||||||
onMouseOver={termtip.bind(null, 'HELP_MODIFICATIONS_MENU')}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>
|
|
||||||
<table style={{ width: '100%' }}>
|
|
||||||
<tbody>
|
|
||||||
{this._renderModifications()}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('select', this.props.className)}
|
<div
|
||||||
|
className={cn('select', this.props.className)}
|
||||||
onClick={(e) => e.stopPropagation() }
|
onClick={(e) => e.stopPropagation() }
|
||||||
onContextMenu={stopCtxPropagation}
|
onContextMenu={stopCtxPropagation}
|
||||||
|
ref={modItem => this.modItems['modMainDiv'] = modItem}
|
||||||
>
|
>
|
||||||
{renderComponents}
|
{ showBlueprintsMenu | showSpecialsMenu ? '' : haveBlueprint ?
|
||||||
|
<div tabIndex="0" className={ cn('section-menu button-inline-menu', { selected: blueprintMenuOpened })} style={{ cursor: 'pointer' }} onMouseOver={termtip.bind(null, blueprintTt)} onMouseOut={tooltip.bind(null, null)} onClick={_toggleBlueprintsMenu} onKeyDown={ this._keyDown } ref={modItems => this.modItems[this.firstBPLabel] = modItems}>{blueprintLabel}</div> :
|
||||||
|
<div tabIndex="0" className={ cn('section-menu button-inline-menu', { selected: blueprintMenuOpened })} style={{ cursor: 'pointer' }} onClick={_toggleBlueprintsMenu} onKeyDown={ this._keyDown } ref={modItems => this.modItems[this.firstBPLabel] = modItems}>{translate('PHRASE_SELECT_BLUEPRINT')}</div> }
|
||||||
|
{ showBlueprintsMenu ? this._renderBlueprints(this.props, this.context) : null }
|
||||||
|
{ showSpecial & !showSpecialsMenu ? <div tabIndex="0" className={ cn('section-menu button-inline-menu', { selected: specialMenuOpened })} style={{ cursor: 'pointer' }} onMouseOver={specialTt ? termtip.bind(null, specialTt) : null} onMouseOut={specialTt ? tooltip.bind(null, null) : null} onClick={_toggleSpecialsMenu} onKeyDown={ this._keyDown }>{specialLabel}</div> : null }
|
||||||
|
{ showSpecialsMenu ? specials : null }
|
||||||
|
{ showReset ? <div tabIndex="0" className={'section-menu button-inline-menu warning'} style={{ cursor: 'pointer' }} onClick={_reset} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RESET')} onMouseOut={tooltip.bind(null, null)}> { translate('reset') } </div> : null }
|
||||||
|
{ showRolls ?
|
||||||
|
|
||||||
|
<table style={{ width: '100%', backgroundColor: 'transparent' }}>
|
||||||
|
<tbody>
|
||||||
|
{ showRolls ?
|
||||||
|
<tr>
|
||||||
|
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: false }) }> { translate('roll') }: </td>
|
||||||
|
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: blueprintCv === 0 }) } style={{ cursor: 'pointer' }} onClick={_rollWorst} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_WORST')} onMouseOut={tooltip.bind(null, null)}> { translate('0%') } </td>
|
||||||
|
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: blueprintCv === 50 })} style={{ cursor: 'pointer' }} onClick={_rollFifty} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_FIFTY')} onMouseOut={tooltip.bind(null, null)}> { translate('50%') } </td>
|
||||||
|
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: blueprintCv === 100 })} style={{ cursor: 'pointer' }} onClick={_rollFull} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_BEST')} onMouseOut={tooltip.bind(null, null)}> { translate('100%') } </td>
|
||||||
|
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: blueprintCv === null || blueprintCv % 50 != 0 })} style={{ cursor: 'pointer' }} onClick={_rollRandom} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RANDOM')} onMouseOut={tooltip.bind(null, null)}> { translate('random') } </td>
|
||||||
|
</tr> : null }
|
||||||
|
</tbody>
|
||||||
|
</table> : null }
|
||||||
|
{ showMods ? <hr /> : null }
|
||||||
|
{ showMods ?
|
||||||
|
<span onMouseOver={termtip.bind(null, 'HELP_MODIFICATIONS_MENU')} onMouseOut={tooltip.bind(null, null)} >
|
||||||
|
{ this._renderModifications(this.props) }
|
||||||
|
</span> : null }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,36 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import { ShipProps } from 'ed-forge';
|
|
||||||
const { SPEED, BOOST_SPEED, ROLL, BOOST_ROLL, YAW, BOOST_YAW, PITCH, BOOST_PITCH } = ShipProps;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Movement
|
* Movement
|
||||||
*/
|
*/
|
||||||
export default class Movement extends TranslatedComponent {
|
export default class Movement extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
code: PropTypes.string.isRequired,
|
marker: PropTypes.string.isRequired,
|
||||||
ship: PropTypes.object.isRequired,
|
ship: PropTypes.object.isRequired,
|
||||||
boost: PropTypes.bool.isRequired,
|
boost: PropTypes.bool.isRequired,
|
||||||
pips: PropTypes.object.isRequired,
|
eng: PropTypes.number.isRequired,
|
||||||
|
fuel: PropTypes.number.isRequired,
|
||||||
|
cargo: PropTypes.number.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param {Object} props React Component properties
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render movement
|
* Render movement
|
||||||
* @return {React.Component} contents
|
* @return {React.Component} contents
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { ship, boost } = this.props;
|
const { ship, boost, eng, cargo, fuel } = this.props;
|
||||||
const { language } = this.context;
|
const { language } = this.context;
|
||||||
const { formats } = language;
|
const { formats, translate, units } = language;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span id='movement'>
|
<span id='movement'>
|
||||||
@@ -49,10 +57,14 @@ export default class Movement extends TranslatedComponent {
|
|||||||
<path d="M359.5 422.4l-1.2 19.3-1.6.4-10.7-16 .2-.2 13-3.4.3.4zm-9 5l5.2 7.8.6-9.3-5.7 1.2zm-10.5 24l-13.2 8.6-2.6-9.7 15.8 1z"/>
|
<path d="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"/>
|
<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"/>
|
||||||
|
|
||||||
<text x="470" y="30" strokeWidth='0'>{formats.int(ship.get(boost ? BOOST_SPEED : SPEED)) + 'm/s'}</text>
|
{/* Speed */}
|
||||||
<text x="355" y="410" strokeWidth='0'>{formats.int(ship.get(boost ? BOOST_PITCH : PITCH)) + '°/s'}</text>
|
<text x="470" y="30" strokeWidth='0'>{ship.canThrust(cargo, fuel) ? formats.int(ship.calcSpeed(eng, fuel, cargo, boost)) + 'm/s' : '-'}</text>
|
||||||
<text x="450" y="110" strokeWidth='0'>{formats.int(ship.get(boost ? BOOST_ROLL : ROLL)) + '°/s'}</text>
|
{/* Pitch */}
|
||||||
<text x="160" y="430" strokeWidth='0'>{formats.int(ship.get(boost ? BOOST_YAW : YAW)) + '°/s'}</text>
|
<text x="355" y="410" strokeWidth='0'>{ship.canThrust(cargo, fuel) ? formats.int(ship.calcPitch(eng, fuel, cargo, boost)) + '°/s' : '-'}</text>
|
||||||
|
{/* Roll */}
|
||||||
|
<text x="450" y="110" strokeWidth='0'>{ship.canThrust(cargo, fuel) ? formats.int(ship.calcRoll(eng, fuel, cargo, boost)) + '°/s' : '-'}</text>
|
||||||
|
{/* Yaw */}
|
||||||
|
<text x="160" y="430" strokeWidth='0'>{ship.canThrust(cargo, fuel) ? formats.int(ship.calcYaw(eng, fuel, cargo, boost)) + '°/s' : '-'}</text>
|
||||||
</svg>
|
</svg>
|
||||||
</span>);
|
</span>);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,103 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
|
import * as Calc from '../shipyard/Calculations';
|
||||||
import PieChart from './PieChart';
|
import PieChart from './PieChart';
|
||||||
|
import { nameComparator } from '../utils/SlotFunctions';
|
||||||
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
|
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
|
||||||
import { Ship } from 'ed-forge';
|
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
import { DAMAGE_METRICS } from 'ed-forge/lib/src/ship-stats';
|
|
||||||
import { clone, mapValues, mergeWith, reverse, sortBy, sum, toPairs, values } from 'lodash';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns an object into a tooltip.
|
* Generates an internationalization friendly weapon comparator that will
|
||||||
* @param {function} translate Translate function
|
* sort by specified property (if provided) then by name/group, class, rating
|
||||||
* @param {object} o Map to make the tooltip from
|
* @param {function} translate Translation function
|
||||||
* @returns {React.Component} Tooltip
|
* @param {function} propComparator Optional property comparator
|
||||||
|
* @param {boolean} desc Use descending order
|
||||||
|
* @return {function} Comparator function for names
|
||||||
*/
|
*/
|
||||||
function objToTooltip(translate, o) {
|
export function weaponComparator(translate, propComparator, desc) {
|
||||||
return toPairs(o)
|
return (a, b) => {
|
||||||
.filter(([k, v]) => Boolean(v))
|
if (!desc) { // Flip A and B if ascending order
|
||||||
.map(([k, v]) => <div key={k}>{`${translate(k)}: ${v}`}</div>);
|
let t = a;
|
||||||
|
a = b;
|
||||||
|
b = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a property comparator is provided use it first
|
||||||
|
let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a, b);
|
||||||
|
|
||||||
|
if (diff) {
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Property matches so sort by name / group, then class, rating
|
||||||
|
if (a.name === b.name && a.grp === b.grp) {
|
||||||
|
if(a.class == b.class) {
|
||||||
|
return a.rating > b.rating ? 1 : -1;
|
||||||
|
}
|
||||||
|
return a.class - b.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nameComparator(translate, a, b);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a tooltip that shows damage by type.
|
||||||
|
* @param {function} translate Translation function
|
||||||
|
* @param {Object} formats Object that holds format functions
|
||||||
|
* @param {Calc.SDps} sdpsObject Object that holds sdps split up by type
|
||||||
|
* @returns {Array} Tooltip
|
||||||
|
*/
|
||||||
|
function getSDpsTooltip(translate, formats, sdpsObject) {
|
||||||
|
return Object.keys(sdpsObject).filter(key => sdpsObject[key])
|
||||||
|
.map(key => {
|
||||||
|
return (
|
||||||
|
<div key={key}>
|
||||||
|
{translate(key) + ' ' + formats.f1(sdpsObject[key])}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a data object used by {@link PieChart} that shows damage by type.
|
* Returns a data object used by {@link PieChart} that shows damage by type.
|
||||||
* @param {function} translate Translation function
|
* @param {function} translate Translation function
|
||||||
* @param {Calc.SDps} o Object that holds sdps split up by type
|
* @param {Calc.SDps} sdpsObject Object that holds sdps split up by type
|
||||||
* @returns {Object} Data object
|
* @returns {Object} Data object
|
||||||
*/
|
*/
|
||||||
function objToPie(translate, o) {
|
function getSDpsData(translate, sdpsObject) {
|
||||||
return toPairs(o).map(([k, value]) => {
|
return Object.keys(sdpsObject).map(key => {
|
||||||
return { label: translate(k), value };
|
return {
|
||||||
|
value: Math.round(sdpsObject[key]),
|
||||||
|
label: translate(key)
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds all damage of `add` onto `addOn`.
|
||||||
|
* @param {Calc.SDps} addOn Object that holds sdps split up by type (will be mutated)
|
||||||
|
* @param {Calc.SDps} add Object that holds sdps split up by type
|
||||||
|
*/
|
||||||
|
function addSDps(addOn, add) {
|
||||||
|
Object.keys(addOn).map(k => addOn[k] += (add[k] ? add[k] : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the overall sdps of an sdps object.
|
||||||
|
* @param {Calc.SDps} sdpsObject Object that holds sdps spluit up by type
|
||||||
|
*/
|
||||||
|
function sumSDps(sdpsObject) {
|
||||||
|
if (sdpsObject.total) {
|
||||||
|
return sdpsObject.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(sdpsObject).reduce(
|
||||||
|
(acc, k) => acc + (sdpsObject[k] ? sdpsObject[k] : 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Offence information
|
* Offence information
|
||||||
* Offence information consists of four panels:
|
* Offence information consists of four panels:
|
||||||
@@ -42,10 +108,12 @@ function objToPie(translate, o) {
|
|||||||
*/
|
*/
|
||||||
export default class Offence extends TranslatedComponent {
|
export default class Offence extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
code: PropTypes.string.isRequired,
|
marker: PropTypes.string.isRequired,
|
||||||
ship: PropTypes.instanceOf(Ship).isRequired,
|
ship: PropTypes.object.isRequired,
|
||||||
opponent: PropTypes.instanceOf(Ship).isRequired,
|
opponent: PropTypes.object.isRequired,
|
||||||
engagementRange: PropTypes.number.isRequired,
|
engagementrange: PropTypes.number.isRequired,
|
||||||
|
wep: PropTypes.number.isRequired,
|
||||||
|
opponentSys: PropTypes.number.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,234 +122,146 @@ export default class Offence extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
|
||||||
|
|
||||||
|
this._sort = this._sort.bind(this);
|
||||||
|
|
||||||
|
const damage = Calc.offenceMetrics(props.ship, props.opponent, props.wep, props.opponentSys, props.engagementrange);
|
||||||
this.state = {
|
this.state = {
|
||||||
predicate: 'classRating',
|
predicate: 'n',
|
||||||
desc: true,
|
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
|
* Set the sort order and sort
|
||||||
* @param {string} predicate Sort predicate
|
* @param {string} predicate Sort predicate
|
||||||
*/
|
*/
|
||||||
_sortOrder(predicate) {
|
_sortOrder(predicate) {
|
||||||
let desc = predicate == this.state.predicate ? !this.state.desc : true;
|
let desc = this.state.desc;
|
||||||
|
|
||||||
|
if (predicate == this.state.predicate) {
|
||||||
|
desc = !desc;
|
||||||
|
} else {
|
||||||
|
desc = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sort(predicate, desc);
|
||||||
this.setState({ 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
|
* Render offence
|
||||||
* @return {React.Component} contents
|
* @return {React.Component} contents
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { ship } = this.props;
|
const { ship, opponent, wep, engagementrange } = this.props;
|
||||||
const { language, tooltip, termtip } = this.context;
|
const { language, tooltip, termtip } = this.context;
|
||||||
const { formats, translate, units } = language;
|
const { formats, translate, units } = language;
|
||||||
|
const { damage } = this.state;
|
||||||
const sortOrder = this._sortOrder;
|
const sortOrder = this._sortOrder;
|
||||||
|
|
||||||
const {
|
const pd = ship.standard[4].m;
|
||||||
drained, sustained, rangeMultiplier, hardnessMultiplier, timeToDrain
|
|
||||||
} = ship.getMetrics(DAMAGE_METRICS);
|
|
||||||
const portions = {
|
|
||||||
Absolute: sustained.types.abs,
|
|
||||||
Explosive: sustained.types.expl,
|
|
||||||
Kinetic: sustained.types.kin,
|
|
||||||
Thermic: sustained.types.therm,
|
|
||||||
};
|
|
||||||
|
|
||||||
const oppShield = ship.getOpponent().getShield();
|
const opponentShields = Calc.shieldMetrics(opponent, 4);
|
||||||
const shieldMults = {
|
const opponentArmour = Calc.armourMetrics(opponent);
|
||||||
Absolute: 1,
|
|
||||||
Explosive: oppShield.explosive.damageMultiplier,
|
|
||||||
Kinetic: oppShield.kinetic.damageMultiplier,
|
|
||||||
Thermic: oppShield.thermal.damageMultiplier,
|
|
||||||
};
|
|
||||||
|
|
||||||
const oppArmour = ship.getOpponent().getArmour();
|
const timeToDrain = Calc.timeToDrainWep(ship, wep);
|
||||||
const armourMults = {
|
|
||||||
Absolute: 1,
|
|
||||||
Explosive: oppArmour.explosive.damageMultiplier,
|
|
||||||
Kinetic: oppArmour.kinetic.damageMultiplier,
|
|
||||||
Thermic: oppArmour.thermal.damageMultiplier,
|
|
||||||
};
|
|
||||||
|
|
||||||
const weapons = sortBy(ship.getHardpoints(), (m) => m.get('distributordraw'));
|
|
||||||
let rows = weapons.map((weapon) => {
|
|
||||||
const sdps = weapon.get('sustaineddamagepersecond');
|
|
||||||
const byRange = weapon.getRangeEffectiveness();
|
|
||||||
const weaponPortions = {
|
|
||||||
Absolute: weapon.get('absolutedamageportion'),
|
|
||||||
Explosive: weapon.get('explosivedamageportion'),
|
|
||||||
Kinetic: weapon.get('kineticdamageportion'),
|
|
||||||
Thermic: weapon.get('thermicdamageportion'),
|
|
||||||
};
|
|
||||||
const baseSdpsTooltip = objToTooltip(
|
|
||||||
translate,
|
|
||||||
mapValues(weaponPortions, (p) => formats.f1(sdps * p)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const bySys = oppShield.absolute.bySys;
|
let totalSEps = 0;
|
||||||
const shieldResEfts = mergeWith(
|
let totalSDpsObject = { 'absolute': 0, 'explosive': 0, 'kinetic': 0, 'thermal': 0 };
|
||||||
clone(weaponPortions),
|
let shieldsSDpsObject = { 'absolute': 0, 'explosive': 0, 'kinetic': 0, 'thermal': 0 };
|
||||||
shieldMults,
|
let armourSDpsObject = { 'absolute': 0, 'explosive': 0, 'kinetic': 0, 'thermal': 0 };
|
||||||
(objV, srcV) => objV * srcV
|
|
||||||
);
|
|
||||||
const byShieldRes = sum(values(shieldResEfts));
|
|
||||||
const shieldsSdpsTooltip = objToTooltip(
|
|
||||||
translate,
|
|
||||||
mapValues(
|
|
||||||
shieldResEfts,
|
|
||||||
(mult) => formats.f1(byRange * mult * bySys * sdps),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const shieldsEftTooltip = objToTooltip(
|
|
||||||
translate,
|
|
||||||
{
|
|
||||||
range: formats.pct1(byRange),
|
|
||||||
resistance: formats.pct1(byShieldRes),
|
|
||||||
'power distributor': formats.pct1(bySys),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const shieldEft = byRange * byShieldRes * bySys;
|
|
||||||
|
|
||||||
const byHardness = weapon.getArmourEffectiveness();
|
const rows = [];
|
||||||
const armourResEfts = mergeWith(
|
for (let i = 0; i < damage.length; i++) {
|
||||||
clone(weaponPortions),
|
const weapon = damage[i];
|
||||||
armourMults,
|
|
||||||
(objV, srcV) => objV * srcV,
|
|
||||||
);
|
|
||||||
const byArmourRes = sum(values(armourResEfts));
|
|
||||||
const armourSdpsTooltip = objToTooltip(
|
|
||||||
translate,
|
|
||||||
mapValues(
|
|
||||||
armourResEfts,
|
|
||||||
(mult) => formats.f1(byRange * mult * byHardness * sdps)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const armourEftTooltip = objToTooltip(
|
|
||||||
translate,
|
|
||||||
{
|
|
||||||
range: formats.pct1(byRange),
|
|
||||||
resistance: formats.pct1(byArmourRes),
|
|
||||||
hardness: formats.pct1(byHardness),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const armourEft = byRange * byArmourRes * byHardness;
|
|
||||||
|
|
||||||
const bp = weapon.getBlueprint();
|
totalSEps += weapon.seps;
|
||||||
const grade = weapon.getBlueprintGrade();
|
addSDps(totalSDpsObject, weapon.sdps.base);
|
||||||
const exp = weapon.getExperimental();
|
addSDps(shieldsSDpsObject, weapon.sdps.shields);
|
||||||
let bpTitle = `${translate(bp)} ${translate('grade')} ${grade}`;
|
addSDps(armourSDpsObject, weapon.sdps.armour);
|
||||||
if (exp) {
|
|
||||||
bpTitle += `, ${translate(exp)}`;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
slot: weapon.getSlot(),
|
|
||||||
mount: weapon.mount,
|
|
||||||
classRating: weapon.getClassRating(),
|
|
||||||
type: weapon.readMeta('type'),
|
|
||||||
bpTitle: bp ? ` (${bpTitle})` : null,
|
|
||||||
sdps,
|
|
||||||
baseSdpsTooltip,
|
|
||||||
shieldSdps: sdps * shieldEft,
|
|
||||||
shieldEft,
|
|
||||||
shieldsSdpsTooltip,
|
|
||||||
shieldsEftTooltip,
|
|
||||||
armourSdps: sdps * armourEft,
|
|
||||||
armourEft,
|
|
||||||
armourSdpsTooltip,
|
|
||||||
armourEftTooltip,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const { predicate, desc } = this.state;
|
const baseSDpsTooltipDetails = getSDpsTooltip(translate, formats, weapon.sdps.base);
|
||||||
rows = sortBy(rows, (row) => row[predicate]);
|
|
||||||
if (desc) {
|
const effectivenessShieldsTooltipDetails = [];
|
||||||
reverse(rows);
|
effectivenessShieldsTooltipDetails.push(<div key='range'>{translate('range') + ' ' + formats.pct1(weapon.effectiveness.shields.range)}</div>);
|
||||||
|
effectivenessShieldsTooltipDetails.push(<div key='resistance'>{translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.shields.resistance)}</div>);
|
||||||
|
effectivenessShieldsTooltipDetails.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(weapon.effectiveness.shields.sys)}</div>);
|
||||||
|
|
||||||
|
const effectiveShieldsSDpsTooltipDetails = getSDpsTooltip(translate, formats, weapon.sdps.armour);
|
||||||
|
|
||||||
|
const effectivenessArmourTooltipDetails = [];
|
||||||
|
effectivenessArmourTooltipDetails.push(<div key='range'>{translate('range') + ' ' + formats.pct1(weapon.effectiveness.armour.range)}</div>);
|
||||||
|
effectivenessArmourTooltipDetails.push(<div key='resistance'>{translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.armour.resistance)}</div>);
|
||||||
|
effectivenessArmourTooltipDetails.push(<div key='hardness'>{translate('hardness') + ' ' + formats.pct1(weapon.effectiveness.armour.hardness)}</div>);
|
||||||
|
|
||||||
|
const effectiveArmourSDpsTooltipDetails = getSDpsTooltip(translate, formats, weapon.sdps.armour);
|
||||||
|
|
||||||
|
rows.push(
|
||||||
|
<tr key={weapon.id}>
|
||||||
|
<td className='ri'>
|
||||||
|
{weapon.mount == 'F' ? <span onMouseOver={termtip.bind(null, 'fixed')} onMouseOut={tooltip.bind(null, null)}><MountFixed className='icon'/></span> : null}
|
||||||
|
{weapon.mount == 'G' ? <span onMouseOver={termtip.bind(null, 'gimballed')} onMouseOut={tooltip.bind(null, null)}><MountGimballed /></span> : null}
|
||||||
|
{weapon.mount == 'T' ? <span onMouseOver={termtip.bind(null, 'turreted')} onMouseOut={tooltip.bind(null, null)}><MountTurret /></span> : null}
|
||||||
|
{weapon.classRating} {translate(weapon.name)}
|
||||||
|
{weapon.engineering ? ' (' + weapon.engineering + ')' : null }
|
||||||
|
</td>
|
||||||
|
<td className='ri'><span onMouseOver={termtip.bind(null, baseSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.f1(weapon.sdps.base.total)}</span></td>
|
||||||
|
<td className='ri'><span onMouseOver={termtip.bind(null, effectiveShieldsSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.f1(weapon.sdps.shields.total)}</span></td>
|
||||||
|
<td className='ri'><span onMouseOver={termtip.bind(null, effectivenessShieldsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(weapon.effectiveness.shields.total)}</span></td>
|
||||||
|
<td className='ri'><span 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 sdpsTooltip = objToTooltip(
|
const totalSDps = sumSDps(totalSDpsObject);
|
||||||
translate,
|
const totalSDpsTooltipDetails = getSDpsTooltip(translate, formats, totalSDpsObject);
|
||||||
mapValues(portions, (p) => formats.f1(sustained.dps * p)),
|
const totalSDpsData = getSDpsData(translate, totalSDpsObject);
|
||||||
);
|
|
||||||
const sdpsPie = objToPie(
|
|
||||||
translate,
|
|
||||||
mapValues(portions, (p) => Math.round(sustained.dps * p)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const shieldSdpsSrcs = mergeWith(
|
const totalShieldsSDps = sumSDps(shieldsSDpsObject);
|
||||||
clone(portions),
|
const totalShieldsSDpsTooltipDetails = getSDpsTooltip(translate, formats, shieldsSDpsObject);
|
||||||
shieldMults,
|
const shieldsSDpsData = getSDpsData(translate, shieldsSDpsObject);
|
||||||
(objV, srcV) => sustained.dps * oppShield.absolute.bySys *
|
|
||||||
rangeMultiplier * objV * srcV,
|
|
||||||
);
|
|
||||||
const shieldsSdps = sum(values(shieldSdpsSrcs));
|
|
||||||
const shieldsSdpsTooltip = objToTooltip(
|
|
||||||
translate,
|
|
||||||
mapValues(shieldSdpsSrcs, (v) => formats.f1(v)),
|
|
||||||
);
|
|
||||||
const shieldsSdpsPie = objToPie(
|
|
||||||
translate,
|
|
||||||
mapValues(shieldSdpsSrcs, (v) => Math.round(v)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const armourSdpsSrcs = mergeWith(
|
const totalArmourSDps = sumSDps(armourSDpsObject);
|
||||||
clone(portions),
|
const totalArmourSDpsTooltipDetails = getSDpsTooltip(translate, formats, armourSDpsObject);
|
||||||
armourMults,
|
const armourSDpsData = getSDpsData(translate, armourSDpsObject);
|
||||||
(objV, srcV) => sustained.dps * hardnessMultiplier * rangeMultiplier *
|
|
||||||
objV * srcV,
|
|
||||||
);
|
|
||||||
const armourSdps = sum(values(armourSdpsSrcs));
|
|
||||||
const totalArmourSDpsTooltipDetails = objToTooltip(
|
|
||||||
translate,
|
|
||||||
mapValues(armourSdpsSrcs, (v) => formats.f1(v)),
|
|
||||||
);
|
|
||||||
const armourSDpsData = objToPie(
|
|
||||||
translate,
|
|
||||||
mapValues(armourSdpsSrcs, (v) => Math.round(v)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const drainedPortions = {
|
const timeToDepleteShields = Calc.timeToDeplete(opponentShields.total, totalShieldsSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
|
||||||
Absolute: drained.types.abs,
|
const timeToDepleteArmour = Calc.timeToDeplete(opponentArmour.total, totalArmourSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
|
||||||
Explosive: drained.types.expl,
|
|
||||||
Kinetic: drained.types.kin,
|
|
||||||
Thermic: drained.types.therm,
|
|
||||||
};
|
|
||||||
|
|
||||||
// How much damage do we deal, before the capacitor is empty?
|
|
||||||
const armourLeft = oppArmour.armour - (timeToDrain * armourSdps);
|
|
||||||
// If we can't kill the enemy on one capacitor, factor in drained damage
|
|
||||||
let timeToDepleteArmour;
|
|
||||||
if (armourLeft > 0) {
|
|
||||||
const effectiveDrainedDps = sum(values(mergeWith(
|
|
||||||
clone(drainedPortions),
|
|
||||||
armourMults,
|
|
||||||
(objV, srcV) => objV * srcV,
|
|
||||||
))) * drained.dps * rangeMultiplier *
|
|
||||||
hardnessMultiplier;
|
|
||||||
timeToDepleteArmour = effectiveDrainedDps === 0 ? Infinity :
|
|
||||||
timeToDrain + (armourLeft / effectiveDrainedDps);
|
|
||||||
} else {
|
|
||||||
timeToDepleteArmour = oppArmour.armour / armourSdps;
|
|
||||||
}
|
|
||||||
|
|
||||||
// How much damage do we deal, before the capacitor is empty?
|
|
||||||
const shieldsLeft = oppShield.withSCBs - (timeToDrain * shieldsSdps);
|
|
||||||
// If we can't kill the enemy on one capacitor, factor in drained damage
|
|
||||||
let timeToDepleteShields;
|
|
||||||
if (shieldsLeft > 0) {
|
|
||||||
const effectiveDrainedDps = sum(values(mergeWith(
|
|
||||||
clone(drainedPortions),
|
|
||||||
shieldMults,
|
|
||||||
(objV, srcV) => objV * srcV,
|
|
||||||
))) * drained.dps * rangeMultiplier;
|
|
||||||
timeToDepleteShields = effectiveDrainedDps === 0 ? Infinity :
|
|
||||||
timeToDrain + (shieldsLeft / effectiveDrainedDps);
|
|
||||||
} else {
|
|
||||||
timeToDepleteShields = oppShield.withSCBs / shieldsSdps;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span id='offence'>
|
<span id='offence'>
|
||||||
@@ -289,76 +269,28 @@ export default class Offence extends TranslatedComponent {
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className='main'>
|
<tr className='main'>
|
||||||
<th rowSpan='2' className='sortable' onClick={sortOrder.bind(this, 'classRating')}>{translate('weapon')}</th>
|
<th rowSpan='2' className='sortable' onClick={sortOrder.bind(this, 'n')}>{translate('weapon')}</th>
|
||||||
<th colSpan='1'>{translate('overall')}</th>
|
<th colSpan='1'>{translate('overall')}</th>
|
||||||
<th colSpan='3'>{translate('opponent\'s shields')}</th>
|
<th colSpan='2'>{translate('opponent\'s shields')}</th>
|
||||||
<th colSpan='3'>{translate('opponent\'s armour')}</th>
|
<th colSpan='2'>{translate('opponent\'s armour')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_SHIELDS')}
|
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_SHIELDS')} onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'esdpss')}>{'sdps'}</th>
|
||||||
onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'sdps')}>sdps</th>
|
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_SHIELDS')} onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'esdpss')}>{'sdps'}</th>
|
||||||
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_SHIELDS')}
|
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_SHIELDS')} onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'es')}>{'eft'}</th>
|
||||||
onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'shieldSdps')}>sdps</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_SHIELDS')}
|
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_ARMOUR')} onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'eh')}>{'eft'}</th>
|
||||||
onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'shieldEft')}>eft</th>
|
|
||||||
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_ARMOUR')}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'armourSdps')}>sdps</th>
|
|
||||||
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_ARMOUR')}
|
|
||||||
onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'armourEft')}>eft</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.map((row) => (
|
{rows}
|
||||||
<tr key={row.slot}>
|
|
||||||
<td className='ri'>
|
|
||||||
{row.mount == 'F' ? <span onMouseOver={termtip.bind(null, 'fixed')} onMouseOut={tooltip.bind(null, null)}><MountFixed className='icon'/></span> : null}
|
|
||||||
{row.mount == 'G' ? <span onMouseOver={termtip.bind(null, 'gimballed')} onMouseOut={tooltip.bind(null, null)}><MountGimballed /></span> : null}
|
|
||||||
{row.mount == 'T' ? <span onMouseOver={termtip.bind(null, 'turreted')} onMouseOut={tooltip.bind(null, null)}><MountTurret /></span> : null}
|
|
||||||
{row.classRating} {translate(row.type)}
|
|
||||||
{row.bpTitle}
|
|
||||||
</td>
|
|
||||||
<td className='ri'>
|
|
||||||
<span onMouseOver={termtip.bind(null, row.baseSdpsTooltip)}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>{formats.f1(row.sdps)}</span></td>
|
|
||||||
<td className='ri'>
|
|
||||||
<span onMouseOver={termtip.bind(null, row.shieldsSdpsTooltip)}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>{formats.f1(row.shieldSdps)}</span></td>
|
|
||||||
<td className='ri'>
|
|
||||||
<span onMouseOver={termtip.bind(null, row.shieldsEftTooltip)}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>{formats.pct1(row.shieldEft)}</span></td>
|
|
||||||
<td className='ri'>
|
|
||||||
<span onMouseOver={termtip.bind(null, row.armourSdpsTooltip)}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>{formats.f1(row.armourSdps)}</span></td>
|
|
||||||
<td className='ri'>
|
|
||||||
<span onMouseOver={termtip.bind(null, row.armourEftTooltip)}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>{formats.pct1(row.armourEft)}</span></td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{rows.length > 0 &&
|
{rows.length > 0 &&
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td className='ri'>
|
<td className='ri'><span onMouseOver={termtip.bind(null, totalSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>={formats.f1(totalSDps)}</span></td>
|
||||||
<span onMouseOver={termtip.bind(null, sdpsTooltip)} onMouseOut={tooltip.bind(null, null)}>
|
<td className='ri'><span onMouseOver={termtip.bind(null, totalShieldsSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>={formats.f1(totalShieldsSDps)}</span></td>
|
||||||
={formats.f1(sustained.dps)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className='ri'>
|
|
||||||
<span onMouseOver={termtip.bind(null, shieldsSdpsTooltip)} onMouseOut={tooltip.bind(null, null)}>
|
|
||||||
={formats.f1(shieldsSdps)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td></td>
|
|
||||||
<td className='ri'>
|
|
||||||
<span onMouseOver={termtip.bind(null, totalArmourSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>
|
|
||||||
={formats.f1(armourSdps)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td></td>
|
<td></td>
|
||||||
|
<td className='ri'><span onMouseOver={termtip.bind(null, totalArmourSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>={formats.f1(totalArmourSDps)}</span></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@@ -367,51 +299,22 @@ export default class Offence extends TranslatedComponent {
|
|||||||
</div>
|
</div>
|
||||||
<div className='group quarter'>
|
<div className='group quarter'>
|
||||||
<h2>{translate('offence metrics')}</h2>
|
<h2>{translate('offence metrics')}</h2>
|
||||||
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_DRAIN_WEP'))}
|
<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>
|
||||||
onMouseOut={tooltip.bind(null, null)}>
|
<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>
|
||||||
{translate('PHRASE_TIME_TO_DRAIN_WEP')}<br/>
|
<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>
|
||||||
{timeToDrain === Infinity ? translate('never') : formats.time(timeToDrain)}
|
<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>
|
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}<br/>{timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)}</h2>
|
||||||
<h2 onMouseOver={termtip.bind(null, translate('TT_EFFECTIVE_SDPS_SHIELDS'))}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}>
|
|
||||||
{translate('PHRASE_EFFECTIVE_SDPS_SHIELDS')}<br/>
|
|
||||||
{formats.f1(shieldsSdps)}
|
|
||||||
</h2>
|
|
||||||
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_SHIELDS'))}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}>
|
|
||||||
{translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}<br/>
|
|
||||||
{timeToDepleteShields === Infinity ? translate('never') : formats.time(timeToDepleteShields)}
|
|
||||||
</h2>
|
|
||||||
<h2 onMouseOver={termtip.bind(null, translate('TT_EFFECTIVE_SDPS_ARMOUR'))}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}>
|
|
||||||
{translate('PHRASE_EFFECTIVE_SDPS_ARMOUR')}<br/>
|
|
||||||
{formats.f1(armourSdps)}
|
|
||||||
</h2>
|
|
||||||
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_ARMOUR'))}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}>
|
|
||||||
{translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}<br/>
|
|
||||||
{timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)}
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='group quarter'>
|
<div className='group quarter'>
|
||||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_OVERALL_DAMAGE'))}
|
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_OVERALL_DAMAGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('overall damage')}</h2>
|
||||||
onMouseOut={tooltip.bind(null, null)}>
|
<PieChart data={totalSDpsData} />
|
||||||
{translate('overall damage')}
|
|
||||||
</h2>
|
|
||||||
<PieChart data={sdpsPie} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className='group quarter'>
|
<div className='group quarter'>
|
||||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SHIELD_DAMAGE'))}
|
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SHIELD_DAMAGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('shield damage sources')}</h2>
|
||||||
onMouseOut={tooltip.bind(null, null)}>
|
<PieChart data={shieldsSDpsData} />
|
||||||
{translate('shield damage sources')}
|
|
||||||
</h2>
|
|
||||||
<PieChart data={shieldsSdpsPie} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className='group quarter'>
|
<div className='group quarter'>
|
||||||
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_ARMOUR_DAMAGE'))}
|
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_ARMOUR_DAMAGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('armour damage sources')}</h2>
|
||||||
onMouseOut={tooltip.bind(null, null)}>
|
|
||||||
{translate('armour damage sources')}
|
|
||||||
</h2>
|
|
||||||
<PieChart data={armourSDpsData} />
|
<PieChart data={armourSDpsData} />
|
||||||
</div>
|
</div>
|
||||||
</span>);
|
</span>);
|
||||||
|
|||||||
@@ -11,24 +11,29 @@ import Movement from './Movement';
|
|||||||
import Offence from './Offence';
|
import Offence from './Offence';
|
||||||
import Defence from './Defence';
|
import Defence from './Defence';
|
||||||
import WeaponDamageChart from './WeaponDamageChart';
|
import WeaponDamageChart from './WeaponDamageChart';
|
||||||
import Pips from '../components/Pips';
|
|
||||||
import Boost from '../components/Boost';
|
|
||||||
import Fuel from '../components/Fuel';
|
|
||||||
import Cargo from '../components/Cargo';
|
|
||||||
import ShipPicker from '../components/ShipPicker';
|
|
||||||
import EngagementRange from '../components/EngagementRange';
|
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
import { ShipProps } from 'ed-forge';
|
|
||||||
const { CARGO_CAPACITY, FUEL_CAPACITY } = ShipProps;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outfitting subpages
|
* Outfitting subpages
|
||||||
*/
|
*/
|
||||||
export default class OutfittingSubpages extends TranslatedComponent {
|
export default class OutfittingSubpages extends TranslatedComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
ship: PropTypes.object.isRequired,
|
ship: PropTypes.object.isRequired,
|
||||||
code: PropTypes.string.isRequired,
|
code: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
buildName: PropTypes.string,
|
buildName: PropTypes.string,
|
||||||
|
sys: PropTypes.number.isRequired,
|
||||||
|
eng: PropTypes.number.isRequired,
|
||||||
|
wep: PropTypes.number.isRequired,
|
||||||
|
cargo: PropTypes.number.isRequired,
|
||||||
|
fuel: PropTypes.number.isRequired,
|
||||||
|
boost: PropTypes.bool.isRequired,
|
||||||
|
engagementRange: PropTypes.number.isRequired,
|
||||||
|
opponent: PropTypes.object.isRequired,
|
||||||
|
opponentBuild: PropTypes.string,
|
||||||
|
opponentSys: PropTypes.number.isRequired,
|
||||||
|
opponentEng: PropTypes.number.isRequired,
|
||||||
|
opponentWep: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,17 +42,13 @@ export default class OutfittingSubpages extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
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.props.ship.setOpponent(this.props.ship);
|
|
||||||
this.state = {
|
this.state = {
|
||||||
boost: false,
|
|
||||||
cargo: props.ship.get(CARGO_CAPACITY),
|
|
||||||
fuel: props.ship.get(FUEL_CAPACITY),
|
|
||||||
pips: props.ship.getDistributorSettingsObject(),
|
|
||||||
tab: Persist.getOutfittingTab() || 'power',
|
tab: Persist.getOutfittingTab() || 'power',
|
||||||
engagementRange: 1000,
|
|
||||||
opponent: this.props.ship,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,113 +57,128 @@ export default class OutfittingSubpages extends TranslatedComponent {
|
|||||||
* @param {string} tab Tab name
|
* @param {string} tab Tab name
|
||||||
*/
|
*/
|
||||||
_showTab(tab) {
|
_showTab(tab) {
|
||||||
Persist.setOutfittingTab(tab);
|
|
||||||
this.setState({ 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
|
* Render the section
|
||||||
* @return {React.Component} Contents
|
* @return {React.Component} Contents
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { buildName, code, ship } = this.props;
|
const tab = this.state.tab;
|
||||||
const { boost, cargo, fuel, pips, tab, engagementRange, opponent } = this.state;
|
const translate = this.context.language.translate;
|
||||||
const { translate } = this.context.language;
|
let tabSection;
|
||||||
|
|
||||||
|
switch (tab) {
|
||||||
|
case 'power': tabSection = this._powerTab(); break;
|
||||||
|
case 'profiles': tabSection = this._profilesTab(); break;
|
||||||
|
case 'offence': tabSection = this._offenceTab(); break;
|
||||||
|
case 'defence': tabSection = this._defenceTab(); break;
|
||||||
|
}
|
||||||
|
|
||||||
const cargoCapacity = ship.get(CARGO_CAPACITY);
|
|
||||||
const showCargoSlider = cargoCapacity > 0;
|
|
||||||
return (
|
return (
|
||||||
<div>
|
|
||||||
{/* Control of ship and opponent */}
|
|
||||||
<div className="group quarter">
|
|
||||||
<h2 style={{ verticalAlign: 'middle', textAlign: 'center' }}>
|
|
||||||
{translate('ship control')}
|
|
||||||
</h2>
|
|
||||||
<Boost boost={boost} onChange={(boost) => this.setState({ boost })} />
|
|
||||||
</div>
|
|
||||||
<div className="group quarter">
|
|
||||||
<h2 style={{ verticalAlign: 'middle', textAlign: 'center' }}>
|
|
||||||
{translate('opponent')}
|
|
||||||
</h2>
|
|
||||||
<ShipPicker ship={ship} onChange={(opponent) => this.setState({ opponent })} />
|
|
||||||
</div>
|
|
||||||
<div className={cn('group', { quarter: showCargoSlider, half: !showCargoSlider })}>
|
|
||||||
<Fuel fuelCapacity={ship.get(FUEL_CAPACITY)} fuel={fuel}
|
|
||||||
onChange={(fuel) => this.setState({ fuel })} />
|
|
||||||
</div>
|
|
||||||
{showCargoSlider ?
|
|
||||||
<div className="group quarter">
|
|
||||||
<Cargo cargoCapacity={cargoCapacity} cargo={cargo}
|
|
||||||
onChange={(cargo) => this.setState({ cargo })} />
|
|
||||||
</div> : null}
|
|
||||||
<div className="group half">
|
|
||||||
<Pips ship={ship} pips={pips} onChange={(pips) => this.setState({ pips })} />
|
|
||||||
</div>
|
|
||||||
<div className="group half">
|
|
||||||
<EngagementRange ship={ship} engagementRange={engagementRange}
|
|
||||||
onChange={(engagementRange) => this.setState({ engagementRange })} />
|
|
||||||
</div>
|
|
||||||
<div className='group full' style={{ minHeight: '1000px' }}>
|
<div className='group full' style={{ minHeight: '1000px' }}>
|
||||||
<table className='tabs'>
|
<table className='tabs'>
|
||||||
{/* Select tab section */}
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{ width:'25%' }} className={cn({ active: tab == 'power' })}
|
<th style={{ width:'25%' }} className={cn({ active: tab == 'power' })} onClick={this._showTab.bind(this, 'power')} >{translate('power and costs')}</th>
|
||||||
onClick={this._showTab.bind(this, 'power')}>
|
<th style={{ width:'25%' }} className={cn({ active: tab == 'profiles' })} onClick={this._showTab.bind(this, 'profiles')} >{translate('profiles')}</th>
|
||||||
{translate('power and costs')}
|
<th style={{ width:'25%' }} className={cn({ active: tab == 'offence' })} onClick={this._showTab.bind(this, 'offence')} >{translate('offence')}</th>
|
||||||
</th>
|
<th style={{ width:'25%' }} className={cn({ active: tab == 'defence' })} onClick={this._showTab.bind(this, 'defence')} >{translate('defence')}</th>
|
||||||
<th style={{ width:'25%' }} className={cn({ active: tab == 'profiles' })}
|
|
||||||
onClick={this._showTab.bind(this, 'profiles')}>
|
|
||||||
{translate('profiles')}</th>
|
|
||||||
<th style={{ width:'25%' }} className={cn({ active: tab == 'offence' })}
|
|
||||||
onClick={this._showTab.bind(this, 'offence')}>
|
|
||||||
{translate('offence')}
|
|
||||||
</th>
|
|
||||||
<th style={{ width:'25%' }} className={cn({ active: tab == 'defence' })}
|
|
||||||
onClick={this._showTab.bind(this, 'defence')}>
|
|
||||||
{translate('tab_defence')}
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
{/* Show selected tab */}
|
{tabSection}
|
||||||
{tab == 'power' ?
|
|
||||||
<div>
|
|
||||||
<PowerManagement ship={ship} code={code} />
|
|
||||||
<CostSection ship={ship} buildName={buildName} code={code} />
|
|
||||||
</div> : null}
|
|
||||||
{tab == 'profiles' ?
|
|
||||||
<div>
|
|
||||||
<div className='group third'>
|
|
||||||
<h1>{translate('engine profile')}</h1>
|
|
||||||
<EngineProfile code={code} ship={ship} fuel={fuel} cargo={cargo} pips={pips} boost={boost} />
|
|
||||||
</div>
|
|
||||||
<div className='group third'>
|
|
||||||
<h1>{translate('fsd profile')}</h1>
|
|
||||||
<FSDProfile code={code} ship={ship} fuel={fuel} cargo={cargo} />
|
|
||||||
</div>
|
|
||||||
<div className='group third'>
|
|
||||||
<h1>{translate('movement profile')}</h1>
|
|
||||||
<Movement code={code} ship={ship} boost={boost} pips={pips} />
|
|
||||||
</div>
|
|
||||||
<div className='group half'>
|
|
||||||
<h1>{translate('damage to opponent\'s shields')}</h1>
|
|
||||||
<WeaponDamageChart code={code} ship={ship} opponentDefence={opponent.getShield()} engagementRange={engagementRange} />
|
|
||||||
</div>
|
|
||||||
<div className='group half'>
|
|
||||||
<h1>{translate('damage to opponent\'s hull')}</h1>
|
|
||||||
<WeaponDamageChart code={code} ship={ship} opponentDefence={opponent.getArmour()} engagementRange={engagementRange} />
|
|
||||||
</div>
|
|
||||||
</div> : null}
|
|
||||||
{tab == 'offence' ?
|
|
||||||
<div>
|
|
||||||
<Offence code={code} ship={ship} opponent={opponent} engagementRange={engagementRange} />
|
|
||||||
</div> : null}
|
|
||||||
{tab == 'defence' ?
|
|
||||||
<div>
|
|
||||||
<Defence code={code} ship={ship} opponent={opponent} engagementRange={engagementRange} />
|
|
||||||
</div> : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const LABEL_COLOUR = '#000000';
|
|||||||
* A pie chart
|
* A pie chart
|
||||||
*/
|
*/
|
||||||
export default class PieChart extends Component {
|
export default class PieChart extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
data : PropTypes.array.isRequired
|
data : PropTypes.array.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import { Pip } from './SvgIcons';
|
import { Pip } from './SvgIcons';
|
||||||
import { autoBind } from 'react-extras';
|
import { autoBind } from 'react-extras';
|
||||||
import { Ship } from 'ed-forge';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pips displays SYS/ENG/WEP pips and allows users to change them with key presses by clicking on the relevant area.
|
* Pips displays SYS/ENG/WEP pips and allows users to change them with key presses by clicking on the relevant area.
|
||||||
@@ -11,9 +10,13 @@ import { Ship } from 'ed-forge';
|
|||||||
*/
|
*/
|
||||||
export default class Pips extends TranslatedComponent {
|
export default class Pips extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
ship: PropTypes.instanceOf(Ship).isRequired,
|
sys: PropTypes.number.isRequired,
|
||||||
pips: PropTypes.object.isRequired,
|
eng: PropTypes.number.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
wep: PropTypes.number.isRequired,
|
||||||
|
mcSys: PropTypes.number.isRequired,
|
||||||
|
mcEng: PropTypes.number.isRequired,
|
||||||
|
mcWep: PropTypes.number.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,12 +27,6 @@ export default class Pips extends TranslatedComponent {
|
|||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
autoBind(this);
|
||||||
|
|
||||||
const { ship } = props;
|
|
||||||
this._incSys = this._change(ship.incSys);
|
|
||||||
this._incEng = this._change(ship.incEng);
|
|
||||||
this._incWep = this._change(ship.incWep);
|
|
||||||
this._reset = this._change(ship.pipsReset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,42 +71,153 @@ export default class Pips extends TranslatedComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a function that handles pip assignment and call `onChance`.
|
* Reset the capacitor
|
||||||
* @param {String} cb Callback that handles the actual pip assignment
|
|
||||||
* @param {Boolean} isMc True when increase is by multi crew
|
|
||||||
* @returns {Function} Function that handles pip assigment
|
|
||||||
*/
|
*/
|
||||||
_change(cb, isMc) {
|
_reset(isMc) {
|
||||||
return () => {
|
let { sys, eng, wep, mcSys, mcEng, mcWep } = this.props;
|
||||||
cb(isMc);
|
if (isMc) {
|
||||||
this.props.onChange(this.props.ship.getDistributorSettingsObject());
|
if (mcSys || mcEng || mcWep) {
|
||||||
|
sys -= mcSys;
|
||||||
|
eng -= mcEng;
|
||||||
|
wep -= mcWep;
|
||||||
|
this.props.onChange(sys, eng, wep, 0, 0, 0);
|
||||||
|
}
|
||||||
|
} else if (sys != 2 || eng != 2 || wep != 2) {
|
||||||
|
sys = eng = wep = 2;
|
||||||
|
this.props.onChange(sys + mcSys, eng + mcEng, wep + mcWep, mcSys, mcEng, mcWep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the SYS capacitor
|
||||||
|
*/
|
||||||
|
_incSys() {
|
||||||
|
this._inc('sys', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the ENG capacitor
|
||||||
|
*/
|
||||||
|
_incEng() {
|
||||||
|
this._inc('eng', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the WEP capacitor
|
||||||
|
*/
|
||||||
|
_incWep() {
|
||||||
|
this._inc('wep', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_wrapMcClick(key) {
|
||||||
|
return (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
if (key == 'rst') {
|
||||||
|
this._reset(true);
|
||||||
|
} else {
|
||||||
|
this._inc(key, true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increases a given capacitor
|
||||||
|
* @param {String} key Pip name to increase (one of 'sys', 'eng', 'wep')
|
||||||
|
* @param {Boolean} isMc True when increase is by multi crew
|
||||||
|
*/
|
||||||
|
_inc(key, isMc) {
|
||||||
|
if (!['sys', 'eng', 'wep'].includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { sys, eng, wep, mcSys, mcEng, mcWep } = this.props;
|
||||||
|
let mc = key == 'sys' ? mcSys : (key == 'eng' ? mcEng : mcWep);
|
||||||
|
let pips = this.props[key] - mc;
|
||||||
|
let other1 = key == 'sys' ? eng - mcEng : sys - mcSys;
|
||||||
|
let other2 = key == 'wep' ? eng - mcEng : wep - mcWep;
|
||||||
|
|
||||||
|
const required = Math.min(1, 4 - mc - pips);
|
||||||
|
if (isMc) {
|
||||||
|
// We can only set full pips in multi-crew also we can only set two pips
|
||||||
|
if (required > 0.5 && mcSys + mcEng + mcWep < 2) {
|
||||||
|
if (key == 'sys') {
|
||||||
|
mcSys += 1;
|
||||||
|
} else if (key == 'eng') {
|
||||||
|
mcEng += 1;
|
||||||
|
} else {
|
||||||
|
mcWep += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (required > 0) {
|
||||||
|
if (required == 0.5) {
|
||||||
|
// Take from whichever is larger
|
||||||
|
if (other1 > other2) {
|
||||||
|
other1 -= 0.5;
|
||||||
|
} else {
|
||||||
|
other2 -= 0.5;
|
||||||
|
}
|
||||||
|
pips += 0.5;
|
||||||
|
} else {
|
||||||
|
// Required is 1 - take from both if possible
|
||||||
|
if (other1 == 0) {
|
||||||
|
other2 -= 1;
|
||||||
|
} else if (other2 == 0) {
|
||||||
|
other1 -= 1;
|
||||||
|
} else {
|
||||||
|
other1 -= 0.5;
|
||||||
|
other2 -= 0.5;
|
||||||
|
}
|
||||||
|
pips += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sys = mcSys + (key == 'sys' ? pips : other1);
|
||||||
|
eng = mcEng + (key == 'eng' ? pips : (key == 'sys' ? other1 : other2));
|
||||||
|
wep = mcWep + (key == 'wep' ? pips : other2);
|
||||||
|
this.props.onChange(sys, eng, wep, mcSys, mcEng, mcWep);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up the rendering for pips
|
* Set up the rendering for pips
|
||||||
|
* @param {Number} sys the SYS pips
|
||||||
|
* @param {Number} eng the ENG pips
|
||||||
|
* @param {Number} wep the WEP pips
|
||||||
|
* @param {Number} mcSys SYS pips from multi-crew
|
||||||
|
* @param {Number} mcEng ENG pips from multi-crew
|
||||||
|
* @param {Number} mcWep WEP pips from multi-crew
|
||||||
* @returns {Object} Object containing the rendering for the pips
|
* @returns {Object} Object containing the rendering for the pips
|
||||||
*/
|
*/
|
||||||
_renderPips() {
|
_renderPips(sys, eng, wep, mcSys, mcEng, mcWep) {
|
||||||
const pipsSvg = {
|
const pipsSvg = {
|
||||||
Sys: [],
|
SYS: [],
|
||||||
Eng: [],
|
ENG: [],
|
||||||
Wep: [],
|
WEP: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let k in this.props.pips) {
|
// Multi-crew pipsSettings actually are included in the overall pip count therefore
|
||||||
let { base, mc } = this.props.pips[k];
|
// we can consider [0, sys - mcSys] as normal pipsSettings whilst [sys - mcSys, sys]
|
||||||
for (let i = 0; i < Math.floor(base); i++) {
|
// are the multi-crew pipsSettings in what follows.
|
||||||
pipsSvg[k].push(<Pip key={i} className='full' />);
|
|
||||||
|
let pipsSettings = {
|
||||||
|
SYS: [sys, mcSys],
|
||||||
|
ENG: [eng, mcEng],
|
||||||
|
WEP: [wep, mcWep],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let pipName in pipsSettings) {
|
||||||
|
let [pips, mcPips] = pipsSettings[pipName];
|
||||||
|
for (let i = 0; i < Math.floor(pips - mcPips); i++) {
|
||||||
|
pipsSvg[pipName].push(<Pip key={i} className='full' />);
|
||||||
}
|
}
|
||||||
if (base > Math.floor(base)) {
|
if (pips > Math.floor(pips)) {
|
||||||
pipsSvg[k].push(<Pip className='half' key={'half'} />);
|
pipsSvg[pipName].push(<Pip className='half' key={'half'} />);
|
||||||
}
|
}
|
||||||
for (let i = 0; i < mc; i++) {
|
for (let i = pips - mcPips; i < Math.floor(pips); i++) {
|
||||||
pipsSvg[k].push(<Pip key={base + i} className='mc' />);
|
pipsSvg[pipName].push(<Pip key={i} className='mc' />);
|
||||||
}
|
}
|
||||||
for (let i = Math.ceil(base + mc); i < 4; i++) {
|
for (let i = Math.floor(pips + 0.5); i < 4; i++) {
|
||||||
pipsSvg[k].push(<Pip className='empty' key={i} />);
|
pipsSvg[pipName].push(<Pip className='empty' key={i} />);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,10 +229,11 @@ export default class Pips extends TranslatedComponent {
|
|||||||
* @return {React.Component} contents
|
* @return {React.Component} contents
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { ship } = this.props;
|
const { tooltip, termtip } = this.context;
|
||||||
const { translate } = this.context.language;
|
const { formats, translate, units } = this.context.language;
|
||||||
|
const { sys, eng, wep, mcSys, mcEng, mcWep } = this.props;
|
||||||
|
|
||||||
const pipsSvg = this._renderPips();
|
const pipsSvg = this._renderPips(sys, eng, wep, mcSys, mcEng, mcWep);
|
||||||
return (
|
return (
|
||||||
<span id='pips'>
|
<span id='pips'>
|
||||||
<table>
|
<table>
|
||||||
@@ -132,40 +241,38 @@ export default class Pips extends TranslatedComponent {
|
|||||||
<tr>
|
<tr>
|
||||||
<td> </td>
|
<td> </td>
|
||||||
<td> </td>
|
<td> </td>
|
||||||
<td className='clickable' onClick={this._incEng}>{pipsSvg.Eng}</td>
|
<td className='clickable' onClick={() => this._inc('eng')}
|
||||||
|
onContextMenu={this._wrapMcClick('eng')}>{pipsSvg['ENG']}</td>
|
||||||
<td> </td>
|
<td> </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td> </td>
|
<td> </td>
|
||||||
<td className='clickable' onClick={this._incSys}>{pipsSvg.Sys}</td>
|
<td className='clickable' onClick={this._incSys}
|
||||||
<td className='clickable' onClick={this._incEng}>
|
onContextMenu={this._wrapMcClick('sys')}>{pipsSvg['SYS']}</td>
|
||||||
{translate('ENG')}
|
<td className='clickable' onClick={this._incEng}
|
||||||
</td>
|
onContextMenu={this._wrapMcClick('eng')}>{translate('ENG')}</td>
|
||||||
<td className='clickable' onClick={this._incWep}>{pipsSvg.Wep}</td>
|
<td className='clickable' onClick={this._incWep}
|
||||||
|
onContextMenu={this._wrapMcClick('wep')}>{pipsSvg['WEP']}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td> </td>
|
<td> </td>
|
||||||
<td className='clickable' onClick={this._incSys}>
|
<td className='clickable' onClick={this._incSys}
|
||||||
{translate('SYS')}
|
onContextMenu={this._wrapMcClick('sys')}>{translate('SYS')}</td>
|
||||||
</td>
|
<td className='clickable' onClick={this._reset.bind(this, false)}>
|
||||||
<td className='clickable' onClick={this._reset}>
|
|
||||||
{translate('RST')}
|
{translate('RST')}
|
||||||
</td>
|
</td>
|
||||||
<td className='clickable' onClick={this._incWep}>
|
<td className='clickable' onClick={this._incWep}
|
||||||
{translate('WEP')}
|
onContextMenu={this._wrapMcClick('wep')}>{translate('WEP')}</td>
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td> </td>
|
<td> </td>
|
||||||
<td className='clickable' onClick={this._change(ship.incSys, true)}>
|
<td> </td>
|
||||||
<Pip className='mc' />
|
<td className='clickable secondary' onClick={this._wrapMcClick('rst')}
|
||||||
</td>
|
onMouseEnter={termtip.bind(null, 'PHRASE_MULTI_CREW_CAPACITOR_POINTS')}
|
||||||
<td className='clickable' onClick={this._change(ship.incEng, true)}>
|
onMouseLeave={tooltip.bind(null, null)}>
|
||||||
<Pip className='mc' />
|
{translate('RST')}
|
||||||
</td>
|
|
||||||
<td className='clickable' onClick={this._change(ship.incWep, true)}>
|
|
||||||
<Pip className='mc' />
|
|
||||||
</td>
|
</td>
|
||||||
|
<td> </td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -4,25 +4,27 @@ import * as d3 from 'd3';
|
|||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||||
import { Ship } from 'ed-forge';
|
|
||||||
import { POWER_METRICS } from 'ed-forge/lib/src/ship-stats';
|
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the band-class.
|
* Round to avoid floating point precision errors
|
||||||
* @param {Boolean} selected Band selected
|
* @param {Boolean} selected Band selected
|
||||||
* @param {Number} relDraw Relative amount of power drawn by this band and
|
* @param {number} sum Band power sum
|
||||||
* all prior
|
* @param {number} avail Total available power
|
||||||
* @return {string} CSS Class name
|
* @return {string} CSS Class name
|
||||||
*/
|
*/
|
||||||
function getClass(selected, relDraw) {
|
function getClass(selected, sum, avail) {
|
||||||
if (selected) {
|
return selected ? 'secondary' : ((Math.round(sum * 100) / 100) >= avail) ? 'warning' : 'primary';
|
||||||
return 'secondary';
|
|
||||||
} else if (relDraw >= 1) {
|
|
||||||
return 'warning';
|
|
||||||
} else {
|
|
||||||
return 'primary';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the # label for a Priority band
|
||||||
|
* @param {number} val Priority Band Watt value
|
||||||
|
* @param {number} index Priority Band index
|
||||||
|
* @param {Function} wattScale Watt Scale function
|
||||||
|
* @return {number} label / text
|
||||||
|
*/
|
||||||
|
function bandText(val, index, wattScale) {
|
||||||
|
return (val > 0 && wattScale(val) > 13) ? index + 1 : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,9 +33,10 @@ function getClass(selected, relDraw) {
|
|||||||
*/
|
*/
|
||||||
export default class PowerBands extends TranslatedComponent {
|
export default class PowerBands extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
ship: PropTypes.instanceOf(Ship).isRequired,
|
bands: PropTypes.array.isRequired,
|
||||||
code: PropTypes.string.isRequired,
|
available: PropTypes.number.isRequired,
|
||||||
width: PropTypes.number.isRequired,
|
width: PropTypes.number.isRequired,
|
||||||
|
code: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,16 +46,20 @@ export default class PowerBands extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
|
||||||
this.wattScale = d3.scaleLinear();
|
this.wattScale = d3.scaleLinear();
|
||||||
this.pctScale = d3.scaleLinear().domain([0, 1]);
|
this.pctScale = d3.scaleLinear().domain([0, 1]);
|
||||||
this.wattAxis = d3.axisTop(this.wattScale).tickSizeOuter(0).tickFormat(context.language.formats.r2);
|
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.pctAxis = d3.axisBottom(this.pctScale).tickSizeOuter(0).tickFormat(context.language.formats.rPct);
|
||||||
|
|
||||||
|
this._updateDimensions = this._updateDimensions.bind(this);
|
||||||
|
this._updateScales = this._updateScales.bind(this);
|
||||||
|
this._selectNone = this._selectNone.bind(this);
|
||||||
this._hidetip = () => this.context.tooltip();
|
this._hidetip = () => this.context.tooltip();
|
||||||
|
|
||||||
this.profile = props.ship.getMetrics(POWER_METRICS);
|
let maxBand = props.bands[props.bands.length - 1];
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
maxPwr: Math.max(props.available, maxBand.retractedSum, maxBand.deployedSum),
|
||||||
ret: {},
|
ret: {},
|
||||||
dep: {}
|
dep: {}
|
||||||
};
|
};
|
||||||
@@ -76,6 +83,8 @@ export default class PowerBands extends TranslatedComponent {
|
|||||||
let mRight = Math.round(140 * scale);
|
let mRight = Math.round(140 * scale);
|
||||||
let innerWidth = props.width - mLeft - mRight;
|
let innerWidth = props.width - mLeft - mRight;
|
||||||
|
|
||||||
|
this._updateScales(innerWidth, this.state.maxPwr, props.available);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
barHeight,
|
barHeight,
|
||||||
innerHeight,
|
innerHeight,
|
||||||
@@ -131,67 +140,41 @@ export default class PowerBands extends TranslatedComponent {
|
|||||||
this.setState({ dep: Object.assign({}, dep) });
|
this.setState({ dep: Object.assign({}, dep) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update scale
|
||||||
|
* @param {number} innerWidth SVG innerwidth
|
||||||
|
* @param {number} maxPwr Maximum power level MJ (deployed or available)
|
||||||
|
* @param {number} available Available power MJ
|
||||||
|
*/
|
||||||
|
_updateScales(innerWidth, maxPwr, available) {
|
||||||
|
this.wattScale.range([0, innerWidth]).domain([0, maxPwr]).clamp(true);
|
||||||
|
this.pctScale.range([0, innerWidth]).domain([0, maxPwr / available]).clamp(true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update state based on property and context changes
|
* Update state based on property and context changes
|
||||||
* @param {Object} nextProps Incoming/Next properties
|
* @param {Object} nextProps Incoming/Next properties
|
||||||
* @param {Object} nextContext Incoming/Next context
|
* @param {Object} nextContext Incoming/Next context
|
||||||
*/
|
*/
|
||||||
componentWillReceiveProps(nextProps, nextContext) {
|
componentWillReceiveProps(nextProps, nextContext) {
|
||||||
|
let { innerWidth, maxPwr } = this.state;
|
||||||
let { language, sizeRatio } = this.context;
|
let { language, sizeRatio } = this.context;
|
||||||
|
let maxBand = nextProps.bands[nextProps.bands.length - 1];
|
||||||
|
let nextMaxPwr = Math.max(nextProps.available, maxBand.retractedSum, maxBand.deployedSum);
|
||||||
|
|
||||||
if (language !== nextContext.language) {
|
if (language !== nextContext.language) {
|
||||||
this.wattAxis.tickFormat(nextContext.language.formats.r2);
|
this.wattAxis.tickFormat(nextContext.language.formats.r2);
|
||||||
this.pctAxis.tickFormat(nextContext.language.formats.rPct);
|
this.pctAxis.tickFormat(nextContext.language.formats.rPct);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextProps.width != this.props.width || sizeRatio != nextContext.sizeRatio) {
|
if (maxPwr != nextMaxPwr) { // Update Axes if max power has changed
|
||||||
|
this._updateScales(innerWidth, nextMaxPwr, nextProps.available);
|
||||||
|
this.setState({ maxPwr: nextMaxPwr });
|
||||||
|
} else if (nextProps.width != this.props.width || sizeRatio != nextContext.sizeRatio) {
|
||||||
this._updateDimensions(nextProps, nextContext.sizeRatio);
|
this._updateDimensions(nextProps, nextContext.sizeRatio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Assemble bands for relative consumption array.
|
|
||||||
* @param {Number[]} consumed Array of relative-consumption numbers
|
|
||||||
* @param {object} selected Object mapping selected bands to 1
|
|
||||||
* @param {Number} yOffset Offset in y-direction of the bar
|
|
||||||
* @param {Function} onClick onClick callback
|
|
||||||
* @returns {React.Component} Bands
|
|
||||||
*/
|
|
||||||
_consumedToBands(consumed, selected, yOffset, onClick) {
|
|
||||||
const { state, wattScale } = this;
|
|
||||||
const bands = [];
|
|
||||||
let consumesPrev = 0;
|
|
||||||
for (let i = 0; i < consumed.length; i++) {
|
|
||||||
consumesPrev = consumed[i - 1] || consumesPrev;
|
|
||||||
const consumes = consumed[i];
|
|
||||||
|
|
||||||
if (!consumes) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bands.push(<rect
|
|
||||||
key={'b' + i}
|
|
||||||
width={Math.ceil(Math.max(wattScale(consumes - consumesPrev), 0))}
|
|
||||||
height={state.barHeight}
|
|
||||||
x={wattScale(consumesPrev)}
|
|
||||||
y={yOffset + 1}
|
|
||||||
onClick={onClick.bind(this, i)}
|
|
||||||
className={getClass(selected[i], consumes)}
|
|
||||||
/>);
|
|
||||||
|
|
||||||
bands.push(<text
|
|
||||||
key={'t' + i}
|
|
||||||
dy='0.5em'
|
|
||||||
textAnchor='middle'
|
|
||||||
height={state.barHeight}
|
|
||||||
x={wattScale(consumesPrev) + (wattScale(consumes - consumesPrev) / 2)}
|
|
||||||
y={yOffset + (state.barHeight / 2)}
|
|
||||||
onClick={onClick.bind(this, i)}
|
|
||||||
className='primary-bg'>{i + 1}</text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return bands;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the power bands
|
* Render the power bands
|
||||||
* @return {React.Component} Power bands
|
* @return {React.Component} Power bands
|
||||||
@@ -201,27 +184,78 @@ export default class PowerBands extends TranslatedComponent {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { pctScale, context, props, state } = this;
|
let { wattScale, pctScale, context, props, state } = this;
|
||||||
let { translate, formats } = context.language;
|
let { translate, formats } = context.language;
|
||||||
let { f2, pct1 } = formats; // wattFmt, pctFmt
|
let { f2, pct1 } = formats; // wattFmt, pctFmt
|
||||||
let { ship } = props;
|
let { available, bands } = props;
|
||||||
let { innerWidth, ret, dep, barHeight } = state;
|
let { innerWidth, ret, dep } = state;
|
||||||
|
let pwrWarningClass = cn('threshold', { exceeded: bands[0].retractedSum > available * 0.4 });
|
||||||
let {
|
let deployed = [];
|
||||||
consumed, generated, relativeConsumed, relativeConsumedRetracted
|
let retracted = [];
|
||||||
} = ship.getMetrics(POWER_METRICS);
|
|
||||||
let maxPwr = Math.max(consumed, generated);
|
|
||||||
let retSum = relativeConsumedRetracted[relativeConsumedRetracted.length - 1];
|
|
||||||
let depSum = relativeConsumed[relativeConsumed.length - 1];
|
|
||||||
|
|
||||||
this.wattScale.range([0, innerWidth]).domain([0, 1]).clamp(true);
|
|
||||||
this.pctScale.range([0, innerWidth]).domain([0, maxPwr / generated]).clamp(true);
|
|
||||||
|
|
||||||
let pwrWarningClass = cn('threshold', { exceeded: retSum > generated * 0.4 });
|
|
||||||
let retracted = this._consumedToBands(relativeConsumedRetracted, ret, 0, this._selectRet);
|
|
||||||
let deployed = this._consumedToBands(relativeConsumed, dep, barHeight, this._selectDep);
|
|
||||||
let retSelected = Object.keys(ret).length > 0;
|
let retSelected = Object.keys(ret).length > 0;
|
||||||
let depSelected = Object.keys(dep).length > 0;
|
let depSelected = Object.keys(dep).length > 0;
|
||||||
|
let retSum = 0;
|
||||||
|
let depSum = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < bands.length; i++) {
|
||||||
|
let b = bands[i];
|
||||||
|
retSum += (!retSelected || ret[i]) ? b.retracted : 0;
|
||||||
|
depSum += (!depSelected || dep[i]) ? b.deployed + b.retracted : 0;
|
||||||
|
|
||||||
|
if (b.retracted > 0) {
|
||||||
|
let retLbl = bandText(b.retracted, i, wattScale);
|
||||||
|
|
||||||
|
retracted.push(<rect
|
||||||
|
key={'rB' + i}
|
||||||
|
width={Math.ceil(Math.max(wattScale(b.retracted), 0))}
|
||||||
|
height={state.barHeight}
|
||||||
|
x={Math.floor(Math.max(wattScale(b.retractedSum) - wattScale(b.retracted), 0))}
|
||||||
|
y={1}
|
||||||
|
onClick={this._selectRet.bind(this, i)}
|
||||||
|
className={getClass(ret[i], b.retractedSum, available)}
|
||||||
|
/>);
|
||||||
|
|
||||||
|
if (retLbl) {
|
||||||
|
retracted.push(<text
|
||||||
|
key={'rT' + i}
|
||||||
|
dy='0.5em'
|
||||||
|
textAnchor='middle'
|
||||||
|
height={state.barHeight}
|
||||||
|
x={wattScale(b.retractedSum) - (wattScale(b.retracted) / 2)}
|
||||||
|
y={state.retY}
|
||||||
|
onClick={this._selectRet.bind(this, i)}
|
||||||
|
className='primary-bg'>{retLbl}</text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.retracted > 0 || b.deployed > 0) {
|
||||||
|
let depLbl = bandText(b.deployed + b.retracted, i, wattScale);
|
||||||
|
|
||||||
|
deployed.push(<rect
|
||||||
|
key={'dB' + i}
|
||||||
|
width={Math.ceil(Math.max(wattScale(b.deployed + b.retracted), 0))}
|
||||||
|
height={state.barHeight}
|
||||||
|
x={Math.floor(Math.max(wattScale(b.deployedSum) - wattScale(b.retracted) - wattScale(b.deployed), 0))}
|
||||||
|
y={state.barHeight + 1}
|
||||||
|
onClick={this._selectDep.bind(this, i)}
|
||||||
|
className={getClass(dep[i], b.deployedSum, available)}
|
||||||
|
/>);
|
||||||
|
|
||||||
|
if (depLbl) {
|
||||||
|
deployed.push(<text
|
||||||
|
key={'dT' + i}
|
||||||
|
dy='0.5em'
|
||||||
|
textAnchor='middle'
|
||||||
|
height={state.barHeight}
|
||||||
|
x={wattScale(b.deployedSum) - ((wattScale(b.retracted) + wattScale(b.deployed)) / 2)}
|
||||||
|
y={state.depY}
|
||||||
|
onClick={this._selectDep.bind(this, i)}
|
||||||
|
className='primary-bg'>{depLbl}</text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg style={{ marginTop: '1em', width: '100%', height: state.height }} onContextMenu={wrapCtxMenu(this._selectNone)}>
|
<svg style={{ marginTop: '1em', width: '100%', height: state.height }} onContextMenu={wrapCtxMenu(this._selectNone)}>
|
||||||
@@ -237,8 +271,8 @@ export default class PowerBands extends TranslatedComponent {
|
|||||||
<line x1={pctScale(0.4)} x2={pctScale(0.4)} 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.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='-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, generated)}>{f2(Math.max(0, retSum * generated))} ({pct1(Math.max(0, retSum))})</text>
|
<text dy='0.5em' x={innerWidth + 5} y={state.retY} className={getClass(retSelected, retSum, available)}>{f2(Math.max(0, retSum))} ({pct1(Math.max(0, retSum / available))})</text>
|
||||||
<text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, generated)}>{f2(Math.max(0, depSum * generated))} ({pct1(Math.max(0, depSum))})</text>
|
<text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, available)}>{f2(Math.max(0, depSum))} ({pct1(Math.max(0, depSum / available))})</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,49 +3,24 @@ import PropTypes from 'prop-types';
|
|||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import PowerBands from './PowerBands';
|
import PowerBands from './PowerBands';
|
||||||
|
import { slotName, slotComparator } from '../utils/SlotFunctions';
|
||||||
import { Power, NoPower } from './SvgIcons';
|
import { Power, NoPower } from './SvgIcons';
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
import { Ship, Module } from 'ed-forge';
|
|
||||||
|
|
||||||
/**
|
const POWER = [
|
||||||
* Makes a comparison based on the order `false < undefined < true` (fut) and
|
null,
|
||||||
* maps it to `[-1, 0, 1]`.
|
null,
|
||||||
* @param {boolean} a Bool or undefined
|
<NoPower className='icon warning' />,
|
||||||
* @param {boolean} b Bool or undefined
|
<Power className='secondary-disabled' />
|
||||||
* @returns {number} Comparison
|
];
|
||||||
*/
|
|
||||||
function futComp(a, b) {
|
|
||||||
switch (a) {
|
|
||||||
case false: return (b === false ? 0 : 1);
|
|
||||||
// The next else-expression maps false to -1 and true to 1
|
|
||||||
case undefined: return (b === undefined ? 0 : 2 * Number(b) - 1);
|
|
||||||
case true: return (b === true ? 0 : -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the enabled-icon.
|
|
||||||
* @param {boolean} enabled Is the module enabled?
|
|
||||||
* @returns {React.Component} Enabled icon.
|
|
||||||
*/
|
|
||||||
function getPowerIcon(enabled) {
|
|
||||||
if (enabled === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (enabled) {
|
|
||||||
return <Power className='secondary-disabled' />;
|
|
||||||
} else {
|
|
||||||
return <NoPower className='icon warning' />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Power Management Section
|
* Power Management Section
|
||||||
*/
|
*/
|
||||||
export default class PowerManagement extends TranslatedComponent {
|
export default class PowerManagement extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
ship: PropTypes.instanceOf(Ship).isRequired,
|
ship: PropTypes.object.isRequired,
|
||||||
code: PropTypes.string.isRequired,
|
code: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,17 +29,19 @@ export default class PowerManagement extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
this._renderPowerRows = this._renderPowerRows.bind(this);
|
||||||
|
this._updateWidth = this._updateWidth.bind(this);
|
||||||
|
this._sort = this._sort.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
predicate: 'pwr',
|
predicate: 'pwr',
|
||||||
desc: true,
|
desc: false,
|
||||||
width: 0
|
width: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the sort order
|
* Set the sort order and sort
|
||||||
* @param {string} predicate Sort predicate
|
* @param {string} predicate Sort predicate
|
||||||
*/
|
*/
|
||||||
_sortOrder(predicate) {
|
_sortOrder(predicate) {
|
||||||
@@ -76,51 +53,50 @@ export default class PowerManagement extends TranslatedComponent {
|
|||||||
desc = true;
|
desc = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._sort(this.props.ship, predicate, desc);
|
||||||
this.setState({ predicate, desc });
|
this.setState({ predicate, desc });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorts the power list
|
* Sorts the power list
|
||||||
* @param {Module[]} modules Modules to sort
|
* @param {Ship} ship Ship instance
|
||||||
* @returns {Module[]} Sorted modules
|
* @param {string} predicate Sort predicate
|
||||||
|
* @param {Boolean} desc Sort order descending
|
||||||
*/
|
*/
|
||||||
_sortAndFilter(modules) {
|
_sort(ship, predicate, desc) {
|
||||||
modules = modules.filter((m) => m.get('powerdraw') >= 0);
|
let powerList = ship.powerList;
|
||||||
let { translate } = this.context.language;
|
let comp = slotComparator.bind(null, this.context.language.translate);
|
||||||
const { predicate, desc } = this.state;
|
|
||||||
let comp;
|
|
||||||
switch (predicate) {
|
switch (predicate) {
|
||||||
case 'n': comp = (a, b) => translate(a.readMeta('type')).localeCompare(
|
case 'n': comp = comp(null, desc); break;
|
||||||
translate(b.readMeta('type'))
|
case 't': comp = comp((a, b) => a.type.localeCompare(b.type), desc); break;
|
||||||
); break;
|
case 'pri': comp = comp((a, b) => a.priority - b.priority, desc); break;
|
||||||
// case 't': comp = comp((a, b) => a.type.localeCompare(b.type), desc); break;
|
case 'pwr': comp = comp((a, b) => a.m.getPowerUsage() - b.m.getPowerUsage(), desc); break;
|
||||||
case 'pri': comp = (a, b) => a.getPowerPriority() - b.getPowerPriority(); break;
|
case 'r': comp = comp((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b), desc); break;
|
||||||
case 'pwr': comp = (a, b) => a.get('powerdraw') - b.get('powerdraw'); break;
|
case 'd': comp = comp((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true), desc); break;
|
||||||
case 'r': comp = (a, b) => futComp(a.isPowered().retracted, b.isPowered().retracted); break;
|
|
||||||
case 'd': comp = (a, b) => futComp(a.isPowered().deployed, b.isPowered().deployed); break;
|
|
||||||
}
|
}
|
||||||
modules.sort(comp);
|
|
||||||
if (desc) {
|
powerList.sort(comp);
|
||||||
modules.reverse();
|
|
||||||
}
|
|
||||||
return modules;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a callback that changes the power priority for the given module
|
* Update slot priority
|
||||||
* based on the given delta.
|
* @param {Object} slot Slot model
|
||||||
* @param {Module} m Module to set the priority for
|
* @param {number} inc increment / decrement
|
||||||
* @param {Number} delta Delta to set
|
|
||||||
* @returns {Function} Callback
|
|
||||||
*/
|
*/
|
||||||
_prioCb(m, delta) {
|
_priority(slot, inc) {
|
||||||
return () => {
|
if (this.props.ship.setSlotPriority(slot, slot.priority + inc)) {
|
||||||
const prio = m.getPowerPriority();
|
this.props.onChange();
|
||||||
const newPrio = Math.max(0, prio + delta);
|
|
||||||
if (0 <= newPrio) {
|
|
||||||
m.setPowerPriority(newPrio);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle slot active/inactive
|
||||||
|
* @param {Object} slot Slot model
|
||||||
|
*/
|
||||||
|
_toggleEnabled(slot) {
|
||||||
|
this.props.ship.setSlotEnabled(slot, !slot.enabled);
|
||||||
|
this.props.onChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,36 +110,37 @@ export default class PowerManagement extends TranslatedComponent {
|
|||||||
_renderPowerRows(ship, translate, pwr, pct) {
|
_renderPowerRows(ship, translate, pwr, pct) {
|
||||||
let powerRows = [];
|
let powerRows = [];
|
||||||
|
|
||||||
let modules = this._sortAndFilter(ship.getModules());
|
for (let i = 0, l = ship.powerList.length; i < l; i++) {
|
||||||
for (let m of modules) {
|
let slot = ship.powerList[i];
|
||||||
|
|
||||||
|
if (slot.m && slot.m.getPowerUsage() > 0) {
|
||||||
|
let m = slot.m;
|
||||||
|
let toggleEnabled = this._toggleEnabled.bind(this, slot);
|
||||||
let retractedElem = null, deployedElem = null;
|
let retractedElem = null, deployedElem = null;
|
||||||
const flipEnabled = () => m.setEnabled();
|
|
||||||
if (m.isEnabled()) {
|
if (slot.enabled) {
|
||||||
let powered = m.isPowered();
|
retractedElem = <td className='ptr upp' onClick={toggleEnabled}>{POWER[ship.getSlotStatus(slot, false)]}</td>;
|
||||||
retractedElem = <td className='ptr upp' onClick={flipEnabled}>{getPowerIcon(powered.retracted)}</td>;
|
deployedElem = <td className='ptr upp' onClick={toggleEnabled}>{POWER[ship.getSlotStatus(slot, true)]}</td>;
|
||||||
deployedElem = <td className='ptr upp' onClick={flipEnabled}>{getPowerIcon(powered.deployed)}</td>;
|
|
||||||
} else {
|
} else {
|
||||||
retractedElem = <td className='ptr disabled upp' colSpan='2' onClick={flipEnabled}>{translate('disabled')}</td>;
|
retractedElem = <td className='ptr disabled upp' colSpan='2' onClick={toggleEnabled}>{translate('disabled')}</td>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const slot = m.getSlot();
|
powerRows.push(<tr key={i} className={cn('highlight', { disabled: !slot.enabled })}>
|
||||||
powerRows.push(<tr key={slot} className={cn('highlight', { disabled: !m.isEnabled() })}>
|
<td className='ptr' style={{ width: '1em' }} onClick={toggleEnabled}>{m.class + m.rating}</td>
|
||||||
<td className='ptr' style={{ width: '1em' }} onClick={flipEnabled}>{String(m.getClass()) + m.getRating()}</td>
|
<td className='ptr le shorten cap' onClick={toggleEnabled}>{slotName(translate, slot)}</td>
|
||||||
<td className='ptr le shorten cap' onClick={flipEnabled}>{translate(m.readMeta('type'))}</td>
|
<td className='ptr' onClick={toggleEnabled}><u>{translate(slot.type)}</u></td>
|
||||||
{/* <td className='ptr' onClick={flipEnabled}><u>{translate(slot.type)}</u></td> */}
|
|
||||||
<td>
|
<td>
|
||||||
<span className='flip ptr btn' onClick={this._prioCb(m, -1)}>►</span>
|
<span className='flip ptr btn' onClick={this._priority.bind(this, slot, -1)}>►</span>
|
||||||
{' ' + (m.getPowerPriority() + 1) + ' '}
|
{' ' + (slot.priority + 1) + ' '}
|
||||||
<span className='ptr btn' onClick={this._prioCb(m, 1)}>►</span>
|
<span className='ptr btn' onClick={this._priority.bind(this, slot, 1)}>►</span>
|
||||||
</td>
|
|
||||||
<td className='ri ptr' style={{ width: '3.25em' }} onClick={flipEnabled}>{pwr(m.get('powerdraw'))}</td>
|
|
||||||
<td className='ri ptr' style={{ width: '3em' }} onClick={flipEnabled}>
|
|
||||||
<u>{pct(m.get('powerdraw') / ship.getPowerPlant().get('powercapacity'))}</u>
|
|
||||||
</td>
|
</td>
|
||||||
|
<td className='ri ptr' style={{ width: '3.25em' }} onClick={toggleEnabled}>{pwr(m.getPowerUsage())}</td>
|
||||||
|
<td className='ri ptr' style={{ width: '3em' }} onClick={toggleEnabled}><u>{pct(m.getPowerUsage() / ship.powerAvailable)}</u></td>
|
||||||
{retractedElem}
|
{retractedElem}
|
||||||
{deployedElem}
|
{deployedElem}
|
||||||
</tr>);
|
</tr>);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return powerRows;
|
return powerRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +155,7 @@ export default class PowerManagement extends TranslatedComponent {
|
|||||||
* Add listeners when about to mount and sort power list
|
* Add listeners when about to mount and sort power list
|
||||||
*/
|
*/
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
|
this._sort(this.props.ship, this.state.predicate, this.state.desc);
|
||||||
this.resizeListener = this.context.onWindowResize(this._updateWidth);
|
this.resizeListener = this.context.onWindowResize(this._updateWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +166,17 @@ export default class PowerManagement extends TranslatedComponent {
|
|||||||
this._updateWidth();
|
this._updateWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort power list if the ship instance has changed
|
||||||
|
* @param {Object} nextProps Incoming/Next properties
|
||||||
|
* @param {Object} nextState Incoming/Next state
|
||||||
|
*/
|
||||||
|
componentWillUpdate(nextProps, nextState) {
|
||||||
|
if (this.props.ship != nextProps.ship) {
|
||||||
|
this._sort(nextProps.ship, nextState.predicate, nextState.desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove listeners on unmount
|
* Remove listeners on unmount
|
||||||
*/
|
*/
|
||||||
@@ -202,38 +191,39 @@ export default class PowerManagement extends TranslatedComponent {
|
|||||||
render() {
|
render() {
|
||||||
let { ship, code } = this.props;
|
let { ship, code } = this.props;
|
||||||
let { translate, formats } = this.context.language;
|
let { translate, formats } = this.context.language;
|
||||||
let pp = ship.getPowerPlant();
|
let pwr = formats.f2;
|
||||||
|
let pp = ship.standard[0].m;
|
||||||
|
let sortOrder = this._sortOrder;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={node => this.node = node} className='group half' id='componentPriority'>
|
<div ref={node => this.node = node} className='group half' id='componentPriority'>
|
||||||
<table style={{ width: '100%' }}>
|
<table style={{ width: '100%' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className='main'>
|
<tr className='main'>
|
||||||
<th colSpan='2' className='sortable le' onClick={() => this._sortOrder('n')} >{translate('module')}</th>
|
<th colSpan='2' className='sortable le' onClick={sortOrder.bind(this, 'n')} >{translate('module')}</th>
|
||||||
{/* <th style={{ width: '3em' }} className='sortable' onClick={() => this._sortOrder('t')} >{translate('type')}</th> */}
|
<th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 't')} >{translate('type')}</th>
|
||||||
<th style={{ width: '4em' }} className='sortable' onClick={() => this._sortOrder('pri')} >{translate('pri')}</th>
|
<th style={{ width: '4em' }} className='sortable' onClick={sortOrder.bind(this, 'pri')} >{translate('pri')}</th>
|
||||||
<th colSpan='2' className='sortable' onClick={() => this._sortOrder('pwr')} >{translate('PWR')}</th>
|
<th colSpan='2' className='sortable' onClick={sortOrder.bind(this, 'pwr')} >{translate('PWR')}</th>
|
||||||
<th style={{ width: '3em' }} className='sortable' onClick={() => this._sortOrder('r')} >{translate('ret')}</th>
|
<th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 'r')} >{translate('ret')}</th>
|
||||||
<th style={{ width: '3em' }} className='sortable' onClick={() => this._sortOrder('d')} >{translate('dep')}</th>
|
<th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 'd')} >{translate('dep')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{String(pp.getClass()) + pp.getRating()}</td>
|
<td>{pp.class + pp.rating}</td>
|
||||||
<td className='le shorten cap' >{translate('pp')}</td>
|
<td className='le shorten cap' >{translate('pp')}</td>
|
||||||
|
<td><u >{translate('SYS')}</u></td>
|
||||||
<td>1</td>
|
<td>1</td>
|
||||||
<td className='ri'>{formats.f2(pp.get('powercapacity'))}</td>
|
<td className='ri'>{pwr(pp.getPowerGeneration())}</td>
|
||||||
<td className='ri'><u>100%</u></td>
|
<td className='ri'><u>100%</u></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr><td style={{ lineHeight:0 }} colSpan='8'>
|
<tr><td style={{ lineHeight:0 }} colSpan='8'><hr style={{ margin: '0 0 3px', background: '#ff8c0d', border: 0, height: 1 }} /></td></tr>
|
||||||
<hr style={{ margin: '0 0 3px', background: '#ff8c0d', border: 0, height: 1 }} />
|
{this._renderPowerRows(ship, translate, pwr, formats.pct1)}
|
||||||
</td></tr>
|
|
||||||
{this._renderPowerRows(ship, translate, formats.f2, formats.pct1)}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<PowerBands width={this.state.width} ship={ship} code={code} />
|
<PowerBands width={this.state.width} code={code} available={pp.getPowerGeneration()} bands={ship.priorityBands} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
|
import { Ships } from 'coriolis-data/dist';
|
||||||
import { Rocket } from './SvgIcons';
|
import { Rocket } from './SvgIcons';
|
||||||
import Persist from '../stores/Persist';
|
import Persist from '../stores/Persist';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { Factory, Ship } from 'ed-forge';
|
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
import { isEqual } from 'lodash';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ship picker
|
* Ship picker
|
||||||
@@ -15,49 +13,40 @@ import { isEqual } from 'lodash';
|
|||||||
export default class ShipPicker extends TranslatedComponent {
|
export default class ShipPicker extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
ship: PropTypes.instanceOf(Ship).isRequired,
|
ship: PropTypes.string.isRequired,
|
||||||
|
build: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
ship: 'eagle'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* constructor
|
* constructor
|
||||||
* @param {object} props Properties react
|
* @param {object} props Properties react
|
||||||
* @param {object} context react context
|
* @param {object} context react context
|
||||||
*/
|
*/
|
||||||
constructor(props, context) {
|
constructor(props, context) { // eslint-disable-line
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
|
||||||
this.state = {
|
this.shipOrder = Object.keys(Ships).sort();
|
||||||
menuOpen: false,
|
this._toggleMenu = this._toggleMenu.bind(this);
|
||||||
opponent: {
|
this._closeMenu = this._closeMenu.bind(this);
|
||||||
self: true,
|
|
||||||
type: props.ship.getShipType(),
|
this.state = { menuOpen: false };
|
||||||
stock: false,
|
|
||||||
id: undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update ship
|
* Update ship
|
||||||
* @param {boolean} self True to compare with ship itself
|
* @param {object} ship the ship
|
||||||
* @param {object} type The ship type
|
* @param {string} build the build, if present
|
||||||
* @param {boolean} stock True to compare with a stock version of given type
|
|
||||||
* @param {string} id The build's stored ID
|
|
||||||
*/
|
*/
|
||||||
_shipChange(self, type, stock = false, id = null) {
|
_shipChange(ship, build) {
|
||||||
const opponent = { self, type, stock, id };
|
this._closeMenu();
|
||||||
if (isEqual(opponent, this.state.opponent)) {
|
|
||||||
this.setState({ menuOpen: false });
|
// Ensure that the ship has changed
|
||||||
} else {
|
if (ship !== this.props.ship || build !== this.props.build) {
|
||||||
const { onChange } = this.props;
|
this.props.onChange(ship, build);
|
||||||
if (self) {
|
|
||||||
onChange(this.props.ship);
|
|
||||||
} else if (stock) {
|
|
||||||
onChange(Factory.newShip(type));
|
|
||||||
} else {
|
|
||||||
onChange(new Ship(Persist.getBuild(type, id)));
|
|
||||||
}
|
|
||||||
this.setState({ menuOpen: false, opponent });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,41 +55,26 @@ export default class ShipPicker extends TranslatedComponent {
|
|||||||
* @returns {object} the picker menu
|
* @returns {object} the picker menu
|
||||||
*/
|
*/
|
||||||
_renderPickerMenu() {
|
_renderPickerMenu() {
|
||||||
const { menuOpen } = this.state;
|
const { ship, build } = this.props;
|
||||||
if (!menuOpen) {
|
const _shipChange = this._shipChange;
|
||||||
return null;
|
const builds = Persist.getBuilds();
|
||||||
|
const buildList = [];
|
||||||
|
for (let shipId of this.shipOrder) {
|
||||||
|
const shipBuilds = [];
|
||||||
|
// Add stock build
|
||||||
|
const stockSelected = (ship == shipId && !build);
|
||||||
|
shipBuilds.push(<li key={shipId} className={ cn({ 'selected': stockSelected })} onClick={_shipChange.bind(this, shipId, null)}>Stock</li>);
|
||||||
|
if (builds[shipId]) {
|
||||||
|
let buildNameOrder = Object.keys(builds[shipId]).sort();
|
||||||
|
for (let buildName of buildNameOrder) {
|
||||||
|
const buildSelected = ship === shipId && build === buildName;
|
||||||
|
shipBuilds.push(<li key={shipId + '-' + buildName} className={ cn({ 'selected': buildSelected })} onClick={_shipChange.bind(this, shipId, buildName)}>{buildName}</li>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildList.push(<ul key={shipId} className='block'>{Ships[shipId].properties.name}{shipBuilds}</ul>);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { translate } = this.context.language;
|
return buildList;
|
||||||
const { self, type, stock, id } = this.state.opponent;
|
|
||||||
return <div className='menu-list' onClick={(e) => e.stopPropagation()}>
|
|
||||||
<div className='quad'>
|
|
||||||
{Factory.getAllShipTypes().sort().map((shipType) =>
|
|
||||||
<ul key={shipType} className='block'>
|
|
||||||
{translate(shipType)}
|
|
||||||
{/* Add stock build */}
|
|
||||||
<li key={shipType}
|
|
||||||
onClick={this._shipChange.bind(this, false, shipType, true)}
|
|
||||||
className={cn({ selected: stock && type === shipType })}>
|
|
||||||
{translate('stock')}
|
|
||||||
</li>
|
|
||||||
{Persist.getBuildsNamesFor(shipType).sort().map((storedId) =>
|
|
||||||
<li key={`${shipType}-${storedId}`}
|
|
||||||
onClick={this._shipChange.bind(this, false, shipType, false, storedId)}
|
|
||||||
className={ cn({ selected: type === shipType && id === storedId })}>
|
|
||||||
{storedId}
|
|
||||||
</li>)}
|
|
||||||
{/* Add ship itself */}
|
|
||||||
{(this.props.ship.getShipType() === shipType ?
|
|
||||||
<li key='self'
|
|
||||||
onClick={this._shipChange.bind(this, true, shipType)}
|
|
||||||
className={cn({ selected: self })}>
|
|
||||||
{translate('THIS_SHIP')}
|
|
||||||
</li> :
|
|
||||||
null)}
|
|
||||||
</ul>)}
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,36 +85,41 @@ export default class ShipPicker extends TranslatedComponent {
|
|||||||
this.setState({ menuOpen: !menuOpen });
|
this.setState({ menuOpen: !menuOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the menu
|
||||||
|
*/
|
||||||
|
_closeMenu() {
|
||||||
|
const { menuOpen } = this.state;
|
||||||
|
if (menuOpen) {
|
||||||
|
this._toggleMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render picker
|
* Render picker
|
||||||
* @return {React.Component} contents
|
* @return {React.Component} contents
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { translate } = this.context.language;
|
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||||
const { ship } = this.props;
|
const { formats, translate, units } = language;
|
||||||
|
const { ship, build } = this.props;
|
||||||
const { menuOpen } = this.state;
|
const { menuOpen } = this.state;
|
||||||
const { self, type, stock, id } = this.state.opponent;
|
|
||||||
|
|
||||||
let label;
|
|
||||||
if (self) {
|
|
||||||
label = translate('THIS_SHIP');
|
|
||||||
} else if (stock) {
|
|
||||||
label = translate('stock');
|
|
||||||
} else {
|
|
||||||
label = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const shipString = ship + ': ' + (build ? build : translate('stock'));
|
||||||
return (
|
return (
|
||||||
<div className='shippicker' onClick={ (e) => e.stopPropagation() }>
|
<div className='shippicker' onClick={ (e) => e.stopPropagation() }>
|
||||||
<div className='menu'>
|
<div className='menu'>
|
||||||
<div className={cn('menu-header', { selected: menuOpen })} onClick={this._toggleMenu}>
|
<div className={cn('menu-header', { selected: menuOpen })} onClick={this._toggleMenu}>
|
||||||
<span><Rocket className='warning' /></span>
|
<span><Rocket className='warning' /></span>
|
||||||
<span className='menu-item-label'>
|
<span className='menu-item-label'>{shipString}</span>
|
||||||
{`${translate(type)}: ${label}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
{ menuOpen ?
|
||||||
|
<div className='menu-list' onClick={ (e) => e.stopPropagation() }>
|
||||||
|
<div className='quad'>
|
||||||
{this._renderPickerMenu()}
|
{this._renderPickerMenu()}
|
||||||
</div>
|
</div>
|
||||||
|
</div> : null }
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
import autoBind from 'auto-bind';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { Warning } from './SvgIcons';
|
import { Warning } from './SvgIcons';
|
||||||
|
import * as Calc from '../shipyard/Calculations';
|
||||||
import { ShipProps } from 'ed-forge';
|
|
||||||
import { BOOST_INTERVAL, MINIMUM_MASS } from 'ed-forge/lib/src/ship-stats';
|
|
||||||
const {
|
|
||||||
SPEED, BOOST_SPEED, DAMAGE_METRICS, JUMP_METRICS, SHIELD_METRICS,
|
|
||||||
ARMOUR_METRICS, CARGO_CAPACITY, FUEL_CAPACITY, UNLADEN_MASS, LADEN_MASS,
|
|
||||||
MODULE_PROTECTION_METRICS, PASSENGER_CAPACITY
|
|
||||||
} = ShipProps;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ship Summary Table / Stats
|
* Ship Summary Table / Stats
|
||||||
*/
|
*/
|
||||||
export default class ShipSummaryTable extends TranslatedComponent {
|
export default class ShipSummaryTable extends TranslatedComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
ship: PropTypes.object.isRequired,
|
ship: PropTypes.object.isRequired,
|
||||||
code: PropTypes.string.isRequired,
|
cargo: PropTypes.number.isRequired,
|
||||||
|
fuel: PropTypes.number.isRequired,
|
||||||
|
marker: PropTypes.string.isRequired,
|
||||||
|
pips: PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +24,7 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
this.didContextChange = this.didContextChange.bind(this);
|
||||||
this.state = {
|
this.state = {
|
||||||
shieldColour: 'blue'
|
shieldColour: 'blue'
|
||||||
};
|
};
|
||||||
@@ -39,53 +35,43 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
|||||||
* @return {React.Component} Summary table
|
* @return {React.Component} Summary table
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { ship } = this.props;
|
const { ship, cargo, fuel, pips } = this.props;
|
||||||
let { language, tooltip, termtip } = this.context;
|
let { language, tooltip, termtip } = this.context;
|
||||||
let translate = language.translate;
|
let translate = language.translate;
|
||||||
let u = language.units;
|
let u = language.units;
|
||||||
let formats = language.formats;
|
let formats = language.formats;
|
||||||
let { time, int, f1, f2 } = formats;
|
let { time, int, round, f1, f2 } = formats;
|
||||||
let hide = tooltip.bind(null, null);
|
let hide = tooltip.bind(null, null);
|
||||||
|
const shieldGenerator = ship.findInternalByGroup('sg') || ship.findInternalByGroup('psg');
|
||||||
const speed = ship.get(SPEED);
|
const sgClassNames = cn({ warning: shieldGenerator && !ship.shield, muted: !shieldGenerator });
|
||||||
const shipBoost = ship.get(BOOST_SPEED);
|
|
||||||
const boostInterval = ship.get(BOOST_INTERVAL);
|
|
||||||
const canThrust = 0 < speed;
|
|
||||||
const canBoost = canThrust && !isNaN(shipBoost);
|
|
||||||
const speedTooltip = canThrust ? 'TT_SUMMARY_SPEED' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
|
|
||||||
const boostTooltip = canBoost ? 'TT_SUMMARY_BOOST' : canThrust ? 'TT_SUMMARY_BOOST_NONFUNCTIONAL' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
|
|
||||||
|
|
||||||
const sgMetrics = ship.get(SHIELD_METRICS);
|
|
||||||
const armourMetrics = ship.get(ARMOUR_METRICS);
|
|
||||||
const damageMetrics = ship.get(DAMAGE_METRICS);
|
|
||||||
const moduleProtectionMetrics = ship.get(MODULE_PROTECTION_METRICS);
|
|
||||||
const timeToDrain = damageMetrics.timeToDrain;
|
|
||||||
|
|
||||||
const shieldGenerator = ship.getShieldGenerator();
|
|
||||||
const sgClassNames = cn({
|
|
||||||
warning: shieldGenerator && !shieldGenerator.isEnabled(),
|
|
||||||
muted: !shieldGenerator,
|
|
||||||
});
|
|
||||||
const sgTooltip = shieldGenerator ? 'TT_SUMMARY_SHIELDS' : 'TT_SUMMARY_SHIELDS_NONFUNCTIONAL';
|
const sgTooltip = shieldGenerator ? 'TT_SUMMARY_SHIELDS' : 'TT_SUMMARY_SHIELDS_NONFUNCTIONAL';
|
||||||
const sgType = shieldGenerator ? shieldGenerator.readMeta('type') : undefined;
|
const timeToDrain = Calc.timeToDrainWep(ship, 4);
|
||||||
|
const canThrust = ship.canThrust(cargo, ship.fuelCapacity);
|
||||||
|
const speedTooltip = canThrust ? 'TT_SUMMARY_SPEED' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
|
||||||
|
const canBoost = ship.canBoost(cargo, ship.fuelCapacity);
|
||||||
|
const boostTooltip = canBoost ? 'TT_SUMMARY_BOOST' : canThrust ? 'TT_SUMMARY_BOOST_NONFUNCTIONAL' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
|
||||||
|
const sgMetrics = Calc.shieldMetrics(ship, pips.sys);
|
||||||
|
const shipBoost = canBoost ? Calc.calcBoost(ship) : 'No Boost';
|
||||||
|
const restingHeat = Math.sqrt(((ship.standard[0].m.pgen * ship.standard[0].m.eff) / ship.heatCapacity) / 0.2);
|
||||||
|
const armourMetrics = Calc.armourMetrics(ship);
|
||||||
let shieldColour = 'blue';
|
let shieldColour = 'blue';
|
||||||
switch (sgType) {
|
if (shieldGenerator && shieldGenerator.m.grp === 'psg') {
|
||||||
case 'biweaveshieldgen': shieldColour = 'purple'; break;
|
shieldColour = 'green';
|
||||||
case 'prismaticshieldgen': shieldColour = 'green'; break;
|
} else if (shieldGenerator && shieldGenerator.m.grp === 'bsg') {
|
||||||
|
shieldColour = 'purple';
|
||||||
}
|
}
|
||||||
this.state = { shieldColour };
|
this.state = {
|
||||||
|
shieldColour
|
||||||
const jumpRangeMetrics = ship.getMetrics(JUMP_METRICS);
|
};
|
||||||
|
|
||||||
return <div id='summary'>
|
return <div id='summary'>
|
||||||
<div style={{ display: 'table', width: '100%' }}>
|
<div style={{display: "table", width: "100%"}}>
|
||||||
<div style={{ display: 'table-row' }}>
|
<div style={{display: "table-row"}}>
|
||||||
<table className={'summaryTable'}>
|
<table className={'summaryTable'}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className='main'>
|
<tr className='main'>
|
||||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': speed == 0 }) }>{translate('speed')}</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 rowSpan={2} className={ cn({ 'bg-warning-disabled': !canBoost }) }>{translate('boost')}</th>
|
||||||
<th colSpan={5} className={ cn({ 'bg-warning-disabled': jumpRangeMetrics.jumpRangeCurrent == 0 }) }>{translate('jump range')}</th>
|
<th colSpan={5}>{translate('jump range')}</th>
|
||||||
<th rowSpan={2}>{translate('shield')}</th>
|
<th rowSpan={2}>{translate('shield')}</th>
|
||||||
<th rowSpan={2}>{translate('integrity')}</th>
|
<th rowSpan={2}>{translate('integrity')}</th>
|
||||||
<th rowSpan={2}>{translate('DPS')}</th>
|
<th rowSpan={2}>{translate('DPS')}</th>
|
||||||
@@ -99,12 +85,11 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
|||||||
<th onMouseEnter={termtip.bind(null, 'hull hardness', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('hrd')}</th>
|
<th onMouseEnter={termtip.bind(null, 'hull hardness', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('hrd')}</th>
|
||||||
<th rowSpan={2}>{translate('crew')}</th>
|
<th rowSpan={2}>{translate('crew')}</th>
|
||||||
<th onMouseEnter={termtip.bind(null, 'mass lock factor', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('MLF')}</th>
|
<th onMouseEnter={termtip.bind(null, 'mass lock factor', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('MLF')}</th>
|
||||||
<th onMouseEnter={termtip.bind(null, 'TT_SUMMARY_BOOST_INTERVAL', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('boost interval')}</th>
|
<th onMouseEnter={termtip.bind(null, 'TT_SUMMARY_BOOST_TIME', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('boost time')}</th>
|
||||||
{/* TODO: Resting heat */}
|
<th rowSpan={2}>{translate('resting heat (Beta)')}</th>
|
||||||
{/* <th rowSpan={2}>{translate('resting heat (Beta)')}</th> */}
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="lft">{translate('max')}</th>
|
<th className='lft'>{translate('max')}</th>
|
||||||
<th>{translate('unladen')}</th>
|
<th>{translate('unladen')}</th>
|
||||||
<th>{translate('laden')}</th>
|
<th>{translate('laden')}</th>
|
||||||
<th>{translate('total unladen')}</th>
|
<th>{translate('total unladen')}</th>
|
||||||
@@ -116,68 +101,30 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td onMouseEnter={termtip.bind(null, speedTooltip, { cap: 0 })}
|
<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>
|
||||||
onMouseLeave={hide}
|
<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>
|
||||||
>{canThrust ?
|
<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(), ship))}{u.LY}</span></td>
|
||||||
<span>{int(speed)}{u['m/s']}</span> :
|
<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, ship))}{u.LY}</span></td>
|
||||||
<span className='warning'>0<Warning/></span>
|
<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, ship))}{u.LY}</span></td>
|
||||||
}</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, ship))}{u.LY}</td>
|
||||||
<td onMouseEnter={termtip.bind(null, boostTooltip, { cap: 0 })}
|
<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, ship))}{u.LY}</td>
|
||||||
onMouseLeave={hide}
|
<td className={sgClassNames} onMouseEnter={termtip.bind(null, sgTooltip, { cap: 0 })} onMouseLeave={hide}>{int(ship.shield)}{u.MJ}</td>
|
||||||
>{canBoost ?
|
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_INTEGRITY', { cap: 0 })} onMouseLeave={hide}>{int(ship.armour)}</td>
|
||||||
<span>{int(shipBoost)}{u['m/s']}</span> :
|
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_DPS', { cap: 0 })} onMouseLeave={hide}>{f1(ship.totalDps)}</td>
|
||||||
<span className='warning'>0<Warning/></span>
|
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_EPS', { cap: 0 })} onMouseLeave={hide}>{f1(ship.totalEps)}</td>
|
||||||
}</td>
|
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_TTD', { cap: 0 })} onMouseLeave={hide}>{timeToDrain === Infinity ? '∞' : time(timeToDrain)}</td>
|
||||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_MAX_SINGLE_JUMP', { cap: 0 })}
|
|
||||||
onMouseLeave={hide}
|
|
||||||
>{<span>{f2(jumpRangeMetrics.jumpRangeMax)}{u.LY}</span>}</td>
|
|
||||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_SINGLE_JUMP', { cap: 0 })}
|
|
||||||
onMouseLeave={hide}
|
|
||||||
>{<span>{f2(jumpRangeMetrics.jumpRangeUnladen)}{u.LY}</span>}</td>
|
|
||||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_SINGLE_JUMP', { cap: 0 })}
|
|
||||||
onMouseLeave={hide}
|
|
||||||
>{<span>{f2(jumpRangeMetrics.jumpRangeLaden)}{u.LY}</span>}</td>
|
|
||||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_TOTAL_JUMP', { cap: 0 })}
|
|
||||||
onMouseLeave={hide}
|
|
||||||
>{<span>{f2(jumpRangeMetrics.totalRangeUnladen)}{u.LY}</span>}</td>
|
|
||||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_TOTAL_JUMP', { cap: 0 })}
|
|
||||||
onMouseLeave={hide}
|
|
||||||
>{<span>{f2(jumpRangeMetrics.totalRangeLaden)}{u.LY}</span>}</td>
|
|
||||||
<td className={sgClassNames}
|
|
||||||
onMouseEnter={termtip.bind(null, sgTooltip, { cap: 0 })}
|
|
||||||
onMouseLeave={hide}
|
|
||||||
>{int(sgMetrics.shieldStrength)}{u.MJ}</td>
|
|
||||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_INTEGRITY', { cap: 0 })}
|
|
||||||
onMouseLeave={hide}
|
|
||||||
>{int(armourMetrics.armour)}</td>
|
|
||||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_DPS', { cap: 0 })}
|
|
||||||
onMouseLeave={hide}
|
|
||||||
>{f1(damageMetrics.dps)}</td>
|
|
||||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_EPS', { cap: 0 })}
|
|
||||||
onMouseLeave={hide}
|
|
||||||
>{f1(damageMetrics.eps)}</td>
|
|
||||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_TTD', { cap: 0 })}
|
|
||||||
onMouseLeave={hide}
|
|
||||||
>{timeToDrain === Infinity ? '∞' : time(timeToDrain)}</td>
|
|
||||||
{/* <td>{f1(ship.totalHps)}</td> */}
|
{/* <td>{f1(ship.totalHps)}</td> */}
|
||||||
<td>{ship.get(CARGO_CAPACITY)}{u.T}</td>
|
<td>{round(ship.cargoCapacity)}{u.T}</td>
|
||||||
<td>{ship.get(PASSENGER_CAPACITY)}</td>
|
<td>{ship.passengerCapacity}</td>
|
||||||
<td>{ship.get(FUEL_CAPACITY)}{u.T}</td>
|
<td>{round(ship.fuelCapacity)}{u.T}</td>
|
||||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_HULL_MASS', { cap: 0 })}
|
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_HULL_MASS', { cap: 0 })} onMouseLeave={hide}>{ship.hullMass}{u.T}</td>
|
||||||
onMouseLeave={hide}
|
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_MASS', { cap: 0 })} onMouseLeave={hide}>{int(ship.unladenMass)}{u.T}</td>
|
||||||
>{ship.readProp('hullmass')}{u.T}</td>
|
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_MASS', { cap: 0 })} onMouseLeave={hide}>{int(ship.ladenMass)}{u.T}</td>
|
||||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_MASS', { cap: 0 })}
|
<td>{int(ship.hardness)}</td>
|
||||||
onMouseLeave={hide}
|
<td>{ship.crew}</td>
|
||||||
>{int(ship.get(MINIMUM_MASS))}{u.T}</td>
|
<td>{ship.masslock}</td>
|
||||||
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_MASS', { cap: 0 })}
|
<td>{shipBoost !== 'No Boost' ? formats.time(shipBoost) : 'No Boost'}</td>
|
||||||
onMouseLeave={hide}
|
<td>{formats.pct(restingHeat)}</td>
|
||||||
>{int(ship.get(LADEN_MASS))}{u.T}</td>
|
|
||||||
<td>{int(ship.readProp('hardness'))}</td>
|
|
||||||
<td>{ship.readMeta('crew')}</td>
|
|
||||||
<td>{ship.readProp('masslock')}</td>
|
|
||||||
<td>{time(boostInterval)}</td>
|
|
||||||
{/* TODO: resting heat */}
|
|
||||||
{/* <td>{NaN}</td> */}
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -206,19 +153,19 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{translate(sgType || 'No Shield')}</td>
|
<td>{translate(shieldGenerator && shieldGenerator.m.grp || 'No Shield')}</td>
|
||||||
<td>{formats.pct1(1 - sgMetrics.explosive.damageMultiplier)}</td>
|
<td>{formats.pct1(ship.shieldExplRes)}</td>
|
||||||
<td>{formats.pct1(1 - sgMetrics.kinetic.damageMultiplier)}</td>
|
<td>{formats.pct1(ship.shieldKinRes)}</td>
|
||||||
<td>{formats.pct1(1 - sgMetrics.thermal.damageMultiplier)}</td>
|
<td>{formats.pct1(ship.shieldThermRes)}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
|
|
||||||
<td>{int(sgMetrics.shieldStrength || 0)}{u.MJ}</td>
|
<td>{int(ship && ship.shield > 0 ? ship.shield : 0)}{u.MJ}</td>
|
||||||
<td>{int(sgMetrics.shieldStrength / sgMetrics.explosive.damageMultiplier || 0)}{u.MJ}</td>
|
<td>{int(ship && ship.shield > 0 ? ship.shield * ((1 / (1 - (ship.shieldExplRes)))) : 0)}{u.MJ}</td>
|
||||||
<td>{int(sgMetrics.shieldStrength / sgMetrics.kinetic.damageMultiplier || 0)}{u.MJ}</td>
|
<td>{int(ship && ship.shield > 0 ? ship.shield * ((1 / (1 - (ship.shieldKinRes)))) : 0)}{u.MJ}</td>
|
||||||
<td>{int(sgMetrics.shieldStrength / sgMetrics.thermal.damageMultiplier || 0)}{u.MJ}</td>
|
<td>{int(ship && ship.shield > 0 ? ship.shield * ((1 / (1 - (ship.shieldThermRes)))) : 0)}{u.MJ}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>{isNaN(sgMetrics.recover) ? translate('Never') : formats.time(sgMetrics.recover)}</td>
|
<td>{sgMetrics && sgMetrics.recover === Math.Inf ? translate('Never') : formats.time(sgMetrics.recover)}</td>
|
||||||
<td>{isNaN(sgMetrics.recharge) ? translate('Never') : formats.time(sgMetrics.recharge)}</td>
|
<td>{sgMetrics && sgMetrics.recharge === Math.Inf ? translate('Never') : formats.time(sgMetrics.recharge)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -245,18 +192,19 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{translate(ship.getAlloys().readMeta('type') || 'No Armour')}</td>
|
<td>{translate(ship && ship.bulkheads && ship.bulkheads.m && ship.bulkheads.m.name || 'No Armour')}</td>
|
||||||
<td>{formats.pct1(1 - armourMetrics.explosive.damageMultiplier)}</td>
|
<td>{formats.pct1(ship.hullExplRes)}</td>
|
||||||
<td>{formats.pct1(1 - armourMetrics.kinetic.damageMultiplier)}</td>
|
<td>{formats.pct1(ship.hullKinRes)}</td>
|
||||||
<td>{formats.pct1(1 - armourMetrics.thermal.damageMultiplier)}</td>
|
<td>{formats.pct1(ship.hullThermRes)}</td>
|
||||||
<td>{formats.pct1(1 - armourMetrics.caustic.damageMultiplier)}</td>
|
<td>{formats.pct1(ship.hullCausRes)}</td>
|
||||||
<td>{int(armourMetrics.armour)}</td>
|
<td>{int(ship.armour)}</td>
|
||||||
<td>{int(armourMetrics.armour / armourMetrics.explosive.damageMultiplier)}</td>
|
<td>{int(ship.armour * ((1 / (1 - (ship.hullExplRes)))))}</td>
|
||||||
<td>{int(armourMetrics.armour / armourMetrics.kinetic.damageMultiplier)}</td>
|
<td>{int(ship.armour * ((1 / (1 - (ship.hullKinRes)))))}</td>
|
||||||
<td>{int(armourMetrics.armour / armourMetrics.thermal.damageMultiplier)}</td>
|
<td>{int(ship.armour * ((1 / (1 - (ship.hullThermRes)))))}</td>
|
||||||
<td>{int(armourMetrics.armour / armourMetrics.caustic.damageMultiplier)}</td>
|
<td>{int(ship.armour * ((1 / (1 - (ship.hullCausRes)))))}</td>
|
||||||
<td>{int(moduleProtectionMetrics.moduleArmour)}</td>
|
<td>{int(armourMetrics.modulearmour)}</td>
|
||||||
<td>{formats.pct1(1 - moduleProtectionMetrics.moduleProtection)}</td>
|
<td>{int(armourMetrics.moduleprotection * 100) + '%'}</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
|
|
||||||
const MARGIN_LR = 8; // Left/ Right margin
|
const MARGIN_LR = 8; // Left/ Right margin
|
||||||
|
|
||||||
@@ -8,6 +7,7 @@ const MARGIN_LR = 8; // Left/ Right margin
|
|||||||
* Horizontal Slider
|
* Horizontal Slider
|
||||||
*/
|
*/
|
||||||
export default class Slider extends React.Component {
|
export default class Slider extends React.Component {
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
axis: false,
|
axis: false,
|
||||||
min: 0,
|
min: 0,
|
||||||
@@ -32,7 +32,16 @@ export default class Slider extends React.Component {
|
|||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
this._down = this._down.bind(this);
|
||||||
|
this._move = this._move.bind(this);
|
||||||
|
this._up = this._up.bind(this);
|
||||||
|
this._keyup = this._keyup.bind(this);
|
||||||
|
this._keydown = this._keydown.bind(this);
|
||||||
|
this._touchstart = this._touchstart.bind(this);
|
||||||
|
this._touchend = this._touchend.bind(this);
|
||||||
|
|
||||||
|
this._updatePercent = this._updatePercent.bind(this);
|
||||||
|
this._updateDimensions = this._updateDimensions.bind(this);
|
||||||
|
|
||||||
this.state = { width: 0 };
|
this.state = { width: 0 };
|
||||||
}
|
}
|
||||||
@@ -46,6 +55,7 @@ export default class Slider extends React.Component {
|
|||||||
this.left = rect.left;
|
this.left = rect.left;
|
||||||
this.width = rect.width;
|
this.width = rect.width;
|
||||||
this._move(event);
|
this._move(event);
|
||||||
|
this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,11 +75,70 @@ export default class Slider extends React.Component {
|
|||||||
* @param {Event} event DOM Event
|
* @param {Event} event DOM Event
|
||||||
*/
|
*/
|
||||||
_up(event) {
|
_up(event) {
|
||||||
|
this.sliderInputBox.sliderVal.focus();
|
||||||
|
clearTimeout(this.touchStartTimer);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.left = null;
|
this.left = null;
|
||||||
this.width = null;
|
this.width = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key up handler for keyboard.
|
||||||
|
* display the number field then set focus to it
|
||||||
|
* when "Enter" key is pressed
|
||||||
|
* @param {Event} event Keyboard event
|
||||||
|
*/
|
||||||
|
_keyup(event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Enter':
|
||||||
|
event.preventDefault();
|
||||||
|
this.sliderInputBox._setDisplay('block');
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Key down handler
|
||||||
|
* increment slider position by +/- 1 when right/left arrow key is pressed or held
|
||||||
|
* @param {Event} event Keyboard even
|
||||||
|
*/
|
||||||
|
_keydown(event) {
|
||||||
|
let newVal = this.props.percent * this.props.max;
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowRight':
|
||||||
|
newVal += 1;
|
||||||
|
if (newVal <= this.props.max) this.props.onChange(newVal / this.props.max);
|
||||||
|
return;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
newVal -= 1;
|
||||||
|
if (newVal >= 0) this.props.onChange(newVal / this.props.max);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Touch start handler
|
||||||
|
* @param {Event} event DOM Event
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
_touchstart(event) {
|
||||||
|
this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Touch end handler
|
||||||
|
* @param {Event} event DOM Event
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
_touchend(event) {
|
||||||
|
this.sliderInputBox.sliderVal.focus();
|
||||||
|
clearTimeout(this.touchStartTimer);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the user is still dragging
|
* Determine if the user is still dragging
|
||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
@@ -144,7 +213,7 @@ export default class Slider extends React.Component {
|
|||||||
let width = outerWidth - (margin * 2);
|
let width = outerWidth - (margin * 2);
|
||||||
let pctPos = width * this.props.percent;
|
let pctPos = width * this.props.percent;
|
||||||
return <div><svg
|
return <div><svg
|
||||||
onMouseUp={this._up} onMouseEnter={this._enter.bind(this)} onMouseMove={this._move} style={style} ref={node => this.node = node} tabIndex="0">
|
onMouseUp={this._up} onMouseEnter={this._enter.bind(this)} onMouseMove={this._move} onKeyUp={this._keyup} onKeyDown={this._keydown} style={style} ref={node => this.node = node} tabIndex="0">
|
||||||
<rect className='primary' style={{ opacity: 0.3 }} x={margin} y='0.25em' rx='0.3em' ry='0.3em' width={width} height='0.7em' />
|
<rect className='primary' style={{ opacity: 0.3 }} x={margin} y='0.25em' rx='0.3em' ry='0.3em' width={width} height='0.7em' />
|
||||||
<rect className='primary-disabled' x={margin} y='0.45em' rx='0.15em' ry='0.15em' width={pctPos} height='0.3em' />
|
<rect className='primary-disabled' x={margin} y='0.45em' rx='0.15em' ry='0.15em' width={pctPos} height='0.3em' />
|
||||||
<circle className='primary' r={margin} cy='0.6em' cx={pctPos + margin} />
|
<circle className='primary' r={margin} cy='0.6em' cx={pctPos + margin} />
|
||||||
@@ -155,6 +224,163 @@ export default class Slider extends React.Component {
|
|||||||
<text className='primary-disabled' y='3em' x='100%' style={{ textAnchor: 'end' }}>{max + axisUnit}</text>
|
<text className='primary-disabled' y='3em' x='100%' style={{ textAnchor: 'end' }}>{max + axisUnit}</text>
|
||||||
</g>}
|
</g>}
|
||||||
</svg>
|
</svg>
|
||||||
|
<TextInputBox ref={(tb) => this.sliderInputBox = tb}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
percent={this.props.percent}
|
||||||
|
axisUnit={this.props.axisUnit}
|
||||||
|
scale={this.props.scale}
|
||||||
|
max={this.props.max}
|
||||||
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* New component to add keyboard support for sliders - works on all devices (desktop, iOS, Android)
|
||||||
|
**/
|
||||||
|
class TextInputBox extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
axisUnit: PropTypes.string,// units (T, M, etc.)
|
||||||
|
max: PropTypes.number,
|
||||||
|
onChange: PropTypes.func.isRequired,// function which determins percent value
|
||||||
|
percent: PropTypes.number.isRequired,// value of slider
|
||||||
|
scale: PropTypes.number
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Determine if the user is still dragging
|
||||||
|
* @param {Object} props React Component properties
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this._handleFocus = this._handleFocus.bind(this);
|
||||||
|
this._handleBlur = this._handleBlur.bind(this);
|
||||||
|
this._handleChange = this._handleChange.bind(this);
|
||||||
|
this._keyup = this._keyup.bind(this);
|
||||||
|
this.state = this._getInitialState();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Update input value if slider changes will change props/state
|
||||||
|
* @param {Object} nextProps React Component properites
|
||||||
|
* @param {Object} nextState React Component state values
|
||||||
|
*/
|
||||||
|
componentWillReceiveProps(nextProps, nextState) {
|
||||||
|
let nextValue = nextProps.percent * nextProps.max;
|
||||||
|
// See https://stackoverflow.com/questions/32414308/updating-state-on-props-change-in-react-form
|
||||||
|
if (nextValue !== this.state.inputValue && nextValue <= nextProps.max) {
|
||||||
|
this.setState({ inputValue: nextValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Update slider textbox visibility/values if changes are made to slider
|
||||||
|
* @param {Object} prevProps React Component properites
|
||||||
|
* @param {Object} prevState React Component state values
|
||||||
|
*/
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (prevState.divStyle.display == 'none' && this.state.divStyle.display == 'block') {
|
||||||
|
this.enterTimer = setTimeout(() => this.sliderVal.focus(), 10);
|
||||||
|
}
|
||||||
|
if (prevProps.max !== this.props.max && this.state.inputValue > this.props.max) {
|
||||||
|
// they chose a different module
|
||||||
|
this.setState({ inputValue: this.props.max });
|
||||||
|
}
|
||||||
|
if (this.state.inputValue != prevState.inputValue && prevProps.max == this.props.max) {
|
||||||
|
this.props.onChange(this.state.inputValue / this.props.max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set initial state for the textbox.
|
||||||
|
* We may want to rethink this to
|
||||||
|
* try and make it a stateless component
|
||||||
|
* @returns {object} React state object with initial values set
|
||||||
|
*/
|
||||||
|
_getInitialState() {
|
||||||
|
return {
|
||||||
|
divStyle: { display:'none' },
|
||||||
|
inputStyle: { width:'4em' },
|
||||||
|
labelStyle: { marginLeft: '.1em' },
|
||||||
|
maxLength:5,
|
||||||
|
size:5,
|
||||||
|
min:0,
|
||||||
|
tabIndex:-1,
|
||||||
|
type:'number',
|
||||||
|
readOnly: true,
|
||||||
|
inputValue: this.props.percent * this.props.max
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} val block or none
|
||||||
|
*/
|
||||||
|
_setDisplay(val) {
|
||||||
|
this.setState({
|
||||||
|
divStyle: { display:val }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Update the input value
|
||||||
|
* when textbox gets focus
|
||||||
|
*/
|
||||||
|
_handleFocus() {
|
||||||
|
this.setState({
|
||||||
|
inputValue:this._getValue()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Update inputValue when textbox loses focus
|
||||||
|
*/
|
||||||
|
_handleBlur() {
|
||||||
|
this._setDisplay('none');
|
||||||
|
if (this.state.inputValue !== '') {
|
||||||
|
this.props.onChange(this.state.inputValue / this.props.max);
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
inputValue: this.props.percent * this.props.max
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get the value in the text box
|
||||||
|
* @returns {number} inputValue Value of the input box
|
||||||
|
*/
|
||||||
|
_getValue() {
|
||||||
|
return this.state.inputValue;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Update and set limits on input box
|
||||||
|
* values depending on what user
|
||||||
|
* has selected
|
||||||
|
*
|
||||||
|
* @param {SyntheticEvent} event ReactJs onChange event
|
||||||
|
*/
|
||||||
|
_handleChange(event) {
|
||||||
|
if (event.target.value < 0) {
|
||||||
|
this.setState({ inputValue: 0 });
|
||||||
|
} else if (event.target.value <= this.props.max) {
|
||||||
|
this.setState({ inputValue: event.target.value });
|
||||||
|
} else {
|
||||||
|
this.setState({ inputValue: this.props.max });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Key up handler for input field.
|
||||||
|
* If user hits Enter key, blur/close the input field
|
||||||
|
* @param {Event} event Keyboard event
|
||||||
|
*/
|
||||||
|
_keyup(event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Enter':
|
||||||
|
this.sliderVal.blur();
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get the value in the text box
|
||||||
|
* @return {React.Component} Text Input component for Slider
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
let { axisUnit, onChange, percent, scale } = this.props;
|
||||||
|
return <div style={this.state.divStyle}><input style={this.state.inputStyle} value={this._getValue()} min={this.state.min} max={this.props.max} onChange={this._handleChange} onKeyUp={this._keyup} tabIndex={this.state.tabIndex} maxLength={this.state.maxLength} size={this.state.size} onBlur={() => {this._handleBlur();}} onFocus={() => {this._handleFocus();}} type={this.state.type} ref={(ip) => this.sliderVal = ip}/><text className="primary upp" style={this.state.labelStyle}>{this.props.axisUnit}</text></div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,37 +2,30 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { ListModifications, Modified } from './SvgIcons';
|
|
||||||
import AvailableModulesMenu from './AvailableModulesMenu';
|
import AvailableModulesMenu from './AvailableModulesMenu';
|
||||||
import ModificationsMenu from './ModificationsMenu';
|
import ModificationsMenu from './ModificationsMenu';
|
||||||
import { stopCtxPropagation, wrapCtxMenu } from '../utils/UtilityFunctions';
|
import { diffDetails } from '../utils/SlotFunctions';
|
||||||
import { blueprintTooltip } from '../utils/BlueprintFunctions';
|
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||||
import { Module } from 'ed-forge';
|
|
||||||
import { TYPES } from 'ed-forge/lib/src/data/slots';
|
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
import { toPairs } from 'lodash';
|
|
||||||
|
|
||||||
const HARDPOINT_SLOT_LABELS = {
|
|
||||||
1: 'S',
|
|
||||||
2: 'M',
|
|
||||||
3: 'L',
|
|
||||||
4: 'H',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract Slot
|
* Abstract Slot
|
||||||
*/
|
*/
|
||||||
export default class Slot extends TranslatedComponent {
|
export default class Slot extends TranslatedComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
currentMenu: PropTypes.any,
|
availableModules: PropTypes.func.isRequired,
|
||||||
hideSearch: PropTypes.bool,
|
onSelect: PropTypes.func.isRequired,
|
||||||
m: PropTypes.instanceOf(Module),
|
onOpen: PropTypes.func.isRequired,
|
||||||
|
maxClass: PropTypes.number.isRequired,
|
||||||
|
selected: PropTypes.bool,
|
||||||
|
m: PropTypes.object,
|
||||||
|
enabled: PropTypes.bool.isRequired,
|
||||||
|
ship: PropTypes.object.isRequired,
|
||||||
|
eligible: PropTypes.object,
|
||||||
warning: PropTypes.func,
|
warning: PropTypes.func,
|
||||||
drag: PropTypes.func,
|
drag: PropTypes.func,
|
||||||
drop: PropTypes.func,
|
drop: PropTypes.func,
|
||||||
dropClass: PropTypes.string,
|
dropClass: PropTypes.string
|
||||||
propsToShow: PropTypes.object.isRequired,
|
|
||||||
onPropToggle: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,122 +34,25 @@ export default class Slot extends TranslatedComponent {
|
|||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
|
||||||
this.state = { menuIndex: 0 };
|
this._modificationsSelected = false;
|
||||||
|
|
||||||
|
this._contextMenu = wrapCtxMenu(this._contextMenu.bind(this));
|
||||||
|
this._getMaxClassLabel = this._getMaxClassLabel.bind(this);
|
||||||
|
this._keyDown = this._keyDown.bind(this);
|
||||||
|
this.slotDiv = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Must be implemented by subclasses:
|
||||||
|
// _getSlotDetails()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a menu while setting state.
|
* Get the CSS class name for the slot. Can/should be overriden
|
||||||
* @param {Object} newMenuIndex New menu index
|
* as necessary.
|
||||||
* @param {Event} event Event object
|
* @return {string} CSS Class name
|
||||||
*/
|
*/
|
||||||
_openMenu(newMenuIndex, event) {
|
_getClassNames() {
|
||||||
const slotName = this.props.m.getSlot();
|
|
||||||
if (
|
|
||||||
this.props.currentMenu === slotName &&
|
|
||||||
newMenuIndex === this.state.menuIndex
|
|
||||||
) {
|
|
||||||
this.context.closeMenu();
|
|
||||||
} {
|
|
||||||
this.setState({ menuIndex: newMenuIndex });
|
|
||||||
this.context.openMenu(slotName);
|
|
||||||
}
|
|
||||||
// If we don't stop event propagation, the underlying divs also might
|
|
||||||
// get clicked which would open up other menus
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the slot contents
|
|
||||||
* @return {React.Component} Slot contents
|
|
||||||
*/
|
|
||||||
_getSlotDetails() {
|
|
||||||
const { m, propsToShow } = this.props;
|
|
||||||
let { termtip, tooltip, language } = this.context;
|
|
||||||
const { translate, units, formats } = language;
|
|
||||||
|
|
||||||
if (m.isEmpty()) {
|
|
||||||
return <div className="empty">
|
|
||||||
{translate(
|
|
||||||
m.isOnSlot(TYPES.MILITARY) ? 'emptyrestricted' : 'empty'
|
|
||||||
)}
|
|
||||||
</div>;
|
|
||||||
} else {
|
|
||||||
let classRating = m.getClassRating();
|
|
||||||
let { drag, drop } = this.props;
|
|
||||||
|
|
||||||
// Modifications tooltip shows blueprint and grade, if available
|
|
||||||
let modTT = translate('modified');
|
|
||||||
const blueprint = m.getBlueprint();
|
|
||||||
const experimental = m.getExperimental();
|
|
||||||
const grade = m.getBlueprintGrade();
|
|
||||||
if (blueprint) {
|
|
||||||
modTT = `${translate(blueprint)} ${translate('grade')}: ${grade}`;
|
|
||||||
if (experimental) {
|
|
||||||
modTT += `, ${translate(experimental)}`;
|
|
||||||
}
|
|
||||||
modTT = (
|
|
||||||
<div>
|
|
||||||
<div>{modTT}</div>
|
|
||||||
{blueprintTooltip(language, m)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mass = m.get('mass') || m.get('cargo') || m.get('fuel') || 0;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn('details', { disabled: !m.isEnabled() })}
|
|
||||||
draggable="true"
|
|
||||||
onDragStart={drag}
|
|
||||||
onDragEnd={drop}
|
|
||||||
>
|
|
||||||
<div className={'cb'}>
|
|
||||||
<div className={'l'}>
|
|
||||||
{classRating} {translate(m.readMeta('type'))}
|
|
||||||
{blueprint && (
|
|
||||||
<span
|
|
||||||
onMouseOver={termtip.bind(null, modTT)}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>
|
|
||||||
<Modified />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{propsToShow.mass ?
|
|
||||||
<div className={'r'}>
|
|
||||||
{formats.round(mass)}
|
|
||||||
{units.T}
|
|
||||||
</div> : null}
|
|
||||||
</div>
|
|
||||||
<div className={'cb'}>
|
|
||||||
{toPairs(propsToShow).sort().map(([prop, show]) => {
|
|
||||||
const { unit, value } = m.getFormatted(prop, true);
|
|
||||||
// Don't show mass again; it's already shown on the top right
|
|
||||||
// corner of a slot
|
|
||||||
if (!show || isNaN(value) || prop == 'mass') {
|
|
||||||
return null;
|
return null;
|
||||||
} else {
|
|
||||||
return (<div className='l'>
|
|
||||||
{translate(prop)}: {formats.round(value)}{unit}
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
{(m.getApplicableBlueprints() || []).length > 0 ? (
|
|
||||||
<div className="r">
|
|
||||||
<button onClick={this._openMenu.bind(this, 1)}
|
|
||||||
onContextMenu={stopCtxPropagation}
|
|
||||||
onMouseOver={termtip.bind(null, translate('modifications'))}
|
|
||||||
onMouseOut={tooltip.bind(null, null)}
|
|
||||||
>
|
|
||||||
<ListModifications />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,19 +61,7 @@ export default class Slot extends TranslatedComponent {
|
|||||||
* @return {string} label
|
* @return {string} label
|
||||||
*/
|
*/
|
||||||
_getMaxClassLabel() {
|
_getMaxClassLabel() {
|
||||||
const { m } = this.props;
|
return this.props.maxClass;
|
||||||
let size = m.getSizeNum();
|
|
||||||
switch (true) {
|
|
||||||
case m.getSlot() === 'armour':
|
|
||||||
return '';
|
|
||||||
case size === 0:
|
|
||||||
// This can also happen for armour but that case was handled above
|
|
||||||
return 'U';
|
|
||||||
case m.isOnSlot(TYPES.HARDPOINT):
|
|
||||||
return HARDPOINT_SLOT_LABELS[size];
|
|
||||||
default:
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,15 +71,25 @@ export default class Slot extends TranslatedComponent {
|
|||||||
_contextMenu(event) {
|
_contextMenu(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const { m } = this.props;
|
this.props.onSelect(null,null);
|
||||||
m.reset();
|
|
||||||
if (this.props.currentMenu === m.getSlot()) {
|
|
||||||
this.context.closeMenu();
|
|
||||||
} else {
|
|
||||||
this.forceUpdate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Key Down handler
|
||||||
|
* @param {SyntheticEvent} event Event
|
||||||
|
* ToDo: see if this can be moved up
|
||||||
|
* we do more or less the same thing
|
||||||
|
* in every section when Enter key is pressed
|
||||||
|
* on a focusable item
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
_keyDown(event) {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
if(event.target.className == 'r') {
|
||||||
|
this._toggleModifications();
|
||||||
|
}
|
||||||
|
this.props.onOpen(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Render the slot
|
* Render the slot
|
||||||
* @return {React.Component} The slot
|
* @return {React.Component} The slot
|
||||||
@@ -203,41 +97,64 @@ export default class Slot extends TranslatedComponent {
|
|||||||
render() {
|
render() {
|
||||||
let language = this.context.language;
|
let language = this.context.language;
|
||||||
let translate = language.translate;
|
let translate = language.translate;
|
||||||
let {
|
let { ship, m, enabled, dropClass, dragOver, onOpen, onChange, selected, eligible, onSelect, warning, availableModules } = this.props;
|
||||||
currentMenu, m, dropClass, dragOver, warning, hideSearch, propsToShow,
|
let slotDetails, modificationsMarker, menu;
|
||||||
onPropToggle,
|
|
||||||
} = this.props;
|
if (!selected) {
|
||||||
const { menuIndex } = this.state;
|
// If not selected then sure that modifications flag is unset
|
||||||
|
this._modificationsSelected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m) {
|
||||||
|
slotDetails = this._getSlotDetails(m, enabled, translate, language.formats, language.units); // Must be implemented by sub classes
|
||||||
|
modificationsMarker = JSON.stringify(m);
|
||||||
|
} else {
|
||||||
|
slotDetails = <div className={'empty'}>{translate(eligible ? 'emptyrestricted' : 'empty')}</div>;
|
||||||
|
modificationsMarker = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
if (this._modificationsSelected) {
|
||||||
|
menu = <ModificationsMenu
|
||||||
|
className={this._getClassNames()}
|
||||||
|
onChange={onChange}
|
||||||
|
ship={ship}
|
||||||
|
m={m}
|
||||||
|
marker={modificationsMarker}
|
||||||
|
modButton = {this.modButton}
|
||||||
|
/>;
|
||||||
|
} else {
|
||||||
|
menu = <AvailableModulesMenu
|
||||||
|
className={this._getClassNames()}
|
||||||
|
modules={availableModules()}
|
||||||
|
shipMass={ship.hullMass}
|
||||||
|
m={m}
|
||||||
|
onSelect={onSelect}
|
||||||
|
warning={warning}
|
||||||
|
diffDetails={diffDetails.bind(ship, this.context.language)}
|
||||||
|
slotDiv = {this.slotDiv}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: implement touch dragging
|
// TODO: implement touch dragging
|
||||||
const selected = currentMenu === m.getSlot();
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn('slot', dropClass, { selected })} onClick={onOpen} onKeyDown={this._keyDown} onContextMenu={this._contextMenu} onDragOver={dragOver} tabIndex="0" ref={slotDiv => this.slotDiv = slotDiv}>
|
||||||
className={cn('slot', dropClass, { selected })}
|
<div className='details-container'>
|
||||||
onContextMenu={this._contextMenu}
|
<div className='sz'>{this._getMaxClassLabel(translate)}</div>
|
||||||
onDragOver={dragOver} tabIndex="0"
|
{slotDetails}
|
||||||
onClick={this._openMenu.bind(this, 0)}
|
|
||||||
>
|
|
||||||
<div className={cn(
|
|
||||||
'details-container',
|
|
||||||
{ warning: warning && warning(m) },
|
|
||||||
)}>
|
|
||||||
<div className="sz">{this._getMaxClassLabel(translate)}</div>
|
|
||||||
{this._getSlotDetails()}
|
|
||||||
</div>
|
</div>
|
||||||
{selected && menuIndex === 0 &&
|
{menu}
|
||||||
<AvailableModulesMenu
|
|
||||||
m={m} hideSearch={hideSearch}
|
|
||||||
onSelect={(item) => {
|
|
||||||
m.setItem(item);
|
|
||||||
this.context.closeMenu();
|
|
||||||
}}
|
|
||||||
warning={warning}
|
|
||||||
/>}
|
|
||||||
{selected && menuIndex === 1 &&
|
|
||||||
<ModificationsMenu m={m} propsToShow={propsToShow}
|
|
||||||
onPropToggle={onPropToggle} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the modifications flag when selecting the modifications icon
|
||||||
|
*/
|
||||||
|
_toggleModifications() {
|
||||||
|
this._modificationsSelected = !this._modificationsSelected;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,35 +2,50 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||||
|
import { canMount } from '../utils/SlotFunctions';
|
||||||
import { Equalizer } from '../components/SvgIcons';
|
import { Equalizer } from '../components/SvgIcons';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { Ship } from 'ed-forge';
|
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
const browser = require('detect-browser');
|
const browser = require('detect-browser');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract Slot Section
|
* Abstract Slot Section
|
||||||
*/
|
*/
|
||||||
export default class SlotSection extends TranslatedComponent {
|
export default class SlotSection extends TranslatedComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
ship: PropTypes.instanceOf(Ship),
|
ship: PropTypes.object.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onCargoChange: PropTypes.func.isRequired,
|
||||||
|
onFuelChange: PropTypes.func.isRequired,
|
||||||
code: PropTypes.string.isRequired,
|
code: PropTypes.string.isRequired,
|
||||||
togglePwr: PropTypes.func,
|
togglePwr: PropTypes.func,
|
||||||
propsToShow: PropTypes.object.isRequired,
|
sectionMenuRefs: PropTypes.object
|
||||||
onPropToggle: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
* @param {Object} props React Component properties
|
* @param {Object} props React Component properties
|
||||||
|
* @param {Object} context React Component context
|
||||||
|
* @param {string} sectionId Section DOM Id
|
||||||
* @param {string} sectionName Section name
|
* @param {string} sectionName Section name
|
||||||
*/
|
*/
|
||||||
constructor(props, sectionName) {
|
constructor(props, context, sectionId, sectionName) {
|
||||||
super(props);
|
super(props);
|
||||||
autoBind(this);
|
this.sectionId = sectionId;
|
||||||
|
|
||||||
this.sectionName = sectionName;
|
this.sectionName = sectionName;
|
||||||
|
this.ssHeadRef = null;
|
||||||
|
|
||||||
|
this.sectionRefArr = this.props.sectionMenuRefs[this.sectionId] = [];
|
||||||
|
this.sectionRefArr['selectedRef'] = null;
|
||||||
|
this._getSlots = this._getSlots.bind(this);
|
||||||
|
this._selectModule = this._selectModule.bind(this);
|
||||||
|
this._getSectionMenu = this._getSectionMenu.bind(this);
|
||||||
|
this._contextMenu = this._contextMenu.bind(this);
|
||||||
|
this._drop = this._drop.bind(this);
|
||||||
|
this._dragOverNone = this._dragOverNone.bind(this);
|
||||||
|
this._close = this._close.bind(this);
|
||||||
|
this._keyDown = this._keyDown.bind(this);
|
||||||
|
this._handleSectionFocus = this._handleSectionFocus.bind(this);
|
||||||
this.state = {};
|
this.state = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +55,82 @@ export default class SlotSection extends TranslatedComponent {
|
|||||||
// _contextMenu()
|
// _contextMenu()
|
||||||
// componentDidUpdate(prevProps)
|
// componentDidUpdate(prevProps)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: May either need to send the function to be triggered when Enter key is pressed, or else
|
||||||
|
* may need a separate keyDown handler for each subclass (StandardSlotSection, HardpointSlotSection, etc.)
|
||||||
|
* ex: _keyDown(_keyDownfn, event)
|
||||||
|
*
|
||||||
|
* @param {SyntheticEvent} event KeyDown event
|
||||||
|
*/
|
||||||
|
_keyDown(event) {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (event.currentTarget.nodeName === 'H1') {
|
||||||
|
this._openMenu(this.sectionName, event);
|
||||||
|
} else {
|
||||||
|
event.currentTarget.click();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key == 'Tab') {
|
||||||
|
if (event.shiftKey) {
|
||||||
|
if ((event.currentTarget === this.sectionRefArr[this.firstRefId]) && this.sectionRefArr[this.lastRefId]) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.sectionRefArr[this.lastRefId].focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ((event.currentTarget === this.sectionRefArr[this.lastRefId]) && this.sectionRefArr[this.firstRefId]) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.sectionRefArr[this.firstRefId].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set focus on appropriate Slot Section Menu element
|
||||||
|
* @param {Object} focusPrevProps prevProps for componentDidUpdate() from ...SlotSection.jsx
|
||||||
|
* @param {String} firstRef id of the first ref in ...SlotSection.jsx
|
||||||
|
* @param {String} lastRef id of the last ref in ...SlotSection.jsx
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
_handleSectionFocus(focusPrevProps, firstRef, lastRef) {
|
||||||
|
if (this.selectedRefId !== null && this.sectionRefArr[this.selectedRefId]) {
|
||||||
|
// set focus on the previously selected option for the currently open section menu
|
||||||
|
this.sectionRefArr[this.selectedRefId].focus();
|
||||||
|
} else if (this.sectionRefArr[firstRef] && this.sectionRefArr[firstRef] != null) {
|
||||||
|
// set focus on the first option in the currently open section menu if none have been selected previously
|
||||||
|
this.sectionRefArr[firstRef].focus();
|
||||||
|
} else if (this.props.currentMenu == null && focusPrevProps.currentMenu == this.sectionName && this.sectionRefArr['ssHeadRef']) {
|
||||||
|
// set focus on the section menu header when section menu is closed
|
||||||
|
this.sectionRefArr['ssHeadRef'].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Open a menu
|
||||||
|
* @param {string} menu Menu name
|
||||||
|
* @param {SyntheticEvent} event Event
|
||||||
|
*/
|
||||||
|
_openMenu(menu, event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (this.props.currentMenu === menu) {
|
||||||
|
menu = null;
|
||||||
|
}
|
||||||
|
this.context.openMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mount/Use the specified module in the slot
|
||||||
|
* @param {Object} slot Slot
|
||||||
|
* @param {Object} m Selected module
|
||||||
|
*/
|
||||||
|
_selectModule(slot, m) {
|
||||||
|
this.props.ship.use(slot, m, false);
|
||||||
|
this.props.onChange();
|
||||||
|
this._close();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Slot Drag Handler
|
* Slot Drag Handler
|
||||||
* @param {object} originSlot Origin slot model
|
* @param {object} originSlot Origin slot model
|
||||||
@@ -104,18 +195,10 @@ export default class SlotSection extends TranslatedComponent {
|
|||||||
if (targetSlot && canMount(this.props.ship, targetSlot, m.grp, m.class)) {
|
if (targetSlot && canMount(this.props.ship, targetSlot, m.grp, m.class)) {
|
||||||
const mCopy = m.clone();
|
const mCopy = m.clone();
|
||||||
this.props.ship.use(targetSlot, mCopy, false);
|
this.props.ship.use(targetSlot, mCopy, false);
|
||||||
let experimentalNum = this.props.ship.hardpoints
|
|
||||||
.filter(s => s.m && s.m.experimental).length;
|
|
||||||
// Remove the module on the last slot if we now exceed the number of
|
|
||||||
// experimentals allowed
|
|
||||||
if (m.experimental && 4 < experimentalNum) {
|
|
||||||
this.props.ship.updateStats(originSlot, null, originSlot.m);
|
|
||||||
originSlot.m = null; // Empty the slot
|
|
||||||
originSlot.discountedCost = 0;
|
|
||||||
}
|
|
||||||
// Copy power info
|
// Copy power info
|
||||||
targetSlot.enabled = originSlot.enabled;
|
targetSlot.enabled = originSlot.enabled;
|
||||||
targetSlot.priority = originSlot.priority;
|
targetSlot.priority = originSlot.priority;
|
||||||
|
this.props.onChange();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Store power info
|
// Store power info
|
||||||
@@ -144,6 +227,7 @@ export default class SlotSection extends TranslatedComponent {
|
|||||||
targetSlot.enabled = targetEnabled;
|
targetSlot.enabled = targetEnabled;
|
||||||
targetSlot.priority = targetPriority;
|
targetSlot.priority = targetPriority;
|
||||||
}
|
}
|
||||||
|
this.props.onChange();
|
||||||
this.props.ship
|
this.props.ship
|
||||||
.updatePowerGenerated()
|
.updatePowerGenerated()
|
||||||
.updatePowerUsed()
|
.updatePowerUsed()
|
||||||
@@ -189,17 +273,6 @@ export default class SlotSection extends TranslatedComponent {
|
|||||||
return 'ineligible'; // Cannot be dropped / invalid drop slot
|
return 'ineligible'; // Cannot be dropped / invalid drop slot
|
||||||
}
|
}
|
||||||
|
|
||||||
_open(newMenu, event) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
const { currentMenu } = this.props;
|
|
||||||
if (currentMenu === newMenu) {
|
|
||||||
this.context.closeMenu();
|
|
||||||
} else {
|
|
||||||
this.context.openMenu(newMenu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close current menu
|
* Close current menu
|
||||||
*/
|
*/
|
||||||
@@ -216,13 +289,14 @@ export default class SlotSection extends TranslatedComponent {
|
|||||||
render() {
|
render() {
|
||||||
let translate = this.context.language.translate;
|
let translate = this.context.language.translate;
|
||||||
let sectionMenuOpened = this.props.currentMenu === this.sectionName;
|
let sectionMenuOpened = this.props.currentMenu === this.sectionName;
|
||||||
|
let open = this._openMenu.bind(this, this.sectionName);
|
||||||
|
let ctx = wrapCtxMenu(this._contextMenu);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group" onDragLeave={this._dragOverNone}>
|
<div id={this.sectionId} className={'group'} onDragLeave={this._dragOverNone}>
|
||||||
<div className={cn('section-menu', { selected: sectionMenuOpened })}
|
<div className={cn('section-menu', { selected: sectionMenuOpened })} onClick={open} onContextMenu={ctx}>
|
||||||
onContextMenu={wrapCtxMenu(this._contextMenu)} onClick={this._open.bind(this, this.sectionName)}>
|
<h1 tabIndex="0" onKeyDown={this._keyDown} ref={ssHead => this.sectionRefArr['ssHeadRef'] = ssHead}>{translate(this.sectionName)} <Equalizer/></h1>
|
||||||
<h1 tabIndex="0">{translate(this.sectionName)}<Equalizer/></h1>
|
{sectionMenuOpened ? this._getSectionMenu(translate, this.props.ship) : null }
|
||||||
{sectionMenuOpened && this._getSectionMenu()}
|
|
||||||
</div>
|
</div>
|
||||||
{this._getSlots()}
|
{this._getSlots()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
162
src/app/components/StandardSlot.jsx
Normal file
162
src/app/components/StandardSlot.jsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import Persist from '../stores/Persist';
|
||||||
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
|
import { diffDetails } from '../utils/SlotFunctions';
|
||||||
|
import AvailableModulesMenu from './AvailableModulesMenu';
|
||||||
|
import ModificationsMenu from './ModificationsMenu';
|
||||||
|
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||||
|
import { ListModifications, Modified } from './SvgIcons';
|
||||||
|
import { Modifications } from 'coriolis-data/dist';
|
||||||
|
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||||
|
import { blueprintTooltip } from '../utils/BlueprintFunctions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard Slot
|
||||||
|
*/
|
||||||
|
export default class StandardSlot extends TranslatedComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
slot: PropTypes.object,
|
||||||
|
modules: PropTypes.array.isRequired,
|
||||||
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
onOpen: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
ship: PropTypes.object.isRequired,
|
||||||
|
selected: PropTypes.bool,
|
||||||
|
warning: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the slot
|
||||||
|
* @param {object} props Object properties
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this._modificationsSelected = false;
|
||||||
|
this._keyDown = this._keyDown.bind(this);
|
||||||
|
this.modButton = null;
|
||||||
|
this.slotDiv = null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Handle Enter key
|
||||||
|
* @param {SyntheticEvent} event KeyDown event
|
||||||
|
*/
|
||||||
|
_keyDown(event) {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
if(event.target.className == 'r') {
|
||||||
|
this._toggleModifications();
|
||||||
|
}
|
||||||
|
this.props.onOpen(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the slot
|
||||||
|
* @return {React.Component} Slot component
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
let { termtip, tooltip } = this.context;
|
||||||
|
let { translate, formats, units } = this.context.language;
|
||||||
|
let { modules, slot, selected, warning, onSelect, onChange, ship } = this.props;
|
||||||
|
let m = slot.m;
|
||||||
|
let classRating = m.class + m.rating;
|
||||||
|
let menu;
|
||||||
|
let validMods = m == null || !Modifications.modules[m.grp] ? [] : (Modifications.modules[m.grp].modifications || []);
|
||||||
|
if (m && m.name && m.name === 'Guardian Hybrid Power Plant') {
|
||||||
|
validMods = [];
|
||||||
|
}
|
||||||
|
if (m && m.name && m.name === 'Guardian Power Distributor') {
|
||||||
|
validMods = [];
|
||||||
|
}
|
||||||
|
let showModuleResistances = Persist.showModuleResistances();
|
||||||
|
let mass = m.getMass() || m.cargo || m.fuel || 0;
|
||||||
|
|
||||||
|
// Modifications tooltip shows blueprint and grade, if available
|
||||||
|
let modTT = translate('modified');
|
||||||
|
if (m && m.blueprint && m.blueprint.name) {
|
||||||
|
modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
|
||||||
|
if (m.blueprint.special && m.blueprint.special.id >= 0) {
|
||||||
|
modTT += ', ' + translate(m.blueprint.special.name);
|
||||||
|
}
|
||||||
|
modTT = (
|
||||||
|
<div>
|
||||||
|
<div>{modTT}</div>
|
||||||
|
{blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selected) {
|
||||||
|
// If not selected then sure that modifications flag is unset
|
||||||
|
this._modificationsSelected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modificationsMarker = JSON.stringify(m);
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
if (this._modificationsSelected) {
|
||||||
|
menu = <ModificationsMenu
|
||||||
|
className='standard'
|
||||||
|
onChange={onChange}
|
||||||
|
ship={ship}
|
||||||
|
m={m}
|
||||||
|
marker={modificationsMarker}
|
||||||
|
modButton = {this.modButton}
|
||||||
|
/>;
|
||||||
|
} else {
|
||||||
|
menu = <AvailableModulesMenu
|
||||||
|
className='standard'
|
||||||
|
modules={modules}
|
||||||
|
shipMass={ModuleUtils.isShieldGenerator(m.grp) ? ship.hullMass : ship.unladenMass}
|
||||||
|
m={m}
|
||||||
|
onSelect={onSelect}
|
||||||
|
warning={warning}
|
||||||
|
diffDetails={diffDetails.bind(ship, this.context.language)}
|
||||||
|
slotDiv = {this.slotDiv}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('slot', { selected: this.props.selected })} onClick={this.props.onOpen} onKeyDown={this._keyDown} onContextMenu={stopCtxPropagation} tabIndex="0" ref={ slotDiv => this.slotDiv = slotDiv }>
|
||||||
|
<div className={cn('details-container', { warning: warning && warning(slot.m), disabled: m.grp !== 'bh' && !slot.enabled })}>
|
||||||
|
<div className={'sz'}>{m.grp == 'bh' ? m.name.charAt(0) : slot.maxClass}</div>
|
||||||
|
<div>
|
||||||
|
<div className={'l'}>{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? <span className='r' onMouseOver={termtip.bind(null, modTT)} onMouseOut={tooltip.bind(null, null)}><Modified /></span> : null }</div>
|
||||||
|
<div className={'r'}>{formats.round(mass)}{units.T}</div>
|
||||||
|
<div/>
|
||||||
|
<div className={'cb'}>
|
||||||
|
{ m.getMinMass() ? <div className='l'>{translate('minimum mass')}: {formats.int(m.getMinMass())}{units.T}</div> : null }
|
||||||
|
{ m.getOptMass() ? <div className='l'>{translate('optimal mass')}: {formats.int(m.getOptMass())}{units.T}</div> : null }
|
||||||
|
{ m.getMaxMass() ? <div className='l'>{translate('max mass')}: {formats.int(m.getMaxMass())}{units.T}</div> : null }
|
||||||
|
{ m.getOptMul() ? <div className='l'>{translate('optimal multiplier')}: {formats.rPct(m.getOptMul())}</div> : null }
|
||||||
|
{ m.getRange() ? <div className='l'>{translate('range', m.grp)}: {formats.f2(m.getRange())}{units.km}</div> : null }
|
||||||
|
{ m.time ? <div className='l'>{translate('time')}: {formats.time(m.time)}</div> : null }
|
||||||
|
{ m.getThermalEfficiency() ? <div className='l'>{translate('efficiency')}: {formats.f2(m.getThermalEfficiency())}</div> : null }
|
||||||
|
{ m.getPowerGeneration() > 0 ? <div className='l'>{translate('pgen')}: {formats.f1(m.getPowerGeneration())}{units.MW}</div> : null }
|
||||||
|
{ m.getMaxFuelPerJump() ? <div className='l'>{translate('max')} {translate('fuel')}: {formats.f1(m.getMaxFuelPerJump())}{units.T}</div> : null }
|
||||||
|
{ m.getWeaponsCapacity() ? <div className='l'>{translate('WEP')}: {formats.f1(m.getWeaponsCapacity())}{units.MJ} / {formats.f1(m.getWeaponsRechargeRate())}{units.MW}</div> : null }
|
||||||
|
{ m.getSystemsCapacity() ? <div className='l'>{translate('SYS')}: {formats.f1(m.getSystemsCapacity())}{units.MJ} / {formats.f1(m.getSystemsRechargeRate())}{units.MW}</div> : null }
|
||||||
|
{ m.getEnginesCapacity() ? <div className='l'>{translate('ENG')}: {formats.f1(m.getEnginesCapacity())}{units.MJ} / {formats.f1(m.getEnginesRechargeRate())}{units.MW}</div> : null }
|
||||||
|
{ showModuleResistances && m.getExplosiveResistance() ? <div className='l'>{translate('explres')}: {formats.pct(m.getExplosiveResistance())}</div> : null }
|
||||||
|
{ showModuleResistances && m.getKineticResistance() ? <div className='l'>{translate('kinres')}: {formats.pct(m.getKineticResistance())}</div> : null }
|
||||||
|
{ showModuleResistances && m.getThermalResistance() ? <div className='l'>{translate('thermres')}: {formats.pct(m.getThermalResistance())}</div> : null }
|
||||||
|
{ m.getIntegrity() ? <div className='l'>{translate('integrity')}: {formats.int(m.getIntegrity())}</div> : null }
|
||||||
|
{ validMods.length > 0 ? <div className='r' tabIndex="0" ref={ modButton => this.modButton = modButton }><button tabIndex="-1" onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{menu}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the modifications flag when selecting the modifications icon
|
||||||
|
*/
|
||||||
|
_toggleModifications() {
|
||||||
|
this._modificationsSelected = !this._modificationsSelected;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,67 +1,252 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import cn from 'classnames';
|
||||||
import SlotSection from './SlotSection';
|
import SlotSection from './SlotSection';
|
||||||
import Slot from './Slot';
|
import StandardSlot from './StandardSlot';
|
||||||
import autoBind from 'auto-bind';
|
import Module from '../shipyard/Module';
|
||||||
import { stopCtxPropagation, moduleGet } from '../utils/UtilityFunctions';
|
import * as ShipRoles from '../shipyard/ShipRoles';
|
||||||
import { ShipProps, Module } from 'ed-forge';
|
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||||
import { getModuleInfo } from 'ed-forge/lib/src/data/items';
|
|
||||||
const { CONSUMED_RETR, LADEN_MASS } = ShipProps;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard Slot section
|
* Standard Slot section
|
||||||
*/
|
*/
|
||||||
export default class StandardSlotSection extends SlotSection {
|
export default class StandardSlotSection extends SlotSection {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
* @param {Object} props React Component properties
|
* @param {Object} props React Component properties
|
||||||
* @param {Object} context React Component context
|
* @param {Object} context React Component context
|
||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props, context) {
|
||||||
super(props, 'core internal');
|
super(props, context, 'standard', 'core internal');
|
||||||
autoBind(this);
|
this._optimizeStandard = this._optimizeStandard.bind(this);
|
||||||
|
this._selectBulkhead = this._selectBulkhead.bind(this);
|
||||||
|
this._showDW2Menu = this._showDW2Menu.bind(this);
|
||||||
|
this._dw2 = this._dw2.bind(this);
|
||||||
|
this.selectedRefId = null;
|
||||||
|
this.firstRefId = 'maxjump';
|
||||||
|
this.lastRefId = 'dw2';
|
||||||
|
this.state = {
|
||||||
|
showDW2: false,
|
||||||
|
DW2Tier: -1,
|
||||||
|
DW2Eng: -1,
|
||||||
|
DW2Role: '',
|
||||||
|
DW2Gfsb: false,
|
||||||
|
DW2Gpp: false,
|
||||||
|
DW2Fighter: false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets all modules of the ship
|
* Handle focus if the component updates
|
||||||
|
* @param {Object} prevProps React Component properties
|
||||||
*/
|
*/
|
||||||
_emptyAll() {
|
componentDidUpdate(prevProps) {
|
||||||
this.props.ship.getModules().forEach((slot) => slot.reset());
|
this._handleSectionFocus(prevProps, this.firstRefId, this.lastRefId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the lightest/optimal available standard modules
|
||||||
|
*/
|
||||||
|
_optimizeStandard() {
|
||||||
|
this.selectedRefId = 'maxjump';
|
||||||
|
this.props.ship.useLightestStandard();
|
||||||
|
this.props.onChange();
|
||||||
|
this.props.onCargoChange(this.props.ship.cargoCapacity);
|
||||||
|
this.props.onFuelChange(this.props.ship.fuelCapacity);
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets all modules to a specific rating
|
* Fill all standard slots with the specificed rating (using max class)
|
||||||
* @param {string} rating Module rating to set
|
* @param {Boolean} shielded True if shield generator should be included
|
||||||
* @param {string} fsdPPException Custom rating for FSD
|
* @param {integer} bulkheadIndex Bulkhead to use see Constants.BulkheadNames
|
||||||
*/
|
*/
|
||||||
_nRated(rating, fsdPPException) {
|
_multiPurpose(shielded, bulkheadIndex) {
|
||||||
const { ship } = this.props;
|
this.selectedRefId = 'multipurpose';
|
||||||
const pp = ship.getPowerPlant();
|
if (bulkheadIndex === 2) this.selectedRefId = 'combat';
|
||||||
pp.setItem('powerplant', pp.getSize(), fsdPPException || rating);
|
ShipRoles.multiPurpose(this.props.ship, shielded, bulkheadIndex);
|
||||||
const eng = ship.getThrusters();
|
this.props.onChange();
|
||||||
eng.setItem('thrusters', eng.getSize(), rating);
|
this.props.onCargoChange(this.props.ship.cargoCapacity);
|
||||||
const fsd = ship.getFSD();
|
this.props.onFuelChange(this.props.ship.fuelCapacity);
|
||||||
fsd.setItem('fsd', fsd.getSize(), fsdPPException || rating);
|
|
||||||
const ls = ship.getLifeSupport();
|
|
||||||
ls.setItem('lifesupport', ls.getSize(), rating);
|
|
||||||
const pd = ship.getPowerDistributor();
|
|
||||||
pd.setItem('powerdistributor', pd.getSize(), rating);
|
|
||||||
const sen = ship.getSensors();
|
|
||||||
sen.setItem('sensors', sen.getSize(), rating);
|
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new slot for a given module.
|
* Trader Build
|
||||||
* @param {Module} m Module to create the slot for
|
* @param {Boolean} shielded True if shield generator should be included
|
||||||
* @param {function} warning Warning callback
|
|
||||||
* @return {React.Component} Slot component
|
|
||||||
*/
|
*/
|
||||||
_mkSlot(m, warning) {
|
_optimizeCargo(shielded) {
|
||||||
const { currentMenu, propsToShow, onPropToggle } = this.props;
|
this.selectedRefId = 'trader';
|
||||||
return <Slot key={m.getSlot()} m={m} warning={warning} hideSearch={true}
|
ShipRoles.trader(this.props.ship, shielded);
|
||||||
currentMenu={currentMenu} propsToShow={propsToShow} onPropToggle={onPropToggle}
|
this.props.onChange();
|
||||||
/>;
|
this.props.onCargoChange(this.props.ship.cargoCapacity);
|
||||||
|
this.props.onFuelChange(this.props.ship.fuelCapacity);
|
||||||
|
this._close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DW2 Build
|
||||||
|
*/
|
||||||
|
_dw2() {
|
||||||
|
this.selectedRefId = 'dw2';
|
||||||
|
this.setState({ showDW2: false });
|
||||||
|
ShipRoles.dw2Build(this.props.ship, this.state.DW2Tier, this.state.DW2Eng, this.state.DW2Role, this.state.DW2Gfsb, this.state.DW2Gpp, this.state.DW2Fighter);
|
||||||
|
this.props.ship.updateModificationsString();
|
||||||
|
this.props.onChange();
|
||||||
|
this.props.onCargoChange(this.props.ship.cargoCapacity);
|
||||||
|
this.props.onFuelChange(this.props.ship.fuelCapacity);
|
||||||
|
this._close();
|
||||||
|
}
|
||||||
|
|
||||||
|
_showDW2Menu(translate) {
|
||||||
|
return (
|
||||||
|
<div className='select' onClick={(e) => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
|
||||||
|
<div className='select-group cap'>{translate('Tier')}</div>
|
||||||
|
<ul id={'tier'}>
|
||||||
|
<li className={cn({ active: this.state.DW2Tier === 1 }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Tier: 1 })} onKeyDown={this._keyDown}
|
||||||
|
>{translate('1 - Max. Jump Range, Unshielded')}</li>
|
||||||
|
<li className={cn({ active: this.state.DW2Tier === 2 }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Tier: 2 })} onKeyDown={this._keyDown}
|
||||||
|
>{translate('2 - Max. Jump Range, Minimal Shields')}</li>
|
||||||
|
<li className={cn({ active: this.state.DW2Tier === 3 }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Tier: 3 })} onKeyDown={this._keyDown}
|
||||||
|
>{translate('3 - Max. Jump Range, Optimal Shields')}</li>
|
||||||
|
<li className={cn({ active: this.state.DW2Tier === 4 }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Tier: 4 })} onKeyDown={this._keyDown}
|
||||||
|
>{translate('4 - Max. Jump Range, Optimal Shields & Thrusters')}</li>
|
||||||
|
</ul>
|
||||||
|
<hr/>
|
||||||
|
<div className='select-group cap'>{translate('Engineering Level')}</div>
|
||||||
|
<ul id={'engLevel'}>
|
||||||
|
<li className={cn({ active: this.state.DW2Eng === 1 }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Eng: 1 })} onKeyDown={this._keyDown}
|
||||||
|
>{translate('No engineering')}</li>
|
||||||
|
<li className={cn({ active: this.state.DW2Eng === 2 }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Eng: 2 })} onKeyDown={this._keyDown}
|
||||||
|
>{translate('Only Felicity Farseer and Elvira Martuuk')}</li>
|
||||||
|
<li className={cn({ active: this.state.DW2Eng === 3 }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Eng: 3 })} onKeyDown={this._keyDown}
|
||||||
|
>{translate('All exploration engineers')}</li>
|
||||||
|
</ul>
|
||||||
|
<hr/>
|
||||||
|
<div className='select-group cap'>{translate('Role')}</div>
|
||||||
|
<ul id={'role'}>
|
||||||
|
<li className={cn({ active: this.state.DW2Role === 'exploration' }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Role: 'exploration' })}
|
||||||
|
onKeyDown={this._keyDown}
|
||||||
|
>{translate('Space exploration')}</li>
|
||||||
|
<li className={cn({ active: this.state.DW2Role === 'surface' }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Role: 'surface' })}
|
||||||
|
onKeyDown={this._keyDown}
|
||||||
|
>{translate('Surface exploration')}</li>
|
||||||
|
<li className={cn({ active: this.state.DW2Role === 'materialProspector' }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Role: 'materialProspector' })}
|
||||||
|
onKeyDown={this._keyDown}
|
||||||
|
>{translate('Material prospector')}</li>
|
||||||
|
<li className={cn({ active: this.state.DW2Role === 'propectorMining' }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Role: 'propectorMining' })}
|
||||||
|
onKeyDown={this._keyDown}
|
||||||
|
>{translate('Prospector/Sapper Miner')}</li>
|
||||||
|
<li className={cn({ active: this.state.DW2Role === 'bigRigMining' }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Role: 'bigRigMining' })}
|
||||||
|
onKeyDown={this._keyDown}
|
||||||
|
>{translate('Big Rig, full mining')}</li>
|
||||||
|
<li className={cn({ active: this.state.DW2Role === 'fuelRat' }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Role: 'fuelRat' })} onKeyDown={this._keyDown}
|
||||||
|
>{translate('Fuel Rat')}</li>
|
||||||
|
<li className={cn({ active: this.state.DW2Role === 'mechanic' }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Role: 'mechanic' })} onKeyDown={this._keyDown}
|
||||||
|
>{translate('Mechanic')}</li>
|
||||||
|
<li className={cn({ active: this.state.DW2Role === 'trucker' }, 'lc')} tabIndex="0"
|
||||||
|
onClick={() => this.setState({ DW2Role: 'trucker' })} onKeyDown={this._keyDown}
|
||||||
|
>{translate('Trucker')}</li>
|
||||||
|
</ul>
|
||||||
|
<hr/>
|
||||||
|
<ul>
|
||||||
|
<li className={cn({ active: this.state.DW2Gfsb === true }, 'lc')}
|
||||||
|
onClick={() => this.setState({ DW2Gfsb: this.state.DW2Gfsb !== true })}>
|
||||||
|
Add Guardian FSD Booster
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
<li className={cn({ active: this.state.DW2Gpp === true }, 'lc')}
|
||||||
|
onClick={() => this.setState({ DW2Gpp: this.state.DW2Gpp !== true })}>
|
||||||
|
Add Guardian Power Plant
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
<li className={cn({ active: this.state.DW2Fighter === true }, 'lc')}
|
||||||
|
onClick={() => this.setState({ DW2Fighter: this.state.DW2Fighter !== true })}>
|
||||||
|
Add Fighter
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr/>
|
||||||
|
<ul>
|
||||||
|
<li onClick={this._dw2} className={cn('lc')} tabIndex="0"
|
||||||
|
onKeyDown={this._keyDown}>
|
||||||
|
<button className="button">Apply</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Miner Build
|
||||||
|
* @param {Boolean} shielded True if shield generator should be included
|
||||||
|
*/
|
||||||
|
_optimizeMiner(shielded) {
|
||||||
|
this.selectedRefId = 'miner';
|
||||||
|
ShipRoles.miner(this.props.ship, shielded);
|
||||||
|
this.props.onChange();
|
||||||
|
this.props.onCargoChange(this.props.ship.cargoCapacity);
|
||||||
|
this.props.onFuelChange(this.props.ship.fuelCapacity);
|
||||||
|
this._close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explorer role
|
||||||
|
* @param {Boolean} planetary True if Planetary Vehicle Hangar (PVH) should be included
|
||||||
|
*/
|
||||||
|
_optimizeExplorer(planetary) {
|
||||||
|
this.selectedRefId = 'explorer';
|
||||||
|
if (planetary) this.selectedRefId = 'planetary';
|
||||||
|
ShipRoles.explorer(this.props.ship, planetary);
|
||||||
|
this.props.onChange();
|
||||||
|
this.props.onCargoChange(this.props.ship.cargoCapacity);
|
||||||
|
this.props.onFuelChange(this.props.ship.fuelCapacity);
|
||||||
|
this._close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Racer role
|
||||||
|
*/
|
||||||
|
_optimizeRacer() {
|
||||||
|
this.selectedRefId = 'racer';
|
||||||
|
ShipRoles.racer(this.props.ship);
|
||||||
|
this.props.onChange();
|
||||||
|
this.props.onCargoChange(this.props.ship.cargoCapacity);
|
||||||
|
this.props.onFuelChange(this.props.ship.fuelCapacity);
|
||||||
|
this._close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the specified bulkhead
|
||||||
|
* @param {Object} bulkhead Bulkhead module details
|
||||||
|
*/
|
||||||
|
_selectBulkhead(bulkhead) {
|
||||||
|
this.props.ship.useBulkhead(bulkhead.index);
|
||||||
|
this.context.tooltip();
|
||||||
|
this.props.onChange();
|
||||||
|
this._close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On right click optimize the standard modules
|
||||||
|
*/
|
||||||
|
_contextMenu() {
|
||||||
|
this._optimizeStandard();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,30 +254,107 @@ export default class StandardSlotSection extends SlotSection {
|
|||||||
* @return {Array} Array of Slots
|
* @return {Array} Array of Slots
|
||||||
*/
|
*/
|
||||||
_getSlots() {
|
_getSlots() {
|
||||||
const { ship } = this.props;
|
let { ship, currentMenu, cargo, fuel } = this.props;
|
||||||
const fsd = ship.getFSD();
|
let slots = new Array(8);
|
||||||
return [
|
let open = this._openMenu;
|
||||||
this._mkSlot(ship.getAlloys()),
|
let select = this._selectModule;
|
||||||
this._mkSlot(
|
let st = ship.standard;
|
||||||
ship.getPowerPlant(),
|
let avail = ship.getAvailableModules().standard;
|
||||||
(m) => moduleGet(m, 'powercapacity') < ship.get(CONSUMED_RETR),
|
let bh = ship.bulkheads;
|
||||||
),
|
|
||||||
this._mkSlot(
|
slots[0] = <StandardSlot
|
||||||
ship.getThrusters(),
|
key='bh'
|
||||||
(m) => moduleGet(m, 'enginemaximalmass') < ship.get(LADEN_MASS),
|
slot={bh}
|
||||||
),
|
modules={ship.getAvailableModules().bulkheads}
|
||||||
this._mkSlot(fsd),
|
onOpen={open.bind(this, bh)}
|
||||||
this._mkSlot(
|
onSelect={this._selectBulkhead}
|
||||||
ship.getPowerDistributor(),
|
selected={currentMenu == bh}
|
||||||
(m) => moduleGet(m, 'enginescapacity') <= ship.readProp('boostenergy'),
|
onChange={this.props.onChange}
|
||||||
),
|
ship={ship}
|
||||||
this._mkSlot(ship.getLifeSupport()),
|
/>;
|
||||||
this._mkSlot(ship.getSensors()),
|
|
||||||
this._mkSlot(
|
slots[1] = <StandardSlot
|
||||||
ship.getCoreFuelTank(),
|
key='pp'
|
||||||
(m) => moduleGet(m, 'fuel') < fsd.get('maxfuel')
|
slot={st[0]}
|
||||||
),
|
modules={avail[0]}
|
||||||
];
|
onOpen={open.bind(this, st[0])}
|
||||||
|
onSelect={select.bind(this, st[0])}
|
||||||
|
selected={currentMenu == st[0]}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
ship={ship}
|
||||||
|
warning={m => m instanceof Module ? m.getPowerGeneration() < ship.powerRetracted : m.pgen < ship.powerRetracted}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
slots[2] = <StandardSlot
|
||||||
|
key='th'
|
||||||
|
slot={st[1]}
|
||||||
|
modules={avail[1]}
|
||||||
|
onOpen={open.bind(this, st[1])}
|
||||||
|
onSelect={select.bind(this, st[1])}
|
||||||
|
selected={currentMenu == st[1]}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
ship={ship}
|
||||||
|
warning={m => m instanceof Module ? m.getMaxMass() < (ship.unladenMass + cargo + fuel - st[1].m.mass + m.mass) : m.maxmass < (ship.unladenMass + cargo + fuel - st[1].m.mass + m.mass)}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
slots[3] = <StandardSlot
|
||||||
|
key='fsd'
|
||||||
|
slot={st[2]}
|
||||||
|
modules={avail[2]}
|
||||||
|
onOpen={open.bind(this, st[2])}
|
||||||
|
onSelect={select.bind(this, st[2])}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
ship={ship}
|
||||||
|
selected={currentMenu == st[2]}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
slots[4] = <StandardSlot
|
||||||
|
key='ls'
|
||||||
|
slot={st[3]}
|
||||||
|
modules={avail[3]}
|
||||||
|
onOpen={open.bind(this, st[3])}
|
||||||
|
onSelect={select.bind(this, st[3])}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
ship={ship}
|
||||||
|
selected={currentMenu == st[3]}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
slots[5] = <StandardSlot
|
||||||
|
key='pd'
|
||||||
|
slot={st[4]}
|
||||||
|
modules={avail[4]}
|
||||||
|
onOpen={open.bind(this, st[4])}
|
||||||
|
onSelect={select.bind(this, st[4])}
|
||||||
|
selected={currentMenu == st[4]}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
ship={ship}
|
||||||
|
warning={m => m instanceof Module ? m.getEnginesCapacity() <= ship.boostEnergy : m.engcap <= ship.boostEnergy}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
slots[6] = <StandardSlot
|
||||||
|
key='ss'
|
||||||
|
slot={st[5]}
|
||||||
|
modules={avail[5]}
|
||||||
|
onOpen={open.bind(this, st[5])}
|
||||||
|
onSelect={select.bind(this, st[5])}
|
||||||
|
selected={currentMenu == st[5]}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
ship={ship}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
slots[7] = <StandardSlot
|
||||||
|
key='ft'
|
||||||
|
slot={st[6]}
|
||||||
|
modules={avail[6]}
|
||||||
|
onOpen={open.bind(this, st[6])}
|
||||||
|
onSelect={select.bind(this, st[6])}
|
||||||
|
selected={currentMenu == st[6]}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
ship={ship}
|
||||||
|
warning={m => m.fuel < st[2].m.maxfuel} // Show warning when fuel tank is smaller than FSD Max Fuel
|
||||||
|
/>;
|
||||||
|
|
||||||
|
return slots;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,18 +362,38 @@ export default class StandardSlotSection extends SlotSection {
|
|||||||
* @param {Function} translate Translate function
|
* @param {Function} translate Translate function
|
||||||
* @return {React.Component} Section menu
|
* @return {React.Component} Section menu
|
||||||
*/
|
*/
|
||||||
_getSectionMenu() {
|
_getSectionMenu(translate) {
|
||||||
const { translate } = this.context.language;
|
let planetaryDisabled = this.props.ship.internal.length < 4;
|
||||||
|
if (this.state.showDW2 === true) {
|
||||||
|
return this._showDW2Menu(translate);
|
||||||
|
}
|
||||||
return <div className='select' onClick={(e) => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
|
return <div className='select' onClick={(e) => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='lc' tabIndex="0" onClick={this._emptyAll}>{translate('empty all slots')}</li>
|
<li className='lc' tabIndex="0" onClick={this._optimizeStandard} onKeyDown={this._keyDown}
|
||||||
|
ref={smRef => this.sectionRefArr['maxjump'] = smRef}>{translate('Maximize Jump Range')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('core')}</div>
|
<div className='select-group cap'>{translate('roles')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='lc' tabIndex="0" onClick={this._nRated.bind(this, '5', undefined)}>{translate('A-rated')}</li>
|
<li className='lc' tabIndex="0" onClick={this._multiPurpose.bind(this, false, 0)} onKeyDown={this._keyDown}
|
||||||
<li className='lc' tabIndex="0" onClick={this._nRated.bind(this, '2', undefined)}>{translate('D-rated')}</li>
|
ref={smRef => this.sectionRefArr['multipurpose'] = smRef}>{translate('Multi-purpose')}</li>
|
||||||
<li className='lc' tabIndex="0" onClick={this._nRated.bind(this, '2', '5')}>{translate('D-rated + A-rated FSD/PP')}</li>
|
<li className='lc' tabIndex="0" onClick={this._multiPurpose.bind(this, true, 2)} onKeyDown={this._keyDown}
|
||||||
|
ref={smRef => this.sectionRefArr['combat'] = smRef}>{translate('Combat')}</li>
|
||||||
|
<li className='lc' tabIndex="0" onClick={this._optimizeCargo.bind(this, true)} onKeyDown={this._keyDown}
|
||||||
|
ref={smRef => this.sectionRefArr['trader'] = smRef}>{translate('Trader')}</li>
|
||||||
|
<li className='lc' tabIndex="0" onClick={this._optimizeExplorer.bind(this, false)} onKeyDown={this._keyDown}
|
||||||
|
ref={smRef => this.sectionRefArr['explorer'] = smRef}>{translate('Explorer')}</li>
|
||||||
|
<li className={cn('lc', { disabled: planetaryDisabled })} tabIndex={planetaryDisabled ? '' : '0'}
|
||||||
|
onClick={!planetaryDisabled && this._optimizeExplorer.bind(this, true)} onKeyDown={this._keyDown}
|
||||||
|
ref={smRef => this.sectionRefArr['planetary'] = smRef}>{translate('Planetary Explorer')}</li>
|
||||||
|
<li className='lc' tabIndex="0" onClick={this._optimizeMiner.bind(this, true)} onKeyDown={this._keyDown}
|
||||||
|
ref={smRef => this.sectionRefArr['miner'] = smRef}>{translate('Miner')}</li>
|
||||||
|
<li className='lc' tabIndex="0" onClick={this._optimizeRacer.bind(this)} onKeyDown={this._keyDown}
|
||||||
|
ref={smRef => this.sectionRefArr['racer'] = smRef}>{translate('Racer')}</li>
|
||||||
|
<li className='lc' tabIndex="0" onClick={() => this.setState({ showDW2: !this.state.showDW2 })}
|
||||||
|
onKeyDown={this._keyDown}
|
||||||
|
ref={smRef => this.sectionRefArr['dw2'] = smRef}>{translate('DW2')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ export class MatIcon extends SvgIcon {
|
|||||||
*/
|
*/
|
||||||
svg() {
|
svg() {
|
||||||
return<g xmlns="http://www.w3.org/2000/svg">
|
return<g xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M 24.86,4.18
|
<path fill="#FF7100" d="M 24.86,4.18
|
||||||
C 24.86,4.18 17.17,7.82 17.17,7.82
|
C 24.86,4.18 17.17,7.82 17.17,7.82
|
||||||
17.17,7.82 15.35,14.55 15.35,14.55
|
17.17,7.82 15.35,14.55 15.35,14.55
|
||||||
15.35,14.55 24.70,9.75 24.70,9.75
|
15.35,14.55 24.70,9.75 24.70,9.75
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import TranslatedComponent from './TranslatedComponent';
|
|||||||
* Document Root Tooltip
|
* Document Root Tooltip
|
||||||
*/
|
*/
|
||||||
export default class Tooltip extends TranslatedComponent {
|
export default class Tooltip extends TranslatedComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
rect: PropTypes.object.isRequired,
|
rect: PropTypes.object.isRequired,
|
||||||
options: PropTypes.object
|
options: PropTypes.object
|
||||||
@@ -126,4 +127,5 @@ export default class Tooltip extends TranslatedComponent {
|
|||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { shallowEqual } from '../utils/UtilityFunctions';
|
|||||||
* Abstract Translated Component
|
* Abstract Translated Component
|
||||||
*/
|
*/
|
||||||
export default class TranslatedComponent extends React.Component {
|
export default class TranslatedComponent extends React.Component {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
language: PropTypes.object.isRequired,
|
language: PropTypes.object.isRequired,
|
||||||
sizeRatio: PropTypes.number.isRequired,
|
sizeRatio: PropTypes.number.isRequired,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SlotSection from './SlotSection';
|
import SlotSection from './SlotSection';
|
||||||
import Slot from './Slot';
|
import HardpointSlot from './HardpointSlot';
|
||||||
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
import { stopCtxPropagation } from '../utils/UtilityFunctions';
|
||||||
import autoBind from 'auto-bind';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility Slot Section
|
* Utility Slot Section
|
||||||
@@ -11,59 +10,88 @@ export default class UtilitySlotSection extends SlotSection {
|
|||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
* @param {Object} props React Component properties
|
* @param {Object} props React Component properties
|
||||||
|
* @param {Object} context React Component context
|
||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props, context) {
|
||||||
super(props, 'utility mounts');
|
super(props, context, 'utility', 'utility mounts');
|
||||||
autoBind(this);
|
this._empty = this._empty.bind(this);
|
||||||
|
this.selectedRefId = null;
|
||||||
|
this.firstRefId = 'emptyall';
|
||||||
|
this.lastRefId = 'po';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Handle focus if the component updates
|
||||||
|
* @param {Object} prevProps React Component properties
|
||||||
|
*/
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
this._handleSectionFocus(prevProps,this.firstRefId, this.lastRefId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Empty all utility slots and close the menu
|
* Empty all utility slots and close the menu
|
||||||
*/
|
*/
|
||||||
_empty() {
|
_empty() {
|
||||||
this.props.ship.getUtilities().forEach((slot) => slot.reset());
|
this.selectedRefId = this.firstRefId;
|
||||||
|
this.props.ship.emptyUtility();
|
||||||
|
this.props.onChange();
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mount module in utility slot, replace all if Alt is held
|
* Mount module in utility slot, replace all if Alt is held
|
||||||
* @param {string} type Module type
|
* @param {string} group Module Group name
|
||||||
* @param {string} rating Module Rating
|
* @param {string} rating Module Rating
|
||||||
|
* @param {string} name Module name
|
||||||
* @param {Synthetic} event Event
|
* @param {Synthetic} event Event
|
||||||
*/
|
*/
|
||||||
_use(type, rating, event) {
|
_use(group, rating, name, event) {
|
||||||
const fillAll = event.getModifierState('Alt');
|
this.selectedRefId = group;
|
||||||
for (const slot of this.props.ship.getUtilities(undefined, true)) {
|
if (rating !== null) this.selectedRefId += '-' + rating;
|
||||||
if (slot.isEmpty() || fillAll) {
|
|
||||||
slot.setItem(type, '', rating);
|
this.props.ship.useUtility(group, rating, name, event.getModifierState('Alt'));
|
||||||
}
|
this.props.onChange();
|
||||||
}
|
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty all utility slots on right-click
|
||||||
|
*/
|
||||||
|
_contextMenu() {
|
||||||
|
this._empty();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create all HardpointSlots (React component) for the slots
|
* Create all HardpointSlots (React component) for the slots
|
||||||
* @return {Array} Array of HardpointSlots
|
* @return {Array} Array of HardpointSlots
|
||||||
*/
|
*/
|
||||||
_getSlots() {
|
_getSlots() {
|
||||||
let slots = [];
|
let slots = [];
|
||||||
let { ship, currentMenu, propsToShow, onPropToggle } = this.props;
|
let { ship, currentMenu } = this.props;
|
||||||
|
let hardpoints = ship.hardpoints;
|
||||||
let { originSlot, targetSlot } = this.state;
|
let { originSlot, targetSlot } = this.state;
|
||||||
|
let availableModules = ship.getAvailableModules();
|
||||||
|
|
||||||
for (let h of ship.getUtilities(undefined, true)) {
|
for (let i = 0, l = hardpoints.length; i < l; i++) {
|
||||||
slots.push(<Slot
|
let h = hardpoints[i];
|
||||||
key={h.object.Slot}
|
if (h.maxClass === 0) {
|
||||||
currentMenu={currentMenu}
|
slots.push(<HardpointSlot
|
||||||
|
key={i}
|
||||||
|
maxClass={h.maxClass}
|
||||||
|
availableModules={() => availableModules.getHps(h.maxClass)}
|
||||||
|
onOpen={this._openMenu.bind(this,h)}
|
||||||
|
onSelect={this._selectModule.bind(this, h)}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
selected={currentMenu == h}
|
||||||
drag={this._drag.bind(this, h)}
|
drag={this._drag.bind(this, h)}
|
||||||
dragOver={this._dragOverSlot.bind(this, h)}
|
dragOver={this._dragOverSlot.bind(this, h)}
|
||||||
drop={this._drop}
|
drop={this._drop}
|
||||||
dropClass={this._dropClass(h, originSlot, targetSlot)}
|
dropClass={this._dropClass(h, originSlot, targetSlot)}
|
||||||
m={h}
|
ship={ship}
|
||||||
|
m={h.m}
|
||||||
enabled={h.enabled ? true : false}
|
enabled={h.enabled ? true : false}
|
||||||
propsToShow={propsToShow}
|
|
||||||
onPropToggle={onPropToggle}
|
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return slots;
|
return slots;
|
||||||
}
|
}
|
||||||
@@ -73,34 +101,33 @@ export default class UtilitySlotSection extends SlotSection {
|
|||||||
* @param {Function} translate Translate function
|
* @param {Function} translate Translate function
|
||||||
* @return {React.Component} Section menu
|
* @return {React.Component} Section menu
|
||||||
*/
|
*/
|
||||||
_getSectionMenu() {
|
_getSectionMenu(translate) {
|
||||||
const { translate } = this.context.language;
|
|
||||||
let _use = this._use;
|
let _use = this._use;
|
||||||
|
|
||||||
return <div className='select' onClick={(e) => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
|
return <div className='select' onClick={(e) => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='lc' tabIndex='0' onClick={this._empty}>{translate('empty all')}</li>
|
<li className='lc' tabIndex='0' onClick={this._empty} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['emptyall'] = smRef}>{translate('empty all')}</li>
|
||||||
<li className='optional-hide' style={{ textAlign: 'center', marginTop: '1em' }}>{translate('PHRASE_ALT_ALL')}</li>
|
<li className='optional-hide' style={{ textAlign: 'center', marginTop: '1em' }}>{translate('PHRASE_ALT_ALL')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('sb')}</div>
|
<div className='select-group cap'>{translate('sb')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'shieldbooster', '5')}>A</li>
|
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'A', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-A'] = smRef}>A</li>
|
||||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'shieldbooster', '4')}>B</li>
|
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'B', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-B'] = smRef}>B</li>
|
||||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'shieldbooster', '3')}>C</li>
|
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'C', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-C'] = smRef}>C</li>
|
||||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'shieldbooster', '2')}>D</li>
|
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'D', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-D'] = smRef}>D</li>
|
||||||
<li className='c' tabIndex='0' onClick={_use.bind(this, 'shieldbooster', '1')}>E</li>
|
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'E', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-E'] = smRef}>E</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('hs')}</div>
|
<div className='select-group cap'>{translate('hs')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'heatsinklauncher', '')}>{translate('Heat Sink Launcher')}</li>
|
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'hs', null, 'Heat Sink Launcher')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['hs'] = smRef}>{translate('Heat Sink Launcher')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('ch')}</div>
|
<div className='select-group cap'>{translate('ch')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'chafflauncher', '')}>{translate('Chaff Launcher')}</li>
|
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'ch', null, 'Chaff Launcher')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ch'] = smRef}>{translate('Chaff Launcher')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className='select-group cap'>{translate('po')}</div>
|
<div className='select-group cap'>{translate('po')}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'pointdefence', '')}>{translate('Point Defence')}</li>
|
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'po', null, 'Point Defence')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['po'] = smRef}>{translate('Point Defence')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,99 +2,194 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TranslatedComponent from './TranslatedComponent';
|
import TranslatedComponent from './TranslatedComponent';
|
||||||
import LineChart from '../components/LineChart';
|
import LineChart from '../components/LineChart';
|
||||||
import { moduleReduce } from 'ed-forge/lib/src/helper';
|
import * as Calc from '../shipyard/Calculations';
|
||||||
import { chain, keys, mapValues, values } from 'lodash';
|
|
||||||
|
|
||||||
const DAMAGE_DEALT_COLORS = ['#FFFFFF', '#FF0000', '#00FF00', '#7777FF', '#FFFF00', '#FF00FF', '#00FFFF', '#777777'];
|
const DAMAGE_DEALT_COLORS = ['#FFFFFF', '#FF0000', '#00FF00', '#7777FF', '#FFFF00', '#FF00FF', '#00FFFF', '#777777'];
|
||||||
const PORTION_MAPPINGS = {
|
|
||||||
'absolute': 'absolutedamageportion',
|
|
||||||
'explosive': 'explosivedamageportion',
|
|
||||||
'kinetic': 'kineticdamageportion',
|
|
||||||
'thermal': 'thermicdamageportion',
|
|
||||||
};
|
|
||||||
const MULTS = keys(PORTION_MAPPINGS);
|
|
||||||
|
|
||||||
// TODO: help with this in ed-forge
|
|
||||||
/**
|
|
||||||
* .
|
|
||||||
* @param {Object} opponentDefence .
|
|
||||||
* @returns {Object} .
|
|
||||||
*/
|
|
||||||
function defenceToMults(opponentDefence) {
|
|
||||||
return chain(opponentDefence)
|
|
||||||
.pick(MULTS)
|
|
||||||
.mapKeys((v, k) => PORTION_MAPPINGS[k])
|
|
||||||
.mapValues((resistanceProfile) => resistanceProfile.damageMultiplier)
|
|
||||||
.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Weapon damage chart
|
* Weapon damage chart
|
||||||
*/
|
*/
|
||||||
export default class WeaponDamageChart extends TranslatedComponent {
|
export default class WeaponDamageChart extends TranslatedComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
code: PropTypes.string.isRequired,
|
|
||||||
ship: PropTypes.object.isRequired,
|
ship: PropTypes.object.isRequired,
|
||||||
opponentDefence: PropTypes.object.isRequired,
|
opponent: PropTypes.object.isRequired,
|
||||||
|
hull: PropTypes.bool.isRequired,
|
||||||
engagementRange: PropTypes.number.isRequired,
|
engagementRange: PropTypes.number.isRequired,
|
||||||
|
opponentSys: PropTypes.number.isRequired,
|
||||||
|
marker: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param {Object} props React Component properties
|
||||||
|
* @param {Object} context React Component context
|
||||||
|
*/
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the initial weapons state
|
||||||
|
*/
|
||||||
|
componentWillMount() {
|
||||||
|
const weaponNames = this._weaponNames(this.props.ship, this.context);
|
||||||
|
const opponentShields = Calc.shieldMetrics(this.props.opponent, this.props.opponentSys);
|
||||||
|
const opponentArmour = Calc.armourMetrics(this.props.opponent);
|
||||||
|
const maxRange = this._calcMaxRange(this.props.ship);
|
||||||
|
const maxDps = this._calcMaxSDps(this.props.ship, this.props.opponent, opponentShields, opponentArmour);
|
||||||
|
|
||||||
|
this.setState({ maxRange, maxDps, weaponNames, opponentShields, opponentArmour, calcSDpsFunc: this._calcSDps.bind(this, this.props.ship, weaponNames, this.props.opponent, opponentShields, opponentArmour, this.props.hull) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the updated weapons state if our ship changes
|
||||||
|
* @param {Object} nextProps Incoming/Next properties
|
||||||
|
* @param {Object} nextContext Incoming/Next conext
|
||||||
|
* @return {boolean} Returns true if the component should be rerendered
|
||||||
|
*/
|
||||||
|
componentWillReceiveProps(nextProps, nextContext) {
|
||||||
|
if (nextProps.marker != this.props.marker) {
|
||||||
|
const weaponNames = this._weaponNames(nextProps.ship, nextContext);
|
||||||
|
const opponentShields = Calc.shieldMetrics(nextProps.opponent, nextProps.opponentSys);
|
||||||
|
const opponentArmour = Calc.armourMetrics(nextProps.opponent);
|
||||||
|
const maxRange = this._calcMaxRange(nextProps.ship);
|
||||||
|
const maxDps = this._calcMaxSDps(nextProps.ship, nextProps.opponent, opponentShields, opponentArmour);
|
||||||
|
this.setState({ weaponNames,
|
||||||
|
opponentShields,
|
||||||
|
opponentArmour,
|
||||||
|
maxRange,
|
||||||
|
maxDps,
|
||||||
|
calcSDpsFunc: this._calcSDps.bind(this, nextProps.ship, weaponNames, nextProps.opponent, opponentShields, opponentArmour, nextProps.hull)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the maximum range of a ship's weapons
|
||||||
|
* @param {Object} ship The ship
|
||||||
|
* @returns {int} The maximum range, in metres
|
||||||
|
*/
|
||||||
|
_calcMaxRange(ship) {
|
||||||
|
let maxRange = 1000; // Minimum
|
||||||
|
for (let i = 0; i < ship.hardpoints.length; i++) {
|
||||||
|
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
|
||||||
|
const thisRange = ship.hardpoints[i].m.getRange();
|
||||||
|
if (thisRange > maxRange) {
|
||||||
|
maxRange = thisRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the maximum sustained single-weapon DPS for this ship
|
||||||
|
* @param {Object} ship The ship
|
||||||
|
* @param {Object} opponent The opponent ship
|
||||||
|
* @param {Object} opponentShields The opponent's shields
|
||||||
|
* @param {Object} opponentArmour The opponent's armour
|
||||||
|
* @return {number} The maximum sustained single-weapon DPS
|
||||||
|
*/
|
||||||
|
_calcMaxSDps(ship, opponent, opponentShields, opponentArmour) {
|
||||||
|
// Additional information to allow effectiveness calculations
|
||||||
|
let maxSDps = 0;
|
||||||
|
for (let i = 0; i < ship.hardpoints.length; i++) {
|
||||||
|
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
|
||||||
|
const m = ship.hardpoints[i].m;
|
||||||
|
|
||||||
|
const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, 0);
|
||||||
|
const thisSDps = sustainedDps.damage.armour.total > sustainedDps.damage.shields.total ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total;
|
||||||
|
if (thisSDps > maxSDps) {
|
||||||
|
maxSDps = thisSDps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxSDps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain the weapon names for this ship
|
||||||
|
* @param {Object} ship The ship
|
||||||
|
* @param {Object} context The context
|
||||||
|
* @return {array} The weapon names
|
||||||
|
*/
|
||||||
|
_weaponNames(ship, context) {
|
||||||
|
const translate = context.language.translate;
|
||||||
|
let names = [];
|
||||||
|
let num = 1;
|
||||||
|
for (let i = 0; i < ship.hardpoints.length; i++) {
|
||||||
|
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
|
||||||
|
const m = ship.hardpoints[i].m;
|
||||||
|
let name = '' + num++ + ': ' + m.class + m.rating + (m.missile ? '/' + m.missile : '') + ' ' + translate(m.name || m.grp);
|
||||||
|
let engineering;
|
||||||
|
if (m.blueprint && m.blueprint.name) {
|
||||||
|
engineering = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
|
||||||
|
if (m.blueprint.special && m.blueprint.special.id) {
|
||||||
|
engineering += ', ' + translate(m.blueprint.special.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (engineering) {
|
||||||
|
name = name + ' (' + engineering + ')';
|
||||||
|
}
|
||||||
|
names.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the per-weapon sustained DPS for this ship against another ship at a given range
|
||||||
|
* @param {Object} ship The ship
|
||||||
|
* @param {Object} weaponNames The names of the weapons for which to calculate DPS
|
||||||
|
* @param {Object} opponent The target
|
||||||
|
* @param {Object} opponentShields The opponent's shields
|
||||||
|
* @param {Object} opponentArmour The opponent's armour
|
||||||
|
* @param {bool} hull true if to calculate against hull, false if to calculate against shields
|
||||||
|
* @param {Object} engagementRange The engagement range
|
||||||
|
* @return {array} The array of weapon DPS
|
||||||
|
*/
|
||||||
|
_calcSDps(ship, weaponNames, opponent, opponentShields, opponentArmour, hull, engagementRange) {
|
||||||
|
let results = {};
|
||||||
|
let weaponNum = 0;
|
||||||
|
for (let i = 0; i < ship.hardpoints.length; i++) {
|
||||||
|
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
|
||||||
|
const m = ship.hardpoints[i].m;
|
||||||
|
const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementRange);
|
||||||
|
results[weaponNames[weaponNum++]] = hull ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render damage dealt
|
* Render damage dealt
|
||||||
* @return {React.Component} contents
|
* @return {React.Component} contents
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { language } = this.context;
|
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
|
||||||
const { translate } = language;
|
const { formats, translate, units } = language;
|
||||||
const { code, ship, opponentDefence, engagementRange } = this.props;
|
const { maxRange } = this.state;
|
||||||
|
const { ship, opponent, engagementRange } = this.props;
|
||||||
|
|
||||||
const hardpoints = ship.getHardpoints();
|
const sortOrder = this._sortOrder;
|
||||||
const hardpointsMap = chain(hardpoints)
|
const onCollapseExpand = this._onCollapseExpand;
|
||||||
.map((m) => [m.getSlot(), m])
|
|
||||||
.fromPairs()
|
const code = `${ship.toString()}:${opponent.toString()}`;
|
||||||
.value();
|
|
||||||
const mults = defenceToMults(opponentDefence);
|
|
||||||
const cb = (range) => {
|
|
||||||
return mapValues(
|
|
||||||
hardpointsMap,
|
|
||||||
(m) => {
|
|
||||||
const sdps = m.get('sustaineddamagepersecond', true);
|
|
||||||
const resistanceMul = chain(mults)
|
|
||||||
.toPairs()
|
|
||||||
.map((pair) => {
|
|
||||||
const [k, mul] = pair;
|
|
||||||
return m.get(k, true) * mul;
|
|
||||||
})
|
|
||||||
.sum()
|
|
||||||
.value();
|
|
||||||
|
|
||||||
const falloff = m.get('damagefalloffrange', true);
|
|
||||||
const rangeMul = Math.min(1, Math.max(0,
|
|
||||||
1 - (range - falloff) / (m.get('maximumrange', true) - falloff)
|
|
||||||
));
|
|
||||||
return sdps * resistanceMul * rangeMul;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<LineChart
|
<LineChart
|
||||||
xMin={0}
|
xMax={maxRange}
|
||||||
xMax={moduleReduce(
|
yMax={this.state.maxDps}
|
||||||
hardpoints, 'maximumrange', true, (a, v) => Math.max(a, v), 1000,
|
|
||||||
)}
|
|
||||||
yMin={0}
|
|
||||||
// Factor in highest damage multiplier to get a safe upper bound
|
|
||||||
yMax={Math.max(1, ...values(mults)) * moduleReduce(
|
|
||||||
hardpoints, 'sustaineddamagepersecond', true, (a, v) => Math.max(a, v), 0,
|
|
||||||
)}
|
|
||||||
xLabel={translate('range')}
|
xLabel={translate('range')}
|
||||||
xUnit={translate('m')}
|
xUnit={translate('m')}
|
||||||
yLabel={translate('sustaineddamagepersecond')}
|
yLabel={translate('sdps')}
|
||||||
series={hardpoints.map((m) => m.getSlot())}
|
series={this.state.weaponNames}
|
||||||
xMark={engagementRange}
|
xMark={this.props.engagementRange}
|
||||||
colors={DAMAGE_DEALT_COLORS}
|
colors={DAMAGE_DEALT_COLORS}
|
||||||
func={cb}
|
func={this.state.calcSDpsFunc}
|
||||||
points={200}
|
points={200}
|
||||||
code={code}
|
code={code}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import * as IT from './it';
|
|||||||
import * as RU from './ru';
|
import * as RU from './ru';
|
||||||
import * as PL from './pl';
|
import * as PL from './pl';
|
||||||
import * as PT from './pt';
|
import * as PT from './pt';
|
||||||
import * as CN from './cn';
|
|
||||||
import * as d3 from 'd3';
|
import * as d3 from 'd3';
|
||||||
|
|
||||||
let fallbackTerms = EN.terms;
|
let fallbackTerms = EN.terms;
|
||||||
@@ -28,7 +27,6 @@ export function getLanguage(langCode) {
|
|||||||
case 'ru': lang = RU; break;
|
case 'ru': lang = RU; break;
|
||||||
case 'pl': lang = PL; break;
|
case 'pl': lang = PL; break;
|
||||||
case 'pt': lang = PT; break;
|
case 'pt': lang = PT; break;
|
||||||
case 'cn': lang = CN; break;
|
|
||||||
default:
|
default:
|
||||||
lang = EN;
|
lang = EN;
|
||||||
}
|
}
|
||||||
@@ -96,6 +94,5 @@ export const Languages = {
|
|||||||
fr: 'Français',
|
fr: 'Français',
|
||||||
ru: 'ру́сский',
|
ru: 'ру́сский',
|
||||||
pl: 'polski',
|
pl: 'polski',
|
||||||
pt: 'português',
|
pt: 'português'
|
||||||
cn: '中文'
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
export const formats = {
|
|
||||||
decimal: '.',
|
|
||||||
thousands: ',',
|
|
||||||
grouping: [3],
|
|
||||||
currency: ['¥', ''],
|
|
||||||
dateTime: '%a %b %e %X %Y',
|
|
||||||
date: '%Y年%m月%d日',
|
|
||||||
time: '%H:%M:%S',
|
|
||||||
periods: ['AM', 'PM'],
|
|
||||||
days: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
|
|
||||||
shortDays: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
|
|
||||||
months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
|
|
||||||
shortMonths: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
|
|
||||||
};
|
|
||||||
|
|
||||||
export { default as terms } from './cn.json';
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -63,7 +63,7 @@
|
|||||||
"TT_SUMMARY_SPEED": "With full fuel tank and 4 pips to ENG",
|
"TT_SUMMARY_SPEED": "With full fuel tank and 4 pips to ENG",
|
||||||
"TT_SUMMARY_SPEED_NONFUNCTIONAL": "Thrusters powered off or over maximum mass with full fuel and cargo loads",
|
"TT_SUMMARY_SPEED_NONFUNCTIONAL": "Thrusters powered off or over maximum mass with full fuel and cargo loads",
|
||||||
"TT_SUMMARY_BOOST": "With full fuel tank and 4 pips to ENG",
|
"TT_SUMMARY_BOOST": "With full fuel tank and 4 pips to ENG",
|
||||||
"TT_SUMMARY_BOOST_INTERVAL": "Time between each boost with 4 pips to ENG",
|
"TT_SUMMARY_BOOST_TIME": "Time between each boost with 4 pips to ENG",
|
||||||
"TT_SUMMARY_BOOST_NONFUNCTIONAL": "Power distributor not able to supply enough power to boost",
|
"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": "Raw shield strength, including boosters",
|
||||||
"TT_SUMMARY_SHIELDS_SCB": "Raw shield strength, including boosters and SCBs",
|
"TT_SUMMARY_SHIELDS_SCB": "Raw shield strength, including boosters and SCBs",
|
||||||
@@ -81,8 +81,6 @@
|
|||||||
"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_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",
|
"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",
|
"HELP_MODIFICATIONS_MENU": "Click on a number to enter a new value, or drag along the bar for small changes",
|
||||||
"PHRASE_FAIL_EDENGINEER": "Failed to send to EDEngineer (Launch EDEngineer and make sure the API is started then refresh the page.)",
|
|
||||||
"PHRASE_FIREFOX_EDENGINEER": "Send to EDEngineer is incompatible with Firefox.",
|
|
||||||
"am": "Auto Field-Maintenance Unit",
|
"am": "Auto Field-Maintenance Unit",
|
||||||
"bh": "Bulkheads",
|
"bh": "Bulkheads",
|
||||||
"bl": "Beam Laser",
|
"bl": "Beam Laser",
|
||||||
@@ -153,7 +151,6 @@
|
|||||||
"sfn": "Shutdown Field Neutraliser",
|
"sfn": "Shutdown Field Neutraliser",
|
||||||
"sg": "Shield Generator",
|
"sg": "Shield Generator",
|
||||||
"ss": "Surface Scanners",
|
"ss": "Surface Scanners",
|
||||||
"sua": "Supercruise Assist",
|
|
||||||
"t": "thrusters",
|
"t": "thrusters",
|
||||||
"tp": "Torpedo Pylon",
|
"tp": "Torpedo Pylon",
|
||||||
"ul": "Burst Laser",
|
"ul": "Burst Laser",
|
||||||
@@ -175,7 +172,6 @@
|
|||||||
"ammunition": "Ammo",
|
"ammunition": "Ammo",
|
||||||
"secs": "s",
|
"secs": "s",
|
||||||
"rebuildsperbay": "Rebuilds per bay",
|
"rebuildsperbay": "Rebuilds per bay",
|
||||||
"mroll": "Roll",
|
|
||||||
"worst": "Worst",
|
"worst": "Worst",
|
||||||
"average": "Average",
|
"average": "Average",
|
||||||
"random": "Random",
|
"random": "Random",
|
||||||
@@ -205,7 +201,7 @@
|
|||||||
"internal protection": "Internal protection",
|
"internal protection": "Internal protection",
|
||||||
"external protection": "External protection",
|
"external protection": "External protection",
|
||||||
"engagement range": "Engagement range",
|
"engagement range": "Engagement range",
|
||||||
"boost interval": "Boost interval",
|
"boost time": "Boost time",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"ammo": "Ammunition maximum",
|
"ammo": "Ammunition maximum",
|
||||||
"boot": "Boot time",
|
"boot": "Boot time",
|
||||||
@@ -324,7 +320,6 @@
|
|||||||
"never": "never",
|
"never": "never",
|
||||||
"stock": "stock",
|
"stock": "stock",
|
||||||
"boost": "boost",
|
"boost": "boost",
|
||||||
"tab_defence": "defence",
|
|
||||||
"federation rank 1": "Recruit",
|
"federation rank 1": "Recruit",
|
||||||
"federation rank 2": "Cadet",
|
"federation rank 2": "Cadet",
|
||||||
"federation rank 3": "Midshipman",
|
"federation rank 3": "Midshipman",
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
"empty all": "vide tout",
|
"empty all": "vide tout",
|
||||||
"Enter Name": "Entrer nom",
|
"Enter Name": "Entrer nom",
|
||||||
"Explorer": "explorateur",
|
"Explorer": "explorateur",
|
||||||
"farthest range": "gamme la plus rapide",
|
"fastest range": "gamme la plus rapide",
|
||||||
"fuel": "carburant",
|
"fuel": "carburant",
|
||||||
"fuel level": "niveau de carburant",
|
"fuel level": "niveau de carburant",
|
||||||
"full tank": "Réservoir plein",
|
"full tank": "Réservoir plein",
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ export const formats = {
|
|||||||
decimal: ',',
|
decimal: ',',
|
||||||
thousands: '.',
|
thousands: '.',
|
||||||
grouping: [3],
|
grouping: [3],
|
||||||
currency: ['$', ''],
|
currency: ['', ' €'],
|
||||||
dateTime: '%A, %e de %B de %Y, %X',
|
dateTime: '%A, %e de %B de %Y, %X',
|
||||||
date: '%d/%m/%Y',
|
date: '%d/%m/%Y',
|
||||||
time: '%H:%M:%S',
|
time: '%H:%M:%S',
|
||||||
periods: ['AM', 'PM'],
|
periods: ['AM', 'PM'],
|
||||||
days: ['domingo', 'segunda', 'terça', 'quarta', 'quinta', 'sexta', 'sábado'],
|
days: ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'],
|
||||||
shortDays: ['dom', 'seg', 'ter', 'qua', 'qui', 'sex', 'sab'],
|
shortDays: ['dom', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb'],
|
||||||
months: ['janeiro', 'fevereiro', 'março', 'abril', 'maio', 'junho', 'julho', 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro'],
|
months: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
|
||||||
shortMonths: ['jan', 'fev', 'mar', 'abr', 'mai', 'jun', 'jul', 'ago', 'set', 'out', 'nov', 'dez']
|
shortMonths: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic']
|
||||||
};
|
};
|
||||||
|
|
||||||
export { default as terms } from './pt.json';
|
export { default as terms } from './pt.json';
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
|||||||
"PHRASE_EXPORT_DESC": "Детальный JSON-экспорт вашей сборки для использования в других местах и инструментах",
|
"PHRASE_EXPORT_DESC": "Детальный JSON-экспорт вашей сборки для использования в других местах и инструментах",
|
||||||
"PHRASE_FASTEST_RANGE": "Последовательные прыжки максимальной дальности",
|
"PHRASE_FASTEST_RANGE": "Последовательные прыжки максимальной дальности",
|
||||||
"PHRASE_IMPORT": "Для импорта вставьте код в эту форму",
|
"PHRASE_IMPORT": "Для импорта вставьте код в эту форму",
|
||||||
"PHRASE_LADEN": "Масса корабля с учетом топлива и грузов",
|
"PHRASE_LADEN": "Масса корабля с учётом топлива и грузов",
|
||||||
"PHRASE_NO_BUILDS": "Нечего сравнивать",
|
"PHRASE_NO_BUILDS": "Нечего сравнивать",
|
||||||
"PHRASE_NO_RETROCH": "Нет ранних версий сборки",
|
"PHRASE_NO_RETROCH": "Нет ранних версий сборки",
|
||||||
"PHRASE_SELECT_BUILDS": "Выберите конфигурацию для сравнения",
|
"PHRASE_SELECT_BUILDS": "Выберите конфигурацию для сравнения",
|
||||||
@@ -13,21 +13,18 @@
|
|||||||
"PHRASE_SG_RECOVER": "Восстановление с 0% до 50% объема щита, учитывая полный аккумулятор СИС в начале",
|
"PHRASE_SG_RECOVER": "Восстановление с 0% до 50% объема щита, учитывая полный аккумулятор СИС в начале",
|
||||||
"PHRASE_UNLADEN": "Масса корабля без учета топлива и грузов",
|
"PHRASE_UNLADEN": "Масса корабля без учета топлива и грузов",
|
||||||
"PHRASE_UPDATE_RDY": "Доступна новая версия. Нажмите для обновления.",
|
"PHRASE_UPDATE_RDY": "Доступна новая версия. Нажмите для обновления.",
|
||||||
"PHRASE_ENGAGEMENT_RANGE": "Дистанция между кораблем и целью",
|
"PHRASE_ENGAGEMENT_RANGE": "Дистанция между кораблём и целью",
|
||||||
"PHRASE_SELECT_BLUEPRINT": "Нажмите чтобы выбрать чертеж",
|
"PHRASE_SELECT_BLUEPRINT": "Нажмите чтобы выбрать чертёж",
|
||||||
"PHRASE_BLUEPRINT_WORST": "Худшие основные значения для чертежа",
|
"PHRASE_BLUEPRINT_WORST": "Худшие основные значения для чертежа",
|
||||||
"PHRASE_BLUEPRINT_FIFTY": "50% значения для чертежа",
|
|
||||||
"PHRASE_BLUEPRINT_SEVEN_FIVE": "75% значения для чертежа",
|
|
||||||
"PHRASE_BLUEPRINT_RANDOM": "Случайный выбор между худшими и лучшими значениями для этого чертежа",
|
"PHRASE_BLUEPRINT_RANDOM": "Случайный выбор между худшими и лучшими значениями для этого чертежа",
|
||||||
"PHRASE_BLUEPRINT_BEST": "Лучшие основные значения для чертежа",
|
"PHRASE_BLUEPRINT_BEST": "Лучшие основные значения для чертежа",
|
||||||
"PHRASE_BLUEPRINT_EXTREME": "Лучшие положительные и худшие отрицательные основные значения для чертежа",
|
"PHRASE_BLUEPRINT_EXTREME": "Лучшие положительные и худшие отрицательные основные значения для чертежа",
|
||||||
"PHRASE_BLUEPRINT_RESET": "Сбросить все модификаторы и чертеж",
|
"PHRASE_BLUEPRINT_RESET": "Убрать все изменения и чертёж",
|
||||||
"PHRASE_SELECT_SPECIAL": "Нажмите, чтобы выбрать экспериментальный эффект",
|
"PHRASE_SELECT_SPECIAL": "Нажмите, чтобы выбрать экспериментальный эффект",
|
||||||
"PHRASE_NO_SPECIAL": "Без экспериментального эффекта",
|
"PHRASE_NO_SPECIAL": "Без экспериментального эффекта",
|
||||||
"PHRASE_SHOPPING_LIST": "Станции, что продают эту сборку",
|
"PHRASE_SHOPPING_LIST": "Станции, что продают эту сборку",
|
||||||
"PHRASE_SHOPPING_MATS": "Материалы которые нужны для сборки",
|
|
||||||
"PHRASE_REFIT_SHOPPING_LIST": "Станции, что продают необходимые модули",
|
"PHRASE_REFIT_SHOPPING_LIST": "Станции, что продают необходимые модули",
|
||||||
"PHRASE_TOTAL_EFFECTIVE_SHIELD": "Общий урон, что может быть нанесен в каждым типе, если используются все щитонакопители",
|
"PHRASE_TOTAL_EFFECTIVE_SHIELD": "Общий урон, что может быть нанесён в каждым типе, если используются все щитонакопители",
|
||||||
"PHRASE_TIME_TO_LOSE_SHIELDS": "Щиты продержатся",
|
"PHRASE_TIME_TO_LOSE_SHIELDS": "Щиты продержатся",
|
||||||
"PHRASE_TIME_TO_RECOVER_SHIELDS": "Щиты восстановятся за",
|
"PHRASE_TIME_TO_RECOVER_SHIELDS": "Щиты восстановятся за",
|
||||||
"PHRASE_TIME_TO_RECHARGE_SHIELDS": "Щиты будут заряжены за",
|
"PHRASE_TIME_TO_RECHARGE_SHIELDS": "Щиты будут заряжены за",
|
||||||
@@ -37,59 +34,51 @@
|
|||||||
"PHRASE_EFFECTIVE_ARMOUR": "Эффективная сила брони против разных типов урона",
|
"PHRASE_EFFECTIVE_ARMOUR": "Эффективная сила брони против разных типов урона",
|
||||||
"PHRASE_DAMAGE_TAKEN": "% общих повреждений полученных в разных типах урона",
|
"PHRASE_DAMAGE_TAKEN": "% общих повреждений полученных в разных типах урона",
|
||||||
"PHRASE_TIME_TO_LOSE_ARMOUR": "Броня продержится",
|
"PHRASE_TIME_TO_LOSE_ARMOUR": "Броня продержится",
|
||||||
"PHRASE_MODULE_PROTECTION_EXTERNAL": "Защита гнезд",
|
"PHRASE_MODULE_PROTECTION_EXTERNAL": "Защита гнёзд",
|
||||||
"PHRASE_MODULE_PROTECTION_INTERNAL": "Защита всех остальных модулей",
|
"PHRASE_MODULE_PROTECTION_INTERNAL": "Защита всех остальных модулей",
|
||||||
"PHRASE_OVERALL_DAMAGE": "Разбивка источников устойчивого ДПС",
|
|
||||||
"PHRASE_SHIELD_DAMAGE": "Подробности источников поддерживаемого ДПС против щитов",
|
"PHRASE_SHIELD_DAMAGE": "Подробности источников поддерживаемого ДПС против щитов",
|
||||||
"PHRASE_ARMOUR_DAMAGE": "Подробности источников поддерживаемого ДПС против брони",
|
"PHRASE_ARMOUR_DAMAGE": "Подробности источников поддерживаемого ДПС против брони",
|
||||||
"PHRASE_TIME_TO_REMOVE_SHIELDS": "Снимет щиты за",
|
"PHRASE_TIME_TO_REMOVE_SHIELDS": "Снимет щиты за",
|
||||||
"PHRASE_MULTI_CREW_CAPACITOR_POINTS": "Щелкните правой кновкой мыши чтобы объединить в группу.",
|
"TT_TIME_TO_REMOVE_SHIELDS": "Непрерывным огнём из всех орудий",
|
||||||
"TT_TIME_TO_REMOVE_SHIELDS": "Непрерывным огнем из всех орудий",
|
|
||||||
"PHRASE_TIME_TO_REMOVE_ARMOUR": "Снимет броню за",
|
"PHRASE_TIME_TO_REMOVE_ARMOUR": "Снимет броню за",
|
||||||
"TT_TIME_TO_REMOVE_ARMOUR": "Непрерывным огнем из всех орудий",
|
"TT_TIME_TO_REMOVE_ARMOUR": "Непрерывным огнём из всех орудий",
|
||||||
"PHRASE_TIME_TO_DRAIN_WEP": "Опустошит ОРУ за",
|
"PHRASE_TIME_TO_DRAIN_WEP": "Опустошит ОРУЖ за",
|
||||||
"TT_TIME_TO_DRAIN_WEP": "Время, за которое опустошится аккумулятор ОРУ при стрельбе из всех орудий",
|
"TT_TIME_TO_DRAIN_WEP": "Время, за которое опустошится аккумулятор ОРУЖ при стрельбе из всех орудий",
|
||||||
"TT_TIME_TO_LOSE_SHIELDS": "Против поддерживаемой стрельбы из всех орудий противника",
|
"TT_TIME_TO_LOSE_SHIELDS": "Против поддерживаемой стрельбы из всех орудий противника",
|
||||||
"TT_TIME_TO_LOSE_ARMOUR": "Против поддерживаемой стрельбы из всех орудий противника",
|
"TT_TIME_TO_LOSE_ARMOUR": "Против поддерживаемой стрельбы из всех орудий противника",
|
||||||
"TT_MODULE_ARMOUR": "Броня, защищающая модули от урона",
|
"TT_MODULE_ARMOUR": "Броня, защищающая модули от урона",
|
||||||
"TT_MODULE_PROTECTION_EXTERNAL": "Процент урона, перенаправленного от гнезд на наборы для усиления модулей",
|
"TT_MODULE_PROTECTION_EXTERNAL": "Процент урона, перенаправленного от гнёзд на наборы для усиления модулей",
|
||||||
"TT_MODULE_PROTECTION_INTERNAL": "Процент урона, перенаправленного от модулей вне гнезд на наборы для усиления модулей",
|
"TT_MODULE_PROTECTION_INTERNAL": "Процент урона, перенаправленного от модулей вне гнёзд на наборы для усиления модулей",
|
||||||
"TT_EFFECTIVE_SDPS_SHIELDS": "Реальный поддерживаемый ДПС пока аккумулятор ОРУ не пуст",
|
"TT_EFFECTIVE_SDPS_SHIELDS": "Реальный поддерживаемый ДПС пока аккумулятор ОРУЖ не пуст",
|
||||||
"TT_EFFECTIVENESS_SHIELDS": "Эффективность в сравнении с попаданием по цели с 0-сопротивляемостью без пунктов в СИС на 0 метрах",
|
"TT_EFFECTIVENESS_SHIELDS": "Эффективность в сравнении с попаданием по цели с 0-сопротивляемостью без пунктов в СИС на 0 метрах",
|
||||||
"TT_EFFECTIVE_SDPS_ARMOUR": "Реальный поддерживаемый ДПС пока аккумулятор ОРУ не пуст",
|
"TT_EFFECTIVE_SDPS_ARMOUR": "Реальный поддерживаемый ДПС пока аккумулятор ОРУЖ не пуст",
|
||||||
"TT_EFFECTIVENESS_ARMOUR": "Эффективность в сравнении с попаданием по цели с 0-сопротивляемостью на 0 метрах",
|
"TT_EFFECTIVENESS_ARMOUR": "Эффективность в сравнении с попаданием по цели с 0-сопротивляемостью на 0 метрах",
|
||||||
"PHRASE_EFFECTIVE_SDPS_SHIELDS": "ПДПС против щитов",
|
"PHRASE_EFFECTIVE_SDPS_SHIELDS": "ПДПС против щитов",
|
||||||
"PHRASE_EFFECTIVE_SDPS_ARMOUR": "ПДПС против брони",
|
"PHRASE_EFFECTIVE_SDPS_ARMOUR": "ПДПС против брони",
|
||||||
"TT_SUMMARY_SPEED": "С полным топливным баком и 4 пунктами в ДВГ",
|
"TT_SUMMARY_SPEED": "С полным топливным баком и 4 пунктами в ДВИ",
|
||||||
"TT_SUMMARY_SPEED_NONFUNCTIONAL": "Маневровые двигатели выключены или превышена максимальная масса с топливом и грузом",
|
"TT_SUMMARY_SPEED_NONFUNCTIONAL": "Маневровые двигатели выключены или превышена максимальная масса с топливом и грузом",
|
||||||
"TT_SUMMARY_BOOST": "С полным топливным баком и 4 пунктами в ДВГ",
|
"TT_SUMMARY_BOOST": "С полным топливным баком и 4 пунктами в ДВИ",
|
||||||
"TT_SUMMARY_BOOST_INTERVAL": "Время заполнения с 4 пунктами в СИС",
|
|
||||||
"TT_SUMMARY_BOOST_NONFUNCTIONAL": "Распределитель питания не может обеспечить достаточно энергии для форсажа",
|
"TT_SUMMARY_BOOST_NONFUNCTIONAL": "Распределитель питания не может обеспечить достаточно энергии для форсажа",
|
||||||
"TT_SUMMARY_SHIELDS": "Чистая сила щита, включая усилители",
|
"TT_SUMMARY_SHIELDS": "Чистая сила щита, включая усилители",
|
||||||
"TT_SUMMARY_SHIELDS_SCB": "Прочность щита, включая бустеры и SCB",
|
|
||||||
"TT_SUMMARY_SHIELDS_NONFUNCTIONAL": "Шитогенератор отсутствует или выключен",
|
"TT_SUMMARY_SHIELDS_NONFUNCTIONAL": "Шитогенератор отсутствует или выключен",
|
||||||
"TT_SUMMARY_INTEGRITY": "Целостность корабля, включая переборки и наборы для усиления корпуса",
|
"TT_SUMMARY_INTEGRITY": "Целостность корабля, включая переборки и наборы для усиления корпуса",
|
||||||
"TT_SUMMARY_HULL_MASS": "Масса корпуса без каких-либо модулей",
|
"TT_SUMMARY_HULL_MASS": "Масса корпуса без каких-либо модулей",
|
||||||
"TT_SUMMARY_UNLADEN_MASS": "Масса корпуса и модулей без топлива и груза",
|
"TT_SUMMARY_UNLADEN_MASS": "Масса корпуса и модулей без топлива и груза",
|
||||||
"TT_SUMMARY_LADEN_MASS": "Масса корпуса и модулей с топливом и грузом",
|
"TT_SUMMARY_LADEN_MASS": "Масса корпуса и модулей с топливом и грузом",
|
||||||
"TT_SUMMARY_DPS": "Урон в секунду при стрельбе из всех орудий",
|
"TT_SUMMARY_DPS": "Урон в секунду при стрельбе из всех орудий",
|
||||||
"TT_SUMMARY_EPS": "Расход аккумулятора ОРУ в секунду при стрельбе из всех орудий",
|
"TT_SUMMARY_EPS": "Расход аккумулятора ОРУЖ в секунду при стрельбе из всех орудий",
|
||||||
"TT_SUMMARY_TTD": "Время расхода аккумулятора ОРУ при стрельбе из всех орудий и с 4 пунктами в ОРУ",
|
"TT_SUMMARY_TTD": "Время расхода аккумулятора ОРУЖ при стрельбе из всех орудий и с 4 пунктами в ОРУЖ",
|
||||||
"TT_SUMMARY_MAX_SINGLE_JUMP": "Самый дальний возможный прыжок без груза и с топливом, достаточным только на сам прыжок",
|
"TT_SUMMARY_MAX_SINGLE_JUMP": "Самый дальний возможный прыжок без груза и с топливом, достаточным только на сам прыжок",
|
||||||
"TT_SUMMARY_UNLADEN_SINGLE_JUMP": "Самый дальний возможный прыжок без груза и с полным топливным баком",
|
"TT_SUMMARY_UNLADEN_SINGLE_JUMP": "Самый дальний возможный прыжок без груза и с полным топливным баком",
|
||||||
"TT_SUMMARY_LADEN_SINGLE_JUMP": "Самый дальний возможный прыжок с полным грузовым отсеком и с полным топливным баком",
|
"TT_SUMMARY_LADEN_SINGLE_JUMP": "Самый дальний возможный прыжок с полным грузовым отсеком и с полным топливным баком",
|
||||||
"TT_SUMMARY_UNLADEN_TOTAL_JUMP": "Самая дальняя общая дистанция без груза, с полным топливным баком и при прыжках на максимальное расстояние",
|
"TT_SUMMARY_UNLADEN_TOTAL_JUMP": "Самая дальняя общая дистанция без груза, с полным топливным баком и при прыжках на максимальное расстояние",
|
||||||
"TT_SUMMARY_LADEN_TOTAL_JUMP": "Самая дальняя общая дистанция с полным грузовым отсеком, с полным топливным баком и при прыжках на максимальное расстояние",
|
"TT_SUMMARY_LADEN_TOTAL_JUMP": "Самая дальняя общая дистанция с полным грузовым отсеком, с полным топливным баком и при прыжках на максимальное расстояние",
|
||||||
"HELP_MODIFICATIONS_MENU": "Нажмите на номер, чтобы ввести новое значение, или потяните вдоль полосы для малых изменений",
|
"HELP_MODIFICATIONS_MENU": "Нажмите на номер, чтобы ввести новое значение, или потяните вдоль полосы для малых изменений",
|
||||||
"PHRASE_FAIL_EDENGINEER": "Не удалось отправить в EDEngineer (запустите EDEngineer и убедитесь, что API запущен, затем обновите страницу).",
|
|
||||||
"PHRASE_FIREFOX_EDENGINEER": "Отправка в EDEngineer несовместима с настройками безопасности Firefox. Пожалуйста, попробуйте еще раз в Google Chrome.",
|
|
||||||
"am": "Блок Автом. Полевого Ремонта",
|
"am": "Блок Автом. Полевого Ремонта",
|
||||||
"bh": "Переборки",
|
"bh": "Переборки",
|
||||||
"bl": "Пучковый лазер",
|
"bl": "Пучковый лазер",
|
||||||
"bsg": "Двухпоточный щитогенератор",
|
"bsg": "Двухпоточный щитогенератор",
|
||||||
"c": "Пушка",
|
"c": "Орудие",
|
||||||
"causres": "Каустическое сопротивление",
|
|
||||||
"Caustic resistance": "Каустическое сопротивление",
|
|
||||||
"cc": "Контроллер магнитного снаряда для сбора",
|
"cc": "Контроллер магнитного снаряда для сбора",
|
||||||
"ch": "Разбрасыватель дипольных отражателей",
|
"ch": "Разбрасыватель дипольных отражателей",
|
||||||
"cr": "Грузовой стеллаж",
|
"cr": "Грузовой стеллаж",
|
||||||
@@ -109,17 +98,14 @@
|
|||||||
"kw": "Сканер преступников",
|
"kw": "Сканер преступников",
|
||||||
"ls": "Система жизнеобеспечения",
|
"ls": "Система жизнеобеспечения",
|
||||||
"mc": "Многоствольное орудие",
|
"mc": "Многоствольное орудие",
|
||||||
"axmc": "Многоствольное орудие АИ",
|
|
||||||
"ml": "Проходочный лазер",
|
"ml": "Проходочный лазер",
|
||||||
"mr": "Ракетный лоток",
|
"mr": "Ракетный лоток",
|
||||||
"axmr": "Блок ракет АИ",
|
|
||||||
"mrp": "Набор для усиления модуля",
|
"mrp": "Набор для усиления модуля",
|
||||||
"nl": "Мины",
|
"nl": "Мины",
|
||||||
"pa": "Ускоритель плазмы",
|
"pa": "Ускоритель плазмы",
|
||||||
"pas": "Комплект для сближения с планетой",
|
"pas": "Комплект для сближения с планетой",
|
||||||
"pc": "Контроллер магнитного снаряда для геологоразведки",
|
"pc": "Контроллер магнитного снаряда для геологоразведки",
|
||||||
"pce": "Каюта пассажира эконом-класса",
|
"pce": "Каюта пассажира эконом-класса",
|
||||||
"passenger capacity": "Количество пассажиров",
|
|
||||||
"pci": "Каюта пассажира бизнес-класса",
|
"pci": "Каюта пассажира бизнес-класса",
|
||||||
"pcm": "Каюта пассажира первого класса",
|
"pcm": "Каюта пассажира первого класса",
|
||||||
"pcq": "Каюта пассажира класса люкс",
|
"pcq": "Каюта пассажира класса люкс",
|
||||||
@@ -127,63 +113,33 @@
|
|||||||
"pl": "Импульсный лазер",
|
"pl": "Импульсный лазер",
|
||||||
"po": "Точечная оборона",
|
"po": "Точечная оборона",
|
||||||
"pp": "Силовая установка",
|
"pp": "Силовая установка",
|
||||||
"gpp": "Силовая установка Стражей",
|
|
||||||
"gpd": "Гибридный распределитель питания Стражей",
|
|
||||||
"gpc": "Плазменная пушка Стражей",
|
|
||||||
"ggc": "Пушка Гаусса Стражей",
|
|
||||||
"gsrp": "Набор для усиления щита Стражей",
|
|
||||||
"gfsb": "Ускоритель FSD Стражей",
|
|
||||||
"ghrp": "Набор для усиления корпуса Стражей",
|
|
||||||
"gmrp": "Набор для усиления модуля Стражей",
|
|
||||||
"pwa": "Анализатор импульсных волн",
|
|
||||||
"abl": "Абразивный бластер",
|
|
||||||
"scl": "Пусковая установка для сейсмических снарядов",
|
|
||||||
"sdm": "Вытесняющая ракета для добычи глубинных залежей",
|
|
||||||
"tbsc": "Шоковое орудие",
|
|
||||||
"gsc": "Осколочное орудие Стражей",
|
|
||||||
"psg": "Призматический щитогенератор",
|
"psg": "Призматический щитогенератор",
|
||||||
"pv": "Гараж для планетарного транспорта",
|
"pv": "Гараж для планетарного транспорта",
|
||||||
"rf": "Устройство переработки",
|
"rf": "Устройство переработки",
|
||||||
"rfl": "Зенитная установка (снаряды с дистанционным подрывом)",
|
|
||||||
"rg": "Электромагнитная пушка",
|
"rg": "Электромагнитная пушка",
|
||||||
"rsl": "Дроны-исследователи",
|
|
||||||
"s": "Сенсоры",
|
"s": "Сенсоры",
|
||||||
"sb": "Усилитель щита",
|
"sb": "Усилитель щита",
|
||||||
"sc": "Сканер обнаружения",
|
"sc": "Сканер обнаружения",
|
||||||
"scb": "Щитонакопитель",
|
"scb": "Щитонакопитель",
|
||||||
"sfn": "Нейтрализатор глушащего поля",
|
|
||||||
"sg": "Щитогенератор",
|
"sg": "Щитогенератор",
|
||||||
"ss": "Сканер поверхностей",
|
"ss": "Сканер поверхностей",
|
||||||
"sua": "Помощь в гиперкрейсерском режиме",
|
|
||||||
"t": "Маневровые двигатели",
|
"t": "Маневровые двигатели",
|
||||||
"tp": "Торпедная стойка",
|
"tp": "Торпедная стойка",
|
||||||
"ul": "Пульсирующие лазеры",
|
"ul": "Пульсирующие лазеры",
|
||||||
"Send To EDEngineer": "Отправить в EDEngineer",
|
|
||||||
"ws": "Сканер следа FSD",
|
"ws": "Сканер следа FSD",
|
||||||
"rpl": "Дроны-ремонтники",
|
|
||||||
"rcpl": "Дроны-разведчики",
|
|
||||||
"xs": "Сканер «инопланетянин»",
|
|
||||||
"tbem": "Блок энзимных ракет",
|
|
||||||
"tbrfl": "Установка для стрельбы стреловидными снарядами с дистанционным подрывом",
|
|
||||||
"dtl": "Дроны-очистители",
|
|
||||||
"mahr": "Набор для усиления корпуса из Метасплава",
|
|
||||||
"emptyrestricted": "пусто (ограниченно)",
|
"emptyrestricted": "пусто (ограниченно)",
|
||||||
"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": "Общие эффективные щиты",
|
||||||
"ammunition": "Припасы",
|
"ammunition": "Припасы",
|
||||||
"secs": "с",
|
"secs": "с",
|
||||||
"bays": "Ячейки",
|
"rebuildsperbay": "Построек за полосу",
|
||||||
"rebuildsperbay": "Истребителей в ячейке",
|
|
||||||
"mroll": "Roll",
|
|
||||||
"feature": "Свойство",
|
|
||||||
"worst": "Худшее",
|
"worst": "Худшее",
|
||||||
"average": "Среднее",
|
"average": "Среднее",
|
||||||
"random": "Случайное",
|
"random": "Случайное",
|
||||||
"best": "Лучшее",
|
"best": "Лучшее",
|
||||||
"current": "Текущее",
|
|
||||||
"extreme": "Экстремальное",
|
"extreme": "Экстремальное",
|
||||||
"reset": "Сброс",
|
"reset": "Сброс",
|
||||||
"dpe": "Урон на МДж энергии",
|
"dpe": "Урон на МДж энергии",
|
||||||
@@ -192,8 +148,6 @@
|
|||||||
"dpssdps": "Урон в секунду (поддерживаемый урон в секунду)",
|
"dpssdps": "Урон в секунду (поддерживаемый урон в секунду)",
|
||||||
"eps": "Энергия в секунду",
|
"eps": "Энергия в секунду",
|
||||||
"epsseps": "Энергия в секунду (поддерживаемая энергия в секунду)",
|
"epsseps": "Энергия в секунду (поддерживаемая энергия в секунду)",
|
||||||
"fallofffromrange": "Спад",
|
|
||||||
"falloff": "Спад",
|
|
||||||
"hps": "Нагрев в секунду",
|
"hps": "Нагрев в секунду",
|
||||||
"hpsshps": "Нагрев в секунду (поддерживаемый нагрев в секунду)",
|
"hpsshps": "Нагрев в секунду (поддерживаемый нагрев в секунду)",
|
||||||
"damage by": "Урон",
|
"damage by": "Урон",
|
||||||
@@ -210,15 +164,13 @@
|
|||||||
"internal protection": "Внутренняя защита",
|
"internal protection": "Внутренняя защита",
|
||||||
"external protection": "Внешняя защита",
|
"external protection": "Внешняя защита",
|
||||||
"engagement range": "Боевое расстояние",
|
"engagement range": "Боевое расстояние",
|
||||||
"boost interval": "Интервал повыш.",
|
|
||||||
"total": "Всего",
|
"total": "Всего",
|
||||||
"ammo": "Макс. боекомплект",
|
"ammo": "Боекомплект",
|
||||||
"boot": "Время загрузки",
|
"boot": "Время загрузки",
|
||||||
"hacktime": "Время взлома",
|
|
||||||
"brokenregen": "Скорость восстановления при пробое",
|
"brokenregen": "Скорость восстановления при пробое",
|
||||||
"burst": "Длина очереди",
|
"burst": "Длина очереди",
|
||||||
"burstrof": "Скорострельность очереди",
|
"burstrof": "Скорострельность очереди",
|
||||||
"clip": "Размер боекомплекта",
|
"clip": "Боекомплект",
|
||||||
"damage": "Урон",
|
"damage": "Урон",
|
||||||
"distdraw": "Тяга распределителя",
|
"distdraw": "Тяга распределителя",
|
||||||
"duration": "Продолжительность",
|
"duration": "Продолжительность",
|
||||||
@@ -247,16 +199,11 @@
|
|||||||
"rof": "Скорострельность",
|
"rof": "Скорострельность",
|
||||||
"angle": "Угол сканера",
|
"angle": "Угол сканера",
|
||||||
"scanrate": "Скорость сканера",
|
"scanrate": "Скорость сканера",
|
||||||
"proberadius": "Радиус зонда",
|
|
||||||
"scantime": "Время сканирования",
|
"scantime": "Время сканирования",
|
||||||
"scan range": "Дальность",
|
|
||||||
"max angle": "Макс. угол",
|
|
||||||
"shield": "Щит",
|
"shield": "Щит",
|
||||||
"armour": "Броня",
|
|
||||||
"shieldboost": "Усиление щитов",
|
"shieldboost": "Усиление щитов",
|
||||||
"shieldreinforcement": "Усилитель щита",
|
"shieldreinforcement": "Усилитель щита",
|
||||||
"shotspeed": "Скорость выстрела",
|
"shotspeed": "Скорость выстрела",
|
||||||
"shotdmg": "Урон за выстрел(DPS)",
|
|
||||||
"spinup": "Время раскрутки",
|
"spinup": "Время раскрутки",
|
||||||
"syscap": "Ресурс систем",
|
"syscap": "Ресурс систем",
|
||||||
"sysrate": "Перезарядка систем",
|
"sysrate": "Перезарядка систем",
|
||||||
@@ -287,12 +234,9 @@
|
|||||||
"explosive": "Взрывч.",
|
"explosive": "Взрывч.",
|
||||||
"kinetic": "Механич.",
|
"kinetic": "Механич.",
|
||||||
"thermal": "Тепл.",
|
"thermal": "Тепл.",
|
||||||
"caustic": "Каустич.",
|
|
||||||
"generator": "Генератор",
|
"generator": "Генератор",
|
||||||
"boosters": "Усилители",
|
"boosters": "Усилители",
|
||||||
"cells": "Ячейки",
|
"cells": "Ячейки",
|
||||||
"shield addition": "ДОбавления к щиту",
|
|
||||||
"jump addition": "ДОбавления к прыжку",
|
|
||||||
"bulkheads": "Переборки",
|
"bulkheads": "Переборки",
|
||||||
"reinforcement": "Усилители",
|
"reinforcement": "Усилители",
|
||||||
"power and costs": "Энергия и стоимость",
|
"power and costs": "Энергия и стоимость",
|
||||||
@@ -306,7 +250,7 @@
|
|||||||
"damage to opponent's shields": "Урон щиту противника",
|
"damage to opponent's shields": "Урон щиту противника",
|
||||||
"damage to opponent's hull": "Урон корпусу противника",
|
"damage to opponent's hull": "Урон корпусу противника",
|
||||||
"offence": "Нападение",
|
"offence": "Нападение",
|
||||||
"defence": "Защита",
|
"defence": "Оборона",
|
||||||
"shield metrics": "Данные щита",
|
"shield metrics": "Данные щита",
|
||||||
"raw shield strength": "Чистая мощность щита",
|
"raw shield strength": "Чистая мощность щита",
|
||||||
"shield sources": "Ресурсы щита",
|
"shield sources": "Ресурсы щита",
|
||||||
@@ -321,70 +265,28 @@
|
|||||||
"defence metrics": "Данные обороны",
|
"defence metrics": "Данные обороны",
|
||||||
"fuel carried": "Топливо на борту",
|
"fuel carried": "Топливо на борту",
|
||||||
"cargo carried": "Груз на борту",
|
"cargo carried": "Груз на борту",
|
||||||
"ship control": "Управление кораблем",
|
"ship control": "Управление кораблём",
|
||||||
"opponent": "Противник",
|
"opponent": "Противник",
|
||||||
"opponent's shields": "Щит противника",
|
"opponent's shields": "Щит противника",
|
||||||
"opponent's armour": "Броня противника",
|
"opponent's armour": "Броня противника",
|
||||||
"overall damage": "общий урон",
|
|
||||||
"overall": "общий",
|
|
||||||
"shield damage sources": "источники урона по щиту",
|
"shield damage sources": "источники урона по щиту",
|
||||||
"armour damage sources": "источники урона по броне",
|
"armour damage sources": "источники урона по броне",
|
||||||
"never": "Никогда",
|
"never": "Никогда",
|
||||||
"stock": "базовый",
|
"stock": "базовый",
|
||||||
"boost": "форсаж",
|
"boost": "форсаж",
|
||||||
"tab_defence": "защита",
|
|
||||||
"federation rank 1": "Рекрут",
|
|
||||||
"federation rank 2": "Кадет",
|
|
||||||
"federation rank 3": "Гардемарин",
|
|
||||||
"federation rank 4": "Старшина",
|
|
||||||
"federation rank 5": "Главный старшина",
|
|
||||||
"federation rank 6": "Уорент-офицер",
|
|
||||||
"federation rank 7": "Энсин",
|
|
||||||
"federation rank 8": "Лейтенант",
|
|
||||||
"federation rank 9": "Капитан-лейтенант",
|
|
||||||
"federation rank 10": "Начальник гарнизона",
|
|
||||||
"federation rank 11": "Командир корабля",
|
|
||||||
"federation rank 12": "Контр-адмирал",
|
|
||||||
"federation rank 13": "Вице-адмирал",
|
|
||||||
"federation rank 14": "Адмирал",
|
|
||||||
"federation rank required": "Минимальный ранг федерации для покупки",
|
|
||||||
"empire rank 1": "Чужак",
|
|
||||||
"empire rank 2": "Крепостной",
|
|
||||||
"empire rank 3": "Мастер",
|
|
||||||
"empire rank 4": "Оруженосец",
|
|
||||||
"empire rank 5": "Рыцарь",
|
|
||||||
"empire rank 6": "Лорд",
|
|
||||||
"empire rank 7": "Барон",
|
|
||||||
"empire rank 8": "Виконт",
|
|
||||||
"empire rank 9": "Граф",
|
|
||||||
"empire rank 10": "Эрл",
|
|
||||||
"empire rank 11": "Маркиз",
|
|
||||||
"empire rank 12": "Герцог",
|
|
||||||
"empire rank 13": "Принц",
|
|
||||||
"empire rank 14": "Король",
|
|
||||||
"empire rank required": "Минимальный ранг империи для покупки",
|
|
||||||
"kg": "кг",
|
|
||||||
"kg/s": "кг/с",
|
|
||||||
"km": "км",
|
|
||||||
"m": "м",
|
|
||||||
"MJ": "МДж",
|
|
||||||
"MW": "МВт",
|
|
||||||
"T": "т",
|
|
||||||
"°/s": "°/с",
|
|
||||||
"/s": "/с",
|
"/s": "/с",
|
||||||
"/min": "/мин",
|
|
||||||
"m/s": "м/с",
|
"m/s": "м/с",
|
||||||
"Ls": "Св.сек",
|
"Ls": "Св.сек",
|
||||||
"LY": "Св.лет",
|
"LY": "Св.лет",
|
||||||
"CR": "кр.",
|
"CR": "кр.",
|
||||||
"S": "М",
|
"S": "M",
|
||||||
"M": "С",
|
"M": "С",
|
||||||
"L": "Б",
|
"L": "б",
|
||||||
"H": "О",
|
"H": "O",
|
||||||
"U": "В",
|
"U": "B",
|
||||||
"small": "Малый",
|
"small": "Малый",
|
||||||
"medium": "Средний",
|
"medium": "Средний",
|
||||||
"large": "Большой",
|
"large": "большой",
|
||||||
"alpha": "Альфа",
|
"alpha": "Альфа",
|
||||||
"beta": "Бета",
|
"beta": "Бета",
|
||||||
"standard": "Стандартный",
|
"standard": "Стандартный",
|
||||||
@@ -397,29 +299,28 @@
|
|||||||
"edit data": "Редактирование",
|
"edit data": "Редактирование",
|
||||||
"empty all": "пусто все",
|
"empty all": "пусто все",
|
||||||
"Enter Name": "Введите имя",
|
"Enter Name": "Введите имя",
|
||||||
"farthest range": "быстрый диапазон",
|
"fastest range": "быстрый диапазон",
|
||||||
"fuel level": "уровень топлива",
|
"fuel level": "уровень топлива",
|
||||||
"full tank": "Полный бак",
|
"full tank": "Полный бак",
|
||||||
"internal compartments": "внутренние отсеки",
|
"internal compartments": "внутренние отсеки",
|
||||||
"jump range": "Дальность прыжка",
|
"jump range": "Дальность прыжка",
|
||||||
"mass lock factor": "Коэффициент гравитационного захвата",
|
"mass lock factor": "Масс. блок",
|
||||||
"max mass": "Максимальная масса",
|
"max mass": "Максимальная масса",
|
||||||
"minimum mass": "Минимальная масса",
|
|
||||||
"optimal mass": "Оптимальная масса",
|
|
||||||
"net cost": "разница в цене",
|
"net cost": "разница в цене",
|
||||||
"none created": "не создано",
|
"none created": "не создано",
|
||||||
"refuel time": "Время дозаправки",
|
"refuel time": "Время дозаправки",
|
||||||
"retrofit from": "модификация от",
|
"retrofit from": "модификация от",
|
||||||
"T-Load": "Тепл.",
|
"T-Load": "Тепл.",
|
||||||
"utility mounts": "Вспомогательное оборудование",
|
"utility mounts": "Вспомогательное оборудование",
|
||||||
"about": "О сайте",
|
"about": "О ...",
|
||||||
"action": "Действие",
|
"action": "Действие",
|
||||||
"added": "Добавлено",
|
"added": "Добавлено",
|
||||||
|
"armour": "Броня",
|
||||||
"available": "доступно",
|
"available": "доступно",
|
||||||
"backup": "Резервная копия",
|
"backup": "Резервная копия",
|
||||||
"bins": "контейнеры",
|
"bins": "контейнеры",
|
||||||
"build": "сборка",
|
"build": "cборка",
|
||||||
"builds": "сборки",
|
"builds": "cборки",
|
||||||
"buy": "купить",
|
"buy": "купить",
|
||||||
"cancel": "отменить",
|
"cancel": "отменить",
|
||||||
"cargo": "Груз",
|
"cargo": "Груз",
|
||||||
@@ -438,16 +339,16 @@
|
|||||||
"DPS": "УВС",
|
"DPS": "УВС",
|
||||||
"efficiency": "Эффективность",
|
"efficiency": "Эффективность",
|
||||||
"empty": "пусто",
|
"empty": "пусто",
|
||||||
"ENG": "ДВГ",
|
"ENG": "ДВИ",
|
||||||
"export": "Экспорт",
|
"export": "Экспорт",
|
||||||
"forum": "Форум",
|
"forum": "Форум",
|
||||||
"fuel": "Топл.",
|
"fuel": "Топливо",
|
||||||
"hardpoints": "Орудийные порты",
|
"hardpoints": "Орудийные порты",
|
||||||
"hull": "Корпус",
|
"hull": "Корпус",
|
||||||
"import": "импортировать ",
|
"import": "импортировать ",
|
||||||
"insurance": "Страховка",
|
"insurance": "Страховка",
|
||||||
"jumps": "Прыжков",
|
"jumps": "Прыжков",
|
||||||
"laden": "Груж",
|
"laden": "Груженый",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"maneuverability": "Маневренность",
|
"maneuverability": "Маневренность",
|
||||||
"max": "Макс",
|
"max": "Макс",
|
||||||
@@ -460,15 +361,13 @@
|
|||||||
"rate": "скорость",
|
"rate": "скорость",
|
||||||
"rename": "Переименовать",
|
"rename": "Переименовать",
|
||||||
"repair": "Починка",
|
"repair": "Починка",
|
||||||
"ret": "Убр",
|
"ret": "Убр.",
|
||||||
"retracted": "Убрано",
|
"retracted": "Убрано",
|
||||||
"ROF": "В\/сек",
|
"ROF": "В/сек",
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"sell": "Продать",
|
"sell": "Продать",
|
||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
"shields": "Щиты",
|
"shields": "Щиты",
|
||||||
"No Shield": "Нет щита",
|
|
||||||
"Never": "Никогда",
|
|
||||||
"ship": "Корабль",
|
"ship": "Корабль",
|
||||||
"ships": "Корабли",
|
"ships": "Корабли",
|
||||||
"shortened": "Укороченный",
|
"shortened": "Укороченный",
|
||||||
@@ -478,296 +377,8 @@
|
|||||||
"SYS": "СИС",
|
"SYS": "СИС",
|
||||||
"time": "Время",
|
"time": "Время",
|
||||||
"type": "Тип",
|
"type": "Тип",
|
||||||
"unladen": "Пуст",
|
"unladen": "Пустой",
|
||||||
"URL": "Ссылка",
|
"URL": "Ссылка",
|
||||||
"WEP": "ОРУ",
|
"WEP": "ОРУЖ",
|
||||||
"yes": "Да",
|
"yes": "Да"
|
||||||
"crew": "экипаж",
|
|
||||||
"jump": "прыж",
|
|
||||||
"pax": "псж",
|
|
||||||
"RST": "СБР",
|
|
||||||
"grade": "уровень",
|
|
||||||
"total laden": "всего груж",
|
|
||||||
"total unladen": "всего пуст",
|
|
||||||
"module": "модуль",
|
|
||||||
"announcements": "объявления",
|
|
||||||
"resistance": "сопротивление",
|
|
||||||
"Lightweight Alloy": "Легкие сплавы",
|
|
||||||
"base": "базовые",
|
|
||||||
"core module classes": "основные модули",
|
|
||||||
"Group highlighted ships": "Сгруппировать выделенные корабли",
|
|
||||||
"tooltips": "всплывающие подсказки",
|
|
||||||
"module resistances": "сопротивление модулей",
|
|
||||||
"power plant": "силовая установка",
|
|
||||||
"thrusters": "маневровые двигатели",
|
|
||||||
"frame shift drive": "рамочно-сместительный двигатель",
|
|
||||||
"life support": "система жизнеобеспечения",
|
|
||||||
"power distributor": "распределитель питания",
|
|
||||||
"sensors": "сенсоры",
|
|
||||||
"fuel tank": "топливный бак",
|
|
||||||
"resting heat (Beta)": "тепло покоя (бета)",
|
|
||||||
"hull hardness": "Прочность корпуса",
|
|
||||||
"weapon": "оружие",
|
|
||||||
"maximum speed": "максимальная скорость",
|
|
||||||
"maximum range": "максимальная дальность",
|
|
||||||
"shortlink": "короткая ссылка",
|
|
||||||
"guardian": "стражи",
|
|
||||||
"engineers": "инженеры",
|
|
||||||
"component": "компонент",
|
|
||||||
"amount": "кол-во",
|
|
||||||
"core internal": "основное оборуднование",
|
|
||||||
"optional internal": "доп. оборудование",
|
|
||||||
"Heat Sink Launcher": "Теплоотводная катапульта",
|
|
||||||
"scanners": "сканеры",
|
|
||||||
"experimental": "экспериментальное",
|
|
||||||
"mining": "шахтерство",
|
|
||||||
"lasers": "лазеры",
|
|
||||||
"ordnance": "артиллерия",
|
|
||||||
"projectiles": "с боеприпасами",
|
|
||||||
"hangars": "ангары",
|
|
||||||
"limpet controllers": "контроллеры снарядов",
|
|
||||||
"passenger cabins": "каюты пассажиров",
|
|
||||||
"structural reinforcement": "структурные усиления",
|
|
||||||
"flight assists": "помощники в полете",
|
|
||||||
"modifications": "модификации",
|
|
||||||
"wep_reload": "перезарядка",
|
|
||||||
"optimal multiplier": "оптимальный усилитель",
|
|
||||||
"Cargo Hatch": "Грузовой люк",
|
|
||||||
"Chaff Launcher": "Разбрасыватель дипольных отражателей",
|
|
||||||
"Point Defence": "Точечная оборона",
|
|
||||||
"Electronic Countermeasure": "Электронное противодействие",
|
|
||||||
"Xeno Scanner": "Сканер «инопланетянин»",
|
|
||||||
"Shutdown Field Neutraliser": "Нейтрализатор глушащего поля",
|
|
||||||
"Disruptor": "Диверсант",
|
|
||||||
"Pacifier": "Миротворец",
|
|
||||||
"Advanced Plasma Accelerator": "Улучшенный ускоритель плазмы",
|
|
||||||
"Cytoscrambler": "Дезинтегратор",
|
|
||||||
"Retributor": "Каратель",
|
|
||||||
"Enforcer": "Убийца",
|
|
||||||
"Imperial Hammer": "Имперский молот",
|
|
||||||
"Rocket Propelled FSD Disruptor": "Ракетный FSD-разрушитель",
|
|
||||||
"Pack-Hound": "Гончие",
|
|
||||||
"Shock Mine Launcher": "Установщик шоковых мин",
|
|
||||||
"Mining Lance": "Копье шахтера",
|
|
||||||
"Corrosion Resistant": "Коррозийно-устойчивый стеллаж",
|
|
||||||
"Standard Docking Computer": "Стандартный стыковочный компьютер",
|
|
||||||
"Advanced Docking Computer": "Улучшенный стыковочный компьютер",
|
|
||||||
"Detailed Surface Scanner": "Подробный сканер поверхности",
|
|
||||||
"Supercruise Assist": "Помощь в гиперкрейсерском режиме",
|
|
||||||
"Guardian Power Distributor": "Рапределитель питания Стражей",
|
|
||||||
"Enhanced Performance": "Усиленные маневровые двигатели",
|
|
||||||
"Guardian Hybrid Power Plant": "Гибридная силовая установка Стражей",
|
|
||||||
"Reinforced Alloy": "Укрепленные сплавы",
|
|
||||||
"Military Grade Composite": "Композит военного класса",
|
|
||||||
"Mirrored Surface Composite": "Композит с зеркальной поверхностью",
|
|
||||||
"Reactive Surface Composite": "Композит с реактивной поверхностью",
|
|
||||||
"Proto Light Alloys": "Опытные легкие сплавы",
|
|
||||||
"Ammo capacity": "Вместимость магазина",
|
|
||||||
"Lightweight": "Облегченный",
|
|
||||||
"Reinforced": "Усиленный",
|
|
||||||
"Shielded": "Защищенный",
|
|
||||||
"Fast scan": "Быстрое сканирование",
|
|
||||||
"Long range": "Дальнего действия",
|
|
||||||
"Wide angle": "Широкоугольный",
|
|
||||||
"Blast resistant": "Взрывостойкий",
|
|
||||||
"Heavy duty": "Надежный",
|
|
||||||
"Kinetic resistant": "Противокинетический",
|
|
||||||
"Resistance augmented": "С универсальной защитой",
|
|
||||||
"Thermal resistant": "Термостойкий",
|
|
||||||
"Thermo Block": "Блокировка тепла",
|
|
||||||
"Force Block": "Усиленная блокировка",
|
|
||||||
"Blast Block": "Блокировка взрыва",
|
|
||||||
"Flow Control": "Контроль интенсивности",
|
|
||||||
"Double Braced": "Двойная прочность",
|
|
||||||
"Super Capacitors": "Суперконденсаторы",
|
|
||||||
"Efficient": "Эффективный",
|
|
||||||
"Focused": "Точный",
|
|
||||||
"Overcharged": "Усиленный",
|
|
||||||
"Rapid fire": "Скорострельный",
|
|
||||||
"Short range": "Ближнего действия",
|
|
||||||
"Sturdy": "Прочный",
|
|
||||||
"Oversized": "Сверхразмер",
|
|
||||||
"Stripped Down": "Урезанный вариант",
|
|
||||||
"Phasing sequence": "Последовательность фазирования",
|
|
||||||
"Concordant sequence": "Последовательность координации",
|
|
||||||
"Scramble spectrum": "Отключающая сетка",
|
|
||||||
"Thermal shock": "Тепловой удар",
|
|
||||||
"Emissive munitions": "Эмиссионные припасы",
|
|
||||||
"Multi-servos": "Сервосистема",
|
|
||||||
"Inertial impact": "Инерционный удар",
|
|
||||||
"Thermal vent": "Теплоотдача",
|
|
||||||
"Regeneration sequence": "Последовательность восстановления",
|
|
||||||
"Thermal conduit": "Проводник тепла",
|
|
||||||
"High capacity": "Вместительный магазин",
|
|
||||||
"Incendiary rounds": "Зажигательные припасы",
|
|
||||||
"Auto loader": "Автоматическая система заряжения",
|
|
||||||
"Smart rounds": "Умные боеприпасы",
|
|
||||||
"Corrosive shell": "Разъедающие припасы",
|
|
||||||
"Force shell": "Усиленные снаряды",
|
|
||||||
"High yield shell": "Снаряд большой мощности",
|
|
||||||
"Dispersal field": "Рассеивающее поле",
|
|
||||||
"Thermal cascade": "Термический залп",
|
|
||||||
"Double shot": "Двойной выстрел",
|
|
||||||
"Dazzle shell": "Ослепляющие снаряды",
|
|
||||||
"Screening shell": "Заслоняющие снаряды",
|
|
||||||
"Drag munitions": "Замедляющие боеприпасы",
|
|
||||||
"Target lock breaker": "Генератор помех для системы захвата цели",
|
|
||||||
"Plasma Slug": "Плазменный рельсовый снаряд",
|
|
||||||
"Feedback Cascade": "Ответный запл",
|
|
||||||
"Super Penetrator": "Модуль сверхпробития",
|
|
||||||
"Overload munitions": "Вызывающие перезагрузку боеприпасы",
|
|
||||||
"Penetrator payload": "Бронебойные снаряды",
|
|
||||||
"Penetrator Payload": "Бронебойные снаряды",
|
|
||||||
"FSD interrupt": "Помеха для FSD",
|
|
||||||
"Penetrator Munitions": "Бронебойные боеголовки",
|
|
||||||
"Mass lock munition": "Боеприпасы с гравитационным захватом",
|
|
||||||
"Mass Lock Munition": "Боеприпасы с гравитационным захватом",
|
|
||||||
"Reverberating cascade": "Отраженный залп",
|
|
||||||
"Shift-lock canister": "Рамоблокирующая кассета",
|
|
||||||
"Ion disruptor": "Ионный дестабилизатор",
|
|
||||||
"Radiant Canister": "Светящаяся кассета",
|
|
||||||
"Expanded capture arc": "Расширенная дуга захвата",
|
|
||||||
"Enhanced low power": "Улучшенное энергосбережение",
|
|
||||||
"Fast Charge": "Быстрый заряд",
|
|
||||||
"Multi-weave": "Мультипрошивка",
|
|
||||||
"Hi-Cap": "Высокая ёмкость",
|
|
||||||
"Lo-draw": "Пониженное потребление",
|
|
||||||
"Rapid charge": "Быстрая зарядка",
|
|
||||||
"Specialised": "Адаптивный",
|
|
||||||
"Boss Cells": "Босс-ячейки",
|
|
||||||
"Recycling Cell": "Рециркуляционная ячейка",
|
|
||||||
"Reflective Plating": "Отражающая броня",
|
|
||||||
"Angled Plating": "Угловая броня",
|
|
||||||
"Layered Plating": "Многослойная броня",
|
|
||||||
"Deep Plating": "Утолщенная броня",
|
|
||||||
"Expanded Probe Scanning Radius": "Увеличение радиуса сканирования зондов",
|
|
||||||
"High charge capacity": "Высокоёмкий",
|
|
||||||
"Charge enhanced": "Быстрозаряжающийся",
|
|
||||||
"Engine focused": "Фокус на двигатель",
|
|
||||||
"System focused": "Фокус на систему",
|
|
||||||
"Weapon focused": "Фокус на орудия",
|
|
||||||
"Super Conduits": "Сверхпроводники",
|
|
||||||
"Cluster Capacitors": "Кассетные конденсаторы",
|
|
||||||
"Increased range": "Увеличенная дальность",
|
|
||||||
"Faster boot sequence": "Ускорение запуска",
|
|
||||||
"Deep Charge": "Заряд повышенной мощности",
|
|
||||||
"Thermal Spread": "Рассеивание тепла",
|
|
||||||
"Mass Manager": "Распределитель гравитации",
|
|
||||||
"Dirty": "«Грязная» донастройка",
|
|
||||||
"Clean": "«Чистая» донастройка",
|
|
||||||
"Drag Drives": "Ускорители",
|
|
||||||
"Drive Distributors": "Распределители тяги",
|
|
||||||
"Armoured": "Бронированный",
|
|
||||||
"Low emissions": "Малое излучение",
|
|
||||||
"Monstered": "Монстрация",
|
|
||||||
"roles": "роли",
|
|
||||||
"Maximize Jump Range": "Максимизировать дальность прыжка",
|
|
||||||
"Multi-purpose": "Многоцелевой",
|
|
||||||
"Combat": "Боец",
|
|
||||||
"Trader": "Торговец",
|
|
||||||
"Explorer": "Исследователь",
|
|
||||||
"Planetary Explorer": "Планетарный исследователь",
|
|
||||||
"Miner": "Шахтер",
|
|
||||||
"Racer": "Гонщик",
|
|
||||||
|
|
||||||
"Aberrant Shield Pattern Analysis": "Анализ аномального поведения щита",
|
|
||||||
"Abnormal Compact Emissions Data": "Аномальные компактные данные об излучении",
|
|
||||||
"Aerogel": "Аэрогель",
|
|
||||||
"Adaptive Encryptors Capture": "Захват адаптивного шифровальщика",
|
|
||||||
"Anomalous Bulk Scan Data": "Аномальный массив данных сканирования",
|
|
||||||
"Anomalous FSD Telemetry": "Аномальная телеметрия FSD",
|
|
||||||
"Antimony": "Сурьма",
|
|
||||||
"Arsenic": "Мышьяк",
|
|
||||||
"Atypical Disrupted Wake Echoes": "Атипичное эхо поврежденного следа",
|
|
||||||
"Atypical Encryption Archives": "Нетипичные архивы шифрования",
|
|
||||||
"Basic Conductors": "Простые проводники",
|
|
||||||
"Bio-Mechanical Conduits": "Биомеханические энергопроводники",
|
|
||||||
"Biotech Conductors": "Биотехнические проводники",
|
|
||||||
"Boron": "Бор",
|
|
||||||
"Cadmium": "Кадмий",
|
|
||||||
"Carbon": "Углерод",
|
|
||||||
"Carbon Fibre Plating": "Углеволоконная броня",
|
|
||||||
"Chemical Distillery": "Оборудование для перегонки химикатов",
|
|
||||||
"Chemical Manipulators": "Манипуляторы для работы с химикатами",
|
|
||||||
"Chemical Processors": "Оборудование для химобработки",
|
|
||||||
"Chromium": "Хром",
|
|
||||||
"Classified Scan Databanks": "Засекреченные базы данных сканированоя",
|
|
||||||
"Classified Scan Fragment": "Засекреченные фрагменты данных сканирования",
|
|
||||||
"Compound Shielding": "Многоступенчатая защита",
|
|
||||||
"Conductive Ceramics": "Проводящая керамика",
|
|
||||||
"Conductive Components": "Проводящие компоненты",
|
|
||||||
"Conductive Polymers": "Проводящие полимеры",
|
|
||||||
"Configurable Components": "Настраиваемые компоненты",
|
|
||||||
"Core Dynamics Composites": "Композиты Core Dynamics",
|
|
||||||
"Cracked Industrial Firmware": "Взломанные промышленные микропрограммы",
|
|
||||||
"Datamined Wake Exceptions": "Исключения из глубинного анализа данных следа",
|
|
||||||
"Decoded Emission Data": "Расшифрованные данные об излучении",
|
|
||||||
"Distorted Shield Cycle Recordings": "Поврежденные цикличные записи щита",
|
|
||||||
"Divergent Scan Data": "Неформатные данные сканирования",
|
|
||||||
"Eccentric Hyperspace Trajectories": "Аномальные траектории в гиперпространстве",
|
|
||||||
"Electrochemical Arrays": "Электрохимические массивы",
|
|
||||||
"Exceptional Scrambled Emission Data": "Исключительные зашифрованные данные об излучении",
|
|
||||||
"Exquisite Focus Crystals": "Отборные фокусировочные кристаллы",
|
|
||||||
"Flawed Focus Crystals": "Поврежденные фокусировочные кристаллы",
|
|
||||||
"Focus Crystals": "Фокусировочные кристаллы",
|
|
||||||
"Galvanising Alloys": "Сплавы для гальванизации",
|
|
||||||
"Germanium": "Германий",
|
|
||||||
"Grid Resistors": "Наборные резисторы",
|
|
||||||
"Heat Conduction Wiring": "Теплопроводящие провода",
|
|
||||||
"Heat Dispersion Plate": "Теплорассеивающая пластина",
|
|
||||||
"Heat Exchangers": "Теплообменные агрегаты",
|
|
||||||
"Heat Vanes": "Тепловые заслонки",
|
|
||||||
"High Density Composites": "Высокоплотные композиты",
|
|
||||||
"Hybrid Capacitors": "Гибридные конденсаторы",
|
|
||||||
"Imperial Shielding": "Имперская защита",
|
|
||||||
"Improvised Components": "Кустарные компоненты",
|
|
||||||
"Inconsistent Shield Soak Analysis": "Неполный анализ поглощения щита",
|
|
||||||
"Iron": "Железо",
|
|
||||||
"Irregular Emission Data": "Нестандартные данные об излучении",
|
|
||||||
"Manganese": "Марганец",
|
|
||||||
"Mechanical Components": "Механические компоненты",
|
|
||||||
"Mechanical Equipment": "Механическое оборудование",
|
|
||||||
"Mechanical Scrap": "Механические отходы",
|
|
||||||
"Mercury": "Ртуть",
|
|
||||||
"Military Grade Alloys": "Сплавы военного назначения",
|
|
||||||
"Military Supercapacitors": "Военные суперконденсаторы",
|
|
||||||
"Modified Consumer Firmware": "Измененные пользовательские микропрограммы",
|
|
||||||
"Modified Embedded Firmware": "Измененные встроенные микропрограммы",
|
|
||||||
"Molybdenum": "Молибден",
|
|
||||||
"Nickel": "Никель",
|
|
||||||
"Niobium": "Ниобий",
|
|
||||||
"Open Symmetric Keys": "Открытые симметричные ключи",
|
|
||||||
"Pharmaceutical Isolators": "Фармацевтические изоляционные материалы",
|
|
||||||
"Phase Alloys": "Фазовые сплавы",
|
|
||||||
"Phosphorus": "Фосфор",
|
|
||||||
"Polymer Capacitors": "Полимерные конденсаторы",
|
|
||||||
"Precipitated Alloys": "Осажденные сплавы",
|
|
||||||
"Proprietary Composites": "Патентованные композиты",
|
|
||||||
"Proto Heat Radiators": "Прототипы теплоизлучателей",
|
|
||||||
"Proto Radiolic Alloys": "Сплавы для изготовления зондов",
|
|
||||||
"Refined Focus Crystals": "Обработанные фокусировочные кристаллы",
|
|
||||||
"Ruthenium": "Рутений",
|
|
||||||
"Salvaged Alloys": "Захваченные сплавы",
|
|
||||||
"Security Firmware Patch": "Обновление для защитной микропрограммы",
|
|
||||||
"Selenium": "Селениум",
|
|
||||||
"Shield Emitters": "Щитоизлучатели",
|
|
||||||
"Shielding Sensors": "Сенсоры системы экранирования",
|
|
||||||
"Specialised Legacy Firmware": "Специальные микропрограммы предыдущего поколения",
|
|
||||||
"Strange Wake Solutions": "Странные расчеты следа",
|
|
||||||
"Sulphur": "Сера",
|
|
||||||
"Tagged Encryption Codes": "Меченные шифровальные коды",
|
|
||||||
"Technetium": "Технеций",
|
|
||||||
"Tellurium": "Теллурий",
|
|
||||||
"Thermic Alloys": "Термические сплавы",
|
|
||||||
"Tin": "Олово",
|
|
||||||
"Tungsten": "Вольфрам",
|
|
||||||
"Unexpected Emission Data": "Неожиданные данные об излучении",
|
|
||||||
"Unidentified Scan Archives": "Неопознанные архивы сканирования",
|
|
||||||
"Untypical Shield Scans": "Нетипичные данные сканирования щитов",
|
|
||||||
"Unusual Encrypted Files": "Особые зашифрованные файлы",
|
|
||||||
"Vanadium": "Ванадий",
|
|
||||||
"Worn Shield Emitters": "Изношенные щитоизлучатели",
|
|
||||||
"Yttrium": "Иттрий",
|
|
||||||
"Zinc": "Цинк",
|
|
||||||
"Zirconium": "Цирконий"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,39 @@ export default class AboutPage extends Page {
|
|||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h3>Supporting Coriolis</h3>
|
||||||
|
<p>
|
||||||
|
Coriolis is an open source project, and I work on it in my free time.
|
||||||
|
I have set up a patreon at{' '}
|
||||||
|
<a href="https://www.patreon.com/coriolis_elite">
|
||||||
|
patreon.com/coriolis_elite
|
||||||
|
</a>
|
||||||
|
, which will be used to keep Coriolis up to date and the servers
|
||||||
|
running.
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
action="https://www.paypal.com/cgi-bin/webscr"
|
||||||
|
method="post"
|
||||||
|
target="_top"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="cmd" value="_s-xclick" />
|
||||||
|
<input type="hidden" name="hosted_button_id" value="SJBKT2SWEEU68" />
|
||||||
|
<input
|
||||||
|
type="image"
|
||||||
|
src="https://www.paypalobjects.com/en_AU/i/btn/btn_donate_SM.gif"
|
||||||
|
border="0"
|
||||||
|
name="submit"
|
||||||
|
alt="PayPal – The safer, easier way to pay online!"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
border="0"
|
||||||
|
src="https://www.paypalobjects.com/en_AU/i/scr/pixel.gif"
|
||||||
|
width="1"
|
||||||
|
height="1"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
627
src/app/pages/ComparisonPage.jsx
Normal file
627
src/app/pages/ComparisonPage.jsx
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Page from './Page';
|
||||||
|
import Router from '../Router';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import { Ships } from 'coriolis-data/dist';
|
||||||
|
import Ship from '../shipyard/Ship';
|
||||||
|
import { fromComparison, toComparison } from '../shipyard/Serializer';
|
||||||
|
import Persist from '../stores/Persist';
|
||||||
|
import { SizeMap, ShipFacets } from '../shipyard/Constants';
|
||||||
|
import ComparisonTable from '../components/ComparisonTable';
|
||||||
|
import BarChart from '../components/BarChart';
|
||||||
|
import ModalCompare from '../components/ModalCompare';
|
||||||
|
import ModalExport from '../components/ModalExport';
|
||||||
|
import ModalPermalink from '../components/ModalPermalink';
|
||||||
|
import ModalImport from '../components/ModalImport';
|
||||||
|
import {
|
||||||
|
FloppyDisk,
|
||||||
|
Bin,
|
||||||
|
Download,
|
||||||
|
Embed,
|
||||||
|
Rocket,
|
||||||
|
LinkIcon
|
||||||
|
} from '../components/SvgIcons';
|
||||||
|
import ShortenUrl from '../utils/ShortenUrl';
|
||||||
|
import { comparisonBBCode } from '../utils/BBCode';
|
||||||
|
const browser = require('detect-browser');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a comparator based on the specified predicate
|
||||||
|
* @param {string} predicate Predicate / propterty name
|
||||||
|
* @return {Function} Comparator
|
||||||
|
*/
|
||||||
|
function sortBy(predicate) {
|
||||||
|
return (a, b) => {
|
||||||
|
if (a[predicate] === b[predicate]) {
|
||||||
|
if (a.name == b.name) {
|
||||||
|
a.buildName.toLowerCase() > b.buildName.toLowerCase() ? 1 : -1;
|
||||||
|
}
|
||||||
|
return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1;
|
||||||
|
}
|
||||||
|
if (typeof a[predicate] == 'string') {
|
||||||
|
return a[predicate].toLowerCase() > b[predicate].toLowerCase() ? 1 : -1;
|
||||||
|
}
|
||||||
|
return a[predicate] > b[predicate] ? 1 : -1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comparison Page
|
||||||
|
*/
|
||||||
|
export default class ComparisonPage extends Page {
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param {Object} props React Component properties
|
||||||
|
* @param {Object} context React Component context
|
||||||
|
*/
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this._sortShips = this._sortShips.bind(this);
|
||||||
|
this._buildsSelected = this._buildsSelected.bind(this);
|
||||||
|
this._updateDiscounts = this._updateDiscounts.bind(this);
|
||||||
|
this.state = this._initState(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Re]Create initial state from context
|
||||||
|
* @param {context} context React component context
|
||||||
|
* @return {Object} New state object
|
||||||
|
*/
|
||||||
|
_initState(context) {
|
||||||
|
let defaultFacets = [13, 12, 11, 9, 6, 4, 1, 3, 2]; // Reverse order of Armour, Shields, Speed, Jump Range, Cargo Capacity, Cost, DPS, EPS, HPS
|
||||||
|
let params = context.route.params;
|
||||||
|
let code = params.code;
|
||||||
|
let name = params.name ? decodeURIComponent(params.name) : null;
|
||||||
|
let newName = '';
|
||||||
|
let compareMode = !code;
|
||||||
|
let builds = [];
|
||||||
|
let saved = false;
|
||||||
|
let predicate = 'name';
|
||||||
|
let desc = false;
|
||||||
|
let importObj = {};
|
||||||
|
|
||||||
|
if (compareMode) {
|
||||||
|
if (name == 'all') {
|
||||||
|
let allBuilds = Persist.getBuilds();
|
||||||
|
newName = name;
|
||||||
|
for (let shipId in allBuilds) {
|
||||||
|
for (let buildName in allBuilds[shipId]) {
|
||||||
|
if (buildName && allBuilds[shipId][buildName]) {
|
||||||
|
builds.push(
|
||||||
|
this._createBuild(
|
||||||
|
shipId,
|
||||||
|
buildName,
|
||||||
|
allBuilds[shipId][buildName]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let comparisonData = Persist.getComparison(name);
|
||||||
|
if (comparisonData) {
|
||||||
|
defaultFacets = comparisonData.facets;
|
||||||
|
comparisonData.builds.forEach(b =>
|
||||||
|
builds.push(this._createBuild(b.shipId, b.buildName))
|
||||||
|
);
|
||||||
|
saved = true;
|
||||||
|
newName = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
let comparisonData = toComparison(code);
|
||||||
|
defaultFacets = comparisonData.f;
|
||||||
|
newName = name = comparisonData.n;
|
||||||
|
predicate = comparisonData.p;
|
||||||
|
desc = comparisonData.d;
|
||||||
|
comparisonData.b.forEach(build => {
|
||||||
|
builds.push(this._createBuild(build.s, build.n, build.c));
|
||||||
|
if (!importObj[build.s]) {
|
||||||
|
importObj[build.s] = {};
|
||||||
|
}
|
||||||
|
importObj[build.s][build.n] = build.c;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw { type: 'bad-comparison', message: e.message, details: e };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let facets = [];
|
||||||
|
let selectedLength = defaultFacets.length;
|
||||||
|
let selectedFacets = new Array(selectedLength);
|
||||||
|
|
||||||
|
for (let i = 0; i < ShipFacets.length; i++) {
|
||||||
|
let facet = Object.assign({}, ShipFacets[i]);
|
||||||
|
let defaultIndex = defaultFacets.indexOf(facet.i);
|
||||||
|
if (defaultIndex == -1) {
|
||||||
|
facets.push(facet);
|
||||||
|
} else {
|
||||||
|
facet.active = true;
|
||||||
|
selectedFacets[selectedLength - defaultIndex - 1] = facet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
facets = selectedFacets.concat(facets);
|
||||||
|
builds.sort(sortBy(predicate));
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: 'Coriolis EDCD Edition - Compare',
|
||||||
|
predicate,
|
||||||
|
desc,
|
||||||
|
facets,
|
||||||
|
builds,
|
||||||
|
compareMode,
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
newName,
|
||||||
|
saved,
|
||||||
|
importObj
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Create a Ship instance / build
|
||||||
|
* @param {string} id Ship Id
|
||||||
|
* @param {name} name Build name
|
||||||
|
* @param {string} code Optional - Serialized ship code
|
||||||
|
* @return {Object} Ship instance with build name
|
||||||
|
*/
|
||||||
|
_createBuild(id, name, code) {
|
||||||
|
code = code ? code : Persist.getBuild(id, name); // Retrieve build code if not passed
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
// No build found
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = Ships[id]; // Get ship properties
|
||||||
|
let b = new Ship(id, data.properties, data.slots); // Create a new Ship instance
|
||||||
|
b.buildFrom(code); // Populate components from code
|
||||||
|
b.buildName = name;
|
||||||
|
b.applyDiscounts(Persist.getShipDiscount(), Persist.getModuleDiscount());
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update state with the specified sort predicates
|
||||||
|
* @param {String} predicate Sort predicate - property name
|
||||||
|
*/
|
||||||
|
_sortShips(predicate) {
|
||||||
|
let { builds, desc } = this.state;
|
||||||
|
if (this.state.predicate == predicate) {
|
||||||
|
desc = !desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
builds.sort(sortBy(predicate));
|
||||||
|
|
||||||
|
if (desc) {
|
||||||
|
builds.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ predicate, desc });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show selected builds modal
|
||||||
|
*/
|
||||||
|
_selectBuilds() {
|
||||||
|
this.context.showModal(
|
||||||
|
<ModalCompare
|
||||||
|
onSelect={this._buildsSelected}
|
||||||
|
builds={this.state.builds}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update selected builds with new list
|
||||||
|
* @param {Array} newBuilds List of new builds
|
||||||
|
*/
|
||||||
|
_buildsSelected(newBuilds) {
|
||||||
|
this.context.hideModal();
|
||||||
|
let builds = [];
|
||||||
|
|
||||||
|
for (let b of newBuilds) {
|
||||||
|
builds.push(this._createBuild(b.id, b.buildName));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ builds, saved: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle facet display
|
||||||
|
* @param {string} facet Facet / Ship Property
|
||||||
|
*/
|
||||||
|
_toggleFacet(facet) {
|
||||||
|
facet.active = !facet.active;
|
||||||
|
this.setState({ facets: [].concat(this.state.facets), saved: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle facet drag
|
||||||
|
* @param {Event} e Drag Event
|
||||||
|
*/
|
||||||
|
_facetDrag(e) {
|
||||||
|
this.nodeAfter = false;
|
||||||
|
this.dragged = e.currentTarget;
|
||||||
|
let placeholder = (this.placeholder = document.createElement('li'));
|
||||||
|
placeholder.style.width = Math.round(this.dragged.offsetWidth) + 'px';
|
||||||
|
placeholder.className = 'facet-placeholder';
|
||||||
|
if (!browser || (browser.name !== 'edge' && browser.name !== 'ie')) {
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('text/html', e.currentTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle facet drop
|
||||||
|
* @param {Event} e Drop Event
|
||||||
|
*/
|
||||||
|
_facetDrop(e) {
|
||||||
|
this.dragged.parentNode.removeChild(this.placeholder);
|
||||||
|
let facets = this.state.facets;
|
||||||
|
let frm = Number(this.dragged.dataset.i);
|
||||||
|
let to = Number(this.over.dataset.i);
|
||||||
|
|
||||||
|
if (frm < to) {
|
||||||
|
to--;
|
||||||
|
}
|
||||||
|
if (this.nodeAfter) {
|
||||||
|
to++;
|
||||||
|
}
|
||||||
|
|
||||||
|
facets.splice(to, 0, facets.splice(frm, 1)[0]);
|
||||||
|
this.dragged.style.display = null;
|
||||||
|
this.setState({ facets: [].concat(facets), saved: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle facet drag over
|
||||||
|
* @param {Event} e Drag over Event
|
||||||
|
*/
|
||||||
|
_facetDragOver(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (e.target.className == 'facet-placeholder') {
|
||||||
|
return;
|
||||||
|
} else if (e.target != e.currentTarget) {
|
||||||
|
this.over = e.target;
|
||||||
|
this.dragged.style.display = 'none';
|
||||||
|
let relX = e.clientX - this.over.getBoundingClientRect().left;
|
||||||
|
let width = this.over.offsetWidth / 2;
|
||||||
|
let parent = e.target.parentNode;
|
||||||
|
|
||||||
|
if (parent == e.currentTarget) {
|
||||||
|
if (relX > width && this.dragged != e.target) {
|
||||||
|
this.nodeAfter = true;
|
||||||
|
parent.insertBefore(this.placeholder, e.target.nextElementSibling);
|
||||||
|
} else {
|
||||||
|
this.nodeAfter = false;
|
||||||
|
parent.insertBefore(this.placeholder, e.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Handle name change and update state
|
||||||
|
* @param {SyntheticEvent} e Event
|
||||||
|
*/
|
||||||
|
_onNameChange(e) {
|
||||||
|
this.setState({ newName: e.target.value, saved: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the current comparison
|
||||||
|
*/
|
||||||
|
_delete() {
|
||||||
|
Persist.deleteComparison(this.state.name);
|
||||||
|
Router.go('/compare');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import the comparison builds
|
||||||
|
*/
|
||||||
|
_import() {
|
||||||
|
let builds = {};
|
||||||
|
|
||||||
|
for (let ship of this.state.builds) {
|
||||||
|
if (!builds[ship.id]) {
|
||||||
|
builds[ship.id] = {};
|
||||||
|
}
|
||||||
|
builds[ship.id][ship.buildName] = ship.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.showModal(<ModalImport builds={builds} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current comparison
|
||||||
|
*/
|
||||||
|
_save() {
|
||||||
|
let { newName, builds, facets } = this.state;
|
||||||
|
let selectedFacets = [];
|
||||||
|
|
||||||
|
facets.forEach(f => {
|
||||||
|
if (f.active) {
|
||||||
|
selectedFacets.unshift(f.i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Persist.saveComparison(newName, builds, selectedFacets);
|
||||||
|
Router.replace(`/compare/${encodeURIComponent(this.state.newName)}`);
|
||||||
|
this.setState({ name: newName, saved: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize and generate a long URL for the current comparison
|
||||||
|
* @return {string} URL for serialized comparison
|
||||||
|
*/
|
||||||
|
_buildUrl() {
|
||||||
|
let { facets, builds, name, predicate, desc } = this.state;
|
||||||
|
let selectedFacets = [];
|
||||||
|
|
||||||
|
for (let f of facets) {
|
||||||
|
if (f.active) {
|
||||||
|
selectedFacets.unshift(f.i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = fromComparison(name, builds, selectedFacets, predicate, desc);
|
||||||
|
let loc = window.location;
|
||||||
|
return (
|
||||||
|
loc.protocol +
|
||||||
|
'//' +
|
||||||
|
loc.host +
|
||||||
|
'/comparison?code=' +
|
||||||
|
encodeURIComponent(code)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the long permalink URL
|
||||||
|
*/
|
||||||
|
_genPermalink() {
|
||||||
|
this.context.showModal(<ModalPermalink url={this._buildUrl()} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate E:D Forum BBCode and show in the export modal
|
||||||
|
*/
|
||||||
|
_genBBcode() {
|
||||||
|
let { translate, formats } = this.context.language;
|
||||||
|
let { facets, builds } = this.state;
|
||||||
|
|
||||||
|
let generator = callback => {
|
||||||
|
let url = this._buildUrl();
|
||||||
|
ShortenUrl(
|
||||||
|
url,
|
||||||
|
shortenedUrl =>
|
||||||
|
callback(
|
||||||
|
comparisonBBCode(translate, formats, facets, builds, shortenedUrl)
|
||||||
|
),
|
||||||
|
error =>
|
||||||
|
callback(comparisonBBCode(translate, formats, facets, builds, url))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.context.showModal(
|
||||||
|
<ModalExport
|
||||||
|
title={translate('forum') + ' BBCode'}
|
||||||
|
generator={generator}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update dimenions from rendered DOM
|
||||||
|
*/
|
||||||
|
_updateDimensions() {
|
||||||
|
this.setState({
|
||||||
|
chartWidth: this.chartRef.offsetWidth
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all ship costs on disount change
|
||||||
|
*/
|
||||||
|
_updateDiscounts() {
|
||||||
|
let shipDiscount = Persist.getShipDiscount();
|
||||||
|
let moduleDiscount = Persist.getModuleDiscount();
|
||||||
|
let builds = [];
|
||||||
|
|
||||||
|
for (let b of this.state.builds) {
|
||||||
|
builds.push(b.applyDiscounts(shipDiscount, moduleDiscount));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ builds });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update state based on context changes
|
||||||
|
* @param {Object} nextProps Incoming/Next properties
|
||||||
|
* @param {Object} nextContext Incoming/Next conext
|
||||||
|
*/
|
||||||
|
componentWillReceiveProps(nextProps, nextContext) {
|
||||||
|
if (this.context.route !== nextContext.route) {
|
||||||
|
// Only reinit state if the route has changed
|
||||||
|
this.setState(this._initState(nextContext));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add listeners when about to mount
|
||||||
|
*/
|
||||||
|
componentWillMount() {
|
||||||
|
this.resizeListener = this.context.onWindowResize(this._updateDimensions);
|
||||||
|
this.persistListener = Persist.addListener(
|
||||||
|
'discounts',
|
||||||
|
this._updateDiscounts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger DOM updates on mount
|
||||||
|
*/
|
||||||
|
componentDidMount() {
|
||||||
|
this._updateDimensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove listeners on unmount
|
||||||
|
*/
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.resizeListener.remove();
|
||||||
|
this.persistListener.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the Page
|
||||||
|
* @return {React.Component} The page contents
|
||||||
|
*/
|
||||||
|
renderPage() {
|
||||||
|
let translate = this.context.language.translate;
|
||||||
|
let compareHeader;
|
||||||
|
let {
|
||||||
|
newName,
|
||||||
|
name,
|
||||||
|
saved,
|
||||||
|
builds,
|
||||||
|
facets,
|
||||||
|
predicate,
|
||||||
|
desc,
|
||||||
|
chartWidth
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (this.state.compareMode) {
|
||||||
|
compareHeader = (
|
||||||
|
<tr>
|
||||||
|
<td className="head">{translate('comparison')}</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
value={newName}
|
||||||
|
onChange={this._onNameChange}
|
||||||
|
placeholder={translate('Enter Name')}
|
||||||
|
maxLength="50"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={this._save}
|
||||||
|
disabled={!newName || newName == 'all' || saved}
|
||||||
|
>
|
||||||
|
<FloppyDisk className="lg" />
|
||||||
|
<span className="button-lbl">{translate('save')}</span>
|
||||||
|
</button>
|
||||||
|
<button onClick={this._delete} disabled={name == 'all' || !saved}>
|
||||||
|
<Bin className="lg warning" />
|
||||||
|
</button>
|
||||||
|
<button onClick={this._selectBuilds}>
|
||||||
|
<Rocket className="lg" />
|
||||||
|
<span className="button-lbl">{translate('builds')}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="r"
|
||||||
|
onClick={this._genPermalink}
|
||||||
|
disabled={builds.length == 0}
|
||||||
|
>
|
||||||
|
<LinkIcon className="lg" />
|
||||||
|
<span className="button-lbl">{translate('permalink')}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="r"
|
||||||
|
onClick={this._genBBcode}
|
||||||
|
disabled={builds.length == 0}
|
||||||
|
>
|
||||||
|
<Embed className="lg" />
|
||||||
|
<span className="button-lbl">{translate('forum')}</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
compareHeader = (
|
||||||
|
<tr>
|
||||||
|
<td className="head">{translate('comparison')}</td>
|
||||||
|
<td>
|
||||||
|
<h3>{name}</h3>
|
||||||
|
<button className="r" onClick={this._import}>
|
||||||
|
<Download className="lg" />
|
||||||
|
{translate('import')}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={'page'}
|
||||||
|
style={{ fontSize: this.context.sizeRatio + 'em' }}
|
||||||
|
>
|
||||||
|
<table id="comparison">
|
||||||
|
<tbody>
|
||||||
|
{compareHeader}
|
||||||
|
<tr key="facets">
|
||||||
|
<td className="head">{translate('compare')}</td>
|
||||||
|
<td>
|
||||||
|
<ul id="facet-container" onDragOver={this._facetDragOver}>
|
||||||
|
{facets.map((f, i) => (
|
||||||
|
<li
|
||||||
|
key={f.title}
|
||||||
|
data-i={i}
|
||||||
|
draggable="true"
|
||||||
|
onDragStart={this._facetDrag}
|
||||||
|
onDragEnd={this._facetDrop}
|
||||||
|
className={cn('facet', { active: f.active })}
|
||||||
|
onClick={this._toggleFacet.bind(this, f)}
|
||||||
|
>
|
||||||
|
{'↔ ' + translate(f.title)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<ComparisonTable
|
||||||
|
builds={builds}
|
||||||
|
facets={facets}
|
||||||
|
onSort={this._sortShips}
|
||||||
|
predicate={predicate}
|
||||||
|
desc={desc}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!builds.length ? (
|
||||||
|
<div className="chart" ref={node => (this.chartRef = node)}>
|
||||||
|
{translate('PHRASE_NO_BUILDS')}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
facets.filter(f => f.active).map((f, i) => (
|
||||||
|
<div
|
||||||
|
key={f.title}
|
||||||
|
className="chart"
|
||||||
|
ref={i == 0 ? node => (this.chartRef = node) : null}
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
className="ptr"
|
||||||
|
onClick={this._sortShips.bind(this, f.props[0])}
|
||||||
|
>
|
||||||
|
{translate(f.title)}
|
||||||
|
</h3>
|
||||||
|
<BarChart
|
||||||
|
width={chartWidth}
|
||||||
|
data={builds}
|
||||||
|
properties={f.props}
|
||||||
|
labels={f.lbls}
|
||||||
|
unit={translate(f.unit)}
|
||||||
|
format={f.fmt}
|
||||||
|
title={translate(f.title)}
|
||||||
|
predicate={predicate}
|
||||||
|
desc={desc}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
|
|||||||
* Unexpected Error page / block
|
* Unexpected Error page / block
|
||||||
*/
|
*/
|
||||||
export default class ErrorDetails extends React.Component {
|
export default class ErrorDetails extends React.Component {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
route: PropTypes.object.isRequired,
|
route: PropTypes.object.isRequired,
|
||||||
language: PropTypes.object.isRequired
|
language: PropTypes.object.isRequired
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import { shallowEqual } from '../utils/UtilityFunctions';
|
|||||||
* Abstract/Base Page
|
* Abstract/Base Page
|
||||||
*/
|
*/
|
||||||
export default class Page extends React.Component {
|
export default class Page extends React.Component {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
closeMenu: PropTypes.func.isRequired,
|
closeMenu: PropTypes.func.isRequired,
|
||||||
hideModal: PropTypes.func.isRequired,
|
hideModal: PropTypes.func.isRequired,
|
||||||
@@ -49,6 +50,19 @@ export default class Page extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pages are 'pure' components that only render when props, state, or context changes.
|
||||||
|
* This method performs a shallow comparison to determine change.
|
||||||
|
*
|
||||||
|
* @param {Object} np Next/Incoming properties
|
||||||
|
* @param {Object} ns Next/Incoming state
|
||||||
|
* @param {Object} nc Next/Incoming context
|
||||||
|
* @return {Boolean} True if props, state, or context has changed
|
||||||
|
*/
|
||||||
|
shouldComponentUpdate(np, ns, nc) {
|
||||||
|
return !shallowEqual(this.props, np) || !shallowEqual(this.state, ns) || !shallowEqual(this.context, nc);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the window title upon mount
|
* Update the window title upon mount
|
||||||
*/
|
*/
|
||||||
@@ -61,14 +75,6 @@ export default class Page extends React.Component {
|
|||||||
*/
|
*/
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.title = this.state.title || 'Coriolis';
|
document.title = this.state.title || 'Coriolis';
|
||||||
try {
|
|
||||||
(window.adsbygoogle = window.adsbygoogle || []).push({
|
|
||||||
google_ad_client: "ca-pub-3709458261881414",
|
|
||||||
enable_page_level_ads: true
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,4 +97,5 @@ export default class Page extends React.Component {
|
|||||||
}
|
}
|
||||||
return this.renderPage();
|
return this.renderPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +1,102 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Page from './Page';
|
import Page from './Page';
|
||||||
|
import { Ships } from 'coriolis-data/dist';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { Factory } from 'ed-forge';
|
import Ship from '../shipyard/Ship';
|
||||||
import { JUMP_METRICS } from 'ed-forge/lib/src/ship-stats';
|
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||||
import { SizeMap } from '../shipyard/Constants';
|
import { SizeMap } from '../shipyard/Constants';
|
||||||
import Link from '../components/Link';
|
import Link from '../components/Link';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the hardpoints by class/size
|
||||||
|
* @param {Object} slot Hardpoint Slot model
|
||||||
|
*/
|
||||||
|
function countHp(slot) {
|
||||||
|
this.hp[slot.maxClass]++;
|
||||||
|
this.hpCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the internal slots and aggregated properties
|
||||||
|
* @param {Object} slot Internal Slots
|
||||||
|
*/
|
||||||
|
function countInt(slot) {
|
||||||
|
let crEligible = !slot.eligible || slot.eligible.cr;
|
||||||
|
this.int[slot.maxClass - 1]++; // Subtract 1 since there is no Class 0 Internal compartment
|
||||||
|
this.intCount++;
|
||||||
|
this.maxCargo += crEligible ?
|
||||||
|
ModuleUtils.findInternal('cr', slot.maxClass, 'E').cargo :
|
||||||
|
0;
|
||||||
|
|
||||||
|
// if no eligiblity, then assume pce
|
||||||
|
let passSlotType = null;
|
||||||
|
let passSlotRating = null;
|
||||||
|
if (!slot.eligible || slot.eligible.pce) {
|
||||||
|
passSlotType = 'pce';
|
||||||
|
passSlotRating = 'E';
|
||||||
|
} else if (slot.eligible.pci) {
|
||||||
|
passSlotType = 'pci';
|
||||||
|
passSlotRating = 'D';
|
||||||
|
} else if (slot.eligible.pcm) {
|
||||||
|
passSlotType = 'pcm';
|
||||||
|
passSlotRating = 'C';
|
||||||
|
} else if (slot.eligible.pcq) {
|
||||||
|
passSlotType = 'pcq';
|
||||||
|
passSlotRating = 'B';
|
||||||
|
}
|
||||||
|
let passengerBay = passSlotType ?
|
||||||
|
ModuleUtils.findMaxInternal(passSlotType, slot.maxClass, passSlotRating) :
|
||||||
|
null;
|
||||||
|
this.maxPassengers += passengerBay ? passengerBay.passengers : 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate Ship summary and aggregated properties
|
* Generate Ship summary and aggregated properties
|
||||||
* @param {String} shipId Ship Id
|
* @param {String} shipId Ship Id
|
||||||
|
* @param {Object} shipData Ship Default Data
|
||||||
* @return {Object} Ship summary and aggregated properties
|
* @return {Object} Ship summary and aggregated properties
|
||||||
*/
|
*/
|
||||||
function shipSummary(shipId) {
|
function shipSummary(shipId, shipData) {
|
||||||
// Build Ship
|
|
||||||
let ship = Factory.newShip(shipId);
|
|
||||||
|
|
||||||
let coreSizes = ship.readMeta('coreSizes');
|
|
||||||
let { jumpRangeLaden, totalRange } = ship.getMetrics(JUMP_METRICS);
|
|
||||||
let summary = {
|
let summary = {
|
||||||
agility: ship.readProp('pitch') + ship.readProp('yaw') + ship.readProp('roll'),
|
|
||||||
baseArmour: ship.readProp('basearmour'),
|
|
||||||
baseShieldStrength: ship.readProp('baseshieldstrength'),
|
|
||||||
boost: ship.readProp('boost'),
|
|
||||||
class: ship.readMeta('class'),
|
|
||||||
crew: ship.readMeta('crew'),
|
|
||||||
id: shipId,
|
id: shipId,
|
||||||
hardness: ship.readProp('hardness'),
|
|
||||||
hpCount: 0,
|
hpCount: 0,
|
||||||
hullMass: ship.readProp('hullmass'),
|
|
||||||
intCount: 0,
|
intCount: 0,
|
||||||
masslock: ship.readProp('masslock'),
|
beta: shipData.beta,
|
||||||
|
maxCargo: 0,
|
||||||
|
maxPassengers: 0,
|
||||||
hp: [0, 0, 0, 0, 0], // Utility, Small, Medium, Large, Huge
|
hp: [0, 0, 0, 0, 0], // Utility, Small, Medium, Large, Huge
|
||||||
int: [0, 0, 0, 0, 0, 0, 0, 0], // Sizes 1 - 8
|
int: [0, 0, 0, 0, 0, 0, 0, 0], // Sizes 1 - 8
|
||||||
jumpRangeLaden,
|
standard: shipData.slots.standard,
|
||||||
pitch: ship.readProp('pitch'),
|
agility:
|
||||||
retailCost: ship.readMeta('retailCost'),
|
shipData.properties.pitch +
|
||||||
roll: ship.readProp('roll'),
|
shipData.properties.yaw +
|
||||||
speed: ship.readProp('speed'),
|
shipData.properties.roll
|
||||||
standard: [
|
|
||||||
'powerplant',
|
|
||||||
'mainengines',
|
|
||||||
'frameshiftdrive',
|
|
||||||
'lifesupport',
|
|
||||||
'powerdistributor',
|
|
||||||
'radar',
|
|
||||||
'fueltank'
|
|
||||||
].map(k => coreSizes[k]),
|
|
||||||
totalRange,
|
|
||||||
yaw: ship.readProp('yaw'),
|
|
||||||
};
|
};
|
||||||
|
Object.assign(summary, shipData.properties);
|
||||||
|
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||||
|
|
||||||
// Count Hardpoints by class
|
// Build Ship
|
||||||
ship.getHardpoints(undefined, true).forEach(hardpoint => {
|
ship.buildWith(shipData.defaults); // Populate with stock/default components
|
||||||
summary.hp[hardpoint.getSizeNum()]++;
|
ship.hardpoints.forEach(countHp.bind(summary)); // Count Hardpoints by class
|
||||||
summary.hpCount++;
|
ship.internal.forEach(countInt.bind(summary)); // Count Internal Compartments by class
|
||||||
});
|
summary.retailCost = ship.totalCost; // Record Stock/Default/retail cost
|
||||||
// Count Internal Compartments by class
|
ship.optimizeMass({ pd: '1D' }); // Optimize Mass with 1D PD for maximum possible jump range
|
||||||
let maxCargo = 0, maxPassengers = 0;
|
summary.maxJumpRange = ship.unladenRange; // Record Jump Range
|
||||||
ship.getInternals(undefined, true).forEach(internal => {
|
|
||||||
const size = String(internal.getSizeNum());
|
|
||||||
summary.int[size]++;
|
|
||||||
summary.intCount++;
|
|
||||||
|
|
||||||
// Try cargo racks
|
// Best thrusters
|
||||||
try {
|
let th;
|
||||||
internal.setItem('cargorack', size);
|
if (ship.standard[1].maxClass === 3) {
|
||||||
maxCargo += internal.get('cargo');
|
th = 'tz';
|
||||||
} catch {}
|
} else if (ship.standard[1].maxClass === 2) {
|
||||||
// Try economy cabins
|
th = 'u0';
|
||||||
try {
|
} else {
|
||||||
internal.setItem('passengercabins', size < '6' ? size : '6', '1');
|
th = ship.standard[1].maxClass + 'A';
|
||||||
maxPassengers += internal.get('cabincapacity');
|
}
|
||||||
} catch {}
|
|
||||||
});
|
|
||||||
|
|
||||||
summary.maxCargo = maxCargo;
|
ship.optimizeMass({ th, fsd: '2D', ft: '1C' }); // Optmize mass with Max Thrusters
|
||||||
summary.maxPassengers = maxPassengers;
|
summary.topSpeed = ship.topSpeed;
|
||||||
|
summary.topBoost = ship.topBoost;
|
||||||
|
summary.baseArmour = ship.armour;
|
||||||
|
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
@@ -96,23 +117,21 @@ export default class ShipyardPage extends Page {
|
|||||||
|
|
||||||
if (!ShipyardPage.cachedShipSummaries) {
|
if (!ShipyardPage.cachedShipSummaries) {
|
||||||
ShipyardPage.cachedShipSummaries = [];
|
ShipyardPage.cachedShipSummaries = [];
|
||||||
for (let s of Factory.getAllShipTypes()) {
|
for (let s in Ships) {
|
||||||
ShipyardPage.cachedShipSummaries.push(shipSummary(s));
|
ShipyardPage.cachedShipSummaries.push(shipSummary(s, Ships[s]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
title: 'Coriolis EDCD Edition - Shipyard',
|
title: 'Coriolis EDCD Edition - Shipyard',
|
||||||
shipPredicate: 'id',
|
shipPredicate: 'name',
|
||||||
shipDesc: true,
|
shipDesc: true,
|
||||||
shipSummaries: ShipyardPage.cachedShipSummaries,
|
shipSummaries: ShipyardPage.cachedShipSummaries
|
||||||
compare: {},
|
|
||||||
groupCompared: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Higlight the current ship in the table on mouse over
|
* Higlight the current ship in the table
|
||||||
* @param {String} shipId Ship Id
|
* @param {String} shipId Ship Id
|
||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
*/
|
*/
|
||||||
@@ -121,24 +140,6 @@ export default class ShipyardPage extends Page {
|
|||||||
this.setState({ shipId });
|
this.setState({ shipId });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle compare highlighting for ships in the table
|
|
||||||
* @param {String} shipId Ship Id
|
|
||||||
*/
|
|
||||||
_toggleCompare(shipId) {
|
|
||||||
let compare = this.state.compare;
|
|
||||||
compare[shipId] = !compare[shipId];
|
|
||||||
this.setState({ compare });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle grouping of compared ships in the table
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_toggleGroupCompared() {
|
|
||||||
this.setState({ groupCompared: !this.state.groupCompared });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update state with the specified sort predicates
|
* Update state with the specified sort predicates
|
||||||
* @param {String} shipPredicate Sort predicate - property name
|
* @param {String} shipPredicate Sort predicate - property name
|
||||||
@@ -179,27 +180,24 @@ export default class ShipyardPage extends Page {
|
|||||||
style={{ height: '1.5em' }}
|
style={{ height: '1.5em' }}
|
||||||
className={cn({
|
className={cn({
|
||||||
highlighted: noTouch && this.state.shipId === s.id,
|
highlighted: noTouch && this.state.shipId === s.id,
|
||||||
comparehighlight: this.state.compare[s.id],
|
|
||||||
})}
|
})}
|
||||||
onMouseEnter={noTouch && this._highlightShip.bind(this, s.id)}
|
onMouseEnter={noTouch && this._highlightShip.bind(this, s.id)}
|
||||||
onClick={() => this._toggleCompare(s.id)}
|
|
||||||
>
|
>
|
||||||
|
<td className="ri">{s.manufacturer}</td>
|
||||||
<td className="ri">{fInt(s.retailCost)}</td>
|
<td className="ri">{fInt(s.retailCost)}</td>
|
||||||
<td className="ri cap">{translate(SizeMap[s.class])}</td>
|
<td className="ri cap">{translate(SizeMap[s.class])}</td>
|
||||||
<td className="ri">{fInt(s.crew)}</td>
|
<td className="ri">{fInt(s.crew)}</td>
|
||||||
<td className="ri">{s.masslock}</td>
|
<td className="ri">{s.masslock}</td>
|
||||||
<td className="ri">{fInt(s.hullMass)}</td>
|
|
||||||
<td className="ri">{fInt(s.agility)}</td>
|
<td className="ri">{fInt(s.agility)}</td>
|
||||||
|
<td className="ri">{fInt(s.hardness)}</td>
|
||||||
|
<td className="ri">{fInt(s.hullMass)}</td>
|
||||||
<td className="ri">{fInt(s.speed)}</td>
|
<td className="ri">{fInt(s.speed)}</td>
|
||||||
<td className="ri">{fInt(s.boost)}</td>
|
<td className="ri">{fInt(s.boost)}</td>
|
||||||
<td className="ri">{fInt(s.pitch)}</td>
|
|
||||||
<td className="ri">{fInt(s.yaw)}</td>
|
|
||||||
<td className="ri">{fInt(s.roll)}</td>
|
|
||||||
<td className="ri">{fRound(s.jumpRangeLaden)}</td>
|
|
||||||
<td className="ri">{fRound(s.totalRange)}</td>
|
|
||||||
<td className="ri">{fInt(s.hardness)}</td>
|
|
||||||
<td className="ri">{fInt(s.baseArmour)}</td>
|
<td className="ri">{fInt(s.baseArmour)}</td>
|
||||||
<td className="ri">{fInt(s.baseShieldStrength)}</td>
|
<td className="ri">{fInt(s.baseShieldStrength)}</td>
|
||||||
|
<td className="ri">{fInt(s.topSpeed)}</td>
|
||||||
|
<td className="ri">{fInt(s.topBoost)}</td>
|
||||||
|
<td className="ri">{fRound(s.maxJumpRange)}</td>
|
||||||
<td className="ri">{fInt(s.maxCargo)}</td>
|
<td className="ri">{fInt(s.maxCargo)}</td>
|
||||||
<td className="ri">{fInt(s.maxPassengers)}</td>
|
<td className="ri">{fInt(s.maxPassengers)}</td>
|
||||||
<td className="cn">{s.standard[0]}</td>
|
<td className="cn">{s.standard[0]}</td>
|
||||||
@@ -235,7 +233,8 @@ export default class ShipyardPage extends Page {
|
|||||||
let { translate, formats, units } = language;
|
let { translate, formats, units } = language;
|
||||||
let hide = this.context.tooltip.bind(null, null);
|
let hide = this.context.tooltip.bind(null, null);
|
||||||
let fInt = formats.int;
|
let fInt = formats.int;
|
||||||
let { shipSummaries, shipPredicate, shipPredicateIndex, compare, groupCompared } = this.state;
|
let fRound = formats.round;
|
||||||
|
let { shipSummaries, shipPredicate, shipPredicateIndex } = this.state;
|
||||||
let sortShips = (predicate, index) =>
|
let sortShips = (predicate, index) =>
|
||||||
this._sortShips.bind(this, predicate, index);
|
this._sortShips.bind(this, predicate, index);
|
||||||
|
|
||||||
@@ -268,17 +267,8 @@ export default class ShipyardPage extends Page {
|
|||||||
valB = val;
|
valB = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupCompared) {
|
|
||||||
if (compare[a.id] && !compare[b.id]) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (!compare[a.id] && compare[b.id]) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valA == valB) {
|
if (valA == valB) {
|
||||||
if (a.id > b.id) {
|
if (a.name > b.name) {
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -308,13 +298,11 @@ export default class ShipyardPage extends Page {
|
|||||||
style={{ height: '1.5em' }}
|
style={{ height: '1.5em' }}
|
||||||
className={cn({
|
className={cn({
|
||||||
highlighted: noTouch && this.state.shipId === s.id,
|
highlighted: noTouch && this.state.shipId === s.id,
|
||||||
comparehighlight: this.state.compare[s.id],
|
|
||||||
})}
|
})}
|
||||||
onMouseEnter={noTouch && this._highlightShip.bind(this, s.id)}
|
onMouseEnter={noTouch && this._highlightShip.bind(this, s.id)}
|
||||||
onClick={() => this._toggleCompare(s.id)}
|
|
||||||
>
|
>
|
||||||
<td className="le">
|
<td className="le">
|
||||||
<Link href={'/outfit/' + s.id}>{s.id} {s.beta === true ? '(Beta)' : null}</Link>
|
<Link href={'/outfit/' + s.id}>{s.name} {s.beta === true ? '(Beta)' : null}</Link>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -323,15 +311,23 @@ export default class ShipyardPage extends Page {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page" style={{ fontSize: sizeRatio + 'em' }}>
|
<div className="page" style={{ fontSize: sizeRatio + 'em' }}>
|
||||||
<div className="content-wrapper">
|
<div
|
||||||
<div className="shipyard-table-wrapper">
|
style={{
|
||||||
<table style={{ width: '12em', position: 'absolute', zIndex: 1 }} className="shipyard-table">
|
whiteSpace: 'nowrap',
|
||||||
|
margin: '0 auto',
|
||||||
|
fontSize: '0.8em',
|
||||||
|
position: 'relative',
|
||||||
|
display: 'inline-block',
|
||||||
|
maxWidth: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<table style={{ width: '12em', position: 'absolute', zIndex: 1 }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="le rgt"> </th>
|
<th className="le rgt"> </th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="main">
|
<tr className="main">
|
||||||
<th className="sortable le rgt" onClick={sortShips('id')}>
|
<th className="sortable le rgt" onClick={sortShips('name')}>
|
||||||
{translate('ship')}
|
{translate('ship')}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -343,91 +339,114 @@ export default class ShipyardPage extends Page {
|
|||||||
{shipRows}
|
{shipRows}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div style={{ overflowX: 'auto', maxWidth: '100%' }}>
|
<div style={{ overflowX: 'scroll', maxWidth: '100%' }}>
|
||||||
<table style={{ marginLeft: 'calc(12em - 1px)', zIndex: 0 }} className="shipyard-table">
|
<table style={{ marginLeft: 'calc(12em - 1px)', zIndex: 0 }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="main">
|
<tr className="main">
|
||||||
{/* First all headers that spread out over three rows */}
|
<th
|
||||||
{/* cost placeholder */}
|
rowSpan={3}
|
||||||
|
className="sortable"
|
||||||
|
onClick={sortShips('manufacturer')}
|
||||||
|
>
|
||||||
|
{translate('manufacturer')}
|
||||||
|
</th>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
<th rowSpan={3} className="sortable" onClick={sortShips('class')}>
|
<th
|
||||||
|
rowSpan={3}
|
||||||
|
className="sortable"
|
||||||
|
onClick={sortShips('class')}
|
||||||
|
>
|
||||||
{translate('size')}
|
{translate('size')}
|
||||||
</th>
|
</th>
|
||||||
<th rowSpan={3} className="sortable" onClick={sortShips('crew')}>
|
<th
|
||||||
|
rowSpan={3}
|
||||||
|
className="sortable"
|
||||||
|
onClick={sortShips('crew')}
|
||||||
|
>
|
||||||
{translate('crew')}
|
{translate('crew')}
|
||||||
</th>
|
</th>
|
||||||
<th rowSpan={3} className="sortable"
|
<th
|
||||||
|
rowSpan={3}
|
||||||
|
className="sortable"
|
||||||
onMouseEnter={termtip.bind(null, 'mass lock factor')}
|
onMouseEnter={termtip.bind(null, 'mass lock factor')}
|
||||||
onMouseLeave={hide} onClick={sortShips('masslock')}>
|
onMouseLeave={hide}
|
||||||
|
onClick={sortShips('masslock')}
|
||||||
|
>
|
||||||
{translate('MLF')}
|
{translate('MLF')}
|
||||||
</th>
|
</th>
|
||||||
{/* hull mass placeholder */}
|
<th
|
||||||
<th> </th>
|
rowSpan={3}
|
||||||
<th colSpan={6}>{translate('agility')}</th>
|
className="sortable"
|
||||||
<th colSpan={2}>{translate('travel')}</th>
|
onClick={sortShips('agility')}
|
||||||
<th colSpan={3}>{translate('defence')}</th>
|
>
|
||||||
{/* cargo placeholder */}
|
{translate('agility')}
|
||||||
<th> </th>
|
</th>
|
||||||
{/* pax placeholder */}
|
<th
|
||||||
|
rowSpan={3}
|
||||||
|
className="sortable"
|
||||||
|
onMouseEnter={termtip.bind(null, 'hardness')}
|
||||||
|
onMouseLeave={hide}
|
||||||
|
onClick={sortShips('hardness')}
|
||||||
|
>
|
||||||
|
{translate('hrd')}
|
||||||
|
</th>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
|
<th colSpan={4}>{translate('base')}</th>
|
||||||
|
<th colSpan={5}>{translate('max')}</th>
|
||||||
<th className="lft" colSpan={7} />
|
<th className="lft" colSpan={7} />
|
||||||
<th className="lft" colSpan={5} />
|
<th className="lft" colSpan={5} />
|
||||||
<th className="lft" colSpan={8} />
|
<th className="lft" colSpan={8} />
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{/* Now all headers in a second-row */}
|
<th
|
||||||
<th className="sortable lft" onClick={sortShips('retailCost')}>
|
className="sortable lft"
|
||||||
|
onClick={sortShips('retailCost')}
|
||||||
|
>
|
||||||
{translate('cost')}
|
{translate('cost')}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable lft" onClick={sortShips('hullMass')}>
|
<th className="sortable lft" onClick={sortShips('hullMass')}>
|
||||||
{translate('hull')}
|
{translate('hull')}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable lft" onClick={sortShips('agility')}>
|
<th className="sortable lft" onClick={sortShips('speed')}>
|
||||||
{translate('rating')}
|
|
||||||
</th>
|
|
||||||
<th className="sortable" onClick={sortShips('speed')}>
|
|
||||||
{translate('speed')}
|
{translate('speed')}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable" onClick={sortShips('boost')}>
|
<th className="sortable" onClick={sortShips('boost')}>
|
||||||
{translate('boost')}
|
{translate('boost')}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable" onClick={sortShips('pitch')}>
|
|
||||||
{translate('pitch')}
|
|
||||||
</th>
|
|
||||||
<th className="sortable" onClick={sortShips('yaw')}>
|
|
||||||
{translate('yaw')}
|
|
||||||
</th>
|
|
||||||
<th className="sortable" onClick={sortShips('roll')}>
|
|
||||||
{translate('roll')}
|
|
||||||
</th>
|
|
||||||
<th className="sortable lft" onClick={sortShips('jumpRangeLaden')}>
|
|
||||||
{translate('jump')}
|
|
||||||
</th>
|
|
||||||
<th className="sortable" onClick={sortShips('totalRange')}>
|
|
||||||
{translate('range')}
|
|
||||||
</th>
|
|
||||||
<th className="sortable lft" onMouseEnter={termtip.bind(null, 'hardness')}
|
|
||||||
onMouseLeave={hide} onClick={sortShips('hardness')}>
|
|
||||||
{translate('hrd')}
|
|
||||||
</th>
|
|
||||||
<th className="sortable" onClick={sortShips('baseArmour')}>
|
<th className="sortable" onClick={sortShips('baseArmour')}>
|
||||||
{translate('armour')}
|
{translate('armour')}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable" onClick={sortShips('baseShieldStrength')}>
|
<th
|
||||||
|
className="sortable"
|
||||||
|
onClick={sortShips('baseShieldStrength')}
|
||||||
|
>
|
||||||
{translate('shields')}
|
{translate('shields')}
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th className="sortable lft" onClick={sortShips('maxCargo')}>
|
<th className="sortable lft" onClick={sortShips('topSpeed')}>
|
||||||
|
{translate('speed')}
|
||||||
|
</th>
|
||||||
|
<th className="sortable" onClick={sortShips('topBoost')}>
|
||||||
|
{translate('boost')}
|
||||||
|
</th>
|
||||||
|
<th className="sortable" onClick={sortShips('maxJumpRange')}>
|
||||||
|
{translate('jump')}
|
||||||
|
</th>
|
||||||
|
<th className="sortable" onClick={sortShips('maxCargo')}>
|
||||||
{translate('cargo')}
|
{translate('cargo')}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable lft" onClick={sortShips('maxPassengers')}
|
<th className="sortable" onClick={sortShips('maxPassengers')} onMouseEnter={termtip.bind(null, 'passenger capacity')}
|
||||||
onMouseEnter={termtip.bind(null, 'passenger capacity')} onMouseLeave={hide}>
|
onMouseLeave={hide}>
|
||||||
{translate('pax')}
|
{translate('pax')}
|
||||||
</th>
|
</th>
|
||||||
<th className="lft" colSpan={7}>
|
<th className="lft" colSpan={7}>
|
||||||
{translate('core module classes')}
|
{translate('core module classes')}
|
||||||
</th>
|
</th>
|
||||||
<th colSpan={5} className="sortable lft" onClick={sortShips('hpCount')}>
|
<th
|
||||||
|
colSpan={5}
|
||||||
|
className="sortable lft"
|
||||||
|
onClick={sortShips('hpCount')}
|
||||||
|
>
|
||||||
{translate('hardpoints')}
|
{translate('hardpoints')}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
@@ -439,7 +458,6 @@ export default class ShipyardPage extends Page {
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{/* Third row headers, i.e., units */}
|
|
||||||
<th
|
<th
|
||||||
className="sortable lft"
|
className="sortable lft"
|
||||||
onClick={sortShips('retailCost')}
|
onClick={sortShips('retailCost')}
|
||||||
@@ -449,31 +467,12 @@ export default class ShipyardPage extends Page {
|
|||||||
<th className="sortable lft" onClick={sortShips('hullMass')}>
|
<th className="sortable lft" onClick={sortShips('hullMass')}>
|
||||||
{units.T}
|
{units.T}
|
||||||
</th>
|
</th>
|
||||||
{/* agility rating placeholder */}
|
<th className="sortable lft" onClick={sortShips('speed')}>
|
||||||
<th className="lft"> </th>
|
|
||||||
<th className="sortable" onClick={sortShips('speed')}>
|
|
||||||
{units['m/s']}
|
{units['m/s']}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable" onClick={sortShips('boost')}>
|
<th className="sortable" onClick={sortShips('boost')}>
|
||||||
{units['m/s']}
|
{units['m/s']}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable" onClick={sortShips('pitch')}>
|
|
||||||
{units['°/s']}
|
|
||||||
</th>
|
|
||||||
<th className="sortable" onClick={sortShips('yaw')}>
|
|
||||||
{units['°/s']}
|
|
||||||
</th>
|
|
||||||
<th className="sortable" onClick={sortShips('roll')}>
|
|
||||||
{units['°/s']}
|
|
||||||
</th>
|
|
||||||
<th className="sortable lft" onClick={sortShips('jumpRangeLaden')}>
|
|
||||||
{units.LY}
|
|
||||||
</th>
|
|
||||||
<th className="sortable" onClick={sortShips('totalRange')}>
|
|
||||||
{units.LY}
|
|
||||||
</th>
|
|
||||||
<th className="lft"> </th>
|
|
||||||
{/* armour placeholder */}
|
|
||||||
<th> </th>
|
<th> </th>
|
||||||
<th
|
<th
|
||||||
className="sortable"
|
className="sortable"
|
||||||
@@ -481,37 +480,73 @@ export default class ShipyardPage extends Page {
|
|||||||
>
|
>
|
||||||
{units.MJ}
|
{units.MJ}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable lft" onClick={sortShips('maxCargo')}>
|
<th className="sortable lft" onClick={sortShips('topSpeed')}>
|
||||||
|
{units['m/s']}
|
||||||
|
</th>
|
||||||
|
<th className="sortable" onClick={sortShips('topBoost')}>
|
||||||
|
{units['m/s']}
|
||||||
|
</th>
|
||||||
|
<th className="sortable" onClick={sortShips('maxJumpRange')}>
|
||||||
|
{units.LY}
|
||||||
|
</th>
|
||||||
|
<th className="sortable" onClick={sortShips('maxCargo')}>
|
||||||
{units.T}
|
{units.T}
|
||||||
</th>
|
</th>
|
||||||
{/* pax placeholder */}
|
<th> </th>
|
||||||
<th className="lft"> </th>
|
<th
|
||||||
<th className="sortable lft" onMouseEnter={termtip.bind(null, 'power plant')}
|
className="sortable lft"
|
||||||
onMouseLeave={hide} onClick={sortShips('standard', 0)}>
|
onMouseEnter={termtip.bind(null, 'power plant')}
|
||||||
|
onMouseLeave={hide}
|
||||||
|
onClick={sortShips('standard', 0)}
|
||||||
|
>
|
||||||
{'pp'}
|
{'pp'}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable" onMouseEnter={termtip.bind(null, 'thrusters')}
|
<th
|
||||||
onMouseLeave={hide} onClick={sortShips('standard', 1)}>
|
className="sortable"
|
||||||
|
onMouseEnter={termtip.bind(null, 'thrusters')}
|
||||||
|
onMouseLeave={hide}
|
||||||
|
onClick={sortShips('standard', 1)}
|
||||||
|
>
|
||||||
{'th'}
|
{'th'}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable" onMouseEnter={termtip.bind(null, 'frame shift drive')}
|
<th
|
||||||
onMouseLeave={hide} onClick={sortShips('standard', 2)}>
|
className="sortable"
|
||||||
|
onMouseEnter={termtip.bind(null, 'frame shift drive')}
|
||||||
|
onMouseLeave={hide}
|
||||||
|
onClick={sortShips('standard', 2)}
|
||||||
|
>
|
||||||
{'fsd'}
|
{'fsd'}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable" onMouseEnter={termtip.bind(null, 'life support')}
|
<th
|
||||||
onMouseLeave={hide} onClick={sortShips('standard', 3)}>
|
className="sortable"
|
||||||
|
onMouseEnter={termtip.bind(null, 'life support')}
|
||||||
|
onMouseLeave={hide}
|
||||||
|
onClick={sortShips('standard', 3)}
|
||||||
|
>
|
||||||
{'ls'}
|
{'ls'}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable" onMouseEnter={termtip.bind(null, 'power distriubtor')}
|
<th
|
||||||
onMouseLeave={hide} onClick={sortShips('standard', 4)}>
|
className="sortable"
|
||||||
|
onMouseEnter={termtip.bind(null, 'power distriubtor')}
|
||||||
|
onMouseLeave={hide}
|
||||||
|
onClick={sortShips('standard', 4)}
|
||||||
|
>
|
||||||
{'pd'}
|
{'pd'}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable" onMouseEnter={termtip.bind(null, 'sensors')}
|
<th
|
||||||
onMouseLeave={hide} onClick={sortShips('standard', 5)}>
|
className="sortable"
|
||||||
|
onMouseEnter={termtip.bind(null, 'sensors')}
|
||||||
|
onMouseLeave={hide}
|
||||||
|
onClick={sortShips('standard', 5)}
|
||||||
|
>
|
||||||
{'s'}
|
{'s'}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable" onMouseEnter={termtip.bind(null, 'fuel tank')}
|
<th
|
||||||
onMouseLeave={hide} onClick={sortShips('standard', 6)}>
|
className="sortable"
|
||||||
|
onMouseEnter={termtip.bind(null, 'fuel tank')}
|
||||||
|
onMouseLeave={hide}
|
||||||
|
onClick={sortShips('standard', 6)}
|
||||||
|
>
|
||||||
{'ft'}
|
{'ft'}
|
||||||
</th>
|
</th>
|
||||||
<th className="sortable lft" onClick={sortShips('hp', 1)}>
|
<th className="sortable lft" onClick={sortShips('hp', 1)}>
|
||||||
@@ -562,10 +597,6 @@ export default class ShipyardPage extends Page {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="table-tools" >
|
|
||||||
<label><input type="checkbox" checked={this.state.groupCompared} onClick={() => this._toggleGroupCompared()}/>Group highlighted ships</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
1047
src/app/shipyard/Calculations.js
Normal file
1047
src/app/shipyard/Calculations.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,6 @@ export const ModuleGroupToName = {
|
|||||||
ghrp: 'Guardian Hull Reinforcement Package',
|
ghrp: 'Guardian Hull Reinforcement Package',
|
||||||
gmrp: 'Guardian Module Reinforcement Package',
|
gmrp: 'Guardian Module Reinforcement Package',
|
||||||
mahr: 'Meta Alloy Hull Reinforcement Package',
|
mahr: 'Meta Alloy Hull Reinforcement Package',
|
||||||
sua: 'Supercruise Assist',
|
|
||||||
|
|
||||||
// Hard Points
|
// Hard Points
|
||||||
bl: 'Beam Laser',
|
bl: 'Beam Laser',
|
||||||
@@ -207,7 +206,7 @@ export const ShipFacets = [
|
|||||||
i: 9
|
i: 9
|
||||||
},
|
},
|
||||||
{ // 10
|
{ // 10
|
||||||
title: 'farthest range',
|
title: 'fastest range',
|
||||||
props: ['unladenFastestRange', 'ladenFastestRange'],
|
props: ['unladenFastestRange', 'ladenFastestRange'],
|
||||||
lbls: ['unladen', 'laden'],
|
lbls: ['unladen', 'laden'],
|
||||||
unit: 'LY',
|
unit: 'LY',
|
||||||
|
|||||||
13
src/app/shipyard/Modification.js
Executable file
13
src/app/shipyard/Modification.js
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Modification - a modification and its value
|
||||||
|
*/
|
||||||
|
export default class Modification {
|
||||||
|
/**
|
||||||
|
* @param {String} id Unique modification ID
|
||||||
|
* @param {Number} value Value of the modification
|
||||||
|
*/
|
||||||
|
constructor(id, value) {
|
||||||
|
this.id = id;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user