mirror of
https://github.com/EDCD/coriolis.git
synced 2025-12-08 22:33:24 +00:00
Continued porting to react, approaching beta
This commit is contained in:
19
.babelrc
19
.babelrc
@@ -1,20 +1,3 @@
|
||||
{
|
||||
"stage": 0,
|
||||
"env": {
|
||||
"development": {
|
||||
"plugins": ["react-transform"],
|
||||
"extra": {
|
||||
"react-transform": {
|
||||
"transforms": [{
|
||||
"transform": "react-transform-hmr",
|
||||
"imports": ["react"],
|
||||
"locals": ["module"]
|
||||
}, {
|
||||
"transform": "react-transform-catch-errors",
|
||||
"imports": ["react", "redbox-react"]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"presets": ["es2015", "react", "stage-0"]
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"$schema": "http://cdn.coriolis.io/schemas/ship-loadout/2.json#",
|
||||
"$schema": "http://cdn.coriolis.io/schemas/ship-loadout/3.json#",
|
||||
"name": "Test",
|
||||
"ship": "Anaconda",
|
||||
"references": [
|
||||
{
|
||||
"name": "Coriolis.io",
|
||||
"url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.Iw18QDBNA%3D%3D%3D.AwhMJBGaei%2BJCyyiA%3D%3D%3D?bn=Test",
|
||||
"code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.Iw18QDBNA===.AwhMJBGaei+JCyyiA===",
|
||||
"url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA?bn=Test?bn=Test",
|
||||
"code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA",
|
||||
"shipId": "anaconda"
|
||||
}
|
||||
],
|
||||
@@ -264,12 +264,12 @@
|
||||
"masslock": 23,
|
||||
"pipSpeed": 0.14,
|
||||
"shipCostMultiplier": 1,
|
||||
"componentCostMultiplier": 1,
|
||||
"moduleCostMultiplier": 1,
|
||||
"fuelCapacity": 32,
|
||||
"cargoCapacity": 128,
|
||||
"ladenMass": 1339.2,
|
||||
"armour": 2078,
|
||||
"armourAdded": 240,
|
||||
"armour": 2228,
|
||||
"armourAdded": 390,
|
||||
"armourMultiplier": 1.95,
|
||||
"shieldMultiplier": 1.4,
|
||||
"totalCost": 882362060,
|
||||
@@ -1,4 +1,4 @@
|
||||
describe('Import Controller', function() {
|
||||
xdescribe('Import Controller', function() {
|
||||
beforeEach(module('app'));
|
||||
|
||||
var importController, $rootScope, $stateParams, scope;
|
||||
54
__tests__/test-service-serializer.js
Normal file
54
__tests__/test-service-serializer.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import Ship from '../src/app/shipyard/Ship';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import * as Serializer from '../src/app/shipyard/Serializer';
|
||||
|
||||
describe("Serializer Service", function() {
|
||||
|
||||
const anacondaTestExport = require.requireActual('./fixtures/anaconda-test-detailed-export-v3');
|
||||
const code = anacondaTestExport.references[0].code;
|
||||
const anaconda = Ships.anaconda;
|
||||
|
||||
describe("To Detailed Build", function() {
|
||||
|
||||
let testBuild, exportData;
|
||||
|
||||
beforeEach(function() {
|
||||
testBuild = new Ship('anaconda', anaconda.properties, anaconda.slots);
|
||||
testBuild.buildFrom(code);
|
||||
exportData = Serializer.toDetailedBuild('Test', testBuild);
|
||||
});
|
||||
|
||||
xit("conforms to the v2 ship-loadout schema", function() {
|
||||
// var validate = jsen(require('../schemas/ship-loadout/3'));
|
||||
// var valid = validate(exportData);
|
||||
expect(valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it("contains the correct components and stats", function() {
|
||||
expect(exportData.components).toEqual(anacondaTestExport.components);
|
||||
expect(exportData.stats).toEqual(anacondaTestExport.stats);
|
||||
expect(exportData.ship).toEqual(anacondaTestExport.ship);
|
||||
expect(exportData.name).toEqual(anacondaTestExport.name);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("From Detailed Build", function() {
|
||||
|
||||
it("builds the ship correctly", function() {
|
||||
let testBuildA = new Ship('anaconda', anaconda.properties, anaconda.slots);
|
||||
testBuildA.buildFrom(code);
|
||||
let testBuildB = Serializer.fromDetailedBuild(anacondaTestExport);
|
||||
|
||||
for(var p in testBuildB) {
|
||||
if (p == 'availCS') {
|
||||
continue;
|
||||
}
|
||||
expect(testBuildB[p]).toEqual(testBuildA[p], p + ' does not match');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,20 +1,15 @@
|
||||
import Ship from '../src/app/shipyard/Ship';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import * as ModuleUtils from '../src/app/shipyard/ModuleUtils';
|
||||
|
||||
describe("Ship Factory", function() {
|
||||
|
||||
var Ship;
|
||||
var Components;
|
||||
|
||||
beforeEach(module('shipyard'));
|
||||
beforeEach(inject(['Ship', 'Components', function (_Ship_, _Components_) {
|
||||
Ship = _Ship_;
|
||||
Components = _Components_;
|
||||
}]));
|
||||
|
||||
it("can build all ships", function() {
|
||||
for (var s in DB.ships) {
|
||||
var shipData = DB.ships[s];
|
||||
var ship = new Ship(s, shipData.properties, shipData.slots);
|
||||
for (let s in Ships) {
|
||||
let shipData = Ships[s];
|
||||
let ship = new Ship(s, shipData.properties, shipData.slots);
|
||||
|
||||
for (p in shipData.properties) {
|
||||
for (let p in shipData.properties) {
|
||||
expect(ship[p]).toEqual(shipData.properties[p], s + ' property [' + p + '] does not match when built');
|
||||
}
|
||||
|
||||
@@ -37,7 +32,7 @@ describe("Ship Factory", function() {
|
||||
|
||||
it("resets and rebuilds properly", function() {
|
||||
var id = 'cobra_mk_iii';
|
||||
var cobra = DB.ships[id];
|
||||
var cobra = Ships[id];
|
||||
var shipA = new Ship(id, cobra.properties, cobra.slots);
|
||||
var shipB = new Ship(id, cobra.properties, cobra.slots);
|
||||
var testShip = new Ship(id, cobra.properties, cobra.slots);
|
||||
@@ -81,7 +76,7 @@ describe("Ship Factory", function() {
|
||||
|
||||
it("discounts hull and components properly", function() {
|
||||
var id = 'cobra_mk_iii';
|
||||
var cobra = DB.ships[id];
|
||||
var cobra = Ships[id];
|
||||
var testShip = new Ship(id, cobra.properties, cobra.slots);
|
||||
testShip.buildWith(cobra.defaults);
|
||||
|
||||
@@ -89,76 +84,76 @@ describe("Ship Factory", function() {
|
||||
var originalTotalCost = testShip.totalCost;
|
||||
var discount = 0.9;
|
||||
|
||||
expect(testShip.c.discountedCost).toEqual(originalHullCost, 'Hull cost does not match');
|
||||
expect(testShip.m.discountedCost).toEqual(originalHullCost, 'Hull cost does not match');
|
||||
|
||||
testShip.applyDiscounts(discount, discount);
|
||||
|
||||
// Floating point errors cause miniscule decimal places which are handled in the app by rounding/formatting
|
||||
|
||||
expect(Math.floor(testShip.c.discountedCost)).toEqual(Math.floor(originalHullCost * discount), 'Discounted Hull cost does not match');
|
||||
expect(Math.floor(testShip.m.discountedCost)).toEqual(Math.floor(originalHullCost * discount), 'Discounted Hull cost does not match');
|
||||
expect(Math.floor(testShip.totalCost)).toEqual(Math.floor(originalTotalCost * discount), 'Discounted Total cost does not match');
|
||||
|
||||
testShip.applyDiscounts(1, 1); // No discount, 100% of cost
|
||||
|
||||
expect(testShip.c.discountedCost).toEqual(originalHullCost, 'Hull cost does not match');
|
||||
expect(testShip.m.discountedCost).toEqual(originalHullCost, 'Hull cost does not match');
|
||||
expect(testShip.totalCost).toEqual(originalTotalCost, 'Total cost does not match');
|
||||
|
||||
testShip.applyDiscounts(discount, 1); // Only discount hull
|
||||
|
||||
expect(Math.floor(testShip.c.discountedCost)).toEqual(Math.round(originalHullCost * discount), 'Discounted Hull cost does not match');
|
||||
expect(testShip.totalCost).toEqual(originalTotalCost - originalHullCost + testShip.c.discountedCost, 'Total cost does not match');
|
||||
expect(Math.floor(testShip.m.discountedCost)).toEqual(Math.round(originalHullCost * discount), 'Discounted Hull cost does not match');
|
||||
expect(testShip.totalCost).toEqual(originalTotalCost - originalHullCost + testShip.m.discountedCost, 'Total cost does not match');
|
||||
|
||||
});
|
||||
|
||||
it("enforces a single shield generator", function() {
|
||||
var id = 'anaconda';
|
||||
var anacondaData = DB.ships[id];
|
||||
var anacondaData = Ships[id];
|
||||
var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots);
|
||||
anaconda.buildWith(anacondaData.defaults);
|
||||
|
||||
expect(anaconda.internal[2].c.grp).toEqual('sg', 'Anaconda default shield generator slot');
|
||||
expect(anaconda.internal[2].m.grp).toEqual('sg', 'Anaconda default shield generator slot');
|
||||
|
||||
anaconda.use(anaconda.internal[1], '4j', Components.internal('4j')); // 6E Shield Generator
|
||||
anaconda.use(anaconda.internal[1], ModuleUtils.internal('4j')); // 6E Shield Generator
|
||||
|
||||
expect(anaconda.internal[2].c).toEqual(null, 'Anaconda default shield generator slot is empty');
|
||||
expect(anaconda.internal[2].id).toEqual(null, 'Anaconda default shield generator slot id is null');
|
||||
expect(anaconda.internal[1].id).toEqual('4j', 'Slot 1 should have SG 4j in it');
|
||||
expect(anaconda.internal[1].c.grp).toEqual('sg','Slot 1 should have SG 4j in it');
|
||||
expect(anaconda.internal[2].m).toEqual(null, 'Anaconda default shield generator slot id is null');
|
||||
expect(anaconda.internal[1].m.id).toEqual('4j', 'Slot 1 should have SG 4j in it');
|
||||
expect(anaconda.internal[1].m.grp).toEqual('sg','Slot 1 should have SG 4j in it');
|
||||
|
||||
});
|
||||
|
||||
it("enforces a single shield fuel scoop", function() {
|
||||
var id = 'anaconda';
|
||||
var anacondaData = DB.ships[id];
|
||||
var anacondaData = Ships[id];
|
||||
var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots);
|
||||
anaconda.buildWith(anacondaData.defaults);
|
||||
|
||||
anaconda.use(anaconda.internal[4], '32', Components.internal('32')); // 4A Fuel Scoop
|
||||
expect(anaconda.internal[4].c.grp).toEqual('fs', 'Anaconda fuel scoop slot');
|
||||
anaconda.use(anaconda.internal[4], ModuleUtils.internal('32')); // 4A Fuel Scoop
|
||||
expect(anaconda.internal[4].m.grp).toEqual('fs', 'Anaconda fuel scoop slot');
|
||||
|
||||
anaconda.use(anaconda.internal[3], '32', Components.internal('32'));
|
||||
anaconda.use(anaconda.internal[3], ModuleUtils.internal('32'));
|
||||
|
||||
expect(anaconda.internal[4].c).toEqual(null, 'Anaconda original fuel scoop slot is empty');
|
||||
expect(anaconda.internal[4].id).toEqual(null, 'Anaconda original fuel scoop slot id is null');
|
||||
expect(anaconda.internal[3].id).toEqual('32', 'Slot 1 should have FS 32 in it');
|
||||
expect(anaconda.internal[3].c.grp).toEqual('fs','Slot 1 should have FS 32 in it');
|
||||
expect(anaconda.internal[4].m).toEqual(null, 'Anaconda original fuel scoop slot id is null');
|
||||
expect(anaconda.internal[3].m.id).toEqual('32', 'Slot 1 should have FS 32 in it');
|
||||
expect(anaconda.internal[3].m.grp).toEqual('fs','Slot 1 should have FS 32 in it');
|
||||
});
|
||||
|
||||
it("enforces a single refinery", function() {
|
||||
var id = 'anaconda';
|
||||
var anacondaData = DB.ships[id];
|
||||
var anacondaData = Ships[id];
|
||||
var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots);
|
||||
anaconda.buildWith(anacondaData.defaults);
|
||||
|
||||
anaconda.use(anaconda.internal[4], '23', Components.internal('23')); // 4E Refinery
|
||||
expect(anaconda.internal[4].c.grp).toEqual('rf', 'Anaconda refinery slot');
|
||||
anaconda.use(anaconda.internal[4], ModuleUtils.internal('23')); // 4E Refinery
|
||||
expect(anaconda.internal[4].m.grp).toEqual('rf', 'Anaconda refinery slot');
|
||||
|
||||
anaconda.use(anaconda.internal[3], '23', Components.internal('23'));
|
||||
anaconda.use(anaconda.internal[3], ModuleUtils.internal('23'));
|
||||
|
||||
expect(anaconda.internal[4].c).toEqual(null, 'Anaconda original refinery slot is empty');
|
||||
expect(anaconda.internal[4].id).toEqual(null, 'Anaconda original refinery slot id is null');
|
||||
expect(anaconda.internal[3].id).toEqual('23', 'Slot 1 should have RF 23 in it');
|
||||
expect(anaconda.internal[3].c.grp).toEqual('rf','Slot 1 should have RF 23 in it');
|
||||
expect(anaconda.internal[4].m).toEqual(null, 'Anaconda original refinery slot id is null');
|
||||
expect(anaconda.internal[3].m.id).toEqual('23', 'Slot 1 should have RF 23 in it');
|
||||
expect(anaconda.internal[3].m.grp).toEqual('rf','Slot 1 should have RF 23 in it');
|
||||
});
|
||||
|
||||
});
|
||||
56
package.json
56
package.json
@@ -8,39 +8,67 @@
|
||||
"homepage": "http://coriolis.io",
|
||||
"bugs": "https://github.com/cmmcleod/coriolis/issues",
|
||||
"private": true,
|
||||
"engine": "node >= 0.12.2",
|
||||
"engine": "node >= 4.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf build",
|
||||
"start": "node devServer.js",
|
||||
"lint": "eslint --ext .js,.jsx src",
|
||||
"test": "jest",
|
||||
"prod-serve": "nginx -p $(pwd) -c nginx.conf",
|
||||
"prod-stop": "kill -QUIT $(cat nginx.pid)",
|
||||
"build:prod": "npm run clean && NODE_ENV=production CDN='//cdn.coriolis.io' webpack -d -p --config webpack.config.prod.js",
|
||||
"build": "npm run clean && NODE_ENV=production webpack -d -p --config webpack.config.prod.js",
|
||||
"rsync": "rsync -e 'ssh -i $CORIOLIS_PEM' -a --delete build/ $CORIOLIS_USER@$CORIOLIS_HOST:~/www",
|
||||
"deploy": "npm run lint && npm run build && npm run rsync"
|
||||
"deploy": "npm run lint && npm test && npm run build:prod && npm run rsync"
|
||||
},
|
||||
"jest": {
|
||||
"scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
|
||||
"testFileExtensions": [
|
||||
"js"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"jsx"
|
||||
],
|
||||
"unmockedModulePathPatterns": [
|
||||
"<rootDir>/node_modules/react",
|
||||
"<rootDir>/node_modules/react-dom",
|
||||
"<rootDir>/node_modules/react-addons-test-utils",
|
||||
"<rootDir>/node_modules/fbjs",
|
||||
"<rootDir>/node_modules/fbemitter",
|
||||
"<rootDir>/node_modules/classnames",
|
||||
"<rootDir>/node_modules/d3",
|
||||
"<rootDir>/node_modules/lz-string",
|
||||
"<rootDir>/node_modules/coriolis-data",
|
||||
"<rootDir>/src/app/shipyard",
|
||||
"<rootDir>/src/app/i18n",
|
||||
"<rootDir>/src/app/utils"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"appcache-webpack-plugin": "^1.2.1",
|
||||
"babel-core": "^5.4.7",
|
||||
"babel-eslint": "^4.1.6",
|
||||
"babel-loader": "^5.1.2",
|
||||
"babel-plugin-react-transform": "^1.1.1",
|
||||
"css-loader": "^0.23.0",
|
||||
"eslint": "^1.10.1",
|
||||
"eslint-plugin-react": "^2.3.0",
|
||||
"babel-core": "*",
|
||||
"babel-eslint": "*",
|
||||
"babel-jest": "*",
|
||||
"babel-loader": "*",
|
||||
"babel-preset-es2015": "^6.3.13",
|
||||
"babel-preset-react": "^6.3.13",
|
||||
"babel-preset-stage-0": "^6.3.13",
|
||||
"css-loader": "^0.23.0",
|
||||
"eslint": "2.0.0-beta.1",
|
||||
"eslint-plugin-react": "^3.15.0",
|
||||
"expose-loader": "^0.7.1",
|
||||
"express": "^4.13.3",
|
||||
"extract-text-webpack-plugin": "^0.9.1",
|
||||
"file-loader": "^0.8.4",
|
||||
"html-webpack-plugin": "^1.7.0",
|
||||
"jest-cli": "*",
|
||||
"json-loader": "^0.5.3",
|
||||
"less": "^2.5.3",
|
||||
"less-loader": "^2.2.1",
|
||||
"react-transform-catch-errors": "^1.0.0",
|
||||
"react-transform-hmr": "^1.0.0",
|
||||
"redbox-react": "^1.0.1",
|
||||
"react-addons-test-utils": "^0.14.6",
|
||||
"rimraf": "^2.4.3",
|
||||
"style-loader": "^0.13.0",
|
||||
"url-loader": "^0.5.6",
|
||||
@@ -52,8 +80,8 @@
|
||||
"d3": "^3.5.9",
|
||||
"fbemitter": "^2.0.0",
|
||||
"lz-string": "^1.4.4",
|
||||
"react": "^0.14.2",
|
||||
"react-dom": "^0.14.2",
|
||||
"react": "^0.14.6",
|
||||
"react-dom": "^0.14.6",
|
||||
"superagent": "^1.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
import React from 'react';
|
||||
import Router from './Router';
|
||||
import { EventEmitter } from 'fbemitter';
|
||||
import { getLanguage } from './i18n/Language';
|
||||
import Persist from './stores/Persist';
|
||||
import InterfaceEvents from './utils/InterfaceEvents';
|
||||
|
||||
import Header from './components/Header';
|
||||
import Tooltip from './components/Tooltip';
|
||||
|
||||
import AboutPage from './pages/AboutPage';
|
||||
import NotFoundPage from './pages/NotFoundPage';
|
||||
import OutfittingPage from './pages/OutfittingPage';
|
||||
import ComparisonPage from './pages/ComparisonPage';
|
||||
import ShipyardPage from './pages/ShipyardPage';
|
||||
|
||||
/**
|
||||
* Coriolis App
|
||||
*/
|
||||
export default class Coriolis extends React.Component {
|
||||
|
||||
static childContextTypes = {
|
||||
language: React.PropTypes.object.isRequired,
|
||||
sizeRatio: React.PropTypes.number.isRequired,
|
||||
route: React.PropTypes.object.isRequired
|
||||
route: React.PropTypes.object.isRequired,
|
||||
openMenu: React.PropTypes.func.isRequired,
|
||||
closeMenu: React.PropTypes.func.isRequired,
|
||||
showModal: React.PropTypes.func.isRequired,
|
||||
hideModal: React.PropTypes.func.isRequired,
|
||||
tooltip: React.PropTypes.func.isRequired,
|
||||
termtip: React.PropTypes.func.isRequired,
|
||||
onWindowResize: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an instance of the Coriolis App
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this._setPage = this._setPage.bind(this);
|
||||
@@ -26,10 +41,14 @@ export default class Coriolis extends React.Component {
|
||||
this._closeMenu = this._closeMenu.bind(this);
|
||||
this._showModal = this._showModal.bind(this);
|
||||
this._hideModal = this._hideModal.bind(this);
|
||||
this._tooltip = this._tooltip.bind(this);
|
||||
this._termtip = this._termtip.bind(this);
|
||||
this._onWindowResize = this._onWindowResize.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.emitter = new EventEmitter();
|
||||
this.state = {
|
||||
page: null,
|
||||
language: getLanguage(Persist.getLangCode()),
|
||||
@@ -45,23 +64,49 @@ export default class Coriolis extends React.Component {
|
||||
Router('*', (r) => this._setPage(null, r));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates / Sets the page and route context
|
||||
* @param {[type]} page The page to be shown
|
||||
* @param {Object} route The current route
|
||||
*/
|
||||
_setPage(page, route) {
|
||||
this.setState({ page, route, currentMenu: null });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle unexpected error
|
||||
* TODO: Implement and fix to work with Webpack (dev + prod)
|
||||
* @param {string} msg Message
|
||||
* @param {string} scriptUrl URL
|
||||
* @param {number} line Line number
|
||||
* @param {number} col Column number
|
||||
* @param {Object} errObj Error Object
|
||||
*/
|
||||
_onError(msg, scriptUrl, line, col, errObj) {
|
||||
console.log('WINDOW ERROR', arguments);
|
||||
//this._setPage(<div>Some errors occured!!</div>);
|
||||
// this._setPage(<div>Some errors occured!!</div>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate language and format changes
|
||||
* @param {string} lang Language code
|
||||
*/
|
||||
_onLanguageChange(lang) {
|
||||
this.setState({ language: getLanguage(Persist.getLangCode()) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate the sizeRatio change
|
||||
* @param {number} sizeRatio Size ratio / scale
|
||||
*/
|
||||
_onSizeRatioChange(sizeRatio) {
|
||||
this.setState({ sizeRatio });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Key Down
|
||||
* @param {Event} e Keyboard Event
|
||||
*/
|
||||
_keyDown(e) {
|
||||
switch (e.keyCode) {
|
||||
case 27:
|
||||
@@ -108,6 +153,43 @@ export default class Coriolis extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show/Hide the tooltip
|
||||
* @param {React.Component} content Tooltip content
|
||||
* @param {DOMRect} rect Target bounding rect
|
||||
* @param {[type]} opts Options
|
||||
*/
|
||||
_tooltip(content, rect, opts) {
|
||||
if (!content && this.state.tooltip) {
|
||||
this.setState({ tooltip: null });
|
||||
} else if (content && Persist.showTooltips()) {
|
||||
this.setState({ tooltip: <Tooltip rect={rect} options={opts}>{content}</Tooltip> });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the term tip
|
||||
* @param {string} term Term
|
||||
* @param {[type]} orientation Tooltip orientation (n,e,s,w)
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_termtip(term, orientation, event) {
|
||||
if (typeof orientation != 'string') {
|
||||
event = orientation;
|
||||
orientation = null;
|
||||
}
|
||||
this._tooltip(<div className='cap cen'>{this.state.language.translate(term)}</div>, event.currentTarget.getBoundingClientRect(), { orientation });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener to on window resize
|
||||
* @param {Function} listener Listener callback
|
||||
* @return {Object} Subscription token
|
||||
*/
|
||||
_onWindowResize(listener) {
|
||||
return this.emitter.addListener('windowResize', listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the context to be passed down to pages / components containing
|
||||
* language, sizeRatio and route references
|
||||
@@ -117,7 +199,14 @@ export default class Coriolis extends React.Component {
|
||||
return {
|
||||
language: this.state.language,
|
||||
route: this.state.route,
|
||||
sizeRatio: this.state.sizeRatio
|
||||
sizeRatio: this.state.sizeRatio,
|
||||
openMenu: this._openMenu,
|
||||
closeMenu: this._closeMenu,
|
||||
showModal: this._showModal,
|
||||
hideModal: this._hideModal,
|
||||
tooltip: this._tooltip,
|
||||
termtip: this._termtip,
|
||||
onWindowResize: this._onWindowResize
|
||||
};
|
||||
}
|
||||
|
||||
@@ -135,14 +224,11 @@ export default class Coriolis extends React.Component {
|
||||
}
|
||||
|
||||
window.onerror = this._onError.bind(this);
|
||||
window.addEventListener('resize', InterfaceEvents.windowResized);
|
||||
window.addEventListener('resize', () => this.emitter.emit('windowResize'));
|
||||
document.body.addEventListener('scroll', () => this._tooltip());
|
||||
document.addEventListener('keydown', this._keyDown);
|
||||
Persist.addListener('language', this._onLanguageChange);
|
||||
Persist.addListener('sizeRatio', this._onSizeRatioChange);
|
||||
InterfaceEvents.addListener('openMenu', this._openMenu);
|
||||
InterfaceEvents.addListener('closeMenu', this._closeMenu);
|
||||
InterfaceEvents.addListener('showModal', this._showModal);
|
||||
InterfaceEvents.addListener('hideModal', this._hideModal);
|
||||
|
||||
Router.start();
|
||||
}
|
||||
@@ -157,6 +243,7 @@ export default class Coriolis extends React.Component {
|
||||
<Header appCacheUpdate={this.state.appCacheUpdate} currentMenu={this.state.currentMenu} />
|
||||
{ this.state.page ? <this.state.page currentMenu={this.state.currentMenu} /> : <NotFoundPage/> }
|
||||
{ this.state.modal }
|
||||
{ this.state.tooltip }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import Persist from './stores/Persist';
|
||||
|
||||
/**
|
||||
* Determine if the app is running in mobile/tablet 'standalone' mode
|
||||
* @return {Boolean} True if the app is in standalone mode
|
||||
*/
|
||||
function isStandAlone() {
|
||||
try {
|
||||
return window.navigator.standalone || (window.external && window.external.msIsSiteMode && window.external.msIsSiteMode());
|
||||
@@ -9,8 +13,7 @@ function isStandAlone() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register `path` with callback `fn()`,
|
||||
* or route `path`, or `Router.start()`.
|
||||
* Register path with callback fn(), or route path`, or Router.start().
|
||||
*
|
||||
* Router('*', fn);
|
||||
* Router('/user/:id', load, user);
|
||||
@@ -18,15 +21,15 @@ function isStandAlone() {
|
||||
* Router('/user/' + user.id);
|
||||
* Router();
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {Function} fn...
|
||||
* @param {String} path path
|
||||
* @param {Function} fn Callbacks (fn, fn, ...)
|
||||
* @api public
|
||||
*/
|
||||
function Router(path, fn) {
|
||||
var route = new Route(path);
|
||||
for (var i = 1; i < arguments.length; ++i) {
|
||||
Router.callbacks.push(route.middleware(arguments[i]));
|
||||
}
|
||||
let route = new Route(path);
|
||||
for (let i = 1; i < arguments.length; ++i) {
|
||||
Router.callbacks.push(route.middleware(arguments[i]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,20 +38,19 @@ function Router(path, fn) {
|
||||
|
||||
Router.callbacks = [];
|
||||
|
||||
Router.start = function(){
|
||||
Router.start = function() {
|
||||
window.addEventListener('popstate', onpopstate, false);
|
||||
|
||||
if (isStandAlone()) {
|
||||
var state = Persist.getState();
|
||||
let state = Persist.getState();
|
||||
// If a previous state has been stored, load that state
|
||||
if (state && state.name && state.params) {
|
||||
Router(this.props.initialPath || '/');
|
||||
//$state.go(state.name, state.params, { location: 'replace' });
|
||||
} else {
|
||||
Router('/');
|
||||
}
|
||||
} else {
|
||||
var url = location.pathname + location.search;
|
||||
let url = location.pathname + location.search;
|
||||
Router.replace(url, null, true, true);
|
||||
}
|
||||
};
|
||||
@@ -56,14 +58,14 @@ Router.start = function(){
|
||||
/**
|
||||
* Show `path` with optional `state` object.
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {Object} state
|
||||
* @return {Context}
|
||||
* @param {String} path Path
|
||||
* @param {Object} state Additional state
|
||||
* @return {Context} New Context
|
||||
* @api public
|
||||
*/
|
||||
Router.go = function(path, state) {
|
||||
gaTrack(path);
|
||||
var ctx = new Context(path, state);
|
||||
let ctx = new Context(path, state);
|
||||
Router.dispatch(ctx);
|
||||
if (!ctx.unhandled) {
|
||||
history.pushState(ctx.state, ctx.title, ctx.canonicalPath);
|
||||
@@ -74,15 +76,15 @@ Router.go = function(path, state) {
|
||||
/**
|
||||
* Replace `path` with optional `state` object.
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {Object} state
|
||||
* @return {Context}
|
||||
* @param {String} path path
|
||||
* @param {Object} state State
|
||||
* @param {Boolean} dispatch If true dispatch the route / trigger update
|
||||
* @return {Context} New Context
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Router.replace = function(path, state, dispatch) {
|
||||
gaTrack(path);
|
||||
var ctx = new Context(path, state);
|
||||
let ctx = new Context(path, state);
|
||||
if (dispatch) Router.dispatch(ctx);
|
||||
history.replaceState(ctx.state, ctx.title, ctx.canonicalPath);
|
||||
return ctx;
|
||||
@@ -91,15 +93,18 @@ Router.replace = function(path, state, dispatch) {
|
||||
/**
|
||||
* Dispatch the given `ctx`.
|
||||
*
|
||||
* @param {Object} ctx
|
||||
* @param {Context} ctx Context
|
||||
* @api private
|
||||
*/
|
||||
Router.dispatch = function(ctx) {
|
||||
let i = 0;
|
||||
|
||||
Router.dispatch = function(ctx){
|
||||
var i = 0;
|
||||
|
||||
/**
|
||||
* Handle the next route
|
||||
* @return {Function} Unhandled
|
||||
*/
|
||||
function next() {
|
||||
var fn = Router.callbacks[i++];
|
||||
let fn = Router.callbacks[i++];
|
||||
if (!fn) return unhandled(ctx);
|
||||
fn(ctx, next);
|
||||
}
|
||||
@@ -112,12 +117,12 @@ Router.dispatch = function(ctx){
|
||||
* popstate then redirect. If you wish to handle
|
||||
* 404s on your own use `Router('*', callback)`.
|
||||
*
|
||||
* @param {Context} ctx
|
||||
* @param {Context} ctx Context
|
||||
* @return {Context} context
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function unhandled(ctx) {
|
||||
var current = window.location.pathname + window.location.search;
|
||||
let current = window.location.pathname + window.location.search;
|
||||
if (current != ctx.canonicalPath) {
|
||||
window.location = ctx.canonicalPath;
|
||||
}
|
||||
@@ -128,13 +133,12 @@ function unhandled(ctx) {
|
||||
* Initialize a new "request" `Context`
|
||||
* with the given `path` and optional initial `state`.
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {Object} state
|
||||
* @param {String} path Path
|
||||
* @param {Object} state State
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function Context(path, state) {
|
||||
var i = path.indexOf('?');
|
||||
let i = path.indexOf('?');
|
||||
|
||||
this.canonicalPath = path;
|
||||
this.path = path || '/';
|
||||
@@ -160,33 +164,28 @@ function Context(path, state) {
|
||||
* - `sensitive` enable case-sensitive routes
|
||||
* - `strict` enable strict matching for trailing slashes
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {Object} options.
|
||||
* @param {String} path Path
|
||||
* @param {Object} options Options
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function Route(path, options) {
|
||||
options = options || {};
|
||||
this.path = path;
|
||||
this.method = 'GET';
|
||||
this.regexp = pathtoRegexp(path
|
||||
, this.keys = []
|
||||
, options.sensitive
|
||||
, options.strict);
|
||||
this.regexp = pathtoRegexp(path, this.keys = [], options.sensitive, options.strict);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return route middleware with
|
||||
* the given callback `fn()`.
|
||||
*
|
||||
* @param {Function} fn
|
||||
* @return {Function}
|
||||
* @param {Function} fn Route function
|
||||
* @return {Function} Callback
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Route.prototype.middleware = function(fn){
|
||||
var self = this;
|
||||
return function(ctx, next){
|
||||
Route.prototype.middleware = function(fn) {
|
||||
let self = this;
|
||||
return function(ctx, next) {
|
||||
if (self.match(ctx.path, ctx.params)) return fn(ctx, next);
|
||||
next();
|
||||
};
|
||||
@@ -196,24 +195,23 @@ Route.prototype.middleware = function(fn){
|
||||
* Check if this route matches `path`, if so
|
||||
* populate `params`.
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {Array} params
|
||||
* @return {Boolean}
|
||||
* @param {String} path Path
|
||||
* @param {Array} params Path params
|
||||
* @return {Boolean} True if path matches
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Route.prototype.match = function(path, params){
|
||||
var keys = this.keys
|
||||
, qsIndex = path.indexOf('?')
|
||||
, pathname = ~qsIndex ? path.slice(0, qsIndex) : path
|
||||
, m = this.regexp.exec(decodeURIComponent(pathname));
|
||||
Route.prototype.match = function(path, params) {
|
||||
let keys = this.keys,
|
||||
qsIndex = path.indexOf('?'),
|
||||
pathname = ~qsIndex ? path.slice(0, qsIndex) : path,
|
||||
m = this.regexp.exec(decodeURIComponent(pathname));
|
||||
|
||||
if (!m) return false;
|
||||
|
||||
for (var i = 1, len = m.length; i < len; ++i) {
|
||||
var key = keys[i - 1];
|
||||
for (let i = 1, len = m.length; i < len; ++i) {
|
||||
let key = keys[i - 1];
|
||||
|
||||
var val = 'string' == typeof m[i] ? decodeURIComponent(m[i]) : m[i];
|
||||
let val = 'string' == typeof m[i] ? decodeURIComponent(m[i]) : m[i];
|
||||
|
||||
if (key) {
|
||||
params[key.name] = undefined !== params[key.name] ? params[key.name] : val;
|
||||
@@ -223,22 +221,9 @@ Route.prototype.match = function(path, params){
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Check if the app is running in stand alone mode.
|
||||
* @return {Boolean} true if running in Standalone mode
|
||||
*/
|
||||
function isStandAlone() {
|
||||
try {
|
||||
return window.navigator.standalone || (window.external && window.external.msIsSiteMode && window.external.msIsSiteMode());
|
||||
} catch (ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a page view in Google Analytics
|
||||
* @param {string} path
|
||||
* @param {string} path Path to track
|
||||
*/
|
||||
function gaTrack(path) {
|
||||
if (window.ga) {
|
||||
@@ -255,29 +240,28 @@ function gaTrack(path) {
|
||||
* key names. For example "/user/:id" will
|
||||
* then contain ["id"].
|
||||
*
|
||||
* @param {String|RegExp|Array} path
|
||||
* @param {Array} keys
|
||||
* @param {Boolean} sensitive
|
||||
* @param {Boolean} strict
|
||||
* @return {RegExp}
|
||||
* @param {String|RegExp|Array} path Path template(s)
|
||||
* @param {Array} keys keys
|
||||
* @param {Boolean} sensitive Case sensitive
|
||||
* @param {Boolean} strict Strict matching
|
||||
* @return {RegExp} Regular expression
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function pathtoRegexp(path, keys, sensitive, strict) {
|
||||
if (path instanceof RegExp) return path;
|
||||
if (path instanceof Array) path = '(' + path.join('|') + ')';
|
||||
path = path
|
||||
.concat(strict ? '' : '/?')
|
||||
.replace(/\/\(/g, '(?:/')
|
||||
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){
|
||||
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional) {
|
||||
keys.push({ name: key, optional: !! optional });
|
||||
slash = slash || '';
|
||||
return ''
|
||||
+ (optional ? '' : slash)
|
||||
+ '(?:'
|
||||
+ (optional ? slash : '')
|
||||
+ (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
|
||||
+ (optional || '');
|
||||
return '' +
|
||||
(optional ? '' : slash) +
|
||||
'(?:' +
|
||||
(optional ? slash : '') +
|
||||
(format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' +
|
||||
(optional || '');
|
||||
})
|
||||
.replace(/([\/.])/g, '\\$1')
|
||||
.replace(/\*/g, '(.*)');
|
||||
@@ -286,11 +270,11 @@ function pathtoRegexp(path, keys, sensitive, strict) {
|
||||
|
||||
/**
|
||||
* Handle "populate" events.
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
|
||||
function onpopstate(e) {
|
||||
if (e.state) {
|
||||
var path = e.state.path;
|
||||
let path = e.state.path;
|
||||
Router.replace(path, e.state, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,31 @@ import React from 'react';
|
||||
import Link from './Link';
|
||||
import cn from 'classnames';
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the current window location equals the link
|
||||
* @return {boolean} If matches
|
||||
*/
|
||||
function isActive(href) {
|
||||
return encodeURI(href) == (window.location.pathname + window.location.search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Active Link - Highlighted when URL matches window location
|
||||
*/
|
||||
export default class ActiveLink extends Link {
|
||||
|
||||
isActive = () => {
|
||||
return encodeURI(this.props.href) == (window.location.pathname + window.location.search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the component
|
||||
* @return {React.Component} The active link
|
||||
*/
|
||||
render() {
|
||||
let className = this.props.className;
|
||||
if (this.isActive()) {
|
||||
if (isActive(this.props.href)) {
|
||||
className = cn(className, 'active');
|
||||
}
|
||||
|
||||
return <a {...this.props} className={className} onClick={this.handler}>{this.props.children}</a>
|
||||
return <a {...this.props} className={className} onClick={this.handler.bind(this)}>{this.props.children}</a>;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,11 +4,15 @@ import TranslatedComponent from './TranslatedComponent';
|
||||
import cn from 'classnames';
|
||||
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
|
||||
|
||||
/**
|
||||
* Available modules menu
|
||||
*/
|
||||
export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
modules: React.PropTypes.oneOfType([ React.PropTypes.object, React.PropTypes.array ]).isRequired,
|
||||
modules: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.array]).isRequired,
|
||||
onSelect: React.PropTypes.func.isRequired,
|
||||
diffDetails: React.PropTypes.func,
|
||||
m: React.PropTypes.object,
|
||||
shipMass: React.PropTypes.number,
|
||||
warning: React.PropTypes.func
|
||||
@@ -18,23 +22,83 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
shipMass: 0
|
||||
};
|
||||
|
||||
buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules) {
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this._hideDiff = this._hideDiff.bind(this);
|
||||
this.state = { list: this._initList(props, context) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the list of available moduels
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
* @return {Array} Array of React Components
|
||||
*/
|
||||
_initList(props, context) {
|
||||
let translate = context.language.translate;
|
||||
let { m, warning, shipMass, onSelect, modules } = props;
|
||||
let list;
|
||||
let buildGroup = this._buildGroup.bind(
|
||||
this,
|
||||
translate,
|
||||
m,
|
||||
warning,
|
||||
shipMass - (m && m.mass ? m.mass : 0),
|
||||
(m) => {
|
||||
this._hideDiff();
|
||||
onSelect(m);
|
||||
}
|
||||
);
|
||||
|
||||
if (modules instanceof Array) {
|
||||
list = buildGroup(modules[0].grp, modules);
|
||||
} else {
|
||||
list = [];
|
||||
// At present time slots with grouped options (Hardpoints and Internal) can be empty
|
||||
list.push(<div className={'empty-c upp'} key={'empty'} onClick={onSelect.bind(null, null)} >{translate('empty')}</div>);
|
||||
for (let g in modules) {
|
||||
list.push(<div ref={g} key={g} className={'select-group cap'}>{translate(g)}</div>);
|
||||
list.push(buildGroup(g, modules[g]));
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate React Components for Module Group
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Objecy} mountedModule Mounted Module
|
||||
* @param {Funciton} warningFunc Warning function
|
||||
* @param {number} mass Mass
|
||||
* @param {function} onSelect Select/Mount callback
|
||||
* @param {string} grp Group name
|
||||
* @param {Array} modules Available modules
|
||||
* @return {React.Component} Available Module Group contents
|
||||
*/
|
||||
_buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules) {
|
||||
let prevClass = null, prevRating = null;
|
||||
let elems = [];
|
||||
|
||||
for (let i = 0; i < modules.length; i++) {
|
||||
let m = modules[i];
|
||||
let mount = null;
|
||||
let disabled = m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass;
|
||||
let classes = cn(m.name ? 'lc' : 'c', {
|
||||
active: mountedModule && mountedModule.id === m.id,
|
||||
warning: warningFunc && warningFunc(m),
|
||||
disabled: m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass
|
||||
warning: !disabled && warningFunc && warningFunc(m),
|
||||
disabled
|
||||
});
|
||||
|
||||
switch(m.mount) {
|
||||
case 'F': mount = <MountFixed className={'lg'} />; break;
|
||||
case 'G': mount = <MountGimballed className={'lg'}/>; break;
|
||||
case 'T': mount = <MountTurret className={'lg'}/>; break;
|
||||
case 'F': mount = <MountFixed className={'lg'} />; break;
|
||||
case 'G': mount = <MountGimballed className={'lg'}/>; break;
|
||||
case 'T': mount = <MountTurret className={'lg'}/>; break;
|
||||
}
|
||||
|
||||
if (i > 0 && modules.length > 3 && m.class != prevClass && (m.rating != prevRating || m.mount) && m.grp != 'pa') {
|
||||
@@ -42,7 +106,13 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
}
|
||||
|
||||
elems.push(
|
||||
<li key={m.id} className={classes} onClick={onSelect.bind(null, m)}>
|
||||
<li
|
||||
key={m.id}
|
||||
className={classes}
|
||||
onMouseOver={disabled ? null : this._showDiff.bind(this, mountedModule, m)}
|
||||
onMouseLeave={this._hideDiff}
|
||||
onClick={disabled ? null : onSelect.bind(null, m)}
|
||||
>
|
||||
{mount}
|
||||
<span>{(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}</span>
|
||||
</li>
|
||||
@@ -54,43 +124,54 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
return <ul key={'modules' + grp} >{elems}</ul>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tooltip content for the difference between the
|
||||
* mounted module and the hovered modules
|
||||
* @param {Object} mm The module mounet currently
|
||||
* @param {Object} m The hovered module
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_showDiff(mm, m, event) {
|
||||
if (this.props.diffDetails) {
|
||||
this.context.tooltip(this.props.diffDetails(m, mm), event.currentTarget.getBoundingClientRect());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide diff tooltip
|
||||
*/
|
||||
_hideDiff() {
|
||||
this.context.tooltip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to mounted (if it exists) component on mount
|
||||
*/
|
||||
componentDidMount() {
|
||||
let m = this.props.m
|
||||
let m = this.props.m;
|
||||
|
||||
if (!(this.props.modules instanceof Array) && m && m.grp) {
|
||||
findDOMNode(this).scrollTop = this.refs[m.grp].offsetTop; // Scroll to currently selected group
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
this.setState({ list: this._initList(nextProps, nextContext) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the list
|
||||
* @return {React.Component} List
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let m = this.props.m;
|
||||
let modules = this.props.modules;
|
||||
let list;
|
||||
let buildGroup = this.buildGroup.bind(
|
||||
null,
|
||||
translate,
|
||||
m,
|
||||
this.props.warning,
|
||||
this.props.shipMass - (m && m.mass ? m.mass : 0),
|
||||
this.props.onSelect
|
||||
);
|
||||
|
||||
if (modules instanceof Array) {
|
||||
list = buildGroup(modules[0].grp, modules);
|
||||
} else {
|
||||
list = [];
|
||||
// At present time slots with grouped options (Hardpoints and Internal) can be empty
|
||||
list.push(<div className={'empty-c upp'} key={'empty'} onClick={this.props.onSelect.bind(null, null)} >{translate('empty')}</div>);
|
||||
for (let g in modules) {
|
||||
list.push(<div ref={g} key={g} className={'select-group cap'}>{translate(g)}</div>);
|
||||
list.push(buildGroup(g, modules[g]));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('select', this.props.className)} onClick={(e) => e.stopPropagation() }>
|
||||
{list}
|
||||
<div className={cn('select', this.props.className)} onScroll={this._hideDiff} onClick={(e) => e.stopPropagation() }>
|
||||
{this.state.list}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
211
src/app/components/BarChart.jsx
Normal file
211
src/app/components/BarChart.jsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import React from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import d3 from 'd3';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
|
||||
const MARGIN = { top: 15, right: 20, bottom: 40, left: 150 };
|
||||
const BAR_HEIGHT = 30;
|
||||
|
||||
/**
|
||||
* Get ship and build name
|
||||
* @param {Object} build Ship build
|
||||
* @return {string} name and build name
|
||||
*/
|
||||
function bName(build) {
|
||||
return build.buildName + '\n' + build.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a SVG text element's content with
|
||||
* tspans that wrap on newline
|
||||
* @param {string} d Data point
|
||||
*/
|
||||
function insertLinebreaks(d) {
|
||||
let el = d3.select(this);
|
||||
let lines = d.split('\n');
|
||||
el.text('').attr('y', -6);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let tspan = el.append('tspan').text(lines[i].length > 18 ? lines[i].substring(0, 15) + '...' : lines[i]);
|
||||
if (i > 0) {
|
||||
tspan.attr('x', -9).attr('dy', '1em');
|
||||
} else {
|
||||
tspan.attr('class', 'primary');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bar Chart
|
||||
*/
|
||||
export default class BarChart extends TranslatedComponent {
|
||||
|
||||
static defaultProps = {
|
||||
colors: ['#7b6888', '#6b486b', '#3182bd', '#a05d56', '#d0743c'],
|
||||
unit: ''
|
||||
};
|
||||
|
||||
static PropTypes = {
|
||||
data: React.PropTypes.array.isRequired,
|
||||
width: React.PropTypes.number.isRequired,
|
||||
format: React.PropTypes.string.isRequired,
|
||||
label: React.PropTypes.string.isRequired,
|
||||
unit: React.PropTypes.string.isRequired,
|
||||
colors: React.PropTypes.array,
|
||||
predicate: React.PropTypes.string,
|
||||
desc: React.PropTypes.bool
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
this._updateDimensions = this._updateDimensions.bind(this);
|
||||
this._hideTip = this._hideTip.bind(this);
|
||||
|
||||
let scale = d3.scale.linear();
|
||||
let y0 = d3.scale.ordinal();
|
||||
let y1 = d3.scale.ordinal();
|
||||
|
||||
this.xAxis = d3.svg.axis().scale(scale).ticks(5).outerTickSize(0).orient('bottom').tickFormat(context.language.formats.s2);
|
||||
this.yAxis = d3.svg.axis().scale(y0).outerTickSize(0).orient('left');
|
||||
this.state = { scale, y0, y1, color: d3.scale.ordinal().range(props.colors) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and Show tooltip
|
||||
* @param {Object} build Ship build
|
||||
* @param {string} property Property to display
|
||||
*/
|
||||
_showTip(build, property) {
|
||||
let { unit, format } = this.props;
|
||||
let { scale, y0, y1 } = this.state;
|
||||
let { formats } = this.context.language;
|
||||
let fontSize = parseFloat(window.getComputedStyle(document.getElementById('coriolis')).getPropertyValue('font-size') || 16);
|
||||
let val = build[property];
|
||||
let valStr = formats[format](val) + ' ' + unit;
|
||||
let width = (valStr.length / 1.7) * fontSize;
|
||||
let midPoint = width / 2;
|
||||
let valMidPoint = scale(val) / 2;
|
||||
let y = y0(bName(build)) + y1(property) - fontSize - 5;
|
||||
|
||||
let tooltip = <g>
|
||||
<g transform={`translate(${Math.max(0, valMidPoint - midPoint)},${y})`}>
|
||||
<rect className='primary-disabled' height={fontSize} width={width} />
|
||||
<text x={midPoint} y={fontSize} dy='-0.4em' style={{ textAnchor: 'middle', fontSize: '0.7em' }}>{valStr}</text>
|
||||
</g>
|
||||
<path className='primary-disabled' d='M0,0L5,5L10,0Z' dy='1em' transform={`translate(${Math.max(0, valMidPoint - 5)},${y + fontSize})`} />
|
||||
</g>;
|
||||
this.setState({ tooltip });
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide tooltip
|
||||
*/
|
||||
_hideTip() {
|
||||
this.setState({ tooltip: null });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dimensions based on properties and scale
|
||||
* @param {Object} props React Component properties
|
||||
* @param {number} scale size ratio / scale
|
||||
*/
|
||||
_updateDimensions(props, scale) {
|
||||
let { width, data, properties } = props;
|
||||
let innerWidth = width - MARGIN.left - MARGIN.right;
|
||||
let barHeight = Math.round(BAR_HEIGHT * scale);
|
||||
let dataSize = data.length;
|
||||
let innerHeight = barHeight * dataSize;
|
||||
let outerHeight = innerHeight + MARGIN.top + MARGIN.bottom;
|
||||
let max = data.reduce((max, build) => (properties.reduce(((m, p) => (m > build[p] ? m : build[p])), max)), 0);
|
||||
|
||||
this.state.scale.range([0, innerWidth]).domain([0, max]);
|
||||
this.state.y0.domain(data.map(bName)).rangeRoundBands([0, innerHeight], 0.3);
|
||||
this.state.y1.domain(properties).rangeRoundBands([0, this.state.y0.rangeBand()]);
|
||||
|
||||
this.setState({
|
||||
barHeight,
|
||||
dataSize,
|
||||
innerWidth,
|
||||
outerHeight,
|
||||
innerHeight
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dimensions based on props and context.
|
||||
*/
|
||||
componentWillMount() {
|
||||
this._updateDimensions(this.props, this.context.sizeRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
let { data, width, predicate, desc } = nextProps;
|
||||
let props = this.props;
|
||||
|
||||
if (width != props.width || this.context.sizeRatio != nextContext.sizeRatio || data != props.data) {
|
||||
this._updateDimensions(nextProps, nextContext.sizeRatio);
|
||||
}
|
||||
|
||||
if (this.context.language != nextContext.language) {
|
||||
this.xAxis.tickFormat(nextContext.language.formats.s2);
|
||||
}
|
||||
|
||||
if (predicate != props.predicate || desc != props.desc) {
|
||||
this.state.y0.domain(data.map(bName));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the chart
|
||||
* @return {React.Component} Chart SVG
|
||||
*/
|
||||
render() {
|
||||
if (!this.props.width) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let { label, unit, width, data, properties } = this.props;
|
||||
let { innerWidth, outerHeight, innerHeight, y0, y1, scale, color, tooltip } = this.state;
|
||||
|
||||
let bars = data.map((build, i) =>
|
||||
<g key={i} transform={`translate(0,${y0(bName(build))})`}>
|
||||
{ properties.map((p) =>
|
||||
<rect
|
||||
key={p}
|
||||
x={0}
|
||||
y={y1(p)}
|
||||
width={scale(build[p])}
|
||||
height={y1.rangeBand()}
|
||||
fill={color(p)}
|
||||
onMouseOver={this._showTip.bind(this, build, p)}
|
||||
onMouseOut={this._hideTip}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
|
||||
return <svg style={{ width, height: outerHeight }}>
|
||||
<g transform={`translate(${MARGIN.left},${MARGIN.top})`}>
|
||||
{bars}
|
||||
{tooltip}
|
||||
<g className='x axis' ref={(elem) => d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}>
|
||||
<text className='cap' y='30' dy='.1em' x={innerWidth / 2} style={{ textAnchor: 'middle' }}>
|
||||
<tspan>{label}</tspan>
|
||||
{ unit ? <tspan className='metric'>{` (${unit})`}</tspan> : null }
|
||||
</text>
|
||||
</g>
|
||||
<g className='y axis' ref={(elem) => { let e = d3.select(elem); e.call(this.yAxis); e.selectAll('text').each(insertLinebreaks); }} />
|
||||
</g>
|
||||
</svg>;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import cn from 'classnames';
|
||||
import { SizeMap } from '../shipyard/Constants';
|
||||
|
||||
|
||||
/**
|
||||
* Comparison Table
|
||||
*/
|
||||
export default class ComparisonTable extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -12,16 +15,27 @@ export default class ComparisonTable extends TranslatedComponent {
|
||||
builds: React.PropTypes.array.isRequired,
|
||||
onSort: React.PropTypes.func.isRequired,
|
||||
predicate: React.PropTypes.string.isRequired, // Used only to test again prop changes for shouldRender
|
||||
desc: React.PropTypes.bool.isRequired, // Used only to test again prop changes for shouldRender
|
||||
}
|
||||
desc: React.PropTypes.oneOfType([React.PropTypes.bool.isRequired, React.PropTypes.number.isRequired]), // Used only to test again prop changes for shouldRender
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this._buildHeaders = this._buildHeaders.bind(this);
|
||||
|
||||
this.state = this._buildHeaders(props.facets, props.onSort, context.language.translate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build table headers
|
||||
* @param {Array} facets Facets list
|
||||
* @param {Function} onSort Sort callback
|
||||
* @param {Function} translate Translate function
|
||||
* @return {Object} Header Components
|
||||
*/
|
||||
_buildHeaders(facets, onSort, translate) {
|
||||
let header = [
|
||||
<th key='ship' rowSpan='2' className='sortable' onClick={onSort.bind(null, 'name')}>{translate('ship')}</th>,
|
||||
@@ -39,7 +53,7 @@ export default class ComparisonTable extends TranslatedComponent {
|
||||
|
||||
if (pl > 1) {
|
||||
for (let i = 0; i < pl; i++) {
|
||||
subHeader.push(<th key={p[i]} className={cn('sortable', { lft: i === 0 } )} onClick={onSort.bind(null, p[i])} >{translate(f.lbls[i])}</th>);
|
||||
subHeader.push(<th key={p[i]} className={cn('sortable', { lft: i === 0 })} onClick={onSort.bind(null, p[i])} >{translate(f.lbls[i])}</th>);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +62,14 @@ export default class ComparisonTable extends TranslatedComponent {
|
||||
return { header, subHeader };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a table row for the build
|
||||
* @param {Object} build Ship build
|
||||
* @param {Array} facets Facets list
|
||||
* @param {Object} formats Localized formats map
|
||||
* @param {Object} units Localized untis map
|
||||
* @return {React.Component} Table row
|
||||
*/
|
||||
_buildRow(build, facets, formats, units) {
|
||||
let url = `/outfit/${build.id}/${build.toString()}?bn=${build.buildName}`;
|
||||
let cells = [
|
||||
@@ -66,6 +88,11 @@ export default class ComparisonTable extends TranslatedComponent {
|
||||
return <tr key={build.id + build.buildName} className='tr'>{cells}</tr>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
// If facets or language has changed re-render header
|
||||
if (nextProps.facets != this.props.facets || nextContext.language != this.context.language) {
|
||||
@@ -73,6 +100,10 @@ export default class ComparisonTable extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the table
|
||||
* @return {React.Component} Comparison table
|
||||
*/
|
||||
render() {
|
||||
let { builds, facets } = this.props;
|
||||
let { header, subHeader } = this.state;
|
||||
@@ -97,6 +128,5 @@ export default class ComparisonTable extends TranslatedComponent {
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@ import { Ships } from 'coriolis-data';
|
||||
import Persist from '../stores/Persist';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import { Insurance } from '../shipyard/Constants';
|
||||
import { slotName, nameComparator } from '../utils/SlotFunctions';
|
||||
import { slotName, slotComparator } from '../utils/SlotFunctions';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
|
||||
/**
|
||||
* Cost Section
|
||||
*/
|
||||
export default class CostSection extends TranslatedComponent {
|
||||
|
||||
static PropTypes = {
|
||||
@@ -15,6 +18,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
buildName: React.PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._costsTab = this._costsTab.bind(this);
|
||||
@@ -52,10 +59,17 @@ export default class CostSection extends TranslatedComponent {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ship instance to base/reference retrofit changes from
|
||||
* @param {string} shipId Ship Id
|
||||
* @param {string} name Build name
|
||||
* @param {Ship} retrofitShip Existing retrofit ship
|
||||
* @return {Ship} Retrofit ship
|
||||
*/
|
||||
_buildRetrofitShip(shipId, name, retrofitShip) {
|
||||
let data = Ships[shipId]; // Retrieve the basic ship properties, slots and defaults
|
||||
|
||||
if (!retrofitShip) {
|
||||
if (!retrofitShip) { // Don't create a new instance unless needed
|
||||
retrofitShip = new Ship(shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison
|
||||
}
|
||||
|
||||
@@ -67,15 +81,28 @@ export default class CostSection extends TranslatedComponent {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show selected tab
|
||||
* @param {string} tab Tab name
|
||||
*/
|
||||
_showTab(tab) {
|
||||
Persist.setCostTab(tab);
|
||||
this.setState({ tab });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update prices on discount change
|
||||
*/
|
||||
_onDiscountChanged() {
|
||||
let shipDiscount = Persist.getShipDiscount();
|
||||
let moduleDiscount = Persist.getModuleDiscount();
|
||||
@@ -84,13 +111,17 @@ export default class CostSection extends TranslatedComponent {
|
||||
this.setState({ shipDiscount, moduleDiscount });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update insurance on change
|
||||
* @param {string} insuranceName Insurance level name
|
||||
*/
|
||||
_onInsuranceChanged(insuranceName) {
|
||||
this.setState({ insurance: Insurance[insuranceName] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Repopulate modules on retrofit ship from existing build
|
||||
* @param {string} retrofitName Build name to base the retrofit ship on
|
||||
* @param {SyntheticEvent} event Build name to base the retrofit ship on
|
||||
*/
|
||||
_onBaseRetrofitChange(event) {
|
||||
let retrofitName = event.target.value;
|
||||
@@ -105,6 +136,12 @@ export default class CostSection extends TranslatedComponent {
|
||||
this.setState({ retrofitName });
|
||||
}
|
||||
|
||||
/**
|
||||
* On build save
|
||||
* @param {string} shipId Ship Id
|
||||
* @param {string} name Build name
|
||||
* @param {string} code Serialized ship 'code'
|
||||
*/
|
||||
_onBuildSaved(shipId, name, code) {
|
||||
if(this.state.retrofitName == name) {
|
||||
this.state.retrofitShip.buildFrom(code); // Repopulate modules from saved build
|
||||
@@ -114,6 +151,12 @@ export default class CostSection extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On build deleted
|
||||
* @param {string} shipId Ship Id
|
||||
* @param {string} name Build name
|
||||
* @param {string} code Serialized ship 'code'
|
||||
*/
|
||||
_onBuildDeleted(shipId, name, code) {
|
||||
if(this.state.retrofitName == name) {
|
||||
this.state.retrofitShip.buildWith(Ships[shipId].defaults); // Retrofit ship becomes stock build
|
||||
@@ -122,11 +165,19 @@ export default class CostSection extends TranslatedComponent {
|
||||
this.setState({ buildOptions: Persist.getBuildsNamesFor(shipId) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle item cost inclusion in overall total
|
||||
* @param {Object} item Cost item
|
||||
*/
|
||||
_toggleCost(item) {
|
||||
this.props.ship.setCostIncluded(item, !item.incCost);
|
||||
this.setState({ total: this.props.ship.totalCost });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -134,6 +185,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
this.setState({ retrofitTotal });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cost list sort predicate
|
||||
* @param {string} predicate sort predicate
|
||||
*/
|
||||
_sortCostBy(predicate) {
|
||||
let { costPredicate, costDesc } = this.state;
|
||||
|
||||
@@ -144,20 +199,27 @@ export default class CostSection extends TranslatedComponent {
|
||||
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(nameComparator(this.context.language.translate));
|
||||
costList.sort(slotComparator(translate, null, desc));
|
||||
} else {
|
||||
costList.sort((a, b) => (a.m && a.m.cost ? a.m.cost : 0) - (b.m && b.m.cost ? b.m.cost : 0));
|
||||
}
|
||||
|
||||
if (!desc) {
|
||||
costList.reverse();
|
||||
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;
|
||||
|
||||
@@ -168,19 +230,26 @@ export default class CostSection extends TranslatedComponent {
|
||||
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(nameComparator(this.context.language.translate));
|
||||
ammoCosts.sort(slotComparator(translate, null, desc));
|
||||
} else {
|
||||
ammoCosts.sort((a, b) => a[predicate] - b[predicate]);
|
||||
}
|
||||
|
||||
if (!desc) {
|
||||
ammoCosts.reverse();
|
||||
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;
|
||||
|
||||
@@ -191,6 +260,12 @@ export default class CostSection extends TranslatedComponent {
|
||||
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;
|
||||
|
||||
@@ -205,6 +280,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the cost tab
|
||||
* @return {React.Component} Tab contents
|
||||
*/
|
||||
_costsTab() {
|
||||
let { ship } = this.props;
|
||||
let { total, shipDiscount, moduleDiscount, insurance } = this.state;
|
||||
@@ -250,6 +329,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the retofit tab
|
||||
* @return {React.Component} Tab contents
|
||||
*/
|
||||
_retrofitTab() {
|
||||
let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state;
|
||||
let { translate, formats, units } = this.context.language;
|
||||
@@ -268,11 +351,11 @@ export default class CostSection extends TranslatedComponent {
|
||||
<td className='le shorten cap'>{translate(item.sellName)}</td>
|
||||
<td style={{ width: '1em' }}>{item.buyClassRating}</td>
|
||||
<td className='le shorten cap'>{translate(item.buyName)}</td>
|
||||
<td colSpan='2' className={cn('ri', item.retroItem.incCost ? item.netCost > 0 ? 'warning' : 'secondary-disabled' : 'disabled' )}>{int(item.netCost)}{units.CR}</td>
|
||||
<td colSpan='2' className={cn('ri', 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>
|
||||
rows = <tr><td colSpan='7' style={{ padding: '3em 0' }}>{translate('PHRASE_NO_RETROCH')}</td></tr>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
@@ -284,7 +367,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
<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')}
|
||||
{moduleDiscount < 1 && <u className='optional-hide'>{`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}</u>}
|
||||
{moduleDiscount < 1 && <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}</u>}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -301,7 +384,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
<td className='val cen' style={{ borderRight: 'none', width: '1em' }}><u className='primary-disabled'>▾</u></td>
|
||||
<td className='val' style={{ borderLeft:'none', padding: 0 }}>
|
||||
<select style={{ width: '100%', padding: 0 }} value={retrofitName} onChange={this._onBaseRetrofitChange}>
|
||||
{options}
|
||||
{options}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -311,9 +394,15 @@ export default class CostSection extends TranslatedComponent {
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update retrofit costs
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {Ship} retrofitShip Retrofit Ship instance
|
||||
*/
|
||||
_updateRetrofit(ship, retrofitShip) {
|
||||
let retrofitCosts = [];
|
||||
var retrofitTotal = 0, i, l, item;
|
||||
let retrofitTotal = 0, i, l, item;
|
||||
|
||||
if (ship.bulkheads.index != retrofitShip.bulkheads.index) {
|
||||
item = {
|
||||
@@ -330,9 +419,9 @@ export default class CostSection extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
for (var g in { standard: 1, internal: 1, hardpoints: 1 }) {
|
||||
var retroSlotGroup = retrofitShip[g];
|
||||
var slotGroup = ship[g];
|
||||
for (let g in { standard: 1, internal: 1, hardpoints: 1 }) {
|
||||
let retroSlotGroup = retrofitShip[g];
|
||||
let slotGroup = ship[g];
|
||||
for (i = 0, l = slotGroup.length; i < l; i++) {
|
||||
if (slotGroup[i].m != retroSlotGroup[i].m) {
|
||||
item = { netCost: 0, retroItem: retroSlotGroup[i] };
|
||||
@@ -358,6 +447,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
this._sortRetrofit(retrofitCosts, this.state.retroPredicate, this.state.retroDesc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the ammo tab
|
||||
* @return {React.Component} Tab contents
|
||||
*/
|
||||
_ammoTab() {
|
||||
let { ammoTotal, ammoCosts } = this.state;
|
||||
let { translate, formats, units } = this.context.language;
|
||||
@@ -400,6 +493,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
|
||||
/**
|
||||
* Recalculate all ammo costs
|
||||
* @param {Ship} ship Ship instance
|
||||
*/
|
||||
_updateAmmoCosts(ship) {
|
||||
let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, srvs = 0, scoop = false;
|
||||
@@ -408,10 +502,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
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
|
||||
// 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
|
||||
case 'fs': // Skip fuel calculation if scoop present
|
||||
scoop = true;
|
||||
break;
|
||||
case 'scb':
|
||||
@@ -429,7 +523,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
default:
|
||||
q = slotGroup[i].m.clip + slotGroup[i].m.ammo;
|
||||
}
|
||||
//calculate ammo costs only if a cost is specified
|
||||
// Calculate ammo costs only if a cost is specified
|
||||
if (slotGroup[i].m.ammocost > 0) {
|
||||
item = {
|
||||
m: slotGroup[i].m,
|
||||
@@ -444,7 +538,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
//limpets if controllers exist and cargo space available
|
||||
// Limpets if controllers exist and cargo space available
|
||||
if (limpets > 0) {
|
||||
item = {
|
||||
m: { name: 'limpets', class: '', rating: '' },
|
||||
@@ -466,7 +560,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
ammoCosts.push(item);
|
||||
ammoTotal += item.total;
|
||||
}
|
||||
//calculate refuel costs if no scoop present
|
||||
// Calculate refuel costs if no scoop present
|
||||
if (!scoop) {
|
||||
item = {
|
||||
m: { name: 'fuel', class: '', rating: '' },
|
||||
@@ -482,7 +576,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
this._sortAmmo(ammoCosts, this.state.ammoPredicate, this.state.ammoDesc);
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
/**
|
||||
* Add listeners on mount and update costs
|
||||
*/
|
||||
componentWillMount() {
|
||||
this.listeners = [
|
||||
Persist.addListener('discounts', this._onDiscountChanged.bind(this)),
|
||||
Persist.addListener('insurance', this._onInsuranceChanged.bind(this)),
|
||||
@@ -494,13 +591,18 @@ export default class CostSection extends TranslatedComponent {
|
||||
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 );
|
||||
retrofitShip = this._buildRetrofitShip(nextId, retrofitName, nextId == this.props.ship.id ? retrofitShip : null);
|
||||
this.setState({
|
||||
retrofitShip,
|
||||
retrofitName,
|
||||
@@ -515,6 +617,11 @@ export default class CostSection extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort lists before render
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextState Incoming/Next state
|
||||
*/
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
let state = this.state;
|
||||
|
||||
@@ -536,10 +643,17 @@ export default class CostSection extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
/**
|
||||
* Remove listeners
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this.listeners.forEach(l => l.remove());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Cost section
|
||||
* @return {React.Component} Contents
|
||||
*/
|
||||
render() {
|
||||
let tab = this.state.tab;
|
||||
let translate = this.context.language.translate;
|
||||
@@ -558,9 +672,9 @@ export default class CostSection extends TranslatedComponent {
|
||||
<table className='tabs'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width:'33%' }} className={cn({active: tab == 'costs'})} onClick={this._showTab.bind(this, 'costs')} >{translate('costs')}</th>
|
||||
<th style={{ width:'33%' }} className={cn({active: tab == 'retrofit'})} onClick={this._showTab.bind(this, 'retrofit')} >{translate('retrofit costs')}</th>
|
||||
<th style={{ width:'34%' }} className={cn({active: tab == 'ammo'})} onClick={this._showTab.bind(this, 'ammo')} >{translate('reload costs')}</th>
|
||||
<th style={{ width:'33%' }} className={cn({ active: tab == 'costs' })} onClick={this._showTab.bind(this, 'costs')} >{translate('costs')}</th>
|
||||
<th style={{ width:'33%' }} className={cn({ active: tab == 'retrofit' })} onClick={this._showTab.bind(this, 'retrofit')} >{translate('retrofit costs')}</th>
|
||||
<th style={{ width:'34%' }} className={cn({ active: tab == 'ammo' })} onClick={this._showTab.bind(this, 'ammo')} >{translate('reload costs')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
|
||||
@@ -1,36 +1,58 @@
|
||||
import React from 'react';
|
||||
import Slot from './Slot';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
_getMaxClassLabel(translate){
|
||||
/**
|
||||
* 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 {Function} translate Translate function
|
||||
* @param {Object} formats Localized Formats map
|
||||
* @param {Object} u Localized Units Map
|
||||
* @return {React.Component} Slot contents
|
||||
*/
|
||||
_getSlotDetails(m, translate, formats, u) {
|
||||
if (m) {
|
||||
let classRating = `${m.class}${m.rating}${m.mount ? '/' + m.mount : ''}${m.missile ? m.missile : ''}`;
|
||||
return (
|
||||
<div>
|
||||
let { drag, drop } = this.props;
|
||||
|
||||
return <div className='details' draggable='true' onDragStart={drag} onDragEnd={drop}>
|
||||
<div className={'cb'}>
|
||||
<div className={'l'}>{classRating + ' ' + translate(m.name || m.grp)}</div>
|
||||
<div className={'r'}>{m.mass}{u.T}</div>
|
||||
<div className={'cb'}>
|
||||
{ m.damage ? <div className={'l'}>{translate('damage')}: {m.damage} { m.ssdam ? <span>({formats.int(m.ssdam)} {u.MJ})</span> : null }</div> : null }
|
||||
{ m.dps ? <div className={'l'}>{translate('DPS')}: {m.dps} { m.mjdps ? <span>({formats.int(m.mjdps)} {u.MJ})</span> : null }</div> : null }
|
||||
{ m.thermload ? <div className={'l'}>{translate('T_LOAD')}: {m.thermload}</div> : null }
|
||||
{ m.type ? <div className={'l'}>{translate('type')}: {m.type}</div> : null }
|
||||
{ m.rof ? <div className={'l'}>{translate('ROF')}: {m.rof}{u.ps}</div> : null }
|
||||
{ m.armourpen ? <div className={'l'}>{translate('pen')}: {m.armourpen}</div> : null }
|
||||
{ m.shieldmul ? <div className={'l'}>+{formats.rPct(m.shieldmul)}</div> : null }
|
||||
{ m.range ? <div className={'l'}>{m.range} <u>km</u></div> : null }
|
||||
{ m.ammo >= 0 ? <div className={'l'}>{translate('ammo')}: {formats.int(m.clip)}+{formats.int(m.ammo)}</div> : null }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className={'cb'}>
|
||||
{ m.damage ? <div className={'l'}>{translate('damage')}: {m.damage} { m.ssdam ? <span>({formats.int(m.ssdam)} {u.MJ})</span> : null }</div> : null }
|
||||
{ m.dps ? <div className={'l'}>{translate('DPS')}: {m.dps} { m.mjdps ? <span>({formats.int(m.mjdps)} {u.MJ})</span> : null }</div> : null }
|
||||
{ m.thermload ? <div className={'l'}>{translate('T_LOAD')}: {m.thermload}</div> : null }
|
||||
{ m.type ? <div className={'l'}>{translate('type')}: {m.type}</div> : null }
|
||||
{ m.rof ? <div className={'l'}>{translate('ROF')}: {m.rof}{u.ps}</div> : null }
|
||||
{ m.armourpen ? <div className={'l'}>{translate('pen')}: {m.armourpen}</div> : null }
|
||||
{ m.shieldmul ? <div className={'l'}>+{formats.rPct(m.shieldmul)}</div> : null }
|
||||
{ m.range ? <div className={'l'}>{m.range} <u>km</u></div> : null }
|
||||
{ m.ammo >= 0 ? <div className={'l'}>{translate('ammo')}: {formats.int(m.clip)}+{formats.int(m.ammo)}</div> : null }
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
return <div className={'empty'}>{translate('empty')}</div>;
|
||||
}
|
||||
|
||||
@@ -4,35 +4,60 @@ import HardpointSlot from './HardpointSlot';
|
||||
import cn from 'classnames';
|
||||
import { MountFixed, MountGimballed, MountTurret } from '../components/SvgIcons';
|
||||
|
||||
/**
|
||||
* Hardpoint slot section
|
||||
*/
|
||||
export default class HardpointsSlotSection extends SlotSection {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context, 'hardpoints', 'hardpoints');
|
||||
|
||||
this._empty = this._empty.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all slots
|
||||
*/
|
||||
_empty() {
|
||||
this.props.ship.emptyWeapons();
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill slots with specified module
|
||||
* @param {string} group Group name
|
||||
* @param {string} mount Mount Type - F, G, T
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fill(group, mount, event) {
|
||||
this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt'));
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all on section header right click
|
||||
*/
|
||||
_contextMenu() {
|
||||
this._empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the slot React Components
|
||||
* @return {Array} Array of Slots
|
||||
*/
|
||||
_getSlots() {
|
||||
let { ship, currentMenu } = this.props;
|
||||
let { originSlot, targetSlot } = this.state;
|
||||
let slots = [];
|
||||
let hardpoints = this.props.ship.hardpoints;
|
||||
let availableModules = this.props.ship.getAvailableModules();
|
||||
let currentMenu = this.props.currentMenu;
|
||||
let hardpoints = ship.hardpoints;
|
||||
let availableModules = ship.getAvailableModules();
|
||||
|
||||
for (let i = 0, l = hardpoints.length; i < l; i++) {
|
||||
let h = hardpoints[i];
|
||||
@@ -44,6 +69,11 @@ export default class HardpointsSlotSection extends SlotSection {
|
||||
onOpen={this._openMenu.bind(this, h)}
|
||||
onSelect={this._selectModule.bind(this, h)}
|
||||
selected={currentMenu == h}
|
||||
drag={this._drag.bind(this, h)}
|
||||
dragOver={this._dragOverSlot.bind(this, h)}
|
||||
drop={this._drop}
|
||||
dropClass={this._dropClass(h, originSlot, targetSlot)}
|
||||
ship={ship}
|
||||
m={h.m}
|
||||
/>);
|
||||
}
|
||||
@@ -52,6 +82,11 @@ export default class HardpointsSlotSection extends SlotSection {
|
||||
return slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the section drop-down menu
|
||||
* @param {Function} translate Translate function
|
||||
* @return {React.Component} Section menu
|
||||
*/
|
||||
_getSectionMenu(translate) {
|
||||
let _fill = this._fill;
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import ActiveLink from './ActiveLink';
|
||||
import cn from 'classnames';
|
||||
import { Cogs, CoriolisLogo, Hammer, Rocket, StatsBars } from './SvgIcons';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import Persist from '../stores/Persist';
|
||||
import { toDetailedExport } from '../shipyard/Serializer';
|
||||
import ModalDeleteAll from './ModalDeleteAll';
|
||||
@@ -18,8 +17,16 @@ import Slider from './Slider';
|
||||
const SIZE_MIN = 0.65;
|
||||
const SIZE_RANGE = 0.55;
|
||||
|
||||
/**
|
||||
* Coriolis App Header section / menus
|
||||
*/
|
||||
export default class Header extends TranslatedComponent {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this.shipOrder = Object.keys(Ships).sort();
|
||||
@@ -44,64 +51,114 @@ export default class Header extends TranslatedComponent {
|
||||
for (let name in Discounts) {
|
||||
this.discountOptions.push(<option key={name} value={Discounts[name]}>{name}</option>);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Update insurance level
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_setInsurance(e) {
|
||||
Persist.setInsurance(e.target.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the Module discount
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_setModuleDiscount(e) {
|
||||
Persist.setModuleDiscount(e.target.value * 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the Ship discount
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_setShipDiscount(e) {
|
||||
Persist.setShipDiscount(e.target.value * 1);
|
||||
}
|
||||
|
||||
_setLanguage(e){
|
||||
/**
|
||||
* Update the current language
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_setLanguage(e) {
|
||||
Persist.setLangCode(e.target.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle tooltips setting
|
||||
*/
|
||||
_toggleTooltips() {
|
||||
Persist.showTooltips(!Persist.showTooltips());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show delete all modal
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_showDeleteAll(e) {
|
||||
e.preventDefault();
|
||||
InterfaceEvents.showModal(<ModalDeleteAll />);
|
||||
this.context.showModal(<ModalDeleteAll />);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show export modal with backup data
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_showBackup(e) {
|
||||
let translate = this.context.language.translate;
|
||||
e.preventDefault();
|
||||
InterfaceEvents.showModal(<ModalExport
|
||||
this.context.showModal(<ModalExport
|
||||
title={translate('backup')}
|
||||
description={translate('PHRASE_BACKUP_DESC')}
|
||||
data={Persist.getAll()}
|
||||
/>);
|
||||
};
|
||||
|
||||
_showDetailedExport(e){
|
||||
/**
|
||||
* Show export modal with detailed export
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_showDetailedExport(e) {
|
||||
let translate = this.context.language.translate;
|
||||
e.preventDefault();
|
||||
|
||||
InterfaceEvents.showModal(<ModalExport
|
||||
this.context.showModal(<ModalExport
|
||||
title={translate('detailed export')}
|
||||
description={translate('PHRASE_EXPORT_DESC')}
|
||||
data={toDetailedExport(Persist.getBuilds())}
|
||||
/>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show import modal
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_showImport(e) {
|
||||
e.preventDefault();
|
||||
InterfaceEvents.showModal(<ModalImport/>);
|
||||
this.context.showModal(<ModalImport/>);
|
||||
}
|
||||
|
||||
_setTextSize(size) {
|
||||
Persist.setSizeRatio((size * SIZE_RANGE) + SIZE_MIN);
|
||||
/**
|
||||
* Update the app scale / size ratio
|
||||
* @param {number} scale scale Size Ratio
|
||||
*/
|
||||
_setTextSize(scale) {
|
||||
Persist.setSizeRatio((scale * SIZE_RANGE) + SIZE_MIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the app scale / size ratio
|
||||
*/
|
||||
_resetTextSize() {
|
||||
Persist.setSizeRatio(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a menu
|
||||
* @param {SyntheticEvent} event Event
|
||||
* @param {string} menu Menu name
|
||||
*/
|
||||
_openMenu(event, menu) {
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -109,23 +166,31 @@ export default class Header extends TranslatedComponent {
|
||||
menu = null;
|
||||
}
|
||||
|
||||
InterfaceEvents.openMenu(menu);
|
||||
this.context.openMenu(menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the ships menu
|
||||
* @return {React.Component} Menu
|
||||
*/
|
||||
_getShipsMenu() {
|
||||
let shipList = [];
|
||||
|
||||
for (let s in Ships) {
|
||||
shipList.push(<ActiveLink key={s} href={'/outfit/' + s} className={'block'}>{Ships[s].properties.name}</ActiveLink>);
|
||||
shipList.push(<ActiveLink key={s} href={'/outfit/' + s} className='block'>{Ships[s].properties.name}</ActiveLink>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'menu-list dbl no-wrap'} onClick={ (e) => e.stopPropagation() }>
|
||||
<div className='menu-list dbl no-wrap' onClick={ (e) => e.stopPropagation() }>
|
||||
{shipList}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the builds menu
|
||||
* @return {React.Component} Menu
|
||||
*/
|
||||
_getBuildsMenu() {
|
||||
let builds = Persist.getBuilds();
|
||||
let buildList = [];
|
||||
@@ -135,19 +200,23 @@ export default class Header extends TranslatedComponent {
|
||||
let buildNameOrder = Object.keys(builds[shipId]).sort();
|
||||
for (let buildName of buildNameOrder) {
|
||||
let href = ['/outfit/', shipId, '/', builds[shipId][buildName], '?bn=', buildName].join('');
|
||||
shipBuilds.push(<li key={shipId + '-' + buildName} ><ActiveLink href={href} className={'block'}>{buildName}</ActiveLink></li>);
|
||||
shipBuilds.push(<li key={shipId + '-' + buildName} ><ActiveLink href={href} className='block'>{buildName}</ActiveLink></li>);
|
||||
}
|
||||
buildList.push(<ul key={shipId}>{Ships[shipId].properties.name}{shipBuilds}</ul>);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'menu-list'} onClick={ (e) => e.stopPropagation() }>
|
||||
<div className={'dbl'}>{buildList}</div>
|
||||
<div className='menu-list' onClick={ (e) => e.stopPropagation() }>
|
||||
<div className='dbl'>{buildList}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the comparison menu
|
||||
* @return {React.Component} Menu
|
||||
*/
|
||||
_getComparisonsMenu() {
|
||||
let comparisons;
|
||||
let translate = this.context.language.translate;
|
||||
@@ -157,69 +226,68 @@ export default class Header extends TranslatedComponent {
|
||||
let comps = Object.keys(Persist.getComparisons()).sort();
|
||||
|
||||
for (let name of comps) {
|
||||
comparisons.push(<ActiveLink key={name} href={'/compare/' + name} className={'block name'}>{name}</ActiveLink>);
|
||||
comparisons.push(<ActiveLink key={name} href={'/compare/' + name} className='block name'>{name}</ActiveLink>);
|
||||
}
|
||||
} else {
|
||||
comparisons = <span className={'cap'}>{translate('none created')}</span>;
|
||||
comparisons = <span className='cap'>{translate('none created')}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'menu-list'} onClick={ (e) => e.stopPropagation() } style={{ whiteSpace: 'nowrap' }}>
|
||||
<div className='menu-list' onClick={ (e) => e.stopPropagation() } style={{ whiteSpace: 'nowrap' }}>
|
||||
{comparisons}
|
||||
<hr />
|
||||
<Link href='/compare/all' ui-sref="compare({name: 'all'})" className={'block cap'}>{translate('compare all')}</Link>
|
||||
<Link href='/compare' className={'block cap'}>{translate('create new')}</Link>
|
||||
<Link href='/compare/all' ui-sref="compare({name: 'all'})" className='block cap'>{translate('compare all')}</Link>
|
||||
<Link href='/compare' className='block cap'>{translate('create new')}</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the settings menu
|
||||
* @return {React.Component} Menu
|
||||
*/
|
||||
_getSettingsMenu() {
|
||||
let translate = this.context.language.translate;
|
||||
let tips = Persist.showTooltips();
|
||||
|
||||
return (
|
||||
<div className={'menu-list no-wrap cap'} onClick={ (e) => e.stopPropagation() }>
|
||||
<ul>
|
||||
{translate('language')}
|
||||
<li>
|
||||
<select className={'cap'} value={Persist.getLangCode()} onChange={this._setLanguage}>
|
||||
{this.languageOptions}
|
||||
</select>
|
||||
</li>
|
||||
</ul><br/>
|
||||
<ul>
|
||||
{translate('insurance')}
|
||||
<li>
|
||||
<select className={'cap'} value={Persist.getInsurance()} onChange={this._setInsurance}>
|
||||
{this.insuranceOptions}
|
||||
</select>
|
||||
</li>
|
||||
</ul><br/>
|
||||
<ul>
|
||||
{translate('ship')} {translate('discount')}
|
||||
<li>
|
||||
<select className={'cap'} value={Persist.getShipDiscount()} onChange={this._setShipDiscount}>
|
||||
{this.discountOptions}
|
||||
</select>
|
||||
</li>
|
||||
</ul><br/>
|
||||
<ul>
|
||||
{translate('module')} {translate('discount')}
|
||||
<li>
|
||||
<select className={'cap'} value={Persist.getModuleDiscount()} onChange={this._setModuleDiscount} >
|
||||
{this.discountOptions}
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
<div className='menu-list no-wrap cap' onClick={ (e) => e.stopPropagation() }>
|
||||
<div style={{ lineHeight: '2em' }}>
|
||||
{translate('language')}
|
||||
<select className='cap' value={Persist.getLangCode()} onChange={this._setLanguage}>
|
||||
{this.languageOptions}
|
||||
</select>
|
||||
<br/>
|
||||
<span className='cap ptr' onClick={this._toggleTooltips} >
|
||||
{translate('tooltips')}
|
||||
<div className={cn({ disabled: !tips, 'primary-disabled': tips })} style={{ marginLeft: '0.5em', display: 'inline-block' }}>{(tips ? '✔' : '✖')}</div>
|
||||
</span>
|
||||
<br/>
|
||||
{translate('insurance')}
|
||||
<select className='cap' value={Persist.getInsurance()} onChange={this._setInsurance}>
|
||||
{this.insuranceOptions}
|
||||
</select>
|
||||
<br/>
|
||||
{translate('ship')} {translate('discount')}
|
||||
<select className='cap' value={Persist.getShipDiscount()} onChange={this._setShipDiscount}>
|
||||
{this.discountOptions}
|
||||
</select>
|
||||
<br/>
|
||||
{translate('module')} {translate('discount')}
|
||||
<select className='cap' value={Persist.getModuleDiscount()} onChange={this._setModuleDiscount} >
|
||||
{this.discountOptions}
|
||||
</select>
|
||||
</div>
|
||||
<hr />
|
||||
<ul>
|
||||
{translate('builds')} & {translate('comparisons')}
|
||||
<li><a href="#" className={'block'} onClick={this._showBackup.bind(this)}>{translate('backup')}</a></li>
|
||||
<li><a href="#" className={'block'} onClick={this._showDetailedExport.bind(this)}>{translate('detailed export')}</a></li>
|
||||
<li><a href="#" className={'block'} onClick={this._showImport.bind(this)}>{translate('import')}</a></li>
|
||||
<li><a href="#" className='block' onClick={this._showBackup.bind(this)}>{translate('backup')}</a></li>
|
||||
<li><a href="#" className='block' onClick={this._showDetailedExport.bind(this)}>{translate('detailed export')}</a></li>
|
||||
<li><a href="#" className='block' onClick={this._showImport.bind(this)}>{translate('import')}</a></li>
|
||||
<li><a href="#" onClick={this._showDeleteAll.bind(this)}>{translate('delete all')}</a></li>
|
||||
</ul>
|
||||
<hr />
|
||||
<table style={{width: 300, backgroundColor: 'transparent'}}>
|
||||
<table style={{ width: 300, backgroundColor: 'transparent' }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: '1em', verticalAlign: 'top' }}><u>A</u></td>
|
||||
@@ -227,22 +295,34 @@ export default class Header extends TranslatedComponent {
|
||||
<td style={{ width: 20 }}><span style={{ fontSize: 30 }}>A</span></td>
|
||||
</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>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<Link href="/about" className={'block'}>{translate('about')}</Link>
|
||||
<Link href="/about" className='block'>{translate('about')}</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
/**
|
||||
* Add listeners on mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
Persist.addListener('language', () => this.forceUpdate());
|
||||
Persist.addListener('insurance', () => this.forceUpdate());
|
||||
Persist.addListener('discounts', () => this.forceUpdate());
|
||||
Persist.addListener('deletedAll', () => this.forceUpdate());
|
||||
Persist.addListener('buildSaved', () => this.forceUpdate());
|
||||
Persist.addListener('buildDeleted', () => this.forceUpdate());
|
||||
Persist.addListener('tooltips', () => this.forceUpdate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
if(this.context.language != nextContext.language) {
|
||||
let translate = nextContext.language.translate;
|
||||
@@ -253,6 +333,10 @@ export default class Header extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the header
|
||||
* @return {React.Component} Header
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let openedMenu = this.props.currentMenu;
|
||||
@@ -264,32 +348,32 @@ export default class Header extends TranslatedComponent {
|
||||
|
||||
return (
|
||||
<header>
|
||||
<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={cn('menu-header', {selected: openedMenu == 's'})} onClick={ (e) => this._openMenu(e,'s') } >
|
||||
<Rocket className={'warning'} /><span className={'menu-item-label'}>{' ' + translate('ships')}</span>
|
||||
<div className='l menu'>
|
||||
<div className={cn('menu-header', { selected: openedMenu == 's' })} onClick={ (e) => this._openMenu(e,'s') } >
|
||||
<Rocket className='warning' /><span className='menu-item-label'>{' ' + translate('ships')}</span>
|
||||
</div>
|
||||
{openedMenu == 's' ? this._getShipsMenu() : null}
|
||||
</div>
|
||||
|
||||
<div className={'l menu'}>
|
||||
<div className={cn('menu-header', {selected: openedMenu == 'b', disabled: !hasBuilds})} onClick={ hasBuilds ? (e) => this._openMenu(e,'b') : null }>
|
||||
<Hammer className={cn('warning', { 'warning-disabled': !hasBuilds})} /><span className={'menu-item-label'}>{' ' + translate('builds')}</span>
|
||||
<div className='l menu'>
|
||||
<div className={cn('menu-header', { selected: openedMenu == 'b', disabled: !hasBuilds })} onClick={ hasBuilds ? (e) => this._openMenu(e,'b') : null }>
|
||||
<Hammer className={cn('warning', { 'warning-disabled': !hasBuilds })} /><span className='menu-item-label'>{' ' + translate('builds')}</span>
|
||||
</div>
|
||||
{openedMenu == 'b' ? this._getBuildsMenu() : null}
|
||||
</div>
|
||||
|
||||
<div className={'l menu'}>
|
||||
<div className={cn('menu-header', {selected: openedMenu == 'comp', disabled: !hasBuilds})} onClick={ hasBuilds ? (e) => this._openMenu(e,'comp') : null }>
|
||||
<StatsBars className={cn('warning', { 'warning-disabled': !hasBuilds})} /><span className={'menu-item-label'}>{' ' + translate('compare')}</span>
|
||||
<div className='l menu'>
|
||||
<div className={cn('menu-header', { selected: openedMenu == 'comp', disabled: !hasBuilds })} onClick={ hasBuilds ? (e) => this._openMenu(e,'comp') : null }>
|
||||
<StatsBars className={cn('warning', { 'warning-disabled': !hasBuilds })} /><span className='menu-item-label'>{' ' + translate('compare')}</span>
|
||||
</div>
|
||||
{openedMenu == 'comp' ? this._getComparisonsMenu() : null}
|
||||
</div>
|
||||
|
||||
<div className={'r menu'}>
|
||||
<div className={cn('menu-header', {selected: openedMenu == 'settings'})}onClick={ (e) => this._openMenu(e,'settings') }>
|
||||
<Cogs className={'xl warning'}/><span className={'menu-item-label'}>{translate('settings')}</span>
|
||||
<div className='r menu'>
|
||||
<div className={cn('menu-header', { selected: openedMenu == 'settings' })}onClick={ (e) => this._openMenu(e,'settings') }>
|
||||
<Cogs className='xl warning'/><span className='menu-item-label'>{translate('settings')}</span>
|
||||
</div>
|
||||
{openedMenu == 'settings' ? this._getSettingsMenu() : null}
|
||||
</div>
|
||||
|
||||
@@ -2,36 +2,48 @@ import React from 'react';
|
||||
import Slot from './Slot';
|
||||
import { Infinite } from './SvgIcons';
|
||||
|
||||
/**
|
||||
* Internal Slot
|
||||
*/
|
||||
export default class InternalSlot extends Slot {
|
||||
|
||||
/**
|
||||
* Generate the slot contents
|
||||
* @param {Object} m Mounted Module
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Object} formats Localized Formats map
|
||||
* @param {Object} u Localized Units Map
|
||||
* @return {React.Component} Slot contents
|
||||
*/
|
||||
_getSlotDetails(m, translate, formats, u) {
|
||||
if (m) {
|
||||
let classRating = m.class + m.rating;
|
||||
let { drag, drop } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
return <div className='details' draggable='true' onDragStart={drag} onDragEnd={drop}>
|
||||
<div className={'cb'}>
|
||||
<div className={'l'}>{classRating + ' ' + translate(m.name || m.grp)}</div>
|
||||
<div className={'r'}>{m.mass || m.capacity || 0}{u.T}</div>
|
||||
<div className={'cb'}>
|
||||
{ m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{u.T}</div> : null }
|
||||
{ m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{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.rate ? <div className={'l'}>{translate('rate')}: {m.rate}{u.kgs} {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}</div> : null }
|
||||
{ m.ammo ? <div className={'l'}>{translate('ammo')}: {formats.gen(m.ammo)}</div> : null }
|
||||
{ m.cells ? <div className={'l'}>{translate('cells')}: {m.cells}</div> : null }
|
||||
{ m.recharge ? <div className={'l'}>{translate('recharge')}: {m.recharge} <u>MJ</u> {translate('total')}: {m.cells * m.recharge}{u.MJ}</div> : null }
|
||||
{ m.repair ? <div className={'l'}>{translate('repair')}: {m.repair}</div> : null }
|
||||
{ m.range ? <div className={'l'}>{translate('range')} {m.range}{u.km}</div> : null }
|
||||
{ m.time ? <div className={'l'}>{translate('time')}: {formats.time(m.time)}</div> : null }
|
||||
{ m.maximum ? <div className={'l'}>{translate('max')}: {(m.maximum)}</div> : null }
|
||||
{ m.rangeLS ? <div className={'l'}>{m.rangeLS}{u.Ls}</div> : null }
|
||||
{ m.rangeLS === null ? <div className={'l'}><Infinite/>{u.Ls}</div> : null }
|
||||
{ m.rangeRating ? <div className={'l'}>{translate('range')}: {m.rangeRating}</div> : null }
|
||||
{ m.armouradd ? <div className={'l'}>+{m.armouradd} <u>{translate('armour')}</u></div> : null }
|
||||
</div>
|
||||
<div className={'r'}>{m.mass || m.cargo || m.fuel || 0}{u.T}</div>
|
||||
</div>
|
||||
);
|
||||
<div className={'cb'}>
|
||||
{ m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{u.T}</div> : null }
|
||||
{ m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{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.rate ? <div className={'l'}>{translate('rate')}: {m.rate}{u.kgs} {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}</div> : null }
|
||||
{ m.ammo ? <div className={'l'}>{translate('ammo')}: {formats.gen(m.ammo)}</div> : null }
|
||||
{ m.cells ? <div className={'l'}>{translate('cells')}: {m.cells}</div> : null }
|
||||
{ m.recharge ? <div className={'l'}>{translate('recharge')}: {m.recharge} <u>MJ</u> {translate('total')}: {m.cells * m.recharge}{u.MJ}</div> : null }
|
||||
{ m.repair ? <div className={'l'}>{translate('repair')}: {m.repair}</div> : null }
|
||||
{ m.range ? <div className={'l'}>{translate('range')} {m.range}{u.km}</div> : null }
|
||||
{ m.time ? <div className={'l'}>{translate('time')}: {formats.time(m.time)}</div> : null }
|
||||
{ m.maximum ? <div className={'l'}>{translate('max')}: {(m.maximum)}</div> : null }
|
||||
{ m.rangeLS ? <div className={'l'}>{m.rangeLS}{u.Ls}</div> : null }
|
||||
{ m.rangeLS === null ? <div className={'l'}><Infinite/>{u.Ls}</div> : null }
|
||||
{ m.rangeRating ? <div className={'l'}>{translate('range')}: {m.rangeRating}</div> : null }
|
||||
{ m.armouradd ? <div className={'l'}>+{m.armouradd} <u>{translate('armour')}</u></div> : null }
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
return <div className={'empty'}>{translate('empty')}</div>;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,16 @@ import SlotSection from './SlotSection';
|
||||
import InternalSlot from './InternalSlot';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
|
||||
|
||||
/**
|
||||
* Internal slot section
|
||||
*/
|
||||
export default class InternalSlotSection extends SlotSection {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context, 'internal', 'internal compartments');
|
||||
|
||||
@@ -16,12 +23,19 @@ export default class InternalSlotSection extends SlotSection {
|
||||
this._fillWithArmor = this._fillWithArmor.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all slots
|
||||
*/
|
||||
_empty() {
|
||||
this.props.ship.emptyInternal();
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill all slots with cargo racks
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithCargo(event) {
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
@@ -34,6 +48,10 @@ export default class InternalSlotSection extends SlotSection {
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill all slots with Shield Cell Banks
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithCells(event) {
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
@@ -49,6 +67,10 @@ export default class InternalSlotSection extends SlotSection {
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill all slots with Hull Reinforcement Packages
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithArmor(event) {
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
@@ -61,18 +83,27 @@ export default class InternalSlotSection extends SlotSection {
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all on section header right click
|
||||
*/
|
||||
_contextMenu() {
|
||||
this._empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the slot React Components
|
||||
* @return {Array} Array of Slots
|
||||
*/
|
||||
_getSlots() {
|
||||
let slots = [];
|
||||
let { currentMenu, ship } = this.props;
|
||||
let {internal, fuelCapacity, ladenMass } = ship;
|
||||
let { originSlot, targetSlot } = this.state;
|
||||
let { internal, fuelCapacity, ladenMass } = ship;
|
||||
let availableModules = ship.getAvailableModules();
|
||||
|
||||
for (let i = 0, l = internal.length; i < l; i++) {
|
||||
let s = internal[i];
|
||||
|
||||
slots.push(<InternalSlot
|
||||
key={i}
|
||||
maxClass={s.maxClass}
|
||||
@@ -82,13 +113,23 @@ export default class InternalSlotSection extends SlotSection {
|
||||
selected={currentMenu == s}
|
||||
enabled={s.enabled}
|
||||
m={s.m}
|
||||
drag={this._drag.bind(this, s)}
|
||||
dragOver={this._dragOverSlot.bind(this, s)}
|
||||
drop={this._drop}
|
||||
dropClass={this._dropClass(s, originSlot, targetSlot)}
|
||||
fuel={fuelCapacity}
|
||||
ship={ship}
|
||||
/>);
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the section drop-down menu
|
||||
* @param {Function} translate Translate function
|
||||
* @return {React.Component} Section menu
|
||||
*/
|
||||
_getSectionMenu(translate) {
|
||||
return <div className='select' onClick={e => e.stopPropagation()}>
|
||||
<ul>
|
||||
|
||||
@@ -4,15 +4,18 @@ import d3 from 'd3';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
|
||||
const RENDER_POINTS = 20; // Only render 20 points on the graph
|
||||
const MARGIN = { top: 15, right: 15, bottom: 35, left: 60 }
|
||||
const MARGIN = { top: 15, right: 15, bottom: 35, left: 60 };
|
||||
|
||||
/**
|
||||
* Line Chart
|
||||
*/
|
||||
export default class LineChart extends TranslatedComponent {
|
||||
|
||||
static defaultProps = {
|
||||
xMin: 0,
|
||||
yMin: 0,
|
||||
colors: ['#ff8c0d']
|
||||
}
|
||||
};
|
||||
|
||||
static PropTypes = {
|
||||
width: React.PropTypes.number.isRequired,
|
||||
@@ -29,6 +32,11 @@ export default class LineChart extends TranslatedComponent {
|
||||
colors: React.PropTypes.array,
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
@@ -42,11 +50,12 @@ export default class LineChart extends TranslatedComponent {
|
||||
let markerElems = [];
|
||||
let detailElems = [<text key={'lbl'} className='label x' y='1.25em'/>];
|
||||
let xScale = d3.scale.linear();
|
||||
let xAxisScale = d3.scale.linear();
|
||||
let yScale = d3.scale.linear();
|
||||
let series = props.series;
|
||||
let seriesLines = [];
|
||||
|
||||
this.xAxis = d3.svg.axis().scale(xScale).outerTickSize(0).orient('bottom');
|
||||
this.xAxis = d3.svg.axis().scale(xAxisScale).outerTickSize(0).orient('bottom');
|
||||
this.yAxis = d3.svg.axis().scale(yScale).ticks(6).outerTickSize(0).orient('left');
|
||||
|
||||
for(let i = 0, l = series ? series.length : 1; i < l; i++) {
|
||||
@@ -58,6 +67,7 @@ export default class LineChart extends TranslatedComponent {
|
||||
|
||||
this.state = {
|
||||
xScale,
|
||||
xAxisScale,
|
||||
yScale,
|
||||
seriesLines,
|
||||
detailElems,
|
||||
@@ -66,15 +76,19 @@ export default class LineChart extends TranslatedComponent {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tooltip content
|
||||
* @param {number} xPos x coordinate
|
||||
*/
|
||||
_tooltip(xPos) {
|
||||
let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props;
|
||||
let { xScale, yScale } = this.state;
|
||||
let { xScale, yScale, innerWidth } = this.state;
|
||||
let { formats, translate } = this.context.language;
|
||||
let x0 = xScale.invert(xPos),
|
||||
y0 = func(x0),
|
||||
tips = this.tipContainer,
|
||||
yTotal = 0,
|
||||
flip = (x0 / xScale.domain()[1] > 0.65),
|
||||
flip = (xPos / innerWidth > 0.65),
|
||||
tipWidth = 0,
|
||||
tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height;
|
||||
|
||||
@@ -100,32 +114,53 @@ export default class LineChart extends TranslatedComponent {
|
||||
this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0));
|
||||
}
|
||||
|
||||
_updateDimensions(props, sizeRatio) {
|
||||
/**
|
||||
* Update dimensions based on properties and scale
|
||||
* @param {Object} props React Component properties
|
||||
* @param {number} scale size ratio / scale
|
||||
*/
|
||||
_updateDimensions(props, scale) {
|
||||
let { width, xMax, xMin, yMin, yMax } = props;
|
||||
let innerWidth = width - MARGIN.left - MARGIN.right;
|
||||
let outerHeight = Math.round(width * 0.5 * sizeRatio);
|
||||
let outerHeight = Math.round(width * 0.5 * scale);
|
||||
let innerHeight = outerHeight - MARGIN.top - MARGIN.bottom;
|
||||
|
||||
this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true);
|
||||
this.state.xAxisScale.range([0, innerWidth]).domain([xMin, xMax]).clamp(true);
|
||||
this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax]);
|
||||
this.setState({ innerWidth, outerHeight, innerHeight });
|
||||
}
|
||||
|
||||
/**
|
||||
* Show tooltip
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_showTip(e) {
|
||||
this._moveTip(e);
|
||||
this.tipContainer.style('display', null);
|
||||
this.markersContainer.style('display', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move and update tooltip
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_moveTip(e) {
|
||||
this._tooltip(Math.round(e.clientX - e.target.getBoundingClientRect().left));
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide tooltip
|
||||
*/
|
||||
_hideTip() {
|
||||
this.tipContainer.style('display', 'none');
|
||||
this.markersContainer.style('display', 'none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update series data generated from props
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
_updateSeriesData(props) {
|
||||
let { func, xMin, xMax, series } = props;
|
||||
let delta = (xMax - xMin) / RENDER_POINTS;
|
||||
@@ -134,23 +169,31 @@ export default class LineChart extends TranslatedComponent {
|
||||
if (delta) {
|
||||
seriesData = new Array(RENDER_POINTS);
|
||||
for (let i = 0, x = xMin; i < RENDER_POINTS; i++) {
|
||||
seriesData[i] = [ x, func(x) ];
|
||||
seriesData[i] = [x, func(x)];
|
||||
x += delta;
|
||||
}
|
||||
seriesData[RENDER_POINTS - 1] = [ xMax, func(xMax) ];
|
||||
seriesData[RENDER_POINTS - 1] = [xMax, func(xMax)];
|
||||
} else {
|
||||
let yVal = func(xMin);
|
||||
seriesData = [ [0, yVal], [1, yVal]];
|
||||
seriesData = [[0, yVal], [1, yVal]];
|
||||
}
|
||||
|
||||
this.setState({ seriesData });
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
/**
|
||||
* Update dimensions and series data based on props and context.
|
||||
*/
|
||||
componentWillMount() {
|
||||
this._updateDimensions(this.props, this.context.sizeRatio);
|
||||
this._updateSeriesData(this.props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
let { func, xMin, xMax, yMin, yMax, width } = nextProps;
|
||||
let props = this.props;
|
||||
@@ -166,6 +209,10 @@ export default class LineChart extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the chart
|
||||
* @return {React.Component} Chart SVG
|
||||
*/
|
||||
render() {
|
||||
if (!this.props.width) {
|
||||
return null;
|
||||
@@ -192,7 +239,7 @@ export default class LineChart extends TranslatedComponent {
|
||||
</text>
|
||||
</g>
|
||||
<g ref={(g) => this.tipContainer = d3.select(g)} className='tooltip' style={{ display: 'none' }}>
|
||||
<rect className='tip' style={{height: tipHeight + 'em'}}></rect>
|
||||
<rect className='tip' style={{ height: tipHeight + 'em' }}></rect>
|
||||
{detailElems}
|
||||
</g>
|
||||
<g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}>
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
import React from 'react';
|
||||
import Router from '../Router';
|
||||
import shallowEqual from '../utils/shallowEqual';
|
||||
import { shallowEqual } from '../utils/UtilityFunctions';
|
||||
|
||||
/**
|
||||
* Link wrapper component
|
||||
*/
|
||||
export default class Link extends React.Component {
|
||||
|
||||
/**
|
||||
* Determine if a component should be rerendered
|
||||
* @param {object} nextProps Next properties
|
||||
* @return {boolean} true if update is needed
|
||||
*/
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return !shallowEqual(this.props, nextProps);
|
||||
}
|
||||
|
||||
handler = (event) => {
|
||||
if (event.getModifierState
|
||||
&& ( event.getModifierState('Shift')
|
||||
|| event.getModifierState('Alt')
|
||||
|| event.getModifierState('Control')
|
||||
|| event.getModifierState('Meta')
|
||||
|| event.button > 1)) {
|
||||
/**
|
||||
* Link click handler
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
handler(event) {
|
||||
if (event.getModifierState &&
|
||||
(event.getModifierState('Shift') ||
|
||||
event.getModifierState('Alt') ||
|
||||
event.getModifierState('Control') ||
|
||||
event.getModifierState('Meta') ||
|
||||
event.button > 1)) {
|
||||
return;
|
||||
}
|
||||
event.nativeEvent && event.preventDefault && event.preventDefault();
|
||||
@@ -24,8 +36,12 @@ export default class Link extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the link
|
||||
* @return {React.Component} A href element
|
||||
*/
|
||||
render() {
|
||||
return <a {...this.props} onClick={this.handler}>{this.props.children}</a>
|
||||
return <a {...this.props} onClick={this.handler.bind(this)}>{this.props.children}</a>;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +1,24 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import Persist from '../stores/Persist';
|
||||
|
||||
/**
|
||||
* Build ship and name comparator
|
||||
* @param {Object} a [description]
|
||||
* @param {Object} b [description]
|
||||
* @return {number} 1, 0, -1
|
||||
*/
|
||||
function buildComparator(a, b) {
|
||||
if (a.name == b.name) {
|
||||
return a.buildName > b.buildName;
|
||||
return a.buildName.localeCompare(b.buildName);
|
||||
}
|
||||
return a.name > b.name;
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compare builds modal
|
||||
*/
|
||||
export default class ModalCompare extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -21,52 +28,71 @@ export default class ModalCompare extends TranslatedComponent {
|
||||
|
||||
static defaultProps = {
|
||||
builds: []
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
let builds = props.builds;
|
||||
let allBuilds = Persist.getBuilds();
|
||||
let unusedBuilds = [];
|
||||
let usedBuilds = [];
|
||||
|
||||
for (let id in allBuilds) {
|
||||
for (let buildName in allBuilds[id]) {
|
||||
if (!builds.find((e) => e.buildName == buildName && e.id == id)) {
|
||||
unusedBuilds.push({ id, buildName, name: Ships[id].properties.name })
|
||||
}
|
||||
let b = { id, buildName, name: Ships[id].properties.name };
|
||||
builds.find((e) => e.buildName == buildName && e.id == id) ? usedBuilds.push(b) : unusedBuilds.push(b);
|
||||
}
|
||||
}
|
||||
|
||||
builds.sort(buildComparator);
|
||||
usedBuilds.sort(buildComparator);
|
||||
unusedBuilds.sort(buildComparator);
|
||||
|
||||
this.state = { builds, unusedBuilds };
|
||||
this.state = { usedBuilds, unusedBuilds, used: usedBuilds.length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a build to the compare list
|
||||
* @param {number} buildIndex Idnex of build in list
|
||||
*/
|
||||
_addBuild(buildIndex) {
|
||||
let { builds, unusedBuilds } = this.state;
|
||||
builds.push(unusedBuilds[buildIndex]);
|
||||
unusedBuilds = unusedBuilds.splice(buildIndex, 1);
|
||||
builds.sort(buildComparator);
|
||||
let { usedBuilds, unusedBuilds } = this.state;
|
||||
usedBuilds.push(unusedBuilds[buildIndex]);
|
||||
unusedBuilds.splice(buildIndex, 1);
|
||||
usedBuilds.sort(buildComparator);
|
||||
|
||||
this.setState({ builds, unusedBuilds });
|
||||
this.setState({ used: usedBuilds.length });
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a build from the compare list
|
||||
* @param {number} buildIndex Idnex of build in list
|
||||
*/
|
||||
_removeBuild(buildIndex) {
|
||||
let { builds, unusedBuilds } = this.state;
|
||||
unusedBuilds.push(builds[buildIndex]);
|
||||
builds = builds.splice(buildIndex, 1);
|
||||
let { usedBuilds, unusedBuilds } = this.state;
|
||||
unusedBuilds.push(usedBuilds[buildIndex]);
|
||||
usedBuilds.splice(buildIndex, 1);
|
||||
unusedBuilds.sort(buildComparator);
|
||||
|
||||
this.setState({ builds, unusedBuilds });
|
||||
this.setState({ used: usedBuilds.length });
|
||||
}
|
||||
|
||||
/**
|
||||
* OK Action - Use selected builds
|
||||
*/
|
||||
_selectBuilds() {
|
||||
this.props.onSelect(this.state.builds);
|
||||
this.props.onSelect(this.state.usedBuilds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modal
|
||||
* @return {React.Component} Modal Content
|
||||
*/
|
||||
render() {
|
||||
let { builds, unusedBuilds } = this.state;
|
||||
let { usedBuilds, unusedBuilds } = this.state;
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
let availableBuilds = unusedBuilds.map((build, i) =>
|
||||
@@ -76,7 +102,7 @@ export default class ModalCompare extends TranslatedComponent {
|
||||
</tr>
|
||||
);
|
||||
|
||||
let selectedBuilds = builds.map((build, i) =>
|
||||
let selectedBuilds = usedBuilds.map((build, i) =>
|
||||
<tr key={i} onClick={this._removeBuild.bind(this, i)}>
|
||||
<td className='tl'>{build.name}</td><
|
||||
td className='tl'>{build.buildName}</td>
|
||||
@@ -102,7 +128,7 @@ export default class ModalCompare extends TranslatedComponent {
|
||||
</div>
|
||||
<br/>
|
||||
<button className='cap' onClick={this._selectBuilds.bind(this)}>{translate('Ok')}</button>
|
||||
<button className='r cap' onClick={() => InterfaceEvents.hideModal()}>{translate('Cancel')}</button>
|
||||
<button className='r cap' onClick={() => this.context.hideModal()}>{translate('Cancel')}</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import Persist from '../stores/Persist';
|
||||
|
||||
/**
|
||||
* Delete All saved data modal
|
||||
*/
|
||||
export default class ModalDeleteAll extends TranslatedComponent {
|
||||
|
||||
/**
|
||||
* Delete everything and hide the modal
|
||||
*/
|
||||
_deleteAll() {
|
||||
Persist.deleteAll();
|
||||
InterfaceEvents.hideModal();
|
||||
this.context.hideModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the component
|
||||
* @return {React.Component} Modal contents
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
return <div className='modal' onClick={(e) => e.stopPropagation()}>
|
||||
<h2>{translate('delete all')}</h2>
|
||||
<p style={{textAlign: 'center'}}>{translate('PHRASE_CONFIRMATION')}</p>
|
||||
<button className='l cap' onClick={this._deleteAll}>{translate('yes')}</button>
|
||||
<button className='r cap' onClick={InterfaceEvents.hideModal}>{translate('no')}</button>
|
||||
<p className='cen'>{translate('PHRASE_CONFIRMATION')}</p>
|
||||
<button className='l cap' onClick={this._deleteAll.bind(this)}>{translate('yes')}</button>
|
||||
<button className='r cap' onClick={this.context.hideModal}>{translate('no')}</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
|
||||
/**
|
||||
* Export Modal
|
||||
*/
|
||||
export default class ModalExport extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: React.PropTypes.string,
|
||||
promise: React.PropTypes.func,
|
||||
generator: React.PropTypes.func,
|
||||
data: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object, React.PropTypes.array])
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
let exportJson;
|
||||
|
||||
if (props.promise) {
|
||||
if (props.generator) {
|
||||
exportJson = 'Generating...';
|
||||
} else if(typeof props.data == 'string') {
|
||||
exportJson = props.data;
|
||||
@@ -25,16 +31,25 @@ export default class ModalExport extends TranslatedComponent {
|
||||
this.state = { exportJson };
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
// When promise is done update exportJson accordingly
|
||||
/**
|
||||
* If generator is provided, execute on mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
if (this.props.generator) {
|
||||
this.props.generator((str) => this.setState({ exportJson: str }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modal
|
||||
* @return {React.Component} Modal Content
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let description;
|
||||
|
||||
if (this.props.description) {
|
||||
description = <div>{translate(this.props.description)}</div>
|
||||
description = <div>{translate(this.props.description)}</div>;
|
||||
}
|
||||
|
||||
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
||||
@@ -43,7 +58,7 @@ export default class ModalExport extends TranslatedComponent {
|
||||
<div>
|
||||
<textarea className='cb json' onFocus={ (e) => e.target.select() } readOnly value={this.state.exportJson} />
|
||||
</div>
|
||||
<button className={'r dismiss cap'} onClick={InterfaceEvents.hideModal}>{translate('close')}</button>
|
||||
<button className='r dismiss cap' onClick={this.context.hideModal}>{translate('close')}</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,45 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import Persist from '../stores/Persist';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import { ModuleNameToGroup } from '../shipyard/Constants';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import { fromDetailedBuild } from '../shipyard/Serializer';
|
||||
import { Download } from './SvgIcons';
|
||||
|
||||
|
||||
const textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n');
|
||||
const lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)');
|
||||
const mountMap = { 'H': 4, 'L': 3, 'M': 2, 'S': 1, 'U': 0 };
|
||||
const standardMap = { 'RB': 0, 'TM': 1, 'FH': 2, 'EC': 3, 'PC': 4, 'SS': 5, 'FS': 6 };
|
||||
const bhMap = { 'lightweight alloy': 0, 'reinforced alloy': 1, 'military grade composite': 2, 'mirrored surface composite': 3, 'reactive surface composite': 4 };
|
||||
|
||||
/**
|
||||
* Check is slot is empty
|
||||
* @param {Object} slot Slot model
|
||||
* @return {Boolean} True if empty
|
||||
*/
|
||||
function isEmptySlot(slot) {
|
||||
return slot.maxClass == this && slot.m === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equal ignore case utility function. Must be bound to a string
|
||||
* @param {string} str String
|
||||
* @return {Boolean} True if equal
|
||||
*/
|
||||
function equalsIgnoreCase(str) {
|
||||
return str.toLowerCase() == this.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a build is valid
|
||||
* @param {string} shipId Ship ID
|
||||
* @param {string} code Serialzied ship build 'code'
|
||||
* @param {string} name Build name
|
||||
* @throws {string} If build is not valid
|
||||
*/
|
||||
function validateBuild(shipId, code, name) {
|
||||
let shipData = Ships[shipId];
|
||||
|
||||
@@ -43,6 +60,11 @@ function validateBuild(shipId, code, name) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a ship-loadout JSON object to a Coriolis build
|
||||
* @param {Object} detailedBuild ship-loadout
|
||||
* @return {Object} Coriolis build
|
||||
*/
|
||||
function detailedJsonToBuild(detailedBuild) {
|
||||
let ship;
|
||||
if (!detailedBuild.name) {
|
||||
@@ -54,7 +76,7 @@ function detailedJsonToBuild(detailedBuild) {
|
||||
}
|
||||
|
||||
try {
|
||||
ship = Serializer.fromDetailedBuild(detailedBuild);
|
||||
ship = fromDetailedBuild(detailedBuild);
|
||||
} catch (e) {
|
||||
throw detailedBuild.ship + ' Build "' + detailedBuild.name + '": Invalid data';
|
||||
}
|
||||
@@ -62,8 +84,15 @@ function detailedJsonToBuild(detailedBuild) {
|
||||
return { shipId: ship.id, name: detailedBuild.name, code: ship.toString() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Import Modal
|
||||
*/
|
||||
export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -86,6 +115,11 @@ export default class ModalImport extends TranslatedComponent {
|
||||
this._validateImport = this._validateImport.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a Coriolis backup
|
||||
* @param {Object} importData Backup Data
|
||||
* @throws {string} If import fails
|
||||
*/
|
||||
_importBackup(importData) {
|
||||
if (importData.builds && typeof importData.builds == 'object') {
|
||||
for (let shipId in importData.builds) {
|
||||
@@ -117,6 +151,10 @@ export default class ModalImport extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import an array of ship-loadout objects / builds
|
||||
* @param {Array} importArr Array of ship-loadout JSON Schema builds
|
||||
*/
|
||||
_importDetailedArray(importArr) {
|
||||
let builds = {};
|
||||
for (let i = 0, l = importArr.length; i < l; i++) {
|
||||
@@ -129,6 +167,11 @@ export default class ModalImport extends TranslatedComponent {
|
||||
this.setState({ builds });
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a text build from ED Shipyard
|
||||
* @param {string} buildStr Build string
|
||||
* @throws {string} If parse / import fails
|
||||
*/
|
||||
_importTextBuild(buildStr) {
|
||||
let buildName = textBuildRegex.exec(buildStr)[1].trim();
|
||||
let shipName = buildName.toLowerCase();
|
||||
@@ -173,43 +216,40 @@ export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
if (cl > slotClass) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; }
|
||||
|
||||
slot = _.find(ship.hardpoints, isEmptySlot, slotClass);
|
||||
slot = ship.hardpoints.find(isEmptySlot, slotClass);
|
||||
|
||||
if (!slot) { throw 'No hardpoint slot available for: "' + line + '"'; }
|
||||
|
||||
group = _.find(GroupMap, equalsIgnoreCase, name);
|
||||
group = ModuleNameToGroup[name.trim()];
|
||||
|
||||
let hp = ModuleUtils.findHardpoint(group, cl, rating, group ? null : name, mount, missile);
|
||||
|
||||
if (!hp) { throw 'Unknown component: "' + line + '"'; }
|
||||
|
||||
ship.use(slot, hp, true);
|
||||
|
||||
} else if (typeSize == 'BH') {
|
||||
let bhId = bhMap[name.toLowerCase()];
|
||||
|
||||
if (bhId === undefined) { throw 'Unknown bulkhead: "' + line + '"'; }
|
||||
|
||||
ship.useBulkhead(bhId, true);
|
||||
|
||||
} else if (standardMap[typeSize] != undefined) {
|
||||
let standardIndex = standardMap[typeSize];
|
||||
|
||||
if (ship.standard[standardIndex].maxClass < cl) { throw name + ' exceeds max class for the ' + ship.name; }
|
||||
|
||||
ship.use(ship.standard[standardIndex], cl + rating, ModuleUtils.standard(standardIndex, cl + rating), true);
|
||||
|
||||
} else {
|
||||
throw 'Unknown component: "' + line + '"';
|
||||
}
|
||||
} else {
|
||||
if (cl > typeSize) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; }
|
||||
|
||||
slot = _.find(ship.internal, isEmptySlot, typeSize);
|
||||
slot = ship.internal.find(isEmptySlot, typeSize);
|
||||
|
||||
if (!slot) { throw 'No internal slot available for: "' + line + '"'; }
|
||||
|
||||
group = _.find(GroupMap, equalsIgnoreCase, name);
|
||||
group = ModuleNameToGroup[name.trim()];
|
||||
|
||||
let intComp = ModuleUtils.findInternal(group, cl, rating, group ? null : name);
|
||||
|
||||
@@ -221,13 +261,18 @@ export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
let builds = {};
|
||||
builds[shipId] = {};
|
||||
builds[shipId]['Imported ' + buildName] = Serializer.fromShip(ship);
|
||||
builds[shipId]['Imported ' + buildName] = ship.toString();
|
||||
this.setState({ builds });
|
||||
}
|
||||
|
||||
_validateImport(e) {
|
||||
/**
|
||||
* Validate the import string / text box contents
|
||||
* @param {SyntheticEvent} event Event
|
||||
* @throws {string} If validation fails
|
||||
*/
|
||||
_validateImport(event) {
|
||||
let importData = null;
|
||||
let importString = e.target.value;
|
||||
let importString = event.target.value;
|
||||
this.setState({
|
||||
builds: null,
|
||||
comparisons: null,
|
||||
@@ -244,7 +289,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
try {
|
||||
if (textBuildRegex.test(importString)) { // E:D Shipyard build text
|
||||
importTextBuild(importString);
|
||||
this._importTextBuild(importString);
|
||||
} else { // JSON Build data
|
||||
importData = JSON.parse(importString);
|
||||
|
||||
@@ -268,6 +313,9 @@ export default class ModalImport extends TranslatedComponent {
|
||||
this.setState({ importValid: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Process imported data
|
||||
*/
|
||||
_process() {
|
||||
let builds = null, comparisons = null;
|
||||
|
||||
@@ -278,7 +326,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
let code = builds[shipId][buildName];
|
||||
// Update builds object such that orginal name retained, but can be renamed
|
||||
builds[shipId][buildName] = {
|
||||
code: code,
|
||||
code,
|
||||
useName: buildName
|
||||
};
|
||||
}
|
||||
@@ -295,8 +343,10 @@ export default class ModalImport extends TranslatedComponent {
|
||||
this.setState({ processed: true, builds, comparisons });
|
||||
};
|
||||
|
||||
/**
|
||||
* Import parsed, processed data and save
|
||||
*/
|
||||
_import() {
|
||||
|
||||
if (this.state.builds) {
|
||||
let builds = this.state.builds;
|
||||
for (let shipId in builds) {
|
||||
@@ -329,16 +379,33 @@ export default class ModalImport extends TranslatedComponent {
|
||||
Persist.setInsurance(this.state.insurance);
|
||||
}
|
||||
|
||||
InterfaceEvents.hideModal();
|
||||
this.context.hideModal();
|
||||
};
|
||||
|
||||
/**
|
||||
* Capture build name changes
|
||||
* @param {Object} build Build import object
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_changeBuildName(build, e) {
|
||||
build.useName = e.target.value;
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* If imported data is already provided process immediately on mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
if (this.props.importingBuilds) {
|
||||
this.setState({ builds: this.props.importingBuilds, canEdit : false});
|
||||
this.setState({ builds: this.props.importingBuilds, canEdit : false });
|
||||
this._process();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the import modal
|
||||
* @return {React.Component} Modal contents
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let state = this.state;
|
||||
@@ -357,27 +424,27 @@ export default class ModalImport extends TranslatedComponent {
|
||||
if (state.comparisons) {
|
||||
let comparisonRows = [];
|
||||
|
||||
for (let name in comparisons) {
|
||||
let comparison = comparisons[name];
|
||||
for (let name in state.comparisons) {
|
||||
let comparison = state.comparisons[name];
|
||||
let hasComparison = Persist.hasComparison(name);
|
||||
comparisonRows.push(
|
||||
<tr key={name} className='cb'>
|
||||
<td>
|
||||
<input type='text' value={comparison.useName}/>
|
||||
</td>
|
||||
<td style={{ textAlign:'center' }} className={ cn({ warning: hasComparison, disabled: comparison.useName == '' }) }>
|
||||
<span>{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}></span>
|
||||
<td style={{ textAlign:'center' }} className={ cn('cap', { warning: hasComparison, disabled: comparison.useName == '' }) }>
|
||||
{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
comparisonTable = (
|
||||
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%'}} >
|
||||
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%' }} >
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign:'left' }}>{translate('comparison')}</th>
|
||||
<th >{translate('action')}</th>
|
||||
<th>{translate('action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -388,7 +455,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
}
|
||||
|
||||
if(this.state.canEdit) {
|
||||
edit = <button className='l cap' style={{ marginLeft: '2em' }} onClick={() => this.setState({processed: false})}>{translate('edit data')}</button>
|
||||
edit = <button className='l cap' style={{ marginLeft: '2em' }} onClick={() => this.setState({ processed: false })}>{translate('edit data')}</button>;
|
||||
}
|
||||
|
||||
let builds = this.state.builds;
|
||||
@@ -398,11 +465,11 @@ export default class ModalImport extends TranslatedComponent {
|
||||
let b = shipBuilds[buildName];
|
||||
let hasBuild = Persist.hasBuild(shipId, b.useName);
|
||||
buildRows.push(
|
||||
<tr className='cb'>
|
||||
<tr key={shipId + buildName} className='cb'>
|
||||
<td>{Ships[shipId].properties.name}</td>
|
||||
<td><input type='text' value={b.useName}/></td>
|
||||
<td style={{ textAlign: 'center' }} className={cn({ warning: hasBuild, disabled: b.useName == ''})}>
|
||||
<span>{translate(b.useName == '' ? 'skip' : (hasBuild ? 'overwrite' : 'create'))}></span>
|
||||
<td><input type='text' onChange={this._changeBuildName.bind(this, b)} value={b.useName}/></td>
|
||||
<td style={{ textAlign: 'center' }} className={cn('cap', { warning: hasBuild, disabled: b.useName == '' })}>
|
||||
{translate(b.useName == '' ? 'skip' : (hasBuild ? 'overwrite' : 'create'))}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -411,12 +478,12 @@ export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
importStage = (
|
||||
<div>
|
||||
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%'}}>
|
||||
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left' }} >{translate('ship')}</th>
|
||||
<th style={{ textAlign: 'left' }} >{translate('build name')}</th>
|
||||
<th >{translate('action')}</th>
|
||||
<th>{translate('action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -433,7 +500,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
||||
<h2 >{translate('import')}</h2>
|
||||
{importStage}
|
||||
<button className={'r dismiss cap'} onClick={InterfaceEvents.hideModal}>{translate('close')}</button>
|
||||
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import ShortenUrl from '../utils/ShortenUrl';
|
||||
|
||||
/**
|
||||
* Permalink modal
|
||||
*/
|
||||
export default class ModalPermalink extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
url: React.propTypes.string.isRequired
|
||||
url: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -16,13 +23,20 @@ export default class ModalPermalink extends TranslatedComponent {
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
/**
|
||||
* Shorten URL on mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
ShortenUrl(this.props.url,
|
||||
(shortenedUrl) => this.setState({ shortenedUrl }),
|
||||
(error) => this.setState({ shortenedUrl: 'Error - ' + e.statusText })
|
||||
(error) => this.setState({ shortenedUrl: 'Error - ' + error })
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modal
|
||||
* @return {React.Component} Modal Content
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
@@ -30,12 +44,12 @@ export default class ModalPermalink extends TranslatedComponent {
|
||||
<h2>{translate('permalink')}</h2>
|
||||
<br/>
|
||||
<h3>{translate('URL')}</h3>
|
||||
<input value={this.props.url} size={40} onFocus={ (e) => e.target.select() }/>
|
||||
<input value={this.props.url} size={40} readOnly onFocus={ (e) => e.target.select() }/>
|
||||
<br/><br/>
|
||||
<h3 >{translate('shortened')}</h3>
|
||||
<input value={this.state.shortenedUrl} size={25} onFocus={ (e) => e.target.select() }/>
|
||||
<input value={this.state.shortenedUrl} readOnly size={25} onFocus={ (e) => e.target.select() }/>
|
||||
<br/><br/>
|
||||
<button className={'r dismiss cap'} onClick={InterfaceEvents.hideModal}>{translate('close')}</button>
|
||||
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,34 @@ import React from 'react';
|
||||
import d3 from 'd3';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { wrapCtxMenu } from '../utils/InterfaceEvents';
|
||||
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||
|
||||
/**
|
||||
* Round to avoid floating point precision errors
|
||||
* @param {[type]} selected [description]
|
||||
* @param {[type]} sum [description]
|
||||
* @param {[type]} avail [description]
|
||||
* @return {[type]} [description]
|
||||
* @param {Boolean} selected Band selected
|
||||
* @param {number} sum Band power sum
|
||||
* @param {number} avail Total available power
|
||||
* @return {string} CSS Class name
|
||||
*/
|
||||
function getClass(selected, sum, avail) {
|
||||
return selected ? 'secondary' : ((Math.round(sum * 100) / 100) >= avail) ? 'warning' : 'primary';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the # label for a Priority band
|
||||
* @param {number} val Priority Band Watt value
|
||||
* @param {number} index Priority Band index
|
||||
* @param {Function} wattScale Watt Scale function
|
||||
* @return {number} label / text
|
||||
*/
|
||||
function bandText(val, index, wattScale) {
|
||||
return (val > 0 && wattScale(val) > 13) ? index + 1 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Power Bands Component
|
||||
* Renders the SVG to simulate in-game power bands
|
||||
*/
|
||||
export default class PowerBands extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -28,6 +39,11 @@ export default class PowerBands extends TranslatedComponent {
|
||||
code: React.PropTypes.string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this.wattScale = d3.scale.linear();
|
||||
@@ -38,6 +54,7 @@ export default class PowerBands extends TranslatedComponent {
|
||||
this._updateDimensions = this._updateDimensions.bind(this);
|
||||
this._updateScales = this._updateScales.bind(this);
|
||||
this._selectNone = this._selectNone.bind(this);
|
||||
this._hidetip = () => this.context.tooltip();
|
||||
|
||||
let maxBand = props.bands[props.bands.length - 1];
|
||||
|
||||
@@ -52,13 +69,18 @@ export default class PowerBands extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
_updateDimensions(props, size) {
|
||||
let barHeight = Math.round(20 * size);
|
||||
/**
|
||||
* Update dimensions based on properties and scale
|
||||
* @param {Object} props React Component properties
|
||||
* @param {number} scale size ratio / scale
|
||||
*/
|
||||
_updateDimensions(props, scale) {
|
||||
let barHeight = Math.round(20 * scale);
|
||||
let innerHeight = (barHeight * 2) + 2;
|
||||
let mTop = Math.round(25 * size);
|
||||
let mBottom = Math.round(25 * size);
|
||||
let mLeft = Math.round(45 * size);
|
||||
let mRight = Math.round(140 * size);
|
||||
let mTop = Math.round(25 * scale);
|
||||
let mBottom = Math.round(25 * scale);
|
||||
let mLeft = Math.round(45 * scale);
|
||||
let mRight = Math.round(140 * scale);
|
||||
let innerWidth = props.width - mLeft - mRight;
|
||||
|
||||
this._updateScales(innerWidth, this.state.maxPwr, props.available);
|
||||
@@ -77,6 +99,9 @@ export default class PowerBands extends TranslatedComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Select no bands
|
||||
*/
|
||||
_selectNone() {
|
||||
this.setState({
|
||||
ret : {},
|
||||
@@ -84,6 +109,10 @@ export default class PowerBands extends TranslatedComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a retracted band
|
||||
* @param {number} index Band index
|
||||
*/
|
||||
_selectRet(index) {
|
||||
let ret = this.state.ret;
|
||||
if(ret[index]) {
|
||||
@@ -95,6 +124,10 @@ export default class PowerBands extends TranslatedComponent {
|
||||
this.setState({ ret: Object.assign({}, ret) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a deployed band
|
||||
* @param {number} index Band index
|
||||
*/
|
||||
_selectDep(index) {
|
||||
let dep = this.state.dep;
|
||||
|
||||
@@ -107,31 +140,45 @@ export default class PowerBands extends TranslatedComponent {
|
||||
this.setState({ dep: Object.assign({}, dep) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update scale
|
||||
* @param {number} innerWidth SVG innerwidth
|
||||
* @param {number} maxPwr Maximum power level MJ (deployed or available)
|
||||
* @param {number} available Available power MJ
|
||||
*/
|
||||
_updateScales(innerWidth, maxPwr, available) {
|
||||
this.wattScale.range([0, innerWidth]).domain([0,maxPwr]).clamp(true);
|
||||
this.wattScale.range([0, innerWidth]).domain([0, maxPwr]).clamp(true);
|
||||
this.pctScale.range([0, innerWidth]).domain([0, maxPwr / available]).clamp(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next context
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
let { innerWidth, maxPwr } = this.state;
|
||||
let { language, sizeRatio } = this.context;
|
||||
let maxBand = nextProps.bands[nextProps.bands.length - 1];
|
||||
let nextMaxPwr = Math.max(nextProps.available, maxBand.retractedSum, maxBand.deployedSum);
|
||||
|
||||
if (maxPwr != nextMaxPwr) { // Update Axes if max power has changed
|
||||
this._updateScales(innerWidth, nextMaxPwr, nextProps.available);
|
||||
this.setState({ maxPwr: nextMaxPwr });
|
||||
}
|
||||
|
||||
if (this.context !== nextContext) {
|
||||
if (language !== nextContext.language) {
|
||||
this.wattAxis.tickFormat(nextContext.language.formats.r2);
|
||||
this.pctAxis.tickFormat(nextContext.language.formats.rPct);
|
||||
}
|
||||
|
||||
if (nextProps.width != this.props.width || this.context !== nextContext) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the power bands
|
||||
* @return {React.Component} Power bands
|
||||
*/
|
||||
render() {
|
||||
if (!this.props.width) {
|
||||
return null;
|
||||
@@ -142,7 +189,7 @@ export default class PowerBands extends TranslatedComponent {
|
||||
let { f2, pct1, rPct, r2 } = formats; // wattFmt, pctFmt, pctAxis, wattAxis
|
||||
let { available, bands, width } = props;
|
||||
let { innerWidth, maxPwr, ret, dep } = state;
|
||||
let pwrWarningClass = cn('threshold', {exceeded: bands[0].retractedSum * 2 >= available });
|
||||
let pwrWarningClass = cn('threshold', { exceeded: bands[0].retractedSum * 2 >= available });
|
||||
let deployed = [];
|
||||
let retracted = [];
|
||||
let retSelected = Object.keys(ret).length > 0;
|
||||
@@ -150,7 +197,7 @@ export default class PowerBands extends TranslatedComponent {
|
||||
let retSum = 0;
|
||||
let depSum = 0;
|
||||
|
||||
for (var i = 0; i < bands.length; i++) {
|
||||
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;
|
||||
@@ -217,14 +264,13 @@ export default class PowerBands extends TranslatedComponent {
|
||||
<g className='power-band'>{deployed}</g>
|
||||
<g ref={ (elem) => d3.select(elem).call(this.wattAxis) } className='watt axis'></g>
|
||||
<g ref={ (elem) => {
|
||||
let axis = d3.select(elem);
|
||||
axis.call(this.pctAxis);
|
||||
axis.select('g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass);
|
||||
}}
|
||||
className='pct axis' transform={`translate(0,${state.innerHeight})`}></g>
|
||||
let axis = d3.select(elem);
|
||||
axis.call(this.pctAxis);
|
||||
axis.select('g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass);
|
||||
}} className='pct axis' transform={`translate(0,${state.innerHeight})`}></g>
|
||||
<line x1={pctScale(0.5)} x2={pctScale(0.5)} y1='0' y2={state.innerHeight} className={pwrWarningClass} />
|
||||
<text dy='0.5em' x='-3' y={state.retY} className='primary upp' textAnchor='end'>{translate('ret')}</text>
|
||||
<text dy='0.5em' x='-3' y={state.depY} className='primary upp' textAnchor='end'>{translate('dep')}</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', 's')} onMouseLeave={this._hidetip}>{translate('dep')}</text>
|
||||
<text dy='0.5em' x={innerWidth + 5} y={state.retY} className={getClass(retSelected, retSum, available)}>{f2(Math.max(0, retSum)) + ' (' + pct1(Math.max(0, retSum / available)) + ')'}</text>
|
||||
<text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, available)}>{f2(Math.max(0, depSum)) + ' (' + pct1(Math.max(0, depSum / available)) + ')'}</text>
|
||||
</g>
|
||||
|
||||
@@ -2,9 +2,8 @@ import React from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import PowerBands from './PowerBands';
|
||||
import { slotName, nameComparator } from '../utils/SlotFunctions';
|
||||
import { slotName, slotComparator } from '../utils/SlotFunctions';
|
||||
import { Power, NoPower } from './SvgIcons';
|
||||
|
||||
const POWER = [
|
||||
@@ -14,6 +13,9 @@ const POWER = [
|
||||
<Power className='secondary-disabled' />
|
||||
];
|
||||
|
||||
/**
|
||||
* Power Management Section
|
||||
*/
|
||||
export default class PowerManagement extends TranslatedComponent {
|
||||
static PropTypes = {
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
@@ -21,6 +23,10 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._renderPowerRows = this._renderPowerRows.bind(this);
|
||||
@@ -34,51 +40,77 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sort order and sort
|
||||
* @param {string} predicate Sort predicate
|
||||
*/
|
||||
_sortOrder(predicate) {
|
||||
let desc = this.state.desc;
|
||||
|
||||
if (predicate == this.state.predicate) {
|
||||
desc = !desc;
|
||||
|
||||
} else {
|
||||
desc = true;
|
||||
}
|
||||
|
||||
this._sort(this.props.ship, predicate, desc);
|
||||
this.setState({ predicate, desc });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the power list
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {string} predicate Sort predicate
|
||||
* @param {Boolean} desc Sort order descending
|
||||
*/
|
||||
_sort(ship, predicate, desc) {
|
||||
let powerList = ship.powerList;
|
||||
let comp = slotComparator.bind(null, this.context.language.translate);
|
||||
|
||||
switch (predicate) {
|
||||
case 'n': powerList.sort(nameComparator(this.context.language.translate)); break;
|
||||
case 't': powerList.sort((a, b) => a.type.localeCompare(b.type)); break;
|
||||
case 'pri': powerList.sort((a, b) => a.priority - b.priority);break;
|
||||
case 'pwr': powerList.sort((a, b) => (a.m ? a.m.power : 0) - (b.m ? b.m.power : 0)); break;
|
||||
case 'r': powerList.sort((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b)); break;
|
||||
case 'd': powerList.sort((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true)); break;
|
||||
case 'n': comp = comp(null, desc); break;
|
||||
case 't': comp = comp((a, b) => a.type.localeCompare(b.type), desc); break;
|
||||
case 'pri': comp = comp((a, b) => a.priority - b.priority, desc); break;
|
||||
case 'pwr': comp = comp((a, b) => a.m.power - b.m.power, desc); break;
|
||||
case 'r': comp = comp((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b), desc); break;
|
||||
case 'd': comp = comp((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true), desc); break;
|
||||
}
|
||||
|
||||
if (!desc) {
|
||||
powerList.reverse();
|
||||
}
|
||||
powerList.sort(comp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update slot priority
|
||||
* @param {Object} slot Slot model
|
||||
* @param {number} inc increment / decrement
|
||||
*/
|
||||
_priority(slot, inc) {
|
||||
if (this.props.ship.setSlotPriority(slot, slot.priority + inc)) {
|
||||
this.props.onChange();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle slot active/inactive
|
||||
* @param {Object} slot Slot model
|
||||
*/
|
||||
_toggleEnabled(slot) {
|
||||
this.props.ship.setSlotEnabled(slot, !slot.enabled);
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate/Render table rows
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Function} pwr Localized Power formatter
|
||||
* @param {Function} pct Localized Percent formatter
|
||||
* @return {Array} Array of React.Component table rows
|
||||
*/
|
||||
_renderPowerRows(ship, translate, pwr, pct) {
|
||||
|
||||
let powerRows = [];
|
||||
|
||||
for (var i = 0, l = ship.powerList.length; i < l; i++) {
|
||||
for (let i = 0, l = ship.powerList.length; i < l; i++) {
|
||||
let slot = ship.powerList[i];
|
||||
|
||||
if (slot.m && slot.m.power) {
|
||||
@@ -102,7 +134,7 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
{' ' + (slot.priority + 1) + ' '}
|
||||
<span className='ptr' onClick={this._priority.bind(this, slot, 1)}>►</span>
|
||||
</td>
|
||||
<td className='ri ptr' style={{ width: '3.25em'}} onClick={toggleEnabled}>{pwr(m.power)}</td>
|
||||
<td className='ri ptr' style={{ width: '3.25em' }} onClick={toggleEnabled}>{pwr(m.power)}</td>
|
||||
<td className='ri ptr' style={{ width: '3em' }} onClick={toggleEnabled}><u>{pct(m.power / ship.powerAvailable)}</u></td>
|
||||
{retractedElem}
|
||||
{deployedElem}
|
||||
@@ -112,32 +144,50 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
return powerRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update power bands width from DOM
|
||||
*/
|
||||
_updateWidth() {
|
||||
this.setState({ width: findDOMNode(this).offsetWidth });
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
/**
|
||||
* Add listeners when about to mount and sort power list
|
||||
*/
|
||||
componentWillMount() {
|
||||
this._sort(this.props.ship, this.state.predicate, this.state.desc);
|
||||
this.resizeListener = InterfaceEvents.addListener('windowResized', this._updateWidth);
|
||||
this.resizeListener = this.context.onWindowResize(this._updateWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger DOM updates on mount
|
||||
*/
|
||||
componentDidMount() {
|
||||
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) {
|
||||
// Can optimize this later: only sort when
|
||||
// - predicate/desc changes
|
||||
// - modules/language change AND sorting by type, name
|
||||
// - power changes and sorting by pwr
|
||||
// - enabled/disabled changes and sorting by priority
|
||||
this._sort(nextProps.ship, nextState.predicate, nextState.desc);
|
||||
if (this.props.ship != nextProps.ship) {
|
||||
this._sort(nextProps.ship, nextState.predicate, nextState.desc);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
/**
|
||||
* Remove listeners on unmount
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this.resizeListener.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render power management section
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
let { ship, code } = this.props;
|
||||
let { translate, formats } = this.context.language;
|
||||
|
||||
@@ -4,86 +4,91 @@ import cn from 'classnames';
|
||||
import { SizeMap } from '../shipyard/Constants';
|
||||
import { Warning } from './SvgIcons';
|
||||
|
||||
/**
|
||||
* Ship Summary Table / Stats
|
||||
*/
|
||||
export default class ShipSummaryTable extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
ship: React.PropTypes.object.isRequired
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the table
|
||||
* @return {React.Component} Summary table
|
||||
*/
|
||||
render() {
|
||||
let ship = this.props.ship;
|
||||
let language = this.context.language;
|
||||
let { language, tooltip, termtip } = this.context;
|
||||
let translate = language.translate;
|
||||
let u = language.units;
|
||||
let formats = language.formats;
|
||||
let round = formats.round;
|
||||
let int = formats.int;
|
||||
let armourDetails = null;
|
||||
let hide = tooltip.bind(null, null);
|
||||
|
||||
if (ship.armourMultiplier > 1 || ship.armourAdded) {
|
||||
armourDetails = <u>({
|
||||
(ship.armourMultiplier > 1 ? formats.rPct(ship.armourMultiplier) : '')
|
||||
+ (ship.armourAdded ? ' + ' + ship.armourAdded : '')
|
||||
(ship.armourMultiplier > 1 ? formats.rPct(ship.armourMultiplier) : '') +
|
||||
(ship.armourAdded ? ' + ' + ship.armourAdded : '')
|
||||
})</u>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='summary'>
|
||||
<table id='summaryTable'>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th rowSpan={2}>{translate('size')}</th>
|
||||
<th rowSpan={2}>{translate('agility')}</th>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canThrust() }) }>{translate('speed')}</th>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canBoost() }) }>{translate('boost')}</th>
|
||||
<th rowSpan={2}>{translate('DPS')}</th>
|
||||
<th rowSpan={2}>{translate('armour')}</th>
|
||||
<th rowSpan={2}>{translate('shields')}</th>
|
||||
<th colSpan={3}>{translate('mass')}</th>
|
||||
<th rowSpan={2}>{translate('cargo')}</th>
|
||||
<th rowSpan={2}>{translate('fuel')}</th>
|
||||
<th colSpan={3}>{translate('jump range')}</th>
|
||||
<th colSpan={3}>{translate('total range')}</th>
|
||||
<th rowSpan={2}>{translate('lock factor')}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className='lft'>{translate('hull')}</th>
|
||||
<th>{translate('unladen')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
<th className='lft'>{translate('max')}</th>
|
||||
<th>{translate('full tank')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
<th className='lft'>{translate('jumps')}</th>
|
||||
<th>{translate('unladen')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className='cap'>{translate(SizeMap[ship.class])}</td>
|
||||
<td>{ship.agility}/10</td>
|
||||
<td>{ ship.canThrust() ? <span>{int(ship.topSpeed)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{ ship.canBoost() ? <span>{int(ship.topBoost)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{round(ship.totalDps)}</td>
|
||||
<td>{int(ship.armour)} {armourDetails}</td>
|
||||
<td>{int(ship.shieldStrength)} {u.MJ} { ship.shieldMultiplier > 1 && ship.shieldStrength > 0 ? <u>({formats.rPct(ship.shieldMultiplier)})</u> : null }</td>
|
||||
<td>{ship.hullMass} {u.T}</td>
|
||||
<td>{round(ship.unladenMass)} {u.T}</td>
|
||||
<td>{round(ship.ladenMass)} {u.T}</td>
|
||||
<td>{round(ship.cargoCapacity)} {u.T}</td>
|
||||
<td>{round(ship.fuelCapacity)} {u.T}</td>
|
||||
<td>{round(ship.unladenRange)} {u.LY}</td>
|
||||
<td>{round(ship.fullTankRange)} {u.LY}</td>
|
||||
<td>{round(ship.ladenRange)} {u.LY}</td>
|
||||
<td>{round(ship.maxJumpCount)}</td>
|
||||
<td>{round(ship.unladenTotalRange)} {u.LY}</td>
|
||||
<td>{round(ship.ladenTotalRange)} {u.LY}</td>
|
||||
<td>{ship.masslock}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
return <div id='summary'>
|
||||
<table id='summaryTable'>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th rowSpan={2}>{translate('size')}</th>
|
||||
<th onMouseEnter={termtip.bind(null, 'maneuverability')} onMouseLeave={hide} rowSpan={2}>{translate('MNV')}</th>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canThrust() }) }>{translate('speed')}</th>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canBoost() }) }>{translate('boost')}</th>
|
||||
<th onMouseOver={termtip.bind(null, 'damage per second')} onMouseOut={hide} rowSpan={2}>{translate('DPS')}</th>
|
||||
<th rowSpan={2}>{translate('armour')}</th>
|
||||
<th rowSpan={2}>{translate('shields')}</th>
|
||||
<th colSpan={3}>{translate('mass')}</th>
|
||||
<th rowSpan={2}>{translate('cargo')}</th>
|
||||
<th rowSpan={2}>{translate('fuel')}</th>
|
||||
<th colSpan={3}>{translate('jump range')}</th>
|
||||
<th colSpan={3}>{translate('total range')}</th>
|
||||
<th onMouseOver={termtip.bind(null, 'mass lock factor')} onMouseOut={hide} rowSpan={2}>{translate('MLF')}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className='lft'>{translate('hull')}</th>
|
||||
<th>{translate('unladen')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
<th className='lft'>{translate('max')}</th>
|
||||
<th>{translate('full tank')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
<th className='lft'>{translate('jumps')}</th>
|
||||
<th>{translate('unladen')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className='cap'>{translate(SizeMap[ship.class])}</td>
|
||||
<td>{ship.agility}/10</td>
|
||||
<td>{ ship.canThrust() ? <span>{int(ship.topSpeed)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{ ship.canBoost() ? <span>{int(ship.topBoost)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{round(ship.totalDps)}</td>
|
||||
<td>{int(ship.armour)} {armourDetails}</td>
|
||||
<td>{int(ship.shieldStrength)} {u.MJ} { ship.shieldMultiplier > 1 && ship.shieldStrength > 0 ? <u>({formats.rPct(ship.shieldMultiplier)})</u> : null }</td>
|
||||
<td>{ship.hullMass} {u.T}</td>
|
||||
<td>{round(ship.unladenMass)} {u.T}</td>
|
||||
<td>{round(ship.ladenMass)} {u.T}</td>
|
||||
<td>{round(ship.cargoCapacity)} {u.T}</td>
|
||||
<td>{round(ship.fuelCapacity)} {u.T}</td>
|
||||
<td>{round(ship.unladenRange)} {u.LY}</td>
|
||||
<td>{round(ship.fullTankRange)} {u.LY}</td>
|
||||
<td>{round(ship.ladenRange)} {u.LY}</td>
|
||||
<td>{round(ship.maxJumpCount)}</td>
|
||||
<td>{round(ship.unladenTotalRange)} {u.LY}</td>
|
||||
<td>{round(ship.ladenTotalRange)} {u.LY}</td>
|
||||
<td>{ship.masslock}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Horizontal Slider
|
||||
*/
|
||||
export default class Slider extends React.Component {
|
||||
|
||||
static defaultProps = {
|
||||
@@ -16,6 +19,10 @@ export default class Slider extends React.Component {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -23,27 +30,47 @@ export default class Slider extends React.Component {
|
||||
this.up = this.up.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* On Mouse down handler
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
down(e) {
|
||||
let rect = e.currentTarget.getBoundingClientRect();
|
||||
this.move = this.updatePercent.bind(this, rect.left, rect.width);
|
||||
this.move = this._updatePercent.bind(this, rect.left, rect.width);
|
||||
this.move(e);
|
||||
document.addEventListener("mousemove", this.move);
|
||||
document.addEventListener("mouseup", this.up);
|
||||
document.addEventListener('mousemove', this.move);
|
||||
document.addEventListener('mouseup', this.up);
|
||||
}
|
||||
|
||||
/**
|
||||
* On Mouse up handler
|
||||
*/
|
||||
up() {
|
||||
document.removeEventListener("mousemove", this.move);
|
||||
document.removeEventListener("mouseup", this.up);
|
||||
document.removeEventListener('mousemove', this.move);
|
||||
document.removeEventListener('mouseup', this.up);
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
this.up();
|
||||
}
|
||||
|
||||
updatePercent(left, width, event) {
|
||||
/**
|
||||
* Update the slider percentage
|
||||
* @param {number} left Slider left position
|
||||
* @param {number} width Slider width
|
||||
* @param {Event} event Event
|
||||
*/
|
||||
_updatePercent(left, width, event) {
|
||||
this.props.onChange(Math.min(Math.max((event.clientX - left) / width, 0), 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove listeners on unmount
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the slider
|
||||
* @return {React.Component} The slider
|
||||
*/
|
||||
render() {
|
||||
let pctStr = (this.props.percent * 100) + '%';
|
||||
let { axis, axisUnit, min, max } = this.props;
|
||||
@@ -51,9 +78,9 @@ export default class Slider extends React.Component {
|
||||
|
||||
if (axis) {
|
||||
axisGroup = <g style={{ fontSize: '.7em' }}>
|
||||
<text className='primary-disabled' y="3em" x="0" 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="99%" style={{ textAnchor: 'middle' }}>{max + axisUnit}</text>
|
||||
<text className='primary-disabled' y='3em' x='0' 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='99%' style={{ textAnchor: 'middle' }}>{max + axisUnit}</text>
|
||||
</g>;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,12 @@ import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import cn from 'classnames';
|
||||
import AvailableModulesMenu from './AvailableModulesMenu';
|
||||
import { wrapCtxMenu } from '../utils/InterfaceEvents';
|
||||
import { diffDetails } from '../utils/SlotFunctions';
|
||||
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||
|
||||
/**
|
||||
* Abstract Slot
|
||||
*/
|
||||
export default class Slot extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -13,10 +17,17 @@ export default class Slot extends TranslatedComponent {
|
||||
maxClass: React.PropTypes.number.isRequired,
|
||||
selected: React.PropTypes.bool,
|
||||
m: React.PropTypes.object,
|
||||
shipMass: React.PropTypes.number,
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
warning: React.PropTypes.func,
|
||||
drag: React.PropTypes.func,
|
||||
drop: React.PropTypes.func,
|
||||
dropClass: React.PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -27,6 +38,11 @@ export default class Slot extends TranslatedComponent {
|
||||
// Must be implemented by subclasses:
|
||||
// _getSlotDetails()
|
||||
|
||||
/**
|
||||
* Get the CSS class name for the slot. Can/should be overriden
|
||||
* as necessary.
|
||||
* @return {string} CSS Class name
|
||||
*/
|
||||
_getClassNames() {
|
||||
return null;
|
||||
}
|
||||
@@ -40,14 +56,22 @@ export default class Slot extends TranslatedComponent {
|
||||
return this.props.maxClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty slot on right-click
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_contextMenu(event) {
|
||||
this.props.onSelect(null,null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the slot
|
||||
* @return {React.Component} The slot
|
||||
*/
|
||||
render() {
|
||||
let language = this.context.language;
|
||||
let translate = language.translate;
|
||||
let m = this.props.m;
|
||||
let { ship, m, dropClass, dragOver, onOpen, selected, onSelect, warning, shipMass, availableModules } = this.props;
|
||||
let slotDetails, menu;
|
||||
|
||||
if (m) {
|
||||
@@ -59,18 +83,19 @@ export default class Slot extends TranslatedComponent {
|
||||
if (this.props.selected) {
|
||||
menu = <AvailableModulesMenu
|
||||
className={this._getClassNames()}
|
||||
modules={this.props.availableModules()}
|
||||
shipMass={this.props.shipMass}
|
||||
modules={availableModules()}
|
||||
shipMass={ship.hullMass}
|
||||
m={m}
|
||||
onSelect={this.props.onSelect}
|
||||
warning={this.props.warning}
|
||||
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={this._contextMenu}>
|
||||
<div className={'details'}>
|
||||
<div className={'sz'}>{this._getMaxClassLabel(translate)}</div>
|
||||
<div className={cn('slot', dropClass, { selected })} onClick={onOpen} onContextMenu={this._contextMenu} onDragOver={dragOver}>
|
||||
<div className='details-container'>
|
||||
<div className='sz'>{this._getMaxClassLabel(translate)}</div>
|
||||
{slotDetails}
|
||||
</div>
|
||||
{menu}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import { wrapCtxMenu } from '../utils/InterfaceEvents';
|
||||
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||
import { Equalizer } from '../components/SvgIcons';
|
||||
import cn from 'classnames';
|
||||
|
||||
/**
|
||||
* Abstract Slot Section
|
||||
*/
|
||||
export default class SlotSection extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -14,6 +16,13 @@ export default class SlotSection extends TranslatedComponent {
|
||||
togglePwr: React.PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
* @param {string} sectionId Section DOM Id
|
||||
* @param {string} sectionName Section name
|
||||
*/
|
||||
constructor(props, context, sectionId, sectionName) {
|
||||
super(props);
|
||||
this.sectionId = sectionId;
|
||||
@@ -23,7 +32,10 @@ export default class SlotSection extends TranslatedComponent {
|
||||
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 = {};
|
||||
}
|
||||
|
||||
// Must be implemented by subclasses:
|
||||
@@ -31,33 +43,143 @@ export default class SlotSection extends TranslatedComponent {
|
||||
// _getSectionMenu()
|
||||
// _contextMenu()
|
||||
|
||||
/**
|
||||
* Open a menu
|
||||
* @param {string} menu Menu name
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_openMenu(menu, event) {
|
||||
event.stopPropagation();
|
||||
if (this.props.currentMenu === menu) {
|
||||
menu = null;
|
||||
}
|
||||
|
||||
InterfaceEvents.openMenu(menu);
|
||||
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);
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Slot Drag Handler
|
||||
* @param {object} originSlot Origin slot model
|
||||
* @param {Event} e Drag Event
|
||||
*/
|
||||
_drag(originSlot, e) {
|
||||
e.dataTransfer.setData('text/html', e.currentTarget);
|
||||
e.dataTransfer.effectAllowed = 'all';
|
||||
this.setState({ originSlot });
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Slot Drag Over Handler
|
||||
* @param {object} targetSlot Potential drop target
|
||||
* @param {Event} e Drag Event
|
||||
*/
|
||||
_dragOverSlot(targetSlot, e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
let os = this.state.originSlot;
|
||||
if (os) {
|
||||
console.log('has origin');
|
||||
e.dataTransfer.dropEffect = os != targetSlot && targetSlot.maxClass >= os.m.class ? 'copyMove' : 'none';
|
||||
this.setState({ targetSlot });
|
||||
} else {
|
||||
e.dataTransfer.dropEffect = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag over non-droppable target/element
|
||||
* @param {Event} e Drag Event
|
||||
*/
|
||||
_dragOverNone(e) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'none';
|
||||
this.setState({ targetSlot: null });
|
||||
}
|
||||
|
||||
/**
|
||||
* Slot drop handler. If the target is eligible swap the origin and target modules.
|
||||
* If the target slot's current module cannot be mounted in the origin slot then
|
||||
* the origin slot will be empty.
|
||||
*/
|
||||
_drop() {
|
||||
let { originSlot, targetSlot } = this.state;
|
||||
let m = originSlot.m;
|
||||
|
||||
if (targetSlot && m && targetSlot.maxClass >= m.class) {
|
||||
// Swap modules if possible
|
||||
if (targetSlot.m && originSlot.maxClass >= targetSlot.m.class) {
|
||||
this.props.ship.use(originSlot, targetSlot.m, true);
|
||||
} else { // Otherwise empty the origin slot
|
||||
this.props.ship.use(originSlot, null, true); // Empty but prevent summary update
|
||||
}
|
||||
this.props.ship.use(targetSlot, m); // update target slot
|
||||
this.props.onChange();
|
||||
}
|
||||
this.setState({ originSlot: null, targetSlot: null });
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine drop eligibilty CSS class
|
||||
* @param {Object} slot Current slot
|
||||
* @param {Object} originSlot Origin slot
|
||||
* @param {Object} targetSlot Target slot
|
||||
* @return {string} CSS Class name
|
||||
*/
|
||||
_dropClass(slot, originSlot, targetSlot) {
|
||||
if (!originSlot) {
|
||||
return null;
|
||||
}
|
||||
if (slot === originSlot) {
|
||||
if (targetSlot && targetSlot.m && originSlot.maxClass < targetSlot.m.class) {
|
||||
return 'dropEmpty'; // Origin slot will be emptied
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (originSlot.m && slot.maxClass >= originSlot.m.class) { // Eligble drop slot
|
||||
if (slot === targetSlot) {
|
||||
return 'drop'; // Can drop
|
||||
}
|
||||
return 'eligible'; // Potential drop slot
|
||||
}
|
||||
|
||||
return 'ineligible'; // Cannot be dropped / invalid drop slot
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle slot Active/Inactive
|
||||
* @param {Object} slot Slot
|
||||
*/
|
||||
_togglePwr(slot) {
|
||||
this.props.ship.setSlotEnabled(slot, !slot.enabled);
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close current menu
|
||||
*/
|
||||
_close() {
|
||||
if (this.props.currentMenu) {
|
||||
InterfaceEvents.closeMenu();
|
||||
this.context.closeMenu();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the slot section
|
||||
* @return {React.Component} Slot section
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let sectionMenuOpened = this.props.currentMenu === this.sectionName;
|
||||
@@ -65,8 +187,8 @@ export default class SlotSection extends TranslatedComponent {
|
||||
let ctx = wrapCtxMenu(this._contextMenu);
|
||||
|
||||
return (
|
||||
<div id={this.sectionId} className={'group'}>
|
||||
<div className={cn('section-menu', {selected: sectionMenuOpened})} onClick={open} onContextMenu={ctx}>
|
||||
<div id={this.sectionId} className={'group'} onDragLeave={this._dragOverNone}>
|
||||
<div className={cn('section-menu', { selected: sectionMenuOpened })} onClick={open} onContextMenu={ctx}>
|
||||
<h1>{translate(this.sectionName)} <Equalizer/></h1>
|
||||
{sectionMenuOpened ? this._getSectionMenu(translate) : null }
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { jumpRange } from '../shipyard/Calculations';
|
||||
import { diffDetails } from '../utils/SlotFunctions';
|
||||
import AvailableModulesMenu from './AvailableModulesMenu';
|
||||
|
||||
/**
|
||||
* Standard Slot
|
||||
*/
|
||||
export default class StandardSlot extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -10,46 +15,51 @@ export default class StandardSlot extends TranslatedComponent {
|
||||
modules: React.PropTypes.array.isRequired,
|
||||
onSelect: React.PropTypes.func.isRequired,
|
||||
onOpen: React.PropTypes.func.isRequired,
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
selected: React.PropTypes.bool,
|
||||
shipMass: React.PropTypes.number,
|
||||
warning: React.PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the slot
|
||||
* @return {React.Component} Slot component
|
||||
*/
|
||||
render() {
|
||||
let { translate, formats, units } = this.context.language;
|
||||
let { slot, warning } = this.props;
|
||||
let { modules, slot, warning, onSelect, ladenMass, ship } = this.props;
|
||||
let m = slot.m;
|
||||
let classRating = m.class + m.rating;
|
||||
let menu;
|
||||
|
||||
if (this.props.selected) {
|
||||
menu = <AvailableModulesMenu
|
||||
modules={this.props.modules}
|
||||
shipMass={this.props.shipMass}
|
||||
modules={modules}
|
||||
shipMass={ship.ladenMass}
|
||||
m={m}
|
||||
onSelect={this.props.onSelect}
|
||||
warning={this.props.warning}
|
||||
onSelect={onSelect}
|
||||
warning={warning}
|
||||
diffDetails={diffDetails.bind(ship, this.context.language)}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('slot', {selected: this.props.selected})} onClick={this.props.onOpen}>
|
||||
<div className={cn('details', {warning: warning && warning(slot.m)})}>
|
||||
<div className={cn('slot', { selected: this.props.selected })} onClick={this.props.onOpen}>
|
||||
<div className={cn('details-container', { warning: warning && warning(slot.m) })}>
|
||||
<div className={'sz'}>{slot.maxClass}</div>
|
||||
<div>
|
||||
<div className={'l'}>{classRating + ' ' + translate(m.grp)}</div>
|
||||
<div className={'r'}>{m.mass || m.capacity}{units.T}</div>
|
||||
<div className='l'>{classRating + ' ' + translate(m.grp)}</div>
|
||||
<div className={'r'}>{m.mass || m.fuel}{units.T}</div>
|
||||
<div className={'cb'}>
|
||||
{ m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{units.T}</div> : null }
|
||||
{ m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{units.T}</div> : null }
|
||||
{ m.range ? <div className={'l'}>{translate('range')}: {m.range}{units.km}</div> : null }
|
||||
{ m.time ? <div className={'l'}>{translate('time')}: {formats.time(m.time)}</div> : null }
|
||||
{ m.eff ? <div className={'l'}>{translate('efficiency')}: {m.eff}</div> : null }
|
||||
{ m.pGen ? <div className={'l'}>{translate('power')}: {m.pGen}{units.MW}</div> : null }
|
||||
{ m.maxfuel ? <div className={'l'}>{translate('max') + ' ' + translate('fuel') + ': '}{m.maxfuel}{units.T}</div> : null }
|
||||
{ m.weaponcapacity ? <div className={'l'}>{translate('WEP')}: {m.weaponcapacity}{units.MJ} / {m.weaponrecharge}{units.MW}</div> : null }
|
||||
{ m.systemcapacity ? <div className={'l'}>{translate('SYS')}: {m.systemcapacity}{units.MJ} / {m.systemrecharge}{units.MW}</div> : null }
|
||||
{ m.enginecapacity ? <div className={'l'}>{translate('ENG')}: {m.enginecapacity}{units.MJ} / {m.enginerecharge}{units.MW}</div> : null }
|
||||
{ m.optmass ? <div className='l'>{translate('optimal mass') + ': '}{m.optmass}{units.T}</div> : null }
|
||||
{ m.maxmass ? <div className='l'>{translate('max mass') + ': '}{m.maxmass}{units.T}</div> : null }
|
||||
{ m.range ? <div className='l'>{translate('range')}: {m.range}{units.km}</div> : null }
|
||||
{ m.time ? <div className='l'>{translate('time')}: {formats.time(m.time)}</div> : null }
|
||||
{ m.eff ? <div className='l'>{translate('efficiency')}: {m.eff}</div> : null }
|
||||
{ m.pGen ? <div className='l'>{translate('power')}: {m.pGen}{units.MW}</div> : null }
|
||||
{ m.maxfuel ? <div className='l'>{translate('max') + ' ' + translate('fuel') + ': '}{m.maxfuel}{units.T}</div> : null }
|
||||
{ m.weaponcapacity ? <div className='l'>{translate('WEP')}: {m.weaponcapacity}{units.MJ} / {m.weaponrecharge}{units.MW}</div> : null }
|
||||
{ m.systemcapacity ? <div className='l'>{translate('SYS')}: {m.systemcapacity}{units.MJ} / {m.systemrecharge}{units.MW}</div> : null }
|
||||
{ m.enginecapacity ? <div className='l'>{translate('ENG')}: {m.enginecapacity}{units.MJ} / {m.enginerecharge}{units.MW}</div> : null }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,41 +2,61 @@ import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import SlotSection from './SlotSection';
|
||||
import StandardSlot from './StandardSlot';
|
||||
import { diffDetails } from '../utils/SlotFunctions';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
|
||||
/**
|
||||
* Standard Slot section
|
||||
*/
|
||||
export default class StandardSlotSection extends SlotSection {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context, 'standard', 'standard');
|
||||
|
||||
this._optimizeStandard = this._optimizeStandard.bind(this);
|
||||
this._optimizeCargo = this._optimizeCargo.bind(this);
|
||||
this._optimizeExplorer = this._optimizeExplorer.bind(this);
|
||||
this._hideDiff = this._hideDiff.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill all standard slots with the specificed rating (using max class)
|
||||
* @param {String} rating [A-E]
|
||||
*/
|
||||
_fill(rating) {
|
||||
this.props.ship.useStandard(rating);
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the lightest/optimal available standard modules
|
||||
*/
|
||||
_optimizeStandard() {
|
||||
this.props.ship.useLightestStandard();
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trader build
|
||||
*/
|
||||
_optimizeCargo() {
|
||||
let ship = this.props.ship;
|
||||
ship.internal.forEach((slot) => {
|
||||
var id = ModuleUtils.findInternalId('cr', slot.maxClass, 'E');
|
||||
ship.use(slot, ModuleUtils.internal(id));
|
||||
});
|
||||
ship.internal.forEach((slot) => ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E')));
|
||||
ship.useLightestStandard();
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Explorer build
|
||||
*/
|
||||
_optimizeExplorer() {
|
||||
let ship = this.props.ship,
|
||||
intLength = ship.internal.length,
|
||||
@@ -68,7 +88,6 @@ export default class StandardSlotSection extends SlotSection {
|
||||
let am = ModuleUtils.findInternal('am', slot.maxClass, afmUnitCount ? 'B' : 'A');
|
||||
ship.use(slot, am);
|
||||
ship.setSlotEnabled(slot, false); // Disabled power for AFM Unit
|
||||
|
||||
} else {
|
||||
ship.use(slot, null);
|
||||
}
|
||||
@@ -98,46 +117,89 @@ export default class StandardSlotSection extends SlotSection {
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the specified bulkhead
|
||||
* @param {number} bulkheadIndex 0 - 4
|
||||
*/
|
||||
_selectBulkhead(bulkheadIndex) {
|
||||
this.props.ship.useBulkhead(bulkheadIndex);
|
||||
this.context.tooltip();
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* On right click optimize the standard modules
|
||||
*/
|
||||
_contextMenu() {
|
||||
this._optimizeStandard();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the bulkhead diff tooltip
|
||||
* @param {number} bhIndex Potential Bulkhead alternative
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_bhDiff(bhIndex, event) {
|
||||
let ship = this.props.ship;
|
||||
this.context.tooltip(
|
||||
diffDetails.call(ship, this.context.language, ModuleUtils.bulkheads(ship.id, bhIndex), ship.bulkheads.m),
|
||||
event.currentTarget.getBoundingClientRect()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the diff tooltip
|
||||
*/
|
||||
_hideDiff() {
|
||||
this.context.tooltip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the slot React Components
|
||||
* @return {Array} Array of Slots
|
||||
*/
|
||||
_getSlots() {
|
||||
let { translate, units } = this.context.language;
|
||||
let { ship, currentMenu } = this.props;
|
||||
let slots = new Array(8);
|
||||
let open = this._openMenu;
|
||||
let select = this._selectModule;
|
||||
let selBulkhead = this._selectBulkhead;
|
||||
let ship = this.props.ship
|
||||
let st = ship.standard;
|
||||
let avail = ship.getAvailableModules().standard;
|
||||
let bulkheads = ship.bulkheads;
|
||||
let currentMenu = this.props.currentMenu;
|
||||
let bh = ship.bulkheads;
|
||||
|
||||
slots[0] = (
|
||||
<div key='bh' className={cn('slot', {selected: currentMenu === bulkheads})} onClick={open.bind(this, bulkheads)}>
|
||||
<div className={'details'}>
|
||||
<div className={'sz'}>8</div>
|
||||
<div>
|
||||
<div className={'l'}>{translate('bh')}</div>
|
||||
<div className={'r'}>{bulkheads.m.mass}{units.T}</div>
|
||||
<div className={'cl l'}>{translate(bulkheads.m.name)}</div>
|
||||
<div key='bh' className={cn('slot', { selected: currentMenu === bh })} onClick={open.bind(this, bh)}>
|
||||
<div className={'details-container'}>
|
||||
<div className={'details'}>
|
||||
<div className={'sz'}>8</div>
|
||||
<div>
|
||||
<div className={'l'}>{translate('bh')}</div>
|
||||
<div className={'r'}>{bh.m.mass}{units.T}</div>
|
||||
<div className={'cl l'}>{translate(bh.m.name)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{currentMenu === bulkheads &&
|
||||
{currentMenu === bh &&
|
||||
<div className='select' onClick={ e => e.stopPropagation() }>
|
||||
<ul>
|
||||
<li onClick={selBulkhead.bind(this, 0)} className={cn('lc', { active: bulkheads.id == '0' })}>{translate('Lightweight Alloy')}</li>
|
||||
<li onClick={selBulkhead.bind(this, 1)} className={cn('lc', { active: bulkheads.id == '1' })}>{translate('Reinforced Alloy')}</li>
|
||||
<li onClick={selBulkhead.bind(this, 2)} className={cn('lc', { active: bulkheads.id == '2' })}>{translate('Military Grade Composite')}</li>
|
||||
<li onClick={selBulkhead.bind(this, 3)} className={cn('lc', { active: bulkheads.id == '3' })}>{translate('Mirrored Surface Composite')}</li>
|
||||
<li onClick={selBulkhead.bind(this, 4)} className={cn('lc', { active: bulkheads.id == '4' })}>{translate('Reactive Surface Composite')}</li>
|
||||
<li onClick={selBulkhead.bind(this, 0)} onMouseOver={this._bhDiff.bind(this, 0)} onMouseLeave={this._hideDiff} className={cn('lc', { active: bh.index == 0 })}>
|
||||
{translate('Lightweight Alloy')}
|
||||
</li>
|
||||
<li onClick={selBulkhead.bind(this, 1)} onMouseOver={this._bhDiff.bind(this, 1)} onMouseLeave={this._hideDiff} className={cn('lc', { active: bh.index == 1 })}>
|
||||
{translate('Reinforced Alloy')}
|
||||
</li>
|
||||
<li onClick={selBulkhead.bind(this, 2)} onMouseOver={this._bhDiff.bind(this, 2)} onMouseLeave={this._hideDiff} className={cn('lc', { active: bh.index == 2 })}>
|
||||
{translate('Military Grade Composite')}
|
||||
</li>
|
||||
<li onClick={selBulkhead.bind(this, 3)} onMouseOver={this._bhDiff.bind(this, 3)} onMouseLeave={this._hideDiff} className={cn('lc', { active: bh.index == 3 })}>
|
||||
{translate('Mirrored Surface Composite')}
|
||||
</li>
|
||||
<li onClick={selBulkhead.bind(this, 4)} onMouseOver={this._bhDiff.bind(this, 4)} onMouseLeave={this._hideDiff} className={cn('lc', { active: bh.index == 4 })}>
|
||||
{translate('Reactive Surface Composite')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
@@ -151,6 +213,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
onOpen={open.bind(this, st[0])}
|
||||
onSelect={select.bind(this, st[0])}
|
||||
selected={currentMenu == st[0]}
|
||||
ship={ship}
|
||||
warning={m => m.pGen < ship.powerRetracted}
|
||||
/>;
|
||||
|
||||
@@ -161,6 +224,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
onOpen={open.bind(this, st[1])}
|
||||
onSelect={select.bind(this, st[1])}
|
||||
selected={currentMenu == st[1]}
|
||||
ship={ship}
|
||||
warning={m => m.maxmass < ship.ladenMass}
|
||||
/>;
|
||||
|
||||
@@ -171,6 +235,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
modules={avail[2]}
|
||||
onOpen={open.bind(this, st[2])}
|
||||
onSelect={select.bind(this, st[2])}
|
||||
ship={ship}
|
||||
selected={currentMenu == st[2]}
|
||||
/>;
|
||||
|
||||
@@ -180,6 +245,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
modules={avail[3]}
|
||||
onOpen={open.bind(this, st[3])}
|
||||
onSelect={select.bind(this, st[3])}
|
||||
ship={ship}
|
||||
selected={currentMenu == st[3]}
|
||||
/>;
|
||||
|
||||
@@ -190,6 +256,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
onOpen={open.bind(this, st[4])}
|
||||
onSelect={select.bind(this, st[4])}
|
||||
selected={currentMenu == st[4]}
|
||||
ship={ship}
|
||||
warning= {m => m.enginecapacity < ship.boostEnergy}
|
||||
/>;
|
||||
|
||||
@@ -200,6 +267,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
onOpen={open.bind(this, st[5])}
|
||||
onSelect={select.bind(this, st[5])}
|
||||
selected={currentMenu == st[5]}
|
||||
ship={ship}
|
||||
warning= {m => m.enginecapacity < ship.boostEnergy}
|
||||
/>;
|
||||
|
||||
@@ -210,12 +278,18 @@ export default class StandardSlotSection extends SlotSection {
|
||||
onOpen={open.bind(this, st[6])}
|
||||
onSelect={select.bind(this, st[6])}
|
||||
selected={currentMenu == st[6]}
|
||||
warning= {m => m.capacity < st[2].m.maxfuel} // Show warning when fuel tank is smaller than FSD Max Fuel
|
||||
ship={ship}
|
||||
warning= {m => m.fuel < st[2].m.maxfuel} // Show warning when fuel tank is smaller than FSD Max Fuel
|
||||
/>;
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the section drop-down menu
|
||||
* @param {Function} translate Translate function
|
||||
* @return {React.Component} Section menu
|
||||
*/
|
||||
_getSectionMenu(translate) {
|
||||
let _fill = this._fill;
|
||||
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import shallowEqual from '../utils/shallowEqual';
|
||||
import { shallowEqual } from '../utils/UtilityFunctions';
|
||||
|
||||
/**
|
||||
* Base SVG Icon Component
|
||||
*/
|
||||
class SvgIcon extends React.Component {
|
||||
|
||||
/**
|
||||
* Only rerender an SVG Icon if properties have changed
|
||||
* @param {Object} nextProps Next/Incoming properties
|
||||
* @return {Boolean} True if properties have changed
|
||||
*/
|
||||
shouldComponentUpdate(nextProps) { return !shallowEqual(this.props, nextProps); }
|
||||
|
||||
svg() { return null; }
|
||||
|
||||
/**
|
||||
* Standard SVG view box, can/should be overriden by sub-classes as necessary
|
||||
* @return {string} view box string
|
||||
*/
|
||||
viewBox() { return '0 0 32 32'; }
|
||||
|
||||
/**
|
||||
* Render the Icon
|
||||
* @return {React.Component} SVG Icon
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<svg className={cn('icon', this.props.className)} style={this.props.style} viewBox={this.viewBox()}>
|
||||
@@ -19,7 +33,14 @@ class SvgIcon extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bin Icon - Delete
|
||||
*/
|
||||
export class Bin extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M4 10v20c0 1.1 0.9 2 2 2h18c1.1 0 2-0.9 2-2v-20h-22zM10 28h-2v-14h2v14zM14 28h-2v-14h2v14zM18 28h-2v-14h2v14zM22 28h-2v-14h2v14z'/>
|
||||
@@ -28,7 +49,14 @@ export class Bin extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coriolis Logo
|
||||
*/
|
||||
export class CoriolisLogo extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g transform='translate(1,1)'>
|
||||
<path stroke='#ff3b00' transform='rotate(45 15 15)' d='m4,4 l 11,-4 l 11,4 l 4,11 l -4,11 l -11,4 l -11,-4 l -4,-11 l 4,-11 l 22,0 l 0,22 l -22,0 z' strokeWidth='1' fill='#000000'/>
|
||||
@@ -37,13 +65,27 @@ export class CoriolisLogo extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download / To Inbox
|
||||
*/
|
||||
export class Download extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M16 18l8-8h-6v-8h-4v8h-6zM23.273 14.727l-2.242 2.242 8.128 3.031-13.158 4.907-13.158-4.907 8.127-3.031-2.242-2.242-8.727 3.273v8l16 6 16-6v-8z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Eddb.io Logo
|
||||
*/
|
||||
export class Eddb extends SvgIcon {
|
||||
/**
|
||||
* Render the Icon
|
||||
* @return {React.Component} SVG Icon
|
||||
*/
|
||||
render() {
|
||||
return <svg className={cn(this.props.className)} style={this.props.style} viewBox='0 0 90 32'>
|
||||
<path d='M19.1,25.2c0.3,0,0.6,0.1,0.7,0.2c0.2,0.1,0.3,0.3,0.4,0.4c0.1,0.2,0.2,0.4,0.2,0.6v3.3c0,0.3-0.1,0.6-0.2,0.7c-0.1,0.2-0.3,0.3-0.4,0.3c-0.2,0.1-0.4,0.2-0.6,0.1H3.6c-0.3,0-0.6-0.1-0.7-0.2c-0.2-0.1-0.3-0.3-0.3-0.4c-0.1-0.2-0.2-0.4-0.1-0.6V10.2c0-0.3,0.1-0.5,0.2-0.7C2.7,9.4,2.9,9.3,3,9.2C3.2,9.1,3.4,9,3.6,9h15.5c0.3,0,0.6,0.1,0.7,0.2c0.2,0.1,0.3,0.3,0.4,0.4c0.1,0.2,0.2,0.4,0.2,0.6V22c0,0.3-0.1,0.6-0.2,0.7c-0.1,0.2-0.3,0.3-0.4,0.3c-0.2,0.1-0.4,0.2-0.6,0.1h-6.8v-6.8c0.3-0.2,0.6-0.4,0.8-0.7c0.2-0.3,0.3-0.7,0.3-1c0-0.6-0.2-1.1-0.6-1.4c-0.4-0.4-0.9-0.6-1.4-0.6c-0.5,0-1,0.2-1.4,0.6c-0.4,0.4-0.6,0.9-0.6,1.4c0,0.8,0.3,1.4,1,1.8v8.7H19.1z'/>
|
||||
@@ -54,7 +96,14 @@ export class Eddb extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed - <>
|
||||
*/
|
||||
export class Embed extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M18 23l3 3 10-10-10-10-3 3 7 7z'/>
|
||||
@@ -63,8 +112,19 @@ export class Embed extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Equalizer
|
||||
*/
|
||||
export class Equalizer extends SvgIcon {
|
||||
viewBox () { return '0 0 1024 1024'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 1024 1024'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M448 128v-16c0-26.4-21.6-48-48-48h-160c-26.4 0-48 21.6-48 48v16h-192v128h192v16c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-16h576v-128h-576zM256 256v-128h128v128h-128zM832 432c0-26.4-21.6-48-48-48h-160c-26.4 0-48 21.6-48 48v16h-576v128h576v16c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-16h192v-128h-192v-16zM640 576v-128h128v128h-128zM448 752c0-26.4-21.6-48-48-48h-160c-26.4 0-48 21.6-48 48v16h-192v128h192v16c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-16h576v-128h-576v-16zM256 896v-128h128v128h-128z'/>
|
||||
@@ -72,32 +132,71 @@ export class Equalizer extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Floppy disk - save
|
||||
*/
|
||||
export class FloppyDisk extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M28 0h-28v32h32v-28l-4-4zM16 4h4v8h-4v-8zM28 28h-24v-24h2v10h18v-10h2.343l1.657 1.657v22.343z' />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuel Gauge
|
||||
*/
|
||||
export class Fuel extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M16 0c-8.837 0-16 7.163-16 16s7.163 16 16 16 16-7.163 16-16-7.163-16-16-16zM9.464 26.067c0.347-0.957 0.536-1.99 0.536-3.067 0-3.886-2.463-7.197-5.913-8.456 0.319-2.654 1.508-5.109 3.427-7.029 2.267-2.266 5.28-3.515 8.485-3.515s6.219 1.248 8.485 3.515c1.92 1.92 3.108 4.375 3.428 7.029-3.45 1.26-5.913 4.57-5.913 8.456 0 1.077 0.189 2.11 0.536 3.067-1.928 1.258-4.18 1.933-6.536 1.933s-4.608-0.675-6.536-1.933zM17.242 20.031c0.434 0.109 0.758 0.503 0.758 0.969v2c0 0.55-0.45 1-1 1h-2c-0.55 0-1-0.45-1-1v-2c0-0.466 0.324-0.86 0.758-0.969l0.742-14.031h1l0.742 14.031z' />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Github Logo
|
||||
*/
|
||||
export class GitHub extends SvgIcon {
|
||||
viewBox() { return '0 0 1024 1024' };
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 1024 1024'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M512 0C229.252 0 0 229.25199999999995 0 512c0 226.251 146.688 418.126 350.155 485.813 25.593 4.686 34.937-11.125 34.937-24.626 0-12.188-0.469-52.562-0.718-95.314-128.708 23.46-161.707-31.541-172.469-60.373-5.525-14.809-30.407-60.249-52.398-72.263-17.988-9.828-43.26-33.237-0.917-33.735 40.434-0.476 69.348 37.308 78.471 52.75 45.938 77.749 119.876 55.627 148.999 42.5 4.654-32.999 17.902-55.627 32.501-68.373-113.657-12.939-233.22-56.875-233.22-253.063 0-55.94 19.968-101.561 52.658-137.404-5.22-12.999-22.844-65.095 5.063-135.563 0 0 42.937-13.749 140.811 52.501 40.811-11.406 84.594-17.031 128.124-17.22 43.499 0.188 87.314 5.874 128.188 17.28 97.689-66.311 140.686-52.501 140.686-52.501 28 70.532 10.375 122.564 5.124 135.499 32.811 35.844 52.626 81.468 52.626 137.404 0 196.686-119.751 240-233.813 252.686 18.439 15.876 34.748 47.001 34.748 94.748 0 68.437-0.686 123.627-0.686 140.501 0 13.625 9.312 29.561 35.25 24.562C877.436 929.998 1024 738.126 1024 512 1024 229.25199999999995 794.748 0 512 0z' />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Infinite / Infinity
|
||||
*/
|
||||
export class Infinite extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M24.5 23.5c-2.003 0-3.887-0.78-5.303-2.197l-3.197-3.196-3.196 3.196c-1.417 1.417-3.3 2.197-5.303 2.197s-3.887-0.78-5.304-2.197c-1.417-1.417-2.197-3.3-2.197-5.303s0.78-3.887 2.197-5.304c1.417-1.417 3.3-2.197 5.304-2.197s3.887 0.78 5.303 2.197l3.196 3.196 3.196-3.196c1.417-1.417 3.3-2.197 5.303-2.197s3.887 0.78 5.303 2.197c1.417 1.417 2.197 3.3 2.197 5.304s-0.78 3.887-2.197 5.303c-1.416 1.417-3.3 2.197-5.303 2.197zM21.304 19.197c0.854 0.853 1.989 1.324 3.196 1.323s2.342-0.47 3.196-1.324c0.854-0.854 1.324-1.989 1.324-3.196s-0.47-2.342-1.324-3.196c-0.854-0.854-1.989-1.324-3.196-1.324s-2.342 0.47-3.196 1.324l-3.196 3.196 3.196 3.197zM7.5 11.48c-1.207 0-2.342 0.47-3.196 1.324s-1.324 1.989-1.324 3.196c0 1.207 0.47 2.342 1.324 3.196s1.989 1.324 3.196 1.324c1.207 0 2.342-0.47 3.196-1.324l3.196-3.196-3.196-3.196c-0.854-0.854-1.989-1.324-3.196-1.324v0z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Info - i within circle
|
||||
*/
|
||||
export class Info extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M14 9.5c0-0.825 0.675-1.5 1.5-1.5h1c0.825 0 1.5 0.675 1.5 1.5v1c0 0.825-0.675 1.5-1.5 1.5h-1c-0.825 0-1.5-0.675-1.5-1.5v-1z'/>
|
||||
@@ -107,7 +206,14 @@ export class Info extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link / Permalink / Chain
|
||||
*/
|
||||
export class LinkIcon extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M13.757 19.868c-0.416 0-0.832-0.159-1.149-0.476-2.973-2.973-2.973-7.81 0-10.783l6-6c1.44-1.44 3.355-2.233 5.392-2.233s3.951 0.793 5.392 2.233c2.973 2.973 2.973 7.81 0 10.783l-2.743 2.743c-0.635 0.635-1.663 0.635-2.298 0s-0.635-1.663 0-2.298l2.743-2.743c1.706-1.706 1.706-4.481 0-6.187-0.826-0.826-1.925-1.281-3.094-1.281s-2.267 0.455-3.094 1.281l-6 6c-1.706 1.706-1.706 4.481 0 6.187 0.635 0.635 0.635 1.663 0 2.298-0.317 0.317-0.733 0.476-1.149 0.476z'/>
|
||||
@@ -116,39 +222,89 @@ export class LinkIcon extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* No Power - Lightning bolt + no entry
|
||||
*/
|
||||
export class NoPower extends SvgIcon {
|
||||
viewBox() { return '0 0 512 512' };
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 512 512'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M437.020 74.98c-48.353-48.351-112.64-74.98-181.020-74.98s-132.667 26.629-181.020 74.98c-48.351 48.353-74.98 112.64-74.98 181.020s26.629 132.667 74.98 181.020c48.353 48.351 112.64 74.98 181.020 74.98s132.667-26.629 181.020-74.98c48.351-48.353 74.98-112.64 74.98-181.020s-26.629-132.667-74.98-181.020zM448 256c0 41.407-13.177 79.794-35.556 111.19l-267.633-267.634c31.396-22.379 69.782-35.556 111.189-35.556 105.869 0 192 86.131 192 192zM64 256c0-41.407 13.177-79.793 35.556-111.189l267.635 267.634c-31.397 22.378-69.784 35.555-111.191 35.555-105.869 0-192-86.131-192-192z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification - Exclamation mark within circle
|
||||
*/
|
||||
export class Notification extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M16 3c-3.472 0-6.737 1.352-9.192 3.808s-3.808 5.72-3.808 9.192c0 3.472 1.352 6.737 3.808 9.192s5.72 3.808 9.192 3.808c3.472 0 6.737-1.352 9.192-3.808s3.808-5.72 3.808-9.192c0-3.472-1.352-6.737-3.808-9.192s-5.72-3.808-9.192-3.808zM16 0v0c8.837 0 16 7.163 16 16s-7.163 16-16 16c-8.837 0-16-7.163-16-16s7.163-16 16-16zM14 22h4v4h-4zM14 6h4v12h-4z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Power - Lightning Bolt
|
||||
*/
|
||||
export class Power extends SvgIcon {
|
||||
viewBox() { return '0 0 512 512' };
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 512 512'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M192 0l-192 256h192l-128 256 448-320h-256l192-192z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Question mark within Circle
|
||||
*/
|
||||
export class Question extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M14 22h4v4h-4zM22 8c1.105 0 2 0.895 2 2v6l-6 4h-4v-2l6-4v-2h-10v-4h12zM16 3c-3.472 0-6.737 1.352-9.192 3.808s-3.808 5.72-3.808 9.192c0 3.472 1.352 6.737 3.808 9.192s5.72 3.808 9.192 3.808c3.472 0 6.737-1.352 9.192-3.808s3.808-5.72 3.808-9.192c0-3.472-1.352-6.737-3.808-9.192s-5.72-3.808-9.192-3.808zM16 0v0c8.837 0 16 7.163 16 16s-7.163 16-16 16c-8.837 0-16-7.163-16-16s7.163-16 16-16z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload - Clockwise circular arrow
|
||||
*/
|
||||
export class Reload extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M32 12h-12l4.485-4.485c-2.267-2.266-5.28-3.515-8.485-3.515s-6.219 1.248-8.485 3.515c-2.266 2.267-3.515 5.28-3.515 8.485s1.248 6.219 3.515 8.485c2.267 2.266 5.28 3.515 8.485 3.515s6.219-1.248 8.485-3.515c0.189-0.189 0.371-0.384 0.546-0.583l3.010 2.634c-2.933 3.349-7.239 5.464-12.041 5.464-8.837 0-16-7.163-16-16s7.163-16 16-16c4.418 0 8.418 1.791 11.313 4.687l4.687-4.687v12z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning - Exclamation point witin triangle
|
||||
*/
|
||||
export class Warning extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M16 2.899l13.409 26.726h-26.819l13.409-26.726zM16 0c-0.69 0-1.379 0.465-1.903 1.395l-13.659 27.222c-1.046 1.86-0.156 3.383 1.978 3.383h27.166c2.134 0 3.025-1.522 1.978-3.383h0l-13.659-27.222c-0.523-0.93-1.213-1.395-1.903-1.395v0z'/>
|
||||
@@ -158,8 +314,19 @@ export class Warning extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed mount hardpoint
|
||||
*/
|
||||
export class MountFixed extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<circle fillOpacity='0' r='70' cy='100' cx='100' strokeWidth='5' />
|
||||
@@ -171,8 +338,19 @@ export class MountFixed extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gimballed mount hardpoint
|
||||
*/
|
||||
export class MountGimballed extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<ellipse ry='25' rx='95' cy='100' cx='100' fillOpacity='0' strokeWidth='5' />
|
||||
@@ -181,8 +359,19 @@ export class MountGimballed extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turrent mount hardpoint
|
||||
*/
|
||||
export class MountTurret extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<line y2='170' x2='162' y1='170' x1='8' strokeWidth='6' />
|
||||
@@ -192,39 +381,84 @@ export class MountTurret extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rocket ship
|
||||
*/
|
||||
export class Rocket extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M22 2l-10 10h-6l-6 8c0 0 6.357-1.77 10.065-0.94l-10.065 12.94 13.184-10.255c1.839 4.208-1.184 10.255-1.184 10.255l8-6v-6l10-10 2-10-10 2z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hammer
|
||||
*/
|
||||
export class Hammer extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M31.562 25.905l-9.423-9.423c-0.583-0.583-1.538-0.583-2.121 0l-0.707 0.707-5.75-5.75 9.439-9.439h-10l-4.439 4.439-0.439-0.439h-2.121v2.121l0.439 0.439-6.439 6.439 5 5 6.439-6.439 5.75 5.75-0.707 0.707c-0.583 0.583-0.583 1.538 0 2.121l9.423 9.423c0.583 0.583 1.538 0.583 2.121 0l3.535-3.535c0.583-0.583 0.583-1.538 0-2.121z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stats bars / Histogram / Compare
|
||||
*/
|
||||
export class StatsBars extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M0 26h32v4h-32zM4 18h4v6h-4zM10 10h4v14h-4zM16 16h4v8h-4zM22 4h4v20h-4z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cogs / Settings
|
||||
*/
|
||||
export class Cogs extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M11.366 22.564l1.291-1.807-1.414-1.414-1.807 1.291c-0.335-0.187-0.694-0.337-1.071-0.444l-0.365-2.19h-2l-0.365 2.19c-0.377 0.107-0.736 0.256-1.071 0.444l-1.807-1.291-1.414 1.414 1.291 1.807c-0.187 0.335-0.337 0.694-0.443 1.071l-2.19 0.365v2l2.19 0.365c0.107 0.377 0.256 0.736 0.444 1.071l-1.291 1.807 1.414 1.414 1.807-1.291c0.335 0.187 0.694 0.337 1.071 0.444l0.365 2.19h2l0.365-2.19c0.377-0.107 0.736-0.256 1.071-0.444l1.807 1.291 1.414-1.414-1.291-1.807c0.187-0.335 0.337-0.694 0.444-1.071l2.19-0.365v-2l-2.19-0.365c-0.107-0.377-0.256-0.736-0.444-1.071zM7 27c-1.105 0-2-0.895-2-2s0.895-2 2-2 2 0.895 2 2-0.895 2-2 2zM32 12v-2l-2.106-0.383c-0.039-0.251-0.088-0.499-0.148-0.743l1.799-1.159-0.765-1.848-2.092 0.452c-0.132-0.216-0.273-0.426-0.422-0.629l1.219-1.761-1.414-1.414-1.761 1.219c-0.203-0.149-0.413-0.29-0.629-0.422l0.452-2.092-1.848-0.765-1.159 1.799c-0.244-0.059-0.492-0.109-0.743-0.148l-0.383-2.106h-2l-0.383 2.106c-0.251 0.039-0.499 0.088-0.743 0.148l-1.159-1.799-1.848 0.765 0.452 2.092c-0.216 0.132-0.426 0.273-0.629 0.422l-1.761-1.219-1.414 1.414 1.219 1.761c-0.149 0.203-0.29 0.413-0.422 0.629l-2.092-0.452-0.765 1.848 1.799 1.159c-0.059 0.244-0.109 0.492-0.148 0.743l-2.106 0.383v2l2.106 0.383c0.039 0.251 0.088 0.499 0.148 0.743l-1.799 1.159 0.765 1.848 2.092-0.452c0.132 0.216 0.273 0.426 0.422 0.629l-1.219 1.761 1.414 1.414 1.761-1.219c0.203 0.149 0.413 0.29 0.629 0.422l-0.452 2.092 1.848 0.765 1.159-1.799c0.244 0.059 0.492 0.109 0.743 0.148l0.383 2.106h2l0.383-2.106c0.251-0.039 0.499-0.088 0.743-0.148l1.159 1.799 1.848-0.765-0.452-2.092c0.216-0.132 0.426-0.273 0.629-0.422l1.761 1.219 1.414-1.414-1.219-1.761c0.149-0.203 0.29-0.413 0.422-0.629l2.092 0.452 0.765-1.848-1.799-1.159c0.059-0.244 0.109-0.492 0.148-0.743l2.106-0.383zM21 15.35c-2.402 0-4.35-1.948-4.35-4.35s1.948-4.35 4.35-4.35 4.35 1.948 4.35 4.35c0 2.402-1.948 4.35-4.35 4.35z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Power Switch - Reset
|
||||
*/
|
||||
export class Switch extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M20 4.581v4.249c1.131 0.494 2.172 1.2 3.071 2.099 1.889 1.889 2.929 4.4 2.929 7.071s-1.040 5.182-2.929 7.071c-1.889 1.889-4.4 2.929-7.071 2.929s-5.182-1.040-7.071-2.929c-1.889-1.889-2.929-4.4-2.929-7.071s1.040-5.182 2.929-7.071c0.899-0.899 1.94-1.606 3.071-2.099v-4.249c-5.783 1.721-10 7.077-10 13.419 0 7.732 6.268 14 14 14s14-6.268 14-14c0-6.342-4.217-11.698-10-13.419zM14 0h4v16h-4z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-game Coriolis Station logo
|
||||
*/
|
||||
export class StationCoriolis extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<rect x='73.001' y='94.017' width='53.997' height='11.945'/>
|
||||
@@ -233,8 +467,19 @@ export class StationCoriolis extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-game Ocellus Station logo
|
||||
*/
|
||||
export class StationOcellus extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M100.002,200C155.139,200,200,155.142,200,100.001c0-55.143-44.861-100.002-99.998-100.002C44.86-0.001-0.002,44.857-0.002,100.001C-0.001,155.142,44.86,200,100.002,200z M100.002,5.574c52.063,0,94.423,42.359,94.423,94.427c0,52.067-42.361,94.422-94.423,94.422c-52.07,0-94.428-42.358-94.428-94.422C5.574,47.933,47.933,5.574,100.002,5.574z'/>
|
||||
@@ -244,8 +489,19 @@ export class StationOcellus extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-game Orbis Station logo
|
||||
*/
|
||||
export class StationOrbis extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M100.002,200c55.138,0,99.996-44.861,99.996-100c0-55.141-44.858-100-99.996-100C44.861,0-0.001,44.857-0.001,100C0,155.139,44.861,200,100.002,200z M100.002,194.424c-35.465,0-66.413-19.663-82.552-48.651l44.426-23.388c7.704,13.067,21.888,21.884,38.127,21.884c16.054,0,30.096-8.621,37.853-21.446l44.441,23.389C166.092,174.961,135.282,194.424,100.002,194.424zM100.002,61.306c21.335,0,38.691,17.356,38.691,38.694c0,21.338-17.364,38.691-38.691,38.691c-21.339,0-38.696-17.354-38.696-38.691C61.307,78.662,78.663,61.306,100.002,61.306zM194.422,100c0,14.802-3.427,28.808-9.521,41.287l-44.447-23.4c2.433-5.477,3.812-11.521,3.812-17.89c0-23.578-18.539-42.852-41.8-44.145V5.636C153.392,6.956,194.422,48.762,194.422,100z M96.895,5.655v50.233C73.938,57.491,55.73,76.635,55.73,100c0,6.187,1.286,12.081,3.592,17.434l-44.455,23.402C8.911,128.472,5.571,114.619,5.571,100C5.577,48.972,46.261,7.297,96.895,5.655z'/>
|
||||
@@ -254,8 +510,19 @@ export class StationOrbis extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-game Outpost Station logo
|
||||
*/
|
||||
export class StationOutpost extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M145.137,59.126h4.498v6.995h5.576V46.556h-5.576v6.994h-4.498V16.328h-5.574v57.667h-15.411v14.824h-7.63v-14.58h-13.044v14.58h-8.295v-14.58H82.138v14.58h-6.573v-14.58H59.072v14.58h-6.573v-14.58H39.458v36.338h13.041V94.391h6.573v16.186h16.493V94.391h6.573v16.186h13.044V94.391h8.295v16.186h13.044V94.391h7.63v40.457l17.634,17.637h13.185v31.182h5.577V73.996H145.14v-14.87H145.137z M154.97,146.907h-10.871l-14.376-14.376V79.57h25.247V146.907z'/>
|
||||
@@ -265,14 +532,32 @@ export class StationOutpost extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload - From inbox
|
||||
*/
|
||||
export class Upload extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M14 18h4v-8h6l-8-8-8 8h6zM20 13.5v3.085l9.158 3.415-13.158 4.907-13.158-4.907 9.158-3.415v-3.085l-12 4.5v8l16 6 16-6v-8z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Elite Dangerous Loader / Spinner
|
||||
*/
|
||||
export class Loader extends SvgIcon {
|
||||
viewBox () { return '0 0 40 40'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 40 40'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g className={'loader'}>
|
||||
<path d='m5,8l5,8l5,-8z' className={'l1 d1'} />
|
||||
|
||||
89
src/app/components/Tooltip.jsx
Normal file
89
src/app/components/Tooltip.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { shallowEqual } from '../utils/UtilityFunctions';
|
||||
|
||||
/**
|
||||
* Document Root Tooltip
|
||||
*/
|
||||
export default class Tooltip extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
rect: React.PropTypes.object.isRequired,
|
||||
options: React.PropTypes.object
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
options: {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adjusts the position of the tooltip if its content
|
||||
* appear outside of the windows left or right border
|
||||
* @param {DomElement} elem Tooltip contents container
|
||||
*/
|
||||
_adjustPosition(elem) {
|
||||
if (elem) {
|
||||
let o = this.props.options.orientation || 'n';
|
||||
let rect = elem.getBoundingClientRect();
|
||||
|
||||
if (o == 'n' || o == 's') {
|
||||
let docWidth = document.documentElement.clientWidth;
|
||||
|
||||
if (rect.left < 0) {
|
||||
elem.style.left = rect.width / 2 + 'px';
|
||||
} else if ((rect.left + rect.width) > docWidth) {
|
||||
elem.style.left = docWidth - (rect.width / 2) + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a component should be rerendered
|
||||
* @param {object} nextProps Next properties
|
||||
* @return {boolean} true if update is needed
|
||||
*/
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return !shallowEqual(this.props, nextProps);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders the component
|
||||
* @return {React.Component} Tooltip
|
||||
*/
|
||||
render() {
|
||||
if (!this.props.children) { // If no content is provided
|
||||
return null;
|
||||
}
|
||||
|
||||
let { children, options, rect } = this.props;
|
||||
let o = options.orientation || 'n';
|
||||
let style = options.style || {};
|
||||
|
||||
switch (o) {
|
||||
case 's':
|
||||
style.top = rect.top + rect.height;
|
||||
style.left = rect.left + (rect.width / 2);
|
||||
break;
|
||||
case 'n':
|
||||
style.top = rect.top;
|
||||
style.left = rect.left + (rect.width / 2);
|
||||
break;
|
||||
case 'e':
|
||||
style.left = rect.left + rect.width;
|
||||
style.top = rect.top + (rect.height / 2);
|
||||
break;
|
||||
case 'w':
|
||||
style.left = rect.left;
|
||||
style.top = rect.top + (rect.height / 2);
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div className={ 'arr ' + o} style={style} />
|
||||
<div className={ 'tip ' + o} style={style} ref={this._adjustPosition.bind(this)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +1,22 @@
|
||||
import React from 'react';
|
||||
import shallowEqual from '../utils/shallowEqual';
|
||||
import { shallowEqual } from '../utils/UtilityFunctions';
|
||||
|
||||
/**
|
||||
* @class Abstract TranslatedComponent
|
||||
* Abstract Translated Component
|
||||
*/
|
||||
export default class TranslatedComponent extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
language: React.PropTypes.object.isRequired,
|
||||
sizeRatio: React.PropTypes.number.isRequired
|
||||
}
|
||||
sizeRatio: React.PropTypes.number.isRequired,
|
||||
openMenu: React.PropTypes.func.isRequired,
|
||||
closeMenu: React.PropTypes.func.isRequired,
|
||||
showModal: React.PropTypes.func.isRequired,
|
||||
hideModal: React.PropTypes.func.isRequired,
|
||||
tooltip: React.PropTypes.func.isRequired,
|
||||
termtip: React.PropTypes.func.isRequired,
|
||||
onWindowResize: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Created an instance of a Translated Component. This is an abstract class.
|
||||
@@ -23,9 +30,9 @@ export default class TranslatedComponent extends React.Component {
|
||||
/**
|
||||
* Determine if the context change incldues a language or size change
|
||||
* @param {object} nextContext The incoming / next context
|
||||
* @return {boolean} true if the language has changed
|
||||
* @return {boolean} true if the language has changed
|
||||
*/
|
||||
didContextChange(nextContext){
|
||||
didContextChange(nextContext) {
|
||||
return nextContext.language !== this.context.language || nextContext.sizeRatio != this.context.sizeRatio;
|
||||
}
|
||||
|
||||
@@ -34,14 +41,12 @@ export default class TranslatedComponent extends React.Component {
|
||||
* props, state, or context changes. This method performs a shallow comparison to
|
||||
* determine change.
|
||||
*
|
||||
* @param {object} nextProps
|
||||
* @param {objec} nextState
|
||||
* @param {objec} nextContext
|
||||
* @param {object} nextProps Next/Incoming Properties
|
||||
* @param {objec} nextState Next/Incoming State
|
||||
* @param {objec} nextContext Next/Incoming Context
|
||||
* @return {boolean} True if props, state, or context has changed
|
||||
*/
|
||||
shouldComponentUpdate(nextProps, nextState, nextContext) {
|
||||
return !shallowEqual(this.props, nextProps)
|
||||
|| !shallowEqual(this.state, nextState)
|
||||
|| this.didContextChange(nextContext);
|
||||
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState) || this.didContextChange(nextContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,35 +3,60 @@ import SlotSection from './SlotSection';
|
||||
import HardpointSlot from './HardpointSlot';
|
||||
import cn from 'classnames';
|
||||
|
||||
/**
|
||||
* Utility Slot Section
|
||||
*/
|
||||
export default class UtilitySlotSection extends SlotSection {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context, 'utility', 'utility mounts');
|
||||
|
||||
this._empty = this._empty.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all utility slots and close the menu
|
||||
*/
|
||||
_empty() {
|
||||
this.props.ship.emptyUtility();
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount module in utility slot, replace all if Alt is held
|
||||
* @param {string} group Module Group name
|
||||
* @param {string} rating Module Rating
|
||||
* @param {string} name Module name
|
||||
* @param {Synthetic} event Event
|
||||
*/
|
||||
_use(group, rating, name, event) {
|
||||
this.props.ship.useUtility(group, rating, name, event.getModifierState('Alt'));
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all utility slots on right-click
|
||||
*/
|
||||
_contextMenu() {
|
||||
this._empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all HardpointSlots (React component) for the slots
|
||||
* @return {Array} Array of HardpointSlots
|
||||
*/
|
||||
_getSlots() {
|
||||
let slots = [];
|
||||
let hardpoints = this.props.ship.hardpoints;
|
||||
let availableModules = this.props.ship.getAvailableModules();
|
||||
let currentMenu = this.props.currentMenu;
|
||||
let { ship, currentMenu } = this.props;
|
||||
let hardpoints = ship.hardpoints;
|
||||
let { originSlot, targetSlot } = this.state;
|
||||
let availableModules = ship.getAvailableModules();
|
||||
|
||||
for (let i = 0, l = hardpoints.length; i < l; i++) {
|
||||
let h = hardpoints[i];
|
||||
@@ -43,7 +68,12 @@ export default class UtilitySlotSection extends SlotSection {
|
||||
onOpen={this._openMenu.bind(this,h)}
|
||||
onSelect={this._selectModule.bind(this, h)}
|
||||
selected={currentMenu == h}
|
||||
drag={this._drag.bind(this, h)}
|
||||
dragOver={this._dragOverSlot.bind(this, h)}
|
||||
drop={this._drop}
|
||||
dropClass={this._dropClass(h, originSlot, targetSlot)}
|
||||
enabled={h.enabled}
|
||||
ship={ship}
|
||||
m={h.m}
|
||||
/>);
|
||||
}
|
||||
@@ -52,6 +82,11 @@ export default class UtilitySlotSection extends SlotSection {
|
||||
return slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the section menu
|
||||
* @param {Function} translate Translate function
|
||||
* @return {React.Component} Section menu
|
||||
*/
|
||||
_getSectionMenu(translate) {
|
||||
let _use = this._use;
|
||||
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
angular.module('app').directive('barChart', ['$window', '$translate', '$rootScope', function($window, $translate, $rootScope) {
|
||||
|
||||
function bName(build) {
|
||||
return build.buildName + '\n' + build.name;
|
||||
}
|
||||
|
||||
function insertLinebreaks(d) {
|
||||
var el = d3.select(this);
|
||||
var lines = d.split('\n');
|
||||
el.text('').attr('y', -6);
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var tspan = el.append('tspan').text(lines[i].length > 18 ? lines[i].substring(0, 15) + '...' : lines[i]);
|
||||
if (i > 0) {
|
||||
tspan.attr('x', -9).attr('dy', '1em');
|
||||
} else {
|
||||
tspan.attr('class', 'primary');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
data: '=',
|
||||
facet: '='
|
||||
},
|
||||
link: function(scope, element) {
|
||||
var color = d3.scale.ordinal().range([ '#7b6888', '#6b486b', '#3182bd', '#a05d56', '#d0743c']),
|
||||
labels = scope.facet.lbls,
|
||||
fmt = null,
|
||||
unit = null,
|
||||
properties = scope.facet.props,
|
||||
margin = { top: 10, right: 20, bottom: 40, left: 150 },
|
||||
y0 = d3.scale.ordinal(),
|
||||
y1 = d3.scale.ordinal(),
|
||||
x = d3.scale.linear(),
|
||||
yAxis = d3.svg.axis().scale(y0).outerTickSize(0).orient('left'),
|
||||
xAxis = d3.svg.axis().scale(x).ticks(5).outerTickSize(0).orient('bottom');
|
||||
|
||||
// Create chart
|
||||
var svg = d3.select(element[0]).append('svg');
|
||||
var vis = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
|
||||
|
||||
// Create and Add tooltip
|
||||
var tip = d3.tip()
|
||||
.attr('class', 'd3-tip')
|
||||
.html(function(property, propertyIndex) {
|
||||
return (labels ? ($translate.instant(labels[propertyIndex]) + ': ') : '') + fmt(property.value) + ' ' + unit;
|
||||
});
|
||||
|
||||
vis.call(tip);
|
||||
|
||||
// Create Y Axis SVG Elements
|
||||
vis.append('g').attr('class', 'y axis');
|
||||
vis.selectAll('g.y.axis g text').each(insertLinebreaks);
|
||||
// Create X Axis SVG Elements
|
||||
var xAxisLbl = vis.append('g')
|
||||
.attr('class', 'x axis cap')
|
||||
.append('text')
|
||||
.attr('y', 33)
|
||||
.attr('dy', '.1em')
|
||||
.style('text-anchor', 'middle');
|
||||
|
||||
updateFormats();
|
||||
|
||||
function render() {
|
||||
var data = scope.data,
|
||||
width = element[0].offsetWidth,
|
||||
w = width - margin.left - margin.right,
|
||||
height = 50 + (30 * data.length * $rootScope.sizeRatio),
|
||||
h = height - margin.top - margin.bottom,
|
||||
maxVal = d3.max(data, function(d) { return d3.max(properties, function(p) {return d[p]; }); });
|
||||
|
||||
// Update chart size
|
||||
svg.attr('width', width).attr('height', height);
|
||||
|
||||
// Remove existing elements
|
||||
vis.selectAll('.ship').remove();
|
||||
vis.selectAll('rect').remove();
|
||||
|
||||
// Update X & Y Axis
|
||||
x.range([0, w]).domain([0, maxVal]);
|
||||
y0.domain(data.map(bName)).rangeRoundBands([0, h], 0.3);
|
||||
y1.domain(properties).rangeRoundBands([0, y0.rangeBand()]);
|
||||
vis.selectAll('.y.axis').call(yAxis);
|
||||
vis.selectAll('.x.axis').attr('transform', 'translate(0,' + h + ')').call(xAxis);
|
||||
xAxisLbl.attr('x', w / 2);
|
||||
// Update Y-Axis labels
|
||||
vis.selectAll('g.y.axis g text').each(insertLinebreaks);
|
||||
|
||||
var group = vis.selectAll('.ship')
|
||||
.data(scope.data, bName)
|
||||
.enter().append('g')
|
||||
.attr('class', 'g')
|
||||
.attr('transform', function(build) { return 'translate(0,' + y0(bName(build)) + ')'; });
|
||||
|
||||
group.selectAll('rect')
|
||||
.data(function(build) {
|
||||
var o = [];
|
||||
for (var i = 0; i < properties.length; i++) {
|
||||
o.push({ name: properties[i], value: build[properties[i]] });
|
||||
}
|
||||
return o;
|
||||
})
|
||||
.enter().append('rect')
|
||||
.attr('height', y1.rangeBand())
|
||||
.attr('x', 0)
|
||||
.attr('y', function(d) {return y1(d.name); })
|
||||
.attr('width', function(d) { return x(d.value); })
|
||||
.on('mouseover', tip.show)
|
||||
.on('mouseout', tip.hide)
|
||||
.style('fill', function(d) { return color(d.name); });
|
||||
|
||||
}
|
||||
|
||||
function updateFormats() {
|
||||
fmt = $rootScope[scope.facet.fmt];
|
||||
unit = $translate.instant(scope.facet.unit);
|
||||
xAxisLbl.text($translate.instant(scope.facet.title) + (unit ? (' (' + $translate.instant(unit) + ')') : ''));
|
||||
xAxis.tickFormat($rootScope.localeFormat.numberFormat('.2s'));
|
||||
render();
|
||||
}
|
||||
|
||||
angular.element($window).bind('orientationchange resize render', render);
|
||||
scope.$watchCollection('data', render); // Watch for changes in the comparison array
|
||||
scope.$on('languageChanged', updateFormats);
|
||||
scope.$on('$destroy', function() {
|
||||
angular.element($window).unbind('orientationchange resize render', render);
|
||||
tip.destroy(); // Remove the tooltip from the DOM
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -12,7 +12,7 @@ let fallbackTerms = EN.terms;
|
||||
/**
|
||||
* Get the units, translation and format functions for the specified language
|
||||
* @param {string} langCode ISO Language code
|
||||
* @return {object} Language units, translation and format functions
|
||||
* @return {Object} Language units, translation and format functions
|
||||
*/
|
||||
export function getLanguage(langCode) {
|
||||
let lang, translate;
|
||||
@@ -39,15 +39,16 @@ export function getLanguage(langCode) {
|
||||
|
||||
return {
|
||||
formats: {
|
||||
gen: gen, // General number format (.e.g 1,001,001.1234)
|
||||
gen, // General number format (.e.g 1,001,001.1234)
|
||||
int: d3Locale.numberFormat(',.0f'), // Fixed to 0 decimal places (.e.g 1,001)
|
||||
f2: d3Locale.numberFormat(',.2f'), // Fixed to 2 decimal places (.e.g 1,001.10)
|
||||
s2: d3Locale.numberFormat('.2s'), // SI Format to 2 decimal places (.e.g 1.1k)
|
||||
pct: d3Locale.numberFormat('.2%'), // % to 2 decimal places (.e.g 5.40%)
|
||||
pct1: d3Locale.numberFormat('.1%'), // % to 1 decimal places (.e.g 5.4%)
|
||||
r2: d3Locale.numberFormat('.2r'), // Rounded to 2 decimal places (.e.g 5.12, 4.10)
|
||||
r2: d3Locale.numberFormat('.2r'), // Rounded to 2 significant numbers (.e.g 512 => 510, 4.122 => 4.1)
|
||||
rPct: d3.format('%'), // % to 0 decimal places (.e.g 5%)
|
||||
round: (d) => gen(d3.round(d, 2)), // Rounded to 0-2 decimal places (.e.g 5.12, 4.1)
|
||||
time: (d) => Math.floor(d / 60) + ':' + ('00' + Math.floor(d % 60)).substr(-2, 2)
|
||||
time: (d) => (d < 0 ? '-' : '') + Math.floor(Math.abs(d) / 60) + ':' + ('00' + Math.floor(Math.abs(d) % 60)).substr(-2, 2)
|
||||
},
|
||||
translate,
|
||||
units: {
|
||||
@@ -64,8 +65,7 @@ export function getLanguage(langCode) {
|
||||
pm: <u>{translate('/min')}</u>, // per minute
|
||||
T: <u>{' ' + translate('T')}</u>, // Metric Tons
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ export const terms = {
|
||||
added: 'Hinzugefügt',
|
||||
Advanced: 'Verbessert',
|
||||
'Advanced Discovery Scanner': 'Fortgeschrittener Aufklärungsscanner',
|
||||
agility: 'Manövrierbarkeit',
|
||||
maneuverability: 'Manövrierbarkeit',
|
||||
ammo: 'Munition',
|
||||
PHRASE_CONFIRMATION: 'Sind Sie sicher?',
|
||||
armour: 'Panzerung',
|
||||
|
||||
@@ -14,220 +14,57 @@ export const formats = {
|
||||
};
|
||||
|
||||
export const terms = {
|
||||
PHRASE_EXPORT_DESC: 'A detailed JSON export of your build for use in other sites and tools',
|
||||
// 'A-Rated': 'A-Rated',
|
||||
// about: 'about',
|
||||
// action: 'action',
|
||||
// added: 'added',
|
||||
// Advanced: 'Advanced',
|
||||
// 'Advanced Discovery Scanner': 'Advanced Discovery Scanner',
|
||||
// agility: 'agility',
|
||||
// alpha: 'alpha',
|
||||
// ammo: 'ammo',
|
||||
PHRASE_BACKUP_DESC: 'Backup of all Coriolis data to save or transfer to another browser/device',
|
||||
PHRASE_CONFIRMATION: 'Are You Sure?',
|
||||
// armour: 'armour',
|
||||
PHRASE_EXPORT_DESC: 'A detailed JSON export of your build for use in other sites and tools',
|
||||
PHRASE_IMPORT: 'Paste JSON or import here',
|
||||
PHRASE_NO_BUILDS: 'No builds added to comparison!',
|
||||
PHRASE_NO_RETROCH: 'No Retrofitting changes',
|
||||
PHRASE_SELECT_BUILDS: 'Select Builds to Compare',
|
||||
PHRASE_UPDATE_RDY: 'Update Available! Click to Refresh',
|
||||
am: 'Auto Field-Maintenance Unit',
|
||||
// available: 'available',
|
||||
// backup: 'backup',
|
||||
'Basic Discovery Scanner': 'Basic Discovery Scanner',
|
||||
bl: 'Beam Laser',
|
||||
// beta: 'beta',
|
||||
// bins: 'bins',
|
||||
// boost: 'boost',
|
||||
// build: 'build',
|
||||
// 'build name': 'Build Name',
|
||||
// builds: 'builds',
|
||||
bh: 'bulkheads',
|
||||
bsg: 'Bi-Weave Shield Generator',
|
||||
ul: 'Burst Laser',
|
||||
buy: 'buy',
|
||||
// cancel: 'cancel',
|
||||
c: 'Cannon',
|
||||
// capital: 'capital',
|
||||
// cargo: 'cargo',
|
||||
// 'Cargo Hatch': 'Cargo Hatch',
|
||||
cr: 'Cargo Rack',
|
||||
cs: 'Cargo Scanner',
|
||||
cells: 'cells',
|
||||
// 'Chaff Launcher': 'Chaff Launcher',
|
||||
// close: 'close',
|
||||
cc: 'Collector Limpet Controller',
|
||||
// compare: 'compare',
|
||||
// 'compare all': 'compare all',
|
||||
// comparison: 'comparison',
|
||||
// comparisons: 'comparisons',
|
||||
// component: 'component',
|
||||
// cost: 'cost',
|
||||
// costs: 'costs',
|
||||
cm: 'Countermeasure',
|
||||
// CR: 'CR',
|
||||
// create: 'create',
|
||||
// 'create new': 'create new',
|
||||
// credits: 'credits',
|
||||
// Cytoscrambler: 'Cytoscrambler',
|
||||
// damage: 'damage',
|
||||
// delete: 'delete',
|
||||
// 'delete all': 'delete all',
|
||||
// dep: 'dep',
|
||||
// deployed: 'deployed',
|
||||
// 'detailed export': 'detailed export',
|
||||
// 'Detailed Surface Scanner': 'Detailed Surface Scanner',
|
||||
// disabled: 'disabled',
|
||||
// discount: 'discount',
|
||||
// Distruptor: 'Distruptor',
|
||||
dc: 'Docking Computer',
|
||||
// done: 'done',
|
||||
// DPS: 'DPS',
|
||||
// 'edit data': 'edit data',
|
||||
// efficiency: 'efficiency',
|
||||
// 'Electronic Countermeasure': 'Electronic Countermeasure',
|
||||
// empty: 'empty',
|
||||
// Enforcer: 'Enforcer',
|
||||
// ENG: 'ENG',
|
||||
// 'Enter Name': 'Enter Name',
|
||||
// EPS: 'EPS',
|
||||
// export: 'export',
|
||||
// fixed: 'fixed',
|
||||
// forum: 'forum',
|
||||
fc: 'Fragment Cannon',
|
||||
fd: 'Frame Shift Drive',
|
||||
ws: 'Frame Shift Wake Scanner',
|
||||
// FSD: 'FSD',
|
||||
fsd: 'Frame Shift Drive',
|
||||
fi: 'FSD Interdictor',
|
||||
// fuel: 'fuel',
|
||||
fs: 'Fuel Scoop',
|
||||
ft: 'Fuel Tank',
|
||||
fx: 'Fuel Transfer Limpet Controller',
|
||||
// 'full tank': 'full tank',
|
||||
// Gimballed: 'Gimballed',
|
||||
// H: 'H',
|
||||
// hardpoints: 'hardpoints',
|
||||
hb: 'Hatch Breaker Limpet Controller',
|
||||
// 'Heat Sink Launcher': 'Heat Sink Launcher',
|
||||
// huge: 'huge',
|
||||
// hull: 'hull',
|
||||
hr: 'Hull Reinforcement Package',
|
||||
// 'Imperial Hammer': 'Imperial Hammer',
|
||||
// import: 'import',
|
||||
// 'import all': 'import all',
|
||||
// insurance: 'insurance',
|
||||
// 'Intermediate Discovery Scanner': 'Intermediate Discovery Scanner',
|
||||
// 'internal compartments': 'internal compartments',
|
||||
// 'jump range': 'jump range',
|
||||
// jumps: 'jumps',
|
||||
kw: 'Kill Warrant Scanner',
|
||||
// L: 'L',
|
||||
// laden: 'laden',
|
||||
// language: 'language',
|
||||
// large: 'large',
|
||||
// 'limpets': 'Limpets',
|
||||
ls: 'life support',
|
||||
// 'Lightweight Alloy': 'Lightweight Alloy',
|
||||
// 'lock factor': 'lock factor',
|
||||
// LS: 'Ls',
|
||||
// LY: 'LY',
|
||||
// M: 'M',
|
||||
// 'm/s': 'm/s',
|
||||
// mass: 'mass',
|
||||
// max: 'max',
|
||||
// 'max mass': 'max mass',
|
||||
// medium: 'medium',
|
||||
// 'Military Grade Composite': 'Military Grade Composite',
|
||||
nl: 'Mine Launcher',
|
||||
// 'Mining Lance': 'Mining Lance',
|
||||
ml: 'Mining Laser',
|
||||
// 'Mirrored Surface Composite': 'Mirrored Surface Composite',
|
||||
mr: 'Missile Rack',
|
||||
mc: 'Multi-cannon',
|
||||
// 'net cost': 'net cost',
|
||||
// no: 'no',
|
||||
PHRASE_NO_BUILDS: 'No builds added to comparison!',
|
||||
PHRASE_NO_RETROCH: 'No Retrofitting changes',
|
||||
// none: 'none',
|
||||
// 'none created': 'none created',
|
||||
// off: 'off',
|
||||
// on: 'on',
|
||||
// optimal: 'optimal',
|
||||
// 'optimal mass': 'optimal mass',
|
||||
// 'optimize mass': 'optimize mass',
|
||||
// overwrite: 'overwrite',
|
||||
// Pacifier: 'Pacifier',
|
||||
// 'Pack-Hound': 'Pack-Hound',
|
||||
PHRASE_IMPORT: 'Paste JSON or import here',
|
||||
// pen: 'pen',
|
||||
// penetration: 'penetration',
|
||||
// permalink: 'permalink',
|
||||
pa: 'Plasma Accelerator',
|
||||
// 'Point Defence': 'Point Defence',
|
||||
// power: 'power',
|
||||
pd: 'power distributor',
|
||||
pp: 'power plant',
|
||||
pri: 'pri',
|
||||
// priority: 'priority',
|
||||
psg: 'Prismatic Shield Generator',
|
||||
// proceed: 'proceed',
|
||||
pc: 'Prospector Limpet Controller',
|
||||
pl: 'Pulse Laser',
|
||||
pv: 'Planetary Vehicle Hanger',
|
||||
// PWR: 'PWR',
|
||||
rg: 'Rail Gun',
|
||||
// range: 'range',
|
||||
// rate: 'rate',
|
||||
// 'Reactive Surface Composite': 'Reactive Surface Composite',
|
||||
// recharge: 'recharge',
|
||||
rf: 'Refinery',
|
||||
// 'refuel time': 'refuel time',
|
||||
// 'Reinforced Alloy': 'Reinforced Alloy',
|
||||
// reload: 'reload',
|
||||
// rename: 'rename',
|
||||
// repair: 'repair',
|
||||
// reset: 'reset',
|
||||
// ret: 'ret',
|
||||
// retracted: 'retracted',
|
||||
// 'retrofit costs': 'retrofit costs',
|
||||
// 'retrofit from': 'retrofit from',
|
||||
// ROF: 'ROF',
|
||||
// S: 'S',
|
||||
// save: 'save',
|
||||
sc: 'scanner',
|
||||
PHRASE_SELECT_BUILDS: 'Select Builds to Compare',
|
||||
// sell: 'sell',
|
||||
s: 'sensors',
|
||||
// settings: 'settings',
|
||||
sb: 'Shield Booster',
|
||||
scb: 'Shield Cell Bank',
|
||||
sg: 'Shield Generator',
|
||||
// shields: 'shields',
|
||||
// ship: 'ship',
|
||||
// ships: 'ships',
|
||||
// shortened: 'shortened',
|
||||
// size: 'size',
|
||||
// skip: 'skip',
|
||||
// small: 'small',
|
||||
// speed: 'speed',
|
||||
// standard: 'standard',
|
||||
// 'Standard Docking Computer': 'Standard Docking Computer',
|
||||
// Stock: 'Stock',
|
||||
// SYS: 'SYS',
|
||||
// T: 'T',
|
||||
T_LOAD: 't-load',
|
||||
// 'The Retributor': 'The Retributor',
|
||||
t: 'thrusters',
|
||||
// time: 'time',
|
||||
tp: 'Torpedo Pylon',
|
||||
// total: 'total',
|
||||
// 'total range': 'total range',
|
||||
// turret: 'turret',
|
||||
// type: 'type',
|
||||
// U: 'U',
|
||||
// unladen: 'unladen',
|
||||
PHRASE_UPDATE_RDY: 'Update Available! Click to Refresh',
|
||||
// URL: 'URL',
|
||||
// utility: 'utility',
|
||||
// 'utility mounts': 'utility mounts',
|
||||
// version: 'version',
|
||||
// WEP: 'WEP',
|
||||
// yes: 'yes',
|
||||
PHRASE_BACKUP_DESC: 'Backup of all Coriolis data to save or transfer to another browser/device'
|
||||
tp: 'Torpedo Pylon'
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export const terms = {
|
||||
'action': 'Acci\u00f3n',
|
||||
'added': 'A\u00f1adido',
|
||||
'Advanced Discovery Scanner': 'Esc\u00e1ner de exploraci\u00f3n avanzado',
|
||||
'agility': 'Maniobrabilidad',
|
||||
maneuverability: 'Maniobrabilidad',
|
||||
'alpha': 'Alfa',
|
||||
'ammo': 'Munici\u00f3n',
|
||||
'PHRASE_CONFIRMATION': '\u00bfEst\u00e1s seguro?',
|
||||
|
||||
@@ -20,7 +20,7 @@ export const terms = {
|
||||
added: 'ajouté',
|
||||
Advanced: 'Avancé',
|
||||
'Advanced Discovery Scanner': 'Détecteur découverte avancé',
|
||||
agility: 'manœuvrabilité',
|
||||
maneuverability: 'manœuvrabilité',
|
||||
ammo: 'munitions',
|
||||
PHRASE_CONFIRMATION: 'Êtes-vous sûr ?',
|
||||
armour: 'Armure',
|
||||
|
||||
@@ -20,7 +20,7 @@ export const terms = {
|
||||
action: 'azione',
|
||||
added: 'aggiunto',
|
||||
Advanced: 'Avanzato',
|
||||
agility: 'agilità',
|
||||
maneuverability: 'agilità',
|
||||
ammo: 'munizioni',
|
||||
PHRASE_CONFIRMATION: 'Sei sicuro ?',
|
||||
armour: 'armatura',
|
||||
|
||||
@@ -21,7 +21,7 @@ export const terms = {
|
||||
added: 'Добавлено',
|
||||
Advanced: 'Продвинутый',
|
||||
'Advanced Discovery Scanner': 'Продвинутый астросканер',
|
||||
agility: 'Маневренность',
|
||||
maneuverability: 'Маневренность',
|
||||
alpha: 'Альфа',
|
||||
ammo: 'Боекомплект',
|
||||
PHRASE_CONFIRMATION: 'Вы уверены?',
|
||||
|
||||
@@ -2,8 +2,15 @@ import React from 'react';
|
||||
import Page from './Page';
|
||||
import { CoriolisLogo, GitHub } from '../components/SvgIcons';
|
||||
|
||||
/**
|
||||
* About Page
|
||||
*/
|
||||
export default class AboutPage extends Page {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -11,6 +18,10 @@ export default class AboutPage extends Page {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Page
|
||||
* @return {React.Component} The page contents
|
||||
*/
|
||||
render() {
|
||||
return <div className={'page'} style={{ textAlign: 'left', maxWidth: 800, margin: '0 auto' }}>
|
||||
<h1><CoriolisLogo style={{ marginRight: '0.4em' }} className='xl'/><span className='warning'>Coriolis</span></h1>
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
import React from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import Page from './Page';
|
||||
import Router from '../Router';
|
||||
import cn from 'classnames';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import Serializer from '../shipyard/Serializer';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import { fromComparison, toComparison } from '../shipyard/Serializer';
|
||||
import Persist from '../stores/Persist';
|
||||
import { SizeMap, ShipFacets } from '../shipyard/Constants';
|
||||
import ComparisonTable from '../components/ComparisonTable';
|
||||
import BarChart from '../components/BarChart';
|
||||
import ModalCompare from '../components/ModalCompare';
|
||||
import ModalExport from '../components/ModalExport';
|
||||
import ModalPermalink from '../components/ModalPermalink';
|
||||
import { FloppyDisk, Bin, Download, Embed, Rocket, LinkIcon } from '../components/SvgIcons';
|
||||
import ShortenUrl from '../utils/ShortenUrl';
|
||||
import { comparisonBBCode } from '../utils/BBCode';
|
||||
|
||||
|
||||
/**
|
||||
* Creates a comparator based on the specified predicate
|
||||
* @param {string} predicate Predicate / propterty name
|
||||
* @return {Function} Comparator
|
||||
*/
|
||||
function sortBy(predicate) {
|
||||
return (a, b) => {
|
||||
if (a[predicate] === b[predicate]) {
|
||||
return 0;
|
||||
if (a.name == b.name) {
|
||||
a.buildName.toLowerCase() > b.buildName.toLowerCase() ? 1 : -1;
|
||||
}
|
||||
return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1;
|
||||
}
|
||||
if (typeof a[predicate] == 'string') {
|
||||
return a[predicate].toLowerCase() > b[predicate].toLowerCase() ? 1 : -1;
|
||||
@@ -25,23 +38,35 @@ function sortBy(predicate) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparison Page
|
||||
*/
|
||||
export default class ComparisonPage extends Page {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this._sortShips = this._sortShips.bind(this);
|
||||
this._buildsSelected = this._buildsSelected.bind(this);
|
||||
this.state = this._initState(props, context);
|
||||
this.state = this._initState(context);
|
||||
}
|
||||
|
||||
_initState(props, context) {
|
||||
/**
|
||||
* [Re]Create initial state from context
|
||||
* @param {context} context React component context
|
||||
* @return {Object} New state object
|
||||
*/
|
||||
_initState(context) {
|
||||
let defaultFacets = [9, 6, 4, 1, 3, 2]; // Reverse order of Armour, Shields, Speed, Jump Range, Cargo Capacity, Cost
|
||||
let params = context.route.params;
|
||||
let code = params.code;
|
||||
let name = params.name ? decodeURIComponent(params.name) : null;
|
||||
let newName = '';
|
||||
let compareMode = !code;
|
||||
let facets = [];
|
||||
let builds = [];
|
||||
let saved = false;
|
||||
let predicate = 'name';
|
||||
@@ -58,7 +83,6 @@ export default class ComparisonPage extends Page {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
let comparisonData = Persist.getComparison(name);
|
||||
if (comparisonData) {
|
||||
defaultFacets = comparisonData.facets;
|
||||
@@ -69,7 +93,7 @@ export default class ComparisonPage extends Page {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
let comparisonData = Serializer.toComparison(code);
|
||||
let comparisonData = toComparison(code);
|
||||
defaultFacets = comparisonData.f;
|
||||
newName = name = comparisonData.n;
|
||||
predicate = comparisonData.p;
|
||||
@@ -86,20 +110,22 @@ export default class ComparisonPage extends Page {
|
||||
}
|
||||
}
|
||||
|
||||
let facets = [];
|
||||
let selectedLength = defaultFacets.length;
|
||||
let selectedFacets = new Array(selectedLength);
|
||||
|
||||
for (let i = 0; i < ShipFacets.length; i++) {
|
||||
facets.push(Object.assign({ index: i }, ShipFacets[i]));
|
||||
}
|
||||
|
||||
let selectedFacets = [];
|
||||
|
||||
for (let fi of defaultFacets) {
|
||||
let facet = facets.splice(fi, 1)[0];
|
||||
facet.active = true;
|
||||
selectedFacets.unshift(facet);
|
||||
let facet = Object.assign({ }, ShipFacets[i]);
|
||||
let defaultIndex = defaultFacets.indexOf(facet.i);
|
||||
if(defaultIndex == -1) {
|
||||
facets.push(facet);
|
||||
} else {
|
||||
facet.active = true;
|
||||
selectedFacets[selectedLength - defaultIndex - 1] = facet;
|
||||
}
|
||||
}
|
||||
|
||||
facets = selectedFacets.concat(facets);
|
||||
console.log(selectedFacets);
|
||||
builds.sort(sortBy(predicate));
|
||||
|
||||
return {
|
||||
@@ -116,7 +142,13 @@ console.log(selectedFacets);
|
||||
importObj
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Ship instance / build
|
||||
* @param {string} id Ship Id
|
||||
* @param {name} name Build name
|
||||
* @param {string} code Optional - Serialized ship code
|
||||
* @return {Object} Ship instance with build name
|
||||
*/
|
||||
_createBuild(id, name, code) {
|
||||
code = code ? code : Persist.getBuild(id, name); // Retrieve build code if not passed
|
||||
|
||||
@@ -132,8 +164,8 @@ console.log(selectedFacets);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sort ships
|
||||
* @param {object} key Sort predicate
|
||||
* Update state with the specified sort predicates
|
||||
* @param {String} predicate Sort predicate - property name
|
||||
*/
|
||||
_sortShips(predicate) {
|
||||
let { builds, desc } = this.state;
|
||||
@@ -150,38 +182,54 @@ console.log(selectedFacets);
|
||||
this.setState({ predicate, desc });
|
||||
};
|
||||
|
||||
/**
|
||||
* Show selected builds modal
|
||||
*/
|
||||
_selectBuilds() {
|
||||
InterfaceEvents.showModal(React.cloneElement(
|
||||
<ModalCompare onSelect={this._buildsSelected}/>,
|
||||
{ builds: this.state.builds }
|
||||
));
|
||||
this.context.showModal(<ModalCompare onSelect={this._buildsSelected} builds={this.state.builds} />);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selected builds with new list
|
||||
* @param {Array} newBuilds List of new builds
|
||||
*/
|
||||
_buildsSelected(newBuilds) {
|
||||
InterfaceEvents.hideModal();
|
||||
this.context.hideModal();
|
||||
let builds = [];
|
||||
|
||||
for (let b of newBuilds) {
|
||||
builds.push(this._createBuild(b.id, b.buildName));
|
||||
}
|
||||
|
||||
this.setState({ builds });
|
||||
this.setState({ builds, saved: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle facet display
|
||||
* @param {string} facet Facet / Ship Property
|
||||
*/
|
||||
_toggleFacet(facet) {
|
||||
facet.active = !facet.active;
|
||||
this.setState({ facets: [].concat(this.state.facets), saved: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle facet drag
|
||||
* @param {Event} e Drag Event
|
||||
*/
|
||||
_facetDrag(e) {
|
||||
this.dragged = e.currentTarget;
|
||||
let placeholder = this.placeholder = document.createElement("li");
|
||||
let placeholder = this.placeholder = document.createElement('li');
|
||||
placeholder.style.width = this.dragged.offsetWidth + 'px';
|
||||
placeholder.className = "facet-placeholder";
|
||||
placeholder.className = 'facet-placeholder';
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData("text/html", e.currentTarget);
|
||||
e.dataTransfer.setData('text/html', e.currentTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle facet drop
|
||||
* @param {Event} e Drop Event
|
||||
*/
|
||||
_facetDrop(e) {
|
||||
this.dragged.parentNode.removeChild(this.placeholder);
|
||||
let facets = this.state.facets;
|
||||
@@ -197,18 +245,22 @@ console.log(selectedFacets);
|
||||
|
||||
facets.splice(to, 0, facets.splice(frm, 1)[0]);
|
||||
this.dragged.style.display = null;
|
||||
this.setState({ facets: [].concat(facets) });
|
||||
this.setState({ facets: [].concat(facets), saved: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle facet drag over
|
||||
* @param {Event} e Drag over Event
|
||||
*/
|
||||
_facetDragOver(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if(e.target.className == "facet-placeholder") {
|
||||
if(e.target.className == 'facet-placeholder') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.over = e.target;
|
||||
this.dragged.style.display = "none";
|
||||
this.dragged.style.display = 'none';
|
||||
let relX = e.clientX - this.over.getBoundingClientRect().left;
|
||||
let width = this.over.offsetWidth / 2;
|
||||
let parent = e.target.parentNode;
|
||||
@@ -217,71 +269,149 @@ console.log(selectedFacets);
|
||||
if(relX > width) {
|
||||
this.nodeAfter = true;
|
||||
parent.insertBefore(this.placeholder, e.target.nextElementSibling);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.nodeAfter = false;
|
||||
parent.insertBefore(this.placeholder, e.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle name change and update state
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_onNameChange(e) {
|
||||
this.setState({ newName: e.target.value, saved: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the current comparison
|
||||
*/
|
||||
_delete() {
|
||||
Persist.deleteComparison(this.state.name);
|
||||
Router.go('/compare');
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the comparison builds
|
||||
*/
|
||||
_import() {
|
||||
|
||||
// TODO: Implement
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current comparison
|
||||
*/
|
||||
_save() {
|
||||
let { newName, builds, facets } = this.state;
|
||||
|
||||
let selectedFacets = [];
|
||||
|
||||
facets.forEach((f) => {
|
||||
if (f.active) {
|
||||
selectedFacets.unshift(f.index);
|
||||
selectedFacets.unshift(f.i);
|
||||
}
|
||||
});
|
||||
console.log(selectedFacets);
|
||||
//Persist.saveComparison(newName, builds, selectedFacets);
|
||||
Router.replace(`/compare/${encodeURIComponent(this.state.newName)}`)
|
||||
this.setState ({ name: newName, saved: true });
|
||||
|
||||
Persist.saveComparison(newName, builds, selectedFacets);
|
||||
Router.replace(`/compare/${encodeURIComponent(this.state.newName)}`);
|
||||
this.setState({ name: newName, saved: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize and generate a long URL for the current comparison
|
||||
* @return {string} URL for serialized comparison
|
||||
*/
|
||||
_buildUrl() {
|
||||
let { facets, builds, name, predicate, desc } = this.state;
|
||||
let selectedFacets = [];
|
||||
|
||||
for (let f of facets) {
|
||||
if (f.active) {
|
||||
selectedFacets.unshift(f.i);
|
||||
}
|
||||
}
|
||||
|
||||
let code = fromComparison(name, builds, selectedFacets, predicate, desc);
|
||||
let loc = window.location;
|
||||
return `${loc.protocol}://${loc.host}/comparison/${code}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the long permalink URL
|
||||
* @return {string} The long permalink URL
|
||||
*/
|
||||
_genPermalink() {
|
||||
let { facets, builds, name, predicate, desc } = this.state;
|
||||
let selectedFacets = [];
|
||||
|
||||
for (let f of facets){
|
||||
if (f.active) {
|
||||
selectedFacets.unshift(f.index);
|
||||
}
|
||||
}
|
||||
|
||||
let code = Serializer.fromComparison(name, builds, selectedFacets, predicate, desc);
|
||||
// send code to permalink modal
|
||||
this.context.showModal(<ModalPermalink url={this._buildUrl()}/>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate E:D Forum BBCode and show in the export modal
|
||||
*/
|
||||
_genBBcode() {
|
||||
let { translate, formats } = this.context.language;
|
||||
let { facets, builds } = this.state;
|
||||
|
||||
let generator = (callback) => {
|
||||
let url = this._buildUrl();
|
||||
ShortenUrl(url,
|
||||
(shortenedUrl) => callback(comparisonBBCode(translate, formats, facets, builds, shortenedUrl)),
|
||||
(error) => callback(comparisonBBCode(translate, formats, facets, builds, url))
|
||||
);
|
||||
};
|
||||
|
||||
this.context.showModal(<ModalExport
|
||||
title={translate('forum') + ' BBCode'}
|
||||
generator={generator}
|
||||
/>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dimenions from rendered DOM
|
||||
*/
|
||||
_updateDimensions() {
|
||||
this.setState({
|
||||
chartWidth: findDOMNode(this.refs.chartRef).offsetWidth
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
|
||||
this.setState(this._initState(nextProps, nextContext));
|
||||
this.setState(this._initState(nextContext));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add listeners when about to mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
this.resizeListener = this.context.onWindowResize(this._updateDimensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger DOM updates on mount
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updateDimensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove listeners on unmount
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this.resizeListener.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Page
|
||||
* @return {React.Component} The page contents
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let compareHeader;
|
||||
let {newName, name, saved, builds, facets, predicate, desc } = this.state;
|
||||
let { newName, name, saved, builds, facets, predicate, desc, chartWidth } = this.state;
|
||||
|
||||
if (this.state.compareMode) {
|
||||
compareHeader = <tr>
|
||||
@@ -295,10 +425,10 @@ console.log(selectedFacets);
|
||||
<button onClick={this._selectBuilds}>
|
||||
<Rocket className='lg'/><span className='button-lbl'>{translate('builds')}</span>
|
||||
</button>
|
||||
<button className='r' ng-click='permalink($event)' ng-disabled='builds.length == 0'>
|
||||
<button className='r' onClick={this._genPermalink} disabled={builds.length == 0}>
|
||||
<LinkIcon className='lg'/><span className='button-lbl'>{translate('permalink')}</span>
|
||||
</button>
|
||||
<button className='r' ng-click='embed($event)' ng-disabled='builds.length == 0'>
|
||||
<button className='r' onClick={this._genBBcode} disabled={builds.length == 0}>
|
||||
<Embed className='lg'/><span className='button-lbl'>{translate('forum')}</span>
|
||||
</button>
|
||||
</td>
|
||||
@@ -314,7 +444,7 @@ console.log(selectedFacets);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'page'} style={{ fontSize: this.context.sizeRatio + 'em'}}>
|
||||
<div className={'page'} style={{ fontSize: this.context.sizeRatio + 'em' }}>
|
||||
<table id='comparison'>
|
||||
<tbody>
|
||||
{compareHeader}
|
||||
@@ -323,7 +453,7 @@ console.log(selectedFacets);
|
||||
<td>
|
||||
<ul id='facet-container' onDragOver={this._facetDragOver}>
|
||||
{facets.map((f, i) =>
|
||||
<li key={f.title} data-i={i} draggable='true' onDragStart={this._facetDrag} onDragEnd={this._facetDrop} className={cn('facet', {active: f.active})} onClick={this._toggleFacet.bind(this, f)}>
|
||||
<li key={f.title} data-i={i} draggable='true' onDragStart={this._facetDrag} onDragEnd={this._facetDrop} className={cn('facet', { active: f.active })} onClick={this._toggleFacet.bind(this, f)}>
|
||||
{'↔ ' + translate(f.title)}
|
||||
</li>
|
||||
)}
|
||||
@@ -335,9 +465,23 @@ console.log(selectedFacets);
|
||||
|
||||
<ComparisonTable builds={builds} facets={facets} onSort={this._sortShips} predicate={predicate} desc={desc} />
|
||||
|
||||
{/*<div ng-repeat='f in facets | filter:{active:true}' ng-if='builds.length > 0' className='chart' bar-chart facet='f' data='builds'>
|
||||
<h3 ng-click='sort(f.props[0])' >{{f.title | translate}}</h3>
|
||||
</div>*/}
|
||||
{!builds.length ?
|
||||
<div className='chart' ref={'chartRef'}>{translate('PHRASE_NO_BUILDS')}</div> :
|
||||
facets.filter((f) => f.active).map((f, i) =>
|
||||
<div key={f.title} className='chart' ref={ i == 0 ? 'chartRef' : null}>
|
||||
<h3 className='ptr' onClick={this._sortShips.bind(this, f.props[0])}>{translate(f.title)}</h3>
|
||||
<BarChart
|
||||
width={chartWidth}
|
||||
data={builds}
|
||||
properties={f.props}
|
||||
unit={translate(f.unit)}
|
||||
format={f.fmt}
|
||||
label={translate(f.title)}
|
||||
predicate={predicate}
|
||||
desc={desc}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import React from 'react';
|
||||
import Page from './Page';
|
||||
|
||||
/**
|
||||
* Unexpected Error page
|
||||
* TODO: Implement properly and test
|
||||
*/
|
||||
export default class ErrorPage extends Page {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -10,22 +18,26 @@ export default class ErrorPage extends Page {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Page
|
||||
* @return {React.Component} The page contents
|
||||
*/
|
||||
render() {
|
||||
let msgPre, msgHighlight, msgPost, errorMessage, details;
|
||||
let msgPre, msgHighlight, msgPost, errorMessage, details, type;
|
||||
|
||||
switch ($scope.type) {
|
||||
switch (type) {
|
||||
case 404:
|
||||
msgPre = 'Page';
|
||||
msgHighlight = this.context.route.path;
|
||||
msgPost = 'Not Found';
|
||||
msgPre = 'Page';
|
||||
msgHighlight = this.context.route.path;
|
||||
msgPost = 'Not Found';
|
||||
break;
|
||||
case 'no-ship':
|
||||
msgPre = 'Ship';
|
||||
msgHighlight = this.props.message;
|
||||
msgPost = 'does not exist';
|
||||
msgPre = 'Ship';
|
||||
msgHighlight = this.props.message;
|
||||
msgPost = 'does not exist';
|
||||
break;
|
||||
case 'build':
|
||||
msgPre = 'Build Failure!';
|
||||
msgPre = 'Build Failure!';
|
||||
break;
|
||||
default:
|
||||
msgPre = 'Uh, Jameson, we have a problem..';
|
||||
@@ -38,9 +50,9 @@ export default class ErrorPage extends Page {
|
||||
|
||||
return <div className='error'>
|
||||
<h1>
|
||||
<span>{{msgPre}}</span>
|
||||
<small>{{msgHighlight}}</small>
|
||||
<span>{{msgPost}}</span>
|
||||
<span>{msgPre}</span>
|
||||
<small>{msgHighlight}</small>
|
||||
<span>{msgPost}</span>
|
||||
</h1>
|
||||
|
||||
<div style={{ textAlign:'left', fontSize:'0.8em', width: '43em', margin: '0 auto' }}>
|
||||
@@ -48,7 +60,7 @@ export default class ErrorPage extends Page {
|
||||
<a href='https://github.com/cmmcleod/coriolis/issues' target='_blank' title='Coriolis Github Project'>Create an issue on Github</a>
|
||||
if this keeps happening. Add these details:
|
||||
</div>
|
||||
<div style={{marginTop: '2em'}}>
|
||||
<div style={{ marginTop: '2em' }}>
|
||||
<div>Browser: {window.navigator.userAgent}</div>
|
||||
<div>Path: {this.context.route.canonicalPath}</div>
|
||||
<div>Error:<br/>{this.props.type || 'Unknown'}</div>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import React from 'react';
|
||||
import Page from './Page';
|
||||
|
||||
/**
|
||||
* 404 Page
|
||||
*/
|
||||
export default class NotFoundPage extends Page {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -10,6 +17,10 @@ export default class NotFoundPage extends Page {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Page
|
||||
* @return {React.Component} The page contents
|
||||
*/
|
||||
render() {
|
||||
return <div className={'page'}>Page {JSON.stringify(this.props.context)} Not Found</div>;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import Page from './Page';
|
||||
import cn from 'classnames';
|
||||
import Router from '../Router';
|
||||
import Persist from '../stores/Persist';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import { toDetailedBuild } from '../shipyard/Serializer';
|
||||
@@ -23,13 +22,26 @@ import Slider from '../components/Slider';
|
||||
const SPEED_SERIES = ['boost', '4 Pips', '2 Pips', '0 Pips'];
|
||||
const SPEED_COLORS = ['#0088d2', '#ff8c0d', '#D26D00', '#c06400'];
|
||||
|
||||
/**
|
||||
* The Outfitting Page
|
||||
*/
|
||||
export default class OutfittingPage extends Page {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = this._initState(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* [Re]Create initial state from context
|
||||
* @param {context} context React component context
|
||||
* @return {Object} New state object
|
||||
*/
|
||||
_initState(context) {
|
||||
let params = context.route.params;
|
||||
let shipId = params.ship;
|
||||
@@ -70,12 +82,16 @@ export default class OutfittingPage extends Page {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle build name change and update state
|
||||
* @param {SyntheticEvent} event React Event
|
||||
*/
|
||||
_buildNameChange(event) {
|
||||
let stateChanges = {
|
||||
buildName: event.target.value
|
||||
}
|
||||
};
|
||||
|
||||
if(Persist.hasBuild(this.state.shipId, stateChanges.buildName)) {
|
||||
if (Persist.hasBuild(this.state.shipId, stateChanges.buildName)) {
|
||||
stateChanges.savedCode = Persist.getBuild(this.state.shipId, stateChanges.buildName);
|
||||
} else {
|
||||
stateChanges.savedCode = null;
|
||||
@@ -84,37 +100,55 @@ export default class OutfittingPage extends Page {
|
||||
this.setState(stateChanges);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current build
|
||||
*/
|
||||
_saveBuild() {
|
||||
let code = this.state.ship.toString();
|
||||
Persist.saveBuild(this.state.shipId, this.state.buildName, code);
|
||||
this.setState({ code, savedCode: code});
|
||||
this.setState({ code, savedCode: code });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload build from last save
|
||||
*/
|
||||
_reloadBuild() {
|
||||
this.state.ship.buildFrom(this.state.savedCode);
|
||||
this._shipUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset build to Stock/Factory defaults
|
||||
*/
|
||||
_resetBuild() {
|
||||
this.state.ship.buildWith(Ships[this.state.shipId].defaults);
|
||||
this._shipUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the build
|
||||
*/
|
||||
_deleteBuild() {
|
||||
Persist.deleteBuild(this.state.shipId, this.state.buildName);
|
||||
Router.go(`/outfit/${this.state.shipId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialized and show the export modal
|
||||
*/
|
||||
_exportBuild() {
|
||||
let translate = this.context.language.translate;
|
||||
let {buildName, ship } = this.state;
|
||||
InterfaceEvents.showModal(<ModalExport
|
||||
let { buildName, ship } = this.state;
|
||||
this.context.showModal(<ModalExport
|
||||
title={buildName + ' ' + translate('export')}
|
||||
description={translate('PHRASE_EXPORT_DESC')}
|
||||
data={toDetailedBuild(buildName, ship, ship.toString())}
|
||||
/>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger render on ship model change
|
||||
*/
|
||||
_shipUpdated() {
|
||||
let { shipId, buildName, ship, fuelCapacity } = this.state;
|
||||
let code = ship.toString();
|
||||
@@ -127,6 +161,12 @@ export default class OutfittingPage extends Page {
|
||||
this.setState({ code });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current route based on build
|
||||
* @param {string} shipId Ship Id
|
||||
* @param {string} code Serialized ship 'code'
|
||||
* @param {string} buildName Current build name
|
||||
*/
|
||||
_updateRoute(shipId, code, buildName) {
|
||||
let qStr = '';
|
||||
|
||||
@@ -137,6 +177,10 @@ export default class OutfittingPage extends Page {
|
||||
Router.replace(`/outfit/${shipId}/${code}${qStr}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current fuel level
|
||||
* @param {number} fuelLevel Fuel leval 0 - 1
|
||||
*/
|
||||
_fuelChange(fuelLevel) {
|
||||
let ship = this.state.ship;
|
||||
let fuelCapacity = ship.fuelCapacity;
|
||||
@@ -150,34 +194,57 @@ export default class OutfittingPage extends Page {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dimenions from rendered DOM
|
||||
*/
|
||||
_updateDimensions() {
|
||||
this.setState({
|
||||
chartWidth: findDOMNode(this.refs.chartThird).offsetWidth
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
|
||||
this.setState(this._initState(nextContext));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
this.resizeListener = InterfaceEvents.addListener('windowResized', this._updateDimensions);
|
||||
/**
|
||||
* Add listeners when about to mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
this.resizeListener = this.context.onWindowResize(this._updateDimensions);
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
/**
|
||||
* Trigger DOM updates on mount
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updateDimensions();
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
/**
|
||||
* Remove listeners on unmount
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this.resizeListener.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Page
|
||||
* @return {React.Component} The page contents
|
||||
*/
|
||||
render() {
|
||||
let { translate, units, formats } = this.context.language;
|
||||
let { translate, units, formats, termtip } = this.context.language;
|
||||
let tip = this.context.termtip;
|
||||
let hide = this.context.tooltip.bind(null, null);
|
||||
let state = this.state;
|
||||
let { ship, code, savedCode, buildName, chartWidth } = state;
|
||||
let { ship, code, savedCode, buildName, chartWidth, fuelCapacity, fuelLevel } = state;
|
||||
let menu = this.props.currentMenu;
|
||||
let shipUpdated = this._shipUpdated;
|
||||
let hStr = ship.getHardpointsString();
|
||||
@@ -185,25 +252,25 @@ export default class OutfittingPage extends Page {
|
||||
let iStr = ship.getInternalString();
|
||||
|
||||
return (
|
||||
<div id='outfit' className={'page'} style={{ fontSize: (this.context.sizeRatio * 0.9) + 'em'}}>
|
||||
<div id='outfit' className={'page'} style={{ fontSize: (this.context.sizeRatio * 0.9) + 'em' }}>
|
||||
<div id='overview'>
|
||||
<h1>{ship.name}</h1>
|
||||
<div id='build'>
|
||||
<input value={buildName} onChange={this._buildNameChange} placeholder={translate('Enter Name')} maxsize={50} />
|
||||
<button onClick={this._saveBuild} disabled={!buildName || savedCode && code == savedCode}>
|
||||
<FloppyDisk className='lg'/><span className='button-lbl'>{translate('save')}</span>
|
||||
<button onClick={this._saveBuild} disabled={!buildName || savedCode && code == savedCode} onMouseOver={tip.bind(null, 'save')} onMouseOut={hide}>
|
||||
<FloppyDisk className='lg' />
|
||||
</button>
|
||||
<button onClick={this._reloadBuild} disabled={!savedCode || code == savedCode}>
|
||||
<Reload className='lg'/><span className='button-lbl' >{translate('reload')}</span>
|
||||
<button onClick={this._reloadBuild} disabled={!savedCode || code == savedCode} onMouseOver={tip.bind(null, 'reload')} onMouseOut={hide}>
|
||||
<Reload className='lg'/>
|
||||
</button>
|
||||
<button className={'danger'} onClick={this._deleteBuild} disabled={!savedCode}>
|
||||
<button className={'danger'} onClick={this._deleteBuild} disabled={!savedCode} onMouseOver={tip.bind(null, 'delete')} onMouseOut={hide}>
|
||||
<Bin className='lg'/>
|
||||
</button>
|
||||
<button onClick={this._resetBuild} disabled={!code}>
|
||||
<Switch className='lg'/><span className='button-lbl'>{translate('reset')}</span>
|
||||
<button onClick={this._resetBuild} disabled={!code} onMouseOver={tip.bind(null, 'reset')} onMouseOut={hide}>
|
||||
<Switch className='lg'/>
|
||||
</button>
|
||||
<button onClick={this._exportBuild} disabled={!buildName}>
|
||||
<Download className='lg'/><span className='button-lbl'>{translate('export')}</span>
|
||||
<button onClick={this._exportBuild} disabled={!buildName} onMouseOver={tip.bind(null, 'export')} onMouseOut={hide}>
|
||||
<Download className='lg'/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -261,12 +328,12 @@ export default class OutfittingPage extends Page {
|
||||
</div>
|
||||
|
||||
<div className='group half'>
|
||||
<table style={{ width: '100%', lineHeight: '1em'}}>
|
||||
<table style={{ width: '100%', lineHeight: '1em' }}>
|
||||
<tbody >
|
||||
<tr>
|
||||
<td style={{ verticalAlign: 'top', padding:0 }}><Fuel className='xl primary-disabled' /></td>
|
||||
<td><Slider axis={true} onChange={this._fuelChange} axisUnit={translate('T')} percent={state.fuelLevel} max={state.fuelCapacity} /></td>
|
||||
<td className='primary' style={{ width: '10em', verticalAlign: 'top', fontSize: '0.9em' }}>{formats.f2(state.fuelLevel * ship.fuelCapacity)}{units.T} {formats.pct1(state.fuelLevel)}</td>
|
||||
<td><Slider axis={true} onChange={this._fuelChange} axisUnit={translate('T')} percent={fuelLevel} max={fuelCapacity} /></td>
|
||||
<td className='primary' style={{ width: '10em', verticalAlign: 'top', fontSize: '0.9em' }}>{formats.f2(fuelLevel * fuelCapacity)}{units.T} {formats.pct1(fuelLevel)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import React from 'react';
|
||||
import shallowEqual from '../utils/shallowEqual';
|
||||
import { shallowEqual } from '../utils/UtilityFunctions';
|
||||
|
||||
/**
|
||||
* @class Abstract Page
|
||||
* Abstract/Base Page
|
||||
*/
|
||||
export default class Page extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
route: React.PropTypes.object.isRequired,
|
||||
language: React.PropTypes.object.isRequired,
|
||||
sizeRatio: React.PropTypes.number.isRequired
|
||||
sizeRatio: React.PropTypes.number.isRequired,
|
||||
openMenu: React.PropTypes.func.isRequired,
|
||||
closeMenu: React.PropTypes.func.isRequired,
|
||||
showModal: React.PropTypes.func.isRequired,
|
||||
hideModal: React.PropTypes.func.isRequired,
|
||||
tooltip: React.PropTypes.func.isRequired,
|
||||
termtip: React.PropTypes.func.isRequired,
|
||||
onWindowResize: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
@@ -32,19 +39,16 @@ export default class Page extends React.Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Translated components are 'pure' components that only render when
|
||||
* props, state, or context changes. This method performs a shallow comparison to
|
||||
* determine change.
|
||||
* Pages are 'pure' components that only render when props, state, or context changes.
|
||||
* This method performs a shallow comparison to determine change.
|
||||
*
|
||||
* @param {object} nextProps
|
||||
* @param {objec} nextState
|
||||
* @param {objec} nextContext
|
||||
* @return {boolean} True if props, state, or context has changed
|
||||
* @param {Object} np Next/Incoming properties
|
||||
* @param {Object} ns Next/Incoming state
|
||||
* @param {Object} nc Next/Incoming context
|
||||
* @return {Boolean} True if props, state, or context has changed
|
||||
*/
|
||||
shouldComponentUpdate(nextProps, nextState, nextContext) {
|
||||
return !shallowEqual(this.props, nextProps)
|
||||
|| !shallowEqual(this.state, nextState)
|
||||
|| !shallowEqual(this.context, nextContext)
|
||||
shouldComponentUpdate(np, ns, nc) {
|
||||
return !shallowEqual(this.props, np) || !shallowEqual(this.state, ns) || !shallowEqual(this.context, nc);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,22 +7,38 @@ import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import { SizeMap } from '../shipyard/Constants';
|
||||
import Link from '../components/Link';
|
||||
|
||||
/**
|
||||
* Counts the hardpoints by class/size
|
||||
* @param {Object} slot Hardpoint Slot model
|
||||
*/
|
||||
function countHp(slot) {
|
||||
this.hp[slot.maxClass]++;
|
||||
this.hpCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the internal slots and aggregated properties
|
||||
* @param {Object} slot Internal Slots
|
||||
*/
|
||||
function countInt(slot) {
|
||||
var crEligible = !slot.eligible || slot.eligible.cr;
|
||||
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').capacity : 0;
|
||||
this.maxCargo += crEligible ? ModuleUtils.findInternal('cr', slot.maxClass, 'E').cargo : 0;
|
||||
}
|
||||
|
||||
let cachedShipSummaries = null;
|
||||
|
||||
/**
|
||||
* The Shipyard summary page
|
||||
*/
|
||||
export default class ShipyardPage extends Page {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
@@ -43,8 +59,9 @@ export default class ShipyardPage extends Page {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort ships
|
||||
* @param {object} key Sort predicate
|
||||
* Update state with the specified sort predicates
|
||||
* @param {String} shipPredicate Sort predicate - property name
|
||||
* @param {number} shipPredicateIndex Sort predicate - property index
|
||||
*/
|
||||
_sortShips(shipPredicate, shipPredicateIndex) {
|
||||
let shipDesc = this.state.shipDesc;
|
||||
@@ -60,6 +77,12 @@ export default class ShipyardPage extends Page {
|
||||
this.setState({ shipPredicate, shipDesc, shipPredicateIndex });
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate Ship summary and aggregated properties
|
||||
* @param {String} shipId Ship Id
|
||||
* @param {Object} shipData Ship Default Data
|
||||
* @return {Object} Ship summary and aggregated properties
|
||||
*/
|
||||
_shipSummary(shipId, shipData) {
|
||||
let summary = {
|
||||
id: shipId,
|
||||
@@ -86,6 +109,15 @@ export default class ShipyardPage extends Page {
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the table row summary for the ship
|
||||
* @param {Object} s Ship summary
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Object} u Localized unit map
|
||||
* @param {Function} fInt Localized integer formatter
|
||||
* @param {Function} fRound Localized round formatter
|
||||
* @return {React.Component} Table Row
|
||||
*/
|
||||
_shipRowElement(s, translate, u, fInt, fRound) {
|
||||
return <tr key={s.id} className='highlight'>
|
||||
<td className='le'><Link href={'/outfit/' + s.id}>{s.name}</Link></td>
|
||||
@@ -114,11 +146,15 @@ export default class ShipyardPage extends Page {
|
||||
<td className={cn({ disabled: !s.int[6] })}>{s.int[6]}</td>
|
||||
<td className={cn({ disabled: !s.int[7] })}>{s.int[7]}</td>
|
||||
<td className='ri'>{fInt(s.hullMass)}{u.T}</td>
|
||||
<td className='ri'>{s.masslock}</td>
|
||||
<td>{s.masslock}</td>
|
||||
<td className='ri'>{fInt(s.retailCost)}{u.CR}</td>
|
||||
</tr>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Page
|
||||
* @return {React.Component} The page contents
|
||||
*/
|
||||
render() {
|
||||
let { translate, formats, units } = this.context.language;
|
||||
let fInt = formats.int;
|
||||
@@ -127,6 +163,8 @@ export default class ShipyardPage extends Page {
|
||||
let shipPredicate = this.state.shipPredicate;
|
||||
let shipPredicateIndex = this.state.shipPredicateIndex;
|
||||
let shipRows = [];
|
||||
let hide = this.context.tooltip.bind(null, null);
|
||||
let tip = this.context.termtip;
|
||||
let sortShips = (predicate, index) => this._sortShips.bind(this, predicate, index);
|
||||
|
||||
// Sort shipsOverview
|
||||
@@ -170,13 +208,13 @@ export default class ShipyardPage extends Page {
|
||||
<th rowSpan={2} className='sortable le' onClick={sortShips('name')}>{translate('ship')}</th>
|
||||
<th rowSpan={2} className='sortable' onClick={sortShips('manufacturer')}>{translate('manufacturer')}</th>
|
||||
<th rowSpan={2} className='sortable' onClick={sortShips('class')}>{translate('size')}</th>
|
||||
<th rowSpan={2} className='sortable' onClick={sortShips('agility')}>{translate('agility')}</th>
|
||||
<th rowSpan={2} className='sortable' onMouseEnter={tip.bind(null, 'maneuverability')} onMouseLeave={hide} onClick={sortShips('agility')}>{translate('mnv')}</th>
|
||||
<th colSpan={4}>{translate('base')}</th>
|
||||
<th colSpan={4}>{translate('max')}</th>
|
||||
<th colSpan={5} className='sortable' onClick={sortShips('hpCount')}>{translate('hardpoints')}</th>
|
||||
<th colSpan={8} className='sortable' onClick={sortShips('intCount')}>{translate('internal compartments')}</th>
|
||||
<th rowSpan={2} className='sortable' onClick={sortShips('hullMass')}>{translate('hull')}</th>
|
||||
<th rowSpan={2} className='sortable' onClick={sortShips('masslock')}>{translate('MLF')}</th>
|
||||
<th rowSpan={2} className='sortable' onMouseEnter={tip.bind(null, 'mass lock factor')} onMouseLeave={hide} onClick={sortShips('masslock')} >{translate('MLF')}</th>
|
||||
<th rowSpan={2} className='sortable' onClick={sortShips('retailCost')}>{translate('cost')}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
*
|
||||
* @param {number} mass Mass of a ship: laden, unlanden, partially laden, etc
|
||||
* @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass
|
||||
* @param {number} fuel Optional - The fuel consumed during the jump (must be less than the drives max fuel per jump)
|
||||
* @param {number} fuel Optional - The fuel consumed during the jump
|
||||
* @return {number} Distance in Light Years
|
||||
*/
|
||||
export function jumpRange(mass, fsd, fuel) {
|
||||
return Math.pow(Math.min(fuel === undefined ? fsd.maxfuel : fuel, fsd.maxfuel) / fsd.fuelmul, 1 / fsd.fuelpower ) * fsd.optmass / mass;
|
||||
return Math.pow(Math.min(fuel === undefined ? fsd.maxfuel : fuel, fsd.maxfuel) / fsd.fuelmul, 1 / fsd.fuelpower) * fsd.optmass / mass;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,15 +20,15 @@ export function jumpRange(mass, fsd, fuel) {
|
||||
* @return {number} Distance in Light Years
|
||||
*/
|
||||
export function totalRange(mass, fsd, fuel) {
|
||||
var fuelRemaining = fuel % fsd.maxfuel; // Fuel left after making N max jumps
|
||||
var jumps = Math.floor(fuel / fsd.maxfuel);
|
||||
let fuelRemaining = fuel % fsd.maxfuel; // Fuel left after making N max jumps
|
||||
let jumps = Math.floor(fuel / fsd.maxfuel);
|
||||
mass += fuelRemaining;
|
||||
// Going backwards, start with the last jump using the remaining fuel
|
||||
var totalRange = fuelRemaining > 0 ? Math.pow(fuelRemaining / fsd.fuelmul, 1 / fsd.fuelpower ) * fsd.optmass / mass : 0;
|
||||
let totalRange = fuelRemaining > 0 ? Math.pow(fuelRemaining / fsd.fuelmul, 1 / fsd.fuelpower) * fsd.optmass / mass : 0;
|
||||
// For each max fuel jump, calculate the max jump range based on fuel mass left in the tank
|
||||
for (var j = 0; j < jumps; j++) {
|
||||
for (let j = 0; j < jumps; j++) {
|
||||
mass += fsd.maxfuel;
|
||||
totalRange += Math.pow(fsd.maxfuel / fsd.fuelmul, 1 / fsd.fuelpower ) * fsd.optmass / mass;
|
||||
totalRange += Math.pow(fsd.maxfuel / fsd.fuelmul, 1 / fsd.fuelpower) * fsd.optmass / mass;
|
||||
}
|
||||
return totalRange;
|
||||
};
|
||||
@@ -43,7 +43,7 @@ export function totalRange(mass, fsd, fuel) {
|
||||
* @return {number} Approximate shield strengh in MJ
|
||||
*/
|
||||
export function shieldStrength(mass, shields, sg, multiplier) {
|
||||
var opt;
|
||||
let opt;
|
||||
if (mass < sg.minmass) {
|
||||
return shields * multiplier * sg.minmul;
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export function shieldStrength(mass, shields, sg, multiplier) {
|
||||
} else {
|
||||
opt = (sg.optmass - mass) / (sg.maxmass - sg.optmass);
|
||||
opt = -1 + Math.pow(1 + opt, 2.425);
|
||||
return shields * multiplier * ( (-1 * opt * sg.maxmul) + ((1 + opt) * sg.optmul) );
|
||||
return shields * multiplier * ((-1 * opt * sg.maxmul) + ((1 + opt) * sg.optmul));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ export function shieldStrength(mass, shields, sg, multiplier) {
|
||||
* @return {object} Approximate speed by pips
|
||||
*/
|
||||
export function speed(mass, baseSpeed, baseBoost, thrusters, pipSpeed) {
|
||||
var multiplier = mass > thrusters.maxmass ? 0 : ((1 - thrusters.M) + (thrusters.M * Math.pow(3 - (2 * Math.max(0.5, mass / thrusters.optmass)), thrusters.P)));
|
||||
var speed = baseSpeed * multiplier;
|
||||
let multiplier = mass > thrusters.maxmass ? 0 : ((1 - thrusters.M) + (thrusters.M * Math.pow(3 - (2 * Math.max(0.5, mass / thrusters.optmass)), thrusters.P)));
|
||||
let speed = baseSpeed * multiplier;
|
||||
|
||||
return {
|
||||
'0 Pips': speed * (1 - (pipSpeed * 4)),
|
||||
|
||||
@@ -62,7 +62,7 @@ export const ModuleGroupToName = {
|
||||
let GrpNameToCodeMap = {};
|
||||
|
||||
for (let grp in ModuleGroupToName) {
|
||||
GrpNameToCodeMap[ModuleGroupToName[grp]] = grp;
|
||||
GrpNameToCodeMap[ModuleGroupToName[grp].toLowerCase()] = grp;
|
||||
}
|
||||
|
||||
export const ModuleNameToGroup = GrpNameToCodeMap;
|
||||
@@ -94,78 +94,89 @@ export const ShipFacets = [
|
||||
{ // 0
|
||||
title: 'agility',
|
||||
props: ['agility'],
|
||||
fmt: 'int'
|
||||
fmt: 'int',
|
||||
i: 0
|
||||
},
|
||||
{ // 1
|
||||
title: 'speed',
|
||||
props: ['topSpeed', 'topBoost'],
|
||||
lbls: ['thrusters', 'boost'],
|
||||
unit: 'm/s',
|
||||
fmt: 'int'
|
||||
fmt: 'int',
|
||||
i: 1
|
||||
},
|
||||
{ // 2
|
||||
title: 'armour',
|
||||
props: ['armour'],
|
||||
unit: '',
|
||||
fmt: 'int'
|
||||
fmt: 'int',
|
||||
i: 2
|
||||
},
|
||||
{ // 3
|
||||
title: 'shields',
|
||||
props: ['shieldStrength'],
|
||||
unit: 'MJ',
|
||||
fmt: 'round'
|
||||
fmt: 'int',
|
||||
i: 3
|
||||
},
|
||||
{ // 4
|
||||
title: 'jump range',
|
||||
props: ['unladenRange', 'fullTankRange', 'ladenRange'],
|
||||
lbls: ['max', 'full tank', 'laden'],
|
||||
unit: 'LY',
|
||||
fmt: 'round'
|
||||
fmt: 'round',
|
||||
i: 4
|
||||
},
|
||||
{ // 5
|
||||
title: 'mass',
|
||||
props: ['unladenMass', 'ladenMass'],
|
||||
lbls: ['unladen', 'laden'],
|
||||
unit: 'T',
|
||||
fmt: 'round'
|
||||
fmt: 'round',
|
||||
i: 5
|
||||
},
|
||||
{ // 6
|
||||
title: 'cargo',
|
||||
props: ['cargoCapacity'],
|
||||
unit: 'T',
|
||||
fmt: 'round'
|
||||
fmt: 'int',
|
||||
i: 6
|
||||
},
|
||||
{ // 7
|
||||
title: 'fuel',
|
||||
props: ['fuelCapacity'],
|
||||
unit: 'T',
|
||||
fmt: 'int'
|
||||
fmt: 'int',
|
||||
i: 7
|
||||
},
|
||||
{ // 8
|
||||
title: 'power',
|
||||
props: ['powerRetracted', 'powerDeployed', 'powerAvailable'],
|
||||
lbls: ['retracted', 'deployed', 'available'],
|
||||
unit: 'MW',
|
||||
fmt: 'f2'
|
||||
fmt: 'f2',
|
||||
i: 8
|
||||
},
|
||||
{ // 9
|
||||
title: 'cost',
|
||||
props: ['totalCost'],
|
||||
unit: 'CR',
|
||||
fmt: 'int'
|
||||
fmt: 'int',
|
||||
i: 9
|
||||
},
|
||||
{ // 10
|
||||
title: 'total range',
|
||||
props: ['unladenTotalRange', 'ladenTotalRange'],
|
||||
lbls: ['unladen', 'laden'],
|
||||
unit: 'LY',
|
||||
fmt: 'round'
|
||||
fmt: 'round',
|
||||
i: 10
|
||||
},
|
||||
{ // 11
|
||||
title: 'DPS',
|
||||
props: ['totalDps'],
|
||||
lbls: ['DPS'],
|
||||
fmt: 'round'
|
||||
fmt: 'round',
|
||||
i: 11
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
|
||||
|
||||
/**
|
||||
* Filter eligble modules based on parameters
|
||||
* @param {Array} arr Available modules array
|
||||
* @param {number} maxClass Max class
|
||||
* @param {number} minClass Minimum class
|
||||
* @param {number} mass Mass
|
||||
* @return {Array} Fitlered module subset
|
||||
*/
|
||||
function filter(arr, maxClass, minClass, mass) {
|
||||
return arr.filter(m => m.class <= maxClass && m.class >= minClass && (m.maxmass === undefined || mass <= m.maxmass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter eligble modules based on parameters
|
||||
* @param {Object} data Available modules object
|
||||
* @param {number} maxClass Max class
|
||||
* @param {number} minClass Minimum class
|
||||
* @param {number} mass Mass
|
||||
* @return {Array} Fitlered module subset
|
||||
*/
|
||||
function filterToArray(data, maxClass, minClass, mass) {
|
||||
let arr = [];
|
||||
|
||||
@@ -17,8 +32,19 @@ function filterToArray(data, maxClass, minClass, mass) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* The available module set for a specific ship
|
||||
*/
|
||||
export default class ModuleSet {
|
||||
|
||||
/**
|
||||
* Instantiate the module set
|
||||
* @param {Object} modules All Modules
|
||||
* @param {number} mass Ship mass
|
||||
* @param {Array} maxStandardArr Array of standard slots classes/sizes
|
||||
* @param {Array} maxInternal Array of internal slots classes/sizes
|
||||
* @param {Array} maxHardPoint Array of hardpoint slots classes/sizes
|
||||
*/
|
||||
constructor(modules, mass, maxStandardArr, maxInternal, maxHardPoint) {
|
||||
this.mass = mass;
|
||||
this.standard = {};
|
||||
@@ -80,12 +106,12 @@ export default class ModuleSet {
|
||||
* @return {object} A map of all eligible modules by group
|
||||
*/
|
||||
getHps(c, eligible) {
|
||||
var o = {};
|
||||
for (var key in this.hardpoints) {
|
||||
let o = {};
|
||||
for (let key in this.hardpoints) {
|
||||
if (eligible && !eligible[key]) {
|
||||
continue;
|
||||
}
|
||||
var data = filter(this.hardpoints[key], c, c ? 1 : 0, this.mass);
|
||||
let data = filter(this.hardpoints[key], c, c ? 1 : 0, this.mass);
|
||||
if (data.length) { // If group is not empty
|
||||
o[key] = data;
|
||||
}
|
||||
@@ -93,8 +119,14 @@ export default class ModuleSet {
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the lightest Power Distributor that provides sufficient
|
||||
* energy to boost.
|
||||
* @param {number} boostEnergy [description]
|
||||
* @return {Object} Power Distributor
|
||||
*/
|
||||
lightestPowerDist(boostEnergy) {
|
||||
var pd = this.standard[4][0];
|
||||
let pd = this.standard[4][0];
|
||||
|
||||
for (let p of this.standard[4]) {
|
||||
if (p.mass < pd.mass && p.enginecapacity >= boostEnergy) {
|
||||
@@ -104,8 +136,13 @@ export default class ModuleSet {
|
||||
return pd;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the lightest Thruster that can handle the specified tonnage
|
||||
* @param {number} ladenMass Ship laden mass (mass + cargo + fuel)
|
||||
* @return {Object} Thruster
|
||||
*/
|
||||
lightestThruster(ladenMass) {
|
||||
var th = this.standard[1][0];
|
||||
let th = this.standard[1][0];
|
||||
|
||||
for (let t of this.standard[1]) {
|
||||
if (t.mass < th.mass && t.maxmass >= ladenMass) {
|
||||
@@ -115,8 +152,13 @@ export default class ModuleSet {
|
||||
return th;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the lightest usable Shield Generator
|
||||
* @param {number} hullMass Ship hull mass
|
||||
* @return {Object} Thruster
|
||||
*/
|
||||
lightestShieldGenerator(hullMass) {
|
||||
var sg = this.internal.sg[0];
|
||||
let sg = this.internal.sg[0];
|
||||
|
||||
for (let s of this.internal.sg) {
|
||||
if (s.mass < sg.mass && s.minmass <= hullMass && s.maxmass > hullMass) {
|
||||
@@ -126,8 +168,13 @@ export default class ModuleSet {
|
||||
return sg;
|
||||
};
|
||||
|
||||
lightestPowerPlant(powerNeeded, rating) {
|
||||
var pp = this.standard[0][0];
|
||||
/**
|
||||
* Find the lightest Power Plant that provides sufficient power
|
||||
* @param {number} powerNeeded Power requirements in MJ
|
||||
* @return {Object} Power Plant
|
||||
*/
|
||||
lightestPowerPlant(powerNeeded) {
|
||||
let pp = this.standard[0][0];
|
||||
|
||||
for (let p of this.standard[0]) {
|
||||
// Provides enough power, is lighter or the same mass as current power plant but better output/efficiency
|
||||
|
||||
@@ -2,17 +2,27 @@ import { ModuleNameToGroup, BulkheadNames } from './Constants';
|
||||
import ModuleSet from './ModuleSet';
|
||||
import { Ships, Modules } from 'coriolis-data';
|
||||
|
||||
/**
|
||||
* Created a cargo hatch model
|
||||
* @return {Object} Cargo hatch model
|
||||
*/
|
||||
export function cargoHatch() {
|
||||
return { name: 'Cargo Hatch', class: 1, rating: 'H', power: 0.6 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the standard module type with the specified ID
|
||||
* @param {number} typeIndex Standard Module Type (0 - Power Plant, 1 - Thrusters, etc)
|
||||
* @param {string} id The module ID or '[Class][Rating]'
|
||||
* @return {Object} The standard module or null
|
||||
*/
|
||||
export function standard(typeIndex, id) {
|
||||
let standard = Modules.standard[typeIndex];
|
||||
if (standard[id]) {
|
||||
return standard[id];
|
||||
} else {
|
||||
for (let k in standard) {
|
||||
if (standard[k].id == id){
|
||||
if (standard[k].id == id) {
|
||||
return standard[k];
|
||||
}
|
||||
}
|
||||
@@ -20,6 +30,11 @@ export function standard(typeIndex, id) {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the hardpoint with the specified ID
|
||||
* @param {string} id Hardpoint ID
|
||||
* @return {Object} Hardpoint module or null
|
||||
*/
|
||||
export function hardpoints(id) {
|
||||
for (let n in Modules.hardpoints) {
|
||||
let group = Modules.hardpoints[n];
|
||||
@@ -32,6 +47,11 @@ export function hardpoints(id) {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the internal module with the specified ID
|
||||
* @param {string} id Internal module ID
|
||||
* @return {Object} Internal module or null
|
||||
*/
|
||||
export function internal(id) {
|
||||
for (let n in Modules.internal) {
|
||||
let group = Modules.internal[n];
|
||||
@@ -45,14 +65,14 @@ export function internal(id) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds an internal Component based on Class, Rating, Group and/or name.
|
||||
* At least one ofGroup name or unique component name must be provided
|
||||
* Finds an internal module based on Class, Rating, Group and/or name.
|
||||
* At least one ofGroup name or unique module name must be provided
|
||||
*
|
||||
* @param {string} groupName [Optional] Full name or abbreviated name for component group
|
||||
* @param {integer} clss Component Class
|
||||
* @param {string} rating Component Rating
|
||||
* @param {string} name [Optional] Long/unique name for component -e.g. 'Advanced Discover Scanner'
|
||||
* @return {String} The id of the component if found, null if not found
|
||||
* @param {string} groupName [Optional] Full name or abbreviated name for module group
|
||||
* @param {integer} clss module Class
|
||||
* @param {string} rating module Rating
|
||||
* @param {string} name [Optional] Long/unique name for module -e.g. 'Advanced Discover Scanner'
|
||||
* @return {String} The id of the module if found, null if not found
|
||||
*/
|
||||
export function findInternal(groupName, clss, rating, name) {
|
||||
let groups = {};
|
||||
@@ -61,7 +81,7 @@ export function findInternal(groupName, clss, rating, name) {
|
||||
if (Modules.internal[groupName]) {
|
||||
groups[groupName] = Modules.internal[groupName];
|
||||
} else {
|
||||
let grpCode = ModuleNameToGroup[groupName];
|
||||
let grpCode = ModuleNameToGroup[groupName.toLowerCase()];
|
||||
if (grpCode && Modules.internal[grpCode]) {
|
||||
groups[grpCode] = Modules.internal[grpCode];
|
||||
}
|
||||
@@ -83,14 +103,14 @@ export function findInternal(groupName, clss, rating, name) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an internal Component ID based on Class, Rating, Group and/or name.
|
||||
* At least one ofGroup name or unique component name must be provided
|
||||
* Finds an internal Module ID based on Class, Rating, Group and/or name.
|
||||
* At least one ofGroup name or unique module name must be provided
|
||||
*
|
||||
* @param {string} groupName [Optional] Full name or abbreviated name for component group
|
||||
* @param {integer} clss Component Class
|
||||
* @param {string} rating Component Rating
|
||||
* @param {string} name [Optional] Long/unique name for component -e.g. 'Advanced Discover Scanner'
|
||||
* @return {String} The id of the component if found, null if not found
|
||||
* @param {string} groupName [Optional] Full name or abbreviated name for module group
|
||||
* @param {integer} clss module Class
|
||||
* @param {string} rating Module Rating
|
||||
* @param {string} name [Optional] Long/unique name for module -e.g. 'Advanced Discover Scanner'
|
||||
* @return {String} The id of the module if found, null if not found
|
||||
*/
|
||||
export function findInternalId(groupName, clss, rating, name) {
|
||||
let i = this.findInternal(groupName, clss, rating, name);
|
||||
@@ -98,16 +118,16 @@ export function findInternalId(groupName, clss, rating, name) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a hardpoint Component based on Class, Rating, Group and/or name.
|
||||
* At least one ofGroup name or unique component name must be provided
|
||||
* Finds a hardpoint Module based on Class, Rating, Group and/or name.
|
||||
* At least one ofGroup name or unique module name must be provided
|
||||
*
|
||||
* @param {string} groupName [Optional] Full name or abbreviated name for component group
|
||||
* @param {integer} clss Component Class
|
||||
* @param {string} rating [Optional] Component Rating
|
||||
* @param {string} name [Optional] Long/unique name for component -e.g. 'Heat Sink Launcher'
|
||||
* @param {string} groupName [Optional] Full name or abbreviated name for module group
|
||||
* @param {integer} clss Module Class
|
||||
* @param {string} rating [Optional] module Rating
|
||||
* @param {string} name [Optional] Long/unique name for module -e.g. 'Heat Sink Launcher'
|
||||
* @param {string} mount Mount type - [F]ixed, [G]imballed, [T]urret
|
||||
* @param {string} missile [Optional] Missile type - [D]umbfire, [S]eeker
|
||||
* @return {String} The id of the component if found, null if not found
|
||||
* @return {String} The id of the module if found, null if not found
|
||||
*/
|
||||
export function findHardpoint(groupName, clss, rating, name, mount, missile) {
|
||||
let groups = {};
|
||||
@@ -116,7 +136,7 @@ export function findHardpoint(groupName, clss, rating, name, mount, missile) {
|
||||
if (Modules.hardpoints[groupName]) {
|
||||
groups[groupName] = Modules.hardpoints[groupName];
|
||||
} else {
|
||||
let grpCode = ModuleNameToGroup[groupName];
|
||||
let grpCode = ModuleNameToGroup[groupName.toLowerCase()];
|
||||
if (grpCode && Modules.hardpoints[grpCode]) {
|
||||
groups[grpCode] = Modules.hardpoints[grpCode];
|
||||
}
|
||||
@@ -127,12 +147,9 @@ export function findHardpoint(groupName, clss, rating, name, mount, missile) {
|
||||
|
||||
for (let g in groups) {
|
||||
let group = groups[g];
|
||||
for (let i = 0, l = group.length; i < l; i++) {
|
||||
if (group[i].class == clss && (!rating || group[i].rating == rating) && group[i].mount == mount
|
||||
&& ((!name && !group[i].name) || group[i].name == name)
|
||||
&& ((!missile && !group[i].missile) || group[i].missile == missile)
|
||||
) {
|
||||
return group[i];
|
||||
for (let h of group) {
|
||||
if (h.class == clss && (!rating || h.rating == rating) && h.mount == mount && h.name == name && h.missile == missile) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,16 +158,16 @@ export function findHardpoint(groupName, clss, rating, name, mount, missile) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a hardpoint Component ID based on Class, Rating, Group and/or name.
|
||||
* At least one of Group name or unique component name must be provided
|
||||
* Finds a hardpoint module ID based on Class, Rating, Group and/or name.
|
||||
* At least one of Group name or unique module name must be provided
|
||||
*
|
||||
* @param {string} groupName [Optional] Full name or abbreviated name for component group
|
||||
* @param {integer} clss Component Class
|
||||
* @param {string} rating Component Rating
|
||||
* @param {string} name [Optional] Long/unique name for component -e.g. 'Heat Sink Launcher'
|
||||
* @param {string} groupName [Optional] Full name or abbreviated name for module group
|
||||
* @param {integer} clss module Class
|
||||
* @param {string} rating module Rating
|
||||
* @param {string} name [Optional] Long/unique name for module -e.g. 'Heat Sink Launcher'
|
||||
* @param {string} mount Mount type - [F]ixed, [G]imballed, [T]urret
|
||||
* @param {string} missile [Optional] Missile type - [D]umbfire, [S]eeker
|
||||
* @return {String} The id of the component if found, null if not found
|
||||
* @return {String} The id of the module if found, null if not found
|
||||
*/
|
||||
export function findHardpointId(groupName, clss, rating, name, mount, missile) {
|
||||
let h = this.findHardpoint(groupName, clss, rating, name, mount, missile);
|
||||
@@ -159,33 +176,44 @@ export function findHardpointId(groupName, clss, rating, name, mount, missile) {
|
||||
|
||||
/**
|
||||
* Looks up the bulkhead module for a specific ship and bulkhead
|
||||
* @param {string} shipId Unique ship Id/Key
|
||||
* @param {string|number} bulkheadsId Id/Index for the specified bulkhead
|
||||
* @return {object} The bulkhead component object
|
||||
* @param {string} shipId Unique ship Id/Key
|
||||
* @param {string|number} index Index for the specified bulkhead
|
||||
* @return {Object} The bulkhead module object
|
||||
*/
|
||||
export function bulkheads(shipId, index) {
|
||||
let bulkhead = Ships[shipId].bulkheads[index];
|
||||
bulkhead.class = 1;
|
||||
bulkhead.rating = 'I';
|
||||
bulkhead.name = BulkheadNames[index]
|
||||
bulkhead.name = BulkheadNames[index];
|
||||
|
||||
return bulkhead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bulkhead index for the given bulkhead name
|
||||
* @param {string} bulkheadName Bulkhead name in english
|
||||
* @return {number} Bulkhead index
|
||||
*/
|
||||
export function bulkheadIndex(bulkheadName) {
|
||||
return BulkheadNames.indexOf(bulkheadName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if a module group is a shield generator
|
||||
* @param {string} g Module Group name
|
||||
* @return {Boolean} True if the group is a shield generator
|
||||
*/
|
||||
export function isShieldGenerator(g) {
|
||||
return g == 'sg' || g == 'psg' || g == 'bsg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ModuleSet that contains all available components
|
||||
* Creates a new ModuleSet that contains all available modules
|
||||
* that the specified ship is eligible to use.
|
||||
*
|
||||
* @param {string} shipId Unique ship Id/Key
|
||||
* @return {ModuleSet} The set of components the ship can install
|
||||
* @return {ModuleSet} The set of modules the ship can install
|
||||
*/
|
||||
export function forShip(shipId) {
|
||||
let ship = Ships[shipId];
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { ModuleGroupToName, MountMap, BulkheadNames } from './Constants';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import Ship from './Ship';
|
||||
import ModuleUtils from './ModuleUtils';
|
||||
import * as ModuleUtils from './ModuleUtils';
|
||||
import LZString from 'lz-string';
|
||||
|
||||
/**
|
||||
* Service managing seralization and deserialization of models for use in URLs and persistene.
|
||||
*/
|
||||
const STANDARD = ['powerPlant', 'thrusters', 'frameShiftDrive', 'lifeSupport', 'powerDistributor', 'sensors', 'fuelTank'];
|
||||
|
||||
/**
|
||||
* Generates ship-loadout JSON Schema slot object
|
||||
* @param {Object} slot Slot model
|
||||
* @return {Object} JSON Schema Slot
|
||||
*/
|
||||
function slotToSchema(slot) {
|
||||
if (slot.m) {
|
||||
let o = {
|
||||
@@ -32,19 +35,26 @@ function slotToSchema(slot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function toDetailedBuild(buildName, ship, code) {
|
||||
var standard = ship.standard,
|
||||
/**
|
||||
* Generates an object conforming to the ship-loadout JSON schema from a Ship model
|
||||
* @param {string} buildName The build name
|
||||
* @param {Ship} ship Ship instance
|
||||
* @return {Object} ship-loadout object
|
||||
*/
|
||||
export function toDetailedBuild(buildName, ship) {
|
||||
let standard = ship.standard,
|
||||
hardpoints = ship.hardpoints,
|
||||
internal = ship.internal;
|
||||
internal = ship.internal,
|
||||
code = ship.toString();
|
||||
|
||||
var data = {
|
||||
let data = {
|
||||
$schema: 'http://cdn.coriolis.io/schemas/ship-loadout/3.json#',
|
||||
name: buildName,
|
||||
ship: ship.name,
|
||||
references: [{
|
||||
name: 'Coriolis.io',
|
||||
url: `http://coriolis.io/outfit/${ship.id}/${code}?bn=${encodeURIComponent(buildName)}`,
|
||||
code: code,
|
||||
url: `https://coriolis.io/outfit/${ship.id}/${code}?bn=${encodeURIComponent(buildName)}`,
|
||||
code,
|
||||
shipId: ship.id
|
||||
}],
|
||||
components: {
|
||||
@@ -66,7 +76,7 @@ export function toDetailedBuild(buildName, ship, code) {
|
||||
stats: {}
|
||||
};
|
||||
|
||||
for (var stat in ship) {
|
||||
for (let stat in ship) {
|
||||
if (!isNaN(ship[stat])) {
|
||||
data.stats[stat] = Math.round(ship[stat] * 100) / 100;
|
||||
}
|
||||
@@ -75,66 +85,75 @@ export function toDetailedBuild(buildName, ship, code) {
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiates a ship from a ship-loadout object
|
||||
* @param {Object} detailedBuild ship-loadout object
|
||||
* @return {Ship} Ship instance
|
||||
*/
|
||||
export function fromDetailedBuild(detailedBuild) {
|
||||
var shipId = Object.keys(Ships).find((shipId) => Ships[shipId].properties.name.toLowerCase() == detailedBuild.ship.toLowerCase());
|
||||
let shipId = Object.keys(Ships).find((shipId) => Ships[shipId].properties.name.toLowerCase() == detailedBuild.ship.toLowerCase());
|
||||
|
||||
if (!shipId) {
|
||||
throw 'No such ship: ' + detailedBuild.ship;
|
||||
}
|
||||
|
||||
var comps = detailedBuild.components;
|
||||
var standard = comps.standard;
|
||||
var priorities = [ standard.cargoHatch && standard.cargoHatch.priority !== undefined ? standard.cargoHatch.priority - 1 : 0 ];
|
||||
var enabled = [ standard.cargoHatch && standard.cargoHatch.enabled !== undefined ? standard.cargoHatch.enabled : true ];
|
||||
var shipData = ShipsDB[shipId];
|
||||
var ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||
var bulkheads = ModuleUtils.bulkheadIndex(standard.bulkheads);
|
||||
let comps = detailedBuild.components;
|
||||
let stn = comps.standard;
|
||||
let priorities = [stn.cargoHatch && stn.cargoHatch.priority !== undefined ? stn.cargoHatch.priority - 1 : 0];
|
||||
let enabled = [stn.cargoHatch && stn.cargoHatch.enabled !== undefined ? stn.cargoHatch.enabled : true];
|
||||
let shipData = Ships[shipId];
|
||||
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||
let bulkheads = ModuleUtils.bulkheadIndex(stn.bulkheads);
|
||||
|
||||
if (bulkheads < 0) {
|
||||
throw 'Invalid bulkheads: ' + standard.bulkheads;
|
||||
throw 'Invalid bulkheads: ' + stn.bulkheads;
|
||||
}
|
||||
|
||||
var standardIds = _.map(
|
||||
['powerPlant', 'thrusters', 'frameShiftDrive', 'lifeSupport', 'powerDistributor', 'sensors', 'fuelTank'],
|
||||
function(c) {
|
||||
if (!standard[c].class || !standard[c].rating) {
|
||||
throw 'Invalid value for ' + c;
|
||||
}
|
||||
priorities.push(standard[c].priority === undefined ? 0 : standard[c].priority - 1);
|
||||
enabled.push(standard[c].enabled === undefined ? true : standard[c].enabled);
|
||||
return standard[c].class + standard[c].rating;
|
||||
let standard = STANDARD.map((c) => {
|
||||
if (!stn[c].class || !stn[c].rating) {
|
||||
throw 'Invalid value for ' + c;
|
||||
}
|
||||
);
|
||||
priorities.push(stn[c].priority === undefined ? 0 : stn[c].priority - 1);
|
||||
enabled.push(stn[c].enabled === undefined ? true : stn[c].enabled);
|
||||
return stn[c].class + stn[c].rating;
|
||||
});
|
||||
|
||||
var internal = _.map(comps.internal, function(c) { return c ? ModuleUtils.findInternalId(c.group, c.class, c.rating, c.name) : 0; });
|
||||
let internal = comps.internal.map(c => c ? ModuleUtils.findInternalId(c.group, c.class, c.rating, c.name) : 0);
|
||||
|
||||
var hardpoints = _.map(comps.hardpoints, function(c) {
|
||||
return c ? ModuleUtils.findHardpointId(c.group, c.class, c.rating, c.name, MountMap[c.mount], c.missile) : 0;
|
||||
}).concat(_.map(comps.utility, function(c) {
|
||||
return c ? ModuleUtils.findHardpointId(c.group, c.class, c.rating, c.name, MountMap[c.mount]) : 0;
|
||||
}));
|
||||
let hardpoints = comps.hardpoints
|
||||
.map(c => c ? ModuleUtils.findHardpointId(c.group, c.class, c.rating, c.name, MountMap[c.mount], c.missile) : 0)
|
||||
.concat(comps.utility.map(c => c ? ModuleUtils.findHardpointId(c.group, c.class, c.rating, c.name, MountMap[c.mount]) : 0));
|
||||
|
||||
// The ordering of these arrays must match the order in which they are read in Ship.buildWith
|
||||
priorities = priorities.concat(_.map(comps.hardpoints, function(c) { return (!c || c.priority === undefined) ? 0 : c.priority - 1; }),
|
||||
_.map(comps.utility, function(c) { return (!c || c.priority === undefined) ? 0 : c.priority - 1; }),
|
||||
_.map(comps.internal, function(c) { return (!c || c.priority === undefined) ? 0 : c.priority - 1; }));
|
||||
enabled = enabled.concat(_.map(comps.hardpoints, function(c) { return (!c || c.enabled === undefined) ? true : c.enabled * 1; }),
|
||||
_.map(comps.utility, function(c) { return (!c || c.enabled === undefined) ? true : c.enabled * 1; }),
|
||||
_.map(comps.internal, function(c) { return (!c || c.enabled === undefined) ? true : c.enabled * 1; }));
|
||||
priorities = priorities.concat(
|
||||
comps.hardpoints.map(c => (!c || c.priority === undefined) ? 0 : c.priority - 1),
|
||||
comps.utility.map(c => (!c || c.priority === undefined) ? 0 : c.priority - 1),
|
||||
comps.internal.map(c => (!c || c.priority === undefined) ? 0 : c.priority - 1)
|
||||
);
|
||||
enabled = enabled.concat(
|
||||
comps.hardpoints.map(c => (!c || c.enabled === undefined) ? true : c.enabled * 1),
|
||||
comps.utility.map(c => (!c || c.enabled === undefined) ? true : c.enabled * 1),
|
||||
comps.internal.map(c => (!c || c.enabled === undefined) ? true : c.enabled * 1)
|
||||
);
|
||||
|
||||
ship.buildWith({ bulkheads: bulkheads, standard: standardIds, hardpoints: hardpoints, internal: internal }, priorities, enabled);
|
||||
ship.buildWith({ bulkheads, standard, hardpoints, internal }, priorities, enabled);
|
||||
|
||||
return ship;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates an array of ship-loadout JSON Schema object for export
|
||||
* @param {Array} builds Array of ship builds
|
||||
* @return {Array} Array of of ship-loadout objects
|
||||
*/
|
||||
export function toDetailedExport(builds) {
|
||||
var data = [];
|
||||
let data = [];
|
||||
|
||||
for (var shipId in builds) {
|
||||
for (var buildName in builds[shipId]) {
|
||||
var code = builds[shipId][buildName];
|
||||
var shipData = Ships[shipId];
|
||||
var ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||
for (let shipId in builds) {
|
||||
for (let buildName in builds[shipId]) {
|
||||
let code = builds[shipId][buildName];
|
||||
let shipData = Ships[shipId];
|
||||
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||
ship.buildFrom(code);
|
||||
data.push(toDetailedBuild(buildName, ship, code));
|
||||
}
|
||||
@@ -142,22 +161,31 @@ export function toDetailedExport(builds) {
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializes a comparion and all of the ships to zipped
|
||||
* Base 64 encoded JSON.
|
||||
* @param {string} name Comparison name
|
||||
* @param {array} builds Array of ship builds
|
||||
* @param {array} facets Selected facets
|
||||
* @param {string} predicate sort predicate
|
||||
* @param {boolean} desc sort order
|
||||
* @return {string} Zipped Base 64 encoded JSON
|
||||
*/
|
||||
export function fromComparison(name, builds, facets, predicate, desc) {
|
||||
var shipBuilds = [];
|
||||
|
||||
builds.forEach(function(b) {
|
||||
shipBuilds.push({ s: b.id, n: b.buildName, c: fromShip(b) });
|
||||
}.bind(this));
|
||||
|
||||
return LZString.compressToBase64(angular.toJson({
|
||||
return LZString.compressToBase64(JSON.stringify({
|
||||
n: name,
|
||||
b: shipBuilds,
|
||||
b: builds.map((b) => { return { s: b.id, n: b.buildName, c: b.toString() }; }),
|
||||
f: facets,
|
||||
p: predicate,
|
||||
d: desc ? 1 : 0
|
||||
})).replace(/\//g, '-');
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the comarison data string back to an object.
|
||||
* @param {string} code Zipped Base 64 encoded JSON comparison data
|
||||
* @return {Object} Comparison data object
|
||||
*/
|
||||
export function toComparison(code) {
|
||||
return JSON.parse(LZString.decompressFromBase64(code.replace(/-/g, '/')));
|
||||
};
|
||||
|
||||
@@ -20,6 +20,14 @@ function powerUsageType(slot, modul) {
|
||||
return slot.cat != 1 ? 'retracted' : 'deployed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the category array with module IDs from
|
||||
* the provided code
|
||||
* @param {string} code Serialized ship code
|
||||
* @param {Array} arr Category array
|
||||
* @param {number} codePos Current position/Index of code string
|
||||
* @return {number} Next position/Index of code string
|
||||
*/
|
||||
function decodeToArray(code, arr, codePos) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (code.charAt(codePos) == '-') {
|
||||
@@ -46,7 +54,7 @@ function reduceToIDs(idArray, slot, slotIndex) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ship model used to track all ship ModuleUtils and properties.
|
||||
* Ship Model - Encapsulates and models in-game ship behavior
|
||||
*/
|
||||
export default class Ship {
|
||||
|
||||
@@ -89,7 +97,7 @@ export default class Ship {
|
||||
this.hardpoints
|
||||
);
|
||||
this.shipCostMultiplier = 1;
|
||||
this.modulCostMultiplier = 1;
|
||||
this.moduleCostMultiplier = 1;
|
||||
this.priorityBands = [
|
||||
{ deployed: 0, retracted: 0, },
|
||||
{ deployed: 0, retracted: 0, },
|
||||
@@ -99,23 +107,33 @@ export default class Ship {
|
||||
];
|
||||
}
|
||||
|
||||
//*********//
|
||||
// GETTERS //
|
||||
//*********//
|
||||
/* GETTERS */
|
||||
|
||||
/**
|
||||
* [getAvailableModules description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
getAvailableModules() {
|
||||
return this.availCS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the ship thrust/move
|
||||
* @return {[type]} True if thrusters operational
|
||||
*/
|
||||
canThrust() {
|
||||
return this.getSlotStatus(this.standard[1]) == 3 // Thrusters are powered
|
||||
&& this.ladenMass < this.standard[1].m.maxmass; // Max mass not exceeded
|
||||
return this.getSlotStatus(this.standard[1]) == 3 && // Thrusters are powered
|
||||
this.ladenMass < this.standard[1].m.maxmass; // Max mass not exceeded
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the ship boost
|
||||
* @return {[type]} True if boost capable
|
||||
*/
|
||||
canBoost() {
|
||||
return this.canThrust() // Thrusters operational
|
||||
&& this.getSlotStatus(this.standard[4]) == 3 // Power distributor operational
|
||||
&& this.boostEnergy <= this.standard[4].m.enginecapacity; // PD capacitor is sufficient for boost
|
||||
return this.canThrust() && // Thrusters operational
|
||||
this.getSlotStatus(this.standard[4]) == 3 && // Power distributor operational
|
||||
this.boostEnergy <= this.standard[4].m.enginecapacity; // PD capacitor is sufficient for boost
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +142,7 @@ export default class Ship {
|
||||
* 1 - Disabled (Switched off)
|
||||
* 2 - Offline (Insufficient power available)
|
||||
* 3 - Online
|
||||
* @param {[type]} slot [description]
|
||||
* @param {Object} slot Slot model
|
||||
* @param {boolean} deployed True - power used when hardpoints are deployed
|
||||
* @return {number} status index
|
||||
*/
|
||||
@@ -145,18 +163,54 @@ export default class Ship {
|
||||
/**
|
||||
* Calculate jump range using the installed FSD and the
|
||||
* specified mass which can be more or less than ships actual mass
|
||||
* @param {number} mass Mass in tons
|
||||
* @param {number} fuel Fuel available in tons
|
||||
* @return {number} Jump range in Light Years
|
||||
* @param {number} fuel Fuel available in tons
|
||||
* @param {number} cargo Cargo in tons
|
||||
* @return {number} Jump range in Light Years
|
||||
*/
|
||||
getJumpRangeWith(fuel, cargo) {
|
||||
return Calc.jumpRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the laden jump range based on a potential change in mass, fuel, or FSD
|
||||
* @param {number} massDelta Optional - Change in laden mass (mass + cargo + fuel)
|
||||
* @param {number} fuel Optional - Available fuel (defaults to max fuel based on FSD)
|
||||
* @param {Object} fsd Optional - Frame Shift Drive (or use mounted FSD)
|
||||
* @return {number} Jump range in Light Years
|
||||
*/
|
||||
getLadenRange(massDelta, fuel, fsd) {
|
||||
return Calc.jumpRange(this.ladenMass + (massDelta || 0), fsd || this.standard[2].m, fuel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unladen jump range based on a potential change in mass, fuel, or FSD
|
||||
* @param {number} massDelta Optional - Change in ship mass
|
||||
* @param {number} fuel Optional - Available fuel (defaults to lesser of fuel capacity or max fuel based on FSD)
|
||||
* @param {Object} fsd Optional - Frame Shift Drive (or use mounted FSD)
|
||||
* @return {number} Jump range in Light Years
|
||||
*/
|
||||
getUnladenRange(massDelta, fuel, fsd) {
|
||||
fsd = fsd || this.standard[2].m;
|
||||
return Calc.jumpRange(this.unladenMass + (massDelta || 0) + Math.min(fsd.maxfuel, fuel || this.fuelCapacity), fsd || this.standard[2].m, fuel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cumulative (total) jump range when making longest jumps using the installed FSD and the
|
||||
* specified mass which can be more or less than ships actual mass
|
||||
* @param {number} fuel Fuel available in tons
|
||||
* @param {number} cargo Cargo in tons
|
||||
* @return {number} Total/Cumulative Jump range in Light Years
|
||||
*/
|
||||
getFastestRangeWith(fuel, cargo) {
|
||||
return Calc.totalRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the top speeds at cargo and fuel tonnage
|
||||
* @param {number} fuel Fuel available in tons
|
||||
* @param {number} cargo Cargo in tons
|
||||
* @return {Object} Speed at pip settings and boost
|
||||
*/
|
||||
getSpeedsWith(fuel, cargo) {
|
||||
return Calc.speed(this.unladenMass + fuel + cargo, this.speed, this.boost, this.standard[1].m, this.pipSpeed);
|
||||
}
|
||||
@@ -168,7 +222,7 @@ export default class Ship {
|
||||
* @return {number} The index of the slot in ship.internal
|
||||
*/
|
||||
findInternalByGroup(group) {
|
||||
var index;
|
||||
let index;
|
||||
if (ModuleUtils.isShieldGenerator(group)) {
|
||||
return this.internal.find(slot => slot.m && ModuleUtils.isShieldGenerator(slot.m.grp));
|
||||
} else {
|
||||
@@ -176,6 +230,10 @@ export default class Ship {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the ship to a string
|
||||
* @return {string} Serialized ship 'code'
|
||||
*/
|
||||
toString() {
|
||||
return [
|
||||
this.getStandardString(),
|
||||
@@ -188,6 +246,10 @@ export default class Ship {
|
||||
].join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the standard modules to a string
|
||||
* @return {string} Serialized standard modules 'code'
|
||||
*/
|
||||
getStandardString() {
|
||||
if(!this.serialized.standard) {
|
||||
this.serialized.standard = this.bulkheads.index + this.standard.reduce((arr, slot, i) => {
|
||||
@@ -198,6 +260,10 @@ export default class Ship {
|
||||
return this.serialized.standard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the internal modules to a string
|
||||
* @return {string} Serialized internal modules 'code'
|
||||
*/
|
||||
getInternalString() {
|
||||
if(!this.serialized.internal) {
|
||||
this.serialized.internal = this.internal.reduce(reduceToIDs, new Array(this.internal.length)).join('');
|
||||
@@ -205,6 +271,10 @@ export default class Ship {
|
||||
return this.serialized.internal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the hardpoints and utility modules to a string
|
||||
* @return {string} Serialized hardpoints and utility modules 'code'
|
||||
*/
|
||||
getHardpointsString() {
|
||||
if(!this.serialized.hardpoints) {
|
||||
this.serialized.hardpoints = this.hardpoints.reduce(reduceToIDs, new Array(this.hardpoints.length)).join('');
|
||||
@@ -212,48 +282,58 @@ export default class Ship {
|
||||
return this.serialized.hardpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the serialized module active/inactive settings
|
||||
* @return {string} Serialized active/inactive settings
|
||||
*/
|
||||
getPowerEnabledString() {
|
||||
return this.serialized.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the serialized module priority settings
|
||||
* @return {string} Serialized priority settings
|
||||
*/
|
||||
getPowerPrioritesString() {
|
||||
return this.serialized.priorities;
|
||||
}
|
||||
|
||||
//**********************//
|
||||
// Mutate / Update Ship //
|
||||
//**********************//
|
||||
/* Mutate / Update Ship */
|
||||
|
||||
/**
|
||||
* Recalculate all item costs and total based on discounts.
|
||||
* @param {number} shipCostMultiplier Ship cost multiplier discount (e.g. 0.9 === 10% discount)
|
||||
* @param {number} modulCostMultiplier Module cost multiplier discount (e.g. 0.75 === 25% discount)
|
||||
* @param {number} moduleCostMultiplier Module cost multiplier discount (e.g. 0.75 === 25% discount)
|
||||
* @return {this} The current ship instance for chaining
|
||||
*/
|
||||
applyDiscounts(shipCostMultiplier, modulCostMultiplier) {
|
||||
var total = 0;
|
||||
var costList = this.costList;
|
||||
applyDiscounts(shipCostMultiplier, moduleCostMultiplier) {
|
||||
let total = 0;
|
||||
let costList = this.costList;
|
||||
|
||||
for (var i = 0, l = costList.length; i < l; i++) {
|
||||
var item = costList[i];
|
||||
for (let i = 0, l = costList.length; i < l; i++) {
|
||||
let item = costList[i];
|
||||
if (item.m && item.m.cost) {
|
||||
item.discountedCost = item.m.cost * (item.type == 'SHIP' ? shipCostMultiplier : modulCostMultiplier);
|
||||
item.discountedCost = item.m.cost * (item.type == 'SHIP' ? shipCostMultiplier : moduleCostMultiplier);
|
||||
if (item.incCost) {
|
||||
total += item.discountedCost;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.shipCostMultiplier = shipCostMultiplier;
|
||||
this.modulCostMultiplier = modulCostMultiplier;
|
||||
this.moduleCostMultiplier = moduleCostMultiplier;
|
||||
this.totalCost = total;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds/Updates the ship instance with the ModuleUtils[comps] passed in.
|
||||
* @param {object} comps Collection of ModuleUtils used to build the ship
|
||||
* @param {Object} comps Collection of ModuleUtils used to build the ship
|
||||
* @param {array} priorities Slot priorities
|
||||
* @param {Array} enabled Slot active/inactive
|
||||
* @return {this} The current ship instance for chaining
|
||||
*/
|
||||
buildWith(comps, priorities, enabled) {
|
||||
var internal = this.internal,
|
||||
let internal = this.internal,
|
||||
standard = this.standard,
|
||||
hps = this.hardpoints,
|
||||
bands = this.priorityBands,
|
||||
@@ -350,7 +430,7 @@ export default class Ship {
|
||||
* @param {string} serializedString The string to deserialize
|
||||
*/
|
||||
buildFrom(serializedString) {
|
||||
var standard = new Array(this.standard.length),
|
||||
let standard = new Array(this.standard.length),
|
||||
hardpoints = new Array(this.hardpoints.length),
|
||||
internal = new Array(this.internal.length),
|
||||
parts = serializedString.split('.'),
|
||||
@@ -371,31 +451,43 @@ export default class Ship {
|
||||
this.buildWith(
|
||||
{
|
||||
bulkheads: code.charAt(0) * 1,
|
||||
standard: standard,
|
||||
hardpoints: hardpoints,
|
||||
internal: internal
|
||||
standard,
|
||||
hardpoints,
|
||||
internal
|
||||
},
|
||||
priorities,
|
||||
enabled
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Empties all hardpoints and utility slots
|
||||
* @return {this} The current ship instance for chaining
|
||||
*/
|
||||
emptyHardpoints() {
|
||||
for (var i = this.hardpoints.length; i--; ) {
|
||||
for (let i = this.hardpoints.length; i--;) {
|
||||
this.use(this.hardpoints[i], null);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties all Internal slots
|
||||
* @return {this} The current ship instance for chaining
|
||||
*/
|
||||
emptyInternal() {
|
||||
for (var i = this.internal.length; i--; ) {
|
||||
for (let i = this.internal.length; i--;) {
|
||||
this.use(this.internal[i], null);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties all Utility slots
|
||||
* @return {this} The current ship instance for chaining
|
||||
*/
|
||||
emptyUtility() {
|
||||
for (var i = this.hardpoints.length; i--; ) {
|
||||
for (let i = this.hardpoints.length; i--;) {
|
||||
if (!this.hardpoints[i].maxClass) {
|
||||
this.use(this.hardpoints[i], null);
|
||||
}
|
||||
@@ -403,8 +495,12 @@ export default class Ship {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties all hardpoints
|
||||
* @return {this} The current ship instance for chaining
|
||||
*/
|
||||
emptyWeapons() {
|
||||
for (var i = this.hardpoints.length; i--; ) {
|
||||
for (let i = this.hardpoints.length; i--;) {
|
||||
if (this.hardpoints[i].maxClass) {
|
||||
this.use(this.hardpoints[i], null);
|
||||
}
|
||||
@@ -416,11 +512,18 @@ export default class Ship {
|
||||
* Optimize for the lower mass build that can still boost and power the ship
|
||||
* without power management.
|
||||
* @param {object} m Standard Module overrides
|
||||
* @return {this} The current ship instance for chaining
|
||||
*/
|
||||
optimizeMass(m) {
|
||||
return this.emptyHardpoints().emptyInternal().useLightestStandard(m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Include/Exclude a item/slot in cost calculations
|
||||
* @param {Object} item Slot or item
|
||||
* @param {Boolean} included Cost included
|
||||
* @return {this} The current ship instance for chaining
|
||||
*/
|
||||
setCostIncluded(item, included) {
|
||||
if (item.incCost != included && item.m) {
|
||||
this.totalCost += included ? item.discountedCost : -item.discountedCost;
|
||||
@@ -429,6 +532,12 @@ export default class Ship {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set slot active/inactive
|
||||
* @param {Object} slot Slot model
|
||||
* @param {Boolean} enabled True - active
|
||||
* @return {this} The current ship instance for chaining
|
||||
*/
|
||||
setSlotEnabled(slot, enabled) {
|
||||
if (slot.enabled != enabled) { // Enabled state is changing
|
||||
slot.enabled = enabled;
|
||||
@@ -459,11 +568,11 @@ export default class Ship {
|
||||
*/
|
||||
setSlotPriority(slot, newPriority) {
|
||||
if (newPriority >= 0 && newPriority < this.priorityBands.length) {
|
||||
var oldPriority = slot.priority;
|
||||
let oldPriority = slot.priority;
|
||||
slot.priority = newPriority;
|
||||
|
||||
if (slot.enabled) { // Only update power if the slot is enabled
|
||||
var usage = powerUsageType(slot, slot.m);
|
||||
let usage = powerUsageType(slot, slot.m);
|
||||
this.priorityBands[oldPriority][usage] -= slot.m.power;
|
||||
this.priorityBands[newPriority][usage] += slot.m.power;
|
||||
this.updatePowerPrioritesString();
|
||||
@@ -483,15 +592,15 @@ export default class Ship {
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
updateStats(slot, n, old, preventUpdate) {
|
||||
var powerChange = slot == this.standard[0];
|
||||
let powerChange = slot == this.standard[0];
|
||||
|
||||
if (old) { // Old modul now being removed
|
||||
switch (old.grp) {
|
||||
case 'ft':
|
||||
this.fuelCapacity -= old.capacity;
|
||||
this.fuelCapacity -= old.fuel;
|
||||
break;
|
||||
case 'cr':
|
||||
this.cargoCapacity -= old.capacity;
|
||||
this.cargoCapacity -= old.cargo;
|
||||
break;
|
||||
case 'hr':
|
||||
this.armourAdded -= old.armouradd;
|
||||
@@ -502,7 +611,7 @@ export default class Ship {
|
||||
}
|
||||
|
||||
if (slot.incCost && old.cost) {
|
||||
this.totalCost -= old.cost * this.modulCostMultiplier;
|
||||
this.totalCost -= old.cost * this.moduleCostMultiplier;
|
||||
}
|
||||
|
||||
if (old.power && slot.enabled) {
|
||||
@@ -519,10 +628,10 @@ export default class Ship {
|
||||
if (n) {
|
||||
switch (n.grp) {
|
||||
case 'ft':
|
||||
this.fuelCapacity += n.capacity;
|
||||
this.fuelCapacity += n.fuel;
|
||||
break;
|
||||
case 'cr':
|
||||
this.cargoCapacity += n.capacity;
|
||||
this.cargoCapacity += n.cargo;
|
||||
break;
|
||||
case 'hr':
|
||||
this.armourAdded += n.armouradd;
|
||||
@@ -533,7 +642,7 @@ export default class Ship {
|
||||
}
|
||||
|
||||
if (slot.incCost && n.cost) {
|
||||
this.totalCost += n.cost * this.modulCostMultiplier;
|
||||
this.totalCost += n.cost * this.moduleCostMultiplier;
|
||||
}
|
||||
|
||||
if (n.power && slot.enabled) {
|
||||
@@ -561,12 +670,16 @@ export default class Ship {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all power calculations
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
updatePower() {
|
||||
var bands = this.priorityBands;
|
||||
var prevRetracted = 0, prevDeployed = 0;
|
||||
let bands = this.priorityBands;
|
||||
let prevRetracted = 0, prevDeployed = 0;
|
||||
|
||||
for (var i = 0, l = bands.length; i < l; i++) {
|
||||
var band = bands[i];
|
||||
for (let i = 0, l = bands.length; i < l; i++) {
|
||||
let band = bands[i];
|
||||
prevRetracted = band.retractedSum = prevRetracted + band.retracted;
|
||||
prevDeployed = band.deployedSum = prevDeployed + band.deployed + band.retracted;
|
||||
}
|
||||
@@ -577,33 +690,47 @@ export default class Ship {
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update top speed and boost
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
updateTopSpeed() {
|
||||
var speeds = Calc.speed(this.unladenMass + this.fuelCapacity, this.speed, this.boost, this.standard[1].m, this.pipSpeed);
|
||||
let speeds = Calc.speed(this.unladenMass + this.fuelCapacity, this.speed, this.boost, this.standard[1].m, this.pipSpeed);
|
||||
this.topSpeed = speeds['4 Pips'];
|
||||
this.topBoost = speeds.boost;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Shield strength
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
updateShieldStrength() {
|
||||
var sgSlot = this.findInternalByGroup('sg'); // Find Shield Generator slot Index if any
|
||||
let sgSlot = this.findInternalByGroup('sg'); // Find Shield Generator slot Index if any
|
||||
this.shieldStrength = sgSlot && sgSlot.enabled ? Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, this.shieldMultiplier) : 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jump Range and total range calculations
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
updateJumpStats() {
|
||||
var fsd = this.standard[2].m; // Frame Shift Drive;
|
||||
this.unladenRange = Calc.jumpRange(this.unladenMass + fsd.maxfuel, fsd, this.fuelCapacity); // Include fuel weight for jump
|
||||
this.fullTankRange = Calc.jumpRange(this.unladenMass + this.fuelCapacity, fsd, this.fuelCapacity); // Full Tanke
|
||||
this.ladenRange = Calc.jumpRange(this.ladenMass, fsd, this.fuelCapacity);
|
||||
this.unladenTotalRange = Calc.totalRange(this.unladenMass, fsd, this.fuelCapacity);
|
||||
this.ladenTotalRange = Calc.totalRange(this.unladenMass + this.cargoCapacity, fsd, this.fuelCapacity);
|
||||
this.maxJumpCount = Math.ceil(this.fuelCapacity / fsd.maxfuel);
|
||||
let fsd = this.standard[2].m; // Frame Shift Drive;
|
||||
let { unladenMass, ladenMass, fuelCapacity } = this;
|
||||
this.unladenRange = this.getUnladenRange(); // Includes fuel weight for jump
|
||||
this.fullTankRange = Calc.jumpRange(unladenMass + fuelCapacity, fsd); // Full Tank
|
||||
this.ladenRange = this.getLadenRange(); // Includes full tank and caro
|
||||
this.unladenTotalRange = Calc.totalRange(unladenMass, fsd, fuelCapacity);
|
||||
this.ladenTotalRange = Calc.totalRange(unladenMass + this.cargoCapacity, fsd, fuelCapacity);
|
||||
this.maxJumpCount = Math.ceil(fuelCapacity / fsd.maxfuel);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the serialized power priorites string
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
updatePowerPrioritesString() {
|
||||
let priorities = [this.cargoHatch.priority];
|
||||
|
||||
@@ -621,6 +748,10 @@ export default class Ship {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the serialized power active/inactive string
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
updatePowerEnabledString() {
|
||||
let enabled = [this.cargoHatch.enabled ? 1 : 0];
|
||||
|
||||
@@ -642,17 +773,17 @@ export default class Ship {
|
||||
* Update a slot with a the modul if the id is different from the current id for this slot.
|
||||
* Has logic handling ModuleUtils that you may only have 1 of (Shield Generator or Refinery).
|
||||
*
|
||||
* @param {object} slot The modul slot
|
||||
* @param {string} id Unique ID for the selected module
|
||||
* @param {object} modul Properties for the selected module
|
||||
* @param {boolean} preventUpdate If true, do not update aggregated stats
|
||||
* @param {Object} slot The modul slot
|
||||
* @param {Object} m Properties for the selected module
|
||||
* @param {Boolean} preventUpdate If true, do not update aggregated stats
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
use(slot, m, preventUpdate) {
|
||||
if (slot.m != m) { // Selecting a different modul
|
||||
// Slot is an internal slot, is not being emptied, and the selected modul group/type must be of unique
|
||||
if (slot.cat == 2 && m && UNIQUE_MODULES.indexOf(m.grp) != -1) {
|
||||
// Find another internal slot that already has this type/group installed
|
||||
var similarSlot = this.findInternalByGroup(m.grp);
|
||||
let similarSlot = this.findInternalByGroup(m.grp);
|
||||
// If another slot has an installed modul with of the same type
|
||||
if (!preventUpdate && similarSlot && similarSlot !== slot) {
|
||||
this.updateStats(similarSlot, null, similarSlot.m);
|
||||
@@ -660,9 +791,9 @@ export default class Ship {
|
||||
similarSlot.discountedCost = 0;
|
||||
}
|
||||
}
|
||||
var oldModule = slot.m;
|
||||
let oldModule = slot.m;
|
||||
slot.m = m;
|
||||
slot.discountedCost = (m && m.cost) ? m.cost * this.modulCostMultiplier : 0;
|
||||
slot.discountedCost = (m && m.cost) ? m.cost * this.moduleCostMultiplier : 0;
|
||||
this.updateStats(slot, m, oldModule, preventUpdate);
|
||||
|
||||
switch (slot.cat) {
|
||||
@@ -675,16 +806,16 @@ export default class Ship {
|
||||
}
|
||||
|
||||
/**
|
||||
* [useBulkhead description]
|
||||
* @param {[type]} index [description]
|
||||
* @param {[type]} preventUpdate [description]
|
||||
* @return {[type]} [description]
|
||||
* Mount the specified bulkhead type (index)
|
||||
* @param {number} index Bulkhead index [0-4]
|
||||
* @param {Boolean} preventUpdate Prevent summary update
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
useBulkhead(index, preventUpdate) {
|
||||
var oldBulkhead = this.bulkheads.m;
|
||||
let oldBulkhead = this.bulkheads.m;
|
||||
this.bulkheads.index = index;
|
||||
this.bulkheads.m = ModuleUtils.bulkheads(this.id, index);
|
||||
this.bulkheads.discountedCost = this.bulkheads.m.cost * this.modulCostMultiplier;
|
||||
this.bulkheads.discountedCost = this.bulkheads.m.cost * this.moduleCostMultiplier;
|
||||
this.armourMultiplier = ArmourMultiplier[index];
|
||||
this.updateStats(this.bulkheads, this.bulkheads.m, oldBulkhead, preventUpdate);
|
||||
this.serialized.standard = null;
|
||||
@@ -692,13 +823,14 @@ export default class Ship {
|
||||
}
|
||||
|
||||
/**
|
||||
* [useStandard description]
|
||||
* @param {[type]} rating [description]
|
||||
* @return {[type]} [description]
|
||||
* Set all standard slots to use the speficied rating and class based on
|
||||
* the slot's max class
|
||||
* @param {string} rating Module Rating (A-E)
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
useStandard(rating) {
|
||||
for (var i = this.standard.length - 1; i--; ) { // All except Fuel Tank
|
||||
var id = this.standard[i].maxClass + rating;
|
||||
for (let i = this.standard.length - 1; i--;) { // All except Fuel Tank
|
||||
let id = this.standard[i].maxClass + rating;
|
||||
this.use(this.standard[i], ModuleUtils.standard(i, id));
|
||||
}
|
||||
return this;
|
||||
@@ -707,6 +839,7 @@ export default class Ship {
|
||||
/**
|
||||
* Use the lightest standard ModuleUtils unless otherwise specified
|
||||
* @param {object} m Module overrides
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
useLightestStandard(m) {
|
||||
m = m || {};
|
||||
@@ -747,9 +880,17 @@ export default class Ship {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill all utility slots with the specified module
|
||||
* @param {string} group Group name
|
||||
* @param {string} rating Rating [A-I]
|
||||
* @param {string} name Module name
|
||||
* @param {boolean} clobber Overwrite non-empty slots
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
useUtility(group, rating, name, clobber) {
|
||||
let m = ModuleUtils.findHardpoint(group, 0, rating, name);
|
||||
for (let i = this.hardpoints.length; i--; ) {
|
||||
for (let i = this.hardpoints.length; i--;) {
|
||||
if ((clobber || !this.hardpoints[i].m) && !this.hardpoints[i].maxClass) {
|
||||
this.use(this.hardpoints[i], m);
|
||||
}
|
||||
@@ -757,9 +898,17 @@ export default class Ship {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* [useWeapon description]
|
||||
* @param {[type]} group [description]
|
||||
* @param {[type]} mount [description]
|
||||
* @param {[type]} missile [description]
|
||||
* @param {boolean} clobber Overwrite non-empty slots
|
||||
* @return {this} The ship instance (for chaining operations)
|
||||
*/
|
||||
useWeapon(group, mount, missile, clobber) {
|
||||
let hps = this.hardpoints;
|
||||
for (let i = hps.length; i--; ) {
|
||||
for (let i = hps.length; i--;) {
|
||||
if (hps[i].maxClass) {
|
||||
let size = hps[i].maxClass, m;
|
||||
do {
|
||||
|
||||
@@ -8,6 +8,7 @@ const LS_KEY_INSURANCE = 'insurance';
|
||||
const LS_KEY_DISCOUNTS = 'discounts';
|
||||
const LS_KEY_STATE = 'state';
|
||||
const LS_KEY_SIZE_RATIO = 'sizeRatio';
|
||||
const LS_KEY_TOOLTIPS = 'tooltips';
|
||||
|
||||
let LS;
|
||||
|
||||
@@ -20,21 +21,40 @@ try {
|
||||
LS = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe localstorage put
|
||||
* @param {string} key key
|
||||
* @param {any} value data to store
|
||||
*/
|
||||
function _put(key, value) {
|
||||
if (LS) {
|
||||
LS.setItem(key, typeof value != 'string' ? JSON.stringify(value) : value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe localstorage get string
|
||||
* @param {string} key key
|
||||
* @return {string} The stored string
|
||||
*/
|
||||
function _getString(key) {
|
||||
return LS.getItem(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe localstorage get
|
||||
* @param {string} key key
|
||||
* @return {object | number} The stored data
|
||||
*/
|
||||
function _get(key) {
|
||||
let str = _getString(key);
|
||||
return str ? JSON.parse(str) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe localstorage delete
|
||||
* @param {string} key key
|
||||
*/
|
||||
function _delete(key) {
|
||||
if (LS) {
|
||||
LS.removeItem(key);
|
||||
@@ -43,36 +63,64 @@ function _delete(key) {
|
||||
|
||||
|
||||
/**
|
||||
* [description]
|
||||
* Persist store / service for all user settings. Currently uses localstorage only
|
||||
*/
|
||||
class Persist extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Create an instance
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
let buildJson = _get(LS_KEY_BUILDS);
|
||||
let comparisonJson = _get(LS_KEY_COMPARISONS);
|
||||
let tips = _get(LS_KEY_TOOLTIPS);
|
||||
|
||||
this.builds = buildJson ? buildJson : {};
|
||||
this.comparisons = comparisonJson ? comparisonJson : {};
|
||||
this.buildCount = Object.keys(this.builds).length;
|
||||
this.langCode = _getString(LS_KEY_LANG) || 'en';
|
||||
this.insurance = _getString(LS_KEY_INSURANCE);
|
||||
this.discounts = _get(LS_KEY_DISCOUNTS);
|
||||
this.discounts = _get(LS_KEY_DISCOUNTS) || [1, 1];
|
||||
this.costTab = _getString(LS_KEY_COST_TAB);
|
||||
this.state = _get(LS_KEY_STATE);
|
||||
this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1;
|
||||
this.tooltipsEnabled = tips === null ? true : tips;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current language code
|
||||
* @return {stirng} language code
|
||||
*/
|
||||
getLangCode() {
|
||||
return this.langCode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update and save the current language
|
||||
* @param {string} langCode language code
|
||||
*/
|
||||
setLangCode(langCode) {
|
||||
this.langCode = langCode;
|
||||
_put(LS_KEY_LANG, langCode);
|
||||
this.emit('language', langCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show tooltips setting
|
||||
* @param {boolean} show Optional - update setting
|
||||
* @return {boolean} True if tooltips should be shown
|
||||
*/
|
||||
showTooltips(show) {
|
||||
if (show !== undefined) {
|
||||
this.tooltipsEnabled = !!show;
|
||||
_put(LS_KEY_TOOLTIPS, this.tooltipsEnabled);
|
||||
this.emit('tooltips', this.tooltipsEnabled);
|
||||
}
|
||||
|
||||
return this.tooltipsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a ship build in local storage.
|
||||
*
|
||||
@@ -107,6 +155,11 @@ class Persist extends EventEmitter {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all builds (object) or builds for a specific ship (array)
|
||||
* @param {string} shipId Optional Ship Id
|
||||
* @return {Object | Array} Object if Ship Id is not provided
|
||||
*/
|
||||
getBuilds(shipId) {
|
||||
if(shipId && shipId.length > 0) {
|
||||
return this.builds[shipId];
|
||||
@@ -114,6 +167,11 @@ class Persist extends EventEmitter {
|
||||
return this.builds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of all builds names for a ship
|
||||
* @param {string} shipId Ship Id
|
||||
* @return {Array} Array of string or empty array
|
||||
*/
|
||||
getBuildsNamesFor(shipId) {
|
||||
if (this.builds[shipId]) {
|
||||
return Object.keys(this.builds[shipId]).sort();
|
||||
@@ -122,10 +180,20 @@ class Persist extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a build has been saved
|
||||
* @param {string} shipId Ship Id
|
||||
* @param {string} name Build name
|
||||
* @return {Boolean} True if the build exists
|
||||
*/
|
||||
hasBuild(shipId, name) {
|
||||
return this.builds[shipId] && this.builds[shipId][name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any builds have been saved
|
||||
* @return {Boolean} True if any builds have been saved
|
||||
*/
|
||||
hasBuilds() {
|
||||
return Object.keys(this.builds).length > 0;
|
||||
}
|
||||
@@ -145,9 +213,9 @@ class Persist extends EventEmitter {
|
||||
}
|
||||
_put(LS_KEY_BUILDS, this.builds);
|
||||
// Check if the build was used in existing comparisons
|
||||
var comps = this.comparisons;
|
||||
for (var c in comps) {
|
||||
for (var i = 0; i < comps[c].builds.length; i++) { // For all builds in the current comparison
|
||||
let comps = this.comparisons;
|
||||
for (let c in comps) {
|
||||
for (let i = 0; i < comps[c].builds.length; i++) { // For all builds in the current comparison
|
||||
if (comps[c].builds[i].shipId == shipId && comps[c].builds[i].buildName == name) {
|
||||
comps[c].builds.splice(i, 1);
|
||||
break; // A build is unique per comparison
|
||||
@@ -171,7 +239,7 @@ class Persist extends EventEmitter {
|
||||
this.comparisons[name] = {};
|
||||
}
|
||||
this.comparisons[name] = {
|
||||
facets: facets,
|
||||
facets,
|
||||
builds: builds.map(b => { return { shipId: b.id || b.shipId, buildName: b.buildName }; })
|
||||
};
|
||||
_put(LS_KEY_COMPARISONS, this.comparisons);
|
||||
@@ -190,14 +258,27 @@ class Persist extends EventEmitter {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all saved comparisons
|
||||
* @return {Object} All comparisons
|
||||
*/
|
||||
getComparisons() {
|
||||
return this.comparisons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comparison has been saved
|
||||
* @param {string} name Comparison name
|
||||
* @return {Boolean} True if a comparison has been saved
|
||||
*/
|
||||
hasComparison(name) {
|
||||
return !!this.comparisons[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any comparisons have been saved
|
||||
* @return {Boolean} True if any comparisons have been saved
|
||||
*/
|
||||
hasComparisons() {
|
||||
return Object.keys(this.comparisons).length > 0;
|
||||
}
|
||||
@@ -222,11 +303,15 @@ class Persist extends EventEmitter {
|
||||
this.comparisons = {};
|
||||
_delete(LS_KEY_BUILDS);
|
||||
_delete(LS_KEY_COMPARISONS);
|
||||
this.emit('deletedall');
|
||||
this.emit('deletedAll');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all saved data and settings
|
||||
* @return {Object} Data and settings
|
||||
*/
|
||||
getAll() {
|
||||
var data = {};
|
||||
let data = {};
|
||||
data[LS_KEY_BUILDS] = this.getBuilds();
|
||||
data[LS_KEY_COMPARISONS] = this.getComparisons();
|
||||
data[LS_KEY_INSURANCE] = this.getInsurance();
|
||||
@@ -244,17 +329,17 @@ class Persist extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Persist selected insurance type
|
||||
* @param {string} name Insurance type name
|
||||
* @param {string} insurance Insurance type name
|
||||
*/
|
||||
setInsurance(insurance) {
|
||||
this.insurance = insurance
|
||||
this.insurance = insurance;
|
||||
_put(LS_KEY_INSURANCE, insurance);
|
||||
this.emit('insurance', insurance);
|
||||
};
|
||||
|
||||
/**
|
||||
* Persist selected discount
|
||||
* @param {number} val Discount value/amount
|
||||
* Persist selected ship discount
|
||||
* @param {number} shipDiscount Discount value/amount
|
||||
*/
|
||||
setShipDiscount(shipDiscount) {
|
||||
this.discounts[0] = shipDiscount;
|
||||
@@ -271,8 +356,8 @@ class Persist extends EventEmitter {
|
||||
};
|
||||
|
||||
/**
|
||||
* Persist selected discount
|
||||
* @param {number} val Discount value/amount
|
||||
* Persist selected module discount
|
||||
* @param {number} moduleDiscount Discount value/amount
|
||||
*/
|
||||
setModuleDiscount(moduleDiscount) {
|
||||
this.discounts[1] = moduleDiscount;
|
||||
@@ -290,7 +375,7 @@ class Persist extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Persist selected cost tab
|
||||
* @param {number} val Discount value/amount
|
||||
* @param {number} tabName Cost tab name
|
||||
*/
|
||||
setCostTab(tabName) {
|
||||
this.costTab = tabName;
|
||||
@@ -310,7 +395,7 @@ class Persist extends EventEmitter {
|
||||
* @return {object} state State object containing state name and params
|
||||
*/
|
||||
getState() {
|
||||
return this.state;
|
||||
return this.state;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -332,7 +417,7 @@ class Persist extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Save the current size ratio to localstorage
|
||||
* @param {number} sizeRatio
|
||||
* @param {number} sizeRatio Size ratio scale
|
||||
*/
|
||||
setSizeRatio(sizeRatio) {
|
||||
if (sizeRatio != this.sizeRatio) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
/**
|
||||
* Generate a BBCode (Forum) compatible table from comparisons
|
||||
* @param {Function} translate Translate language function
|
||||
@@ -9,8 +7,8 @@
|
||||
* @param {string} link Link to the comparison
|
||||
* @return {string} the BBCode
|
||||
*/
|
||||
export default function comparisonBBCode(translate, formats, facets, builds, link) {
|
||||
var colCount = 2, b, i, j, k, f, fl, p, pl, l = [];
|
||||
export function comparisonBBCode(translate, formats, facets, builds, link) {
|
||||
let colCount = 2, b, i, j, k, f, fl, p, pl, l = [];
|
||||
|
||||
for (i = 0; i < facets.length; i++) {
|
||||
if (facets[i].active) {
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { EventEmitter } from 'fbemitter';
|
||||
|
||||
/**
|
||||
* Utility class to be used as a Singleton for handling common
|
||||
* interface events and operations
|
||||
*/
|
||||
class InterfaceEvents extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Binds the class methods
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.openMenu = this.openMenu.bind(this);
|
||||
this.closeMenu = this.closeMenu.bind(this);
|
||||
this.hideModal = this.hideModal.bind(this);
|
||||
this.showModal = this.showModal.bind(this);
|
||||
this.windowResized = this.windowResized.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* [openMenu description]
|
||||
* @param {[type]} menu [description]
|
||||
*/
|
||||
openMenu(menu) {
|
||||
this.emit('openMenu', menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the close menu event
|
||||
*/
|
||||
closeMenu() {
|
||||
this.emit('closeMenu');
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the hide modal event
|
||||
*/
|
||||
hideModal() {
|
||||
this.emit('hideModal');
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the show modal event the content/component passed
|
||||
* @param {React.Component} content React Component content
|
||||
*/
|
||||
showModal(content) {
|
||||
this.emit('showModal', content);
|
||||
}
|
||||
|
||||
windowResized() {
|
||||
// debounce/ throttle
|
||||
this.emit('windowResized');
|
||||
}
|
||||
}
|
||||
|
||||
export default new InterfaceEvents();
|
||||
|
||||
/**
|
||||
* Wraps the callback/context menu handler such that the default
|
||||
* operation can proceed if the SHIFT key is held while right-clicked
|
||||
* @param {Function} cb Callback for contextMenu
|
||||
* @return {Function} Wrapped contextmenu handler
|
||||
*/
|
||||
export function wrapCtxMenu(cb) {
|
||||
return (event) => {
|
||||
if (!event.getModifierState('Shift')) {
|
||||
event.preventDefault();
|
||||
cb.call(null, event);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -10,15 +10,19 @@ const SHORTEN_API = 'https://www.googleapis.com/urlshortener/v1/url?key=';
|
||||
*/
|
||||
export default function shortenUrl(url, success, error) {
|
||||
if (window.navigator.onLine) {
|
||||
request.post(SHORTEN_API + window.CORIOLIS_GAPI_KEY)
|
||||
.send({ longUrl: url })
|
||||
.end(function(err, response) {
|
||||
if (err) {
|
||||
error('Error');
|
||||
} else {
|
||||
success(response.data.id);
|
||||
}
|
||||
});
|
||||
try {
|
||||
request.post(SHORTEN_API + window.CORIOLIS_GAPI_KEY)
|
||||
.send({ longUrl: url })
|
||||
.end(function(err, response) {
|
||||
if (err) {
|
||||
error(response.statusText == 'OK' ? 'Bad Request' : response.statusText);
|
||||
} else {
|
||||
success(response.body.id);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
error(e.message ? e.message : e);
|
||||
}
|
||||
} else {
|
||||
error('Not Online');
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Infinite } from '../components/SvgIcons';
|
||||
|
||||
/**
|
||||
* Returns the translate name for the module mounted in the specified
|
||||
@@ -11,27 +14,191 @@ export function slotName(translate, slot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an internationalization friendly slot name comparator
|
||||
* @param {function} translate Tranlation function
|
||||
* @return {function} Comparator function for slot names
|
||||
* Slot name comparator
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Object} a Slot object
|
||||
* @param {Object} b Slot object
|
||||
* @return {number} 1, 0, -1
|
||||
*/
|
||||
export function nameComparator(translate) {
|
||||
return (a, b) => {
|
||||
a = a.m;
|
||||
b = b.m;
|
||||
export function nameComparator(translate, a, b) {
|
||||
return translate(a.name || a.grp).localeCompare(translate(b.name || b.grp));
|
||||
}
|
||||
|
||||
if (a && !b) {
|
||||
return 1;
|
||||
} else if (!a && b) {
|
||||
return -1;
|
||||
} else if (!a && !b) {
|
||||
return 0;
|
||||
} else 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;
|
||||
/**
|
||||
* Generates an internationalization friendly slot comparator that will
|
||||
* sort by specified property (if provided) then by name/group, class, rating
|
||||
* @param {function} translate Tranlation function
|
||||
* @param {function} propComparator Optional property comparator
|
||||
* @param {boolean} desc Use descending order
|
||||
* @return {function} Comparator function for slot names
|
||||
*/
|
||||
export function slotComparator(translate, propComparator, desc) {
|
||||
return (a, b) => {
|
||||
// retain descending order when sorting sorting by name/group/class/rating
|
||||
let am = a.m; // Slot A's mounted module
|
||||
let bm = b.m; // Slot B's mounted module
|
||||
|
||||
if (!desc) { // Flip A and B if ascending order
|
||||
let t = a;
|
||||
a = b;
|
||||
b = t;
|
||||
}
|
||||
return translate(a.name || a.grp).localeCompare(translate(b.name || b.grp));
|
||||
|
||||
// Check for empty slots first
|
||||
if (a.m && !b.m) {
|
||||
return 1;
|
||||
} else if (!a.m && b.m) {
|
||||
return -1;
|
||||
} else if (!a.m && !b.m) {
|
||||
return 0;
|
||||
}
|
||||
// If a property comparator is provided use it first
|
||||
let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a.m, b.m);
|
||||
|
||||
if (diff) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
// Property matches so sort by name / group, then class, rating
|
||||
if (am.name === bm.name && am.grp === bm.grp) {
|
||||
if(am.class == bm.class) {
|
||||
return am.rating > bm.rating ? 1 : -1;
|
||||
}
|
||||
return am.class - bm.class;
|
||||
}
|
||||
|
||||
return nameComparator(translate, am, bm);
|
||||
};
|
||||
}
|
||||
|
||||
const PROP_BLACKLIST = {
|
||||
eddbID: 1,
|
||||
'class': 1,
|
||||
id: 1,
|
||||
maxfuel: 1,
|
||||
fuelmul: 1,
|
||||
fuelpower: 1,
|
||||
optmass: 1,
|
||||
maxmass: 1,
|
||||
passive: 1,
|
||||
thermload: 1,
|
||||
ammocost: 1,
|
||||
activepower: 1,
|
||||
cooldown: 1,
|
||||
chargeup: 1,
|
||||
ssdam: 1,
|
||||
mjdps: 1,
|
||||
mjeps: 1,
|
||||
M: 1,
|
||||
P: 1,
|
||||
mass: 1,
|
||||
cost: 1
|
||||
};
|
||||
|
||||
const TERM_LOOKUP = {
|
||||
pGen: 'power',
|
||||
armouradd: 'armour',
|
||||
shieldmul: 'shield',
|
||||
rof: 'ROF',
|
||||
dps: 'DPS'
|
||||
};
|
||||
|
||||
const FORMAT_LOOKUP = {
|
||||
time: 'time',
|
||||
shieldmul: 'rPct'
|
||||
};
|
||||
|
||||
const UNIT_LOOKUP = {
|
||||
fuel: 'T',
|
||||
cargo: 'T',
|
||||
rate: 'kgs',
|
||||
range: 'km',
|
||||
recharge: 'MJ',
|
||||
rangeLS: 'Ls',
|
||||
power: 'MJ',
|
||||
pGen: 'MJ',
|
||||
rof: 'ps'
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the appropriate class based on diff value
|
||||
* @param {number} a Potential Module (cannot be null)
|
||||
* @param {number} b Currently mounted module (optional - null)
|
||||
* @param {boolean} negative A positive diff has a negative implication
|
||||
* @return {string} CSS Class name
|
||||
*/
|
||||
function diffClass(a, b, negative) {
|
||||
if (b === undefined || a === b) {
|
||||
return 'muted';
|
||||
} else if (a > b) {
|
||||
return negative ? 'warning' : 'secondary';
|
||||
}
|
||||
return negative ? 'secondary' : 'warning';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the displayable diff of a module's proprty
|
||||
* @param {Function} format Formatter
|
||||
* @param {number} mVal Potential Module property value
|
||||
* @param {number} mmVal Currently mounted module property value
|
||||
* @return {string | React.Component} Component to be rendered
|
||||
*/
|
||||
function diff(format, mVal, mmVal) {
|
||||
if (mVal == Infinity) {
|
||||
return <Infinite/>;
|
||||
} else {
|
||||
let diff = mVal - mmVal;
|
||||
if (!diff || diff == mVal || Math.abs(diff) == Infinity) {
|
||||
return format(mVal);
|
||||
}
|
||||
return `${format(mVal)} (${diff > 0 ? '+' : ''}${format(diff)})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a a summary and diff of the potential module
|
||||
* versus the currently mounted module if there is one
|
||||
* @param {Object} language Current language
|
||||
* @param {Object} m Potential Module (cannot be null)
|
||||
* @param {Object} mm Currently mounted module (optional - null)
|
||||
* @return {React.Component} Component to be rendered
|
||||
*/
|
||||
export function diffDetails(language, m, mm) {
|
||||
mm = mm || {};
|
||||
let { formats, translate, units } = language;
|
||||
let propDiffs = [];
|
||||
let mMass = m.mass || 0;
|
||||
let mmMass = mm.mass || 0;
|
||||
let massDiff = mMass - mmMass;
|
||||
let capDiff = (m.fuel || m.cargo || 0) - (mm.fuel || mm.cargo || 0);
|
||||
|
||||
propDiffs.push(<div key='cost'>{translate('cost')}: <span className={diffClass(m.cost, mm.cost, true) }>{formats.int(m.cost || 0)}{units.CR}</span></div>);
|
||||
propDiffs.push(<div key='mass'>{translate('mass')}: <span className={diffClass(mMass, mm.mass, true)}>{diff(formats.round, mMass, mmMass)}{units.T}</span></div>);
|
||||
|
||||
for (let p in m) {
|
||||
if (!PROP_BLACKLIST[p] && !isNaN(m[p])) {
|
||||
let mVal = m[p] || Infinity;
|
||||
let mmVal = mm[p] === null ? Infinity : mm[p];
|
||||
let format = formats[FORMAT_LOOKUP[p]] || formats.round;
|
||||
propDiffs.push(<div key={p}>
|
||||
{`${translate(TERM_LOOKUP[p] || p)}: `}
|
||||
<span className={diffClass(mVal, mmVal, p == 'power')}>{diff(format, mVal, mmVal)}{units[UNIT_LOOKUP[p]]}</span>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
if (m.grp == 'fd' || massDiff || capDiff) {
|
||||
let fsd = m.grp == 'fd' ? m : null;
|
||||
let maxRange = this.getUnladenRange(massDiff, m.fuel, fsd);
|
||||
let ladenRange = this.getLadenRange(massDiff + capDiff, m.fuel, fsd);
|
||||
|
||||
if (maxRange != this.unladenRange) {
|
||||
propDiffs.push(<div key='maxRange'>{`${translate('max')} ${translate('jump range')}: `}<span className={maxRange > this.unladenRange ? 'secondary' : 'warning'}>{formats.round(maxRange)}{units.LY}</span></div>);
|
||||
}
|
||||
if (ladenRange != this.ladenRange) {
|
||||
propDiffs.push(<div key='unladenRange'>{`${translate('laden')} ${translate('jump range')}: `}<span className={ladenRange > this.ladenRange ? 'secondary' : 'warning'}>{formats.round(ladenRange)}{units.LY}</span></div>);
|
||||
}
|
||||
}
|
||||
|
||||
return <div className='cap' style={{ whiteSpace: 'nowrap' }}>{propDiffs}</div>;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
|
||||
/**
|
||||
* Wraps the callback/context menu handler such that the default
|
||||
* operation can proceed if the SHIFT key is held while right-clicked
|
||||
* @param {Function} cb Callback for contextMenu
|
||||
* @return {Function} Wrapped contextmenu handler
|
||||
*/
|
||||
export function wrapCtxMenu(cb) {
|
||||
return (event) => {
|
||||
if (!event.getModifierState('Shift')) {
|
||||
event.preventDefault();
|
||||
cb.call(null, event);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares A and B and return true using strict comparison (===)
|
||||
* @param {any} objA A
|
||||
* @param {any} objB B
|
||||
* @return {boolean} true if A === B OR A properties === B properties
|
||||
*/
|
||||
export default function shallowEqual(objA, objB) {
|
||||
export function shallowEqual(objA, objB) {
|
||||
if (objA === objB) {
|
||||
return true;
|
||||
}
|
||||
@@ -31,15 +31,15 @@
|
||||
|
||||
<script src="{%= o.htmlWebpackPlugin.files.chunks.lib.entry %}" charset="utf-8" crossorigin="anonymous"></script>
|
||||
<script src="{%= o.htmlWebpackPlugin.files.chunks.app.entry %}" charset="utf-8" crossorigin="anonymous"></script>
|
||||
{% if (o.htmlWebpackPlugin.options.uaTracking) { %}
|
||||
<script>
|
||||
<script>
|
||||
{% if (o.htmlWebpackPlugin.options.uaTracking) { %}
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
ga('create', '{%= o.htmlWebpackPlugin.options.uaTracking %}', 'auto');
|
||||
window.CORIOLIS_GAPI_KEY = '{%= o.htmlWebpackPlugin.options.gapiKey %}';
|
||||
</script>
|
||||
{% } %}
|
||||
{% } %}
|
||||
window.CORIOLIS_GAPI_KEY = '{%= o.htmlWebpackPlugin.options.gapiKey %}';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
@import 'select';
|
||||
@import 'modal';
|
||||
@import 'charts';
|
||||
@import 'chart-tooltip';
|
||||
@import 'tooltip';
|
||||
@import 'buttons';
|
||||
@import 'error';
|
||||
@import 'sortable';
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
.d3-tip {
|
||||
font-size: 0.8em;
|
||||
padding: 0.25em 0.5em;
|
||||
background: @primary-disabled;
|
||||
text-transform: capitalize;
|
||||
color: @primary-bg;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Creates a small triangle extender for the tooltip */
|
||||
.d3-tip:after {
|
||||
box-sizing: border-box;
|
||||
display: inline;
|
||||
font-size: 10px;
|
||||
width: 100%;
|
||||
line-height: 1;
|
||||
color: @primary-disabled;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Northward tooltips */
|
||||
.d3-tip.n {
|
||||
margin-top: -7px;
|
||||
&:after {
|
||||
content: "\25BC";
|
||||
margin: -1px 0 0 0;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Eastward tooltips */
|
||||
.d3-tip.e {
|
||||
margin-left: 8px;
|
||||
&:after {
|
||||
content: "\25C0";
|
||||
margin: -4px 0 0 0;
|
||||
top: 50%;
|
||||
left: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Southward tooltips */
|
||||
.d3-tip.s {
|
||||
margin-top: 8px;
|
||||
&:after {
|
||||
content: "\25B2";
|
||||
margin: 0 0 1px 0;
|
||||
top: -7px;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Westward tooltips */
|
||||
.d3-tip.w:after {
|
||||
content: "\25B6";
|
||||
margin: -4px 0 0 -1px;
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
}
|
||||
@@ -27,6 +27,11 @@
|
||||
fill: @fg;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: @muted;
|
||||
fill: @muted;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: @disabled;
|
||||
fill: @disabled;
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
clear: both;
|
||||
margin: 1em 0 0;
|
||||
overflow-y:auto;
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
display: inline-block;
|
||||
@@ -107,8 +108,7 @@
|
||||
table {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
vertical-align: top;
|
||||
width: 45%;
|
||||
|
||||
.smallTablet({
|
||||
width: 100%;
|
||||
@@ -120,17 +120,22 @@
|
||||
|
||||
tbody {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
z-index: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
height: 8em;
|
||||
max-height: 8em;
|
||||
}
|
||||
|
||||
td {
|
||||
cursor: pointer;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px solid @primary-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
#comp-tbl {
|
||||
|
||||
@@ -9,7 +9,7 @@ select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
padding: 0.1em 0.5em;
|
||||
padding: 0 0.5em;
|
||||
outline:none;
|
||||
border: 0;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
color: @fg;
|
||||
fill: @fg;
|
||||
|
||||
.details {
|
||||
.details-container {
|
||||
min-height: 2.7em;
|
||||
padding: 0.25em 0;
|
||||
box-sizing: border-box;
|
||||
@@ -24,6 +24,11 @@
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.details {
|
||||
min-height: 2.2em;
|
||||
background-color: @primary-bg;
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
@@ -75,5 +80,58 @@
|
||||
background-color: @primary-bg;
|
||||
border-right: 1px solid @primary;
|
||||
}
|
||||
.details {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.eligible {
|
||||
border: 1px solid @secondary-disabled;
|
||||
.sz {
|
||||
color: @secondary-disabled;
|
||||
border-right: 1px solid @secondary-disabled;
|
||||
}
|
||||
.details {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.ineligible {
|
||||
cursor: no-drop;
|
||||
color: @disabled;
|
||||
fill: @disabled;
|
||||
border: 1px solid @disabled;
|
||||
.sz {
|
||||
color: @disabled;
|
||||
border-right: 1px solid @disabled;
|
||||
}
|
||||
}
|
||||
|
||||
&.dropEmpty {
|
||||
color: @warning-disabled;
|
||||
fill: @warning-disabled;
|
||||
border: 1px solid @warning-disabled;
|
||||
.sz {
|
||||
color: @warning-disabled;
|
||||
border-right: 1px solid @warning-disabled;
|
||||
}
|
||||
.details {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.drop {
|
||||
color: @secondary-bg;
|
||||
fill: @secondary-bg;
|
||||
border: 1px solid @secondary;
|
||||
background-color: @secondary-disabled;
|
||||
.sz {
|
||||
color: @secondary;
|
||||
background-color: @primary-bg;
|
||||
border-right: 1px solid @secondary;
|
||||
}
|
||||
.details {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
68
src/less/tooltip.less
Executable file
68
src/less/tooltip.less
Executable file
@@ -0,0 +1,68 @@
|
||||
.tip {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
font-size: 0.8em;
|
||||
padding: 0.25em 0.5em;
|
||||
display: inline-block;
|
||||
background: @bgBlack;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid @primary;
|
||||
color: @fg;
|
||||
pointer-events: none;
|
||||
.border-radius();
|
||||
|
||||
&.n {
|
||||
margin-top: -6px;
|
||||
left: 50%;
|
||||
.transform(translate(-50%, -100%));
|
||||
}
|
||||
&.s {
|
||||
margin-top: 6px;
|
||||
left: 50%;
|
||||
.transform(translate(-50%, 0));
|
||||
}
|
||||
&.e {
|
||||
margin-left: 6px;
|
||||
.transform(translate(0, -50%));
|
||||
}
|
||||
&.w {
|
||||
margin-left: -6px;
|
||||
.transform(translate(-100%, -50%));
|
||||
}
|
||||
}
|
||||
|
||||
/* Triangle 'pointer' for the tooltip */
|
||||
.arr {
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
|
||||
&.n {
|
||||
border-top: 6px solid @primary;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
margin-top: -6px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
&.s {
|
||||
border-bottom: 6px solid @primary;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
margin-left: -8px;
|
||||
}
|
||||
&.e {
|
||||
border-right: 6px solid @primary;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-top: 8px solid transparent;
|
||||
margin-top: -8px;
|
||||
}
|
||||
&.w {
|
||||
border-left: 6px solid @primary;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-top: 8px solid transparent;
|
||||
margin-top: -8px;
|
||||
margin-left: -6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,3 +29,11 @@
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.transform(@transform) {
|
||||
-webkit-transform: @transform;
|
||||
-moz-transform: @transform;
|
||||
-ms-transform: @transform;
|
||||
-o-transform: @transform;
|
||||
transform: @transform;
|
||||
}
|
||||
220
test/fixtures/anaconda-test-detailed-export-v1.json
vendored
220
test/fixtures/anaconda-test-detailed-export-v1.json
vendored
@@ -1,220 +0,0 @@
|
||||
{
|
||||
"$schema": "http://cdn.coriolis.io/schemas/ship-loadout/1.json#",
|
||||
"name": "Test",
|
||||
"ship": "Anaconda",
|
||||
"references": [
|
||||
{
|
||||
"name": "Coriolis.io",
|
||||
"url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b?bn=Test",
|
||||
"code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b",
|
||||
"shipId": "anaconda"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"standard": {
|
||||
"bulkheads": "Reactive Surface Composite",
|
||||
"powerPlant": {
|
||||
"class": 8,
|
||||
"rating": "A"
|
||||
},
|
||||
"thrusters": {
|
||||
"class": 6,
|
||||
"rating": "A"
|
||||
},
|
||||
"frameShiftDrive": {
|
||||
"class": 6,
|
||||
"rating": "A"
|
||||
},
|
||||
"lifeSupport": {
|
||||
"class": 5,
|
||||
"rating": "A"
|
||||
},
|
||||
"powerDistributor": {
|
||||
"class": 8,
|
||||
"rating": "A"
|
||||
},
|
||||
"sensors": {
|
||||
"class": 8,
|
||||
"rating": "A"
|
||||
},
|
||||
"fuelTank": {
|
||||
"class": 5,
|
||||
"rating": "C"
|
||||
}
|
||||
},
|
||||
"hardpoints": [
|
||||
{
|
||||
"class": 4,
|
||||
"rating": "A",
|
||||
"group": "Plasma Accelerator",
|
||||
"mount": "Fixed"
|
||||
},
|
||||
{
|
||||
"class": 3,
|
||||
"rating": "D",
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 3,
|
||||
"rating": "D",
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 3,
|
||||
"rating": "D",
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "E",
|
||||
"group": "Cannon",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "E",
|
||||
"group": "Cannon",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 1,
|
||||
"rating": "F",
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
},
|
||||
{
|
||||
"class": 1,
|
||||
"rating": "F",
|
||||
"group": "Beam Laser",
|
||||
"mount": "Turret"
|
||||
}
|
||||
],
|
||||
"utility": [
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "A",
|
||||
"group": "Shield Booster"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "A",
|
||||
"group": "Shield Booster"
|
||||
},
|
||||
null,
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "C",
|
||||
"group": "Kill Warrant Scanner"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "C",
|
||||
"group": "Cargo Scanner"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "F",
|
||||
"group": "Countermeasure",
|
||||
"name": "Electronic Countermeasure"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "I",
|
||||
"group": "Countermeasure",
|
||||
"name": "Chaff Launcher"
|
||||
},
|
||||
{
|
||||
"class": 0,
|
||||
"rating": "I",
|
||||
"group": "Countermeasure",
|
||||
"name": "Point Defence"
|
||||
}
|
||||
],
|
||||
"internal": [
|
||||
{
|
||||
"class": 7,
|
||||
"rating": "A",
|
||||
"group": "Shield Generator"
|
||||
},
|
||||
{
|
||||
"class": 6,
|
||||
"rating": "A",
|
||||
"group": "Shield Cell Bank"
|
||||
},
|
||||
{
|
||||
"class": 6,
|
||||
"rating": "E",
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
{
|
||||
"class": 5,
|
||||
"rating": "D",
|
||||
"group": "Hull Reinforcement Package"
|
||||
},
|
||||
{
|
||||
"class": 5,
|
||||
"rating": "E",
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"class": 4,
|
||||
"rating": "E",
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
{
|
||||
"class": 4,
|
||||
"rating": "E",
|
||||
"group": "Cargo Rack"
|
||||
},
|
||||
{
|
||||
"class": 4,
|
||||
"rating": "A",
|
||||
"group": "Fuel Scoop"
|
||||
},
|
||||
{
|
||||
"class": 2,
|
||||
"rating": "A",
|
||||
"group": "Frame Shift Drive Interdictor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {
|
||||
"class": 3,
|
||||
"hullCost": 141889932,
|
||||
"speed": 180,
|
||||
"boost": 244,
|
||||
"boostEnergy": 29,
|
||||
"agility": 2,
|
||||
"baseShieldStrength": 350,
|
||||
"baseArmour": 945,
|
||||
"hullMass": 400,
|
||||
"masslock": 23,
|
||||
"shipCostMultiplier": 1,
|
||||
"componentCostMultiplier": 1,
|
||||
"fuelCapacity": 32,
|
||||
"cargoCapacity": 128,
|
||||
"ladenMass": 1339.2,
|
||||
"armour": 2078,
|
||||
"armourAdded": 240,
|
||||
"armourMultiplier": 1.95,
|
||||
"shieldMultiplier": 1.4,
|
||||
"totalCost": 882362049,
|
||||
"unladenMass": 1179.2,
|
||||
"totalDps": 29,
|
||||
"powerAvailable": 36,
|
||||
"powerRetracted": 23.93,
|
||||
"powerDeployed": 35.56,
|
||||
"unladenRange": 18.49,
|
||||
"fullTankRange": 18.12,
|
||||
"ladenRange": 16.39,
|
||||
"unladenTotalRange": 73.21,
|
||||
"ladenTotalRange": 66.15,
|
||||
"maxJumpCount": 4,
|
||||
"shieldStrength": 833
|
||||
}
|
||||
}
|
||||
1
test/fixtures/eddb-modules.json
vendored
1
test/fixtures/eddb-modules.json
vendored
File diff suppressed because one or more lines are too long
@@ -1,30 +0,0 @@
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', 'fixture'],
|
||||
preprocessors: {
|
||||
'../build/schemas/**/*.json': ['json_fixtures'],
|
||||
'fixtures/**/*.json': ['json_fixtures']
|
||||
},
|
||||
files: [
|
||||
'../build/lib*.js',
|
||||
'../node_modules/angular-mocks/angular-mocks.js',
|
||||
'../node_modules/jsen/dist/jsen.js',
|
||||
'../build/app*.js',
|
||||
'../build/schemas/**/*.json',
|
||||
'fixtures/**/*.json',
|
||||
'tests/**/*.js',
|
||||
],
|
||||
jsonFixturesPreprocessor: {
|
||||
stripPrefix: '.*(/build/)',
|
||||
variableName: '__json__'
|
||||
},
|
||||
reporters: ['mocha'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: false,
|
||||
browsers: ['PhantomJS'],
|
||||
singleRun: false
|
||||
});
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
describe("Outfit Controller", function() {
|
||||
beforeEach(module('app'));
|
||||
|
||||
var outfitController, $rootScope, $stateParams, scope;
|
||||
|
||||
var eventStub = {
|
||||
preventDefault: function(){ },
|
||||
stopPropagation: function(){ }
|
||||
};
|
||||
|
||||
beforeEach(inject(function(_$rootScope_, $controller) {
|
||||
$rootScope = _$rootScope_;
|
||||
$rootScope.discounts = { ship: 1, components: 1};
|
||||
$stateParams = { shipId: 'anaconda'};
|
||||
scope = $rootScope.$new();
|
||||
outfitController = $controller('OutfitController', { $rootScope: $rootScope, $scope: scope, $stateParams: $stateParams });
|
||||
}));
|
||||
|
||||
describe("Retrofitting Costs", function() {
|
||||
|
||||
it("are empty by default", function() {
|
||||
expect(scope.retrofitTotal).toEqual(0);
|
||||
expect(scope.retrofitList.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("updates on bulkheads change", function() {
|
||||
scope.select('b', scope.ship.bulkheads, eventStub, "1"); // Use Reinforced Alloy Bulkheads
|
||||
expect(scope.retrofitTotal).toEqual(58787780);
|
||||
expect(scope.retrofitList.length).toEqual(1);
|
||||
scope.select('b', scope.ship.bulkheads, eventStub, "0"); // Use Reinforced Alloy Bulkheads
|
||||
expect(scope.retrofitTotal).toEqual(0);
|
||||
expect(scope.retrofitList.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("updates on component change", function() {
|
||||
scope.select('h', scope.ship.hardpoints[0], eventStub, "0u"); // 3C/F Beam Laser
|
||||
expect(scope.retrofitTotal).toEqual(1177600);
|
||||
expect(scope.retrofitList.length).toEqual(1);
|
||||
scope.select('h', scope.ship.hardpoints[6], eventStub, "empty"); // Remove default pulse laser
|
||||
scope.select('h', scope.ship.hardpoints[7], eventStub, "empty"); // Remove default pulse laser
|
||||
expect(scope.retrofitTotal).toEqual(1173200);
|
||||
expect(scope.retrofitList.length).toEqual(3);
|
||||
scope.select('i', scope.ship.internal[3], eventStub, "11"); // Use 6A Auto field maintenance unit
|
||||
expect(scope.retrofitTotal).toEqual(16478700);
|
||||
expect(scope.retrofitList.length).toEqual(4);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,132 +0,0 @@
|
||||
describe('Database', function() {
|
||||
|
||||
var shipProperties = [
|
||||
'name',
|
||||
'manufacturer',
|
||||
'class',
|
||||
'hullCost',
|
||||
'speed',
|
||||
'boost',
|
||||
'boostEnergy',
|
||||
'agility',
|
||||
'baseShieldStrength',
|
||||
'baseArmour',
|
||||
'hullMass',
|
||||
'masslock',
|
||||
'pipSpeed'
|
||||
];
|
||||
|
||||
var eddbModules = __json__['fixtures/eddb-modules'];
|
||||
var eddbIdToModule = {};
|
||||
|
||||
for (var e = 0; e < eddbModules.length; e++) {
|
||||
eddbIdToModule[eddbModules[e].id] = eddbModules[e];
|
||||
}
|
||||
|
||||
function validateEDDBId (category, component) {
|
||||
var id = component.id;
|
||||
expect(component.eddbID).toBeDefined(category + ' ' + id + ' is missing EDDB ID');
|
||||
var eddbModule = eddbIdToModule[component.eddbID];
|
||||
|
||||
expect(eddbModule).toBeDefined(category + ' [' + id + ']: no EDDB Module found for EDDB ID ' + component.eddbID);
|
||||
expect(component.class == eddbModule.class).toBeTruthy(category + ' [' + id + '] class does not match ' + component.eddbID);
|
||||
expect(component.rating == eddbModule.rating).toBeTruthy(category + ' [' + id + '] rating does not match ' + component.eddbID);
|
||||
expect(component.mode === undefined || component.mode == eddbModule.weapon_mode.charAt(0))
|
||||
.toBeTruthy(category + ' [' + id + '] mode/mount does not match ' + component.eddbID);
|
||||
expect(component.name === undefined || (component.name == eddbModule.name || component.name == eddbModule.group.name))
|
||||
.toBeTruthy(category + ' [' + id + '] name does not match ' + component.eddbID);
|
||||
}
|
||||
|
||||
it('has ships and components', function() {
|
||||
expect(DB.ships).toBeDefined()
|
||||
expect(DB.components.standard).toBeDefined();
|
||||
expect(DB.components.hardpoints).toBeDefined();
|
||||
expect(DB.components.internal).toBeDefined();
|
||||
expect(DB.components.bulkheads).toBeDefined();
|
||||
});
|
||||
|
||||
it('has same number of components as EDDB', function() {
|
||||
var totalComponentCount = 0, g;
|
||||
for (g = 0; g < DB.components.standard.length; g++) {
|
||||
var group = DB.components.standard[g];
|
||||
for (var i in group) {
|
||||
totalComponentCount++;
|
||||
}
|
||||
}
|
||||
for (g in DB.components.bulkheads) {
|
||||
totalComponentCount += 5;
|
||||
}
|
||||
for (g in DB.components.hardpoints) {
|
||||
totalComponentCount += DB.components.hardpoints[g].length;
|
||||
}
|
||||
for (g in DB.components.internal) {
|
||||
if (g != 'ft') { // EDDB does not have internal fuel tanks listed seperately
|
||||
totalComponentCount += DB.components.internal[g].length;
|
||||
}
|
||||
}
|
||||
expect(totalComponentCount).toEqual(eddbModules.length, 'Component count mismatch with EDDB');
|
||||
});
|
||||
|
||||
it('has valid standard components', function() {
|
||||
var ids = {};
|
||||
for (var i = 0; i < DB.components.standard.length; i++) {
|
||||
var group = DB.components.standard[i];
|
||||
for (var c in group) {
|
||||
var id = group[c].id;
|
||||
expect(ids[id]).toBeFalsy('ID already exists: ' + id);
|
||||
expect(group[c].eddbID).toBeDefined('Standard component' + id + ' is missing EDDB ID');
|
||||
validateEDDBId('Standard', group[c]);
|
||||
expect(group[c].grp).toBeDefined('Common component has no group defined, Type: ' + i + ', ID: ' + c);
|
||||
ids[id] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('has valid hardpoints', function() {
|
||||
var ids = {};
|
||||
var groups = DB.components.hardpoints;
|
||||
|
||||
for (var g in groups) {
|
||||
var group = groups[g];
|
||||
for (var i = 0; i < group.length; i++) {
|
||||
var id = group[i].id;
|
||||
expect(ids[id]).toBeFalsy('ID already exists: ' + id);
|
||||
expect(group[i].grp).toBeDefined('Hardpoint has no group defined, ID:' + id);
|
||||
validateEDDBId('Hardpoint', group[i]);
|
||||
ids[id] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('has valid internal components', function() {
|
||||
var ids = {};
|
||||
var groups = DB.components.internal;
|
||||
|
||||
for (var g in groups) {
|
||||
var group = groups[g];
|
||||
for (var i = 0; i < group.length; i++) {
|
||||
var id = group[i].id;
|
||||
expect(ids[id]).toBeFalsy('ID already exists: ' + id);
|
||||
expect(group[i].grp).toBeDefined('Internal component has no group defined, ID:' + id);
|
||||
validateEDDBId('Internal', group[i]);
|
||||
ids[id] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('has data for every ship', function() {
|
||||
for (var s in DB.ships) {
|
||||
for (var p = 0; p < shipProperties.length; p++) {
|
||||
expect(DB.ships[s].properties[shipProperties[p]]).toBeDefined(shipProperties[p] + ' is missing for ' + s);
|
||||
}
|
||||
expect(DB.ships[s].eddbID).toBeDefined(s + ' is missing EDDB ID');
|
||||
expect(DB.ships[s].slots.standard.length).toEqual(7, s + ' is missing standard slots');
|
||||
expect(DB.ships[s].defaults.standard.length).toEqual(7, s + ' is missing standard defaults');
|
||||
expect(DB.ships[s].slots.hardpoints.length).toEqual(DB.ships[s].defaults.hardpoints.length, s + ' hardpoint slots and defaults dont match');
|
||||
expect(DB.ships[s].slots.internal.length).toEqual(DB.ships[s].defaults.internal.length, s + ' hardpoint slots and defaults dont match');
|
||||
expect(DB.ships[s].retailCost).toBeGreaterThan(DB.ships[s].properties.hullCost, s + ' has invalid retail cost');
|
||||
expect(DB.components.bulkheads[s]).toBeDefined(s + ' is missing bulkheads');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
describe("Serializer Service", function() {
|
||||
beforeEach(module('app'));
|
||||
|
||||
var Ship,
|
||||
Serializer,
|
||||
code = '48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA',
|
||||
anaconda = DB.ships['anaconda'],
|
||||
testBuild,
|
||||
exportData;
|
||||
|
||||
beforeEach(inject(function (_Ship_, _Serializer_) {
|
||||
Ship = _Ship_;
|
||||
Serializer = _Serializer_;
|
||||
}));
|
||||
|
||||
describe("To Detailed Build", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
testBuild = new Ship('anaconda', anaconda.properties, anaconda.slots);
|
||||
Serializer.toShip(testBuild, code);
|
||||
exportData = Serializer.toDetailedBuild('Test', testBuild, code);
|
||||
});
|
||||
|
||||
it("conforms to the v2 ship-loadout schema", function() {
|
||||
var shipLoadoutSchema = __json__['schemas/ship-loadout/2'];
|
||||
var validate = jsen(shipLoadoutSchema);
|
||||
var valid = validate(exportData);
|
||||
expect(valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it("contains the correct components and stats", function() {
|
||||
var anacondaTestExport = __json__['fixtures/anaconda-test-detailed-export-v2'];
|
||||
expect(exportData.components).toEqual(anacondaTestExport.components);
|
||||
expect(exportData.stats).toEqual(anacondaTestExport.stats);
|
||||
expect(exportData.ship).toEqual(anacondaTestExport.ship);
|
||||
expect(exportData.name).toEqual(anacondaTestExport.name);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("From Detailed Build", function() {
|
||||
|
||||
it("builds the ship correctly", function() {
|
||||
var anacondaTestExport = __json__['fixtures/anaconda-test-detailed-export-v2'];
|
||||
testBuildA = new Ship('anaconda', anaconda.properties, anaconda.slots);
|
||||
Serializer.toShip(testBuildA, code);
|
||||
testBuildB = Serializer.fromDetailedBuild(anacondaTestExport);
|
||||
|
||||
for(var p in testBuildB) {
|
||||
if (p == 'availCS') {
|
||||
continue;
|
||||
}
|
||||
expect(testBuildB[p]).toEqual(testBuildA[p], p + ' does not match');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -27,7 +27,8 @@ module.exports = {
|
||||
new HtmlWebpackPlugin({
|
||||
inject: false,
|
||||
template: path.join(__dirname, "src/index.html"),
|
||||
version: pkgJson.version
|
||||
version: pkgJson.version,
|
||||
gapiKey: process.env.CORIOLIS_GAPI_KEY || '',
|
||||
}),
|
||||
new ExtractTextPlugin('app.css', {
|
||||
allChunks: true
|
||||
|
||||
Reference in New Issue
Block a user