Continued porting to react, approaching beta

This commit is contained in:
Colin McLeod
2016-01-21 22:06:05 -08:00
parent 653cb30dd9
commit 8227a4e361
86 changed files with 3810 additions and 2030 deletions

View File

@@ -1,20 +1,3 @@
{ {
"stage": 0, "presets": ["es2015", "react", "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"]
}]
}
}
}
}
} }

View File

@@ -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", "name": "Test",
"ship": "Anaconda", "ship": "Anaconda",
"references": [ "references": [
{ {
"name": "Coriolis.io", "name": "Coriolis.io",
"url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.Iw18QDBNA%3D%3D%3D.AwhMJBGaei%2BJCyyiA%3D%3D%3D?bn=Test", "url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA?bn=Test?bn=Test",
"code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.Iw18QDBNA===.AwhMJBGaei+JCyyiA===", "code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA",
"shipId": "anaconda" "shipId": "anaconda"
} }
], ],
@@ -264,12 +264,12 @@
"masslock": 23, "masslock": 23,
"pipSpeed": 0.14, "pipSpeed": 0.14,
"shipCostMultiplier": 1, "shipCostMultiplier": 1,
"componentCostMultiplier": 1, "moduleCostMultiplier": 1,
"fuelCapacity": 32, "fuelCapacity": 32,
"cargoCapacity": 128, "cargoCapacity": 128,
"ladenMass": 1339.2, "ladenMass": 1339.2,
"armour": 2078, "armour": 2228,
"armourAdded": 240, "armourAdded": 390,
"armourMultiplier": 1.95, "armourMultiplier": 1.95,
"shieldMultiplier": 1.4, "shieldMultiplier": 1.4,
"totalCost": 882362060, "totalCost": 882362060,

View File

@@ -1,4 +1,4 @@
describe('Import Controller', function() { xdescribe('Import Controller', function() {
beforeEach(module('app')); beforeEach(module('app'));
var importController, $rootScope, $stateParams, scope; var importController, $rootScope, $stateParams, scope;

View 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');
}
});
});
});

View File

@@ -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() { 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() { it("can build all ships", function() {
for (var s in DB.ships) { for (let s in Ships) {
var shipData = DB.ships[s]; let shipData = Ships[s];
var ship = new Ship(s, shipData.properties, shipData.slots); 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'); 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() { it("resets and rebuilds properly", function() {
var id = 'cobra_mk_iii'; var id = 'cobra_mk_iii';
var cobra = DB.ships[id]; var cobra = Ships[id];
var shipA = new Ship(id, cobra.properties, cobra.slots); var shipA = new Ship(id, cobra.properties, cobra.slots);
var shipB = new Ship(id, cobra.properties, cobra.slots); var shipB = new Ship(id, cobra.properties, cobra.slots);
var testShip = new Ship(id, cobra.properties, cobra.slots); var testShip = new Ship(id, cobra.properties, cobra.slots);
@@ -81,7 +76,7 @@ describe("Ship Factory", function() {
it("discounts hull and components properly", function() { it("discounts hull and components properly", function() {
var id = 'cobra_mk_iii'; var id = 'cobra_mk_iii';
var cobra = DB.ships[id]; var cobra = Ships[id];
var testShip = new Ship(id, cobra.properties, cobra.slots); var testShip = new Ship(id, cobra.properties, cobra.slots);
testShip.buildWith(cobra.defaults); testShip.buildWith(cobra.defaults);
@@ -89,76 +84,76 @@ describe("Ship Factory", function() {
var originalTotalCost = testShip.totalCost; var originalTotalCost = testShip.totalCost;
var discount = 0.9; 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); testShip.applyDiscounts(discount, discount);
// Floating point errors cause miniscule decimal places which are handled in the app by rounding/formatting // 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'); 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 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'); expect(testShip.totalCost).toEqual(originalTotalCost, 'Total cost does not match');
testShip.applyDiscounts(discount, 1); // Only discount hull 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(Math.floor(testShip.m.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(testShip.totalCost).toEqual(originalTotalCost - originalHullCost + testShip.m.discountedCost, 'Total cost does not match');
}); });
it("enforces a single shield generator", function() { it("enforces a single shield generator", function() {
var id = 'anaconda'; var id = 'anaconda';
var anacondaData = DB.ships[id]; var anacondaData = Ships[id];
var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots); var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots);
anaconda.buildWith(anacondaData.defaults); 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].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[2].m).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].m.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[1].m.grp).toEqual('sg','Slot 1 should have SG 4j in it');
}); });
it("enforces a single shield fuel scoop", function() { it("enforces a single shield fuel scoop", function() {
var id = 'anaconda'; var id = 'anaconda';
var anacondaData = DB.ships[id]; var anacondaData = Ships[id];
var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots); var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots);
anaconda.buildWith(anacondaData.defaults); anaconda.buildWith(anacondaData.defaults);
anaconda.use(anaconda.internal[4], '32', Components.internal('32')); // 4A Fuel Scoop anaconda.use(anaconda.internal[4], ModuleUtils.internal('32')); // 4A Fuel Scoop
expect(anaconda.internal[4].c.grp).toEqual('fs', 'Anaconda fuel scoop slot'); 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].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[4].m).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].m.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[3].m.grp).toEqual('fs','Slot 1 should have FS 32 in it');
}); });
it("enforces a single refinery", function() { it("enforces a single refinery", function() {
var id = 'anaconda'; var id = 'anaconda';
var anacondaData = DB.ships[id]; var anacondaData = Ships[id];
var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots); var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots);
anaconda.buildWith(anacondaData.defaults); anaconda.buildWith(anacondaData.defaults);
anaconda.use(anaconda.internal[4], '23', Components.internal('23')); // 4E Refinery anaconda.use(anaconda.internal[4], ModuleUtils.internal('23')); // 4E Refinery
expect(anaconda.internal[4].c.grp).toEqual('rf', 'Anaconda refinery slot'); 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].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[4].m).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].m.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[3].m.grp).toEqual('rf','Slot 1 should have RF 23 in it');
}); });
}); });

View File

@@ -8,39 +8,67 @@
"homepage": "http://coriolis.io", "homepage": "http://coriolis.io",
"bugs": "https://github.com/cmmcleod/coriolis/issues", "bugs": "https://github.com/cmmcleod/coriolis/issues",
"private": true, "private": true,
"engine": "node >= 0.12.2", "engine": "node >= 4.0.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"clean": "rimraf build", "clean": "rimraf build",
"start": "node devServer.js", "start": "node devServer.js",
"lint": "eslint --ext .js,.jsx src", "lint": "eslint --ext .js,.jsx src",
"test": "jest",
"prod-serve": "nginx -p $(pwd) -c nginx.conf", "prod-serve": "nginx -p $(pwd) -c nginx.conf",
"prod-stop": "kill -QUIT $(cat nginx.pid)", "prod-stop": "kill -QUIT $(cat nginx.pid)",
"build:prod": "npm run clean && NODE_ENV=production CDN='//cdn.coriolis.io' webpack -d -p --config webpack.config.prod.js", "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", "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", "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": { "devDependencies": {
"appcache-webpack-plugin": "^1.2.1", "appcache-webpack-plugin": "^1.2.1",
"babel-core": "^5.4.7", "babel-core": "*",
"babel-eslint": "^4.1.6", "babel-eslint": "*",
"babel-loader": "^5.1.2", "babel-jest": "*",
"babel-plugin-react-transform": "^1.1.1", "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", "css-loader": "^0.23.0",
"eslint": "^1.10.1", "eslint": "2.0.0-beta.1",
"eslint-plugin-react": "^2.3.0", "eslint-plugin-react": "^3.15.0",
"expose-loader": "^0.7.1", "expose-loader": "^0.7.1",
"express": "^4.13.3", "express": "^4.13.3",
"extract-text-webpack-plugin": "^0.9.1", "extract-text-webpack-plugin": "^0.9.1",
"file-loader": "^0.8.4", "file-loader": "^0.8.4",
"html-webpack-plugin": "^1.7.0", "html-webpack-plugin": "^1.7.0",
"jest-cli": "*",
"json-loader": "^0.5.3", "json-loader": "^0.5.3",
"less": "^2.5.3", "less": "^2.5.3",
"less-loader": "^2.2.1", "less-loader": "^2.2.1",
"react-transform-catch-errors": "^1.0.0", "react-addons-test-utils": "^0.14.6",
"react-transform-hmr": "^1.0.0",
"redbox-react": "^1.0.1",
"rimraf": "^2.4.3", "rimraf": "^2.4.3",
"style-loader": "^0.13.0", "style-loader": "^0.13.0",
"url-loader": "^0.5.6", "url-loader": "^0.5.6",
@@ -52,8 +80,8 @@
"d3": "^3.5.9", "d3": "^3.5.9",
"fbemitter": "^2.0.0", "fbemitter": "^2.0.0",
"lz-string": "^1.4.4", "lz-string": "^1.4.4",
"react": "^0.14.2", "react": "^0.14.6",
"react-dom": "^0.14.2", "react-dom": "^0.14.6",
"superagent": "^1.4.0" "superagent": "^1.4.0"
} }
} }

View File

