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,
"env": {
"development": {
"plugins": ["react-transform"],
"extra": {
"react-transform": {
"transforms": [{
"transform": "react-transform-hmr",
"imports": ["react"],
"locals": ["module"]
}, {
"transform": "react-transform-catch-errors",
"imports": ["react", "redbox-react"]
}]
}
}
}
}
"presets": ["es2015", "react", "stage-0"]
}

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

View File

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

View File

@@ -8,39 +8,67 @@
"homepage": "http://coriolis.io",
"bugs": "https://github.com/cmmcleod/coriolis/issues",
"private": true,
"engine": "node >= 0.12.2",
"engine": "node >= 4.0.0",
"license": "MIT",
"scripts": {
"clean": "rimraf build",
"start": "node devServer.js",
"lint": "eslint --ext .js,.jsx src",
"test": "jest",
"prod-serve": "nginx -p $(pwd) -c nginx.conf",
"prod-stop": "kill -QUIT $(cat nginx.pid)",
"build:prod": "npm run clean && NODE_ENV=production CDN='//cdn.coriolis.io' webpack -d -p --config webpack.config.prod.js",
"build": "npm run clean && NODE_ENV=production webpack -d -p --config webpack.config.prod.js",
"rsync": "rsync -e 'ssh -i $CORIOLIS_PEM' -a --delete build/ $CORIOLIS_USER@$CORIOLIS_HOST:~/www",
"deploy": "npm run lint && npm run build && npm run rsync"
"deploy": "npm run lint && npm test && npm run build:prod && npm run rsync"
},
"jest": {
"scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
"testFileExtensions": [
"js"
],
"moduleFileExtensions": [
"js",
"json",
"jsx"
],
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/react",
"<rootDir>/node_modules/react-dom",
"<rootDir>/node_modules/react-addons-test-utils",
"<rootDir>/node_modules/fbjs",
"<rootDir>/node_modules/fbemitter",
"<rootDir>/node_modules/classnames",
"<rootDir>/node_modules/d3",
"<rootDir>/node_modules/lz-string",
"<rootDir>/node_modules/coriolis-data",
"<rootDir>/src/app/shipyard",
"<rootDir>/src/app/i18n",
"<rootDir>/src/app/utils"
]
},
"devDependencies": {
"appcache-webpack-plugin": "^1.2.1",
"babel-core": "^5.4.7",
"babel-eslint": "^4.1.6",
"babel-loader": "^5.1.2",
"babel-plugin-react-transform": "^1.1.1",
"css-loader": "^0.23.0",
"eslint": "^1.10.1",
"eslint-plugin-react": "^2.3.0",
"babel-core": "*",
"babel-eslint": "*",
"babel-jest": "*",
"babel-loader": "*",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"babel-preset-stage-0": "^6.3.13",
"css-loader": "^0.23.0",
"eslint": "2.0.0-beta.1",
"eslint-plugin-react": "^3.15.0",
"expose-loader": "^0.7.1",
"express": "^4.13.3",
"extract-text-webpack-plugin": "^0.9.1",
"file-loader": "^0.8.4",
"html-webpack-plugin": "^1.7.0",
"jest-cli": "*",
"json-loader": "^0.5.3",
"less": "^2.5.3",
"less-loader": "^2.2.1",
"react-transform-catch-errors": "^1.0.0",
"react-transform-hmr": "^1.0.0",
"redbox-react": "^1.0.1",
"react-addons-test-utils": "^0.14.6",
"rimraf": "^2.4.3",
"style-loader": "^0.13.0",
"url-loader": "^0.5.6",
@@ -52,8 +80,8 @@
"d3": "^3.5.9",
"fbemitter": "^2.0.0",
"lz-string": "^1.4.4",
"react": "^0.14.2",
"react-dom": "^0.14.2",
"react": "^0.14.6",
"react-dom": "^0.14.6",
"superagent": "^1.4.0"
}
}

View File

