More refactoring and porting to React

This commit is contained in:
Colin McLeod
2015-12-13 11:51:58 -08:00
parent 035f6b3efa
commit ab0019424f
58 changed files with 2243 additions and 2507 deletions

View File

@@ -1,21 +1,52 @@
{
"parser": "babel-eslint",
"ecmaFeatures": {
"jsx": true,
"classes": true,
"modules": true
},
"env": {
"browser": true,
"node": true
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"strict": 0,
"no-underscore-dangle": 0,
"valid-jsdoc": [2, {
"requireReturn": false
}],
"require-jsdoc": [2, {
"require": {
"FunctionDeclaration": true,
"MethodDefinition": true,
"ClassDeclaration": true
}
}],
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
"comma-style": [2, "last"],
"indent": [2, 2, { "SwitchCase": 1, "VariableDeclarator": 2 }],
"quotes": [2, "single"],
"strict": [2, "never"],
"no-spaced-func": 2,
"operator-linebreak": [2, "after"],
"padded-blocks": [2, "never"],
"semi": [2, "always"],
"no-undef": 2,
"semi-spacing": [2, { "before": false, "after": true }],
"space-before-blocks": [2, "always"],
"space-before-function-paren": [2, "never"],
"object-curly-spacing": [2, "always"],
"array-bracket-spacing": [2, "never"],
"computed-property-spacing": [2, "never"],
"space-in-parens": [2, "never"],
"space-infix-ops": 2,
"spaced-comment": [2, "always"],
"no-var": 2,
"object-shorthand": [2, "always"],
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/react-in-jsx-scope": 2
},
"plugins": [
"react"
]
}
}

View File