@@ -1,24 +1,39 @@
import React from 'react'; import React from 'react';
import Router from './Router'; import Router from './Router';
import { EventEmitter } from 'fbemitter';
import { getLanguage } from './i18n/Language'; import { getLanguage } from './i18n/Language';
import Persist from './stores/Persist'; import Persist from './stores/Persist';
import InterfaceEvents from './utils/InterfaceEvents';
import Header from './components/Header'; import Header from './components/Header';
import Tooltip from './components/Tooltip';
import AboutPage from './pages/AboutPage'; import AboutPage from './pages/AboutPage';
import NotFoundPage from './pages/NotFoundPage'; import NotFoundPage from './pages/NotFoundPage';
import OutfittingPage from './pages/OutfittingPage'; import OutfittingPage from './pages/OutfittingPage';
import ComparisonPage from './pages/ComparisonPage'; import ComparisonPage from './pages/ComparisonPage';
import ShipyardPage from './pages/ShipyardPage'; import ShipyardPage from './pages/ShipyardPage';
/**
* Coriolis App
*/
export default class Coriolis extends React.Component { export default class Coriolis extends React.Component {
static childContextTypes = { static childContextTypes = {
language: React.PropTypes.object.isRequired, language: React.PropTypes.object.isRequired,
sizeRatio: React.PropTypes.number.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() { constructor() {
super(); super();
this._setPage = this._setPage.bind(this); this._setPage = this._setPage.bind(this);
@@ -26,10 +41,14 @@ export default class Coriolis extends React.Component {
this._closeMenu = this._closeMenu.bind(this); this._closeMenu = this._closeMenu.bind(this);
this._showModal = this._showModal.bind(this); this._showModal = this._showModal.bind(this);
this._hideModal = this._hideModal.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._onLanguageChange = this._onLanguageChange.bind(this);
this._onSizeRatioChange = this._onSizeRatioChange.bind(this) this._onSizeRatioChange = this._onSizeRatioChange.bind(this);
this._keyDown = this._keyDown.bind(this); this._keyDown = this._keyDown.bind(this);
this.emitter = new EventEmitter();
this.state = { this.state = {
page: null, page: null,
language: getLanguage(Persist.getLangCode()), language: getLanguage(Persist.getLangCode()),
@@ -45,23 +64,49 @@ export default class Coriolis extends React.Component {
Router('*', (r) => this._setPage(null, r)); 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) { _setPage(page, route) {
this.setState({ page, route, currentMenu: null }); 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) { _onError(msg, scriptUrl, line, col, errObj) {
console.log('WINDOW ERROR', arguments); 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) { _onLanguageChange(lang) {
this.setState({ language: getLanguage(Persist.getLangCode()) }); this.setState({ language: getLanguage(Persist.getLangCode()) });
} }
/**
* Propagate the sizeRatio change
* @param {number} sizeRatio Size ratio / scale
*/
_onSizeRatioChange(sizeRatio) { _onSizeRatioChange(sizeRatio) {
this.setState({ sizeRatio }); this.setState({ sizeRatio });
} }
/**
* Handle Key Down
* @param {Event} e Keyboard Event
*/
_keyDown(e) { _keyDown(e) {
switch (e.keyCode) { switch (e.keyCode) {
case 27: 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 * Creates the context to be passed down to pages / components containing
* language, sizeRatio and route references * language, sizeRatio and route references
@@ -117,7 +199,14 @@ export default class Coriolis extends React.Component {
return { return {
language: this.state.language, language: this.state.language,
route: this.state.route, 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.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); document.addEventListener('keydown', this._keyDown);
Persist.addListener('language', this._onLanguageChange); Persist.addListener('language', this._onLanguageChange);
Persist.addListener('sizeRatio', this._onSizeRatioChange); 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(); Router.start();
} }
@@ -157,6 +243,7 @@ export default class Coriolis extends React.Component {
<Header appCacheUpdate={this.state.appCacheUpdate} currentMenu={this.state.currentMenu} /> <Header appCacheUpdate={this.state.appCacheUpdate} currentMenu={this.state.currentMenu} />
{ this.state.page ? <this.state.page currentMenu={this.state.currentMenu} /> : <NotFoundPage/> } { this.state.page ? <this.state.page currentMenu={this.state.currentMenu} /> : <NotFoundPage/> }
{ this.state.modal } { this.state.modal }
{ this.state.tooltip }
</div> </div>
); );
} }

View File

@@ -1,5 +1,9 @@
import Persist from './stores/Persist'; 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() { function isStandAlone() {
try { try {
return window.navigator.standalone || (window.external && window.external.msIsSiteMode && window.external.msIsSiteMode()); return window.navigator.standalone || (window.external && window.external.msIsSiteMode && window.external.msIsSiteMode());
@@ -9,8 +13,7 @@ function isStandAlone() {
} }
/** /**
* Register `path` with callback `fn()`, * Register path with callback fn(), or route path`, or Router.start().
* or route `path`, or `Router.start()`.
* *
* Router('*', fn); * Router('*', fn);
* Router('/user/:id', load, user); * Router('/user/:id', load, user);
@@ -18,13 +21,13 @@ function isStandAlone() {
* Router('/user/' + user.id); * Router('/user/' + user.id);
* Router(); * Router();
* *
* @param {String} path * @param {String} path path
* @param {Function} fn... * @param {Function} fn Callbacks (fn, fn, ...)
* @api public * @api public
*/ */
function Router(path, fn) { function Router(path, fn) {
var route = new Route(path); let route = new Route(path);
for (var i = 1; i < arguments.length; ++i) { for (let i = 1; i < arguments.length; ++i) {
Router.callbacks.push(route.middleware(arguments[i])); Router.callbacks.push(route.middleware(arguments[i]));
} }
} }
@@ -39,16 +42,15 @@ Router.start = function(){
window.addEventListener('popstate', onpopstate, false); window.addEventListener('popstate', onpopstate, false);
if (isStandAlone()) { if (isStandAlone()) {
var state = Persist.getState(); let state = Persist.getState();
// If a previous state has been stored, load that state // If a previous state has been stored, load that state
if (state && state.name && state.params) { if (state && state.name && state.params) {
Router(this.props.initialPath || '/'); Router(this.props.initialPath || '/');
//$state.go(state.name, state.params, { location: 'replace' });
} else { } else {
Router('/'); Router('/');
} }
} else { } else {
var url = location.pathname + location.search; let url = location.pathname + location.search;
Router.replace(url, null, true, true); Router.replace(url, null, true, true);
} }
}; };
@@ -56,14 +58,14 @@ Router.start = function(){
/** /**
* Show `path` with optional `state` object. * Show `path` with optional `state` object.
* *
* @param {String} path * @param {String} path Path
* @param {Object} state * @param {Object} state Additional state
* @return {Context} * @return {Context} New Context
* @api public * @api public
*/ */
Router.go = function(path, state) { Router.go = function(path, state) {
gaTrack(path); gaTrack(path);
var ctx = new Context(path, state); let ctx = new Context(path, state);
Router.dispatch(ctx); Router.dispatch(ctx);
if (!ctx.unhandled) { if (!ctx.unhandled) {
history.pushState(ctx.state, ctx.title, ctx.canonicalPath); history.pushState(ctx.state, ctx.title, ctx.canonicalPath);
@@ -74,15 +76,15 @@ Router.go = function(path, state) {
/** /**
* Replace `path` with optional `state` object. * Replace `path` with optional `state` object.
* *
* @param {String} path * @param {String} path path
* @param {Object} state * @param {Object} state State
* @return {Context} * @param {Boolean} dispatch If true dispatch the route / trigger update
* @return {Context} New Context
* @api public * @api public
*/ */
Router.replace = function(path, state, dispatch) { Router.replace = function(path, state, dispatch) {
gaTrack(path); gaTrack(path);
var ctx = new Context(path, state); let ctx = new Context(path, state);
if (dispatch) Router.dispatch(ctx); if (dispatch) Router.dispatch(ctx);
history.replaceState(ctx.state, ctx.title, ctx.canonicalPath); history.replaceState(ctx.state, ctx.title, ctx.canonicalPath);
return ctx; return ctx;
@@ -91,15 +93,18 @@ Router.replace = function(path, state, dispatch) {
/** /**
* Dispatch the given `ctx`. * Dispatch the given `ctx`.
* *
* @param {Object} ctx * @param {Context} ctx Context
* @api private * @api private
*/ */
Router.dispatch = function(ctx) { Router.dispatch = function(ctx) {
var i = 0; let i = 0;
/**
* Handle the next route
* @return {Function} Unhandled
*/
function next() { function next() {
var fn = Router.callbacks[i++]; let fn = Router.callbacks[i++];
if (!fn) return unhandled(ctx); if (!fn) return unhandled(ctx);
fn(ctx, next); fn(ctx, next);
} }
@@ -112,12 +117,12 @@ Router.dispatch = function(ctx){
* popstate then redirect. If you wish to handle * popstate then redirect. If you wish to handle
* 404s on your own use `Router('*', callback)`. * 404s on your own use `Router('*', callback)`.
* *
* @param {Context} ctx * @param {Context} ctx Context
* @return {Context} context
* @api private * @api private
*/ */
function unhandled(ctx) { function unhandled(ctx) {
var current = window.location.pathname + window.location.search; let current = window.location.pathname + window.location.search;
if (current != ctx.canonicalPath) { if (current != ctx.canonicalPath) {
window.location = ctx.canonicalPath; window.location = ctx.canonicalPath;
} }
@@ -128,13 +133,12 @@ function unhandled(ctx) {
* Initialize a new "request" `Context` * Initialize a new "request" `Context`
* with the given `path` and optional initial `state`. * with the given `path` and optional initial `state`.
* *
* @param {String} path * @param {String} path Path
* @param {Object} state * @param {Object} state State
* @api public * @api public
*/ */
function Context(path, state) { function Context(path, state) {
var i = path.indexOf('?'); let i = path.indexOf('?');
this.canonicalPath = path; this.canonicalPath = path;
this.path = path || '/'; this.path = path || '/';
@@ -160,32 +164,27 @@ function Context(path, state) {
* - `sensitive` enable case-sensitive routes * - `sensitive` enable case-sensitive routes
* - `strict` enable strict matching for trailing slashes * - `strict` enable strict matching for trailing slashes
* *
* @param {String} path * @param {String} path Path
* @param {Object} options. * @param {Object} options Options
* @api private * @api private
*/ */
function Route(path, options) { function Route(path, options) {
options = options || {}; options = options || {};
this.path = path; this.path = path;
this.method = 'GET'; this.method = 'GET';
this.regexp = pathtoRegexp(path this.regexp = pathtoRegexp(path, this.keys = [], options.sensitive, options.strict);
, this.keys = []
, options.sensitive
, options.strict);
} }
/** /**
* Return route middleware with * Return route middleware with
* the given callback `fn()`. * the given callback `fn()`.
* *
* @param {Function} fn * @param {Function} fn Route function
* @return {Function} * @return {Function} Callback
* @api public * @api public
*/ */
Route.prototype.middleware = function(fn) { Route.prototype.middleware = function(fn) {
var self = this; let self = this;
return function(ctx, next) { return function(ctx, next) {
if (self.match(ctx.path, ctx.params)) return fn(ctx, next); if (self.match(ctx.path, ctx.params)) return fn(ctx, next);
next(); next();
@@ -196,24 +195,23 @@ Route.prototype.middleware = function(fn){
* Check if this route matches `path`, if so * Check if this route matches `path`, if so
* populate `params`. * populate `params`.
* *
* @param {String} path * @param {String} path Path
* @param {Array} params * @param {Array} params Path params
* @return {Boolean} * @return {Boolean} True if path matches
* @api private * @api private
*/ */
Route.prototype.match = function(path, params) { Route.prototype.match = function(path, params) {
var keys = this.keys let keys = this.keys,
, qsIndex = path.indexOf('?') qsIndex = path.indexOf('?'),
, pathname = ~qsIndex ? path.slice(0, qsIndex) : path pathname = ~qsIndex ? path.slice(0, qsIndex) : path,
, m = this.regexp.exec(decodeURIComponent(pathname)); m = this.regexp.exec(decodeURIComponent(pathname));
if (!m) return false; if (!m) return false;
for (var i = 1, len = m.length; i < len; ++i) { for (let i = 1, len = m.length; i < len; ++i) {
var key = keys[i - 1]; 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) { if (key) {
params[key.name] = undefined !== params[key.name] ? params[key.name] : val; params[key.name] = undefined !== params[key.name] ? params[key.name] : val;
@@ -223,22 +221,9 @@ Route.prototype.match = function(path, params){
return true; 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 * Track a page view in Google Analytics
* @param {string} path * @param {string} path Path to track
*/ */
function gaTrack(path) { function gaTrack(path) {
if (window.ga) { if (window.ga) {
@@ -255,14 +240,13 @@ function gaTrack(path) {
* key names. For example "/user/:id" will * key names. For example "/user/:id" will
* then contain ["id"]. * then contain ["id"].
* *
* @param {String|RegExp|Array} path * @param {String|RegExp|Array} path Path template(s)
* @param {Array} keys * @param {Array} keys keys
* @param {Boolean} sensitive * @param {Boolean} sensitive Case sensitive
* @param {Boolean} strict * @param {Boolean} strict Strict matching
* @return {RegExp} * @return {RegExp} Regular expression
* @api private * @api private
*/ */
function pathtoRegexp(path, keys, sensitive, strict) { function pathtoRegexp(path, keys, sensitive, strict) {
if (path instanceof RegExp) return path; if (path instanceof RegExp) return path;
if (path instanceof Array) path = '(' + path.join('|') + ')'; if (path instanceof Array) path = '(' + path.join('|') + ')';
@@ -272,12 +256,12 @@ function pathtoRegexp(path, keys, sensitive, strict) {
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional) { .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional) {
keys.push({ name: key, optional: !! optional }); keys.push({ name: key, optional: !! optional });
slash = slash || ''; slash = slash || '';
return '' return '' +
+ (optional ? '' : slash) (optional ? '' : slash) +
+ '(?:' '(?:' +
+ (optional ? slash : '') (optional ? slash : '') +
+ (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' +
+ (optional || ''); (optional || '');
}) })
.replace(/([\/.])/g, '\\$1') .replace(/([\/.])/g, '\\$1')
.replace(/\*/g, '(.*)'); .replace(/\*/g, '(.*)');
@@ -286,11 +270,11 @@ function pathtoRegexp(path, keys, sensitive, strict) {
/** /**
* Handle "populate" events. * Handle "populate" events.
* @param {Event} e Event object
*/ */
function onpopstate(e) { function onpopstate(e) {
if (e.state) { if (e.state) {
var path = e.state.path; let path = e.state.path;
Router.replace(path, e.state, true); Router.replace(path, e.state, true);
} }
} }

View File

@@ -2,19 +2,31 @@ import React from 'react';
import Link from './Link'; import Link from './Link';
import cn from 'classnames'; import cn from 'classnames';
export default class ActiveLink extends Link {
isActive = () => { /**
return encodeURI(this.props.href) == (window.location.pathname + window.location.search); * 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 {
/**
* Renders the component
* @return {React.Component} The active link
*/
render() { render() {
let className = this.props.className; let className = this.props.className;
if (this.isActive()) { if (isActive(this.props.href)) {
className = cn(className, 'active'); 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>;
} }
} }

View File

@@ -4,11 +4,15 @@ import TranslatedComponent from './TranslatedComponent';
import cn from 'classnames'; import cn from 'classnames';
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons'; import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
/**
* Available modules menu
*/
export default class AvailableModulesMenu extends TranslatedComponent { export default class AvailableModulesMenu extends TranslatedComponent {
static propTypes = { 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, onSelect: React.PropTypes.func.isRequired,
diffDetails: React.PropTypes.func,
m: React.PropTypes.object, m: React.PropTypes.object,
shipMass: React.PropTypes.number, shipMass: React.PropTypes.number,
warning: React.PropTypes.func warning: React.PropTypes.func
@@ -18,17 +22,77 @@ export default class AvailableModulesMenu extends TranslatedComponent {
shipMass: 0 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 prevClass = null, prevRating = null;
let elems = []; let elems = [];
for (let i = 0; i < modules.length; i++) { for (let i = 0; i < modules.length; i++) {
let m = modules[i]; let m = modules[i];
let mount = null; let mount = null;
let disabled = m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass;
let classes = cn(m.name ? 'lc' : 'c', { let classes = cn(m.name ? 'lc' : 'c', {
active: mountedModule && mountedModule.id === m.id, active: mountedModule && mountedModule.id === m.id,
warning: warningFunc && warningFunc(m), warning: !disabled && warningFunc && warningFunc(m),
disabled: m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass disabled
}); });
switch(m.mount) { switch(m.mount) {
@@ -42,7 +106,13 @@ export default class AvailableModulesMenu extends TranslatedComponent {
} }
elems.push( 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} {mount}
<span>{(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}</span> <span>{(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}</span>
</li> </li>
@@ -54,43 +124,54 @@ export default class AvailableModulesMenu extends TranslatedComponent {
return <ul key={'modules' + grp} >{elems}</ul>; 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() { componentDidMount() {
let m = this.props.m let m = this.props.m;
if (!(this.props.modules instanceof Array) && m && m.grp) { if (!(this.props.modules instanceof Array) && m && m.grp) {
findDOMNode(this).scrollTop = this.refs[m.grp].offsetTop; // Scroll to currently selected group 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() { 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 ( return (
<div className={cn('select', this.props.className)} onClick={(e) => e.stopPropagation() }> <div className={cn('select', this.props.className)} onScroll={this._hideDiff} onClick={(e) => e.stopPropagation() }>
{list} {this.state.list}
</div> </div>
); );
} }

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

View File

@@ -5,6 +5,9 @@ import cn from 'classnames';
import { SizeMap } from '../shipyard/Constants'; import { SizeMap } from '../shipyard/Constants';
/**
* Comparison Table
*/
export default class ComparisonTable extends TranslatedComponent { export default class ComparisonTable extends TranslatedComponent {
static propTypes = { static propTypes = {
@@ -12,16 +15,27 @@ export default class ComparisonTable extends TranslatedComponent {
builds: React.PropTypes.array.isRequired, builds: React.PropTypes.array.isRequired,
onSort: React.PropTypes.func.isRequired, onSort: React.PropTypes.func.isRequired,
predicate: React.PropTypes.string.isRequired, // Used only to test again prop changes for shouldRender 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) { constructor(props, context) {
super(props, context); super(props, context);
this._buildHeaders = this._buildHeaders.bind(this); this._buildHeaders = this._buildHeaders.bind(this);
this.state = this._buildHeaders(props.facets, props.onSort, context.language.translate); 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) { _buildHeaders(facets, onSort, translate) {
let header = [ let header = [
<th key='ship' rowSpan='2' className='sortable' onClick={onSort.bind(null, 'name')}>{translate('ship')}</th>, <th key='ship' rowSpan='2' className='sortable' onClick={onSort.bind(null, 'name')}>{translate('ship')}</th>,
@@ -48,6 +62,14 @@ export default class ComparisonTable extends TranslatedComponent {
return { header, subHeader }; 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) { _buildRow(build, facets, formats, units) {
let url = `/outfit/${build.id}/${build.toString()}?bn=${build.buildName}`; let url = `/outfit/${build.id}/${build.toString()}?bn=${build.buildName}`;
let cells = [ let cells = [
@@ -66,6 +88,11 @@ export default class ComparisonTable extends TranslatedComponent {
return <tr key={build.id + build.buildName} className='tr'>{cells}</tr>; 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) { componentWillReceiveProps(nextProps, nextContext) {
// If facets or language has changed re-render header // If facets or language has changed re-render header
if (nextProps.facets != this.props.facets || nextContext.language != this.context.language) { 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() { render() {
let { builds, facets } = this.props; let { builds, facets } = this.props;
let { header, subHeader } = this.state; let { header, subHeader } = this.state;
@@ -97,6 +128,5 @@ export default class ComparisonTable extends TranslatedComponent {
</table> </table>
</div> </div>
); );
} }
} }

View File

@@ -4,9 +4,12 @@ import { Ships } from 'coriolis-data';
import Persist from '../stores/Persist'; import Persist from '../stores/Persist';
import Ship from '../shipyard/Ship'; import Ship from '../shipyard/Ship';
import { Insurance } from '../shipyard/Constants'; import { Insurance } from '../shipyard/Constants';
import { slotName, nameComparator } from '../utils/SlotFunctions'; import { slotName, slotComparator } from '../utils/SlotFunctions';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
/**
* Cost Section
*/
export default class CostSection extends TranslatedComponent { export default class CostSection extends TranslatedComponent {
static PropTypes = { static PropTypes = {
@@ -15,6 +18,10 @@ export default class CostSection extends TranslatedComponent {
buildName: React.PropTypes.string buildName: React.PropTypes.string
}; };
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) { constructor(props) {
super(props); super(props);
this._costsTab = this._costsTab.bind(this); 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) { _buildRetrofitShip(shipId, name, retrofitShip) {
let data = Ships[shipId]; // Retrieve the basic ship properties, slots and defaults 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 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; 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) { _defaultRetrofitName(shipId, name) {
return Persist.hasBuild(shipId, name) ? name : null; return Persist.hasBuild(shipId, name) ? name : null;
} }
/**
* Show selected tab
* @param {string} tab Tab name
*/
_showTab(tab) { _showTab(tab) {
Persist.setCostTab(tab); Persist.setCostTab(tab);
this.setState({ tab }); this.setState({ tab });
} }
/**
* Update prices on discount change
*/
_onDiscountChanged() { _onDiscountChanged() {
let shipDiscount = Persist.getShipDiscount(); let shipDiscount = Persist.getShipDiscount();
let moduleDiscount = Persist.getModuleDiscount(); let moduleDiscount = Persist.getModuleDiscount();
@@ -84,13 +111,17 @@ export default class CostSection extends TranslatedComponent {
this.setState({ shipDiscount, moduleDiscount }); this.setState({ shipDiscount, moduleDiscount });
} }
/**
* Update insurance on change
* @param {string} insuranceName Insurance level name
*/
_onInsuranceChanged(insuranceName) { _onInsuranceChanged(insuranceName) {
this.setState({ insurance: Insurance[insuranceName] }); this.setState({ insurance: Insurance[insuranceName] });
} }
/** /**
* Repopulate modules on retrofit ship from existing build * 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) { _onBaseRetrofitChange(event) {
let retrofitName = event.target.value; let retrofitName = event.target.value;
@@ -105,6 +136,12 @@ export default class CostSection extends TranslatedComponent {
this.setState({ retrofitName }); 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) { _onBuildSaved(shipId, name, code) {
if(this.state.retrofitName == name) { if(this.state.retrofitName == name) {
this.state.retrofitShip.buildFrom(code); // Repopulate modules from saved build 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) { _onBuildDeleted(shipId, name, code) {
if(this.state.retrofitName == name) { if(this.state.retrofitName == name) {
this.state.retrofitShip.buildWith(Ships[shipId].defaults); // Retrofit ship becomes stock build 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) }); this.setState({ buildOptions: Persist.getBuildsNamesFor(shipId) });
} }
/**
* Toggle item cost inclusion in overall total
* @param {Object} item Cost item
*/
_toggleCost(item) { _toggleCost(item) {
this.props.ship.setCostIncluded(item, !item.incCost); this.props.ship.setCostIncluded(item, !item.incCost);
this.setState({ total: this.props.ship.totalCost }); this.setState({ total: this.props.ship.totalCost });
} }
/**
* Toggle item cost inclusion in retrofit total
* @param {Object} item Cost item
*/
_toggleRetrofitCost(item) { _toggleRetrofitCost(item) {
let retrofitTotal = this.state.retrofitTotal; let retrofitTotal = this.state.retrofitTotal;
item.retroItem.incCost = !item.retroItem.incCost; item.retroItem.incCost = !item.retroItem.incCost;
@@ -134,6 +185,10 @@ export default class CostSection extends TranslatedComponent {
this.setState({ retrofitTotal }); this.setState({ retrofitTotal });
} }
/**
* Set cost list sort predicate
* @param {string} predicate sort predicate
*/
_sortCostBy(predicate) { _sortCostBy(predicate) {
let { costPredicate, costDesc } = this.state; let { costPredicate, costDesc } = this.state;
@@ -144,20 +199,27 @@ export default class CostSection extends TranslatedComponent {
this.setState({ costPredicate: predicate, costDesc }); this.setState({ costPredicate: predicate, costDesc });
} }
/**
* Sort cost list
* @param {Ship} ship Ship instance
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort descending
*/
_sortCost(ship, predicate, desc) { _sortCost(ship, predicate, desc) {
let costList = ship.costList; let costList = ship.costList;
let translate = this.context.language.translate;
if (predicate == 'm') { if (predicate == 'm') {
costList.sort(nameComparator(this.context.language.translate)); costList.sort(slotComparator(translate, null, desc));
} else { } else {
costList.sort((a, b) => (a.m && a.m.cost ? a.m.cost : 0) - (b.m && b.m.cost ? b.m.cost : 0)); costList.sort(slotComparator(translate, (a, b) => (a.m.cost || 0) - (b.m.cost || 0), desc));
}
if (!desc) {
costList.reverse();
} }
} }
/**
* Set ammo list sort predicate
* @param {string} predicate sort predicate
*/
_sortAmmoBy(predicate) { _sortAmmoBy(predicate) {
let { ammoPredicate, ammoDesc } = this.state; let { ammoPredicate, ammoDesc } = this.state;
@@ -168,19 +230,26 @@ export default class CostSection extends TranslatedComponent {
this.setState({ ammoPredicate: predicate, ammoDesc }); this.setState({ ammoPredicate: predicate, ammoDesc });
} }
/**
* Sort ammo cost list
* @param {Array} ammoCosts Ammo cost list
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort descending
*/
_sortAmmo(ammoCosts, predicate, desc) { _sortAmmo(ammoCosts, predicate, desc) {
let translate = this.context.language.translate;
if (predicate == 'm') { if (predicate == 'm') {
ammoCosts.sort(nameComparator(this.context.language.translate)); ammoCosts.sort(slotComparator(translate, null, desc));
} else { } else {
ammoCosts.sort((a, b) => a[predicate] - b[predicate]); ammoCosts.sort(slotComparator(translate, (a, b) => a[predicate] - b[predicate], desc));
}
if (!desc) {
ammoCosts.reverse();
} }
} }
/**
* Set retrofit list sort predicate
* @param {string} predicate sort predicate
*/
_sortRetrofitBy(predicate) { _sortRetrofitBy(predicate) {
let { retroPredicate, retroDesc } = this.state; let { retroPredicate, retroDesc } = this.state;
@@ -191,6 +260,12 @@ export default class CostSection extends TranslatedComponent {
this.setState({ retroPredicate: predicate, retroDesc }); this.setState({ retroPredicate: predicate, retroDesc });
} }
/**
* Sort retrofit cost list
* @param {Array} retrofitCosts Retrofit cost list
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort descending
*/
_sortRetrofit(retrofitCosts, predicate, desc) { _sortRetrofit(retrofitCosts, predicate, desc) {
let translate = this.context.language.translate; 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() { _costsTab() {
let { ship } = this.props; let { ship } = this.props;
let { total, shipDiscount, moduleDiscount, insurance } = this.state; let { total, shipDiscount, moduleDiscount, insurance } = this.state;
@@ -250,6 +329,10 @@ export default class CostSection extends TranslatedComponent {
</div>; </div>;
} }
/**
* Render the retofit tab
* @return {React.Component} Tab contents
*/
_retrofitTab() { _retrofitTab() {
let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state; let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state;
let { translate, formats, units } = this.context.language; let { translate, formats, units } = this.context.language;
@@ -272,7 +355,7 @@ export default class CostSection extends TranslatedComponent {
</tr>); </tr>);
} }
} else { } 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> 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, 'buyName')}>{translate('buy')}</th>
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'cr')}> <th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'cr')}>
{translate('net cost')} {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> </th>
</tr> </tr>
</thead> </thead>
@@ -311,9 +394,15 @@ export default class CostSection extends TranslatedComponent {
</div>; </div>;
} }
/**
* Update retrofit costs
* @param {Ship} ship Ship instance
* @param {Ship} retrofitShip Retrofit Ship instance
*/
_updateRetrofit(ship, retrofitShip) { _updateRetrofit(ship, retrofitShip) {
let retrofitCosts = []; let retrofitCosts = [];
var retrofitTotal = 0, i, l, item; let retrofitTotal = 0, i, l, item;
if (ship.bulkheads.index != retrofitShip.bulkheads.index) { if (ship.bulkheads.index != retrofitShip.bulkheads.index) {
item = { item = {
@@ -330,9 +419,9 @@ export default class CostSection extends TranslatedComponent {
} }
} }
for (var g in { standard: 1, internal: 1, hardpoints: 1 }) { for (let g in { standard: 1, internal: 1, hardpoints: 1 }) {
var retroSlotGroup = retrofitShip[g]; let retroSlotGroup = retrofitShip[g];
var slotGroup = ship[g]; let slotGroup = ship[g];
for (i = 0, l = slotGroup.length; i < l; i++) { for (i = 0, l = slotGroup.length; i < l; i++) {
if (slotGroup[i].m != retroSlotGroup[i].m) { if (slotGroup[i].m != retroSlotGroup[i].m) {
item = { netCost: 0, retroItem: retroSlotGroup[i] }; 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); this._sortRetrofit(retrofitCosts, this.state.retroPredicate, this.state.retroDesc);
} }
/**
* Render the ammo tab
* @return {React.Component} Tab contents
*/
_ammoTab() { _ammoTab() {
let { ammoTotal, ammoCosts } = this.state; let { ammoTotal, ammoCosts } = this.state;
let { translate, formats, units } = this.context.language; let { translate, formats, units } = this.context.language;
@@ -400,6 +493,7 @@ export default class CostSection extends TranslatedComponent {
/** /**
* Recalculate all ammo costs * Recalculate all ammo costs
* @param {Ship} ship Ship instance
*/ */
_updateAmmoCosts(ship) { _updateAmmoCosts(ship) {
let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, srvs = 0, scoop = false; 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]; let slotGroup = ship[g];
for (let i = 0, l = slotGroup.length; i < l; i++) { for (let i = 0, l = slotGroup.length; i < l; i++) {
if (slotGroup[i].m) { 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; q = 0;
switch (slotGroup[i].m.grp) { switch (slotGroup[i].m.grp) {
case 'fs': //skip fuel calculation if scoop present case 'fs': // Skip fuel calculation if scoop present
scoop = true; scoop = true;
break; break;
case 'scb': case 'scb':
@@ -429,7 +523,7 @@ export default class CostSection extends TranslatedComponent {
default: default:
q = slotGroup[i].m.clip + slotGroup[i].m.ammo; 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) { if (slotGroup[i].m.ammocost > 0) {
item = { item = {
m: slotGroup[i].m, 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) { if (limpets > 0) {
item = { item = {
m: { name: 'limpets', class: '', rating: '' }, m: { name: 'limpets', class: '', rating: '' },
@@ -466,7 +560,7 @@ export default class CostSection extends TranslatedComponent {
ammoCosts.push(item); ammoCosts.push(item);
ammoTotal += item.total; ammoTotal += item.total;
} }
//calculate refuel costs if no scoop present // Calculate refuel costs if no scoop present
if (!scoop) { if (!scoop) {
item = { item = {
m: { name: 'fuel', class: '', rating: '' }, m: { name: 'fuel', class: '', rating: '' },
@@ -482,6 +576,9 @@ export default class CostSection extends TranslatedComponent {
this._sortAmmo(ammoCosts, this.state.ammoPredicate, this.state.ammoDesc); this._sortAmmo(ammoCosts, this.state.ammoPredicate, this.state.ammoDesc);
} }
/**
* Add listeners on mount and update costs
*/
componentWillMount() { componentWillMount() {
this.listeners = [ this.listeners = [
Persist.addListener('discounts', this._onDiscountChanged.bind(this)), Persist.addListener('discounts', this._onDiscountChanged.bind(this)),
@@ -494,6 +591,11 @@ export default class CostSection extends TranslatedComponent {
this._sortCost(this.props.ship); 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) { componentWillReceiveProps(nextProps, nextContext) {
let retrofitShip = this.state.retrofitShip; let retrofitShip = this.state.retrofitShip;
@@ -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) { componentWillUpdate(nextProps, nextState) {
let state = this.state; let state = this.state;
@@ -536,10 +643,17 @@ export default class CostSection extends TranslatedComponent {
} }
} }
/**
* Remove listeners
*/
componentWillUnmount() { componentWillUnmount() {
this.listeners.forEach(l => l.remove()); this.listeners.forEach(l => l.remove());
} }
/**
* Render the Cost section
* @return {React.Component} Contents
*/
render() { render() {
let tab = this.state.tab; let tab = this.state.tab;
let translate = this.context.language.translate; let translate = this.context.language.translate;

View File

@@ -1,23 +1,46 @@
import React from 'react'; import React from 'react';
import Slot from './Slot'; import Slot from './Slot';
/**
* Hardpoint / Utility Slot
*/
export default class HardpointSlot extends Slot { export default class HardpointSlot extends Slot {
/**
* Get the CSS class name for the slot.
* @return {string} CSS Class name
*/
_getClassNames() { _getClassNames() {
return this.props.maxClass > 0 ? 'hardpoint' : null; return this.props.maxClass > 0 ? 'hardpoint' : null;
} }
/**
* Get the label for the slot
* @param {Function} translate Translate function
* @return {string} Label
*/
_getMaxClassLabel(translate) { _getMaxClassLabel(translate) {
return translate(['U','S','M','L','H'][this.props.maxClass]); 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) { _getSlotDetails(m, translate, formats, u) {
if (m) { if (m) {
let classRating = `${m.class}${m.rating}${m.mount ? '/' + m.mount : ''}${m.missile ? m.missile : ''}`; let classRating = `${m.class}${m.rating}${m.mount ? '/' + m.mount : ''}${m.missile ? m.missile : ''}`;
return ( let { drag, drop } = this.props;
<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={'l'}>{classRating + ' ' + translate(m.name || m.grp)}</div>
<div className={'r'}>{m.mass}{u.T}</div> <div className={'r'}>{m.mass}{u.T}</div>
</div>
<div className={'cb'}> <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.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.dps ? <div className={'l'}>{translate('DPS')}: {m.dps} { m.mjdps ? <span>({formats.int(m.mjdps)} {u.MJ})</span> : null }</div> : null }
@@ -29,8 +52,7 @@ export default class HardpointSlot extends Slot {
{ m.range ? <div className={'l'}>{m.range} <u>km</u></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 } { m.ammo >= 0 ? <div className={'l'}>{translate('ammo')}: {formats.int(m.clip)}+{formats.int(m.ammo)}</div> : null }
</div> </div>
</div> </div>;
);
} else { } else {
return <div className={'empty'}>{translate('empty')}</div>; return <div className={'empty'}>{translate('empty')}</div>;
} }

View File

@@ -4,35 +4,60 @@ import HardpointSlot from './HardpointSlot';
import cn from 'classnames'; import cn from 'classnames';
import { MountFixed, MountGimballed, MountTurret } from '../components/SvgIcons'; import { MountFixed, MountGimballed, MountTurret } from '../components/SvgIcons';
/**
* Hardpoint slot section
*/
export default class HardpointsSlotSection extends SlotSection { export default class HardpointsSlotSection extends SlotSection {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) { constructor(props, context) {
super(props, context, 'hardpoints', 'hardpoints'); super(props, context, 'hardpoints', 'hardpoints');
this._empty = this._empty.bind(this); this._empty = this._empty.bind(this);
} }
/**
* Empty all slots
*/
_empty() { _empty() {
this.props.ship.emptyWeapons(); this.props.ship.emptyWeapons();
this.props.onChange(); this.props.onChange();
this._close(); 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) { _fill(group, mount, event) {
this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt')); this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt'));
this.props.onChange(); this.props.onChange();
this._close(); this._close();
} }
/**
* Empty all on section header right click
*/
_contextMenu() { _contextMenu() {
this._empty(); this._empty();
} }
/**
* Generate the slot React Components
* @return {Array} Array of Slots
*/
_getSlots() { _getSlots() {
let { ship, currentMenu } = this.props;
let { originSlot, targetSlot } = this.state;
let slots = []; let slots = [];
let hardpoints = this.props.ship.hardpoints; let hardpoints = ship.hardpoints;
let availableModules = this.props.ship.getAvailableModules(); let availableModules = ship.getAvailableModules();
let currentMenu = this.props.currentMenu;
for (let i = 0, l = hardpoints.length; i < l; i++) { for (let i = 0, l = hardpoints.length; i < l; i++) {
let h = hardpoints[i]; let h = hardpoints[i];
@@ -44,6 +69,11 @@ export default class HardpointsSlotSection extends SlotSection {
onOpen={this._openMenu.bind(this, h)} onOpen={this._openMenu.bind(this, h)}
onSelect={this._selectModule.bind(this, h)} onSelect={this._selectModule.bind(this, h)}
selected={currentMenu == 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} m={h.m}
/>); />);
} }
@@ -52,6 +82,11 @@ export default class HardpointsSlotSection extends SlotSection {
return slots; return slots;
} }
/**
* Generate the section drop-down menu
* @param {Function} translate Translate function
* @return {React.Component} Section menu
*/
_getSectionMenu(translate) { _getSectionMenu(translate) {
let _fill = this._fill; let _fill = this._fill;

View File

@@ -7,7 +7,6 @@ import ActiveLink from './ActiveLink';
import cn from 'classnames'; import cn from 'classnames';
import { Cogs, CoriolisLogo, Hammer, Rocket, StatsBars } from './SvgIcons'; import { Cogs, CoriolisLogo, Hammer, Rocket, StatsBars } from './SvgIcons';
import { Ships } from 'coriolis-data'; import { Ships } from 'coriolis-data';
import InterfaceEvents from '../utils/InterfaceEvents';
import Persist from '../stores/Persist'; import Persist from '../stores/Persist';
import { toDetailedExport } from '../shipyard/Serializer'; import { toDetailedExport } from '../shipyard/Serializer';
import ModalDeleteAll from './ModalDeleteAll'; import ModalDeleteAll from './ModalDeleteAll';
@@ -18,8 +17,16 @@ import Slider from './Slider';
const SIZE_MIN = 0.65; const SIZE_MIN = 0.65;
const SIZE_RANGE = 0.55; const SIZE_RANGE = 0.55;
/**
* Coriolis App Header section / menus
*/
export default class Header extends TranslatedComponent { export default class Header extends TranslatedComponent {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) { constructor(props, context) {
super(props); super(props);
this.shipOrder = Object.keys(Ships).sort(); this.shipOrder = Object.keys(Ships).sort();
@@ -44,64 +51,114 @@ export default class Header extends TranslatedComponent {
for (let name in Discounts) { for (let name in Discounts) {
this.discountOptions.push(<option key={name} value={Discounts[name]}>{name}</option>); this.discountOptions.push(<option key={name} value={Discounts[name]}>{name}</option>);
} }
} }
/**
* Update insurance level
* @param {SyntheticEvent} e Event
*/
_setInsurance(e) { _setInsurance(e) {
Persist.setInsurance(e.target.value); Persist.setInsurance(e.target.value);
} }
/**
* Update the Module discount
* @param {SyntheticEvent} e Event
*/
_setModuleDiscount(e) { _setModuleDiscount(e) {
Persist.setModuleDiscount(e.target.value * 1); Persist.setModuleDiscount(e.target.value * 1);
} }
/**
* Update the Ship discount
* @param {SyntheticEvent} e Event
*/
_setShipDiscount(e) { _setShipDiscount(e) {
Persist.setShipDiscount(e.target.value * 1); Persist.setShipDiscount(e.target.value * 1);
} }
/**
* Update the current language
* @param {SyntheticEvent} e Event
*/
_setLanguage(e) { _setLanguage(e) {
Persist.setLangCode(e.target.value); Persist.setLangCode(e.target.value);
} }
/**
* Toggle tooltips setting
*/
_toggleTooltips() {
Persist.showTooltips(!Persist.showTooltips());
}
/**
* Show delete all modal
* @param {SyntheticEvent} e Event
*/
_showDeleteAll(e) { _showDeleteAll(e) {
e.preventDefault(); e.preventDefault();
InterfaceEvents.showModal(<ModalDeleteAll />); this.context.showModal(<ModalDeleteAll />);
}; };
/**
* Show export modal with backup data
* @param {SyntheticEvent} e Event
*/
_showBackup(e) { _showBackup(e) {
let translate = this.context.language.translate; let translate = this.context.language.translate;
e.preventDefault(); e.preventDefault();
InterfaceEvents.showModal(<ModalExport this.context.showModal(<ModalExport
title={translate('backup')} title={translate('backup')}
description={translate('PHRASE_BACKUP_DESC')} description={translate('PHRASE_BACKUP_DESC')}
data={Persist.getAll()} data={Persist.getAll()}
/>); />);
}; };
/**
* Show export modal with detailed export
* @param {SyntheticEvent} e Event
*/
_showDetailedExport(e) { _showDetailedExport(e) {
let translate = this.context.language.translate; let translate = this.context.language.translate;
e.preventDefault(); e.preventDefault();
InterfaceEvents.showModal(<ModalExport this.context.showModal(<ModalExport
title={translate('detailed export')} title={translate('detailed export')}
description={translate('PHRASE_EXPORT_DESC')} description={translate('PHRASE_EXPORT_DESC')}
data={toDetailedExport(Persist.getBuilds())} data={toDetailedExport(Persist.getBuilds())}
/>); />);
} }
/**
* Show import modal
* @param {SyntheticEvent} e Event
*/
_showImport(e) { _showImport(e) {
e.preventDefault(); 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() { _resetTextSize() {
Persist.setSizeRatio(1); Persist.setSizeRatio(1);
} }
/**
* Open a menu
* @param {SyntheticEvent} event Event
* @param {string} menu Menu name
*/
_openMenu(event, menu) { _openMenu(event, menu) {
event.stopPropagation(); event.stopPropagation();
@@ -109,23 +166,31 @@ export default class Header extends TranslatedComponent {
menu = null; menu = null;
} }
InterfaceEvents.openMenu(menu); this.context.openMenu(menu);
} }
/**
* Generate the ships menu
* @return {React.Component} Menu
*/
_getShipsMenu() { _getShipsMenu() {
let shipList = []; let shipList = [];
for (let s in Ships) { 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 ( return (
<div className={'menu-list dbl no-wrap'} onClick={ (e) => e.stopPropagation() }> <div className='menu-list dbl no-wrap' onClick={ (e) => e.stopPropagation() }>
{shipList} {shipList}
</div> </div>
); );
} }
/**
* Generate the builds menu
* @return {React.Component} Menu
*/
_getBuildsMenu() { _getBuildsMenu() {
let builds = Persist.getBuilds(); let builds = Persist.getBuilds();
let buildList = []; let buildList = [];
@@ -135,19 +200,23 @@ export default class Header extends TranslatedComponent {
let buildNameOrder = Object.keys(builds[shipId]).sort(); let buildNameOrder = Object.keys(builds[shipId]).sort();
for (let buildName of buildNameOrder) { for (let buildName of buildNameOrder) {
let href = ['/outfit/', shipId, '/', builds[shipId][buildName], '?bn=', buildName].join(''); 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>); buildList.push(<ul key={shipId}>{Ships[shipId].properties.name}{shipBuilds}</ul>);
} }
} }
return ( return (
<div className={'menu-list'} onClick={ (e) => e.stopPropagation() }> <div className='menu-list' onClick={ (e) => e.stopPropagation() }>
<div className={'dbl'}>{buildList}</div> <div className='dbl'>{buildList}</div>
</div> </div>
); );
} }
/**
* Generate the comparison menu
* @return {React.Component} Menu
*/
_getComparisonsMenu() { _getComparisonsMenu() {
let comparisons; let comparisons;
let translate = this.context.language.translate; let translate = this.context.language.translate;
@@ -157,65 +226,64 @@ export default class Header extends TranslatedComponent {
let comps = Object.keys(Persist.getComparisons()).sort(); let comps = Object.keys(Persist.getComparisons()).sort();
for (let name of comps) { 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 { } else {
comparisons = <span className={'cap'}>{translate('none created')}</span>; comparisons = <span className='cap'>{translate('none created')}</span>;
} }
return ( return (
<div className={'menu-list'} onClick={ (e) => e.stopPropagation() } style={{ whiteSpace: 'nowrap' }}> <div className='menu-list' onClick={ (e) => e.stopPropagation() } style={{ whiteSpace: 'nowrap' }}>
{comparisons} {comparisons}
<hr /> <hr />
<Link href='/compare/all' ui-sref="compare({name: 'all'})" className={'block cap'}>{translate('compare all')}</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> <Link href='/compare' className='block cap'>{translate('create new')}</Link>
</div> </div>
); );
} }
/**
* Generate the settings menu
* @return {React.Component} Menu
*/
_getSettingsMenu() { _getSettingsMenu() {
let translate = this.context.language.translate; let translate = this.context.language.translate;
let tips = Persist.showTooltips();
return ( return (
<div className={'menu-list no-wrap cap'} onClick={ (e) => e.stopPropagation() }> <div className='menu-list no-wrap cap' onClick={ (e) => e.stopPropagation() }>
<ul> <div style={{ lineHeight: '2em' }}>
{translate('language')} {translate('language')}
<li> <select className='cap' value={Persist.getLangCode()} onChange={this._setLanguage}>
<select className={'cap'} value={Persist.getLangCode()} onChange={this._setLanguage}>
{this.languageOptions} {this.languageOptions}
</select> </select>
</li> <br/>
</ul><br/> <span className='cap ptr' onClick={this._toggleTooltips} >
<ul> {translate('tooltips')}
<div className={cn({ disabled: !tips, 'primary-disabled': tips })} style={{ marginLeft: '0.5em', display: 'inline-block' }}>{(tips ? '✔' : '✖')}</div>
</span>
<br/>
{translate('insurance')} {translate('insurance')}
<li> <select className='cap' value={Persist.getInsurance()} onChange={this._setInsurance}>
<select className={'cap'} value={Persist.getInsurance()} onChange={this._setInsurance}>
{this.insuranceOptions} {this.insuranceOptions}
</select> </select>
</li> <br/>
</ul><br/>
<ul>
{translate('ship')} {translate('discount')} {translate('ship')} {translate('discount')}
<li> <select className='cap' value={Persist.getShipDiscount()} onChange={this._setShipDiscount}>
<select className={'cap'} value={Persist.getShipDiscount()} onChange={this._setShipDiscount}>
{this.discountOptions} {this.discountOptions}
</select> </select>
</li> <br/>
</ul><br/>
<ul>
{translate('module')} {translate('discount')} {translate('module')} {translate('discount')}
<li> <select className='cap' value={Persist.getModuleDiscount()} onChange={this._setModuleDiscount} >
<select className={'cap'} value={Persist.getModuleDiscount()} onChange={this._setModuleDiscount} >
{this.discountOptions} {this.discountOptions}
</select> </select>
</li> </div>
</ul>
<hr /> <hr />
<ul> <ul>
{translate('builds')} & {translate('comparisons')} {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._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._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._showImport.bind(this)}>{translate('import')}</a></li>
<li><a href="#" onClick={this._showDeleteAll.bind(this)}>{translate('delete all')}</a></li> <li><a href="#" onClick={this._showDeleteAll.bind(this)}>{translate('delete all')}</a></li>
</ul> </ul>
<hr /> <hr />
@@ -227,22 +295,34 @@ export default class Header extends TranslatedComponent {
<td style={{ width: 20 }}><span style={{ fontSize: 30 }}>A</span></td> <td style={{ width: 20 }}><span style={{ fontSize: 30 }}>A</span></td>
</tr> </tr>
<tr> <tr>
<td colSpan='3' style={{ textAlign: 'center', cursor: 'pointer' }} className={'primary-disabled cap'} onClick={this._resetTextSize.bind(this)}>{translate('reset')}</td> <td colSpan='3' style={{ textAlign: 'center', cursor: 'pointer' }} className='primary-disabled cap' onClick={this._resetTextSize.bind(this)}>{translate('reset')}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<hr /> <hr />
<Link href="/about" className={'block'}>{translate('about')}</Link> <Link href="/about" className='block'>{translate('about')}</Link>
</div> </div>
); );
} }
/**
* Add listeners on mount
*/
componentWillMount() { componentWillMount() {
Persist.addListener('language', () => this.forceUpdate()); Persist.addListener('language', () => this.forceUpdate());
Persist.addListener('insurance', () => this.forceUpdate()); Persist.addListener('insurance', () => this.forceUpdate());
Persist.addListener('discounts', () => 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) { componentWillReceiveProps(nextProps, nextContext) {
if(this.context.language != nextContext.language) { if(this.context.language != nextContext.language) {
let translate = nextContext.language.translate; let translate = nextContext.language.translate;
@@ -253,6 +333,10 @@ export default class Header extends TranslatedComponent {
} }
} }
/**
* Render the header
* @return {React.Component} Header
*/
render() { render() {
let translate = this.context.language.translate; let translate = this.context.language.translate;
let openedMenu = this.props.currentMenu; let openedMenu = this.props.currentMenu;
@@ -264,32 +348,32 @@ export default class Header extends TranslatedComponent {
return ( return (
<header> <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='l menu'>
<div className={cn('menu-header', { selected: openedMenu == 's' })} onClick={ (e) => this._openMenu(e,'s') } > <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> <Rocket className='warning' /><span className='menu-item-label'>{' ' + translate('ships')}</span>
</div> </div>
{openedMenu == 's' ? this._getShipsMenu() : null} {openedMenu == 's' ? this._getShipsMenu() : null}
</div> </div>
<div className={'l menu'}> <div className='l menu'>
<div className={cn('menu-header', { selected: openedMenu == 'b', disabled: !hasBuilds })} onClick={ hasBuilds ? (e) => this._openMenu(e,'b') : null }> <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> <Hammer className={cn('warning', { 'warning-disabled': !hasBuilds })} /><span className='menu-item-label'>{' ' + translate('builds')}</span>
</div> </div>
{openedMenu == 'b' ? this._getBuildsMenu() : null} {openedMenu == 'b' ? this._getBuildsMenu() : null}
</div> </div>
<div className={'l menu'}> <div className='l menu'>
<div className={cn('menu-header', { selected: openedMenu == 'comp', disabled: !hasBuilds })} onClick={ hasBuilds ? (e) => this._openMenu(e,'comp') : null }> <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> <StatsBars className={cn('warning', { 'warning-disabled': !hasBuilds })} /><span className='menu-item-label'>{' ' + translate('compare')}</span>
</div> </div>
{openedMenu == 'comp' ? this._getComparisonsMenu() : null} {openedMenu == 'comp' ? this._getComparisonsMenu() : null}
</div> </div>
<div className={'r menu'}> <div className='r menu'>
<div className={cn('menu-header', { selected: openedMenu == 'settings' })}onClick={ (e) => this._openMenu(e,'settings') }> <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> <Cogs className='xl warning'/><span className='menu-item-label'>{translate('settings')}</span>
</div> </div>
{openedMenu == 'settings' ? this._getSettingsMenu() : null} {openedMenu == 'settings' ? this._getSettingsMenu() : null}
</div> </div>

View File

@@ -2,16 +2,29 @@ import React from 'react';
import Slot from './Slot'; import Slot from './Slot';
import { Infinite } from './SvgIcons'; import { Infinite } from './SvgIcons';
/**
* Internal Slot
*/
export default class InternalSlot extends 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) { _getSlotDetails(m, translate, formats, u) {
if (m) { if (m) {
let classRating = m.class + m.rating; let classRating = m.class + m.rating;
let { drag, drop } = this.props;
return ( return <div className='details' draggable='true' onDragStart={drag} onDragEnd={drop}>
<div> <div className={'cb'}>
<div className={'l'}>{classRating + ' ' + translate(m.name || m.grp)}</div> <div className={'l'}>{classRating + ' ' + translate(m.name || m.grp)}</div>
<div className={'r'}>{m.mass || m.capacity || 0}{u.T}</div> <div className={'r'}>{m.mass || m.cargo || m.fuel || 0}{u.T}</div>
</div>
<div className={'cb'}> <div className={'cb'}>
{ m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{u.T}</div> : null } { 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.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{u.T}</div> : null }
@@ -30,8 +43,7 @@ export default class InternalSlot extends Slot {
{ m.rangeRating ? <div className={'l'}>{translate('range')}: {m.rangeRating}</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 } { m.armouradd ? <div className={'l'}>+{m.armouradd} <u>{translate('armour')}</u></div> : null }
</div> </div>
</div> </div>;
);
} else { } else {
return <div className={'empty'}>{translate('empty')}</div>; return <div className={'empty'}>{translate('empty')}</div>;
} }

View File

@@ -4,9 +4,16 @@ import SlotSection from './SlotSection';
import InternalSlot from './InternalSlot'; import InternalSlot from './InternalSlot';
import * as ModuleUtils from '../shipyard/ModuleUtils'; import * as ModuleUtils from '../shipyard/ModuleUtils';
/**
* Internal slot section
*/
export default class InternalSlotSection extends SlotSection { export default class InternalSlotSection extends SlotSection {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) { constructor(props, context) {
super(props, context, 'internal', 'internal compartments'); super(props, context, 'internal', 'internal compartments');
@@ -16,12 +23,19 @@ export default class InternalSlotSection extends SlotSection {
this._fillWithArmor = this._fillWithArmor.bind(this); this._fillWithArmor = this._fillWithArmor.bind(this);
} }
/**
* Empty all slots
*/
_empty() { _empty() {
this.props.ship.emptyInternal(); this.props.ship.emptyInternal();
this.props.onChange(); this.props.onChange();
this._close(); this._close();
} }
/**
* Fill all slots with cargo racks
* @param {SyntheticEvent} event Event
*/
_fillWithCargo(event) { _fillWithCargo(event) {
let clobber = event.getModifierState('Alt'); let clobber = event.getModifierState('Alt');
let ship = this.props.ship; let ship = this.props.ship;
@@ -34,6 +48,10 @@ export default class InternalSlotSection extends SlotSection {
this._close(); this._close();
} }
/**
* Fill all slots with Shield Cell Banks
* @param {SyntheticEvent} event Event
*/
_fillWithCells(event) { _fillWithCells(event) {
let clobber = event.getModifierState('Alt'); let clobber = event.getModifierState('Alt');
let ship = this.props.ship; let ship = this.props.ship;
@@ -49,6 +67,10 @@ export default class InternalSlotSection extends SlotSection {
this._close(); this._close();
} }
/**
* Fill all slots with Hull Reinforcement Packages
* @param {SyntheticEvent} event Event
*/
_fillWithArmor(event) { _fillWithArmor(event) {
let clobber = event.getModifierState('Alt'); let clobber = event.getModifierState('Alt');
let ship = this.props.ship; let ship = this.props.ship;
@@ -61,18 +83,27 @@ export default class InternalSlotSection extends SlotSection {
this._close(); this._close();
} }
/**
* Empty all on section header right click
*/
_contextMenu() { _contextMenu() {
this._empty(); this._empty();
} }
/**
* Generate the slot React Components
* @return {Array} Array of Slots
*/
_getSlots() { _getSlots() {
let slots = []; let slots = [];
let { currentMenu, ship } = this.props; let { currentMenu, ship } = this.props;
let { originSlot, targetSlot } = this.state;
let { internal, fuelCapacity, ladenMass } = ship; let { internal, fuelCapacity, ladenMass } = ship;
let availableModules = ship.getAvailableModules(); let availableModules = ship.getAvailableModules();
for (let i = 0, l = internal.length; i < l; i++) { for (let i = 0, l = internal.length; i < l; i++) {
let s = internal[i]; let s = internal[i];
slots.push(<InternalSlot slots.push(<InternalSlot
key={i} key={i}
maxClass={s.maxClass} maxClass={s.maxClass}
@@ -82,13 +113,23 @@ export default class InternalSlotSection extends SlotSection {
selected={currentMenu == s} selected={currentMenu == s}
enabled={s.enabled} enabled={s.enabled}
m={s.m} 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} fuel={fuelCapacity}
ship={ship}
/>); />);
} }
return slots; return slots;
} }
/**
* Generate the section drop-down menu
* @param {Function} translate Translate function
* @return {React.Component} Section menu
*/
_getSectionMenu(translate) { _getSectionMenu(translate) {
return <div className='select' onClick={e => e.stopPropagation()}> return <div className='select' onClick={e => e.stopPropagation()}>
<ul> <ul>

View File

@@ -4,15 +4,18 @@ import d3 from 'd3';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
const RENDER_POINTS = 20; // Only render 20 points on the graph 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 { export default class LineChart extends TranslatedComponent {
static defaultProps = { static defaultProps = {
xMin: 0, xMin: 0,
yMin: 0, yMin: 0,
colors: ['#ff8c0d'] colors: ['#ff8c0d']
} };
static PropTypes = { static PropTypes = {
width: React.PropTypes.number.isRequired, width: React.PropTypes.number.isRequired,
@@ -29,6 +32,11 @@ export default class LineChart extends TranslatedComponent {
colors: React.PropTypes.array, colors: React.PropTypes.array,
}; };
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) { constructor(props, context) {
super(props); super(props);
@@ -42,11 +50,12 @@ export default class LineChart extends TranslatedComponent {
let markerElems = []; let markerElems = [];
let detailElems = [<text key={'lbl'} className='label x' y='1.25em'/>]; let detailElems = [<text key={'lbl'} className='label x' y='1.25em'/>];
let xScale = d3.scale.linear(); let xScale = d3.scale.linear();
let xAxisScale = d3.scale.linear();
let yScale = d3.scale.linear(); let yScale = d3.scale.linear();
let series = props.series; let series = props.series;
let seriesLines = []; 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'); 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++) { for(let i = 0, l = series ? series.length : 1; i < l; i++) {
@@ -58,6 +67,7 @@ export default class LineChart extends TranslatedComponent {
this.state = { this.state = {
xScale, xScale,
xAxisScale,
yScale, yScale,
seriesLines, seriesLines,
detailElems, detailElems,
@@ -66,15 +76,19 @@ export default class LineChart extends TranslatedComponent {
}; };
} }
/**
* Update tooltip content
* @param {number} xPos x coordinate
*/
_tooltip(xPos) { _tooltip(xPos) {
let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props; let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props;
let { xScale, yScale } = this.state; let { xScale, yScale, innerWidth } = this.state;
let { formats, translate } = this.context.language; let { formats, translate } = this.context.language;
let x0 = xScale.invert(xPos), let x0 = xScale.invert(xPos),
y0 = func(x0), y0 = func(x0),
tips = this.tipContainer, tips = this.tipContainer,
yTotal = 0, yTotal = 0,
flip = (x0 / xScale.domain()[1] > 0.65), flip = (xPos / innerWidth > 0.65),
tipWidth = 0, tipWidth = 0,
tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height; 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)); 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 { width, xMax, xMin, yMin, yMax } = props;
let innerWidth = width - MARGIN.left - MARGIN.right; 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; let innerHeight = outerHeight - MARGIN.top - MARGIN.bottom;
this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true); 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.state.yScale.range([innerHeight, 0]).domain([yMin, yMax]);
this.setState({ innerWidth, outerHeight, innerHeight }); this.setState({ innerWidth, outerHeight, innerHeight });
} }
/**
* Show tooltip
* @param {SyntheticEvent} e Event
*/
_showTip(e) { _showTip(e) {
this._moveTip(e); this._moveTip(e);
this.tipContainer.style('display', null); this.tipContainer.style('display', null);
this.markersContainer.style('display', null); this.markersContainer.style('display', null);
} }
/**
* Move and update tooltip
* @param {SyntheticEvent} e Event
*/
_moveTip(e) { _moveTip(e) {
this._tooltip(Math.round(e.clientX - e.target.getBoundingClientRect().left)); this._tooltip(Math.round(e.clientX - e.target.getBoundingClientRect().left));
} }
/**
* Hide tooltip
*/
_hideTip() { _hideTip() {
this.tipContainer.style('display', 'none'); this.tipContainer.style('display', 'none');
this.markersContainer.style('display', 'none'); this.markersContainer.style('display', 'none');
} }
/**
* Update series data generated from props
* @param {Object} props React Component properties
*/
_updateSeriesData(props) { _updateSeriesData(props) {
let { func, xMin, xMax, series } = props; let { func, xMin, xMax, series } = props;
let delta = (xMax - xMin) / RENDER_POINTS; let delta = (xMax - xMin) / RENDER_POINTS;
@@ -146,11 +181,19 @@ export default class LineChart extends TranslatedComponent {
this.setState({ seriesData }); this.setState({ seriesData });
} }
/**
* Update dimensions and series data based on props and context.
*/
componentWillMount() { componentWillMount() {
this._updateDimensions(this.props, this.context.sizeRatio); this._updateDimensions(this.props, this.context.sizeRatio);
this._updateSeriesData(this.props); 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) { componentWillReceiveProps(nextProps, nextContext) {
let { func, xMin, xMax, yMin, yMax, width } = nextProps; let { func, xMin, xMax, yMin, yMax, width } = nextProps;
let props = this.props; let props = this.props;
@@ -166,6 +209,10 @@ export default class LineChart extends TranslatedComponent {
} }
} }
/**
* Render the chart
* @return {React.Component} Chart SVG
*/
render() { render() {
if (!this.props.width) { if (!this.props.width) {
return null; return null;

View File

@@ -1,20 +1,32 @@
import React from 'react'; import React from 'react';
import Router from '../Router'; import Router from '../Router';
import shallowEqual from '../utils/shallowEqual'; import { shallowEqual } from '../utils/UtilityFunctions';
/**
* Link wrapper component
*/
export default class Link extends React.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) { shouldComponentUpdate(nextProps) {
return !shallowEqual(this.props, nextProps); return !shallowEqual(this.props, nextProps);
} }
handler = (event) => { /**
if (event.getModifierState * Link click handler
&& ( event.getModifierState('Shift') * @param {SyntheticEvent} event Event
|| event.getModifierState('Alt') */
|| event.getModifierState('Control') handler(event) {
|| event.getModifierState('Meta') if (event.getModifierState &&
|| event.button > 1)) { (event.getModifierState('Shift') ||
event.getModifierState('Alt') ||
event.getModifierState('Control') ||
event.getModifierState('Meta') ||
event.button > 1)) {
return; return;
} }
event.nativeEvent && event.preventDefault && event.preventDefault(); 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() { 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>;
} }
} }

View File

@@ -1,17 +1,24 @@
import React from 'react'; import React from 'react';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents';
import { Ships } from 'coriolis-data'; import { Ships } from 'coriolis-data';
import Persist from '../stores/Persist'; 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) { function buildComparator(a, b) {
if (a.name == b.name) { 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 { export default class ModalCompare extends TranslatedComponent {
static propTypes = { static propTypes = {
@@ -21,52 +28,71 @@ export default class ModalCompare extends TranslatedComponent {
static defaultProps = { static defaultProps = {
builds: [] builds: []
} };
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) { constructor(props) {
super(props); super(props);
let builds = props.builds; let builds = props.builds;
let allBuilds = Persist.getBuilds(); let allBuilds = Persist.getBuilds();
let unusedBuilds = []; let unusedBuilds = [];
let usedBuilds = [];
for (let id in allBuilds) { for (let id in allBuilds) {
for (let buildName in allBuilds[id]) { for (let buildName in allBuilds[id]) {
if (!builds.find((e) => e.buildName == buildName && e.id == id)) { let b = { id, buildName, name: Ships[id].properties.name };
unusedBuilds.push({ 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); 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) { _addBuild(buildIndex) {
let { builds, unusedBuilds } = this.state; let { usedBuilds, unusedBuilds } = this.state;
builds.push(unusedBuilds[buildIndex]); usedBuilds.push(unusedBuilds[buildIndex]);
unusedBuilds = unusedBuilds.splice(buildIndex, 1); unusedBuilds.splice(buildIndex, 1);
builds.sort(buildComparator); 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) { _removeBuild(buildIndex) {
let { builds, unusedBuilds } = this.state; let { usedBuilds, unusedBuilds } = this.state;
unusedBuilds.push(builds[buildIndex]); unusedBuilds.push(usedBuilds[buildIndex]);
builds = builds.splice(buildIndex, 1); usedBuilds.splice(buildIndex, 1);
unusedBuilds.sort(buildComparator); unusedBuilds.sort(buildComparator);
this.setState({ builds, unusedBuilds }); this.setState({ used: usedBuilds.length });
} }
/**
* OK Action - Use selected builds
*/
_selectBuilds() { _selectBuilds() {
this.props.onSelect(this.state.builds); this.props.onSelect(this.state.usedBuilds);
} }
/**
* Render the modal
* @return {React.Component} Modal Content
*/
render() { render() {
let { builds, unusedBuilds } = this.state; let { usedBuilds, unusedBuilds } = this.state;
let translate = this.context.language.translate; let translate = this.context.language.translate;
let availableBuilds = unusedBuilds.map((build, i) => let availableBuilds = unusedBuilds.map((build, i) =>
@@ -76,7 +102,7 @@ export default class ModalCompare extends TranslatedComponent {
</tr> </tr>
); );
let selectedBuilds = builds.map((build, i) => let selectedBuilds = usedBuilds.map((build, i) =>
<tr key={i} onClick={this._removeBuild.bind(this, i)}> <tr key={i} onClick={this._removeBuild.bind(this, i)}>
<td className='tl'>{build.name}</td>< <td className='tl'>{build.name}</td><
td className='tl'>{build.buildName}</td> td className='tl'>{build.buildName}</td>
@@ -102,7 +128,7 @@ export default class ModalCompare extends TranslatedComponent {
</div> </div>
<br/> <br/>
<button className='cap' onClick={this._selectBuilds.bind(this)}>{translate('Ok')}</button> <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>; </div>;
} }
} }

View File

@@ -1,22 +1,32 @@
import React from 'react'; import React from 'react';
import TranslatedComponent from './TranslatedComponent'; 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 { export default class ModalDeleteAll extends TranslatedComponent {
/**
* Delete everything and hide the modal
*/
_deleteAll() { _deleteAll() {
Persist.deleteAll(); Persist.deleteAll();
InterfaceEvents.hideModal(); this.context.hideModal();
} }
/**
* Renders the component
* @return {React.Component} Modal contents
*/
render() { render() {
let translate = this.context.language.translate; let translate = this.context.language.translate;
return <div className='modal' onClick={(e) => e.stopPropagation()}> return <div className='modal' onClick={(e) => e.stopPropagation()}>
<h2>{translate('delete all')}</h2> <h2>{translate('delete all')}</h2>
<p style={{textAlign: 'center'}}>{translate('PHRASE_CONFIRMATION')}</p> <p className='cen'>{translate('PHRASE_CONFIRMATION')}</p>
<button className='l cap' onClick={this._deleteAll}>{translate('yes')}</button> <button className='l cap' onClick={this._deleteAll.bind(this)}>{translate('yes')}</button>
<button className='r cap' onClick={InterfaceEvents.hideModal}>{translate('no')}</button> <button className='r cap' onClick={this.context.hideModal}>{translate('no')}</button>
</div>; </div>;
} }
} }

View File

@@ -1,20 +1,26 @@
import React from 'react'; import React from 'react';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents';
/**
* Export Modal
*/
export default class ModalExport extends TranslatedComponent { export default class ModalExport extends TranslatedComponent {
static propTypes = { static propTypes = {
title: React.PropTypes.string, 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]) data: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object, React.PropTypes.array])
}; };
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) { constructor(props) {
super(props); super(props);
let exportJson; let exportJson;
if (props.promise) { if (props.generator) {
exportJson = 'Generating...'; exportJson = 'Generating...';
} else if(typeof props.data == 'string') { } else if(typeof props.data == 'string') {
exportJson = props.data; exportJson = props.data;
@@ -25,16 +31,25 @@ export default class ModalExport extends TranslatedComponent {
this.state = { exportJson }; this.state = { exportJson };
} }
/**
* If generator is provided, execute on mount
*/
componentWillMount() { componentWillMount() {
// When promise is done update exportJson accordingly if (this.props.generator) {
this.props.generator((str) => this.setState({ exportJson: str }));
}
} }
/**
* Render the modal
* @return {React.Component} Modal Content
*/
render() { render() {
let translate = this.context.language.translate; let translate = this.context.language.translate;
let description; let description;
if (this.props.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() }> return <div className='modal' onClick={ (e) => e.stopPropagation() }>
@@ -43,7 +58,7 @@ export default class ModalExport extends TranslatedComponent {
<div> <div>
<textarea className='cb json' onFocus={ (e) => e.target.select() } readOnly value={this.state.exportJson} /> <textarea className='cb json' onFocus={ (e) => e.target.select() } readOnly value={this.state.exportJson} />
</div> </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>; </div>;
} }
} }

View File

@@ -1,28 +1,45 @@
import React from 'react'; import React from 'react';
import cn from 'classnames'; import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents';
import Persist from '../stores/Persist'; import Persist from '../stores/Persist';
import { Ships } from 'coriolis-data'; import { Ships } from 'coriolis-data';
import Ship from '../shipyard/Ship'; import Ship from '../shipyard/Ship';
import { ModuleNameToGroup } from '../shipyard/Constants';
import * as ModuleUtils from '../shipyard/ModuleUtils'; import * as ModuleUtils from '../shipyard/ModuleUtils';
import { fromDetailedBuild } from '../shipyard/Serializer';
import { Download } from './SvgIcons'; import { Download } from './SvgIcons';
const textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n'); const textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n');
const lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)'); const lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)');
const mountMap = { 'H': 4, 'L': 3, 'M': 2, 'S': 1, 'U': 0 }; const mountMap = { 'H': 4, 'L': 3, 'M': 2, 'S': 1, 'U': 0 };
const standardMap = { 'RB': 0, 'TM': 1, 'FH': 2, 'EC': 3, 'PC': 4, 'SS': 5, 'FS': 6 }; 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 }; 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) { function isEmptySlot(slot) {
return slot.maxClass == this && slot.m === null; 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) { function equalsIgnoreCase(str) {
return str.toLowerCase() == this.toLowerCase(); 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) { function validateBuild(shipId, code, name) {
let shipData = Ships[shipId]; 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) { function detailedJsonToBuild(detailedBuild) {
let ship; let ship;
if (!detailedBuild.name) { if (!detailedBuild.name) {
@@ -54,7 +76,7 @@ function detailedJsonToBuild(detailedBuild) {
} }
try { try {
ship = Serializer.fromDetailedBuild(detailedBuild); ship = fromDetailedBuild(detailedBuild);
} catch (e) { } catch (e) {
throw detailedBuild.ship + ' Build "' + detailedBuild.name + '": Invalid data'; 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() }; return { shipId: ship.id, name: detailedBuild.name, code: ship.toString() };
} }
/**
* Import Modal
*/
export default class ModalImport extends TranslatedComponent { export default class ModalImport extends TranslatedComponent {
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) { constructor(props) {
super(props); super(props);
@@ -86,6 +115,11 @@ export default class ModalImport extends TranslatedComponent {
this._validateImport = this._validateImport.bind(this); this._validateImport = this._validateImport.bind(this);
} }
/**
* Import a Coriolis backup
* @param {Object} importData Backup Data
* @throws {string} If import fails
*/
_importBackup(importData) { _importBackup(importData) {
if (importData.builds && typeof importData.builds == 'object') { if (importData.builds && typeof importData.builds == 'object') {
for (let shipId in importData.builds) { for (let shipId in importData.builds) {
@@ -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) { _importDetailedArray(importArr) {
let builds = {}; let builds = {};
for (let i = 0, l = importArr.length; i < l; i++) { for (let i = 0, l = importArr.length; i < l; i++) {
@@ -129,6 +167,11 @@ export default class ModalImport extends TranslatedComponent {
this.setState({ builds }); this.setState({ builds });
} }
/**
* Import a text build from ED Shipyard
* @param {string} buildStr Build string
* @throws {string} If parse / import fails
*/
_importTextBuild(buildStr) { _importTextBuild(buildStr) {
let buildName = textBuildRegex.exec(buildStr)[1].trim(); let buildName = textBuildRegex.exec(buildStr)[1].trim();
let shipName = buildName.toLowerCase(); 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 + '"'; } 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 + '"'; } 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); let hp = ModuleUtils.findHardpoint(group, cl, rating, group ? null : name, mount, missile);
if (!hp) { throw 'Unknown component: "' + line + '"'; } if (!hp) { throw 'Unknown component: "' + line + '"'; }
ship.use(slot, hp, true); ship.use(slot, hp, true);
} else if (typeSize == 'BH') { } else if (typeSize == 'BH') {
let bhId = bhMap[name.toLowerCase()]; let bhId = bhMap[name.toLowerCase()];
if (bhId === undefined) { throw 'Unknown bulkhead: "' + line + '"'; } if (bhId === undefined) { throw 'Unknown bulkhead: "' + line + '"'; }
ship.useBulkhead(bhId, true); ship.useBulkhead(bhId, true);
} else if (standardMap[typeSize] != undefined) { } else if (standardMap[typeSize] != undefined) {
let standardIndex = standardMap[typeSize]; let standardIndex = standardMap[typeSize];
if (ship.standard[standardIndex].maxClass < cl) { throw name + ' exceeds max class for the ' + ship.name; } 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); ship.use(ship.standard[standardIndex], cl + rating, ModuleUtils.standard(standardIndex, cl + rating), true);
} else { } else {
throw 'Unknown component: "' + line + '"'; throw 'Unknown component: "' + line + '"';
} }
} else { } else {
if (cl > typeSize) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; } 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 + '"'; } 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); let intComp = ModuleUtils.findInternal(group, cl, rating, group ? null : name);
@@ -221,13 +261,18 @@ export default class ModalImport extends TranslatedComponent {
let builds = {}; let builds = {};
builds[shipId] = {}; builds[shipId] = {};
builds[shipId]['Imported ' + buildName] = Serializer.fromShip(ship); builds[shipId]['Imported ' + buildName] = ship.toString();
this.setState({ builds }); 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 importData = null;
let importString = e.target.value; let importString = event.target.value;
this.setState({ this.setState({
builds: null, builds: null,
comparisons: null, comparisons: null,
@@ -244,7 +289,7 @@ export default class ModalImport extends TranslatedComponent {
try { try {
if (textBuildRegex.test(importString)) { // E:D Shipyard build text if (textBuildRegex.test(importString)) { // E:D Shipyard build text
importTextBuild(importString); this._importTextBuild(importString);
} else { // JSON Build data } else { // JSON Build data
importData = JSON.parse(importString); importData = JSON.parse(importString);
@@ -268,6 +313,9 @@ export default class ModalImport extends TranslatedComponent {
this.setState({ importValid: true }); this.setState({ importValid: true });
}; };
/**
* Process imported data
*/
_process() { _process() {
let builds = null, comparisons = null; let builds = null, comparisons = null;
@@ -278,7 +326,7 @@ export default class ModalImport extends TranslatedComponent {
let code = builds[shipId][buildName]; let code = builds[shipId][buildName];
// Update builds object such that orginal name retained, but can be renamed // Update builds object such that orginal name retained, but can be renamed
builds[shipId][buildName] = { builds[shipId][buildName] = {
code: code, code,
useName: buildName useName: buildName
}; };
} }
@@ -295,8 +343,10 @@ export default class ModalImport extends TranslatedComponent {
this.setState({ processed: true, builds, comparisons }); this.setState({ processed: true, builds, comparisons });
}; };
/**
* Import parsed, processed data and save
*/
_import() { _import() {
if (this.state.builds) { if (this.state.builds) {
let builds = this.state.builds; let builds = this.state.builds;
for (let shipId in builds) { for (let shipId in builds) {
@@ -329,9 +379,22 @@ export default class ModalImport extends TranslatedComponent {
Persist.setInsurance(this.state.insurance); 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() { componentWillMount() {
if (this.props.importingBuilds) { if (this.props.importingBuilds) {
this.setState({ builds: this.props.importingBuilds, canEdit : false }); this.setState({ builds: this.props.importingBuilds, canEdit : false });
@@ -339,6 +402,10 @@ export default class ModalImport extends TranslatedComponent {
} }
} }
/**
* Render the import modal
* @return {React.Component} Modal contents
*/
render() { render() {
let translate = this.context.language.translate; let translate = this.context.language.translate;
let state = this.state; let state = this.state;
@@ -357,16 +424,16 @@ export default class ModalImport extends TranslatedComponent {
if (state.comparisons) { if (state.comparisons) {
let comparisonRows = []; let comparisonRows = [];
for (let name in comparisons) { for (let name in state.comparisons) {
let comparison = comparisons[name]; let comparison = state.comparisons[name];
let hasComparison = Persist.hasComparison(name); let hasComparison = Persist.hasComparison(name);
comparisonRows.push( comparisonRows.push(
<tr key={name} className='cb'> <tr key={name} className='cb'>
<td> <td>
<input type='text' value={comparison.useName}/> <input type='text' value={comparison.useName}/>
</td> </td>
<td style={{ textAlign:'center' }} className={ cn({ warning: hasComparison, disabled: comparison.useName == '' }) }> <td style={{ textAlign:'center' }} className={ cn('cap', { warning: hasComparison, disabled: comparison.useName == '' }) }>
<span>{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}></span> {translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}>
</td> </td>
</tr> </tr>
); );
@@ -388,7 +455,7 @@ export default class ModalImport extends TranslatedComponent {
} }
if(this.state.canEdit) { 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; let builds = this.state.builds;
@@ -398,11 +465,11 @@ export default class ModalImport extends TranslatedComponent {
let b = shipBuilds[buildName]; let b = shipBuilds[buildName];
let hasBuild = Persist.hasBuild(shipId, b.useName); let hasBuild = Persist.hasBuild(shipId, b.useName);
buildRows.push( buildRows.push(
<tr className='cb'> <tr key={shipId + buildName} className='cb'>
<td>{Ships[shipId].properties.name}</td> <td>{Ships[shipId].properties.name}</td>
<td><input type='text' value={b.useName}/></td> <td><input type='text' onChange={this._changeBuildName.bind(this, b)} value={b.useName}/></td>
<td style={{ textAlign: 'center' }} className={cn({ warning: hasBuild, disabled: b.useName == ''})}> <td style={{ textAlign: 'center' }} className={cn('cap', { warning: hasBuild, disabled: b.useName == '' })}>
<span>{translate(b.useName == '' ? 'skip' : (hasBuild ? 'overwrite' : 'create'))}></span> {translate(b.useName == '' ? 'skip' : (hasBuild ? 'overwrite' : 'create'))}
</td> </td>
</tr> </tr>
); );
@@ -433,7 +500,7 @@ export default class ModalImport extends TranslatedComponent {
return <div className='modal' onClick={ (e) => e.stopPropagation() }> return <div className='modal' onClick={ (e) => e.stopPropagation() }>
<h2 >{translate('import')}</h2> <h2 >{translate('import')}</h2>
{importStage} {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>; </div>;
} }
} }

View File

@@ -1,13 +1,20 @@
import React from 'react'; import React from 'react';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents'; import ShortenUrl from '../utils/ShortenUrl';
/**
* Permalink modal
*/
export default class ModalPermalink extends TranslatedComponent { export default class ModalPermalink extends TranslatedComponent {
static propTypes = { static propTypes = {
url: React.propTypes.string.isRequired url: React.PropTypes.string.isRequired
}; };
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) { constructor(props) {
super(props); super(props);
@@ -16,13 +23,20 @@ export default class ModalPermalink extends TranslatedComponent {
}; };
} }
/**
* Shorten URL on mount
*/
componentWillMount() { componentWillMount() {
ShortenUrl(this.props.url, ShortenUrl(this.props.url,
(shortenedUrl) => this.setState({ shortenedUrl }), (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() { render() {
let translate = this.context.language.translate; let translate = this.context.language.translate;
@@ -30,12 +44,12 @@ export default class ModalPermalink extends TranslatedComponent {
<h2>{translate('permalink')}</h2> <h2>{translate('permalink')}</h2>
<br/> <br/>
<h3>{translate('URL')}</h3> <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/> <br/><br/>
<h3 >{translate('shortened')}</h3> <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/> <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>; </div>;
} }
} }

View File

@@ -2,23 +2,34 @@ import React from 'react';
import d3 from 'd3'; import d3 from 'd3';
import cn from 'classnames'; import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import { wrapCtxMenu } from '../utils/InterfaceEvents'; import { wrapCtxMenu } from '../utils/UtilityFunctions';
/** /**
* Round to avoid floating point precision errors * Round to avoid floating point precision errors
* @param {[type]} selected [description] * @param {Boolean} selected Band selected
* @param {[type]} sum [description] * @param {number} sum Band power sum
* @param {[type]} avail [description] * @param {number} avail Total available power
* @return {[type]} [description] * @return {string} CSS Class name
*/ */
function getClass(selected, sum, avail) { function getClass(selected, sum, avail) {
return selected ? 'secondary' : ((Math.round(sum * 100) / 100) >= avail) ? 'warning' : 'primary'; 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) { function bandText(val, index, wattScale) {
return (val > 0 && wattScale(val) > 13) ? index + 1 : null; 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 { export default class PowerBands extends TranslatedComponent {
static propTypes = { static propTypes = {
@@ -28,6 +39,11 @@ export default class PowerBands extends TranslatedComponent {
code: React.PropTypes.string, code: React.PropTypes.string,
}; };
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) { constructor(props, context) {
super(props); super(props);
this.wattScale = d3.scale.linear(); this.wattScale = d3.scale.linear();
@@ -38,6 +54,7 @@ export default class PowerBands extends TranslatedComponent {
this._updateDimensions = this._updateDimensions.bind(this); this._updateDimensions = this._updateDimensions.bind(this);
this._updateScales = this._updateScales.bind(this); this._updateScales = this._updateScales.bind(this);
this._selectNone = this._selectNone.bind(this); this._selectNone = this._selectNone.bind(this);
this._hidetip = () => this.context.tooltip();
let maxBand = props.bands[props.bands.length - 1]; 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 innerHeight = (barHeight * 2) + 2;
let mTop = Math.round(25 * size); let mTop = Math.round(25 * scale);
let mBottom = Math.round(25 * size); let mBottom = Math.round(25 * scale);
let mLeft = Math.round(45 * size); let mLeft = Math.round(45 * scale);
let mRight = Math.round(140 * size); let mRight = Math.round(140 * scale);
let innerWidth = props.width - mLeft - mRight; let innerWidth = props.width - mLeft - mRight;
this._updateScales(innerWidth, this.state.maxPwr, props.available); this._updateScales(innerWidth, this.state.maxPwr, props.available);
@@ -77,6 +99,9 @@ export default class PowerBands extends TranslatedComponent {
}); });
} }
/**
* Select no bands
*/
_selectNone() { _selectNone() {
this.setState({ this.setState({
ret : {}, ret : {},
@@ -84,6 +109,10 @@ export default class PowerBands extends TranslatedComponent {
}); });
} }
/**
* Select a retracted band
* @param {number} index Band index
*/
_selectRet(index) { _selectRet(index) {
let ret = this.state.ret; let ret = this.state.ret;
if(ret[index]) { if(ret[index]) {
@@ -95,6 +124,10 @@ export default class PowerBands extends TranslatedComponent {
this.setState({ ret: Object.assign({}, ret) }); this.setState({ ret: Object.assign({}, ret) });
} }
/**
* Select a deployed band
* @param {number} index Band index
*/
_selectDep(index) { _selectDep(index) {
let dep = this.state.dep; let dep = this.state.dep;
@@ -107,31 +140,45 @@ export default class PowerBands extends TranslatedComponent {
this.setState({ dep: Object.assign({}, dep) }); this.setState({ dep: Object.assign({}, dep) });
} }
/**
* Update scale
* @param {number} innerWidth SVG innerwidth
* @param {number} maxPwr Maximum power level MJ (deployed or available)
* @param {number} available Available power MJ
*/
_updateScales(innerWidth, maxPwr, available) { _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); 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) { componentWillReceiveProps(nextProps, nextContext) {
let { innerWidth, maxPwr } = this.state; let { innerWidth, maxPwr } = this.state;
let { language, sizeRatio } = this.context;
let maxBand = nextProps.bands[nextProps.bands.length - 1]; let maxBand = nextProps.bands[nextProps.bands.length - 1];
let nextMaxPwr = Math.max(nextProps.available, maxBand.retractedSum, maxBand.deployedSum); let nextMaxPwr = Math.max(nextProps.available, maxBand.retractedSum, maxBand.deployedSum);
if (maxPwr != nextMaxPwr) { // Update Axes if max power has changed if (language !== nextContext.language) {
this._updateScales(innerWidth, nextMaxPwr, nextProps.available);
this.setState({ maxPwr: nextMaxPwr });
}
if (this.context !== nextContext) {
this.wattAxis.tickFormat(nextContext.language.formats.r2); this.wattAxis.tickFormat(nextContext.language.formats.r2);
this.pctAxis.tickFormat(nextContext.language.formats.rPct); this.pctAxis.tickFormat(nextContext.language.formats.rPct);
} }
if (nextProps.width != this.props.width || 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); this._updateDimensions(nextProps, nextContext.sizeRatio);
} }
} }
/**
* Render the power bands
* @return {React.Component} Power bands
*/
render() { render() {
if (!this.props.width) { if (!this.props.width) {
return null; return null;
@@ -150,7 +197,7 @@ export default class PowerBands extends TranslatedComponent {
let retSum = 0; let retSum = 0;
let depSum = 0; let depSum = 0;
for (var i = 0; i < bands.length; i++) { for (let i = 0; i < bands.length; i++) {
let b = bands[i]; let b = bands[i];
retSum += (!retSelected || ret[i]) ? b.retracted : 0; retSum += (!retSelected || ret[i]) ? b.retracted : 0;
depSum += (!depSelected || dep[i]) ? b.deployed + b.retracted : 0; depSum += (!depSelected || dep[i]) ? b.deployed + b.retracted : 0;
@@ -220,11 +267,10 @@ export default class PowerBands extends TranslatedComponent {
let axis = d3.select(elem); let axis = d3.select(elem);
axis.call(this.pctAxis); axis.call(this.pctAxis);
axis.select('g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass); axis.select('g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass);
}} }} className='pct axis' transform={`translate(0,${state.innerHeight})`}></g>
className='pct axis' transform={`translate(0,${state.innerHeight})`}></g>
<line x1={pctScale(0.5)} x2={pctScale(0.5)} y1='0' y2={state.innerHeight} className={pwrWarningClass} /> <line x1={pctScale(0.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.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'>{translate('dep')}</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.retY} className={getClass(retSelected, retSum, available)}>{f2(Math.max(0, retSum)) + ' (' + pct1(Math.max(0, retSum / available)) + ')'}</text>
<text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, available)}>{f2(Math.max(0, depSum)) + ' (' + pct1(Math.max(0, depSum / available)) + ')'}</text> <text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, available)}>{f2(Math.max(0, depSum)) + ' (' + pct1(Math.max(0, depSum / available)) + ')'}</text>
</g> </g>

View File

@@ -2,9 +2,8 @@ import React from 'react';
import { findDOMNode } from 'react-dom'; import { findDOMNode } from 'react-dom';
import cn from 'classnames'; import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents';
import PowerBands from './PowerBands'; import PowerBands from './PowerBands';
import { slotName, nameComparator } from '../utils/SlotFunctions'; import { slotName, slotComparator } from '../utils/SlotFunctions';
import { Power, NoPower } from './SvgIcons'; import { Power, NoPower } from './SvgIcons';
const POWER = [ const POWER = [
@@ -14,6 +13,9 @@ const POWER = [
<Power className='secondary-disabled' /> <Power className='secondary-disabled' />
]; ];
/**
* Power Management Section
*/
export default class PowerManagement extends TranslatedComponent { export default class PowerManagement extends TranslatedComponent {
static PropTypes = { static PropTypes = {
ship: React.PropTypes.object.isRequired, ship: React.PropTypes.object.isRequired,
@@ -21,6 +23,10 @@ export default class PowerManagement extends TranslatedComponent {
onChange: React.PropTypes.func.isRequired onChange: React.PropTypes.func.isRequired
}; };
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) { constructor(props) {
super(props); super(props);
this._renderPowerRows = this._renderPowerRows.bind(this); 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) { _sortOrder(predicate) {
let desc = this.state.desc; let desc = this.state.desc;
if (predicate == this.state.predicate) { if (predicate == this.state.predicate) {
desc = !desc; desc = !desc;
} else { } else {
desc = true; desc = true;
} }
this._sort(this.props.ship, predicate, desc);
this.setState({ 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) { _sort(ship, predicate, desc) {
let powerList = ship.powerList; let powerList = ship.powerList;
let comp = slotComparator.bind(null, this.context.language.translate);
switch (predicate) { switch (predicate) {
case 'n': powerList.sort(nameComparator(this.context.language.translate)); break; case 'n': comp = comp(null, desc); break;
case 't': powerList.sort((a, b) => a.type.localeCompare(b.type)); break; case 't': comp = comp((a, b) => a.type.localeCompare(b.type), desc); break;
case 'pri': powerList.sort((a, b) => a.priority - b.priority);break; case 'pri': comp = comp((a, b) => a.priority - b.priority, desc); break;
case 'pwr': powerList.sort((a, b) => (a.m ? a.m.power : 0) - (b.m ? b.m.power : 0)); break; case 'pwr': comp = comp((a, b) => a.m.power - b.m.power, desc); break;
case 'r': powerList.sort((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b)); break; case 'r': comp = comp((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b), desc); break;
case 'd': powerList.sort((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true)); break; case 'd': comp = comp((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true), desc); break;
} }
if (!desc) { powerList.sort(comp);
powerList.reverse();
}
} }
/**
* Update slot priority
* @param {Object} slot Slot model
* @param {number} inc increment / decrement
*/
_priority(slot, inc) { _priority(slot, inc) {
if (this.props.ship.setSlotPriority(slot, slot.priority + inc)) { if (this.props.ship.setSlotPriority(slot, slot.priority + inc)) {
this.props.onChange(); this.props.onChange();
} }
} }
/**
* Toggle slot active/inactive
* @param {Object} slot Slot model
*/
_toggleEnabled(slot) { _toggleEnabled(slot) {
this.props.ship.setSlotEnabled(slot, !slot.enabled); this.props.ship.setSlotEnabled(slot, !slot.enabled);
this.props.onChange(); 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) { _renderPowerRows(ship, translate, pwr, pct) {
let powerRows = []; 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]; let slot = ship.powerList[i];
if (slot.m && slot.m.power) { if (slot.m && slot.m.power) {
@@ -112,32 +144,50 @@ export default class PowerManagement extends TranslatedComponent {
return powerRows; return powerRows;
} }
/**
* Update power bands width from DOM
*/
_updateWidth() { _updateWidth() {
this.setState({ width: findDOMNode(this).offsetWidth }); this.setState({ width: findDOMNode(this).offsetWidth });
} }
/**
* Add listeners when about to mount and sort power list
*/
componentWillMount() { componentWillMount() {
this._sort(this.props.ship, this.state.predicate, this.state.desc); 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() { componentDidMount() {
this._updateWidth(); this._updateWidth();
} }
/**
* Sort power list if the ship instance has changed
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextState Incoming/Next state
*/
componentWillUpdate(nextProps, nextState) { componentWillUpdate(nextProps, nextState) {
// Can optimize this later: only sort when if (this.props.ship != nextProps.ship) {
// - 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); this._sort(nextProps.ship, nextState.predicate, nextState.desc);
} }
}
/**
* Remove listeners on unmount
*/
componentWillUnmount() { componentWillUnmount() {
this.resizeListener.remove(); this.resizeListener.remove();
} }
/**
* Render power management section
* @return {React.Component} contents
*/
render() { render() {
let { ship, code } = this.props; let { ship, code } = this.props;
let { translate, formats } = this.context.language; let { translate, formats } = this.context.language;

View File

@@ -4,39 +4,46 @@ import cn from 'classnames';
import { SizeMap } from '../shipyard/Constants'; import { SizeMap } from '../shipyard/Constants';
import { Warning } from './SvgIcons'; import { Warning } from './SvgIcons';
/**
* Ship Summary Table / Stats
*/
export default class ShipSummaryTable extends TranslatedComponent { export default class ShipSummaryTable extends TranslatedComponent {
static propTypes = { static propTypes = {
ship: React.PropTypes.object.isRequired ship: React.PropTypes.object.isRequired
} };
/**
* Render the table
* @return {React.Component} Summary table
*/
render() { render() {
let ship = this.props.ship; let ship = this.props.ship;
let language = this.context.language; let { language, tooltip, termtip } = this.context;
let translate = language.translate; let translate = language.translate;
let u = language.units; let u = language.units;
let formats = language.formats; let formats = language.formats;
let round = formats.round; let round = formats.round;
let int = formats.int; let int = formats.int;
let armourDetails = null; let armourDetails = null;
let hide = tooltip.bind(null, null);
if (ship.armourMultiplier > 1 || ship.armourAdded) { if (ship.armourMultiplier > 1 || ship.armourAdded) {
armourDetails = <u>({ armourDetails = <u>({
(ship.armourMultiplier > 1 ? formats.rPct(ship.armourMultiplier) : '') (ship.armourMultiplier > 1 ? formats.rPct(ship.armourMultiplier) : '') +
+ (ship.armourAdded ? ' + ' + ship.armourAdded : '') (ship.armourAdded ? ' + ' + ship.armourAdded : '')
})</u>; })</u>;
} }
return ( return <div id='summary'>
<div id='summary'>
<table id='summaryTable'> <table id='summaryTable'>
<thead> <thead>
<tr className='main'> <tr className='main'>
<th rowSpan={2}>{translate('size')}</th> <th rowSpan={2}>{translate('size')}</th>
<th rowSpan={2}>{translate('agility')}</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.canThrust() }) }>{translate('speed')}</th>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canBoost() }) }>{translate('boost')}</th> <th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canBoost() }) }>{translate('boost')}</th>
<th rowSpan={2}>{translate('DPS')}</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('armour')}</th>
<th rowSpan={2}>{translate('shields')}</th> <th rowSpan={2}>{translate('shields')}</th>
<th colSpan={3}>{translate('mass')}</th> <th colSpan={3}>{translate('mass')}</th>
@@ -44,7 +51,7 @@ export default class ShipSummaryTable extends TranslatedComponent {
<th rowSpan={2}>{translate('fuel')}</th> <th rowSpan={2}>{translate('fuel')}</th>
<th colSpan={3}>{translate('jump range')}</th> <th colSpan={3}>{translate('jump range')}</th>
<th colSpan={3}>{translate('total range')}</th> <th colSpan={3}>{translate('total range')}</th>
<th rowSpan={2}>{translate('lock factor')}</th> <th onMouseOver={termtip.bind(null, 'mass lock factor')} onMouseOut={hide} rowSpan={2}>{translate('MLF')}</th>
</tr> </tr>
<tr> <tr>
<th className='lft'>{translate('hull')}</th> <th className='lft'>{translate('hull')}</th>
@@ -82,8 +89,6 @@ export default class ShipSummaryTable extends TranslatedComponent {
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>;
);
} }
} }

View File

@@ -1,5 +1,8 @@
import React from 'react'; import React from 'react';
/**
* Horizontal Slider
*/
export default class Slider extends React.Component { export default class Slider extends React.Component {
static defaultProps = { static defaultProps = {
@@ -16,6 +19,10 @@ export default class Slider extends React.Component {
onChange: React.PropTypes.func.isRequired, onChange: React.PropTypes.func.isRequired,
}; };
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) { constructor(props) {
super(props); super(props);
@@ -23,27 +30,47 @@ export default class Slider extends React.Component {
this.up = this.up.bind(this); this.up = this.up.bind(this);
} }
/**
* On Mouse down handler
* @param {SyntheticEvent} e Event
*/
down(e) { down(e) {
let rect = e.currentTarget.getBoundingClientRect(); 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); this.move(e);
document.addEventListener("mousemove", this.move); document.addEventListener('mousemove', this.move);
document.addEventListener("mouseup", this.up); document.addEventListener('mouseup', this.up);
} }
/**
* On Mouse up handler
*/
up() { up() {
document.removeEventListener("mousemove", this.move); document.removeEventListener('mousemove', this.move);
document.removeEventListener("mouseup", this.up); document.removeEventListener('mouseup', this.up);
} }
/**
* 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() { componentWillUnmount() {
this.up(); this.up();
} }
updatePercent(left, width, event) { /**
this.props.onChange(Math.min(Math.max((event.clientX - left) / width, 0), 1)); * Render the slider
} * @return {React.Component} The slider
*/
render() { render() {
let pctStr = (this.props.percent * 100) + '%'; let pctStr = (this.props.percent * 100) + '%';
let { axis, axisUnit, min, max } = this.props; let { axis, axisUnit, min, max } = this.props;
@@ -51,9 +78,9 @@ export default class Slider extends React.Component {
if (axis) { if (axis) {
axisGroup = <g style={{ fontSize: '.7em' }}> 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='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='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='99%' style={{ textAnchor: 'middle' }}>{max + axisUnit}</text>
</g>; </g>;
} }

View File

@@ -2,8 +2,12 @@ import React from 'react';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import cn from 'classnames'; import cn from 'classnames';
import AvailableModulesMenu from './AvailableModulesMenu'; 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 { export default class Slot extends TranslatedComponent {
static propTypes = { static propTypes = {
@@ -13,10 +17,17 @@ export default class Slot extends TranslatedComponent {
maxClass: React.PropTypes.number.isRequired, maxClass: React.PropTypes.number.isRequired,
selected: React.PropTypes.bool, selected: React.PropTypes.bool,
m: React.PropTypes.object, m: React.PropTypes.object,
shipMass: React.PropTypes.number, ship: React.PropTypes.object.isRequired,
warning: React.PropTypes.func, 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) { constructor(props) {
super(props); super(props);
@@ -27,6 +38,11 @@ export default class Slot extends TranslatedComponent {
// Must be implemented by subclasses: // Must be implemented by subclasses:
// _getSlotDetails() // _getSlotDetails()
/**
* Get the CSS class name for the slot. Can/should be overriden
* as necessary.
* @return {string} CSS Class name
*/
_getClassNames() { _getClassNames() {
return null; return null;
} }
@@ -40,14 +56,22 @@ export default class Slot extends TranslatedComponent {
return this.props.maxClass; return this.props.maxClass;
} }
/**
* Empty slot on right-click
* @param {SyntheticEvent} event Event
*/
_contextMenu(event) { _contextMenu(event) {
this.props.onSelect(null,null); this.props.onSelect(null,null);
} }
/**
* Render the slot
* @return {React.Component} The slot
*/
render() { render() {
let language = this.context.language; let language = this.context.language;
let translate = language.translate; 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; let slotDetails, menu;
if (m) { if (m) {
@@ -59,18 +83,19 @@ export default class Slot extends TranslatedComponent {
if (this.props.selected) { if (this.props.selected) {
menu = <AvailableModulesMenu menu = <AvailableModulesMenu
className={this._getClassNames()} className={this._getClassNames()}
modules={this.props.availableModules()} modules={availableModules()}
shipMass={this.props.shipMass} shipMass={ship.hullMass}
m={m} m={m}
onSelect={this.props.onSelect} onSelect={onSelect}
warning={this.props.warning} warning={warning}
diffDetails={diffDetails.bind(ship, this.context.language)}
/>; />;
} }
return ( return (
<div className={cn('slot', {selected: this.props.selected})} onClick={this.props.onOpen} onContextMenu={this._contextMenu}> <div className={cn('slot', dropClass, { selected })} onClick={onOpen} onContextMenu={this._contextMenu} onDragOver={dragOver}>
<div className={'details'}> <div className='details-container'>
<div className={'sz'}>{this._getMaxClassLabel(translate)}</div> <div className='sz'>{this._getMaxClassLabel(translate)}</div>
{slotDetails} {slotDetails}
</div> </div>
{menu} {menu}

View File

@@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents'; import { wrapCtxMenu } from '../utils/UtilityFunctions';
import { wrapCtxMenu } from '../utils/InterfaceEvents';
import { Equalizer } from '../components/SvgIcons'; import { Equalizer } from '../components/SvgIcons';
import cn from 'classnames'; import cn from 'classnames';
/**
* Abstract Slot Section
*/
export default class SlotSection extends TranslatedComponent { export default class SlotSection extends TranslatedComponent {
static propTypes = { static propTypes = {
@@ -14,6 +16,13 @@ export default class SlotSection extends TranslatedComponent {
togglePwr: React.PropTypes.func 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) { constructor(props, context, sectionId, sectionName) {
super(props); super(props);
this.sectionId = sectionId; this.sectionId = sectionId;
@@ -23,7 +32,10 @@ export default class SlotSection extends TranslatedComponent {
this._selectModule = this._selectModule.bind(this); this._selectModule = this._selectModule.bind(this);
this._getSectionMenu = this._getSectionMenu.bind(this); this._getSectionMenu = this._getSectionMenu.bind(this);
this._contextMenu = this._contextMenu.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._close = this._close.bind(this);
this.state = {};
} }
// Must be implemented by subclasses: // Must be implemented by subclasses:
@@ -31,33 +43,143 @@ export default class SlotSection extends TranslatedComponent {
// _getSectionMenu() // _getSectionMenu()
// _contextMenu() // _contextMenu()
/**
* Open a menu
* @param {string} menu Menu name
* @param {SyntheticEvent} event Event
*/
_openMenu(menu, event) { _openMenu(menu, event) {
event.stopPropagation(); event.stopPropagation();
if (this.props.currentMenu === menu) { if (this.props.currentMenu === menu) {
menu = null; 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) { _selectModule(slot, m) {
this.props.ship.use(slot, m); this.props.ship.use(slot, m);
this.props.onChange(); this.props.onChange();
this._close(); 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) { _togglePwr(slot) {
this.props.ship.setSlotEnabled(slot, !slot.enabled); this.props.ship.setSlotEnabled(slot, !slot.enabled);
this.props.onChange(); this.props.onChange();
this._close(); this._close();
} }
/**
* Close current menu
*/
_close() { _close() {
if (this.props.currentMenu) { if (this.props.currentMenu) {
InterfaceEvents.closeMenu(); this.context.closeMenu();
} }
} }
/**
* Render the slot section
* @return {React.Component} Slot section
*/
render() { render() {
let translate = this.context.language.translate; let translate = this.context.language.translate;
let sectionMenuOpened = this.props.currentMenu === this.sectionName; let sectionMenuOpened = this.props.currentMenu === this.sectionName;
@@ -65,7 +187,7 @@ export default class SlotSection extends TranslatedComponent {
let ctx = wrapCtxMenu(this._contextMenu); let ctx = wrapCtxMenu(this._contextMenu);
return ( return (
<div id={this.sectionId} className={'group'}> <div id={this.sectionId} className={'group'} onDragLeave={this._dragOverNone}>
<div className={cn('section-menu', { selected: sectionMenuOpened })} onClick={open} onContextMenu={ctx}> <div className={cn('section-menu', { selected: sectionMenuOpened })} onClick={open} onContextMenu={ctx}>
<h1>{translate(this.sectionName)} <Equalizer/></h1> <h1>{translate(this.sectionName)} <Equalizer/></h1>
{sectionMenuOpened ? this._getSectionMenu(translate) : null } {sectionMenuOpened ? this._getSectionMenu(translate) : null }

View File

@@ -1,8 +1,13 @@
import React from 'react'; import React from 'react';
import cn from 'classnames'; import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import { jumpRange } from '../shipyard/Calculations';
import { diffDetails } from '../utils/SlotFunctions';
import AvailableModulesMenu from './AvailableModulesMenu'; import AvailableModulesMenu from './AvailableModulesMenu';
/**
* Standard Slot
*/
export default class StandardSlot extends TranslatedComponent { export default class StandardSlot extends TranslatedComponent {
static propTypes = { static propTypes = {
@@ -10,46 +15,51 @@ export default class StandardSlot extends TranslatedComponent {
modules: React.PropTypes.array.isRequired, modules: React.PropTypes.array.isRequired,
onSelect: React.PropTypes.func.isRequired, onSelect: React.PropTypes.func.isRequired,
onOpen: React.PropTypes.func.isRequired, onOpen: React.PropTypes.func.isRequired,
ship: React.PropTypes.object.isRequired,
selected: React.PropTypes.bool, selected: React.PropTypes.bool,
shipMass: React.PropTypes.number,
warning: React.PropTypes.func, warning: React.PropTypes.func,
}; };
/**
* Render the slot
* @return {React.Component} Slot component
*/
render() { render() {
let { translate, formats, units } = this.context.language; 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 m = slot.m;
let classRating = m.class + m.rating; let classRating = m.class + m.rating;
let menu; let menu;
if (this.props.selected) { if (this.props.selected) {
menu = <AvailableModulesMenu menu = <AvailableModulesMenu
modules={this.props.modules} modules={modules}
shipMass={this.props.shipMass} shipMass={ship.ladenMass}
m={m} m={m}
onSelect={this.props.onSelect} onSelect={onSelect}
warning={this.props.warning} warning={warning}
diffDetails={diffDetails.bind(ship, this.context.language)}
/>; />;
} }
return ( return (
<div className={cn('slot', { selected: this.props.selected })} onClick={this.props.onOpen}> <div className={cn('slot', { selected: this.props.selected })} onClick={this.props.onOpen}>
<div className={cn('details', {warning: warning && warning(slot.m)})}> <div className={cn('details-container', { warning: warning && warning(slot.m) })}>
<div className={'sz'}>{slot.maxClass}</div> <div className={'sz'}>{slot.maxClass}</div>
<div> <div>
<div className={'l'}>{classRating + ' ' + translate(m.grp)}</div> <div className='l'>{classRating + ' ' + translate(m.grp)}</div>
<div className={'r'}>{m.mass || m.capacity}{units.T}</div> <div className={'r'}>{m.mass || m.fuel}{units.T}</div>
<div className={'cb'}> <div className={'cb'}>
{ m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{units.T}</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.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.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.time ? <div className='l'>{translate('time')}: {formats.time(m.time)}</div> : null }
{ m.eff ? <div className={'l'}>{translate('efficiency')}: {m.eff}</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.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.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.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.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.enginecapacity ? <div className='l'>{translate('ENG')}: {m.enginecapacity}{units.MJ} / {m.enginerecharge}{units.MW}</div> : null }
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,41 +2,61 @@ import React from 'react';
import cn from 'classnames'; import cn from 'classnames';
import SlotSection from './SlotSection'; import SlotSection from './SlotSection';
import StandardSlot from './StandardSlot'; import StandardSlot from './StandardSlot';
import { diffDetails } from '../utils/SlotFunctions';
import * as ModuleUtils from '../shipyard/ModuleUtils'; import * as ModuleUtils from '../shipyard/ModuleUtils';
/**
* Standard Slot section
*/
export default class StandardSlotSection extends SlotSection { export default class StandardSlotSection extends SlotSection {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) { constructor(props, context) {
super(props, context, 'standard', 'standard'); super(props, context, 'standard', 'standard');
this._optimizeStandard = this._optimizeStandard.bind(this); this._optimizeStandard = this._optimizeStandard.bind(this);
this._optimizeCargo = this._optimizeCargo.bind(this); this._optimizeCargo = this._optimizeCargo.bind(this);
this._optimizeExplorer = this._optimizeExplorer.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) { _fill(rating) {
this.props.ship.useStandard(rating); this.props.ship.useStandard(rating);
this.props.onChange(); this.props.onChange();
this._close(); this._close();
} }
/**
* Use the lightest/optimal available standard modules
*/
_optimizeStandard() { _optimizeStandard() {
this.props.ship.useLightestStandard(); this.props.ship.useLightestStandard();
this.props.onChange(); this.props.onChange();
this._close(); this._close();
} }
/**
* Trader build
*/
_optimizeCargo() { _optimizeCargo() {
let ship = this.props.ship; let ship = this.props.ship;
ship.internal.forEach((slot) => { ship.internal.forEach((slot) => ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E')));
var id = ModuleUtils.findInternalId('cr', slot.maxClass, 'E');
ship.use(slot, ModuleUtils.internal(id));
});
ship.useLightestStandard(); ship.useLightestStandard();
this.props.onChange(); this.props.onChange();
this._close(); this._close();
} }
/**
* Explorer build
*/
_optimizeExplorer() { _optimizeExplorer() {
let ship = this.props.ship, let ship = this.props.ship,
intLength = ship.internal.length, intLength = ship.internal.length,
@@ -68,7 +88,6 @@ export default class StandardSlotSection extends SlotSection {
let am = ModuleUtils.findInternal('am', slot.maxClass, afmUnitCount ? 'B' : 'A'); let am = ModuleUtils.findInternal('am', slot.maxClass, afmUnitCount ? 'B' : 'A');
ship.use(slot, am); ship.use(slot, am);
ship.setSlotEnabled(slot, false); // Disabled power for AFM Unit ship.setSlotEnabled(slot, false); // Disabled power for AFM Unit
} else { } else {
ship.use(slot, null); ship.use(slot, null);
} }
@@ -98,46 +117,89 @@ export default class StandardSlotSection extends SlotSection {
this._close(); this._close();
} }
/**
* Use the specified bulkhead
* @param {number} bulkheadIndex 0 - 4
*/
_selectBulkhead(bulkheadIndex) { _selectBulkhead(bulkheadIndex) {
this.props.ship.useBulkhead(bulkheadIndex); this.props.ship.useBulkhead(bulkheadIndex);
this.context.tooltip();
this.props.onChange(); this.props.onChange();
this._close(); this._close();
} }
/**
* On right click optimize the standard modules
*/
_contextMenu() { _contextMenu() {
this._optimizeStandard(); 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() { _getSlots() {
let { translate, units } = this.context.language; let { translate, units } = this.context.language;
let { ship, currentMenu } = this.props;
let slots = new Array(8); let slots = new Array(8);
let open = this._openMenu; let open = this._openMenu;
let select = this._selectModule; let select = this._selectModule;
let selBulkhead = this._selectBulkhead; let selBulkhead = this._selectBulkhead;
let ship = this.props.ship
let st = ship.standard; let st = ship.standard;
let avail = ship.getAvailableModules().standard; let avail = ship.getAvailableModules().standard;
let bulkheads = ship.bulkheads; let bh = ship.bulkheads;
let currentMenu = this.props.currentMenu;
slots[0] = ( slots[0] = (
<div key='bh' className={cn('slot', {selected: currentMenu === bulkheads})} onClick={open.bind(this, bulkheads)}> <div key='bh' className={cn('slot', { selected: currentMenu === bh })} onClick={open.bind(this, bh)}>
<div className={'details-container'}>
<div className={'details'}> <div className={'details'}>
<div className={'sz'}>8</div> <div className={'sz'}>8</div>
<div> <div>
<div className={'l'}>{translate('bh')}</div> <div className={'l'}>{translate('bh')}</div>
<div className={'r'}>{bulkheads.m.mass}{units.T}</div> <div className={'r'}>{bh.m.mass}{units.T}</div>
<div className={'cl l'}>{translate(bulkheads.m.name)}</div> <div className={'cl l'}>{translate(bh.m.name)}</div>
</div> </div>
</div> </div>
{currentMenu === bulkheads && </div>
{currentMenu === bh &&
<div className='select' onClick={ e => e.stopPropagation() }> <div className='select' onClick={ e => e.stopPropagation() }>
<ul> <ul>
<li onClick={selBulkhead.bind(this, 0)} className={cn('lc', { active: bulkheads.id == '0' })}>{translate('Lightweight Alloy')}</li> <li onClick={selBulkhead.bind(this, 0)} onMouseOver={this._bhDiff.bind(this, 0)} onMouseLeave={this._hideDiff} className={cn('lc', { active: bh.index == 0 })}>
<li onClick={selBulkhead.bind(this, 1)} className={cn('lc', { active: bulkheads.id == '1' })}>{translate('Reinforced Alloy')}</li> {translate('Lightweight Alloy')}
<li onClick={selBulkhead.bind(this, 2)} className={cn('lc', { active: bulkheads.id == '2' })}>{translate('Military Grade Composite')}</li> </li>
<li onClick={selBulkhead.bind(this, 3)} className={cn('lc', { active: bulkheads.id == '3' })}>{translate('Mirrored Surface Composite')}</li> <li onClick={selBulkhead.bind(this, 1)} onMouseOver={this._bhDiff.bind(this, 1)} onMouseLeave={this._hideDiff} className={cn('lc', { active: bh.index == 1 })}>
<li onClick={selBulkhead.bind(this, 4)} className={cn('lc', { active: bulkheads.id == '4' })}>{translate('Reactive Surface Composite')}</li> {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> </ul>
</div> </div>
} }
@@ -151,6 +213,7 @@ export default class StandardSlotSection extends SlotSection {
onOpen={open.bind(this, st[0])} onOpen={open.bind(this, st[0])}
onSelect={select.bind(this, st[0])} onSelect={select.bind(this, st[0])}
selected={currentMenu == st[0]} selected={currentMenu == st[0]}
ship={ship}
warning={m => m.pGen < ship.powerRetracted} warning={m => m.pGen < ship.powerRetracted}
/>; />;
@@ -161,6 +224,7 @@ export default class StandardSlotSection extends SlotSection {
onOpen={open.bind(this, st[1])} onOpen={open.bind(this, st[1])}
onSelect={select.bind(this, st[1])} onSelect={select.bind(this, st[1])}
selected={currentMenu == st[1]} selected={currentMenu == st[1]}
ship={ship}
warning={m => m.maxmass < ship.ladenMass} warning={m => m.maxmass < ship.ladenMass}
/>; />;
@@ -171,6 +235,7 @@ export default class StandardSlotSection extends SlotSection {
modules={avail[2]} modules={avail[2]}
onOpen={open.bind(this, st[2])} onOpen={open.bind(this, st[2])}
onSelect={select.bind(this, st[2])} onSelect={select.bind(this, st[2])}
ship={ship}
selected={currentMenu == st[2]} selected={currentMenu == st[2]}
/>; />;
@@ -180,6 +245,7 @@ export default class StandardSlotSection extends SlotSection {
modules={avail[3]} modules={avail[3]}
onOpen={open.bind(this, st[3])} onOpen={open.bind(this, st[3])}
onSelect={select.bind(this, st[3])} onSelect={select.bind(this, st[3])}
ship={ship}
selected={currentMenu == st[3]} selected={currentMenu == st[3]}
/>; />;
@@ -190,6 +256,7 @@ export default class StandardSlotSection extends SlotSection {
onOpen={open.bind(this, st[4])} onOpen={open.bind(this, st[4])}
onSelect={select.bind(this, st[4])} onSelect={select.bind(this, st[4])}
selected={currentMenu == st[4]} selected={currentMenu == st[4]}
ship={ship}
warning= {m => m.enginecapacity < ship.boostEnergy} warning= {m => m.enginecapacity < ship.boostEnergy}
/>; />;
@@ -200,6 +267,7 @@ export default class StandardSlotSection extends SlotSection {
onOpen={open.bind(this, st[5])} onOpen={open.bind(this, st[5])}
onSelect={select.bind(this, st[5])} onSelect={select.bind(this, st[5])}
selected={currentMenu == st[5]} selected={currentMenu == st[5]}
ship={ship}
warning= {m => m.enginecapacity < ship.boostEnergy} warning= {m => m.enginecapacity < ship.boostEnergy}
/>; />;
@@ -210,12 +278,18 @@ export default class StandardSlotSection extends SlotSection {
onOpen={open.bind(this, st[6])} onOpen={open.bind(this, st[6])}
onSelect={select.bind(this, st[6])} onSelect={select.bind(this, st[6])}
selected={currentMenu == 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; return slots;
} }
/**
* Generate the section drop-down menu
* @param {Function} translate Translate function
* @return {React.Component} Section menu
*/
_getSectionMenu(translate) { _getSectionMenu(translate) {
let _fill = this._fill; let _fill = this._fill;

View File

@@ -1,15 +1,29 @@
import React from 'react'; import React from 'react';
import cn from 'classnames'; import cn from 'classnames';
import shallowEqual from '../utils/shallowEqual'; import { shallowEqual } from '../utils/UtilityFunctions';
/**
* Base SVG Icon Component
*/
class SvgIcon extends React.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); } 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'; } viewBox() { return '0 0 32 32'; }
/**
* Render the Icon
* @return {React.Component} SVG Icon
*/
render() { render() {
return ( return (
<svg className={cn('icon', this.props.className)} style={this.props.style} viewBox={this.viewBox()}> <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 { export class Bin extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g> 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'/> <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 { export class CoriolisLogo extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g transform='translate(1,1)'> 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'/> <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 { export class Download extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { 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'/>; 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 { export class Eddb extends SvgIcon {
/**
* Render the Icon
* @return {React.Component} SVG Icon
*/
render() { render() {
return <svg className={cn(this.props.className)} style={this.props.style} viewBox='0 0 90 32'> 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'/> <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 { export class Embed extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g> return <g>
<path d='M18 23l3 3 10-10-10-10-3 3 7 7z'/> <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 { export class Equalizer extends SvgIcon {
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 1024 1024'; } viewBox() { return '0 0 1024 1024'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g> 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'/> <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 { export class FloppyDisk extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <path d='M28 0h-28v32h32v-28l-4-4zM16 4h4v8h-4v-8zM28 28h-24v-24h2v10h18v-10h2.343l1.657 1.657v22.343z' />; 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 { export class Fuel extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { 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' />; 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 { 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() { 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' />; 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 { export class Infinite extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { 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'/>; 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 { export class Info extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g> 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'/> <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 { export class LinkIcon extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g> 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'/> <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 { 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() { 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'/>; 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 { export class Notification extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { 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'/>; 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 { 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() { svg() {
return <path d='M192 0l-192 256h192l-128 256 448-320h-256l192-192z'/>; return <path d='M192 0l-192 256h192l-128 256 448-320h-256l192-192z'/>;
} }
} }
/**
* Question mark within Circle
*/
export class Question extends SvgIcon { export class Question extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { 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'/>; 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 { export class Reload extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { 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'/>; 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 { export class Warning extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g> 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'/> <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 { export class MountFixed extends SvgIcon {
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; } viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g> return <g>
<circle fillOpacity='0' r='70' cy='100' cx='100' strokeWidth='5' /> <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 { export class MountGimballed extends SvgIcon {
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; } viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g> return <g>
<ellipse ry='25' rx='95' cy='100' cx='100' fillOpacity='0' strokeWidth='5' /> <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 { export class MountTurret extends SvgIcon {
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; } viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g> return <g>
<line y2='170' x2='162' y1='170' x1='8' strokeWidth='6' /> <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 { export class Rocket extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { 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'/>; 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 { export class Hammer extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { 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'/>; 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 { export class StatsBars extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <path d='M0 26h32v4h-32zM4 18h4v6h-4zM10 10h4v14h-4zM16 16h4v8h-4zM22 4h4v20h-4z'/>; return <path d='M0 26h32v4h-32zM4 18h4v6h-4zM10 10h4v14h-4zM16 16h4v8h-4zM22 4h4v20h-4z'/>;
} }
} }
/**
* Cogs / Settings
*/
export class Cogs extends SvgIcon { export class Cogs extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { 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'/>; 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 { export class Switch extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { 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'/>; 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 { export class StationCoriolis extends SvgIcon {
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; } viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g> return <g>
<rect x='73.001' y='94.017' width='53.997' height='11.945'/> <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 { export class StationOcellus extends SvgIcon {
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; } viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g> 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'/> <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 { export class StationOrbis extends SvgIcon {
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; } viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g> 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'/> <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 { export class StationOutpost extends SvgIcon {
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; } viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g> 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'/> <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 { export class Upload extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { 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'/>; 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 { export class Loader extends SvgIcon {
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 40 40'; } viewBox() { return '0 0 40 40'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() { svg() {
return <g className={'loader'}> return <g className={'loader'}>
<path d='m5,8l5,8l5,-8z' className={'l1 d1'} /> <path d='m5,8l5,8l5,-8z' className={'l1 d1'} />

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

View File

@@ -1,15 +1,22 @@
import React from 'react'; 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 { export default class TranslatedComponent extends React.Component {
static contextTypes = { static contextTypes = {
language: 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
};
/** /**
* Created an instance of a Translated Component. This is an abstract class. * Created an instance of a Translated Component. This is an abstract class.
@@ -34,14 +41,12 @@ export default class TranslatedComponent extends React.Component {
* props, state, or context changes. This method performs a shallow comparison to * props, state, or context changes. This method performs a shallow comparison to
* determine change. * determine change.
* *
* @param {object} nextProps * @param {object} nextProps Next/Incoming Properties
* @param {objec} nextState * @param {objec} nextState Next/Incoming State
* @param {objec} nextContext * @param {objec} nextContext Next/Incoming Context
* @return {boolean} True if props, state, or context has changed * @return {boolean} True if props, state, or context has changed
*/ */
shouldComponentUpdate(nextProps, nextState, nextContext) { shouldComponentUpdate(nextProps, nextState, nextContext) {
return !shallowEqual(this.props, nextProps) return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState) || this.didContextChange(nextContext);
|| !shallowEqual(this.state, nextState)
|| this.didContextChange(nextContext);
} }
} }

View File

@@ -3,35 +3,60 @@ import SlotSection from './SlotSection';
import HardpointSlot from './HardpointSlot'; import HardpointSlot from './HardpointSlot';
import cn from 'classnames'; import cn from 'classnames';
/**
* Utility Slot Section
*/
export default class UtilitySlotSection extends SlotSection { export default class UtilitySlotSection extends SlotSection {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) { constructor(props, context) {
super(props, context, 'utility', 'utility mounts'); super(props, context, 'utility', 'utility mounts');
this._empty = this._empty.bind(this); this._empty = this._empty.bind(this);
} }
/**
* Empty all utility slots and close the menu
*/
_empty() { _empty() {
this.props.ship.emptyUtility(); this.props.ship.emptyUtility();
this.props.onChange(); this.props.onChange();
this._close(); 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) { _use(group, rating, name, event) {
this.props.ship.useUtility(group, rating, name, event.getModifierState('Alt')); this.props.ship.useUtility(group, rating, name, event.getModifierState('Alt'));
this.props.onChange(); this.props.onChange();
this._close(); this._close();
} }
/**
* Empty all utility slots on right-click
*/
_contextMenu() { _contextMenu() {
this._empty(); this._empty();
} }
/**
* Create all HardpointSlots (React component) for the slots
* @return {Array} Array of HardpointSlots
*/
_getSlots() { _getSlots() {
let slots = []; let slots = [];
let hardpoints = this.props.ship.hardpoints; let { ship, currentMenu } = this.props;
let availableModules = this.props.ship.getAvailableModules(); let hardpoints = ship.hardpoints;
let currentMenu = this.props.currentMenu; let { originSlot, targetSlot } = this.state;
let availableModules = ship.getAvailableModules();
for (let i = 0, l = hardpoints.length; i < l; i++) { for (let i = 0, l = hardpoints.length; i < l; i++) {
let h = hardpoints[i]; let h = hardpoints[i];
@@ -43,7 +68,12 @@ export default class UtilitySlotSection extends SlotSection {
onOpen={this._openMenu.bind(this,h)} onOpen={this._openMenu.bind(this,h)}
onSelect={this._selectModule.bind(this, h)} onSelect={this._selectModule.bind(this, h)}
selected={currentMenu == 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} enabled={h.enabled}
ship={ship}
m={h.m} m={h.m}
/>); />);
} }
@@ -52,6 +82,11 @@ export default class UtilitySlotSection extends SlotSection {
return slots; return slots;
} }
/**
* Generate the section menu
* @param {Function} translate Translate function
* @return {React.Component} Section menu
*/
_getSectionMenu(translate) { _getSectionMenu(translate) {
let _use = this._use; let _use = this._use;

View File

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

View File

@@ -12,7 +12,7 @@ let fallbackTerms = EN.terms;
/** /**
* Get the units, translation and format functions for the specified language * Get the units, translation and format functions for the specified language
* @param {string} langCode ISO Language code * @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) { export function getLanguage(langCode) {
let lang, translate; let lang, translate;
@@ -39,15 +39,16 @@ export function getLanguage(langCode) {
return { return {
formats: { 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) 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) 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%) pct: d3Locale.numberFormat('.2%'), // % to 2 decimal places (.e.g 5.40%)
pct1: d3Locale.numberFormat('.1%'), // % to 1 decimal places (.e.g 5.4%) 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%) 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) 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, translate,
units: { units: {
@@ -64,8 +65,7 @@ export function getLanguage(langCode) {
pm: <u>{translate('/min')}</u>, // per minute pm: <u>{translate('/min')}</u>, // per minute
T: <u>{' ' + translate('T')}</u>, // Metric Tons T: <u>{' ' + translate('T')}</u>, // Metric Tons
} }
} };
} }
/** /**

View File

@@ -21,7 +21,7 @@ export const terms = {
added: 'Hinzugefügt', added: 'Hinzugefügt',
Advanced: 'Verbessert', Advanced: 'Verbessert',
'Advanced Discovery Scanner': 'Fortgeschrittener Aufklärungsscanner', 'Advanced Discovery Scanner': 'Fortgeschrittener Aufklärungsscanner',
agility: 'Manövrierbarkeit', maneuverability: 'Manövrierbarkeit',
ammo: 'Munition', ammo: 'Munition',
PHRASE_CONFIRMATION: 'Sind Sie sicher?', PHRASE_CONFIRMATION: 'Sind Sie sicher?',
armour: 'Panzerung', armour: 'Panzerung',

View File

@@ -14,220 +14,57 @@ export const formats = {
}; };
export const terms = { export const terms = {
PHRASE_EXPORT_DESC: 'A detailed JSON export of your build for use in other sites and tools', PHRASE_BACKUP_DESC: 'Backup of all Coriolis data to save or transfer to another browser/device',
// '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_CONFIRMATION: 'Are You Sure?', 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', am: 'Auto Field-Maintenance Unit',
// available: 'available',
// backup: 'backup',
'Basic Discovery Scanner': 'Basic Discovery Scanner', 'Basic Discovery Scanner': 'Basic Discovery Scanner',
bl: 'Beam Laser', bl: 'Beam Laser',
// beta: 'beta',
// bins: 'bins',
// boost: 'boost',
// build: 'build',
// 'build name': 'Build Name',
// builds: 'builds',
bh: 'bulkheads', bh: 'bulkheads',
bsg: 'Bi-Weave Shield Generator', bsg: 'Bi-Weave Shield Generator',
ul: 'Burst Laser', ul: 'Burst Laser',
buy: 'buy',
// cancel: 'cancel',
c: 'Cannon', c: 'Cannon',
// capital: 'capital',
// cargo: 'cargo',
// 'Cargo Hatch': 'Cargo Hatch',
cr: 'Cargo Rack', cr: 'Cargo Rack',
cs: 'Cargo Scanner', cs: 'Cargo Scanner',
cells: 'cells',
// 'Chaff Launcher': 'Chaff Launcher',
// close: 'close',
cc: 'Collector Limpet Controller', cc: 'Collector Limpet Controller',
// compare: 'compare',
// 'compare all': 'compare all',
// comparison: 'comparison',
// comparisons: 'comparisons',
// component: 'component',
// cost: 'cost',
// costs: 'costs',
cm: 'Countermeasure', 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', 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', fc: 'Fragment Cannon',
fd: 'Frame Shift Drive', fd: 'Frame Shift Drive',
ws: 'Frame Shift Wake Scanner', ws: 'Frame Shift Wake Scanner',
// FSD: 'FSD',
fsd: 'Frame Shift Drive', fsd: 'Frame Shift Drive',
fi: 'FSD Interdictor', fi: 'FSD Interdictor',
// fuel: 'fuel',
fs: 'Fuel Scoop', fs: 'Fuel Scoop',
ft: 'Fuel Tank', ft: 'Fuel Tank',
fx: 'Fuel Transfer Limpet Controller', fx: 'Fuel Transfer Limpet Controller',
// 'full tank': 'full tank',
// Gimballed: 'Gimballed',
// H: 'H',
// hardpoints: 'hardpoints',
hb: 'Hatch Breaker Limpet Controller', hb: 'Hatch Breaker Limpet Controller',
// 'Heat Sink Launcher': 'Heat Sink Launcher',
// huge: 'huge',
// hull: 'hull',
hr: 'Hull Reinforcement Package', 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', kw: 'Kill Warrant Scanner',
// L: 'L',
// laden: 'laden',
// language: 'language',
// large: 'large',
// 'limpets': 'Limpets',
ls: 'life support', 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', nl: 'Mine Launcher',
// 'Mining Lance': 'Mining Lance',
ml: 'Mining Laser', ml: 'Mining Laser',
// 'Mirrored Surface Composite': 'Mirrored Surface Composite',
mr: 'Missile Rack', mr: 'Missile Rack',
mc: 'Multi-cannon', 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', pa: 'Plasma Accelerator',
// 'Point Defence': 'Point Defence',
// power: 'power',
pd: 'power distributor', pd: 'power distributor',
pp: 'power plant', pp: 'power plant',
pri: 'pri',
// priority: 'priority',
psg: 'Prismatic Shield Generator', psg: 'Prismatic Shield Generator',
// proceed: 'proceed',
pc: 'Prospector Limpet Controller', pc: 'Prospector Limpet Controller',
pl: 'Pulse Laser', pl: 'Pulse Laser',
pv: 'Planetary Vehicle Hanger', pv: 'Planetary Vehicle Hanger',
// PWR: 'PWR',
rg: 'Rail Gun', rg: 'Rail Gun',
// range: 'range',
// rate: 'rate',
// 'Reactive Surface Composite': 'Reactive Surface Composite',
// recharge: 'recharge',
rf: 'Refinery', 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', sc: 'scanner',
PHRASE_SELECT_BUILDS: 'Select Builds to Compare',
// sell: 'sell',
s: 'sensors', s: 'sensors',
// settings: 'settings',
sb: 'Shield Booster', sb: 'Shield Booster',
scb: 'Shield Cell Bank', scb: 'Shield Cell Bank',
sg: 'Shield Generator', 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', T_LOAD: 't-load',
// 'The Retributor': 'The Retributor',
t: 'thrusters', t: 'thrusters',
// time: 'time', tp: 'Torpedo Pylon'
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'
}; };

View File

@@ -20,7 +20,7 @@ export const terms = {
'action': 'Acci\u00f3n', 'action': 'Acci\u00f3n',
'added': 'A\u00f1adido', 'added': 'A\u00f1adido',
'Advanced Discovery Scanner': 'Esc\u00e1ner de exploraci\u00f3n avanzado', 'Advanced Discovery Scanner': 'Esc\u00e1ner de exploraci\u00f3n avanzado',
'agility': 'Maniobrabilidad', maneuverability: 'Maniobrabilidad',
'alpha': 'Alfa', 'alpha': 'Alfa',
'ammo': 'Munici\u00f3n', 'ammo': 'Munici\u00f3n',
'PHRASE_CONFIRMATION': '\u00bfEst\u00e1s seguro?', 'PHRASE_CONFIRMATION': '\u00bfEst\u00e1s seguro?',

View File

@@ -20,7 +20,7 @@ export const terms = {
added: 'ajouté', added: 'ajouté',
Advanced: 'Avancé', Advanced: 'Avancé',
'Advanced Discovery Scanner': 'Détecteur découverte avancé', 'Advanced Discovery Scanner': 'Détecteur découverte avancé',
agility: 'manœuvrabilité', maneuverability: 'manœuvrabilité',
ammo: 'munitions', ammo: 'munitions',
PHRASE_CONFIRMATION: 'Êtes-vous sûr ?', PHRASE_CONFIRMATION: 'Êtes-vous sûr ?',
armour: 'Armure', armour: 'Armure',

View File

@@ -20,7 +20,7 @@ export const terms = {
action: 'azione', action: 'azione',
added: 'aggiunto', added: 'aggiunto',
Advanced: 'Avanzato', Advanced: 'Avanzato',
agility: 'agilità', maneuverability: 'agilità',
ammo: 'munizioni', ammo: 'munizioni',
PHRASE_CONFIRMATION: 'Sei sicuro ?', PHRASE_CONFIRMATION: 'Sei sicuro ?',
armour: 'armatura', armour: 'armatura',

View File

@@ -21,7 +21,7 @@ export const terms = {
added: 'Добавлено', added: 'Добавлено',
Advanced: 'Продвинутый', Advanced: 'Продвинутый',
'Advanced Discovery Scanner': 'Продвинутый астросканер', 'Advanced Discovery Scanner': 'Продвинутый астросканер',
agility: 'Маневренность', maneuverability: 'Маневренность',
alpha: 'Альфа', alpha: 'Альфа',
ammo: 'Боекомплект', ammo: 'Боекомплект',
PHRASE_CONFIRMATION: 'Вы уверены?', PHRASE_CONFIRMATION: 'Вы уверены?',

View File

@@ -2,8 +2,15 @@ import React from 'react';
import Page from './Page'; import Page from './Page';
import { CoriolisLogo, GitHub } from '../components/SvgIcons'; import { CoriolisLogo, GitHub } from '../components/SvgIcons';
/**
* About Page
*/
export default class AboutPage extends Page { export default class AboutPage extends Page {
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@@ -11,6 +18,10 @@ export default class AboutPage extends Page {
}; };
} }
/**
* Render the Page
* @return {React.Component} The page contents
*/
render() { render() {
return <div className={'page'} style={{ textAlign: 'left', maxWidth: 800, margin: '0 auto' }}> 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> <h1><CoriolisLogo style={{ marginRight: '0.4em' }} className='xl'/><span className='warning'>Coriolis</span></h1>

View File

@@ -1,22 +1,35 @@
import React from 'react'; import React from 'react';
import { findDOMNode } from 'react-dom';
import Page from './Page'; import Page from './Page';
import Router from '../Router'; import Router from '../Router';
import cn from 'classnames'; import cn from 'classnames';
import { Ships } from 'coriolis-data'; import { Ships } from 'coriolis-data';
import Ship from '../shipyard/Ship'; import Ship from '../shipyard/Ship';
import Serializer from '../shipyard/Serializer'; import { fromComparison, toComparison } from '../shipyard/Serializer';
import InterfaceEvents from '../utils/InterfaceEvents';
import Persist from '../stores/Persist'; import Persist from '../stores/Persist';
import { SizeMap, ShipFacets } from '../shipyard/Constants'; import { SizeMap, ShipFacets } from '../shipyard/Constants';
import ComparisonTable from '../components/ComparisonTable'; import ComparisonTable from '../components/ComparisonTable';
import BarChart from '../components/BarChart';
import ModalCompare from '../components/ModalCompare'; 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 { 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) { function sortBy(predicate) {
return (a, b) => { return (a, b) => {
if (a[predicate] === b[predicate]) { 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') { if (typeof a[predicate] == 'string') {
return a[predicate].toLowerCase() > b[predicate].toLowerCase() ? 1 : -1; return a[predicate].toLowerCase() > b[predicate].toLowerCase() ? 1 : -1;
@@ -25,23 +38,35 @@ function sortBy(predicate) {
}; };
} }
/**
* Comparison Page
*/
export default class ComparisonPage extends Page { export default class ComparisonPage extends Page {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this._sortShips = this._sortShips.bind(this); this._sortShips = this._sortShips.bind(this);
this._buildsSelected = this._buildsSelected.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 defaultFacets = [9, 6, 4, 1, 3, 2]; // Reverse order of Armour, Shields, Speed, Jump Range, Cargo Capacity, Cost
let params = context.route.params; let params = context.route.params;
let code = params.code; let code = params.code;
let name = params.name ? decodeURIComponent(params.name) : null; let name = params.name ? decodeURIComponent(params.name) : null;
let newName = ''; let newName = '';
let compareMode = !code; let compareMode = !code;
let facets = [];
let builds = []; let builds = [];
let saved = false; let saved = false;
let predicate = 'name'; let predicate = 'name';
@@ -58,7 +83,6 @@ export default class ComparisonPage extends Page {
} }
} }
} else { } else {
let comparisonData = Persist.getComparison(name); let comparisonData = Persist.getComparison(name);
if (comparisonData) { if (comparisonData) {
defaultFacets = comparisonData.facets; defaultFacets = comparisonData.facets;
@@ -69,7 +93,7 @@ export default class ComparisonPage extends Page {
} }
} else { } else {
try { try {
let comparisonData = Serializer.toComparison(code); let comparisonData = toComparison(code);
defaultFacets = comparisonData.f; defaultFacets = comparisonData.f;
newName = name = comparisonData.n; newName = name = comparisonData.n;
predicate = comparisonData.p; 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++) { for (let i = 0; i < ShipFacets.length; i++) {
facets.push(Object.assign({ index: i }, ShipFacets[i])); let facet = Object.assign({ }, ShipFacets[i]);
} let defaultIndex = defaultFacets.indexOf(facet.i);
if(defaultIndex == -1) {
let selectedFacets = []; facets.push(facet);
} else {
for (let fi of defaultFacets) {
let facet = facets.splice(fi, 1)[0];
facet.active = true; facet.active = true;
selectedFacets.unshift(facet); selectedFacets[selectedLength - defaultIndex - 1] = facet;
}
} }
facets = selectedFacets.concat(facets); facets = selectedFacets.concat(facets);
console.log(selectedFacets);
builds.sort(sortBy(predicate)); builds.sort(sortBy(predicate));
return { return {
@@ -116,7 +142,13 @@ console.log(selectedFacets);
importObj 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) { _createBuild(id, name, code) {
code = code ? code : Persist.getBuild(id, name); // Retrieve build code if not passed code = code ? code : Persist.getBuild(id, name); // Retrieve build code if not passed
@@ -132,8 +164,8 @@ console.log(selectedFacets);
}; };
/** /**
* Sort ships * Update state with the specified sort predicates
* @param {object} key Sort predicate * @param {String} predicate Sort predicate - property name
*/ */
_sortShips(predicate) { _sortShips(predicate) {
let { builds, desc } = this.state; let { builds, desc } = this.state;
@@ -150,38 +182,54 @@ console.log(selectedFacets);
this.setState({ predicate, desc }); this.setState({ predicate, desc });
}; };
/**
* Show selected builds modal
*/
_selectBuilds() { _selectBuilds() {
InterfaceEvents.showModal(React.cloneElement( this.context.showModal(<ModalCompare onSelect={this._buildsSelected} builds={this.state.builds} />);
<ModalCompare onSelect={this._buildsSelected}/>,
{ builds: this.state.builds }
));
} }
/**
* Update selected builds with new list
* @param {Array} newBuilds List of new builds
*/
_buildsSelected(newBuilds) { _buildsSelected(newBuilds) {
InterfaceEvents.hideModal(); this.context.hideModal();
let builds = []; let builds = [];
for (let b of newBuilds) { for (let b of newBuilds) {
builds.push(this._createBuild(b.id, b.buildName)); 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) { _toggleFacet(facet) {
facet.active = !facet.active; facet.active = !facet.active;
this.setState({ facets: [].concat(this.state.facets), saved: false }); this.setState({ facets: [].concat(this.state.facets), saved: false });
} }
/**
* Handle facet drag
* @param {Event} e Drag Event
*/
_facetDrag(e) { _facetDrag(e) {
this.dragged = e.currentTarget; this.dragged = e.currentTarget;
let placeholder = this.placeholder = document.createElement("li"); let placeholder = this.placeholder = document.createElement('li');
placeholder.style.width = this.dragged.offsetWidth + 'px'; placeholder.style.width = this.dragged.offsetWidth + 'px';
placeholder.className = "facet-placeholder"; placeholder.className = 'facet-placeholder';
e.dataTransfer.effectAllowed = 'move'; 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) { _facetDrop(e) {
this.dragged.parentNode.removeChild(this.placeholder); this.dragged.parentNode.removeChild(this.placeholder);
let facets = this.state.facets; let facets = this.state.facets;
@@ -197,18 +245,22 @@ console.log(selectedFacets);
facets.splice(to, 0, facets.splice(frm, 1)[0]); facets.splice(to, 0, facets.splice(frm, 1)[0]);
this.dragged.style.display = null; 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) { _facetDragOver(e) {
e.preventDefault(); e.preventDefault();
if(e.target.className == "facet-placeholder") { if(e.target.className == 'facet-placeholder') {
return; return;
} }
this.over = e.target; this.over = e.target;
this.dragged.style.display = "none"; this.dragged.style.display = 'none';
let relX = e.clientX - this.over.getBoundingClientRect().left; let relX = e.clientX - this.over.getBoundingClientRect().left;
let width = this.over.offsetWidth / 2; let width = this.over.offsetWidth / 2;
let parent = e.target.parentNode; let parent = e.target.parentNode;
@@ -217,71 +269,149 @@ console.log(selectedFacets);
if(relX > width) { if(relX > width) {
this.nodeAfter = true; this.nodeAfter = true;
parent.insertBefore(this.placeholder, e.target.nextElementSibling); parent.insertBefore(this.placeholder, e.target.nextElementSibling);
} } else {
else {
this.nodeAfter = false; this.nodeAfter = false;
parent.insertBefore(this.placeholder, e.target); parent.insertBefore(this.placeholder, e.target);
} }
} }
} }
/**
* Handle name change and update state
* @param {SyntheticEvent} e Event
*/
_onNameChange(e) { _onNameChange(e) {
this.setState({ newName: e.target.value, saved: false }); this.setState({ newName: e.target.value, saved: false });
} }
/**
* Delete the current comparison
*/
_delete() { _delete() {
Persist.deleteComparison(this.state.name); Persist.deleteComparison(this.state.name);
Router.go('/compare'); Router.go('/compare');
} }
/**
* Import the comparison builds
*/
_import() { _import() {
// TODO: Implement
} }
/**
* Save the current comparison
*/
_save() { _save() {
let { newName, builds, facets } = this.state; let { newName, builds, facets } = this.state;
let selectedFacets = []; let selectedFacets = [];
facets.forEach((f) => { facets.forEach((f) => {
if (f.active) { if (f.active) {
selectedFacets.unshift(f.index); selectedFacets.unshift(f.i);
} }
}); });
console.log(selectedFacets);
//Persist.saveComparison(newName, builds, selectedFacets); Persist.saveComparison(newName, builds, selectedFacets);
Router.replace(`/compare/${encodeURIComponent(this.state.newName)}`) Router.replace(`/compare/${encodeURIComponent(this.state.newName)}`);
this.setState({ name: newName, saved: true }); this.setState({ name: newName, saved: true });
} }
/** /**
* Generates the long permalink URL * Serialize and generate a long URL for the current comparison
* @return {string} The long permalink URL * @return {string} URL for serialized comparison
*/ */
_genPermalink() { _buildUrl() {
let { facets, builds, name, predicate, desc } = this.state; let { facets, builds, name, predicate, desc } = this.state;
let selectedFacets = []; let selectedFacets = [];
for (let f of facets) { for (let f of facets) {
if (f.active) { if (f.active) {
selectedFacets.unshift(f.index); selectedFacets.unshift(f.i);
} }
} }
let code = Serializer.fromComparison(name, builds, selectedFacets, predicate, desc); let code = fromComparison(name, builds, selectedFacets, predicate, desc);
// send code to permalink modal let loc = window.location;
return `${loc.protocol}://${loc.host}/comparison/${code}`;
} }
/**
* Generates the long permalink URL
*/
_genPermalink() {
this.context.showModal(<ModalPermalink url={this._buildUrl()}/>);
}
/**
* Generate E:D Forum BBCode and show in the export modal
*/
_genBBcode() {
let { translate, formats } = this.context.language;
let { facets, builds } = this.state;
let generator = (callback) => {
let url = this._buildUrl();
ShortenUrl(url,
(shortenedUrl) => callback(comparisonBBCode(translate, formats, facets, builds, shortenedUrl)),
(error) => callback(comparisonBBCode(translate, formats, facets, builds, url))
);
};
this.context.showModal(<ModalExport
title={translate('forum') + ' BBCode'}
generator={generator}
/>);
}
/**
* Update dimenions from rendered DOM
*/
_updateDimensions() {
this.setState({
chartWidth: 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) { componentWillReceiveProps(nextProps, nextContext) {
if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed 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() { render() {
let translate = this.context.language.translate; let translate = this.context.language.translate;
let compareHeader; 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) { if (this.state.compareMode) {
compareHeader = <tr> compareHeader = <tr>
@@ -295,10 +425,10 @@ console.log(selectedFacets);
<button onClick={this._selectBuilds}> <button onClick={this._selectBuilds}>
<Rocket className='lg'/><span className='button-lbl'>{translate('builds')}</span> <Rocket className='lg'/><span className='button-lbl'>{translate('builds')}</span>
</button> </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> <LinkIcon className='lg'/><span className='button-lbl'>{translate('permalink')}</span>
</button> </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> <Embed className='lg'/><span className='button-lbl'>{translate('forum')}</span>
</button> </button>
</td> </td>
@@ -335,9 +465,23 @@ console.log(selectedFacets);
<ComparisonTable builds={builds} facets={facets} onSort={this._sortShips} predicate={predicate} desc={desc} /> <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'> {!builds.length ?
<h3 ng-click='sort(f.props[0])' >{{f.title | translate}}</h3> <div className='chart' ref={'chartRef'}>{translate('PHRASE_NO_BUILDS')}</div> :
</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> </div>
); );

View File

@@ -1,8 +1,16 @@
import React from 'react'; import React from 'react';
import Page from './Page'; import Page from './Page';
/**
* Unexpected Error page
* TODO: Implement properly and test
*/
export default class ErrorPage extends Page { export default class ErrorPage extends Page {
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@@ -10,10 +18,14 @@ export default class ErrorPage extends Page {
}; };
} }
/**
* Render the Page
* @return {React.Component} The page contents
*/
render() { render() {
let msgPre, msgHighlight, msgPost, errorMessage, details; let msgPre, msgHighlight, msgPost, errorMessage, details, type;
switch ($scope.type) { switch (type) {
case 404: case 404:
msgPre = 'Page'; msgPre = 'Page';
msgHighlight = this.context.route.path; msgHighlight = this.context.route.path;
@@ -38,9 +50,9 @@ export default class ErrorPage extends Page {
return <div className='error'> return <div className='error'>
<h1> <h1>
<span>{{msgPre}}</span> <span>{msgPre}</span>
<small>{{msgHighlight}}</small> <small>{msgHighlight}</small>
<span>{{msgPost}}</span> <span>{msgPost}</span>
</h1> </h1>
<div style={{ textAlign:'left', fontSize:'0.8em', width: '43em', margin: '0 auto' }}> <div style={{ textAlign:'left', fontSize:'0.8em', width: '43em', margin: '0 auto' }}>

View File

@@ -1,8 +1,15 @@
import React from 'react'; import React from 'react';
import Page from './Page'; import Page from './Page';
/**
* 404 Page
*/
export default class NotFoundPage extends Page { export default class NotFoundPage extends Page {
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@@ -10,6 +17,10 @@ export default class NotFoundPage extends Page {
}; };
} }
/**
* Render the Page
* @return {React.Component} The page contents
*/
render() { render() {
return <div className={'page'}>Page {JSON.stringify(this.props.context)} Not Found</div>; return <div className={'page'}>Page {JSON.stringify(this.props.context)} Not Found</div>;
} }

View File

@@ -4,7 +4,6 @@ import Page from './Page';
import cn from 'classnames'; import cn from 'classnames';
import Router from '../Router'; import Router from '../Router';
import Persist from '../stores/Persist'; import Persist from '../stores/Persist';
import InterfaceEvents from '../utils/InterfaceEvents';
import { Ships } from 'coriolis-data'; import { Ships } from 'coriolis-data';
import Ship from '../shipyard/Ship'; import Ship from '../shipyard/Ship';
import { toDetailedBuild } from '../shipyard/Serializer'; 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_SERIES = ['boost', '4 Pips', '2 Pips', '0 Pips'];
const SPEED_COLORS = ['#0088d2', '#ff8c0d', '#D26D00', '#c06400']; const SPEED_COLORS = ['#0088d2', '#ff8c0d', '#D26D00', '#c06400'];
/**
* The Outfitting Page
*/
export default class OutfittingPage extends Page { export default class OutfittingPage extends Page {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = this._initState(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) { _initState(context) {
let params = context.route.params; let params = context.route.params;
let shipId = params.ship; let shipId = params.ship;
@@ -70,10 +82,14 @@ export default class OutfittingPage extends Page {
}; };
} }
/**
* Handle build name change and update state
* @param {SyntheticEvent} event React Event
*/
_buildNameChange(event) { _buildNameChange(event) {
let stateChanges = { let stateChanges = {
buildName: event.target.value 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); stateChanges.savedCode = Persist.getBuild(this.state.shipId, stateChanges.buildName);
@@ -84,37 +100,55 @@ export default class OutfittingPage extends Page {
this.setState(stateChanges); this.setState(stateChanges);
} }
/**
* Save the current build
*/
_saveBuild() { _saveBuild() {
let code = this.state.ship.toString(); let code = this.state.ship.toString();
Persist.saveBuild(this.state.shipId, this.state.buildName, code); 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() { _reloadBuild() {
this.state.ship.buildFrom(this.state.savedCode); this.state.ship.buildFrom(this.state.savedCode);
this._shipUpdated(); this._shipUpdated();
} }
/**
* Reset build to Stock/Factory defaults
*/
_resetBuild() { _resetBuild() {
this.state.ship.buildWith(Ships[this.state.shipId].defaults); this.state.ship.buildWith(Ships[this.state.shipId].defaults);
this._shipUpdated(); this._shipUpdated();
} }
/**
* Delete the build
*/
_deleteBuild() { _deleteBuild() {
Persist.deleteBuild(this.state.shipId, this.state.buildName); Persist.deleteBuild(this.state.shipId, this.state.buildName);
Router.go(`/outfit/${this.state.shipId}`); Router.go(`/outfit/${this.state.shipId}`);
} }
/**
* Serialized and show the export modal
*/
_exportBuild() { _exportBuild() {
let translate = this.context.language.translate; let translate = this.context.language.translate;
let { buildName, ship } = this.state; let { buildName, ship } = this.state;
InterfaceEvents.showModal(<ModalExport this.context.showModal(<ModalExport
title={buildName + ' ' + translate('export')} title={buildName + ' ' + translate('export')}
description={translate('PHRASE_EXPORT_DESC')} description={translate('PHRASE_EXPORT_DESC')}
data={toDetailedBuild(buildName, ship, ship.toString())} data={toDetailedBuild(buildName, ship, ship.toString())}
/>); />);
} }
/**
* Trigger render on ship model change
*/
_shipUpdated() { _shipUpdated() {
let { shipId, buildName, ship, fuelCapacity } = this.state; let { shipId, buildName, ship, fuelCapacity } = this.state;
let code = ship.toString(); let code = ship.toString();
@@ -127,6 +161,12 @@ export default class OutfittingPage extends Page {
this.setState({ code }); 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) { _updateRoute(shipId, code, buildName) {
let qStr = ''; let qStr = '';
@@ -137,6 +177,10 @@ export default class OutfittingPage extends Page {
Router.replace(`/outfit/${shipId}/${code}${qStr}`); Router.replace(`/outfit/${shipId}/${code}${qStr}`);
} }
/**
* Update current fuel level
* @param {number} fuelLevel Fuel leval 0 - 1
*/
_fuelChange(fuelLevel) { _fuelChange(fuelLevel) {
let ship = this.state.ship; let ship = this.state.ship;
let fuelCapacity = ship.fuelCapacity; let fuelCapacity = ship.fuelCapacity;
@@ -150,34 +194,57 @@ export default class OutfittingPage extends Page {
}); });
} }
/**
* Update dimenions from rendered DOM
*/
_updateDimensions() { _updateDimensions() {
this.setState({ this.setState({
chartWidth: findDOMNode(this.refs.chartThird).offsetWidth 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) { componentWillReceiveProps(nextProps, nextContext) {
if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
this.setState(this._initState(nextContext)); this.setState(this._initState(nextContext));
} }
} }
/**
* Add listeners when about to mount
*/
componentWillMount() { componentWillMount() {
this.resizeListener = InterfaceEvents.addListener('windowResized', this._updateDimensions); this.resizeListener = this.context.onWindowResize(this._updateDimensions);
} }
/**
* Trigger DOM updates on mount
*/
componentDidMount() { componentDidMount() {
this._updateDimensions(); this._updateDimensions();
} }
/**
* Remove listeners on unmount
*/
componentWillUnmount() { componentWillUnmount() {
this.resizeListener.remove(); this.resizeListener.remove();
} }
/**
* Render the Page
* @return {React.Component} The page contents
*/
render() { 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 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 menu = this.props.currentMenu;
let shipUpdated = this._shipUpdated; let shipUpdated = this._shipUpdated;
let hStr = ship.getHardpointsString(); let hStr = ship.getHardpointsString();
@@ -190,20 +257,20 @@ export default class OutfittingPage extends Page {
<h1>{ship.name}</h1> <h1>{ship.name}</h1>
<div id='build'> <div id='build'>
<input value={buildName} onChange={this._buildNameChange} placeholder={translate('Enter Name')} maxsize={50} /> <input value={buildName} onChange={this._buildNameChange} placeholder={translate('Enter Name')} maxsize={50} />
<button onClick={this._saveBuild} disabled={!buildName || savedCode && code == savedCode}> <button onClick={this._saveBuild} disabled={!buildName || savedCode && code == savedCode} onMouseOver={tip.bind(null, 'save')} onMouseOut={hide}>
<FloppyDisk className='lg'/><span className='button-lbl'>{translate('save')}</span> <FloppyDisk className='lg' />
</button> </button>
<button onClick={this._reloadBuild} disabled={!savedCode || code == savedCode}> <button onClick={this._reloadBuild} disabled={!savedCode || code == savedCode} onMouseOver={tip.bind(null, 'reload')} onMouseOut={hide}>
<Reload className='lg'/><span className='button-lbl' >{translate('reload')}</span> <Reload className='lg'/>
</button> </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'/> <Bin className='lg'/>
</button> </button>
<button onClick={this._resetBuild} disabled={!code}> <button onClick={this._resetBuild} disabled={!code} onMouseOver={tip.bind(null, 'reset')} onMouseOut={hide}>
<Switch className='lg'/><span className='button-lbl'>{translate('reset')}</span> <Switch className='lg'/>
</button> </button>
<button onClick={this._exportBuild} disabled={!buildName}> <button onClick={this._exportBuild} disabled={!buildName} onMouseOver={tip.bind(null, 'export')} onMouseOut={hide}>
<Download className='lg'/><span className='button-lbl'>{translate('export')}</span> <Download className='lg'/>
</button> </button>
</div> </div>
</div> </div>
@@ -265,8 +332,8 @@ export default class OutfittingPage extends Page {
<tbody > <tbody >
<tr> <tr>
<td style={{ verticalAlign: 'top', padding:0 }}><Fuel className='xl primary-disabled' /></td> <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><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(state.fuelLevel * ship.fuelCapacity)}{units.T} {formats.pct1(state.fuelLevel)}</td> <td className='primary' style={{ width: '10em', verticalAlign: 'top', fontSize: '0.9em' }}>{formats.f2(fuelLevel * fuelCapacity)}{units.T} {formats.pct1(fuelLevel)}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -1,15 +1,22 @@
import React from 'react'; 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 { export default class Page extends React.Component {
static contextTypes = { static contextTypes = {
route: React.PropTypes.object.isRequired, route: React.PropTypes.object.isRequired,
language: 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 = { static propTypes = {
@@ -32,19 +39,16 @@ export default class Page extends React.Component {
} }
/** /**
* Translated components are 'pure' components that only render when * Pages are 'pure' components that only render when props, state, or context changes.
* props, state, or context changes. This method performs a shallow comparison to * This method performs a shallow comparison to determine change.
* determine change.
* *
* @param {object} nextProps * @param {Object} np Next/Incoming properties
* @param {objec} nextState * @param {Object} ns Next/Incoming state
* @param {objec} nextContext * @param {Object} nc Next/Incoming context
* @return {boolean} True if props, state, or context has changed * @return {Boolean} True if props, state, or context has changed
*/ */
shouldComponentUpdate(nextProps, nextState, nextContext) { shouldComponentUpdate(np, ns, nc) {
return !shallowEqual(this.props, nextProps) return !shallowEqual(this.props, np) || !shallowEqual(this.state, ns) || !shallowEqual(this.context, nc);
|| !shallowEqual(this.state, nextState)
|| !shallowEqual(this.context, nextContext)
} }
/** /**

View File

@@ -7,22 +7,38 @@ import * as ModuleUtils from '../shipyard/ModuleUtils';
import { SizeMap } from '../shipyard/Constants'; import { SizeMap } from '../shipyard/Constants';
import Link from '../components/Link'; import Link from '../components/Link';
/**
* Counts the hardpoints by class/size
* @param {Object} slot Hardpoint Slot model
*/
function countHp(slot) { function countHp(slot) {
this.hp[slot.maxClass]++; this.hp[slot.maxClass]++;
this.hpCount++; this.hpCount++;
} }
/**
* Counts the internal slots and aggregated properties
* @param {Object} slot Internal Slots
*/
function countInt(slot) { 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.int[slot.maxClass - 1]++; // Subtract 1 since there is no Class 0 Internal compartment
this.intCount++; 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; let cachedShipSummaries = null;
/**
* The Shipyard summary page
*/
export default class ShipyardPage extends Page { export default class ShipyardPage extends Page {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this.state = {
@@ -43,8 +59,9 @@ export default class ShipyardPage extends Page {
} }
/** /**
* Sort ships * Update state with the specified sort predicates
* @param {object} key Sort predicate * @param {String} shipPredicate Sort predicate - property name
* @param {number} shipPredicateIndex Sort predicate - property index
*/ */
_sortShips(shipPredicate, shipPredicateIndex) { _sortShips(shipPredicate, shipPredicateIndex) {
let shipDesc = this.state.shipDesc; let shipDesc = this.state.shipDesc;
@@ -60,6 +77,12 @@ export default class ShipyardPage extends Page {
this.setState({ shipPredicate, shipDesc, shipPredicateIndex }); 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) { _shipSummary(shipId, shipData) {
let summary = { let summary = {
id: shipId, id: shipId,
@@ -86,6 +109,15 @@ export default class ShipyardPage extends Page {
return summary; 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) { _shipRowElement(s, translate, u, fInt, fRound) {
return <tr key={s.id} className='highlight'> return <tr key={s.id} className='highlight'>
<td className='le'><Link href={'/outfit/' + s.id}>{s.name}</Link></td> <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[6] })}>{s.int[6]}</td>
<td className={cn({ disabled: !s.int[7] })}>{s.int[7]}</td> <td className={cn({ disabled: !s.int[7] })}>{s.int[7]}</td>
<td className='ri'>{fInt(s.hullMass)}{u.T}</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> <td className='ri'>{fInt(s.retailCost)}{u.CR}</td>
</tr>; </tr>;
} }
/**
* Render the Page
* @return {React.Component} The page contents
*/
render() { render() {
let { translate, formats, units } = this.context.language; let { translate, formats, units } = this.context.language;
let fInt = formats.int; let fInt = formats.int;
@@ -127,6 +163,8 @@ export default class ShipyardPage extends Page {
let shipPredicate = this.state.shipPredicate; let shipPredicate = this.state.shipPredicate;
let shipPredicateIndex = this.state.shipPredicateIndex; let shipPredicateIndex = this.state.shipPredicateIndex;
let shipRows = []; 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); let sortShips = (predicate, index) => this._sortShips.bind(this, predicate, index);
// Sort shipsOverview // 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 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('manufacturer')}>{translate('manufacturer')}</th>
<th rowSpan={2} className='sortable' onClick={sortShips('class')}>{translate('size')}</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('base')}</th>
<th colSpan={4}>{translate('max')}</th> <th colSpan={4}>{translate('max')}</th>
<th colSpan={5} className='sortable' onClick={sortShips('hpCount')}>{translate('hardpoints')}</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 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('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> <th rowSpan={2} className='sortable' onClick={sortShips('retailCost')}>{translate('cost')}</th>
</tr> </tr>
<tr> <tr>

View File

@@ -4,7 +4,7 @@
* *
* @param {number} mass Mass of a ship: laden, unlanden, partially laden, etc * @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 {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 * @return {number} Distance in Light Years
*/ */
export function jumpRange(mass, fsd, fuel) { export function jumpRange(mass, fsd, fuel) {
@@ -20,13 +20,13 @@ export function jumpRange(mass, fsd, fuel) {
* @return {number} Distance in Light Years * @return {number} Distance in Light Years
*/ */
export function totalRange(mass, fsd, fuel) { export function totalRange(mass, fsd, fuel) {
var fuelRemaining = fuel % fsd.maxfuel; // Fuel left after making N max jumps let fuelRemaining = fuel % fsd.maxfuel; // Fuel left after making N max jumps
var jumps = Math.floor(fuel / fsd.maxfuel); let jumps = Math.floor(fuel / fsd.maxfuel);
mass += fuelRemaining; mass += fuelRemaining;
// Going backwards, start with the last jump using the remaining fuel // 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 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; 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;
} }
@@ -43,7 +43,7 @@ export function totalRange(mass, fsd, fuel) {
* @return {number} Approximate shield strengh in MJ * @return {number} Approximate shield strengh in MJ
*/ */
export function shieldStrength(mass, shields, sg, multiplier) { export function shieldStrength(mass, shields, sg, multiplier) {
var opt; let opt;
if (mass < sg.minmass) { if (mass < sg.minmass) {
return shields * multiplier * sg.minmul; return shields * multiplier * sg.minmul;
} }
@@ -72,8 +72,8 @@ export function shieldStrength(mass, shields, sg, multiplier) {
* @return {object} Approximate speed by pips * @return {object} Approximate speed by pips
*/ */
export function speed(mass, baseSpeed, baseBoost, thrusters, pipSpeed) { 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))); let 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 speed = baseSpeed * multiplier;
return { return {
'0 Pips': speed * (1 - (pipSpeed * 4)), '0 Pips': speed * (1 - (pipSpeed * 4)),

View File

@@ -62,7 +62,7 @@ export const ModuleGroupToName = {
let GrpNameToCodeMap = {}; let GrpNameToCodeMap = {};
for (let grp in ModuleGroupToName) { for (let grp in ModuleGroupToName) {
GrpNameToCodeMap[ModuleGroupToName[grp]] = grp; GrpNameToCodeMap[ModuleGroupToName[grp].toLowerCase()] = grp;
} }
export const ModuleNameToGroup = GrpNameToCodeMap; export const ModuleNameToGroup = GrpNameToCodeMap;
@@ -94,78 +94,89 @@ export const ShipFacets = [
{ // 0 { // 0
title: 'agility', title: 'agility',
props: ['agility'], props: ['agility'],
fmt: 'int' fmt: 'int',
i: 0
}, },
{ // 1 { // 1
title: 'speed', title: 'speed',
props: ['topSpeed', 'topBoost'], props: ['topSpeed', 'topBoost'],
lbls: ['thrusters', 'boost'], lbls: ['thrusters', 'boost'],
unit: 'm/s', unit: 'm/s',
fmt: 'int' fmt: 'int',
i: 1
}, },
{ // 2 { // 2
title: 'armour', title: 'armour',
props: ['armour'], props: ['armour'],
unit: '', fmt: 'int',
fmt: 'int' i: 2
}, },
{ // 3 { // 3
title: 'shields', title: 'shields',
props: ['shieldStrength'], props: ['shieldStrength'],
unit: 'MJ', unit: 'MJ',
fmt: 'round' fmt: 'int',
i: 3
}, },
{ // 4 { // 4
title: 'jump range', title: 'jump range',
props: ['unladenRange', 'fullTankRange', 'ladenRange'], props: ['unladenRange', 'fullTankRange', 'ladenRange'],
lbls: ['max', 'full tank', 'laden'], lbls: ['max', 'full tank', 'laden'],
unit: 'LY', unit: 'LY',
fmt: 'round' fmt: 'round',
i: 4
}, },
{ // 5 { // 5
title: 'mass', title: 'mass',
props: ['unladenMass', 'ladenMass'], props: ['unladenMass', 'ladenMass'],
lbls: ['unladen', 'laden'], lbls: ['unladen', 'laden'],
unit: 'T', unit: 'T',
fmt: 'round' fmt: 'round',
i: 5
}, },
{ // 6 { // 6
title: 'cargo', title: 'cargo',
props: ['cargoCapacity'], props: ['cargoCapacity'],
unit: 'T', unit: 'T',
fmt: 'round' fmt: 'int',
i: 6
}, },
{ // 7 { // 7
title: 'fuel', title: 'fuel',
props: ['fuelCapacity'], props: ['fuelCapacity'],
unit: 'T', unit: 'T',
fmt: 'int' fmt: 'int',
i: 7
}, },
{ // 8 { // 8
title: 'power', title: 'power',
props: ['powerRetracted', 'powerDeployed', 'powerAvailable'], props: ['powerRetracted', 'powerDeployed', 'powerAvailable'],
lbls: ['retracted', 'deployed', 'available'], lbls: ['retracted', 'deployed', 'available'],
unit: 'MW', unit: 'MW',
fmt: 'f2' fmt: 'f2',
i: 8
}, },
{ // 9 { // 9
title: 'cost', title: 'cost',
props: ['totalCost'], props: ['totalCost'],
unit: 'CR', unit: 'CR',
fmt: 'int' fmt: 'int',
i: 9
}, },
{ // 10 { // 10
title: 'total range', title: 'total range',
props: ['unladenTotalRange', 'ladenTotalRange'], props: ['unladenTotalRange', 'ladenTotalRange'],
lbls: ['unladen', 'laden'], lbls: ['unladen', 'laden'],
unit: 'LY', unit: 'LY',
fmt: 'round' fmt: 'round',
i: 10
}, },
{ // 11 { // 11
title: 'DPS', title: 'DPS',
props: ['totalDps'], props: ['totalDps'],
lbls: ['DPS'], lbls: ['DPS'],
fmt: 'round' fmt: 'round',
i: 11
} }
]; ];

View File

@@ -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) { function filter(arr, maxClass, minClass, mass) {
return arr.filter(m => m.class <= maxClass && m.class >= minClass && (m.maxmass === undefined || mass <= m.maxmass)); 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) { function filterToArray(data, maxClass, minClass, mass) {
let arr = []; let arr = [];
@@ -17,8 +32,19 @@ function filterToArray(data, maxClass, minClass, mass) {
return arr; return arr;
} }
/**
* The available module set for a specific ship
*/
export default class ModuleSet { 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) { constructor(modules, mass, maxStandardArr, maxInternal, maxHardPoint) {
this.mass = mass; this.mass = mass;
this.standard = {}; this.standard = {};
@@ -80,12 +106,12 @@ export default class ModuleSet {
* @return {object} A map of all eligible modules by group * @return {object} A map of all eligible modules by group
*/ */
getHps(c, eligible) { getHps(c, eligible) {
var o = {}; let o = {};
for (var key in this.hardpoints) { for (let key in this.hardpoints) {
if (eligible && !eligible[key]) { if (eligible && !eligible[key]) {
continue; 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 if (data.length) { // If group is not empty
o[key] = data; o[key] = data;
} }
@@ -93,8 +119,14 @@ export default class ModuleSet {
return o; return o;
} }
/**
* Find the lightest Power Distributor that provides sufficient
* energy to boost.
* @param {number} boostEnergy [description]
* @return {Object} Power Distributor
*/
lightestPowerDist(boostEnergy) { lightestPowerDist(boostEnergy) {
var pd = this.standard[4][0]; let pd = this.standard[4][0];
for (let p of this.standard[4]) { for (let p of this.standard[4]) {
if (p.mass < pd.mass && p.enginecapacity >= boostEnergy) { if (p.mass < pd.mass && p.enginecapacity >= boostEnergy) {
@@ -104,8 +136,13 @@ export default class ModuleSet {
return pd; 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) { lightestThruster(ladenMass) {
var th = this.standard[1][0]; let th = this.standard[1][0];
for (let t of this.standard[1]) { for (let t of this.standard[1]) {
if (t.mass < th.mass && t.maxmass >= ladenMass) { if (t.mass < th.mass && t.maxmass >= ladenMass) {
@@ -115,8 +152,13 @@ export default class ModuleSet {
return th; return th;
}; };
/**
* Finds the lightest usable Shield Generator
* @param {number} hullMass Ship hull mass
* @return {Object} Thruster
*/
lightestShieldGenerator(hullMass) { lightestShieldGenerator(hullMass) {
var sg = this.internal.sg[0]; let sg = this.internal.sg[0];
for (let s of this.internal.sg) { for (let s of this.internal.sg) {
if (s.mass < sg.mass && s.minmass <= hullMass && s.maxmass > hullMass) { if (s.mass < sg.mass && s.minmass <= hullMass && s.maxmass > hullMass) {
@@ -126,8 +168,13 @@ export default class ModuleSet {
return sg; 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]) { for (let p of this.standard[0]) {
// Provides enough power, is lighter or the same mass as current power plant but better output/efficiency // Provides enough power, is lighter or the same mass as current power plant but better output/efficiency

View File

@@ -2,10 +2,20 @@ import { ModuleNameToGroup, BulkheadNames } from './Constants';
import ModuleSet from './ModuleSet'; import ModuleSet from './ModuleSet';
import { Ships, Modules } from 'coriolis-data'; import { Ships, Modules } from 'coriolis-data';
/**
* Created a cargo hatch model
* @return {Object} Cargo hatch model
*/
export function cargoHatch() { export function cargoHatch() {
return { name: 'Cargo Hatch', class: 1, rating: 'H', power: 0.6 }; 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) { export function standard(typeIndex, id) {
let standard = Modules.standard[typeIndex]; let standard = Modules.standard[typeIndex];
if (standard[id]) { if (standard[id]) {
@@ -20,6 +30,11 @@ export function standard(typeIndex, id) {
return null; return null;
}; };
/**
* Finds the hardpoint with the specified ID
* @param {string} id Hardpoint ID
* @return {Object} Hardpoint module or null
*/
export function hardpoints(id) { export function hardpoints(id) {
for (let n in Modules.hardpoints) { for (let n in Modules.hardpoints) {
let group = Modules.hardpoints[n]; let group = Modules.hardpoints[n];
@@ -32,6 +47,11 @@ export function hardpoints(id) {
return null; 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) { export function internal(id) {
for (let n in Modules.internal) { for (let n in Modules.internal) {
let group = Modules.internal[n]; 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. * Finds an internal module based on Class, Rating, Group and/or name.
* At least one ofGroup name or unique component name must be provided * 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 {string} groupName [Optional] Full name or abbreviated name for module group
* @param {integer} clss Component Class * @param {integer} clss module Class
* @param {string} rating Component Rating * @param {string} rating module Rating
* @param {string} name [Optional] Long/unique name for component -e.g. 'Advanced Discover Scanner' * @param {string} name [Optional] Long/unique name for module -e.g. 'Advanced Discover Scanner'
* @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 findInternal(groupName, clss, rating, name) { export function findInternal(groupName, clss, rating, name) {
let groups = {}; let groups = {};
@@ -61,7 +81,7 @@ export function findInternal(groupName, clss, rating, name) {
if (Modules.internal[groupName]) { if (Modules.internal[groupName]) {
groups[groupName] = Modules.internal[groupName]; groups[groupName] = Modules.internal[groupName];
} else { } else {
let grpCode = ModuleNameToGroup[groupName]; let grpCode = ModuleNameToGroup[groupName.toLowerCase()];
if (grpCode && Modules.internal[grpCode]) { if (grpCode && Modules.internal[grpCode]) {
groups[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. * Finds an internal Module ID based on Class, Rating, Group and/or name.
* At least one ofGroup name or unique component name must be provided * 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 {string} groupName [Optional] Full name or abbreviated name for module group
* @param {integer} clss Component Class * @param {integer} clss module Class
* @param {string} rating Component Rating * @param {string} rating Module Rating
* @param {string} name [Optional] Long/unique name for component -e.g. 'Advanced Discover Scanner' * @param {string} name [Optional] Long/unique name for module -e.g. 'Advanced Discover Scanner'
* @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 findInternalId(groupName, clss, rating, name) { export function findInternalId(groupName, clss, rating, name) {
let i = this.findInternal(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. * Finds a hardpoint Module based on Class, Rating, Group and/or name.
* At least one ofGroup name or unique component name must be provided * 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 {string} groupName [Optional] Full name or abbreviated name for module group
* @param {integer} clss Component Class * @param {integer} clss Module Class
* @param {string} rating [Optional] Component Rating * @param {string} rating [Optional] module Rating
* @param {string} name [Optional] Long/unique name for component -e.g. 'Heat Sink Launcher' * @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} mount Mount type - [F]ixed, [G]imballed, [T]urret
* @param {string} missile [Optional] Missile type - [D]umbfire, [S]eeker * @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) { export function findHardpoint(groupName, clss, rating, name, mount, missile) {
let groups = {}; let groups = {};
@@ -116,7 +136,7 @@ export function findHardpoint(groupName, clss, rating, name, mount, missile) {
if (Modules.hardpoints[groupName]) { if (Modules.hardpoints[groupName]) {
groups[groupName] = Modules.hardpoints[groupName]; groups[groupName] = Modules.hardpoints[groupName];
} else { } else {
let grpCode = ModuleNameToGroup[groupName]; let grpCode = ModuleNameToGroup[groupName.toLowerCase()];
if (grpCode && Modules.hardpoints[grpCode]) { if (grpCode && Modules.hardpoints[grpCode]) {
groups[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) { for (let g in groups) {
let group = groups[g]; let group = groups[g];
for (let i = 0, l = group.length; i < l; i++) { for (let h of group) {
if (group[i].class == clss && (!rating || group[i].rating == rating) && group[i].mount == mount if (h.class == clss && (!rating || h.rating == rating) && h.mount == mount && h.name == name && h.missile == missile) {
&& ((!name && !group[i].name) || group[i].name == name) return h;
&& ((!missile && !group[i].missile) || group[i].missile == missile)
) {
return group[i];
} }
} }
} }
@@ -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. * Finds a hardpoint module ID based on Class, Rating, Group and/or name.
* At least one of Group name or unique component name must be provided * 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 {string} groupName [Optional] Full name or abbreviated name for module group
* @param {integer} clss Component Class * @param {integer} clss module Class
* @param {string} rating Component Rating * @param {string} rating module Rating
* @param {string} name [Optional] Long/unique name for component -e.g. 'Heat Sink Launcher' * @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} mount Mount type - [F]ixed, [G]imballed, [T]urret
* @param {string} missile [Optional] Missile type - [D]umbfire, [S]eeker * @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) { export function findHardpointId(groupName, clss, rating, name, mount, missile) {
let h = this.findHardpoint(groupName, clss, rating, name, mount, missile); let h = this.findHardpoint(groupName, clss, rating, name, mount, missile);
@@ -160,32 +177,43 @@ export function findHardpointId(groupName, clss, rating, name, mount, missile) {
/** /**
* Looks up the bulkhead module for a specific ship and bulkhead * Looks up the bulkhead module for a specific ship and bulkhead
* @param {string} shipId Unique ship Id/Key * @param {string} shipId Unique ship Id/Key
* @param {string|number} bulkheadsId Id/Index for the specified bulkhead * @param {string|number} index Index for the specified bulkhead
* @return {object} The bulkhead component object * @return {Object} The bulkhead module object
*/ */
export function bulkheads(shipId, index) { export function bulkheads(shipId, index) {
let bulkhead = Ships[shipId].bulkheads[index]; let bulkhead = Ships[shipId].bulkheads[index];
bulkhead.class = 1; bulkhead.class = 1;
bulkhead.rating = 'I'; bulkhead.rating = 'I';
bulkhead.name = BulkheadNames[index] bulkhead.name = BulkheadNames[index];
return bulkhead; 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) { export function bulkheadIndex(bulkheadName) {
return BulkheadNames.indexOf(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) { export function isShieldGenerator(g) {
return g == 'sg' || g == 'psg' || g == 'bsg'; 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. * that the specified ship is eligible to use.
* *
* @param {string} shipId Unique ship Id/Key * @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) { export function forShip(shipId) {
let ship = Ships[shipId]; let ship = Ships[shipId];

View File

@@ -1,13 +1,16 @@
import { ModuleGroupToName, MountMap, BulkheadNames } from './Constants'; import { ModuleGroupToName, MountMap, BulkheadNames } from './Constants';
import { Ships } from 'coriolis-data'; import { Ships } from 'coriolis-data';
import Ship from './Ship'; import Ship from './Ship';
import ModuleUtils from './ModuleUtils'; import * as ModuleUtils from './ModuleUtils';
import LZString from 'lz-string'; import LZString from 'lz-string';
/** const STANDARD = ['powerPlant', 'thrusters', 'frameShiftDrive', 'lifeSupport', 'powerDistributor', 'sensors', 'fuelTank'];
* Service managing seralization and deserialization of models for use in URLs and persistene.
*/
/**
* Generates ship-loadout JSON Schema slot object
* @param {Object} slot Slot model
* @return {Object} JSON Schema Slot
*/
function slotToSchema(slot) { function slotToSchema(slot) {
if (slot.m) { if (slot.m) {
let o = { let o = {
@@ -32,19 +35,26 @@ function slotToSchema(slot) {
return null; 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, 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#', $schema: 'http://cdn.coriolis.io/schemas/ship-loadout/3.json#',
name: buildName, name: buildName,
ship: ship.name, ship: ship.name,
references: [{ references: [{
name: 'Coriolis.io', name: 'Coriolis.io',
url: `http://coriolis.io/outfit/${ship.id}/${code}?bn=${encodeURIComponent(buildName)}`, url: `https://coriolis.io/outfit/${ship.id}/${code}?bn=${encodeURIComponent(buildName)}`,
code: code, code,
shipId: ship.id shipId: ship.id
}], }],
components: { components: {
@@ -66,7 +76,7 @@ export function toDetailedBuild(buildName, ship, code) {
stats: {} stats: {}
}; };
for (var stat in ship) { for (let stat in ship) {
if (!isNaN(ship[stat])) { if (!isNaN(ship[stat])) {
data.stats[stat] = Math.round(ship[stat] * 100) / 100; data.stats[stat] = Math.round(ship[stat] * 100) / 100;
} }
@@ -75,66 +85,75 @@ export function toDetailedBuild(buildName, ship, code) {
return data; return data;
}; };
/**
* Instantiates a ship from a ship-loadout object
* @param {Object} detailedBuild ship-loadout object
* @return {Ship} Ship instance
*/
export function fromDetailedBuild(detailedBuild) { 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) { if (!shipId) {
throw 'No such ship: ' + detailedBuild.ship; throw 'No such ship: ' + detailedBuild.ship;
} }
var comps = detailedBuild.components; let comps = detailedBuild.components;
var standard = comps.standard; let stn = comps.standard;
var priorities = [ standard.cargoHatch && standard.cargoHatch.priority !== undefined ? standard.cargoHatch.priority - 1 : 0 ]; let priorities = [stn.cargoHatch && stn.cargoHatch.priority !== undefined ? stn.cargoHatch.priority - 1 : 0];
var enabled = [ standard.cargoHatch && standard.cargoHatch.enabled !== undefined ? standard.cargoHatch.enabled : true ]; let enabled = [stn.cargoHatch && stn.cargoHatch.enabled !== undefined ? stn.cargoHatch.enabled : true];
var shipData = ShipsDB[shipId]; let shipData = Ships[shipId];
var ship = new Ship(shipId, shipData.properties, shipData.slots); let ship = new Ship(shipId, shipData.properties, shipData.slots);
var bulkheads = ModuleUtils.bulkheadIndex(standard.bulkheads); let bulkheads = ModuleUtils.bulkheadIndex(stn.bulkheads);
if (bulkheads < 0) { if (bulkheads < 0) {
throw 'Invalid bulkheads: ' + standard.bulkheads; throw 'Invalid bulkheads: ' + stn.bulkheads;
} }
var standardIds = _.map( let standard = STANDARD.map((c) => {
['powerPlant', 'thrusters', 'frameShiftDrive', 'lifeSupport', 'powerDistributor', 'sensors', 'fuelTank'], if (!stn[c].class || !stn[c].rating) {
function(c) {
if (!standard[c].class || !standard[c].rating) {
throw 'Invalid value for ' + c; throw 'Invalid value for ' + c;
} }
priorities.push(standard[c].priority === undefined ? 0 : standard[c].priority - 1); priorities.push(stn[c].priority === undefined ? 0 : stn[c].priority - 1);
enabled.push(standard[c].enabled === undefined ? true : standard[c].enabled); enabled.push(stn[c].enabled === undefined ? true : stn[c].enabled);
return standard[c].class + standard[c].rating; 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) { let hardpoints = comps.hardpoints
return c ? ModuleUtils.findHardpointId(c.group, c.class, c.rating, c.name, MountMap[c.mount], c.missile) : 0; .map(c => c ? ModuleUtils.findHardpointId(c.group, c.class, c.rating, c.name, MountMap[c.mount], c.missile) : 0)
}).concat(_.map(comps.utility, function(c) { .concat(comps.utility.map(c => c ? ModuleUtils.findHardpointId(c.group, c.class, c.rating, c.name, MountMap[c.mount]) : 0));
return 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 // 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; }), priorities = priorities.concat(
_.map(comps.utility, function(c) { return (!c || c.priority === undefined) ? 0 : c.priority - 1; }), comps.hardpoints.map(c => (!c || c.priority === undefined) ? 0 : c.priority - 1),
_.map(comps.internal, function(c) { return (!c || c.priority === undefined) ? 0 : c.priority - 1; })); comps.utility.map(c => (!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; }), comps.internal.map(c => (!c || c.priority === undefined) ? 0 : c.priority - 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; })); 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; 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) { export function toDetailedExport(builds) {
var data = []; let data = [];
for (var shipId in builds) { for (let shipId in builds) {
for (var buildName in builds[shipId]) { for (let buildName in builds[shipId]) {
var code = builds[shipId][buildName]; let code = builds[shipId][buildName];
var shipData = Ships[shipId]; let shipData = Ships[shipId];
var ship = new Ship(shipId, shipData.properties, shipData.slots); let ship = new Ship(shipId, shipData.properties, shipData.slots);
ship.buildFrom(code); ship.buildFrom(code);
data.push(toDetailedBuild(buildName, ship, code)); data.push(toDetailedBuild(buildName, ship, code));
} }
@@ -142,22 +161,31 @@ export function toDetailedExport(builds) {
return data; 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) { export function fromComparison(name, builds, facets, predicate, desc) {
var shipBuilds = []; return LZString.compressToBase64(JSON.stringify({
builds.forEach(function(b) {
shipBuilds.push({ s: b.id, n: b.buildName, c: fromShip(b) });
}.bind(this));
return LZString.compressToBase64(angular.toJson({
n: name, n: name,
b: shipBuilds, b: builds.map((b) => { return { s: b.id, n: b.buildName, c: b.toString() }; }),
f: facets, f: facets,
p: predicate, p: predicate,
d: desc ? 1 : 0 d: desc ? 1 : 0
})).replace(/\//g, '-'); })).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) { export function toComparison(code) {
return JSON.parse(LZString.decompressFromBase64(code.replace(/-/g, '/'))); return JSON.parse(LZString.decompressFromBase64(code.replace(/-/g, '/')));
}; };

View File

@@ -20,6 +20,14 @@ function powerUsageType(slot, modul) {
return slot.cat != 1 ? 'retracted' : 'deployed'; 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) { function decodeToArray(code, arr, codePos) {
for (let i = 0; i < arr.length; i++) { for (let i = 0; i < arr.length; i++) {
if (code.charAt(codePos) == '-') { 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 { export default class Ship {
@@ -89,7 +97,7 @@ export default class Ship {
this.hardpoints this.hardpoints
); );
this.shipCostMultiplier = 1; this.shipCostMultiplier = 1;
this.modulCostMultiplier = 1; this.moduleCostMultiplier = 1;
this.priorityBands = [ this.priorityBands = [
{ deployed: 0, retracted: 0, }, { deployed: 0, retracted: 0, },
{ deployed: 0, retracted: 0, }, { deployed: 0, retracted: 0, },
@@ -99,23 +107,33 @@ export default class Ship {
]; ];
} }
//*********// /* GETTERS */
// GETTERS //
//*********//
/**
* [getAvailableModules description]
* @return {[type]} [description]
*/
getAvailableModules() { getAvailableModules() {
return this.availCS; return this.availCS;
} }
/**
* Can the ship thrust/move
* @return {[type]} True if thrusters operational
*/
canThrust() { canThrust() {
return this.getSlotStatus(this.standard[1]) == 3 // Thrusters are powered return this.getSlotStatus(this.standard[1]) == 3 && // Thrusters are powered
&& this.ladenMass < this.standard[1].m.maxmass; // Max mass not exceeded this.ladenMass < this.standard[1].m.maxmass; // Max mass not exceeded
} }
/**
* Can the ship boost
* @return {[type]} True if boost capable
*/
canBoost() { canBoost() {
return this.canThrust() // Thrusters operational return this.canThrust() && // Thrusters operational
&& this.getSlotStatus(this.standard[4]) == 3 // Power distributor operational this.getSlotStatus(this.standard[4]) == 3 && // Power distributor operational
&& this.boostEnergy <= this.standard[4].m.enginecapacity; // PD capacitor is sufficient for boost 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) * 1 - Disabled (Switched off)
* 2 - Offline (Insufficient power available) * 2 - Offline (Insufficient power available)
* 3 - Online * 3 - Online
* @param {[type]} slot [description] * @param {Object} slot Slot model
* @param {boolean} deployed True - power used when hardpoints are deployed * @param {boolean} deployed True - power used when hardpoints are deployed
* @return {number} status index * @return {number} status index
*/ */
@@ -145,18 +163,54 @@ export default class Ship {
/** /**
* Calculate jump range using the installed FSD and the * Calculate jump range using the installed FSD and the
* specified mass which can be more or less than ships actual mass * 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 * @param {number} fuel Fuel available in tons
* @param {number} cargo Cargo in tons
* @return {number} Jump range in Light Years * @return {number} Jump range in Light Years
*/ */
getJumpRangeWith(fuel, cargo) { getJumpRangeWith(fuel, cargo) {
return Calc.jumpRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel); 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) { getFastestRangeWith(fuel, cargo) {
return Calc.totalRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel); 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) { getSpeedsWith(fuel, cargo) {
return Calc.speed(this.unladenMass + fuel + cargo, this.speed, this.boost, this.standard[1].m, this.pipSpeed); 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 * @return {number} The index of the slot in ship.internal
*/ */
findInternalByGroup(group) { findInternalByGroup(group) {
var index; let index;
if (ModuleUtils.isShieldGenerator(group)) { if (ModuleUtils.isShieldGenerator(group)) {
return this.internal.find(slot => slot.m && ModuleUtils.isShieldGenerator(slot.m.grp)); return this.internal.find(slot => slot.m && ModuleUtils.isShieldGenerator(slot.m.grp));
} else { } else {
@@ -176,6 +230,10 @@ export default class Ship {
} }
} }
/**
* Serializes the ship to a string
* @return {string} Serialized ship 'code'
*/
toString() { toString() {
return [ return [
this.getStandardString(), this.getStandardString(),
@@ -188,6 +246,10 @@ export default class Ship {
].join(''); ].join('');
} }
/**
* Serializes the standard modules to a string
* @return {string} Serialized standard modules 'code'
*/
getStandardString() { getStandardString() {
if(!this.serialized.standard) { if(!this.serialized.standard) {
this.serialized.standard = this.bulkheads.index + this.standard.reduce((arr, slot, i) => { this.serialized.standard = this.bulkheads.index + this.standard.reduce((arr, slot, i) => {
@@ -198,6 +260,10 @@ export default class Ship {
return this.serialized.standard; return this.serialized.standard;
} }
/**
* Serializes the internal modules to a string
* @return {string} Serialized internal modules 'code'
*/
getInternalString() { getInternalString() {
if(!this.serialized.internal) { if(!this.serialized.internal) {
this.serialized.internal = this.internal.reduce(reduceToIDs, new Array(this.internal.length)).join(''); 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; return this.serialized.internal;
} }
/**
* Serializes the hardpoints and utility modules to a string
* @return {string} Serialized hardpoints and utility modules 'code'
*/
getHardpointsString() { getHardpointsString() {
if(!this.serialized.hardpoints) { if(!this.serialized.hardpoints) {
this.serialized.hardpoints = this.hardpoints.reduce(reduceToIDs, new Array(this.hardpoints.length)).join(''); 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; return this.serialized.hardpoints;
} }
/**
* Get the serialized module active/inactive settings
* @return {string} Serialized active/inactive settings
*/
getPowerEnabledString() { getPowerEnabledString() {
return this.serialized.enabled; return this.serialized.enabled;
} }
/**
* Get the serialized module priority settings
* @return {string} Serialized priority settings
*/
getPowerPrioritesString() { getPowerPrioritesString() {
return this.serialized.priorities; return this.serialized.priorities;
} }
//**********************// /* Mutate / Update Ship */
// Mutate / Update Ship //
//**********************//
/** /**
* Recalculate all item costs and total based on discounts. * Recalculate all item costs and total based on discounts.
* @param {number} shipCostMultiplier Ship cost multiplier discount (e.g. 0.9 === 10% discount) * @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) { applyDiscounts(shipCostMultiplier, moduleCostMultiplier) {
var total = 0; let total = 0;
var costList = this.costList; let costList = this.costList;
for (var i = 0, l = costList.length; i < l; i++) { for (let i = 0, l = costList.length; i < l; i++) {
var item = costList[i]; let item = costList[i];
if (item.m && item.m.cost) { 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) { if (item.incCost) {
total += item.discountedCost; total += item.discountedCost;
} }
} }
} }
this.shipCostMultiplier = shipCostMultiplier; this.shipCostMultiplier = shipCostMultiplier;
this.modulCostMultiplier = modulCostMultiplier; this.moduleCostMultiplier = moduleCostMultiplier;
this.totalCost = total; this.totalCost = total;
return this; return this;
} }
/** /**
* Builds/Updates the ship instance with the ModuleUtils[comps] passed in. * 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) { buildWith(comps, priorities, enabled) {
var internal = this.internal, let internal = this.internal,
standard = this.standard, standard = this.standard,
hps = this.hardpoints, hps = this.hardpoints,
bands = this.priorityBands, bands = this.priorityBands,
@@ -350,7 +430,7 @@ export default class Ship {
* @param {string} serializedString The string to deserialize * @param {string} serializedString The string to deserialize
*/ */
buildFrom(serializedString) { buildFrom(serializedString) {
var standard = new Array(this.standard.length), let standard = new Array(this.standard.length),
hardpoints = new Array(this.hardpoints.length), hardpoints = new Array(this.hardpoints.length),
internal = new Array(this.internal.length), internal = new Array(this.internal.length),
parts = serializedString.split('.'), parts = serializedString.split('.'),
@@ -371,31 +451,43 @@ export default class Ship {
this.buildWith( this.buildWith(
{ {
bulkheads: code.charAt(0) * 1, bulkheads: code.charAt(0) * 1,
standard: standard, standard,
hardpoints: hardpoints, hardpoints,
internal: internal internal
}, },
priorities, priorities,
enabled enabled
); );
}; };
/**
* Empties all hardpoints and utility slots
* @return {this} The current ship instance for chaining
*/
emptyHardpoints() { emptyHardpoints() {
for (var i = this.hardpoints.length; i--; ) { for (let i = this.hardpoints.length; i--;) {
this.use(this.hardpoints[i], null); this.use(this.hardpoints[i], null);
} }
return this; return this;
} }
/**
* Empties all Internal slots
* @return {this} The current ship instance for chaining
*/
emptyInternal() { emptyInternal() {
for (var i = this.internal.length; i--; ) { for (let i = this.internal.length; i--;) {
this.use(this.internal[i], null); this.use(this.internal[i], null);
} }
return this; return this;
} }
/**
* Empties all Utility slots
* @return {this} The current ship instance for chaining
*/
emptyUtility() { emptyUtility() {
for (var i = this.hardpoints.length; i--; ) { for (let i = this.hardpoints.length; i--;) {
if (!this.hardpoints[i].maxClass) { if (!this.hardpoints[i].maxClass) {
this.use(this.hardpoints[i], null); this.use(this.hardpoints[i], null);
} }
@@ -403,8 +495,12 @@ export default class Ship {
return this; return this;
} }
/**
* Empties all hardpoints
* @return {this} The current ship instance for chaining
*/
emptyWeapons() { emptyWeapons() {
for (var i = this.hardpoints.length; i--; ) { for (let i = this.hardpoints.length; i--;) {
if (this.hardpoints[i].maxClass) { if (this.hardpoints[i].maxClass) {
this.use(this.hardpoints[i], null); 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 * Optimize for the lower mass build that can still boost and power the ship
* without power management. * without power management.
* @param {object} m Standard Module overrides * @param {object} m Standard Module overrides
* @return {this} The current ship instance for chaining
*/ */
optimizeMass(m) { optimizeMass(m) {
return this.emptyHardpoints().emptyInternal().useLightestStandard(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) { setCostIncluded(item, included) {
if (item.incCost != included && item.m) { if (item.incCost != included && item.m) {
this.totalCost += included ? item.discountedCost : -item.discountedCost; this.totalCost += included ? item.discountedCost : -item.discountedCost;
@@ -429,6 +532,12 @@ export default class Ship {
return this; 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) { setSlotEnabled(slot, enabled) {
if (slot.enabled != enabled) { // Enabled state is changing if (slot.enabled != enabled) { // Enabled state is changing
slot.enabled = enabled; slot.enabled = enabled;
@@ -459,11 +568,11 @@ export default class Ship {
*/ */
setSlotPriority(slot, newPriority) { setSlotPriority(slot, newPriority) {
if (newPriority >= 0 && newPriority < this.priorityBands.length) { if (newPriority >= 0 && newPriority < this.priorityBands.length) {
var oldPriority = slot.priority; let oldPriority = slot.priority;
slot.priority = newPriority; slot.priority = newPriority;
if (slot.enabled) { // Only update power if the slot is enabled 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[oldPriority][usage] -= slot.m.power;
this.priorityBands[newPriority][usage] += slot.m.power; this.priorityBands[newPriority][usage] += slot.m.power;
this.updatePowerPrioritesString(); this.updatePowerPrioritesString();
@@ -483,15 +592,15 @@ export default class Ship {
* @return {this} The ship instance (for chaining operations) * @return {this} The ship instance (for chaining operations)
*/ */
updateStats(slot, n, old, preventUpdate) { updateStats(slot, n, old, preventUpdate) {
var powerChange = slot == this.standard[0]; let powerChange = slot == this.standard[0];
if (old) { // Old modul now being removed if (old) { // Old modul now being removed
switch (old.grp) { switch (old.grp) {
case 'ft': case 'ft':
this.fuelCapacity -= old.capacity; this.fuelCapacity -= old.fuel;
break; break;
case 'cr': case 'cr':
this.cargoCapacity -= old.capacity; this.cargoCapacity -= old.cargo;
break; break;
case 'hr': case 'hr':
this.armourAdded -= old.armouradd; this.armourAdded -= old.armouradd;
@@ -502,7 +611,7 @@ export default class Ship {
} }
if (slot.incCost && old.cost) { if (slot.incCost && old.cost) {
this.totalCost -= old.cost * this.modulCostMultiplier; this.totalCost -= old.cost * this.moduleCostMultiplier;
} }
if (old.power && slot.enabled) { if (old.power && slot.enabled) {
@@ -519,10 +628,10 @@ export default class Ship {
if (n) { if (n) {
switch (n.grp) { switch (n.grp) {
case 'ft': case 'ft':
this.fuelCapacity += n.capacity; this.fuelCapacity += n.fuel;
break; break;
case 'cr': case 'cr':
this.cargoCapacity += n.capacity; this.cargoCapacity += n.cargo;
break; break;
case 'hr': case 'hr':
this.armourAdded += n.armouradd; this.armourAdded += n.armouradd;
@@ -533,7 +642,7 @@ export default class Ship {
} }
if (slot.incCost && n.cost) { if (slot.incCost && n.cost) {
this.totalCost += n.cost * this.modulCostMultiplier; this.totalCost += n.cost * this.moduleCostMultiplier;
} }
if (n.power && slot.enabled) { if (n.power && slot.enabled) {
@@ -561,12 +670,16 @@ export default class Ship {
return this; return this;
} }
/**
* Update all power calculations
* @return {this} The ship instance (for chaining operations)
*/
updatePower() { updatePower() {
var bands = this.priorityBands; let bands = this.priorityBands;
var prevRetracted = 0, prevDeployed = 0; let prevRetracted = 0, prevDeployed = 0;
for (var i = 0, l = bands.length; i < l; i++) { for (let i = 0, l = bands.length; i < l; i++) {
var band = bands[i]; let band = bands[i];
prevRetracted = band.retractedSum = prevRetracted + band.retracted; prevRetracted = band.retractedSum = prevRetracted + band.retracted;
prevDeployed = band.deployedSum = prevDeployed + band.deployed + band.retracted; prevDeployed = band.deployedSum = prevDeployed + band.deployed + band.retracted;
} }
@@ -577,33 +690,47 @@ export default class Ship {
return this; return this;
}; };
/**
* Update top speed and boost
* @return {this} The ship instance (for chaining operations)
*/
updateTopSpeed() { 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.topSpeed = speeds['4 Pips'];
this.topBoost = speeds.boost; this.topBoost = speeds.boost;
return this; return this;
} }
/**
* Update Shield strength
* @return {this} The ship instance (for chaining operations)
*/
updateShieldStrength() { 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; this.shieldStrength = sgSlot && sgSlot.enabled ? Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, this.shieldMultiplier) : 0;
return this; return this;
} }
/** /**
* Jump Range and total range calculations * Jump Range and total range calculations
* @return {this} The ship instance (for chaining operations)
*/ */
updateJumpStats() { updateJumpStats() {
var fsd = this.standard[2].m; // Frame Shift Drive; let fsd = this.standard[2].m; // Frame Shift Drive;
this.unladenRange = Calc.jumpRange(this.unladenMass + fsd.maxfuel, fsd, this.fuelCapacity); // Include fuel weight for jump let { unladenMass, ladenMass, fuelCapacity } = this;
this.fullTankRange = Calc.jumpRange(this.unladenMass + this.fuelCapacity, fsd, this.fuelCapacity); // Full Tanke this.unladenRange = this.getUnladenRange(); // Includes fuel weight for jump
this.ladenRange = Calc.jumpRange(this.ladenMass, fsd, this.fuelCapacity); this.fullTankRange = Calc.jumpRange(unladenMass + fuelCapacity, fsd); // Full Tank
this.unladenTotalRange = Calc.totalRange(this.unladenMass, fsd, this.fuelCapacity); this.ladenRange = this.getLadenRange(); // Includes full tank and caro
this.ladenTotalRange = Calc.totalRange(this.unladenMass + this.cargoCapacity, fsd, this.fuelCapacity); this.unladenTotalRange = Calc.totalRange(unladenMass, fsd, fuelCapacity);
this.maxJumpCount = Math.ceil(this.fuelCapacity / fsd.maxfuel); this.ladenTotalRange = Calc.totalRange(unladenMass + this.cargoCapacity, fsd, fuelCapacity);
this.maxJumpCount = Math.ceil(fuelCapacity / fsd.maxfuel);
return this; return this;
} }
/**
* Update the serialized power priorites string
* @return {this} The ship instance (for chaining operations)
*/
updatePowerPrioritesString() { updatePowerPrioritesString() {
let priorities = [this.cargoHatch.priority]; let priorities = [this.cargoHatch.priority];
@@ -621,6 +748,10 @@ export default class Ship {
return this; return this;
} }
/**
* Update the serialized power active/inactive string
* @return {this} The ship instance (for chaining operations)
*/
updatePowerEnabledString() { updatePowerEnabledString() {
let enabled = [this.cargoHatch.enabled ? 1 : 0]; 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. * 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). * Has logic handling ModuleUtils that you may only have 1 of (Shield Generator or Refinery).
* *
* @param {object} slot The modul slot * @param {Object} slot The modul slot
* @param {string} id Unique ID for the selected module * @param {Object} m Properties for the selected module
* @param {object} modul Properties for the selected module * @param {Boolean} preventUpdate If true, do not update aggregated stats
* @param {boolean} preventUpdate If true, do not update aggregated stats * @return {this} The ship instance (for chaining operations)
*/ */
use(slot, m, preventUpdate) { use(slot, m, preventUpdate) {
if (slot.m != m) { // Selecting a different modul 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 // 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) { if (slot.cat == 2 && m && UNIQUE_MODULES.indexOf(m.grp) != -1) {
// Find another internal slot that already has this type/group installed // 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 another slot has an installed modul with of the same type
if (!preventUpdate && similarSlot && similarSlot !== slot) { if (!preventUpdate && similarSlot && similarSlot !== slot) {
this.updateStats(similarSlot, null, similarSlot.m); this.updateStats(similarSlot, null, similarSlot.m);
@@ -660,9 +791,9 @@ export default class Ship {
similarSlot.discountedCost = 0; similarSlot.discountedCost = 0;
} }
} }
var oldModule = slot.m; let oldModule = slot.m;
slot.m = 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); this.updateStats(slot, m, oldModule, preventUpdate);
switch (slot.cat) { switch (slot.cat) {
@@ -675,16 +806,16 @@ export default class Ship {
} }
/** /**
* [useBulkhead description] * Mount the specified bulkhead type (index)
* @param {[type]} index [description] * @param {number} index Bulkhead index [0-4]
* @param {[type]} preventUpdate [description] * @param {Boolean} preventUpdate Prevent summary update
* @return {[type]} [description] * @return {this} The ship instance (for chaining operations)
*/ */
useBulkhead(index, preventUpdate) { useBulkhead(index, preventUpdate) {
var oldBulkhead = this.bulkheads.m; let oldBulkhead = this.bulkheads.m;
this.bulkheads.index = index; this.bulkheads.index = index;
this.bulkheads.m = ModuleUtils.bulkheads(this.id, 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.armourMultiplier = ArmourMultiplier[index];
this.updateStats(this.bulkheads, this.bulkheads.m, oldBulkhead, preventUpdate); this.updateStats(this.bulkheads, this.bulkheads.m, oldBulkhead, preventUpdate);
this.serialized.standard = null; this.serialized.standard = null;
@@ -692,13 +823,14 @@ export default class Ship {
} }
/** /**
* [useStandard description] * Set all standard slots to use the speficied rating and class based on
* @param {[type]} rating [description] * the slot's max class
* @return {[type]} [description] * @param {string} rating Module Rating (A-E)
* @return {this} The ship instance (for chaining operations)
*/ */
useStandard(rating) { useStandard(rating) {
for (var i = this.standard.length - 1; i--; ) { // All except Fuel Tank for (let i = this.standard.length - 1; i--;) { // All except Fuel Tank
var id = this.standard[i].maxClass + rating; let id = this.standard[i].maxClass + rating;
this.use(this.standard[i], ModuleUtils.standard(i, id)); this.use(this.standard[i], ModuleUtils.standard(i, id));
} }
return this; return this;
@@ -707,6 +839,7 @@ export default class Ship {
/** /**
* Use the lightest standard ModuleUtils unless otherwise specified * Use the lightest standard ModuleUtils unless otherwise specified
* @param {object} m Module overrides * @param {object} m Module overrides
* @return {this} The ship instance (for chaining operations)
*/ */
useLightestStandard(m) { useLightestStandard(m) {
m = m || {}; m = m || {};
@@ -747,6 +880,14 @@ export default class Ship {
return this; 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) { useUtility(group, rating, name, clobber) {
let m = ModuleUtils.findHardpoint(group, 0, rating, name); let m = ModuleUtils.findHardpoint(group, 0, rating, name);
for (let i = this.hardpoints.length; i--;) { for (let i = this.hardpoints.length; i--;) {
@@ -757,6 +898,14 @@ export default class Ship {
return this; 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) { useWeapon(group, mount, missile, clobber) {
let hps = this.hardpoints; let hps = this.hardpoints;
for (let i = hps.length; i--;) { for (let i = hps.length; i--;) {

View File

@@ -8,6 +8,7 @@ const LS_KEY_INSURANCE = 'insurance';
const LS_KEY_DISCOUNTS = 'discounts'; const LS_KEY_DISCOUNTS = 'discounts';
const LS_KEY_STATE = 'state'; const LS_KEY_STATE = 'state';
const LS_KEY_SIZE_RATIO = 'sizeRatio'; const LS_KEY_SIZE_RATIO = 'sizeRatio';
const LS_KEY_TOOLTIPS = 'tooltips';
let LS; let LS;
@@ -20,21 +21,40 @@ try {
LS = null; LS = null;
} }
/**
* Safe localstorage put
* @param {string} key key
* @param {any} value data to store
*/
function _put(key, value) { function _put(key, value) {
if (LS) { if (LS) {
LS.setItem(key, typeof value != 'string' ? JSON.stringify(value) : value); 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) { function _getString(key) {
return LS.getItem(key); return LS.getItem(key);
} }
/**
* Safe localstorage get
* @param {string} key key
* @return {object | number} The stored data
*/
function _get(key) { function _get(key) {
let str = _getString(key); let str = _getString(key);
return str ? JSON.parse(str) : null; return str ? JSON.parse(str) : null;
} }
/**
* Safe localstorage delete
* @param {string} key key
*/
function _delete(key) { function _delete(key) {
if (LS) { if (LS) {
LS.removeItem(key); 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 { class Persist extends EventEmitter {
/**
* Create an instance
*/
constructor() { constructor() {
super(); super();
let buildJson = _get(LS_KEY_BUILDS); let buildJson = _get(LS_KEY_BUILDS);
let comparisonJson = _get(LS_KEY_COMPARISONS); let comparisonJson = _get(LS_KEY_COMPARISONS);
let tips = _get(LS_KEY_TOOLTIPS);
this.builds = buildJson ? buildJson : {}; this.builds = buildJson ? buildJson : {};
this.comparisons = comparisonJson ? comparisonJson : {}; this.comparisons = comparisonJson ? comparisonJson : {};
this.buildCount = Object.keys(this.builds).length; this.buildCount = Object.keys(this.builds).length;
this.langCode = _getString(LS_KEY_LANG) || 'en'; this.langCode = _getString(LS_KEY_LANG) || 'en';
this.insurance = _getString(LS_KEY_INSURANCE); 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.costTab = _getString(LS_KEY_COST_TAB);
this.state = _get(LS_KEY_STATE); this.state = _get(LS_KEY_STATE);
this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1; this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1;
this.tooltipsEnabled = tips === null ? true : tips;
} }
/**
* Get the current language code
* @return {stirng} language code
*/
getLangCode() { getLangCode() {
return this.langCode; return this.langCode;
}; };
/**
* Update and save the current language
* @param {string} langCode language code
*/
setLangCode(langCode) { setLangCode(langCode) {
this.langCode = langCode; this.langCode = langCode;
_put(LS_KEY_LANG, langCode); _put(LS_KEY_LANG, langCode);
this.emit('language', 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. * Persist a ship build in local storage.
* *
@@ -107,6 +155,11 @@ class Persist extends EventEmitter {
return null; 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) { getBuilds(shipId) {
if(shipId && shipId.length > 0) { if(shipId && shipId.length > 0) {
return this.builds[shipId]; return this.builds[shipId];
@@ -114,6 +167,11 @@ class Persist extends EventEmitter {
return this.builds; 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) { getBuildsNamesFor(shipId) {
if (this.builds[shipId]) { if (this.builds[shipId]) {
return Object.keys(this.builds[shipId]).sort(); 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) { hasBuild(shipId, name) {
return this.builds[shipId] && this.builds[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() { hasBuilds() {
return Object.keys(this.builds).length > 0; return Object.keys(this.builds).length > 0;
} }
@@ -145,9 +213,9 @@ class Persist extends EventEmitter {
} }
_put(LS_KEY_BUILDS, this.builds); _put(LS_KEY_BUILDS, this.builds);
// Check if the build was used in existing comparisons // Check if the build was used in existing comparisons
var comps = this.comparisons; let comps = this.comparisons;
for (var c in comps) { for (let c in comps) {
for (var i = 0; i < comps[c].builds.length; i++) { // For all builds in the current comparison 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) { if (comps[c].builds[i].shipId == shipId && comps[c].builds[i].buildName == name) {
comps[c].builds.splice(i, 1); comps[c].builds.splice(i, 1);
break; // A build is unique per comparison break; // A build is unique per comparison
@@ -171,7 +239,7 @@ class Persist extends EventEmitter {
this.comparisons[name] = {}; this.comparisons[name] = {};
} }
this.comparisons[name] = { this.comparisons[name] = {
facets: facets, facets,
builds: builds.map(b => { return { shipId: b.id || b.shipId, buildName: b.buildName }; }) builds: builds.map(b => { return { shipId: b.id || b.shipId, buildName: b.buildName }; })
}; };
_put(LS_KEY_COMPARISONS, this.comparisons); _put(LS_KEY_COMPARISONS, this.comparisons);
@@ -190,14 +258,27 @@ class Persist extends EventEmitter {
return null; return null;
}; };
/**
* Get all saved comparisons
* @return {Object} All comparisons
*/
getComparisons() { getComparisons() {
return this.comparisons; 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) { hasComparison(name) {
return !!this.comparisons[name]; return !!this.comparisons[name];
} }
/**
* Check if any comparisons have been saved
* @return {Boolean} True if any comparisons have been saved
*/
hasComparisons() { hasComparisons() {
return Object.keys(this.comparisons).length > 0; return Object.keys(this.comparisons).length > 0;
} }
@@ -222,11 +303,15 @@ class Persist extends EventEmitter {
this.comparisons = {}; this.comparisons = {};
_delete(LS_KEY_BUILDS); _delete(LS_KEY_BUILDS);
_delete(LS_KEY_COMPARISONS); _delete(LS_KEY_COMPARISONS);
this.emit('deletedall'); this.emit('deletedAll');
}; };
/**
* Get all saved data and settings
* @return {Object} Data and settings
*/
getAll() { getAll() {
var data = {}; let data = {};
data[LS_KEY_BUILDS] = this.getBuilds(); data[LS_KEY_BUILDS] = this.getBuilds();
data[LS_KEY_COMPARISONS] = this.getComparisons(); data[LS_KEY_COMPARISONS] = this.getComparisons();
data[LS_KEY_INSURANCE] = this.getInsurance(); data[LS_KEY_INSURANCE] = this.getInsurance();
@@ -244,17 +329,17 @@ class Persist extends EventEmitter {
/** /**
* Persist selected insurance type * Persist selected insurance type
* @param {string} name Insurance type name * @param {string} insurance Insurance type name
*/ */
setInsurance(insurance) { setInsurance(insurance) {
this.insurance = insurance this.insurance = insurance;
_put(LS_KEY_INSURANCE, insurance); _put(LS_KEY_INSURANCE, insurance);
this.emit('insurance', insurance); this.emit('insurance', insurance);
}; };
/** /**
* Persist selected discount * Persist selected ship discount
* @param {number} val Discount value/amount * @param {number} shipDiscount Discount value/amount
*/ */
setShipDiscount(shipDiscount) { setShipDiscount(shipDiscount) {
this.discounts[0] = shipDiscount; this.discounts[0] = shipDiscount;
@@ -271,8 +356,8 @@ class Persist extends EventEmitter {
}; };
/** /**
* Persist selected discount * Persist selected module discount
* @param {number} val Discount value/amount * @param {number} moduleDiscount Discount value/amount
*/ */
setModuleDiscount(moduleDiscount) { setModuleDiscount(moduleDiscount) {
this.discounts[1] = moduleDiscount; this.discounts[1] = moduleDiscount;
@@ -290,7 +375,7 @@ class Persist extends EventEmitter {
/** /**
* Persist selected cost tab * Persist selected cost tab
* @param {number} val Discount value/amount * @param {number} tabName Cost tab name
*/ */
setCostTab(tabName) { setCostTab(tabName) {
this.costTab = tabName; this.costTab = tabName;
@@ -332,7 +417,7 @@ class Persist extends EventEmitter {
/** /**
* Save the current size ratio to localstorage * Save the current size ratio to localstorage
* @param {number} sizeRatio * @param {number} sizeRatio Size ratio scale
*/ */
setSizeRatio(sizeRatio) { setSizeRatio(sizeRatio) {
if (sizeRatio != this.sizeRatio) { if (sizeRatio != this.sizeRatio) {

View File

@@ -1,5 +1,3 @@
/** /**
* Generate a BBCode (Forum) compatible table from comparisons * Generate a BBCode (Forum) compatible table from comparisons
* @param {Function} translate Translate language function * @param {Function} translate Translate language function
@@ -9,8 +7,8 @@
* @param {string} link Link to the comparison * @param {string} link Link to the comparison
* @return {string} the BBCode * @return {string} the BBCode
*/ */
export default function comparisonBBCode(translate, formats, facets, builds, link) { export function comparisonBBCode(translate, formats, facets, builds, link) {
var colCount = 2, b, i, j, k, f, fl, p, pl, l = []; let colCount = 2, b, i, j, k, f, fl, p, pl, l = [];
for (i = 0; i < facets.length; i++) { for (i = 0; i < facets.length; i++) {
if (facets[i].active) { if (facets[i].active) {

View File

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

View File

@@ -10,15 +10,19 @@ const SHORTEN_API = 'https://www.googleapis.com/urlshortener/v1/url?key=';
*/ */
export default function shortenUrl(url, success, error) { export default function shortenUrl(url, success, error) {
if (window.navigator.onLine) { if (window.navigator.onLine) {
try {
request.post(SHORTEN_API + window.CORIOLIS_GAPI_KEY) request.post(SHORTEN_API + window.CORIOLIS_GAPI_KEY)
.send({ longUrl: url }) .send({ longUrl: url })
.end(function(err, response) { .end(function(err, response) {
if (err) { if (err) {
error('Error'); error(response.statusText == 'OK' ? 'Bad Request' : response.statusText);
} else { } else {
success(response.data.id); success(response.body.id);
} }
}); });
} catch (e) {
error(e.message ? e.message : e);
}
} else { } else {
error('Not Online'); error('Not Online');
} }

View File

@@ -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 * 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 * Slot name comparator
* @param {Function} translate Translate function
* @param {Object} a Slot object
* @param {Object} b Slot object
* @return {number} 1, 0, -1
*/
export function nameComparator(translate, a, b) {
return translate(a.name || a.grp).localeCompare(translate(b.name || b.grp));
}
/**
* Generates an internationalization friendly slot comparator that will
* sort by specified property (if provided) then by name/group, class, rating
* @param {function} translate Tranlation function * @param {function} translate Tranlation function
* @param {function} propComparator Optional property comparator
* @param {boolean} desc Use descending order
* @return {function} Comparator function for slot names * @return {function} Comparator function for slot names
*/ */
export function nameComparator(translate) { export function slotComparator(translate, propComparator, desc) {
return (a, b) => { return (a, b) => {
a = a.m; // retain descending order when sorting sorting by name/group/class/rating
b = b.m; let am = a.m; // Slot A's mounted module
let bm = b.m; // Slot B's mounted module
if (a && !b) { if (!desc) { // Flip A and B if ascending order
let t = a;
a = b;
b = t;
}
// Check for empty slots first
if (a.m && !b.m) {
return 1; return 1;
} else if (!a && b) { } else if (!a.m && b.m) {
return -1; return -1;
} else if (!a && !b) { } else if (!a.m && !b.m) {
return 0; 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; // 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;
} }
return translate(a.name || a.grp).localeCompare(translate(b.name || b.grp));
// 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>;
}

View File

@@ -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 (===) * Compares A and B and return true using strict comparison (===)
* @param {any} objA A * @param {any} objA A
* @param {any} objB B * @param {any} objB B
* @return {boolean} true if A === B OR A properties === B properties * @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) { if (objA === objB) {
return true; return true;
} }

View File

@@ -31,15 +31,15 @@
<script src="{%= o.htmlWebpackPlugin.files.chunks.lib.entry %}" charset="utf-8" crossorigin="anonymous"></script> <script src="{%= o.htmlWebpackPlugin.files.chunks.lib.entry %}" charset="utf-8" crossorigin="anonymous"></script>
<script src="{%= o.htmlWebpackPlugin.files.chunks.app.entry %}" charset="utf-8" crossorigin="anonymous"></script> <script 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(){ (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), (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) 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'); })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{%= o.htmlWebpackPlugin.options.uaTracking %}', 'auto'); ga('create', '{%= o.htmlWebpackPlugin.options.uaTracking %}', 'auto');
{% } %}
window.CORIOLIS_GAPI_KEY = '{%= o.htmlWebpackPlugin.options.gapiKey %}'; window.CORIOLIS_GAPI_KEY = '{%= o.htmlWebpackPlugin.options.gapiKey %}';
</script> </script>
{% } %}
</body> </body>
</html> </html>

View File

@@ -13,7 +13,7 @@
@import 'select'; @import 'select';
@import 'modal'; @import 'modal';
@import 'charts'; @import 'charts';
@import 'chart-tooltip'; @import 'tooltip';
@import 'buttons'; @import 'buttons';
@import 'error'; @import 'error';
@import 'sortable'; @import 'sortable';

View File

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

View File

@@ -27,6 +27,11 @@
fill: @fg; fill: @fg;
} }
.muted {
color: @muted;
fill: @muted;
}
.disabled { .disabled {
color: @disabled; color: @disabled;
fill: @disabled; fill: @disabled;

View File

@@ -97,6 +97,7 @@
clear: both; clear: both;
margin: 1em 0 0; margin: 1em 0 0;
overflow-y:auto; overflow-y:auto;
text-align: center;
h1 { h1 {
display: inline-block; display: inline-block;
@@ -107,8 +108,7 @@
table { table {
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
width: 40%; width: 45%;
vertical-align: top;
.smallTablet({ .smallTablet({
width: 100%; width: 100%;
@@ -120,19 +120,24 @@
tbody { tbody {
display: block; display: block;
font-size: 0.8em;
width: 100%;
overflow-y: auto; overflow-y: auto;
z-index: 0; z-index: 0;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
height: 8em; height: 8em;
max-height: 8em; max-height: 8em;
} }
}
td { td {
cursor: pointer; cursor: pointer;
vertical-align: top;
border-bottom: 1px solid @primary-disabled;
} }
} }
}
#comp-tbl { #comp-tbl {
margin: 1em auto; margin: 1em auto;
white-space: nowrap; white-space: nowrap;

View File

@@ -9,7 +9,7 @@ select {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
appearance: none; appearance: none;
padding: 0.1em 0.5em; padding: 0 0.5em;
outline:none; outline:none;
border: 0; border: 0;

View File

@@ -14,7 +14,7 @@
color: @fg; color: @fg;
fill: @fg; fill: @fg;
.details { .details-container {
min-height: 2.7em; min-height: 2.7em;
padding: 0.25em 0; padding: 0.25em 0;
box-sizing: border-box; box-sizing: border-box;
@@ -24,6 +24,11 @@
text-transform: none; text-transform: none;
} }
.details {
min-height: 2.2em;
background-color: @primary-bg;
}
.name { .name {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
@@ -75,5 +80,58 @@
background-color: @primary-bg; background-color: @primary-bg;
border-right: 1px solid @primary; 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
View 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;
}
}

View File

@@ -29,3 +29,11 @@
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
} }
.transform(@transform) {
-webkit-transform: @transform;
-moz-transform: @transform;
-ms-transform: @transform;
-o-transform: @transform;
transform: @transform;
}

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,8 @@ module.exports = {
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
inject: false, inject: false,
template: path.join(__dirname, "src/index.html"), template: path.join(__dirname, "src/index.html"),
version: pkgJson.version version: pkgJson.version,
gapiKey: process.env.CORIOLIS_GAPI_KEY || '',
}), }),
new ExtractTextPlugin('app.css', { new ExtractTextPlugin('app.css', {
allChunks: true allChunks: true