Compare commits

..

8 Commits

Author SHA1 Message Date
willyb321
4d8dce8407 change colour depending on shield 2018-04-28 13:38:55 +10:00
willyb321
8cbcff8878 make shield summary bar blue, change "damage from x" to "x hp" 2018-04-28 13:26:06 +10:00
willyb321
53b30e64b6 more work on summary 2018-04-26 18:24:15 +10:00
willyb321
00b149521d use set pips to calc shield 2018-04-26 17:14:33 +10:00
willyb321
236f8c686a pass pips to summary table 2018-04-26 17:14:20 +10:00
willyb321
5e68685a8b more wip table 2018-04-26 16:23:38 +10:00
willyb321
25e7be9081 fix crash when removing shield 2018-04-26 16:01:50 +10:00
willyb321
fcd8506694 WIP summary table 2018-04-26 15:18:41 +10:00
125 changed files with 21897 additions and 35354 deletions

View File

@@ -1,34 +1,3 @@
{ {
"presets": [ "presets": ["env", "react", "stage-0"]
["@babel/preset-env", {"modules": "commonjs"}],
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-import-meta",
["@babel/plugin-proposal-class-properties", { "loose": true }],
"@babel/plugin-proposal-json-strings",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions",
"@babel/plugin-proposal-export-default-from",
"@babel/plugin-proposal-logical-assignment-operators",
"@babel/plugin-proposal-optional-chaining",
[
"@babel/plugin-proposal-pipeline-operator",
{
"proposal": "minimal"
}
],
"@babel/plugin-proposal-nullish-coalescing-operator",
"@babel/plugin-proposal-do-expressions",
"@babel/plugin-proposal-function-bind"
]
} }

View File

@@ -1,77 +0,0 @@
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

View File

@@ -1,103 +0,0 @@
{
"source": "./src/app",
"includes": ["\\.js$", "\\.jsx$"],
"destination": "./docs",
"index": "./README.md",
"plugins": [
{
"name": "esdoc-standard-plugin",
"option": {
"lint": {
"enable": false
},
"coverage": {
"enable": false
},
"accessor": {
"access": [
"public",
"protected",
"private"
],
"autoPrivate": true
},
"undocumentIdentifier": {
"enable": true
},
"unexportedIdentifier": {
"enable": false
},
"typeInference": {
"enable": true
},
"brand": {
"logo": "./src/images/logo/192x192.png",
"title": "Coriolis",
"description": "Coriolis Shipyard for Elite Dangerous",
"repository": "https://github.com/EDCD/coriolis",
"site": "https://coriolis.io",
"author": "https://github.com/edcd",
"image": "./src/images/logo/192x192.png"
}
}
},
{
"name": "esdoc-ecmascript-proposal-plugin",
"option": {
"all": true
}
},
{
"name": "esdoc-react-plugin"
},
{
"name": "esdoc-standard-plugin",
"option": {
"lint": {
"enable": false
},
"coverage": {
"enable": false
},
"accessor": {
"access": [
"public",
"protected",
"private"
],
"autoPrivate": true
},
"undocumentIdentifier": {
"enable": true
},
"unexportedIdentifier": {
"enable": false
},
"typeInference": {
"enable": true
},
"brand": {
"logo": "./src/images/logo/192x192.png",
"title": "Coriolis",
"description": "Coriolis Shipyard for Elite Dangerous",
"repository": "https://github.com/EDCD/coriolis",
"site": "https://coriolis.io",
"author": "https://github.com/edcd",
"image": "./src/images/logo/192x192.png"
}
}
},
{
"name": "esdoc-jsx-plugin",
"option": {
"enable": true
}
},
{
"name": "esdoc-publish-html-plugin",
"option": {
"template": "./node_modules/esdoc-custom-theme/template"
}
}
]
}

View File

@@ -5,12 +5,11 @@
"jsx": true, "jsx": true,
"classes": true, "classes": true,
"modules": true "modules": true
} },
}, },
"env": { "env": {
"browser": true, "browser": true,
"node": true, "node": true
"es6": true
}, },
"plugins": [ "plugins": [
"react" "react"
@@ -34,6 +33,7 @@
"ClassDeclaration": true "ClassDeclaration": true
} }
}], }],
"no-console": 2,
"brace-style": [2, "1tbs", { "allowSingleLine": true }], "brace-style": [2, "1tbs", { "allowSingleLine": true }],
"comma-style": [2, "last"], "comma-style": [2, "last"],
"indent": [2, 2, { "SwitchCase": 1, "VariableDeclarator": 2 }], "indent": [2, 2, { "SwitchCase": 1, "VariableDeclarator": 2 }],

3
.gitignore vendored
View File

@@ -7,6 +7,3 @@ nginx.pid
/bin /bin
env env
*.swp *.swp
.project
.vscode/
docs/

View File

@@ -1,13 +0,0 @@
image: docker:stable
services:
- docker:dind
stages:
- Build image
docker build:
stage: Build image
script:
- img build --build-arg branch=$CI_COMMIT_REF_NAME -t edcd/coriolis:$CI_COMMIT_REF_NAME .
- echo "$REGISTRY_PASSWORD" | img login --username "$REGISTRY_USER" --password-stdin
- img push edcd/coriolis:$CI_COMMIT_REF_NAME

View File

@@ -8,8 +8,7 @@ cache:
directories: directories:
- node_modules - node_modules
before_install: before_script:
- git clone https://github.com/EDCD/coriolis-data.git ../coriolis-data
script: script:
- npm run lint - npm run lint

View File

@@ -1,35 +0,0 @@
### 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 git checkout ${BRANCH}
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;"]

View File

@@ -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.

View File

@@ -23,14 +23,11 @@ Chat to us on [Discord](https://discord.gg/0uwCh6R62aPRjk9w)!
See the [Developer's Guide](https://github.com/EDCD/coriolis/wiki/Developing-for-Coriolis) in the wiki. See the [Developer's Guide](https://github.com/EDCD/coriolis/wiki/Developing-for-Coriolis) in the wiki.
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.
You can find hosted and compiled versions of these data-jsons under https://coriolis.io/data/ and https://beta.coriolis.io/data/.
You might want to load these as depedency instead of reyling on the npm-dependency.
## License ## License

11
d3-funcs.js vendored Normal file
View File

@@ -0,0 +1,11 @@
export {
axisBottom,
axisLeft,
axisTop,
formatLocale,
line,
scaleBand,
scaleLinear,
scaleOrdinal,
select
} from 'd3';

12477
d3.js vendored

File diff suppressed because it is too large Load Diff

4
d3.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,56 +0,0 @@
version: '2.2'
services:
coriolis_prod:
image: edcd/coriolis:master
build:
dockerfile: Dockerfile
args:
branch: 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
build:
dockerfile: Dockerfile
args:
branch: 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

View File

@@ -1,96 +1,59 @@
worker_processes 1; worker_processes 2;
user nobody nobody; error_log ./nginx.error.log;
error_log /tmp/error.log; worker_rlimit_nofile 8192;
pid /tmp/nginx.pid; pid nginx.pid;
events { events {
worker_connections 1024;
worker_connections 1024; multi_accept on;
} }
http { http {
include /etc/nginx/mime.types; access_log off;
default_type application/octet-stream; charset UTF-8;
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 types {
# Enable gzip compression. text/html html htm shtml;
# Default: off text/css css;
gzip off; text/xml xml rss;
image/gif gif;
image/jpeg jpeg jpg;
application/x-javascript js;
text/plain txt;
image/png png;
image/svg+xml svg;
image/x-icon ico;
application/pdf pdf;
text/cache-manifest appcache;
}
# Compression level (1-9). gzip on;
# 5 is a perfect compromise between size and CPU usage, offering about gzip_vary on;
# 75% reduction for most ASCII files (almost identical to level 9). gzip_proxied any;
# Default: 1 gzip_comp_level 6;
gzip_comp_level 5; gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
# Don't compress anything that's already small and unlikely to shrink much server {
# if at all (the default is 20 bytes, which is bad as that usually leads to listen 3301;
# larger files after gzipping). server_name localhost;
# Default: 20 root ./build/;
gzip_min_length 256; index index.html;
# Compress data even for clients that are connecting to us via proxies, location ~* \.(?:manifest|appcache|html?|xml|json|css|js|map|jpg|jpeg|gif|png|ico|svg|eot|ttf|woff|woff2)$ {
# identified by the "Via" header (required for CloudFront). expires -1;
# Default: off add_header Access-Control-Allow-Origin *;
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-Credentials true;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; 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'; 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; access_log off;
} }
location / {
try_files $uri $uri/ /index.html =404; location / {
} try_files $uri $uri/ /index.html =404;
location /iframe.html { }
try_files $uri $uri/ /iframe.html =404; }
}
}
} }

29249
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,17 @@
{ {
"name": "coriolis_shipyard", "name": "coriolis_shipyard",
"version": "3.0.0", "version": "2.9.11",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/EDCD/coriolis" "url": "https://github.com/EDCD/coriolis"
}, },
"homepage": "https://coriolis.io", "homepage": "https://coriolis.edcd.io",
"bugs": "https://github.com/EDCD/coriolis/issues", "bugs": "https://github.com/EDCD/coriolis/issues",
"private": true, "private": true,
"engine": "node >= 4.8.1", "engine": "node >= 4.8.1",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"prepublish": "rollup -c && uglifyjs d3.js -c -m -o d3.min.js",
"extract-translations": "grep -hroE \"(translate\\('[^']+'\\))|(tip.bind\\(null, '[^']+')\" src/* | grep -oE \"'[^']+'\" | grep -oE \"[^']+\" | sort -u -f", "extract-translations": "grep -hroE \"(translate\\('[^']+'\\))|(tip.bind\\(null, '[^']+')\" src/* | grep -oE \"'[^']+'\" | grep -oE \"[^']+\" | sort -u -f",
"clean": "rimraf build", "clean": "rimraf build",
"start": "node devServer.js", "start": "node devServer.js",
@@ -55,94 +56,61 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.0.0", "appcache-webpack-plugin": "^1.3.0",
"@babel/plugin-proposal-class-properties": "^7.0.0", "babel-core": "*",
"@babel/plugin-proposal-decorators": "^7.0.0", "babel-eslint": "*",
"@babel/plugin-proposal-do-expressions": "^7.0.0", "babel-jest": "*",
"@babel/plugin-proposal-export-default-from": "^7.0.0", "babel-loader": "*",
"@babel/plugin-proposal-export-namespace-from": "^7.0.0", "babel-preset-env": "*",
"@babel/plugin-proposal-function-bind": "^7.0.0", "babel-preset-react": "*",
"@babel/plugin-proposal-function-sent": "^7.0.0", "babel-preset-stage-0": "*",
"@babel/plugin-proposal-json-strings": "^7.0.0", "create-react-class": "^15.6.2",
"@babel/plugin-proposal-logical-assignment-operators": "^7.0.0", "css-loader": "^0.28.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0", "cross-env": "^5.1.4",
"@babel/plugin-proposal-numeric-separator": "^7.0.0", "d3-selection": "1",
"@babel/plugin-proposal-optional-chaining": "^7.0.0", "eslint": "3.19.0",
"@babel/plugin-proposal-pipeline-operator": "^7.0.0", "eslint-plugin-react": "^6.10.3",
"@babel/plugin-proposal-throw-expressions": "^7.0.0", "expose-loader": "^0.7.3",
"@babel/plugin-syntax-dynamic-import": "^7.0.0", "express": "^4.15.2",
"@babel/plugin-syntax-import-meta": "^7.0.0", "extract-text-webpack-plugin": "2.1.0",
"@babel/preset-env": "^7.0.0", "file-loader": "^0.11.1",
"@babel/preset-react": "^7.0.0", "html-webpack-plugin": "^2.28.0",
"appcache-webpack-plugin": "^1.4.0", "jest-cli": "^21.2.1",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.0",
"copy-webpack-plugin": "^4.5.2",
"create-react-class": "^15.6.3",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"d3-selection": "^1.3.2",
"esdoc": "^1.1.0",
"esdoc-custom-theme": "^1.4.2",
"esdoc-ecmascript-proposal-plugin": "^1.0.0",
"esdoc-jsx-plugin": "^1.0.0",
"esdoc-publish-html-plugin": "^1.1.2",
"esdoc-react-plugin": "^1.0.1",
"esdoc-standard-plugin": "^1.0.0",
"eslint": "^5.6.0",
"eslint-plugin-react": "^7.11.1",
"expose-loader": "^0.7.5",
"express": "^4.16.3",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^2.0.0",
"html-webpack-plugin": "^3.0.7",
"jest-cli": "^23.6.0",
"jsen": "^0.6.4", "jsen": "^0.6.4",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"less": "^3.8.1", "less": "^2.7.2",
"less-loader": "^4.1.0", "less-loader": "^4.0.3",
"react-addons-perf": "^15.4.2", "react-addons-perf": "^15.4.2",
"react-container-dimensions": "^1.4.1", "react-measure": "^1.4.7",
"react-testutils-additions": "^16.0.0", "react-testutils-additions": "^15.2.0",
"react-transition-group": "^2.5.0", "react-transition-group": "^1.1.2",
"rimraf": "^2.6.1", "rimraf": "^2.6.1",
"rollup": "^0.66.2", "rollup": "0.41",
"rollup-plugin-node-resolve": "^3.4.0", "rollup-plugin-node-resolve": "3",
"style-loader": "^0.23.0", "style-loader": "^0.16.1",
"uglify-js": "^3.4.9", "uglify-js": "^2.4.11",
"url-loader": "^1.1.1", "url-loader": "^0.5.8",
"webpack": "^4.20.2", "webpack": "^2.4.1",
"webpack-bugsnag-plugins": "^1.2.2", "webpack-dev-server": "^2.4.4",
"webpack-cli": "^3.1.1",
"webpack-dev-server": "^3.1.9",
"webpack-notifier": "^1.6.0", "webpack-notifier": "^1.6.0",
"workbox-webpack-plugin": "^3.6.1" "webpack-bugsnag-plugins": "^1.1.1"
}, },
"sideEffects": false,
"dependencies": { "dependencies": {
"@babel/polyfill": "^7.0.0", "babel-polyfill": "*",
"auto-bind": "^2.1.1",
"browserify-zlib-next": "^1.0.1", "browserify-zlib-next": "^1.0.1",
"classnames": "^2.2.6", "classnames": "^2.2.5",
"coriolis-data": "../coriolis-data", "coriolis-data": "../coriolis-data",
"d3": "^5.7.0", "d3": "4.8.0",
"detect-browser": "^3.0.1", "detect-browser": "^1.7.0",
"ed-forge": "github:EDCD/ed-forge",
"fbemitter": "^2.1.1", "fbemitter": "^2.1.1",
"lodash": "^4.17.11", "lodash": "^4.17.4",
"lz-string": "^1.4.4", "lz-string": "^1.4.4",
"pako": "^1.0.6", "pako": "^1.0.6",
"prop-types": "^15.6.2", "prop-types": "^15.5.8",
"react": "^15.5.4", "react": "^15.5.4",
"react-dom": "^15.5.4", "react-dom": "^15.5.4",
"react-extras": "^0.7.1", "react-number-editor": "Athanasius/react-number-editor.git#miggy",
"react-fuzzy": "^0.5.2", "recharts": "^0.22.3",
"react-ga": "^2.5.3", "superagent": "^3.5.2"
"react-number-editor": "^4.0.3",
"recharts": "^1.2.0",
"register-service-worker": "^1.5.2",
"superagent": "^3.8.3"
} }
} }

View File

@@ -1,18 +1,19 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Router from './Router'; import Router from './Router';
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 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';
@@ -20,12 +21,13 @@ 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 request = require('superagent'); const zlib = require('pako');
/** /**
* Coriolis App * Coriolis App
*/ */
export default class Coriolis extends React.Component { export default class Coriolis extends React.Component {
static childContextTypes = { static childContextTypes = {
closeMenu: PropTypes.func.isRequired, closeMenu: PropTypes.func.isRequired,
hideModal: PropTypes.func.isRequired, hideModal: PropTypes.func.isRequired,
@@ -58,17 +60,17 @@ 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 = {
noTouch: !('ontouchstart' in window || navigator.msMaxTouchPoints || navigator.maxTouchPoints), noTouch: !('ontouchstart' in window || navigator.msMaxTouchPoints || navigator.maxTouchPoints),
page: null, page: null,
announcements: [],
language: getLanguage(Persist.getLangCode()), language: getLanguage(Persist.getLangCode()),
route: {}, route: {},
sizeRatio: Persist.getSizeRatio() sizeRatio: Persist.getSizeRatio()
}; };
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));
@@ -89,25 +91,24 @@ 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.log('Ship import data: ');
console.log(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); 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() {
try {
const announces = await request.get('https://orbis.zone/api/announcement')
.query({ showInCoriolis: true });
this.setState({ announcements: announces.body });
} catch (err) {
console.error(err)
}
}
/** /**
* Updates / Sets the page and route context * Updates / Sets the page and route context
* @param {[type]} page The page to be shown * @param {[type]} page The page to be shown
@@ -133,9 +134,9 @@ export default class Coriolis extends React.Component {
console && console.error && console.error(arguments); // eslint-disable-line no-console console && console.error && console.error(arguments); // eslint-disable-line no-console
if (errObj) { if (errObj) {
if (errObj instanceof Error) { if (errObj instanceof Error) {
bugsnagClient.notify(errObj); // eslint-disable-line bugsnagClient.notify(errObj) // eslint-disable-line
} else if (errObj instanceof String) { } else if (errObj instanceof String) {
bugsnagClient.notify(msg, errObj); // eslint-disable-line bugsnagClient.notify(msg, errObj) // eslint-disable-line
} }
} }
this.setState({ this.setState({
@@ -179,13 +180,13 @@ export default class Coriolis extends React.Component {
case 72: // 'h' case 72: // 'h'
if (e.ctrlKey || e.metaKey) { // CTRL/CMD + h if (e.ctrlKey || e.metaKey) { // CTRL/CMD + h
e.preventDefault(); e.preventDefault();
this._showModal(<ModalHelp/>); this._showModal(<ModalHelp />);
} }
break; break;
case 73: // 'i' case 73: // 'i'
if (e.ctrlKey || e.metaKey) { // CTRL/CMD + i if (e.ctrlKey || e.metaKey) { // CTRL/CMD + i
e.preventDefault(); e.preventDefault();
this._showModal(<ModalImport/>); this._showModal(<ModalImport />);
} }
break; break;
case 79: // 'o' case 79: // 'o'
@@ -207,7 +208,7 @@ export default class Coriolis extends React.Component {
* @param {React.Component} content Modal Content * @param {React.Component} content Modal Content
*/ */
_showModal(content) { _showModal(content) {
let modal = <div className='modal-bg' onClick={(e) => this._hideModal()}>{content}</div>; let modal = <div className='modal-bg' onClick={(e) => this._hideModal() }>{content}</div>;
this.setState({ modal }); this.setState({ modal });
} }
@@ -285,7 +286,7 @@ export default class Coriolis extends React.Component {
return this.emitter.addListener('windowResize', listener); return this.emitter.addListener('windowResize', listener);
} }
/** /**
* Add a listener to global commands such as save, * Add a listener to global commands such as save,
* @param {Function} listener Listener callback * @param {Function} listener Listener callback
* @return {Object} Subscription token * @return {Object} Subscription token
@@ -321,50 +322,14 @@ export default class Coriolis extends React.Component {
*/ */
componentWillMount() { componentWillMount() {
// Listen for appcache updated event, present refresh to update view // Listen for appcache updated event, present refresh to update view
// Check that service workers are registered if (window.applicationCache) {
if (navigator.storage && navigator.storage.persist) { window.applicationCache.addEventListener('updateready', () => {
window.addEventListener('load', () => { if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
navigator.storage.persist().then(granted => { this.setState({ appCacheUpdate: true }); // Browser downloaded a new app cache.
if (granted) }
console.log('Storage will not be cleared except by explicit user action');
else
console.log('Storage may be cleared by the UA under storage pressure.');
});
}); });
} }
if ('serviceWorker' in navigator) {
// Your service-worker.js *must* be located at the top-level directory relative to your site.
// It won't be able to control pages unless it's located at the same level or higher than them.
// *Don't* register service worker file in, e.g., a scripts/ sub-directory!
// See https://github.com/slightlyoff/ServiceWorker/issues/468
const self = this;
if (process.env.NODE_ENV === 'production') {
register('/service-worker.js', {
ready(registration) {
console.log('Service worker is active.');
},
registered(registration) {
console.log('Service worker has been registered.');
},
cached(registration) {
console.log('Content has been cached for offline use.');
},
updatefound(registration) {
console.log('New content is downloading.');
},
updated(registration) {
self.setState({ appCacheUpdate: true });
console.log('New content is available; please refresh.');
},
offline() {
console.log('No internet connection found. App is running in offline mode.');
},
error(error) {
console.error('Error during service worker registration:', error);
}
});
}
}
window.onerror = this._onError.bind(this); window.onerror = this._onError.bind(this);
window.addEventListener('resize', () => this.emitter.emit('windowResize')); window.addEventListener('resize', () => this.emitter.emit('windowResize'));
document.getElementById('coriolis').addEventListener('scroll', () => this._tooltip()); document.getElementById('coriolis').addEventListener('scroll', () => this._tooltip());
@@ -382,26 +347,14 @@ 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 appCacheUpdate={this.state.appCacheUpdate} currentMenu={currentMenu} />
<Header announcements={this.state.announcements} appCacheUpdate={this.state.appCacheUpdate} { this.state.error ? this.state.error : this.state.page ? React.createElement(this.state.page, { currentMenu }) : <NotFoundPage/> }
currentMenu={currentMenu}/> { this.state.modal }
<div className="announcement-container">{this.state.announcements.map(a => <Announcement { this.state.tooltip }
text={a.message}/>)}</div>
{this.state.error ? this.state.error : this.state.page ? React.createElement(this.state.page, { currentMenu }) :
<NotFoundPage/>}
{this.state.modal}
{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" title="Coriolis Github Project">{window.CORIOLIS_VERSION} - {window.CORIOLIS_DATE}</a>
title="Coriolis Github Project">{window.CORIOLIS_VERSION} - {window.CORIOLIS_DATE}</a>
<br/>
<a
href={'https://github.com/EDCD/coriolis/compare/edcd:develop@{' + window.CORIOLIS_DATE + '}...edcd:develop'}
target="_blank" rel="noopener noreferrer" title={'Coriolis Commits since' + window.CORIOLIS_DATE}>Commits
since last release
({window.CORIOLIS_DATE})</a>
</div> </div>
</footer> </footer>
</div>; </div>;

View File

@@ -257,8 +257,9 @@ 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 || []; if (window.ga) {
_paq.push(['trackPageView']); window.ga('send', 'pageview', path);
}
} }
/** /**

View File

@@ -16,6 +16,7 @@ function isActive(href) {
* Active Link - Highlighted when URL matches window location * Active Link - Highlighted when URL matches window location
*/ */
export default class ActiveLink extends Link { export default class ActiveLink extends Link {
/** /**
* Renders the component * Renders the component
* @return {React.Component} The active link * @return {React.Component} The active link
@@ -28,4 +29,5 @@ export default class ActiveLink extends Link {
return <a {...this.props} className={className} onClick={this.handler}>{this.props.children}</a>; return <a {...this.props} className={className} onClick={this.handler}>{this.props.children}</a>;
} }
} }

View File

@@ -1,29 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { autoBind } from 'react-extras';
/**
* Announcement component
*/
export default class Announcement extends React.Component {
static propTypes = {
text: PropTypes.string
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
autoBind(this);
}
/**
* Renders the announcement
* @return {React.Component} A href element
*/
render() {
return <div className="announcement" >{this.props.text}</div>;
}
}

View File

@@ -1,33 +1,113 @@
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 { getModuleInfo } from 'ed-forge/lib/data/items';
import { groupBy, mapValues, sortBy } from 'lodash';
import autoBind from 'auto-bind';
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',
'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',
// Standard
'gpp': 'guardian',
'gpc': 'guardian',
'ggc': 'guardian'
};
// 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', 'ml'],
'projectiles': ['mc', 'c', 'fc', 'pa', 'rg'],
'ordnance': ['mr', 'tp', 'nl'],
// Utilities
'sb': ['sb'],
'hs': ['hs'],
'defence': ['ch', 'po', 'ec'],
'scanners': ['sc', 'ss', 'cs', 'kw', 'ws'], // Overloaded with internal scanners
// Experimental
'experimental': ['axmc', 'axmr', 'rfl', 'xs', 'sfn'],
// Guardian
'guardian': ['gpp', 'gpc', 'ggc']
}; };
/** /**
* Available modules menu * Available modules menu
*/ */
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,
diffDetails: PropTypes.func, diffDetails: PropTypes.func,
hideSearch: PropTypes.bool,
m: PropTypes.object, m: PropTypes.object,
warning: PropTypes.func, shipMass: PropTypes.number,
slotDiv: PropTypes.object warning: PropTypes.func
};
static defaultProps = {
shipMass: 0
}; };
/** /**
@@ -37,7 +117,7 @@ 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.state = this._initState(props, context); this.state = this._initState(props, context);
} }
@@ -48,199 +128,182 @@ 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 } = props;
const list = [], fuzzy = []; let list, currentGroup;
let currentGroup; let buildGroup = this._buildGroup.bind(
this,
const modules = m.getApplicableItems().map(getModuleInfo); translate,
const groups = mapValues( m,
groupBy(modules, (info) => info.meta.group), warning,
(infos) => groupBy(infos, (info) => info.meta.type), shipMass - (m && m.mass ? m.mass : 0),
(m, event) => {
this._hideDiff(event);
onSelect(m);
}
); );
// Build categories sorted by translated category name
const groupKeys = sortBy(Object.keys(groups), translate); if (modules instanceof Array) {
for (const group of groupKeys) { list = buildGroup(modules[0].grp, modules);
const groupName = translate(group); } else {
if (groupKeys.length > 1) { list = [];
list.push(<div key={`group-${group}`} className="select-category upp">{groupName}</div>); // At present time slots with grouped options (Hardpoints and Internal) can be empty
if (m) {
list.push(<div className='empty-c upp' key='empty' onClick={onSelect.bind(null, null)} >{translate('empty')}</div>);
} }
const categories = groups[group]; // Need to regroup the modules by our own categorisation
const categoryKeys = sortBy(Object.keys(categories), translate); let catmodules = {};
for (const category of categoryKeys) { // Pre-create to preserve ordering
const categoryName = translate(category); for (let cat in CATEGORIES) {
const infos = categories[category]; catmodules[cat] = [];
if (categoryKeys.length > 1) { }
list.push(<div key={`category-${category}`} className="select-group cap">{categoryName}</div>); for (let g in modules) {
const moduleCategory = GRPCAT[g] || g;
const existing = catmodules[moduleCategory] || [];
catmodules[moduleCategory] = existing.concat(modules[g]);
}
for (let category in catmodules) {
let categoryHeader = false;
// Order through CATEGORIES if present
const categories = CATEGORIES[category] || [category];
if (categories && categories.length) {
for (let n in categories) {
const grp = categories[n];
// We now have the group and the category. We might not have any modules, though...
if (modules[grp]) {
// Decide if we need a category header as well as a group header
if (categories.length === 1) {
// Show category header instead of group header
if (m && grp == m.grp) {
list.push(<div ref={(elem) => this.groupElem = elem} key={category} className={'select-category upp'}>{translate(category)}</div>);
} else {
list.push(<div key={category} className={'select-category upp'}>{translate(category)}</div>);
}
} else {
// Show category header as well as group header
if (!categoryHeader) {
list.push(<div key={category} className={'select-category upp'}>{translate(category)}</div>);
categoryHeader = true;
}
if (m && grp == m.grp) {
list.push(<div ref={(elem) => this.groupElem = elem} key={grp} className={'select-group cap'}>{translate(grp)}</div>);
} else {
list.push(<div key={grp} className={'select-group cap'}>{translate(grp)}</div>);
}
}
list.push(buildGroup(grp, modules[grp]));
}
}
} }
list.push(
this._buildGroup(
m,
category,
infos,
),
);
fuzzy.push(
...infos.map((info) => {
const { meta } = info;
const mount = meta.mount ? ' ' + translate(meta.mount) : '';
return {
grp: groupName,
cat: categoryName,
m: info.proto.Item,
name: `${meta.class}${meta.rating}${mount} ${categoryName}`,
};
}),
);
} }
} }
return { list, currentGroup, fuzzy, trackingFocus: false };
return { list, currentGroup };
} }
/** /**
* 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
* @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) {
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) => { count[cls] = ++count[cls] || 1; return count; }, {});
for (let info of sortBy( const itemsPerClass = Math.max.apply(null, Object.keys(tmp).map(key => tmp[key]));
classMapping[clazz],
(info) => info.meta.mount || info.meta.rating,
)) {
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
let disabled = info.props.shieldgenmaximalmass < ship.readProp('hullmass');
if (meta.experimental && !mountedModule.readMeta('experimental')) {
disabled =
4 <=
ship.getHardpoints().filter((m) => m.readMeta('experimental'))
.length;
}
// Default event handlers for objects that are disabled for (let i = 0; i < sortedModules.length; i++) {
let eventHandlers = {}; let m = sortedModules[i];
if (!disabled) { let mount = null;
const showDiff = this._showDiff.bind(this, mountedModule, info); let disabled = false;
const select = (event) => { prevName = m.name
this._hideDiff(event); if (ModuleUtils.isShieldGenerator(m.grp)) {
this.props.onSelect(Item); // Shield generators care about maximum hull mass
}; disabled = mass > m.maxmass;
} else if (m.maxmass) {
eventHandlers = { // Thrusters care about total mass
onMouseEnter: this._over.bind(this, showDiff), disabled = mass + m.mass > m.maxmass;
onTouchStart: this._touchStart.bind(this, showDiff),
onTouchEnd: this._touchEnd.bind(this, select),
onMouseLeave: this._hideDiff,
onClick: select,
};
}
const mountSymbol = MOUNT_MAP[meta.mount];
const li = (
<li key={Item} data-id={Item}
ref={Item === mountedModule.getItem() ? (ref) => { this.activeSlotRef = ref; } : undefined}
className={cn(meta.type === 'armour' ? 'lc' : 'c', {
warning: !disabled && warning && warning(info),
active: mountedModule.getItem() === Item,
disabled,
hardpoint: mountSymbol,
})}
{...eventHandlers}
>{mountSymbol}{meta.type === 'armour' ? Item : `${meta.class}${meta.rating}`}</li>
);
const tail = elems.pop();
let newTail = [tail];
if (tail.length < itemsPerRow) {
// If the row has not grown too long, the new <li> element can be
// added to the row itself
tail.push(li);
} else {
// Otherwise, the last row gets a line break element added and this
// item is put into a new row
tail.push(<br key={elems.length}/>);
newTail.push([li]);
}
elems.push(...newTail);
} }
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 || active) {
eventHandlers = {};
} else {
let showDiff = this._showDiff.bind(this, mountedModule, m);
let select = onSelect.bind(null, m);
eventHandlers = {
onMouseEnter: this._over.bind(this, showDiff),
onTouchStart: this._touchStart.bind(this, showDiff),
onTouchEnd: this._touchEnd.bind(this, select),
onMouseLeave: this._hideDiff,
onClick: select
};
}
switch(m.mount) {
case 'F': mount = <MountFixed className={'lg'} />; break;
case 'G': mount = <MountGimballed className={'lg'}/>; break;
case 'T': mount = <MountTurret className={'lg'}/>; break;
}
if (m.name && m.name === prevName) {
// elems.push(<br key={'b' + m.grp + i} />);
itemsOnThisRow = 0;
}
if (itemsOnThisRow == 6 || i > 0 && sortedModules.length > 3 && itemsPerClass > 2 && m.class != prevClass && (m.rating != prevRating || m.mount)) {
elems.push(<br key={'b' + m.grp + i} />);
itemsOnThisRow = 0;
}
elems.push(
<li key={m.id} className={classes} {...eventHandlers}>
{mount}
{(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}
</li>
);
itemsOnThisRow++;
prevClass = m.class;
prevRating = m.rating;
prevName = m.name;
} }
return <ul key={'ul' + category}>{[].concat(...elems)}</ul>; return <ul key={'modules' + grp} >{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) {
if (this.props.diffDetails) { if (this.props.diffDetails) {
this.touchTimeout = null; this.touchTimeout = null;
// TODO: this.context.tooltip(this.props.diffDetails(m, mm), rect);
// this.context.tooltip(
// this.props.diffDetails(hoveringModule, mountedModule),
// rect,
// );
} }
} }
/**
* Generate tooltip content for the difference between the
* mounted module and the hovered modules
* @returns {React.Component} Search component if available
*/
_showSearch() {
if (this.props.hideSearch) {
return;
}
return (
<FuzzySearch
list={this.state.fuzzy}
keys={['grp', 'name']}
tokenize={true}
className={'input'}
width={'100%'}
style={{ padding: 0 }}
onSelect={e => this.props.onSelect.bind(null, e.m)()}
resultsTemplate={(props, state, styles, clickHandler) => {
return state.results.map((val, i) => {
return (
<div
key={i}
className={'lc'}
onClick={() => clickHandler(i)}
>
{val.name}
</div>
);
});
}}
/>
);
}
/** /**
* Mouse over diff handler * Mouse over diff handler
* @param {Function} showDiff diff tooltip callback * @param {Function} showDiff diff tooltip callback
@@ -286,21 +349,51 @@ export default class AvailableModulesMenu extends TranslatedComponent {
} }
/** /**
* Scroll to mounted (if it exists) module group on mount * 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
*/ */
componentDidMount() { _moduleOrder(a, b) {
if (this.activeSlotRef) { // Named modules go last
this.activeSlotRef.focus(); 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;
} }
/** /**
* Handle focus if the component updates * Scroll to mounted (if it exists) module group on mount
*
*/ */
componentWillUnmount() { componentDidMount() {
if (this.props.slotDiv) { if (this.groupElem) { // Scroll to currently selected group
this.props.slotDiv.focus(); this.node.scrollTop = this.groupElem.offsetTop;
} }
} }
@@ -320,14 +413,14 @@ export default class AvailableModulesMenu extends TranslatedComponent {
render() { render() {
return ( return (
<div ref={node => this.node = node} <div ref={node => this.node = node}
className={cn('select', this.props.className)} className={cn('select', this.props.className)}
onScroll={this._hideDiff} onScroll={this._hideDiff}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation() }
onContextMenu={stopCtxPropagation} onContextMenu={stopCtxPropagation}
> >
{this._showSearch()}
{this.state.list} {this.state.list}
</div> </div>
); );
} }
} }

View File

@@ -38,6 +38,7 @@ function insertLinebreaks(d) {
* Bar Chart * Bar Chart
*/ */
export default class BarChart extends TranslatedComponent { export default class BarChart extends TranslatedComponent {
static defaultProps = { static defaultProps = {
colors: ['#7b6888', '#6b486b', '#3182bd', '#a05d56', '#d0743c'], colors: ['#7b6888', '#6b486b', '#3182bd', '#a05d56', '#d0743c'],
labels: null, labels: null,

View File

@@ -1,7 +1,13 @@
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'; import { Ships } from 'coriolis-data/dist';
import { nameComparator } from '../utils/SlotFunctions';
import { Pip } from './SvgIcons';
import LineChart from '../components/LineChart';
import Slider from '../components/Slider';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import Module from '../shipyard/Module';
/** /**
* Boost displays a boost button that toggles bosot * Boost displays a boost button that toggles bosot
@@ -9,6 +15,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 +26,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 +77,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>
); );
} }

View File

@@ -1,8 +1,8 @@
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 Slider from '../components/Slider'; import Slider from '../components/Slider';
import autoBind from 'auto-bind';
/** /**
* Cargo slider * Cargo slider
@@ -22,7 +22,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);
} }
/** /**

View File

@@ -10,6 +10,7 @@ import { outfitURL } from '../utils/UrlGenerators';
* Comparison Table * Comparison Table
*/ */
export default class ComparisonTable extends TranslatedComponent { export default class ComparisonTable extends TranslatedComponent {
static propTypes = { static propTypes = {
facets: PropTypes.array.isRequired, facets: PropTypes.array.isRequired,
builds: PropTypes.array.isRequired, builds: PropTypes.array.isRequired,

View File

@@ -1,23 +1,23 @@
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/ship-stats';
/** /**
* Cost Section * Cost Section
*/ */
export default class CostSection extends TranslatedComponent { 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 +26,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 +108,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 +127,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)) {
* Set list sort predicate retrofitShip.buildWith(Ships[ship.id].defaults); // Retrofit ship becomes stock build
* @param {string} newPredicate sort predicate this.setState({ retrofitName: null });
*/ update = true;
_sortBy(newPredicate) { } else if (Persist.getBuild(ship.id, retrofitName) != retrofitShip.toString()) {
let { predicate, desc } = this.state; retrofitShip.buildFrom(Persist.getBuild(ship.id, retrofitName)); // Repopulate modules from saved build
update = true;
if (newPredicate == predicate) {
desc = !desc;
} }
this.setState({ predicate: newPredicate, desc }); if (update) { // Update retrofit comparison
this._updateRetrofit(ship, retrofitShip);
}
// Update list of retrofit base build options
this.setState({ buildOptions: Persist.getBuildsNamesFor(ship.id) });
}
/**
* Toggle item cost inclusion in overall total
* @param {Object} item Cost item
*/
_toggleCost(item) {
this.props.ship.setCostIncluded(item, !item.incCost);
this.forceUpdate();
}
/**
* 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 +285,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 +305,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(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(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 +332,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 +347,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'>&#9662;</u></td> <td className='val cen' style={{ borderRight: 'none', width: '1em' }}><u className='primary-disabled'>&#9662;</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 +409,63 @@ export default class CostSection extends TranslatedComponent {
</div>; </div>;
} }
/**
*
* @param {*} modules
*/
_ammoInfo() {
const { ship } = this.props;
const { desc, predicate } = this.state;
let info = [{ /**
key: 'fuel', * Update retrofit costs
item: 'Fuel', * @param {Ship} ship Ship instance
qty: ship.get(FUEL_CAPACITY), * @param {Ship} retrofitShip Retrofit Ship instance
unitCost: 50, */
cost: 50 * ship.get(FUEL_CAPACITY), _updateRetrofit(ship, retrofitShip) {
}]; let retrofitCosts = [];
for (let m of ship.getModules()) { let retrofitTotal = 0, i, l, item;
const rebuilds = m.get('bays') * m.get('rebuildsperbay');
const ammo = (m.get('ammomaximum') + m.get('ammoclipsize')) || rebuilds; if (ship.bulkheads.m.index != retrofitShip.bulkheads.m.index) {
if (ammo) { item = {
const unitCost = m.readMeta('ammocost'); buyClassRating: ship.bulkheads.m.class + ship.bulkheads.m.rating,
info.push({ buyId: ship.bulkheads.m.eddbID,
key: `restock_${m.getSlot()}`, buyPp: ship.bulkheads.m.pp,
rating: m.getClassRating(), buyName: ship.bulkheads.m.name,
item: m.readMeta('type'), sellClassRating: retrofitShip.bulkheads.m.class + retrofitShip.bulkheads.m.rating,
qty: ammo, sellName: retrofitShip.bulkheads.m.name,
unitCost, cost: unitCost * ammo, netCost: ship.bulkheads.discountedCost - retrofitShip.bulkheads.discountedCost,
}); retroItem: retrofitShip.bulkheads
};
retrofitCosts.push(item);
if (retrofitShip.bulkheads.incCost) {
retrofitTotal += item.netCost;
} }
} }
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] };
info = sortBy(info, _sortF); if (slotGroup[i].m) {
if (desc) { item.buyId = slotGroup[i].m.eddbID,
reverse(info); 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;
}
}
}
} }
return info; this.setState({ retrofitCosts, retrofitTotal });
this._sortRetrofit(retrofitCosts, this.state.retroPredicate, this.state.retroDesc);
} }
/** /**
@@ -355,24 +473,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 +494,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 +512,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 +616,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);
}
}
} }
/** /**

View File

@@ -4,8 +4,6 @@ import TranslatedComponent from './TranslatedComponent';
import * as Calc from '../shipyard/Calculations'; 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/ship-stats';
/** /**
* Defence information * Defence information
@@ -17,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
}; };
/** /**
@@ -29,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;
} }
/** /**
@@ -37,126 +52,197 @@ 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') });
// 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.generator * shield.explosive.boosters) - 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.generator * shield.kinetic.boosters) - 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.generator * shield.thermal.boosters) - 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.sys - rawMj)}{units.MJ}</div>);
effectiveShieldExplosiveTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.explosive.sys - rawMj)}{units.MJ}</div>);
effectiveShieldKineticTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.kinetic.sys - rawMj)}{units.MJ}</div>);
effectiveShieldThermalTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.thermal.sys - rawMj)}{units.MJ}</div>);
}
}
// 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 = [];
if (armour.bulkheads > 0) {
armourSourcesTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
effectiveArmourAbsoluteTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
effectiveArmourExplosiveTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
effectiveArmourKineticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
effectiveArmourThermalTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
if (armour.reinforcement > 0) {
armourSourcesTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
effectiveArmourAbsoluteTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
effectiveArmourExplosiveTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
effectiveArmourKineticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
effectiveArmourThermalTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
}
}
const rawArmour = armour.bulkheads + armour.reinforcement;
const armourDamageTakenTt = [];
armourDamageTakenTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.absolute.bulkheads)}</div>);
armourDamageTakenTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.absolute.reinforcement)}</div>);
const armourDamageTakenExplosiveTt = [];
armourDamageTakenExplosiveTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.explosive.bulkheads)}</div>);
armourDamageTakenExplosiveTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.explosive.reinforcement)}</div>);
if (armour.explosive.bulkheads * armour.explosive.reinforcement != 1) effectiveArmourExplosiveTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / (armour.explosive.bulkheads * armour.explosive.reinforcement) - rawArmour)}</div>);
const armourDamageTakenKineticTt = [];
armourDamageTakenKineticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.kinetic.bulkheads)}</div>);
armourDamageTakenKineticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.kinetic.reinforcement)}</div>);
if (armour.kinetic.bulkheads * armour.kinetic.reinforcement != 1) effectiveArmourKineticTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / (armour.kinetic.bulkheads * armour.kinetic.reinforcement) - rawArmour)}</div>);
const armourDamageTakenThermalTt = [];
armourDamageTakenThermalTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.thermal.bulkheads)}</div>);
armourDamageTakenThermalTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.thermal.reinforcement)}</div>);
if (armour.thermal.bulkheads * armour.thermal.reinforcement != 1) effectiveArmourThermalTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / (armour.thermal.bulkheads * armour.thermal.reinforcement) - rawArmour)}</div>);
const effectiveArmourData = [];
const effectiveAbsoluteArmour = armour.total / armour.absolute.total;
effectiveArmourData.push({ value: Math.round(effectiveAbsoluteArmour), label: translate('absolute'), tooltip: effectiveArmourAbsoluteTt });
const effectiveExplosiveArmour = armour.total / armour.explosive.total;
effectiveArmourData.push({ value: Math.round(effectiveExplosiveArmour), label: translate('explosive'), tooltip: effectiveArmourExplosiveTt });
const effectiveKineticArmour = armour.total / armour.kinetic.total;
effectiveArmourData.push({ value: Math.round(effectiveKineticArmour), label: translate('kinetic'), tooltip: effectiveArmourKineticTt });
const effectiveThermalArmour = armour.total / armour.thermal.total;
effectiveArmourData.push({ value: Math.round(effectiveThermalArmour), label: translate('thermal'), tooltip: effectiveArmourThermalTt });
const armourDamageTakenData = [];
armourDamageTakenData.push({ value: Math.round(armour.absolute.total * 100), label: translate('absolute'), tooltip: armourDamageTakenTt });
armourDamageTakenData.push({ value: Math.round(armour.explosive.total * 100), label: translate('explosive'), tooltip: armourDamageTakenExplosiveTt });
armourDamageTakenData.push({ value: Math.round(armour.kinetic.total * 100), label: translate('kinetic'), tooltip: armourDamageTakenKineticTt });
armourDamageTakenData.push({ value: Math.round(armour.thermal.total * 100), label: translate('thermal'), tooltip: armourDamageTakenThermalTt });
return ( 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>
<PieChart data={shieldSourcesData} /> <PieChart data={shieldSourcesData} />
</div> </div>
<div className='group quarter'> <div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_DAMAGE_TAKEN'))} onMouseOut={tooltip.bind(null, null)}>{translate('damage taken')}(%)</h2> <h2 onMouseOver={termtip.bind(null, translate('PHRASE_DAMAGE_TAKEN'))} onMouseOut={tooltip.bind(null, null)}>{translate('damage taken')}(%)</h2>
<VerticalBarChart data={shieldDamageTakenData} yMax={140} /> <VerticalBarChart data={shieldDamageTakenData} yMax={140} />
</div> </div>
<div className='group quarter'> <div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_EFFECTIVE_SHIELD'))} onMouseOut={tooltip.bind(null, null)}>{translate('effective shield')}(MJ)</h2> <h2 onMouseOver={termtip.bind(null, translate('PHRASE_EFFECTIVE_SHIELD'))} onMouseOut={tooltip.bind(null, null)}>{translate('effective shield')}(MJ)</h2>
<VerticalBarChart data={effectiveShieldData} yMax={maxEffectiveShield}/> <VerticalBarChart data={effectiveShieldData} yMax={maxEffectiveShield}/>
</div> </div>
</span> : null } </span> : null }
<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'>

View File

@@ -1,6 +1,7 @@
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 Slider from '../components/Slider'; import Slider from '../components/Slider';
/** /**

View File

@@ -1,59 +1,104 @@
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 { nameComparator } from '../utils/SlotFunctions';
import LineChart from '../components/LineChart'; import LineChart from '../components/LineChart';
import { getBoostMultiplier, getSpeedMultipliers } from 'ed-forge/lib/stats/SpeedProfile'; import Slider from '../components/Slider';
import { ShipProps } from 'ed-forge'; import * as ModuleUtils from '../shipyard/ModuleUtils';
const { LADEN_MASS } = ShipProps; import Module from '../shipyard/Module';
import * as Calc from '../shipyard/Calculations';
/** /**
* 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}
/> />
); );

View File

@@ -1,23 +1,53 @@
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 { nameComparator } from '../utils/SlotFunctions';
import LineChart from '../components/LineChart'; import LineChart from '../components/LineChart';
import Slider from '../components/Slider';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import Module from '../shipyard/Module';
import * as Calc from '../shipyard/Calculations'; import * as Calc from '../shipyard/Calculations';
import { calculateJumpRange } from 'ed-forge/lib/stats/JumpRangeProfile';
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 * Calculate the maximum range for this ship across its applicable mass
* @param {Object} ship The ship * @param {Object} ship The ship
@@ -27,7 +57,7 @@ export default class FSDProfile extends TranslatedComponent {
*/ */
_calcMaxRange(ship, fuel, mass) { _calcMaxRange(ship, fuel, mass) {
// Obtain the maximum range // Obtain the maximum range
return Calc.jumpRange(mass, ship.standard[2].m, Math.min(fuel, ship.standard[2].m.getMaxFuelPerJump()), ship); return Calc.jumpRange(mass, ship.standard[2].m, Math.min(fuel, ship.standard[2].m.getMaxFuelPerJump()));
} }
/** /**
@@ -35,27 +65,36 @@ export default class FSDProfile extends TranslatedComponent {
* @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());
// 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, 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}

View File

@@ -1,8 +1,8 @@
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 Slider from '../components/Slider'; import Slider from '../components/Slider';
import autoBind from 'auto-bind';
/** /**
* Fuel slider * Fuel slider
@@ -22,7 +22,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);
} }
/** /**

View File

@@ -0,0 +1,107 @@
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.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload())) })</span> : null }</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 }
{ showModuleResistances && m.getExplosiveResistance() ? <div className='l'>{translate('explres')}: {formats.pct(m.getExplosiveResistance())}</div> : null }
{ showModuleResistances && m.getKineticResistance() ? <div className='l'>{translate('kinres')}: {formats.pct(m.getKineticResistance())}</div> : null }
{ showModuleResistances && m.getThermalResistance() ? <div className='l'>{translate('thermres')}: {formats.pct(m.getThermalResistance())}</div> : null }
{ m.getIntegrity() ? <div className='l'>{translate('integrity')}: {formats.int(m.getIntegrity())}</div> : null }
{ m && validMods.length > 0 ? <div className='r' ><button onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
</div>
</div>;
} else {
return <div className={'empty'}>{translate('empty')}</div>;
}
}
}

View File

@@ -1,29 +1,32 @@
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 cn from 'classnames';
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';
/** /**
* 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);
} }
/** /**
* Empty all slots * Empty all slots
*/ */
_empty() { _empty() {
// TODO: this.props.ship.emptyWeapons();
// this.props.ship.emptyWeapons(); this.props.onChange();
this._close(); this._close();
} }
@@ -34,8 +37,8 @@ export default class HardpointSlotSection extends SlotSection {
* @param {SyntheticEvent} event Event * @param {SyntheticEvent} event Event
*/ */
_fill(group, mount, event) { _fill(group, mount, event) {
// TODO: this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt'));
// this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt')); this.props.onChange();
this._close(); this._close();
} }
@@ -51,24 +54,32 @@ export default class HardpointSlotSection extends SlotSection {
* @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) {
maxClass={h.getSize()} slots.push(<HardpointSlot
currentMenu={currentMenu} key={i}
drag={this._drag.bind(this, h)} maxClass={h.maxClass}
dragOver={this._dragOverSlot.bind(this, h)} availableModules={() => availableModules.getHps(h.maxClass)}
drop={this._drop} onOpen={this._openMenu.bind(this, h)}
dropClass={this._dropClass(h, originSlot, targetSlot)} onSelect={this._selectModule.bind(this, h)}
m={h} onChange={this.props.onChange}
enabled={h.enabled ? true : false} selected={currentMenu == h}
propsToShow={propsToShow} drag={this._drag.bind(this, h)}
onPropToggle={onPropToggle} dragOver={this._dragOverSlot.bind(this, h)}
/>); drop={this._drop}
dropClass={this._dropClass(h, originSlot, targetSlot)}
ship={ship}
m={h.m}
enabled={h.enabled ? true : false}
/>);
}
} }
return slots; return slots;
@@ -79,68 +90,59 @@ 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} ref={smRef => this.sectionRefArr['emptyall'] = smRef}>{translate('empty all')}</li> <li className='lc' onClick={this._empty}>{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('pl')}</div> <div className='select-group cap'>{translate('pl')}</div>
<ul> <ul>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'pl', 'F')}><MountFixed className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'pl', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'pl', 'G')}><MountGimballed className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'pl', 'G')}><MountGimballed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'pl', 'T')}><MountTurret className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'pl', 'T')}><MountTurret className='lg'/></li>
</ul> </ul>
<div className='select-group cap'>{translate('ul')}</div> <div className='select-group cap'>{translate('ul')}</div>
<ul> <ul>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'ul', 'F')}><MountFixed className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'ul', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'ul', 'G')}><MountGimballed className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'ul', 'G')}><MountGimballed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'ul', 'T')}><MountTurret className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'ul', 'T')}><MountTurret className='lg'/></li>
</ul> </ul>
<div className='select-group cap'>{translate('bl')}</div> <div className='select-group cap'>{translate('bl')}</div>
<ul> <ul>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'bl', 'F')}><MountFixed className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'bl', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'bl', 'G')}><MountGimballed className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'bl', 'G')}><MountGimballed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'bl', 'T')}><MountTurret className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'bl', 'T')}><MountTurret className='lg'/></li>
</ul> </ul>
<div className='select-group cap'>{translate('mc')}</div> <div className='select-group cap'>{translate('mc')}</div>
<ul> <ul>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'mc', 'F')}><MountFixed className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'mc', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'mc', 'G')}><MountGimballed className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'mc', 'G')}><MountGimballed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'mc', 'T')}><MountTurret className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'mc', 'T')}><MountTurret className='lg'/></li>
</ul> </ul>
<div className='select-group cap'>{translate('c')}</div> <div className='select-group cap'>{translate('c')}</div>
<ul> <ul>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'c', 'F')}><MountFixed className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'c', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'c', 'G')}><MountGimballed className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'c', 'G')}><MountGimballed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'c', 'T')}><MountTurret className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'c', 'T')}><MountTurret className='lg'/></li>
</ul> </ul>
<div className='select-group cap'>{translate('fc')}</div> <div className='select-group cap'>{translate('fc')}</div>
<ul> <ul>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'fc', 'F')}><MountFixed className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'fc', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'fc', 'G')}><MountGimballed className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'fc', 'G')}><MountGimballed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'fc', 'T')}><MountTurret className='lg'/></li> <li className='c' onClick={_fill.bind(this, 'fc', 'T')}><MountTurret className='lg'/></li>
</ul> </ul>
<div className='select-group cap'>{translate('pa')}</div> <div className='select-group cap'>{translate('pa')}</div>
<ul> <ul>
<li className='lc' tabIndex="0" onClick={_fill.bind(this, 'pa', 'F')}>{translate('pa')}</li> <li className='lc' onClick={_fill.bind(this, 'pa', 'F')}>{translate('pa')}</li>
</ul>
<div className='select-group cap'>{translate('rg')}</div>
<ul>
<li className='lc' tabIndex="0" onClick={_fill.bind(this, 'rg', 'F')}>{translate('rg')}</li>
</ul> </ul>
<div className='select-group cap'>{translate('nl')}</div> <div className='select-group cap'>{translate('nl')}</div>
<ul> <ul>
<li className='lc' tabIndex="0" onClick={_fill.bind(this, 'nl', 'F')}>{translate('nl')}</li> <li className='lc' onClick={_fill.bind(this, 'nl', 'F')}>{translate('nl')}</li>
</ul>
<div className='select-group cap'>{translate('rfl')}</div>
<ul>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'rfl', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'rfl', 'T')}><MountTurret className='lg'/></li>
</ul> </ul>
</div>; </div>;
} }
} }

View File

@@ -9,14 +9,11 @@ import { Cogs, CoriolisLogo, Hammer, Help, Rocket, StatsBars } from './SvgIcons'
import { Ships } from 'coriolis-data/dist'; import { Ships } from 'coriolis-data/dist';
import Persist from '../stores/Persist'; import Persist from '../stores/Persist';
import { toDetailedExport } from '../shipyard/Serializer'; 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';
import ModalImport from './ModalImport'; import ModalImport from './ModalImport';
import Slider from './Slider'; import Slider from './Slider';
import Announcement from './Announcement';
import { outfitURL } from '../utils/UrlGenerators'; import { outfitURL } from '../utils/UrlGenerators';
const SIZE_MIN = 0.65; const SIZE_MIN = 0.65;
@@ -56,11 +53,12 @@ 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 /**
* @param {Object} props React Component properties * Constructor
* @param {Object} context React Component context * @param {Object} props React Component properties
*/ * @param {Object} context React Component context
*/
constructor(props, context) { constructor(props, context) {
super(props); super(props);
this.shipOrder = Object.keys(Ships).sort(); this.shipOrder = Object.keys(Ships).sort();
@@ -76,11 +74,8 @@ export default class Header extends TranslatedComponent {
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._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._showHelp = this._showHelp.bind(this);
this.update = this.update.bind(this);
this.languageOptions = []; this.languageOptions = [];
this.insuranceOptions = []; this.insuranceOptions = [];
this.state = { this.state = {
@@ -240,43 +235,6 @@ export default class Header extends TranslatedComponent {
/>); />);
}; };
/**
* 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
@@ -348,7 +306,7 @@ export default class Header extends TranslatedComponent {
_getShipsMenu() { _getShipsMenu() {
let shipList = []; let shipList = [];
for (let s of this.shipOrder) { for (let s in Ships) {
shipList.push(<ActiveLink key={s} href={outfitURL(s)} className='block'>{Ships[s].properties.name}</ActiveLink>); shipList.push(<ActiveLink key={s} href={outfitURL(s)} className='block'>{Ships[s].properties.name}</ActiveLink>);
} }
@@ -414,29 +372,6 @@ export default class Header extends TranslatedComponent {
); );
} }
/**
* Generate the announcement menu
* @return {React.Component} Menu
*/
_getAnnouncementsMenu() {
let announcements;
let translate = this.context.language.translate;
if (this.props.announcements) {
announcements = [];
for (let announce of this.props.announcements) {
announcements.push(<Announcement text={announce.message} />);
announcements.push(<hr/>);
}
}
return (
<div className='menu-list' onClick={ (e) => e.stopPropagation() } style={{ whiteSpace: 'nowrap' }}>
{announcements}
<hr />
</div>
);
}
/** /**
* Generate the settings menu * Generate the settings menu
* @return {React.Component} Menu * @return {React.Component} Menu
@@ -495,7 +430,6 @@ export default class Header extends TranslatedComponent {
{translate('builds')} & {translate('comparisons')} {translate('builds')} & {translate('comparisons')}
<li><Link href="#" className='block' onClick={this._showBackup.bind(this)}>{translate('backup')}</Link></li> <li><Link href="#" className='block' onClick={this._showBackup.bind(this)}>{translate('backup')}</Link></li>
<li><Link href="#" className='block' onClick={this._showDetailedExport.bind(this)}>{translate('detailed export')}</Link></li> <li><Link href="#" className='block' onClick={this._showDetailedExport.bind(this)}>{translate('detailed export')}</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._showImport.bind(this)}>{translate('import')}</Link></li>
<li><Link href="#" className='block' onClick={this._showDeleteAll.bind(this)}>{translate('delete all')}</Link></li> <li><Link href="#" className='block' onClick={this._showDeleteAll.bind(this)}>{translate('delete all')}</Link></li>
</ul> </ul>
@@ -508,7 +442,7 @@ export default class Header extends TranslatedComponent {
<td style={{ width: 20 }}><span style={{ fontSize: 30 }}>A</span></td> <td style={{ width: 20 }}><span style={{ fontSize: 30 }}>A</span></td>
</tr> </tr>
<tr> <tr>
<td colSpan='3' style={{ textAlign: 'center', cursor: 'pointer' }} className='primary-disabled cap' onClick={this._resetTextSize.bind(this)}>{translate('reset')}</td> <td colSpan='3' style={{ textAlign: 'center', cursor: 'pointer' }} className='primary-disabled cap' onClick={this._resetTextSize.bind(this)}>{translate('reset')}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -560,15 +494,6 @@ export default class Header extends TranslatedComponent {
} }
} }
async update() {
const reg = await navigator.serviceWorker.getRegistration();
if (!reg || !reg.waiting) {
return window.location.reload();
}
reg.waiting.postMessage('skipWaiting');
window.location.reload();
}
/** /**
* Render the header * Render the header
* @return {React.Component} Header * @return {React.Component} Header
@@ -579,10 +504,7 @@ export default class Header extends TranslatedComponent {
let hasBuilds = Persist.hasBuilds(); let hasBuilds = Persist.hasBuilds();
return ( return (
<header> <header>
{this.props.appCacheUpdate && <div id="app-update" onClick={this.update}>{translate('PHRASE_UPDATE_RDY')}</div>} {this.props.appCacheUpdate && <div id="app-update" onClick={() => window.location.reload() }>{translate('PHRASE_UPDATE_RDY')}</div>}
{this.props.appCacheUpdate ? <a className={'view-changes'} href={'https://github.com/EDCD/coriolis/compare/edcd:develop@{' + window.CORIOLIS_DATE + '}...edcd:develop'} target="_blank">
{'View Release Changes'}
</a> : null}
<Link className='l' href='/' style={{ marginRight: '1em' }} title='Home'><CoriolisLogo className='icon xl' /></Link> <Link className='l' href='/' style={{ marginRight: '1em' }} title='Home'><CoriolisLogo className='icon xl' /></Link>
<div className='l menu'> <div className='l menu'>
@@ -606,23 +528,6 @@ export default class Header extends TranslatedComponent {
{openedMenu == 'comp' ? this._getComparisonsMenu() : null} {openedMenu == 'comp' ? this._getComparisonsMenu() : null}
</div> </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}>
<span className='menu-item-label'>{translate('announcements')}</span>
</div>
{openedMenu == 'announce' ? this._getAnnouncementsMenu() : null}
</div>
{window.location.origin.search('.edcd.io') >= 0 ?
<div className='l menu'>
<a href="https://youtu.be/4SvnLcefhtI" target="_blank">
<div className={cn('menu-header')}>
<Rocket className='warning'/><span className='menu-item-label'>{translate('please migrate to coriolis.io')}</span>
</div>
</a>
</div> : null
}
<div className='r menu'> <div className='r menu'>
<div className={cn('menu-header', { selected: openedMenu == 'settings' })} onClick={this._openSettings}> <div className={cn('menu-header', { selected: openedMenu == 'settings' })} onClick={this._openSettings}>
<Cogs className='xl warning'/><span className='menu-item-label'>{translate('settings')}</span> <Cogs className='xl warning'/><span className='menu-item-label'>{translate('settings')}</span>
@@ -638,4 +543,5 @@ export default class Header extends TranslatedComponent {
</header> </header>
); );
} }
} }

View File

@@ -0,0 +1,94 @@
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}&nbsp;&nbsp;&nbsp;{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.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.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 }
{ m.getHullReinforcement() ? <div className='l'>{translate('armour')}: {formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost') / 10000)}</div> : null }
{ m.getProtection() ? <div className='l'>{translate('protection')}: {formats.rPct(m.getProtection())}</div> : null }
{ m.getIntegrity() ? <div className='l'>{translate('integrity')}: {formats.int(m.getIntegrity())}</div> : null }
{ m && validMods.length > 0 ? <div className='r' ><button onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
</div>
</div>;
} else {
return <div className={'empty'}>{translate('empty')}</div>;
}
}
}

View File

@@ -1,30 +1,42 @@
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 InternalSlot from './InternalSlot';
import * as ModuleUtils from '../shipyard/ModuleUtils'; import * as ModuleUtils from '../shipyard/ModuleUtils';
import { stopCtxPropagation } from '../utils/UtilityFunctions'; import { stopCtxPropagation } from '../utils/UtilityFunctions';
import { canMount } from '../utils/SlotFunctions'; import { canMount } from '../utils/SlotFunctions';
import autoBind from 'auto-bind';
/** /**
* 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);
} }
/** /**
* Empty all slots * Empty all slots
*/ */
_empty() { _empty() {
// TODO: this.props.ship.emptyInternal();
// this.props.ship.emptyInternal(); this.props.onChange();
this._close(); this._close();
} }
@@ -40,6 +52,7 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E')); ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E'));
} }
}); });
this.props.onChange();
this._close(); this._close();
} }
@@ -55,6 +68,7 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('ft', slot.maxClass, 'C')); ship.use(slot, ModuleUtils.findInternal('ft', slot.maxClass, 'C'));
} }
}); });
this.props.onChange();
this._close(); this._close();
} }
@@ -70,6 +84,7 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('pcq', Math.min(slot.maxClass, 6), 'B')); // Passenger cabins top out at 6 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();
} }
@@ -85,6 +100,7 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('pcm', Math.min(slot.maxClass, 6), 'C')); // Passenger cabins top out at 6 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();
} }
@@ -100,6 +116,7 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('pci', Math.min(slot.maxClass, 6), 'D')); // Passenger cabins top out at 6 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();
} }
@@ -115,6 +132,7 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('pce', Math.min(slot.maxClass, 6), 'E')); // Passenger cabins top out at 6 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();
} }
@@ -133,6 +151,7 @@ export default class InternalSlotSection extends SlotSection {
chargeCap += slot.m.recharge; chargeCap += slot.m.recharge;
} }
}); });
this.props.onChange();
this._close(); this._close();
} }
@@ -148,6 +167,7 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('hr', Math.min(slot.maxClass, 5), 'D')); // Hull reinforcements top out at 5D 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();
} }
@@ -163,6 +183,7 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('mrp', Math.min(slot.maxClass, 5), 'D')); // Module reinforcements top out at 5D 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();
} }
@@ -179,20 +200,31 @@ export default class InternalSlotSection extends SlotSection {
*/ */
_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}
/>); />);
} }
@@ -205,23 +237,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' onClick={this._empty}>{translate('empty all')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithCargo}>{translate('cargo')}</li> <li className='lc' onClick={this._fillWithCargo}>{translate('cargo')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithCells}>{translate('scb')}</li> <li className='lc' onClick={this._fillWithCells}>{translate('scb')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithArmor}>{translate('hr')}</li> <li className='lc' onClick={this._fillWithArmor}>{translate('hr')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithModuleReinforcementPackages}>{translate('mrp')}</li> <li className='lc' onClick={this._fillWithModuleReinforcementPackages}>{translate('mrp')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithFuelTanks}>{translate('ft')}</li> <li className='lc' onClick={this._fillWithFuelTanks}>{translate('ft')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithEconomyClassCabins}>{translate('pce')}</li> <li className='lc' onClick={this._fillWithEconomyClassCabins}>{translate('pce')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithBusinessClassCabins}>{translate('pci')}</li> <li className='lc' onClick={this._fillWithBusinessClassCabins}>{translate('pci')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithFirstClassCabins} onKeyDown={ship.luxuryCabins ? '' : this._keyDown}>{translate('pcm')}</li> <li className='lc' onClick={this._fillWithFirstClassCabins}>{translate('pcm')}</li>
{ ship.luxuryCabins ? <li className='lc' tabIndex='0' onClick={this._fillWithLuxuryCabins}>{translate('pcq')}</li> : ''} { ship.luxuryCabins ? <li className='lc' onClick={this._fillWithLuxuryCabins}>{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>;
} }
} }

View File

@@ -1,8 +1,12 @@
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 { nameComparator } from '../utils/SlotFunctions';
import LineChart from '../components/LineChart'; import LineChart from '../components/LineChart';
import Slider from '../components/Slider'; import Slider from '../components/Slider';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import Module from '../shipyard/Module';
import * as Calc from '../shipyard/Calculations'; import * as Calc from '../shipyard/Calculations';
/** /**
@@ -57,7 +61,7 @@ export default class JumpRange extends TranslatedComponent {
const fuel = this.state.fuelLevel * ship.fuelCapacity; const fuel = this.state.fuelLevel * ship.fuelCapacity;
// Obtain the jump range // Obtain the jump range
return Calc.jumpRange(ship.unladenMass + fuel + cargo, fsd, fuel, ship); return Calc.jumpRange(ship.unladenMass + fuel + cargo, fsd, fuel);
} }
/** /**

View File

@@ -1,9 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ContainerDimensions from 'react-container-dimensions'; import Measure from 'react-measure';
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;
@@ -61,17 +67,21 @@ export default class LineChart extends TranslatedComponent {
xAxisScale, xAxisScale,
yScale, yScale,
tipHeight: 2 + (1.2 * (series ? series.length : 0.8)), tipHeight: 2 + (1.2 * (series ? series.length : 0.8)),
dimensions: {
width: 100,
height: 100
}
}; };
} }
/** /**
* Update tooltip content * Update tooltip content
* @param {number} xPos x coordinate * @param {number} xPos x coordinate
* @param {number} width current container width
*/ */
_tooltip(xPos, width) { _tooltip(xPos) {
let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props; let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props;
let { xScale, yScale } = this.state; let { xScale, yScale } = this.state;
let { width } = this.state.dimensions;
let { formats, translate } = this.context.language; let { formats, translate } = this.context.language;
let x0 = xScale.invert(xPos), let x0 = xScale.invert(xPos),
y0 = func(x0), y0 = func(x0),
@@ -110,11 +120,11 @@ export default class LineChart extends TranslatedComponent {
* Update dimensions based on properties and scale * Update dimensions based on properties and scale
* @param {Object} props React Component properties * @param {Object} props React Component properties
* @param {number} scale size ratio / scale * @param {number} scale size ratio / scale
* @param {number} width current width of the container
* @returns {Object} calculated dimensions * @returns {Object} calculated dimensions
*/ */
_updateDimensions(props, scale, width) { _updateDimensions(props, scale) {
const { xMax, xMin, yMin, yMax } = props; const { xMax, xMin, yMin, yMax } = props;
const { width, height } = this.state.dimensions;
const innerWidth = width - MARGIN.left - MARGIN.right; const innerWidth = width - MARGIN.left - MARGIN.right;
const outerHeight = Math.round(width * props.aspect); const outerHeight = Math.round(width * props.aspect);
const innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; const innerHeight = outerHeight - MARGIN.top - MARGIN.bottom;
@@ -139,11 +149,10 @@ export default class LineChart extends TranslatedComponent {
/** /**
* Move and update tooltip * Move and update tooltip
* @param {SyntheticEvent} e Event * @param {SyntheticEvent} e Event
* @param {number} width current container width
*/ */
_moveTip(e, width) { _moveTip(e) {
let clientX = e.touches ? e.touches[0].clientX : e.clientX; let clientX = e.touches ? e.touches[0].clientX : e.clientX;
this._tooltip(Math.round(clientX - e.currentTarget.getBoundingClientRect().left), width); this._tooltip(Math.round(clientX - e.currentTarget.getBoundingClientRect().left));
} }
/** /**
@@ -218,58 +227,57 @@ export default class LineChart extends TranslatedComponent {
* @return {React.Component} Chart SVG * @return {React.Component} Chart SVG
*/ */
render() { render() {
return ( const { innerWidth, outerHeight, innerHeight } = this._updateDimensions(this.props, this.context.sizeRatio);
<ContainerDimensions> const { width, height } = this.state.dimensions;
{ ({ width, height }) => { const { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props;
const { innerWidth, outerHeight, innerHeight } = this._updateDimensions(this.props, this.context.sizeRatio, width, height); const { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state;
const { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props; const line = this.line;
const { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; const lines = seriesLines.map((line, i) => <path key={i} className='line' fill='none' stroke={colors[i]} strokeWidth='1' d={line(seriesData)} />).reverse();
const lines = seriesLines.map((line, i) => <path key={i} className='line' fill='none' stroke={colors[i]} strokeWidth='1' d={line(seriesData)} />).reverse();
const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0; const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0;
const xmark = xMark ? <path key={'mark'} className='line' fill='none' strokeDasharray='5,5' stroke={'#ff8c0d'} strokeWidth='1' d={'M ' + markX + ' ' + innerHeight + ' L ' + markX + ' 0'} /> : ''; const xmark = xMark ? <path key={'mark'} className='line' fill='none' strokeDasharray='5,5' stroke={'#ff8c0d'} strokeWidth='1' d={'M ' + markX + ' ' + innerHeight + ' L ' + markX + ' 0'} /> : '';
return (
<div width={width} height={height}> return (
<svg style={{ width: '100%', height: outerHeight }}> <Measure width='100%' whitelist={['width', 'top']} onMeasure={ (dimensions) => { this.setState({ dimensions }); }}>
<g transform={`translate(${MARGIN.left},${MARGIN.top})`}> <div width={width} height={height}>
<g>{xmark}</g> <svg style={{ width: '100%', height: outerHeight }}>
<g>{lines}</g> <g transform={`translate(${MARGIN.left},${MARGIN.top})`}>
<g className='x axis' ref={(elem) => d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> <g>{xmark}</g>
<text className='cap' y='30' dy='.1em' x={innerWidth / 2} style={{ textAnchor: 'middle' }}> <g>{lines}</g>
<tspan>{xLabel}</tspan> <g className='x axis' ref={(elem) => d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}>
<tspan className='metric'> ({xUnit})</tspan> <text className='cap' y='30' dy='.1em' x={innerWidth / 2} style={{ textAnchor: 'middle' }}>
</text> <tspan>{xLabel}</tspan>
</g> <tspan className='metric'> ({xUnit})</tspan>
<g className='y axis' ref={(elem) => d3.select(elem).call(this.yAxis)}> </text>
<text className='cap' transform='rotate(-90)' y='-50' dy='.1em' x={innerHeight / -2} style={{ textAnchor: 'middle' }}> </g>
<tspan>{yLabel}</tspan> <g className='y axis' ref={(elem) => d3.select(elem).call(this.yAxis)}>
{ yUnit && <tspan className='metric'> ({yUnit})</tspan> } <text className='cap' transform='rotate(-90)' y='-50' dy='.1em' x={innerHeight / -2} style={{ textAnchor: 'middle' }}>
</text> <tspan>{yLabel}</tspan>
</g> { yUnit && <tspan className='metric'> ({yUnit})</tspan> }
<g ref={(g) => this.tipContainer = d3.select(g)} style={{ display: 'none' }}> </text>
<rect className='tooltip' height={tipHeight + 'em'}></rect> </g>
{detailElems} <g ref={(g) => this.tipContainer = d3.select(g)} style={{ display: 'none' }}>
</g> <rect className='tooltip' height={tipHeight + 'em'}></rect>
<g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}> {detailElems}
{markerElems} </g>
</g> <g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}>
<rect {markerElems}
fillOpacity='0' </g>
height={innerHeight} <rect
width={innerWidth + 1} fillOpacity='0'
onMouseEnter={this._showTip} height={innerHeight}
onTouchStart={this._showTip} width={innerWidth + 1}
onMouseLeave={this._hideTip} onMouseEnter={this._showTip}
onTouchEnd={this._hideTip} onTouchStart={this._showTip}
onMouseMove={e => this._moveTip(e, width)} onMouseLeave={this._hideTip}
onTouchMove={e => this._moveTip(e, width)} onTouchEnd={this._hideTip}
/> onMouseMove={this._moveTip}
</g> onTouchMove={this._moveTip}
</svg> />
</div> </g>
); </svg>
}} </div>
</ContainerDimensions> </Measure>
); );
} }
} }

View File

@@ -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>;
} }
} }

View File

@@ -1,92 +0,0 @@
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>;
}
}

View File

@@ -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>
); );

View File

@@ -6,6 +6,7 @@ import Persist from '../stores/Persist';
* Delete All saved data modal * Delete All saved data modal
*/ */
export default class ModalDeleteAll extends TranslatedComponent { export default class ModalDeleteAll extends TranslatedComponent {
/** /**
* Delete everything and hide the modal * Delete everything and hide the modal
*/ */

View File

@@ -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,

View File

@@ -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

View File

@@ -13,8 +13,6 @@ import { Download } from './SvgIcons';
import { outfitURL } from '../utils/UrlGenerators'; import { outfitURL } from '../utils/UrlGenerators';
import * as CompanionApiUtils from '../utils/CompanionApiUtils'; import * as CompanionApiUtils from '../utils/CompanionApiUtils';
const zlib = require('pako');
const textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n'); const textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n');
const lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)'); const lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)');
const mountMap = { 'H': 4, 'L': 3, 'M': 2, 'S': 1, 'U': 0 }; const mountMap = { 'H': 4, 'L': 3, 'M': 2, 'S': 1, 'U': 0 };
@@ -85,6 +83,8 @@ function detailedJsonToBuild(detailedBuild) {
* Import Modal * Import Modal
*/ */
export default class ModalImport extends TranslatedComponent { export default class ModalImport extends TranslatedComponent {
static propTypes = { static propTypes = {
builds: PropTypes.object, // Optional: Import object builds: PropTypes.object, // Optional: Import object
}; };
@@ -99,7 +99,6 @@ export default class ModalImport extends TranslatedComponent {
this.state = { this.state = {
builds: props.builds, builds: props.builds,
canEdit: !props.builds, canEdit: !props.builds,
loadoutEvent: null,
comparisons: null, comparisons: null,
shipDiscount: null, shipDiscount: null,
moduleDiscount: null, moduleDiscount: null,
@@ -112,28 +111,12 @@ export default class ModalImport extends TranslatedComponent {
this._process = this._process.bind(this); this._process = this._process.bind(this);
this._import = this._import.bind(this); this._import = this._import.bind(this);
this._importBackup = this._importBackup.bind(this); this._importBackup = this._importBackup.bind(this);
this._importLoadout = this._importLoadout.bind(this);
this._importDetailedArray = this._importDetailedArray.bind(this); this._importDetailedArray = this._importDetailedArray.bind(this);
this._importTextBuild = this._importTextBuild.bind(this); this._importTextBuild = this._importTextBuild.bind(this);
this._importCompanionApiBuild = this._importCompanionApiBuild.bind(this); this._importCompanionApiBuild = this._importCompanionApiBuild.bind(this);
this._validateImport = this._validateImport.bind(this); this._validateImport = this._validateImport.bind(this);
} }
/**
* Import a Loadout event from Elite: Dangerous journal files
* @param {Object} data Loadout event
* @throws {string} If import fails
*/
_importLoadout(data) {
if (data && data.Ship && data.Modules) {
const deflated = zlib.deflate(JSON.stringify(data), { to: 'string' });
let compressed = btoa(deflated);
this.setState({ loadoutEvent: compressed });
} else {
throw 'Loadout event must contain Ship and Modules';
}
}
/** /**
* Import a Coriolis backup * Import a Coriolis backup
* @param {Object} importData Backup Data * @param {Object} importData Backup Data
@@ -143,11 +126,7 @@ export default class ModalImport extends TranslatedComponent {
if (importData.builds && typeof importData.builds == 'object') { if (importData.builds && typeof importData.builds == 'object') {
for (let shipId in importData.builds) { for (let shipId in importData.builds) {
for (let buildName in importData.builds[shipId]) { for (let buildName in importData.builds[shipId]) {
try { validateBuild(shipId, importData.builds[shipId][buildName], buildName);
validateBuild(shipId, importData.builds[shipId][buildName], buildName);
} catch (err) {
delete importData.builds[shipId][buildName];
}
} }
} }
this.setState({ builds: importData.builds }); this.setState({ builds: importData.builds });
@@ -176,7 +155,7 @@ export default class ModalImport extends TranslatedComponent {
} }
// Check for module discount // Check for module discount
if (!isNaN(importData.moduleDiscount)) { if (!isNaN(importData.moduleDiscount)) {
this.setState({ moduleDiscount: importData.moduleDiscount * 1 }); this.setState({ shipDiscount: importData.moduleDiscount * 1 });
} }
if (typeof importData.insurance == 'string') { if (typeof importData.insurance == 'string') {
@@ -362,14 +341,12 @@ export default class ModalImport extends TranslatedComponent {
} else if (importData.ship && typeof importData.name !== undefined) { // Using JSON from a single ship build export } 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._importDetailedArray([importData]); // Convert to array with singleobject
this.setState({ singleBuild: true }); this.setState({ singleBuild: true });
} else if (importData.Modules != null && importData.Modules[0] != null) {
this._importLoadout(importData);
} else { // Using Backup JSON } else { // Using Backup JSON
this._importBackup(importData); this._importBackup(importData);
} }
} }
} catch (e) { } catch (e) {
console.log(e); // console.log(e.stack);
this.setState({ errorMsg: (typeof e == 'string') ? e : 'Cannot Parse the data!' }); this.setState({ errorMsg: (typeof e == 'string') ? e : 'Cannot Parse the data!' });
return; return;
} }
@@ -383,10 +360,6 @@ export default class ModalImport extends TranslatedComponent {
_process() { _process() {
let builds = null, comparisons = null; let builds = null, comparisons = null;
if (this.state.loadoutEvent) {
return Router.go(`/import?data=${this.state.loadoutEvent}`);
}
// If only importing a single build go straight to the outfitting page // If only importing a single build go straight to the outfitting page
if (this.state.singleBuild) { if (this.state.singleBuild) {
builds = this.state.builds; builds = this.state.builds;
@@ -503,7 +476,7 @@ export default class ModalImport extends TranslatedComponent {
if (!state.processed) { if (!state.processed) {
importStage = ( importStage = (
<div> <div>
<textarea spellCheck={false} className='cb json' ref={node => this.importField = node} onChange={this._validateImport} defaultValue={this.state.importString} placeholder={translate('PHRASE_IMPORT')} /> <textarea className='cb json' ref={node => this.importField = node} onChange={this._validateImport} defaultValue={this.state.importString} placeholder={translate('PHRASE_IMPORT')} />
<button id='proceed' className='l cap' onClick={this._process} disabled={!state.importValid} >{translate('proceed')}</button> <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 className='l warning' style={{ marginLeft:'3em' }}>{state.errorMsg}</div>
</div> </div>
@@ -540,7 +513,7 @@ export default class ModalImport extends TranslatedComponent {
{comparisonRows} {comparisonRows}
</tbody> </tbody>
</table> </table>
); );
} }
if(this.state.canEdit) { if(this.state.canEdit) {

View File

@@ -1,140 +0,0 @@
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>;
}
}

View File

@@ -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
}; };
@@ -49,7 +50,6 @@ export default class ModalPermalink extends TranslatedComponent {
<h3 >{translate('shortened')}</h3> <h3 >{translate('shortened')}</h3>
<input value={this.state.shortenedUrl} readOnly size={25} onFocus={ (e) => e.target.select() }/> <input value={this.state.shortenedUrl} readOnly size={25} onFocus={ (e) => e.target.select() }/>
<br/><br/> <br/><br/>
<p>s.orbis.zone is the new URL shortener domain, old eddp.co urls are considered end of life and could go down at any moment. Sorry for any inconvenience.</p>
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button> <button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
</div>; </div>;
} }

View File

@@ -1,262 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent';
import request from 'superagent';
import Persist from '../stores/Persist';
/**
* Permalink modal
*/
export default class ModalShoppingList extends TranslatedComponent {
static propTypes = {
ship: PropTypes.object.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
this.state = {
matsList: '',
mats: {},
failed: false,
cmdrName: Persist.getCmdr().selected,
cmdrs: Persist.getCmdr().cmdrs,
matsPerGrade: Persist.getRolls(),
blueprints: []
};
}
/**
* React component did mount
*/
componentDidMount() {
this.renderMats();
if (this.checkBrowserIsCompatible()) {
this.getCommanders();
this.registerBPs();
}
}
/**
* Find all blueprints needed to make a build.
*/
registerBPs() {
const ship = this.props.ship;
let blueprints = [];
for (const module of ship.costList) {
if (module.type === 'SHIP') {
continue;
}
if (module.m && module.m.blueprint) {
if (!module.m.blueprint.grade || !module.m.blueprint.grades) {
continue;
}
if (module.m.blueprint.special) {
console.log(module.m.blueprint.special);
blueprints.push({ uuid: module.m.blueprint.special.uuid, number: 1 });
}
for (const g in module.m.blueprint.grades) {
if (!module.m.blueprint.grades.hasOwnProperty(g)) {
continue;
}
if (g > module.m.blueprint.grade) {
continue;
}
blueprints.push({ uuid: module.m.blueprint.grades[g].uuid, number: this.state.matsPerGrade[g] });
}
}
}
this.setState({ blueprints });
}
/**
* Check browser isn't firefox.
* @return {boolean} true if compatible, false if not.
*/
checkBrowserIsCompatible() {
// Firefox 1.0+
return typeof InstallTrigger === 'undefined';
}
/**
* Get a list of commanders from EDEngineer.
*/
getCommanders() {
request
.get('http://localhost:44405/commanders')
.end((err, res) => {
if (err) {
console.log(err);
return this.setState({ failed: true });
}
const cmdrs = JSON.parse(res.text);
if (!this.state.cmdrName) {
this.setState({ cmdrName: cmdrs[0] });
}
this.setState({ cmdrs }, () => {
Persist.setCmdr({ selected: this.state.cmdrName, cmdrs });
});
});
}
/**
* Send all blueprints to ED Engineer
* @param {Event} event React event
*/
sendToEDEng(event) {
event.preventDefault();
let translate = this.context.language.translate;
const target = event.target;
target.disabled = this.state.blueprints.length > 0;
if (this.state.blueprints.length === 0) {
target.innerText = translate('No modded components.');
target.disabled = true;
setTimeout(() => {
target.innerText = translate('Send to EDEngineer');
target.disabled = false;
}, 3000);
} else {
target.innerText = translate('Sending...');
}
let countSent = 0;
let countTotal = this.state.blueprints.length;
for (const i of this.state.blueprints) {
request
.patch(`http://localhost:44405/${this.state.cmdrName}/shopping-list`)
.field('uuid', i.uuid)
.field('size', i.number)
.end(err => {
if (err) {
console.log(err);
if (err.message !== 'Bad Request') {
this.setState({ failed: true });
}
}
countSent++;
if (countSent === countTotal) {
target.disabled = false;
target.innerText = translate('Send to EDEngineer');
}
});
}
}
/**
* Convert mats object to string
*/
renderMats() {
const ship = this.props.ship;
let mats = {};
for (const module of ship.costList) {
if (module.type === 'SHIP') {
continue;
}
if (module.m && module.m.blueprint) {
if (!module.m.blueprint.grade || !module.m.blueprint.grades) {
continue;
}
for (const g in module.m.blueprint.grades) {
if (!module.m.blueprint.grades.hasOwnProperty(g)) {
continue;
}
if (g > module.m.blueprint.grade) {
continue;
}
for (const i in module.m.blueprint.grades[g].components) {
if (!module.m.blueprint.grades[g].components.hasOwnProperty(i)) {
continue;
}
if (mats[i]) {
mats[i] += module.m.blueprint.grades[g].components[i] * this.state.matsPerGrade[g];
} else {
mats[i] = module.m.blueprint.grades[g].components[i] * this.state.matsPerGrade[g];
}
}
}
}
}
let matsString = '';
for (const i in mats) {
if (!mats.hasOwnProperty(i)) {
continue;
}
if (mats[i] === 0) {
delete mats[i];
continue;
}
matsString += `${i}: ${mats[i]}\n`;
}
this.setState({ matsList: matsString, mats });
}
/**
* Handler for changing roll amounts
* @param {SyntheticEvent} e React Event
*/
changeHandler(e) {
let grade = e.target.id;
let newState = this.state.matsPerGrade;
newState[grade] = parseInt(e.target.value);
this.setState({ matsPerGrade: newState });
Persist.setRolls(newState);
this.renderMats();
this.registerBPs();
}
/**
* Handler for changing cmdr name
* @param {SyntheticEvent} e React Event
*/
cmdrChangeHandler(e) {
let cmdrName = e.target.value;
this.setState({ cmdrName }, () => {
Persist.setCmdr({ selected: this.state.cmdrName, cmdrs: this.state.cmdrs });
});
}
/**
* Render the modal
* @return {React.Component} Modal Content
*/
render() {
let translate = this.context.language.translate;
this.changeHandler = this.changeHandler.bind(this);
const compatible = this.checkBrowserIsCompatible();
this.cmdrChangeHandler = this.cmdrChangeHandler.bind(this);
this.sendToEDEng = this.sendToEDEng.bind(this);
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
<h2>{translate('PHRASE_SHOPPING_MATS')}</h2>
<label>{translate('Grade 1 rolls ')}</label>
<input id={1} type={'number'} min={0} defaultValue={this.state.matsPerGrade[1]} onChange={this.changeHandler} />
<br/>
<label>{translate('Grade 2 rolls ')}</label>
<input id={2} type={'number'} min={0} defaultValue={this.state.matsPerGrade[2]} onChange={this.changeHandler} />
<br/>
<label>{translate('Grade 3 rolls ')}</label>
<input id={3} type={'number'} min={0} value={this.state.matsPerGrade[3]} onChange={this.changeHandler} />
<br/>
<label>{translate('Grade 4 rolls ')}</label>
<input id={4} type={'number'} min={0} value={this.state.matsPerGrade[4]} onChange={this.changeHandler} />
<br/>
<label>{translate('Grade 5 rolls ')}</label>
<input id={5} type={'number'} min={0} value={this.state.matsPerGrade[5]} onChange={this.changeHandler} />
<div>
<textarea className='cb json' readOnly value={this.state.matsList} />
</div>
<label hidden={!compatible} className={'l cap'}>{translate('CMDR Name')}</label>
<br/>
<select hidden={!compatible} className={'cmdr-select l cap'} onChange={this.cmdrChangeHandler} defaultValue={this.state.cmdrName}>
{this.state.cmdrs.map(e => <option key={e}>{e}</option>)}
</select>
<br/>
<p hidden={!this.state.failed} id={'failed'} className={'l'}>{translate('PHRASE_FAIL_EDENGINEER')}</p>
<p hidden={compatible} id={'browserbad'} className={'l'}>{translate('PHRASE_FIREFOX_EDENGINEER')}</p>
<button className={'l cb dismiss cap'} disabled={!!this.state.failed || !compatible} onClick={this.sendToEDEng}>{translate('Send to EDEngineer')}</button>
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
</div>;
}
}

View File

@@ -3,52 +3,63 @@ 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';
/** /**
* 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,
}; };
/** /**
* 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; const name = this.props.name;
const { inputValue } = this.state;
const numValue = Number(inputValue); let scaledValue = Math.round(Number(value) * 100);
if (!isNaN(numValue) && this.state.value !== numValue) { // Limit to +1000% / -99.99%
onSet(property, numValue); if (scaledValue > 100000) {
const { beneficial, unit, value } = m.getFormatted(property, true); scaledValue = 100000;
this.setState({ beneficial, unit, value }); value = 1000;
} }
if (scaledValue < -9999) {
scaledValue = -9999;
value = -99.99;
}
let m = this.props.m;
let ship = this.props.ship;
ship.setModification(m, name, scaledValue, true);
this.setState({ value });
} }
_toggleProperty() { /**
const { onPropToggle, property } = this.props; * Triggered when an update to slider value is finished i.e. when losing focus
const showProp = !this.state.showProp; */
// TODO: defer until menu closed _updateFinished() {
onPropToggle(property, showProp); this.props.onChange();
this.setState({ showProp });
} }
/** /**
@@ -56,53 +67,29 @@ export default class Modification extends TranslatedComponent {
* @return {React.Component} modification * @return {React.Component} modification
*/ */
render() { render() {
const { formats } = this.context.language; let translate = this.context.language.translate;
const { highlight, m, property } = this.props; let { m, name } = this.props;
const { beneficial, unit, value, inputValue, showProp } = this.state;
// 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 symbol;
if (name === 'jitter') {
symbol = '°';
} else if (name !== 'burst' && name != 'burstrof') {
symbol = '%';
}
if (symbol) {
symbol = ' (' + symbol + ')';
}
return ( return (
<tr> <div onBlur={this._updateFinished.bind(this)} className={'cb'} key={name}>
<td> <div className={'cb'}>{translate(name, m.grp)}{symbol}</div>
<span> <NumberEditor className={'cb'} style={{ width: '90%', textAlign: 'center' }} step={0.01} stepModifier={1} decimals={2} value={this.state.value} onValueChange={this._updateValue.bind(this)} />
<input type="checkbox" checked={showProp} onClick={() => this._toggleProperty()}/> </div>
</span>
</td>
<td className="input-container">
<span>
<NumberEditor value={inputValue || value} stepModifier={1}
decimals={2} step={0.01} style={{ textAlign: 'right', width: '100%' }}
className={cn('cb', { 'greyed-out': !highlight })}
onKeyDown={(event) => {
if (event.key == 'Enter') {
this._updateFinished();
event.stopPropagation();
}
}}
onValueChange={(inputValue) => {
if (inputValue.length <= 15) {
this.setState({ inputValue });
}
}} />
</span>
</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>
); );
} }
} }

View File

@@ -1,27 +1,30 @@
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 { isEmpty, 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/data/blueprints';
import { getModuleInfo } from 'ed-forge/lib/data/items';
import { SHOW } from '../shipyard/StatsMapping';
/** /**
* 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
}; };
/** /**
@@ -34,57 +37,50 @@ 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);
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 = m.getApplicableBlueprints().map(blueprint => { const blueprints = [];
const info = getBlueprintInfo(blueprint); for (const blueprintName in Modifications.modules[m.grp].blueprints) {
let blueprintGrades = keys(info.features).map(grade => { const blueprint = getBlueprint(blueprintName, m);
let blueprintGrades = [];
for (let grade in Modifications.modules[m.grp].blueprints[blueprintName].grades) {
// Grade is a string in the JSON so make it a number // Grade 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' }} const close = this._blueprintSelected.bind(this, blueprintName, grade);
onMouseOver={termtip.bind(null, blueprintTooltip(language, m, blueprint, grade))} const key = blueprintName + ':' + grade;
onMouseOut={tooltip.bind(null, null)} const tooltipContent = blueprintTooltip(translate, blueprint.grades[grade], Modifications.modules[m.grp].blueprints[blueprintName].grades[grade].engineers, m.grp);
onClick={() => { blueprintGrades.unshift(<li key={key} className={classes} style={{ width: '2em' }} onMouseOver={termtip.bind(null, tooltipContent)} onMouseOut={tooltip.bind(null, null)} onClick={close}>{grade}</li>);
m.setBlueprint(blueprint, grade, 1); }
this.setState({ if (blueprintGrades) {
blueprintMenuOpened: false, blueprints.push(<div key={blueprint.name} className={'select-group cap'}>{translate(blueprint.name)}</div>);
specialMenuOpened: true, blueprints.push(<ul key={blueprintName}>{blueprintGrades}</ul>);
}); }
}} }
ref={active ? (ref) => { this.selectedModRef = ref; } : undefined} return blueprints;
>{grade}</li>;
});
return [
<div key={'div' + blueprint} className={'select-group cap'}>
{translate(blueprint)}
</div>,
<ul key={'ul' + blueprint}>{blueprintGrades}</ul>
];
});
return flatMap(blueprints);
} }
/** /**
@@ -93,133 +89,155 @@ 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 style={{ cursor: 'pointer', fontWeight: 'bold' }} className={ 'button-inline-menu warning' } key={ 'none' } onClick={ close }>{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 })} const classes = cn('button-inline-menu', {
onClick={this._specialSelected(experimental)} active: m.blueprint && m.blueprint.special && m.blueprint.special.edname == specialName
ref={active ? (ref) => { this.selectedSpecialRef = ref; } : undefined} });
onMouseOver={termtip.bind(null, specialTt)} const close = this._specialSelected.bind(this, specialName);
onMouseOut={tooltip.bind(null, null)} if (m.blueprint && m.blueprint.name) {
>{translate(experimental)}</div> let tmp = {};
); if (m.blueprint.special) {
tmp = m.blueprint.special;
} else {
tmp = undefined;
}
m.blueprint.special = Modifications.specials[specialName];
let specialTt = specialToolTip(translate, m.blueprint.grades[m.blueprint.grade], m.grp, m, specialName);
m.blueprint.special = tmp;
specials.push(<div style={{ cursor: 'pointer' }} className={classes} key={ specialName } onMouseOver={termtip.bind(null, specialTt)} onMouseOut={tooltip.bind(null, null)} onClick={ close }>{translate(Modifications.specials[specialName].name)}</div>);
} else {
specials.push(<div style={{ cursor: 'pointer' }} className={classes} key={ specialName } onClick={ close }>{translate(Modifications.specials[specialName].name)}</div>);
}
}
} }
return specials;
if (experimentals.length) {
experimentals.unshift(
<div style={{ cursor: 'pointer', fontWeight: 'bold' }}
className="button-inline-menu warning" key="none" data-id="none"
// Setting the special effect to undefined clears it
onClick={this._specialSelected(undefined)}
ref={!applied ? (ref) => { this.selectedSpecialRef = ref; } : undefined}
>{translate('PHRASE_NO_SPECIAL')}</div>
);
}
return experimentals;
}
/**
* Create a modification component
*/
_mkModification(property, highlight) {
const { translate } = this.context.language;
const { m, propsToShow, onPropToggle } = this.props;
let onSet = m.set.bind(m);
// Show resistance instead of effectiveness
const mapped = SHOW[property];
if (mapped) {
property = mapped.as;
onSet = mapped.setter.bind(undefined, m);
}
return [
<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 modifications = [];
const blueprintFeatures = getBlueprintInfo(m.getBlueprint()).features[ for (const modName of Modifications.modules[m.grp].modifications) {
m.getBlueprintGrade() if (!Modifications.modifications[modName].hidden) {
]; const key = modName + (m.getModValue(modName) / 100 || 0);
const blueprintModifications = chain(keys(blueprintFeatures)) modifications.push(<Modification key={ key } ship={ ship } m={ m } name={ modName } value={ m.getModValue(modName) / 100 || 0 } onChange={ onChange }/>);
.map((feature) => this._mkModification(feature, true)) }
.filter(([_, mod]) => Boolean(mod)) }
.flatMap() return modifications;
.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);
this.setState({ specialMenuOpened: false }); if (special === null) {
}; ship.clearModuleSpecial(m);
} else {
ship.setModuleSpecial(m, Modifications.specials[special]);
}
this.setState({ specialMenuOpened: false });
this.props.onChange();
} }
/** /**
* Set focus on first element in modifications menu * Provide a '50%' roll within the information we have
* if component updates, unless update is due to value change
* in a modification
*/ */
componentDidUpdate() { _rollFifty() {
if (this.selectedModRef) { const { m, ship } = this.props;
this.selectedModRef.focus(); setPercent(ship, m, 50);
return; this.props.onChange();
} else if (this.selectedSpecialRef) { }
this.selectedSpecialRef.focus();
return; /**
} * Provide a random roll within the information we have
*/
_rollRandom() {
const { m, ship } = this.props;
setRandom(ship, m);
this.props.onChange();
}
/**
* Provide a 'best' roll within the information we have
*/
_rollBest() {
const { m, ship } = this.props;
setPercent(ship, m, 100);
this.props.onChange();
}
/**
* Provide a 'worst' roll within the information we have
*/
_rollWorst() {
const { m, ship } = this.props;
setPercent(ship, m, 0);
this.props.onChange();
}
/**
* Reset modification information
*/
_reset() {
const { m, ship } = this.props;
ship.clearModifications(m);
ship.clearModuleBlueprint(m);
this.props.onChange();
} }
/** /**
@@ -230,155 +248,77 @@ 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; if (m.blueprint && m.blueprint.name) {
case specialMenuOpened: blueprintLabel = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
renderComponents = this._renderSpecials(); haveBlueprint = true;
break; blueprintTt = blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], Modifications.modules[m.grp].blueprints[m.blueprint.fdname].grades[m.blueprint.grade].engineers, m.grp);
default: blueprintCv = getPercent(m);
// Since the first case didn't apply, there is a blueprint applied so
// we render the modifications
let blueprintTt = blueprintTooltip(language, m, appliedBlueprint, appliedGrade);
renderComponents.push(
<div style={{ cursor: 'pointer' }} key="blueprintsMenu"
className="section-menu button-inline-menu"
onMouseOver={termtip.bind(null, blueprintTt)}
onMouseOut={tooltip.bind(null, null)}
onClick={this._toggleBlueprintsMenu}
>
{translate(appliedBlueprint)} {translate('grade')} {appliedGrade}
</div>
);
if (m.getApplicableExperimentals().length) {
let specialLabel = translate('PHRASE_SELECT_SPECIAL');
let specialTt;
if (appliedExperimental) {
specialLabel = appliedExperimental;
specialTt = specialToolTip(language, m, appliedExperimental);
}
renderComponents.push(
<div className="section-menu button-inline-menu"
style={{ cursor: 'pointer' }}
onMouseOver={specialTt ? termtip.bind(null, specialTt) : null}
onMouseOut={specialTt ? tooltip.bind(null, null) : null}
onClick={this._toggleSpecialsMenu}
>{specialLabel}</div>
);
}
renderComponents.push(
<div
className="section-menu button-inline-menu warning"
style={{ cursor: 'pointer' }}
onClick={() => {
m.resetEngineering();
this.selectedModRef = null;
this.selectedSpecialRef = null;
tooltip(null);
this.setState({
blueprintMenuOpened: true,
blueprintProgress: undefined,
});
}}
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RESET')}
onMouseOut={tooltip.bind(null, null)}
>{translate('reset')}</div>,
<table style={{ width: '100%', backgroundColor: 'transparent' }}>
<tbody>
<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>
);
} }
let specialLabel;
let specialTt;
if (m.blueprint && m.blueprint.special) {
specialLabel = m.blueprint.special.name;
specialTt = specialToolTip(translate, m.blueprint.grades[m.blueprint.grade], m.grp, m, m.blueprint.special.edname);
} else {
specialLabel = translate('PHRASE_SELECT_SPECIAL');
}
const specials = this._renderSpecials(this.props, this.context);
const showBlueprintsMenu = blueprintMenuOpened;
const showSpecial = haveBlueprint && specials.length && !blueprintMenuOpened;
const showSpecialsMenu = specialMenuOpened;
const showRolls = haveBlueprint && !blueprintMenuOpened && !specialMenuOpened;
const showReset = !blueprintMenuOpened && !specialMenuOpened && haveBlueprint;
const showMods = !blueprintMenuOpened && !specialMenuOpened && haveBlueprint;
return ( return (
<div className={cn('select', this.props.className)} <div
onClick={(e) => e.stopPropagation()} className={cn('select', this.props.className)}
onContextMenu={stopCtxPropagation} onClick={(e) => e.stopPropagation() }
onContextMenu={stopCtxPropagation}
> >
{renderComponents} { showBlueprintsMenu | showSpecialsMenu ? '' : haveBlueprint ?
<div className={ cn('section-menu button-inline-menu', { selected: blueprintMenuOpened })} style={{ cursor: 'pointer' }} onMouseOver={termtip.bind(null, blueprintTt)} onMouseOut={tooltip.bind(null, null)} onClick={_toggleBlueprintsMenu}>{blueprintLabel}</div> :
<div className={ cn('section-menu button-inline-menu', { selected: blueprintMenuOpened })} style={{ cursor: 'pointer' }} onClick={_toggleBlueprintsMenu}>{translate('PHRASE_SELECT_BLUEPRINT')}</div> }
{ showBlueprintsMenu ? this._renderBlueprints(this.props, this.context) : null }
{ showSpecial & !showSpecialsMenu ? <div 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}>{specialLabel}</div> : null }
{ showSpecialsMenu ? specials : null }
{ showReset ? <div className={'section-menu button-inline-menu warning'} style={{ cursor: 'pointer' }} onClick={_reset} 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 className={ cn('section-menu button-inline-menu', {active: false})}> { translate('roll') }: </td>
<td className={ cn('section-menu button-inline-menu', { active: blueprintCv === 0 })} style={{ cursor: 'pointer' }} onClick={_rollWorst} onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_WORST')} onMouseOut={tooltip.bind(null, null)}> { translate('0%') } </td>
<td className={ cn('section-menu button-inline-menu', { active: blueprintCv === 50 })} style={{ cursor: 'pointer' }} onClick={_rollFifty} onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_FIFTY')} onMouseOut={tooltip.bind(null, null)}> { translate('50%') } </td>
<td className={ cn('section-menu button-inline-menu', { active: blueprintCv === 100 })} style={{ cursor: 'pointer' }} onClick={_rollFull} onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_BEST')} onMouseOut={tooltip.bind(null, null)}> { translate('100%') } </td>
<td className={ cn('section-menu button-inline-menu', { active: blueprintCv === null || blueprintCv % 50 != 0 })} style={{ cursor: 'pointer' }} onClick={_rollRandom} 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>
); );
} }

View File

@@ -1,39 +1,47 @@
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'>
<svg viewBox='0 0 600 600' fillRule="evenodd" clipRule="evenodd"> <svg viewBox='0 0 600 600' fillRule="evenodd" clipRule="evenodd">
{/* Axes */} // Axes
<path d="M150 250v300" strokeWidth='1'/> <path d="M150 250v300" strokeWidth='1'/>
<path d="M150 250l236 236" strokeWidth='1'/> <path d="M150 250l236 236" strokeWidth='1'/>
<path d="M150 250l350 -200" strokeWidth='1'/> <path d="M150 250l350 -200" strokeWidth='1'/>
{/* End Arrow */} // End Arrow
<path d="M508 43.3L487 67l-10-17.3 31-6.4z"/> <path d="M508 43.3L487 67l-10-17.3 31-6.4z"/>
{/* Axes arcs and arrows */} // Axes arcs and arrows
<path d="M71.7 251.7C64.2 259.2 60 269.4 60 280c0 22 18 40 40 40s40-18 40-40c0-10.6-4.2-20.8-11.7-28.3 7.5 7.5 11.7 17.7 11.7 28.3 0 22-18 40-40 40s-40-18-40-40c0-10.6 4.2-20.8 11.7-28.3z" strokeWidth='4' transform="matrix(.6 0 0 .3 87.5 376.3)"/> <path d="M71.7 251.7C64.2 259.2 60 269.4 60 280c0 22 18 40 40 40s40-18 40-40c0-10.6-4.2-20.8-11.7-28.3 7.5 7.5 11.7 17.7 11.7 28.3 0 22-18 40-40 40s-40-18-40-40c0-10.6 4.2-20.8 11.7-28.3z" strokeWidth='4' transform="matrix(.6 0 0 .3 87.5 376.3)"/>
<path d="M142.8 453l-13.2 8.7-2.6-9.7 15.8 1z"/> <path d="M142.8 453l-13.2 8.7-2.6-9.7 15.8 1z"/>
<path d="M144.7 451.6l.5 1.6-16.2 10.6h-.4l-3.5-13 .7-.4 19.3 1.2zm-14.2 7.7l7.7-5-9.2-.7 1.5 5.7zm25.7-6.3l15.8-1-2.6 9.7-13.2-8.8z"/> <path d="M144.7 451.6l.5 1.6-16.2 10.6h-.4l-3.5-13 .7-.4 19.3 1.2zm-14.2 7.7l7.7-5-9.2-.7 1.5 5.7zm25.7-6.3l15.8-1-2.6 9.7-13.2-8.8z"/>
@@ -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>);
} }

View File

@@ -3,34 +3,43 @@ import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import * as Calc from '../shipyard/Calculations'; 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 VerticalBarChart from './VerticalBarChart';
import autoBind from 'auto-bind';
import { DAMAGE_METRICS } from 'ed-forge/lib/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
* Returns a data object used by {@link PieChart} that shows damage by type. let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a, b);
* @param {function} translate Translation function
* @param {Calc.SDps} o Object that holds sdps split up by type if (diff) {
* @returns {Object} Data object return diff;
*/ }
function objToPie(translate, o) {
return toPairs(o).map(([k, value]) => { // Property matches so sort by name / group, then class, rating
return { label: translate(k), value }; 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);
};
} }
/** /**
@@ -43,10 +52,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
}; };
/** /**
@@ -55,327 +66,200 @@ 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 damage = ship.getMetrics(DAMAGE_METRICS); const pd = ship.standard[4].m;
const portions = {
Absolute: damage.types.abs,
Explosive: damage.types.expl,
Kinetic: damage.types.kin,
Thermic: damage.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,
};
let rows = []; let absoluteShieldsSDps = 0;
for (let weapon of ship.getHardpoints()) { let explosiveShieldsSDps = 0;
const sdps = weapon.get('sustaineddamagepersecond'); let kineticShieldsSDps = 0;
const byRange = weapon.getRangeEffectiveness(); let thermalShieldsSDps = 0;
const weaponPortions = { let absoluteArmourSDps = 0;
Absolute: weapon.get('absolutedamageportion'), let explosiveArmourSDps = 0;
Explosive: weapon.get('explosivedamageportion'), let kineticArmourSDps = 0;
Kinetic: weapon.get('kineticdamageportion'), let thermalArmourSDps = 0;
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(
clone(weaponPortions),
shieldMults,
(objV, srcV) => objV * srcV
);
const byShieldRes = sum(values(shieldResEfts));
const shieldsSdpsTooltip = objToTooltip(
translate,
mapValues(
shieldResEfts,
(mult) => formats.f1(byRange * mult * bySys * sdps),
),
);
const shieldsEftTooltip = objToTooltip(
translate,
{
range: formats.pct1(byRange),
resistance: formats.pct1(byShieldRes),
'power distributor': formats.pct1(bySys),
},
);
const shieldEft = byRange * byShieldRes * bySys;
const 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(); absoluteShieldsSDps += weapon.sdps.shields.absolute;
const exp = weapon.getExperimental(); explosiveShieldsSDps += weapon.sdps.shields.explosive;
let bpTitle = `${translate(bp)} ${translate('grade')} ${grade}`; kineticShieldsSDps += weapon.sdps.shields.kinetic;
if (exp) { thermalShieldsSDps += weapon.sdps.shields.thermal;
bpTitle += `, ${translate(exp)}`; absoluteArmourSDps += weapon.sdps.armour.absolute;
} explosiveArmourSDps += weapon.sdps.armour.explosive;
rows.push({ kineticArmourSDps += weapon.sdps.armour.kinetic;
slot: weapon.getSlot(), thermalArmourSDps += weapon.sdps.armour.thermal;
mount: weapon.mount,
classRating: weapon.getClassRating(), const effectivenessShieldsTooltipDetails = [];
type: weapon.readMeta('type'), effectivenessShieldsTooltipDetails.push(<div key='range'>{translate('range') + ' ' + formats.pct1(weapon.effectiveness.shields.range)}</div>);
bpTitle: bp ? ` (${bpTitle})` : null, effectivenessShieldsTooltipDetails.push(<div key='resistance'>{translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.shields.resistance)}</div>);
sdps, effectivenessShieldsTooltipDetails.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(weapon.effectiveness.shields.sys)}</div>);
baseSdpsTooltip,
shieldSdps: sdps * shieldEft, const effectiveShieldsSDpsTooltipDetails = [];
shieldEft, if (weapon.sdps.shields.absolute) effectiveShieldsSDpsTooltipDetails.push(<div key='absolute'>{translate('absolute') + ' ' + formats.f1(weapon.sdps.shields.absolute)}</div>);
shieldsSdpsTooltip, if (weapon.sdps.shields.explosive) effectiveShieldsSDpsTooltipDetails.push(<div key='explosive'>{translate('explosive') + ' ' + formats.f1(weapon.sdps.shields.explosive)}</div>);
shieldsEftTooltip, if (weapon.sdps.shields.kinetic) effectiveShieldsSDpsTooltipDetails.push(<div key='kinetic'>{translate('kinetic') + ' ' + formats.f1(weapon.sdps.shields.kinetic)}</div>);
armourSdps: sdps * armourEft, if (weapon.sdps.shields.thermal) effectiveShieldsSDpsTooltipDetails.push(<div key='thermal'>{translate('thermal') + ' ' + formats.f1(weapon.sdps.shields.thermal)}</div>);
armourEft,
armourSdpsTooltip, const effectivenessArmourTooltipDetails = [];
armourEftTooltip, 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 { predicate, desc } = this.state; const effectiveArmourSDpsTooltipDetails = [];
rows = sortBy(rows, (row) => row[predicate]); if (weapon.sdps.armour.absolute) effectiveArmourSDpsTooltipDetails.push(<div key='absolute'>{translate('absolute') + ' ' + formats.f1(weapon.sdps.armour.absolute)}</div>);
if (desc) { if (weapon.sdps.armour.explosive) effectiveArmourSDpsTooltipDetails.push(<div key='explosive'>{translate('explosive') + ' ' + formats.f1(weapon.sdps.armour.explosive)}</div>);
reverse(rows); if (weapon.sdps.armour.kinetic) effectiveArmourSDpsTooltipDetails.push(<div key='kinetic'>{translate('kinetic') + ' ' + formats.f1(weapon.sdps.armour.kinetic)}</div>);
if (weapon.sdps.armour.thermal) effectiveArmourSDpsTooltipDetails.push(<div key='thermal'>{translate('thermal') + ' ' + formats.f1(weapon.sdps.armour.thermal)}</div>);
rows.push(
<tr key={weapon.id}>
<td className='ri'>
{weapon.mount == 'F' ? <span onMouseOver={termtip.bind(null, 'fixed')} onMouseOut={tooltip.bind(null, null)}><MountFixed className='icon'/></span> : null}
{weapon.mount == 'G' ? <span onMouseOver={termtip.bind(null, 'gimballed')} onMouseOut={tooltip.bind(null, null)}><MountGimballed /></span> : null}
{weapon.mount == 'T' ? <span onMouseOver={termtip.bind(null, 'turreted')} onMouseOut={tooltip.bind(null, null)}><MountTurret /></span> : null}
{weapon.classRating} {translate(weapon.name)}
{weapon.engineering ? ' (' + weapon.engineering + ')' : null }
</td>
<td className='ri'><span onMouseOver={termtip.bind(null, effectiveShieldsSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.f1(weapon.sdps.shields.total)}</span></td>
<td className='ri'><span onMouseOver={termtip.bind(null, effectivenessShieldsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(weapon.effectiveness.shields.total)}</span></td>
<td className='ri'><span onMouseOver={termtip.bind(null, effectiveArmourSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.f1(weapon.sdps.armour.total)}</span></td>
<td className='ri'><span onMouseOver={termtip.bind(null, effectivenessArmourTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(weapon.effectiveness.armour.total)}</span></td>
</tr>);
} }
const sdpsTooltip = objToTooltip( const totalShieldsSDps = absoluteShieldsSDps + explosiveShieldsSDps + kineticShieldsSDps + thermalShieldsSDps;
translate, const totalArmourSDps = absoluteArmourSDps + explosiveArmourSDps + kineticArmourSDps + thermalArmourSDps;
mapValues(portions, (p) => formats.f1(damage.sustained.dps * p)),
);
const sdpsPie = objToPie(
translate,
mapValues(portions, (p) => Math.round(damage.sustained.dps * p)),
);
const shieldSdpsSrcs = mergeWith( const shieldsSDpsData = [];
clone(portions), shieldsSDpsData.push({ value: Math.round(absoluteShieldsSDps), label: translate('absolute') });
shieldMults, shieldsSDpsData.push({ value: Math.round(explosiveShieldsSDps), label: translate('explosive') });
(objV, srcV) => damage.sustained.dps * oppShield.absolute.bySys * shieldsSDpsData.push({ value: Math.round(kineticShieldsSDps), label: translate('kinetic') });
damage.rangeMultiplier * objV * srcV, shieldsSDpsData.push({ value: Math.round(thermalShieldsSDps), label: translate('thermal') });
);
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 armourSDpsData = [];
clone(portions), armourSDpsData.push({ value: Math.round(absoluteArmourSDps), label: translate('absolute') });
armourMults, armourSDpsData.push({ value: Math.round(explosiveArmourSDps), label: translate('explosive') });
(objV, srcV) => damage.sustained.dps * damage.hardnessMultiplier * armourSDpsData.push({ value: Math.round(kineticArmourSDps), label: translate('kinetic') });
damage.rangeMultiplier * objV * srcV, armourSDpsData.push({ value: Math.round(thermalArmourSDps), label: translate('thermal') });
);
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 pd = ship.getPowerDistributor(); const timeToDepleteShields = Calc.timeToDeplete(opponentShields.total, totalShieldsSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
const timeToDrain = damage.sustained.timeToDrain[ship.getDistributorSettings().Wep]; const timeToDepleteArmour = Calc.timeToDeplete(opponentArmour.total, totalArmourSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
// const timeToDepleteShields = Calc.timeToDeplete(opponentShields.total, shieldsSdps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
// const timeToDepleteArmour = Calc.timeToDeplete(opponentArmour.total, armourSdps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
return ( return (
<span id='offence'> <span id='offence'>
<div className='group full'> <div className='group full'>
<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='2'>{translate('opponent\'s shields')}</th>
<th colSpan='2'>{translate('opponent\'s shields')}</th> <th colSpan='2'>{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')} 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, 'sdps')}>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='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_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, 'shieldSdps')}>sdps</th> </tr>
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_SHIELDS')} </thead>
onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'shieldEft')}>eft</th> <tbody>
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_ARMOUR')} {rows}
onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'armourSdps')}>sdps</th> </tbody>
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_ARMOUR')} </table>
onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'armourEft')}>eft</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.slot}>
<td className='ri'>
{row.mount == 'F' ? <span onMouseOver={termtip.bind(null, 'fixed')} onMouseOut={tooltip.bind(null, null)}><MountFixed className='icon'/></span> : null}
{row.mount == 'G' ? <span onMouseOver={termtip.bind(null, 'gimballed')} onMouseOut={tooltip.bind(null, null)}><MountGimballed /></span> : null}
{row.mount == 'T' ? <span onMouseOver={termtip.bind(null, 'turreted')} onMouseOut={tooltip.bind(null, null)}><MountTurret /></span> : null}
{row.classRating} {translate(row.type)}
{row.bpTitle}
</td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, row.baseSdpsTooltip)}
onMouseOut={tooltip.bind(null, null)}
>{formats.f1(row.sdps)}</span></td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, row.shieldsSdpsTooltip)}
onMouseOut={tooltip.bind(null, null)}
>{formats.f1(row.shieldSdps)}</span></td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, row.shieldsEftTooltip)}
onMouseOut={tooltip.bind(null, null)}
>{formats.pct1(row.shieldEft)}</span></td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, row.armourSdpsTooltip)}
onMouseOut={tooltip.bind(null, null)}
>{formats.f1(row.armourSdps)}</span></td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, row.armourEftTooltip)}
onMouseOut={tooltip.bind(null, null)}
>{formats.pct1(row.armourEft)}</span></td>
</tr>
))}
{rows.length > 0 &&
<tr>
<td></td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, sdpsTooltip)} onMouseOut={tooltip.bind(null, null)}>
={formats.f1(damage.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>
</tr>
}
</tbody>
</table>
</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/>
ToDo
{/* {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/>
ToDo
{/* {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_SHIELD_DAMAGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('shield damage sources')}</h2>
onMouseOut={tooltip.bind(null, null)}> <PieChart data={shieldsSDpsData} />
{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_ARMOUR_DAMAGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('armour damage sources')}</h2>
onMouseOut={tooltip.bind(null, null)}>
{translate('shield damage sources')}
</h2>
<PieChart data={shieldsSdpsPie} />
</div>
<div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_ARMOUR_DAMAGE'))}
onMouseOut={tooltip.bind(null, null)}>
{translate('armour damage sources')}
</h2>
<PieChart data={armourSDpsData} /> <PieChart data={armourSDpsData} />
</div> </div>
</span>); </span>);

View File

@@ -1,6 +1,8 @@
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 Ship from '../shipyard/Ship';
import Persist from '../stores/Persist'; import Persist from '../stores/Persist';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import PowerManagement from './PowerManagement'; import PowerManagement from './PowerManagement';
@@ -11,24 +13,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 +44,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,114 +59,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> <div className='group full' style={{ minHeight: '1000px' }}>
{/* Control of ship and opponent */} <table className='tabs'>
<div className="group quarter"> <thead>
<h2 style={{ verticalAlign: 'middle', textAlign: 'center' }}> <tr>
{translate('ship control')} <th style={{ width:'25%' }} className={cn({ active: tab == 'power' })} onClick={this._showTab.bind(this, 'power')} >{translate('power and costs')}</th>
</h2> <th style={{ width:'25%' }} className={cn({ active: tab == 'profiles' })} onClick={this._showTab.bind(this, 'profiles')} >{translate('profiles')}</th>
<Boost boost={boost} onChange={(boost) => this.setState({ boost })} /> <th style={{ width:'25%' }} className={cn({ active: tab == 'offence' })} onClick={this._showTab.bind(this, 'offence')} >{translate('offence')}</th>
</div> <th style={{ width:'25%' }} className={cn({ active: tab == 'defence' })} onClick={this._showTab.bind(this, 'defence')} >{translate('defence')}</th>
<div className="group quarter"> </tr>
<h2 style={{ verticalAlign: 'middle', textAlign: 'center' }}> </thead>
{translate('opponent')} </table>
</h2> {tabSection}
<ShipPicker ship={ship} onChange={(opponent) => this.setState({ opponent })} />
</div>
<div className={cn('group', { quarter: showCargoSlider, half: !showCargoSlider })}>
<Fuel fuelCapacity={ship.get(FUEL_CAPACITY)} fuel={fuel}
onChange={(fuel) => this.setState({ fuel })} />
</div>
{showCargoSlider ?
<div className="group quarter">
<Cargo cargoCapacity={cargoCapacity} cargo={cargo}
onChange={(cargo) => this.setState({ cargo })} />
</div> : null}
<div className="group half">
<Pips ship={ship} pips={pips} onChange={(pips) => this.setState({ pips })} />
</div>
<div className="group half">
<EngagementRange ship={ship} engagementRange={engagementRange}
onChange={(engagementRange) => this.setState({ engagementRange })} />
</div>
<div className='group full' style={{ minHeight: '1000px' }}>
<table className='tabs'>
{/* Select tab section */}
<thead>
<tr>
<th style={{ width:'25%' }} className={cn({ active: tab == 'power' })}
onClick={this._showTab.bind(this, 'power')}>
{translate('power and costs')}
</th>
<th style={{ width:'25%' }} className={cn({ active: tab == 'profiles' })}
onClick={this._showTab.bind(this, 'profiles')}>
{translate('profiles')}</th>
<th style={{ width:'25%' }} className={cn({ active: tab == 'offence' })}
onClick={this._showTab.bind(this, 'offence')}>
{translate('offence')}
</th>
<th style={{ width:'25%' }} className={cn({ active: tab == 'defence' })}
onClick={this._showTab.bind(this, 'defence')}>
{translate('tab_defence')}
</th>
</tr>
</thead>
</table>
{/* Show selected tab */}
{tab == 'power' ?
<div>
<PowerManagement ship={ship} code={code} />
<CostSection ship={ship} buildName={buildName} code={code} />
</div> : null}
{tab == 'profiles' ?
<div>
<div className='group third'>
<h1>{translate('engine profile')}</h1>
<EngineProfile code={code} ship={ship} fuel={fuel} cargo={cargo} pips={pips} boost={boost} />
</div>
<div className='group third'>
<h1>{translate('fsd profile')}</h1>
<FSDProfile code={code} ship={ship} fuel={fuel} cargo={cargo} />
</div>
<div className='group third'>
<h1>{translate('movement profile')}</h1>
<Movement code={code} ship={ship} boost={boost} pips={pips} />
</div>
<div className='group third'>
<h1>{translate('damage to opponent\'s shields')}</h1>
<WeaponDamageChart code={code} ship={ship} opponentDefence={opponent.getShield()} engagementRange={engagementRange} />
</div>
<div className='group third'>
<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>
); );
} }

View File

@@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ContainerDimensions from 'react-container-dimensions'; import Measure from 'react-measure';
import * as d3 from 'd3'; import * as d3 from 'd3';
const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D'];
@@ -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
}; };
@@ -26,6 +27,13 @@ export default class PieChart extends Component {
this.colors = CORIOLIS_COLOURS; this.colors = CORIOLIS_COLOURS;
this.arc = d3.arc(); this.arc = d3.arc();
this.arc.innerRadius(0); this.arc.innerRadius(0);
this.state = {
dimensions: {
width: 100,
height: 100
}
};
} }
@@ -33,15 +41,15 @@ export default class PieChart extends Component {
* Generate a slice of the pie chart * Generate a slice of the pie chart
* @param {Object} d the data for this slice * @param {Object} d the data for this slice
* @param {number} i the index of this slice * @param {number} i the index of this slice
* @param {number} width the current width of the parent container
* @returns {Object} the SVG for the slice * @returns {Object} the SVG for the slice
*/ */
sliceGenerator(d, i, width) { sliceGenerator(d, i) {
if (!d || d.value == 0) { if (!d || d.value == 0) {
// Ignore 0 values // Ignore 0 values
return null; return null;
} }
const { width, height } = this.state.dimensions;
const { data } = this.props; const { data } = this.props;
// Push the labels further out from the centre of the slice // Push the labels further out from the centre of the slice
@@ -68,24 +76,22 @@ export default class PieChart extends Component {
* @returns {object} Markup * @returns {object} Markup
*/ */
render() { render() {
return ( const { width, height } = this.state.dimensions;
<ContainerDimensions> const pie = this.pie(this.props.data),
{ ({ width }) => { translate = `translate(${width / 2}, ${width * 0.4})`;
const pie = this.pie(this.props.data),
translate = `translate(${width / 2}, ${width * 0.4})`;
this.arc.outerRadius(width * 0.4); this.arc.outerRadius(width * 0.4);
return (
<div width={width} height={width}> return (
<svg style={{ stroke: 'None' }} width={width} height={width * 0.9}> <Measure width='100%' whitelist={['width', 'top']} onMeasure={ (dimensions) => { this.setState({ dimensions }); }}>
<g transform={translate}> <div width={width} height={width}>
{pie.map((d, i) => this.sliceGenerator(d, i, width))} <svg style={{ stroke: 'None' }} width={width} height={width * 0.9}>
</g> <g transform={translate}>
</svg> {pie.map((d, i) => this.sliceGenerator(d, i))}
</div> </g>
); </svg>
}} </div>
</ContainerDimensions> </Measure>
); );
} }
} }

View File

@@ -1,9 +1,13 @@
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 { nameComparator } from '../utils/SlotFunctions';
import { Pip } from './SvgIcons'; import { Pip } from './SvgIcons';
import { autoBind } from 'react-extras'; import LineChart from '../components/LineChart';
import { Ship } from 'ed-forge'; import Slider from '../components/Slider';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import Module from '../shipyard/Module';
/** /**
* Pips displays SYS/ENG/WEP pips and allows users to change them with key presses by clicking on the relevant area. * Pips displays SYS/ENG/WEP pips and allows users to change them with key presses by clicking on the relevant area.
@@ -11,9 +15,10 @@ 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,
onChange: PropTypes.func.isRequired
}; };
/** /**
@@ -23,13 +28,9 @@ export default class Pips extends TranslatedComponent {
*/ */
constructor(props, context) { constructor(props, context) {
super(props); super(props);
autoBind(this); const { sys, eng, wep } = props;
const { ship } = props; this._keyDown = this._keyDown.bind(this);
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,43 +75,181 @@ export default class Pips extends TranslatedComponent {
} }
/** /**
* Creates a function that handles pip assignment and call `onChance`. * Handle a click
* @param {String} cb Callback that handles the actual pip assignment * @param {string} which Which item was clicked
* @param {Boolean} isMc True when increase is by multi crew
* @returns {Function} Function that handles pip assigment
*/ */
_change(cb, isMc) { onClick(which) {
return () => { if (which == 'SYS') {
cb(isMc); this._incSys();
this.props.onChange(this.props.ship.getDistributorSettingsObject()); } else if (which == 'ENG') {
}; this._incEng();
} else if (which == 'WEP') {
this._incWep();
} else if (which == 'RST') {
this._reset();
}
}
/**
* Reset the capacitor
*/
_reset() {
let { sys, eng, wep } = this.props;
if (sys != 2 || eng != 2 || wep != 2) {
sys = eng = wep = 2;
this.props.onChange(sys, eng, wep);
}
}
/**
* Increment the SYS capacitor
*/
_incSys() {
let { sys, eng, wep } = this.props;
const required = Math.min(1, 4 - sys);
if (required > 0) {
if (required == 0.5) {
// Take from whichever is larger
if (eng > wep) {
eng -= 0.5;
sys += 0.5;
} else {
wep -= 0.5;
sys += 0.5;
}
} else {
// Required is 1 - take from both if possible
if (eng == 0) {
wep -= 1;
sys += 1;
} else if (wep == 0) {
eng -= 1;
sys += 1;
} else {
eng -= 0.5;
wep -= 0.5;
sys += 1;
}
}
this.props.onChange(sys, eng, wep);
}
}
/**
* Increment the ENG capacitor
*/
_incEng() {
let { sys, eng, wep } = this.props;
const required = Math.min(1, 4 - eng);
if (required > 0) {
if (required == 0.5) {
// Take from whichever is larger
if (sys > wep) {
sys -= 0.5;
eng += 0.5;
} else {
wep -= 0.5;
eng += 0.5;
}
} else {
// Required is 1 - take from both if possible
if (sys == 0) {
wep -= 1;
eng += 1;
} else if (wep == 0) {
sys -= 1;
eng += 1;
} else {
sys -= 0.5;
wep -= 0.5;
eng += 1;
}
}
this.props.onChange(sys, eng, wep);
}
}
/**
* Increment the WEP capacitor
*/
_incWep() {
let { sys, eng, wep } = this.props;
const required = Math.min(1, 4 - wep);
if (required > 0) {
if (required == 0.5) {
// Take from whichever is larger
if (sys > eng) {
sys -= 0.5;
wep += 0.5;
} else {
eng -= 0.5;
wep += 0.5;
}
} else {
// Required is 1 - take from both if possible
if (sys == 0) {
eng -= 1;
wep += 1;
} else if (eng == 0) {
sys -= 1;
wep += 1;
} else {
sys -= 0.5;
eng -= 0.5;
wep += 1;
}
}
this.props.onChange(sys, eng, wep);
}
} }
/** /**
* Set up the rendering for pips * Set up the rendering for pips
* @param {int} sys the SYS pips
* @param {int} eng the ENG pips
* @param {int} wep the WEP pips
* @returns {Object} Object containing the rendering for the pips * @returns {Object} Object containing the rendering for the pips
*/ */
_renderPips() { _renderPips(sys, eng, wep) {
const pipsSvg = { const pipsSvg = {};
Sys: [],
Eng: [],
Wep: [],
};
for (let k in this.props.pips) { // SYS
let { base, mc } = this.props.pips[k]; pipsSvg['SYS'] = [];
for (let i = 0; i < Math.floor(base); i++) { for (let i = 0; i < Math.floor(sys); i++) {
pipsSvg[k].push(<Pip key={i} className='full' />); pipsSvg['SYS'].push(<Pip className='full' key={i} />);
} }
if (base > Math.floor(base)) { if (sys > Math.floor(sys)) {
pipsSvg[k].push(<Pip className='half' key={'half'} />); pipsSvg['SYS'].push(<Pip className='half' key={'half'} />);
} }
for (let i = 0; i < mc; i++) { for (let i = Math.floor(sys + 0.5); i < 4; i++) {
pipsSvg[k].push(<Pip key={base + i} className='mc' />); pipsSvg['SYS'].push(<Pip className='empty' key={i} />);
} }
for (let i = Math.ceil(base + mc); i < 4; i++) {
pipsSvg[k].push(<Pip className='empty' key={i} />); // ENG
} pipsSvg['ENG'] = [];
for (let i = 0; i < Math.floor(eng); i++) {
pipsSvg['ENG'].push(<Pip className='full' key={i} />);
}
if (eng > Math.floor(eng)) {
pipsSvg['ENG'].push(<Pip className='half' key={'half'} />);
}
for (let i = Math.floor(eng + 0.5); i < 4; i++) {
pipsSvg['ENG'].push(<Pip className='empty' key={i} />);
}
// WEP
pipsSvg['WEP'] = [];
for (let i = 0; i < Math.floor(wep); i++) {
pipsSvg['WEP'].push(<Pip className='full' key={i} />);
}
if (wep > Math.floor(wep)) {
pipsSvg['WEP'].push(<Pip className='half' key={'half'} />);
}
for (let i = Math.floor(wep + 0.5); i < 4; i++) {
pipsSvg['WEP'].push(<Pip className='empty' key={i} />);
} }
return pipsSvg; return pipsSvg;
@@ -121,10 +260,15 @@ export default class Pips extends TranslatedComponent {
* @return {React.Component} contents * @return {React.Component} contents
*/ */
render() { render() {
const { ship } = this.props; const { formats, translate, units } = this.context.language;
const { translate } = this.context.language; const { sys, eng, wep } = this.props;
const pipsSvg = this._renderPips(); const onSysClicked = this.onClick.bind(this, 'SYS');
const onEngClicked = this.onClick.bind(this, 'ENG');
const onWepClicked = this.onClick.bind(this, 'WEP');
const onRstClicked = this.onClick.bind(this, 'RST');
const pipsSvg = this._renderPips(sys, eng, wep);
return ( return (
<span id='pips'> <span id='pips'>
<table> <table>
@@ -132,40 +276,20 @@ export default class Pips extends TranslatedComponent {
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
<td>&nbsp;</td> <td>&nbsp;</td>
<td className='clickable' onClick={this._incEng}>{pipsSvg.Eng}</td> <td className='clickable' onClick={onEngClicked}>{pipsSvg['ENG']}</td>
<td>&nbsp;</td> <td>&nbsp;</td>
</tr> </tr>
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
<td className='clickable' onClick={this._incSys}>{pipsSvg.Sys}</td> <td className='clickable' onClick={onSysClicked}>{pipsSvg['SYS']}</td>
<td className='clickable' onClick={this._incEng}> <td className='clickable' onClick={onEngClicked}>{translate('ENG')}</td>
{translate('ENG')} <td className='clickable' onClick={onWepClicked}>{pipsSvg['WEP']}</td>
</td>
<td className='clickable' onClick={this._incWep}>{pipsSvg.Wep}</td>
</tr> </tr>
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
<td className='clickable' onClick={this._incSys}> <td className='clickable' onClick={onSysClicked}>{translate('SYS')}</td>
{translate('SYS')} <td className='clickable' onClick={onRstClicked}>{translate('RST')}</td>
</td> <td className='clickable' onClick={onWepClicked}>{translate('WEP')}</td>
<td className='clickable' onClick={this._reset}>
{translate('RST')}
</td>
<td className='clickable' onClick={this._incWep}>
{translate('WEP')}
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td className='clickable' onClick={this._change(ship.incSys, true)}>
<Pip className='mc' />
</td>
<td className='clickable' onClick={this._change(ship.incEng, true)}>
<Pip className='mc' />
</td>
<td className='clickable' onClick={this._change(ship.incWep, true)}>
<Pip className='mc' />
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -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/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 { * Get the # label for a Priority band
return 'primary'; * @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;
} }
/** /**
@@ -30,10 +32,12 @@ function getClass(selected, relDraw) {
* Renders the SVG to simulate in-game power bands * Renders the SVG to simulate in-game power bands
*/ */
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 +47,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 +84,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 +141,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 +185,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 +272,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>
); );

View File

@@ -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,35 +110,36 @@ 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];
let retractedElem = null, deployedElem = null;
const flipEnabled = () => m.setEnabled();
if (m.isEnabled()) {
let powered = m.isPowered();
retractedElem = <td className='ptr upp' onClick={flipEnabled}>{getPowerIcon(powered.retracted)}</td>;
deployedElem = <td className='ptr upp' onClick={flipEnabled}>{getPowerIcon(powered.deployed)}</td>;
} else {
retractedElem = <td className='ptr disabled upp' colSpan='2' onClick={flipEnabled}>{translate('disabled')}</td>;
}
const slot = m.getSlot(); if (slot.m && slot.m.getPowerUsage() > 0) {
powerRows.push(<tr key={slot} className={cn('highlight', { disabled: !m.isEnabled() })}> let m = slot.m;
<td className='ptr' style={{ width: '1em' }} onClick={flipEnabled}>{String(m.getClass()) + m.getRating()}</td> let toggleEnabled = this._toggleEnabled.bind(this, slot);
<td className='ptr le shorten cap' onClick={flipEnabled}>{translate(m.readMeta('type'))}</td> let retractedElem = null, deployedElem = null;
{/* <td className='ptr' onClick={flipEnabled}><u>{translate(slot.type)}</u></td> */}
<td> if (slot.enabled) {
<span className='flip ptr btn' onClick={this._prioCb(m, -1)}>&#9658;</span> retractedElem = <td className='ptr upp' onClick={toggleEnabled}>{POWER[ship.getSlotStatus(slot, false)]}</td>;
{' ' + (m.getPowerPriority() + 1) + ' '} deployedElem = <td className='ptr upp' onClick={toggleEnabled}>{POWER[ship.getSlotStatus(slot, true)]}</td>;
<span className='ptr btn' onClick={this._prioCb(m, 1)}>&#9658;</span> } else {
</td> retractedElem = <td className='ptr disabled upp' colSpan='2' onClick={toggleEnabled}>{translate('disabled')}</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> powerRows.push(<tr key={i} className={cn('highlight', { disabled: !slot.enabled })}>
</td> <td className='ptr' style={{ width: '1em' }} onClick={toggleEnabled}>{m.class + m.rating}</td>
{retractedElem} <td className='ptr le shorten cap' onClick={toggleEnabled}>{slotName(translate, slot)}</td>
{deployedElem} <td className='ptr' onClick={toggleEnabled}><u>{translate(slot.type)}</u></td>
</tr>); <td>
<span className='flip ptr btn' onClick={this._priority.bind(this, slot, -1)}>&#9658;</span>
{' ' + (slot.priority + 1) + ' '}
<span className='ptr btn' onClick={this._priority.bind(this, slot, 1)}>&#9658;</span>
</td>
<td className='ri ptr' style={{ width: '3.25em' }} onClick={toggleEnabled}>{pwr(m.getPowerUsage())}</td>
<td className='ri ptr' style={{ width: '3em' }} onClick={toggleEnabled}><u>{pct(m.getPowerUsage() / ship.powerAvailable)}</u></td>
{retractedElem}
{deployedElem}
</tr>);
}
} }
return powerRows; 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>
); );
} }

View File

@@ -1,11 +1,11 @@
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 Ship from '../shipyard/Ship';
import { Ships } from 'coriolis-data/dist'; 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 autoBind from 'auto-bind';
/** /**
* Ship picker * Ship picker
@@ -27,9 +27,13 @@ export default class ShipPicker extends TranslatedComponent {
* @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.shipOrder = Object.keys(Ships).sort();
this._toggleMenu = this._toggleMenu.bind(this);
this._closeMenu = this._closeMenu.bind(this);
this.state = { menuOpen: false }; this.state = { menuOpen: false };
} }
@@ -111,11 +115,11 @@ export default class ShipPicker extends TranslatedComponent {
<span className='menu-item-label'>{shipString}</span> <span className='menu-item-label'>{shipString}</span>
</div> </div>
{ menuOpen ? { menuOpen ?
<div className='menu-list' onClick={ (e) => e.stopPropagation() }> <div className='menu-list' onClick={ (e) => e.stopPropagation() }>
<div className='quad'> <div className='quad'>
{this._renderPickerMenu()} {this._renderPickerMenu()}
</div> </div>
</div> : null } </div> : null }
</div> </div>
</div> </div>
); );

View File

@@ -1,36 +1,29 @@
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';
const {
SPEED, BOOST_SPEED, DAMAGE_METRICS, JUMP_METRICS, SHIELD_METRICS,
ARMOUR_METRICS, CARGO_CAPACITY, FUEL_CAPACITY, UNLADEN_MASS, MAXIMUM_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
}; };
/**
* The ShipSummaryTable constructor
* @param {Object} props The props
*/
constructor(props) { constructor(props) {
super(props); super(props)
autoBind(this); this.didContextChange = this.didContextChange.bind(this);
this.state = { this.state = {
shieldColour: 'blue' shieldColour: 'blue'
}; }
} }
/** /**
@@ -38,248 +31,145 @@ 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 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[8];
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);
let shieldColour = 'blue'; const canThrust = ship.canThrust(cargo, ship.fuelCapacity);
switch (sgType) { const speedTooltip = canThrust ? 'TT_SUMMARY_SPEED' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
case 'biweaveshieldgen': shieldColour = 'purple'; break; const canBoost = ship.canBoost(cargo, ship.fuelCapacity);
case 'prismaticshieldgen': shieldColour = 'green'; break; const boostTooltip = canBoost ? 'TT_SUMMARY_BOOST' : canThrust ? 'TT_SUMMARY_BOOST_NONFUNCTIONAL' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
const sgMetrics = Calc.shieldMetrics(ship, pips.sys || 2);
const armourMetrics = Calc.armourMetrics(ship);
this.state = {
shieldColour: shieldGenerator && shieldGenerator.m.grp === 'psg' ? 'green' : 'blue'
} }
this.state = { shieldColour };
const jumpRangeMetrics = ship.getMetrics(JUMP_METRICS);
// TODO:
const canJump = true;
return <div id='summary'> return <div id='summary'>
<div style={{ display: 'table', width: '100%' }}> <table className={'summaryTable'}>
<div style={{ display: 'table-row' }}> <thead>
<table className={'summaryTable'}> <tr className='main'>
<thead> <th rowSpan={2} className={ cn({ 'bg-warning-disabled': !canThrust }) }>{translate('speed')}</th>
<tr className='main'> <th rowSpan={2} className={ cn({ 'bg-warning-disabled': !canBoost }) }>{translate('boost')}</th>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': speed == 0 }) }>{translate('speed')}</th> <th colSpan={5}>{translate('jump range')}</th>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !canBoost }) }>{translate('boost')}</th> <th rowSpan={2}>{translate('shield')}</th>
<th colSpan={5} className={ cn({ 'bg-warning-disabled': jumpRangeMetrics.jumpRange == 0 }) }>{translate('jump range')}</th> <th rowSpan={2}>{translate('integrity')}</th>
<th rowSpan={2}>{translate('shield')}</th> <th rowSpan={2}>{translate('DPS')}</th>
<th rowSpan={2}>{translate('integrity')}</th> <th rowSpan={2}>{translate('EPS')}</th>
<th rowSpan={2}>{translate('DPS')}</th> <th rowSpan={2}>{translate('TTD')}</th>
<th rowSpan={2}>{translate('EPS')}</th> {/* <th onMouseEnter={termtip.bind(null, 'heat per second')} onMouseLeave={hide} rowSpan={2}>{translate('HPS')}</th> */}
<th rowSpan={2}>{translate('TTD')}</th> <th rowSpan={2}>{translate('cargo')}</th>
{/* <th onMouseEnter={termtip.bind(null, 'heat per second')} onMouseLeave={hide} rowSpan={2}>{translate('HPS')}</th> */} <th rowSpan={2}>{translate('pax')}</th>
<th rowSpan={2}>{translate('cargo')}</th> <th rowSpan={2}>{translate('fuel')}</th>
<th rowSpan={2} onMouseEnter={termtip.bind(null, 'passenger capacity', { cap: 0 })} onMouseLeave={hide}>{translate('pax')}</th> <th colSpan={3}>{translate('mass')}</th>
<th rowSpan={2}>{translate('fuel')}</th> <th onMouseEnter={termtip.bind(null, 'hull hardness', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('hrd')}</th>
<th colSpan={3}>{translate('mass')}</th> <th rowSpan={2}>{translate('crew')}</th>
<th onMouseEnter={termtip.bind(null, 'hull hardness', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('hrd')}</th> <th onMouseEnter={termtip.bind(null, 'mass lock factor', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('MLF')}</th>
<th rowSpan={2}>{translate('crew')}</th> </tr>
<th onMouseEnter={termtip.bind(null, 'mass lock factor', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('MLF')}</th> <tr>
<th onMouseEnter={termtip.bind(null, 'TT_SUMMARY_BOOST_INTERVAL', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('boost interval')}</th> <th className='lft'>{translate('max')}</th>
<th rowSpan={2}>{translate('resting heat (Beta)')}</th> <th>{translate('unladen')}</th>
</tr> <th>{translate('laden')}</th>
<tr> <th>{translate('total unladen')}</th>
<th className="lft">{translate('max')}</th> <th>{translate('total laden')}</th>
<th>{translate('unladen')}</th> <th className='lft'>{translate('hull')}</th>
<th>{translate('laden')}</th> <th>{translate('unladen')}</th>
<th>{translate('total unladen')}</th> <th>{translate('laden')}</th>
<th>{translate('total laden')}</th> </tr>
<th className='lft'>{translate('hull')}</th> </thead>
<th>{translate('unladen')}</th> <tbody>
<th>{translate('laden')}</th> <tr>
</tr> <td onMouseEnter={termtip.bind(null, speedTooltip, { cap: 0 })} onMouseLeave={hide}>{ canThrust ? <span>{int(ship.calcSpeed(4, ship.fuelCapacity, 0, false))}{u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
</thead> <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>
<tbody> <td><span onMouseEnter={termtip.bind(null, 'TT_SUMMARY_MAX_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.jumpRange(ship.unladenMass + ship.standard[2].m.getMaxFuelPerJump(), ship.standard[2].m, ship.standard[2].m.getMaxFuelPerJump()))}{u.LY}</span></td>
<tr> <td><span onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}</span></td>
<td onMouseEnter={termtip.bind(null, speedTooltip, { cap: 0 })} <td><span onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}</span></td>
onMouseLeave={hide} <td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_TOTAL_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}</td>
>{canThrust ? <td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_TOTAL_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}</td>
<span>{int(speed)}{u['m/s']}</span> : <td className={sgClassNames} onMouseEnter={termtip.bind(null, sgTooltip, { cap: 0 })} onMouseLeave={hide}>{int(ship.shield)}{u.MJ}</td>
<span className='warning'>0<Warning/></span> <td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_INTEGRITY', { cap: 0 })} onMouseLeave={hide}>{int(ship.armour)}</td>
}</td> <td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_DPS', { cap: 0 })} onMouseLeave={hide}>{f1(ship.totalDps)}</td>
<td onMouseEnter={termtip.bind(null, boostTooltip, { cap: 0 })} <td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_EPS', { cap: 0 })} onMouseLeave={hide}>{f1(ship.totalEps)}</td>
onMouseLeave={hide} <td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_TTD', { cap: 0 })} onMouseLeave={hide}>{timeToDrain === Infinity ? '∞' : time(timeToDrain)}</td>
>{canBoost ? {/* <td>{f1(ship.totalHps)}</td> */}
<span>{int(shipBoost)}{u['m/s']}</span> : <td>{round(ship.cargoCapacity)}{u.T}</td>
<span className='warning'>0<Warning/></span> <td>{ship.passengerCapacity}</td>
}</td> <td>{round(ship.fuelCapacity)}{u.T}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_MAX_SINGLE_JUMP', { 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>
>{canJump ? <td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_MASS', { cap: 0 })} onMouseLeave={hide}>{int(ship.ladenMass)}{u.T}</td>
// TODO: <td>{int(ship.hardness)}</td>
<span>{NaN}{u.LY}</span> : <td>{ship.crew}</td>
<span className='warning'>0<Warning/></span> <td>{ship.masslock}</td>
}</td> </tr>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_SINGLE_JUMP', { cap: 0 })} </tbody>
onMouseLeave={hide} </table>
>{canJump ? <table className={'summaryTable'}>
// TODO: <thead className={this.state.shieldColour}>
<span>{NaN}{u.LY}</span> : <tr>
<span className='warning'>0<Warning/></span> <th onMouseEnter={termtip.bind(null, 'shield ', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('explres')}</th>
}</td> <th onMouseEnter={termtip.bind(null, 'shield ', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('kinres')}</th>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_SINGLE_JUMP', { cap: 0 })} <th onMouseEnter={termtip.bind(null, 'shield ', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('thermres')}</th>
onMouseLeave={hide}
>{canJump ?
<span>{f2(jumpRangeMetrics.jumpRange)}{u.LY}</span> :
<span className='warning'>0<Warning/></span>
}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_TOTAL_JUMP', { cap: 0 })}
onMouseLeave={hide}
>{canJump ?
// TODO:
<span>{NaN}{u.LY}</span> :
<span className='warning'>0 <Warning/></span>
}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_TOTAL_JUMP', { cap: 0 })}
onMouseLeave={hide}
>{canJump ?
<span>{f2(jumpRangeMetrics.totalRange)}{u.LY}</span> :
<span className='warning'>0<Warning/></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>{ship.get(CARGO_CAPACITY)}{u.T}</td>
<td>{ship.get(PASSENGER_CAPACITY)}</td>
<td>{ship.get(FUEL_CAPACITY)}{u.T}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_HULL_MASS', { cap: 0 })}
onMouseLeave={hide}
>{ship.readProp('hullmass')}{u.T}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_MASS', { cap: 0 })}
onMouseLeave={hide}
>{int(ship.get(UNLADEN_MASS))}{u.T}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_MASS', { cap: 0 })}
onMouseLeave={hide}
>{int(ship.get(MAXIMUM_MASS))}{u.T}</td>
<td>{int(ship.readProp('hardness'))}</td>
<td>{ship.readMeta('crew')}</td>
<td>{ship.readProp('masslock')}</td>
{/* TODO: boost intervall */}
<td>{NaN}</td>
{/* TODO: resting heat */}
<td>{NaN}</td>
</tr>
</tbody>
</table>
<table className={'summaryTable'}>
<thead className={this.state.shieldColour}>
<tr>
<th rowSpan={2} onMouseEnter={termtip.bind(null, 'shield', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('shield')}</th>
<th colSpan={4} className='lft'>{translate('resistance')}</th>
<th colSpan={5} onMouseEnter={termtip.bind(null, 'TT_SUMMARY_SHIELDS_SCB', { cap: 0 })} onMouseLeave={hide} className='lft'>{`${translate('HP')}`}</th> <th onMouseEnter={termtip.bind(null, 'shield ', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('absolute') + ' ' + translate('HP')}</th>
<th rowSpan={2} onMouseEnter={termtip.bind(null, 'PHRASE_SG_RECOVER', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('recovery')}</th> <th onMouseEnter={termtip.bind(null, 'shield ', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('explosive') + ' ' + translate('HP')}</th>
<th rowSpan={2} onMouseEnter={termtip.bind(null, 'PHRASE_SG_RECHARGE', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('recharge')}</th> <th onMouseEnter={termtip.bind(null, 'shield ', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('kinetic') + ' ' + translate('HP')}</th>
</tr> <th onMouseEnter={termtip.bind(null, 'shield ', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('thermal') + ' ' + translate('HP')}</th>
<tr> <th onMouseEnter={termtip.bind(null, 'PHRASE_SG_RECOVER', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('recovery')}</th>
<th>{`${translate('explosive')}`}</th> <th onMouseEnter={termtip.bind(null, 'PHRASE_SG_RECHARGE', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('recharge')}</th>
<th>{`${translate('kinetic')}`}</th> </tr>
<th>{`${translate('thermal')}`}</th> </thead>
<th></th> <tbody>
<tr >
<td>{int(ship.shieldExplRes * 100) + '%'}</td>
<td>{int(ship.shieldThermRes * 100) + '%'}</td>
<td>{int(ship.shieldKinRes * 100) + '%'}</td>
<td>{int(sgMetrics && sgMetrics.generator ? sgMetrics.total / sgMetrics.absolute.total : 0)}</td>
<td>{int(sgMetrics && sgMetrics.generator ? sgMetrics.total / sgMetrics.explosive.total : 0)}</td>
<td>{int(sgMetrics && sgMetrics.generator ? sgMetrics.total / sgMetrics.kinetic.total : 0 )}</td>
<td>{int(sgMetrics && sgMetrics.generator ? sgMetrics.total / sgMetrics.thermal.total : 0 )}</td>
<td>{formats.time(sgMetrics.recover)}</td>
<td>{formats.time(sgMetrics.recharge)}</td>
</tr>
</tbody>
<thead>
<tr>
<th onMouseEnter={termtip.bind(null, 'armour', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('explres')}</th>
<th onMouseEnter={termtip.bind(null, 'armour', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('kinres')}</th>
<th onMouseEnter={termtip.bind(null, 'armour', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('thermres')}</th>
<th className={'bordered'}>{`${translate('absolute')}`}</th> <th onMouseEnter={termtip.bind(null, 'armour', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('absolute') + ' ' + translate('HP')}</th>
<th>{`${translate('explosive')}`}</th> <th onMouseEnter={termtip.bind(null, 'armour', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('explosive') + ' ' + translate('HP')}</th>
<th>{`${translate('kinetic')}`}</th> <th onMouseEnter={termtip.bind(null, 'armour', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('kinetic') + ' ' + translate('HP')}</th>
<th>{`${translate('thermal')}`}</th> <th onMouseEnter={termtip.bind(null, 'armour', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('thermal') + ' ' + translate('HP')}</th>
<th></th> <th onMouseEnter={termtip.bind(null, 'armour', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('raw module armour')}</th>
</tr> <th onMouseEnter={termtip.bind(null, 'armour', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('internal protection')}</th>
</thead>
<tbody>
<tr>
<td>{translate(sgType || 'No Shield')}</td>
<td>{formats.pct1(1 - sgMetrics.explosive.damageMultiplier)}</td>
<td>{formats.pct1(1 - sgMetrics.kinetic.damageMultiplier)}</td>
<td>{formats.pct1(1 - sgMetrics.thermal.damageMultiplier)}</td>
<td></td>
<td>{int(sgMetrics.shieldStrength || 0)}{u.MJ}</td>
<td>{int(sgMetrics.shieldStrength / sgMetrics.explosive.damageMultiplier || 0)}{u.MJ}</td>
<td>{int(sgMetrics.shieldStrength / sgMetrics.kinetic.damageMultiplier || 0)}{u.MJ}</td>
<td>{int(sgMetrics.shieldStrength / sgMetrics.thermal.damageMultiplier || 0)}{u.MJ}</td>
<td></td>
<td>{formats.time(sgMetrics.recover) || translate('Never')}</td>
<td>{formats.time(sgMetrics.recharge) || translate('Never')}</td>
</tr>
</tbody>
<thead>
<tr>
<th rowSpan={2} onMouseEnter={termtip.bind(null, 'armour', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('armour')}</th>
<th colSpan={4} className='lft'>{translate('resistance')}</th>
<th colSpan={5} onMouseEnter={termtip.bind(null, 'PHRASE_EFFECTIVE_ARMOUR', { cap: 0 })} onMouseLeave={hide} className='lft'>{`${translate('HP')}`}</th> </tr>
<th rowSpan={2} onMouseEnter={termtip.bind(null, 'TT_MODULE_ARMOUR', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('raw module armour')}</th> </thead>
<th rowSpan={2} onMouseEnter={termtip.bind(null, 'TT_MODULE_PROTECTION_INTERNAL', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('internal protection')}</th> <tbody>
</tr> <tr>
<tr> <td>{int(ship.hullExplRes * 100) + '%'}</td>
<th>{`${translate('explosive')}`}</th> <td>{int(ship.hullThermRes * 100) + '%'}</td>
<th>{`${translate('kinetic')}`}</th> <td>{int(ship.hullKinRes * 100) + '%'}</td>
<th>{`${translate('thermal')}`}</th> <td>{int(armourMetrics.total / armourMetrics.absolute.total)}</td>
<th>{`${translate('caustic')}`}</th> <td>{int(armourMetrics.total / armourMetrics.explosive.total)}</td>
<td>{int(armourMetrics.total / armourMetrics.kinetic.total)}</td>
<td>{int(armourMetrics.total / armourMetrics.thermal.total)}</td>
<td>{int(armourMetrics.modulearmour)}</td>
<td>{int(armourMetrics.moduleprotection * 100) + '%'}</td>
</tr>
</tbody>
</table>
<th className={'bordered'}>{`${translate('absolute')}`}</th>
<th>{`${translate('explosive')}`}</th>
<th>{`${translate('kinetic')}`}</th>
<th>{`${translate('thermal')}`}</th>
<th>{`${translate('caustic')}`}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{translate(ship.getAlloys().readMeta('type') || 'No Armour')}</td>
<td>{formats.pct1(1 - armourMetrics.explosive.damageMultiplier)}</td>
<td>{formats.pct1(1 - armourMetrics.kinetic.damageMultiplier)}</td>
<td>{formats.pct1(1 - armourMetrics.thermal.damageMultiplier)}</td>
<td>{formats.pct1(1 - armourMetrics.caustic.damageMultiplier)}</td>
<td>{int(armourMetrics.armour)}</td>
<td>{int(armourMetrics.armour / armourMetrics.explosive.damageMultiplier)}</td>
<td>{int(armourMetrics.armour / armourMetrics.kinetic.damageMultiplier)}</td>
<td>{int(armourMetrics.armour / armourMetrics.thermal.damageMultiplier)}</td>
<td>{int(armourMetrics.armour / armourMetrics.caustic.damageMultiplier)}</td>
<td>{int(moduleProtectionMetrics.moduleArmour)}</td>
<td>{formats.pct1(1 - moduleProtectionMetrics.moduleProtection)}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>; </div>;
} }
} }

View File

@@ -17,12 +17,12 @@ export default class Slider extends React.Component {
static propTypes = { static propTypes = {
axis: PropTypes.bool, axis: PropTypes.bool,
axisUnit: PropTypes.string,// units (T, M, etc.) axisUnit: PropTypes.string,
max: PropTypes.number, max: PropTypes.number,
min: PropTypes.number, min: PropTypes.number,
onChange: PropTypes.func.isRequired,// function which determins percent value onChange: PropTypes.func.isRequired,
onResize: PropTypes.func, onResize: PropTypes.func,
percent: PropTypes.number.isRequired,// value of slider percent: PropTypes.number.isRequired,
scale: PropTypes.number scale: PropTypes.number
}; };
@@ -35,11 +35,6 @@ export default class Slider extends React.Component {
this._down = this._down.bind(this); this._down = this._down.bind(this);
this._move = this._move.bind(this); this._move = this._move.bind(this);
this._up = this._up.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._updatePercent = this._updatePercent.bind(this);
this._updateDimensions = this._updateDimensions.bind(this); this._updateDimensions = this._updateDimensions.bind(this);
@@ -55,7 +50,6 @@ 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);
} }
/** /**
@@ -75,70 +69,11 @@ 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
@@ -201,186 +136,31 @@ export default class Slider extends React.Component {
render() { render() {
let outerWidth = this.state.outerWidth; let outerWidth = this.state.outerWidth;
let { axis, axisUnit, min, max, scale } = this.props; let { axis, axisUnit, min, max, scale } = this.props;
let style = { let style = {
width: '100%', width: '100%',
height: axis ? '2.5em' : '1.5em', height: axis ? '2.5em' : '1.5em',
boxSizing: 'border-box' boxSizing: 'border-box'
}; };
if (!outerWidth) { if (!outerWidth) {
return <svg style={style} ref={node => this.node = node} />; return <svg style={style} ref={node => this.node = node} />;
} }
let margin = MARGIN_LR * scale; let margin = MARGIN_LR * scale;
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
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"> return <svg onMouseUp={this._up} onMouseEnter={this._enter.bind(this)} onMouseMove={this._move} onTouchEnd={this._up} style={style} ref={node => this.node = node}>
<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} />
<rect x={margin} width={width} height='100%' fillOpacity='0' style={{ cursor: 'col-resize' }} onMouseDown={this._down} onTouchMove={this._move} onTouchStart={this._down} onTouchEnd={this._touchend} /> <rect x={margin} width={width} height='100%' fillOpacity='0' style={{ cursor: 'col-resize' }} onMouseDown={this._down} onTouchMove={this._move} onTouchStart={this._down} />
{axis && <g style={{ fontSize: '.7em' }}> {axis && <g style={{ fontSize: '.7em' }}>
<text className='primary-disabled' y='3em' x={margin} style={{ textAnchor: 'middle' }}>{min + axisUnit}</text> <text className='primary-disabled' y='3em' x={margin} style={{ textAnchor: 'middle' }}>{min + axisUnit}</text>
<text className='primary-disabled' y='3em' x='50%' style={{ textAnchor: 'middle' }}>{(min + max / 2) + axisUnit}</text> <text className='primary-disabled' y='3em' x='50%' style={{ textAnchor: 'middle' }}>{(min + max / 2) + axisUnit}</text>
<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>;
} }
} }
/**
* 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>;
}
}

View File

@@ -2,38 +2,31 @@ 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 { diffDetails } from '../utils/SlotFunctions'; import { diffDetails } from '../utils/SlotFunctions';
import { stopCtxPropagation, wrapCtxMenu } from '../utils/UtilityFunctions'; import { wrapCtxMenu } from '../utils/UtilityFunctions';
import { blueprintTooltip } from '../utils/BlueprintFunctions'; import { stopCtxPropagation } from '../utils/UtilityFunctions';
import { Module } from 'ed-forge';
import { REG_MILITARY_SLOT, REG_HARDPOINT_SLOT } from 'ed-forge/lib/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,
}; };
/** /**
@@ -42,122 +35,23 @@ 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);
} }
/** // Must be implemented by subclasses:
* Opens a menu while setting state. // _getSlotDetails()
* @param {Object} newMenuIndex New menu index
* @param {Event} event Event object
*/
_openMenu(newMenuIndex, event) {
const slotName = this.props.m.getSlot();
if (
this.props.currentMenu === slotName &&
newMenuIndex === this.state.menuIndex
) {
this.context.closeMenu();
} {
this.setState({ menuIndex: newMenuIndex });
this.context.openMenu(slotName);
}
// If we don't stop event propagation, the underlying divs also might
// get clicked which would open up other menus
event.stopPropagation();
}
/** /**
* Generate the slot contents * Get the CSS class name for the slot. Can/should be overriden
* @return {React.Component} Slot contents * as necessary.
* @return {string} CSS Class name
*/ */
_getSlotDetails() { _getClassNames() {
const { m, propsToShow } = this.props; return null;
let { termtip, tooltip, language } = this.context;
const { translate, units, formats } = language;
if (m.isEmpty()) {
return <div className="empty">
{translate(
m.getSlot().match(REG_MILITARY_SLOT) ? 'emptyrestricted' : 'empty'
)}
</div>;
} else {
let classRating = m.getClassRating();
let { drag, drop } = this.props;
// Modifications tooltip shows blueprint and grade, if available
let modTT = translate('modified');
const blueprint = m.getBlueprint();
const experimental = m.getExperimental();
const grade = m.getBlueprintGrade();
if (blueprint) {
modTT = `${translate(blueprint)} ${translate('grade')}: ${grade}`;
if (experimental) {
modTT += `, ${translate(experimental)}`;
}
modTT = (
<div>
<div>{modTT}</div>
{blueprintTooltip(language, m)}
</div>
);
}
let mass = m.get('mass') || m.get('cargo') || m.get('fuel') || 0;
return (
<div
className={cn('details', { disabled: !m.isEnabled() })}
draggable="true"
onDragStart={drag}
onDragEnd={drop}
>
<div className={'cb'}>
<div className={'l'}>
{classRating} {translate(m.readMeta('type'))}
{blueprint && (
<span
onMouseOver={termtip.bind(null, modTT)}
onMouseOut={tooltip.bind(null, null)}
>
<Modified />
</span>
)}
</div>
{propsToShow.mass ?
<div className={'r'}>
{formats.round(mass)}
{units.T}
</div> : null}
</div>
<div className={'cb'}>
{toPairs(propsToShow).sort().map(([prop, show]) => {
const { unit, value } = m.getFormatted(prop, true);
// Don't show mass again; it's already shown on the top right
// corner of a slot
if (!show || isNaN(value) || prop == 'mass') {
return null;
} else {
return (<div className='l'>
{translate(prop)}: {formats.round(value)}{unit}
</div>);
}
})}
{(m.getApplicableBlueprints() || []).length > 0 ? (
<div className="r">
<button onClick={this._openMenu.bind(this, 1)}
onContextMenu={stopCtxPropagation}
onMouseOver={termtip.bind(null, translate('modifications'))}
onMouseOut={tooltip.bind(null, null)}
>
<ListModifications />
</button>
</div>
) : null}
</div>
</div>
);
}
} }
/** /**
@@ -166,19 +60,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.getSize();
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 Boolean(m.getSlot().match(REG_HARDPOINT_SLOT)):
return HARDPOINT_SLOT_LABELS[size];
default:
return size;
}
} }
/** /**
@@ -188,13 +70,7 @@ 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();
}
} }
/** /**
@@ -204,42 +80,61 @@ 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}
/>;
} else {
menu = <AvailableModulesMenu
className={this._getClassNames()}
modules={availableModules()}
shipMass={ship.hullMass}
m={m}
onSelect={onSelect}
warning={warning}
diffDetails={diffDetails.bind(ship, this.context.language)}
/>;
}
}
// TODO: implement touch dragging // TODO: implement touch dragging
const selected = currentMenu === m.getSlot();
return ( return (
<div <div className={cn('slot', dropClass, { selected })} onClick={onOpen} onContextMenu={this._contextMenu} onDragOver={dragOver}>
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>
> {menu}
<div className={cn(
'details-container',
{ warning: warning && warning(m) },
)}>
<div className="sz">{this._getMaxClassLabel(translate)}</div>
{this._getSlotDetails()}
</div>
{selected && menuIndex === 0 &&
<AvailableModulesMenu
m={m} hideSearch={hideSearch}
onSelect={(item) => {
m.setItem(item);
this.context.closeMenu();
}}
warning={warning}
// diffDetails={diffDetails.bind(ship, this.context.language)}
/>}
{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;
}
} }

View File

@@ -5,33 +5,41 @@ import { wrapCtxMenu } from '../utils/UtilityFunctions';
import { canMount } from '../utils/SlotFunctions'; 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,
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._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.state = {}; this.state = {};
} }
@@ -39,7 +47,31 @@ export default class SlotSection extends TranslatedComponent {
// _getSlots() // _getSlots()
// _getSectionMenu() // _getSectionMenu()
// _contextMenu() // _contextMenu()
// componentDidUpdate(prevProps)
/**
* 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
@@ -105,18 +137,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
@@ -145,18 +169,7 @@ export default class SlotSection extends TranslatedComponent {
targetSlot.enabled = targetEnabled; targetSlot.enabled = targetEnabled;
targetSlot.priority = targetPriority; targetSlot.priority = targetPriority;
} }
this.props.ship this.props.onChange();
.updatePowerGenerated()
.updatePowerUsed()
.recalculateMass()
.updateJumpStats()
.recalculateShield()
.recalculateShieldCells()
.recalculateArmour()
.recalculateDps()
.recalculateEps()
.recalculateHps()
.updateMovement();
} }
} }
} }
@@ -190,17 +203,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
*/ */
@@ -217,13 +219,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>{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>

View File

@@ -0,0 +1,139 @@
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;
}
/**
* 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 || []);
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}
/>;
} 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)}
/>;
}
}
return (
<div className={cn('slot', { selected: this.props.selected })} onClick={this.props.onOpen} onContextMenu={stopCtxPropagation}>
<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' ><button 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;
}
}

View File

@@ -1,26 +1,25 @@
import React from 'react'; import React from 'react';
import cn from 'classnames'; import cn from 'classnames';
import SlotSection from './SlotSection'; import SlotSection from './SlotSection';
import Slot from './Slot'; import StandardSlot from './StandardSlot';
import Module from '../shipyard/Module'; import Module from '../shipyard/Module';
import * as ShipRoles from '../shipyard/ShipRoles'; import * as ShipRoles from '../shipyard/ShipRoles';
import autoBind from 'auto-bind'; import { stopCtxPropagation } from '../utils/UtilityFunctions';
import { stopCtxPropagation, moduleGet } from '../utils/UtilityFunctions';
import { ShipProps } from 'ed-forge';
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);
} }
/** /**
@@ -28,6 +27,9 @@ export default class StandardSlotSection extends SlotSection {
*/ */
_optimizeStandard() { _optimizeStandard() {
this.props.ship.useLightestStandard(); 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();
} }
@@ -38,6 +40,9 @@ export default class StandardSlotSection extends SlotSection {
*/ */
_multiPurpose(shielded, bulkheadIndex) { _multiPurpose(shielded, bulkheadIndex) {
ShipRoles.multiPurpose(this.props.ship, shielded, bulkheadIndex); ShipRoles.multiPurpose(this.props.ship, shielded, bulkheadIndex);
this.props.onChange();
this.props.onCargoChange(this.props.ship.cargoCapacity);
this.props.onFuelChange(this.props.ship.fuelCapacity);
this._close(); this._close();
} }
@@ -47,6 +52,9 @@ export default class StandardSlotSection extends SlotSection {
*/ */
_optimizeCargo(shielded) { _optimizeCargo(shielded) {
ShipRoles.trader(this.props.ship, shielded); ShipRoles.trader(this.props.ship, shielded);
this.props.onChange();
this.props.onCargoChange(this.props.ship.cargoCapacity);
this.props.onFuelChange(this.props.ship.fuelCapacity);
this._close(); this._close();
} }
@@ -56,6 +64,9 @@ export default class StandardSlotSection extends SlotSection {
*/ */
_optimizeMiner(shielded) { _optimizeMiner(shielded) {
ShipRoles.miner(this.props.ship, shielded); 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(); this._close();
} }
@@ -65,6 +76,9 @@ export default class StandardSlotSection extends SlotSection {
*/ */
_optimizeExplorer(planetary) { _optimizeExplorer(planetary) {
ShipRoles.explorer(this.props.ship, 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(); this._close();
} }
@@ -73,6 +87,20 @@ export default class StandardSlotSection extends SlotSection {
*/ */
_optimizeRacer() { _optimizeRacer() {
ShipRoles.racer(this.props.ship); 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(); this._close();
} }
@@ -83,48 +111,113 @@ export default class StandardSlotSection extends SlotSection {
this._optimizeStandard(); this._optimizeStandard();
} }
/**
* Creates a new slot for a given module.
* @param {Module} m Module to create the slot for
* @param {function} warning Warning callback
* @return {React.Component} Slot component
*/
_mkSlot(m, warning) {
const { currentMenu, propsToShow, onPropToggle } = this.props;
return <Slot key={m.getSlot()} m={m} warning={warning} hideSearch={true}
currentMenu={currentMenu} propsToShow={propsToShow} onPropToggle={onPropToggle}
/>;
}
/** /**
* Generate the slot React Components * Generate the slot React Components
* @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;
} }
/** /**
@@ -132,22 +225,23 @@ 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;
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._optimizeStandard}>{translate('Maximize Jump Range')}</li> <li className='lc' onClick={this._optimizeStandard}>{translate('Maximize Jump Range')}</li>
</ul> </ul>
<div className='select-group cap'>{translate('roles')}</div> <div className='select-group cap'>{translate('roles')}</div>
<ul> <ul>
<li className='lc' tabIndex="0" onClick={this._multiPurpose.bind(this, false, 0)}>{translate('Multi-purpose')}</li> <li className='lc' onClick={this._multiPurpose.bind(this, false, 0)}>{translate('Multi-purpose')}</li>
<li className='lc' tabIndex="0" onClick={this._multiPurpose.bind(this, true, 2)}>{translate('Combat')}</li> <li className='lc' onClick={this._multiPurpose.bind(this, true, 2)}>{translate('Combat')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeCargo.bind(this, true)}>{translate('Trader')}</li> <li className='lc' onClick={this._optimizeCargo.bind(this, true)}>{translate('Trader')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeExplorer.bind(this, false)}>{translate('Explorer')}</li> <li className='lc' onClick={this._optimizeExplorer.bind(this, false)}>{translate('Explorer')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeExplorer.bind(this, true)}>{translate('Planetary Explorer')}</li> <li className={cn('lc', { disabled: planetaryDisabled })} onClick={!planetaryDisabled && this._optimizeExplorer.bind(this, true)}>{translate('Planetary Explorer')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeMiner.bind(this, true)}>{translate('Miner')}</li> <li className='lc' onClick={this._optimizeMiner.bind(this, true)}>{translate('Miner')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeRacer.bind(this)}>{translate('Racer')}</li> <li className='lc' onClick={this._optimizeRacer.bind(this)}>{translate('Racer')}</li>
</ul> </ul>
</div>; </div>;
} }
} }

View File

@@ -228,96 +228,6 @@ export class LinkIcon extends SvgIcon {
} }
} }
/**
* Link / Permalink / Chain
*/
export class OrbisIcon extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return (
<g transform="scale(.037296)">
<path d="m429 319c60.75 0 110 49.248 110 110 0 60.75-49.247 110-110 110s-110-49.247-110-110c0-60.749 49.248-110 110-110m0-20c-34.724 0-67.369 13.522-91.922 38.075s-38.075 57.198-38.075 91.922 13.522 67.369 38.075 91.922c24.553 24.554 57.198 38.075 91.922 38.075s67.369-13.521 91.922-38.075c24.554-24.553 38.075-57.198 38.075-91.922s-13.521-67.369-38.075-91.922c-24.553-24.553-57.198-38.075-91.922-38.075z" />
<path d="m429 235c107.14 0 194 86.855 194 194s-86.855 194-194 194-194-86.855-194-194 86.855-194 194-194m0-20c-28.881 0-56.908 5.661-83.304 16.825-25.485 10.779-48.368 26.207-68.016 45.853-19.646 19.647-35.074 42.53-45.853 68.015-11.163 26.396-16.824 54.423-16.824 83.304s5.661 56.908 16.825 83.304c10.779 25.484 26.207 48.368 45.853 68.016 19.647 19.646 42.53 35.073 68.015 45.854 26.396 11.164 54.423 16.825 83.304 16.825s56.908-5.661 83.304-16.825c25.484-10.779 48.368-26.206 68.016-45.854 19.646-19.646 35.073-42.53 45.854-68.016 11.164-26.396 16.825-54.423 16.825-83.304s-5.661-56.908-16.825-83.304c-10.779-25.485-26.206-48.368-45.854-68.015-19.646-19.646-42.53-35.074-68.016-45.853-26.396-11.164-54.423-16.825-83.304-16.825z" />
<path d="m429 63c202.14 0 366 163.86 366 366s-163.86 366-366 366-366-163.86-366-366 163.86-366 366-366m0-20c-52.101 0-102.65 10.208-150.25 30.342-45.966 19.442-87.244 47.271-122.69 82.714s-63.272 76.721-82.714 122.69c-20.134 47.601-30.342 98.153-30.342 150.25s10.208 102.65 30.342 150.25c19.442 45.967 47.271 87.244 82.714 122.69 35.443 35.442 76.721 63.271 122.69 82.715 47.601 20.133 98.153 30.342 150.25 30.342s102.65-10.209 150.25-30.342c45.967-19.442 87.244-47.271 122.69-82.715 35.441-35.442 63.271-76.721 82.714-122.69 20.133-47.601 30.342-98.153 30.342-150.25s-10.209-102.65-30.342-150.25c-19.442-45.966-47.271-87.244-82.714-122.69s-76.722-63.272-122.69-82.714c-47.601-20.134-98.153-30.342-150.25-30.342z"/>
<path d="m429 63c202.14 0 366 163.86 366 366s-163.86 366-366 366-366-163.86-366-366 163.86-366 366-366m0-20c-52.101 0-102.65 10.208-150.25 30.342-45.966 19.442-87.244 47.271-122.69 82.714s-63.272 76.721-82.714 122.69c-20.134 47.601-30.342 98.153-30.342 150.25s10.208 102.65 30.342 150.25c19.442 45.967 47.271 87.244 82.714 122.69 35.443 35.442 76.721 63.271 122.69 82.715 47.601 20.133 98.153 30.342 150.25 30.342s102.65-10.209 150.25-30.342c45.967-19.442 87.244-47.271 122.69-82.715 35.441-35.442 63.271-76.721 82.714-122.69 20.133-47.601 30.342-98.153 30.342-150.25s-10.209-102.65-30.342-150.25c-19.442-45.966-47.271-87.244-82.714-122.69s-76.722-63.272-122.69-82.714c-47.601-20.134-98.153-30.342-150.25-30.342z" />
<path d="m429 20c225.88 0 409 183.11 409 409s-183.11 409-409 409-409-183.11-409-409 183.11-409 409-409m0-20c-57.905 0-114.09 11.345-166.99 33.721-51.087 21.608-96.963 52.538-136.36 91.93s-70.321 85.269-91.93 136.36c-22.376 52.902-33.721 109.09-33.721 166.99s11.345 114.09 33.721 166.99c21.608 51.087 52.538 96.964 91.93 136.35 39.392 39.392 85.269 70.321 136.36 91.931 52.902 22.375 109.09 33.721 166.99 33.721s114.09-11.346 166.99-33.721c51.087-21.608 96.964-52.538 136.35-91.931 39.392-39.392 70.321-85.269 91.931-136.35 22.375-52.902 33.721-109.09 33.721-166.99s-11.346-114.09-33.721-166.99c-21.608-51.087-52.538-96.963-91.931-136.36-39.392-39.392-85.269-70.321-136.35-91.93-52.902-22.376-109.09-33.721-166.99-33.721z"/>
<path d="m155.34 679.12 173.25-190.21-15.626-13.721-170.9 190.4zm31.01 31.714 202.41-169.1-16.418-14.417-198.76 170.43z"/>
<path d="m702.66 178.87-173.25 190.21 15.625 13.721 170.9-190.4zm-31.01-31.714-202.41 169.1 16.418 14.417 198.76-170.43z" />
<rect transform="matrix(-.7071 -.7071 .7071 -.7071 429.34 1036.2)" x="387.09" y="420.77" width="84.379" height="16.859" />
</g>);
}
}
/**
* Material
*/
export class MatIcon extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return<g xmlns="http://www.w3.org/2000/svg">
<path fill="#FF7100" d="M 24.86,4.18
C 24.86,4.18 17.17,7.82 17.17,7.82
17.17,7.82 15.35,14.55 15.35,14.55
15.35,14.55 24.70,9.75 24.70,9.75
24.70,9.75 24.86,4.18 24.86,4.18 Z
M 32.21,17.45
C 32.21,17.45 26.41,11.18 26.41,11.18
26.41,11.18 19.51,11.51 19.51,11.51
19.51,11.51 26.92,19.01 26.92,19.01
26.92,19.01 32.21,17.45 32.21,17.45 Z
M 21.99,28.62
C 21.99,28.62 26.10,21.10 26.10,21.10
26.10,21.10 23.66,14.57 23.66,14.57
23.66,14.57 18.89,24.01 18.89,24.01
18.89,24.01 21.99,28.62 21.99,28.62 Z
M 8.33,22.24
C 8.33,22.24 16.67,23.87 16.67,23.87
16.67,23.87 22.06,19.51 22.06,19.51
22.06,19.51 11.70,17.84 11.70,17.84
11.70,17.84 8.33,22.24 8.33,22.24 Z
M 10.11,7.14
C 10.11,7.14 11.15,15.66 11.15,15.66
11.15,15.66 16.92,19.49 16.92,19.49
16.92,19.49 15.29,9.02 15.29,9.02
15.29,9.02 10.11,7.14 10.11,7.14 Z
M 27.69,2.67
C 27.69,2.67 35.89,16.00 35.89,16.00
35.89,16.00 27.69,29.33 27.69,29.33
27.69,29.33 11.31,29.33 11.31,29.33
11.31,29.33 3.11,16.00 3.11,16.00
3.11,16.00 11.31,2.67 11.31,2.67
11.31,2.67 27.67,2.67 27.67,2.67M 29.16,0.00
C 29.16,0.00 27.69,0.00 27.69,0.00
27.69,0.00 11.31,0.00 11.31,0.00
11.31,0.00 9.84,0.00 9.84,0.00
9.84,0.00 9.06,1.25 9.06,1.25
9.06,1.25 0.87,14.57 0.87,14.57
0.87,14.57 0.00,15.98 0.00,15.98
0.00,15.98 0.87,17.39 0.87,17.39
0.87,17.39 9.06,30.73 9.06,30.73
9.06,30.73 9.84,32.00 9.84,32.00
9.84,32.00 11.31,32.00 11.31,32.00
11.31,32.00 27.69,32.00 27.69,32.00
27.69,32.00 29.16,32.00 29.16,32.00
29.16,32.00 29.94,30.73 29.94,30.73
29.94,30.73 38.13,17.39 38.13,17.39
38.13,17.39 39.00,15.98 39.00,15.98
39.00,15.98 38.13,14.57 38.13,14.57
38.13,14.57 29.94,1.25 29.94,1.25
29.94,1.25 29.16,0.00 29.16,0.00
29.16,0.00 29.16,0.00 29.16,0.00 Z" />
</g>;
}
}
/** /**
* Shopping icon (dollar sign) * Shopping icon (dollar sign)
*/ */
@@ -741,9 +651,9 @@ export class Modified extends SvgIcon {
*/ */
svg() { svg() {
return <g> return <g>
<path d="M100,5L18,52.5L18,147.5L100,195L182,147.5L182,52.5L100,5Z"/> <path d="M100,5L18,52.5L18,147.5L100,195L182,147.5L182,52.5L100,5Z"/>
<path d="M100,70L74,85L74,115L100,130L126,115L126,85L100,70Z"/> <path d="M100,70L74,85L74,115L100,130L126,115L126,85L100,70Z"/>
</g>; </g>;
} }
} }

View File

@@ -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>;
} }
} }

View File

@@ -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,

View File

@@ -1,20 +1,22 @@
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 HardpointSlot from './HardpointSlot';
import { stopCtxPropagation } from '../utils/UtilityFunctions'; import { stopCtxPropagation } from '../utils/UtilityFunctions';
import autoBind from 'auto-bind';
/** /**
* Utility Slot Section * Utility Slot Section
*/ */
export default class UtilitySlotSection extends SlotSection { 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);
} }
/** /**
@@ -52,24 +54,31 @@ export default class UtilitySlotSection extends SlotSection {
*/ */
_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) {
maxClass={h.getSize()} slots.push(<HardpointSlot
onChange={this.props.onChange} key={i}
currentMenu={currentMenu} maxClass={h.maxClass}
drag={this._drag.bind(this, h)} availableModules={() => availableModules.getHps(h.maxClass)}
dragOver={this._dragOverSlot.bind(this, h)} onOpen={this._openMenu.bind(this,h)}
drop={this._drop} onSelect={this._selectModule.bind(this, h)}
dropClass={this._dropClass(h, originSlot, targetSlot)} onChange={this.props.onChange}
m={h} selected={currentMenu == h}
enabled={h.enabled ? true : false} drag={this._drag.bind(this, h)}
propsToShow={propsToShow} dragOver={this._dragOverSlot.bind(this, h)}
onPropToggle={onPropToggle} drop={this._drop}
/>); dropClass={this._dropClass(h, originSlot, targetSlot)}
ship={ship}
m={h.m}
enabled={h.enabled ? true : false}
/>);
}
} }
return slots; return slots;
@@ -80,35 +89,35 @@ 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' onClick={this._empty}>{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, 'sb', 'A', null)}>A</li> <li className='c' onClick={_use.bind(this, 'sb', 'A', null)}>A</li>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'B', null)}>B</li> <li className='c' onClick={_use.bind(this, 'sb', 'B', null)}>B</li>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'C', null)}>C</li> <li className='c' onClick={_use.bind(this, 'sb', 'C', null)}>C</li>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'D', null)}>D</li> <li className='c' onClick={_use.bind(this, 'sb', 'D', null)}>D</li>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'E', null)}>E</li> <li className='c' onClick={_use.bind(this, 'sb', 'E', null)}>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, 'hs', null, 'Heat Sink Launcher')}>{translate('Heat Sink Launcher')}</li> <li className='lc' onClick={_use.bind(this, 'hs', null, 'Heat Sink Launcher')}>{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, 'ch', null, 'Chaff Launcher')}>{translate('Chaff Launcher')}</li> <li className='lc' onClick={_use.bind(this, 'ch', null, 'Chaff Launcher')}>{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, 'po', null, 'Point Defence')}>{translate('Point Defence')}</li> <li className='lc' onClick={_use.bind(this, 'po', null, 'Point Defence')}>{translate('Point Defence')}</li>
</ul> </ul>
</div>; </div>;
} }
} }

View File

@@ -1,7 +1,7 @@
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import ContainerDimensions from 'react-container-dimensions'; import Measure from 'react-measure';
import { BarChart, Bar, XAxis, YAxis, LabelList } from 'recharts'; import { BarChart, Bar, XAxis, YAxis } from 'recharts';
const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D'];
const LABEL_COLOUR = '#000000'; const LABEL_COLOUR = '#000000';
@@ -17,6 +17,7 @@ const merge = function(one, two) {
* A vertical bar chart * A vertical bar chart
*/ */
export default class VerticalBarChart extends TranslatedComponent { export default class VerticalBarChart extends TranslatedComponent {
static propTypes = { static propTypes = {
data : PropTypes.array.isRequired, data : PropTypes.array.isRequired,
yMax : PropTypes.number yMax : PropTypes.number
@@ -31,6 +32,13 @@ export default class VerticalBarChart extends TranslatedComponent {
super(props); super(props);
this._termtip = this._termtip.bind(this); this._termtip = this._termtip.bind(this);
this.state = {
dimensions: {
width: 300,
height: 300
}
};
} }
/** /**
@@ -38,6 +46,7 @@ export default class VerticalBarChart extends TranslatedComponent {
* @returns {Object} the markup * @returns {Object} the markup
*/ */
render() { render() {
const { width, height } = this.state.dimensions;
const { tooltip, termtip } = this.context; const { tooltip, termtip } = this.context;
// Calculate maximum for Y // Calculate maximum for Y
@@ -47,19 +56,15 @@ export default class VerticalBarChart extends TranslatedComponent {
const localMax = Math.max(dataMax, yMax); const localMax = Math.max(dataMax, yMax);
return ( return (
<ContainerDimensions> <Measure whitelist={['width', 'top']} onMeasure={ (dimensions) => this.setState({ dimensions }) }>
{ ({ width }) => ( <div width='100%'>
<div width='100%'> <BarChart width={width} height={width * ASPECT} data={this.props.data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<BarChart width={width} height={width * ASPECT} data={this.props.data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}> <XAxis interval={0} fontSize='0.8em' stroke={AXIS_COLOUR} dataKey='label' />
<XAxis interval={0} fontSize='0.8em' stroke={AXIS_COLOUR} dataKey='label' /> <YAxis interval={'preserveStart'} tickCount={11} fontSize='0.8em' stroke={AXIS_COLOUR} type='number' domain={[0, localMax]}/>
<YAxis interval={'preserveStart'} tickCount={11} fontSize='0.8em' stroke={AXIS_COLOUR} type='number' domain={[0, localMax]}/> <Bar dataKey='value' label={<ValueLabel />} fill={CORIOLIS_COLOURS[0]} isAnimationActive={false} onMouseOver={this._termtip} onMouseOut={tooltip.bind(null, null)}/>
<Bar dataKey='value' fill={CORIOLIS_COLOURS[0]} isAnimationActive={false} onMouseOver={this._termtip} onMouseOut={tooltip.bind(null, null)}> </BarChart>
<LabelList dataKey='value' position='insideTop'/> </div>
</Bar> </Measure>
</BarChart>
</div>
)}
</ContainerDimensions>
); );
} }
@@ -78,3 +83,29 @@ export default class VerticalBarChart extends TranslatedComponent {
} }
} }
} }
/**
* A label that displays the value within the bar of the chart
*/
class ValueLabel extends React.Component {
static propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
value: PropTypes.number
};
/**
* Render offence
* @return {React.Component} contents
*/
render() {
const { x, y, payload, value } = this.props;
const em = value < 1000 ? '1em' : value < 1000 ? '0.8em' : '0.7em';
return (
<text x={x} y={y} fill="#000000" textAnchor="middle" dy={20} style={{ fontSize: em }}>{value}</text>
);
}
};

View File

@@ -1,105 +1,204 @@
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 { nameComparator } from '../utils/SlotFunctions';
import { CollapseSection, ExpandSection, MountFixed, MountGimballed, MountTurret } from './SvgIcons';
import LineChart from '../components/LineChart'; import LineChart from '../components/LineChart';
import Slider from '../components/Slider';
import * as Calc from '../shipyard/Calculations'; import * as Calc from '../shipyard/Calculations';
import { moduleReduce } from 'ed-forge/lib/helper'; import Module from '../shipyard/Module';
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> <span>
<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}
/> />
</div> </span>
); );
} }
} }

View File

@@ -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;
} }
@@ -62,20 +60,17 @@ export function getLanguage(langCode) {
}, },
translate, translate,
units: { units: {
ang: '°', // Angle
CR: <u>{translate('CR')}</u>, // Credits CR: <u>{translate('CR')}</u>, // Credits
kg: <u>{translate('kg')}</u>, // Kilograms kg: <u>{translate('kg')}</u>, // Kilograms
kgs: <u>{translate('kg/s')}</u>, // Kilograms per second kgs: <u>{translate('kg/s')}</u>, // Kilograms per second
km: <u>{translate('km')}</u>, // Kilometers km: <u>{translate('km')}</u>, // Kilometers
Ls: <u>{translate('Ls')}</u>, // Light Seconds Ls: <u>{translate('Ls')}</u>, // Light Seconds
LY: <u>{translate('LY')}</u>, // Light Years LY: <u>{translate('LY')}</u>, // Light Years
m: <u>{translate('m')}</u>, // Meters
MJ: <u>{translate('MJ')}</u>, // Mega Joules MJ: <u>{translate('MJ')}</u>, // Mega Joules
'm/s': <u>{translate('m/s')}</u>, // Meters per second 'm/s': <u>{translate('m/s')}</u>, // Meters per second
'°/s': <u>{translate('°/s')}</u>, // Degrees per second '°/s': <u>{translate('°/s')}</u>, // Degrees per second
MW: <u>{translate('MW')}</u>, // Mega Watts (same as Mega Joules per second) MW: <u>{translate('MW')}</u>, // Mega Watts (same as Mega Joules per second)
mps: <u>{translate('m/s')}</u>, // Metres per second mps: <u>{translate('m/s')}</u>, // Metres per second
pct: '%', // Percent
ps: <u>{translate('/s')}</u>, // per second ps: <u>{translate('/s')}</u>, // per second
pm: <u>{translate('/min')}</u>, // per minute pm: <u>{translate('/min')}</u>, // per minute
s: <u>{translate('secs')}</u>, // Seconds s: <u>{translate('secs')}</u>, // Seconds
@@ -96,6 +91,5 @@ export const Languages = {
fr: 'Français', fr: 'Français',
ru: 'ру́сский', ru: 'ру́сский',
pl: 'polski', pl: 'polski',
pt: 'português', pt: 'português'
cn: '中文'
}; };

View File

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -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",

View File

@@ -20,11 +20,11 @@
"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_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": "Щиты будут заряжены за",
@@ -43,12 +43,12 @@
"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": "Реальный поддерживаемый ДПС пока аккумулятор ОРУЖ не пуст",
@@ -56,7 +56,7 @@
"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_NONFUNCTIONAL": "Распределитель питания не может обеспечить достаточно энергии для форсажа", "TT_SUMMARY_BOOST_NONFUNCTIONAL": "Распределитель питания не может обеспечить достаточно энергии для форсажа",
"TT_SUMMARY_SHIELDS": "Чистая сила щита, включая усилители", "TT_SUMMARY_SHIELDS": "Чистая сила щита, включая усилители",
@@ -68,16 +68,16 @@
"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": "Ткните на номер чтобы ввести новое значение, или потяните вдоль полосы для малых изменений",
"am": "Блок Автом. Полевого Ремонта", "am": "Блок Автом. Полевого Ремонта",
"bh": "Переборки", "bh": "Переборки",
"bl": "Пучковый лазер", "bl": "Пучковый Лазер",
"bsg": "Двухпоточный щитогенератор", "bsg": "Двухпоточный Щитогенератор",
"c": "Орудие", "c": "Орудие",
"cc": "Контроллер магнитного снаряда для сбора", "cc": "Контроллер магнитного снаряда для сбора",
"ch": "Разбрасыватель дипольных отражателей", "ch": "Разбрасыватель дипольных отражателей",
@@ -89,7 +89,7 @@
"fh": "Ангар для истребителя", "fh": "Ангар для истребителя",
"fi": "FSD-перехватчик", "fi": "FSD-перехватчик",
"fs": "Топливозаборник", "fs": "Топливозаборник",
"fsd": "Рамочно-сместительный двигатель", "fsd": "Рамочно Сместительный двигатель",
"ft": "Топливный бак", "ft": "Топливный бак",
"fx": "Контроллер магнитного снаряда для топлива", "fx": "Контроллер магнитного снаряда для топлива",
"hb": "Контроллер магнитного снаряда для взлома трюма", "hb": "Контроллер магнитного снаряда для взлома трюма",
@@ -110,7 +110,7 @@
"pcm": "Каюта пассажира первого класса", "pcm": "Каюта пассажира первого класса",
"pcq": "Каюта пассажира класса люкс", "pcq": "Каюта пассажира класса люкс",
"pd": "Распределитель питания", "pd": "Распределитель питания",
"pl": мпульсный лазер", "pl": "Ипмульсный лазер",
"po": "Точечная оборона", "po": "Точечная оборона",
"pp": "Силовая установка", "pp": "Силовая установка",
"psg": "Призматический щитогенератор", "psg": "Призматический щитогенератор",
@@ -122,7 +122,7 @@
"sc": "Сканер обнаружения", "sc": "Сканер обнаружения",
"scb": "Щитонакопитель", "scb": "Щитонакопитель",
"sg": "Щитогенератор", "sg": "Щитогенератор",
"ss": "Сканер поверхностей", "ss": "Сканер Поверхностей",
"t": "Маневровые двигатели", "t": "Маневровые двигатели",
"tp": "Торпедная стойка", "tp": "Торпедная стойка",
"ul": "Пульсирующие лазеры", "ul": "Пульсирующие лазеры",
@@ -130,7 +130,7 @@
"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": "Припасы",
@@ -149,7 +149,7 @@
"eps": "Энергия в секунду", "eps": "Энергия в секунду",
"epsseps": "Энергия в секунду (поддерживаемая энергия в секунду)", "epsseps": "Энергия в секунду (поддерживаемая энергия в секунду)",
"hps": "Нагрев в секунду", "hps": "Нагрев в секунду",
"hpsshps": "Нагрев в секунду (поддерживаемый нагрев в секунду)", "hpsshps": "Heat per second (sustained heat per second)",
"damage by": "Урон", "damage by": "Урон",
"damage from": "Урон от", "damage from": "Урон от",
"shield cells": "Щитонакопители", "shield cells": "Щитонакопители",
@@ -183,7 +183,7 @@
"hullreinforcement": "Укрепление корпуса", "hullreinforcement": "Укрепление корпуса",
"integrity": "Целостность", "integrity": "Целостность",
"jitter": "Дрожание", "jitter": "Дрожание",
"kinres": "Сопротивление кинетическому урону", "kinres": "Сопротивление китетическому урону",
"maxfuel": "Макс. топлива на прыжок", "maxfuel": "Макс. топлива на прыжок",
"mass": "Масса", "mass": "Масса",
"optmass": "Оптимизированная масса", "optmass": "Оптимизированная масса",
@@ -299,7 +299,7 @@
"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": "внутренние отсеки",

View File

@@ -1,4 +1,4 @@
import '@babel/polyfill'; import 'babel-polyfill';
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import '../less/app.less'; import '../less/app.less';

View File

@@ -6,6 +6,7 @@ import { CoriolisLogo, GitHub } from '../components/SvgIcons';
* About Page * About Page
*/ */
export default class AboutPage extends Page { export default class AboutPage extends Page {
/** /**
* Constructor * Constructor
* @param {Object} props React Component properties * @param {Object} props React Component properties
@@ -22,112 +23,27 @@ export default class AboutPage extends Page {
* @return {React.Component} The page contents * @return {React.Component} The page contents
*/ */
renderPage() { renderPage() {
return ( return <div className={'page'} style={{ textAlign: 'left', maxWidth: 800, margin: '0 auto' }}>
<div <h1><CoriolisLogo style={{ marginRight: '0.4em' }} className='xl'/><span className='warning'>Coriolis EDCD Edition</span></h1>
className={'page'}
style={{ textAlign: 'left', maxWidth: 800, margin: '0 auto' }}
>
<h1>
<CoriolisLogo style={{ marginRight: '0.4em' }} className="xl" />
<span className="warning">Coriolis EDCD Edition</span>
</h1>
<p> <p>This is a clone of the Coriolis project, whose original author is currently unable to maintain it. This clone is maintained by the <a href="http://edcd.github.io/">EDCD community</a>.</p>
This is a clone of the Coriolis project, whose original author is <p>To recover your builds, go to <a href='https://coriolis.io/' target='_blank'>https://coriolis.io/</a>, backup your builds (Settings / Backup), copy the text, return here and import (Settings / Import).</p>
currently unable to maintain it. This clone is maintained by the{' '} <p>The Coriolis project was inspired by <a href='http://www.edshipyard.com/' target='_blank'>E:D Shipyard</a> and, of course, <a href='http://www.elitedangerous.com' target='_blank'>Elite Dangerous</a>. The ultimate goal of Coriolis is to provide rich features to support in-game play and planning while engaging the E:D community to support its development.</p>
<a href="http://edcd.github.io/">EDCD community</a>. <p>Coriolis was created using assets and imagery from Elite: Dangerous, with the permission of Frontier Developments plc, for non-commercial purposes. It is not endorsed by nor reflects the views or opinions of Frontier Developments. A number of assets were sourced from <a href='http://edassets.org' target='_blank'>ED Assets</a></p>
</p>
<p>
To recover your builds, go to{' '}
<a href="https://coriolis.io/" target="_blank">
https://coriolis.io/
</a>
, backup your builds (Settings / Backup), copy the text, return here
and import (Settings / Import).
</p>
<p>
The Coriolis project was inspired by{' '}
<a href="http://www.edshipyard.com/" target="_blank">
E:D Shipyard
</a>{' '}
and, of course,{' '}
<a href="http://www.elitedangerous.com" target="_blank">
Elite Dangerous
</a>
. The ultimate goal of Coriolis is to provide rich features to support
in-game play and planning while engaging the E:D community to support
its development.
</p>
<p>
Coriolis was created using assets and imagery from Elite: Dangerous,
with the permission of Frontier Developments plc, for non-commercial
purposes. It is not endorsed by nor reflects the views or opinions of
Frontier Developments. A number of assets were sourced from{' '}
<a href="http://edassets.org" target="_blank">
ED Assets
</a>
</p>
<a <a style={{ display: 'block', textDecoration: 'none' }} href='https://github.com/EDCD/coriolis' target='_blank' title='Coriolis Github Project'>
style={{ display: 'block', textDecoration: 'none' }} <GitHub style={{ margin: '0.4em' }} className='l fg xl'/>
href="https://github.com/EDCD/coriolis" <h2 style={{ margin: 0, textDecoration: 'none' }}>Github</h2>
target="_blank" github.com/EDCD/coriolis
title="Coriolis Github Project" </a>
>
<GitHub style={{ margin: '0.4em' }} className="l fg xl" />
<h2 style={{ margin: 0, textDecoration: 'none' }}>Github</h2>
github.com/EDCD/coriolis
</a>
<p> <p>Coriolis is an open source project. Checkout the list of upcoming features and to-do list on github. Any and all contributions and feedback are welcome. If you encounter any bugs please report them and provide as much detail as possible.</p>
Coriolis is an open source project. Checkout the list of upcoming
features and to-do list on github. Any and all contributions and
feedback are welcome. If you encounter any bugs please report them and
provide as much detail as possible.
</p>
<h3>Chat</h3> <h3>Chat</h3>
<p> <p>You can chat to us on our <a href='https://discord.gg/0uwCh6R62aPRjk9w' target='_blank'>EDCD Discord server</a>.</p>
You can chat to us on our{' '}
<a href="https://discord.gg/0uwCh6R62aPRjk9w" target="_blank">
EDCD Discord server
</a>
.
</p>
<h3>Supporting Coriolis</h3> <h3>Supporting Coriolis</h3>
<p> <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>
Coriolis is an open source project, and I work on it in my free time. </div>;
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>
);
} }
} }

View File

@@ -13,14 +13,7 @@ import ModalCompare from '../components/ModalCompare';
import ModalExport from '../components/ModalExport'; import ModalExport from '../components/ModalExport';
import ModalPermalink from '../components/ModalPermalink'; import ModalPermalink from '../components/ModalPermalink';
import ModalImport from '../components/ModalImport'; import ModalImport from '../components/ModalImport';
import { import { FloppyDisk, Bin, Download, Embed, Rocket, LinkIcon } from '../components/SvgIcons';
FloppyDisk,
Bin,
Download,
Embed,
Rocket,
LinkIcon
} from '../components/SvgIcons';
import ShortenUrl from '../utils/ShortenUrl'; import ShortenUrl from '../utils/ShortenUrl';
import { comparisonBBCode } from '../utils/BBCode'; import { comparisonBBCode } from '../utils/BBCode';
const browser = require('detect-browser'); const browser = require('detect-browser');
@@ -49,6 +42,7 @@ function sortBy(predicate) {
* Comparison Page * Comparison Page
*/ */
export default class ComparisonPage extends Page { export default class ComparisonPage extends Page {
/** /**
* Constructor * Constructor
* @param {Object} props React Component properties * @param {Object} props React Component properties
@@ -87,13 +81,7 @@ export default class ComparisonPage extends Page {
for (let shipId in allBuilds) { for (let shipId in allBuilds) {
for (let buildName in allBuilds[shipId]) { for (let buildName in allBuilds[shipId]) {
if (buildName && allBuilds[shipId][buildName]) { if (buildName && allBuilds[shipId][buildName]) {
builds.push( builds.push(this._createBuild(shipId, buildName, allBuilds[shipId][buildName]));
this._createBuild(
shipId,
buildName,
allBuilds[shipId][buildName]
)
);
} }
} }
} }
@@ -101,9 +89,7 @@ export default class ComparisonPage extends Page {
let comparisonData = Persist.getComparison(name); let comparisonData = Persist.getComparison(name);
if (comparisonData) { if (comparisonData) {
defaultFacets = comparisonData.facets; defaultFacets = comparisonData.facets;
comparisonData.builds.forEach(b => comparisonData.builds.forEach((b) => builds.push(this._createBuild(b.shipId, b.buildName)));
builds.push(this._createBuild(b.shipId, b.buildName))
);
saved = true; saved = true;
newName = name; newName = name;
} }
@@ -115,7 +101,7 @@ export default class ComparisonPage extends Page {
newName = name = comparisonData.n; newName = name = comparisonData.n;
predicate = comparisonData.p; predicate = comparisonData.p;
desc = comparisonData.d; desc = comparisonData.d;
comparisonData.b.forEach(build => { comparisonData.b.forEach((build) => {
builds.push(this._createBuild(build.s, build.n, build.c)); builds.push(this._createBuild(build.s, build.n, build.c));
if (!importObj[build.s]) { if (!importObj[build.s]) {
importObj[build.s] = {}; importObj[build.s] = {};
@@ -132,9 +118,9 @@ export default class ComparisonPage extends Page {
let selectedFacets = new Array(selectedLength); let selectedFacets = new Array(selectedLength);
for (let i = 0; i < ShipFacets.length; i++) { for (let i = 0; i < ShipFacets.length; i++) {
let facet = Object.assign({}, ShipFacets[i]); let facet = Object.assign({ }, ShipFacets[i]);
let defaultIndex = defaultFacets.indexOf(facet.i); let defaultIndex = defaultFacets.indexOf(facet.i);
if (defaultIndex == -1) { if(defaultIndex == -1) {
facets.push(facet); facets.push(facet);
} else { } else {
facet.active = true; facet.active = true;
@@ -169,18 +155,17 @@ export default class ComparisonPage extends Page {
_createBuild(id, name, code) { _createBuild(id, name, code) {
code = code ? code : Persist.getBuild(id, name); // Retrieve build code if not passed code = code ? code : Persist.getBuild(id, name); // Retrieve build code if not passed
if (!code) { if (!code) { // No build found
// No build found
return; return;
} }
let data = Ships[id]; // Get ship properties let data = Ships[id]; // Get ship properties
let b = new Ship(id, data.properties, data.slots); // Create a new Ship instance let b = new Ship(id, data.properties, data.slots); // Create a new Ship instance
b.buildFrom(code); // Populate components from code b.buildFrom(code); // Populate components from code
b.buildName = name; b.buildName = name;
b.applyDiscounts(Persist.getShipDiscount(), Persist.getModuleDiscount()); b.applyDiscounts(Persist.getShipDiscount(), Persist.getModuleDiscount());
return b; return b;
} };
/** /**
* Update state with the specified sort predicates * Update state with the specified sort predicates
@@ -199,18 +184,13 @@ export default class ComparisonPage extends Page {
} }
this.setState({ predicate, desc }); this.setState({ predicate, desc });
} };
/** /**
* Show selected builds modal * Show selected builds modal
*/ */
_selectBuilds() { _selectBuilds() {
this.context.showModal( this.context.showModal(<ModalCompare onSelect={this._buildsSelected} builds={this.state.builds} />);
<ModalCompare
onSelect={this._buildsSelected}
builds={this.state.builds}
/>
);
} }
/** /**
@@ -244,7 +224,7 @@ export default class ComparisonPage extends Page {
_facetDrag(e) { _facetDrag(e) {
this.nodeAfter = false; this.nodeAfter = false;
this.dragged = e.currentTarget; this.dragged = e.currentTarget;
let placeholder = (this.placeholder = document.createElement('li')); let placeholder = this.placeholder = document.createElement('li');
placeholder.style.width = Math.round(this.dragged.offsetWidth) + 'px'; placeholder.style.width = Math.round(this.dragged.offsetWidth) + 'px';
placeholder.className = 'facet-placeholder'; placeholder.className = 'facet-placeholder';
if (!browser || (browser.name !== 'edge' && browser.name !== 'ie')) { if (!browser || (browser.name !== 'edge' && browser.name !== 'ie')) {
@@ -282,7 +262,7 @@ export default class ComparisonPage extends Page {
_facetDragOver(e) { _facetDragOver(e) {
e.preventDefault(); e.preventDefault();
if (e.target.className == 'facet-placeholder') { if(e.target.className == 'facet-placeholder') {
return; return;
} else if (e.target != e.currentTarget) { } else if (e.target != e.currentTarget) {
this.over = e.target; this.over = e.target;
@@ -292,7 +272,7 @@ export default class ComparisonPage extends Page {
let parent = e.target.parentNode; let parent = e.target.parentNode;
if (parent == e.currentTarget) { if (parent == e.currentTarget) {
if (relX > width && this.dragged != e.target) { if(relX > width && this.dragged != e.target) {
this.nodeAfter = true; this.nodeAfter = true;
parent.insertBefore(this.placeholder, e.target.nextElementSibling); parent.insertBefore(this.placeholder, e.target.nextElementSibling);
} else { } else {
@@ -341,7 +321,7 @@ export default class ComparisonPage extends Page {
let { newName, builds, facets } = this.state; let { newName, builds, facets } = this.state;
let selectedFacets = []; let selectedFacets = [];
facets.forEach(f => { facets.forEach((f) => {
if (f.active) { if (f.active) {
selectedFacets.unshift(f.i); selectedFacets.unshift(f.i);
} }
@@ -368,20 +348,14 @@ export default class ComparisonPage extends Page {
let code = fromComparison(name, builds, selectedFacets, predicate, desc); let code = fromComparison(name, builds, selectedFacets, predicate, desc);
let loc = window.location; let loc = window.location;
return ( return loc.protocol + '//' + loc.host + '/comparison?code=' + encodeURIComponent(code);
loc.protocol +
'//' +
loc.host +
'/comparison?code=' +
encodeURIComponent(code)
);
} }
/** /**
* Generates the long permalink URL * Generates the long permalink URL
*/ */
_genPermalink() { _genPermalink() {
this.context.showModal(<ModalPermalink url={this._buildUrl()} />); this.context.showModal(<ModalPermalink url={this._buildUrl()}/>);
} }
/** /**
@@ -391,25 +365,18 @@ export default class ComparisonPage extends Page {
let { translate, formats } = this.context.language; let { translate, formats } = this.context.language;
let { facets, builds } = this.state; let { facets, builds } = this.state;
let generator = callback => { let generator = (callback) => {
let url = this._buildUrl(); let url = this._buildUrl();
ShortenUrl( ShortenUrl(url,
url, (shortenedUrl) => callback(comparisonBBCode(translate, formats, facets, builds, shortenedUrl)),
shortenedUrl => (error) => callback(comparisonBBCode(translate, formats, facets, builds, url))
callback(
comparisonBBCode(translate, formats, facets, builds, shortenedUrl)
),
error =>
callback(comparisonBBCode(translate, formats, facets, builds, url))
); );
}; };
this.context.showModal( this.context.showModal(<ModalExport
<ModalExport title={translate('forum') + ' BBCode'}
title={translate('forum') + ' BBCode'} generator={generator}
generator={generator} />);
/>
);
} }
/** /**
@@ -442,8 +409,7 @@ export default class ComparisonPage extends Page {
* @param {Object} nextContext Incoming/Next conext * @param {Object} nextContext Incoming/Next conext
*/ */
componentWillReceiveProps(nextProps, nextContext) { componentWillReceiveProps(nextProps, nextContext) {
if (this.context.route !== nextContext.route) { if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
// Only reinit state if the route has changed
this.setState(this._initState(nextContext)); this.setState(this._initState(nextContext));
} }
} }
@@ -453,10 +419,7 @@ export default class ComparisonPage extends Page {
*/ */
componentWillMount() { componentWillMount() {
this.resizeListener = this.context.onWindowResize(this._updateDimensions); this.resizeListener = this.context.onWindowResize(this._updateDimensions);
this.persistListener = Persist.addListener( this.persistListener = Persist.addListener('discounts', this._updateDiscounts);
'discounts',
this._updateDiscounts
);
} }
/** /**
@@ -481,132 +444,65 @@ export default class ComparisonPage extends Page {
renderPage() { renderPage() {
let translate = this.context.language.translate; let translate = this.context.language.translate;
let compareHeader; let compareHeader;
let { let { newName, name, saved, builds, facets, predicate, desc, chartWidth } = this.state;
newName,
name,
saved,
builds,
facets,
predicate,
desc,
chartWidth
} = this.state;
if (this.state.compareMode) { if (this.state.compareMode) {
compareHeader = ( compareHeader = <tr>
<tr> <td className='head'>{translate('comparison')}</td>
<td className="head">{translate('comparison')}</td> <td>
<td> <input value={newName} onChange={this._onNameChange} placeholder={translate('Enter Name')} maxLength='50' />
<input <button onClick={this._save} disabled={!newName || newName == 'all' || saved}>
value={newName} <FloppyDisk className='lg'/><span className='button-lbl'>{translate('save')}</span>
onChange={this._onNameChange} </button>
placeholder={translate('Enter Name')} <button onClick={this._delete} disabled={name == 'all' || !saved}><Bin className='lg warning'/></button>
maxLength="50" <button onClick={this._selectBuilds}>
/> <Rocket className='lg'/><span className='button-lbl'>{translate('builds')}</span>
<button </button>
onClick={this._save} <button className='r' onClick={this._genPermalink} disabled={builds.length == 0}>
disabled={!newName || newName == 'all' || saved} <LinkIcon className='lg'/><span className='button-lbl'>{translate('permalink')}</span>
> </button>
<FloppyDisk className="lg" /> <button className='r' onClick={this._genBBcode} disabled={builds.length == 0}>
<span className="button-lbl">{translate('save')}</span> <Embed className='lg'/><span className='button-lbl'>{translate('forum')}</span>
</button> </button>
<button onClick={this._delete} disabled={name == 'all' || !saved}> </td>
<Bin className="lg warning" /> </tr>;
</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 { } else {
compareHeader = ( compareHeader = <tr>
<tr> <td className='head'>{translate('comparison')}</td>
<td className="head">{translate('comparison')}</td> <td>
<td> <h3>{name}</h3>
<h3>{name}</h3> <button className='r' onClick={this._import}><Download className='lg'/>{translate('import')}</button>
<button className="r" onClick={this._import}> </td>
<Download className="lg" /> </tr>;
{translate('import')}
</button>
</td>
</tr>
);
} }
return ( return (
<div <div className={'page'} style={{ fontSize: this.context.sizeRatio + 'em' }}>
className={'page'} <table id='comparison'>
style={{ fontSize: this.context.sizeRatio + 'em' }}
>
<table id="comparison">
<tbody> <tbody>
{compareHeader} {compareHeader}
<tr key="facets"> <tr key='facets'>
<td className="head">{translate('compare')}</td> <td className='head'>{translate('compare')}</td>
<td> <td>
<ul id="facet-container" onDragOver={this._facetDragOver}> <ul id='facet-container' onDragOver={this._facetDragOver}>
{facets.map((f, i) => ( {facets.map((f, i) =>
<li <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)}>
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)} {'↔ ' + translate(f.title)}
</li> </li>
))} )}
</ul> </ul>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<ComparisonTable <ComparisonTable builds={builds} facets={facets} onSort={this._sortShips} predicate={predicate} desc={desc} />
builds={builds}
facets={facets}
onSort={this._sortShips}
predicate={predicate}
desc={desc}
/>
{!builds.length ? ( {!builds.length ?
<div className="chart" ref={node => (this.chartRef = node)}> <div className='chart' ref={node => this.chartRef = node}>{translate('PHRASE_NO_BUILDS')}</div> :
{translate('PHRASE_NO_BUILDS')} facets.filter((f) => f.active).map((f, i) =>
</div> <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>
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 <BarChart
width={chartWidth} width={chartWidth}
data={builds} data={builds}
@@ -619,8 +515,8 @@ export default class ComparisonPage extends Page {
desc={desc} desc={desc}
/> />
</div> </div>
))
)} )}
</div> </div>
); );
} }

View File

@@ -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
@@ -45,7 +46,7 @@ export default class ErrorDetails extends React.Component {
<h1>Jameson, we have a problem..</h1> <h1>Jameson, we have a problem..</h1>
<h1><small>{error.message}</small></h1> <h1><small>{error.message}</small></h1>
<br/> <br/>
{importerror ? <div>If you are attempting to import a ship from EDDI or EDMC and are seeing a 'Z_BUF_ERROR' it means that the URL has not been provided correctly. This is a common problem when using Microsoft Internet Explorer or Microsoft Edge, and you should use another browser instead.</div> : null } {importerror ? <div>If you are attempting to import a ship from EDDI or EDMC and are seeing a 'Z_BUF_ERROR' it means that the URL has not been provided correctly. This is a common problem when using Microsoft Internet Explorer or Microsoft Edge, and you should use another browser instead.</div> : null }
<br/> <br/>
<div>Please note that this site uses Google Analytics to track performance and usage. If you are blocking cookies, for example using Ghostery, please disable blocking for this site and try again.</div> <div>Please note that this site uses Google Analytics to track performance and usage. If you are blocking cookies, for example using Ghostery, please disable blocking for this site and try again.</div>
<br/> <br/>

View File

@@ -5,6 +5,7 @@ import Page from './Page';
* 404 Page * 404 Page
*/ */
export default class NotFoundPage extends Page { export default class NotFoundPage extends Page {
/** /**
* Constructor * Constructor
* @param {Object} props React Component properties * @param {Object} props React Component properties
@@ -21,10 +22,6 @@ export default class NotFoundPage extends Page {
* @return {React.Component} The page contents * @return {React.Component} The page contents
*/ */
renderPage() { renderPage() {
return ( return <div className='page' style={{ marginTop: 30 }}>Page <small>{this.context.route.path}</small> Not Found</div>;
<div className="page" style={{ marginTop: 30 }}>
Page <small>{this.context.route.path}</small> Not Found
</div>
);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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,
@@ -50,16 +51,22 @@ export default class Page extends React.Component {
} }
/** /**
* Update the window title upon mount * 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
*/ */
componentWillMount() { shouldComponentUpdate(np, ns, nc) {
document.title = this.state.title || 'Coriolis'; 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
*/ */
componentDidMount() { componentWillMount() {
document.title = this.state.title || 'Coriolis'; document.title = this.state.title || 'Coriolis';
} }
@@ -83,4 +90,5 @@ export default class Page extends React.Component {
} }
return this.renderPage(); return this.renderPage();
} }
} }

View File

@@ -1,81 +1,94 @@
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/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 { jumpRange, 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'), 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
jumpRange, standard: shipData.slots.standard,
pitch: ship.readProp('pitch'), agility: shipData.properties.pitch + shipData.properties.yaw + shipData.properties.roll
retailCost: ship.readMeta('retailCost'),
roll: ship.readProp('roll'),
speed: ship.readProp('speed'),
standard: [
'powerplant',
'mainengines',
'frameshiftdrive',
'lifesupport',
'powerdistributor',
'radar',
'fueltank'
].map(k => coreSizes[k]),
totalRange,
yaw: ship.readProp('yaw'),
}; };
Object.assign(summary, shipData.properties);
let ship = new Ship(shipId, shipData.properties, shipData.slots);
// Count Hardpoints by class // Build Ship
ship.getHardpoints(undefined, true).forEach(hardpoint => { ship.buildWith(shipData.defaults); // Populate with stock/default components
summary.hp[hardpoint.getSize()]++; 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.getSize());
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;
} }
@@ -84,6 +97,7 @@ function shipSummary(shipId) {
* The Shipyard summary page * The Shipyard summary page
*/ */
export default class ShipyardPage extends Page { export default class ShipyardPage extends Page {
static cachedShipSummaries = null; static cachedShipSummaries = null;
/** /**
@@ -96,23 +110,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 +133,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
@@ -151,15 +145,12 @@ export default class ShipyardPage extends Page {
shipPredicateIndex = undefined; shipPredicateIndex = undefined;
} }
if ( if (this.state.shipPredicate == shipPredicate && this.state.shipPredicateIndex == shipPredicateIndex) {
this.state.shipPredicate == shipPredicate &&
this.state.shipPredicateIndex == shipPredicateIndex
) {
shipDesc = !shipDesc; shipDesc = !shipDesc;
} }
this.setState({ shipPredicate, shipDesc, shipPredicateIndex }); this.setState({ shipPredicate, shipDesc, shipPredicateIndex });
} };
/** /**
* Generate the table row summary for the ship * Generate the table row summary for the ship
@@ -168,62 +159,56 @@ export default class ShipyardPage extends Page {
* @param {Object} u Localized unit map * @param {Object} u Localized unit map
* @param {Function} fInt Localized integer formatter * @param {Function} fInt Localized integer formatter
* @param {Function} fRound Localized round formatter * @param {Function} fRound Localized round formatter
* @param {Boolean} highlight Should this row be highlighted
* @return {React.Component} Table Row * @return {React.Component} Table Row
*/ */
_shipRowElement(s, translate, u, fInt, fRound) { _shipRowElement(s, translate, u, fInt, fRound, highlight) {
let noTouch = this.context.noTouch; let noTouch = this.context.noTouch;
return ( return <tr
<tr
key={s.id} key={s.id}
style={{ height: '1.5em' }} style={{ height: '1.5em' }}
className={cn({ className={cn({ highlighted: noTouch && this.state.shipId === s.id, alt: highlight })}
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">{fInt(s.retailCost)}</td> <td className='ri'>{s.manufacturer}</td>
<td className="ri cap">{translate(SizeMap[s.class])}</td> <td className='ri'>{fInt(s.retailCost)}</td>
<td className="ri">{fInt(s.crew)}</td> <td className='ri cap'>{translate(SizeMap[s.class])}</td>
<td className="ri">{s.masslock}</td> <td className='ri'>{fInt(s.crew)}</td>
<td className="ri">{fInt(s.hullMass)}</td> <td className='ri'>{s.masslock}</td>
<td className="ri">{fInt(s.agility)}</td> <td className='ri'>{fInt(s.agility)}</td>
<td className="ri">{fInt(s.speed)}</td> <td className='ri'>{fInt(s.hardness)}</td>
<td className="ri">{fInt(s.boost)}</td> <td className='ri'>{fInt(s.hullMass)}</td>
<td className="ri">{fInt(s.pitch)}</td> <td className='ri'>{fInt(s.speed)}</td>
<td className="ri">{fInt(s.yaw)}</td> <td className='ri'>{fInt(s.boost)}</td>
<td className="ri">{fInt(s.roll)}</td> <td className='ri'>{fInt(s.baseArmour)}</td>
<td className="ri">{fRound(s.jumpRange)}</td> <td className='ri'>{fInt(s.baseShieldStrength)}</td>
<td className="ri">{fRound(s.totalRange)}</td> <td className='ri'>{fInt(s.topSpeed)}</td>
<td className="ri">{fInt(s.hardness)}</td> <td className='ri'>{fInt(s.topBoost)}</td>
<td className="ri">{fInt(s.baseArmour)}</td> <td className='ri'>{fRound(s.maxJumpRange)}</td>
<td className="ri">{fInt(s.baseShieldStrength)}</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> <td className='cn'>{s.standard[1]}</td>
<td className="cn">{s.standard[1]}</td> <td className='cn'>{s.standard[2]}</td>
<td className="cn">{s.standard[2]}</td> <td className='cn'>{s.standard[3]}</td>
<td className="cn">{s.standard[3]}</td> <td className='cn'>{s.standard[4]}</td>
<td className="cn">{s.standard[4]}</td> <td className='cn'>{s.standard[5]}</td>
<td className="cn">{s.standard[5]}</td> <td className='cn'>{s.standard[6]}</td>
<td className="cn">{s.standard[6]}</td> <td className={cn({ disabled: !s.hp[1] })}>{s.hp[1]}</td>
<td className={cn({ disabled: !s.hp[1] })}>{s.hp[1]}</td> <td className={cn({ disabled: !s.hp[2] })}>{s.hp[2]}</td>
<td className={cn({ disabled: !s.hp[2] })}>{s.hp[2]}</td> <td className={cn({ disabled: !s.hp[3] })}>{s.hp[3]}</td>
<td className={cn({ disabled: !s.hp[3] })}>{s.hp[3]}</td> <td className={cn({ disabled: !s.hp[4] })}>{s.hp[4]}</td>
<td className={cn({ disabled: !s.hp[4] })}>{s.hp[4]}</td> <td className={cn({ disabled: !s.hp[0] })}>{s.hp[0]}</td>
<td className={cn({ disabled: !s.hp[0] })}>{s.hp[0]}</td> <td className={cn({ disabled: !s.int[0] })}>{s.int[0]}</td>
<td className={cn({ disabled: !s.int[0] })}>{s.int[0]}</td> <td className={cn({ disabled: !s.int[1] })}>{s.int[1]}</td>
<td className={cn({ disabled: !s.int[1] })}>{s.int[1]}</td> <td className={cn({ disabled: !s.int[2] })}>{s.int[2]}</td>
<td className={cn({ disabled: !s.int[2] })}>{s.int[2]}</td> <td className={cn({ disabled: !s.int[3] })}>{s.int[3]}</td>
<td className={cn({ disabled: !s.int[3] })}>{s.int[3]}</td> <td className={cn({ disabled: !s.int[4] })}>{s.int[4]}</td>
<td className={cn({ disabled: !s.int[4] })}>{s.int[4]}</td> <td className={cn({ disabled: !s.int[5] })}>{s.int[5]}</td>
<td className={cn({ disabled: !s.int[5] })}>{s.int[5]}</td> <td className={cn({ disabled: !s.int[6] })}>{s.int[6]}</td>
<td className={cn({ disabled: !s.int[6] })}>{s.int[6]}</td> <td className={cn({ disabled: !s.int[7] })}>{s.int[7]}</td>
<td className={cn({ disabled: !s.int[7] })}>{s.int[7]}</td> </tr>;
</tr>
);
} }
/** /**
@@ -235,9 +220,9 @@ 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 sortShips = (predicate, index) => let { shipSummaries, shipPredicate, shipPredicateIndex } = this.state;
this._sortShips.bind(this, predicate, index); let sortShips = (predicate, index) => this._sortShips.bind(this, predicate, index);
let filters = { let filters = {
// 'class': { 1: 1, 2: 1} // 'class': { 1: 1, 2: 1}
@@ -254,8 +239,7 @@ export default class ShipyardPage extends Page {
// Sort shipsOverview // Sort shipsOverview
shipSummaries.sort((a, b) => { shipSummaries.sort((a, b) => {
let valA = a[shipPredicate], let valA = a[shipPredicate], valB = b[shipPredicate];
valB = b[shipPredicate];
if (shipPredicateIndex != undefined) { if (shipPredicateIndex != undefined) {
valA = valA[shipPredicateIndex]; valA = valA[shipPredicateIndex];
@@ -268,17 +252,8 @@ export default class ShipyardPage extends Page {
valB = val; valB = val;
} }
if (groupCompared) { if(valA == valB) {
if (compare[a.id] && !compare[b.id]) { if (a.name > b.name) {
return -1;
}
if (!compare[a.id] && compare[b.id]) {
return 1;
}
}
if (valA == valB) {
if (a.id > b.id) {
return 1; return 1;
} else { } else {
return -1; return -1;
@@ -294,276 +269,128 @@ export default class ShipyardPage extends Page {
let shipRows = new Array(shipSummaries.length); let shipRows = new Array(shipSummaries.length);
let detailRows = new Array(shipSummaries.length); let detailRows = new Array(shipSummaries.length);
let lastShipSortValue = null;
let backgroundHighlight = false;
for (let s of shipSummaries) { for (let s of shipSummaries) {
detailRows[i] = this._shipRowElement( let shipSortValue = s[shipPredicate];
s, if( shipPredicateIndex != undefined ) {
translate, shipSortValue = shipSortValue[shipPredicateIndex];
units, }
fInt,
formats.f1, if( shipSortValue != lastShipSortValue ) {
); backgroundHighlight = !backgroundHighlight;
lastShipSortValue = shipSortValue;
}
detailRows[i] = this._shipRowElement(s, translate, units, fInt, formats.f1, backgroundHighlight);
shipRows[i] = ( shipRows[i] = (
<tr <tr
key={i} key={i}
style={{ height: '1.5em' }} style={{ height: '1.5em' }}
className={cn({ className={cn({ highlighted: noTouch && this.state.shipId === s.id, alt: backgroundHighlight })}
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.name}</Link></td>
<Link href={'/outfit/' + s.id}>{s.id} {s.beta === true ? '(Beta)' : null}</Link>
</td>
</tr> </tr>
); );
i++; i++;
} }
return ( return (
<div className="page" style={{ fontSize: sizeRatio + 'em' }}> <div className='page' style={{ fontSize: sizeRatio + 'em' }}>
<div className="content-wrapper"> <div style={{ whiteSpace: 'nowrap', margin: '0 auto', fontSize: '0.8em', position: 'relative', display: 'inline-block', maxWidth: '100%' }}>
<div className="shipyard-table-wrapper"> <table style={{ width: '12em', position: 'absolute', zIndex: 1 }}>
<table style={{ width: '12em', position: 'absolute', zIndex: 1 }} className="shipyard-table"> <thead>
<thead> <tr>
<tr> <th className='le rgt'>&nbsp;</th>
<th className="le rgt">&nbsp;</th> </tr>
</tr> <tr className='main'>
<tr className="main"> <th className='sortable le rgt' onClick={sortShips('name')}>{translate('ship')}</th>
<th className="sortable le rgt" onClick={sortShips('id')}> </tr>
{translate('ship')} <tr>
</th> <th className='le rgt invisible'>{units['m/s']}</th>
</tr> </tr>
<tr> </thead>
<th className="le rgt invisible">{units['m/s']}</th> <tbody onMouseLeave={this._highlightShip.bind(this, null)}>
</tr> {shipRows}
</thead> </tbody>
<tbody onMouseLeave={this._highlightShip.bind(this, null)}> </table>
{shipRows} <div style={{ overflowX: 'scroll', maxWidth: '100%' }}>
</tbody> <table style={{ marginLeft: 'calc(12em - 1px)', zIndex: 0 }}>
</table> <thead>
<div style={{ overflowX: 'auto', maxWidth: '100%' }}> <tr className='main'>
<table style={{ marginLeft: 'calc(12em - 1px)', zIndex: 0 }} className="shipyard-table"> <th rowSpan={3} className='sortable' onClick={sortShips('manufacturer')}>{translate('manufacturer')}</th>
<thead> <th>&nbsp;</th>
<tr className="main"> <th rowSpan={3} className='sortable' onClick={sortShips('class')}>{translate('size')}</th>
{/* First all headers that spread out over three rows */} <th rowSpan={3} className='sortable' onClick={sortShips('crew')}>{translate('crew')}</th>
{/* cost placeholder */} <th rowSpan={3} className='sortable' onMouseEnter={termtip.bind(null, 'mass lock factor')} onMouseLeave={hide} onClick={sortShips('masslock')} >{translate('MLF')}</th>
<th>&nbsp;</th> <th rowSpan={3} className='sortable' onClick={sortShips('agility')}>{translate('agility')}</th>
<th rowSpan={3} className="sortable" onClick={sortShips('class')}> <th rowSpan={3} className='sortable' onMouseEnter={termtip.bind(null, 'hardness')} onMouseLeave={hide} onClick={sortShips('hardness')}>{translate('hrd')}</th>
{translate('size')} <th>&nbsp;</th>
</th> <th colSpan={4}>{translate('base')}</th>
<th rowSpan={3} className="sortable" onClick={sortShips('crew')}> <th colSpan={5}>{translate('max')}</th>
{translate('crew')} <th className='lft' colSpan={7}></th>
</th> <th className='lft' colSpan={5}></th>
<th rowSpan={3} className="sortable" <th className='lft' colSpan={8}></th>
onMouseEnter={termtip.bind(null, 'mass lock factor')} </tr>
onMouseLeave={hide} onClick={sortShips('masslock')}> <tr>
{translate('MLF')} <th className='sortable lft' onClick={sortShips('retailCost')}>{translate('cost')}</th>
</th> <th className='sortable lft' onClick={sortShips('hullMass')}>{translate('hull')}</th>
{/* hull mass placeholder */} <th className='sortable lft' onClick={sortShips('speed')}>{translate('speed')}</th>
<th>&nbsp;</th> <th className='sortable' onClick={sortShips('boost')}>{translate('boost')}</th>
<th colSpan={6}>{translate('agility')}</th> <th className='sortable' onClick={sortShips('baseArmour')}>{translate('armour')}</th>
<th colSpan={2}>{translate('travel')}</th> <th className='sortable' onClick={sortShips('baseShieldStrength')}>{translate('shields')}</th>
<th colSpan={3}>{translate('defence')}</th>
{/* cargo placeholder */}
<th>&nbsp;</th>
{/* pax placeholder */}
<th>&nbsp;</th>
<th className="lft" colSpan={7} />
<th className="lft" colSpan={5} />
<th className="lft" colSpan={8} />
</tr>
<tr>
{/* Now all headers in a second-row */}
<th className="sortable lft" onClick={sortShips('retailCost')}>
{translate('cost')}
</th>
<th className="sortable lft" onClick={sortShips('hullMass')}>
{translate('hull')}
</th>
<th className="sortable lft" onClick={sortShips('agility')}>
{translate('rating')}
</th>
<th className="sortable" onClick={sortShips('speed')}>
{translate('speed')}
</th>
<th className="sortable" onClick={sortShips('boost')}>
{translate('boost')}
</th>
<th className="sortable" onClick={sortShips('pitch')}>
{translate('pitch')}
</th>
<th className="sortable" onClick={sortShips('yaw')}>
{translate('yaw')}
</th>
<th className="sortable" onClick={sortShips('roll')}>
{translate('roll')}
</th>
<th className="sortable lft" onClick={sortShips('jumpRange')}>
{translate('jump')}
</th>
<th className="sortable" onClick={sortShips('totalRange')}>
{translate('range')}
</th>
<th className="sortable lft" onMouseEnter={termtip.bind(null, 'hardness')}
onMouseLeave={hide} onClick={sortShips('hardness')}>
{translate('hrd')}
</th>
<th className="sortable" onClick={sortShips('baseArmour')}>
{translate('armour')}
</th>
<th className="sortable" onClick={sortShips('baseShieldStrength')}>
{translate('shields')}
</th>
<th className="sortable lft" onClick={sortShips('maxCargo')}> <th className='sortable lft' onClick={sortShips('topSpeed')}>{translate('speed')}</th>
{translate('cargo')} <th className='sortable' onClick={sortShips('topBoost')}>{translate('boost')}</th>
</th> <th className='sortable' onClick={sortShips('maxJumpRange')}>{translate('jump')}</th>
<th className="sortable lft" onClick={sortShips('maxPassengers')} <th className='sortable' onClick={sortShips('maxCargo')}>{translate('cargo')}</th>
onMouseEnter={termtip.bind(null, 'passenger capacity')} onMouseLeave={hide}> <th className='sortable' onClick={sortShips('maxPassengers')}>{translate('pax')}</th>
{translate('pax')}
</th>
<th className="lft" colSpan={7}>
{translate('core module classes')}
</th>
<th colSpan={5} className="sortable lft" onClick={sortShips('hpCount')}>
{translate('hardpoints')}
</th>
<th
colSpan={8}
className="sortable lft"
onClick={sortShips('intCount')}
>
{translate('internal compartments')}
</th>
</tr>
<tr>
{/* Third row headers, i.e., units */}
<th
className="sortable lft"
onClick={sortShips('retailCost')}
>
{units.CR}
</th>
<th className="sortable lft" onClick={sortShips('hullMass')}>
{units.T}
</th>
{/* agility rating placeholder */}
<th className="lft">&nbsp;</th>
<th className="sortable" onClick={sortShips('speed')}>
{units['m/s']}
</th>
<th className="sortable" onClick={sortShips('boost')}>
{units['m/s']}
</th>
<th className="sortable" onClick={sortShips('pitch')}>
{units['°/s']}
</th>
<th className="sortable" onClick={sortShips('yaw')}>
{units['°/s']}
</th>
<th className="sortable" onClick={sortShips('roll')}>
{units['°/s']}
</th>
<th className="sortable lft" onClick={sortShips('jumpRange')}>
{units.LY}
</th>
<th className="sortable" onClick={sortShips('totalRange')}>
{units.LY}
</th>
<th className="lft">&nbsp;</th>
{/* armour placeholder */}
<th>&nbsp;</th>
<th
className="sortable"
onClick={sortShips('baseShieldStrength')}
>
{units.MJ}
</th>
<th className="sortable lft" onClick={sortShips('maxCargo')}>
{units.T}
</th>
{/* pax placeholder */}
<th className="lft">&nbsp;</th>
<th className="sortable lft" onMouseEnter={termtip.bind(null, 'power plant')}
onMouseLeave={hide} onClick={sortShips('standard', 0)}>
{'pp'}
</th>
<th className="sortable" onMouseEnter={termtip.bind(null, 'thrusters')}
onMouseLeave={hide} onClick={sortShips('standard', 1)}>
{'th'}
</th>
<th className="sortable" onMouseEnter={termtip.bind(null, 'frame shift drive')}
onMouseLeave={hide} onClick={sortShips('standard', 2)}>
{'fsd'}
</th>
<th className="sortable" onMouseEnter={termtip.bind(null, 'life support')}
onMouseLeave={hide} onClick={sortShips('standard', 3)}>
{'ls'}
</th>
<th className="sortable" onMouseEnter={termtip.bind(null, 'power distriubtor')}
onMouseLeave={hide} onClick={sortShips('standard', 4)}>
{'pd'}
</th>
<th className="sortable" onMouseEnter={termtip.bind(null, 'sensors')}
onMouseLeave={hide} onClick={sortShips('standard', 5)}>
{'s'}
</th>
<th className="sortable" onMouseEnter={termtip.bind(null, 'fuel tank')}
onMouseLeave={hide} onClick={sortShips('standard', 6)}>
{'ft'}
</th>
<th className="sortable lft" onClick={sortShips('hp', 1)}>
{translate('S')}
</th>
<th className="sortable" onClick={sortShips('hp', 2)}>
{translate('M')}
</th>
<th className="sortable" onClick={sortShips('hp', 3)}>
{translate('L')}
</th>
<th className="sortable" onClick={sortShips('hp', 4)}>
{translate('H')}
</th>
<th className="sortable" onClick={sortShips('hp', 0)}>
{translate('U')}
</th>
<th className="sortable lft" onClick={sortShips('int', 0)}> <th className='lft' colSpan={7}>{translate('core module classes')}</th>
1 <th colSpan={5} className='sortable lft' onClick={sortShips('hpCount')}>{translate('hardpoints')}</th>
</th> <th colSpan={8} className='sortable lft' onClick={sortShips('intCount')}>{translate('internal compartments')}</th>
<th className="sortable" onClick={sortShips('int', 1)}> </tr>
2 <tr>
</th> <th className='sortable lft' onClick={sortShips('retailCost')}>{units.CR}</th>
<th className="sortable" onClick={sortShips('int', 2)}> <th className='sortable lft' onClick={sortShips('hullMass')}>{units.T}</th>
3 <th className='sortable lft' onClick={sortShips('speed')}>{units['m/s']}</th>
</th> <th className='sortable' onClick={sortShips('boost')}>{units['m/s']}</th>
<th className="sortable" onClick={sortShips('int', 3)}> <th>&nbsp;</th>
4 <th className='sortable' onClick={sortShips('baseShieldStrength')}>{units.MJ}</th>
</th> <th className='sortable lft' onClick={sortShips('topSpeed')}>{units['m/s']}</th>
<th className="sortable" onClick={sortShips('int', 4)}> <th className='sortable' onClick={sortShips('topBoost')}>{units['m/s']}</th>
5 <th className='sortable' onClick={sortShips('maxJumpRange')}>{units.LY}</th>
</th> <th className='sortable' onClick={sortShips('maxCargo')}>{units.T}</th>
<th className="sortable" onClick={sortShips('int', 5)}> <th>&nbsp;</th>
6 <th className='sortable lft' onMouseEnter={termtip.bind(null, 'power plant')} onMouseLeave={hide} onClick={sortShips('standard', 0)}>{'pp'}</th>
</th> <th className='sortable' onMouseEnter={termtip.bind(null, 'thrusters')} onMouseLeave={hide} onClick={sortShips('standard', 1)}>{'th'}</th>
<th className="sortable" onClick={sortShips('int', 6)}> <th className='sortable' onMouseEnter={termtip.bind(null, 'frame shift drive')} onMouseLeave={hide} onClick={sortShips('standard', 2)}>{'fsd'}</th>
7 <th className='sortable' onMouseEnter={termtip.bind(null, 'life support')} onMouseLeave={hide} onClick={sortShips('standard', 3)}>{'ls'}</th>
</th> <th className='sortable' onMouseEnter={termtip.bind(null, 'power distriubtor')} onMouseLeave={hide} onClick={sortShips('standard', 4)}>{'pd'}</th>
<th className="sortable" onClick={sortShips('int', 7)}> <th className='sortable' onMouseEnter={termtip.bind(null, 'sensors')} onMouseLeave={hide} onClick={sortShips('standard', 5)}>{'s'}</th>
8 <th className='sortable' onMouseEnter={termtip.bind(null, 'fuel tank')} onMouseLeave={hide} onClick={sortShips('standard', 6)}>{'ft'}</th>
</th> <th className='sortable lft' onClick={sortShips('hp',1)}>{translate('S')}</th>
</tr> <th className='sortable' onClick={sortShips('hp', 2)}>{translate('M')}</th>
</thead> <th className='sortable' onClick={sortShips('hp', 3)}>{translate('L')}</th>
<tbody onMouseLeave={this._highlightShip.bind(this, null)}> <th className='sortable' onClick={sortShips('hp', 4)}>{translate('H')}</th>
{detailRows} <th className='sortable' onClick={sortShips('hp', 0)}>{translate('U')}</th>
</tbody>
</table> <th className='sortable lft' onClick={sortShips('int', 0)} >1</th>
</div> <th className='sortable' onClick={sortShips('int', 1)} >2</th>
</div> <th className='sortable' onClick={sortShips('int', 2)} >3</th>
<div className="table-tools" > <th className='sortable' onClick={sortShips('int', 3)} >4</th>
<label><input type="checkbox" checked={this.state.groupCompared} onClick={() => this._toggleGroupCompared()}/>Group highlighted ships</label> <th className='sortable' onClick={sortShips('int', 4)} >5</th>
<th className='sortable' onClick={sortShips('int', 5)} >6</th>
<th className='sortable' onClick={sortShips('int', 6)} >7</th>
<th className='sortable' onClick={sortShips('int', 7)} >8</th>
</tr>
</thead>
<tbody onMouseLeave={this._highlightShip.bind(this, null)}>
{detailRows}
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,20 +7,11 @@ import Module from './Module';
* @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass * @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass
* @param {number} fuel Optional - The fuel consumed during the jump * @param {number} fuel Optional - The fuel consumed during the jump
* @return {number} Distance in Light Years * @return {number} Distance in Light Years
* @param {object} ship Ship instance
*/ */
export function jumpRange(mass, fsd, fuel, ship) { export function jumpRange(mass, fsd, fuel) {
const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel;
const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass;
let jumpAddition = 0; return Math.pow(Math.min(fuel === undefined ? fsdMaxFuelPerJump : fuel, fsdMaxFuelPerJump) / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass;
if (ship) {
for (const module of ship.internal) {
if (module && module.m && module.m.grp === 'gfsb' && ship.getSlotStatus(module) == 3) {
jumpAddition += module.m.getJumpBoost();
}
}
}
return (Math.pow(Math.min(fuel === undefined ? fsdMaxFuelPerJump : fuel, fsdMaxFuelPerJump) / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass) + jumpAddition;
} }
/** /**
@@ -30,9 +21,8 @@ export function jumpRange(mass, fsd, fuel, ship) {
* @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass * @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass
* @param {number} fuel The total fuel available * @param {number} fuel The total fuel available
* @return {number} Distance in Light Years * @return {number} Distance in Light Years
* @param {object} ship Ship instance
*/ */
export function totalJumpRange(mass, fsd, fuel, ship) { export function totalJumpRange(mass, fsd, fuel) {
const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel;
const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass;
@@ -40,7 +30,7 @@ export function totalJumpRange(mass, fsd, fuel, ship) {
let totalRange = 0; let totalRange = 0;
while (fuelRemaining > 0) { while (fuelRemaining > 0) {
const fuelForThisJump = Math.min(fuelRemaining, fsdMaxFuelPerJump); const fuelForThisJump = Math.min(fuelRemaining, fsdMaxFuelPerJump);
totalRange += this.jumpRange(mass, fsd, fuelForThisJump, ship); totalRange += this.jumpRange(mass, fsd, fuelForThisJump);
// Mass is reduced // Mass is reduced
mass -= fuelForThisJump; mass -= fuelForThisJump;
fuelRemaining -= fuelForThisJump; fuelRemaining -= fuelForThisJump;
@@ -55,7 +45,7 @@ export function totalJumpRange(mass, fsd, fuel, ship) {
* @param {number} baseShield Base Shield strength MJ for ship * @param {number} baseShield Base Shield strength MJ for ship
* @param {object} sg The shield generator used * @param {object} sg The shield generator used
* @param {number} multiplier Shield multiplier for ship (1 + shield boosters if any) * @param {number} multiplier Shield multiplier for ship (1 + shield boosters if any)
* @return {number} Approximate shield strengh in MJ * @return {number} Approximate shield strengh in MJ
*/ */
export function shieldStrength(mass, baseShield, sg, multiplier) { export function shieldStrength(mass, baseShield, sg, multiplier) {
// sg might be a module or a template; handle either here // sg might be a module or a template; handle either here
@@ -65,12 +55,13 @@ export function shieldStrength(mass, baseShield, sg, multiplier) {
let minMul = sg instanceof Module ? sg.getMinMul() : sg.minmul; let minMul = sg instanceof Module ? sg.getMinMul() : sg.minmul;
let optMul = sg instanceof Module ? sg.getOptMul() : sg.optmul; let optMul = sg instanceof Module ? sg.getOptMul() : sg.optmul;
let maxMul = sg instanceof Module ? sg.getMaxMul() : sg.maxmul; let maxMul = sg instanceof Module ? sg.getMaxMul() : sg.maxmul;
let xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass)); let xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass));
let exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass))); let exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass)));
let ynorm = Math.pow(xnorm, exponent); let ynorm = Math.pow(xnorm, exponent);
let mul = minMul + ynorm * (maxMul - minMul); let mul = minMul + ynorm * (maxMul - minMul);
return (baseShield * mul * multiplier); return baseShield * mul * multiplier;
} }
/** /**
@@ -96,16 +87,6 @@ export function speed(mass, baseSpeed, thrusters, engpip) {
return results; return results;
} }
/**
* Calculate pip multiplier for speed.
* @param {number} baseSpeed The base speed of ship in data
* @param {number} topSpeed The top speed of ship in data
* @return {number} The multiplier that pips affect speed.
*/
export function calcPipSpeed(baseSpeed, topSpeed) {
return (topSpeed - baseSpeed) / (4 * topSpeed);
}
/** /**
* Calculate pitch of a ship based on mass and thrusters * Calculate pitch of a ship based on mass and thrusters
* @param {number} mass the mass of the ship * @param {number} mass the mass of the ship
@@ -221,7 +202,7 @@ function calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, base
* Calculate speed for a given setup * Calculate speed for a given setup
* @param {number} mass the mass of the ship * @param {number} mass the mass of the ship
* @param {number} baseSpeed the base speed of the ship * @param {number} baseSpeed the base speed of the ship
* @param {object} thrusters the thrusters of the ship * @param {ojbect} thrusters the thrusters of the ship
* @param {number} engpip the multiplier per pip to engines * @param {number} engpip the multiplier per pip to engines
* @param {number} eng the pips to engines * @param {number} eng the pips to engines
* @param {number} boostFactor the boost factor for ths ship * @param {number} boostFactor the boost factor for ths ship
@@ -249,7 +230,7 @@ export function calcSpeed(mass, baseSpeed, thrusters, engpip, eng, boostFactor,
* Calculate pitch for a given setup * Calculate pitch for a given setup
* @param {number} mass the mass of the ship * @param {number} mass the mass of the ship
* @param {number} basePitch the base pitch of the ship * @param {number} basePitch the base pitch of the ship
* @param {object} thrusters the thrusters of the ship * @param {ojbect} thrusters the thrusters of the ship
* @param {number} engpip the multiplier per pip to engines * @param {number} engpip the multiplier per pip to engines
* @param {number} eng the pips to engines * @param {number} eng the pips to engines
* @param {number} boostFactor the boost factor for ths ship * @param {number} boostFactor the boost factor for ths ship
@@ -340,76 +321,41 @@ export function shieldMetrics(ship, sys) {
const maxSysResistance = this.sysResistance(4); const maxSysResistance = this.sysResistance(4);
let shield = {}; let shield = {};
const dimReturnLine = (res) => 1 - (1 - res) * 0.7;
const shieldGeneratorSlot = ship.findInternalByGroup('sg'); const shieldGeneratorSlot = ship.findInternalByGroup('sg');
if (shieldGeneratorSlot && shieldGeneratorSlot.enabled && shieldGeneratorSlot.m) { if (shieldGeneratorSlot && shieldGeneratorSlot.enabled && shieldGeneratorSlot.m) {
const shieldGenerator = shieldGeneratorSlot.m; const shieldGenerator = shieldGeneratorSlot.m;
let res = {
kin: shieldGenerator.kinres,
therm: shieldGenerator.thermres,
expl: shieldGenerator.explres
};
// Boosters // Boosters
let boost = 1; let boost = 1;
let boosterExplDmg = 1; let boosterExplDmg = 1;
let boosterKinDmg = 1; let boosterKinDmg = 1;
let boosterThermDmg = 1; let boosterThermDmg = 1;
// const explDim = dimReturnLine(shieldGenerator.explres);
// const thermDim = dimReturnLine(shieldGenerator.thermres);
// const kinDim = dimReturnLine(shieldGenerator.kinres);
for (let slot of ship.hardpoints) { for (let slot of ship.hardpoints) {
if (slot.enabled && slot.m && slot.m.grp == 'sb') { if (slot.enabled && slot.m && slot.m.grp == 'sb') {
boost += slot.m.getShieldBoost(); boost += slot.m.getShieldBoost();
res.expl += slot.m.getExplosiveResistance();
res.kin += slot.m.getKineticResistance();
res.therm += slot.m.getThermalResistance();
boosterExplDmg = boosterExplDmg * (1 - slot.m.getExplosiveResistance()); boosterExplDmg = boosterExplDmg * (1 - slot.m.getExplosiveResistance());
boosterKinDmg = boosterKinDmg * (1 - slot.m.getKineticResistance()); boosterKinDmg = boosterKinDmg * (1 - slot.m.getKineticResistance());
boosterThermDmg = boosterThermDmg * (1 - slot.m.getThermalResistance()); boosterThermDmg = boosterThermDmg * (1 - slot.m.getThermalResistance());
} }
if (slot.m && slot.m.grp == 'gsrp') {
}
} }
// Calculate diminishing returns for boosters // Calculate diminishing returns for boosters
// Diminishing returns not currently in-game // Diminishing returns not currently in-game
// boost = Math.min(boost, (1 - Math.pow(Math.E, -0.7 * boost)) * 2.5); // boost = Math.min(boost, (1 - Math.pow(Math.E, -0.7 * boost)) * 2.5);
// Remove base shield generator strength // Remove base shield generator strength
boost -= 1; boost -= 1;
// Apply diminishing returns
boosterExplDmg = boosterExplDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterExplDmg) / 2;
boosterKinDmg = boosterKinDmg > 0.7 ? boosterKinDmg : 0.7 - (0.7 - boosterKinDmg) / 2;
boosterThermDmg = boosterThermDmg > 0.7 ? boosterThermDmg : 0.7 - (0.7 - boosterThermDmg) / 2;
// if (res.expl > explDim) { const generatorStrength = this.shieldStrength(ship.hullMass, ship.baseShieldStrength, shieldGenerator, 1);
// const overage = (res.expl - explDim) * 0.5;
// res.expl = explDim + overage;
// boosterExplDmg = explDim + overage;
// }
//
// if (res.therm > thermDim) {
// const overage = (res.therm - thermDim) * 0.5;
// res.therm = thermDim + overage;
// boosterThermDmg = thermDim + overage;
// }
//
// if (res.kin > kinDim) {
// const overage = (res.kin - kinDim) * 0.5;
// res.kin = kinDim + overage;
// boosterKinDmg = kinDim + overage;
// }
let shieldAddition = 0;
if (ship) {
for (const module of ship.internal) {
if (module && module.m && module.m.grp === 'gsrp' && module.enabled) {
shieldAddition += module.m.getShieldAddition();
}
}
}
let generatorStrength = this.shieldStrength(ship.hullMass, ship.baseShieldStrength, shieldGenerator, 1);
const boostersStrength = generatorStrength * boost; const boostersStrength = generatorStrength * boost;
// Recover time is the time taken to go from 0 to 50%. It includes a 16-second wait before shields start to recover // Recover time is the time taken to go from 0 to 50%. It includes a 16-second wait before shields start to recover
const shieldToRecover = (generatorStrength + boostersStrength + shieldAddition) / 2; const shieldToRecover = (generatorStrength + boostersStrength) / 2;
const powerDistributor = ship.standard[4].m; const powerDistributor = ship.standard[4].m;
const sysRechargeRate = this.sysRechargeRate(powerDistributor, sys); const sysRechargeRate = this.sysRechargeRate(powerDistributor, sys);
@@ -428,7 +374,7 @@ export function shieldMetrics(ship, sys) {
const remainingShieldToRecover = shieldToRecover - capacitorLifetime * shieldGenerator.getBrokenRegenerationRate(); const remainingShieldToRecover = shieldToRecover - capacitorLifetime * shieldGenerator.getBrokenRegenerationRate();
if (sys === 0) { if (sys === 0) {
// No system pips so will never recover shields // No system pips so will never recover shields
recover = Math.Infinity; recover = Math.Inf;
} else { } else {
// Recover remaining shields at the rate of the power distributor's recharge // Recover remaining shields at the rate of the power distributor's recharge
recover += remainingShieldToRecover / (sysRechargeRate / 0.6); recover += remainingShieldToRecover / (sysRechargeRate / 0.6);
@@ -436,7 +382,7 @@ export function shieldMetrics(ship, sys) {
} }
// Recharge time is the time taken to go from 50% to 100% // Recharge time is the time taken to go from 50% to 100%
const shieldToRecharge = (generatorStrength + boostersStrength + shieldAddition) / 2; const shieldToRecharge = (generatorStrength + boostersStrength) / 2;
// Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes // Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes
// 0.6 is a magic number from FD: each 0.6 MW of energy from the power distributor recharges 1 MJ/s of regeneration // 0.6 is a magic number from FD: each 0.6 MW of energy from the power distributor recharges 1 MJ/s of regeneration
@@ -463,10 +409,8 @@ export function shieldMetrics(ship, sys) {
shield = { shield = {
generator: generatorStrength, generator: generatorStrength,
boosters: boostersStrength, boosters: boostersStrength,
addition: shieldAddition,
cells: ship.shieldCells, cells: ship.shieldCells,
summary: generatorStrength + boostersStrength + shieldAddition, total: generatorStrength + boostersStrength + ship.shieldCells,
total: generatorStrength + boostersStrength + ship.shieldCells + shieldAddition,
recover, recover,
recharge, recharge,
}; };
@@ -481,74 +425,34 @@ export function shieldMetrics(ship, sys) {
max: 1 - maxSysResistance max: 1 - maxSysResistance
}; };
/**
* An object that stores a selection of difference damage multipliers that
* deal with a ship's shield strength.
* @typedef {Object} ShieldDamageMults
* @property {number} generator Base damage multiplier of the shield
* contributing it's base resistance.
* @property {number} boosters Damage multiplier contributed by all
* boosters, i.e. `rawMj / (generator * boosters)` equals shield strength
* with 0 pips to sys.
* @property {number} sys Damage multiplier contributed by pips to sys.
* @property {number} base Damage multiplier with 0 pips to sys; just
* boosters and shield generator. Equals `generator * boosters`.
* @property {number} total Damage multiplier with current pip settings.
* @property {number} max Damage multiplier with 4 pips to sys.
*/
let sgExplosiveDmg = 1 - shieldGenerator.getExplosiveResistance();
let sgSbExplosiveDmg = diminishDamageMult(sgExplosiveDmg * 0.7, (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg);
/** @type {ShieldDamageMults} */
shield.explosive = { shield.explosive = {
generator: sgExplosiveDmg, generator: 1 - shieldGenerator.getExplosiveResistance(),
boosters: sgSbExplosiveDmg / sgExplosiveDmg, boosters: boosterExplDmg,
sys: (1 - sysResistance), sys: (1 - sysResistance),
base: sgSbExplosiveDmg, total: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - sysResistance),
total: sgSbExplosiveDmg * (1 - sysResistance), max: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - maxSysResistance)
max: sgSbExplosiveDmg * (1 - maxSysResistance),
}; };
let sgKineticDmg = 1 - shieldGenerator.getKineticResistance();
let sgSbKineticDmg = diminishDamageMult(sgKineticDmg * 0.7, (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg);
/** @type {ShieldDamageMults} */
shield.kinetic = { shield.kinetic = {
generator: sgKineticDmg, generator: 1 - shieldGenerator.getKineticResistance(),
boosters: sgSbKineticDmg / sgKineticDmg, boosters: boosterKinDmg,
sys: (1 - sysResistance), sys: (1 - sysResistance),
base: sgSbKineticDmg, total: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - sysResistance),
total: sgSbKineticDmg * (1 - sysResistance), max: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - maxSysResistance)
max: sgSbKineticDmg * (1 - maxSysResistance),
}; };
let sgThermalDmg = 1 - shieldGenerator.getThermalResistance();
let sgSbThermalDmg = diminishDamageMult(sgThermalDmg * 0.7, (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg);
/** @type {ShieldDamageMults} */
shield.thermal = { shield.thermal = {
generator: sgThermalDmg, generator: 1 - shieldGenerator.getThermalResistance(),
boosters: sgSbThermalDmg / sgThermalDmg, boosters: boosterThermDmg,
sys: (1 - sysResistance), sys: (1 - sysResistance),
base: sgSbThermalDmg, total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance),
total: sgSbThermalDmg * (1 - sysResistance), max: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - maxSysResistance)
max: sgSbThermalDmg * (1 - maxSysResistance),
}; };
} }
return shield; return shield;
} }
/**
* Calculate time from one boost to another
* @return {number} Boost frequency in seconds
* @param {Ship} ship Ship object
*/
export function calcBoost(ship) {
if (!ship.boostEnergy || !ship.standard[4] || !ship.standard[4].m) {
return undefined;
}
return ship.boostEnergy / ship.standard[4].m.getEnginesRechargeRate();
}
/** /**
* Calculate armour metrics * Calculate armour metrics
* @param {Object} ship The ship * @param {Object} ship The ship
@@ -561,58 +465,33 @@ export function armourMetrics(ship) {
let moduleArmour = 0; let moduleArmour = 0;
let moduleProtection = 1; let moduleProtection = 1;
const bulkheads = ship.bulkheads.m;
let hullExplDmg = 1; let hullExplDmg = 1;
let hullKinDmg = 1; let hullKinDmg = 1;
let hullThermDmg = 1; let hullThermDmg = 1;
let hullCausDmg = 1;
// const dimReturnLine = (res) => 1 - (1 - res) * 0.7;
// let res = {
// kin: 0,
// therm: 0,
// expl: 0
// };
// Armour from HRPs and module armour from MRPs // Armour from HRPs and module armour from MRPs
for (let slot of ship.internal) { for (let slot of ship.internal) {
if (slot.m && slot.enabled && (slot.m.grp === 'hr' || slot.m.grp === 'ghrp' || slot.m.grp == 'mahr')) { if (slot.m && slot.m.grp == 'hr') {
armourReinforcement += slot.m.getHullReinforcement(); armourReinforcement += slot.m.getHullReinforcement();
// Hull boost for HRPs is applied against the ship's base armour // Hull boost for HRPs is applied against the ship's base armour
armourReinforcement += ship.baseArmour * slot.m.getModValue('hullboost') / 10000; armourReinforcement += ship.baseArmour * slot.m.getModValue('hullboost') / 10000;
// res.expl += slot.m.getExplosiveResistance();
// res.kin += slot.m.getKineticResistance();
// res.therm += slot.m.getThermalResistance();
hullExplDmg = hullExplDmg * (1 - slot.m.getExplosiveResistance()); hullExplDmg = hullExplDmg * (1 - slot.m.getExplosiveResistance());
hullKinDmg = hullKinDmg * (1 - slot.m.getKineticResistance()); hullKinDmg = hullKinDmg * (1 - slot.m.getKineticResistance());
hullThermDmg = hullThermDmg * (1 - slot.m.getThermalResistance()); hullThermDmg = hullThermDmg * (1 - slot.m.getThermalResistance());
hullCausDmg = hullCausDmg * (1 - slot.m.getCausticResistance());
} }
if (slot.m && slot.enabled && (slot.m.grp == 'mrp' || slot.m.grp == 'gmrp')) { if (slot.m && slot.m.grp == 'mrp') {
moduleArmour += slot.m.getIntegrity(); moduleArmour += slot.m.getIntegrity();
moduleProtection = moduleProtection * (1 - slot.m.getProtection()); moduleProtection = moduleProtection * (1 - slot.m.getProtection());
} }
} }
moduleProtection = 1 - moduleProtection; moduleProtection = 1 - moduleProtection;
// const explDim = dimReturnLine(bulkheads.explres); // Apply diminishing returns
// const thermDim = dimReturnLine(bulkheads.thermres); hullExplDmg = hullExplDmg > 0.7 ? hullExplDmg : 0.7 - (0.7 - hullExplDmg) / 2;
// const kinDim = dimReturnLine(bulkheads.kinres); hullKinDmg = hullKinDmg > 0.7 ? hullKinDmg : 0.7 - (0.7 - hullKinDmg) / 2;
// if (res.expl > explDim) { hullThermDmg = hullThermDmg > 0.7 ? hullThermDmg : 0.7 - (0.7 - hullThermDmg) / 2;
// const overage = (res.expl - explDim) * 0.5;
// res.expl = explDim + overage;
// hullExplDmg = explDim + overage;
// }
//
// if (res.therm > thermDim) {
// const overage = (res.therm - thermDim) * 0.5;
// res.therm = thermDim + overage;
// hullThermDmg = thermDim + overage;
// }
//
// if (res.kin > kinDim) {
// const overage = (res.kin - kinDim) * 0.5;
// res.kin = kinDim + overage;
// hullKinDmg = kinDim + overage;
// }
const armour = { const armour = {
bulkheads: armourBulkheads, bulkheads: armourBulkheads,
@@ -630,41 +509,24 @@ export function armourMetrics(ship) {
total: 1 total: 1
}; };
let armourExplDmg = diminishDamageMult(0.7, 1 - ship.bulkheads.m.getExplosiveResistance());
let armourReinforcedExplDmg = diminishDamageMult(0.7, (1 - ship.bulkheads.m.getExplosiveResistance()) * hullExplDmg);
armour.explosive = { armour.explosive = {
bulkheads: armourExplDmg, bulkheads: 1 - ship.bulkheads.m.getExplosiveResistance(),
reinforcement: armourReinforcedExplDmg / armourExplDmg, reinforcement: hullExplDmg,
total: armourReinforcedExplDmg, total: (1 - ship.bulkheads.m.getExplosiveResistance()) * hullExplDmg
res: 1 - armourReinforcedExplDmg
}; };
let armourKinDmg = diminishDamageMult(0.7, 1 - ship.bulkheads.m.getKineticResistance());
let armourReinforcedKinDmg = diminishDamageMult(0.7, (1 - ship.bulkheads.m.getKineticResistance()) * hullKinDmg);
armour.kinetic = { armour.kinetic = {
bulkheads: armourKinDmg, bulkheads: 1 - ship.bulkheads.m.getKineticResistance(),
reinforcement: armourReinforcedKinDmg / armourKinDmg, reinforcement: hullKinDmg,
total: armourReinforcedKinDmg, total: (1 - ship.bulkheads.m.getKineticResistance()) * hullKinDmg
res: 1 - armourReinforcedKinDmg
}; };
let armourThermDmg = diminishDamageMult(0.7, 1 - ship.bulkheads.m.getThermalResistance());
let armourReinforcedThermDmg = diminishDamageMult(0.7, (1 - ship.bulkheads.m.getThermalResistance()) * hullThermDmg);
armour.thermal = { armour.thermal = {
bulkheads: armourThermDmg, bulkheads: 1 - ship.bulkheads.m.getThermalResistance(),
reinforcement: armourReinforcedThermDmg / armourThermDmg, reinforcement: hullThermDmg,
total: armourReinforcedThermDmg, total: (1 - ship.bulkheads.m.getThermalResistance()) * hullThermDmg
res: 1 - armourReinforcedThermDmg
}; };
let armourCausDmg = diminishDamageMult(0.7, 1 - ship.bulkheads.m.getCausticResistance());
let armourReinforcedCausDmg = diminishDamageMult(0.7, (1 - ship.bulkheads.m.getCausticResistance()) * hullCausDmg);
armour.caustic = {
bulkheads: armourCausDmg,
reinforcement: armourReinforcedCausDmg / armourCausDmg,
total: armourReinforcedCausDmg,
res: 1 - armourReinforcedCausDmg,
};
return armour; return armour;
} }
@@ -836,55 +698,20 @@ export function _sustainedDps(ship, opponent, opponentShields, opponentArmour, e
return { shieldsdps, armoursdps, eps }; return { shieldsdps, armoursdps, eps };
} }
/**
* Stores SDPS split up by type.
* @typedef {Object} SDps
* @property {number} absolute Damage of type absolute
* @property {number} explosive Damage of type explosive
* @property {number} kinetic Damage of type kinetic
* @property {number} thermal Damage of type thermal
* @property {number} [total] Sum of all damage types
*/
/**
* An object that holds information about SDPS for a given weapon and opponent.
* @typedef {Object} WeaponDamage
* @property {number} eps Energy per second
* @property {Object} damage An object that stores damage inflicted by
* the weapon.
* @property {Object} effectiveness An object that stores the effectiveness of
* the weapon against the opponent given.
*/
/**
* Stores overall SDPS and against a given opponent's shields and armour.
* @typedef {Object} WeaponDamage~damage
* @property {SDps} base Overall SDPS.
* @property {SDps} shields SDPS against the given opponent's shields.
* @property {SDps} armour SDPS against the given opponent's armour.
*/
/** /**
* Calculate the sustained DPS for a weapon at a given range * Calculate the sustained DPS for a weapon at a given range
* @param {Object} m The weapon * @param {Object} m The weapon
* @param {Object} opponent The opponent ship * @param {Object} opponent The opponent ship
* @param {Object} opponentShields The opponent's shield resistances * @param {Object} opponentShields The opponent's shield resistances
* @param {Object} opponentArmour The opponent's armour resistances * @param {Object} opponentArmour The opponent's armour resistances
* @param {int} engagementrange The range between the ship and opponent * @param {int} engagementrange The range between the ship and opponent
* @returns {WeaponDamage} Sustained DPS for shield and armour * @returns {Object} Sustained DPS for shield and armour
*/ */
export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange) { export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange) {
const opponentHasShields = opponentShields.generator ? true : false; const opponentHasShields = opponentShields.generator ? true : false;
const weapon = { const weapon = {
eps: 0, eps: 0,
damage: { damage: {
base: {
absolute: 0,
explosive: 0,
kinetic: 0,
thermal: 0,
total: 0,
},
shields: { shields: {
absolute: 0, absolute: 0,
explosive: 0, explosive: 0,
@@ -918,7 +745,7 @@ export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour
weapon.eps = m.getClip() ? (m.getClip() * m.getEps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getEps(); weapon.eps = m.getClip() ? (m.getClip() * m.getEps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getEps();
// Initial sustained DPS // Initial sustained DPS
let sDps = m.getSDps(); let sDps = m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getDps();
// Take fall-off in to account // Take fall-off in to account
const falloff = m.getFalloff(); const falloff = m.getFalloff();
@@ -929,12 +756,6 @@ export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour
sDps *= dropoff; sDps *= dropoff;
} }
weapon.damage.base.absolute = sDps * m.getDamageDist().A;
weapon.damage.base.explosive = sDps * m.getDamageDist().E;
weapon.damage.base.kinetic = sDps * m.getDamageDist().K;
weapon.damage.base.thermal = sDps * m.getDamageDist().T;
weapon.damage.base.total = sDps;
// Piercing/hardness modifier (for armour only) // Piercing/hardness modifier (for armour only)
const armourMultiple = m.getPiercing() >= opponent.hardness ? 1 : m.getPiercing() / opponent.hardness; const armourMultiple = m.getPiercing() >= opponent.hardness ? 1 : m.getPiercing() / opponent.hardness;
weapon.effectiveness.armour.hardness = armourMultiple; weapon.effectiveness.armour.hardness = armourMultiple;
@@ -1015,10 +836,7 @@ export function timeToDrainWep(ship, wep) {
*/ */
export function timeToDeplete(amount, dps, eps, capacity, recharge) { export function timeToDeplete(amount, dps, eps, capacity, recharge) {
const drainPerSecond = eps - recharge; const drainPerSecond = eps - recharge;
// If there is nothing to remove, we're don instantly if (drainPerSecond <= 0) {
if (!amount) {
return 0;
} if (drainPerSecond <= 0) {
// Simple result // Simple result
return amount / dps; return amount / dps;
} else { } else {
@@ -1035,17 +853,3 @@ export function timeToDeplete(amount, dps, eps, capacity, recharge) {
} }
} }
} }
/**
* Applies diminishing returns to resistances.
* @param {number} diminishFrom The base resistance up to which no diminishing returns are applied.
* @param {number} damageMult Resistance as damage multiplier
* @returns {number} Actual damage multiplier
*/
export function diminishDamageMult(diminishFrom, damageMult) {
if (damageMult > diminishFrom) {
return damageMult;
} else {
return (diminishFrom / 2) + 0.5 * damageMult;
}
}

View File

@@ -18,7 +18,7 @@ export const ModuleGroupToName = {
// Standard // Standard
pp: 'Power Plant', pp: 'Power Plant',
gpp: 'Guardian Hybrid Power Plant', gpp: 'Guardian Hybrid Power Plant',
gpd: 'Guardian Power Distributor', gpd: 'Guardian Hybrid Power Distributor',
t: 'Thrusters', t: 'Thrusters',
fsd: 'Frame Shift Drive', fsd: 'Frame Shift Drive',
ls: 'Life Support', ls: 'Life Support',
@@ -52,12 +52,6 @@ export const ModuleGroupToName = {
pcq: 'Luxury Passenger Cabin', pcq: 'Luxury Passenger Cabin',
cc: 'Collector Limpet Controller', cc: 'Collector Limpet Controller',
ss: 'Surface Scanner', ss: 'Surface Scanner',
gsrp: 'Guardian Shield Reinforcement Packages',
gfsb: 'Guardian Frame Shift Drive Booster',
ghrp: 'Guardian Hull Reinforcement Package',
gmrp: 'Guardian Module Reinforcement Package',
mahr: 'Meta Alloy Hull Reinforcement Package',
sua: 'Supercruise Assist',
// Hard Points // Hard Points
bl: 'Beam Laser', bl: 'Beam Laser',
@@ -86,19 +80,8 @@ export const ModuleGroupToName = {
tp: 'Torpedo Pylon', tp: 'Torpedo Pylon',
sfn: 'Shutdown Field Neutraliser', sfn: 'Shutdown Field Neutraliser',
xs: 'Xeno Scanner', xs: 'Xeno Scanner',
rcpl: 'Recon Limpet Controller',
rsl: 'Research Limpet Controller',
dtl: 'Decontamination Limpet Controller',
gpc: 'Guardian Plasma Charger', gpc: 'Guardian Plasma Charger',
ggc: 'Guardian Gauss Cannon', ggc: 'Guardian Gauss Cannon',
tbsc: 'Shock Cannon',
gsc: 'Guardian Shard Cannon',
tbem: 'Enzyme Missile Rack',
tbrfl: 'Remote Release Flechette Launcher',
pwa: 'Pulse Wave Analyser',
abl: 'Abrasion Blaster',
scl: 'Seismic Charge Launcher',
sdm: 'Sub-Surface Displacement Missile',
}; };
let GrpNameToCodeMap = {}; let GrpNameToCodeMap = {};
@@ -207,7 +190,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',

View File

@@ -2,6 +2,7 @@
* Modification - a modification and its value * Modification - a modification and its value
*/ */
export default class Modification { export default class Modification {
/** /**
* @param {String} id Unique modification ID * @param {String} id Unique modification ID
* @param {Number} value Value of the modification * @param {Number} value Value of the modification
@@ -10,4 +11,5 @@ export default class Modification {
this.id = id; this.id = id;
this.value = value; this.value = value;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@ function filter(arr, maxClass, minClass, mass) {
* The available module set for a specific ship * The available module set for a specific ship
*/ */
export default class ModuleSet { export default class ModuleSet {
/** /**
* Instantiate the module set * Instantiate the module set
* @param {Object} modules All Modules * @param {Object} modules All Modules

View File

@@ -63,10 +63,7 @@ export function standard(type, id) {
if (!isNaN(type)) { if (!isNaN(type)) {
type = StandardArray[type]; type = StandardArray[type];
} }
let s = Modules.standard[type].find(e => e.id === id); let s = Modules.standard[type].find(e => e.id == id || (e.class == id.charAt(0) && e.rating == id.charAt(1)));
if (!s) {
s = Modules.standard[type].find(e => (e.class == id.charAt(0) && e.rating == id.charAt(1)));
}
if (s) { if (s) {
s = new Module({ template: s }); s = new Module({ template: s });
} }

View File

@@ -85,12 +85,12 @@ export function toDetailedBuild(buildName, ship) {
code = ship.toString(); code = ship.toString();
let data = { let data = {
$schema: 'https://coriolis.io/schemas/ship-loadout/4.json#', $schema: 'https://coriolis.edcd.io/schemas/ship-loadout/4.json#',
name: buildName, name: buildName,
ship: ship.name, ship: ship.name,
references: [{ references: [{
name: 'Coriolis.io', name: 'Coriolis.io',
url: 'https://coriolis.io' + outfitURL(ship.id, code, buildName), url: 'https://coriolis.edcd.io' + outfitURL(ship.id, code, buildName),
code, code,
shipId: ship.id shipId: ship.id
}], }],

View File

@@ -7,10 +7,9 @@ import LZString from 'lz-string';
import * as _ from 'lodash'; import * as _ from 'lodash';
import isEqual from 'lodash/lang'; import isEqual from 'lodash/lang';
import { Ships, Modifications } from 'coriolis-data/dist'; import { Ships, Modifications } from 'coriolis-data/dist';
import { chain } from 'lodash';
const zlib = require('zlib'); const zlib = require('zlib');
const UNIQUE_MODULES = ['psg', 'sg', 'bsg', 'rf', 'fs', 'fh', 'gfsb', 'dc']; const UNIQUE_MODULES = ['psg', 'sg', 'bsg', 'rf', 'fs', 'fh'];
// Constants for modifications struct // Constants for modifications struct
const SLOT_ID_DONE = -1; const SLOT_ID_DONE = -1;
@@ -71,6 +70,7 @@ function reduceToIDs(idArray, slot, slotIndex) {
* Ship Model - Encapsulates and models in-game ship behavior * Ship Model - Encapsulates and models in-game ship behavior
*/ */
export default class Ship { export default class Ship {
/** /**
* @param {String} id Unique ship Id / Key * @param {String} id Unique ship Id / Key
* @param {Object} properties Basic ship properties such as name, manufacturer, mass, etc * @param {Object} properties Basic ship properties such as name, manufacturer, mass, etc
@@ -151,7 +151,7 @@ export default class Ship {
* @return {Number} Jump range in Light Years * @return {Number} Jump range in Light Years
*/ */
calcLadenRange(massDelta, fuel, fsd) { calcLadenRange(massDelta, fuel, fsd) {
return Calc.jumpRange(this.ladenMass + (massDelta || 0), fsd || this.standard[2].m, fuel, this); return Calc.jumpRange(this.ladenMass + (massDelta || 0), fsd || this.standard[2].m, fuel);
} }
/** /**
@@ -164,7 +164,7 @@ export default class Ship {
calcUnladenRange(massDelta, fuel, fsd) { calcUnladenRange(massDelta, fuel, fsd) {
fsd = fsd || this.standard[2].m; fsd = fsd || this.standard[2].m;
let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel;
return Calc.jumpRange(this.unladenMass + (massDelta || 0) + Math.min(fsdMaxFuelPerJump, fuel || this.fuelCapacity), fsd || this.standard[2].m, fuel, this); return Calc.jumpRange(this.unladenMass + (massDelta || 0) + Math.min(fsdMaxFuelPerJump, fuel || this.fuelCapacity), fsd || this.standard[2].m, fuel);
} }
/** /**
@@ -239,6 +239,7 @@ export default class Ship {
} }
sg = sgSlot.m; sg = sgSlot.m;
} }
// TODO Not accurate if the ship has modified shield boosters // TODO Not accurate if the ship has modified shield boosters
return Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sg, 1 + (multiplierDelta || 0)); return Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sg, 1 + (multiplierDelta || 0));
} }
@@ -415,16 +416,16 @@ export default class Ship {
clearModifications(m) { clearModifications(m) {
m.mods = {}; m.mods = {};
this.updatePowerGenerated() this.updatePowerGenerated()
.updatePowerUsed() .updatePowerUsed()
.recalculateMass() .recalculateMass()
.updateJumpStats() .updateJumpStats()
.recalculateShield() .recalculateShield()
.recalculateShieldCells() .recalculateShieldCells()
.recalculateArmour() .recalculateArmour()
.recalculateDps() .recalculateDps()
.recalculateEps() .recalculateEps()
.recalculateHps() .recalculateHps()
.updateMovement(); .updateMovement();
} }
/** /**
@@ -436,15 +437,12 @@ export default class Ship {
m.blueprint = bp; m.blueprint = bp;
this.clearModifications(m); this.clearModifications(m);
// Set any hidden items for the blueprint now // Set any hidden items for the blueprint now
if (m.blueprint.grades[m.blueprint.grade] && m.blueprint.grades[m.blueprint.grade].features) { const features = m.blueprint.grades[m.blueprint.grade].features;
const features = m.blueprint.grades[m.blueprint.grade].features; for (const featureName in features) {
for (const featureName in features) { if (Modifications.modifications[featureName].hidden) {
if (Modifications.modifications[featureName].hidden) { this.setModification(m, featureName, bp.grades[bp.grade].features[featureName][0]);
this.setModification(m, featureName, bp.grades[bp.grade].features[featureName][0]);
}
} }
} }
this.updateModificationsString(); this.updateModificationsString();
} }
@@ -484,7 +482,10 @@ export default class Ship {
* @param {Object} m The module for which to clear the blueprint * @param {Object} m The module for which to clear the blueprint
*/ */
clearModuleSpecial(m) { clearModuleSpecial(m) {
this.setModuleSpecial(m, null); if (m.blueprint) {
m.blueprint.special = null;
}
this.recalculateDps().recalculateHps().recalculateEps();
} }
/** /**
@@ -493,67 +494,68 @@ export default class Ship {
* @param {Object} name The name of the modification to change * @param {Object} name The name of the modification to change
* @param {Number} value The new value of the modification. The value of the modification is scaled to provide two decimal places of precision in an integer. For example 1.23% is stored as 123 * @param {Number} value The new value of the modification. The value of the modification is scaled to provide two decimal places of precision in an integer. For example 1.23% is stored as 123
* @param {bool} sentfromui True if this update was sent from the UI * @param {bool} sentfromui True if this update was sent from the UI
* @param {bool} isAbsolute True if value is an absolute value and not a
* modification value
*/ */
setModification(m, name, value, sentfromui, isAbsolute) { setModification(m, name, value, sentfromui) {
if (isNaN(value)) { if (isNaN(value)) {
// Value passed is invalid; reset it to 0 // Value passed is invalid; reset it to 0
value = 0; value = 0;
} }
if (isAbsolute) {
m.setPretty(name, value, sentfromui);
} else {
// Resistance modifiers scale with the base value
if (name == 'kinres' || name == 'thermres' || name == 'causres' || name == 'explres') {
let baseValue = m.get(name, false);
value = (1 - baseValue) * value;
}
m.setModValue(name, value, sentfromui);
}
// Handle special cases // Handle special cases
if (name === 'pgen') { if (name === 'pgen') {
// Power generation // Power generation
m.setModValue(name, value, sentfromui);
this.updatePowerGenerated(); this.updatePowerGenerated();
} else if (name === 'power') { } else if (name === 'power') {
// Power usage // Power usage
m.setModValue(name, value, sentfromui);
this.updatePowerUsed(); this.updatePowerUsed();
} else if (name === 'mass') { } else if (name === 'mass') {
// Mass // Mass
m.setModValue(name, value, sentfromui);
this.recalculateMass(); this.recalculateMass();
this.updateMovement(); this.updateMovement();
this.updateJumpStats(); this.updateJumpStats();
} else if (name === 'maxfuel') { } else if (name === 'maxfuel') {
m.setModValue(name, value, sentfromui);
this.updateJumpStats(); this.updateJumpStats();
} else if (name === 'optmass') { } else if (name === 'optmass') {
m.setModValue(name, value, sentfromui);
// Could be for any of thrusters, FSD or shield // Could be for any of thrusters, FSD or shield
this.updateMovement(); this.updateMovement();
this.updateJumpStats(); this.updateJumpStats();
this.recalculateShield(); this.recalculateShield();
} else if (name === 'optmul') { } else if (name === 'optmul') {
m.setModValue(name, value, sentfromui);
// Could be for any of thrusters, FSD or shield // Could be for any of thrusters, FSD or shield
this.updateMovement(); this.updateMovement();
this.updateJumpStats(); this.updateJumpStats();
this.recalculateShield(); this.recalculateShield();
} else if (name === 'shieldboost') { } else if (name === 'shieldboost') {
m.setModValue(name, value, sentfromui);
this.recalculateShield(); this.recalculateShield();
} else if (name === 'hullboost' || name === 'hullreinforcement' || name === 'modulereinforcement') { } else if (name === 'hullboost' || name === 'hullreinforcement' || name === 'modulereinforcement') {
m.setModValue(name, value, sentfromui);
this.recalculateArmour(); this.recalculateArmour();
} else if (name === 'shieldreinforcement') { } else if (name === 'shieldreinforcement') {
m.setModValue(name, value, sentfromui);
this.recalculateShieldCells(); this.recalculateShieldCells();
} else if (name === 'burst' || name == 'burstrof' || name === 'clip' || name === 'damage' || name === 'distdraw' || name === 'jitter' || name === 'piercing' || name === 'range' || name === 'reload' || name === 'rof' || name === 'thermload') { } else if (name === 'burst' || name == 'burstrof' || name === 'clip' || name === 'damage' || name === 'distdraw' || name === 'jitter' || name === 'piercing' || name === 'range' || name === 'reload' || name === 'rof' || name === 'thermload') {
m.setModValue(name, value, sentfromui);
this.recalculateDps(); this.recalculateDps();
this.recalculateHps(); this.recalculateHps();
this.recalculateEps(); this.recalculateEps();
} else if (name === 'explres' || name === 'kinres' || name === 'thermres') { } else if (name === 'explres' || name === 'kinres' || name === 'thermres') {
m.setModValue(name, value, sentfromui);
// Could be for shields or armour // Could be for shields or armour
this.recalculateArmour(); this.recalculateArmour();
this.recalculateShield(); this.recalculateShield();
} else if (name === 'engcap') { } else if (name === 'engcap') {
m.setModValue(name, value, sentfromui);
// Might have resulted in a change in boostability // Might have resulted in a change in boostability
this.updateMovement(); this.updateMovement();
} else {
// Generic
m.setModValue(name, value, sentfromui);
} }
} }
@@ -700,16 +702,16 @@ export default class Ship {
// Update aggragated stats // Update aggragated stats
if (comps) { if (comps) {
this.updatePowerGenerated() this.updatePowerGenerated()
.updatePowerUsed() .updatePowerUsed()
.recalculateMass() .recalculateMass()
.updateJumpStats() .updateJumpStats()
.recalculateShield() .recalculateShield()
.recalculateShieldCells() .recalculateShieldCells()
.recalculateArmour() .recalculateArmour()
.recalculateDps() .recalculateDps()
.recalculateEps() .recalculateEps()
.recalculateHps() .recalculateHps()
.updateMovement(); .updateMovement();
} }
return this.updatePowerPrioritesString().updatePowerEnabledString().updateModificationsString(); return this.updatePowerPrioritesString().updatePowerEnabledString().updateModificationsString();
@@ -936,14 +938,9 @@ export default class Ship {
let epsChanged = n && n.getEps() || old && old.getEps(); let epsChanged = n && n.getEps() || old && old.getEps();
let hpsChanged = n && n.getHps() || old && old.getHps(); let hpsChanged = n && n.getHps() || old && old.getHps();
let armourChange = (slot === this.bulkheads) || let armourChange = (slot === this.bulkheads) || (n && n.grp === 'hr') || (old && old.grp === 'hr') || (n && n.grp === 'mrp') || (old && old.grp === 'mrp');
(n && n.grp === 'hr') || (old && old.grp === 'hr') ||
(n && n.grp === 'ghrp') || (old && old.grp === 'ghrp') ||
(n && n.grp == 'mahr') || (old && old.grp == 'mahr') ||
(n && n.grp === 'mrp') || (old && old.grp === 'mrp') ||
(n && n.grp === 'gmrp') || (old && old.grp == 'gmrp');
let shieldChange = (n && n.grp === 'bsg') || (old && old.grp === 'bsg') || (n && n.grp === 'psg') || (old && old.grp === 'psg') || (n && n.grp === 'sg') || (old && old.grp === 'sg') || (n && n.grp === 'sb') || (old && old.grp === 'sb') || (old && old.grp === 'gsrp') || (n && n.grp === 'gsrp'); let shieldChange = (n && n.grp === 'bsg') || (old && old.grp === 'bsg') || (n && n.grp === 'psg') || (old && old.grp === 'psg') || (n && n.grp === 'sg') || (old && old.grp === 'sg') || (n && n.grp === 'sb') || (old && old.grp === 'sb');
let shieldCellsChange = (n && n.grp === 'scb') || (old && old.grp === 'scb'); let shieldCellsChange = (n && n.grp === 'scb') || (old && old.grp === 'scb');
@@ -1000,6 +997,25 @@ export default class Ship {
return this; return this;
} }
/**
* Calculate diminishing returns value, where values below a given limit are returned
* as-is, and values between the lower and upper limit of the diminishing returns are
* given at half value.
* Commonly used for resistances.
* @param {Number} val The value
* @param {Number} drll The lower limit for diminishing returns
* @param {Number} drul The upper limit for diminishing returns
* @return {this} The ship instance (for chaining operations)
*/
diminishingReturns(val, drll, drul) {
if (val < drll) {
val = drll;
} else if (val < drul) {
val = drul - (drul - val) / 2;
}
return val;
}
/** /**
* Calculate damage per second and related items for weapons * Calculate damage per second and related items for weapons
* @return {this} The ship instance (for chaining operations) * @return {this} The ship instance (for chaining operations)
@@ -1186,35 +1202,38 @@ export default class Ship {
unladenMass += this.bulkheads.m.getMass(); unladenMass += this.bulkheads.m.getMass();
let slots = this.standard.concat(this.internal, this.hardpoints); for (let slotNum in this.standard) {
// TODO: create class for slot and also add slot.get const slot = this.standard[slotNum];
// handle unladen mass if (slot.m) {
unladenMass += chain(slots) unladenMass += slot.m.getMass();
.map(slot => slot.m ? slot.m.get('mass') : null) if (slot.m.grp === 'ft') {
.map(mass => mass || 0) fuelCapacity += slot.m.fuel;
.reduce((sum, mass) => sum + mass) }
.value(); }
}
// handle fuel capacity for (let slotNum in this.internal) {
fuelCapacity += chain(slots) const slot = this.internal[slotNum];
.map(slot => slot.m ? slot.m.get('fuel') : null) if (slot.m) {
.map(fuel => fuel || 0) unladenMass += slot.m.getMass();
.reduce((sum, fuel) => sum + fuel) if (slot.m.grp === 'ft') {
.value(); fuelCapacity += slot.m.fuel;
} else if (slot.m.grp === 'cr') {
cargoCapacity += slot.m.cargo;
} else if (slot.m.grp.slice(0,2) === 'pc') {
if (slot.m.passengers) {
passengerCapacity += slot.m.passengers;
}
}
}
}
// handle cargo capacity for (let slotNum in this.hardpoints) {
cargoCapacity += chain(slots) const slot = this.hardpoints[slotNum];
.map(slot => slot.m ? slot.m.get('cargo') : null) if (slot.m) {
.map(cargo => cargo || 0) unladenMass += slot.m.getMass();
.reduce((sum, cargo) => sum + cargo) }
.value(); }
// handle passenger capacity
passengerCapacity += chain(slots)
.map(slot => slot.m ? slot.m.get('passengers') : null)
.map(passengers => passengers || 0)
.reduce((sum, passengers) => sum + passengers)
.value();
// Update global stats // Update global stats
this.unladenMass = unladenMass; this.unladenMass = unladenMass;
@@ -1255,7 +1274,7 @@ export default class Ship {
// Obtain shield metrics with 0 pips to sys (parts affected by SYS aren't used here) // Obtain shield metrics with 0 pips to sys (parts affected by SYS aren't used here)
const metrics = Calc.shieldMetrics(this, 0); const metrics = Calc.shieldMetrics(this, 0);
this.shield = metrics.generator ? metrics.generator + metrics.boosters + metrics.addition : 0; this.shield = metrics.generator ? metrics.generator + metrics.boosters : 0;
this.shieldExplRes = this.shield > 0 ? 1 - metrics.explosive.total : null; this.shieldExplRes = this.shield > 0 ? 1 - metrics.explosive.total : null;
this.shieldKinRes = this.shield > 0 ? 1 - metrics.kinetic.total : null; this.shieldKinRes = this.shield > 0 ? 1 - metrics.kinetic.total : null;
this.shieldThermRes = this.shield > 0 ? 1 - metrics.thermal.total : null; this.shieldThermRes = this.shield > 0 ? 1 - metrics.thermal.total : null;
@@ -1288,15 +1307,45 @@ export default class Ship {
*/ */
recalculateArmour() { recalculateArmour() {
// Armour from bulkheads // Armour from bulkheads
let metrics = Calc.armourMetrics(this); let bulkhead = this.bulkheads.m;
let armour = this.baseArmour + (this.baseArmour * bulkhead.getHullBoost());
let modulearmour = 0;
let moduleprotection = 1;
let hullExplRes = 1 - bulkhead.getExplosiveResistance();
const hullExplResDRStart = hullExplRes * 0.7;
const hullExplResDREnd = hullExplRes * 0;
let hullKinRes = 1 - bulkhead.getKineticResistance();
const hullKinResDRStart = hullKinRes * 0.7;
const hullKinResDREnd = hullKinRes * 0;
let hullThermRes = 1 - bulkhead.getThermalResistance();
const hullThermResDRStart = hullThermRes * 0.7;
const hullThermResDREnd = hullThermRes * 0;
// Armour from HRPs and module armour from MRPs
for (let slot of this.internal) {
if (slot.m && slot.m.grp == 'hr') {
armour += slot.m.getHullReinforcement();
// Hull boost for HRPs is applied against the ship's base armour
armour += this.baseArmour * slot.m.getModValue('hullboost') / 10000;
hullExplRes *= (1 - slot.m.getExplosiveResistance());
hullKinRes *= (1 - slot.m.getKineticResistance());
hullThermRes *= (1 - slot.m.getThermalResistance());
}
if (slot.m && slot.m.grp == 'mrp') {
modulearmour += slot.m.getIntegrity();
moduleprotection = moduleprotection * (1 - slot.m.getProtection());
}
}
moduleprotection = 1 - moduleprotection;
this.armour = armour;
this.modulearmour = modulearmour;
this.moduleprotection = moduleprotection;
this.hullExplRes = 1 - this.diminishingReturns(hullExplRes, hullExplResDREnd, hullExplResDRStart);
this.hullKinRes = 1 - this.diminishingReturns(hullKinRes, hullKinResDREnd, hullKinResDRStart);
this.hullThermRes = 1 - this.diminishingReturns(hullThermRes, hullThermResDREnd, hullThermResDRStart);
this.armour = metrics.total ? metrics.total : 0;
this.modulearmour = metrics.modulearmour;
this.moduleprotection = metrics.moduleprotection;
this.hullExplRes = 1 - metrics.explosive.total;
this.hullKinRes = 1 - metrics.kinetic.total;
this.hullThermRes = 1 - metrics.thermal.total;
this.hullCausRes = 1 - metrics.caustic.total;
return this; return this;
} }
@@ -1308,10 +1357,10 @@ export default class Ship {
let fsd = this.standard[2].m; // Frame Shift Drive; let fsd = this.standard[2].m; // Frame Shift Drive;
let { unladenMass, fuelCapacity } = this; let { unladenMass, fuelCapacity } = this;
this.unladenRange = this.calcUnladenRange(); // Includes fuel weight for jump this.unladenRange = this.calcUnladenRange(); // Includes fuel weight for jump
this.fullTankRange = Calc.jumpRange(unladenMass + fuelCapacity, fsd, fuelCapacity, this); // Full Tank this.fullTankRange = Calc.jumpRange(unladenMass + fuelCapacity, fsd); // Full Tank
this.ladenRange = this.calcLadenRange(); // Includes full tank and caro this.ladenRange = this.calcLadenRange(); // Includes full tank and caro
this.unladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity, fsd, fuelCapacity, this); this.unladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity, fsd, fuelCapacity);
this.ladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity + this.cargoCapacity, fsd, fuelCapacity, this); this.ladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity + this.cargoCapacity, fsd, fuelCapacity);
this.maxJumpCount = Math.ceil(fuelCapacity / fsd.getMaxFuelPerJump()); this.maxJumpCount = Math.ceil(fuelCapacity / fsd.getMaxFuelPerJump());
return this; return this;
} }
@@ -1506,7 +1555,7 @@ export default class Ship {
} else { } else {
buffer.writeInt32LE(slotMod.value, curpos); buffer.writeInt32LE(slotMod.value, curpos);
} }
const modification = _.find(Modifications.modifications, function(o) { return o.id === slotMod.id; }); // const modification = _.find(Modifications.modifications, function(o) { return o.id === slotMod.id; });
// console.log('ENCODE Slot ' + i + ': ' + modification.name + ' = ' + slotMod.value); // console.log('ENCODE Slot ' + i + ': ' + modification.name + ' = ' + slotMod.value);
curpos += 4; curpos += 4;
} }
@@ -1519,7 +1568,6 @@ export default class Ship {
} }
this.serialized.modifications = zlib.gzipSync(buffer).toString('base64'); this.serialized.modifications = zlib.gzipSync(buffer).toString('base64');
// console.log(this.serialized.modifications)
} else { } else {
this.serialized.modifications = null; this.serialized.modifications = null;
} }
@@ -1688,11 +1736,11 @@ export default class Ship {
updated; updated;
this.useBulkhead(0) this.useBulkhead(0)
.use(standard[2], fsd) // FSD .use(standard[2], fsd) // FSD
.use(standard[3], ls) // Life Support .use(standard[3], ls) // Life Support
.use(standard[5], s) // Sensors .use(standard[5], s) // Sensors
.use(standard[4], pd) // Power Distributor .use(standard[4], pd) // Power Distributor
.use(standard[6], ft); // Fuel Tank .use(standard[6], ft); // Fuel Tank
// Turn off nearly everything // Turn off nearly everything
if (m.fsdDisabled) this.setSlotEnabled(this.standard[2], false); if (m.fsdDisabled) this.setSlotEnabled(this.standard[2], false);

View File

@@ -1,5 +1,5 @@
import * as ModuleUtils from './ModuleUtils'; import * as ModuleUtils from './ModuleUtils'
import { canMount } from '../utils/SlotFunctions'; import { canMount } from '../utils/SlotFunctions'
/** /**
* Standard / typical role for multi-purpose or combat (if shielded with better bulkheads) * Standard / typical role for multi-purpose or combat (if shielded with better bulkheads)
@@ -7,20 +7,20 @@ import { canMount } from '../utils/SlotFunctions';
* @param {Boolean} shielded True if shield generator should be included * @param {Boolean} shielded True if shield generator should be included
* @param {integer} bulkheadIndex Bulkhead to use see Constants.BulkheadNames * @param {integer} bulkheadIndex Bulkhead to use see Constants.BulkheadNames
*/ */
export function multiPurpose(ship, shielded, bulkheadIndex) { export function multiPurpose (ship, shielded, bulkheadIndex) {
ship.useStandard('A') ship.useStandard('A')
.use(ship.standard[3], ModuleUtils.standard(3, ship.standard[3].maxClass + 'D')) // D Life Support .use(ship.standard[3], ModuleUtils.standard(3, ship.standard[3].maxClass + 'D')) // D Life Support
.use(ship.standard[5], ModuleUtils.standard(5, ship.standard[5].maxClass + 'D')) // D Sensors .use(ship.standard[5], ModuleUtils.standard(5, ship.standard[5].maxClass + 'D')) // D Sensors
.useBulkhead(bulkheadIndex); .useBulkhead(bulkheadIndex)
if (shielded) { if (shielded) {
ship.internal.some(function(slot) { ship.internal.some(function (slot) {
if (canMount(ship, slot, 'sg')) { // Assuming largest slot can hold an eligible shield if (canMount(ship, slot, 'sg')) { // Assuming largest slot can hold an eligible shield
ship.use(slot, ModuleUtils.findInternal('sg', slot.maxClass, 'A')); ship.use(slot, ModuleUtils.findInternal('sg', slot.maxClass, 'A'))
ship.setSlotEnabled(slot, true); ship.setSlotEnabled(slot, true)
return true; return true
} }
}); })
} }
} }
@@ -30,51 +30,52 @@ export function multiPurpose(ship, shielded, bulkheadIndex) {
* @param {Boolean} shielded True if shield generator should be included * @param {Boolean} shielded True if shield generator should be included
* @param {Object} standardOpts [Optional] Standard module optional overrides * @param {Object} standardOpts [Optional] Standard module optional overrides
*/ */
export function trader(ship, shielded, standardOpts) { export function trader (ship, shielded, standardOpts) {
let usedSlots = []; let usedSlots = []
let bstCount = 2; let bstCount = 2
let sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass); let sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass)
ship.useStandard('A') ship.useStandard('A')
.use(ship.standard[3], ModuleUtils.standard(3, ship.standard[3].maxClass + 'D')) // D Life Support .use(ship.standard[3], ModuleUtils.standard(3, ship.standard[3].maxClass + 'D')) // D Life Support
.use(ship.standard[1], ModuleUtils.standard(1, ship.standard[1].maxClass + 'D')) // D Life Support .use(ship.standard[1], ModuleUtils.standard(1, ship.standard[1].maxClass + 'D')) // D Life Support
.use(ship.standard[4], ModuleUtils.standard(4, ship.standard[4].maxClass + 'D')) // D Life Support .use(ship.standard[4], ModuleUtils.standard(4, ship.standard[4].maxClass + 'D')) // D Life Support
.use(ship.standard[5], ModuleUtils.standard(5, ship.standard[5].maxClass + 'D')); // D Sensors .use(ship.standard[5], ModuleUtils.standard(5, ship.standard[5].maxClass + 'D')) // D Sensors
const shieldOrder = [1, 2, 3, 4, 5, 6, 7, 8]; const shieldOrder = [1, 2, 3, 4, 5, 6, 7, 8]
const shieldInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1) const shieldInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
.filter(a => (!a.eligible) || a.eligible.sg) .filter(a => (!a.eligible) || a.eligible.sg)
.filter(a => a.maxClass >= sg.class) .filter(a => a.maxClass >= sg.class)
.sort((a, b) => shieldOrder.indexOf(a.maxClass) - shieldOrder.indexOf(b.maxClass)); .sort((a, b) => shieldOrder.indexOf(a.maxClass) - shieldOrder.indexOf(b.maxClass))
shieldInternals.some(function(slot) { shieldInternals.some(function (slot) {
if (canMount(ship, slot, 'sg')) { // Assuming largest slot can hold an eligible shield if (canMount(ship, slot, 'sg')) { // Assuming largest slot can hold an eligible shield
const shield = ModuleUtils.findInternal('sg', slot.maxClass, 'A'); const shield = ModuleUtils.findInternal('sg', slot.maxClass, 'A')
if (shield && shield.maxmass > ship.hullMass) { if (shield && shield.maxmass > ship.hullMass) {
ship.use(slot, shield); console.log(shield)
ship.setSlotEnabled(slot, true); ship.use(slot, shield)
usedSlots.push(slot); ship.setSlotEnabled(slot, true)
return true; usedSlots.push(slot)
return true
} }
} }
}); })
// Fill the empty internals with cargo racks // Fill the empty internals with cargo racks
for (let i = ship.internal.length; i--;) { for (let i = ship.internal.length; i--;) {
let slot = ship.internal[i]; let slot = ship.internal[i]
if (usedSlots.indexOf(slot) == -1 && canMount(ship, slot, 'cr')) { if (usedSlots.indexOf(slot) == -1 && canMount(ship, slot, 'cr')) {
ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E')); ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E'))
} }
} }
// Empty the hardpoints // Empty the hardpoints
for (let s of ship.hardpoints) { for (let s of ship.hardpoints) {
ship.use(s, null); ship.use(s, null)
} }
for (let s of ship.hardpoints) { for (let s of ship.hardpoints) {
if (s.maxClass == 0 && bstCount) { // Mount up to 2 boosters if (s.maxClass == 0 && bstCount) { // Mount up to 2 boosters
ship.use(s, ModuleUtils.hardpoints('04')); ship.use(s, ModuleUtils.hardpoints('04'))
bstCount--; bstCount--
} else { } else {
ship.use(s, null); ship.use(s, null)
} }
} }
// ship.useLightestStandard(standardOpts); // ship.useLightestStandard(standardOpts);
@@ -85,127 +86,127 @@ export function trader(ship, shielded, standardOpts) {
* @param {Ship} ship Ship instance * @param {Ship} ship Ship instance
* @param {Boolean} planetary True if Planetary Vehicle Hangar (PVH) should be included * @param {Boolean} planetary True if Planetary Vehicle Hangar (PVH) should be included
*/ */
export function explorer(ship, planetary) { export function explorer (ship, planetary) {
let standardOpts = { ppRating: 'A' }, let standardOpts = {ppRating: 'A'},
heatSinkCount = 2, // Fit 2 heat sinks if possible heatSinkCount = 2, // Fit 2 heat sinks if possible
usedSlots = [], usedSlots = [],
sgSlot, sgSlot,
fuelScoopSlot, fuelScoopSlot,
sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass); sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass)
if (!planetary) { // Non-planetary explorers don't really need to boost if (!planetary) { // Non-planetary explorers don't really need to boost
standardOpts.pd = '1D'; standardOpts.pd = '1D'
} }
// Cargo hatch can be disabled // Cargo hatch can be disabled
ship.setSlotEnabled(ship.cargoHatch, false); ship.setSlotEnabled(ship.cargoHatch, false)
// Advanced Discovery Scanner - class 1 or higher // Advanced Discovery Scanner - class 1 or higher
const adsOrder = [1, 2, 3, 4, 5, 6, 7, 8]; const adsOrder = [1, 2, 3, 4, 5, 6, 7, 8]
const adsInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1) const adsInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
.filter(a => (!a.eligible) || a.eligible.sc) .filter(a => (!a.eligible) || a.eligible.sc)
.sort((a, b) => adsOrder.indexOf(a.maxClass) - adsOrder.indexOf(b.maxClass)); .sort((a, b) => adsOrder.indexOf(a.maxClass) - adsOrder.indexOf(b.maxClass))
for (let i = 0; i < adsInternals.length; i++) { for (let i = 0; i < adsInternals.length; i++) {
if (canMount(ship, adsInternals[i], 'sc')) { if (canMount(ship, adsInternals[i], 'sc')) {
ship.use(adsInternals[i], ModuleUtils.internal('2f')); ship.use(adsInternals[i], ModuleUtils.internal('2f'))
usedSlots.push(adsInternals[i]); usedSlots.push(adsInternals[i])
break; break
} }
} }
if (planetary) { if (planetary) {
// Planetary Vehicle Hangar - class 2 or higher // Planetary Vehicle Hangar - class 2 or higher
const pvhOrder = [2, 3, 4, 5, 6, 7, 8, 1]; const pvhOrder = [2, 3, 4, 5, 6, 7, 8, 1]
const pvhInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1) const pvhInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
.filter(a => (!a.eligible) || a.eligible.pv) .filter(a => (!a.eligible) || a.eligible.pv)
.sort((a, b) => pvhOrder.indexOf(a.maxClass) - pvhOrder.indexOf(b.maxClass)); .sort((a, b) => pvhOrder.indexOf(a.maxClass) - pvhOrder.indexOf(b.maxClass))
for (let i = 0; i < pvhInternals.length; i++) { for (let i = 0; i < pvhInternals.length; i++) {
if (canMount(ship, pvhInternals[i], 'pv')) { if (canMount(ship, pvhInternals[i], 'pv')) {
// Planetary Vehical Hangar only has even classes // Planetary Vehical Hangar only has even classes
const pvhClass = pvhInternals[i].maxClass % 2 === 1 ? pvhInternals[i].maxClass - 1 : pvhInternals[i].maxClass; const pvhClass = pvhInternals[i].maxClass % 2 === 1 ? pvhInternals[i].maxClass - 1 : pvhInternals[i].maxClass
ship.use(pvhInternals[i], ModuleUtils.findInternal('pv', pvhClass, 'G')); // G is lower mass ship.use(pvhInternals[i], ModuleUtils.findInternal('pv', pvhClass, 'G')) // G is lower mass
ship.setSlotEnabled(pvhInternals[i], false); // Disable power for Planetary Vehical Hangar ship.setSlotEnabled(pvhInternals[i], false) // Disable power for Planetary Vehical Hangar
usedSlots.push(pvhInternals[i]); usedSlots.push(pvhInternals[i])
break; break
} }
} }
} }
// Shield generator // Shield generator
const shieldOrder = [1, 2, 3, 4, 5, 6, 7, 8]; const shieldOrder = [1, 2, 3, 4, 5, 6, 7, 8]
const shieldInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1) const shieldInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
.filter(a => (!a.eligible) || a.eligible.sg) .filter(a => (!a.eligible) || a.eligible.sg)
.filter(a => a.maxClass >= sg.class) .filter(a => a.maxClass >= sg.class)
.sort((a, b) => shieldOrder.indexOf(a.maxClass) - shieldOrder.indexOf(b.maxClass)); .sort((a, b) => shieldOrder.indexOf(a.maxClass) - shieldOrder.indexOf(b.maxClass))
for (let i = 0; i < shieldInternals.length; i++) { for (let i = 0; i < shieldInternals.length; i++) {
if (canMount(ship, shieldInternals[i], 'sg')) { if (canMount(ship, shieldInternals[i], 'sg')) {
ship.use(shieldInternals[i], sg); ship.use(shieldInternals[i], sg)
usedSlots.push(shieldInternals[i]); usedSlots.push(shieldInternals[i])
sgSlot = shieldInternals[i]; sgSlot = shieldInternals[i]
break; break
} }
} }
// Detailed Surface Scanner // Detailed Surface Scanner
const dssOrder = [1, 2, 3, 4, 5, 6, 7, 8]; const dssOrder = [1, 2, 3, 4, 5, 6, 7, 8]
const dssInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1) const dssInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
.filter(a => (!a.eligible) || a.eligible.sc) .filter(a => (!a.eligible) || a.eligible.sc)
.sort((a, b) => dssOrder.indexOf(a.maxClass) - dssOrder.indexOf(b.maxClass)); .sort((a, b) => dssOrder.indexOf(a.maxClass) - dssOrder.indexOf(b.maxClass))
for (let i = 0; i < dssInternals.length; i++) { for (let i = 0; i < dssInternals.length; i++) {
if (canMount(ship, dssInternals[i], 'sc')) { if (canMount(ship, dssInternals[i], 'sc')) {
ship.use(dssInternals[i], ModuleUtils.internal('2i')); ship.use(dssInternals[i], ModuleUtils.internal('2i'))
usedSlots.push(dssInternals[i]); usedSlots.push(dssInternals[i])
break; break
} }
} }
// Fuel scoop - best possible // Fuel scoop - best possible
const fuelScoopOrder = [8, 7, 6, 5, 4, 3, 2, 1]; const fuelScoopOrder = [8, 7, 6, 5, 4, 3, 2, 1]
const fuelScoopInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1) const fuelScoopInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
.filter(a => (!a.eligible) || a.eligible.fs) .filter(a => (!a.eligible) || a.eligible.fs)
.sort((a, b) => fuelScoopOrder.indexOf(a.maxClass) - fuelScoopOrder.indexOf(b.maxClass)); .sort((a, b) => fuelScoopOrder.indexOf(a.maxClass) - fuelScoopOrder.indexOf(b.maxClass))
for (let i = 0; i < fuelScoopInternals.length; i++) { for (let i = 0; i < fuelScoopInternals.length; i++) {
if (canMount(ship, fuelScoopInternals[i], 'fs')) { if (canMount(ship, fuelScoopInternals[i], 'fs')) {
ship.use(fuelScoopInternals[i], ModuleUtils.findInternal('fs', fuelScoopInternals[i].maxClass, 'A')); ship.use(fuelScoopInternals[i], ModuleUtils.findInternal('fs', fuelScoopInternals[i].maxClass, 'A'))
usedSlots.push(fuelScoopInternals[i]); usedSlots.push(fuelScoopInternals[i])
fuelScoopSlot = fuelScoopInternals[i]; fuelScoopSlot = fuelScoopInternals[i]
break; break
} }
} }
// AFMUs - fill as they are 0-weight // AFMUs - fill as they are 0-weight
const afmuOrder = [8, 7, 6, 5, 4, 3, 2, 1]; const afmuOrder = [8, 7, 6, 5, 4, 3, 2, 1]
const afmuInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1) const afmuInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
.filter(a => (!a.eligible) || a.eligible.pc) .filter(a => (!a.eligible) || a.eligible.pc)
.sort((a, b) => afmuOrder.indexOf(a.maxClass) - afmuOrder.indexOf(b.maxClass)); .sort((a, b) => afmuOrder.indexOf(a.maxClass) - afmuOrder.indexOf(b.maxClass))
for (let i = 0; i < afmuInternals.length; i++) { for (let i = 0; i < afmuInternals.length; i++) {
if (canMount(ship, afmuInternals[i], 'am')) { if (canMount(ship, afmuInternals[i], 'am')) {
ship.use(afmuInternals[i], ModuleUtils.findInternal('am', afmuInternals[i].maxClass, 'A')); ship.use(afmuInternals[i], ModuleUtils.findInternal('am', afmuInternals[i].maxClass, 'A'))
usedSlots.push(afmuInternals[i]); usedSlots.push(afmuInternals[i])
ship.setSlotEnabled(afmuInternals[i], false); // Disable power for AFM Unit ship.setSlotEnabled(afmuInternals[i], false) // Disable power for AFM Unit
} }
} }
for (let s of ship.hardpoints) { for (let s of ship.hardpoints) {
if (s.maxClass == 0 && heatSinkCount) { // Mount up to 2 heatsinks if (s.maxClass == 0 && heatSinkCount) { // Mount up to 2 heatsinks
ship.use(s, ModuleUtils.hardpoints('02')); ship.use(s, ModuleUtils.hardpoints('02'))
ship.setSlotEnabled(s, heatSinkCount == 2); // Only enable a single Heatsink ship.setSlotEnabled(s, heatSinkCount == 2) // Only enable a single Heatsink
heatSinkCount--; heatSinkCount--
} else { } else {
ship.use(s, null); ship.use(s, null)
} }
} }
if (sgSlot && fuelScoopSlot) { if (sgSlot && fuelScoopSlot) {
// The SG and Fuel scoop to not need to be powered at the same time // The SG and Fuel scoop to not need to be powered at the same time
if (sgSlot.m.getPowerUsage() > fuelScoopSlot.m.getPowerUsage()) { // The Shield generator uses the most power if (sgSlot.m.getPowerUsage() > fuelScoopSlot.m.getPowerUsage()) { // The Shield generator uses the most power
ship.setSlotEnabled(fuelScoopSlot, false); ship.setSlotEnabled(fuelScoopSlot, false)
} else { // The Fuel scoop uses the most power } else { // The Fuel scoop uses the most power
ship.setSlotEnabled(sgSlot, false); ship.setSlotEnabled(sgSlot, false)
} }
} }
ship.useLightestStandard(standardOpts); ship.useLightestStandard(standardOpts)
} }
/** /**
@@ -213,188 +214,188 @@ export function explorer(ship, planetary) {
* @param {Ship} ship Ship instance * @param {Ship} ship Ship instance
* @param {Boolean} shielded True if shield generator should be included * @param {Boolean} shielded True if shield generator should be included
*/ */
export function miner(ship, shielded) { export function miner (ship, shielded) {
shielded = true; shielded = true
let standardOpts = { ppRating: 'A' }, let standardOpts = {ppRating: 'A'},
miningLaserCount = 2, miningLaserCount = 2,
usedSlots = [], usedSlots = [],
sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass); sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass)
// Cargo hatch should be enabled // Cargo hatch should be enabled
ship.setSlotEnabled(ship.cargoHatch, true); ship.setSlotEnabled(ship.cargoHatch, true)
// Largest possible refinery // Largest possible refinery
const refineryOrder = [4, 5, 6, 7, 8, 3, 2, 1]; const refineryOrder = [4, 5, 6, 7, 8, 3, 2, 1]
const refineryInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1) const refineryInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
.filter(a => (!a.eligible) || a.eligible.rf) .filter(a => (!a.eligible) || a.eligible.rf)
.sort((a, b) => refineryOrder.indexOf(a.maxClass) - refineryOrder.indexOf(b.maxClass)); .sort((a, b) => refineryOrder.indexOf(a.maxClass) - refineryOrder.indexOf(b.maxClass))
for (let i = 0; i < refineryInternals.length; i++) { for (let i = 0; i < refineryInternals.length; i++) {
if (canMount(ship, refineryInternals[i], 'rf')) { if (canMount(ship, refineryInternals[i], 'rf')) {
ship.use(refineryInternals[i], ModuleUtils.findInternal('rf', Math.min(refineryInternals[i].maxClass, 4), 'A')); ship.use(refineryInternals[i], ModuleUtils.findInternal('rf', Math.min(refineryInternals[i].maxClass, 4), 'A'))
usedSlots.push(refineryInternals[i]); usedSlots.push(refineryInternals[i])
break; break
} }
} }
// Prospector limpet controller - 3A if possible // Prospector limpet controller - 3A if possible
const prospectorOrder = [3, 4, 5, 6, 7, 8, 2, 1]; const prospectorOrder = [3, 4, 5, 6, 7, 8, 2, 1]
const prospectorInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1) const prospectorInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
.filter(a => (!a.eligible) || a.eligible.pc) .filter(a => (!a.eligible) || a.eligible.pc)
.sort((a, b) => prospectorOrder.indexOf(a.maxClass) - prospectorOrder.indexOf(b.maxClass)); .sort((a, b) => prospectorOrder.indexOf(a.maxClass) - prospectorOrder.indexOf(b.maxClass))
for (let i = 0; i < prospectorInternals.length; i++) { for (let i = 0; i < prospectorInternals.length; i++) {
if (canMount(ship, prospectorInternals[i], 'pc')) { if (canMount(ship, prospectorInternals[i], 'pc')) {
// Prospector only has odd classes // Prospector only has odd classes
const prospectorClass = prospectorInternals[i].maxClass % 2 === 0 ? prospectorInternals[i].maxClass - 1 : prospectorInternals[i].maxClass; const prospectorClass = prospectorInternals[i].maxClass % 2 === 0 ? prospectorInternals[i].maxClass - 1 : prospectorInternals[i].maxClass
ship.use(prospectorInternals[i], ModuleUtils.findInternal('pc', prospectorClass, 'A')); ship.use(prospectorInternals[i], ModuleUtils.findInternal('pc', prospectorClass, 'A'))
usedSlots.push(prospectorInternals[i]); usedSlots.push(prospectorInternals[i])
break; break
} }
} }
// Shield generator if required // Shield generator if required
if (shielded) { if (shielded) {
const shieldOrder = [1, 2, 3, 4, 5, 6, 7, 8]; const shieldOrder = [1, 2, 3, 4, 5, 6, 7, 8]
const shieldInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1) const shieldInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
.filter(a => (!a.eligible) || a.eligible.sg) .filter(a => (!a.eligible) || a.eligible.sg)
.filter(a => a.maxClass >= sg.class) .filter(a => a.maxClass >= sg.class)
.sort((a, b) => shieldOrder.indexOf(a.maxClass) - shieldOrder.indexOf(b.maxClass)); .sort((a, b) => shieldOrder.indexOf(a.maxClass) - shieldOrder.indexOf(b.maxClass))
for (let i = 0; i < shieldInternals.length; i++) { for (let i = 0; i < shieldInternals.length; i++) {
if (canMount(ship, shieldInternals[i], 'sg')) { if (canMount(ship, shieldInternals[i], 'sg')) {
ship.use(shieldInternals[i], sg); ship.use(shieldInternals[i], sg)
usedSlots.push(shieldInternals[i]); usedSlots.push(shieldInternals[i])
break; break
} }
} }
} }
// Dual mining lasers of highest possible class; remove anything else // Dual mining lasers of highest possible class; remove anything else
const miningLaserOrder = [2, 3, 4, 1, 0]; const miningLaserOrder = [2, 3, 4, 1, 0]
const miningLaserHardpoints = ship.hardpoints.concat().sort(function(a, b) { const miningLaserHardpoints = ship.hardpoints.concat().sort(function (a, b) {
return miningLaserOrder.indexOf(a.maxClass) - miningLaserOrder.indexOf(b.maxClass); return miningLaserOrder.indexOf(a.maxClass) - miningLaserOrder.indexOf(b.maxClass)
}); })
for (let s of miningLaserHardpoints) { for (let s of miningLaserHardpoints) {
if (s.maxClass >= 1 && miningLaserCount) { if (s.maxClass >= 1 && miningLaserCount) {
ship.use(s, ModuleUtils.hardpoints(s.maxClass >= 2 ? '2m' : '2l')); ship.use(s, ModuleUtils.hardpoints(s.maxClass >= 2 ? '2m' : '2l'))
miningLaserCount--; miningLaserCount--
} else { } else {
ship.use(s, null); ship.use(s, null)
} }
} }
// Number of collector limpets required to be active is a function of the size of the ship and the power of the lasers // Number of collector limpets required to be active is a function of the size of the ship and the power of the lasers
const miningLaserDps = ship.hardpoints.filter(h => h.m != null) const miningLaserDps = ship.hardpoints.filter(h => h.m != null)
.reduce(function(a, b) { .reduce(function (a, b) {
return a + b.m.getDps(); return a + b.m.getDps()
}, 0); }, 0)
// Find out how many internal slots we have, and their potential cargo size // Find out how many internal slots we have, and their potential cargo size
const potentialCargo = ship.internal.filter(a => usedSlots.indexOf(a) == -1) const potentialCargo = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
.filter(a => (!a.eligible) || a.eligible.cr) .filter(a => (!a.eligible) || a.eligible.cr)
.map(b => Math.pow(2, b.maxClass)); .map(b => Math.pow(2, b.maxClass))
// One collector for each 1.25 DPS, multiply by 1.25 for medium ships and 1.5 for large ships as they have further to travel // One collector for each 1.25 DPS, multiply by 1.25 for medium ships and 1.5 for large ships as they have further to travel
// 0 if we only have 1 cargo slot, otherwise minium of 1 and maximum of 6 (excluding size modifier) // 0 if we only have 1 cargo slot, otherwise minium of 1 and maximum of 6 (excluding size modifier)
const sizeModifier = ship.class == 2 ? 1.2 : ship.class == 3 ? 1.5 : 1; const sizeModifier = ship.class == 2 ? 1.2 : ship.class == 3 ? 1.5 : 1
let collectorLimpetsRequired = potentialCargo.length == 1 ? 0 : Math.ceil(sizeModifier * Math.min(6, Math.floor(miningLaserDps / 1.25))); let collectorLimpetsRequired = potentialCargo.length == 1 ? 0 : Math.ceil(sizeModifier * Math.min(6, Math.floor(miningLaserDps / 1.25)))
if (collectorLimpetsRequired > 0) { if (collectorLimpetsRequired > 0) {
const collectorOrder = [1, 2, 3, 4, 5, 6, 7, 8]; const collectorOrder = [1, 2, 3, 4, 5, 6, 7, 8]
const collectorInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1) const collectorInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
.filter(a => (!a.eligible) || a.eligible.cc) .filter(a => (!a.eligible) || a.eligible.cc)
.sort((a, b) => collectorOrder.indexOf(a.maxClass) - collectorOrder.indexOf(b.maxClass)); .sort((a, b) => collectorOrder.indexOf(a.maxClass) - collectorOrder.indexOf(b.maxClass))
// Always keep at least 2 slots free for cargo racks (1 for shielded) // Always keep at least 2 slots free for cargo racks (1 for shielded)
for (let i = 0; i < collectorInternals.length - (shielded ? 1 : 2) && collectorLimpetsRequired > 0; i++) { for (let i = 0; i < collectorInternals.length - (shielded ? 1 : 2) && collectorLimpetsRequired > 0; i++) {
if (canMount(ship, collectorInternals[i], 'cc')) { if (canMount(ship, collectorInternals[i], 'cc')) {
// Collector only has odd classes // Collector only has odd classes
const collectorClass = collectorInternals[i].maxClass % 2 === 0 ? collectorInternals[i].maxClass - 1 : collectorInternals[i].maxClass; const collectorClass = collectorInternals[i].maxClass % 2 === 0 ? collectorInternals[i].maxClass - 1 : collectorInternals[i].maxClass
ship.use(collectorInternals[i], ModuleUtils.findInternal('cc', collectorClass, 'D')); ship.use(collectorInternals[i], ModuleUtils.findInternal('cc', collectorClass, 'D'))
usedSlots.push(collectorInternals[i]); usedSlots.push(collectorInternals[i])
collectorLimpetsRequired -= collectorInternals[i].m.maximum; collectorLimpetsRequired -= collectorInternals[i].m.maximum
} }
} }
} }
// Power distributor to power the mining lasers indefinitely // Power distributor to power the mining lasers indefinitely
const wepRateRequired = ship.hardpoints.filter(h => h.m != null) const wepRateRequired = ship.hardpoints.filter(h => h.m != null)
.reduce(function(a, b) { .reduce(function (a, b) {
return a + b.m.getEps(); return a + b.m.getEps()
}, 0); }, 0)
standardOpts.pd = ship.getAvailableModules().matchingPowerDist({ weprate: wepRateRequired }).id; standardOpts.pd = ship.getAvailableModules().matchingPowerDist({weprate: wepRateRequired}).id
// Fill the empty internals with cargo racks // Fill the empty internals with cargo racks
for (let i = ship.internal.length; i--;) { for (let i = ship.internal.length; i--;) {
let slot = ship.internal[i]; let slot = ship.internal[i]
if (usedSlots.indexOf(slot) == -1 && canMount(ship, slot, 'cr')) { if (usedSlots.indexOf(slot) == -1 && canMount(ship, slot, 'cr')) {
ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E')); ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E'))
} }
} }
ship.useLightestStandard(standardOpts); ship.useLightestStandard(standardOpts)
} }
/** /**
* Racer Role * Racer Role
* @param {Ship} ship Ship instance * @param {Ship} ship Ship instance
*/ */
export function racer(ship) { export function racer (ship) {
let standardOpts = {}, let standardOpts = {},
usedSlots = [], usedSlots = [],
sgSlot, sgSlot,
sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass); sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass)
// Cargo hatch can be disabled // Cargo hatch can be disabled
ship.setSlotEnabled(ship.cargoHatch, false); ship.setSlotEnabled(ship.cargoHatch, false)
// Shield generator // Shield generator
const shieldOrder = [1, 2, 3, 4, 5, 6, 7, 8]; const shieldOrder = [1, 2, 3, 4, 5, 6, 7, 8]
const shieldInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1) const shieldInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1)
.filter(a => (!a.eligible) || a.eligible.sg) .filter(a => (!a.eligible) || a.eligible.sg)
.filter(a => a.maxClass >= sg.class) .filter(a => a.maxClass >= sg.class)
.sort((a, b) => shieldOrder.indexOf(a.maxClass) - shieldOrder.indexOf(b.maxClass)); .sort((a, b) => shieldOrder.indexOf(a.maxClass) - shieldOrder.indexOf(b.maxClass))
for (let i = 0; i < shieldInternals.length; i++) { for (let i = 0; i < shieldInternals.length; i++) {
if (canMount(ship, shieldInternals[i], 'sg')) { if (canMount(ship, shieldInternals[i], 'sg')) {
ship.use(shieldInternals[i], sg); ship.use(shieldInternals[i], sg)
usedSlots.push(shieldInternals[i]); usedSlots.push(shieldInternals[i])
sgSlot = shieldInternals[i]; sgSlot = shieldInternals[i]
break; break
} }
} }
// Empty the hardpoints // Empty the hardpoints
for (let s of ship.hardpoints) { for (let s of ship.hardpoints) {
ship.use(s, null); ship.use(s, null)
} }
// Empty the internals // Empty the internals
for (let i = ship.internal.length; i--;) { for (let i = ship.internal.length; i--;) {
let slot = ship.internal[i]; let slot = ship.internal[i]
if (usedSlots.indexOf(slot) == -1) { if (usedSlots.indexOf(slot) == -1) {
ship.use(slot, null); ship.use(slot, null)
} }
} }
// Best thrusters // Best thrusters
if (ship.standard[1].maxClass === 3) { if (ship.standard[1].maxClass === 3) {
standardOpts.th = 'tz'; standardOpts.th = 'tz'
} else if (ship.standard[1].maxClass === 2) { } else if (ship.standard[1].maxClass === 2) {
standardOpts.th = 'u0'; standardOpts.th = 'u0'
} else { } else {
standardOpts.th = ship.standard[1].maxClass + 'A'; standardOpts.th = ship.standard[1].maxClass + 'A'
} }
// Best power distributor for more boosting // Best power distributor for more boosting
standardOpts.pd = ship.standard[4].maxClass + 'A'; standardOpts.pd = ship.standard[4].maxClass + 'A'
// Smallest possible FSD drive // Smallest possible FSD drive
standardOpts.fsd = '2D'; standardOpts.fsd = '2D'
// Minimal fuel tank // Minimal fuel tank
standardOpts.ft = '1C'; standardOpts.ft = '1C'
// Disable nearly everything // Disable nearly everything
standardOpts.fsdDisabled = true; standardOpts.fsdDisabled = true
standardOpts.sDisabled = true; standardOpts.sDisabled = true
standardOpts.pdDisabled = true; standardOpts.pdDisabled = true
standardOpts.lsDisabled = true; standardOpts.lsDisabled = true
ship.useLightestStandard(standardOpts); ship.useLightestStandard(standardOpts)
// Apply engineering to each module // Apply engineering to each module
// ship.standard[1].m.blueprint = getBlueprint('Engine_Dirty', ship.standard[0]); // ship.standard[1].m.blueprint = getBlueprint('Engine_Dirty', ship.standard[0]);
@@ -413,3 +414,4 @@ export function racer(ship) {
// ship.standard[5].m.blueprint.grade = 5; // ship.standard[5].m.blueprint.grade = 5;
// setBest(ship, ship.standard[5].m); // setBest(ship, ship.standard[5].m);
} }

View File

@@ -1,83 +0,0 @@
export const SI_PREFIXES = {
'Y': 1e+24, // Yotta
'Z': 1e+21, // Zetta
'E': 1e+18, // Peta
'P': 1e+15, // Peta
'T': 1e+12, // Tera
'G': 1e+9, // Giga
'M': 1e+6, // Mega
'k': 1e+3, // Kilo
'h': 1e+2, // Hekto
'da': 1e+1, // Deka
'': 1,
'd': 1e-1, // Dezi
'c': 1e-2, // Zenti
'm': 1e-3, // Milli
'μ': 1e-6, // mikro not supported due to charset
'n': 10e-9, // Nano
'p': 1e-12, // Nano
'f': 1e-15, // Femto
'a': 1e-18, // Atto
'z': 1e-21, // Zepto
'y': 1e-24 // Yokto
};
export const STATS_FORMATTING = {
'ammo': { 'format': 'int', },
'boot': { 'format': 'int', 'unit': 'secs' },
'brokenregen': { 'format': 'round1', 'unit': 'ps' },
'burst': { 'format': 'int', 'change': 'additive' },
'burstrof': { 'format': 'round1', 'unit': 'ps', 'change': 'additive' },
'causres': { 'format': 'pct' },
'clip': { 'format': 'int' },
'damage': { 'format': 'round' },
'dps': { 'format': 'round', 'units': 'ps', 'synthetic': 'getDps' },
'dpe': { 'format': 'round', 'units': 'ps', 'synthetic': 'getDpe' },
'distdraw': { 'format': 'round', 'unit': 'MW' },
'duration': { 'format': 'round1', 'unit': 's' },
'eff': { 'format': 'round2' },
'engcap': { 'format': 'round1', 'unit': 'MJ' },
'engrate': { 'format': 'round1', 'unit': 'MW' },
'eps': { 'format': 'round', 'units': 'ps', 'synthetic': 'getEps' },
'explres': { 'format': 'pct' },
'facinglimit': { 'format': 'round1', 'unit': 'ang' },
'falloff': { 'format': 'round', 'unit': 'km', 'storedUnit': 'm' },
'fallofffromrange': { 'format': 'round', 'unit': 'km', 'storedUnit': 'm', 'synthetic': 'getFalloff' },
'hps': { 'format': 'round', 'units': 'ps', 'synthetic': 'getHps' },
'hullboost': { 'format': 'pct1', 'change': 'additive' },
'hullreinforcement': { 'format': 'int' },
'integrity': { 'format': 'round1' },
'jitter': { 'format': 'round', 'unit': 'ang' },
'kinres': { 'format': 'pct' },
'mass': { 'format': 'round1', 'unit': 'T' },
'maxfuel': { 'format': 'round1', 'unit': 'T' },
'optmass': { 'format': 'int', 'unit': 'T' },
'optmul': { 'format': 'pct', 'change': 'additive' },
'pgen': { 'format': 'round1', 'unit': 'MW' },
'piercing': { 'format': 'int' },
'power': { 'format': 'round', 'unit': 'MW' },
'protection': { 'format': 'pct' },
'range': { 'format': 'f2', 'unit': 'km', 'storedUnit': 'm' },
'ranget': { 'format': 'f1', 'unit': 's' },
'regen': { 'format': 'round1', 'unit': 'ps' },
'reload': { 'format': 'int', 'unit': 's' },
'rof': { 'format': 'round1', 'unit': 'ps', 'synthetic': 'getRoF', 'higherbetter': true },
'angle': { 'format': 'round1', 'unit': 'ang' },
'scanrate': { 'format': 'int' },
'scantime': { 'format': 'round1', 'unit': 's' },
'sdps': { 'format': 'round1', 'units': 'ps', 'synthetic': 'getSDps' },
'shield': { 'format': 'int', 'unit': 'MJ' },
'shieldaddition': { 'format': 'round1', 'unit': 'MJ' },
'shieldboost': { 'format': 'pct1', 'change': 'additive' },
'shieldreinforcement': { 'format': 'round1', 'unit': 'MJ' },
'shotspeed': { 'format': 'int', 'unit': 'm/s' },
'spinup': { 'format': 'round1', 'unit': 's' },
'syscap': { 'format': 'round1', 'unit': 'MJ' },
'sysrate': { 'format': 'round1', 'unit': 'MW' },
'thermload': { 'format': 'round1' },
'thermres': { 'format': 'pct' },
'wepcap': { 'format': 'round1', 'unit': 'MJ' },
'weprate': { 'format': 'round1', 'unit': 'MW' },
'jumpboost': { 'format': 'round1', 'unit': 'LY' },
'proberadius': { 'format': 'pct1', 'unit': 'pct' },
};

View File

@@ -1,33 +0,0 @@
import { Module } from 'ed-forge';
/**
* Sets a resistance value of a module as
* @param {Module} module Module to set the property
* @param {string} prop Property name; must end with 'resistance'
* @param {number} val Resistance value to set
*/
function setterResToEff(module, prop, val) {
module.set(
prop.replace('resistance', 'effectiveness'),
1 - val / 100,
);
}
export const SHOW = {
causticeffectiveness: {
as: 'causticresistance',
setter: setterResToEff,
},
explosiveeffectiveness: {
as: 'explosiveresistance',
setter: setterResToEff,
},
kineticeffectiveness: {
as: 'kineticresistance',
setter: setterResToEff,
},
thermiceffectiveness: {
as: 'thermicresistance',
setter: setterResToEff,
},
};

View File

@@ -5,7 +5,6 @@ const LS_KEY_BUILDS = 'builds';
const LS_KEY_COMPARISONS = 'comparisons'; const LS_KEY_COMPARISONS = 'comparisons';
const LS_KEY_LANG = 'NG_TRANSLATE_LANG_KEY'; const LS_KEY_LANG = 'NG_TRANSLATE_LANG_KEY';
const LS_KEY_COST_TAB = 'costTab'; const LS_KEY_COST_TAB = 'costTab';
const LS_KEY_CMDR_NAME = 'cmdrName';
const LS_KEY_OUTFITTING_TAB = 'outfittingTab'; const LS_KEY_OUTFITTING_TAB = 'outfittingTab';
const LS_KEY_INSURANCE = 'insurance'; const LS_KEY_INSURANCE = 'insurance';
const LS_KEY_SHIP_DISCOUNT = 'shipDiscount'; const LS_KEY_SHIP_DISCOUNT = 'shipDiscount';
@@ -14,8 +13,6 @@ const LS_KEY_STATE = 'state';
const LS_KEY_SIZE_RATIO = 'sizeRatio'; const LS_KEY_SIZE_RATIO = 'sizeRatio';
const LS_KEY_TOOLTIPS = 'tooltips'; const LS_KEY_TOOLTIPS = 'tooltips';
const LS_KEY_MODULE_RESISTANCES = 'moduleResistances'; const LS_KEY_MODULE_RESISTANCES = 'moduleResistances';
const LS_KEY_ROLLS = 'matsPerGrade';
const LS_KEY_ORBIS = 'orbis';
let LS; let LS;
@@ -70,6 +67,7 @@ function _delete(key) {
* export is an instance (see end of this file). * export is an instance (see end of this file).
*/ */
export class Persist extends EventEmitter { export class Persist extends EventEmitter {
/** /**
* Create an instance * Create an instance
*/ */
@@ -86,8 +84,6 @@ export class Persist extends EventEmitter {
} }
let moduleResistances = _get(LS_KEY_MODULE_RESISTANCES); let moduleResistances = _get(LS_KEY_MODULE_RESISTANCES);
let matsPerGrade = _get(LS_KEY_ROLLS);
let cmdrName = _get(LS_KEY_CMDR_NAME);
let tips = _get(LS_KEY_TOOLTIPS); let tips = _get(LS_KEY_TOOLTIPS);
let insurance = _getString(LS_KEY_INSURANCE); let insurance = _getString(LS_KEY_INSURANCE);
let shipDiscount = _get(LS_KEY_SHIP_DISCOUNT); let shipDiscount = _get(LS_KEY_SHIP_DISCOUNT);
@@ -95,7 +91,6 @@ export class Persist extends EventEmitter {
let buildJson = _get(LS_KEY_BUILDS); let buildJson = _get(LS_KEY_BUILDS);
let comparisonJson = _get(LS_KEY_COMPARISONS); let comparisonJson = _get(LS_KEY_COMPARISONS);
this.orbisCreds = _get(LS_KEY_ORBIS) || { email: '', password: '' };
this.onStorageChange = this.onStorageChange.bind(this); this.onStorageChange = this.onStorageChange.bind(this);
this.langCode = _getString(LS_KEY_LANG) || 'en'; this.langCode = _getString(LS_KEY_LANG) || 'en';
this.insurance = insurance && Insurance[insurance.toLowerCase()] !== undefined ? insurance : 'standard'; this.insurance = insurance && Insurance[insurance.toLowerCase()] !== undefined ? insurance : 'standard';
@@ -107,14 +102,6 @@ export class Persist extends EventEmitter {
this.outfittingTab = _getString(LS_KEY_OUTFITTING_TAB); this.outfittingTab = _getString(LS_KEY_OUTFITTING_TAB);
this.state = _get(LS_KEY_STATE); this.state = _get(LS_KEY_STATE);
this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1; this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1;
this.matsPerGrade = matsPerGrade || {
1: 2,
2: 2,
3: 4,
4: 4,
5: 10
};
this.cmdrName = cmdrName || { selected: '', cmdrs: [] };
this.tooltipsEnabled = tips === null ? true : tips; this.tooltipsEnabled = tips === null ? true : tips;
this.moduleResistancesEnabled = moduleResistances === null ? true : moduleResistances; this.moduleResistancesEnabled = moduleResistances === null ? true : moduleResistances;
@@ -165,14 +152,6 @@ export class Persist extends EventEmitter {
this.moduleResistancesEnabled = !!newValue && newValue.toLowerCase() == 'true'; this.moduleResistancesEnabled = !!newValue && newValue.toLowerCase() == 'true';
this.emit('moduleresistances', this.moduleResistancesEnabled); this.emit('moduleresistances', this.moduleResistancesEnabled);
break; break;
case LS_KEY_ROLLS:
this.matsPerGrade = JSON.parse(newValue);
this.emit('matsPerGrade', this.matsPerGrade);
break;
case LS_KEY_ORBIS:
this.orbisCreds = JSON.parse(newValue);
this.emit('orbis', this.orbisCreds);
break;
} }
} catch (e) { } catch (e) {
// On JSON.Parse Error - don't sync or do anything // On JSON.Parse Error - don't sync or do anything
@@ -198,24 +177,6 @@ export class Persist extends EventEmitter {
this.emit('language', langCode); this.emit('language', langCode);
} }
/**
* Get the current orbis.zone credentials
* @return {String} language code
*/
getOrbisCreds() {
return this.orbisCreds;
};
/**
* Update and save the orbis.zone credentials
* @param {Object} creds object with username and password properties.
*/
setOrbisCreds(creds) {
this.langCode = creds;
_put(LS_KEY_ORBIS, creds);
this.emit('orbis', creds);
}
/** /**
* Show tooltips setting * Show tooltips setting
* @param {boolean} show Optional - update setting * @param {boolean} show Optional - update setting
@@ -496,31 +457,6 @@ export class Persist extends EventEmitter {
return this.moduleDiscount; return this.moduleDiscount;
} }
/**
* Get the saved ship discount
* @param {Object} matsPerGrade # of rolls per grade
*/
setRolls(matsPerGrade) {
this.matsPerGrade = matsPerGrade;
_put(LS_KEY_ROLLS, this.matsPerGrade);
this.emit('matsPerGrade');
}
/**
* Get the saved Mats per grade
* @return {Object} # of rolls per grade
*/
getRolls() {
return this.matsPerGrade;
}
/**
* Get the saved Mats per grade
* @return {Object} # of rolls per grade
*/
getCmdr() {
return this.cmdrName;
}
/** /**
* Persist selected cost tab * Persist selected cost tab
* @param {number} tabName Cost tab name * @param {number} tabName Cost tab name
@@ -530,16 +466,6 @@ export class Persist extends EventEmitter {
_put(LS_KEY_COST_TAB, tabName); _put(LS_KEY_COST_TAB, tabName);
} }
/**
* Persist cmdr name
* @param {Object} cmdrName Commander name for EDEngineer
*/
setCmdr(cmdrName) {
this.cmdrName = cmdrName;
_put(LS_KEY_CMDR_NAME, cmdrName);
this.emit('cmdr');
}
/** /**
* Get the saved discount * Get the saved discount
* @return {number} val Discount value/amount * @return {number} val Discount value/amount

View File

@@ -1,43 +1,61 @@
import React from 'react'; import React from 'react';
import { Modifications } from 'coriolis-data/dist'; import { Modifications } from 'coriolis-data/dist';
import { Module } from 'ed-forge';
import { getBlueprintInfo, getExperimentalInfo } from 'ed-forge/lib/data/blueprints';
import { entries, keys, uniq } from 'lodash';
/** /**
* Generate a tooltip with details of a blueprint's specials * Generate a tooltip with details of a blueprint's specials
* @param {Object} language The translate object * @param {Object} translate The translate object
* @param {Module} m The module to compare with * @param {Object} blueprint The blueprint at the required grade
* @param {string} specialName The name of the special * @param {string} grp The group of the module
* @param {Object} m The module to compare with
* @param specialName
* @returns {Object} The react components * @returns {Object} The react components
*/ */
export function specialToolTip(language, m, specialName) { export function specialToolTip(translate, blueprint, grp, m, specialName) {
const { formats, translate } = language; const effects = [];
if (!blueprint || !blueprint.features) {
return undefined;
}
if (m) {
// We also add in any benefits from specials that aren't covered above
if (m.blueprint) {
for (const feature in Modifications.modifierActions[specialName]) {
// if (!blueprint.features[feature] && !m.mods.feature) {
const featureDef = Modifications.modifications[feature];
if (featureDef && !featureDef.hidden) {
let symbol = '';
if (feature === 'jitter') {
symbol = '°';
} else if (featureDef.type === 'percentage') {
symbol = '%';
}
let current = m.getModValue(feature) - m.getModValue(feature, true);
if (featureDef.type === 'percentage') {
current = Math.round(current / 10) / 10;
} else if (featureDef.type === 'numeric') {
current /= 100;
}
const currentIsBeneficial = isValueBeneficial(feature, current);
effects.push(
<tr key={feature + '_specialTT'}>
<td style={{textAlign: 'left'}}>{translate(feature, grp)}</td>
<td>&nbsp;</td>
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'}
style={{textAlign: 'right'}}>{current}{symbol}</td>
<td>&nbsp;</td>
</tr>
);
}
}
}
}
return ( return (
<div> <div>
<table width='100%'> <table width='100%'>
<tbody> <tbody>
{entries(getExperimentalInfo(specialName).features).map( {effects}
([prop, feats]) => {
const { max, only } = feats;
if (only && !m.getItem().match(only)) {
return null;
}
const { value, unit, beneficial } = m.getModifierFormatted(prop);
// If the product of value and min/max is positive, both values
// point into the same direction, i.e. positive/negative.
const specialBeneficial = (value * max) > 0 === beneficial;
return <tr key={prop + '_specialTT'}>
<td style={{ textAlign: 'left' }}>{translate(prop)}</td>
<td>&nbsp;</td>
<td className={specialBeneficial ? 'secondary' : 'warning'}
style={{ textAlign: 'right' }}>{formats.round(max * 100)}{unit}</td>
<td>&nbsp;</td>
</tr>;
}
)}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -45,70 +63,224 @@ export function specialToolTip(language, m, specialName) {
} }
/** /**
* Generate a tooltip with details and preview of a blueprint's effects * Generate a tooltip with details of a blueprint's effects
* @param {Object} language The language object * @param {Object} translate The translate object
* @param {Module} m The module to compare with * @param {Object} blueprint The blueprint at the required grade
* @param {string} previewBP Blueprint to preview * @param {Array} engineers The engineers supplying this blueprint
* @param {number} previewGrade Grade to preview * @param {string} grp The group of the module
* @returns {Object} The react components * @param {Object} m The module to compare with
* @returns {Object} The react components
*/ */
export function blueprintTooltip(language, m, previewBP, previewGrade) { export function blueprintTooltip(translate, blueprint, engineers, grp, m) {
const { translate, formats } = language; const effects = [];
const blueprint = previewBP || m.getBlueprint(); if (!blueprint || !blueprint.features) {
const grade = previewGrade || m.getBlueprintGrade(); return undefined;
if (!blueprint) { }
return null; for (const feature in blueprint.features) {
const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]);
const featureDef = Modifications.modifications[feature];
if (!featureDef.hidden) {
let symbol = '';
if (feature === 'jitter') {
symbol = '°';
} else if (featureDef.type === 'percentage') {
symbol = '%';
}
let lowerBound = blueprint.features[feature][0];
let upperBound = blueprint.features[feature][1];
if (featureDef.type === 'percentage') {
lowerBound = Math.round(lowerBound * 1000) / 10;
upperBound = Math.round(upperBound * 1000) / 10;
}
const lowerIsBeneficial = isValueBeneficial(feature, lowerBound);
const upperIsBeneficial = isValueBeneficial(feature, upperBound);
if (m) {
// We have a module - add in the current value
let current = m.getModValue(feature);
if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
current = Math.round(current / 10) / 10;
} else if (featureDef.type === 'numeric') {
current /= 100;
}
const currentIsBeneficial = isValueBeneficial(feature, current);
effects.push(
<tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td className={lowerBound === 0 ? '' : lowerIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{lowerBound}{symbol}</td>
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td>
<td className={upperBound === 0 ? '' : upperIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{upperBound}{symbol}</td>
</tr>
);
} else {
// We do not have a module, no value
effects.push(
<tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td className={lowerBound === 0 ? '' : lowerIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{lowerBound}{symbol}</td>
<td className={upperBound === 0 ? '' : upperIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{upperBound}{symbol}</td>
</tr>
);
}
}
}
if (m) {
// Because we have a module add in any benefits that aren't part of the primary blueprint
for (const feature in m.mods) {
if (!blueprint.features[feature]) {
const featureDef = Modifications.modifications[feature];
if (featureDef && !featureDef.hidden) {
let symbol = '';
if (feature === 'jitter') {
symbol = '°';
} else if (featureDef.type === 'percentage') {
symbol = '%';
}
let current = m.getModValue(feature);
if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
current = Math.round(current / 10) / 10;
} else if (featureDef.type === 'numeric') {
current /= 100;
}
const currentIsBeneficial = isValueBeneficial(feature, current);
effects.push(
<tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td>&nbsp;</td>
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td>
<td>&nbsp;</td>
</tr>
);
}
}
}
// We also add in any benefits from specials that aren't covered above
if (m.blueprint && m.blueprint.special) {
for (const feature in Modifications.modifierActions[m.blueprint.special.edname]) {
if (!blueprint.features[feature] && !m.mods.feature) {
const featureDef = Modifications.modifications[feature];
if (featureDef && !featureDef.hidden) {
let symbol = '';
if (feature === 'jitter') {
symbol = '°';
} else if (featureDef.type === 'percentage') {
symbol = '%';
}
let current = m.getModValue(feature);
if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
current = Math.round(current / 10) / 10;
} else if (featureDef.type === 'numeric') {
current /= 100;
}
const currentIsBeneficial = isValueBeneficial(feature, current);
effects.push(
<tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td>&nbsp;</td>
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td>
<td>&nbsp;</td>
</tr>
);
}
}
}
}
} }
const bpFeatures = getBlueprintInfo(blueprint).features[grade]; let components;
const features = uniq(m.getModifiedProperties().concat(keys(bpFeatures))); if (!m) {
components = [];
for (const component in blueprint.components) {
components.push(
<tr key={component}>
<td style={{ textAlign: 'left' }}>{translate(component)}</td>
<td style={{ textAlign: 'right' }}>{blueprint.components[component]}</td>
</tr>
);
}
}
let engineersList;
if (engineers) {
engineersList = [];
for (const engineer of engineers) {
engineersList.push(
<tr key={engineer}>
<td style={{ textAlign: 'left' }}>{engineer}</td>
</tr>
);
}
}
return ( return (
<div> <div>
<table width='100%'> <table width='100%'>
<thead> <thead>
<tr> <tr>
<td>{translate('feature')}</td> <td>{translate('feature')}</td>
<td>{translate('worst')}</td> <td>{translate('worst')}</td>
<td>{translate('current')}</td> {m ? <td>{translate('current')}</td> : null }
<td>{translate('best')}</td> <td>{translate('best')}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{features.map((prop) => { {effects}
const { min, max, only } = bpFeatures[prop] || {};
// Skip this property if it doesn't apply to this module
if (only && !m.getItem().match(only)) {
return null;
}
const { value, unit, beneficial } = m.getModifierFormatted(prop);
if (!bpFeatures[prop] && !value) {
// Can happen for exported synthetics
return null;
}
// If the product of value and min/max is positive, both values
// point into the same direction, i.e. positive/negative.
const minBeneficial = (value * min) > 0 === beneficial;
const maxBeneficial = (value * max) > 0 === beneficial;
return (<tr key={prop}>
<td style={{ textAlign: 'left' }}>{translate(prop)}</td>
<td className={!min ? '' : minBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>
{!isNaN(min) && formats.round(min * 100)}{!isNaN(min) && unit}
</td>
<td className={!value ? '' : beneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>
{formats.round(value || 0)}{unit}
</td>
<td className={!max ? '' : maxBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>
{!isNaN(max) && formats.round(max * 100)}{!isNaN(max) && unit}
</td>
</tr>);
})}
</tbody> </tbody>
</table> </table>
{ components ? <table width='100%'>
<thead>
<tr>
<td>{translate('component')}</td>
<td>{translate('amount')}</td>
</tr>
</thead>
<tbody>
{components}
</tbody>
</table> : null }
{ engineersList ? <table width='100%'>
<thead>
<tr>
<td>{translate('engineers')}</td>
</tr>
</thead>
<tbody>
{engineersList}
</tbody>
</table> : null }
</div> </div>
); );
} }
/**
* Is this blueprint feature beneficial?
* @param {string} feature The name of the feature
* @param {array} values The value of the feature
* @returns {boolean} True if this feature is beneficial
*/
export function isBeneficial(feature, values) {
const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0));
if (Modifications.modifications[feature].higherbetter) {
return !fact;
} else {
return fact;
}
}
/**
* Is this feature value beneficial?
* @param {string} feature The name of the feature
* @param {number} value The value of the feature
* @returns {boolean} True if this value is beneficial
*/
export function isValueBeneficial(feature, value) {
if (Modifications.modifications[feature].higherbetter) {
return value > 0;
} else {
return value < 0;
}
}
/** /**
* Get a blueprint with a given name and an optional module * Get a blueprint with a given name and an optional module
* @param {string} name The name of the blueprint * @param {string} name The name of the blueprint
@@ -117,11 +289,143 @@ export function blueprintTooltip(language, m, previewBP, previewGrade) {
*/ */
export function getBlueprint(name, module) { export function getBlueprint(name, module) {
// Start with a copy of the blueprint // Start with a copy of the blueprint
const findMod = val => Object.keys(Modifications.blueprints).find(elem => elem.toString().toLowerCase().search(val.toString().toLowerCase().replace(/(OutfittingFieldType_|persecond)/igm, '')) >= 0); const findMod = val => Object.keys(Modifications.blueprints).find(elem => elem.toString().toLowerCase().search(val.toString().toLowerCase().replace(/(OutfittingFieldType_|persecond)/igm, '')) >= 0)
const found = Modifications.blueprints[findMod(name)]; const found = Modifications.blueprints[findMod(name)];
if (!found || !found.fdname) { if (!found || !found.fdname) {
return {}; return {};
} }
const blueprint = JSON.parse(JSON.stringify(found)); const blueprint = JSON.parse(JSON.stringify(found));
if (module) {
if (module.grp === 'sb') {
// Shield boosters are treated internally as straight modifiers, so rather than (for example)
// being a 4% boost they are a 104% multiplier. We need to fix the values here so that they look
// accurate as per the information in Elite
for (const grade in blueprint.grades) {
for (const feature in blueprint.grades[grade].features) {
if (feature === 'shieldboost') {
blueprint.grades[grade].features[feature][0] = ((1 + blueprint.grades[grade].features[feature][0]) * (1 + module.shieldboost) - 1) / module.shieldboost - 1;
blueprint.grades[grade].features[feature][1] = ((1 + blueprint.grades[grade].features[feature][1]) * (1 + module.shieldboost) - 1) / module.shieldboost - 1;
}
}
}
}
}
return blueprint; return blueprint;
} }
/**
* Provide 'percent' primary modifications
* @param {Object} ship The ship for which to perform the modifications
* @param {Object} m The module for which to perform the modifications
* @param {Number} percent The percent to set values to of full.
*/
export function setPercent(ship, m, percent) {
ship.clearModifications(m);
// Pick given value as multiplier
const mult = percent / 100;
const features = m.blueprint.grades[m.blueprint.grade].features;
for (const featureName in features) {
let value;
if (Modifications.modifications[featureName].higherbetter) {
// Higher is better, but is this making it better or worse?
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * mult);
} else {
value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * mult);
}
} else {
// Higher is worse, but is this making it better or worse?
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * mult);
} else {
value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * mult);
}
}
_setValue(ship, m, featureName, value);
}
}
/**
* Provide 'random' primary modifications
* @param {Object} ship The ship for which to perform the modifications
* @param {Object} m The module for which to perform the modifications
*/
export function setRandom(ship, m) {
// Pick a single value for our randomness
setPercent(ship, m, Math.random() * 100);
}
/**
* Set a modification feature value
* @param {Object} ship The ship for which to perform the modifications
* @param {Object} m The module for which to perform the modifications
* @param {string} featureName The feature being set
* @param {number} value The value being set for the feature
*/
function _setValue(ship, m, featureName, value) {
if (Modifications.modifications[featureName].type == 'percentage') {
ship.setModification(m, featureName, value * 10000);
} else if (Modifications.modifications[featureName].type == 'numeric') {
ship.setModification(m, featureName, value * 100);
} else {
ship.setModification(m, featureName, value);
}
}
/**
* Provide 'percent' primary query
* @param {Object} m The module for which to perform the query
* @returns {Number} percent The percentage indicator of current applied values.
*/
export function getPercent(m) {
let result = null;
const features = m.blueprint.grades[m.blueprint.grade].features;
for (const featureName in features) {
if (features[featureName][0] === features[featureName][1]) {
continue;
}
let value = _getValue(m, featureName);
let mult;
if (Modifications.modifications[featureName].higherbetter) {
// Higher is better, but is this making it better or worse?
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100);
} else {
mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100);
}
} else {
// Higher is worse, but is this making it better or worse?
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100);
} else {
mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100);
}
}
if (result && result != mult) {
return null;
} else if (result != mult) {
result = mult;
}
}
return result;
}
/**
* Query a feature value
* @param {Object} m The module for which to perform the query
* @param {string} featureName The feature being queried
*/
function _getValue(m, featureName) {
if (Modifications.modifications[featureName].type == 'percentage') {
return m.getModValue(featureName, true) / 10000;
} else if (Modifications.modifications[featureName].type == 'numeric') {
return m.getModValue(featureName, true) / 100;
} else {
return m.getModValue(featureName, true);
}
}

View File

@@ -6,7 +6,7 @@ import { getBlueprint } from '../utils/BlueprintFunctions';
import * as ModuleUtils from '../shipyard/ModuleUtils'; import * as ModuleUtils from '../shipyard/ModuleUtils';
// mapping from fd's ship model names to coriolis' // mapping from fd's ship model names to coriolis'
export const SHIP_FD_NAME_TO_CORIOLIS_NAME = { const SHIP_FD_NAME_TO_CORIOLIS_NAME = {
'Adder': 'adder', 'Adder': 'adder',
'Anaconda': 'anaconda', 'Anaconda': 'anaconda',
'Asp': 'asp', 'Asp': 'asp',
@@ -29,9 +29,6 @@ export const SHIP_FD_NAME_TO_CORIOLIS_NAME = {
'FerDeLance': 'fer_de_lance', 'FerDeLance': 'fer_de_lance',
'Hauler': 'hauler', 'Hauler': 'hauler',
'Independant_Trader': 'keelback', 'Independant_Trader': 'keelback',
'Krait_MkII': 'krait_mkii',
'Mamba': 'mamba',
'Krait_Light': 'krait_phantom',
'Orca': 'orca', 'Orca': 'orca',
'Python': 'python', 'Python': 'python',
'SideWinder': 'sidewinder', 'SideWinder': 'sidewinder',
@@ -40,8 +37,6 @@ export const SHIP_FD_NAME_TO_CORIOLIS_NAME = {
'Type9': 'type_9_heavy', 'Type9': 'type_9_heavy',
'Type9_Military': 'type_10_defender', 'Type9_Military': 'type_10_defender',
'TypeX': 'alliance_chieftain', 'TypeX': 'alliance_chieftain',
'TypeX_2': 'alliance_crusader',
'TypeX_3': 'alliance_challenger',
'Viper': 'viper', 'Viper': 'viper',
'Viper_MkIV': 'viper_mk_iv', 'Viper_MkIV': 'viper_mk_iv',
'Vulture': 'vulture' 'Vulture': 'vulture'
@@ -323,7 +318,7 @@ function _addModifications(module, modifiers, blueprint, grade, specialModificat
if (!modifiers) return; if (!modifiers) return;
let special; let special;
if (specialModifications) { if (specialModifications) {
special = Modifications.specials[Object.keys(specialModifications)[0]]; special = Modifications.specials[Object.keys(specialModifications)[0]]
} }
for (const i in modifiers) { for (const i in modifiers) {
// Some special modifications // Some special modifications
@@ -350,7 +345,7 @@ function _addModifications(module, modifiers, blueprint, grade, specialModificat
let value; let value;
if (i === 'OutfittingFieldType_DefenceModifierShieldMultiplier') { if (i === 'OutfittingFieldType_DefenceModifierShieldMultiplier') {
value = modifiers[i].value - 1; value = modifiers[i].value - 1;
} else if (i === 'OutfittingFieldType_DefenceModifierHealthMultiplier' && blueprint.startsWith('Armour_')) { } else if (i === 'OutfittingFieldType_DefenceModifierHealthMultiplier' && blueprint.startsWith('Armour_')) {
value = (modifiers[i].value - module.hullboost) / module.hullboost; value = (modifiers[i].value - module.hullboost) / module.hullboost;
} else if (i === 'OutfittingFieldType_DefenceModifierHealthMultiplier') { } else if (i === 'OutfittingFieldType_DefenceModifierHealthMultiplier') {
value = modifiers[i].value / module.hullboost; value = modifiers[i].value / module.hullboost;

Some files were not shown because too many files have changed in this diff Show More