@@ -5,7 +5,12 @@ var config = require('./webpack.config.dev');
new WebpackDevServer(webpack(config), {
publicPath: config.output.publicPath,
hot: true,
historyApiFallback: true
historyApiFallback: {
rewrites: [
// For some reason connect-history-api-fallback does not allow '.' in the URL for history fallback...
{ from: /\/outfit\//, to: '/index.html' }
]
}
}).listen(3300, "0.0.0.0", function (err, result) {
if (err) {
console.log(err);

View File

@@ -13,9 +13,10 @@
"scripts": {
"clean": "rimraf build",
"start": "node devServer.js",
"lint": "eslint ./src",
"lint": "eslint --ext .js,.jsx src",
"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"
@@ -23,13 +24,15 @@
"devDependencies": {
"appcache-webpack-plugin": "^1.2.1",
"babel-core": "^5.4.7",
"babel-eslint": "^3.1.9",
"babel-eslint": "^4.1.6",
"babel-loader": "^5.1.2",
"babel-plugin-react-transform": "^1.1.1",
"css-loader": "^0.23.0",
"eslint": "^1.3.1",
"eslint": "^1.10.1",
"eslint-plugin-react": "^2.3.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",
"json-loader": "^0.5.3",
@@ -47,7 +50,6 @@
"dependencies": {
"classnames": "^2.2.0",
"d3": "^3.5.9",
"extract-text-webpack-plugin": "^0.9.1",
"fbemitter": "^2.0.0",
"lz-string": "^1.4.4",
"react": "^0.14.2",

View File

@@ -20,6 +20,12 @@ export default class Coriolis extends React.Component {
constructor() {
super();
this._setPage = this._setPage.bind(this);
this._openMenu = this._openMenu.bind(this);
this._closeMenu = this._closeMenu.bind(this);
this._showModal = this._showModal.bind(this);
this._hideModal = this._hideModal.bind(this);
this._onLanguageChange = this._onLanguageChange.bind(this)
this._keyDown = this._keyDown.bind(this);
this.state = {
page: null,
@@ -28,22 +34,20 @@ export default class Coriolis extends React.Component {
};
Router('', (r) => this._setPage(ShipyardPage, r));
// Router('/', (ctx) => this._setPage(ShipyardPage, ctx));
Router('/outfitting/:ship', (r) => this._setPage(OutfittingPage, r));
Router('/outfitting/:ship/:code', (r) => this._setPage(OutfittingPage, r));
Router('/outfit/:ship/:code?', (r) => this._setPage(OutfittingPage, r));
// Router('/compare/:name', compare);
// Router('/comparison/:code', comparison);
// Router('/settings', settings);
Router('/about', (r) => this._setPage(AboutPage, r));
Router('*', (r) => this._setPage(null, r));
}
_setPage(page, route) {
this.setState({ page, route });
this.setState({ page, route, currentMenu: null });
}
_onError(msg, scriptUrl, line, col, errObj) {
this._setPage(<div>Some errors occured!!</div>);
console.log('WINDOW ERROR', arguments);
//this._setPage(<div>Some errors occured!!</div>);
}
_onLanguageChange(lang) {
@@ -53,8 +57,8 @@ export default class Coriolis extends React.Component {
_keyDown(e) {
switch (e.keyCode) {
case 27:
InterfaceEvents.closeAll();
this._hideModal();
this._closeMenu();
break;
}
}
@@ -70,6 +74,18 @@ export default class Coriolis extends React.Component {
}
}
_openMenu(currentMenu) {
if (this.state.currentMenu != currentMenu) {
this.setState({ currentMenu });
}
}
_closeMenu() {
if (this.state.currentMenu) {
this.setState({ currentMenu: null });
}
}
getChildContext() {
return {
language: this.state.language,
@@ -82,28 +98,30 @@ export default class Coriolis extends React.Component {
if (window.applicationCache) {
window.applicationCache.addEventListener('updateready', () => {
if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
this.setState({appCacheUpdate: true}); // Browser downloaded a new app cache.
this.setState({ appCacheUpdate: true }); // Browser downloaded a new app cache.
}
}, false);
});
}
window.onerror = this._onError.bind(this);
document.addEventListener('keydown', this._keyDown.bind(this));
Persist.addListener('language', this._onLanguageChange.bind(this));
Persist.addListener('language', this._onLanguageChange.bind(this));
InterfaceEvents.addListener('showModal', this._showModal.bind(this));
InterfaceEvents.addListener('hideModal', this._hideModal.bind(this));
window.addEventListener('resize', InterfaceEvents.windowResized);
document.addEventListener('keydown', this._keyDown);
Persist.addListener('language', this._onLanguageChange);
Persist.addListener('language', this._onLanguageChange);
InterfaceEvents.addListener('openMenu', this._openMenu);
InterfaceEvents.addListener('closeMenu', this._closeMenu);
InterfaceEvents.addListener('showModal', this._showModal);
InterfaceEvents.addListener('hideModal', this._hideModal);
Router.start();
}
render() {
return (
<div onClick={InterfaceEvents.closeAll}>
<Header appCacheUpdate={this.state.appCacheUpdate} />
{this.state.page? <this.state.page /> : <NotFoundPage/>}
{this.state.modal}
<div onClick={this._closeMenu}>
<Header appCacheUpdate={this.state.appCacheUpdate} currentMenu={this.state.currentMenu} />
{ this.state.page ? <this.state.page currentMenu={this.state.currentMenu} /> : <NotFoundPage/> }
{ this.state.modal }
</div>
);
}

View File

@@ -1,5 +1,4 @@
import Persist from './stores/Persist';
import InterfaceEvents from './utils/InterfaceEvents';
function isStandAlone() {
try {
@@ -49,7 +48,7 @@ Router.start = function(){
Router('/');
}
} else {
var url = location.pathname + location.search + location.hash;
var url = location.pathname + location.search;
Router.replace(url, null, true, true);
}
};
@@ -64,10 +63,11 @@ Router.start = function(){
*/
Router.go = function(path, state) {
gaTrack(path);
InterfaceEvents.closeAll();
var ctx = new Context(path, state);
Router.dispatch(ctx);
if (!ctx.unhandled) ctx.pushState();
if (!ctx.unhandled) {
history.pushState(ctx.state, ctx.title, ctx.canonicalPath);
}
return ctx;
};
@@ -80,12 +80,11 @@ Router.go = function(path, state) {
* @api public
*/
Router.replace = function(path, state, init, dispatch) {
Router.replace = function(path, state, dispatch) {
gaTrack(path);
var ctx = new Context(path, state);
ctx.init = init;
if (dispatch !== false) Router.dispatch(ctx);
ctx.save();
if (dispatch) Router.dispatch(ctx);
history.replaceState(ctx.state, ctx.title, ctx.canonicalPath);
return ctx;
};
@@ -119,8 +118,10 @@ Router.dispatch = function(ctx){
function unhandled(ctx) {
var current = window.location.pathname + window.location.search;
if (current == ctx.canonicalPath) return;
if (current != ctx.canonicalPath) {
window.location = ctx.canonicalPath;
}
return ctx;
}
/**
@@ -142,43 +143,14 @@ function Context(path, state) {
this.state.path = path;
this.querystring = ~i ? path.slice(i + 1) : '';
this.pathname = ~i ? path.slice(0, i) : path;
this.params = [];
this.params = {};
// fragment
this.hash = '';
if (!~this.path.indexOf('#')) return;
var parts = this.path.split('#');
this.path = parts[0];
this.hash = parts[1] || '';
this.querystring = this.querystring.split('#')[0];
this.querystring.split('&').forEach((str) =>{
let query = str.split('=');
this.params[query[0]] = decodeURIComponent(query[1]);
}, this);
}
/**
* Expose `Context`.
*/
Router.Context = Context;
/**
* Push state.
*
* @api private
*/
Context.prototype.pushState = function(){
history.pushState(this.state, this.title, this.canonicalPath);
};
/**
* Save the context state.
*
* @api public
*/
Context.prototype.save = function(){
history.replaceState(this.state, this.title, this.canonicalPath);
};
/**
* Initialize `Route` with the given HTTP `path`,
* and an array of `callbacks` and `options`.
@@ -203,12 +175,6 @@ function Route(path, options) {
, options.strict);
}
/**
* Expose `Route`.
*/
Router.Route = Route;
/**
* Return route middleware with
* the given callback `fn()`.
@@ -247,22 +213,33 @@ Route.prototype.match = function(path, params){
for (var i = 1, len = m.length; i < len; ++i) {
var key = keys[i - 1];
var val = 'string' == typeof m[i]
? decodeURIComponent(m[i])
: m[i];
var val = 'string' == typeof m[i] ? decodeURIComponent(m[i]) : m[i];
if (key) {
params[key.name] = undefined !== params[key.name]
? params[key.name]
: val;
} else {
params.push(val);
params[key.name] = undefined !== params[key.name] ? params[key.name] : val;
}
}
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
*/
function gaTrack(path) {
if (window.ga) {
window.ga('send', 'pageview', { page: path });
@@ -314,7 +291,7 @@ function pathtoRegexp(path, keys, sensitive, strict) {
function onpopstate(e) {
if (e.state) {
var path = e.state.path;
Router.replace(path, e.state);
Router.replace(path, e.state, true);
}
}

View File

@@ -31,13 +31,13 @@ export default class AvailableModulesMenu extends TranslatedComponent {
disabled: m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass
});
switch(m.mode) {
switch(m.mount) {
case 'F': mount = <MountFixed className={'lg'} />; break;
case 'G': mount = <MountGimballed className={'lg'}/>; break;
case 'T': mount = <MountTurret className={'lg'}/>; break;
}
if (i > 0 && modules.length > 3 && m.class != prevClass && (m.rating != prevRating || m.mode) && m.grp != 'pa') {
if (i > 0 && modules.length > 3 && m.class != prevClass && (m.rating != prevRating || m.mount) && m.grp != 'pa') {
elems.push(<br key={m.grp + i} />);
}
@@ -77,15 +77,12 @@ export default class AvailableModulesMenu extends TranslatedComponent {
);
if (modules instanceof Array) {
console.log(modules[0].grp, modules);
list = buildGroup(modules[0].grp, modules);
} else {
console.log('menu object')
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) {
let grp = modules[g];
list.push(<div ref={g} key={g} className={'select-group cap'}>{translate(g)}</div>);
list.push(buildGroup(g, modules[g]));
}

View File

@@ -0,0 +1,435 @@
import React from 'react';
import cn from 'classnames';
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 TranslatedComponent from './TranslatedComponent';
export default class CostSection extends TranslatedComponent {
static PropTypes = {
ship: React.PropTypes.object.isRequired,
shipId: React.PropTypes.string.isRequired,
code: React.PropTypes.string.isRequired,
buildName: React.PropTypes.string
};
constructor(props) {
super(props);
this._costsTab = this._costsTab.bind(this);
let data = Ships[props.shipId]; // Retrieve the basic ship properties, slots and defaults
let retrofitName = props.buildName;
let shipDiscount = Persist.getShipDiscount();
let moduleDiscount = Persist.getModuleDiscount();
let existingBuild = Persist.getBuild(props.shipId, retrofitName);
let retrofitShip = new Ship(props.shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison
if (existingBuild) {
retrofitShip.buildFrom(existingBuild); // Populate modules from existing build
} else {
retrofitShip.buildWith(data.defaults); // Populate with default components
}
this.props.ship.applyDiscounts(shipDiscount, moduleDiscount);
retrofitShip.applyDiscounts(shipDiscount, moduleDiscount);
this.state = {
retrofitShip,
retrofitName,
shipDiscount,
moduleDiscount,
total: props.ship.totalCost,
insurance: Insurance[Persist.getInsurance()],
tab: Persist.getCostTab(),
buildOptions: Persist.getBuildsNamesFor(props.shipId),
ammoPredicate: 'module',
ammoDesc: true,
costPredicate: 'cr',
costDesc: true,
retroPredicate: 'module',
retroDesc: true
};
}
_showTab(tab) {
Persist.setCostTab(tab);
this.setState({ tab });
}
_onDiscountChanged() {
let shipDiscount = Persist.getShipDiscount();
let moduleDiscount = Persist.getModuleDiscount();
this.props.ship.applyDiscounts(shipDiscount, moduleDiscount);
this.state.retrofitShip.applyDiscounts(shipDiscount, moduleDiscount);
this.setState({ shipDiscount, moduleDiscount });
}
_onInsuranceChanged(insuranceName) {
this.setState({ insurance: Insurance[insuranceName] });
}
_onBaseRetrofitChange(retrofitName) {
let existingBuild = Persist.getBuild(this.props.shipId, retrofitName);
this.state.retrofitShip.buildFrom(existingBuild); // Repopulate modules from existing build
this.setState({ retrofitName });
}
_onBuildSaved(shipId, name, code) {
if(this.state.retrofitName == name) {
this.state.retrofitShip.buildFrom(code); // Repopulate modules from saved build
} else {
this.setState({buildOptions: Persist.getBuildsNamesFor(this.props.shipId) });
}
}
_toggleCost(item) {
this.props.ship.setCostIncluded(item, !item.incCost);
this.setState({ total: this.props.ship.totalCost });
}
_sortCost(predicate) {
let costList = this.props.ship.costList;
let { costPredicate, costDesc } = this.state;
if (predicate) {
if (costPredicate == predicate) {
costDesc = !costDesc;
}
} else {
predicate = costPredicate;
}
if (predicate == 'm') {
let translate = this.context.language.translate;
costList.sort(nameComparator(translate));
} else {
costList.sort((a, b) => (a.m && a.m.cost ? a.m.cost : 0) - (b.m && b.m.cost ? b.m.cost : 0));
}
if (!costDesc) {
costList.reverse();
}
this.setState({ costPredicate: predicate, costDesc });
}
_sortAmmo(predicate) {
let { ammoPredicate, ammoDesc, ammoCosts } = this.state;
if (ammoPredicate == predicate) {
ammoDesc = !ammoDesc;
}
switch (predicate) {
case 'm':
let translate = this.context.language.translate;
ammoCosts.sort(nameComparator(translate));
break;
default:
ammoCosts.sort((a, b) => a[predicate] - b[predicate]);
}
if (!ammoDesc) {
ammoCosts.reverse();
}
this.setState({ ammoPredicate: predicate, ammoDesc });
}
_costsTab() {
let { ship } = this.props;
let { total, shipDiscount, moduleDiscount, insurance } = this.state;
let { translate, formats, units } = this.context.language;
let rows = [];
for (let i = 0, l = ship.costList.length; i < l; i++) {
let item = ship.costList[i];
if (item.m && item.m.cost) {
let toggle = this._toggleCost.bind(this, item);
rows.push(<tr key={i} className={cn('highlight', { disabled: !item.incCost })}>
<td className='ptr' style={{ width: '1em' }} onClick={toggle}>{item.m.class + item.m.rating}</td>
<td className='le ptr shorten cap' onClick={toggle}>{slotName(translate, item)}</td>
<td className='ri ptr' onClick={toggle}>{formats.int(item.discountedCost)}{units.CR}</td>
</tr>);
}
}
return <div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr className='main'>
<th colSpan='2' className='sortable le' onClick={this._sortCost.bind(this,'m')}>
{translate('component')}
{shipDiscount < 1 && <u className='optional-hide'>{`[${translate('ship')} -${formats.rPct(1 - shipDiscount)}]`}</u>}
{moduleDiscount < 1 && <u className='optional-hide'>{`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}</u>}
</th>
<th className='sortable le' onClick={this._sortCost.bind(this, 'cr')} >{translate('credits')}</th>
</tr>
</thead>
<tbody>
{rows}
<tr className='ri'>
<td colSpan='2' className='lbl' >{translate('total')}</td>
<td className='val'>{formats.int(total)}{units.CR}</td>
</tr>
<tr className='ri'>
<td colSpan='2' className='lbl'>{translate('insurance')}</td>
<td className='val'>{formats.int(total * insurance)}{units.CR}</td>
</tr>
</tbody>
</table>
</div>;
}
updateRetrofitCosts() {
var costs = $scope.retrofitList = [];
var total = 0, i, l, item;
if (ship.bulkheads.id != retrofitShip.bulkheads.id) {
item = {
buyClassRating: ship.bulkheads.m.class + ship.bulkheads.m.rating,
buyName: ship.bulkheads.m.name,
sellClassRating: retrofitShip.bulkheads.m.class + retrofitShip.bulkheads.m.rating,
sellName: retrofitShip.bulkheads.m.name,
netCost: ship.bulkheads.discountedCost - retrofitShip.bulkheads.discountedCost,
retroItem: retrofitShip.bulkheads
};
costs.push(item);
if (retrofitShip.bulkheads.incCost) {
total += item.netCost;
}
}
for (var g in { standard: 1, internal: 1, hardpoints: 1 }) {
var retroSlotGroup = retrofitShip[g];
var slotGroup = ship[g];
for (i = 0, l = slotGroup.length; i < l; i++) {
if (slotGroup[i].id != retroSlotGroup[i].id) {
item = { netCost: 0, retroItem: retroSlotGroup[i] };
if (slotGroup[i].id) {
item.buyName = slotGroup[i].m.name || slotGroup[i].m.grp;
item.buyClassRating = slotGroup[i].m.class + slotGroup[i].m.rating;
item.netCost = slotGroup[i].discountedCost;
}
if (retroSlotGroup[i].id) {
item.sellName = retroSlotGroup[i].m.name || retroSlotGroup[i].m.grp;
item.sellClassRating = retroSlotGroup[i].m.class + retroSlotGroup[i].m.rating;
item.netCost -= retroSlotGroup[i].discountedCost;
}
costs.push(item);
if (retroSlotGroup[i].incCost) {
total += item.netCost;
}
}
}
}
$scope.retrofitTotal = total;
}
_retrofitTab() {
// return <div>
// <div className='scroll-x'>
// <table style='width:100%'>
// <thead>
// <tr className='main'>
// <th colspan='2' className='sortable le' ng-click='sortRetrofit('sellName | translate')' >{translate('sell')}</th>
// <th colspan='2' className='sortable le' ng-click='sortRetrofit('buyName | translate')' >{translate('buy')}</th>
// <th className='sortable le' ng-click='sortRetrofit('netCost')'>
// {{'net cost' | translate}} <u className='optional-hide' ng-if='discounts.components < 1'>[-{{fRPct(1 - discounts.components)}}]</u>
// </th>
// </tr>
// </thead>
// <tbody>
// <tr ng-if='!retrofitList || retrofitList.length == 0'>
// <td colspan='5' style='padding: 3em 0;' >{translate('PHRASE_NO_RETROCH')}</td>
// </tr>
// <tr className='highlight' ng-repeat='item in retrofitList | orderBy:retroPredicate:retroDesc' ng-click='toggleRetrofitCost(item.retroItem)' className={cn({disabled: !item.retroItem.incCost})}>
// <td style='width:1em;'>{{item.sellClassRating}}</td>
// <td className='le shorten cap'>{{item.sellName | translate}}</td>
// <td style='width:1em;'>{{item.buyClassRating}}</td>
// <td className='le shorten cap'>{{item.buyName | translate}}</td>
// <td className={cn('ri', item.retroItem.incCost ? item.netCost > 0 ? 'warning' : 'secondary-disabled' : 'disabled' )}>{{ fCrd(item.netCost)}} <u translate>CR</u></td>
// </tr>
// </tbody>
// </table>
// </div>
// <table className='total'>
// <tr className='ri'>
// <td className='lbl' >{translate('cost')}</td>
// <td colSpan={2} className={retrofitTotal > 0 ? 'warning' : 'secondary-disabled'}>{{fCrd(retrofitTotal)}} <u translate>CR</u></td>
// </tr>
// <tr className='ri'>
// <td className='lbl cap' >{translate('retrofit from')}</td>
// <td className='cen' style='border-right:none;width: 1em;'><u className='primary-disabled'>&#9662;</u></td>
// <td style='border-left:none;padding:0;'>
// <select style='width: 100%;padding: 0' ng-model='$parent.retrofitBuild' ng-change='setRetrofitBase()' ng-options='name as name for (name, build) in allBuilds[ship.id]'>
// <option value=''>{{'Stock' | translate}}</option>
// </select>
// </td>
// </tr>
// </table>
// </div>;
}
_ammoTab() {
let { ammoTotal, ammoCosts } = this.state;
let { translate, formats, units } = this.context.language;
let int = formats.int;
let rows = [];
for (let i = 0, l = ammoCosts.length; i < l; i++) {
let item = ammoCosts[i];
rows.push(<tr key={i} className='highlight'>
<td style={{ width: '1em' }}>{item.m.class + item.m.rating}</td>
<td className='le shorten cap'>{slotName(translate, item)}</td>
<td className='ri'>{int(item.max)}</td>
<td className='ri'>{int(item.cost)}{units.CR}</td>
<td className='ri'>{int(item.total)}{units.CR}</td>
</tr>);
}
console.log(rows);
return <div>
<div className='scroll-x' >
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr className='main'>
<th colSpan='2' className='sortable le' onClick={this._sortAmmo.bind(this, 'm')} >{translate('module')}</th>
<th colSpan='1' className='sortable le' onClick={this._sortAmmo.bind(this, 'max')} >{translate('qty')}</th>
<th colSpan='1' className='sortable le' onClick={this._sortAmmo.bind(this, 'cost')} >{translate('unit cost')}</th>
<th className='sortable le' onClick={this._sortAmmo.bind(this, 'total')}>{translate('total cost')}</th>
</tr>
</thead>
<tbody>
{rows}
<tr className='ri'>
<td colSpan='4' className='lbl' >{translate('total')}</td>
<td className='val'>{int(ammoTotal)}{units.CR}</td>
</tr>
</tbody>
</table>
</div>
</div>;
}
/**
* Recalculate all ammo costs
*/
_updateAmmoCosts() {
let ship = this.props.ship;
let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, scoop = false;
for (let g in { standard: 1, internal: 1, hardpoints: 1 }) {
let slotGroup = ship[g];
for (let i = 0, l = slotGroup.length; i < l; i++) {
if (slotGroup[i].id) {
//special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip
q = 0;
switch (slotGroup[i].m.grp) {
case 'fs': //skip fuel calculation if scoop present
scoop = true;
break;
case 'scb':
q = slotGroup[i].m.cells;
break;
case 'am':
q = slotGroup[i].m.ammo;
break;
case 'fx': case 'hb': case 'cc': case 'pc':
limpets = ship.cargoCapacity;
break;
default:
q = slotGroup[i].m.clip + slotGroup[i].m.ammo;
}
//calculate ammo costs only if a cost is specified
if (slotGroup[i].m.ammocost > 0) {
item = {
m: slotGroup[i].m,
max: q,
cost: slotGroup[i].m.ammocost,
total: q * slotGroup[i].m.ammocost
};
ammoCosts.push(item);
ammoTotal += item.total;
}
}
}
}
//limpets if controllers exist and cargo space available
if (limpets > 0) {
item = {
m: { name: 'limpets', class: '', rating: '' },
max: ship.cargoCapacity,
cost: 101,
total: ship.cargoCapacity * 101
};
ammoCosts.push(item);
ammoTotal += item.total;
}
//calculate refuel costs if no scoop present
if (!scoop) {
item = {
m: { name: 'fuel', class: '', rating: '' },
max: ship.fuelCapacity,
cost: 50,
total: ship.fuelCapacity * 50
};
ammoCosts.push(item);
ammoTotal += item.total;
}
this.setState({ ammoTotal, ammoCosts });
}
componentWillMount(){
this.listeners = [
Persist.addListener('discounts', this._onDiscountChanged.bind(this)),
Persist.addListener('insurance', this._onInsuranceChanged.bind(this)),
];
this._updateAmmoCosts(this.props.ship);
this._sortCost.call(this, this.state.costPredicate);
}
componentWillReceiveProps(nextProps, nextContext) {
this._updateAmmoCosts(nextProps.ship);
this._sortCost();
}
componentWillUnmount(){
// remove window listener
this.listeners.forEach(l => l.remove());
}
render() {
let tab = this.state.tab;
let translate = this.context.language.translate;
let tabSection;
switch (tab) {
case 'ammo': tabSection = this._ammoTab(); break;
case 'retrofit': tabSection = this._retrofitTab(); break;
default:
tab = 'costs';
tabSection = this._costsTab();
}
return (
<div className='group half'>
<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>
</tr>
</thead>
</table>
{tabSection}
</div>
);
}
}

View File

@@ -3,17 +3,17 @@ import Slot from './Slot';
export default class HardpointSlot extends Slot {
getClassNames() {
return this.props.size > 0 ? 'hardpoint' : null;
_getClassNames() {
return this.props.maxClass > 0 ? 'hardpoint' : null;
}
getSize(translate){
return translate(['U','S','M','L','H'][this.props.size]);
_getMaxClassLabel(translate){
return translate(['U','S','M','L','H'][this.props.maxClass]);
}
getSlotDetails(m, translate, formats, u) {
_getSlotDetails(m, translate, formats, u) {
if (m) {
let classRating = `${m.class}${m.rating}${m.mode ? '/' + m.mode : ''}${m.missile ? m.missile : ''}`;
let classRating = `${m.class}${m.rating}${m.mount ? '/' + m.mount : ''}${m.missile ? m.missile : ''}`;
return (
<div>
<div className={'l'}>{classRating + ' ' + translate(m.name || m.grp)}</div>

View File

@@ -13,27 +13,35 @@ export default class HardpointsSlotSection extends SlotSection {
}
_empty() {
this.props.ship.emptyWeapons();
this.props.onChange();
this._close();
}
_fill(grp, mount) {
_fill(group, mount, event) {
this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt'));
this.props.onChange();
this._close();
}
_contextMenu() {
this._empty();
}
_getSlots() {
let slots = [];
let hardpoints = this.props.ship.hardpoints;
let availableModules = this.props.ship.getAvailableModules();
let currentMenu = this.state.currentMenu;
let currentMenu = this.props.currentMenu;
for (let i = 0, l = hardpoints.length; i < l; i++) {
let h = hardpoints[i];
if (h.maxClass) {
slots.push(<HardpointSlot
key={i}
size={h.maxClass}
modules={availableModules.getHps(h.maxClass)}
onOpen={this._openMenu.bind(this,h)}
maxClass={h.maxClass}
availableModules={() => availableModules.getHps(h.maxClass)}
onOpen={this._openMenu.bind(this, h)}
onSelect={this._selectModule.bind(this, h)}
selected={currentMenu == h}
m={h.m}

View File

@@ -4,7 +4,7 @@ import Link from './Link';
import ActiveLink from './ActiveLink';
import cn from 'classnames';
import { Cogs, CoriolisLogo, Hammer, Rocket, StatsBars } from './SvgIcons';
import Ships from '../shipyard/Ships';
import { Ships } from 'coriolis-data';
import InterfaceEvents from '../utils/InterfaceEvents';
import Persist from '../stores/Persist';
import ModalDeleteAll from './ModalDeleteAll';
@@ -13,21 +13,12 @@ export default class Header extends TranslatedComponent {
constructor(props) {
super(props);
this.state = {
openedMenu: null
};
this._close = this._close.bind(this);
this.shipOrder = Object.keys(Ships).sort();
}
_close() {
this.setState({ openedMenu: null });
}
_setInsurance(e) {
e.stopPropagation();
Persist.setInsurance('Beta'); // TODO: get insurance name
Persist.setInsurance('beta'); // TODO: get insurance name
}
_setModuleDiscount(e) {
@@ -43,7 +34,6 @@ export default class Header extends TranslatedComponent {
_showDeleteAll(e) {
e.preventDefault();
InterfaceEvents.showModal(<ModalDeleteAll />);
this._close();
};
_showBackup(e) {
@@ -76,21 +66,21 @@ export default class Header extends TranslatedComponent {
Persist.setSizeRatio(1);
}
_openMenu(event, openedMenu) {
_openMenu(event, menu) {
event.stopPropagation();
if (this.state.openedMenu == openedMenu) {
openedMenu = null;
if (this.props.currentMenu == menu) {
menu = null;
}
this.setState({ openedMenu });
InterfaceEvents.openMenu(menu);
}
_getShipsMenu() {
let shipList = [];
for (let s in Ships) {
shipList.push(<ActiveLink key={s} href={'/outfitting/' + s} className={'block'}>{Ships[s].properties.name}</ActiveLink>);
shipList.push(<ActiveLink key={s} href={'/outfit/' + s} className={'block'}>{Ships[s].properties.name}</ActiveLink>);
}
return (
@@ -108,7 +98,7 @@ export default class Header extends TranslatedComponent {
let shipBuilds = [];
let buildNameOrder = Object.keys(builds[shipId]).sort();
for (let buildName of buildNameOrder) {
let href = ['/outfitting/', shipId, '/', builds[shipId][buildName], '?bn=', buildName].join('');
let href = ['/outfit/', shipId, '/', builds[shipId][buildName], '?bn=', buildName].join('');
shipBuilds.push(<li key={shipId + '-' + buildName} ><ActiveLink href={href} className={'block'}>{buildName}</ActiveLink></li>);
}
buildList.push(<ul key={shipId}>{Ships[shipId].properties.name}{shipBuilds}</ul>);
@@ -196,21 +186,13 @@ export default class Header extends TranslatedComponent {
);
}
componentWillMount() {
this.closeAllListener = InterfaceEvents.addListener('closeAll', this._close);
}
componentWillUnmount() {
this.closeAllListener.remove();
}
render() {
let translate = this.context.language.translate;
let openedMenu = this.state.openedMenu;
let openedMenu = this.props.currentMenu;
let hasBuilds = Persist.hasBuilds();
if (this.props.appCacheUpdate) {
return <div id="app-update" onClick={window.location.reload}>{ 'PHRASE_UPDATE_RDY' | translate }</div>;
return <div id="app-update" onClick={() => window.location.reload() }>{translate('PHRASE_UPDATE_RDY')}</div>;
}
return (

View File

@@ -4,7 +4,7 @@ import { Infinite } from './SvgIcons';
export default class InternalSlot extends Slot {
getSlotDetails(m, translate, formats, u) {
_getSlotDetails(m, translate, formats, u) {
if (m) {
let classRating = m.class + m.rating;

View File

@@ -1,7 +1,8 @@
import React from 'react';
import cn from 'classnames';
import SlotSection from './SlotSection';
import InternalSlot from './InternalSlot';
import cn from 'classnames';
import * as ModuleUtils from '../shipyard/ModuleUtils';
export default class InternalSlotSection extends SlotSection {
@@ -16,36 +17,70 @@ export default class InternalSlotSection extends SlotSection {
}
_empty() {
this.props.ship.emptyInternal();
this.props.onChange();
this._close();
}
_fillWithCargo() {
_fillWithCargo(event) {
let clobber = event.getModifierState('Alt');
let ship = this.props.ship;
ship.internal.forEach((slot) => {
if (clobber || !slot.m) {
ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E'));
}
});
this.props.onChange();
this._close();
}
_fillWithCells() {
_fillWithCells(event) {
let clobber = event.getModifierState('Alt');
let ship = this.props.ship;
let chargeCap = 0; // Capacity of single activation
ship.internal.forEach(function(slot) {
if ((!slot.m || (clobber && !ModuleUtils.isShieldGenerator(slot.m.grp))) && (!slot.eligible || slot.eligible.scb)) { // Check eligibility due to Orca special case
ship.use(slot, ModuleUtils.findInternal('scb', slot.maxClass, 'A'));
ship.setSlotEnabled(slot, chargeCap <= ship.shieldStrength); // Don't waste cell capacity on overcharge
chargeCap += slot.m.recharge;
}
});
this.props.onChange();
this._close();
}
_fillWithArmor() {
_fillWithArmor(event) {
let clobber = event.getModifierState('Alt');
let ship = this.props.ship;
ship.internal.forEach((slot) => {
if (clobber || !slot.c) {
ship.use(slot, ModuleUtils.findInternal('hr', Math.min(slot.maxClass, 5), 'D')); // Hull reinforcements top out at 5D
}
});
this.props.onChange();
this._close();
}
_contextMenu() {
this._empty();
}
_getSlots() {
let slots = [];
let {internal, fuelCapacity, ladenMass } = this.props.ship;
let availableModules = this.props.ship.getAvailableModules();
let currentMenu = this.state.currentMenu;
let { currentMenu, ship } = this.props;
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}
size={s.maxClass}
modules={availableModules.getInts(s.maxClass, s.eligible)}
maxClass={s.maxClass}
availableModules={() => availableModules.getInts(s.maxClass, s.eligible)}
onOpen={this._openMenu.bind(this,s)}
onSelect={this._selectModule.bind(this, i, s)}
onSelect={this._selectModule.bind(this, s)}
selected={currentMenu == s}
enabled={s.enabled}
m={s.m}
fuel={fuelCapacity}
shipMass={ladenMass}

View File

@@ -0,0 +1,51 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
const RENDER_POINTS = 20; // Only render 20 points on the graph
export default class LineChart extends TranslatedComponent {
static defaultProps = {
xMin: 0,
yMin: 0,
colors: ['#ff8c0d']
}
static PropTypes = {
xMax: React.PropTypes.number.isRequired,
yMax: React.PropTypes.number.isRequired,
func: React.PropTypes.func.isRequired,
series: React.PropTypes.array,
colors: React.PropTypes.array,
xMin: React.PropTypes.number,
yMin: React.PropTypes.number,
xUnit: React.PropTypes.string,
yUnit: React.PropTypes.string,
xLabel: React.PropTypes.string,
xLabel: React.PropTypes.string,
};
constructor(props) {
super(props);
// init
}
componentWillMount(){
// Listen to window resize
}
componentWillUnmount(){
// remove window listener
// remove mouse move listener / touch listner?
}
componentWillReceiveProps(nextProps, nextContext) {
// on language change update formatting
}
render() {
return <div></div>;
}
}

View File

@@ -2,12 +2,12 @@ import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents';
export default class DeleteAllModal extends TranslatedComponent {
export default class ModalExport extends TranslatedComponent {
static propTypes = {
title: React.propTypes.string,
promise: : React.propTypes.func,
data: React.propTypes.oneOfType[React.propTypes.string, React.propTypes.object]
title: React.PropTypes.string,
promise: React.PropTypes.func,
data: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object])
};
constructor(props) {
@@ -19,7 +19,7 @@ export default class DeleteAllModal extends TranslatedComponent {
} else if(typeof props.data == 'string') {
exportJson = props.data;
} else {
exportJson = JSON.stringify(this.props.data);
exportJson = JSON.stringify(this.props.data, null, 2);
}
this.state = { exportJson };
@@ -41,7 +41,7 @@ export default class DeleteAllModal extends TranslatedComponent {
<h2>{translate(this.props.title || 'Export')}</h2>
{description}
<div>
<textarea className='cb json' onFocus={ (e) => e.target.select() }>{this.state.exportJson}</textarea>
<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>
</div>;

View File

@@ -65,7 +65,7 @@ export default class ModalImport extends TranslatedComponent {
static propTypes = {
title: React.propTypes.string,
promise: : React.propTypes.func,
promise: React.propTypes.func,
data: React.propTypes.oneOfType[React.propTypes.string, React.propTypes.object]
};
@@ -322,7 +322,7 @@ export default class ModalImport extends TranslatedComponent {
}
if (this.state.discounts) {
Persist.setDiscount((this.state.discounts);
Persist.setDiscount(this.state.discounts);
}
if (this.state.insurance) {
@@ -369,6 +369,7 @@ export default class ModalImport extends TranslatedComponent {
</td>
</tr>
);
}
comparisonTable = (
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%'}} >
@@ -397,7 +398,7 @@ export default class ModalImport extends TranslatedComponent {
let hasBuild = Persist.hasBuild(shipId, b.useName);
buildRows.push(
<tr className='cb'>
<td>{{Ships[shipId].properties.name}}</td>
<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>

View File

@@ -30,10 +30,10 @@ 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} 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} size={25} onFocus={ (e) => e.target.select() }/>
<br/><br/>
<button className={'r dismiss cap'} onClick={InterfaceEvents.hideModal}>{translate('close')}</button>
</div>;

View File

@@ -0,0 +1,244 @@
import React from 'react';
import { findDOMNode } from 'react-dom';
import d3 from 'd3';
import cn from 'classnames';
import Persist from '../stores/Persist';
import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents';
import { wrapCtxMenu } from '../utils/InterfaceEvents';
/**
* Round to avoid floating point precision errors
* @param {[type]} selected [description]
* @param {[type]} sum [description]
* @param {[type]} avail [description]
* @return {[type]} [description]
*/
function getClass(selected, sum, avail) {
return selected ? 'secondary' : ((Math.round(sum * 100) / 100) >= avail) ? 'warning' : 'primary';
}
function bandText(val, index, wattScale) {
return (val > 0 && wattScale(val) > 13) ? index + 1 : null;
}
export default class PowerBands extends TranslatedComponent {
static propTypes = {
bands: React.PropTypes.array.isRequired,
available: React.PropTypes.number.isRequired,
code: React.PropTypes.string
};
constructor(props, context) {
super(props);
this.wattScale = d3.scale.linear();
this.pctScale = d3.scale.linear().domain([0, 1]);
this.wattAxis = d3.svg.axis().scale(this.wattScale).outerTickSize(0).orient('top').tickFormat(context.language.formats.r2);
this.pctAxis = d3.svg.axis().scale(this.pctScale).outerTickSize(0).orient('bottom').tickFormat(context.language.formats.rPct);
this._updateWidth = this._updateWidth.bind(this);
this._updateAxes = this._updateAxes.bind(this);
this._selectNone = this._selectNone.bind(this);
let maxBand = props.bands[props.bands.length - 1];
this.state = {
outerWidth: 0,
innerWidth: 0,
sizes: this._initSizes(Persist.getSizeRatio()),
maxPwr: Math.max(props.available, maxBand.retractedSum, maxBand.deployedSum),
ret: {},
dep: {}
};
}
_initSizes(size) {
let barHeight = Math.round(20 * size);
let innerHeight = (barHeight * 2) + 2;
let mTop = Math.round(25 * size);
let mBottom = Math.round(25 * size);
return {
barHeight,
innerHeight,
mTop,
mBottom,
mLeft: Math.round(45 * size),
mRight: Math.round(130 * size),
height: innerHeight + mTop + mBottom,
retY: (barHeight / 2),
depY: (barHeight * 1.5) - 1
};
}
_selectNone() {
this.setState({
ret : {},
dep: {}
});
}
_selectRet(index) {
let ret = this.state.ret;
if(ret[index]) {
delete ret[index];
} else {
ret[index] = 1;
}
this.setState({ ret: Object.assign({}, ret) });
}
_selectDep(index) {
let dep = this.state.dep;
if(dep[index]) {
delete dep[index];
} else {
dep[index] = 1;
}
this.setState({ dep: Object.assign({}, dep) });
}
_updateAxes(innerWidth, maxPwr, available) {
this.wattScale.range([0, innerWidth]).domain([0,maxPwr]).clamp(true);
this.pctScale.range([0, innerWidth]).domain([0, maxPwr / available]).clamp(true);
}
_updateWidth() {
let outerWidth = findDOMNode(this).offsetWidth;
let innerWidth = outerWidth - this.state.sizes.mLeft - this.state.sizes.mRight;
this._updateAxes(innerWidth, this.state.maxPwr, this.props.available);
this.setState({ outerWidth, innerWidth });
}
componentWillMount(){
this.resizeListener = InterfaceEvents.addListener('windowResized', this._updateWidth);
this.sizeListener = Persist.addListener('sizeRatio', (sizeRatio) => this.setState({ sizes: this._initSizes(sizeRatio) }));
}
componentDidMount() {
this._updateWidth();
}
componentWillReceiveProps(nextProps, nextContext) {
let { innerWidth, maxPwr } = this.state;
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._updateAxes(innerWidth, nextMaxPwr, nextProps.available);
this.setState({ maxPwr: nextMaxPwr });
}
if (this.context !== nextContext) {
this.wattAxis.tickFormat(nextContext.language.formats.r2);
this.pctAxis.tickFormat(nextContext.language.formats.rPct);
}
}
componentWillUnmount(){
this.resizeListener.remove();
this.sizeListener.remove();
}
render() {
let { wattScale, pctScale, context, props, state } = this;
let { translate, formats } = context.language;
let { f2, pct1, rPct, r2 } = formats; // wattFmt, pctFmt, pctAxis, wattAxis
let { available, bands } = props;
let { sizes, outerWidth, innerWidth, maxPwr, ret, dep } = state;
let pwrWarningClass = cn('threshold', {exceeded: bands[0].retractedSum * 2 >= available });
let deployed = [];
let retracted = [];
let retSelected = Object.keys(ret).length > 0;
let depSelected = Object.keys(dep).length > 0;
let retSum = 0;
let depSum = 0;
if (outerWidth > 0) {
for (var i = 0; i < bands.length; i++) {
let b = bands[i];
retSum += (!retSelected || ret[i]) ? b.retracted : 0;
depSum += (!depSelected || dep[i]) ? b.deployed + b.retracted : 0;
if (outerWidth && b.retracted > 0) {
let retLbl = bandText(b.retracted, i, wattScale);
retracted.push(<rect
key={'rB' + i}
width={Math.ceil(Math.max(wattScale(b.retracted), 0))}
height={sizes.barHeight}
x={Math.floor(Math.max(wattScale(b.retractedSum) - wattScale(b.retracted), 0))}
y={1}
onClick={this._selectRet.bind(this, i)}
className={getClass(ret[i], b.retractedSum, available)}
/>);
if (retLbl) {
retracted.push(<text
key={'rT' + i}
dy='0.5em'
textAnchor='middle'
height={sizes.barHeight}
x={wattScale(b.retractedSum) - (wattScale(b.retracted) / 2)}
y={sizes.retY}
onClick={this._selectRet.bind(this, i)}
className='primary-bg'>{retLbl}</text>
);
}
}
if (outerWidth && (b.retracted > 0 || b.deployed > 0)) {
let depLbl = bandText(b.deployed + b.retracted, i, wattScale);
deployed.push(<rect
key={'dB' + i}
width={Math.ceil(Math.max(wattScale(b.deployed + b.retracted), 0))}
height={sizes.barHeight}
x={Math.floor(Math.max(wattScale(b.deployedSum) - wattScale(b.retracted) - wattScale(b.deployed), 0))}
y={sizes.barHeight + 1}
onClick={this._selectDep.bind(this, i)}
className={getClass(dep[i], b.deployedSum, available)}
/>);
if (depLbl) {
deployed.push(<text
key={'dT' + i}
dy='0.5em'
textAnchor='middle'
height={sizes.barHeight}
x={wattScale(b.deployedSum) - ((wattScale(b.retracted) + wattScale(b.deployed)) / 2)}
y={sizes.depY}
onClick={this._selectDep.bind(this, i)}
className='primary-bg'>{depLbl}</text>
);
}
}
}
}
return (
<svg style={{ marginTop: '1em', width: '100%', height: sizes.height }} onContextMenu={wrapCtxMenu(this._selectNone)}>
<g transform={`translate(${sizes.mLeft},${sizes.mTop})`}>
<g className='power-band'>{retracted}</g>
<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,${sizes.innerHeight})`}></g>
<line x1={pctScale(0.5)} x2={pctScale(0.5)} y1='0' y2={sizes.innerHeight} className={pwrWarningClass} />
<text dy='0.5em' x='-3' y={sizes.retY} className='primary upp' textAnchor='end'>{translate('ret')}</text>
<text dy='0.5em' x='-3' y={sizes.depY} className='primary upp' textAnchor='end'>{translate('dep')}</text>
<text dy='0.5em' x={innerWidth + 5} y={sizes.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={sizes.depY} className={getClass(depSelected, depSum, available)}>{f2(Math.max(0, depSum)) + ' (' + pct1(Math.max(0, depSum / available)) + ')'}</text>
</g>
</svg>
);
}
}

View File

@@ -0,0 +1,179 @@
import React from 'react';
import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent';
import PowerBands from './PowerBands';
import { slotName, nameComparator } from '../utils/SlotFunctions';
import { Power, NoPower } from './SvgIcons';
const POWER = [
null,
null,
<NoPower className='icon warning' />,
<Power className='secondary-disabled' />
];
export default class PowerManagement extends TranslatedComponent {
static PropTypes = {
ship: React.PropTypes.object.isRequired,
code: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired
};
constructor(props) {
super(props);
this._renderPowerRows = this._renderPowerRows.bind(this);
this._sortName = this._sortName.bind(this);
this._sortType = this._sortType.bind(this);
this._sortPriority = this._sortPriority.bind(this);
this._sortPower = this._sortPower.bind(this);
this._sortRetracted = this._sortRetracted.bind(this);
this._sortDeployed = this._sortDeployed.bind(this);
this.state = {}; // State is initialized through componentWillMount
}
_sortOrder(predicate) {
let desc = this.state.desc;
if (predicate == this.state.predicate) {
desc = !desc;
} else {
desc = true;
}
if (!desc) {
this.props.ship.powerList.reverse();
}
this.setState({ predicate, desc });
}
_sortName() {
let translate = this.context.language.translate;
this.props.ship.powerList.sort(nameComparator(translate));
this._sortOrder('n');
}
_sortType() {
this.props.ship.powerList.sort((a, b) => a.type.localeCompare(b.type));
this._sortOrder('t');
}
_sortPriority() {
this.props.ship.powerList.sort((a, b) => a.priority - b.priority);
this._sortOrder('pri');
}
_sortPower() {
this.props.ship.powerList.sort((a, b) => (a.m ? a.m.power : 0) - (b.m ? b.m.power : 0));
this._sortOrder('pwr');
}
_sortRetracted() {
let ship = this.props.ship;
this.props.ship.powerList.sort((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b));
this._sortOrder('ret');
}
_sortDeployed() {
let ship = this.props.ship;
this.props.ship.powerList.sort((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true));
this._sortOrder('dep');
}
_priority(slot, inc) {
if (this.props.ship.setSlotPriority(slot, slot.priority + inc)) {
this.props.onChange();
}
}
_toggleEnabled(slot) {
this.props.ship.setSlotEnabled(slot, !slot.enabled);
this.props.onChange();
}
_renderPowerRows(ship, translate, pwr, pct) {
let powerRows = [];
for (var i = 0, l = ship.powerList.length; i < l; i++) {
let slot = ship.powerList[i];
if (slot.m && slot.m.power) {
let m = slot.m;
let toggleEnabled = this._toggleEnabled.bind(this, slot);
let retractedElem = null, deployedElem = null;
if (slot.enabled) {
retractedElem = <td className='ptr upp' onClick={toggleEnabled}>{POWER[ship.getSlotStatus(slot, false)]}</td>;
deployedElem = <td className='ptr upp' onClick={toggleEnabled}>{POWER[ship.getSlotStatus(slot, true)]}</td>;
} else {
retractedElem = <td className='ptr disabled upp' colSpan='2' onClick={toggleEnabled}>{translate('disabled')}</td>;
}
powerRows.push(<tr key={i} className={cn('highlight', { disabled: !slot.enabled })}>
<td className='ptr' style={{ width: '1em' }} onClick={toggleEnabled}>{m.class + m.rating}</td>
<td className='ptr le shorten cap' onClick={toggleEnabled}>{slotName(translate, slot)}</td>
<td className='ptr' onClick={toggleEnabled}><u>{translate(slot.type)}</u></td>
<td>
<span className='flip ptr' onClick={this._priority.bind(this, slot, -1)}>&#9658;</span>
{' ' + (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: '3em' }} onClick={toggleEnabled}><u>{pct(m.power / ship.powerAvailable)}</u></td>
{retractedElem}
{deployedElem}
</tr>);
}
}
return powerRows;
}
componentWillMount(){
this._sortName();
// Listen to window resize
}
componentWillUnmount(){
// remove window listener
// remove mouse move listener / touch listner?
}
render() {
let { ship, code } = this.props;
let { translate, formats } = this.context.language;
let pwr = formats.f2;
let pp = ship.standard[0].m;
return (
<div className='group half' id='componentPriority'>
<table style={{ width: '100%' }}>
<thead>
<tr className='main'>
<th colSpan='2' className='sortable le' onClick={this._sortName} >{translate('module')}</th>
<th style={{ width: '3em' }} className='sortable' onClick={this._sortType} >{translate('type')}</th>
<th style={{ width: '4em' }} className='sortable' onClick={this._sortPriority} >{translate('pri')}</th>
<th colSpan='2' className='sortable' onClick={this._sortPower} >{translate('PWR')}</th>
<th style={{ width: '3em' }} className='sortable' onClick={this._sortRetracted} >{translate('ret')}</th>
<th style={{ width: '3em' }} className='sortable' onClick={this._sortDeployed} >{translate('dep')}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{pp.class + pp.rating}</td>
<td className='le shorten cap' >{translate('pp')}</td>
<td><u >{translate('SYS')}</u></td>
<td>1</td>
<td className='ri'>{pwr(pp.pGen)}</td>
<td className='ri'><u>100%</u></td>
<td></td>
<td></td>
</tr>
<tr><td style={{ lineHeight:0 }} colSpan='8'><hr style={{ margin: '0 0 3px', background: '#ff8c0d', border: 0, height: 1 }} /></td></tr>
{this._renderPowerRows(ship, translate, pwr, formats.pct1)}
</tbody>
</table>
<PowerBands code={code} available={ship.standard[0].m.pGen} bands={ship.priorityBands} />
</div>
);
}
}

View File

@@ -11,10 +11,6 @@ export default class ShipSummaryTable extends TranslatedComponent {
ship: React.PropTypes.object.isRequired
}
shouldComponentUpdate() {
return true;
}
render() {
let ship = this.props.ship;
let language = this.context.language;

View File

@@ -2,27 +2,46 @@ import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import cn from 'classnames';
import AvailableModulesMenu from './AvailableModulesMenu';
import { contextMenuHandler } from '../utils/InterfaceEvents';
import { wrapCtxMenu } from '../utils/InterfaceEvents';
export default class Slot extends TranslatedComponent {
static propTypes = {
modules: React.PropTypes.oneOfType([ React.PropTypes.object, React.PropTypes.array ]).isRequired,
availableModules: React.PropTypes.func.isRequired,
onSelect: React.PropTypes.func.isRequired,
onOpen: React.PropTypes.func.isRequired,
size: React.PropTypes.number.isRequired,
maxClass: React.PropTypes.number.isRequired,
selected: React.PropTypes.bool,
m: React.PropTypes.object,
shipMass: React.PropTypes.number,
warning: React.PropTypes.func,
};
getClassNames() {
constructor(props) {
super(props);
this._contextMenu = wrapCtxMenu(this._contextMenu.bind(this));
this._getMaxClassLabel = this._getMaxClassLabel.bind(this);
}
// Must be implemented by subclasses:
// _getSlotDetails()
_getClassNames() {
return null;
}
getSize() {
return this.props.size;
/**
* Get the label for the slot size/class
* Should be overriden if necessary
* @return {string} label
*/
_getMaxClassLabel() {
return this.props.maxClass;
}
_contextMenu(event) {
this.props.onSelect(null,null);
}
render() {
@@ -32,15 +51,15 @@ export default class Slot extends TranslatedComponent {
let slotDetails, menu;
if (m) {
slotDetails = this.getSlotDetails(m, translate, language.formats, language.units); // Must be implemented by sub classes
slotDetails = this._getSlotDetails(m, translate, language.formats, language.units); // Must be implemented by sub classes
} else {
slotDetails = <div className={'empty'}>{translate('empty')}</div>;
}
if (this.props.selected) {
menu = <AvailableModulesMenu
className={this.getClassNames()}
modules={this.props.modules}
className={this._getClassNames()}
modules={this.props.availableModules()}
shipMass={this.props.shipMass}
m={m}
onSelect={this.props.onSelect}
@@ -49,9 +68,9 @@ export default class Slot extends TranslatedComponent {
}
return (
<div className={cn('slot', {selected: this.props.selected})} onClick={this.props.onOpen} onContextMenu={ this.contextmenu === false ? null : contextMenuHandler(this.props.onSelect.bind(null, null))}>
<div className={cn('slot', {selected: this.props.selected})} onClick={this.props.onOpen} onContextMenu={this._contextMenu}>
<div className={'details'}>
<div className={'sz'}>{this.getSize(translate)}</div>
<div className={'sz'}>{this._getMaxClassLabel(translate)}</div>
{slotDetails}
</div>
{menu}

View File

@@ -1,13 +1,17 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents';
import { wrapCtxMenu } from '../utils/InterfaceEvents';
import { Equalizer } from '../components/SvgIcons';
import cn from 'classnames';
export default class SlotSection extends TranslatedComponent {
static propTypes = {
ship: React.PropTypes.object.isRequired
ship: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
code: React.PropTypes.string.isRequired,
togglePwr: React.PropTypes.func
};
constructor(props, context, sectionId, sectionName) {
@@ -18,51 +22,51 @@ export default class SlotSection extends TranslatedComponent {
this._getSlots = this._getSlots.bind(this);
this._selectModule = this._selectModule.bind(this);
this._getSectionMenu = this._getSectionMenu.bind(this);
this.state = {
currentMenu: null
}
this._contextMenu = this._contextMenu.bind(this);
this._close = this._close.bind(this);
}
// Must be implemented by subclasses:
// _getSlots()
// _getSectionMenu()
// _contextMenu()
_openMenu(menu) {
this.setState({ currentMenu: menu });
InterfaceEvents.closeAll(menu);
_openMenu(menu, event) {
event.stopPropagation();
if (this.props.currentMenu === menu) {
menu = null;
}
_closeMenu() {
if (this.state.currentMenu) {
this.setState({ currentMenu: null });
}
InterfaceEvents.openMenu(menu);
}
_selectModule(index, slot, m) {
_selectModule(slot, m) {
this.props.ship.use(slot, m);
this._closeMenu();
this.props.onChange();
this._close();
}
componentWillReceiveProps(nextProps, nextContext) {
this.setState({ currentMenu: null });
_togglePwr(slot) {
this.props.ship.setSlotEnabled(slot, !slot.enabled);
this.props.onChange();
this._close();
}
componentWillMount() {
this.closeAllListener = InterfaceEvents.addListener('closeAll', this._closeMenu.bind(this));
_close() {
if (this.props.currentMenu) {
InterfaceEvents.closeMenu();
}
componentWillUnmount() {
this.closeAllListener.remove();
}
render() {
let translate = this.context.language.translate;
let sectionMenuOpened = this.state.currentMenu === this.sectionName;
let sectionMenuOpened = this.props.currentMenu === this.sectionName;
let open = this._openMenu.bind(this, this.sectionName);
let ctx = wrapCtxMenu(this._contextMenu);
return (
<div id={this.sectionId} className={'group'}>
<div className={cn('section-menu', {selected: sectionMenuOpened})} onClick={this._openMenu.bind(this, this.sectionName)}>
<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

@@ -7,7 +7,7 @@ export default class StandardSlot extends TranslatedComponent {
static propTypes = {
slot: React.PropTypes.object,
modules: React.PropTypes.oneOfType([ React.PropTypes.object, React.PropTypes.array ]).isRequired,
modules: React.PropTypes.array.isRequired,
onSelect: React.PropTypes.func.isRequired,
onOpen: React.PropTypes.func.isRequired,
selected: React.PropTypes.bool,
@@ -17,7 +17,7 @@ export default class StandardSlot extends TranslatedComponent {
render() {
let { translate, formats, units } = this.context.language;
let slot = this.props.slot
let { slot, warning } = this.props;
let m = slot.m;
let classRating = m.class + m.rating;
let menu;
@@ -34,7 +34,7 @@ export default class StandardSlot extends TranslatedComponent {
return (
<div className={cn('slot', {selected: this.props.selected})} onClick={this.props.onOpen}>
<div className={'details'}>
<div className={cn('details', {warning: warning && warning(slot.m)})}>
<div className={'sz'}>{slot.maxClass}</div>
<div>
<div className={'l'}>{classRating + ' ' + translate(m.grp)}</div>

View File

@@ -1,8 +1,8 @@
import React from 'react';
import cn from 'classnames';
import SlotSection from './SlotSection';
import StandardSlot from './StandardSlot';
import cn from 'classnames';
import { ArmourMultiplier } from '../shipyard/Constants';
import * as ModuleUtils from '../shipyard/ModuleUtils';
export default class StandardSlotSection extends SlotSection {
@@ -15,24 +15,97 @@ export default class StandardSlotSection extends SlotSection {
}
_fill(rating) {
this.props.ship.useStandard(rating);
this.props.onChange();
this._close();
}
_optimizeStandard() {
this.props.ship.useLightestStandard();
this.props.onChange();
this._close();
}
_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.useLightestStandard();
this.props.onChange();
this._close();
}
_optimizeExplorer() {
let ship = this.props.ship,
intLength = ship.internal.length,
heatSinkCount = 2, // Fit 2 heat sinks if possible
afmUnitCount = 2, // Fit 2 AFM Units if possible
sgSlot,
fuelScoopSlot,
sg = ship.getAvailableModules().lightestShieldGenerator(ship.hullMass);
ship.setSlotEnabled(ship.cargoHatch, false)
.use(ship.internal[--intLength], ModuleUtils.internal('2f')) // Advanced Discovery Scanner
.use(ship.internal[--intLength], ModuleUtils.internal('2i')); // Detailed Surface Scanner
for (let i = 0; i < intLength; i++) {
let slot = ship.internal[i];
let nextSlot = (i + 1) < intLength ? ship.internal[i + 1] : null;
if (!fuelScoopSlot && (!slot.eligible || slot.eligible.fs)) { // Fit best possible Fuel Scoop
fuelScoopSlot = slot;
ship.use(fuelScoopSlot, ModuleUtils.findInternal('fs', slot.maxClass, 'A'));
ship.setSlotEnabled(fuelScoopSlot, true);
// Mount a Shield generator if possible AND an AFM Unit has been mounted already (Guarantees at least 1 AFM Unit)
} else if (!sgSlot && afmUnitCount < 2 && sg.class <= slot.maxClass && (!slot.eligible || slot.eligible.sg) && (!nextSlot || nextSlot.maxClass < sg.class)) {
sgSlot = slot;
ship.use(sgSlot, sg);
ship.setSlotEnabled(sgSlot, true);
} else if (afmUnitCount > 0 && (!slot.eligible || slot.eligible.am)) {
afmUnitCount--;
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);
}
}
ship.hardpoints.forEach((s) => {
if (s.maxClass == 0 && heatSinkCount) { // Mount up to 2 heatsinks
ship.use(s, ModuleUtils.hardpoints('02'));
ship.setSlotEnabled(s, heatSinkCount == 2); // Only enable a single Heatsink
heatSinkCount--;
} else {
ship.use(s, null);
}
});
if (sgSlot) {
// The SG and Fuel scoop to not need to be powered at the same time
if (sgSlot.m.power > fuelScoopSlot.m.power) { // The Shield generator uses the most power
ship.setSlotEnabled(fuelScoopSlot, false);
} else { // The Fuel scoop uses the most power
ship.setSlotEnabled(sgSlot, false);
}
}
ship.useLightestStandard({ pd: '1D', ppRating: 'A' });
this.props.onChange();
this._close();
}
_selectBulkhead(bulkheadIndex) {
this.props.ship.useBulkhead(bulkheadIndex);
this._closeMenu();
this.props.onChange();
this._close();
}
_contextMenu() {
this._optimizeStandard();
}
_getSlots() {
@@ -45,7 +118,7 @@ export default class StandardSlotSection extends SlotSection {
let st = ship.standard;
let avail = ship.getAvailableModules().standard;
let bulkheads = ship.bulkheads;
let currentMenu = this.state.currentMenu;
let currentMenu = this.props.currentMenu;
slots[0] = (
<div key='bh' className={cn('slot', {selected: currentMenu === bulkheads})} onClick={open.bind(this, bulkheads)}>
@@ -76,9 +149,9 @@ export default class StandardSlotSection extends SlotSection {
slot={st[0]}
modules={avail[0]}
onOpen={open.bind(this, st[0])}
onSelect={select.bind(this, 1, st[0])}
onSelect={select.bind(this, st[0])}
selected={currentMenu == st[0]}
warning={(m) => m.pGen < ship.powerRetracted}
warning={m => m.pGen < ship.powerRetracted}
/>;
slots[2] = <StandardSlot
@@ -86,9 +159,9 @@ export default class StandardSlotSection extends SlotSection {
slot={st[1]}
modules={avail[1]}
onOpen={open.bind(this, st[1])}
onSelect={select.bind(this, 2, st[1])}
onSelect={select.bind(this, st[1])}
selected={currentMenu == st[1]}
warning={(m) => m.maxmass < ship.ladenMass}
warning={m => m.maxmass < ship.ladenMass}
/>;
@@ -97,7 +170,7 @@ export default class StandardSlotSection extends SlotSection {
slot={st[2]}
modules={avail[2]}
onOpen={open.bind(this, st[2])}
onSelect={select.bind(this, 3, st[2])}
onSelect={select.bind(this, st[2])}
selected={currentMenu == st[2]}
/>;
@@ -106,7 +179,7 @@ export default class StandardSlotSection extends SlotSection {
slot={st[3]}
modules={avail[3]}
onOpen={open.bind(this, st[3])}
onSelect={select.bind(this, 4, st[3])}
onSelect={select.bind(this, st[3])}
selected={currentMenu == st[3]}
/>;
@@ -115,7 +188,7 @@ export default class StandardSlotSection extends SlotSection {
slot={st[4]}
modules={avail[4]}
onOpen={open.bind(this, st[4])}
onSelect={select.bind(this, 5, st[4])}
onSelect={select.bind(this, st[4])}
selected={currentMenu == st[4]}
warning= {m => m.enginecapacity < ship.boostEnergy}
/>;
@@ -125,7 +198,7 @@ export default class StandardSlotSection extends SlotSection {
slot={st[5]}
modules={avail[5]}
onOpen={open.bind(this, st[5])}
onSelect={select.bind(this, 6, st[5])}
onSelect={select.bind(this, st[5])}
selected={currentMenu == st[5]}
warning= {m => m.enginecapacity < ship.boostEnergy}
/>;
@@ -135,7 +208,7 @@ export default class StandardSlotSection extends SlotSection {
slot={st[6]}
modules={avail[6]}
onOpen={open.bind(this, st[6])}
onSelect={select.bind(this, 7, 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
/>;

View File

@@ -117,6 +117,7 @@ export class LinkIcon extends SvgIcon {
}
export class NoPower extends SvgIcon {
viewBox() { return '0 0 512 512' };
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'/>;
}
@@ -129,6 +130,7 @@ export class Notification extends SvgIcon {
}
export class Power extends SvgIcon {
viewBox() { return '0 0 512 512' };
svg() {
return <path d='M192 0l-192 256h192l-128 256 448-320h-256l192-192z'/>;
}

View File

@@ -12,29 +12,38 @@ export default class UtilitySlotSection extends SlotSection {
}
_empty() {
this.props.ship.emptyUtility();
this.props.onChange();
this._close();
}
_use(grp, rating) {
_use(group, rating, name, event) {
this.props.ship.useUtility(group, rating, name, event.getModifierState('Alt'));
this.props.onChange();
this._close();
}
_contextMenu() {
this._empty();
}
_getSlots() {
let slots = [];
let hardpoints = this.props.ship.hardpoints;
let availableModules = this.props.ship.getAvailableModules();
let currentMenu = this.state.currentMenu;
let currentMenu = this.props.currentMenu;
for (let i = 0, l = hardpoints.length; i < l; i++) {
let h = hardpoints[i];
if (h.maxClass === 0) {
slots.push(<HardpointSlot
key={i}
size={h.maxClass}
modules={availableModules.getHps(h.maxClass)}
maxClass={h.maxClass}
availableModules={() => availableModules.getHps(h.maxClass)}
onOpen={this._openMenu.bind(this,h)}
onSelect={this._selectModule.bind(this, h)}
selected={currentMenu == h}
enabled={h.enabled}
m={h.m}
/>);
}
@@ -52,11 +61,15 @@ export default class UtilitySlotSection extends SlotSection {
</ul>
<div className='select-group cap'>{translate('sb')}</div>
<ul>
<li className='c' onClick={_use.bind(this, 'sb', 'E')}>E</li>
<li className='c' onClick={_use.bind(this, 'sb', 'D')}>D</li>
<li className='c' onClick={_use.bind(this, 'sb', 'C')}>C</li>
<li className='c' onClick={_use.bind(this, 'sb', 'B')}>B</li>
<li className='c' onClick={_use.bind(this, 'sb', 'A')}>A</li>
<li className='c' onClick={_use.bind(this, 'sb', 'E', null)}>E</li>
<li className='c' onClick={_use.bind(this, 'sb', 'D', null)}>D</li>
<li className='c' onClick={_use.bind(this, 'sb', 'C', null)}>C</li>
<li className='c' onClick={_use.bind(this, 'sb', 'B', null)}>B</li>
<li className='c' onClick={_use.bind(this, 'sb', 'A', null)}>A</li>
</ul>
<div className='select-group cap'>{translate('cm')}</div>
<ul>
<li className='lc' onClick={_use.bind(this, 'cm', null, 'Heat Sink Launcher')}>{translate('Heat Sink Launcher')}</li>
</ul>
</div>;
}

View File

@@ -1,164 +0,0 @@
angular.module('app').directive('areaChart', ['$window', '$translate', function($window, $translate) {
return {
restrict: 'A',
scope: {
config: '=',
series: '='
},
link: function(scope, element) {
var series = scope.series,
config = scope.config,
labels = config.labels,
margin = { top: 15, right: 15, bottom: 35, left: 60 },
fmt = d3.format('.3r'),
fmtLong = d3.format('.2f'),
func = series.func,
drag = d3.behavior.drag(),
dragging = false,
// Define Axes
xAxis = d3.svg.axis().outerTickSize(0).orient('bottom').tickFormat(d3.format('.2r')),
yAxis = d3.svg.axis().ticks(6).outerTickSize(0).orient('left').tickFormat(fmt),
x = d3.scale.linear(),
y = d3.scale.linear(),
data = [];
// Create chart
var svg = d3.select(element[0]).append('svg');
var vis = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// Define Area
var area = d3.svg.area();
var gradient = vis.append('defs')
.append('linearGradient')
.attr('id', 'gradient')
.attr('x1', '0%').attr('y1', '0%')
.attr('x2', '100%').attr('y2', '100%')
.attr('spreadMethod', 'pad');
gradient.append('stop')
.attr('offset', '0%')
.attr('stop-color', '#ff8c0d')
.attr('stop-opacity', 1);
gradient.append('stop')
.attr('offset', '100%')
.attr('stop-color', '#ff3b00')
.attr('stop-opacity', 1);
// Create Y Axis SVG Elements
var yTxt = vis.append('g').attr('class', 'y axis')
.append('text')
.attr('class', 'cap')
.attr('transform', 'rotate(-90)')
.attr('y', -50)
.attr('dy', '.1em')
.style('text-anchor', 'middle')
.text($translate.instant(labels.yAxis.title) + ' (' + $translate.instant(labels.yAxis.unit) + ')');
// Create X Axis SVG Elements
var xLbl = vis.append('g').attr('class', 'x axis');
var xTxt = xLbl.append('text')
.attr('y', 30)
.attr('dy', '.1em')
.style('text-anchor', 'middle')
.text($translate.instant(labels.xAxis.title) + ' (' + $translate.instant(labels.xAxis.unit) + ')');
// Create and Add tooltip
var tip = vis.append('g').style('display', 'none');
tip.append('rect').attr('width', '4.5em').attr('height', '2em').attr('x', '0.5em').attr('y', '-1em').attr('class', 'tip');
tip.append('circle')
.attr('class', 'marker')
.attr('r', 4);
tip.append('text').attr('class', 'label x').attr('y', '-0.25em');
tip.append('text').attr('class', 'label y').attr('y', '0.85em');
vis.insert('path', ':first-child') // Area/Path to appear behind everything else
.data([data])
.attr('class', 'area')
.attr('fill', 'url(#gradient)')
.attr('d', area)
.on('mouseover', showTip)
.on('mouseout', hideTip)
.on('mousemove', moveTip)
.call(drag);
drag
.on('dragstart', function() {
dragging = true;
moveTip.call(this);
showTip();
})
.on('dragend', function() {
dragging = false;
hideTip();
})
.on('drag', moveTip);
/**
* Watch for changes in the series data (mass changes, etc)
*/
scope.$watchCollection('series', render);
angular.element($window).bind('orientationchange resize render', render);
function render() {
var width = element[0].parentElement.offsetWidth,
height = width * 0.5,
w = width - margin.left - margin.right,
h = height - margin.top - margin.bottom;
data.length = 0; // Reset Data array
if (series.xMax == series.xMin) {
var yVal = func(series.xMin);
data.push([ series.xMin, yVal ]);
data.push([ series.xMin, yVal ]);
area.x(function(d, i) { return i * w; }).y0(h).y1(function(d) { return y(d[1]); });
} else {
for (var val = series.xMin; val <= series.xMax; val += 1) {
data.push([ val, func(val) ]);
}
area.x(function(d) { return x(d[0]); }).y0(h).y1(function(d) { return y(d[1]); });
}
// Update Chart Size
svg.attr('width', width).attr('height', height);
// Update domain and scale for axes
x.range([0, w]).domain([series.xMin, series.xMax]).clamp(true);
xAxis.scale(x);
xLbl.attr('transform', 'translate(0,' + h + ')');
xTxt.attr('x', w / 2);
y.range([h, 0]).domain([series.yMin, series.yMax]);
yAxis.scale(y);
yTxt.attr('x', -h / 2);
vis.selectAll('.y.axis').call(yAxis);
vis.selectAll('.x.axis').call(xAxis);
vis.selectAll('path.area') // Area/Path to appear behind everything else
.data([data])
.attr('d', area);
}
function showTip() {
tip.style('display', null);
}
function hideTip() {
if (!dragging) {
tip.style('display', 'none');
}
}
function moveTip() {
var xPos = d3.mouse(this)[0], x0 = x.invert(xPos), y0 = func(x0), flip = (x0 / x.domain()[1] > 0.65);
tip.attr('transform', 'translate(' + x(x0) + ',' + y(y0) + ')');
tip.selectAll('rect').attr('x', flip ? '-5.75em' : '0.5em').style('text-anchor', flip ? 'end' : 'start');
tip.selectAll('text.label').attr('x', flip ? '-2em' : '1em').style('text-anchor', flip ? 'end' : 'start');
tip.select('text.label.x').text(fmtLong(x0) + ' ' + $translate.instant(labels.xAxis.unit));
tip.select('text.label.y').text(fmtLong(y0) + ' ' + $translate.instant(labels.yAxis.unit));
}
scope.$on('$destroy', function() {
angular.element($window).unbind('orientationchange resize render', render);
});
}
};
}]);

View File

@@ -1,209 +0,0 @@
angular.module('app').directive('powerBands', ['$window', '$translate', '$rootScope', function($window, $translate, $rootScope) {
return {
restrict: 'A',
scope: {
bands: '=',
available: '='
},
link: function(scope, element) {
var bands = null,
available = 0,
maxBand,
maxPwr,
deployedSum = 0,
retractedSum = 0,
retBandsSelected = false,
depBandsSelected = false,
wattScale = d3.scale.linear(),
pctScale = d3.scale.linear().domain([0, 1]),
wattFmt,
pctFmt,
wattAxis = d3.svg.axis().scale(wattScale).outerTickSize(0).orient('top'),
pctAxis = d3.svg.axis().scale(pctScale).outerTickSize(0).orient('bottom'),
// Create chart
svg = d3.select(element[0]).append('svg'),
vis = svg.append('g'),
deployed = vis.append('g').attr('class', 'power-band'),
retracted = vis.append('g').attr('class', 'power-band');
svg.on('contextmenu', function() {
if (!d3.event.shiftKey) {
d3.event.preventDefault();
for (var i = 0, l = bands.length; i < l; i++) {
bands[i].retSelected = false;
bands[i].depSelected = false;
}
dataChange();
}
});
// Create Y Axis SVG Elements
var wattAxisGroup = vis.append('g').attr('class', 'watt axis');
vis.append('g').attr('class', 'pct axis');
var retText = vis.append('text').attr('x', -3).style('text-anchor', 'end').attr('dy', '0.5em').attr('class', 'primary upp');
var depText = vis.append('text').attr('x', -3).style('text-anchor', 'end').attr('dy', '0.5em').attr('class', 'primary upp');
var retLbl = vis.append('text').attr('dy', '0.5em');
var depLbl = vis.append('text').attr('dy', '0.5em');
updateFormats(true);
function dataChange() {
bands = scope.bands;
available = scope.available;
maxBand = bands[bands.length - 1];
deployedSum = 0;
retractedSum = 0;
retBandsSelected = false;
depBandsSelected = false;
maxPwr = Math.max(available, maxBand.retractedSum, maxBand.deployedSum);
for (var b = 0, l = bands.length; b < l; b++) {
if (bands[b].retSelected) {
retractedSum += bands[b].retracted + bands[b].retOnly;
retBandsSelected = true;
}
if (bands[b].depSelected) {
deployedSum += bands[b].deployed + bands[b].retracted;
depBandsSelected = true;
}
}
render();
}
function render() {
var size = $rootScope.sizeRatio,
mTop = Math.round(25 * size),
mRight = Math.round(130 * size),
mBottom = Math.round(25 * size),
mLeft = Math.round(45 * size),
barHeight = Math.round(20 * size),
width = element[0].offsetWidth,
innerHeight = (barHeight * 2) + 2,
height = innerHeight + mTop + mBottom,
w = width - mLeft - mRight,
repY = (barHeight / 2),
depY = (barHeight * 1.5) - 1;
// Update chart size
svg.attr('width', width).attr('height', height);
vis.attr('transform', 'translate(' + mLeft + ',' + mTop + ')');
// Remove existing elements
retracted.selectAll('rect').remove();
retracted.selectAll('text').remove();
deployed.selectAll('rect').remove();
deployed.selectAll('text').remove();
wattAxisGroup.selectAll('line.threshold').remove();
// Update X & Y Axis
wattScale.range([0, w]).domain([0, maxPwr]).clamp(true);
pctScale.range([0, w]).domain([0, maxPwr / available]).clamp(true);
wattAxisGroup.call(wattAxis);
vis.selectAll('.pct.axis').attr('transform', 'translate(0,' + innerHeight + ')').call(pctAxis);
var pwrWarningClass = 'threshold' + (bands[0].retractedSum * 2 >= available ? ' exceeded' : '');
vis.select('.pct.axis g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass);
wattAxisGroup.append('line')
.attr('x1', pctScale(0.5))
.attr('x2', pctScale(0.5))
.attr('y1', 0)
.attr('y2', innerHeight)
.attr('class', pwrWarningClass);
retText.attr('y', repY);
depText.attr('y', depY);
updateLabel(retLbl, w, repY, retBandsSelected, retBandsSelected ? retractedSum : maxBand.retractedSum, available);
updateLabel(depLbl, w, depY, depBandsSelected, depBandsSelected ? deployedSum : maxBand.deployedSum, available);
retracted.selectAll('rect').data(bands).enter().append('rect')
.attr('height', barHeight)
.attr('width', function(d) { return Math.ceil(Math.max(wattScale(d.retracted + d.retOnly), 0)); })
.attr('x', function(d) { return Math.floor(Math.max(wattScale(d.retractedSum) - wattScale(d.retracted + d.retOnly), 0)); })
.attr('y', 1)
.on('click', function(d) {
d.retSelected = !d.retSelected;
dataChange();
})
.attr('class', function(d) { return getClass(d.retSelected, d.retractedSum, available); });
retracted.selectAll('text').data(bands).enter().append('text')
.attr('x', function(d) { return wattScale(d.retractedSum) - (wattScale(d.retracted + d.retOnly) / 2); })
.attr('y', repY)
.attr('dy', '0.5em')
.style('text-anchor', 'middle')
.attr('class', 'primary-bg')
.on('click', function(d) {
d.retSelected = !d.retSelected;
dataChange();
})
.text(function(d, i) { return bandText(d.retracted + d.retOnly, i); });
deployed.selectAll('rect').data(bands).enter().append('rect')
.attr('height', barHeight)
.attr('width', function(d) { return Math.ceil(Math.max(wattScale(d.deployed + d.retracted), 0)); })
.attr('x', function(d) { return Math.floor(Math.max(wattScale(d.deployedSum) - wattScale(d.retracted) - wattScale(d.deployed), 0)); })
.attr('y', barHeight + 1)
.on('click', function(d) {
d.depSelected = !d.depSelected;
dataChange();
})
.attr('class', function(d) { return getClass(d.depSelected, d.deployedSum, available); });
deployed.selectAll('text').data(bands).enter().append('text')
.attr('x', function(d) { return wattScale(d.deployedSum) - ((wattScale(d.retracted) + wattScale(d.deployed)) / 2); })
.attr('y', depY)
.attr('dy', '0.5em')
.style('text-anchor', 'middle')
.attr('class', 'primary-bg')
.on('click', function(d) {
d.depSelected = !d.depSelected;
dataChange();
})
.text(function(d, i) { return bandText(d.deployed + d.retracted, i); });
}
function updateLabel(lbl, width, y, selected, sum, avail) {
lbl
.attr('x', width + 5 )
.attr('y', y)
.attr('class', getClass(selected, sum, avail))
.text(wattFmt(Math.max(0, sum)) + ' (' + pctFmt(Math.max(0, sum / avail)) + ')');
}
function getClass(selected, sum, avail) {
// Round to avoid floating point precision errors
return selected ? 'secondary' : ((Math.round(sum * 100) / 100) >= avail) ? 'warning' : 'primary';
}
function bandText(val, index) {
if (val > 0 && wattScale(val) > 13) {
return index + 1;
}
return '';
}
function updateFormats(preventRender) {
retText.text($translate.instant('ret'));
depText.text($translate.instant('dep'));
wattFmt = $rootScope.localeFormat.numberFormat('.2f');
pctFmt = $rootScope.localeFormat.numberFormat('.1%');
wattAxis.tickFormat($rootScope.localeFormat.numberFormat('.2r'));
pctAxis.tickFormat($rootScope.localeFormat.numberFormat('%'));
if (!preventRender) {
render();
}
}
// Watch for changes to data and events
angular.element($window).bind('pwrchange', dataChange);
angular.element($window).bind('orientationchange resize', render);
scope.$watchCollection('available', dataChange);
scope.$on('languageChanged', updateFormats);
scope.$on('$destroy', function() {
angular.element($window).unbind('orientationchange resize pwrchange', render);
});
}
};
}]);

View File

@@ -34,13 +34,14 @@ export function getLanguage(langCode) {
return {
formats: {
gen: gen,
int: d3Locale.numberFormat(',.0f'),
pwr: d3Locale.numberFormat(',.2f'),
round: (d) => gen(d3.round(d, 2)),
pct: d3Locale.numberFormat('.2%'),
pct1: d3Locale.numberFormat('.1%'),
rPct: d3.format('%'),
gen: 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)
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)
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)
},
translate,
@@ -63,10 +64,9 @@ export function getLanguage(langCode) {
export const Languages = {
en: 'English',
de: 'Deutsh',
de: 'Deutsch',
it: 'Italiano',
es: 'Español',
fr: 'Français',
ru: 'ру́сский'
};

View File

@@ -15,216 +15,219 @@ 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',
// 'A-Rated': 'A-Rated',
// about: 'about',
// action: 'action',
// added: 'added',
// Advanced: 'Advanced',
// 'Advanced Discovery Scanner': 'Advanced Discovery Scanner',
// agility: 'agility',
// alpha: 'alpha',
// ammo: 'ammo',
PHRASE_CONFIRMATION: 'Are You Sure?',
armour: 'armour',
// armour: 'armour',
am: 'Auto Field-Maintenance Unit',
available: 'available',
backup: 'backup',
// 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',
// 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',
// cancel: 'cancel',
c: 'Cannon',
capital: 'capital',
cargo: 'cargo',
'Cargo Hatch': 'Cargo Hatch',
// capital: 'capital',
// cargo: 'cargo',
// 'Cargo Hatch': 'Cargo Hatch',
cr: 'Cargo Rack',
cs: 'Cargo Scanner',
cells: 'cells',
'Chaff Launcher': 'Chaff Launcher',
close: 'close',
// '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',
// 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',
// 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',
// 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: 'FSD',
fsd: 'Frame Shift Drive',
fi: 'FSD Interdictor',
fuel: 'fuel',
// fuel: 'fuel',
fs: 'Fuel Scoop',
ft: 'Fuel Tank',
fx: 'Fuel Transfer Limpet Controller',
'full tank': 'full tank',
Gimballed: 'Gimballed',
H: 'H',
hardpoints: 'hardpoints',
// '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',
// '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',
// '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',
// 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',
// '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',
// 'Mining Lance': 'Mining Lance',
ml: 'Mining Laser',
'Mirrored Surface Composite': 'Mirrored Surface Composite',
// 'Mirrored Surface Composite': 'Mirrored Surface Composite',
mr: 'Missile Rack',
mc: 'Multi-cannon',
'net cost': 'net cost',
no: 'no',
// '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',
// 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',
// pen: 'pen',
// penetration: 'penetration',
// permalink: 'permalink',
pa: 'Plasma Accelerator',
'Point Defence': 'Point Defence',
power: 'power',
// 'Point Defence': 'Point Defence',
// power: 'power',
pd: 'power distributor',
pp: 'power plant',
pri: 'pri',
priority: 'priority',
// priority: 'priority',
psg: 'Prismatic Shield Generator',
proceed: 'proceed',
// proceed: 'proceed',
pc: 'Prospector Limpet Controller',
pl: 'Pulse Laser',
PWR: 'PWR',
pv: 'Planetary Vehicle Hanger',
// PWR: 'PWR',
rg: 'Rail Gun',
range: 'range',
rate: 'rate',
'Reactive Surface Composite': 'Reactive Surface Composite',
recharge: 'recharge',
// 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',
// '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',
// sell: 'sell',
s: 'sensors',
settings: 'settings',
// 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',
// 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',
// 'The Retributor': 'The Retributor',
t: 'thrusters',
time: 'time',
// time: 'time',
tp: 'Torpedo Pylon',
total: 'total',
'total range': 'total range',
turret: 'turret',
type: 'type',
U: 'U',
unladen: 'unladen',
// 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',
// URL: 'URL',
// utility: 'utility',
// 'utility mounts': 'utility mounts',
// version: 'version',
// WEP: 'WEP',
// yes: 'yes',
PHRASE_BACKUP_DESC: 'Backup of all Coriolis data to save or transfer to another browser/device'
};

View File

@@ -91,7 +91,6 @@ export const terms = {
fc: 'Осколочное Орудие',
fd: 'Двигатель FSD',
ws: 'FSD Сканнер',
fi: 'Перехватчик FSD',
fuel: 'Топливо',
fs: 'Топливосборщик',

View File

@@ -31,7 +31,7 @@ export default class ComparisonPage extends Page {
_sortShips(shipPredicate, shipPredicateIndex) {
let shipDesc = this.state.shipDesc;
if (typeof shipPredicateIndex == "object") {
if (typeof shipPredicateIndex == 'object') {
shipPredicateIndex = undefined;
}
@@ -137,79 +137,79 @@ export default class ComparisonPage extends Page {
return (
<div className={'page'}>
<table id="comparison">
<tr ng-show="compareMode">
<td class="head" translate="comparison"></td>
{/*<table id='comparison'>
<tr ng-show='compareMode'>
<td class='head' translate='comparison'></td>
<td>
<input ng-model="name" ng-change="nameChange()" placeholder="{{'enter name' | translate}}" maxlength="50" />
<button ng-click="save()" ng-disabled="!name || name == 'all' || saved">
<svg class="icon lg "><use xlink:href="#floppy-disk"></use></svg><span class="button-lbl"> {{'save' | translate}}</span>
<input ng-model='name' ng-change='nameChange()' placeholder={translate('enter name')} maxlength={50} />
<button ng-click='save()'disabled{!name || name == 'all' || saved}>
<svg class='icon lg '><use xlink:href='#floppy-disk'></use></svg><span class='button-lbl'> {{'save' | translate}}</span>
</button>
<button ng-click="delete()" ng-disabled="name == 'all' || !saved"><svg class="icon lg warning "><use xlink:href="#bin"></use></svg></button>
<button ng-click="selectBuilds(true, $event)">
<svg class="icon lg "><use xlink:href="#rocket"></use></svg><span class="button-lbl"> {{'builds' | translate}}</span>
<button ng-click='delete()' ng-disabled='name == 'all' || !saved'><svg class='icon lg warning '><use xlink:href='#bin'></use></svg></button>
<button ng-click='selectBuilds(true, $event)'>
<svg class='icon lg '><use xlink:href='#rocket'></use></svg><span class='button-lbl'> {{'builds' | translate}}</span>
</button>
<button class="r" ng-click="permalink($event)" ng-disabled="builds.length == 0">
<svg class="icon lg "><use xlink:href="#link"></use></svg><span class="button-lbl"> {{'permalink' | translate}}</span>
<button class='r' ng-click='permalink($event)' ng-disabled='builds.length == 0'>
<svg class='icon lg '><use xlink:href='#link'></use></svg><span class='button-lbl'> {{'permalink' | translate}}</span>
</button>
<button class="r" ng-click="embed($event)" ng-disabled="builds.length == 0">
<svg class="icon lg "><use xlink:href="#embed"></use></svg><span class="button-lbl"> {{'forum' | translate}}</span>
<button class='r' ng-click='embed($event)' ng-disabled='builds.length == 0'>
<svg class='icon lg '><use xlink:href='#embed'></use></svg><span class='button-lbl'> {{'forum' | translate}}</span>
</button>
</td>
</tr>
<tr ng-show="!compareMode">
<td class="head" translate="comparison"></td>
<tr ng-show='!compareMode'>
<td class='head' translate='comparison'></td>
<td>
<h3 ng-bind="name"></h3>
<button class="r" ui-sref="modal.import({obj:importObj})"><svg class="icon lg "><use xlink:href="#download"></use></svg> {{'import' | translate}}</button>
<h3 ng-bind='name'></h3>
<button class='r' ui-sref='modal.import({obj:importObj})'><svg class='icon lg '><use xlink:href='#download'></use></svg> {{'import' | translate}}</button>
</td>
</tr>
<tr>
<td class="head" translate="compare"></td>
<td class='head' translate='compare'></td>
<td>
<ul id="facet-container" as-sortable="facetSortOpts" ng-model="facets" class="sortable" update="tblUpdate">
<li ng-repeat="(i,f) in facets" as-sortable-item class="facet" ng-class="{active: f.active}" ng-click="toggleFacet(i)">
<div as-sortable-item-handle>&#x2194; <span ng-bind="f.title | translate"></span></div>
<ul id='facet-container' as-sortable='facetSortOpts' ng-model='facets' class='sortable' update='tblUpdate'>
<li ng-repeat='(i,f) in facets' as-sortable-item class='facet' ng-class='{active: f.active}' ng-click='toggleFacet(i)'>
<div as-sortable-item-handle>&#x2194; <span ng-bind='f.title | translate'></span></div>
</li>
</ul>
</td>
</tr>
</table>
<div class="scroll-x">
<table id="comp-tbl" comparison-table ng-click="handleClick($event)"></table>
<div class='scroll-x'>
<table id='comp-tbl' comparison-table ng-click='handleClick($event)'></table>
</div>
<div ng-repeat="f in facets | filter:{active:true}" ng-if="builds.length > 0" class="chart" bar-chart facet="f" data="builds">
<h3 ng-click="sort(f.props[0])" >{{f.title | translate}}</h3>
<div ng-repeat='f in facets | filter:{active:true}' ng-if='builds.length > 0' class='chart' bar-chart facet='f' data='builds'>
<h3 ng-click='sort(f.props[0])' >{{f.title | translate}}</h3>
</div>
<div class="modal-bg" ng-show="showBuilds" ng-click="selectBuilds(false, $event)">
<div class="modal" ui-view="modal-content" ng-click="$event.stopPropagation()">
<h3 translate="PHRASE_SELECT_BUILDS"></h3>
<div id="build-select">
<div class='modal-bg' ng-show='showBuilds' ng-click='selectBuilds(false, $event)'>
<div class='modal' ui-view='modal-content' ng-click='$event.stopPropagation()'>
<h3 translate='PHRASE_SELECT_BUILDS'></h3>
<div id='build-select'>
<table>
<thead><tr><th colspan="2" translate="available"></th></tr></thead>
<thead><tr><th colspan='2' translate='available'></th></tr></thead>
<tbody>
<tr ng-repeat="b in unusedBuilds | orderBy:'name'" ng-click="addBuild(b.id, b.buildName)">
<td class="tl" ng-bind="b.name"></td><td class="tl" ng-bind="b.buildName"></td>
<tr ng-repeat='b in unusedBuilds | orderBy:'name'' ng-click='addBuild(b.id, b.buildName)'>
<td class='tl' ng-bind='b.name'></td><td class='tl' ng-bind='b.buildName'></td>
</tr>
</tbody>
</table>
<h1>⇆</h1>
<table>
<thead><tr><th colspan="2" translate="added"></th></tr></thead>
<thead><tr><th colspan='2' translate='added'></th></tr></thead>
<tbody>
<tr ng-repeat="b in builds | orderBy:'name'" ng-click="removeBuild(b.id, b.buildName)">
<td class="tl" ng-bind="b.name"></td><td class="tl" ng-bind="b.buildName"></td>
<tr ng-repeat='b in builds | orderBy:'name'' ng-click='removeBuild(b.id, b.buildName)'>
<td class='tl' ng-bind='b.name'></td><td class='tl' ng-bind='b.buildName'></td>
</tr>
</tbody>
</table>
</div>
<br/>
<button class="r dismiss cap" ng-click="selectBuilds(false, $event)" translate="done"></button>
</div>
<button class='r dismiss cap' ng-click='selectBuilds(false, $event)' translate='done'></button>
</div>
</div> */}
</div>
);
}

View File

@@ -1,25 +1,34 @@
import React from 'react';
import Page from './Page';
import cn from 'classnames';
import Router from '../Router';
import Persist from '../stores/Persist';
import Ships from '../shipyard/Ships';
import InterfaceEvents from '../utils/InterfaceEvents';
import { Ships } from 'coriolis-data';
import Ship from '../shipyard/Ship';
import { toShip } from '../shipyard/Serializer';
import { toDetailedBuild } from '../shipyard/Serializer';
import { FloppyDisk, Bin, Switch, Download, Reload } from '../components/SvgIcons';
import ShipSummaryTable from '../components/ShipSummaryTable';
import StandardSlotSection from '../components/StandardSlotSection';
import HardpointsSlotSection from '../components/HardpointsSlotSection';
import InternalSlotSection from '../components/InternalSlotSection';
import UtilitySlotSection from '../components/UtilitySlotSection';
import LineChart from '../components/LineChart';
import PowerManagement from '../components/PowerManagement';
import CostSection from '../components/CostSection';
import ModalExport from '../components/ModalExport';
const SPEED_SERIES = ['boost', '4 Pips', '2 Pips', '0 Pips'];
const SPEED_COLORS = ['#0088d2', '#ff8c0d', '#D26D00', '#c06400'];
export default class OutfittingPage extends Page {
constructor(props, context) {
super(props, context);
this.state = this._initState(props, context);
this.state = this._initState(context);
}
_initState(props, context) {
_initState(context) {
let params = context.route.params;
let shipId = params.ship;
let code = params.code;
@@ -34,22 +43,13 @@ export default class OutfittingPage extends Page {
}
let ship = new Ship(shipId, data.properties, data.slots); // Create a new Ship instance
let retrofitShip = new Ship(shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison
if (code) {
toShip(ship, code); // Populate components from 'code' URL param
ship.buildFrom(code); // Populate modules from serialized 'code' URL param
} else {
ship.buildWith(data.defaults); // Populate with default components
}
if (savedCode) {
toShip(retrofitShip, savedCode); // Populate components from last save
} else {
retrofitShip.buildWith(data.defaults);
}
//this.applyDiscounts();
return {
title: 'Outfitting - ' + data.properties.name,
costTab: Persist.getCostTab() || 'costs',
@@ -57,43 +57,94 @@ export default class OutfittingPage extends Page {
shipId,
ship,
code,
savedCode,
retrofitShip,
retrofitBuild: savedCode ? buildName : null
savedCode
};
}
_applyDiscounts() {
this.state.ship.applyDiscounts(Persist.getShipDiscount(), Persist.getComponentDiscount());
this.state.retrofitShip.applyDiscounts(Persist.getShipDiscount(), Persist.getComponentDiscount());
_buildNameChange(event) {
let stateChanges = {
buildName: event.target.value
}
_buildNameChange() {
if(Persist.hasBuild(this.state.shipId, stateChanges.buildName)) {
stateChanges.savedCode = Persist.getBuild(this.state.shipId, stateChanges.buildName);
} else {
stateChanges.savedCode = null;
}
_saveBuild() {}
this.setState(stateChanges);
}
_reloadBuild() {}
_saveBuild() {
let code = this.state.ship.toString();
Persist.saveBuild(this.state.shipId, this.state.buildName, code);
this.setState({ code, savedCode: code});
}
_resetBuild() {}
_reloadBuild() {
this.state.ship.buildFrom(this.state.savedCode);
this._shipUpdated();
}
_exportBuild() {}
_resetBuild() {
this.state.ship.buildWith(Ships[this.state.shipId].defaults);
this._shipUpdated();
}
_deleteBuild() {
Persist.deleteBuild(this.state.shipId, this.state.buildName);
Router.go(`/outfit/${this.state.shipId}`);
}
_exportBuild() {
let translate = this.context.language.translate;
let {buildName, ship } = this.state;
InterfaceEvents.showModal(<ModalExport
title={buildName + ' ' + translate('export')}
description={translate('PHRASE_EXPORT_DESC')}
data={toDetailedBuild(buildName, ship, ship.toString())}
/>);
}
_shipUpdated() {
let { shipId, buildName } = this.state;
let newCode = this.state.ship.toString();
this._updateRoute(shipId, newCode, buildName);
this.setState({ code: newCode });
}
_updateRoute(shipId, code, buildName) {
let qStr = '';
if (buildName) {
qStr = '?bn=' + encodeURIComponent(buildName);
}
Router.replace(`/outfit/${shipId}/${code}${qStr}`);
}
componentWillReceiveProps(nextProps, nextContext) {
this.setState(this._initState(nextProps, nextContext));
if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
this.setState(this._initState(nextContext));
}
}
render() {
let translate = this.context.language.translate;
let { ship, code, savedCode, buildName } = this.state;
let { translate, units } = this.context.language;
let state = this.state;
let { ship, code, savedCode, buildName } = state;
let menu = this.props.currentMenu;
let shipUpdated = this._shipUpdated;
let hStr = ship.getHardpointsString();
let sStr = ship.getStandardString();
let iStr = ship.getInternalString();
return (
<div id='outfit' className={'page'}>
<div id='overview'>
<h1>{ship.name}</h1>
<div id='build'>
<input value={buildName} onChange={this._buildNameChange} placeholder={translate('enter name')} maxsize={50} />
<input value={buildName} onChange={this._buildNameChange} placeholder={translate('Enter Name')} maxsize={50} />
<button onClick={this._saveBuild} disabled={!buildName || savedCode && code == savedCode}>
<FloppyDisk className='lg'/><span className='button-lbl'>{translate('save')}</span>
</button>
@@ -112,20 +163,65 @@ export default class OutfittingPage extends Page {
</div>
</div>
<ShipSummaryTable ship={ship} />
<ShipSummaryTable ship={ship} code={code} />
<StandardSlotSection ship={ship} />
<InternalSlotSection ship={ship} />
<HardpointsSlotSection ship={ship} />
<UtilitySlotSection ship={ship} />
<StandardSlotSection ship={ship} code={sStr} onChange={shipUpdated} currentMenu={menu} />
<InternalSlotSection ship={ship} code={iStr} onChange={shipUpdated} currentMenu={menu} />
<HardpointsSlotSection ship={ship} code={hStr} onChange={shipUpdated} currentMenu={menu} />
<UtilitySlotSection ship={ship} code={hStr} onChange={shipUpdated} currentMenu={menu} />
Component Priorities & Power
<PowerManagement ship={ship} code={code} onChange={shipUpdated} />
<CostSection ship={ship} shipId={state.shipId} buildName={buildName} code={sStr + hStr + iStr} />
Cost/Pricing List
<div className='group third'>
<h1>{translate('jump range')}</h1>
<LineChart
xMax={ship.cargoCapacity}
yMax={ship.unladenRange}
xUnit={units.T}
yUnit={units.LY}
yLabel={translate('jump range')}
xLabel={translate('cargo')}
func={state.jumpRangeChartFunc}
/>
</div>
<div className='group third'>
<h1>{translate('total range')}</h1>
<LineChart
xMax={ship.cargoCapacity}
yMax={ship.boostSpeed}
xUnit={units.T}
yUnit={units.ms}
yLabel={translate('total range')}
xLabel={translate('cargo')}
func={state.totalRangeChartFunc}
/>
</div>
<div className='group third'>
<h1>{translate('speed')}</h1>
<LineChart
xMax={ship.cargoCapacity}
yMax={ship.boostSpeed}
xUnit={units.T}
yUnit={units.ms}
yLabel={translate('speed')}
series={SPEED_SERIES}
color={SPEED_COLORS}
xLabel={translate('cargo')}
func={state.speedChartFunc}
/>
</div>
{/*
<div class='group half'>
<div slider max='ship.fuelCapacity' unit=''T'' on-change='::fuelChange(val)' style='position:relative; margin: 0 auto;'>
<svg class='icon xl primary-disabled' style='position:absolute;height: 100%;'><use xlink:href='#fuel'></use></svg>
</div>
</div>
*/}
Jump Range Chart
Total Range Chart
Speed Chart
</div>
);
}

View File

@@ -8,12 +8,16 @@ export default class Page extends React.Component {
language: React.PropTypes.object.isRequired
};
static propTypes = {
currentMenu: React.PropTypes.any
};
constructor(props) {
super(props);
// Autobind private functions
Object.getOwnPropertyNames(this.constructor.prototype).forEach(prop => {
if(prop.charAt(0) == '_' && typeof this[prop] === "function") {
if(prop.charAt(0) == '_' && typeof this[prop] === 'function') {
this[prop] = this[prop].bind(this);
}
});

View File

@@ -1,6 +1,6 @@
import React from 'react';
import Page from './Page';
import Ships from '../shipyard/Ships';
import { Ships } from 'coriolis-data';
import cn from 'classnames';
import Ship from '../shipyard/Ship';
import * as ModuleUtils from '../shipyard/ModuleUtils';
@@ -19,6 +19,8 @@ function countInt(slot) {
this.maxCargo += crEligible ? ModuleUtils.findInternal('cr', slot.maxClass, 'E').capacity : 0;
}
let cachedShipSummaries = null;
export default class ShipyardPage extends Page {
constructor(props, context) {
@@ -26,16 +28,20 @@ export default class ShipyardPage extends Page {
this.state = {
title: 'Coriolis - Shipyard',
shipPredicate: 'name',
shipDesc: false
shipDesc: true
};
this.context = context;
this.shipSummaries = [];
if (!cachedShipSummaries) {
cachedShipSummaries = [];
for (let s in Ships) {
this.shipSummaries.push(this._shipSummary(s, Ships[s]));
cachedShipSummaries.push(this._shipSummary(s, Ships[s]));
}
}
this.shipSummaries = cachedShipSummaries;
}
/**
* Sort ships
* @param {object} key Sort predicate
@@ -43,7 +49,7 @@ export default class ShipyardPage extends Page {
_sortShips(shipPredicate, shipPredicateIndex) {
let shipDesc = this.state.shipDesc;
if (typeof shipPredicateIndex == "object") {
if (typeof shipPredicateIndex == 'object') {
shipPredicateIndex = undefined;
}
@@ -55,11 +61,6 @@ export default class ShipyardPage extends Page {
};
_shipSummary(shipId, shipData) {
let language = this.context.language;
let u = language.units;
let fInt = language.formats.int;
let fRound = language.formats.round;
let translate = language.translate;
let summary = {
id: shipId,
hpCount: 0,
@@ -81,24 +82,23 @@ export default class ShipyardPage extends Page {
ship.optimizeMass({ th: ship.standard[1].maxClass + 'A' }); // Optmize mass with Max Thrusters
summary.topSpeed = ship.topSpeed;
summary.topBoost = ship.topBoost;
summary.rowElement = this._shipRowElement(summary, translate, u, fInt, fRound);
return summary;
}
_shipRowElement(s, translate, u, fInt, fRound) {
return <tr key={s.id} className={'highlight'}>
<td className={'le'}><Link href={'/outfitting/' + s.id}>{s.name}</Link></td>
<td className={'le'}>{s.manufacturer}</td>
<td className={'cap'}>{translate(SizeMap[s.class])}</td>
<td className={'ri'}>{fInt(s.speed)}{u.ms}</td>
<td className={'ri'}>{fInt(s.boost)}{u.ms}</td>
<td className={'ri'}>{s.baseArmour}</td>
<td className={'ri'}>{fInt(s.baseShieldStrength)}{u.MJ}</td>
<td className={'ri'}>{fInt(s.topSpeed)}{u.ms}</td>
<td className={'ri'}>{fInt(s.topBoost)}{u.ms}</td>
<td className={'ri'}>{fRound(s.maxJumpRange)}{u.LY}</td>
<td className={'ri'}>{fInt(s.maxCargo)}{u.T}</td>
return <tr key={s.id} className='highlight'>
<td className='le'><Link href={'/outfit/' + s.id}>{s.name}</Link></td>
<td className='le'>{s.manufacturer}</td>
<td className='cap'>{translate(SizeMap[s.class])}</td>
<td className='ri'>{fInt(s.speed)}{u.ms}</td>
<td className='ri'>{fInt(s.boost)}{u.ms}</td>
<td className='ri'>{s.baseArmour}</td>
<td className='ri'>{fInt(s.baseShieldStrength)}{u.MJ}</td>
<td className='ri'>{fInt(s.topSpeed)}{u.ms}</td>
<td className='ri'>{fInt(s.topBoost)}{u.ms}</td>
<td className='ri'>{fRound(s.maxJumpRange)}{u.LY}</td>
<td className='ri'>{fInt(s.maxCargo)}{u.T}</td>
<td className={cn({ disabled: !s.hp[1] })}>{s.hp[1]}</td>
<td className={cn({ disabled: !s.hp[2] })}>{s.hp[2]}</td>
<td className={cn({ disabled: !s.hp[3] })}>{s.hp[3]}</td>
@@ -112,31 +112,16 @@ export default class ShipyardPage extends Page {
<td className={cn({ disabled: !s.int[5] })}>{s.int[5]}</td>
<td className={cn({ disabled: !s.int[6] })}>{s.int[6]}</td>
<td className={cn({ disabled: !s.int[7] })}>{s.int[7]}</td>
<td className={'ri'}>{fInt(s.hullMass)}{u.T}</td>
<td className={'ri'}>{s.masslock}</td>
<td className={'ri'}>{fInt(s.retailCost)}{u.CR}</td>
<td className='ri'>{fInt(s.hullMass)}{u.T}</td>
<td className='ri'>{s.masslock}</td>
<td className='ri'>{fInt(s.retailCost)}{u.CR}</td>
</tr>;
}
_renderSummaries(language) {
let fInt = language.formats.int;
let fRound = language.formats.round;
let translate = language.translate;
let u = language.units;
// Regenerate ship rows on prop change
for (let s of this.shipSummaries) {
s.rowElement = this._shipRowElement(s, translate, u, fInt, fRound);
}
}
componentWillUpdate(nextProps, nextState, nextContext) {
if (this.context.language !== nextContext.language) {
this._renderSummaries(language);
}
}
render() {
let translate = this.context.language.translate;
let { translate, formats, units } = this.context.language;
let fInt = formats.int;
let fRound = formats.round;
let shipSummaries = this.shipSummaries;
let shipPredicate = this.state.shipPredicate;
let shipPredicateIndex = this.state.shipPredicateIndex;
@@ -172,52 +157,52 @@ export default class ShipyardPage extends Page {
});
for (let s of shipSummaries) {
shipRows.push(s.rowElement);
shipRows.push(this._shipRowElement(s, translate, units, fInt, fRound));
}
return (
<div className={'page'}>
<div className={'scroll-x'}>
<div className='page'>
<div className='scroll-x'>
<table style={{ fontSize:'0.85em', whiteSpace:'nowrap', margin: '0 auto' }} align='center'>
<thead>
<tr className={'main'}>
<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>
<tr className='main'>
<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 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'} onClick={sortShips('retailCost')}>{translate('cost')}</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' onClick={sortShips('retailCost')}>{translate('cost')}</th>
</tr>
<tr>
{/* Base */}
<th className={'sortable lft'} onClick={sortShips('speed')}>{translate('speed')}</th>
<th className={'sortable'} onClick={sortShips('boost')}>{translate('boost')}</th>
<th className={'sortable'} onClick={sortShips('baseArmour')}>{translate('armour')}</th>
<th className={'sortable'} onClick={sortShips('baseShieldStrength')}>{translate('shields')}</th>
<th className='sortable lft' onClick={sortShips('speed')}>{translate('speed')}</th>
<th className='sortable' onClick={sortShips('boost')}>{translate('boost')}</th>
<th className='sortable' onClick={sortShips('baseArmour')}>{translate('armour')}</th>
<th className='sortable' onClick={sortShips('baseShieldStrength')}>{translate('shields')}</th>
{/* Max */}
<th className={'sortable lft'} onClick={sortShips('topSpeed')}>{translate('speed')}</th>
<th className={'sortable'} onClick={sortShips('topBoost')}>{translate('boost')}</th>
<th className={'sortable'} onClick={sortShips('maxJumpRange')}>{translate('jump')}</th>
<th className={'sortable'} onClick={sortShips('maxCargo')}>{translate('cargo')}</th>
<th className='sortable lft' onClick={sortShips('topSpeed')}>{translate('speed')}</th>
<th className='sortable' onClick={sortShips('topBoost')}>{translate('boost')}</th>
<th className='sortable' onClick={sortShips('maxJumpRange')}>{translate('jump')}</th>
<th className='sortable' onClick={sortShips('maxCargo')}>{translate('cargo')}</th>
{/* Hardpoints */}
<th className={'sortable lft'} onClick={sortShips('hp',1)}>{translate('S')}</th>
<th className={'sortable'} onClick={sortShips('hp', 2)}>{translate('M')}</th>
<th className={'sortable'} onClick={sortShips('hp', 3)}>{translate('L')}</th>
<th className={'sortable'} onClick={sortShips('hp', 4)}>{translate('H')}</th>
<th className={'sortable'} onClick={sortShips('hp', 0)}>{translate('U')}</th>
<th className='sortable lft' onClick={sortShips('hp',1)}>{translate('S')}</th>
<th className='sortable' onClick={sortShips('hp', 2)}>{translate('M')}</th>
<th className='sortable' onClick={sortShips('hp', 3)}>{translate('L')}</th>
<th className='sortable' onClick={sortShips('hp', 4)}>{translate('H')}</th>
<th className='sortable' onClick={sortShips('hp', 0)}>{translate('U')}</th>
{/* Internal */}
<th className={'sortable lft'} onClick={sortShips('int', 0)} >1</th>
<th className={'sortable'} onClick={sortShips('int', 1)} >2</th>
<th className={'sortable'} onClick={sortShips('int', 2)} >3</th>
<th className={'sortable'} onClick={sortShips('int', 3)} >4</th>
<th className={'sortable'} onClick={sortShips('int', 4)} >5</th>
<th className={'sortable'} onClick={sortShips('int', 5)} >6</th>
<th className={'sortable'} onClick={sortShips('int', 6)} >7</th>
<th className={'sortable'} onClick={sortShips('int', 7)} >8</th>
<th className='sortable lft' onClick={sortShips('int', 0)} >1</th>
<th className='sortable' onClick={sortShips('int', 1)} >2</th>
<th className='sortable' onClick={sortShips('int', 2)} >3</th>
<th className='sortable' onClick={sortShips('int', 3)} >4</th>
<th className='sortable' onClick={sortShips('int', 4)} >5</th>
<th className='sortable' onClick={sortShips('int', 5)} >6</th>
<th className='sortable' onClick={sortShips('int', 6)} >7</th>
<th className='sortable' onClick={sortShips('int', 7)} >8</th>
</tr>
</thead>
<tbody>

View File

@@ -1,641 +0,0 @@
angular.module('app').controller('OutfitController', ['$window', '$rootScope', '$scope', '$state', '$stateParams', '$translate', 'ShipsDB', 'Ship', 'Components', 'Serializer', 'Persist', 'calcTotalRange', 'calcSpeed', function($window, $rootScope, $scope, $state, $p, $translate, Ships, Ship, Components, Serializer, Persist, calcTotalRange, calcSpeed) {
var win = angular.element($window); // Angularized window object for event triggering
var data = Ships[$p.shipId]; // Retrieve the basic ship properties, slots and defaults
var ship = new Ship($p.shipId, data.properties, data.slots); // Create a new Ship instance
var retrofitShip = new Ship($p.shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison
// Update the ship instance with the code (if provided) or the 'factory' defaults.
if ($p.code) {
Serializer.toShip(ship, $p.code); // Populate components from 'code' URL param
$scope.code = $p.code;
} else {
ship.buildWith(data.defaults); // Populate with default components
}
$scope.buildName = $p.bn ? $window.decodeURIComponent($p.bn) : null;
$scope.ships = Ships;
$rootScope.title = ship.name + ($scope.buildName ? ' - ' + $scope.buildName : '');
$scope.ship = ship;
$scope.pp = ship.standard[0]; // Power Plant
$scope.th = ship.standard[1]; // Thruster
$scope.fsd = ship.standard[2]; // Frame Shrift Drive
$scope.ls = ship.standard[3]; // Life Support
$scope.pd = ship.standard[4]; // Power Distributor
$scope.ss = ship.standard[5]; // Sensors
$scope.ft = ship.standard[6]; // Fuel Tank
$scope.hps = ship.hardpoints;
$scope.internal = ship.internal;
$scope.costList = ship.costList;
$scope.powerList = ship.powerList;
$scope.priorityBands = ship.priorityBands;
$scope.availCS = ship.getAvailableComponents();
$scope.selectedSlot = null;
$scope.savedCode = Persist.getBuild(ship.id, $scope.buildName);
$scope.canSave = Persist.isEnabled();
$scope.allBuilds = Persist.builds;
$scope.fuel = 0;
$scope.pwrDesc = false;
$scope.pwrPredicate = 'type';
$scope.retroDesc = false;
$scope.retroPredicate = 'netCost';
$scope.costDesc = true;
$scope.costPredicate = 'c.cost';
$scope.ammoDesc = true;
$scope.ammoPredicate = 'ammoUnitCost';
$scope.costTab = Persist.getCostTab() || 'costs';
if ($scope.savedCode) {
Serializer.toShip(retrofitShip, $scope.savedCode); // Populate components from last save
$scope.retrofitBuild = $scope.buildName;
} else {
retrofitShip.buildWith(data.defaults);
$scope.retrofitBuild = null;
}
ship.applyDiscounts($rootScope.discounts.ship, $rootScope.discounts.components);
retrofitShip.applyDiscounts($rootScope.discounts.ship, $rootScope.discounts.components);
updateRetrofitCosts();
$scope.jrSeries = {
xMin: 0,
xMax: ship.cargoCapacity,
yMax: ship.unladenRange,
yMin: 0,
func: function(cargo) { // X Axis is Cargo
return ship.getJumpRangeForMass(ship.unladenMass + $scope.fuel + cargo, $scope.fuel);
}
};
$scope.jrChart = {
labels: {
xAxis: {
title: 'cargo',
unit: 'T'
},
yAxis: {
title: 'jump range',
unit: 'LY'
}
}
};
$scope.trSeries = {
xMin: 0,
xMax: ship.cargoCapacity,
yMax: ship.unladenTotalRange,
yMin: 0,
func: function(cargo) { // X Axis is Cargo
return calcTotalRange(ship.unladenMass + cargo, $scope.fsd.c, $scope.fuel);
}
};
$scope.trChart = {
labels: {
xAxis: {
title: 'cargo',
unit: 'T'
},
yAxis: {
title: 'total range',
unit: 'LY'
}
}
};
$scope.speedSeries = {
xMin: 0,
xMax: ship.cargoCapacity,
yMax: calcSpeed(ship.unladenMass, ship.speed, ship.boost, $scope.th.c, ship.pipSpeed).boost,
yMin: 0,
series: ['boost', '4 Pips', '2 Pips', '0 Pips'],
colors: ['#0088d2', '#ff8c0d', '#D26D00', '#c06400'],
func: function(cargo) { // X Axis is Cargo
return calcSpeed(ship.unladenMass + $scope.fuel + cargo, ship.speed, ship.boost, $scope.th.c, ship.pipSpeed);
}
};
$scope.speedChart = {
labels: {
xAxis: {
title: 'cargo',
unit: 'T'
},
yAxis: {
title: 'speed',
unit: 'm/s'
}
}
};
/**
* 'Opens' a select for component selection.
*
* @param {[type]} e The event object
* @param {[type]} slot The slot that is being 'opened' for selection
*/
$scope.selectSlot = function(e, slot) {
e.stopPropagation();
if ($scope.selectedSlot == slot) {
$scope.selectedSlot = null;
} else {
$scope.selectedSlot = slot;
}
};
/**
* Updates the ships build with the selected component for the
* specified slot. Prevents the click event from propagation.
*
* @param {string} type Shorthand key/string for identifying the slot & component type
* @param {[type]} slot The slot object belonging to the ship instance
* @param {[type]} e The event object
*/
$scope.select = function(type, slot, e, id) {
e.stopPropagation();
if (!id) { // Find component id if not passed
var elem = e.target;
while (elem && elem !== e.currentTarget && !elem.getAttribute('cpid')) {
elem = elem.parentElement;
}
if (elem) {
id = elem.getAttribute('cpid');
}
}
if (id) {
if (id == 'empty') {
ship.use(slot, null, null);
} else if (type == 'h') {
ship.use(slot, id, Components.hardpoints(id));
} else if (type == 'c') {
ship.use(slot, id, Components.standard(ship.standard.indexOf(slot), id));
} else if (type == 'i') {
ship.use(slot, id, Components.internal(id));
} else if (type == 'b') {
ship.useBulkhead(id);
}
$scope.selectedSlot = null;
updateState(Serializer.fromShip(ship));
}
};
/**
* Reload the build from the last save.
*/
$scope.reloadBuild = function() {
if ($scope.buildName && $scope.savedCode) {
Serializer.toShip(ship, $scope.savedCode); // Repopulate with components from last save
updateState($scope.savedCode);
}
};
$scope.resetBuild = function() {
ship.buildWith(data.defaults); // Populate with default components
updateState(null);
};
/**
* Optimize for the lower mass build that can still boost and power the ship
* without power management.
*/
$scope.optimizeMassBuild = function() {
updateState(Serializer.fromShip(ship.optimizeMass()));
};
/**
* Optimize for the lower mass build that can still boost and power the ship
* without power management.
*/
$scope.optimizeStandard = function() {
updateState(Serializer.fromShip(ship.useLightestStandard()));
};
$scope.useStandard = function(rating) {
updateState(Serializer.fromShip(ship.useStandard(rating)));
};
$scope.useHardpoint = function(group, mount, clobber, missile) {
updateState(Serializer.fromShip(ship.useWeapon(group, mount, clobber, missile)));
};
$scope.useUtility = function(group, rating, clobber) {
updateState(Serializer.fromShip(ship.useUtility(group, rating, clobber)));
};
$scope.emptyInternal = function() {
updateState(Serializer.fromShip(ship.emptyInternal()));
};
$scope.emptyHardpoints = function() {
updateState(Serializer.fromShip(ship.emptyWeapons()));
};
$scope.emptyUtility = function() {
updateState(Serializer.fromShip(ship.emptyUtility()));
};
$scope.fillWithCargo = function() {
ship.internal.forEach(function(slot) {
var id = Components.findInternalId('cr', slot.maxClass, 'E');
if (!slot.c) {
ship.use(slot, id, Components.internal(id));
}
});
updateState(Serializer.fromShip(ship));
};
$scope.fillWithCells = function() {
var chargeCap = 0; // Capacity of single activation
ship.internal.forEach(function(slot) {
var id = Components.findInternalId('scb', slot.maxClass, 'A');
if (!slot.c && (!slot.eligible || slot.eligible.scb)) { // Check eligibility because of Orca, don't overwrite generator
ship.use(slot, id, Components.internal(id));
chargeCap += Components.internal(id).recharge;
ship.setSlotEnabled(slot, chargeCap <= ship.shieldStrength); // Don't waste cell capacity on overcharge
}
});
updateState(Serializer.fromShip(ship));
};
$scope.fillWithArmor = function() {
ship.internal.forEach(function(slot) {
var hr = Components.findInternal('hr', Math.min(slot.maxClass, 5), 'D'); // Hull reinforcements top out at 5D
if (!slot.c && hr) {
ship.use(slot, hr.id, hr);
}
});
updateState(Serializer.fromShip(ship));
};
/**
* Fill all internal slots with Cargo Racks, and optmize internal components.
* Hardpoints are not altered.
*/
$scope.optimizeCargo = function() {
ship.internal.forEach(function(slot) {
var id = Components.findInternalId('cr', slot.maxClass, 'E');
ship.use(slot, id, Components.internal(id));
});
ship.useLightestStandard();
updateState(Serializer.fromShip(ship));
};
/**
* Optimize standard and internal components, hardpoints for exploration
*/
$scope.optimizeExplorer = function() {
var intLength = ship.internal.length,
heatSinkCount = 2, // Fit 2 heat sinks if possible
afmUnitCount = 2, // Fit 2 AFM Units if possible
sgSlot,
fuelScoopSlot,
sgId = $scope.availCS.lightestShieldGenerator(ship.hullMass),
sg = Components.internal(sgId);
ship.setSlotEnabled(ship.cargoHatch, false)
.use(ship.internal[--intLength], '2f', Components.internal('2f')) // Advanced Discovery Scanner
.use(ship.internal[--intLength], '2i', Components.internal('2i')); // Detailed Surface Scanner
for (var i = 0; i < intLength; i++) {
var slot = ship.internal[i];
var nextSlot = (i + 1) < intLength ? ship.internal[i + 1] : null;
if (!fuelScoopSlot && (!slot.eligible || slot.eligible.fs)) { // Fit best possible Fuel Scoop
var fuelScoopId = Components.findInternalId('fs', slot.maxClass, 'A');
fuelScoopSlot = slot;
ship.use(fuelScoopSlot, fuelScoopId, Components.internal(fuelScoopId));
ship.setSlotEnabled(fuelScoopSlot, true);
// Mount a Shield generator if possible AND an AFM Unit has been mounted already (Guarantees at least 1 AFM Unit)
} else if (!sgSlot && afmUnitCount < 2 && sg.class <= slot.maxClass && (!slot.eligible || slot.eligible.sg) && (!nextSlot || nextSlot.maxClass < sg.class)) {
sgSlot = slot;
ship.use(sgSlot, sgId, sg);
ship.setSlotEnabled(sgSlot, true);
} else if (afmUnitCount > 0 && (!slot.eligible || slot.eligible.am)) {
afmUnitCount--;
var am = Components.findInternal('am', slot.maxClass, afmUnitCount ? 'B' : 'A');
ship.use(slot, am.id, am);
ship.setSlotEnabled(slot, false); // Disabled power for AFM Unit
} else {
ship.use(slot, null, null);
}
}
ship.hardpoints.forEach(function(s) {
if (s.maxClass == 0 && heatSinkCount) { // Mount up to 2 heatsinks
ship.use(s, '02', Components.hardpoints('02'));
ship.setSlotEnabled(s, heatSinkCount == 2); // Only enable a single Heatsink
heatSinkCount--;
} else {
ship.use(s, null, null);
}
});
if (sgSlot) {
// The SG and Fuel scoop to not need to be powered at the same time
if (sgSlot.c.power > fuelScoopSlot.c.power) { // The Shield generator uses the most power
ship.setSlotEnabled(fuelScoopSlot, false);
} else { // The Fuel scoop uses the most power
ship.setSlotEnabled(sgSlot, false);
}
}
ship.useLightestStandard({ pd: '1D', ppRating: 'A' });
updateState(Serializer.fromShip(ship));
};
/**
* Save the current build. Will replace the saved build if there is one
* for this ship & with the exact name.
*/
$scope.saveBuild = function() {
if (!$scope.buildName) {
return;
}
// No change hav been made, i.e. save ship default build under a name
if (!$scope.code) {
$scope.code = Serializer.fromShip(ship);
}
// Only save if there a build name and a change has been made or the build has never been saved
if ($scope.code != $scope.savedCode) {
Persist.saveBuild(ship.id, $scope.buildName, $scope.code);
$scope.savedCode = $scope.code;
if ($scope.retrofitBuild === $scope.buildName) {
Serializer.toShip(retrofitShip, $scope.code);
}
updateState($scope.code);
}
};
/**
* Export the build to detailed JSON
*/
$scope.exportBuild = function(e) {
e.stopPropagation();
if ($scope.buildName) {
$state.go('modal.export', {
title: $scope.buildName + ' ' + $translate.instant('export'),
description: $translate.instant('PHRASE_EXPORT_DESC'),
data: Serializer.toDetailedBuild($scope.buildName, ship, $scope.code || Serializer.fromShip(ship))
});
}
};
/**
* Permanently delete the current build and redirect/reload this controller
* with the 'factory' build of the current ship.
*/
$scope.deleteBuild = function() {
Persist.deleteBuild(ship.id, $scope.buildName);
$state.go('outfit', { shipId: ship.id, code: null, bn: null }, { location: 'replace', reload: true });
};
/**
* On build name change, retrieve the existing saved code if there is one
*/
$scope.bnChange = function() {
$scope.savedCode = Persist.getBuild(ship.id, $scope.buildName);
};
/**
* Toggle cost of the selected component
* @param {object} item The component being toggled
*/
$scope.toggleCost = function(item) {
ship.setCostIncluded(item, !item.incCost);
};
/**
* Toggle cost of the selected component for retrofitting comparison
* @param {object} item The component being toggled
*/
$scope.toggleRetrofitCost = function(item) {
retrofitShip.setCostIncluded(item, !item.incCost);
updateRetrofitCosts();
};
/**
* [sortCost description]
* @param {[type]} key [description]
* @return {[type]} [description]
*/
$scope.sortCost = function(key) {
$scope.costDesc = $scope.costPredicate == key ? !$scope.costDesc : $scope.costDesc;
$scope.costPredicate = key;
};
$scope.sortPwr = function(key) {
$scope.pwrDesc = $scope.pwrPredicate == key ? !$scope.pwrDesc : $scope.pwrDesc;
$scope.pwrPredicate = key;
};
$scope.sortRetrofit = function(key) {
$scope.retroDesc = $scope.retroPredicate == key ? !$scope.retroDesc : $scope.retroDesc;
$scope.retroPredicate = key;
};
$scope.sortAmmo = function(key) {
$scope.ammoDesc = $scope.ammoPredicate == key ? !$scope.ammoDesc : $scope.ammoDesc;
$scope.ammoPredicate = key;
};
/**
* Toggle the power on/off for the selected component
* @param {object} item The component being toggled
*/
$scope.togglePwr = function(c) {
ship.setSlotEnabled(c, !c.enabled);
updateState(Serializer.fromShip(ship));
};
$scope.incPriority = function(c) {
if (ship.changePriority(c, c.priority + 1)) {
updateState(Serializer.fromShip(ship));
}
};
$scope.decPriority = function(c) {
if (ship.changePriority(c, c.priority - 1)) {
updateState(Serializer.fromShip(ship));
}
};
$scope.fuelChange = function(fuel) {
$scope.fuel = fuel;
updateAmmoCosts();
win.triggerHandler('render');
};
$scope.statusRetracted = function(slot) {
return ship.getSlotStatus(slot, false);
};
$scope.statusDeployed = function(slot) {
return ship.getSlotStatus(slot, true);
};
$scope.setRetrofitBase = function() {
if ($scope.retrofitBuild) {
Serializer.toShip(retrofitShip, Persist.getBuild(ship.id, $scope.retrofitBuild));
} else {
retrofitShip.buildWith(data.defaults);
}
updateRetrofitCosts();
};
$scope.updateCostTab = function(tab) {
Persist.setCostTab(tab);
$scope.costTab = tab;
};
$scope.ppWarning = function(pp) {
return pp.pGen < ship.powerRetracted;
};
$scope.pdWarning = function(pd) {
return pd.enginecapacity < ship.boostEnergy;
};
// Utilify functions
function updateState(code) {
$scope.code = code;
$state.go('outfit', { shipId: ship.id, code: $scope.code, bn: $scope.buildName }, { location: 'replace', notify: false });
$scope.speedSeries.xMax = $scope.trSeries.xMax = $scope.jrSeries.xMax = ship.cargoCapacity;
$scope.jrSeries.yMax = ship.unladenRange;
$scope.trSeries.yMax = ship.unladenTotalRange;
$scope.speedSeries.yMax = calcSpeed(ship.unladenMass, ship.speed, ship.boost, $scope.th.c, ship.pipSpeed).boost;
updateRetrofitCosts();
win.triggerHandler('pwrchange');
}
function updateRetrofitCosts() {
var costs = $scope.retrofitList = [];
var total = 0, i, l, item;
if (ship.bulkheads.id != retrofitShip.bulkheads.id) {
item = {
buyClassRating: ship.bulkheads.c.class + ship.bulkheads.c.rating,
buyName: ship.bulkheads.c.name,
sellClassRating: retrofitShip.bulkheads.c.class + retrofitShip.bulkheads.c.rating,
sellName: retrofitShip.bulkheads.c.name,
netCost: ship.bulkheads.discountedCost - retrofitShip.bulkheads.discountedCost,
retroItem: retrofitShip.bulkheads
};
costs.push(item);
if (retrofitShip.bulkheads.incCost) {
total += item.netCost;
}
}
for (var g in { standard: 1, internal: 1, hardpoints: 1 }) {
var retroSlotGroup = retrofitShip[g];
var slotGroup = ship[g];
for (i = 0, l = slotGroup.length; i < l; i++) {
if (slotGroup[i].id != retroSlotGroup[i].id) {
item = { netCost: 0, retroItem: retroSlotGroup[i] };
if (slotGroup[i].id) {
item.buyName = slotGroup[i].c.name || slotGroup[i].c.grp;
item.buyClassRating = slotGroup[i].c.class + slotGroup[i].c.rating;
item.netCost = slotGroup[i].discountedCost;
}
if (retroSlotGroup[i].id) {
item.sellName = retroSlotGroup[i].c.name || retroSlotGroup[i].c.grp;
item.sellClassRating = retroSlotGroup[i].c.class + retroSlotGroup[i].c.rating;
item.netCost -= retroSlotGroup[i].discountedCost;
}
costs.push(item);
if (retroSlotGroup[i].incCost) {
total += item.netCost;
}
}
}
}
$scope.retrofitTotal = total;
updateAmmoCosts();
}
function updateAmmoCosts() {
var costs = $scope.ammoList = [];
var total = 0, i, l, item, q, limpets = 0, scoop = false;
for (var g in { standard: 1, internal: 1, hardpoints: 1 }) {
var slotGroup = ship[g];
for (i = 0, l = slotGroup.length; i < l; i++) {
if (slotGroup[i].id) {
//special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip
q = 0;
switch (slotGroup[i].c.grp) {
case 'fs': //skip fuel calculation if scoop present
scoop = true;
break;
case 'scb':
q = slotGroup[i].c.cells;
break;
case 'am':
q = slotGroup[i].c.ammo;
break;
case 'fx': case 'hb': case 'cc': case 'pc':
limpets = ship.cargoCapacity;
break;
default:
q = slotGroup[i].c.clip + slotGroup[i].c.ammo;
}
//calculate ammo costs only if a cost is specified
if (slotGroup[i].c.ammocost > 0) {
item = {
ammoClassRating: slotGroup[i].c.class + slotGroup[i].c.rating,
ammoName: slotGroup[i].c.name || slotGroup[i].c.grp,
ammoMax: q,
ammoUnitCost: slotGroup[i].c.ammocost,
ammoTotalCost: q * slotGroup[i].c.ammocost
};
costs.push(item);
total += item.ammoTotalCost;
}
}
}
}
//limpets if controllers exist and cargo space available
if (limpets > 0) {
item = {
ammoName: 'limpets',
ammoMax: ship.cargoCapacity,
ammoUnitCost: 101,
ammoTotalCost: ship.cargoCapacity * 101
};
costs.push(item);
total += item.ammoTotalCost;
}
//calculate refuel costs if no scoop present
if (!scoop) {
item = {
ammoName: 'fuel',
ammoMax: $scope.fuel,
ammoUnitCost: 50,
ammoTotalCost: $scope.fuel * 50
};
costs.push(item);
total += item.ammoTotalCost;
}
$scope.ammoTotal = total;
}
// Hide any open menu/slot/etc if the background is clicked
$scope.$on('close', function() {
$scope.selectedSlot = null;
});
// Hide any open menu/slot/etc if the background is clicked
$scope.$on('languageChanged', function() {
$scope.selectedSlot = null;
});
// Hide any open menu/slot/etc if the background is clicked
$scope.$on('discountChange', function() {
ship.applyDiscounts($rootScope.discounts.ship, $rootScope.discounts.components);
retrofitShip.applyDiscounts($rootScope.discounts.ship, $rootScope.discounts.components);
updateRetrofitCosts();
});
}]);

View File

@@ -24,6 +24,7 @@ export const ModuleGroupToName = {
fs: 'Fuel Scoop',
sc: 'Scanner',
am: 'Auto Field-Maintenance Unit',
bsg: 'Bi-Weave Shield Generator',
cr: 'Cargo Rack',
fi: 'Frame Shift Drive Interdictor',
hb: 'Hatch Breaker Limpet Controller',
@@ -31,6 +32,7 @@ export const ModuleGroupToName = {
rf: 'Refinery',
scb: 'Shield Cell Bank',
sg: 'Shield Generator',
pv: 'Planetary Vehicle Hanger',
psg: 'Prismatic Shield Generator',
dc: 'Docking Computer',
fx: 'Fuel Transfer Limpet Controller',
@@ -168,7 +170,17 @@ export const ShipFacets = [
fmt: 'fRound'
}
];
/**
/**
* Set of all insurance levels
*/
export const Insurance = {
'standard': 0.05,
'alpha': 0.025,
'beta': 0.0375
};
/**
* Set of all available / theoretical discounts
*/
export const Discounts = {
@@ -179,4 +191,3 @@ export const Discounts = {
'20%': 0.80,
'25%': 0.75
};

View File

@@ -126,11 +126,12 @@ export default class ModuleSet {
return sg;
};
lightestPowerPlant(powerUsed, rating) {
lightestPowerPlant(powerNeeded, rating) {
var pp = this.standard[0][0];
for (let p of this.standard[0]) {
if (p.mass < pp.mass && p.pGen >= powerUsed) {
// Provides enough power, is lighter or the same mass as current power plant but better output/efficiency
if (p.pGen >= powerNeeded && (p.mass < pp.mass || (p.mass == pp.mass && p.pGen > pp.pGen))) {
pp = p;
}
}

View File

@@ -1,14 +1,23 @@
import { ModuleNameToGroup, BulkheadNames } from './Constants';
import ModuleSet from './ModuleSet';
import Ships from './Ships';
import Modules from './Modules';
import { Ships, Modules } from 'coriolis-data';
export function cargoHatch() {
return { name: 'Cargo Hatch', class: 1, rating: 'H', power: 0.6 };
};
export function standard(typeIndex, componentId) {
return Modules.standard[typeIndex][componentId];
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){
return standard[k];
}
}
}
return null;
};
export function hardpoints(id) {
@@ -149,17 +158,26 @@ export function findHardpointId(groupName, clss, rating, name, mount, missile) {
}
/**
* Looks up the bulkhead component for a specific ship and bulkhead
* Looks up the bulkhead module for a specific ship and bulkhead
* @param {string} shipId Unique ship Id/Key
* @param {number} bulkheadsId Id/Index for the specified bulkhead
* @param {string|number} bulkheadsId Id/Index for the specified bulkhead
* @return {object} The bulkhead component object
*/
export function bulkheads(shipId, bulkheadsId) {
return Modules.bulkheads[shipId][bulkheadsId];
export function bulkheads(shipId, index) {
let bulkhead = Ships[shipId].bulkheads[index];
bulkhead.class = 8;
bulkhead.rating = 'I';
bulkhead.name = BulkheadNames[index]
return bulkhead;
}
export function bulkheadIndex(bulkheadName) {
return Bulkheads.indexOf(bulkheadName);
return BulkheadNames.indexOf(bulkheadName);
}
export function isShieldGenerator(g) {
return g == 'sg' || g == 'psg' || g == 'bsg';
}
/**

View File

@@ -1,96 +0,0 @@
import bulkheads from 'coriolis-data/components/bulkheads';
// Standard Modules
import frame_shift_drive from 'coriolis-data/components/standard/frame_shift_drive';
import fuel_tank from 'coriolis-data/components/standard/fuel_tank';
import life_support from 'coriolis-data/components/standard/life_support';
import power_distributor from 'coriolis-data/components/standard/power_distributor';
import power_plant from 'coriolis-data/components/standard/power_plant';
import sensors from 'coriolis-data/components/standard/sensors';
import thrusters from 'coriolis-data/components/standard/thrusters';
// Hardpoints and Utility modules
import { pl } from 'coriolis-data/components/hardpoints/pulse_laser';
import { ul } from 'coriolis-data/components/hardpoints/burst_laser';
import { bl } from 'coriolis-data/components/hardpoints/beam_laser';
import { mc } from 'coriolis-data/components/hardpoints/multi_cannon';
import { c } from 'coriolis-data/components/hardpoints/cannon';
import { fc } from 'coriolis-data/components/hardpoints/fragment_cannon';
import { rg } from 'coriolis-data/components/hardpoints/rail_gun';
import { pa } from 'coriolis-data/components/hardpoints/plasma_accelerator';
import { mr } from 'coriolis-data/components/hardpoints/missile_rack';
import { tp } from 'coriolis-data/components/hardpoints/torpedo_pylon';
import { nl } from 'coriolis-data/components/hardpoints/mine_launcher';
import { ml } from 'coriolis-data/components/hardpoints/mining_laser';
import { cs } from 'coriolis-data/components/hardpoints/cargo_scanner';
import { cm } from 'coriolis-data/components/hardpoints/countermeasures';
import { ws } from 'coriolis-data/components/hardpoints/frame_shift_wake_scanner';
import { kw } from 'coriolis-data/components/hardpoints/kill_warrant_scanner';
import { sb } from 'coriolis-data/components/hardpoints/shield_booster';
// Internal
import { am } from 'coriolis-data/components/internal/auto_field_maintenance_unit';
import { bsg } from 'coriolis-data/components/internal/bi_weave_shield_generator';
import { cr } from 'coriolis-data/components/internal/cargo_rack';
import { cc } from 'coriolis-data/components/internal/collector_limpet_controllers';
import { dc } from 'coriolis-data/components/internal/docking_computer';
import { fi } from 'coriolis-data/components/internal/frame_shift_drive_interdictor';
import { fs } from 'coriolis-data/components/internal/fuel_scoop';
import { fx } from 'coriolis-data/components/internal/fuel_transfer_limpet_controllers';
import { hb } from 'coriolis-data/components/internal/hatch_breaker_limpet_controller';
import { hr } from 'coriolis-data/components/internal/hull_reinforcement_package';
import { ft } from 'coriolis-data/components/internal/internal_fuel_tank';
import { psg } from 'coriolis-data/components/internal/pristmatic_shield_generator';
import { pc } from 'coriolis-data/components/internal/prospector_limpet_controllers';
import { rf } from 'coriolis-data/components/internal/refinery';
import { sc } from 'coriolis-data/components/internal/scanner';
import { scb } from 'coriolis-data/components/internal/shield_cell_bank';
import { sg } from 'coriolis-data/components/internal/shield_generator';
export default {
standard: [
power_plant,
thrusters,
frame_shift_drive,
life_support,
power_distributor,
sensors,
fuel_tank
],
hardpoints: {
pl,
ul,
bl,
mc,
c,
fc,
rg,
pa,
mr,
tp,
nl,
ml,
cs,
cm,
ws,
kw,
sb
},
internal: {
am,
bsg,
cr,
cc,
dc,
fi,
fs,
fx,
hb,
hr,
ft,
psg,
pc,
rf,
sc,
scb,
sg
},
bulkheads
};

View File

@@ -1,5 +1,5 @@
import { ModuleGroupToName, MountMap } from './Constants';
import Ships from './Ships';
import { ModuleGroupToName, MountMap, BulkheadNames } from './Constants';
import { Ships } from 'coriolis-data';
import Ship from './Ship';
import ModuleUtils from './ModuleUtils';
import LZString from 'lz-string';
@@ -8,122 +8,30 @@ import LZString from 'lz-string';
* Service managing seralization and deserialization of models for use in URLs and persistene.
*/
/**
* Utility function to retrieve a safe string for selected component for a slot.
* Used for serialization to code only.
* TODO: comment on binding
* @private
* @param {object} slot The slot object.
* @return {string} The id of the selected component or '-' if none selected
*/
function mapGroup(slot) {
this.enabled.push(slot.enabled ? 1 : 0);
this.priorities.push(slot.priority);
return slot.id === null ? '-' : slot.id;
}
function decodeToArray(code, arr, codePos) {
for (let i = 0; i < arr.length; i++) {
if (code.charAt(codePos) == '-') {
arr[i] = 0;
codePos++;
} else {
arr[i] = code.substring(codePos, codePos + 2);
codePos += 2;
}
}
return codePos;
}
function slotToSchema(slot) {
if (slot.c) {
if (slot.m) {
let o = {
class: slot.c.class,
rating: slot.c.rating,
class: slot.m.class,
rating: slot.m.rating,
enabled: Boolean(slot.enabled),
priority: slot.priority + 1,
group: ModuleGroupToName[slot.c.grp]
group: ModuleGroupToName[slot.m.grp]
};
if (slot.c.name) {
o.name = slot.c.name;
if (slot.m.name) {
o.name = slot.m.name;
}
if (slot.c.mode) {
o.mount = MountMap[slot.c.mode];
if (slot.m.mount) {
o.mount = MountMap[slot.m.mount];
}
if (slot.c.missile) {
o.missile = slot.c.missile;
if (slot.m.missile) {
o.missile = slot.m.missile;
}
return o;
}
return null;
}
/**
* Serializes the ships selected components for all slots to a URL friendly string.
* @param {Ship} ship The ship to be serialized.
* @return {string} Encoded string of components
*/
export function fromShip(ship) {
let power = {
enabled: [ship.cargoHatch.enabled ? 1 : 0],
priorities: [ship.cargoHatch.priority]
};
let data = [
ship.bulkheads.id,
_.map(ship.standard, mapGroup, power),
_.map(ship.hardpoints, mapGroup, power),
_.map(ship.internal, mapGroup, power),
'.',
LZString.compressToBase64(power.enabled.join('')).replace(/\//g, '-'),
'.',
LZString.compressToBase64(power.priorities.join('')).replace(/\//g, '-')
];
return _.flatten(data).join('');
};
/**
* Updates an existing ship instance's slots with components determined by the
* code.
*
* @param {Ship} ship The ship instance to be updated
* @param {string} dataString The string to deserialize
*/
export function toShip(ship, dataString) {
var standard = new Array(ship.standard.length),
hardpoints = new Array(ship.hardpoints.length),
internal = new Array(ship.internal.length),
parts = dataString.split('.'),
priorities = null,
enabled = null,
code = parts[0];
if (parts[1]) {
enabled = LZString.decompressFromBase64(parts[1].replace(/-/g, '/')).split('');
}
if (parts[2]) {
priorities = LZString.decompressFromBase64(parts[2].replace(/-/g, '/')).split('');
}
decodeToArray(code, internal, decodeToArray(code, hardpoints, decodeToArray(code, standard, 1)));
ship.buildWith(
{
bulkheads: code.charAt(0) * 1,
standard: standard,
hardpoints: hardpoints,
internal: internal
},
priorities,
enabled
);
};
export function toDetailedBuild(buildName, ship, code) {
var standard = ship.standard,
hardpoints = ship.hardpoints,
@@ -135,25 +43,25 @@ export function toDetailedBuild(buildName, ship, code) {
ship: ship.name,
references: [{
name: 'Coriolis.io',
url: $state.href('outfit', { shipId: ship.id, code: code, bn: buildName }, { absolute: true }),
url: `http://coriolis.io/outfit/${ship.id}/${code}?bn=${encodeURIComponent(buildName)}`,
code: code,
shipId: ship.id
}],
components: {
standard: {
bulkheads: ship.bulkheads.c.name,
bulkheads: BulkheadNames[ship.bulkheads.index],
cargoHatch: { enabled: Boolean(ship.cargoHatch.enabled), priority: ship.cargoHatch.priority + 1 },
powerPlant: { class: standard[0].c.class, rating: standard[0].c.rating, enabled: Boolean(standard[0].enabled), priority: standard[0].priority + 1 },
thrusters: { class: standard[1].c.class, rating: standard[1].c.rating, enabled: Boolean(standard[1].enabled), priority: standard[1].priority + 1 },
frameShiftDrive: { class: standard[2].c.class, rating: standard[2].c.rating, enabled: Boolean(standard[2].enabled), priority: standard[2].priority + 1 },
lifeSupport: { class: standard[3].c.class, rating: standard[3].c.rating, enabled: Boolean(standard[3].enabled), priority: standard[3].priority + 1 },
powerDistributor: { class: standard[4].c.class, rating: standard[4].c.rating, enabled: Boolean(standard[4].enabled), priority: standard[4].priority + 1 },
sensors: { class: standard[5].c.class, rating: standard[5].c.rating, enabled: Boolean(standard[5].enabled), priority: standard[5].priority + 1 },
fuelTank: { class: standard[6].c.class, rating: standard[6].c.rating, enabled: Boolean(standard[6].enabled), priority: standard[6].priority + 1 }
powerPlant: { class: standard[0].m.class, rating: standard[0].m.rating, enabled: Boolean(standard[0].enabled), priority: standard[0].priority + 1 },
thrusters: { class: standard[1].m.class, rating: standard[1].m.rating, enabled: Boolean(standard[1].enabled), priority: standard[1].priority + 1 },
frameShiftDrive: { class: standard[2].m.class, rating: standard[2].m.rating, enabled: Boolean(standard[2].enabled), priority: standard[2].priority + 1 },
lifeSupport: { class: standard[3].m.class, rating: standard[3].m.rating, enabled: Boolean(standard[3].enabled), priority: standard[3].priority + 1 },
powerDistributor: { class: standard[4].m.class, rating: standard[4].m.rating, enabled: Boolean(standard[4].enabled), priority: standard[4].priority + 1 },
sensors: { class: standard[5].m.class, rating: standard[5].m.rating, enabled: Boolean(standard[5].enabled), priority: standard[5].priority + 1 },
fuelTank: { class: standard[6].m.class, rating: standard[6].m.rating, enabled: Boolean(standard[6].enabled), priority: standard[6].priority + 1 }
},
hardpoints: _.map(_.filter(hardpoints, function(slot) { return slot.maxClass > 0; }), slotToSchema),
utility: _.map(_.filter(hardpoints, function(slot) { return slot.maxClass === 0; }), slotToSchema),
internal: _.map(internal, slotToSchema)
hardpoints: hardpoints.filter(slot => slot.maxClass > 0).map(slotToSchema),
utility: hardpoints.filter(slot => slot.maxClass === 0).map(slotToSchema),
internal: internal.map(slotToSchema)
},
stats: {}
};

View File

@@ -1,8 +1,9 @@
import { ArmourMultiplier } from './Constants';
import * as Calc from './Calculations';
import * as ModuleUtils from './ModuleUtils';
import LZString from 'lz-string';
const UNIQUE_MODULES = ['psg', 'sg', 'rf', 'fs'];
const UNIQUE_MODULES = ['psg', 'sg', 'bsg', 'rf', 'fs'];
/**
* Returns the power usage type of a slot and it's particular modul
@@ -19,6 +20,31 @@ function powerUsageType(slot, modul) {
return slot.cat != 1 ? 'retracted' : 'deployed';
}
function decodeToArray(code, arr, codePos) {
for (let i = 0; i < arr.length; i++) {
if (code.charAt(codePos) == '-') {
arr[i] = 0;
codePos++;
} else {
arr[i] = code.substring(codePos, codePos + 2);
codePos += 2;
}
}
return codePos;
}
/**
* Reduce function used to get the IDs for a slot group (or array of slots)
* @param {array} idArray The current Array of IDs
* @param {object} slot Slot object
* @param {integer} slotIndex The index for the slot in its group
* @return {array} The mutated idArray
*/
function reduceToIDs(idArray, slot, slotIndex) {
idArray[slotIndex] = slot.m ? slot.m.id : '-';
return idArray;
}
/**
* Ship model used to track all ship ModuleUtils and properties.
*/
@@ -31,6 +57,7 @@ export default class Ship {
*/
constructor(id, properties, slots) {
this.id = id;
this.serialized = {};
this.cargoHatch = { m: ModuleUtils.cargoHatch(), type: 'SYS' };
this.bulkheads = { incCost: true, maxClass: 8 };
this.availCS = ModuleUtils.forShip(id);
@@ -49,7 +76,7 @@ export default class Ship {
}
}
// Make a Ship 'slot'/item similar to other slots
this.m = { incCost: true, type: 'SHIP', discountedCost: this.hullCost, m: { name: this.name, cost: this.hullCost } };
this.m = { incCost: true, type: 'SHIP', discountedCost: this.hullCost, m: { class: '', rating: '', name: this.name, cost: this.hullCost } };
this.costList = this.internal.concat(this.m, this.standard, this.hardpoints, this.bulkheads);
this.powerList = this.internal.concat(
this.cargoHatch,
@@ -81,14 +108,26 @@ export default class Ship {
}
canThrust() {
// TODO: check thrusters are powered;
return this.ladenMass < this.standard[1].m.maxmass;
return this.getSlotStatus(this.standard[1]) == 3 // Thrusters are powered
&& this.ladenMass < this.standard[1].m.maxmass; // Max mass not exceeded
}
canBoost() {
return this.canThrust() && this.boostEnergy < this.standard[4].m.enginecapacity;
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
}
/**
* Returns the a slots power status:
* 0 - No status [Blank]
* 1 - Disabled (Switched off)
* 2 - Offline (Insufficient power available)
* 3 - Online
* @param {[type]} slot [description]
* @param {boolean} deployed True - power used when hardpoints are deployed
* @return {number} status index
*/
getSlotStatus(slot, deployed) {
if (!slot.m) { // Empty Slot
return 0; // No Status (Not possible to be active in this state)
@@ -122,13 +161,57 @@ export default class Ship {
*/
findInternalByGroup(group) {
var index;
if (group == 'sg' || group == 'psg') {
return this.internal.find(slot => slot.m && (slot.m.grp == 'sg' || slot.m.grp == 'psg'));
if (ModuleUtils.isShieldGenerator(group)) {
return this.internal.find(slot => slot.m && ModuleUtils.isShieldGenerator(slot.m.grp));
} else {
return this.internal.find(slot => slot.m && slot.m.grp == group);
}
}
toString() {
return [
this.getStandardString(),
this.getHardpointsString(),
this.getInternalString(),
'.',
this.getPowerEnabledString(),
'.',
this.getPowerPrioritesString()
].join('');
}
getStandardString() {
if(!this.serialized.standard) {
this.serialized.standard = this.bulkheads.index + this.standard.reduce((arr, slot, i) => {
arr[i] = slot.m ? slot.m.class + slot.m.rating : '-';
return arr;
}, new Array(this.standard.length)).join('');
}
return this.serialized.standard;
}
getInternalString() {
if(!this.serialized.internal) {
this.serialized.internal = this.internal.reduce(reduceToIDs, new Array(this.internal.length)).join('');
}
return this.serialized.internal;
}
getHardpointsString() {
if(!this.serialized.hardpoints) {
this.serialized.hardpoints = this.hardpoints.reduce(reduceToIDs, new Array(this.hardpoints.length)).join('');
}
return this.serialized.hardpoints;
}
getPowerEnabledString() {
return this.serialized.enabled;
}
getPowerPrioritesString() {
return this.serialized.priorities;
}
//**********************//
// Mutate / Update Ship //
//**********************//
@@ -244,12 +327,51 @@ export default class Ship {
this.updatePower()
.updateJumpStats()
.updateShieldStrength()
.updateTopSpeed();
.updateTopSpeed()
.updatePowerPrioritesString()
.updatePowerEnabledString();
}
return this;
}
/**
* Updates an existing ship instance's slots with modules determined by the
* code.
*
* @param {string} serializedString The string to deserialize
*/
buildFrom(serializedString) {
var standard = new Array(this.standard.length),
hardpoints = new Array(this.hardpoints.length),
internal = new Array(this.internal.length),
parts = serializedString.split('.'),
priorities = null,
enabled = null,
code = parts[0];
if (parts[1]) {
enabled = LZString.decompressFromBase64(parts[1].replace(/-/g, '/')).split('');
}
if (parts[2]) {
priorities = LZString.decompressFromBase64(parts[2].replace(/-/g, '/')).split('');
}
decodeToArray(code, internal, decodeToArray(code, hardpoints, decodeToArray(code, standard, 1)));
this.buildWith(
{
bulkheads: code.charAt(0) * 1,
standard: standard,
hardpoints: hardpoints,
internal: internal
},
priorities,
enabled
);
};
emptyHardpoints() {
for (var i = this.hardpoints.length; i--; ) {
this.use(this.hardpoints[i], null);
@@ -305,7 +427,7 @@ export default class Ship {
if (slot.m) {
this.priorityBands[slot.priority][powerUsageType(slot, slot.m)] += enabled ? slot.m.power : -slot.m.power;
if (slot.m.grp == 'sg' || slot.m.grp == 'psg') {
if (ModuleUtils.isShieldGenerator(slot.m.grp)) {
this.updateShieldStrength();
} else if (slot.m.grp == 'sb') {
this.shieldMultiplier += slot.m.shieldmul * (enabled ? 1 : -1);
@@ -315,13 +437,42 @@ export default class Ship {
}
this.updatePower();
this.updatePowerEnabledString();
}
}
return this;
}
/**
* Updates the ship's cumulative and aggregated stats based on the modul change.
* Will change the priority of the specified slot if the new priority is valid
* @param {object} slot The slot to be updated
* @param {number} newPriority The new priority to be set
* @return {boolean} Returns true if the priority was changed (within range)
*/
setSlotPriority(slot, newPriority) {
if (newPriority >= 0 && newPriority < this.priorityBands.length) {
var oldPriority = slot.priority;
slot.priority = newPriority;
if (slot.enabled) { // Only update power if the slot is enabled
var usage = powerUsageType(slot, slot.m);
this.priorityBands[oldPriority][usage] -= slot.m.power;
this.priorityBands[newPriority][usage] += slot.m.power;
this.updatePowerPrioritesString();
this.updatePower();
}
return true;
}
return false;
}
/**
* Updates the ship's cumulative and aggregated stats based on the module change.
* @param {object} slot The slot being updated
* @param {object} n The new module (may be null)
* @param {object} old The old module (may be null)
* @param {boolean} preventUpdate If true the global ship state will not be updated
* @return {this} The ship instance (for chaining operations)
*/
updateStats(slot, n, old, preventUpdate) {
var powerChange = slot == this.standard[0];
@@ -445,6 +596,39 @@ export default class Ship {
return this;
}
updatePowerPrioritesString() {
let priorities = [this.cargoHatch.priority];
for (let slot of this.standard) {
priorities.push(slot.priority);
}
for (let slot of this.hardpoints) {
priorities.push(slot.priority);
}
for (let slot of this.internal) {
priorities.push(slot.priority);
}
this.serialized.priorities = LZString.compressToBase64(priorities.join('')).replace(/\//g, '-');
return this;
}
updatePowerEnabledString() {
let enabled = [this.cargoHatch.enabled ? 1 : 0];
for (let slot of this.standard) {
enabled.push(slot.enabled ? 1 : 0);
}
for (let slot of this.hardpoints) {
enabled.push(slot.enabled ? 1 : 0);
}
for (let slot of this.internal) {
enabled.push(slot.enabled ? 1 : 0);
}
this.serialized.enabled = LZString.compressToBase64(enabled.join('')).replace(/\//g, '-');
return this;
}
/**
* Update a slot with a the modul if the id is different from the current id for this slot.
@@ -472,6 +656,12 @@ export default class Ship {
slot.m = m;
slot.discountedCost = (m && m.cost) ? m.cost * this.modulCostMultiplier : 0;
this.updateStats(slot, m, oldModule, preventUpdate);
switch (slot.cat) {
case 0: this.serialized.standard = null; break;
case 1: this.serialized.hardpoints = null; break;
case 2: this.serialized.internal = null;
}
}
return this;
}
@@ -484,12 +674,12 @@ export default class Ship {
*/
useBulkhead(index, preventUpdate) {
var oldBulkhead = this.bulkheads.m;
this.bulkheads.id = index;
this.bulkheads.index = index;
this.bulkheads.m = ModuleUtils.bulkheads(this.id, index);
this.bulkheads.discountedCost = this.bulkheads.m.cost * this.modulCostMultiplier;
this.armourMultiplier = ArmourMultiplier[index];
this.updateStats(this.bulkheads, this.bulkheads.m, oldBulkhead, preventUpdate);
this.serialized.standard = null;
return this;
}
@@ -549,52 +739,31 @@ export default class Ship {
return this;
}
useUtility(group, rating, clobber) {
var modul = ModuleUtils.findHardpoint(group, 0, rating);
for (var i = this.hardpoints.length; i--; ) {
useUtility(group, rating, name, clobber) {
let m = ModuleUtils.findHardpoint(group, 0, rating, name);
for (let i = this.hardpoints.length; i--; ) {
if ((clobber || !this.hardpoints[i].m) && !this.hardpoints[i].maxClass) {
this.use(this.hardpoints[i], modul);
this.use(this.hardpoints[i], m);
}
}
return this;
}
useWeapon(group, mount, clobber, missile) {
var hps = this.hardpoints;
for (var i = hps.length; i--; ) {
useWeapon(group, mount, missile, clobber) {
let hps = this.hardpoints;
for (let i = hps.length; i--; ) {
if (hps[i].maxClass) {
var size = hps[i].maxClass, modul;
let size = hps[i].maxClass, m;
do {
modul = ModuleUtils.findHardpoint(group, size, null, null, mount, missile);
if ((clobber || !hps[i].m) && modul) {
this.use(hps[i], modul);
m = ModuleUtils.findHardpoint(group, size, null, null, mount, missile);
if ((clobber || !hps[i].m) && m) {
this.use(hps[i], m);
break;
}
} while (!modul && (--size > 0));
} while (!m && (--size > 0));
}
}
return this;
}
/**
* Will change the priority of the specified slot if the new priority is valid
* @param {object} slot The slot to be updated
* @param {number} newPriority The new priority to be set
* @return {boolean} Returns true if the priority was changed (within range)
*/
changePriority(slot, newPriority) {
if (newPriority >= 0 && newPriority < this.priorityBands.length) {
var oldPriority = slot.priority;
slot.priority = newPriority;
if (slot.enabled) { // Only update power if the slot is enabled
var usage = powerUsageType(slot, slot.m);
this.priorityBands[oldPriority][usage] -= slot.m.power;
this.priorityBands[newPriority][usage] += slot.m.power;
this.updatePower();
}
return true;
}
return false;
}
}

View File

@@ -1,59 +0,0 @@
import { adder } from 'coriolis-data/ships/adder';
import { anaconda } from 'coriolis-data/ships/anaconda';
import { asp } from 'coriolis-data/ships/asp';
import { asp_scout } from 'coriolis-data/ships/asp_scout';
import { cobra_mk_iii } from 'coriolis-data/ships/cobra_mk_iii';
import { diamondback_explorer } from 'coriolis-data/ships/diamondback_explorer';
import { diamondback } from 'coriolis-data/ships/diamondback_scout';
import { eagle } from 'coriolis-data/ships/eagle';
import { federal_assault_ship } from 'coriolis-data/ships/federal_assault_ship';
import { federal_corvette } from 'coriolis-data/ships/federal_corvette';
import { federal_dropship } from 'coriolis-data/ships/federal_dropship';
import { federal_gunship } from 'coriolis-data/ships/federal_gunship';
import { fer_de_lance } from 'coriolis-data/ships/fer_de_lance';
import { hauler } from 'coriolis-data/ships/hauler';
import { imperial_clipper } from 'coriolis-data/ships/imperial_clipper';
import { imperial_courier } from 'coriolis-data/ships/imperial_courier';
import { imperial_cutter } from 'coriolis-data/ships/imperial_cutter';
import { imperial_eagle } from 'coriolis-data/ships/imperial_eagle';
import { keelback } from 'coriolis-data/ships/keelback';
import { orca } from 'coriolis-data/ships/orca';
import { python } from 'coriolis-data/ships/python';
import { sidewinder } from 'coriolis-data/ships/sidewinder';
import { type_6_transporter } from 'coriolis-data/ships/type_6_transporter';
import { type_7_transport } from 'coriolis-data/ships/type_7_transport';
import { type_9_heavy } from 'coriolis-data/ships/type_9_heavy';
import { viper } from 'coriolis-data/ships/viper';
import { viper_mk_iv } from 'coriolis-data/ships/viper_mk_iv';
import { vulture } from 'coriolis-data/ships/vulture';
export default {
adder,
anaconda,
asp,
asp_scout,
cobra_mk_iii,
diamondback_explorer,
diamondback,
eagle,
federal_assault_ship,
federal_corvette,
federal_dropship,
federal_gunship,
fer_de_lance,
hauler,
imperial_clipper,
imperial_courier,
imperial_cutter,
imperial_eagle,
keelback,
orca,
python,
sidewinder,
type_6_transporter,
type_7_transport,
type_9_heavy,
viper,
viper_mk_iv,
vulture
};

View File

@@ -53,7 +53,7 @@ class Persist extends EventEmitter {
let comparisonJson = _get(LS_KEY_COMPARISONS);
this.builds = buildJson ? buildJson : {};
this.comparisons = comparisonJson ? comparisonJson: {};
this.comparisons = comparisonJson ? comparisonJson : {};
this.buildCount = Object.keys(this.builds).length;
this.langCode = _getString(LS_KEY_LANG) || 'en';
this.insurance = _getString(LS_KEY_INSURANCE);
@@ -89,7 +89,7 @@ class Persist extends EventEmitter {
this.builds[shipId][name] = code;
_put(LS_KEY_BUILDS, this.builds);
if (newBuild) {
this.emit('builds', this.builds);
this.emit('buildSaved', shipId, name, code);
}
}
};
@@ -109,10 +109,21 @@ class Persist extends EventEmitter {
return null;
};
getBuilds() {
getBuilds(shipId) {
if(shipId && shipId.length > 0) {
return this.builds[shipId];
}
return this.builds;
}
getBuildsNamesFor(shipId) {
if (this.builds[shipId]) {
return Object.keys(this.builds[shipId]).sort();
} else {
return [];
}
}
hasBuild(shipId, name) {
return this.builds[shipId] && this.builds[shipId][name];
}
@@ -146,7 +157,7 @@ class Persist extends EventEmitter {
}
}
_put(LS_KEY_COMPARISONS, this.comparisons);
this.emit('builds', this.builds);
this.emit('buildDeleted', shipId, name);
}
};
@@ -275,7 +286,7 @@ class Persist extends EventEmitter {
* Get the saved ship discount
* @return {number} val Discount value/amount
*/
getComponentDiscount() {
getModuleDiscount() {
return this.discounts[1];
};

View File

@@ -1,6 +1,15 @@
import { term, format } from '../i18n/Language';
export default function comparisonBBCode(facets, builds, link) {
/**
* Generate a BBCode (Forum) compatible table from comparisons
* @param {Function} translate Translate language function
* @param {object} formats Number Formats
* @param {array} facets Ship Facets
* @param {object} builds Ship builds
* @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 = [];
for (i = 0; i < facets.length; i++) {
@@ -9,11 +18,11 @@ export default function comparisonBBCode(facets, builds, link) {
p = f.props;
if (p.length == 1) {
l.push('[th][B][COLOR=#FF8C0D]', term(f.title).toUpperCase(), '[/COLOR][/B][/th]');
l.push('[th][B][COLOR=#FF8C0D]', translate(f.title).toUpperCase(), '[/COLOR][/B][/th]');
colCount++;
} else {
for (j = 0; j < p.length; j++) {
l.push('[th][B][COLOR=#FF8C0D]', term(f.title).toUpperCase(), '\n', term(f.lbls[j]).toUpperCase(), '[/COLOR][/B][/th]');
l.push('[th][B][COLOR=#FF8C0D]', translate(f.title).toUpperCase(), '\n', translate(f.lbls[j]).toUpperCase(), '[/COLOR][/B][/th]');
colCount++;
}
}
@@ -23,7 +32,7 @@ export default function comparisonBBCode(facets, builds, link) {
for (i = 0; i < builds.length; i++) {
b = builds[i];
//var href = $state.href('outfit',{shipId: b.id, code: b.code, bn: b.buildName}, {absolute: true});
l.push('[tr][td]', b.name, '[/td][td]', b.buildName, '[/td]');
for (j = 0, fl = facets.length; j < fl; j++) {
@@ -31,7 +40,7 @@ export default function comparisonBBCode(facets, builds, link) {
f = facets[j];
p = f.props;
for (k = 0, pl = p.length; k < pl; k++) {
l.push('[td="align: right"]', format[f.fmt](b[p[k]]), ' [size=-2]', term(f.unit), '[/size][/td]');
l.push('[td="align: right"]', formats[f.fmt](b[p[k]]), ' [size=-2]', translate(f.unit), '[/size][/td]');
}
}
}

View File

@@ -1,37 +1,70 @@
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.closeAll = this.closeAll.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);
}
closeAll() {
this.emit('closeAll', null);
/**
* 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();
export function contextMenuHandler(cb) {
/**
* 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')){
if (!event.getModifierState('Shift')) {
event.preventDefault();
cb.call(null, event);
}

View File

@@ -2,9 +2,15 @@ import request from 'superagent';
const SHORTEN_API = 'https://www.googleapis.com/urlshortener/v1/url?key=';
/**
* Shorten a URL using Google's URL shortener API
* @param {string} url The URL to shorten
* @param {function} success Success callback
* @param {function} error Failure/Error callback
*/
export default function shortenUrl(url, success, error) {
if (window.navigator.onLine) {
request.post(SHORTEN_API + GAPI_KEY)
request.post(SHORTEN_API + window.CORIOLIS_GAPI_KEY)
.send({ longUrl: url })
.end(function(err, response) {
if (err) {
@@ -14,6 +20,6 @@ export default function shortenUrl(url, success, error) {
}
});
} else {
return error('Not Online');
error('Not Online');
}
}

View File

@@ -0,0 +1,19 @@
/**
* [slotName description]
* @param {[type]} translate [description]
* @param {[type]} slot [description]
* @return {[type]} [description]
*/
export function slotName(translate, slot) {
return slot.m ? translate(slot.m.name || slot.m.grp) : '';
}
/**
* Generates an internationalization friendly slot name comparator
* @param {function} translate Tranlation function
* @return {function} Comparator function for slot names
*/
export function nameComparator(translate) {
return (a, b) => slotName(translate, a).toLowerCase().localeCompare(slotName(translate, b).toLowerCase());
}

View File

@@ -1,3 +1,10 @@
/**
* 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) {
if (objA === objB) {
return true;
@@ -8,16 +15,16 @@ export default function shallowEqual(objA, objB) {
return false;
}
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);
let keysA = Object.keys(objA);
let keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
var bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB);
for (var i = 0; i < keysA.length; i++) {
let bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB);
for (let i = 0; i < keysA.length; i++) {
if (!bHasOwnProperty(keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
return false;
}

View File

@@ -2,10 +2,10 @@
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="images/logo/mstile-70x70.png"/>
<square150x150logo src="images/logo/mstile-150x150.png"/>
<square310x310logo src="images/logo/mstile-310x310.png"/>
<wide310x150logo src="images/logo/mstile-310x150.png"/>
<square70x70logo src="/mstile-70x70.png"/>
<square150x150logo src="/mstile-150x150.png"/>
<square310x310logo src="/mstile-310x310.png"/>
<wide310x150logo src="/mstile-310x150.png"/>
<TileColor>#000000</TileColor>
</tile>
</msapplication>

View File

@@ -3,25 +3,25 @@
"short_name": "Coriolis",
"icons": [
{
"src": "images\/logo\/72x72.png",
"src": "\/72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "images\/logo\/96x96.png",
"src": "\/96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "images\/logo\/144x144.png",
"src": "\/144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "images\/logo\/192x192.png",
"src": "\/192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html {%= o.htmlWebpackPlugin.options.appCache ? 'manifest="/' + o.htmlWebpackPlugin.options.appCache + '"' : '' %} >
<html {%= o.htmlWebpackPlugin.options.appCache ? 'manifest=/' + o.htmlWebpackPlugin.options.appCache : '' %} >
<head>
<title>Coriolis</title>
<link rel="stylesheet" href="{%= o.htmlWebpackPlugin.files.css[0] %}">
@@ -7,70 +7,38 @@
<meta name="description" content="A ship builder, outfitting and comparison tool for Elite Dangerous">
<meta name="mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<link rel="manifest" href="/images/logo/manifest.json">
<link rel="icon" sizes="152x152 192x192" type="image/png" href="/images/logo/192x192.png">
<link rel="shortcut icon" href="/images/logo/favicon.ico">
<link rel="manifest" href="/manifest.json">
<link rel="icon" sizes="152x152 192x192" type="image/png" href="/192x192.png">
<!-- Apple/iOS headers -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="Coriolis">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon-precomposed" sizes="180x180" href="/images/logo/apple-touch-icon-precomposed.png">
<link rel="apple-touch-icon" href="/images/logo/apple-touch-icon.png">
<!-- iPhone, iPod Touch, portrait -->
<link href="/images/splash/320x460.png" media="(device-width: 320px) and (device-height: 480px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)" rel="apple-touch-startup-image">
<!-- iPhone, iPod Touch, landscape -->
<link href="/images/splash/480x320.png" media="(device-width: 320px) and (device-height: 480px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)" rel="apple-touch-startup-image">
<!-- iPhone 4, 4S, portrait -->
<link href="/images/splash/640x920.png" media="(device-width: 320px) and (device-height: 480px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image">
<!-- iPhone 4, 4S, landscape -->
<link href="/images/splash/960x640.png" media="(device-width: 320px) and (device-height: 480px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image">
<!-- iPhone 5, 5S, 5C, portrait -->
<link href="/images/splash/640x1096.png" media="(device-width: 320px) and (device-height: 568px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image">
<!-- iPhone 5, 5S, 5C, landscape -->
<link href="/images/splash/1136x640.png" media="(device-width: 320px) and (device-height: 568px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image">
<!-- iPhone 6, portrait -->
<link href="/images/splash/750x1294.png" media="(device-width: 375px) and (device-height: 667px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image">
<!-- iPhone 6, landscape -->
<link href="/images/splash/1334x750.png" media="(device-width: 375px) and (device-height: 667px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image">
<!-- iPhone 6+, portrait -->
<link href="/images/splash/1242x2148.png" media="(device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image">
<!-- iPhone 6+, landscape -->
<link href="/images/splash/2208x1242.png" media="(device-width: 414px) and (device-height: 736px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image">
<!-- iPad 1, 2, Mini, portrait -->
<link href="/images/splash/768x1004.png" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)" rel="apple-touch-startup-image">
<!-- iPad 1, 2, Mini, landscape -->
<link href="/images/splash/1024x748.png" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)" rel="apple-touch-startup-image">
<!-- iPad 3, 4, Air, Air 2, Mini 2, Mini 3, portrait -->
<link href="/images/splash/1536x2008.png" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image">
<!-- iPad 3, 4, Air, Air 2, Mini 2, Mini 3, landscape -->
<link href="/images/splash/2048x1496.png" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image">
<!-- Microsoft Windows Phone/Tablet headers -->
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-TileImage" content="/images/logo/mstile-144x144.png">
<meta name="msapplication-config" content="/images/logo/browserconfig.xml">
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
<meta name="msapplication-config" content="/browserconfig.xml">
<meta name="theme-color" content="#000000">
</head>
<body style="background-color:#000;">
<section id="coriolis"></section>
<footer>
<div class="right cap">
<a href="https://github.com/cmmcleod/coriolis/releases/" target="_blank" title="Coriolis Github Project"><span translate="version"></span> {%= o.htmlWebpackPlugin.options.version %} - {%= new Date().toISOString().slice(0, 10) %}</a>
<a href="https://github.com/cmmcleod/coriolis/releases/" target="_blank" title="Coriolis Github Project">{%= o.htmlWebpackPlugin.options.version %} - {%= new Date().toISOString().slice(0, 10) %}</a>
</div>
</footer>
<script src="{%= o.htmlWebpackPlugin.files.chunks.lib.entry %}" charset="utf-8"></script>
<script src="{%= o.htmlWebpackPlugin.files.chunks.app.entry %}" charset="utf-8" crossorigin></script>
<script src="{%= o.htmlWebpackPlugin.files.chunks.lib.entry %}" charset="utf-8" crossorigin="anonymous"></script>
<script src="{%= o.htmlWebpackPlugin.files.chunks.app.entry %}" charset="utf-8" crossorigin="anonymous"></script>
{% if (o.htmlWebpackPlugin.options.uaTracking) { %}
<script>
(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');
var GAPI_KEY = '{%= o.htmlWebpackPlugin.options.gapiKey %}';
window.CORIOLIS_GAPI_KEY = '{%= o.htmlWebpackPlugin.options.gapiKey %}';
</script>
{% } %}
</body>

View File

@@ -48,11 +48,12 @@ div, a, li {
.page {
margin: 0;
padding: 0.5em 0.5em;
padding: 0.5em 0;
width: 100%;
min-height: 100%;
clear: both;
text-align: center;
box-sizing: border-box;
}
.l {
@@ -100,6 +101,10 @@ div, a, li {
-webkit-overflow-scrolling: touch;
}
.ptr {
cursor: pointer;
}
h1 {
font-family: @fTitle;
color: @primary;

View File

@@ -8,11 +8,8 @@
line-height: 3em;
text-align: center;
background-color: @bg;
a, a:visited {
text-decoration: none;
color: @warning;
}
cursor: pointer;
}
header {

View File

@@ -132,26 +132,13 @@
});
}
table.total {
width: 100%;
&, td {
border-collapse: collapse;
border: 1px solid @primary-disabled;
}
.lbl {
text-transform: uppercase;
color: @primary-bg;
background-color: @primary-disabled;
}
}
.tabs {
width: 100%;
box-sizing: border-box;
margin-bottom: 1px;
&, th {
cursor: pointer;
border-collapse: collapse;
color: @primary-disabled;
background-color: @primary-bg;

View File

@@ -8,10 +8,6 @@ table {
color: @primary;
text-decoration: none;
}
[ng-click] {
cursor: pointer;
}
}
thead {
@@ -67,8 +63,17 @@ tbody tr {
td {
line-height: 1.4em;
padding: 0 0.3em;
}
&.val {
border: 1px solid @primary-disabled;
}
&.lbl {
border: 1px solid @primary-disabled;
text-transform: uppercase;
color: @primary-bg;
background-color: @primary-disabled;
}
}
}
td {

View File

@@ -1,408 +0,0 @@
<div id="outfit">
<div id="standard" class="group">
<div class="section-menu" ng-class="{selected: selectedSlot=='standard'}" context-menu="optimizeStandard()" ng-click="selectSlot($event, 'standard')">
<h1>
{{'standard' | translate}}
<svg class="icon"><use xlink:href="#equalizer"></use></svg>
</h1>
<div class="select" ng-if="selectedSlot=='standard'">
<ul>
<li class="lc" ng-click="optimizeStandard()" translate="Optimize"></li>
<li class="c" ng-click="useStandard('E')">E</li>
<li class="c" ng-click="useStandard('D')">D</li>
<li class="c" ng-click="useStandard('C')">C</li>
<li class="c" ng-click="useStandard('B')">B</li>
<li class="c" ng-click="useStandard('A')">A</li>
</ul>
<div class="select-group cap" translate="builds / roles"></div>
<ul>
<li class="lc" ng-click="optimizeCargo()" translate="Trader"></li>
<li class="lc" ng-click="optimizeExplorer()" translate="Explorer"></li>
</ul>
</div>
</div>
<div class="slot" ng-click="selectSlot($event, ship.bulkheads)" ng-class="{selected: selectedSlot==ship.bulkheads}">
<div class="details">
<div class="sz"><span>8</span></div>
<div class="l cap" translate="bh"></div>
<div class="r">{{ship.bulkheads.c.mass}} <u translate>T</u></div>
<div class="cl l">{{ship.bulkheads.c.name | translate}}</div>
</div>
<div class="select" ng-if="selectedSlot==ship.bulkheads" ng-click="select('b',ship.bulkheads,$event)">
<ul>
<li class="lc" ng-class="{active: ship.bulkheads.id=='0'}" cpid="0" translate="Lightweight Alloy"></li>
<li class="lc" ng-class="{active: ship.bulkheads.id=='1'}" cpid="1" translate="Reinforced Alloy"></li>
<li class="lc" ng-class="{active: ship.bulkheads.id=='2'}" cpid="2" translate="Military Grade Composite"></li>
<li class="lc" ng-class="{active: ship.bulkheads.id=='3'}" cpid="3" translate="Mirrored Surface Composite"></li>
<li class="lc" ng-class="{active: ship.bulkheads.id=='4'}" cpid="4" translate="Reactive Surface Composite"></li>
</ul>
</div>
</div>
<div class="slot" ng-click="selectSlot($event, pp)" ng-class="{selected: selectedSlot==pp}">
<div class="details" ng-class="{warning: pp.c.pGen < ship.powerRetracted}">
<div class="sz">{{::pp.maxClass}}</div>
<div class="l">{{pp.id}} {{'pp' | translate}}</div>
<div class="r">{{pp.c.mass}} <u translate>T</u></div>
<div class="cb"></div>
<div class="l cap">{{'efficiency' | translate}}: {{pp.c.eff}}</div>
<div class="l cap">{{'power' | translate}}: {{pp.c.pGen}} <u translate>MW</u></div>
</div>
<div component-select class="select" s="pp" warning="ppWarning" opts="availCS.standard[0]" ng-if="selectedSlot==pp" ng-click="select('c',pp,$event)"></div>
</div>
<div class="slot" ng-click="selectSlot($event, th)" ng-class="{selected: selectedSlot==th}">
<div class="details" ng-class="{'warning': th.c.maxmass < ship.ladenMass}">
<div class="sz">{{::th.maxClass}}</div>
<div class="l">{{th.id}} {{'t' | translate}}</div>
<div class="r">{{th.c.mass}} <u translate>T</u></div>
<div class="cb"></div>
<div class="l">{{'optimal mass' | translate}}: {{th.c.optmass}} <u translate>T</u></div>
<div class="l">{{'max mass' | translate}}: {{th.c.maxmass}} <u translate>T</u></div>
</div>
<div component-select class="select" s="th" mass="ship.ladenMass" opts="availCS.standard[1]" ng-if="selectedSlot==th" ng-click="select('c',th,$event)"></div>
</div>
<div class="slot" ng-click="selectSlot($event, fsd)" ng-class="{selected: selectedSlot==fsd}">
<div class="details">
<div class="sz">{{::fsd.maxClass}}</div>
<div class="l">{{fsd.id}} {{'fd' | translate}}</div>
<div class="r cr">{{fsd.c.mass}} <u translate>T</u></div>
<div class="cb"></div>
<div class="l cap">{{'optimal mass' | translate}}: {{fsd.c.optmass}} <u translate>T</u></div>
<div class="l cap">{{'max' | translate}} {{'fuel' | translate}}: {{fsd.c.maxfuel}} <u translate>T</u></div>
</div>
<div component-select class="select" s="fsd" opts="availCS.standard[2]" ng-if="selectedSlot==fsd" ng-click="select('c',fsd,$event)"></div>
</div>
<div class="slot" ng-click="selectSlot($event, ls)" ng-class="{selected: selectedSlot==ls}">
<div class="details">
<div class="sz">{{::ls.maxClass}}</div>
<div class="l">{{ls.id}} {{'ls' | translate}}</div>
<div class="r">{{ls.c.mass}} <u translate>T</u></div>
<div class="cb"></div>
<div class="l cap">{{'time' | translate}}: {{fTime(ls.c.time)}}</div>
</div>
<div component-select class="select" s="ls" opts="availCS.standard[3]" ng-if="selectedSlot==ls" ng-click="select('c',ls,$event)"></div>
</div>
<div class="slot" ng-click="selectSlot($event, pd)" ng-class="{selected: selectedSlot==pd}">
<div class="details" ng-class="{warning: pd.c.enginecapacity < ship.boostEnergy}">
<div class="sz">{{::pd.maxClass}}</div>
<div class="l">{{pd.id}} {{'pd' | translate}}</div>
<div class="r">{{pd.c.mass}} <u translate>T</u></div>
<div class="cb"></div>
<div class="l">{{'WEP' | translate}}: {{pd.c.weaponcapacity}} <u translate>MJ</u> / {{pd.c.weaponrecharge}} <u translate>MW</u></div>
<div class="l">{{'SYS' | translate}}: {{pd.c.systemcapacity}} <u translate>MJ</u> / {{pd.c.systemrecharge}} <u translate>MW</u></div>
<div class="l">{{'ENG' | translate}}: {{pd.c.enginecapacity}} <u translate>MJ</u> / {{pd.c.enginerecharge}} <u translate>MW</u></div>
</div>
<div component-select class="select" s="pd" warning="pdWarning" opts="availCS.standard[4]" ng-if="selectedSlot==pd" ng-click="select('c',pd,$event)"></div>
</div>
<div class="slot" ng-click="selectSlot($event, ss)" ng-class="{selected: selectedSlot==ss}">
<div class="details">
<div class="sz">{{::ss.maxClass}}</div>
<div class="l">{{ss.id}} {{'s' | translate}}</div>
<div class="r">{{ss.c.mass}} <u translate>T</u></div>
<div class="cb"></div>
<div class="l cap">{{'range' | translate}}: {{ss.c.range}} <u translate>km</u></div>
</div>
<div component-select class="select" s="ss" opts="availCS.standard[5]" ng-if="selectedSlot==ss" ng-click="select('c',ss,$event)"></div>
</div>
<div class="slot" ng-click="selectSlot($event, ft)" ng-class="{selected: selectedSlot==ft}">
<div class="details">
<div class="sz">{{::ft.maxClass}}</div>
<div class="l">{{ft.id}} {{'ft' | translate}}</div>
<div class="r">{{ft.c.capacity}} <u translate>T</u></div>
</div>
<div component-select class="select" s="ft" opts="availCS.standard[6]" ng-if="selectedSlot==ft" ng-click="select('c',ft,$event)"></div>
</div>
</div>
<div id="internal" class="group">
<div class="section-menu" ng-class="{selected: selectedSlot=='internal'}" context-menu="emptyInternal()" ng-click="selectSlot($event, 'internal')">
<h1>
{{'internal compartments' | translate}}
<svg class="icon"><use xlink:href="#equalizer"></use></svg>
</h1>
<div class="select" ng-if="selectedSlot=='internal'">
<ul>
<li class="lc" ng-click="emptyInternal()" translate="empty all"></li>
<li class="lc" ng-click="fillWithCargo()" translate="cargo"></li>
<li class="lc" ng-click="fillWithCells()" translate="scb"></li>
<li class="lc" ng-click="fillWithArmor()" translate="hr"></li>
</ul>
</div>
</div>
<div class="slot" ng-repeat="i in ship.internal" ng-click="selectSlot($event, i)" context-menu="select('i', i, $event, 'empty')" ng-class="{selected: selectedSlot==i}">
<div slot-internal class="details" slot="i" fuel="ship.fuelCapacity"></div>
<div class="select" ng-if="selectedSlot==i" ng-click="select('i',i,$event)">
<div component-select s="i" groups="availCS.getInts(i.maxClass, i.eligible)"></div>
</div>
</div>
</div>
<div id="hardpoints" class="group">
<div class="section-menu" ng-class="{selected: selectedSlot=='hardpoints'}" context-menu="emptyHardpoints()" ng-click="selectSlot($event, 'hardpoints')">
<h1>
{{'hardpoints' | translate}}
<svg class="icon"><use xlink:href="#equalizer"></use></svg>
</h1>
<div class="select hardpoint" ng-if="selectedSlot=='hardpoints'">
<ul>
<li class="lc" ng-click="emptyHardpoints()" translate="empty all"></li>
</ul>
<div class="select-group cap" translate="pl"></div>
<ul>
<li class="c" ng-click="useHardpoint('pl','F')"><svg class="icon lg"><use xlink:href="#mount-F"></use></svg></li>
<li class="c" ng-click="useHardpoint('pl','G')"><svg class="icon lg"><use xlink:href="#mount-G"></use></svg></li>
<li class="c" ng-click="useHardpoint('pl','T')"><svg class="icon lg"><use xlink:href="#mount-T"></use></svg></li>
</ul>
<div class="select-group cap" translate="ul"></div>
<ul>
<li class="c" ng-click="useHardpoint('ul','F')"><svg class="icon lg"><use xlink:href="#mount-F"></use></svg></li>
<li class="c" ng-click="useHardpoint('ul','G')"><svg class="icon lg"><use xlink:href="#mount-G"></use></svg></li>
<li class="c" ng-click="useHardpoint('ul','T')"><svg class="icon lg"><use xlink:href="#mount-T"></use></svg></li>
</ul>
<div class="select-group cap" translate="bl"></div>
<ul>
<li class="c" ng-click="useHardpoint('bl','F')"><svg class="icon lg"><use xlink:href="#mount-F"></use></svg></li>
<li class="c" ng-click="useHardpoint('bl','G')"><svg class="icon lg"><use xlink:href="#mount-G"></use></svg></li>
<li class="c" ng-click="useHardpoint('bl','T')"><svg class="icon lg"><use xlink:href="#mount-T"></use></svg></li>
</ul>
<div class="select-group cap" translate="mc"></div>
<ul>
<li class="c" ng-click="useHardpoint('mc','F')"><svg class="icon lg"><use xlink:href="#mount-F"></use></svg></li>
<li class="c" ng-click="useHardpoint('mc','G')"><svg class="icon lg"><use xlink:href="#mount-G"></use></svg></li>
<li class="c" ng-click="useHardpoint('mc','T')"><svg class="icon lg"><use xlink:href="#mount-T"></use></svg></li>
</ul>
<div class="select-group cap" translate="c"></div>
<ul>
<li class="c" ng-click="useHardpoint('c','F')"><svg class="icon lg"><use xlink:href="#mount-F"></use></svg></li>
<li class="c" ng-click="useHardpoint('c','G')"><svg class="icon lg"><use xlink:href="#mount-G"></use></svg></li>
<li class="c" ng-click="useHardpoint('c','T')"><svg class="icon lg"><use xlink:href="#mount-T"></use></svg></li>
</ul>
</div>
</div>
<div class="slot" ng-repeat="h in ship.hardpoints | filter:{maxClass: '!0'}" ng-click="selectSlot($event, h)" context-menu="select('h', h, $event, 'empty')" ng-class="{selected: selectedSlot==h}">
<div slot-hardpoint class="details" hp="h" size="HPC[h.maxClass]"></div>
<div class="select" ng-class="{hardpoint: h.maxClass > 0}" ng-if="selectedSlot==h" ng-click="select('h',h,$event)">
<div component-select s="h" groups="availCS.getHps(h.maxClass)"></div>
</div>
</div>
</div>
<div id="utility" class="group">
<div class="section-menu" ng-class="{selected: selectedSlot=='utility'}" context-menu="emptyUtility()" ng-click="selectSlot($event, 'utility')">
<h1>
{{'utility mounts' | translate}}
<svg class="icon"><use xlink:href="#equalizer"></use></svg>
</h1>
<div class="select" ng-if="selectedSlot=='utility'">
<ul>
<li class="lc" ng-click="emptyUtility()" translate="empty all"></li>
</ul>
<div class="select-group cap" translate="sb"></div>
<ul>
<li class="c" ng-click="useUtility('sb','E')">E</li>
<li class="c" ng-click="useUtility('sb','D')">D</li>
<li class="c" ng-click="useUtility('sb','C')">C</li>
<li class="c" ng-click="useUtility('sb','B')">B</li>
<li class="c" ng-click="useUtility('sb','A')">A</li>
</ul>
</div>
</div>
<div class="slot" ng-repeat="h in ship.hardpoints | filter:{maxClass: '0'}" ng-click="selectSlot($event, h)" context-menu="select('h', h, $event, 'empty')" ng-class="{selected: selectedSlot==h}">
<div slot-hardpoint class="details" hp="h" size="HPC[h.maxClass]"></div>
<div class="select" ng-class="{hardpoint: h.maxClass > 0}" ng-if="selectedSlot==h" ng-click="select('h',h,$event)">
<div component-select s="h" groups="availCS.getHps(h.maxClass)"></div>
</div>
</div>
</div>
<div class="group half" id="componentPriority">
<table style="width:100%">
<thead>
<tr class="main">
<th colspan="2" class="sortable le" ng-click="sortPwr(cName)" translate="COMPONENT"></th>
<th style="width:3em;" class="sortable" ng-click="sortPwr('type')" translate="TYPE"></th>
<th style="width:4em;" class="sortable" ng-click="sortPwr('priority')" translate="PRI"></th>
<th colspan="2" class="sortable" ng-click="sortPwr('c.power')" translate="PWR"></th>
<th style="width:3em;" class="sortable" ng-click="sortPwr(statusRetracted)" translate="ret"></th>
<th style="width:3em;" class="sortable" ng-click="sortPwr(statusDeployed)" translate="dep"></th>
</tr>
</thead>
<tbody>
<tr>
<td>{{pp.c.class}}{{pp.c.rating}}</td>
<td class="le shorten cap" translate="pp"></td>
<td><u translate="SYS"></u></td>
<td>1</td>
<td class="ri">{{fPwr(pp.c.pGen)}}</td>
<td class="ri"><u>100%</u></td>
<td></td>
<td></td>
</tr>
<tr><td style="line-height:0;" colspan="8"><hr style="margin: 0 0 3px;background: #ff8c0d;border: 0;height: 1px;" /></td></tr>
<tr class="highlight" ng-repeat="c in powerList | orderBy:pwrPredicate:pwrDesc" ng-if="c.c.power" ng-class="{disabled:!c.enabled}">
<td style="width:1em;" ng-click="togglePwr(c)">{{c.c.class}}{{c.c.rating}}</td>
<td class="le shorten cap" ng-click="togglePwr(c)" ng-bind="cName(c)"></td>
<td ng-click="togglePwr(c)"><u ng-bind="c.type | translate"></u></td>
<td><span ng-click="decPriority(c)" class="flip">&#9658;</span> {{c.priority + 1}} <span ng-click="incPriority(c)">&#9658;</span></td>
<td class="ri" style="width:3.25em;" ng-click="togglePwr(c)">{{fPwr(c.c.power)}}</td>
<td class="ri" style="width:3em;" ng-click="togglePwr(c)"><u>{{f1Pct(c.c.power/ship.powerAvailable)}}</u></td>
<td ng-if="!c.enabled" class="disabled upp" colspan="2" translate="disabled" ng-click="togglePwr(c)"></td>
<td class="upp" ng-if="c.enabled" ng-click="togglePwr(c)">
<svg class="icon secondary-disabled" ng-if="statusRetracted(c) == 3"><use xlink:href="#power"><title class="cap">{{'on' | translate}}</title></use></svg>
<svg class="icon warning" ng-if="statusRetracted(c) == 2"><use xlink:href="#no-power"><title class="cap">{{'off' | translate}}</title></use></svg>
<span class="disabled" translate="disabled" ng-if="statusRetracted(c) == 1"></span>
</td>
<td class="upp" ng-if="c.enabled" ng-click="togglePwr(c)">
<svg class="icon secondary-disabled" ng-if="statusDeployed(c) == 3"><use xlink:href="#power"><title class="cap">{{'on' | translate}}</title></use></svg>
<svg class="icon warning" ng-if="statusDeployed(c) == 2"><use xlink:href="#no-power"><title class="cap">{{'off' | translate}}</title></use></svg>
<span class="disabled" translate="disabled" ng-if="statusDeployed(c) == 1"></span>
</td>
</tr>
</tbody>
</table>
<div style="margin-top: 1em" power-bands bands="priorityBands" available="ship.powerAvailable"></div>
</div>
<div class="group half">
<table class="tabs">
<thead>
<tr>
<th style="width:34%" ng-class="{active: costTab == 'ammo'}" ng-click="updateCostTab('ammo')" translate="reload costs"></th>
<th style="width:33%" ng-class="{active: costTab == 'retrofit'}" ng-click="updateCostTab('retrofit')" translate="retrofit costs"></th>
<th style="width:33%" ng-class="{active: costTab == 'costs'}" ng-click="updateCostTab('costs')" translate="costs"></th>
</tr>
</thead>
</table>
<div ng-if="costTab == 'costs'">
<table style="width:100%">
<thead>
<tr class="main">
<th colspan="2" class="sortable le" ng-click="sortCost(cName)">
{{'component' | translate}}
<u class="optional-hide" ng-if="discounts.ship < 1">[{{'ship' | translate }} -{{fRPct(1 - discounts.ship)}}]</u>
<u class="optional-hide" ng-if="discounts.components < 1">[{{'components' | translate}} -{{fRPct(1 - discounts.components)}}]</u>
</th>
<th class="sortable le" ng-click="sortCost('discountedCost')" translate="credits"></th>
</tr>
</thead>
<tbody>
<tr class="highlight" ng-repeat="item in costList | orderBy:costPredicate:costDesc" ng-if="item.c.cost > 0" ng-class="{disabled:!item.incCost}">
<td class="toggleable" style="width:1em;" ng-click="toggleCost(item)">{{item.c.class}}{{item.c.rating}}</td>
<td class="le toggleable shorten cap" ng-click="toggleCost(item)">{{cName(item)}}</td>
<td class="ri toggleable" ng-click="toggleCost(item)">{{fCrd(item.discountedCost)}} <u translate>CR</u></td>
</tr>
</tbody>
</table>
<table class="total">
<tr class="ri">
<td class="lbl" translate="total"></td>
<td>{{fCrd(ship.totalCost)}} <u translate>CR</u></td>
</tr>
<tr class="ri">
<td class="lbl" translate="insurance"></td>
<td>{{fCrd(ship.totalCost * insurance.current.pct)}} <u translate>CR</u></td>
</tr>
</table>
</div>
<div ng-if="costTab == 'retrofit'">
<div class="scroll-x">
<table style="width:100%">
<thead>
<tr class="main">
<th colspan="2" class="sortable le" ng-click="sortRetrofit('sellName | translate')" translate="sell"></th>
<th colspan="2" class="sortable le" ng-click="sortRetrofit('buyName | translate')" translate="buy"></th>
<th class="sortable le" ng-click="sortRetrofit('netCost')">
{{'net cost' | translate}} <u class="optional-hide" ng-if="discounts.components < 1">[-{{fRPct(1 - discounts.components)}}]</u>
</th>
</tr>
</thead>
<tbody>
<tr ng-if="!retrofitList || retrofitList.length == 0">
<td colspan="5" style="padding: 3em 0;" translate="PHRASE_NO_RETROCH"></td>
</tr>
<tr class="highlight" ng-repeat="item in retrofitList | orderBy:retroPredicate:retroDesc" ng-click="toggleRetrofitCost(item.retroItem)" ng-class="{disabled: !item.retroItem.incCost}">
<td style="width:1em;">{{item.sellClassRating}}</td>
<td class="le shorten cap">{{item.sellName | translate}}</td>
<td style="width:1em;">{{item.buyClassRating}}</td>
<td class="le shorten cap">{{item.buyName | translate}}</td>
<td class="ri" ng-class="item.retroItem.incCost ? item.netCost > 0 ? 'warning' : 'secondary-disabled' : 'disabled'">{{ fCrd(item.netCost)}} <u translate>CR</u></td>
</tr>
</tbody>
</table>
</div>
<table class="total">
<tr class="ri">
<td class="lbl" translate="cost"></td>
<td colspan="2" ng-class="retrofitTotal > 0 ? 'warning' : 'secondary-disabled'">{{fCrd(retrofitTotal)}} <u translate>CR</u></td>
</tr>
<tr class="ri">
<td class="lbl cap" translate="retrofit from"></td>
<td class="cen" style="border-right:none;width: 1em;"><u class="primary-disabled">&#9662;</u></td>
<td style="border-left:none;padding:0;">
<select style="width: 100%;padding: 0" ng-model="$parent.retrofitBuild" ng-change="setRetrofitBase()" ng-options="name as name for (name, build) in allBuilds[ship.id]">
<option value="">{{'Stock' | translate}}</option>
</select>
</td>
</tr>
</table>
</div>
<div ng-if="costTab == 'ammo'">
<div class="scroll-x">
<table style="width:100%">
<thead>
<tr class="main">
<th colspan="2" class="sortable le" ng-click="sortAmmo('ammoName | translate')" translate="component"></th>
<th colspan="1" class="sortable le" ng-click="sortAmmo('ammoMax')" translate="quantity"></th>
<th colspan="1" class="sortable le" ng-click="sortAmmo('ammoUnitCost')" translate="unit cost"></th>
<th class="sortable le" ng-click="sortAmmo('ammoTotalCost')">
{{'total cost' | translate}} <u class="optional-hide" ng-if="discounts.ammo < 1">-[{{fRPct(1 - discounts.ammo)}}]</u>
</th>
</tr>
</thead>
<tbody>
<tr class="highlight" ng-repeat="item in ammoList | orderBy:ammoPredicate:ammoDesc">
<td style="width:1em;">{{item.ammoClassRating}}</td>
<td class="le shorten cap">{{item.ammoName | translate}}</td>
<td class="ri">{{fCrd(item.ammoMax)}}</td>
<td class="ri">{{fCrd(item.ammoUnitCost)}} <u translate>CR</u></td>
<td class="ri">{{fCrd(item.ammoTotalCost)}} <u translate>CR</u></td>
</tr>
</tbody>
</table>
</div>
<table class="total">
<tr class="ri">
<td class="lbl" translate="total"></td>
<td>{{fCrd(ammoTotal)}} <u translate>CR</u></td>
</tr>
</table>
</div>
</div>
<div class="group third">
<h1 translate="jump range"></h1>
<div line-chart config="jrChart" series="jrSeries"></div>
</div>
<div class="group third">
<h1 translate="total range"></h1>
<div line-chart config="trChart" series="trSeries"></div>
</div>
<div class="group third">
<h1 translate="speed"></h1>
<div line-chart config="speedChart" series="speedSeries"></div>
</div>
<div class="group half">
<div slider max="ship.fuelCapacity" unit="'T'" on-change="::fuelChange(val)" style="position:relative; margin: 0 auto;">
<svg class="icon xl primary-disabled" style="position:absolute;height: 100%;"><use xlink:href="#fuel"></use></svg>
</div>
</div>
</div>

View File

@@ -6,16 +6,16 @@ var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
devtool: 'eval',
devServer: {
headers: { "Access-Control-Allow-Origin": "*" }
},
entry: {
app: [ 'webpack-dev-server/client?http://localhost:3300', 'webpack/hot/only-dev-server', path.join(__dirname, "src/app/index.js") ],
lib: ['d3', 'react', 'react-dom', 'classnames', 'fbemitter', 'lz-string']
},
resolve: {
// When requiring, you don't need to add these extensions
extensions: ['', '.js', '.jsx', '.json', '.less'],
alias: {
'coriolis-data': path.resolve(__dirname, 'node_modules/coriolis-data')
}
extensions: ['', '.js', '.jsx', '.json', '.less']
},
output: {
path: path.join(__dirname, 'build'),
@@ -27,7 +27,6 @@ module.exports = {
new HtmlWebpackPlugin({
inject: false,
template: path.join(__dirname, "src/index.html"),
cdn: '',
version: pkgJson.version
}),
new ExtractTextPlugin('app.css', {
@@ -45,9 +44,7 @@ module.exports = {
{ test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/font-woff' },
{ test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/font-woff' },
{ test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/octet-stream' },
{ test: /\.(png|jpg|jpeg|gif|ico)$/, loader: 'file-loader?name=/images/[name].[ext]' },
{ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file' },
{ test: /\.xml$/, loader: 'file' },
{ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=image/svg+xml' }
]
}

View File

@@ -23,14 +23,12 @@ CopyDirPlugin.prototype.apply = function(compiler) {
}.bind(this));
};
module.exports = {
entry: {
app: path.resolve(__dirname, 'src/app/index'),
lib: ['d3', 'react', 'react-dom', 'classnames', 'fbemitter', 'lz-string']
},
resolve: {
// When requiring, you don't need to add these extensions
extensions: ['', '.js', '.jsx', '.json', '.less'],
alias: {
'd3': d3Path,
@@ -42,7 +40,8 @@ module.exports = {
output: {
path: path.join(__dirname, 'build'),
filename: '[name].[hash:6].js',
publicPath: '//cdn.coriolis.io/'
chunkFilename: '[name].[hash:6]',
publicPath: process.env.CDN || '/'
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
@@ -54,7 +53,7 @@ module.exports = {
new webpack.optimize.CommonsChunkPlugin('lib', 'lib.[hash:6].js'),
new HtmlWebpackPlugin({
inject: false,
appCache: 'coriolis.appcache'
appCache: 'coriolis.appcache',
minify: {
collapseBooleanAttributes: true,
collapseWhitespace: true,
@@ -74,6 +73,7 @@ module.exports = {
allChunks: true
}),
new CopyDirPlugin(path.join(__dirname, 'src/schemas'), 'schemas'),
new CopyDirPlugin(path.join(__dirname, 'src/images/logo/*'), ''),
new AppCachePlugin({
network: ['*'],
settings: ['prefer-online'],
@@ -84,6 +84,11 @@ module.exports = {
module: {
noParse: [d3Path, reactPath, lzStringPath],
loaders: [
// Expose non-parsed globally scoped libs
{ test: reactPath, loader: "expose?React" },
{ test: d3Path, loader: "expose?d3" },
{ test: lzStringPath, loader: "expose?LZString" },
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader','css-loader') },
{ test: /\.less$/, loader: ExtractTextPlugin.extract('style-loader','css-loader!less-loader') },
{ test: /\.(js|jsx)$/, loaders: [ 'babel' ], include: path.join(__dirname, 'src') },
@@ -92,9 +97,7 @@ module.exports = {
{ test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/font-woff' },
{ test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/octet-stream' },
{ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file' },
{ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=image/svg+xml' },
{ test: /\.xml$/, loader: 'file' },
{ test: /\.(png|jpg|jpeg|gif|ico)$/, loader: 'file-loader?name=/images/[name].[hash:6].[ext]' }
{ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=image/svg+xml' }
]
}
};