@@ -1,24 +1,39 @@
import React from 'react';
import Router from './Router';
import { EventEmitter } from 'fbemitter';
import { getLanguage } from './i18n/Language';
import Persist from './stores/Persist';
import InterfaceEvents from './utils/InterfaceEvents';
import Header from './components/Header';
import Tooltip from './components/Tooltip';
import AboutPage from './pages/AboutPage';
import NotFoundPage from './pages/NotFoundPage';
import OutfittingPage from './pages/OutfittingPage';
import ComparisonPage from './pages/ComparisonPage';
import ShipyardPage from './pages/ShipyardPage';
/**
* Coriolis App
*/
export default class Coriolis extends React.Component {
static childContextTypes = {
language: React.PropTypes.object.isRequired,
sizeRatio: React.PropTypes.number.isRequired,
route: React.PropTypes.object.isRequired
route: React.PropTypes.object.isRequired,
openMenu: React.PropTypes.func.isRequired,
closeMenu: React.PropTypes.func.isRequired,
showModal: React.PropTypes.func.isRequired,
hideModal: React.PropTypes.func.isRequired,
tooltip: React.PropTypes.func.isRequired,
termtip: React.PropTypes.func.isRequired,
onWindowResize: React.PropTypes.func.isRequired
};
/**
* Creates an instance of the Coriolis App
*/
constructor() {
super();
this._setPage = this._setPage.bind(this);
@@ -26,10 +41,14 @@ export default class Coriolis extends React.Component {
this._closeMenu = this._closeMenu.bind(this);
this._showModal = this._showModal.bind(this);
this._hideModal = this._hideModal.bind(this);
this._tooltip = this._tooltip.bind(this);
this._termtip = this._termtip.bind(this);
this._onWindowResize = this._onWindowResize.bind(this);
this._onLanguageChange = this._onLanguageChange.bind(this);
this._onSizeRatioChange = this._onSizeRatioChange.bind(this)
this._onSizeRatioChange = this._onSizeRatioChange.bind(this);
this._keyDown = this._keyDown.bind(this);
this.emitter = new EventEmitter();
this.state = {
page: null,
language: getLanguage(Persist.getLangCode()),
@@ -45,23 +64,49 @@ export default class Coriolis extends React.Component {
Router('*', (r) => this._setPage(null, r));
}
/**
* Updates / Sets the page and route context
* @param {[type]} page The page to be shown
* @param {Object} route The current route
*/
_setPage(page, route) {
this.setState({ page, route, currentMenu: null });
}
/**
* Handle unexpected error
* TODO: Implement and fix to work with Webpack (dev + prod)
* @param {string} msg Message
* @param {string} scriptUrl URL
* @param {number} line Line number
* @param {number} col Column number
* @param {Object} errObj Error Object
*/
_onError(msg, scriptUrl, line, col, errObj) {
console.log('WINDOW ERROR', arguments);
//this._setPage(<div>Some errors occured!!</div>);
// this._setPage(<div>Some errors occured!!</div>);
}
/**
* Propagate language and format changes
* @param {string} lang Language code
*/
_onLanguageChange(lang) {
this.setState({ language: getLanguage(Persist.getLangCode()) });
}
/**
* Propagate the sizeRatio change
* @param {number} sizeRatio Size ratio / scale
*/
_onSizeRatioChange(sizeRatio) {
this.setState({ sizeRatio });
}
/**
* Handle Key Down
* @param {Event} e Keyboard Event
*/
_keyDown(e) {
switch (e.keyCode) {
case 27:
@@ -108,6 +153,43 @@ export default class Coriolis extends React.Component {
}
}
/**
* Show/Hide the tooltip
* @param {React.Component} content Tooltip content
* @param {DOMRect} rect Target bounding rect
* @param {[type]} opts Options
*/
_tooltip(content, rect, opts) {
if (!content && this.state.tooltip) {
this.setState({ tooltip: null });
} else if (content && Persist.showTooltips()) {
this.setState({ tooltip: <Tooltip rect={rect} options={opts}>{content}</Tooltip> });
}
}
/**
* Show the term tip
* @param {string} term Term
* @param {[type]} orientation Tooltip orientation (n,e,s,w)
* @param {SyntheticEvent} event Event
*/
_termtip(term, orientation, event) {
if (typeof orientation != 'string') {
event = orientation;
orientation = null;
}
this._tooltip(<div className='cap cen'>{this.state.language.translate(term)}</div>, event.currentTarget.getBoundingClientRect(), { orientation });
}
/**
* Add a listener to on window resize
* @param {Function} listener Listener callback
* @return {Object} Subscription token
*/
_onWindowResize(listener) {
return this.emitter.addListener('windowResize', listener);
}
/**
* Creates the context to be passed down to pages / components containing
* language, sizeRatio and route references
@@ -117,7 +199,14 @@ export default class Coriolis extends React.Component {
return {
language: this.state.language,
route: this.state.route,
sizeRatio: this.state.sizeRatio
sizeRatio: this.state.sizeRatio,
openMenu: this._openMenu,
closeMenu: this._closeMenu,
showModal: this._showModal,
hideModal: this._hideModal,
tooltip: this._tooltip,
termtip: this._termtip,
onWindowResize: this._onWindowResize
};
}
@@ -135,14 +224,11 @@ export default class Coriolis extends React.Component {
}
window.onerror = this._onError.bind(this);
window.addEventListener('resize', InterfaceEvents.windowResized);
window.addEventListener('resize', () => this.emitter.emit('windowResize'));
document.body.addEventListener('scroll', () => this._tooltip());
document.addEventListener('keydown', this._keyDown);
Persist.addListener('language', this._onLanguageChange);
Persist.addListener('sizeRatio', this._onSizeRatioChange);
InterfaceEvents.addListener('openMenu', this._openMenu);
InterfaceEvents.addListener('closeMenu', this._closeMenu);
InterfaceEvents.addListener('showModal', this._showModal);
InterfaceEvents.addListener('hideModal', this._hideModal);
Router.start();
}
@@ -157,6 +243,7 @@ export default class Coriolis extends React.Component {
<Header appCacheUpdate={this.state.appCacheUpdate} currentMenu={this.state.currentMenu} />
{ this.state.page ? <this.state.page currentMenu={this.state.currentMenu} /> : <NotFoundPage/> }
{ this.state.modal }
{ this.state.tooltip }
</div>
);
}

View File

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

View File

@@ -2,19 +2,31 @@ import React from 'react';
import Link from './Link';
import cn from 'classnames';
/**
* Returns true if the current window location equals the link
* @return {boolean} If matches
*/
function isActive(href) {
return encodeURI(href) == (window.location.pathname + window.location.search);
}
/**
* Active Link - Highlighted when URL matches window location
*/
export default class ActiveLink extends Link {
isActive = () => {
return encodeURI(this.props.href) == (window.location.pathname + window.location.search);
}
/**
* Renders the component
* @return {React.Component} The active link
*/
render() {
let className = this.props.className;
if (this.isActive()) {
if (isActive(this.props.href)) {
className = cn(className, 'active');
}
return <a {...this.props} className={className} onClick={this.handler}>{this.props.children}</a>
return <a {...this.props} className={className} onClick={this.handler.bind(this)}>{this.props.children}</a>;
}
}

View File

@@ -4,11 +4,15 @@ import TranslatedComponent from './TranslatedComponent';
import cn from 'classnames';
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
/**
* Available modules menu
*/
export default class AvailableModulesMenu extends TranslatedComponent {
static propTypes = {
modules: React.PropTypes.oneOfType([ React.PropTypes.object, React.PropTypes.array ]).isRequired,
modules: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.array]).isRequired,
onSelect: React.PropTypes.func.isRequired,
diffDetails: React.PropTypes.func,
m: React.PropTypes.object,
shipMass: React.PropTypes.number,
warning: React.PropTypes.func
@@ -18,23 +22,83 @@ export default class AvailableModulesMenu extends TranslatedComponent {
shipMass: 0
};
buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules) {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
this._hideDiff = this._hideDiff.bind(this);
this.state = { list: this._initList(props, context) };
}
/**
* Initiate the list of available moduels
* @param {Object} props React Component properties
* @param {Object} context React Component context
* @return {Array} Array of React Components
*/
_initList(props, context) {
let translate = context.language.translate;
let { m, warning, shipMass, onSelect, modules } = props;
let list;
let buildGroup = this._buildGroup.bind(
this,
translate,
m,
warning,
shipMass - (m && m.mass ? m.mass : 0),
(m) => {
this._hideDiff();
onSelect(m);
}
);
if (modules instanceof Array) {
list = buildGroup(modules[0].grp, modules);
} else {
list = [];
// At present time slots with grouped options (Hardpoints and Internal) can be empty
list.push(<div className={'empty-c upp'} key={'empty'} onClick={onSelect.bind(null, null)} >{translate('empty')}</div>);
for (let g in modules) {
list.push(<div ref={g} key={g} className={'select-group cap'}>{translate(g)}</div>);
list.push(buildGroup(g, modules[g]));
}
}
return list;
}
/**
* Generate React Components for Module Group
* @param {Function} translate Translate function
* @param {Objecy} mountedModule Mounted Module
* @param {Funciton} warningFunc Warning function
* @param {number} mass Mass
* @param {function} onSelect Select/Mount callback
* @param {string} grp Group name
* @param {Array} modules Available modules
* @return {React.Component} Available Module Group contents
*/
_buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules) {
let prevClass = null, prevRating = null;
let elems = [];
for (let i = 0; i < modules.length; i++) {
let m = modules[i];
let mount = null;
let disabled = m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass;
let classes = cn(m.name ? 'lc' : 'c', {
active: mountedModule && mountedModule.id === m.id,
warning: warningFunc && warningFunc(m),
disabled: m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass
warning: !disabled && warningFunc && warningFunc(m),
disabled
});
switch(m.mount) {
case 'F': mount = <MountFixed className={'lg'} />; break;
case 'G': mount = <MountGimballed className={'lg'}/>; break;
case 'T': mount = <MountTurret className={'lg'}/>; break;
case 'F': mount = <MountFixed className={'lg'} />; break;
case 'G': mount = <MountGimballed className={'lg'}/>; break;
case 'T': mount = <MountTurret className={'lg'}/>; break;
}
if (i > 0 && modules.length > 3 && m.class != prevClass && (m.rating != prevRating || m.mount) && m.grp != 'pa') {
@@ -42,7 +106,13 @@ export default class AvailableModulesMenu extends TranslatedComponent {
}
elems.push(
<li key={m.id} className={classes} onClick={onSelect.bind(null, m)}>
<li
key={m.id}
className={classes}
onMouseOver={disabled ? null : this._showDiff.bind(this, mountedModule, m)}
onMouseLeave={this._hideDiff}
onClick={disabled ? null : onSelect.bind(null, m)}
>
{mount}
<span>{(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}</span>
</li>
@@ -54,43 +124,54 @@ export default class AvailableModulesMenu extends TranslatedComponent {
return <ul key={'modules' + grp} >{elems}</ul>;
}
/**
* Generate tooltip content for the difference between the
* mounted module and the hovered modules
* @param {Object} mm The module mounet currently
* @param {Object} m The hovered module
* @param {SyntheticEvent} event Event
*/
_showDiff(mm, m, event) {
if (this.props.diffDetails) {
this.context.tooltip(this.props.diffDetails(m, mm), event.currentTarget.getBoundingClientRect());
}
}
/**
* Hide diff tooltip
*/
_hideDiff() {
this.context.tooltip();
}
/**
* Scroll to mounted (if it exists) component on mount
*/
componentDidMount() {
let m = this.props.m
let m = this.props.m;
if (!(this.props.modules instanceof Array) && m && m.grp) {
findDOMNode(this).scrollTop = this.refs[m.grp].offsetTop; // Scroll to currently selected group
}
}
/**
* Update state based on property and context changes
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextContext Incoming/Next conext
*/
componentWillReceiveProps(nextProps, nextContext) {
this.setState({ list: this._initList(nextProps, nextContext) });
}
/**
* Render the list
* @return {React.Component} List
*/
render() {
let translate = this.context.language.translate;
let m = this.props.m;
let modules = this.props.modules;
let list;
let buildGroup = this.buildGroup.bind(
null,
translate,
m,
this.props.warning,
this.props.shipMass - (m && m.mass ? m.mass : 0),
this.props.onSelect
);
if (modules instanceof Array) {
list = buildGroup(modules[0].grp, modules);
} else {
list = [];
// At present time slots with grouped options (Hardpoints and Internal) can be empty
list.push(<div className={'empty-c upp'} key={'empty'} onClick={this.props.onSelect.bind(null, null)} >{translate('empty')}</div>);
for (let g in modules) {
list.push(<div ref={g} key={g} className={'select-group cap'}>{translate(g)}</div>);
list.push(buildGroup(g, modules[g]));
}
}
return (
<div className={cn('select', this.props.className)} onClick={(e) => e.stopPropagation() }>
{list}
<div className={cn('select', this.props.className)} onScroll={this._hideDiff} onClick={(e) => e.stopPropagation() }>
{this.state.list}
</div>
);
}

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';
/**
* Comparison Table
*/
export default class ComparisonTable extends TranslatedComponent {
static propTypes = {
@@ -12,16 +15,27 @@ export default class ComparisonTable extends TranslatedComponent {
builds: React.PropTypes.array.isRequired,
onSort: React.PropTypes.func.isRequired,
predicate: React.PropTypes.string.isRequired, // Used only to test again prop changes for shouldRender
desc: React.PropTypes.bool.isRequired, // Used only to test again prop changes for shouldRender
}
desc: React.PropTypes.oneOfType([React.PropTypes.bool.isRequired, React.PropTypes.number.isRequired]), // Used only to test again prop changes for shouldRender
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props, context);
this._buildHeaders = this._buildHeaders.bind(this);
this.state = this._buildHeaders(props.facets, props.onSort, context.language.translate);
}
/**
* Build table headers
* @param {Array} facets Facets list
* @param {Function} onSort Sort callback
* @param {Function} translate Translate function
* @return {Object} Header Components
*/
_buildHeaders(facets, onSort, translate) {
let header = [
<th key='ship' rowSpan='2' className='sortable' onClick={onSort.bind(null, 'name')}>{translate('ship')}</th>,
@@ -39,7 +53,7 @@ export default class ComparisonTable extends TranslatedComponent {
if (pl > 1) {
for (let i = 0; i < pl; i++) {
subHeader.push(<th key={p[i]} className={cn('sortable', { lft: i === 0 } )} onClick={onSort.bind(null, p[i])} >{translate(f.lbls[i])}</th>);
subHeader.push(<th key={p[i]} className={cn('sortable', { lft: i === 0 })} onClick={onSort.bind(null, p[i])} >{translate(f.lbls[i])}</th>);
}
}
}
@@ -48,6 +62,14 @@ export default class ComparisonTable extends TranslatedComponent {
return { header, subHeader };
}
/**
* Generate a table row for the build
* @param {Object} build Ship build
* @param {Array} facets Facets list
* @param {Object} formats Localized formats map
* @param {Object} units Localized untis map
* @return {React.Component} Table row
*/
_buildRow(build, facets, formats, units) {
let url = `/outfit/${build.id}/${build.toString()}?bn=${build.buildName}`;
let cells = [
@@ -66,6 +88,11 @@ export default class ComparisonTable extends TranslatedComponent {
return <tr key={build.id + build.buildName} className='tr'>{cells}</tr>;
}
/**
* Update state based on property and context changes
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextContext Incoming/Next conext
*/
componentWillReceiveProps(nextProps, nextContext) {
// If facets or language has changed re-render header
if (nextProps.facets != this.props.facets || nextContext.language != this.context.language) {
@@ -73,6 +100,10 @@ export default class ComparisonTable extends TranslatedComponent {
}
}
/**
* Render the table
* @return {React.Component} Comparison table
*/
render() {
let { builds, facets } = this.props;
let { header, subHeader } = this.state;
@@ -97,6 +128,5 @@ export default class ComparisonTable extends TranslatedComponent {
</table>
</div>
);
}
}

View File

@@ -4,9 +4,12 @@ import { Ships } from 'coriolis-data';
import Persist from '../stores/Persist';
import Ship from '../shipyard/Ship';
import { Insurance } from '../shipyard/Constants';
import { slotName, nameComparator } from '../utils/SlotFunctions';
import { slotName, slotComparator } from '../utils/SlotFunctions';
import TranslatedComponent from './TranslatedComponent';
/**
* Cost Section
*/
export default class CostSection extends TranslatedComponent {
static PropTypes = {
@@ -15,6 +18,10 @@ export default class CostSection extends TranslatedComponent {
buildName: React.PropTypes.string
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
this._costsTab = this._costsTab.bind(this);
@@ -52,10 +59,17 @@ export default class CostSection extends TranslatedComponent {
};
}
/**
* Create a ship instance to base/reference retrofit changes from
* @param {string} shipId Ship Id
* @param {string} name Build name
* @param {Ship} retrofitShip Existing retrofit ship
* @return {Ship} Retrofit ship
*/
_buildRetrofitShip(shipId, name, retrofitShip) {
let data = Ships[shipId]; // Retrieve the basic ship properties, slots and defaults
if (!retrofitShip) {
if (!retrofitShip) { // Don't create a new instance unless needed
retrofitShip = new Ship(shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison
}
@@ -67,15 +81,28 @@ export default class CostSection extends TranslatedComponent {
return retrofitShip;
}
/**
* Get the default retrofit build name if it exists
* @param {string} shipId Ship Id
* @param {string} name Build name
* @return {string} Build name or null
*/
_defaultRetrofitName(shipId, name) {
return Persist.hasBuild(shipId, name) ? name : null;
}
/**
* Show selected tab
* @param {string} tab Tab name
*/
_showTab(tab) {
Persist.setCostTab(tab);
this.setState({ tab });
}
/**
* Update prices on discount change
*/
_onDiscountChanged() {
let shipDiscount = Persist.getShipDiscount();
let moduleDiscount = Persist.getModuleDiscount();
@@ -84,13 +111,17 @@ export default class CostSection extends TranslatedComponent {
this.setState({ shipDiscount, moduleDiscount });
}
/**
* Update insurance on change
* @param {string} insuranceName Insurance level name
*/
_onInsuranceChanged(insuranceName) {
this.setState({ insurance: Insurance[insuranceName] });
}
/**
* Repopulate modules on retrofit ship from existing build
* @param {string} retrofitName Build name to base the retrofit ship on
* @param {SyntheticEvent} event Build name to base the retrofit ship on
*/
_onBaseRetrofitChange(event) {
let retrofitName = event.target.value;
@@ -105,6 +136,12 @@ export default class CostSection extends TranslatedComponent {
this.setState({ retrofitName });
}
/**
* On build save
* @param {string} shipId Ship Id
* @param {string} name Build name
* @param {string} code Serialized ship 'code'
*/
_onBuildSaved(shipId, name, code) {
if(this.state.retrofitName == name) {
this.state.retrofitShip.buildFrom(code); // Repopulate modules from saved build
@@ -114,6 +151,12 @@ export default class CostSection extends TranslatedComponent {
}
}
/**
* On build deleted
* @param {string} shipId Ship Id
* @param {string} name Build name
* @param {string} code Serialized ship 'code'
*/
_onBuildDeleted(shipId, name, code) {
if(this.state.retrofitName == name) {
this.state.retrofitShip.buildWith(Ships[shipId].defaults); // Retrofit ship becomes stock build
@@ -122,11 +165,19 @@ export default class CostSection extends TranslatedComponent {
this.setState({ buildOptions: Persist.getBuildsNamesFor(shipId) });
}
/**
* Toggle item cost inclusion in overall total
* @param {Object} item Cost item
*/
_toggleCost(item) {
this.props.ship.setCostIncluded(item, !item.incCost);
this.setState({ total: this.props.ship.totalCost });
}
/**
* Toggle item cost inclusion in retrofit total
* @param {Object} item Cost item
*/
_toggleRetrofitCost(item) {
let retrofitTotal = this.state.retrofitTotal;
item.retroItem.incCost = !item.retroItem.incCost;
@@ -134,6 +185,10 @@ export default class CostSection extends TranslatedComponent {
this.setState({ retrofitTotal });
}
/**
* Set cost list sort predicate
* @param {string} predicate sort predicate
*/
_sortCostBy(predicate) {
let { costPredicate, costDesc } = this.state;
@@ -144,20 +199,27 @@ export default class CostSection extends TranslatedComponent {
this.setState({ costPredicate: predicate, costDesc });
}
/**
* Sort cost list
* @param {Ship} ship Ship instance
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort descending
*/
_sortCost(ship, predicate, desc) {
let costList = ship.costList;
let translate = this.context.language.translate;
if (predicate == 'm') {
costList.sort(nameComparator(this.context.language.translate));
costList.sort(slotComparator(translate, null, desc));
} else {
costList.sort((a, b) => (a.m && a.m.cost ? a.m.cost : 0) - (b.m && b.m.cost ? b.m.cost : 0));
}
if (!desc) {
costList.reverse();
costList.sort(slotComparator(translate, (a, b) => (a.m.cost || 0) - (b.m.cost || 0), desc));
}
}
/**
* Set ammo list sort predicate
* @param {string} predicate sort predicate
*/
_sortAmmoBy(predicate) {
let { ammoPredicate, ammoDesc } = this.state;
@@ -168,19 +230,26 @@ export default class CostSection extends TranslatedComponent {
this.setState({ ammoPredicate: predicate, ammoDesc });
}
/**
* Sort ammo cost list
* @param {Array} ammoCosts Ammo cost list
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort descending
*/
_sortAmmo(ammoCosts, predicate, desc) {
let translate = this.context.language.translate;
if (predicate == 'm') {
ammoCosts.sort(nameComparator(this.context.language.translate));
ammoCosts.sort(slotComparator(translate, null, desc));
} else {
ammoCosts.sort((a, b) => a[predicate] - b[predicate]);
}
if (!desc) {
ammoCosts.reverse();
ammoCosts.sort(slotComparator(translate, (a, b) => a[predicate] - b[predicate], desc));
}
}
/**
* Set retrofit list sort predicate
* @param {string} predicate sort predicate
*/
_sortRetrofitBy(predicate) {
let { retroPredicate, retroDesc } = this.state;
@@ -191,6 +260,12 @@ export default class CostSection extends TranslatedComponent {
this.setState({ retroPredicate: predicate, retroDesc });
}
/**
* Sort retrofit cost list
* @param {Array} retrofitCosts Retrofit cost list
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort descending
*/
_sortRetrofit(retrofitCosts, predicate, desc) {
let translate = this.context.language.translate;
@@ -205,6 +280,10 @@ export default class CostSection extends TranslatedComponent {
}
}
/**
* Render the cost tab
* @return {React.Component} Tab contents
*/
_costsTab() {
let { ship } = this.props;
let { total, shipDiscount, moduleDiscount, insurance } = this.state;
@@ -250,6 +329,10 @@ export default class CostSection extends TranslatedComponent {
</div>;
}
/**
* Render the retofit tab
* @return {React.Component} Tab contents
*/
_retrofitTab() {
let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state;
let { translate, formats, units } = this.context.language;
@@ -268,11 +351,11 @@ export default class CostSection extends TranslatedComponent {
<td className='le shorten cap'>{translate(item.sellName)}</td>
<td style={{ width: '1em' }}>{item.buyClassRating}</td>
<td className='le shorten cap'>{translate(item.buyName)}</td>
<td colSpan='2' className={cn('ri', item.retroItem.incCost ? item.netCost > 0 ? 'warning' : 'secondary-disabled' : 'disabled' )}>{int(item.netCost)}{units.CR}</td>
<td colSpan='2' className={cn('ri', item.retroItem.incCost ? item.netCost > 0 ? 'warning' : 'secondary-disabled' : 'disabled')}>{int(item.netCost)}{units.CR}</td>
</tr>);
}
} else {
rows = <tr><td colSpan='7' style={{ padding: '3em 0' }}>{translate('PHRASE_NO_RETROCH')}</td></tr>
rows = <tr><td colSpan='7' style={{ padding: '3em 0' }}>{translate('PHRASE_NO_RETROCH')}</td></tr>;
}
return <div>
@@ -284,7 +367,7 @@ export default class CostSection extends TranslatedComponent {
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'buyName')}>{translate('buy')}</th>
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'cr')}>
{translate('net cost')}
{moduleDiscount < 1 && <u className='optional-hide'>{`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}</u>}
{moduleDiscount < 1 && <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}</u>}
</th>
</tr>
</thead>
@@ -301,7 +384,7 @@ export default class CostSection extends TranslatedComponent {
<td className='val cen' style={{ borderRight: 'none', width: '1em' }}><u className='primary-disabled'>&#9662;</u></td>
<td className='val' style={{ borderLeft:'none', padding: 0 }}>
<select style={{ width: '100%', padding: 0 }} value={retrofitName} onChange={this._onBaseRetrofitChange}>
{options}
{options}
</select>
</td>
</tr>
@@ -311,9 +394,15 @@ export default class CostSection extends TranslatedComponent {
</div>;
}
/**
* Update retrofit costs
* @param {Ship} ship Ship instance
* @param {Ship} retrofitShip Retrofit Ship instance
*/
_updateRetrofit(ship, retrofitShip) {
let retrofitCosts = [];
var retrofitTotal = 0, i, l, item;
let retrofitTotal = 0, i, l, item;
if (ship.bulkheads.index != retrofitShip.bulkheads.index) {
item = {
@@ -330,9 +419,9 @@ export default class CostSection extends TranslatedComponent {
}
}
for (var g in { standard: 1, internal: 1, hardpoints: 1 }) {
var retroSlotGroup = retrofitShip[g];
var slotGroup = ship[g];
for (let g in { standard: 1, internal: 1, hardpoints: 1 }) {
let retroSlotGroup = retrofitShip[g];
let slotGroup = ship[g];
for (i = 0, l = slotGroup.length; i < l; i++) {
if (slotGroup[i].m != retroSlotGroup[i].m) {
item = { netCost: 0, retroItem: retroSlotGroup[i] };
@@ -358,6 +447,10 @@ export default class CostSection extends TranslatedComponent {
this._sortRetrofit(retrofitCosts, this.state.retroPredicate, this.state.retroDesc);
}
/**
* Render the ammo tab
* @return {React.Component} Tab contents
*/
_ammoTab() {
let { ammoTotal, ammoCosts } = this.state;
let { translate, formats, units } = this.context.language;
@@ -400,6 +493,7 @@ export default class CostSection extends TranslatedComponent {
/**
* Recalculate all ammo costs
* @param {Ship} ship Ship instance
*/
_updateAmmoCosts(ship) {
let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, srvs = 0, scoop = false;
@@ -408,10 +502,10 @@ export default class CostSection extends TranslatedComponent {
let slotGroup = ship[g];
for (let i = 0, l = slotGroup.length; i < l; i++) {
if (slotGroup[i].m) {
//special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip
// Special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip
q = 0;
switch (slotGroup[i].m.grp) {
case 'fs': //skip fuel calculation if scoop present
case 'fs': // Skip fuel calculation if scoop present
scoop = true;
break;
case 'scb':
@@ -429,7 +523,7 @@ export default class CostSection extends TranslatedComponent {
default:
q = slotGroup[i].m.clip + slotGroup[i].m.ammo;
}
//calculate ammo costs only if a cost is specified
// Calculate ammo costs only if a cost is specified
if (slotGroup[i].m.ammocost > 0) {
item = {
m: slotGroup[i].m,
@@ -444,7 +538,7 @@ export default class CostSection extends TranslatedComponent {
}
}
//limpets if controllers exist and cargo space available
// Limpets if controllers exist and cargo space available
if (limpets > 0) {
item = {
m: { name: 'limpets', class: '', rating: '' },
@@ -466,7 +560,7 @@ export default class CostSection extends TranslatedComponent {
ammoCosts.push(item);
ammoTotal += item.total;
}
//calculate refuel costs if no scoop present
// Calculate refuel costs if no scoop present
if (!scoop) {
item = {
m: { name: 'fuel', class: '', rating: '' },
@@ -482,7 +576,10 @@ export default class CostSection extends TranslatedComponent {
this._sortAmmo(ammoCosts, this.state.ammoPredicate, this.state.ammoDesc);
}
componentWillMount(){
/**
* Add listeners on mount and update costs
*/
componentWillMount() {
this.listeners = [
Persist.addListener('discounts', this._onDiscountChanged.bind(this)),
Persist.addListener('insurance', this._onInsuranceChanged.bind(this)),
@@ -494,13 +591,18 @@ export default class CostSection extends TranslatedComponent {
this._sortCost(this.props.ship);
}
/**
* Update state based on property and context changes
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextContext Incoming/Next context
*/
componentWillReceiveProps(nextProps, nextContext) {
let retrofitShip = this.state.retrofitShip;
if (nextProps.ship != this.props.ship) { // Ship has changed
let nextId = nextProps.ship.id;
let retrofitName = this._defaultRetrofitName(nextId, nextProps.buildName);
retrofitShip = this._buildRetrofitShip(nextId, retrofitName, nextId == this.props.ship.id ? retrofitShip : null );
retrofitShip = this._buildRetrofitShip(nextId, retrofitName, nextId == this.props.ship.id ? retrofitShip : null);
this.setState({
retrofitShip,
retrofitName,
@@ -515,6 +617,11 @@ export default class CostSection extends TranslatedComponent {
}
}
/**
* Sort lists before render
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextState Incoming/Next state
*/
componentWillUpdate(nextProps, nextState) {
let state = this.state;
@@ -536,10 +643,17 @@ export default class CostSection extends TranslatedComponent {
}
}
componentWillUnmount(){
/**
* Remove listeners
*/
componentWillUnmount() {
this.listeners.forEach(l => l.remove());
}
/**
* Render the Cost section
* @return {React.Component} Contents
*/
render() {
let tab = this.state.tab;
let translate = this.context.language.translate;
@@ -558,9 +672,9 @@ export default class CostSection extends TranslatedComponent {
<table className='tabs'>
<thead>
<tr>
<th style={{ width:'33%' }} className={cn({active: tab == 'costs'})} onClick={this._showTab.bind(this, 'costs')} >{translate('costs')}</th>
<th style={{ width:'33%' }} className={cn({active: tab == 'retrofit'})} onClick={this._showTab.bind(this, 'retrofit')} >{translate('retrofit costs')}</th>
<th style={{ width:'34%' }} className={cn({active: tab == 'ammo'})} onClick={this._showTab.bind(this, 'ammo')} >{translate('reload costs')}</th>
<th style={{ width:'33%' }} className={cn({ active: tab == 'costs' })} onClick={this._showTab.bind(this, 'costs')} >{translate('costs')}</th>
<th style={{ width:'33%' }} className={cn({ active: tab == 'retrofit' })} onClick={this._showTab.bind(this, 'retrofit')} >{translate('retrofit costs')}</th>
<th style={{ width:'34%' }} className={cn({ active: tab == 'ammo' })} onClick={this._showTab.bind(this, 'ammo')} >{translate('reload costs')}</th>
</tr>
</thead>
</table>

View File

@@ -1,36 +1,58 @@
import React from 'react';
import Slot from './Slot';
/**
* Hardpoint / Utility Slot
*/
export default class HardpointSlot extends Slot {
/**
* Get the CSS class name for the slot.
* @return {string} CSS Class name
*/
_getClassNames() {
return this.props.maxClass > 0 ? 'hardpoint' : null;
}
_getMaxClassLabel(translate){
/**
* Get the label for the slot
* @param {Function} translate Translate function
* @return {string} Label
*/
_getMaxClassLabel(translate) {
return translate(['U','S','M','L','H'][this.props.maxClass]);
}
/**
* Generate the slot contents
* @param {Object} m Mounted Module
* @param {Function} translate Translate function
* @param {Object} formats Localized Formats map
* @param {Object} u Localized Units Map
* @return {React.Component} Slot contents
*/
_getSlotDetails(m, translate, formats, u) {
if (m) {
let classRating = `${m.class}${m.rating}${m.mount ? '/' + m.mount : ''}${m.missile ? m.missile : ''}`;
return (
<div>
let { drag, drop } = this.props;
return <div className='details' draggable='true' onDragStart={drag} onDragEnd={drop}>
<div className={'cb'}>
<div className={'l'}>{classRating + ' ' + translate(m.name || m.grp)}</div>
<div className={'r'}>{m.mass}{u.T}</div>
<div className={'cb'}>
{ m.damage ? <div className={'l'}>{translate('damage')}: {m.damage} { m.ssdam ? <span>({formats.int(m.ssdam)} {u.MJ})</span> : null }</div> : null }
{ m.dps ? <div className={'l'}>{translate('DPS')}: {m.dps} { m.mjdps ? <span>({formats.int(m.mjdps)} {u.MJ})</span> : null }</div> : null }
{ m.thermload ? <div className={'l'}>{translate('T_LOAD')}: {m.thermload}</div> : null }
{ m.type ? <div className={'l'}>{translate('type')}: {m.type}</div> : null }
{ m.rof ? <div className={'l'}>{translate('ROF')}: {m.rof}{u.ps}</div> : null }
{ m.armourpen ? <div className={'l'}>{translate('pen')}: {m.armourpen}</div> : null }
{ m.shieldmul ? <div className={'l'}>+{formats.rPct(m.shieldmul)}</div> : null }
{ m.range ? <div className={'l'}>{m.range} <u>km</u></div> : null }
{ m.ammo >= 0 ? <div className={'l'}>{translate('ammo')}: {formats.int(m.clip)}+{formats.int(m.ammo)}</div> : null }
</div>
</div>
);
<div className={'cb'}>
{ m.damage ? <div className={'l'}>{translate('damage')}: {m.damage} { m.ssdam ? <span>({formats.int(m.ssdam)} {u.MJ})</span> : null }</div> : null }
{ m.dps ? <div className={'l'}>{translate('DPS')}: {m.dps} { m.mjdps ? <span>({formats.int(m.mjdps)} {u.MJ})</span> : null }</div> : null }
{ m.thermload ? <div className={'l'}>{translate('T_LOAD')}: {m.thermload}</div> : null }
{ m.type ? <div className={'l'}>{translate('type')}: {m.type}</div> : null }
{ m.rof ? <div className={'l'}>{translate('ROF')}: {m.rof}{u.ps}</div> : null }
{ m.armourpen ? <div className={'l'}>{translate('pen')}: {m.armourpen}</div> : null }
{ m.shieldmul ? <div className={'l'}>+{formats.rPct(m.shieldmul)}</div> : null }
{ m.range ? <div className={'l'}>{m.range} <u>km</u></div> : null }
{ m.ammo >= 0 ? <div className={'l'}>{translate('ammo')}: {formats.int(m.clip)}+{formats.int(m.ammo)}</div> : null }
</div>
</div>;
} else {
return <div className={'empty'}>{translate('empty')}</div>;
}

View File

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

View File

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

View File

@@ -2,36 +2,48 @@ import React from 'react';
import Slot from './Slot';
import { Infinite } from './SvgIcons';
/**
* Internal Slot
*/
export default class InternalSlot extends Slot {
/**
* Generate the slot contents
* @param {Object} m Mounted Module
* @param {Function} translate Translate function
* @param {Object} formats Localized Formats map
* @param {Object} u Localized Units Map
* @return {React.Component} Slot contents
*/
_getSlotDetails(m, translate, formats, u) {
if (m) {
let classRating = m.class + m.rating;
let { drag, drop } = this.props;
return (
<div>
return <div className='details' draggable='true' onDragStart={drag} onDragEnd={drop}>
<div className={'cb'}>
<div className={'l'}>{classRating + ' ' + translate(m.name || m.grp)}</div>
<div className={'r'}>{m.mass || m.capacity || 0}{u.T}</div>
<div className={'cb'}>
{ m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{u.T}</div> : null }
{ m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{u.T}</div> : null }
{ m.bins ? <div className={'l'}>{m.bins + ' '}<u>{translate('bins')}</u></div> : null }
{ m.bays ? <div className={'l'}>{translate('bays') + ': ' + m.bays}</div> : null }
{ m.rate ? <div className={'l'}>{translate('rate')}: {m.rate}{u.kgs}&nbsp;&nbsp;&nbsp;{translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}</div> : null }
{ m.ammo ? <div className={'l'}>{translate('ammo')}: {formats.gen(m.ammo)}</div> : null }
{ m.cells ? <div className={'l'}>{translate('cells')}: {m.cells}</div> : null }
{ m.recharge ? <div className={'l'}>{translate('recharge')}: {m.recharge} <u>MJ</u>&nbsp;&nbsp;&nbsp;{translate('total')}: {m.cells * m.recharge}{u.MJ}</div> : null }
{ m.repair ? <div className={'l'}>{translate('repair')}: {m.repair}</div> : null }
{ m.range ? <div className={'l'}>{translate('range')} {m.range}{u.km}</div> : null }
{ m.time ? <div className={'l'}>{translate('time')}: {formats.time(m.time)}</div> : null }
{ m.maximum ? <div className={'l'}>{translate('max')}: {(m.maximum)}</div> : null }
{ m.rangeLS ? <div className={'l'}>{m.rangeLS}{u.Ls}</div> : null }
{ m.rangeLS === null ? <div className={'l'}><Infinite/>{u.Ls}</div> : null }
{ m.rangeRating ? <div className={'l'}>{translate('range')}: {m.rangeRating}</div> : null }
{ m.armouradd ? <div className={'l'}>+{m.armouradd} <u>{translate('armour')}</u></div> : null }
</div>
<div className={'r'}>{m.mass || m.cargo || m.fuel || 0}{u.T}</div>
</div>
);
<div className={'cb'}>
{ m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{u.T}</div> : null }
{ m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{u.T}</div> : null }
{ m.bins ? <div className={'l'}>{m.bins + ' '}<u>{translate('bins')}</u></div> : null }
{ m.bays ? <div className={'l'}>{translate('bays') + ': ' + m.bays}</div> : null }
{ m.rate ? <div className={'l'}>{translate('rate')}: {m.rate}{u.kgs}&nbsp;&nbsp;&nbsp;{translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}</div> : null }
{ m.ammo ? <div className={'l'}>{translate('ammo')}: {formats.gen(m.ammo)}</div> : null }
{ m.cells ? <div className={'l'}>{translate('cells')}: {m.cells}</div> : null }
{ m.recharge ? <div className={'l'}>{translate('recharge')}: {m.recharge} <u>MJ</u>&nbsp;&nbsp;&nbsp;{translate('total')}: {m.cells * m.recharge}{u.MJ}</div> : null }
{ m.repair ? <div className={'l'}>{translate('repair')}: {m.repair}</div> : null }
{ m.range ? <div className={'l'}>{translate('range')} {m.range}{u.km}</div> : null }
{ m.time ? <div className={'l'}>{translate('time')}: {formats.time(m.time)}</div> : null }
{ m.maximum ? <div className={'l'}>{translate('max')}: {(m.maximum)}</div> : null }
{ m.rangeLS ? <div className={'l'}>{m.rangeLS}{u.Ls}</div> : null }
{ m.rangeLS === null ? <div className={'l'}><Infinite/>{u.Ls}</div> : null }
{ m.rangeRating ? <div className={'l'}>{translate('range')}: {m.rangeRating}</div> : null }
{ m.armouradd ? <div className={'l'}>+{m.armouradd} <u>{translate('armour')}</u></div> : null }
</div>
</div>;
} else {
return <div className={'empty'}>{translate('empty')}</div>;
}

View File

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

View File

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

View File

@@ -1,20 +1,32 @@
import React from 'react';
import Router from '../Router';
import shallowEqual from '../utils/shallowEqual';
import { shallowEqual } from '../utils/UtilityFunctions';
/**
* Link wrapper component
*/
export default class Link extends React.Component {
/**
* Determine if a component should be rerendered
* @param {object} nextProps Next properties
* @return {boolean} true if update is needed
*/
shouldComponentUpdate(nextProps) {
return !shallowEqual(this.props, nextProps);
}
handler = (event) => {
if (event.getModifierState
&& ( event.getModifierState('Shift')
|| event.getModifierState('Alt')
|| event.getModifierState('Control')
|| event.getModifierState('Meta')
|| event.button > 1)) {
/**
* Link click handler
* @param {SyntheticEvent} event Event
*/
handler(event) {
if (event.getModifierState &&
(event.getModifierState('Shift') ||
event.getModifierState('Alt') ||
event.getModifierState('Control') ||
event.getModifierState('Meta') ||
event.button > 1)) {
return;
}
event.nativeEvent && event.preventDefault && event.preventDefault();
@@ -24,8 +36,12 @@ export default class Link extends React.Component {
}
}
/**
* Renders the link
* @return {React.Component} A href element
*/
render() {
return <a {...this.props} onClick={this.handler}>{this.props.children}</a>
return <a {...this.props} onClick={this.handler.bind(this)}>{this.props.children}</a>;
}
}

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,45 @@
import React from 'react';
import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents';
import Persist from '../stores/Persist';
import { Ships } from 'coriolis-data';
import Ship from '../shipyard/Ship';
import { ModuleNameToGroup } from '../shipyard/Constants';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import { fromDetailedBuild } from '../shipyard/Serializer';
import { Download } from './SvgIcons';
const textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n');
const lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)');
const mountMap = { 'H': 4, 'L': 3, 'M': 2, 'S': 1, 'U': 0 };
const standardMap = { 'RB': 0, 'TM': 1, 'FH': 2, 'EC': 3, 'PC': 4, 'SS': 5, 'FS': 6 };
const bhMap = { 'lightweight alloy': 0, 'reinforced alloy': 1, 'military grade composite': 2, 'mirrored surface composite': 3, 'reactive surface composite': 4 };
/**
* Check is slot is empty
* @param {Object} slot Slot model
* @return {Boolean} True if empty
*/
function isEmptySlot(slot) {
return slot.maxClass == this && slot.m === null;
}
/**
* Equal ignore case utility function. Must be bound to a string
* @param {string} str String
* @return {Boolean} True if equal
*/
function equalsIgnoreCase(str) {
return str.toLowerCase() == this.toLowerCase();
}
/**
* Determine if a build is valid
* @param {string} shipId Ship ID
* @param {string} code Serialzied ship build 'code'
* @param {string} name Build name
* @throws {string} If build is not valid
*/
function validateBuild(shipId, code, name) {
let shipData = Ships[shipId];
@@ -43,6 +60,11 @@ function validateBuild(shipId, code, name) {
}
}
/**
* Convert a ship-loadout JSON object to a Coriolis build
* @param {Object} detailedBuild ship-loadout
* @return {Object} Coriolis build
*/
function detailedJsonToBuild(detailedBuild) {
let ship;
if (!detailedBuild.name) {
@@ -54,7 +76,7 @@ function detailedJsonToBuild(detailedBuild) {
}
try {
ship = Serializer.fromDetailedBuild(detailedBuild);
ship = fromDetailedBuild(detailedBuild);
} catch (e) {
throw detailedBuild.ship + ' Build "' + detailedBuild.name + '": Invalid data';
}
@@ -62,8 +84,15 @@ function detailedJsonToBuild(detailedBuild) {
return { shipId: ship.id, name: detailedBuild.name, code: ship.toString() };
}
/**
* Import Modal
*/
export default class ModalImport extends TranslatedComponent {
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
@@ -86,6 +115,11 @@ export default class ModalImport extends TranslatedComponent {
this._validateImport = this._validateImport.bind(this);
}
/**
* Import a Coriolis backup
* @param {Object} importData Backup Data
* @throws {string} If import fails
*/
_importBackup(importData) {
if (importData.builds && typeof importData.builds == 'object') {
for (let shipId in importData.builds) {
@@ -117,6 +151,10 @@ export default class ModalImport extends TranslatedComponent {
}
}
/**
* Import an array of ship-loadout objects / builds
* @param {Array} importArr Array of ship-loadout JSON Schema builds
*/
_importDetailedArray(importArr) {
let builds = {};
for (let i = 0, l = importArr.length; i < l; i++) {
@@ -129,6 +167,11 @@ export default class ModalImport extends TranslatedComponent {
this.setState({ builds });
}
/**
* Import a text build from ED Shipyard
* @param {string} buildStr Build string
* @throws {string} If parse / import fails
*/
_importTextBuild(buildStr) {
let buildName = textBuildRegex.exec(buildStr)[1].trim();
let shipName = buildName.toLowerCase();
@@ -173,43 +216,40 @@ export default class ModalImport extends TranslatedComponent {
if (cl > slotClass) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; }
slot = _.find(ship.hardpoints, isEmptySlot, slotClass);
slot = ship.hardpoints.find(isEmptySlot, slotClass);
if (!slot) { throw 'No hardpoint slot available for: "' + line + '"'; }
group = _.find(GroupMap, equalsIgnoreCase, name);
group = ModuleNameToGroup[name.trim()];
let hp = ModuleUtils.findHardpoint(group, cl, rating, group ? null : name, mount, missile);
if (!hp) { throw 'Unknown component: "' + line + '"'; }
ship.use(slot, hp, true);
} else if (typeSize == 'BH') {
let bhId = bhMap[name.toLowerCase()];
if (bhId === undefined) { throw 'Unknown bulkhead: "' + line + '"'; }
ship.useBulkhead(bhId, true);
} else if (standardMap[typeSize] != undefined) {
let standardIndex = standardMap[typeSize];
if (ship.standard[standardIndex].maxClass < cl) { throw name + ' exceeds max class for the ' + ship.name; }
ship.use(ship.standard[standardIndex], cl + rating, ModuleUtils.standard(standardIndex, cl + rating), true);
} else {
throw 'Unknown component: "' + line + '"';
}
} else {
if (cl > typeSize) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; }
slot = _.find(ship.internal, isEmptySlot, typeSize);
slot = ship.internal.find(isEmptySlot, typeSize);
if (!slot) { throw 'No internal slot available for: "' + line + '"'; }
group = _.find(GroupMap, equalsIgnoreCase, name);
group = ModuleNameToGroup[name.trim()];
let intComp = ModuleUtils.findInternal(group, cl, rating, group ? null : name);
@@ -221,13 +261,18 @@ export default class ModalImport extends TranslatedComponent {
let builds = {};
builds[shipId] = {};
builds[shipId]['Imported ' + buildName] = Serializer.fromShip(ship);
builds[shipId]['Imported ' + buildName] = ship.toString();
this.setState({ builds });
}
_validateImport(e) {
/**
* Validate the import string / text box contents
* @param {SyntheticEvent} event Event
* @throws {string} If validation fails
*/
_validateImport(event) {
let importData = null;
let importString = e.target.value;
let importString = event.target.value;
this.setState({
builds: null,
comparisons: null,
@@ -244,7 +289,7 @@ export default class ModalImport extends TranslatedComponent {
try {
if (textBuildRegex.test(importString)) { // E:D Shipyard build text
importTextBuild(importString);
this._importTextBuild(importString);
} else { // JSON Build data
importData = JSON.parse(importString);
@@ -268,6 +313,9 @@ export default class ModalImport extends TranslatedComponent {
this.setState({ importValid: true });
};
/**
* Process imported data
*/
_process() {
let builds = null, comparisons = null;
@@ -278,7 +326,7 @@ export default class ModalImport extends TranslatedComponent {
let code = builds[shipId][buildName];
// Update builds object such that orginal name retained, but can be renamed
builds[shipId][buildName] = {
code: code,
code,
useName: buildName
};
}
@@ -295,8 +343,10 @@ export default class ModalImport extends TranslatedComponent {
this.setState({ processed: true, builds, comparisons });
};
/**
* Import parsed, processed data and save
*/
_import() {
if (this.state.builds) {
let builds = this.state.builds;
for (let shipId in builds) {
@@ -329,16 +379,33 @@ export default class ModalImport extends TranslatedComponent {
Persist.setInsurance(this.state.insurance);
}
InterfaceEvents.hideModal();
this.context.hideModal();
};
/**
* Capture build name changes
* @param {Object} build Build import object
* @param {SyntheticEvent} e Event
*/
_changeBuildName(build, e) {
build.useName = e.target.value;
this.forceUpdate();
}
/**
* If imported data is already provided process immediately on mount
*/
componentWillMount() {
if (this.props.importingBuilds) {
this.setState({ builds: this.props.importingBuilds, canEdit : false});
this.setState({ builds: this.props.importingBuilds, canEdit : false });
this._process();
}
}
/**
* Render the import modal
* @return {React.Component} Modal contents
*/
render() {
let translate = this.context.language.translate;
let state = this.state;
@@ -357,27 +424,27 @@ export default class ModalImport extends TranslatedComponent {
if (state.comparisons) {
let comparisonRows = [];
for (let name in comparisons) {
let comparison = comparisons[name];
for (let name in state.comparisons) {
let comparison = state.comparisons[name];
let hasComparison = Persist.hasComparison(name);
comparisonRows.push(
<tr key={name} className='cb'>
<td>
<input type='text' value={comparison.useName}/>
</td>
<td style={{ textAlign:'center' }} className={ cn({ warning: hasComparison, disabled: comparison.useName == '' }) }>
<span>{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}></span>
<td style={{ textAlign:'center' }} className={ cn('cap', { warning: hasComparison, disabled: comparison.useName == '' }) }>
{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}>
</td>
</tr>
);
}
comparisonTable = (
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%'}} >
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%' }} >
<thead>
<tr>
<th style={{ textAlign:'left' }}>{translate('comparison')}</th>
<th >{translate('action')}</th>
<th>{translate('action')}</th>
</tr>
</thead>
<tbody>
@@ -388,7 +455,7 @@ export default class ModalImport extends TranslatedComponent {
}
if(this.state.canEdit) {
edit = <button className='l cap' style={{ marginLeft: '2em' }} onClick={() => this.setState({processed: false})}>{translate('edit data')}</button>
edit = <button className='l cap' style={{ marginLeft: '2em' }} onClick={() => this.setState({ processed: false })}>{translate('edit data')}</button>;
}
let builds = this.state.builds;
@@ -398,11 +465,11 @@ export default class ModalImport extends TranslatedComponent {
let b = shipBuilds[buildName];
let hasBuild = Persist.hasBuild(shipId, b.useName);
buildRows.push(
<tr className='cb'>
<tr key={shipId + buildName} className='cb'>
<td>{Ships[shipId].properties.name}</td>
<td><input type='text' value={b.useName}/></td>
<td style={{ textAlign: 'center' }} className={cn({ warning: hasBuild, disabled: b.useName == ''})}>
<span>{translate(b.useName == '' ? 'skip' : (hasBuild ? 'overwrite' : 'create'))}></span>
<td><input type='text' onChange={this._changeBuildName.bind(this, b)} value={b.useName}/></td>
<td style={{ textAlign: 'center' }} className={cn('cap', { warning: hasBuild, disabled: b.useName == '' })}>
{translate(b.useName == '' ? 'skip' : (hasBuild ? 'overwrite' : 'create'))}
</td>
</tr>
);
@@ -411,12 +478,12 @@ export default class ModalImport extends TranslatedComponent {
importStage = (
<div>
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%'}}>
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%' }}>
<thead>
<tr>
<th style={{ textAlign: 'left' }} >{translate('ship')}</th>
<th style={{ textAlign: 'left' }} >{translate('build name')}</th>
<th >{translate('action')}</th>
<th>{translate('action')}</th>
</tr>
</thead>
<tbody>
@@ -433,7 +500,7 @@ export default class ModalImport extends TranslatedComponent {
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
<h2 >{translate('import')}</h2>
{importStage}
<button className={'r dismiss cap'} onClick={InterfaceEvents.hideModal}>{translate('close')}</button>
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
</div>;
}
}

View File

@@ -1,13 +1,20 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents';
import ShortenUrl from '../utils/ShortenUrl';
/**
* Permalink modal
*/
export default class ModalPermalink extends TranslatedComponent {
static propTypes = {
url: React.propTypes.string.isRequired
url: React.PropTypes.string.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
@@ -16,13 +23,20 @@ export default class ModalPermalink extends TranslatedComponent {
};
}
componentWillMount(){
/**
* Shorten URL on mount
*/
componentWillMount() {
ShortenUrl(this.props.url,
(shortenedUrl) => this.setState({ shortenedUrl }),
(error) => this.setState({ shortenedUrl: 'Error - ' + e.statusText })
(error) => this.setState({ shortenedUrl: 'Error - ' + error })
);
}
/**
* Render the modal
* @return {React.Component} Modal Content
*/
render() {
let translate = this.context.language.translate;
@@ -30,12 +44,12 @@ export default class ModalPermalink extends TranslatedComponent {
<h2>{translate('permalink')}</h2>
<br/>
<h3>{translate('URL')}</h3>
<input value={this.props.url} size={40} onFocus={ (e) => e.target.select() }/>
<input value={this.props.url} size={40} readOnly onFocus={ (e) => e.target.select() }/>
<br/><br/>
<h3 >{translate('shortened')}</h3>
<input value={this.state.shortenedUrl} size={25} onFocus={ (e) => e.target.select() }/>
<input value={this.state.shortenedUrl} readOnly size={25} onFocus={ (e) => e.target.select() }/>
<br/><br/>
<button className={'r dismiss cap'} onClick={InterfaceEvents.hideModal}>{translate('close')}</button>
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
</div>;
}
}

View File

@@ -2,23 +2,34 @@ import React from 'react';
import d3 from 'd3';
import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent';
import { wrapCtxMenu } from '../utils/InterfaceEvents';
import { wrapCtxMenu } from '../utils/UtilityFunctions';
/**
* Round to avoid floating point precision errors
* @param {[type]} selected [description]
* @param {[type]} sum [description]
* @param {[type]} avail [description]
* @return {[type]} [description]
* @param {Boolean} selected Band selected
* @param {number} sum Band power sum
* @param {number} avail Total available power
* @return {string} CSS Class name
*/
function getClass(selected, sum, avail) {
return selected ? 'secondary' : ((Math.round(sum * 100) / 100) >= avail) ? 'warning' : 'primary';
}
/**
* Get the # label for a Priority band
* @param {number} val Priority Band Watt value
* @param {number} index Priority Band index
* @param {Function} wattScale Watt Scale function
* @return {number} label / text
*/
function bandText(val, index, wattScale) {
return (val > 0 && wattScale(val) > 13) ? index + 1 : null;
}
/**
* Power Bands Component
* Renders the SVG to simulate in-game power bands
*/
export default class PowerBands extends TranslatedComponent {
static propTypes = {
@@ -28,6 +39,11 @@ export default class PowerBands extends TranslatedComponent {
code: React.PropTypes.string,
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
this.wattScale = d3.scale.linear();
@@ -38,6 +54,7 @@ export default class PowerBands extends TranslatedComponent {
this._updateDimensions = this._updateDimensions.bind(this);
this._updateScales = this._updateScales.bind(this);
this._selectNone = this._selectNone.bind(this);
this._hidetip = () => this.context.tooltip();
let maxBand = props.bands[props.bands.length - 1];
@@ -52,13 +69,18 @@ export default class PowerBands extends TranslatedComponent {
}
}
_updateDimensions(props, size) {
let barHeight = Math.round(20 * size);
/**
* Update dimensions based on properties and scale
* @param {Object} props React Component properties
* @param {number} scale size ratio / scale
*/
_updateDimensions(props, scale) {
let barHeight = Math.round(20 * scale);
let innerHeight = (barHeight * 2) + 2;
let mTop = Math.round(25 * size);
let mBottom = Math.round(25 * size);
let mLeft = Math.round(45 * size);
let mRight = Math.round(140 * size);
let mTop = Math.round(25 * scale);
let mBottom = Math.round(25 * scale);
let mLeft = Math.round(45 * scale);
let mRight = Math.round(140 * scale);
let innerWidth = props.width - mLeft - mRight;
this._updateScales(innerWidth, this.state.maxPwr, props.available);
@@ -77,6 +99,9 @@ export default class PowerBands extends TranslatedComponent {
});
}
/**
* Select no bands
*/
_selectNone() {
this.setState({
ret : {},
@@ -84,6 +109,10 @@ export default class PowerBands extends TranslatedComponent {
});
}
/**
* Select a retracted band
* @param {number} index Band index
*/
_selectRet(index) {
let ret = this.state.ret;
if(ret[index]) {
@@ -95,6 +124,10 @@ export default class PowerBands extends TranslatedComponent {
this.setState({ ret: Object.assign({}, ret) });
}
/**
* Select a deployed band
* @param {number} index Band index
*/
_selectDep(index) {
let dep = this.state.dep;
@@ -107,31 +140,45 @@ export default class PowerBands extends TranslatedComponent {
this.setState({ dep: Object.assign({}, dep) });
}
/**
* Update scale
* @param {number} innerWidth SVG innerwidth
* @param {number} maxPwr Maximum power level MJ (deployed or available)
* @param {number} available Available power MJ
*/
_updateScales(innerWidth, maxPwr, available) {
this.wattScale.range([0, innerWidth]).domain([0,maxPwr]).clamp(true);
this.wattScale.range([0, innerWidth]).domain([0, maxPwr]).clamp(true);
this.pctScale.range([0, innerWidth]).domain([0, maxPwr / available]).clamp(true);
}
/**
* Update state based on property and context changes
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextContext Incoming/Next context
*/
componentWillReceiveProps(nextProps, nextContext) {
let { innerWidth, maxPwr } = this.state;
let { language, sizeRatio } = this.context;
let maxBand = nextProps.bands[nextProps.bands.length - 1];
let nextMaxPwr = Math.max(nextProps.available, maxBand.retractedSum, maxBand.deployedSum);
if (maxPwr != nextMaxPwr) { // Update Axes if max power has changed
this._updateScales(innerWidth, nextMaxPwr, nextProps.available);
this.setState({ maxPwr: nextMaxPwr });
}
if (this.context !== nextContext) {
if (language !== nextContext.language) {
this.wattAxis.tickFormat(nextContext.language.formats.r2);
this.pctAxis.tickFormat(nextContext.language.formats.rPct);
}
if (nextProps.width != this.props.width || this.context !== nextContext) {
if (maxPwr != nextMaxPwr) { // Update Axes if max power has changed
this._updateScales(innerWidth, nextMaxPwr, nextProps.available);
this.setState({ maxPwr: nextMaxPwr });
} else if (nextProps.width != this.props.width || sizeRatio != nextContext.sizeRatio) {
this._updateDimensions(nextProps, nextContext.sizeRatio);
}
}
/**
* Render the power bands
* @return {React.Component} Power bands
*/
render() {
if (!this.props.width) {
return null;
@@ -142,7 +189,7 @@ export default class PowerBands extends TranslatedComponent {
let { f2, pct1, rPct, r2 } = formats; // wattFmt, pctFmt, pctAxis, wattAxis
let { available, bands, width } = props;
let { innerWidth, maxPwr, ret, dep } = state;
let pwrWarningClass = cn('threshold', {exceeded: bands[0].retractedSum * 2 >= available });
let pwrWarningClass = cn('threshold', { exceeded: bands[0].retractedSum * 2 >= available });
let deployed = [];
let retracted = [];
let retSelected = Object.keys(ret).length > 0;
@@ -150,7 +197,7 @@ export default class PowerBands extends TranslatedComponent {
let retSum = 0;
let depSum = 0;
for (var i = 0; i < bands.length; i++) {
for (let i = 0; i < bands.length; i++) {
let b = bands[i];
retSum += (!retSelected || ret[i]) ? b.retracted : 0;
depSum += (!depSelected || dep[i]) ? b.deployed + b.retracted : 0;
@@ -217,14 +264,13 @@ export default class PowerBands extends TranslatedComponent {
<g className='power-band'>{deployed}</g>
<g ref={ (elem) => d3.select(elem).call(this.wattAxis) } className='watt axis'></g>
<g ref={ (elem) => {
let axis = d3.select(elem);
axis.call(this.pctAxis);
axis.select('g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass);
}}
className='pct axis' transform={`translate(0,${state.innerHeight})`}></g>
let axis = d3.select(elem);
axis.call(this.pctAxis);
axis.select('g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass);
}} className='pct axis' transform={`translate(0,${state.innerHeight})`}></g>
<line x1={pctScale(0.5)} x2={pctScale(0.5)} y1='0' y2={state.innerHeight} className={pwrWarningClass} />
<text dy='0.5em' x='-3' y={state.retY} className='primary upp' textAnchor='end'>{translate('ret')}</text>
<text dy='0.5em' x='-3' y={state.depY} className='primary upp' textAnchor='end'>{translate('dep')}</text>
<text dy='0.5em' x='-3' y={state.retY} className='primary upp' textAnchor='end' onMouseOver={this.context.termtip.bind(null, 'retracted')} onMouseLeave={this._hidetip}>{translate('ret')}</text>
<text dy='0.5em' x='-3' y={state.depY} className='primary upp' textAnchor='end' onMouseOver={this.context.termtip.bind(null, 'deployed', 's')} onMouseLeave={this._hidetip}>{translate('dep')}</text>
<text dy='0.5em' x={innerWidth + 5} y={state.retY} className={getClass(retSelected, retSum, available)}>{f2(Math.max(0, retSum)) + ' (' + pct1(Math.max(0, retSum / available)) + ')'}</text>
<text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, available)}>{f2(Math.max(0, depSum)) + ' (' + pct1(Math.max(0, depSum / available)) + ')'}</text>
</g>

View File

@@ -2,9 +2,8 @@ import React from 'react';
import { findDOMNode } from 'react-dom';
import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents';
import PowerBands from './PowerBands';
import { slotName, nameComparator } from '../utils/SlotFunctions';
import { slotName, slotComparator } from '../utils/SlotFunctions';
import { Power, NoPower } from './SvgIcons';
const POWER = [
@@ -14,6 +13,9 @@ const POWER = [
<Power className='secondary-disabled' />
];
/**
* Power Management Section
*/
export default class PowerManagement extends TranslatedComponent {
static PropTypes = {
ship: React.PropTypes.object.isRequired,
@@ -21,6 +23,10 @@ export default class PowerManagement extends TranslatedComponent {
onChange: React.PropTypes.func.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
this._renderPowerRows = this._renderPowerRows.bind(this);
@@ -34,51 +40,77 @@ export default class PowerManagement extends TranslatedComponent {
};
}
/**
* Set the sort order and sort
* @param {string} predicate Sort predicate
*/
_sortOrder(predicate) {
let desc = this.state.desc;
if (predicate == this.state.predicate) {
desc = !desc;
} else {
desc = true;
}
this._sort(this.props.ship, predicate, desc);
this.setState({ predicate, desc });
}
/**
* Sorts the power list
* @param {Ship} ship Ship instance
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort order descending
*/
_sort(ship, predicate, desc) {
let powerList = ship.powerList;
let comp = slotComparator.bind(null, this.context.language.translate);
switch (predicate) {
case 'n': powerList.sort(nameComparator(this.context.language.translate)); break;
case 't': powerList.sort((a, b) => a.type.localeCompare(b.type)); break;
case 'pri': powerList.sort((a, b) => a.priority - b.priority);break;
case 'pwr': powerList.sort((a, b) => (a.m ? a.m.power : 0) - (b.m ? b.m.power : 0)); break;
case 'r': powerList.sort((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b)); break;
case 'd': powerList.sort((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true)); break;
case 'n': comp = comp(null, desc); break;
case 't': comp = comp((a, b) => a.type.localeCompare(b.type), desc); break;
case 'pri': comp = comp((a, b) => a.priority - b.priority, desc); break;
case 'pwr': comp = comp((a, b) => a.m.power - b.m.power, desc); break;
case 'r': comp = comp((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b), desc); break;
case 'd': comp = comp((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true), desc); break;
}
if (!desc) {
powerList.reverse();
}
powerList.sort(comp);
}
/**
* Update slot priority
* @param {Object} slot Slot model
* @param {number} inc increment / decrement
*/
_priority(slot, inc) {
if (this.props.ship.setSlotPriority(slot, slot.priority + inc)) {
this.props.onChange();
}
}
/**
* Toggle slot active/inactive
* @param {Object} slot Slot model
*/
_toggleEnabled(slot) {
this.props.ship.setSlotEnabled(slot, !slot.enabled);
this.props.onChange();
}
/**
* Generate/Render table rows
* @param {Ship} ship Ship instance
* @param {Function} translate Translate function
* @param {Function} pwr Localized Power formatter
* @param {Function} pct Localized Percent formatter
* @return {Array} Array of React.Component table rows
*/
_renderPowerRows(ship, translate, pwr, pct) {
let powerRows = [];
for (var i = 0, l = ship.powerList.length; i < l; i++) {
for (let i = 0, l = ship.powerList.length; i < l; i++) {
let slot = ship.powerList[i];
if (slot.m && slot.m.power) {
@@ -102,7 +134,7 @@ export default class PowerManagement extends TranslatedComponent {
{' ' + (slot.priority + 1) + ' '}
<span className='ptr' onClick={this._priority.bind(this, slot, 1)}>&#9658;</span>
</td>
<td className='ri ptr' style={{ width: '3.25em'}} onClick={toggleEnabled}>{pwr(m.power)}</td>
<td className='ri ptr' style={{ width: '3.25em' }} onClick={toggleEnabled}>{pwr(m.power)}</td>
<td className='ri ptr' style={{ width: '3em' }} onClick={toggleEnabled}><u>{pct(m.power / ship.powerAvailable)}</u></td>
{retractedElem}
{deployedElem}
@@ -112,32 +144,50 @@ export default class PowerManagement extends TranslatedComponent {
return powerRows;
}
/**
* Update power bands width from DOM
*/
_updateWidth() {
this.setState({ width: findDOMNode(this).offsetWidth });
}
componentWillMount(){
/**
* Add listeners when about to mount and sort power list
*/
componentWillMount() {
this._sort(this.props.ship, this.state.predicate, this.state.desc);
this.resizeListener = InterfaceEvents.addListener('windowResized', this._updateWidth);
this.resizeListener = this.context.onWindowResize(this._updateWidth);
}
/**
* Trigger DOM updates on mount
*/
componentDidMount() {
this._updateWidth();
}
/**
* Sort power list if the ship instance has changed
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextState Incoming/Next state
*/
componentWillUpdate(nextProps, nextState) {
// Can optimize this later: only sort when
// - predicate/desc changes
// - modules/language change AND sorting by type, name
// - power changes and sorting by pwr
// - enabled/disabled changes and sorting by priority
this._sort(nextProps.ship, nextState.predicate, nextState.desc);
if (this.props.ship != nextProps.ship) {
this._sort(nextProps.ship, nextState.predicate, nextState.desc);
}
}
componentWillUnmount(){
/**
* Remove listeners on unmount
*/
componentWillUnmount() {
this.resizeListener.remove();
}
/**
* Render power management section
* @return {React.Component} contents
*/
render() {
let { ship, code } = this.props;
let { translate, formats } = this.context.language;

View File

@@ -4,86 +4,91 @@ import cn from 'classnames';
import { SizeMap } from '../shipyard/Constants';
import { Warning } from './SvgIcons';
/**
* Ship Summary Table / Stats
*/
export default class ShipSummaryTable extends TranslatedComponent {
static propTypes = {
ship: React.PropTypes.object.isRequired
}
};
/**
* Render the table
* @return {React.Component} Summary table
*/
render() {
let ship = this.props.ship;
let language = this.context.language;
let { language, tooltip, termtip } = this.context;
let translate = language.translate;
let u = language.units;
let formats = language.formats;
let round = formats.round;
let int = formats.int;
let armourDetails = null;
let hide = tooltip.bind(null, null);
if (ship.armourMultiplier > 1 || ship.armourAdded) {
armourDetails = <u>({
(ship.armourMultiplier > 1 ? formats.rPct(ship.armourMultiplier) : '')
+ (ship.armourAdded ? ' + ' + ship.armourAdded : '')
(ship.armourMultiplier > 1 ? formats.rPct(ship.armourMultiplier) : '') +
(ship.armourAdded ? ' + ' + ship.armourAdded : '')
})</u>;
}
return (
<div id='summary'>
<table id='summaryTable'>
<thead>
<tr className='main'>
<th rowSpan={2}>{translate('size')}</th>
<th rowSpan={2}>{translate('agility')}</th>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canThrust() }) }>{translate('speed')}</th>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canBoost() }) }>{translate('boost')}</th>
<th rowSpan={2}>{translate('DPS')}</th>
<th rowSpan={2}>{translate('armour')}</th>
<th rowSpan={2}>{translate('shields')}</th>
<th colSpan={3}>{translate('mass')}</th>
<th rowSpan={2}>{translate('cargo')}</th>
<th rowSpan={2}>{translate('fuel')}</th>
<th colSpan={3}>{translate('jump range')}</th>
<th colSpan={3}>{translate('total range')}</th>
<th rowSpan={2}>{translate('lock factor')}</th>
</tr>
<tr>
<th className='lft'>{translate('hull')}</th>
<th>{translate('unladen')}</th>
<th>{translate('laden')}</th>
<th className='lft'>{translate('max')}</th>
<th>{translate('full tank')}</th>
<th>{translate('laden')}</th>
<th className='lft'>{translate('jumps')}</th>
<th>{translate('unladen')}</th>
<th>{translate('laden')}</th>
</tr>
</thead>
<tbody>
<tr>
<td className='cap'>{translate(SizeMap[ship.class])}</td>
<td>{ship.agility}/10</td>
<td>{ ship.canThrust() ? <span>{int(ship.topSpeed)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td>{ ship.canBoost() ? <span>{int(ship.topBoost)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td>{round(ship.totalDps)}</td>
<td>{int(ship.armour)} {armourDetails}</td>
<td>{int(ship.shieldStrength)} {u.MJ} { ship.shieldMultiplier > 1 && ship.shieldStrength > 0 ? <u>({formats.rPct(ship.shieldMultiplier)})</u> : null }</td>
<td>{ship.hullMass} {u.T}</td>
<td>{round(ship.unladenMass)} {u.T}</td>
<td>{round(ship.ladenMass)} {u.T}</td>
<td>{round(ship.cargoCapacity)} {u.T}</td>
<td>{round(ship.fuelCapacity)} {u.T}</td>
<td>{round(ship.unladenRange)} {u.LY}</td>
<td>{round(ship.fullTankRange)} {u.LY}</td>
<td>{round(ship.ladenRange)} {u.LY}</td>
<td>{round(ship.maxJumpCount)}</td>
<td>{round(ship.unladenTotalRange)} {u.LY}</td>
<td>{round(ship.ladenTotalRange)} {u.LY}</td>
<td>{ship.masslock}</td>
</tr>
</tbody>
</table>
</div>
);
return <div id='summary'>
<table id='summaryTable'>
<thead>
<tr className='main'>
<th rowSpan={2}>{translate('size')}</th>
<th onMouseEnter={termtip.bind(null, 'maneuverability')} onMouseLeave={hide} rowSpan={2}>{translate('MNV')}</th>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canThrust() }) }>{translate('speed')}</th>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canBoost() }) }>{translate('boost')}</th>
<th onMouseOver={termtip.bind(null, 'damage per second')} onMouseOut={hide} rowSpan={2}>{translate('DPS')}</th>
<th rowSpan={2}>{translate('armour')}</th>
<th rowSpan={2}>{translate('shields')}</th>
<th colSpan={3}>{translate('mass')}</th>
<th rowSpan={2}>{translate('cargo')}</th>
<th rowSpan={2}>{translate('fuel')}</th>
<th colSpan={3}>{translate('jump range')}</th>
<th colSpan={3}>{translate('total range')}</th>
<th onMouseOver={termtip.bind(null, 'mass lock factor')} onMouseOut={hide} rowSpan={2}>{translate('MLF')}</th>
</tr>
<tr>
<th className='lft'>{translate('hull')}</th>
<th>{translate('unladen')}</th>
<th>{translate('laden')}</th>
<th className='lft'>{translate('max')}</th>
<th>{translate('full tank')}</th>
<th>{translate('laden')}</th>
<th className='lft'>{translate('jumps')}</th>
<th>{translate('unladen')}</th>
<th>{translate('laden')}</th>
</tr>
</thead>
<tbody>
<tr>
<td className='cap'>{translate(SizeMap[ship.class])}</td>
<td>{ship.agility}/10</td>
<td>{ ship.canThrust() ? <span>{int(ship.topSpeed)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td>{ ship.canBoost() ? <span>{int(ship.topBoost)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td>{round(ship.totalDps)}</td>
<td>{int(ship.armour)} {armourDetails}</td>
<td>{int(ship.shieldStrength)} {u.MJ} { ship.shieldMultiplier > 1 && ship.shieldStrength > 0 ? <u>({formats.rPct(ship.shieldMultiplier)})</u> : null }</td>
<td>{ship.hullMass} {u.T}</td>
<td>{round(ship.unladenMass)} {u.T}</td>
<td>{round(ship.ladenMass)} {u.T}</td>
<td>{round(ship.cargoCapacity)} {u.T}</td>
<td>{round(ship.fuelCapacity)} {u.T}</td>
<td>{round(ship.unladenRange)} {u.LY}</td>
<td>{round(ship.fullTankRange)} {u.LY}</td>
<td>{round(ship.ladenRange)} {u.LY}</td>
<td>{round(ship.maxJumpCount)}</td>
<td>{round(ship.unladenTotalRange)} {u.LY}</td>
<td>{round(ship.ladenTotalRange)} {u.LY}</td>
<td>{ship.masslock}</td>
</tr>
</tbody>
</table>
</div>;
}
}

View File

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

View File

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

View File

@@ -1,10 +1,12 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents';
import { wrapCtxMenu } from '../utils/InterfaceEvents';
import { wrapCtxMenu } from '../utils/UtilityFunctions';
import { Equalizer } from '../components/SvgIcons';
import cn from 'classnames';
/**
* Abstract Slot Section
*/
export default class SlotSection extends TranslatedComponent {
static propTypes = {
@@ -14,6 +16,13 @@ export default class SlotSection extends TranslatedComponent {
togglePwr: React.PropTypes.func
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
* @param {string} sectionId Section DOM Id
* @param {string} sectionName Section name
*/
constructor(props, context, sectionId, sectionName) {
super(props);
this.sectionId = sectionId;
@@ -23,7 +32,10 @@ export default class SlotSection extends TranslatedComponent {
this._selectModule = this._selectModule.bind(this);
this._getSectionMenu = this._getSectionMenu.bind(this);
this._contextMenu = this._contextMenu.bind(this);
this._drop = this._drop.bind(this);
this._dragOverNone = this._dragOverNone.bind(this);
this._close = this._close.bind(this);
this.state = {};
}
// Must be implemented by subclasses:
@@ -31,33 +43,143 @@ export default class SlotSection extends TranslatedComponent {
// _getSectionMenu()
// _contextMenu()
/**
* Open a menu
* @param {string} menu Menu name
* @param {SyntheticEvent} event Event
*/
_openMenu(menu, event) {
event.stopPropagation();
if (this.props.currentMenu === menu) {
menu = null;
}
InterfaceEvents.openMenu(menu);
this.context.openMenu(menu);
}
/**
* Mount/Use the specified module in the slot
* @param {Object} slot Slot
* @param {Object} m Selected module
*/
_selectModule(slot, m) {
this.props.ship.use(slot, m);
this.props.onChange();
this._close();
}
/**
* Slot Drag Handler
* @param {object} originSlot Origin slot model
* @param {Event} e Drag Event
*/
_drag(originSlot, e) {
e.dataTransfer.setData('text/html', e.currentTarget);
e.dataTransfer.effectAllowed = 'all';
this.setState({ originSlot });
this._close();
}
/**
* Slot Drag Over Handler
* @param {object} targetSlot Potential drop target
* @param {Event} e Drag Event
*/
_dragOverSlot(targetSlot, e) {
e.preventDefault();
e.stopPropagation();
let os = this.state.originSlot;
if (os) {
console.log('has origin');
e.dataTransfer.dropEffect = os != targetSlot && targetSlot.maxClass >= os.m.class ? 'copyMove' : 'none';
this.setState({ targetSlot });
} else {
e.dataTransfer.dropEffect = 'none';
}
}
/**
* Drag over non-droppable target/element
* @param {Event} e Drag Event
*/
_dragOverNone(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'none';
this.setState({ targetSlot: null });
}
/**
* Slot drop handler. If the target is eligible swap the origin and target modules.
* If the target slot's current module cannot be mounted in the origin slot then
* the origin slot will be empty.
*/
_drop() {
let { originSlot, targetSlot } = this.state;
let m = originSlot.m;
if (targetSlot && m && targetSlot.maxClass >= m.class) {
// Swap modules if possible
if (targetSlot.m && originSlot.maxClass >= targetSlot.m.class) {
this.props.ship.use(originSlot, targetSlot.m, true);
} else { // Otherwise empty the origin slot
this.props.ship.use(originSlot, null, true); // Empty but prevent summary update
}
this.props.ship.use(targetSlot, m); // update target slot
this.props.onChange();
}
this.setState({ originSlot: null, targetSlot: null });
}
/**
* Determine drop eligibilty CSS class
* @param {Object} slot Current slot
* @param {Object} originSlot Origin slot
* @param {Object} targetSlot Target slot
* @return {string} CSS Class name
*/
_dropClass(slot, originSlot, targetSlot) {
if (!originSlot) {
return null;
}
if (slot === originSlot) {
if (targetSlot && targetSlot.m && originSlot.maxClass < targetSlot.m.class) {
return 'dropEmpty'; // Origin slot will be emptied
}
return null;
}
if (originSlot.m && slot.maxClass >= originSlot.m.class) { // Eligble drop slot
if (slot === targetSlot) {
return 'drop'; // Can drop
}
return 'eligible'; // Potential drop slot
}
return 'ineligible'; // Cannot be dropped / invalid drop slot
}
/**
* Toggle slot Active/Inactive
* @param {Object} slot Slot
*/
_togglePwr(slot) {
this.props.ship.setSlotEnabled(slot, !slot.enabled);
this.props.onChange();
this._close();
}
/**
* Close current menu
*/
_close() {
if (this.props.currentMenu) {
InterfaceEvents.closeMenu();
this.context.closeMenu();
}
}
/**
* Render the slot section
* @return {React.Component} Slot section
*/
render() {
let translate = this.context.language.translate;
let sectionMenuOpened = this.props.currentMenu === this.sectionName;
@@ -65,8 +187,8 @@ export default class SlotSection extends TranslatedComponent {
let ctx = wrapCtxMenu(this._contextMenu);
return (
<div id={this.sectionId} className={'group'}>
<div className={cn('section-menu', {selected: sectionMenuOpened})} onClick={open} onContextMenu={ctx}>
<div id={this.sectionId} className={'group'} onDragLeave={this._dragOverNone}>
<div className={cn('section-menu', { selected: sectionMenuOpened })} onClick={open} onContextMenu={ctx}>
<h1>{translate(this.sectionName)} <Equalizer/></h1>
{sectionMenuOpened ? this._getSectionMenu(translate) : null }
</div>

View File

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

View File

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

View File

@@ -1,15 +1,29 @@
import React from 'react';
import cn from 'classnames';
import shallowEqual from '../utils/shallowEqual';
import { shallowEqual } from '../utils/UtilityFunctions';
/**
* Base SVG Icon Component
*/
class SvgIcon extends React.Component {
/**
* Only rerender an SVG Icon if properties have changed
* @param {Object} nextProps Next/Incoming properties
* @return {Boolean} True if properties have changed
*/
shouldComponentUpdate(nextProps) { return !shallowEqual(this.props, nextProps); }
svg() { return null; }
/**
* Standard SVG view box, can/should be overriden by sub-classes as necessary
* @return {string} view box string
*/
viewBox() { return '0 0 32 32'; }
/**
* Render the Icon
* @return {React.Component} SVG Icon
*/
render() {
return (
<svg className={cn('icon', this.props.className)} style={this.props.style} viewBox={this.viewBox()}>
@@ -19,7 +33,14 @@ class SvgIcon extends React.Component {
}
}
/**
* Bin Icon - Delete
*/
export class Bin extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g>
<path d='M4 10v20c0 1.1 0.9 2 2 2h18c1.1 0 2-0.9 2-2v-20h-22zM10 28h-2v-14h2v14zM14 28h-2v-14h2v14zM18 28h-2v-14h2v14zM22 28h-2v-14h2v14z'/>
@@ -28,7 +49,14 @@ export class Bin extends SvgIcon {
}
}
/**
* Coriolis Logo
*/
export class CoriolisLogo extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g transform='translate(1,1)'>
<path stroke='#ff3b00' transform='rotate(45 15 15)' d='m4,4 l 11,-4 l 11,4 l 4,11 l -4,11 l -11,4 l -11,-4 l -4,-11 l 4,-11 l 22,0 l 0,22 l -22,0 z' strokeWidth='1' fill='#000000'/>
@@ -37,13 +65,27 @@ export class CoriolisLogo extends SvgIcon {
}
}
/**
* Download / To Inbox
*/
export class Download extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M16 18l8-8h-6v-8h-4v8h-6zM23.273 14.727l-2.242 2.242 8.128 3.031-13.158 4.907-13.158-4.907 8.127-3.031-2.242-2.242-8.727 3.273v8l16 6 16-6v-8z'/>;
}
}
/**
* Eddb.io Logo
*/
export class Eddb extends SvgIcon {
/**
* Render the Icon
* @return {React.Component} SVG Icon
*/
render() {
return <svg className={cn(this.props.className)} style={this.props.style} viewBox='0 0 90 32'>
<path d='M19.1,25.2c0.3,0,0.6,0.1,0.7,0.2c0.2,0.1,0.3,0.3,0.4,0.4c0.1,0.2,0.2,0.4,0.2,0.6v3.3c0,0.3-0.1,0.6-0.2,0.7c-0.1,0.2-0.3,0.3-0.4,0.3c-0.2,0.1-0.4,0.2-0.6,0.1H3.6c-0.3,0-0.6-0.1-0.7-0.2c-0.2-0.1-0.3-0.3-0.3-0.4c-0.1-0.2-0.2-0.4-0.1-0.6V10.2c0-0.3,0.1-0.5,0.2-0.7C2.7,9.4,2.9,9.3,3,9.2C3.2,9.1,3.4,9,3.6,9h15.5c0.3,0,0.6,0.1,0.7,0.2c0.2,0.1,0.3,0.3,0.4,0.4c0.1,0.2,0.2,0.4,0.2,0.6V22c0,0.3-0.1,0.6-0.2,0.7c-0.1,0.2-0.3,0.3-0.4,0.3c-0.2,0.1-0.4,0.2-0.6,0.1h-6.8v-6.8c0.3-0.2,0.6-0.4,0.8-0.7c0.2-0.3,0.3-0.7,0.3-1c0-0.6-0.2-1.1-0.6-1.4c-0.4-0.4-0.9-0.6-1.4-0.6c-0.5,0-1,0.2-1.4,0.6c-0.4,0.4-0.6,0.9-0.6,1.4c0,0.8,0.3,1.4,1,1.8v8.7H19.1z'/>
@@ -54,7 +96,14 @@ export class Eddb extends SvgIcon {
}
}
/**
* Embed - <>
*/
export class Embed extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g>
<path d='M18 23l3 3 10-10-10-10-3 3 7 7z'/>
@@ -63,8 +112,19 @@ export class Embed extends SvgIcon {
}
}
/**
* Equalizer
*/
export class Equalizer extends SvgIcon {
viewBox () { return '0 0 1024 1024'; }
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 1024 1024'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g>
<path d='M448 128v-16c0-26.4-21.6-48-48-48h-160c-26.4 0-48 21.6-48 48v16h-192v128h192v16c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-16h576v-128h-576zM256 256v-128h128v128h-128zM832 432c0-26.4-21.6-48-48-48h-160c-26.4 0-48 21.6-48 48v16h-576v128h576v16c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-16h192v-128h-192v-16zM640 576v-128h128v128h-128zM448 752c0-26.4-21.6-48-48-48h-160c-26.4 0-48 21.6-48 48v16h-192v128h192v16c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-16h576v-128h-576v-16zM256 896v-128h128v128h-128z'/>
@@ -72,32 +132,71 @@ export class Equalizer extends SvgIcon {
}
}
/**
* Floppy disk - save
*/
export class FloppyDisk extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M28 0h-28v32h32v-28l-4-4zM16 4h4v8h-4v-8zM28 28h-24v-24h2v10h18v-10h2.343l1.657 1.657v22.343z' />;
}
}
/**
* Fuel Gauge
*/
export class Fuel extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M16 0c-8.837 0-16 7.163-16 16s7.163 16 16 16 16-7.163 16-16-7.163-16-16-16zM9.464 26.067c0.347-0.957 0.536-1.99 0.536-3.067 0-3.886-2.463-7.197-5.913-8.456 0.319-2.654 1.508-5.109 3.427-7.029 2.267-2.266 5.28-3.515 8.485-3.515s6.219 1.248 8.485 3.515c1.92 1.92 3.108 4.375 3.428 7.029-3.45 1.26-5.913 4.57-5.913 8.456 0 1.077 0.189 2.11 0.536 3.067-1.928 1.258-4.18 1.933-6.536 1.933s-4.608-0.675-6.536-1.933zM17.242 20.031c0.434 0.109 0.758 0.503 0.758 0.969v2c0 0.55-0.45 1-1 1h-2c-0.55 0-1-0.45-1-1v-2c0-0.466 0.324-0.86 0.758-0.969l0.742-14.031h1l0.742 14.031z' />;
}
}
/**
* Github Logo
*/
export class GitHub extends SvgIcon {
viewBox() { return '0 0 1024 1024' };
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 1024 1024'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M512 0C229.252 0 0 229.25199999999995 0 512c0 226.251 146.688 418.126 350.155 485.813 25.593 4.686 34.937-11.125 34.937-24.626 0-12.188-0.469-52.562-0.718-95.314-128.708 23.46-161.707-31.541-172.469-60.373-5.525-14.809-30.407-60.249-52.398-72.263-17.988-9.828-43.26-33.237-0.917-33.735 40.434-0.476 69.348 37.308 78.471 52.75 45.938 77.749 119.876 55.627 148.999 42.5 4.654-32.999 17.902-55.627 32.501-68.373-113.657-12.939-233.22-56.875-233.22-253.063 0-55.94 19.968-101.561 52.658-137.404-5.22-12.999-22.844-65.095 5.063-135.563 0 0 42.937-13.749 140.811 52.501 40.811-11.406 84.594-17.031 128.124-17.22 43.499 0.188 87.314 5.874 128.188 17.28 97.689-66.311 140.686-52.501 140.686-52.501 28 70.532 10.375 122.564 5.124 135.499 32.811 35.844 52.626 81.468 52.626 137.404 0 196.686-119.751 240-233.813 252.686 18.439 15.876 34.748 47.001 34.748 94.748 0 68.437-0.686 123.627-0.686 140.501 0 13.625 9.312 29.561 35.25 24.562C877.436 929.998 1024 738.126 1024 512 1024 229.25199999999995 794.748 0 512 0z' />;
}
}
/**
* Infinite / Infinity
*/
export class Infinite extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M24.5 23.5c-2.003 0-3.887-0.78-5.303-2.197l-3.197-3.196-3.196 3.196c-1.417 1.417-3.3 2.197-5.303 2.197s-3.887-0.78-5.304-2.197c-1.417-1.417-2.197-3.3-2.197-5.303s0.78-3.887 2.197-5.304c1.417-1.417 3.3-2.197 5.304-2.197s3.887 0.78 5.303 2.197l3.196 3.196 3.196-3.196c1.417-1.417 3.3-2.197 5.303-2.197s3.887 0.78 5.303 2.197c1.417 1.417 2.197 3.3 2.197 5.304s-0.78 3.887-2.197 5.303c-1.416 1.417-3.3 2.197-5.303 2.197zM21.304 19.197c0.854 0.853 1.989 1.324 3.196 1.323s2.342-0.47 3.196-1.324c0.854-0.854 1.324-1.989 1.324-3.196s-0.47-2.342-1.324-3.196c-0.854-0.854-1.989-1.324-3.196-1.324s-2.342 0.47-3.196 1.324l-3.196 3.196 3.196 3.197zM7.5 11.48c-1.207 0-2.342 0.47-3.196 1.324s-1.324 1.989-1.324 3.196c0 1.207 0.47 2.342 1.324 3.196s1.989 1.324 3.196 1.324c1.207 0 2.342-0.47 3.196-1.324l3.196-3.196-3.196-3.196c-0.854-0.854-1.989-1.324-3.196-1.324v0z'/>;
}
}
/**
* Info - i within circle
*/
export class Info extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g>
<path d='M14 9.5c0-0.825 0.675-1.5 1.5-1.5h1c0.825 0 1.5 0.675 1.5 1.5v1c0 0.825-0.675 1.5-1.5 1.5h-1c-0.825 0-1.5-0.675-1.5-1.5v-1z'/>
@@ -107,7 +206,14 @@ export class Info extends SvgIcon {
}
}
/**
* Link / Permalink / Chain
*/
export class LinkIcon extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g>
<path d='M13.757 19.868c-0.416 0-0.832-0.159-1.149-0.476-2.973-2.973-2.973-7.81 0-10.783l6-6c1.44-1.44 3.355-2.233 5.392-2.233s3.951 0.793 5.392 2.233c2.973 2.973 2.973 7.81 0 10.783l-2.743 2.743c-0.635 0.635-1.663 0.635-2.298 0s-0.635-1.663 0-2.298l2.743-2.743c1.706-1.706 1.706-4.481 0-6.187-0.826-0.826-1.925-1.281-3.094-1.281s-2.267 0.455-3.094 1.281l-6 6c-1.706 1.706-1.706 4.481 0 6.187 0.635 0.635 0.635 1.663 0 2.298-0.317 0.317-0.733 0.476-1.149 0.476z'/>
@@ -116,39 +222,89 @@ export class LinkIcon extends SvgIcon {
}
}
/**
* No Power - Lightning bolt + no entry
*/
export class NoPower extends SvgIcon {
viewBox() { return '0 0 512 512' };
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 512 512'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M437.020 74.98c-48.353-48.351-112.64-74.98-181.020-74.98s-132.667 26.629-181.020 74.98c-48.351 48.353-74.98 112.64-74.98 181.020s26.629 132.667 74.98 181.020c48.353 48.351 112.64 74.98 181.020 74.98s132.667-26.629 181.020-74.98c48.351-48.353 74.98-112.64 74.98-181.020s-26.629-132.667-74.98-181.020zM448 256c0 41.407-13.177 79.794-35.556 111.19l-267.633-267.634c31.396-22.379 69.782-35.556 111.189-35.556 105.869 0 192 86.131 192 192zM64 256c0-41.407 13.177-79.793 35.556-111.189l267.635 267.634c-31.397 22.378-69.784 35.555-111.191 35.555-105.869 0-192-86.131-192-192z'/>;
}
}
/**
* Notification - Exclamation mark within circle
*/
export class Notification extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M16 3c-3.472 0-6.737 1.352-9.192 3.808s-3.808 5.72-3.808 9.192c0 3.472 1.352 6.737 3.808 9.192s5.72 3.808 9.192 3.808c3.472 0 6.737-1.352 9.192-3.808s3.808-5.72 3.808-9.192c0-3.472-1.352-6.737-3.808-9.192s-5.72-3.808-9.192-3.808zM16 0v0c8.837 0 16 7.163 16 16s-7.163 16-16 16c-8.837 0-16-7.163-16-16s7.163-16 16-16zM14 22h4v4h-4zM14 6h4v12h-4z'/>;
}
}
/**
* Power - Lightning Bolt
*/
export class Power extends SvgIcon {
viewBox() { return '0 0 512 512' };
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 512 512'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M192 0l-192 256h192l-128 256 448-320h-256l192-192z'/>;
}
}
/**
* Question mark within Circle
*/
export class Question extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M14 22h4v4h-4zM22 8c1.105 0 2 0.895 2 2v6l-6 4h-4v-2l6-4v-2h-10v-4h12zM16 3c-3.472 0-6.737 1.352-9.192 3.808s-3.808 5.72-3.808 9.192c0 3.472 1.352 6.737 3.808 9.192s5.72 3.808 9.192 3.808c3.472 0 6.737-1.352 9.192-3.808s3.808-5.72 3.808-9.192c0-3.472-1.352-6.737-3.808-9.192s-5.72-3.808-9.192-3.808zM16 0v0c8.837 0 16 7.163 16 16s-7.163 16-16 16c-8.837 0-16-7.163-16-16s7.163-16 16-16z'/>;
}
}
/**
* Reload - Clockwise circular arrow
*/
export class Reload extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M32 12h-12l4.485-4.485c-2.267-2.266-5.28-3.515-8.485-3.515s-6.219 1.248-8.485 3.515c-2.266 2.267-3.515 5.28-3.515 8.485s1.248 6.219 3.515 8.485c2.267 2.266 5.28 3.515 8.485 3.515s6.219-1.248 8.485-3.515c0.189-0.189 0.371-0.384 0.546-0.583l3.010 2.634c-2.933 3.349-7.239 5.464-12.041 5.464-8.837 0-16-7.163-16-16s7.163-16 16-16c4.418 0 8.418 1.791 11.313 4.687l4.687-4.687v12z'/>;
}
}
/**
* Warning - Exclamation point witin triangle
*/
export class Warning extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g>
<path d='M16 2.899l13.409 26.726h-26.819l13.409-26.726zM16 0c-0.69 0-1.379 0.465-1.903 1.395l-13.659 27.222c-1.046 1.86-0.156 3.383 1.978 3.383h27.166c2.134 0 3.025-1.522 1.978-3.383h0l-13.659-27.222c-0.523-0.93-1.213-1.395-1.903-1.395v0z'/>
@@ -158,8 +314,19 @@ export class Warning extends SvgIcon {
}
}
/**
* Fixed mount hardpoint
*/
export class MountFixed extends SvgIcon {
viewBox () { return '0 0 200 200'; }
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g>
<circle fillOpacity='0' r='70' cy='100' cx='100' strokeWidth='5' />
@@ -171,8 +338,19 @@ export class MountFixed extends SvgIcon {
}
}
/**
* Gimballed mount hardpoint
*/
export class MountGimballed extends SvgIcon {
viewBox () { return '0 0 200 200'; }
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g>
<ellipse ry='25' rx='95' cy='100' cx='100' fillOpacity='0' strokeWidth='5' />
@@ -181,8 +359,19 @@ export class MountGimballed extends SvgIcon {
}
}
/**
* Turrent mount hardpoint
*/
export class MountTurret extends SvgIcon {
viewBox () { return '0 0 200 200'; }
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g>
<line y2='170' x2='162' y1='170' x1='8' strokeWidth='6' />
@@ -192,39 +381,84 @@ export class MountTurret extends SvgIcon {
}
}
/**
* Rocket ship
*/
export class Rocket extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M22 2l-10 10h-6l-6 8c0 0 6.357-1.77 10.065-0.94l-10.065 12.94 13.184-10.255c1.839 4.208-1.184 10.255-1.184 10.255l8-6v-6l10-10 2-10-10 2z'/>;
}
}
/**
* Hammer
*/
export class Hammer extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M31.562 25.905l-9.423-9.423c-0.583-0.583-1.538-0.583-2.121 0l-0.707 0.707-5.75-5.75 9.439-9.439h-10l-4.439 4.439-0.439-0.439h-2.121v2.121l0.439 0.439-6.439 6.439 5 5 6.439-6.439 5.75 5.75-0.707 0.707c-0.583 0.583-0.583 1.538 0 2.121l9.423 9.423c0.583 0.583 1.538 0.583 2.121 0l3.535-3.535c0.583-0.583 0.583-1.538 0-2.121z'/>;
}
}
/**
* Stats bars / Histogram / Compare
*/
export class StatsBars extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M0 26h32v4h-32zM4 18h4v6h-4zM10 10h4v14h-4zM16 16h4v8h-4zM22 4h4v20h-4z'/>;
}
}
/**
* Cogs / Settings
*/
export class Cogs extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M11.366 22.564l1.291-1.807-1.414-1.414-1.807 1.291c-0.335-0.187-0.694-0.337-1.071-0.444l-0.365-2.19h-2l-0.365 2.19c-0.377 0.107-0.736 0.256-1.071 0.444l-1.807-1.291-1.414 1.414 1.291 1.807c-0.187 0.335-0.337 0.694-0.443 1.071l-2.19 0.365v2l2.19 0.365c0.107 0.377 0.256 0.736 0.444 1.071l-1.291 1.807 1.414 1.414 1.807-1.291c0.335 0.187 0.694 0.337 1.071 0.444l0.365 2.19h2l0.365-2.19c0.377-0.107 0.736-0.256 1.071-0.444l1.807 1.291 1.414-1.414-1.291-1.807c0.187-0.335 0.337-0.694 0.444-1.071l2.19-0.365v-2l-2.19-0.365c-0.107-0.377-0.256-0.736-0.444-1.071zM7 27c-1.105 0-2-0.895-2-2s0.895-2 2-2 2 0.895 2 2-0.895 2-2 2zM32 12v-2l-2.106-0.383c-0.039-0.251-0.088-0.499-0.148-0.743l1.799-1.159-0.765-1.848-2.092 0.452c-0.132-0.216-0.273-0.426-0.422-0.629l1.219-1.761-1.414-1.414-1.761 1.219c-0.203-0.149-0.413-0.29-0.629-0.422l0.452-2.092-1.848-0.765-1.159 1.799c-0.244-0.059-0.492-0.109-0.743-0.148l-0.383-2.106h-2l-0.383 2.106c-0.251 0.039-0.499 0.088-0.743 0.148l-1.159-1.799-1.848 0.765 0.452 2.092c-0.216 0.132-0.426 0.273-0.629 0.422l-1.761-1.219-1.414 1.414 1.219 1.761c-0.149 0.203-0.29 0.413-0.422 0.629l-2.092-0.452-0.765 1.848 1.799 1.159c-0.059 0.244-0.109 0.492-0.148 0.743l-2.106 0.383v2l2.106 0.383c0.039 0.251 0.088 0.499 0.148 0.743l-1.799 1.159 0.765 1.848 2.092-0.452c0.132 0.216 0.273 0.426 0.422 0.629l-1.219 1.761 1.414 1.414 1.761-1.219c0.203 0.149 0.413 0.29 0.629 0.422l-0.452 2.092 1.848 0.765 1.159-1.799c0.244 0.059 0.492 0.109 0.743 0.148l0.383 2.106h2l0.383-2.106c0.251-0.039 0.499-0.088 0.743-0.148l1.159 1.799 1.848-0.765-0.452-2.092c0.216-0.132 0.426-0.273 0.629-0.422l1.761 1.219 1.414-1.414-1.219-1.761c0.149-0.203 0.29-0.413 0.422-0.629l2.092 0.452 0.765-1.848-1.799-1.159c0.059-0.244 0.109-0.492 0.148-0.743l2.106-0.383zM21 15.35c-2.402 0-4.35-1.948-4.35-4.35s1.948-4.35 4.35-4.35 4.35 1.948 4.35 4.35c0 2.402-1.948 4.35-4.35 4.35z'/>;
}
}
/**
* Power Switch - Reset
*/
export class Switch extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M20 4.581v4.249c1.131 0.494 2.172 1.2 3.071 2.099 1.889 1.889 2.929 4.4 2.929 7.071s-1.040 5.182-2.929 7.071c-1.889 1.889-4.4 2.929-7.071 2.929s-5.182-1.040-7.071-2.929c-1.889-1.889-2.929-4.4-2.929-7.071s1.040-5.182 2.929-7.071c0.899-0.899 1.94-1.606 3.071-2.099v-4.249c-5.783 1.721-10 7.077-10 13.419 0 7.732 6.268 14 14 14s14-6.268 14-14c0-6.342-4.217-11.698-10-13.419zM14 0h4v16h-4z'/>;
}
}
/**
* In-game Coriolis Station logo
*/
export class StationCoriolis extends SvgIcon {
viewBox () { return '0 0 200 200'; }
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g>
<rect x='73.001' y='94.017' width='53.997' height='11.945'/>
@@ -233,8 +467,19 @@ export class StationCoriolis extends SvgIcon {
}
}
/**
* In-game Ocellus Station logo
*/
export class StationOcellus extends SvgIcon {
viewBox () { return '0 0 200 200'; }
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g>
<path d='M100.002,200C155.139,200,200,155.142,200,100.001c0-55.143-44.861-100.002-99.998-100.002C44.86-0.001-0.002,44.857-0.002,100.001C-0.001,155.142,44.86,200,100.002,200z M100.002,5.574c52.063,0,94.423,42.359,94.423,94.427c0,52.067-42.361,94.422-94.423,94.422c-52.07,0-94.428-42.358-94.428-94.422C5.574,47.933,47.933,5.574,100.002,5.574z'/>
@@ -244,8 +489,19 @@ export class StationOcellus extends SvgIcon {
}
}
/**
* In-game Orbis Station logo
*/
export class StationOrbis extends SvgIcon {
viewBox () { return '0 0 200 200'; }
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g>
<path d='M100.002,200c55.138,0,99.996-44.861,99.996-100c0-55.141-44.858-100-99.996-100C44.861,0-0.001,44.857-0.001,100C0,155.139,44.861,200,100.002,200z M100.002,194.424c-35.465,0-66.413-19.663-82.552-48.651l44.426-23.388c7.704,13.067,21.888,21.884,38.127,21.884c16.054,0,30.096-8.621,37.853-21.446l44.441,23.389C166.092,174.961,135.282,194.424,100.002,194.424zM100.002,61.306c21.335,0,38.691,17.356,38.691,38.694c0,21.338-17.364,38.691-38.691,38.691c-21.339,0-38.696-17.354-38.696-38.691C61.307,78.662,78.663,61.306,100.002,61.306zM194.422,100c0,14.802-3.427,28.808-9.521,41.287l-44.447-23.4c2.433-5.477,3.812-11.521,3.812-17.89c0-23.578-18.539-42.852-41.8-44.145V5.636C153.392,6.956,194.422,48.762,194.422,100z M96.895,5.655v50.233C73.938,57.491,55.73,76.635,55.73,100c0,6.187,1.286,12.081,3.592,17.434l-44.455,23.402C8.911,128.472,5.571,114.619,5.571,100C5.577,48.972,46.261,7.297,96.895,5.655z'/>
@@ -254,8 +510,19 @@ export class StationOrbis extends SvgIcon {
}
}
/**
* In-game Outpost Station logo
*/
export class StationOutpost extends SvgIcon {
viewBox () { return '0 0 200 200'; }
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g>
<path d='M145.137,59.126h4.498v6.995h5.576V46.556h-5.576v6.994h-4.498V16.328h-5.574v57.667h-15.411v14.824h-7.63v-14.58h-13.044v14.58h-8.295v-14.58H82.138v14.58h-6.573v-14.58H59.072v14.58h-6.573v-14.58H39.458v36.338h13.041V94.391h6.573v16.186h16.493V94.391h6.573v16.186h13.044V94.391h8.295v16.186h13.044V94.391h7.63v40.457l17.634,17.637h13.185v31.182h5.577V73.996H145.14v-14.87H145.137z M154.97,146.907h-10.871l-14.376-14.376V79.57h25.247V146.907z'/>
@@ -265,14 +532,32 @@ export class StationOutpost extends SvgIcon {
}
}
/**
* Upload - From inbox
*/
export class Upload extends SvgIcon {
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <path d='M14 18h4v-8h6l-8-8-8 8h6zM20 13.5v3.085l9.158 3.415-13.158 4.907-13.158-4.907 9.158-3.415v-3.085l-12 4.5v8l16 6 16-6v-8z'/>;
}
}
/**
* Elite Dangerous Loader / Spinner
*/
export class Loader extends SvgIcon {
viewBox () { return '0 0 40 40'; }
/**
* Overriden view box
* @return {string} view box
*/
viewBox() { return '0 0 40 40'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <g className={'loader'}>
<path d='m5,8l5,8l5,-8z' className={'l1 d1'} />

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 shallowEqual from '../utils/shallowEqual';
import { shallowEqual } from '../utils/UtilityFunctions';
/**
* @class Abstract TranslatedComponent
* Abstract Translated Component
*/
export default class TranslatedComponent extends React.Component {
static contextTypes = {
language: React.PropTypes.object.isRequired,
sizeRatio: React.PropTypes.number.isRequired
}
sizeRatio: React.PropTypes.number.isRequired,
openMenu: React.PropTypes.func.isRequired,
closeMenu: React.PropTypes.func.isRequired,
showModal: React.PropTypes.func.isRequired,
hideModal: React.PropTypes.func.isRequired,
tooltip: React.PropTypes.func.isRequired,
termtip: React.PropTypes.func.isRequired,
onWindowResize: React.PropTypes.func.isRequired
};
/**
* Created an instance of a Translated Component. This is an abstract class.
@@ -23,9 +30,9 @@ export default class TranslatedComponent extends React.Component {
/**
* Determine if the context change incldues a language or size change
* @param {object} nextContext The incoming / next context
* @return {boolean} true if the language has changed
* @return {boolean} true if the language has changed
*/
didContextChange(nextContext){
didContextChange(nextContext) {
return nextContext.language !== this.context.language || nextContext.sizeRatio != this.context.sizeRatio;
}
@@ -34,14 +41,12 @@ export default class TranslatedComponent extends React.Component {
* props, state, or context changes. This method performs a shallow comparison to
* determine change.
*
* @param {object} nextProps
* @param {objec} nextState
* @param {objec} nextContext
* @param {object} nextProps Next/Incoming Properties
* @param {objec} nextState Next/Incoming State
* @param {objec} nextContext Next/Incoming Context
* @return {boolean} True if props, state, or context has changed
*/
shouldComponentUpdate(nextProps, nextState, nextContext) {
return !shallowEqual(this.props, nextProps)
|| !shallowEqual(this.state, nextState)
|| this.didContextChange(nextContext);
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState) || this.didContextChange(nextContext);
}
}

View File

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

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
* @param {string} langCode ISO Language code
* @return {object} Language units, translation and format functions
* @return {Object} Language units, translation and format functions
*/
export function getLanguage(langCode) {
let lang, translate;
@@ -39,15 +39,16 @@ export function getLanguage(langCode) {
return {
formats: {
gen: gen, // General number format (.e.g 1,001,001.1234)
gen, // General number format (.e.g 1,001,001.1234)
int: d3Locale.numberFormat(',.0f'), // Fixed to 0 decimal places (.e.g 1,001)
f2: d3Locale.numberFormat(',.2f'), // Fixed to 2 decimal places (.e.g 1,001.10)
s2: d3Locale.numberFormat('.2s'), // SI Format to 2 decimal places (.e.g 1.1k)
pct: d3Locale.numberFormat('.2%'), // % to 2 decimal places (.e.g 5.40%)
pct1: d3Locale.numberFormat('.1%'), // % to 1 decimal places (.e.g 5.4%)
r2: d3Locale.numberFormat('.2r'), // Rounded to 2 decimal places (.e.g 5.12, 4.10)
r2: d3Locale.numberFormat('.2r'), // Rounded to 2 significant numbers (.e.g 512 => 510, 4.122 => 4.1)
rPct: d3.format('%'), // % to 0 decimal places (.e.g 5%)
round: (d) => gen(d3.round(d, 2)), // Rounded to 0-2 decimal places (.e.g 5.12, 4.1)
time: (d) => Math.floor(d / 60) + ':' + ('00' + Math.floor(d % 60)).substr(-2, 2)
time: (d) => (d < 0 ? '-' : '') + Math.floor(Math.abs(d) / 60) + ':' + ('00' + Math.floor(Math.abs(d) % 60)).substr(-2, 2)
},
translate,
units: {
@@ -64,8 +65,7 @@ export function getLanguage(langCode) {
pm: <u>{translate('/min')}</u>, // per minute
T: <u>{' ' + translate('T')}</u>, // Metric Tons
}
}
};
}
/**

View File

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

View File

@@ -14,220 +14,57 @@ export const formats = {
};
export const terms = {
PHRASE_EXPORT_DESC: 'A detailed JSON export of your build for use in other sites and tools',
// 'A-Rated': 'A-Rated',
// about: 'about',
// action: 'action',
// added: 'added',
// Advanced: 'Advanced',
// 'Advanced Discovery Scanner': 'Advanced Discovery Scanner',
// agility: 'agility',
// alpha: 'alpha',
// ammo: 'ammo',
PHRASE_BACKUP_DESC: 'Backup of all Coriolis data to save or transfer to another browser/device',
PHRASE_CONFIRMATION: 'Are You Sure?',
// armour: 'armour',
PHRASE_EXPORT_DESC: 'A detailed JSON export of your build for use in other sites and tools',
PHRASE_IMPORT: 'Paste JSON or import here',
PHRASE_NO_BUILDS: 'No builds added to comparison!',
PHRASE_NO_RETROCH: 'No Retrofitting changes',
PHRASE_SELECT_BUILDS: 'Select Builds to Compare',
PHRASE_UPDATE_RDY: 'Update Available! Click to Refresh',
am: 'Auto Field-Maintenance Unit',
// available: 'available',
// backup: 'backup',
'Basic Discovery Scanner': 'Basic Discovery Scanner',
bl: 'Beam Laser',
// beta: 'beta',
// bins: 'bins',
// boost: 'boost',
// build: 'build',
// 'build name': 'Build Name',
// builds: 'builds',
bh: 'bulkheads',
bsg: 'Bi-Weave Shield Generator',
ul: 'Burst Laser',
buy: 'buy',
// cancel: 'cancel',
c: 'Cannon',
// capital: 'capital',
// cargo: 'cargo',
// 'Cargo Hatch': 'Cargo Hatch',
cr: 'Cargo Rack',
cs: 'Cargo Scanner',
cells: 'cells',
// 'Chaff Launcher': 'Chaff Launcher',
// close: 'close',
cc: 'Collector Limpet Controller',
// compare: 'compare',
// 'compare all': 'compare all',
// comparison: 'comparison',
// comparisons: 'comparisons',
// component: 'component',
// cost: 'cost',
// costs: 'costs',
cm: 'Countermeasure',
// CR: 'CR',
// create: 'create',
// 'create new': 'create new',
// credits: 'credits',
// Cytoscrambler: 'Cytoscrambler',
// damage: 'damage',
// delete: 'delete',
// 'delete all': 'delete all',
// dep: 'dep',
// deployed: 'deployed',
// 'detailed export': 'detailed export',
// 'Detailed Surface Scanner': 'Detailed Surface Scanner',
// disabled: 'disabled',
// discount: 'discount',
// Distruptor: 'Distruptor',
dc: 'Docking Computer',
// done: 'done',
// DPS: 'DPS',
// 'edit data': 'edit data',
// efficiency: 'efficiency',
// 'Electronic Countermeasure': 'Electronic Countermeasure',
// empty: 'empty',
// Enforcer: 'Enforcer',
// ENG: 'ENG',
// 'Enter Name': 'Enter Name',
// EPS: 'EPS',
// export: 'export',
// fixed: 'fixed',
// forum: 'forum',
fc: 'Fragment Cannon',
fd: 'Frame Shift Drive',
ws: 'Frame Shift Wake Scanner',
// FSD: 'FSD',
fsd: 'Frame Shift Drive',
fi: 'FSD Interdictor',
// fuel: 'fuel',
fs: 'Fuel Scoop',
ft: 'Fuel Tank',
fx: 'Fuel Transfer Limpet Controller',
// 'full tank': 'full tank',
// Gimballed: 'Gimballed',
// H: 'H',
// hardpoints: 'hardpoints',
hb: 'Hatch Breaker Limpet Controller',
// 'Heat Sink Launcher': 'Heat Sink Launcher',
// huge: 'huge',
// hull: 'hull',
hr: 'Hull Reinforcement Package',
// 'Imperial Hammer': 'Imperial Hammer',
// import: 'import',
// 'import all': 'import all',
// insurance: 'insurance',
// 'Intermediate Discovery Scanner': 'Intermediate Discovery Scanner',
// 'internal compartments': 'internal compartments',
// 'jump range': 'jump range',
// jumps: 'jumps',
kw: 'Kill Warrant Scanner',
// L: 'L',
// laden: 'laden',
// language: 'language',
// large: 'large',
// 'limpets': 'Limpets',
ls: 'life support',
// 'Lightweight Alloy': 'Lightweight Alloy',
// 'lock factor': 'lock factor',
// LS: 'Ls',
// LY: 'LY',
// M: 'M',
// 'm/s': 'm/s',
// mass: 'mass',
// max: 'max',
// 'max mass': 'max mass',
// medium: 'medium',
// 'Military Grade Composite': 'Military Grade Composite',
nl: 'Mine Launcher',
// 'Mining Lance': 'Mining Lance',
ml: 'Mining Laser',
// 'Mirrored Surface Composite': 'Mirrored Surface Composite',
mr: 'Missile Rack',
mc: 'Multi-cannon',
// 'net cost': 'net cost',
// no: 'no',
PHRASE_NO_BUILDS: 'No builds added to comparison!',
PHRASE_NO_RETROCH: 'No Retrofitting changes',
// none: 'none',
// 'none created': 'none created',
// off: 'off',
// on: 'on',
// optimal: 'optimal',
// 'optimal mass': 'optimal mass',
// 'optimize mass': 'optimize mass',
// overwrite: 'overwrite',
// Pacifier: 'Pacifier',
// 'Pack-Hound': 'Pack-Hound',
PHRASE_IMPORT: 'Paste JSON or import here',
// pen: 'pen',
// penetration: 'penetration',
// permalink: 'permalink',
pa: 'Plasma Accelerator',
// 'Point Defence': 'Point Defence',
// power: 'power',
pd: 'power distributor',
pp: 'power plant',
pri: 'pri',
// priority: 'priority',
psg: 'Prismatic Shield Generator',
// proceed: 'proceed',
pc: 'Prospector Limpet Controller',
pl: 'Pulse Laser',
pv: 'Planetary Vehicle Hanger',
// PWR: 'PWR',
rg: 'Rail Gun',
// range: 'range',
// rate: 'rate',
// 'Reactive Surface Composite': 'Reactive Surface Composite',
// recharge: 'recharge',
rf: 'Refinery',
// 'refuel time': 'refuel time',
// 'Reinforced Alloy': 'Reinforced Alloy',
// reload: 'reload',
// rename: 'rename',
// repair: 'repair',
// reset: 'reset',
// ret: 'ret',
// retracted: 'retracted',
// 'retrofit costs': 'retrofit costs',
// 'retrofit from': 'retrofit from',
// ROF: 'ROF',
// S: 'S',
// save: 'save',
sc: 'scanner',
PHRASE_SELECT_BUILDS: 'Select Builds to Compare',
// sell: 'sell',
s: 'sensors',
// settings: 'settings',
sb: 'Shield Booster',
scb: 'Shield Cell Bank',
sg: 'Shield Generator',
// shields: 'shields',
// ship: 'ship',
// ships: 'ships',
// shortened: 'shortened',
// size: 'size',
// skip: 'skip',
// small: 'small',
// speed: 'speed',
// standard: 'standard',
// 'Standard Docking Computer': 'Standard Docking Computer',
// Stock: 'Stock',
// SYS: 'SYS',
// T: 'T',
T_LOAD: 't-load',
// 'The Retributor': 'The Retributor',
t: 'thrusters',
// time: 'time',
tp: 'Torpedo Pylon',
// total: 'total',
// 'total range': 'total range',
// turret: 'turret',
// type: 'type',
// U: 'U',
// unladen: 'unladen',
PHRASE_UPDATE_RDY: 'Update Available! Click to Refresh',
// URL: 'URL',
// utility: 'utility',
// 'utility mounts': 'utility mounts',
// version: 'version',
// WEP: 'WEP',
// yes: 'yes',
PHRASE_BACKUP_DESC: 'Backup of all Coriolis data to save or transfer to another browser/device'
tp: 'Torpedo Pylon'
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,35 @@
import React from 'react';
import { findDOMNode } from 'react-dom';
import Page from './Page';
import Router from '../Router';
import cn from 'classnames';
import { Ships } from 'coriolis-data';
import Ship from '../shipyard/Ship';
import Serializer from '../shipyard/Serializer';
import InterfaceEvents from '../utils/InterfaceEvents';
import { fromComparison, toComparison } from '../shipyard/Serializer';
import Persist from '../stores/Persist';
import { SizeMap, ShipFacets } from '../shipyard/Constants';
import ComparisonTable from '../components/ComparisonTable';
import BarChart from '../components/BarChart';
import ModalCompare from '../components/ModalCompare';
import ModalExport from '../components/ModalExport';
import ModalPermalink from '../components/ModalPermalink';
import { FloppyDisk, Bin, Download, Embed, Rocket, LinkIcon } from '../components/SvgIcons';
import ShortenUrl from '../utils/ShortenUrl';
import { comparisonBBCode } from '../utils/BBCode';
/**
* Creates a comparator based on the specified predicate
* @param {string} predicate Predicate / propterty name
* @return {Function} Comparator
*/
function sortBy(predicate) {
return (a, b) => {
if (a[predicate] === b[predicate]) {
return 0;
if (a.name == b.name) {
a.buildName.toLowerCase() > b.buildName.toLowerCase() ? 1 : -1;
}
return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1;
}
if (typeof a[predicate] == 'string') {
return a[predicate].toLowerCase() > b[predicate].toLowerCase() ? 1 : -1;
@@ -25,23 +38,35 @@ function sortBy(predicate) {
};
}
/**
* Comparison Page
*/
export default class ComparisonPage extends Page {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props, context);
this._sortShips = this._sortShips.bind(this);
this._buildsSelected = this._buildsSelected.bind(this);
this.state = this._initState(props, context);
this.state = this._initState(context);
}
_initState(props, context) {
/**
* [Re]Create initial state from context
* @param {context} context React component context
* @return {Object} New state object
*/
_initState(context) {
let defaultFacets = [9, 6, 4, 1, 3, 2]; // Reverse order of Armour, Shields, Speed, Jump Range, Cargo Capacity, Cost
let params = context.route.params;
let code = params.code;
let name = params.name ? decodeURIComponent(params.name) : null;
let newName = '';
let compareMode = !code;
let facets = [];
let builds = [];
let saved = false;
let predicate = 'name';
@@ -58,7 +83,6 @@ export default class ComparisonPage extends Page {
}
}
} else {
let comparisonData = Persist.getComparison(name);
if (comparisonData) {
defaultFacets = comparisonData.facets;
@@ -69,7 +93,7 @@ export default class ComparisonPage extends Page {
}
} else {
try {
let comparisonData = Serializer.toComparison(code);
let comparisonData = toComparison(code);
defaultFacets = comparisonData.f;
newName = name = comparisonData.n;
predicate = comparisonData.p;
@@ -86,20 +110,22 @@ export default class ComparisonPage extends Page {
}
}
let facets = [];
let selectedLength = defaultFacets.length;
let selectedFacets = new Array(selectedLength);
for (let i = 0; i < ShipFacets.length; i++) {
facets.push(Object.assign({ index: i }, ShipFacets[i]));
}
let selectedFacets = [];
for (let fi of defaultFacets) {
let facet = facets.splice(fi, 1)[0];
facet.active = true;
selectedFacets.unshift(facet);
let facet = Object.assign({ }, ShipFacets[i]);
let defaultIndex = defaultFacets.indexOf(facet.i);
if(defaultIndex == -1) {
facets.push(facet);
} else {
facet.active = true;
selectedFacets[selectedLength - defaultIndex - 1] = facet;
}
}
facets = selectedFacets.concat(facets);
console.log(selectedFacets);
builds.sort(sortBy(predicate));
return {
@@ -116,7 +142,13 @@ console.log(selectedFacets);
importObj
};
}
/**
* Create a Ship instance / build
* @param {string} id Ship Id
* @param {name} name Build name
* @param {string} code Optional - Serialized ship code
* @return {Object} Ship instance with build name
*/
_createBuild(id, name, code) {
code = code ? code : Persist.getBuild(id, name); // Retrieve build code if not passed
@@ -132,8 +164,8 @@ console.log(selectedFacets);
};
/**
* Sort ships
* @param {object} key Sort predicate
* Update state with the specified sort predicates
* @param {String} predicate Sort predicate - property name
*/
_sortShips(predicate) {
let { builds, desc } = this.state;
@@ -150,38 +182,54 @@ console.log(selectedFacets);
this.setState({ predicate, desc });
};
/**
* Show selected builds modal
*/
_selectBuilds() {
InterfaceEvents.showModal(React.cloneElement(
<ModalCompare onSelect={this._buildsSelected}/>,
{ builds: this.state.builds }
));
this.context.showModal(<ModalCompare onSelect={this._buildsSelected} builds={this.state.builds} />);
}
/**
* Update selected builds with new list
* @param {Array} newBuilds List of new builds
*/
_buildsSelected(newBuilds) {
InterfaceEvents.hideModal();
this.context.hideModal();
let builds = [];
for (let b of newBuilds) {
builds.push(this._createBuild(b.id, b.buildName));
}
this.setState({ builds });
this.setState({ builds, saved: false });
}
/**
* Toggle facet display
* @param {string} facet Facet / Ship Property
*/
_toggleFacet(facet) {
facet.active = !facet.active;
this.setState({ facets: [].concat(this.state.facets), saved: false });
}
/**
* Handle facet drag
* @param {Event} e Drag Event
*/
_facetDrag(e) {
this.dragged = e.currentTarget;
let placeholder = this.placeholder = document.createElement("li");
let placeholder = this.placeholder = document.createElement('li');
placeholder.style.width = this.dragged.offsetWidth + 'px';
placeholder.className = "facet-placeholder";
placeholder.className = 'facet-placeholder';
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData("text/html", e.currentTarget);
e.dataTransfer.setData('text/html', e.currentTarget);
}
/**
* Handle facet drop
* @param {Event} e Drop Event
*/
_facetDrop(e) {
this.dragged.parentNode.removeChild(this.placeholder);
let facets = this.state.facets;
@@ -197,18 +245,22 @@ console.log(selectedFacets);
facets.splice(to, 0, facets.splice(frm, 1)[0]);
this.dragged.style.display = null;
this.setState({ facets: [].concat(facets) });
this.setState({ facets: [].concat(facets), saved: false });
}
/**
* Handle facet drag over
* @param {Event} e Drag over Event
*/
_facetDragOver(e) {
e.preventDefault();
if(e.target.className == "facet-placeholder") {
if(e.target.className == 'facet-placeholder') {
return;
}
this.over = e.target;
this.dragged.style.display = "none";
this.dragged.style.display = 'none';
let relX = e.clientX - this.over.getBoundingClientRect().left;
let width = this.over.offsetWidth / 2;
let parent = e.target.parentNode;
@@ -217,71 +269,149 @@ console.log(selectedFacets);
if(relX > width) {
this.nodeAfter = true;
parent.insertBefore(this.placeholder, e.target.nextElementSibling);
}
else {
} else {
this.nodeAfter = false;
parent.insertBefore(this.placeholder, e.target);
}
}
}
/**
* Handle name change and update state
* @param {SyntheticEvent} e Event
*/
_onNameChange(e) {
this.setState({ newName: e.target.value, saved: false });
}
/**
* Delete the current comparison
*/
_delete() {
Persist.deleteComparison(this.state.name);
Router.go('/compare');
}
/**
* Import the comparison builds
*/
_import() {
// TODO: Implement
}
/**
* Save the current comparison
*/
_save() {
let { newName, builds, facets } = this.state;
let selectedFacets = [];
facets.forEach((f) => {
if (f.active) {
selectedFacets.unshift(f.index);
selectedFacets.unshift(f.i);
}
});
console.log(selectedFacets);
//Persist.saveComparison(newName, builds, selectedFacets);
Router.replace(`/compare/${encodeURIComponent(this.state.newName)}`)
this.setState ({ name: newName, saved: true });
Persist.saveComparison(newName, builds, selectedFacets);
Router.replace(`/compare/${encodeURIComponent(this.state.newName)}`);
this.setState({ name: newName, saved: true });
}
/**
* Serialize and generate a long URL for the current comparison
* @return {string} URL for serialized comparison
*/
_buildUrl() {
let { facets, builds, name, predicate, desc } = this.state;
let selectedFacets = [];
for (let f of facets) {
if (f.active) {
selectedFacets.unshift(f.i);
}
}
let code = fromComparison(name, builds, selectedFacets, predicate, desc);
let loc = window.location;
return `${loc.protocol}://${loc.host}/comparison/${code}`;
}
/**
* Generates the long permalink URL
* @return {string} The long permalink URL
*/
_genPermalink() {
let { facets, builds, name, predicate, desc } = this.state;
let selectedFacets = [];
for (let f of facets){
if (f.active) {
selectedFacets.unshift(f.index);
}
}
let code = Serializer.fromComparison(name, builds, selectedFacets, predicate, desc);
// send code to permalink modal
this.context.showModal(<ModalPermalink url={this._buildUrl()}/>);
}
/**
* Generate E:D Forum BBCode and show in the export modal
*/
_genBBcode() {
let { translate, formats } = this.context.language;
let { facets, builds } = this.state;
let generator = (callback) => {
let url = this._buildUrl();
ShortenUrl(url,
(shortenedUrl) => callback(comparisonBBCode(translate, formats, facets, builds, shortenedUrl)),
(error) => callback(comparisonBBCode(translate, formats, facets, builds, url))
);
};
this.context.showModal(<ModalExport
title={translate('forum') + ' BBCode'}
generator={generator}
/>);
}
/**
* Update dimenions from rendered DOM
*/
_updateDimensions() {
this.setState({
chartWidth: findDOMNode(this.refs.chartRef).offsetWidth
});
}
/**
* Update state based on context changes
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextContext Incoming/Next conext
*/
componentWillReceiveProps(nextProps, nextContext) {
if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
this.setState(this._initState(nextProps, nextContext));
this.setState(this._initState(nextContext));
}
}
/**
* Add listeners when about to mount
*/
componentWillMount() {
this.resizeListener = this.context.onWindowResize(this._updateDimensions);
}
/**
* Trigger DOM updates on mount
*/
componentDidMount() {
this._updateDimensions();
}
/**
* Remove listeners on unmount
*/
componentWillUnmount() {
this.resizeListener.remove();
}
/**
* Render the Page
* @return {React.Component} The page contents
*/
render() {
let translate = this.context.language.translate;
let compareHeader;
let {newName, name, saved, builds, facets, predicate, desc } = this.state;
let { newName, name, saved, builds, facets, predicate, desc, chartWidth } = this.state;
if (this.state.compareMode) {
compareHeader = <tr>
@@ -295,10 +425,10 @@ console.log(selectedFacets);
<button onClick={this._selectBuilds}>
<Rocket className='lg'/><span className='button-lbl'>{translate('builds')}</span>
</button>
<button className='r' ng-click='permalink($event)' ng-disabled='builds.length == 0'>
<button className='r' onClick={this._genPermalink} disabled={builds.length == 0}>
<LinkIcon className='lg'/><span className='button-lbl'>{translate('permalink')}</span>
</button>
<button className='r' ng-click='embed($event)' ng-disabled='builds.length == 0'>
<button className='r' onClick={this._genBBcode} disabled={builds.length == 0}>
<Embed className='lg'/><span className='button-lbl'>{translate('forum')}</span>
</button>
</td>
@@ -314,7 +444,7 @@ console.log(selectedFacets);
}
return (
<div className={'page'} style={{ fontSize: this.context.sizeRatio + 'em'}}>
<div className={'page'} style={{ fontSize: this.context.sizeRatio + 'em' }}>
<table id='comparison'>
<tbody>
{compareHeader}
@@ -323,7 +453,7 @@ console.log(selectedFacets);
<td>
<ul id='facet-container' onDragOver={this._facetDragOver}>
{facets.map((f, i) =>
<li key={f.title} data-i={i} draggable='true' onDragStart={this._facetDrag} onDragEnd={this._facetDrop} className={cn('facet', {active: f.active})} onClick={this._toggleFacet.bind(this, f)}>
<li key={f.title} data-i={i} draggable='true' onDragStart={this._facetDrag} onDragEnd={this._facetDrop} className={cn('facet', { active: f.active })} onClick={this._toggleFacet.bind(this, f)}>
{'↔ ' + translate(f.title)}
</li>
)}
@@ -335,9 +465,23 @@ console.log(selectedFacets);
<ComparisonTable builds={builds} facets={facets} onSort={this._sortShips} predicate={predicate} desc={desc} />
{/*<div ng-repeat='f in facets | filter:{active:true}' ng-if='builds.length > 0' className='chart' bar-chart facet='f' data='builds'>
<h3 ng-click='sort(f.props[0])' >{{f.title | translate}}</h3>
</div>*/}
{!builds.length ?
<div className='chart' ref={'chartRef'}>{translate('PHRASE_NO_BUILDS')}</div> :
facets.filter((f) => f.active).map((f, i) =>
<div key={f.title} className='chart' ref={ i == 0 ? 'chartRef' : null}>
<h3 className='ptr' onClick={this._sortShips.bind(this, f.props[0])}>{translate(f.title)}</h3>
<BarChart
width={chartWidth}
data={builds}
properties={f.props}
unit={translate(f.unit)}
format={f.fmt}
label={translate(f.title)}
predicate={predicate}
desc={desc}
/>
</div>
)}
</div>
);

View File

@@ -1,8 +1,16 @@
import React from 'react';
import Page from './Page';
/**
* Unexpected Error page
* TODO: Implement properly and test
*/
export default class ErrorPage extends Page {
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
this.state = {
@@ -10,22 +18,26 @@ export default class ErrorPage extends Page {
};
}
/**
* Render the Page
* @return {React.Component} The page contents
*/
render() {
let msgPre, msgHighlight, msgPost, errorMessage, details;
let msgPre, msgHighlight, msgPost, errorMessage, details, type;
switch ($scope.type) {
switch (type) {
case 404:
msgPre = 'Page';
msgHighlight = this.context.route.path;
msgPost = 'Not Found';
msgPre = 'Page';
msgHighlight = this.context.route.path;
msgPost = 'Not Found';
break;
case 'no-ship':
msgPre = 'Ship';
msgHighlight = this.props.message;
msgPost = 'does not exist';
msgPre = 'Ship';
msgHighlight = this.props.message;
msgPost = 'does not exist';
break;
case 'build':
msgPre = 'Build Failure!';
msgPre = 'Build Failure!';
break;
default:
msgPre = 'Uh, Jameson, we have a problem..';
@@ -38,9 +50,9 @@ export default class ErrorPage extends Page {
return <div className='error'>
<h1>
<span>{{msgPre}}</span>
<small>{{msgHighlight}}</small>
<span>{{msgPost}}</span>
<span>{msgPre}</span>
<small>{msgHighlight}</small>
<span>{msgPost}</span>
</h1>
<div style={{ textAlign:'left', fontSize:'0.8em', width: '43em', margin: '0 auto' }}>
@@ -48,7 +60,7 @@ export default class ErrorPage extends Page {
<a href='https://github.com/cmmcleod/coriolis/issues' target='_blank' title='Coriolis Github Project'>Create an issue on Github</a>
if this keeps happening. Add these details:
</div>
<div style={{marginTop: '2em'}}>
<div style={{ marginTop: '2em' }}>
<div>Browser: {window.navigator.userAgent}</div>
<div>Path: {this.context.route.canonicalPath}</div>
<div>Error:<br/>{this.props.type || 'Unknown'}</div>

View File

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

View File

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

View File

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

View File

@@ -7,22 +7,38 @@ import * as ModuleUtils from '../shipyard/ModuleUtils';
import { SizeMap } from '../shipyard/Constants';
import Link from '../components/Link';
/**
* Counts the hardpoints by class/size
* @param {Object} slot Hardpoint Slot model
*/
function countHp(slot) {
this.hp[slot.maxClass]++;
this.hpCount++;
}
/**
* Counts the internal slots and aggregated properties
* @param {Object} slot Internal Slots
*/
function countInt(slot) {
var crEligible = !slot.eligible || slot.eligible.cr;
let crEligible = !slot.eligible || slot.eligible.cr;
this.int[slot.maxClass - 1]++; // Subtract 1 since there is no Class 0 Internal compartment
this.intCount++;
this.maxCargo += crEligible ? ModuleUtils.findInternal('cr', slot.maxClass, 'E').capacity : 0;
this.maxCargo += crEligible ? ModuleUtils.findInternal('cr', slot.maxClass, 'E').cargo : 0;
}
let cachedShipSummaries = null;
/**
* The Shipyard summary page
*/
export default class ShipyardPage extends Page {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props, context);
this.state = {
@@ -43,8 +59,9 @@ export default class ShipyardPage extends Page {
}
/**
* Sort ships
* @param {object} key Sort predicate
* Update state with the specified sort predicates
* @param {String} shipPredicate Sort predicate - property name
* @param {number} shipPredicateIndex Sort predicate - property index
*/
_sortShips(shipPredicate, shipPredicateIndex) {
let shipDesc = this.state.shipDesc;
@@ -60,6 +77,12 @@ export default class ShipyardPage extends Page {
this.setState({ shipPredicate, shipDesc, shipPredicateIndex });
};
/**
* Generate Ship summary and aggregated properties
* @param {String} shipId Ship Id
* @param {Object} shipData Ship Default Data
* @return {Object} Ship summary and aggregated properties
*/
_shipSummary(shipId, shipData) {
let summary = {
id: shipId,
@@ -86,6 +109,15 @@ export default class ShipyardPage extends Page {
return summary;
}
/**
* Generate the table row summary for the ship
* @param {Object} s Ship summary
* @param {Function} translate Translate function
* @param {Object} u Localized unit map
* @param {Function} fInt Localized integer formatter
* @param {Function} fRound Localized round formatter
* @return {React.Component} Table Row
*/
_shipRowElement(s, translate, u, fInt, fRound) {
return <tr key={s.id} className='highlight'>
<td className='le'><Link href={'/outfit/' + s.id}>{s.name}</Link></td>
@@ -114,11 +146,15 @@ export default class ShipyardPage extends Page {
<td className={cn({ disabled: !s.int[6] })}>{s.int[6]}</td>
<td className={cn({ disabled: !s.int[7] })}>{s.int[7]}</td>
<td className='ri'>{fInt(s.hullMass)}{u.T}</td>
<td className='ri'>{s.masslock}</td>
<td>{s.masslock}</td>
<td className='ri'>{fInt(s.retailCost)}{u.CR}</td>
</tr>;
}
/**
* Render the Page
* @return {React.Component} The page contents
*/
render() {
let { translate, formats, units } = this.context.language;
let fInt = formats.int;
@@ -127,6 +163,8 @@ export default class ShipyardPage extends Page {
let shipPredicate = this.state.shipPredicate;
let shipPredicateIndex = this.state.shipPredicateIndex;
let shipRows = [];
let hide = this.context.tooltip.bind(null, null);
let tip = this.context.termtip;
let sortShips = (predicate, index) => this._sortShips.bind(this, predicate, index);
// Sort shipsOverview
@@ -170,13 +208,13 @@ export default class ShipyardPage extends Page {
<th rowSpan={2} className='sortable le' onClick={sortShips('name')}>{translate('ship')}</th>
<th rowSpan={2} className='sortable' onClick={sortShips('manufacturer')}>{translate('manufacturer')}</th>
<th rowSpan={2} className='sortable' onClick={sortShips('class')}>{translate('size')}</th>
<th rowSpan={2} className='sortable' onClick={sortShips('agility')}>{translate('agility')}</th>
<th rowSpan={2} className='sortable' onMouseEnter={tip.bind(null, 'maneuverability')} onMouseLeave={hide} onClick={sortShips('agility')}>{translate('mnv')}</th>
<th colSpan={4}>{translate('base')}</th>
<th colSpan={4}>{translate('max')}</th>
<th colSpan={5} className='sortable' onClick={sortShips('hpCount')}>{translate('hardpoints')}</th>
<th colSpan={8} className='sortable' onClick={sortShips('intCount')}>{translate('internal compartments')}</th>
<th rowSpan={2} className='sortable' onClick={sortShips('hullMass')}>{translate('hull')}</th>
<th rowSpan={2} className='sortable' onClick={sortShips('masslock')}>{translate('MLF')}</th>
<th rowSpan={2} className='sortable' onMouseEnter={tip.bind(null, 'mass lock factor')} onMouseLeave={hide} onClick={sortShips('masslock')} >{translate('MLF')}</th>
<th rowSpan={2} className='sortable' onClick={sortShips('retailCost')}>{translate('cost')}</th>
</tr>
<tr>

View File

@@ -4,11 +4,11 @@
*
* @param {number} mass Mass of a ship: laden, unlanden, partially laden, etc
* @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass
* @param {number} fuel Optional - The fuel consumed during the jump (must be less than the drives max fuel per jump)
* @param {number} fuel Optional - The fuel consumed during the jump
* @return {number} Distance in Light Years
*/
export function jumpRange(mass, fsd, fuel) {
return Math.pow(Math.min(fuel === undefined ? fsd.maxfuel : fuel, fsd.maxfuel) / fsd.fuelmul, 1 / fsd.fuelpower ) * fsd.optmass / mass;
return Math.pow(Math.min(fuel === undefined ? fsd.maxfuel : fuel, fsd.maxfuel) / fsd.fuelmul, 1 / fsd.fuelpower) * fsd.optmass / mass;
}
/**
@@ -20,15 +20,15 @@ export function jumpRange(mass, fsd, fuel) {
* @return {number} Distance in Light Years
*/
export function totalRange(mass, fsd, fuel) {
var fuelRemaining = fuel % fsd.maxfuel; // Fuel left after making N max jumps
var jumps = Math.floor(fuel / fsd.maxfuel);
let fuelRemaining = fuel % fsd.maxfuel; // Fuel left after making N max jumps
let jumps = Math.floor(fuel / fsd.maxfuel);
mass += fuelRemaining;
// Going backwards, start with the last jump using the remaining fuel
var totalRange = fuelRemaining > 0 ? Math.pow(fuelRemaining / fsd.fuelmul, 1 / fsd.fuelpower ) * fsd.optmass / mass : 0;
let totalRange = fuelRemaining > 0 ? Math.pow(fuelRemaining / fsd.fuelmul, 1 / fsd.fuelpower) * fsd.optmass / mass : 0;
// For each max fuel jump, calculate the max jump range based on fuel mass left in the tank
for (var j = 0; j < jumps; j++) {
for (let j = 0; j < jumps; j++) {
mass += fsd.maxfuel;
totalRange += Math.pow(fsd.maxfuel / fsd.fuelmul, 1 / fsd.fuelpower ) * fsd.optmass / mass;
totalRange += Math.pow(fsd.maxfuel / fsd.fuelmul, 1 / fsd.fuelpower) * fsd.optmass / mass;
}
return totalRange;
};
@@ -43,7 +43,7 @@ export function totalRange(mass, fsd, fuel) {
* @return {number} Approximate shield strengh in MJ
*/
export function shieldStrength(mass, shields, sg, multiplier) {
var opt;
let opt;
if (mass < sg.minmass) {
return shields * multiplier * sg.minmul;
}
@@ -57,7 +57,7 @@ export function shieldStrength(mass, shields, sg, multiplier) {
} else {
opt = (sg.optmass - mass) / (sg.maxmass - sg.optmass);
opt = -1 + Math.pow(1 + opt, 2.425);
return shields * multiplier * ( (-1 * opt * sg.maxmul) + ((1 + opt) * sg.optmul) );
return shields * multiplier * ((-1 * opt * sg.maxmul) + ((1 + opt) * sg.optmul));
}
}
@@ -72,8 +72,8 @@ export function shieldStrength(mass, shields, sg, multiplier) {
* @return {object} Approximate speed by pips
*/
export function speed(mass, baseSpeed, baseBoost, thrusters, pipSpeed) {
var multiplier = mass > thrusters.maxmass ? 0 : ((1 - thrusters.M) + (thrusters.M * Math.pow(3 - (2 * Math.max(0.5, mass / thrusters.optmass)), thrusters.P)));
var speed = baseSpeed * multiplier;
let multiplier = mass > thrusters.maxmass ? 0 : ((1 - thrusters.M) + (thrusters.M * Math.pow(3 - (2 * Math.max(0.5, mass / thrusters.optmass)), thrusters.P)));
let speed = baseSpeed * multiplier;
return {
'0 Pips': speed * (1 - (pipSpeed * 4)),

View File

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

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

View File

@@ -2,17 +2,27 @@ import { ModuleNameToGroup, BulkheadNames } from './Constants';
import ModuleSet from './ModuleSet';
import { Ships, Modules } from 'coriolis-data';
/**
* Created a cargo hatch model
* @return {Object} Cargo hatch model
*/
export function cargoHatch() {
return { name: 'Cargo Hatch', class: 1, rating: 'H', power: 0.6 };
};
/**
* Finds the standard module type with the specified ID
* @param {number} typeIndex Standard Module Type (0 - Power Plant, 1 - Thrusters, etc)
* @param {string} id The module ID or '[Class][Rating]'
* @return {Object} The standard module or null
*/
export function standard(typeIndex, id) {
let standard = Modules.standard[typeIndex];
if (standard[id]) {
return standard[id];
} else {
for (let k in standard) {
if (standard[k].id == id){
if (standard[k].id == id) {
return standard[k];
}
}
@@ -20,6 +30,11 @@ export function standard(typeIndex, id) {
return null;
};
/**
* Finds the hardpoint with the specified ID
* @param {string} id Hardpoint ID
* @return {Object} Hardpoint module or null
*/
export function hardpoints(id) {
for (let n in Modules.hardpoints) {
let group = Modules.hardpoints[n];
@@ -32,6 +47,11 @@ export function hardpoints(id) {
return null;
};
/**
* Finds the internal module with the specified ID
* @param {string} id Internal module ID
* @return {Object} Internal module or null
*/
export function internal(id) {
for (let n in Modules.internal) {
let group = Modules.internal[n];
@@ -45,14 +65,14 @@ export function internal(id) {
};
/**
* Finds an internal Component based on Class, Rating, Group and/or name.
* At least one ofGroup name or unique component name must be provided
* Finds an internal module based on Class, Rating, Group and/or name.
* At least one ofGroup name or unique module name must be provided
*
* @param {string} groupName [Optional] Full name or abbreviated name for component group
* @param {integer} clss Component Class
* @param {string} rating Component Rating
* @param {string} name [Optional] Long/unique name for component -e.g. 'Advanced Discover Scanner'
* @return {String} The id of the component if found, null if not found
* @param {string} groupName [Optional] Full name or abbreviated name for module group
* @param {integer} clss module Class
* @param {string} rating module Rating
* @param {string} name [Optional] Long/unique name for module -e.g. 'Advanced Discover Scanner'
* @return {String} The id of the module if found, null if not found
*/
export function findInternal(groupName, clss, rating, name) {
let groups = {};
@@ -61,7 +81,7 @@ export function findInternal(groupName, clss, rating, name) {
if (Modules.internal[groupName]) {
groups[groupName] = Modules.internal[groupName];
} else {
let grpCode = ModuleNameToGroup[groupName];
let grpCode = ModuleNameToGroup[groupName.toLowerCase()];
if (grpCode && Modules.internal[grpCode]) {
groups[grpCode] = Modules.internal[grpCode];
}
@@ -83,14 +103,14 @@ export function findInternal(groupName, clss, rating, name) {
}
/**
* Finds an internal Component ID based on Class, Rating, Group and/or name.
* At least one ofGroup name or unique component name must be provided
* Finds an internal Module ID based on Class, Rating, Group and/or name.
* At least one ofGroup name or unique module name must be provided
*
* @param {string} groupName [Optional] Full name or abbreviated name for component group
* @param {integer} clss Component Class
* @param {string} rating Component Rating
* @param {string} name [Optional] Long/unique name for component -e.g. 'Advanced Discover Scanner'
* @return {String} The id of the component if found, null if not found
* @param {string} groupName [Optional] Full name or abbreviated name for module group
* @param {integer} clss module Class
* @param {string} rating Module Rating
* @param {string} name [Optional] Long/unique name for module -e.g. 'Advanced Discover Scanner'
* @return {String} The id of the module if found, null if not found
*/
export function findInternalId(groupName, clss, rating, name) {
let i = this.findInternal(groupName, clss, rating, name);
@@ -98,16 +118,16 @@ export function findInternalId(groupName, clss, rating, name) {
}
/**
* Finds a hardpoint Component based on Class, Rating, Group and/or name.
* At least one ofGroup name or unique component name must be provided
* Finds a hardpoint Module based on Class, Rating, Group and/or name.
* At least one ofGroup name or unique module name must be provided
*
* @param {string} groupName [Optional] Full name or abbreviated name for component group
* @param {integer} clss Component Class
* @param {string} rating [Optional] Component Rating
* @param {string} name [Optional] Long/unique name for component -e.g. 'Heat Sink Launcher'
* @param {string} groupName [Optional] Full name or abbreviated name for module group
* @param {integer} clss Module Class
* @param {string} rating [Optional] module Rating
* @param {string} name [Optional] Long/unique name for module -e.g. 'Heat Sink Launcher'
* @param {string} mount Mount type - [F]ixed, [G]imballed, [T]urret
* @param {string} missile [Optional] Missile type - [D]umbfire, [S]eeker
* @return {String} The id of the component if found, null if not found
* @return {String} The id of the module if found, null if not found
*/
export function findHardpoint(groupName, clss, rating, name, mount, missile) {
let groups = {};
@@ -116,7 +136,7 @@ export function findHardpoint(groupName, clss, rating, name, mount, missile) {
if (Modules.hardpoints[groupName]) {
groups[groupName] = Modules.hardpoints[groupName];
} else {
let grpCode = ModuleNameToGroup[groupName];
let grpCode = ModuleNameToGroup[groupName.toLowerCase()];
if (grpCode && Modules.hardpoints[grpCode]) {
groups[grpCode] = Modules.hardpoints[grpCode];
}
@@ -127,12 +147,9 @@ export function findHardpoint(groupName, clss, rating, name, mount, missile) {
for (let g in groups) {
let group = groups[g];
for (let i = 0, l = group.length; i < l; i++) {
if (group[i].class == clss && (!rating || group[i].rating == rating) && group[i].mount == mount
&& ((!name && !group[i].name) || group[i].name == name)
&& ((!missile && !group[i].missile) || group[i].missile == missile)
) {
return group[i];
for (let h of group) {
if (h.class == clss && (!rating || h.rating == rating) && h.mount == mount && h.name == name && h.missile == missile) {
return h;
}
}
}
@@ -141,16 +158,16 @@ export function findHardpoint(groupName, clss, rating, name, mount, missile) {
}
/**
* Finds a hardpoint Component ID based on Class, Rating, Group and/or name.
* At least one of Group name or unique component name must be provided
* Finds a hardpoint module ID based on Class, Rating, Group and/or name.
* At least one of Group name or unique module name must be provided
*
* @param {string} groupName [Optional] Full name or abbreviated name for component group
* @param {integer} clss Component Class
* @param {string} rating Component Rating
* @param {string} name [Optional] Long/unique name for component -e.g. 'Heat Sink Launcher'
* @param {string} groupName [Optional] Full name or abbreviated name for module group
* @param {integer} clss module Class
* @param {string} rating module Rating
* @param {string} name [Optional] Long/unique name for module -e.g. 'Heat Sink Launcher'
* @param {string} mount Mount type - [F]ixed, [G]imballed, [T]urret
* @param {string} missile [Optional] Missile type - [D]umbfire, [S]eeker
* @return {String} The id of the component if found, null if not found
* @return {String} The id of the module if found, null if not found
*/
export function findHardpointId(groupName, clss, rating, name, mount, missile) {
let h = this.findHardpoint(groupName, clss, rating, name, mount, missile);
@@ -159,33 +176,44 @@ export function findHardpointId(groupName, clss, rating, name, mount, missile) {
/**
* Looks up the bulkhead module for a specific ship and bulkhead
* @param {string} shipId Unique ship Id/Key
* @param {string|number} bulkheadsId Id/Index for the specified bulkhead
* @return {object} The bulkhead component object
* @param {string} shipId Unique ship Id/Key
* @param {string|number} index Index for the specified bulkhead
* @return {Object} The bulkhead module object
*/
export function bulkheads(shipId, index) {
let bulkhead = Ships[shipId].bulkheads[index];
bulkhead.class = 1;
bulkhead.rating = 'I';
bulkhead.name = BulkheadNames[index]
bulkhead.name = BulkheadNames[index];
return bulkhead;
}
/**
* Get the bulkhead index for the given bulkhead name
* @param {string} bulkheadName Bulkhead name in english
* @return {number} Bulkhead index
*/
export function bulkheadIndex(bulkheadName) {
return BulkheadNames.indexOf(bulkheadName);
}
/**
* Determine if a module group is a shield generator
* @param {string} g Module Group name
* @return {Boolean} True if the group is a shield generator
*/
export function isShieldGenerator(g) {
return g == 'sg' || g == 'psg' || g == 'bsg';
}
/**
* Creates a new ModuleSet that contains all available components
* Creates a new ModuleSet that contains all available modules
* that the specified ship is eligible to use.
*
* @param {string} shipId Unique ship Id/Key
* @return {ModuleSet} The set of components the ship can install
* @return {ModuleSet} The set of modules the ship can install
*/
export function forShip(shipId) {
let ship = Ships[shipId];

View File

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

View File

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

View File

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

View File

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

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) {
if (window.navigator.onLine) {
request.post(SHORTEN_API + window.CORIOLIS_GAPI_KEY)
.send({ longUrl: url })
.end(function(err, response) {
if (err) {
error('Error');
} else {
success(response.data.id);
}
});
try {
request.post(SHORTEN_API + window.CORIOLIS_GAPI_KEY)
.send({ longUrl: url })
.end(function(err, response) {
if (err) {
error(response.statusText == 'OK' ? 'Bad Request' : response.statusText);
} else {
success(response.body.id);
}
});
} catch (e) {
error(e.message ? e.message : e);
}
} else {
error('Not Online');
}

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
@@ -11,27 +14,191 @@ export function slotName(translate, slot) {
}
/**
* Generates an internationalization friendly slot name comparator
* @param {function} translate Tranlation function
* @return {function} Comparator function for slot names
* Slot name comparator
* @param {Function} translate Translate function
* @param {Object} a Slot object
* @param {Object} b Slot object
* @return {number} 1, 0, -1
*/
export function nameComparator(translate) {
return (a, b) => {
a = a.m;
b = b.m;
export function nameComparator(translate, a, b) {
return translate(a.name || a.grp).localeCompare(translate(b.name || b.grp));
}
if (a && !b) {
return 1;
} else if (!a && b) {
return -1;
} else if (!a && !b) {
return 0;
} else if (a.name === b.name && a.grp === b.grp) {
if(a.class == b.class) {
return a.rating > b.rating ? 1 : -1;
}
return a.class - b.class;
/**
* Generates an internationalization friendly slot comparator that will
* sort by specified property (if provided) then by name/group, class, rating
* @param {function} translate Tranlation function
* @param {function} propComparator Optional property comparator
* @param {boolean} desc Use descending order
* @return {function} Comparator function for slot names
*/
export function slotComparator(translate, propComparator, desc) {
return (a, b) => {
// retain descending order when sorting sorting by name/group/class/rating
let am = a.m; // Slot A's mounted module
let bm = b.m; // Slot B's mounted module
if (!desc) { // Flip A and B if ascending order
let t = a;
a = b;
b = t;
}
return translate(a.name || a.grp).localeCompare(translate(b.name || b.grp));
// Check for empty slots first
if (a.m && !b.m) {
return 1;
} else if (!a.m && b.m) {
return -1;
} else if (!a.m && !b.m) {
return 0;
}
// If a property comparator is provided use it first
let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a.m, b.m);
if (diff) {
return diff;
}
// Property matches so sort by name / group, then class, rating
if (am.name === bm.name && am.grp === bm.grp) {
if(am.class == bm.class) {
return am.rating > bm.rating ? 1 : -1;
}
return am.class - bm.class;
}
return nameComparator(translate, am, bm);
};
}
const PROP_BLACKLIST = {
eddbID: 1,
'class': 1,
id: 1,
maxfuel: 1,
fuelmul: 1,
fuelpower: 1,
optmass: 1,
maxmass: 1,
passive: 1,
thermload: 1,
ammocost: 1,
activepower: 1,
cooldown: 1,
chargeup: 1,
ssdam: 1,
mjdps: 1,
mjeps: 1,
M: 1,
P: 1,
mass: 1,
cost: 1
};
const TERM_LOOKUP = {
pGen: 'power',
armouradd: 'armour',
shieldmul: 'shield',
rof: 'ROF',
dps: 'DPS'
};
const FORMAT_LOOKUP = {
time: 'time',
shieldmul: 'rPct'
};
const UNIT_LOOKUP = {
fuel: 'T',
cargo: 'T',
rate: 'kgs',
range: 'km',
recharge: 'MJ',
rangeLS: 'Ls',
power: 'MJ',
pGen: 'MJ',
rof: 'ps'
};
/**
* Determine the appropriate class based on diff value
* @param {number} a Potential Module (cannot be null)
* @param {number} b Currently mounted module (optional - null)
* @param {boolean} negative A positive diff has a negative implication
* @return {string} CSS Class name
*/
function diffClass(a, b, negative) {
if (b === undefined || a === b) {
return 'muted';
} else if (a > b) {
return negative ? 'warning' : 'secondary';
}
return negative ? 'secondary' : 'warning';
}
/**
* Determine the displayable diff of a module's proprty
* @param {Function} format Formatter
* @param {number} mVal Potential Module property value
* @param {number} mmVal Currently mounted module property value
* @return {string | React.Component} Component to be rendered
*/
function diff(format, mVal, mmVal) {
if (mVal == Infinity) {
return <Infinite/>;
} else {
let diff = mVal - mmVal;
if (!diff || diff == mVal || Math.abs(diff) == Infinity) {
return format(mVal);
}
return `${format(mVal)} (${diff > 0 ? '+' : ''}${format(diff)})`;
}
}
/**
* Returns a a summary and diff of the potential module
* versus the currently mounted module if there is one
* @param {Object} language Current language
* @param {Object} m Potential Module (cannot be null)
* @param {Object} mm Currently mounted module (optional - null)
* @return {React.Component} Component to be rendered
*/
export function diffDetails(language, m, mm) {
mm = mm || {};
let { formats, translate, units } = language;
let propDiffs = [];
let mMass = m.mass || 0;
let mmMass = mm.mass || 0;
let massDiff = mMass - mmMass;
let capDiff = (m.fuel || m.cargo || 0) - (mm.fuel || mm.cargo || 0);
propDiffs.push(<div key='cost'>{translate('cost')}: <span className={diffClass(m.cost, mm.cost, true) }>{formats.int(m.cost || 0)}{units.CR}</span></div>);
propDiffs.push(<div key='mass'>{translate('mass')}: <span className={diffClass(mMass, mm.mass, true)}>{diff(formats.round, mMass, mmMass)}{units.T}</span></div>);
for (let p in m) {
if (!PROP_BLACKLIST[p] && !isNaN(m[p])) {
let mVal = m[p] || Infinity;
let mmVal = mm[p] === null ? Infinity : mm[p];
let format = formats[FORMAT_LOOKUP[p]] || formats.round;
propDiffs.push(<div key={p}>
{`${translate(TERM_LOOKUP[p] || p)}: `}
<span className={diffClass(mVal, mmVal, p == 'power')}>{diff(format, mVal, mmVal)}{units[UNIT_LOOKUP[p]]}</span>
</div>);
}
}
if (m.grp == 'fd' || massDiff || capDiff) {
let fsd = m.grp == 'fd' ? m : null;
let maxRange = this.getUnladenRange(massDiff, m.fuel, fsd);
let ladenRange = this.getLadenRange(massDiff + capDiff, m.fuel, fsd);
if (maxRange != this.unladenRange) {
propDiffs.push(<div key='maxRange'>{`${translate('max')} ${translate('jump range')}: `}<span className={maxRange > this.unladenRange ? 'secondary' : 'warning'}>{formats.round(maxRange)}{units.LY}</span></div>);
}
if (ladenRange != this.ladenRange) {
propDiffs.push(<div key='unladenRange'>{`${translate('laden')} ${translate('jump range')}: `}<span className={ladenRange > this.ladenRange ? 'secondary' : 'warning'}>{formats.round(ladenRange)}{units.LY}</span></div>);
}
}
return <div className='cap' style={{ whiteSpace: 'nowrap' }}>{propDiffs}</div>;
}

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 (===)
* @param {any} objA A
* @param {any} objB B
* @return {boolean} true if A === B OR A properties === B properties
*/
export default function shallowEqual(objA, objB) {
export function shallowEqual(objA, objB) {
if (objA === objB) {
return true;
}

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.app.entry %}" charset="utf-8" crossorigin="anonymous"></script>
{% if (o.htmlWebpackPlugin.options.uaTracking) { %}
<script>
<script>
{% if (o.htmlWebpackPlugin.options.uaTracking) { %}
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{%= o.htmlWebpackPlugin.options.uaTracking %}', 'auto');
window.CORIOLIS_GAPI_KEY = '{%= o.htmlWebpackPlugin.options.gapiKey %}';
</script>
{% } %}
{% } %}
window.CORIOLIS_GAPI_KEY = '{%= o.htmlWebpackPlugin.options.gapiKey %}';
</script>
</body>
</html>

View File

@@ -13,7 +13,7 @@
@import 'select';
@import 'modal';
@import 'charts';
@import 'chart-tooltip';
@import 'tooltip';
@import 'buttons';
@import 'error';
@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;
}
.muted {
color: @muted;
fill: @muted;
}
.disabled {
color: @disabled;
fill: @disabled;

View File

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

View File

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

View File

@@ -14,7 +14,7 @@
color: @fg;
fill: @fg;
.details {
.details-container {
min-height: 2.7em;
padding: 0.25em 0;
box-sizing: border-box;
@@ -24,6 +24,11 @@
text-transform: none;
}
.details {
min-height: 2.2em;
background-color: @primary-bg;
}
.name {
overflow: hidden;
white-space: nowrap;
@@ -75,5 +80,58 @@
background-color: @primary-bg;
border-right: 1px solid @primary;
}
.details {
background-color: transparent;
}
}
&.eligible {
border: 1px solid @secondary-disabled;
.sz {
color: @secondary-disabled;
border-right: 1px solid @secondary-disabled;
}
.details {
background-color: transparent;
}
}
&.ineligible {
cursor: no-drop;
color: @disabled;
fill: @disabled;
border: 1px solid @disabled;
.sz {
color: @disabled;
border-right: 1px solid @disabled;
}
}
&.dropEmpty {
color: @warning-disabled;
fill: @warning-disabled;
border: 1px solid @warning-disabled;
.sz {
color: @warning-disabled;
border-right: 1px solid @warning-disabled;
}
.details {
background-color: transparent;
}
}
&.drop {
color: @secondary-bg;
fill: @secondary-bg;
border: 1px solid @secondary;
background-color: @secondary-disabled;
.sz {
color: @secondary;
background-color: @primary-bg;
border-right: 1px solid @secondary;
}
.details {
background-color: transparent;
}
}
}

68
src/less/tooltip.less Executable file
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;
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({
inject: false,
template: path.join(__dirname, "src/index.html"),
version: pkgJson.version
version: pkgJson.version,
gapiKey: process.env.CORIOLIS_GAPI_KEY || '',
}),
new ExtractTextPlugin('app.css', {
allChunks: true